refactor: use module store

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-03-03 21:57:17 -05:00
parent 00cbd15cf7
commit 4fd3042271
4 changed files with 284 additions and 184 deletions

View File

@@ -116,6 +116,11 @@ const readCount = computed(() => {
const totalCount = computed(() => {
return props.selectedCollection?.properties.total ?? 0
})
// True only when the collection explicitly provides total/unread counts
const hasCountData = computed(() => {
return props.selectedCollection?.properties.total != null
})
</script>
<template>
@@ -124,13 +129,13 @@ const totalCount = computed(() => {
<div v-if="selectedCollection" class="message-list-header">
<h2 class="text-h6">{{ selectedCollection.properties.label || 'Folder' }}</h2>
<div class="folder-counts text-caption text-medium-emphasis">
<span v-if="totalCount > 0">
<span v-if="hasCountData">
<span class="unread-count">{{ unreadCount }}</span>
<span class="mx-1">/</span>
<span>{{ totalCount }}</span>
</span>
<span v-else>
Empty
<span v-else-if="messages.length > 0">
{{ messages.length }} loaded
</span>
</div>
</div>

View File

@@ -7,8 +7,7 @@ const integrations: ModuleIntegrations = {
label: 'Mail',
path: '/',
icon: 'mdi-email',
priority: 15,
caption: 'Email client'
priority: 15
},
],
};

View File

@@ -1,22 +1,20 @@
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useDisplay } from 'vuetify'
import { useModuleStore } from '@KTXC'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useMailSync } from '@MailManager/composables/useMailSync'
import type { CollectionObject } from '@MailManager/models/collection'
import type { EntityInterface } from '@MailManager/types/entity'
import type { MessageInterface } from '@MailManager/types/message'
import { useMailStore } from '@/stores/mailStore'
import FolderTree from '@/components/FolderTree.vue'
import MessageList from '@/components/MessageList.vue'
import MessageReader from '@/components/MessageReader.vue'
import MessageComposer from '@/components/MessageComposer.vue'
import SettingsDialog from '@/components/settings/SettingsDialog.vue'
import type { EntityInterface } from '@MailManager/types/entity'
import type { MessageInterface } from '@MailManager/types/message'
// Vuetify display for responsive behavior
const display = useDisplay()
const isMobile = computed(() => display.mdAndDown.value)
// Check if mail manager is available
const moduleStore = useModuleStore()
@@ -24,194 +22,59 @@ const isMailManagerAvailable = computed(() => {
return moduleStore.has('mail_manager') || moduleStore.has('MailManager')
})
// Snackbar state for notifications
const snackbarVisible = ref(false)
const snackbarMessage = ref('')
const snackbarColor = ref('success')
// Mail store — single source of truth for all mail UI state
const mailStore = useMailStore()
// Stores
const collectionsStore = useCollectionsStore()
const entitiesStore = useEntitiesStore()
const servicesStore = useServicesStore()
// storeToRefs preserves reactivity for state and computed properties
const {
sidebarVisible,
settingsDialogVisible,
loading,
selectedFolder,
selectedMessage,
composeMode,
composeReplyTo,
snackbarVisible,
snackbarMessage,
snackbarColor,
currentMessages,
} = storeToRefs(mailStore)
// Background mail sync
const mailSync = useMailSync({
interval: 30000, // Check every 30 seconds
autoStart: false, // We'll start it manually after initialization
fetchDetails: true, // Auto-fetch full message details for new/modified messages
})
// UI state
const sidebarVisible = ref(true)
const selectedFolder = ref<CollectionObject | null>(null)
const selectedMessage = ref<EntityInterface<MessageInterface> | null>(null)
const composeMode = ref(false)
const composeReplyTo = ref<EntityInterface<MessageInterface> | null>(null)
const settingsDialogVisible = ref(false)
// Loading state
const loading = ref(false)
// Computed
const isMobile = computed(() => display.mdAndDown.value)
// Complex store/composable objects accessed directly (not simple refs)
const { mailSync, entitiesStore } = mailStore
// Initialize
onMounted(async () => {
if (!isMailManagerAvailable.value) return
loading.value = true
try {
// Load services (accounts)
await servicesStore.list()
// Load collections (folders)
await collectionsStore.list()
// Select inbox by default if available
const inbox = collectionsStore.collections.find(c => c.properties.role === 'inbox')
if (inbox) {
handleFolderSelect(inbox)
}
// Start background sync after initialization
mailSync.start()
} catch (error) {
console.error('[Mail] Failed to initialize:', error)
} finally {
loading.value = false
}
await mailStore.initialize()
})
// Watch for folder and service changes to update sync sources
watch(
[selectedFolder, () => servicesStore.services],
() => {
if (!isMailManagerAvailable.value) return
// Handlers — thin wrappers that delegate to the store
const handleFolderSelect = (folder: Parameters<typeof mailStore.selectFolder>[0]) =>
mailStore.selectFolder(folder)
mailSync.clearSources()
const handleMessageSelect = (message: EntityInterface<MessageInterface>) =>
mailStore.selectMessage(message, isMobile.value)
// Add currently selected folder to sync
if (selectedFolder.value) {
mailSync.addSource({
provider: selectedFolder.value.provider,
service: selectedFolder.value.service,
collections: [selectedFolder.value.identifier],
})
}
const handleCompose = (replyTo?: EntityInterface<MessageInterface>) =>
mailStore.openCompose(replyTo)
// Add inbox for each service to get notifications
servicesStore.services.forEach(service => {
// Find inbox collection for this service
const inboxes = collectionsStore.collections.filter(
c => c.service === service.identifier &&
(c.properties.role === 'inbox' ||
String(c.identifier).toLowerCase() === 'inbox')
)
const handleComposeClose = () => mailStore.closeCompose()
if (inboxes.length > 0) {
mailSync.addSource({
provider: service.provider,
service: service.identifier as string | number,
collections: inboxes.map(inbox => inbox.identifier),
})
}
})
const handleComposeSent = () => mailStore.afterSent()
// Restart sync with updated sources
if (mailSync.sources.value.length > 0 && !mailSync.isRunning.value) {
mailSync.start()
}
},
{ deep: true }
)
const handleReply = (message: EntityInterface<MessageInterface>) =>
mailStore.openCompose(message)
// Handlers
const handleFolderSelect = async (folder: CollectionObject) => {
selectedFolder.value = folder
selectedMessage.value = null
composeMode.value = false
// Load messages for this folder
try {
await entitiesStore.list({
[folder.provider]: {
[folder.service]: {
[folder.identifier]: true
}
}
})
} catch (error) {
console.error('[Mail] Failed to load messages:', error)
}
}
const handleDelete = (message: EntityInterface<MessageInterface>) =>
mailStore.deleteMessage(message)
const handleMessageSelect = (message: EntityInterface<MessageInterface>) => {
selectedMessage.value = message
composeMode.value = false
// Close sidebar on mobile after selection
if (isMobile.value) {
sidebarVisible.value = false
}
}
const toggleSidebar = () => mailStore.toggleSidebar()
const handleCompose = (replyTo?: EntityInterface<MessageInterface>) => {
composeMode.value = true
composeReplyTo.value = replyTo || null
selectedMessage.value = null
}
const handleSettingsOpen = () => mailStore.openSettings()
const handleComposeClose = () => {
composeMode.value = false
composeReplyTo.value = null
}
const handleComposeSent = () => {
composeMode.value = false
composeReplyTo.value = null
// Reload current folder to show sent message in Sent folder
if (selectedFolder.value) {
handleFolderSelect(selectedFolder.value)
}
}
const handleReply = (message: EntityInterface<MessageInterface>) => {
handleCompose(message)
}
const handleDelete = async (message: EntityInterface<MessageInterface>) => {
// TODO: Implement delete functionality
console.log('[Mail] Delete message:', message.identifier)
}
const toggleSidebar = () => {
sidebarVisible.value = !sidebarVisible.value
}
const handleSettingsOpen = () => {
settingsDialogVisible.value = true
}
const handleFolderCreated = (folder: CollectionObject) => {
snackbarMessage.value = `Folder "${folder.properties.label}" created successfully`
snackbarColor.value = 'success'
snackbarVisible.value = true
// Reload collections to ensure UI is in sync
collectionsStore.list()
}
// Messages for current folder
const currentMessages = computed(() => {
if (!selectedFolder.value) return []
return entitiesStore.entitiesForCollection(
selectedFolder.value.provider,
selectedFolder.value.service,
selectedFolder.value.identifier
)
})
const handleFolderCreated = (folder: Parameters<typeof mailStore.onFolderCreated>[0]) =>
mailStore.onFolderCreated(folder)
</script>
<template>

