feat(ui): drop Telegram fullscreen; own back chevron everywhere; hidden debug panel
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m13s

Finalises the Telegram Mini App navigation work after on-device testing (Pixel
10 / Android 17 + iOS, fresh beta clients):

- Remove requestFullscreen entirely. Immersive fullscreen hid Telegram's native
  header (and its BackButton) and the Android system swipe-back minimised the
  app; the owner prefers the windowed full-size (expand) presentation, so the
  app never requests fullscreen on any platform now.
- The app's own back chevron (Header, showBack = !!back) drives back-navigation
  on every platform; the native Telegram BackButton is dropped — it does not
  render in the windowed Mini App (backVisible=false on iOS and Android), so
  relying on it lost back navigation (notably none on iOS).
- Replace the temporary always-on diagnostic overlay with a hidden debug panel
  (components/DebugPanel): ten quick taps on the header title open it; it shows a
  privacy-safe client diagnostic snapshot (app version, locale, online, userId,
  Telegram chrome / viewport / SDK state — no secrets, no IP) and shares it via
  the OS share sheet / clipboard; a tap anywhere except Share dismisses it.
- Drop the now-dead telegramRequestFullscreen / telegramBackButton /
  isTelegramAndroid helpers and the iOS-fullscreen unit test.

Telegram has no native non-modal notification API (only modal showPopup /
showAlert), so in-app toasts stay ours. Docs: UI_DESIGN.md.
This commit is contained in:
Ilia Denisov
2026-06-23 15:05:13 +02:00
parent 53d6883ffd
commit 37070c3cb7
7 changed files with 120 additions and 114 deletions
+14 -5
View File
@@ -25,7 +25,6 @@ import {
telegramLaunch,
type TelegramLaunch,
telegramOnEvent,
telegramRequestFullscreen,
telegramSetChrome,
} from './telegram';
import { parseStartParam } from './deeplink';
@@ -56,6 +55,9 @@ export const app = $state<{
* launch-error screen (screens/TelegramLaunchError) — a shareable probe for why Telegram
* delivered no initData (seen on some Android clients) — instead of bouncing to the landing. */
launchError: TelegramDiag | null;
/** Whether the hidden on-device debug panel (components/DebugPanel) is open — toggled by tapping
* the header title ten times in quick succession. A support aid; carries no secrets. */
debugOpen: boolean;
/** Whether the lobby's first cold load has settled (success or error). The loading splash
* (components/Splash.svelte) watches it to know when to dismiss; set by screens/Lobby. */
lobbyReady: boolean;
@@ -106,6 +108,7 @@ export const app = $state<{
ready: false,
bootError: false,
launchError: null,
debugOpen: false,
lobbyReady: false,
splashDone: false,
streamAlive: false,
@@ -197,6 +200,16 @@ export function dismissWelcomeRedeem(): void {
app.welcomeRedeem = false;
}
/** openDebug / closeDebug toggle the hidden on-device debug panel (components/DebugPanel), opened
* by tapping the header title ten times — a support aid that shows and shares client diagnostics. */
export function openDebug(): void {
app.debugOpen = true;
}
export function closeDebug(): void {
app.debugOpen = false;
}
/**
* seedChatUnread sets a game's unread flags from an authoritative per-viewer REST view (the lobby
* list, a game's state, or a move result): unread is any unread entry, message whether one of them
@@ -557,10 +570,6 @@ function applyTelegramChrome(launch: TelegramLaunch): void {
syncTelegramChrome();
syncTelegramSafeArea();
telegramDisableVerticalSwipes();
// On mobile, go immersive fullscreen like Telegram's own Mini Apps; the fullscreenChanged
// listener (registered at bootstrap) then re-syncs the safe-area insets. Desktop keeps the bot's
// full-size window. No-op on clients predating Bot API 8.0.
telegramRequestFullscreen();
}
/** How long to wait for the dynamically loaded Telegram Mini App SDK before giving up and showing
-39
View File
@@ -7,7 +7,6 @@ import {
routeExternalLinkInTelegram,
telegramLaunch,
telegramOpenExternalLink,
telegramRequestFullscreen,
} from './telegram';
function stubWebApp(initData: string, startParam?: string) {
@@ -47,44 +46,6 @@ describe('telegram launch detection', () => {
});
});
// stubClient stands up a fake WebApp on the given platform with a requestFullscreen spy, so the
// platform gate can be asserted without a real Telegram client.
function stubClient(platform?: string) {
const requestFullscreen = vi.fn();
vi.stubGlobal('window', { Telegram: { WebApp: { platform, requestFullscreen } } });
return { requestFullscreen };
}
const desktopPlatforms = ['tdesktop', 'macos', 'web', undefined];
describe('telegramRequestFullscreen', () => {
afterEach(() => vi.unstubAllGlobals());
// TEMP: fullscreen is disabled on all platforms for an owner test (telegramRequestFullscreen is a
// no-op); restore this (toHaveBeenCalledOnce) and un-skip when the iOS path is restored.
it.skip('goes immersive fullscreen on iOS', () => {
const { requestFullscreen } = stubClient('ios');
telegramRequestFullscreen();
expect(requestFullscreen).toHaveBeenCalledOnce();
});
it('stays windowed on Android, where fullscreen hides the native back button', () => {
for (const p of ['android', 'android_x']) {
const { requestFullscreen } = stubClient(p);
telegramRequestFullscreen();
expect(requestFullscreen, `platform=${p}`).not.toHaveBeenCalled();
}
});
it('leaves desktop clients as a standard window', () => {
for (const p of desktopPlatforms) {
const { requestFullscreen } = stubClient(p);
telegramRequestFullscreen();
expect(requestFullscreen, `platform=${p}`).not.toHaveBeenCalled();
}
});
});
describe('telegramOpenExternalLink', () => {
afterEach(() => vi.unstubAllGlobals());
+5 -53
View File
@@ -22,7 +22,6 @@ interface TelegramWebApp {
contentSafeAreaInset?: { top: number; bottom: number; left: number; right: number };
ready?: () => void;
expand?: () => void;
requestFullscreen?: () => void;
openTelegramLink?: (url: string) => void;
openLink?: (url: string) => void;
onEvent?: (event: string, handler: () => void) => void;
@@ -295,54 +294,6 @@ export function telegramHaptic(kind: Haptic): void {
else h.impactOccurred?.(kind);
}
/**
* telegramRequestFullscreen asks Telegram to open the Mini App in immersive fullscreen (Bot API
* 8.0+), but only on iOS. On Android, fullscreen replaces the native header — and its BackButton —
* with a bare close/menu pill: the back control disappears and the Android system swipe-back falls
* through to minimising the app instead of navigating, so the app stays windowed there to keep the
* native header + BackButton (which also captures the system back). A no-op outside Telegram, on
* Android, on desktop, or on clients predating the method.
*/
export function telegramRequestFullscreen(): void {
// TEMP (owner test): fullscreen fully disabled, incl. iOS, to confirm the Android "fullscreen
// look" is Telegram's own Mini App presentation, not our requestFullscreen (isFullscreen is
// already false on Android). Restore the iOS path after the test:
// if (webApp()?.platform === 'ios') webApp()?.requestFullscreen?.();
}
/** isTelegramAndroid reports whether the Mini App runs on a Telegram Android client (reported as
* 'android', or 'android_x' for Telegram X). The native header BackButton does not render in the
* Android (non-fullscreen) presentation, so back navigation there uses the app's own chevron. */
export function isTelegramAndroid(): boolean {
const p = webApp()?.platform;
return p === 'android' || p === 'android_x';
}
let backHandler: (() => void) | null = null;
let lastBackShow = false; // TEMP diag: the last telegramBackButton(show) request
/**
* telegramBackButton shows or hides Telegram's native header back button, wiring its
* click to onClick (replacing any previous handler). The app hides its own back chevron
* inside Telegram so only the native control shows.
*/
export function telegramBackButton(show: boolean, onClick?: () => void): void {
lastBackShow = show;
const b = webApp()?.BackButton;
if (!b) return;
if (backHandler) b.offClick?.(backHandler);
backHandler = null;
if (show) {
if (onClick) {
backHandler = onClick;
b.onClick?.(onClick);
}
b.show?.();
} else {
b.hide?.();
}
}
/**
* startParamFromURL reads a startapp parameter from the page URL — a bot web_app
* launch button carries the deep-link there rather than in initDataUnsafe.
@@ -488,9 +439,10 @@ export function collectTelegramDiag(): TelegramDiag {
}
/**
* telegramChromeDiag is a TEMPORARY on-device readout of Telegram's viewport / fullscreen state,
* rendered in the lobby to debug the Android fullscreen-persistence issue (the app still opens
* fullscreen on Android even though requestFullscreen is now iOS-only). REMOVE before merge.
* telegramChromeDiag returns a compact, privacy-safe readout of Telegram's viewport / chrome state
* (platform, version, fullscreen/expanded, viewport geometry, safe-area insets, SDK-load outcome,
* back-button state, UA). It feeds the hidden debug panel (components/DebugPanel), opened by tapping
* the header title ten times. No secrets: no initData values, no IP.
*/
export function telegramChromeDiag(): string {
const w = webApp();
@@ -505,7 +457,7 @@ export function telegramChromeDiag(): string {
return [
`platform: ${w.platform ?? '—'} version: ${w.version ?? '—'} scheme: ${w.colorScheme ?? '—'}`,
`isFullscreen: ${w.isFullscreen} isExpanded: ${w.isExpanded}`,
`inTG: ${insideTelegram()} backReq: ${lastBackShow} backPresent: ${!!w.BackButton} backVisible: ${w.BackButton?.isVisible}`,
`inTG: ${insideTelegram()} sdkLoad: ${sdkLoadOutcome} backPresent: ${!!w.BackButton} backVisible: ${w.BackButton?.isVisible}`,
`innerH: ${n(win?.innerHeight)} outerH: ${n(win?.outerHeight)} screenH: ${n(scr?.height)} availH: ${n(scr?.availHeight)}`,
`screenY: ${n(win?.screenY)} vv.offTop: ${n(vv?.offsetTop)} vv.h: ${n(vv?.height)}`,
`tgViewportH: ${n(w.viewportHeight)} stableH: ${n(w.viewportStableHeight)}`,