v3.1.1 - RC - more testing needed - Details in Changelog.md
Some checks failed
Docker Image - Webmail / build_self_hosted (push) Waiting to run
Docker Image - Webmail / build_github_hosted_fallback (push) Blocked by required conditions
Docker Image - DockFlare / build_self_hosted (push) Has been cancelled
Docker Image - Mail Manager / build_self_hosted (push) Has been cancelled
Docker Image - DockFlare / build_github_hosted_fallback (push) Has been cancelled
Docker Image - Mail Manager / build_github_hosted_fallback (push) Has been cancelled

This commit is contained in:
ChrispyBacon-dev 2026-04-24 23:32:33 +02:00
parent b3c0da2d75
commit 0fad7c592f
65 changed files with 5980 additions and 1619 deletions

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0f172a" />
<title>DockFlare Mail</title>
<link rel="apple-touch-icon" href="/favicon/apple-touch-icon.png">

View file

@ -8,6 +8,7 @@
"name": "dockflare-webmail",
"version": "1.0.0",
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@tiptap/extension-color": "^2.27.2",
"@tiptap/extension-font-family": "^2.27.2",
"@tiptap/extension-highlight": "^2.27.2",
@ -25,6 +26,7 @@
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"dompurify": "^3.4.0",
"emoji-mart": "^5.6.0",
"lucide-vue-next": "^0.400.0",
"pinia": "^2.2.0",
"radix-vue": "^1.9.0",
@ -1595,6 +1597,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@emoji-mart/data": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz",
"integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@ -4285,6 +4293,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/emoji-mart": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz",
"integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",

View file

@ -8,6 +8,7 @@
"preview": "vite preview"
},
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@tiptap/extension-color": "^2.27.2",
"@tiptap/extension-font-family": "^2.27.2",
"@tiptap/extension-highlight": "^2.27.2",
@ -25,6 +26,7 @@
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"dompurify": "^3.4.0",
"emoji-mart": "^5.6.0",
"lucide-vue-next": "^0.400.0",
"pinia": "^2.2.0",
"radix-vue": "^1.9.0",

View file

@ -9,5 +9,17 @@ export const authApi = {
});
return response.json();
},
changePassword: async (currentPassword, newPassword) => {
const token = localStorage.getItem('jwt_token');
const response = await fetch('/email/auth/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }),
});
return response.json();
},
};
//# sourceMappingURL=auth.js.map

View file

@ -1 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,UAAU,CAAA;AAEhC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;IAE1C,iBAAiB,EAAE,KAAK,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF,CAAA"}
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,UAAU,CAAA;AAEhC,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC;IAE1C,iBAAiB,EAAE,KAAK,EAAE,KAAa,EAAE,QAAgB,EAAE,EAAE;QAC3D,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;SAC1C,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;IAED,cAAc,EAAE,KAAK,EAAE,eAAuB,EAAE,WAAmB,EAAE,EAAE;QACrE,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;QAC/C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,6BAA6B,EAAE;YAC1D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,eAAe,EAAE,UAAU,KAAK,EAAE;aACnC;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,gBAAgB,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,CAAC;SACvF,CAAC,CAAA;QACF,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAA;IACxB,CAAC;CACF,CAAA"}

View file

