From 4ec8cf11ea572d7e5ac3e1f27bc95e5ac3f9975d Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 15 Jun 2021 18:52:40 -0400
Subject: [PATCH 01/11] Add more types to TextForEvent

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/TextForEvent.ts | 50 +++++++++++++++++++++++----------------------
 1 file changed, 26 insertions(+), 24 deletions(-)

diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts
index 649c53664e..6956da098e 100644
--- a/src/TextForEvent.ts
+++ b/src/TextForEvent.ts
@@ -13,6 +13,8 @@ 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 {MatrixEvent} from "matrix-js-sdk/src/models/event";
+
 import {MatrixClientPeg} from './MatrixClientPeg';
 import { _t } from './languageHandler';
 import * as Roles from './Roles';
@@ -25,7 +27,7 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
 // 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): () => string | null {
+function textForMemberEvent(ev: MatrixEvent): () => string | null {
     // XXX: SYJS-16 "sender is sometimes null for join messages"
     const senderName = ev.sender ? ev.sender.name : ev.getSender();
     const targetName = ev.target ? ev.target.name : ev.getStateKey();
@@ -107,7 +109,7 @@ function textForMemberEvent(ev): () => string | null {
     }
 }
 
-function textForTopicEvent(ev): () => string | null {
+function textForTopicEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     return () => _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
         senderDisplayName,
@@ -115,7 +117,7 @@ function textForTopicEvent(ev): () => string | null {
     });
 }
 
-function textForRoomNameEvent(ev): () => string | null {
+function textForRoomNameEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
 
     if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
@@ -134,12 +136,12 @@ function textForRoomNameEvent(ev): () => string | null {
     });
 }
 
-function textForTombstoneEvent(ev): () => string | null {
+function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
 }
 
