Add email reply functionality to thread view
- Add reply button to each email in thread timeline - Pre-fill composer with recipient, subject, and quoted content when replying - Remove readonly constraint from recipient field in composer - Handle reply-to-email event to populate composer with proper reply data
This commit is contained in:
parent
e388779e11
commit
bb2f5d37c8
|
|
@ -30,6 +30,7 @@
|
||||||
|
|
||||||
<!-- Email Composer -->
|
<!-- Email Composer -->
|
||||||
<EmailComposer
|
<EmailComposer
|
||||||
|
ref="emailComposer"
|
||||||
:interest="interest"
|
:interest="interest"
|
||||||
@sent="onEmailSent"
|
@sent="onEmailSent"
|
||||||
@eoiGenerated="onEoiGenerated"
|
@eoiGenerated="onEoiGenerated"
|
||||||
|
|
@ -40,6 +41,7 @@
|
||||||
<EmailThreadView
|
<EmailThreadView
|
||||||
ref="threadView"
|
ref="threadView"
|
||||||
:interest="interest"
|
:interest="interest"
|
||||||
|
@reply-to-email="onReplyToEmail"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
@ -67,6 +69,7 @@ const emit = defineEmits<Emits>();
|
||||||
const isConnected = ref(false);
|
const isConnected = ref(false);
|
||||||
const connectedEmail = ref('');
|
const connectedEmail = ref('');
|
||||||
const threadView = ref<InstanceType<typeof EmailThreadView>>();
|
const threadView = ref<InstanceType<typeof EmailThreadView>>();
|
||||||
|
const emailComposer = ref<InstanceType<typeof EmailComposer>>();
|
||||||
|
|
||||||
const checkConnection = () => {
|
const checkConnection = () => {
|
||||||
// Check if we have a session and connected email
|
// Check if we have a session and connected email
|
||||||
|
|
@ -100,6 +103,29 @@ const onEoiGenerated = () => {
|
||||||
emit('interestUpdated');
|
emit('interestUpdated');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onReplyToEmail = (email: any, thread: any) => {
|
||||||
|
// Extract the original sender's email address
|
||||||
|
const replyTo = email.direction === 'received' ? email.from : email.to;
|
||||||
|
const cleanEmail = replyTo.match(/<(.+)>/)?.[1] || replyTo;
|
||||||
|
|
||||||
|
// Prepare reply content with quote
|
||||||
|
const quotedContent = email.body
|
||||||
|
.split('\n')
|
||||||
|
.map((line: string) => `> ${line}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
const replyBody = `\n\n\nOn ${new Date(email.timestamp).toLocaleString()}, ${email.from} wrote:\n${quotedContent}`;
|
||||||
|
|
||||||
|
// Pre-fill the composer with reply data
|
||||||
|
if (emailComposer.value) {
|
||||||
|
(emailComposer.value as any).setReplyData({
|
||||||
|
to: cleanEmail,
|
||||||
|
subject: thread.subject.startsWith('Re: ') ? thread.subject : `Re: ${thread.subject}`,
|
||||||
|
body: replyBody
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const disconnect = () => {
|
const disconnect = () => {
|
||||||
// Clear session storage
|
// Clear session storage
|
||||||
sessionStorage.removeItem('emailSessionId');
|
sessionStorage.removeItem('emailSessionId');
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
prepend-inner-icon="mdi-email-outline"
|
prepend-inner-icon="mdi-email-outline"
|
||||||
:rules="[rules.required, rules.email]"
|
:rules="[rules.required, rules.email]"
|
||||||
:disabled="sending"
|
:disabled="sending"
|
||||||
readonly
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
|
@ -329,4 +328,19 @@ onMounted(() => {
|
||||||
localStorage.setItem('emailSignatureConfig', JSON.stringify(newConfig));
|
localStorage.setItem('emailSignatureConfig', JSON.stringify(newConfig));
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Method to set reply data
|
||||||
|
const setReplyData = (replyData: { to: string; subject: string; body: string }) => {
|
||||||
|
email.value.to = replyData.to;
|
||||||
|
email.value.subject = replyData.subject;
|
||||||
|
email.value.body = replyData.body;
|
||||||
|
|
||||||
|
// Scroll to the top of the composer
|
||||||
|
form.value?.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose the setReplyData method to parent
|
||||||
|
defineExpose({
|
||||||
|
setReplyData
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-reply"
|
||||||
|
@click="replyToEmail(email, thread)"
|
||||||
|
>
|
||||||
|
Reply
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-timeline-item>
|
</v-timeline-item>
|
||||||
</v-timeline>
|
</v-timeline>
|
||||||
|
|
@ -129,6 +141,9 @@ interface EmailThread {
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'reply-to-email': [email: EmailMessage, thread: EmailThread];
|
||||||
|
}>();
|
||||||
|
|
||||||
const user = useDirectusUser();
|
const user = useDirectusUser();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
@ -237,6 +252,11 @@ const loadEmails = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Reply to an email
|
||||||
|
const replyToEmail = (email: EmailMessage, thread: EmailThread) => {
|
||||||
|
emit('reply-to-email', email, thread);
|
||||||
|
};
|
||||||
|
|
||||||
// Reload emails when an email is sent
|
// Reload emails when an email is sent
|
||||||
const reloadEmails = () => {
|
const reloadEmails = () => {
|
||||||
loadEmails();
|
loadEmails();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue