|
|
|
|
@@ -1,40 +1,34 @@
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
|
|
|
import type { EntityIdentifier } from '@MailManager/types/common'
|
|
|
|
|
import type { EntityInterface } from '@MailManager/types/entity'
|
|
|
|
|
import type { MessageInterface } from '@MailManager/types/message'
|
|
|
|
|
import type { EntityObject } from '@MailManager/models'
|
|
|
|
|
import type { CollectionObject } from '@MailManager/models/collection'
|
|
|
|
|
|
|
|
|
|
// Props
|
|
|
|
|
interface Props {
|
|
|
|
|
messages: EntityInterface<MessageInterface>[]
|
|
|
|
|
selectedMessage?: EntityInterface<MessageInterface> | null
|
|
|
|
|
selectedMessageIds?: EntityIdentifier[]
|
|
|
|
|
selectionModeActive?: boolean
|
|
|
|
|
selectionCount?: number
|
|
|
|
|
hasSelection?: boolean
|
|
|
|
|
allCurrentMessagesSelected?: boolean
|
|
|
|
|
messages: EntityObject[]
|
|
|
|
|
selectedMessage?: EntityObject | null
|
|
|
|
|
selectionList?: EntityIdentifier[]
|
|
|
|
|
selectionMode?: boolean
|
|
|
|
|
selectedCollection?: CollectionObject | null
|
|
|
|
|
loading?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
loading: false,
|
|
|
|
|
selectedMessageIds: () => [],
|
|
|
|
|
selectionModeActive: false,
|
|
|
|
|
selectionCount: 0,
|
|
|
|
|
hasSelection: false,
|
|
|
|
|
allCurrentMessagesSelected: false,
|
|
|
|
|
selectionList: () => [],
|
|
|
|
|
selectionMode: false,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Emits
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
open: [message: EntityInterface<MessageInterface>]
|
|
|
|
|
toggleSelection: [message: EntityInterface<MessageInterface>]
|
|
|
|
|
activateSelectionMode: [message: EntityInterface<MessageInterface>]
|
|
|
|
|
open: [message: EntityObject]
|
|
|
|
|
toggleSelection: [message: EntityObject]
|
|
|
|
|
activateSelectionMode: [message: EntityObject]
|
|
|
|
|
toggleSelectAll: [value: boolean]
|
|
|
|
|
clearSelection: []
|
|
|
|
|
moveSelection: []
|
|
|
|
|
deleteSelection: []
|
|
|
|
|
}>()
|
|
|
|
|
|
|
|
|
|
const longPressTimer = ref<number | null>(null)
|
|
|
|
|
@@ -42,19 +36,19 @@ const longPressActivated = ref(false)
|
|
|
|
|
const suppressNextClick = ref(false)
|
|
|
|
|
const LONG_PRESS_MS = 450
|
|
|
|
|
|
|
|
|
|
const selectedIdSet = computed(() => new Set(props.selectedMessageIds))
|
|
|
|
|
const selectedIdSet = computed(() => new Set(props.selectionList))
|
|
|
|
|
|
|
|
|
|
const isOpened = (message: EntityInterface<MessageInterface>): boolean => {
|
|
|
|
|
const isOpened = (message: EntityObject): boolean => {
|
|
|
|
|
if (!props.selectedMessage) return false
|
|
|
|
|
return (
|
|
|
|
|
message.provider === selectedMessage.value.provider &&
|
|
|
|
|
message.service === selectedMessage.value.service &&
|
|
|
|
|
message.collection === selectedMessage.value.collection &&
|
|
|
|
|
message.identifier === selectedMessage.value.identifier
|
|
|
|
|
message.provider === props.selectedMessage.provider &&
|
|
|
|
|
message.service === props.selectedMessage.service &&
|
|
|
|
|
message.collection === props.selectedMessage.collection &&
|
|
|
|
|
message.identifier === props.selectedMessage.identifier
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isSelected = (message: EntityInterface<MessageInterface>): boolean => {
|
|
|
|
|
const isSelected = (message: EntityObject): boolean => {
|
|
|
|
|
return selectedIdSet.value.has(
|
|
|
|
|
`${message.provider}:${String(message.service)}:${String(message.collection)}:${String(message.identifier)}` as EntityIdentifier,
|
|
|
|
|
)
|
|
|
|
|
@@ -70,6 +64,16 @@ const isFlagged = (message: EntityObject): boolean => {
|
|
|
|
|
return message.properties.flags?.flagged || false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentMessages = computed(() => props.messages ?? [])
|
|
|
|
|
|
|
|
|
|
const selectionCount = computed(() => props.selectionList.length)
|
|
|
|
|
|
|
|
|
|
const hasSelection = computed(() => selectionCount.value > 0)
|
|
|
|
|
|
|
|
|
|
const allCurrentMessagesSelected = computed(() => {
|
|
|
|
|
return currentMessages.value.length > 0 && currentMessages.value.every(message => isSelected(message))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Format date for display
|
|
|
|
|
const formatDate = (date: Date | string | null | undefined): string => {
|
|
|
|
|
if (!date) return ''
|
|
|
|
|
@@ -101,6 +105,7 @@ const formatDate = (date: Date | string | null | undefined): string => {
|
|
|
|
|
day: 'numeric'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Other years - show full date
|
|
|
|
|
return messageDate.toLocaleDateString('en-US', {
|
|
|
|
|
@@ -116,8 +121,18 @@ const truncate = (text: string | null | undefined, length: number = 100): string
|
|
|
|
|
return text.length > length ? text.substring(0, length) + '...' : text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle message click
|
|
|
|
|
const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
const handleSelectionToggle = (message: EntityObject) => {
|
|
|
|
|
emit('toggleSelection', message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMessageMouseClick = (event: MouseEvent | KeyboardEvent, message: EntityObject) => {
|
|
|
|
|
if (event.shiftKey && !props.selectionMode) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
emit('activateSelectionMode', message)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (longPressActivated.value) {
|
|
|
|
|
longPressActivated.value = false
|
|
|
|
|
return
|
|
|
|
|
@@ -128,7 +143,7 @@ const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (props.selectionModeActive) {
|
|
|
|
|
if (props.selectionMode) {
|
|
|
|
|
emit('toggleSelection', message)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
@@ -136,23 +151,8 @@ const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
emit('open', message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSelectionToggle = (message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
emit('toggleSelection', message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMessageMouseClick = (event: MouseEvent | KeyboardEvent, message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
if (event.shiftKey && !props.selectionModeActive) {
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
event.stopPropagation()
|
|
|
|
|
emit('activateSelectionMode', message)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleMessageClick(message)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleMessageMouseDown = (event: MouseEvent, message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
if (!event.shiftKey || props.selectionModeActive) {
|
|
|
|
|
const handleMessageMouseDown = (event: MouseEvent, message: EntityObject) => {
|
|
|
|
|
if (!event.shiftKey || props.selectionMode) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -169,12 +169,12 @@ const clearLongPressTimer = () => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleTouchStart = (message: EntityInterface<MessageInterface>) => {
|
|
|
|
|
const handleTouchStart = (message: EntityObject) => {
|
|
|
|
|
clearLongPressTimer()
|
|
|
|
|
longPressActivated.value = false
|
|
|
|
|
|
|
|
|
|
longPressTimer.value = window.setTimeout(() => {
|
|
|
|
|
if (!props.selectionModeActive) {
|
|
|
|
|
if (!props.selectionMode) {
|
|
|
|
|
emit('activateSelectionMode', message)
|
|
|
|
|
} else {
|
|
|
|
|
emit('toggleSelection', message)
|
|
|
|
|
@@ -216,12 +216,12 @@ const unreadCount = computed(() => {
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const totalCount = computed(() => {
|
|
|
|
|
return selectedFolder.value?.properties.total ?? 0
|
|
|
|
|
return props.selectedCollection?.properties.total ?? 0
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// True only when the collection explicitly provides total/unread counts
|
|
|
|
|
const hasCountData = computed(() => {
|
|
|
|
|
return selectedFolder.value?.properties.total != null
|
|
|
|
|
return props.selectedCollection?.properties.total != null
|
|
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
@@ -243,7 +243,7 @@ const hasCountData = computed(() => {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="selectionModeActive && messages.length > 0" class="selection-summary">
|
|
|
|
|
<div v-if="selectionMode && messages.length > 0" class="selection-summary">
|
|
|
|
|
<div class="selection-controls">
|
|
|
|
|
<v-checkbox-btn
|
|
|
|
|
:model-value="allCurrentMessagesSelected"
|
|
|
|
|
@@ -267,6 +267,15 @@ const hasCountData = computed(() => {
|
|
|
|
|
>
|
|
|
|
|
Move
|
|
|
|
|
</v-btn>
|
|
|
|
|
<v-btn
|
|
|
|
|
size="small"
|
|
|
|
|
variant="text"
|
|
|
|
|
prepend-icon="mdi-delete-outline"
|
|
|
|
|
:disabled="!hasSelection"
|
|
|
|
|
@click="emit('deleteSelection')"
|
|
|
|
|
>
|
|
|
|
|
Delete
|
|
|
|
|
</v-btn>
|
|
|
|
|
<v-btn
|
|
|
|
|
size="small"
|
|
|
|
|
variant="text"
|
|
|
|
|
@@ -307,12 +316,12 @@ const hasCountData = computed(() => {
|
|
|
|
|
>
|
|
|
|
|
<template v-slot:default="{ item: message }">
|
|
|
|
|
<v-list-item
|
|
|
|
|
:key="`${message.provider}-${message.service}-${message.collection}-${message.identifier}`"
|
|
|
|
|
:key="`${message.provider}:${message.service}:${message.collection}:${message.identifier}`"
|
|
|
|
|
class="message-item"
|
|
|
|
|
:class="{
|
|
|
|
|
'opened': isOpened(message),
|
|
|
|
|
'selected': isSelected(message),
|
|
|
|
|
'selection-mode': selectionModeActive,
|
|
|
|
|
'selection-mode': selectionMode,
|
|
|
|
|
'unread': isUnread(message)
|
|
|
|
|
}"
|
|
|
|
|
@mousedown="handleMessageMouseDown($event, message)"
|
|
|
|
|
@@ -326,7 +335,7 @@ const hasCountData = computed(() => {
|
|
|
|
|
<template v-slot:prepend>
|
|
|
|
|
<div class="message-item-prepend">
|
|
|
|
|
<v-checkbox-btn
|
|
|
|
|
v-if="selectionModeActive || isSelected(message)"
|
|
|
|
|
v-if="selectionMode || isSelected(message)"
|
|
|
|
|
:model-value="isSelected(message)"
|
|
|
|
|
density="compact"
|
|
|
|
|
hide-details
|
|
|
|
|
|