feat(P5C.T2): per-conv 'mentions only' toggle + notification gate

This commit is contained in:
byGalax
2026-05-16 22:22:04 +02:00
parent b8b451ef4f
commit 7ebc5f6c9d
6 changed files with 71 additions and 5 deletions
+3
View File
@@ -57,6 +57,7 @@ export type Database = {
accepted: boolean
conversation_id: string
joined_at: string
mentions_only: boolean
role: Database["public"]["Enums"]["member_role"]
user_id: string
}
@@ -64,6 +65,7 @@ export type Database = {
accepted?: boolean
conversation_id: string
joined_at?: string
mentions_only?: boolean
role?: Database["public"]["Enums"]["member_role"]
user_id: string
}
@@ -71,6 +73,7 @@ export type Database = {
accepted?: boolean
conversation_id?: string
joined_at?: string
mentions_only?: boolean
role?: Database["public"]["Enums"]["member_role"]
user_id?: string
}
+28 -2
View File
@@ -35,7 +35,7 @@ export async function listConversations(client: AppSupabaseClient): Promise<Conv
// cast the select to bypass typing.
const { data: myMembers, error: mErr } = await client
.from('conversation_members')
.select('conversation_id, role, accepted, archived, muted_until' as '*')
.select('conversation_id, role, accepted, archived, muted_until, mentions_only' as '*')
.eq('user_id', myId);
if (mErr) throw mErr;
const myMembersList = (myMembers ?? []) as unknown as Array<{
@@ -44,6 +44,7 @@ export async function listConversations(client: AppSupabaseClient): Promise<Conv
accepted: boolean;
archived: boolean | null;
muted_until: string | null;
mentions_only: boolean | null;
}>;
if (myMembersList.length === 0) return [];
@@ -114,7 +115,13 @@ export async function listConversations(client: AppSupabaseClient): Promise<Conv
? (members.find((m) => m.userId !== myId)?.profile ?? null)
: null;
const mineRow = mine as
| { accepted: boolean; role: string; archived?: boolean; muted_until?: string | null }
| {
accepted: boolean;
role: string;
archived?: boolean;
muted_until?: string | null;
mentions_only?: boolean | null;
}
| undefined;
return {
id: c.id,
@@ -129,6 +136,7 @@ export async function listConversations(client: AppSupabaseClient): Promise<Conv
lastMessageAt: lastSeen.get(c.id) ?? null,
archived: mineRow?.archived ?? false,
mutedUntil: mineRow?.muted_until ?? null,
mentionsOnly: mineRow?.mentions_only ?? false,
};
});
}
@@ -164,6 +172,24 @@ export async function setConversationMutedUntil(
if (error) throw error;
}
// Toggle "mentions only" — when true the renderer's notification gate
// suppresses non-mention alerts for this conversation. Mentions still fire
// via the independent useMentionNotifications subscription on
// message_mentions, so the @-alerts are never lost.
export async function setConversationMentionsOnly(
client: AppSupabaseClient,
params: { conversationId: string; mentionsOnly: boolean },
): Promise<void> {
const { data: session } = await client.auth.getUser();
if (!session.user) throw new Error('not authenticated');
const { error } = await client
.from('conversation_members')
.update({ mentions_only: params.mentionsOnly } as never)
.eq('conversation_id', params.conversationId)
.eq('user_id', session.user.id);
if (error) throw error;
}
// Convenience: `null` unmutes, number means minutes from now. For "forever"
// pass a very large number (e.g. 100 years worth of minutes).
export function muteDurationToIso(minutes: number | null): string | null {
+4
View File
@@ -28,6 +28,10 @@ export interface ConversationSummary {
// ISO timestamp. null = not muted. Past timestamp = expired mute (treat as
// not muted — the server row is kept for history until the next toggle).
mutedUntil: string | null;
// When true the renderer suppresses non-mention notifications. Mentions
// still fire via the independent useMentionNotifications subscription on
// message_mentions, so this flag never silences @-alerts.
mentionsOnly: boolean;
}
export interface ChatMessage {