fix(ui): Android Telegram nav — windowed mode, no close guard
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 1m18s
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 1m18s
Three Android Mini App issues, all in the Telegram chrome: - Entering a game showed no native back button, and the Android system swipe-back minimised the app instead of navigating. Root cause: immersive fullscreen (requestFullscreen). On Android, fullscreen replaces the native header — and its BackButton, which also captures the system back — with a bare close/menu pill, so back navigation has no control to land on. Request fullscreen on iOS only; Android stays windowed, keeping the native header + BackButton (and the system swipe-back that routes to it). iOS is unchanged. - Closing the game board always prompted "changes that you made may not be saved", even on a board just opened and untouched. The close-confirmation was armed unconditionally on game mount. Remove it entirely: move drafts auto-save (debounced during play + flushed on destroy), so nothing is lost on close. Drops the now-unused telegramClosingConfirmation + isMobilePlatform helpers and their SDK interface fields. Docs: UI_DESIGN.md.
This commit is contained in:
+6
-6
@@ -98,12 +98,12 @@ dismisses as soon as the lobby is ready. The pure layout and timing live in `lib
|
||||
theme switcher is hidden; the nav bar takes Telegram's background and `setHeaderColor` /
|
||||
`setBackgroundColor` / `setBottomBarColor` paint Telegram's own chrome to match; the
|
||||
native header **BackButton** drives back-navigation (the app's chevron is hidden in
|
||||
Telegram); **HapticFeedback** fires on tile placement / commit / error; on **mobile**
|
||||
clients the app enters **immersive fullscreen** on launch (`requestFullscreen`, Bot API
|
||||
8.0+) like Telegram's own Mini Apps, while desktop keeps the bot's full-size window;
|
||||
**closing confirmation** is enabled while a game is open **on mobile only** (on desktop
|
||||
closing is deliberate and the "changes may not be saved" dialog is just noise — move drafts
|
||||
auto-save); **vertical swipes** (swipe-to-minimise)
|
||||
Telegram); **HapticFeedback** fires on tile placement / commit / error; on **iOS** the app
|
||||
enters **immersive fullscreen** on launch (`requestFullscreen`, Bot API 8.0+) like Telegram's
|
||||
own Mini Apps, while **Android stays windowed** — there fullscreen hides the native header and
|
||||
its BackButton, and the Android system swipe-back then minimises the app instead of navigating —
|
||||
and desktop keeps the bot's full-size window; move drafts auto-save, so there is **no
|
||||
closing-confirmation guard**; **vertical swipes** (swipe-to-minimise)
|
||||
are disabled so they don't fight tile drag or the board scroll; **external links** (the word-check
|
||||
dictionary lookup, the rules link, operator-reply links) open through `Telegram.WebApp.openLink`
|
||||
so Telegram shows them in its in-app browser instead of the WebView's "open this link?"
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
import { getCachedGame, setCachedGame, setCachedDraft, type CachedGame } from '../lib/gamecache';
|
||||
import { patchLobbyGame } from '../lib/lobbycache';
|
||||
import { applyGameOver, applyMoveDelta, applyOpponentJoined, type DeltaResult } from '../lib/gamedelta';
|
||||
import { telegramClosingConfirmation, telegramHaptic } from '../lib/telegram';
|
||||
import { telegramHaptic } from '../lib/telegram';
|
||||
import {
|
||||
BLANK,
|
||||
newPlacement,
|
||||
@@ -228,8 +228,6 @@
|
||||
placement = tiles.length ? placementFromHint(tiles, rack) : newPlacement(rack);
|
||||
}
|
||||
onMount(() => {
|
||||
// Guard against an accidental swipe-close losing the open game (Telegram).
|
||||
telegramClosingConfirmation(true);
|
||||
// Render instantly from the cache (a game opened before), then refresh in the
|
||||
// background. A cold open shows the loading state until load() resolves.
|
||||
const cached = getCachedGame(id);
|
||||
@@ -579,7 +577,6 @@
|
||||
clearTimeout(draftSaveTimer);
|
||||
void gateway.draftSave(id, serializeDraft(rackIds, placement.pending)).catch(() => {});
|
||||
}
|
||||
telegramClosingConfirmation(false);
|
||||
});
|
||||
|
||||
function onCell(row: number, col: number) {
|
||||
|
||||
+14
-42
@@ -5,7 +5,6 @@ import {
|
||||
loadTelegramSDK,
|
||||
telegramSdkOutcome,
|
||||
routeExternalLinkInTelegram,
|
||||
telegramClosingConfirmation,
|
||||
telegramLaunch,
|
||||
telegramOpenExternalLink,
|
||||
telegramRequestFullscreen,
|
||||
@@ -48,61 +47,34 @@ describe('telegram launch detection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// stubClient stands up a fake WebApp on the given platform with spies for the mobile-gated
|
||||
// chrome toggles, so the platform gate can be asserted without a real Telegram client.
|
||||
// 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 enable = vi.fn();
|
||||
const disable = vi.fn();
|
||||
const requestFullscreen = vi.fn();
|
||||
vi.stubGlobal('window', {
|
||||
Telegram: {
|
||||
WebApp: { platform, enableClosingConfirmation: enable, disableClosingConfirmation: disable, requestFullscreen },
|
||||
},
|
||||
});
|
||||
return { enable, disable, requestFullscreen };
|
||||
vi.stubGlobal('window', { Telegram: { WebApp: { platform, requestFullscreen } } });
|
||||
return { requestFullscreen };
|
||||
}
|
||||
|
||||
const mobilePlatforms = ['ios', 'android', 'android_x'];
|
||||
const desktopPlatforms = ['tdesktop', 'macos', 'web', undefined];
|
||||
|
||||
describe('telegramClosingConfirmation', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it('arms the close guard on mobile clients', () => {
|
||||
for (const p of mobilePlatforms) {
|
||||
const { enable } = stubClient(p);
|
||||
telegramClosingConfirmation(true);
|
||||
expect(enable, `platform=${p}`).toHaveBeenCalledOnce();
|
||||
}
|
||||
});
|
||||
|
||||
it('skips the close guard on desktop clients (the dialog there is just noise)', () => {
|
||||
for (const p of desktopPlatforms) {
|
||||
const { enable } = stubClient(p);
|
||||
telegramClosingConfirmation(true);
|
||||
expect(enable, `platform=${p}`).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('always lifts the guard on leave, regardless of platform', () => {
|
||||
const { disable } = stubClient('tdesktop');
|
||||
telegramClosingConfirmation(false);
|
||||
expect(disable).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
describe('telegramRequestFullscreen', () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it('goes immersive fullscreen on mobile clients', () => {
|
||||
for (const p of mobilePlatforms) {
|
||||
it('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}`).toHaveBeenCalledOnce();
|
||||
expect(requestFullscreen, `platform=${p}`).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('leaves desktop clients as a standard window (the bot full-size setting fills it)', () => {
|
||||
it('leaves desktop clients as a standard window', () => {
|
||||
for (const p of desktopPlatforms) {
|
||||
const { requestFullscreen } = stubClient(p);
|
||||
telegramRequestFullscreen();
|
||||
|
||||
+7
-34
@@ -26,8 +26,6 @@ interface TelegramWebApp {
|
||||
setBackgroundColor?: (color: string) => void;
|
||||
setBottomBarColor?: (color: string) => void;
|
||||
disableVerticalSwipes?: () => void;
|
||||
enableClosingConfirmation?: () => void;
|
||||
disableClosingConfirmation?: () => void;
|
||||
HapticFeedback?: {
|
||||
impactOccurred?: (style: string) => void;
|
||||
notificationOccurred?: (type: string) => void;
|
||||
@@ -293,40 +291,15 @@ export function telegramHaptic(kind: Haptic): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* isMobilePlatform reports whether the Mini App runs on a Telegram mobile client — iOS or
|
||||
* Android (the latter reported as 'android' by Telegram for Android and 'android_x' by
|
||||
* Telegram X). Desktop clients (tdesktop, macOS, web) report other values. Used to limit
|
||||
* mobile-only chrome such as the close guard and immersive fullscreen.
|
||||
*/
|
||||
function isMobilePlatform(): boolean {
|
||||
const p = webApp()?.platform;
|
||||
return p === 'ios' || p === 'android' || p === 'android_x';
|
||||
}
|
||||
|
||||
/**
|
||||
* telegramRequestFullscreen asks Telegram to open the Mini App in fullscreen (Bot API 8.0+),
|
||||
* but only on mobile clients — mirroring how Telegram's own Mini Apps go immersive on phones
|
||||
* while staying a standard window on desktop (where the bot's full-size setting already fills
|
||||
* the window). A no-op outside Telegram, on desktop, or on clients predating the method.
|
||||
* 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 {
|
||||
if (isMobilePlatform()) webApp()?.requestFullscreen?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* telegramClosingConfirmation toggles the confirmation Telegram shows when the user swipes
|
||||
* the Mini App closed — enabled during an active game so it is not lost by accident. The
|
||||
* guard is only armed on mobile clients: on desktop, closing a window is deliberate and
|
||||
* Telegram surfaces a "changes may not be saved" dialog that is just noise here (drafts
|
||||
* auto-save), so the confirmation is skipped there.
|
||||
*/
|
||||
export function telegramClosingConfirmation(on: boolean): void {
|
||||
const w = webApp();
|
||||
if (on) {
|
||||
if (isMobilePlatform()) w?.enableClosingConfirmation?.();
|
||||
} else {
|
||||
w?.disableClosingConfirmation?.();
|
||||
}
|
||||
if (webApp()?.platform === 'ios') webApp()?.requestFullscreen?.();
|
||||
}
|
||||
|
||||
let backHandler: (() => void) | null = null;
|
||||
|
||||
Reference in New Issue
Block a user