diff --git a/frontend/src/stores/messages.ts b/frontend/src/stores/messages.ts index 749f674..8533192 100644 --- a/frontend/src/stores/messages.ts +++ b/frontend/src/stores/messages.ts @@ -8,6 +8,7 @@ import type { SmsMessage, SmsOutgoingMessage, } from '@/types/messages' +import { sortConversationsByRecency } from '@/utils/messages' import { nuiCall, type NuiResponse } from '@/utils/nui' export const useMessagesStore = defineStore('messages', () => { @@ -23,10 +24,12 @@ export const useMessagesStore = defineStore('messages', () => { async function loadConversations(): Promise { const response = await nuiCall('messages:conversations') if (response.success && response.data) { - conversations.value = response.data.map((conversation) => ({ - ...conversation, - phoneNumber: String(conversation.phoneNumber), - })) + conversations.value = sortConversationsByRecency( + response.data.map((conversation) => ({ + ...conversation, + phoneNumber: String(conversation.phoneNumber), + })), + ) } else if (!response.success) conversations.value = [] return response.success diff --git a/frontend/src/utils/messages.test.ts b/frontend/src/utils/messages.test.ts new file mode 100644 index 0000000..3ebed9a --- /dev/null +++ b/frontend/src/utils/messages.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' + +import type { SmsConversation } from '@/types/messages' +import { + sortContactsByMessageRecency, + sortConversationsByRecency, +} from '@/utils/messages' + +function conversation( + phoneNumber: string, + lastMessageAt: string, +): SmsConversation { + return { + lastMessage: phoneNumber, + lastMessageAt, + lastMessageType: 'text', + phoneNumber, + unread: 0, + } +} + +const conversations = [ + conversation('5550000001', '2026-08-12 10:00:00'), + conversation('5550000002', '2026-08-12 12:00:00'), +] + +describe('SMS recency ordering', () => { + it('places the conversation with the newest message first', () => { + expect( + sortConversationsByRecency(conversations).map( + (item) => item.phoneNumber, + ), + ).toEqual(['5550000002', '5550000001']) + }) + + it('places recently messaged contacts before unused contacts', () => { + const contacts = [ + { name: 'Unused', phone_number: '5550000003' }, + { name: 'Older', phone_number: '5550000001' }, + { name: 'Newest', phone_number: '5550000002' }, + ] + + expect( + sortContactsByMessageRecency(contacts, conversations).map( + (item) => item.name, + ), + ).toEqual(['Newest', 'Older', 'Unused']) + }) +}) diff --git a/frontend/src/utils/messages.ts b/frontend/src/utils/messages.ts new file mode 100644 index 0000000..64fde58 --- /dev/null +++ b/frontend/src/utils/messages.ts @@ -0,0 +1,43 @@ +import type { SmsConversation } from '@/types/messages' +import { parseDatabaseDate } from '@/utils/date' + +function conversationTime(conversation: SmsConversation): number { + const timestamp = parseDatabaseDate(conversation.lastMessageAt).getTime() + return Number.isNaN(timestamp) ? 0 : timestamp +} + +export function sortConversationsByRecency( + conversations: SmsConversation[], +): SmsConversation[] { + return conversations + .map((conversation, index) => ({ conversation, index })) + .sort( + (left, right) => + conversationTime(right.conversation) - + conversationTime(left.conversation) || left.index - right.index, + ) + .map(({ conversation }) => conversation) +} + +export function sortContactsByMessageRecency< + T extends { phone_number: string }, +>(contacts: T[], conversations: SmsConversation[]): T[] { + const activity = new Map( + conversations.map((conversation) => [ + conversation.phoneNumber, + conversationTime(conversation), + ]), + ) + return contacts + .map((contact, index) => ({ contact, index })) + .sort((left, right) => { + const leftActivity = activity.get(left.contact.phone_number) + const rightActivity = activity.get(right.contact.phone_number) + if (leftActivity === undefined && rightActivity === undefined) + return left.index - right.index + if (leftActivity === undefined) return 1 + if (rightActivity === undefined) return -1 + return rightActivity - leftActivity || left.index - right.index + }) + .map(({ contact }) => contact) +} diff --git a/frontend/src/views/apps/MessagesApp.vue b/frontend/src/views/apps/MessagesApp.vue index 2b87689..3711643 100644 --- a/frontend/src/views/apps/MessagesApp.vue +++ b/frontend/src/views/apps/MessagesApp.vue @@ -54,6 +54,7 @@ import { useMessagesStore } from '@/stores/messages' import { useMessageMediaStore } from '@/stores/messageMedia' import { usePhoneStore } from '@/stores/phone' import { parseDatabaseDate, type DatabaseDateValue } from '@/utils/date' +import { sortContactsByMessageRecency } from '@/utils/messages' import type { GifSearchResult, SmsAttachmentType, @@ -139,8 +140,9 @@ const contactAvatarUrls = computed( ) const contactSuggestions = computed(() => { const query = composerNumber.value.trim().toLocaleLowerCase(phone.lang) - const contacts = calls.contacts.filter( - (contact) => contact.canMessage !== false, + const contacts = sortContactsByMessageRecency( + calls.contacts.filter((contact) => contact.canMessage !== false), + messages.conversations, ) if (!query) return contacts.slice(0, 8) return contacts