Skip to content

Chat: render inline markdown in messages - #3484

Merged
feruzm merged 1 commit into
developmentfrom
feature/chat-inline-markdown
Aug 11, 2026
Merged

Chat: render inline markdown in messages#3484
feruzm merged 1 commit into
developmentfrom
feature/chat-inline-markdown

Conversation

@feruzm

@feruzm feruzm commented Aug 11, 2026

Copy link
Copy Markdown
Member

Part of #3468, scoped to the inline subset (see the issue comment for why block-level parity was deferred).

What renders now

**bold**, *italic* / _italic_, ~~strike~~, `code` — as nested <Text>. Block constructs are deliberately untouched, so a message starting with - or # still reads the way it was typed, and the existing link, mention, emoji and image handling stays on its current code path.

The design constraint

The risk is not missing formatting, it is mangling ordinary chat. Every ambiguous case resolves toward leaving text alone:

input result
file_name_here untouched — _ does not open inside a word
2 * 3 * 4 = 24 untouched — a delimiter followed by space does not open
*hello there untouched — unterminated delimiters stay literal
*** untouched — a span of only delimiters does not format
`a **b** c` code span, contents literal

URLs get a second, independent guard

Anything inside a linkify range is passed through as a raw string, so a URL is never split into styled nodes and stays tappable via <Hyperlink>. This matters because a URL containing _ could otherwise be reformatted into something that no longer resolves.

The two defences are independent, which I verified by removing the link-range guard: the underscore cases still passed on the parser rule alone, and only the a*b*c URL case failed. Both are kept.

Tests

23 new. The parser is covered directly; a separate integration spec covers the wiring, since the parser being right does not prove the renderer uses it correctly.

One regression is pinned specifically: an invalid opening delimiter must keep scanning the same rule rather than abandoning it, so 2 * 3 and *italic* still finds the real pair. I broke exactly this while removing a continue for the lint rule, and the test now catches it.

Checks

typecheck ok: 0 errors (baseline 0). Full suite 790 passed / 55 suites. Lint clean on the chat directory (0 errors; 25 pre-existing warnings unchanged).

Summary by CodeRabbit

  • New Features

    • Added rich text formatting in chat messages, including bold, italic, strikethrough, and inline code.
    • Preserved tappable links and correctly handled mentions alongside formatted text.
    • Improved handling of ambiguous, incomplete, and nested formatting syntax.
  • Tests

    • Added comprehensive coverage for formatting, links, mentions, edge cases, and performance.

Bold, italic, strikethrough and inline code, rendered as nested Text. Block
constructs are deliberately left alone: a message starting with '-' or '#'
keeps reading the way it was typed, and the existing link, mention, emoji and
image handling stays on its current code path.

The risk here is not missing formatting, it is mangling ordinary chat, so every
ambiguous case resolves toward leaving text untouched. Underscore emphasis does
not fire inside a word (snake_case survives), a delimiter followed by a space
does not open (2 * 3 * 4 survives), unterminated delimiters stay literal, and a
span whose content is only delimiters does not format.

URLs get a second, independent guard: anything inside a linkify range is passed
through as a raw string, so a URL is never split into styled nodes and stays
tappable via Hyperlink.
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: be642d87-434a-4e99-9f2e-8766e564e1c5

📥 Commits

Reviewing files that changed from the base of the PR and between c3314c9 and 8165e17.

📒 Files selected for processing (4)
  • src/screens/chats/utils/inlineMarkdown.test.ts
  • src/screens/chats/utils/inlineMarkdown.ts
  • src/screens/chats/utils/messageFormatters.tsx
  • src/screens/chats/utils/messageMarkdown.test.tsx

📝 Walkthrough

Walkthrough

The PR adds inline Markdown parsing for chat text. It supports bold, italic, strikethrough, and code spans. Chat formatting applies styles while preserving URLs and mentions. Unit and integration tests cover syntax, nesting, invalid delimiters, link handling, and performance.

Changes

Chat Markdown rendering

