diff --git a/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx b/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx index 5b9afb49341a5..cb91718bb3045 100644 --- a/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx +++ b/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx @@ -5,7 +5,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell import { effectiveSnoozed } from "@t3tools/client-runtime/state/thread-settled"; import { sortInboxThreadsByReturn } from "@t3tools/client-runtime/state/thread-inbox"; import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; -import { Animated, FlatList, Modal, Pressable, View } from "react-native"; +import { Animated, FlatList, Modal, Pressable, useWindowDimensions, View } from "react-native"; import { Gesture, GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler"; import Reanimated, { ReduceMotion, useAnimatedStyle, withTiming } from "react-native-reanimated"; import { threadDragGapOffset } from "./threadDragGap"; @@ -14,6 +14,7 @@ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/ui/AppText"; import { SymbolView } from "../../components/ui/AppSymbol"; import { scopedThreadKey } from "../../lib/scopedEntities"; +import { deriveThreadArrangementSizing } from "../../lib/layout"; import { environmentServerConfigsAtom } from "../../state/server"; import { environmentThreadShells } from "../../state/threads"; import { sortSettledThreads } from "@t3tools/client-runtime/state/thread-sort"; @@ -38,9 +39,8 @@ import { } from "./threadListV2"; import { useThreadListNow } from "./use-thread-list-now"; import { useThreadListV2ShelfPreferences } from "./use-thread-list-v2-shelf-preferences"; +import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; -const ROW_HEIGHT = 56; -const HEADER_HEIGHT = 48; const keyOf = (thread: EnvironmentThreadShell) => scopedThreadKey(thread.environmentId, thread.id); type Section = "pinned" | "active" | "snoozed" | "settled"; type Destination = Exclude; @@ -162,6 +162,12 @@ function DragHandle(props: { } function ThreadArrangementSheet(props: { onClose: () => void }) { + const { appearance } = useAppearancePreferences(); + const { fontScale } = useWindowDimensions(); + const { rowHeight, headerHeight } = deriveThreadArrangementSizing({ + baseFontSize: appearance.baseFontSize, + fontScale, + }); const threads = useAtomValue(environmentThreadShells.navigationThreadShellsAtom); const configs = useAtomValue(environmentServerConfigsAtom); const queuedThreadKeys = useAtomValue(queuedThreadKeysAtom); @@ -225,20 +231,20 @@ function ThreadArrangementSheet(props: { onClose: () => void }) { let offset = 0; for (const section of ["pinned", "active", "snoozed", "settled"] as const) { if (section === "snoozed" && sections[section].length === 0) continue; - result.push({ key: section, section, offset, height: HEADER_HEIGHT }); - offset += HEADER_HEIGHT; + result.push({ key: section, section, offset, height: headerHeight }); + offset += headerHeight; if ((section === "snoozed" || section === "settled") && !expanded[section]) continue; for (const thread of section === "settled" ? visibleSettled : sections[section]) { - result.push({ key: keyOf(thread), section, thread, offset, height: ROW_HEIGHT }); - offset += ROW_HEIGHT; + result.push({ key: keyOf(thread), section, thread, offset, height: rowHeight }); + offset += rowHeight; } if (section === "settled" && settledCount > visibleSettled.length) { - result.push({ key: "settled-more", section, more: true, offset, height: ROW_HEIGHT }); - offset += ROW_HEIGHT; + result.push({ key: "settled-more", section, more: true, offset, height: rowHeight }); + offset += rowHeight; } } return result; - }, [sections, expanded, visibleSettled, settledCount]); + }, [sections, expanded, visibleSettled, settledCount, rowHeight, headerHeight]); const list = useRef>(null); const geometry = useRef({ height: 0, offset: 0 }); const drag = useRef(null); @@ -256,7 +262,9 @@ function ThreadArrangementSheet(props: { onClose: () => void }) { setPreview(null); } const orderVersion = rows - .map((row) => `${row.key}:${row.thread?.pinOrderKey}:${row.thread?.activeOrderKey}`) + .map( + (row) => `${row.key}:${row.height}:${row.thread?.pinOrderKey}:${row.thread?.activeOrderKey}`, + ) .join("|"); // oxlint-disable-next-line t3code/no-unreasoned-state-sync -- cancels the in-progress drag gesture when the thread order changes underneath it useEffect(() => { @@ -277,7 +285,7 @@ function ThreadArrangementSheet(props: { onClose: () => void }) { current.translation = translation; const { height, offset } = geometry.current; const y = current.startY + translation; - translateY.setValue(Math.max(0, Math.min(height - ROW_HEIGHT, y - ROW_HEIGHT / 2))); + translateY.setValue(Math.max(0, Math.min(height - rowHeight, y - rowHeight / 2))); const contentY = Math.max(0, y + offset); const target = latest.current.rows.find((row) => contentY < row.offset + row.height) ?? @@ -316,7 +324,7 @@ function ThreadArrangementSheet(props: { onClose: () => void }) { orderVersion, sourceSection: row.section, thread: row.thread, - startY: row.offset + ROW_HEIGHT / 2 - geometry.current.offset, + startY: row.offset + row.height / 2 - geometry.current.offset, translation: 0, destination: null, }; @@ -544,7 +552,7 @@ function ThreadArrangementSheet(props: { onClose: () => void }) { { + it("fits two title lines and section labels with combined app and system text scaling", () => { + const sizing = deriveThreadArrangementSizing({ baseFontSize: 22, fontScale: 1.3 }); + expect(sizing.rowHeight).toBeGreaterThanOrEqual(2 * 32 * 1.3 + 10); + expect(sizing.headerHeight).toBeGreaterThanOrEqual(26 * 1.3 + 24); + expect(deriveThreadArrangementSizing({ baseFontSize: 16, fontScale: 1 })).toEqual({ + rowHeight: 56, + headerHeight: 48, + }); + }); +}); + describe("thread work-log text sizing", () => { it.each([ { baseFontSize: 16, fontScale: 1.25, estimatedRowHeight: 28 }, diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index ccff7fdf6c890..4155d834ba24d 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -19,6 +19,21 @@ export const ANDROID_READING_MAX_WIDTH = 640; // min-h-8 uses the 14px rem configured in metro.config.js. export const THREAD_WORK_ROW_MIN_HEIGHT = 28; +export function deriveThreadArrangementSizing(input: { + readonly baseFontSize: number; + readonly fontScale: number; +}) { + const bodyLineHeight = scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.body, input.baseFontSize); + const headerLineHeight = scaledTypographyLineHeight( + MOBILE_TYPOGRAPHY.footnote, + input.baseFontSize, + ); + return { + rowHeight: Math.max(56, Math.ceil(2 * bodyLineHeight * input.fontScale) + 10), + headerHeight: Math.max(48, Math.ceil(headerLineHeight * input.fontScale) + 24), + }; +} + export function deriveThreadWorkLogSizing(input: { readonly baseFontSize: number; readonly fontScale: number;