1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

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: vector-im/element-web#5180
Signed-off-by: Johannes Marbach <johannesm@element.io>

[MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767

* Use correct MSC

* Add overloads for setRoomTopic

* Fix indentation

* Add more tests to pass the quality gate

Co-authored-by: Johannes Marbach <jm@Johanness-Mini.fritz.box>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Johannes Marbach
2022-05-16 11:37:34 +02:00
committed by GitHub
parent ba1f6ffc84
commit f44510e65f
5 changed files with 221 additions and 5 deletions

View File

@ -17,7 +17,13 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk"; import { REFERENCE_RELATION } from "matrix-events-sdk";
import { LocationAssetType, M_ASSET, M_LOCATION, M_TIMESTAMP } from "../../src/@types/location"; 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('Beacon content helpers', () => {
describe('makeBeaconInfoContent()', () => { describe('makeBeaconInfoContent()', () => {
@ -122,3 +128,68 @@ 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", "<b>pizza</b>")).toEqual({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
mimetype: "text/plain",
}, {
body: "<b>pizza</b>",
mimetype: "text/html",
}],
});
});
});
describe('parseTopicContent()', () => {
it('parses event content with plain text topic without mimetype', () => {
expect(parseTopicContent({
topic: "pizza",
[M_TOPIC.name]: [{
body: "pizza",
}],
})).toEqual({
text: "pizza",
});
});
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: "<b>pizza</b>",
mimetype: "text/html",
}],
})).toEqual({
text: "pizza",
html: "<b>pizza</b>",
});
});
});
});

View File

@ -33,7 +33,7 @@ import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils"; import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers"; import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon"; import { M_BEACON_INFO } from "../../src/@types/beacon";
import { Room } from "../../src"; import { ContentHelpers, Room } from "../../src";
import { makeBeaconEvent } from "../test-utils/beacon"; import { makeBeaconEvent } from "../test-utils/beacon";
jest.useFakeTimers(); 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", "<b>pizza</b>");
client.sendStateEvent = sendStateEvent;
await client.setRoomTopic(roomId, "pizza", "<b>pizza</b>");
expect(sendStateEvent).toHaveBeenCalledTimes(1);
});
});
describe("setPassword", () => { describe("setPassword", () => {
const auth = { session: 'abcdef', type: 'foo' }; const auth = { session: 'abcdef', type: 'foo' };
const newPassword = 'newpassword'; const newPassword = 'newpassword';

62
src/@types/topic.ts Normal file
View File

@ -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 MSC3765
* https://github.com/matrix-org/matrix-spec-proposals/pull/3765
*/
/**
* Eg
* {
* "type": "m.room.topic,
* "state_key": "",
* "content": {
* "topic": "All about **pizza**",
* "m.topic": [{
* "body": "All about **pizza**",
* "mimetype": "text/plain",
* }, {
* "body": "All about <b>pizza</b>",
* "mimetype": "text/html",
* }],
* }
* }
*/
/**
* The event type for an m.topic event (in content)
*/
export const M_TOPIC = new UnstableValue("m.topic", "org.matrix.msc3765.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;

View File

@ -3557,12 +3557,31 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
/** /**
* @param {string} roomId * @param {string} roomId
* @param {string} topic * @param {string} topic
* @param {string} htmlTopic Optional.
* @param {module:client.callback} callback Optional. * @param {module:client.callback} callback Optional.
* @return {Promise} Resolves: TODO * @return {Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response. * @return {module:http-api.MatrixError} Rejects: with an error response.
*/ */
public setRoomTopic(roomId: string, topic: string, callback?: Callback): Promise<ISendEventResponse> { public setRoomTopic(
return this.sendStateEvent(roomId, EventType.RoomTopic, { topic: topic }, undefined, callback); roomId: string,
topic: string,
htmlTopic?: string,
): Promise<ISendEventResponse>;
public setRoomTopic(
roomId: string,
topic: string,
callback?: Callback,
): Promise<ISendEventResponse>;
public setRoomTopic(
roomId: string,
topic: string,
htmlTopicOrCallback?: string | Callback,
): Promise<ISendEventResponse> {
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);
} }
/** /**

View File

@ -16,7 +16,7 @@ limitations under the License.
/** @module ContentHelpers */ /** @module ContentHelpers */
import { REFERENCE_RELATION } from "matrix-events-sdk"; import { isProvided, REFERENCE_RELATION } from "matrix-events-sdk";
import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon"; import { MBeaconEventContent, MBeaconInfoContent, MBeaconInfoEventContent } from "./@types/beacon";
import { MsgType } from "./@types/event"; import { MsgType } from "./@types/event";
@ -32,6 +32,7 @@ import {
MAssetContent, MAssetContent,
LegacyLocationEventContent, LegacyLocationEventContent,
} from "./@types/location"; } from "./@types/location";
import { MRoomTopicEventContent, MTopicContent, M_TOPIC } from "./@types/topic";
/** /**
* Generates the content for a HTML Message event * Generates the content for a HTML Message event
@ -190,6 +191,34 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType); return makeLocationContent(fallbackText, geoUri, timestamp, description, assetType);
}; };
/**
* 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<MTopicContent>(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 };
};
/** /**
* Beacon event helpers * Beacon event helpers
*/ */