Skip to content

feat(web): WhatsApp-style formatting for URL-backed phone messages - #1016

Merged
AchoArnold merged 6 commits into
mainfrom
feat/whatsapp-style-message-formatting
Oct 4, 2026
Merged

AchoArnold merged 6 commits into
mainfrom
feat/whatsapp-style-message-formatting

Conversation

@AchoArnold

Copy link
Copy Markdown
Member

Summary

Add WhatsApp-style text formatting to the thread view (web/app/pages/threads/[id]/index.vue), applied only to messages whose owner phone is backed by an HTTP notification URL rather than an FCM token.

What changed

  • New web/app/utils/whatsapp-format.ts:
    • isUrlNotificationToken(fcmToken) — mirrors isNotificationURLCandidate in api/pkg/entities/phone.go to detect whether a phone's fcm_token is actually an HTTP notification URL.
    • formatWhatsappText(text) — converts WhatsApp markdown into sanitized HTML:
      • *bold*, _italic_, ~strikethrough~, `code`/```code block```
      • "> " quoted lines
      • Raw URLs auto-linked as clickable target="_blank" anchors
    • Formatting markers must be bounded by whitespace, punctuation (.,!?;:'"()[]{}-), or the start/end of the string — matching WhatsApp's own rule that markers can't have internal leading/trailing whitespace and can't be embedded mid-word (e.g. *donotboldme*no space is left untouched, but *word*. bolds correctly).
    • Input is HTML-escaped before any formatting is applied to prevent XSS; URLs are protected behind placeholder tokens so markdown characters inside a URL (e.g. _) aren't misinterpreted as formatting.
  • threads/[id]/index.vue: looks up the message owner's phone, and when isUrlNotificationToken is true, renders the message content via v-html="formatWhatsappText(message.content)" instead of plain text.

Why

Phones that use an HTTP push-notification URL instead of FCM are typically relaying messages from services (e.g. WhatsApp Business-style bots) that already use WhatsApp's own markdown conventions. Rendering that formatting makes those messages readable instead of showing raw */_/~ characters.

Testing

  • Manually verified formatting and boundary rules with sample messages (bold/italic/strikethrough/code/quotes, URL linkification, and punctuation vs. mid-word boundaries).
  • pnpm lint:js, eslint, and prettier --check pass on changed files.

- Render *bold*, _italic_, ~strikethrough~, `code`/```code block``` and
  "> " quoted lines on the thread page, only for messages whose owner
  phone uses an HTTP notification URL instead of an FCM token.
- Auto-link raw URLs as clickable anchors, protected from markdown
  replacements via placeholder tokens.
- Require markers to be bounded by whitespace, punctuation, or the
  start/end of the string, matching WhatsApp's own formatting rules.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a795297d-527c-4f30-a35a-ab379f2d50a8
* transport) instead of an FCM token. This mirrors `isNotificationURLCandidate` in
* api/pkg/entities/phone.go.
*/
export function isUrlNotificationToken(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

use tioken.trim() starts with "https://" only.

Address review comment: use token.trim().toLowerCase().startsWith('https://') instead of checking multiple schemes/substrings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a795297d-527c-4f30-a35a-ab379f2d50a8
@greptile-apps

greptile-apps Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[Medium risk] Adds WhatsApp text formatting to message display.

The PR is not ready to merge because valid links can open the wrong destination and conventional multiline code blocks remain unformatted.

Findings

  1. P1 Valid URL parenthesis removed ▶
  2. P1 Multiline code blocks remain unformatted ▶
  3. P2 URL absorbs closing formatting marker ▶
  4. P2 Code text receives formatting ▶
  5. P2 Bare greater-than becomes quotation ▶
  6. P2 Placeholder collision changes message text ▶
  7. P2 Formatter lacks automated tests ▶

Summary

The PR adds WhatsApp-style formatting and URL linkification to thread messages owned by phones with URL notification tokens.

  • The formatter escapes message text before producing HTML for the thread view.
  • URL punctuation and marker handling, fenced code blocks, and literal text preservation need correction.
  • Focused automated tests would help protect the new parsing behavior.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  M[Thread message] --> P[Find owner phone]
  P --> G{URL notification token?}
  G -- No --> T[Render plain text]
  G -- Yes --> E[Escape HTML]
  E --> L[Linkify URLs with placeholders]
  L --> F[Apply quote and markdown formatting]
  F --> R[Restore links and render HTML]
Loading

Reviews (1) · Last reviewed commit: "feat(web): add WhatsApp-style formatting..."

Comment thread web/app/utils/whatsapp-format.ts Outdated
Comment on lines +43 to +44
const trailing = match.match(/[),.;:!?'"]+$/)?.[0] ?? ''
const urlText = trailing ? match.slice(0, -trailing.length) : match

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Valid URL parenthesis removed A URL such as https://example.com/page_(v2) includes the closing parenthesis, but this rule removes it from the link destination while leaving it visible in the message. Clicking the link therefore opens the wrong address.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in ad45dfe. splitTrailingPunctuation() now balance-checks parentheses (counts '(' vs ')' before the candidate split point) instead of unconditionally stripping a trailing ')'. https://example.com/page_(v2) now keeps the closing paren in the href.

Comment thread web/app/utils/whatsapp-format.ts Outdated
const formatted = lines
.join('\n')
.replace(
/(?<=^|[\s.,!?;:'"()[\]{}-])```(\S(?:[^`]*\S)?)```(?=$|[\s.,!?;:'"()[\]{}-])/g,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Multiline code blocks remain unformatted When code begins on the line after an opening triple-backtick fence, the required \S cannot match that newline. The message displays the raw backticks instead of the supported code-block formatting.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in ad45dfe. CODE_BLOCK_PATTERN changed from requiring a non-whitespace boundary char on each side of the fenced content to just '[^backtick]+', so a fence whose content starts/ends with a newline (code on its own lines) still matches.

Comment thread web/app/utils/whatsapp-format.ts Outdated
Comment on lines +42 to +44
return text.replace(/(https?:\/\/[^\s<]+|www\.[^\s<]+)/gi, (match) => {
const trailing = match.match(/[),.;:!?'"]+$/)?.[0] ?? ''
const urlText = trailing ? match.slice(0, -trailing.length) : match

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 URL absorbs closing formatting marker In *https://example.com*, the URL match consumes the closing * before bold formatting runs. The message shows an unmatched opening marker and an unformatted link whose text and destination include the closing marker.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed, refined across ad45dfe and 131ed20. linkifyUrls now checks the character immediately preceding the URL match: a trailing marker char is only stripped from the URL when it matches that preceding character (confirms a real opening delimiter), so https://example.com bolds a clean link instead of leaving the marker in the href/text.

Comment thread web/app/utils/whatsapp-format.ts Outdated
Comment on lines +85 to +90
.replace(
/(?<=^|[\s.,!?;:'"()[\]{}-])`(\S(?:[^`\n]*\S)?)`(?=$|[\s.,!?;:'"()[\]{}-])/g,
'<code>$1</code>',
)
.replace(
/(?<=^|[\s.,!?;:'"()[\]{}-])\*(\S(?:[^*\n]*\S)?)\*(?=$|[\s.,!?;:'"()[\]{}-])/g,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Code text receives formatting Later replacements also process text already wrapped in <code>. For example, `a *b* c` produces <code>a <strong>b</strong> c</code>, so code containing formatting markers no longer displays as literal text.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in ad45dfe. Added extractCodeSpans(), which replaces inline/fenced code with placeholder tokens (same placeholder store used for links) before the bold/italic/strikethrough passes run, then restores them untouched at the end. 'a b c' inside backticks now stays literal.

Comment thread web/app/utils/whatsapp-format.ts Outdated
const withPlaceholders = linkifyUrls(escaped, links)

const lines = withPlaceholders.split('\n').map((line) => {
const quoteMatch = line.match(/^&gt;\s?(.*)$/)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Bare greater-than becomes quotation The whitespace after > is optional here, although the documented quote prefix is > . A message such as >threshold is displayed as a quotation and loses its visible greater-than sign.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in ad45dfe. The quote-line regex now requires a literal greater-than followed by a space, instead of an optional space, so '>threshold' is left as literal text.

Comment thread web/app/utils/whatsapp-format.ts Outdated
Comment on lines +102 to +105
return formatted.replace(
LINK_PLACEHOLDER_PATTERN,
(_, idx: string) => links[Number(idx)] ?? '',
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Placeholder collision changes message text The restoration pattern also matches the same private-use-character sequence when it appears in a message. If the text contains \uE0000\uE000 without a URL, that text disappears; with a URL, it can be replaced by the link. This silently changes what the reader sees.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in ad45dfe. createPlaceholderStore() now generates a random per-call nonce embedded in the placeholder token, so literal message text can never collide with another call's placeholders.

* - "> " quoted lines
* - raw URLs, turned into clickable links
*/
export function formatWhatsappText(text: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Formatter lacks automated tests The new formatter depends on the order of linkification, quote handling, markdown replacements, and placeholder restoration, but has no automated input/output tests. Focused tests for boundaries, code blocks, URLs, and HTML escaping would make regressions easier to catch than manual checks alone.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added in ad45dfe: web/app/utils/whatsapp-format.test.ts with 29 cases covering boundaries, quotes, code blocks, links, and escaping/placeholder safety, run via Node's built-in test runner (pnpm test).

- Preserve balanced parentheses in linkified URLs (e.g.
  https://example.com/page_(v2)) instead of always stripping the
  closing paren.
- Support multiline fenced code blocks (```...```) where the content
  starts on the line after the opening fence.
- Strip trailing formatting markers (*_~`) from a matched URL so
  "*https://example.com*" bolds the link instead of leaving an
  unmatched opening marker.
- Protect already-rendered inline/fenced code from the bold/italic/
  strikethrough passes so markers inside code render literally.
- Use a per-call random nonce for placeholder tokens so literal
  message text can never collide with a placeholder and silently
  disappear or get replaced.
- Require the quote prefix to be exactly "> " (space required) so a
  bare ">" with no following space is left untouched.
- Add an automated test suite (app/utils/whatsapp-format.test.ts) run
  via Node's built-in test runner (`pnpm test`), covering boundaries,
  quotes, code blocks, links, and escaping/placeholder safety.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a795297d-527c-4f30-a35a-ab379f2d50a8
@codacy-production

codacy-production Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 59 complexity · -2 duplication

Metric Results
Complexity 59
Duplication -2

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Comment thread web/app/utils/whatsapp-format.test.ts Fixed
Comment thread web/app/utils/whatsapp-format.test.ts Fixed
Comment thread web/app/utils/whatsapp-format.test.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Formatting boundaries, URL parsing, and code-span processing currently produce incorrect output for valid inputs.

Review effort: Balanced
Findings: 1 Medium severity · 1 Low severity

Open (2)
What changed in this PR

Adds WhatsApp-style formatting for messages associated with URL-backed phones.

Changes:

  • Adds sanitized formatting, quoting, code, and URL linkification.
  • Conditionally renders formatted message HTML.
  • Adds formatter tests and a TypeScript test runner.
File Description
web/​app/​utils/​whatsapp-format.ts Implements formatting and URL detection.
web/​app/​utils/​whatsapp-format.test.ts Tests formatting behavior.
web/​app/​pages/​threads/​[id]/​index.vue Renders eligible messages as formatted HTML.
web/​package.json Configures TypeScript tests.
web/​pnpm-lock.yaml Locks the added test dependency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/app/utils/whatsapp-format.ts Outdated
Comment on lines +56 to +59
'*',
'_',
'~',
'`',

@AchoArnold AchoArnold Oct 4, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed. Trailing marker chars are now only stripped when the same character immediately precedes the URL (confirming it is a paired opening delimiter, e.g. https://x.com). Otherwise kept as part of the URL -- https://example.com/file_name_ now links correctly. Added a regression test.

Comment thread web/app/utils/whatsapp-format.test.ts Outdated
AchoArnold and others added 3 commits October 4, 2026 16:24
Trailing *_~` characters were stripped from every linkified URL, even
when they're legitimate URL path characters (e.g.
https://example.com/file_name_ linked to /file_name instead of
/file_name_). Now a trailing marker is only dropped when the same
character immediately precedes the URL, confirming it's an actual
wrapping delimiter (e.g. *https://example.com*) rather than part of
the URL itself. Sentence punctuation and balanced-parenthesis handling
are unchanged.

Also fixes a typo ("oe" -> "or") in a test case.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a795297d-527c-4f30-a35a-ab379f2d50a8
Replace .includes() URL substring checks in a test assertion with
regex matches against the rendered href attribute. CodeQL flags raw
.includes(url) checks as a potential (flawed) security validation
pattern; this was only a test assertion on rendered output, but the
rewrite is also a more precise check (it verifies the URL became an
anchor's href rather than just appearing anywhere in the string).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: a795297d-527c-4f30-a35a-ab379f2d50a8
TestSendSMS_RateLimit failed intermittently in CI: fireEvent dispatches
the SENT/DELIVERED cloud event through an async loopback HTTP push
(emulatorPushQueue), and the 15s poll window was too tight under CI
load, failing right at the boundary. Other tests already poll the
'delivered' status with a 30s timeout; align this test with that
margin for both 'sent' and 'delivered' polls.
@AchoArnold
AchoArnold merged commit 9c74028 into main Oct 4, 2026
12 checks passed
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.

3 participants