Files
scrabble-game/ui/src/game/CheckScreen.svelte
T
Ilia Denisov db17287113 fix(ui): route external links out of the Android VK WebView
The Android VK client's WebView ignores target=_blank and navigates the
Mini App's own window to the target, stranding the player outside the
game with no way back (the dictionary lookup, About/Feedback links, the
ad banner and the bot-link modal fallbacks). vk-bridge 3.x has no method
to open an external URL, so external links are routed through VK's own
leave-VK redirect (vk.com/away.php), which the client intercepts
natively and hands to the system browser.

onExternalLinkClick moves from lib/telegram to a new lib/links that
composes the Telegram and VK routers; iOS and desktop VK open _blank
correctly and are left alone.
2026-07-02 00:09:52 +02:00

152 lines
4.0 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { gateway } from '../lib/gateway';
import { handleError, showToast } from '../lib/app.svelte';
import { t } from '../lib/i18n/index.svelte';
import { alphabetLetters } from '../lib/alphabet';
import { canCheckWord, dictionaryLookupUrl, sanitizeCheckWord } from '../lib/checkword';
import { onExternalLinkClick } from '../lib/links';
import type { Variant } from '../lib/model';
// Word-check on its own screen: unlimited dictionary lookups, each with a
// complaint, off the board so the soft keyboard never relayouts the play area.
let { id }: { id: string } = $props();
let variant = $state<Variant>('scrabble_en');
let word = $state('');
let result = $state<{ word: string; legal: boolean } | null>(null);
let cooling = $state(false);
const checked = new Map<string, boolean>();
onMount(async () => {
try {
// Include the alphabet so input sanitising + the check accept the variant's letters.
const st = await gateway.gameState(id, true);
variant = st.game.variant;
} catch (e) {
handleError(e);
}
});
function onInput(e: Event) {
word = sanitizeCheckWord((e.target as HTMLInputElement).value, alphabetLetters(variant));
}
// Disabled while cooling, for an already-checked word, or an out-of-range length.
function canCheck(): boolean {
return canCheckWord(word, checked.has(word.trim().toUpperCase()), cooling);
}
async function runCheck() {
if (!canCheck()) return;
const w = word.trim().toUpperCase();
cooling = true;
setTimeout(() => (cooling = false), 5000);
try {
const r = await gateway.checkWord(id, w, variant);
checked.set(w, r.legal);
result = { word: w, legal: r.legal };
} catch (e) {
handleError(e);
}
}
async function complain() {
if (!result) return;
try {
await gateway.complaint(id, result.word, '');
showToast(t('game.complaintSent'));
result = null;
} catch (e) {
handleError(e);
}
}
</script>
<div class="wrap">
<div class="check">
<input
value={word}
oninput={onInput}
onkeydown={(e) => e.key === 'Enter' && runCheck()}
placeholder={t('game.checkWordPrompt')}
/>
<button onclick={runCheck} disabled={!canCheck()}>{t('game.check')}</button>
</div>
{#if result}
<p class="verdict" class:ok={result.legal} class:bad={!result.legal}>
{result.legal
? t('game.wordLegal', { word: result.word })
: t('game.wordIllegal', { word: result.word })}
</p>
<div class="actions">
<button class="complain" onclick={complain}>{t('game.complain')}</button>
{#if result.legal}
<a
class="lookup"
href={dictionaryLookupUrl(result.word, variant)}
target="_blank"
rel="noopener noreferrer"
onclick={onExternalLinkClick}>{t('game.lookup')}</a>
{/if}
</div>
{/if}
</div>
<style>
.wrap {
padding: 16px var(--pad);
display: flex;
flex-direction: column;
gap: 14px;
}
.check {
display: flex;
gap: 8px;
}
.check input {
flex: 1;
min-width: 0;
padding: 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg);
color: var(--text);
text-transform: uppercase;
}
.check button {
padding: 10px 16px;
border: 1px solid var(--accent);
background: var(--accent);
color: var(--accent-text);
border-radius: var(--radius-sm);
}
.check button:disabled {
opacity: 0.5;
}
.verdict {
margin: 0;
font-weight: 600;
}
.verdict.ok {
color: var(--ok, #2e7d32);
}
.verdict.bad {
color: var(--danger, #c0392b);
}
.actions {
display: flex;
align-items: center;
gap: 12px;
}
.complain {
padding: 8px 14px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
border-radius: var(--radius-sm);
}
.lookup {
color: var(--accent);
text-decoration: underline;
font-size: 0.95em;
}
</style>