feat: mentions badge in chat list and muted-mention delivery
All checks were successful
CI / test (push) Successful in 21s

This commit is contained in:
2026-03-08 12:23:39 +03:00
parent fc7a9cc3a6
commit 0594b890c3
9 changed files with 92 additions and 75 deletions

View File

@@ -3,7 +3,6 @@ import { ChatList } from "../components/ChatList";
import { ChatInfoPanel } from "../components/ChatInfoPanel";
import { MessageComposer } from "../components/MessageComposer";
import { MessageList } from "../components/MessageList";
import { getNotifications, type NotificationItem } from "../api/notifications";
import { searchMessages } from "../api/chats";
import type { Message } from "../chat/types";
import { useRealtime } from "../hooks/useRealtime";
@@ -14,7 +13,6 @@ import { useState } from "react";
export function ChatsPage() {
const me = useAuthStore((s) => s.me);
const logout = useAuthStore((s) => s.logout);
const loadChats = useChatStore((s) => s.loadChats);
const activeChatId = useChatStore((s) => s.activeChatId);
const chats = useChatStore((s) => s.chats);
@@ -29,9 +27,6 @@ export function ChatsPage() {
const [searchLoading, setSearchLoading] = useState(false);
const [searchResults, setSearchResults] = useState<Message[]>([]);
const [searchActiveIndex, setSearchActiveIndex] = useState(0);
const [notificationsOpen, setNotificationsOpen] = useState(false);
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [loadingNotifications, setLoadingNotifications] = useState(false);
const searchInputRef = useRef<HTMLInputElement | null>(null);
const activeTrack = useAudioPlayerStore((s) => s.track);
const isAudioPlaying = useAudioPlayerStore((s) => s.isPlaying);
@@ -139,33 +134,6 @@ export function ChatsPage() {
setFocusedMessage(current.chat_id, current.id);
}, [searchOpen, searchActiveIndex, searchResults, setFocusedMessage]);
useEffect(() => {
if (!notificationsOpen) {
return;
}
let cancelled = false;
setLoadingNotifications(true);
void (async () => {
try {
const items = await getNotifications(30);
if (!cancelled) {
setNotifications(items);
}
} catch {
if (!cancelled) {
setNotifications([]);
}
} finally {
if (!cancelled) {
setLoadingNotifications(false);
}
}
})();
return () => {
cancelled = true;
};
}, [notificationsOpen]);
return (
<main className="h-screen w-full p-2 text-text md:p-3">
<div className="mx-auto flex h-full max-w-[1500px] gap-2 md:gap-3">
@@ -192,15 +160,6 @@ export function ChatsPage() {
</div>
</div>
<div className="flex items-center gap-1.5">
<button
className="relative rounded-full bg-slate-700/70 px-2.5 py-1.5 text-xs font-semibold hover:bg-slate-600/80"
onClick={() => setNotificationsOpen(true)}
>
Notifications
{notifications.length > 0 ? (
<span className="ml-1 rounded-full bg-sky-500 px-1.5 py-0.5 text-[10px] text-slate-950">{notifications.length}</span>
) : null}
</button>
<button
className="rounded-full bg-slate-700/70 px-2.5 py-1.5 text-xs font-semibold hover:bg-slate-600/80"
onClick={() => {
@@ -212,9 +171,6 @@ export function ChatsPage() {
>
Search
</button>
<button className="rounded-full bg-slate-700/70 px-2.5 py-1.5 text-xs font-semibold hover:bg-slate-600/80" onClick={logout}>
Logout
</button>
</div>
</>
) : (
@@ -311,26 +267,6 @@ export function ChatsPage() {
</section>
</div>
<ChatInfoPanel chatId={activeChatId} open={infoOpen} onClose={() => setInfoOpen(false)} />
{notificationsOpen ? (
<div className="fixed inset-0 z-[130] flex items-start justify-center bg-slate-950/60 p-3 md:p-6" onClick={() => setNotificationsOpen(false)}>
<div className="w-full max-w-xl rounded-2xl border border-slate-700/80 bg-slate-900 p-3 shadow-2xl" onClick={(e) => e.stopPropagation()}>
<div className="mb-2 flex items-center justify-between">
<p className="text-sm font-semibold">Notifications</p>
<button className="rounded bg-slate-700 px-2 py-1 text-xs" onClick={() => setNotificationsOpen(false)}>Close</button>
</div>
{loadingNotifications ? <p className="px-2 py-1 text-xs text-slate-400">Loading...</p> : null}
{!loadingNotifications && notifications.length === 0 ? <p className="px-2 py-1 text-xs text-slate-400">No notifications</p> : null}
<div className="tg-scrollbar max-h-[50vh] space-y-1 overflow-auto">
{notifications.map((item) => (
<div className="rounded-lg bg-slate-800/80 px-3 py-2" key={item.id}>
<p className="text-xs font-semibold text-slate-200">{item.event_type}</p>
<p className="text-[11px] text-slate-400">{new Date(item.created_at).toLocaleString()}</p>
</div>
))}
</div>
</div>
</div>
) : null}
</main>
);
}