feat(telegram): native dialogs for confirms and deep-link notices
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 56s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m19s

Inside the Mini App, route the destructive confirmations (resign, block, unfriend)
through Telegram's native showConfirm, and present the deep-link info modals (stale
invite, welcome-on-redeem) as a native showPopup whose button opens the bot chat.
Outside Telegram, or on a client predating the dialogs, the existing in-app Modal is
used unchanged; offline also keeps the modal so the action retains its disabled state.

Adds showConfirm/showPopup wrappers to telegram.ts and a pure popup-params builder
(nativedialogs.ts) with unit tests; the deep-link modal components choose native vs
in-app via an effect gated on insideTelegram + dialog availability.
This commit is contained in:
Ilia Denisov
2026-06-24 12:36:52 +02:00
parent 29b6c7e4d8
commit 2ba7cc3086
8 changed files with 209 additions and 15 deletions
+12 -2
View File
@@ -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 { telegramHaptic } from '../lib/telegram';
import { insideTelegram, telegramDialogsAvailable, telegramHaptic, telegramShowConfirm } from '../lib/telegram';
import {
BLANK,
newPlacement,
@@ -712,6 +712,16 @@
busy = false;
}
}
// onResignClick: inside the Mini App (and online) confirm with Telegram's native dialog and resign
// on accept; otherwise open the in-app confirm modal (which also carries the offline-disabled action).
async function onResignClick(): Promise<void> {
if (connection.online && insideTelegram() && telegramDialogsAvailable()) {
if (await telegramShowConfirm(t('game.confirmResign'))) doResign();
return;
}
resignOpen = true;
}
async function doResign() {
resignOpen = false;
busy = true;
@@ -1149,7 +1159,7 @@
<button class="hicon" onclick={exportGcg} aria-label={t('game.exportGcg')}>📤</button>
{/if}
{:else}
<button class="hicon" onclick={() => (resignOpen = true)} disabled={waitingForOpponent} aria-label={t('game.dropGame')}>🏁</button>
<button class="hicon" onclick={onResignClick} disabled={waitingForOpponent} aria-label={t('game.dropGame')}>🏁</button>
{/if}
{#if !view.game.multipleWordsPerTurn}<span class="oneword-label">{t('game.oneWordRule')}</span>{/if}
<!-- A finished AI game has no comms at all (no chat, and the dictionary closes with the