diff --git a/packages/web/src/utils/dmFormatters.test.ts b/packages/web/src/utils/dmFormatters.test.ts index 84633c0b..3513442b 100644 --- a/packages/web/src/utils/dmFormatters.test.ts +++ b/packages/web/src/utils/dmFormatters.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; -import { formatDmTimestamp } from './dmFormatters'; -import { formatDmPreview } from './dmFormatters'; +import { formatDmTimestamp, formatDmPreview, formatDmSidebarPreview } from './dmFormatters'; +import type { DmChannel, DmLastMessagePreview, User } from '@backspace/shared'; /** Build a local-time Date: new Date(year, month-1, day, hour, minute) as a timestamp. */ function localTs(year: number, month: number, day: number, hour = 12, minute = 0): number { @@ -180,3 +180,85 @@ describe('formatDmPreview', () => { })).toBe('check this 🎬'); }); }); + +// ─── System message previews — exercised via formatDmSidebarPreview ────────── + +const actor: User = { + id: 'U1', + username: 'jannis', + displayName: 'Jannis', + avatarColor: 'mint', + avatar: null, + bio: null, + banner: null, + accentColor: null, + homeUserId: null, + homeInstance: null, + status: 'online', + customStatus: null, + isAdmin: false, + createdAt: 0, + replicatedInstances: [], +}; + +function makeGroupDm(lastMessage: DmLastMessagePreview): Pick { + return { + ownerId: 'U1', // makes it a group DM + members: [actor], + lastMessage, + }; +} + +describe('formatDmSidebarPreview — name_changed system message', () => { + it('happy path with newName → " renamed the group"', () => { + const dm = makeGroupDm({ + type: 'system', + userId: 'U1', + content: JSON.stringify({ event: 'name_changed', oldName: null, newName: 'Cool Group' }), + createdAt: 1, + }); + expect(formatDmSidebarPreview(dm, { id: 'OTHER', username: 'other' })).toBe('Jannis renamed the group'); + }); + + it('newName=null (cleared) → " cleared the group name"', () => { + const dm = makeGroupDm({ + type: 'system', + userId: 'U1', + content: JSON.stringify({ event: 'name_changed', oldName: 'Old', newName: null }), + createdAt: 1, + }); + expect(formatDmSidebarPreview(dm, { id: 'OTHER', username: 'other' })).toBe('Jannis cleared the group name'); + }); + + it('unresolvable actor → "Unknown renamed the group"', () => { + const dm = makeGroupDm({ + type: 'system', + userId: 'GHOST', // not in members roster + content: JSON.stringify({ event: 'name_changed', oldName: null, newName: 'X' }), + createdAt: 1, + }); + expect(formatDmSidebarPreview(dm, { id: 'OTHER', username: 'other' })).toBe('Unknown renamed the group'); + }); +}); + +describe('formatDmSidebarPreview — icon_changed system message', () => { + it('happy path → " updated the group icon"', () => { + const dm = makeGroupDm({ + type: 'system', + userId: 'U1', + content: JSON.stringify({ event: 'icon_changed' }), + createdAt: 1, + }); + expect(formatDmSidebarPreview(dm, { id: 'OTHER', username: 'other' })).toBe('Jannis updated the group icon'); + }); + + it('unresolvable actor → "Unknown updated the group icon"', () => { + const dm = makeGroupDm({ + type: 'system', + userId: 'GHOST', + content: JSON.stringify({ event: 'icon_changed' }), + createdAt: 1, + }); + expect(formatDmSidebarPreview(dm, { id: 'OTHER', username: 'other' })).toBe('Unknown updated the group icon'); + }); +}); diff --git a/packages/web/src/utils/dmFormatters.ts b/packages/web/src/utils/dmFormatters.ts index 58234a37..f4f1000c 100644 --- a/packages/web/src/utils/dmFormatters.ts +++ b/packages/web/src/utils/dmFormatters.ts @@ -101,6 +101,9 @@ interface SystemEventPayload { newOwnerId?: unknown; newOwnerDisplayName?: unknown; reason?: unknown; + // name_changed payload + oldName?: unknown; + newName?: unknown; // space_invite payload snapshot?: { spaceName?: unknown }; } @@ -149,6 +152,16 @@ function formatSystemPreview(content: string | null, actor: User | null | undefi const newOwner = asString(data.newOwnerDisplayName) ?? 'A member'; return `${newOwner} is now the group owner`; } + case 'name_changed': { + // newName === null is a meaningful "cleared" state — distinct from a + // missing field — so we check for a non-empty string explicitly. + return asString(data.newName) + ? `${actorName} renamed the group` + : `${actorName} cleared the group name`; + } + case 'icon_changed': { + return `${actorName} updated the group icon`; + } default: return 'System message'; }