Show message previews on the new room list tiles

They're heavily cached.
This commit is contained in:
Travis Ralston 2020-06-10 18:37:59 -06:00
parent 95fb8e3c78
commit 6ccb566587
5 changed files with 166 additions and 11 deletions

View file

@ -42,18 +42,20 @@ export const UPDATE_EVENT = "update";
* help prevent lock conflicts.
*/
export abstract class AsyncStore<T extends Object> extends EventEmitter {
private storeState: T = <T>{};
private storeState: T;
private lock = new AwaitLock();
private readonly dispatcherRef: string;
/**
* Creates a new AsyncStore using the given dispatcher.
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
* @param {T} initialState The initial state for the store.
*/
protected constructor(private dispatcher: Dispatcher<ActionPayload>) {
protected constructor(private dispatcher: Dispatcher<ActionPayload>, initialState: T = <T>{}) {
super();
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
this.storeState = initialState;
}
/**

View file

@ -0,0 +1,132 @@
/*
Copyright 2020 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 { Room } from "matrix-js-sdk/src/models/room";
import { ActionPayload } from "../dispatcher/payloads";
import { AsyncStoreWithClient } from "./AsyncStoreWithClient";
import defaultDispatcher from "../dispatcher/dispatcher";
import { RoomListStoreTempProxy } from "./room-list/RoomListStoreTempProxy";
import { textForEvent } from "../TextForEvent";
import { MatrixEvent } from "matrix-js-sdk/src/models/Event";
import { _t } from "../languageHandler";
const PREVIEWABLE_EVENTS = [
// This is the same list from RiotX
{type: "m.room.message", isState: false},
{type: "m.room.name", isState: true},
{type: "m.room.topic", isState: true},
{type: "m.room.member", isState: true},
{type: "m.room.history_visibility", isState: true},
{type: "m.call.invite", isState: false},
{type: "m.call.hangup", isState: false},
{type: "m.call.answer", isState: false},
{type: "m.room.encrypted", isState: false},
{type: "m.room.encryption", isState: true},
{type: "m.room.third_party_invite", isState: true},
{type: "m.sticker", isState: false},
{type: "m.room.create", isState: true},
];
interface IState {
[roomId: string]: string;
}
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new MessagePreviewStore();
private constructor() {
super(defaultDispatcher, {});
}
public static get instance(): MessagePreviewStore {
return MessagePreviewStore.internalInstance;
}
/**
* Gets the pre-translated preview for a given room
* @param room The room to get the preview for.
* @returns The preview, or null if none present.
*/
public getPreviewForRoom(room: Room): string {
if (!room) return null; // invalid room, just return nothing
// It's faster to do a lookup this way than it is to use Object.keys().includes()
const val = this.state[room.roomId];
if (val !== null && typeof(val) !== "string") {
this.generatePreview(room);
}
return this.state[room.roomId];
}
private generatePreview(room: Room) {
const maxEventsBackwards = 50; // any further and we just assume there's nothing important
const timeline = room.getLiveTimeline();
if (!timeline) return; // usually only happens in tests
const events = timeline.getEvents();
for (let i = events.length - 1; i >= 0; i--) {
if (i === events.length - maxEventsBackwards) return; // limit reached
const event = events[i];
const preview = this.generatePreviewForEvent(event);
if (preview.isPreviewable) {
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: preview.preview});
return; // break - we found some text
}
}
// if we didn't find anything, subscribe ourselves to an update
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: null});
}
protected async onAction(payload: ActionPayload) {
if (!this.matrixClient) return;
// TODO: Remove when new room list is made the default
if (!RoomListStoreTempProxy.isUsingNewStore()) return;
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
const event = payload.event; // TODO: Type out the dispatcher
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
const preview = this.generatePreviewForEvent(event);
if (preview.isPreviewable) {
await this.updateState({[event.getRoomId()]: preview.preview});
return; // break - we found some text
}
}
}
private generatePreviewForEvent(event: MatrixEvent): { isPreviewable: boolean, preview: string } {
if (PREVIEWABLE_EVENTS.some(p => p.type === event.getType() && p.isState === event.isState())) {
const isSelf = event.getSender() === this.matrixClient.getUserId();
let text = textForEvent(event, /*skipUserPrefix=*/isSelf);
if (!text || text.trim().length === 0) text = null; // force null if useless to us
if (text && isSelf) {
// XXX: i18n doesn't really work here if the language doesn't support prefixing.
// We'd ideally somehow route the `You:` bit to the textForEvent call, however
// threading that through is non-trivial.
text = _t("You: %(message)s", {message: text});
}
return {isPreviewable: true, preview: text};
}
return {isPreviewable: false, preview: null};
}
}