-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Display a warning when an unverified user's identity changes #28211
base: develop
Are you sure you want to change the base?
Display a warning when an unverified user's identity changes #28211
Conversation
ada3de0
to
6fa98f8
Compare
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.
In a first time, we should move UserIdentifyWarning
into a functional component and break down its behaviour into multiple hooks. Let me know if you need help to do it
There are differences with the figma design too.
I think that I've addressed all of @florianduros 's concerns. The test failure looks like possible flakiness, since it's testing components that I haven't touched. |
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.
You are in the right direction! Now, we can refactor this hook into multiple custom hooks seperate by logic concern et will be ready to go.
About the test failure, yes it's a flackiness which has been adresse in another PR
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.
It's looking better! Now we can move theses hooks and their logic into separate custom hooks in order to encapsulate their behaviour and make the component easier to maintain.
In test:
- use
jest.spyOn
in test to handle the mocking of the client or the crypto api. - use https://testing-library.com/docs/queries/about/ to get the dom element. We want to favour the
role
query in order to make it sure that we are also accessible - In case of async behaviour when we need to wait for a rerender,
waitFor
is doing for us the retry/waiting.
test/unit-tests/components/views/rooms/UserIdentityWarning-test.tsx
Outdated
Show resolved
Hide resolved
test/unit-tests/components/views/rooms/UserIdentityWarning-test.tsx
Outdated
Show resolved
Hide resolved
test/unit-tests/components/views/rooms/UserIdentityWarning-test.tsx
Outdated
Show resolved
Hide resolved
@florianduros ping |
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.
Sorry for the delay.
I think it's in a good state to be merged.
One comment about room.hasEncryptionStateEvent
.
I've addressed all of Florian's concerns. @richvdh can you give this a crypto team review? |
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.
Looks mostly ok to me; a couple of bugs though.
It would be good to get at least one playwright test of this behaviour. Could be a followup PR though.
|
||
if ( | ||
event.getContent().membership === KnownMembership.Join || | ||
(event.getContent().membership === KnownMembership.Join && room.shouldEncryptForInvitedMembers()) |
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.
(event.getContent().membership === KnownMembership.Join && room.shouldEncryptForInvitedMembers()) | |
(event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers()) |
I'd feel happier if we had a unit test that picked this up...
I managed to find another race condition while writing a test, so I need to investigate and fix it. |
) { | ||
// Someone's membership changed and we will now encrypt to them. If | ||
// their identity needs approval, show a warning. | ||
await addMemberIfNeedsApproval(userId); |
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.
Why do we catch
on the loadMembers()
call at line 241, but not here? Any errors thrown here will get lost.
if ( | ||
event.getContent().membership === KnownMembership.Join || | ||
(event.getContent().membership === KnownMembership.Invite && room.shouldEncryptForInvitedMembers()) | ||
) { | ||
// Someone's membership changed and we will now encrypt to them. If | ||
// their identity needs approval, show a warning. | ||
await addMemberIfNeedsApproval(userId); | ||
} else { | ||
// Someone's membership changed and we no longer encrypt to them. | ||
// If we're showing a warning about them, we don't need to any more. | ||
removeMemberNeedingApproval(userId); | ||
} |
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.
Suppose we see a member join and leave in quick succession (potentially, in the same /sync response)
The join will trigger a call to addMemberIfNeedsApproval
, which will start a call to userNeedsApproval
.
The leave will run removeMemberNeedingApproval
, which won't do much.
userNeedsApproval
will complete, and the user may be added to the list of approvals needed. We now have a non-member in the list of approvals.
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.
Right. Similar thing if someone leaves while we're still processing the initial room membership. Will have to think about how to solve this. I think that setting the flag in gotVerificationStatusUpdate
might fix the join-then-leave issue, but not the leave-while-initialising problem.
} | ||
gotVerificationStatusUpdate.set(userId, false); | ||
if (await userNeedsApproval(crypto!, userId)) { | ||
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) { |
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.
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) { | |
// Only actually update the list if we haven't received a `UserTrustStatusChanged` for this user in | |
// the meantime. | |
if (!membersNeedingApproval.has(userId) && gotVerificationStatusUpdate.get(userId) === false) { |
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.
Also, consider removing !membersNeedingApproval.has(userId)
from this condition. AFAICT, addMemberNeedingApproval
is a no-op if the user is already in the list, and removing the check would simplify this function.
const selectCurrentPrompt = useCallback((): RoomMember | undefined => { | ||
const membersNeedingApproval = membersNeedingApprovalRef.current; | ||
if (membersNeedingApproval.size === 0) { | ||
return undefined; | ||
} | ||
|
||
// We pick the user with the smallest user ID. | ||
const keys = Array.from(membersNeedingApproval.keys()).sort((a, b) => a.localeCompare(b)); | ||
const selection = membersNeedingApproval.get(keys[0]!); | ||
return selection; | ||
}, []); |
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.
aside: I wondered if it would be good to have a single updateCurrentPrompt
function which updates currentPrompt
based on the current state of the list. Hence we'd have something like:
const updateCurrentPrompt = useCallback(() => {
// Some sort of comment about callbacks and await
setCurrentPrompt((currentPrompt) => {
// If we have an existing prompt, leave it in place unless the user has been removed from the list
if (currentPrompt && membersNeedingApproval.has(currentPrompt.userId)) {
return currentPrompt;
}
return selectCurrentPrompt(); // probably inline this
});
}, [setCurrentPrompt]);
... and then use that in the three places we currently call setCurrentPrompt
.
It feels cognitively simpler to me, and has the advantage that we don't need to worry about someone slipping in an async
before a call to setCurrentPrompt
and breaking everything.
[cli, room, updateCurrentPrompt], | ||
); | ||
|
||
// Check if the user's identity needs approval, and if so, add them to the |
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.
// Check if the user's identity needs approval, and if so, add them to the | |
// For each user in the list, check if their identity needs approval, and if so, add them to the |
} else if (eventType !== EventType.RoomMember) { | ||
return; | ||
} | ||
|
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.
// We're processing an m.room.member event | |
const gotVerificationStatusUpdate = gotVerificationStatusUpdateRef.current; | ||
if (gotVerificationStatusUpdate.has(userId)) { | ||
// There is an ongoing call to `addMembersWhoNeedApproval`. | ||
// Indicate that we've received a verification status update | ||
// to prevent it from trying to add the user as needing | ||
// verification. | ||
gotVerificationStatusUpdate.set(userId, true); | ||
} |
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.
okayyy, but now don't we have a problem for a join-leave-join scenario?
I can't help but feel we need a proper queue of things to process :/
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.
I think I figured out a way to solve it while minimising the changes.
} | ||
gotVerificationStatusUpdate.set(userId, false); | ||
promises.push( |
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.
out of interest, was the switch to doing this in parallel meant to fix something, or just an optimisation?
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.
Just an optimisation. I changed the function to take a list of users so that it would update gotVerificationStatusUpdate
for all the users at once, so that if we get an update for, say, Alice, but we haven't started fetching her update status, we won't try to overwrite the update with a fetched value. Then I figured it was silly that we were waiting for one to complete before querying the next.
Fixes element-hq/element-meta#2513
Checklist
public
/exported
symbols have accurate TSDoc documentation.