| 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,6 +151,11 @@ export const COMPOSER_LAYOUT_TRANSITION = |
| 151 | ? undefined | 151 | ? undefined |
| 152 | : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS).reduceMotion(ReduceMotion.System); | 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 | const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface); | 159 | const AnimatedGlassSurface = Animated.createAnimatedComponent(GlassSurface); |
| 155 | 160 | ||
| 156 | export function ComposerSurface(props: { | 161 | export 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.View | 632 | <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 | <ComposerAttachmentStrip | 637 | <ComposerAttachmentStrip |
apps/mobile/src/features/threads/ThreadFeed.tsx+4| ... | @@ -178,6 +178,10 @@ import { | ... | @@ -178,6 +178,10 @@ import { |
| 178 | import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize"; | 178 | import { MARKDOWN_IMAGE_MAX_WIDTH, resolveMarkdownImageDisplaySize } from "./markdownImageSize"; |
| 179 | 179 | ||
| 180 | const WIDE_MARKDOWN_BLOCK_OPTIONS = { | 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 | includeOrderedLists: Platform.OS === "android", | 185 | includeOrderedLists: Platform.OS === "android", |
| 182 | } as const; | 186 | } as const; |
| 183 | 187 |
apps/mobile/src/lib/wideMarkdownBlocks.test.ts+8| ... | @@ -41,6 +41,14 @@ describe("hasWideMarkdownBlock", () => { | ... | @@ -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 | 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 definite | 2 | * 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 their | 6 | * Fenced code blocks and tables report an intrinsic width equal to their |
| 6 | * widest line, which is effectively unbounded. A user bubble sizes itself | 7 | * 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]?/; |
| 31 | 32 | ||
| 32 | export interface WideMarkdownBlockOptions { | 33 | export interface WideMarkdownBlockOptions { |
| 33 | readonly includeOrderedLists?: boolean; | 34 | readonly includeOrderedLists?: boolean; |
| 35 | readonly includeBlockquotes?: boolean; | ||
| 34 | } | 36 | } |
| 35 | 37 | ||
| 36 | function stripBlockquotePrefixes(line: string): string { | 38 | function 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 | } |
| 43 | 45 | ||
| 46 | function hasBlockquote(text: string): boolean { | ||
| 47 | return text.split("\n").some((line) => BLOCKQUOTE_PREFIX.test(line)); | ||
| 48 | } | ||
| 49 | |||
| 44 | function hasOrderedListItem(text: string): boolean { | 50 | function hasOrderedListItem(text: string): boolean { |
| 45 | let previousNonEmptyLine: string | null = null; | 51 | let previousNonEmptyLine: string | null = null; |
| 46 | 52 | ||
| ... | @@ -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 | } |