You've already forked matrix-js-sdk
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:
@ -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>",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -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
62
src/@types/topic.ts
Normal 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;
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user