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

Add support for MSC2762's timeline functionality #6684

Merged
merged 5 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import {
Capability,
isTimelineCapability,
Widget,
WidgetEventCapability,
WidgetKind,
Expand All @@ -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";

interface IProps extends IDialogProps {
requestedCapabilities: Set<Capability>;
Expand Down Expand Up @@ -91,7 +93,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
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{ text.byline }</span>
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,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 <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",
"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",
Expand Down
6 changes: 2 additions & 4 deletions src/stores/widgets/StopGapWidget.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -408,21 +408,19 @@ 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);
};

private feedEvent(ev: MatrixEvent) {
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);
});
}
Expand Down
105 changes: 66 additions & 39 deletions src/stores/widgets/StopGapWidgetDriver.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,7 @@ import {
MatrixCapabilities,
OpenIDRequestState,
SimpleObservable,
Symbols,
Widget,
WidgetDriver,
WidgetEventCapability,
Expand All @@ -42,7 +43,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

Expand Down Expand Up @@ -133,9 +135,14 @@ export class StopGapWidgetDriver extends WidgetDriver {
return allAllowed;
}

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 roomId = ActiveRoomObserver.activeRoomId;
const roomId = targetRoomId || ActiveRoomObserver.activeRoomId;

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

Expand All @@ -162,48 +169,68 @@ export class StopGapWidgetDriver extends WidgetDriver {
return { roomId, eventId: r.event_id };
}

public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<object[]> {
limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary

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<object[]> {
limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary

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<string, MatrixEvent> = 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<object[]> {
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary

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<object[]> {
limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary

const rooms = this.pickRooms(roomIds);
const allResults: IEvent[] = [];
for (const room of rooms) {
const results: MatrixEvent[] = [];
const state: Map<string, MatrixEvent> = 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<IOpenIDUpdate>) {
Expand Down
42 changes: 38 additions & 4 deletions src/widgets/CapabilityText.tsx
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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";
Expand Down Expand Up @@ -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 <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
Expand Down