diff --git a/packages/shared/src/chat/index.ts b/packages/shared/src/chat/index.ts index cb04224..2172065 100644 --- a/packages/shared/src/chat/index.ts +++ b/packages/shared/src/chat/index.ts @@ -7,6 +7,7 @@ export * from './groups'; export * from './messages'; export * from './types'; export * from './userKeyMigration'; +export * from './pinnedMessages'; // ----- RPC wrappers --------------------------------------------------------- diff --git a/packages/shared/src/chat/pinnedMessages.test.ts b/packages/shared/src/chat/pinnedMessages.test.ts new file mode 100644 index 0000000..fa25c79 --- /dev/null +++ b/packages/shared/src/chat/pinnedMessages.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; + +import { listPinnedMessages, pinMessage, unpinMessage } from './pinnedMessages'; + +function fakeClient(rows: Array>) { + const calls: Array<{ name: string; payload: unknown }> = []; + const client = { + from(_table: string) { + return { + select: () => ({ + eq: () => ({ + order: () => Promise.resolve({ data: rows, error: null }), + }), + }), + insert: (payload: unknown) => { + calls.push({ name: 'insert', payload }); + return Promise.resolve({ error: null }); + }, + delete: () => ({ + eq: () => ({ + eq: () => { + calls.push({ name: 'delete', payload: null }); + return Promise.resolve({ error: null }); + }, + }), + }), + }; + }, + }; + return { client, calls }; +} + +describe('pinnedMessages', () => { + it('listPinnedMessages maps DB rows to camelCase', async () => { + const rows = [ + { conversation_id: 'c1', message_id: 'm1', pinned_by: 'u1', pinned_at: '2026-05-16T10:00:00Z' }, + ]; + const { client } = fakeClient(rows); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const result = await listPinnedMessages(client as any, 'c1'); + expect(result).toEqual([ + { conversationId: 'c1', messageId: 'm1', pinnedBy: 'u1', pinnedAt: '2026-05-16T10:00:00Z' }, + ]); + }); + + it('pinMessage inserts with the right shape', async () => { + const { client, calls } = fakeClient([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await pinMessage(client as any, 'c1', 'm1', 'u1'); + expect(calls).toEqual([ + { name: 'insert', payload: { conversation_id: 'c1', message_id: 'm1', pinned_by: 'u1' } }, + ]); + }); + + it('unpinMessage deletes by conversation_id + message_id', async () => { + const { client, calls } = fakeClient([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await unpinMessage(client as any, 'c1', 'm1'); + expect(calls).toEqual([{ name: 'delete', payload: null }]); + }); +}); diff --git a/packages/shared/src/chat/pinnedMessages.ts b/packages/shared/src/chat/pinnedMessages.ts new file mode 100644 index 0000000..139d047 --- /dev/null +++ b/packages/shared/src/chat/pinnedMessages.ts @@ -0,0 +1,65 @@ +import type { AppSupabaseClient } from '../supabase/client'; + +export interface PinnedMessage { + conversationId: string; + messageId: string; + pinnedBy: string; + pinnedAt: string; +} + +interface Row { + conversation_id: string; + message_id: string; + pinned_by: string; + pinned_at: string; +} + +function mapRow(r: Row): PinnedMessage { + return { + conversationId: r.conversation_id, + messageId: r.message_id, + pinnedBy: r.pinned_by, + pinnedAt: r.pinned_at, + }; +} + +export async function listPinnedMessages( + client: AppSupabaseClient, + conversationId: string, +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { data, error } = await (client as any) + .from('pinned_messages') + .select('conversation_id, message_id, pinned_by, pinned_at') + .eq('conversation_id', conversationId) + .order('pinned_at', { ascending: false }); + if (error) throw error; + return (data ?? []).map(mapRow); +} + +export async function pinMessage( + client: AppSupabaseClient, + conversationId: string, + messageId: string, + pinnedBy: string, +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error } = await (client as any) + .from('pinned_messages') + .insert({ conversation_id: conversationId, message_id: messageId, pinned_by: pinnedBy }); + if (error) throw error; +} + +export async function unpinMessage( + client: AppSupabaseClient, + conversationId: string, + messageId: string, +): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const { error } = await (client as any) + .from('pinned_messages') + .delete() + .eq('conversation_id', conversationId) + .eq('message_id', messageId); + if (error) throw error; +}