UNPKG

matrix-react-sdk

Version:
1,004 lines (972 loc) 217 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.UnwrappedEventTile = void 0; exports.isEligibleForSpecialReceipt = isEligibleForSpecialReceipt; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireWildcard(require("react")); var _classnames = _interopRequireDefault(require("classnames")); var _matrix = require("matrix-js-sdk/src/matrix"); var _logger = require("matrix-js-sdk/src/logger"); var _call = require("matrix-js-sdk/src/webrtc/call"); var _crypto = require("matrix-js-sdk/src/crypto"); var _cryptoApi = require("matrix-js-sdk/src/crypto-api"); var _compoundWeb = require("@vector-im/compound-web"); var _ReplyChain = _interopRequireDefault(require("../elements/ReplyChain")); var _languageHandler = require("../../../languageHandler"); var _dispatcher = _interopRequireDefault(require("../../../dispatcher/dispatcher")); var _Layout = require("../../../settings/enums/Layout"); var _DateUtils = require("../../../DateUtils"); var _MatrixClientPeg = require("../../../MatrixClientPeg"); var _DecryptionFailureBody = require("../messages/DecryptionFailureBody"); var _RoomAvatar = _interopRequireDefault(require("../avatars/RoomAvatar")); var _MessageContextMenu = _interopRequireDefault(require("../context_menus/MessageContextMenu")); var _ContextMenu = require("../../structures/ContextMenu"); var _objects = require("../../../utils/objects"); var _StaticNotificationState = require("../../../stores/notifications/StaticNotificationState"); var _NotificationBadge = _interopRequireDefault(require("./NotificationBadge")); var _actions = require("../../../dispatcher/actions"); var _PlatformPeg = _interopRequireDefault(require("../../../PlatformPeg")); var _MemberAvatar = _interopRequireDefault(require("../avatars/MemberAvatar")); var _SenderProfile = _interopRequireDefault(require("../messages/SenderProfile")); var _MessageTimestamp = _interopRequireDefault(require("../messages/MessageTimestamp")); var _MessageActionBar = _interopRequireDefault(require("../messages/MessageActionBar")); var _ReactionsRow = _interopRequireDefault(require("../messages/ReactionsRow")); var _EventRenderingUtils = require("../../../utils/EventRenderingUtils"); var _MessagePreviewStore = require("../../../stores/room-list/MessagePreviewStore"); var _RoomContext = _interopRequireWildcard(require("../../../contexts/RoomContext")); var _MediaEventHelper = require("../../../utils/MediaEventHelper"); var _strings = require("../../../utils/strings"); var _DecryptionFailureTracker = require("../../../DecryptionFailureTracker"); var _RedactedBody = _interopRequireDefault(require("../messages/RedactedBody")); var _Reply = require("../../../utils/Reply"); var _PosthogTrackers = _interopRequireDefault(require("../../../PosthogTrackers")); var _TileErrorBoundary = _interopRequireDefault(require("../messages/TileErrorBoundary")); var _EventTileFactory = require("../../../events/EventTileFactory"); var _ThreadSummary = _interopRequireWildcard(require("./ThreadSummary")); var _ReadReceiptGroup = require("./ReadReceiptGroup"); var _isLocalRoom = require("../../../utils/localRoom/isLocalRoom"); var _Call = require("../../../models/Call"); var _UnreadNotificationBadge = require("./NotificationBadge/UnreadNotificationBadge"); var _EventTileThreadToolbar = require("./EventTile/EventTileThreadToolbar"); var _LateEventGrouper = require("../../structures/grouper/LateEventGrouper"); var _PinningUtils = _interopRequireDefault(require("../../../utils/PinningUtils.ts")); var _PinnedMessageBadge = require("../messages/PinnedMessageBadge.tsx"); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } /* Copyright 2024 New Vector Ltd. Copyright 2015-2023 The Matrix.org Foundation C.I.C. Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ // Our component structure for EventTiles on the timeline is: // // .-EventTile------------------------------------------------. // | MemberAvatar (SenderProfile) TimeStamp | // | .-{Message,Textual}Event---------------. Read Avatars | // | | .-MFooBody-------------------. | | // | | | (only if MessageEvent) | | | // | | '----------------------------' | | // | '--------------------------------------' | // '----------------------------------------------------------' /** * When true, the tile qualifies for some sort of special read receipt. * This could be a 'sending' or 'sent' receipt, for example. * @returns {boolean} */ function isEligibleForSpecialReceipt(event) { // Determine if the type is relevant to the user. // This notably excludes state events and pretty much anything that can't be sent by the composer as a message. // For those we rely on local echo giving the impression of things changing, and expect them to be quick. if (!(0, _EventTileFactory.isMessageEvent)(event) && event.getType() !== _matrix.EventType.RoomMessageEncrypted) return false; // Default case return true; } // MUST be rendered within a RoomContext with a set timelineRenderingType class UnwrappedEventTile extends _react.default.Component { constructor(props, context) { super(props, context); (0, _defineProperty2.default)(this, "suppressReadReceiptAnimation", void 0); (0, _defineProperty2.default)(this, "isListeningForReceipts", void 0); (0, _defineProperty2.default)(this, "tile", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "replyChain", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "ref", /*#__PURE__*/(0, _react.createRef)()); (0, _defineProperty2.default)(this, "unmounted", false); (0, _defineProperty2.default)(this, "updateThread", thread => { this.setState({ thread }); }); (0, _defineProperty2.default)(this, "onNewThread", thread => { if (thread.id === this.props.mxEvent.getId()) { this.updateThread(thread); const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId()); room?.off(_matrix.ThreadEvent.New, this.onNewThread); } }); (0, _defineProperty2.default)(this, "viewInRoom", evt => { evt.preventDefault(); evt.stopPropagation(); _dispatcher.default.dispatch({ action: _actions.Action.ViewRoom, event_id: this.props.mxEvent.getId(), highlighted: true, room_id: this.props.mxEvent.getRoomId(), metricsTrigger: undefined // room doesn't change }); }); (0, _defineProperty2.default)(this, "copyLinkToThread", async evt => { evt.preventDefault(); evt.stopPropagation(); const { permalinkCreator, mxEvent } = this.props; if (!permalinkCreator) return; const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId()); await (0, _strings.copyPlaintext)(matrixToUrl); }); (0, _defineProperty2.default)(this, "onRoomReceipt", (ev, room) => { // ignore events for other rooms const tileRoom = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId()); if (room !== tileRoom) return; if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) { return; } // We force update because we have no state or prop changes to queue up, instead relying on // the getters we use here to determine what needs rendering. this.forceUpdate(() => { // Per elsewhere in this file, we can remove the listener once we will have no further purpose for it. if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt) { _MatrixClientPeg.MatrixClientPeg.safeGet().removeListener(_matrix.RoomEvent.Receipt, this.onRoomReceipt); this.isListeningForReceipts = false; } }); }); /** called when the event is decrypted after we show it. */ (0, _defineProperty2.default)(this, "onDecrypted", () => { // we need to re-verify the sending device. this.verifyEvent(); // decryption might, of course, trigger a height change, so call onHeightChanged after the re-render this.forceUpdate(this.props.onHeightChanged); }); (0, _defineProperty2.default)(this, "onUserVerificationChanged", (userId, _trustStatus) => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(); } }); /** called when the event is edited after we show it. */ (0, _defineProperty2.default)(this, "onReplaced", () => { // re-verify the event if it is replaced (the edit may not be verified) this.verifyEvent(); }); (0, _defineProperty2.default)(this, "onSenderProfileClick", () => { _dispatcher.default.dispatch({ action: _actions.Action.ComposerInsert, userId: this.props.mxEvent.getSender(), timelineRenderingType: this.context.timelineRenderingType }); }); (0, _defineProperty2.default)(this, "onPermalinkClicked", e => { // This allows the permalink to be opened in a new tab/window or copied as // matrix.to, but also for it to enable routing within Element when clicked. e.preventDefault(); _dispatcher.default.dispatch({ action: _actions.Action.ViewRoom, event_id: this.props.mxEvent.getId(), highlighted: true, room_id: this.props.mxEvent.getRoomId(), metricsTrigger: this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Search ? "MessageSearch" : undefined }); }); (0, _defineProperty2.default)(this, "onActionBarFocusChange", actionBarFocused => { this.setState({ actionBarFocused }); }); (0, _defineProperty2.default)(this, "getTile", () => this.tile.current); (0, _defineProperty2.default)(this, "getReplyChain", () => this.replyChain.current); (0, _defineProperty2.default)(this, "getReactions", () => { if (!this.props.showReactions || !this.props.getRelationsForEvent) { return null; } const eventId = this.props.mxEvent.getId(); return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction") ?? null; }); (0, _defineProperty2.default)(this, "onReactionsCreated", (relationType, eventType) => { if (relationType !== "m.annotation" || eventType !== "m.reaction") { return; } this.setState({ reactions: this.getReactions() }); }); (0, _defineProperty2.default)(this, "onContextMenu", ev => { this.showContextMenu(ev); }); (0, _defineProperty2.default)(this, "onTimestampContextMenu", ev => { this.showContextMenu(ev, this.props.permalinkCreator?.forEvent(this.props.mxEvent.getId())); }); (0, _defineProperty2.default)(this, "onCloseMenu", () => { this.setState({ contextMenu: undefined, actionBarFocused: false }); }); (0, _defineProperty2.default)(this, "setQuoteExpanded", expanded => { this.setState({ isQuoteExpanded: expanded }); }); const _thread = this.thread; this.state = { // Whether the action bar is focused. actionBarFocused: false, shieldColour: _cryptoApi.EventShieldColour.NONE, shieldReason: null, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), hover: false, thread: _thread }; // don't do RR animations until we are mounted this.suppressReadReceiptAnimation = true; // Throughout the component we manage a read receipt listener to see if our tile still // qualifies for a "sent" or "sending" state (based on their relevant conditions). We // don't want to over-subscribe to the read receipt events being fired, so we use a flag // to determine if we've already subscribed and use a combination of other flags to find // out if we should even be subscribed at all. this.isListeningForReceipts = false; } /** * When true, the tile qualifies for some sort of special read receipt. This could be a 'sending' * or 'sent' receipt, for example. * @returns {boolean} */ get isEligibleForSpecialReceipt() { // First, if there are other read receipts then just short-circuit this. if (this.props.readReceipts && this.props.readReceipts.length > 0) return false; if (!this.props.mxEvent) return false; // Sanity check (should never happen, but we shouldn't explode if it does) const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId()); if (!room) return false; // Quickly check to see if the event was sent by us. If it wasn't, it won't qualify for // special read receipts. const myUserId = _MatrixClientPeg.MatrixClientPeg.safeGet().getSafeUserId(); // Check to see if the event was sent by us. If it wasn't, it won't qualify for special read receipts. if (this.props.mxEvent.getSender() !== myUserId) return false; return isEligibleForSpecialReceipt(this.props.mxEvent); } get shouldShowSentReceipt() { // If we're not even eligible, don't show the receipt. if (!this.isEligibleForSpecialReceipt) return false; // We only show the 'sent' receipt on the last successful event. if (!this.props.lastSuccessful) return false; // Check to make sure the sending state is appropriate. A null/undefined send status means // that the message is 'sent', so we're just double checking that it's explicitly not sent. if (this.props.eventSendStatus && this.props.eventSendStatus !== _matrix.EventStatus.SENT) return false; // If anyone has read the event besides us, we don't want to show a sent receipt. const receipts = this.props.readReceipts || []; const myUserId = _MatrixClientPeg.MatrixClientPeg.safeGet().getUserId(); if (receipts.some(r => r.userId !== myUserId)) return false; // Finally, we should show a receipt. return true; } get shouldShowSendingReceipt() { // If we're not even eligible, don't show the receipt. if (!this.isEligibleForSpecialReceipt) return false; // Check the event send status to see if we are pending. Null/undefined status means the // message was sent, so check for that and 'sent' explicitly. if (!this.props.eventSendStatus || this.props.eventSendStatus === _matrix.EventStatus.SENT) return false; // Default to showing - there's no other event properties/behaviours we care about at // this point. return true; } componentDidMount() { this.suppressReadReceiptAnimation = false; const client = _MatrixClientPeg.MatrixClientPeg.safeGet(); if (!this.props.forExport) { client.on(_crypto.CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); this.props.mxEvent.on(_matrix.MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.on(_matrix.MatrixEventEvent.Replaced, this.onReplaced); _DecryptionFailureTracker.DecryptionFailureTracker.instance.addVisibleEvent(this.props.mxEvent); if (this.props.showReactions) { this.props.mxEvent.on(_matrix.MatrixEventEvent.RelationsCreated, this.onReactionsCreated); } if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { client.on(_matrix.RoomEvent.Receipt, this.onRoomReceipt); this.isListeningForReceipts = true; } } this.props.mxEvent.on(_matrix.ThreadEvent.Update, this.updateThread); client.decryptEventIfNeeded(this.props.mxEvent); const room = client.getRoom(this.props.mxEvent.getRoomId()); room?.on(_matrix.ThreadEvent.New, this.onNewThread); this.verifyEvent(); } shouldComponentUpdate(nextProps, nextState) { if ((0, _objects.objectHasDiff)(this.state, nextState)) { return true; } return !this.propsEqual(this.props, nextProps); } componentWillUnmount() { const client = _MatrixClientPeg.MatrixClientPeg.get(); if (client) { client.removeListener(_crypto.CryptoEvent.UserTrustStatusChanged, this.onUserVerificationChanged); client.removeListener(_matrix.RoomEvent.Receipt, this.onRoomReceipt); const room = client.getRoom(this.props.mxEvent.getRoomId()); room?.off(_matrix.ThreadEvent.New, this.onNewThread); } this.isListeningForReceipts = false; this.props.mxEvent.removeListener(_matrix.MatrixEventEvent.Decrypted, this.onDecrypted); this.props.mxEvent.removeListener(_matrix.MatrixEventEvent.Replaced, this.onReplaced); if (this.props.showReactions) { this.props.mxEvent.removeListener(_matrix.MatrixEventEvent.RelationsCreated, this.onReactionsCreated); } this.props.mxEvent.off(_matrix.ThreadEvent.Update, this.updateThread); this.unmounted = false; } componentDidUpdate(prevProps, prevState) { // If the shield state changed, the height might have changed. // XXX: does the shield *actually* cause a change in height? Not sure. if (prevState.shieldColour !== this.state.shieldColour && this.props.onHeightChanged) { this.props.onHeightChanged(); } // If we're not listening for receipts and expect to be, register a listener. if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) { _MatrixClientPeg.MatrixClientPeg.safeGet().on(_matrix.RoomEvent.Receipt, this.onRoomReceipt); this.isListeningForReceipts = true; } // re-check the sender verification as outgoing events progress through the send process. if (prevProps.eventSendStatus !== this.props.eventSendStatus) { this.verifyEvent(); } } get thread() { let thread = this.props.mxEvent.getThread(); /** * Accessing the threads value through the room due to a race condition * that will be solved when there are proper backend support for threads * We currently have no reliable way to discover than an event is a thread * when we are at the sync stage */ if (!thread) { const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId()); thread = room?.findThreadForEvent(this.props.mxEvent) ?? undefined; } return thread ?? null; } renderThreadPanelSummary() { if (!this.state.thread) { return null; } return /*#__PURE__*/_react.default.createElement("div", { className: "mx_ThreadPanel_replies" }, /*#__PURE__*/_react.default.createElement("span", { className: "mx_ThreadPanel_replies_amount" }, this.state.thread.length), /*#__PURE__*/_react.default.createElement(_ThreadSummary.ThreadMessagePreview, { thread: this.state.thread })); } renderThreadInfo() { if (this.state.thread && this.state.thread.id === this.props.mxEvent.getId()) { return /*#__PURE__*/_react.default.createElement(_ThreadSummary.default, { mxEvent: this.props.mxEvent, thread: this.state.thread, "data-testid": "thread-summary" }); } if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Search && this.props.mxEvent.threadRootId) { if (this.props.highlightLink) { return /*#__PURE__*/_react.default.createElement("a", { className: "mx_ThreadSummary_icon", href: this.props.highlightLink }, (0, _languageHandler._t)("timeline|thread_info_basic")); } return /*#__PURE__*/_react.default.createElement("p", { className: "mx_ThreadSummary_icon" }, (0, _languageHandler._t)("timeline|thread_info_basic")); } } verifyEvent() { this.doVerifyEvent().catch(e => { const event = this.props.mxEvent; _logger.logger.error(`Error getting encryption info on event ${event.getId()} in room ${event.getRoomId()}`, e); }); } async doVerifyEvent() { // if the event was edited, show the verification info for the edit, not // the original const mxEvent = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; if (!mxEvent.isEncrypted() || mxEvent.isRedacted()) { this.setState({ shieldColour: _cryptoApi.EventShieldColour.NONE, shieldReason: null }); return; } const encryptionInfo = (await _MatrixClientPeg.MatrixClientPeg.safeGet().getCrypto()?.getEncryptionInfoForEvent(mxEvent)) ?? null; if (this.unmounted) return; if (encryptionInfo === null) { // likely a decryption error this.setState({ shieldColour: _cryptoApi.EventShieldColour.NONE, shieldReason: null }); return; } this.setState({ shieldColour: encryptionInfo.shieldColour, shieldReason: encryptionInfo.shieldReason }); } propsEqual(objA, objB) { const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } for (let i = 0; i < keysA.length; i++) { const key = keysA[i]; if (!objB.hasOwnProperty(key)) { return false; } // need to deep-compare readReceipts if (key === "readReceipts") { const rA = objA[key]; const rB = objB[key]; if (rA === rB) { continue; } if (!rA || !rB) { return false; } if (rA.length !== rB.length) { return false; } for (let j = 0; j < rA.length; j++) { if (rA[j].userId !== rB[j].userId) { return false; } // one has a member set and the other doesn't? if (rA[j].roomMember !== rB[j].roomMember) { return false; } } } else { if (objA[key] !== objB[key]) { return false; } } } return true; } /** * Determine whether an event should be highlighted * For edited events, if a previous version of the event was highlighted * the event should remain highlighted as the user may have been notified * (Clearer explanation of why an event is highlighted is planned - * https://github.com/vector-im/element-web/issues/24927) * @returns boolean */ shouldHighlight() { if (this.props.forExport) return false; if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Notification) return false; if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList) return false; const cli = _MatrixClientPeg.MatrixClientPeg.safeGet(); const actions = cli.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); // get the actions for the previous version of the event too if it is an edit const previousActions = this.props.mxEvent.replacingEvent() ? cli.getPushActionsForEvent(this.props.mxEvent) : undefined; if (!actions?.tweaks && !previousActions?.tweaks) { return false; } // don't show self-highlights from another of our clients if (this.props.mxEvent.getSender() === cli.credentials.userId) { return false; } return !!(actions?.tweaks.highlight || previousActions?.tweaks.highlight); } renderE2EPadlock() { // if the event was edited, show the verification info for the edit, not // the original const ev = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; // no icon for local rooms if ((0, _isLocalRoom.isLocalRoom)(ev.getRoomId())) return null; // event could not be decrypted if (ev.isDecryptionFailure()) { return /*#__PURE__*/_react.default.createElement(E2ePadlockDecryptionFailure, null); } if (this.state.shieldColour !== _cryptoApi.EventShieldColour.NONE) { let shieldReasonMessage; switch (this.state.shieldReason) { case null: case _cryptoApi.EventShieldReason.UNKNOWN: shieldReasonMessage = (0, _languageHandler._t)("error|unknown"); break; case _cryptoApi.EventShieldReason.UNVERIFIED_IDENTITY: shieldReasonMessage = (0, _languageHandler._t)("encryption|event_shield_reason_unverified_identity"); break; case _cryptoApi.EventShieldReason.UNSIGNED_DEVICE: shieldReasonMessage = (0, _languageHandler._t)("encryption|event_shield_reason_unsigned_device"); break; case _cryptoApi.EventShieldReason.UNKNOWN_DEVICE: shieldReasonMessage = (0, _languageHandler._t)("encryption|event_shield_reason_unknown_device"); break; case _cryptoApi.EventShieldReason.AUTHENTICITY_NOT_GUARANTEED: shieldReasonMessage = (0, _languageHandler._t)("encryption|event_shield_reason_authenticity_not_guaranteed"); break; case _cryptoApi.EventShieldReason.MISMATCHED_SENDER_KEY: shieldReasonMessage = (0, _languageHandler._t)("encryption|event_shield_reason_mismatched_sender_key"); break; } if (this.state.shieldColour === _cryptoApi.EventShieldColour.GREY) { return /*#__PURE__*/_react.default.createElement(E2ePadlock, { icon: E2ePadlockIcon.Normal, title: shieldReasonMessage }); } else { // red, by elimination return /*#__PURE__*/_react.default.createElement(E2ePadlock, { icon: E2ePadlockIcon.Warning, title: shieldReasonMessage }); } } if (_MatrixClientPeg.MatrixClientPeg.safeGet().isRoomEncrypted(ev.getRoomId())) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === _matrix.EventStatus.ENCRYPTING) { return null; } if (ev.status === _matrix.EventStatus.NOT_SENT) { return null; } if (ev.isState()) { return null; // we expect this to be unencrypted } if (ev.isRedacted()) { return null; // we expect this to be unencrypted } if (!ev.isEncrypted()) { // if the event is not encrypted, but it's an e2e room, show a warning return /*#__PURE__*/_react.default.createElement(E2ePadlockUnencrypted, null); } } // no padlock needed return null; } showContextMenu(ev, permalink) { const clickTarget = ev.target; // Try to find an anchor element const anchorElement = clickTarget instanceof HTMLAnchorElement ? clickTarget : clickTarget.closest("a"); // There is no way to copy non-PNG images into clipboard, so we can't // have our own handling for copying images, so we leave it to the // Electron layer (webcontents-handler.ts) if (clickTarget instanceof HTMLImageElement) return; // Return if we're in a browser and click either an a tag or we have // selected text, as in those cases we want to use the native browser // menu if (!_PlatformPeg.default.get()?.allowOverridingNativeContextMenus() && ((0, _strings.getSelectedText)() || anchorElement)) return; // We don't want to show the menu when editing a message if (this.props.editState) return; ev.preventDefault(); ev.stopPropagation(); this.setState({ contextMenu: { position: { left: ev.clientX, top: ev.clientY, bottom: ev.clientY }, link: anchorElement?.href || permalink }, actionBarFocused: true }); } /** * In some cases we can't use shouldHideEvent() since whether or not we hide * an event depends on other things that the event itself * @returns {boolean} true if event should be hidden */ shouldHideEvent() { // If the call was replaced we don't render anything since we render the other call if (this.props.callEventGrouper?.hangupReason === _call.CallErrorCode.Replaced) return true; return false; } renderContextMenu() { if (!this.state.contextMenu) return null; const tile = this.getTile(); const replyChain = this.getReplyChain(); const eventTileOps = tile?.getEventTileOps ? tile.getEventTileOps() : undefined; const collapseReplyChain = replyChain?.canCollapse() ? replyChain.collapse : undefined; return /*#__PURE__*/_react.default.createElement(_MessageContextMenu.default, (0, _extends2.default)({}, (0, _ContextMenu.aboveRightOf)(this.state.contextMenu.position), { mxEvent: this.props.mxEvent, permalinkCreator: this.props.permalinkCreator, eventTileOps: eventTileOps, collapseReplyChain: collapseReplyChain, onFinished: this.onCloseMenu, rightClick: true, reactions: this.state.reactions, link: this.state.contextMenu.link, getRelationsForEvent: this.props.getRelationsForEvent })); } render() { const msgtype = this.props.mxEvent.getContent().msgtype; const eventType = this.props.mxEvent.getType(); const { hasRenderer, isBubbleMessage, isInfoMessage, isLeftAlignedBubbleMessage, noBubbleEvent, isSeeingThroughMessageHiddenForModeration } = (0, _EventRenderingUtils.getEventDisplayInfo)(_MatrixClientPeg.MatrixClientPeg.safeGet(), this.props.mxEvent, this.context.showHiddenEvents, this.shouldHideEvent()); const { isQuoteExpanded } = this.state; // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!hasRenderer) { const { mxEvent } = this.props; _logger.logger.warn(`Event type not supported: type:${eventType} isState:${mxEvent.isState()}`); return /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile mx_EventTile_info mx_MNoticeBody" }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_line" }, (0, _languageHandler._t)("timeline|error_no_renderer"))); } const isProbablyMedia = _MediaEventHelper.MediaEventHelper.isEligible(this.props.mxEvent); const lineClasses = (0, _classnames.default)("mx_EventTile_line", { mx_EventTile_mediaLine: isProbablyMedia, mx_EventTile_image: this.props.mxEvent.getType() === _matrix.EventType.RoomMessage && this.props.mxEvent.getContent().msgtype === _matrix.MsgType.Image, mx_EventTile_sticker: this.props.mxEvent.getType() === _matrix.EventType.Sticker, mx_EventTile_emote: this.props.mxEvent.getType() === _matrix.EventType.RoomMessage && this.props.mxEvent.getContent().msgtype === _matrix.MsgType.Emote }); const isSending = ["sending", "queued", "encrypting"].includes(this.props.eventSendStatus); const isRedacted = (0, _EventTileFactory.isMessageEvent)(this.props.mxEvent) && this.props.isRedacted; const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); let isContinuation = this.props.continuation; if (this.context.timelineRenderingType !== _RoomContext.TimelineRenderingType.Room && this.context.timelineRenderingType !== _RoomContext.TimelineRenderingType.Search && this.context.timelineRenderingType !== _RoomContext.TimelineRenderingType.Thread && this.props.layout !== _Layout.Layout.Bubble) { isContinuation = false; } const isRenderingNotification = this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Notification; const isEditing = !!this.props.editState; const classes = (0, _classnames.default)({ mx_EventTile_bubbleContainer: isBubbleMessage, mx_EventTile_leftAlignedBubble: isLeftAlignedBubbleMessage, mx_EventTile: true, mx_EventTile_isEditing: isEditing, mx_EventTile_info: isInfoMessage, mx_EventTile_12hr: this.props.isTwelveHour, // Note: we keep the `sending` state class for tests, not for our styles mx_EventTile_sending: !isEditing && isSending, mx_EventTile_highlight: this.shouldHighlight(), mx_EventTile_selected: this.props.isSelectedEvent || this.state.contextMenu, mx_EventTile_continuation: isContinuation || eventType === _matrix.EventType.CallInvite || _Call.ElementCall.CALL_EVENT_TYPE.matches(eventType), mx_EventTile_last: this.props.last, mx_EventTile_lastInSection: this.props.lastInSection, mx_EventTile_contextual: this.props.contextual, mx_EventTile_actionBarFocused: this.state.actionBarFocused, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === _matrix.MsgType.Emote, mx_EventTile_noSender: this.props.hideSender, mx_EventTile_clamp: this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList || isRenderingNotification, mx_EventTile_noBubble: noBubbleEvent }); // If the tile is in the Sending state, don't speak the message. const ariaLive = this.props.eventSendStatus !== null ? "off" : undefined; let permalink = "#"; if (this.props.permalinkCreator) { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } // we can't use local echoes as scroll tokens, because their event IDs change. // Local echos have a send "status". const scrollToken = this.props.mxEvent.status ? undefined : this.props.mxEvent.getId(); let avatar = null; let sender = null; let avatarSize; let needsSenderProfile; if (isRenderingNotification) { avatarSize = "24px"; needsSenderProfile = true; } else if (isInfoMessage) { // a small avatar, with no sender profile, for // joins/parts/etc avatarSize = "14px"; needsSenderProfile = false; } else if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Thread && !this.props.continuation) { avatarSize = "32px"; needsSenderProfile = true; } else if (eventType === _matrix.EventType.RoomCreate || isBubbleMessage) { avatarSize = null; needsSenderProfile = false; } else if (this.props.layout == _Layout.Layout.IRC) { avatarSize = "14px"; needsSenderProfile = true; } else if (this.props.continuation && this.context.timelineRenderingType !== _RoomContext.TimelineRenderingType.File || eventType === _matrix.EventType.CallInvite || _Call.ElementCall.CALL_EVENT_TYPE.matches(eventType)) { // no avatar or sender profile for continuation messages and call tiles avatarSize = null; needsSenderProfile = false; } else if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.File) { avatarSize = "20px"; needsSenderProfile = true; } else { avatarSize = "30px"; needsSenderProfile = true; } if (this.props.mxEvent.sender && avatarSize !== null) { let member = null; // set member to receiver (target) if it is a 3PID invite // so that the correct avatar is shown as the text is // `$target accepted the invitation for $email` if (this.props.mxEvent.getContent().third_party_invite) { member = this.props.mxEvent.target; } else { member = this.props.mxEvent.sender; } // In the ThreadsList view we use the entire EventTile as a click target to open the thread instead const viewUserOnClick = !this.props.inhibitInteraction && ![_RoomContext.TimelineRenderingType.ThreadsList, _RoomContext.TimelineRenderingType.Notification].includes(this.context.timelineRenderingType); avatar = /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_avatar" }, /*#__PURE__*/_react.default.createElement(_MemberAvatar.default, { member: member, size: avatarSize, viewUserOnClick: viewUserOnClick, forceHistorical: this.props.mxEvent.getType() === _matrix.EventType.RoomMember })); } if (needsSenderProfile && this.props.hideSender !== true) { if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Room || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Search || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Pinned || this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.Thread) { sender = /*#__PURE__*/_react.default.createElement(_SenderProfile.default, { onClick: this.onSenderProfileClick, mxEvent: this.props.mxEvent }); } else if (this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList) { sender = /*#__PURE__*/_react.default.createElement(_SenderProfile.default, { mxEvent: this.props.mxEvent, withTooltip: true }); } else { sender = /*#__PURE__*/_react.default.createElement(_SenderProfile.default, { mxEvent: this.props.mxEvent }); } } const showMessageActionBar = !isEditing && !this.props.forExport; const actionBar = showMessageActionBar ? /*#__PURE__*/_react.default.createElement(_MessageActionBar.default, { mxEvent: this.props.mxEvent, reactions: this.state.reactions, permalinkCreator: this.props.permalinkCreator, getTile: this.getTile, getReplyChain: this.getReplyChain, onFocusChange: this.onActionBarFocusChange, isQuoteExpanded: isQuoteExpanded, toggleThreadExpanded: () => this.setQuoteExpanded(!isQuoteExpanded), getRelationsForEvent: this.props.getRelationsForEvent }) : undefined; const showTimestamp = this.props.mxEvent.getTs() && !this.props.hideTimestamp && (this.props.alwaysShowTimestamps || this.props.last || this.state.hover || this.state.actionBarFocused || Boolean(this.state.contextMenu)); // Thread panel shows the timestamp of the last reply in that thread let ts = this.context.timelineRenderingType !== _RoomContext.TimelineRenderingType.ThreadsList ? this.props.mxEvent.getTs() : this.state.thread?.replyToEvent?.getTs(); if (typeof ts !== "number") { // Fall back to something we can use ts = this.props.mxEvent.getTs(); } const messageTimestamp = /*#__PURE__*/_react.default.createElement(_MessageTimestamp.default, { showRelative: this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList, showTwelveHour: this.props.isTwelveHour, ts: ts, receivedTs: (0, _LateEventGrouper.getLateEventInfo)(this.props.mxEvent)?.received_ts }); const timestamp = showTimestamp && ts ? messageTimestamp : null; let pinnedMessageBadge; if (_PinningUtils.default.isPinned(_MatrixClientPeg.MatrixClientPeg.safeGet(), this.props.mxEvent)) { pinnedMessageBadge = /*#__PURE__*/_react.default.createElement(_PinnedMessageBadge.PinnedMessageBadge, null); } let reactionsRow; if (!isRedacted) { reactionsRow = /*#__PURE__*/_react.default.createElement(_ReactionsRow.default, { mxEvent: this.props.mxEvent, reactions: this.state.reactions, key: "mx_EventTile_reactionsRow" }); } // If we have reactions or a pinned message badge, we need a footer const hasFooter = Boolean(reactionsRow && this.state.reactions || pinnedMessageBadge); const linkedTimestamp = !this.props.hideTimestamp ? /*#__PURE__*/_react.default.createElement("a", { href: permalink, onClick: this.onPermalinkClicked, "aria-label": (0, _DateUtils.formatTime)(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour), onContextMenu: this.onTimestampContextMenu }, timestamp) : null; const useIRCLayout = this.props.layout === _Layout.Layout.IRC; const groupTimestamp = !useIRCLayout ? linkedTimestamp : null; const ircTimestamp = useIRCLayout ? linkedTimestamp : null; const bubbleTimestamp = this.props.layout === _Layout.Layout.Bubble ? messageTimestamp : undefined; const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); let msgOption; if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { msgOption = /*#__PURE__*/_react.default.createElement(SentReceipt, { messageState: this.props.mxEvent.getAssociatedStatus() }); } else if (this.props.showReadReceipts) { msgOption = /*#__PURE__*/_react.default.createElement(_ReadReceiptGroup.ReadReceiptGroup, { readReceipts: this.props.readReceipts ?? [], readReceiptMap: this.props.readReceiptMap ?? {}, checkUnmounting: this.props.checkUnmounting, suppressAnimation: this.suppressReadReceiptAnimation, isTwelveHour: this.props.isTwelveHour }); } let replyChain; if ((0, _EventTileFactory.haveRendererForEvent)(this.props.mxEvent, _MatrixClientPeg.MatrixClientPeg.safeGet(), this.context.showHiddenEvents) && (0, _Reply.shouldDisplayReply)(this.props.mxEvent)) { replyChain = /*#__PURE__*/_react.default.createElement(_ReplyChain.default, { 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 }); } // Use `getSender()` because searched events might not have a proper `sender`. const isOwnEvent = this.props.mxEvent?.getSender() === _MatrixClientPeg.MatrixClientPeg.safeGet().getUserId(); switch (this.context.timelineRenderingType) { case _RoomContext.TimelineRenderingType.Thread: { return /*#__PURE__*/_react.default.createElement(this.props.as || "li", { "ref": this.ref, "className": classes, "aria-live": ariaLive, "aria-atomic": true, "data-scroll-tokens": scrollToken, "data-has-reply": !!replyChain, "data-layout": this.props.layout, "data-self": isOwnEvent, "data-event-id": this.props.mxEvent.getId(), "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }) }, [/*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_senderDetails", key: "mx_EventTile_senderDetails" }, avatar, sender), /*#__PURE__*/_react.default.createElement("div", { className: lineClasses, key: "mx_EventTile_line", onContextMenu: this.onContextMenu }, this.renderContextMenu(), replyChain, (0, _EventTileFactory.renderTile)(_RoomContext.TimelineRenderingType.Thread, _objectSpread(_objectSpread({}, 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 }), this.context.showHiddenEvents), actionBar, /*#__PURE__*/_react.default.createElement("a", { href: permalink, onClick: this.onPermalinkClicked }, timestamp), msgOption), hasFooter && /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_footer", key: "mx_EventTile_footer" }, (this.props.layout === _Layout.Layout.Group || !isOwnEvent) && pinnedMessageBadge, reactionsRow, this.props.layout === _Layout.Layout.Bubble && isOwnEvent && pinnedMessageBadge)]); } case _RoomContext.TimelineRenderingType.Notification: case _RoomContext.TimelineRenderingType.ThreadsList: { const room = _MatrixClientPeg.MatrixClientPeg.safeGet().getRoom(this.props.mxEvent.getRoomId()); // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers return /*#__PURE__*/_react.default.createElement(this.props.as || "li", { "ref": this.ref, "className": classes, "tabIndex": -1, "aria-live": ariaLive, "aria-atomic": "true", "data-scroll-tokens": scrollToken, "data-layout": this.props.layout, "data-shape": this.context.timelineRenderingType, "data-self": isOwnEvent, "data-has-reply": !!replyChain, "onMouseEnter": () => this.setState({ hover: true }), "onMouseLeave": () => this.setState({ hover: false }), "onClick": ev => { const target = ev.currentTarget; let index = -1; if (target.parentElement) index = Array.from(target.parentElement.children).indexOf(target); switch (this.context.timelineRenderingType) { case _RoomContext.TimelineRenderingType.Notification: this.viewInRoom(ev); break; case _RoomContext.TimelineRenderingType.ThreadsList: _dispatcher.default.dispatch({ action: _actions.Action.ShowThread, rootEvent: this.props.mxEvent, push: true }); _PosthogTrackers.default.trackInteraction("WebThreadsPanelThreadItem", ev, index ?? -1); break; } } }, /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_details" }, sender, isRenderingNotification && room ? /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_truncated" }, " ", (0, _languageHandler._t)("timeline|in_room_name", { room: room.name }, { strong: sub => /*#__PURE__*/_react.default.createElement("strong", null, sub) })) : "", timestamp, /*#__PURE__*/_react.default.createElement(_UnreadNotificationBadge.UnreadNotificationBadge, { room: room || undefined, threadId: this.props.mxEvent.getId(), forceDot: true })), isRenderingNotification && room ? /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_avatar" }, /*#__PURE__*/_react.default.createElement(_RoomAvatar.default, { room: room, size: "28px" })) : avatar, /*#__PURE__*/_react.default.createElement("div", { className: lineClasses, key: "mx_EventTile_line" }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_body" }, this.props.mxEvent.isRedacted() ? /*#__PURE__*/_react.default.createElement(_RedactedBody.default, { mxEvent: this.props.mxEvent }) : this.props.mxEvent.isDecryptionFailure() ? /*#__PURE__*/_react.default.createElement(_DecryptionFailureBody.DecryptionFailureBody, { mxEvent: this.props.mxEvent }) : _MessagePreviewStore.MessagePreviewStore.instance.generatePreviewForEvent(this.props.mxEvent)), this.renderThreadPanelSummary()), this.context.timelineRenderingType === _RoomContext.TimelineRenderingType.ThreadsList && /*#__PURE__*/_react.default.createElement(_EventTileThreadToolbar.EventTileThreadToolbar, { viewInRoom: this.viewInRoom, copyLinkToThread: this.copyLinkToThread }), msgOption)); } case _RoomContext.TimelineRenderingType.File: { return /*#__PURE__*/_react.default.createElement(this.props.as || "li", { "className": classes, "aria-live": ariaLive, "aria-atomic": true, "data-scroll-tokens": scrollToken }, [/*#__PURE__*/_react.default.createElement("a", { className: "mx_EventTile_senderDetailsLink", key: "mx_EventTile_sen