@@ -5,7 +5,7 @@ import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
|
||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||
import { useMailSync } from '@MailManager/composables/useMailSync'
|
||||
import { useSnackbar } from '@KTXC'
|
||||
import type { CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||
import type { ServiceIdentifier, CollectionIdentifier, EntityIdentifier } from '@MailManager/types/common'
|
||||
import type { ServiceObject, CollectionObject, EntityObject } from '@MailManager/models'
|
||||
|
||||
export const useMailStore = defineStore('mailStore', () => {
|
||||
@@ -41,8 +41,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
// ── Selection State ───────────────────────────────────────────────────────
|
||||
const selectedFolder = shallowRef<CollectionObject | null>(null)
|
||||
const selectedMessage = shallowRef<EntityObject | null>(null)
|
||||
const selectedMessageIds = ref<EntityIdentifier[]>([])
|
||||
const selectionModeActive = ref(false)
|
||||
const selectionMode = ref(false)
|
||||
const selectionList = ref<EntityIdentifier[]>([])
|
||||
|
||||
// ── Compose State ─────────────────────────────────────────────────────────
|
||||
const composeMode = ref(false)
|
||||
@@ -50,7 +50,8 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
|
||||
// ── Move State ────────────────────────────────────────────────────────────
|
||||
const moveDialogVisible = ref(false)
|
||||
const moveMessageCandidates = shallowRef<EntityObject[]>([])
|
||||
const moveDialogService = ref<ServiceIdentifier | null>(null)
|
||||
const moveDialogCandidates = ref<EntityIdentifier[] | null>(null)
|
||||
|
||||
// ── Computed ──────────────────────────────────────────────────────────────
|
||||
const currentMessages = computed(() => {
|
||||
@@ -65,31 +66,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 ────────────────────────────────────────────────────────
|
||||
|
||||
async function initialize() {
|
||||
@@ -162,36 +138,36 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
function _serviceKey(provider: string, service: string | number) {
|
||||
return `${provider}:${String(service)}`
|
||||
function _serviceIdentifier(item: ServiceObject | CollectionObject | EntityObject | { provider: string, service: string | number }): ServiceIdentifier {
|
||||
return `${item.provider}:${String(item.service)}` as ServiceIdentifier
|
||||
}
|
||||
|
||||
function _collectionIdentifier(collection: CollectionObject): CollectionIdentifier {
|
||||
return `${collection.provider}:${String(collection.service)}:${String(collection.identifier)}` as CollectionIdentifier
|
||||
function _collectionIdentifier(item: CollectionObject | EntityObject): CollectionIdentifier {
|
||||
return `${item.provider}:${String(item.service)}:${String(item.identifier)}` as CollectionIdentifier
|
||||
}
|
||||
|
||||
function _entityIdentifier(entity: EntityObject): EntityIdentifier {
|
||||
return `${entity.provider}:${String(entity.service)}:${String(entity.collection)}:${String(entity.identifier)}` as EntityIdentifier
|
||||
function _entityIdentifier(item: EntityObject): EntityIdentifier {
|
||||
return `${item.provider}:${String(item.service)}:${String(item.collection)}:${String(item.identifier)}` as EntityIdentifier
|
||||
}
|
||||
|
||||
function _setServiceFolderLoading(provider: string, service: string | number, loadingState: boolean) {
|
||||
serviceFolderLoadingState.value = {
|
||||
...serviceFolderLoadingState.value,
|
||||
[_serviceKey(provider, service)]: loadingState,
|
||||
[_serviceIdentifier({ provider, service })]: loadingState,
|
||||
}
|
||||
}
|
||||
|
||||
function _setServiceFolderLoaded(provider: string, service: string | number, loaded: boolean) {
|
||||
serviceFolderLoadedState.value = {
|
||||
...serviceFolderLoadedState.value,
|
||||
[_serviceKey(provider, service)]: loaded,
|
||||
[_serviceIdentifier({ provider, service })]: loaded,
|
||||
}
|
||||
}
|
||||
|
||||
function _setServiceFolderError(provider: string, service: string | number, error: string | null) {
|
||||
serviceFolderErrorState.value = {
|
||||
...serviceFolderErrorState.value,
|
||||
[_serviceKey(provider, service)]: error,
|
||||
[_serviceIdentifier({ provider, service })]: error,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,22 +207,15 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
}
|
||||
|
||||
function isServiceFolderLoading(provider: string, service: string | number) {
|
||||
return serviceFolderLoadingState.value[_serviceKey(provider, service)] === true
|
||||
return serviceFolderLoadingState.value[_serviceIdentifier({ provider, service })] === true
|
||||
}
|
||||
|
||||
function hasServiceFoldersLoaded(provider: string, service: string | number) {
|
||||
return serviceFolderLoadedState.value[_serviceKey(provider, service)] === true
|
||||
return serviceFolderLoadedState.value[_serviceIdentifier({ provider, service })] === true
|
||||
}
|
||||
|
||||
function getServiceFolderError(provider: string, service: string | number) {
|
||||
return serviceFolderErrorState.value[_serviceKey(provider, service)] ?? null
|
||||
}
|
||||
|
||||
function _serviceFor(provider: string, serviceIdentifier: string | number) {
|
||||
return servicesStore.services.find(service =>
|
||||
service.provider === provider &&
|
||||
String(service.identifier) === String(serviceIdentifier),
|
||||
) ?? null
|
||||
return serviceFolderErrorState.value[_serviceIdentifier({ provider, service })] ?? null
|
||||
}
|
||||
|
||||
function _reloadFolderMessages(folder: CollectionObject) {
|
||||
@@ -259,17 +228,12 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function _setSelectedMessageIds(nextIds: EntityIdentifier[]) {
|
||||
selectedMessageIds.value = Array.from(new Set(nextIds))
|
||||
}
|
||||
function _setSelectionList(nextIds: EntityIdentifier[]) {
|
||||
selectionList.value = Array.from(new Set(nextIds))
|
||||
|
||||
function _removeSelection(sourceIdentifiers: EntityIdentifier[]) {
|
||||
if (sourceIdentifiers.length === 0 || selectedMessageIds.value.length === 0) {
|
||||
return
|
||||
if (selectionList.value.length === 0) {
|
||||
selectionMode.value = false
|
||||
}
|
||||
|
||||
const removedIds = new Set(sourceIdentifiers)
|
||||
selectedMessageIds.value = selectedMessageIds.value.filter(identifier => !removedIds.has(identifier))
|
||||
}
|
||||
|
||||
function _reconcileSelection() {
|
||||
@@ -280,10 +244,10 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
}
|
||||
|
||||
const currentMessageIdentifiers = new Set(currentMessages.value.map(message => _entityIdentifier(message)))
|
||||
const nextSelectedIds = selectedMessageIds.value.filter(identifier => currentMessageIdentifiers.has(identifier))
|
||||
const nextSelectedIds = selectionList.value.filter(identifier => currentMessageIdentifiers.has(identifier))
|
||||
|
||||
if (nextSelectedIds.length !== selectedMessageIds.value.length) {
|
||||
selectedMessageIds.value = nextSelectedIds
|
||||
if (nextSelectedIds.length !== selectionList.value.length) {
|
||||
_setSelectionList(nextSelectedIds)
|
||||
}
|
||||
|
||||
if (selectedMessage.value && !currentMessageIdentifiers.has(_entityIdentifier(selectedMessage.value))) {
|
||||
@@ -321,7 +285,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
selectedFolder.value = folder
|
||||
selectedMessage.value = null
|
||||
clearSelection()
|
||||
selectionModeActive.value = false
|
||||
selectionMode.value = false
|
||||
composeMode.value = false
|
||||
|
||||
try {
|
||||
@@ -347,82 +311,7 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
composeReplyTo.value = replyTo ?? null
|
||||
selectedMessage.value = null
|
||||
}
|
||||
|
||||
function isMessageSelected(message: EntityObject) {
|
||||
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
|
||||
}
|
||||
|
||||
function openMoveDialogForSelection() {
|
||||
if (selectedMessages.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
openMoveDialog(selectedMessages.value)
|
||||
}
|
||||
|
||||
function closeMoveDialog() {
|
||||
moveDialogVisible.value = false
|
||||
moveMessageCandidates.value = []
|
||||
}
|
||||
|
||||
|
||||
function closeCompose() {
|
||||
composeMode.value = false
|
||||
composeReplyTo.value = null
|
||||
@@ -438,20 +327,91 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function moveMessages(targetFolder: CollectionObject) {
|
||||
const candidates = moveMessageCandidates.value
|
||||
function isMessageSelected(message: EntityObject) {
|
||||
return selectionList.value.includes(_entityIdentifier(message))
|
||||
}
|
||||
|
||||
function toggleMessageSelection(message: EntityObject) {
|
||||
const identifier = _entityIdentifier(message)
|
||||
|
||||
selectionMode.value = true
|
||||
|
||||
if (selectionList.value.includes(identifier)) {
|
||||
_setSelectionList(selectionList.value.filter(selectedId => selectedId !== identifier))
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const movableCandidates = candidates.filter(message => !(
|
||||
targetFolder.provider === message.provider &&
|
||||
String(targetFolder.service) === String(message.service) &&
|
||||
String(targetFolder.identifier) === String(message.collection)
|
||||
))
|
||||
_setSelectionList([...selectionList.value, identifier])
|
||||
}
|
||||
|
||||
if (movableCandidates.length === 0) {
|
||||
function selectAllCurrentMessages() {
|
||||
selectionMode.value = true
|
||||
_setSelectionList(currentMessages.value.map(message => _entityIdentifier(message)))
|
||||
}
|
||||
|
||||
function activateSelectionMode(message?: EntityObject) {
|
||||
selectionMode.value = true
|
||||
|
||||
if (message) {
|
||||
const identifier = _entityIdentifier(message)
|
||||
|
||||
if (!selectionList.value.includes(identifier)) {
|
||||
_setSelectionList([...selectionList.value, identifier])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deactivateSelectionMode() {
|
||||
selectionMode.value = false
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
_setSelectionList([])
|
||||
}
|
||||
|
||||
function openMoveDialog(entities?: EntityObject | EntityObject[]) {
|
||||
|
||||
moveDialogCandidates.value = []
|
||||
|
||||
if (entities) {
|
||||
if (Array.isArray(entities)) {
|
||||
moveDialogCandidates.value = entities.map(entity => _entityIdentifier(entity))
|
||||
moveDialogService.value = _serviceIdentifier(entities[0])
|
||||
} else {
|
||||
moveDialogCandidates.value = [_entityIdentifier(entities)]
|
||||
moveDialogService.value = _serviceIdentifier(entities)
|
||||
}
|
||||
} else {
|
||||
moveDialogCandidates.value = selectionList.value
|
||||
moveDialogService.value = _serviceIdentifier(selectedFolder.value)
|
||||
}
|
||||
|
||||
moveDialogVisible.value = true
|
||||
}
|
||||
|
||||
function closeMoveDialog() {
|
||||
moveDialogVisible.value = false
|
||||
moveDialogService.value = null
|
||||
moveDialogCandidates.value = null
|
||||
}
|
||||
|
||||
async function moveMessages(target: CollectionObject, entityIdentifiers: EntityIdentifier[]) {
|
||||
const movableIdentifiers = entityIdentifiers.filter(identifier => {
|
||||
const entity = entitiesStore.entityByIdentifier(identifier)
|
||||
|
||||
if (!entity) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only allow moving messages within the same service and disallow moving into the same folder
|
||||
return entity.provider === target.provider &&
|
||||
String(entity.service) === String(target.service) &&
|
||||
String(entity.collection) !== String(target.identifier)
|
||||
})
|
||||
|
||||
if (movableIdentifiers.length === 0) {
|
||||
closeMoveDialog()
|
||||
return
|
||||
}
|
||||
@@ -459,55 +419,31 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const sourceIdentifiers = movableCandidates.map(message => _entityIdentifier(message))
|
||||
const response = await entitiesStore.move(_collectionIdentifier(targetFolder), sourceIdentifiers)
|
||||
const successfulMoves: EntityIdentifier[] = []
|
||||
const failedMoves: string[] = []
|
||||
const response = await entitiesStore.move(_collectionIdentifier(target), movableIdentifiers)
|
||||
const operationSucceeded: EntityIdentifier[] = []
|
||||
const operationFailures: EntityIdentifier[] = []
|
||||
|
||||
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
||||
if (result.success) {
|
||||
successfulMoves.push(sourceIdentifier as EntityIdentifier)
|
||||
operationSucceeded.push(sourceIdentifier as EntityIdentifier)
|
||||
return
|
||||
}
|
||||
|
||||
failedMoves.push(result.error)
|
||||
operationFailures.push(sourceIdentifier as EntityIdentifier)
|
||||
})
|
||||
|
||||
if (successfulMoves.length === 0) {
|
||||
throw new Error(failedMoves[0] ?? 'Failed to move messages')
|
||||
if (operationSucceeded.length === 0) {
|
||||
throw new Error(operationFailures[0] ?? 'Failed to move messages')
|
||||
}
|
||||
|
||||
_removeSelection(successfulMoves)
|
||||
|
||||
if (selectedMessage.value && successfulMoves.includes(_entityIdentifier(selectedMessage.value))) {
|
||||
if (selectedMessage.value && operationSucceeded.includes(_entityIdentifier(selectedMessage.value))) {
|
||||
selectedMessage.value = null
|
||||
}
|
||||
|
||||
if (selectedMessageIds.value.length === 0) {
|
||||
selectionModeActive.value = false
|
||||
}
|
||||
|
||||
clearSelection()
|
||||
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)
|
||||
const notification = _formatMoveNotification(operationSucceeded.length, operationFailures.length, target)
|
||||
notify(notification.message, notification.color)
|
||||
} catch (error) {
|
||||
const messageText = error instanceof Error ? error.message : 'Failed to move messages'
|
||||
@@ -519,9 +455,61 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMessage(message: EntityObject) {
|
||||
// TODO: implement delete via entity / collection store
|
||||
console.log('[Mail] Delete message:', message.identifier)
|
||||
async function deleteMessages(entityIdentifiers: EntityIdentifier[]) {
|
||||
if (entityIdentifiers.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await entitiesStore.delete(entityIdentifiers)
|
||||
const operationSucceeded: EntityIdentifier[] = []
|
||||
const operationFailures: EntityIdentifier[] = []
|
||||
|
||||
Object.entries(response).forEach(([sourceIdentifier, result]) => {
|
||||
if (result.success) {
|
||||
operationSucceeded.push(sourceIdentifier as EntityIdentifier)
|
||||
return
|
||||
}
|
||||
|
||||
operationFailures.push(sourceIdentifier as EntityIdentifier)
|
||||
})
|
||||
|
||||
if (operationSucceeded.length === 0) {
|
||||
throw new Error(operationFailures[0] ?? 'Failed to delete messages')
|
||||
}
|
||||
|
||||
if (selectedMessage.value && operationSucceeded.includes(_entityIdentifier(selectedMessage.value))) {
|
||||
selectedMessage.value = null
|
||||
}
|
||||
|
||||
clearSelection()
|
||||
|
||||
const successCount = operationSucceeded.length
|
||||
const failureCount = operationFailures.length
|
||||
|
||||
if (failureCount === 0) {
|
||||
notify(
|
||||
successCount === 1 ? 'Message deleted' : `${successCount} messages deleted`,
|
||||
'success',
|
||||
)
|
||||
} else {
|
||||
notify(
|
||||
successCount === 0
|
||||
? `Delete failed for ${failureCount === 1 ? '1 message' : `${failureCount} messages`}`
|
||||
: `Deleted ${successCount} ${successCount === 1 ? 'message' : 'messages'}. ${failureCount} failed.`,
|
||||
successCount === 0 ? 'error' : 'warning',
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
const messageText = error instanceof Error ? error.message : 'Failed to delete messages'
|
||||
console.error('[Mail] Failed to delete messages:', error)
|
||||
notify(messageText, 'error')
|
||||
throw error
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
@@ -551,23 +539,19 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
loading,
|
||||
selectedFolder,
|
||||
selectedMessage,
|
||||
selectedMessageIds,
|
||||
selectionModeActive,
|
||||
selectionList,
|
||||
selectionMode,
|
||||
composeMode,
|
||||
composeReplyTo,
|
||||
moveDialogVisible,
|
||||
moveMessageCandidates,
|
||||
moveDialogService,
|
||||
moveDialogCandidates,
|
||||
serviceFolderLoadingState,
|
||||
serviceFolderLoadedState,
|
||||
serviceFolderErrorState,
|
||||
|
||||
// Computed
|
||||
currentMessages,
|
||||
selectedMessageMap,
|
||||
selectedMessages,
|
||||
selectionCount,
|
||||
hasSelection,
|
||||
allCurrentMessagesSelected,
|
||||
|
||||
// Actions
|
||||
selectFolder,
|
||||
@@ -580,11 +564,10 @@ export const useMailStore = defineStore('mailStore', () => {
|
||||
clearSelection,
|
||||
openCompose,
|
||||
openMoveDialog,
|
||||
openMoveDialogForSelection,
|
||||
closeMoveDialog,
|
||||
closeCompose,
|
||||
afterSent,
|
||||
deleteMessage,
|
||||
deleteMessages,
|
||||
moveMessages,
|
||||
toggleSidebar,
|
||||
openSettings,
|
||||
|
||||
Reference in New Issue
Block a user