Skip to content

Commit f44510e

Browse files
JohennesJohannes Marbacht3chguy
authored
Add support for HTML renderings of room topics (#2272)
* Add support for HTML renderings of room topics Based on extensible events as defined in [MSC1767] Relates to: element-hq/element-web#5180 Signed-off-by: Johannes Marbach <[email protected]> [MSC1767]: matrix-org/matrix-spec-proposals#1767 * Use correct MSC * Add overloads for setRoomTopic * Fix indentation * Add more tests to pass the quality gate Co-authored-by: Johannes Marbach <[email protected]> Co-authored-by: Michael Telatynski <[email protected]>
1 parent ba1f6ff commit f44510e

File tree

5 files changed

+221
-5
lines changed

5 files changed

+221
-5
lines changed

spec/unit/content-helpers.spec.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ limitations under the License.
1717
import { REFERENCE_RELATION } from "matrix-events-sdk";
1818

1919
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location";
20-
import { makeBeaconContent, makeBeaconInfoContent } from "../../src/content-helpers";
20+
import { M_TOPIC } from "../../src/@types/topic";
21+
import {
22+
makeBeaconContent,
23+
makeBeaconInfoContent,
24+
makeTopicContent,
25+
parseTopicContent,
26+
} from "../../src/content-helpers";
2127

2228
describe('Beacon content helpers', () => {
2329
describe('makeBeaconInfoContent()', () => {
@@ -122,3 +128,68 @@ describe('Beacon content helpers', () => {
122128
});
123129
});
124130
});
131+
132+
describe('Topic content helpers', () => {
133+
describe('makeTopicContent()', () => {
134+
it('creates fully defined event content without html', () => {
135+
expect(makeTopicContent("pizza")).toEqual({
136+
topic: "pizza",
137+
[M_TOPIC.name]: [{
138+
body: "pizza",
139+
mimetype: "text/plain",
140+
}],
141+
});
142+
});
143+
144+
it('creates fully defined event content with html', () => {
145+
expect(makeTopicContent("pizza", "<b>pizza</b>")).toEqual({
146+
topic: "pizza",
147+
[M_TOPIC.name]: [{
148+
body: "pizza",
149+
mimetype: "text/plain",
150+
}, {
151+
body: "<b>pizza</b>",
152+
mimetype: "text/html",
153+
}],
154+
});
155+
});
156+
});
157+
158+
describe('parseTopicContent()', () => {
159+
it('parses event content with plain text topic without mimetype', () => {
160+
expect(parseTopicContent({
161+
topic: "pizza",
162+
[M_TOPIC.name]: [{
163+
body: "pizza",
164+
}],
165+
})).toEqual({
166+
text: "pizza",
167+
});
168+
});
169+
170+
it('parses event content with plain text topic', () => {
171+
expect(parseTopicContent({
172+
topic: "pizza",
173+
[M_TOPIC.name]: [{
174+
body: "pizza",
175+
mimetype: "text/plain",
176+
}],
177+
})).toEqual({
178+
text: "pizza",
179+
});
180+
});
181+
182+
it('parses event content with html topic', () => {
183+
expect(parseTopicContent({
184+
topic: "pizza",
185+
[M_TOPIC.name]: [{
186+
body: "<b>pizza</b>",
187+
mimetype: "text/html",
188+
}],
189+
})).toEqual({
190+
text: "pizza",
191+
html: "<b>pizza</b>",
192+
});
193+
});
194+
});
195+
});

spec/unit/matrix-client.spec.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ReceiptType } from "../../src/@types/read_receipts";
3333
import * as testUtils from "../test-utils/test-utils";
3434
import { makeBeaconInfoContent } from "../../src/content-helpers";
3535
import { M_BEACON_INFO } from "../../src/@types/beacon";
36-
import { Room } from "../../src";
36+
import { ContentHelpers, Room } from "../../src";
3737
import { makeBeaconEvent } from "../test-utils/beacon";
3838

3939
jest.useFakeTimers();
@@ -1104,6 +1104,41 @@ describe("MatrixClient", function() {
11041104
});
11051105
});
11061106