Layer / File(s) Summary
Inline Markdown parser
src/screens/chats/utils/inlineMarkdown.ts, src/screens/chats/utils/inlineMarkdown.test.ts
The parser recognizes valid formatting delimiters, supports nested styles and literal code spans, preserves unmatched content, and exposes parsing helpers. Tests cover syntax, invalid input, preservation, and pathological delimiter input.
Chat formatter integration
src/screens/chats/utils/messageFormatters.tsx, src/screens/chats/utils/messageMarkdown.test.tsx
Chat rendering applies Markdown styles to non-mention text and keeps linkified ranges unchanged. Tests cover URLs, mentions, plain text, and snake_case text.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Issue 3468 — This PR addresses the same mobile chat Markdown-rendering objective with a custom inline parser and formatter integration.

Poem

A rabbit found stars in the chat,
And wrapped them in bold with a pat.
Links stayed clear,
Mentions stayed near,
While code wore a neat little hat.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/chat-inline-markdown

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@feruzm
feruzm merged commit 9aa896b into development Aug 11, 2026
8 checks passed
@feruzm
feruzm deleted the feature/chat-inline-markdown branch August 11, 2026 11:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8165e17a57

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


type Style = Omit<InlineSpan, 'text'>;

const isWordChar = (ch: string | undefined) => !!ch && /[A-Za-z0-9]/.test(ch);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat all identifier characters as underscore boundaries

For identifiers containing non-ASCII letters or consecutive underscores, this ASCII-only predicate lets _ open and close inside the identifier. For example, привет_мир_тест renders мир as italic, while foo__bar__baz renders bar as italic and removes two underscores, contradicting the parser's guarantee that ordinary variable names remain intact. Use a Unicode-aware identifier predicate that also treats adjacent underscores as word characters.

Useful? React with 👍 / 👎.

if (!slice) {
return;
}
parseInlineMarkdown(slice).forEach((span, i) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve markdown state across link ranges

When a markdown pair encloses a link, each side of the link is parsed independently, so neither side can find its matching delimiter. For example, **see https://example.com/foo now** displays the literal ** and applies no bold formatting. The URL can remain a raw string for Hyperlink, but delimiter matching must span across the protected link range.

Useful? React with 👍 / 👎.

);
}
return part.text;
return renderSegmentWithMarkdown(part.text, part.start, linkRanges, textStyle, `s${index}_`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve markdown state across mention runs

When formatting encloses a mention along with surrounding text, splitting the message into mention and non-mention parts prevents the delimiters from matching. For example, ~~ignore @bob please~~ leaves both ~~ markers visible and does not strike the text because the prefix and suffix are parsed separately. Parse the formatting across the complete message while treating the mention as a styled span.

Useful? React with 👍 / 👎.

spans.push(...parse(content, style, depth + 1));
}

spans.push(...parse(text.slice(match.end), inherited, depth));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Iterate over sequential spans instead of recursing

For a long message containing many sequential valid spans such as repeated *x* , this tail-recursive call adds one stack frame per span and reruns findMatch over every remaining suffix, producing quadratic work despite the nesting-depth guard. A roughly 16 KB message can already block the JS thread for close to a second, and longer server-supplied messages can overflow the stack while rendering; process sequential matches iteratively instead.

Useful? React with 👍 / 👎.

@feruzm

feruzm commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

This merged without a review (rate limit). The inline markdown parser in src/screens/chats/utils/inlineMarkdown.ts runs on every chat message, so a look would be worthwhile even post-merge — any findings will be handled in a follow-up.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Pull request metadata and changed files =='
gh pr view 3484 --repo ecency/vision-mobile --json number,state,mergedAt,baseRefName,headRefName,files --jq '
  "state=\(.state) mergedAt=\(.mergedAt) base=\(.baseRefName) head=\(.headRefName)",
  (.files[] | "\(.path)")

echo
echo '== Candidate source and test files =='
fd -a -i 'inlineMarkdown' . || true
fd -a -i 'chat' . | head -100 || true

