Repository navigation
fix(mentions): show the mention after a link that ends at CJK punctuation - #2777
cheng-block wants to merge 9 commits into
Conversation
…tion A person mentioned right after a bare link and fullwidth or CJK punctuation, as in `https://x.com,@Honey`, was notified, but the timeline showed `@Honey` as plain text. The mention display still ended a URL only at whitespace, so the name counted as part of the link, while rendered links end at CJK punctuation since block#2404. Bare URLs there now end with the pattern messageLinkParts uses, moved to relay/message-content.ts exactly as block#2772 moves it. `<url>` and `](url)` keep CJK punctuation as before. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
The first commit kept a URL whole only right after `<` or `](`. A Markdown destination after a line break or space, as in `[a](\nhttps://x.com/a,@honey)` or `[[a]]( https://x.com/a,@Honey)`, or after a pasted `]\(`, ended at the CJK punctuation instead. Edit then turned `@Honey` into a mention link inside the URL, which would change it on save, and Copy did the same for `]\(`. The timeline shows these as one link, as before. The check now accepts the `]\(` opener and the whitespace that messageLinkParts accepts, the same lookbehind block#2772 uses in emoji.ts. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
Two problems in the URL check. A link right after the CJK punctuation that ends a bare link was not seen as a URL, so in `https://a.test,https://b.test/?to=@Honey` Edit and Copy turned the `@Honey` inside the second link into a mention link, and `https://a.test,nostr:npub1…?to=@Honey` showed a mention inside the nostr text. A URL can now start after any character a bare URL cannot contain, which also covers a link right after `:`, `“` or `>`. And the `<` and `](` lookbehind ran at every whitespace boundary and rescanned the whole run, so 100,000 spaces took about 5 s. It now runs only where a URL starts, about 6 ms, as on main. The negative cases move into the existing table. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
block#2772 now splits emoji text with a case-sensitive regex, so its copy of BARE_URL_REST spells out A-Z in the host label check instead of relying on the i flag. This takes the same hunk, so relay/message-content.ts stays byte for byte the same in both PRs. messageLinkParts and the mention check both use the i flag, so neither changes what it matches. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
|
🤖 Design review at Must-fix: editing unrelated prose can silently change a reference link's destination. With Mic already a recipient, send: @Mic look at [ref].
[ref]: https://example.test/a,@MicOpen Edit, change “look” to “see”, and save. The definition's Evidence: changed URL protection, edit serialization. Reproduced with exact base/head functions and Markdown destination parsing. Unchecked: native app/IME, keyboard/focus and screen-reader integration, themes, widths, enlarged text and many-agent states. No attached screenshots; no app run or package test suite in this review. |
Opening Edit turned a bound name into a profile link. Now that a bare URL ends at CJK punctuation, `[ref]: https://x.test/a,@Sam` exposed the name, and saving put the link inside the definition's URL. Edit now skips names inside reference definitions, which the Markdown parser finds, as the renderer and Copy already treat definitions as literal. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
…Ranges Edit now uses the `definitionRanges` helper that block#2772 adds for emoji, copied byte for byte, instead of walking the Markdown tree itself. The helper skips the parse when the body has no `]:`, so the separate early return for bodies without a bound name goes away. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
`definitionRanges` gave up on Markdown nested past the depth limit, so Edit turned a name inside a definition under 101 quote levels into a link inside its URL. Take block#2772's fix, which walks the whole tree, byte for byte, and cover that case in the Edit round trip. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
Fixed in 1fe736c, with follow-ups through 0af467b. Edit now leaves mentions inside reference definition destinations unbound, using the Markdown parser's definition ranges (the same helper #2772 uses). Your example round trip is a regression test: changing "look" to "see" keeps |
|
🤖 Could you add a screenshot or short video of the change to the PR? Design uses these for faster reviews. |
Added under Screenshots in the description. They come from the debug build I tested. |
…#2834) ## Why Edit turns a name inside literal Markdown (HTML, link titles, reference definitions) into a mention link, so saving changes text you never touched. `[ref]: https://example.com "Ask @sam"` is saved as `[ref]: https://example.com "Ask [@sam](nostr:npub1…)"`. The timeline and Copy show those names as plain text. `messageEditText` links every name `profileMentionParts` finds, and that finder skips only code, links and URLs (`src/features/messages/profile-mentions.ts:99`). The timeline drops a name that overlaps a range from `literalRanges` (`src/features/messages/markdown-preparation.ts:39`). Edit had no such check. ## What Edit now leaves a name as written when it sits inside literal Markdown, using the timeline's ranges. Names outside literal Markdown still become mention links. ## How - `messageEditText` parses the text it edits (`sourceContent`, which keeps attachment Markdown) with `scanMarkdown` and takes `literalRanges(tree, true)`. `literalRanges` is now exported. - It walks the ranges in order beside the names, like the timeline, so it checks each range once. - No second Markdown heuristic. Copy's own walk is left alone. ## Risk Low. Only the text Edit loads changes. Recipients stay the same: an edited message keeps the original event's `p` tags, so a name left as text drops no one. - Copy and the timeline read a body over 100,000 characters or 100 levels deep as plain text. Edit has no such guard, but it opens only your own messages, and Buzz sends at most 32 KiB. Another client can send more. - Known limit: the timeline repairs a pasted `[Hi @sam]\(https://example.com)` into a link and shows no mention. Edit and Copy read it as written and still link that `@Sam`, same as on `main`. - Out of scope: bare URL ends (#2777), suggestions after CJK punctuation (#2768), picked name ends (#2713), `protectedTextEnd` time on unmatched brackets. - With #2777 merged in, this also closes the Edit limit #2777 lists: `[ref]: https://example.com/a,@Sam` stays as written. ## Testing - No browser cases added or removed. The change is text only. - A one-off script compared names linked by the timeline, Copy and Edit on 55 bodies. All three agree on 54; the other is the known limit. On `main`, Edit linked more than Copy on 20. - The 8 new Edit test texts pass through the composer's load and save helpers unchanged. - `messageEditText` runs once when Edit opens (Node and jsdom, median of five runs). A body that alternates names and code spans took about 72 ms at 16,000 characters and 270 ms at 32 KiB before the in-order check. After it, 8 ms and 16 ms. ## Screenshots Debug build of this PR: Edit shows the real mention as a chip and leaves `@BeeBee` inside the span title as plain text. <img width="760" alt="Composer in Editing message mode with Hi `@BeeBee` on line one and a literal span tag with title \"`@BeeBee`\" below." src="https://github.com/user-attachments/assets/26434d42-f342-4007-a9b3-382d1627742e" /> After saving, the message keeps the real mention and the literal span exactly as written. <img width="744" alt="Edited message reading Hello with a BeeBee mention chip and the literal span tag below." src="https://github.com/user-attachments/assets/4abdf2a0-6162-482c-84ca-c90f24f445a0" /> ## In-app check Verified in the macOS app by Cheng. Build: this PR at `bcc62911e`, merged locally with my other open PRs onto `main` at `c037057a1`, run as a debug app bundle. Later commits only merge `main`. Use a test channel with a person or agent in it. The steps call them Name. 1. Type `Hi @`, pick Name, press Shift+Enter twice, then paste `<span title="@name">hi</span>` with their exact name. Send. 2. The message shows `Hi`, the mention, then `<span title="@name">hi</span>` as typed. 3. Open Edit. The second line still reads `<span title="@name">hi</span>`. On `main`, Edit put a profile link inside the quotes. 4. Change `Hi` to `Hello` and save. The message shows `Hello`, the mention, and the same `<span>` line. buzz-review-completed --------- Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
main's literal Markdown ranges (block#2834) already skip reference definitions when an edit turns names back into links, so take main's messageEditText and keep both test sets. Signed-off-by: Cheng Peng <cheng@block.xyz> Co-authored-by: Cursor <cursoragent@cursor.com>
Why
A person tagged right after a bare link and CJK punctuation got notified, but the message showed no mention. In
https://x.com,@Honey, the text,@Honeystayed plain.The timeline finds mentions with
profileMentionParts, which skips text inside a URL. That URL ran to the next whitespace, so@Honeycounted as part of it. Edit and Copy use the same parts, so they lost the mention too.What
protectedTextEndnow ends a bare URL where the rendered link ends, so the name is a mention in the timeline, Edit and Copy.Edit leaves names inside a reference definition alone, as the timeline and Copy already do. So
[ref]: https://x.com/a,@Honeykeeps its URL when you edit other text.How
BARE_URL_RESTinsrc/features/relay/message-content.ts, and build themessageLinkPartsregex from it. These hunks match fix(emoji): show custom emoji after a link that ends at CJK punctuation #2772 byte for byte.A-Zthat theiflag already covered.<,](or]\(still runs to whitespace, so<https://x.com/a,@Honey>stays one link. Binding a name there would change the URL on save.messageEditTextfinds reference definitions withdefinitionRanges, which uses the renderer's Markdown parser, not a regex. A bound name inside one stays plain text.definitionRangescomes from fix(emoji): show custom emoji after a link that ends at CJK punctuation #2772, which uses it for emoji.message-content.tsmatches fix(emoji): show custom emoji after a link that ends at CJK punctuation #2772 byte for byte.Risk
Only mentions right after a bare URL change, in the timeline, Edit and Copy.
mailto:,nostr:and otherscheme://text now also ends at CJK punctuation. Bare URLs now also end at", as rendered links do."or>is now kept whole. So链接:https://x.com/?to=@Honeyno longer gets a mention in Edit and Copy.看https://x.com/?to=@Honey), as onmain.main, Edit already turns names in other literal Markdown into mentions (<!-- @Honey -->). fix(messages): keep names in literal Markdown as written when editing #2834 fixes that, and covers definitions too.[already scans slowly onmain, about 6 s for 100,000. Such a run after a link and CJK punctuation is now scanned too. Bounding it needs its own PR.Testing
profileMentionPartson 100,000-character bodies (spaces, line breaks,,, quotes, chained links and prose) took at most about 8 ms each, as onmain.56ca07e6e, where every definition URL gains a profile link. It passes now.Screenshots
Debug build of this PR: the link ends at example.com, and the name after the Chinese comma shows as a BeeBee mention chip.
Debug build of this PR: Edit keeps the mention after the link and Chinese comma.
In-app check
Verified in the macOS app by Cheng.
Build: this PR at
56ca07e6e, merged locally with my other open PRs ontomainatc037057a1, run as a debug app bundle (BUZZ_DEV_VIEWER= just desktop-bundle).Use a test channel with one of your running agents. An agent only wakes on a real mention, so a reply confirms the tag.
看这个 https://example.com, @, pick the agent, then delete the space before@.https://example.comis a link, followed by,and the mention. The agent replies.<https://example.com/a,@AgentName>by hand, and send. Only the first name is a mention.Step 1 needs the space until #2768 lands.
Not yet checked in the app: the reference definition case in Edit. A unit test covers it.
buzz-review-completed