feat(telegram): native dialogs (experimental, stacked on #136) #137
@@ -2,7 +2,8 @@
|
|||||||
import { app, dismissStaleInvite } from '../lib/app.svelte';
|
import { app, dismissStaleInvite } from '../lib/app.svelte';
|
||||||
import { t } from '../lib/i18n/index.svelte';
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
import { botUsername } from '../lib/deeplink';
|
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';
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
// The single bot's @username, for the deep link.
|
// 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.
|
// Split the message around the {bot} token so the bot handle renders as an inline link.
|
||||||
const parts = $derived(t('friends.staleInvite').split('{bot}'));
|
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() {
|
function openBot() {
|
||||||
if (username) {
|
if (username) {
|
||||||
const url = `https://t.me/${username}`;
|
const url = `https://t.me/${username}`;
|
||||||
@@ -19,9 +24,24 @@
|
|||||||
}
|
}
|
||||||
dismissStaleInvite();
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if app.staleInvite}
|
{#if app.staleInvite && !useNative}
|
||||||
<Modal title={t('friends.staleInviteTitle')} onclose={dismissStaleInvite}>
|
<Modal title={t('friends.staleInviteTitle')} onclose={dismissStaleInvite}>
|
||||||
<p class="msg">{parts[0]}{#if username}<button type="button" class="bot" onclick={openBot}>@{username}</button>{/if}{parts[1] ?? ''}</p>
|
<p class="msg">{parts[0]}{#if username}<button type="button" class="bot" onclick={openBot}>@{username}</button>{/if}{parts[1] ?? ''}</p>
|
||||||
<button class="ok" onclick={dismissStaleInvite}>{t('common.ok')}</button>
|
<button class="ok" onclick={dismissStaleInvite}>{t('common.ok')}</button>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { app, dismissWelcomeRedeem } from '../lib/app.svelte';
|
import { app, dismissWelcomeRedeem } from '../lib/app.svelte';
|
||||||
import { t } from '../lib/i18n/index.svelte';
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
import { botUsername } from '../lib/deeplink';
|
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';
|
import Modal from './Modal.svelte';
|
||||||
|
|
||||||
// The single bot's @username, for the deep link.
|
// 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).
|
// 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}'));
|
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() {
|
function openBot() {
|
||||||
if (username) {
|
if (username) {
|
||||||
const url = `https://t.me/${username}`;
|
const url = `https://t.me/${username}`;
|
||||||
@@ -22,9 +27,24 @@
|
|||||||
}
|
}
|
||||||
dismissWelcomeRedeem();
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if app.welcomeRedeem}
|
{#if app.welcomeRedeem && !useNative}
|
||||||
<Modal title={t('friends.welcomeRedeemTitle')} onclose={dismissWelcomeRedeem}>
|
<Modal title={t('friends.welcomeRedeemTitle')} onclose={dismissWelcomeRedeem}>
|
||||||
<p class="msg">{parts[0]}{#if username}<button type="button" class="bot" onclick={openBot}>@{username}</button>{/if}{parts[1] ?? ''}</p>
|
<p class="msg">{parts[0]}{#if username}<button type="button" class="bot" onclick={openBot}>@{username}</button>{/if}{parts[1] ?? ''}</p>
|
||||||
<button class="ok" onclick={dismissWelcomeRedeem}>{t('common.ok')}</button>
|
<button class="ok" onclick={dismissWelcomeRedeem}>{t('common.ok')}</button>
|
||||||
|
|||||||
+12
-2
@@ -24,7 +24,7 @@
|
|||||||
import { getCachedGame, setCachedGame, setCachedDraft, type CachedGame } from '../lib/gamecache';
|
import { getCachedGame, setCachedGame, setCachedDraft, type CachedGame } from '../lib/gamecache';
|
||||||
import { patchLobbyGame } from '../lib/lobbycache';
|
import { patchLobbyGame } from '../lib/lobbycache';
|
||||||
import { applyGameOver, applyMoveDelta, applyOpponentJoined, type DeltaResult } from '../lib/gamedelta';
|
import { applyGameOver, applyMoveDelta, applyOpponentJoined, type DeltaResult } from '../lib/gamedelta';
|
||||||
import { telegramHaptic } from '../lib/telegram';
|
import { insideTelegram, telegramDialogsAvailable, telegramHaptic, telegramShowConfirm } from '../lib/telegram';
|
||||||
import {
|
import {
|
||||||
BLANK,
|
BLANK,
|
||||||
newPlacement,
|
newPlacement,
|
||||||
@@ -712,6 +712,16 @@
|
|||||||
busy = false;
|
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() {
|
async function doResign() {
|
||||||
resignOpen = false;
|
resignOpen = false;
|
||||||
busy = true;
|
busy = true;
|
||||||
@@ -1149,7 +1159,7 @@
|
|||||||
<button class="hicon" onclick={exportGcg} aria-label={t('game.exportGcg')}>📤</button>
|
<button class="hicon" onclick={exportGcg} aria-label={t('game.exportGcg')}>📤</button>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{: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}
|
||||||
{#if !view.game.multipleWordsPerTurn}<span class="oneword-label">{t('game.oneWordRule')}</span>{/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
|
<!-- A finished AI game has no comms at all (no chat, and the dictionary closes with the
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { BOT_BUTTON_ID, botInfoPopup } from './nativedialogs';
|
||||||
|
|
||||||
|
describe('botInfoPopup', () => {
|
||||||
|
it('inlines the bot handle and adds an open-bot button', () => {
|
||||||
|
const p = botInfoPopup('Title', 'Open the bot {bot} to play.', 'erudit_bot', 'OK');
|
||||||
|
expect(p.title).toBe('Title');
|
||||||
|
expect(p.message).toBe('Open the bot @erudit_bot to play.');
|
||||||
|
expect(p.buttons).toEqual([
|
||||||
|
{ id: BOT_BUTTON_ID, text: '@erudit_bot' },
|
||||||
|
{ id: 'ok', text: 'OK' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops the token and the open-bot button when no username is known', () => {
|
||||||
|
const p = botInfoPopup('Title', 'Open the bot {bot} to play.', '', 'OK');
|
||||||
|
expect(p.message).toBe('Open the bot to play.');
|
||||||
|
expect(p.buttons).toEqual([{ id: 'ok', text: 'OK' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves newlines in the message', () => {
|
||||||
|
const p = botInfoPopup('Hi', 'Welcome!\n\nUse {bot} now.', 'b', 'OK');
|
||||||
|
expect(p.message).toBe('Welcome!\n\nUse @b now.');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Pure builder for the Telegram native popup (showPopup) used by the deep-link info modals
|
||||||
|
// (StaleInviteModal / WelcomeRedeemModal) inside the Mini App. Kept free of the SDK and the DOM so
|
||||||
|
// it unit-tests in the node environment; the showPopup transport wrapper lives in telegram.ts and
|
||||||
|
// the wiring (native inside Telegram, the in-app Modal elsewhere) in the modal components.
|
||||||
|
|
||||||
|
import type { TelegramPopupParams } from './telegram';
|
||||||
|
|
||||||
|
/** BOT_BUTTON_ID is the showPopup button id that means "open the bot chat". */
|
||||||
|
export const BOT_BUTTON_ID = 'bot';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* botInfoPopup builds the native popup for a deep-link info modal: the message with the `{bot}`
|
||||||
|
* token replaced by the `@username` as plain text (a native popup has no inline link, unlike the
|
||||||
|
* in-app Modal), plus an "open bot" button when a username is known and a closing OK button. With
|
||||||
|
* no username it is the message (token removed) and OK only.
|
||||||
|
*/
|
||||||
|
export function botInfoPopup(title: string, message: string, username: string, okText: string): TelegramPopupParams {
|
||||||
|
const handle = username ? `@${username}` : '';
|
||||||
|
const text = (username ? message.replace('{bot}', handle) : message.replace('{bot}', '').replace(' ', ' ')).trim();
|
||||||
|
const buttons = username
|
||||||
|
? [{ id: BOT_BUTTON_ID, text: handle }, { id: 'ok', text: okText }]
|
||||||
|
: [{ id: 'ok', text: okText }];
|
||||||
|
return { title, message: text, buttons };
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
telegramThemeParams,
|
telegramThemeParams,
|
||||||
telegramSafeAreaInset,
|
telegramSafeAreaInset,
|
||||||
telegramShowSettingsButton,
|
telegramShowSettingsButton,
|
||||||
|
telegramDialogsAvailable,
|
||||||
|
telegramShowConfirm,
|
||||||
|
telegramShowPopup,
|
||||||
} from './telegram';
|
} from './telegram';
|
||||||
|
|
||||||
function stubWebApp(initData: string, startParam?: string) {
|
function stubWebApp(initData: string, startParam?: string) {
|
||||||
@@ -96,6 +99,38 @@ describe('telegramShowSettingsButton', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('native dialogs', () => {
|
||||||
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
|
it('telegramDialogsAvailable reflects showPopup presence', () => {
|
||||||
|
expect(telegramDialogsAvailable()).toBe(false);
|
||||||
|
vi.stubGlobal('window', { Telegram: { WebApp: { showPopup: () => {} } } });
|
||||||
|
expect(telegramDialogsAvailable()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('telegramShowConfirm resolves the user choice inside Telegram', async () => {
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
Telegram: { WebApp: { showConfirm: (_m: string, cb: (ok: boolean) => void) => cb(true) } },
|
||||||
|
});
|
||||||
|
await expect(telegramShowConfirm('Sure?')).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('telegramShowConfirm resolves false without the SDK dialog', async () => {
|
||||||
|
await expect(telegramShowConfirm('Sure?')).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('telegramShowPopup resolves the pressed button id', async () => {
|
||||||
|
vi.stubGlobal('window', {
|
||||||
|
Telegram: { WebApp: { showPopup: (_p: unknown, cb: (id: string) => void) => cb('bot') } },
|
||||||
|
});
|
||||||
|
await expect(telegramShowPopup({ message: 'Hi' })).resolves.toBe('bot');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('telegramShowPopup resolves null without the SDK', async () => {
|
||||||
|
await expect(telegramShowPopup({ message: 'Hi' })).resolves.toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('routeExternalLinkInTelegram', () => {
|
describe('routeExternalLinkInTelegram', () => {
|
||||||
afterEach(() => vi.unstubAllGlobals());
|
afterEach(() => vi.unstubAllGlobals());
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,20 @@
|
|||||||
|
|
||||||
import type { TelegramThemeParams } from './theme';
|
import type { TelegramThemeParams } from './theme';
|
||||||
|
|
||||||
|
/** TelegramPopupButton is one button of a native showPopup (Bot API 6.2). */
|
||||||
|
export interface TelegramPopupButton {
|
||||||
|
id?: string;
|
||||||
|
type?: 'default' | 'ok' | 'close' | 'cancel' | 'destructive';
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** TelegramPopupParams configures a native showPopup (title optional, message required, up to 3 buttons). */
|
||||||
|
export interface TelegramPopupParams {
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
buttons?: TelegramPopupButton[];
|
||||||
|
}
|
||||||
|
|
||||||
interface TelegramWebApp {
|
interface TelegramWebApp {
|
||||||
initData: string;
|
initData: string;
|
||||||
initDataUnsafe?: { start_param?: string };
|
initDataUnsafe?: { start_param?: string };
|
||||||
@@ -51,6 +65,8 @@ interface TelegramWebApp {
|
|||||||
getItem?: (key: string, cb: (err: string | null, value?: string) => void) => void;
|
getItem?: (key: string, cb: (err: string | null, value?: string) => void) => void;
|
||||||
setItem?: (key: string, value: string, cb?: (err: string | null, ok?: boolean) => void) => void;
|
setItem?: (key: string, value: string, cb?: (err: string | null, ok?: boolean) => void) => void;
|
||||||
};
|
};
|
||||||
|
showConfirm?: (message: string, cb?: (ok: boolean) => void) => void;
|
||||||
|
showPopup?: (params: TelegramPopupParams, cb?: (buttonId: string) => void) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function webApp(): TelegramWebApp | undefined {
|
function webApp(): TelegramWebApp | undefined {
|
||||||
@@ -347,6 +363,34 @@ export function telegramCloudSet(key: string, value: string): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** telegramDialogsAvailable reports whether Telegram's native dialogs (showConfirm / showPopup, Bot
|
||||||
|
* API 6.2) are usable, so a caller can choose the native path over its own modal. */
|
||||||
|
export function telegramDialogsAvailable(): boolean {
|
||||||
|
return !!webApp()?.showPopup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* telegramShowConfirm shows Telegram's native confirm dialog and resolves true when the user
|
||||||
|
* accepts. Resolves false outside Telegram or on a client predating the dialog, so callers should
|
||||||
|
* gate on telegramDialogsAvailable and fall back to their own modal otherwise.
|
||||||
|
*/
|
||||||
|
export function telegramShowConfirm(message: string): Promise<boolean> {
|
||||||
|
const w = webApp();
|
||||||
|
if (!w?.showConfirm) return Promise.resolve(false);
|
||||||
|
return new Promise((resolve) => w.showConfirm!(message, (ok) => resolve(!!ok)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* telegramShowPopup shows Telegram's native popup and resolves the pressed button id (the empty
|
||||||
|
* string when dismissed without pressing a button). Resolves null outside Telegram or on a client
|
||||||
|
* predating the popup, so callers can fall back to their own modal.
|
||||||
|
*/
|
||||||
|
export function telegramShowPopup(params: TelegramPopupParams): Promise<string | null> {
|
||||||
|
const w = webApp();
|
||||||
|
if (!w?.showPopup) return Promise.resolve(null);
|
||||||
|
return new Promise((resolve) => w.showPopup!(params, (id) => resolve(id ?? '')));
|
||||||
|
}
|
||||||
|
|
||||||
/** Haptic is the set of feedbacks the app triggers. */
|
/** Haptic is the set of feedbacks the app triggers. */
|
||||||
export type Haptic = 'select' | 'success' | 'error' | 'warning' | 'light' | 'medium' | 'heavy';
|
export type Haptic = 'select' | 'success' | 'error' | 'warning' | 'light' | 'medium' | 'heavy';
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { GatewayError } from '../lib/client';
|
import { GatewayError } from '../lib/client';
|
||||||
import { t } from '../lib/i18n/index.svelte';
|
import { t } from '../lib/i18n/index.svelte';
|
||||||
import { friendCodeParam, shareLink } from '../lib/deeplink';
|
import { friendCodeParam, shareLink } from '../lib/deeplink';
|
||||||
import { shareTelegramLink } from '../lib/telegram';
|
import { insideTelegram, shareTelegramLink, telegramDialogsAvailable, telegramShowConfirm } from '../lib/telegram';
|
||||||
import type { AccountRef, FriendCode, RobotBlockEntry } from '../lib/model';
|
import type { AccountRef, FriendCode, RobotBlockEntry } from '../lib/model';
|
||||||
|
|
||||||
let friends = $state<AccountRef[]>([]);
|
let friends = $state<AccountRef[]>([]);
|
||||||
@@ -66,20 +66,36 @@
|
|||||||
|
|
||||||
// confirmBlock / confirmUnfriend run the pending action once its modal is
|
// confirmBlock / confirmUnfriend run the pending action once its modal is
|
||||||
// accepted, then clear the target and the revealed row.
|
// accepted, then clear the target and the revealed row.
|
||||||
function confirmBlock(): void {
|
function confirmBlock(target = blockTarget): void {
|
||||||
const target = blockTarget;
|
|
||||||
blockTarget = null;
|
blockTarget = null;
|
||||||
revealedId = null;
|
revealedId = null;
|
||||||
if (target) void blockUser(target.accountId);
|
if (target) void blockUser(target.accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmUnfriend(): void {
|
function confirmUnfriend(target = unfriendTarget): void {
|
||||||
const target = unfriendTarget;
|
|
||||||
unfriendTarget = null;
|
unfriendTarget = null;
|
||||||
revealedId = null;
|
revealedId = null;
|
||||||
if (target) void remove(target.accountId);
|
if (target) void remove(target.accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onBlockClick / onUnfriendClick: inside the Mini App (and online) confirm with Telegram's native
|
||||||
|
// dialog and act on accept; otherwise open the in-app confirm modal (the offline / web path).
|
||||||
|
async function onBlockClick(f: AccountRef): Promise<void> {
|
||||||
|
if (connection.online && insideTelegram() && telegramDialogsAvailable()) {
|
||||||
|
if (await telegramShowConfirm(`${t('friends.blockConfirm')}\n${f.displayName}`)) confirmBlock(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
blockTarget = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUnfriendClick(f: AccountRef): Promise<void> {
|
||||||
|
if (connection.online && insideTelegram() && telegramDialogsAvailable()) {
|
||||||
|
if (await telegramShowConfirm(`${t('friends.unfriendConfirm')}\n${f.displayName}`)) confirmUnfriend(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unfriendTarget = f;
|
||||||
|
}
|
||||||
|
|
||||||
// While a friend row is slid open, a tap anywhere outside its action buttons
|
// While a friend row is slid open, a tap anywhere outside its action buttons
|
||||||
// closes it again. Taps on a kebab are skipped so its own toggle stays in charge.
|
// closes it again. Taps on a kebab are skipped so its own toggle stays in charge.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -216,8 +232,8 @@
|
|||||||
{#each friends as f (f.accountId)}
|
{#each friends as f (f.accountId)}
|
||||||
<div class="rowwrap" class:revealed={revealedId === f.accountId}>
|
<div class="rowwrap" class:revealed={revealedId === f.accountId}>
|
||||||
<div class="acts">
|
<div class="acts">
|
||||||
<button class="iconbtn" onclick={() => (blockTarget = f)} disabled={!connection.online} aria-label={t('friends.block')}>🚫</button>
|
<button class="iconbtn" onclick={() => onBlockClick(f)} disabled={!connection.online} aria-label={t('friends.block')}>🚫</button>
|
||||||
<button class="iconbtn" onclick={() => (unfriendTarget = f)} disabled={!connection.online} aria-label={t('friends.unfriend')}>✖️</button>
|
<button class="iconbtn" onclick={() => onUnfriendClick(f)} disabled={!connection.online} aria-label={t('friends.unfriend')}>✖️</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<span class="who">{f.displayName}</span>
|
<span class="who">{f.displayName}</span>
|
||||||
@@ -264,7 +280,7 @@
|
|||||||
<p class="confirm-name">{blockTarget.displayName}</p>
|
<p class="confirm-name">{blockTarget.displayName}</p>
|
||||||
<div class="confirm-row">
|
<div class="confirm-row">
|
||||||
<button class="cancel" onclick={() => (blockTarget = null)}>{t('common.cancel')}</button>
|
<button class="cancel" onclick={() => (blockTarget = null)}>{t('common.cancel')}</button>
|
||||||
<button class="danger" onclick={confirmBlock} disabled={!connection.online}>{t('friends.block')}</button>
|
<button class="danger" onclick={() => confirmBlock()} disabled={!connection.online}>{t('friends.block')}</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -273,7 +289,7 @@
|
|||||||
<p class="confirm-name">{unfriendTarget.displayName}</p>
|
<p class="confirm-name">{unfriendTarget.displayName}</p>
|
||||||
<div class="confirm-row">
|
<div class="confirm-row">
|
||||||
<button class="cancel" onclick={() => (unfriendTarget = null)}>{t('common.cancel')}</button>
|
<button class="cancel" onclick={() => (unfriendTarget = null)}>{t('common.cancel')}</button>
|
||||||
<button class="danger" onclick={confirmUnfriend} disabled={!connection.online}>{t('friends.unfriend')}</button>
|
<button class="danger" onclick={() => confirmUnfriend()} disabled={!connection.online}>{t('friends.unfriend')}</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user