import { sendMentionEmail } from '../email'; export type ChatActor = { id: string; type: 'admin' | 'member'; name: string; color: string; }; // Everyone in the family (children + parents) for the @mention picker. export async function chatMe(pb: any, famId: string, actor: ChatActor) { const users = await pb .collection('users') .getFullList({ filter: `famId = '${famId}'` }) .catch(() => []); return { famId, actor, members: users.map((m: any) => ({ id: m.id, name: m.name || '', color: m.color || '#6366f1', role: m.role === 'parent' ? 'parent' : 'child' })) }; } export async function send( pb: any, famId: string, actor: ChatActor, body: { content?: string; clientId?: string; siteUrl?: string } ) { const content = (body.content || '').trim(); if (!content) throw new Error('content required'); const record = await pb.collection('messages').create({ famId, authorType: actor.type, authorId: actor.id, authorName: actor.name, authorColor: actor.color, content, createdAt: new Date().toISOString(), clientId: body.clientId ? String(body.clientId).slice(0, 64) : '' }); // Email any @mentioned parents (their in-app toast is client-side, via the // same mention tokens). Fire-and-forget — never fail the message itself. notifyMentionedParents(pb, famId, actor, record, body.siteUrl || '').catch(() => {}); return record; } // Scan the message for `@Parent Name` tokens (case-insensitive) and email every // mentioned parent who isn't the sender, with a link back to the family site. async function notifyMentionedParents( pb: any, famId: string, actor: ChatActor, message: any, siteUrl: string ) { const content = String(message.content || ''); const parents = await pb .collection('users') .getFullList({ filter: `famId = '${famId}' && role = 'parent'` }) .catch(() => []); const mentioned = parents.filter((u: any) => { if (u.id === actor.id || !u.email) return false; const name = (u.name || '').trim().toLowerCase(); return !!name && content.toLowerCase().includes('@' + name); }); if (!mentioned.length) return; const fam = await pb .collection('fams') .getOne(famId) .catch(() => null); // Don't send emails for the demo family. if (fam?.slug === 'showboaters') return; const dashboardUrl = `${siteUrl}/${fam?.slug || famId}`; for (const parent of mentioned) { try { await sendMentionEmail({ to: parent.email, parentName: parent.name || 'there', authorName: message.authorName || 'Family member', message: content, dashboardUrl }); } catch (err) { console.error(`[chat] Failed to send mention email to ${parent.email}:`, err); } } } export async function typing(pb: any, famId: string, actor: ChatActor, body: { typing?: boolean }) { const existing = await pb.collection('chat_typing').getFullList({ filter: `famId = '${famId}' && actorId = '${actor.id}' && actorType = '${actor.type}'` }); const row = { famId, actorId: actor.id, actorType: actor.type, authorName: actor.name, authorColor: actor.color, typing: !!body.typing }; if (existing?.length) { await pb.collection('chat_typing').update(existing[0].id, row); } else { await pb.collection('chat_typing').create(row); } return { ok: true }; }