-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(crypto): Support verification violation composer banner #29067
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
ede3058
feat(crypto): Support verification violation composer banner
BillCarsonFr 3aa4a6e
refactor UserIdentityWarning by using now a ViewModel
BillCarsonFr e1c3950
review: comments on types and inline some const
BillCarsonFr 61ca0eb
review: Quick refactor, better handling of action on button click
BillCarsonFr 673e0d4
review: Small updates, remove commented code
BillCarsonFr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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[]> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could also do with some doc as it's not super obvious what it does. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 6b76fbc |
||
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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could all of these exported things have tsdoc please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done 6b76fbc