diff --git a/frontend/testserver/index.cjs b/frontend/testserver/index.cjs index e237df0..8ebe931 100644 --- a/frontend/testserver/index.cjs +++ b/frontend/testserver/index.cjs @@ -12,6 +12,44 @@ let draft = null let linkedAccount = null let mockNotes = [] const deviceData = {} +let contactSequence = 2 +const contacts = [ + { + created_at: '2026-08-04 12:00:00', + id: 'contact-1', + name: 'Jenica Chong', + phone_number: '5558675309', + updated_at: '2026-08-04 12:00:00', + }, +] +const attachmentAssets = { + gif: new Set(['celebrate', 'hearts', 'party', 'thumbs_up', 'wow']), + image: new Set([ + 'camera-1', 'camera-2', 'camera-3', 'city-lights', 'desert-road', + 'ocean-air', 'sunset-drive', + ]), + video: new Set(['city-loop', 'ocean-loop', 'sunset-loop']), +} +const gifMocks = [ + ['ICOgUNjpvO0PC', 'Cat reaction'], + ['MDJ9IbxxvDUQM', 'Happy dog'], + ['l0HlPystfePnAI3G8', 'Celebrate'], + ['26ufdipQqU2lhNA4g', 'Wow'], + ['3o7abKhOpu0NwenH3O', 'Perfect'], + ['xT0xeJpnrWC4XWblEk', 'Party'], + ['111ebonMs90YLu', 'Thumbs up'], + ['5GoVLqeAOo6PK', 'Excited'], + ['TdfyKrN7HGTIY', 'Happy dance'], + ['14udF3WUwwGMaA', 'Surprised'], + ['3o6Zt6ML6BklcajjsA', 'Applause'], + ['13CoXDiaCcCoyk', 'Let us go'], + ['R6gvnAxj2ISzJdbA63', 'Yes'], + ['xUPGcEliCc7bETyfO8', 'Laughing'], + ['26BRuo6sLetdllPAQ', 'Dancing'], + ['l46CyJmS9KUbokzsI', 'Amazing'], + ['g9582DNuQppxC', 'Celebration'], + ['artj92V8o75VPL7AeQ', 'High five'], +] const accountDevices = [ { created_at: '2026-08-04 12:00:00', @@ -75,6 +113,38 @@ const messages = [ trashed_at: '2026-08-04 08:00:00', }, ] +const smsMessages = [ + { + body: 'Bin gleich am Würfelpark. Kommst du auch?', + created_at: '2026-08-06 13:04:00', + direction: 'received', + id: 'sms-1', + media_duration_ms: null, + media_mime: null, + media_payload: null, + media_waveform: null, + message_type: 'text', + media_asset_id: null, + read_at: null, + recipient_number: '5551234567', + sender_number: '5558675309', + }, + { + body: 'Ja, gib mir fünf Minuten.', + created_at: '2026-08-06 13:05:00', + direction: 'sent', + id: 'sms-2', + media_duration_ms: null, + media_mime: null, + media_payload: null, + media_waveform: null, + message_type: 'text', + media_asset_id: null, + read_at: null, + recipient_number: '5558675309', + sender_number: '5551234567', + }, +] function counts() { return { @@ -114,6 +184,196 @@ app.post('/api/:endpoint', (request, response) => { }) return } + if (endpoint === 'messages:conversations') { + const grouped = new Map() + for (const message of [...smsMessages].reverse()) { + const phoneNumber = + message.direction === 'sent' + ? message.recipient_number + : message.sender_number + const conversation = grouped.get(phoneNumber) + if (conversation) { + if (message.direction === 'received' && !message.read_at) + conversation.unread += 1 + continue + } + grouped.set(phoneNumber, { + lastMessage: message.body, + lastMessageAt: message.created_at, + lastMessageType: message.message_type, + phoneNumber, + unread: message.direction === 'received' && !message.read_at ? 1 : 0, + }) + } + response.json({ success: true, data: [...grouped.values()] }) + return + } + if (endpoint === 'messages:gifs') { + const offset = Math.max(0, Number(request.body.offset ?? 0)) + const pageSize = 6 + const results = gifMocks.slice(offset, offset + pageSize).map(([id, title]) => ({ + height: 200, + id, + previewUrl: `https://media.giphy.com/media/${id}/200w.gif`, + title, + url: `https://media.giphy.com/media/${id}/giphy.gif`, + width: 200, + })) + response.json({ + success: true, + data: { + hasMore: offset + results.length < gifMocks.length, + nextOffset: offset + results.length, + results, + }, + }) + return + } + if (endpoint === 'messages:thread') { + const number = String(request.body.phoneNumber) + const thread = smsMessages.filter( + (message) => + message.sender_number === number || message.recipient_number === number, + ) + for (const message of thread) { + if (message.direction === 'received') message.read_at = message.read_at ?? '2026-08-06 13:06:00' + } + response.json({ + success: true, + data: thread.map(({ media_payload, ...message }) => ({ + ...message, + media_asset_id: ['image', 'gif', 'video'].includes(message.message_type) + ? media_payload + : null, + })), + }) + return + } + if (endpoint === 'messages:media') { + const message = smsMessages.find( + (item) => item.id === request.body.id && item.message_type === 'voice', + ) + response.json( + message + ? { + success: true, + data: { + mime: message.media_mime, + payload: message.media_payload, + }, + } + : { success: false, error: 'message_not_found' }, + ) + return + } + if (endpoint === 'messages:send') { + const body = String(request.body.body ?? '').trim() + const phoneNumber = String(request.body.phoneNumber ?? '') + const messageType = request.body.messageType ?? 'text' + const isAttachment = ['image', 'gif', 'video'].includes(messageType) + const attachmentId = String(request.body.mediaAssetId ?? '') + if ( + !phoneNumber || + (messageType === 'text' && !body) || + (messageType === 'voice' && !request.body.mediaPayload) || + (isAttachment && + !attachmentAssets[messageType].has(attachmentId) && + !attachmentId.startsWith('https://')) + ) { + response.json({ success: false, error: 'invalid_message' }) + return + } + const message = { + body, + created_at: new Date().toISOString().slice(0, 19).replace('T', ' '), + direction: 'sent', + id: `sms-${Date.now()}`, + media_duration_ms: + ['voice', 'video'].includes(messageType) + ? request.body.mediaDurationMs ?? null + : null, + media_mime: + messageType === 'voice' + ? request.body.mediaMime + : messageType === 'image' + ? 'image/jpeg' + : messageType === 'gif' + ? 'image/gif' + : messageType === 'video' + ? 'video/mp4' + : null, + media_payload: + messageType === 'voice' + ? request.body.mediaPayload + : isAttachment + ? attachmentId + : null, + media_waveform: + messageType === 'voice' ? request.body.mediaWaveform : null, + message_type: messageType, + media_asset_id: isAttachment ? attachmentId : null, + read_at: null, + recipient_number: phoneNumber, + sender_number: '5551234567', + } + smsMessages.push(message) + const { media_payload, ...publicMessage } = message + response.json({ success: true, data: publicMessage }) + return + } + if (endpoint === 'messages:delete') { + const phoneNumbers = new Set( + Array.isArray(request.body.phoneNumbers) + ? request.body.phoneNumbers.map(String) + : [], + ) + for (let index = smsMessages.length - 1; index >= 0; index -= 1) { + const message = smsMessages[index] + const otherNumber = + message.direction === 'sent' + ? message.recipient_number + : message.sender_number + if (phoneNumbers.has(otherNumber)) smsMessages.splice(index, 1) + } + response.json({ success: true }) + return + } + if (endpoint === 'contacts:list') { + response.json({ success: true, data: contacts }) + return + } + if (endpoint === 'contacts:save') { + const name = String(request.body.name ?? '').trim() + const phoneNumber = String(request.body.phoneNumber ?? '').trim() + if (!name || !phoneNumber) { + response.json({ success: false, error: 'invalid_contact' }) + return + } + let contact = contacts.find((item) => item.id === request.body.id) + if (contact) { + contact.name = name + contact.phone_number = phoneNumber + contact.updated_at = new Date().toISOString().slice(0, 19).replace('T', ' ') + } else { + const now = new Date().toISOString().slice(0, 19).replace('T', ' ') + contact = { + created_at: now, + id: `contact-${contactSequence++}`, + name, + phone_number: phoneNumber, + updated_at: now, + } + contacts.push(contact) + } + response.json({ success: true, data: contact }) + return + } + if (endpoint === 'contacts:delete') { + const index = contacts.findIndex((item) => item.id === request.body.id) + if (index >= 0) contacts.splice(index, 1) + response.json({ success: true }) + return + } if (endpoint === 'account:login' || endpoint === 'account:register') { authenticated = true linkedAccount = {