2015-11-27 18:02:32 +03:00
|
|
|
/*
|
2021-04-06 14:26:50 +03:00
|
|
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
2019-07-31 14:19:29 +03:00
|
|
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
2015-11-27 18:02:32 +03:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2021-04-06 14:26:50 +03:00
|
|
|
import React from 'react';
|
2019-12-21 00:41:07 +03:00
|
|
|
import classNames from "classnames";
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
|
|
|
import { EventStatus, MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
|
|
import { Relations } from "matrix-js-sdk/src/models/relations";
|
|
|
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
2021-04-13 11:34:37 +03:00
|
|
|
|
|
|
|
import ReplyThread from "../elements/ReplyThread";
|
2021-03-05 06:10:47 +03:00
|
|
|
import { _t } from '../../../languageHandler';
|
2019-12-20 04:19:56 +03:00
|
|
|
import * as TextForEvent from "../../../TextForEvent";
|
2019-12-21 00:41:07 +03:00
|
|
|
import * as sdk from "../../../index";
|
2020-05-14 05:41:41 +03:00
|
|
|
import dis from '../../../dispatcher/dispatcher';
|
2018-04-30 19:22:16 +03:00
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
2021-04-06 14:26:50 +03:00
|
|
|
import {Layout} from "../../../settings/Layout";
|
2019-11-01 13:50:58 +03:00
|
|
|
import {formatTime} from "../../../DateUtils";
|
2019-12-21 00:13:46 +03:00
|
|
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
2019-12-10 06:36:00 +03:00
|
|
|
import {ALL_RULE_TYPES} from "../../../mjolnir/BanList";
|
2019-12-17 20:26:12 +03:00
|
|
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
2020-01-22 19:54:31 +03:00
|
|
|
import {E2E_STATE} from "./E2EIcon";
|
2020-05-14 00:08:29 +03:00
|
|
|
import {toRem} from "../../../utils/units";
|
2020-09-16 21:38:47 +03:00
|
|
|
import {WidgetType} from "../../../widgets/WidgetType";
|
2020-09-17 18:23:06 +03:00
|
|
|
import RoomAvatar from "../avatars/RoomAvatar";
|
2021-01-19 05:31:11 +03:00
|
|
|
import {WIDGET_LAYOUT_EVENT_TYPE} from "../../../stores/widgets/WidgetLayoutStore";
|
2021-02-19 03:00:10 +03:00
|
|
|
import {objectHasDiff} from "../../../utils/objects";
|
2021-03-09 06:12:00 +03:00
|
|
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
2021-03-10 07:13:23 +03:00
|
|
|
import Tooltip from "../elements/Tooltip";
|
2021-04-06 14:26:50 +03:00
|
|
|
import { EditorStateTransfer } from "../../../utils/EditorStateTransfer";
|
|
|
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
2021-04-22 01:16:05 +03:00
|
|
|
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
|
|
|
import NotificationBadge from "./NotificationBadge";
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const eventTileTypes = {
|
2021-04-13 11:34:37 +03:00
|
|
|
[EventType.RoomMessage]: 'messages.MessageEvent',
|
|
|
|
[EventType.Sticker]: 'messages.MessageEvent',
|
|
|
|
[EventType.KeyVerificationCancel]: 'messages.MKeyVerificationConclusion',
|
|
|
|
[EventType.KeyVerificationDone]: 'messages.MKeyVerificationConclusion',
|
|
|
|
[EventType.CallInvite]: 'messages.TextualEvent',
|
|
|
|
[EventType.CallAnswer]: 'messages.TextualEvent',
|
|
|
|
[EventType.CallHangup]: 'messages.TextualEvent',
|
|
|
|
[EventType.CallReject]: 'messages.TextualEvent',
|
2017-11-15 18:56:54 +03:00
|
|
|
};
|
2017-08-16 19:46:20 +03:00
|
|
|
|
2017-11-15 18:56:54 +03:00
|
|
|
const stateEventTileTypes = {
|
2021-04-13 11:34:37 +03:00
|
|
|
[EventType.RoomEncryption]: 'messages.EncryptionEvent',
|
|
|
|
[EventType.RoomCanonicalAlias]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomCreate]: 'messages.RoomCreate',
|
|
|
|
[EventType.RoomMember]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomName]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomAvatar]: 'messages.RoomAvatarEvent',
|
|
|
|
[EventType.RoomThirdPartyInvite]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomHistoryVisibility]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomTopic]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomPowerLevels]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomPinnedEvents]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomServerAcl]: 'messages.TextualEvent',
|
2020-08-03 18:02:26 +03:00
|
|
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
2017-08-16 19:46:20 +03:00
|
|
|
'im.vector.modular.widgets': 'messages.TextualEvent',
|
2021-01-19 05:31:11 +03:00
|
|
|
[WIDGET_LAYOUT_EVENT_TYPE]: 'messages.TextualEvent',
|
2021-04-13 11:34:37 +03:00
|
|
|
[EventType.RoomTombstone]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomJoinRules]: 'messages.TextualEvent',
|
|
|
|
[EventType.RoomGuestAccess]: 'messages.TextualEvent',
|
|
|
|
'm.room.related_groups': 'messages.TextualEvent', // legacy communities flair
|
2015-11-27 18:02:32 +03:00
|
|
|
};
|
|
|
|
|
2021-04-13 11:40:44 +03:00
|
|
|
const stateEventSingular = new Set([
|
|
|
|
EventType.RoomEncryption,
|
|
|
|
EventType.RoomCanonicalAlias,
|
|
|
|
EventType.RoomCreate,
|
|
|
|
EventType.RoomName,
|
|
|
|
EventType.RoomAvatar,
|
|
|
|
EventType.RoomHistoryVisibility,
|
|
|
|
EventType.RoomTopic,
|
|
|
|
EventType.RoomPowerLevels,
|
|
|
|
EventType.RoomPinnedEvents,
|
|
|
|
EventType.RoomServerAcl,
|
|
|
|
WIDGET_LAYOUT_EVENT_TYPE,
|
|
|
|
EventType.RoomTombstone,
|
|
|
|
EventType.RoomJoinRules,
|
|
|
|
EventType.RoomGuestAccess,
|
|
|
|
'm.room.related_groups',
|
|
|
|
]);
|
|
|
|
|
2019-12-10 06:36:00 +03:00
|
|
|
// Add all the Mjolnir stuff to the renderer
|
|
|
|
for (const evType of ALL_RULE_TYPES) {
|
|
|
|
stateEventTileTypes[evType] = 'messages.TextualEvent';
|
|
|
|
}
|
|
|
|
|
2019-12-20 03:57:50 +03:00
|
|
|
export function getHandlerTile(ev) {
|
2017-11-15 18:56:54 +03:00
|
|
|
const type = ev.getType();
|
2019-11-07 19:42:06 +03:00
|
|
|
|
|
|
|
// 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.get();
|
|
|
|
const me = client && client.getUserId();
|
|
|
|
if (ev.getSender() !== me && content.to !== me) {
|
|
|
|
return undefined;
|
|
|
|
} else {
|
|
|
|
return "messages.MKeyVerificationRequest";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-11-07 19:43:50 +03:00
|
|
|
// 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.get();
|
|
|
|
const me = client && client.getUserId();
|
|
|
|
if (ev.getSender() !== me) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-24 19:51:06 +03:00
|
|
|
// sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and
|
|
|
|
// fall back to showing hidden events, if we're viewing hidden events
|
2020-02-24 19:54:10 +03:00
|
|
|
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
|
|
|
// declining to render?
|
2020-05-28 23:41:31 +03:00
|
|
|
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
|
2020-02-24 19:51:06 +03:00
|
|
|
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
|
|
|
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-16 21:38:47 +03:00
|
|
|
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
|
|
|
if (type === "im.vector.modular.widgets") {
|
|
|
|
let type = ev.getContent()['type'];
|
|
|
|
if (!type) {
|
|
|
|
// deleted/invalid widget - try the past widget type
|
|
|
|
type = ev.getPrevContent()['type'];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (WidgetType.JITSI.matches(type)) {
|
|
|
|
return "messages.MJitsiWidgetEvent";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-13 11:40:44 +03:00
|
|
|
if (ev.isState()) {
|
|
|
|
if (stateEventSingular.has(type) && ev.getStateKey() !== "") return undefined;
|
|
|
|
return stateEventTileTypes[type];
|
|
|
|
}
|
|
|
|
|
|
|
|
return eventTileTypes[type];
|
2017-11-15 18:56:54 +03:00
|
|
|
}
|
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const MAX_READ_AVATARS = 5;
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2015-11-30 18:19:43 +03:00
|
|
|
// Our component structure for EventTiles on the timeline is:
|
|
|
|
//
|
|
|
|
// .-EventTile------------------------------------------------.
|
|
|
|
// | MemberAvatar (SenderProfile) TimeStamp |
|
|
|
|
// | .-{Message,Textual}Event---------------. Read Avatars |
|
|
|
|
// | | .-MFooBody-------------------. | |
|
|
|
|
// | | | (only if MessageEvent) | | |
|
|
|
|
// | | '----------------------------' | |
|
|
|
|
// | '--------------------------------------' |
|
|
|
|
// '----------------------------------------------------------'
|
|
|
|
|
2021-04-06 14:26:50 +03:00
|
|
|
interface IReadReceiptProps {
|
|
|
|
userId: string;
|
|
|
|
roomMember: RoomMember;
|
|
|
|
ts: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IProps {
|
|
|
|
// the MatrixEvent to show
|
|
|
|
mxEvent: MatrixEvent;
|
|
|
|
|
|
|
|
// true if mxEvent is redacted. This is a prop because using mxEvent.isRedacted()
|
|
|
|
// might not be enough when deciding shouldComponentUpdate - prevProps.mxEvent
|
|
|
|
// references the same this.props.mxEvent.
|
|
|
|
isRedacted?: boolean;
|
|
|
|
|
|
|
|
// true if this is a continuation of the previous event (which has the
|
|
|
|
// effect of not showing another avatar/displayname
|
|
|
|
continuation?: boolean;
|
|
|
|
|
|
|
|
// true if this is the last event in the timeline (which has the effect
|
|
|
|
// of always showing the timestamp)
|
|
|
|
last?: boolean;
|
|
|
|
|
|
|
|
// true if the event is the last event in a section (adds a css class for
|
|
|
|
// targeting)
|
|
|
|
lastInSection?: boolean;
|
|
|
|
|
|
|
|
// True if the event is the last successful (sent) event.
|
|
|
|
lastSuccessful?: boolean;
|
|
|
|
|
|
|
|
// true if this is search context (which has the effect of greying out
|
|
|
|
// the text
|
|
|
|
contextual?: boolean;
|
|
|
|
|
|
|
|
// a list of words to highlight, ordered by longest first
|
|
|
|
highlights?: string[];
|
|
|
|
|
|
|
|
// link URL for the highlights
|
|
|
|
highlightLink?: string;
|
|
|
|
|
|
|
|
// should show URL previews for this event
|
|
|
|
showUrlPreview?: boolean;
|
|
|
|
|
|
|
|
// is this the focused event
|
|
|
|
isSelectedEvent?: boolean;
|
|
|
|
|
|
|
|
// callback called when dynamic content in events are loaded
|
2021-04-26 18:16:43 +03:00
|
|
|
onHeightChanged?: () => void;
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// a list of read-receipts we should show. Each object has a 'roomMember' and 'ts'.
|
2021-04-26 18:16:43 +03:00
|
|
|
readReceipts?: IReadReceiptProps[];
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
|
|
|
// to manage its animations. Should be an empty object when the room
|
|
|
|
// first loads
|
2021-04-26 18:16:43 +03:00
|
|
|
readReceiptMap?: any;
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// A function which is used to check if the parent panel is being
|
|
|
|
// unmounted, to avoid unnecessary work. Should return true if we
|
|
|
|
// are being unmounted.
|
2021-04-26 18:16:43 +03:00
|
|
|
checkUnmounting?: () => boolean;
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// the status of this event - ie, mxEvent.status. Denormalised to here so
|
|
|
|
// that we can tell when it changes.
|
|
|
|
eventSendStatus?: string;
|
|
|
|
|
|
|
|
// the shape of the tile. by default, the layout is intended for the
|
|
|
|
// normal room timeline. alternative values are: "file_list", "file_grid"
|
|
|
|
// and "notif". This could be done by CSS, but it'd be horribly inefficient.
|
|
|
|
// It could also be done by subclassing EventTile, but that'd be quite
|
|
|
|
// boiilerplatey. So just make the necessary render decisions conditional
|
|
|
|
// for now.
|
2021-04-26 17:55:04 +03:00
|
|
|
tileShape?: 'notif' | 'file_grid' | 'reply' | 'reply_preview';
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// show twelve hour timestamps
|
|
|
|
isTwelveHour?: boolean;
|
|
|
|
|
|
|
|
// helper function to access relations for this event
|
2021-04-26 18:16:43 +03:00
|
|
|
getRelationsForEvent?: (eventId: string, relationType: string, eventType: string) => Relations;
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// whether to show reactions for this event
|
|
|
|
showReactions?: boolean;
|
|
|
|
|
|
|
|
// which layout to use
|
2021-04-26 18:16:43 +03:00
|
|
|
layout: Layout;
|
2021-04-06 14:26:50 +03:00
|
|
|
|
|
|
|
// whether or not to show flair at all
|
|
|
|
enableFlair?: boolean;
|
|
|
|
|
|
|
|
// whether or not to show read receipts
|
|
|
|
showReadReceipts?: boolean;
|
|
|
|
|
|
|
|
// Used while editing, to pass the event, and to preserve editor state
|
|
|
|
// from one editor instance to another when remounting the editor
|
|
|
|
// upon receiving the remote echo for an unsent event.
|
|
|
|
editState?: EditorStateTransfer;
|
|
|
|
|
|
|
|
// Event ID of the event replacing the content of this event, if any
|
|
|
|
replacingEventId?: string;
|
|
|
|
|
|
|
|
// Helper to build permalinks for the room
|
|
|
|
permalinkCreator?: RoomPermalinkCreator;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IState {
|
|
|
|
// Whether the action bar is focused.
|
|
|
|
actionBarFocused: boolean;
|
|
|
|
// Whether all read receipts are being displayed. If not, only display
|
|
|
|
// a truncation of them.
|
|
|
|
allReadAvatars: boolean;
|
|
|
|
// Whether the event's sender has been verified.
|
|
|
|
verified: string;
|
|
|
|
// Whether onRequestKeysClick has been called since mounting.
|
|
|
|
previouslyRequestedKeys: boolean;
|
|
|
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
|
|
|
reactions: Relations;
|
|
|
|
}
|
|
|
|
|
2021-03-09 06:12:00 +03:00
|
|
|
@replaceableComponent("views.rooms.EventTile")
|
2021-04-06 14:26:50 +03:00
|
|
|
export default class EventTile extends React.Component<IProps, IState> {
|
2021-04-26 16:02:53 +03:00
|
|
|
private suppressReadReceiptAnimation: boolean;
|
|
|
|
private isListeningForReceipts: boolean;
|
|
|
|
private tile = React.createRef();
|
|
|
|
private replyThread = React.createRef();
|
2015-12-24 03:12:37 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
static defaultProps = {
|
|
|
|
// no-op function because onHeightChanged is optional yet some sub-components assume its existence
|
|
|
|
onHeightChanged: function() {},
|
|
|
|
};
|
|
|
|
|
|
|
|
static contextType = MatrixClientContext;
|
2015-12-24 03:12:37 +03:00
|
|
|
|
2020-08-29 14:51:37 +03:00
|
|
|
constructor(props, context) {
|
|
|
|
super(props, context);
|
2020-08-29 14:14:16 +03:00
|
|
|
|
|
|
|
this.state = {
|
2019-04-26 14:14:30 +03:00
|
|
|
// Whether the action bar is focused.
|
|
|
|
actionBarFocused: false,
|
2018-03-08 14:48:04 +03:00
|
|
|
// 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,
|
2019-05-13 16:52:55 +03:00
|
|
|
// The Relations model from the JS SDK for reactions to `mxEvent`
|
|
|
|
reactions: this.getReactions(),
|
2018-03-08 14:48:04 +03:00
|
|
|
};
|
2019-08-05 14:26:31 +03:00
|
|
|
|
2016-04-21 01:03:05 +03:00
|
|
|
// don't do RR animations until we are mounted
|
2021-04-26 16:02:53 +03:00
|
|
|
this.suppressReadReceiptAnimation = true;
|
2019-12-08 15:16:17 +03:00
|
|
|
|
2021-03-02 02:21:04 +03:00
|
|
|
// 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.
|
2021-04-26 16:02:53 +03:00
|
|
|
this.isListeningForReceipts = false;
|
2021-03-02 02:21:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When true, the tile qualifies for some sort of special read receipt. This could be a 'sending'
|
|
|
|
* or 'sent' receipt, for example.
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
2021-04-26 16:02:53 +03:00
|
|
|
private get isEligibleForSpecialReceipt() {
|
2021-03-02 02:21:04 +03:00
|
|
|
// 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.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.
|
2021-03-05 20:56:13 +03:00
|
|
|
const simpleSendableEvents = [
|
|
|
|
EventType.Sticker,
|
|
|
|
EventType.RoomMessage,
|
|
|
|
EventType.RoomMessageEncrypted,
|
|
|
|
];
|
2021-03-02 02:21:04 +03:00
|
|
|
if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false;
|
|
|
|
|
|
|
|
// Default case
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private get shouldShowSentReceipt() {
|
2021-03-02 02:21:04 +03:00
|
|
|
// If we're not even eligible, don't show the receipt.
|
2021-04-26 16:02:53 +03:00
|
|
|
if (!this.isEligibleForSpecialReceipt) return false;
|
2021-03-02 02:21:04 +03:00
|
|
|
|
2021-03-05 20:56:13 +03:00
|
|
|
// We only show the 'sent' receipt on the last successful event.
|
|
|
|
if (!this.props.lastSuccessful) return false;
|
|
|
|
|
2021-03-02 02:21:04 +03:00
|
|
|
// 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;
|
|
|
|
|
2021-03-05 20:56:13 +03:00
|
|
|
// If anyone has read the event besides us, we don't want to show a sent receipt.
|
|
|
|
const receipts = this.props.readReceipts || [];
|
2021-03-02 02:21:04 +03:00
|
|
|
const myUserId = MatrixClientPeg.get().getUserId();
|
2021-03-05 20:56:13 +03:00
|
|
|
if (receipts.some(r => r.userId !== myUserId)) return false;
|
|
|
|
|
|
|
|
// Finally, we should show a receipt.
|
|
|
|
return true;
|
2021-03-02 02:21:04 +03:00
|
|
|
}
|
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private get shouldShowSendingReceipt() {
|
2021-03-02 02:21:04 +03:00
|
|
|
// If we're not even eligible, don't show the receipt.
|
2021-04-26 16:02:53 +03:00
|
|
|
if (!this.isEligibleForSpecialReceipt) return false;
|
2021-03-02 02:21:04 +03:00
|
|
|
|
|
|
|
// 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;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2020-08-29 14:51:37 +03:00
|
|
|
// TODO: [REACT-WARNING] Move into constructor
|
2020-08-29 14:57:11 +03:00
|
|
|
// eslint-disable-next-line camelcase
|
2020-08-29 14:51:37 +03:00
|
|
|
UNSAFE_componentWillMount() {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.verifyEvent(this.props.mxEvent);
|
2020-08-29 14:51:37 +03:00
|
|
|
}
|
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
componentDidMount() {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.suppressReadReceiptAnimation = false;
|
2019-12-17 20:26:12 +03:00
|
|
|
const client = this.context;
|
2019-05-09 19:59:51 +03:00
|
|
|
client.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
2020-01-20 18:16:41 +03:00
|
|
|
client.on("userTrustStatusChanged", this.onUserVerificationChanged);
|
2021-04-26 16:02:53 +03:00
|
|
|
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
2019-07-17 19:00:36 +03:00
|
|
|
if (this.props.showReactions) {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.props.mxEvent.on("Event.relationsCreated", this.onReactionsCreated);
|
2019-05-09 19:59:51 +03:00
|
|
|
}
|
2021-03-02 02:21:04 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
|
|
|
client.on("Room.receipt", this.onRoomReceipt);
|
|
|
|
this.isListeningForReceipts = true;
|
2021-03-02 02:21:04 +03:00
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-04-21 01:03:05 +03:00
|
|
|
|
2020-04-01 23:35:39 +03:00
|
|
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
2020-08-29 14:57:11 +03:00
|
|
|
// eslint-disable-next-line camelcase
|
2020-08-29 14:14:16 +03:00
|
|
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
2017-08-08 14:34:40 +03:00
|
|
|
// re-check the sender verification as outgoing events progress through
|
|
|
|
// the send process.
|
|
|
|
if (nextProps.eventSendStatus !== this.props.eventSendStatus) {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.verifyEvent(nextProps.mxEvent);
|
2016-06-08 19:01:13 +03:00
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-06-08 19:01:13 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
shouldComponentUpdate(nextProps, nextState) {
|
2021-02-19 03:00:10 +03:00
|
|
|
if (objectHasDiff(this.state, nextState)) {
|
2016-04-19 21:38:54 +03:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
return !this.propsEqual(this.props, nextProps);
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-04-19 21:38:54 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
componentWillUnmount() {
|
2019-12-17 20:26:12 +03:00
|
|
|
const client = this.context;
|
2018-05-06 21:28:18 +03:00
|
|
|
client.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged);
|
2020-01-20 18:16:41 +03:00
|
|
|
client.removeListener("userTrustStatusChanged", this.onUserVerificationChanged);
|
2021-04-26 16:02:53 +03:00
|
|
|
client.removeListener("Room.receipt", this.onRoomReceipt);
|
|
|
|
this.isListeningForReceipts = false;
|
|
|
|
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
2019-07-17 19:00:36 +03:00
|
|
|
if (this.props.showReactions) {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
|
2019-05-09 19:59:51 +03:00
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-11-15 14:12:52 +03:00
|
|
|
|
2021-03-02 02:21:04 +03:00
|
|
|
componentDidUpdate(prevProps, prevState, snapshot) {
|
|
|
|
// If we're not listening for receipts and expect to be, register a listener.
|
2021-04-26 16:02:53 +03:00
|
|
|
if (!this.isListeningForReceipts && (this.shouldShowSentReceipt || this.shouldShowSendingReceipt)) {
|
|
|
|
this.context.on("Room.receipt", this.onRoomReceipt);
|
|
|
|
this.isListeningForReceipts = true;
|
2021-03-02 02:21:04 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private onRoomReceipt = (ev, room) => {
|
2021-03-02 02:21:04 +03:00
|
|
|
// ignore events for other rooms
|
|
|
|
const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
|
|
|
if (room !== tileRoom) return;
|
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt && !this.isListeningForReceipts) {
|
2021-03-02 02:21:04 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-02 21:04:12 +03:00
|
|
|
// 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.
|
2021-03-02 02:21:04 +03:00
|
|
|
this.forceUpdate(() => {
|
|
|
|
// Per elsewhere in this file, we can remove the listener once we will have no further purpose for it.
|
2021-04-26 16:02:53 +03:00
|
|
|
if (!this.shouldShowSentReceipt && !this.shouldShowSendingReceipt) {
|
|
|
|
this.context.removeListener("Room.receipt", this.onRoomReceipt);
|
|
|
|
this.isListeningForReceipts = false;
|
2021-03-02 02:21:04 +03:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2016-11-15 14:12:52 +03:00
|
|
|
/** called when the event is decrypted after we show it.
|
|
|
|
*/
|
2021-04-26 16:02:53 +03:00
|
|
|
private onDecrypted = () => {
|
2016-11-15 14:12:52 +03:00
|
|
|
// we need to re-verify the sending device.
|
2021-04-26 16:02:53 +03:00
|
|
|
// (we call onHeightChanged in verifyEvent to handle the case where decryption
|
2017-12-15 17:01:07 +03:00
|
|
|
// has caused a change in size of the event tile)
|
2021-04-26 16:02:53 +03:00
|
|
|
this.verifyEvent(this.props.mxEvent);
|
2016-11-15 14:12:52 +03:00
|
|
|
this.forceUpdate();
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2016-06-08 20:35:43 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private onDeviceVerificationChanged = (userId, device) => {
|
2018-05-06 21:28:18 +03:00
|
|
|
if (userId === this.props.mxEvent.getSender()) {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.verifyEvent(this.props.mxEvent);
|
2016-06-08 20:35:43 +03:00
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2016-06-08 20:35:43 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private onUserVerificationChanged = (userId, _trustStatus) => {
|
2020-01-20 18:16:41 +03:00
|
|
|
if (userId === this.props.mxEvent.getSender()) {
|
2021-04-26 16:02:53 +03:00
|
|
|
this.verifyEvent(this.props.mxEvent);
|
2020-01-20 18:16:41 +03:00
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2020-01-20 18:16:41 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private async verifyEvent(mxEvent) {
|
2017-07-19 01:46:03 +03:00
|
|
|
if (!mxEvent.isEncrypted()) {
|
|
|
|
return;
|
2016-06-08 19:01:13 +03:00
|
|
|
}
|
|
|
|
|
2020-06-19 04:57:33 +03:00
|
|
|
const encryptionInfo = this.context.getEventEncryptionInfo(mxEvent);
|
|
|
|
const senderId = mxEvent.getSender();
|
|
|
|
const userTrust = this.context.checkUserTrust(senderId);
|
2020-01-20 18:16:41 +03:00
|
|
|
|
2020-06-19 04:57:33 +03:00
|
|
|
if (encryptionInfo.mismatchedSender) {
|
|
|
|
// something definitely wrong is going on here
|
2020-01-22 14:17:54 +03:00
|
|
|
this.setState({
|
|
|
|
verified: E2E_STATE.WARNING,
|
2020-06-19 04:57:33 +03:00
|
|
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
2020-01-22 14:17:54 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-06-19 04:57:33 +03:00
|
|
|
if (!userTrust.isCrossSigningVerified()) {
|
|
|
|
// user is not verified, so default to everything is normal
|
2020-01-22 14:17:54 +03:00
|
|
|
this.setState({
|
|
|
|
verified: E2E_STATE.NORMAL,
|
2020-06-19 04:57:33 +03:00
|
|
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
2020-01-22 14:17:54 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-07-15 19:25:10 +03:00
|
|
|
const eventSenderTrust = encryptionInfo.sender && this.context.checkDeviceTrust(
|
2020-06-19 04:57:33 +03:00
|
|
|
senderId, encryptionInfo.sender.deviceId,
|
|
|
|
);
|
2020-01-20 18:16:41 +03:00
|
|
|
if (!eventSenderTrust) {
|
|
|
|
this.setState({
|
2020-01-21 20:19:10 +03:00
|
|
|
verified: E2E_STATE.UNKNOWN,
|
2020-06-19 04:57:33 +03:00
|
|
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!eventSenderTrust.isVerified()) {
|
|
|
|
this.setState({
|
|
|
|
verified: E2E_STATE.WARNING,
|
|
|
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!encryptionInfo.authenticated) {
|
|
|
|
this.setState({
|
|
|
|
verified: E2E_STATE.UNAUTHENTICATED,
|
|
|
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
2020-01-20 18:16:41 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-06-08 19:01:13 +03:00
|
|
|
this.setState({
|
2020-06-19 04:57:33 +03:00
|
|
|
verified: E2E_STATE.VERIFIED,
|
2020-01-20 18:16:41 +03:00
|
|
|
}, this.props.onHeightChanged); // Decryption may have caused a change in size
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-06-08 19:01:13 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private propsEqual(objA, objB) {
|
2017-10-11 19:56:17 +03:00
|
|
|
const keysA = Object.keys(objA);
|
|
|
|
const keysB = Object.keys(objB);
|
2016-04-19 21:38:54 +03:00
|
|
|
|
|
|
|
if (keysA.length !== keysB.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
for (let i = 0; i < keysA.length; i++) {
|
|
|
|
const key = keysA[i];
|
2016-04-19 21:38:54 +03:00
|
|
|
|
|
|
|
if (!objB.hasOwnProperty(key)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// need to deep-compare readReceipts
|
2018-05-06 21:28:18 +03:00
|
|
|
if (key === 'readReceipts') {
|
2017-10-11 19:56:17 +03:00
|
|
|
const rA = objA[key];
|
|
|
|
const rB = objB[key];
|
2016-04-19 23:10:23 +03:00
|
|
|
if (rA === rB) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rA || !rB) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2016-04-19 21:38:54 +03:00
|
|
|
if (rA.length !== rB.length) {
|
|
|
|
return false;
|
|
|
|
}
|
2017-10-11 19:56:17 +03:00
|
|
|
for (let j = 0; j < rA.length; j++) {
|
2018-10-10 17:14:09 +03:00
|
|
|
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) {
|
2016-04-19 21:38:54 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (objA[key] !== objB[key]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2016-04-19 21:38:54 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
shouldHighlight() {
|
2020-05-25 18:17:46 +03:00
|
|
|
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
|
2015-11-27 18:02:32 +03:00
|
|
|
if (!actions || !actions.tweaks) { return false; }
|
2016-02-19 04:56:03 +03:00
|
|
|
|
|
|
|
// don't show self-highlights from another of our clients
|
2019-12-17 20:26:12 +03:00
|
|
|
if (this.props.mxEvent.getSender() === this.context.credentials.userId) {
|
2016-02-19 04:56:03 +03:00
|
|
|
return false;
|
|
|
|
}
|
2016-04-08 23:42:29 +03:00
|
|
|
|
2015-11-27 18:02:32 +03:00
|
|
|
return actions.tweaks.highlight;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
toggleAllReadAvatars = () => {
|
2015-11-27 18:02:32 +03:00
|
|
|
this.setState({
|
2017-10-11 19:56:17 +03:00
|
|
|
allReadAvatars: !this.state.allReadAvatars,
|
2015-11-27 18:02:32 +03:00
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
getReadAvatars() {
|
2021-04-26 16:02:53 +03:00
|
|
|
if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) {
|
2021-03-10 07:13:23 +03:00
|
|
|
return <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />;
|
2021-03-02 02:21:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// return early if there are no read receipts
|
2017-04-22 17:40:29 +03:00
|
|
|
if (!this.props.readReceipts || this.props.readReceipts.length === 0) {
|
2018-05-06 21:28:18 +03:00
|
|
|
return (<span className="mx_EventTile_readAvatars" />);
|
2017-04-21 23:28:28 +03:00
|
|
|
}
|
|
|
|
|
2017-03-27 16:34:05 +03:00
|
|
|
const ReadReceiptMarker = sdk.getComponent('rooms.ReadReceiptMarker');
|
|
|
|
const avatars = [];
|
|
|
|
const receiptOffset = 15;
|
|
|
|
let left = 0;
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const receipts = this.props.readReceipts || [];
|
|
|
|
for (let i = 0; i < receipts.length; ++i) {
|
|
|
|
const receipt = receipts[i];
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
let hidden = true;
|
2016-04-21 01:03:05 +03:00
|
|
|
if ((i < MAX_READ_AVATARS) || this.state.allReadAvatars) {
|
|
|
|
hidden = false;
|
2015-11-27 18:02:32 +03:00
|
|
|
}
|
2017-03-27 16:34:05 +03:00
|
|
|
// 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;
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2018-10-10 17:14:09 +03:00
|
|
|
const userId = receipt.userId;
|
2018-05-06 21:28:18 +03:00
|
|
|
let readReceiptInfo;
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2016-04-21 01:03:05 +03:00
|
|
|
if (this.props.readReceiptMap) {
|
|
|
|
readReceiptInfo = this.props.readReceiptMap[userId];
|
|
|
|
if (!readReceiptInfo) {
|
|
|
|
readReceiptInfo = {};
|
|
|
|
this.props.readReceiptMap[userId] = readReceiptInfo;
|
2015-11-27 18:02:32 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// add to the start so the most recent is on the end (ie. ends up rightmost)
|
|
|
|
avatars.unshift(
|
2016-12-09 14:24:10 +03:00
|
|
|
<ReadReceiptMarker key={userId} member={receipt.roomMember}
|
2018-10-10 17:14:09 +03:00
|
|
|
fallbackUserId={userId}
|
2016-04-21 01:03:05 +03:00
|
|
|
leftOffset={left} hidden={hidden}
|
|
|
|
readReceiptInfo={readReceiptInfo}
|
2016-04-22 19:03:15 +03:00
|
|
|
checkUnmounting={this.props.checkUnmounting}
|
2021-04-26 16:02:53 +03:00
|
|
|
suppressAnimation={this.suppressReadReceiptAnimation}
|
2015-11-27 18:02:32 +03:00
|
|
|
onClick={this.toggleAllReadAvatars}
|
2016-12-09 14:24:10 +03:00
|
|
|
timestamp={receipt.ts}
|
2017-06-22 17:53:58 +03:00
|
|
|
showTwelveHour={this.props.isTwelveHour}
|
2017-10-11 19:56:17 +03:00
|
|
|
/>,
|
2015-11-27 18:02:32 +03:00
|
|
|
);
|
|
|
|
}
|
2017-10-11 19:56:17 +03:00
|
|
|
let remText;
|
2015-11-27 18:02:32 +03:00
|
|
|
if (!this.state.allReadAvatars) {
|
2017-10-11 19:56:17 +03:00
|
|
|
const remainder = receipts.length - MAX_READ_AVATARS;
|
2015-11-27 18:02:32 +03:00
|
|
|
if (remainder > 0) {
|
|
|
|
remText = <span className="mx_EventTile_readAvatarRemainder"
|
|
|
|
onClick={this.toggleAllReadAvatars}
|
2020-04-28 16:30:56 +03:00
|
|
|
style={{ right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }}>{ remainder }+
|
2015-11-27 18:02:32 +03:00
|
|
|
</span>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-21 01:03:05 +03:00
|
|
|
return <span className="mx_EventTile_readAvatars">
|
2015-11-27 18:02:32 +03:00
|
|
|
{ remText }
|
2016-04-21 01:03:05 +03:00
|
|
|
{ avatars }
|
2015-11-27 18:02:32 +03:00
|
|
|
</span>;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
onSenderProfileClick = event => {
|
2017-07-20 17:46:36 +03:00
|
|
|
const mxEvent = this.props.mxEvent;
|
2017-03-08 19:55:44 +03:00
|
|
|
dis.dispatch({
|
2017-07-20 20:02:54 +03:00
|
|
|
action: 'insert_mention',
|
2017-07-20 17:46:36 +03:00
|
|
|
user_id: mxEvent.getSender(),
|
2016-03-17 14:56:46 +03:00
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2016-03-17 14:56:46 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
onRequestKeysClick = () => {
|
2018-03-08 14:48:04 +03:00
|
|
|
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.
|
2019-12-17 20:26:12 +03:00
|
|
|
this.context.cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2018-03-08 14:48:04 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
onPermalinkClicked = e => {
|
2017-03-09 12:56:52 +03:00
|
|
|
// This allows the permalink to be opened in a new tab/window or copied as
|
2020-08-03 18:02:26 +03:00
|
|
|
// matrix.to, but also for it to enable routing within Element when clicked.
|
2017-03-08 19:55:44 +03:00
|
|
|
e.preventDefault();
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_room',
|
|
|
|
event_id: this.props.mxEvent.getId(),
|
2017-06-08 16:17:49 +03:00
|
|
|
highlighted: true,
|
2017-03-08 19:55:44 +03:00
|
|
|
room_id: this.props.mxEvent.getRoomId(),
|
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2017-03-08 19:55:44 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private renderE2EPadlock() {
|
2017-08-08 14:34:40 +03:00
|
|
|
const ev = this.props.mxEvent;
|
|
|
|
|
2018-06-13 17:52:50 +03:00
|
|
|
// event could not be decrypted
|
2017-08-08 14:34:40 +03:00
|
|
|
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
2020-01-02 19:52:25 +03:00
|
|
|
return <E2ePadlockUndecryptable />;
|
2018-06-13 17:52:50 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// event is encrypted, display padlock corresponding to whether or not it is verified
|
|
|
|
if (ev.isEncrypted()) {
|
2020-01-22 14:17:54 +03:00
|
|
|
if (this.state.verified === E2E_STATE.NORMAL) {
|
|
|
|
return; // no icon if we've not even cross-signed the user
|
|
|
|
} else if (this.state.verified === E2E_STATE.VERIFIED) {
|
2019-02-01 18:43:12 +03:00
|
|
|
return; // no icon for verified
|
2020-06-19 04:57:33 +03:00
|
|
|
} else if (this.state.verified === E2E_STATE.UNAUTHENTICATED) {
|
|
|
|
return (<E2ePadlockUnauthenticated />);
|
2020-01-21 20:19:10 +03:00
|
|
|
} else if (this.state.verified === E2E_STATE.UNKNOWN) {
|
2020-01-20 20:14:31 +03:00
|
|
|
return (<E2ePadlockUnknown />);
|
2019-02-01 18:43:12 +03:00
|
|
|
} else {
|
2020-01-02 19:52:25 +03:00
|
|
|
return (<E2ePadlockUnverified />);
|
2019-02-01 18:43:12 +03:00
|
|
|
}
|
2018-06-13 17:52:50 +03:00
|
|
|
}
|
|
|
|
|
2019-12-17 20:26:12 +03:00
|
|
|
if (this.context.isRoomEncrypted(ev.getRoomId())) {
|
2018-06-13 15:51:04 +03:00
|
|
|
// else if room is encrypted
|
|
|
|
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
2018-06-13 17:52:50 +03:00
|
|
|
if (ev.status === EventStatus.ENCRYPTING) {
|
2019-02-01 18:43:12 +03:00
|
|
|
return;
|
2017-08-08 14:34:40 +03:00
|
|
|
}
|
2018-06-13 17:52:50 +03:00
|
|
|
if (ev.status === EventStatus.NOT_SENT) {
|
2019-02-01 18:43:12 +03:00
|
|
|
return;
|
2017-08-08 14:34:40 +03:00
|
|
|
}
|
2019-12-04 08:41:22 +03:00
|
|
|
if (ev.isState()) {
|
|
|
|
return; // we expect this to be unencrypted
|
|
|
|
}
|
2018-06-13 17:52:50 +03:00
|
|
|
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
2020-01-02 19:52:25 +03:00
|
|
|
return <E2ePadlockUnencrypted />;
|
2017-08-08 14:34:40 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// no padlock needed
|
|
|
|
return null;
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
2017-08-08 14:34:40 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
onActionBarFocusChange = focused => {
|
2019-04-26 14:14:30 +03:00
|
|
|
this.setState({
|
|
|
|
actionBarFocused: focused,
|
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2019-04-26 14:14:30 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
getTile = () => this.tile.current;
|
2019-05-07 14:06:50 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
getReplyThread = () => this.replyThread.current;
|
2019-05-07 14:06:50 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
getReactions = () => {
|
2019-05-08 20:16:27 +03:00
|
|
|
if (
|
2019-05-21 19:23:19 +03:00
|
|
|
!this.props.showReactions ||
|
2019-07-17 19:00:36 +03:00
|
|
|
!this.props.getRelationsForEvent
|
2019-05-08 20:16:27 +03:00
|
|
|
) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const eventId = this.props.mxEvent.getId();
|
2019-10-29 20:32:22 +03:00
|
|
|
if (!eventId) {
|
2020-08-03 18:02:26 +03:00
|
|
|
// XXX: Temporary diagnostic logging for https://github.com/vector-im/element-web/issues/11120
|
2019-10-29 20:32:22 +03:00
|
|
|
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));
|
2020-08-03 18:02:26 +03:00
|
|
|
console.trace("Stacktrace for https://github.com/vector-im/element-web/issues/11120");
|
2019-10-29 20:32:22 +03:00
|
|
|
}
|
2019-05-08 20:16:27 +03:00
|
|
|
return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction");
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2019-05-08 20:16:27 +03:00
|
|
|
|
2021-04-26 16:02:53 +03:00
|
|
|
private onReactionsCreated = (relationType, eventType) => {
|
2019-05-09 19:59:51 +03:00
|
|
|
if (relationType !== "m.annotation" || eventType !== "m.reaction") {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-26 16:02:53 +03:00
|
|
|
this.props.mxEvent.removeListener("Event.relationsCreated", this.onReactionsCreated);
|
2019-05-13 16:52:55 +03:00
|
|
|
this.setState({
|
|
|
|
reactions: this.getReactions(),
|
|
|
|
});
|
2020-08-29 14:14:16 +03:00
|
|
|
};
|
2019-05-09 19:59:51 +03:00
|
|
|
|
2020-08-29 14:14:16 +03:00
|
|
|
render() {
|
2017-10-11 19:56:17 +03:00
|
|
|
const MessageTimestamp = sdk.getComponent('messages.MessageTimestamp');
|
|
|
|
const SenderProfile = sdk.getComponent('messages.SenderProfile');
|
|
|
|
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2019-11-26 04:14:03 +03:00
|
|
|
//console.info("EventTile showUrlPreview for %s is %s", this.props.mxEvent.getId(), this.props.showUrlPreview);
|
2016-07-20 14:03:13 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const content = this.props.mxEvent.getContent();
|
|
|
|
const msgtype = content.msgtype;
|
|
|
|
const eventType = this.props.mxEvent.getType();
|
2016-08-19 00:19:23 +03:00
|
|
|
|
2020-09-16 21:38:47 +03:00
|
|
|
let tileHandler = getHandlerTile(this.props.mxEvent);
|
|
|
|
|
2017-04-26 20:00:25 +03:00
|
|
|
// Info messages are basically information about commands processed on a room
|
2019-11-07 19:44:53 +03:00
|
|
|
const isBubbleMessage = eventType.startsWith("m.key.verification") ||
|
2020-11-10 13:00:55 +03:00
|
|
|
(eventType === EventType.RoomMessage && msgtype && msgtype.startsWith("m.key.verification")) ||
|
|
|
|
(eventType === EventType.RoomCreate) ||
|
|
|
|
(eventType === EventType.RoomEncryption) ||
|
2020-09-16 21:38:47 +03:00
|
|
|
(tileHandler === "messages.MJitsiWidgetEvent");
|
2019-06-03 18:29:16 +03:00
|
|
|
let isInfoMessage = (
|
2020-11-10 13:00:55 +03:00
|
|
|
!isBubbleMessage && eventType !== EventType.RoomMessage &&
|
|
|
|
eventType !== EventType.Sticker && eventType !== EventType.RoomCreate
|
2018-09-17 20:20:12 +03:00
|
|
|
);
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2019-06-03 18:29:16 +03:00
|
|
|
// 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).
|
2020-10-14 11:46:54 +03:00
|
|
|
if (SettingsStore.getValue("showHiddenEventsInTimeline") && !haveTileForEvent(this.props.mxEvent)) {
|
2019-05-02 17:11:06 +03:00
|
|
|
tileHandler = "messages.ViewSourceEvent";
|
2019-06-03 18:29:16 +03:00
|
|
|
// Reuse info message avatar and sender profile styling
|
|
|
|
isInfoMessage = true;
|
2019-05-02 17:11:06 +03:00
|
|
|
}
|
2015-11-27 18:02:32 +03:00
|
|
|
// This shouldn't happen: the caller should check we support this type
|
|
|
|
// before trying to instantiate us
|
2018-06-27 13:44:11 +03:00
|
|
|
if (!tileHandler) {
|
|
|
|
const {mxEvent} = this.props;
|
2018-06-29 14:54:36 +03:00
|
|
|
console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
|
|
|
|
return <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
|
|
|
|
<div className="mx_EventTile_line">
|
|
|
|
{ _t('This event could not be displayed') }
|
|
|
|
</div>
|
|
|
|
</div>;
|
2015-11-27 18:02:32 +03:00
|
|
|
}
|
2018-06-27 13:44:11 +03:00
|
|
|
const EventTileType = sdk.getComponent(tileHandler);
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2017-10-11 19:56:17 +03:00
|
|
|
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
2018-06-13 11:28:35 +03:00
|
|
|
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
2018-03-08 14:48:04 +03:00
|
|
|
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
2016-09-12 03:37:51 +03:00
|
|
|
|
2019-06-12 19:52:34 +03:00
|
|
|
const isEditing = !!this.props.editState;
|
2017-05-20 00:26:24 +03:00
|
|
|
const classes = classNames({
|
2019-11-07 19:44:53 +03:00
|
|
|
mx_EventTile_bubbleContainer: isBubbleMessage,
|
2015-11-27 18:02:32 +03:00
|
|
|
mx_EventTile: true,
|
2019-06-12 19:52:34 +03:00
|
|
|
mx_EventTile_isEditing: isEditing,
|
2016-08-18 23:53:37 +03:00
|
|
|
mx_EventTile_info: isInfoMessage,
|
2017-05-20 00:45:56 +03:00
|
|
|
mx_EventTile_12hr: this.props.isTwelveHour,
|
2021-03-02 21:04:12 +03:00
|
|
|
// Note: we keep the `sending` state class for tests, not for our styles
|
2019-06-12 19:52:34 +03:00
|
|
|
mx_EventTile_sending: !isEditing && isSending,
|
2018-05-06 21:28:18 +03:00
|
|
|
mx_EventTile_highlight: this.props.tileShape === 'notif' ? false : this.shouldHighlight(),
|
2016-02-03 11:03:10 +03:00
|
|
|
mx_EventTile_selected: this.props.isSelectedEvent,
|
2016-09-11 04:14:27 +03:00
|
|
|
mx_EventTile_continuation: this.props.tileShape ? '' : this.props.continuation,
|
2015-11-27 18:02:32 +03:00
|
|
|
mx_EventTile_last: this.props.last,
|
2020-09-17 19:40:48 +03:00
|
|
|
mx_EventTile_lastInSection: this.props.lastInSection,
|
2015-11-27 18:02:32 +03:00
|
|
|
mx_EventTile_contextual: this.props.contextual,
|
2019-04-26 14:14:30 +03:00
|
|
|
mx_EventTile_actionBarFocused: this.state.actionBarFocused,
|
2020-01-21 20:19:10 +03:00
|
|
|
mx_EventTile_verified: !isBubbleMessage && this.state.verified === E2E_STATE.VERIFIED,
|
|
|
|
mx_EventTile_unverified: !isBubbleMessage && this.state.verified === E2E_STATE.WARNING,
|
|
|
|
mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN,
|
2018-03-08 14:48:04 +03:00
|
|
|
mx_EventTile_bad: isEncryptionFailure,
|
2017-04-26 20:00:25 +03:00
|
|
|
mx_EventTile_emote: msgtype === 'm.emote',
|
2015-11-27 18:02:32 +03:00
|
|
|
});
|
2017-03-08 19:55:44 +03:00
|
|
|
|
2020-02-14 14:34:22 +03:00
|
|
|
// If the tile is in the Sending state, don't speak the message.
|
2020-07-15 20:02:00 +03:00
|
|
|
const ariaLive = (this.props.eventSendStatus !== null) ? 'off' : undefined;
|
2020-02-14 14:34:22 +03:00
|
|
|
|
2019-02-21 17:44:20 +03:00
|
|
|
let permalink = "#";
|
2019-02-22 14:22:36 +03:00
|
|
|
if (this.props.permalinkCreator) {
|
|
|
|
permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId());
|
2019-02-21 17:44:20 +03:00
|
|
|
}
|
2015-11-27 18:02:32 +03:00
|
|
|
|
2018-05-06 21:28:18 +03:00
|
|
|
let avatar;
|
|
|
|
let sender;
|
2016-08-25 18:55:09 +03:00
|
|
|
let avatarSize;
|
|
|
|
let needsSenderProfile;
|
|
|
|
|
2017-03-16 20:00:10 +03:00
|
|
|
if (this.props.tileShape === "notif") {
|
2016-09-11 04:14:27 +03:00
|
|
|
avatarSize = 24;
|
|
|
|
needsSenderProfile = true;
|
2019-11-07 19:44:53 +03:00
|
|
|
} else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) {
|
2018-08-20 18:20:59 +03:00
|
|
|
avatarSize = 0;
|
|
|
|
needsSenderProfile = false;
|
2016-09-11 04:14:27 +03:00
|
|
|
} else if (isInfoMessage) {
|
2016-09-12 01:01:20 +03:00
|
|
|
// a small avatar, with no sender profile, for
|
2016-08-25 18:55:09 +03:00
|
|
|
// joins/parts/etc
|
|
|
|
avatarSize = 14;
|
|
|
|
needsSenderProfile = false;
|
2021-01-22 15:44:45 +03:00
|
|
|
} else if (this.props.layout == Layout.IRC) {
|
2020-04-29 17:07:41 +03:00
|
|
|
avatarSize = 14;
|
|
|
|
needsSenderProfile = true;
|
2016-09-16 04:40:09 +03:00
|
|
|
} else if (this.props.continuation && this.props.tileShape !== "file_grid") {
|
2016-08-25 18:55:09 +03:00
|
|
|
// no avatar or sender profile for continuation messages
|
|
|
|
avatarSize = 0;
|
|
|
|
needsSenderProfile = false;
|
|
|
|
} else {
|
|
|
|
avatarSize = 30;
|
2016-09-12 01:01:20 +03:00
|
|
|
needsSenderProfile = true;
|
2016-08-25 18:55:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (this.props.mxEvent.sender && avatarSize) {
|
2020-11-25 10:16:14 +03:00
|
|
|
let member;
|
|
|
|
// set member to receiver (target) if it is a 3PID invite
|
2020-11-27 11:43:42 +03:00
|
|
|
// so that the correct avatar is shown as the text is
|
|
|
|
// `$target accepted the invitation for $email`
|
2020-11-25 10:16:14 +03:00
|
|
|
if (this.props.mxEvent.getContent().third_party_invite) {
|
2021-04-23 20:11:54 +03:00
|
|
|
member = this.props.mxEvent.target;
|
2020-11-25 10:16:14 +03:00
|
|
|
} else {
|
2020-11-26 14:48:11 +03:00
|
|
|
member = this.props.mxEvent.sender;
|
2020-11-24 15:15:20 +03:00
|
|
|
}
|
2020-11-25 10:16:14 +03:00
|
|
|
avatar = (
|
|
|
|
<div className="mx_EventTile_avatar">
|
|
|
|
<MemberAvatar member={member}
|
|
|
|
width={avatarSize} height={avatarSize}
|
|
|
|
viewUserOnClick={true}
|
|
|
|
/>
|
|
|
|
</div>
|
2020-11-26 14:48:11 +03:00
|
|
|
);
|
2016-08-25 18:55:09 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if (needsSenderProfile) {
|
2018-05-06 21:18:41 +03:00
|
|
|
if (!this.props.tileShape || this.props.tileShape === 'reply' || this.props.tileShape === 'reply_preview') {
|
|
|
|
sender = <SenderProfile onClick={this.onSenderProfileClick}
|
2021-04-23 20:11:54 +03:00
|
|
|
mxEvent={this.props.mxEvent}
|
|
|
|
enableFlair={this.props.enableFlair}
|
|
|
|
/>;
|
2017-10-11 19:56:17 +03:00
|
|
|
} else {
|
2020-09-16 13:26:15 +03:00
|
|
|
sender = <SenderProfile mxEvent={this.props.mxEvent} enableFlair={this.props.enableFlair} />;
|
2016-09-11 04:14:27 +03:00
|
|
|
}
|
2015-11-27 18:02:32 +03:00
|
|
|
}
|
2016-08-16 13:59:26 +03:00
|
|
|
|
2019-04-26 14:14:30 +03:00
|
|
|
const MessageActionBar = sdk.getComponent('messages.MessageActionBar');
|
2019-06-12 19:52:34 +03:00
|
|
|
const actionBar = !isEditing ? <MessageActionBar
|
2019-04-26 14:14:30 +03:00
|
|
|
mxEvent={this.props.mxEvent}
|
2019-05-13 16:52:55 +03:00
|
|
|
reactions={this.state.reactions}
|
2019-04-26 14:14:30 +03:00
|
|
|
permalinkCreator={this.props.permalinkCreator}
|
2019-05-07 14:06:50 +03:00
|
|
|
getTile={this.getTile}
|
|
|
|
getReplyThread={this.getReplyThread}
|
2019-04-26 14:14:30 +03:00
|
|
|
onFocusChange={this.onActionBarFocusChange}
|
2019-05-17 17:35:28 +03:00
|
|
|
/> : undefined;
|
2017-08-08 14:34:40 +03:00
|
|
|
|
2019-05-28 14:19:35 +03:00
|
|
|
const timestamp = this.props.mxEvent.getTs() ?
|
|
|
|
<MessageTimestamp showTwelveHour={this.props.isTwelveHour} ts={this.props.mxEvent.getTs()} /> : null;
|
2016-09-12 03:37:51 +03:00
|
|
|
|
2018-03-08 14:48:04 +03:00
|
|
|
const keyRequestHelpText =
|
|
|
|
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
|
|
|
|
<p>
|
|
|
|
{ this.state.previouslyRequestedKeys ?
|
2020-01-29 18:48:25 +03:00
|
|
|
_t( 'Your key share request has been sent - please check your other sessions ' +
|
2018-03-08 14:48:04 +03:00
|
|
|
'for key share requests.') :
|
2020-01-29 18:48:25 +03:00
|
|
|
_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 ' +
|
2018-03-08 14:48:04 +03:00
|
|
|
'here to request the keys for this session again.')
|
|
|
|
}
|
|
|
|
</p>
|
|
|
|
<p>
|
2020-01-29 18:48:25 +03:00
|
|
|
{ _t( 'If your other sessions do not have the key for this message you will not ' +
|
2018-03-08 14:48:04 +03:00
|
|
|
'be able to decrypt them.')
|
|
|
|
}
|
|
|
|
</p>
|
|
|
|
</div>;
|
|
|
|
const keyRequestInfoContent = this.state.previouslyRequestedKeys ?
|
|
|
|
_t('Key request sent.') :
|
|
|
|
_t(
|
2020-01-29 18:48:25 +03:00
|
|
|
'<requestLink>Re-request encryption keys</requestLink> from your other sessions.',
|
2018-03-08 14:48:04 +03:00
|
|
|
{},
|
|
|
|
{'requestLink': (sub) => <a onClick={this.onRequestKeysClick}>{ sub }</a>},
|
2016-09-11 04:14:27 +03:00
|
|
|
);
|
2018-03-08 14:48:04 +03:00
|
|
|
|
2019-06-19 13:26:11 +03:00
|
|
|
const TooltipButton = sdk.getComponent('elements.TooltipButton');
|
2021-03-29 06:35:08 +03:00
|
|
|
const keyRequestInfo = isEncryptionFailure && !isRedacted ?
|
2018-03-08 14:48:04 +03:00
|
|
|
<div className="mx_EventTile_keyRequestInfo">
|
|
|
|
<span className="mx_EventTile_keyRequestInfo_text">
|
|
|
|
{ keyRequestInfoContent }
|
|
|
|
</span>
|
2019-06-19 13:26:11 +03:00
|
|
|
<TooltipButton helpText={keyRequestHelpText} />
|
2018-03-08 14:48:04 +03:00
|
|
|
</div> : null;
|
|
|
|
|
2019-05-08 20:16:27 +03:00
|
|
|
let reactionsRow;
|
2019-07-17 19:00:36 +03:00
|
|
|
if (!isRedacted) {
|
2019-05-01 20:05:11 +03:00
|
|
|
const ReactionsRow = sdk.getComponent('messages.ReactionsRow');
|
2019-05-08 20:16:27 +03:00
|
|
|
reactionsRow = <ReactionsRow
|
2019-05-01 20:05:11 +03:00
|
|
|
mxEvent={this.props.mxEvent}
|
2019-05-13 16:52:55 +03:00
|
|
|
reactions={this.state.reactions}
|
2019-05-01 20:05:11 +03:00
|
|
|
/>;
|
|
|
|
}
|
|
|
|
|
2020-05-05 12:54:44 +03:00
|
|
|
const linkedTimestamp = <a
|
2021-04-23 20:11:54 +03:00
|
|
|
href={permalink}
|
|
|
|
onClick={this.onPermalinkClicked}
|
|
|
|
aria-label={formatTime(new Date(this.props.mxEvent.getTs()), this.props.isTwelveHour)}
|
|
|
|
>
|
|
|
|
{ timestamp }
|
|
|
|
</a>;
|
2020-05-05 12:54:44 +03:00
|
|
|
|
2021-01-22 15:44:45 +03:00
|
|
|
const useIRCLayout = this.props.layout == Layout.IRC;
|
|
|
|
const groupTimestamp = !useIRCLayout ? linkedTimestamp : null;
|
|
|
|
const ircTimestamp = useIRCLayout ? linkedTimestamp : null;
|
2021-04-26 16:02:53 +03:00
|
|
|
const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
|
|
|
|
const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock();
|
2020-05-05 12:54:44 +03:00
|
|
|
|
2021-04-14 12:43:42 +03:00
|
|
|
let msgOption;
|
2021-04-14 13:24:33 +03:00
|
|
|
if (this.props.showReadReceipts) {
|
2021-04-14 13:25:48 +03:00
|
|
|
const readAvatars = this.getReadAvatars();
|
2021-04-14 12:43:42 +03:00
|
|
|
msgOption = (
|
|
|
|
<div className="mx_EventTile_msgOption">
|
|
|
|
{ readAvatars }
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-12-10 15:50:41 +03:00
|
|
|
switch (this.props.tileShape) {
|
|
|
|
case 'notif': {
|
2019-12-17 20:26:12 +03:00
|
|
|
const room = this.context.getRoom(this.props.mxEvent.getRoomId());
|
2017-12-10 15:50:41 +03:00
|
|
|
return (
|
2020-07-24 02:03:42 +03:00
|
|
|
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
2017-12-10 15:50:41 +03:00
|
|
|
<div className="mx_EventTile_roomName">
|
2020-09-17 18:23:06 +03:00
|
|
|
<RoomAvatar room={room} width={28} height={28} />
|
2019-05-19 17:23:43 +03:00
|
|
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
2017-12-10 15:50:41 +03:00
|
|
|
{ room ? room.name : '' }
|
2019-05-19 17:23:43 +03:00
|
|
|
</a>
|
2017-12-10 15:50:41 +03:00
|
|
|
</div>
|
2016-09-11 04:14:27 +03:00
|
|
|
<div className="mx_EventTile_senderDetails">
|
2017-12-10 15:50:41 +03:00
|
|
|
{ avatar }
|
|
|
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
2016-09-11 04:14:27 +03:00
|
|
|
{ sender }
|
2017-03-03 18:42:24 +03:00
|
|
|
{ timestamp }
|
2017-12-10 15:50:41 +03:00
|
|
|
</a>
|
|
|
|
</div>
|
2019-10-23 20:30:29 +03:00
|
|
|
<div className="mx_EventTile_line">
|
2021-04-26 16:02:53 +03:00
|
|
|
<EventTileType ref={this.tile}
|
2021-04-23 20:11:54 +03:00
|
|
|
mxEvent={this.props.mxEvent}
|
|
|
|
highlights={this.props.highlights}
|
|
|
|
highlightLink={this.props.highlightLink}
|
|
|
|
showUrlPreview={this.props.showUrlPreview}
|
|
|
|
onHeightChanged={this.props.onHeightChanged}
|
|
|
|
/>
|
2016-09-11 04:14:27 +03:00
|
|
|
</div>
|
|
|
|
</div>
|
2017-12-10 15:50:41 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
case 'file_grid': {
|
|
|
|
return (
|
2020-07-24 02:03:42 +03:00
|
|
|
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
2019-10-23 20:30:29 +03:00
|
|
|
<div className="mx_EventTile_line">
|
2021-04-26 16:02:53 +03:00
|
|
|
<EventTileType ref={this.tile}
|
2021-04-23 20:11:54 +03:00
|
|
|
mxEvent={this.props.mxEvent}
|
|
|
|
highlights={this.props.highlights}
|
|
|
|
highlightLink={this.props.highlightLink}
|
|
|
|
showUrlPreview={this.props.showUrlPreview}
|
|
|
|
tileShape={this.props.tileShape}
|
|
|
|
onHeightChanged={this.props.onHeightChanged}
|
|
|
|
/>
|
2017-12-10 15:50:41 +03:00
|
|
|
</div>
|
|
|
|
<a
|
|
|
|
className="mx_EventTile_senderDetailsLink"
|
|
|
|
href={permalink}
|
|
|
|
onClick={this.onPermalinkClicked}
|
|
|
|
>
|
|
|
|
<div className="mx_EventTile_senderDetails">
|
|
|
|
{ sender }
|
|
|
|
{ timestamp }
|
|
|
|
</div>
|
2016-09-11 04:14:27 +03:00
|
|
|
</a>
|
|
|
|
</div>
|
2017-12-10 15:50:41 +03:00
|
|
|
);
|
|
|
|
}
|
2018-02-17 00:07:25 +03:00
|
|
|
|
|
|
|
case 'reply':
|
2018-02-16 23:58:39 +03:00
|
|
|
case 'reply_preview': {
|
2019-02-26 12:26:33 +03:00
|
|
|
let thread;
|
|
|
|
if (this.props.tileShape === 'reply_preview') {
|
|
|
|
thread = ReplyThread.makeThread(
|
|
|
|
this.props.mxEvent,
|
2019-03-06 14:27:16 +03:00
|
|
|
this.props.onHeightChanged,
|
2019-02-26 12:26:33 +03:00
|
|
|
this.props.permalinkCreator,
|
2021-04-26 16:02:53 +03:00
|
|
|
this.replyThread,
|
2019-02-26 12:26:33 +03:00
|
|
|
);
|
|
|
|
}
|
2018-02-16 23:58:39 +03:00
|
|
|
return (
|
2020-07-24 02:03:42 +03:00
|
|
|
<div className={classes} aria-live={ariaLive} aria-atomic="true">
|
2020-05-05 12:54:44 +03:00
|
|
|
{ ircTimestamp }
|
2018-02-16 23:58:39 +03:00
|
|
|
{ avatar }
|
|
|
|
{ sender }
|
2020-05-26 20:15:06 +03:00
|
|
|
{ ircPadlock }
|
2018-02-17 00:07:25 +03:00
|
|
|
<div className="mx_EventTile_reply">
|
2020-05-05 12:54:44 +03:00
|
|
|
{ groupTimestamp }
|
2020-05-26 20:15:06 +03:00
|
|
|
{ groupPadlock }
|
2019-02-26 12:26:33 +03:00
|
|
|
{ thread }
|
2021-04-26 16:02:53 +03:00
|
|
|
<EventTileType ref={this.tile}
|
2021-04-23 20:11:54 +03:00
|
|
|
mxEvent={this.props.mxEvent}
|
|
|
|
highlights={this.props.highlights}
|
|
|
|
highlightLink={this.props.highlightLink}
|
|
|
|
onHeightChanged={this.props.onHeightChanged}
|
|
|
|
replacingEventId={this.props.replacingEventId}
|
|
|
|
showUrlPreview={false}
|
|
|
|
/>
|
2017-12-10 15:50:41 +03:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
default: {
|
2019-02-26 12:26:33 +03:00
|
|
|
const thread = ReplyThread.makeThread(
|
|
|
|
this.props.mxEvent,
|
2019-03-06 14:27:16 +03:00
|
|
|
this.props.onHeightChanged,
|
2019-02-26 12:26:33 +03:00
|
|
|
this.props.permalinkCreator,
|
2021-04-26 16:02:53 +03:00
|
|
|
this.replyThread,
|
2021-01-22 15:44:45 +03:00
|
|
|
this.props.layout,
|
2019-02-26 12:26:33 +03:00
|
|
|
);
|
2020-04-29 17:07:41 +03:00
|
|
|
|
2019-11-04 13:16:16 +03:00
|
|
|
// tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers
|
2017-12-10 15:50:41 +03:00
|
|
|
return (
|
2020-07-24 02:03:42 +03:00
|
|
|
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
2020-04-29 17:07:41 +03:00
|
|
|
{ ircTimestamp }
|
2017-12-10 15:50:41 +03:00
|
|
|
{ sender }
|
2020-05-26 20:15:06 +03:00
|
|
|
{ ircPadlock }
|
2019-11-13 20:37:25 +03:00
|
|
|
<div className="mx_EventTile_line">
|
2020-04-29 17:07:41 +03:00
|
|
|
{ groupTimestamp }
|
2020-05-26 20:15:06 +03:00
|
|
|
{ groupPadlock }
|
2019-02-26 12:26:33 +03:00
|
|
|
{ thread }
|
2021-04-26 16:02:53 +03:00
|
|
|
<EventTileType ref={this.tile}
|
2021-04-23 20:11:54 +03:00
|
|
|
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}
|
|
|
|
/>
|
2018-03-08 14:48:04 +03:00
|
|
|
{ keyRequestInfo }
|
2019-05-08 20:16:27 +03:00
|
|
|
{ reactionsRow }
|
2019-04-26 14:14:30 +03:00
|
|
|
{ actionBar }
|
2017-12-10 15:50:41 +03:00
|
|
|
</div>
|
2021-04-14 12:43:42 +03:00
|
|
|
{msgOption}
|
2018-07-12 19:48:49 +03:00
|
|
|
{
|
2019-05-22 03:54:40 +03:00
|
|
|
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
2018-07-12 19:48:49 +03:00
|
|
|
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
|
|
|
// the need for further z-indexing chaos)
|
|
|
|
}
|
|
|
|
{ avatar }
|
2017-12-10 15:50:41 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
2016-09-11 04:14:27 +03:00
|
|
|
}
|
2020-08-29 14:14:16 +03:00
|
|
|
}
|
|
|
|
}
|
2016-11-14 19:00:24 +03:00
|
|
|
|
2018-06-13 11:28:35 +03:00
|
|
|
// 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()));
|
|
|
|
}
|
|
|
|
|
2019-12-20 03:57:50 +03:00
|
|
|
export function haveTileForEvent(e) {
|
2017-03-03 19:45:29 +03:00
|
|
|
// Only messages have a tile (black-rectangle) if redacted
|
2018-06-13 11:28:35 +03:00
|
|
|
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
2017-11-15 18:56:54 +03:00
|
|
|
|
2019-06-03 19:51:40 +03:00
|
|
|
// No tile for replacement events since they update the original tile
|
2019-07-17 18:56:15 +03:00
|
|
|
if (e.isRelation("m.replace")) return false;
|
2019-06-03 19:51:40 +03:00
|
|
|
|
2017-11-15 18:56:54 +03:00
|
|
|
const handler = getHandlerTile(e);
|
|
|
|
if (handler === undefined) return false;
|
|
|
|
if (handler === 'messages.TextualEvent') {
|
2016-11-14 19:00:24 +03:00
|
|
|
return TextForEvent.textForEvent(e) !== '';
|
2018-08-20 18:20:59 +03:00
|
|
|
} else if (handler === 'messages.RoomCreate') {
|
|
|
|
return Boolean(e.getContent()['predecessor']);
|
2016-11-14 19:00:24 +03:00
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
2019-12-20 03:57:50 +03:00
|
|
|
}
|
2017-08-08 14:34:40 +03:00
|
|
|
|
|
|
|
function E2ePadlockUndecryptable(props) {
|
|
|
|
return (
|
2019-12-04 08:41:22 +03:00
|
|
|
<E2ePadlock title={_t("This message cannot be decrypted")} icon="undecryptable" {...props} />
|
2017-08-08 14:34:40 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function E2ePadlockUnverified(props) {
|
|
|
|
return (
|
2020-01-29 18:48:25 +03:00
|
|
|
<E2ePadlock title={_t("Encrypted by an unverified session")} icon="unverified" {...props} />
|
2017-08-08 14:34:40 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
function E2ePadlockUnencrypted(props) {
|
|
|
|
return (
|
2019-12-04 08:41:22 +03:00
|
|
|
<E2ePadlock title={_t("Unencrypted")} icon="unencrypted" {...props} />
|
2017-08-08 14:34:40 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-01-20 20:14:31 +03:00
|
|
|
function E2ePadlockUnknown(props) {
|
2020-01-20 18:16:41 +03:00
|
|
|
return (
|
2020-01-29 18:48:25 +03:00
|
|
|
<E2ePadlock title={_t("Encrypted by a deleted session")} icon="unknown" {...props} />
|
2020-01-20 18:16:41 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-19 04:57:33 +03:00
|
|
|
function E2ePadlockUnauthenticated(props) {
|
|
|
|
return (
|
2021-04-01 16:15:21 +03:00
|
|
|
<E2ePadlock
|
|
|
|
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
|
|
|
|
icon="unauthenticated"
|
|
|
|
{...props}
|
|
|
|
/>
|
2020-06-19 04:57:33 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-04-06 14:26:50 +03:00
|
|
|
interface IE2ePadlockProps {
|
|
|
|
icon: string;
|
|
|
|
title: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IE2ePadlockState {
|
|
|
|
hover: boolean;
|
|
|
|
}
|
2019-12-04 08:41:22 +03:00
|
|
|
|
2021-04-06 14:26:50 +03:00
|
|
|
class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> {
|
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2019-12-04 08:41:22 +03:00
|
|
|
|
|
|
|
this.state = {
|
|
|
|
hover: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
onHoverStart = () => {
|
|
|
|
this.setState({hover: true});
|
|
|
|
};
|
|
|
|
|
|
|
|
onHoverEnd = () => {
|
|
|
|
this.setState({hover: false});
|
|
|
|
};
|
|
|
|
|
|
|
|
render() {
|
|
|
|
let tooltip = null;
|
|
|
|
if (this.state.hover) {
|
2021-04-06 14:26:50 +03:00
|
|
|
tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} />;
|
2019-12-04 08:41:22 +03:00
|
|
|
}
|
2019-12-04 09:15:49 +03:00
|
|
|
|
2020-09-15 07:03:54 +03:00
|
|
|
const classes = `mx_EventTile_e2eIcon mx_EventTile_e2eIcon_${this.props.icon}`;
|
2019-12-04 09:15:49 +03:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={classes}
|
|
|
|
onMouseEnter={this.onHoverStart}
|
|
|
|
onMouseLeave={this.onHoverEnd}
|
|
|
|
>{tooltip}</div>
|
|
|
|
);
|
2018-04-23 05:30:37 +03:00
|
|
|
}
|
2017-08-08 14:34:40 +03:00
|
|
|
}
|
2021-03-10 07:13:23 +03:00
|
|
|
|
|
|
|
interface ISentReceiptProps {
|
|
|
|
messageState: string; // TODO: Types for message sending state
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ISentReceiptState {
|
|
|
|
hover: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
class SentReceipt extends React.PureComponent<ISentReceiptProps, ISentReceiptState> {
|
2021-04-06 14:26:50 +03:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
2021-03-10 07:13:23 +03:00
|
|
|
|
|
|
|
this.state = {
|
|
|
|
hover: false,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
onHoverStart = () => {
|
|
|
|
this.setState({hover: true});
|
|
|
|
};
|
|
|
|
|
|
|
|
onHoverEnd = () => {
|
|
|
|
this.setState({hover: false});
|
|
|
|
};
|
|
|
|
|
|
|
|
render() {
|
|
|
|
const isSent = !this.props.messageState || this.props.messageState === 'sent';
|
2021-04-22 01:16:05 +03:00
|
|
|
const isFailed = this.props.messageState === 'not_sent';
|
2021-03-10 07:13:23 +03:00
|
|
|
const receiptClasses = classNames({
|
|
|
|
'mx_EventTile_receiptSent': isSent,
|
2021-04-22 01:16:05 +03:00
|
|
|
'mx_EventTile_receiptSending': !isSent && !isFailed,
|
2021-03-10 07:13:23 +03:00
|
|
|
});
|
|
|
|
|
2021-04-22 01:16:05 +03:00
|
|
|
let nonCssBadge = null;
|
|
|
|
if (isFailed) {
|
|
|
|
nonCssBadge = <NotificationBadge
|
2021-04-22 01:43:25 +03:00
|
|
|
notification={StaticNotificationState.RED_EXCLAMATION}
|
2021-04-22 01:16:05 +03:00
|
|
|
/>;
|
|
|
|
}
|
|
|
|
|
2021-03-10 07:13:23 +03:00
|
|
|
let tooltip = null;
|
|
|
|
if (this.state.hover) {
|
|
|
|
let label = _t("Sending your message...");
|
|
|
|
if (this.props.messageState === 'encrypting') {
|
|
|
|
label = _t("Encrypting your message...");
|
|
|
|
} else if (isSent) {
|
|
|
|
label = _t("Your message was sent");
|
2021-04-22 01:16:05 +03:00
|
|
|
} else if (isFailed) {
|
|
|
|
label = _t("Failed to send");
|
2021-03-10 07:13:23 +03:00
|
|
|
}
|
|
|
|
// The yOffset is somewhat arbitrary - it just brings the tooltip down to be more associated
|
|
|
|
// with the read receipt.
|
|
|
|
tooltip = <Tooltip className="mx_EventTile_readAvatars_receiptTooltip" label={label} yOffset={20} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
return <span className="mx_EventTile_readAvatars">
|
|
|
|
<span className={receiptClasses} onMouseEnter={this.onHoverStart} onMouseLeave={this.onHoverEnd}>
|
2021-04-22 01:16:05 +03:00
|
|
|
{nonCssBadge}
|
2021-03-10 07:13:23 +03:00
|
|
|
{tooltip}
|
|
|
|
</span>
|
|
|
|
</span>;
|
|
|
|
}
|
|
|
|
}
|