echo
echo '== Parser file outline =='
ast-grep outline src/screens/chats/utils/inlineMarkdown.ts --items all --view expanded 2>/dev/null || true

echo
echo '== Parser file =='
cat -n src/screens/chats/utils/inlineMarkdown.ts 2>/dev/null || true

echo
echo '== Direct parser and renderer references =='
rg -n -C 4 --glob '!node_modules/**' --glob '!dist/**' \
  '\b(parseInlineMarkdown|inlineMarkdown)\b' src 2>/dev/null || true

Length of output: 1266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Pull request metadata =='
gh pr view 3484 --repo ecency/vision-mobile \
  --json number,state,mergedAt,baseRefName,headRefName \
  --jq '"state=\(.state) mergedAt=\(.mergedAt) base=\(.baseRefName) head=\(.headRefName)"'

echo
echo '== Pull request changed files =='
gh pr view 3484 --repo ecency/vision-mobile --json files --jq '.files[].path'

echo
echo '== Candidate source and test files =='
fd -a -i 'inlineMarkdown' . || true

echo
echo '== Parser file outline =='
ast-grep outline src/screens/chats/utils/inlineMarkdown.ts --items all --view expanded 2>/dev/null || true

echo
echo '== Parser file =='
cat -n src/screens/chats/utils/inlineMarkdown.ts 2>/dev/null || true

echo
echo '== Direct parser and renderer references =='
rg -n -C 4 --glob '!node_modules/**' --glob '!dist/**' \
  '\b(parseInlineMarkdown|inlineMarkdown)\b' src 2>/dev/null || true

Length of output: 18355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Changed pull request diff =='
gh pr diff 3484 --repo ecency/vision-mobile -- \
  src/screens/chats/utils/inlineMarkdown.ts \
  src/screens/chats/utils/messageFormatters.tsx \
  src/screens/chats/utils/inlineMarkdown.test.ts \
  src/screens/chats/utils/messageMarkdown.test.tsx

echo
echo '== messageFormatters outline =='
ast-grep outline src/screens/chats/utils/messageFormatters.tsx --items all --view expanded 2>/dev/null || true

echo
echo '== Formatter range containing Markdown integration =='
sed -n '170,285p' src/screens/chats/utils/messageFormatters.tsx

echo
echo '== Parser tests =='
cat -n src/screens/chats/utils/inlineMarkdown.test.ts

echo
echo '== Renderer integration tests =='
cat -n src/screens/chats/utils/messageMarkdown.test.tsx

echo
echo '== Text component and hyperlink call sites in formatter =='
rg -n -C 6 '<(Text|Hyperlink)\b|renderTextWithBoldMentions\s*\(' \
  src/screens/chats/utils/messageFormatters.tsx

Length of output: 1355


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Changed pull request diff =='
gh pr diff 3484 --repo ecency/vision-mobile

echo
echo '== Formatter range containing Markdown integration =='
sed -n '170,285p' src/screens/chats/utils/messageFormatters.tsx

echo
echo '== Parser tests =='
cat -n src/screens/chats/utils/inlineMarkdown.test.ts

echo
echo '== Renderer integration tests =='
cat -n src/screens/chats/utils/messageMarkdown.test.tsx

echo
echo '== Text component and hyperlink call sites in formatter =='
rg -n -C 6 '<(Text|Hyperlink)\b|renderTextWithBoldMentions\s*\(' \
  src/screens/chats/utils/messageFormatters.tsx || true

Length of output: 38048


@feruzm I found one issue.

isWordChar on Line 23 accepts only ASCII letters and digits. Therefore, the underscore rule treats a non-ASCII letter as a word boundary. For example, café_test_ and тест_значение_ render the suffix as italic and remove the underscores. These are ordinary identifiers or text, not Markdown.

Use a Unicode-aware letter-or-number check for the underscore boundary logic. Add regression tests for accented Latin and Cyrillic examples. This preserves the stated rule that _ does not open inside a word.

⚠️ Action not completed

Pull request is closed.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant