-
{_t("You’re all caught up")}
-
{_t("You have no visible notifications.")}
+ content =
+
+
+
+
{ _t("Nothing pinned, yet") }
+ { _t("If you have permissions, open the menu on any message and select " +
+ "
Pin to stick them here.", {}, {
+ b: sub =>
{ sub },
+ }) }
+
;
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9e85ea28c8..302c8709fa 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1717,8 +1717,8 @@
"The homeserver the user you’re verifying is connected to": "The homeserver the user you’re verifying is connected to",
"Yours, or the other users’ internet connection": "Yours, or the other users’ internet connection",
"Yours, or the other users’ session": "Yours, or the other users’ session",
- "You’re all caught up": "You’re all caught up",
- "You have no visible notifications.": "You have no visible notifications.",
+ "Nothing pinned, yet": "Nothing pinned, yet",
+ "If you have permissions, open the menu on any message and select
Pin to stick them here.": "If you have permissions, open the menu on any message and select
Pin to stick them here.",
"Pinned messages": "Pinned messages",
"Room Info": "Room Info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
@@ -2628,6 +2628,8 @@
"Create a new community": "Create a new community",
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.",
"Communities are changing to Spaces": "Communities are changing to Spaces",
+ "You’re all caught up": "You’re all caught up",
+ "You have no visible notifications.": "You have no visible notifications.",
"%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.": "%(brand)s failed to get the protocol list from the homeserver. The homeserver may be too old to support third party networks.",
"%(brand)s failed to get the public room list.": "%(brand)s failed to get the public room list.",
"The homeserver may be unavailable or overloaded.": "The homeserver may be unavailable or overloaded.",
From 48d3e41351bd869c232176d956d8254d17f5dc77 Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sat, 5 Jun 2021 01:23:51 -0400
Subject: [PATCH 03/17] Cache frequently used settings values in RoomContext
Signed-off-by: Robin Townsend
---
src/components/structures/RoomView.tsx | 64 +++++++++++++++++++++-----
src/contexts/RoomContext.ts | 6 ++-
2 files changed, 57 insertions(+), 13 deletions(-)
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 5ffc2fd0da..fa67013ccb 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -155,7 +155,6 @@ export interface IState {
canPeek: boolean;
showApps: boolean;
isPeeking: boolean;
- showReadReceipts: boolean;
showRightPanel: boolean;
// error object, as from the matrix client/server API
// If we failed to load information about the room,
@@ -183,6 +182,11 @@ export interface IState {
canReact: boolean;
canReply: boolean;
layout: Layout;
+ showReadReceipts: boolean;
+ showRedactions: boolean;
+ showJoinLeaves: boolean;
+ showAvatarChanges: boolean;
+ showDisplaynameChanges: boolean;
matrixClientIsReady: boolean;
showUrlPreview?: boolean;
e2eStatus?: E2EStatus;
@@ -200,8 +204,7 @@ export default class RoomView extends React.Component {
private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription;
private readonly rightPanelStoreToken: EventSubscription;
- private readonly showReadReceiptsWatchRef: string;
- private readonly layoutWatcherRef: string;
+ private settingWatchers: string[];
private unmounted = false;
private permalinkCreators: Record = {};
@@ -232,7 +235,6 @@ export default class RoomView extends React.Component {
canPeek: false,
showApps: false,
isPeeking: false,
- showReadReceipts: true,
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
joining: false,
atEndOfLiveTimeline: true,
@@ -242,6 +244,11 @@ export default class RoomView extends React.Component {
canReact: false,
canReply: false,
layout: SettingsStore.getValue("layout"),
+ showReadReceipts: true,
+ showRedactions: true,
+ showJoinLeaves: true,
+ showAvatarChanges: true,
+ showDisplaynameChanges: true,
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0,
};
@@ -268,9 +275,11 @@ export default class RoomView extends React.Component {
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
- this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
- this.onReadReceiptsChange);
- this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onLayoutChange);
+ this.settingWatchers = [
+ SettingsStore.watchSetting("layout", null, () =>
+ this.setState({ layout: SettingsStore.getValue("layout") }),
+ ),
+ ];
}
private onWidgetStoreUpdate = () => {
@@ -327,9 +336,42 @@ export default class RoomView extends React.Component {
// we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
+ showRedactions: SettingsStore.getValue("showRedactions", roomId),
+ showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
+ showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
+ showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
wasContextSwitch: RoomViewStore.getWasContextSwitch(),
};
+ // Add watchers for each of the settings we just looked up
+ this.settingWatchers = this.settingWatchers.concat([
+ SettingsStore.watchSetting("showReadReceipts", null, () =>
+ this.setState({
+ showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showRedactions", null, () =>
+ this.setState({
+ showRedactions: SettingsStore.getValue("showRedactions", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showJoinLeaves", null, () =>
+ this.setState({
+ showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showAvatarChanges", null, () =>
+ this.setState({
+ showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
+ }),
+ ),
+ SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
+ this.setState({
+ showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
+ }),
+ ),
+ ]);
+
if (!initial && this.state.shouldPeek && !newState.shouldPeek) {
// Stop peeking because we have joined this room now
this.context.stopPeeking();
@@ -638,10 +680,6 @@ export default class RoomView extends React.Component {
);
}
- if (this.showReadReceiptsWatchRef) {
- SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
- }
-
// cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall();
@@ -649,7 +687,9 @@ export default class RoomView extends React.Component {
// console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme
- SettingsStore.unwatchSetting(this.layoutWatcherRef);
+ for (const watcher of this.settingWatchers) {
+ SettingsStore.unwatchSetting(watcher);
+ }
}
private onUserScroll = () => {
diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index e925f8624b..1efa1c03e7 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -31,7 +31,6 @@ const RoomContext = createContext({
canPeek: false,
showApps: false,
isPeeking: false,
- showReadReceipts: true,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
@@ -41,6 +40,11 @@ const RoomContext = createContext({
canReact: false,
canReply: false,
layout: Layout.Group,
+ showReadReceipts: true,
+ showRedactions: true,
+ showJoinLeaves: true,
+ showAvatarChanges: true,
+ showDisplaynameChanges: true,
matrixClientIsReady: false,
dragCounter: 0,
});
From 13196b8146919f8ef781904d02c1edfd5f376dfe Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sat, 5 Jun 2021 01:25:01 -0400
Subject: [PATCH 04/17] Prefer cached settings values in shouldHideEvent
Signed-off-by: Robin Townsend
---
src/shouldHideEvent.ts | 18 +++++++++++++-----
1 file changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/shouldHideEvent.ts b/src/shouldHideEvent.ts
index 2a47b9c417..31d610b28b 100644
--- a/src/shouldHideEvent.ts
+++ b/src/shouldHideEvent.ts
@@ -17,6 +17,7 @@
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import SettingsStore from "./settings/SettingsStore";
+import {IState} from "./components/structures/RoomView";
interface IDiff {
isMemberEvent: boolean;
@@ -47,11 +48,18 @@ function memberEventDiff(ev: MatrixEvent): IDiff {
return diff;
}
-export default function shouldHideEvent(ev: MatrixEvent): boolean {
- // Wrap getValue() for readability. Calling the SettingsStore can be
- // fairly resource heavy, so the checks below should avoid hitting it
- // where possible.
- const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
+/**
+ * Determines whether the given event should be hidden from timelines.
+ * @param ev The event
+ * @param ctx An optional RoomContext to pull cached settings values from to avoid
+ * hitting the settings store
+ */
+export default function shouldHideEvent(ev: MatrixEvent, ctx?: IState): boolean {
+ // Accessing the settings store directly can be expensive if done frequently,
+ // so we should prefer using cached values if a RoomContext is available
+ const isEnabled = ctx ?
+ name => ctx[name] :
+ name => SettingsStore.getValue(name, ev.getRoomId());
// Hide redacted events
if (ev.isRedacted() && !isEnabled('showRedactions')) return true;
From 3bf8e54d7f37477868c1fa229449749bd02f2894 Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sat, 5 Jun 2021 01:25:43 -0400
Subject: [PATCH 05/17] Use cached RoomContext settings values throughout rooms
Signed-off-by: Robin Townsend
---
src/components/structures/MessagePanel.js | 5 ++++-
src/components/structures/RoomView.tsx | 2 +-
src/components/structures/TimelinePanel.js | 5 ++++-
3 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 6709fef814..bca62e7b2f 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -26,6 +26,7 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore';
+import RoomContext from "../../contexts/RoomContext";
import {Layout, LayoutPropType} from "../../settings/Layout";
import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
@@ -152,6 +153,8 @@ export default class MessagePanel extends React.Component {
enableFlair: PropTypes.bool,
};
+ static contextType = RoomContext;
+
constructor(props) {
super(props);
@@ -381,7 +384,7 @@ export default class MessagePanel extends React.Component {
// Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true;
- return !shouldHideEvent(mxEv);
+ return !shouldHideEvent(mxEv, this.context);
}
_readMarkerForEvent(eventId, isLastEvent) {
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index fa67013ccb..b80d909a94 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -859,7 +859,7 @@ export default class RoomView extends React.Component {
// update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change
- } else if (!shouldHideEvent(ev)) {
+ } else if (!shouldHideEvent(ev, this.state)) {
this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1};
});
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index 6300c7532e..c5ae2e87ba 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -26,6 +26,7 @@ import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
+import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity";
import Modal from "../../Modal";
import dis from "../../dispatcher/dispatcher";
@@ -122,6 +123,8 @@ class TimelinePanel extends React.Component {
layout: LayoutPropType,
}
+ static contextType = RoomContext;
+
// a map from room id to read marker event timestamp
static roomReadMarkerTsMap = {};
@@ -1285,7 +1288,7 @@ class TimelinePanel extends React.Component {
const shouldIgnore = !!ev.status || // local echo
(ignoreOwn && ev.sender && ev.sender.userId == myUserId); // own message
- const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev);
+ const isWithoutTile = !haveTileForEvent(ev) || shouldHideEvent(ev, this.context);
if (isWithoutTile || !node) {
// don't start counting if the event should be ignored,
From c24b239478aef6e4e19e3651a9f8376753f17f12 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 5 Jun 2021 13:36:25 +0000
Subject: [PATCH 06/17] Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests
Bumps [ws](https://github.com/websockets/ws) from 6.2.1 to 6.2.2.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits)
---
updated-dependencies:
- dependency-name: ws
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
---
test/end-to-end-tests/yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/test/end-to-end-tests/yarn.lock b/test/end-to-end-tests/yarn.lock
index 97b348fe50..bc942c4f51 100644
--- a/test/end-to-end-tests/yarn.lock
+++ b/test/end-to-end-tests/yarn.lock
@@ -760,9 +760,9 @@ wrappy@1:
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^6.1.0:
- version "6.2.1"
- resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
- integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+ version "6.2.2"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
+ integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
dependencies:
async-limiter "~1.0.0"
From 0f64f4d692f36287ace222eb13e07244fa6e1c63 Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sat, 5 Jun 2021 10:49:44 -0400
Subject: [PATCH 07/17] Fix MessagePanel tests
Signed-off-by: Robin Townsend
---
test/components/structures/MessagePanel-test.js | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 5b466b4bb0..4f7fca1759 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -51,8 +51,20 @@ class WrappedMessagePanel extends React.Component {
};
render() {
+ const roomContext = {
+ room,
+ roomId: room.roomId,
+ canReact: true,
+ canReply: true,
+ showReadReceipts: true,
+ showRedactions: false,
+ showJoinLeaves: false,
+ showAvatarChanges: false,
+ showDisplaynameChanges: true,
+ };
+
return
-
+
;
From 903d4d252a0c2ac6043ec24251d4c446f33d7990 Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sun, 6 Jun 2021 23:06:56 -0400
Subject: [PATCH 08/17] Add optimized function to determine whether event has
text to display
Signed-off-by: Robin Townsend
---
src/TextForEvent.js | 274 ++++++++++++----------
src/components/structures/MessagePanel.js | 9 +-
src/components/views/rooms/EventTile.tsx | 4 +-
3 files changed, 155 insertions(+), 132 deletions(-)
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 86f9ff20f4..22ce0dd9cf 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -21,6 +21,10 @@ import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
+// These functions are frequently used just to check whether an event has
+// any text to display at all. For this reason they return deferred values
+// to avoid the expense of looking up translations when they're not needed.
+
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender ? ev.sender.name : ev.getSender();
@@ -28,84 +32,84 @@ function textForMemberEvent(ev) {
const prevContent = ev.getPrevContent();
const content = ev.getContent();
- const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
+ const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) {
case 'invite': {
const threePidContent = content.third_party_invite;
if (threePidContent) {
if (threePidContent.display_name) {
- return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
+ return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
targetName,
displayName: threePidContent.display_name,
});
} else {
- return _t('%(targetName)s accepted an invitation.', {targetName});
+ return () => _t('%(targetName)s accepted an invitation.', {targetName});
}
} else {
- return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
+ return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
}
}
case 'ban':
- return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
+ return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
case 'join':
if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
- return _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
+ return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
oldDisplayName: prevContent.displayname,
displayName: content.displayname,
});
} else if (!prevContent.displayname && content.displayname) {
- return _t('%(senderName)s set their display name to %(displayName)s.', {
+ return () => _t('%(senderName)s set their display name to %(displayName)s.', {
senderName: ev.getSender(),
displayName: content.displayname,
});
} else if (prevContent.displayname && !content.displayname) {
- return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
+ return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
senderName,
oldDisplayName: prevContent.displayname,
});
} else if (prevContent.avatar_url && !content.avatar_url) {
- return _t('%(senderName)s removed their profile picture.', {senderName});
+ return () => _t('%(senderName)s removed their profile picture.', {senderName});
} else if (prevContent.avatar_url && content.avatar_url &&
prevContent.avatar_url !== content.avatar_url) {
- return _t('%(senderName)s changed their profile picture.', {senderName});
+ return () => _t('%(senderName)s changed their profile picture.', {senderName});
} else if (!prevContent.avatar_url && content.avatar_url) {
- return _t('%(senderName)s set a profile picture.', {senderName});
+ return () => _t('%(senderName)s set a profile picture.', {senderName});
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
// This is a null rejoin, it will only be visible if the Labs option is enabled
- return _t("%(senderName)s made no change.", {senderName});
+ return () => _t("%(senderName)s made no change.", {senderName});
} else {
- return "";
+ return null;
}
} else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
- return _t('%(targetName)s joined the room.', {targetName});
+ return () => _t('%(targetName)s joined the room.', {targetName});
}
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (prevContent.membership === "invite") {
- return _t('%(targetName)s rejected the invitation.', {targetName});
+ return () => _t('%(targetName)s rejected the invitation.', {targetName});
} else {
- return _t('%(targetName)s left the room.', {targetName});
+ return () => _t('%(targetName)s left the room.', {targetName});
}
} else if (prevContent.membership === "ban") {
- return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
+ return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
} else if (prevContent.membership === "invite") {
- return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
+ return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
senderName,
targetName,
- }) + ' ' + reason;
+ }) + ' ' + getReason();
} else if (prevContent.membership === "join") {
- return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
+ return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
} else {
- return "";
+ return null;
}
}
}
function textForTopicEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
- return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
+ return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName,
topic: ev.getContent().topic,
});
@@ -115,16 +119,16 @@ function textForRoomNameEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
- return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
+ return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
}
if (ev.getPrevContent().name) {
- return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
+ return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
senderDisplayName,
oldRoomName: ev.getPrevContent().name,
newRoomName: ev.getContent().name,
});
}
- return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
+ return () => _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
senderDisplayName,
roomName: ev.getContent().name,
});
@@ -132,19 +136,23 @@ function textForRoomNameEvent(ev) {
function textForTombstoneEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
- return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
+ return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
}
function textForJoinRulesEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().join_rule) {
case "public":
- return _t('%(senderDisplayName)s made the room public to whoever knows the link.', {senderDisplayName});
+ return () => _t('%(senderDisplayName)s made the room public to whoever knows the link.', {
+ senderDisplayName,
+ });
case "invite":
- return _t('%(senderDisplayName)s made the room invite only.', {senderDisplayName});
+ return () => _t('%(senderDisplayName)s made the room invite only.', {
+ senderDisplayName,
+ });
default:
// The spec supports "knock" and "private", however nothing implements these.
- return _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
+ return () => _t('%(senderDisplayName)s changed the join rule to %(rule)s', {
senderDisplayName,
rule: ev.getContent().join_rule,
});
@@ -155,12 +163,12 @@ function textForGuestAccessEvent(ev) {
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
switch (ev.getContent().guest_access) {
case "can_join":
- return _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
+ return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
case "forbidden":
- return _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
+ return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
default:
// There's no other options we can expect, however just for safety's sake we'll do this.
- return _t('%(senderDisplayName)s changed guest access to %(rule)s', {
+ return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
senderDisplayName,
rule: ev.getContent().guest_access,
});
@@ -175,17 +183,17 @@ function textForRelatedGroupsEvent(ev) {
const removed = prevGroups.filter((g) => !groups.includes(g));
if (added.length && !removed.length) {
- return _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
+ return () => _t('%(senderDisplayName)s enabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: added.join(', '),
});
} else if (!added.length && removed.length) {
- return _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
+ return () => _t('%(senderDisplayName)s disabled flair for %(groups)s in this room.', {
senderDisplayName,
groups: removed.join(', '),
});
} else if (added.length && removed.length) {
- return _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
+ return () => _t('%(senderDisplayName)s enabled flair for %(newGroups)s and disabled flair for ' +
'%(oldGroups)s in this room.', {
senderDisplayName,
newGroups: added.join(', '),
@@ -193,7 +201,7 @@ function textForRelatedGroupsEvent(ev) {
});
} else {
// Don't bother rendering this change (because there were no changes)
- return '';
+ return null;
}
}
@@ -207,11 +215,11 @@ function textForServerACLEvent(ev) {
allow_ip_literals: !(prevContent.allow_ip_literals === false),
};
- let text = "";
+ let getText = null;
if (prev.deny.length === 0 && prev.allow.length === 0) {
- text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
+ getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
} else {
- text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
+ getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
}
if (!Array.isArray(current.allow)) {
@@ -220,21 +228,24 @@ function textForServerACLEvent(ev) {
// If we know for sure everyone is banned, mark the room as obliterated
if (current.allow.length === 0) {
- return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
+ return () => getText() + " " +
+ _t("🎉 All servers are banned from participating! This room can no longer be used.");
}
- return text;
+ return getText;
}
function textForMessageEvent(ev) {
- 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});
- }
- return message;
+ return () => {
+ 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});
+ }
+ return message;
+ };
}
function textForCanonicalAliasEvent(ev) {
@@ -248,96 +259,100 @@ function textForCanonicalAliasEvent(ev) {
if (!removedAltAliases.length && !addedAltAliases.length) {
if (newAlias) {
- return _t('%(senderName)s set the main address for this room to %(address)s.', {
+ return () => _t('%(senderName)s set the main address for this room to %(address)s.', {
senderName: senderName,
address: ev.getContent().alias,
});
} else if (oldAlias) {
- return _t('%(senderName)s removed the main address for this room.', {
+ return () => _t('%(senderName)s removed the main address for this room.', {
senderName: senderName,
});
}
} else if (newAlias === oldAlias) {
if (addedAltAliases.length && !removedAltAliases.length) {
- return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
+ return () => _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
senderName: senderName,
addresses: addedAltAliases.join(", "),
count: addedAltAliases.length,
});
} if (removedAltAliases.length && !addedAltAliases.length) {
- return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
+ return () => _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
senderName: senderName,
addresses: removedAltAliases.join(", "),
count: removedAltAliases.length,
});
} if (removedAltAliases.length && addedAltAliases.length) {
- return _t('%(senderName)s changed the alternative addresses for this room.', {
+ return () => _t('%(senderName)s changed the alternative addresses for this room.', {
senderName: senderName,
});
}
} else {
// both alias and alt_aliases where modified
- return _t('%(senderName)s changed the main and alternative addresses for this room.', {
+ return () => _t('%(senderName)s changed the main and alternative addresses for this room.', {
senderName: senderName,
});
}
// in case there is no difference between the two events,
// say something as we can't simply hide the tile from here
- return _t('%(senderName)s changed the addresses for this room.', {
+ return () => _t('%(senderName)s changed the addresses for this room.', {
senderName: senderName,
});
}
function textForCallAnswerEvent(event) {
- const senderName = event.sender ? event.sender.name : _t('Someone');
- const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
- return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
+ return () => {
+ const senderName = event.sender ? event.sender.name : _t('Someone');
+ const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
+ return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
+ };
}
function textForCallHangupEvent(event) {
- const senderName = event.sender ? event.sender.name : _t('Someone');
+ const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent();
- let reason = "";
+ let getReason = () => "";
if (!MatrixClientPeg.get().supportsVoip()) {
- reason = _t('(not supported by this browser)');
+ getReason = () => _t('(not supported by this browser)');
} else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") {
// We couldn't establish a connection at all
- reason = _t('(could not connect media)');
+ getReason = () => _t('(could not connect media)');
} else if (eventContent.reason === "ice_timeout") {
// We established a connection but it died
- reason = _t('(connection failed)');
+ getReason = () => _t('(connection failed)');
} else if (eventContent.reason === "user_media_failed") {
// The other side couldn't open capture devices
- reason = _t("(their device couldn't start the camera / microphone)");
+ getReason = () => _t("(their device couldn't start the camera / microphone)");
} else if (eventContent.reason === "unknown_error") {
// An error code the other side doesn't have a way to express
// (as opposed to an error code they gave but we don't know about,
// in which case we show the error code)
- reason = _t("(an error occurred)");
+ getReason = () => _t("(an error occurred)");
} else if (eventContent.reason === "invite_timeout") {
- reason = _t('(no answer)');
+ getReason = () => _t('(no answer)');
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
- reason = '';
+ getReason = () => '';
} else {
- reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
+ getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
}
}
- return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
+ return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
}
function textForCallRejectEvent(event) {
- const senderName = event.sender ? event.sender.name : _t('Someone');
- return _t('%(senderName)s declined the call.', {senderName});
+ return () => {
+ const senderName = event.sender ? event.sender.name : _t('Someone');
+ return _t('%(senderName)s declined the call.', {senderName});
+ };
}
function textForCallInviteEvent(event) {
- const senderName = event.sender ? event.sender.name : _t('Someone');
+ const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event?
let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp &&
@@ -350,13 +365,21 @@ function textForCallInviteEvent(event) {
// can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) {
- return _t("%(senderName)s placed a voice call.", {senderName});
+ return () => _t("%(senderName)s placed a voice call.", {
+ senderName: getSenderName(),
+ });
} else if (isVoice && !isSupported) {
- return _t("%(senderName)s placed a voice call. (not supported by this browser)", {senderName});
+ return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
+ senderName: getSenderName(),
+ });
} else if (!isVoice && isSupported) {
- return _t("%(senderName)s placed a video call.", {senderName});
+ return () => _t("%(senderName)s placed a video call.", {
+ senderName: getSenderName(),
+ });
} else if (!isVoice && !isSupported) {
- return _t("%(senderName)s placed a video call. (not supported by this browser)", {senderName});
+ return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
+ senderName: getSenderName(),
+ });
}
}
@@ -364,14 +387,13 @@ function textForThreePidInviteEvent(event) {
const senderName = event.sender ? event.sender.name : event.getSender();
if (!isValid3pidInvite(event)) {
- const targetDisplayName = event.getPrevContent().display_name || _t("Someone");
- return _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
+ return () => _t('%(senderName)s revoked the invitation for %(targetDisplayName)s to join the room.', {
senderName,
- targetDisplayName,
+ targetDisplayName: event.getPrevContent().display_name || _t("Someone"),
});
}
- return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
+ return () => _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
senderName,
targetDisplayName: event.getContent().display_name,
});
@@ -381,17 +403,17 @@ function textForHistoryVisibilityEvent(event) {
const senderName = event.sender ? event.sender.name : event.getSender();
switch (event.getContent().history_visibility) {
case 'invited':
- return _t('%(senderName)s made future room history visible to all room members, '
+ return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they are invited.', {senderName});
case 'joined':
- return _t('%(senderName)s made future room history visible to all room members, '
+ return () => _t('%(senderName)s made future room history visible to all room members, '
+ 'from the point they joined.', {senderName});
case 'shared':
- return _t('%(senderName)s made future room history visible to all room members.', {senderName});
+ return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
case 'world_readable':
- return _t('%(senderName)s made future room history visible to anyone.', {senderName});
+ return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
default:
- return _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
+ return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
senderName,
visibility: event.getContent().history_visibility,
});
@@ -403,7 +425,7 @@ function textForPowerEvent(event) {
const senderName = event.sender ? event.sender.name : event.getSender();
if (!event.getPrevContent() || !event.getPrevContent().users ||
!event.getContent() || !event.getContent().users) {
- return '';
+ return null;
}
const userDefault = event.getContent().users_default || 0;
// Construct set of userIds
@@ -418,35 +440,35 @@ function textForPowerEvent(event) {
if (users.indexOf(userId) === -1) users.push(userId);
},
);
- const diff = [];
- // XXX: This is also surely broken for i18n
+ const diffs = [];
users.forEach((userId) => {
// Previous power level
const from = event.getPrevContent().users[userId];
// Current power level
const to = event.getContent().users[userId];
if (to !== from) {
- diff.push(
- _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
- userId,
- fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
- toPowerLevel: Roles.textualPowerLevel(to, userDefault),
- }),
- );
+ diffs.push({ userId, from, to });
}
});
- if (!diff.length) {
- return '';
+ if (!diffs.length) {
+ return null;
}
- return _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
+ // XXX: This is also surely broken for i18n
+ return () => _t('%(senderName)s changed the power level of %(powerLevelDiffText)s.', {
senderName,
- powerLevelDiffText: diff.join(", "),
+ powerLevelDiffText: diffs.map(diff =>
+ _t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
+ userId: diff.userId,
+ fromPowerLevel: Roles.textualPowerLevel(diff.from, userDefault),
+ toPowerLevel: Roles.textualPowerLevel(diff.to, userDefault),
+ }),
+ ).join(", "),
});
}
function textForPinnedEvent(event) {
const senderName = event.sender ? event.sender.name : event.getSender();
- return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
+ return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
}
function textForWidgetEvent(event) {
@@ -464,16 +486,16 @@ function textForWidgetEvent(event) {
// equivalent to that condition.
if (url) {
if (prevUrl) {
- return _t('%(widgetName)s widget modified by %(senderName)s', {
+ return () => _t('%(widgetName)s widget modified by %(senderName)s', {
widgetName, senderName,
});
} else {
- return _t('%(widgetName)s widget added by %(senderName)s', {
+ return () => _t('%(widgetName)s widget added by %(senderName)s', {
widgetName, senderName,
});
}
} else {
- return _t('%(widgetName)s widget removed by %(senderName)s', {
+ return () => _t('%(widgetName)s widget removed by %(senderName)s', {
widgetName, senderName,
});
}
@@ -481,7 +503,7 @@ function textForWidgetEvent(event) {
function textForWidgetLayoutEvent(event) {
const senderName = event.sender?.name || event.getSender();
- return _t("%(senderName)s has updated the widget layout", {senderName});
+ return () => _t("%(senderName)s has updated the widget layout", {senderName});
}
function textForMjolnirEvent(event) {
@@ -492,74 +514,74 @@ function textForMjolnirEvent(event) {
// Rule removed
if (!entity) {
if (USER_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s removed the rule banning users matching %(glob)s",
+ return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
{senderName, glob: prevEntity});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
+ return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
{senderName, glob: prevEntity});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s removed the rule banning servers matching %(glob)s",
+ return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
{senderName, glob: prevEntity});
}
// Unknown type. We'll say something, but we shouldn't end up here.
- return _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
+ return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
}
// Invalid rule
- if (!recommendation || !reason) return _t(`%(senderName)s updated an invalid ban rule`, {senderName});
+ if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
// Rule updated
if (entity === prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// Unknown type. We'll say something but we shouldn't end up here.
- return _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// New rule
if (!prevEntity) {
if (USER_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
- return _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// Unknown type. We'll say something but we shouldn't end up here.
- return _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
+ return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
{senderName, glob: entity, reason});
}
// else the entity !== prevEntity - count as a removal & add
if (USER_RULE_TYPES.includes(event.getType())) {
- return _t(
+ return () => _t(
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
);
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
- return _t(
+ return () => _t(
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
);
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
- return _t(
+ return () => _t(
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
"%(newGlob)s for %(reason)s",
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
@@ -567,7 +589,7 @@ function textForMjolnirEvent(event) {
}
// Unknown type. We'll say something but we shouldn't end up here.
- return _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
+ return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
}
@@ -604,8 +626,12 @@ for (const evType of ALL_RULE_TYPES) {
stateHandlers[evType] = textForMjolnirEvent;
}
+export function hasText(ev) {
+ const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
+ return Boolean(handler?.(ev));
+}
+
export function textForEvent(ev) {
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
- if (handler) return handler(ev);
- return '';
+ return handler?.(ev)?.() || '';
}
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index bca62e7b2f..d040944833 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -30,7 +30,7 @@ import RoomContext from "../../contexts/RoomContext";
import {Layout, LayoutPropType} from "../../settings/Layout";
import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile";
-import {textForEvent} from "../../TextForEvent";
+import {hasText} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
@@ -1180,11 +1180,8 @@ class MemberGrouper {
add(ev) {
if (ev.getType() === 'm.room.member') {
- // We'll just double check that it's worth our time to do so, through an
- // ugly hack. If textForEvent returns something, we should group it for
- // rendering but if it doesn't then we'll exclude it.
- const renderText = textForEvent(ev);
- if (!renderText || renderText.trim().length === 0) return; // quietly ignore
+ // We can ignore any events that don't actually have a message to display
+ if (!hasText(ev)) return;
}
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
ev.getId(),
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 8cec067c39..fda2906f07 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -25,7 +25,7 @@ import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import ReplyThread from "../elements/ReplyThread";
import { _t } from '../../../languageHandler';
-import * as TextForEvent from "../../../TextForEvent";
+import { hasText } from "../../../TextForEvent";
import * as sdk from "../../../index";
import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
@@ -1200,7 +1200,7 @@ export function haveTileForEvent(e) {
const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
- return TextForEvent.textForEvent(e) !== '';
+ return hasText(e);
} else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']);
} else {
From 1e574307d0a74c498429c6e2a22f07c61369550b Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sun, 6 Jun 2021 23:08:47 -0400
Subject: [PATCH 09/17] Cache lowBandwidth setting to speed up BaseAvatar
Signed-off-by: Robin Townsend
---
src/components/structures/RoomView.tsx | 5 +++++
src/components/views/avatars/BaseAvatar.tsx | 15 +++++++++++----
src/contexts/RoomContext.ts | 1 +
3 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index b80d909a94..e4dc90f141 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -182,6 +182,7 @@ export interface IState {
canReact: boolean;
canReply: boolean;
layout: Layout;
+ lowBandwidth: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
showJoinLeaves: boolean;
@@ -244,6 +245,7 @@ export default class RoomView extends React.Component {
canReact: false,
canReply: false,
layout: SettingsStore.getValue("layout"),
+ lowBandwidth: SettingsStore.getValue("lowBandwidth"),
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
@@ -279,6 +281,9 @@ export default class RoomView extends React.Component {
SettingsStore.watchSetting("layout", null, () =>
this.setState({ layout: SettingsStore.getValue("layout") }),
),
+ SettingsStore.watchSetting("lowBandwidth", null, () =>
+ this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
+ ),
];
}
diff --git a/src/components/views/avatars/BaseAvatar.tsx b/src/components/views/avatars/BaseAvatar.tsx
index 8ce05e0a55..6949c14636 100644
--- a/src/components/views/avatars/BaseAvatar.tsx
+++ b/src/components/views/avatars/BaseAvatar.tsx
@@ -22,6 +22,7 @@ import classNames from 'classnames';
import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
+import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units";
@@ -44,12 +45,12 @@ interface IProps {
className?: string;
}
-const calculateUrls = (url, urls) => {
+const calculateUrls = (url, urls, lowBandwidth) => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
- if (!SettingsStore.getValue("lowBandwidth")) {
+ if (!lowBandwidth) {
_urls = urls || [];
if (url) {
@@ -63,7 +64,13 @@ const calculateUrls = (url, urls) => {
};
const useImageUrl = ({url, urls}): [string, () => void] => {
- const [imageUrls, setUrls] = useState(calculateUrls(url, urls));
+ // Since this is a hot code path and the settings store can be slow, we
+ // use the cached lowBandwidth value from the room context if it exists
+ const roomContext = useContext(RoomContext);
+ const lowBandwidth = roomContext ?
+ roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth");
+
+ const [imageUrls, setUrls] = useState(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState(0);
const onError = useCallback(() => {
@@ -71,7 +78,7 @@ const useImageUrl = ({url, urls}): [string, () => void] => {
}, []);
useEffect(() => {
- setUrls(calculateUrls(url, urls));
+ setUrls(calculateUrls(url, urls, lowBandwidth));
setIndex(0);
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index 1efa1c03e7..3464f952a6 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -40,6 +40,7 @@ const RoomContext = createContext({
canReact: false,
canReply: false,
layout: Layout.Group,
+ lowBandwidth: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
From 21ff1f521fa191308852429214d2697ee35adda8 Mon Sep 17 00:00:00 2001
From: Robin Townsend
Date: Sun, 6 Jun 2021 23:14:26 -0400
Subject: [PATCH 10/17] Fix i18n strings
Signed-off-by: Robin Townsend
---
src/i18n/strings/en_EN.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 9e85ea28c8..5108ea7fe2 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -556,8 +556,8 @@
"%(senderName)s made future room history visible to all room members.": "%(senderName)s made future room history visible to all room members.",
"%(senderName)s made future room history visible to anyone.": "%(senderName)s made future room history visible to anyone.",
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s made future room history visible to unknown (%(visibility)s).",
- "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the power level of %(powerLevelDiffText)s.": "%(senderName)s changed the power level of %(powerLevelDiffText)s.",
+ "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"%(widgetName)s widget modified by %(senderName)s": "%(widgetName)s widget modified by %(senderName)s",
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
From 26a030abcd76daf27c9f360000e20d54fc65cb19 Mon Sep 17 00:00:00 2001
From: David Baker
Date: Tue, 8 Jun 2021 12:40:38 +0100
Subject: [PATCH 11/17] Better handling for widgets that fail to load
---
src/components/views/elements/AppTile.js | 40 +++++++++++++++++-------
1 file changed, 28 insertions(+), 12 deletions(-)
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index b898ad2ebc..91ffbf2c27 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -47,9 +47,14 @@ export default class AppTile extends React.Component {
// The key used for PersistedElement
this._persistKey = getPersistKey(this.props.app.id);
- this._sgWidget = new StopGapWidget(this.props);
- this._sgWidget.on("preparing", this._onWidgetPrepared);
- this._sgWidget.on("ready", this._onWidgetReady);
+ try {
+ this._sgWidget = new StopGapWidget(this.props);
+ this._sgWidget.on("preparing", this._onWidgetPrepared);
+ this._sgWidget.on("ready", this._onWidgetReady);
+ } catch (e) {
+ console.log("Failed to construct widget", e);
+ this._sgWidget = null;
+ }
this.iframe = null; // ref to the iframe (callback style)
this.state = this._getNewState(props);
@@ -97,7 +102,7 @@ export default class AppTile extends React.Component {
// Force the widget to be non-persistent (able to be deleted/forgotten)
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this._persistKey);
- this._sgWidget.stop();
+ if (this._sgWidget) this._sgWidget.stop();
}
this.setState({ hasPermissionToLoad });
@@ -117,7 +122,7 @@ export default class AppTile extends React.Component {
componentDidMount() {
// Only fetch IM token on mount if we're showing and have permission to load
- if (this.state.hasPermissionToLoad) {
+ if (this._sgWidget && this.state.hasPermissionToLoad) {
this._startWidget();
}
@@ -146,10 +151,15 @@ export default class AppTile extends React.Component {
if (this._sgWidget) {
this._sgWidget.stop();
}
- this._sgWidget = new StopGapWidget(newProps);
- this._sgWidget.on("preparing", this._onWidgetPrepared);
- this._sgWidget.on("ready", this._onWidgetReady);
- this._startWidget();
+ try {
+ this._sgWidget = new StopGapWidget(newProps);
+ this._sgWidget.on("preparing", this._onWidgetPrepared);
+ this._sgWidget.on("ready", this._onWidgetReady);
+ this._startWidget();
+ } catch (e) {
+ console.log("Failed to construct widget", e);
+ this._sgWidget = null;
+ }
}
_startWidget() {
@@ -161,7 +171,7 @@ export default class AppTile extends React.Component {
_iframeRefChange = (ref) => {
this.iframe = ref;
if (ref) {
- this._sgWidget.start(ref);
+ if (this._sgWidget) this._sgWidget.start(ref);
} else {
this._resetWidget(this.props);
}
@@ -209,7 +219,7 @@ export default class AppTile extends React.Component {
// Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this._persistKey);
- this._sgWidget.stop({forceDestroy: true});
+ if (this._sgWidget) this._sgWidget.stop({forceDestroy: true});
}
_onWidgetPrepared = () => {
@@ -340,7 +350,13 @@ export default class AppTile extends React.Component {
);
- if (!this.state.hasPermissionToLoad) {
+ if (this._sgWidget === null) {
+ appTileBody = (
+