diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 95341705bf..0056a37c85 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -13,7 +13,6 @@ 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 React from 'react'; import { MatrixClientPeg } from './MatrixClientPeg'; import { _t } from './languageHandler'; @@ -32,7 +31,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; // 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, allowJSX: boolean, 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(); @@ -84,7 +83,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 using 'show hidden events' (labs) return () => _t("%(senderName)s made no change", { senderName }); } else { @@ -319,7 +318,7 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => 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)'); @@ -327,7 +326,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 = () => ""; @@ -364,14 +363,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; @@ -403,7 +402,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)) { @@ -419,7 +418,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': @@ -441,7 +440,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) { @@ -523,7 +522,7 @@ function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string 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() || {}; @@ -553,12 +552,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(); @@ -646,7 +645,9 @@ function textForMjolnirEvent(event): () => string | null { } interface IHandlers { - [type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null); + [type: string]: + (ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean) => + (() => string | JSX.Element | null); } const handlers: IHandlers = { @@ -682,14 +683,27 @@ 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, false, showHiddenEvents)); } +/** + * Gets the textual content of the given event. + * @param ev The event + * @param allowJSX Whether to output rich JSX content + * @param showHiddenEvents An optional cached setting value for showHiddenEventsInTimeline + * to avoid hitting the settings store + */ export function textForEvent(ev: MatrixEvent): string; -export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element; -export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element { +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)?.() || ''; + return handler?.(ev, allowJSX, showHiddenEvents)?.() || ''; } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 47f8c218dc..8977549697 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -54,7 +54,11 @@ const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, E // 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: MatrixEvent, mxEvent: MatrixEvent): boolean { +function shouldFormContinuation( + prevEvent: MatrixEvent, + mxEvent: MatrixEvent, + showHiddenEvents: boolean, +): boolean { // sanity check inputs if (!prevEvent || !prevEvent.sender || !mxEvent.sender) return false; // check if within the max continuation period @@ -74,7 +78,7 @@ function shouldFormContinuation(prevEvent: MatrixEvent, mxEvent: MatrixEvent): b 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; } @@ -239,7 +243,8 @@ export default class MessagePanel extends React.Component { }; // 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"); this.showTypingNotificationsWatcherRef = @@ -399,17 +404,21 @@ export default class MessagePanel extends React.Component { 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.showHiddenEventsInTimeline) { + if (this.showHiddenEvents) { return true; } - if (!haveTileForEvent(mxEv)) { + if (!haveTileForEvent(mxEv, this.showHiddenEvents)) { return false; // no tile = no show } @@ -569,7 +578,7 @@ export default class MessagePanel extends React.Component { if (grouper) { if (grouper.shouldGroup(mxEv)) { - grouper.add(mxEv); + grouper.add(mxEv, this.showHiddenEvents); continue; } else { // not part of group, so get the group tiles, close the @@ -649,7 +658,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.showHiddenEvents); const eventId = mxEv.getId(); const highlight = (eventId === this.props.highlightedEventId); @@ -946,7 +956,7 @@ abstract class BaseGrouper { } public abstract shouldGroup(ev: MatrixEvent): boolean; - public abstract add(ev: MatrixEvent): void; + public abstract add(ev: MatrixEvent, showHiddenEvents?: boolean): void; public abstract getTiles(): ReactNode[]; public abstract getNewPrevEvent(): MatrixEvent; } @@ -1200,10 +1210,10 @@ class MemberGrouper extends BaseGrouper { return membershipTypes.includes(ev.getType() as EventType); } - public add(ev: MatrixEvent): void { + public add(ev: MatrixEvent, showHiddenEvents?: boolean): void { if (ev.getType() === EventType.RoomMember) { // We can ignore any events that don't actually have a message to display - if (!hasText(ev)) return; + if (!hasText(ev, showHiddenEvents)) 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 7e3bcbc962..0c10a2aeca 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -166,6 +166,7 @@ export interface IState { canReply: boolean; layout: Layout; lowBandwidth: boolean; + showHiddenEventsInTimeline: boolean; showReadReceipts: boolean; showRedactions: boolean; showJoinLeaves: boolean; @@ -230,6 +231,7 @@ export default class RoomView extends React.Component { canReply: false, layout: SettingsStore.getValue("layout"), lowBandwidth: SettingsStore.getValue("lowBandwidth"), + showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"), showReadReceipts: true, showRedactions: true, showJoinLeaves: true, @@ -267,6 +269,9 @@ export default class RoomView extends React.Component { SettingsStore.watchSetting("lowBandwidth", null, () => this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }), ), + SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () => + this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }), + ), ]; } @@ -1388,7 +1393,7 @@ export default class RoomView extends React.Component { 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.tsx b/src/components/structures/TimelinePanel.tsx index 5f9d9b7026..59930bf41d 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -1337,7 +1337,8 @@ class TimelinePanel extends React.Component { const shouldIgnore = !!ev.status || // local echo (ignoreOwn && ev.getSender() === 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.tsx b/src/components/views/messages/TextualEvent.tsx index 70f90a33e4..8fc116b5d0 100644 --- a/src/components/views/messages/TextualEvent.tsx +++ b/src/components/views/messages/TextualEvent.tsx @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +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"; @@ -26,11 +27,11 @@ interface IProps { @replaceableComponent("views.messages.TextualEvent") export default class TextualEvent extends React.Component { - render() { - const text = TextForEvent.textForEvent(this.props.mxEvent, true); - if (!text || (text as string).length === 0) return null; - return ( -
{ text }
- ); + static contextType = RoomContext; + + public render() { + const text = TextForEvent.textForEvent(this.props.mxEvent, true, this.context?.showHiddenEventsInTimeline); + if (!text) return null; + return
{ text }
; } } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 63ac8ff375..e0a924f1e7 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -1160,7 +1160,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; @@ -1170,7 +1170,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.tsx b/src/components/views/rooms/SearchResultTile.tsx index 980e8835f8..c033855eb5 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -15,14 +15,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React from "react"; import { SearchResult } from "matrix-js-sdk/src/models/search-result"; -import EventTile, { haveTileForEvent } from "./EventTile"; -import DateSeparator from '../messages/DateSeparator'; +import RoomContext from "../../../contexts/RoomContext"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import DateSeparator from "../messages/DateSeparator"; +import EventTile, { haveTileForEvent } from "./EventTile"; interface IProps { // a matrix-js-sdk SearchResult containing the details of this result @@ -37,6 +38,8 @@ interface IProps { @replaceableComponent("views.rooms.SearchResultTile") export default class SearchResultTile extends React.Component { + static contextType = RoomContext; + public render() { const result = this.props.searchResult; const mxEv = result.context.getEvent(); @@ -44,7 +47,10 @@ export default class SearchResultTile extends React.Component { const ts1 = mxEv.getTs(); const ret = []; + 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++) { @@ -54,26 +60,25 @@ export default class SearchResultTile extends React.Component { if (!contextual) { highlights = this.props.searchHighlights; } - if (haveTileForEvent(ev)) { - ret.push(( + if (haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline)) { + ret.push( - )); + enableFlair={enableFlair} + />, + ); } } - return ( -
  • - { ret } -
  • ); + + return
  • { ret }
  • ; } } 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({ canReply: false, layout: Layout.Group, lowBandwidth: false, + showHiddenEventsInTimeline: false, showReadReceipts: true, showRedactions: true, showJoinLeaves: true,