From 4ee44a1396951e99a6ed681924d9e85a822f29ad Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 00:24:26 -0500 Subject: [PATCH 1/3] fix(mobile): fit large titles in thread arrangement rows Size rows and drag geometry from app typography and system font scaling. Co-Authored-By: GPT-6.1 Sol --- .../threads/ThreadArrangementSheet.tsx | 30 +++++++++++-------- apps/mobile/src/lib/layout.test.ts | 14 +++++++++ apps/mobile/src/lib/layout.ts | 12 ++++++++ 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx b/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx index 77ecc530b90b7..10708b2d91f3b 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 { useSafeAreaInsets } 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 { @@ -30,9 +31,8 @@ import { } from "./threadOrder"; import { getThreadListV2OrderedSection, threadListInboxReturns } from "./threadListV2"; 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; @@ -150,6 +150,12 @@ function DragHandle(props: { export function ThreadArrangementSheet(props: { onClose: () => void }) { const insets = useSafeAreaInsets(); + 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); @@ -230,16 +236,16 @@ export 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 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; } } return result; - }, [sections, expanded]); + }, [sections, expanded, rowHeight, headerHeight]); const list = useRef>(null); const geometry = useRef({ height: 0, offset: 0 }); const drag = useRef(null); @@ -256,7 +262,7 @@ export 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("|"); useEffect(() => { stop(); @@ -274,7 +280,7 @@ export 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 +322,7 @@ export 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, }; @@ -531,7 +537,7 @@ export 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 }); + // The same dimensions feed FlatList offsets, drop targets and the floating preview. + 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 8b1e344826e60..9efeecbdf49ef 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -20,6 +20,18 @@ 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; From 426f97651f2f5594c1ee6b984d4e5a093125f281 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Thu, 22 Oct 2026 00:39:06 -0500 Subject: [PATCH 2/3] chore(mobile): format arrangement sizing changes Co-Authored-By: GPT-6.1 Sol --- apps/mobile/src/features/threads/ThreadArrangementSheet.tsx | 4 +++- apps/mobile/src/lib/layout.ts | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx b/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx index 10708b2d91f3b..c90e078e62621 100644 --- a/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx +++ b/apps/mobile/src/features/threads/ThreadArrangementSheet.tsx @@ -262,7 +262,9 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) { setPreview(null); } const orderVersion = rows - .map((row) => `${row.key}:${row.height}:${row.thread?.pinOrderKey}:${row.thread?.activeOrderKey}`) + .map( + (row) => `${row.key}:${row.height}:${row.thread?.pinOrderKey}:${row.thread?.activeOrderKey}`, + ) .join("|"); useEffect(() => { stop(); diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index 9efeecbdf49ef..0b24f22245533 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -25,7 +25,10 @@ export function deriveThreadArrangementSizing(input: { readonly fontScale: number; }) { const bodyLineHeight = scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.body, input.baseFontSize); - const headerLineHeight = scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.footnote, 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), From 73c7e536fbd73cfb6af572aaaffe9616b5348b85 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 05:00:29 +0000 Subject: [PATCH 3/3] refactor(mobile): trim redundant arrangement sizing comments --- apps/mobile/src/lib/layout.test.ts | 1 - apps/mobile/src/lib/layout.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/apps/mobile/src/lib/layout.test.ts b/apps/mobile/src/lib/layout.test.ts index 5ace35809d660..0eaed72c1979e 100644 --- a/apps/mobile/src/lib/layout.test.ts +++ b/apps/mobile/src/lib/layout.test.ts @@ -13,7 +13,6 @@ import { describe("thread arrangement text sizing", () => { it("fits two title lines and section labels with combined app and system text scaling", () => { const sizing = deriveThreadArrangementSizing({ baseFontSize: 22, fontScale: 1.3 }); - // The same dimensions feed FlatList offsets, drop targets and the floating preview. expect(sizing.rowHeight).toBeGreaterThanOrEqual(2 * 32 * 1.3 + 10); expect(sizing.headerHeight).toBeGreaterThanOrEqual(26 * 1.3 + 24); expect(deriveThreadArrangementSizing({ baseFontSize: 16, fontScale: 1 })).toEqual({ diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index d2a172f754848..728da9f0168d8 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -28,8 +28,6 @@ export function deriveThreadArrangementSizing(input: { MOBILE_TYPOGRAPHY.footnote, input.baseFontSize, ); - // Two body lines and one footnote line plus the padding the original fixed rows had: - // at the default text size these are exactly the old 56pt rows and 48pt headers. return { rowHeight: Math.max(56, Math.ceil(2 * bodyLineHeight * input.fontScale) + 10), headerHeight: Math.max(48, Math.ceil(headerLineHeight * input.fontScale) + 24),