-function textForJoinRulesEvent(ev): () => string | null {
+function textForJoinRulesEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     switch (ev.getContent().join_rule) {
         case "public":
@@ -159,7 +161,7 @@ function textForJoinRulesEvent(ev): () => string | null {
     }
 }
 
-function textForGuestAccessEvent(ev): () => string | null {
+function textForGuestAccessEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     switch (ev.getContent().guest_access) {
         case "can_join":
@@ -175,7 +177,7 @@ function textForGuestAccessEvent(ev): () => string | null {
     }
 }
 
-function textForRelatedGroupsEvent(ev): () => string | null {
+function textForRelatedGroupsEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     const groups = ev.getContent().groups || [];
     const prevGroups = ev.getPrevContent().groups || [];
@@ -205,7 +207,7 @@ function textForRelatedGroupsEvent(ev): () => string | null {
     }
 }
 
-function textForServerACLEvent(ev): () => string | null {
+function textForServerACLEvent(ev: MatrixEvent): () => string | null {
     const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     const prevContent = ev.getPrevContent();
     const current = ev.getContent();
@@ -235,7 +237,7 @@ function textForServerACLEvent(ev): () => string | null {
     return getText;
 }
 
-function textForMessageEvent(ev): () => string | null {
+function textForMessageEvent(ev: MatrixEvent): () => string | null {
     return () => {
         const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
         let message = senderDisplayName + ': ' + ev.getContent().body;
@@ -248,7 +250,7 @@ function textForMessageEvent(ev): () => string | null {
     };
 }
 
-function textForCanonicalAliasEvent(ev): () => string | null {
+function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
     const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
     const oldAlias = ev.getPrevContent().alias;
     const oldAltAliases = ev.getPrevContent().alt_aliases || [];
@@ -299,7 +301,7 @@ function textForCanonicalAliasEvent(ev): () => string | null {
     });
 }
 
-function textForCallAnswerEvent(event): () => string | null {
+function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
     return () => {
         const senderName = event.sender ? event.sender.name : _t('Someone');
         const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
@@ -307,7 +309,7 @@ function textForCallAnswerEvent(event): () => string | null {
     };
 }
 
-function textForCallHangupEvent(event): () => string | null {
+function textForCallHangupEvent(event: MatrixEvent): () => string | null {
     const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
     const eventContent = event.getContent();
     let getReason = () => "";
@@ -344,14 +346,14 @@ function textForCallHangupEvent(event): () => string | null {
     return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
 }
 
-function textForCallRejectEvent(event): () => string | null {
+function textForCallRejectEvent(event: MatrixEvent): () => string | null {
     return () => {
         const senderName = event.sender ? event.sender.name : _t('Someone');
         return _t('%(senderName)s declined the call.', {senderName});
     };
 }
 
-function textForCallInviteEvent(event): () => string | null {
+function textForCallInviteEvent(event: MatrixEvent): () => string | null {
     const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
     // FIXME: Find a better way to determine this from the event?
     let isVoice = true;
@@ -383,7 +385,7 @@ function textForCallInviteEvent(event): () => string | null {
     }
 }
 
-function textForThreePidInviteEvent(event): () => string | null {
+function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
 
     if (!isValid3pidInvite(event)) {
@@ -399,7 +401,7 @@ function textForThreePidInviteEvent(event): () => string | null {
     });
 }
 
-function textForHistoryVisibilityEvent(event): () => string | null {
+function textForHistoryVisibilityEvent(event: MatrixEvent): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
     switch (event.getContent().history_visibility) {
         case 'invited':
@@ -421,7 +423,7 @@ function textForHistoryVisibilityEvent(event): () => string | null {
 }
 
 // Currently will only display a change if a user's power level is changed
-function textForPowerEvent(event): () => string | null {
+function textForPowerEvent(event: MatrixEvent): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
     if (!event.getPrevContent() || !event.getPrevContent().users ||
         !event.getContent() || !event.getContent().users) {
@@ -466,12 +468,12 @@ function textForPowerEvent(event): () => string | null {
     });
 }
 
-function textForPinnedEvent(event): () => string | null {
+function textForPinnedEvent(event: MatrixEvent): () => string | null {
     const senderName = event.sender ? event.sender.name : event.getSender();
     return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
 }
 
-function textForWidgetEvent(event): () => string | null {
+function textForWidgetEvent(event: MatrixEvent): () => string | null {
     const senderName = event.getSender();
     const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
     const {name, type, url} = event.getContent() || {};
@@ -501,12 +503,12 @@ function textForWidgetEvent(event): () => string | null {
     }
 }
 
-function textForWidgetLayoutEvent(event): () => string | null {
+function textForWidgetLayoutEvent(event: MatrixEvent): () => string | null {
     const senderName = event.sender?.name || event.getSender();
     return () => _t("%(senderName)s has updated the widget layout", {senderName});
 }
 
-function textForMjolnirEvent(event): () => string | null {
+function textForMjolnirEvent(event: MatrixEvent): () => string | null {
     const senderName = event.getSender();
     const {entity: prevEntity} = event.getPrevContent();
     const {entity, recommendation, reason} = event.getContent();
@@ -594,7 +596,7 @@ function textForMjolnirEvent(event): () => string | null {
 }
 
 interface IHandlers {
-    [type: string]: (ev: any) => (() => string | null);
+    [type: string]: (ev: MatrixEvent) => (() => string | null);
 }
 
 const handlers: IHandlers = {
@@ -630,12 +632,12 @@ for (const evType of ALL_RULE_TYPES) {
     stateHandlers[evType] = textForMjolnirEvent;
 }
 
-export function hasText(ev): boolean {
+export function hasText(ev: MatrixEvent): boolean {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
     return Boolean(handler?.(ev));
 }
 
-export function textForEvent(ev): string {
+export function textForEvent(ev: MatrixEvent): string {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
     return handler?.(ev)?.() || '';
 }

From 819fe419b749f641a941a21cb21c08fbc637aca3 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 15 Jun 2021 18:59:42 -0400
Subject: [PATCH 02/11] Allow using cached setting values in TextForEvent

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/TextForEvent.ts | 26 +++++++++++++++++++-------
 1 file changed, 19 insertions(+), 7 deletions(-)

diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts
index 6956da098e..652a1d6e54 100644
--- a/src/TextForEvent.ts
+++ b/src/TextForEvent.ts
@@ -27,7 +27,7 @@ import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
 // 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: MatrixEvent): () => string | null {
+function textForMemberEvent(ev: MatrixEvent, showHiddenEvents?: boolean): () => string | null {
     // XXX: SYJS-16 "sender is sometimes null for join messages"
     const senderName = ev.sender ? ev.sender.name : ev.getSender();
     const targetName = ev.target ? ev.target.name : ev.getStateKey();
@@ -77,7 +77,7 @@ function textForMemberEvent(ev: MatrixEvent): () => string | null {
                     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});
-                } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
+                } else if (showHiddenEvents ?? 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});
                 } else {
@@ -596,7 +596,7 @@ function textForMjolnirEvent(event: MatrixEvent): () => string | null {
 }
 
 interface IHandlers {
-    [type: string]: (ev: MatrixEvent) => (() => string | null);
+    [type: string]: (ev: MatrixEvent, showHiddenEvents?: boolean) => (() => string | null);
 }
 
 const handlers: IHandlers = {
@@ -632,12 +632,24 @@ for (const evType of ALL_RULE_TYPES) {
     stateHandlers[evType] = textForMjolnirEvent;
 }
 
-export function hasText(ev: MatrixEvent): boolean {
+/**
+ * Determines whether the given event has text to display.
+ * @param ev The event
+ * @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
+ *     to avoid hitting the settings store
+ */
+export function hasText(ev: MatrixEvent, showHiddenEvents?: boolean): boolean {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
-    return Boolean(handler?.(ev));
+    return Boolean(handler?.(ev, showHiddenEvents));
 }
 
-export function textForEvent(ev: MatrixEvent): string {
+/**
+ * Gets the textual content of the given event.
+ * @param ev The event
+ * @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
+ *     to avoid hitting the settings store
+ */
+export function textForEvent(ev: MatrixEvent, showHiddenEvents?: boolean): string {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
-    return handler?.(ev)?.() || '';
+    return handler?.(ev, showHiddenEvents)?.() || '';
 }

From af11878e0c22212093c5a85aa4ce6b9a3dbc77b2 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Wed, 16 Jun 2021 20:40:47 -0400
Subject: [PATCH 03/11] Use cached setting values when calling TextForEvent

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/MessagePanel.js      | 16 +++++++++-------
 src/components/structures/RoomView.tsx         |  7 ++++++-
 src/components/structures/TimelinePanel.js     |  3 ++-
 src/components/views/messages/TextualEvent.js  |  5 ++++-
 src/components/views/rooms/EventTile.tsx       |  4 ++--
 src/components/views/rooms/SearchResultTile.js |  5 ++++-
 src/contexts/RoomContext.ts                    |  1 +
 7 files changed, 28 insertions(+), 13 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index eb9611a6fc..b8d3f4f830 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -41,7 +41,7 @@ const continuedTypes = ['m.sticker', 'm.room.message'];
 
 // check if there is a previous event and it has the same sender as this event
 // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
-function shouldFormContinuation(prevEvent, mxEvent) {
+function shouldFormContinuation(prevEvent, mxEvent, showHiddenEvents) {
     // sanity check inputs
     if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;
     // check if within the max continuation period
@@ -61,7 +61,7 @@ function shouldFormContinuation(prevEvent, mxEvent) {
         mxEvent.sender.getMxcAvatarUrl() !== prevEvent.sender.getMxcAvatarUrl()) return false;
 
     // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile
-    if (!haveTileForEvent(prevEvent)) return false;
+    if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false;
 
     return true;
 }
@@ -202,7 +202,8 @@ export default class MessagePanel extends React.Component {
         this._readReceiptsByUserId = {};
 
         // Cache hidden events setting on mount since Settings is expensive to
-        // query, and we check this in a hot code path.
+        // query, and we check this in a hot code path. This is also cached in
+        // our RoomContext, however we still need a fallback for roomless MessagePanels.
         this._showHiddenEventsInTimeline =
             SettingsStore.getValue("showHiddenEventsInTimeline");
 
@@ -372,11 +373,11 @@ export default class MessagePanel extends React.Component {
             return false; // ignored = no show (only happens if the ignore happens after an event was received)
         }
 
-        if (this._showHiddenEventsInTimeline) {
+        if (this.context?.showHiddenEventsInTimeline ?? this._showHiddenEventsInTimeline) {
             return true;
         }
 
-        if (!haveTileForEvent(mxEv)) {
+        if (!haveTileForEvent(mxEv, this.context?.showHiddenEventsInTimeline)) {
             return false; // no tile = no show
         }
 
@@ -613,7 +614,8 @@ export default class MessagePanel extends React.Component {
         }
 
         // is this a continuation of the previous message?
-        const continuation = !wantsDateSeparator && shouldFormContinuation(prevEvent, mxEv);
+        const continuation = !wantsDateSeparator &&
+            shouldFormContinuation(prevEvent, mxEv, this.context?.showHiddenEventsInTimeline);
 
         const eventId = mxEv.getId();
         const highlight = (eventId === this.props.highlightedEventId);
@@ -1168,7 +1170,7 @@ class MemberGrouper {
     add(ev) {
         if (ev.getType() === 'm.room.member') {
             // We can ignore any events that don't actually have a message to display
-            if (!hasText(ev)) return;
+            if (!hasText(ev, this.context?.showHiddenEventsInTimeline)) return;
         }
         this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
             ev.getId(),
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index fe90d2f873..d1c68f0cc7 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -181,6 +181,7 @@ export interface IState {
     canReply: boolean;
     layout: Layout;
     lowBandwidth: boolean;
+    showHiddenEventsInTimeline: boolean;
     showReadReceipts: boolean;
     showRedactions: boolean;
     showJoinLeaves: boolean;
@@ -244,6 +245,7 @@ export default class RoomView extends React.Component<IProps, IState> {
             canReply: false,
             layout: SettingsStore.getValue("layout"),
             lowBandwidth: SettingsStore.getValue("lowBandwidth"),
+            showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
             showReadReceipts: true,
             showRedactions: true,
             showJoinLeaves: true,
@@ -282,6 +284,9 @@ export default class RoomView extends React.Component<IProps, IState> {
             SettingsStore.watchSetting("lowBandwidth", null, () =>
                 this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
             ),
+            SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
+                this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
+            ),
         ];
     }
 
@@ -1411,7 +1416,7 @@ export default class RoomView extends React.Component<IProps, IState> {
                 continue;
             }
 
-            if (!haveTileForEvent(mxEv)) {
+            if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) {
                 // XXX: can this ever happen? It will make the result count
                 // not match the displayed count.
                 continue;
diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js
index bb62745d98..20f70df4dc 100644
--- a/src/components/structures/TimelinePanel.js
+++ b/src/components/structures/TimelinePanel.js
@@ -1291,7 +1291,8 @@ 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, this.context);
+            const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) ||
+                shouldHideEvent(ev, this.context);
 
             if (isWithoutTile || !node) {
                 // don't start counting if the event should be ignored,
diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.js
index a020cc6c52..0cdd573076 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.js
@@ -17,6 +17,7 @@ limitations under the License.
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import RoomContext from "../../../contexts/RoomContext";
 import * as TextForEvent from "../../../TextForEvent";
 import {replaceableComponent} from "../../../utils/replaceableComponent";
 
@@ -27,8 +28,10 @@ export default class TextualEvent extends React.Component {
         mxEvent: PropTypes.object.isRequired,
     };
 
+    static contextType = RoomContext;
+
     render() {
-        const text = TextForEvent.textForEvent(this.props.mxEvent);
+        const text = TextForEvent.textForEvent(this.props.mxEvent, this.context?.showHiddenEventsInTimeline);
         if (text == null || text.length === 0) return null;
         return (
             <div className="mx_TextualEvent">{ text }</div>
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index 85b9cac2c4..8de371ea15 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -1217,7 +1217,7 @@ function isMessageEvent(ev) {
     return (messageTypes.includes(ev.getType()));
 }
 
-export function haveTileForEvent(e) {
+export function haveTileForEvent(e: MatrixEvent, showHiddenEvents?: boolean) {
     // Only messages have a tile (black-rectangle) if redacted
     if (e.isRedacted() && !isMessageEvent(e)) return false;
 
@@ -1227,7 +1227,7 @@ export function haveTileForEvent(e) {
     const handler = getHandlerTile(e);
     if (handler === undefined) return false;
     if (handler === 'messages.TextualEvent') {
-        return hasText(e);
+        return hasText(e, showHiddenEvents);
     } else if (handler === 'messages.RoomCreate') {
         return Boolean(e.getContent()['predecessor']);
     } else {
diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.js
index 3b79aa6246..2963265317 100644
--- a/src/components/views/rooms/SearchResultTile.js
+++ b/src/components/views/rooms/SearchResultTile.js
@@ -18,6 +18,7 @@ limitations under the License.
 import React from 'react';
 import PropTypes from 'prop-types';
 import * as sdk from '../../../index';
+import RoomContext from "../../../contexts/RoomContext";
 import {haveTileForEvent} from "./EventTile";
 import SettingsStore from "../../../settings/SettingsStore";
 import {UIFeature} from "../../../settings/UIFeature";
@@ -38,6 +39,8 @@ export default class SearchResultTile extends React.Component {
         onHeightChanged: PropTypes.func,
     };
 
+    static contextType = RoomContext;
+
     render() {
         const DateSeparator = sdk.getComponent('messages.DateSeparator');
         const EventTile = sdk.getComponent('rooms.EventTile');
@@ -57,7 +60,7 @@ export default class SearchResultTile extends React.Component {
             if (!contextual) {
                 highlights = this.props.searchHighlights;
             }
-            if (haveTileForEvent(ev)) {
+            if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
                 ret.push((
                     <EventTile
                         key={`${eventId}+${j}`}
diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts
index 3464f952a6..2a84c1f110 100644
--- a/src/contexts/RoomContext.ts
+++ b/src/contexts/RoomContext.ts
@@ -41,6 +41,7 @@ const RoomContext = createContext<IState>({
     canReply: false,
     layout: Layout.Group,
     lowBandwidth: false,
+    showHiddenEventsInTimeline: false,
     showReadReceipts: true,
     showRedactions: true,
     showJoinLeaves: true,

From 9e2ab0d432d5ef7facae1ecccdf25dd71b0baeca Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Thu, 17 Jun 2021 07:35:40 -0400
Subject: [PATCH 04/11] Fix import whitespace in TextForEvent

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/TextForEvent.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts
index 652a1d6e54..5275ff0a63 100644
--- a/src/TextForEvent.ts
+++ b/src/TextForEvent.ts
@@ -13,15 +13,15 @@ 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 {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 
-import {MatrixClientPeg} from './MatrixClientPeg';
+import { MatrixClientPeg } from './MatrixClientPeg';
 import { _t } from './languageHandler';
 import * as Roles from './Roles';
-import {isValid3pidInvite} from "./RoomInvite";
+import { isValid3pidInvite } from "./RoomInvite";
 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";
+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

From e4250e254c7253dc1679b0e9ae84065a35fa6b61 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Thu, 17 Jun 2021 09:52:15 -0400
Subject: [PATCH 05/11] Propertly thread showHiddenEventsInTimeline through
 groupers

---
 src/components/structures/MessagePanel.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index b8d3f4f830..16563bd4e9 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -537,7 +537,7 @@ export default class MessagePanel extends React.Component {
 
             if (grouper) {
                 if (grouper.shouldGroup(mxEv)) {
-                    grouper.add(mxEv);
+                    grouper.add(mxEv, this.context?.showHiddenEventsInTimeline);
                     continue;
                 } else {
                     // not part of group, so get the group tiles, close the
@@ -1167,10 +1167,10 @@ class MemberGrouper {
         return isMembershipChange(ev);
     }
 
-    add(ev) {
+    add(ev, showHiddenEvents) {
         if (ev.getType() === 'm.room.member') {
             // We can ignore any events that don't actually have a message to display
-            if (!hasText(ev, this.context?.showHiddenEventsInTimeline)) return;
+            if (!hasText(ev, showHiddenEvents)) return;
         }
         this.readMarker = this.readMarker || this.panel._readMarkerForEvent(
             ev.getId(),

From e35e836052d4f918c36f4c017aabf6a44534d8ae Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Thu, 24 Jun 2021 18:45:23 -0400
Subject: [PATCH 06/11] Convert TextualEvent and SearchResultTile to TypeScript

Signed-off-by: Robin Townsend <robin@robin.town>
---
 .../{TextualEvent.js => TextualEvent.tsx}     | 24 ++++----
 ...archResultTile.js => SearchResultTile.tsx} | 61 +++++++++----------
 2 files changed, 41 insertions(+), 44 deletions(-)
 rename src/components/views/messages/{TextualEvent.js => TextualEvent.tsx} (70%)
 rename src/components/views/rooms/{SearchResultTile.js => SearchResultTile.tsx} (64%)

diff --git a/src/components/views/messages/TextualEvent.js b/src/components/views/messages/TextualEvent.tsx
similarity index 70%
rename from src/components/views/messages/TextualEvent.js
rename to src/components/views/messages/TextualEvent.tsx
index 0cdd573076..e96390d7bc 100644
--- a/src/components/views/messages/TextualEvent.js
+++ b/src/components/views/messages/TextualEvent.tsx
@@ -15,26 +15,24 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
+import React from "react";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 import RoomContext from "../../../contexts/RoomContext";
 import * as TextForEvent from "../../../TextForEvent";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+
+interface IProps {
+    // The event to show
+    mxEvent: MatrixEvent;
+}
 
 @replaceableComponent("views.messages.TextualEvent")
-export default class TextualEvent extends React.Component {
-    static propTypes = {
-        /* the MatrixEvent to show */
-        mxEvent: PropTypes.object.isRequired,
-    };
-
+export default class TextualEvent extends React.Component<IProps> {
     static contextType = RoomContext;
 
-    render() {
+    public render() {
         const text = TextForEvent.textForEvent(this.props.mxEvent, this.context?.showHiddenEventsInTimeline);
         if (text == null || text.length === 0) return null;
-        return (
-            <div className="mx_TextualEvent">{ text }</div>
-        );
+        return <div className="mx_TextualEvent">{ text }</div>;
     }
 }
diff --git a/src/components/views/rooms/SearchResultTile.js b/src/components/views/rooms/SearchResultTile.tsx
similarity index 64%
rename from src/components/views/rooms/SearchResultTile.js
rename to src/components/views/rooms/SearchResultTile.tsx
index 2963265317..8af0fa5abd 100644
--- a/src/components/views/rooms/SearchResultTile.js
+++ b/src/components/views/rooms/SearchResultTile.tsx
@@ -15,41 +15,41 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import React from 'react';
-import PropTypes from 'prop-types';
-import * as sdk from '../../../index';
+import React from "react";
+import { SearchResult } from "matrix-js-sdk/src/models/search-result";
 import RoomContext from "../../../contexts/RoomContext";
-import {haveTileForEvent} from "./EventTile";
+import { haveTileForEvent } from "./EventTile";
 import SettingsStore from "../../../settings/SettingsStore";
-import {UIFeature} from "../../../settings/UIFeature";
-import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { UIFeature } from "../../../settings/UIFeature";
+import { replaceableComponent } from "../../../utils/replaceableComponent";
+import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
+import DateSeparator from "../messages/DateSeparator";
+import EventTile from "./EventTile";
+
+interface IProps {
+    // The details of this result
+    searchResult: SearchResult;
+    // Strings to be highlighted in the results
+    searchHighlights?: string[];
+    // href for the highlights in this result
+    resultLink?: string;
+    onHeightChanged: () => void;
+    permalinkCreator: RoomPermalinkCreator;
+}
 
 @replaceableComponent("views.rooms.SearchResultTile")
-export default class SearchResultTile extends React.Component {
-    static propTypes = {
-        // a matrix-js-sdk SearchResult containing the details of this result
-        searchResult: PropTypes.object.isRequired,
-
-        // a list of strings to be highlighted in the results
-        searchHighlights: PropTypes.array,
-
-        // href for the highlights in this result
-        resultLink: PropTypes.string,
-
-        onHeightChanged: PropTypes.func,
-    };
-
+export default class SearchResultTile extends React.Component<IProps> {
     static contextType = RoomContext;
 
-    render() {
-        const DateSeparator = sdk.getComponent('messages.DateSeparator');
-        const EventTile = sdk.getComponent('rooms.EventTile');
+    public render() {
         const result = this.props.searchResult;
         const mxEv = result.context.getEvent();
         const eventId = mxEv.getId();
 
         const ts1 = mxEv.getTs();
         const ret = [<DateSeparator key={ts1 + "-search"} ts={ts1} />];
+        const layout = SettingsStore.getValue("layout");
+        const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
         const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
 
         const timeline = result.context.getTimeline();
@@ -61,25 +61,24 @@ export default class SearchResultTile extends React.Component {
                 highlights = this.props.searchHighlights;
             }
             if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) {
-                ret.push((
+                ret.push(
                     <EventTile
                         key={`${eventId}+${j}`}
                         mxEvent={ev}
+                        layout={layout}
                         contextual={contextual}
                         highlights={highlights}
                         permalinkCreator={this.props.permalinkCreator}
                         highlightLink={this.props.resultLink}
                         onHeightChanged={this.props.onHeightChanged}
-                        isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
+                        isTwelveHour={isTwelveHour}
                         alwaysShowTimestamps={alwaysShowTimestamps}
                         enableFlair={SettingsStore.getValue(UIFeature.Flair)}
-                    />
-                ));
+                    />,
+                );
             }
         }
-        return (
-            <li data-scroll-tokens={eventId}>
-                { ret }
-            </li>);
+
+        return <li data-scroll-tokens={eventId}>{ ret }</li>;
     }
 }

From a921d32f44fdedc6489158ab69c43347da0bffcc Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Thu, 24 Jun 2021 18:51:46 -0400
Subject: [PATCH 07/11] Fix lint

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/MessagePanel.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index c7d9944435..19ef6b3350 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -56,7 +56,7 @@ const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
 function shouldFormContinuation(
     prevEvent: MatrixEvent,
     mxEvent: MatrixEvent,
-    showHiddenEvents: boolean
+    showHiddenEvents: boolean,
 ): boolean {
     // sanity check inputs
     if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false;

From c0e10218d9039a248974959e8965c7218493c67a Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 29 Jun 2021 22:42:46 -0400
Subject: [PATCH 08/11] Fix lints

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/TextForEvent.tsx                           | 6 +++---
 src/components/views/messages/TextualEvent.tsx | 2 +-
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index ee57f7dacb..c6ade33cbe 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -693,9 +693,9 @@ export function hasText(ev: MatrixEvent, showHiddenEvents?: boolean): boolean {
  * @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline
  *     to avoid hitting the settings store
  */
-export function textForEvent(
-    ev: MatrixEvent, allowJSX: boolean = false, showHiddenEvents?: boolean
-): string | JSX.Element {
+export function textForEvent(ev: MatrixEvent): string;
+export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
+export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
     return handler?.(ev, allowJSX, showHiddenEvents)?.() ?? '';
 }
diff --git a/src/components/views/messages/TextualEvent.tsx b/src/components/views/messages/TextualEvent.tsx
index ab25b21323..beaf605e1f 100644
--- a/src/components/views/messages/TextualEvent.tsx
+++ b/src/components/views/messages/TextualEvent.tsx
@@ -32,7 +32,7 @@ export default class TextualEvent extends React.Component<IProps> {
 
     public render() {
         const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline);
-        if (text == null || text.length === 0) return null;
+        if (!text) return null;
         return <div className="mx_TextualEvent">{ text }</div>;
     }
 }

From 6c4f0526d7c2949ba4f39809dd51af03f5a0aae0 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 13 Jul 2021 23:26:09 -0400
Subject: [PATCH 09/11] Coalesce falsy values from TextForEvent handlers

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/TextForEvent.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index 3e3b5aa2e0..0056a37c85 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -705,5 +705,5 @@ export function textForEvent(ev: MatrixEvent): string;
 export function textForEvent(ev: MatrixEvent, allowJSX: true, showHiddenEvents?: boolean): string | JSX.Element;
 export function textForEvent(ev: MatrixEvent, allowJSX = false, showHiddenEvents?: boolean): string | JSX.Element {
     const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
-    return handler?.(ev, allowJSX, showHiddenEvents)?.() ?? '';
+    return handler?.(ev, allowJSX, showHiddenEvents)?.() || '';
 }

From deab0407cb0d8f60ac6c5897d7b50db091207173 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Tue, 13 Jul 2021 23:27:49 -0400
Subject: [PATCH 10/11] Pull another settings lookup out of SearchResultTile
 loop

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/views/rooms/SearchResultTile.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx
index 47e9849214..c033855eb5 100644
--- a/src/components/views/rooms/SearchResultTile.tsx
+++ b/src/components/views/rooms/SearchResultTile.tsx
@@ -50,6 +50,7 @@ export default class SearchResultTile extends React.Component<IProps> {
         const layout = SettingsStore.getValue("layout");
         const isTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps");
         const alwaysShowTimestamps = SettingsStore.getValue("alwaysShowTimestamps");
+        const enableFlair = SettingsStore.getValue(UIFeature.Flair);
 
         const timeline = result.context.getTimeline();
         for (let j = 0; j < timeline.length; j++) {
@@ -72,7 +73,7 @@ export default class SearchResultTile extends React.Component<IProps> {
                         onHeightChanged={this.props.onHeightChanged}
                         isTwelveHour={isTwelveHour}
                         alwaysShowTimestamps={alwaysShowTimestamps}
-                        enableFlair={SettingsStore.getValue(UIFeature.Flair)}
+                        enableFlair={enableFlair}
                     />,
                 );
             }

From 092fdf5e5e62abd8078a27bf7f695a1ed39f7629 Mon Sep 17 00:00:00 2001
From: Robin Townsend <robin@robin.town>
Date: Fri, 16 Jul 2021 18:46:29 -0400
Subject: [PATCH 11/11] Be consistent about MessagePanel setting lookups

Signed-off-by: Robin Townsend <robin@robin.town>
---
 src/components/structures/MessagePanel.tsx | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index fce5040b70..8977549697 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -404,17 +404,21 @@ export default class MessagePanel extends React.Component<IProps, IState> {
         return !this.isMounted;
     };
 
+    private get showHiddenEvents(): boolean {
+        return this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline;
+    }
+
     // TODO: Implement granular (per-room) hide options
     public shouldShowEvent(mxEv: MatrixEvent): boolean {
         if (MatrixClientPeg.get().isUserIgnored(mxEv.getSender())) {
             return false; // ignored = no show (only happens if the ignore happens after an event was received)
         }
 
-        if (this.context?.showHiddenEventsInTimeline ?? this.showHiddenEventsInTimeline) {
+        if (this.showHiddenEvents) {
             return true;
         }
 
-        if (!haveTileForEvent(mxEv, this.context?.showHiddenEventsInTimeline)) {
+        if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
             return false; // no tile = no show
         }
 
@@ -574,7 +578,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
 
             if (grouper) {
                 if (grouper.shouldGroup(mxEv)) {
-                    grouper.add(mxEv, this.context?.showHiddenEventsInTimeline);
+                    grouper.add(mxEv, this.showHiddenEvents);
                     continue;
                 } else {
                     // not part of group, so get the group tiles, close the
@@ -655,7 +659,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
 
         // is this a continuation of the previous message?
         const continuation = !wantsDateSeparator &&
-            shouldFormContinuation(prevEvent, mxEv, this.context?.showHiddenEventsInTimeline);
+            shouldFormContinuation(prevEvent, mxEv, this.showHiddenEvents);
 
         const eventId = mxEv.getId();
         const highlight = (eventId === this.props.highlightedEventId);