From 72e7447d28a6f9ec7e14a55dd9a3d65110de6451 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Sun, 19 Jul 2026 01:56:55 -0400 Subject: [PATCH] fix(mobile): stack embed cards + secondary 44px touch targets (r2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mobile follow-ups round 2 (survey findings deferred from the mobile audit), reviewed by 2 agents on the staged diff (both SHIP). - URL-preview cards: the Twitch / Twitter / TikTok-fallback cards render their thumbnail/header BESIDE the content as direct children of the UrlPreview flex row, which squeezes both on a phone. Add `StackOnMobile` (@media max-width:750px -> flex-direction:column) scoped to those variants via cardClass. folds Box has no default `direction`, so the override wins uncontested; desktop (>750px) is unchanged. No-op for the single-column embed cards (MediaEmbedCard/TikTokEmbedCard). - 44px touch targets (MobileTouchTarget, @media max-width:750px) on the otherwise ~28px controls: embed-player Close/Collapse/Fullscreen/View-post buttons; image-viewer close/zoom/download; the read-receipt "seen by" pill. Deferred (rationale, not built): PiP resize handles + fullscreen button — enlarging four 24px corners to 44px would swallow a ~160px mobile PiP and block "Return to call"; presence dot is a non-interactive status indicator. Gates: tsc 0, eslint 0, prettier clean, 856/856 tests, build ok. Co-Authored-By: Claude Opus 4.8 --- .../components/image-viewer/ImageViewer.tsx | 19 +++++++++++++++++-- .../ReadReceiptAvatars.tsx | 3 ++- .../components/url-preview/UrlPreview.css.tsx | 13 +++++++++++++ .../components/url-preview/UrlPreviewCard.tsx | 16 +++++++++++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/app/components/image-viewer/ImageViewer.tsx b/src/app/components/image-viewer/ImageViewer.tsx index 6136ba256..08763deba 100644 --- a/src/app/components/image-viewer/ImageViewer.tsx +++ b/src/app/components/image-viewer/ImageViewer.tsx @@ -7,6 +7,7 @@ import * as css from './ImageViewer.css'; import { useZoom } from '../../hooks/useZoom'; import { usePan } from '../../hooks/usePan'; import { downloadMedia } from '../../utils/matrix'; +import { MobileTouchTarget } from '../../styles/mobile.css'; export type ImageViewerProps = { alt: string; @@ -35,7 +36,13 @@ export const ImageViewer = as<'div', ImageViewerProps>( >
- + @@ -48,12 +55,18 @@ export const ImageViewer = as<'div', ImageViewerProps>( outlined={zoom < 1} size="300" radii="Pill" + className={MobileTouchTarget} onClick={zoomOut} aria-label="Zoom Out" > - setZoom(zoom === 1 ? 2 : 1)}> + setZoom(zoom === 1 ? 2 : 1)} + > {Math.round(zoom * 100)}% ( outlined={zoom > 1} size="300" radii="Pill" + className={MobileTouchTarget} onClick={zoomIn} aria-label="Zoom In" > @@ -70,6 +84,7 @@ export const ImageViewer = as<'div', ImageViewerProps>( variant="Primary" onClick={handleDownload} radii="300" + className={MobileTouchTarget} before={} > {t('Organisms.ImageViewer.download')} diff --git a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx index 6b3d12597..1b8f0f684 100644 --- a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx +++ b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx @@ -22,6 +22,7 @@ import { stopPropagation } from '../../utils/keyboard'; import { useModalStyle } from '../../hooks/useModalStyle'; import { useMemberAvatar } from '../../hooks/useMemberAvatar'; import { useRoomMembersChange } from '../../hooks/useRoomMemberChange'; +import { MobileTouchTarget } from '../../styles/mobile.css'; import * as css from './ReadReceiptAvatars.css'; const MAX_DISPLAY = 5; @@ -92,7 +93,7 @@ export function ReadReceiptAvatars({ onClick={() => setOpen(true)} title={tooltipNames} aria-label={tooltipNames} - className={css.ReceiptTrigger} + className={`${css.ReceiptTrigger} ${MobileTouchTarget}`} > {/* Pill wrapper ensures visibility on any wallpaper/background */} 750px) is unchanged. +export const StackOnMobile = style({ + '@media': { + '(max-width: 750px)': { + flexDirection: 'column', + }, + }, +}); + export const UrlPreviewImg = style([ DefaultReset, { diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx index 8be8f6e99..aba926e57 100644 --- a/src/app/components/url-preview/UrlPreviewCard.tsx +++ b/src/app/components/url-preview/UrlPreviewCard.tsx @@ -15,6 +15,7 @@ import { config, } from 'folds'; import { ImageOverlay } from '../ImageOverlay'; +import { MobileTouchTarget } from '../../styles/mobile.css'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { UrlPreview, UrlPreviewContent, UrlPreviewDescription, UrlPreviewImg } from './UrlPreview'; @@ -726,6 +727,7 @@ function TwitterCard({ size="300" radii="300" variant="SurfaceVariant" + className={MobileTouchTarget} onClick={() => setExpanded(false)} aria-label="Collapse post" > @@ -780,6 +782,7 @@ function TwitterCard({ setExpanded(true)} before={} > @@ -1249,6 +1252,7 @@ function MediaEmbedCard({ @@ -1259,6 +1263,7 @@ function MediaEmbedCard({ size="300" radii="300" variant="SurfaceVariant" + className={MobileTouchTarget} onClick={() => setPlaying(false)} aria-label="Close player" > @@ -1407,6 +1412,7 @@ function TikTokEmbedCard({ url, prev }: { url: string; prev: IPreviewUrlResponse @@ -1416,6 +1422,7 @@ function TikTokEmbedCard({ url, prev }: { url: string; prev: IPreviewUrlResponse size="300" radii="300" variant="SurfaceVariant" + className={MobileTouchTarget} onClick={() => setPlaying(false)} aria-label="Close player" > @@ -2069,7 +2076,14 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>( // player chrome / tweet content isn't cramped or clipped. const embed = parseMediaEmbed(url, window.location.hostname); const wide = !!embed || isTwitterTweet(url); - const cardClass = wide ? previewCss.UrlPreviewWide : undefined; + // Twitter/Twitch/TikTok(fallback) cards render header/thumbnail beside content + // in the card flex row; stack them on phones (no-op for the single-column + // embed cards). Desktop keeps the row layout. + const stackOnMobile = isTwitter(url) || isTwitch(url) || isTikTok(url); + const cardClass = + [wide && previewCss.UrlPreviewWide, stackOnMobile && previewCss.StackOnMobile] + .filter(Boolean) + .join(' ') || undefined; const renderContent = (prev: IPreviewUrlResponse): React.ReactNode => { // Embeddable media (YouTube/Vimeo/TikTok/Dailymotion/Streamable/Twitch/