UNPKG

matrix-react-sdk

Version:
1,173 lines (961 loc) 166 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); Object.defineProperty(exports, "__esModule", { value: true }); exports.getHandlerTile = getHandlerTile; exports.haveTileForEvent = haveTileForEvent; exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _react = _interopRequireDefault(require("react")); var _classnames = _interopRequireDefault(require("classnames")); var _event = require("matrix-js-sdk/src/@types/event"); var _event2 = require("matrix-js-sdk/src/models/event"); var _ReplyThread = _interopRequireDefault(require("../elements/ReplyThread")); var _languageHandler = require("../../../languageHandler"); var TextForEvent = _interopRequireWildcard(require("../../../TextForEvent")); var sdk = _interopRequireWildcard(require("../../../index")); var _dispatcher = _interopRequireDefault(require("../../../dispatcher/dispatcher")); var _SettingsStore = _interopRequireDefault(require("../../../settings/SettingsStore")); var _Layout = require("../../../settings/Layout"); var _DateUtils = require("../../../DateUtils"); var _MatrixClientPeg = require("../../../MatrixClientPeg"); var _BanList = require("../../../mjolnir/BanList"); var _MatrixClientContext = _interopRequireDefault(require("../../../contexts/MatrixClientContext")); var _E2EIcon = require("./E2EIcon"); var _units = require("../../../utils/units"); var _WidgetType = require("../../../widgets/WidgetType"); var _RoomAvatar = _interopRequireDefault(require("../avatars/RoomAvatar")); var _WidgetLayoutStore = require("../../../stores/widgets/WidgetLayoutStore"); var _objects = require("../../../utils/objects"); var _replaceableComponent = require("../../../utils/replaceableComponent"); var _Tooltip = _interopRequireDefault(require("../elements/Tooltip")); var _StaticNotificationState = require("../../../stores/notifications/StaticNotificationState"); var _NotificationBadge = _interopRequireDefault(require("./NotificationBadge")); var _dec, _class, _class2, _temp; const eventTileTypes = { [_event.EventType.RoomMessage]: 'messages.MessageEvent', [_event.EventType.Sticker]: 'messages.MessageEvent', [_event.EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion', [_event.EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion', [_event.EventType.CallInvite]: 'messages.TextualEvent', [_event.EventType.CallAnswer]: 'messages.TextualEvent', [_event.EventType.CallHangup]: 'messages.TextualEvent', [_event.EventType.CallReject]: 'messages.TextualEvent' }; const stateEventTileTypes = { [_event.EventType.RoomEncryption]: 'messages.EncryptionEvent', [_event.EventType.RoomCanonicalAlias]: 'messages.TextualEvent', [_event.EventType.RoomCreate]: 'messages.RoomCreate', [_event.EventType.RoomMember]: 'messages.TextualEvent', [_event.EventType.RoomName]: 'messages.TextualEvent', [_event.EventType.RoomAvatar]: 'messages.RoomAvatarEvent', [_event.EventType.RoomThirdPartyInvite]: 'messages.TextualEvent', [_event.EventType.RoomHistoryVisibility]: 'messages.TextualEvent', [_event.EventType.RoomTopic]: 'messages.TextualEvent', [_event.EventType.RoomPowerLevels]: 'messages.TextualEvent', [_event.EventType.RoomPinnedEvents]: 'messages.TextualEvent', [_event.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', [_WidgetLayoutStore.WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent', [_event.EventType.RoomTombstone]: 'messages.TextualEvent', [_event.EventType.RoomJoinRules]: 'messages.TextualEvent', [_event.EventType.RoomGuestAccess]: 'messages.TextualEvent', 'm.room.related_groups': 'messages.TextualEvent' // legacy communities flair }; const stateEventSingular = new Set([_event.EventType.RoomEncryption, _event.EventType.RoomCanonicalAlias, _event.EventType.RoomCreate, _event.EventType.RoomName, _event.EventType.RoomAvatar, _event.EventType.RoomHistoryVisibility, _event.EventType.RoomTopic, _event.EventType.RoomPowerLevels, _event.EventType.RoomPinnedEvents, _event.EventType.RoomServerAcl, _WidgetLayoutStore.WIDGET_LAYOUT_EVENT_TYPE, _event.EventType.RoomTombstone, _event.EventType.RoomJoinRules, _event.EventType.RoomGuestAccess, 'm.room.related_groups']); // Add all the Mjolnir stuff to the renderer for (const evType of _BanList.ALL_RULE_TYPES) { stateEventTileTypes[evType] = 'messages.TextualEvent'; } function getHandlerTile(ev) { const type = ev.getType(); // don't show verification requests we're not involved in, // not even when showing hidden events if (type === "m.room.message") { const content = ev.getContent(); if (content && content.msgtype === "m.key.verification.request") { const client = _MatrixClientPeg.MatrixClientPeg.get(); const me = client && client.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 === "m.key.verification.done") { const client = _MatrixClientPeg.MatrixClientPeg.get(); const me = client && client.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 === "m.key.verification.cancel" || type === "m.key.verification.done") { const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion"); if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) { 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.WidgetType.JITSI.matches(type)) { return "messages.MJitsiWidgetEvent"; } } if (ev.isState()) { if (stateEventSingular.has(type) && ev.getStateKey() !== "") return undefined; return stateEventTileTypes[type]; } return eventTileTypes[type]; } const MAX_READ_AVATARS = 5; // Our component structure for EventTiles on the timeline is: // // .-EventTile------------------------------------------------. // | MemberAvatar (SenderProfile) TimeStamp | // | .-{Message,Textual}Event---------------. Read Avatars | // | | .-MFooBody-------------------. | | // | | | (only if MessageEvent) | | | // | | '----------------------------' | | // | '--------------------------------------' | // '----------------------------------------------------------' let EventTile = (_dec = (0, _replaceableComponent.replaceableComponent)("views.rooms.EventTile"), _dec(_class = (_temp = _class2 = class EventTile extends _react.default.Component /*:: <IProps, IState>*/ { 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__*/_react.default.createRef()); (0, _defineProperty2.default)(this, "replyThread", /*#__PURE__*/_react.default.createRef()); (0, _defineProperty2.default)(this, "onRoomReceipt", (ev, room) => { // ignore events for other rooms const tileRoom = _MatrixClientPeg.MatrixClientPeg.get().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) { this.context.removeListener("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = false; } }); }); (0, _defineProperty2.default)(this, "onDecrypted", () => { // we need to re-verify the sending device. // (we call onHeightChanged in verifyEvent to handle the case where decryption // has caused a change in size of the event tile) this.verifyEvent(this.props.mxEvent); this.forceUpdate(); }); (0, _defineProperty2.default)(this, "onDeviceVerificationChanged", (userId, device) => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(this.props.mxEvent); } }); (0, _defineProperty2.default)(this, "onUserVerificationChanged", (userId, _trustStatus) => { if (userId === this.props.mxEvent.getSender()) { this.verifyEvent(this.props.mxEvent); } }); (0, _defineProperty2.default)(this, "toggleAllReadAvatars", () => { this.setState({ allReadAvatars: !this.state.allReadAvatars }); }); (0, _defineProperty2.default)(this, "onSenderProfileClick", event => { const mxEvent = this.props.mxEvent; _dispatcher.default.dispatch({ action: 'insert_mention', user_id: mxEvent.getSender() }); }); (0, _defineProperty2.default)(this, "onRequestKeysClick", () => { this.setState({ // Indicate in the UI that the keys have been requested (this is expected to // be reset if the component is mounted in the future). previouslyRequestedKeys: true }); // Cancel any outgoing key request for this event and resend it. If a response // is received for the request with the required keys, the event could be // decrypted successfully. this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent); }); (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: 'view_room', event_id: this.props.mxEvent.getId(), highlighted: true, room_id: this.props.mxEvent.getRoomId() }); }); (0, _defineProperty2.default)(this, "onActionBarFocusChange", focused => { this.setState({ actionBarFocused: focused }); }); (0, _defineProperty2.default)(this, "getTile", () => this.tile.current); (0, _defineProperty2.default)(this, "getReplyThread", () => this.replyThread.current); (0, _defineProperty2.default)(this, "getReactions", () => { if (!this.props.showReactions || !this.props.getRelationsForEvent) { return null; } const eventId = this.props.mxEvent.getId(); if (!eventId) { // XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120 console.error("EventTile attempted to get relations for an event without an ID"); // Use event's special `toJSON` method to log key data. console.log(JSON.stringify(this.props.mxEvent, null, 4)); console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120"); } return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); }); (0, _defineProperty2.default)(this, "onReactionsCreated", (relationType, eventType) => { if (relationType !== "m.annotation" || eventType !== "m.reaction") { return; } this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated); this.setState({ reactions: this.getReactions() }); }); this.state = { // Whether the action bar is focused. actionBarFocused: false, // Whether all read receipts are being displayed. If not, only display // a truncation of them. allReadAvatars: false, // Whether the event's sender has been verified. verified: null, // Whether onRequestKeysClick has been called since mounting. previouslyRequestedKeys: false, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions() }; // 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 = this.context.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.get().getUserId(); if (this.props.mxEvent.getSender() !== myUserId) return false; // Finally, 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. const simpleSendableEvents = [_event.EventType.Sticker, _event.EventType.RoomMessage, _event.EventType.RoomMessageEncrypted]; if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false; // Default case return true; } 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 !== '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.get().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 === 'sent') return false; // Default to showing - there's no other event properties/behaviours we care about at // this point. return true; } // TODO: [REACT-WARNING] Move into constructor // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { this.verifyEvent(this.props.mxEvent); } componentDidMount() { this.suppressReadReceiptAnimation = false; const client = this.context; client.on("deviceVerificationChanged", this.onDeviceVerificationChanged); client.on("userTrustStatusChanged", this.onUserVerificationChanged); this.props.mxEvent.on("Event.decrypted", this.onDecrypted); if (this.props.showReactions) { this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated); } if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { client.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event // eslint-disable-next-line camelcase UNSAFE_componentWillReceiveProps(nextProps) { // re-check the sender verification as outgoing events progress through // the send process. if (nextProps.eventSendStatus !== this.props.eventSendStatus) { this.verifyEvent(nextProps.mxEvent); } } shouldComponentUpdate(nextProps, nextState) { if ((0, _objects.objectHasDiff)(this.state, nextState)) { return true; } return !this.propsEqual(this.props, nextProps); } componentWillUnmount() { const client = this.context; client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); client.removeListener("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = false; this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted); if (this.props.showReactions) { this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated); } } componentDidUpdate(prevProps, prevState, snapshot) { // If we're not listening for receipts and expect to be, register a listener. if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) { this.context.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } } async verifyEvent(mxEvent) { if (!mxEvent.isEncrypted()) { return; } const encryptionInfo = this.context.getEventEncryptionInfo(mxEvent); const senderId = mxEvent.getSender(); const userTrust = this.context.checkUserTrust(senderId); if (encryptionInfo.mismatchedSender) { // something definitely wrong is going on here this.setState({ verified: _E2EIcon.E2E_STATE.WARNING }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } if (!userTrust.isCrossSigningVerified()) { // user is not verified, so default to everything is normal this.setState({ verified: _E2EIcon.E2E_STATE.NORMAL }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } const eventSenderTrust = encryptionInfo.sender && this.context.checkDeviceTrust(senderId, encryptionInfo.sender.deviceId); if (!eventSenderTrust) { this.setState({ verified: _E2EIcon.E2E_STATE.UNKNOWN }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } if (!eventSenderTrust.isVerified()) { this.setState({ verified: _E2EIcon.E2E_STATE.WARNING }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } if (!encryptionInfo.authenticated) { this.setState({ verified: _E2EIcon.E2E_STATE.UNAUTHENTICATED }, this.props.onHeightChanged); // Decryption may have caused a change in size return; } this.setState({ verified: _E2EIcon.E2E_STATE.VERIFIED }, this.props.onHeightChanged); // Decryption may have caused a change in size } 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; } shouldHighlight() { const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent); if (!actions || !actions.tweaks) { return false; } // don't show self-highlights from another of our clients if (this.props.mxEvent.getSender() === this.context.credentials.userId) { return false; } return actions.tweaks.highlight; } getReadAvatars() { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { return /*#__PURE__*/_react.default.createElement(SentReceipt, { messageState: this.props.mxEvent.getAssociatedStatus() }); } // return early if there are no read receipts if (!this.props.readReceipts || this.props.readReceipts.length === 0) { return /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_readAvatars" }); } const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker'); const avatars = []; const receiptOffset = 15; let left = 0; const receipts = this.props.readReceipts || []; for (let i = 0; i < receipts.length; ++i) { const receipt = receipts[i]; let hidden = true; if (i < MAX_READ_AVATARS || this.state.allReadAvatars) { hidden = false; } // TODO: we keep the extra read avatars in the dom to make animation simpler // we could optimise this to reduce the dom size. // If hidden, set offset equal to the offset of the final visible avatar or // else set it proportional to index left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; const userId = receipt.userId; let readReceiptInfo; if (this.props.readReceiptMap) { readReceiptInfo = this.props.readReceiptMap[userId]; if (!readReceiptInfo) { readReceiptInfo = {}; this.props.readReceiptMap[userId] = readReceiptInfo; } } // add to the start so the most recent is on the end (ie. ends up rightmost) avatars.unshift( /*#__PURE__*/_react.default.createElement(ReadReceiptMarker, { key: userId, member: receipt.roomMember, fallbackUserId: userId, leftOffset: left, hidden: hidden, readReceiptInfo: readReceiptInfo, checkUnmounting: this.props.checkUnmounting, suppressAnimation: this.suppressReadReceiptAnimation, onClick: this.toggleAllReadAvatars, timestamp: receipt.ts, showTwelveHour: this.props.isTwelveHour })); } let remText; if (!this.state.allReadAvatars) { const remainder = receipts.length - MAX_READ_AVATARS; if (remainder > 0) { remText = /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_readAvatarRemainder", onClick: this.toggleAllReadAvatars, style: { right: "calc(" + (0, _units.toRem)(-left) + " + " + receiptOffset + "px)" } }, remainder, "+"); } } return /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_readAvatars" }, remText, avatars); } renderE2EPadlock() { const ev = this.props.mxEvent; // event could not be decrypted if (ev.getContent().msgtype === 'm.bad.encrypted') { return /*#__PURE__*/_react.default.createElement(E2ePadlockUndecryptable, null); } // event is encrypted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted()) { if (this.state.verified === _E2EIcon.E2E_STATE.NORMAL) { return; // no icon if we've not even cross-signed the user } else if (this.state.verified === _E2EIcon.E2E_STATE.VERIFIED) { return; // no icon for verified } else if (this.state.verified === _E2EIcon.E2E_STATE.UNAUTHENTICATED) { return /*#__PURE__*/_react.default.createElement(E2ePadlockUnauthenticated, null); } else if (this.state.verified === _E2EIcon.E2E_STATE.UNKNOWN) { return /*#__PURE__*/_react.default.createElement(E2ePadlockUnknown, null); } else { return /*#__PURE__*/_react.default.createElement(E2ePadlockUnverified, null); } } if (this.context.isRoomEncrypted(ev.getRoomId())) { // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === _event2.EventStatus.ENCRYPTING) { return; } if (ev.status === _event2.EventStatus.NOT_SENT) { return; } if (ev.isState()) { return; // we expect this to be unencrypted } // if the event is not encrypted, but it's an e2e room, show the open padlock return /*#__PURE__*/_react.default.createElement(E2ePadlockUnencrypted, null); } // no padlock needed return null; } render() { const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp'); const SenderProfile = sdk.getComponent('messages.SenderProfile'); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); //console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview); const content = this.props.mxEvent.getContent(); const msgtype = content.msgtype; const eventType = this.props.mxEvent.getType(); let tileHandler = getHandlerTile(this.props.mxEvent); // Info messages are basically information about commands processed on a room const isBubbleMessage = eventType.startsWith("m.key.verification") || eventType === _event.EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification") || eventType === _event.EventType.RoomCreate || eventType === _event.EventType.RoomEncryption || tileHandler === "messages.MJitsiWidgetEvent"; let isInfoMessage = !isBubbleMessage && eventType !== _event.EventType.RoomMessage && eventType !== _event.EventType.Sticker && eventType !== _event.EventType.RoomCreate; // If we're showing hidden events in the timeline, we should use the // 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 (_SettingsStore.default.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) { tileHandler = "messages.ViewSourceEvent"; // Reuse info message avatar and sender profile styling isInfoMessage = true; } // This shouldn't happen: the caller should check we support this type // before trying to instantiate us if (!tileHandler) { const { mxEvent } = this.props; console.warn(`Event type not supported: type:${mxEvent.getType()} 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)('This event could not be displayed'))); } const EventTileType = sdk.getComponent(tileHandler); const isSending = ['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1; const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); const isEditing = !!this.props.editState; const classes = (0, _classnames.default)({ mx_EventTile_bubbleContainer: isBubbleMessage, 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.props.tileShape === 'notif' ? false : this.shouldHighlight(), mx_EventTile_selected: this.props.isSelectedEvent, mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation, 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_verified: !isBubbleMessage && this.state.verified === _E2EIcon.E2E_STATE.VERIFIED, mx_EventTile_unverified: !isBubbleMessage && this.state.verified === _E2EIcon.E2E_STATE.WARNING, mx_EventTile_unknown: !isBubbleMessage && this.state.verified === _E2EIcon.E2E_STATE.UNKNOWN, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote' }); // 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()); } let avatar; let sender; let avatarSize; let needsSenderProfile; if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; } else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) { avatarSize = 0; needsSenderProfile = false; } else if (isInfoMessage) { // a small avatar, with no sender profile, for // joins/parts/etc avatarSize = 14; needsSenderProfile = false; } else if (this.props.layout == _Layout.Layout.IRC) { avatarSize = 14; needsSenderProfile = true; } else if (this.props.continuation && this.props.tileShape !== "file_grid") { // no avatar or sender profile for continuation messages avatarSize = 0; needsSenderProfile = false; } else { avatarSize = 30; needsSenderProfile = true; } if (this.props.mxEvent.sender && avatarSize) { let member; // 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; } avatar = /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_avatar" }, /*#__PURE__*/_react.default.createElement(MemberAvatar, { member: member, width: avatarSize, height: avatarSize, viewUserOnClick: true })); } if (needsSenderProfile) { if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') { sender = /*#__PURE__*/_react.default.createElement(SenderProfile, { onClick: this.onSenderProfileClick, mxEvent: this.props.mxEvent, enableFlair: this.props.enableFlair }); } else { sender = /*#__PURE__*/_react.default.createElement(SenderProfile, { mxEvent: this.props.mxEvent, enableFlair: this.props.enableFlair }); } } const MessageActionBar = sdk.getComponent('messages.MessageActionBar'); const actionBar = !isEditing ? /*#__PURE__*/_react.default.createElement(MessageActionBar, { mxEvent: this.props.mxEvent, reactions: this.state.reactions, permalinkCreator: this.props.permalinkCreator, getTile: this.getTile, getReplyThread: this.getReplyThread, onFocusChange: this.onActionBarFocusChange }) : undefined; const timestamp = this.props.mxEvent.getTs() ? /*#__PURE__*/_react.default.createElement(MessageTimestamp, { showTwelveHour: this.props.isTwelveHour, ts: this.props.mxEvent.getTs() }) : null; const keyRequestHelpText = /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_keyRequestInfo_tooltip_contents" }, /*#__PURE__*/_react.default.createElement("p", null, this.state.previouslyRequestedKeys ? (0, _languageHandler._t)('Your key share request has been sent - please check your other sessions ' + 'for key share requests.') : (0, _languageHandler._t)('Key share requests are sent to your other sessions automatically. If you ' + 'rejected or dismissed the key share request on your other sessions, click ' + 'here to request the keys for this session again.')), /*#__PURE__*/_react.default.createElement("p", null, (0, _languageHandler._t)('If your other sessions do not have the key for this message you will not ' + 'be able to decrypt them.'))); const keyRequestInfoContent = this.state.previouslyRequestedKeys ? (0, _languageHandler._t)('Key request sent.') : (0, _languageHandler._t)('<requestLink>Re-request encryption keys</requestLink> from your other sessions.', {}, { 'requestLink': sub => /*#__PURE__*/_react.default.createElement("a", { onClick: this.onRequestKeysClick }, sub) }); const TooltipButton = sdk.getComponent('elements.TooltipButton'); const keyRequestInfo = isEncryptionFailure && !isRedacted ? /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_keyRequestInfo" }, /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_keyRequestInfo_text" }, keyRequestInfoContent), /*#__PURE__*/_react.default.createElement(TooltipButton, { helpText: keyRequestHelpText })) : null; let reactionsRow; if (!isRedacted) { const ReactionsRow = sdk.getComponent('messages.ReactionsRow'); reactionsRow = /*#__PURE__*/_react.default.createElement(ReactionsRow, { mxEvent: this.props.mxEvent, reactions: this.state.reactions }); } const linkedTimestamp = /*#__PURE__*/_react.default.createElement("a", { href: permalink, onClick: this.onPermalinkClicked, "aria-label": (0, _DateUtils.formatTime)(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour) }, timestamp); const useIRCLayout = this.props.layout == _Layout.Layout.IRC; const groupTimestamp = !useIRCLayout ? linkedTimestamp : null; const ircTimestamp = useIRCLayout ? linkedTimestamp : null; const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); let msgOption; if (this.props.showReadReceipts) { const readAvatars = this.getReadAvatars(); msgOption = /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_msgOption" }, readAvatars); } switch (this.props.tileShape) { case 'notif': { const room = this.context.getRoom(this.props.mxEvent.getRoomId()); return /*#__PURE__*/_react.default.createElement("div", { className: classes, "aria-live": ariaLive, "aria-atomic": "true" }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_roomName" }, /*#__PURE__*/_react.default.createElement(_RoomAvatar.default, { room: room, width: 28, height: 28 }), /*#__PURE__*/_react.default.createElement("a", { href: permalink, onClick: this.onPermalinkClicked }, room ? room.name : '')), /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_senderDetails" }, avatar, /*#__PURE__*/_react.default.createElement("a", { href: permalink, onClick: this.onPermalinkClicked }, sender, timestamp)), /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_line" }, /*#__PURE__*/_react.default.createElement(EventTileType, { ref: this.tile, mxEvent: this.props.mxEvent, highlights: this.props.highlights, highlightLink: this.props.highlightLink, showUrlPreview: this.props.showUrlPreview, onHeightChanged: this.props.onHeightChanged }))); } case 'file_grid': { return /*#__PURE__*/_react.default.createElement("div", { className: classes, "aria-live": ariaLive, "aria-atomic": "true" }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_line" }, /*#__PURE__*/_react.default.createElement(EventTileType, { ref: this.tile, mxEvent: this.props.mxEvent, highlights: this.props.highlights, highlightLink: this.props.highlightLink, showUrlPreview: this.props.showUrlPreview, tileShape: this.props.tileShape, onHeightChanged: this.props.onHeightChanged })), /*#__PURE__*/_react.default.createElement("a", { className: "mx_EventTile_senderDetailsLink", href: permalink, onClick: this.onPermalinkClicked }, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_senderDetails" }, sender, timestamp))); } case 'reply': case 'reply_preview': { let thread; if (this.props.tileShape === 'reply_preview') { thread = _ReplyThread.default.makeThread(this.props.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread); } return /*#__PURE__*/_react.default.createElement("div", { className: classes, "aria-live": ariaLive, "aria-atomic": "true" }, ircTimestamp, avatar, sender, ircPadlock, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_reply" }, groupTimestamp, groupPadlock, thread, /*#__PURE__*/_react.default.createElement(EventTileType, { ref: this.tile, mxEvent: this.props.mxEvent, highlights: this.props.highlights, highlightLink: this.props.highlightLink, onHeightChanged: this.props.onHeightChanged, replacingEventId: this.props.replacingEventId, showUrlPreview: false }))); } default: { const thread = _ReplyThread.default.makeThread(this.props.mxEvent, this.props.onHeightChanged, this.props.permalinkCreator, this.replyThread, this.props.layout); // 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("div", { className: classes, tabIndex: -1, "aria-live": ariaLive, "aria-atomic": "true" }, ircTimestamp, sender, ircPadlock, /*#__PURE__*/_react.default.createElement("div", { className: "mx_EventTile_line" }, groupTimestamp, groupPadlock, thread, /*#__PURE__*/_react.default.createElement(EventTileType, { ref: this.tile, mxEvent: this.props.mxEvent, 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 }), keyRequestInfo, reactionsRow, actionBar), msgOption, avatar); } } } }, (0, _defineProperty2.default)(_class2, "defaultProps", { // no-op function because onHeightChanged is optional yet some sub-components assume its existence onHeightChanged: function () {} }), (0, _defineProperty2.default)(_class2, "contextType", _MatrixClientContext.default), _temp)) || _class); exports.default = EventTile; // XXX this'll eventually be dynamic based on the fields once we have extensible event types const messageTypes = ['m.room.message', 'm.sticker']; function isMessageEvent(ev) { return messageTypes.includes(ev.getType()); } function haveTileForEvent(e) { // Only messages have a tile (black-rectangle) if redacted if (e.isRedacted() && !isMessageEvent(e)) return false; // No tile for replacement events since they update the original tile if (e.isRelation("m.replace")) return false; const handler = getHandlerTile(e); if (handler === undefined) return false; if (handler === 'messages.TextualEvent') { return TextForEvent.textForEvent(e) !== ''; } else if (handler === 'messages.RoomCreate') { return Boolean(e.getContent()['predecessor']); } else { return true; } } function E2ePadlockUndecryptable(props) { return /*#__PURE__*/_react.default.createElement(E2ePadlock, (0, _extends2.default)({ title: (0, _languageHandler._t)("This message cannot be decrypted"), icon: "undecryptable" }, props)); } function E2ePadlockUnverified(props) { return /*#__PURE__*/_react.default.createElement(E2ePadlock, (0, _extends2.default)({ title: (0, _languageHandler._t)("Encrypted by an unverified session"), icon: "unverified" }, props)); } function E2ePadlockUnencrypted(props) { return /*#__PURE__*/_react.default.createElement(E2ePadlock, (0, _extends2.default)({ title: (0, _languageHandler._t)("Unencrypted"), icon: "unencrypted" }, props)); } function E2ePadlockUnknown(props) { return /*#__PURE__*/_react.default.createElement(E2ePadlock, (0, _extends2.default)({ title: (0, _languageHandler._t)("Encrypted by a deleted session"), icon: "unknown" }, props)); } function E2ePadlockUnauthenticated(props) { return /*#__PURE__*/_react.default.createElement(E2ePadlock, (0, _extends2.default)({ title: (0, _languageHandler._t)("The authenticity of this encrypted message can't be guaranteed on this device."), icon: "unauthenticated" }, props)); } class E2ePadlock extends _react.default.Component /*:: <IE2ePadlockProps, IE2ePadlockState>*/ { constructor(props) { super(props); (0, _defineProperty2.default)(this, "onHoverStart", () => { this.setState({ hover: true }); }); (0, _defineProperty2.default)(this, "onHoverEnd", () => { this.setState({ hover: false }); }); this.state = { hover: false }; } render() { let tooltip = null; if (this.state.hover) { tooltip = /*#__PURE__*/_react.default.createElement(_Tooltip.default, { className: "mx_EventTile_e2eIcon_tooltip", label: this.props.title }); } const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`; return /*#__PURE__*/_react.default.createElement("div", { className: classes, onMouseEnter: this.onHoverStart, onMouseLeave: this.onHoverEnd }, tooltip); } } class SentReceipt extends _react.default.PureComponent /*:: <ISentReceiptProps, ISentReceiptState>*/ { constructor(props) { super(props); (0, _defineProperty2.default)(this, "onHoverStart", () => { this.setState({ hover: true }); }); (0, _defineProperty2.default)(this, "onHoverEnd", () => { this.setState({ hover: false }); }); this.state = { hover: false }; } render() { const isSent = !this.props.messageState || this.props.messageState === 'sent'; const isFailed = this.props.messageState === 'not_sent'; const receiptClasses = (0, _classnames.default)({ 'mx_EventTile_receiptSent': isSent, 'mx_EventTile_receiptSending': !isSent && !isFailed }); let nonCssBadge = null; if (isFailed) { nonCssBadge = /*#__PURE__*/_react.default.createElement(_NotificationBadge.default, { notification: _StaticNotificationState.StaticNotificationState.RED_EXCLAMATION }); } let tooltip = null; if (this.state.hover) { let label = (0, _languageHandler._t)("Sending your message..."); if (this.props.messageState === 'encrypting') { label = (0, _languageHandler._t)("Encrypting your message..."); } else if (isSent) { label = (0, _languageHandler._t)("Your message was sent"); } else if (isFailed) { label = (0, _languageHandler._t)("Failed to send"); } // The yOffset is somewhat arbitrary - it just brings the tooltip down to be more associated // with the read receipt. tooltip = /*#__PURE__*/_react.default.createElement(_Tooltip.default, { className: "mx_EventTile_readAvatars_receiptTooltip", label: label, yOffset: 20 }); } return /*#__PURE__*/_react.default.createElement("span", { className: "mx_EventTile_readAvatars" }, /*#__PURE__*/_react.default.createElement("span", { className: receiptClasses, onMouseEnter: this.onHoverStart, onMouseLeave: this.onHoverEnd }, nonCssBadge, tooltip)); } } //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9jb21wb25lbnRzL3ZpZXdzL3Jvb21zL0V2ZW50VGlsZS50c3giXSwibmFtZXMiOlsiZXZlbnRUaWxlVHlwZXMiLCJFdmVudFR5cGUiLCJSb29tTWVzc2FnZSIsIlN0aWNrZXIiLCJLZXlWZXJpZmljYXRpb25DYW5jZWwiLCJLZXlWZXJpZmljYXRpb25Eb25lIiwiQ2FsbEludml0ZSIsIkNhbGxBbnN3ZXIiLCJDYWxsSGFuZ3VwIiwiQ2FsbFJlamVjdCIsInN0YXRlRXZlbnRUaWxlVHlwZXMiLCJSb29tRW5jcnlwdGlvbiIsIlJvb21DYW5vbmljYWxBbGlhcyIsIlJvb21DcmVhdGUiLCJSb29tTWVtYmVyIiwiUm9vbU5hbWUiLCJSb29tQXZhdGFyIiwiUm9vbVRoaXJkUGFydHlJbnZpdGUiLCJSb29tSGlzdG9yeVZpc2liaWxpdHkiLCJSb29tVG9waWMiLCJSb29tUG93ZXJMZXZlbHMiLCJSb29tUGlubmVkRXZlbnRzIiwiUm9vbVNlcnZlckFjbCIsIldJREdFVF9MQVlPVVRfRVZFTlRfVFlQRSIsIlJvb21Ub21ic3RvbmUiLCJSb29tSm9pblJ1bGVzIiwiUm9vbUd1ZXN0QWNjZXNzIiwic3RhdGVFdmVudFNpbmd1bGFyIiwiU2V0IiwiZXZUeXBlIiwiQUxMX1JVTEVfVFlQRVMiLCJnZXRIYW5kbGVyVGlsZSIsImV2IiwidHlwZSIsImdldFR5cGUiLCJjb250ZW50IiwiZ2V0Q29udGVudCIsIm1zZ3R5cGUiLCJjbGllbnQiLCJNYXRyaXhDbGllbnRQZWciLCJnZXQiLCJtZSIsImdldFVzZXJJZCIsImdldFNlbmRlciIsInRvIiwidW5kZWZpbmVkIiwiTUtleVZlcmlmaWNhdGlvbkNvbmNsdXNpb24iLCJzZGsiLCJnZXRDb21wb25lbnQiLCJwcm90b3R5cGUiLCJfc2hvdWxkUmVuZGVyIiwiY2FsbCIsInJlcXVlc3QiLCJnZXRQcmV2Q29udGVudCIsIldpZGdldFR5cGUiLCJKSVRTSSIsIm1hdGNoZXMiLCJpc1N0YXRlIiwiaGFzIiwiZ2V0U3RhdGVLZXkiLCJNQVhfUkVBRF9BVkFUQVJTIiwiRXZlbnRUaWxlIiwiUmVhY3QiLCJDb21wb25lbnQiLCJjb25zdHJ1Y3RvciIsInByb3BzIiwiY29udGV4dCIsImNyZWF0ZVJlZiIsInJvb20iLCJ0aWxlUm9vbSIsImdldFJvb20iLCJteEV2ZW50IiwiZ2V0Um9vbUlkIiwic2hvdWxkU2hvd1NlbnRSZWNlaXB0Iiwic2hvdWxkU2hvd1NlbmRpbmdSZWNlaXB0IiwiaXNMaXN0ZW5pbmdGb3JSZWNlaXB0cyIsImZvcmNlVXBkYXRlIiwicmVtb3ZlTGlzdGVuZXIiLCJvblJvb21SZWNlaXB0IiwidmVyaWZ5RXZlbnQiLCJ1c2VySWQiLCJkZXZpY2UiLCJfdHJ1c3RTdGF0dXMiLCJzZXRTdGF0ZSIsImFsbFJlYWRBdmF0YXJzIiwic3RhdGUiLCJldmVudCIsImRpcyIsImRpc3BhdGNoIiwiYWN0aW9uIiwidXNlcl9pZCIsInByZXZpb3VzbHlSZXF1ZXN0ZWRLZXlzIiwiY2FuY2VsQW5kUmVzZW5kRXZlbnRSb29tS2V5UmVxdWVzdCIsImUiLCJwcmV2ZW50RGVmYXVsdCIsImV2ZW50X2lkIiwiZ2V0SWQiLCJoaWdobGlnaHRlZCIsInJvb21faWQiLCJmb2N1c2VkIiwiYWN0aW9uQmFyRm9jdXNlZCIsInRpbGUiLCJjdXJyZW50IiwicmVwbHlUaHJlYWQiLCJzaG93UmVhY3Rpb25zIiwiZ2V0UmVsYXRpb25zRm9yRXZlbnQiLCJldmVudElkIiwiY29uc29sZSIsImVycm9yIiwibG9nIiwiSlNPTiIsInN0cmluZ2lmeSIsInRyYWNlIiwicmVsYXRpb25UeXBlIiwiZXZlbnRUeXBlIiwib25SZWFjdGlvbnNDcmVhdGVkIiwicmVhY3Rpb25zIiwiZ2V0UmVhY3Rpb25zIiwidmVyaWZpZWQiLCJzdXBwcmVzc1JlYWRSZWNlaXB0QW5pbWF0aW9uIiwiaXNFbGlnaWJsZUZvclNwZWNpYWxSZWNlaXB0IiwicmVhZFJlY2VpcHRzIiwibGVuZ3RoIiwibXlVc2VySWQiLCJzaW1wbGVTZW5kYWJsZUV2ZW50cyIsIlJvb21NZXNzYWdlRW5jcnlwdGVkIiwiaW5jbHVkZXMiLCJsYXN0U3VjY2Vzc2Z1bCIsImV2ZW50U2VuZFN0YXR1cyIsInJlY2VpcHRzIiwic29tZSIsInIiLCJVTlNBRkVfY29tcG9uZW50V2lsbE1vdW50IiwiY29tcG9uZW50RGlkTW91bnQiLCJvbiIsIm9uRGV2aWNlVmVyaWZpY2F0aW9uQ2hhbmdlZCIsIm9uVXNlclZlcmlmaWNhdGlvbkNoYW5nZWQiLCJvbkRlY3J5cHRlZCIsIlVOU0FGRV9jb21wb25lbnRXaWxsUmVjZWl2ZVByb3BzIiwibmV4dFByb3BzIiwic2hvdWxkQ29tcG9uZW50VXBkYXRlIiwibmV4dFN0YXRlIiwicHJvcHNFcXVhbCIsImNvbXBvbmVudFdpbGxVbm1vdW50IiwiY29tcG9uZW50RGlkVXBkYXRlIiwicHJldlByb3BzIiwicHJldlN0YXRlIiwic25hcHNob3QiLCJpc0VuY3J5cHRlZCIsImVuY3J5cHRpb25JbmZvIiwiZ2V0RXZlbnRFbmNyeXB0aW9uSW5mbyIsInNlbmRlcklkIiwidXNlclRydXN0IiwiY2hlY2tVc2VyVHJ1c3QiLCJtaXNtYXRjaGVkU2VuZGVyIiwiRTJFX1NUQVRFIiwiV0FSTklORyIsIm9uSGVpZ2h0Q2hhbmdlZCIsImlzQ3Jvc3NTaWduaW5nVmVyaWZpZWQiLCJOT1JNQUwiLCJldmVudFNlbmRlclRydXN0Iiwic2VuZGVyIiwiY2hlY2tEZXZpY2VUcnVzdCIsImRldmljZUlkIiwiVU5LTk9XTiIsImlzVmVyaWZpZWQiLCJhdXRoZW50aWNhdGVkIiw