233
src/stores/mailStore.ts Normal file
View File

@@ -0,0 +1,233 @@
import { ref, computed, shallowRef } from 'vue'
import { defineStore } from 'pinia'
import { useCollectionsStore } from '@MailManager/stores/collectionsStore'
import { useEntitiesStore } from '@MailManager/stores/entitiesStore'
import { useServicesStore } from '@MailManager/stores/servicesStore'
import { useMailSync } from '@MailManager/composables/useMailSync'
import type { CollectionObject } from '@MailManager/models/collection'
import type { EntityInterface } from '@MailManager/types/entity'
import type { MessageInterface } from '@MailManager/types/message'
export const useMailStore = defineStore('mailStore', () => {
const collectionsStore = useCollectionsStore()
const entitiesStore = useEntitiesStore()
const servicesStore = useServicesStore()
// Background mail sync
const mailSync = useMailSync({
interval: 30000,
autoStart: false,
fetchDetails: true,
})
// ── UI State ──────────────────────────────────────────────────────────────
const sidebarVisible = ref(true)
const settingsDialogVisible = ref(false)
const loading = ref(false)
// ── Selection State ───────────────────────────────────────────────────────
const selectedFolder = shallowRef<CollectionObject | null>(null)
const selectedMessage = shallowRef<EntityInterface<MessageInterface> | null>(null)
// ── Compose State ─────────────────────────────────────────────────────────
const composeMode = ref(false)
const composeReplyTo = shallowRef<EntityInterface<MessageInterface> | null>(null)
// ── Notification State ────────────────────────────────────────────────────
const snackbarVisible = ref(false)
const snackbarMessage = ref('')
const snackbarColor = ref<'success' | 'error' | 'info' | 'warning'>('success')
// ── Computed ──────────────────────────────────────────────────────────────
const currentMessages = computed(() => {
if (!selectedFolder.value) return []
const folder = selectedFolder.value
// Access entitiesStore.entities (reactive computed array) so Vue tracks it
return entitiesStore.entities.filter(e =>
e.provider === folder.provider &&
String(e.service) === String(folder.service) &&
String(e.collection) === String(folder.identifier),
)
})
// ── Sync Helpers ──────────────────────────────────────────────────────────
function _updateSyncSources() {
mailSync.clearSources()
// Track the currently selected folder
if (selectedFolder.value) {
mailSync.addSource({
provider: selectedFolder.value.provider,
service: selectedFolder.value.service,
collections: [selectedFolder.value.identifier],
})
}
// Always track inboxes for each account (for new-mail notifications)
servicesStore.services.forEach(service => {
const inboxes = collectionsStore.collections.filter(
c =>
c.service === service.identifier &&
(c.properties.role === 'inbox' ||
String(c.identifier).toLowerCase() === 'inbox'),
)
if (inboxes.length > 0) {
mailSync.addSource({
provider: service.provider,
service: service.identifier as string | number,
collections: inboxes.map(inbox => inbox.identifier),
})
}
})
if (mailSync.sources.value.length > 0 && !mailSync.isRunning.value) {
mailSync.start()
}
}
// ── Actions ───────────────────────────────────────────────────────────────
async function selectFolder(folder: CollectionObject) {
selectedFolder.value = folder
selectedMessage.value = null
composeMode.value = false
try {
await entitiesStore.list({
[folder.provider]: {
[folder.service]: {
[folder.identifier]: true,
},
},
})
} catch (error) {
console.error('[Mail] Failed to load messages:', error)
}
_updateSyncSources()
}
function selectMessage(message: EntityInterface<MessageInterface>, closeSidebar = false) {
selectedMessage.value = message
composeMode.value = false
if (closeSidebar) {
sidebarVisible.value = false
}
}
function openCompose(replyTo?: EntityInterface<MessageInterface>) {
composeMode.value = true
composeReplyTo.value = replyTo ?? null
selectedMessage.value = null
}
function closeCompose() {
composeMode.value = false
composeReplyTo.value = null
}
async function afterSent() {
composeMode.value = false
composeReplyTo.value = null
// Reload the current folder so the sent message appears in Sent
if (selectedFolder.value) {
await selectFolder(selectedFolder.value)
}
}
async function deleteMessage(message: EntityInterface<MessageInterface>) {
// TODO: implement delete via entity / collection store
console.log('[Mail] Delete message:', message.identifier)
}
function toggleSidebar() {
sidebarVisible.value = !sidebarVisible.value
}
function openSettings() {
settingsDialogVisible.value = true
}
function notify(message: string, color: typeof snackbarColor.value = 'success') {
snackbarMessage.value = message
snackbarColor.value = color
snackbarVisible.value = true
}
async function onFolderCreated(folder: CollectionObject) {
notify(`Folder "${folder.properties.label}" created successfully`)
// Reload collections so the sidebar reflects the new folder
await collectionsStore.list()
}
// ── Initialization ────────────────────────────────────────────────────────
async function initialize() {
loading.value = true
try {
await servicesStore.list()
await collectionsStore.list()
// Select inbox by default
const inbox = collectionsStore.collections.find(c => c.properties.role === 'inbox')
if (inbox) {
await selectFolder(inbox)
}
mailSync.start()
} catch (error) {
console.error('[Mail] Failed to initialize:', error)
} finally {
loading.value = false
}
}
// ── Exports ───────────────────────────────────────────────────────────────
return {
// Sub-stores (forwarded for template convenience)
collectionsStore,
entitiesStore,
servicesStore,
mailSync,
// State
sidebarVisible,
settingsDialogVisible,
loading,
selectedFolder,
selectedMessage,
composeMode,
composeReplyTo,
snackbarVisible,
snackbarMessage,
snackbarColor,
// Computed
currentMessages,
// Actions
selectFolder,
selectMessage,
openCompose,
closeCompose,
afterSent,
deleteMessage,
toggleSidebar,
openSettings,
notify,
onFolderCreated,
initialize,
}
})