Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 289ac34

Browse files
committed
Add support for MSC2762's timeline functionality
See matrix-org/matrix-widget-api#41
1 parent ee95e36 commit 289ac34

File tree

5 files changed

+120
-49
lines changed

5 files changed

+120
-49
lines changed

src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2020 The Matrix.org Foundation C.I.C.
2+
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler";
2020
import { IDialogProps } from "./IDialogProps";
2121
import {
2222
Capability,
23+
isTimelineCapability,
2324
Widget,
2425
WidgetEventCapability,
2526
WidgetKind,
@@ -30,6 +31,7 @@ import DialogButtons from "../elements/DialogButtons";
3031
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
3132
import { CapabilityText } from "../../../widgets/CapabilityText";
3233
import { replaceableComponent } from "../../../utils/replaceableComponent";
34+
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
3335

3436
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
3537
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
@@ -102,7 +104,20 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
102104
}
103105

104106
public render() {
105-
const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
107+
// We specifically order the timeline capabilities down to the bottom. The capability text
108+
// generation cares strongly about this.
109+
const orderedCapabilities = Object.entries(this.state.booleanStates).sort(([capA], [capB]) => {
110+
const isTimelineA = isTimelineCapability(capA);
111+
const isTimelineB = isTimelineCapability(capB);
112+
113+
if (!isTimelineA && !isTimelineB) return lexicographicCompare(capA, capB);
114+
if (isTimelineA && !isTimelineB) return 1;
115+
if (!isTimelineA && isTimelineB) return -1;
116+
if (isTimelineA && isTimelineB) return lexicographicCompare(capA, capB);
117+
118+
return 0;
119+
});
120+
const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => {
106121
const text = CapabilityText.for(cap, this.props.widgetKind);
107122
const byline = text.byline
108123
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>

src/i18n/strings/en_EN.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,8 @@
604604
"See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room",
605605
"with an empty state key": "with an empty state key",
606606
"with state key %(stateKey)s": "with state key %(stateKey)s",
607+
"The above, but in any room you are joined or invited to as well": "The above, but in any room you are joined or invited to as well",
608+
"The above, but in <Room /> as well": "The above, but in <Room /> as well",
607609
"Send <b>%(eventType)s</b> events as you in this room": "Send <b>%(eventType)s</b> events as you in this room",
608610
"See <b>%(eventType)s</b> events posted to this room": "See <b>%(eventType)s</b> events posted to this room",
609611
"Send <b>%(eventType)s</b> events as you in your active room": "Send <b>%(eventType)s</b> events as you in your active room",

src/stores/widgets/StopGapWidget.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -408,21 +408,19 @@ export class StopGapWidget extends EventEmitter {
408408
private onEvent = (ev: MatrixEvent) => {
409409
MatrixClientPeg.get().decryptEventIfNeeded(ev);
410410
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
411-
if (ev.getRoomId() !== this.eventListenerRoomId) return;
412411
this.feedEvent(ev);
413412
};
414413

415414
private onEventDecrypted = (ev: MatrixEvent) => {
416415
if (ev.isDecryptionFailure()) return;
417-
if (ev.getRoomId() !== this.eventListenerRoomId) return;
418416
this.feedEvent(ev);
419417
};
420418

421419
private feedEvent(ev: MatrixEvent) {
422420
if (!this.messaging) return;
423421

424422
const raw = ev.getEffectiveEvent();
425-
this.messaging.feedEvent(raw).catch(e => {
423+
this.messaging.feedEvent(raw, this.eventListenerRoomId).catch(e => {
426424
console.error("Error sending event to widget: ", e);
427425
});
428426
}

src/stores/widgets/StopGapWidgetDriver.ts

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 The Matrix.org Foundation C.I.C.
2+
* Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import {
2323
MatrixCapabilities,
2424
OpenIDRequestState,
2525
SimpleObservable,
26+
Symbols,
2627
Widget,
2728
WidgetDriver,
2829
WidgetEventCapability,
@@ -44,7 +45,8 @@ import { CHAT_EFFECTS } from "../../effects";
4445
import { containsEmoji } from "../../effects/utils";
4546
import dis from "../../dispatcher/dispatcher";
4647
import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks";
47-
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
48+
import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event";
49+
import { Room } from "matrix-js-sdk";
4850

4951
// TODO: Purge this from the universe
5052

@@ -119,9 +121,9 @@ export class StopGapWidgetDriver extends WidgetDriver {
119121
return new Set(iterableUnion(allowedSoFar, requested));
120122
}
121123

122-
public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise<ISendEventDetails> {
124+
public async sendEvent(eventType: string, content: any, stateKey: string = null, targetRoomId: string = null): Promise<ISendEventDetails> {
123125
const client = MatrixClientPeg.get();
124-
const roomId = ActiveRoomObserver.activeRoomId;
126+
const roomId = targetRoomId || ActiveRoomObserver.activeRoomId;
125127

126128
if (!client || !roomId) throw new Error("Not in a room or not attached to a client");
127129

@@ -145,48 +147,68 @@ export class StopGapWidgetDriver extends WidgetDriver {
145147
return { roomId, eventId: r.event_id };
146148
}
147149

148-
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<object[]> {
149-
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
150-
150+
private pickRooms(roomIds: (string | Symbols.AnyRoom)[] = null): Room[] {
151151
const client = MatrixClientPeg.get();
152-
const roomId = ActiveRoomObserver.activeRoomId;
153-
const room = client.getRoom(roomId);
154-
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
155-
156-
const results: MatrixEvent[] = [];
157-
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
158-
for (let i = events.length - 1; i > 0; i--) {
159-
if (results.length >= limit) break;
160-
161-
const ev = events[i];
162-
if (ev.getType() !== eventType || ev.isState()) continue;
163-
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue;
164-
results.push(ev);
165-
}
152+
if (!client) throw new Error("Not attached to a client");
166153

167-
return results.map(e => e.getEffectiveEvent());
154+
const targetRooms = roomIds
155+
? (roomIds.includes(Symbols.AnyRoom) ? client.getVisibleRooms() : roomIds.map(r => client.getRoom(r)))
156+
: [client.getRoom(ActiveRoomObserver.activeRoomId)];
157+
return targetRooms.filter(r => !!r);
168158
}
169159

170-
public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<object[]> {
171-
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
172-
173-
const client = MatrixClientPeg.get();
174-
const roomId = ActiveRoomObserver.activeRoomId;
175-
const room = client.getRoom(roomId);
176-
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
177-
178-
const results: MatrixEvent[] = [];
179-
const state: Map<string, MatrixEvent> = room.currentState.events.get(eventType);
180-
if (state) {
181-
if (stateKey === "" || !!stateKey) {
182-
const forKey = state.get(stateKey);
183-
if (forKey) results.push(forKey);
184-
} else {
185-
results.push(...Array.from(state.values()));
160+
public async readRoomEvents(
161+
eventType: string,
162+
msgtype: string | undefined,
163+
limitPerRoom: number,
164+
roomIds: (string | Symbols.AnyRoom)[] = null,
165+
): Promise<object[]> {
166+
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, 25) : 25; // arbitrary choice
167+
168+
const rooms = this.pickRooms(roomIds);
169+
const allResults: IEvent[] = [];
170+
for (const room of rooms) {
171+
const results: MatrixEvent[] = [];
172+
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
173+
for (let i = events.length - 1; i > 0; i--) {
174+
if (results.length >= limitPerRoom) break;
175+
176+
const ev = events[i];
177+
if (ev.getType() !== eventType || ev.isState()) continue;
178+
if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue;
179+
results.push(ev);
186180
}
181+
182+
results.forEach(e => allResults.push(e.getEffectiveEvent()));
187183
}
184+
return allResults;
185+
}
188186

189-
return results.slice(0, limit).map(e => e.event);
187+
public async readStateEvents(
188+
eventType: string,
189+
stateKey: string | undefined,
190+
limitPerRoom: number,
191+
roomIds: (string | Symbols.AnyRoom)[] = null,
192+
): Promise<object[]> {
193+
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, 100) : 100; // arbitrary choice
194+
195+
const rooms = this.pickRooms(roomIds);
196+
const allResults: IEvent[] = [];
197+
for (const room of rooms) {
198+
const results: MatrixEvent[] = [];
199+
const state: Map<string, MatrixEvent> = room.currentState.events.get(eventType);
200+
if (state) {
201+
if (stateKey === "" || !!stateKey) {
202+
const forKey = state.get(stateKey);
203+
if (forKey) results.push(forKey);
204+
} else {
205+
results.push(...Array.from(state.values()));
206+
}
207+
}
208+
209+
results.slice(0, limitPerRoom).forEach(e => allResults.push(e.getEffectiveEvent()));
210+
}
211+
return allResults;
190212
}
191213

192214
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {

src/widgets/CapabilityText.tsx

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2020 The Matrix.org Foundation C.I.C.
2+
Copyright 2020 - 2021 The Matrix.org Foundation C.I.C.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -14,11 +14,22 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { Capability, EventDirection, MatrixCapabilities, WidgetEventCapability, WidgetKind } from "matrix-widget-api";
17+
import {
18+
Capability,
19+
EventDirection,
20+
getTimelineRoomIDFromCapability,
21+
isTimelineCapability,
22+
isTimelineCapabilityFor,
23+
MatrixCapabilities, Symbols,
24+
WidgetEventCapability,
25+
WidgetKind
26+
} from "matrix-widget-api";
1827
import { _t, _td, TranslatedString } from "../languageHandler";
1928
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
2029
import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
2130
import React from "react";
31+
import { MatrixClientPeg } from "../MatrixClientPeg";
32+
import TextWithTooltip from "../components/views/elements/TextWithTooltip";
2233

2334
type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention
2435
const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic";
@@ -138,8 +149,31 @@ export class CapabilityText {
138149
if (textForKind[GENERIC_WIDGET_KIND]) return { primary: _t(textForKind[GENERIC_WIDGET_KIND]) };
139150

140151
// ... we'll fall through to the generic capability processing at the end of this
141-
// function if we fail to locate a simple string and the capability isn't for an
142-
// event.
152+
// function if we fail to generate a string for the capability.
153+
}
154+
155+
// Try to handle timeline capabilities. The text here implies that the caller has sorted
156+
// the timeline caps to the end for UI purposes.
157+
if (isTimelineCapability(capability)) {
158+
if (isTimelineCapabilityFor(capability, Symbols.AnyRoom)) {
159+
return { primary: _t("The above, but in any room you are joined or invited to as well") };
160+
} else {
161+
const roomId = getTimelineRoomIDFromCapability(capability);
162+
const room = MatrixClientPeg.get().getRoom(roomId);
163+
return {
164+
primary: _t("The above, but in <Room /> as well", {}, {
165+
Room: () => {
166+
if (room) {
167+
return <TextWithTooltip tooltip={room.getCanonicalAlias() ?? roomId}>
168+
<b>{ room.name }</b>
169+
</TextWithTooltip>;
170+
} else {
171+
return <b><code>{ roomId }</code></b>;
172+
}
173+
},
174+
}),
175+
};
176+
}
143177
}
144178

145179
// We didn't have a super simple line of text, so try processing the capability as the

0 commit comments

Comments
 (0)