From 601e8209d8ad53e5f807f4342394896a09fd8196 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Jun 2020 21:48:39 +0100 Subject: [PATCH 1/7] Prioritize text on the clipboard over file Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/SendMessageComposer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 25ad192ea4..130135f641 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -427,7 +427,9 @@ export default class SendMessageComposer extends React.Component { _onPaste = (event) => { const {clipboardData} = event; - if (clipboardData.files.length) { + // Prioritize text on the clipboard over files as Office on macOS puts a bitmap + // in the clipboard as well as the content being copied. + if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) { // This actually not so much for 'files' as such (at time of writing // neither chrome nor firefox let you paste a plain file copied // from Finder) but more images copied from a different website From be366bdd7f6fcbd28d13966dd1d86541c4bff140 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 10 Jun 2020 15:05:17 -0600 Subject: [PATCH 2/7] Fix various layout concerns with the new room list It had ~32px of overflow causing a horizontal scrollbar. Turns out the layout engine gets confused when hidden elements are at the end of a flexbox, so we just move them higher in the DOM (where they logically still make sense). The remaining changes are all alignment and misc changes in that respect, and probably not obvious. --- res/css/structures/_LeftPanel2.scss | 5 +++++ res/css/views/rooms/_NotificationBadge.scss | 2 -- res/css/views/rooms/_RoomSublist2.scss | 19 ++++++++++--------- res/css/views/rooms/_RoomTile2.scss | 4 +--- src/components/views/rooms/RoomSublist2.tsx | 4 ++-- 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/res/css/structures/_LeftPanel2.scss b/res/css/structures/_LeftPanel2.scss index 65d23fc23a..bd2a3ba96e 100644 --- a/res/css/structures/_LeftPanel2.scss +++ b/res/css/structures/_LeftPanel2.scss @@ -131,6 +131,11 @@ $roomListMinimizedWidth: 50px; .mx_LeftPanel2_actualRoomListContainer { flex-grow: 1; // fill the available space overflow-y: auto; + width: 100%; + max-width: 100%; + + // Create a flexbox to trick the layout engine + display: flex; } } } diff --git a/res/css/views/rooms/_NotificationBadge.scss b/res/css/views/rooms/_NotificationBadge.scss index 500a7b7e05..9f9698bac5 100644 --- a/res/css/views/rooms/_NotificationBadge.scss +++ b/res/css/views/rooms/_NotificationBadge.scss @@ -28,7 +28,6 @@ limitations under the License. &.mx_NotificationBadge_visible { background-color: $roomtile2-badge-color; - margin-right: 14px; // Create a flexbox to order the count a bit easier display: flex; @@ -46,7 +45,6 @@ limitations under the License. width: 6px; height: 6px; border-radius: 6px; - margin-right: 8px; } &.mx_NotificationBadge_2char { diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 53a01088c9..2c34f28721 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -21,7 +21,7 @@ limitations under the License. display: flex; flex-direction: column; - padding-left: 8px; + margin-left: 8px; margin-top: 12px; margin-bottom: 12px; width: 100%; @@ -30,16 +30,18 @@ limitations under the License. // Create a flexbox to make ordering easy display: flex; align-items: center; + padding-bottom: 8px; + height: 24px; .mx_RoomSublist2_badgeContainer { opacity: 0.8; - padding-right: 7px; + width: 16px; + margin-right: 5px; // aligns with the room tile's badge - // Create another flexbox row because it's super easy to position the badge at - // the end this way. + // Create another flexbox row because it's super easy to position the badge this way. display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; } // Both of these buttons are hidden by default until the list is hovered @@ -77,10 +79,9 @@ limitations under the License. opacity: 0.5; line-height: $font-16px; font-size: $font-12px; - padding-bottom: 8px; - width: 100%; flex: 1; + max-width: calc(100% - 16px); // 16px is the badge width // Ellipsize any text overflow text-overflow: ellipsis; @@ -184,7 +185,7 @@ limitations under the License. &:not(.mx_RoomSublist2_headerContainer_withAux) { // The menu button will be the rightmost button, so make it correctly aligned. .mx_RoomSublist2_menuButton { - margin-right: 16px; + margin-right: 1px; // line it up with the badges on the room tiles } } @@ -218,7 +219,7 @@ limitations under the License. // Show the aux button, but not the list button width: 24px; height: 24px; - margin-right: 16px; + margin-right: 1px; // line it up with the badges on the room tiles visibility: visible; } } diff --git a/res/css/views/rooms/_RoomTile2.scss b/res/css/views/rooms/_RoomTile2.scss index b1fd813655..67b1470550 100644 --- a/res/css/views/rooms/_RoomTile2.scss +++ b/res/css/views/rooms/_RoomTile2.scss @@ -18,9 +18,7 @@ limitations under the License. // Note: the room tile expects to be in a flexbox column container .mx_RoomTile2 { - width: calc(100% - 21px); // 10px for alignment/inset, 8px for padding on sides, 3px for margin margin-bottom: 4px; - margin-right: 3px; padding: 4px; // The tile is also a flexbox row itself @@ -84,7 +82,7 @@ limitations under the License. // the end this way. display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; } // The menu button is hidden by default diff --git a/src/components/views/rooms/RoomSublist2.tsx b/src/components/views/rooms/RoomSublist2.tsx index 0df9c2ad73..12f3e96bac 100644 --- a/src/components/views/rooms/RoomSublist2.tsx +++ b/src/components/views/rooms/RoomSublist2.tsx @@ -237,11 +237,11 @@ export default class RoomSublist2 extends React.Component { > {this.props.label} + {this.renderMenu()} + {addRoomButton}
{badge}
- {this.renderMenu()} - {addRoomButton} ); }} From ed7f4755cc297aafb077cf1cdb09fc17bc5124a9 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 10 Jun 2020 22:05:29 +0100 Subject: [PATCH 3/7] use ButtonEvent instead of MouseEvent | KeyboardEvent for AccessibleButton (make CI happy) Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserMenuButton.tsx | 10 +++++----- src/components/views/elements/AccessibleButton.tsx | 4 +++- src/components/views/rooms/RoomTile2.tsx | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/structures/UserMenuButton.tsx b/src/components/structures/UserMenuButton.tsx index a8a6535805..41b2c3ab60 100644 --- a/src/components/structures/UserMenuButton.tsx +++ b/src/components/structures/UserMenuButton.tsx @@ -31,7 +31,7 @@ import LogoutDialog from "../views/dialogs/LogoutDialog"; import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import {getCustomTheme} from "../../theme"; import {getHostingLink} from "../../utils/HostingLink"; -import AccessibleButton from "../views/elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; interface IProps { } @@ -114,7 +114,7 @@ export default class UserMenuButton extends React.Component { SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme); }; - private onSettingsOpen = (ev: React.MouseEvent, tabId: string) => { + private onSettingsOpen = (ev: ButtonEvent, tabId: string) => { ev.preventDefault(); ev.stopPropagation(); @@ -123,7 +123,7 @@ export default class UserMenuButton extends React.Component { this.setState({menuDisplayed: false}); // also close the menu }; - private onShowArchived = (ev: React.MouseEvent) => { + private onShowArchived = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -131,7 +131,7 @@ export default class UserMenuButton extends React.Component { console.log("TODO: Show archived rooms"); }; - private onProvideFeedback = (ev: React.MouseEvent) => { + private onProvideFeedback = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -139,7 +139,7 @@ export default class UserMenuButton extends React.Component { this.setState({menuDisplayed: false}); // also close the menu }; - private onSignOutClick = (ev: React.MouseEvent) => { + private onSignOutClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 3a542387c5..18dd43ad02 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -19,6 +19,8 @@ import React from 'react'; import {Key} from '../../../Keyboard'; import classnames from 'classnames'; +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent + /** * children: React's magic prop. Represents all children given to the element. * element: (optional) The base element type. "div" by default. @@ -37,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes { tabIndex?: number; disabled?: boolean; className?: string; - onClick?(e?: React.MouseEvent | React.KeyboardEvent): void; + onClick?(e?: ButtonEvent): void; }; interface IAccessibleButtonProps extends React.InputHTMLAttributes { diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index 7778b5be4b..e8056349e2 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -21,7 +21,7 @@ import React, { createRef } from "react"; import { Room } from "matrix-js-sdk/src/models/room"; import classNames from "classnames"; import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex"; -import AccessibleButton from "../../views/elements/AccessibleButton"; +import AccessibleButton, {ButtonEvent} from "../../views/elements/AccessibleButton"; import RoomAvatar from "../../views/avatars/RoomAvatar"; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; @@ -123,7 +123,7 @@ export default class RoomTile2 extends React.Component { this.setState({generalMenuDisplayed: false}); }; - private onTagRoom = (ev: React.MouseEvent, tagId: TagID) => { + private onTagRoom = (ev: ButtonEvent, tagId: TagID) => { ev.preventDefault(); ev.stopPropagation(); @@ -134,7 +134,7 @@ export default class RoomTile2 extends React.Component { } }; - private onLeaveRoomClick = (ev: React.MouseEvent) => { + private onLeaveRoomClick = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); @@ -145,7 +145,7 @@ export default class RoomTile2 extends React.Component { this.setState({generalMenuDisplayed: false}); // hide the menu }; - private onOpenRoomSettings = (ev: React.MouseEvent) => { + private onOpenRoomSettings = (ev: ButtonEvent) => { ev.preventDefault(); ev.stopPropagation(); From 6c31bf1a618b5e5301c2d824684cd4554419529f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 10 Jun 2020 15:15:58 -0600 Subject: [PATCH 4/7] Fix layout when resizing the list width --- res/css/views/rooms/_RoomList2.scss | 2 ++ res/css/views/rooms/_RoomSublist2.scss | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomList2.scss b/res/css/views/rooms/_RoomList2.scss index 89760958f9..40549d25b4 100644 --- a/res/css/views/rooms/_RoomList2.scss +++ b/res/css/views/rooms/_RoomList2.scss @@ -17,6 +17,8 @@ limitations under the License. // TODO: Rename to mx_RoomList during replacement of old component .mx_RoomList2 { + width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists) + // Create a column-based flexbox for the sublists. That's pretty much all we have to // worry about in this stylesheet. display: flex; diff --git a/res/css/views/rooms/_RoomSublist2.scss b/res/css/views/rooms/_RoomSublist2.scss index 2c34f28721..ed17c071b6 100644 --- a/res/css/views/rooms/_RoomSublist2.scss +++ b/res/css/views/rooms/_RoomSublist2.scss @@ -159,7 +159,7 @@ limitations under the License. // either side of the list. We define this after the positioning to // trick the browser. margin-left: 4px; - margin-right: 8px; + margin-right: 4px; } } From 1d84b4668126180a1e81eff2d10af605f157611c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 10 Jun 2020 15:22:17 -0600 Subject: [PATCH 5/7] Fix vertical height causing sublists to wrap --- res/css/views/rooms/_RoomList2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/rooms/_RoomList2.scss b/res/css/views/rooms/_RoomList2.scss index 40549d25b4..5b78020626 100644 --- a/res/css/views/rooms/_RoomList2.scss +++ b/res/css/views/rooms/_RoomList2.scss @@ -23,5 +23,5 @@ limitations under the License. // worry about in this stylesheet. display: flex; flex-direction: column; - flex-wrap: wrap; + flex-wrap: nowrap; // let the column overflow } From 6ccb56658722473c7277069fed444c6e04554b8c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 10 Jun 2020 18:37:59 -0600 Subject: [PATCH 6/7] Show message previews on the new room list tiles They're heavily cached. --- src/TextForEvent.js | 23 ++-- src/components/views/rooms/RoomTile2.tsx | 14 ++- src/i18n/strings/en_EN.json | 2 + src/stores/AsyncStore.ts | 6 +- src/stores/MessagePreviewStore.ts | 132 +++++++++++++++++++++++ 5 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 src/stores/MessagePreviewStore.ts diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 3607d7a676..09cfb67de7 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -265,13 +265,22 @@ function textForServerACLEvent(ev) { return text + changes.join(" "); } -function textForMessageEvent(ev) { +function textForMessageEvent(ev, skipUserPrefix) { const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); let message = senderDisplayName + ': ' + ev.getContent().body; - if (ev.getContent().msgtype === "m.emote") { - message = "* " + senderDisplayName + " " + message; - } else if (ev.getContent().msgtype === "m.image") { - message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName}); + if (skipUserPrefix) { + message = ev.getContent().body; + if (ev.getContent().msgtype === "m.emote") { + 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; } @@ -612,8 +621,8 @@ for (const evType of ALL_RULE_TYPES) { stateHandlers[evType] = textForMjolnirEvent; } -export function textForEvent(ev) { +export function textForEvent(ev, skipUserPrefix) { const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()]; - if (handler) return handler(ev); + if (handler) return handler(ev, skipUserPrefix); return ''; } diff --git a/src/components/views/rooms/RoomTile2.tsx b/src/components/views/rooms/RoomTile2.tsx index e8056349e2..2b1c418294 100644 --- a/src/components/views/rooms/RoomTile2.tsx +++ b/src/components/views/rooms/RoomTile2.tsx @@ -30,6 +30,7 @@ import NotificationBadge, { INotificationState, NotificationColor, RoomNotificat import { _t } from "../../../languageHandler"; import { ContextMenu, ContextMenuButton } from "../../structures/ContextMenu"; import { DefaultTagID, TagID } from "../../../stores/room-list/models"; +import { MessagePreviewStore } from "../../../stores/MessagePreviewStore"; /******************************************************************* * CAUTION * @@ -253,8 +254,17 @@ export default class RoomTile2 extends React.Component { let messagePreview = null; if (this.props.showMessagePreview) { - // TODO: Actually get the real message preview from state - messagePreview =
I just ate a pie.
; + // The preview store heavily caches this info, so should be safe to hammer. + const text = MessagePreviewStore.instance.getPreviewForRoom(this.props.room); + + // Only show the preview if there is one to show. + if (text) { + messagePreview = ( +
+ {text} +
+ ); + } } const nameClasses = classNames({ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dcd6819368..81577d740e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 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.", + "sent an image.": "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 removed the main address for this room.": "%(senderName)s removed the main address for this room.", @@ -419,6 +420,7 @@ "Restart": "Restart", "Upgrade your Riot": "Upgrade your Riot", "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", "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.", diff --git a/src/stores/AsyncStore.ts b/src/stores/AsyncStore.ts index 3519050078..1977e808dc 100644 --- a/src/stores/AsyncStore.ts +++ b/src/stores/AsyncStore.ts @@ -42,18 +42,20 @@ export const UPDATE_EVENT = "update"; * help prevent lock conflicts. */ export abstract class AsyncStore extends EventEmitter { - private storeState: T = {}; + private storeState: T; private lock = new AwaitLock(); private readonly dispatcherRef: string; /** * Creates a new AsyncStore using the given dispatcher. * @param {Dispatcher} dispatcher The dispatcher to rely upon. + * @param {T} initialState The initial state for the store. */ - protected constructor(private dispatcher: Dispatcher) { + protected constructor(private dispatcher: Dispatcher, initialState: T = {}) { super(); this.dispatcherRef = dispatcher.register(this.onDispatch.bind(this)); + this.storeState = initialState; } /** diff --git a/src/stores/MessagePreviewStore.ts b/src/stores/MessagePreviewStore.ts new file mode 100644 index 0000000000..d0d4664951 --- /dev/null +++ b/src/stores/MessagePreviewStore.ts @@ -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 { + 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}; + } +} From 853ae6d5bf3a60d689708e8d10e52bd9d3eba6a3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 10 Jun 2020 18:56:21 -0600 Subject: [PATCH 7/7] misc code quality improvements --- src/stores/MessagePreviewStore.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/stores/MessagePreviewStore.ts b/src/stores/MessagePreviewStore.ts index d0d4664951..64d65a72f3 100644 --- a/src/stores/MessagePreviewStore.ts +++ b/src/stores/MessagePreviewStore.ts @@ -20,7 +20,7 @@ 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 { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { _t } from "../languageHandler"; const PREVIEWABLE_EVENTS = [ @@ -40,8 +40,11 @@ const PREVIEWABLE_EVENTS = [ {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; + [roomId: string]: string | null; // null indicates the preview is empty } export class MessagePreviewStore extends AsyncStoreWithClient { @@ -64,6 +67,8 @@ export class MessagePreviewStore extends AsyncStoreWithClient { 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); @@ -73,14 +78,12 @@ export class MessagePreviewStore extends AsyncStoreWithClient { } 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 + if (i === events.length - MAX_EVENTS_BACKWARDS) return; // limit reached const event = events[i]; const preview = this.generatePreviewForEvent(event);