diff --git a/.changeset/fix-emojis-edits.md b/.changeset/fix-emojis-edits.md
new file mode 100644
index 000000000..9d42e0fed
--- /dev/null
+++ b/.changeset/fix-emojis-edits.md
@@ -0,0 +1,5 @@
+---
+default: patch
+---
+
+Fix editing messages with custom emojis being converted into html tags.
diff --git a/src/app/components/editor/input.ts b/src/app/components/editor/input.ts
index 7222cfcae..471a0e5f5 100644
--- a/src/app/components/editor/input.ts
+++ b/src/app/components/editor/input.ts
@@ -1,19 +1,67 @@
import type { Descendant } from 'slate';
+import {
+ MX_EMOTICON_MD_END,
+ MX_EMOTICON_MD_SEP,
+ MX_EMOTICON_MD_START,
+ validateMxcUrl,
+} from '$plugins/markdown/extensions/matrix-emoticon';
import { BlockType } from './types';
import type { ParagraphElement } from './slate';
+import { createEmoticonElement } from './utils';
+
+/** Matches placeholders emitted by htmlToMarkdown for <img data-mx-emoticon>. */
+const MX_EMOTICON_MD_TOKEN = new RegExp(
+ `${MX_EMOTICON_MD_START}([^${MX_EMOTICON_MD_SEP}]+)${MX_EMOTICON_MD_SEP}([^${MX_EMOTICON_MD_END}]+)${MX_EMOTICON_MD_END}`,
+ 'g'
+);
+
+function mergeAdjacentTextNodes(
+ children: ParagraphElement['children']
+): ParagraphElement['children'] {
+ const out: ParagraphElement['children'] = [];
+ for (const c of children) {
+ if ('type' in c) {
+ out.push(c);
+ continue;
+ }
+ const prev = out[out.length - 1];
+ if (prev && !('type' in prev)) {
+ prev.text += c.text;
+ } else {
+ out.push({ ...c });
+ }
+ }
+ return out.length > 0 ? out : [{ text: '' }];
+}
+
+function lineToParagraphChildren(line: string): ParagraphElement['children'] {
+ MX_EMOTICON_MD_TOKEN.lastIndex = 0;
+ const parts: ParagraphElement['children'] = [];
+ let last = 0;
+ let match: RegExpExecArray | null;
+ while ((match = MX_EMOTICON_MD_TOKEN.exec(line)) !== null) {
+ if (match.index > last) {
+ parts.push({ text: line.slice(last, match.index) });
+ }
+ const [, src, shortcode] = match;
+ if (src && shortcode && validateMxcUrl(src)) {
+ parts.push(createEmoticonElement(src, shortcode));
+ } else if (shortcode) {
+ parts.push({ text: `:${shortcode.replace(/^:|:$/g, '')}:` });
+ }
+ last = MX_EMOTICON_MD_TOKEN.lastIndex;
+ }
+ if (last < line.length) {
+ parts.push({ text: line.slice(last) });
+ }
+ return mergeAdjacentTextNodes(parts);
+}
export const plainToEditorInput = (text: string): Descendant[] => {
- const editorNodes: Descendant[] = text.split('\n').map((lineText) => {
- const paragraphNode: ParagraphElement = {
- type: BlockType.Paragraph,
- children: [
- {
- text: lineText,
- },
- ],
- };
- return paragraphNode;
- });
- return editorNodes;
+ const lines = text.split('\n');
+ return lines.map((lineText) => ({
+ type: BlockType.Paragraph,
+ children: lineToParagraphChildren(lineText),
+ }));
};
diff --git a/src/app/plugins/markdown/bidirectional.test.ts b/src/app/plugins/markdown/bidirectional.test.ts
index b7aa44d5b..dff56737c 100644
--- a/src/app/plugins/markdown/bidirectional.test.ts
+++ b/src/app/plugins/markdown/bidirectional.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
+import { encodeMxEmoticonForMarkdownPlaceholder } from './extensions/matrix-emoticon';
import { markdownToHtml } from './markdownToHtml';
import { htmlToMarkdown } from './htmlToMarkdown';
import { injectDataMd } from './injectDataMd';
@@ -108,12 +109,14 @@ describe('bidirectional round-trip', () => {
expect(result).toContain('k.');
});
- it('round-trips img[data-mx-emoticon] tags', () => {
+ it('preserves mx emoticons as editor placeholders (mxc URI + shortcode)', () => {
const html = '';
const injected = injectDataMd(html);
const result = htmlToMarkdown(injected);
- expect(result).toContain('
{
@@ -81,4 +88,32 @@ describe('htmlToMarkdown', () => {
const result = htmlToMarkdown('
Hello *world*
'); expect(result).toContain('\\*'); }); + + it('encodes mx emoticons as private-use placeholders instead of literal img snippets', () => { + const src = 'mxc://matrix.org/emote'; + const html = `hibye