Improve decryption error UI by consolidating error messages and providing instructions when possible (#9544)
* Improve decryption error UI by consolidating error messages and providing instructions when possible * Fix TS strict errors * Rename .scss to .pcss * Avoid accessing clipboard, Cypress doesn't like it * Display DecryptionFailureBar alongside other AuxPanel bars * Add comments * Add small margin off-screen for visible decryption failures * Fix some more TS strict errors * Add unit tests for DecryptionFailureBar * Add button to resend key requests manually * Remove references to matrix-js-sdk crypto internals * Add hysteresis to visible decryption failures * Add comment Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Add comment Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Don't create empty div if we're not showing resend requests button * cancel updateSessions on unmount * Update unit tests * Fix lint and implicit any * Simplify visible event bounds checking * Adjust cypress test descriptions * Add percy snapshots * Update src/components/structures/TimelinePanel.tsx Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> * Add comments on TimelinePanel IState * comment * Add names to percy snapshots * Show Resend Key Requests button when there are sessions that haven't already been requested via this bar * We no longer request keys from senders * update i18n * update expected text in cypress test * don't download keys ourselves, update device info in response to updates from client * fix ts strict errors * visibledecryptionfailures undefined handling * Fix implicitAny errors Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
b728b27435
commit
4724506320
18 changed files with 1779 additions and 165 deletions
|
@ -61,6 +61,7 @@ import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
|||
import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||
import { DecryptionFailureBar } from "../views/rooms/DecryptionFailureBar";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader, { ISearchInfo } from "../views/rooms/RoomHeader";
|
||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
|
@ -220,6 +221,8 @@ export interface IRoomState {
|
|||
threadId?: string;
|
||||
liveTimeline?: EventTimeline;
|
||||
narrow: boolean;
|
||||
// List of undecryptable events currently visible on-screen
|
||||
visibleDecryptionFailures?: MatrixEvent[];
|
||||
}
|
||||
|
||||
interface LocalRoomViewProps {
|
||||
|
@ -412,6 +415,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
timelineRenderingType: TimelineRenderingType.Room,
|
||||
liveTimeline: undefined,
|
||||
narrow: false,
|
||||
visibleDecryptionFailures: [],
|
||||
};
|
||||
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -1166,6 +1170,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
private onEventDecrypted = (ev: MatrixEvent) => {
|
||||
if (!this.state.room || !this.state.matrixClientIsReady) return; // not ready at all
|
||||
if (ev.getRoomId() !== this.state.room.roomId) return; // not for us
|
||||
this.updateVisibleDecryptionFailures();
|
||||
if (ev.isDecryptionFailure()) return;
|
||||
this.handleEffects(ev);
|
||||
};
|
||||
|
@ -1470,7 +1475,21 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onMessageListScroll = (ev) => {
|
||||
private updateVisibleDecryptionFailures = throttle(
|
||||
() =>
|
||||
this.setState((prevState) => ({
|
||||
visibleDecryptionFailures:
|
||||
this.messagePanel?.getVisibleDecryptionFailures(
|
||||
// If there were visible failures last time we checked,
|
||||
// add a margin to provide hysteresis and prevent flickering
|
||||
(prevState.visibleDecryptionFailures?.length ?? 0) > 0,
|
||||
) ?? [],
|
||||
})),
|
||||
500,
|
||||
{ leading: false, trailing: true },
|
||||
);
|
||||
|
||||
private onMessageListScroll = () => {
|
||||
if (this.messagePanel.isAtEndOfLiveTimeline()) {
|
||||
this.setState({
|
||||
numUnreadMessages: 0,
|
||||
|
@ -1482,6 +1501,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
});
|
||||
}
|
||||
this.updateTopUnreadMessagesBar();
|
||||
this.updateVisibleDecryptionFailures();
|
||||
};
|
||||
|
||||
private resetJumpToEvent = (eventId?: string) => {
|
||||
|
@ -2028,7 +2048,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
|
||||
const hiddenHighlightCount = this.getHiddenHighlightCount();
|
||||
|
||||
let aux = null;
|
||||
let aux: JSX.Element | undefined;
|
||||
let previewBar;
|
||||
if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
|
||||
aux = (
|
||||
|
@ -2079,6 +2099,11 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
);
|
||||
}
|
||||
|
||||
let decryptionFailureBar: JSX.Element | undefined;
|
||||
if (this.state.visibleDecryptionFailures && this.state.visibleDecryptionFailures.length > 0) {
|
||||
decryptionFailureBar = <DecryptionFailureBar failures={this.state.visibleDecryptionFailures} />;
|
||||
}
|
||||
|
||||
if (this.state.room?.isSpaceRoom() && !this.props.forceTimeline) {
|
||||
return (
|
||||
<SpaceRoomView
|
||||
|
@ -2103,6 +2128,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
>
|
||||
{aux}
|
||||
{decryptionFailureBar}
|
||||
</AuxPanel>
|
||||
);
|
||||
|
||||
|
|
|
@ -64,6 +64,9 @@ const READ_RECEIPT_INTERVAL_MS = 500;
|
|||
|
||||
const READ_MARKER_DEBOUNCE_MS = 100;
|
||||
|
||||
// How far off-screen a decryption failure can be for it to still count as "visible"
|
||||
const VISIBLE_DECRYPTION_FAILURE_MARGIN = 100;
|
||||
|
||||
const debuglog = (...args: any[]) => {
|
||||
if (SettingsStore.getValue("debug_timeline_panel")) {
|
||||
logger.log.call(console, "TimelinePanel debuglog:", ...args);
|
||||
|
@ -149,7 +152,9 @@ interface IProps {
|
|||
}
|
||||
|
||||
interface IState {
|
||||
// All events, including still-pending events being sent by us
|
||||
events: MatrixEvent[];
|
||||
// Only events that are actually in the live timeline
|
||||
liveEvents: MatrixEvent[];
|
||||
// track whether our room timeline is loading
|
||||
timelineLoading: boolean;
|
||||
|
@ -1690,6 +1695,45 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
return index > -1 ? index : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of undecryptable events currently visible on-screen.
|
||||
*
|
||||
* @param {boolean} addMargin Whether to add an extra margin beyond the viewport
|
||||
* where events are still considered "visible"
|
||||
*
|
||||
* @returns {MatrixEvent[] | null} A list of undecryptable events, or null if
|
||||
* the list of events could not be determined.
|
||||
*/
|
||||
public getVisibleDecryptionFailures(addMargin?: boolean): MatrixEvent[] | null {
|
||||
const messagePanel = this.messagePanel.current;
|
||||
if (!messagePanel) return null;
|
||||
|
||||
const messagePanelNode = ReactDOM.findDOMNode(messagePanel) as Element;
|
||||
if (!messagePanelNode) return null; // sometimes this happens for fresh rooms/post-sync
|
||||
const wrapperRect = messagePanelNode.getBoundingClientRect();
|
||||
const margin = addMargin ? VISIBLE_DECRYPTION_FAILURE_MARGIN : 0;
|
||||
const screenTop = wrapperRect.top - margin;
|
||||
const screenBottom = wrapperRect.bottom + margin;
|
||||
|
||||
const result: MatrixEvent[] = [];
|
||||
for (const ev of this.state.liveEvents) {
|
||||
const eventId = ev.getId();
|
||||
if (!eventId) continue;
|
||||
const node = messagePanel.getNodeForEventId(eventId);
|
||||
if (!node) continue;
|
||||
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
if (boundingRect.top > screenBottom) {
|
||||
// we have gone past the visible section of timeline
|
||||
break;
|
||||
} else if (boundingRect.bottom >= screenTop) {
|
||||
// the tile for this event is in the visible part of the screen (or just above/below it).
|
||||
if (ev.isDecryptionFailure()) result.push(ev);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getLastDisplayedEventIndex(opts: IEventIndexOpts = {}): number | null {
|
||||
const ignoreOwn = opts.ignoreOwn || false;
|
||||
const allowPartial = opts.allowPartial || false;
|
||||
|
@ -1702,7 +1746,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
|
|||
const wrapperRect = messagePanelNode.getBoundingClientRect();
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
|
||||
const isNodeInView = (node) => {
|
||||
const isNodeInView = (node: HTMLElement) => {
|
||||
if (node) {
|
||||
const boundingRect = node.getBoundingClientRect();
|
||||
if (
|
||||
|
|
27
src/components/views/messages/DecryptionFailureBody.tsx
Normal file
27
src/components/views/messages/DecryptionFailureBody.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { IBodyProps } from "./IBodyProps";
|
||||
|
||||
// A placeholder element for messages that could not be decrypted
|
||||
export default class DecryptionFailureBody extends React.Component<Partial<IBodyProps>> {
|
||||
render() {
|
||||
return <div className="mx_DecryptionFailureBody mx_EventTile_content">{_t("Unable to decrypt message")}</div>;
|
||||
}
|
||||
}
|
|
@ -40,6 +40,7 @@ import MPollBody from "./MPollBody";
|
|||
import MLocationBody from "./MLocationBody";
|
||||
import MjolnirBody from "./MjolnirBody";
|
||||
import MBeaconBody from "./MBeaconBody";
|
||||
import DecryptionFailureBody from "./DecryptionFailureBody";
|
||||
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
|
||||
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";
|
||||
|
||||
|
@ -152,7 +153,9 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
|
|||
let BodyType: React.ComponentType<Partial<IBodyProps>> | ReactAnyComponent = RedactedBody;
|
||||
if (!this.props.mxEvent.isRedacted()) {
|
||||
// only resolve BodyType if event is not redacted
|
||||
if (type && this.evTypes.has(type)) {
|
||||
if (this.props.mxEvent.isDecryptionFailure()) {
|
||||
BodyType = DecryptionFailureBody;
|
||||
} else if (type && this.evTypes.has(type)) {
|
||||
BodyType = this.evTypes.get(type);
|
||||
} else if (msgtype && this.bodyTypes.has(msgtype)) {
|
||||
BodyType = this.bodyTypes.get(msgtype);
|
||||
|
|
238
src/components/views/rooms/DecryptionFailureBar.tsx
Normal file
238
src/components/views/rooms/DecryptionFailureBar.tsx
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
|
||||
import Modal from "../../../Modal";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { OpenToTabPayload } from "../../../dispatcher/payloads/OpenToTabPayload";
|
||||
import { UserTab } from "../dialogs/UserTab";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import SetupEncryptionDialog from "../dialogs/security/SetupEncryptionDialog";
|
||||
import { SetupEncryptionStore } from "../../../stores/SetupEncryptionStore";
|
||||
import Spinner from "../elements/Spinner";
|
||||
|
||||
interface IProps {
|
||||
failures: MatrixEvent[];
|
||||
}
|
||||
|
||||
// Number of milliseconds to display a loading spinner before prompting the user for action
|
||||
const WAIT_PERIOD = 5000;
|
||||
|
||||
export const DecryptionFailureBar: React.FC<IProps> = ({ failures }) => {
|
||||
const context = useContext(MatrixClientContext);
|
||||
|
||||
// Display a spinner for a few seconds before presenting an error message,
|
||||
// in case keys are about to arrive
|
||||
const [waiting, setWaiting] = useState<boolean>(true);
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => setWaiting(false), WAIT_PERIOD);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
// Is this device unverified?
|
||||
const [needsVerification, setNeedsVerification] = useState<boolean>(false);
|
||||
// Does this user have verified devices other than this device?
|
||||
const [hasOtherVerifiedDevices, setHasOtherVerifiedDevices] = useState<boolean>(false);
|
||||
// Does this user have key backups?
|
||||
const [hasKeyBackup, setHasKeyBackup] = useState<boolean>(false);
|
||||
|
||||
// Keep track of session IDs that the user has sent key
|
||||
// requests for using the Resend Key Requests button
|
||||
const [requestedSessions, setRequestedSessions] = useState<Set<string>>(new Set());
|
||||
// Keep track of whether there are any sessions the user has not yet sent requests for
|
||||
const [anyUnrequestedSessions, setAnyUnrequestedSessions] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
setAnyUnrequestedSessions(
|
||||
failures.some((event) => {
|
||||
const sessionId = event.getWireContent().session_id;
|
||||
return sessionId && !requestedSessions.has(sessionId);
|
||||
}),
|
||||
);
|
||||
}, [failures, requestedSessions, setAnyUnrequestedSessions]);
|
||||
|
||||
// Send key requests for any sessions that we haven't previously
|
||||
// sent requests for. This is important if, for instance, we
|
||||
// failed to decrypt a message because a key was withheld (in
|
||||
// which case, we wouldn't bother requesting the key), but have
|
||||
// since verified our device. In that case, now that the device is
|
||||
// verified, other devices might be willing to share the key with us
|
||||
// now.
|
||||
const sendKeyRequests = useCallback(() => {
|
||||
const newRequestedSessions = new Set(requestedSessions);
|
||||
|
||||
for (const event of failures) {
|
||||
const sessionId = event.getWireContent().session_id;
|
||||
if (!sessionId || newRequestedSessions.has(sessionId)) continue;
|
||||
newRequestedSessions.add(sessionId);
|
||||
context.cancelAndResendEventRoomKeyRequest(event);
|
||||
}
|
||||
setRequestedSessions(newRequestedSessions);
|
||||
}, [context, requestedSessions, setRequestedSessions, failures]);
|
||||
|
||||
// Recheck which devices are verified and whether we have key backups
|
||||
const updateDeviceInfo = useCallback(async () => {
|
||||
const deviceId = context.getDeviceId()!;
|
||||
let verified = true; // if we can't get a clear answer, don't bug the user about verifying
|
||||
try {
|
||||
verified = context.checkIfOwnDeviceCrossSigned(deviceId);
|
||||
} catch (e) {
|
||||
console.error("Error getting device cross-signing info", e);
|
||||
}
|
||||
setNeedsVerification(!verified);
|
||||
|
||||
let otherVerifiedDevices = false;
|
||||
try {
|
||||
const devices = context.getStoredDevicesForUser(context.getUserId()!);
|
||||
otherVerifiedDevices = devices.some(
|
||||
(device) => device.deviceId !== deviceId && context.checkIfOwnDeviceCrossSigned(device.deviceId),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("Error getting info about other devices", e);
|
||||
}
|
||||
setHasOtherVerifiedDevices(otherVerifiedDevices);
|
||||
|
||||
let keyBackup = false;
|
||||
try {
|
||||
const keys = await context.isSecretStored("m.cross_signing.master");
|
||||
keyBackup = keys !== null && Object.keys(keys).length > 0;
|
||||
} catch (e) {
|
||||
console.error("Error getting info about key backups", e);
|
||||
}
|
||||
setHasKeyBackup(keyBackup);
|
||||
}, [context]);
|
||||
|
||||
// Update our device info on initial render, and continue updating
|
||||
// it whenever the client has an update
|
||||
useEffect(() => {
|
||||
updateDeviceInfo().catch(console.error);
|
||||
context.on(CryptoEvent.DevicesUpdated, updateDeviceInfo);
|
||||
return () => {
|
||||
context.off(CryptoEvent.DevicesUpdated, updateDeviceInfo);
|
||||
};
|
||||
}, [context, updateDeviceInfo]);
|
||||
|
||||
const onVerifyClick = (): void => {
|
||||
Modal.createDialog(SetupEncryptionDialog);
|
||||
};
|
||||
|
||||
const onDeviceListClick = (): void => {
|
||||
const payload: OpenToTabPayload = { action: Action.ViewUserSettings, initialTabId: UserTab.Security };
|
||||
defaultDispatcher.dispatch(payload);
|
||||
};
|
||||
|
||||
const onResetClick = (): void => {
|
||||
const store = SetupEncryptionStore.sharedInstance();
|
||||
store.resetConfirm();
|
||||
};
|
||||
|
||||
const statusIndicator = waiting ? <Spinner /> : <div className="mx_DecryptionFailureBar_icon" />;
|
||||
|
||||
let headline: JSX.Element;
|
||||
let body: JSX.Element;
|
||||
let button = <React.Fragment />;
|
||||
if (waiting) {
|
||||
headline = <React.Fragment>{_t("Decrypting messages...")}</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
{_t("Please wait as we try to decrypt your messages. This may take a few moments.")}
|
||||
</React.Fragment>
|
||||
);
|
||||
} else if (needsVerification) {
|
||||
if (hasOtherVerifiedDevices || hasKeyBackup) {
|
||||
headline = <React.Fragment>{_t("Verify this device to access all messages")}</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
{_t("This device was unable to decrypt some messages because it has not been verified yet.")}
|
||||
</React.Fragment>
|
||||
);
|
||||
button = (
|
||||
<AccessibleButton kind="primary" onClick={onVerifyClick}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
headline = <React.Fragment>{_t("Reset your keys to prevent future decryption errors")}</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
{_t(
|
||||
"You will not be able to access old undecryptable messages, " +
|
||||
"but resetting your keys will allow you to receive new messages.",
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
button = (
|
||||
<AccessibleButton kind="primary" onClick={onResetClick}>
|
||||
{_t("Reset")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
}
|
||||
} else if (hasOtherVerifiedDevices) {
|
||||
headline = <React.Fragment>{_t("Open another device to load encrypted messages")}</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
{_t(
|
||||
"This device is requesting decryption keys from your other devices. " +
|
||||
"Opening one of your other devices may speed this up.",
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
button = (
|
||||
<AccessibleButton kind="primary_outline" onClick={onDeviceListClick}>
|
||||
{_t("View your device list")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
} else {
|
||||
headline = <React.Fragment>{_t("Some messages could not be decrypted")}</React.Fragment>;
|
||||
body = (
|
||||
<React.Fragment>
|
||||
{_t(
|
||||
"Unfortunately, there are no other verified devices to request decryption keys from. " +
|
||||
"Signing in and verifying other devices may help avoid this situation in the future.",
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
let keyRequestButton = <React.Fragment />;
|
||||
if (!needsVerification && hasOtherVerifiedDevices && anyUnrequestedSessions) {
|
||||
keyRequestButton = (
|
||||
<div className="mx_DecryptionFailureBar_button">
|
||||
<AccessibleButton kind="primary" onClick={sendKeyRequests}>
|
||||
{_t("Resend key requests")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_DecryptionFailureBar">
|
||||
{statusIndicator}
|
||||
<div className="mx_DecryptionFailureBar_message">
|
||||
<div className="mx_DecryptionFailureBar_message_headline">{headline}</div>
|
||||
<div className="mx_DecryptionFailureBar_message_body">{body}</div>
|
||||
</div>
|
||||
<div className="mx_DecryptionFailureBar_button">{button}</div>
|
||||
{keyRequestButton}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -55,7 +55,6 @@ import PlatformPeg from "../../../PlatformPeg";
|
|||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import SenderProfile from "../messages/SenderProfile";
|
||||
import MessageTimestamp from "../messages/MessageTimestamp";
|
||||
import TooltipButton from "../elements/TooltipButton";
|
||||
import { IReadReceiptInfo } from "./ReadReceiptMarker";
|
||||
import MessageActionBar from "../messages/MessageActionBar";
|
||||
import ReactionsRow from "../messages/ReactionsRow";
|
||||
|
@ -70,7 +69,7 @@ import { ThreadNotificationState } from "../../../stores/notifications/ThreadNot
|
|||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import { NotificationColor } from "../../../stores/notifications/NotificationColor";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { copyPlaintext, getSelectedText } from "../../../utils/strings";
|
||||
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||
import RedactedBody from "../messages/RedactedBody";
|
||||
|
@ -234,8 +233,6 @@ interface IState {
|
|||
actionBarFocused: 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 | null | undefined;
|
||||
|
||||
|
@ -283,8 +280,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
actionBarFocused: 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(),
|
||||
// Context menu position
|
||||
|
@ -758,20 +753,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
});
|
||||
};
|
||||
|
||||
private 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.
|
||||
MatrixClientPeg.get().cancelAndResendEventRoomKeyRequest(this.props.mxEvent);
|
||||
};
|
||||
|
||||
private onPermalinkClicked = (e) => {
|
||||
private onPermalinkClicked = (e: MouseEvent) => {
|
||||
// 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();
|
||||
|
@ -789,11 +771,11 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
const ev = this.props.mxEvent;
|
||||
|
||||
// no icon for local rooms
|
||||
if (isLocalRoom(ev.getRoomId())) return;
|
||||
if (isLocalRoom(ev.getRoomId()!)) return;
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === "m.bad.encrypted") {
|
||||
return <E2ePadlockUndecryptable />;
|
||||
if (ev.isDecryptionFailure()) {
|
||||
return <E2ePadlockDecryptionFailure />;
|
||||
}
|
||||
|
||||
// event is encrypted and not redacted, display padlock corresponding to whether or not it is verified
|
||||
|
@ -1160,55 +1142,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
|
||||
const timestamp = showTimestamp && ts ? messageTimestamp : null;
|
||||
|
||||
const keyRequestHelpText = (
|
||||
<div className="mx_EventTile_keyRequestInfo_tooltip_contents">
|
||||
<p>
|
||||
{this.state.previouslyRequestedKeys
|
||||
? _t(
|
||||
"Your key share request has been sent - please check your other sessions " +
|
||||
"for key share requests.",
|
||||
)
|
||||
: _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.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{_t(
|
||||
"If your other sessions do not have the key for this message you will not " +
|
||||
"be able to decrypt them.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
const keyRequestInfoContent = this.state.previouslyRequestedKeys
|
||||
? _t("Key request sent.")
|
||||
: _t(
|
||||
"<requestLink>Re-request encryption keys</requestLink> from your other sessions.",
|
||||
{},
|
||||
{
|
||||
requestLink: (sub) => (
|
||||
<AccessibleButton
|
||||
className="mx_EventTile_rerequestKeysCta"
|
||||
kind="link_inline"
|
||||
tabIndex={0}
|
||||
onClick={this.onRequestKeysClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const keyRequestInfo =
|
||||
isEncryptionFailure && !isRedacted ? (
|
||||
<div className="mx_EventTile_keyRequestInfo">
|
||||
<span className="mx_EventTile_keyRequestInfo_text">{keyRequestInfoContent}</span>
|
||||
<TooltipButton helpText={keyRequestHelpText} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
let reactionsRow;
|
||||
if (!isRedacted) {
|
||||
reactionsRow = (
|
||||
|
@ -1543,7 +1476,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||
},
|
||||
this.context.showHiddenEvents,
|
||||
)}
|
||||
{keyRequestInfo}
|
||||
{actionBar}
|
||||
{this.props.layout === Layout.IRC && (
|
||||
<>
|
||||
|
@ -1578,23 +1510,19 @@ const SafeEventTile = forwardRef((props: EventTileProps, ref: RefObject<Unwrappe
|
|||
});
|
||||
export default SafeEventTile;
|
||||
|
||||
function E2ePadlockUndecryptable(props) {
|
||||
return <E2ePadlock title={_t("This message cannot be decrypted")} icon={E2ePadlockIcon.Warning} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnverified(props) {
|
||||
function E2ePadlockUnverified(props: Omit<IE2ePadlockProps, "title" | "icon">) {
|
||||
return <E2ePadlock title={_t("Encrypted by an unverified session")} icon={E2ePadlockIcon.Warning} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnencrypted(props) {
|
||||
function E2ePadlockUnencrypted(props: Omit<IE2ePadlockProps, "title" | "icon">) {
|
||||
return <E2ePadlock title={_t("Unencrypted")} icon={E2ePadlockIcon.Warning} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnknown(props) {
|
||||
function E2ePadlockUnknown(props: Omit<IE2ePadlockProps, "title" | "icon">) {
|
||||
return <E2ePadlock title={_t("Encrypted by a deleted session")} icon={E2ePadlockIcon.Normal} {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockUnauthenticated(props) {
|
||||
function E2ePadlockUnauthenticated(props: Omit<IE2ePadlockProps, "title" | "icon">) {
|
||||
return (
|
||||
<E2ePadlock
|
||||
title={_t("The authenticity of this encrypted message can't be guaranteed on this device.")}
|
||||
|
@ -1604,9 +1532,20 @@ function E2ePadlockUnauthenticated(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockDecryptionFailure(props: Omit<IE2ePadlockProps, "title" | "icon">) {
|
||||
return (
|
||||
<E2ePadlock
|
||||
title={_t("This message could not be decrypted")}
|
||||
icon={E2ePadlockIcon.DecryptionFailure}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
enum E2ePadlockIcon {
|
||||
Normal = "normal",
|
||||
Warning = "warning",
|
||||
DecryptionFailure = "decryption_failure",
|
||||
}
|
||||
|
||||
interface IE2ePadlockProps {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue