From 0e44988634dec807e61ba2bcb1162a82ba34efd0 Mon Sep 17 00:00:00 2001 From: benya Date: Sun, 8 Mar 2026 11:07:30 +0300 Subject: [PATCH] fix(web): notification media preview and theme switching - show media labels instead of raw URLs in browser notifications - support notification icon preview for image messages - implement effective light/dark/system theme application - apply appearance prefs on app startup --- web/src/app/App.tsx | 5 +++ web/src/hooks/useRealtime.ts | 35 ++++++++++++++++++-- web/src/index.css | 62 +++++++++++++++++++++++++++++++++--- web/src/utils/preferences.ts | 15 +++++++-- 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/web/src/app/App.tsx b/web/src/app/App.tsx index ce9c99e..7ca17e9 100644 --- a/web/src/app/App.tsx +++ b/web/src/app/App.tsx @@ -5,6 +5,7 @@ import { AuthPage } from "../pages/AuthPage"; import { ChatsPage } from "../pages/ChatsPage"; import { useAuthStore } from "../store/authStore"; import { useChatStore } from "../store/chatStore"; +import { applyAppearancePreferences, getAppPreferences } from "../utils/preferences"; const PENDING_INVITE_TOKEN_KEY = "bm_pending_invite_token"; @@ -18,6 +19,10 @@ export function App() { const setActiveChatId = useChatStore((s) => s.setActiveChatId); const [joiningInvite, setJoiningInvite] = useState(false); + useEffect(() => { + applyAppearancePreferences(getAppPreferences()); + }, []); + useEffect(() => { if (!accessToken) { return; diff --git a/web/src/hooks/useRealtime.ts b/web/src/hooks/useRealtime.ts index 2e9f857..307aa18 100644 --- a/web/src/hooks/useRealtime.ts +++ b/web/src/hooks/useRealtime.ts @@ -255,9 +255,10 @@ function maybeShowBrowserNotification(chatId: number, message: Message, activeCh return; } const title = chat?.display_title || chat?.title || "New message"; - const body = prefs.messagePreview ? (message.text?.trim() || messagePreviewByType(message.type)) : "New message"; + const preview = buildNotificationPreview(message, prefs.messagePreview); const notification = new Notification(title, { - body, + body: preview.body, + icon: preview.image, tag: `chat-${chatId}`, }); notification.onclick = () => { @@ -278,3 +279,33 @@ function messagePreviewByType(type: Message["type"]): string { if (type === "circle_video") return "Video message"; return "New message"; } + +function buildNotificationPreview( + message: Message, + withPreview: boolean +): { body: string; image?: string } { + if (!withPreview) { + return { body: "New message" }; + } + if (message.type !== "text") { + if (message.type === "image") { + const imageUrl = typeof message.text === "string" && isLikelyUrl(message.text) ? message.text : undefined; + return { body: "🖼 Photo", image: imageUrl }; + } + return { body: messagePreviewByType(message.type) }; + } + const text = message.text?.trim(); + if (!text) { + return { body: "New message" }; + } + return { body: text }; +} + +function isLikelyUrl(value: string): boolean { + try { + const parsed = new URL(value); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } +} diff --git a/web/src/index.css b/web/src/index.css index ada1956..1e3d5f9 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -4,6 +4,16 @@ @tailwind components; @tailwind utilities; +:root { + --bm-font-size: 16px; + --bm-bg-primary: #101e30; + --bm-bg-secondary: #162233; + --bm-bg-tertiary: #19283a; + --bm-text-color: #e5edf9; + --bm-panel-bg: rgba(19, 31, 47, 0.9); + --bm-panel-border: rgba(146, 174, 208, 0.14); +} + html, body, #root { @@ -17,8 +27,8 @@ body { background: radial-gradient(circle at 22% 18%, rgba(36, 68, 117, 0.35), transparent 26%), radial-gradient(circle at 82% 12%, rgba(24, 95, 102, 0.25), transparent 30%), - linear-gradient(180deg, #101e30 0%, #162233 55%, #19283a 100%); - color: #e5edf9; + linear-gradient(180deg, var(--bm-bg-primary) 0%, var(--bm-bg-secondary) 55%, var(--bm-bg-tertiary) 100%); + color: var(--bm-text-color); } * { @@ -26,8 +36,8 @@ body { } .tg-panel { - background: rgba(19, 31, 47, 0.9); - border: 1px solid rgba(146, 174, 208, 0.14); + background: var(--bm-panel-bg); + border: 1px solid var(--bm-panel-border); backdrop-filter: blur(8px); } @@ -46,3 +56,47 @@ body { background: rgba(126, 159, 201, 0.35); border-radius: 999px; } + +html[data-theme="light"] { + --bm-bg-primary: #eef3fb; + --bm-bg-secondary: #f5f8fd; + --bm-bg-tertiary: #ffffff; + --bm-text-color: #0f172a; + --bm-panel-bg: rgba(255, 255, 255, 0.93); + --bm-panel-border: rgba(15, 23, 42, 0.12); +} + +html[data-theme="light"] .tg-chat-wallpaper { + background: + radial-gradient(circle at 14% 18%, rgba(59, 130, 246, 0.08), transparent 30%), + radial-gradient(circle at 86% 74%, rgba(34, 197, 94, 0.06), transparent 33%), + linear-gradient(160deg, rgba(15, 23, 42, 0.01) 0%, rgba(15, 23, 42, 0.02) 100%); +} + +html[data-theme="light"] .bg-slate-900\/95, +html[data-theme="light"] .bg-slate-900\/90, +html[data-theme="light"] .bg-slate-900\/80, +html[data-theme="light"] .bg-slate-900\/70, +html[data-theme="light"] .bg-slate-900\/60, +html[data-theme="light"] .bg-slate-900, +html[data-theme="light"] .bg-slate-800\/80, +html[data-theme="light"] .bg-slate-800\/70, +html[data-theme="light"] .bg-slate-800\/60, +html[data-theme="light"] .bg-slate-800 { + background-color: rgba(255, 255, 255, 0.92) !important; +} + +html[data-theme="light"] .border-slate-700\/80, +html[data-theme="light"] .border-slate-700\/70, +html[data-theme="light"] .border-slate-700\/60, +html[data-theme="light"] .border-slate-700\/50, +html[data-theme="light"] .border-slate-700 { + border-color: rgba(15, 23, 42, 0.14) !important; +} + +html[data-theme="light"] .text-slate-100, +html[data-theme="light"] .text-slate-200, +html[data-theme="light"] .text-slate-300, +html[data-theme="light"] .text-slate-400 { + color: #334155 !important; +} diff --git a/web/src/utils/preferences.ts b/web/src/utils/preferences.ts index 9438052..7d1debd 100644 --- a/web/src/utils/preferences.ts +++ b/web/src/utils/preferences.ts @@ -73,8 +73,20 @@ export function applyAppearancePreferences(prefs: AppPreferences): void { if (typeof document === "undefined") { return; } + const resolvedTheme = resolveTheme(prefs.theme); document.documentElement.style.setProperty("--bm-font-size", `${prefs.messageFontSize}px`); - document.documentElement.setAttribute("data-theme", prefs.theme); + document.documentElement.setAttribute("data-theme", resolvedTheme); + document.documentElement.setAttribute("data-theme-mode", prefs.theme); +} + +function resolveTheme(theme: ThemeMode): "light" | "dark" { + if (theme === "light" || theme === "dark") { + return theme; + } + if (typeof window === "undefined") { + return "dark"; + } + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } function normalizeFontSize(value: number | undefined): number { @@ -84,4 +96,3 @@ function normalizeFontSize(value: number | undefined): number { } return Math.max(12, Math.min(24, Math.round(input))); } -