Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/scripts/check-px-text.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const rules = [
// glyph is a fixed display size sized to its avatar box (not readable message
// text), so it stays as the lone documented `text-[6rem]` literal.
const overrides = new Set([
"src/features/settings/ui/ProfileSettingsCard.tsx:584",
"src/features/settings/ui/ProfileSettingsCard.tsx:619",
"src/features/onboarding/ui/AvatarStep.tsx:89",
]);

Expand Down
47 changes: 36 additions & 11 deletions desktop/src/app/useWebviewZoomShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { getCurrentWebview } from "@tauri-apps/api/webview";

import { hasPrimaryShortcutModifier } from "@/shared/lib/platform";

const DEFAULT_ZOOM_FACTOR = 1;
const DEFAULT_ZOOM_FACTOR = 1.1;
const DEFAULT_WEBVIEW_ZOOM_FACTOR = 1;
const MIN_ZOOM_FACTOR = 0.75;
const MAX_ZOOM_FACTOR = 1.5;
const ZOOM_STEP = 0.1;
Expand Down Expand Up @@ -61,28 +62,49 @@ function getNextZoomFactor(action: ZoomAction, zoomFactor: number) {
return Math.max(roundZoomFactor(zoomFactor - ZOOM_STEP), MIN_ZOOM_FACTOR);
}

function readStoredZoomFactor() {
type StoredZoomFactor = {
zoomFactor: number;
hasStoredPreference: boolean;
};

type ApplyTextScaleOptions = {
persistPreference?: boolean;
};

function readStoredZoomFactor(): StoredZoomFactor {
const raw = window.localStorage.getItem(TEXT_SCALE_STORAGE_KEY);
if (!raw) {
return DEFAULT_ZOOM_FACTOR;
return { zoomFactor: DEFAULT_ZOOM_FACTOR, hasStoredPreference: false };
}

const parsed = Number.parseFloat(raw);
if (!Number.isFinite(parsed)) {
return DEFAULT_ZOOM_FACTOR;
return { zoomFactor: DEFAULT_ZOOM_FACTOR, hasStoredPreference: false };
}

return Math.min(Math.max(parsed, MIN_ZOOM_FACTOR), MAX_ZOOM_FACTOR);
return {
zoomFactor: Math.min(Math.max(parsed, MIN_ZOOM_FACTOR), MAX_ZOOM_FACTOR),
hasStoredPreference: true,
};
}

function applyTextScale(zoomFactor: number) {
if (zoomFactor === DEFAULT_ZOOM_FACTOR) {
function applyTextScale(
zoomFactor: number,
{
persistPreference = zoomFactor !== DEFAULT_ZOOM_FACTOR,
}: ApplyTextScaleOptions = {},
) {
if (zoomFactor === DEFAULT_WEBVIEW_ZOOM_FACTOR) {
document.documentElement.style.fontSize = "";
} else {
document.documentElement.style.fontSize = `${BASE_FONT_SIZE_PX * zoomFactor}px`;
}

if (!persistPreference) {
window.localStorage.removeItem(TEXT_SCALE_STORAGE_KEY);
return;
}

document.documentElement.style.fontSize = `${BASE_FONT_SIZE_PX * zoomFactor}px`;
window.localStorage.setItem(TEXT_SCALE_STORAGE_KEY, String(zoomFactor));
}

Expand All @@ -91,13 +113,16 @@ export function useWebviewZoomShortcuts() {

React.useLayoutEffect(() => {
const webview = getCurrentWebview();
const storedZoomFactor = readStoredZoomFactor();
const { zoomFactor: storedZoomFactor, hasStoredPreference } =
readStoredZoomFactor();

zoomFactorRef.current = storedZoomFactor;
applyTextScale(storedZoomFactor);
applyTextScale(storedZoomFactor, {
persistPreference: hasStoredPreference,
});

// Keep the webview coordinate system stable; only text should scale.
void webview.setZoom(DEFAULT_ZOOM_FACTOR).catch((error) => {
void webview.setZoom(DEFAULT_WEBVIEW_ZOOM_FACTOR).catch((error) => {
console.error("Failed to reset webview zoom", error);
});

Expand Down
6 changes: 3 additions & 3 deletions desktop/src/features/messages/lib/rowHeightEstimate.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ test("estimateRowHeight: bare URL line adds a preview card", () => {
assert.ok(withUrl > withoutUrl + 50, `url ${withUrl} vs ${withoutUrl}`);
});

test("timelineRowReserveStyle: message item yields containIntrinsicSize", () => {
test("timelineRowReserveStyle: message item yields rem containIntrinsicSize", () => {
const style = timelineRowReserveStyle({
kind: "message",
key: "k",
entry: { message: msg({ body: "hi" }), summary: null },
});
assert.match(String(style.containIntrinsicSize), /^auto \d+px$/);
assert.match(String(style.containIntrinsicSize), /^auto \d+(?:\.\d+)?rem$/);
});

test("timelineRowReserveStyle: divider is short fixed height", () => {
Expand All @@ -105,5 +105,5 @@ test("timelineRowReserveStyle: divider is short fixed height", () => {
key: "k",
headingTimestamp: 0,
});
assert.equal(style.containIntrinsicSize, "auto 32px");
assert.equal(style.containIntrinsicSize, "auto 2rem");
});
2 changes: 1 addition & 1 deletion desktop/src/features/messages/lib/rowHeightEstimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,5 @@ export function timelineRowReserveStyle(
: item.kind === "system"
? estimateRowHeight(item.entry.message)
: DIVIDER_HEIGHT;
return { containIntrinsicSize: `auto ${height}px` };
return { containIntrinsicSize: `auto ${height / 16}rem` };
}
12 changes: 11 additions & 1 deletion desktop/src/features/messages/ui/TimelineMessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,18 @@ function SystemRow({
onToggleReaction?: TimelineMessageListProps["onToggleReaction"];
profiles?: UserProfileLookup;
}) {
// `data-message-id` is load-bearing: useAnchoredScroll's computeAnchor walks
// `[data-message-id]` rows to decide whether the user is anchored mid-history
// or at the bottom. A young channel can be scrollable while its only rows are
// system events (channel_created / member_joined) — without an id here the
// walk finds nothing, falls through to "at-bottom", and a user who scrolled
// up never gets the scroll-to-latest pill and gets yanked to the bottom by
// the next arrival.
return (
<div className="flex flex-col gap-1 pb-2.5">
<div
className="flex flex-col gap-1 pb-2.5"
data-message-id={entry.message.id}
>
<SystemMessageRow
message={entry.message}
currentPubkey={currentPubkey}
Expand Down
Loading
Loading