diff --git a/frontend/src/stores/messages.ts b/frontend/src/stores/messages.ts new file mode 100644 index 0000000..d8fc2cf --- /dev/null +++ b/frontend/src/stores/messages.ts @@ -0,0 +1,158 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +import type { + GifSearchPage, + SmsConversation, + SmsMedia, + SmsMessage, + SmsOutgoingMessage, +} from '@/types/messages' +import { nuiCall, type NuiResponse } from '@/utils/nui' + +export const useMessagesStore = defineStore('messages', () => { + const conversations = ref([]) + const messages = ref([]) + const mediaSources = ref>({}) + const activeNumber = ref(null) + const loading = ref(false) + const unreadCount = computed(() => + conversations.value.reduce((total, item) => total + item.unread, 0), + ) + + async function loadConversations(): Promise { + const response = await nuiCall('messages:conversations') + if (response.success && response.data) conversations.value = response.data + else if (!response.success) conversations.value = [] + return response.success + } + + async function openThread(phoneNumber: string): Promise { + loading.value = true + const response = await nuiCall('messages:thread', { + phoneNumber, + }) + loading.value = false + if (!response.success || !response.data) return false + activeNumber.value = phoneNumber + messages.value = response.data.map((message) => ({ + ...message, + delivery_status: + message.direction === 'sent' ? 'delivered' : undefined, + })) + await loadConversations() + return true + } + + async function send( + outgoing: SmsOutgoingMessage, + ): Promise> { + if (!activeNumber.value) return { success: false, error: 'invalid_number' } + const clientId = `pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + const optimistic: SmsMessage = { + body: outgoing.messageType === 'text' ? outgoing.body.trim() : '', + client_id: clientId, + created_at: new Date().toISOString().slice(0, 19).replace('T', ' '), + delivery_status: 'sending', + direction: 'sent', + id: clientId, + media_asset_id: + outgoing.messageType === 'image' || + outgoing.messageType === 'gif' || + outgoing.messageType === 'video' + ? outgoing.mediaAssetId + : null, + media_duration_ms: + outgoing.messageType === 'voice' || outgoing.messageType === 'video' + ? outgoing.mediaDurationMs ?? null + : null, + media_mime: + outgoing.messageType === 'voice' ? outgoing.mediaMime : null, + media_waveform: + outgoing.messageType === 'voice' ? outgoing.mediaWaveform : null, + message_type: outgoing.messageType, + read_at: null, + recipient_number: activeNumber.value, + sender_number: '', + } + messages.value.push(optimistic) + if (outgoing.messageType === 'voice') { + mediaSources.value[clientId] = + `data:${outgoing.mediaMime};base64,${outgoing.mediaPayload}` + } + + const response = await nuiCall('messages:send', { + ...outgoing, + phoneNumber: activeNumber.value, + }) + const index = messages.value.findIndex( + (message) => message.client_id === clientId, + ) + if (!response.success || !response.data) { + if (index >= 0) messages.value[index].delivery_status = 'failed' + return response + } + + const source = mediaSources.value[clientId] + delete mediaSources.value[clientId] + if (source) mediaSources.value[response.data.id] = source + if (index >= 0) { + messages.value[index] = { + ...response.data, + client_id: clientId, + delivery_status: 'delivered', + } + } + await loadConversations() + return response + } + + async function loadMedia(id: string): Promise { + if (mediaSources.value[id]) return true + const response = await nuiCall('messages:media', { id }) + if (!response.success || !response.data) return false + mediaSources.value[id] = + `data:${response.data.mime};base64,${response.data.payload}` + return true + } + + async function deleteConversations(phoneNumbers: string[]): Promise { + if (!phoneNumbers.length) return false + const response = await nuiCall('messages:delete', { phoneNumbers }) + if (!response.success) return false + if (activeNumber.value && phoneNumbers.includes(activeNumber.value)) { + closeThread() + } + await loadConversations() + return true + } + + async function searchGifs( + query: string, + offset = 0, + ): Promise> { + return nuiCall('messages:gifs', { offset, query }) + } + + function closeThread(): void { + activeNumber.value = null + messages.value = [] + mediaSources.value = {} + } + + return { + activeNumber, + closeThread, + conversations, + deleteConversations, + loadConversations, + loadMedia, + loading, + mediaSources, + messages, + openThread, + searchGifs, + send, + unreadCount, + } +})