diff --git a/spec/unit/content-helpers.spec.ts b/spec/unit/content-helpers.spec.ts
index 71b7344ed6a..ba431d9d586 100644
--- a/spec/unit/content-helpers.spec.ts
+++ b/spec/unit/content-helpers.spec.ts
@@ -17,7 +17,13 @@ limitations under the License.
import { REFERENCE_RELATION } from "matrix-events-sdk";
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()', () => {
@@ -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", "pizza")).toEqual({
+ topic: "pizza",
+ [M_TOPIC.name]: [{
+ body: "pizza",
+ mimetype: "text/plain",
+ }, {
+ body: "pizza",
+ 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: "pizza",
+ mimetype: "text/html",
+ }],
+ })).toEqual({
+ text: "pizza",
+ html: "pizza",
+ });
+ });
+ });
+});
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';
diff --git a/src/@types/topic.ts b/src/@types/topic.ts
new file mode 100644
index 00000000000..0d2708b2e50
--- /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 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 pizza",
+ * "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;
diff --git a/src/client.ts b/src/client.ts
index bb7b9e96e6a..5187a13b0d6 100644
--- a/src/client.ts
+++ b/src/client.ts
@@ -3557,12 +3557,31 @@ export class MatrixClient extends TypedEventEmitter {
- return this.sendStateEvent(roomId, EventType.RoomTopic, { topic: topic }, undefined, callback);
+ public setRoomTopic(
+ roomId: string,
+ topic: string,
+ htmlTopic?: string,
+ ): Promise;
+ 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);
}
/**
diff --git a/src/content-helpers.ts b/src/content-helpers.ts
index 383b9b34396..8c813b7aad6 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 } 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
@@ -190,6 +191,34 @@ export const parseLocationEvent = (wireEventContent: LocationEventWireContent):
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(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
*/