335 lines
8.3 KiB
Vue
335 lines
8.3 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
|
import { storeToRefs } from 'pinia'
|
|
import { useEditor } from '@tiptap/vue-3'
|
|
import StarterKit from '@tiptap/starter-kit'
|
|
import Link from '@tiptap/extension-link'
|
|
import Underline from '@tiptap/extension-underline'
|
|
import TextAlign from '@tiptap/extension-text-align'
|
|
import Placeholder from '@tiptap/extension-placeholder'
|
|
import { EntityObject } from '@MailManager/models/entity'
|
|
import type { CollectionObject } from '@MailManager/models'
|
|
import { useMailStore } from '@/stores/mailStore'
|
|
import ComposerToolbar from '@/components/composer/ComposerToolbar.vue'
|
|
import ComposerRecipients from '@/components/composer/ComposerRecipients.vue'
|
|
import ComposerEditor from '@/components/composer/ComposerEditor.vue'
|
|
|
|
// Props
|
|
interface Props {
|
|
mode: 'new' | 'reply' | 'forward'
|
|
source?: EntityObject | null
|
|
folder?: CollectionObject | null
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
// Emits
|
|
const emit = defineEmits<{
|
|
close: []
|
|
sent: []
|
|
}>()
|
|
|
|
const mailStore = useMailStore()
|
|
const {
|
|
composerSending: sending,
|
|
composerSaving: saving,
|
|
composerLastSaved: lastSaved,
|
|
} = storeToRefs(mailStore)
|
|
|
|
// State
|
|
const to = ref<string[]>([])
|
|
const cc = ref<string[]>([])
|
|
const bcc = ref<string[]>([])
|
|
const subject = ref('')
|
|
const showCc = ref(false)
|
|
const showBcc = ref(false)
|
|
|
|
// Auto-save timer
|
|
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null
|
|
|
|
// Initialize Tiptap editor
|
|
const editor = useEditor({
|
|
extensions: [
|
|
StarterKit,
|
|
Link.configure({
|
|
openOnClick: false,
|
|
}),
|
|
Underline,
|
|
TextAlign.configure({
|
|
types: ['heading', 'paragraph'],
|
|
}),
|
|
Placeholder.configure({
|
|
placeholder: 'Write your message...',
|
|
}),
|
|
],
|
|
content: '',
|
|
editorProps: {
|
|
attributes: {
|
|
class: 'tiptap-editor pa-4',
|
|
},
|
|
},
|
|
})
|
|
|
|
function resetComposerFields() {
|
|
to.value = []
|
|
cc.value = []
|
|
bcc.value = []
|
|
subject.value = ''
|
|
showCc.value = false
|
|
showBcc.value = false
|
|
editor.value?.commands.setContent('')
|
|
}
|
|
|
|
function initializeComposerFromProps() {
|
|
mailStore.resetComposerState()
|
|
resetComposerFields()
|
|
|
|
if (!props.source) {
|
|
return
|
|
}
|
|
|
|
const sourceMessage = props.source.properties
|
|
const originalSubject = sourceMessage.subject || ''
|
|
const originalBody = sourceMessage.getHtmlContent() || sourceMessage.getTextContent() || ''
|
|
const senderName = sourceMessage.from?.label || sourceMessage.from?.address || 'Unknown'
|
|
const sentAt = sourceMessage.sent || props.source.created || ''
|
|
const sentLabel = sentAt ? new Date(sentAt).toLocaleString() : 'an unknown time'
|
|
|
|
if (props.mode === 'reply') {
|
|
const fromEmail = sourceMessage.replyTo?.[0]?.address || sourceMessage.from?.address
|
|
to.value = fromEmail ? [fromEmail] : []
|
|
subject.value = /^Re:/i.test(originalSubject)
|
|
? originalSubject
|
|
: `Re: ${originalSubject}`
|
|
editor.value?.commands.setContent(
|
|
`<p><br></p><p>On ${sentLabel}, ${senderName} wrote:</p><blockquote>${originalBody}</blockquote>`,
|
|
)
|
|
return
|
|
}
|
|
|
|
if (props.mode === 'forward') {
|
|
subject.value = /^Fwd:/i.test(originalSubject)
|
|
? originalSubject
|
|
: `Fwd: ${originalSubject}`
|
|
editor.value?.commands.setContent(
|
|
`<p><br></p><p>---------- Forwarded message ---------</p><p>From: ${senderName}</p><p>Date: ${sentLabel}</p><p>Subject: ${originalSubject}</p><blockquote>${originalBody}</blockquote>`,
|
|
)
|
|
}
|
|
}
|
|
|
|
watch(
|
|
[() => props.mode, () => props.source, () => editor.value],
|
|
([, , currentEditor]) => {
|
|
if (!currentEditor) {
|
|
return
|
|
}
|
|
|
|
initializeComposerFromProps()
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
// Computed
|
|
const canSend = computed(() => {
|
|
return to.value.length > 0 && subject.value.trim().length > 0
|
|
})
|
|
|
|
const saveStatus = computed(() => {
|
|
if (saving.value) return 'Saving...'
|
|
if (lastSaved.value) {
|
|
const seconds = Math.floor((Date.now() - lastSaved.value.getTime()) / 1000)
|
|
if (seconds < 60) return 'Saved just now'
|
|
if (seconds < 3600) return `Saved ${Math.floor(seconds / 60)} min ago`
|
|
return `Saved at ${lastSaved.value.toLocaleTimeString()}`
|
|
}
|
|
return ''
|
|
})
|
|
|
|
// Auto-save function
|
|
const saveDraft = async () => {
|
|
if (saving.value || sending.value) return
|
|
if (!props.folder) return
|
|
|
|
// Don't save if completely empty
|
|
if (to.value.length === 0 && subject.value.trim().length === 0 && !editor.value?.getText().trim()) {
|
|
return
|
|
}
|
|
|
|
try {
|
|
await mailStore.saveComposerDraft(props.folder, {
|
|
to: to.value,
|
|
cc: cc.value,
|
|
bcc: bcc.value,
|
|
subject: subject.value,
|
|
body: {
|
|
html: editor.value?.getHTML() || '',
|
|
text: editor.value?.getText() || '',
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('[Mail][Composer] Failed to save draft:', error)
|
|
}
|
|
}
|
|
|
|
// Watch for changes and trigger auto-save
|
|
const scheduleAutoSave = () => {
|
|
if (autoSaveTimer) {
|
|
clearTimeout(autoSaveTimer)
|
|
}
|
|
|
|
autoSaveTimer = setTimeout(() => {
|
|
saveDraft()
|
|
}, 30000) // 30 seconds
|
|
}
|
|
|
|
watch([to, cc, bcc, subject], () => {
|
|
scheduleAutoSave()
|
|
}, { deep: true })
|
|
|
|
// Watch editor content changes
|
|
if (editor.value) {
|
|
editor.value.on('update', () => {
|
|
scheduleAutoSave()
|
|
})
|
|
}
|
|
|
|
// Cleanup
|
|
onBeforeUnmount(() => {
|
|
if (autoSaveTimer) {
|
|
clearTimeout(autoSaveTimer)
|
|
}
|
|
mailStore.resetComposerState()
|
|
editor.value?.destroy()
|
|
})
|
|
|
|
// Handlers
|
|
const handleClose = () => {
|
|
mailStore.resetComposerState()
|
|
emit('close')
|
|
}
|
|
|
|
const handleSend = async () => {
|
|
if (!canSend.value || sending.value) return
|
|
|
|
try {
|
|
await mailStore.sendComposerMessage({
|
|
to: to.value,
|
|
cc: cc.value,
|
|
bcc: bcc.value,
|
|
subject: subject.value,
|
|
body: {
|
|
html: editor.value?.getHTML() || '',
|
|
text: editor.value?.getText() || '',
|
|
},
|
|
})
|
|
emit('sent')
|
|
} catch (error) {
|
|
console.error('[Mail][Composer] Failed to send message:', error)
|
|
}
|
|
}
|
|
|
|
const toggleCc = () => {
|
|
showCc.value = !showCc.value
|
|
}
|
|
|
|
const toggleBcc = () => {
|
|
showBcc.value = !showBcc.value
|
|
}
|
|
|
|
// Toolbar actions
|
|
const toggleBold = () => editor.value?.chain().focus().toggleBold().run()
|
|
const toggleItalic = () => editor.value?.chain().focus().toggleItalic().run()
|
|
const toggleUnderline = () => editor.value?.chain().focus().toggleUnderline().run()
|
|
const toggleBulletList = () => editor.value?.chain().focus().toggleBulletList().run()
|
|
const toggleOrderedList = () => editor.value?.chain().focus().toggleOrderedList().run()
|
|
const setLink = () => {
|
|
const url = window.prompt('Enter URL:')
|
|
if (url) {
|
|
editor.value?.chain().focus().setLink({ href: url }).run()
|
|
}
|
|
}
|
|
const removeLink = () => editor.value?.chain().focus().unsetLink().run()
|
|
|
|
const isActive = (name: string, attrs?: any) => {
|
|
return editor.value?.isActive(name, attrs) || false
|
|
}
|
|
|
|
const toggleLink = () => {
|
|
if (isActive('link')) {
|
|
removeLink()
|
|
return
|
|
}
|
|
|
|
setLink()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="message-composer">
|
|
<ComposerToolbar
|
|
:mode="mode"
|
|
:save-status="saveStatus"
|
|
:can-send="canSend"
|
|
:sending="sending"
|
|
@close="handleClose"
|
|
@send="handleSend"
|
|
/>
|
|
|
|
<div class="composer-content">
|
|
<ComposerRecipients
|
|
:to="to"
|
|
:cc="cc"
|
|
:bcc="bcc"
|
|
:subject="subject"
|
|
:show-cc="showCc"
|
|
:show-bcc="showBcc"
|
|
@update:to="to = $event"
|
|
@update:cc="cc = $event"
|
|
@update:bcc="bcc = $event"
|
|
@update:subject="subject = $event"
|
|
@toggle:cc="toggleCc"
|
|
@toggle:bcc="toggleBcc"
|
|
/>
|
|
|
|
<v-divider />
|
|
|
|
<ComposerEditor
|
|
:editor="editor"
|
|
:is-bold-active="isActive('bold')"
|
|
:is-italic-active="isActive('italic')"
|
|
:is-underline-active="isActive('underline')"
|
|
:is-bullet-list-active="isActive('bulletList')"
|
|
:is-ordered-list-active="isActive('orderedList')"
|
|
:is-link-active="isActive('link')"
|
|
@bold="toggleBold"
|
|
@italic="toggleItalic"
|
|
@underline="toggleUnderline"
|
|
@bullet-list="toggleBulletList"
|
|
@ordered-list="toggleOrderedList"
|
|
@link="toggleLink"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped lang="scss">
|
|
.message-composer {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.composer-toolbar {
|
|
flex-shrink: 0;
|
|
border-bottom: 1px solid rgb(var(--v-border-color));
|
|
}
|
|
|
|
.composer-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
</style>
|