feat: realtime sync, settings UX and chat list improvements
Some checks failed
CI / test (push) Failing after 21s

- add chat_updated realtime event and dynamic chat subscriptions

- auto-join invite links in web app

- implement Telegram-like settings panel (general/notifications/privacy)

- add browser notification preferences and keyboard send mode

- improve chat list with last message preview/time and online badge

- rework chat members UI with context actions and role crowns
This commit is contained in:
2026-03-08 10:59:44 +03:00
parent a4fa72df30
commit 99e7c70901
18 changed files with 1007 additions and 78 deletions

View File

@@ -0,0 +1,87 @@
export type ThemeMode = "light" | "dark" | "system";
export type SendMode = "enter" | "ctrl_enter";
export interface AppPreferences {
theme: ThemeMode;
messageFontSize: number;
sendMode: SendMode;
webNotifications: boolean;
privateNotifications: boolean;
groupNotifications: boolean;
channelNotifications: boolean;
messagePreview: boolean;
}
const STORAGE_KEY = "bm_preferences_v1";
const DEFAULTS: AppPreferences = {
theme: "system",
messageFontSize: 16,
sendMode: "enter",
webNotifications: true,
privateNotifications: true,
groupNotifications: true,
channelNotifications: true,
messagePreview: true,
};
export function getAppPreferences(): AppPreferences {
if (typeof window === "undefined") {
return DEFAULTS;
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) {
return DEFAULTS;
}
const parsed = JSON.parse(raw) as Partial<AppPreferences>;
return {
theme: parsed.theme === "light" || parsed.theme === "dark" || parsed.theme === "system" ? parsed.theme : DEFAULTS.theme,
messageFontSize: normalizeFontSize(parsed.messageFontSize),
sendMode: parsed.sendMode === "ctrl_enter" ? "ctrl_enter" : "enter",
webNotifications: typeof parsed.webNotifications === "boolean" ? parsed.webNotifications : DEFAULTS.webNotifications,
privateNotifications: typeof parsed.privateNotifications === "boolean" ? parsed.privateNotifications : DEFAULTS.privateNotifications,
groupNotifications: typeof parsed.groupNotifications === "boolean" ? parsed.groupNotifications : DEFAULTS.groupNotifications,
channelNotifications: typeof parsed.channelNotifications === "boolean" ? parsed.channelNotifications : DEFAULTS.channelNotifications,
messagePreview: typeof parsed.messagePreview === "boolean" ? parsed.messagePreview : DEFAULTS.messagePreview,
};
} catch {
return DEFAULTS;
}
}
export function saveAppPreferences(next: AppPreferences): void {
if (typeof window === "undefined") {
return;
}
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
}
export function updateAppPreferences(patch: Partial<AppPreferences>): AppPreferences {
const current = getAppPreferences();
const next: AppPreferences = {
...current,
...patch,
messageFontSize: normalizeFontSize((patch.messageFontSize ?? current.messageFontSize)),
};
saveAppPreferences(next);
applyAppearancePreferences(next);
return next;
}
export function applyAppearancePreferences(prefs: AppPreferences): void {
if (typeof document === "undefined") {
return;
}
document.documentElement.style.setProperty("--bm-font-size", `${prefs.messageFontSize}px`);
document.documentElement.setAttribute("data-theme", prefs.theme);
}
function normalizeFontSize(value: number | undefined): number {
const input = Number(value);
if (!Number.isFinite(input)) {
return DEFAULTS.messageFontSize;
}
return Math.max(12, Math.min(24, Math.round(input)));
}