-
-
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.
feat(crypto): Support verification violation composer banner (#29067)
* feat(crypto): Support verification violation composer banner * refactor UserIdentityWarning by using now a ViewModel fixup: logger import fixup: test lint type problems fix test having an unexpected verification violation fixup sonarcubes warnings * review: comments on types and inline some const * review: Quick refactor, better handling of action on button click * review: Small updates, remove commented code
- Loading branch information
1 parent
dcce9c7
commit d3a6f34
Showing
6 changed files
with
547 additions
and
451 deletions.
There are no files selected for viewing
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
192 changes: 192 additions & 0 deletions
192
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,192 @@ | ||
/* | ||
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, RoomMember, RoomStateEvent } from "matrix-js-sdk/src/matrix"; | ||
import { CryptoApi, CryptoEvent } from "matrix-js-sdk/src/crypto-api"; | ||
import { throttle } from "lodash"; | ||
import { logger } from "matrix-js-sdk/src/logger"; | ||
|
||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx"; | ||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter.ts"; | ||
|
||
export type ViolationType = "PinViolation" | "VerificationViolation"; | ||
|
||
/** | ||
* Represents a prompt to the user about a violation in the room. | ||
* The type of violation and the member it relates to are included. | ||
* If the type is "VerificationViolation", the warning is critical and should be reported with more urgency. | ||
*/ | ||
export type ViolationPrompt = { | ||
member: RoomMember; | ||
type: ViolationType; | ||
}; | ||
|
||
/** | ||
* The state of the UserIdentityWarningViewModel. | ||
* This includes the current prompt to show to the user and a callback to handle button clicks. | ||
* If currentPrompt is undefined, there are no violations to show. | ||
*/ | ||
export interface UserIdentityWarningState { | ||
currentPrompt?: ViolationPrompt; | ||
dispatchAction: (action: UserIdentityWarningViewModelAction) => void; | ||
} | ||
|
||
/** | ||
* List of actions that can be dispatched to the UserIdentityWarningViewModel. | ||
*/ | ||
export type UserIdentityWarningViewModelAction = | ||
| { type: "PinUserIdentity"; userId: string } | ||
| { type: "WithdrawVerification"; userId: string }; | ||
|
||
/** | ||
* Maps a list of room members to a list of violations. | ||
* Checks for all members in the room to see if they have any violations. | ||
* If no violations are found, an empty list is returned. | ||
* | ||
* @param cryptoApi | ||
* @param members - The list of room members to check for violations. | ||
*/ | ||
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, setCurrentPrompt] = useState<ViolationPrompt | undefined>(undefined); | ||
|
||
const loadViolations = useMemo( | ||
() => | ||
throttle(async (): Promise<void> => { | ||
const isEncrypted = crypto && (await crypto.isEncryptionEnabledInRoom(room.roomId)); | ||
if (!isEncrypted) { | ||
setMembers([]); | ||
setCurrentPrompt(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 | ||
const sortedViolations = violations.sort((a, b) => a.member.userId.localeCompare(b.member.userId)); | ||
candidatePrompt = sortedViolations[0]; | ||
} else { | ||
candidatePrompt = undefined; | ||
} | ||
|
||
// is the current prompt still valid? | ||
setCurrentPrompt((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 | ||
useTypedEventEmitter( | ||
cli, | ||
RoomStateEvent.Events, | ||
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; | ||
} | ||
|
||
if (shouldRefresh) { | ||
loadViolations().catch((e) => { | ||
logger.error("Error refreshing UserIdentityWarningViewModel:", e); | ||
}); | ||
} | ||
}, | ||
[crypto, room, loadViolations], | ||
), | ||
); | ||
|
||
// We need to listen for changes to the verification status of the members to refresh violations | ||
useTypedEventEmitter( | ||
cli, | ||
CryptoEvent.UserTrustStatusChanged, | ||
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], | ||
), | ||
); | ||
|
||
useEffect(() => { | ||
loadViolations().catch((e) => { | ||
logger.error("Error initialising UserIdentityWarning:", e); | ||
}); | ||
}, [loadViolations]); | ||
|
||
const dispatchAction = useCallback( | ||
(action: UserIdentityWarningViewModelAction): void => { | ||
if (!crypto) { | ||
return; | ||
} | ||
if (action.type === "PinUserIdentity") { | ||
crypto.pinCurrentUserIdentity(action.userId).catch((e) => { | ||
logger.error("Error pinning user identity:", e); | ||
}); | ||
} else if (action.type === "WithdrawVerification") { | ||
crypto.withdrawVerificationRequirement(action.userId).catch((e) => { | ||
logger.error("Error withdrawing verification requirement:", e); | ||
}); | ||
} | ||
}, | ||
[crypto], | ||
); | ||
|
||
return { | ||
currentPrompt, | ||
dispatchAction, | ||
}; | ||
} |
Oops, something went wrong.