feat: add conversation image previews and sharing

This commit is contained in:
jjbrunton committed 2026-08-09 22:28:41 +01:00
1 parent 488145340d
commit b573b88b08
20 files changed
+634 -26

No files matched your search

+66
View File
@@ -133,6 +133,72 @@ describe('conversations route', () => {
expect(first.resources.activityLogRepo.insert).not.toHaveBeenCalled();
});
it('lists and sends only request-matched library media through the owning runtime', async () => {
const accountId = '@t2_one:reddit.com';
const assetId = '11111111-1111-4111-8111-111111111111';
const conversation = {
username: 'alice', roomName: 'alice', roomId: 'room-one', url: '/room/room-one',
lastMessageTime: 2, lastMessageSnippet: 'send a selfie', isFromUs: false, unread: true,
messages: [
{ sender: 'me', text: 'hello', timestamp: 1, isSelf: true },
{ eventId: '$request', sender: 'alice', text: 'send a selfie', timestamp: 2, isSelf: false },
],
};
const asset = {
id: assetId, tags: ['selfie'], safety: 'sfw', classificationStatus: 'ready', description: 'selfie', confidence: 1,
fileName: 'selfie.jpg', relativePath: 'nested/selfie.jpg', mimeType: 'image/jpeg', sizeBytes: 4,
width: 10, height: 10, enabled: true, consentConfirmedAt: 1, createdAt: 1, updatedAt: 1,
sendCount: 0, lastSentAt: null,
};
const mediaExchange = {
listShareable: vi.fn().mockReturnValue({ requestKind: 'selfie', assets: [asset] }),
handleSelected: vi.fn().mockResolvedValue({ kind: 'sent', assetId, requestKind: 'selfie' }),
};
const runtime = {
snapshot: () => ({ state: 'running' }),
resources: {
chatService: {
getCachedConversations: vi.fn().mockResolvedValue([conversation]),
loadConversationDetails: vi.fn().mockResolvedValue(undefined),
refreshTopChats: vi.fn().mockResolvedValue(undefined),
},
conversationStateRepo: { get: vi.fn(), save: vi.fn() },
activityLogRepo: { insert: vi.fn() },
mediaExchange,
rateLimiter: { throttle: vi.fn((work: () => Promise<unknown>) => work()) },
},
};
const context = {
logger: { error: vi.fn() }, activity: { push: vi.fn() }, broadcaster: { push: vi.fn() },
services: {
chat: {},
accountRuntimes: {
list: () => [{ accountId, redditUsername: 'owner-one', state: 'running' }],
get: (requested: string) => requested === accountId ? runtime : null,
},
},
} as unknown as WebContext;
const app = express().use(express.json()).use('/api', createConversationsRouter(context));
const ownedId = `${encodeURIComponent(accountId)}~room-one`;
const optionsResponse = await request(app, `/api/conversations/${encodeURIComponent(ownedId)}/shareable-media`);
expect(optionsResponse.status).toBe(200);
expect(await optionsResponse.json()).toEqual({
requestKind: 'selfie', reason: null,
assets: [expect.objectContaining({ id: assetId, urlPath: '/library-media/nested/selfie.jpg' })],
});
const sendResponse = await request(app, `/api/conversations/${encodeURIComponent(ownedId)}/share-media`, {
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ assetId }),
});
expect(sendResponse.status).toBe(200);
expect(await sendResponse.json()).toEqual({ success: true });
expect(mediaExchange.handleSelected).toHaveBeenCalledWith('room-one', '$request', 'send a selfie', assetId);
expect(runtime.resources.conversationStateRepo.save).toHaveBeenCalledWith('room-one', expect.objectContaining({
automation: expect.objectContaining({ status: 'user-overridden' }),
}));
});
it('rejects an unowned conversation ID while account runtimes are active', async () => {
const context = {
logger: { error: vi.fn() },