Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 02:54:58 +00:00
co-authored by renee-png
parent f524dc8fd1
commit ca5b7ea01a
7 changed files with 1659 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
import { supabase } from "@/integrations/supabase/client";
import type { Database } from "@/integrations/supabase/types";
export type NotificationKind = Database["public"]["Enums"]["notification_kind"];
export interface CreateNotificationInput {
user_id: string;
kind: NotificationKind;
title: string;
body?: string | null;
link?: string | null;
task_id?: string | null;
case_id?: string | null;
conversation_id?: string | null;
created_by?: string | null;
}
export async function createNotifications(rows: CreateNotificationInput[]) {
if (!rows.length) return;
await supabase.from("notifications").insert(rows);
}
const MENTION_RE = /@\[([^\]]+)\]\(user:([0-9a-f-]{36})\)/g;
export function extractMentions(body: string): string[] {
const ids = new Set<string>();
let m: RegExpExecArray | null;
while ((m = MENTION_RE.exec(body)) !== null) {
ids.add(m[2]);
}
return Array.from(ids);
}
export function renderMentionsToText(body: string): string {
return body.replace(MENTION_RE, "@$1");
}
+79
View File
@@ -0,0 +1,79 @@
import { useEffect, useState, useCallback } from "react";
import { supabase } from "@/integrations/supabase/client";
import { useAuth } from "@/lib/auth";
interface Counts {
messages: number;
tasks: number;
notifications: number;
}
export function useBubbleCounts(): Counts {
const { user } = useAuth();
const [counts, setCounts] = useState<Counts>({ messages: 0, tasks: 0, notifications: 0 });
const load = useCallback(async () => {
if (!user) {
setCounts({ messages: 0, tasks: 0, notifications: 0 });
return;
}
const [memberRes, msgRes, taskRes, notifRes] = await Promise.all([
supabase.from("conversation_members").select("conversation_id, last_read_at").eq("user_id", user.id),
// unread messages: query per-convo handled below
Promise.resolve(null),
supabase
.from("task_assignees")
.select("task_id, tasks!inner(status, due_date)")
.eq("user_id", user.id)
.eq("tasks.status", "incomplete"),
supabase
.from("notifications")
.select("id", { count: "exact", head: true })
.eq("user_id", user.id)
.is("read_at", null),
]);
let unreadMessages = 0;
const memberRows = memberRes.data ?? [];
if (memberRows.length) {
// For each conversation, count messages newer than last_read_at and not from self
const results = await Promise.all(
memberRows.map(async (m) => {
const { count } = await supabase
.from("messages")
.select("id", { count: "exact", head: true })
.eq("conversation_id", m.conversation_id)
.gt("created_at", m.last_read_at)
.neq("sender_id", user.id);
return count ?? 0;
}),
);
unreadMessages = results.reduce((a, b) => a + b, 0);
}
const taskCount = (taskRes.data ?? []).length;
setCounts({
messages: unreadMessages,
tasks: taskCount,
notifications: notifRes.count ?? 0,
});
}, [user]);
useEffect(() => {
if (!user) return;
load();
const ch = supabase
.channel(`bubbles-${user.id}`)
.on("postgres_changes", { event: "*", schema: "public", table: "messages" }, () => load())
.on("postgres_changes", { event: "*", schema: "public", table: "task_assignees" }, () => load())
.on("postgres_changes", { event: "*", schema: "public", table: "tasks" }, () => load())
.on("postgres_changes", { event: "*", schema: "public", table: "notifications", filter: `user_id=eq.${user.id}` }, () => load())
.on("postgres_changes", { event: "*", schema: "public", table: "conversation_members", filter: `user_id=eq.${user.id}` }, () => load())
.subscribe();
return () => {
supabase.removeChannel(ch);
};
}, [user, load]);
return counts;
}