-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor UserIdentityWarning by using now a ViewModel
- Loading branch information
1 parent
e839ab2
commit 02a2640
Showing
3 changed files
with
441 additions
and
535 deletions.
There are no files selected for viewing
160 changes: 160 additions & 0 deletions
160
src/components/viewmodels/rooms/UserIdentityWarningViewModel.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
/* | ||
Copyright 2025 New Vector Ltd. | ||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial | ||
Please see LICENSE files in the repository root for full details. | ||
*/ | ||
|
||
import { useCallback, useEffect, useMemo, useState } from "react"; | ||
import { EventType, MatrixEvent, Room, RoomStateEvent, RoomMember } from "matrix-js-sdk/src/matrix"; | ||
import { CryptoEvent, CryptoApi } from "matrix-js-sdk/src/crypto-api"; | ||
import { throttle } from "lodash"; | ||
|
||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx"; | ||
import { logger } from "../../../../../matrix-js-sdk/src/logger.ts"; | ||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts"; | ||
import { ButtonEvent } from "../../views/elements/AccessibleButton.tsx"; | ||
|
||
export type ViolationType = "PinViolation" | "VerificationViolation"; | ||
|
||
export type ViolationPrompt = { | ||
member: RoomMember; | ||
type: ViolationType; | ||
}; | ||
|
||
export interface UserIdentityWarningState { | ||
currentPrompt?: ViolationPrompt; | ||
onButtonClick: (ev: ButtonEvent) => void; | ||
} | ||
|
||
async function mapToViolations(cryptoApi: CryptoApi, members: RoomMember[]): Promise<ViolationPrompt[]> { | ||
const violationList = new Array<ViolationPrompt>(); | ||
for (const member of members) { | ||
const verificationStatus = await cryptoApi.getUserVerificationStatus(member.userId); | ||
if (verificationStatus.wasCrossSigningVerified() && !verificationStatus.isCrossSigningVerified()) { | ||
violationList.push({ member, type: "VerificationViolation" }); | ||
} else if (verificationStatus.needsUserApproval) { | ||
violationList.push({ member, type: "PinViolation" }); | ||
} | ||
} | ||
return violationList; | ||
} | ||
|
||
export function useUserIdentityWarningViewModel(room: Room, key: string): UserIdentityWarningState { | ||
const cli = useMatrixClientContext(); | ||
const crypto = cli.getCrypto(); | ||
|
||
const [members, setMembers] = useState<RoomMember[]>([]); | ||
const [currentPrompt, setPrompt] = useState<ViolationPrompt | undefined>(undefined); | ||
|
||
const loadViolations = useMemo( | ||
() => | ||
throttle(async (): Promise<void> => { | ||
const isEncrypted = crypto && (await crypto.isEncryptionEnabledInRoom(room.roomId)); | ||
if (!isEncrypted) { | ||
setMembers([]); | ||
setPrompt(undefined); | ||
return; | ||
} | ||
|
||
const targetMembers = await room.getEncryptionTargetMembers(); | ||
setMembers(targetMembers); | ||
const violations = await mapToViolations(crypto!, targetMembers); | ||
|
||
let candidatePrompt: ViolationPrompt | undefined; | ||
if (violations.length > 0) { | ||
// sort by user ID to ensure consistent ordering | ||
candidatePrompt = violations.sort((a, b) => a.member.userId.localeCompare(b.member.userId))[0]; | ||
} else { | ||
candidatePrompt = undefined; | ||
} | ||
|
||
// is the current prompt still valid? | ||
setPrompt((existingPrompt): ViolationPrompt | undefined => { | ||
if (existingPrompt && violations.includes(existingPrompt)) { | ||
return existingPrompt; | ||
} else if (candidatePrompt) { | ||
return candidatePrompt; | ||
} else { | ||
return undefined; | ||
} | ||
}); | ||
}), | ||
[crypto, room], | ||
); | ||
|
||
// We need to listen for changes to the members list | ||
const onRoomStateEvent = useCallback( | ||
async (event: MatrixEvent): Promise<void> => { | ||
if (!crypto || event.getRoomId() !== room.roomId) { | ||
return; | ||
} | ||
let shouldRefresh = false; | ||
|
||
const eventType = event.getType(); | ||
|
||
if (eventType === EventType.RoomEncryption && event.getStateKey() === "") { | ||
// Room is now encrypted, so we can initialise the component. | ||
shouldRefresh = true; | ||
} else if (eventType == EventType.RoomMember) { | ||
// We're processing an m.room.member event | ||
// Something has changed in membership, someone joined or someone left or | ||
// someone changed their display name. Anyhow let's refresh. | ||
const userId = event.getStateKey(); | ||
shouldRefresh = !!userId; | ||
} else { | ||
shouldRefresh = false; | ||
} | ||
|
||
if (shouldRefresh) { | ||
loadViolations().catch((e) => { | ||
logger.error("Error refreshing UserIdentityWarningViewModel:", e); | ||
}); | ||
} | ||
}, | ||
[crypto, room, loadViolations], | ||
); | ||
useTypedEventEmitter(cli, RoomStateEvent.Events, onRoomStateEvent); | ||
|
||
// We need to listen for changes to the verification status of the members to refresh violations | ||
const onUserVerificationStatusChanged = useCallback( | ||
(userId: string): void => { | ||
if (members.find((m) => m.userId == userId)) { | ||
// This member is tracked, we need to refresh. | ||
// refresh all for now? | ||
// As a later optimisation we could store the current violations and only update the relevant one. | ||
loadViolations().catch((e) => { | ||
logger.error("Error refreshing UserIdentityWarning:", e); | ||
}); | ||
} | ||
}, | ||
[loadViolations, members], | ||
); | ||
useTypedEventEmitter(cli, CryptoEvent.UserTrustStatusChanged, onUserVerificationStatusChanged); | ||
|
||
useEffect(() => { | ||
loadViolations().catch((e) => { | ||
logger.error("Error initialising UserIdentityWarning:", e); | ||
}); | ||
}, [loadViolations]); | ||
|
||
let onButtonClick: (ev: ButtonEvent) => void = async (ev: ButtonEvent) => {}; | ||
if (currentPrompt) { | ||
onButtonClick = async (ev: ButtonEvent): Promise<void> => { | ||
// XXX do we want some posthog tracking? | ||
ev.preventDefault(); | ||
if (currentPrompt) { | ||
if (currentPrompt.type === "VerificationViolation") { | ||
await crypto?.withdrawVerificationRequirement(currentPrompt.member.userId); | ||
} else if (currentPrompt.type === "PinViolation") { | ||
await crypto?.pinCurrentUserIdentity(currentPrompt.member.userId); | ||
} | ||
} | ||
}; | ||
} | ||
|
||
return { | ||
currentPrompt, | ||
onButtonClick, | ||
}; | ||
} |
Oops, something went wrong.