From 9f95622f7c4d4a7d39bca6e09dc46c6a8d12088a Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 15:56:29 -0400 Subject: [PATCH 01/45] team slice --- shared/profile/add-to-team.tsx | 185 +++++++++++++++++++++++++-------- shared/stores/teams.tsx | 98 ----------------- 2 files changed, 143 insertions(+), 140 deletions(-) diff --git a/shared/profile/add-to-team.tsx b/shared/profile/add-to-team.tsx index 3c8de4099e82..bcde245c9b51 100644 --- a/shared/profile/add-to-team.tsx +++ b/shared/profile/add-to-team.tsx @@ -1,10 +1,11 @@ import * as C from '@/constants' import * as React from 'react' import * as Teams from '@/stores/teams' -import type * as T from '@/constants/types' +import * as T from '@/constants/types' import {FloatingRolePicker, sendNotificationFooter} from '@/teams/role-picker' import * as Kb from '@/common-adapters' import {InlineDropdown} from '@/common-adapters/dropdown' +import logger from '@/logger' const getOwnerDisabledReason = ( selected: Set, @@ -22,29 +23,56 @@ const getOwnerDisabledReason = ( .find(v => !!v) } +const makeAddUserToTeamsResult = ( + user: string, + teamsAddedTo: ReadonlyArray, + errorAddingTo: ReadonlyArray +) => { + let result = '' + if (teamsAddedTo.length) { + result += `${user} was added to ` + if (teamsAddedTo.length > 3) { + result += `${teamsAddedTo[0]}, ${teamsAddedTo[1]}, and ${teamsAddedTo.length - 2} teams.` + } else if (teamsAddedTo.length === 3) { + result += `${teamsAddedTo[0]}, ${teamsAddedTo[1]}, and ${teamsAddedTo[2]}.` + } else if (teamsAddedTo.length === 2) { + result += `${teamsAddedTo[0]} and ${teamsAddedTo[1]}.` + } else { + result += `${teamsAddedTo[0]}.` + } + } + + if (errorAddingTo.length) { + result += result.length > 0 ? ' But we ' : 'We ' + result += `were unable to add ${user} to ${errorAddingTo.join(', ')}.` + } + + return result +} + type OwnProps = {username: string} const Container = (ownProps: OwnProps) => { const {username: them} = ownProps const roles = Teams.useTeamsState(s => s.teamRoleMap.roles) const teams = Teams.useTeamsState(s => s.teamMeta) - const addUserToTeamsResults = Teams.useTeamsState(s => s.addUserToTeamsResults) - const addUserToTeamsState = Teams.useTeamsState(s => s.addUserToTeamsState) - const clearAddUserToTeamsResults = Teams.useTeamsState(s => s.dispatch.clearAddUserToTeamsResults) - const addUserToTeams = Teams.useTeamsState(s => s.dispatch.addUserToTeams) - const teamProfileAddList = Teams.useTeamsState(s => s.teamProfileAddList) + const teamNameToID = Teams.useTeamsState(s => s.teamNameToID) const waiting = C.Waiting.useAnyWaiting(C.waitingKeyTeamsProfileAddList) - const _onAddToTeams = addUserToTeams - const getTeamProfileAddList = Teams.useTeamsState(s => s.dispatch.getTeamProfileAddList) - const resetTeamProfileAddList = Teams.useTeamsState(s => s.dispatch.resetTeamProfileAddList) const clearModals = C.Router2.clearModals const navigateUp = C.Router2.navigateUp + const loadTeamProfileAddList = C.useRPC(T.RPCGen.teamsTeamProfileAddListRpcPromise) + const addUserToTeam = C.useRPC(T.RPCGen.teamsTeamAddMemberRpcPromise) + const [teamProfileAddList, setTeamProfileAddList] = React.useState>([]) + const [addUserToTeamsResults, setAddUserToTeamsResults] = React.useState('') + const [addUserToTeamsState, setAddUserToTeamsState] = + React.useState('notStarted') + const teamListRequestID = React.useRef(0) + const submitRequestID = React.useRef(0) // TODO Y2K-1086 use team ID given in teamProfileAddList to avoid this mapping const _teamNameToRole = [...teams.values()].reduce( (res, curr) => res.set(curr.teamname, roles.get(curr.id)?.role || 'none'), new Map() ) - const onAddToTeams = (role: T.Teams.TeamRoleType, teams: Array) => _onAddToTeams(role, teams, them) const [selectedTeams, setSelectedTeams] = React.useState(new Set()) const [rolePickerOpen, setRolePickerOpen] = React.useState(false) const [selectedRole, setSelectedRole] = React.useState('writer') @@ -53,17 +81,117 @@ const Container = (ownProps: OwnProps) => { const ownerDisabledReason = getOwnerDisabledReason(selectedTeams, _teamNameToRole) React.useEffect(() => { - clearAddUserToTeamsResults() - getTeamProfileAddList(them) - }, [clearAddUserToTeamsResults, getTeamProfileAddList, them]) + return () => { + teamListRequestID.current += 1 + submitRequestID.current += 1 + } + }, []) + + const loadTeamList = React.useEffectEvent(() => { + const requestID = teamListRequestID.current + 1 + teamListRequestID.current = requestID + loadTeamProfileAddList( + [{username: them}, C.waitingKeyTeamsProfileAddList], + result => { + if (teamListRequestID.current !== requestID) { + return + } + const teamlist = (result ?? []).map(team => ({ + disabledReason: team.disabledReason, + open: team.open, + teamName: team.teamName.parts ? team.teamName.parts.join('.') : '', + })) + teamlist.sort((a, b) => a.teamName.localeCompare(b.teamName)) + setTeamProfileAddList(teamlist) + }, + error => { + if (teamListRequestID.current !== requestID) { + return + } + logger.info(`Failed to load profile add-to-team list for ${them}: ${error.message}`) + setTeamProfileAddList([]) + } + ) + }) + + const onAddToTeams = React.useEffectEvent(async (role: T.Teams.TeamRoleType, teams: Array) => { + const requestID = submitRequestID.current + 1 + submitRequestID.current = requestID + setAddUserToTeamsResults('') + setAddUserToTeamsState('notStarted') + + const teamsAddedTo: Array = [] + const errorAddingTo: Array = [] + + for (const team of teams) { + const teamID = teamNameToID.get(team) + if (!teamID) { + logger.warn(`no team ID found for ${team}`) + errorAddingTo.push(team) + continue + } + + const added = await new Promise(resolve => { + addUserToTeam( + [ + { + email: '', + phone: '', + role: T.RPCGen.TeamRole[role], + sendChatNotification: true, + teamID, + username: them, + }, + [C.waitingKeyTeamsTeam(teamID), C.waitingKeyTeamsAddUserToTeams(them)], + ], + () => resolve(true), + _ => resolve(false) + ) + }) + + if (submitRequestID.current !== requestID) { + return + } + + if (added) { + teamsAddedTo.push(team) + } else { + errorAddingTo.push(team) + } + } + + if (submitRequestID.current !== requestID) { + return + } + + const result = makeAddUserToTeamsResult(them, teamsAddedTo, errorAddingTo) + setAddUserToTeamsResults(result) + if (errorAddingTo.length > 0) { + setAddUserToTeamsState('failed') + loadTeamList() + } else { + setAddUserToTeamsState('succeeded') + clearModals() + } + }) + + React.useEffect(() => { + setAddUserToTeamsResults('') + setAddUserToTeamsState('notStarted') + setSelectedTeams(new Set()) + setRolePickerOpen(false) + setSelectedRole('writer') + setSendNotification(true) + setTeamProfileAddList([]) + loadTeamList() + }, [loadTeamList, them]) const onBack = () => { navigateUp() - resetTeamProfileAddList() } const onSave = () => { - onAddToTeams(selectedRole, [...selectedTeams]) + void onAddToTeams(selectedRole, [...selectedTeams]) } const toggleTeamSelected = (teamName: string, selected: boolean) => { @@ -102,15 +230,6 @@ const Container = (ownProps: OwnProps) => { } const onToggle = toggleTeamSelected - React.useEffect(() => { - if (addUserToTeamsState === 'succeeded') { - clearModals() - resetTeamProfileAddList() - } else if (addUserToTeamsState === 'failed') { - getTeamProfileAddList(them) - } - }, [addUserToTeamsState, clearModals, resetTeamProfileAddList, getTeamProfileAddList, them]) - const selectedTeamCount = selectedTeams.size return ( @@ -223,24 +342,6 @@ type RowProps = { them: string } -// This state is handled by the state wrapper in the container -export type ComponentState = { - selectedTeams: Set - onSave: () => void - onToggle: (teamName: string, selected: boolean) => void -} - -export type AddToTeamProps = { - title: string - addUserToTeamsResults: string - addUserToTeamsState: T.Teams.AddUserToTeamsState - loadTeamList: () => void - onBack: () => void - teamProfileAddList: ReadonlyArray - them: string - waiting: boolean -} - const TeamRow = (props: RowProps) => { return ( props.onCheck(!props.checked) : undefined}> diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 56ab146d06c8..b66e11d9dee5 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -753,8 +753,6 @@ export const maybeGetMostRecentValidInviteLink = (inviteLinks: ReadonlyArray> channelSelectedMembers: Map> deletedTeams: Array @@ -799,14 +797,11 @@ type Store = T.Immutable<{ treeLoaderTeamIDToSparseMemberInfos: Map> teamMemberToTreeMemberships: Map> teamMemberToLastActivity: Map> - teamProfileAddList: Array }> const initialStore: Store = { activityLevels: {channels: new Map(), loaded: false, teams: new Map()}, addMembersWizard: addMembersWizardEmptyState, - addUserToTeamsResults: '', - addUserToTeamsState: 'notStarted', channelInfo: new Map(), channelSelectedMembers: new Map(), deletedTeams: [], @@ -843,7 +838,6 @@ const initialStore: Store = { teamMetaSubscribeCount: 0, teamNameToID: new Map(), teamNameToLoadingInvites: new Map(), - teamProfileAddList: [], teamRoleMap: {latestKnownVersion: -1, loadedVersion: -1, roles: new Map()}, teamSelectedChannels: new Map(), teamSelectedMembers: new Map(), @@ -878,7 +872,6 @@ export type State = Store & { sendChatNotification: boolean, fromTeamBuilder?: boolean ) => void - addUserToTeams: (role: T.Teams.TeamRoleType, teams: Array, user: string) => void cancelAddMembersWizard: () => void channelSetMemberSelected: ( conversationIDKey: T.Chat.ConversationIDKey, @@ -887,7 +880,6 @@ export type State = Store & { clearAll?: boolean ) => void checkRequestedAccess: (teamname: string) => void - clearAddUserToTeamsResults: () => void clearNavBadges: () => void createNewTeam: ( teamname: string, @@ -912,7 +904,6 @@ export type State = Store & { getMembers: (teamID: T.Teams.TeamID) => Promise getTeamRetentionPolicy: (teamID: T.Teams.TeamID) => void getTeams: (subscribe?: boolean, forceReload?: boolean) => void - getTeamProfileAddList: (username: string) => void ignoreRequest: (teamID: T.Teams.TeamID, teamname: string, username: string) => void inviteToTeamByEmail: ( invitees: string, @@ -956,7 +947,6 @@ export type State = Store & { resetState: () => void resetTeamJoin: () => void resetTeamMetaStale: () => void - resetTeamProfileAddList: () => void saveChannelMembership: ( teamID: T.Teams.TeamID, oldChannelState: T.Teams.ChannelMembershipState, @@ -1256,66 +1246,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - addUserToTeams: (role, teams, user) => { - const f = async () => { - const teamsAddedTo: Array = [] - const errorAddingTo: Array = [] - for (const team of teams) { - try { - const teamID = getTeamID(get(), team) - if (teamID === T.Teams.noTeamID) { - logger.warn(`no team ID found for ${team}`) - errorAddingTo.push(team) - continue - } - await T.RPCGen.teamsTeamAddMemberRpcPromise( - { - email: '', - phone: '', - role: T.RPCGen.TeamRole[role], - sendChatNotification: true, - teamID, - username: user, - }, - [S.waitingKeyTeamsTeam(teamID), S.waitingKeyTeamsAddUserToTeams(user)] - ) - teamsAddedTo.push(team) - } catch { - errorAddingTo.push(team) - } - } - - // TODO: We should split these results into two messages, showing one in green and - // the other in red instead of lumping them together. - let result = '' - if (teamsAddedTo.length) { - result += `${user} was added to ` - if (teamsAddedTo.length > 3) { - result += `${teamsAddedTo[0]}, ${teamsAddedTo[1]}, and ${teamsAddedTo.length - 2} teams.` - } else if (teamsAddedTo.length === 3) { - result += `${teamsAddedTo[0]}, ${teamsAddedTo[1]}, and ${teamsAddedTo[2]}.` - } else if (teamsAddedTo.length === 2) { - result += `${teamsAddedTo[0]} and ${teamsAddedTo[1]}.` - } else { - result += `${teamsAddedTo[0]}.` - } - } - - if (errorAddingTo.length) { - if (result.length > 0) { - result += ' But we ' - } else { - result += 'We ' - } - result += `were unable to add ${user} to ${errorAddingTo.join(', ')}.` - } - set(s => { - s.addUserToTeamsResults = result - s.addUserToTeamsState = errorAddingTo.length > 0 ? 'failed' : 'succeeded' - }) - } - ignorePromise(f()) - }, cancelAddMembersWizard: () => { set(s => { s.addMembersWizard = T.castDraft({...addMembersWizardEmptyState}) @@ -1348,12 +1278,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - clearAddUserToTeamsResults: () => { - set(s => { - s.addUserToTeamsResults = '' - s.addUserToTeamsState = 'notStarted' - }) - }, clearNavBadges: () => { const f = async () => { try { @@ -1647,23 +1571,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } return }, - getTeamProfileAddList: username => { - const f = async () => { - const res = - (await T.RPCGen.teamsTeamProfileAddListRpcPromise({username}, S.waitingKeyTeamsProfileAddList)) ?? - [] - const teamlist = res.map(team => ({ - disabledReason: team.disabledReason, - open: team.open, - teamName: team.teamName.parts ? team.teamName.parts.join('.') : '', - })) - teamlist.sort((a, b) => a.teamName.localeCompare(b.teamName)) - set(s => { - s.teamProfileAddList = teamlist - }) - } - ignorePromise(f()) - }, getTeamRetentionPolicy: teamID => { const f = async () => { let retentionPolicy = Util.makeRetentionPolicy() @@ -2417,11 +2324,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { s.teamMetaStale = true }) }, - resetTeamProfileAddList: () => { - set(s => { - s.teamProfileAddList = [] - }) - }, saveChannelMembership: (teamID, oldChannelState, newChannelState) => { const f = async () => { const waitingKey = S.waitingKeyTeamsTeam(teamID) From ade25ee870ed2dfab13f402ebc2328fc10e09ee4 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:05:58 -0400 Subject: [PATCH 02/45] WIP --- shared/stores/teams.tsx | 36 -------------------------- shared/stores/tests/teams.test.ts | 9 ------- shared/teams/container.tsx | 19 +++++++++----- shared/teams/edit-team-description.tsx | 14 +++++++--- shared/teams/get-options.tsx | 9 +++++-- shared/teams/main/index.tsx | 11 ++++---- shared/teams/team/team-info.tsx | 12 ++++++--- 7 files changed, 43 insertions(+), 67 deletions(-) diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index b66e11d9dee5..8349ead61e92 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -757,7 +757,6 @@ type Store = T.Immutable<{ channelSelectedMembers: Map> deletedTeams: Array errorInAddToTeam: string - errorInEditDescription: string errorInEditMember: {error: string; teamID: T.Teams.TeamID; username: string} errorInEditWelcomeMessage: string errorInEmailInvite: T.Teams.EmailInviteError @@ -782,8 +781,6 @@ type Store = T.Immutable<{ teamSelectedChannels: Map> teamSelectedMembers: Map> teamAccessRequestsPending: Set - teamListFilter: string - teamListSort: T.Teams.TeamListSort newTeamWizard: T.Teams.NewTeamWizardState addMembersWizard: T.Teams.AddMembersWizardState errorInTeamJoin: string @@ -806,7 +803,6 @@ const initialStore: Store = { channelSelectedMembers: new Map(), deletedTeams: [], errorInAddToTeam: '', - errorInEditDescription: '', errorInEditMember: emptyErrorInEditMember, errorInEditWelcomeMessage: '', errorInEmailInvite: emptyEmailInviteError, @@ -829,8 +825,6 @@ const initialStore: Store = { teamJoinSuccess: false, teamJoinSuccessOpen: false, teamJoinSuccessTeamName: '', - teamListFilter: '', - teamListSort: 'role', teamMemberToLastActivity: new Map(), teamMemberToTreeMemberships: new Map(), teamMeta: new Map(), @@ -897,7 +891,6 @@ export type State = Store & { deleteTeam: (teamID: T.Teams.TeamID) => void eagerLoadTeams: () => void editMembership: (teamID: T.Teams.TeamID, usernames: Array, role: T.Teams.TeamRoleType) => void - editTeamDescription: (teamID: T.Teams.TeamID, description: string) => void finishNewTeamWizard: () => void finishedAddMembersWizard: () => void getActivityForTeams: () => void @@ -975,8 +968,6 @@ export type State = Store & { ) => void setNewTeamRequests: (newTeamRequests: Map>) => void setPublicity: (teamID: T.Teams.TeamID, settings: T.Teams.PublicitySettings) => void - setTeamListFilter: (filter: string) => void - setTeamListSort: (sortOrder: T.Teams.TeamListSort) => void setTeamRetentionPolicy: (teamID: T.Teams.TeamID, policy: T.Retention.RetentionPolicy) => void setTeamRoleMapLatestKnownVersion: (version: number) => void setTeamSawChatBanner: () => void @@ -1453,23 +1444,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - editTeamDescription: (teamID, description) => { - set(s => { - s.errorInEditDescription = '' - }) - const f = async () => { - try { - await T.RPCGen.teamsSetTeamShowcaseRpcPromise({description, teamID}, S.waitingKeyTeamsTeam(teamID)) - } catch (error) { - set(s => { - if (error instanceof RPCError) { - s.errorInEditDescription = error.message - } - }) - } - } - ignorePromise(f()) - }, finishNewTeamWizard: () => { set(s => { s.newTeamWizard.error = undefined @@ -2491,16 +2465,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - setTeamListFilter: filter => { - set(s => { - s.teamListFilter = filter - }) - }, - setTeamListSort: (sortOrder: T.Teams.TeamListSort) => { - set(s => { - s.teamListSort = sortOrder - }) - }, setTeamRetentionPolicy: (teamID, policy) => { const f = async () => { try { diff --git a/shared/stores/tests/teams.test.ts b/shared/stores/tests/teams.test.ts index 493046f68d5c..e44f9820c0af 100644 --- a/shared/stores/tests/teams.test.ts +++ b/shared/stores/tests/teams.test.ts @@ -37,15 +37,6 @@ test('channel and member selection can add, remove, and clear all choices', () = expect(useTeamsState.getState().teamSelectedMembers.has(parentTeamID)).toBe(false) }) -test('team list controls update store-local view state', () => { - const state = useTeamsState.getState() - - state.dispatch.setTeamListFilter('acme') - state.dispatch.setTeamListSort('activity') - expect(useTeamsState.getState().teamListFilter).toBe('acme') - expect(useTeamsState.getState().teamListSort).toBe('activity') -}) - test('add members role updates synchronize top-level and per-member roles', () => { useTeamsState.setState({ addMembersWizard: { diff --git a/shared/teams/container.tsx b/shared/teams/container.tsx index 6d6f60555d68..846362bf3da7 100644 --- a/shared/teams/container.tsx +++ b/shared/teams/container.tsx @@ -42,11 +42,16 @@ const orderTeams = ( }) } -const Connected = () => { +type Props = { + filter?: string + sort?: T.Teams.TeamListSort +} + +const Connected = ({filter = '', sort = 'role'}: Props) => { const data = Teams.useTeamsState( C.useShallow(s => { - const {deletedTeams, activityLevels, teamMeta, teamListFilter, dispatch} = s - const {newTeamRequests, newTeams, teamListSort, teamIDToResetUsers} = s + const {deletedTeams, activityLevels, teamMeta, dispatch} = s + const {newTeamRequests, newTeams, teamIDToResetUsers} = s const {getTeams, launchNewTeamWizardOrModal} = dispatch return { activityLevels, @@ -56,17 +61,15 @@ const Connected = () => { newTeamRequests, newTeams, teamIDToResetUsers, - teamListFilter, - teamListSort, teamMeta, } }) ) const {activityLevels, deletedTeams, newTeamRequests, newTeams} = data - const {teamIDToResetUsers, teamListFilter: filter, teamListSort: sortOrder, teamMeta: _teams} = data + const {teamIDToResetUsers, teamMeta: _teams} = data const {getTeams, launchNewTeamWizardOrModal} = data - const teams = orderTeams(_teams, newTeamRequests, teamIDToResetUsers, newTeams, sortOrder, activityLevels, filter) + const teams = orderTeams(_teams, newTeamRequests, teamIDToResetUsers, newTeams, sort, activityLevels, filter) // subscribe to teams changes useTeamsSubscribe() @@ -83,6 +86,8 @@ const Connected = () => { onCreateTeam={onCreateTeam} onJoinTeam={onJoinTeam} deletedTeams={deletedTeams} + onChangeSort={sortOrder => C.Router2.navigateAppend({name: 'teamsRoot', params: {filter, sort: sortOrder}}, true)} + sortOrder={sort} teams={teams} /> diff --git a/shared/teams/edit-team-description.tsx b/shared/teams/edit-team-description.tsx index 38ead0f44cf9..3756267f5684 100644 --- a/shared/teams/edit-team-description.tsx +++ b/shared/teams/edit-team-description.tsx @@ -12,8 +12,8 @@ const EditTeamDescription = (props: Props) => { const teamname = Teams.useTeamsState(s => Teams.getTeamNameFromID(s, teamID)) const waitingKey = C.waitingKeyTeamsTeam(teamID) const waiting = C.Waiting.useAnyWaiting(waitingKey) - const error = Teams.useTeamsState(s => s.errorInEditDescription) const origDescription = Teams.useTeamsState(s => s.teamDetails.get(teamID))?.description ?? '' + const editTeamDescription = C.useRPC(T.RPCGen.teamsSetTeamShowcaseRpcPromise) if (teamID === T.Teams.noTeamID || teamname === undefined) { throw new Error( @@ -22,10 +22,17 @@ const EditTeamDescription = (props: Props) => { } const [description, setDescription] = React.useState(origDescription) - const editTeamDescription = Teams.useTeamsState(s => s.dispatch.editTeamDescription) + const [error, setError] = React.useState('') const navigateUp = C.Router2.navigateUp - const onSave = () => editTeamDescription(teamID, description) + const onSave = () => { + setError('') + editTeamDescription( + [{description, teamID}, waitingKey], + () => {}, + error => setError(error.message) + ) + } const onClose = () => navigateUp() const wasWaitingRef = React.useRef(waiting) @@ -96,4 +103,3 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ })) export default EditTeamDescription - diff --git a/shared/teams/get-options.tsx b/shared/teams/get-options.tsx index f0cf90c88b0f..b7dddaffaba2 100644 --- a/shared/teams/get-options.tsx +++ b/shared/teams/get-options.tsx @@ -2,6 +2,9 @@ import * as Kb from '@/common-adapters' import {HeaderRightActions} from './main/header' import {useSafeNavigation} from '@/util/safe-navigation' import {useTeamsState} from '@/stores/teams' +import {useNavigation, useRoute} from '@react-navigation/native' +import type {RootParamList, RootRouteProps} from '@/router-v2/route-params' +import type {NativeStackNavigationProp} from '@react-navigation/native-stack' const useHeaderActions = () => { const nav = useSafeNavigation() @@ -13,9 +16,11 @@ const useHeaderActions = () => { } const TeamsFilter = () => { - const filterValue = useTeamsState(s => s.teamListFilter) + const {params} = useRoute>() + const navigation = useNavigation>() + const filterValue = params?.filter ?? '' const numTeams = useTeamsState(s => s.teamMeta.size) - const setFilter = useTeamsState(s => s.dispatch.setTeamListFilter) + const setFilter = (filter: string) => navigation.setParams({...params, filter}) return numTeams >= 20 ? ( + onChangeSort: (sortOrder: T.Teams.TeamListSort) => void onCreateTeam: () => void onJoinTeam: () => void + sortOrder: T.Teams.TeamListSort teams: ReadonlyArray } @@ -56,8 +57,7 @@ const sortOrderToTitle = { alphabetical: 'Alphabetical', role: 'Your role', } -const SortHeader = () => { - const onChangeSort = useTeamsState(s => s.dispatch.setTeamListSort) +const SortHeader = ({onChangeSort, sortOrder}: {onChangeSort: Props['onChangeSort']; sortOrder: Props['sortOrder']}) => { const makePopup = (p: Kb.Popup2Parms) => { const {attachTo, hidePopup} = p return ( @@ -85,7 +85,6 @@ const SortHeader = () => { } const {popup, showPopup, popupAnchor} = Kb.usePopup2(makePopup) - const sortOrder = useTeamsState(s => s.teamListSort) return ( @@ -105,12 +104,12 @@ const teamRowItemHeight = {height: teamRowHeight, type: 'fixed' as const} type TeamItem = T.Teams.TeamMeta const Teams = function Teams(p: Props) { - const {deletedTeams, teams, onCreateTeam, onJoinTeam} = p + const {deletedTeams, teams, onCreateTeam, onJoinTeam, onChangeSort, sortOrder} = p const listHeader = ( <> - + {deletedTeams.map(dt => ( { const [newName, _setName] = React.useState(_leafName) const setName = (newName: string) => _setName(newName.replace(/[^a-zA-Z0-9_]/, '')) const [description, setDescription] = React.useState(teamDetails?.description ?? '') + const [descError, setDescError] = React.useState('') const saveDisabled = (description === teamDetails?.description && newName === _leafName) || newName.length < 3 const waiting = C.Waiting.useAnyWaiting([C.waitingKeyTeamsTeam(teamID), C.waitingKeyTeamsRename]) + const editTeamDescription = C.useRPC(T.RPCGen.teamsSetTeamShowcaseRpcPromise) const errors = { - desc: Teams.useTeamsState(s => s.errorInEditDescription), + desc: descError, rename: C.Waiting.useAnyErrors(C.waitingKeyTeamsRename)?.message, } - const editTeamDescription = Teams.useTeamsState(s => s.dispatch.editTeamDescription) const renameTeam = Teams.useTeamsState(s => s.dispatch.renameTeam) const onSave = () => { if (newName !== _leafName) { renameTeam(teamname, parentTeamNameWithDot + newName) } if (description !== teamDetails?.description) { - editTeamDescription(teamID, description) + setDescError('') + editTeamDescription( + [{description, teamID}, C.waitingKeyTeamsTeam(teamID)], + () => {}, + error => setDescError(error.message) + ) } } const navigateAppend = C.Router2.navigateAppend From 2ec6f858ae84c1afa71b18d5b484c16efef11749 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:09:32 -0400 Subject: [PATCH 03/45] WIP --- shared/chat/new-team-dialog-container.tsx | 6 ------ shared/stores/teams.tsx | 22 +--------------------- shared/teams/new-team/index.tsx | 20 ++++++++------------ 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/shared/chat/new-team-dialog-container.tsx b/shared/chat/new-team-dialog-container.tsx index 45cf5c75fd4b..1cfe0ba2c5b5 100644 --- a/shared/chat/new-team-dialog-container.tsx +++ b/shared/chat/new-team-dialog-container.tsx @@ -2,27 +2,21 @@ import * as C from '@/constants' import * as Chat from '@/stores/chat' import {CreateNewTeam} from '../teams/new-team' import {useTeamsState} from '@/stores/teams' -import upperFirst from 'lodash/upperFirst' const NewTeamDialog = () => { const conversationIDKey = Chat.useChatContext(s => s.id) const baseTeam = '' - const errorText = useTeamsState(s => upperFirst(s.errorInTeamCreation)) const navigateUp = C.Router2.navigateUp const onCancel = () => { navigateUp() } - const resetErrorInTeamCreation = useTeamsState(s => s.dispatch.resetErrorInTeamCreation) const createNewTeamFromConversation = useTeamsState(s => s.dispatch.createNewTeamFromConversation) - const onClearError = resetErrorInTeamCreation const onSubmit = (teamname: string) => { createNewTeamFromConversation(conversationIDKey, teamname) } const props = { baseTeam, - errorText, onCancel, - onClearError, onSubmit, } return diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 8349ead61e92..8cec80fbc94d 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -766,7 +766,6 @@ type Store = T.Immutable<{ teamIDToResetUsers: Map> teamIDToWelcomeMessage: Map teamNameToLoadingInvites: Map> - errorInTeamCreation: string teamNameToID: Map teamMetaSubscribeCount: number // if >0 we are eagerly reloading team list teamnames: Set // TODO remove @@ -807,7 +806,6 @@ const initialStore: Store = { errorInEditWelcomeMessage: '', errorInEmailInvite: emptyEmailInviteError, errorInSettings: '', - errorInTeamCreation: '', errorInTeamJoin: '', newTeamRequests: new Map(), newTeamWizard: newTeamWizardEmptyState, @@ -936,7 +934,6 @@ export type State = Store & { requestInviteLinkDetails: () => void resetErrorInEmailInvite: () => void resetErrorInSettings: () => void - resetErrorInTeamCreation: () => void resetState: () => void resetTeamJoin: () => void resetTeamMetaStale: () => void @@ -1281,9 +1278,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { ignorePromise(f()) }, createNewTeam: (teamname, joinSubteam, fromChat, thenAddMembers) => { - set(s => { - s.errorInTeamCreation = '' - }) const f = async () => { try { const {teamID} = await T.RPCGen.teamsTeamCreateRpcPromise( @@ -1312,20 +1306,11 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { navigateAppend({name: 'profileEditAvatar', params: {createdTeam: true, teamID}}) } } - } catch (error) { - set(s => { - if (error instanceof RPCError) { - s.errorInTeamCreation = error.desc - } - }) - } + } catch {} } ignorePromise(f()) }, createNewTeamFromConversation: (conversationIDKey, teamname) => { - set(s => { - s.errorInTeamCreation = '' - }) const me = useCurrentUserState.getState().username const participantInfo = storeRegistry.getConvoState(conversationIDKey).participants // exclude bots from the newly created team, they can be added back later. @@ -2279,11 +2264,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { s.errorInSettings = '' }) }, - resetErrorInTeamCreation: () => { - set(s => { - s.errorInTeamCreation = '' - }) - }, resetState: Z.defaultReset, resetTeamJoin: () => { set(s => { diff --git a/shared/teams/new-team/index.tsx b/shared/teams/new-team/index.tsx index ba72e49f04ad..aa4fcd0dc632 100644 --- a/shared/teams/new-team/index.tsx +++ b/shared/teams/new-team/index.tsx @@ -10,9 +10,7 @@ const openSubteamInfo = () => openUrl('https://book.keybase.io/docs/teams/design type Props = { baseTeam?: string // if set we're creating a subteam of this teamname - errorText: string onCancel: () => void - onClearError: () => void onSubmit: (fullName: string, joinSubteam: boolean) => void } @@ -21,16 +19,19 @@ export const CreateNewTeam = (props: Props) => { const [name, setName] = React.useState('') const [joinSubteam, setJoinSubteam] = React.useState(true) const waiting = C.Waiting.useAnyWaiting(C.waitingKeyTeamsCreation) + const errorText = upperFirst(C.Waiting.useAnyErrors(C.waitingKeyTeamsCreation)?.message ?? '') + const dispatchClearWaiting = C.Waiting.useDispatchClearWaiting() const {baseTeam, onSubmit} = props const isSubteam = !!baseTeam - const onSubmitCb = () => (isSubteam ? onSubmit(baseTeam + '.' + name, joinSubteam) : onSubmit(name, false)) + const onSubmitCb = () => { + dispatchClearWaiting(C.waitingKeyTeamsCreation) + return isSubteam ? onSubmit(baseTeam + '.' + name, joinSubteam) : onSubmit(name, false) + } const disabled = name.length < 2 - // clear error we may have hit on unmount - const {onClearError} = props - React.useEffect(() => () => onClearError(), [onClearError]) + React.useEffect(() => () => dispatchClearWaiting(C.waitingKeyTeamsCreation), [dispatchClearWaiting]) return ( <> @@ -51,7 +52,7 @@ export const CreateNewTeam = (props: Props) => { /> ) : null} - {props.errorText ? {props.errorText} : null} + {errorText ? {errorText} : null} { const subteamOf = ownProps.subteamOf ?? T.Teams.noTeamID const baseTeam = Teams.useTeamsState(s => Teams.getTeamMeta(s, subteamOf).teamname) - const errorText = Teams.useTeamsState(s => upperFirst(s.errorInTeamCreation)) const navigateUp = C.Router2.navigateUp const onCancel = () => { navigateUp() } - const resetErrorInTeamCreation = Teams.useTeamsState(s => s.dispatch.resetErrorInTeamCreation) const createNewTeam = Teams.useTeamsState(s => s.dispatch.createNewTeam) - const onClearError = resetErrorInTeamCreation const onSubmit = (teamname: string, joinSubteam: boolean) => { createNewTeam(teamname, joinSubteam) } const props = { baseTeam, - errorText, onCancel, - onClearError, onSubmit, } return From 17aceac55d37931eaf2f36e3ac95d40fec4bff1e Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:13:08 -0400 Subject: [PATCH 04/45] WIP --- shared/constants/strings.tsx | 1 + shared/stores/teams.tsx | 36 +++++++----------------- shared/teams/team/index.tsx | 15 +++++++--- shared/teams/team/settings-tab/index.tsx | 26 ++++++++--------- 4 files changed, 35 insertions(+), 43 deletions(-) diff --git a/shared/constants/strings.tsx b/shared/constants/strings.tsx index bd5c4925a05e..a272b074f8b2 100644 --- a/shared/constants/strings.tsx +++ b/shared/constants/strings.tsx @@ -65,6 +65,7 @@ export const waitingKeyTeamsDeleteTeam = (teamID: T.Teams.TeamID) => `teamDelete export const waitingKeyTeamsLeaveTeam = (teamname: T.Teams.Teamname) => `teamLeave:${teamname}` export const waitingKeyTeamsRename = 'teams:rename' export const waitingKeyTeamsLoadWelcomeMessage = (teamID: T.Teams.TeamID) => `loadWelcomeMessage:${teamID}` +export const waitingKeyTeamsSetRetentionPolicy = (teamID: T.Teams.TeamID) => `teamRetention:${teamID}` export const waitingKeyTeamsLoadTeamTreeActivity = (teamID: T.Teams.TeamID, username: string) => `loadTeamTreeActivity:${teamID};${username}` export const waitingKeyTeamsEditMembership = (teamID: T.Teams.TeamID, ...usernames: ReadonlyArray) => diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 8cec80fbc94d..2ea2407ed605 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -760,7 +760,6 @@ type Store = T.Immutable<{ errorInEditMember: {error: string; teamID: T.Teams.TeamID; username: string} errorInEditWelcomeMessage: string errorInEmailInvite: T.Teams.EmailInviteError - errorInSettings: string newTeamRequests: Map> newTeams: Set teamIDToResetUsers: Map> @@ -805,7 +804,6 @@ const initialStore: Store = { errorInEditMember: emptyErrorInEditMember, errorInEditWelcomeMessage: '', errorInEmailInvite: emptyEmailInviteError, - errorInSettings: '', errorInTeamJoin: '', newTeamRequests: new Map(), newTeamWizard: newTeamWizardEmptyState, @@ -933,7 +931,6 @@ export type State = Store & { renameTeam: (oldName: string, newName: string) => void requestInviteLinkDetails: () => void resetErrorInEmailInvite: () => void - resetErrorInSettings: () => void resetState: () => void resetTeamJoin: () => void resetTeamMetaStale: () => void @@ -1918,12 +1915,9 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { s.teamIDToWelcomeMessage.set(teamID, message) }) } catch (error) { - set(s => { - if (error instanceof RPCError) { - logger.error(error) - s.errorInSettings = error.desc - } - }) + if (error instanceof RPCError) { + logger.error(error) + } } } ignorePromise(f()) @@ -2259,11 +2253,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { s.errorInEmailInvite.malformed = new Set() }) }, - resetErrorInSettings: () => { - set(s => { - s.errorInSettings = '' - }) - }, resetState: Z.defaultReset, resetTeamJoin: () => { set(s => { @@ -2352,11 +2341,9 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { ]) get().dispatch.getTeams(false) } catch (error) { - set(s => { - if (error instanceof RPCError) { - s.errorInSettings = error.desc - } - }) + if (error instanceof RPCError) { + logger.info(error.message) + } } } ignorePromise(f()) @@ -2450,15 +2437,12 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { try { const servicePolicy = Util.retentionPolicyToServiceRetentionPolicy(policy) await T.RPCChat.localSetTeamRetentionLocalRpcPromise({policy: servicePolicy, teamID}, [ - S.waitingKeyTeamsTeam(teamID), + S.waitingKeyTeamsSetRetentionPolicy(teamID), ]) } catch (error) { - set(s => { - if (error instanceof RPCError) { - logger.error(error.message) - s.errorInSettings = error.desc - } - }) + if (error instanceof RPCError) { + logger.error(error.message) + } } } ignorePromise(f()) diff --git a/shared/teams/team/index.tsx b/shared/teams/team/index.tsx index 30c700f072d4..46d75ea57e51 100644 --- a/shared/teams/team/index.tsx +++ b/shared/teams/team/index.tsx @@ -28,6 +28,13 @@ type Props = { const lastSelectedTabs = new Map() const defaultTab: T.Teams.TabKey = 'members' +const getSettingsErrorWaitingKeys = (teamID: T.Teams.TeamID) => + [ + C.waitingKeyTeamsLoadWelcomeMessage(teamID), + C.waitingKeyTeamsSetMemberPublicity(teamID), + C.waitingKeyTeamsSetRetentionPolicy(teamID), + ] as const + const useTabsState = ( teamID: T.Teams.TeamID, providedTab?: T.Teams.TabKey @@ -35,11 +42,11 @@ const useTabsState = ( const loadTeamChannelList = Teams.useTeamsState(s => s.dispatch.loadTeamChannelList) const defaultSelectedTab = lastSelectedTabs.get(teamID) ?? providedTab ?? defaultTab const [selectedTab, _setSelectedTab] = React.useState(defaultSelectedTab) - const resetErrorInSettings = Teams.useTeamsState(s => s.dispatch.resetErrorInSettings) + const dispatchClearWaiting = C.Waiting.useDispatchClearWaiting() const setSelectedTab = (t: T.Teams.TabKey) => { lastSelectedTabs.set(teamID, t) if (selectedTab !== 'settings' && t === 'settings') { - resetErrorInSettings() + dispatchClearWaiting(getSettingsErrorWaitingKeys(teamID)) } if (selectedTab !== 'channels' && t === 'channels') { loadTeamChannelList(teamID) @@ -54,14 +61,14 @@ const useTabsState = ( prevTeamIDRef.current = teamID lastSelectedTabs.set(teamID, defaultSelectedTab) if (defaultSelectedTab === 'settings') { - resetErrorInSettings() + dispatchClearWaiting(getSettingsErrorWaitingKeys(teamID)) } if (defaultSelectedTab === 'channels') { loadTeamChannelList(teamID) } _setSelectedTab(defaultSelectedTab) } - }, [teamID, defaultSelectedTab, resetErrorInSettings, loadTeamChannelList]) + }, [teamID, defaultSelectedTab, dispatchClearWaiting, loadTeamChannelList]) return [selectedTab, setSelectedTab] } diff --git a/shared/teams/team/settings-tab/index.tsx b/shared/teams/team/settings-tab/index.tsx index e16856da8c6a..d30c5f5b5423 100644 --- a/shared/teams/team/settings-tab/index.tsx +++ b/shared/teams/team/settings-tab/index.tsx @@ -13,7 +13,6 @@ import isEqual from 'lodash/isEqual' type Props = { allowOpenTrigger: number canShowcase: boolean - error?: string isBigTeam: boolean ignoreAccessRequests: boolean publicityAnyMember: boolean @@ -187,7 +186,7 @@ const IgnoreAccessRequests = (props: { export const Settings = (p: Props) => { const {savePublicity, isBigTeam, teamID, yourOperations, teamname, showOpenTeamWarning} = p - const {canShowcase, error, allowOpenTrigger} = p + const {canShowcase, allowOpenTrigger} = p const [newPublicityAnyMember, setNewPublicityAnyMember] = React.useState(p.publicityAnyMember) const [newPublicityTeam, setNewPublicityTeam] = React.useState(p.publicityTeam) @@ -338,8 +337,6 @@ const Container = (ownProps: OwnProps) => { const teamDetails = s.teamDetails.get(teamID) ?? Teams.emptyTeamDetails return { _loadWelcomeMessage: s.dispatch.loadWelcomeMessage, - error: s.errorInSettings, - resetErrorInSettings: s.dispatch.resetErrorInSettings, setPublicity: s.dispatch.setPublicity, teamDetails, teamMeta, @@ -348,7 +345,7 @@ const Container = (ownProps: OwnProps) => { } }) ) - const {error, _loadWelcomeMessage, resetErrorInSettings, setPublicity, teamDetails} = teamsState + const {_loadWelcomeMessage, setPublicity, teamDetails} = teamsState const {teamMeta, welcomeMessage, yourOperations} = teamsState const publicityAnyMember = teamMeta.allowPromote const publicityMember = teamMeta.showcasing @@ -361,23 +358,26 @@ const Container = (ownProps: OwnProps) => { const openTeamRole = teamDetails.settings.openJoinAs const teamname = teamMeta.teamname const waitingForWelcomeMessage = C.Waiting.useAnyWaiting(C.waitingKeyTeamsLoadWelcomeMessage(teamID)) - const clearError = resetErrorInSettings + const error = C.Waiting.useAnyErrors([ + C.waitingKeyTeamsLoadWelcomeMessage(teamID), + C.waitingKeyTeamsSetMemberPublicity(teamID), + C.waitingKeyTeamsSetRetentionPolicy(teamID), + ])?.message const loadWelcomeMessage = () => { _loadWelcomeMessage(teamID) } const navigateAppend = C.Router2.navigateAppend const _savePublicity = (settings: T.Teams.PublicitySettings) => { - setPublicity(teamID, settings) - } + setPublicity(teamID, settings) + } const showOpenTeamWarning = (isOpenTeam: boolean, teamname: string) => { - navigateAppend({name: 'openTeamWarning', params: {isOpenTeam, teamname}}) - } + navigateAppend({name: 'openTeamWarning', params: {isOpenTeam, teamname}}) + } const allowOpenTrigger = useSettingsTabState(s => s.allowOpenTrigger) const savePublicity = (settings: T.Teams.PublicitySettings) => { - _savePublicity(settings) - clearError() - } + _savePublicity(settings) + } // reset if incoming props change on us const [key, setKey] = React.useState(0) From a4394e116108603fbc46e107afa9bb82280cd87f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:31:09 -0400 Subject: [PATCH 05/45] WIP --- AGENTS.md | 1 + shared/constants/deeplinks.tsx | 2 +- shared/stores/teams.tsx | 103 +++------------ shared/teams/join-team/container.tsx | 131 ++++++++++++++++---- shared/teams/join-team/join-from-invite.tsx | 131 +++++++++++++------- shared/teams/routes.tsx | 19 +-- 6 files changed, 213 insertions(+), 174 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 69636d35905f..ea7b2808ac9f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,5 +7,6 @@ - When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions. - 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. - 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. +- 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. - 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. - 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. diff --git a/shared/constants/deeplinks.tsx b/shared/constants/deeplinks.tsx index 666183c19a68..b0fe1b1c30f0 100644 --- a/shared/constants/deeplinks.tsx +++ b/shared/constants/deeplinks.tsx @@ -149,7 +149,7 @@ const handleKeybaseLink = (link: string) => { } break case 'team-invite-link': - useTeamsState.getState().dispatch.openInviteLink(parts[1] ?? '', parts[2] || '') + navigateAppend({name: 'teamInviteLinkJoin', params: {inviteID: parts[1] ?? '', inviteKey: parts[2] || ''}}) return default: break diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 2ea2407ed605..7d84b55fbfa1 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -781,11 +781,6 @@ type Store = T.Immutable<{ teamAccessRequestsPending: Set newTeamWizard: T.Teams.NewTeamWizardState addMembersWizard: T.Teams.AddMembersWizardState - errorInTeamJoin: string - teamInviteDetails: T.Teams.TeamInviteState - teamJoinSuccess: boolean - teamJoinSuccessOpen: boolean - teamJoinSuccessTeamName: string teamVersion: Map teamIDToMembers: Map> // Used by chat sidebar until team loading gets easier teamIDToRetentionPolicy: Map @@ -804,7 +799,6 @@ const initialStore: Store = { errorInEditMember: emptyErrorInEditMember, errorInEditWelcomeMessage: '', errorInEmailInvite: emptyEmailInviteError, - errorInTeamJoin: '', newTeamRequests: new Map(), newTeamWizard: newTeamWizardEmptyState, newTeams: new Set(), @@ -817,10 +811,6 @@ const initialStore: Store = { teamIDToResetUsers: new Map(), teamIDToRetentionPolicy: new Map(), teamIDToWelcomeMessage: new Map(), - teamInviteDetails: {inviteID: '', inviteKey: ''}, - teamJoinSuccess: false, - teamJoinSuccessOpen: false, - teamJoinSuccessTeamName: '', teamMemberToLastActivity: new Map(), teamMemberToTreeMemberships: new Map(), teamMeta: new Map(), @@ -909,7 +899,7 @@ export type State = Store & { fullName: string, loadingKey?: string ) => void - joinTeam: (teamname: string, deeplink?: boolean) => void + joinTeam: (teamname: string) => void launchNewTeamWizardOrModal: (subteamOf?: T.Teams.TeamID) => void leaveTeam: (teamname: string, permanent: boolean, context: 'teams' | 'chat') => void loadTeam: (teamID: T.Teams.TeamID, _subscribe?: boolean) => void @@ -922,17 +912,14 @@ export type State = Store & { notifyTreeMembershipsPartial: (membership: T.RPCChat.Keybase1.TeamTreeMembership) => void notifyTeamTeamRoleMapChanged: (newVersion: number) => void onEngineIncomingImpl: (action: EngineGen.Actions) => void - openInviteLink: (inviteID: string, inviteKey: string) => void onGregorPushState: (gs: Array<{md: T.RPCGen.Gregor1.Metadata; item: T.RPCGen.Gregor1.Item}>) => void reAddToTeam: (teamID: T.Teams.TeamID, username: string) => void refreshTeamRoleMap: () => void removeMember: (teamID: T.Teams.TeamID, username: string) => void removePendingInvite: (teamID: T.Teams.TeamID, inviteID: string) => void renameTeam: (oldName: string, newName: string) => void - requestInviteLinkDetails: () => void resetErrorInEmailInvite: () => void resetState: () => void - resetTeamJoin: () => void resetTeamMetaStale: () => void saveChannelMembership: ( teamID: T.Teams.TeamID, @@ -1711,30 +1698,12 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - joinTeam: (teamname, deeplink) => { - set(s => { - s.teamInviteDetails.inviteDetails = undefined - }) - + joinTeam: teamname => { const f = async () => { - // In the deeplink flow, a modal is displayed which runs `joinTeam` (or an - // alternative flow, but we're not concerned with that here). In that case, - // we can fully manage the UX from inside of this handler. - // In the "Join team" flow, user pastes their link into the input box, which - // then calls `joinTeam` on its own. Since we need to switch to another modal, - // we simply plumb `deeplink` into the `promptInviteLinkJoin` handler and - // do the nav in the modal. - get().dispatch.resetTeamJoin() try { - const result = await T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener({ + await T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener({ customResponseIncomingCallMap: { 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { - set(s => { - s.teamInviteDetails.inviteDetails = T.castDraft(params.details) - }) - if (!deeplink) { - navigateAppend('teamInviteLinkJoin', true) - } set(s => { s.dispatch.dynamic.respondToInviteLink = wrapErrors((accept: boolean) => { set(s => { @@ -1743,28 +1712,25 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { response.result(accept) }) }) + navigateAppend( + { + name: 'teamInviteLinkJoin', + params: { + inviteDetails: params.details, + inviteKey: teamname, + }, + }, + true + ) }, }, incomingCallMap: {}, params: {tokenOrName: teamname}, waitingKey: S.waitingKeyTeamsJoinTeam, }) - set(s => { - s.teamJoinSuccess = true - s.teamJoinSuccessOpen = result.wasOpenTeam - s.teamJoinSuccessTeamName = result.wasTeamName ? teamname : '' - }) } catch (error) { if (error instanceof RPCError) { - const desc = - error.code === T.RPCGen.StatusCode.scteaminvitebadtoken - ? 'Sorry, that team name or token is not valid.' - : error.code === T.RPCGen.StatusCode.scnotfound - ? 'This invitation is no longer valid, or has expired.' - : error.desc - set(s => { - s.errorInTeamJoin = desc - }) + logger.info(error.message) } } finally { set(s => { @@ -2129,14 +2095,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { new Set(bodyToJSON(chosenChannels?.item.body) as Array) ) }, - openInviteLink: (inviteID, inviteKey) => { - set(s => { - s.teamInviteDetails.inviteDetails = undefined - s.teamInviteDetails.inviteID = inviteID - s.teamInviteDetails.inviteKey = inviteKey - }) - navigateAppend('teamInviteLinkJoin') - }, reAddToTeam: (teamID, username) => { const f = async () => { try { @@ -2222,31 +2180,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - requestInviteLinkDetails: () => { - const f = async () => { - try { - const details = await T.RPCGen.teamsGetInviteLinkDetailsRpcPromise({ - inviteID: get().teamInviteDetails.inviteID, - }) - set(s => { - s.teamInviteDetails.inviteDetails = T.castDraft(details) - }) - } catch (error) { - if (error instanceof RPCError) { - const desc = - error.code === T.RPCGen.StatusCode.scteaminvitebadtoken - ? 'Sorry, that invite token is not valid.' - : error.code === T.RPCGen.StatusCode.scnotfound - ? 'This invitation is no longer valid, or has expired.' - : error.desc - set(s => { - s.errorInTeamJoin = desc - }) - } - } - } - ignorePromise(f()) - }, resetErrorInEmailInvite: () => { set(s => { s.errorInEmailInvite.message = '' @@ -2254,14 +2187,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { }) }, resetState: Z.defaultReset, - resetTeamJoin: () => { - set(s => { - s.errorInTeamJoin = '' - s.teamJoinSuccess = false - s.teamJoinSuccessOpen = false - s.teamJoinSuccessTeamName = '' - }) - }, resetTeamMetaStale: () => { set(s => { s.teamMetaStale = true diff --git a/shared/teams/join-team/container.tsx b/shared/teams/join-team/container.tsx index 25c414e21486..8f9252398e32 100644 --- a/shared/teams/join-team/container.tsx +++ b/shared/teams/join-team/container.tsx @@ -1,33 +1,110 @@ import * as C from '@/constants' -import upperFirst from 'lodash/upperFirst' +import * as T from '@/constants/types' +import {wrapErrors} from '@/constants/utils' +import * as Kb from '@/common-adapters' +import type {RootParamList} from '@/router-v2/route-params' import {useTeamsState} from '@/stores/teams' +import {RPCError} from '@/util/errors' +import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +import upperFirst from 'lodash/upperFirst' import * as React from 'react' -import * as Kb from '@/common-adapters' +import {useNavigation} from '@react-navigation/native' -type OwnProps = {initialTeamname?: string} +type OwnProps = {initialTeamname?: string; success?: boolean} -const Container = (ownProps: OwnProps) => { - const initialTeamname = ownProps.initialTeamname - const errorText = useTeamsState(s => upperFirst(s.errorInTeamJoin)) - const open = useTeamsState(s => s.teamJoinSuccessOpen) - const success = useTeamsState(s => s.teamJoinSuccess) - const successTeamName = useTeamsState(s => s.teamJoinSuccessTeamName) - const navigateUp = C.Router2.navigateUp - const onBack = () => { - navigateUp() +const getJoinTeamError = (error: unknown) => { + if (error instanceof RPCError) { + return ( + error.code === T.RPCGen.StatusCode.scteaminvitebadtoken + ? 'Sorry, that team name or token is not valid.' + : error.code === T.RPCGen.StatusCode.scnotfound + ? 'This invitation is no longer valid, or has expired.' + : error.desc + ) } - const joinTeam = useTeamsState(s => s.dispatch.joinTeam) - const onJoinTeam = joinTeam + return error instanceof Error ? error.message : 'Something went wrong.' +} +const Container = ({initialTeamname, success: successParam}: OwnProps) => { + const [errorText, setErrorText] = React.useState('') + const [open, setOpen] = React.useState(false) + const [successTeamName, setSuccessTeamName] = React.useState('') const [name, _setName] = React.useState(initialTeamname ?? '') + const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) + const navigation = useNavigation>() + const navigateUp = C.Router2.navigateUp + const success = !!successParam + const setName = (n: string) => _setName(n.toLowerCase()) - const resetTeamJoin = useTeamsState(s => s.dispatch.resetTeamJoin) + const onBack = () => navigateUp() + React.useEffect(() => { - resetTeamJoin() - }, [resetTeamJoin]) + _setName(initialTeamname ?? '') + setErrorText('') + setOpen(false) + setSuccessTeamName('') + if (successParam) { + navigation.setParams({initialTeamname, success: false}) + } + }, [initialTeamname, navigation, successParam]) const onSubmit = () => { - onJoinTeam(name) + setErrorText('') + setOpen(false) + setSuccessTeamName('') + joinTeam( + [ + { + customResponseIncomingCallMap: { + 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { + const currentDispatch = useTeamsState.getState().dispatch + useTeamsState.setState({ + dispatch: { + ...currentDispatch, + dynamic: { + ...currentDispatch.dynamic, + respondToInviteLink: wrapErrors((accept: boolean) => { + const latestDispatch = useTeamsState.getState().dispatch + useTeamsState.setState({ + dispatch: { + ...latestDispatch, + dynamic: { + ...latestDispatch.dynamic, + respondToInviteLink: undefined, + }, + }, + }) + response.result(accept) + }), + }, + }, + }) + C.Router2.navigateAppend( + { + name: 'teamInviteLinkJoin', + params: { + inviteDetails: params.details, + inviteKey: name, + }, + }, + true + ) + }, + }, + incomingCallMap: {}, + params: {tokenOrName: name}, + waitingKey: C.waitingKeyTeamsJoinTeam, + }, + ], + result => { + setOpen(result.wasOpenTeam) + setSuccessTeamName(result.wasTeamName ? name : '') + navigation.setParams({initialTeamname, success: true}) + }, + error => { + setErrorText(upperFirst(getJoinTeamError(error))) + } + ) } return ( @@ -74,15 +151,15 @@ const Container = (ownProps: OwnProps) => { )} - - - + + + ) diff --git a/shared/teams/join-team/join-from-invite.tsx b/shared/teams/join-team/join-from-invite.tsx index 04b1b9fbf6be..7aaa6b9949e7 100644 --- a/shared/teams/join-team/join-from-invite.tsx +++ b/shared/teams/join-team/join-from-invite.tsx @@ -1,38 +1,105 @@ import * as C from '@/constants' -import * as React from 'react' -import {useTeamsState} from '@/stores/teams' +import * as T from '@/constants/types' import * as Kb from '@/common-adapters' -import {Success} from './container' +import {useTeamsState} from '@/stores/teams' +import {RPCError} from '@/util/errors' +import * as React from 'react' import {useSafeNavigation} from '@/util/safe-navigation' +import {Success} from './container' + +type Props = { + inviteDetails?: T.RPCGen.InviteLinkDetails + inviteID?: string + inviteKey?: string +} + +const getInviteError = (error: unknown, missingKey: boolean) => { + if (error instanceof RPCError) { + return ( + error.code === T.RPCGen.StatusCode.scteaminvitebadtoken + ? missingKey + ? 'Sorry, that invite token is not valid.' + : 'Sorry, that team name or token is not valid.' + : error.code === T.RPCGen.StatusCode.scnotfound + ? 'This invitation is no longer valid, or has expired.' + : error.desc + ) + } + return error instanceof Error ? error.message : 'Something went wrong.' +} -const JoinFromInvite = () => { - const {inviteID: id, inviteKey: key, inviteDetails: details} = useTeamsState(s => s.teamInviteDetails) - const error = useTeamsState(s => s.errorInTeamJoin) +const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inviteKey = ''}: Props) => { + const [details, setDetails] = React.useState(initialInviteDetails) + const [error, setError] = React.useState('') + const [localRespondToInviteLink, setLocalRespondToInviteLink] = React.useState< + ((accept: boolean) => void) | undefined + >() + const storedRespondToInviteLink = useTeamsState(s => s.dispatch.dynamic.respondToInviteLink) + const respondToInviteLink = localRespondToInviteLink ?? storedRespondToInviteLink const loaded = details !== undefined || !!error + const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) + const requestInviteLinkDetails = C.useRPC(T.RPCGen.teamsGetInviteLinkDetailsRpcPromise) + const [clickedJoin, setClickedJoin] = React.useState(false) + const [showSuccess, setShowSuccess] = React.useState(false) + const rpcWaiting = C.Waiting.useAnyWaiting(C.waitingKeyTeamsJoinTeam) + const waiting = rpcWaiting && clickedJoin + const wasWaitingRef = React.useRef(waiting) - const joinTeam = useTeamsState(s => s.dispatch.joinTeam) - const requestInviteLinkDetails = useTeamsState(s => s.dispatch.requestInviteLinkDetails) + React.useEffect(() => { + setDetails(initialInviteDetails) + setError('') + setLocalRespondToInviteLink(undefined) + setClickedJoin(false) + setShowSuccess(false) + wasWaitingRef.current = false + }, [initialInviteDetails, inviteID, inviteKey]) React.useEffect(() => { if (loaded) { return } - if (key === '') { - // If we're missing the key, we want the user to paste the whole link again - requestInviteLinkDetails() + if (inviteKey === '') { + requestInviteLinkDetails( + [{inviteID}], + result => { + setDetails(result) + setError('') + }, + rpcError => { + setError(getInviteError(rpcError, true)) + } + ) return } - // Otherwise we're reusing the join flow, so that we don't look up the invite id twice - // (the invite id is derived from the key). - joinTeam(key, true) - }, [requestInviteLinkDetails, joinTeam, loaded, key, id]) + joinTeam( + [ + { + customResponseIncomingCallMap: { + 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { + setDetails(params.details) + setLocalRespondToInviteLink(() => accept => { + setLocalRespondToInviteLink(undefined) + response.result(accept) + }) + }, + }, + incomingCallMap: {}, + params: {tokenOrName: inviteKey}, + waitingKey: C.waitingKeyTeamsJoinTeam, + }, + ], + () => {}, + rpcError => { + setLocalRespondToInviteLink(undefined) + setError(getInviteError(rpcError, false)) + } + ) + }, [inviteID, inviteKey, joinTeam, loaded, requestInviteLinkDetails]) - const [clickedJoin, setClickedJoin] = React.useState(false) const nav = useSafeNavigation() const onNavUp = () => nav.safeNavigateUp() - const respondToInviteLink = useTeamsState(s => s.dispatch.dynamic.respondToInviteLink) const onJoinTeam = () => { setClickedJoin(true) respondToInviteLink?.(true) @@ -41,16 +108,10 @@ const JoinFromInvite = () => { respondToInviteLink?.(true) onNavUp() } - - const rpcWaiting = C.Waiting.useAnyWaiting(C.waitingKeyTeamsJoinTeam) - const waiting = rpcWaiting && clickedJoin - const wasWaitingRef = React.useRef(waiting) React.useEffect(() => { wasWaitingRef.current = waiting }, [waiting]) - const [showSuccess, setShowSuccess] = React.useState(false) - React.useEffect(() => { setShowSuccess(wasWaitingRef.current && !waiting && !error) }, [waiting, error]) @@ -60,35 +121,17 @@ const JoinFromInvite = () => { const body = details === undefined ? ( loaded ? ( - + ERROR: {error} ) : ( - + Loading... ) ) : showSuccess ? ( - + diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 6860a557c57b..5f72936eea11 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -155,16 +155,9 @@ const AddFromWhereHeaderTitle = () => { return } -const JoinTeamHeaderTitle = () => { - const success = Teams.useTeamsState(s => s.teamJoinSuccess) - return <>{success ? 'Request sent' : 'Join a team'} -} +const JoinTeamHeaderTitle = ({success}: {success?: boolean}) => <>{success ? 'Request sent' : 'Join a team'} -const JoinTeamHeaderLeft = () => { - const success = Teams.useTeamsState(s => s.teamJoinSuccess) - if (success) return null - return -} +const JoinTeamHeaderLeft = ({success}: {success?: boolean}) => (success ? null : ) const NewTeamInfoHeaderTitle = () => { const {teamType, parentTeamID} = Teams.useTeamsState( @@ -299,10 +292,10 @@ export const newModalRoutes = { teamInviteByEmail: C.makeScreen(React.lazy(async () => import('./invite-by-email'))), teamInviteLinkJoin: C.makeScreen(React.lazy(async () => import('./join-team/join-from-invite'))), teamJoinTeamDialog: C.makeScreen(React.lazy(async () => import('./join-team/container')), { - getOptions: { - headerLeft: () => , - headerTitle: () => , - }, + getOptions: ({route}) => ({ + headerLeft: () => , + headerTitle: () => , + }), }), teamNewTeamDialog: C.makeScreen(React.lazy(async () => import('./new-team')), { getOptions: {title: 'Create a team'}, From fd4e03a38727bdbb71dbda0888f53058f45ed25a Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:38:29 -0400 Subject: [PATCH 06/45] WIP --- AGENTS.md | 1 + shared/stores/teams.tsx | 6 ++++++ shared/teams/join-team/container.tsx | 29 +++++++--------------------- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ea7b2808ac9f..9fd715b5503d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,5 +8,6 @@ - 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. - 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. - 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. +- Components must not mutate Zustand stores directly with `useXState.setState`, `getState()`-based writes, or similar ad hoc store mutation. If a component needs to affect store state, route it through a store dispatch action or move the state out of the store. - 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. - 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. diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 7d84b55fbfa1..769847dd8be9 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -942,6 +942,7 @@ export type State = Store & { selected: boolean, clearAll?: boolean ) => void + setRespondToInviteLink: (respondToInviteLink?: (accept: boolean) => void) => void setNewTeamInfo: ( deletedTeams: ReadonlyArray, newTeams: Set, @@ -2287,6 +2288,11 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } }) }, + setRespondToInviteLink: respondToInviteLink => { + set(s => { + s.dispatch.dynamic.respondToInviteLink = respondToInviteLink + }) + }, setNewTeamInfo: (deletedTeams, newTeams, teamIDToResetUsers) => { set(s => { s.deletedTeams = T.castDraft(deletedTeams) diff --git a/shared/teams/join-team/container.tsx b/shared/teams/join-team/container.tsx index 8f9252398e32..be1f2d3a3578 100644 --- a/shared/teams/join-team/container.tsx +++ b/shared/teams/join-team/container.tsx @@ -30,6 +30,7 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { const [open, setOpen] = React.useState(false) const [successTeamName, setSuccessTeamName] = React.useState('') const [name, _setName] = React.useState(initialTeamname ?? '') + const setRespondToInviteLink = useTeamsState(s => s.dispatch.setRespondToInviteLink) const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) const navigation = useNavigation>() const navigateUp = C.Router2.navigateUp @@ -57,28 +58,12 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { { customResponseIncomingCallMap: { 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { - const currentDispatch = useTeamsState.getState().dispatch - useTeamsState.setState({ - dispatch: { - ...currentDispatch, - dynamic: { - ...currentDispatch.dynamic, - respondToInviteLink: wrapErrors((accept: boolean) => { - const latestDispatch = useTeamsState.getState().dispatch - useTeamsState.setState({ - dispatch: { - ...latestDispatch, - dynamic: { - ...latestDispatch.dynamic, - respondToInviteLink: undefined, - }, - }, - }) - response.result(accept) - }), - }, - }, - }) + setRespondToInviteLink( + wrapErrors((accept: boolean) => { + setRespondToInviteLink(undefined) + response.result(accept) + }) + ) C.Router2.navigateAppend( { name: 'teamInviteLinkJoin', From 99cd892da9614ffd5ef21172efa9e63bfc0a1112 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:44:55 -0400 Subject: [PATCH 07/45] WIP --- shared/stores/teams.tsx | 32 +++---------- shared/teams/join-team/container.tsx | 16 +++---- shared/teams/join-team/join-from-invite.tsx | 52 +++++++-------------- 3 files changed, 31 insertions(+), 69 deletions(-) diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 769847dd8be9..89d20ef3d1b2 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -1,5 +1,5 @@ import * as S from '@/constants/strings' -import {ignorePromise, wrapErrors} from '@/constants/utils' +import {ignorePromise} from '@/constants/utils' import * as T from '@/constants/types' import type * as EngineGen from '@/constants/rpc' import { @@ -836,9 +836,6 @@ export type State = Store & { ) => void onUsersUpdates?: (updates: ReadonlyArray<{name: string; info: Partial}>) => void } - dynamic: { - respondToInviteLink?: (accept: boolean) => void - } addMembersWizardPushMembers: (members: Array) => void addMembersWizardRemoveMember: (assertion: string) => void addMembersWizardSetDefaultChannels: ( @@ -942,7 +939,6 @@ export type State = Store & { selected: boolean, clearAll?: boolean ) => void - setRespondToInviteLink: (respondToInviteLink?: (accept: boolean) => void) => void setNewTeamInfo: ( deletedTeams: ReadonlyArray, newTeams: Set, @@ -1377,9 +1373,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - dynamic: { - respondToInviteLink: undefined, - }, eagerLoadTeams: () => { if (get().teamMetaSubscribeCount > 0) { logger.info('eagerly reloading') @@ -1701,18 +1694,12 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { }, joinTeam: teamname => { const f = async () => { + let handedOffToInvite = false try { await T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener({ customResponseIncomingCallMap: { 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { - set(s => { - s.dispatch.dynamic.respondToInviteLink = wrapErrors((accept: boolean) => { - set(s => { - s.dispatch.dynamic.respondToInviteLink = undefined - }) - response.result(accept) - }) - }) + handedOffToInvite = true navigateAppend( { name: 'teamInviteLinkJoin', @@ -1723,6 +1710,7 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { }, true ) + response.result(false) }, }, incomingCallMap: {}, @@ -1730,13 +1718,12 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { waitingKey: S.waitingKeyTeamsJoinTeam, }) } catch (error) { + if (handedOffToInvite) { + return + } if (error instanceof RPCError) { logger.info(error.message) } - } finally { - set(s => { - s.dispatch.dynamic.respondToInviteLink = undefined - }) } } ignorePromise(f()) @@ -2288,11 +2275,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } }) }, - setRespondToInviteLink: respondToInviteLink => { - set(s => { - s.dispatch.dynamic.respondToInviteLink = respondToInviteLink - }) - }, setNewTeamInfo: (deletedTeams, newTeams, teamIDToResetUsers) => { set(s => { s.deletedTeams = T.castDraft(deletedTeams) diff --git a/shared/teams/join-team/container.tsx b/shared/teams/join-team/container.tsx index be1f2d3a3578..6e436eb7657d 100644 --- a/shared/teams/join-team/container.tsx +++ b/shared/teams/join-team/container.tsx @@ -1,9 +1,7 @@ import * as C from '@/constants' import * as T from '@/constants/types' -import {wrapErrors} from '@/constants/utils' import * as Kb from '@/common-adapters' import type {RootParamList} from '@/router-v2/route-params' -import {useTeamsState} from '@/stores/teams' import {RPCError} from '@/util/errors' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' import upperFirst from 'lodash/upperFirst' @@ -30,11 +28,11 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { const [open, setOpen] = React.useState(false) const [successTeamName, setSuccessTeamName] = React.useState('') const [name, _setName] = React.useState(initialTeamname ?? '') - const setRespondToInviteLink = useTeamsState(s => s.dispatch.setRespondToInviteLink) const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) const navigation = useNavigation>() const navigateUp = C.Router2.navigateUp const success = !!successParam + const handoffToInviteRef = React.useRef(false) const setName = (n: string) => _setName(n.toLowerCase()) const onBack = () => navigateUp() @@ -58,12 +56,7 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { { customResponseIncomingCallMap: { 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { - setRespondToInviteLink( - wrapErrors((accept: boolean) => { - setRespondToInviteLink(undefined) - response.result(accept) - }) - ) + handoffToInviteRef.current = true C.Router2.navigateAppend( { name: 'teamInviteLinkJoin', @@ -74,6 +67,7 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { }, true ) + response.result(false) }, }, incomingCallMap: {}, @@ -87,6 +81,10 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { navigation.setParams({initialTeamname, success: true}) }, error => { + if (handoffToInviteRef.current) { + handoffToInviteRef.current = false + return + } setErrorText(upperFirst(getJoinTeamError(error))) } ) diff --git a/shared/teams/join-team/join-from-invite.tsx b/shared/teams/join-team/join-from-invite.tsx index 7aaa6b9949e7..3085614ae1e9 100644 --- a/shared/teams/join-team/join-from-invite.tsx +++ b/shared/teams/join-team/join-from-invite.tsx @@ -1,7 +1,6 @@ import * as C from '@/constants' import * as T from '@/constants/types' import * as Kb from '@/common-adapters' -import {useTeamsState} from '@/stores/teams' import {RPCError} from '@/util/errors' import * as React from 'react' import {useSafeNavigation} from '@/util/safe-navigation' @@ -31,11 +30,6 @@ const getInviteError = (error: unknown, missingKey: boolean) => { const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inviteKey = ''}: Props) => { const [details, setDetails] = React.useState(initialInviteDetails) const [error, setError] = React.useState('') - const [localRespondToInviteLink, setLocalRespondToInviteLink] = React.useState< - ((accept: boolean) => void) | undefined - >() - const storedRespondToInviteLink = useTeamsState(s => s.dispatch.dynamic.respondToInviteLink) - const respondToInviteLink = localRespondToInviteLink ?? storedRespondToInviteLink const loaded = details !== undefined || !!error const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) const requestInviteLinkDetails = C.useRPC(T.RPCGen.teamsGetInviteLinkDetailsRpcPromise) @@ -43,15 +37,12 @@ const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inv const [showSuccess, setShowSuccess] = React.useState(false) const rpcWaiting = C.Waiting.useAnyWaiting(C.waitingKeyTeamsJoinTeam) const waiting = rpcWaiting && clickedJoin - const wasWaitingRef = React.useRef(waiting) React.useEffect(() => { setDetails(initialInviteDetails) setError('') - setLocalRespondToInviteLink(undefined) setClickedJoin(false) setShowSuccess(false) - wasWaitingRef.current = false }, [initialInviteDetails, inviteID, inviteKey]) React.useEffect(() => { @@ -69,19 +60,25 @@ const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inv setError(getInviteError(rpcError, true)) } ) - return } + }, [inviteID, inviteKey, loaded, requestInviteLinkDetails]) + + const nav = useSafeNavigation() + const onNavUp = () => nav.safeNavigateUp() + const onJoinTeam = () => { + if (!inviteKey) { + return + } + setClickedJoin(true) + setError('') joinTeam( [ { customResponseIncomingCallMap: { 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { setDetails(params.details) - setLocalRespondToInviteLink(() => accept => { - setLocalRespondToInviteLink(undefined) - response.result(accept) - }) + response.result(true) }, }, incomingCallMap: {}, @@ -89,32 +86,17 @@ const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inv waitingKey: C.waitingKeyTeamsJoinTeam, }, ], - () => {}, + () => { + setClickedJoin(false) + setShowSuccess(true) + }, rpcError => { - setLocalRespondToInviteLink(undefined) + setClickedJoin(false) setError(getInviteError(rpcError, false)) } ) - }, [inviteID, inviteKey, joinTeam, loaded, requestInviteLinkDetails]) - - const nav = useSafeNavigation() - - const onNavUp = () => nav.safeNavigateUp() - const onJoinTeam = () => { - setClickedJoin(true) - respondToInviteLink?.(true) - } - const onClose = () => { - respondToInviteLink?.(true) - onNavUp() } - React.useEffect(() => { - wasWaitingRef.current = waiting - }, [waiting]) - - React.useEffect(() => { - setShowSuccess(wasWaitingRef.current && !waiting && !error) - }, [waiting, error]) + const onClose = () => onNavUp() const teamname = (details?.teamName.parts || []).join('.') From d4a2dbc62dd569a19a349d09a5b9af043dd0a608 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 16:49:55 -0400 Subject: [PATCH 08/45] WIP --- shared/chat/inbox-search/index.tsx | 6 +-- .../markdown/maybe-mention/team.tsx | 5 ++- shared/common-adapters/team-with-popup.tsx | 4 +- shared/profile/user/teams/index.tsx | 5 +-- shared/stores/teams.tsx | 37 ------------------- 5 files changed, 9 insertions(+), 48 deletions(-) diff --git a/shared/chat/inbox-search/index.tsx b/shared/chat/inbox-search/index.tsx index c6dc510f0849..edef226c2db8 100644 --- a/shared/chat/inbox-search/index.tsx +++ b/shared/chat/inbox-search/index.tsx @@ -381,14 +381,14 @@ const OpenTeamRow = (p: OpenTeamProps) => { const [hovering, setHovering] = React.useState(false) const {name, description, memberCount, publicAdmins, inTeam, isSelected} = p const showingDueToSelect = React.useRef(false) - const {joinTeam, showTeamByName} = useTeamsState( + const {showTeamByName} = useTeamsState( C.useShallow(s => ({ - joinTeam: s.dispatch.joinTeam, showTeamByName: s.dispatch.showTeamByName, })) ) const clearModals = C.Router2.clearModals + const navigateAppend = C.Router2.navigateAppend const makePopup = (p: Kb.Popup2Parms) => { const {attachTo, hidePopup} = p return ( @@ -402,7 +402,7 @@ const OpenTeamRow = (p: OpenTeamProps) => { position="right center" onChat={undefined} onHidden={hidePopup} - onJoinTeam={() => joinTeam(name)} + onJoinTeam={() => navigateAppend({name: 'teamJoinTeamDialog', params: {initialTeamname: name}})} onViewTeam={() => { clearModals() showTeamByName(name) diff --git a/shared/common-adapters/markdown/maybe-mention/team.tsx b/shared/common-adapters/markdown/maybe-mention/team.tsx index 1d1d91445992..d393efc0a3a0 100644 --- a/shared/common-adapters/markdown/maybe-mention/team.tsx +++ b/shared/common-adapters/markdown/maybe-mention/team.tsx @@ -39,12 +39,13 @@ const TeamMention = (ownProps: OwnProps) => { const previewConversation = Chat.useChatState(s => s.dispatch.previewConversation) const showTeamByName = useTeamsState(s => s.dispatch.showTeamByName) const clearModals = C.Router2.clearModals + const navigateAppend = C.Router2.navigateAppend const _onViewTeam = (teamname: string) => { clearModals() showTeamByName(teamname) } - const joinTeam = useTeamsState(s => s.dispatch.joinTeam) - const onJoinTeam = joinTeam + const onJoinTeam = (teamname: string) => + navigateAppend({name: 'teamJoinTeamDialog', params: {initialTeamname: teamname}}) const convID = _convID ? T.Chat.stringToConversationIDKey(_convID) : undefined const onChat = convID diff --git a/shared/common-adapters/team-with-popup.tsx b/shared/common-adapters/team-with-popup.tsx index 316daa039a67..973362d9ed52 100644 --- a/shared/common-adapters/team-with-popup.tsx +++ b/shared/common-adapters/team-with-popup.tsx @@ -114,8 +114,6 @@ const ConnectedTeamWithPopup = (ownProps: OwnProps) => { memberCount: meta.memberCount, teamID, } - const joinTeam = Teams.useTeamsState(s => s.dispatch.joinTeam) - const _onJoinTeam = joinTeam const clearModals = C.Router2.clearModals const navigateAppend = C.Router2.navigateAppend const _onViewTeam = (teamID: T.Teams.TeamID) => { @@ -129,7 +127,7 @@ const ConnectedTeamWithPopup = (ownProps: OwnProps) => { isMember: stateProps.isMember, isOpen: stateProps.isOpen, memberCount: stateProps.memberCount, - onJoinTeam: () => _onJoinTeam(ownProps.teamName), + onJoinTeam: () => navigateAppend({name: 'teamJoinTeamDialog', params: {initialTeamname: ownProps.teamName}}), onViewTeam: () => _onViewTeam(stateProps.teamID), prefix: ownProps.prefix, shouldLoadTeam: ownProps.shouldLoadTeam, diff --git a/shared/profile/user/teams/index.tsx b/shared/profile/user/teams/index.tsx index 78b231bfd35e..9995ef83133b 100644 --- a/shared/profile/user/teams/index.tsx +++ b/shared/profile/user/teams/index.tsx @@ -19,18 +19,17 @@ const Container = (ownProps: OwnProps) => { _roles: s.teamRoleMap.roles, _teamNameToID: s.teamNameToID, _youAreInTeams: s.teamnames.size > 0, - joinTeam: s.dispatch.joinTeam, showTeamByName: s.dispatch.showTeamByName, })) ) - const {joinTeam, showTeamByName, _roles} = teamsState + const {showTeamByName, _roles} = teamsState const {_teamNameToID, _youAreInTeams} = teamsState const teamShowcase = d.teamShowcase || noTeams const {clearModals, navigateAppend} = C.Router2 const _onEdit = () => { navigateAppend('profileShowcaseTeamOffer') } - const onJoinTeam = joinTeam + const onJoinTeam = (teamname: string) => navigateAppend({name: 'teamJoinTeamDialog', params: {initialTeamname: teamname}}) const onViewTeam = (teamname: string) => { clearModals() showTeamByName(teamname) diff --git a/shared/stores/teams.tsx b/shared/stores/teams.tsx index 89d20ef3d1b2..2e70dd303772 100644 --- a/shared/stores/teams.tsx +++ b/shared/stores/teams.tsx @@ -896,7 +896,6 @@ export type State = Store & { fullName: string, loadingKey?: string ) => void - joinTeam: (teamname: string) => void launchNewTeamWizardOrModal: (subteamOf?: T.Teams.TeamID) => void leaveTeam: (teamname: string, permanent: boolean, context: 'teams' | 'chat') => void loadTeam: (teamID: T.Teams.TeamID, _subscribe?: boolean) => void @@ -1692,42 +1691,6 @@ export const useTeamsState = Z.createZustand('teams', (set, get) => { } ignorePromise(f()) }, - joinTeam: teamname => { - const f = async () => { - let handedOffToInvite = false - try { - await T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener({ - customResponseIncomingCallMap: { - 'keybase.1.teamsUi.confirmInviteLinkAccept': (params, response) => { - handedOffToInvite = true - navigateAppend( - { - name: 'teamInviteLinkJoin', - params: { - inviteDetails: params.details, - inviteKey: teamname, - }, - }, - true - ) - response.result(false) - }, - }, - incomingCallMap: {}, - params: {tokenOrName: teamname}, - waitingKey: S.waitingKeyTeamsJoinTeam, - }) - } catch (error) { - if (handedOffToInvite) { - return - } - if (error instanceof RPCError) { - logger.info(error.message) - } - } - } - ignorePromise(f()) - }, launchNewTeamWizardOrModal: subteamOf => { set(s => { s.newTeamWizard = T.castDraft({ From b08f97083d922ea33eb3b9d7ddee199ff4bdac3c Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 17:13:20 -0400 Subject: [PATCH 09/45] WIP --- shared/profile/add-to-team.tsx | 2 +- shared/teams/container.tsx | 12 ++++++------ shared/teams/get-options.tsx | 4 ++-- shared/teams/routes.tsx | 4 ++-- shared/teams/team/settings-tab/index.tsx | 3 ++- shared/teams/team/team-info.tsx | 2 +- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/shared/profile/add-to-team.tsx b/shared/profile/add-to-team.tsx index bcde245c9b51..5d2cc0348fcd 100644 --- a/shared/profile/add-to-team.tsx +++ b/shared/profile/add-to-team.tsx @@ -184,7 +184,7 @@ const Container = (ownProps: OwnProps) => { setSendNotification(true) setTeamProfileAddList([]) loadTeamList() - }, [loadTeamList, them]) + }, [them]) const onBack = () => { navigateUp() diff --git a/shared/teams/container.tsx b/shared/teams/container.tsx index 846362bf3da7..552bdba998e4 100644 --- a/shared/teams/container.tsx +++ b/shared/teams/container.tsx @@ -2,6 +2,7 @@ import * as C from '@/constants' import * as Teams from '@/stores/teams' import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' +import type {RouteProps2} from '@/router-v2/route-params' import Main from './main' import {useTeamsSubscribe} from './subscriber' import {useActivityLevels} from './common' @@ -42,12 +43,11 @@ const orderTeams = ( }) } -type Props = { - filter?: string - sort?: T.Teams.TeamListSort -} +type Props = RouteProps2<'teamsRoot'> -const Connected = ({filter = '', sort = 'role'}: Props) => { +const Connected = ({navigation, route}: Props) => { + const filter = route.params?.filter ?? '' + const sort = route.params?.sort ?? 'role' const data = Teams.useTeamsState( C.useShallow(s => { const {deletedTeams, activityLevels, teamMeta, dispatch} = s @@ -86,7 +86,7 @@ const Connected = ({filter = '', sort = 'role'}: Props) => { onCreateTeam={onCreateTeam} onJoinTeam={onJoinTeam} deletedTeams={deletedTeams} - onChangeSort={sortOrder => C.Router2.navigateAppend({name: 'teamsRoot', params: {filter, sort: sortOrder}}, true)} + onChangeSort={sortOrder => navigation.setParams({...(route.params ?? {}), filter, sort: sortOrder})} sortOrder={sort} teams={teams} /> diff --git a/shared/teams/get-options.tsx b/shared/teams/get-options.tsx index b7dddaffaba2..44eb330a0111 100644 --- a/shared/teams/get-options.tsx +++ b/shared/teams/get-options.tsx @@ -16,9 +16,9 @@ const useHeaderActions = () => { } const TeamsFilter = () => { - const {params} = useRoute>() + const params = useRoute>().params ?? {} const navigation = useNavigation>() - const filterValue = params?.filter ?? '' + const filterValue = params.filter ?? '' const numTeams = useTeamsState(s => s.teamMeta.size) const setFilter = (filter: string) => navigation.setParams({...params, filter}) return numTeams >= 20 ? ( diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 5f72936eea11..4cb014d2d6fc 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -293,8 +293,8 @@ export const newModalRoutes = { teamInviteLinkJoin: C.makeScreen(React.lazy(async () => import('./join-team/join-from-invite'))), teamJoinTeamDialog: C.makeScreen(React.lazy(async () => import('./join-team/container')), { getOptions: ({route}) => ({ - headerLeft: () => , - headerTitle: () => , + headerLeft: () => , + headerTitle: () => , }), }), teamNewTeamDialog: C.makeScreen(React.lazy(async () => import('./new-team')), { diff --git a/shared/teams/team/settings-tab/index.tsx b/shared/teams/team/settings-tab/index.tsx index d30c5f5b5423..d0513bf3d879 100644 --- a/shared/teams/team/settings-tab/index.tsx +++ b/shared/teams/team/settings-tab/index.tsx @@ -13,6 +13,7 @@ import isEqual from 'lodash/isEqual' type Props = { allowOpenTrigger: number canShowcase: boolean + error?: string isBigTeam: boolean ignoreAccessRequests: boolean publicityAnyMember: boolean @@ -185,7 +186,7 @@ const IgnoreAccessRequests = (props: { } export const Settings = (p: Props) => { - const {savePublicity, isBigTeam, teamID, yourOperations, teamname, showOpenTeamWarning} = p + const {error, savePublicity, isBigTeam, teamID, yourOperations, teamname, showOpenTeamWarning} = p const {canShowcase, allowOpenTrigger} = p const [newPublicityAnyMember, setNewPublicityAnyMember] = React.useState(p.publicityAnyMember) diff --git a/shared/teams/team/team-info.tsx b/shared/teams/team/team-info.tsx index df22dfff6e32..fc46bf4c6de0 100644 --- a/shared/teams/team/team-info.tsx +++ b/shared/teams/team/team-info.tsx @@ -2,7 +2,7 @@ import * as C from '@/constants' import * as React from 'react' import * as Teams from '@/stores/teams' import * as Kb from '@/common-adapters' -import type * as T from '@/constants/types' +import * as T from '@/constants/types' type Props = {teamID: T.Teams.TeamID} From 666e7b7daa10b6bbdf59dc7b100c604bab68682f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 17:20:05 -0400 Subject: [PATCH 10/45] WIP --- shared/chat/inbox-and-conversation-header.tsx | 10 +++++++--- shared/constants/init/index.native.tsx | 2 +- shared/constants/router.tsx | 4 ++-- shared/constants/types/router.tsx | 17 ++++++++--------- shared/router-v2/common.native.tsx | 2 +- shared/router-v2/routes.tsx | 7 +++---- shared/router-v2/screen-layout.desktop.tsx | 4 ++-- shared/teams/get-options.tsx | 7 +++++-- 8 files changed, 29 insertions(+), 24 deletions(-) diff --git a/shared/chat/inbox-and-conversation-header.tsx b/shared/chat/inbox-and-conversation-header.tsx index 250d3ee0e2ed..00655b18836f 100644 --- a/shared/chat/inbox-and-conversation-header.tsx +++ b/shared/chat/inbox-and-conversation-header.tsx @@ -5,13 +5,16 @@ import type {StyleOverride} from '@/common-adapters/markdown' import SearchRow from './inbox/search-row' import NewChatButton from './inbox/new-chat-button' import {useRoute} from '@react-navigation/native' -import type {RootRouteProps} from '@/router-v2/route-params' +import type {RouteProp} from '@react-navigation/native' +import {getRouteParamsFromRoute} from '@/router-v2/route-params' +import type {RootParamList} from '@/router-v2/route-params' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' import * as Teams from '@/stores/teams' const Header = () => { - const {params} = useRoute>() + const route = useRoute>() + const params = getRouteParamsFromRoute<'chatRoot'>(route) return ( { } const Header2 = () => { - const {params} = useRoute>() + const route = useRoute>() + const params = getRouteParamsFromRoute<'chatRoot'>(route) const username = useCurrentUserState(s => s.username) const infoPanelShowing = !!params?.infoPanel const data = Chat.useChatContext( diff --git a/shared/constants/init/index.native.tsx b/shared/constants/init/index.native.tsx index 7e936f7b7926..ee6149d76aa2 100644 --- a/shared/constants/init/index.native.tsx +++ b/shared/constants/init/index.native.tsx @@ -275,7 +275,7 @@ export const initPlatformListener = () => { routeName = cur } const ap = getVisiblePath() - ap.some(r => { + ap.some((r: ReturnType[number]) => { if (r.name === 'chatConversation') { const rParams = r.params as undefined | {conversationIDKey?: T.Chat.ConversationIDKey} param = {selectedConversationIDKey: rParams?.conversationIDKey} diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 5ff876cc819c..fb0f42b9f74c 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -124,7 +124,7 @@ export const getModalStack = (navState?: T.Immutable) => { if (!_isLoggedIn(rs)) { return [] } - return (rs.routes?.slice(1) ?? []).filter(r => !rootNonModalRouteNames.has(r.name)) + return (rs.routes?.slice(1) ?? []).filter((r: Route) => !rootNonModalRouteNames.has(r.name)) } export const getVisibleScreen = (navState?: T.Immutable, _inludeModals?: boolean) => { @@ -184,7 +184,7 @@ export const clearModals = () => { } const rootRoutes = ns?.routes ?? [] const keepRoutes = rootRoutes.filter( - (route, index) => index === 0 || rootNonModalRouteNames.has(route.name) + (route: Route, index: number) => index === 0 || rootNonModalRouteNames.has(route.name) ) if (keepRoutes.length !== rootRoutes.length) { n.dispatch({ diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 5e320597d845..29e71bf9b8ee 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -1,23 +1,22 @@ import type * as Styles from '@/styles' -import type {RootParamList as KBRootParamList} from '@/router-v2/route-params' import type {NativeStackNavigationProp, NativeStackNavigationOptions} from '@react-navigation/native-stack' -import type {RouteProp} from '@react-navigation/native' +import type {ParamListBase, RouteProp} from '@react-navigation/native' import type {HeaderBackButtonProps} from '@react-navigation/elements' -export type GetOptionsParams = { - navigation: NativeStackNavigationProp - route: RouteProp +export type GetOptionsParams = { + navigation: NativeStackNavigationProp + route: RouteProp } // Type for screen components that receive navigation props -export type ScreenProps = { - navigation: NativeStackNavigationProp - route: RouteProp +export type ScreenProps = { + navigation: NativeStackNavigationProp + route: RouteProp } export type ScreenComponentProps = { route: {params: any} - navigation: NativeStackNavigationProp + navigation: NativeStackNavigationProp } // Properties consumed by our layout functions (not React Navigation) export type LayoutOptions = { diff --git a/shared/router-v2/common.native.tsx b/shared/router-v2/common.native.tsx index 94a1c3bb7bd8..fd68f5e02a4f 100644 --- a/shared/router-v2/common.native.tsx +++ b/shared/router-v2/common.native.tsx @@ -87,7 +87,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ export const useSubnavTabAction = (navigation: NavigationContainerRef, state: NavState) => { const onSelectTab = (tab: string) => { - const route = state?.routes?.find(r => r.name === tab) + const route = state?.routes?.find((r: NonNullable[number]) => r.name === tab) const event = route ? navigation.emit({ canPreventDefault: true, diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index dcc08c870689..5ffea62a1508 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -14,7 +14,6 @@ import {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/r import type * as React from 'react' import * as Tabs from '@/constants/tabs' import type {GetOptions, GetOptionsParams, GetOptionsRet, RouteDef, RouteMap} from '@/constants/types/router' -import type {RootParamList as KBRootParamList} from '@/router-v2/route-params' import type {NativeStackNavigationOptions} from '@react-navigation/native-stack' // We have normal routes, modal routes, and logged out routes. @@ -165,12 +164,12 @@ export function routeMapToScreenElements( isLoggedOut: boolean, isTabScreen: boolean ) { - return (Object.keys(rs) as Array).flatMap(name => { - const rd = rs[name as string] + return Object.keys(rs).flatMap(name => { + const rd = rs[name] if (!rd) return [] return [ , n type ModalWrapperProps = { children: React.ReactNode navigationOptions?: GetOptionsRet - navigation: NativeStackNavigationProp + navigation: NativeStackNavigationProp } const ModalWrapper = (p: ModalWrapperProps) => { diff --git a/shared/teams/get-options.tsx b/shared/teams/get-options.tsx index 44eb330a0111..e7dcfdfe5554 100644 --- a/shared/teams/get-options.tsx +++ b/shared/teams/get-options.tsx @@ -3,7 +3,9 @@ import {HeaderRightActions} from './main/header' import {useSafeNavigation} from '@/util/safe-navigation' import {useTeamsState} from '@/stores/teams' import {useNavigation, useRoute} from '@react-navigation/native' -import type {RootParamList, RootRouteProps} from '@/router-v2/route-params' +import type {RouteProp} from '@react-navigation/native' +import type {RootParamList} from '@/router-v2/route-params' +import {getRouteParamsFromRoute} from '@/router-v2/route-params' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' const useHeaderActions = () => { @@ -16,7 +18,8 @@ const useHeaderActions = () => { } const TeamsFilter = () => { - const params = useRoute>().params ?? {} + const route = useRoute>() + const params = getRouteParamsFromRoute<'teamsRoot'>(route) ?? {} const navigation = useNavigation>() const filterValue = params.filter ?? '' const numTeams = useTeamsState(s => s.teamMeta.size) From 74f574317e495ff043181c0b47e0fd873824b317 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 17:26:08 -0400 Subject: [PATCH 11/45] WIP --- shared/constants/types/router.tsx | 2 +- shared/router-v2/screen-layout.desktop.tsx | 4 ++-- shared/teams/container.tsx | 16 ++++++++++------ shared/teams/routes.tsx | 5 ++--- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 29e71bf9b8ee..1b6fcb142437 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -16,7 +16,7 @@ export type ScreenProps = { export type ScreenComponentProps = { route: {params: any} - navigation: NativeStackNavigationProp + navigation: NativeStackNavigationProp } // Properties consumed by our layout functions (not React Navigation) export type LayoutOptions = { diff --git a/shared/router-v2/screen-layout.desktop.tsx b/shared/router-v2/screen-layout.desktop.tsx index 0c9ebcc9aebd..973d070408cc 100644 --- a/shared/router-v2/screen-layout.desktop.tsx +++ b/shared/router-v2/screen-layout.desktop.tsx @@ -36,7 +36,7 @@ const ModalHeader = (props: ModalHeaderProps) => ( const mouseResetValue = -9999 const mouseDistanceThreshold = 5 -const useMouseClick = (navigation: NativeStackNavigationProp, noClose?: boolean) => { +const useMouseClick = (navigation: NativeStackNavigationProp, noClose?: boolean) => { const backgroundRef = React.useRef(null) // we keep track of mouse down/up to determine if we should call it a 'click'. We don't want dragging the // window around to count @@ -71,7 +71,7 @@ const useMouseClick = (navigation: NativeStackNavigationProp, n type ModalWrapperProps = { children: React.ReactNode navigationOptions?: GetOptionsRet - navigation: NativeStackNavigationProp + navigation: NativeStackNavigationProp } const ModalWrapper = (p: ModalWrapperProps) => { diff --git a/shared/teams/container.tsx b/shared/teams/container.tsx index 552bdba998e4..907d006479da 100644 --- a/shared/teams/container.tsx +++ b/shared/teams/container.tsx @@ -2,11 +2,13 @@ import * as C from '@/constants' import * as Teams from '@/stores/teams' import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' -import type {RouteProps2} from '@/router-v2/route-params' +import type {RootParamList} from '@/router-v2/route-params' import Main from './main' import {useTeamsSubscribe} from './subscriber' import {useActivityLevels} from './common' import {useSafeNavigation} from '@/util/safe-navigation' +import {useNavigation} from '@react-navigation/native' +import type {NativeStackNavigationProp} from '@react-navigation/native-stack' const orderTeams = ( teams: ReadonlyMap, @@ -43,11 +45,12 @@ const orderTeams = ( }) } -type Props = RouteProps2<'teamsRoot'> +type Props = { + filter?: string + sort?: T.Teams.TeamListSort +} -const Connected = ({navigation, route}: Props) => { - const filter = route.params?.filter ?? '' - const sort = route.params?.sort ?? 'role' +const Connected = ({filter = '', sort = 'role'}: Props) => { const data = Teams.useTeamsState( C.useShallow(s => { const {deletedTeams, activityLevels, teamMeta, dispatch} = s @@ -77,6 +80,7 @@ const Connected = ({navigation, route}: Props) => { useActivityLevels(true) const nav = useSafeNavigation() + const navigation = useNavigation>() const onCreateTeam = () => launchNewTeamWizardOrModal() const onJoinTeam = () => nav.safeNavigateAppend('teamJoinTeamDialog') @@ -86,7 +90,7 @@ const Connected = ({navigation, route}: Props) => { onCreateTeam={onCreateTeam} onJoinTeam={onJoinTeam} deletedTeams={deletedTeams} - onChangeSort={sortOrder => navigation.setParams({...(route.params ?? {}), filter, sort: sortOrder})} + onChangeSort={sortOrder => navigation.setParams({filter, sort: sortOrder})} sortOrder={sort} teams={teams} /> diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 4cb014d2d6fc..201c8639efeb 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -209,10 +209,9 @@ export const newRoutes = { React.lazy(async () => import('./team/member/index.new')), {getOptions: {headerShadowVisible: false, headerTitle: ''}} ), - teamsRoot: { + teamsRoot: C.makeScreen(React.lazy(async () => import('./container')), { getOptions: teamsRootGetOptions, - screen: React.lazy(async () => import('./container')), - }, + }), } export const newModalRoutes = { From 135d84e79be1e9ebd0c928e2161c915047179bd8 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 17:33:53 -0400 Subject: [PATCH 12/45] WIP --- shared/constants/router.tsx | 4 ++-- shared/router-v2/common.native.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index fb0f42b9f74c..5ff876cc819c 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -124,7 +124,7 @@ export const getModalStack = (navState?: T.Immutable) => { if (!_isLoggedIn(rs)) { return [] } - return (rs.routes?.slice(1) ?? []).filter((r: Route) => !rootNonModalRouteNames.has(r.name)) + return (rs.routes?.slice(1) ?? []).filter(r => !rootNonModalRouteNames.has(r.name)) } export const getVisibleScreen = (navState?: T.Immutable, _inludeModals?: boolean) => { @@ -184,7 +184,7 @@ export const clearModals = () => { } const rootRoutes = ns?.routes ?? [] const keepRoutes = rootRoutes.filter( - (route: Route, index: number) => index === 0 || rootNonModalRouteNames.has(route.name) + (route, index) => index === 0 || rootNonModalRouteNames.has(route.name) ) if (keepRoutes.length !== rootRoutes.length) { n.dispatch({ diff --git a/shared/router-v2/common.native.tsx b/shared/router-v2/common.native.tsx index fd68f5e02a4f..a65be4af1a1b 100644 --- a/shared/router-v2/common.native.tsx +++ b/shared/router-v2/common.native.tsx @@ -87,7 +87,8 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ export const useSubnavTabAction = (navigation: NavigationContainerRef, state: NavState) => { const onSelectTab = (tab: string) => { - const route = state?.routes?.find((r: NonNullable[number]) => r.name === tab) + const routes = state && 'routes' in state ? state.routes : undefined + const route = routes?.find(r => r.name === tab) const event = route ? navigation.emit({ canPreventDefault: true, From 86583c931b442f264dbfe3c7f6ac41d68f2d243f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 18:47:37 -0400 Subject: [PATCH 13/45] WIP --- shared/constants/types/router.tsx | 65 ++++++++++++++++++++++++------- shared/router-v2/routes.tsx | 11 +++--- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 1b6fcb142437..980c7baa3ebb 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -3,20 +3,31 @@ import type {NativeStackNavigationProp, NativeStackNavigationOptions} from '@rea import type {ParamListBase, RouteProp} from '@react-navigation/native' import type {HeaderBackButtonProps} from '@react-navigation/elements' -export type GetOptionsParams = { - navigation: NativeStackNavigationProp - route: RouteProp +type RouteNameFor = Extract + +export type GetOptionsParams< + ParamList extends ParamListBase = ParamListBase, + RouteName extends RouteNameFor = RouteNameFor, +> = { + navigation: NativeStackNavigationProp + route: RouteProp } // Type for screen components that receive navigation props -export type ScreenProps = { - navigation: NativeStackNavigationProp - route: RouteProp +export type ScreenProps< + ParamList extends ParamListBase = ParamListBase, + RouteName extends RouteNameFor = RouteNameFor, +> = { + navigation: NativeStackNavigationProp + route: RouteProp } -export type ScreenComponentProps = { - route: {params: any} - navigation: NativeStackNavigationProp +export type ScreenComponentProps< + ParamList extends ParamListBase = ParamListBase, + RouteName extends RouteNameFor = RouteNameFor, +> = { + route: RouteProp + navigation: NativeStackNavigationProp } // Properties consumed by our layout functions (not React Navigation) export type LayoutOptions = { @@ -61,9 +72,37 @@ export type GetOptionsRet = }) | undefined -export type GetOptions = GetOptionsRet | ((p: any) => GetOptionsRet) -export type RouteDef = { - getOptions?: GetOptions - screen: React.ComponentType +type AnyScreen = React.ComponentType + +export type GetOptions = + | GetOptionsRet + | ((p: React.ComponentProps) => GetOptionsRet) + +export type RouteDef = { + getOptions?: GetOptions + screen: Screen } export type RouteMap = {[K in string]?: RouteDef} + +type RouteDefMatchesScreen = + R extends {screen: infer Screen; getOptions: infer Options} + ? Screen extends AnyScreen + ? Options extends GetOptionsRet + ? R + : Options extends (p: infer Props) => GetOptionsRet + ? [Props] extends [React.ComponentProps] + ? [React.ComponentProps] extends [Props] + ? R + : never + : never + : never + : never + : R extends {screen: infer Screen} + ? Screen extends AnyScreen + ? R + : never + : never + +export const defineRouteMap = >( + routes: {[K in keyof Routes]: RouteDefMatchesScreen} +) => routes diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index 5ffea62a1508..2cddf1f70617 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -13,13 +13,14 @@ import {newModalRoutes as walletsNewModalRoutes} from '../wallets/routes' import {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/routes' import type * as React from 'react' import * as Tabs from '@/constants/tabs' +import {defineRouteMap} from '@/constants/types/router' import type {GetOptions, GetOptionsParams, GetOptionsRet, RouteDef, RouteMap} from '@/constants/types/router' import type {NativeStackNavigationOptions} from '@react-navigation/native-stack' // We have normal routes, modal routes, and logged out routes. // We also end up using existence of a nameToTab value for a route as a test // of whether we're on a loggedIn route: loggedOut routes have no selected tab. -export const routes = { +export const routes = defineRouteMap({ ...deviceNewRoutes, ...chatNewRoutes, ...cryptoNewRoutes, @@ -29,7 +30,7 @@ export const routes = { ...settingsNewRoutes, ...teamsNewRoutes, ...gitNewRoutes, -} satisfies RouteMap +} satisfies RouteMap) if (__DEV__) { const allRouteKeys = [ @@ -64,7 +65,7 @@ export const tabRoots = { [Tabs.searchTab]: '', } as const -export const modalRoutes = { +export const modalRoutes = defineRouteMap({ ...chatNewModalRoutes, ...cryptoNewModalRoutes, ...deviceNewModalRoutes, @@ -78,7 +79,7 @@ export const modalRoutes = { ...teamsNewModalRoutes, ...walletsNewModalRoutes, ...incomingShareNewModalRoutes, -} satisfies RouteMap +} satisfies RouteMap) if (__DEV__) { const allModalKeys = [ @@ -103,7 +104,7 @@ if (__DEV__) { } } -export const loggedOutRoutes = {..._loggedOutRoutes, ...signupNewRoutes} satisfies RouteMap +export const loggedOutRoutes = defineRouteMap({..._loggedOutRoutes, ...signupNewRoutes} satisfies RouteMap) type LayoutFn = (props: { children: React.ReactNode From 2820b9f1f4e10c397aad45b88d6ee6c7020e26bc Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 18:54:32 -0400 Subject: [PATCH 14/45] WIP --- shared/router-v2/routes.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index 2cddf1f70617..1ea17a0d7660 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -30,7 +30,7 @@ export const routes = defineRouteMap({ ...settingsNewRoutes, ...teamsNewRoutes, ...gitNewRoutes, -} satisfies RouteMap) +}) if (__DEV__) { const allRouteKeys = [ @@ -79,7 +79,7 @@ export const modalRoutes = defineRouteMap({ ...teamsNewModalRoutes, ...walletsNewModalRoutes, ...incomingShareNewModalRoutes, -} satisfies RouteMap) +}) if (__DEV__) { const allModalKeys = [ @@ -104,7 +104,7 @@ if (__DEV__) { } } -export const loggedOutRoutes = defineRouteMap({..._loggedOutRoutes, ...signupNewRoutes} satisfies RouteMap) +export const loggedOutRoutes = defineRouteMap({..._loggedOutRoutes, ...signupNewRoutes}) type LayoutFn = (props: { children: React.ReactNode From 41e227a3c5f98e64f98e2f3bbf798047084d4d5f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 18:56:48 -0400 Subject: [PATCH 15/45] WIP --- shared/constants/types/router.tsx | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 980c7baa3ebb..585bc483b47c 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -85,23 +85,14 @@ export type RouteDef = { export type RouteMap = {[K in string]?: RouteDef} type RouteDefMatchesScreen = - R extends {screen: infer Screen; getOptions: infer Options} + R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Options extends GetOptionsRet - ? R - : Options extends (p: infer Props) => GetOptionsRet - ? [Props] extends [React.ComponentProps] - ? [React.ComponentProps] extends [Props] - ? R - : never - : never - : never - : never - : R extends {screen: infer Screen} - ? Screen extends AnyScreen - ? R - : never + ? Omit & { + getOptions?: GetOptions + screen: Screen + } : never + : never export const defineRouteMap = >( routes: {[K in keyof Routes]: RouteDefMatchesScreen} From 6dc81604dc01d8fe259c487ad657f80498467768 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 19:05:10 -0400 Subject: [PATCH 16/45] WIP --- shared/chat/inbox-and-conversation-header.tsx | 18 ++++------------ shared/router-v2/route-params.tsx | 21 ++++++++++++------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/shared/chat/inbox-and-conversation-header.tsx b/shared/chat/inbox-and-conversation-header.tsx index 00655b18836f..9850ecf4b537 100644 --- a/shared/chat/inbox-and-conversation-header.tsx +++ b/shared/chat/inbox-and-conversation-header.tsx @@ -5,32 +5,22 @@ import type {StyleOverride} from '@/common-adapters/markdown' import SearchRow from './inbox/search-row' import NewChatButton from './inbox/new-chat-button' import {useRoute} from '@react-navigation/native' -import type {RouteProp} from '@react-navigation/native' -import {getRouteParamsFromRoute} from '@/router-v2/route-params' -import type {RootParamList} from '@/router-v2/route-params' +import type {RootRouteProps} from '@/router-v2/route-params' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' import * as Teams from '@/stores/teams' const Header = () => { - const route = useRoute>() - const params = getRouteParamsFromRoute<'chatRoot'>(route) + const {params} = useRoute>() return ( - + ) } const Header2 = () => { - const route = useRoute>() - const params = getRouteParamsFromRoute<'chatRoot'>(route) + const {params} = useRoute>() const username = useCurrentUserState(s => s.username) const infoPanelShowing = !!params?.infoPanel const data = Chat.useChatContext( diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index 6137c4343727..b82d439cf8d3 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -1,3 +1,4 @@ +import type * as React from 'react' import type {RouteProp} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' // import type {StaticParamList} from '@react-navigation/core' @@ -8,14 +9,20 @@ import type {routes, modalRoutes, loggedOutRoutes} from './routes' // Once tsgo fixes re-exported generic type aliases, replace _ExtractParams: // type _SyntheticConfig = {readonly config: {readonly screens: _AllScreens}} // export type RootParamList = StaticParamList<_SyntheticConfig> & Tabs & {...} -type _ExtractParams = { - [K in keyof T]: T[K] extends {screen: infer U} - ? U extends (args: infer V) => any - ? V extends {route: {params: infer W}} - ? W - : undefined +type IsUnknown = unknown extends T ? ([keyof T] extends [never] ? true : false) : false +type NormalizeParams = IsUnknown extends true ? undefined : T extends object | undefined ? T : undefined +type ExtractScreenParams = + Screen extends React.LazyExoticComponent + ? ExtractScreenParams + : Screen extends React.ComponentType + ? Props extends {route: {params: infer Params}} + ? NormalizeParams + : Props extends {route: {params?: infer Params}} + ? NormalizeParams + : undefined : undefined - : undefined +type _ExtractParams = { + [K in keyof T]: T[K] extends {screen: infer Screen} ? ExtractScreenParams : undefined } type Tabs = { From d164b03730962dbadad38f23a60869e49c843348 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 19:07:18 -0400 Subject: [PATCH 17/45] WIP --- shared/constants/types/router.tsx | 2 +- shared/settings/routes.tsx | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 585bc483b47c..211712a86af8 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -95,5 +95,5 @@ type RouteDefMatchesScreen = : never export const defineRouteMap = >( - routes: {[K in keyof Routes]: RouteDefMatchesScreen} + routes: Routes & {[K in keyof Routes]: RouteDefMatchesScreen} ) => routes diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 4a1938645740..1d408f2d6d07 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -8,8 +8,6 @@ import * as Settings from '@/constants/settings' import {usePushState} from '@/stores/push' import {usePWState} from '@/stores/settings-password' import {e164ToDisplay} from '@/util/phone-numbers' -import {useRoute} from '@react-navigation/native' -import type {RootRouteProps} from '@/router-v2/route-params' const PushPromptSkipButton = () => { const rejectPermissions = usePushState(s => s.dispatch.rejectPermissions) @@ -47,9 +45,8 @@ const CheckPassphraseCancelButton = () => { ) } -const VerifyPhoneHeaderTitle = () => { - const {params} = useRoute>() - const displayPhone = e164ToDisplay(params.phoneNumber) +const VerifyPhoneHeaderTitle = ({phoneNumber}: {phoneNumber?: string}) => { + const displayPhone = e164ToDisplay(phoneNumber ?? '') return ( {displayPhone || 'Unknown number'} @@ -178,11 +175,11 @@ const sharedNewModalRoutes = { return {default: VerifyPhone} }), { - getOptions: { + getOptions: ({route}) => ({ headerLeft: Kb.Styles.isMobile ? () => : undefined, headerStyle: {backgroundColor: Kb.Styles.globalColors.blue}, - headerTitle: () => , - }, + headerTitle: () => , + }), } ), } From 8bb5a701b2a0ebfea40cbe17253f724e6a9fbabd Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 19:10:51 -0400 Subject: [PATCH 18/45] WIP --- shared/chat/inbox-and-conversation-header.tsx | 13 ++++-- shared/devices/index.tsx | 5 ++- shared/router-v2/route-params.tsx | 40 ++++++++++++++++++- shared/settings/root-desktop-tablet.tsx | 3 +- shared/teams/container.tsx | 10 ++++- shared/teams/get-options.tsx | 15 ++++--- shared/teams/join-team/container.tsx | 8 +++- 7 files changed, 75 insertions(+), 19 deletions(-) diff --git a/shared/chat/inbox-and-conversation-header.tsx b/shared/chat/inbox-and-conversation-header.tsx index 9850ecf4b537..fa8c469bdc80 100644 --- a/shared/chat/inbox-and-conversation-header.tsx +++ b/shared/chat/inbox-and-conversation-header.tsx @@ -4,14 +4,19 @@ import * as Kb from '@/common-adapters' import type {StyleOverride} from '@/common-adapters/markdown' import SearchRow from './inbox/search-row' import NewChatButton from './inbox/new-chat-button' -import {useRoute} from '@react-navigation/native' -import type {RootRouteProps} from '@/router-v2/route-params' +import {useRoute, type RouteProp} from '@react-navigation/native' import {useUsersState} from '@/stores/users' import {useCurrentUserState} from '@/stores/current-user' import * as Teams from '@/stores/teams' +type ChatRootParams = { + conversationIDKey?: string + infoPanel?: object +} +type ChatRootRoute = RouteProp<{chatRoot: ChatRootParams | undefined}, 'chatRoot'> + const Header = () => { - const {params} = useRoute>() + const {params} = useRoute() return ( @@ -20,7 +25,7 @@ const Header = () => { } const Header2 = () => { - const {params} = useRoute>() + const {params} = useRoute() const username = useCurrentUserState(s => s.username) const infoPanelShowing = !!params?.infoPanel const data = Chat.useChatContext( diff --git a/shared/devices/index.tsx b/shared/devices/index.tsx index b865d8d36105..bacc816467d2 100644 --- a/shared/devices/index.tsx +++ b/shared/devices/index.tsx @@ -9,9 +9,10 @@ import {useLocalBadging} from '@/util/use-local-badging' import {useModalHeaderState} from '@/stores/modal-header' import {HeaderTitle} from './nav-header' import {useNavigation} from '@react-navigation/native' -import type {RootParamList} from '@/router-v2/route-params' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +type DevicesRootParamList = {devicesRoot: undefined} + const sortDevices = (a: T.Devices.Device, b: T.Devices.Device) => { if (a.currentDevice) return -1 if (b.currentDevice) return 1 @@ -44,7 +45,7 @@ const splitAndSortDevices = (devices: ReadonlyArray) => const itemHeight = {height: 48, type: 'fixed'} as const function ReloadableDevices() { - const navigation = useNavigation>() + const navigation = useNavigation>() const [devices, setDevices] = React.useState>([]) const waiting = C.Waiting.useAnyWaiting(C.waitingKeyDevices) const loadDevicesRPC = C.useRPC(T.RPCGen.deviceDeviceHistoryListRpcPromise) diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index b82d439cf8d3..4eef66ba1ab9 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -2,7 +2,19 @@ import type * as React from 'react' import type {RouteProp} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' // import type {StaticParamList} from '@react-navigation/core' -import type {routes, modalRoutes, loggedOutRoutes} from './routes' +import type {newRoutes as chatNewRoutes, newModalRoutes as chatNewModalRoutes} from '../chat/routes' +import type {newRoutes as cryptoNewRoutes, newModalRoutes as cryptoNewModalRoutes} from '../crypto/routes' +import type {newRoutes as deviceNewRoutes, newModalRoutes as deviceNewModalRoutes} from '../devices/routes' +import type {newRoutes as fsNewRoutes, newModalRoutes as fsNewModalRoutes} from '../fs/routes' +import type {newRoutes as gitNewRoutes, newModalRoutes as gitNewModalRoutes} from '../git/routes' +import type {newRoutes as loginNewRoutes, newModalRoutes as loginNewModalRoutes} from '../login/routes' +import type {newRoutes as peopleNewRoutes, newModalRoutes as peopleNewModalRoutes} from '../people/routes' +import type {newRoutes as profileNewRoutes, newModalRoutes as profileNewModalRoutes} from '../profile/routes' +import type {newRoutes as settingsNewRoutes, newModalRoutes as settingsNewModalRoutes} from '../settings/routes' +import type {newRoutes as signupNewRoutes, newModalRoutes as signupNewModalRoutes} from '../signup/routes' +import type {newRoutes as teamsNewRoutes, newModalRoutes as teamsNewModalRoutes} from '../teams/routes' +import type {newModalRoutes as walletsNewModalRoutes} from '../wallets/routes' +import type {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/routes' // tsgo bug: StaticParamList is the idiomatic React Navigation equivalent of _ExtractParams, // but tsgo reports "TS2315: Type 'StaticParamList' is not generic" (works fine with regular tsc). @@ -51,7 +63,31 @@ type TabRoots = | 'devicesRoot' | 'settingsRoot' -type _AllScreens = typeof routes & typeof modalRoutes & typeof loggedOutRoutes +type _AllScreens = + & typeof deviceNewRoutes + & typeof chatNewRoutes + & typeof cryptoNewRoutes + & typeof peopleNewRoutes + & typeof profileNewRoutes + & typeof fsNewRoutes + & typeof settingsNewRoutes + & typeof teamsNewRoutes + & typeof gitNewRoutes + & typeof chatNewModalRoutes + & typeof cryptoNewModalRoutes + & typeof deviceNewModalRoutes + & typeof fsNewModalRoutes + & typeof gitNewModalRoutes + & typeof loginNewModalRoutes + & typeof peopleNewModalRoutes + & typeof profileNewModalRoutes + & typeof settingsNewModalRoutes + & typeof signupNewModalRoutes + & typeof teamsNewModalRoutes + & typeof walletsNewModalRoutes + & typeof incomingShareNewModalRoutes + & typeof loginNewRoutes + & typeof signupNewRoutes export type RootParamList = _ExtractParams<_AllScreens> & Tabs & {loading: undefined; loggedOut: undefined; loggedIn: undefined} diff --git a/shared/settings/root-desktop-tablet.tsx b/shared/settings/root-desktop-tablet.tsx index 0360f83b73c6..9603bf4c48e7 100644 --- a/shared/settings/root-desktop-tablet.tsx +++ b/shared/settings/root-desktop-tablet.tsx @@ -7,7 +7,6 @@ import type {RouteDef, GetOptionsParams} from '@/constants/types/router' import LeftNav from './sub-nav/left-nav' import {useNavigationBuilder, TabRouter, createNavigatorFactory} from '@react-navigation/core' import type {TypedNavigator, NavigatorTypeBagBase} from '@react-navigation/native' -import type {RootParamList} from '@/router-v2/route-params' import {settingsDesktopTabRoutes} from './routes' import {settingsAccountTab} from '@/constants/settings' @@ -73,7 +72,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ })) type NavType = NavigatorTypeBagBase & { - ParamList: Pick + ParamList: {[K in keyof typeof settingsDesktopTabRoutes]: undefined} } export const createLeftTabNavigator = createNavigatorFactory(LeftTabNavigator) as unknown as () => TypedNavigator diff --git a/shared/teams/container.tsx b/shared/teams/container.tsx index 907d006479da..009887e3e5d2 100644 --- a/shared/teams/container.tsx +++ b/shared/teams/container.tsx @@ -2,7 +2,6 @@ import * as C from '@/constants' import * as Teams from '@/stores/teams' import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' -import type {RootParamList} from '@/router-v2/route-params' import Main from './main' import {useTeamsSubscribe} from './subscriber' import {useActivityLevels} from './common' @@ -10,6 +9,13 @@ import {useSafeNavigation} from '@/util/safe-navigation' import {useNavigation} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +type TeamsRootParamList = { + teamsRoot: { + filter?: string + sort?: T.Teams.TeamListSort + } | undefined +} + const orderTeams = ( teams: ReadonlyMap, newRequests: T.Immutable, @@ -80,7 +86,7 @@ const Connected = ({filter = '', sort = 'role'}: Props) => { useActivityLevels(true) const nav = useSafeNavigation() - const navigation = useNavigation>() + const navigation = useNavigation>() const onCreateTeam = () => launchNewTeamWizardOrModal() const onJoinTeam = () => nav.safeNavigateAppend('teamJoinTeamDialog') diff --git a/shared/teams/get-options.tsx b/shared/teams/get-options.tsx index e7dcfdfe5554..7c1730f2aad8 100644 --- a/shared/teams/get-options.tsx +++ b/shared/teams/get-options.tsx @@ -1,13 +1,18 @@ import * as Kb from '@/common-adapters' +import type * as T from '@/constants/types' import {HeaderRightActions} from './main/header' import {useSafeNavigation} from '@/util/safe-navigation' import {useTeamsState} from '@/stores/teams' import {useNavigation, useRoute} from '@react-navigation/native' import type {RouteProp} from '@react-navigation/native' -import type {RootParamList} from '@/router-v2/route-params' -import {getRouteParamsFromRoute} from '@/router-v2/route-params' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +type TeamsRootParams = { + filter?: string + sort?: T.Teams.TeamListSort +} +type TeamsRootParamList = {teamsRoot: TeamsRootParams | undefined} + const useHeaderActions = () => { const nav = useSafeNavigation() const launchNewTeamWizardOrModal = useTeamsState(s => s.dispatch.launchNewTeamWizardOrModal) @@ -18,9 +23,9 @@ const useHeaderActions = () => { } const TeamsFilter = () => { - const route = useRoute>() - const params = getRouteParamsFromRoute<'teamsRoot'>(route) ?? {} - const navigation = useNavigation>() + const route = useRoute>() + const params = route.params ?? {} + const navigation = useNavigation>() const filterValue = params.filter ?? '' const numTeams = useTeamsState(s => s.teamMeta.size) const setFilter = (filter: string) => navigation.setParams({...params, filter}) diff --git a/shared/teams/join-team/container.tsx b/shared/teams/join-team/container.tsx index 6e436eb7657d..519e8b279b57 100644 --- a/shared/teams/join-team/container.tsx +++ b/shared/teams/join-team/container.tsx @@ -1,7 +1,6 @@ import * as C from '@/constants' import * as T from '@/constants/types' import * as Kb from '@/common-adapters' -import type {RootParamList} from '@/router-v2/route-params' import {RPCError} from '@/util/errors' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' import upperFirst from 'lodash/upperFirst' @@ -9,6 +8,9 @@ import * as React from 'react' import {useNavigation} from '@react-navigation/native' type OwnProps = {initialTeamname?: string; success?: boolean} +type TeamJoinTeamDialogParamList = { + teamJoinTeamDialog: OwnProps | undefined +} const getJoinTeamError = (error: unknown) => { if (error instanceof RPCError) { @@ -29,7 +31,9 @@ const Container = ({initialTeamname, success: successParam}: OwnProps) => { const [successTeamName, setSuccessTeamName] = React.useState('') const [name, _setName] = React.useState(initialTeamname ?? '') const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) - const navigation = useNavigation>() + const navigation = useNavigation< + NativeStackNavigationProp + >() const navigateUp = C.Router2.navigateUp const success = !!successParam const handoffToInviteRef = React.useRef(false) From 3b25ce6ff9efa1c1ef435f3897f00420f1c07b41 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 19:12:10 -0400 Subject: [PATCH 19/45] WIP --- shared/settings/routes.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 1d408f2d6d07..8445505401bb 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -67,6 +67,9 @@ const VerifyPhoneHeaderLeft = () => { } const SettingsRootDesktop = React.lazy(async () => import('./root-desktop-tablet')) +const EmptySettingsScreen = () => <> +const ManageContactsScreen: React.ComponentType = + C.isMobile ? React.lazy(async () => import('./manage-contacts')) : EmptySettingsScreen const feedback = C.makeScreen( React.lazy(async () => import('./feedback/container')), @@ -195,7 +198,7 @@ export const newRoutes = { ...sharedNewRoutes, [Settings.settingsContactsTab]: { getOptions: {header: undefined, title: 'Contacts'}, - screen: C.isMobile ? React.lazy(async () => import('./manage-contacts')) : () => <>, + screen: ManageContactsScreen, }, webLinks: C.makeScreen(WebLinks, { getOptions: ({route}) => ({ From 9e656dd11224a0b052c211815e89459e6a19292c Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 21:41:48 -0400 Subject: [PATCH 20/45] WIP --- shared/profile/add-to-team.tsx | 102 ++++++++++---------- shared/teams/join-team/join-from-invite.tsx | 34 ++++--- shared/teams/routes.tsx | 4 +- 3 files changed, 72 insertions(+), 68 deletions(-) diff --git a/shared/profile/add-to-team.tsx b/shared/profile/add-to-team.tsx index 5d2cc0348fcd..0c613764355c 100644 --- a/shared/profile/add-to-team.tsx +++ b/shared/profile/add-to-team.tsx @@ -114,66 +114,68 @@ const Container = (ownProps: OwnProps) => { ) }) - const onAddToTeams = React.useEffectEvent(async (role: T.Teams.TeamRoleType, teams: Array) => { - const requestID = submitRequestID.current + 1 - submitRequestID.current = requestID - setAddUserToTeamsResults('') - setAddUserToTeamsState('notStarted') + const onAddToTeams = React.useEffectEvent( + async (role: T.Teams.TeamRoleType, teams: Array, sendChatNotification: boolean) => { + const requestID = submitRequestID.current + 1 + submitRequestID.current = requestID + setAddUserToTeamsResults('') + setAddUserToTeamsState('notStarted') + + const teamsAddedTo: Array = [] + const errorAddingTo: Array = [] + + for (const team of teams) { + const teamID = teamNameToID.get(team) + if (!teamID) { + logger.warn(`no team ID found for ${team}`) + errorAddingTo.push(team) + continue + } - const teamsAddedTo: Array = [] - const errorAddingTo: Array = [] + const added = await new Promise(resolve => { + addUserToTeam( + [ + { + email: '', + phone: '', + role: T.RPCGen.TeamRole[role], + sendChatNotification, + teamID, + username: them, + }, + [C.waitingKeyTeamsTeam(teamID), C.waitingKeyTeamsAddUserToTeams(them)], + ], + () => resolve(true), + _ => resolve(false) + ) + }) + + if (submitRequestID.current !== requestID) { + return + } - for (const team of teams) { - const teamID = teamNameToID.get(team) - if (!teamID) { - logger.warn(`no team ID found for ${team}`) - errorAddingTo.push(team) - continue + if (added) { + teamsAddedTo.push(team) + } else { + errorAddingTo.push(team) + } } - const added = await new Promise(resolve => { - addUserToTeam( - [ - { - email: '', - phone: '', - role: T.RPCGen.TeamRole[role], - sendChatNotification: true, - teamID, - username: them, - }, - [C.waitingKeyTeamsTeam(teamID), C.waitingKeyTeamsAddUserToTeams(them)], - ], - () => resolve(true), - _ => resolve(false) - ) - }) - if (submitRequestID.current !== requestID) { return } - if (added) { - teamsAddedTo.push(team) + const result = makeAddUserToTeamsResult(them, teamsAddedTo, errorAddingTo) + setAddUserToTeamsResults(result) + if (errorAddingTo.length > 0) { + setAddUserToTeamsState('failed') + loadTeamList() } else { - errorAddingTo.push(team) + setAddUserToTeamsState('succeeded') + clearModals() } } - - if (submitRequestID.current !== requestID) { - return - } - - const result = makeAddUserToTeamsResult(them, teamsAddedTo, errorAddingTo) - setAddUserToTeamsResults(result) - if (errorAddingTo.length > 0) { - setAddUserToTeamsState('failed') - loadTeamList() - } else { - setAddUserToTeamsState('succeeded') - clearModals() - } - }) + ) React.useEffect(() => { setAddUserToTeamsResults('') @@ -191,7 +193,7 @@ const Container = (ownProps: OwnProps) => { } const onSave = () => { - void onAddToTeams(selectedRole, [...selectedTeams]) + void onAddToTeams(selectedRole, [...selectedTeams], sendNotification) } const toggleTeamSelected = (teamName: string, selected: boolean) => { diff --git a/shared/teams/join-team/join-from-invite.tsx b/shared/teams/join-team/join-from-invite.tsx index 3085614ae1e9..a24ce3593b00 100644 --- a/shared/teams/join-team/join-from-invite.tsx +++ b/shared/teams/join-team/join-from-invite.tsx @@ -31,6 +31,9 @@ const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inv const [details, setDetails] = React.useState(initialInviteDetails) const [error, setError] = React.useState('') const loaded = details !== undefined || !!error + const canLoadDetails = details === undefined && !error && !!inviteID + const canJoin = !!inviteKey + const missingInviteKeyError = details !== undefined && !canJoin ? 'Sorry, that invite token is not valid.' : '' const joinTeam = C.useRPC(T.RPCGen.teamsTeamAcceptInviteOrRequestAccessRpcListener) const requestInviteLinkDetails = C.useRPC(T.RPCGen.teamsGetInviteLinkDetailsRpcPromise) const [clickedJoin, setClickedJoin] = React.useState(false) @@ -46,28 +49,26 @@ const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inv }, [initialInviteDetails, inviteID, inviteKey]) React.useEffect(() => { - if (loaded) { + if (!canLoadDetails) { return } - if (inviteKey === '') { - requestInviteLinkDetails( - [{inviteID}], - result => { - setDetails(result) - setError('') - }, - rpcError => { - setError(getInviteError(rpcError, true)) - } - ) - } - }, [inviteID, inviteKey, loaded, requestInviteLinkDetails]) + requestInviteLinkDetails( + [{inviteID}], + result => { + setDetails(result) + setError('') + }, + rpcError => { + setError(getInviteError(rpcError, true)) + } + ) + }, [canLoadDetails, inviteID, requestInviteLinkDetails]) const nav = useSafeNavigation() const onNavUp = () => nav.safeNavigateUp() const onJoinTeam = () => { - if (!inviteKey) { + if (!canJoin) { return } setClickedJoin(true) @@ -161,11 +162,12 @@ const JoinFromInvite = ({inviteDetails: initialInviteDetails, inviteID = '', inv label="Join team" onClick={onJoinTeam} style={styles.button} + disabled={!canJoin} waiting={waiting} /> - {!!error && {error}} + {!!(error || missingInviteKeyError) && {error || missingInviteKeyError}} diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 201c8639efeb..260765865853 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -292,8 +292,8 @@ export const newModalRoutes = { teamInviteLinkJoin: C.makeScreen(React.lazy(async () => import('./join-team/join-from-invite'))), teamJoinTeamDialog: C.makeScreen(React.lazy(async () => import('./join-team/container')), { getOptions: ({route}) => ({ - headerLeft: () => , - headerTitle: () => , + headerLeft: () => , + headerTitle: () => , }), }), teamNewTeamDialog: C.makeScreen(React.lazy(async () => import('./new-team')), { From 5881a08287ce971e52859e39c79fc41c9f593208 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 22:03:25 -0400 Subject: [PATCH 21/45] WIP --- shared/constants/router.tsx | 14 ++++++++++++-- shared/stores/chat.tsx | 26 +++++++++++++++++++++----- shared/stores/convostate.tsx | 6 +++--- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 5ff876cc819c..4d10f28d899e 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -28,6 +28,16 @@ type InferComponentProps = ? P : undefined +type NavigatorParamsFromProps

| undefined> = P extends undefined + ? undefined + : {} extends P + ? P | undefined + : P + +type ScreenParams> = NavigatorParamsFromProps< + InferComponentProps +> + export const navigationRef = createNavigationContainerRef() registerDebugClear(() => { @@ -162,12 +172,12 @@ export const useSafeFocusEffect = (fn: () => void) => { export function makeScreen>( Component: COM, options?: { - getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) + getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } ) { return { ...options, - screen: function Screen(p: StaticScreenProps>) { + screen: function Screen(p: StaticScreenProps>) { const Comp = Component as any return }, diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index e76d87a8c69c..ca6ad592b305 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -11,7 +11,7 @@ import isEqual from 'lodash/isEqual' import logger from '@/logger' import type {State as DaemonState} from '@/stores/daemon' import type * as Router2 from '@/constants/router' -import {type ChatProviderProps, ProviderScreen} from '@/stores/convostate' +import {ProviderScreen} from '@/stores/convostate' import type {GetOptionsRet} from '@/constants/types/router' import {RPCError} from '@/util/errors' import {bodyToJSON} from '@/constants/rpc-utils' @@ -2023,25 +2023,41 @@ type InferComponentProps = ? P : undefined +type NavigatorParamsFromProps

| undefined> = P extends undefined + ? undefined + : {} extends P + ? P | undefined + : P + +type AddConversationIDKey

| undefined> = P extends undefined + ? {conversationIDKey?: T.Chat.ConversationIDKey} + : Omit & {conversationIDKey?: T.Chat.ConversationIDKey} + +type ChatScreenParams> = NavigatorParamsFromProps< + AddConversationIDKey> +> + +type ChatScreenProps> = StaticScreenProps> + export function makeChatScreen>( Component: COM, options?: { getOptions?: | GetOptionsRet - | ((props: ChatProviderProps>>) => GetOptionsRet) + | ((props: ChatScreenProps) => GetOptionsRet) skipProvider?: boolean canBeNullConvoID?: boolean } ) { return { ...options, - screen: function Screen(p: ChatProviderProps>>) { + screen: function Screen(p: ChatScreenProps) { const Comp = Component as any return options?.skipProvider ? ( - + ) : ( - + ) }, diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 25d30901a7f9..88a712ed8bfd 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -3619,14 +3619,14 @@ export function useConvoState(id: T.Chat.ConversationIDKey, selector: (state: return useStore(store, selector) } -export type ChatProviderProps = T & {route: {params: {conversationIDKey?: T.Chat.ConversationIDKey}}} +type ChatRouteParams = {conversationIDKey?: T.Chat.ConversationIDKey} type RouteParams = { - route: {params: {conversationIDKey?: T.Chat.ConversationIDKey}} + route: {params?: ChatRouteParams} } export function ProviderScreen(p: {children: React.ReactNode; rp: RouteParams; canBeNull?: boolean}) { return ( - + {p.children} ) From acfaa3f6447e0e876c07bd3d74ebe093cdcc26e6 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Tue, 31 Mar 2026 22:07:48 -0400 Subject: [PATCH 22/45] WIP --- shared/chat/conversation/header-area/index.d.ts | 2 +- shared/chat/routes.tsx | 4 ++-- shared/profile/routes.tsx | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/shared/chat/conversation/header-area/index.d.ts b/shared/chat/conversation/header-area/index.d.ts index 2196679d1e13..a367e95900fc 100644 --- a/shared/chat/conversation/header-area/index.d.ts +++ b/shared/chat/conversation/header-area/index.d.ts @@ -1,6 +1,6 @@ import type {GetOptionsRet} from '@/constants/types/router' declare function headerNavigationOptions(route: { - params: {conversationIDKey?: string} + params?: {conversationIDKey?: string} }): Partial export {headerNavigationOptions} diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index 0776be9a3f76..7116b350d9ed 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -202,8 +202,8 @@ export const newModalRoutes = { React.lazy(async () => import('./send-to-chat')), { getOptions: ({route}) => ({ - headerLeft: () => , - title: FS.getSharePathArrayDescription(route.params.sendPaths || []), + headerLeft: () => , + title: FS.getSharePathArrayDescription(route.params?.sendPaths || []), }), skipProvider: true, } diff --git a/shared/profile/routes.tsx b/shared/profile/routes.tsx index 45659c35c355..c62c8349c06c 100644 --- a/shared/profile/routes.tsx +++ b/shared/profile/routes.tsx @@ -82,9 +82,13 @@ export const newModalRoutes = { }), profileEditAvatar: C.makeScreen(React.lazy(async () => import('./edit-avatar')), { getOptions: ({route}) => ({ - headerLeft: () => , - headerRight: () => , - headerTitle: () => , + headerLeft: () => ( + + ), + headerRight: () => , + headerTitle: () => ( + + ), }), }), profileImport: C.makeScreen(React.lazy(async () => import('./pgp/import'))), From 9b31dcf2bea1e39f1535219038b65f53e57ea930 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 09:54:54 -0400 Subject: [PATCH 23/45] WIP --- shared/crypto/sub-nav/index.desktop.tsx | 5 +++-- shared/router-v2/routes.tsx | 27 ++++++++++++++++++------- shared/settings/routes.tsx | 5 +++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/shared/crypto/sub-nav/index.desktop.tsx b/shared/crypto/sub-nav/index.desktop.tsx index 89918e1cd57b..1fdfbddc7004 100644 --- a/shared/crypto/sub-nav/index.desktop.tsx +++ b/shared/crypto/sub-nav/index.desktop.tsx @@ -13,9 +13,10 @@ import type {TypedNavigator, NavigatorTypeBagBase} from '@react-navigation/nativ import {routeMapToScreenElements} from '@/router-v2/routes' import {makeLayout} from '@/router-v2/screen-layout.desktop' import type {RouteDef, GetOptionsParams} from '@/constants/types/router' +import {defineRouteMap} from '@/constants/types/router' /* Desktop SubNav */ -const cryptoSubRoutes = { +const cryptoSubRoutes = defineRouteMap({ [Crypto.decryptTab]: { screen: React.lazy(async () => { const {DecryptIO} = await import('../decrypt') @@ -41,7 +42,7 @@ const cryptoSubRoutes = { return {default: VerifyIO} }), }, -} +}) function LeftTabNavigator({ initialRouteName, children, diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index 1ea17a0d7660..f7566c030f19 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -14,7 +14,7 @@ import {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/r import type * as React from 'react' import * as Tabs from '@/constants/tabs' import {defineRouteMap} from '@/constants/types/router' -import type {GetOptions, GetOptionsParams, GetOptionsRet, RouteDef, RouteMap} from '@/constants/types/router' +import type {GetOptions, GetOptionsParams, GetOptionsRet, RouteDef} from '@/constants/types/router' import type {NativeStackNavigationOptions} from '@react-navigation/native-stack' // We have normal routes, modal routes, and logged out routes. @@ -118,14 +118,27 @@ type MakeLayoutFn = ( getOptions?: GetOptions ) => LayoutFn type MakeOptionsFn = (rd: RouteDef) => (params: GetOptionsParams) => GetOptionsRet +type AnyScreen = React.ComponentType +type RouteDefForScreen = + R extends {screen: infer Screen} + ? Screen extends AnyScreen + ? Omit & { + getOptions?: GetOptions + screen: Screen + } + : never + : never +type CheckedRouteMap> = Routes & { + [K in keyof Routes]: RouteDefForScreen +} function toNavOptions(opts: GetOptionsRet): NativeStackNavigationOptions { if (!opts) return {} return opts as NativeStackNavigationOptions } -export function routeMapToStaticScreens( - rs: RouteMap, +export function routeMapToStaticScreens>( + rs: CheckedRouteMap, makeLayoutFn: MakeLayoutFn, isModal: boolean, isLoggedOut: boolean, @@ -139,7 +152,7 @@ export function routeMapToStaticScreens( screen: React.ComponentType } > = {} - for (const [name, rd] of Object.entries(rs)) { + for (const [name, rd] of Object.entries(rs) as Array<[string, RS[keyof RS]]>) { if (!rd) continue result[name] = { // Layout functions return JSX (ReactElement) and accept any route/navigation. @@ -156,8 +169,8 @@ export function routeMapToStaticScreens( return result } -export function routeMapToScreenElements( - rs: RouteMap, +export function routeMapToScreenElements>( + rs: CheckedRouteMap, Screen: React.ComponentType, makeLayoutFn: MakeLayoutFn, makeOptionsFn: MakeOptionsFn, @@ -165,7 +178,7 @@ export function routeMapToScreenElements( isLoggedOut: boolean, isTabScreen: boolean ) { - return Object.keys(rs).flatMap(name => { + return (Object.keys(rs) as Array).flatMap(name => { const rd = rs[name] if (!rd) return [] return [ diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 8445505401bb..97839f79fa5b 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -5,6 +5,7 @@ import {newRoutes as devicesRoutes} from '../devices/routes' import {newRoutes as gitRoutes} from '../git/routes' import {newRoutes as walletsRoutes} from '../wallets/routes' import * as Settings from '@/constants/settings' +import {defineRouteMap} from '@/constants/types/router' import {usePushState} from '@/stores/push' import {usePWState} from '@/stores/settings-password' import {e164ToDisplay} from '@/util/phone-numbers' @@ -129,7 +130,7 @@ export const sharedNewRoutes = { makeIcons: {screen: React.lazy(async () => import('./make-icons.page'))}, } -export const settingsDesktopTabRoutes = { +export const settingsDesktopTabRoutes = defineRouteMap({ [Settings.settingsAboutTab]: sharedNewRoutes[Settings.settingsAboutTab], [Settings.settingsAccountTab]: sharedNewRoutes[Settings.settingsAccountTab], [Settings.settingsAdvancedTab]: sharedNewRoutes[Settings.settingsAdvancedTab], @@ -144,7 +145,7 @@ export const settingsDesktopTabRoutes = { [Settings.settingsNotificationsTab]: sharedNewRoutes[Settings.settingsNotificationsTab], [Settings.settingsScreenprotectorTab]: sharedNewRoutes[Settings.settingsScreenprotectorTab], [Settings.settingsWalletsTab]: sharedNewRoutes[Settings.settingsWalletsTab], -} +}) const sharedNewModalRoutes = { [Settings.settingsLogOutTab]: C.makeScreen(React.lazy(async () => import('./logout')), { From 696e3064dab874a12f4644863dd05c65f87b7707 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 10:06:04 -0400 Subject: [PATCH 24/45] WIP --- shared/router-v2/routes.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index f7566c030f19..b162e05af2f2 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -131,6 +131,8 @@ type RouteDefForScreen = type CheckedRouteMap> = Routes & { [K in keyof Routes]: RouteDefForScreen } +type CheckedRouteEntry> = + CheckedRouteMap[keyof Routes] function toNavOptions(opts: GetOptionsRet): NativeStackNavigationOptions { if (!opts) return {} @@ -152,8 +154,7 @@ export function routeMapToStaticScreens } > = {} - for (const [name, rd] of Object.entries(rs) as Array<[string, RS[keyof RS]]>) { - if (!rd) continue + for (const [name, rd] of Object.entries(rs) as Array<[string, CheckedRouteEntry]>) { result[name] = { // Layout functions return JSX (ReactElement) and accept any route/navigation. // Cast bridges our specific KBRootParamList types to RN's generic ParamListBase. @@ -179,8 +180,7 @@ export function routeMapToScreenElements).flatMap(name => { - const rd = rs[name] - if (!rd) return [] + const rd = rs[name] as CheckedRouteEntry return [ Date: Wed, 1 Apr 2026 11:25:10 -0400 Subject: [PATCH 25/45] WIP --- .../conversation/input-area/normal/index.tsx | 2 +- shared/chat/inbox-and-conversation-header.tsx | 6 ++-- shared/chat/inbox/row/build-team.tsx | 2 +- shared/chat/routes.tsx | 26 +++++++++------ shared/constants/router.tsx | 18 +++++++--- shared/constants/types/router.tsx | 10 +++++- shared/profile/routes.tsx | 6 ++-- shared/router-v2/linking.tsx | 2 +- shared/router-v2/route-params.tsx | 33 ++----------------- shared/router-v2/routes.tsx | 3 ++ shared/stores/chat.tsx | 16 +++++++-- shared/teams/container.tsx | 4 +-- shared/teams/get-options.tsx | 6 ++-- shared/teams/join-team/container.tsx | 2 +- shared/teams/routes.tsx | 13 +++++--- 15 files changed, 80 insertions(+), 69 deletions(-) diff --git a/shared/chat/conversation/input-area/normal/index.tsx b/shared/chat/conversation/input-area/normal/index.tsx index 8571056cd57e..990ee298e2d1 100644 --- a/shared/chat/conversation/input-area/normal/index.tsx +++ b/shared/chat/conversation/input-area/normal/index.tsx @@ -110,7 +110,7 @@ const doInjectText = (inputRef: React.RefObject, text: string, const ConnectedPlatformInput = function ConnectedPlatformInput() { const route = useRoute | RootRouteProps<'chatRoot'>>() - const infoPanelShowing = route.name === 'chatRoot' ? !!route.params?.infoPanel : false + const infoPanelShowing = route.name === 'chatRoot' ? !!route.params.infoPanel : false const data = Chat.useChatContext( C.useShallow(s => { const {meta, id: conversationIDKey, editing: editOrdinal, messageMap, unsentText} = s diff --git a/shared/chat/inbox-and-conversation-header.tsx b/shared/chat/inbox-and-conversation-header.tsx index fa8c469bdc80..955b3fa7206c 100644 --- a/shared/chat/inbox-and-conversation-header.tsx +++ b/shared/chat/inbox-and-conversation-header.tsx @@ -13,12 +13,12 @@ type ChatRootParams = { conversationIDKey?: string infoPanel?: object } -type ChatRootRoute = RouteProp<{chatRoot: ChatRootParams | undefined}, 'chatRoot'> +type ChatRootRoute = RouteProp<{chatRoot: ChatRootParams}, 'chatRoot'> const Header = () => { const {params} = useRoute() return ( - + ) @@ -27,7 +27,7 @@ const Header = () => { const Header2 = () => { const {params} = useRoute() const username = useCurrentUserState(s => s.username) - const infoPanelShowing = !!params?.infoPanel + const infoPanelShowing = !!params.infoPanel const data = Chat.useChatContext( C.useShallow(s => { const {meta, id, dispatch} = s diff --git a/shared/chat/inbox/row/build-team.tsx b/shared/chat/inbox/row/build-team.tsx index 085471b04473..9c6e44c532d4 100644 --- a/shared/chat/inbox/row/build-team.tsx +++ b/shared/chat/inbox/row/build-team.tsx @@ -12,7 +12,7 @@ function BuildTeam() { launchNewTeamWizardOrModal() } const onJoinTeam = () => { - nav.safeNavigateAppend('teamJoinTeamDialog') + nav.safeNavigateAppend({name: 'teamJoinTeamDialog', params: {}}) } return ( diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index 7116b350d9ed..001e0e5ded59 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -98,14 +98,20 @@ export const newRoutes = { screen: React.lazy(async () => import('./conversation/rekey/enter-paper-key')), }, chatRoot: Chat.isSplit - ? Chat.makeChatScreen( - React.lazy(async () => import('./inbox-and-conversation')), - {getOptions: inboxAndConvoGetOptions, skipProvider: true} - ) - : Chat.makeChatScreen( - React.lazy(async () => import('./inbox/defer-loading')), - {getOptions: inboxGetOptions, skipProvider: true} - ), + ? { + ...Chat.makeChatScreen(React.lazy(async () => import('./inbox-and-conversation')), { + getOptions: inboxAndConvoGetOptions, + skipProvider: true, + }), + initialParams: {}, + } + : { + ...Chat.makeChatScreen(React.lazy(async () => import('./inbox/defer-loading')), { + getOptions: inboxGetOptions, + skipProvider: true, + }), + initialParams: {}, + }, } export const newModalRoutes = { @@ -202,8 +208,8 @@ export const newModalRoutes = { React.lazy(async () => import('./send-to-chat')), { getOptions: ({route}) => ({ - headerLeft: () => , - title: FS.getSharePathArrayDescription(route.params?.sendPaths || []), + headerLeft: () => , + title: FS.getSharePathArrayDescription(route.params.sendPaths || []), }), skipProvider: true, } diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 4d10f28d899e..c58669324fd5 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -30,9 +30,7 @@ type InferComponentProps = type NavigatorParamsFromProps

| undefined> = P extends undefined ? undefined - : {} extends P - ? P | undefined - : P + : P type ScreenParams> = NavigatorParamsFromProps< InferComponentProps @@ -175,8 +173,20 @@ export function makeScreen>( getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } ) { + const getOptionsOption = options?.getOptions + const getOptions = typeof getOptionsOption === 'function' + ? (p: StaticScreenProps>) => + getOptionsOption({ + ...p, + route: { + ...p.route, + params: (p.route.params ?? {}) as ScreenParams, + }, + }) + : getOptionsOption return { ...options, + getOptions, screen: function Screen(p: StaticScreenProps>) { const Comp = Component as any return @@ -371,7 +381,7 @@ export const navToThread = (conversationIDKey: T.Chat.ConversationIDKey) => { { name: 'loggedIn', state: { - routes: [{name: Tabs.chatTab, state: {index: 0, routes: [{name: 'chatRoot'}]}}], + routes: [{name: Tabs.chatTab, state: {index: 0, routes: [{name: 'chatRoot', params: {}}]}}], }, }, {name: 'chatConversation', params: {conversationIDKey}}, diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 211712a86af8..b5a06756b20f 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -73,6 +73,12 @@ export type GetOptionsRet = | undefined type AnyScreen = React.ComponentType +type ScreenRouteParams = + React.ComponentProps extends {route: {params: infer Params}} + ? Params + : React.ComponentProps extends {route: {params?: infer Params}} + ? Params + : undefined export type GetOptions = | GetOptionsRet @@ -80,6 +86,7 @@ export type GetOptions = export type RouteDef = { getOptions?: GetOptions + initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams screen: Screen } export type RouteMap = {[K in string]?: RouteDef} @@ -87,8 +94,9 @@ export type RouteMap = {[K in string]?: RouteDef} type RouteDefMatchesScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Omit & { + ? Omit & { getOptions?: GetOptions + initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams screen: Screen } : never diff --git a/shared/profile/routes.tsx b/shared/profile/routes.tsx index c62c8349c06c..2758427e7687 100644 --- a/shared/profile/routes.tsx +++ b/shared/profile/routes.tsx @@ -83,11 +83,11 @@ export const newModalRoutes = { profileEditAvatar: C.makeScreen(React.lazy(async () => import('./edit-avatar')), { getOptions: ({route}) => ({ headerLeft: () => ( - + ), - headerRight: () => , + headerRight: () => , headerTitle: () => ( - + ), }), }), diff --git a/shared/router-v2/linking.tsx b/shared/router-v2/linking.tsx index fa2a1041267b..a5ea7992bfee 100644 --- a/shared/router-v2/linking.tsx +++ b/shared/router-v2/linking.tsx @@ -103,7 +103,7 @@ export const makeChatConversationState = (conversationIDKey: string): PartialNav name: 'loggedIn', state: { index: 0, - routes: [{name: Tabs.chatTab, state: {index: 0, routes: [{name: 'chatRoot'}]}}], + routes: [{name: Tabs.chatTab, state: {index: 0, routes: [{name: 'chatRoot', params: {}}]}}], }, }, {name: 'chatConversation', params: {conversationIDKey}}, diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index 4eef66ba1ab9..aca3ae020b44 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -52,17 +52,6 @@ type Tabs = { 'tabs.walletsTab': undefined } -type TabRoots = - | 'peopleRoot' - | 'chatRoot' - | 'cryptoRoot' - | 'fsRoot' - | 'teamsRoot' - | 'walletsRoot' - | 'gitRoot' - | 'devicesRoot' - | 'settingsRoot' - type _AllScreens = & typeof deviceNewRoutes & typeof chatNewRoutes @@ -93,32 +82,14 @@ export type RootParamList = _ExtractParams<_AllScreens> & Tabs & {loading: undefined; loggedOut: undefined; loggedIn: undefined} export type RouteKeys = keyof RootParamList -type AllOptional = { - [K in keyof T]-?: undefined extends T[K] ? true : false -}[keyof T] extends true - ? true - : false type Distribute = U extends RouteKeys ? RootParamList[U] extends undefined ? U - : AllOptional extends true - ? {name: U; params: RootParamList[U]} | U - : {name: U; params: RootParamList[U]} + : {name: U; params: RootParamList[U]} : never export type NavigateAppendType = Distribute +export type RootRouteProps = RouteProp -type MaybeMissingParamsRouteProp = Omit< - RouteProp, - 'params' -> & { - params?: RootParamList[RouteName] -} - -export type RootRouteProps = RouteName extends TabRoots - ? MaybeMissingParamsRouteProp - : RouteProp - -// Tab roots can mount before any params object exists, even when the screen prop type is an object. export type RouteProps2 = { route: RootRouteProps navigation: NativeStackNavigationProp diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index b162e05af2f2..ac0919739f1c 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -149,6 +149,7 @@ export function routeMapToStaticScreens React.ReactElement options: (p: {route: any; navigation: any}) => NativeStackNavigationOptions screen: React.ComponentType @@ -156,6 +157,7 @@ export function routeMapToStaticScreens = {} for (const [name, rd] of Object.entries(rs) as Array<[string, CheckedRouteEntry]>) { result[name] = { + ...(rd.initialParams === undefined ? {} : {initialParams: rd.initialParams as object}), // Layout functions return JSX (ReactElement) and accept any route/navigation. // Cast bridges our specific KBRootParamList types to RN's generic ParamListBase. layout: makeLayoutFn(isModal, isLoggedOut, isTabScreen, rd.getOptions) as (props: any) => React.ReactElement, @@ -186,6 +188,7 @@ export function routeMapToScreenElements, diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index ca6ad592b305..7d40431e57d6 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2025,9 +2025,7 @@ type InferComponentProps = type NavigatorParamsFromProps

| undefined> = P extends undefined ? undefined - : {} extends P - ? P | undefined - : P + : P type AddConversationIDKey

| undefined> = P extends undefined ? {conversationIDKey?: T.Chat.ConversationIDKey} @@ -2049,8 +2047,20 @@ export function makeChatScreen>( canBeNullConvoID?: boolean } ) { + const getOptionsOption = options?.getOptions + const getOptions = typeof getOptionsOption === 'function' + ? (p: ChatScreenProps) => + getOptionsOption({ + ...p, + route: { + ...p.route, + params: (p.route.params ?? {}) as ChatScreenParams, + }, + }) + : getOptionsOption return { ...options, + getOptions, screen: function Screen(p: ChatScreenProps) { const Comp = Component as any return options?.skipProvider ? ( diff --git a/shared/teams/container.tsx b/shared/teams/container.tsx index 009887e3e5d2..973b80d4d787 100644 --- a/shared/teams/container.tsx +++ b/shared/teams/container.tsx @@ -13,7 +13,7 @@ type TeamsRootParamList = { teamsRoot: { filter?: string sort?: T.Teams.TeamListSort - } | undefined + } } const orderTeams = ( @@ -88,7 +88,7 @@ const Connected = ({filter = '', sort = 'role'}: Props) => { const nav = useSafeNavigation() const navigation = useNavigation>() const onCreateTeam = () => launchNewTeamWizardOrModal() - const onJoinTeam = () => nav.safeNavigateAppend('teamJoinTeamDialog') + const onJoinTeam = () => nav.safeNavigateAppend({name: 'teamJoinTeamDialog', params: {}}) return ( diff --git a/shared/teams/get-options.tsx b/shared/teams/get-options.tsx index 7c1730f2aad8..147e3884ad64 100644 --- a/shared/teams/get-options.tsx +++ b/shared/teams/get-options.tsx @@ -11,20 +11,20 @@ type TeamsRootParams = { filter?: string sort?: T.Teams.TeamListSort } -type TeamsRootParamList = {teamsRoot: TeamsRootParams | undefined} +type TeamsRootParamList = {teamsRoot: TeamsRootParams} const useHeaderActions = () => { const nav = useSafeNavigation() const launchNewTeamWizardOrModal = useTeamsState(s => s.dispatch.launchNewTeamWizardOrModal) return { onCreateTeam: () => launchNewTeamWizardOrModal(), - onJoinTeam: () => nav.safeNavigateAppend('teamJoinTeamDialog'), + onJoinTeam: () => nav.safeNavigateAppend({name: 'teamJoinTeamDialog', params: {}}), } } const TeamsFilter = () => { const route = useRoute>() - const params = route.params ?? {} + const params = route.params const navigation = useNavigation>() const filterValue = params.filter ?? '' const numTeams = useTeamsState(s => s.teamMeta.size) diff --git a/shared/teams/join-team/container.tsx b/shared/teams/join-team/container.tsx index 519e8b279b57..71691127ca6a 100644 --- a/shared/teams/join-team/container.tsx +++ b/shared/teams/join-team/container.tsx @@ -9,7 +9,7 @@ import {useNavigation} from '@react-navigation/native' type OwnProps = {initialTeamname?: string; success?: boolean} type TeamJoinTeamDialogParamList = { - teamJoinTeamDialog: OwnProps | undefined + teamJoinTeamDialog: OwnProps } const getJoinTeamError = (error: unknown) => { diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 260765865853..319f6e1d8df2 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -209,9 +209,12 @@ export const newRoutes = { React.lazy(async () => import('./team/member/index.new')), {getOptions: {headerShadowVisible: false, headerTitle: ''}} ), - teamsRoot: C.makeScreen(React.lazy(async () => import('./container')), { - getOptions: teamsRootGetOptions, - }), + teamsRoot: { + ...C.makeScreen(React.lazy(async () => import('./container')), { + getOptions: teamsRootGetOptions, + }), + initialParams: {}, + }, } export const newModalRoutes = { @@ -292,8 +295,8 @@ export const newModalRoutes = { teamInviteLinkJoin: C.makeScreen(React.lazy(async () => import('./join-team/join-from-invite'))), teamJoinTeamDialog: C.makeScreen(React.lazy(async () => import('./join-team/container')), { getOptions: ({route}) => ({ - headerLeft: () => , - headerTitle: () => , + headerLeft: () => , + headerTitle: () => , }), }), teamNewTeamDialog: C.makeScreen(React.lazy(async () => import('./new-team')), { From 51e7bb00e3b8d831acd4d3ec9fb2051ac49b05f9 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 13:46:07 -0400 Subject: [PATCH 26/45] WIP --- shared/constants/router.tsx | 5 ++++- shared/stores/chat.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index c58669324fd5..870e72bf7e09 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -28,9 +28,12 @@ type InferComponentProps = ? P : undefined +type HasKeys

> = keyof P extends never ? false : true type NavigatorParamsFromProps

| undefined> = P extends undefined ? undefined - : P + : HasKeys

extends true + ? P + : undefined type ScreenParams> = NavigatorParamsFromProps< InferComponentProps diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 7d40431e57d6..573759d52f85 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2023,9 +2023,12 @@ type InferComponentProps = ? P : undefined +type HasKeys

> = keyof P extends never ? false : true type NavigatorParamsFromProps

| undefined> = P extends undefined ? undefined - : P + : HasKeys

extends true + ? P + : undefined type AddConversationIDKey

| undefined> = P extends undefined ? {conversationIDKey?: T.Chat.ConversationIDKey} From bb57e3ec0798c36955f53d03505b81689593d782 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 13:49:53 -0400 Subject: [PATCH 27/45] WIP --- shared/constants/router.tsx | 16 +++++++++------- shared/constants/types/router.tsx | 8 +++++--- shared/router-v2/route-params.tsx | 6 +++++- shared/stores/chat.tsx | 16 +++++++++------- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 870e72bf7e09..e845c8c45438 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -28,12 +28,11 @@ type InferComponentProps = ? P : undefined -type HasKeys

> = keyof P extends never ? false : true -type NavigatorParamsFromProps

| undefined> = P extends undefined - ? undefined - : HasKeys

extends true - ? P - : undefined +type NavigatorParamsFromProps

= P extends Record + ? keyof P extends never + ? undefined + : P + : undefined type ScreenParams> = NavigatorParamsFromProps< InferComponentProps @@ -175,7 +174,10 @@ export function makeScreen>( options?: { getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -) { +): import('./types/router').RouteDef< + React.ComponentType>>, + ScreenParams +> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: StaticScreenProps>) => diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index b5a06756b20f..fe15490834c2 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -84,9 +84,10 @@ export type GetOptions = | GetOptionsRet | ((p: React.ComponentProps) => GetOptionsRet) -export type RouteDef = { +export type RouteDef> = { + __routeParams?: Params getOptions?: GetOptions - initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams + initialParams?: Params extends undefined ? undefined : Params screen: Screen } export type RouteMap = {[K in string]?: RouteDef} @@ -94,7 +95,8 @@ export type RouteMap = {[K in string]?: RouteDef} type RouteDefMatchesScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Omit & { + ? Omit & { + __routeParams?: ScreenRouteParams getOptions?: GetOptions initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams screen: Screen diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index aca3ae020b44..cb78cb95a8f3 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -34,7 +34,11 @@ type ExtractScreenParams = : undefined : undefined type _ExtractParams = { - [K in keyof T]: T[K] extends {screen: infer Screen} ? ExtractScreenParams : undefined + [K in keyof T]: T[K] extends {__routeParams?: infer Params} + ? NormalizeParams + : T[K] extends {screen: infer Screen} + ? ExtractScreenParams + : undefined } type Tabs = { diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 573759d52f85..936223d60f16 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2023,12 +2023,11 @@ type InferComponentProps = ? P : undefined -type HasKeys

> = keyof P extends never ? false : true -type NavigatorParamsFromProps

| undefined> = P extends undefined - ? undefined - : HasKeys

extends true - ? P - : undefined +type NavigatorParamsFromProps

= P extends Record + ? keyof P extends never + ? undefined + : P + : undefined type AddConversationIDKey

| undefined> = P extends undefined ? {conversationIDKey?: T.Chat.ConversationIDKey} @@ -2049,7 +2048,10 @@ export function makeChatScreen>( skipProvider?: boolean canBeNullConvoID?: boolean } -) { +): import('@/constants/types/router').RouteDef< + React.ComponentType>, + ChatScreenParams +> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: ChatScreenProps) => From c53888e4b72cf408ea20f3440821cf0b30efeecc Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 13:56:48 -0400 Subject: [PATCH 28/45] WIP --- shared/constants/router.tsx | 5 +--- shared/constants/types/router.tsx | 8 +++--- shared/router-v2/route-params.tsx | 41 +++++++++---------------------- shared/stores/chat.tsx | 5 +--- 4 files changed, 17 insertions(+), 42 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index e845c8c45438..5659ab4f86d2 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -174,10 +174,7 @@ export function makeScreen>( options?: { getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -): import('./types/router').RouteDef< - React.ComponentType>>, - ScreenParams -> { +): import('./types/router').RouteDef>>> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: StaticScreenProps>) => diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index fe15490834c2..b5a06756b20f 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -84,10 +84,9 @@ export type GetOptions = | GetOptionsRet | ((p: React.ComponentProps) => GetOptionsRet) -export type RouteDef> = { - __routeParams?: Params +export type RouteDef = { getOptions?: GetOptions - initialParams?: Params extends undefined ? undefined : Params + initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams screen: Screen } export type RouteMap = {[K in string]?: RouteDef} @@ -95,8 +94,7 @@ export type RouteMap = {[K in string]?: RouteDef} type RouteDefMatchesScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Omit & { - __routeParams?: ScreenRouteParams + ? Omit & { getOptions?: GetOptions initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams screen: Screen diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index cb78cb95a8f3..bce937170567 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -1,7 +1,5 @@ -import type * as React from 'react' -import type {RouteProp} from '@react-navigation/native' +import type {RouteProp, StaticParamList} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' -// import type {StaticParamList} from '@react-navigation/core' import type {newRoutes as chatNewRoutes, newModalRoutes as chatNewModalRoutes} from '../chat/routes' import type {newRoutes as cryptoNewRoutes, newModalRoutes as cryptoNewModalRoutes} from '../crypto/routes' import type {newRoutes as deviceNewRoutes, newModalRoutes as deviceNewModalRoutes} from '../devices/routes' @@ -16,31 +14,6 @@ import type {newRoutes as teamsNewRoutes, newModalRoutes as teamsNewModalRoutes} import type {newModalRoutes as walletsNewModalRoutes} from '../wallets/routes' import type {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/routes' -// tsgo bug: StaticParamList is the idiomatic React Navigation equivalent of _ExtractParams, -// but tsgo reports "TS2315: Type 'StaticParamList' is not generic" (works fine with regular tsc). -// Once tsgo fixes re-exported generic type aliases, replace _ExtractParams: -// type _SyntheticConfig = {readonly config: {readonly screens: _AllScreens}} -// export type RootParamList = StaticParamList<_SyntheticConfig> & Tabs & {...} -type IsUnknown = unknown extends T ? ([keyof T] extends [never] ? true : false) : false -type NormalizeParams = IsUnknown extends true ? undefined : T extends object | undefined ? T : undefined -type ExtractScreenParams = - Screen extends React.LazyExoticComponent - ? ExtractScreenParams - : Screen extends React.ComponentType - ? Props extends {route: {params: infer Params}} - ? NormalizeParams - : Props extends {route: {params?: infer Params}} - ? NormalizeParams - : undefined - : undefined -type _ExtractParams = { - [K in keyof T]: T[K] extends {__routeParams?: infer Params} - ? NormalizeParams - : T[K] extends {screen: infer Screen} - ? ExtractScreenParams - : undefined -} - type Tabs = { 'tabs.chatTab': undefined 'tabs.cryptoTab': undefined @@ -82,8 +55,18 @@ type _AllScreens = & typeof loginNewRoutes & typeof signupNewRoutes -export type RootParamList = _ExtractParams<_AllScreens> & +type _SyntheticConfig = {readonly config: {readonly screens: _AllScreens}} +type AppRouteParamList = StaticParamList<_SyntheticConfig> + +type KeybaseRootParamList = AppRouteParamList & Tabs & {loading: undefined; loggedOut: undefined; loggedIn: undefined} +export type RootParamList = KeybaseRootParamList + +declare global { + namespace ReactNavigation { + interface RootParamList extends KeybaseRootParamList {} + } +} export type RouteKeys = keyof RootParamList type Distribute = U extends RouteKeys diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 936223d60f16..bb0d9d3daedd 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2048,10 +2048,7 @@ export function makeChatScreen>( skipProvider?: boolean canBeNullConvoID?: boolean } -): import('@/constants/types/router').RouteDef< - React.ComponentType>, - ChatScreenParams -> { +): import('@/constants/types/router').RouteDef>> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: ChatScreenProps) => From 9611dfb9330d5ba8eadc5147f0264c5d93521278 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 14:01:41 -0400 Subject: [PATCH 29/45] WIP --- shared/constants/router.tsx | 9 ++++++--- shared/router-v2/common.native.tsx | 2 +- shared/router-v2/route-params.tsx | 3 ++- shared/router-v2/routes.tsx | 7 ++++++- shared/stores/chat.tsx | 5 ++++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 5659ab4f86d2..2de74ab04018 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -37,6 +37,9 @@ type NavigatorParamsFromProps

= P extends Record type ScreenParams> = NavigatorParamsFromProps< InferComponentProps > +type ScreenComponent> = ( + p: StaticScreenProps> +) => React.ReactElement export const navigationRef = createNavigationContainerRef() @@ -134,7 +137,7 @@ export const getModalStack = (navState?: T.Immutable) => { if (!_isLoggedIn(rs)) { return [] } - return (rs.routes?.slice(1) ?? []).filter(r => !rootNonModalRouteNames.has(r.name)) + return (rs.routes?.slice(1) ?? []).filter((r: Route) => !rootNonModalRouteNames.has(r.name)) } export const getVisibleScreen = (navState?: T.Immutable, _inludeModals?: boolean) => { @@ -174,7 +177,7 @@ export function makeScreen>( options?: { getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -): import('./types/router').RouteDef>>> { +): import('./types/router').RouteDef> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: StaticScreenProps>) => @@ -206,7 +209,7 @@ export const clearModals = () => { } const rootRoutes = ns?.routes ?? [] const keepRoutes = rootRoutes.filter( - (route, index) => index === 0 || rootNonModalRouteNames.has(route.name) + (route: Route, index: number) => index === 0 || rootNonModalRouteNames.has(route.name) ) if (keepRoutes.length !== rootRoutes.length) { n.dispatch({ diff --git a/shared/router-v2/common.native.tsx b/shared/router-v2/common.native.tsx index a65be4af1a1b..36d399998fbc 100644 --- a/shared/router-v2/common.native.tsx +++ b/shared/router-v2/common.native.tsx @@ -88,7 +88,7 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ export const useSubnavTabAction = (navigation: NavigationContainerRef, state: NavState) => { const onSelectTab = (tab: string) => { const routes = state && 'routes' in state ? state.routes : undefined - const route = routes?.find(r => r.name === tab) + const route = routes?.find((r: {name?: string; key?: string}) => r.name === tab) const event = route ? navigation.emit({ canPreventDefault: true, diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index bce937170567..e211b85b6d9e 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -1,4 +1,5 @@ -import type {RouteProp, StaticParamList} from '@react-navigation/native' +import type {RouteProp} from '@react-navigation/native' +import type {StaticParamList} from '@react-navigation/core' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' import type {newRoutes as chatNewRoutes, newModalRoutes as chatNewModalRoutes} from '../chat/routes' import type {newRoutes as cryptoNewRoutes, newModalRoutes as cryptoNewModalRoutes} from '../crypto/routes' diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index ac0919739f1c..023f1fd304bc 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -122,8 +122,13 @@ type AnyScreen = React.ComponentType type RouteDefForScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Omit & { + ? Omit & { getOptions?: GetOptions + initialParams?: React.ComponentProps extends {route: {params: infer Params}} + ? Params + : React.ComponentProps extends {route: {params?: infer Params}} + ? Params + : undefined screen: Screen } : never diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index bb0d9d3daedd..ac7162bcfacf 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2038,6 +2038,9 @@ type ChatScreenParams> = NavigatorPar > type ChatScreenProps> = StaticScreenProps> +type ChatScreenComponent> = ( + p: ChatScreenProps +) => React.ReactElement export function makeChatScreen>( Component: COM, @@ -2048,7 +2051,7 @@ export function makeChatScreen>( skipProvider?: boolean canBeNullConvoID?: boolean } -): import('@/constants/types/router').RouteDef>> { +): import('@/constants/types/router').RouteDef> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: ChatScreenProps) => From 9ec65f025e1af26df63abb074a388c08dde9cf41 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 14:17:35 -0400 Subject: [PATCH 30/45] WIP --- shared/router-v2/route-params.tsx | 23 ++++++++++++++++++----- shared/settings/routes.tsx | 4 ++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index e211b85b6d9e..b61a7e39fe85 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -1,5 +1,5 @@ +import type * as React from 'react' import type {RouteProp} from '@react-navigation/native' -import type {StaticParamList} from '@react-navigation/core' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' import type {newRoutes as chatNewRoutes, newModalRoutes as chatNewModalRoutes} from '../chat/routes' import type {newRoutes as cryptoNewRoutes, newModalRoutes as cryptoNewModalRoutes} from '../crypto/routes' @@ -15,6 +15,22 @@ import type {newRoutes as teamsNewRoutes, newModalRoutes as teamsNewModalRoutes} import type {newModalRoutes as walletsNewModalRoutes} from '../wallets/routes' import type {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/routes' +type IsUnknown = unknown extends T ? ([keyof T] extends [never] ? true : false) : false +type NormalizeParams = IsUnknown extends true ? undefined : T extends object | undefined ? T : undefined +type ExtractScreenParams = + Screen extends React.LazyExoticComponent + ? ExtractScreenParams + : Screen extends React.ComponentType + ? Props extends {route: {params: infer Params}} + ? NormalizeParams + : Props extends {route: {params?: infer Params}} + ? NormalizeParams + : undefined + : undefined +type _ExtractParams = { + [K in keyof T]: T[K] extends {screen: infer Screen} ? ExtractScreenParams : undefined +} + type Tabs = { 'tabs.chatTab': undefined 'tabs.cryptoTab': undefined @@ -56,10 +72,7 @@ type _AllScreens = & typeof loginNewRoutes & typeof signupNewRoutes -type _SyntheticConfig = {readonly config: {readonly screens: _AllScreens}} -type AppRouteParamList = StaticParamList<_SyntheticConfig> - -type KeybaseRootParamList = AppRouteParamList & +type KeybaseRootParamList = _ExtractParams<_AllScreens> & Tabs & {loading: undefined; loggedOut: undefined; loggedIn: undefined} export type RootParamList = KeybaseRootParamList diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 97839f79fa5b..46e6ac2b7cd3 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -77,7 +77,7 @@ const feedback = C.makeScreen( {getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}} ) -export const sharedNewRoutes = { +export const sharedNewRoutes = defineRouteMap({ [Settings.settingsAboutTab]: { getOptions: {title: 'About'}, screen: React.lazy(async () => import('./about')), @@ -128,7 +128,7 @@ export const sharedNewRoutes = { }, keybaseLinkError: {screen: React.lazy(async () => import('../deeplinks/error'))}, makeIcons: {screen: React.lazy(async () => import('./make-icons.page'))}, -} +}) export const settingsDesktopTabRoutes = defineRouteMap({ [Settings.settingsAboutTab]: sharedNewRoutes[Settings.settingsAboutTab], From e34b11f27ce3f154351f8f92aa1bc5b97ac19d88 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 14:20:04 -0400 Subject: [PATCH 31/45] WIP --- shared/constants/router.tsx | 6 +++--- shared/router-v2/route-params.tsx | 6 ------ shared/router-v2/router.native.tsx | 3 ++- shared/stores/chat.tsx | 12 +++++++----- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 2de74ab04018..95f2163c4c2c 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -12,7 +12,7 @@ import { } from '@react-navigation/core' import type {StaticScreenProps} from '@react-navigation/core' import type {NavigateAppendType, RouteKeys, RootParamList as KBRootParamList} from '@/router-v2/route-params' -import type {GetOptionsRet} from './types/router' +import type {GetOptionsRet, RouteDef} from './types/router' import {isSplit} from './chat/layout' import {isMobile} from './platform' import {shallowEqual} from './utils' @@ -41,7 +41,7 @@ type ScreenComponent> = ( p: StaticScreenProps> ) => React.ReactElement -export const navigationRef = createNavigationContainerRef() +export const navigationRef = createNavigationContainerRef() registerDebugClear(() => { navigationRef.current = null @@ -177,7 +177,7 @@ export function makeScreen>( options?: { getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -): import('./types/router').RouteDef> { +): RouteDef> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: StaticScreenProps>) => diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index b61a7e39fe85..844fc09bfbc5 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -76,12 +76,6 @@ type KeybaseRootParamList = _ExtractParams<_AllScreens> & Tabs & {loading: undefined; loggedOut: undefined; loggedIn: undefined} export type RootParamList = KeybaseRootParamList -declare global { - namespace ReactNavigation { - interface RootParamList extends KeybaseRootParamList {} - } -} - export type RouteKeys = keyof RootParamList type Distribute = U extends RouteKeys ? RootParamList[U] extends undefined diff --git a/shared/router-v2/router.native.tsx b/shared/router-v2/router.native.tsx index 68725aaeffb3..afa6840ba243 100644 --- a/shared/router-v2/router.native.tsx +++ b/shared/router-v2/router.native.tsx @@ -22,6 +22,7 @@ import type {SFSymbol} from 'sf-symbols-typescript' import {makeLayout} from './screen-layout.native' import {useRootKey} from './hooks.native' import {createLinkingConfig} from './linking' +import type {RootParamList} from './route-params' import {handleAppLink} from '@/constants/deeplinks' import {useDaemonState} from '@/stores/daemon' import {useNotifState} from '@/stores/notifications' @@ -220,7 +221,7 @@ const rootStackScreenOptions = {headerBackButtonDisplayMode: 'minimal'} satisfie const modalScreenOptions = ({ navigation, }: { - navigation: NavigationProp + navigation: NavigationProp }): NativeStackNavigationOptions => { const cancelItem: NativeStackNavigationOptions = Platform.OS === 'ios' diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index ac7162bcfacf..5e4efee9f9cf 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -12,7 +12,7 @@ import logger from '@/logger' import type {State as DaemonState} from '@/stores/daemon' import type * as Router2 from '@/constants/router' import {ProviderScreen} from '@/stores/convostate' -import type {GetOptionsRet} from '@/constants/types/router' +import type {GetOptionsRet, RouteDef} from '@/constants/types/router' import {RPCError} from '@/util/errors' import {bodyToJSON} from '@/constants/rpc-utils' import {clearChatStores, chatStores} from '@/stores/convostate' @@ -2051,15 +2051,16 @@ export function makeChatScreen>( skipProvider?: boolean canBeNullConvoID?: boolean } -): import('@/constants/types/router').RouteDef> { +): RouteDef> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: ChatScreenProps) => + // getOptions can run before params are materialized on the route object. getOptionsOption({ ...p, route: { ...p.route, - params: (p.route.params ?? {}) as ChatScreenParams, + params: (((p.route as {params?: ChatScreenParams}).params ?? {}) as ChatScreenParams), }, }) : getOptionsOption @@ -2068,11 +2069,12 @@ export function makeChatScreen>( getOptions, screen: function Screen(p: ChatScreenProps) { const Comp = Component as any + const params = (((p.route as {params?: ChatScreenParams}).params ?? {}) as ChatScreenParams) return options?.skipProvider ? ( - + ) : ( - + ) }, From 7ac7909897b8d8775af9924e2b0feca154b7602e Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 14:33:45 -0400 Subject: [PATCH 32/45] WIP --- shared/constants/router.tsx | 6 +++--- shared/constants/types/router.tsx | 11 ++++++++--- shared/router-v2/route-params.tsx | 10 +++++++++- shared/stores/chat.tsx | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 95f2163c4c2c..219b76cca951 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -137,7 +137,7 @@ export const getModalStack = (navState?: T.Immutable) => { if (!_isLoggedIn(rs)) { return [] } - return (rs.routes?.slice(1) ?? []).filter((r: Route) => !rootNonModalRouteNames.has(r.name)) + return (rs.routes?.slice(1) ?? []).filter(r => !rootNonModalRouteNames.has(r.name)) } export const getVisibleScreen = (navState?: T.Immutable, _inludeModals?: boolean) => { @@ -177,7 +177,7 @@ export function makeScreen>( options?: { getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -): RouteDef> { +): RouteDef, ScreenParams> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: StaticScreenProps>) => @@ -209,7 +209,7 @@ export const clearModals = () => { } const rootRoutes = ns?.routes ?? [] const keepRoutes = rootRoutes.filter( - (route: Route, index: number) => index === 0 || rootNonModalRouteNames.has(route.name) + (route, index) => index === 0 || rootNonModalRouteNames.has(route.name) ) if (keepRoutes.length !== rootRoutes.length) { n.dispatch({ diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index b5a06756b20f..a2c2f690b7da 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -84,9 +84,13 @@ export type GetOptions = | GetOptionsRet | ((p: React.ComponentProps) => GetOptionsRet) -export type RouteDef = { +export type RouteDef< + Screen extends AnyScreen = AnyScreen, + Params = ScreenRouteParams, +> = { + __routeParams?: Params getOptions?: GetOptions - initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams + initialParams?: Params extends undefined ? undefined : Params screen: Screen } export type RouteMap = {[K in string]?: RouteDef} @@ -94,7 +98,8 @@ export type RouteMap = {[K in string]?: RouteDef} type RouteDefMatchesScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Omit & { + ? Omit & { + __routeParams?: ScreenRouteParams getOptions?: GetOptions initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams screen: Screen diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index 844fc09bfbc5..d450e580722c 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -27,8 +27,16 @@ type ExtractScreenParams = ? NormalizeParams : undefined : undefined +type ExtractRouteParams = '__routeParams' extends keyof Route + ? Route extends {__routeParams?: infer Params} + ? NormalizeParams + : undefined + : Route extends {screen: infer Screen} + ? ExtractScreenParams + : undefined + type _ExtractParams = { - [K in keyof T]: T[K] extends {screen: infer Screen} ? ExtractScreenParams : undefined + [K in keyof T]: ExtractRouteParams } type Tabs = { diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 5e4efee9f9cf..cc7142fa6ce1 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2051,7 +2051,7 @@ export function makeChatScreen>( skipProvider?: boolean canBeNullConvoID?: boolean } -): RouteDef> { +): RouteDef, ChatScreenParams> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' ? (p: ChatScreenProps) => From 7d40855c143dcda5316af7bd261e0e8d925daa3c Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 14:42:11 -0400 Subject: [PATCH 33/45] WIP --- shared/app/global-errors.tsx | 4 +-- shared/chat/conversation/bot/install.tsx | 2 +- .../conversation/input-area/normal/index.tsx | 3 ++- .../messages/text/coinflip/errors.tsx | 2 +- shared/common-adapters/reload.tsx | 4 +-- shared/constants/types/router.tsx | 8 ++++-- shared/devices/nav-header.tsx | 2 +- shared/devices/routes.tsx | 9 ++++--- shared/login/loading.tsx | 2 +- shared/login/routes.tsx | 9 ++++--- shared/profile/routes.tsx | 9 ++++--- shared/profile/user/hooks.tsx | 2 +- shared/router-v2/routes.tsx | 25 ++++++++++++++++--- shared/settings/routes.tsx | 8 +++--- 14 files changed, 57 insertions(+), 32 deletions(-) diff --git a/shared/app/global-errors.tsx b/shared/app/global-errors.tsx index e81516cb8372..4b35e8b78f67 100644 --- a/shared/app/global-errors.tsx +++ b/shared/app/global-errors.tsx @@ -32,9 +32,9 @@ const useData = () => { setGlobalError() if (loggedIn) { clearModals() - navigateAppend(settingsFeedbackTab) + navigateAppend({name: settingsFeedbackTab, params: {}}) } else { - navigateAppend('feedback') + navigateAppend({name: 'feedback', params: {}}) } } const onDismiss = () => { diff --git a/shared/chat/conversation/bot/install.tsx b/shared/chat/conversation/bot/install.tsx index 6f124ea6d305..fe61338060e9 100644 --- a/shared/chat/conversation/bot/install.tsx +++ b/shared/chat/conversation/bot/install.tsx @@ -157,7 +157,7 @@ const InstallBotPopup = (props: Props) => { }) } const onFeedback = () => { - navigateAppend('feedback') + navigateAppend({name: 'feedback', params: {}}) } const refreshBotSettings = Chat.useChatContext(s => s.dispatch.refreshBotSettings) diff --git a/shared/chat/conversation/input-area/normal/index.tsx b/shared/chat/conversation/input-area/normal/index.tsx index 990ee298e2d1..29ce889c81a8 100644 --- a/shared/chat/conversation/input-area/normal/index.tsx +++ b/shared/chat/conversation/input-area/normal/index.tsx @@ -110,7 +110,8 @@ const doInjectText = (inputRef: React.RefObject, text: string, const ConnectedPlatformInput = function ConnectedPlatformInput() { const route = useRoute | RootRouteProps<'chatRoot'>>() - const infoPanelShowing = route.name === 'chatRoot' ? !!route.params.infoPanel : false + const infoPanelShowing = + route.name === 'chatRoot' && 'infoPanel' in route.params ? !!route.params.infoPanel : false const data = Chat.useChatContext( C.useShallow(s => { const {meta, id: conversationIDKey, editing: editOrdinal, messageMap, unsentText} = s diff --git a/shared/chat/conversation/messages/text/coinflip/errors.tsx b/shared/chat/conversation/messages/text/coinflip/errors.tsx index 9a57f46b51ac..34be65bc968c 100644 --- a/shared/chat/conversation/messages/text/coinflip/errors.tsx +++ b/shared/chat/conversation/messages/text/coinflip/errors.tsx @@ -30,7 +30,7 @@ const CoinFlipError = (props: Props) => { const CoinFlipGenericError = () => { const navigateAppend = C.Router2.navigateAppend const sendFeedback = () => { - navigateAppend('modalFeedback') + navigateAppend({name: 'modalFeedback', params: {}}) } return ( diff --git a/shared/common-adapters/reload.tsx b/shared/common-adapters/reload.tsx index d9c809e107da..c3731a5acb07 100644 --- a/shared/common-adapters/reload.tsx +++ b/shared/common-adapters/reload.tsx @@ -183,9 +183,9 @@ const ReloadContainer = (ownProps: OwnProps) => { const _onFeedback = (loggedIn: boolean) => { if (loggedIn) { navigateAppend(C.Tabs.settingsTab) - navigateAppend(settingsFeedbackTab) + navigateAppend({name: settingsFeedbackTab, params: {}}) } else { - navigateAppend('feedback') + navigateAppend({name: 'feedback', params: {}}) } } diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index a2c2f690b7da..01901842287d 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -99,9 +99,13 @@ type RouteDefMatchesScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen ? Omit & { - __routeParams?: ScreenRouteParams + __routeParams?: R extends {__routeParams?: infer Params} ? Params : ScreenRouteParams getOptions?: GetOptions - initialParams?: ScreenRouteParams extends undefined ? undefined : ScreenRouteParams + initialParams?: (R extends {__routeParams?: infer Params} ? Params : ScreenRouteParams) extends undefined + ? undefined + : R extends {__routeParams?: infer Params} + ? Params + : ScreenRouteParams screen: Screen } : never diff --git a/shared/devices/nav-header.tsx b/shared/devices/nav-header.tsx index ac0b795cac62..6c6e415ee807 100644 --- a/shared/devices/nav-header.tsx +++ b/shared/devices/nav-header.tsx @@ -14,7 +14,7 @@ export const HeaderTitle = ({activeCount, revokedCount}: {activeCount: number; r export const HeaderRightActions = () => { const navigateAppend = C.Router2.navigateAppend - const onAdd = () => navigateAppend('deviceAdd') + const onAdd = () => navigateAppend({name: 'deviceAdd', params: {}}) return ( { const cancel = useProvisionState(s => s.dispatch.dynamic.cancel) @@ -22,7 +23,7 @@ const AddDeviceCancelButton = () => { ) } -export const newRoutes = { +export const newRoutes = defineRouteMap({ devicePage: C.makeScreen( React.lazy(async () => import('./device-page')), {getOptions: {title: ''}} @@ -46,9 +47,9 @@ export const newRoutes = { }, screen: React.lazy(async () => import('.')), }, -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ ...provisionNewRoutes, deviceAdd: C.makeScreen(React.lazy(async () => import('./add-device')), { getOptions: { @@ -61,4 +62,4 @@ export const newModalRoutes = { getOptions: {gestureEnabled: false, overlayNoClose: true}, screen: React.lazy(async () => import('./paper-key')), }, -} +}) diff --git a/shared/login/loading.tsx b/shared/login/loading.tsx index c2fa815d8d5c..26dc83ad97cf 100644 --- a/shared/login/loading.tsx +++ b/shared/login/loading.tsx @@ -25,7 +25,7 @@ const SplashContainer = () => { const onFeedback = C.isMobile ? () => { - navigateAppend('feedback') + navigateAppend({name: 'feedback', params: {}}) } : undefined const onRetry = retriesLeft === 0 ? startHandshake : undefined diff --git a/shared/login/routes.tsx b/shared/login/routes.tsx index fad6ede4f86c..4c7a0224d832 100644 --- a/shared/login/routes.tsx +++ b/shared/login/routes.tsx @@ -6,6 +6,7 @@ import {newRoutes as provisionRoutes} from '../provision/routes-sub' import {sharedNewRoutes as settingsRoutes} from '../settings/routes' import {newRoutes as signupRoutes} from './signup/routes' import {settingsFeedbackTab} from '@/constants/settings' +import {defineRouteMap} from '@/constants/types/router' const recoverPasswordStyles = Kb.Styles.styleSheetCreate(() => ({ questionBox: Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.tiny, 0), @@ -23,7 +24,7 @@ const recoverPasswordGetOptions = { title: 'Recover password', } -export const newRoutes = { +export const newRoutes = defineRouteMap({ feedback: settingsRoutes[settingsFeedbackTab], login: {getOptions: {headerShown: false}, screen: React.lazy(async () => import('.'))}, recoverPasswordDeviceSelector: { @@ -71,8 +72,8 @@ export const newRoutes = { }), ...provisionRoutes, ...signupRoutes, -} -export const newModalRoutes = { +}) +export const newModalRoutes = defineRouteMap({ proxySettingsModal: { getOptions: {title: 'Proxy settings'}, screen: React.lazy(async () => import('../settings/proxy')), @@ -85,4 +86,4 @@ export const newModalRoutes = { getOptions: {gestureEnabled: false, title: 'Set password'}, screen: React.lazy(async () => import('./recover-password/password')), }, -} +}) diff --git a/shared/profile/routes.tsx b/shared/profile/routes.tsx index 2758427e7687..afd009898cb7 100644 --- a/shared/profile/routes.tsx +++ b/shared/profile/routes.tsx @@ -5,6 +5,7 @@ import * as Teams from '@/stores/teams' import {HeaderLeftButton} from '@/common-adapters/header-buttons' import {ModalTitle} from '@/teams/common' import {useModalHeaderState} from '@/stores/modal-header' +import {defineRouteMap} from '@/constants/types/router' const Title = React.lazy(async () => import('./search')) @@ -48,7 +49,7 @@ const EditAvatarHeaderTitle = ({teamID, wizard}: {teamID?: string; wizard?: bool return Upload an avatar } -export const newRoutes = { +export const newRoutes = defineRouteMap({ profile: C.makeScreen( React.lazy(async () => import('./user')), { @@ -64,9 +65,9 @@ export const newRoutes = { }, } ), -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ profileAddToTeam: C.makeScreen( React.lazy(async () => import('./add-to-team')), { @@ -107,4 +108,4 @@ export const newModalRoutes = { profileShowcaseTeamOffer: C.makeScreen(React.lazy(async () => import('./showcase-team-offer')), { getOptions: {modalStyle: {maxHeight: 600, maxWidth: 600}, title: 'Feature your teams'}, }), -} +}) diff --git a/shared/profile/user/hooks.tsx b/shared/profile/user/hooks.tsx index 997128b71c98..2d124adf0d61 100644 --- a/shared/profile/user/hooks.tsx +++ b/shared/profile/user/hooks.tsx @@ -147,7 +147,7 @@ const useUserData = (username: string) => { } const {navigateAppend, navigateUp} = C.Router2 const onAddIdentity = () => { - navigateAppend('profileProofsList') + navigateAppend({name: 'profileProofsList', params: {}}) } const onBack = () => { navigateUp() diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index 023f1fd304bc..4667ef91ef01 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -122,13 +122,30 @@ type AnyScreen = React.ComponentType type RouteDefForScreen = R extends {screen: infer Screen} ? Screen extends AnyScreen - ? Omit & { + ? Omit & { + __routeParams?: R extends {__routeParams?: infer Params} + ? Params + : React.ComponentProps extends {route: {params: infer Params}} + ? Params + : React.ComponentProps extends {route: {params?: infer Params}} + ? Params + : undefined getOptions?: GetOptions - initialParams?: React.ComponentProps extends {route: {params: infer Params}} + initialParams?: (R extends {__routeParams?: infer Params} ? Params - : React.ComponentProps extends {route: {params?: infer Params}} + : React.ComponentProps extends {route: {params: infer Params}} + ? Params + : React.ComponentProps extends {route: {params?: infer Params}} + ? Params + : undefined) extends undefined + ? undefined + : R extends {__routeParams?: infer Params} ? Params - : undefined + : React.ComponentProps extends {route: {params: infer Params}} + ? Params + : React.ComponentProps extends {route: {params?: infer Params}} + ? Params + : undefined screen: Screen } : never diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 46e6ac2b7cd3..26ee55c52b1f 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -190,7 +190,7 @@ const sharedNewModalRoutes = { const WebLinks = React.lazy(async () => import('./web-links')) -export const newRoutes = { +export const newRoutes = defineRouteMap({ settingsRoot: C.isMobile ? C.isPhone ? {getOptions: {title: 'More'}, screen: React.lazy(async () => import('./root-phone'))} @@ -207,9 +207,9 @@ export const newRoutes = { title: route.params.title, }), }), -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ ...sharedNewModalRoutes, checkPassphraseBeforeDeleteAccount: C.makeScreen( React.lazy(async () => import('./delete-confirm/check-passphrase')), @@ -231,4 +231,4 @@ export const newModalRoutes = { }, }) : {screen: () => <>}, -} +}) From 611ce24f99bbe7b4fcd095b141ff2db1ad34526e Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 15:43:10 -0400 Subject: [PATCH 34/45] WIP --- shared/constants/router.tsx | 16 +++++++++---- shared/people/routes.tsx | 9 ++++---- shared/router-v2/routes.tsx | 46 ++++--------------------------------- shared/stores/chat.tsx | 16 +++++++++---- 4 files changed, 32 insertions(+), 55 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 219b76cca951..ca77716c1343 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -20,16 +20,22 @@ import {registerDebugClear} from '@/util/debug' import {makeUUID} from '@/util/uuid' type InferComponentProps = - T extends React.LazyExoticComponent< - React.ComponentType | undefined> - > + T extends React.LazyExoticComponent> ? P - : T extends React.ComponentType | undefined> + : T extends React.ComponentType ? P : undefined +type IsExactlyRecord = [T] extends [Record] + ? [Record] extends [T] + ? true + : false + : false + type NavigatorParamsFromProps

= P extends Record - ? keyof P extends never + ? IsExactlyRecord

extends true + ? undefined + : keyof P extends never ? undefined : P : undefined diff --git a/shared/people/routes.tsx b/shared/people/routes.tsx index ae522940fd4c..21a986557ff1 100644 --- a/shared/people/routes.tsx +++ b/shared/people/routes.tsx @@ -5,6 +5,7 @@ import peopleTeamBuilder from '../team-building/page' import ProfileSearch from '../profile/search' import {useCurrentUserState} from '@/stores/current-user' import {settingsLogOutTab} from '@/constants/settings' +import {defineRouteMap} from '@/constants/types/router' const HeaderAvatar = () => { const myUsername = useCurrentUserState(s => s.username) @@ -13,7 +14,7 @@ const HeaderAvatar = () => { return } -export const newRoutes = { +export const newRoutes = defineRouteMap({ peopleRoot: { getOptions: { headerRight: Kb.Styles.isMobile ? () => : undefined, @@ -21,7 +22,7 @@ export const newRoutes = { }, screen: React.lazy(async () => import('./container')), }, -} +}) const AccountSignOutButton = () => { const navigateAppend = C.Router2.navigateAppend @@ -36,10 +37,10 @@ const AccountSignOutButton = () => { ) } -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ accountSwitcher: { getOptions: {headerRight: () => }, screen: React.lazy(async () => import('../router-v2/account-switcher')), }, peopleTeamBuilder, -} +}) diff --git a/shared/router-v2/routes.tsx b/shared/router-v2/routes.tsx index 4667ef91ef01..d2b85612aa81 100644 --- a/shared/router-v2/routes.tsx +++ b/shared/router-v2/routes.tsx @@ -118,51 +118,15 @@ type MakeLayoutFn = ( getOptions?: GetOptions ) => LayoutFn type MakeOptionsFn = (rd: RouteDef) => (params: GetOptionsParams) => GetOptionsRet -type AnyScreen = React.ComponentType -type RouteDefForScreen = - R extends {screen: infer Screen} - ? Screen extends AnyScreen - ? Omit & { - __routeParams?: R extends {__routeParams?: infer Params} - ? Params - : React.ComponentProps extends {route: {params: infer Params}} - ? Params - : React.ComponentProps extends {route: {params?: infer Params}} - ? Params - : undefined - getOptions?: GetOptions - initialParams?: (R extends {__routeParams?: infer Params} - ? Params - : React.ComponentProps extends {route: {params: infer Params}} - ? Params - : React.ComponentProps extends {route: {params?: infer Params}} - ? Params - : undefined) extends undefined - ? undefined - : R extends {__routeParams?: infer Params} - ? Params - : React.ComponentProps extends {route: {params: infer Params}} - ? Params - : React.ComponentProps extends {route: {params?: infer Params}} - ? Params - : undefined - screen: Screen - } - : never - : never -type CheckedRouteMap> = Routes & { - [K in keyof Routes]: RouteDefForScreen -} -type CheckedRouteEntry> = - CheckedRouteMap[keyof Routes] +type CheckedRouteEntry> = Routes[keyof Routes] function toNavOptions(opts: GetOptionsRet): NativeStackNavigationOptions { if (!opts) return {} return opts as NativeStackNavigationOptions } -export function routeMapToStaticScreens>( - rs: CheckedRouteMap, +export function routeMapToStaticScreens>( + rs: RS, makeLayoutFn: MakeLayoutFn, isModal: boolean, isLoggedOut: boolean, @@ -194,8 +158,8 @@ export function routeMapToStaticScreens>( - rs: CheckedRouteMap, +export function routeMapToScreenElements>( + rs: RS, Screen: React.ComponentType, makeLayoutFn: MakeLayoutFn, makeOptionsFn: MakeOptionsFn, diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index cc7142fa6ce1..11ccf5abc40f 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2015,16 +2015,22 @@ export const useChatState = Z.createZustand('chat', (set, get) => { }) type InferComponentProps = - T extends React.LazyExoticComponent< - React.ComponentType | undefined> - > + T extends React.LazyExoticComponent> ? P - : T extends React.ComponentType | undefined> + : T extends React.ComponentType ? P : undefined +type IsExactlyRecord = [T] extends [Record] + ? [Record] extends [T] + ? true + : false + : false + type NavigatorParamsFromProps

= P extends Record - ? keyof P extends never + ? IsExactlyRecord

extends true + ? undefined + : keyof P extends never ? undefined : P : undefined From 81a77dfdf550421b3202e2e9eb1d6b86a9e6a56f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 15:51:11 -0400 Subject: [PATCH 35/45] WIP --- shared/constants/router.tsx | 8 +++++--- shared/router-v2/route-params.tsx | 10 ++++------ shared/stores/convostate.tsx | 7 +++++-- shared/util/safe-navigation.tsx | 7 +++++-- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index ca77716c1343..b8ed8e29713b 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -11,7 +11,7 @@ import { type NavigationState, } from '@react-navigation/core' import type {StaticScreenProps} from '@react-navigation/core' -import type {NavigateAppendType, RouteKeys, RootParamList as KBRootParamList} from '@/router-v2/route-params' +import type {NavigateAppendArg, RouteKeys, RootParamList as KBRootParamList} from '@/router-v2/route-params' import type {GetOptionsRet, RouteDef} from './types/router' import {isSplit} from './chat/layout' import {isMobile} from './platform' @@ -56,7 +56,6 @@ registerDebugClear(() => { export type Route = NavigationState['routes'][0] // still a little paranoid about some things being missing in this type export type NavState = Partial -export type PathParam = NavigateAppendType export type Navigator = NavigationContainerRef const DEBUG_NAV = __DEV__ && (false as boolean) @@ -248,7 +247,10 @@ export const navUpToScreen = (name: RouteKeys) => { n.dispatch(StackActions.popTo(typeof name === 'string' ? name : String(name))) } -export const navigateAppend = (path: PathParam, replace?: boolean) => { +export const navigateAppend = ( + path: NavigateAppendArg, + replace?: boolean +) => { DEBUG_NAV && console.log('[Nav] navigateAppend', {path}) const n = _getNavigator() if (!n) { diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index d450e580722c..2a6590f076a5 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -85,12 +85,10 @@ type KeybaseRootParamList = _ExtractParams<_AllScreens> & export type RootParamList = KeybaseRootParamList export type RouteKeys = keyof RootParamList -type Distribute = U extends RouteKeys - ? RootParamList[U] extends undefined - ? U - : {name: U; params: RootParamList[U]} - : never -export type NavigateAppendType = Distribute +export type NavigateAppendArg = RootParamList[RouteName] extends undefined + ? RouteName + : {name: RouteName; params: RootParamList[RouteName]} +export type NavigateAppendType = NavigateAppendArg export type RootRouteProps = RouteProp export type RouteProps2 = { diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 88a712ed8bfd..9fc721a52d2b 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -3632,10 +3632,13 @@ export function ProviderScreen(p: {children: React.ReactNode; rp: RouteParams; c ) } -import type {NavigateAppendType} from '@/router-v2/route-params' +import type {NavigateAppendArg, RouteKeys} from '@/router-v2/route-params' export const useChatNavigateAppend = () => { const cid = useChatContext(s => s.id) - return (makePath: (cid: T.Chat.ConversationIDKey) => NavigateAppendType, replace?: boolean) => { + return ( + makePath: (cid: T.Chat.ConversationIDKey) => NavigateAppendArg, + replace?: boolean + ) => { navigateAppend(makePath(cid), replace) } } diff --git a/shared/util/safe-navigation.tsx b/shared/util/safe-navigation.tsx index 4fa865772be2..19848896cc7e 100644 --- a/shared/util/safe-navigation.tsx +++ b/shared/util/safe-navigation.tsx @@ -1,13 +1,16 @@ import * as C from '@/constants' import {useIsFocused} from '@react-navigation/core' -import type {NavigateAppendType} from '@/router-v2/route-params' +import type {NavigateAppendArg, RouteKeys} from '@/router-v2/route-params' export const useSafeNavigation = () => { const isFocused = useIsFocused() const navigateUp = C.Router2.navigateUp const navigateAppend = C.Router2.navigateAppend return { - safeNavigateAppend: (path: NavigateAppendType, replace?: boolean) => + safeNavigateAppend: ( + path: NavigateAppendArg, + replace?: boolean + ) => isFocused && navigateAppend(path, replace), safeNavigateUp: () => isFocused && navigateUp(), } From af5baafd9f05072591d8bfa3a00ff88a912b0798 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 15:56:55 -0400 Subject: [PATCH 36/45] WIP --- shared/constants/router.tsx | 31 ++++++++++------------------ shared/stores/chat.tsx | 41 +++++++++++++++---------------------- 2 files changed, 28 insertions(+), 44 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index b8ed8e29713b..c0d37258c0d0 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -19,13 +19,6 @@ import {shallowEqual} from './utils' import {registerDebugClear} from '@/util/debug' import {makeUUID} from '@/util/uuid' -type InferComponentProps = - T extends React.LazyExoticComponent> - ? P - : T extends React.ComponentType - ? P - : undefined - type IsExactlyRecord = [T] extends [Record] ? [Record] extends [T] ? true @@ -37,14 +30,12 @@ type NavigatorParamsFromProps

= P extends Record ? undefined : keyof P extends never ? undefined - : P + : P : undefined -type ScreenParams> = NavigatorParamsFromProps< - InferComponentProps -> -type ScreenComponent> = ( - p: StaticScreenProps> +type ScreenParams

= NavigatorParamsFromProps

+type ScreenComponent

= ( + p: StaticScreenProps> ) => React.ReactElement export const navigationRef = createNavigationContainerRef() @@ -177,27 +168,27 @@ export const useSafeFocusEffect = (fn: () => void) => { // Helper to reduce boilerplate in route definitions // Works for components with or without route params -export function makeScreen>( - Component: COM, +export function makeScreen>( + Component: React.LazyExoticComponent, options?: { - getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) + getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -): RouteDef, ScreenParams> { +): RouteDef, ScreenParams

> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' - ? (p: StaticScreenProps>) => + ? (p: StaticScreenProps>) => getOptionsOption({ ...p, route: { ...p.route, - params: (p.route.params ?? {}) as ScreenParams, + params: (p.route.params ?? {}) as ScreenParams

, }, }) : getOptionsOption return { ...options, getOptions, - screen: function Screen(p: StaticScreenProps>) { + screen: function Screen(p: StaticScreenProps>) { const Comp = Component as any return }, diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 11ccf5abc40f..f2c51308c238 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2014,13 +2014,6 @@ export const useChatState = Z.createZustand('chat', (set, get) => { } }) -type InferComponentProps = - T extends React.LazyExoticComponent> - ? P - : T extends React.ComponentType - ? P - : undefined - type IsExactlyRecord = [T] extends [Record] ? [Record] extends [T] ? true @@ -2032,50 +2025,50 @@ type NavigatorParamsFromProps

= P extends Record ? undefined : keyof P extends never ? undefined - : P + : P : undefined -type AddConversationIDKey

| undefined> = P extends undefined - ? {conversationIDKey?: T.Chat.ConversationIDKey} - : Omit & {conversationIDKey?: T.Chat.ConversationIDKey} +type AddConversationIDKey

= P extends Record + ? Omit & {conversationIDKey?: T.Chat.ConversationIDKey} + : {conversationIDKey?: T.Chat.ConversationIDKey} -type ChatScreenParams> = NavigatorParamsFromProps< - AddConversationIDKey> +type ChatScreenParams

= NavigatorParamsFromProps< + AddConversationIDKey

> -type ChatScreenProps> = StaticScreenProps> -type ChatScreenComponent> = ( - p: ChatScreenProps +type ChatScreenProps

= StaticScreenProps> +type ChatScreenComponent

= ( + p: ChatScreenProps

) => React.ReactElement -export function makeChatScreen>( - Component: COM, +export function makeChatScreen>( + Component: React.LazyExoticComponent, options?: { getOptions?: | GetOptionsRet - | ((props: ChatScreenProps) => GetOptionsRet) + | ((props: ChatScreenProps

) => GetOptionsRet) skipProvider?: boolean canBeNullConvoID?: boolean } -): RouteDef, ChatScreenParams> { +): RouteDef, ChatScreenParams

> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' - ? (p: ChatScreenProps) => + ? (p: ChatScreenProps

) => // getOptions can run before params are materialized on the route object. getOptionsOption({ ...p, route: { ...p.route, - params: (((p.route as {params?: ChatScreenParams}).params ?? {}) as ChatScreenParams), + params: (((p.route as {params?: ChatScreenParams

}).params ?? {}) as ChatScreenParams

), }, }) : getOptionsOption return { ...options, getOptions, - screen: function Screen(p: ChatScreenProps) { + screen: function Screen(p: ChatScreenProps

) { const Comp = Component as any - const params = (((p.route as {params?: ChatScreenParams}).params ?? {}) as ChatScreenParams) + const params = (((p.route as {params?: ChatScreenParams

}).params ?? {}) as ChatScreenParams

) return options?.skipProvider ? ( ) : ( From bbb7bd0e6c979979233f2a590a3c705e361e96ff Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:00:27 -0400 Subject: [PATCH 37/45] WIP --- shared/chat/routes.tsx | 9 +++++---- shared/teams/routes.tsx | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index 001e0e5ded59..842aaa9ee9c6 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -11,6 +11,7 @@ import {useModalHeaderState} from '@/stores/modal-header' import {ModalTitle} from '@/teams/common' import inboxGetOptions from './inbox/get-options' import inboxAndConvoGetOptions from './inbox-and-conversation-get-options' +import {defineRouteMap} from '@/constants/types/router' const Convo = React.lazy(async () => import('./conversation/container')) const PDFShareButton = ({url}: {url?: string}) => { @@ -86,7 +87,7 @@ const SendToChatHeaderLeft = ({canBack}: {canBack?: boolean}) => { return Cancel } -export const newRoutes = { +export const newRoutes = defineRouteMap({ chatConversation: Chat.makeChatScreen(Convo, { canBeNullConvoID: true, getOptions: p => ({ @@ -112,9 +113,9 @@ export const newRoutes = { }), initialParams: {}, }, -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ chatAddToChannel: Chat.makeChatScreen( React.lazy(async () => import('./conversation/info-panel/add-to-channel')), { @@ -219,4 +220,4 @@ export const newModalRoutes = { React.lazy(async () => import('./conversation/messages/text/unfurl/unfurl-list/map-popup')), {getOptions: {title: 'Location'}} ), -} +}) diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 319f6e1d8df2..b81622e91f36 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -10,6 +10,7 @@ import contactRestricted from '../team-building/contact-restricted.page' import teamsTeamBuilder from '../team-building/page' import {useModalHeaderState} from '@/stores/modal-header' import teamsRootGetOptions from './get-options' +import {defineRouteMap} from '@/constants/types/router' const AddToChannelsHeaderTitle = ({teamID}: {teamID: T.Teams.TeamID}) => { const title = useModalHeaderState(s => s.title) @@ -185,7 +186,7 @@ const NewTeamInfoHeaderLeft = () => { return } -export const newRoutes = { +export const newRoutes = defineRouteMap({ team: C.makeScreen( React.lazy(async () => import('./team')), {getOptions: {headerShadowVisible: false, headerTitle: ''}} @@ -215,9 +216,9 @@ export const newRoutes = { }), initialParams: {}, }, -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ contactRestricted, openTeamWarning: C.makeScreen(React.lazy(async () => import('./team/settings-tab/open-team-warning'))), retentionWarning: C.makeScreen(React.lazy(async () => import('./team/settings-tab/retention/warning'))), @@ -345,4 +346,4 @@ export const newModalRoutes = { }, }), teamsTeamBuilder, -} +}) From 08f2b0e0fd2d3a7574f085d6b373f19b2473f810 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:05:45 -0400 Subject: [PATCH 38/45] WIP --- shared/crypto/routes.tsx | 9 +++++---- shared/fs/routes.tsx | 9 +++++---- shared/git/routes.tsx | 9 +++++---- shared/incoming-share/routes.tsx | 5 +++-- shared/login/signup/routes.tsx | 5 +++-- shared/signup/routes.tsx | 9 +++++---- shared/wallets/routes.tsx | 9 +++++---- 7 files changed, 31 insertions(+), 24 deletions(-) diff --git a/shared/crypto/routes.tsx b/shared/crypto/routes.tsx index ee7517e072e2..782e2402ddab 100644 --- a/shared/crypto/routes.tsx +++ b/shared/crypto/routes.tsx @@ -4,6 +4,7 @@ import * as Crypto from '@/constants/crypto' import {HeaderLeftButton, type HeaderBackButtonProps} from '@/common-adapters/header-buttons' import cryptoTeamBuilder from '../team-building/page' import type {StaticScreenProps} from '@react-navigation/core' +import {defineRouteMap} from '@/constants/types/router' import type { CommonOutputRouteParams, CryptoInputRouteParams, @@ -82,7 +83,7 @@ const CryptoTeamBuilderScreen = React.lazy(async () => { } }) -export const newRoutes = { +export const newRoutes = defineRouteMap({ [Crypto.decryptTab]: { getOptions: {headerShown: true, title: 'Decrypt'}, screen: DecryptInputScreen, @@ -103,9 +104,9 @@ export const newRoutes = { getOptions: C.isMobile ? {title: 'Crypto'} : {title: 'Crypto tools'}, screen: React.lazy(async () => import('./sub-nav')), }, -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ [Crypto.decryptOutput]: { getOptions: { headerLeft: (p: HeaderBackButtonProps) => , @@ -138,4 +139,4 @@ export const newModalRoutes = { ...cryptoTeamBuilder, screen: CryptoTeamBuilderScreen, }, -} +}) diff --git a/shared/fs/routes.tsx b/shared/fs/routes.tsx index ce6d818c5767..710214aa8cfa 100644 --- a/shared/fs/routes.tsx +++ b/shared/fs/routes.tsx @@ -6,6 +6,7 @@ import * as FS from '@/stores/fs' import {Actions, MainBanner, MobileHeader, Title} from './nav-header' import {Filename, ItemIcon} from './common' import {OriginalOrCompressedButton} from '@/incoming-share' +import {defineRouteMap} from '@/constants/types/router' const FsRoot = React.lazy(async () => import('.')) @@ -66,7 +67,7 @@ const destPickerDesktopHeaderStyle = Kb.Styles.padding( ) const noShrinkStyle = {flexShrink: 0} as const -export const newRoutes = { +export const newRoutes = defineRouteMap({ fsRoot: C.makeScreen(FsRoot, { getOptions: (ownProps?) => { // strange edge case where the root can actually have no params @@ -84,9 +85,9 @@ export const newRoutes = { } }, }), -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ barePreview: C.makeScreen( React.lazy(async () => { const {BarePreview} = await import('./filepreview') @@ -114,4 +115,4 @@ export const newModalRoutes = { async () => import('./banner/system-file-manager-integration-banner/kext-permission-popup') ), }, -} +}) diff --git a/shared/git/routes.tsx b/shared/git/routes.tsx index e208eca3a922..f2f655a2d272 100644 --- a/shared/git/routes.tsx +++ b/shared/git/routes.tsx @@ -1,8 +1,9 @@ import * as React from 'react' import * as C from '@/constants' import type * as NavHeader from './nav-header' +import {defineRouteMap} from '@/constants/types/router' -export const newRoutes = { +export const newRoutes = defineRouteMap({ gitRoot: C.makeScreen( React.lazy(async () => import('.')), { @@ -18,9 +19,9 @@ export const newRoutes = { }, } ), -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ gitDeleteRepo: C.makeScreen(React.lazy(async () => import('./delete-repo')), { getOptions: {title: 'Delete repo?'}, }), @@ -28,4 +29,4 @@ export const newModalRoutes = { getOptions: {title: 'New repository'}, }), gitSelectChannel: C.makeScreen(React.lazy(async () => import('./select-channel'))), -} +}) diff --git a/shared/incoming-share/routes.tsx b/shared/incoming-share/routes.tsx index 3ff9d7a88ae3..f64791156230 100644 --- a/shared/incoming-share/routes.tsx +++ b/shared/incoming-share/routes.tsx @@ -4,6 +4,7 @@ import * as Kb from '@/common-adapters' import type * as T from '@/constants/types' import {useModalHeaderState} from '@/stores/modal-header' import {OriginalOrCompressedButton} from '.' +import {defineRouteMap} from '@/constants/types/router' const IncomingShareHeaderLeft = () => { const onAction = useModalHeaderState(s => s.onAction) @@ -35,7 +36,7 @@ const IncomingShareHeaderTitle = () => { ) } -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ incomingShareNew: C.makeScreen(React.lazy(async () => import('.')), { getOptions: { headerLeft: () => , @@ -43,4 +44,4 @@ export const newModalRoutes = { headerTitle: () => , }, }), -} +}) diff --git a/shared/login/signup/routes.tsx b/shared/login/signup/routes.tsx index 069b2ea04170..3643872c31ed 100644 --- a/shared/login/signup/routes.tsx +++ b/shared/login/signup/routes.tsx @@ -1,5 +1,6 @@ import * as React from 'react' +import {defineRouteMap} from '@/constants/types/router' -export const newRoutes = { +export const newRoutes = defineRouteMap({ signupError: {getOptions: {headerLeft: undefined, title: 'Error'}, screen: React.lazy(async () => import('./error'))}, -} +}) diff --git a/shared/signup/routes.tsx b/shared/signup/routes.tsx index 51556418c18c..c5cfe50fde68 100644 --- a/shared/signup/routes.tsx +++ b/shared/signup/routes.tsx @@ -4,6 +4,7 @@ import * as Kb from '@/common-adapters' import {InfoIcon} from './common' import {usePushState} from '@/stores/push' import {setSignupEmail} from '@/people/signup-email' +import {defineRouteMap} from '@/constants/types/router' const EmailSkipButton = () => { const showPushPrompt = usePushState(s => C.isMobile && !s.hasPermissions && s.showPushPrompt) @@ -36,7 +37,7 @@ const PhoneSkipButton = () => { ) } -export const newRoutes = { +export const newRoutes = defineRouteMap({ signupEnterDevicename: { getOptions: {title: 'Name this device'}, screen: React.lazy(async () => import('./device-name')), @@ -61,10 +62,10 @@ export const newRoutes = { getOptions: {title: 'Send feedback'}, screen: React.lazy(async () => import('./feedback')), }, -} +}) // Some screens in signup show up after we've actually signed up -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ signupEnterEmail: { getOptions: {headerLeft: () => null, headerRight: () => , title: 'Your email address'}, screen: React.lazy(async () => import('./email')), @@ -81,4 +82,4 @@ export const newModalRoutes = { getOptions: {title: 'Verify phone number'}, screen: React.lazy(async () => import('./phone-number/verify')), }, -} +}) diff --git a/shared/wallets/routes.tsx b/shared/wallets/routes.tsx index 2991aa0ac14a..ef9728f16487 100644 --- a/shared/wallets/routes.tsx +++ b/shared/wallets/routes.tsx @@ -1,14 +1,15 @@ import * as React from 'react' import * as C from '@/constants' +import {defineRouteMap} from '@/constants/types/router' -export const newRoutes = { +export const newRoutes = defineRouteMap({ walletsRoot: { getOptions: {title: 'Wallet'}, screen: React.lazy(async () => import('.')), }, -} +}) -export const newModalRoutes = { +export const newModalRoutes = defineRouteMap({ reallyRemoveAccount: C.makeScreen(React.lazy(async () => import('./really-remove-account'))), removeAccount: C.makeScreen(React.lazy(async () => import('./remove-account'))), -} +}) From 3cc5650a0fd8958e98cae9d33814eaab8263a0ef Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:07:27 -0400 Subject: [PATCH 39/45] WIP --- shared/constants/router.tsx | 18 +++++++++--------- shared/stores/chat.tsx | 24 ++++++++++++------------ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index c0d37258c0d0..014daa9d8549 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -33,9 +33,9 @@ type NavigatorParamsFromProps

= P extends Record : P : undefined -type ScreenParams

= NavigatorParamsFromProps

-type ScreenComponent

= ( - p: StaticScreenProps> +type ScreenParams> = NavigatorParamsFromProps> +type ScreenComponent> = ( + p: StaticScreenProps> ) => React.ReactElement export const navigationRef = createNavigationContainerRef() @@ -168,27 +168,27 @@ export const useSafeFocusEffect = (fn: () => void) => { // Helper to reduce boilerplate in route definitions // Works for components with or without route params -export function makeScreen>( +export function makeScreen>( Component: React.LazyExoticComponent, options?: { - getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) + getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } -): RouteDef, ScreenParams

> { +): RouteDef, ScreenParams> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' - ? (p: StaticScreenProps>) => + ? (p: StaticScreenProps>) => getOptionsOption({ ...p, route: { ...p.route, - params: (p.route.params ?? {}) as ScreenParams

, + params: (p.route.params ?? {}) as ScreenParams, }, }) : getOptionsOption return { ...options, getOptions, - screen: function Screen(p: StaticScreenProps>) { + screen: function Screen(p: StaticScreenProps>) { const Comp = Component as any return }, diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index f2c51308c238..a4c15b08b5d7 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2032,43 +2032,43 @@ type AddConversationIDKey

= P extends Record ? Omit & {conversationIDKey?: T.Chat.ConversationIDKey} : {conversationIDKey?: T.Chat.ConversationIDKey} -type ChatScreenParams

= NavigatorParamsFromProps< - AddConversationIDKey

+type ChatScreenParams> = NavigatorParamsFromProps< + AddConversationIDKey> > -type ChatScreenProps

= StaticScreenProps> -type ChatScreenComponent

= ( - p: ChatScreenProps

+type ChatScreenProps> = StaticScreenProps> +type ChatScreenComponent> = ( + p: ChatScreenProps ) => React.ReactElement -export function makeChatScreen>( +export function makeChatScreen>( Component: React.LazyExoticComponent, options?: { getOptions?: | GetOptionsRet - | ((props: ChatScreenProps

) => GetOptionsRet) + | ((props: ChatScreenProps) => GetOptionsRet) skipProvider?: boolean canBeNullConvoID?: boolean } -): RouteDef, ChatScreenParams

> { +): RouteDef, ChatScreenParams> { const getOptionsOption = options?.getOptions const getOptions = typeof getOptionsOption === 'function' - ? (p: ChatScreenProps

) => + ? (p: ChatScreenProps) => // getOptions can run before params are materialized on the route object. getOptionsOption({ ...p, route: { ...p.route, - params: (((p.route as {params?: ChatScreenParams

}).params ?? {}) as ChatScreenParams

), + params: (((p.route as {params?: ChatScreenParams}).params ?? {}) as ChatScreenParams), }, }) : getOptionsOption return { ...options, getOptions, - screen: function Screen(p: ChatScreenProps

) { + screen: function Screen(p: ChatScreenProps) { const Comp = Component as any - const params = (((p.route as {params?: ChatScreenParams

}).params ?? {}) as ChatScreenParams

) + const params = (((p.route as {params?: ChatScreenParams}).params ?? {}) as ChatScreenParams) return options?.skipProvider ? ( ) : ( From cf77bc09e8c91dc96b099ef6622346e2a482f515 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:09:22 -0400 Subject: [PATCH 40/45] WIP --- shared/constants/router.tsx | 15 ++++++++++----- shared/stores/chat.tsx | 17 ++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 014daa9d8549..6cb1fc8bcd7e 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -29,12 +29,17 @@ type NavigatorParamsFromProps

= P extends Record ? IsExactlyRecord

extends true ? undefined : keyof P extends never - ? undefined + ? undefined : P : undefined -type ScreenParams> = NavigatorParamsFromProps> -type ScreenComponent> = ( +type LazyInnerComponent> = + COM extends React.LazyExoticComponent ? Inner : never + +type ScreenParams> = NavigatorParamsFromProps< + React.ComponentProps> +> +type ScreenComponent> = ( p: StaticScreenProps> ) => React.ReactElement @@ -168,8 +173,8 @@ export const useSafeFocusEffect = (fn: () => void) => { // Helper to reduce boilerplate in route definitions // Works for components with or without route params -export function makeScreen>( - Component: React.LazyExoticComponent, +export function makeScreen>( + Component: COM, options?: { getOptions?: GetOptionsRet | ((props: StaticScreenProps>) => GetOptionsRet) } diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index a4c15b08b5d7..3962a89f0bb9 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -2024,7 +2024,7 @@ type NavigatorParamsFromProps

= P extends Record ? IsExactlyRecord

extends true ? undefined : keyof P extends never - ? undefined + ? undefined : P : undefined @@ -2032,17 +2032,20 @@ type AddConversationIDKey

= P extends Record ? Omit & {conversationIDKey?: T.Chat.ConversationIDKey} : {conversationIDKey?: T.Chat.ConversationIDKey} -type ChatScreenParams> = NavigatorParamsFromProps< - AddConversationIDKey> +type LazyInnerComponent> = + COM extends React.LazyExoticComponent ? Inner : never + +type ChatScreenParams> = NavigatorParamsFromProps< + AddConversationIDKey>> > -type ChatScreenProps> = StaticScreenProps> -type ChatScreenComponent> = ( +type ChatScreenProps> = StaticScreenProps> +type ChatScreenComponent> = ( p: ChatScreenProps ) => React.ReactElement -export function makeChatScreen>( - Component: React.LazyExoticComponent, +export function makeChatScreen>( + Component: COM, options?: { getOptions?: | GetOptionsRet From 5053e7380f0069f1f2a6d3dd708e5f04074198a0 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:13:41 -0400 Subject: [PATCH 41/45] WIP --- shared/chat/routes.tsx | 97 +++++++++++++++++++++++++++---- shared/constants/router.tsx | 18 ++++-- shared/router-v2/route-params.tsx | 4 ++ shared/settings/routes.tsx | 10 +++- shared/stores/convostate.tsx | 21 +++++-- shared/teams/routes.tsx | 15 ++++- shared/util/safe-navigation.tsx | 19 ++++-- 7 files changed, 155 insertions(+), 29 deletions(-) diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index 842aaa9ee9c6..58a41521c22f 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -12,8 +12,87 @@ import {ModalTitle} from '@/teams/common' import inboxGetOptions from './inbox/get-options' import inboxAndConvoGetOptions from './inbox-and-conversation-get-options' import {defineRouteMap} from '@/constants/types/router' +import type {StaticScreenProps} from '@react-navigation/core' +import type {BlockModalContext} from './blocking/block-modal' const Convo = React.lazy(async () => import('./conversation/container')) +type ChatAddToChannelRouteParams = { + conversationIDKey?: T.Chat.ConversationIDKey + teamID: T.Teams.TeamID +} +type ChatBlockingRouteParams = { + blockUserByDefault?: boolean + filterUserByDefault?: boolean + flagUserByDefault?: boolean + reportsUserByDefault?: boolean + context?: BlockModalContext + conversationIDKey?: T.Chat.ConversationIDKey + others?: Array + team?: string + username?: string +} +type ChatConfirmRemoveBotRouteParams = { + botUsername: string + teamID?: T.Teams.TeamID + conversationIDKey?: T.Chat.ConversationIDKey +} +type ChatInstallBotRouteParams = { + botUsername: string + conversationIDKey?: T.Chat.ConversationIDKey + teamID?: T.Teams.TeamID +} +type ChatSearchBotsRouteParams = { + teamID?: T.Teams.TeamID + conversationIDKey?: T.Chat.ConversationIDKey +} +type ChatShowNewTeamDialogRouteParams = { + conversationIDKey?: T.Chat.ConversationIDKey +} + +const ChatAddToChannelScreen = React.lazy(async () => { + const {default: AddToChannel} = await import('./conversation/info-panel/add-to-channel') + return { + default: (p: StaticScreenProps) => , + } +}) + +const ChatBlockingModalScreen = React.lazy(async () => { + const {default: BlockModal} = await import('./blocking/block-modal') + return { + default: (p: StaticScreenProps) => , + } +}) + +const ChatConfirmRemoveBotScreen = React.lazy(async () => { + const {default: ConfirmRemoveBot} = await import('./conversation/bot/confirm') + return { + default: (p: StaticScreenProps) => ( + + ), + } +}) + +const ChatInstallBotScreen = React.lazy(async () => { + const {default: InstallBot} = await import('./conversation/bot/install') + return { + default: (p: StaticScreenProps) => , + } +}) + +const ChatSearchBotsScreen = React.lazy(async () => { + const {default: SearchBots} = await import('./conversation/bot/search') + return { + default: (p: StaticScreenProps) => , + } +}) + +const ChatShowNewTeamDialogScreen = React.lazy(async () => { + const {default: NewTeamDialog} = await import('./new-team-dialog-container') + return { + default: (_p: StaticScreenProps) => , + } +}) + const PDFShareButton = ({url}: {url?: string}) => { const showShareActionSheet = useConfigState(s => s.dispatch.defer.showShareActionSheet) return ( @@ -117,7 +196,7 @@ export const newRoutes = defineRouteMap({ export const newModalRoutes = defineRouteMap({ chatAddToChannel: Chat.makeChatScreen( - React.lazy(async () => import('./conversation/info-panel/add-to-channel')), + ChatAddToChannelScreen, { getOptions: ({route}) => ({ headerRight: () => , @@ -141,7 +220,7 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./conversation/attachment-get-titles')), {getOptions: {modalStyle: {height: 660, maxHeight: 660}}} ), - chatBlockingModal: Chat.makeChatScreen(React.lazy(async () => import('./blocking/block-modal')), { + chatBlockingModal: Chat.makeChatScreen(ChatBlockingModalScreen, { getOptions: {headerTitle: () => }, }), chatChooseEmoji: Chat.makeChatScreen(React.lazy(async () => import('./emoji-picker/container')), { @@ -151,10 +230,7 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./punycode-link-warning')), {skipProvider: true} ), - chatConfirmRemoveBot: Chat.makeChatScreen( - React.lazy(async () => import('./conversation/bot/confirm')), - {canBeNullConvoID: true} - ), + chatConfirmRemoveBot: Chat.makeChatScreen(ChatConfirmRemoveBotScreen, {canBeNullConvoID: true}), chatCreateChannel: Chat.makeChatScreen( React.lazy(async () => import('./create-channel')), {skipProvider: true} @@ -168,7 +244,7 @@ export const newModalRoutes = defineRouteMap({ {getOptions: C.isMobile ? undefined : {modalStyle: {height: '80%', width: '80%'}}} ), chatInstallBot: Chat.makeChatScreen( - React.lazy(async () => import('./conversation/bot/install')), + ChatInstallBotScreen, { getOptions: { headerLeft: () => , @@ -201,10 +277,7 @@ export const newModalRoutes = defineRouteMap({ overlayStyle: {alignSelf: 'stretch'}, }), }), - chatSearchBots: Chat.makeChatScreen( - React.lazy(async () => import('./conversation/bot/search')), - {canBeNullConvoID: true, getOptions: {title: 'Add a bot'}} - ), + chatSearchBots: Chat.makeChatScreen(ChatSearchBotsScreen, {canBeNullConvoID: true, getOptions: {title: 'Add a bot'}}), chatSendToChat: Chat.makeChatScreen( React.lazy(async () => import('./send-to-chat')), { @@ -215,7 +288,7 @@ export const newModalRoutes = defineRouteMap({ skipProvider: true, } ), - chatShowNewTeamDialog: Chat.makeChatScreen(React.lazy(async () => import('./new-team-dialog-container'))), + chatShowNewTeamDialog: Chat.makeChatScreen(ChatShowNewTeamDialogScreen), chatUnfurlMapPopup: Chat.makeChatScreen( React.lazy(async () => import('./conversation/messages/text/unfurl/unfurl-list/map-popup')), {getOptions: {title: 'Location'}} diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 6cb1fc8bcd7e..7e9c3935e11b 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -11,7 +11,12 @@ import { type NavigationState, } from '@react-navigation/core' import type {StaticScreenProps} from '@react-navigation/core' -import type {NavigateAppendArg, RouteKeys, RootParamList as KBRootParamList} from '@/router-v2/route-params' +import type { + NoParamRouteKeys, + ParamRouteKeys, + RouteKeys, + RootParamList as KBRootParamList, +} from '@/router-v2/route-params' import type {GetOptionsRet, RouteDef} from './types/router' import {isSplit} from './chat/layout' import {isMobile} from './platform' @@ -243,10 +248,15 @@ export const navUpToScreen = (name: RouteKeys) => { n.dispatch(StackActions.popTo(typeof name === 'string' ? name : String(name))) } -export const navigateAppend = ( - path: NavigateAppendArg, +export function navigateAppend(path: RouteName, replace?: boolean): void +export function navigateAppend( + path: {name: RouteName; params: KBRootParamList[RouteName]}, replace?: boolean -) => { +): void +export function navigateAppend( + path: RouteKeys | {name: RouteKeys; params: object | undefined}, + replace?: boolean +) { DEBUG_NAV && console.log('[Nav] navigateAppend', {path}) const n = _getNavigator() if (!n) { diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index 2a6590f076a5..8f31ca5153b3 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -85,6 +85,10 @@ type KeybaseRootParamList = _ExtractParams<_AllScreens> & export type RootParamList = KeybaseRootParamList export type RouteKeys = keyof RootParamList +export type NoParamRouteKeys = { + [K in RouteKeys]: RootParamList[K] extends undefined ? K : never +}[RouteKeys] +export type ParamRouteKeys = Exclude export type NavigateAppendArg = RootParamList[RouteName] extends undefined ? RouteName : {name: RouteName; params: RootParamList[RouteName]} diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 26ee55c52b1f..22416823c96d 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -9,6 +9,8 @@ import {defineRouteMap} from '@/constants/types/router' import {usePushState} from '@/stores/push' import {usePWState} from '@/stores/settings-password' import {e164ToDisplay} from '@/util/phone-numbers' +import type {StaticScreenProps} from '@react-navigation/core' +import type {Props as FeedbackRouteParams} from './feedback/container' const PushPromptSkipButton = () => { const rejectPermissions = usePushState(s => s.dispatch.rejectPermissions) @@ -71,9 +73,15 @@ const SettingsRootDesktop = React.lazy(async () => import('./root-desktop-tablet const EmptySettingsScreen = () => <> const ManageContactsScreen: React.ComponentType = C.isMobile ? React.lazy(async () => import('./manage-contacts')) : EmptySettingsScreen +const FeedbackScreen = React.lazy(async () => { + const {default: FeedbackContainer} = await import('./feedback/container') + return { + default: (p: StaticScreenProps) => , + } +}) const feedback = C.makeScreen( - React.lazy(async () => import('./feedback/container')), + FeedbackScreen, {getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}} ) diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 9fc721a52d2b..5c1a168f1d30 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -3632,13 +3632,24 @@ export function ProviderScreen(p: {children: React.ReactNode; rp: RouteParams; c ) } -import type {NavigateAppendArg, RouteKeys} from '@/router-v2/route-params' +import type {NoParamRouteKeys, ParamRouteKeys, RootParamList} from '@/router-v2/route-params' export const useChatNavigateAppend = () => { const cid = useChatContext(s => s.id) - return ( - makePath: (cid: T.Chat.ConversationIDKey) => NavigateAppendArg, + function chatNavigateAppend( + makePath: (cid: T.Chat.ConversationIDKey) => RouteName, replace?: boolean - ) => { - navigateAppend(makePath(cid), replace) + ): void + function chatNavigateAppend( + makePath: (cid: T.Chat.ConversationIDKey) => {name: RouteName; params: RootParamList[RouteName]}, + replace?: boolean + ): void + function chatNavigateAppend( + makePath: + | ((cid: T.Chat.ConversationIDKey) => NoParamRouteKeys) + | ((cid: T.Chat.ConversationIDKey) => {name: ParamRouteKeys; params: object | undefined}), + replace?: boolean + ) { + navigateAppend(makePath(cid) as never, replace) } + return chatNavigateAppend } diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index b81622e91f36..0962bbfc7b5d 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -11,6 +11,19 @@ import teamsTeamBuilder from '../team-building/page' import {useModalHeaderState} from '@/stores/modal-header' import teamsRootGetOptions from './get-options' import {defineRouteMap} from '@/constants/types/router' +import type {StaticScreenProps} from '@react-navigation/core' + +type TeamRouteParams = { + teamID: T.Teams.TeamID + initialTab?: T.Teams.TabKey +} + +const TeamScreen = React.lazy(async () => { + const {default: Team} = await import('./team') + return { + default: (p: StaticScreenProps) => , + } +}) const AddToChannelsHeaderTitle = ({teamID}: {teamID: T.Teams.TeamID}) => { const title = useModalHeaderState(s => s.title) @@ -188,7 +201,7 @@ const NewTeamInfoHeaderLeft = () => { export const newRoutes = defineRouteMap({ team: C.makeScreen( - React.lazy(async () => import('./team')), + TeamScreen, {getOptions: {headerShadowVisible: false, headerTitle: ''}} ), teamChannel: Chat.makeChatScreen( diff --git a/shared/util/safe-navigation.tsx b/shared/util/safe-navigation.tsx index 19848896cc7e..cdb510ebdddb 100644 --- a/shared/util/safe-navigation.tsx +++ b/shared/util/safe-navigation.tsx @@ -1,17 +1,24 @@ import * as C from '@/constants' import {useIsFocused} from '@react-navigation/core' -import type {NavigateAppendArg, RouteKeys} from '@/router-v2/route-params' +import type {NoParamRouteKeys, ParamRouteKeys, RootParamList} from '@/router-v2/route-params' export const useSafeNavigation = () => { const isFocused = useIsFocused() const navigateUp = C.Router2.navigateUp const navigateAppend = C.Router2.navigateAppend + function safeNavigateAppend(path: RouteName, replace?: boolean): void + function safeNavigateAppend( + path: {name: RouteName; params: RootParamList[RouteName]}, + replace?: boolean + ): void + function safeNavigateAppend( + path: NoParamRouteKeys | {name: ParamRouteKeys; params: object | undefined}, + replace?: boolean + ) { + return isFocused && navigateAppend(path as never, replace) + } return { - safeNavigateAppend: ( - path: NavigateAppendArg, - replace?: boolean - ) => - isFocused && navigateAppend(path, replace), + safeNavigateAppend, safeNavigateUp: () => isFocused && navigateUp(), } } From f698324ecb4f9f7485bd30ff087f9ef73b90d833 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:17:49 -0400 Subject: [PATCH 42/45] WIP --- shared/chat/routes.tsx | 78 +++++++++---------------------- shared/constants/types/router.tsx | 4 ++ shared/settings/routes.tsx | 16 ++----- shared/teams/routes.tsx | 16 ++----- 4 files changed, 34 insertions(+), 80 deletions(-) diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index 58a41521c22f..3f522810179a 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -11,8 +11,7 @@ import {useModalHeaderState} from '@/stores/modal-header' import {ModalTitle} from '@/teams/common' import inboxGetOptions from './inbox/get-options' import inboxAndConvoGetOptions from './inbox-and-conversation-get-options' -import {defineRouteMap} from '@/constants/types/router' -import type {StaticScreenProps} from '@react-navigation/core' +import {defineRouteMap, withRouteParams} from '@/constants/types/router' import type {BlockModalContext} from './blocking/block-modal' const Convo = React.lazy(async () => import('./conversation/container')) @@ -49,50 +48,6 @@ type ChatShowNewTeamDialogRouteParams = { conversationIDKey?: T.Chat.ConversationIDKey } -const ChatAddToChannelScreen = React.lazy(async () => { - const {default: AddToChannel} = await import('./conversation/info-panel/add-to-channel') - return { - default: (p: StaticScreenProps) => , - } -}) - -const ChatBlockingModalScreen = React.lazy(async () => { - const {default: BlockModal} = await import('./blocking/block-modal') - return { - default: (p: StaticScreenProps) => , - } -}) - -const ChatConfirmRemoveBotScreen = React.lazy(async () => { - const {default: ConfirmRemoveBot} = await import('./conversation/bot/confirm') - return { - default: (p: StaticScreenProps) => ( - - ), - } -}) - -const ChatInstallBotScreen = React.lazy(async () => { - const {default: InstallBot} = await import('./conversation/bot/install') - return { - default: (p: StaticScreenProps) => , - } -}) - -const ChatSearchBotsScreen = React.lazy(async () => { - const {default: SearchBots} = await import('./conversation/bot/search') - return { - default: (p: StaticScreenProps) => , - } -}) - -const ChatShowNewTeamDialogScreen = React.lazy(async () => { - const {default: NewTeamDialog} = await import('./new-team-dialog-container') - return { - default: (_p: StaticScreenProps) => , - } -}) - const PDFShareButton = ({url}: {url?: string}) => { const showShareActionSheet = useConfigState(s => s.dispatch.defer.showShareActionSheet) return ( @@ -195,15 +150,15 @@ export const newRoutes = defineRouteMap({ }) export const newModalRoutes = defineRouteMap({ - chatAddToChannel: Chat.makeChatScreen( - ChatAddToChannelScreen, + chatAddToChannel: withRouteParams(Chat.makeChatScreen( + React.lazy(async () => import('./conversation/info-panel/add-to-channel')), { getOptions: ({route}) => ({ headerRight: () => , headerTitle: () => , }), } - ), + )), chatAttachmentFullscreen: Chat.makeChatScreen( React.lazy(async () => import('./conversation/attachment-fullscreen/screen')), { @@ -220,9 +175,9 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./conversation/attachment-get-titles')), {getOptions: {modalStyle: {height: 660, maxHeight: 660}}} ), - chatBlockingModal: Chat.makeChatScreen(ChatBlockingModalScreen, { + chatBlockingModal: withRouteParams(Chat.makeChatScreen(React.lazy(async () => import('./blocking/block-modal')), { getOptions: {headerTitle: () => }, - }), + })), chatChooseEmoji: Chat.makeChatScreen(React.lazy(async () => import('./emoji-picker/container')), { getOptions: {headerShown: false}, }), @@ -230,7 +185,9 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./punycode-link-warning')), {skipProvider: true} ), - chatConfirmRemoveBot: Chat.makeChatScreen(ChatConfirmRemoveBotScreen, {canBeNullConvoID: true}), + chatConfirmRemoveBot: withRouteParams( + Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/confirm')), {canBeNullConvoID: true}) + ), chatCreateChannel: Chat.makeChatScreen( React.lazy(async () => import('./create-channel')), {skipProvider: true} @@ -243,8 +200,8 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./conversation/info-panel')), {getOptions: C.isMobile ? undefined : {modalStyle: {height: '80%', width: '80%'}}} ), - chatInstallBot: Chat.makeChatScreen( - ChatInstallBotScreen, + chatInstallBot: withRouteParams(Chat.makeChatScreen( + React.lazy(async () => import('./conversation/bot/install')), { getOptions: { headerLeft: () => , @@ -253,7 +210,7 @@ export const newModalRoutes = defineRouteMap({ }, skipProvider: true, } - ), + )), chatInstallBotPick: Chat.makeChatScreen( React.lazy(async () => import('./conversation/bot/team-picker')), {getOptions: {title: 'Add to team or chat'}, skipProvider: true} @@ -277,7 +234,12 @@ export const newModalRoutes = defineRouteMap({ overlayStyle: {alignSelf: 'stretch'}, }), }), - chatSearchBots: Chat.makeChatScreen(ChatSearchBotsScreen, {canBeNullConvoID: true, getOptions: {title: 'Add a bot'}}), + chatSearchBots: withRouteParams( + Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/search')), { + canBeNullConvoID: true, + getOptions: {title: 'Add a bot'}, + }) + ), chatSendToChat: Chat.makeChatScreen( React.lazy(async () => import('./send-to-chat')), { @@ -288,7 +250,9 @@ export const newModalRoutes = defineRouteMap({ skipProvider: true, } ), - chatShowNewTeamDialog: Chat.makeChatScreen(ChatShowNewTeamDialogScreen), + chatShowNewTeamDialog: withRouteParams( + Chat.makeChatScreen(React.lazy(async () => import('./new-team-dialog-container'))) + ), chatUnfurlMapPopup: Chat.makeChatScreen( React.lazy(async () => import('./conversation/messages/text/unfurl/unfurl-list/map-popup')), {getOptions: {title: 'Location'}} diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index 01901842287d..fd842bcef757 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -114,3 +114,7 @@ type RouteDefMatchesScreen = export const defineRouteMap = >( routes: Routes & {[K in keyof Routes]: RouteDefMatchesScreen} ) => routes + +export const withRouteParams = ( + route: RouteDef +): RouteDef => route as RouteDef diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 22416823c96d..015313c6afa0 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -5,11 +5,10 @@ import {newRoutes as devicesRoutes} from '../devices/routes' import {newRoutes as gitRoutes} from '../git/routes' import {newRoutes as walletsRoutes} from '../wallets/routes' import * as Settings from '@/constants/settings' -import {defineRouteMap} from '@/constants/types/router' +import {defineRouteMap, withRouteParams} from '@/constants/types/router' import {usePushState} from '@/stores/push' import {usePWState} from '@/stores/settings-password' import {e164ToDisplay} from '@/util/phone-numbers' -import type {StaticScreenProps} from '@react-navigation/core' import type {Props as FeedbackRouteParams} from './feedback/container' const PushPromptSkipButton = () => { @@ -73,16 +72,11 @@ const SettingsRootDesktop = React.lazy(async () => import('./root-desktop-tablet const EmptySettingsScreen = () => <> const ManageContactsScreen: React.ComponentType = C.isMobile ? React.lazy(async () => import('./manage-contacts')) : EmptySettingsScreen -const FeedbackScreen = React.lazy(async () => { - const {default: FeedbackContainer} = await import('./feedback/container') - return { - default: (p: StaticScreenProps) => , - } -}) -const feedback = C.makeScreen( - FeedbackScreen, - {getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}} +const feedback = withRouteParams( + C.makeScreen(React.lazy(async () => import('./feedback/container')), { + getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}, + }) ) export const sharedNewRoutes = defineRouteMap({ diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index 0962bbfc7b5d..bae6108edaa3 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -10,21 +10,13 @@ import contactRestricted from '../team-building/contact-restricted.page' import teamsTeamBuilder from '../team-building/page' import {useModalHeaderState} from '@/stores/modal-header' import teamsRootGetOptions from './get-options' -import {defineRouteMap} from '@/constants/types/router' -import type {StaticScreenProps} from '@react-navigation/core' +import {defineRouteMap, withRouteParams} from '@/constants/types/router' type TeamRouteParams = { teamID: T.Teams.TeamID initialTab?: T.Teams.TabKey } -const TeamScreen = React.lazy(async () => { - const {default: Team} = await import('./team') - return { - default: (p: StaticScreenProps) => , - } -}) - const AddToChannelsHeaderTitle = ({teamID}: {teamID: T.Teams.TeamID}) => { const title = useModalHeaderState(s => s.title) return @@ -200,10 +192,10 @@ const NewTeamInfoHeaderLeft = () => { } export const newRoutes = defineRouteMap({ - team: C.makeScreen( - TeamScreen, + team: withRouteParams(C.makeScreen( + React.lazy(async () => import('./team')), {getOptions: {headerShadowVisible: false, headerTitle: ''}} - ), + )), teamChannel: Chat.makeChatScreen( React.lazy(async () => import('./channel')), {getOptions: {headerShadowVisible: false, headerTitle: ''}} From b5b59f2eadee597d1529e5239d330d702fb3e0e7 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:25:52 -0400 Subject: [PATCH 43/45] WIP --- shared/login/routes.tsx | 5 +++-- shared/router-v2/route-params.tsx | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/shared/login/routes.tsx b/shared/login/routes.tsx index 4c7a0224d832..7b630abd22ea 100644 --- a/shared/login/routes.tsx +++ b/shared/login/routes.tsx @@ -6,7 +6,8 @@ import {newRoutes as provisionRoutes} from '../provision/routes-sub' import {sharedNewRoutes as settingsRoutes} from '../settings/routes' import {newRoutes as signupRoutes} from './signup/routes' import {settingsFeedbackTab} from '@/constants/settings' -import {defineRouteMap} from '@/constants/types/router' +import {defineRouteMap, withRouteParams} from '@/constants/types/router' +import type {Props as FeedbackRouteParams} from '../settings/feedback/container' const recoverPasswordStyles = Kb.Styles.styleSheetCreate(() => ({ questionBox: Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.tiny, 0), @@ -25,7 +26,7 @@ const recoverPasswordGetOptions = { } export const newRoutes = defineRouteMap({ - feedback: settingsRoutes[settingsFeedbackTab], + feedback: withRouteParams(settingsRoutes[settingsFeedbackTab]), login: {getOptions: {headerShown: false}, screen: React.lazy(async () => import('.'))}, recoverPasswordDeviceSelector: { getOptions: {title: 'Recover password'}, diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index 8f31ca5153b3..e5689a9aa0d6 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -1,6 +1,7 @@ import type * as React from 'react' import type {RouteProp} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' +import type {RouteDef} from '@/constants/types/router' import type {newRoutes as chatNewRoutes, newModalRoutes as chatNewModalRoutes} from '../chat/routes' import type {newRoutes as cryptoNewRoutes, newModalRoutes as cryptoNewModalRoutes} from '../crypto/routes' import type {newRoutes as deviceNewRoutes, newModalRoutes as deviceNewModalRoutes} from '../devices/routes' @@ -27,10 +28,12 @@ type ExtractScreenParams = ? NormalizeParams : undefined : undefined -type ExtractRouteParams = '__routeParams' extends keyof Route - ? Route extends {__routeParams?: infer Params} - ? NormalizeParams - : undefined +type ExtractRouteParams = Route extends RouteDef + ? NormalizeParams + : '__routeParams' extends keyof Route + ? Route extends {__routeParams?: infer Params} + ? NormalizeParams + : undefined : Route extends {screen: infer Screen} ? ExtractScreenParams : undefined From 0ca6f113d305eaee14511743cac1918f3f34391c Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Wed, 1 Apr 2026 16:29:07 -0400 Subject: [PATCH 44/45] WIP --- shared/chat/routes.tsx | 30 ++++++++++++++++++++---------- shared/router-v2/route-params.tsx | 2 ++ shared/settings/routes.tsx | 10 +++++++--- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index 3f522810179a..f9bc0df46e26 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -47,6 +47,9 @@ type ChatSearchBotsRouteParams = { type ChatShowNewTeamDialogRouteParams = { conversationIDKey?: T.Chat.ConversationIDKey } +const emptyChatBlockingRouteParams: ChatBlockingRouteParams = {} +const emptyChatSearchBotsRouteParams: ChatSearchBotsRouteParams = {} +const emptyChatShowNewTeamDialogRouteParams: ChatShowNewTeamDialogRouteParams = {} const PDFShareButton = ({url}: {url?: string}) => { const showShareActionSheet = useConfigState(s => s.dispatch.defer.showShareActionSheet) @@ -175,9 +178,14 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./conversation/attachment-get-titles')), {getOptions: {modalStyle: {height: 660, maxHeight: 660}}} ), - chatBlockingModal: withRouteParams(Chat.makeChatScreen(React.lazy(async () => import('./blocking/block-modal')), { - getOptions: {headerTitle: () => }, - })), + chatBlockingModal: withRouteParams({ + ...Chat.makeChatScreen(React.lazy(async () => import('./blocking/block-modal')), { + getOptions: { + headerTitle: () => , + }, + }), + initialParams: emptyChatBlockingRouteParams, + }), chatChooseEmoji: Chat.makeChatScreen(React.lazy(async () => import('./emoji-picker/container')), { getOptions: {headerShown: false}, }), @@ -234,12 +242,13 @@ export const newModalRoutes = defineRouteMap({ overlayStyle: {alignSelf: 'stretch'}, }), }), - chatSearchBots: withRouteParams( - Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/search')), { + chatSearchBots: withRouteParams({ + ...Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/search')), { canBeNullConvoID: true, getOptions: {title: 'Add a bot'}, - }) - ), + }), + initialParams: emptyChatSearchBotsRouteParams, + }), chatSendToChat: Chat.makeChatScreen( React.lazy(async () => import('./send-to-chat')), { @@ -250,9 +259,10 @@ export const newModalRoutes = defineRouteMap({ skipProvider: true, } ), - chatShowNewTeamDialog: withRouteParams( - Chat.makeChatScreen(React.lazy(async () => import('./new-team-dialog-container'))) - ), + chatShowNewTeamDialog: withRouteParams({ + ...Chat.makeChatScreen(React.lazy(async () => import('./new-team-dialog-container'))), + initialParams: emptyChatShowNewTeamDialogRouteParams, + }), chatUnfurlMapPopup: Chat.makeChatScreen( React.lazy(async () => import('./conversation/messages/text/unfurl/unfurl-list/map-popup')), {getOptions: {title: 'Location'}} diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index e5689a9aa0d6..da07df4ce915 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -30,6 +30,8 @@ type ExtractScreenParams = : undefined type ExtractRouteParams = Route extends RouteDef ? NormalizeParams + : Route extends {initialParams: infer Params} + ? NormalizeParams : '__routeParams' extends keyof Route ? Route extends {__routeParams?: infer Params} ? NormalizeParams diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index 015313c6afa0..a24c0a10f82f 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -72,11 +72,15 @@ const SettingsRootDesktop = React.lazy(async () => import('./root-desktop-tablet const EmptySettingsScreen = () => <> const ManageContactsScreen: React.ComponentType = C.isMobile ? React.lazy(async () => import('./manage-contacts')) : EmptySettingsScreen +const emptyFeedbackParams: FeedbackRouteParams = {} const feedback = withRouteParams( - C.makeScreen(React.lazy(async () => import('./feedback/container')), { - getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}, - }) + { + ...C.makeScreen(React.lazy(async () => import('./feedback/container')), { + getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}, + }), + initialParams: emptyFeedbackParams, + } ) export const sharedNewRoutes = defineRouteMap({ From 3b3daf77148c26493eb4dd77a88627e6165b8543 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Thu, 2 Apr 2026 09:18:34 -0400 Subject: [PATCH 45/45] WIP --- shared/chat/routes.tsx | 26 +++++----- shared/constants/types/router.tsx | 27 +++------- shared/login/routes.tsx | 5 +- shared/router-v2/route-params.tsx | 84 +++++++------------------------ shared/settings/routes.tsx | 16 +++--- shared/teams/routes.tsx | 6 +-- 6 files changed, 48 insertions(+), 116 deletions(-) diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index f9bc0df46e26..0e5545d72a4a 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -11,7 +11,7 @@ import {useModalHeaderState} from '@/stores/modal-header' import {ModalTitle} from '@/teams/common' import inboxGetOptions from './inbox/get-options' import inboxAndConvoGetOptions from './inbox-and-conversation-get-options' -import {defineRouteMap, withRouteParams} from '@/constants/types/router' +import {defineRouteMap} from '@/constants/types/router' import type {BlockModalContext} from './blocking/block-modal' const Convo = React.lazy(async () => import('./conversation/container')) @@ -153,7 +153,7 @@ export const newRoutes = defineRouteMap({ }) export const newModalRoutes = defineRouteMap({ - chatAddToChannel: withRouteParams(Chat.makeChatScreen( + chatAddToChannel: Chat.makeChatScreen( React.lazy(async () => import('./conversation/info-panel/add-to-channel')), { getOptions: ({route}) => ({ @@ -161,7 +161,7 @@ export const newModalRoutes = defineRouteMap({ headerTitle: () => , }), } - )), + ), chatAttachmentFullscreen: Chat.makeChatScreen( React.lazy(async () => import('./conversation/attachment-fullscreen/screen')), { @@ -178,14 +178,14 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./conversation/attachment-get-titles')), {getOptions: {modalStyle: {height: 660, maxHeight: 660}}} ), - chatBlockingModal: withRouteParams({ + chatBlockingModal: { ...Chat.makeChatScreen(React.lazy(async () => import('./blocking/block-modal')), { getOptions: { headerTitle: () => , }, }), initialParams: emptyChatBlockingRouteParams, - }), + }, chatChooseEmoji: Chat.makeChatScreen(React.lazy(async () => import('./emoji-picker/container')), { getOptions: {headerShown: false}, }), @@ -193,9 +193,7 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./punycode-link-warning')), {skipProvider: true} ), - chatConfirmRemoveBot: withRouteParams( - Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/confirm')), {canBeNullConvoID: true}) - ), + chatConfirmRemoveBot: Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/confirm')), {canBeNullConvoID: true}), chatCreateChannel: Chat.makeChatScreen( React.lazy(async () => import('./create-channel')), {skipProvider: true} @@ -208,7 +206,7 @@ export const newModalRoutes = defineRouteMap({ React.lazy(async () => import('./conversation/info-panel')), {getOptions: C.isMobile ? undefined : {modalStyle: {height: '80%', width: '80%'}}} ), - chatInstallBot: withRouteParams(Chat.makeChatScreen( + chatInstallBot: Chat.makeChatScreen( React.lazy(async () => import('./conversation/bot/install')), { getOptions: { @@ -218,7 +216,7 @@ export const newModalRoutes = defineRouteMap({ }, skipProvider: true, } - )), + ), chatInstallBotPick: Chat.makeChatScreen( React.lazy(async () => import('./conversation/bot/team-picker')), {getOptions: {title: 'Add to team or chat'}, skipProvider: true} @@ -242,13 +240,13 @@ export const newModalRoutes = defineRouteMap({ overlayStyle: {alignSelf: 'stretch'}, }), }), - chatSearchBots: withRouteParams({ + chatSearchBots: { ...Chat.makeChatScreen(React.lazy(async () => import('./conversation/bot/search')), { canBeNullConvoID: true, getOptions: {title: 'Add a bot'}, }), initialParams: emptyChatSearchBotsRouteParams, - }), + }, chatSendToChat: Chat.makeChatScreen( React.lazy(async () => import('./send-to-chat')), { @@ -259,10 +257,10 @@ export const newModalRoutes = defineRouteMap({ skipProvider: true, } ), - chatShowNewTeamDialog: withRouteParams({ + chatShowNewTeamDialog: { ...Chat.makeChatScreen(React.lazy(async () => import('./new-team-dialog-container'))), initialParams: emptyChatShowNewTeamDialogRouteParams, - }), + }, chatUnfurlMapPopup: Chat.makeChatScreen( React.lazy(async () => import('./conversation/messages/text/unfurl/unfurl-list/map-popup')), {getOptions: {title: 'Location'}} diff --git a/shared/constants/types/router.tsx b/shared/constants/types/router.tsx index fd842bcef757..96448c49db45 100644 --- a/shared/constants/types/router.tsx +++ b/shared/constants/types/router.tsx @@ -90,31 +90,16 @@ export type RouteDef< > = { __routeParams?: Params getOptions?: GetOptions - initialParams?: Params extends undefined ? undefined : Params + initialParams?: Params screen: Screen } export type RouteMap = {[K in string]?: RouteDef} -type RouteDefMatchesScreen = - R extends {screen: infer Screen} - ? Screen extends AnyScreen - ? Omit & { - __routeParams?: R extends {__routeParams?: infer Params} ? Params : ScreenRouteParams - getOptions?: GetOptions - initialParams?: (R extends {__routeParams?: infer Params} ? Params : ScreenRouteParams) extends undefined - ? undefined - : R extends {__routeParams?: infer Params} - ? Params - : ScreenRouteParams - screen: Screen - } - : never - : never +export const defineRouteMap = (routes: Routes) => routes -export const defineRouteMap = >( - routes: Routes & {[K in keyof Routes]: RouteDefMatchesScreen} -) => routes - -export const withRouteParams = ( +// tsgo does not support partial type argument application: providing Params explicitly +// while letting Screen be inferred causes "Expected 2 type arguments, but got 1". +// Adding Screen = AnyScreen as a default fixes this. +export const withRouteParams = ( route: RouteDef ): RouteDef => route as RouteDef diff --git a/shared/login/routes.tsx b/shared/login/routes.tsx index 7b630abd22ea..4c7a0224d832 100644 --- a/shared/login/routes.tsx +++ b/shared/login/routes.tsx @@ -6,8 +6,7 @@ import {newRoutes as provisionRoutes} from '../provision/routes-sub' import {sharedNewRoutes as settingsRoutes} from '../settings/routes' import {newRoutes as signupRoutes} from './signup/routes' import {settingsFeedbackTab} from '@/constants/settings' -import {defineRouteMap, withRouteParams} from '@/constants/types/router' -import type {Props as FeedbackRouteParams} from '../settings/feedback/container' +import {defineRouteMap} from '@/constants/types/router' const recoverPasswordStyles = Kb.Styles.styleSheetCreate(() => ({ questionBox: Kb.Styles.padding(Kb.Styles.globalMargins.tiny, Kb.Styles.globalMargins.tiny, 0), @@ -26,7 +25,7 @@ const recoverPasswordGetOptions = { } export const newRoutes = defineRouteMap({ - feedback: withRouteParams(settingsRoutes[settingsFeedbackTab]), + feedback: settingsRoutes[settingsFeedbackTab], login: {getOptions: {headerShown: false}, screen: React.lazy(async () => import('.'))}, recoverPasswordDeviceSelector: { getOptions: {title: 'Recover password'}, diff --git a/shared/router-v2/route-params.tsx b/shared/router-v2/route-params.tsx index da07df4ce915..f88f72328f7b 100644 --- a/shared/router-v2/route-params.tsx +++ b/shared/router-v2/route-params.tsx @@ -1,47 +1,24 @@ -import type * as React from 'react' import type {RouteProp} from '@react-navigation/native' import type {NativeStackNavigationProp} from '@react-navigation/native-stack' -import type {RouteDef} from '@/constants/types/router' -import type {newRoutes as chatNewRoutes, newModalRoutes as chatNewModalRoutes} from '../chat/routes' -import type {newRoutes as cryptoNewRoutes, newModalRoutes as cryptoNewModalRoutes} from '../crypto/routes' -import type {newRoutes as deviceNewRoutes, newModalRoutes as deviceNewModalRoutes} from '../devices/routes' -import type {newRoutes as fsNewRoutes, newModalRoutes as fsNewModalRoutes} from '../fs/routes' -import type {newRoutes as gitNewRoutes, newModalRoutes as gitNewModalRoutes} from '../git/routes' -import type {newRoutes as loginNewRoutes, newModalRoutes as loginNewModalRoutes} from '../login/routes' -import type {newRoutes as peopleNewRoutes, newModalRoutes as peopleNewModalRoutes} from '../people/routes' -import type {newRoutes as profileNewRoutes, newModalRoutes as profileNewModalRoutes} from '../profile/routes' -import type {newRoutes as settingsNewRoutes, newModalRoutes as settingsNewModalRoutes} from '../settings/routes' -import type {newRoutes as signupNewRoutes, newModalRoutes as signupNewModalRoutes} from '../signup/routes' -import type {newRoutes as teamsNewRoutes, newModalRoutes as teamsNewModalRoutes} from '../teams/routes' -import type {newModalRoutes as walletsNewModalRoutes} from '../wallets/routes' -import type {newModalRoutes as incomingShareNewModalRoutes} from '../incoming-share/routes' +import type {routes, modalRoutes, loggedOutRoutes} from './routes' -type IsUnknown = unknown extends T ? ([keyof T] extends [never] ? true : false) : false -type NormalizeParams = IsUnknown extends true ? undefined : T extends object | undefined ? T : undefined -type ExtractScreenParams = - Screen extends React.LazyExoticComponent - ? ExtractScreenParams - : Screen extends React.ComponentType - ? Props extends {route: {params: infer Params}} - ? NormalizeParams - : Props extends {route: {params?: infer Params}} - ? NormalizeParams - : undefined - : undefined -type ExtractRouteParams = Route extends RouteDef - ? NormalizeParams - : Route extends {initialParams: infer Params} - ? NormalizeParams - : '__routeParams' extends keyof Route - ? Route extends {__routeParams?: infer Params} - ? NormalizeParams +// tsgo bug: StaticParamList is the idiomatic React Navigation equivalent of _ExtractParams, +// but tsgo reports "TS2315: Type 'StaticParamList' is not generic" (works fine with regular tsc). +// Once tsgo fixes re-exported generic type aliases, replace _ExtractParams: +// type _SyntheticConfig = {readonly config: {readonly screens: _AllScreens}} +// export type RootParamList = StaticParamList<_SyntheticConfig> & Tabs & {...} +// +// Similarly, avoid matching on RouteDef — tsgo fails to infer Params +// through conditional types in RouteDef's field definitions. Instead, extract params +// directly from the screen function's route prop, which tsgo handles correctly. +type _ExtractParams = { + [K in keyof T]: T[K] extends {screen: infer U} + ? U extends (args: infer V) => any + ? V extends {route: {params: infer W}} + ? W + : undefined : undefined - : Route extends {screen: infer Screen} - ? ExtractScreenParams : undefined - -type _ExtractParams = { - [K in keyof T]: ExtractRouteParams } type Tabs = { @@ -59,35 +36,10 @@ type Tabs = { 'tabs.walletsTab': undefined } -type _AllScreens = - & typeof deviceNewRoutes - & typeof chatNewRoutes - & typeof cryptoNewRoutes - & typeof peopleNewRoutes - & typeof profileNewRoutes - & typeof fsNewRoutes - & typeof settingsNewRoutes - & typeof teamsNewRoutes - & typeof gitNewRoutes - & typeof chatNewModalRoutes - & typeof cryptoNewModalRoutes - & typeof deviceNewModalRoutes - & typeof fsNewModalRoutes - & typeof gitNewModalRoutes - & typeof loginNewModalRoutes - & typeof peopleNewModalRoutes - & typeof profileNewModalRoutes - & typeof settingsNewModalRoutes - & typeof signupNewModalRoutes - & typeof teamsNewModalRoutes - & typeof walletsNewModalRoutes - & typeof incomingShareNewModalRoutes - & typeof loginNewRoutes - & typeof signupNewRoutes +type _AllScreens = typeof routes & typeof modalRoutes & typeof loggedOutRoutes -type KeybaseRootParamList = _ExtractParams<_AllScreens> & +export type RootParamList = _ExtractParams<_AllScreens> & Tabs & {loading: undefined; loggedOut: undefined; loggedIn: undefined} -export type RootParamList = KeybaseRootParamList export type RouteKeys = keyof RootParamList export type NoParamRouteKeys = { diff --git a/shared/settings/routes.tsx b/shared/settings/routes.tsx index a24c0a10f82f..b0d66a250bff 100644 --- a/shared/settings/routes.tsx +++ b/shared/settings/routes.tsx @@ -5,7 +5,7 @@ import {newRoutes as devicesRoutes} from '../devices/routes' import {newRoutes as gitRoutes} from '../git/routes' import {newRoutes as walletsRoutes} from '../wallets/routes' import * as Settings from '@/constants/settings' -import {defineRouteMap, withRouteParams} from '@/constants/types/router' +import {defineRouteMap} from '@/constants/types/router' import {usePushState} from '@/stores/push' import {usePWState} from '@/stores/settings-password' import {e164ToDisplay} from '@/util/phone-numbers' @@ -74,14 +74,12 @@ const ManageContactsScreen: React.ComponentType = C.isMobile ? React.lazy(async () => import('./manage-contacts')) : EmptySettingsScreen const emptyFeedbackParams: FeedbackRouteParams = {} -const feedback = withRouteParams( - { - ...C.makeScreen(React.lazy(async () => import('./feedback/container')), { - getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}, - }), - initialParams: emptyFeedbackParams, - } -) +const feedback = { + ...C.makeScreen(React.lazy(async () => import('./feedback/container')), { + getOptions: C.isMobile ? {headerShown: true, title: 'Feedback'} : {}, + }), + initialParams: emptyFeedbackParams, +} export const sharedNewRoutes = defineRouteMap({ [Settings.settingsAboutTab]: { diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index bae6108edaa3..2d140b0446c4 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -10,7 +10,7 @@ import contactRestricted from '../team-building/contact-restricted.page' import teamsTeamBuilder from '../team-building/page' import {useModalHeaderState} from '@/stores/modal-header' import teamsRootGetOptions from './get-options' -import {defineRouteMap, withRouteParams} from '@/constants/types/router' +import {defineRouteMap} from '@/constants/types/router' type TeamRouteParams = { teamID: T.Teams.TeamID @@ -192,10 +192,10 @@ const NewTeamInfoHeaderLeft = () => { } export const newRoutes = defineRouteMap({ - team: withRouteParams(C.makeScreen( + team: C.makeScreen( React.lazy(async () => import('./team')), {getOptions: {headerShadowVisible: false, headerTitle: ''}} - )), + ), teamChannel: Chat.makeChatScreen( React.lazy(async () => import('./channel')), {getOptions: {headerShadowVisible: false, headerTitle: ''}}