Chat + word-check as their own screens; in-game unread badge (review item 7)
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 34s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m6s
- Chat and word-check are now routed screens (/game/:id/chat, /game/:id/check) with a header back to the game and no tab-bar, replacing their modals. The soft keyboard just resizes the visible viewport (tracked into --vvh, which the Screen height uses since iOS does not shrink dvh for the keyboard) with the input pinned to the bottom: no modal relayout, no page jump. Supersedes the earlier bottom-sheet Modal attempt. - A new chat message raises an unread badge on the in-game hamburger + the Chat menu row (per game, cleared on opening the chat), mirroring the lobby badge. - TG native back + the header back chevron return chat/check to their game. - Exposes --tg-safe-top (device notch) for the finalised TG-fullscreen header. Tests: e2e for chat/check opening as their own screens + back. Docs: PLAN, FUNCTIONAL(+ru).
This commit is contained in:
@@ -47,6 +47,8 @@ export const app = $state<{
|
||||
localeLocked: boolean;
|
||||
/** Pending incoming friend requests + invitations, for the lobby badge. */
|
||||
notifications: number;
|
||||
/** Unread chat-message count per game id, for the in-game menu/hamburger badge. */
|
||||
chatUnread: Record<string, number>;
|
||||
}>({
|
||||
ready: false,
|
||||
session: null,
|
||||
@@ -60,6 +62,7 @@ export const app = $state<{
|
||||
boardLines: false,
|
||||
localeLocked: false,
|
||||
notifications: 0,
|
||||
chatUnread: {},
|
||||
});
|
||||
|
||||
let unsubscribeStream: (() => void) | null = null;
|
||||
@@ -105,6 +108,11 @@ export function showToast(text: string, kind: Toast['kind'] = 'info'): void {
|
||||
toastTimer = setTimeout(() => (app.toast = null), 4000);
|
||||
}
|
||||
|
||||
/** clearChatUnread resets a game's unread chat-message count (called when its chat is opened). */
|
||||
export function clearChatUnread(gameId: string): void {
|
||||
if (app.chatUnread[gameId]) app.chatUnread = { ...app.chatUnread, [gameId]: 0 };
|
||||
}
|
||||
|
||||
/** handleError maps a GatewayError to a toast; an invalid session logs out. */
|
||||
export function handleError(err: unknown): void {
|
||||
telegramHaptic('error');
|
||||
@@ -126,7 +134,15 @@ function openStream(): void {
|
||||
(e) => {
|
||||
app.lastEvent = e;
|
||||
if (e.kind === 'chat_message' && e.message.senderId !== app.session?.userId) {
|
||||
showToast(e.message.kind === 'nudge' ? t('chat.nudge') : e.message.body, 'info');
|
||||
// While the player is on that game's chat screen, neither toast nor bump the unread.
|
||||
const onChat = router.route.name === 'gameChat' && router.route.params.id === e.message.gameId;
|
||||
if (!onChat) {
|
||||
if (e.message.kind !== 'nudge') {
|
||||
const gid = e.message.gameId;
|
||||
app.chatUnread = { ...app.chatUnread, [gid]: (app.chatUnread[gid] ?? 0) + 1 };
|
||||
}
|
||||
showToast(e.message.kind === 'nudge' ? t('chat.nudge') : e.message.body, 'info');
|
||||
}
|
||||
} else if (e.kind === 'nudge') {
|
||||
showToast(t('chat.nudge'), 'info');
|
||||
} else if (e.kind === 'your_turn') {
|
||||
@@ -243,6 +259,19 @@ function syncTelegramSafeArea(): void {
|
||||
document.documentElement.classList.toggle('tg-fullscreen', top > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* syncViewportHeight mirrors the visual-viewport height into the --vvh CSS var so a screen can
|
||||
* fit the visible area above an open soft keyboard (iOS does not shrink dvh for the keyboard).
|
||||
* On a screen whose input sits at the bottom (chat, word-check) this keeps the input visible
|
||||
* without the page scrolling, so the layout no longer jumps when the keyboard appears (Stage 17).
|
||||
*/
|
||||
function syncViewportHeight(): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
const vv = typeof window !== 'undefined' ? window.visualViewport : null;
|
||||
const h = vv ? vv.height : typeof window !== 'undefined' ? window.innerHeight : 0;
|
||||
if (h > 0) document.documentElement.style.setProperty('--vvh', `${h}px`);
|
||||
}
|
||||
|
||||
export async function bootstrap(): Promise<void> {
|
||||
const prefs = await loadPrefs();
|
||||
app.theme = prefs.theme ?? 'auto';
|
||||
@@ -261,6 +290,13 @@ export async function bootstrap(): Promise<void> {
|
||||
setLocale(guess);
|
||||
}
|
||||
|
||||
// Track the visual-viewport height so screens fit above an open soft keyboard (--vvh).
|
||||
syncViewportHeight();
|
||||
if (typeof window !== 'undefined' && window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', syncViewportHeight);
|
||||
window.visualViewport.addEventListener('scroll', syncViewportHeight);
|
||||
}
|
||||
|
||||
// Telegram Mini App launch: apply the platform theme, authenticate via initData,
|
||||
// and route any deep-link start parameter. On the dedicated /telegram/ entry path
|
||||
// outside Telegram (no initData), refuse to render and send the visitor to the
|
||||
|
||||
Reference in New Issue
Block a user