From 6bf4370de0190ec578f70b2daeeede3c514f1b5d Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 31 Mar 2022 21:29:06 +0200 Subject: [PATCH 1/5] Add support for HTML renderings of room topics Based on extensible events as defined in [MSC1767] Relates to: vector-im/element-web#5180 Signed-off-by: Johannes Marbach [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 --- spec/unit/content-helpers.spec.ts | 65 ++++++++++++++++++++++++++++++- src/@types/topic.ts | 62 +++++++++++++++++++++++++++++ src/client.ts | 11 +++++- src/content-helpers.ts | 31 ++++++++++++++- 4 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 src/@types/topic.ts diff --git a/spec/unit/content-helpers.spec.ts b/spec/unit/content-helpers.spec.ts index 3430bf4c2c1..8c110775df3 100644 --- a/spec/unit/content-helpers.spec.ts +++ b/spec/unit/content-helpers.spec.ts @@ -18,7 +18,13 @@ import { REFERENCE_RELATION } from "matrix-events-sdk"; import { M_BEACON_INFO } from "../../src/@types/beacon"; import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location"; -import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers"; +import { M_TOPIC } from "../../src/@types/topic"; +import { + makeBeaconContent, + makeBeaconInfoContent, + makeTopicContent, + parseTopicContent, +} from "../../src/content-helpers"; describe('Beacon content helpers', () => { describe('makeBeaconInfoContent()', () => { @@ -125,3 +131,60 @@ describe('Beacon content helpers', () => { }); }); }); + +describe('Topic content helpers', () => { + describe('makeTopicContent()', () => { + it('creates fully defined event content without html', () => { + expect(makeTopicContent("pizza")).toEqual({ + topic: "pizza", + [M_TOPIC.name]: [{ + body: "pizza", + mimetype: "text/plain", + }], + }); + }); + + it('creates fully defined event content with html', () => { + expect(makeTopicContent("pizza", "pizza")).toEqual({ + topic: "pizza", + [M_TOPIC.name]: [{ + body: "pizza", + mimetype: "text/plain", + }, { + body: "pizza", + mimetype: "text/html", + }], + }); + }); + }); + + describe('parseTopicContent()', () => { + it('parses event content without html', () => { + expect(parseTopicContent({ + topic: "pizza", + [M_TOPIC.name]: [{ + body: "pizza", + mimetype: "text/plain", + }], + })).toEqual({ + text: "pizza", + }); + }); + + it('parses event content with html', () => { + expect(parseTopicContent({ + topic: "pizza", + [M_TOPIC.name]: [{ + body: "pizza", + mimetype: "text/plain", + }, { + body: "pizza", + mimetype: "text/html", + }], + })).toEqual({ + text: "pizza", + html: "pizza", + }); + }); + }); +}); diff --git a/src/@types/topic.ts b/src/@types/topic.ts new file mode 100644 index 00000000000..2203f1840e3 --- /dev/null +++ b/src/@types/topic.ts @@ -0,0 +1,62 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { EitherAnd, IMessageRendering } from "matrix-events-sdk"; + +import { UnstableValue } from "../NamespacedValue"; + +/** + * Extensible topic event type based on MSC1767 + * https://github.com/matrix-org/matrix-spec-proposals/pull/1767 + */ + +/** + * Eg + * { + * "type": "m.room.topic, + * "state_key": "", + * "content": { + * "topic": "All about **pizza**", + * "m.topic": [{ + * "body": "All about **pizza**", + * "mimetype": "text/plain", + * }, { + * "body": "All about pizza", + * "mimetype": "text/html", + * }], + * } + * } + */ + +/** + * The event type for an m.topic event (in content) + */ +export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc1767.topic"); + +/** + * The event content for an m.topic event (in content) + */ +export type MTopicContent = IMessageRendering[]; + +/** + * The event definition for an m.topic event (in content) + */ +export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>; + +/** + * The event content for an m.room.topic event + */ +export type MRoomTopicEventContent = { topic: string } & MTopicEvent; diff --git a/src/client.ts b/src/client.ts index 11ec7fa4d56..afd507684b1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3534,12 +3534,19 @@ export class MatrixClient extends TypedEventEmitter { - return this.sendStateEvent(roomId, EventType.RoomTopic, { topic: topic }, undefined, callback); + public setRoomTopic( + roomId: string, + topic: string, + htmlTopic?: string, + callback?: Callback, + ): Promise { + const content = ContentHelpers.makeTopicContent(topic, htmlTopic); + return this.sendStateEvent(roomId, EventType.RoomTopic, content, undefined, callback); } /** diff --git a/src/content-helpers.ts b/src/content-helpers.ts index 393cb2e54a2..ff589c6c81b 100644 --- a/src/content-helpers.ts +++ b/src/content-helpers.ts @@ -16,7 +16,7 @@ limitations under the License. /** @module ContentHelpers */ -import { REFERENCE_RELATION } from "matrix-events-sdk"; +import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk"; import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent, M_BEACON_INFO } from "./@types/beacon"; import { MsgType } from "./@types/event"; @@ -32,6 +32,7 @@ import { MAssetContent, LegacyLocationEventContent, } from "./@types/location"; +import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic"; /** * Generates the content for a HTML Message event @@ -263,3 +264,31 @@ export const makeBeaconContent: MakeBeaconContent = ( event_id: beaconInfoId, }, }); + +/** + * Topic event helpers + */ +export type MakeTopicContent = ( + topic: string, + htmlTopic?: string, +) => MRoomTopicEventContent; + +export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => { + const renderings = [{ body: topic, mimetype: "text/plain" }]; + if (isProvided(htmlTopic)) { + renderings.push({ body: htmlTopic, mimetype: "text/html" }); + } + return { topic, [M_TOPIC.name]: renderings }; +}; + +export type TopicState = { + text: string; + html?: string; +}; + +export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => { + const mtopic = M_TOPIC.findIn(content); + const text = mtopic?.find(r => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic; + const html = mtopic?.find(r => r.mimetype === "text/html")?.body; + return { text, html }; +}; From e2306f86c8a8b2dc874a66bc94339e78686cc8a0 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Sun, 3 Apr 2022 14:41:17 +0200 Subject: [PATCH 2/5] Use correct MSC --- src/@types/topic.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/@types/topic.ts b/src/@types/topic.ts index 2203f1840e3..0d2708b2e50 100644 --- a/src/@types/topic.ts +++ b/src/@types/topic.ts @@ -19,8 +19,8 @@ import { EitherAnd, IMessageRendering } from "matrix-events-sdk"; import { UnstableValue } from "../NamespacedValue"; /** - * Extensible topic event type based on MSC1767 - * https://github.com/matrix-org/matrix-spec-proposals/pull/1767 + * Extensible topic event type based on MSC3765 + * https://github.com/matrix-org/matrix-spec-proposals/pull/3765 */ /** @@ -44,7 +44,7 @@ import { UnstableValue } from "../NamespacedValue"; /** * The event type for an m.topic event (in content) */ -export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc1767.topic"); +export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic"); /** * The event content for an m.topic event (in content) From a0814ae021ee73d54e0e4edcb95c4e9b2295ac02 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Sat, 9 Apr 2022 20:15:18 +0200 Subject: [PATCH 3/5] Add overloads for setRoomTopic --- src/client.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/client.ts b/src/client.ts index 63dc3cb4fd6..db3ed54da2c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3543,8 +3543,20 @@ export class MatrixClient extends TypedEventEmitter; + public setRoomTopic( + roomId: string, + topic: string, callback?: Callback, + ): Promise; + public setRoomTopic( + roomId: string, + topic: string, + htmlTopicOrCallback?: string | Callback, ): Promise { + const isCallback = typeof htmlTopicOrCallback === 'function'; + const htmlTopic = isCallback ? undefined : htmlTopicOrCallback; + const callback = isCallback ? htmlTopicOrCallback : undefined; const content = ContentHelpers.makeTopicContent(topic, htmlTopic); return this.sendStateEvent(roomId, EventType.RoomTopic, content, undefined, callback); } From ec8c080762a1c0d2cda2d1e6504818782e048926 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Sat, 9 Apr 2022 20:17:08 +0200 Subject: [PATCH 4/5] Fix indentation --- src/content-helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content-helpers.ts b/src/content-helpers.ts index 9e396c7673d..8c813b7aad6 100644 --- a/src/content-helpers.ts +++ b/src/content-helpers.ts @@ -194,7 +194,7 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent): /** * Topic event helpers */ - export type MakeTopicContent = ( +export type MakeTopicContent = ( topic: string, htmlTopic?: string, ) => MRoomTopicEventContent; From 5013c5bf5ca14c12ef2546f7baac26eca950c4e9 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 12 May 2022 20:49:17 +0200 Subject: [PATCH 5/5] Add more tests to pass the quality gate --- spec/unit/content-helpers.spec.ts | 16 +++++++++---- spec/unit/matrix-client.spec.ts | 37 ++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/spec/unit/content-helpers.spec.ts b/spec/unit/content-helpers.spec.ts index fe6fea1a669..ba431d9d586 100644 --- a/spec/unit/content-helpers.spec.ts +++ b/spec/unit/content-helpers.spec.ts @@ -156,25 +156,33 @@ describe('Topic content helpers', () => { }); describe('parseTopicContent()', () => { - it('parses event content without html', () => { + it('parses event content with plain text topic without mimetype', () => { expect(parseTopicContent({ topic: "pizza", [M_TOPIC.name]: [{ body: "pizza", - mimetype: "text/plain", }], })).toEqual({ text: "pizza", }); }); - it('parses event content with html', () => { + it('parses event content with plain text topic', () => { expect(parseTopicContent({ topic: "pizza", [M_TOPIC.name]: [{ body: "pizza", mimetype: "text/plain", - }, { + }], + })).toEqual({ + text: "pizza", + }); + }); + + it('parses event content with html topic', () => { + expect(parseTopicContent({ + topic: "pizza", + [M_TOPIC.name]: [{ body: "pizza", mimetype: "text/html", }], diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index d1562d766cc..489aa167950 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -33,7 +33,7 @@ import { ReceiptType } from "../../src/@types/read_receipts"; import * as testUtils from "../test-utils/test-utils"; import { makeBeaconInfoContent } from "../../src/content-helpers"; import { M_BEACON_INFO } from "../../src/@types/beacon"; -import { Room } from "../../src"; +import { ContentHelpers, Room } from "../../src"; import { makeBeaconEvent } from "../test-utils/beacon"; jest.useFakeTimers(); @@ -1104,6 +1104,41 @@ describe("MatrixClient", function() { }); }); + describe("setRoomTopic", () => { + const roomId = "!foofoofoofoofoofoo:matrix.org"; + const createSendStateEventMock = (topic: string, htmlTopic?: string) => { + return jest.fn() + .mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => { + expect(roomId).toEqual(roomId); + expect(eventType).toEqual(EventType.RoomTopic); + expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic)); + expect(stateKey).toBeUndefined(); + return Promise.resolve(); + }); + }; + + it("is called with plain text topic and sends state event", async () => { + const sendStateEvent = createSendStateEventMock("pizza"); + client.sendStateEvent = sendStateEvent; + await client.setRoomTopic(roomId, "pizza"); + expect(sendStateEvent).toHaveBeenCalledTimes(1); + }); + + it("is called with plain text topic and callback and sends state event", async () => { + const sendStateEvent = createSendStateEventMock("pizza"); + client.sendStateEvent = sendStateEvent; + await client.setRoomTopic(roomId, "pizza", () => {}); + expect(sendStateEvent).toHaveBeenCalledTimes(1); + }); + + it("is called with plain text and HTML topic and sends state event", async () => { + const sendStateEvent = createSendStateEventMock("pizza", "pizza"); + client.sendStateEvent = sendStateEvent; + await client.setRoomTopic(roomId, "pizza", "pizza"); + expect(sendStateEvent).toHaveBeenCalledTimes(1); + }); + }); + describe("setPassword", () => { const auth = { session: 'abcdef', type: 'foo' }; const newPassword = 'newpassword';