feat: recipient search

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-06-22 22:54:02 -04:00
parent 949bbfa538
commit b419af09eb
8 changed files with 294 additions and 26 deletions
+6 -4
View File
@@ -14,6 +14,7 @@ import type {
import { ComposerMode } from '@/types/composer'
import type { ComposerDraft, ComposerDraftAttachment, ComposerDraftMessage, ComposerSenderIdentity } from '@/types/composer'
import { EntityObject, MessageAddressObject, ServiceObject } from '@MailManager/models'
import type { MessageAddressInterface } from '@MailManager/types/message'
export const useMailCompositionStore = defineStore('mailCompositionStore', () => {
const servicesStore = useServicesStore()
@@ -234,7 +235,7 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
}
}
function updateRecipients(field: 'to' | 'cc' | 'bcc', values: string[]) {
function updateRecipients(field: 'to' | 'cc' | 'bcc', values: MessageAddressInterface[]) {
if (!activeDraft.value) {
return
}
@@ -464,7 +465,7 @@ function buildMessage(
const freshMessage = emptyMessage()
if (source && source instanceof MessageAddressObject) {
freshMessage.to = [source.address]
freshMessage.to = [{ address: source.address, label: source.label }]
}
return freshMessage
@@ -482,9 +483,10 @@ function buildMessage(
const sentLabel = sentAt ? new Date(sentAt).toLocaleString() : 'an unknown time'
if (mode === ComposerMode.Reply) {
const fromEmail = sourceMessage.replyTo?.[0]?.address || sourceMessage.from?.address || ''
const replyToAddr = sourceMessage.replyTo?.[0] ?? sourceMessage.from ?? null
const replyToAddress = replyToAddr?.address || ''
return {
to: fromEmail ? [fromEmail] : [],
to: replyToAddress ? [{ address: replyToAddress, label: replyToAddr?.label }] : [],
cc: [],
bcc: [],
subject: /^Re:/i.test(originalSubject) ? originalSubject : `Re: ${originalSubject}`,
+85
View File
@@ -0,0 +1,85 @@
/**
* Recipients Store
*/
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { recipientService } from '../services/recipientService'
import type { MessageAddressInterface } from '@MailManager/types/message'
/** Minimum characters before a lookup fires. */
const MIN_QUERY_LENGTH = 3
/** Debounce window for keystroke-driven searches. */
const DEBOUNCE_MS = 250
export const useRecipientLookupStore = defineStore('mailRecipientsStore', () => {
const suggestions = ref<MessageAddressInterface[]>([])
const searching = ref(false)
let debounceTimer: ReturnType<typeof setTimeout> | null = null
// Monotonic token so out-of-order/stale responses are discarded.
let requestToken = 0
function cancelPending() {
if (debounceTimer) {
clearTimeout(debounceTimer)
debounceTimer = null
}
}
/**
* Clear suggestions and cancel any in-flight lookup.
*/
function clear() {
cancelPending()
requestToken++
suggestions.value = []
searching.value = false
}
/**
* Debounced contact search. Queries shorter than MIN_QUERY_LENGTH clear the
* suggestion list without hitting the backend.
*/
function search(query: string) {
const trimmed = (query ?? '').trim()
cancelPending()
if (trimmed.length < MIN_QUERY_LENGTH) {
requestToken++
suggestions.value = []
searching.value = false
return
}
searching.value = true
const token = ++requestToken
debounceTimer = setTimeout(async () => {
try {
const matches = await recipientService.search(trimmed)
if (token !== requestToken) {
return
}
suggestions.value = matches
} catch (error: any) {
if (token === requestToken) {
suggestions.value = []
}
console.error('[Mail][Recipient Lookup] - Search failed:', error)
} finally {
if (token === requestToken) {
searching.value = false
}
}
}, DEBOUNCE_MS)
}
return {
suggestions,
searching,
search,
clear,
}
})