114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
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 };
|
|
}
|