import { computed, ref, shallowRef, watch } from 'vue' import { defineStore } from 'pinia' import { useCollectionsStore } from '@MailManager/stores/collectionsStore' import { useMailStore } from '@/stores/mailStore' import { useMailSettingsStore } from '@/stores/mailSettingsStore' import { ComposerMode } from '@/types/composer' import type { ServiceIdentifier, EntityIdentifier } from '@MailManager/types/common' import { EntityObject, type ServiceObject } from '@MailManager/models' import type { CollectionObject } from '@MailManager/models/collection' import type { MessageAddressInterface } from '@MailManager/types/message' export const useMailUiStore = defineStore('mailUiStore', () => { const collectionsStore = useCollectionsStore() const mailStore = useMailStore() const mailSettingsStore = useMailSettingsStore() const sidebarVisible = ref(true) const settingsDialogVisible = ref(false) const selectedFolder = shallowRef(null) const selectedMessage = shallowRef(null) const composerMode = ref(ComposerMode.Fresh) const composerSource = shallowRef(null) const composerVisible = ref(false) const selectionMode = ref(false) const selectionList = ref([]) const moveMessagesDialogVisible = ref(false) const moveMessagesDialogService = shallowRef(null) const moveMessagesDialogCandidates = ref(null) const createFolderDialogVisible = ref(false) const createFolderDialogService = shallowRef(null) const createFolderDialogParent = shallowRef(null) const createFolderDialogLoading = ref(false) const createFolderDialogError = ref('') const renameFolderDialogVisible = ref(false) const renameFolderDialogService = shallowRef(null) const renameFolderDialogFolder = shallowRef(null) const renameFolderDialogLoading = ref(false) const renameFolderDialogError = ref('') const moveFolderDialogVisible = ref(false) const moveFolderDialogService = shallowRef(null) const moveFolderDialogSource = shallowRef(null) const deleteFolderDialogVisible = ref(false) const deleteFolderDialogService = shallowRef(null) const deleteFolderDialogFolder = shallowRef(null) const deleteFolderDialogLoading = ref(false) const deleteFolderDialogError = ref('') const messageReadIdentifier = ref(null) const messageReadTimer = ref | null>(null) const createFolderDialogParentLabel = computed(() => { return createFolderDialogParent.value?.properties.label || 'Root' }) const renameFolderDialogParentLabel = computed(() => { const folder = renameFolderDialogFolder.value if (!folder || folder.collection === null || folder.collection === undefined) { return 'Root' } const parent = collectionsStore.collectionsInCollection(folder.provider, folder.service, null) .flatMap(rootFolder => [rootFolder, ...collectionsStore.collectionsForService(folder.provider, folder.service)]) .find(candidate => String(candidate.identifier) === String(folder.collection)) return parent?.properties.label || 'Root' }) const moveFolderDialogInvalidFolderKeys = computed(() => { const sourceFolder = moveFolderDialogSource.value if (!sourceFolder) { return [] } const invalidKeys = new Set() const queue = [sourceFolder] while (queue.length > 0) { const currentFolder = queue.shift() if (!currentFolder) { continue } invalidKeys.add(String(currentFolder.identifier)) collectionsStore .collectionsInCollection(currentFolder.provider, currentFolder.service, currentFolder.identifier) .forEach(childFolder => { queue.push(childFolder) }) } return Array.from(invalidKeys) }) watch( () => mailStore.selectedMessage, message => { if (message) { closeComposer() } selectMessage(message) }, ) watch( () => mailStore.currentMessages, () => { messageSelectionReconcile() }, ) function sidebarToggle() { sidebarVisible.value = !sidebarVisible.value } function sidebarHide() { sidebarVisible.value = false } function settingsOpen() { settingsDialogVisible.value = true } function settingsClose() { settingsDialogVisible.value = false } function _sameCollection(left: CollectionObject | null | undefined, right: CollectionObject | null | undefined): boolean { if (!left || !right) { return false } return left.provider === right.provider && String(left.service) === String(right.service) && String(left.identifier) === String(right.identifier) } async function initialize() { await mailStore.initialize() if (!selectedFolder.value) { const inbox = mailStore.findFoldersByRole('inbox')[0] ?? null if (inbox) { await selectFolder(inbox) } } } async function selectFolder(folder: CollectionObject | null) { closeComposer() messageSelectionModeDeactivate() clearMessageReadTimer() selectedMessage.value = null selectedFolder.value = folder await mailStore.selectFolder(folder) } async function selectMessage(message: EntityObject | null) { messageSelectionModeDeactivate() createMessageReadTimer(message) selectedMessage.value = message } function createMessageReadTimer(entity: EntityObject | null) { clearMessageReadTimer() if (!entity) { return } if (entity.properties.isRead || !mailSettingsStore.messageReadEnabled) { return } const delayMilliseconds = mailSettingsStore.messageReadDelay * 1000 if (delayMilliseconds <= 0) { return } messageReadIdentifier.value = entity.identifier messageReadTimer.value = setTimeout(() => { void completeMessageRead(entity.identifier) }, delayMilliseconds) } function clearMessageReadTimer() { if (messageReadTimer.value !== null) { clearTimeout(messageReadTimer.value) } messageReadTimer.value = null messageReadIdentifier.value = null } async function completeMessageRead(identifier: EntityIdentifier) { try { if (selectedMessage.value && selectedMessage.value.identifier === identifier && selectedMessage.value.properties.isRead === false) { await mailStore.flagMessages([selectedMessage.value.identifier], { read: true }, { notify: false }) } } catch (error) { console.error('[Mail][UI] Failed to auto-mark message as read:', error) } finally { clearMessageReadTimer() } } function openCreateFolderDialog(service: ServiceObject, parentFolder: CollectionObject | null = null) { createFolderDialogService.value = service createFolderDialogParent.value = parentFolder createFolderDialogError.value = '' createFolderDialogLoading.value = false createFolderDialogVisible.value = true } function closeCreateFolderDialog() { createFolderDialogVisible.value = false createFolderDialogService.value = null createFolderDialogParent.value = null createFolderDialogError.value = '' createFolderDialogLoading.value = false } async function confirmCreateFolder(label: string) { const service = createFolderDialogService.value if (!service) { return null } createFolderDialogLoading.value = true createFolderDialogError.value = '' try { const folder = await mailStore.createFolder(service, label, createFolderDialogParent.value) closeCreateFolderDialog() return folder } catch (error) { createFolderDialogError.value = error instanceof Error ? error.message : 'Failed to create folder. Please try again.' throw error } finally { createFolderDialogLoading.value = false } } async function openRenameFolderDialog(target: CollectionObject) { const service = await mailStore.retrieveService(target.service) renameFolderDialogService.value = service renameFolderDialogFolder.value = target renameFolderDialogError.value = '' renameFolderDialogLoading.value = false renameFolderDialogVisible.value = true } function closeRenameFolderDialog() { renameFolderDialogVisible.value = false renameFolderDialogService.value = null renameFolderDialogFolder.value = null renameFolderDialogError.value = '' renameFolderDialogLoading.value = false } async function confirmRenameFolder(label: string) { const folder = renameFolderDialogFolder.value if (!folder) { return null } renameFolderDialogLoading.value = true renameFolderDialogError.value = '' try { const updatedFolder = await mailStore.renameFolder(folder, label) if (_sameCollection(selectedFolder.value, folder)) { selectedFolder.value = updatedFolder } closeRenameFolderDialog() return updatedFolder } catch (error) { renameFolderDialogError.value = error instanceof Error ? error.message : 'Failed to rename folder. Please try again.' throw error } finally { renameFolderDialogLoading.value = false } } async function openMoveFolderDialog(source: CollectionObject) { const service = await mailStore.retrieveService(source.service) moveFolderDialogService.value = service moveFolderDialogSource.value = source moveFolderDialogVisible.value = true } function closeMoveFolderDialog() { moveFolderDialogVisible.value = false moveFolderDialogService.value = null moveFolderDialogSource.value = null } async function confirmMoveFolder(target: CollectionObject) { const source = moveFolderDialogSource.value if (!source) { return null } const movedFolder = await mailStore.moveFolder(source, target) if (_sameCollection(selectedFolder.value, source)) { selectedFolder.value = movedFolder } closeMoveFolderDialog() return movedFolder } async function openDeleteFolderDialog(target: CollectionObject) { const service = await mailStore.retrieveService(target.service) deleteFolderDialogService.value = service deleteFolderDialogFolder.value = target deleteFolderDialogError.value = '' deleteFolderDialogLoading.value = false deleteFolderDialogVisible.value = true } function closeDeleteFolderDialog() { deleteFolderDialogVisible.value = false deleteFolderDialogService.value = null deleteFolderDialogFolder.value = null deleteFolderDialogError.value = '' deleteFolderDialogLoading.value = false } async function confirmDeleteFolder() { const folder = deleteFolderDialogFolder.value if (!folder) { return null } deleteFolderDialogLoading.value = true deleteFolderDialogError.value = '' try { const deleted = await mailStore.deleteFolder(folder) if (_sameCollection(selectedFolder.value, folder)) { selectFolder(null) } closeDeleteFolderDialog() return deleted } catch (error) { deleteFolderDialogError.value = error instanceof Error ? error.message : 'Failed to delete folder. Please try again.' throw error } finally { deleteFolderDialogLoading.value = false } } function validateFolderNameBase(service: ServiceObject, name: string): string[] { const errors: string[] = [] if (!name || name.trim().length === 0) { errors.push('Folder name is required') return errors } if (name.length > 255) { errors.push('Folder name too long (max 255 characters)') } if (/[<>:"|?*\x00-\x1F]/.test(name)) { errors.push('Folder name contains invalid characters') } if (service.provider === 'imap' && /[\/\\]/.test(name)) { errors.push('IMAP folder names cannot contain / or \\') } if (name !== name.trim()) { errors.push('Folder name cannot have leading or trailing spaces') } return errors } function validateCreateFolderName(name: string): string[] { const service = createFolderDialogService.value if (!service || service.identifier === null) { return ['Folder service is unavailable'] } const errors = validateFolderNameBase(service, name) if (errors.length > 0) { return errors } const parentIdentifier = createFolderDialogParent.value?.identifier ?? null const duplicate = collectionsStore .collectionsInCollection(service.provider, service.identifier, parentIdentifier) .some(folder => folder.properties.label === name) if (duplicate) { errors.push('A folder with this name already exists in this location') } return errors } function validateRenameFolderName(name: string): string[] { const service = renameFolderDialogService.value const folder = renameFolderDialogFolder.value if (!service || !folder || service.identifier === null) { return ['Folder service is unavailable'] } const errors = validateFolderNameBase(service, name) if (errors.length > 0) { return errors } const parentIdentifier = folder.collection ?? null const duplicate = collectionsStore .collectionsInCollection(service.provider, service.identifier, parentIdentifier) .some(candidate => String(candidate.identifier) !== String(folder.identifier) && candidate.properties.label === name, ) if (duplicate) { errors.push('A folder with this name already exists in this location') } return errors } function setSelectionList(nextIds: EntityIdentifier[]) { selectionList.value = Array.from(new Set(nextIds)) } function openComposer(source?: EntityObject | MessageAddressInterface, mode: ComposerMode = ComposerMode.Fresh) { mailStore.selectMessage(null) composerSource.value = source ?? null composerMode.value = mode composerVisible.value = true } function closeComposer() { composerMode.value = ComposerMode.Fresh composerSource.value = null composerVisible.value = false } function messageSelectionClear() { setSelectionList([]) } function messageSelectionModeActivate(message?: EntityObject) { selectionMode.value = true if (!message) { return } const identifier = message.identifier if (!selectionList.value.includes(identifier)) { setSelectionList([...selectionList.value, identifier]) } } function messageSelectionModeDeactivate() { selectionMode.value = false messageSelectionClear() } function messageSelectionToggleOne(message: EntityObject) { const identifier = message.identifier selectionMode.value = true if (selectionList.value.includes(identifier)) { setSelectionList(selectionList.value.filter(selectedId => selectedId !== identifier)) return } setSelectionList([...selectionList.value, identifier]) } function messageSelectionToggleAll(value: boolean) { selectionMode.value = true if (value) { setSelectionList(mailStore.currentMessages.map(message => message.identifier)) } else { setSelectionList([]) } } function messageSelectionReconcile() { if (!selectedFolder.value) { messageSelectionClear() return } const currentMessageIdentifiers = new Set(mailStore.currentMessages.map(message => message.identifier)) const nextSelectedIds = selectionList.value.filter(identifier => currentMessageIdentifiers.has(identifier)) if (nextSelectedIds.length !== selectionList.value.length) { setSelectionList(nextSelectedIds) } if (nextSelectedIds.length === 0) { selectionMode.value = false } } async function openMoveMessagesDialog(entities?: EntityObject | EntityObject[]) { let moveMessagesServiceIdentifier = null as ServiceIdentifier | null moveMessagesDialogCandidates.value = [] if (entities) { if (Array.isArray(entities)) { moveMessagesDialogCandidates.value = entities.map(entity => entity.identifier) moveMessagesServiceIdentifier = entities[0]?.service as ServiceIdentifier || null } else { moveMessagesDialogCandidates.value = [entities.identifier] moveMessagesServiceIdentifier = entities.service as ServiceIdentifier || null } } else { moveMessagesDialogCandidates.value = [...selectionList.value] moveMessagesServiceIdentifier = selectedFolder.value?.service as ServiceIdentifier || null } moveMessagesDialogService.value = await mailStore.retrieveService(moveMessagesServiceIdentifier) moveMessagesDialogVisible.value = true } function closeMoveMessagesDialog() { moveMessagesDialogVisible.value = false moveMessagesDialogService.value = null moveMessagesDialogCandidates.value = null } async function confirmMoveMessages(targetIdentifier: Parameters[0]) { await mailStore.moveMessages(targetIdentifier, moveMessagesDialogCandidates.value ?? []) messageSelectionModeDeactivate() closeMoveMessagesDialog() } async function deleteSelectedMessages() { await mailStore.deleteMessages([...selectionList.value]) messageSelectionModeDeactivate() } async function flagSelectedMessages(flag: string, value: boolean) { await mailStore.flagMessages([...selectionList.value], { [flag]: value }) messageSelectionModeDeactivate() } return { sidebarVisible, settingsDialogVisible, selectedFolder, selectedMessage, composerMode, composerSource, composerVisible, selectionMode, selectionList, moveMessagesDialogVisible, moveMessagesDialogService, moveMessagesDialogCandidates, createFolderDialogParentLabel, createFolderDialogVisible, createFolderDialogService, createFolderDialogParent, createFolderDialogLoading, createFolderDialogError, renameFolderDialogVisible, renameFolderDialogService, renameFolderDialogFolder, renameFolderDialogParentLabel, renameFolderDialogLoading, renameFolderDialogError, moveFolderDialogVisible, moveFolderDialogService, moveFolderDialogSource, moveFolderDialogInvalidFolderKeys, deleteFolderDialogVisible, deleteFolderDialogService, deleteFolderDialogFolder, deleteFolderDialogLoading, deleteFolderDialogError, sidebarToggle, sidebarHide, settingsOpen, settingsClose, initialize, selectFolder, openComposer, closeComposer, messageSelectionModeActivate, messageSelectionModeDeactivate, messageSelectionToggleOne, messageSelectionToggleAll, messageSelectionClear, validateCreateFolderName, validateRenameFolderName, openMoveMessagesDialog, closeMoveMessagesDialog, confirmMoveMessages, deleteSelectedMessages, openCreateFolderDialog, closeCreateFolderDialog, confirmCreateFolder, openRenameFolderDialog, closeRenameFolderDialog, confirmRenameFolder, openMoveFolderDialog, closeMoveFolderDialog, confirmMoveFolder, openDeleteFolderDialog, closeDeleteFolderDialog, confirmDeleteFolder, flagSelectedMessages, } })