Compare commits
3 Commits
749d92285f
...
11de31397d
| Author | SHA1 | Date | |
|---|---|---|---|
| 11de31397d | |||
| 4ff377c6ac | |||
| 69f3c430cc |
@@ -50,7 +50,7 @@ interface ServiceGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serviceGroups = computed<ServiceGroup[]>(() => {
|
const serviceGroups = computed<ServiceGroup[]>(() => {
|
||||||
const moveCandidate = mailStore.moveMessageCandidate
|
const moveCandidate = mailStore.moveMessageCandidates[0]
|
||||||
|
|
||||||
if (!moveCandidate) {
|
if (!moveCandidate) {
|
||||||
return []
|
return []
|
||||||
@@ -111,7 +111,7 @@ const canConfirm = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.modelValue, mailStore.moveMessageCandidate],
|
() => [props.modelValue, mailStore.moveMessageCandidates],
|
||||||
([isOpen]) => {
|
([isOpen]) => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||||
|
import type { EntityIdentifier } from '@MailManager/types/common'
|
||||||
import type { EntityInterface } from '@MailManager/types/entity'
|
import type { EntityInterface } from '@MailManager/types/entity'
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
import type { MessageInterface } from '@MailManager/types/message'
|
||||||
import type { CollectionObject } from '@MailManager/models/collection'
|
import type { CollectionObject } from '@MailManager/models/collection'
|
||||||
@@ -8,37 +9,64 @@ import type { CollectionObject } from '@MailManager/models/collection'
|
|||||||
interface Props {
|
interface Props {
|
||||||
messages: EntityInterface<MessageInterface>[]
|
messages: EntityInterface<MessageInterface>[]
|
||||||
selectedMessage?: EntityInterface<MessageInterface> | null
|
selectedMessage?: EntityInterface<MessageInterface> | null
|
||||||
|
selectedMessageIds?: EntityIdentifier[]
|
||||||
|
selectionModeActive?: boolean
|
||||||
|
selectionCount?: number
|
||||||
|
hasSelection?: boolean
|
||||||
|
allCurrentMessagesSelected?: boolean
|
||||||
selectedCollection?: CollectionObject | null
|
selectedCollection?: CollectionObject | null
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
loading: false
|
loading: false,
|
||||||
|
selectedMessageIds: () => [],
|
||||||
|
selectionModeActive: false,
|
||||||
|
selectionCount: 0,
|
||||||
|
hasSelection: false,
|
||||||
|
allCurrentMessagesSelected: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
select: [message: EntityInterface<MessageInterface>]
|
open: [message: EntityInterface<MessageInterface>]
|
||||||
|
toggleSelection: [message: EntityInterface<MessageInterface>]
|
||||||
|
activateSelectionMode: [message: EntityInterface<MessageInterface>]
|
||||||
|
toggleSelectAll: [value: boolean]
|
||||||
|
clearSelection: []
|
||||||
|
moveSelection: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Check if message is selected
|
const longPressTimer = ref<number | null>(null)
|
||||||
const isSelected = (message: EntityInterface<MessageInterface>): boolean => {
|
const longPressActivated = ref(false)
|
||||||
|
const suppressNextClick = ref(false)
|
||||||
|
const LONG_PRESS_MS = 450
|
||||||
|
|
||||||
|
const selectedIdSet = computed(() => new Set(props.selectedMessageIds))
|
||||||
|
|
||||||
|
const isOpened = (message: EntityInterface<MessageInterface>): boolean => {
|
||||||
if (!props.selectedMessage) return false
|
if (!props.selectedMessage) return false
|
||||||
return (
|
return (
|
||||||
message.provider === props.selectedMessage.provider &&
|
message.provider === selectedMessage.value.provider &&
|
||||||
message.service === props.selectedMessage.service &&
|
message.service === selectedMessage.value.service &&
|
||||||
message.collection === props.selectedMessage.collection &&
|
message.collection === selectedMessage.value.collection &&
|
||||||
message.identifier === props.selectedMessage.identifier
|
message.identifier === selectedMessage.value.identifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSelected = (message: EntityInterface<MessageInterface>): boolean => {
|
||||||
|
return selectedIdSet.value.has(
|
||||||
|
`${message.provider}:${String(message.service)}:${String(message.collection)}:${String(message.identifier)}` as EntityIdentifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if message is unread
|
// Check if message is unread
|
||||||
const isUnread = (message: EntityInterface<MessageInterface>): boolean => {
|
const isUnread = (message: EntityObject): boolean => {
|
||||||
return !message.properties.flags?.read
|
return !message.properties.flags?.read
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if message is flagged
|
// Check if message is flagged
|
||||||
const isFlagged = (message: EntityInterface<MessageInterface>): boolean => {
|
const isFlagged = (message: EntityObject): boolean => {
|
||||||
return message.properties.flags?.flagged || false
|
return message.properties.flags?.flagged || false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,12 +118,92 @@ const truncate = (text: string | null | undefined, length: number = 100): string
|
|||||||
|
|
||||||
// Handle message click
|
// Handle message click
|
||||||
const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
const handleMessageClick = (message: EntityInterface<MessageInterface>) => {
|
||||||
emit('select', message)
|
if (longPressActivated.value) {
|
||||||
|
longPressActivated.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suppressNextClick.value) {
|
||||||
|
suppressNextClick.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.selectionModeActive) {
|
||||||
|
emit('toggleSelection', message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
suppressNextClick.value = true
|
||||||
|
emit('activateSelectionMode', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearLongPressTimer = () => {
|
||||||
|
if (longPressTimer.value !== null) {
|
||||||
|
window.clearTimeout(longPressTimer.value)
|
||||||
|
longPressTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTouchStart = (message: EntityInterface<MessageInterface>) => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
longPressActivated.value = false
|
||||||
|
|
||||||
|
longPressTimer.value = window.setTimeout(() => {
|
||||||
|
if (!props.selectionModeActive) {
|
||||||
|
emit('activateSelectionMode', message)
|
||||||
|
} else {
|
||||||
|
emit('toggleSelection', message)
|
||||||
|
}
|
||||||
|
|
||||||
|
longPressActivated.value = true
|
||||||
|
clearLongPressTimer()
|
||||||
|
}, LONG_PRESS_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTouchMove = () => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearLongPressTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSelectAllToggle = (value: boolean | null) => {
|
||||||
|
emit('toggleSelectAll', value === true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorted messages (newest first)
|
// Sorted messages (newest first)
|
||||||
const sortedMessages = computed(() => {
|
const sortedMessages = computed(() => {
|
||||||
return [...props.messages].sort((a, b) => {
|
return [...currentMessages.value].sort((a, b) => {
|
||||||
const dateA = a.properties.date ? new Date(a.properties.date).getTime() : 0
|
const dateA = a.properties.date ? new Date(a.properties.date).getTime() : 0
|
||||||
const dateB = b.properties.date ? new Date(b.properties.date).getTime() : 0
|
const dateB = b.properties.date ? new Date(b.properties.date).getTime() : 0
|
||||||
return dateB - dateA
|
return dateB - dateA
|
||||||
@@ -107,19 +215,13 @@ const unreadCount = computed(() => {
|
|||||||
return props.selectedCollection?.properties.unread ?? 0
|
return props.selectedCollection?.properties.unread ?? 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const readCount = computed(() => {
|
|
||||||
const total = props.selectedCollection?.properties.total ?? 0
|
|
||||||
const unread = props.selectedCollection?.properties.unread ?? 0
|
|
||||||
return total - unread
|
|
||||||
})
|
|
||||||
|
|
||||||
const totalCount = computed(() => {
|
const totalCount = computed(() => {
|
||||||
return props.selectedCollection?.properties.total ?? 0
|
return selectedFolder.value?.properties.total ?? 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// True only when the collection explicitly provides total/unread counts
|
// True only when the collection explicitly provides total/unread counts
|
||||||
const hasCountData = computed(() => {
|
const hasCountData = computed(() => {
|
||||||
return props.selectedCollection?.properties.total != null
|
return selectedFolder.value?.properties.total != null
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -127,16 +229,53 @@ const hasCountData = computed(() => {
|
|||||||
<div class="message-list">
|
<div class="message-list">
|
||||||
<!-- Header with folder name and counts -->
|
<!-- Header with folder name and counts -->
|
||||||
<div v-if="selectedCollection" class="message-list-header">
|
<div v-if="selectedCollection" class="message-list-header">
|
||||||
<h2 class="text-h6">{{ selectedCollection.properties.label || 'Folder' }}</h2>
|
<div class="message-list-heading">
|
||||||
<div class="folder-counts text-caption text-medium-emphasis">
|
<h2 class="text-h6">{{ selectedCollection.properties.label || 'Folder' }}</h2>
|
||||||
<span v-if="hasCountData">
|
<div class="folder-counts text-caption text-medium-emphasis">
|
||||||
<span class="unread-count">{{ unreadCount }}</span>
|
<span v-if="hasCountData">
|
||||||
<span class="mx-1">/</span>
|
<span class="unread-count">{{ unreadCount }}</span>
|
||||||
<span>{{ totalCount }}</span>
|
<span class="mx-1">/</span>
|
||||||
</span>
|
<span>{{ totalCount }}</span>
|
||||||
<span v-else-if="messages.length > 0">
|
</span>
|
||||||
{{ messages.length }} loaded
|
<span v-else-if="messages.length > 0">
|
||||||
</span>
|
{{ messages.length }} loaded
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectionModeActive && messages.length > 0" class="selection-summary">
|
||||||
|
<div class="selection-controls">
|
||||||
|
<v-checkbox-btn
|
||||||
|
:model-value="allCurrentMessagesSelected"
|
||||||
|
:indeterminate="hasSelection && !allCurrentMessagesSelected"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
@update:model-value="handleSelectAllToggle"
|
||||||
|
/>
|
||||||
|
<span class="text-caption text-medium-emphasis">
|
||||||
|
{{ selectionCount > 0 ? `${selectionCount} selected` : 'Select all loaded' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="selection-actions">
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
prepend-icon="mdi-folder-move-outline"
|
||||||
|
:disabled="!hasSelection"
|
||||||
|
@click="emit('moveSelection')"
|
||||||
|
>
|
||||||
|
Move
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
:disabled="!hasSelection"
|
||||||
|
@click="emit('clearSelection')"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -151,7 +290,7 @@ const hasCountData = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<div v-else-if="messages.length === 0" class="pa-8 text-center">
|
<div v-else-if="currentMessages.length === 0" class="pa-8 text-center">
|
||||||
<v-icon size="64" color="grey-lighten-1">mdi-email-outline</v-icon>
|
<v-icon size="64" color="grey-lighten-1">mdi-email-outline</v-icon>
|
||||||
<div class="text-h6 mt-4 text-medium-emphasis">No messages</div>
|
<div class="text-h6 mt-4 text-medium-emphasis">No messages</div>
|
||||||
<div class="text-body-2 text-medium-emphasis">
|
<div class="text-body-2 text-medium-emphasis">
|
||||||
@@ -171,18 +310,36 @@ const hasCountData = computed(() => {
|
|||||||
:key="`${message.provider}-${message.service}-${message.collection}-${message.identifier}`"
|
:key="`${message.provider}-${message.service}-${message.collection}-${message.identifier}`"
|
||||||
class="message-item"
|
class="message-item"
|
||||||
:class="{
|
:class="{
|
||||||
|
'opened': isOpened(message),
|
||||||
'selected': isSelected(message),
|
'selected': isSelected(message),
|
||||||
|
'selection-mode': selectionModeActive,
|
||||||
'unread': isUnread(message)
|
'unread': isUnread(message)
|
||||||
}"
|
}"
|
||||||
@click="handleMessageClick(message)"
|
@mousedown="handleMessageMouseDown($event, message)"
|
||||||
|
@click="handleMessageMouseClick($event, message)"
|
||||||
|
@touchstart.passive="handleTouchStart(message)"
|
||||||
|
@touchend="handleTouchEnd"
|
||||||
|
@touchcancel="handleTouchEnd"
|
||||||
|
@touchmove="handleTouchMove"
|
||||||
lines="three"
|
lines="three"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-avatar size="40" color="primary">
|
<div class="message-item-prepend">
|
||||||
<span class="text-white text-body-1">
|
<v-checkbox-btn
|
||||||
{{ (message.properties.from?.label || message.properties.from?.address || 'U')[0].toUpperCase() }}
|
v-if="selectionModeActive || isSelected(message)"
|
||||||
</span>
|
:model-value="isSelected(message)"
|
||||||
</v-avatar>
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
@click.stop
|
||||||
|
@update:model-value="handleSelectionToggle(message)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-avatar size="40" color="primary">
|
||||||
|
<span class="text-white text-body-1">
|
||||||
|
{{ (message.properties.from?.label || message.properties.from?.address || 'U')[0].toUpperCase() }}
|
||||||
|
</span>
|
||||||
|
</v-avatar>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-list-item-title class="d-flex align-center">
|
<v-list-item-title class="d-flex align-center">
|
||||||
@@ -241,14 +398,42 @@ const hasCountData = computed(() => {
|
|||||||
background-color: rgb(var(--v-theme-surface));
|
background-color: rgb(var(--v-theme-surface));
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
|
||||||
|
.message-list-heading {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selection-summary {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.folder-counts {
|
.folder-counts {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -270,19 +455,41 @@ const hasCountData = computed(() => {
|
|||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-item.selection-mode {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item-prepend {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
.message-item:hover {
|
.message-item:hover {
|
||||||
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
background-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-item.selected {
|
.message-item.opened {
|
||||||
background-color: rgba(var(--v-theme-primary), 0.12);
|
background-color: rgba(var(--v-theme-primary), 0.12);
|
||||||
border-left-color: rgb(var(--v-theme-primary));
|
border-left-color: rgb(var(--v-theme-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-item.selected:not(.opened) {
|
||||||
|
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.message-item.unread {
|
.message-item.unread {
|
||||||
:deep(.v-list-item-title),
|
:deep(.v-list-item-title),
|
||||||
:deep(.v-list-item-subtitle:first-of-type) {
|
:deep(.v-list-item-subtitle:first-of-type) {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.selection-summary {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useUser } from '@KTXC'
|
import { useUser } from '@KTXC'
|
||||||
import type { EntityInterface } from '@MailManager/types/entity'
|
import type { EntityObject } from '@MailManager/models'
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
|
||||||
import { MessageObject } from '@MailManager/models/message'
|
import { MessageObject } from '@MailManager/models/message'
|
||||||
import { SecurityLevel } from '@/utile/emailSanitizer'
|
import { SecurityLevel } from '@/utile/emailSanitizer'
|
||||||
import ReaderEmpty from './reader/ReaderEmpty.vue'
|
import ReaderEmpty from './reader/ReaderEmpty.vue'
|
||||||
@@ -12,7 +11,7 @@ import ReaderBody from './reader/ReaderBody.vue'
|
|||||||
|
|
||||||
// Props
|
// Props
|
||||||
interface Props {
|
interface Props {
|
||||||
message?: EntityInterface<MessageInterface> | null
|
message?: EntityObject | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
@@ -22,11 +21,11 @@ const { getSetting } = useUser()
|
|||||||
|
|
||||||
// Emits
|
// Emits
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
reply: [message: EntityInterface<MessageInterface>]
|
reply: [message: EntityObject]
|
||||||
forward: [message: EntityInterface<MessageInterface>]
|
forward: [message: EntityObject]
|
||||||
move: [message: EntityInterface<MessageInterface>]
|
move: [message: EntityObject]
|
||||||
delete: [message: EntityInterface<MessageInterface>]
|
delete: [message: EntityObject]
|
||||||
flag: [message: EntityInterface<MessageInterface>]
|
flag: [message: EntityObject]
|
||||||
compose: []
|
compose: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import type { EntityInterface } from '@MailManager/types/entity'
|
import type { EntityObject } from '@MailManager/models'
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
|
||||||
import { SecurityLevel } from '@/utile/emailSanitizer'
|
import { SecurityLevel } from '@/utile/emailSanitizer'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: EntityInterface<MessageInterface>
|
message: EntityObject
|
||||||
isHtml: boolean
|
isHtml: boolean
|
||||||
allowImages: boolean
|
allowImages: boolean
|
||||||
securityLevel: SecurityLevel
|
securityLevel: SecurityLevel
|
||||||
|
|||||||
@@ -32,10 +32,14 @@ const {
|
|||||||
loading,
|
loading,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
selectedMessage,
|
selectedMessage,
|
||||||
|
selectedMessageIds,
|
||||||
|
selectionModeActive,
|
||||||
composeMode,
|
composeMode,
|
||||||
composeReplyTo,
|
composeReplyTo,
|
||||||
currentMessages,
|
|
||||||
moveDialogVisible,
|
moveDialogVisible,
|
||||||
|
selectionCount,
|
||||||
|
hasSelection,
|
||||||
|
allCurrentMessagesSelected,
|
||||||
} = storeToRefs(mailStore)
|
} = storeToRefs(mailStore)
|
||||||
|
|
||||||
// Complex store/composable objects accessed directly (not simple refs)
|
// Complex store/composable objects accessed directly (not simple refs)
|
||||||
@@ -50,7 +54,24 @@ onMounted(async () => {
|
|||||||
// Handlers — thin wrappers that delegate to the store
|
// Handlers — thin wrappers that delegate to the store
|
||||||
const handleFolderSelect = (folder: CollectionObject) => mailStore.selectFolder(folder)
|
const handleFolderSelect = (folder: CollectionObject) => mailStore.selectFolder(folder)
|
||||||
|
|
||||||
const handleMessageSelect = (message: EntityObject) => mailStore.selectMessage(message, isMobile.value)
|
const handleMessageOpen = (message: EntityObject) => mailStore.selectMessage(message, isMobile.value)
|
||||||
|
|
||||||
|
const handleMessageSelectionToggle = (message: EntityObject) => mailStore.toggleMessageSelection(message)
|
||||||
|
|
||||||
|
const handleSelectionModeActivate = (message: EntityObject) => mailStore.activateSelectionMode(message)
|
||||||
|
|
||||||
|
const handleSelectAllToggle = (value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
mailStore.selectAllCurrentMessages()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mailStore.clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectionClear = () => mailStore.deactivateSelectionMode()
|
||||||
|
|
||||||
|
const handleSelectionMove = () => mailStore.openMoveDialogForSelection()
|
||||||
|
|
||||||
const handleCompose = (replyTo?: EntityObject) => mailStore.openCompose(replyTo)
|
const handleCompose = (replyTo?: EntityObject) => mailStore.openCompose(replyTo)
|
||||||
|
|
||||||
@@ -65,7 +86,7 @@ const handleDelete = (message: EntityObject) => mailStore.deleteMessage(message)
|
|||||||
const handleMove = (message: EntityObject) => mailStore.openMoveDialog(message)
|
const handleMove = (message: EntityObject) => mailStore.openMoveDialog(message)
|
||||||
|
|
||||||
const handleMoveConfirm = async (folder: CollectionObject) => {
|
const handleMoveConfirm = async (folder: CollectionObject) => {
|
||||||
await mailStore.moveMessage(folder)
|
await mailStore.moveMessages(folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMoveCancel = () => mailStore.closeMoveDialog()
|
const handleMoveCancel = () => mailStore.closeMoveDialog()
|
||||||
@@ -179,11 +200,24 @@ const handleFolderCreated = (folder: CollectionObject) => mailStore.notify(`Fold
|
|||||||
<!-- Message list panel -->
|
<!-- Message list panel -->
|
||||||
<div class="mail-list-panel">
|
<div class="mail-list-panel">
|
||||||
<MessageList
|
<MessageList
|
||||||
|
<<<<<<< HEAD
|
||||||
:messages="currentMessages"
|
:messages="currentMessages"
|
||||||
:selected-message="selectedMessage"
|
:selected-message="selectedMessage"
|
||||||
|
:selected-message-ids="selectedMessageIds"
|
||||||
|
:selection-mode-active="selectionModeActive"
|
||||||
|
:selection-count="selectionCount"
|
||||||
|
:has-selection="hasSelection"
|
||||||
|
:all-current-messages-selected="allCurrentMessagesSelected"
|
||||||
:selected-collection="selectedFolder"
|
:selected-collection="selectedFolder"
|
||||||
|
=======
|
||||||
|
>>>>>>> 749d922 (refactor: improve logic)
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@select="handleMessageSelect"
|
@open="handleMessageOpen"
|
||||||
|
@toggle-selection="handleMessageSelectionToggle"
|
||||||
|
@activate-selection-mode="handleSelectionModeActivate"
|
||||||
|
@toggle-select-all="handleSelectAllToggle"
|
||||||
|
@clear-selection="handleSelectionClear"
|
||||||
|
@move-selection="handleSelectionMove"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ref, computed, shallowRef } from 'vue'
|
import { ref, computed, shallowRef, watch } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
||||||
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
||||||
@@ -6,13 +6,12 @@ import { useServicesStore } from '@MailManager/stores/servicesStore'
|
|||||||
import { useMailSync } from '@MailManager/composables/useMailSync'
|
import { useMailSync } from '@MailManager/composables/useMailSync'
|
||||||
import { useSnackbar } from '@KTXC'
|
import { useSnackbar } from '@KTXC'
|
||||||
import type { CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
import type { CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||||
import type { MessageInterface } from '@MailManager/types/message'
|
|
||||||
import type { ServiceObject, CollectionObject, EntityObject } from '@MailManager/models'
|
import type { ServiceObject, CollectionObject, EntityObject } from '@MailManager/models'
|
||||||
|
|
||||||
export const useMailStore = defineStore('mailStore', () => {
|
export const useMailStore = defineStore('mailStore', () => {
|
||||||
|
const servicesStore = useServicesStore()
|
||||||
const collectionsStore = useCollectionsStore()
|
const collectionsStore = useCollectionsStore()
|
||||||
const entitiesStore = useEntitiesStore()
|
const entitiesStore = useEntitiesStore()
|
||||||
const servicesStore = useServicesStore()
|
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
|
|
||||||
// Background mail sync
|
// Background mail sync
|
||||||
@@ -42,6 +41,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
// ── Selection State ───────────────────────────────────────────────────────
|
// ── Selection State ───────────────────────────────────────────────────────
|
||||||
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
||||||
const selectedMessage = shallowRef<EntityObject | null>(null)
|
const selectedMessage = shallowRef<EntityObject | null>(null)
|
||||||
|
const selectedMessageIds = ref<EntityIdentifier[]>([])
|
||||||
|
const selectionModeActive = ref(false)
|
||||||
|
|
||||||
// ── Compose State ─────────────────────────────────────────────────────────
|
// ── Compose State ─────────────────────────────────────────────────────────
|
||||||
const composeMode = ref(false)
|
const composeMode = ref(false)
|
||||||
@@ -49,7 +50,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
|
|
||||||
// ── Move State ────────────────────────────────────────────────────────────
|
// ── Move State ────────────────────────────────────────────────────────────
|
||||||
const moveDialogVisible = ref(false)
|
const moveDialogVisible = ref(false)
|
||||||
const moveMessageCandidate = shallowRef<EntityObject | null>(null)
|
const moveMessageCandidates = shallowRef<EntityObject[]>([])
|
||||||
|
|
||||||
// ── Computed ──────────────────────────────────────────────────────────────
|
// ── Computed ──────────────────────────────────────────────────────────────
|
||||||
const currentMessages = computed(() => {
|
const currentMessages = computed(() => {
|
||||||
@@ -64,6 +65,31 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedMessageIdSet = computed(() => new Set(selectedMessageIds.value))
|
||||||
|
|
||||||
|
const selectedMessageMap = computed(() => {
|
||||||
|
const messageMap = new Map<EntityIdentifier, EntityObject>()
|
||||||
|
|
||||||
|
currentMessages.value.forEach(message => {
|
||||||
|
const identifier = _entityIdentifier(message)
|
||||||
|
if (selectedMessageIdSet.value.has(identifier)) {
|
||||||
|
messageMap.set(identifier, message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return messageMap
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedMessages = computed(() => Array.from(selectedMessageMap.value.values()))
|
||||||
|
|
||||||
|
const selectionCount = computed(() => selectedMessageIds.value.length)
|
||||||
|
|
||||||
|
const hasSelection = computed(() => selectionCount.value > 0)
|
||||||
|
|
||||||
|
const allCurrentMessagesSelected = computed(() => {
|
||||||
|
return currentMessages.value.length > 0 && currentMessages.value.every(message => isMessageSelected(message))
|
||||||
|
})
|
||||||
|
|
||||||
// ── Initialization ────────────────────────────────────────────────────────
|
// ── Initialization ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
@@ -134,12 +160,20 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sync Helpers ──────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _serviceKey(provider: string, service: string | number) {
|
function _serviceKey(provider: string, service: string | number) {
|
||||||
return `${provider}:${String(service)}`
|
return `${provider}:${String(service)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _collectionIdentifier(collection: CollectionObject): CollectionIdentifier {
|
||||||
|
return `${collection.provider}:${String(collection.service)}:${String(collection.identifier)}` as CollectionIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
function _entityIdentifier(entity: EntityObject): EntityIdentifier {
|
||||||
|
return `${entity.provider}:${String(entity.service)}:${String(entity.collection)}:${String(entity.identifier)}` as EntityIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
|
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
|
||||||
serviceFolderLoadingState.value = {
|
serviceFolderLoadingState.value = {
|
||||||
...serviceFolderLoadingState.value,
|
...serviceFolderLoadingState.value,
|
||||||
@@ -208,42 +242,90 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
|
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function _entityIdentifier(entity: EntityObject): EntityIdentifier {
|
function _serviceFor(provider: string, serviceIdentifier: string | number) {
|
||||||
return `${entity.provider}:${String(entity.service)}:${String(entity.collection)}:${String(entity.identifier)}` as EntityIdentifier
|
return servicesStore.services.find(service =>
|
||||||
|
service.provider === provider &&
|
||||||
|
String(service.identifier) === String(serviceIdentifier),
|
||||||
|
) ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
function _collectionIdentifier(collection: CollectionObject): CollectionIdentifier {
|
function _reloadFolderMessages(folder: CollectionObject) {
|
||||||
return `${collection.provider}:${String(collection.service)}:${String(collection.identifier)}` as CollectionIdentifier
|
return entitiesStore.list({
|
||||||
|
[folder.provider]: {
|
||||||
|
[String(folder.service)]: {
|
||||||
|
[String(folder.identifier)]: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function _isSameMessage(left: EntityObject | null, right: EntityObject): boolean {
|
function _setSelectedMessageIds(nextIds: EntityIdentifier[]) {
|
||||||
if (!left) {
|
selectedMessageIds.value = Array.from(new Set(nextIds))
|
||||||
return false
|
}
|
||||||
|
|
||||||
|
function _removeSelection(sourceIdentifiers: EntityIdentifier[]) {
|
||||||
|
if (sourceIdentifiers.length === 0 || selectedMessageIds.value.length === 0) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const removedIds = new Set(sourceIdentifiers)
|
||||||
left.provider === right.provider &&
|
selectedMessageIds.value = selectedMessageIds.value.filter(identifier => !removedIds.has(identifier))
|
||||||
String(left.service) === String(right.service) &&
|
|
||||||
String(left.collection) === String(right.collection) &&
|
|
||||||
String(left.identifier) === String(right.identifier)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _reconcileSelection() {
|
||||||
|
if (!selectedFolder.value) {
|
||||||
|
clearSelection()
|
||||||
|
selectedMessage.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMessageIdentifiers = new Set(currentMessages.value.map(message => _entityIdentifier(message)))
|
||||||
|
const nextSelectedIds = selectedMessageIds.value.filter(identifier => currentMessageIdentifiers.has(identifier))
|
||||||
|
|
||||||
|
if (nextSelectedIds.length !== selectedMessageIds.value.length) {
|
||||||
|
selectedMessageIds.value = nextSelectedIds
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMessage.value && !currentMessageIdentifiers.has(_entityIdentifier(selectedMessage.value))) {
|
||||||
|
selectedMessage.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _formatMoveNotification(successCount: number, failureCount: number, targetFolder: CollectionObject) {
|
||||||
|
const folderLabel = targetFolder.properties.label || String(targetFolder.identifier)
|
||||||
|
|
||||||
|
if (failureCount === 0) {
|
||||||
|
return {
|
||||||
|
message: successCount === 1
|
||||||
|
? `Message moved to "${folderLabel}"`
|
||||||
|
: `${successCount} messages moved to "${folderLabel}"`,
|
||||||
|
color: 'success' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: successCount === 0
|
||||||
|
? `Move failed for ${failureCount === 1 ? '1 message' : `${failureCount} messages`}`
|
||||||
|
: `Moved ${successCount} ${successCount === 1 ? 'message' : 'messages'} to "${folderLabel}". ${failureCount} failed.`,
|
||||||
|
color: successCount === 0 ? 'error' as const : 'warning' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currentMessages, () => {
|
||||||
|
_reconcileSelection()
|
||||||
|
})
|
||||||
|
|
||||||
// ── Actions ───────────────────────────────────────────────────────────────
|
// ── Actions ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function selectFolder(folder: CollectionObject) {
|
async function selectFolder(folder: CollectionObject) {
|
||||||
selectedFolder.value = folder
|
selectedFolder.value = folder
|
||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
|
clearSelection()
|
||||||
|
selectionModeActive.value = false
|
||||||
composeMode.value = false
|
composeMode.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await entitiesStore.list({
|
await _reloadFolderMessages(folder)
|
||||||
[folder.provider]: {
|
|
||||||
[folder.service]: {
|
|
||||||
[folder.identifier]: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Mail] Failed to load messages:', error)
|
console.error('[Mail] Failed to load messages:', error)
|
||||||
}
|
}
|
||||||
@@ -266,14 +348,79 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMoveDialog(message: EntityObject) {
|
function isMessageSelected(message: EntityObject) {
|
||||||
moveMessageCandidate.value = message
|
return selectedMessageIdSet.value.has(_entityIdentifier(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMessageSelection(message: EntityObject) {
|
||||||
|
const identifier = _entityIdentifier(message)
|
||||||
|
|
||||||
|
selectionModeActive.value = true
|
||||||
|
|
||||||
|
if (selectedMessageIdSet.value.has(identifier)) {
|
||||||
|
selectedMessageIds.value = selectedMessageIds.value.filter(selectedId => selectedId !== identifier)
|
||||||
|
|
||||||
|
if (selectedMessageIds.value.length === 0) {
|
||||||
|
selectionModeActive.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSelectedMessageIds([...selectedMessageIds.value, identifier])
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllCurrentMessages() {
|
||||||
|
selectionModeActive.value = true
|
||||||
|
_setSelectedMessageIds(currentMessages.value.map(message => _entityIdentifier(message)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateSelectionMode(message?: EntityObject) {
|
||||||
|
selectionModeActive.value = true
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
const identifier = _entityIdentifier(message)
|
||||||
|
|
||||||
|
if (!selectedMessageIdSet.value.has(identifier)) {
|
||||||
|
_setSelectedMessageIds([...selectedMessageIds.value, identifier])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivateSelectionMode() {
|
||||||
|
selectionModeActive.value = false
|
||||||
|
clearSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedMessageIds.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMoveDialog(messages: EntityObject | EntityObject[]) {
|
||||||
|
const nextCandidates = Array.isArray(messages) ? messages : [messages]
|
||||||
|
|
||||||
|
moveMessageCandidates.value = Array.from(
|
||||||
|
new Map(nextCandidates.map(candidate => [_entityIdentifier(candidate), candidate])).values(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (moveMessageCandidates.value.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
moveDialogVisible.value = true
|
moveDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openMoveDialogForSelection() {
|
||||||
|
if (selectedMessages.value.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
openMoveDialog(selectedMessages.value)
|
||||||
|
}
|
||||||
|
|
||||||
function closeMoveDialog() {
|
function closeMoveDialog() {
|
||||||
moveDialogVisible.value = false
|
moveDialogVisible.value = false
|
||||||
moveMessageCandidate.value = null
|
moveMessageCandidates.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeCompose() {
|
function closeCompose() {
|
||||||
@@ -291,24 +438,20 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMessage(message: EntityObject) {
|
async function moveMessages(targetFolder: CollectionObject) {
|
||||||
// TODO: implement delete via entity / collection store
|
const candidates = moveMessageCandidates.value
|
||||||
console.log('[Mail] Delete message:', message.identifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function moveMessage(targetFolder: CollectionObject) {
|
if (candidates.length === 0) {
|
||||||
const message = moveMessageCandidate.value
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSameCollection =
|
const movableCandidates = candidates.filter(message => !(
|
||||||
targetFolder.provider === message.provider &&
|
targetFolder.provider === message.provider &&
|
||||||
String(targetFolder.service) === String(message.service) &&
|
String(targetFolder.service) === String(message.service) &&
|
||||||
String(targetFolder.identifier) === String(message.collection)
|
String(targetFolder.identifier) === String(message.collection)
|
||||||
|
))
|
||||||
|
|
||||||
if (isSameCollection) {
|
if (movableCandidates.length === 0) {
|
||||||
closeMoveDialog()
|
closeMoveDialog()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -316,32 +459,59 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sourceIdentifier = _entityIdentifier(message)
|
const sourceIdentifiers = movableCandidates.map(message => _entityIdentifier(message))
|
||||||
const response = await entitiesStore.move(_collectionIdentifier(targetFolder), [sourceIdentifier])
|
const response = await entitiesStore.move(_collectionIdentifier(targetFolder), sourceIdentifiers)
|
||||||
const result = response[sourceIdentifier]
|
const successfulMoves: EntityIdentifier[] = []
|
||||||
|
const failedMoves: string[] = []
|
||||||
|
|
||||||
if (!result || !result.success) {
|
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
||||||
throw new Error(result && 'error' in result ? result.error : 'Failed to move message')
|
if (result.success) {
|
||||||
|
successfulMoves.push(sourceIdentifier as EntityIdentifier)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
failedMoves.push(result.error)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (successfulMoves.length === 0) {
|
||||||
|
throw new Error(failedMoves[0] ?? 'Failed to move messages')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isSameMessage(selectedMessage.value, message)) {
|
_removeSelection(successfulMoves)
|
||||||
|
|
||||||
|
if (selectedMessage.value && successfulMoves.includes(_entityIdentifier(selectedMessage.value))) {
|
||||||
selectedMessage.value = null
|
selectedMessage.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = servicesStore.services.find(entry =>
|
if (selectedMessageIds.value.length === 0) {
|
||||||
entry.provider === message.provider &&
|
selectionModeActive.value = false
|
||||||
String(entry.identifier) === String(message.service),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (service) {
|
|
||||||
void loadFoldersForService(service)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(`Message moved to "${targetFolder.properties.label || targetFolder.identifier}"`, 'success')
|
|
||||||
closeMoveDialog()
|
closeMoveDialog()
|
||||||
|
|
||||||
|
const servicesToRefresh = new Map<string, ServiceObject>()
|
||||||
|
movableCandidates.forEach(message => {
|
||||||
|
const service = _serviceFor(message.provider, message.service)
|
||||||
|
if (service && service.identifier !== null) {
|
||||||
|
servicesToRefresh.set(`${service.provider}:${String(service.identifier)}`, service)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const targetService = _serviceFor(targetFolder.provider, targetFolder.service)
|
||||||
|
if (targetService && targetService.identifier !== null) {
|
||||||
|
servicesToRefresh.set(`${targetService.provider}:${String(targetService.identifier)}`, targetService)
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled([
|
||||||
|
...Array.from(servicesToRefresh.values()).map(service => loadFoldersForService(service)),
|
||||||
|
...(selectedFolder.value ? [_reloadFolderMessages(selectedFolder.value)] : []),
|
||||||
|
])
|
||||||
|
|
||||||
|
const notification = _formatMoveNotification(successfulMoves.length, failedMoves.length, targetFolder)
|
||||||
|
notify(notification.message, notification.color)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const messageText = error instanceof Error ? error.message : 'Failed to move message'
|
const messageText = error instanceof Error ? error.message : 'Failed to move messages'
|
||||||
console.error('[Mail] Failed to move message:', error)
|
console.error('[Mail] Failed to move messages:', error)
|
||||||
notify(messageText, 'error')
|
notify(messageText, 'error')
|
||||||
throw error
|
throw error
|
||||||
} finally {
|
} finally {
|
||||||
@@ -349,6 +519,11 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(message: EntityObject) {
|
||||||
|
// TODO: implement delete via entity / collection store
|
||||||
|
console.log('[Mail] Delete message:', message.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
sidebarVisible.value = !sidebarVisible.value
|
sidebarVisible.value = !sidebarVisible.value
|
||||||
}
|
}
|
||||||
@@ -376,27 +551,41 @@ export const useMailStore = defineStore('mailStore', () => {
|
|||||||
loading,
|
loading,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
selectedMessage,
|
selectedMessage,
|
||||||
|
selectedMessageIds,
|
||||||
|
selectionModeActive,
|
||||||
composeMode,
|
composeMode,
|
||||||
composeReplyTo,
|
composeReplyTo,
|
||||||
moveDialogVisible,
|
moveDialogVisible,
|
||||||
moveMessageCandidate,
|
moveMessageCandidates,
|
||||||
serviceFolderLoadingState,
|
serviceFolderLoadingState,
|
||||||
serviceFolderLoadedState,
|
serviceFolderLoadedState,
|
||||||
serviceFolderErrorState,
|
serviceFolderErrorState,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
currentMessages,
|
currentMessages,
|
||||||
|
selectedMessageMap,
|
||||||
|
selectedMessages,
|
||||||
|
selectionCount,
|
||||||
|
hasSelection,
|
||||||
|
allCurrentMessagesSelected,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
selectFolder,
|
selectFolder,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
|
isMessageSelected,
|
||||||
|
activateSelectionMode,
|
||||||
|
deactivateSelectionMode,
|
||||||
|
toggleMessageSelection,
|
||||||
|
selectAllCurrentMessages,
|
||||||
|
clearSelection,
|
||||||
openCompose,
|
openCompose,
|
||||||
openMoveDialog,
|
openMoveDialog,
|
||||||
|
openMoveDialogForSelection,
|
||||||
closeMoveDialog,
|
closeMoveDialog,
|
||||||
closeCompose,
|
closeCompose,
|
||||||
afterSent,
|
afterSent,
|
||||||
deleteMessage,
|
deleteMessage,
|
||||||
moveMessage,
|
moveMessages,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
openSettings,
|
openSettings,
|
||||||
notify,
|
notify,
|
||||||
|
|||||||
Reference in New Issue
Block a user