Skip to content

feat(desktop): bump default text scale#1455

Draft
tellaho wants to merge 5 commits into
mainfrom
tho/default-zoom-bump
Draft

feat(desktop): bump default text scale#1455
tellaho wants to merge 5 commits into
mainfrom
tho/default-zoom-bump

Conversation

@tellaho

@tellaho tellaho commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Category: improvement
User Impact: Desktop now opens with text and UI chrome slightly larger by default for better readability.
Problem: The previous default zoom made the desktop app feel a little too dense, especially in message-heavy views where text and controls sit close together. Bumping the default text scale exposed visual tests that were hard-coded to the old 16px baseline, plus real scroll/interaction regressions around profile restore, custom-section drag reorder, scroll anchoring, and young-channel scroll-to-latest behavior.
Solution: Raise the default text scale from 1.0 to 1.1 while keeping webview coordinates stable, preserve custom zoom persistence and keyboard shortcuts, rebase affected visual expectations around the active root scale, and fix the app-side regressions exposed by the larger default. The latest re-gate confirmed root font is 17.6px under the harness, all PR checks are green at e9ad63d8, and no scroll/test guard thresholds were weakened.

File changes

desktop/src/app/useWebviewZoomShortcuts.ts
Updates the default text scale to 1.1 while preserving persisted user overrides and existing zoom shortcut behavior. The underlying webview zoom remains 1.0 so pointer coordinates and browser geometry stay stable.

desktop/src/features/messages/lib/rowHeightEstimate.ts
Expresses timeline intrinsic row reserves in rem so reserved heights scale with the root text size.

desktop/src/features/messages/lib/rowHeightEstimate.test.mjs
Updates unit expectations for the new rem-based timeline reserve output.

desktop/src/features/messages/ui/TimelineMessageList.tsx
Adds data-message-id to system rows so young channels containing only system events can still participate in scroll anchoring and correctly show the scroll-to-latest pill when the user scrolls away from bottom.

desktop/src/features/messages/ui/useAnchoredScroll.ts
Fixes two scale-exposed scroll-anchor bugs app-side: mid-history anchors are re-pinned through delayed content-visibility size realization, and bottom settling now uses a requestAnimationFrame loop that can issue the final corrective pin even when smooth scrolling stops short of the true floor without another scroll event.

desktop/src/features/settings/ui/ProfileSettingsCard.tsx
Fixes the profile scroll-restore regression: snapshots the settings scroller position when the avatar editor opens and re-applies it on close, so the pre-edit view is restored exactly instead of landing ~69px lower after the tall editor collapses.

desktop/src/features/settings/ui/SettingsView.tsx
Tags the settings content scroller with data-settings-scroller so the profile card can find it for the scroll snapshot/restore.

desktop/scripts/check-px-text.mjs
Updates the line number of the existing documented text-[6rem] avatar-glyph exception after the ProfileSettingsCard edits shifted it.

desktop/tests/helpers/css.ts
Adds helpers for reading the active root font scale and checking scaled radii where the UI intentionally grows with zoom.

desktop/tests/e2e/channels.spec.ts
Rebases intro card/inset geometry assertions around the active root scale.

desktop/tests/e2e/custom-emoji.spec.ts
Rebases inline reaction button dimensions around the active root scale while preserving the existing interaction checks. Note: the reaction-button height check is floor-based rather than exact while width stays precise.

desktop/tests/e2e/file-attachment.spec.ts
Marks file-card corner radius as root-scale-dependent.

desktop/tests/e2e/image-attachment-gallery.spec.ts
Marks image thumbnail and lightbox surface radii as root-scale-dependent.

desktop/tests/e2e/messaging.spec.ts
Rebases link-preview/code-block radii and thread participant avatar sizing around the active root scale.

desktop/tests/e2e/profile.spec.ts
Updates profile editor geometry expectations for the larger default scale and reinstates the original <8px scroll-restore assertion, which now passes against the ProfileSettingsCard fix (measured drift: 0px).

desktop/tests/e2e/video-attachment.spec.ts
Marks inline video surface radius as root-scale-dependent.

