Step 6: Refactor event rendering to stop using `getComponent`

We move all of the event tile rendering into a factory manager for a couple reasons:
1. `EventTile` is uncomfortably large for a file
2. A simple map isn't possible anymore (can't convert the existing maps like `eventTileTypes` to `Record<string, typeof React.Component>` because the types are actually incompatible)

So, by having a factory manager place we can more easily render components without having to use `getComponent()` all over the place, and without lying to ourselves about how simple the event rendering path is.

This change also moves quite a bit of the rendering path into the new `EventTileFactory` file so it can be easily seen by future developers.
pull/21833/head
Travis Ralston 2022-03-22 23:16:25 -06:00
parent 115ae198c8
commit 9350c50f87
14 changed files with 523 additions and 283 deletions

View File

@ -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 {

View File

@ -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<IProps, IState> {
return true;
}
if (!haveTileForEvent(mxEv, this.showHiddenEvents)) {
if (!haveRendererForEvent(mxEv, this.showHiddenEvents)) {
return false; // no tile = no show
}

View File

@ -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<IRoomProps, IRoomState> {
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;

View File

@ -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<IProps, IState> {
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) {

View File

@ -41,10 +41,10 @@ import MLocationBody from "./MLocationBody";
import MjolnirBody from "./MjolnirBody";
// onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed"> {
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
/* overrides for the msgtype-specific components, used by ReplyTile to override file rendering */
overrideBodyTypes?: Record<string, React.Component>;
overrideEventTypes?: Record<string, React.Component>;
overrideBodyTypes?: Record<string, typeof React.Component>;
overrideEventTypes?: Record<string, typeof React.Component>;
// helper function to access relations for this event
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;

View File

@ -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<IProps, IState> {
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<IProps, IState> {
// 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 <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
@ -1075,7 +956,6 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
</div>;
}
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<IProps, IState> {
) {
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<IProps, IState> {
msgOption = readAvatars;
}
const replyChain = haveTileForEvent(this.props.mxEvent) && shouldDisplayReply(this.props.mxEvent)
? <ReplyChain
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyChain}
forExport={this.props.forExport}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
isQuoteExpanded={isQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
: null;
const replyChain =
(haveRendererForEvent(this.props.mxEvent) && shouldDisplayReply(this.props.mxEvent))
? <ReplyChain
parentEv={this.props.mxEvent}
onHeightChanged={this.props.onHeightChanged}
ref={this.replyChain}
forExport={this.props.forExport}
permalinkCreator={this.props.permalinkCreator}
layout={this.props.layout}
alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.hover}
isQuoteExpanded={isQuoteExpanded}
setQuoteExpanded={this.setQuoteExpanded}
getRelationsForEvent={this.props.getRelationsForEvent}
/>
: null;
const isOwnEvent = this.props.mxEvent?.sender?.userId === MatrixClientPeg.get().getUserId();
@ -1362,16 +1243,19 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
</a>
</div>,
<div className={lineClasses} key="mx_EventTile_line">
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
editState={this.props.editState}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
/>
{ 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,
}) }
</div>,
]);
}
@ -1403,17 +1287,19 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
</div>,
<div className={lineClasses} key="mx_EventTile_line">
{ replyChain }
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
editState={this.props.editState}
replacingEventId={this.props.replacingEventId}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
/>
{ 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 }
</div>,
@ -1485,16 +1371,19 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
"data-scroll-tokens": scrollToken,
}, [
<div className={lineClasses} key="mx_EventTile_line">
<EventTileType ref={this.tile}
mxEvent={this.props.mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
onHeightChanged={this.props.onHeightChanged}
editState={this.props.editState}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
/>
{ 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,
}) }
</div>,
<a
className="mx_EventTile_senderDetailsLink"
@ -1534,22 +1423,20 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
{ groupTimestamp }
{ groupPadlock }
{ replyChain }
<EventTileType
ref={this.tile}
mxEvent={this.props.mxEvent}
forExport={this.props.forExport}
replacingEventId={this.props.replacingEventId}
editState={this.props.editState}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
showUrlPreview={this.props.showUrlPreview}
permalinkCreator={this.props.permalinkCreator}
onHeightChanged={this.props.onHeightChanged}
callEventGrouper={this.props.callEventGrouper}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
timestamp={bubbleTimestamp}
/>
{ 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<UnwrappedEventTi
});
export default SafeEventTile;
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
const messageTypes = [EventType.RoomMessage, EventType.Sticker];
function isMessageEvent(ev: MatrixEvent): boolean {
return (messageTypes.includes(ev.getType() as EventType)) || M_POLL_START.matches(ev.getType());
}
export function haveTileForEvent(e: 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 (e.isRedacted() && !e.isEncrypted() && !isMessageEvent(e) && !e.isState()) return false;
// No tile for replacement events since they update the original tile
if (e.isRelation(RelationType.Replace)) return false;
const handler = getHandlerTile(e);
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return hasText(e, showHiddenEvents);
} else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']);
} else {
return true;
}
}
function E2ePadlockUndecryptable(props) {
return (
<E2ePadlock title={_t("This message cannot be decrypted")} icon={E2ePadlockIcon.Warning} {...props} />

View File

@ -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<IProps> {
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 <div className="mx_ReplyTile mx_ReplyTile_info mx_MNoticeBody">
@ -120,8 +120,6 @@ export default class ReplyTile extends React.PureComponent<IProps> {
</div>;
}
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<IProps> {
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<IProps> {
/>;
}
const msgtypeOverrides = {
const msgtypeOverrides: Record<string, typeof React.Component> = {
[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<string, typeof React.Component> = {
// 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<IProps> {
<div className={classes}>
<a href={permalink} onClick={this.onClick} ref={this.anchorElement}>
{ sender }
<EventTileType
ref="tile"
mxEvent={mxEvent}
highlights={this.props.highlights}
highlightLink={this.props.highlightLink}
onHeightChanged={this.props.onHeightChanged}
showUrlPreview={false}
overrideBodyTypes={msgtypeOverrides}
overrideEventTypes={evOverrides}
replacingEventId={mxEvent.replacingEventId()}
maxImageHeight={96}
getRelationsForEvent={this.props.getRelationsForEvent}
isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration}
/>
{ renderReplyTile({
...this.props,
// overrides
ref: null,
showUrlPreview: false,
overrideBodyTypes: msgtypeOverrides,
overrideEventTypes: evOverrides,
maxImageHeight: 96,
isSeeingThroughMessageHiddenForModeration,
// appease TS
highlights: this.props.highlights,
highlightLink: this.props.highlightLink,
onHeightChanged: this.props.onHeightChanged,
permalinkCreator: this.props.permalinkCreator,
}) }
</a>
</div>
);

View File

@ -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<IProps> {
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?

View File

@ -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>; // `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<string, typeof React.Component>;
overrideEventTypes?: Record<string, typeof React.Component>;
}
type FactoryProps = Omit<EventTileTypeProps, "ref">;
type Factory<X = FactoryProps> = (ref: Optional<React.RefObject<any>>, props: X) => JSX.Element;
type FactoryMap = Record<string, Factory>;
const MessageEventFactory: Factory = (ref, props) => <MessageEvent ref={ref} {...props} />;
const KeyVerificationConclFactory: Factory = (ref, props) => <MKeyVerificationConclusion ref={ref} {...props} />;
const CallEventFactory: Factory<FactoryProps & { callEventGrouper: CallEventGrouper }> = (ref, props) => (
<CallEvent ref={ref} {...props} />
);
const TextualEventFactory: Factory = (ref, props) => <TextualEvent ref={ref} {...props} />;
const VerificationReqFactory: Factory = (ref, props) => <MKeyVerificationRequest ref={ref} {...props} />;
const HiddenEventFactory: Factory = (ref, props) => <HiddenBody ref={ref} {...props} />;
// These factories are exported for reference comparison against pickFactory()
export const JitsiEventFactory: Factory = (ref, props) => <MJitsiWidgetEvent ref={ref} {...props} />;
export const JSONEventFactory: Factory = (ref, props) => <ViewSourceEvent ref={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) => <EncryptionEvent ref={ref} {...props} />,
[EventType.RoomCanonicalAlias]: TextualEventFactory,
[EventType.RoomCreate]: (ref, props) => <RoomCreate ref={ref} {...props} />,
[EventType.RoomMember]: TextualEventFactory,
[EventType.RoomName]: TextualEventFactory,
[EventType.RoomAvatar]: (ref, props) => <RoomAvatarEvent ref={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<Factory> {
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<Factory>) = () => 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<JSX.Element> {
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<JSX.Element> {
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;
}
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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) &&

View File

@ -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();

View File

@ -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`;
}