Add support for MSC2762's timeline functionality
See https://github.com/matrix-org/matrix-widget-api/pull/41
This commit is contained in:
parent
ee95e36a7c
commit
289ac34764
5 changed files with 119 additions and 48 deletions
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { IDialogProps } from "./IDialogProps";
|
||||||
import {
|
import {
|
||||||
Capability,
|
Capability,
|
||||||
|
isTimelineCapability,
|
||||||
Widget,
|
Widget,
|
||||||
WidgetEventCapability,
|
WidgetEventCapability,
|
||||||
WidgetKind,
|
WidgetKind,
|
||||||
|
@ -30,6 +31,7 @@ import DialogButtons from "../elements/DialogButtons";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||||
import { CapabilityText } from "../../../widgets/CapabilityText";
|
import { CapabilityText } from "../../../widgets/CapabilityText";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
|
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
|
||||||
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
|
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
|
||||||
|
@ -102,7 +104,20 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
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 text = CapabilityText.for(cap, this.props.widgetKind);
|
||||||
const byline = text.byline
|
const byline = text.byline
|
||||||
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>
|
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>
|
||||||
|
|
|
@ -604,6 +604,8 @@
|
||||||
"See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room",
|
"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 an empty state key": "with an empty state key",
|
||||||
"with state key %(stateKey)s": "with state key %(stateKey)s",
|
"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 <Room /> as well": "The above, but in <Room /> as well",
|
||||||
"Send <b>%(eventType)s</b> events as you in this room": "Send <b>%(eventType)s</b> events as you in this room",
|
"Send <b>%(eventType)s</b> events as you in this room": "Send <b>%(eventType)s</b> events as you in this room",
|
||||||
"See <b>%(eventType)s</b> events posted to this room": "See <b>%(eventType)s</b> events posted to this room",
|
"See <b>%(eventType)s</b> events posted to this room": "See <b>%(eventType)s</b> events posted to this room",
|
||||||
"Send <b>%(eventType)s</b> events as you in your active room": "Send <b>%(eventType)s</b> events as you in your active room",
|
"Send <b>%(eventType)s</b> events as you in your active room": "Send <b>%(eventType)s</b> events as you in your active room",
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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) => {
|
private onEvent = (ev: MatrixEvent) => {
|
||||||
MatrixClientPeg.get().decryptEventIfNeeded(ev);
|
MatrixClientPeg.get().decryptEventIfNeeded(ev);
|
||||||
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
|
||||||
if (ev.getRoomId() !== this.eventListenerRoomId) return;
|
|
||||||
this.feedEvent(ev);
|
this.feedEvent(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEventDecrypted = (ev: MatrixEvent) => {
|
private onEventDecrypted = (ev: MatrixEvent) => {
|
||||||
if (ev.isDecryptionFailure()) return;
|
if (ev.isDecryptionFailure()) return;
|
||||||
if (ev.getRoomId() !== this.eventListenerRoomId) return;
|
|
||||||
this.feedEvent(ev);
|
this.feedEvent(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -422,7 +420,7 @@ export class StopGapWidget extends EventEmitter {
|
||||||
if (!this.messaging) return;
|
if (!this.messaging) return;
|
||||||
|
|
||||||
const raw = ev.getEffectiveEvent();
|
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);
|
console.error("Error sending event to widget: ", e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -23,6 +23,7 @@ import {
|
||||||
MatrixCapabilities,
|
MatrixCapabilities,
|
||||||
OpenIDRequestState,
|
OpenIDRequestState,
|
||||||
SimpleObservable,
|
SimpleObservable,
|
||||||
|
Symbols,
|
||||||
Widget,
|
Widget,
|
||||||
WidgetDriver,
|
WidgetDriver,
|
||||||
WidgetEventCapability,
|
WidgetEventCapability,
|
||||||
|
@ -44,7 +45,8 @@ import { CHAT_EFFECTS } from "../../effects";
|
||||||
import { containsEmoji } from "../../effects/utils";
|
import { containsEmoji } from "../../effects/utils";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks";
|
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
|
// TODO: Purge this from the universe
|
||||||
|
|
||||||
|
@ -119,9 +121,9 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
return new Set(iterableUnion(allowedSoFar, requested));
|
return new Set(iterableUnion(allowedSoFar, requested));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise<ISendEventDetails> {
|
public async sendEvent(eventType: string, content: any, stateKey: string = null, targetRoomId: string = null): Promise<ISendEventDetails> {
|
||||||
const client = MatrixClientPeg.get();
|
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");
|
if (!client || !roomId) throw new Error("Not in a room or not attached to a client");
|
||||||
|
|
||||||
|
@ -145,18 +147,31 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
return { roomId, eventId: r.event_id };
|
return { roomId, eventId: r.event_id };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<object[]> {
|
private pickRooms(roomIds: (string | Symbols.AnyRoom)[] = null): Room[] {
|
||||||
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
|
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const roomId = ActiveRoomObserver.activeRoomId;
|
if (!client) throw new Error("Not attached to a client");
|
||||||
const room = client.getRoom(roomId);
|
|
||||||
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
|
|
||||||
|
|
||||||
|
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 readRoomEvents(
|
||||||
|
eventType: string,
|
||||||
|
msgtype: string | undefined,
|
||||||
|
limitPerRoom: number,
|
||||||
|
roomIds: (string | Symbols.AnyRoom)[] = null,
|
||||||
|
): Promise<object[]> {
|
||||||
|
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 results: MatrixEvent[] = [];
|
||||||
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
const events = room.getLiveTimeline().getEvents(); // timelines are most recent last
|
||||||
for (let i = events.length - 1; i > 0; i--) {
|
for (let i = events.length - 1; i > 0; i--) {
|
||||||
if (results.length >= limit) break;
|
if (results.length >= limitPerRoom) break;
|
||||||
|
|
||||||
const ev = events[i];
|
const ev = events[i];
|
||||||
if (ev.getType() !== eventType || ev.isState()) continue;
|
if (ev.getType() !== eventType || ev.isState()) continue;
|
||||||
|
@ -164,17 +179,22 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
results.push(ev);
|
results.push(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.map(e => e.getEffectiveEvent());
|
results.forEach(e => allResults.push(e.getEffectiveEvent()));
|
||||||
|
}
|
||||||
|
return allResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<object[]> {
|
public async readStateEvents(
|
||||||
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
|
eventType: string,
|
||||||
|
stateKey: string | undefined,
|
||||||
const client = MatrixClientPeg.get();
|
limitPerRoom: number,
|
||||||
const roomId = ActiveRoomObserver.activeRoomId;
|
roomIds: (string | Symbols.AnyRoom)[] = null,
|
||||||
const room = client.getRoom(roomId);
|
): Promise<object[]> {
|
||||||
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
|
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 results: MatrixEvent[] = [];
|
||||||
const state: Map<string, MatrixEvent> = room.currentState.events.get(eventType);
|
const state: Map<string, MatrixEvent> = room.currentState.events.get(eventType);
|
||||||
if (state) {
|
if (state) {
|
||||||
|
@ -186,7 +206,9 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.slice(0, limit).map(e => e.event);
|
results.slice(0, limitPerRoom).forEach(e => allResults.push(e.getEffectiveEvent()));
|
||||||
|
}
|
||||||
|
return allResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
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 { _t, _td, TranslatedString } from "../languageHandler";
|
||||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
|
import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
|
||||||
import React from "react";
|
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
|
type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic";
|
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]) };
|
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
|
// ... 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
|
// function if we fail to generate a string for the capability.
|
||||||
// event.
|
}
|
||||||
|
|
||||||
|
// 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 <Room /> as well", {}, {
|
||||||
|
Room: () => {
|
||||||
|
if (room) {
|
||||||
|
return <TextWithTooltip tooltip={room.getCanonicalAlias() ?? roomId}>
|
||||||
|
<b>{ room.name }</b>
|
||||||
|
</TextWithTooltip>;
|
||||||
|
} else {
|
||||||
|
return <b><code>{ roomId }</code></b>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We didn't have a super simple line of text, so try processing the capability as the
|
// We didn't have a super simple line of text, so try processing the capability as the
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue