| author | |
| committer | |
| log | 7e9d5a7efa70f92f91d960a4f50243ba44d805da |
| tree | 74c5b7f6d5b289d7a7f1997c60f1b3ceb9bf998d |
| parent | 5b7d72aad14ed37e8e5e4c02a6d49814bfe528ac |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 151 | ? undefined |
| 152 | 152 | : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS).reduceMotion(ReduceMotion.System); |
| 153 | 153 | |
| 154 | const COMPOSER_ATTACHMENT_ENTERING = | |
| 155 | Platform.OS === "android" | |
| 156 | ? FadeIn.duration(160) | |
| 157 | : FadeIn.delay(COMPOSER_TRANSITION_DURATION_MS).duration(160).reduceMotion(ReduceMotion.System); | |
| 158 | ||
| 154 | 159 | const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface); |
| 155 | 160 | |
| 156 | 161 | export function ComposerSurface(props: { |
| ... | ... | @@ -623,10 +628,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 623 | 628 | onPickFiles={props.onPickDraftFiles} |
| 624 | 629 | /> |
| 625 | 630 | ) : null} |
| 626 | {isExpanded ? ( | |
| 631 | {isExpanded && props.draftAttachments.length > 0 ? ( | |
| 627 | 632 | <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} | |
| 630 | 635 | exiting={FadeOut.duration(120)} |
| 631 | 636 | > |
| 632 | 637 | <ComposerAttachmentStrip |
apps/mobile/src/features/threads/ThreadFeed.tsx+4| ... | ... | @@ -178,6 +178,10 @@ import { |
| 178 | 178 | import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize"; |
| 179 | 179 | |
| 180 | 180 | const 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 | 185 | includeOrderedLists: Platform.OS === "android", |
| 182 | 186 | } as const; |
| 183 | 187 |
apps/mobile/src/lib/wideMarkdownBlocks.test.ts+8| ... | ... | @@ -41,6 +41,14 @@ describe("hasWideMarkdownBlock", () => { |
| 41 | 41 | ); |
| 42 | 42 | }); |
| 43 | 43 | |
| 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 | 52 | it("detects GFM tables", () => { |
| 45 | 53 | expect(hasWideMarkdownBlock("| a | b |\n| --- | --- |\n| 1 | 2 |")).toBe(true); |
| 46 | 54 | expect(hasWideMarkdownBlock("a | b\n:-- | --:\n1 | 2")).toBe(true); |
apps/mobile/src/lib/wideMarkdownBlocks.ts+11-2| ... | ... | @@ -1,6 +1,7 @@ |
| 1 | 1 | /** |
| 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. | |
| 4 | 5 | * |
| 5 | 6 | * Fenced code blocks and tables report an intrinsic width equal to their |
| 6 | 7 | * widest line, which is effectively unbounded. A user bubble sizes itself |
| ... | ... | @@ -31,6 +32,7 @@ const BLOCKQUOTE_PREFIX = /^ {0,3}>[ \t]?/; |
| 31 | 32 | |
| 32 | 33 | export interface WideMarkdownBlockOptions { |
| 33 | 34 | readonly includeOrderedLists?: boolean; |
| 35 | readonly includeBlockquotes?: boolean; | |
| 34 | 36 | } |
| 35 | 37 | |
| 36 | 38 | function stripBlockquotePrefixes(line: string): string { |
| ... | ... | @@ -41,6 +43,10 @@ function stripBlockquotePrefixes(line: string): string { |
| 41 | 43 | return content; |
| 42 | 44 | } |
| 43 | 45 | |
| 46 | function hasBlockquote(text: string): boolean { | |
| 47 | return text.split("\n").some((line) => BLOCKQUOTE_PREFIX.test(line)); | |
| 48 | } | |
| 49 | ||
| 44 | 50 | function hasOrderedListItem(text: string): boolean { |
| 45 | 51 | let previousNonEmptyLine: string | null = null; |
| 46 | 52 | |
| ... | ... | @@ -80,6 +86,9 @@ export function hasWideMarkdownBlock( |
| 80 | 86 | if (FENCED_CODE_BLOCK.test(text)) { |
| 81 | 87 | return true; |
| 82 | 88 | } |
| 89 | if (options.includeBlockquotes === true && hasBlockquote(text)) { | |
| 90 | return true; | |
| 91 | } | |
| 83 | 92 | if (options.includeOrderedLists !== false && hasOrderedListItem(text)) { |
| 84 | 93 | return true; |
| 85 | 94 | } |