From 0fb6004a8b12f18885e47160fcb98c17fb2947f1 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 24 Jun 2026 09:20:41 +0200 Subject: [PATCH] feat(telegram): re-apply theme live on themeChanged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Mini App read Telegram's themeParams/colorScheme only once at launch, so switching Telegram's light/dark theme (or its auto day/night) while the app was open left the SPA on stale colours until a relaunch. Subscribe to the themeChanged WebApp event and re-apply the theme live. Extract the launch-time token + colour-scheme + chrome application into syncTelegramTheme (reading live themeParams when no launch snapshot is passed) and call it from both applyTelegramChrome (launch) and the new event handler. Add a telegramThemeParams live getter (+ unit test). Drop the stale 'immersive fullscreen' note from applyTelegramChrome — the app deliberately does not request fullscreen. --- ui/src/lib/app.svelte.ts | 32 +++++++++++++++++++++++--------- ui/src/lib/telegram.test.ts | 7 +++++++ ui/src/lib/telegram.ts | 9 +++++++++ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/ui/src/lib/app.svelte.ts b/ui/src/lib/app.svelte.ts index 962a0dc..5233ae1 100644 --- a/ui/src/lib/app.svelte.ts +++ b/ui/src/lib/app.svelte.ts @@ -9,7 +9,7 @@ import { GatewayError } from './client'; import { navigate, router } from './router.svelte'; import { errorKey, localeFrom, setLocale, t, type Locale } from './i18n/index.svelte'; import { languageNeedsServerSync } from './language'; -import { applyReduceMotion, applyTelegramTheme, applyTheme, type ThemePref } from './theme'; +import { applyReduceMotion, applyTelegramTheme, applyTheme, type ThemePref, type TelegramThemeParams } from './theme'; import { insideTelegram, collectTelegramDiag, @@ -18,6 +18,7 @@ import { hasLaunchFragment, loadTelegramSDK, telegramColorScheme, + telegramThemeParams, telegramContentSafeAreaTop, telegramSafeAreaTop, telegramDisableVerticalSwipes, @@ -554,20 +555,31 @@ function syncViewportHeight(): void { } /** - * applyTelegramChrome applies a Mini App launch's visual integration: Telegram's authoritative - * colour scheme and theme, the matching header / background / bottom chrome, the safe-area insets, - * the swipe-down guard, and immersive fullscreen on mobile. It is idempotent, so both the initial - * bootstrap and a manual launch retry call it. + * syncTelegramTheme re-applies Telegram's theme integration — the themeParams token overrides, + * Telegram's authoritative colour scheme, and the matching chrome — from theme, or from the SDK's + * current themeParams when omitted. Called on launch with the launch snapshot and live on the + * themeChanged event, so switching Telegram's light/dark theme while the app is open is picked up + * without a relaunch. */ -function applyTelegramChrome(launch: TelegramLaunch): void { - if (launch.theme) applyTelegramTheme(launch.theme); +function syncTelegramTheme(theme: TelegramThemeParams | undefined = telegramThemeParams()): void { + if (theme) applyTelegramTheme(theme); // Inside Telegram the colour scheme is Telegram's to decide; force it explicitly so the OS // prefers-color-scheme (which leaks into the Telegram Desktop webview) cannot fight it. Falls // back to the stored preference when the SDK omits it. applyTheme(telegramColorScheme() ?? app.theme); - // Match Telegram's chrome to the app and stop its swipe-down-to-minimise from fighting tile - // drag / board scroll. syncTelegramChrome(); +} + +/** + * applyTelegramChrome applies a Mini App launch's visual integration: Telegram's authoritative + * colour scheme and theme (syncTelegramTheme), the matching header / background / bottom chrome, + * the safe-area insets, and the swipe-down-to-minimise guard. It is idempotent, so both the + * initial bootstrap and a manual launch retry call it. + */ +function applyTelegramChrome(launch: TelegramLaunch): void { + syncTelegramTheme(launch.theme); + // Mirror the safe-area insets and stop Telegram's swipe-down-to-minimise from fighting tile drag + // / board scroll. syncTelegramSafeArea(); telegramDisableVerticalSwipes(); } @@ -625,6 +637,8 @@ export async function bootstrap(): Promise { telegramOnEvent('contentSafeAreaChanged', syncTelegramSafeArea); telegramOnEvent('safeAreaChanged', syncTelegramSafeArea); telegramOnEvent('fullscreenChanged', syncTelegramSafeArea); + // Re-apply the theme live when the user switches Telegram's light/dark mode while the app is open. + telegramOnEvent('themeChanged', () => syncTelegramTheme()); await bootTelegram(launch); app.ready = true; return; diff --git a/ui/src/lib/telegram.test.ts b/ui/src/lib/telegram.test.ts index 7999800..b6f4816 100644 --- a/ui/src/lib/telegram.test.ts +++ b/ui/src/lib/telegram.test.ts @@ -7,6 +7,7 @@ import { routeExternalLinkInTelegram, telegramLaunch, telegramOpenExternalLink, + telegramThemeParams, } from './telegram'; function stubWebApp(initData: string, startParam?: string) { @@ -44,6 +45,12 @@ describe('telegram launch detection', () => { expect(launch.startParam).toBe('g123'); expect(launch.theme?.bg_color).toBe('#101418'); }); + + it('telegramThemeParams reads the live palette (undefined outside Telegram)', () => { + expect(telegramThemeParams()).toBeUndefined(); + stubWebApp('query_id=abc'); + expect(telegramThemeParams()?.bg_color).toBe('#101418'); + }); }); describe('telegramOpenExternalLink', () => { diff --git a/ui/src/lib/telegram.ts b/ui/src/lib/telegram.ts index 3a50c60..f0d85cc 100644 --- a/ui/src/lib/telegram.ts +++ b/ui/src/lib/telegram.ts @@ -243,6 +243,15 @@ export function telegramColorScheme(): 'light' | 'dark' | undefined { return webApp()?.colorScheme; } +/** + * telegramThemeParams returns Telegram's current theme palette (WebApp.themeParams), or undefined + * outside Telegram. It reads the live value rather than a launch snapshot, so the themeChanged + * event can re-apply the palette when the user switches Telegram's light/dark theme mid-session. + */ +export function telegramThemeParams(): TelegramThemeParams | undefined { + return webApp()?.themeParams; +} + /** * telegramSetChrome paints Telegram's own header, background and bottom bar to match the * app's colours, so the surrounding Telegram chrome does not clash with the UI. No-op