desktop/tests/e2e/virtualization.spec.ts
Fixes the custom-section DnD reorder smoke, which this PR's scale bump broke: at 1.1 the sidebar overflows the 720px test viewport and Playwright's click auto-scroll leaves the section headers above the scroller clip, so raw mouse coords landed on the workspace rail and the drag never activated. dragOver now scrolls both handles into view before reading bounding boxes; the reorder still has to activate, drop, and commit for the test to pass. The app-side reorder logic itself works correctly at 1.1.

Reproduction Steps

  1. Launch the desktop app with no saved custom zoom preference.
  2. Open a message-heavy channel such as general.
  3. Confirm the default UI reads larger than the previous baseline: root text renders at 17.6px instead of 16px.
  4. Use zoom shortcuts/settings to confirm custom zoom behavior still overrides the default.
  5. Open profile avatar editing, then close it: the page restores to exactly the pre-edit scroll position (this previously drifted ~69px at 1.1; now fixed and guarded by the reinstated <8px assertion).
  6. Open the profile view fresh at 1.1 and confirm the Profile header is visible at the top of the scroller; this PR did not find a separate initial-load header cutoff beyond the editor-close drift case.
  7. Seed two custom sidebar sections and drag-reorder them: the reorder commits at the 1.1 default scale.
  8. Scroll upward while older history prepends and confirm the reading row stays pinned instead of drifting after delayed row-size realization.
  9. Use the scroll-to-latest affordance after jumping back to bottom from a mid-fetch state and confirm the timeline settles at the true floor.
  10. In a newly created channel with only system rows initially visible, scroll away from bottom and confirm the scroll-to-latest pill can mount when new messages arrive.

Screenshots/Demos

Before — default 1.0 / 16px:

Before default zoom 1.0

After — default 1.1 / 17.6px:

After default zoom 1.1

Latest re-gate timeline at 1.1:

Timeline at default zoom 1.1

Profile initial-load header at 1.1 — confirmed visible, not cut off:

Profile header visible at default zoom 1.1

Profile scroll-restore regression at 1.1 (now fixed — kept for review context):

Profile scroll restore shift at 1.1, before fix

Reference behavior at 1.0 after closing profile editor:

Reference profile scroll restore at 1.0

Validation

At e9ad63d8, Marge re-ran the full gate and confirmed every PR check is passing:

  • CI: Desktop E2E Integration (1/2 + 2/2), Desktop E2E Relay, and all PR checks green at this commit.
  • pnpm check from desktop/
  • pnpm typecheck from desktop/
  • pnpm test from desktop/ (1475 pass)
  • pnpm build from desktop/
  • pnpm exec playwright test scroll-history.spec.ts --project=smoke --repeat-each=5 (14 tests × 5 = 70/70; includes the :114 prepend-drift and :288 bottom-jump cases)
  • pnpm exec playwright test virtualization --project=smoke plus overscroll coverage (18/18 in the final gate)
  • pnpm exec playwright test timeline-no-shift.spec.ts --project=smoke (15/15 in the final gate)
  • pnpm exec playwright test profile.spec.ts --project=integration (includes the reinstated <8px scroll-restore guard; earlier gated 19/19)
  • pnpm exec playwright test custom-emoji.spec.ts --project=smoke (13/13)
  • Push hooks passed for desktop, Tauri, Rust, and mobile tests on the final fix push.

Regression accounting

Review correctly flagged that the earlier revision of this PR (a) caused the custom-section DnD smoke failure while describing it as unrelated, and (b) replaced the profile scroll-restore assertion with a weaker visibility check while calling the drift "documented". Both are now actually fixed: the DnD failure was a direct consequence of the 1.1 bump (viewport-clip interaction described above), and the ~69px profile drift is eliminated at the source in ProfileSettingsCard, with the original numeric guard restored unchanged.

