Compare commits

..

1 Commits

Author SHA1 Message Date
749d92285f refactor: improve logic
Signed-off-by: Sebastian <krupinski01@gmail.com>
2026-03-29 22:32:55 -04:00
4 changed files with 84 additions and 525 deletions

View File

@@ -50,7 +50,7 @@ interface ServiceGroup {
} }
const serviceGroups = computed<ServiceGroup[]>(() => { const serviceGroups = computed<ServiceGroup[]>(() => {
const moveCandidate = mailStore.moveMessageCandidates[0] const moveCandidate = mailStore.moveMessageCandidate
if (!moveCandidate) { if (!moveCandidate) {
return [] return []
@@ -111,7 +111,7 @@ const canConfirm = computed(() => {
}) })
watch( watch(
() => [props.modelValue, mailStore.moveMessageCandidates], () => [props.modelValue, mailStore.moveMessageCandidate],
([isOpen]) => { ([isOpen]) => {
if (!isOpen) { if (!isOpen) {
return return

View File

@@ -1,51 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, ref } from 'vue' import { computed } from 'vue'
import type { EntityIdentifier } from '@MailManager/types/common' import { storeToRefs } from 'pinia'
import type { EntityInterface } from '@MailManager/types/entity' import { useMailStore } from '@/stores/mailStore'
import type { MessageInterface } from '@MailManager/types/message' import type { EntityObject } from '@MailManager/models'
import type { CollectionObject } from '@MailManager/models/collection'
// Props withDefaults(defineProps<{
interface Props {
messages: EntityInterface<MessageInterface>[]
selectedMessage?: EntityInterface<MessageInterface> | null
selectedMessageIds?: EntityIdentifier[]
selectionModeActive?: boolean
selectionCount?: number
hasSelection?: boolean
allCurrentMessagesSelected?: boolean
selectedCollection?: CollectionObject | null
loading?: boolean loading?: boolean
} }>(), {
loading: false
const props = withDefaults(defineProps<Props>(), {
loading: false,
selectedMessageIds: () => [],
selectionModeActive: false,
selectionCount: 0,
hasSelection: false,
allCurrentMessagesSelected: false,
}) })
// Emits // Emits
const emit = defineEmits<{ const emit = defineEmits<{
open: [message: EntityInterface<MessageInterface>] select: [message: EntityObject]
toggleSelection: [message: EntityInterface<MessageInterface>]
activateSelectionMode: [message: EntityInterface<MessageInterface>]
toggleSelectAll: [value: boolean]
clearSelection: []
moveSelection: []
}>() }>()
const longPressTimer = ref<number | null>(null) const mailStore = useMailStore()
const longPressActivated = ref(false) const { currentMessages, selectedFolder, selectedMessage } = storeToRefs(mailStore)
const suppressNextClick = ref(false)
const LONG_PRESS_MS = 450
const selectedIdSet = computed(() => new Set(props.selectedMessageIds)) // Check if message is selected
const isSelected = (message: EntityObject): boolean => {
const isOpened = (message: EntityInterface<MessageInterface>): boolean => { if (!selectedMessage.value) return false
if (!props.selectedMessage) return false
return ( return (
message.provider === selectedMessage.value.provider && message.provider === selectedMessage.value.provider &&
message.service === selectedMessage.value.service && message.service === selectedMessage.value.service &&
@@ -54,12 +29,6 @@ const isOpened = (message: EntityInterface<MessageInterface>): boolean => {
) )
} }
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: EntityObject): boolean => { const isUnread = (message: EntityObject): boolean => {
return !message.properties.flags?.read return !message.properties.flags?.read
@@ -117,88 +86,8 @@ const truncate = (text: string | null | undefined, length: number = 100): string
} }
// Handle message click // Handle message click
const handleMessageClick = (message: EntityInterface<MessageInterface>) => { const handleMessageClick = (message: EntityObject) => {
if (longPressActivated.value) { emit('select', message)
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)
@@ -212,7 +101,7 @@ const sortedMessages = computed(() => {
// Read/Unread counts from collection properties // Read/Unread counts from collection properties
const unreadCount = computed(() => { const unreadCount = computed(() => {
return props.selectedCollection?.properties.unread ?? 0 return selectedFolder.value?.properties.unread ?? 0
}) })
const totalCount = computed(() => { const totalCount = computed(() => {
@@ -228,57 +117,20 @@ const hasCountData = computed(() => {
<template> <template>
<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="selectedFolder" class="message-list-header">
<div class="message-list-heading"> <h2 class="text-h6">{{ selectedFolder.properties.label || 'Folder' }}</h2>
<h2 class="text-h6">{{ selectedCollection.properties.label || 'Folder' }}</h2>
<div class="folder-counts text-caption text-medium-emphasis"> <div class="folder-counts text-caption text-medium-emphasis">
<span v-if="hasCountData"> <span v-if="hasCountData">
<span class="unread-count">{{ unreadCount }}</span> <span class="unread-count">{{ unreadCount }}</span>
<span class="mx-1">/</span> <span class="mx-1">/</span>
<span>{{ totalCount }}</span> <span>{{ totalCount }}</span>
</span> </span>
<span v-else-if="messages.length > 0"> <span v-else-if="currentMessages.length > 0">
{{ messages.length }} loaded {{ currentMessages.length }} loaded
</span> </span>
</div> </div>
</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>
<!-- Loading state --> <!-- Loading state -->
<div v-if="loading" class="pa-4"> <div v-if="loading" class="pa-4">
<v-skeleton-loader <v-skeleton-loader
@@ -310,36 +162,18 @@ 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)
}" }"
@mousedown="handleMessageMouseDown($event, message)" @click="handleMessageClick(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>
<div class="message-item-prepend">
<v-checkbox-btn
v-if="selectionModeActive || isSelected(message)"
:model-value="isSelected(message)"
density="compact"
hide-details
@click.stop
@update:model-value="handleSelectionToggle(message)"
/>
<v-avatar size="40" color="primary"> <v-avatar size="40" color="primary">
<span class="text-white text-body-1"> <span class="text-white text-body-1">
{{ (message.properties.from?.label || message.properties.from?.address || 'U')[0].toUpperCase() }} {{ (message.properties.from?.label || message.properties.from?.address || 'U')[0].toUpperCase() }}
</span> </span>
</v-avatar> </v-avatar>
</div>
</template> </template>
<v-list-item-title class="d-flex align-center"> <v-list-item-title class="d-flex align-center">
@@ -398,42 +232,14 @@ 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;
flex-direction: column;
gap: 12px;
.message-list-heading {
width: 100%;
display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 12px; 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;
@@ -455,41 +261,19 @@ 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.opened { .message-item.selected {
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>

View File

@@ -32,14 +32,9 @@ const {
loading, loading,
selectedFolder, selectedFolder,
selectedMessage, selectedMessage,
selectedMessageIds,
selectionModeActive,
composeMode, composeMode,
composeReplyTo, composeReplyTo,
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)
@@ -54,24 +49,7 @@ 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 handleMessageOpen = (message: EntityObject) => mailStore.selectMessage(message, isMobile.value) const handleMessageSelect = (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)
@@ -86,7 +64,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.moveMessages(folder) await mailStore.moveMessage(folder)
} }
const handleMoveCancel = () => mailStore.closeMoveDialog() const handleMoveCancel = () => mailStore.closeMoveDialog()
@@ -200,21 +178,8 @@ 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
:messages="currentMessages"
: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"
:loading="loading" :loading="loading"
@open="handleMessageOpen" @select="handleMessageSelect"
@toggle-selection="handleMessageSelectionToggle"
@activate-selection-mode="handleSelectionModeActivate"
@toggle-select-all="handleSelectAllToggle"
@clear-selection="handleSelectionClear"
@move-selection="handleSelectionMove"
/> />
</div> </div>

View File

@@ -1,4 +1,4 @@
import { ref, computed, shallowRef, watch } from 'vue' import { ref, computed, shallowRef } 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'
@@ -41,8 +41,6 @@ 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)
@@ -50,7 +48,7 @@ export const useMailStore = defineStore('mailStore', () => {
// ── Move State ──────────────────────────────────────────────────────────── // ── Move State ────────────────────────────────────────────────────────────
const moveDialogVisible = ref(false) const moveDialogVisible = ref(false)
const moveMessageCandidates = shallowRef<EntityObject[]>([]) const moveMessageCandidate = shallowRef<EntityObject | null>(null)
// ── Computed ────────────────────────────────────────────────────────────── // ── Computed ──────────────────────────────────────────────────────────────
const currentMessages = computed(() => { const currentMessages = computed(() => {
@@ -65,31 +63,6 @@ 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() {
@@ -242,90 +215,34 @@ export const useMailStore = defineStore('mailStore', () => {
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
} }
function _serviceFor(provider: string, serviceIdentifier: string | number) { function _isSameMessage(left: EntityObject | null, right: EntityObject): boolean {
return servicesStore.services.find(service => if (!left) {
service.provider === provider && return false
String(service.identifier) === String(serviceIdentifier),
) ?? null
} }
function _reloadFolderMessages(folder: CollectionObject) { return (
return entitiesStore.list({ left.provider === right.provider &&
[folder.provider]: { String(left.service) === String(right.service) &&
[String(folder.service)]: { String(left.collection) === String(right.collection) &&
[String(folder.identifier)]: true, String(left.identifier) === String(right.identifier)
}, )
},
})
} }
function _setSelectedMessageIds(nextIds: EntityIdentifier[]) {
selectedMessageIds.value = Array.from(new Set(nextIds))
}
function _removeSelection(sourceIdentifiers: EntityIdentifier[]) {
if (sourceIdentifiers.length === 0 || selectedMessageIds.value.length === 0) {
return
}
const removedIds = new Set(sourceIdentifiers)
selectedMessageIds.value = selectedMessageIds.value.filter(identifier => !removedIds.has(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 _reloadFolderMessages(folder) await entitiesStore.list({
[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)
} }
@@ -348,79 +265,14 @@ export const useMailStore = defineStore('mailStore', () => {
selectedMessage.value = null selectedMessage.value = null
} }
function isMessageSelected(message: EntityObject) { function openMoveDialog(message: EntityObject) {
return selectedMessageIdSet.value.has(_entityIdentifier(message)) moveMessageCandidate.value = 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
moveMessageCandidates.value = [] moveMessageCandidate.value = null
} }
function closeCompose() { function closeCompose() {
@@ -438,20 +290,19 @@ export const useMailStore = defineStore('mailStore', () => {
} }
} }
async function moveMessages(targetFolder: CollectionObject) { async function moveMessage(targetFolder: CollectionObject) {
const candidates = moveMessageCandidates.value const message = moveMessageCandidate.value
if (candidates.length === 0) { if (!message) {
return return
} }
const movableCandidates = candidates.filter(message => !( const isSameCollection =
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 (movableCandidates.length === 0) { if (isSameCollection) {
closeMoveDialog() closeMoveDialog()
return return
} }
@@ -459,59 +310,32 @@ export const useMailStore = defineStore('mailStore', () => {
loading.value = true loading.value = true
try { try {
const sourceIdentifiers = movableCandidates.map(message => _entityIdentifier(message)) const sourceIdentifier = _entityIdentifier(message)
const response = await entitiesStore.move(_collectionIdentifier(targetFolder), sourceIdentifiers) const response = await entitiesStore.move(_collectionIdentifier(targetFolder), [sourceIdentifier])
const successfulMoves: EntityIdentifier[] = [] const result = response[sourceIdentifier]
const failedMoves: string[] = []
Object.entries(response).forEach(([sourceIdentifier, result]) => { if (!result || !result.success) {
if (result.success) { throw new Error(result && 'error' in result ? result.error : 'Failed to move message')
successfulMoves.push(sourceIdentifier as EntityIdentifier)
return
} }
failedMoves.push(result.error) if (_isSameMessage(selectedMessage.value, message)) {
})
if (successfulMoves.length === 0) {
throw new Error(failedMoves[0] ?? 'Failed to move messages')
}
_removeSelection(successfulMoves)
if (selectedMessage.value && successfulMoves.includes(_entityIdentifier(selectedMessage.value))) {
selectedMessage.value = null selectedMessage.value = null
} }
if (selectedMessageIds.value.length === 0) { const service = servicesStore.services.find(entry =>
selectionModeActive.value = false entry.provider === message.provider &&
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 messages' const messageText = error instanceof Error ? error.message : 'Failed to move message'
console.error('[Mail] Failed to move messages:', error) console.error('[Mail] Failed to move message:', error)
notify(messageText, 'error') notify(messageText, 'error')
throw error throw error
} finally { } finally {
@@ -551,41 +375,27 @@ export const useMailStore = defineStore('mailStore', () => {
loading, loading,
selectedFolder, selectedFolder,
selectedMessage, selectedMessage,
selectedMessageIds,
selectionModeActive,
composeMode, composeMode,
composeReplyTo, composeReplyTo,
moveDialogVisible, moveDialogVisible,
moveMessageCandidates, moveMessageCandidate,
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,
moveMessages, moveMessage,
toggleSidebar, toggleSidebar,
openSettings, openSettings,
notify, notify,