Skip to content

Commit d5f7849

Browse files
WIP
1 parent 1915fa3 commit d5f7849

6 files changed

Lines changed: 213 additions & 174 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
- When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions.
88
- Keep types accurate. Do not use casts or misleading annotations to mask a real type mismatch just to get around an issue; fix the type or fix the implementation.
99
- Do not add new exported functions, types, or constants unless they are required outside the file. Prefer file-local helpers for one-off implementation details and tests.
10+
- Do not use `navigation.setOptions` for header state in this repo. Pass header-driving state through route params so `getOptions` can read it synchronously, or use [`shared/stores/modal-header.tsx`](/Users/ChrisNojima/SourceCode/go/src/github.com/keybase/client/shared/stores/modal-header.tsx) when the flow already uses the shared modal header mechanism.
1011
- During refactors, do not delete existing guards, conditionals, or platform/test-specific behavior unless you have proven they are dead and the user asked for that behavior change. Port checks like `androidIsTestDevice` forward into the new code path instead of silently dropping them.
1112
- When addressing PR or review feedback, including bot or lint-style suggestions, do not apply it mechanically. Verify that the reported issue is real in this codebase and that the proposed fix is consistent with repo rules and improves correctness, behavior, or maintainability before making changes.

shared/constants/deeplinks.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const handleKeybaseLink = (link: string) => {
149149
}
150150
break
151151
case 'team-invite-link':
152-
useTeamsState.getState().dispatch.openInviteLink(parts[1] ?? '', parts[2] || '')
152+
navigateAppend({name: 'teamInviteLinkJoin', params: {inviteID: parts[1] ?? '', inviteKey: parts[2] || ''}})
153153
return
154154
default:
155155
break

shared/stores/teams.tsx

