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
235
cypress/e2e/crypto/decryption-failure.spec.ts
Normal file
235
cypress/e2e/crypto/decryption-failure.spec.ts
Normal file
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
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 type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import { UserCredentials } from "../../support/login";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
const ROOM_NAME = "Test room";
|
||||
const TEST_USER = "Alia";
|
||||
const BOT_USER = "Benjamin";
|
||||
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
|
||||
const waitForVerificationRequest = (cli: MatrixClient): Promise<VerificationRequest> => {
|
||||
return new Promise<VerificationRequest>((resolve) => {
|
||||
const onVerificationRequestEvent = (request: VerificationRequest) => {
|
||||
// @ts-ignore CryptoEvent is not exported to window.matrixcs; using the string value here
|
||||
cli.off("crypto.verification.request", onVerificationRequestEvent);
|
||||
resolve(request);
|
||||
};
|
||||
// @ts-ignore
|
||||
cli.on("crypto.verification.request", onVerificationRequestEvent);
|
||||
});
|
||||
};
|
||||
|
||||
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
||||
return cy.wrap(
|
||||
new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
verifier.off("show_sas", onShowSas);
|
||||
event.confirm();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
const verifier = request.beginKeyVerification("m.sas.v1");
|
||||
verifier.on("show_sas", onShowSas);
|
||||
verifier.verify();
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
describe("Decryption Failure Bar", () => {
|
||||
let synapse: SynapseInstance | undefined;
|
||||
let testUser: UserCredentials | undefined;
|
||||
let bot: MatrixClient | undefined;
|
||||
let roomId: string;
|
||||
|
||||
beforeEach(function () {
|
||||
cy.startSynapse("default").then((syn: SynapseInstance) => {
|
||||
synapse = syn;
|
||||
cy.initTestUser(synapse, TEST_USER)
|
||||
.then((creds: UserCredentials) => {
|
||||
testUser = creds;
|
||||
})
|
||||
.then(() => {
|
||||
cy.getBot(synapse, { displayName: BOT_USER }).then((cli) => {
|
||||
bot = cli;
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
cy.createRoom({ name: ROOM_NAME }).then((id) => {
|
||||
roomId = id;
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit("/#/room/" + roomId);
|
||||
cy.contains(".mx_TextualEvent", BOT_USER + " joined the room").should("exist");
|
||||
})
|
||||
.then(() => {
|
||||
cy.getClient()
|
||||
.then(async (cli) => {
|
||||
await cli.setRoomEncryption(roomId, { algorithm: "m.megolm.v1.aes-sha2" });
|
||||
await bot.setRoomEncryption(roomId, { algorithm: "m.megolm.v1.aes-sha2" });
|
||||
})
|
||||
.then(() => {
|
||||
bot.getRoom(roomId).setBlacklistUnverifiedDevices(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
});
|
||||
|
||||
it(
|
||||
"should prompt the user to verify, if this device isn't verified " +
|
||||
"and there are other verified devices or backups",
|
||||
() => {
|
||||
let otherDevice: MatrixClient | undefined;
|
||||
cy.loginBot(synapse, testUser.username, testUser.password, {})
|
||||
.then(async (cli) => {
|
||||
otherDevice = cli;
|
||||
await otherDevice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
await makeRequest({});
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
cy.wait(5000);
|
||||
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
|
||||
"have.text",
|
||||
"Verify this device to access all messages",
|
||||
);
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar prompts user to verify");
|
||||
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Verify").click();
|
||||
|
||||
const verificationRequestPromise = waitForVerificationRequest(otherDevice);
|
||||
cy.get(".mx_CompleteSecurity_actionRow .mx_AccessibleButton").click();
|
||||
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
|
||||
cy.wrap(verificationRequest.accept());
|
||||
handleVerificationRequest(verificationRequest).then((emojis) => {
|
||||
cy.get(".mx_VerificationShowSas_emojiSas_block").then((emojiBlocks) => {
|
||||
emojis.forEach((emoji: EmojiMapping, index: number) => {
|
||||
expect(emojiBlocks[index].textContent.toLowerCase()).to.eq(emoji[0] + emoji[1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
cy.contains(".mx_AccessibleButton", "They match").click();
|
||||
cy.get(".mx_VerificationPanel_verified_section .mx_E2EIcon_verified").should("exist");
|
||||
cy.contains(".mx_AccessibleButton", "Got it").click();
|
||||
|
||||
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
|
||||
"have.text",
|
||||
"Open another device to load encrypted messages",
|
||||
);
|
||||
|
||||
cy.percySnapshot(
|
||||
"DecryptionFailureBar prompts user to open another device, with Resend Key Requests button",
|
||||
);
|
||||
|
||||
cy.intercept("/_matrix/client/r0/sendToDevice/m.room_key_request/*").as("keyRequest");
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").click();
|
||||
cy.wait("@keyRequest");
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Resend key requests").should("not.exist");
|
||||
|
||||
cy.percySnapshot(
|
||||
"DecryptionFailureBar prompts user to open another device, " + "without Resend Key Requests button",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it(
|
||||
"should prompt the user to reset keys, if this device isn't verified " +
|
||||
"and there are no other verified devices or backups",
|
||||
() => {
|
||||
cy.loginBot(synapse, testUser.username, testUser.password, {}).then(async (cli) => {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
await makeRequest({});
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
await cli.logout(true);
|
||||
});
|
||||
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
cy.wait(5000);
|
||||
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
|
||||
"have.text",
|
||||
"Reset your keys to prevent future decryption errors",
|
||||
);
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar prompts user to reset keys");
|
||||
|
||||
cy.contains(".mx_DecryptionFailureBar_button", "Reset").click();
|
||||
|
||||
cy.get(".mx_Dialog").within(() => {
|
||||
cy.contains(".mx_Dialog_primary", "Continue").click();
|
||||
cy.get(".mx_CreateSecretStorageDialog_recoveryKey code").invoke("text").as("securityKey");
|
||||
// Clicking download instead of Copy because of https://github.com/cypress-io/cypress/issues/2851
|
||||
cy.contains(".mx_AccessibleButton", "Download").click();
|
||||
cy.contains(".mx_Dialog_primary:not([disabled])", "Continue").click();
|
||||
});
|
||||
|
||||
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should(
|
||||
"have.text",
|
||||
"Some messages could not be decrypted",
|
||||
);
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar displays general message with no call to action");
|
||||
},
|
||||
);
|
||||
|
||||
it("should appear and disappear as undecryptable messages enter and leave view", () => {
|
||||
cy.getClient().then((cli) => {
|
||||
for (let i = 0; i < 25; i++) {
|
||||
cy.botSendMessage(cli, roomId, `test ${i}`);
|
||||
}
|
||||
});
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
cy.get(".mx_DecryptionFailureBar").should("exist");
|
||||
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("exist");
|
||||
|
||||
cy.percySnapshot("DecryptionFailureBar displays loading spinner");
|
||||
|
||||
cy.wait(5000);
|
||||
cy.get(".mx_DecryptionFailureBar .mx_Spinner").should("not.exist");
|
||||
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_icon").should("exist");
|
||||
|
||||
cy.get(".mx_RoomView_messagePanel").scrollTo("top");
|
||||
cy.get(".mx_DecryptionFailureBar").should("not.exist");
|
||||
|
||||
cy.botSendMessage(bot, roomId, "another test");
|
||||
cy.get(".mx_DecryptionFailureBar").should("not.exist");
|
||||
|
||||
cy.get(".mx_RoomView_messagePanel").scrollTo("bottom");
|
||||
cy.get(".mx_DecryptionFailureBar").should("exist");
|
||||
});
|
||||
});
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
|
||||
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
||||
import { Credentials } from "./synapse";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
interface CreateBotOpts {
|
||||
|
@ -33,11 +34,16 @@ interface CreateBotOpts {
|
|||
* Whether or not to start the syncing client.
|
||||
*/
|
||||
startClient?: boolean;
|
||||
/**
|
||||
* Whether or not to generate cross-signing keys
|
||||
*/
|
||||
bootstrapCrossSigning?: boolean;
|
||||
}
|
||||
|
||||
const defaultCreateBotOptions = {
|
||||
autoAcceptInvites: true,
|
||||
startClient: true,
|
||||
bootstrapCrossSigning: true,
|
||||
} as CreateBotOpts;
|
||||
|
||||
declare global {
|
||||
|
@ -50,6 +56,19 @@ declare global {
|
|||
* @param opts create bot options
|
||||
*/
|
||||
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient>;
|
||||
/**
|
||||
* Returns a new Bot instance logged in as an existing user
|
||||
* @param synapse the instance on which to register the bot user
|
||||
* @param username the username for the bot to log in with
|
||||
* @param password the password for the bot to log in with
|
||||
* @param opts create bot options
|
||||
*/
|
||||
loginBot(
|
||||
synapse: SynapseInstance,
|
||||
username: string,
|
||||
password: string,
|
||||
opts: CreateBotOpts,
|
||||
): Chainable<MatrixClient>;
|
||||
/**
|
||||
* Let a bot join a room
|
||||
* @param cli The bot's MatrixClient
|
||||
|
@ -73,13 +92,23 @@ declare global {
|
|||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient> => {
|
||||
function setupBotClient(
|
||||
synapse: SynapseInstance,
|
||||
credentials: Credentials,
|
||||
opts: CreateBotOpts,
|
||||
): Chainable<MatrixClient> {
|
||||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => {
|
||||
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
const keys = {};
|
||||
|
||||
const getCrossSigningKey = (type: string) => {
|
||||
return keys[type];
|
||||
};
|
||||
|
||||
const saveCrossSigningKeys = (k: Record<string, Uint8Array>) => {
|
||||
Object.assign(keys, k);
|
||||
};
|
||||
|
||||
const cli = new win.matrixcs.MatrixClient({
|
||||
baseUrl: synapse.baseUrl,
|
||||
userId: credentials.userId,
|
||||
|
@ -88,6 +117,7 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
|||
store: new win.matrixcs.MemoryStore(),
|
||||
scheduler: new win.matrixcs.MatrixScheduler(),
|
||||
cryptoStore: new win.matrixcs.MemoryCryptoStore(),
|
||||
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||
});
|
||||
|
||||
if (opts.autoAcceptInvites) {
|
||||
|
@ -107,19 +137,40 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
|||
.initCrypto()
|
||||
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
||||
.then(() => cli.startClient())
|
||||
.then(() =>
|
||||
cli.bootstrapCrossSigning({
|
||||
.then(async () => {
|
||||
if (opts.bootstrapCrossSigning) {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
},
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(() => cli),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<MatrixClient> => {
|
||||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||
const username = Cypress._.uniqueId("userId_");
|
||||
const password = Cypress._.uniqueId("password_");
|
||||
return cy.registerUser(synapse, username, password, opts.displayName).then((credentials) => {
|
||||
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
||||
return setupBotClient(synapse, credentials, opts);
|
||||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add(
|
||||
"loginBot",
|
||||
(synapse: SynapseInstance, username: string, password: string, opts: CreateBotOpts): Chainable<MatrixClient> => {
|
||||
opts = Object.assign({}, defaultCreateBotOptions, { bootstrapCrossSigning: false }, opts);
|
||||
return cy.loginUser(synapse, username, password).then((credentials) => {
|
||||
return setupBotClient(synapse, credentials, opts);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Cypress.Commands.add("botJoinRoom", (cli: MatrixClient, roomId: string): Chainable<Room> => {
|
||||
return cy.wrap(cli.joinRoom(roomId));
|
||||
});
|
||||
|
|
|
@ -69,7 +69,7 @@ function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
|
|||
});
|
||||
}
|
||||
|
||||
interface Credentials {
|
||||
export interface Credentials {
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
|
|
|
@ -218,6 +218,7 @@
|
|||
@import "./views/messages/_CallEvent.pcss";
|
||||
@import "./views/messages/_CreateEvent.pcss";
|
||||
@import "./views/messages/_DateSeparator.pcss";
|
||||
@import "./views/messages/_DecryptionFailureBody.pcss";
|
||||
@import "./views/messages/_DisambiguatedProfile.pcss";
|
||||
@import "./views/messages/_EventTileBubble.pcss";
|
||||
@import "./views/messages/_HiddenBody.pcss";
|
||||
|
@ -260,6 +261,7 @@
|
|||
@import "./views/rooms/_Autocomplete.pcss";
|
||||
@import "./views/rooms/_AuxPanel.pcss";
|
||||
@import "./views/rooms/_BasicMessageComposer.pcss";
|
||||
@import "./views/rooms/_DecryptionFailureBar.pcss";
|
||||
@import "./views/rooms/_E2EIcon.pcss";
|
||||
@import "./views/rooms/_EditMessageComposer.pcss";
|
||||
@import "./views/rooms/_EmojiButton.pcss";
|
||||
|
|
20
res/css/views/messages/_DecryptionFailureBody.pcss
Normal file
20
res/css/views/messages/_DecryptionFailureBody.pcss
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_DecryptionFailureBody {
|
||||
color: $secondary-content;
|
||||
font-style: italic;
|
||||
}
|
60
res/css/views/rooms/_DecryptionFailureBar.pcss
Normal file
60
res/css/views/rooms/_DecryptionFailureBar.pcss
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_DecryptionFailureBar {
|
||||
background-color: $system;
|
||||
padding: $spacing-12;
|
||||
margin-left: $spacing-16;
|
||||
margin-right: $spacing-16;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: $spacing-12;
|
||||
}
|
||||
|
||||
.mx_DecryptionFailureBar_icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
mask-image: url("$(res)/img/e2e/decryption-failure.svg");
|
||||
background-color: $alert;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
.mx_DecryptionFailureBar_icon,
|
||||
.mx_DecryptionFailureBar .mx_Spinner {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.mx_DecryptionFailureBar_message {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.mx_DecryptionFailureBar_message_headline {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-16px;
|
||||
margin-bottom: $spacing-4;
|
||||
}
|
||||
|
||||
.mx_DecryptionFailureBar_message_body {
|
||||
color: $secondary-content;
|
||||
}
|
||||
|
||||
.mx_DecryptionFailureBar_button {
|
||||
flex-shrink: 0;
|
||||
}
|
|
@ -478,11 +478,9 @@ limitations under the License.
|
|||
grid-template:
|
||||
"reply reply" auto
|
||||
"shield body" auto
|
||||
"shield link" auto
|
||||
/ auto 1fr;
|
||||
|
||||
.mx_UnknownBody,
|
||||
.mx_EventTile_keyRequestInfo,
|
||||
.mx_ReplyChain_wrapper,
|
||||
.mx_ViewSourceEvent {
|
||||
min-width: 0; /* Prevent a grid blowout */
|
||||
|
@ -490,16 +488,15 @@ limitations under the License.
|
|||
|
||||
.mx_EventTile_e2eIcon {
|
||||
grid-area: shield;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.mx_UnknownBody {
|
||||
.mx_UnknownBody,
|
||||
.mx_DecryptionFailureBody {
|
||||
grid-area: body;
|
||||
}
|
||||
|
||||
.mx_EventTile_keyRequestInfo {
|
||||
grid-area: link;
|
||||
}
|
||||
|
||||
.mx_ReplyChain_wrapper {
|
||||
grid-area: reply;
|
||||
}
|
||||
|
@ -512,7 +509,6 @@ limitations under the License.
|
|||
align-items: center;
|
||||
grid-template:
|
||||
"shield source" auto
|
||||
"shield link" auto
|
||||
/ auto 1fr;
|
||||
|
||||
.mx_ViewSourceEvent {
|
||||
|
|
|
@ -685,6 +685,11 @@ $left-gutter: 64px;
|
|||
mask-image: url("$(res)/img/e2e/normal.svg");
|
||||
background-color: $header-panel-text-primary-color;
|
||||
}
|
||||
|
||||
&.mx_EventTile_e2eIcon_decryption_failure::after {
|
||||
mask-image: url("$(res)/img/e2e/decryption-failure.svg");
|
||||
background-color: $secondary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_body {
|
||||
|
@ -793,33 +798,6 @@ $left-gutter: 64px;
|
|||
mask-image: url("$(res)/img/element-icons/maximise-expand.svg");
|
||||
}
|
||||
|
||||
.mx_EventTile_keyRequestInfo {
|
||||
font-size: $font-12px;
|
||||
|
||||
.mx_EventTile_keyRequestInfo_text {
|
||||
opacity: 0.5;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
color: $primary-content;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_keyRequestInfo_tooltip_contents p {
|
||||
text-align: auto;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EventTile_tileError {
|
||||
color: red;
|
||||
text-align: center;
|
||||
|
|
3
res/img/e2e/decryption-failure.svg
Normal file
3
res/img/e2e/decryption-failure.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" fill="#000000" d="m 13.693359,4.8320312 c -0.831702,0.014848 -1.62869,0.3142589 -2.236328,0.921875 -1.205391,1.2054645 -1.19044,3.1514722 -0.164062,4.6464848 l -2.9101565,2.96875 -0.2617187,-0.269532 -0.00195,-0.002 C 8.0071963,12.985739 7.8869254,12.852667 7.6484375,12.767578 7.4099496,12.68249 7.0329991,12.769696 6.8398438,12.962891 L 5.8964844,13.90625 c -0.3865104,0.386392 -0.3518945,1.031029 0.015625,1.398438 l 0.2890625,0.289062 -0.1640625,0.167969 c -0.1940263,0.194049 -0.3312306,0.444035 -0.3574219,0.730469 -0.026191,0.286433 0.082836,0.602331 0.3027344,0.822265 0.2199669,0.219905 0.5630302,0.325807 0.84375,0.273438 0.2807198,-0.05237 0.4907236,-0.203587 0.6621093,-0.375 L 13.003906,11.896484 C 14.40572,12.59862 16.0549,12.487976 17.123047,11.419922 18.511821,10.031028 18.283868,7.6569034 16.751953,6.125 15.986,5.3590578 15.009306,4.9188574 14.050781,4.84375 c -0.119815,-0.00939 -0.238607,-0.01384 -0.357422,-0.011719 z m 0.111329,1.7011719 c 0.064,9.655e-4 0.128025,0.00455 0.193359,0.011719 0.522674,0.05732 1.080601,0.3130273 1.550781,0.7832031 0.940357,0.9403487 1.026123,2.2336423 0.371094,2.888672 C 15.26488,10.871761 13.971607,10.786061 13.03125,9.8457031 12.090894,8.905364 12.005137,7.6120286 12.660156,6.9570312 12.946738,6.670459 13.356711,6.5264448 13.804688,6.5332031 Z M 22,11 A 11,11 0 0 1 11,22 11,11 0 0 1 0,11 11,11 0 0 1 11,0 11,11 0 0 1 22,11 Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -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 {
|
||||
|
|
|
@ -1850,6 +1850,18 @@
|
|||
"Remove %(phone)s?": "Remove %(phone)s?",
|
||||
"A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.": "A text message has been sent to +%(msisdn)s. Please enter the verification code it contains.",
|
||||
"Phone Number": "Phone Number",
|
||||
"Decrypting messages...": "Decrypting messages...",
|
||||
"Please wait as we try to decrypt your messages. This may take a few moments.": "Please wait as we try to decrypt your messages. This may take a few moments.",
|
||||
"Verify this device to access all messages": "Verify this device to access all messages",
|
||||
"This device was unable to decrypt some messages because it has not been verified yet.": "This device was unable to decrypt some messages because it has not been verified yet.",
|
||||
"Reset your keys to prevent future decryption errors": "Reset your keys to prevent future decryption errors",
|
||||
"You will not be able to access old undecryptable messages, but resetting your keys will allow you to receive new messages.": "You will not be able to access old undecryptable messages, but resetting your keys will allow you to receive new messages.",
|
||||
"Open another device to load encrypted messages": "Open another device to load encrypted messages",
|
||||
"This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.": "This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.",
|
||||
"View your device list": "View your device list",
|
||||
"Some messages could not be decrypted": "Some messages could not be decrypted",
|
||||
"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.": "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.",
|
||||
"Resend key requests": "Resend key requests",
|
||||
"This user has not verified all of their sessions.": "This user has not verified all of their sessions.",
|
||||
"You have not verified this user.": "You have not verified this user.",
|
||||
"You have verified this user. This user has verified all of their sessions.": "You have verified this user. This user has verified all of their sessions.",
|
||||
|
@ -1861,19 +1873,14 @@
|
|||
"Mod": "Mod",
|
||||
"From a thread": "From a thread",
|
||||
"This event could not be displayed": "This event could not be displayed",
|
||||
"Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.",
|
||||
"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.": "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.",
|
||||
"If your other sessions do not have the key for this message you will not be able to decrypt them.": "If your other sessions do not have the key for this message you will not be able to decrypt them.",
|
||||
"Key request sent.": "Key request sent.",
|
||||
"<requestLink>Re-request encryption keys</requestLink> from your other sessions.": "<requestLink>Re-request encryption keys</requestLink> from your other sessions.",
|
||||
"Message Actions": "Message Actions",
|
||||
"View in room": "View in room",
|
||||
"Copy link to thread": "Copy link to thread",
|
||||
"This message cannot be decrypted": "This message cannot be decrypted",
|
||||
"Encrypted by an unverified session": "Encrypted by an unverified session",
|
||||
"Unencrypted": "Unencrypted",
|
||||
"Encrypted by a deleted session": "Encrypted by a deleted session",
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.": "The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
"This message could not be decrypted": "This message could not be decrypted",
|
||||
"Sending your message...": "Sending your message...",
|
||||
"Encrypting your message...": "Encrypting your message...",
|
||||
"Your message was sent": "Your message was sent",
|
||||
|
@ -2286,6 +2293,7 @@
|
|||
"Last month": "Last month",
|
||||
"The beginning of the room": "The beginning of the room",
|
||||
"Jump to date": "Jump to date",
|
||||
"Unable to decrypt message": "Unable to decrypt message",
|
||||
"Downloading": "Downloading",
|
||||
"Decrypting": "Decrypting",
|
||||
"Download": "Download",
|
||||
|
|
411
test/components/views/rooms/DecryptionFailureBar-test.tsx
Normal file
411
test/components/views/rooms/DecryptionFailureBar-test.tsx
Normal file
|
@ -0,0 +1,411 @@
|
|||
/*
|
||||
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 { act, fireEvent, render, screen, waitFor, RenderResult } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
import { DecryptionFailureBar } from "../../../../src/components/views/rooms/DecryptionFailureBar";
|
||||
|
||||
type MockDevice = { deviceId: string };
|
||||
|
||||
const verifiedDevice1: MockDevice = { deviceId: "verified1" };
|
||||
const verifiedDevice2: MockDevice = { deviceId: "verified2" };
|
||||
const unverifiedDevice1: MockDevice = { deviceId: "unverified1" };
|
||||
const unverifiedDevice2: MockDevice = { deviceId: "unverified2" };
|
||||
|
||||
const mockEvent1 = {
|
||||
event: { event_id: "mockEvent1" },
|
||||
getWireContent: () => ({ session_id: "sessionA" }),
|
||||
};
|
||||
|
||||
const mockEvent2 = {
|
||||
event: { event_id: "mockEvent2" },
|
||||
getWireContent: () => ({ session_id: "sessionB" }),
|
||||
};
|
||||
|
||||
const mockEvent3 = {
|
||||
event: { event_id: "mockEvent3" },
|
||||
getWireContent: () => ({ session_id: "sessionB" }),
|
||||
};
|
||||
|
||||
const userId = "@user:example.com";
|
||||
|
||||
let ourDevice: MockDevice | undefined;
|
||||
let allDevices: MockDevice[] | undefined;
|
||||
let keyBackup = false;
|
||||
let callback = async () => {};
|
||||
|
||||
const mockClient = {
|
||||
getUserId: () => userId,
|
||||
getDeviceId: () => ourDevice?.deviceId,
|
||||
getStoredDevicesForUser: () => allDevices,
|
||||
isSecretStored: jest.fn(() => Promise.resolve(keyBackup ? { key: "yes" } : null)),
|
||||
checkIfOwnDeviceCrossSigned: (deviceId: string) => deviceId.startsWith("verified"),
|
||||
downloadKeys: jest.fn(() => {}),
|
||||
cancelAndResendEventRoomKeyRequest: jest.fn(() => {}),
|
||||
on: (_: any, cb: () => Promise<void>) => {
|
||||
callback = cb;
|
||||
},
|
||||
off: () => {},
|
||||
};
|
||||
|
||||
function getBar(wrapper: RenderResult) {
|
||||
return wrapper.container.querySelector(".mx_DecryptionFailureBar");
|
||||
}
|
||||
|
||||
describe("<DecryptionFailureBar />", () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const container = document.body.firstChild;
|
||||
container && document.body.removeChild(container);
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
|
||||
mockClient.cancelAndResendEventRoomKeyRequest.mockClear();
|
||||
});
|
||||
|
||||
it("Displays a loading spinner", async () => {
|
||||
ourDevice = unverifiedDevice1;
|
||||
allDevices = [unverifiedDevice1];
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Prompts the user to verify if they have other devices", async () => {
|
||||
ourDevice = unverifiedDevice1;
|
||||
allDevices = [unverifiedDevice1, verifiedDevice1];
|
||||
keyBackup = false;
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Prompts the user to verify if they have backups", async () => {
|
||||
ourDevice = unverifiedDevice1;
|
||||
allDevices = [unverifiedDevice1];
|
||||
keyBackup = true;
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Prompts the user to reset if they have no other verified devices and no backups", async () => {
|
||||
ourDevice = unverifiedDevice1;
|
||||
allDevices = [unverifiedDevice1, unverifiedDevice2];
|
||||
keyBackup = false;
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Recommends opening other devices if there are other verified devices", async () => {
|
||||
ourDevice = verifiedDevice1;
|
||||
allDevices = [verifiedDevice1, verifiedDevice2];
|
||||
keyBackup = false;
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Displays a general error message if there are no other verified devices", async () => {
|
||||
ourDevice = verifiedDevice1;
|
||||
allDevices = [verifiedDevice1, unverifiedDevice1];
|
||||
keyBackup = true;
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Displays button to resend key requests if we are verified", async () => {
|
||||
ourDevice = verifiedDevice1;
|
||||
allDevices = [verifiedDevice1, verifiedDevice2];
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
// @ts-ignore
|
||||
mockEvent2,
|
||||
// @ts-ignore
|
||||
mockEvent3,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(screen.getByText("Resend key requests"));
|
||||
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledTimes(2);
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledWith(mockEvent1);
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledWith(mockEvent2);
|
||||
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Does not display a button to send key requests if we are unverified", async () => {
|
||||
ourDevice = unverifiedDevice1;
|
||||
allDevices = [unverifiedDevice1, verifiedDevice2];
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
// @ts-ignore
|
||||
mockEvent2,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Displays the button to resend key requests only if there are sessions we haven't already requested", async () => {
|
||||
ourDevice = verifiedDevice1;
|
||||
allDevices = [verifiedDevice1, verifiedDevice2];
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent3,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
fireEvent.click(screen.getByText("Resend key requests"));
|
||||
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledWith(mockEvent3);
|
||||
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.rerender(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
// @ts-ignore
|
||||
mockEvent2,
|
||||
// @ts-ignore
|
||||
mockEvent3,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
mockClient.cancelAndResendEventRoomKeyRequest.mockClear();
|
||||
|
||||
fireEvent.click(screen.getByText("Resend key requests"));
|
||||
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockClient.cancelAndResendEventRoomKeyRequest).toHaveBeenCalledWith(mockEvent1);
|
||||
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
|
||||
it("Handles device updates", async () => {
|
||||
ourDevice = unverifiedDevice1;
|
||||
allDevices = [unverifiedDevice1, verifiedDevice2];
|
||||
|
||||
const bar = render(
|
||||
// @ts-ignore
|
||||
<MatrixClientContext.Provider value={mockClient}>
|
||||
<DecryptionFailureBar
|
||||
failures={[
|
||||
// @ts-ignore
|
||||
mockEvent1,
|
||||
// @ts-ignore
|
||||
mockEvent2,
|
||||
]}
|
||||
/>
|
||||
,
|
||||
</MatrixClientContext.Provider>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(mockClient.isSecretStored).toHaveBeenCalled());
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
ourDevice = verifiedDevice1;
|
||||
await act(callback);
|
||||
expect(getBar(bar)).toMatchSnapshot();
|
||||
|
||||
bar.unmount();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,573 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays a general error message if there are no other verified devices 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Some messages could not be decrypted
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays a loading spinner 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_Spinner"
|
||||
>
|
||||
<div
|
||||
aria-label="Loading..."
|
||||
class="mx_Spinner_icon"
|
||||
data-testid="spinner"
|
||||
role="progressbar"
|
||||
style="width: 32px; height: 32px;"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Decrypting messages...
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
Please wait as we try to decrypt your messages. This may take a few moments.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays button to resend key requests if we are verified 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend key requests
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays button to resend key requests if we are verified 2`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays the button to resend key requests only if there are sessions we haven't already requested 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend key requests
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays the button to resend key requests only if there are sessions we haven't already requested 2`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays the button to resend key requests only if there are sessions we haven't already requested 3`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend key requests
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Displays the button to resend key requests only if there are sessions we haven't already requested 4`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Does not display a button to send key requests if we are unverified 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Verify this device to access all messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device was unable to decrypt some messages because it has not been verified yet.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Handles device updates 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Verify this device to access all messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device was unable to decrypt some messages because it has not been verified yet.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Handles device updates 2`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend key requests
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Prompts the user to reset if they have no other verified devices and no backups 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Reset your keys to prevent future decryption errors
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
You will not be able to access old undecryptable messages, but resetting your keys will allow you to receive new messages.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Reset
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Prompts the user to verify if they have backups 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Verify this device to access all messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device was unable to decrypt some messages because it has not been verified yet.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Prompts the user to verify if they have other devices 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Verify this device to access all messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device was unable to decrypt some messages because it has not been verified yet.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Verify
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<DecryptionFailureBar /> Recommends opening other devices if there are other verified devices 1`] = `
|
||||
<div
|
||||
class="mx_DecryptionFailureBar"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_icon"
|
||||
/>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message"
|
||||
>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_headline"
|
||||
>
|
||||
Open another device to load encrypted messages
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_message_body"
|
||||
>
|
||||
This device is requesting decryption keys from your other devices. Opening one of your other devices may speed this up.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
View your device list
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mx_DecryptionFailureBar_button"
|
||||
>
|
||||
<div
|
||||
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
Resend key requests
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
Loading…
Reference in a new issue