@ -12,7 +12,6 @@ apiClient.interceptors.request.use(config => {
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Let the browser set Content-Type (with boundary) for FormData
if (config.data instanceof FormData) {
delete config.headers['Content-Type'];
}
@ -21,6 +20,10 @@ apiClient.interceptors.request.use(config => {
apiClient.interceptors.response.use(response => response, error => {
if (error.response?.status === 401) {
localStorage.removeItem('jwt_token');
import('../stores/auth').then(({ useAuthStore }) => {
const authStore = useAuthStore();
authStore.token = '';
});
router.push('/login');
}
return Promise.reject(error);

View file

@ -1 +1 @@
{"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;KACnC;CACF,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;IAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;IAClD,CAAC;IACD,gEAAgE;IAChE,IAAI,MAAM,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CACjC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EACpB,KAAK,CAAC,EAAE;IACN,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;QACnC,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}
{"version":3,"file":"client.js","sourceRoot":"","sources":["client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,MAAM,MAAM,WAAW,CAAA;AAE9B,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;IAC7B,OAAO,EAAE,SAAS;IAClB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;KACnC;CACF,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE;IAC1C,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,CAAC,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;QACpC,OAAO,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACvC,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CACjC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EACpB,KAAK,CAAC,EAAE;IACN,IAAI,KAAK,CAAC,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,CAAC;QACnC,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;QACpC,MAAM,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE;YACjD,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;YAChC,SAAS,CAAC,KAAK,GAAG,EAAE,CAAA;QACtB,CAAC,CAAC,CAAA;QACF,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CACF,CAAA;AAED,eAAe,SAAS,CAAA"}

View file

@ -15,9 +15,20 @@ export const mailApi = {
sendMessage: (address, data) => apiClient.post(`/mailboxes/${address}/send`, data),
searchMessages: (address, params) => apiClient.get(`/mailboxes/${address}/search`, { params }),
getMailboxStatus: () => apiClient.get('/mailboxes/status'),
getMailboxPreferences: (address) => apiClient.get(`/mailboxes/${address}/preferences`),
updateMailboxPreferences: (address, data) => apiClient.patch(`/mailboxes/${address}/preferences`, data),
getAttachmentUrl: (id) => `/api/v1/attachments/${id}/download`,
downloadAttachment: (id) => apiClient.get(`/attachments/${id}/download`, { responseType: 'blob' }).then(r => r.data),
createDraft: (address, data) => apiClient.post(`/mailboxes/${address}/drafts`, data),
updateDraft: (address, id, data) => apiClient.put(`/mailboxes/${address}/drafts/${id}`, data),
getAutoResponder: (address) => apiClient.get(`/mailboxes/${address}/auto-responder`),
setAutoResponder: (address, data) => apiClient.post(`/mailboxes/${address}/auto-responder`, data),
deleteAutoResponder: (address) => apiClient.delete(`/mailboxes/${address}/auto-responder`),
getAliases: (mailbox) => apiClient.get('/aliases', { params: { mailbox, active: 1 } }),
getAllAliases: (mailbox) => apiClient.get('/aliases', { params: { mailbox } }),
createAlias: (data) => apiClient.post('/aliases', data),
updateAlias: (address, data) => apiClient.patch(`/aliases/${encodeURIComponent(address)}`, data),
deleteAlias: (address) => apiClient.delete(`/aliases/${encodeURIComponent(address)}`),
generateAlias: (mailbox, domain, style) => apiClient.post('/aliases/generate', { mailbox_address: mailbox, domain, style: style || 'word-word-num' }),
};
//# sourceMappingURL=mail.js.map

View file

@ -1 +1 @@
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}

View file

@ -37,4 +37,16 @@ export const mailApi = {
apiClient.post(`/mailboxes/${address}/auto-responder`, data),
deleteAutoResponder: (address: string) =>
apiClient.delete(`/mailboxes/${address}/auto-responder`),
getAliases: (mailbox: string) =>
apiClient.get('/aliases', { params: { mailbox, active: 1 } }),
getAllAliases: (mailbox: string) =>
apiClient.get('/aliases', { params: { mailbox } }),
createAlias: (data: Record<string, any>) =>
apiClient.post('/aliases', data),
updateAlias: (address: string, data: Record<string, any>) =>
apiClient.patch(`/aliases/${encodeURIComponent(address)}`, data),
deleteAlias: (address: string) =>
apiClient.delete(`/aliases/${encodeURIComponent(address)}`),
generateAlias: (mailbox: string, domain: string, style?: string) =>
apiClient.post('/aliases/generate', { mailbox_address: mailbox, domain, style: style || 'word-word-num' }),
}

View file

@ -2,6 +2,15 @@
@tailwind components;
@tailwind utilities;
@layer utilities {
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.h-\[100dvh\] {
height: 100dvh;
}
}
@layer base {
:root {
--background: 0 0% 100%;

View file

@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref, watch, nextTick, onUnmounted } from 'vue'
import { ref, watch, nextTick, onUnmounted, onMounted, type Ref, computed } from 'vue'
import { useBreakpoint } from '../../composables/useBreakpoint'
import {
Paperclip, X, Bold as BoldIcon, Italic as ItalicIcon, Link2, List as ListIcon, ListOrdered, Minus,
Underline as UnderlineIcon, AlignLeft as AlignLeftIcon, AlignCenter as AlignCenterIcon, AlignRight as AlignRightIcon, AlignJustify as AlignJustifyIcon,
Quote as QuoteIcon, RemoveFormatting, Baseline, Trash2, Strikethrough as StrikethroughIcon, Type, BookmarkCheck, Maximize2, Minimize2
Quote as QuoteIcon, RemoveFormatting, Baseline, Trash2, Strikethrough as StrikethroughIcon, Type, BookmarkCheck, Maximize2, Minimize2, Smile
} from 'lucide-vue-next'
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
@ -22,11 +23,25 @@ import { useMailStore } from '../../stores/mail'
import Button from '../ui/Button.vue'
import Input from '../ui/Input.vue'
defineProps({ panelMode: { type: Boolean, default: false } })
const props = defineProps({ panelMode: { type: Boolean, default: false } })
const store = useMailStore()
const { isMobile } = useBreakpoint()
const effectivePanelMode = computed(() => props.panelMode || isMobile.value)
const to = ref('')
const _EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const toTags = ref<string[]>([])
const toInput = ref('')
const ccTags = ref<string[]>([])
const ccInput = ref('')
const bccTags = ref<string[]>([])
const bccInput = ref('')
const showCc = ref(false)
const showBcc = ref(false)
const fromAddress = ref('')
const aliases = ref<string[]>([])
const subject = ref('')
const attachments = ref<File[]>([])
const sending = ref(false)
@ -65,8 +80,24 @@ const editor = useEditor({
},
})
const loadAliases = async () => {
if (!store.currentMailbox) return
try {
const res = await mailApi.getAliases(store.currentMailbox)
aliases.value = (res.data.aliases || []).map((a: any) => a.address)
} catch { aliases.value = [] }
}
const reset = () => {
to.value = ''
toTags.value = []
toInput.value = ''
ccTags.value = []
ccInput.value = ''
bccTags.value = []
bccInput.value = ''
showCc.value = false
showBcc.value = false
fromAddress.value = store.currentMailbox || ''
subject.value = ''
attachments.value = []
error.value = ''
@ -78,17 +109,67 @@ const reset = () => {
store.composeDefaults = null
}
const addTag = (tags: Ref<string[]>, input: Ref<string>) => {
const val = input.value.trim().replace(/[,;]+$/, '')
if (val && _EMAIL_RE.test(val) && !tags.value.includes(val)) {
tags.value.push(val)
}
input.value = ''
}
const makeTagHandlers = (tags: Ref<string[]>, input: Ref<string>) => ({
onKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === ',' || e.key === 'Tab') {
e.preventDefault()
addTag(tags, input)
} else if (e.key === 'Backspace' && !input.value && tags.value.length) {
tags.value.pop()
}
},
onBlur() { addTag(tags, input) },
onPaste(e: ClipboardEvent) {
e.preventDefault()
const text = e.clipboardData?.getData('text') || ''
for (const addr of text.split(/[,;\s]+/)) {
const trimmed = addr.trim()
if (trimmed && _EMAIL_RE.test(trimmed) && !tags.value.includes(trimmed)) {
tags.value.push(trimmed)
}
}
},
})
const toHandlers = makeTagHandlers(toTags, toInput)
const ccHandlers = makeTagHandlers(ccTags, ccInput)
const bccHandlers = makeTagHandlers(bccTags, bccInput)
onMounted(loadAliases)
watch(() => store.isComposeOpen, async (open) => {
if (open && store.composeDefaults) {
to.value = store.composeDefaults.to || ''
subject.value = store.composeDefaults.subject || ''
quotedHtml.value = store.composeDefaults.quotedHtml || ''
if (store.composeDefaults.draftId) {
draftId.value = store.composeDefaults.draftId
if (open) {
await loadAliases()
if (store.composeDefaults) {
const rawTo = store.composeDefaults.to || ''
if (rawTo) {
for (const addr of rawTo.split(',').map((s: string) => s.trim()).filter(Boolean)) {
if (_EMAIL_RE.test(addr) && !toTags.value.includes(addr)) toTags.value.push(addr)
}
}
subject.value = store.composeDefaults.subject || ''
quotedHtml.value = store.composeDefaults.quotedHtml || ''
if (store.composeDefaults.draftId) {
draftId.value = store.composeDefaults.draftId
}
const requestedFrom = store.composeDefaults.from
fromAddress.value = (requestedFrom && aliases.value.includes(requestedFrom))
? requestedFrom
: (store.currentMailbox || '')
} else {
fromAddress.value = store.currentMailbox || ''
}
minimized.value = false
await nextTick()
if (store.composeDefaults.body) {
if (store.composeDefaults?.body) {
editor.value?.commands.setContent(store.composeDefaults.body)
} else {
editor.value?.commands.clearContent()
@ -98,24 +179,6 @@ watch(() => store.isComposeOpen, async (open) => {
}
}, { immediate: true })
watch(to, (val) => {
if (store.composeDefaults !== null) {
store.composeDefaults = { ...store.composeDefaults, to: val }
}
})
watch(subject, (val) => {
if (store.composeDefaults !== null) {
store.composeDefaults = { ...store.composeDefaults, subject: val }
}
})
watch(quotedHtml, (val) => {
if (store.composeDefaults !== null) {
store.composeDefaults = { ...store.composeDefaults, quotedHtml: val }
}
})
onUnmounted(() => editor.value?.destroy())
const close = () => {
@ -167,24 +230,44 @@ const formatBytes = (bytes: number) => {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
const setLink = () => {
const prev = editor.value?.getAttributes('link').href || ''
const url = window.prompt('Enter URL', prev)
if (url === null) return
if (url === '') {
const showLinkPopover = ref(false)
const linkInput = ref('')
const openLinkPopover = () => {
linkInput.value = editor.value?.getAttributes('link').href || ''
showLinkPopover.value = true
nextTick(() => {
const el = document.getElementById('compose-link-input')
el?.focus()
})
}
const applyLink = () => {
const url = linkInput.value.trim()
if (!url) {
editor.value?.chain().focus().unsetLink().run()
} else {
editor.value?.chain().focus().setLink({ href: url }).run()
const href = url.startsWith('http') ? url : `https://${url}`
editor.value?.chain().focus().setLink({ href }).run()
}
showLinkPopover.value = false
}
const onLinkKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter') { e.preventDefault(); applyLink() }
if (e.key === 'Escape') { showLinkPopover.value = false }
}
const saveDraft = async () => {
if (!store.currentMailbox || !editor.value) return
toHandlers.onBlur()
savingDraft.value = true
error.value = ''
try {
const payload = {
to: to.value ? to.value.split(',').map((s: string) => s.trim()).filter(Boolean) : [],
to: toTags.value,
cc: ccTags.value,
bcc: bccTags.value,
subject: subject.value,
html_body: editor.value.getHTML() + (quotedHtml.value || ''),
text_body: editor.value.getText(),
@ -209,6 +292,12 @@ const saveDraft = async () => {
const send = async () => {
if (!store.currentMailbox || !editor.value) return
toHandlers.onBlur()
if (!toTags.value.length) {
error.value = 'Please add at least one recipient.'
return
}
const totalSize = attachments.value.reduce((sum, f) => sum + f.size, 0)
if (totalSize > MAX_ATTACHMENT_BYTES) {
error.value = `Attachments exceed 10 MB limit (${formatBytes(totalSize)} total).`
@ -221,10 +310,15 @@ const send = async () => {
const html = editor.value.getHTML() + (quotedHtml.value || '')
const text = editor.value.getText()
const formData = new FormData()
formData.append('to', to.value)
for (const addr of toTags.value) formData.append('to', addr)
for (const addr of ccTags.value) formData.append('cc', addr)
for (const addr of bccTags.value) formData.append('bcc', addr)
formData.append('subject', subject.value)
formData.append('html', html)
formData.append('text', text)
if (fromAddress.value && fromAddress.value !== store.currentMailbox) {
formData.append('from_address', fromAddress.value)
}
for (const file of attachments.value) {
formData.append('attachments', file)
}
@ -266,18 +360,55 @@ const setHighlight = (e: Event) => {
const target = e.target as HTMLInputElement
editor.value?.chain().focus().setHighlight({ color: target.value }).run()
}
const showEmojiPicker = ref(false)
const emojiPickerContainer = ref<HTMLElement | null>(null)
const openEmojiPicker = async () => {
showEmojiPicker.value = !showEmojiPicker.value
if (!showEmojiPicker.value) return
await nextTick()
if (!emojiPickerContainer.value) return
emojiPickerContainer.value.innerHTML = ''
const { Picker } = await import('emoji-mart')
const data = (await import('@emoji-mart/data')).default
new Picker({
data,
onEmojiSelect: (emoji: any) => {
editor.value?.chain().focus().insertContent(emoji.native).run()
showEmojiPicker.value = false
},
parent: emojiPickerContainer.value,
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
})
}
const onEmojiClickOutside = (e: MouseEvent) => {
if (!emojiPickerContainer.value) return
const wrapper = emojiPickerContainer.value.closest('.emoji-picker-wrapper')
if (wrapper && !wrapper.contains(e.target as Node)) {
showEmojiPicker.value = false
}
}
watch(showEmojiPicker, (val) => {
if (val) document.addEventListener('mousedown', onEmojiClickOutside)
else document.removeEventListener('mousedown', onEmojiClickOutside)
})
onUnmounted(() => document.removeEventListener('mousedown', onEmojiClickOutside))
</script>
<template>
<div
v-if="store.isComposeOpen && (panelMode || !store.isComposeFullView)"
:class="panelMode
v-if="store.isComposeOpen && (effectivePanelMode || !store.isComposeFullView)"
:class="effectivePanelMode
? 'flex flex-col h-full w-full bg-background'
: 'fixed bottom-0 right-6 z-50 flex flex-col rounded-t-xl shadow-2xl border border-border bg-background'"
:style="!panelMode ? (minimized ? 'width:320px' : 'width:560px') : ''"
:style="!effectivePanelMode ? (minimized ? 'width:320px' : 'width:620px') : ''"
>
<!-- Panel mode title bar -->
<div v-if="panelMode" class="h-[52px] flex items-center gap-2 px-4 border-b border-border flex-shrink-0">
<div v-if="effectivePanelMode" class="h-[52px] flex items-center gap-2 px-4 border-b border-border flex-shrink-0">
<span class="flex-1 text-base font-semibold truncate">{{ subject || 'New Message' }}</span>
<button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" title="Pop out" @click="toggleFullView">
<Minimize2 class="size-4" />
@ -288,7 +419,7 @@ const setHighlight = (e: Event) => {
</div>
<!-- Popup mode title bar -->
<div v-else class="flex items-center gap-2 rounded-t-xl bg-primary px-4 py-2.5 cursor-pointer select-none" @click="toggleMinimize">
<div v-else-if="!effectivePanelMode" class="flex items-center gap-2 rounded-t-xl bg-primary px-4 py-2.5 cursor-pointer select-none" @click="toggleMinimize">
<span class="flex-1 text-sm font-semibold text-primary-foreground truncate">{{ subject || 'New Message' }}</span>
<button type="button" class="rounded p-0.5 text-primary-foreground/70 hover:text-primary-foreground hover:bg-white/10 transition-colors" title="Full view" @click.stop="toggleFullView">
<Maximize2 class="size-4" />
@ -301,14 +432,94 @@ const setHighlight = (e: Event) => {
</button>
</div>
<!-- Body always visible in panel mode, hidden when minimized in popup mode -->
<!-- Body -->
<div
v-show="panelMode || !minimized"
:class="panelMode ? 'flex flex-col flex-1 overflow-hidden' : 'flex flex-col flex-1 overflow-hidden max-h-[80vh]'"
v-show="effectivePanelMode || !minimized"
:class="effectivePanelMode ? 'flex flex-col flex-1 overflow-hidden' : 'flex flex-col flex-1 overflow-hidden max-h-[80vh]'"
>
<!-- Fields -->
<div class="flex flex-col border-b border-border flex-shrink-0">
<input v-model="to" placeholder="To" class="w-full border-b border-border px-4 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none" />
<!-- To row -->
<div class="flex items-start border-b border-border min-h-[36px]">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">To</span>
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
<span
v-for="(tag, i) in toTags" :key="tag"
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
>
{{ tag }}
<button type="button" @click="toTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
</span>
<input
v-model="toInput"
placeholder="Add recipient…"
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
@keydown="toHandlers.onKeydown"
@blur="toHandlers.onBlur"
@paste="toHandlers.onPaste"
/>
</div>
<div class="flex items-center gap-2 px-3 py-2 shrink-0">
<button v-if="!showCc" type="button" class="text-xs text-muted-foreground hover:text-foreground transition-colors" @click="showCc = true">Cc</button>
<button v-if="!showBcc" type="button" class="text-xs text-muted-foreground hover:text-foreground transition-colors" @click="showBcc = true">Bcc</button>
</div>
</div>
<!-- Cc row -->
<div v-if="showCc" class="flex items-start border-b border-border min-h-[36px]">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">Cc</span>
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
<span
v-for="(tag, i) in ccTags" :key="tag"
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
>
{{ tag }}
<button type="button" @click="ccTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
</span>
<input
v-model="ccInput"
placeholder="Add Cc…"
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
@keydown="ccHandlers.onKeydown"
@blur="ccHandlers.onBlur"
@paste="ccHandlers.onPaste"
/>
</div>
</div>
<!-- Bcc row -->
<div v-if="showBcc" class="flex items-start border-b border-border min-h-[36px]">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0 leading-5">Bcc</span>
<div class="flex flex-wrap items-center gap-1 flex-1 py-1.5 pr-2 min-w-0">
<span
v-for="(tag, i) in bccTags" :key="tag"
class="flex items-center gap-1 bg-muted rounded-full px-2 py-0.5 text-xs text-foreground border border-border"
>
{{ tag }}
<button type="button" @click="bccTags.splice(i, 1)" class="hover:text-destructive leading-none"><X :size="10" /></button>
</span>
<input
v-model="bccInput"
placeholder="Add Bcc…"
class="flex-1 min-w-[120px] bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none py-0.5"
@keydown="bccHandlers.onKeydown"
@blur="bccHandlers.onBlur"
@paste="bccHandlers.onPaste"
/>
</div>
</div>
<!-- From row (aliases) -->
<div v-if="aliases.length" class="flex items-center border-b border-border">
<span class="px-4 py-2 text-sm text-muted-foreground shrink-0">From</span>
<select v-model="fromAddress" class="flex-1 px-2 py-2 text-sm bg-background text-foreground focus:outline-none">
<option :value="store.currentMailbox">{{ store.currentMailbox }}</option>
<option v-for="alias in aliases" :key="alias" :value="alias">{{ alias }} (alias)</option>
</select>
</div>
<!-- Subject -->
<input v-model="subject" placeholder="Subject" class="w-full px-4 py-2 text-sm bg-background text-foreground placeholder:text-muted-foreground focus:outline-none" />
</div>
@ -330,7 +541,7 @@ const setHighlight = (e: Event) => {
<div v-if="error" class="px-4 py-1 text-xs text-red-500 flex-shrink-0">{{ error }}</div>
<!-- Formatting Toolbar (Collapsible) -->
<!-- Formatting Toolbar -->
<div v-if="showFormatting" class="flex flex-wrap items-center gap-1 border-t border-border bg-muted/30 px-3 py-1.5 flex-shrink-0">
<select @change="setFont" class="text-xs bg-transparent border-none focus:ring-0 text-foreground cursor-pointer mr-1 max-w-[100px]">
<option value="">Default Font</option>
@ -364,12 +575,18 @@ const setHighlight = (e: Event) => {
<!-- Bottom Action Bar -->
<div class="flex items-center justify-between gap-2 border-t border-border px-4 py-2.5 flex-shrink-0 bg-background">
<div class="flex items-center gap-1">
<Button as="button" type="button" size="sm" class="rounded-full px-5 font-semibold tracking-wide" @click.prevent="send" :disabled="sending || !to">
<Button as="button" type="button" size="sm" class="rounded-full px-5 font-semibold tracking-wide" @click.prevent="send" :disabled="sending || (!toTags.length && !toInput)">
{{ sending ? 'Sending…' : 'Send' }}
</Button>
<button type="button" class="ml-1 rounded p-1.5 transition-colors" :class="savedDraft ? 'text-green-500' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" :disabled="savingDraft" title="Save draft" @click="saveDraft">
<BookmarkCheck class="size-4" />
</button>
<div class="relative emoji-picker-wrapper">
<button type="button" class="rounded p-1.5 transition-colors" :class="showEmojiPicker ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" @click="openEmojiPicker" title="Insert emoji">
<Smile class="size-4" />
</button>
<div v-if="showEmojiPicker" class="absolute bottom-10 left-0 z-50 shadow-xl rounded-xl overflow-hidden" ref="emojiPickerContainer" />
</div>
<button type="button" class="ml-1 rounded p-1.5 hover:bg-accent transition-colors" :class="showFormatting ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'" @click="showFormatting = !showFormatting" title="Formatting options">
<Type class="size-4" />
</button>
@ -377,9 +594,26 @@ const setHighlight = (e: Event) => {
<Paperclip class="size-4" />
<input type="file" multiple class="hidden" @change="onFileChange" />
</label>
<button type="button" class="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" @click="setLink" title="Insert link">
<Link2 class="size-4" />
</button>
<div class="relative">
<button type="button" class="rounded p-1.5 transition-colors" :class="showLinkPopover ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'" @click="openLinkPopover" title="Insert link">
<Link2 class="size-4" />
</button>
<div v-if="showLinkPopover" class="absolute bottom-10 left-0 z-50 w-72 rounded-lg border border-border bg-background shadow-xl p-3 flex flex-col gap-2">
<label class="text-xs font-medium text-muted-foreground">Insert link</label>
<input
id="compose-link-input"
v-model="linkInput"
type="url"
placeholder="https://example.com"
class="w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring"
@keydown="onLinkKeydown"
/>
<div class="flex gap-2 justify-end">
<button type="button" class="rounded-md px-3 py-1 text-xs text-muted-foreground hover:bg-accent transition-colors" @click="showLinkPopover = false">Cancel</button>
<button type="button" class="rounded-md px-3 py-1 text-xs bg-primary text-primary-foreground hover:bg-primary/90 transition-colors" @click="applyLink">Apply</button>
</div>
</div>
</div>
</div>
<button type="button" class="rounded p-1.5 text-muted-foreground hover:bg-accent hover:text-destructive transition-colors" :title="draftId ? 'Delete draft' : 'Discard'" @click="discardDraft">
<Trash2 class="size-4" />
@ -390,7 +624,6 @@ const setHighlight = (e: Event) => {
</template>
<style>
/* Tiptap editor inside compose */
.compose-editor {
display: flex;
flex-direction: column;
@ -433,7 +666,6 @@ const setHighlight = (e: Event) => {
cursor: pointer;
}
/* Pop-up animation */
.compose-pop-enter-active { transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.18s ease; }
.compose-pop-leave-active { transition: transform 0.14s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.14s ease; }
.compose-pop-enter-from { transform: translateY(24px) scale(0.95); opacity: 0; }

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -48,8 +48,8 @@ const confirmNewFolder = async () => {
store.folders = res.data;
showNewFolder.value = false;
}
catch (e) {
console.error('Failed to create folder', e);
catch {
store.showToast('Failed to create folder');
}
finally {
creatingFolder.value = false;
@ -69,8 +69,8 @@ const deleteFolder = async (f) => {
store.currentFolder = store.folders[0]?.name || '';
}
}
catch (e) {
console.error('Failed to delete folder', e);
catch {
store.showToast('Failed to delete folder');
}
};
// ── Folder rename / colour edit ──────────────────────────────────────
@ -100,8 +100,8 @@ const confirmEdit = async () => {
}
editingFolder.value = null;
}
catch (e) {
console.error('Failed to rename folder', e);
catch {
store.showToast('Failed to rename folder');
}
};
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
@ -182,17 +182,19 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
}, ...__VLS_functionalComponentArgsRest(__VLS_17));
__VLS_19.slots.default;
(f.name);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "ml-auto text-muted-foreground flex gap-1" },
});
if (f.unread_count) {
if (f.total_count > 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-bold" },
...{ class: "ml-auto text-muted-foreground flex gap-1" },
});
(f.unread_count);
if (f.unread_count) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-bold" },
});
(f.unread_count);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(f.total_count);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(f.total_count || 0);
var __VLS_19;
var __VLS_15;
var __VLS_3;
@ -279,17 +281,19 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.store.folders))) {
...{ class: "truncate" },
});
(f.name);
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: (__VLS_ctx.cn('ml-auto text-xs flex-shrink-0 flex gap-1', __VLS_ctx.store.currentFolder === f.name ? 'text-primary-foreground' : 'text-muted-foreground')) },
});
if (f.unread_count) {
if (f.total_count > 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-bold" },
...{ class: (__VLS_ctx.cn('ml-auto text-xs flex-shrink-0 flex gap-1', __VLS_ctx.store.currentFolder === f.name ? 'text-primary-foreground' : 'text-muted-foreground')) },
});
(f.unread_count);
if (f.unread_count) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-bold" },
});
(f.unread_count);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(f.total_count);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({});
(f.total_count || 0);
if (!f.system_folder) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: (__VLS_ctx.cn('absolute right-1 flex gap-0.5 opacity-0 group-hover/row:opacity-100 transition-opacity rounded px-0.5', __VLS_ctx.store.currentFolder === f.name ? 'bg-primary' : 'bg-accent')) },
@ -372,6 +376,16 @@ if (__VLS_ctx.showNewFolder && !__VLS_ctx.isCollapsed) {
...{ style: (`background:${c}; border-color:${__VLS_ctx.newFolderColor === c ? '#000' : 'transparent'}`) },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.showNewFolder && !__VLS_ctx.isCollapsed))
return;
__VLS_ctx.newFolderColor = '';
} },
...{ class: "h-5 w-5 rounded-full border-2 text-xs flex items-center justify-center text-muted-foreground hover:bg-accent" },
...{ style: (`border-color:${!__VLS_ctx.newFolderColor ? '#888' : 'transparent'}`) },
title: "No colour",
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex gap-1 justify-end" },
});
@ -531,6 +545,16 @@ if (!__VLS_ctx.isCollapsed) {
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-transform']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:scale-110']} */ ;
/** @type {__VLS_StyleScopedClasses['h-5']} */ ;
/** @type {__VLS_StyleScopedClasses['w-5']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-full']} */ ;
/** @type {__VLS_StyleScopedClasses['border-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-1']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-end']} */ ;

File diff suppressed because one or more lines are too long

View file

@ -3,10 +3,9 @@ import {
SplitterGroup, SplitterPanel, SplitterResizeHandle,
TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal,
} from 'radix-vue'
import { defineAsyncComponent } from 'vue'
import { PenSquare, Sun, Moon, LogOut, Settings, Columns2, Maximize2 } from 'lucide-vue-next'
import { defineAsyncComponent, ref, watch, computed } from 'vue'
import { PenSquare, Sun, Moon, LogOut, Settings, Columns2, Maximize2, ChevronLeft, Menu } from 'lucide-vue-next'
import { cn } from '../../lib/utils'
import Separator from '../ui/Separator.vue'
import MailboxSelector from './MailboxSelector.vue'
import FolderNav from './FolderNav.vue'
import MessageList from './MessageList.vue'
@ -14,11 +13,13 @@ import MessageDisplay from './MessageDisplay.vue'
import ComposeDialog from './ComposeDialog.vue'
import { useMailStore } from '../../stores/mail'
import { useAuth } from '../../composables/useAuth'
import { useBreakpoint } from '../../composables/useBreakpoint'
const SettingsDialog = defineAsyncComponent(() => import('./SettingsDialog.vue'))
const store = useMailStore()
const { logout } = useAuth()
const { isMobile } = useBreakpoint()
const onCollapse = () => { store.isCollapsed = true }
const onExpand = () => { store.isCollapsed = false }
@ -27,11 +28,134 @@ const compose = () => {
store.composeDefaults = null
store.isComposeOpen = true
}
// Mobile navigation stack
type MobilePanel = 'folders' | 'list' | 'detail'
const mobilePanel = ref<MobilePanel>('list')
watch(() => store.currentFolder, () => {
if (isMobile.value) mobilePanel.value = 'list'
})
watch(() => store.currentMessage, (msg) => {
if (isMobile.value && msg) mobilePanel.value = 'detail'
})
const goBack = () => {
if (mobilePanel.value === 'detail') {
store.currentMessage = null
mobilePanel.value = 'list'
} else if (mobilePanel.value === 'list') {
mobilePanel.value = 'folders'
}
}
const mobileTitle = computed(() => {
if (mobilePanel.value === 'folders') return store.currentMailbox || 'Folders'
if (mobilePanel.value === 'list') return store.currentFolder || 'Inbox'
return store.currentMessage?.subject || 'Message'
})
</script>
<template>
<TooltipProvider :delay-duration="0">
<!--
MOBILE LAYOUT (< 768px)
-->
<div v-if="isMobile" class="flex flex-col h-[100dvh] w-screen bg-background overflow-hidden">
<!-- Top bar -->
<div class="h-14 flex items-center gap-2 px-3 border-b border-border flex-shrink-0 bg-background">
<button
v-if="mobilePanel !== 'folders'"
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="goBack"
>
<ChevronLeft class="size-5" />
</button>
<button
v-else
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="store.isSettingsOpen = true"
>
<Settings class="size-4" />
</button>
<span class="flex-1 text-base font-semibold truncate">{{ mobileTitle }}</span>
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="store.toggleTheme()"
>
<Sun v-if="store.isDark" class="size-4" />
<Moon v-else class="size-4" />
</button>
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground hover:bg-accent transition-colors flex-shrink-0"
@click="logout"
>
<LogOut class="size-4" />
</button>
</div>
<!-- Panel content -->
<div class="flex-1 min-h-0 overflow-hidden">
<!-- Folders panel -->
<div v-if="mobilePanel === 'folders'" class="h-full flex flex-col overflow-y-auto">
<div class="px-3 py-3 border-b border-border">
<MailboxSelector :is-collapsed="false" />
</div>
<FolderNav :is-collapsed="false" />
</div>
<!-- Message list panel -->
<div v-else-if="mobilePanel === 'list'" class="h-full flex flex-col overflow-hidden">
<MessageList />
</div>
<!-- Message detail panel -->
<div v-else-if="mobilePanel === 'detail'" class="h-full flex flex-col overflow-hidden">
<MessageDisplay :message="store.currentMessage ?? undefined" />
</div>
</div>
<!-- Bottom nav bar -->
<div class="h-16 flex items-center justify-around border-t border-border flex-shrink-0 bg-background pb-safe">
<button
class="flex flex-col items-center gap-0.5 px-4 py-2 rounded-lg transition-colors"
:class="mobilePanel === 'folders' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
@click="mobilePanel = 'folders'"
>
<Menu class="size-5" />
<span class="text-[10px] font-medium">Folders</span>
</button>
<button
class="flex items-center justify-center h-12 w-12 rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors"
@click="compose"
>
<PenSquare class="size-5" />
</button>
<button
class="flex flex-col items-center gap-0.5 px-4 py-2 rounded-lg transition-colors"
:class="mobilePanel === 'list' ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
@click="mobilePanel = 'list'"
>
<Columns2 class="size-5" />
<span class="text-[10px] font-medium">Mail</span>
</button>
</div>
</div>
<!--
DESKTOP LAYOUT ( 768px) unchanged
-->
<SplitterGroup
v-else
id="mail-layout"
direction="horizontal"
class="h-screen w-screen items-stretch"
@ -50,7 +174,6 @@ const compose = () => {
@collapse="onCollapse"
@expand="onExpand"
>
<!-- Single header row (same height as middle + right panel headers) -->
<div
:class="cn(
'h-[52px] flex items-center gap-1 px-2 flex-shrink-0',
@ -58,11 +181,9 @@ const compose = () => {
)"
>
<template v-if="!store.isCollapsed">
<!-- Mailbox selector fills available space -->
<div class="flex-1 min-w-0">
<MailboxSelector :is-collapsed="false" />
</div>
<!-- Compose -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -74,7 +195,6 @@ const compose = () => {
</button>
</TooltipTrigger>
</TooltipRoot>
<!-- View Mode toggle -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -91,7 +211,6 @@ const compose = () => {
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Theme toggle -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -108,7 +227,6 @@ const compose = () => {
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Settings -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -124,7 +242,6 @@ const compose = () => {
</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Logout -->
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
<button
@ -142,7 +259,6 @@ const compose = () => {
</TooltipRoot>
</template>
<!-- Collapsed: stacked icon buttons -->
<template v-else>
<TooltipRoot :delay-duration="0">
<TooltipTrigger as-child>
@ -185,7 +301,6 @@ const compose = () => {
<TooltipContent side="right" class="z-50 rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md">Logout</TooltipContent>
</TooltipPortal>
</TooltipRoot>
<!-- Collapsed mailbox icon below -->
<MailboxSelector :is-collapsed="true" />
</template>
</div>
@ -242,7 +357,15 @@ const compose = () => {
</template>
</SplitterGroup>
<ComposeDialog />
<!-- Floating compose (desktop only) + settings dialog -->
<template v-if="!isMobile">
<ComposeDialog />
</template>
<Teleport v-else to="body">
<div v-if="store.isComposeOpen" class="fixed inset-0 z-50 flex flex-col bg-background">
<ComposeDialog :panel-mode="true" />
</div>
</Teleport>
<SettingsDialog />
</TooltipProvider>
</template>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -81,6 +81,15 @@ watch(emailIframe, (el) => {
onUnmounted(() => resizeObserver?.disconnect())
const parseAddrs = (raw: string | null | undefined) => {
let addrs: string[] = []
try { addrs = JSON.parse(raw || '[]') } catch { addrs = [] }
return addrs.map((a: string) => { const m = a.match(/<([^>]+)>/); return m ? m[1] : a }).join(', ')
}
const toDisplay = computed(() => parseAddrs(props.message?.to_addresses))
const ccDisplay = computed(() => parseAddrs(props.message?.cc_addresses))
const bccDisplay = computed(() => parseAddrs(props.message?.bcc_addresses))
const displayTimestamp = computed(() => {
const ts = props.message?.received_at || props.message?.sent_at
return ts ? format(new Date(ts), 'PPpp') : ''
@ -104,6 +113,7 @@ const replyTo = () => {
if (!props.message) return
store.composeDefaults = {
to: props.message.from_address,
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -126,6 +136,7 @@ const replyAll = () => {
].filter((a: string) => a && a !== store.currentMailbox)
store.composeDefaults = {
to: allAddresses.join(', '),
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -495,10 +506,16 @@ const sendInlineReply = async () => {
<Avatar :initials="message.from_name?.[0] || message.from_address?.[0] || '?'" />
<div class="grid gap-1">
<div class="font-semibold">{{ message.from_name || message.from_address }}</div>
<div class="line-clamp-1 text-xs">{{ message.subject }}</div>
<div class="line-clamp-1 text-xs">
<span class="font-medium">Reply-To:</span> {{ message.from_address }}
<div v-if="toDisplay" class="line-clamp-1 text-xs">
<span class="font-medium">To:</span> {{ toDisplay }}
</div>
<div v-if="ccDisplay" class="line-clamp-1 text-xs">
<span class="font-medium">Cc:</span> {{ ccDisplay }}
</div>
<div v-if="bccDisplay" class="line-clamp-1 text-xs">
<span class="font-medium">Bcc:</span> {{ bccDisplay }}
</div>
<div class="line-clamp-1 text-xs">{{ message.subject }}</div>
</div>
</div>
<div v-if="displayTimestamp" class="ml-auto text-xs text-muted-foreground">

View file

@ -65,6 +65,19 @@ watch(emailIframe, (el) => {
resizeObserver.observe(el.parentElement);
});
onUnmounted(() => resizeObserver?.disconnect());
const parseAddrs = (raw) => {
let addrs = [];
try {
addrs = JSON.parse(raw || '[]');
}
catch {
addrs = [];
}
return addrs.map((a) => { const m = a.match(/<([^>]+)>/); return m ? m[1] : a; }).join(', ');
};
const toDisplay = computed(() => parseAddrs(props.message?.to_addresses));
const ccDisplay = computed(() => parseAddrs(props.message?.cc_addresses));
const bccDisplay = computed(() => parseAddrs(props.message?.bcc_addresses));
const displayTimestamp = computed(() => {
const ts = props.message?.received_at || props.message?.sent_at;
return ts ? format(new Date(ts), 'PPpp') : '';
@ -85,6 +98,7 @@ const replyTo = () => {
return;
store.composeDefaults = {
to: props.message.from_address,
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -96,13 +110,28 @@ const replyTo = () => {
const replyAll = () => {
if (!props.message)
return;
let toList = [];
let ccList = [];
try {
toList = JSON.parse(props.message.to_addresses || '[]');
}
catch {
toList = [];
}
try {
ccList = JSON.parse(props.message.cc_addresses || '[]');
}
catch {
ccList = [];
}
const allAddresses = [
props.message.from_address,
...(JSON.parse(props.message.to_addresses || '[]')),
...(JSON.parse(props.message.cc_addresses || '[]')),
...toList,
...ccList,
].filter((a) => a && a !== store.currentMailbox);
store.composeDefaults = {
to: allAddresses.join(', '),
from: props.message.received_via_alias || undefined,
subject: props.message.subject?.startsWith('Re:')
? props.message.subject
: `Re: ${props.message.subject || ''}`,
@ -134,9 +163,11 @@ const trash = async () => {
await mailApi.deleteMessage(store.currentMailbox, props.message.id);
store.messages = store.messages.filter((m) => m.id !== props.message.id);
store.currentMessage = null;
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
console.error('Failed to trash message', e);
catch {
store.showToast('Failed to move message to trash');
}
};
const markUnread = async () => {
@ -147,10 +178,28 @@ const markUnread = async () => {
const idx = store.messages.findIndex((m) => m.id === props.message.id);
if (idx !== -1)
store.messages[idx] = { ...store.messages[idx], is_read: 0 };
store.currentMessage = null;
store.currentMessage = { ...store.currentMessage, is_read: 0 };
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
console.error('Failed to mark unread', e);
catch {
store.showToast('Failed to mark as unread');
}
};
const markRead = async () => {
if (!props.message || !store.currentMailbox)
return;
try {
await mailApi.updateMessage(store.currentMailbox, props.message.id, { is_read: true });
const idx = store.messages.findIndex((m) => m.id === props.message.id);
if (idx !== -1)
store.messages[idx] = { ...store.messages[idx], is_read: 1 };
store.currentMessage = { ...store.currentMessage, is_read: 1 };
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch {
store.showToast('Failed to mark as read');
}
};
const toggleStar = async () => {
@ -165,8 +214,8 @@ const toggleStar = async () => {
if (store.currentMessage)
store.currentMessage = { ...store.currentMessage, is_starred: newVal };
}
catch (e) {
console.error('Failed to toggle star', e);
catch {
store.showToast('Failed to update star');
}
};
const moveToFolder = async (targetFolder) => {
@ -179,17 +228,26 @@ const moveToFolder = async (targetFolder) => {
});
store.messages = store.messages.filter((m) => m.id !== props.message.id);
store.currentMessage = null;
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch (e) {
console.error('Failed to move message', e);
catch {
store.showToast('Failed to move message');
}
};
const escapeHtml = (str) => str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const printMessage = () => {
if (!props.message)
return;
const from = props.message.from_name ? `${props.message.from_name} <${props.message.from_address}>` : props.message.from_address;
const toRaw = JSON.parse(props.message.to_addresses || '[]');
const to = Array.isArray(toRaw) ? toRaw.join(', ') : toRaw;
let toRaw = [];
try {
toRaw = JSON.parse(props.message.to_addresses || '[]');
}
catch {
toRaw = [];
}
const to = Array.isArray(toRaw) ? toRaw.join(', ') : String(toRaw);
const date = displayTimestamp.value;
const subject = props.message.subject || '(No Subject)';
let content = '';
@ -225,9 +283,9 @@ const printMessage = () => {
</head>
<body>
<div class="header">
<h1 class="subject">${subject}</h1>
<div class="meta"><div class="label">From:</div><div class="val">${from.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div></div>
<div class="meta"><div class="label">To:</div><div class="val">${to.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div></div>
<h1 class="subject">${escapeHtml(subject)}</h1>
<div class="meta"><div class="label">From:</div><div class="val">${escapeHtml(from)}</div></div>
<div class="meta"><div class="label">To:</div><div class="val">${escapeHtml(to)}</div></div>
<div class="meta"><div class="label">Date:</div><div class="val">${date}</div></div>
</div>
<div class="content">
@ -248,6 +306,8 @@ const printMessage = () => {
const sendInlineReply = async () => {
if (!props.message || !store.currentMailbox || !replyText.value.trim())
return;
if (!props.message.from_address)
return;
sendingReply.value = true;
try {
await mailApi.sendMessage(store.currentMailbox, {
@ -261,8 +321,8 @@ const sendInlineReply = async () => {
});
replyText.value = '';
}
catch (e) {
console.error('Failed to send reply', e);
catch {
store.showToast('Failed to send reply');
}
finally {
sendingReply.value = false;
@ -882,137 +942,169 @@ const __VLS_223 = __VLS_222({
...{ class: "z-50 min-w-[160px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
}, ...__VLS_functionalComponentArgsRest(__VLS_222));
__VLS_224.slots.default;
const __VLS_225 = {}.DropdownMenuItem;
if (props.message?.is_read) {
const __VLS_225 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_226 = __VLS_asFunctionalComponent(__VLS_225, new __VLS_225({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_227 = __VLS_226({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_226));
let __VLS_229;
let __VLS_230;
let __VLS_231;
const __VLS_232 = {
onClick: (__VLS_ctx.markUnread)
};
__VLS_228.slots.default;
const __VLS_233 = {}.MailOpen;
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
// @ts-ignore
const __VLS_234 = __VLS_asFunctionalComponent(__VLS_233, new __VLS_233({
...{ class: "mr-2 size-4" },
}));
const __VLS_235 = __VLS_234({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_234));
var __VLS_228;
}
else {
const __VLS_237 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_238 = __VLS_asFunctionalComponent(__VLS_237, new __VLS_237({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_239 = __VLS_238({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_238));
let __VLS_241;
let __VLS_242;
let __VLS_243;
const __VLS_244 = {
onClick: (__VLS_ctx.markRead)
};
__VLS_240.slots.default;
const __VLS_245 = {}.MailOpen;
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
// @ts-ignore
const __VLS_246 = __VLS_asFunctionalComponent(__VLS_245, new __VLS_245({
...{ class: "mr-2 size-4" },
}));
const __VLS_247 = __VLS_246({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_246));
var __VLS_240;
}
const __VLS_249 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_226 = __VLS_asFunctionalComponent(__VLS_225, new __VLS_225({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_227 = __VLS_226({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_226));
let __VLS_229;
let __VLS_230;
let __VLS_231;
const __VLS_232 = {
onClick: (__VLS_ctx.markUnread)
};
__VLS_228.slots.default;
const __VLS_233 = {}.MailOpen;
/** @type {[typeof __VLS_components.MailOpen, ]} */ ;
// @ts-ignore
const __VLS_234 = __VLS_asFunctionalComponent(__VLS_233, new __VLS_233({
...{ class: "mr-2 size-4" },
}));
const __VLS_235 = __VLS_234({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_234));
var __VLS_228;
const __VLS_237 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_238 = __VLS_asFunctionalComponent(__VLS_237, new __VLS_237({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_239 = __VLS_238({
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_238));
let __VLS_241;
let __VLS_242;
let __VLS_243;
const __VLS_244 = {
onClick: (__VLS_ctx.toggleStar)
};
__VLS_240.slots.default;
const __VLS_245 = {}.Star;
/** @type {[typeof __VLS_components.Star, ]} */ ;
// @ts-ignore
const __VLS_246 = __VLS_asFunctionalComponent(__VLS_245, new __VLS_245({
...{ class: "mr-2 size-4" },
}));
const __VLS_247 = __VLS_246({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_246));
(__VLS_ctx.message?.is_starred ? 'Unstar' : 'Star');
var __VLS_240;
const __VLS_249 = {}.DropdownMenuSeparator;
/** @type {[typeof __VLS_components.DropdownMenuSeparator, ]} */ ;
// @ts-ignore
const __VLS_250 = __VLS_asFunctionalComponent(__VLS_249, new __VLS_249({
...{ class: "my-1 h-px bg-border" },
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_251 = __VLS_250({
...{ class: "my-1 h-px bg-border" },
...{ 'onClick': {} },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_250));
const __VLS_253 = {}.DropdownMenuSub;
/** @type {[typeof __VLS_components.DropdownMenuSub, typeof __VLS_components.DropdownMenuSub, ]} */ ;
// @ts-ignore
const __VLS_254 = __VLS_asFunctionalComponent(__VLS_253, new __VLS_253({}));
const __VLS_255 = __VLS_254({}, ...__VLS_functionalComponentArgsRest(__VLS_254));
__VLS_256.slots.default;
const __VLS_257 = {}.DropdownMenuSubTrigger;
/** @type {[typeof __VLS_components.DropdownMenuSubTrigger, typeof __VLS_components.DropdownMenuSubTrigger, ]} */ ;
let __VLS_253;
let __VLS_254;
let __VLS_255;
const __VLS_256 = {
onClick: (__VLS_ctx.toggleStar)
};
__VLS_252.slots.default;
const __VLS_257 = {}.Star;
/** @type {[typeof __VLS_components.Star, ]} */ ;
// @ts-ignore
const __VLS_258 = __VLS_asFunctionalComponent(__VLS_257, new __VLS_257({
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
...{ class: "mr-2 size-4" },
}));
const __VLS_259 = __VLS_258({
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_258));
__VLS_260.slots.default;
const __VLS_261 = {}.FolderInput;
/** @type {[typeof __VLS_components.FolderInput, ]} */ ;
(__VLS_ctx.message?.is_starred ? 'Unstar' : 'Star');
var __VLS_252;
const __VLS_261 = {}.DropdownMenuSeparator;
/** @type {[typeof __VLS_components.DropdownMenuSeparator, ]} */ ;
// @ts-ignore
const __VLS_262 = __VLS_asFunctionalComponent(__VLS_261, new __VLS_261({
...{ class: "mr-2 size-4" },
...{ class: "my-1 h-px bg-border" },
}));
const __VLS_263 = __VLS_262({
...{ class: "mr-2 size-4" },
...{ class: "my-1 h-px bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_262));
var __VLS_260;
const __VLS_265 = {}.DropdownMenuPortal;
/** @type {[typeof __VLS_components.DropdownMenuPortal, typeof __VLS_components.DropdownMenuPortal, ]} */ ;
const __VLS_265 = {}.DropdownMenuSub;
/** @type {[typeof __VLS_components.DropdownMenuSub, typeof __VLS_components.DropdownMenuSub, ]} */ ;
// @ts-ignore
const __VLS_266 = __VLS_asFunctionalComponent(__VLS_265, new __VLS_265({}));
const __VLS_267 = __VLS_266({}, ...__VLS_functionalComponentArgsRest(__VLS_266));
__VLS_268.slots.default;
const __VLS_269 = {}.DropdownMenuSubContent;
/** @type {[typeof __VLS_components.DropdownMenuSubContent, typeof __VLS_components.DropdownMenuSubContent, ]} */ ;
const __VLS_269 = {}.DropdownMenuSubTrigger;
/** @type {[typeof __VLS_components.DropdownMenuSubTrigger, typeof __VLS_components.DropdownMenuSubTrigger, ]} */ ;
// @ts-ignore
const __VLS_270 = __VLS_asFunctionalComponent(__VLS_269, new __VLS_269({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_271 = __VLS_270({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_270));
__VLS_272.slots.default;
const __VLS_273 = {}.FolderInput;
/** @type {[typeof __VLS_components.FolderInput, ]} */ ;
// @ts-ignore
const __VLS_274 = __VLS_asFunctionalComponent(__VLS_273, new __VLS_273({
...{ class: "mr-2 size-4" },
}));
const __VLS_275 = __VLS_274({
...{ class: "mr-2 size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_274));
var __VLS_272;
const __VLS_277 = {}.DropdownMenuPortal;
/** @type {[typeof __VLS_components.DropdownMenuPortal, typeof __VLS_components.DropdownMenuPortal, ]} */ ;
// @ts-ignore
const __VLS_278 = __VLS_asFunctionalComponent(__VLS_277, new __VLS_277({}));
const __VLS_279 = __VLS_278({}, ...__VLS_functionalComponentArgsRest(__VLS_278));
__VLS_280.slots.default;
const __VLS_281 = {}.DropdownMenuSubContent;
/** @type {[typeof __VLS_components.DropdownMenuSubContent, typeof __VLS_components.DropdownMenuSubContent, ]} */ ;
// @ts-ignore
const __VLS_282 = __VLS_asFunctionalComponent(__VLS_281, new __VLS_281({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
}));
const __VLS_283 = __VLS_282({
...{ class: "z-50 min-w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md" },
}, ...__VLS_functionalComponentArgsRest(__VLS_282));
__VLS_284.slots.default;
for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
const __VLS_273 = {}.DropdownMenuItem;
const __VLS_285 = {}.DropdownMenuItem;
/** @type {[typeof __VLS_components.DropdownMenuItem, typeof __VLS_components.DropdownMenuItem, ]} */ ;
// @ts-ignore
const __VLS_274 = __VLS_asFunctionalComponent(__VLS_273, new __VLS_273({
const __VLS_286 = __VLS_asFunctionalComponent(__VLS_285, new __VLS_285({
...{ 'onClick': {} },
key: (f.id),
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}));
const __VLS_275 = __VLS_274({
const __VLS_287 = __VLS_286({
...{ 'onClick': {} },
key: (f.id),
...{ class: "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent" },
}, ...__VLS_functionalComponentArgsRest(__VLS_274));
let __VLS_277;
let __VLS_278;
let __VLS_279;
const __VLS_280 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_286));
let __VLS_289;
let __VLS_290;
let __VLS_291;
const __VLS_292 = {
onClick: (...[$event]) => {
__VLS_ctx.moveToFolder(f);
}
};
__VLS_276.slots.default;
__VLS_288.slots.default;
if (f.color) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span)({
...{ class: "mr-2 inline-block h-2 w-2 rounded-full flex-shrink-0" },
@ -1020,11 +1112,11 @@ for (const [f] of __VLS_getVForSourceType((__VLS_ctx.otherFolders))) {
});
}
(f.name);
var __VLS_276;
var __VLS_288;
}
var __VLS_272;
var __VLS_284;
var __VLS_280;
var __VLS_268;
var __VLS_256;
var __VLS_224;
var __VLS_220;
var __VLS_205;
@ -1052,12 +1144,12 @@ if (__VLS_ctx.message) {
});
/** @type {[typeof Avatar, ]} */ ;
// @ts-ignore
const __VLS_281 = __VLS_asFunctionalComponent(Avatar, new Avatar({
const __VLS_293 = __VLS_asFunctionalComponent(Avatar, new Avatar({
initials: (__VLS_ctx.message.from_name?.[0] || __VLS_ctx.message.from_address?.[0] || '?'),
}));
const __VLS_282 = __VLS_281({
const __VLS_294 = __VLS_293({
initials: (__VLS_ctx.message.from_name?.[0] || __VLS_ctx.message.from_address?.[0] || '?'),
}, ...__VLS_functionalComponentArgsRest(__VLS_281));
}, ...__VLS_functionalComponentArgsRest(__VLS_293));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "grid gap-1" },
});
@ -1065,17 +1157,37 @@ if (__VLS_ctx.message) {
...{ class: "font-semibold" },
});
(__VLS_ctx.message.from_name || __VLS_ctx.message.from_address);
if (__VLS_ctx.toDisplay) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.toDisplay);
}
if (__VLS_ctx.ccDisplay) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.ccDisplay);
}
if (__VLS_ctx.bccDisplay) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.bccDisplay);
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
(__VLS_ctx.message.subject);
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "line-clamp-1 text-xs" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "font-medium" },
});
(__VLS_ctx.message.from_address);
if (__VLS_ctx.displayTimestamp) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "ml-auto text-xs text-muted-foreground" },
@ -1084,8 +1196,8 @@ if (__VLS_ctx.message) {
}
/** @type {[typeof Separator, ]} */ ;
// @ts-ignore
const __VLS_284 = __VLS_asFunctionalComponent(Separator, new Separator({}));
const __VLS_285 = __VLS_284({}, ...__VLS_functionalComponentArgsRest(__VLS_284));
const __VLS_296 = __VLS_asFunctionalComponent(Separator, new Separator({}));
const __VLS_297 = __VLS_296({}, ...__VLS_functionalComponentArgsRest(__VLS_296));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex-1 overflow-y-auto" },
});
@ -1109,20 +1221,20 @@ if (__VLS_ctx.message) {
}
/** @type {[typeof AttachmentBar, ]} */ ;
// @ts-ignore
const __VLS_287 = __VLS_asFunctionalComponent(AttachmentBar, new AttachmentBar({
const __VLS_299 = __VLS_asFunctionalComponent(AttachmentBar, new AttachmentBar({
attachments: (__VLS_ctx.message.attachments),
}));
const __VLS_288 = __VLS_287({
const __VLS_300 = __VLS_299({
attachments: (__VLS_ctx.message.attachments),
}, ...__VLS_functionalComponentArgsRest(__VLS_287));
}, ...__VLS_functionalComponentArgsRest(__VLS_299));
/** @type {[typeof Separator, ]} */ ;
// @ts-ignore
const __VLS_290 = __VLS_asFunctionalComponent(Separator, new Separator({
const __VLS_302 = __VLS_asFunctionalComponent(Separator, new Separator({
...{ class: "mt-auto print-hide" },
}));
const __VLS_291 = __VLS_290({
const __VLS_303 = __VLS_302({
...{ class: "mt-auto print-hide" },
}, ...__VLS_functionalComponentArgsRest(__VLS_290));
}, ...__VLS_functionalComponentArgsRest(__VLS_302));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-4 print-hide" },
});
@ -1134,36 +1246,36 @@ if (__VLS_ctx.message) {
});
/** @type {[typeof Textarea, ]} */ ;
// @ts-ignore
const __VLS_293 = __VLS_asFunctionalComponent(Textarea, new Textarea({
const __VLS_305 = __VLS_asFunctionalComponent(Textarea, new Textarea({
modelValue: (__VLS_ctx.replyText),
...{ class: "p-4 min-h-[100px]" },
placeholder: (`Reply ${__VLS_ctx.message.from_name || __VLS_ctx.message.from_address}...`),
}));
const __VLS_294 = __VLS_293({
const __VLS_306 = __VLS_305({
modelValue: (__VLS_ctx.replyText),
...{ class: "p-4 min-h-[100px]" },
placeholder: (`Reply ${__VLS_ctx.message.from_name || __VLS_ctx.message.from_address}...`),
}, ...__VLS_functionalComponentArgsRest(__VLS_293));
}, ...__VLS_functionalComponentArgsRest(__VLS_305));
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center" },
});
/** @type {[typeof Button, typeof Button, ]} */ ;
// @ts-ignore
const __VLS_296 = __VLS_asFunctionalComponent(Button, new Button({
const __VLS_308 = __VLS_asFunctionalComponent(Button, new Button({
type: "submit",
size: "sm",
...{ class: "ml-auto" },
disabled: (__VLS_ctx.sendingReply || !__VLS_ctx.replyText.trim()),
}));
const __VLS_297 = __VLS_296({
const __VLS_309 = __VLS_308({
type: "submit",
size: "sm",
...{ class: "ml-auto" },
disabled: (__VLS_ctx.sendingReply || !__VLS_ctx.replyText.trim()),
}, ...__VLS_functionalComponentArgsRest(__VLS_296));
__VLS_298.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_308));
__VLS_310.slots.default;
(__VLS_ctx.sendingReply ? 'Sending...' : 'Send');
var __VLS_298;
var __VLS_310;
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
@ -1298,6 +1410,19 @@ else {
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['cursor-pointer']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['px-2']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['outline-none']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['mr-2']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
/** @type {__VLS_StyleScopedClasses['my-1']} */ ;
/** @type {__VLS_StyleScopedClasses['h-px']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-border']} */ ;
@ -1363,9 +1488,15 @@ else {
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['line-clamp-1']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['ml-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['text-xs']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
@ -1434,6 +1565,9 @@ const __VLS_self = (await import('vue')).defineComponent({
emailIframe: emailIframe,
safeHtml: safeHtml,
resizeIframe: resizeIframe,
toDisplay: toDisplay,
ccDisplay: ccDisplay,
bccDisplay: bccDisplay,
displayTimestamp: displayTimestamp,
otherFolders: otherFolders,
replyTo: replyTo,
@ -1442,6 +1576,7 @@ const __VLS_self = (await import('vue')).defineComponent({
backToList: backToList,
trash: trash,
markUnread: markUnread,
markRead: markRead,
toggleStar: toggleStar,
moveToFolder: moveToFolder,
printMessage: printMessage,

File diff suppressed because one or more lines are too long

View file

@ -1,31 +1,21 @@
/// <reference types="../../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { ref, computed } from 'vue';
import { Search, ArrowDownUp, Trash2 } from 'lucide-vue-next';
import { ArrowDownUp, Trash2 } from 'lucide-vue-next';
import { TabsRoot, TabsList, TabsTrigger, TabsContent, } from 'radix-vue';
import { ScrollAreaRoot, ScrollAreaViewport, ScrollAreaScrollbar, ScrollAreaThumb, } from 'radix-vue';
import { useMailStore } from '../../stores/mail';
import { mailApi } from '../../api/mail';
import MessageListItem from './MessageListItem.vue';
import Input from '../ui/Input.vue';
import SearchBar from './SearchBar.vue';
import Dialog from '../ui/Dialog.vue';
import Button from '../ui/Button.vue';
const store = useMailStore();
const searchValue = ref('');
const showTrashConfirm = ref(false);
const folderColor = computed(() => store.currentFolderObj?.color || '');
const filteredMessages = computed(() => {
const q = searchValue.value.trim().toLowerCase();
if (!q)
return store.messages;
return store.messages.filter((m) => (m.from_name || '').toLowerCase().includes(q) ||
(m.from_address || '').toLowerCase().includes(q) ||
(m.subject || '').toLowerCase().includes(q) ||
(m.text_body || '').toLowerCase().includes(q));
});
const unreadMessages = computed(() => filteredMessages.value.filter((m) => !m.is_read));
const starredMessages = computed(() => filteredMessages.value.filter((m) => m.is_starred));
const unreadMessages = computed(() => store.messages.filter((m) => !m.is_read));
const starredMessages = computed(() => store.messages.filter((m) => m.is_starred));
const displayMessages = computed(() => {
let msgs = filteredMessages.value;
let msgs = store.messages;
if (store.activeTab === 'unread')
msgs = unreadMessages.value;
else if (store.activeTab === 'starred')
@ -68,17 +58,20 @@ const emptyTrash = () => {
}
};
const performEmptyTrash = async () => {
if (store.currentFolderObj) {
try {
await mailApi.emptyFolder(store.currentMailbox, store.currentFolderObj.id);
store.messages = [];
store.currentMessage = null;
}
catch (e) {
}
finally {
showTrashConfirm.value = false;
}
if (!store.currentFolderObj)
return;
try {
await mailApi.emptyFolder(store.currentMailbox, store.currentFolderObj.id);
store.messages = [];
store.currentMessage = null;
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
catch {
store.showToast('Failed to empty trash');
}
finally {
showTrashConfirm.value = false;
}
};
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
@ -195,44 +188,34 @@ var __VLS_15;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "relative" },
});
const __VLS_28 = {}.Search;
/** @type {[typeof __VLS_components.Search, ]} */ ;
/** @type {[typeof SearchBar, ]} */ ;
// @ts-ignore
const __VLS_29 = __VLS_asFunctionalComponent(__VLS_28, new __VLS_28({
...{ class: "absolute left-2 top-2.5 size-4 text-muted-foreground" },
}));
const __VLS_30 = __VLS_29({
...{ class: "absolute left-2 top-2.5 size-4 text-muted-foreground" },
}, ...__VLS_functionalComponentArgsRest(__VLS_29));
/** @type {[typeof Input, ]} */ ;
// @ts-ignore
const __VLS_32 = __VLS_asFunctionalComponent(Input, new Input({
modelValue: (__VLS_ctx.searchValue),
placeholder: "Search",
...{ class: "pl-8" },
}));
const __VLS_33 = __VLS_32({
modelValue: (__VLS_ctx.searchValue),
placeholder: "Search",
...{ class: "pl-8" },
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
const __VLS_35 = {}.TabsContent;
const __VLS_28 = __VLS_asFunctionalComponent(SearchBar, new SearchBar({}));
const __VLS_29 = __VLS_28({}, ...__VLS_functionalComponentArgsRest(__VLS_28));
const __VLS_31 = {}.TabsContent;
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
// @ts-ignore
const __VLS_36 = __VLS_asFunctionalComponent(__VLS_35, new __VLS_35({
const __VLS_32 = __VLS_asFunctionalComponent(__VLS_31, new __VLS_31({
value: "all",
...{ class: "m-0 flex-1 overflow-hidden" },
}));
const __VLS_37 = __VLS_36({
const __VLS_33 = __VLS_32({
value: "all",
...{ class: "m-0 flex-1 overflow-hidden" },
}, ...__VLS_functionalComponentArgsRest(__VLS_32));
__VLS_34.slots.default;
const __VLS_35 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
// @ts-ignore
const __VLS_36 = __VLS_asFunctionalComponent(__VLS_35, new __VLS_35({
...{ class: "h-full" },
}));
const __VLS_37 = __VLS_36({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_36));
__VLS_38.slots.default;
const __VLS_39 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
const __VLS_39 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_40 = __VLS_asFunctionalComponent(__VLS_39, new __VLS_39({
...{ class: "h-full" },
@ -241,103 +224,129 @@ const __VLS_41 = __VLS_40({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_40));
__VLS_42.slots.default;
const __VLS_43 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_44 = __VLS_asFunctionalComponent(__VLS_43, new __VLS_43({
...{ class: "h-full" },
}));
const __VLS_45 = __VLS_44({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_44));
__VLS_46.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-col gap-2 p-4 pt-0" },
});
const __VLS_47 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_48 = __VLS_asFunctionalComponent(__VLS_47, new __VLS_47({
name: "list",
appear: true,
}));
const __VLS_49 = __VLS_48({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_48));
__VLS_50.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
if (__VLS_ctx.store.messagesLoading) {
for (const [n] of __VLS_getVForSourceType((6))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
key: (n),
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
});
}
}
else {
const __VLS_43 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_51 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
const __VLS_44 = __VLS_asFunctionalComponent(__VLS_43, new __VLS_43({
name: "list",
appear: true,
}));
const __VLS_52 = __VLS_51({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_51));
let __VLS_54;
let __VLS_55;
let __VLS_56;
const __VLS_57 = {
onClick: (...[$event]) => {
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_53;
const __VLS_45 = __VLS_44({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_44));
__VLS_46.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
// @ts-ignore
const __VLS_47 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}));
const __VLS_48 = __VLS_47({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_47));
let __VLS_50;
let __VLS_51;
let __VLS_52;
const __VLS_53 = {
onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_49;
}
var __VLS_46;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.store.loadMore();
} },
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
disabled: (__VLS_ctx.store.isFetchingNextPage),
});
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
}
}
var __VLS_50;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
var __VLS_46;
const __VLS_58 = {}.ScrollAreaScrollbar;
var __VLS_42;
const __VLS_54 = {}.ScrollAreaScrollbar;
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
// @ts-ignore
const __VLS_59 = __VLS_asFunctionalComponent(__VLS_58, new __VLS_58({
const __VLS_55 = __VLS_asFunctionalComponent(__VLS_54, new __VLS_54({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}));
const __VLS_60 = __VLS_59({
const __VLS_56 = __VLS_55({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}, ...__VLS_functionalComponentArgsRest(__VLS_59));
__VLS_61.slots.default;
const __VLS_62 = {}.ScrollAreaThumb;
}, ...__VLS_functionalComponentArgsRest(__VLS_55));
__VLS_57.slots.default;
const __VLS_58 = {}.ScrollAreaThumb;
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
// @ts-ignore
const __VLS_63 = __VLS_asFunctionalComponent(__VLS_62, new __VLS_62({
const __VLS_59 = __VLS_asFunctionalComponent(__VLS_58, new __VLS_58({
...{ class: "relative flex-1 rounded-full bg-border" },
}));
const __VLS_64 = __VLS_63({
const __VLS_60 = __VLS_59({
...{ class: "relative flex-1 rounded-full bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_63));
var __VLS_61;
var __VLS_42;
}, ...__VLS_functionalComponentArgsRest(__VLS_59));
var __VLS_57;
var __VLS_38;
const __VLS_66 = {}.TabsContent;
var __VLS_34;
const __VLS_62 = {}.TabsContent;
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
// @ts-ignore
const __VLS_67 = __VLS_asFunctionalComponent(__VLS_66, new __VLS_66({
const __VLS_63 = __VLS_asFunctionalComponent(__VLS_62, new __VLS_62({
value: "unread",
...{ class: "m-0 flex-1 overflow-hidden" },
}));
const __VLS_68 = __VLS_67({
const __VLS_64 = __VLS_63({
value: "unread",
...{ class: "m-0 flex-1 overflow-hidden" },
}, ...__VLS_functionalComponentArgsRest(__VLS_63));
__VLS_65.slots.default;
const __VLS_66 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
// @ts-ignore
const __VLS_67 = __VLS_asFunctionalComponent(__VLS_66, new __VLS_66({
...{ class: "h-full" },
}));
const __VLS_68 = __VLS_67({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_67));
__VLS_69.slots.default;
const __VLS_70 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
const __VLS_70 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_71 = __VLS_asFunctionalComponent(__VLS_70, new __VLS_70({
...{ class: "h-full" },
@ -346,103 +355,129 @@ const __VLS_72 = __VLS_71({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_71));
__VLS_73.slots.default;
const __VLS_74 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_75 = __VLS_asFunctionalComponent(__VLS_74, new __VLS_74({
...{ class: "h-full" },
}));
const __VLS_76 = __VLS_75({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_75));
__VLS_77.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-col gap-2 p-4 pt-0" },
});
const __VLS_78 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_79 = __VLS_asFunctionalComponent(__VLS_78, new __VLS_78({
name: "list",
appear: true,
}));
const __VLS_80 = __VLS_79({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_79));
__VLS_81.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
if (__VLS_ctx.store.messagesLoading) {
for (const [n] of __VLS_getVForSourceType((6))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
key: (n),
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
});
}
}
else {
const __VLS_74 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_82 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
const __VLS_75 = __VLS_asFunctionalComponent(__VLS_74, new __VLS_74({
name: "list",
appear: true,
}));
const __VLS_83 = __VLS_82({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_82));
let __VLS_85;
let __VLS_86;
let __VLS_87;
const __VLS_88 = {
onClick: (...[$event]) => {
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_84;
const __VLS_76 = __VLS_75({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_75));
__VLS_77.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
// @ts-ignore
const __VLS_78 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}));
const __VLS_79 = __VLS_78({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_78));
let __VLS_81;
let __VLS_82;
let __VLS_83;
const __VLS_84 = {
onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_80;
}
var __VLS_77;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.store.loadMore();
} },
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
disabled: (__VLS_ctx.store.isFetchingNextPage),
});
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
}
}
var __VLS_81;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
var __VLS_77;
const __VLS_89 = {}.ScrollAreaScrollbar;
var __VLS_73;
const __VLS_85 = {}.ScrollAreaScrollbar;
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
// @ts-ignore
const __VLS_90 = __VLS_asFunctionalComponent(__VLS_89, new __VLS_89({
const __VLS_86 = __VLS_asFunctionalComponent(__VLS_85, new __VLS_85({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}));
const __VLS_91 = __VLS_90({
const __VLS_87 = __VLS_86({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}, ...__VLS_functionalComponentArgsRest(__VLS_90));
__VLS_92.slots.default;
const __VLS_93 = {}.ScrollAreaThumb;
}, ...__VLS_functionalComponentArgsRest(__VLS_86));
__VLS_88.slots.default;
const __VLS_89 = {}.ScrollAreaThumb;
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
// @ts-ignore
const __VLS_94 = __VLS_asFunctionalComponent(__VLS_93, new __VLS_93({
const __VLS_90 = __VLS_asFunctionalComponent(__VLS_89, new __VLS_89({
...{ class: "relative flex-1 rounded-full bg-border" },
}));
const __VLS_95 = __VLS_94({
const __VLS_91 = __VLS_90({
...{ class: "relative flex-1 rounded-full bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_94));
var __VLS_92;
var __VLS_73;
}, ...__VLS_functionalComponentArgsRest(__VLS_90));
var __VLS_88;
var __VLS_69;
const __VLS_97 = {}.TabsContent;
var __VLS_65;
const __VLS_93 = {}.TabsContent;
/** @type {[typeof __VLS_components.TabsContent, typeof __VLS_components.TabsContent, ]} */ ;
// @ts-ignore
const __VLS_98 = __VLS_asFunctionalComponent(__VLS_97, new __VLS_97({
const __VLS_94 = __VLS_asFunctionalComponent(__VLS_93, new __VLS_93({
value: "starred",
...{ class: "m-0 flex-1 overflow-hidden" },
}));
const __VLS_99 = __VLS_98({
const __VLS_95 = __VLS_94({
value: "starred",
...{ class: "m-0 flex-1 overflow-hidden" },
}, ...__VLS_functionalComponentArgsRest(__VLS_94));
__VLS_96.slots.default;
const __VLS_97 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
// @ts-ignore
const __VLS_98 = __VLS_asFunctionalComponent(__VLS_97, new __VLS_97({
...{ class: "h-full" },
}));
const __VLS_99 = __VLS_98({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_98));
__VLS_100.slots.default;
const __VLS_101 = {}.ScrollAreaRoot;
/** @type {[typeof __VLS_components.ScrollAreaRoot, typeof __VLS_components.ScrollAreaRoot, ]} */ ;
const __VLS_101 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_102 = __VLS_asFunctionalComponent(__VLS_101, new __VLS_101({
...{ class: "h-full" },
@ -451,99 +486,115 @@ const __VLS_103 = __VLS_102({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_102));
__VLS_104.slots.default;
const __VLS_105 = {}.ScrollAreaViewport;
/** @type {[typeof __VLS_components.ScrollAreaViewport, typeof __VLS_components.ScrollAreaViewport, ]} */ ;
// @ts-ignore
const __VLS_106 = __VLS_asFunctionalComponent(__VLS_105, new __VLS_105({
...{ class: "h-full" },
}));
const __VLS_107 = __VLS_106({
...{ class: "h-full" },
}, ...__VLS_functionalComponentArgsRest(__VLS_106));
__VLS_108.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex flex-col gap-2 p-4 pt-0" },
});
const __VLS_109 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_110 = __VLS_asFunctionalComponent(__VLS_109, new __VLS_109({
name: "list",
appear: true,
}));
const __VLS_111 = __VLS_110({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_110));
__VLS_112.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
if (__VLS_ctx.store.messagesLoading) {
for (const [n] of __VLS_getVForSourceType((6))) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div)({
key: (n),
...{ class: "h-16 rounded-lg bg-muted animate-pulse" },
});
}
}
else {
const __VLS_105 = {}.TransitionGroup;
/** @type {[typeof __VLS_components.TransitionGroup, typeof __VLS_components.TransitionGroup, ]} */ ;
// @ts-ignore
const __VLS_113 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
const __VLS_106 = __VLS_asFunctionalComponent(__VLS_105, new __VLS_105({
name: "list",
appear: true,
}));
const __VLS_114 = __VLS_113({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_113));
let __VLS_116;
let __VLS_117;
let __VLS_118;
const __VLS_119 = {
onClick: (...[$event]) => {
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_115;
const __VLS_107 = __VLS_106({
name: "list",
appear: true,
}, ...__VLS_functionalComponentArgsRest(__VLS_106));
__VLS_108.slots.default;
for (const [msg] of __VLS_getVForSourceType((__VLS_ctx.displayMessages))) {
/** @type {[typeof MessageListItem, ]} */ ;
// @ts-ignore
const __VLS_109 = __VLS_asFunctionalComponent(MessageListItem, new MessageListItem({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}));
const __VLS_110 = __VLS_109({
...{ 'onClick': {} },
key: (msg.id),
message: (msg),
selected: (__VLS_ctx.store.currentMessage?.id === msg.id),
folderColor: (__VLS_ctx.folderColor),
}, ...__VLS_functionalComponentArgsRest(__VLS_109));
let __VLS_112;
let __VLS_113;
let __VLS_114;
const __VLS_115 = {
onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.selectMessage(msg);
}
};
var __VLS_111;
}
var __VLS_108;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
if (__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.store.messagesLoading))
return;
if (!(__VLS_ctx.store.hasMoreMessages && !__VLS_ctx.store.messagesLoading))
return;
__VLS_ctx.store.loadMore();
} },
...{ class: "w-full py-2 text-sm text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50" },
disabled: (__VLS_ctx.store.isFetchingNextPage),
});
(__VLS_ctx.store.isFetchingNextPage ? 'Loading…' : 'Load more');
}
}
var __VLS_112;
if (__VLS_ctx.displayMessages.length === 0) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 text-center text-muted-foreground" },
});
}
var __VLS_108;
const __VLS_120 = {}.ScrollAreaScrollbar;
var __VLS_104;
const __VLS_116 = {}.ScrollAreaScrollbar;
/** @type {[typeof __VLS_components.ScrollAreaScrollbar, typeof __VLS_components.ScrollAreaScrollbar, ]} */ ;
// @ts-ignore
const __VLS_121 = __VLS_asFunctionalComponent(__VLS_120, new __VLS_120({
const __VLS_117 = __VLS_asFunctionalComponent(__VLS_116, new __VLS_116({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}));
const __VLS_122 = __VLS_121({
const __VLS_118 = __VLS_117({
orientation: "vertical",
...{ class: "flex touch-none select-none bg-transparent p-0.5 transition-colors w-2.5" },
}, ...__VLS_functionalComponentArgsRest(__VLS_121));
__VLS_123.slots.default;
const __VLS_124 = {}.ScrollAreaThumb;
}, ...__VLS_functionalComponentArgsRest(__VLS_117));
__VLS_119.slots.default;
const __VLS_120 = {}.ScrollAreaThumb;
/** @type {[typeof __VLS_components.ScrollAreaThumb, ]} */ ;
// @ts-ignore
const __VLS_125 = __VLS_asFunctionalComponent(__VLS_124, new __VLS_124({
const __VLS_121 = __VLS_asFunctionalComponent(__VLS_120, new __VLS_120({
...{ class: "relative flex-1 rounded-full bg-border" },
}));
const __VLS_126 = __VLS_125({
const __VLS_122 = __VLS_121({
...{ class: "relative flex-1 rounded-full bg-border" },
}, ...__VLS_functionalComponentArgsRest(__VLS_125));
var __VLS_123;
var __VLS_104;
}, ...__VLS_functionalComponentArgsRest(__VLS_121));
var __VLS_119;
var __VLS_100;
var __VLS_96;
var __VLS_3;
/** @type {[typeof Dialog, typeof Dialog, ]} */ ;
// @ts-ignore
const __VLS_128 = __VLS_asFunctionalComponent(Dialog, new Dialog({
const __VLS_124 = __VLS_asFunctionalComponent(Dialog, new Dialog({
open: (__VLS_ctx.showTrashConfirm),
}));
const __VLS_129 = __VLS_128({
const __VLS_125 = __VLS_124({
open: (__VLS_ctx.showTrashConfirm),
}, ...__VLS_functionalComponentArgsRest(__VLS_128));
__VLS_130.slots.default;
}, ...__VLS_functionalComponentArgsRest(__VLS_124));
__VLS_126.slots.default;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "space-y-4" },
});
@ -558,43 +609,43 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
});
/** @type {[typeof Button, typeof Button, ]} */ ;
// @ts-ignore
const __VLS_131 = __VLS_asFunctionalComponent(Button, new Button({
const __VLS_127 = __VLS_asFunctionalComponent(Button, new Button({
...{ 'onClick': {} },
variant: "outline",
}));
const __VLS_132 = __VLS_131({
const __VLS_128 = __VLS_127({
...{ 'onClick': {} },
variant: "outline",
}, ...__VLS_functionalComponentArgsRest(__VLS_131));
let __VLS_134;
let __VLS_135;
let __VLS_136;
const __VLS_137 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_127));
let __VLS_130;
let __VLS_131;
let __VLS_132;
const __VLS_133 = {
onClick: (...[$event]) => {
__VLS_ctx.showTrashConfirm = false;
}
};
__VLS_133.slots.default;
var __VLS_133;
__VLS_129.slots.default;
var __VLS_129;
/** @type {[typeof Button, typeof Button, ]} */ ;
// @ts-ignore
const __VLS_138 = __VLS_asFunctionalComponent(Button, new Button({
const __VLS_134 = __VLS_asFunctionalComponent(Button, new Button({
...{ 'onClick': {} },
variant: "destructive",
}));
const __VLS_139 = __VLS_138({
const __VLS_135 = __VLS_134({
...{ 'onClick': {} },
variant: "destructive",
}, ...__VLS_functionalComponentArgsRest(__VLS_138));
let __VLS_141;
let __VLS_142;
let __VLS_143;
const __VLS_144 = {
}, ...__VLS_functionalComponentArgsRest(__VLS_134));
let __VLS_137;
let __VLS_138;
let __VLS_139;
const __VLS_140 = {
onClick: (__VLS_ctx.performEmptyTrash)
};
__VLS_140.slots.default;
var __VLS_140;
var __VLS_130;
__VLS_136.slots.default;
var __VLS_136;
var __VLS_126;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-col']} */ ;
@ -697,13 +748,6 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['backdrop-blur']} */ ;
/** @type {__VLS_StyleScopedClasses['supports-[backdrop-filter]:bg-background/60']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['left-2']} */ ;
/** @type {__VLS_StyleScopedClasses['top-2.5']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['pl-8']} */ ;
/** @type {__VLS_StyleScopedClasses['m-0']} */ ;
/** @type {__VLS_StyleScopedClasses['flex-1']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-hidden']} */ ;
@ -714,9 +758,20 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
@ -738,9 +793,20 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
@ -762,9 +828,20 @@ var __VLS_130;
/** @type {__VLS_StyleScopedClasses['gap-2']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['pt-0']} */ ;
/** @type {__VLS_StyleScopedClasses['h-16']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['animate-pulse']} */ ;
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['text-center']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['touch-none']} */ ;
/** @type {__VLS_StyleScopedClasses['select-none']} */ ;
@ -791,7 +868,6 @@ var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
Search: Search,
ArrowDownUp: ArrowDownUp,
Trash2: Trash2,
TabsRoot: TabsRoot,
@ -803,11 +879,10 @@ const __VLS_self = (await import('vue')).defineComponent({
ScrollAreaScrollbar: ScrollAreaScrollbar,
ScrollAreaThumb: ScrollAreaThumb,
MessageListItem: MessageListItem,
Input: Input,
SearchBar: SearchBar,
Dialog: Dialog,
Button: Button,
store: store,
searchValue: searchValue,
showTrashConfirm: showTrashConfirm,
folderColor: folderColor,
displayMessages: displayMessages,

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ import { Paperclip, Star } from 'lucide-vue-next'
import { TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal } from 'radix-vue'
import { cn } from '../../lib/utils'
import Badge from '../ui/Badge.vue'
import { useMailStore } from '../../stores/mail'
const props = defineProps({
message: { type: Object, required: true },
@ -12,6 +13,8 @@ const props = defineProps({
folderColor: { type: String, default: '' },
})
const store = useMailStore()
const timestamp = computed(() => props.message.received_at || props.message.sent_at)
const relativeTime = computed(() =>
timestamp.value ? formatDistanceToNow(new Date(timestamp.value), { addSuffix: true }) : ''
@ -19,6 +22,22 @@ const relativeTime = computed(() =>
const exactTime = computed(() =>
timestamp.value ? format(new Date(timestamp.value), 'PPpp') : ''
)
const isSentOrDrafts = computed(() => {
const name = store.currentFolderObj?.name?.toLowerCase() ?? ''
return name === 'sent' || name === 'drafts'
})
const recipientLabel = computed(() => {
if (!isSentOrDrafts.value) return null
let addrs: string[] = []
try { addrs = JSON.parse(props.message.to_addresses || '[]') } catch { addrs = [] }
if (!addrs.length) return null
return 'To: ' + addrs.map((a: string) => {
const m = a.match(/<([^>]+)>/)
return m ? m[1] : a
}).join(', ')
})
</script>
<template>
@ -32,7 +51,7 @@ const exactTime = computed(() =>
<div class="flex w-full flex-col gap-1">
<div class="flex items-center">
<div class="flex items-center gap-2">
<div class="font-semibold">{{ message.from_name || message.from_address }}</div>
<div class="font-semibold">{{ recipientLabel ?? (message.from_name || message.from_address) }}</div>
<span v-if="!message.is_read" class="flex h-2 w-2 rounded-full bg-primary" />
<Star v-if="message.is_starred" class="size-3 fill-yellow-400 text-yellow-400" />
</div>

View file

@ -5,14 +5,37 @@ import { Paperclip, Star } from 'lucide-vue-next';
import { TooltipRoot, TooltipTrigger, TooltipContent, TooltipPortal } from 'radix-vue';
import { cn } from '../../lib/utils';
import Badge from '../ui/Badge.vue';
import { useMailStore } from '../../stores/mail';
const props = defineProps({
message: { type: Object, required: true },
selected: { type: Boolean, default: false },
folderColor: { type: String, default: '' },
});
const store = useMailStore();
const timestamp = computed(() => props.message.received_at || props.message.sent_at);
const relativeTime = computed(() => timestamp.value ? formatDistanceToNow(new Date(timestamp.value), { addSuffix: true }) : '');
const exactTime = computed(() => timestamp.value ? format(new Date(timestamp.value), 'PPpp') : '');
const isSentOrDrafts = computed(() => {
const name = store.currentFolderObj?.name?.toLowerCase() ?? '';
return name === 'sent' || name === 'drafts';
});
const recipientLabel = computed(() => {
if (!isSentOrDrafts.value)
return null;
let addrs = [];
try {
addrs = JSON.parse(props.message.to_addresses || '[]');
}
catch {
addrs = [];
}
if (!addrs.length)
return null;
return 'To: ' + addrs.map((a) => {
const m = a.match(/<([^>]+)>/);
return m ? m[1] : a;
}).join(', ');
});
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
@ -33,7 +56,7 @@ __VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.d
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "font-semibold" },
});
(__VLS_ctx.message.from_name || __VLS_ctx.message.from_address);
(__VLS_ctx.recipientLabel ?? (__VLS_ctx.message.from_name || __VLS_ctx.message.from_address));
if (!__VLS_ctx.message.is_read) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.span)({
...{ class: "flex h-2 w-2 rounded-full bg-primary" },
@ -183,6 +206,7 @@ const __VLS_self = (await import('vue')).defineComponent({
timestamp: timestamp,
relativeTime: relativeTime,
exactTime: exactTime,
recipientLabel: recipientLabel,
};
},
props: {

File diff suppressed because one or more lines are too long

View file

@ -14,7 +14,7 @@ const store = useMailStore()
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
@click.self="store.isSettingsOpen = false"
>
<div class="relative w-full max-w-3xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto">
<div class="relative w-full max-w-5xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto">
<button
class="absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
@click="store.isSettingsOpen = false"

View file

@ -0,0 +1,115 @@
/// <reference types="../../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { X } from 'lucide-vue-next';
import { useMailStore } from '../../stores/mail';
import SettingsContent from '../settings/SettingsContent.vue';
const store = useMailStore();
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
// CSS variable injection
// CSS variable injection end
const __VLS_0 = {}.Teleport;
/** @type {[typeof __VLS_components.Teleport, typeof __VLS_components.Teleport, ]} */ ;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent(__VLS_0, new __VLS_0({
to: "body",
}));
const __VLS_2 = __VLS_1({
to: "body",
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
__VLS_3.slots.default;
const __VLS_4 = {}.Transition;
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
// @ts-ignore
const __VLS_5 = __VLS_asFunctionalComponent(__VLS_4, new __VLS_4({
name: "fade",
}));
const __VLS_6 = __VLS_5({
name: "fade",
}, ...__VLS_functionalComponentArgsRest(__VLS_5));
__VLS_7.slots.default;
if (__VLS_ctx.store.isSettingsOpen) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.store.isSettingsOpen))
return;
__VLS_ctx.store.isSettingsOpen = false;
} },
...{ class: "fixed inset-0 z-50 flex items-center justify-center bg-black/50" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "relative w-full max-w-5xl rounded-xl border bg-background shadow-xl mx-4 max-h-[90vh] overflow-y-auto" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!(__VLS_ctx.store.isSettingsOpen))
return;
__VLS_ctx.store.isSettingsOpen = false;
} },
...{ class: "absolute right-4 top-4 inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors" },
});
const __VLS_8 = {}.X;
/** @type {[typeof __VLS_components.X, ]} */ ;
// @ts-ignore
const __VLS_9 = __VLS_asFunctionalComponent(__VLS_8, new __VLS_8({
...{ class: "size-4" },
}));
const __VLS_10 = __VLS_9({
...{ class: "size-4" },
}, ...__VLS_functionalComponentArgsRest(__VLS_9));
/** @type {[typeof SettingsContent, ]} */ ;
// @ts-ignore
const __VLS_12 = __VLS_asFunctionalComponent(SettingsContent, new SettingsContent({}));
const __VLS_13 = __VLS_12({}, ...__VLS_functionalComponentArgsRest(__VLS_12));
}
var __VLS_7;
var __VLS_3;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['inset-0']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-black/50']} */ ;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['w-full']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-5xl']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-background']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['mx-4']} */ ;
/** @type {__VLS_StyleScopedClasses['max-h-[90vh]']} */ ;
/** @type {__VLS_StyleScopedClasses['overflow-y-auto']} */ ;
/** @type {__VLS_StyleScopedClasses['absolute']} */ ;
/** @type {__VLS_StyleScopedClasses['right-4']} */ ;
/** @type {__VLS_StyleScopedClasses['top-4']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['h-7']} */ ;
/** @type {__VLS_StyleScopedClasses['w-7']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-accent']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-accent-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['size-4']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
X: X,
SettingsContent: SettingsContent,
store: store,
};
},
});
export default (await import('vue')).defineComponent({
setup() {
return {};
},
});
; /* PartiallyEnd: #4569/main.vue */
//# sourceMappingURL=SettingsDialog.vue.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"SettingsDialog.vue.js","sourceRoot":"","sources":["SettingsDialog.vue"],"names":[],"mappings":"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"}

View file

@ -6,6 +6,7 @@ import { usePushSubscription } from '@/composables/usePushSubscription'
import { useMailStore } from '@/stores/mail'
import { mailApi } from '@/api/mail'
import { authApi } from '@/api/auth'
import type { Alias } from '@/types/mail'
const { canInstall, promptInstall } = useInstallPrompt()
const notificationsStore = useNotificationsStore()
@ -167,6 +168,98 @@ async function deleteAutoResponder() {
arDeleteLoading.value = false
}
}
const alAliases = ref<Alias[]>([])
const alLoading = ref(false)
const alError = ref('')
const alSuccess = ref('')
const alNewAddress = ref('')
const alNewLabel = ref('')
const alNewDescription = ref('')
const alNewExpires = ref('')
const alNeverExpire = ref(true)
const alCreateLoading = ref(false)
const alGenerating = ref(false)
async function loadAliases(address: string) {
alLoading.value = true
alError.value = ''
try {
const res = await mailApi.getAllAliases(address)
alAliases.value = res.data.aliases || []
} catch {
alError.value = 'Failed to load aliases.'
} finally {
alLoading.value = false
}
}
watch(
() => mailStore.currentMailbox,
(address) => { if (address) loadAliases(address) },
{ immediate: true }
)
async function generateAlias() {
if (!mailStore.currentMailbox) return
const domain = mailStore.currentMailbox.split('@')[1]
if (!domain) return
alGenerating.value = true
try {
const res = await mailApi.generateAlias(mailStore.currentMailbox, domain)
alNewAddress.value = res.data.suggestion?.split('@')[0] || ''
} catch { /* ignore */ } finally {
alGenerating.value = false
}
}
async function createAlias() {
if (!mailStore.currentMailbox || !alNewAddress.value) return
const domain = mailStore.currentMailbox.split('@')[1]
alError.value = ''
alSuccess.value = ''
alCreateLoading.value = true
try {
await mailApi.createAlias({
address: `${alNewAddress.value}@${domain}`,
mailbox_address: mailStore.currentMailbox,
label: alNewLabel.value || undefined,
description: alNewDescription.value || undefined,
expires_at: alNeverExpire.value ? null : (alNewExpires.value || null),
})
alNewAddress.value = ''
alNewLabel.value = ''
alNewDescription.value = ''
alNewExpires.value = ''
alNeverExpire.value = true
alSuccess.value = 'Alias created.'
await loadAliases(mailStore.currentMailbox)
} catch (e: any) {
alError.value = e?.response?.data?.error || 'Failed to create alias.'
} finally {
alCreateLoading.value = false
}
}
async function toggleAlias(alias: Alias) {
try {
await mailApi.updateAlias(alias.address, { is_active: !alias.is_active })
alias.is_active = !alias.is_active
} catch { /* ignore */ }
}
async function deleteAlias(alias: Alias) {
if (!confirm(`Delete alias ${alias.address}? This cannot be undone.`)) return
try {
await mailApi.deleteAlias(alias.address)
alAliases.value = alAliases.value.filter(a => a.address !== alias.address)
} catch { /* ignore */ }
}
function formatDate(iso: string | null) {
if (!iso) return '—'
return new Date(iso).toLocaleDateString()
}
</script>
<template>
@ -410,6 +503,111 @@ async function deleteAutoResponder() {
</button>
</div>
<div v-if="mailStore.currentMailbox" class="mb-4 rounded-lg border p-4 space-y-3">
<div>
<h2 class="text-sm font-medium">Email Aliases</h2>
<p class="text-sm text-muted-foreground mt-0.5">
Create aliases that forward mail to your mailbox while keeping your real address private.
</p>
</div>
<div v-if="alLoading" class="text-sm text-muted-foreground">Loading</div>
<template v-else>
<div v-if="alAliases.length" class="divide-y divide-border rounded-md border overflow-hidden">
<div
v-for="alias in alAliases"
:key="alias.address"
class="flex items-center gap-3 px-3 py-2.5 bg-background text-sm"
>
<div class="flex-1 min-w-0">
<div class="font-mono text-xs truncate">{{ alias.address }}</div>
<div class="flex items-center gap-2 mt-0.5">
<span v-if="alias.label" class="inline-flex items-center rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ alias.label }}</span>
<span v-if="alias.expires_at" class="text-xs text-muted-foreground">Expires {{ formatDate(alias.expires_at) }}</span>
<span class="text-xs text-muted-foreground">{{ alias.use_count }} received</span>
</div>
</div>
<button
class="relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200"
:class="alias.is_active ? 'bg-primary' : 'bg-muted'"
role="switch"
:aria-checked="alias.is_active"
@click="toggleAlias(alias)"
>
<span
class="pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow ring-0 transition-transform duration-200"
:class="alias.is_active ? 'translate-x-4' : 'translate-x-0'"
/>
</button>
<button
class="text-muted-foreground hover:text-destructive transition-colors text-xs"
@click="deleteAlias(alias)"
>
Delete
</button>
</div>
</div>
<p v-else class="text-sm text-muted-foreground">No aliases yet.</p>
<div class="space-y-2 pt-1">
<label class="text-sm font-medium">Create alias</label>
<div class="flex items-center gap-2">
<input
v-model="alNewAddress"
type="text"
placeholder="local-part"
class="flex-1 rounded-md border bg-background px-3 py-2 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-ring"
/>
<span class="text-sm text-muted-foreground shrink-0">@{{ mailStore.currentMailbox.split('@')[1] }}</span>
<button
:disabled="alGenerating"
class="inline-flex items-center justify-center rounded-md border px-3 py-2 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50 shrink-0"
@click="generateAlias"
>
{{ alGenerating ? '…' : 'Generate' }}
</button>
</div>
<input
v-model="alNewLabel"
type="text"
placeholder="Label (optional, e.g. shopping)"
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<input
v-model="alNewDescription"
type="text"
placeholder="Description (optional)"
maxlength="200"
class="w-full rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
<div class="flex items-center gap-3">
<label class="flex items-center gap-2 text-sm cursor-pointer">
<input v-model="alNeverExpire" type="checkbox" class="rounded border-border" />
Never expire
</label>
<input
v-if="!alNeverExpire"
v-model="alNewExpires"
type="date"
class="rounded-md border bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
/>
</div>
</div>
<p v-if="alError" class="text-xs text-destructive">{{ alError }}</p>
<p v-if="alSuccess" class="text-xs text-green-600">{{ alSuccess }}</p>
<button
:disabled="alCreateLoading || !alNewAddress"
class="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50"
@click="createAlias"
>
{{ alCreateLoading ? 'Creating…' : 'Create Alias' }}
</button>
</template>
</div>
<p class="text-sm text-muted-foreground">Additional settings are managed in DockFlare Master.</p>
</div>
</template>

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
import { ref, onMounted, onUnmounted } from 'vue';
const isMobile = ref(false);
export function useBreakpoint() {
const mq = window.matchMedia('(max-width: 767px)');
const update = (e) => {
isMobile.value = e.matches;
};
onMounted(() => {
isMobile.value = mq.matches;
mq.addEventListener('change', update);
});
onUnmounted(() => mq.removeEventListener('change', update));
return { isMobile };
}
//# sourceMappingURL=useBreakpoint.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"useBreakpoint.js","sourceRoot":"","sources":["useBreakpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,KAAK,CAAA;AAEjD,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,CAAA;AAE3B,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAA;IAElD,MAAM,MAAM,GAAG,CAAC,CAAuC,EAAE,EAAE;QACzD,QAAQ,CAAC,KAAK,GAAG,CAAC,CAAC,OAAO,CAAA;IAC5B,CAAC,CAAA;IAED,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,KAAK,GAAG,EAAE,CAAC,OAAO,CAAA;QAC3B,EAAE,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,WAAW,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAA;IAE3D,OAAO,EAAE,QAAQ,EAAE,CAAA;AACrB,CAAC"}

View file

@ -0,0 +1,20 @@
import { ref, onMounted, onUnmounted } from 'vue'
const isMobile = ref(false)
export function useBreakpoint() {
const mq = window.matchMedia('(max-width: 767px)')
const update = (e: MediaQueryListEvent | MediaQueryList) => {
isMobile.value = e.matches
}
onMounted(() => {
isMobile.value = mq.matches
mq.addEventListener('change', update)
})
onUnmounted(() => mq.removeEventListener('change', update))
return { isMobile }
}

View file

@ -1,18 +1,41 @@
import { onUnmounted, ref, watch } from 'vue';
import { mailApi } from '@/api/mail';
import { useNotificationsStore } from '@/stores/notifications';
import { useMailStore } from '@/stores/mail';
function updateBadge(count) {
if (!('setAppBadge' in navigator))
return;
if (count > 0) {
navigator.setAppBadge(count).catch(() => { });
}
else {
navigator.clearAppBadge().catch(() => { });
}
}
async function getPushIntervalMs() {
try {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
if (sub)
return 300_000;
}
}
catch { /* SW not available */ }
return 30_000 + Math.random() * 20_000 - 10_000;
}
export function useMailPolling() {
const notificationsStore = useNotificationsStore();
const mailStore = useMailStore();
const lastSeen = ref({});
const initialized = ref(false);
let intervalId = null;
const poll = async () => {
if (mailStore.mailboxes.length === 0)
return;
try {
const res = await mailApi.getMailboxStatus();
const statuses = res.data;
const totalUnread = statuses.reduce((sum, s) => sum + s.unread_count, 0);
updateBadge(totalUnread);
if (!initialized.value) {
for (const s of statuses) {
lastSeen.value[s.address] = s.latest_received_at;
@ -20,14 +43,36 @@ export function useMailPolling() {
initialized.value = true;
return;
}
if (!notificationsStore.isGranted)
return;
for (const s of statuses) {
const prev = lastSeen.value[s.address];
if (s.latest_received_at &&
(prev === undefined || prev === null || s.latest_received_at > prev)) {
lastSeen.value[s.address] = s.latest_received_at;
fireNotification(s.address, s.unread_count);
if (s.address === mailStore.currentMailbox && mailStore.currentFolder) {
try {
const mRes = await mailApi.getMessages(s.address, {
folder: mailStore.currentFolder,
order: mailStore.sortOrder,
page: 1,
per_page: 50,
});
const payload = mRes.data;
const items = Array.isArray(payload) ? payload : payload.items || [];
mailStore.messages = items;
mailStore.totalMessages = payload.total ?? items.length;
mailStore.messagesPage = 1;
mailStore.hasMoreMessages = items.length === 50;
}
catch { /* network error — skip */ }
try {
const fRes = await mailApi.getFolders(s.address);
mailStore.folders = fRes.data;
}
catch { /* network error — skip */ }
}
if (Notification.permission === 'granted') {
fireNotification(s.address, s.unread_count);
}
}
}
}
@ -48,11 +93,34 @@ export function useMailPolling() {
n.close();
};
};
const startInterval = async () => {
if (intervalId)
clearInterval(intervalId);
const ms = await getPushIntervalMs();
intervalId = setInterval(poll, ms);
};
const onVisibilityChange = () => {
if (document.visibilityState === 'hidden') {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
}
else {
poll();
startInterval();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);
watch(() => mailStore.mailboxes, (boxes) => {
if (boxes.length > 0 && !initialized.value)
poll();
}, { immediate: true });
const interval = setInterval(poll, 60_000);
onUnmounted(() => clearInterval(interval));
startInterval();
onUnmounted(() => {
if (intervalId)
clearInterval(intervalId);
document.removeEventListener('visibilitychange', onVisibilityChange);
});
}
//# sourceMappingURL=useMailPolling.js.map

View file

@ -1 +1 @@
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"useMailPolling.js","sourceRoot":"","sources":["useMailPolling.ts"],"names":[],"mappings":"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"}

View file

@ -1,5 +1,6 @@
import { ref } from 'vue';
import apiClient from '@/api/client';
import { useMailStore } from '@/stores/mail';
const isSupported = typeof window !== 'undefined' && 'serviceWorker' in navigator && 'PushManager' in window;
function urlBase64ToUint8Array(base64) {
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
@ -12,6 +13,8 @@ function urlBase64ToUint8Array(base64) {
export function usePushSubscription() {
const isSubscribed = ref(false);
const isLoading = ref(false);
const error = ref(null);
const mailStore = useMailStore();
const checkSubscription = async () => {
if (!isSupported)
return;
@ -19,10 +22,11 @@ export function usePushSubscription() {
const sub = await reg.pushManager.getSubscription();
isSubscribed.value = !!sub;
};
const subscribe = async (mailboxAddress) => {
const subscribe = async () => {
if (!isSupported)
return;
isLoading.value = true;
error.value = null;
try {
const { data } = await apiClient.get('/notifications/vapid-key');
const reg = await navigator.serviceWorker.ready;
@ -31,13 +35,28 @@ export function usePushSubscription() {
applicationServerKey: urlBase64ToUint8Array(data.public_key),
});
const subJson = sub.toJSON();
await apiClient.post('/notifications/subscribe', {
endpoint: subJson.endpoint,
keys: subJson.keys,
mailbox_address: mailboxAddress,
});
let addresses = mailStore.mailboxes.map((m) => m.address);
if (addresses.length === 0) {
const statusRes = await apiClient.get('/mailboxes/status');
addresses = statusRes.data.map((s) => s.address);
}
if (addresses.length === 0) {
error.value = 'No mailboxes found';
return;
}
for (const address of addresses) {
await apiClient.post('/notifications/subscribe', {
endpoint: subJson.endpoint,
keys: subJson.keys,
mailbox_address: address,
});
}
isSubscribed.value = true;
}
catch (err) {
console.error('Push subscribe failed:', err);
error.value = err?.message ?? 'Subscription failed';
}
finally {
isLoading.value = false;
}
@ -46,6 +65,7 @@ export function usePushSubscription() {
if (!isSupported)
return;
isLoading.value = true;
error.value = null;
try {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.getSubscription();
@ -57,11 +77,15 @@ export function usePushSubscription() {
}
isSubscribed.value = false;
}
catch (err) {
console.error('Push unsubscribe failed:', err);
error.value = err?.message ?? 'Unsubscribe failed';
}
finally {
isLoading.value = false;
}
};
checkSubscription();
return { isSubscribed, isLoading, isSupported, subscribe, unsubscribe };
return { isSubscribed, isLoading, isSupported, error, subscribe, unsubscribe };
}
//# sourceMappingURL=usePushSubscription.js.map

View file

@ -1 +1 @@
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"usePushSubscription.js","sourceRoot":"","sources":["usePushSubscription.ts"],"names":[],"mappings":"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"}

View file

@ -1,5 +1,6 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { setIDBItem, removeIDBItem } from '@/lib/idb';
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('jwt_token') || '');
const isAuthenticated = computed(() => {
@ -16,6 +17,7 @@ export const useAuthStore = defineStore('auth', () => {
const setToken = (newToken) => {
token.value = newToken;
localStorage.setItem('jwt_token', newToken);
setIDBItem('jwt_token', newToken).catch(() => { });
};
const logout = async () => {
if ('serviceWorker' in navigator) {
@ -38,6 +40,7 @@ export const useAuthStore = defineStore('auth', () => {
}
token.value = '';
localStorage.removeItem('jwt_token');
removeIDBItem('jwt_token').catch(() => { });
};
const decodeToken = () => {
if (!token.value)

View file

@ -1 +1 @@
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAA;AAEnC,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,EAAE,GAAG,EAAE;IACnD,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAA;IAE1D,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,EAAE;QACpC,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAC9B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;YAC3D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,MAAM,QAAQ,GAAG,CAAC,QAAgB,EAAE,EAAE;QACpC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAA;QACtB,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;IAC7C,CAAC,CAAA;IAED,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE;QACxB,IAAI,eAAe,IAAI,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,aAAa,CAAC,KAAK,CAAA;gBAC/C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,CAAC,eAAe,EAAE,CAAA;gBACnD,IAAI,GAAG,EAAE,CAAC;oBACR,MAAM,KAAK,CAAC,iCAAiC,EAAE;wBAC7C,MAAM,EAAE,QAAQ;wBAChB,OAAO,EAAE;4BACP,cAAc,EAAE,kBAAkB;4BAClC,eAAe,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE;yBACzC;wBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC;qBACjD,CAAC,CAAA;oBACF,MAAM,GAAG,CAAC,WAAW,EAAE,CAAA;gBACzB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,gCAAgC,CAAC,CAAC;QAC9C,CAAC;QACD,KAAK,CAAC,KAAK,GAAG,EAAE,CAAA;QAChB,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAA;IACtC,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,IAAI,CAAA;QAC7B,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;YACzC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;QAClC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC,CAAA;IAED,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,CAAA;AAClE,CAAC,CAAC,CAAA"}
{"version":3,"file":"auth.js","sourceRoot":"","sources":["auth.ts"],"names":[],"mappings":"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"}

View file

@ -1,14 +1,20 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { ref, shallowRef, computed } from 'vue';
export const useMailStore = defineStore('mail', () => {
const mailboxes = ref([]);
const currentMailbox = ref('');
const folders = ref([]);
const currentFolder = ref('');
const messages = ref([]);
const messages = shallowRef([]);
const totalMessages = ref(0);
const hasMoreMessages = ref(false);
const messagesPage = ref(1);
const isFetchingNextPage = ref(false);
const currentMessage = ref(null);
const messagesLoading = ref(false);
const isComposeOpen = ref(false);
const isComposeFullView = ref(false);
const isSettingsOpen = ref(false);
const composeDefaults = ref(null);
const composeBody = ref('');
const activeTab = ref('all');
@ -16,6 +22,22 @@ export const useMailStore = defineStore('mail', () => {
const sortOrder = ref('desc');
const isDark = ref(localStorage.getItem('theme') === 'dark');
const viewMode = ref(localStorage.getItem('viewMode') || 'split');
const toast = ref(null);
let toastTimer = null;
let _loadMore = null;
function showToast(message, type = 'error') {
if (toastTimer)
clearTimeout(toastTimer);
toast.value = { message, type };
toastTimer = setTimeout(() => { toast.value = null; }, 4000);
}
function registerLoadMore(fn) {
_loadMore = fn;
}
function loadMore() {
if (_loadMore)
_loadMore();
}
const unreadMessages = computed(() => messages.value.filter((m) => !m.is_read));
const starredMessages = computed(() => messages.value.filter((m) => m.is_starred));
const currentFolderObj = computed(() => folders.value.find((f) => f.name === currentFolder.value) || null);
@ -37,12 +59,15 @@ export const useMailStore = defineStore('mail', () => {
return {
mailboxes, currentMailbox,
folders, currentFolder, currentFolderObj,
messages, currentMessage,
isComposeOpen, isComposeFullView, composeDefaults, composeBody,
messages, totalMessages, hasMoreMessages, messagesPage, isFetchingNextPage,
currentMessage, messagesLoading,
isComposeOpen, isComposeFullView, isSettingsOpen, composeDefaults, composeBody,
activeTab, isCollapsed,
sortOrder, isDark, toggleTheme,
viewMode, toggleViewMode,
unreadMessages, starredMessages,
toast, showToast,
registerLoadMore, loadMore,
};
});
//# sourceMappingURL=mail.js.map

View file

@ -1 +1 @@
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":"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"}

View file

@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=mail.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"mail.js","sourceRoot":"","sources":["mail.ts"],"names":[],"mappings":""}

View file

@ -28,6 +28,7 @@ export interface Message {
from_address: string
to_addresses: string
cc_addresses: string
bcc_addresses: string
subject: string | null
text_body: string | null
html_body: string | null
@ -36,9 +37,23 @@ export interface Message {
is_read: 0 | 1
is_starred: 0 | 1
is_draft: boolean
received_via_alias: string | null
attachments?: Attachment[]
}
export interface Alias {
address: string
mailbox_address: string
domain: string
label: string | null
description: string | null
is_active: boolean
expires_at: string | null
created_at: string
use_count: number
last_use_at: string | null
}
export interface Toast {
message: string
type: 'error' | 'success' | 'info'
@ -46,6 +61,7 @@ export interface Toast {
export interface ComposeDefaults {
to?: string
from?: string
subject?: string
body?: string
quotedHtml?: string

View file

@ -1,26 +1,65 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { onMounted, watch } from 'vue';
import { onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useMail } from '../composables/useMail';
import { useMailPolling } from '../composables/useMailPolling';
import { useNotificationsStore } from '../stores/notifications';
import { mailApi } from '../api/mail';
import MailLayout from '../components/mail/MailLayout.vue';
const route = useRoute();
const { store, loadMailboxes } = useMail();
const mailStore = store;
const notificationsStore = useNotificationsStore();
useMailPolling();
const loadMessages = async (addr, folder) => {
const showNotifPrompt = ref(false);
let mailboxLoadSeq = 0;
const loadMessages = async (addr, folder, page = 1) => {
if (!addr || !folder)
return;
try {
const mRes = await mailApi.getMessages(addr, { folder, order: store.sortOrder });
const payload = mRes.data;
store.messages = Array.isArray(payload) ? payload : payload.items || [];
store.currentMessage = null;
if (page === 1) {
store.messagesLoading = true;
store.messages = [];
store.messagesPage = 1;
}
catch (e) {
console.error('Failed to load messages', e);
else {
store.isFetchingNextPage = true;
}
try {
const mRes = await mailApi.getMessages(addr, { folder, order: store.sortOrder, page, per_page: 50 });
const payload = mRes.data;
const items = Array.isArray(payload) ? payload : payload.items || [];
store.messages = page === 1 ? items : [...store.messages, ...items];
store.totalMessages = payload.total ?? items.length;
store.messagesPage = page;
store.hasMoreMessages = items.length === 50;
if (page === 1)
store.currentMessage = null;
}
catch {
store.showToast('Failed to load messages');
}
finally {
store.messagesLoading = false;
store.isFetchingNextPage = false;
}
};
store.registerLoadMore(() => {
if (store.hasMoreMessages && !store.isFetchingNextPage) {
loadMessages(store.currentMailbox, store.currentFolder, store.messagesPage + 1);
}
});
async function enableNotifications() {
await notificationsStore.requestPermission();
showNotifPrompt.value = false;
localStorage.setItem('notif_prompted', '1');
if (notificationsStore.isGranted) {
mailStore.isSettingsOpen = true;
}
}
function dismissPrompt() {
showNotifPrompt.value = false;
localStorage.setItem('notif_prompted', '1');
}
onMounted(async () => {
await loadMailboxes();
const mailboxParam = route.query.mailbox;
@ -29,27 +68,46 @@ onMounted(async () => {
if (found)
store.currentMailbox = mailboxParam;
}
if (Notification.permission === 'default' && !localStorage.getItem('notif_prompted')) {
showNotifPrompt.value = true;
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (ev) => {
if (ev.data?.type === 'NOTIFICATION_CLICK' && ev.data.mailbox) {
store.currentMailbox = ev.data.mailbox;
}
if (ev.data?.type === 'SET_BADGE') {
const count = ev.data.count ?? 0;
if ('setAppBadge' in navigator) {
if (count > 0) {
navigator.setAppBadge(count).catch(() => { });
}
else {
navigator.clearAppBadge().catch(() => { });
}
}
}
});
}
});
watch(() => store.currentMailbox, async (addr) => {
if (!addr)
return;
const seq = ++mailboxLoadSeq;
try {
const fRes = await mailApi.getFolders(addr);
if (seq !== mailboxLoadSeq)
return;
store.folders = fRes.data;
if (store.folders.length > 0) {
const inbox = store.folders.find((f) => f.name.toLowerCase() === 'inbox');
store.currentFolder = inbox ? inbox.name : store.folders[0].name;
}
}
catch (e) {
console.error('Failed to load folders', e);
catch {
if (seq !== mailboxLoadSeq)
return;
store.showToast('Failed to load folders');
}
});
watch(() => [store.currentMailbox, store.currentFolder], ([addr, folder]) => {
@ -58,44 +116,154 @@ watch(() => [store.currentMailbox, store.currentFolder], ([addr, folder]) => {
watch(() => store.sortOrder, () => {
loadMessages(store.currentMailbox, store.currentFolder);
});
let openedMessageId = null;
watch(() => store.currentMessage, async (msg) => {
if (!msg || msg.attachments !== undefined)
if (!msg)
return;
try {
const res = await mailApi.getMessage(store.currentMailbox, msg.id);
const fullMsg = res.data;
store.currentMessage = fullMsg;
const idx = store.messages.findIndex((m) => m.id === msg.id);
if (idx !== -1) {
store.messages[idx] = fullMsg;
let fullMsg = msg;
const isUserOpen = msg.attachments === undefined || msg.id !== openedMessageId;
if (msg.attachments === undefined) {
const res = await mailApi.getMessage(store.currentMailbox, msg.id);
fullMsg = res.data;
store.currentMessage = fullMsg;
if (idx !== -1)
store.messages[idx] = fullMsg;
}
if (!fullMsg.is_read) {
if (!fullMsg.is_read && isUserOpen) {
openedMessageId = msg.id;
await mailApi.updateMessage(store.currentMailbox, msg.id, { is_read: true });
if (idx !== -1) {
store.messages[idx] = { ...store.messages[idx], is_read: 1 };
}
store.currentMessage = { ...store.currentMessage, is_read: 1 };
const fRes = await mailApi.getFolders(store.currentMailbox);
store.folders = fRes.data;
}
else {
openedMessageId = msg.id;
}
}
catch (e) {
console.error('Failed to load message', e);
catch {
store.showToast('Failed to load message');
}
});
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "relative h-full" },
});
/** @type {[typeof MailLayout, ]} */ ;
// @ts-ignore
const __VLS_0 = __VLS_asFunctionalComponent(MailLayout, new MailLayout({}));
const __VLS_1 = __VLS_0({}, ...__VLS_functionalComponentArgsRest(__VLS_0));
var __VLS_3 = {};
var __VLS_2;
const __VLS_3 = {}.Transition;
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
// @ts-ignore
const __VLS_4 = __VLS_asFunctionalComponent(__VLS_3, new __VLS_3({
name: "slide-up",
}));
const __VLS_5 = __VLS_4({
name: "slide-up",
}, ...__VLS_functionalComponentArgsRest(__VLS_4));
__VLS_6.slots.default;
if (__VLS_ctx.showNotifPrompt) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-4 rounded-xl border bg-background shadow-lg px-5 py-3.5 text-sm" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-muted-foreground" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.enableNotifications) },
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.dismissPrompt) },
...{ class: "text-muted-foreground hover:text-foreground transition-colors" },
});
}
var __VLS_6;
const __VLS_7 = {}.Transition;
/** @type {[typeof __VLS_components.Transition, typeof __VLS_components.Transition, ]} */ ;
// @ts-ignore
const __VLS_8 = __VLS_asFunctionalComponent(__VLS_7, new __VLS_7({
name: "slide-up",
}));
const __VLS_9 = __VLS_8({
name: "slide-up",
}, ...__VLS_functionalComponentArgsRest(__VLS_8));
__VLS_10.slots.default;
if (__VLS_ctx.store.toast) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "fixed bottom-4 right-4 z-50 flex items-center gap-3 rounded-xl border px-5 py-3.5 text-sm shadow-lg" },
...{ class: (__VLS_ctx.store.toast.type === 'error'
? 'bg-destructive text-destructive-foreground border-destructive'
: __VLS_ctx.store.toast.type === 'success'
? 'bg-green-600 text-white border-green-700'
: 'bg-background text-foreground border-border') },
});
(__VLS_ctx.store.toast.message);
}
var __VLS_10;
/** @type {__VLS_StyleScopedClasses['relative']} */ ;
/** @type {__VLS_StyleScopedClasses['h-full']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['bottom-4']} */ ;
/** @type {__VLS_StyleScopedClasses['left-1/2']} */ ;
/** @type {__VLS_StyleScopedClasses['-translate-x-1/2']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-4']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-background']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['px-5']} */ ;
/** @type {__VLS_StyleScopedClasses['py-3.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:text-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['fixed']} */ ;
/** @type {__VLS_StyleScopedClasses['bottom-4']} */ ;
/** @type {__VLS_StyleScopedClasses['right-4']} */ ;
/** @type {__VLS_StyleScopedClasses['z-50']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-xl']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['px-5']} */ ;
/** @type {__VLS_StyleScopedClasses['py-3.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['shadow-lg']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
MailLayout: MailLayout,
store: store,
showNotifPrompt: showNotifPrompt,
enableNotifications: enableNotifications,
dismissPrompt: dismissPrompt,
};
},
});

File diff suppressed because one or more lines are too long

View file

@ -1,12 +1,5 @@
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
import { useInstallPrompt } from '@/composables/useInstallPrompt';
import { useNotificationsStore } from '@/stores/notifications';
import { usePushSubscription } from '@/composables/usePushSubscription';
import { useMailStore } from '@/stores/mail';
const { canInstall, promptInstall } = useInstallPrompt();
const notificationsStore = useNotificationsStore();
const push = usePushSubscription();
const mailStore = useMailStore();
import SettingsContent from '@/components/settings/SettingsContent.vue';
debugger; /* PartiallyEnd: #3632/scriptSetup.vue */
const __VLS_ctx = {};
let __VLS_components;
@ -14,197 +7,17 @@ let __VLS_directives;
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "p-8 max-w-2xl" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h1, __VLS_intrinsicElements.h1)({
...{ class: "text-2xl font-semibold mb-6" },
});
if (__VLS_ctx.canInstall) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mb-4 rounded-lg border p-4 space-y-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-sm font-medium" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground mt-0.5" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.promptInstall) },
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
});
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "mb-4 rounded-lg border p-4 space-y-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({});
__VLS_asFunctionalElement(__VLS_intrinsicElements.h2, __VLS_intrinsicElements.h2)({
...{ class: "text-sm font-medium" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground mt-0.5" },
});
if (__VLS_ctx.notificationsStore.isDenied) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
}
else if (!__VLS_ctx.notificationsStore.isGranted) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (__VLS_ctx.notificationsStore.requestPermission) },
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
if (__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.div, __VLS_intrinsicElements.div)({
...{ class: "flex items-center gap-3" },
});
__VLS_asFunctionalElement(__VLS_intrinsicElements.span, __VLS_intrinsicElements.span)({
...{ class: "text-sm" },
});
(__VLS_ctx.mailStore.currentMailbox);
if (!__VLS_ctx.push.isSubscribed.value) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.notificationsStore.isDenied))
return;
if (!!(!__VLS_ctx.notificationsStore.isGranted))
return;
if (!(__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox))
return;
if (!(!__VLS_ctx.push.isSubscribed.value))
return;
__VLS_ctx.push.subscribe(__VLS_ctx.mailStore.currentMailbox);
} },
disabled: (__VLS_ctx.push.isLoading.value),
...{ class: "inline-flex items-center justify-center rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors disabled:opacity-50" },
});
}
else {
__VLS_asFunctionalElement(__VLS_intrinsicElements.button, __VLS_intrinsicElements.button)({
...{ onClick: (...[$event]) => {
if (!!(__VLS_ctx.notificationsStore.isDenied))
return;
if (!!(!__VLS_ctx.notificationsStore.isGranted))
return;
if (!(__VLS_ctx.push.isSupported && __VLS_ctx.mailStore.currentMailbox))
return;
if (!!(!__VLS_ctx.push.isSubscribed.value))
return;
__VLS_ctx.push.unsubscribe();
} },
disabled: (__VLS_ctx.push.isLoading.value),
...{ class: "inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50" },
});
}
}
else if (!__VLS_ctx.push.isSupported) {
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
}
}
__VLS_asFunctionalElement(__VLS_intrinsicElements.p, __VLS_intrinsicElements.p)({
...{ class: "text-sm text-muted-foreground" },
});
/** @type {[typeof SettingsContent, ]} */ ;
// @ts-ignore
const __VLS_0 = __VLS_asFunctionalComponent(SettingsContent, new SettingsContent({}));
const __VLS_1 = __VLS_0({}, ...__VLS_functionalComponentArgsRest(__VLS_0));
/** @type {__VLS_StyleScopedClasses['p-8']} */ ;
/** @type {__VLS_StyleScopedClasses['max-w-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['text-2xl']} */ ;
/** @type {__VLS_StyleScopedClasses['font-semibold']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-6']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['mb-4']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-lg']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['p-4']} */ ;
/** @type {__VLS_StyleScopedClasses['space-y-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['mt-0.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-4']} */ ;
/** @type {__VLS_StyleScopedClasses['py-2']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['gap-3']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['bg-primary']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['text-primary-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-primary/90']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['inline-flex']} */ ;
/** @type {__VLS_StyleScopedClasses['items-center']} */ ;
/** @type {__VLS_StyleScopedClasses['justify-center']} */ ;
/** @type {__VLS_StyleScopedClasses['rounded-md']} */ ;
/** @type {__VLS_StyleScopedClasses['border']} */ ;
/** @type {__VLS_StyleScopedClasses['px-3']} */ ;
/** @type {__VLS_StyleScopedClasses['py-1.5']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['font-medium']} */ ;
/** @type {__VLS_StyleScopedClasses['hover:bg-muted']} */ ;
/** @type {__VLS_StyleScopedClasses['transition-colors']} */ ;
/** @type {__VLS_StyleScopedClasses['disabled:opacity-50']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
/** @type {__VLS_StyleScopedClasses['text-sm']} */ ;
/** @type {__VLS_StyleScopedClasses['text-muted-foreground']} */ ;
var __VLS_dollars;
const __VLS_self = (await import('vue')).defineComponent({
setup() {
return {
canInstall: canInstall,
promptInstall: promptInstall,
notificationsStore: notificationsStore,
push: push,
mailStore: mailStore,
SettingsContent: SettingsContent,
};
},
});

File diff suppressed because one or more lines are too long