Lines changed: 14 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -781,11 +781,6 @@ type Store = T.Immutable<{
781781
teamAccessRequestsPending: Set<T.Teams.Teamname>
782782
newTeamWizard: T.Teams.NewTeamWizardState
783783
addMembersWizard: T.Teams.AddMembersWizardState
784-
errorInTeamJoin: string
785-
teamInviteDetails: T.Teams.TeamInviteState
786-
teamJoinSuccess: boolean
787-
teamJoinSuccessOpen: boolean
788-
teamJoinSuccessTeamName: string
789784
teamVersion: Map<T.Teams.TeamID, T.Teams.TeamVersion>
790785
teamIDToMembers: Map<T.Teams.TeamID, Map<string, T.Teams.MemberInfo>> // Used by chat sidebar until team loading gets easier
791786
teamIDToRetentionPolicy: Map<T.Teams.TeamID, T.Retention.RetentionPolicy>
@@ -804,7 +799,6 @@ const initialStore: Store = {
804799
errorInEditMember: emptyErrorInEditMember,
805800
errorInEditWelcomeMessage: '',
806801
errorInEmailInvite: emptyEmailInviteError,
807-
errorInTeamJoin: '',
808802
newTeamRequests: new Map(),
809803
newTeamWizard: newTeamWizardEmptyState,
810804
newTeams: new Set(),
@@ -817,10 +811,6 @@ const initialStore: Store = {
817811
teamIDToResetUsers: new Map(),
818812
teamIDToRetentionPolicy: new Map(),
819813
teamIDToWelcomeMessage: new Map(),
820-
teamInviteDetails: {inviteID: '', inviteKey: ''},
821-
teamJoinSuccess: false,
822-
teamJoinSuccessOpen: false,
823-
teamJoinSuccessTeamName: '',
824814
teamMemberToLastActivity: new Map(),
825815
teamMemberToTreeMemberships: new Map(),
826816
teamMeta: new Map(),
@@ -909,7 +899,7 @@ export type State = Store & {
909899
fullName: string,
910900
loadingKey?: string
911901
) => void
912-
joinTeam: (teamname: string, deeplink?: boolean) => void
902+
joinTeam: (teamname: string) => void
913903
launchNewTeamWizardOrModal: (subteamOf?: T.Teams.TeamID) => void
914904
leaveTeam: (teamname: string, permanent: boolean, context: 'teams' | 'chat') => void
915905
loadTeam: (teamID: T.Teams.TeamID, _subscribe?: boolean) => void
@@ -922,17 +912,14 @@ export type State = Store & {
922912
notifyTreeMembershipsPartial: (membership: T.RPCChat.Keybase1.TeamTreeMembership) => void
923913
notifyTeamTeamRoleMapChanged: (newVersion: number) => void
924914
onEngineIncomingImpl: (action: EngineGen.Actions) => void
925-
openInviteLink: (inviteID: string, inviteKey: string) => void
926915
onGregorPushState: (gs: Array<{md: T.RPCGen.Gregor1.Metadata; item: T.RPCGen.Gregor1.Item}>) => void
927916
reAddToTeam: (teamID: T.Teams.TeamID, username: string) => void
928917
refreshTeamRoleMap: () => void
929918
removeMember: (teamID: T.Teams.TeamID, username: string) => void
930919
removePendingInvite: (teamID: T.Teams.TeamID, inviteID: string) => void
931920
renameTeam: (oldName: string, newName: string) => void
932-
requestInviteLinkDetails: () => void
933921
resetErrorInEmailInvite: () => void
934922
resetState: () => void
935-
resetTeamJoin: () => void
936923
resetTeamMetaStale: () => void
937924
saveChannelMembership: (
938925
teamID: T.Teams.TeamID,
@@ -1711,30 +1698,12 @@ export const useTeamsState = Z.createZustand<State>('teams', (set, get) => {
17111698
}
17121699
ignorePromise(f())
17131700
},
1714-
joinTeam: (teamname, deeplink) => {
1715-
set(s => {
1716-
s.teamInviteDetails.inviteDetails = undefined
1717-
})
1718-
1701+
joinTeam: teamname => {
17191702
const f = async () => {
1720-
// In the deeplink flow, a modal is displayed which runs `joinTeam` (or an
1721-
// alternative flow, but we're not concerned with that here). In that case,
1722-
// we can fully manage the UX from inside of this handler.
1723-
// In the "Join team" flow, user pastes their link into the input box, which
1724-
// then calls `joinTeam` on its own. Since we need to switch to another modal,
1725-
// we simply plumb `deeplink` into the `promptInviteLinkJoin` handler and
1726-
// do the nav in the modal.
1727-
get().dispatch.resetTeamJoin()
17281703
try {
1729-
const result = await T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener({
1704+
await T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener({
17301705
customResponseIncomingCallMap: {
17311706
'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => {
1732-
set(s => {
1733-
s.teamInviteDetails.inviteDetails = T.castDraft(params.details)
1734-
})
1735-
if (!deeplink) {
1736-
navigateAppend('teamInviteLinkJoin', true)
1737-
}
17381707
set(s => {
17391708
s.dispatch.dynamic.respondToInviteLink = wrapErrors((accept: boolean) => {
17401709
set(s => {
@@ -1743,28 +1712,25 @@ export const useTeamsState = Z.createZustand<State>('teams', (set, get) => {
17431712
response.result(accept)
17441713
})
17451714
})
1715+
navigateAppend(
1716+
{
1717+
name: 'teamInviteLinkJoin',
1718+
params: {
1719+
inviteDetails: params.details,
1720+
inviteKey: teamname,
1721+
},
1722+
},
1723+
true
1724+
)
17461725
},
17471726
},
17481727
incomingCallMap: {},
17491728
params: {tokenOrName: teamname},
17501729
waitingKey: S.waitingKeyTeamsJoinTeam,
17511730
})
1752-
set(s => {
1753-
s.teamJoinSuccess = true
1754-
s.teamJoinSuccessOpen = result.wasOpenTeam
1755-
s.teamJoinSuccessTeamName = result.wasTeamName ? teamname : ''
1756-
})
17571731
} catch (error) {
17581732
if (error instanceof RPCError) {
1759-
const desc =
1760-
error.code === T.RPCGen.StatusCode.scteaminvitebadtoken
1761-
? 'Sorry, that team name or token is not valid.'
1762-
: error.code === T.RPCGen.StatusCode.scnotfound
1763-
? 'This invitation is no longer valid, or has expired.'
1764-
: error.desc
1765-
set(s => {
1766-
s.errorInTeamJoin = desc
1767-
})
1733+
logger.info(error.message)
17681734
}
17691735
} finally {
17701736
set(s => {
@@ -2129,14 +2095,6 @@ export const useTeamsState = Z.createZustand<State>('teams', (set, get) => {
21292095
new Set<T.Teams.Teamname>(bodyToJSON(chosenChannels?.item.body) as Array<string>)
21302096
)
21312097
},
2132-
openInviteLink: (inviteID, inviteKey) => {
2133-
set(s => {
2134-
s.teamInviteDetails.inviteDetails = undefined
2135-
s.teamInviteDetails.inviteID = inviteID
2136-
s.teamInviteDetails.inviteKey = inviteKey
2137-
})
2138-
navigateAppend('teamInviteLinkJoin')
2139-
},
21402098
reAddToTeam: (teamID, username) => {
21412099
const f = async () => {
21422100
try {
@@ -2222,46 +2180,13 @@ export const useTeamsState = Z.createZustand<State>('teams', (set, get) => {
22222180
}
22232181
ignorePromise(f())
22242182
},
2225-
requestInviteLinkDetails: () => {
2226-
const f = async () => {
2227-
try {
2228-
const details = await T.RPCGen.teamsGetInviteLinkDetailsRpcPromise({
2229-
inviteID: get().teamInviteDetails.inviteID,
2230-
})
2231-
set(s => {
2232-
s.teamInviteDetails.inviteDetails = T.castDraft(details)
2233-
})
2234-
} catch (error) {
2235-
if (error instanceof RPCError) {
2236-
const desc =
2237-
error.code === T.RPCGen.StatusCode.scteaminvitebadtoken
2238-
? 'Sorry, that invite token is not valid.'
2239-
: error.code === T.RPCGen.StatusCode.scnotfound
2240-
? 'This invitation is no longer valid, or has expired.'
2241-
: error.desc
2242-
set(s => {
2243-
s.errorInTeamJoin = desc
2244-
})
2245-
}
2246-
}
2247-
}
2248-
ignorePromise(f())
2249-
},
22502183
resetErrorInEmailInvite: () => {
22512184
set(s => {
22522185
s.errorInEmailInvite.message = ''
22532186
s.errorInEmailInvite.malformed = new Set()
22542187
})
22552188
},
22562189
resetState: Z.defaultReset,
2257-
resetTeamJoin: () => {
2258-
set(s => {
2259-
s.errorInTeamJoin = ''
2260-
s.teamJoinSuccess = false
2261-
s.teamJoinSuccessOpen = false
2262-
s.teamJoinSuccessTeamName = ''
2263-
})
2264-
},
22652190
resetTeamMetaStale: () => {
22662191
set(s => {
22672192
s.teamMetaStale = true

shared/teams/join-team/container.tsx

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,110 @@
11
import * as C from '@/constants'
2-
import upperFirst from 'lodash/upperFirst'
2+
import * as T from '@/constants/types'
3+
import {wrapErrors} from '@/constants/utils'
4+
import * as Kb from '@/common-adapters'
5+
import type {RootParamList} from '@/router-v2/route-params'
36
import {useTeamsState} from '@/stores/teams'
7+
import {RPCError} from '@/util/errors'
8+
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
9+
import upperFirst from 'lodash/upperFirst'
410
import * as React from 'react'
5-
import * as Kb from '@/common-adapters'
11+
import {useNavigation} from '@react-navigation/native'
612

7-
type OwnProps = {initialTeamname?: string}
13+
type OwnProps = {initialTeamname?: string; success?: boolean}
814

9-
const Container = (ownProps: OwnProps) => {
10-
const initialTeamname = ownProps.initialTeamname
11-
const errorText = useTeamsState(s => upperFirst(s.errorInTeamJoin))
12-
const open = useTeamsState(s => s.teamJoinSuccessOpen)
13-
const success = useTeamsState(s => s.teamJoinSuccess)
14-
const successTeamName = useTeamsState(s => s.teamJoinSuccessTeamName)
15-
const navigateUp = C.Router2.navigateUp
16-
const onBack = () => {
17-
navigateUp()
15+
const getJoinTeamError = (error: unknown) => {
16+
if (error instanceof RPCError) {
17+
return (
18+
error.code === T.RPCGen.StatusCode.scteaminvitebadtoken
19+
? 'Sorry, that team name or token is not valid.'
20+
: error.code === T.RPCGen.StatusCode.scnotfound
21+
? 'This invitation is no longer valid, or has expired.'
22+
: error.desc
23+
)
1824
}
19-
const joinTeam = useTeamsState(s => s.dispatch.joinTeam)
20-
const onJoinTeam = joinTeam
25+
return error instanceof Error ? error.message : 'Something went wrong.'
26+
}
2127

28+
const Container = ({initialTeamname, success: successParam}: OwnProps) => {
29+
const [errorText, setErrorText] = React.useState('')
30+
const [open, setOpen] = React.useState(false)
31+
const [successTeamName, setSuccessTeamName] = React.useState('')
2232
const [name, _setName] = React.useState(initialTeamname ?? '')
33+
const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener)
34+
const navigation = useNavigation<NativeStackNavigationProp<RootParamList, 'teamJoinTeamDialog'>>()
35+
const navigateUp = C.Router2.navigateUp
36+
const success = !!successParam
37+
2338
const setName = (n: string) => _setName(n.toLowerCase())
24-
const resetTeamJoin = useTeamsState(s => s.dispatch.resetTeamJoin)
39+
const onBack = () => navigateUp()
40+
2541
React.useEffect(() => {
26-
resetTeamJoin()
27-
}, [resetTeamJoin])
42+
_setName(initialTeamname ?? '')
43+
setErrorText('')
44+
setOpen(false)
45+
setSuccessTeamName('')
46+
if (successParam) {
47+
navigation.setParams({initialTeamname, success: false})
48+
}
49+
}, [initialTeamname, navigation, successParam])
2850

2951
const onSubmit = () => {
30-
onJoinTeam(name)
52+
setErrorText('')
53+
setOpen(false)
54+
setSuccessTeamName('')
55+
joinTeam(
56+
[
57+
{
58+
customResponseIncomingCallMap: {
59+
'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => {
60+
const currentDispatch = useTeamsState.getState().dispatch
61+
useTeamsState.setState({
62+
dispatch: {
63+
...currentDispatch,
64+
dynamic: {
65+
...currentDispatch.dynamic,
66+
respondToInviteLink: wrapErrors((accept: boolean) => {
67+
const latestDispatch = useTeamsState.getState().dispatch
68+
useTeamsState.setState({
69+
dispatch: {
70+
...latestDispatch,
71+
dynamic: {
72+
...latestDispatch.dynamic,
73+
respondToInviteLink: undefined,
74+
},
75+
},
76+
})
77+
response.result(accept)
78+
}),
79+
},
80+
},
81+
})
82+
C.Router2.navigateAppend(
83+
{
84+
name: 'teamInviteLinkJoin',
85+
params: {
86+
inviteDetails: params.details,
87+
inviteKey: name,
88+
},
89+
},
90+
true
91+
)
92+
},
93+
},
94+
incomingCallMap: {},
95+
params: {tokenOrName: name},
96+
waitingKey: C.waitingKeyTeamsJoinTeam,
97+
},
98+
],
99+
result => {
100+
setOpen(result.wasOpenTeam)
101+
setSuccessTeamName(result.wasTeamName ? name : '')
102+
navigation.setParams({initialTeamname, success: true})
103+
},
104+
error => {
105+
setErrorText(upperFirst(getJoinTeamError(error)))
106+
}
107+
)
31108
}
32109

33110
return (
@@ -74,15 +151,15 @@ const Container = (ownProps: OwnProps) => {
74151
)}
75152
</Kb.ScrollView>
76153
<Kb.Box2 direction="vertical" centerChildren={true} fullWidth={true} style={styles.modalFooter}>
77-
<Kb.ButtonBar align="center" direction="row" fullWidth={true} style={styles.buttonBar}>
78-
<Kb.WaitingButton
79-
fullWidth={true}
80-
label={success ? 'Close' : 'Continue'}
81-
onClick={success ? onBack : onSubmit}
82-
type={success ? 'Dim' : 'Default'}
83-
waitingKey={C.waitingKeyTeamsJoinTeam}
84-
/>
85-
</Kb.ButtonBar>
154+
<Kb.ButtonBar align="center" direction="row" fullWidth={true} style={styles.buttonBar}>
155+
<Kb.WaitingButton
156+
fullWidth={true}
157+
label={success ? 'Close' : 'Continue'}
158+
onClick={success ? onBack : onSubmit}
159+
type={success ? 'Dim' : 'Default'}
160+
waitingKey={C.waitingKeyTeamsJoinTeam}
161+
/>
162+
</Kb.ButtonBar>
86163
</Kb.Box2>
87164
</>
88165
)

0 commit comments

Comments
 (0)