fix(web): notification media preview and theme switching
Some checks failed
CI / test (push) Failing after 19s
Some checks failed
CI / test (push) Failing after 19s
- 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
This commit is contained in:
@@ -5,6 +5,7 @@ import { AuthPage } from "../pages/AuthPage";
|
|||||||
import { ChatsPage } from "../pages/ChatsPage";
|
import { ChatsPage } from "../pages/ChatsPage";
|
||||||
import { useAuthStore } from "../store/authStore";
|
import { useAuthStore } from "../store/authStore";
|
||||||
import { useChatStore } from "../store/chatStore";
|
import { useChatStore } from "../store/chatStore";
|
||||||
|
import { applyAppearancePreferences, getAppPreferences } from "../utils/preferences";
|
||||||
|
|
||||||
const PENDING_INVITE_TOKEN_KEY = "bm_pending_invite_token";
|
const PENDING_INVITE_TOKEN_KEY = "bm_pending_invite_token";
|
||||||
|
|
||||||
@@ -18,6 +19,10 @@ export function App() {
|
|||||||
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
|
const setActiveChatId = useChatStore((s) => s.setActiveChatId);
|
||||||
const [joiningInvite, setJoiningInvite] = useState(false);
|
const [joiningInvite, setJoiningInvite] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyAppearancePreferences(getAppPreferences());
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -255,9 +255,10 @@ function maybeShowBrowserNotification(chatId: number, message: Message, activeCh
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const title = chat?.display_title || chat?.title || "New message";
|
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, {
|
const notification = new Notification(title, {
|
||||||
body,
|
body: preview.body,
|
||||||
|
icon: preview.image,
|
||||||
tag: `chat-${chatId}`,
|
tag: `chat-${chatId}`,
|
||||||
});
|
});
|
||||||
notification.onclick = () => {
|
notification.onclick = () => {
|
||||||
@@ -278,3 +279,33 @@ function messagePreviewByType(type: Message["type"]): string {
|
|||||||
if (type === "circle_video") return "Video message";
|
if (type === "circle_video") return "Video message";
|
||||||
return "New 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@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,
|
html,
|
||||||
body,
|
body,
|
||||||
#root {
|
#root {
|
||||||
@@ -17,8 +27,8 @@ body {
|
|||||||
background:
|
background:
|
||||||
radial-gradient(circle at 22% 18%, rgba(36, 68, 117, 0.35), transparent 26%),
|
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%),
|
radial-gradient(circle at 82% 12%, rgba(24, 95, 102, 0.25), transparent 30%),
|
||||||
linear-gradient(180deg, #101e30 0%, #162233 55%, #19283a 100%);
|
linear-gradient(180deg, var(--bm-bg-primary) 0%, var(--bm-bg-secondary) 55%, var(--bm-bg-tertiary) 100%);
|
||||||
color: #e5edf9;
|
color: var(--bm-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -26,8 +36,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tg-panel {
|
.tg-panel {
|
||||||
background: rgba(19, 31, 47, 0.9);
|
background: var(--bm-panel-bg);
|
||||||
border: 1px solid rgba(146, 174, 208, 0.14);
|
border: 1px solid var(--bm-panel-border);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,3 +56,47 @@ body {
|
|||||||
background: rgba(126, 159, 201, 0.35);
|
background: rgba(126, 159, 201, 0.35);
|
||||||
border-radius: 999px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,8 +73,20 @@ export function applyAppearancePreferences(prefs: AppPreferences): void {
|
|||||||
if (typeof document === "undefined") {
|
if (typeof document === "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const resolvedTheme = resolveTheme(prefs.theme);
|
||||||
document.documentElement.style.setProperty("--bm-font-size", `${prefs.messageFontSize}px`);
|
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 {
|
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)));
|
return Math.max(12, Math.min(24, Math.round(input)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user