feat(web): add notifications history block in settings
All checks were successful
CI / test (push) Successful in 23s

This commit is contained in:
2026-03-08 13:33:50 +03:00
parent 10b11b065f
commit cf1a77ae76

View File

@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
import QRCode from "qrcode"; import QRCode from "qrcode";
import { listBlockedUsers, updateMyProfile } from "../api/users"; import { listBlockedUsers, updateMyProfile } from "../api/users";
import { disableTwoFactor, enableTwoFactor, listSessions, revokeAllSessions, revokeSession, setupTwoFactor } from "../api/auth"; import { disableTwoFactor, enableTwoFactor, listSessions, revokeAllSessions, revokeSession, setupTwoFactor } from "../api/auth";
import { getNotifications, type NotificationItem } from "../api/notifications";
import type { AuthSession, AuthUser } from "../chat/types"; import type { AuthSession, AuthUser } from "../chat/types";
import { useAuthStore } from "../store/authStore"; import { useAuthStore } from "../store/authStore";
import { AppPreferences, getAppPreferences, updateAppPreferences } from "../utils/preferences"; import { AppPreferences, getAppPreferences, updateAppPreferences } from "../utils/preferences";
@@ -32,6 +33,8 @@ export function SettingsPanel({ open, onClose }: Props) {
const [privacyLastSeen, setPrivacyLastSeen] = useState<"everyone" | "contacts" | "nobody">("everyone"); const [privacyLastSeen, setPrivacyLastSeen] = useState<"everyone" | "contacts" | "nobody">("everyone");
const [privacyAvatar, setPrivacyAvatar] = useState<"everyone" | "contacts" | "nobody">("everyone"); const [privacyAvatar, setPrivacyAvatar] = useState<"everyone" | "contacts" | "nobody">("everyone");
const [privacyGroupInvites, setPrivacyGroupInvites] = useState<"everyone" | "contacts">("everyone"); const [privacyGroupInvites, setPrivacyGroupInvites] = useState<"everyone" | "contacts">("everyone");
const [notificationItems, setNotificationItems] = useState<NotificationItem[]>([]);
const [notificationItemsLoading, setNotificationItemsLoading] = useState(false);
const [profileDraft, setProfileDraft] = useState({ const [profileDraft, setProfileDraft] = useState({
name: "", name: "",
username: "", username: "",
@@ -107,6 +110,33 @@ export function SettingsPanel({ open, onClose }: Props) {
}; };
}, [open, page]); }, [open, page]);
useEffect(() => {
if (!open || page !== "notifications") {
return;
}
let cancelled = false;
setNotificationItemsLoading(true);
void (async () => {
try {
const rows = await getNotifications(30);
if (!cancelled) {
setNotificationItems(rows);
}
} catch {
if (!cancelled) {
setNotificationItems([]);
}
} finally {
if (!cancelled) {
setNotificationItemsLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, [open, page]);
useEffect(() => { useEffect(() => {
if (!open || page !== "privacy") { if (!open || page !== "privacy") {
return; return;
@@ -292,6 +322,37 @@ export function SettingsPanel({ open, onClose }: Props) {
<CheckboxOption checked={prefs.groupNotifications} label="Notifications for groups" onChange={(checked) => updatePrefs({ groupNotifications: checked })} /> <CheckboxOption checked={prefs.groupNotifications} label="Notifications for groups" onChange={(checked) => updatePrefs({ groupNotifications: checked })} />
<CheckboxOption checked={prefs.channelNotifications} label="Notifications for channels" onChange={(checked) => updatePrefs({ channelNotifications: checked })} /> <CheckboxOption checked={prefs.channelNotifications} label="Notifications for channels" onChange={(checked) => updatePrefs({ channelNotifications: checked })} />
</section> </section>
<section className="rounded border border-slate-700/70 bg-slate-800/50 p-3">
<div className="mb-2 flex items-center justify-between">
<p className="text-xs uppercase tracking-wide text-slate-400">Recent Notifications</p>
<button
className="rounded bg-slate-700 px-2 py-1 text-[11px]"
onClick={async () => {
setNotificationItemsLoading(true);
try {
setNotificationItems(await getNotifications(30));
} finally {
setNotificationItemsLoading(false);
}
}}
type="button"
>
Refresh
</button>
</div>
{notificationItemsLoading ? <p className="text-xs text-slate-400">Loading...</p> : null}
{!notificationItemsLoading && notificationItems.length === 0 ? <p className="text-xs text-slate-400">No notifications yet</p> : null}
<div className="space-y-2">
{notificationItems.slice(0, 20).map((item) => (
<div className="rounded bg-slate-900/50 px-2 py-2" key={item.id}>
<p className="text-xs font-semibold text-slate-200">{item.event_type}</p>
<p className="mt-0.5 line-clamp-2 text-[11px] text-slate-400">{renderNotificationPayload(item.payload)}</p>
<p className="mt-0.5 text-[11px] text-slate-500">{new Date(item.created_at).toLocaleString()}</p>
</div>
))}
</div>
</section>
</div> </div>
) : null} ) : null}
@@ -495,6 +556,15 @@ export function SettingsPanel({ open, onClose }: Props) {
); );
} }
function renderNotificationPayload(payload: string): string {
try {
const parsed = JSON.parse(payload) as { text?: string; title?: string; body?: string; chat_id?: number };
return parsed.text || parsed.body || parsed.title || (parsed.chat_id ? `Chat #${parsed.chat_id}` : payload);
} catch {
return payload;
}
}
function SettingsRow({ label, value, onClick }: { label: string; value: string; onClick: () => void }) { function SettingsRow({ label, value, onClick }: { label: string; value: string; onClick: () => void }) {
return ( return (
<button className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-slate-800/60" onClick={onClick} type="button"> <button className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-slate-800/60" onClick={onClick} type="button">