-
Notifications
You must be signed in to change notification settings - Fork 1
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
Es/invite flow #79
Es/invite flow #79
Conversation
7026eb7
to
6fb33f5
Compare
5287e17
to
8657e84
Compare
src/frontend/screens/Settings/ProjectSettings/YourTeam/index.tsx
Outdated
Show resolved
Hide resolved
// this is not exposed yet | ||
const deviceType = 'mobile'; |
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.
Do we expose the deviceType
(whether it is mobile
or desktop
)?
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.
Good point, no we do not. I've opened an issue to initially hardcode this digidem/comapeo-core#451, and we can follow-up with sharing this (digidem/comapeo-core#452)
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.
Can you open an issue to fix this once we update mapeo core with this change?
React.useEffect(() => { | ||
project.$member | ||
.invite(route.params.deviceId, {roleId: route.params.role}) | ||
.then(() => { | ||
queryClient.invalidateQueries({queryKey: ['projectMembers']}), | ||
navigation.navigate('InviteAccepted', route.params); | ||
}) | ||
.catch(err => { | ||
openSheet(); | ||
}); | ||
}, []); |
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.
The architecture is a little odd here. But essentially, when the coordinator clicks to invite someone, they are navigated to this page to wait for the invite. So im deferring the actual invite till after the navigation has happened so we have access to the promise. Otherwise, the user clicks invite, the invite promise is created, and then the user is navigated to this page. Since react-nav doesnt allow for non-serializable things to be passed between pages, it would not have access to the promise.
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 understand the reasons for doing this within the limitations of react-nav, but unfortunately this approach will lead to subtle and hard-to-track-down bugs (e.g. see https://react.dev/learn/you-might-not-need-an-effect#sending-a-post-request). Currently:
- An invite will be sent twice in development mode (because useEffects run twice in development mode)
- If the user closes the app and re-opens it, the invite could be re-sent if the screen re-renders
- If
queryClient
oropenSheet
change between renders, then this will fail because they are not included in the useEffect dependencies. This is currently ok because they are stable across renders, but a change to theuseBottomSheetModal
that madeopenSheet
change between renders would cause bugs.
Some of this could be avoided by adding a ref that tracks whether this has run once, but some issues above would remain. I think to solve this we need to either:
- Track invite state in context (lots of additional code)
- Move all these invite states into a single screen (preferable I think), e.g.
[inviteState, setInviteState] = useState<'pending' | 'waiting' | 'accepted' | 'rejected'>('pending')
(then swapnavigate()
withsetInviteState()
)
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 updated this.
I dont think we need to worry about all 4 states in one screen. One screen just needs to handle sending the invite, and tracking the promise that comes along with it. As soon as the promise is resolved or rejected, we can just navigate to a new page. So I updated the page to do that.
Unfortunately we don't get the transition animation between sending and invite and waiting for an invite that react navigation gave us, but for now I think that is ok.
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.
Good work on this, it's looking good, however I think the approach of sending an invite as a side-effect of rendering the WaitingForInviteAccept
screen is prone to bugs (especially as we don't have a meaningful way to cancel invites yet).
There are a couple of issues this throws up around the limitations of the API, and I've created some issues for those. I think the role names and IDs also needs cleaned up - it's created to be forwards compatible with custom role definitions, but I think it's not clear how to use it right now and maybe we are not exposing enough from the backend. It would help to discuss this together and figure out how to adapt the API, and the best workaround for the meantime.
src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewInvitation.tsx
Outdated
Show resolved
Hide resolved
src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectInviteeRole.tsx
Outdated
Show resolved
Hide resolved
src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectInviteeRole.tsx
Outdated
Show resolved
Hide resolved
src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectInviteeRole.tsx
Outdated
Show resolved
Hide resolved
src/frontend/screens/Settings/ProjectSettings/YourTeam/SelectInviteeRole.tsx
Outdated
Show resolved
Hide resolved
React.useEffect(() => { | ||
project.$member | ||
.invite(route.params.deviceId, {roleId: route.params.role}) | ||
.then(() => { | ||
queryClient.invalidateQueries({queryKey: ['projectMembers']}), | ||
navigation.navigate('InviteAccepted', route.params); | ||
}) | ||
.catch(err => { | ||
openSheet(); | ||
}); | ||
}, []); |
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 understand the reasons for doing this within the limitations of react-nav, but unfortunately this approach will lead to subtle and hard-to-track-down bugs (e.g. see https://react.dev/learn/you-might-not-need-an-effect#sending-a-post-request). Currently:
- An invite will be sent twice in development mode (because useEffects run twice in development mode)
- If the user closes the app and re-opens it, the invite could be re-sent if the screen re-renders
- If
queryClient
oropenSheet
change between renders, then this will fail because they are not included in the useEffect dependencies. This is currently ok because they are stable across renders, but a change to theuseBottomSheetModal
that madeopenSheet
change between renders would cause bugs.
Some of this could be avoided by adding a ref that tracks whether this has run once, but some issues above would remain. I think to solve this we need to either:
- Track invite state in context (lots of additional code)
- Move all these invite states into a single screen (preferable I think), e.g.
[inviteState, setInviteState] = useState<'pending' | 'waiting' | 'accepted' | 'rejected'>('pending')
(then swapnavigate()
withsetInviteState()
)
name={coordinator.name || ''} | ||
deviceId={coordinator.deviceId} | ||
deviceType="mobile" | ||
// This is a weak check. We should be using deviceIds, but those are not exposed |
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.
Good point, fix coming in digidem/comapeo-core#454
const ssid = useLocalDiscoveryState(state => state.ssid); | ||
const {formatMessage: t} = useIntl(); | ||
|
||
const devices = useLocalPeers(); |
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.
Note, this will include devices that are already members of the project until #167 is fixed
src/frontend/screens/Settings/ProjectSettings/YourTeam/ReviewInvitation.tsx
Outdated
Show resolved
Hide resolved
onPress={() => | ||
navigation.navigate('ReviewInvitation', { | ||
...route.params, | ||
role: 'coordinator', |
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.
This is used in the WaitingForInviteAccept
component as the roleId, which is a unique ID. This should be renamed maybe, and use the roleId
, however I'm not sure if we correctly expose this in the API. I think the API is confusing here, and maybe we can quickly jump on a call to discuss how to improve it.
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.
Good work on this, it's looking good, however I think the approach of sending an invite as a side-effect of rendering the WaitingForInviteAccept
screen is prone to bugs (especially as we don't have a meaningful way to cancel invites yet).
There are a couple of issues this throws up around the limitations of the API, and I've created some issues for those. I think the role names and IDs also needs cleaned up - it's created to be forwards compatible with custom role definitions, but I think it's not clear how to use it right now and maybe we are not exposing enough from the backend. It would help to discuss this together and figure out how to adapt the API, and the best workaround for the meantime.
I tested this with 2 devices today. Unfortunately I do not think peer discover is running. I had both devices running the dev environment (I learned today that if you plug in 2 devices, metro will simultaneouly run on both devices). |
This change makes several improvements to the concept formerly known as "capabilities": - `Capability` is now called `Role` across the project, resulting in many simple renames. - `Role`s have an attached `roleId` property, which they didn't before. - The creator role and "no role" role now have role IDs.[^1] - The `RoleId` type now includes all possible role types, now including the creator role and the "no role" role. I created narrower types such as `RoleIdAssignableToOthers` and `RoleIdAssignableToAnyone`, which don't include those. This should help the front-end know what role someone is (for example, <digidem/comapeo-mobile#79 (comment)>). [^1]: I generated these with `crypto.randomBytes(8).toString('hex')`.
return | ||
} | ||
+ | ||
+ const { address } = referer |
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.
Evan and I discovered that address
was showing up in referer
, but not in service
. @gmaclennan
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.
Hmm I have a feeling that the changes in this patch will break things in other network environments, but if it’s enough to get it working for initial testing then that’s ok for now.
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.
This doesn't change how the front end is being implemented, so if we need to revert it and fix it another way, it shouldn't make a difference
setInviteSent(true); | ||
project.$member | ||
.invite(rest.deviceId, {roleId: role}) | ||
.then(val => navigation.navigate('InviteAccepted', {...route.params})) |
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.
This is timing out after 5 seconds:
Error: Server timed out after 5000ms. The server could be closed or the transport is down.
Otherwise, the other device is recieving the invite and is able to accept it and join the project @gmaclennan
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.
not a full review but overall changes look good. will try to get to the rest of it tomorrow!
<TouchableOpacity | ||
style={[styles.flexRow, styles.cardContainer, style]} | ||
onPress={onPress}> | ||
<MaterialIcon name="radio-button-off" size={25} color={DARK_GREY} /> |
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.
nit: is this needed given that the screen immediately navigates after pressing?
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 we are going to show it, we should set it to on when pressed, because navigation is never immediate in React Native.
@@ -25,7 +25,7 @@ | |||
"@formatjs/intl-pluralrules": "^5.2.4", | |||
"@formatjs/intl-relativetimeformat": "^11.2.4", | |||
"@gorhom/bottom-sheet": "^4.5.1", | |||
"@mapeo/ipc": "^0.1.3", | |||
"@mapeo/ipc": "^0.1.5", |
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.
nit: can update the backend package.json too to keep in sync (not really necessary though)
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.
<ErrorModal | ||
sheetRef={sheetRef} | ||
closeSheet={closeSheet} | ||
isOpen={isOpen} | ||
clearError={() => navigation.navigate('YourTeam')} | ||
/> |
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.
Suggestion: move this to ReviewInvitation
. simplifies implementation of WaitingForInviteAccept
and co-locates the usage of the bottom sheet modal with where the hook is used and what triggers the modal (which is generally easier to follow). would probably result in a little bit of refactoring in ReviewInvitation
though. something along the lines of this is what i'm thinking:
export const ReviewInvitation: NativeNavigationComponent<
'ReviewInvitation'
> = ({route, navigation}) => {
const {role, name, deviceId, deviceType} = route.params;
const [inviteStatus, setInviteStatus] = React.useState<
'reviewing' | 'waiting'
>('reviewing');
const {openSheet, ...restBottomSheet} = useBottomSheetModal({
openOnMount: false,
});
const project = useProject();
const queryClient = useQueryClient();
function sendInvite() {
setInviteStatus('waiting');
project.$member
.invite(deviceId, {roleId: role})
.then(val => {
if (val == 'ACCEPT')
queryClient.invalidateQueries({queryKey: ['projectMembers']});
navigation.navigate('InviteAccepted', {...route.params});
})
.catch(err => {
console.log(err);
openSheet();
});
}
return (
<React.Fragment>
{inviteStatus === 'reviewing' ? (
// Name can be whatever but just wanted to show a purely view-based component that you could create to display in the reviewing state
<InvitationReview
sendInvite={sendInvite}
role={role}
name={name}
deviceId={deviceId}
deviceType={deviceType}
/>
) : (
<WaitingForInviteAccept />
)}
<ErrorModal
{...restBottomSheet}
clearError={() => navigation.navigate('YourTeam')}
/>
</React.Fragment>
);
};
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.
This change makes several improvements to the concept formerly known as "capabilities": - `Capability` is now called `Role` across the project, resulting in many simple renames. - `Role`s have an attached `roleId` property, which they didn't before. - The creator role and "no role" role now have role IDs.[^1] - The `RoleId` type now includes all possible role types, now including the creator role and the "no role" role. I created narrower types such as `RoleIdAssignableToOthers` and `RoleIdAssignableToAnyone`, which don't include those. This should help the front-end know what role someone is (for example, <digidem/comapeo-mobile#79 (comment)>). [^1]: I generated these with `crypto.randomBytes(8).toString('hex')`. Co-Authored-By: Gregor MacLennan <[email protected]>
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.
Thanks Erik, I mainly focussed on reviewing the logic - I have not tested this in the emulator or on a device. There are a few small things, but nothing blocking this I don't think - we can fix things up in a follow-up as long as you open the issues. Two questions here about text size accessibility, and possible error when cancelling an invite then the invite errors. I think cancellable invites is an important missing feature from the backend that we need to address.
const {formatMessage: t} = useIntl(); | ||
const membersQuery = useProjectMembers(); | ||
const deviceInfo = useDeviceInfo(); | ||
const coordinators = React.useMemo( |
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 wouldn't bother using useMemo on these, the overhead of memoization is likely greater than the cost of filtering. Array filtering operations like this are very fast unless you are dealing with millions of entries.
Fine to leave in for now.
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.
src/frontend/screens/Settings/ProjectSettings/YourTeam/index.tsx
Outdated
Show resolved
Hide resolved
name={participant.name || ''} | ||
deviceId={participant.deviceId} | ||
deviceType="mobile" | ||
// This is a weak check. We should be using deviceIds, but those are not exposed |
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.
Can you make sure an issue is open to fix this once we update to latest mapeo core?
deviceType="mobile" | ||
// This is a weak check. We should be using deviceIds, but those are not exposed | ||
thisDevice={ | ||
deviceInfo.data && deviceInfo.data.name === coordinator.name |
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.
can you make sure an issue is open to fix this once we update to latest mapeo core?
// this is not exposed yet | ||
const deviceType = 'mobile'; |
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.
Can you open an issue to fix this once we update mapeo core with this change?
}, | ||
timerMessage: { | ||
id: 'screens.Setting.ProjectSettings.YourTeam.WaitingForInviteAccept.timerMessage', | ||
defaultMessage: 'Invite sent {seconds}s ago', |
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.
Can you open an issue to internationalize this? Will need to handle plurals for different languages.
}) | ||
.catch(err => { | ||
console.log(err); | ||
openSheet(); |
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.
This screen could be unmounted at this point (if the user had clicked cancel invite on the waiting screen), so I think openSheet()
would fail?
<Text style={{fontWeight: 'bold'}}>{name}</Text> | ||
{deviceId && ( | ||
<Text style={{color: MEDIUM_GREY}} numberOfLines={1}> | ||
{`${deviceId.slice(0, 12)}...`} |
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.
Can you open an issue to decide how we display deviceIds to the user. There is a previous discussion here.
style, | ||
}: DeviceNameWithIconProps) => { | ||
const {formatMessage} = useIntl(); | ||
console.log({name}); |
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.
remove before merge
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.
.then(val => { | ||
if (val == 'ACCEPT') | ||
queryClient.invalidateQueries({queryKey: ['projectMembers']}); | ||
navigation.navigate('InviteAccepted', {...route.params}); |
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 realized there is no code path for when an invite is rejected, nor do we have a screen for that. Maybe a follow-up?
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.
Yeah, I discussed this with Sabella. We are just going to show the error screen with a message. Ill create a ticket and we can do it as follow up
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 I missed something in my first review. An invite response can be ACCEPT
, REJECT
, ALREADY
(device being invited is already part of the project) or UNRECOGNIZED
(this is an unexpected/unknown response, I think actually the method should throw instead of resolving with this response). Currently all responses result in navigation to the InviteAccepted screen. I think maybe we need to show the different responses on that screen.
I updated so it only navigates on accept. Ill create a ticket for handling the other states (Im just going to throw them and pass a message to the error screen) |
… resolved" This reverts commit 7fc1ed2.
2fbdd5e
to
1a1d0ab
Compare
Allows user to list devices on the same network, and invite them to the project!
to note:
There seems to be a lot of files that were edited, but a large number of them are just SVGs (aka, this isn't a huge PR)