');
});
- it('plainToEditorInput expands emoticon placeholders into Slate emoticon elements', () => {
+ it('plainToEditorInput expands emoticon placeholders into editor emoticon tokens', () => {
const src = 'mxc://matrix.org/emote';
const md = `before${MX_EMOTICON_MD_START}${src}${MX_EMOTICON_MD_SEP}blobcat${MX_EMOTICON_MD_END}after`;
const doc = plainToEditorInput(md);
diff --git a/src/app/plugins/markdown/htmlToMarkdown.ts b/src/app/plugins/markdown/htmlToMarkdown.ts
index 0e3756d8db..ed9d50a85b 100644
--- a/src/app/plugins/markdown/htmlToMarkdown.ts
+++ b/src/app/plugins/markdown/htmlToMarkdown.ts
@@ -67,7 +67,7 @@ function processNodes(nodes: ChildNode[]): string {
for (let i = 0; i < filtered.length; i += 1) {
const cur = filtered[i]!;
const prev = filtered[i - 1];
- // Adjacent blocks must become \n\n in markdown so the editor gets separate Slate
+ // Adjacent
blocks must become \n\n in markdown so the editor gets separate paragraphs.
// paragraphs and marked emits
per block again on send (single \n would collapse).
if (i > 0 && prev && isTag(prev) && isTag(cur)) {
const prevTag = prev.name.toLowerCase();
diff --git a/src/app/state/room/roomInputDrafts.ts b/src/app/state/room/roomInputDrafts.ts
index 10525f64a5..4ed07f9cfc 100644
--- a/src/app/state/room/roomInputDrafts.ts
+++ b/src/app/state/room/roomInputDrafts.ts
@@ -1,6 +1,6 @@
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
-import type { Descendant } from 'slate';
+import type { EditorDocument } from '$components/editor/model';
import type { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
import type { IEventRelation } from '$types/matrix-sdk';
import type { TUploadContent } from '$utils/matrix';
@@ -30,7 +30,8 @@ export const roomIdToUploadItemsAtomFamily = atomFamily
export const roomUploadAtomFamily = createUploadAtomFamily();
-const createMsgDraftAtom = () => atom([]);
+// Room drafts are Sable documents, not a rendering-engine data structure.
+const createMsgDraftAtom = () => atom([]);
type TMsgDraftAtom = ReturnType;
export const roomIdToMsgDraftAtomFamily = atomFamily(() =>
createMsgDraftAtom()
diff --git a/tests/e2e/live-timeline.spec.ts b/tests/e2e/live-timeline.spec.ts
index 6dc1def02e..d1fe710557 100644
--- a/tests/e2e/live-timeline.spec.ts
+++ b/tests/e2e/live-timeline.spec.ts
@@ -15,7 +15,7 @@ test.describe('live timeline', () => {
test('renders an editor-sent message as a single server-confirmed row', async ({
page,
}, testInfo) => {
- test.skip(testInfo.project.name !== 'desktop', 'desktop-focused');
+ test.skip(testInfo.project.name === 'touch', 'desktop and mobile viewport coverage');
test.setTimeout(300_000);
const storageStatePath = testInfo.project.use.storageState as string;
const hsBaseUrl = await homeserverBaseUrl(storageStatePath);
@@ -34,16 +34,30 @@ test.describe('live timeline', () => {
});
await app.openRoom(`${tag} Room`);
- const body = `${tag}-hello`;
+ const body = `${tag}-hello!\n${tag}-again`;
const editor = page.locator('[data-editable-name="RoomInput"]');
await editor.click();
- await editor.pressSequentially(body);
+ await editor.pressSequentially('before #');
+ await expect(page.locator('[data-autocomplete-menu="true"]')).toContainText('Rooms');
+ await editor.press('Backspace');
+ await editor.pressSequentially('@');
+ await expect(page.locator('[data-autocomplete-menu="true"]')).toContainText('Mentions');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.press('Backspace');
+ await editor.pressSequentially(`${tag}-hello`);
+ await editor.press('Tab');
+ await editor.press('Shift+Tab');
+ await editor.pressSequentially('!');
+ await editor.press('Shift+Enter');
+ await editor.pressSequentially(`${tag}-again`);
await editor.press('Enter');
- await expect(page.getByText(body, { exact: true })).toBeVisible({
- timeout: 120_000,
- });
-
let serverId: string | undefined;
await expect(async () => {
const messages = await getRoomMessages(hsBaseUrl, user.accessToken, room);
@@ -51,8 +65,42 @@ test.describe('live timeline', () => {
expect(serverId).toMatch(/^\$/);
}).toPass({ timeout: 120_000, intervals: [500] });
- await expect(app.messageByEventId(serverId!).getByText(body, { exact: true })).toHaveCount(1);
- expect(await page.getByText(body, { exact: true }).count()).toBe(1);
+ await expect(app.messageByEventId(serverId!)).toBeVisible();
+ });
+
+ test('keeps a copied one-line message on one line when pasted over its selection', async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== 'desktop', 'desktop-focused');
+ test.setTimeout(300_000);
+ const storageStatePath = testInfo.project.use.storageState as string;
+ const hsBaseUrl = await homeserverBaseUrl(storageStatePath);
+ const tag = `paste-${process.pid}-${Date.now().toString(36)}`;
+ const app = new AppShell(page);
+ const user = await loginAsFreshUser(page, hsBaseUrl, `${tag}-u`);
+
+ const room = await createRoom(hsBaseUrl, user.accessToken, {
+ name: `${tag} Room`,
+ preset: 'private_chat',
+ });
+
+ await page.goto('/');
+ await expect(page.getByText(`${tag} Room`).first()).toBeVisible({ timeout: 180_000 });
+ await app.openRoom(`${tag} Room`);
+
+ const body = `${tag}-short`;
+ const editor = page.locator('[data-editable-name="RoomInput"]');
+ await editor.click();
+ await editor.pressSequentially(body);
+ await editor.press('Control+A');
+ await editor.press('Control+C');
+ await editor.press('Control+V');
+ await editor.press('Enter');
+
+ await expect(async () => {
+ const messages = await getRoomMessages(hsBaseUrl, user.accessToken, room);
+ expect(messages.find((message) => message.body === body)?.eventId).toMatch(/^\$/);
+ }).toPass({ timeout: 120_000, intervals: [500] });
});
test('renders a remote burst in an open room exactly once and in canonical order', async ({