Merge pull request #4751 from matrix-org/travis/room-list/previews
Show message previews on the new room list tiles
This commit is contained in:
commit
f98f045005
5 changed files with 169 additions and 11 deletions
|
@ -265,13 +265,22 @@ function textForServerACLEvent(ev) {
|
||||||
return text + changes.join(" ");
|
return text + changes.join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev, skipUserPrefix) {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
if (skipUserPrefix) {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = ev.getContent().body;
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
message = senderDisplayName + " " + message;
|
||||||
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
|
message = _t('sent an image.');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
|
message = "* " + senderDisplayName + " " + message;
|
||||||
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
|
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
@ -612,8 +621,8 @@ for (const evType of ALL_RULE_TYPES) {
|
||||||
stateHandlers[evType] = textForMjolnirEvent;
|
stateHandlers[evType] = textForMjolnirEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textForEvent(ev) {
|
export function textForEvent(ev, skipUserPrefix) {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
if (handler) return handler(ev);
|
if (handler) return handler(ev, skipUserPrefix);
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import NotificationBadge, { INotificationState, NotificationColor, RoomNotificat
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu";
|
||||||
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
||||||
|
import { MessagePreviewStore } from "../../../stores/MessagePreviewStore";
|
||||||
|
|
||||||
/*******************************************************************
|
/*******************************************************************
|
||||||
* CAUTION *
|
* CAUTION *
|
||||||
|
@ -253,8 +254,17 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
let messagePreview = null;
|
let messagePreview = null;
|
||||||
if (this.props.showMessagePreview) {
|
if (this.props.showMessagePreview) {
|
||||||
// TODO: Actually get the real message preview from state
|
// The preview store heavily caches this info, so should be safe to hammer.
|
||||||
messagePreview = <div className="mx_RoomTile2_messagePreview">I just ate a pie.</div>;
|
const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room);
|
||||||
|
|
||||||
|
// Only show the preview if there is one to show.
|
||||||
|
if (text) {
|
||||||
|
messagePreview = (
|
||||||
|
<div className="mx_RoomTile2_messagePreview">
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nameClasses = classNames({
|
const nameClasses = classNames({
|
||||||
|
|
|
@ -246,6 +246,7 @@
|
||||||
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
|
"%(senderDisplayName)s enabled flair for %(groups)s in this room.": "%(senderDisplayName)s enabled flair for %(groups)s in this room.",
|
||||||
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
"%(senderDisplayName)s disabled flair for %(groups)s in this room.": "%(senderDisplayName)s disabled flair for %(groups)s in this room.",
|
||||||
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
"%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.": "%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for %(oldGroups)s in this room.",
|
||||||
|
"sent an image.": "sent an image.",
|
||||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||||
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
"%(senderName)s set the main address for this room to %(address)s.": "%(senderName)s set the main address for this room to %(address)s.",
|
||||||
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
"%(senderName)s removed the main address for this room.": "%(senderName)s removed the main address for this room.",
|
||||||
|
@ -419,6 +420,7 @@
|
||||||
"Restart": "Restart",
|
"Restart": "Restart",
|
||||||
"Upgrade your Riot": "Upgrade your Riot",
|
"Upgrade your Riot": "Upgrade your Riot",
|
||||||
"A new version of Riot is available!": "A new version of Riot is available!",
|
"A new version of Riot is available!": "A new version of Riot is available!",
|
||||||
|
"You: %(message)s": "You: %(message)s",
|
||||||
"There was an error joining the room": "There was an error joining the room",
|
"There was an error joining the room": "There was an error joining the room",
|
||||||
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
"Sorry, your homeserver is too old to participate in this room.": "Sorry, your homeserver is too old to participate in this room.",
|
||||||
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
"Please contact your homeserver administrator.": "Please contact your homeserver administrator.",
|
||||||
|
|
|
@ -42,18 +42,20 @@ export const UPDATE_EVENT = "update";
|
||||||
* help prevent lock conflicts.
|
* help prevent lock conflicts.
|
||||||
*/
|
*/
|
||||||
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||||
private storeState: T = <T>{};
|
private storeState: T;
|
||||||
private lock = new AwaitLock();
|
private lock = new AwaitLock();
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new AsyncStore using the given dispatcher.
|
* Creates a new AsyncStore using the given dispatcher.
|
||||||
* @param {Dispatcher<ActionPayload>} dispatcher The dispatcher to rely upon.
|
* @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();
|
super();
|
||||||
|
|
||||||
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
|
this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this));
|
||||||
|
this.storeState = initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
135
src/stores/MessagePreviewStore.ts
Normal file
135
src/stores/MessagePreviewStore.ts
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
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},
|
||||||
|
];
|
||||||
|
|
||||||
|
// The maximum number of events we're willing to look back on to get a preview.
|
||||||
|
const MAX_EVENTS_BACKWARDS = 50;
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
[roomId: string]: string | null; // null indicates the preview is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
// We only want to generate a preview if there's one actually missing and not explicitly
|
||||||
|
// set as 'none'.
|
||||||
|
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 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 - MAX_EVENTS_BACKWARDS) 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};
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue