authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-02 01:56:39-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 01:56:39-07:00
log7e9d5a7efa70f92f91d960a4f50243ba44d805da
tree74c5b7f6d5b289d7a7f1997c60f1b3ceb9bf998d
parent5b7d72aad14ed37e8e5e4c02a6d49814bfe528ac
signature Signed by PGP key B5690EEEBB952194

fix(mobile): prevent message and composer overlap (#9195)


4 files changed, 31 insertions(+), 5 deletions(-)

apps/mobile/src/features/threads/ThreadComposer.tsx+8-3
......@@ -151,6 +151,11 @@ export const COMPOSER_LAYOUT_TRANSITION =
151151 ? undefined
152152 : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS).reduceMotion(ReduceMotion.System);
153153
154const COMPOSER_ATTACHMENT_ENTERING =
155 Platform.OS === "android"
156 ? FadeIn.duration(160)
157 : FadeIn.delay(COMPOSER_TRANSITION_DURATION_MS).duration(160).reduceMotion(ReduceMotion.System);
158
154159const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface);
155160
156161export function ComposerSurface(props: {
......@@ -623,10 +628,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
623628 onPickFiles={props.onPickDraftFiles}
624629 />
625630 ) : null}
626 {isExpanded ? (
631 {isExpanded && props.draftAttachments.length > 0 ? (
627632 <Animated.View
628 className={props.draftAttachments.length > 0 ? "px-[14px] pb-2.5" : undefined}
629 entering={FadeIn.duration(160)}
633 className="px-[14px] pb-2.5"
634 entering={COMPOSER_ATTACHMENT_ENTERING}
630635 exiting={FadeOut.duration(120)}
631636 >
632637 <ComposerAttachmentStrip
apps/mobile/src/features/threads/ThreadFeed.tsx+4
......@@ -178,6 +178,10 @@ import {
178178import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize";
179179
180180const WIDE_MARKDOWN_BLOCK_OPTIONS = {
181 // Native iOS blockquotes and adjacent selectable text are separate layout
182 // chunks. Giving their shrink-to-fit bubble a definite width keeps both
183 // chunks measured against the width at which UIKit draws them.
184 includeBlockquotes: Platform.OS === "ios",
181185 includeOrderedLists: Platform.OS === "android",
182186} as const;
183187
apps/mobile/src/lib/wideMarkdownBlocks.test.ts+8
......@@ -41,6 +41,14 @@ describe("hasWideMarkdownBlock", () => {
4141 );
4242 });
4343
44 it("detects blockquotes only when the native renderer needs width pinning", () => {
45 expect(hasWideMarkdownBlock("> quoted", { includeBlockquotes: true })).toBe(true);
46 expect(hasWideMarkdownBlock(" > quoted", { includeBlockquotes: true })).toBe(true);
47 expect(hasWideMarkdownBlock("> quoted")).toBe(false);
48 expect(hasWideMarkdownBlock("prose > quoted", { includeBlockquotes: true })).toBe(false);
49 expect(hasWideMarkdownBlock(" > indented code", { includeBlockquotes: true })).toBe(false);
50 });
51
4452 it("detects GFM tables", () => {
4553 expect(hasWideMarkdownBlock("| a | b |\n| --- | --- |\n| 1 | 2 |")).toBe(true);
4654 expect(hasWideMarkdownBlock("a | b\n:-- | --:\n1 | 2")).toBe(true);
apps/mobile/src/lib/wideMarkdownBlocks.ts+11-2
......@@ -1,6 +1,7 @@
11/**
2 * Detects markdown that the JS renderer draws as a block requiring a definite
3 * user-bubble width — fenced code blocks, GFM tables, and ordered lists.
2 * Detects markdown that the renderer draws as a block requiring a definite
3 * user-bubble width: fenced code blocks, GFM tables, ordered lists, and
4 * blockquotes when requested by the caller.
45 *
56 * Fenced code blocks and tables report an intrinsic width equal to their
67 * widest line, which is effectively unbounded. A user bubble sizes itself
......@@ -31,6 +32,7 @@ const BLOCKQUOTE_PREFIX = /^ {0,3}>[ \t]?/;
3132
3233export interface WideMarkdownBlockOptions {
3334 readonly includeOrderedLists?: boolean;
35 readonly includeBlockquotes?: boolean;
3436}
3537
3638function stripBlockquotePrefixes(line: string): string {
......@@ -41,6 +43,10 @@ function stripBlockquotePrefixes(line: string): string {
4143 return content;
4244}
4345
46function hasBlockquote(text: string): boolean {
47 return text.split("\n").some((line) => BLOCKQUOTE_PREFIX.test(line));
48}
49
4450function hasOrderedListItem(text: string): boolean {
4551 let previousNonEmptyLine: string | null = null;
4652
......@@ -80,6 +86,9 @@ export function hasWideMarkdownBlock(
8086 if (FENCED_CODE_BLOCK.test(text)) {
8187 return true;
8288 }
89 if (options.includeBlockquotes === true && hasBlockquote(text)) {
90 return true;
91 }
8392 if (options.includeOrderedLists !== false && hasOrderedListItem(text)) {
8493 return true;
8594 }