The final scroll round fixed three additional app-side bugs exposed by the 1.1 scale without weakening any test guard:

  1. Older-history prepend drift: delayed content-visibility row-size realization above the viewport moved the reading row after the first re-pin. useAnchoredScroll now re-pins mid-history message anchors through those resize settles and swallows only echoes of its own anchor-holding writes. Drift measured ~31px before and ~0.16px after.
  2. Abandon-fetch bottom jump: the scroll-to-latest smooth-scroll target could be computed from stale metrics and stop ~52px above the true floor with no further scroll event. The bottom settle guard now runs outside the event stream in requestAnimationFrame, stays hands-off while motion continues, pins only once the position is stable short of the floor, and disarms at the floor, on trusted user input, or after a 2s cap.
  3. Young-channel scroll-to-latest pill: system rows lacked data-message-id, so channels showing only system events could not produce an anchor and were treated as still at bottom. SystemRow now exposes a message id for anchoring, with a load-bearing comment.

Caveats are resolved/covered: the locally observed timeline-no-shift.spec.ts:205 flake was conservative and unrelated (Marge got 15/15), and the relay-backed stream.spec.ts:455 case could not be run locally because of Docker org sign-in, but CI proves Desktop E2E Integration and Relay are green at e9ad63d8.


Coordination: Buzz channel buzz-message-timeline, thread cca5fa4c25340bf73e42688e3ad10720a98e9e7564c3d3ebf79d83a8deab012a.

npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w and others added 2 commits July 2, 2026 00:35
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho changed the title bump default desktop text scale feat(desktop): bump default text scale Jul 2, 2026
@tellaho tellaho marked this pull request as ready for review July 2, 2026 08:01
npub14vtk7pvazqrq9639qu7e560wnqtl0d53ca4gjuvq6jzf3k2el23qqlwa7f and others added 3 commits July 2, 2026 01:35
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Two regressions surfaced by the 1.1 default text scale:

- Closing the profile avatar editor stranded the settings panel ~69px
  below where the user left it: the tall editor invites scroll (browser
  focus-scroll to the Done button), and on collapse the scroller clamps
  to the shorter closed layout. ProfileSettingsCard now snapshots the
  settings scroller position when the editor opens and re-applies it on
  close, so the pre-edit view is restored exactly. The original <8px
  scroll-restore assertion in profile.spec.ts is reinstated unchanged.

- The custom-section dnd reorder smoke failed at 1.1 because the sidebar
  overflows the test viewport and Playwright's click auto-scroll leaves
  the section headers above the scroller clip; raw mouse coords then hit
  the workspace rail and the drag never activates. dragOver now scrolls
  both handles into view before reading bounding boxes — the reorder
  still has to activate, drop, and commit for the test to pass.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Three scroll-anchor gaps surfaced by the 1.1 default text scale (all
app-side; no test tolerances changed):

- Prepend drift: after the older-history re-pin, prepended rows above
  the viewport realize their content-visibility intrinsic-size
  estimates a few frames later. No scroll event fires for that shrink
  and native scroll anchoring has no anchor node above the viewport, so
  the reading row drifted ~30px (scales with root font size — 1.1
  pushed it past the 28px e2e budget). The content ResizeObserver now
  re-pins mid-history message anchors (it only handled at-bottom), and
  onScroll swallows the echoes of our own anchor-holding writes instead
  of re-capturing the drifted position as the new anchor.

- Stranded bottom jump: the scroll-to-latest pill's smooth jump
  computes its animation target from metrics at call time; rows
  realizing their true heights mid-flight make that target stale, the
  engine settles ~52px above the real floor, and then emits no further
  scroll events — an event-driven settle guard can never issue the
  final corrective write. The settle guard is now a rAF loop outside
  the event stream: hands-off while the position is moving, corrective
  pin once it holds still short of the true floor, disarmed at the
  floor, on trusted user input, or a hard time cap.

- Anchor-blind system rows: SystemRow rendered no data-message-id, so
  in a young channel whose only rows are system events computeAnchor
  found nothing and fell through to at-bottom — a user who scrolled up
  never got the scroll-to-latest pill and was yanked down by the next
  arrival. System rows now carry their message id.

Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
@tellaho tellaho marked this pull request as draft July 2, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant