From 2ba7cc3086deb62d40a714159d6c784cd50ba8c6 Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Wed, 24 Jun 2026 12:36:52 +0200 Subject: [PATCH] feat(telegram): native dialogs for confirms and deep-link notices 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. --- ui/src/components/StaleInviteModal.svelte | 24 ++++++++++- ui/src/components/WelcomeRedeemModal.svelte | 24 ++++++++++- ui/src/game/Game.svelte | 14 ++++++- ui/src/lib/nativedialogs.test.ts | 25 ++++++++++++ ui/src/lib/nativedialogs.ts | 24 +++++++++++ ui/src/lib/telegram.test.ts | 35 ++++++++++++++++ ui/src/lib/telegram.ts | 44 +++++++++++++++++++++ ui/src/screens/Friends.svelte | 34 +++++++++++----- 8 files changed, 209 insertions(+), 15 deletions(-) create mode 100644 ui/src/lib/nativedialogs.test.ts create mode 100644 ui/src/lib/nativedialogs.ts diff --git a/ui/src/components/StaleInviteModal.svelte b/ui/src/components/StaleInviteModal.svelte index f8a266b..d38259b 100644 --- a/ui/src/components/StaleInviteModal.svelte +++ b/ui/src/components/StaleInviteModal.svelte @@ -2,7 +2,8 @@ import { app, dismissStaleInvite } from '../lib/app.svelte'; import { t } from '../lib/i18n/index.svelte'; import { botUsername } from '../lib/deeplink'; - import { telegramOpenLink } from '../lib/telegram'; + import { insideTelegram, telegramDialogsAvailable, telegramOpenLink, telegramShowPopup } from '../lib/telegram'; + import { BOT_BUTTON_ID, botInfoPopup } from '../lib/nativedialogs'; import Modal from './Modal.svelte'; // The single bot's @username, for the deep link. @@ -10,6 +11,10 @@ // Split the message around the {bot} token so the bot handle renders as an inline link. const parts = $derived(t('friends.staleInvite').split('{bot}')); + // Inside the Mini App with native popups, present the notice as Telegram's own popup instead of + // the in-app modal. + const useNative = insideTelegram() && telegramDialogsAvailable(); + function openBot() { if (username) { const url = `https://t.me/${username}`; @@ -19,9 +24,24 @@ } dismissStaleInvite(); } + + // Native path: raise Telegram's popup when the notice appears; its "open bot" button opens the + // bot chat, any other dismissal just clears the notice. The `shown` guard fires it once per notice. + let shown = false; + $effect(() => { + if (!useNative) return; + if (app.staleInvite && !shown) { + shown = true; + void telegramShowPopup( + botInfoPopup(t('friends.staleInviteTitle'), t('friends.staleInvite'), username ?? '', t('common.ok')), + ).then((id) => (id === BOT_BUTTON_ID ? openBot() : dismissStaleInvite())); + } else if (!app.staleInvite) { + shown = false; + } + }); -{#if app.staleInvite} +{#if app.staleInvite && !useNative}

{parts[0]}{#if username}{/if}{parts[1] ?? ''}

diff --git a/ui/src/components/WelcomeRedeemModal.svelte b/ui/src/components/WelcomeRedeemModal.svelte index 7783c5c..a57f517 100644 --- a/ui/src/components/WelcomeRedeemModal.svelte +++ b/ui/src/components/WelcomeRedeemModal.svelte @@ -2,7 +2,8 @@ import { app, dismissWelcomeRedeem } from '../lib/app.svelte'; import { t } from '../lib/i18n/index.svelte'; import { botUsername } from '../lib/deeplink'; - import { telegramOpenLink } from '../lib/telegram'; + import { insideTelegram, telegramDialogsAvailable, telegramOpenLink, telegramShowPopup } from '../lib/telegram'; + import { BOT_BUTTON_ID, botInfoPopup } from '../lib/nativedialogs'; import Modal from './Modal.svelte'; // The single bot's @username, for the deep link. @@ -13,6 +14,10 @@ // as an inline link (the {name} value is substituted first; {bot} is left for the split). const parts = $derived(t('friends.welcomeRedeem', { name }).split('{bot}')); + // Inside the Mini App with native popups, present the greeting as Telegram's own popup instead of + // the in-app modal. + const useNative = insideTelegram() && telegramDialogsAvailable(); + function openBot() { if (username) { const url = `https://t.me/${username}`; @@ -22,9 +27,24 @@ } dismissWelcomeRedeem(); } + + // Native path: raise Telegram's popup when the greeting appears; its "open bot" button opens the + // bot chat, any other dismissal just clears it. The `shown` guard fires it once per greeting. + let shown = false; + $effect(() => { + if (!useNative) return; + if (app.welcomeRedeem && !shown) { + shown = true; + void telegramShowPopup( + botInfoPopup(t('friends.welcomeRedeemTitle'), t('friends.welcomeRedeem', { name }), username ?? '', t('common.ok')), + ).then((id) => (id === BOT_BUTTON_ID ? openBot() : dismissWelcomeRedeem())); + } else if (!app.welcomeRedeem) { + shown = false; + } + }); -{#if app.welcomeRedeem} +{#if app.welcomeRedeem && !useNative}

{parts[0]}{#if username}{/if}{parts[1] ?? ''}

diff --git a/ui/src/game/Game.svelte b/ui/src/game/Game.svelte index 67e1a09..5170c49 100644 --- a/ui/src/game/Game.svelte +++ b/ui/src/game/Game.svelte @@ -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 { + 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 @@ {/if} {:else} - + {/if} {#if !view.game.multipleWordsPerTurn}{t('game.oneWordRule')}{/if}