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 =...@@ -151,6 +151,11 @@ export const COMPOSER_LAYOUT_TRANSITION =
151 ? undefined151 ? undefined
152 : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS).reduceMotion(ReduceMotion.System);152 : 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
154const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface);159const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface);
155160
156export function ComposerSurface(props: {161export function ComposerSurface(props: {
...@@ -623,10 +628,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer...@@ -623,10 +628,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer
623 onPickFiles={props.onPickDraftFiles}628 onPickFiles={props.onPickDraftFiles}
624 />629 />
625 ) : null}630 ) : null}
626 {isExpanded ? (631 {isExpanded && props.draftAttachments.length > 0 ? (
627 <Animated.View632 <Animated.View
628 className={props.draftAttachments.length > 0 ? "px-[14px] pb-2.5" : undefined}633 className="px-[14px] pb-2.5"
629 entering={FadeIn.duration(160)}634 entering={COMPOSER_ATTACHMENT_ENTERING}
630 exiting={FadeOut.duration(120)}635 exiting={FadeOut.duration(120)}
631 >636 >
632 <ComposerAttachmentStrip637 <ComposerAttachmentStrip
apps/mobile/src/features/threads/ThreadFeed.tsx+4
...@@ -178,6 +178,10 @@ import {...@@ -178,6 +178,10 @@ import {
178import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize";178import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize";
179179
180const WIDE_MARKDOWN_BLOCK_OPTIONS = {180const 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",
181 includeOrderedLists: Platform.OS === "android",185 includeOrderedLists: Platform.OS === "android",
182} as const;186} as const;
183187
apps/mobile/src/lib/wideMarkdownBlocks.test.ts+8
...@@ -41,6 +41,14 @@ describe("hasWideMarkdownBlock", () => {...@@ -41,6 +41,14 @@ describe("hasWideMarkdownBlock", () => {
41 );41 );
42 });42 });
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
44 it("detects GFM tables", () => {52 it("detects GFM tables", () => {
45 expect(hasWideMarkdownBlock("| a | b |\n| --- | --- |\n| 1 | 2 |")).toBe(true);53 expect(hasWideMarkdownBlock("| a | b |\n| --- | --- |\n| 1 | 2 |")).toBe(true);
46 expect(hasWideMarkdownBlock("a | b\n:-- | --:\n1 | 2")).toBe(true);54 expect(hasWideMarkdownBlock("a | b\n:-- | --:\n1 | 2")).toBe(true);
apps/mobile/src/lib/wideMarkdownBlocks.ts+11-2
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1/**1/**
2 * Detects markdown that the JS renderer draws as a block requiring a definite2 * Detects markdown that the renderer draws as a block requiring a definite
3 * user-bubble width — fenced code blocks, GFM tables, and ordered lists.3 * user-bubble width: fenced code blocks, GFM tables, ordered lists, and
4 * blockquotes when requested by the caller.
4 *5 *
5 * Fenced code blocks and tables report an intrinsic width equal to their6 * Fenced code blocks and tables report an intrinsic width equal to their
6 * widest line, which is effectively unbounded. A user bubble sizes itself7 * widest line, which is effectively unbounded. A user bubble sizes itself
...@@ -31,6 +32,7 @@ const BLOCKQUOTE_PREFIX = /^ {0,3}>[ \t]?/;...@@ -31,6 +32,7 @@ const BLOCKQUOTE_PREFIX = /^ {0,3}>[ \t]?/;
3132
32export interface WideMarkdownBlockOptions {33export interface WideMarkdownBlockOptions {
33 readonly includeOrderedLists?: boolean;34 readonly includeOrderedLists?: boolean;
35 readonly includeBlockquotes?: boolean;
34}36}
3537
36function stripBlockquotePrefixes(line: string): string {38function stripBlockquotePrefixes(line: string): string {
...@@ -41,6 +43,10 @@ function stripBlockquotePrefixes(line: string): string {...@@ -41,6 +43,10 @@ function stripBlockquotePrefixes(line: string): string {
41 return content;43 return content;
42}44}
4345
46function hasBlockquote(text: string): boolean {
47 return text.split("\n").some((line) => BLOCKQUOTE_PREFIX.test(line));
48}
49
44function hasOrderedListItem(text: string): boolean {50function hasOrderedListItem(text: string): boolean {
45 let previousNonEmptyLine: string | null = null;51 let previousNonEmptyLine: string | null = null;
4652
...@@ -80,6 +86,9 @@ export function hasWideMarkdownBlock(...@@ -80,6 +86,9 @@ export function hasWideMarkdownBlock(
80 if (FENCED_CODE_BLOCK.test(text)) {86 if (FENCED_CODE_BLOCK.test(text)) {
81 return true;87 return true;
82 }88 }
89 if (options.includeBlockquotes === true && hasBlockquote(text)) {
90 return true;
91 }
83 if (options.includeOrderedLists !== false && hasOrderedListItem(text)) {92 if (options.includeOrderedLists !== false && hasOrderedListItem(text)) {
84 return true;93 return true;
85 }94 }