matrix-react-sdk
Version:
SDK for matrix.org using React
1,004 lines (972 loc) • 217 kB
JavaScript
"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