feat(notifications): per-chat mute settings
Some checks failed
CI / test (push) Failing after 18s

- add chat_notification_settings table and migration
- add chat notifications API (get/update muted)
- skip message notifications for muted recipients
- add mute/unmute control in chat info panel
This commit is contained in:
2026-03-08 02:17:09 +03:00
parent eddd4bda0b
commit ea8a50ee05
9 changed files with 238 additions and 3 deletions

View File

@@ -2,6 +2,12 @@ import { http } from "./http";
import type { Chat, ChatDetail, ChatMember, ChatMemberRole, ChatType, DiscoverChat, Message, MessageType } from "../chat/types";
import axios from "axios";
export interface ChatNotificationSettings {
chat_id: number;
user_id: number;
muted: boolean;
}
export async function getChats(query?: string): Promise<Chat[]> {
const { data } = await http.get<Chat[]>("/chats", {
params: query?.trim() ? { query: query.trim() } : undefined
@@ -207,3 +213,13 @@ export async function removeChatMember(chatId: number, userId: number): Promise<
export async function leaveChat(chatId: number): Promise<void> {
await http.post(`/chats/${chatId}/leave`);
}
export async function getChatNotificationSettings(chatId: number): Promise<ChatNotificationSettings> {
const { data } = await http.get<ChatNotificationSettings>(`/chats/${chatId}/notifications`);
return data;
}
export async function updateChatNotificationSettings(chatId: number, muted: boolean): Promise<ChatNotificationSettings> {
const { data } = await http.put<ChatNotificationSettings>(`/chats/${chatId}/notifications`, { muted });
return data;
}

View File

@@ -2,10 +2,12 @@ import { useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import {
addChatMember,
getChatNotificationSettings,
getChatDetail,
leaveChat,
listChatMembers,
removeChatMember,
updateChatNotificationSettings,
updateChatMemberRole,
updateChatTitle
} from "../api/chats";
@@ -33,6 +35,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
const [savingTitle, setSavingTitle] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<UserSearchItem[]>([]);
const [muted, setMuted] = useState(false);
const [savingMute, setSavingMute] = useState(false);
const myRole = useMemo(() => members.find((m) => m.user_id === me?.id)?.role, [members, me?.id]);
const isGroupLike = chat?.type === "group" || chat?.type === "channel";
@@ -65,6 +69,10 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
if (cancelled) return;
setChat(detail);
setTitleDraft(detail.title ?? "");
const notificationSettings = await getChatNotificationSettings(chatId);
if (!cancelled) {
setMuted(notificationSettings.muted);
}
await refreshMembers(chatId);
} catch {
if (!cancelled) setError("Failed to load chat info");
@@ -107,6 +115,27 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
{chat ? (
<>
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs text-slate-400">Notifications</p>
<button
className="rounded bg-slate-700 px-2 py-1 text-xs disabled:opacity-60"
disabled={savingMute}
onClick={async () => {
setSavingMute(true);
try {
const updated = await updateChatNotificationSettings(chatId, !muted);
setMuted(updated.muted);
} catch {
setError("Failed to update notifications");
} finally {
setSavingMute(false);
}
}}
>
{muted ? "Unmute" : "Mute"}
</button>
</div>
<p className="mb-2 text-xs text-slate-300">{muted ? "Chat notifications are muted." : "Chat notifications are enabled."}</p>
<p className="text-xs text-slate-400">Type</p>
<p className="text-sm">{chat.type}</p>
<p className="mt-2 text-xs text-slate-400">Title</p>