-
Notifications
You must be signed in to change notification settings - Fork 1.5k
fix(desktop): keep the space after a mention through the markdown parse #2924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
peterw
wants to merge
1
commit into
block:main
Choose a base branch
from
peterw:fix/composer-mention-trailing-space
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+177
−8
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
45 changes: 45 additions & 0 deletions
45
desktop/src/features/messages/lib/markdownTrailingWhitespace.test.mjs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import assert from "node:assert/strict"; | ||
| import test from "node:test"; | ||
|
|
||
| import { markdownTrailingWhitespace } from "./markdownTrailingWhitespace.ts"; | ||
|
|
||
| // The markdown parse behind `setContent` drops end-of-line whitespace, which | ||
| // silently breaks mention chips (`@Name ` → `@Name`, next keystroke → | ||
| // `@Nameabc`, recipient dropped). This decides what gets put back. | ||
|
|
||
| test("captures the single trailing space after a mention", () => { | ||
| assert.equal(markdownTrailingWhitespace("@Morgarita "), " "); | ||
| }); | ||
|
|
||
| test("captures the trailing space after multiple mentions", () => { | ||
| assert.equal(markdownTrailingWhitespace("@Vogue @Morgarita "), " "); | ||
| }); | ||
|
|
||
| test("returns null when there is no trailing whitespace", () => { | ||
| assert.equal(markdownTrailingWhitespace("@Morgarita"), null); | ||
| assert.equal(markdownTrailingWhitespace(""), null); | ||
| }); | ||
|
|
||
| test("returns null for whitespace-only input", () => { | ||
| // Nothing to trail. Restoring here would leave a composer that reads as | ||
| // non-empty — Send enabled, placeholder suppressed — on an empty draft. | ||
| assert.equal(markdownTrailingWhitespace(" "), null); | ||
| assert.equal(markdownTrailingWhitespace(" \t"), null); | ||
| }); | ||
|
|
||
| test("captures a mixed run of spaces and tabs", () => { | ||
| assert.equal(markdownTrailingWhitespace("@Morgarita \t "), " \t "); | ||
| }); | ||
|
|
||
| test("captures whitespace trailing the last line only", () => { | ||
| assert.equal(markdownTrailingWhitespace("first line\n@Morgarita "), " "); | ||
| }); | ||
|
|
||
| test("returns null when the string ends on a newline", () => { | ||
| // The caret lands on the empty final line, so nothing needs restoring. | ||
| assert.equal(markdownTrailingWhitespace("@Morgarita \n"), null); | ||
| }); | ||
|
|
||
| test("ignores interior whitespace", () => { | ||
| assert.equal(markdownTrailingWhitespace("@Morgarita hello"), null); | ||
| }); |
58 changes: 58 additions & 0 deletions
58
desktop/src/features/messages/lib/markdownTrailingWhitespace.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import type { Editor } from "@tiptap/core"; | ||
| import { Selection } from "@tiptap/pm/state"; | ||
|
|
||
| /** | ||
| * The run of spaces/tabs at the very end of `markdown`, or `null` when there | ||
| * is none. | ||
| * | ||
| * A whitespace-only string yields `null` on purpose: there is no content for | ||
| * the whitespace to trail, and restoring it would leave a "blank" composer | ||
| * that reads as non-empty (enabling Send, suppressing the placeholder). | ||
| */ | ||
| export function markdownTrailingWhitespace(markdown: string): string | null { | ||
| return /[^ \t]([ \t]+)$/.exec(markdown)?.[1] ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * Re-append trailing spaces/tabs that a markdown parse dropped. | ||
| * | ||
| * `setContent` runs its argument through `tiptap-markdown`, and markdown-it | ||
| * discards end-of-line whitespace — `"@Name "` parses to `<p>@Name</p>`. That | ||
| * lone character is load-bearing: mention chips are inline decorations over | ||
| * plain text (see `mentionHighlightExtension`), matched only when a boundary | ||
| * follows the name. Restore the caret flush against `@Name` and the next | ||
| * keystroke produces `@Nameabc`, which stops matching — the chip disappears | ||
| * and `extractMentionPubkeys` no longer resolves the name, so the recipient is | ||
| * silently dropped from the outgoing event. | ||
| * | ||
| * The whitespace survives *serialization* (drafts on disk keep it), so it is | ||
| * recoverable here, at the single parse boundary, rather than at each of the | ||
| * call sites that reload composer content: post-send refill, draft restore on | ||
| * channel/thread switch, loading a message to edit, cancelling an edit, and | ||
| * restoring after a failed send. | ||
| * | ||
| * Uses the `preventUpdate` meta so the repair is invisible to the user-edit | ||
| * observers — the same mechanism `setContent`'s `emitUpdate: false` uses. | ||
| * Without it the insert looks like a keystroke and re-opens the mention | ||
| * autocomplete on every channel switch. | ||
| */ | ||
| export function restoreMarkdownTrailingWhitespace( | ||
| editor: Editor, | ||
| markdown: string, | ||
| ): void { | ||
| const trailing = markdownTrailingWhitespace(markdown); | ||
| if (!trailing) return; | ||
|
|
||
| const end = Selection.atEnd(editor.state.doc).from; | ||
| // Idempotence guard: if the parser ever starts preserving the whitespace, | ||
| // this must not double it. | ||
| const existing = editor.state.doc.textBetween( | ||
| Math.max(0, end - trailing.length), | ||
| end, | ||
| ); | ||
| if (existing === trailing) return; | ||
|
|
||
| editor.view.dispatch( | ||
| editor.state.tr.insertText(trailing, end).setMeta("preventUpdate", true), | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a restored draft or edited message ends with formatted text followed by whitespace, such as
**bold**or`command`,tr.insertText(trailing, end)inherits the active marks at that ProseMirror position. The restored space therefore becomes bold/code text, and afterfocus("end")subsequent typing can continue inside that mark even though the original Markdown placed the space outside it. Insert an explicitly unmarked text node (or otherwise clear the inherited marks) so this repair preserves the formatting boundary as well as the whitespace.Useful? React with 👍 / 👎.