Merge pull request 'feat: implement standardized protocol' (#2) from feat/implement-stanardized-protocol into main

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-02-14 16:56:03 +00:00
7 changed files with 28 additions and 39 deletions

9
package-lock.json generated
View File

@@ -7,6 +7,7 @@
"": { "": {
"name": "@ktrix/mail", "name": "@ktrix/mail",
"version": "0.0.1", "version": "0.0.1",
"license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"@tiptap/extension-link": "^2.1.13", "@tiptap/extension-link": "^2.1.13",
"@tiptap/extension-placeholder": "^2.1.13", "@tiptap/extension-placeholder": "^2.1.13",
@@ -841,7 +842,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz",
"integrity": "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==", "integrity": "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
@@ -1198,7 +1198,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz",
"integrity": "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==", "integrity": "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prosemirror-changeset": "^2.3.0", "prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1", "prosemirror-collab": "^1.3.1",
@@ -1913,7 +1912,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"orderedmap": "^2.0.0" "orderedmap": "^2.0.0"
} }
@@ -1943,7 +1941,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prosemirror-model": "^1.0.0", "prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0", "prosemirror-transform": "^1.0.0",
@@ -1992,7 +1989,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==", "integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prosemirror-model": "^1.20.0", "prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0", "prosemirror-state": "^1.0.0",
@@ -2096,7 +2092,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -2117,7 +2112,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@@ -2177,7 +2171,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.26", "@vue/compiler-dom": "3.5.26",
"@vue/compiler-sfc": "3.5.26", "@vue/compiler-sfc": "3.5.26",

View File

@@ -140,12 +140,12 @@ const handleCreate = async () => {
properties.subscribed = true properties.subscribed = true
// Create the collection // Create the collection
const newFolder = await collectionsStore.createCollection({ const newFolder = await collectionsStore.create(
provider: props.service.provider, props.service.provider,
service: props.service.identifier, props.service.identifier as string | number,
collection: props.parentFolder?.identifier ?? null, props.parentFolder?.identifier ?? null,
properties: properties properties
}) )
// Success! // Success!
emit('created', newFolder) emit('created', newFolder)

View File

@@ -33,9 +33,7 @@ const { settings } = useUser()
// Folder view mode from user settings // Folder view mode from user settings
const folderViewMode = computed(() => { const folderViewMode = computed(() => {
const allSettings = settings.value?.all || {} return (settings.value.get('mail.folderViewMode') as FolderViewMode) || 'tree'
const mailSettings = allSettings.mail || {}
return (mailSettings.folderViewMode as FolderViewMode) || 'tree'
}) })
// Create folder dialog state // Create folder dialog state
@@ -112,7 +110,7 @@ const serviceGroups = computed(() => {
}> = [] }> = []
servicesStore.services.forEach(service => { servicesStore.services.forEach(service => {
const folders = collectionsStore.collectionList.filter( const folders = collectionsStore.collections.filter(
c => c.provider === service.provider && String(c.service) === String(service.identifier) c => c.provider === service.provider && String(c.service) === String(service.identifier)
) )

View File

@@ -131,11 +131,11 @@ const saveDraft = async () => {
provider: props.folder.provider, provider: props.folder.provider,
service: props.folder.service, service: props.folder.service,
collection: props.folder.identifier, // Should be drafts folder ID collection: props.folder.identifier, // Should be drafts folder ID
data: draftData, properties: draftData,
}) })
if (response && response.entity) { if (response) {
draftId.value = response.entity.identifier draftId.value = String(response.identifier)
} }
lastSaved.value = new Date() lastSaved.value = new Date()
@@ -187,7 +187,7 @@ const handleSend = async () => {
sending.value = true sending.value = true
try { try {
await entityService.send({ await entityService.transmit({
message: { message: {
to: to.value, to: to.value,
cc: cc.value.length > 0 ? cc.value : undefined, cc: cc.value.length > 0 ? cc.value : undefined,
@@ -203,7 +203,7 @@ const handleSend = async () => {
// Delete draft if it was saved // Delete draft if it was saved
if (draftId.value && props.folder) { if (draftId.value && props.folder) {
try { try {
await entityService.destroy({ await entityService.delete({
provider: props.folder.provider, provider: props.folder.provider,
service: props.folder.service, service: props.folder.service,
collection: props.folder.identifier, collection: props.folder.identifier,

View File

@@ -12,9 +12,7 @@ const compactMode = ref(false)
const folderViewMode = computed({ const folderViewMode = computed({
get: () => { get: () => {
const allSettings = settings.value?.all || {} return (settings.value.get('mail.folderViewMode') as FolderViewMode) || 'tree'
const mailSettings = allSettings.mail || {}
return (mailSettings.folderViewMode as FolderViewMode) || 'tree'
}, },
set: (value: FolderViewMode) => setSetting('mail.folderViewMode', value) set: (value: FolderViewMode) => setSetting('mail.folderViewMode', value)
}) })

View File

@@ -56,10 +56,10 @@ onMounted(async () => {
await servicesStore.list() await servicesStore.list()
// Load collections (folders) // Load collections (folders)
await collectionsStore.loadCollections() await collectionsStore.list()
// Select inbox by default if available // Select inbox by default if available
const inbox = collectionsStore.collectionList.find(c => c.properties.role === 'inbox') const inbox = collectionsStore.collections.find(c => c.properties.role === 'inbox')
if (inbox) { if (inbox) {
handleFolderSelect(inbox) handleFolderSelect(inbox)
} }
@@ -91,7 +91,7 @@ watch(
// Add inbox for each service to get notifications // Add inbox for each service to get notifications
servicesStore.services.forEach(service => { servicesStore.services.forEach(service => {
// Find inbox collection for this service // Find inbox collection for this service
const inboxes = collectionsStore.collectionList.filter( const inboxes = collectionsStore.collections.filter(
c => c.service === service.identifier && c => c.service === service.identifier &&
(c.properties.role === 'inbox' || (c.properties.role === 'inbox' ||
String(c.identifier).toLowerCase() === 'inbox') String(c.identifier).toLowerCase() === 'inbox')
@@ -100,7 +100,7 @@ watch(
if (inboxes.length > 0) { if (inboxes.length > 0) {
mailSync.addSource({ mailSync.addSource({
provider: service.provider, provider: service.provider,
service: service.identifier, service: service.identifier as string | number,
collections: inboxes.map(inbox => inbox.identifier), collections: inboxes.map(inbox => inbox.identifier),
}) })
} }
@@ -122,7 +122,7 @@ const handleFolderSelect = async (folder: CollectionObject) => {
// Load messages for this folder // Load messages for this folder
try { try {
await entitiesStore.loadMessages({ await entitiesStore.list({
[folder.provider]: { [folder.provider]: {
[folder.service]: { [folder.service]: {
[folder.identifier]: true [folder.identifier]: true
@@ -188,19 +188,18 @@ const handleFolderCreated = (folder: CollectionObject) => {
snackbarVisible.value = true snackbarVisible.value = true
// Reload collections to ensure UI is in sync // Reload collections to ensure UI is in sync
collectionsStore.loadCollections() collectionsStore.list()
} }
// Messages for current folder // Messages for current folder
const currentMessages = computed(() => { const currentMessages = computed(() => {
if (!selectedFolder.value) return [] if (!selectedFolder.value) return []
const provider = selectedFolder.value.provider return entitiesStore.entitiesForCollection(
const service = String(selectedFolder.value.service) selectedFolder.value.provider,
const collection = String(selectedFolder.value.identifier) selectedFolder.value.service,
selectedFolder.value.identifier
const messages = entitiesStore.messages[provider]?.[service]?.[collection] )
return messages ? Object.values(messages) : []
}) })
</script> </script>
@@ -230,7 +229,7 @@ const currentMessages = computed(() => {
<v-btn <v-btn
icon="mdi-refresh" icon="mdi-refresh"
@click="mailSync.sync()" @click="mailSync.sync()"
:loading="mailSync.isRunning.value && entitiesStore.loading" :loading="mailSync.isRunning.value && entitiesStore.transceiving"
variant="text" variant="text"
> >
<v-icon>mdi-refresh</v-icon> <v-icon>mdi-refresh</v-icon>

View File

@@ -47,6 +47,7 @@ export default defineConfig({
'vue', 'vue',
'vue-router', 'vue-router',
'pinia', 'pinia',
'@MailManager',
], ],
output: { output: {
assetFileNames: (assetInfo) => { assetFileNames: (assetInfo) => {