From 289ac347645c9b3e5e370279f671e1af22b1ca1f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Aug 2021 18:16:40 -0600 Subject: [PATCH 1/3] Add support for MSC2762's timeline functionality See https://github.com/matrix-org/matrix-widget-api/pull/41 --- .../WidgetCapabilitiesPromptDialog.tsx | 19 +++- src/i18n/strings/en_EN.json | 2 + src/stores/widgets/StopGapWidget.ts | 6 +- src/stores/widgets/StopGapWidgetDriver.ts | 100 +++++++++++------- src/widgets/CapabilityText.tsx | 42 +++++++- 5 files changed, 120 insertions(+), 49 deletions(-) diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx index ebeab191b18..556dc057f92 100644 --- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx +++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2021 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. @@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler"; import { IDialogProps } from "./IDialogProps"; import { Capability, + isTimelineCapability, Widget, WidgetEventCapability, WidgetKind, @@ -30,6 +31,7 @@ import DialogButtons from "../elements/DialogButtons"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { CapabilityText } from "../../../widgets/CapabilityText"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { lexicographicCompare } from "matrix-js-sdk/src/utils"; export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] { return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]"); @@ -102,7 +104,20 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< } public render() { - const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => { + // We specifically order the timeline capabilities down to the bottom. The capability text + // generation cares strongly about this. + const orderedCapabilities = Object.entries(this.state.booleanStates).sort(([capA], [capB]) => { + const isTimelineA = isTimelineCapability(capA); + const isTimelineB = isTimelineCapability(capB); + + if (!isTimelineA && !isTimelineB) return lexicographicCompare(capA, capB); + if (isTimelineA && !isTimelineB) return 1; + if (!isTimelineA && isTimelineB) return -1; + if (isTimelineA && isTimelineB) return lexicographicCompare(capA, capB); + + return 0; + }); + const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => { const text = CapabilityText.for(cap, this.props.widgetKind); const byline = text.byline ? { text.byline } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21859fb1aaa..3b67db374cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -604,6 +604,8 @@ "See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room", "with an empty state key": "with an empty state key", "with state key %(stateKey)s": "with state key %(stateKey)s", + "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", + "The above, but in as well": "The above, but in as well", "Send %(eventType)s events as you in this room": "Send %(eventType)s events as you in this room", "See %(eventType)s events posted to this room": "See %(eventType)s events posted to this room", "Send %(eventType)s events as you in your active room": "Send %(eventType)s events as you in your active room", diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index daa1e0e7873..49653626c15 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + * Copyright 2020 - 2021 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. @@ -408,13 +408,11 @@ export class StopGapWidget extends EventEmitter { private onEvent = (ev: MatrixEvent) => { MatrixClientPeg.get().decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); }; private onEventDecrypted = (ev: MatrixEvent) => { if (ev.isDecryptionFailure()) return; - if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); }; @@ -422,7 +420,7 @@ export class StopGapWidget extends EventEmitter { if (!this.messaging) return; const raw = ev.getEffectiveEvent(); - this.messaging.feedEvent(raw).catch(e => { + this.messaging.feedEvent(raw, this.eventListenerRoomId).catch(e => { console.error("Error sending event to widget: ", e); }); } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 13cd260ef00..78d7c9ede0c 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2020 - 2021 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. @@ -23,6 +23,7 @@ import { MatrixCapabilities, OpenIDRequestState, SimpleObservable, + Symbols, Widget, WidgetDriver, WidgetEventCapability, @@ -44,7 +45,8 @@ import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk"; // TODO: Purge this from the universe @@ -119,9 +121,9 @@ export class StopGapWidgetDriver extends WidgetDriver { return new Set(iterableUnion(allowedSoFar, requested)); } - public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise { + public async sendEvent(eventType: string, content: any, stateKey: string = null, targetRoomId: string = null): Promise { const client = MatrixClientPeg.get(); - const roomId = ActiveRoomObserver.activeRoomId; + const roomId = targetRoomId || ActiveRoomObserver.activeRoomId; if (!client || !roomId) throw new Error("Not in a room or not attached to a client"); @@ -145,48 +147,68 @@ export class StopGapWidgetDriver extends WidgetDriver { return { roomId, eventId: r.event_id }; } - public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { - limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice - + private pickRooms(roomIds: (string | Symbols.AnyRoom)[] = null): Room[] { const client = MatrixClientPeg.get(); - const roomId = ActiveRoomObserver.activeRoomId; - const room = client.getRoom(roomId); - if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); - - const results: MatrixEvent[] = []; - const events = room.getLiveTimeline().getEvents(); // timelines are most recent last - for (let i = events.length - 1; i > 0; i--) { - if (results.length >= limit) break; - - const ev = events[i]; - if (ev.getType() !== eventType || ev.isState()) continue; - if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; - results.push(ev); - } + if (!client) throw new Error("Not attached to a client"); - return results.map(e => e.getEffectiveEvent()); + const targetRooms = roomIds + ? (roomIds.includes(Symbols.AnyRoom) ? client.getVisibleRooms() : roomIds.map(r => client.getRoom(r))) + : [client.getRoom(ActiveRoomObserver.activeRoomId)]; + return targetRooms.filter(r => !!r); } - public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { - limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice - - const client = MatrixClientPeg.get(); - const roomId = ActiveRoomObserver.activeRoomId; - const room = client.getRoom(roomId); - if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); - - const results: MatrixEvent[] = []; - const state: Map = room.currentState.events.get(eventType); - if (state) { - if (stateKey === "" || !!stateKey) { - const forKey = state.get(stateKey); - if (forKey) results.push(forKey); - } else { - results.push(...Array.from(state.values())); + public async readRoomEvents( + eventType: string, + msgtype: string | undefined, + limitPerRoom: number, + roomIds: (string | Symbols.AnyRoom)[] = null, + ): Promise { + limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, 25) : 25; // arbitrary choice + + const rooms = this.pickRooms(roomIds); + const allResults: IEvent[] = []; + for (const room of rooms) { + const results: MatrixEvent[] = []; + const events = room.getLiveTimeline().getEvents(); // timelines are most recent last + for (let i = events.length - 1; i > 0; i--) { + if (results.length >= limitPerRoom) break; + + const ev = events[i]; + if (ev.getType() !== eventType || ev.isState()) continue; + if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; + results.push(ev); } + + results.forEach(e => allResults.push(e.getEffectiveEvent())); } + return allResults; + } - return results.slice(0, limit).map(e => e.event); + public async readStateEvents( + eventType: string, + stateKey: string | undefined, + limitPerRoom: number, + roomIds: (string | Symbols.AnyRoom)[] = null, + ): Promise { + limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, 100) : 100; // arbitrary choice + + const rooms = this.pickRooms(roomIds); + const allResults: IEvent[] = []; + for (const room of rooms) { + const results: MatrixEvent[] = []; + const state: Map = room.currentState.events.get(eventType); + if (state) { + if (stateKey === "" || !!stateKey) { + const forKey = state.get(stateKey); + if (forKey) results.push(forKey); + } else { + results.push(...Array.from(state.values())); + } + } + + results.slice(0, limitPerRoom).forEach(e => allResults.push(e.getEffectiveEvent())); + } + return allResults; } public async askOpenID(observer: SimpleObservable) { diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index 63e34eea7ad..30349fe0f6b 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2021 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. @@ -14,11 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Capability, EventDirection, MatrixCapabilities, WidgetEventCapability, WidgetKind } from "matrix-widget-api"; +import { + Capability, + EventDirection, + getTimelineRoomIDFromCapability, + isTimelineCapability, + isTimelineCapabilityFor, + MatrixCapabilities, Symbols, + WidgetEventCapability, + WidgetKind +} from "matrix-widget-api"; import { _t, _td, TranslatedString } from "../languageHandler"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities"; import React from "react"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import TextWithTooltip from "../components/views/elements/TextWithTooltip"; type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic"; @@ -138,8 +149,31 @@ export class CapabilityText { if (textForKind[GENERIC_WIDGET_KIND]) return { primary: _t(textForKind[GENERIC_WIDGET_KIND]) }; // ... we'll fall through to the generic capability processing at the end of this - // function if we fail to locate a simple string and the capability isn't for an - // event. + // function if we fail to generate a string for the capability. + } + + // Try to handle timeline capabilities. The text here implies that the caller has sorted + // the timeline caps to the end for UI purposes. + if (isTimelineCapability(capability)) { + if (isTimelineCapabilityFor(capability, Symbols.AnyRoom)) { + return { primary: _t("The above, but in any room you are joined or invited to as well") }; + } else { + const roomId = getTimelineRoomIDFromCapability(capability); + const room = MatrixClientPeg.get().getRoom(roomId); + return { + primary: _t("The above, but in as well", {}, { + Room: () => { + if (room) { + return + { room.name } + ; + } else { + return { roomId }; + } + }, + }), + }; + } } // We didn't have a super simple line of text, so try processing the capability as the From f912d9d1b40308f55e0db008abc0f1288f0a255e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Aug 2021 18:25:20 -0600 Subject: [PATCH 2/3] Appease the linter --- src/stores/widgets/StopGapWidgetDriver.ts | 7 ++++++- src/widgets/CapabilityText.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 78d7c9ede0c..45c7d6bd2ea 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -121,7 +121,12 @@ export class StopGapWidgetDriver extends WidgetDriver { return new Set(iterableUnion(allowedSoFar, requested)); } - public async sendEvent(eventType: string, content: any, stateKey: string = null, targetRoomId: string = null): Promise { + public async sendEvent( + eventType: string, + content: any, + stateKey: string = null, + targetRoomId: string = null, + ): Promise { const client = MatrixClientPeg.get(); const roomId = targetRoomId || ActiveRoomObserver.activeRoomId; diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index 30349fe0f6b..8c13a4b2fc5 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -22,7 +22,7 @@ import { isTimelineCapabilityFor, MatrixCapabilities, Symbols, WidgetEventCapability, - WidgetKind + WidgetKind, } from "matrix-widget-api"; import { _t, _td, TranslatedString } from "../languageHandler"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; From 4b557fe0adcd9f72f144af6a5aa6a5f377a10cc7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 1 Sep 2021 13:22:09 -0600 Subject: [PATCH 3/3] Update widget-api --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5c62100587a..6245b2c34e8 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.15", + "matrix-widget-api": "^0.1.0-beta.16", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 4e58909563e..f70f0e75c17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5827,10 +5827,10 @@ matrix-react-test-utils@^0.2.3: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.15: - version "0.1.0-beta.15" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745" - integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg== +matrix-widget-api@^0.1.0-beta.16: + version "0.1.0-beta.16" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.16.tgz#32655f05cab48239b97fe4111a1d0858f2aad61a" + integrity sha512-9zqaNLaM14YDHfFb7WGSUOivGOjYw+w5Su84ZfOl6A4IUy1xT9QPp0nsSA8wNfz0LpxOIPn3nuoF8Tn/40F5tg== dependencies: "@types/events" "^3.0.0" events "^3.2.0"