Skip to content

Commit cced7aa

Browse files
fix(mobile): treat the feed width as a bound and let the decoded size win
A list item or blockquote indents its column below the feed width, so the measured width takes over once the row has laid out. The decoded size wins over the server's header hint so a wrong hint cannot stick. The padding constants use the mobile 14px rem. Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 3aad878 commit cced7aa

2 files changed

Lines changed: 19 additions & 9 deletions

File tree

‎apps/mobile/src/features/threads/ThreadFeed.tsx‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ function formatMessageTime(input: string): string {
207207
// text fits at the current font settings. Larger accessibility text is measured.
208208
const TURN_FOLD_HEIGHT = 42; // min-h-11 (38.5) + mb-1 (3.5), with the mobile 14px rem
209209
const THREAD_FEED_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS);
210+
// Tailwind spacing on the mobile 14px rem: px-3.5 on the user bubble, px-1 on
211+
// assistant rows. Images size their frame from these before their own layout.
212+
const USER_BUBBLE_HORIZONTAL_PADDING = 3.5 * 3.5;
213+
const ASSISTANT_ROW_HORIZONTAL_PADDING = 3.5;
210214
// Let neighboring rows move out of the new rows' space before showing their text.
211215
const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.delay(
212216
THREAD_DISCLOSURE_TRANSITION_MS,
@@ -1455,7 +1459,9 @@ function renderFeedEntry(
14551459
}}
14561460
>
14571461
{message.text.trim().length > 0 ? (
1458-
<MarkdownImageAvailableWidthContext value={props.userBubbleMaxWidth - 28}>
1462+
<MarkdownImageAvailableWidthContext
1463+
value={props.userBubbleMaxWidth - USER_BUBBLE_HORIZONTAL_PADDING * 2}
1464+
>
14591465
<UserMessageContent
14601466
text={renderedText}
14611467
markdownStyles={styles}
@@ -1949,8 +1955,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
19491955
});
19501956
const contentWidth = Math.max(0, viewportWidth - contentHorizontalPadding * 2);
19511957
const userBubbleMaxWidth = contentWidth * 0.85;
1952-
// Assistant rows sit inside px-1.
1953-
const markdownContentWidth = Math.max(0, contentWidth - 8);
1958+
const markdownContentWidth = Math.max(0, contentWidth - ASSISTANT_ROW_HORIZONTAL_PADDING * 2);
19541959
const reviewCommentBubbleWidth = Math.min(Math.max(280, contentWidth * 0.85), contentWidth);
19551960
const insets = useSafeAreaInsets();
19561961
const topContentInset = props.contentTopInset ?? insets.top + IOS_NAV_BAR_HEIGHT;

‎apps/mobile/src/features/threads/ThreadMarkdownImage.tsx‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import {
2525
* Width the feed lays markdown out in. The feed already knows this from its
2626
* viewport, so an image can size its frame on the first render instead of
2727
* waiting for its own onLayout, which would change the row's height once
28-
* more after the list has positioned the rows below it.
28+
* more after the list has positioned the rows below it. It is an upper
29+
* bound: a list item or blockquote indents its column, and the measured
30+
* width takes over once it is known.
2931
*/
3032
export const MarkdownImageAvailableWidthContext = createContext(0);
3133

@@ -43,7 +45,10 @@ export function ThreadMarkdownImageView(props: {
4345
const mediaActions = useMediaActions(props.actionsSource);
4446
const contextWidth = useContext(MarkdownImageAvailableWidthContext);
4547
const [measuredWidth, setMeasuredWidth] = useState(0);
46-
const availableWidth = contextWidth > 0 ? contextWidth : measuredWidth;
48+
const availableWidth =
49+
measuredWidth > 0 && contextWidth > 0
50+
? Math.min(contextWidth, measuredWidth)
51+
: contextWidth || measuredWidth;
4752
const [decodedSize, setDecodedSize] = useState<{ width: number; height: number } | null>(null);
4853
const [failedUri, setFailedUri] = useState<string | null>(null);
4954

@@ -55,7 +60,9 @@ export function ThreadMarkdownImageView(props: {
5560
setFailedUri(null);
5661
}, [props.uri]);
5762

58-
const sourceSize = props.knownSize ?? decodedSize;
63+
// The decoded size is what the platform actually drew, so it wins over the
64+
// server's header hint once it exists.
65+
const sourceSize = decodedSize ?? props.knownSize ?? null;
5966
const displaySize: MarkdownImageDisplaySize | null =
6067
sourceSize === null || availableWidth <= 0
6168
? null
@@ -71,9 +78,7 @@ export function ThreadMarkdownImageView(props: {
7178

7279
return (
7380
<View
74-
onLayout={
75-
contextWidth > 0 ? undefined : (event) => setMeasuredWidth(event.nativeEvent.layout.width)
76-
}
81+
onLayout={(event) => setMeasuredWidth(event.nativeEvent.layout.width)}
7782
style={{ alignSelf: "stretch", gap: 6 }}
7883
>
7984
{props.uri === null || failed ? (

0 commit comments

Comments
 (0)