1107+
describe("setRoomTopic", () => {
1108+
const roomId = "!foofoofoofoofoofoo:matrix.org";
1109+
const createSendStateEventMock = (topic: string, htmlTopic?: string) => {
1110+
return jest.fn()
1111+
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
1112+
expect(roomId).toEqual(roomId);
1113+
expect(eventType).toEqual(EventType.RoomTopic);
1114+
expect(content).toMatchObject(ContentHelpers.makeTopicContent(topic, htmlTopic));
1115+
expect(stateKey).toBeUndefined();
1116+
return Promise.resolve();
1117+
});
1118+
};
1119+
1120+
it("is called with plain text topic and sends state event", async () => {
1121+
const sendStateEvent = createSendStateEventMock("pizza");
1122+
client.sendStateEvent = sendStateEvent;
1123+
await client.setRoomTopic(roomId, "pizza");
1124+
expect(sendStateEvent).toHaveBeenCalledTimes(1);
1125+
});
1126+
1127+
it("is called with plain text topic and callback and sends state event", async () => {
1128+
const sendStateEvent = createSendStateEventMock("pizza");
1129+
client.sendStateEvent = sendStateEvent;
1130+
await client.setRoomTopic(roomId, "pizza", () => {});
1131+
expect(sendStateEvent).toHaveBeenCalledTimes(1);
1132+
});
1133+
1134+
it("is called with plain text and HTML topic and sends state event", async () => {
1135+
const sendStateEvent = createSendStateEventMock("pizza", "<b>pizza</b>");
1136+
client.sendStateEvent = sendStateEvent;
1137+
await client.setRoomTopic(roomId, "pizza", "<b>pizza</b>");
1138+
expect(sendStateEvent).toHaveBeenCalledTimes(1);
1139+
});
1140+
});
1141+
11071142
describe("setPassword", () => {
11081143
const auth = { session: 'abcdef', type: 'foo' };
11091144
const newPassword = 'newpassword';

src/@types/topic.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { EitherAnd, IMessageRendering } from "matrix-events-sdk";
18+
19+
import { UnstableValue } from "../NamespacedValue";
20+
21+
/**
22+
* Extensible topic event type based on MSC3765
23+
* https://github.com/matrix-org/matrix-spec-proposals/pull/3765
24+
*/
25+
26+
/**
27+
* Eg
28+
* {
29+
* "type": "m.room.topic,
30+
* "state_key": "",
31+
* "content": {
32+
* "topic": "All about **pizza**",
33+
* "m.topic": [{
34+
* "body": "All about **pizza**",
35+
* "mimetype": "text/plain",
36+
* }, {
37+
* "body": "All about <b>pizza</b>",
38+
* "mimetype": "text/html",
39+
* }],
40+
* }
41+
* }
42+
*/
43+
44+
/**
45+
* The event type for an m.topic event (in content)
46+
*/
47+
export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.topic");
48+
49+
/**
50+
* The event content for an m.topic event (in content)
51+
*/
52+
export type MTopicContent = IMessageRendering[];
53+
54+
/**
55+
* The event definition for an m.topic event (in content)
56+
*/
57+
export type MTopicEvent = EitherAnd<{ [M_TOPIC.name]: MTopicContent }, { [M_TOPIC.altName]: MTopicContent }>;
58+
59+
/**
60+
* The event content for an m.room.topic event
61+
*/
62+
export type MRoomTopicEventContent = { topic: string } & MTopicEvent;

src/client.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3557,12 +3557,31 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
35573557
/**
35583558
* @param {string} roomId
35593559
* @param {string} topic
3560+
* @param {string} htmlTopic Optional.
35603561
* @param {module:client.callback} callback Optional.
35613562
* @return {Promise} Resolves: TODO
35623563
* @return {module:http-api.MatrixError} Rejects: with an error response.
35633564
*/
3564-
public setRoomTopic(roomId: string, topic: string, callback?: Callback): Promise<ISendEventResponse> {
3565-
return this.sendStateEvent(roomId, EventType.RoomTopic, { topic: topic }, undefined, callback);
3565+
public setRoomTopic(
3566+
roomId: string,
3567+
topic: string,
3568+
htmlTopic?: string,
3569+
): Promise<ISendEventResponse>;
3570+
public setRoomTopic(
3571+
roomId: string,
3572+
topic: string,
3573+
callback?: Callback,
3574+
): Promise<ISendEventResponse>;
3575+
public setRoomTopic(
3576+
roomId: string,
3577+
topic: string,
3578+
htmlTopicOrCallback?: string | Callback,
3579+
): Promise<ISendEventResponse> {
3580+
const isCallback = typeof htmlTopicOrCallback === 'function';
3581+
const htmlTopic = isCallback ? undefined : htmlTopicOrCallback;
3582+
const callback = isCallback ? htmlTopicOrCallback : undefined;
3583+
const content = ContentHelpers.makeTopicContent(topic, htmlTopic);
3584+
return this.sendStateEvent(roomId, EventType.RoomTopic, content, undefined, callback);
35663585
}
35673586

35683587
/**

src/content-helpers.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ limitations under the License.
1616

1717
/** @module ContentHelpers */
1818

19-
import { REFERENCE_RELATION } from "matrix-events-sdk";
19+
import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk";
2020

2121
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
2222
import { MsgType } from "./@types/event";
@@ -32,6 +32,7 @@ import {
3232
MAssetContent,
3333
LegacyLocationEventContent,
3434
} from "./@types/location";
35+
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
3536

3637
/**
3738
* Generates the content for a HTML Message event
@@ -190,6 +191,34 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
190191
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
191192
};
192193

194+
/**
195+
* Topic event helpers
196+
*/
197+
export type MakeTopicContent = (
198+
topic: string,
199+
htmlTopic?: string,
200+
) => MRoomTopicEventContent;
201+
202+
export const makeTopicContent: MakeTopicContent = (topic, htmlTopic) => {
203+
const renderings = [{ body: topic, mimetype: "text/plain" }];
204+
if (isProvided(htmlTopic)) {
205+
renderings.push({ body: htmlTopic, mimetype: "text/html" });
206+
}
207+
return { topic, [M_TOPIC.name]: renderings };
208+
};
209+
210+
export type TopicState = {
211+
text: string;
212+
html?: string;
213+
};
214+
215+
export const parseTopicContent = (content: MRoomTopicEventContent): TopicState => {
216+
const mtopic = M_TOPIC.findIn<MTopicContent>(content);
217+
const text = mtopic?.find(r => !isProvided(r.mimetype) || r.mimetype === "text/plain")?.body ?? content.topic;
218+
const html = mtopic?.find(r => r.mimetype === "text/html")?.body;
219+
return { text, html };
220+
};
221+
193222
/**
194223
* Beacon event helpers
195224
*/

0 commit comments

Comments
 (0)