diff --git a/src/Unread.ts b/src/Unread.ts index 91e192b371..95046db29d 100644 --- a/src/Unread.ts +++ b/src/Unread.ts @@ -20,7 +20,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event"; import { MatrixClientPeg } from "./MatrixClientPeg"; import shouldHideEvent from './shouldHideEvent'; -import { haveTileForEvent } from "./components/views/rooms/EventTile"; +import { haveRendererForEvent } from "./events/EventTileFactory"; /** * Returns true if this event arriving in a room should affect the room's @@ -46,7 +46,7 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean { } if (ev.isRedacted()) return false; - return haveTileForEvent(ev); + return haveRendererForEvent(ev); } export function doesRoomHaveUnreadMessages(room: Room): boolean { diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 7758375a69..a5a9164bb7 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -31,7 +31,7 @@ import SettingsStore from '../../settings/SettingsStore'; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import { Layout } from "../../settings/enums/Layout"; import { _t } from "../../languageHandler"; -import EventTile, { UnwrappedEventTile, haveTileForEvent, IReadReceiptProps } from "../views/rooms/EventTile"; +import EventTile, { UnwrappedEventTile, IReadReceiptProps } from "../views/rooms/EventTile"; import { hasText } from "../../TextForEvent"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import DMRoomMap from "../../utils/DMRoomMap"; @@ -52,6 +52,7 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer"; import { Action } from '../../dispatcher/actions'; import { getEventDisplayInfo } from "../../utils/EventUtils"; import { IReadReceiptInfo } from "../views/rooms/ReadReceiptMarker"; +import { haveRendererForEvent } from "../../events/EventTileFactory"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = [EventType.Sticker, EventType.RoomMessage]; @@ -95,7 +96,7 @@ export function shouldFormContinuation( timelineRenderingType !== TimelineRenderingType.Thread) return false; // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile - if (!haveTileForEvent(prevEvent, showHiddenEvents)) return false; + if (!haveRendererForEvent(prevEvent, showHiddenEvents)) return false; return true; } @@ -488,7 +489,7 @@ export default class MessagePanel extends React.Component { return true; } - if (!haveTileForEvent(mxEv, this.showHiddenEvents)) { + if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) { return false; // no tile = no show } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index d52786c39b..906c32c0d9 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -55,7 +55,6 @@ import SettingsStore from "../../settings/SettingsStore"; import { Layout } from "../../settings/enums/Layout"; import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/right-panel/RightPanelStore"; -import { haveTileForEvent } from "../views/rooms/EventTile"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import MatrixClientContext, { MatrixClientProps, withMatrixClientHOC } from "../../contexts/MatrixClientContext"; import { E2EStatus, shieldStatusForRoom } from '../../utils/ShieldUtils'; @@ -108,6 +107,7 @@ import { DoAfterSyncPreparedPayload } from '../../dispatcher/payloads/DoAfterSyn import FileDropTarget from './FileDropTarget'; import Measured from '../views/elements/Measured'; import { FocusComposerPayload } from '../../dispatcher/payloads/FocusComposerPayload'; +import { haveRendererForEvent } from "../../events/EventTileFactory"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -1451,7 +1451,7 @@ export class RoomView extends React.Component { continue; } - if (!haveTileForEvent(mxEv, this.state.showHiddenEventsInTimeline)) { + if (!haveRendererForEvent(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 dd372b9381..52094a84c9 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -40,7 +40,6 @@ import dis from "../../dispatcher/dispatcher"; import { Action } from '../../dispatcher/actions'; import Timer from '../../utils/Timer'; import shouldHideEvent from '../../shouldHideEvent'; -import { haveTileForEvent } from "../views/rooms/EventTile"; import { arrayFastClone } from "../../utils/arrays"; import MessagePanel from "./MessagePanel"; import { IScrollState } from "./ScrollPanel"; @@ -54,6 +53,7 @@ import CallEventGrouper, { buildCallEventGroupers } from "./CallEventGrouper"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; +import { haveRendererForEvent } from "../../events/EventTileFactory"; const PAGINATE_SIZE = 20; const INITIAL_SIZE = 20; @@ -1473,7 +1473,7 @@ class TimelinePanel extends React.Component { const shouldIgnore = !!ev.status || // local echo (ignoreOwn && ev.getSender() === myUserId); // own message - const isWithoutTile = !haveTileForEvent(ev, this.context?.showHiddenEventsInTimeline) || + const isWithoutTile = !haveRendererForEvent(ev, this.context?.showHiddenEventsInTimeline) || shouldHideEvent(ev, this.context); if (isWithoutTile || !node) { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index b0277cd464..90fd844672 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -41,10 +41,10 @@ import MLocationBody from "./MLocationBody"; import MjolnirBody from "./MjolnirBody"; // onMessageAllowed is handled internally -interface IProps extends Omit { +interface IProps extends Omit { /* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */ - overrideBodyTypes?: Record; - overrideEventTypes?: Record; + overrideBodyTypes?: Record; + overrideEventTypes?: Record; // helper function to access relations for this event getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index d830a4f5a1..aa2e38ea8d 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -17,7 +17,7 @@ limitations under the License. import React, { createRef, forwardRef, MouseEvent, RefObject } from 'react'; import classNames from "classnames"; -import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; +import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { EventStatus, MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/models/event"; import { Relations } from "matrix-js-sdk/src/models/relations"; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; @@ -25,25 +25,19 @@ import { Thread, ThreadEvent } from 'matrix-js-sdk/src/models/thread'; import { logger } from "matrix-js-sdk/src/logger"; import { NotificationCountType, Room, RoomEvent } from 'matrix-js-sdk/src/models/room'; import { CallErrorCode } from "matrix-js-sdk/src/webrtc/call"; -import { M_POLL_START } from "matrix-events-sdk"; import { CryptoEvent } from "matrix-js-sdk/src/crypto"; import { UserTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; import ReplyChain from "../elements/ReplyChain"; import { _t } from '../../../languageHandler'; -import { hasText } from "../../../TextForEvent"; -import * as sdk from "../../../index"; import dis from '../../../dispatcher/dispatcher'; import { Layout } from "../../../settings/enums/Layout"; import { formatTime } from "../../../DateUtils"; import { MatrixClientPeg } from '../../../MatrixClientPeg'; -import { ALL_RULE_TYPES } from "../../../mjolnir/BanList"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { E2EState } from "./E2EIcon"; import { toRem } from "../../../utils/units"; -import { WidgetType } from "../../../widgets/WidgetType"; import RoomAvatar from "../avatars/RoomAvatar"; -import { WIDGET_LAYOUT_EVENT_TYPE } from "../../../stores/widgets/WidgetLayoutStore"; import { objectHasDiff } from "../../../utils/objects"; import Tooltip from "../elements/Tooltip"; import EditorStateTransfer from "../../../utils/EditorStateTransfer"; @@ -62,7 +56,6 @@ import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; import SettingsStore from "../../../settings/SettingsStore"; -import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion"; import { showThread } from '../../../dispatcher/dispatch-actions/threads'; import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore'; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; @@ -81,123 +74,11 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { shouldDisplayReply } from '../../../utils/Reply'; import PosthogTrackers from "../../../PosthogTrackers"; import TileErrorBoundary from '../messages/TileErrorBoundary'; -import ThreadSummary, { ThreadMessagePreview } from './ThreadSummary'; +import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../events/EventTileFactory"; +import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary"; export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations; -const eventTileTypes = { - [EventType.RoomMessage]: 'messages.MessageEvent', - [EventType.Sticker]: 'messages.MessageEvent', - [M_POLL_START.name]: 'messages.MessageEvent', - [M_POLL_START.altName]: 'messages.MessageEvent', - [EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion', - [EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion', - [EventType.CallInvite]: 'messages.CallEvent', -}; - -const stateEventTileTypes = { - [EventType.RoomEncryption]: 'messages.EncryptionEvent', - [EventType.RoomCanonicalAlias]: 'messages.TextualEvent', - [EventType.RoomCreate]: 'messages.RoomCreate', - [EventType.RoomMember]: 'messages.TextualEvent', - [EventType.RoomName]: 'messages.TextualEvent', - [EventType.RoomAvatar]: 'messages.RoomAvatarEvent', - [EventType.RoomThirdPartyInvite]: 'messages.TextualEvent', - [EventType.RoomHistoryVisibility]: 'messages.TextualEvent', - [EventType.RoomTopic]: 'messages.TextualEvent', - [EventType.RoomPowerLevels]: 'messages.TextualEvent', - [EventType.RoomPinnedEvents]: 'messages.TextualEvent', - [EventType.RoomServerAcl]: 'messages.TextualEvent', - // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - 'im.vector.modular.widgets': 'messages.TextualEvent', - [WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent', - [EventType.RoomTombstone]: 'messages.TextualEvent', - [EventType.RoomJoinRules]: 'messages.TextualEvent', - [EventType.RoomGuestAccess]: 'messages.TextualEvent', -}; - -const stateEventSingular = new Set([ - EventType.RoomEncryption, - EventType.RoomCanonicalAlias, - EventType.RoomCreate, - EventType.RoomName, - EventType.RoomAvatar, - EventType.RoomHistoryVisibility, - EventType.RoomTopic, - EventType.RoomPowerLevels, - EventType.RoomPinnedEvents, - EventType.RoomServerAcl, - WIDGET_LAYOUT_EVENT_TYPE, - EventType.RoomTombstone, - EventType.RoomJoinRules, - EventType.RoomGuestAccess, -]); - -// Add all the Mjolnir stuff to the renderer -for (const evType of ALL_RULE_TYPES) { - stateEventTileTypes[evType] = 'messages.TextualEvent'; -} - -export function getHandlerTile(ev: MatrixEvent): string { - const type = ev.getType(); - - // don't show verification requests we're not involved in, - // not even when showing hidden events - if (type === EventType.RoomMessage) { - const content = ev.getContent(); - if (content && content.msgtype === MsgType.KeyVerificationRequest) { - const me = MatrixClientPeg.get()?.getUserId(); - if (ev.getSender() !== me && content.to !== me) { - return undefined; - } else { - return "messages.MKeyVerificationRequest"; - } - } - } - // these events are sent by both parties during verification, but we only want to render one - // tile once the verification concludes, so filter out the one from the other party. - if (type === EventType.KeyVerificationDone) { - const me = MatrixClientPeg.get()?.getUserId(); - if (ev.getSender() !== me) { - return undefined; - } - } - - // sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and - // fall back to showing hidden events, if we're viewing hidden events - // XXX: This is extremely a hack. Possibly these components should have an interface for - // declining to render? - if (type === EventType.KeyVerificationCancel || type === EventType.KeyVerificationDone) { - if (!MKeyVerificationConclusion.shouldRender(ev, ev.verificationRequest)) { - return; - } - } - - // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - if (type === "im.vector.modular.widgets") { - let type = ev.getContent()['type']; - if (!type) { - // deleted/invalid widget - try the past widget type - type = ev.getPrevContent()['type']; - } - - if (WidgetType.JITSI.matches(type)) { - return "messages.MJitsiWidgetEvent"; - } - } - - if (ev.isState()) { - if (stateEventSingular.has(type) && ev.getStateKey() !== "") return undefined; - return stateEventTileTypes[type]; - } - - if (ev.isRedacted()) { - return "messages.MessageEvent"; - } - - return eventTileTypes[type]; -} - // Our component structure for EventTiles on the timeline is: // // .-EventTile------------------------------------------------. @@ -1054,7 +935,7 @@ export class UnwrappedEventTile extends React.Component { const msgtype = this.props.mxEvent.getContent().msgtype; const eventType = this.props.mxEvent.getType() as EventType; const { - tileHandler, + hasRenderer, isBubbleMessage, isInfoMessage, isLeftAlignedBubbleMessage, @@ -1065,7 +946,7 @@ export class UnwrappedEventTile extends React.Component { // This shouldn't happen: the caller should check we support this type // before trying to instantiate us - if (!tileHandler) { + if (!hasRenderer) { const { mxEvent } = this.props; logger.warn(`Event type not supported: type:${eventType} isState:${mxEvent.isState()}`); return
@@ -1075,7 +956,6 @@ export class UnwrappedEventTile extends React.Component {
; } - const EventTileType = sdk.getComponent(tileHandler); const isProbablyMedia = MediaEventHelper.isEligible(this.props.mxEvent); const lineClasses = classNames("mx_EventTile_line", { @@ -1154,7 +1034,7 @@ export class UnwrappedEventTile extends React.Component { ) { avatarSize = 24; needsSenderProfile = true; - } else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) { + } else if (eventType === EventType.RoomCreate || isBubbleMessage) { avatarSize = 0; needsSenderProfile = false; } else if (isInfoMessage) { @@ -1322,20 +1202,21 @@ export class UnwrappedEventTile extends React.Component { msgOption = readAvatars; } - const replyChain = haveTileForEvent(this.props.mxEvent) && shouldDisplayReply(this.props.mxEvent) - ? - : null; + const replyChain = + (haveRendererForEvent(this.props.mxEvent) && shouldDisplayReply(this.props.mxEvent)) + ? + : null; const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId(); @@ -1362,16 +1243,19 @@ export class UnwrappedEventTile extends React.Component { ,
- + { renderTile(TimelineRenderingType.Notification, { + ...this.props, + + // overrides + ref: this.tile, + isSeeingThroughMessageHiddenForModeration, + + // appease TS + highlights: this.props.highlights, + highlightLink: this.props.highlightLink, + onHeightChanged: this.props.onHeightChanged, + permalinkCreator: this.props.permalinkCreator, + }) }
, ]); } @@ -1403,17 +1287,19 @@ export class UnwrappedEventTile extends React.Component { ,
{ replyChain } - + { renderTile(TimelineRenderingType.Thread, { + ...this.props, + + // overrides + ref: this.tile, + isSeeingThroughMessageHiddenForModeration, + + // appease TS + highlights: this.props.highlights, + highlightLink: this.props.highlightLink, + onHeightChanged: this.props.onHeightChanged, + permalinkCreator: this.props.permalinkCreator, + }) } { actionBar } { timestamp }
, @@ -1485,16 +1371,19 @@ export class UnwrappedEventTile extends React.Component { "data-scroll-tokens": scrollToken, }, [
- + { renderTile(TimelineRenderingType.File, { + ...this.props, + + // overrides + ref: this.tile, + isSeeingThroughMessageHiddenForModeration, + + // appease TS + highlights: this.props.highlights, + highlightLink: this.props.highlightLink, + onHeightChanged: this.props.onHeightChanged, + permalinkCreator: this.props.permalinkCreator, + }) }
, { { groupTimestamp } { groupPadlock } { replyChain } - + { renderTile(this.context.timelineRenderingType, { + ...this.props, + + // overrides + ref: this.tile, + isSeeingThroughMessageHiddenForModeration, + timestamp: bubbleTimestamp, + + // appease TS + highlights: this.props.highlights, + highlightLink: this.props.highlightLink, + onHeightChanged: this.props.onHeightChanged, + permalinkCreator: this.props.permalinkCreator, + }) } { keyRequestInfo } { actionBar } { this.props.layout === Layout.IRC && <> @@ -1577,31 +1464,6 @@ const SafeEventTile = forwardRef((props: IProps, ref: RefObject diff --git a/src/components/views/rooms/ReplyTile.tsx b/src/components/views/rooms/ReplyTile.tsx index 95d249dd16..1d23e7a56a 100644 --- a/src/components/views/rooms/ReplyTile.tsx +++ b/src/components/views/rooms/ReplyTile.tsx @@ -27,11 +27,11 @@ import { Action } from '../../../dispatcher/actions'; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import SenderProfile from "../messages/SenderProfile"; import MImageReplyBody from "../messages/MImageReplyBody"; -import * as sdk from '../../../index'; import { getEventDisplayInfo, isVoiceMessage } from '../../../utils/EventUtils'; import MFileBody from "../messages/MFileBody"; import MVoiceMessageBody from "../messages/MVoiceMessageBody"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { renderReplyTile } from "../../../events/EventTileFactory"; interface IProps { mxEvent: MatrixEvent; @@ -109,10 +109,10 @@ export default class ReplyTile extends React.PureComponent { const msgType = mxEvent.getContent().msgtype; const evType = mxEvent.getType() as EventType; - const { tileHandler, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo(mxEvent); + const { hasRenderer, isInfoMessage, isSeeingThroughMessageHiddenForModeration } = getEventDisplayInfo(mxEvent); // This shouldn't happen: the caller should check we support this type // before trying to instantiate us - if (!tileHandler) { + if (!hasRenderer) { const { mxEvent } = this.props; logger.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`); return
@@ -120,8 +120,6 @@ export default class ReplyTile extends React.PureComponent {
; } - const EventTileType = sdk.getComponent(tileHandler); - const classes = classNames("mx_ReplyTile", { mx_ReplyTile_info: isInfoMessage && !mxEvent.isRedacted(), mx_ReplyTile_audio: msgType === MsgType.Audio, @@ -135,10 +133,10 @@ export default class ReplyTile extends React.PureComponent { let sender; const needsSenderProfile = ( - !isInfoMessage && - msgType !== MsgType.Image && - tileHandler !== EventType.RoomCreate && - evType !== EventType.Sticker + !isInfoMessage + && msgType !== MsgType.Image + && evType !== EventType.Sticker + && evType !== EventType.RoomCreate ); if (needsSenderProfile) { @@ -147,13 +145,13 @@ export default class ReplyTile extends React.PureComponent { />; } - const msgtypeOverrides = { + const msgtypeOverrides: Record = { [MsgType.Image]: MImageReplyBody, // Override audio and video body with file body. We also hide the download/decrypt button using CSS [MsgType.Audio]: isVoiceMessage(mxEvent) ? MVoiceMessageBody : MFileBody, [MsgType.Video]: MFileBody, }; - const evOverrides = { + const evOverrides: Record = { // Use MImageReplyBody so that the sticker isn't taking up a lot of space [EventType.Sticker]: MImageReplyBody, }; @@ -162,20 +160,23 @@ export default class ReplyTile extends React.PureComponent {
); diff --git a/src/components/views/rooms/SearchResultTile.tsx b/src/components/views/rooms/SearchResultTile.tsx index 85bd5c0ed6..a3d646aed3 100644 --- a/src/components/views/rooms/SearchResultTile.tsx +++ b/src/components/views/rooms/SearchResultTile.tsx @@ -23,10 +23,11 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex import SettingsStore from "../../../settings/SettingsStore"; import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks'; import DateSeparator from "../messages/DateSeparator"; -import EventTile, { haveTileForEvent } from "./EventTile"; +import EventTile from "./EventTile"; import { shouldFormContinuation } from "../../structures/MessagePanel"; import { wantsDateSeparator } from "../../../DateUtils"; import CallEventGrouper, { buildCallEventGroupers } from "../../structures/CallEventGrouper"; +import { haveRendererForEvent } from "../../../events/EventTileFactory"; interface IProps { // a matrix-js-sdk SearchResult containing the details of this result @@ -77,7 +78,7 @@ export default class SearchResultTile extends React.Component { highlights = this.props.searchHighlights; } - if (haveTileForEvent(mxEv, this.context?.showHiddenEventsInTimeline)) { + if (haveRendererForEvent(mxEv, this.context?.showHiddenEventsInTimeline)) { // do we need a date separator since the last event? const prevEv = timeline[j - 1]; // is this a continuation of the previous message? diff --git a/src/events/EventTileFactory.tsx b/src/events/EventTileFactory.tsx new file mode 100644 index 0000000000..ceb2abab95 --- /dev/null +++ b/src/events/EventTileFactory.tsx @@ -0,0 +1,376 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { EventType, MsgType, RelationType } from "matrix-js-sdk/src/@types/event"; +import { M_POLL_START, Optional } from "matrix-events-sdk"; +import { MatrixClient } from "matrix-js-sdk/src/client"; + +import EditorStateTransfer from "../utils/EditorStateTransfer"; +import { RoomPermalinkCreator } from "../utils/permalinks/Permalinks"; +import CallEventGrouper from "../components/structures/CallEventGrouper"; +import { GetRelationsForEvent } from "../components/views/rooms/EventTile"; +import { TimelineRenderingType } from "../contexts/RoomContext"; +import MessageEvent from "../components/views/messages/MessageEvent"; +import MKeyVerificationConclusion from "../components/views/messages/MKeyVerificationConclusion"; +import CallEvent from "../components/views/messages/CallEvent"; +import TextualEvent from "../components/views/messages/TextualEvent"; +import EncryptionEvent from "../components/views/messages/EncryptionEvent"; +import RoomCreate from "../components/views/messages/RoomCreate"; +import RoomAvatarEvent from "../components/views/messages/RoomAvatarEvent"; +import { WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/WidgetLayoutStore"; +import { ALL_RULE_TYPES } from "../mjolnir/BanList"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import MKeyVerificationRequest from "../components/views/messages/MKeyVerificationRequest"; +import { WidgetType } from "../widgets/WidgetType"; +import MJitsiWidgetEvent from "../components/views/messages/MJitsiWidgetEvent"; +import { hasText } from "../TextForEvent"; +import { getMessageModerationState, MessageModerationState } from "../utils/EventUtils"; +import HiddenBody from "../components/views/messages/HiddenBody"; +import SettingsStore from "../settings/SettingsStore"; +import ViewSourceEvent from "../components/views/messages/ViewSourceEvent"; + +// Subset of EventTile's IProps plus some mixins +export interface EventTileTypeProps { + ref?: React.RefObject; // `any` because it's effectively impossible to convince TS of a reasonable type + mxEvent: MatrixEvent; + highlights: string[]; + highlightLink: string; + showUrlPreview?: boolean; + onHeightChanged: () => void; + forExport?: boolean; + getRelationsForEvent?: GetRelationsForEvent; + editState?: EditorStateTransfer; + replacingEventId?: string; + permalinkCreator: RoomPermalinkCreator; + callEventGrouper?: CallEventGrouper; + isSeeingThroughMessageHiddenForModeration?: boolean; + timestamp?: JSX.Element; + maxImageHeight?: number; // pixels + overrideBodyTypes?: Record; + overrideEventTypes?: Record; +} + +type FactoryProps = Omit; +type Factory = (ref: Optional>, props: X) => JSX.Element; +type FactoryMap = Record; + +const MessageEventFactory: Factory = (ref, props) => ; +const KeyVerificationConclFactory: Factory = (ref, props) => ; +const CallEventFactory: Factory = (ref, props) => ( + +); +const TextualEventFactory: Factory = (ref, props) => ; +const VerificationReqFactory: Factory = (ref, props) => ; +const HiddenEventFactory: Factory = (ref, props) => ; + +// These factories are exported for reference comparison against pickFactory() +export const JitsiEventFactory: Factory = (ref, props) => ; +export const JSONEventFactory: Factory = (ref, props) => ; + +const EVENT_TILE_TYPES: FactoryMap = { + [EventType.RoomMessage]: MessageEventFactory, // note that verification requests are handled in pickFactory() + [EventType.Sticker]: MessageEventFactory, + [M_POLL_START.name]: MessageEventFactory, + [M_POLL_START.altName]: MessageEventFactory, + [EventType.KeyVerificationCancel]: KeyVerificationConclFactory, + [EventType.KeyVerificationDone]: KeyVerificationConclFactory, + [EventType.CallInvite]: CallEventFactory, // note that this requires a special factory type +}; + +const STATE_EVENT_TILE_TYPES: FactoryMap = { + [EventType.RoomEncryption]: (ref, props) => , + [EventType.RoomCanonicalAlias]: TextualEventFactory, + [EventType.RoomCreate]: (ref, props) => , + [EventType.RoomMember]: TextualEventFactory, + [EventType.RoomName]: TextualEventFactory, + [EventType.RoomAvatar]: (ref, props) => , + [EventType.RoomThirdPartyInvite]: TextualEventFactory, + [EventType.RoomHistoryVisibility]: TextualEventFactory, + [EventType.RoomTopic]: TextualEventFactory, + [EventType.RoomPowerLevels]: TextualEventFactory, + [EventType.RoomPinnedEvents]: TextualEventFactory, + [EventType.RoomServerAcl]: TextualEventFactory, + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) + 'im.vector.modular.widgets': TextualEventFactory, // note that Jitsi widgets are special in pickFactory() + [WIDGET_LAYOUT_EVENT_TYPE]: TextualEventFactory, + [EventType.RoomTombstone]: TextualEventFactory, + [EventType.RoomJoinRules]: TextualEventFactory, + [EventType.RoomGuestAccess]: TextualEventFactory, +}; + +// Add all the Mjolnir stuff to the renderer too +for (const evType of ALL_RULE_TYPES) { + STATE_EVENT_TILE_TYPES[evType] = TextualEventFactory; +} + +// These events should be recorded in the STATE_EVENT_TILE_TYPES +const SINGULAR_STATE_EVENTS = new Set([ + EventType.RoomEncryption, + EventType.RoomCanonicalAlias, + EventType.RoomCreate, + EventType.RoomName, + EventType.RoomAvatar, + EventType.RoomHistoryVisibility, + EventType.RoomTopic, + EventType.RoomPowerLevels, + EventType.RoomPinnedEvents, + EventType.RoomServerAcl, + WIDGET_LAYOUT_EVENT_TYPE, + EventType.RoomTombstone, + EventType.RoomJoinRules, + EventType.RoomGuestAccess, +]); + +/** + * Find an event tile factory for the given conditions. + * @param mxEvent The event. + * @param cli The matrix client to reference when needed. + * @param asHiddenEv When true, treat the event as always hidden. + * @returns The factory, or falsy if not possible. + */ +export function pickFactory(mxEvent: MatrixEvent, cli: MatrixClient, asHiddenEv?: boolean): Optional { + const evType = mxEvent.getType(); // cache this to reduce call stack execution hits + + // Note: we avoid calling SettingsStore unless absolutely necessary - this code is on the critical path. + + if (asHiddenEv && SettingsStore.getValue("showHiddenEventsInTimeline")) { + return JSONEventFactory; + } + + const noEventFactoryFactory: (() => Optional) = () => SettingsStore.getValue("showHiddenEventsInTimeline") + ? JSONEventFactory + : undefined; // just don't render things that we shouldn't render + + // We run all the event type checks first as they might override the factory entirely. + + const moderationState = getMessageModerationState(mxEvent, cli); + if (moderationState === MessageModerationState.HIDDEN_TO_CURRENT_USER) { + return HiddenEventFactory; + } + + if (evType === EventType.RoomMessage) { + // don't show verification requests we're not involved in, + // not even when showing hidden events + const content = mxEvent.getContent(); + if (content?.msgtype === MsgType.KeyVerificationRequest) { + const me = cli.getUserId(); + if (mxEvent.getSender() !== me && content['to'] !== me) { + return noEventFactoryFactory(); // not for/from us + } else { + // override the factory + return VerificationReqFactory; + } + } + } else if (evType === EventType.KeyVerificationDone) { + // these events are sent by both parties during verification, but we only want to render one + // tile once the verification concludes, so filter out the one from the other party. + const me = cli.getUserId(); + if (mxEvent.getSender() !== me) { + return noEventFactoryFactory(); + } + } + + if (evType === EventType.KeyVerificationCancel || evType === EventType.KeyVerificationDone) { + // sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and + // fall back to showing hidden events, if we're viewing hidden events + // XXX: This is extremely a hack. Possibly these components should have an interface for + // declining to render? + if (!MKeyVerificationConclusion.shouldRender(mxEvent, mxEvent.verificationRequest)) { + return noEventFactoryFactory(); + } + } + + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) + if (evType === "im.vector.modular.widgets") { + let type = mxEvent.getContent()['type']; + if (!type) { + // deleted/invalid widget - try the past widget type + type = mxEvent.getPrevContent()['type']; + } + + if (WidgetType.JITSI.matches(type)) { + // override the factory + return JitsiEventFactory; + } + } + + // Try and pick a state event factory, if we can. + if (mxEvent.isState()) { + if (SINGULAR_STATE_EVENTS.has(evType) && mxEvent.getStateKey() !== '') { + return noEventFactoryFactory(); // improper event type to render + } + return STATE_EVENT_TILE_TYPES[evType] ?? noEventFactoryFactory(); + } + + // Blanket override for all events. The MessageEvent component handles redacted states for us. + if (mxEvent.isRedacted()) { + return MessageEventFactory; + } + + return EVENT_TILE_TYPES[evType] ?? noEventFactoryFactory(); +} + +/** + * Render an event as a tile + * @param renderType The render type. Used to inform properties given to the eventual component. + * @param props The properties to provide to the eventual component. + * @param cli Optional client instance to use, otherwise the default MatrixClientPeg will be used. + * @returns The tile as JSX, or falsy if unable to render. + */ +export function renderTile( + renderType: TimelineRenderingType, + props: EventTileTypeProps, + cli?: MatrixClient, +): Optional { + cli = cli ?? MatrixClientPeg.get(); // because param defaults don't do the correct thing + + const factory = pickFactory(props.mxEvent, cli); + if (!factory) return undefined; + + // Note that we split off the ones we actually care about here just to be sure that we're + // not going to accidentally send things we shouldn't from lazy callers. Eg: EventTile's + // lazy calls of `renderTile(..., this.props)` will have a lot more than we actually care + // about. + const { + ref, + mxEvent, + forExport, + replacingEventId, + editState, + highlights, + highlightLink, + showUrlPreview, + permalinkCreator, + onHeightChanged, + callEventGrouper, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + timestamp, + } = props; + + switch (renderType) { + case TimelineRenderingType.File: + case TimelineRenderingType.Notification: + case TimelineRenderingType.Thread: + // We only want a subset of props, so we don't end up causing issues for downstream components. + return factory(props.ref, { + mxEvent, + highlights, + highlightLink, + showUrlPreview, + onHeightChanged, + editState, + replacingEventId, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + }); + default: + // NEARLY ALL THE OPTIONS! + return factory(ref, { + mxEvent, + forExport, + replacingEventId, + editState, + highlights, + highlightLink, + showUrlPreview, + permalinkCreator, + onHeightChanged, + callEventGrouper, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + timestamp, + }); + } +} + +/** + * A version of renderTile() specifically for replies. + * @param props The properties to specify on the eventual object. + * @param cli Optional client instance to use, otherwise the default MatrixClientPeg will be used. + * @returns The tile as JSX, or falsy if unable to render. + */ +export function renderReplyTile( + props: EventTileTypeProps, + cli?: MatrixClient, +): Optional { + cli = cli ?? MatrixClientPeg.get(); // because param defaults don't do the correct thing + + const factory = pickFactory(props.mxEvent, cli); + if (!factory) return undefined; + + // See renderTile() for why we split off so much + const { + ref, + mxEvent, + highlights, + highlightLink, + onHeightChanged, + showUrlPreview, + overrideBodyTypes, + overrideEventTypes, + replacingEventId, + maxImageHeight, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + } = props; + + return factory(ref, { + mxEvent, + highlights, + highlightLink, + onHeightChanged, + showUrlPreview, + overrideBodyTypes, + overrideEventTypes, + replacingEventId, + maxImageHeight, + getRelationsForEvent, + isSeeingThroughMessageHiddenForModeration, + permalinkCreator, + }); +} + +// XXX: this'll eventually be dynamic based on the fields once we have extensible event types +const messageTypes = [EventType.RoomMessage, EventType.Sticker]; +export function isMessageEvent(ev: MatrixEvent): boolean { + return (messageTypes.includes(ev.getType() as EventType)) || M_POLL_START.matches(ev.getType()); +} + +export function haveRendererForEvent(mxEvent: MatrixEvent, showHiddenEvents?: boolean): boolean { + // Only show "Message deleted" tile for plain message events, encrypted events, + // and state events as they'll likely still contain enough keys to be relevant. + if (mxEvent.isRedacted() && !mxEvent.isEncrypted() && !isMessageEvent(mxEvent) && !mxEvent.isState()) { + return false; + } + + // No tile for replacement events since they update the original tile + if (mxEvent.isRelation(RelationType.Replace)) return false; + + const handler = pickFactory(mxEvent, MatrixClientPeg.get()); + if (!handler) return false; + if (handler === TextualEventFactory) { + return hasText(mxEvent, showHiddenEvents); + } else if (handler === STATE_EVENT_TILE_TYPES[EventType.RoomCreate]) { + return Boolean(mxEvent.getContent()['predecessor']); + } else { + return true; + } +} diff --git a/src/index.ts b/src/index.ts index 9b4d431983..6d942597b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,8 +18,3 @@ limitations under the License. // Import the js-sdk so the proper `request` object can be set. This does some // magic with the browser injection to make all subsequent imports work fine. import "matrix-js-sdk/src/browser-index"; - -export function getComponent(componentName: string): any { - // return Skinner.getComponent(componentName); -} - diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index a8d27d3f79..4300ee26e0 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -1,5 +1,5 @@ /* -Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,12 +23,13 @@ import { M_POLL_START } from "matrix-events-sdk"; import { MatrixClientPeg } from '../MatrixClientPeg'; import shouldHideEvent from "../shouldHideEvent"; -import { getHandlerTile, GetRelationsForEvent, haveTileForEvent } from "../components/views/rooms/EventTile"; +import { GetRelationsForEvent } from "../components/views/rooms/EventTile"; import SettingsStore from "../settings/SettingsStore"; import defaultDispatcher from "../dispatcher/dispatcher"; import { TimelineRenderingType } from "../contexts/RoomContext"; import { launchPollEditor } from "../components/views/messages/MPollBody"; import { Action } from "../dispatcher/actions"; +import { haveRendererForEvent, JitsiEventFactory, JSONEventFactory, pickFactory } from "../events/EventTileFactory"; /** * Returns whether an event should allow actions like reply, reactions, edit, etc. @@ -134,7 +135,7 @@ export function findEditableEvent({ /** * How we should render a message depending on its moderation state. */ -enum MessageModerationState { +export enum MessageModerationState { /** * The message is visible to all. */ @@ -158,7 +159,9 @@ enum MessageModerationState { * If MSC3531 is deactivated in settings, all messages are considered visible * to all. */ -export function getMessageModerationState(mxEvent: MatrixEvent): MessageModerationState { +export function getMessageModerationState(mxEvent: MatrixEvent, client?: MatrixClient): MessageModerationState { + client = client ?? MatrixClientPeg.get(); // because param defaults don't do the correct thing + if (!SettingsStore.getValue("feature_msc3531_hide_messages_pending_moderation")) { return MessageModerationState.VISIBLE_FOR_ALL; } @@ -171,7 +174,6 @@ export function getMessageModerationState(mxEvent: MatrixEvent): MessageModerati // pending moderation. However, if we're the author or a moderator, // we still need to display it. - const client = MatrixClientPeg.get(); if (mxEvent.sender?.userId === client.getUserId()) { // We're the author, show the message. return MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER; @@ -196,7 +198,7 @@ export function getMessageModerationState(mxEvent: MatrixEvent): MessageModerati export function getEventDisplayInfo(mxEvent: MatrixEvent, hideEvent?: boolean): { isInfoMessage: boolean; - tileHandler: string; + hasRenderer: boolean; isBubbleMessage: boolean; isLeftAlignedBubbleMessage: boolean; noBubbleEvent: boolean; @@ -207,15 +209,11 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, hideEvent?: boolean): const eventType = mxEvent.getType(); let isSeeingThroughMessageHiddenForModeration = false; - let tileHandler; if (SettingsStore.getValue("feature_msc3531_hide_messages_pending_moderation")) { switch (getMessageModerationState(mxEvent)) { case MessageModerationState.VISIBLE_FOR_ALL: - // Default behavior, nothing to do. - break; case MessageModerationState.HIDDEN_TO_CURRENT_USER: - // Hide message. - tileHandler = "messages.HiddenBody"; + // Nothing specific to do here break; case MessageModerationState.SEE_THROUGH_FOR_CURRENT_USER: // Show message with a marker. @@ -223,9 +221,9 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, hideEvent?: boolean): break; } } - if (!tileHandler) { - tileHandler = getHandlerTile(mxEvent); - } + + // TODO: Thread a MatrixClient through to here + let factory = pickFactory(mxEvent, MatrixClientPeg.get()); // Info messages are basically information about commands processed on a room let isBubbleMessage = ( @@ -233,7 +231,7 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, hideEvent?: boolean): (eventType === EventType.RoomMessage && msgtype?.startsWith("m.key.verification")) || (eventType === EventType.RoomCreate) || (eventType === EventType.RoomEncryption) || - (tileHandler === "messages.MJitsiWidgetEvent") + (factory === JitsiEventFactory) ); const isLeftAlignedBubbleMessage = ( !isBubbleMessage && @@ -263,15 +261,20 @@ export function getEventDisplayInfo(mxEvent: MatrixEvent, hideEvent?: boolean): // source tile when there's no regular tile for an event and also for // replace relations (which otherwise would display as a confusing // duplicate of the thing they are replacing). - if ((hideEvent || !haveTileForEvent(mxEvent)) && SettingsStore.getValue("showHiddenEventsInTimeline")) { - tileHandler = "messages.ViewSourceEvent"; - isBubbleMessage = false; - // Reuse info message avatar and sender profile styling - isInfoMessage = true; + if (hideEvent || !haveRendererForEvent(mxEvent)) { + // forcefully ask for a factory for a hidden event (hidden event + // setting is checked internally) + // TODO: Thread a MatrixClient through to here + factory = pickFactory(mxEvent, MatrixClientPeg.get(), true); + if (factory === JSONEventFactory) { + isBubbleMessage = false; + // Reuse info message avatar and sender profile styling + isInfoMessage = true; + } } return { - tileHandler, + hasRenderer: !!factory, isInfoMessage, isBubbleMessage, isLeftAlignedBubbleMessage, diff --git a/src/utils/exportUtils/HtmlExport.tsx b/src/utils/exportUtils/HtmlExport.tsx index a7afeda928..89ac6146ca 100644 --- a/src/utils/exportUtils/HtmlExport.tsx +++ b/src/utils/exportUtils/HtmlExport.tsx @@ -31,7 +31,7 @@ import { formatFullDateNoDayNoTime, wantsDateSeparator } from "../../DateUtils"; import { RoomPermalinkCreator } from "../permalinks/Permalinks"; import { _t } from "../../languageHandler"; import * as Avatar from "../../Avatar"; -import EventTile, { haveTileForEvent } from "../../components/views/rooms/EventTile"; +import EventTile from "../../components/views/rooms/EventTile"; import DateSeparator from "../../components/views/messages/DateSeparator"; import BaseAvatar from "../../components/views/avatars/BaseAvatar"; import { ExportType } from "./exportUtils"; @@ -39,6 +39,7 @@ import { IExportOptions } from "./exportUtils"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import getExportCSS from "./exportCSS"; import { textForEvent } from "../../TextForEvent"; +import { haveRendererForEvent } from "../../events/EventTileFactory"; import exportJS from "!!raw-loader!./exportJS"; @@ -406,7 +407,7 @@ export default class HTMLExporter extends Exporter { total: events.length, }), false, true); if (this.cancelled) return this.cleanUp(); - if (!haveTileForEvent(event)) continue; + if (!haveRendererForEvent(event)) continue; content += this.needsDateSeparator(event, prevEvent) ? this.getDateSeparator(event) : ""; const shouldBeJoined = !this.needsDateSeparator(event, prevEvent) && diff --git a/src/utils/exportUtils/JSONExport.ts b/src/utils/exportUtils/JSONExport.ts index 27fdb5a847..673420327e 100644 --- a/src/utils/exportUtils/JSONExport.ts +++ b/src/utils/exportUtils/JSONExport.ts @@ -21,9 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import Exporter from "./Exporter"; import { formatFullDateNoDay, formatFullDateNoDayNoTime } from "../../DateUtils"; -import { haveTileForEvent } from "../../components/views/rooms/EventTile"; import { ExportType, IExportOptions } from "./exportUtils"; import { _t } from "../../languageHandler"; +import { haveRendererForEvent } from "../../events/EventTileFactory"; export default class JSONExporter extends Exporter { protected totalSize = 0; @@ -85,7 +85,7 @@ export default class JSONExporter extends Exporter { total: events.length, }), false, true); if (this.cancelled) return this.cleanUp(); - if (!haveTileForEvent(event)) continue; + if (!haveRendererForEvent(event)) continue; this.messages.push(await this.getJSONString(event)); } return this.createJSONString(); diff --git a/src/utils/exportUtils/PlainTextExport.ts b/src/utils/exportUtils/PlainTextExport.ts index edabb80fe0..d41d06d35c 100644 --- a/src/utils/exportUtils/PlainTextExport.ts +++ b/src/utils/exportUtils/PlainTextExport.ts @@ -21,9 +21,9 @@ import { logger } from "matrix-js-sdk/src/logger"; import Exporter from "./Exporter"; import { formatFullDateNoDay } from "../../DateUtils"; import { _t } from "../../languageHandler"; -import { haveTileForEvent } from "../../components/views/rooms/EventTile"; import { ExportType, IExportOptions } from "./exportUtils"; import { textForEvent } from "../../TextForEvent"; +import { haveRendererForEvent } from "../../events/EventTileFactory"; export default class PlainTextExporter extends Exporter { protected totalSize: number; @@ -112,7 +112,7 @@ export default class PlainTextExporter extends Exporter { total: events.length, }), false, true); if (this.cancelled) return this.cleanUp(); - if (!haveTileForEvent(event)) continue; + if (!haveRendererForEvent(event)) continue; const textForEvent = await this.plainTextForEvent(event); content += textForEvent && `${new Date(event.getTs()).toLocaleString()} - ${textForEvent}\n`; }