534 lines
16 KiB
TypeScript
534 lines
16 KiB
TypeScript
import { computed, ref, shallowRef, watch } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
|
|
import { useMailStore } from '@/stores/mailStore'
|
|
import type { ServiceIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
|
import type { EntityObject, ServiceObject } from '@MailManager/models'
|
|
import type { CollectionObject } from '@MailManager/models/collection'
|
|
|
|
export const useMailUiStore = defineStore('mailUiStore', () => {
|
|
const collectionsStore = useCollectionsStore()
|
|
const mailStore = useMailStore()
|
|
|
|
const sidebarVisible = ref(true)
|
|
const settingsDialogVisible = ref(false)
|
|
const composeMode = ref<'new' | 'reply' | 'forward'>('new')
|
|
const composeSource = shallowRef<EntityObject | null>(null)
|
|
const composeVisible = ref(false)
|
|
const selectionMode = ref(false)
|
|
const selectionList = ref<EntityIdentifier[]>([])
|
|
const moveMessagesDialogVisible = ref(false)
|
|
const moveMessagesDialogService = shallowRef<ServiceObject | null>(null)
|
|
const moveMessagesDialogCandidates = ref<EntityIdentifier[] | null>(null)
|
|
const createFolderDialogVisible = ref(false)
|
|
const createFolderDialogService = shallowRef<ServiceObject | null>(null)
|
|
const createFolderDialogParent = shallowRef<CollectionObject | null>(null)
|
|
const createFolderDialogLoading = ref(false)
|
|
const createFolderDialogError = ref('')
|
|
const renameFolderDialogVisible = ref(false)
|
|
const renameFolderDialogService = shallowRef<ServiceObject | null>(null)
|
|
const renameFolderDialogFolder = shallowRef<CollectionObject | null>(null)
|
|
const renameFolderDialogLoading = ref(false)
|
|
const renameFolderDialogError = ref('')
|
|
const moveFolderDialogVisible = ref(false)
|
|
const moveFolderDialogService = shallowRef<ServiceObject | null>(null)
|
|
const moveFolderDialogSource = shallowRef<CollectionObject | null>(null)
|
|
const deleteFolderDialogVisible = ref(false)
|
|
const deleteFolderDialogService = shallowRef<ServiceObject | null>(null)
|
|
const deleteFolderDialogFolder = shallowRef<CollectionObject | null>(null)
|
|
const deleteFolderDialogLoading = ref(false)
|
|
const deleteFolderDialogError = ref('')
|
|
|
|
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<string>()
|
|
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.selectedFolder,
|
|
() => {
|
|
closeCompose()
|
|
deactivateSelectionMode()
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => mailStore.selectedMessage,
|
|
selectedMessage => {
|
|
if (selectedMessage) {
|
|
closeCompose()
|
|
}
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => mailStore.currentMessages,
|
|
() => {
|
|
reconcileSelection()
|
|
},
|
|
)
|
|
|
|
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 openCompose(source?: EntityObject, mode: 'reply' | 'forward' = 'reply') {
|
|
mailStore.clearSelectedMessage()
|
|
composeSource.value = source ?? null
|
|
composeMode.value = mode
|
|
composeVisible.value = true
|
|
}
|
|
|
|
function closeCompose() {
|
|
composeMode.value = 'new'
|
|
composeSource.value = null
|
|
composeVisible.value = false
|
|
}
|
|
|
|
async function afterSent() {
|
|
closeCompose()
|
|
await mailStore.reloadSelectedFolder()
|
|
}
|
|
|
|
function clearSelection() {
|
|
setSelectionList([])
|
|
}
|
|
|
|
function activateSelectionMode(message?: EntityObject) {
|
|
selectionMode.value = true
|
|
|
|
if (!message) {
|
|
return
|
|
}
|
|
|
|
const identifier = message.identifier
|
|
|
|
if (!selectionList.value.includes(identifier)) {
|
|
setSelectionList([...selectionList.value, identifier])
|
|
}
|
|
}
|
|
|
|
function deactivateSelectionMode() {
|
|
selectionMode.value = false
|
|
clearSelection()
|
|
}
|
|
|
|
function toggleMessageSelection(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 selectAllCurrentMessages() {
|
|
selectionMode.value = true
|
|
setSelectionList(mailStore.currentMessages.map(message => message.identifier))
|
|
}
|
|
|
|
function reconcileSelection() {
|
|
if (!mailStore.selectedFolder) {
|
|
clearSelection()
|
|
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
|
|
}
|
|
}
|
|
|
|
function toggleSidebar() {
|
|
sidebarVisible.value = !sidebarVisible.value
|
|
}
|
|
|
|
function closeSidebar() {
|
|
sidebarVisible.value = false
|
|
}
|
|
|
|
function openSettings() {
|
|
settingsDialogVisible.value = true
|
|
}
|
|
|
|
function closeSettings() {
|
|
settingsDialogVisible.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 = mailStore.selectedFolder?.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<typeof mailStore.moveMessages>[0]) {
|
|
await mailStore.moveMessages(targetIdentifier, moveMessagesDialogCandidates.value ?? [])
|
|
deactivateSelectionMode()
|
|
closeMoveMessagesDialog()
|
|
}
|
|
|
|
async function deleteSelectedMessages() {
|
|
await mailStore.deleteMessages([...selectionList.value])
|
|
deactivateSelectionMode()
|
|
}
|
|
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
|
}
|
|
}
|
|
|
|
return {
|
|
sidebarVisible,
|
|
settingsDialogVisible,
|
|
composeMode,
|
|
composeSource,
|
|
composeVisible,
|
|
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,
|
|
toggleSidebar,
|
|
closeSidebar,
|
|
openSettings,
|
|
closeSettings,
|
|
openCompose,
|
|
closeCompose,
|
|
afterSent,
|
|
activateSelectionMode,
|
|
deactivateSelectionMode,
|
|
toggleMessageSelection,
|
|
selectAllCurrentMessages,
|
|
clearSelection,
|
|
validateCreateFolderName,
|
|
validateRenameFolderName,
|
|
openMoveMessagesDialog,
|
|
closeMoveMessagesDialog,
|
|
confirmMoveMessages,
|
|
deleteSelectedMessages,
|
|
openCreateFolderDialog,
|
|
closeCreateFolderDialog,
|
|
confirmCreateFolder,
|
|
openRenameFolderDialog,
|
|
closeRenameFolderDialog,
|
|
confirmRenameFolder,
|
|
openMoveFolderDialog,
|
|
closeMoveFolderDialog,
|
|
confirmMoveFolder,
|
|
openDeleteFolderDialog,
|
|
closeDeleteFolderDialog,
|
|
confirmDeleteFolder,
|
|
}
|
|
}) |