split into multiple hooks

pull/28211/head
Hubert Chathi 2024-10-23 15:34:30 -04:00
parent 0d186dd3ac
commit 0b1e7adf91
1 changed files with 146 additions and 120 deletions

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { useEffect, useRef, useState } from "react"; import React, { useCallback, useRef, useState } from "react";
import { import {
CryptoEvent, CryptoEvent,
EventType, EventType,
@ -21,7 +21,8 @@ import { Button, Separator } from "@vector-im/compound-web";
import type { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; import type { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
interface UserIdentityWarningProps { interface UserIdentityWarningProps {
/** /**
@ -50,7 +51,7 @@ async function userNeedsApproval(crypto: CryptoApi, userId: string): Promise<boo
* button to acknowledge the change. * button to acknowledge the change.
*/ */
export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => { export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }) => {
const cli = MatrixClientPeg.safeGet(); const cli = useMatrixClientContext();
const crypto = cli.getCrypto(); const crypto = cli.getCrypto();
// The current room member that we are prompting the user to approve. // The current room member that we are prompting the user to approve.
@ -76,17 +77,10 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
// with the newer value, so it will fix itself in the end. // with the newer value, so it will fix itself in the end.
const gotVerificationStatusUpdateRef = useRef<Map<string, boolean>>(new Map()); const gotVerificationStatusUpdateRef = useRef<Map<string, boolean>>(new Map());
useEffect(() => { // Select a new user to display a warning for. This is called after the
if (!crypto) return; // current prompted user no longer needs their identity approved.
const selectCurrentPrompt = useCallback((): void => {
const membersNeedingApproval = membersNeedingApprovalRef.current; const membersNeedingApproval = membersNeedingApprovalRef.current;
const gotVerificationStatusUpdate = gotVerificationStatusUpdateRef.current;
/**
* Select a new user to display a warning for. This is called after the
* current prompted user no longer needs their identity approved.
*/
function selectCurrentPrompt(): void {
if (membersNeedingApproval.size === 0) { if (membersNeedingApproval.size === 0) {
setCurrentPrompt(undefined); setCurrentPrompt(undefined);
return; return;
@ -95,44 +89,55 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
// We return the user with the smallest user ID. // We return the user with the smallest user ID.
const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b)); const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b));
setCurrentPrompt(membersNeedingApproval.get(keys[0]!)); setCurrentPrompt(membersNeedingApproval.get(keys[0]!));
} }, [membersNeedingApprovalRef]);
function addMemberNeedingApproval(userId: string): void { // Add a user to the membersNeedingApproval map, and update the current
// prompt if necessary.
const addMemberNeedingApproval = useCallback(
(userId: string): void => {
if (userId === cli.getUserId()) { if (userId === cli.getUserId()) {
// We always skip our own user, because we can't pin our own identity. // We always skip our own user, because we can't pin our own identity.
return; return;
} }
const member = room.getMember(userId); const member = room.getMember(userId);
if (member) { if (member) {
membersNeedingApproval.set(userId, member); membersNeedingApprovalRef.current.set(userId, member);
if (!currentPrompt) { if (!currentPrompt) {
// If we're not currently displaying a prompt, then we should // If we're not currently displaying a prompt, then we should
// display a prompt for this user. // display a prompt for this user.
selectCurrentPrompt(); selectCurrentPrompt();
} }
} }
} },
[cli, room, membersNeedingApprovalRef, currentPrompt, selectCurrentPrompt],
);
function removeMemberNeedingApproval(userId: string): void { // Add a user from the membersNeedingApproval map, and update the current
membersNeedingApproval.delete(userId); // prompt if necessary.
const removeMemberNeedingApproval = useCallback(
(userId: string): void => {
membersNeedingApprovalRef.current.delete(userId);
// If we removed the currently displayed user, we need to pick a new one // If we removed the currently displayed user, we need to pick a new one
// to display. // to display.
if (currentPrompt?.userId === userId) { if (currentPrompt?.userId === userId) {
selectCurrentPrompt(); selectCurrentPrompt();
} }
} },
[membersNeedingApprovalRef, currentPrompt, selectCurrentPrompt],
);
/** // Initialise the component. Get the room members, check which ones need
* Initialise the component. Get the room members, check which ones need // their identity approved, and pick one to display.
* their identity approved, and pick one to display. const loadMembers = useCallback(async (): Promise<void> => {
*/ if (!crypto || initialisedRef.current) {
async function initialise(): Promise<void> {
if (initialisedRef.current) {
return; return;
} }
initialisedRef.current = true; initialisedRef.current = true;
const gotVerificationStatusUpdate = gotVerificationStatusUpdateRef.current;
const membersNeedingApproval = membersNeedingApprovalRef.current;
const members = await room.getEncryptionTargetMembers(); const members = await room.getEncryptionTargetMembers();
for (const member of members) { for (const member of members) {
@ -143,7 +148,7 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
continue; continue;
} }
gotVerificationStatusUpdate.set(userId, false); gotVerificationStatusUpdate.set(userId, false);
if (await userNeedsApproval(crypto!, userId)) { if (await userNeedsApproval(crypto, userId)) {
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) { if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) {
membersNeedingApproval.set(userId, member); membersNeedingApproval.set(userId, member);
} }
@ -152,9 +157,23 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
} }
selectCurrentPrompt(); selectCurrentPrompt();
}, [crypto, room, initialisedRef, gotVerificationStatusUpdateRef, membersNeedingApprovalRef, selectCurrentPrompt]);
// If the room has encryption enabled, we load the room members right away.
// If not, we wait until encryption is enabled before loading the room
// members, since we don't need to display anything in unencrypted rooms.
if (crypto && room.hasEncryptionStateEvent()) {
loadMembers().catch((e) => {
logger.error("Error initialising UserIdentityWarning:", e);
});
} }
const onUserTrustStatusChanged = (userId: string, verificationStatus: UserVerificationStatus): void => { // When a user's verification status changes, we check if they need to be
// added/removed from the set of members needing approval.
const onUserVerificationStatusChanged = useCallback(
(userId: string, verificationStatus: UserVerificationStatus): void => {
const gotVerificationStatusUpdate = gotVerificationStatusUpdateRef.current;
if (!initialisedRef.current) { if (!initialisedRef.current) {
return; return;
} }
@ -168,17 +187,27 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
} else { } else {
removeMemberNeedingApproval(userId); removeMemberNeedingApproval(userId);
} }
}; },
[initialisedRef, gotVerificationStatusUpdateRef, addMemberNeedingApproval, removeMemberNeedingApproval],
);
useTypedEventEmitter(cli, CryptoEvent.UserTrustStatusChanged, onUserVerificationStatusChanged);
const onRoomStateEvent = async (event: MatrixEvent): Promise<void> => { // We watch for encryption events (since we only display warnings in
if (event.getRoomId() !== room.roomId) { // encrypted rooms), and for membership changes (since we only display
// warnings for users in the room).
const onRoomStateEvent = useCallback(
async (event: MatrixEvent): Promise<void> => {
if (!crypto || event.getRoomId() !== room.roomId) {
return; return;
} }
const gotVerificationStatusUpdate = gotVerificationStatusUpdateRef.current;
const membersNeedingApproval = membersNeedingApprovalRef.current;
const eventType = event.getType(); const eventType = event.getType();
if (eventType === EventType.RoomEncryption && event.getStateKey() === "") { if (eventType === EventType.RoomEncryption && event.getStateKey() === "") {
// Room is now encrypted, so we can initialise the component. // Room is now encrypted, so we can initialise the component.
return initialise().catch((e) => { return loadMembers().catch((e) => {
logger.error("Error initialising UserIdentityWarning:", e); logger.error("Error initialising UserIdentityWarning:", e);
}); });
} else if (eventType !== EventType.RoomMember) { } else if (eventType !== EventType.RoomMember) {
@ -205,8 +234,7 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
return; return;
} }
gotVerificationStatusUpdate.set(userId, false); gotVerificationStatusUpdate.set(userId, false);
const crypto = MatrixClientPeg.safeGet().getCrypto()!; if (await userNeedsApproval(crypto, userId)) {
if (await userNeedsApproval(crypto!, userId)) {
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) { if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) {
addMemberNeedingApproval(userId); addMemberNeedingApproval(userId);
} }
@ -217,36 +245,25 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
// If we're showing a warning about them, we don't need to any more. // If we're showing a warning about them, we don't need to any more.
removeMemberNeedingApproval(userId); removeMemberNeedingApproval(userId);
} }
}; },
[
crypto,
room,
gotVerificationStatusUpdateRef,
membersNeedingApprovalRef,
addMemberNeedingApproval,
removeMemberNeedingApproval,
loadMembers,
],
);
useTypedEventEmitter(cli, RoomStateEvent.Events, onRoomStateEvent);
cli.on(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged); if (!crypto || !currentPrompt) return null;
cli.on(RoomStateEvent.Events, onRoomStateEvent);
if (room.hasEncryptionStateEvent()) {
initialise().catch((e) => {
logger.error("Error initialising UserIdentityWarning:", e);
});
}
return () => {
cli.removeListener(CryptoEvent.UserTrustStatusChanged, onUserTrustStatusChanged);
cli.removeListener(RoomStateEvent.Events, onRoomStateEvent);
};
}, [currentPrompt, room, cli, crypto]);
if (!crypto) return null;
if (currentPrompt) {
const confirmIdentity = async (): Promise<void> => { const confirmIdentity = async (): Promise<void> => {
await crypto.pinCurrentUserIdentity(currentPrompt.userId); await crypto.pinCurrentUserIdentity(currentPrompt.userId);
}; };
const substituteATag = (sub: string): React.ReactNode => (
<a href="https://element.io/help#encryption18" target="_blank" rel="noreferrer noopener">
{sub}
</a>
);
const substituteBTag = (sub: string): React.ReactNode => <b>{sub}</b>;
return ( return (
<div className="mx_UserIdentityWarning"> <div className="mx_UserIdentityWarning">
<Separator /> <Separator />
@ -277,7 +294,16 @@ export const UserIdentityWarning: React.FC<UserIdentityWarningProps> = ({ room }
</div> </div>
</div> </div>
); );
} else {
return null;
}
}; };
function substituteATag(sub: string): React.ReactNode {
return (
<a href="https://element.io/help#encryption18" target="_blank" rel="noreferrer noopener">
{sub}
</a>
);
}
function substituteBTag(sub: string): React.ReactNode {
return <b>{sub}</b>;
}