From 05cf8217a7877745518f9597bbe1b0c83c713544 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 16:31:06 -0400 Subject: [PATCH 01/10] WIP --- shared/chat/blocking/invitation-to-block.tsx | 10 ++++++++++ shared/stores/chat.tsx | 6 ------ shared/stores/convostate.tsx | 4 ---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/shared/chat/blocking/invitation-to-block.tsx b/shared/chat/blocking/invitation-to-block.tsx index 5d2dacaac26b..3eddddeb4880 100644 --- a/shared/chat/blocking/invitation-to-block.tsx +++ b/shared/chat/blocking/invitation-to-block.tsx @@ -3,6 +3,7 @@ import {isAssertion} from '@/constants/chat/helpers' import * as Chat from '@/stores/chat' import * as ConvoState from '@/stores/convostate' import * as Kb from '@/common-adapters' +import * as React from 'react' import {useCurrentUserState} from '@/stores/current-user' import {navToProfile} from '@/constants/router' @@ -18,6 +19,9 @@ const BlockButtons = () => { }) const participantInfo = ConvoState.useChatContext(s => s.participants) const currentUser = useCurrentUserState(s => s.username) + const hasOwnMessage = ConvoState.useChatContext(s => + !!currentUser && [...(s.messageOrdinals ?? [])].some(ordinal => s.messageMap.get(ordinal)?.author === currentUser) + ) const dismissBlockButtons = Chat.useChatState(s => s.dispatch.dismissBlockButtons) if (!blockButtonInfo) { return null @@ -42,6 +46,12 @@ const BlockButtons = () => { }) const onDismiss = () => dismissBlockButtons(teamID) + React.useEffect(() => { + if (hasOwnMessage) { + dismissBlockButtons(teamID) + } + }, [dismissBlockButtons, hasOwnMessage, teamID]) + const buttonRow = ( void dismissBlockButtons: (teamID: T.RPCGen.TeamID) => void - dismissBlockButtonsIfPresent: (teamID: T.RPCGen.TeamID) => void inboxRefresh: (reason: RefreshReason) => Promise setInboxRetriedOnCurrentEmpty: (retried: boolean) => void loadStaticConfig: () => void @@ -116,11 +115,6 @@ export const useChatState = Z.createZustand('chat', (set, get) => { } ignorePromise(f()) }, - dismissBlockButtonsIfPresent: teamID => { - if (get().blockButtonsMap.has(teamID)) { - get().dispatch.dismissBlockButtons(teamID) - } - }, inboxRefresh: async reason => requestInboxLayout(reason), loadStaticConfig: () => { if (get().staticConfig) { diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 602ad2e5c3c0..04a0b8a9af7d 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -56,7 +56,6 @@ import {useCurrentUserState} from '@/stores/current-user' import {useUsersState} from '@/stores/users' import {getUsernameToShow} from '@/chat/conversation/messages/separator-utils' import type {RefreshReason} from '@/stores/chat-shared' -import {storeRegistry} from '@/stores/store-registry' const {darwinCopyToChatTempUploadFile} = KB2.functions @@ -2293,9 +2292,6 @@ const createSlice = logger.info('error') } - // If there are block buttons on this conversation, clear them. - storeRegistry.getState('chat').dispatch.dismissBlockButtonsIfPresent(meta.teamID) - // Do some logging to track down the root cause of a bug causing // messages to not send. Do this after creating the objects above to // narrow down the places where the action can possibly stop. From 91c0baff45cefdf0f1b8fd9d9bf424b90c37b601 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 16:33:05 -0400 Subject: [PATCH 02/10] WIP --- shared/constants/types/chat/index.tsx | 14 ++++++++++++++ shared/stores/chat-shared.tsx | 13 ------------- shared/stores/chat.tsx | 7 +++---- shared/stores/convostate.tsx | 5 ++--- 4 files changed, 19 insertions(+), 20 deletions(-) delete mode 100644 shared/stores/chat-shared.tsx diff --git a/shared/constants/types/chat/index.tsx b/shared/constants/types/chat/index.tsx index a2bb7f7c8bdc..a6992fd4297d 100644 --- a/shared/constants/types/chat/index.tsx +++ b/shared/constants/types/chat/index.tsx @@ -102,6 +102,20 @@ export type BlockButtonsInfo = { adder: string } +export type RefreshReason = + | 'bootstrap' + | 'componentNeverLoaded' + | 'inboxSyncedCurrentButEmpty' + | 'inboxStale' + | 'inboxSyncedClear' + | 'inboxSyncedUnknown' + | 'joinedAConversation' + | 'leftAConversation' + | 'teamTypeChanged' + | 'maybeKickedFromTeam' + | 'widgetRefresh' + | 'shareConfigSearch' + export type BotPublicCommands = T.Immutable<{ loadError: boolean commands: Array diff --git a/shared/stores/chat-shared.tsx b/shared/stores/chat-shared.tsx deleted file mode 100644 index c953728a4345..000000000000 --- a/shared/stores/chat-shared.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export type RefreshReason = - | 'bootstrap' - | 'componentNeverLoaded' - | 'inboxSyncedCurrentButEmpty' - | 'inboxStale' - | 'inboxSyncedClear' - | 'inboxSyncedUnknown' - | 'joinedAConversation' - | 'leftAConversation' - | 'teamTypeChanged' - | 'maybeKickedFromTeam' - | 'widgetRefresh' - | 'shareConfigSearch' diff --git a/shared/stores/chat.tsx b/shared/stores/chat.tsx index 0e5034e9340a..946919622f07 100644 --- a/shared/stores/chat.tsx +++ b/shared/stores/chat.tsx @@ -5,7 +5,6 @@ import * as Z from '@/util/zustand' import isEqual from 'lodash/isEqual' import logger from '@/logger' import {getTeamMentionName} from '@/constants/chat/helpers' -import type {RefreshReason} from '@/stores/chat-shared' import {RPCError} from '@/util/errors' import {bodyToJSON} from '@/constants/rpc-utils' import {ignorePromise} from '@/constants/utils' @@ -60,7 +59,7 @@ export type State = Store & { dispatch: { badgesUpdated: (badgeState?: T.RPCGen.BadgeState) => void dismissBlockButtons: (teamID: T.RPCGen.TeamID) => void - inboxRefresh: (reason: RefreshReason) => Promise + inboxRefresh: (reason: T.Chat.RefreshReason) => Promise setInboxRetriedOnCurrentEmpty: (retried: boolean) => void loadStaticConfig: () => void onEngineIncomingImpl: (action: EngineGen.Actions) => void @@ -76,7 +75,7 @@ export type State = Store & { // generic chat store export const useChatState = Z.createZustand('chat', (set, get) => { - const requestInboxLayout = async (reason: RefreshReason) => { + const requestInboxLayout = async (reason: T.Chat.RefreshReason) => { const {username} = useCurrentUserState.getState() const {loggedIn} = useConfigState.getState() if (!loggedIn || !username) { @@ -300,7 +299,7 @@ export const useChatState = Z.createZustand('chat', (set, get) => { }) export * from '@/stores/inbox-rows' -export type {RefreshReason} from '@/stores/chat-shared' +export type {RefreshReason} from '@/constants/types/chat' export * from '@/constants/chat/common' export * from '@/constants/chat/meta' export * from '@/constants/chat/message' diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 04a0b8a9af7d..60d7029282e1 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -55,7 +55,6 @@ import {useConfigState} from '@/stores/config' import {useCurrentUserState} from '@/stores/current-user' import {useUsersState} from '@/stores/users' import {getUsernameToShow} from '@/chat/conversation/messages/separator-utils' -import type {RefreshReason} from '@/stores/chat-shared' const {darwinCopyToChatTempUploadFile} = KB2.functions @@ -255,7 +254,7 @@ export interface ConvoState extends ConvoStore { clearAttachmentView: () => void defer: { chatInboxLayoutSmallTeamsFirstConvID: () => T.Chat.ConversationIDKey | undefined - chatInboxRefresh: (reason: RefreshReason) => void + chatInboxRefresh: (reason: T.Chat.RefreshReason) => void chatMetasReceived: (metas: ReadonlyArray) => void } dismissBottomBanner: () => void @@ -866,7 +865,7 @@ export const onInboxLayoutChanged = ( export const onChatInboxSynced = async ( action: EngineGen.EngineAction<'chat.1.NotifyChat.ChatInboxSynced'>, - refreshInbox: (reason: RefreshReason) => void | Promise + refreshInbox: (reason: T.Chat.RefreshReason) => void | Promise ) => { const {syncRes} = action.payload.params From 3042299c40e69f695bb87e4e4d704bd3ed2a9fa5 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 16:43:22 -0400 Subject: [PATCH 03/10] WIP --- shared/chat/conversation/info-panel/menu.tsx | 4 +-- .../info-panel/settings/index.tsx | 5 ++- .../chat/conversation/input-area/preview.tsx | 5 +-- shared/chat/inbox-and-conversation-shared.tsx | 18 +++++++---- shared/constants/init/shared.tsx | 2 -- shared/constants/router.tsx | 19 +++++++++++ shared/stores/convostate.tsx | 32 ------------------- 7 files changed, 38 insertions(+), 47 deletions(-) diff --git a/shared/chat/conversation/info-panel/menu.tsx b/shared/chat/conversation/info-panel/menu.tsx index b43dfaffeb54..11e130fae306 100644 --- a/shared/chat/conversation/info-panel/menu.tsx +++ b/shared/chat/conversation/info-panel/menu.tsx @@ -118,7 +118,8 @@ const InfoPanelMenuConnector = function InfoPanelMenuConnector(p: OwnProps) { } const onJoinChannel = ConvoState.useChatContext(s => s.dispatch.joinConversation) - const onLeaveChannel = ConvoState.useChatContext(s => s.dispatch.leaveConversation) + const conversationIDKey = ConvoState.useChatContext(s => s.id) + const onLeaveChannel = () => C.Router2.leaveConversation(conversationIDKey) const onLeaveTeam = () => teamID && chatNavigateAppend(() => ({name: 'teamReallyLeaveTeam', params: {teamID}})) const onManageChannels = () => { manageChatChannels(teamID) @@ -210,7 +211,6 @@ const InfoPanelMenuConnector = function InfoPanelMenuConnector(p: OwnProps) { ), } as const - const conversationIDKey = ConvoState.useChatContext(s => s.id) const hideItem = (() => { if (!conversationIDKey) { return null diff --git a/shared/chat/conversation/info-panel/settings/index.tsx b/shared/chat/conversation/info-panel/settings/index.tsx index dddc99e33694..1f17056fd43e 100644 --- a/shared/chat/conversation/info-panel/settings/index.tsx +++ b/shared/chat/conversation/info-panel/settings/index.tsx @@ -64,9 +64,9 @@ const SettingsPanel = (props: SettingsPanelProps) => { } } - const leaveConversation = ConvoState.useChatContext(s => s.dispatch.leaveConversation) + const conversationIDKey = ConvoState.useChatContext(s => s.id) const onLeaveConversation = () => { - leaveConversation() + C.Router2.leaveConversation(conversationIDKey) } const onArchive = () => { @@ -77,7 +77,6 @@ const SettingsPanel = (props: SettingsPanelProps) => { } const showDangerZone = canDeleteHistory || entityType === 'adhoc' || entityType !== 'channel' - const conversationIDKey = ConvoState.useChatContext(s => s.id) return ( { + const conversationIDKey = ConvoState.useChatContext(s => s.id) const meta = ConvoState.useChatContext(s => s.meta) const onJoinChannel = ConvoState.useChatContext(s => s.dispatch.joinConversation) - const onLeaveChannel = ConvoState.useChatContext(s => s.dispatch.leaveConversation) const {channelname} = meta const [clicked, setClicked] = React.useState(undefined) @@ -14,7 +15,7 @@ const Preview = () => { if (join) { onJoinChannel() } else { - onLeaveChannel() + C.Router2.leaveConversation(conversationIDKey) } } diff --git a/shared/chat/inbox-and-conversation-shared.tsx b/shared/chat/inbox-and-conversation-shared.tsx index 530f646e0526..214f9bd5ba2c 100644 --- a/shared/chat/inbox-and-conversation-shared.tsx +++ b/shared/chat/inbox-and-conversation-shared.tsx @@ -24,24 +24,30 @@ export function InboxAndConversationShell(props: Props) { const conversationIDKey = props.conversationIDKey ?? Chat.noConversationIDKey const infoPanel = props.infoPanel const validConvoID = conversationIDKey && conversationIDKey !== Chat.noConversationIDKey - const seenValidCIDRef = React.useRef(validConvoID ? conversationIDKey : '') + const lastValidCIDRef = React.useRef(validConvoID ? conversationIDKey : '') + const lastAutoSelectedCIDRef = React.useRef('') const selectNextConvo = Chat.useChatState(s => { - if (seenValidCIDRef.current) { + if (validConvoID) { return null } const first = s.inboxLayout?.smallTeams?.[0] - return first?.convID + return first?.convID === lastValidCIDRef.current ? null : first?.convID }) React.useEffect(() => { - if (selectNextConvo && seenValidCIDRef.current !== selectNextConvo) { - seenValidCIDRef.current = selectNextConvo + if (validConvoID) { + lastValidCIDRef.current = conversationIDKey + lastAutoSelectedCIDRef.current = '' + return + } + if (selectNextConvo && lastAutoSelectedCIDRef.current !== selectNextConvo) { + lastAutoSelectedCIDRef.current = selectNextConvo // need to defer , not sure why, shouldn't be setTimeout(() => { ConvoState.getConvoState(selectNextConvo).dispatch.navigateToThread('findNewestConversationFromLayout') }, 100) } - }, [selectNextConvo]) + }, [conversationIDKey, selectNextConvo, validConvoID]) return ( diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 02599c8284f5..9ecc73182703 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -187,8 +187,6 @@ export const initSharedSubscriptions = () => { _sharedUnsubs.length = 0 setConvoDefer({ - chatInboxLayoutSmallTeamsFirstConvID: () => - storeRegistry.getState('chat').inboxLayout?.smallTeams?.[0]?.convID, chatInboxRefresh: reason => { ignorePromise(storeRegistry.getState('chat').dispatch.inboxRefresh(reason)) }, diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index c223dcbebfc9..3109a06a5ee7 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -22,6 +22,7 @@ import {registerDebugClear} from '@/util/debug' import {makeUUID} from '@/util/uuid' import {storeRegistry} from '@/stores/store-registry' import * as Meta from './chat/meta' +import * as Strings from './strings' import {RPCError} from '@/util/errors' // Detects the unconstrained Record index-signature type. @@ -348,6 +349,24 @@ export const navigateToInbox = (allowSwitchTab = true) => { }, 1) } +export const leaveConversation = ( + conversationIDKey: T.Chat.ConversationIDKey, + navToInbox = true +) => { + ignorePromise( + T.RPCChat.localLeaveConversationLocalRpcPromise( + {convID: T.Chat.keyToConversationID(conversationIDKey)}, + Strings.waitingKeyChatLeaveConversation + ) + ) + clearModals() + if (!navToInbox) { + return + } + navUpToScreen('chatRoot') + switchTab(Tabs.chatTab) +} + export const previewConversation = (p: PreviewConversationParams) => { const previewConversationPersonMakesAConversation = () => { const {participants, teamname, highlightMessageID} = p diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 60d7029282e1..8f85d2281ff5 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -8,9 +8,7 @@ import { navigateAppend, navigateToInbox, navigateUp, - navUpToScreen, previewConversation, - switchTab, getVisibleScreen, getModalStack, navToThread, @@ -253,7 +251,6 @@ export interface ConvoState extends ConvoStore { channelSuggestionsTriggered: () => void clearAttachmentView: () => void defer: { - chatInboxLayoutSmallTeamsFirstConvID: () => T.Chat.ConversationIDKey | undefined chatInboxRefresh: (reason: T.Chat.RefreshReason) => void chatMetasReceived: (metas: ReadonlyArray) => void } @@ -269,7 +266,6 @@ export interface ConvoState extends ConvoStore { hideConversation: (hide: boolean) => void joinConversation: () => void jumpToRecent: () => void - leaveConversation: (navToInbox?: boolean) => void loadAttachmentView: (viewType: T.RPCChat.GalleryItemTyp, fromMsgID?: T.Chat.MessageID) => void loadMessagesCentered: ( messageID: T.Chat.MessageID, @@ -418,9 +414,6 @@ export const numMessagesOnInitialLoad = isMobile ? 20 : 100 export const numMessagesOnScrollback = isMobile ? 100 : 100 const stubDefer: ConvoState['dispatch']['defer'] = { - chatInboxLayoutSmallTeamsFirstConvID: () => { - throw new Error('convostate defer not initialized') - }, chatInboxRefresh: () => { throw new Error('convostate defer not initialized') }, @@ -2578,31 +2571,6 @@ const createSlice = }) get().dispatch.loadMoreMessages({reason: 'jump to recent'}) }, - leaveConversation: (navToInbox = true) => { - const f = async () => { - await T.RPCChat.localLeaveConversationLocalRpcPromise( - {convID: get().getConvID()}, - Strings.waitingKeyChatLeaveConversation - ) - } - ignorePromise(f()) - clearModals() - if (navToInbox) { - navUpToScreen('chatRoot') - switchTab(Tabs.chatTab) - if (!isMobile) { - const vs = getVisibleScreen() - const params = vs?.params as undefined | {conversationIDKey?: T.Chat.ConversationIDKey} - if (params?.conversationIDKey === get().id) { - // select a convo - const next = get().dispatch.defer.chatInboxLayoutSmallTeamsFirstConvID() - if (next) { - getConvoState(next).dispatch.navigateToThread('findNewestConversationFromLayout') - } - } - } - } - }, loadAttachmentView: (viewType, fromMsgID) => { set(s => { const {attachmentViewMap} = s From 828796ca35be702735449cf509d82c8debefa5c9 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 17:50:51 -0400 Subject: [PATCH 04/10] WIP --- .../conversation/attachment-get-titles.tsx | 4 +- .../messages/special-top-message.tsx | 2 +- .../container.tsx | 4 +- .../messages/system-profile-reset-notice.tsx | 3 +- shared/chat/inbox-and-conversation-shared.tsx | 2 +- shared/chat/inbox/row/big-team-channel.tsx | 5 +- shared/chat/inbox/row/small-team/index.tsx | 2 +- shared/chat/inbox/use-inbox-search.tsx | 10 +- shared/chat/send-to-chat/index.tsx | 5 +- shared/constants/deeplinks.tsx | 4 +- shared/constants/router.tsx | 189 ++++++++++++++++-- .../renderer/remote-event-handler.desktop.tsx | 4 +- shared/incoming-share/index.tsx | 3 +- shared/stores/convostate.tsx | 179 +---------------- 14 files changed, 204 insertions(+), 212 deletions(-) diff --git a/shared/chat/conversation/attachment-get-titles.tsx b/shared/chat/conversation/attachment-get-titles.tsx index a617e0828db0..bb7213ee928d 100644 --- a/shared/chat/conversation/attachment-get-titles.tsx +++ b/shared/chat/conversation/attachment-get-titles.tsx @@ -41,7 +41,7 @@ const Container = (ownProps: OwnProps) => { const noDragDrop = ownProps.noDragDrop ?? false const selectConversationWithReason = ownProps.selectConversationWithReason const navigateUp = C.Router2.navigateUp - const navigateToThread = ConvoState.useChatContext(s => s.dispatch.navigateToThread) + const conversationIDKey = ConvoState.useChatContext(s => s.id) const attachmentUploadCanceled = ConvoState.useChatContext(s => s.dispatch.attachmentUploadCanceled) const onCancel = () => { attachmentUploadCanceled( @@ -65,7 +65,7 @@ const Container = (ownProps: OwnProps) => { clearModals() if (selectConversationWithReason) { - navigateToThread(selectConversationWithReason) + C.Router2.navigateToThread(conversationIDKey, selectConversationWithReason) } } const pathAndInfos = pathAndOutboxIDs.map(({path, outboxID, url}) => { diff --git a/shared/chat/conversation/messages/special-top-message.tsx b/shared/chat/conversation/messages/special-top-message.tsx index 265edc9c34ff..feb4143413de 100644 --- a/shared/chat/conversation/messages/special-top-message.tsx +++ b/shared/chat/conversation/messages/special-top-message.tsx @@ -18,7 +18,7 @@ const ErrorMessage = () => { const createConversationError = useChatThreadRouteParams()?.createConversationError const _onCreateWithoutThem = (allowedUsers: ReadonlyArray) => { - ConvoState.createConversation(allowedUsers) + C.Router2.createConversation(allowedUsers) } const navigateToInbox = C.Router2.navigateToInbox diff --git a/shared/chat/conversation/messages/system-old-profile-reset-notice/container.tsx b/shared/chat/conversation/messages/system-old-profile-reset-notice/container.tsx index c0efb62178e1..8b3addc87e65 100644 --- a/shared/chat/conversation/messages/system-old-profile-reset-notice/container.tsx +++ b/shared/chat/conversation/messages/system-old-profile-reset-notice/container.tsx @@ -1,6 +1,6 @@ import * as ConvoState from '@/stores/convostate' import type * as T from '@/constants/types' -import {previewConversation} from '@/constants/router' +import {navigateToThread, previewConversation} from '@/constants/router' import {Text} from '@/common-adapters' import UserNotice from '../user-notice' @@ -11,7 +11,7 @@ const SystemOldProfileResetNotice = () => { const nextConversationIDKey = meta.supersededBy const username = meta.wasFinalizedBy || '' const onOpenConversation = (conversationIDKey: T.Chat.ConversationIDKey) => { - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread('jumpFromReset') + navigateToThread(conversationIDKey, 'jumpFromReset') } const startConversation = (participants: ReadonlyArray) => { previewConversation({participants, reason: 'fromAReset'}) diff --git a/shared/chat/conversation/messages/system-profile-reset-notice.tsx b/shared/chat/conversation/messages/system-profile-reset-notice.tsx index 31c849cc533e..1a8260b181f5 100644 --- a/shared/chat/conversation/messages/system-profile-reset-notice.tsx +++ b/shared/chat/conversation/messages/system-profile-reset-notice.tsx @@ -1,4 +1,5 @@ import * as ConvoState from '@/stores/convostate' +import * as C from '@/constants' import type * as T from '@/constants/types' import * as Kb from '@/common-adapters' import UserNotice from './user-notice' @@ -8,7 +9,7 @@ const SystemProfileResetNotice = () => { const prevConversationIDKey = meta.supersedes const username = meta.wasFinalizedBy || '' const _onOpenOlderConversation = (conversationIDKey: T.Chat.ConversationIDKey) => { - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread('jumpToReset') + C.Router2.navigateToThread(conversationIDKey, 'jumpToReset') } const onOpenOlderConversation = () => { prevConversationIDKey && _onOpenOlderConversation(prevConversationIDKey) diff --git a/shared/chat/inbox-and-conversation-shared.tsx b/shared/chat/inbox-and-conversation-shared.tsx index 214f9bd5ba2c..909a85284f8d 100644 --- a/shared/chat/inbox-and-conversation-shared.tsx +++ b/shared/chat/inbox-and-conversation-shared.tsx @@ -44,7 +44,7 @@ export function InboxAndConversationShell(props: Props) { lastAutoSelectedCIDRef.current = selectNextConvo // need to defer , not sure why, shouldn't be setTimeout(() => { - ConvoState.getConvoState(selectNextConvo).dispatch.navigateToThread('findNewestConversationFromLayout') + C.Router2.navigateToThread(selectNextConvo, 'findNewestConversationFromLayout') }, 100) } }, [conversationIDKey, selectNextConvo, validConvoID]) diff --git a/shared/chat/inbox/row/big-team-channel.tsx b/shared/chat/inbox/row/big-team-channel.tsx index 2d23f9fd5a01..aad2e5893a91 100644 --- a/shared/chat/inbox/row/big-team-channel.tsx +++ b/shared/chat/inbox/row/big-team-channel.tsx @@ -1,4 +1,4 @@ -import * as ConvoState from '@/stores/convostate' +import * as C from '@/constants' import type * as React from 'react' import * as Kb from '@/common-adapters' import * as RowSizes from './sizes' @@ -15,8 +15,7 @@ const BigTeamChannel = (props: Props) => { const row = useInboxRowBig(conversationIDKey) const {channelname, isMuted, hasBadge, hasDraft, hasUnread, isError, snippetDecoration} = row - const onSelectConversation = () => - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread('inboxBig') + const onSelectConversation = () => C.Router2.navigateToThread(conversationIDKey, 'inboxBig') let outboxTooltip: string | undefined let outboxIcon: React.ReactNode = null diff --git a/shared/chat/inbox/row/small-team/index.tsx b/shared/chat/inbox/row/small-team/index.tsx index a45c94b4cd28..67542cede4cc 100644 --- a/shared/chat/inbox/row/small-team/index.tsx +++ b/shared/chat/inbox/row/small-team/index.tsx @@ -35,7 +35,7 @@ const SmallTeam = (p: Props) => { : (p.onSelectConversation ?? (() => { setOpenedRow(Chat.noConversationIDKey) - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread('inboxSmall') + C.Router2.navigateToThread(conversationIDKey, 'inboxSmall') })) const backgroundColor = isSelected diff --git a/shared/chat/inbox/use-inbox-search.tsx b/shared/chat/inbox/use-inbox-search.tsx index e79686b7ea8d..7001e66f5412 100644 --- a/shared/chat/inbox/use-inbox-search.tsx +++ b/shared/chat/inbox/use-inbox-search.tsx @@ -1,3 +1,4 @@ +import * as C from '@/constants' import {ignorePromise} from '@/constants/utils' import * as T from '@/constants/types' import logger from '@/logger' @@ -447,14 +448,9 @@ export function useInboxSearch(): InboxSearchController { } if (query) { - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread( - 'inboxSearch', - undefined, - undefined, - query - ) + C.Router2.navigateToThread(conversationIDKey, 'inboxSearch', undefined, query) } else { - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread('inboxSearch') + C.Router2.navigateToThread(conversationIDKey, 'inboxSearch') clearSearch() } }, diff --git a/shared/chat/send-to-chat/index.tsx b/shared/chat/send-to-chat/index.tsx index a60fe04d14d1..fff79474ec7e 100644 --- a/shared/chat/send-to-chat/index.tsx +++ b/shared/chat/send-to-chat/index.tsx @@ -36,7 +36,6 @@ export const MobileSendToChat = (props: Props) => { const clearModals = C.Router2.clearModals const fileContext = useFSState(s => s.fileContext) const onSelect = (conversationIDKey: T.Chat.ConversationIDKey, tlfName: string) => { - const {dispatch} = ConvoState.getConvoState(conversationIDKey) text && ConvoState.getConvoUIState(conversationIDKey).dispatch.injectIntoInput(text) if (sendPaths?.length) { navigateAppend({ @@ -53,7 +52,7 @@ export const MobileSendToChat = (props: Props) => { }) } else { clearModals() - dispatch.navigateToThread(isFromShareExtension ? 'extension' : 'files') + C.Router2.navigateToThread(conversationIDKey, isFromShareExtension ? 'extension' : 'files') } } return @@ -84,7 +83,7 @@ const DesktopSendToChat = (props: Props) => { ) ) clearModals() - ConvoState.getConvoState(conversationIDKey).dispatch.navigateToThread('files') + C.Router2.navigateToThread(conversationIDKey, 'files') } return ( { switch (parts[0]) { case 'convid': if (parts[1]) { - navToThread(parts[1]) + navigateToThread(parts[1], 'push') return } break diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 3109a06a5ee7..bc5a9a6d665c 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -2,6 +2,8 @@ import type * as React from 'react' import * as T from './types' import type * as ConvoRegistryType from '@/stores/convo-registry' import type * as ConvoStateType from '@/stores/convostate' +import type * as UseChatStateType from '@/stores/chat' +import type * as UseCurrentUserStateType from '@/stores/current-user' import * as Tabs from './tabs' import { StackActions, @@ -15,7 +17,7 @@ import { import type {StaticScreenProps} from '@react-navigation/core' import type {NavigateAppendType, RouteKeys, RootParamList as KBRootParamList} from '@/router-v2/route-params' import type {GetOptionsRet, RouteDef} from './types/router' -import {isSplit} from './chat/layout' +import {isSplit, threadRouteName} from './chat/layout' import {isMobile} from './platform' import {ignorePromise, shallowEqual} from './utils' import {registerDebugClear} from '@/util/debug' @@ -23,6 +25,7 @@ import {makeUUID} from '@/util/uuid' import {storeRegistry} from '@/stores/store-registry' import * as Meta from './chat/meta' import * as Strings from './strings' +import logger from '@/logger' import {RPCError} from '@/util/errors' // Detects the unconstrained Record index-signature type. @@ -66,6 +69,23 @@ export type Navigator = NavigationContainerRef const DEBUG_NAV = __DEV__ && (false as boolean) const rootNonModalRouteNames = new Set(['chatConversation']) +const uiParticipantsToParticipantInfo = ( + uiParticipants: ReadonlyArray +): T.Chat.ParticipantInfo => { + const participantInfo = {all: new Array(), contactName: new Map(), name: new Array()} + uiParticipants.forEach(part => { + const {assertion, contactName, inConvName} = part + participantInfo.all.push(assertion) + if (inConvName) { + participantInfo.name.push(assertion) + } + if (contactName) { + participantInfo.contactName.set(assertion, contactName) + } + }) + return participantInfo +} + export const getRootState = (): NavState | undefined => { if (!navigationRef.isReady()) return return navigationRef.getRootState() @@ -367,6 +387,79 @@ export const leaveConversation = ( switchTab(Tabs.chatTab) } +export const createConversation = ( + participants: ReadonlyArray, + highlightMessageID?: T.Chat.MessageID +) => { + // TODO This will break if you try to make 2 new conversations at the same time because there is + // only one pending conversation state. + // The fix involves being able to make multiple pending conversations. + const f = async () => { + const {useCurrentUserState} = require('@/stores/current-user') as typeof UseCurrentUserStateType + const username = useCurrentUserState.getState().username + if (!username) { + logger.error('Making a convo while logged out?') + return + } + + try { + const result = await T.RPCChat.localNewConversationLocalRpcPromise( + { + identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, + membersType: T.RPCChat.ConversationMembersType.impteamnative, + tlfName: [...new Set([username, ...participants])].join(','), + tlfVisibility: T.RPCGen.TLFVisibility.private, + topicType: T.RPCChat.TopicType.chat, + }, + Strings.waitingKeyChatCreating + ) + const {conv, uiConv} = result + const conversationIDKey = T.Chat.conversationIDToKey(conv.info.id) + if (!conversationIDKey) { + logger.warn("Couldn't make a new conversation?") + return + } + + const {metasReceived} = require('@/stores/convostate') as typeof ConvoStateType + const meta = Meta.inboxUIItemToConversationMeta(uiConv) + if (meta) { + metasReceived([meta]) + } + + const participantInfo = uiParticipantsToParticipantInfo(uiConv.participants ?? []) + if (participantInfo.all.length > 0) { + storeRegistry.getConvoState(conversationIDKey).dispatch.setParticipants(participantInfo) + } + + navigateToThread(conversationIDKey, 'justCreated', highlightMessageID) + + const {useChatState} = require('@/stores/chat') as typeof UseChatStateType + ignorePromise(useChatState.getState().dispatch.inboxRefresh('joinedAConversation')) + } catch (error) { + if (error instanceof RPCError) { + const fields = error.fields as Array<{key?: string}> | undefined + const errUsernames = fields?.filter(elem => elem.key === 'usernames') as + | undefined + | Array<{key: string; value: string}> + let disallowedUsers: Array = [] + if (errUsernames?.length) { + const {value} = errUsernames[0] ?? {value: ''} + disallowedUsers = value.split(',') + } + const allowedUsers = participants.filter(x => !disallowedUsers.includes(x)) + navigateToThread(T.Chat.pendingErrorConversationIDKey, 'justCreated', highlightMessageID, undefined, { + allowedUsers, + code: error.code, + disallowedUsers, + message: error.desc, + }) + } + } + } + + ignorePromise(f()) +} + export const previewConversation = (p: PreviewConversationParams) => { const previewConversationPersonMakesAConversation = () => { const {participants, teamname, highlightMessageID} = p @@ -380,17 +473,12 @@ export const previewConversation = (p: PreviewConversationParams) => { if (names.length !== toFindN) continue const participantSet = [...names].sort().join(',') if (participantSet === toFind) { - storeRegistry - .getConvoState(cs.getState().id) - .dispatch.navigateToThread('justCreated', highlightMessageID) + navigateToThread(cs.getState().id, 'justCreated', highlightMessageID) return } } - storeRegistry - .getConvoState(T.Chat.pendingWaitingConversationIDKey) - .dispatch.navigateToThread('justCreated') - const {createConversation} = require('@/stores/convostate') as typeof ConvoStateType + navigateToThread(T.Chat.pendingWaitingConversationIDKey, 'justCreated') createConversation(participants, highlightMessageID) } @@ -408,9 +496,7 @@ export const previewConversation = (p: PreviewConversationParams) => { }) } - storeRegistry - .getConvoState(conversationIDKey) - .dispatch.navigateToThread('previewResolved', highlightMessageID) + navigateToThread(conversationIDKey, 'previewResolved', highlightMessageID) return } @@ -456,9 +542,7 @@ export const previewConversation = (p: PreviewConversationParams) => { metasReceived([meta]) } - storeRegistry - .getConvoState(first.conversationIDKey) - .dispatch.navigateToThread('previewResolved', highlightMessageID) + navigateToThread(first.conversationIDKey, 'previewResolved', highlightMessageID) } catch (error) { if ( error instanceof RPCError && @@ -545,7 +629,36 @@ type ThreadNavParams = { threadSearch?: {query?: string} } -export const navToThread = (conversationIDKey: T.Chat.ConversationIDKey, navParams?: ThreadNavParams) => { +export type NavigateToThreadReason = + | 'focused' + | 'clearSelected' + | 'desktopNotification' + | 'createdMessagePrivately' + | 'extension' + | 'files' + | 'findNewestConversation' + | 'findNewestConversationFromLayout' + | 'inboxBig' + | 'inboxFilterArrow' + | 'inboxFilterChanged' + | 'inboxSmall' + | 'inboxNewConversation' + | 'inboxSearch' + | 'jumpFromReset' + | 'jumpToReset' + | 'justCreated' + | 'manageView' + | 'previewResolved' + | 'push' + | 'savedLastState' + | 'startFoundExisting' + | 'teamChat' + | 'addedToChannel' + | 'navChanged' + | 'misc' + | 'teamMention' + +const navToThread = (conversationIDKey: T.Chat.ConversationIDKey, navParams?: ThreadNavParams) => { DEBUG_NAV && console.log('[Nav] navToThread', conversationIDKey) const n = _getNavigator() if (!n) return @@ -584,6 +697,52 @@ export const navToThread = (conversationIDKey: T.Chat.ConversationIDKey, navPara } } +export const navigateToThread = ( + conversationIDKey: T.Chat.ConversationIDKey, + reason: NavigateToThreadReason, + highlightMessageID?: T.Chat.MessageID, + threadSearchQuery?: string, + createConversationError?: T.Chat.CreateConversationError +) => { + storeRegistry.getConvoState(conversationIDKey).dispatch.prepareToNavigateToThread(highlightMessageID) + + if (reason === 'navChanged') { + return + } + + const visible = getVisibleScreen() + const params = visible?.params as {conversationIDKey?: T.Chat.ConversationIDKey} | undefined + const visibleConvo = params?.conversationIDKey + const visibleRouteName = visible?.name + + if (visibleRouteName !== threadRouteName && reason === 'findNewestConversation') { + return + } + + const threadSearch = threadSearchQuery ? {query: threadSearchQuery} : undefined + const navParams = {createConversationError, threadSearch} + if (isSplit) { + navToThread(conversationIDKey, navParams) + } else if (reason === 'push' || reason === 'savedLastState') { + navToThread(conversationIDKey, navParams) + } else { + const replace = + visibleRouteName === threadRouteName && !T.Chat.isValidConversationIDKey(visibleConvo ?? '') + const modalPath = getModalStack() + if (modalPath.length > 0) { + clearModals() + } + + navigateAppend( + { + name: threadRouteName, + params: {conversationIDKey, createConversationError, threadSearch}, + }, + replace + ) + } +} + export const appendPeopleBuilder = () => { navigateAppend({ name: 'peopleTeamBuilder', diff --git a/shared/desktop/renderer/remote-event-handler.desktop.tsx b/shared/desktop/renderer/remote-event-handler.desktop.tsx index f0ee8825c1e2..538dc0da2f1e 100644 --- a/shared/desktop/renderer/remote-event-handler.desktop.tsx +++ b/shared/desktop/renderer/remote-event-handler.desktop.tsx @@ -4,7 +4,7 @@ import * as Crypto from '@/constants/crypto' import * as Tabs from '@/constants/tabs' import {RPCError} from '@/util/errors' import {ignorePromise} from '@/constants/utils' -import {navigateAppend, previewConversation, switchTab} from '@/constants/router' +import {navigateAppend, navigateToThread, previewConversation, switchTab} from '@/constants/router' import {storeRegistry} from '@/stores/store-registry' import {onEngineConnected, onEngineDisconnected} from '@/constants/init/index.desktop' import {emitDeepLink} from '@/router-v2/linking' @@ -69,7 +69,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { break case RemoteGen.openChatFromWidget: { useConfigState.getState().dispatch.showMain() - storeRegistry.getConvoState(action.payload.conversationIDKey).dispatch.navigateToThread('inboxSmall') + navigateToThread(action.payload.conversationIDKey, 'inboxSmall') break } case RemoteGen.inboxRefresh: { diff --git a/shared/incoming-share/index.tsx b/shared/incoming-share/index.tsx index c167b93adddc..4ce95fd6b656 100644 --- a/shared/incoming-share/index.tsx +++ b/shared/incoming-share/index.tsx @@ -183,9 +183,8 @@ const IncomingShare = (props: IncomingShareWithSelectionProps) => { React.useEffect(() => { if (!canDirectNav || hasNavigatedRef.current) return hasNavigatedRef.current = true - const {dispatch} = ConvoState.getConvoState(selectedConversationIDKey) text && ConvoState.getConvoUIState(selectedConversationIDKey).dispatch.injectIntoInput(text) - dispatch.navigateToThread('extension') + C.Router2.navigateToThread(selectedConversationIDKey, 'extension') if (sendPaths.length > 0) { const meta = ConvoState.getConvoState(selectedConversationIDKey).meta const tlfName = meta.conversationIDKey === selectedConversationIDKey ? meta.tlfname : '' diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 8f85d2281ff5..63abe6d742df 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -4,14 +4,15 @@ import * as TeamsUtil from '@/constants/teams' import * as PlatformSpecific from '@/util/platform-specific' import { clearModals, + createConversation, getTab, navigateAppend, navigateToInbox, + navigateToThread as routerNavigateToThread, navigateUp, previewConversation, getVisibleScreen, getModalStack, - navToThread, setChatRootParams, } from '@/constants/router' import type * as Router2 from '@/constants/router' @@ -62,35 +63,6 @@ const noParticipantInfo: T.Chat.ParticipantInfo = { name: [], } -type NavReason = - | 'focused' // nav focus changed - | 'clearSelected' // deselect - | 'desktopNotification' // clicked notification - | 'createdMessagePrivately' // messaging privately and maybe made it - | 'extension' // from a notification from iOS share extension - | 'files' // from the Files tab - | 'findNewestConversation' // find a new chat to select (from service) - | 'findNewestConversationFromLayout' // find a small chat to select (from js) - | 'inboxBig' // inbox row - | 'inboxFilterArrow' // arrow keys in inbox filter - | 'inboxFilterChanged' // inbox filter made first one selected - | 'inboxSmall' // inbox row - | 'inboxNewConversation' // new conversation row - | 'inboxSearch' // selected from inbox seaech - | 'jumpFromReset' // from older reset convo - | 'jumpToReset' // going to an older reset convo - | 'justCreated' // just made it and select it - | 'manageView' // clicked from manage screen - | 'previewResolved' // did a preview and are now selecting it - | 'push' // from a push - | 'savedLastState' // last seen chat tab - | 'startFoundExisting' // starting a conversation and found one already - | 'teamChat' // from team - | 'addedToChannel' // just added people to this channel - | 'navChanged' // the nav state changed - | 'misc' // misc - | 'teamMention' // from team mention - type LoadMoreReason = | 'jumpAttachment' | 'foregrounding' @@ -100,7 +72,7 @@ type LoadMoreReason = | 'scroll forward' | 'scroll back' | 'tab selected' - | NavReason + | Router2.NavigateToThreadReason type ConvoStore = T.Immutable<{ id: T.Chat.ConversationIDKey @@ -292,14 +264,8 @@ export interface ConvoState extends ConvoStore { ordinals?: ReadonlyArray }) => void mute: (m: boolean) => void - navigateToThread: ( - reason: NavReason, - highlightMessageID?: T.Chat.MessageID, - pushBody?: string, - threadSearchQuery?: string, - createConversationError?: T.Chat.CreateConversationError - ) => void openFolder: () => void + prepareToNavigateToThread: (highlightMessageID?: T.Chat.MessageID) => void onEngineIncoming: (action: EngineGen.Actions) => void onIncomingMessage: (incoming: T.RPCChat.IncomingMessage) => void onMessageErrored: (outboxID: T.Chat.OutboxID, reason: string, errorTyp?: number) => void @@ -584,7 +550,7 @@ export const maybeChangeSelectedConversation = (inboxLayout?: T.RPCChat.UIInboxL logger.info( `maybeChangeSelectedConversation: selecting new conv: new:${newConvID} old:${oldConvID} prevselected ${selectedConversation}` ) - getConvoState(newConvID).dispatch.navigateToThread('findNewestConversation') + routerNavigateToThread(newConvID, 'findNewestConversation') } export const loadSelectedConversationIfStale = (metas: ReadonlyArray) => { @@ -1240,85 +1206,11 @@ export const handleConvoEngineIncoming = ( } } -export const createConversation = ( - participants: ReadonlyArray, - highlightMessageID?: T.Chat.MessageID -) => { - // TODO This will break if you try to make 2 new conversations at the same time because there is - // only one pending conversation state. - // The fix involves being able to make multiple pending conversations. - const f = async () => { - const username = useCurrentUserState.getState().username - if (!username) { - logger.error('Making a convo while logged out?') - return - } - try { - const result = await T.RPCChat.localNewConversationLocalRpcPromise( - { - identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, - membersType: T.RPCChat.ConversationMembersType.impteamnative, - tlfName: [...new Set([username, ...participants])].join(','), - tlfVisibility: T.RPCGen.TLFVisibility.private, - topicType: T.RPCChat.TopicType.chat, - }, - Strings.waitingKeyChatCreating - ) - const {conv, uiConv} = result - const conversationIDKey = T.Chat.conversationIDToKey(conv.info.id) - if (!conversationIDKey) { - logger.warn("Couldn't make a new conversation?") - return - } - - const meta = Meta.inboxUIItemToConversationMeta(uiConv) - if (meta) { - metasReceived([meta]) - } - - const participantInfo: T.Chat.ParticipantInfo = Common.uiParticipantsToParticipantInfo( - uiConv.participants ?? [] - ) - if (participantInfo.all.length > 0) { - getConvoState(T.Chat.stringToConversationIDKey(uiConv.convID)).dispatch.setParticipants(participantInfo) - } - getConvoState(conversationIDKey).dispatch.navigateToThread('justCreated', highlightMessageID) - getConvoState(conversationIDKey).dispatch.defer.chatInboxRefresh('joinedAConversation') - } catch (error) { - if (error instanceof RPCError) { - const f = error.fields as Array<{key?: string}> | undefined - const errUsernames = f?.filter(elem => elem.key === 'usernames') as - | undefined - | Array<{key: string; value: string}> - let disallowedUsers: Array = [] - if (errUsernames?.length) { - const {value} = errUsernames[0] ?? {value: ''} - disallowedUsers = value.split(',') - } - const allowedUsers = participants.filter(x => !disallowedUsers.includes(x)) - getConvoState(T.Chat.pendingErrorConversationIDKey).dispatch.navigateToThread( - 'justCreated', - highlightMessageID, - undefined, - undefined, - { - allowedUsers, - code: error.code, - disallowedUsers, - message: error.desc, - } - ) - } - } - } - ignorePromise(f()) -} - export const onTeamBuildingFinished = (users: ReadonlySet) => { const f = async () => { // need to let the modal hide first else its thrashy await timeoutPromise(500) - getConvoState(T.Chat.pendingWaitingConversationIDKey).dispatch.navigateToThread('justCreated') + routerNavigateToThread(T.Chat.pendingWaitingConversationIDKey, 'justCreated') createConversation([...users].map(u => u.id)) } ignorePromise(f()) @@ -1628,7 +1520,7 @@ const createSlice = const onClick = () => { useConfigState.getState().dispatch.showMain() navigateToInbox() - get().dispatch.navigateToThread('desktopNotification') + routerNavigateToThread(get().id, 'desktopNotification') } const onClose = () => {} logger.info('invoking NotifyPopup for chat notification') @@ -3158,7 +3050,7 @@ const createSlice = const text = formatTextForQuoting(message.text.stringValue()) getConvoUIState(newThreadCID).dispatch.injectIntoInput(text) get().dispatch.defer.chatMetasReceived([meta]) - getConvoState(newThreadCID).dispatch.navigateToThread('createdMessagePrivately') + routerNavigateToThread(newThreadCID, 'createdMessagePrivately') } ignorePromise(f()) }, @@ -3265,13 +3157,7 @@ const createSlice = } ignorePromise(f()) }, - navigateToThread: ( - _reason, - highlightMessageID, - _pushBody, - threadSearchQuery, - createConversationError - ) => { + prepareToNavigateToThread: highlightMessageID => { set(s => { // force loaded if we're an error if (s.id === T.Chat.pendingErrorConversationIDKey) { @@ -3279,53 +3165,6 @@ const createSlice = } s.pendingJumpMessageID = highlightMessageID }) - - const updateNav = () => { - const reason = _reason - if (reason === 'navChanged') { - return - } - const conversationIDKey = get().id - const visible = getVisibleScreen() - const params = visible?.params as {conversationIDKey?: T.Chat.ConversationIDKey} | undefined - const visibleConvo = params?.conversationIDKey - const visibleRouteName = visible?.name - - if (visibleRouteName !== Common.threadRouteName && reason === 'findNewestConversation') { - // service is telling us to change our selection but we're not looking, ignore - return - } - - // we select the chat tab and change the params - const threadSearch = threadSearchQuery ? {query: threadSearchQuery} : undefined - const navParams = {createConversationError, threadSearch} - if (Common.isSplit) { - navToThread(conversationIDKey, navParams) - // immediately switch stack to an inbox | thread stack - } else if (reason === 'push' || reason === 'savedLastState') { - navToThread(conversationIDKey, navParams) - return - } else { - // replace if looking at the pending / waiting screen - const replace = - visibleRouteName === Common.threadRouteName && - !T.Chat.isValidConversationIDKey(visibleConvo ?? '') - // note: we don't switch tabs on non split - const modalPath = getModalStack() - if (modalPath.length > 0) { - clearModals() - } - - navigateAppend( - { - name: Common.threadRouteName, - params: {conversationIDKey, createConversationError, threadSearch}, - }, - replace - ) - } - } - updateNav() }, onEngineIncoming: action => { switch (action.type) { From 6ae0d2adf3c72b0ad7ee40332a744d561dba482f Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 21:44:11 -0400 Subject: [PATCH 05/10] WIP --- shared/chat/inbox-and-conversation-shared.tsx | 1 + shared/chat/inbox-and-conversation.desktop.tsx | 2 +- shared/chat/inbox-and-conversation.native.tsx | 4 +++- shared/chat/inbox/index.desktop.tsx | 17 +++++++++++------ shared/chat/inbox/index.native.tsx | 15 ++++++++++----- shared/chat/inbox/use-inbox-state.tsx | 17 ++++++++++++++++- shared/constants/init/shared.tsx | 3 --- shared/constants/router.tsx | 13 +++++++++---- shared/constants/types/chat/index.tsx | 6 ++++++ shared/stores/convostate.tsx | 7 +------ 10 files changed, 58 insertions(+), 27 deletions(-) diff --git a/shared/chat/inbox-and-conversation-shared.tsx b/shared/chat/inbox-and-conversation-shared.tsx index 909a85284f8d..381479f2df60 100644 --- a/shared/chat/inbox-and-conversation-shared.tsx +++ b/shared/chat/inbox-and-conversation-shared.tsx @@ -12,6 +12,7 @@ import type {ThreadSearchRouteProps} from './conversation/thread-search-route' export type InboxAndConversationProps = ThreadSearchRouteProps & { conversationIDKey?: T.Chat.ConversationIDKey infoPanel?: {tab?: Panel} + refreshInbox?: T.Chat.ChatRootInboxRefresh } export type ChatRootRouteParams = InboxAndConversationProps diff --git a/shared/chat/inbox-and-conversation.desktop.tsx b/shared/chat/inbox-and-conversation.desktop.tsx index f410c722ffd8..ee182300e376 100644 --- a/shared/chat/inbox-and-conversation.desktop.tsx +++ b/shared/chat/inbox-and-conversation.desktop.tsx @@ -10,7 +10,7 @@ export default function InboxAndConversationDesktop(props: InboxAndConversationP const headerPortal = useInboxHeaderPortal(search) const leftPane = ( - + ) diff --git a/shared/chat/inbox-and-conversation.native.tsx b/shared/chat/inbox-and-conversation.native.tsx index 75bc3fc293b1..9d091e887f14 100644 --- a/shared/chat/inbox-and-conversation.native.tsx +++ b/shared/chat/inbox-and-conversation.native.tsx @@ -12,7 +12,9 @@ export default function InboxAndConversationNative(props: InboxAndConversationPr {headerPortal} } + leftPane={ + + } /> ) diff --git a/shared/chat/inbox/index.desktop.tsx b/shared/chat/inbox/index.desktop.tsx index bcf3c5d1c8c3..ecfa55b093ee 100644 --- a/shared/chat/inbox/index.desktop.tsx +++ b/shared/chat/inbox/index.desktop.tsx @@ -200,22 +200,27 @@ const DragLine = (p: { type InboxProps = { conversationIDKey?: T.Chat.ConversationIDKey + refreshInbox?: T.Chat.ChatRootInboxRefresh search?: InboxSearchController } type ControlledInboxProps = { conversationIDKey?: T.Chat.ConversationIDKey + refreshInbox?: T.Chat.ChatRootInboxRefresh search: InboxSearchController } -function InboxWithSearch(props: {conversationIDKey?: T.Chat.ConversationIDKey}) { +function InboxWithSearch(props: { + conversationIDKey?: T.Chat.ConversationIDKey + refreshInbox?: T.Chat.ChatRootInboxRefresh +}) { const search = useInboxSearch() - return + return } function InboxBody(props: ControlledInboxProps) { - const {conversationIDKey, search} = props - const inbox = useInboxState(conversationIDKey, search.isSearching) + const {conversationIDKey, refreshInbox, search} = props + const inbox = useInboxState(conversationIDKey, search.isSearching, refreshInbox) const {smallTeamsExpanded, rows, unreadIndices, unreadTotal, inboxNumSmallRows} = inbox const {toggleSmallTeamsExpanded, selectedConversationIDKey, onUntrustedInboxVisible} = inbox const {setInboxNumSmallRows, allowShowFloatingButton} = inbox @@ -338,9 +343,9 @@ function InboxBody(props: ControlledInboxProps) { function Inbox(props: InboxProps) { return props.search ? ( - + ) : ( - + ) } diff --git a/shared/chat/inbox/index.native.tsx b/shared/chat/inbox/index.native.tsx index 035e11daa48f..c3d9ed97420a 100644 --- a/shared/chat/inbox/index.native.tsx +++ b/shared/chat/inbox/index.native.tsx @@ -53,22 +53,27 @@ const NoChats = (props: {onNewChat: () => void}) => ( type InboxProps = { conversationIDKey?: T.Chat.ConversationIDKey + refreshInbox?: T.Chat.ChatRootInboxRefresh search?: InboxSearchController } type ControlledInboxProps = { conversationIDKey?: T.Chat.ConversationIDKey + refreshInbox?: T.Chat.ChatRootInboxRefresh search: InboxSearchController } -function InboxWithSearch(props: {conversationIDKey?: T.Chat.ConversationIDKey}) { +function InboxWithSearch(props: { + conversationIDKey?: T.Chat.ConversationIDKey + refreshInbox?: T.Chat.ChatRootInboxRefresh +}) { const search = useInboxSearch() - return + return } function InboxBody(p: ControlledInboxProps) { const {search} = p - const inbox = useInboxState(p.conversationIDKey, search.isSearching) + const inbox = useInboxState(p.conversationIDKey, search.isSearching, p.refreshInbox) const {onUntrustedInboxVisible, toggleSmallTeamsExpanded, selectedConversationIDKey} = inbox const {unreadIndices, unreadTotal, rows, smallTeamsExpanded, isSearching, allowShowFloatingButton} = inbox const {neverLoaded, onNewChat, inboxNumSmallRows, setInboxNumSmallRows} = inbox @@ -200,9 +205,9 @@ function InboxBody(p: ControlledInboxProps) { function Inbox(props: InboxProps) { return props.search ? ( - + ) : ( - + ) } diff --git a/shared/chat/inbox/use-inbox-state.tsx b/shared/chat/inbox/use-inbox-state.tsx index 7012594bd3ee..6c57e2248a22 100644 --- a/shared/chat/inbox/use-inbox-state.tsx +++ b/shared/chat/inbox/use-inbox-state.tsx @@ -9,7 +9,11 @@ import {useInboxRowsState} from '@/stores/inbox-rows' import {useIsFocused} from '@react-navigation/core' import {buildInboxRows} from './rows' -export function useInboxState(conversationIDKey?: string, isSearching = false) { +export function useInboxState( + conversationIDKey?: string, + isSearching = false, + refreshInbox?: T.Chat.ChatRootInboxRefresh +) { const isFocused = useIsFocused() const loggedIn = useConfigState(s => s.loggedIn) const username = useCurrentUserState(s => s.username) @@ -82,6 +86,7 @@ export function useInboxState(conversationIDKey?: string, isSearching = false) { // Handle focus changes on mobile const prevIsFocusedRef = React.useRef(isFocused) + const handledRefreshNonceRef = React.useRef('') React.useEffect(() => { if (prevIsFocusedRef.current === isFocused) return prevIsFocusedRef.current = isFocused @@ -99,6 +104,16 @@ export function useInboxState(conversationIDKey?: string, isSearching = false) { } }) + React.useEffect(() => { + const ready = loggedIn && !!username && (!C.isMobile || isFocused) + if (!ready || !refreshInbox || handledRefreshNonceRef.current === refreshInbox.nonce) { + return + } + handledRefreshNonceRef.current = refreshInbox.nonce + C.ignorePromise(inboxRefresh(refreshInbox.reason)) + C.Router2.setChatRootParams({refreshInbox: undefined}) + }, [inboxRefresh, isFocused, loggedIn, refreshInbox, username]) + C.Router2.useSafeFocusEffect(() => { if (!inboxHasLoaded) { C.ignorePromise(inboxRefresh('componentNeverLoaded')) diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 9ecc73182703..821620740c7e 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -187,9 +187,6 @@ export const initSharedSubscriptions = () => { _sharedUnsubs.length = 0 setConvoDefer({ - chatInboxRefresh: reason => { - ignorePromise(storeRegistry.getState('chat').dispatch.inboxRefresh(reason)) - }, chatMetasReceived: metas => convoMetasReceived(metas), }) _sharedUnsubs.push( diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index bc5a9a6d665c..554edc871db8 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -356,15 +356,21 @@ export type PreviewConversationParams = { reason: PreviewReason } -export const navigateToInbox = (allowSwitchTab = true) => { +export const navigateToInbox = ( + allowSwitchTab = true, + refreshReason: T.Chat.RefreshReason = 'navigatedToInbox' +) => { // Components can call this during render sometimes, so always defer. setTimeout(() => { + const refreshInbox = {nonce: makeUUID(), reason: refreshReason} if (getTab() !== Tabs.chatTab) { if (allowSwitchTab) { + setChatRootParams({refreshInbox}) switchTab(Tabs.chatTab) } return } + setChatRootParams({refreshInbox}) navUpToScreen('chatRoot') }, 1) } @@ -383,8 +389,7 @@ export const leaveConversation = ( if (!navToInbox) { return } - navUpToScreen('chatRoot') - switchTab(Tabs.chatTab) + navigateToInbox(true, 'leftAConversation') } export const createConversation = ( @@ -568,7 +573,7 @@ export const previewConversation = (p: PreviewConversationParams) => { export const setChatRootParams = (params: Partial>) => { const n = _getNavigator() - if (!n || !isSplit) return + if (!n) return const rs = getRootState() const tabNavState = rs?.routes?.[0]?.state if (!tabNavState?.key) return diff --git a/shared/constants/types/chat/index.tsx b/shared/constants/types/chat/index.tsx index a6992fd4297d..2da28cb4cb6a 100644 --- a/shared/constants/types/chat/index.tsx +++ b/shared/constants/types/chat/index.tsx @@ -109,6 +109,7 @@ export type RefreshReason = | 'inboxStale' | 'inboxSyncedClear' | 'inboxSyncedUnknown' + | 'navigatedToInbox' | 'joinedAConversation' | 'leftAConversation' | 'teamTypeChanged' @@ -116,6 +117,11 @@ export type RefreshReason = | 'widgetRefresh' | 'shareConfigSearch' +export type ChatRootInboxRefresh = { + nonce: string + reason: RefreshReason +} + export type BotPublicCommands = T.Immutable<{ loadError: boolean commands: Array diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 63abe6d742df..f1f041063bd6 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -223,7 +223,6 @@ export interface ConvoState extends ConvoStore { channelSuggestionsTriggered: () => void clearAttachmentView: () => void defer: { - chatInboxRefresh: (reason: T.Chat.RefreshReason) => void chatMetasReceived: (metas: ReadonlyArray) => void } dismissBottomBanner: () => void @@ -380,9 +379,6 @@ export const numMessagesOnInitialLoad = isMobile ? 20 : 100 export const numMessagesOnScrollback = isMobile ? 100 : 100 const stubDefer: ConvoState['dispatch']['defer'] = { - chatInboxRefresh: () => { - throw new Error('convostate defer not initialized') - }, chatMetasReceived: () => { throw new Error('convostate defer not initialized') }, @@ -2723,8 +2719,7 @@ const createSlice = logger.warn(`loadMoreMessages: error: ${error.desc}`) // no longer in team if (error.code === T.RPCGen.StatusCode.scchatnotinteam) { - get().dispatch.defer.chatInboxRefresh('maybeKickedFromTeam') - navigateToInbox() + navigateToInbox(true, 'maybeKickedFromTeam') } if (error.code !== T.RPCGen.StatusCode.scteamreaderror) { // scteamreaderror = user is not in team. they'll see the rekey screen so don't throw for that From 60e48640ef573e407ae97866d0a352bc1d525a4a Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 21:48:06 -0400 Subject: [PATCH 06/10] WIP --- shared/constants/init/shared.tsx | 8 -------- shared/stores/convostate.tsx | 32 +------------------------------- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 821620740c7e..77b83f3f6577 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -13,8 +13,6 @@ declare global { var __hmr_oneTimeInitDone: boolean | undefined - var __hmr_convoDeferImpl: unknown - var __hmr_chatStores: Map | undefined var __hmr_TBstores: Map | undefined @@ -58,10 +56,8 @@ import { onInboxLayoutChanged, onIncomingInboxUIItem, handleConvoEngineIncoming, - metasReceived as convoMetasReceived, onRouteChanged as onConvoRouteChanged, onTeamBuildingFinished as onConvoTeamBuildingFinished, - setConvoDefer, syncBadgeState, syncGregorExplodingModes, } from '@/stores/convostate' @@ -185,10 +181,6 @@ export const initSharedSubscriptions = () => { // HMR cleanup: unsubscribe old store subscriptions before re-subscribing for (const unsub of _sharedUnsubs) unsub() _sharedUnsubs.length = 0 - - setConvoDefer({ - chatMetasReceived: metas => convoMetasReceived(metas), - }) _sharedUnsubs.push( useConfigState.subscribe((s, old) => { if (s.loadOnStartPhase !== old.loadOnStartPhase) { diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index f1f041063bd6..09489ad13cfb 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -222,9 +222,6 @@ export interface ConvoState extends ConvoStore { botCommandsUpdateStatus: (b: T.RPCChat.UIBotCommandsUpdateStatus) => void channelSuggestionsTriggered: () => void clearAttachmentView: () => void - defer: { - chatMetasReceived: (metas: ReadonlyArray) => void - } dismissBottomBanner: () => void dismissJourneycard: (cardType: T.RPCChat.JourneycardType, ordinal: T.Chat.Ordinal) => void editBotSettings: ( @@ -378,31 +375,6 @@ type ScrollDirection = 'none' | 'back' | 'forward' export const numMessagesOnInitialLoad = isMobile ? 20 : 100 export const numMessagesOnScrollback = isMobile ? 100 : 100 -const stubDefer: ConvoState['dispatch']['defer'] = { - chatMetasReceived: () => { - throw new Error('convostate defer not initialized') - }, -} - -let convoDeferImpl: ConvoState['dispatch']['defer'] | undefined = __DEV__ - ? (globalThis.__hmr_convoDeferImpl as ConvoState['dispatch']['defer'] | undefined) - : undefined - -export const setConvoDefer = (impl: ConvoState['dispatch']['defer']) => { - convoDeferImpl = impl - if (__DEV__) globalThis.__hmr_convoDeferImpl = impl - for (const store of chatStores.values()) { - const s = store.getState() - store.setState({ - ...s, - dispatch: { - ...s.dispatch, - defer: impl, - }, - }) - } -} - export const onRouteChanged = (prev: T.Immutable, next: T.Immutable) => { const wasModal = prev && getModalStack(prev).length > 0 const isModal = next && getModalStack(next).length > 0 @@ -1262,7 +1234,6 @@ const createSlice = getLinkedUIState: () => ConvoUIState = () => getConvoUIState(id) ): Z.ImmerStateCreator => (set, get) => { - const defer = convoDeferImpl ?? stubDefer const getUI = getLinkedUIState const getLastOrdinal = () => get().messageOrdinals?.at(-1) ?? T.Chat.numberToOrdinal(0) const getCurrentUser = () => { @@ -2369,7 +2340,6 @@ const createSlice = s.attachmentViewMap = new Map() }) }, - defer, dismissBottomBanner: () => { set(s => { s.dismissedInviteBanners = true @@ -3044,7 +3014,7 @@ const createSlice = const text = formatTextForQuoting(message.text.stringValue()) getConvoUIState(newThreadCID).dispatch.injectIntoInput(text) - get().dispatch.defer.chatMetasReceived([meta]) + metasReceived([meta]) routerNavigateToThread(newThreadCID, 'createdMessagePrivately') } ignorePromise(f()) From c95d22230b4e555ab523a3423f24fe0df2f38f3d Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 21:54:22 -0400 Subject: [PATCH 07/10] WIP --- shared/chat/conversation/bot/install.tsx | 2 +- shared/chat/inbox/index.d.ts | 3 ++- shared/constants/router.tsx | 10 ++++++---- shared/stores/convostate.tsx | 19 +++++++++---------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/shared/chat/conversation/bot/install.tsx b/shared/chat/conversation/bot/install.tsx index cc89c5d4cc80..bc1adff7cc70 100644 --- a/shared/chat/conversation/bot/install.tsx +++ b/shared/chat/conversation/bot/install.tsx @@ -255,7 +255,7 @@ const InstallBotPopup = (props: Props) => { const dispatchClearWaiting = C.Waiting.useDispatchClearWaiting() const loadBotPublicCommands = C.useRPC(T.RPCChat.localListPublicBotCommandsLocalRpcPromise) const botPublicCommandsRequestIDRef = React.useRef(0) - const clearedWaitingForBotRef = React.useRef() + const clearedWaitingForBotRef = React.useRef(undefined) React.useEffect(() => { setBotPublicCommands(undefined) }, [botUsername]) diff --git a/shared/chat/inbox/index.d.ts b/shared/chat/inbox/index.d.ts index 40f155cf43b3..760c639820a8 100644 --- a/shared/chat/inbox/index.d.ts +++ b/shared/chat/inbox/index.d.ts @@ -1,9 +1,10 @@ import type * as React from 'react' -import type {ConversationIDKey} from '@/constants/types/chat' +import type {ChatRootInboxRefresh, ConversationIDKey} from '@/constants/types/chat' import type {InboxSearchController} from './use-inbox-search' type Props = { conversationIDKey?: ConversationIDKey + refreshInbox?: ChatRootInboxRefresh search?: InboxSearchController } diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 554edc871db8..8f6bd41f608e 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -380,10 +380,12 @@ export const leaveConversation = ( navToInbox = true ) => { ignorePromise( - T.RPCChat.localLeaveConversationLocalRpcPromise( - {convID: T.Chat.keyToConversationID(conversationIDKey)}, - Strings.waitingKeyChatLeaveConversation - ) + (async () => { + await T.RPCChat.localLeaveConversationLocalRpcPromise( + {convID: T.Chat.keyToConversationID(conversationIDKey)}, + Strings.waitingKeyChatLeaveConversation + ) + })() ) clearModals() if (!navToInbox) { diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 09489ad13cfb..7456a0d199d3 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -3,7 +3,6 @@ import * as TeamsUtil from '@/constants/teams' import * as PlatformSpecific from '@/util/platform-specific' import { - clearModals, createConversation, getTab, navigateAppend, @@ -3122,15 +3121,6 @@ const createSlice = } ignorePromise(f()) }, - prepareToNavigateToThread: highlightMessageID => { - set(s => { - // force loaded if we're an error - if (s.id === T.Chat.pendingErrorConversationIDKey) { - s.loaded = true - } - s.pendingJumpMessageID = highlightMessageID - }) - }, onEngineIncoming: action => { switch (action.type) { case 'chat.1.NotifyChat.ChatAttachmentDownloadComplete': { @@ -3200,6 +3190,15 @@ const createSlice = default: } }, + prepareToNavigateToThread: highlightMessageID => { + set(s => { + // force loaded if we're an error + if (s.id === T.Chat.pendingErrorConversationIDKey) { + s.loaded = true + } + s.pendingJumpMessageID = highlightMessageID + }) + }, onIncomingMessage: incoming => { const {message: cMsg} = incoming const {username, devicename} = getCurrentUser() From bad8df0d66b460cfb413bfde0116a1da4b6ca6f5 Mon Sep 17 00:00:00 2001 From: Chris Nojima Date: Thu, 16 Apr 2026 21:55:19 -0400 Subject: [PATCH 08/10] WIP --- shared/chat/blocking/invitation-to-block.tsx | 13 +++++++------ shared/stores/convostate.tsx | 18 +++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/shared/chat/blocking/invitation-to-block.tsx b/shared/chat/blocking/invitation-to-block.tsx index 3eddddeb4880..5914a2799b77 100644 --- a/shared/chat/blocking/invitation-to-block.tsx +++ b/shared/chat/blocking/invitation-to-block.tsx @@ -23,6 +23,13 @@ const BlockButtons = () => { !!currentUser && [...(s.messageOrdinals ?? [])].some(ordinal => s.messageMap.get(ordinal)?.author === currentUser) ) const dismissBlockButtons = Chat.useChatState(s => s.dispatch.dismissBlockButtons) + + React.useEffect(() => { + if (hasOwnMessage && blockButtonInfo && teamID) { + dismissBlockButtons(teamID) + } + }, [blockButtonInfo, dismissBlockButtons, hasOwnMessage, teamID]) + if (!blockButtonInfo) { return null } @@ -46,12 +53,6 @@ const BlockButtons = () => { }) const onDismiss = () => dismissBlockButtons(teamID) - React.useEffect(() => { - if (hasOwnMessage) { - dismissBlockButtons(teamID) - } - }, [dismissBlockButtons, hasOwnMessage, teamID]) - const buttonRow = ( { - set(s => { - // force loaded if we're an error - if (s.id === T.Chat.pendingErrorConversationIDKey) { - s.loaded = true - } - s.pendingJumpMessageID = highlightMessageID - }) - }, onIncomingMessage: incoming => { const {message: cMsg} = incoming const {username, devicename} = getCurrentUser() @@ -3269,6 +3260,15 @@ const createSlice = messagesAdd([message], {incomingMessage: true, why: 'incoming general'}) } }, + prepareToNavigateToThread: highlightMessageID => { + set(s => { + // force loaded if we're an error + if (s.id === T.Chat.pendingErrorConversationIDKey) { + s.loaded = true + } + s.pendingJumpMessageID = highlightMessageID + }) + }, onMessageErrored: (outboxID, reason, errorTyp) => { set(s => { const ordinal = s.pendingOutboxToOrdinal.get(outboxID) From f4c1ce9abde9240339fd33d82681d1e8322c8235 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Thu, 16 Apr 2026 21:56:07 -0400 Subject: [PATCH 09/10] WIP --- shared/stores/convostate.tsx | 47 +++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx index 05616e559015..2763387e7409 100644 --- a/shared/stores/convostate.tsx +++ b/shared/stores/convostate.tsx @@ -451,7 +451,9 @@ const updateInboxParticipants = (inboxUIItems: ReadonlyArray 0) { - getConvoState(T.Chat.stringToConversationIDKey(inboxUIItem.convID)).dispatch.setParticipants(participantInfo) + getConvoState(T.Chat.stringToConversationIDKey(inboxUIItem.convID)).dispatch.setParticipants( + participantInfo + ) } }) } @@ -705,7 +707,9 @@ export const unboxRows = (ids: ReadonlyArray, force?: if (!conversationIDKeys.length) { return } - logger.info(`unboxRows: unboxing len: ${conversationIDKeys.length} convs: ${conversationIDKeys.join(',')}`) + logger.info( + `unboxRows: unboxing len: ${conversationIDKeys.length} convs: ${conversationIDKeys.join(',')}` + ) try { await T.RPCChat.localRequestInboxUnboxRpcPromise({ convIDs: conversationIDKeys.map(k => T.Chat.keyToConversationID(k)), @@ -778,10 +782,7 @@ export const onGetInboxUnverifiedConvs = ( metasReceived(metas) } -export const onInboxLayoutChanged = ( - inboxLayout: T.RPCChat.UIInboxLayout, - hadInboxLoaded: boolean -) => { +export const onInboxLayoutChanged = (inboxLayout: T.RPCChat.UIInboxLayout, hadInboxLoaded: boolean) => { maybeChangeSelectedConversation(inboxLayout) ensureWidgetMetas(inboxLayout.widgetList) if (!hadInboxLoaded) { @@ -818,7 +819,9 @@ export const onChatInboxSynced = async ( } unboxRows( - items.filter(item => item.shouldUnbox).map(item => T.Chat.stringToConversationIDKey(item.conv.convID)), + items + .filter(item => item.shouldUnbox) + .map(item => T.Chat.stringToConversationIDKey(item.conv.convID)), true ) return @@ -884,7 +887,8 @@ type ConvoEngineIncomingResult = { userReacjis?: T.RPCGen.UserReacjis } -type NewChatActivity = EngineGen.EngineAction<'chat.1.NotifyChat.NewChatActivity'>['payload']['params']['activity'] +type NewChatActivity = + EngineGen.EngineAction<'chat.1.NotifyChat.NewChatActivity'>['payload']['params']['activity'] type ThreadStaleUpdates = EngineGen.EngineAction<'chat.1.NotifyChat.ChatThreadsStale'>['payload']['params']['updates'] @@ -901,7 +905,9 @@ const onChatThreadsStale = (updates: ThreadStaleUpdates) => { } } const selectedConversation = Common.getSelectedConversation() - const shouldLoadMore = (updates ?? []).some(u => T.Chat.conversationIDToKey(u.convID) === selectedConversation) + const shouldLoadMore = (updates ?? []).some( + u => T.Chat.conversationIDToKey(u.convID) === selectedConversation + ) keys.forEach(key => { const conversationIDKeys = (updates ?? []).reduce>((arr, u) => { const conversationIDKey = T.Chat.conversationIDToKey(u.convID) @@ -1113,7 +1119,10 @@ export const handleConvoEngineIncoming = ( onChatThreadsStale(action.payload.params.updates) return handledConvoEngineIncoming() case 'chat.1.NotifyChat.ChatSubteamRename': - unboxRows((action.payload.params.convs ?? []).map(c => T.Chat.stringToConversationIDKey(c.convID)), true) + unboxRows( + (action.payload.params.convs ?? []).map(c => T.Chat.stringToConversationIDKey(c.convID)), + true + ) return handledConvoEngineIncoming() case 'chat.1.NotifyChat.ChatTLFFinalize': unboxRows([T.Chat.conversationIDToKey(action.payload.params.convID)]) @@ -3260,15 +3269,6 @@ const createSlice = messagesAdd([message], {incomingMessage: true, why: 'incoming general'}) } }, - prepareToNavigateToThread: highlightMessageID => { - set(s => { - // force loaded if we're an error - if (s.id === T.Chat.pendingErrorConversationIDKey) { - s.loaded = true - } - s.pendingJumpMessageID = highlightMessageID - }) - }, onMessageErrored: (outboxID, reason, errorTyp) => { set(s => { const ordinal = s.pendingOutboxToOrdinal.get(outboxID) @@ -3329,6 +3329,15 @@ const createSlice = } ignorePromise(f()) }, + prepareToNavigateToThread: highlightMessageID => { + set(s => { + // force loaded if we're an error + if (s.id === T.Chat.pendingErrorConversationIDKey) { + s.loaded = true + } + s.pendingJumpMessageID = highlightMessageID + }) + }, refreshBotRoleInConv: username => { const f = async () => { let role: T.RPCGen.TeamRole | undefined From c65d50f82c4b0ebe54a796d16fa9661f44e0ade6 Mon Sep 17 00:00:00 2001 From: chrisnojima-zoom <83838430+chrisnojima-zoom@users.noreply.github.com> Date: Fri, 17 Apr 2026 08:42:57 -0400 Subject: [PATCH 10/10] tb cleanup part 2 (#29155) * remove registry (#29156) --- shared/chat/routes.tsx | 11 +- shared/common-adapters/wave-button.tsx | 3 +- shared/constants/chat/meta.tsx | 9 +- shared/constants/init/shared.tsx | 136 +++++------------- shared/constants/router.tsx | 10 +- shared/crypto/routes.tsx | 34 +++-- .../renderer/remote-event-handler.desktop.tsx | 22 +-- shared/login/reset/account-reset.test.ts | 4 +- shared/login/reset/account-reset.tsx | 4 +- shared/stores/store-registry.tsx | 133 ----------------- shared/stores/team-building.tsx | 69 ++------- shared/stores/tests/team-building.test.ts | 27 ---- shared/team-building/container.tsx | 12 +- shared/team-building/index.tsx | 26 ++-- shared/team-building/page.tsx | 88 ++++++++++-- shared/teams/routes.tsx | 16 ++- 16 files changed, 230 insertions(+), 374 deletions(-) delete mode 100644 shared/stores/store-registry.tsx diff --git a/shared/chat/routes.tsx b/shared/chat/routes.tsx index d6fba32a3f11..a1f92ce65f83 100644 --- a/shared/chat/routes.tsx +++ b/shared/chat/routes.tsx @@ -6,6 +6,7 @@ import {makeChatScreen} from './make-chat-screen' import * as FS from '@/constants/fs' import type * as T from '@/constants/types' import chatNewChat from '../team-building/page' +import {TeamBuilderScreen} from '../team-building/page' import {headerNavigationOptions} from './conversation/header-area' import {useConfigState} from '@/stores/config' import {useModalHeaderState} from '@/stores/modal-header' @@ -15,6 +16,7 @@ import inboxAndConvoGetOptions from './inbox-and-conversation-get-options' import {defineRouteMap} from '@/constants/types/router' import type {BlockModalContext} from './blocking/block-modal' import type {ChatRootRouteParams} from './inbox-and-conversation' +import {onTeamBuildingFinished} from '@/stores/convostate' const Convo = React.lazy(async () => import('./conversation/container')) type ChatBlockingRouteParams = { @@ -40,6 +42,10 @@ const emptyChatSearchBotsRouteParams: ChatSearchBotsRouteParams = {} const emptyChatShowNewTeamDialogRouteParams: ChatShowNewTeamDialogRouteParams = {} const emptyChatRootRouteParams: ChatRootRouteParams = {} +const ChatTeamBuilderScreen = (p: Parameters[0]) => ( + +) + const PDFShareButton = ({url}: {url?: string}) => { const showShareActionSheet = useConfigState(s => s.dispatch.defer.showShareActionSheet) return ( @@ -267,7 +273,10 @@ export const newModalRoutes = defineRouteMap({ return {default: MessagePopupModal} }) ), - chatNewChat, + chatNewChat: { + ...chatNewChat, + screen: ChatTeamBuilderScreen, + }, chatPDF: makeChatScreen( React.lazy(async () => import('./pdf')), { diff --git a/shared/common-adapters/wave-button.tsx b/shared/common-adapters/wave-button.tsx index 19d5d5691f11..612b5925862b 100644 --- a/shared/common-adapters/wave-button.tsx +++ b/shared/common-adapters/wave-button.tsx @@ -9,7 +9,6 @@ import NativeEmoji from './emoji/native-emoji' import * as Styles from '@/styles' import * as T from '@/constants/types' import logger from '@/logger' -import {storeRegistry} from '@/stores/store-registry' import {useCurrentUserState} from '@/stores/current-user' const Kb = { @@ -86,7 +85,7 @@ const WaveButtonImpl = (props: Props) => { logger.warn("WaveButton: couldn't resolve wave conversation") return } - storeRegistry.getConvoState(conversationIDKey).dispatch.sendMessage(':wave:') + ConvoState.getConvoState(conversationIDKey).dispatch.sendMessage(':wave:') }, error => { logger.warn('Could not send in WaveButton', error.message) diff --git a/shared/constants/chat/meta.tsx b/shared/constants/chat/meta.tsx index 350f7e7f2719..75760e97a2a1 100644 --- a/shared/constants/chat/meta.tsx +++ b/shared/constants/chat/meta.tsx @@ -4,9 +4,14 @@ import * as T from '@/constants/types' import * as Teams from '@/constants/teams' import * as Message from './message' import {base64ToUint8Array, uint8ArrayToHex} from '@/util/uint8array' -import {storeRegistry} from '@/stores/store-registry' +import type * as ConvoStateType from '@/stores/convostate' import {useCurrentUserState} from '@/stores/current-user' +const getConvoState = (conversationIDKey: T.Chat.ConversationIDKey) => { + const {getConvoState} = require('@/stores/convostate') as typeof ConvoStateType + return getConvoState(conversationIDKey) +} + const conversationMemberStatusToMembershipType = (m: T.RPCChat.ConversationMemberStatus) => { switch (m) { case T.RPCChat.ConversationMemberStatus.active: @@ -267,7 +272,7 @@ export const inboxUIItemToConversationMeta = ( const username = useCurrentUserState.getState().username const devicename = useCurrentUserState.getState().deviceName const getLastOrdinal = () => - storeRegistry.getConvoState(conversationIDKey).messageOrdinals?.at(-1) ?? T.Chat.numberToOrdinal(0) + getConvoState(conversationIDKey).messageOrdinals?.at(-1) ?? T.Chat.numberToOrdinal(0) const message = Message.uiMessageToMessage( conversationIDKey, i.pinnedMsg.message, diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx index 77b83f3f6577..cc8c7c77a94f 100644 --- a/shared/constants/init/shared.tsx +++ b/shared/constants/init/shared.tsx @@ -28,13 +28,11 @@ import type * as UseTeamsStateType from '@/stores/teams' import type * as UseTracker2StateType from '@/stores/tracker' import type * as UnlockFoldersType from '@/stores/unlock-folders' import type * as UseUsersStateType from '@/stores/users' -import {createTBStore, getTBStore} from '@/stores/team-building' +import {getTBStore} from '@/stores/team-building' import {getSelectedConversation} from '@/constants/chat/common' -import * as CryptoRoutes from '@/constants/crypto' import {emitDeepLink} from '@/router-v2/linking' import {ignorePromise} from '../utils' import {isMobile, isPhone, serverConfigFileName} from '../platform' -import {storeRegistry} from '@/stores/store-registry' import {useAvatarState} from '@/common-adapters/avatar/store' import {useChatState} from '@/stores/chat' import {useConfigState} from '@/stores/config' @@ -42,14 +40,20 @@ import {useCurrentUserState} from '@/stores/current-user' import {useDaemonState} from '@/stores/daemon' import {useDarkModeState} from '@/stores/darkmode' import {useFollowerState} from '@/stores/followers' +import {useFSState} from '@/stores/fs' import {useModalHeaderState} from '@/stores/modal-header' +import {usePeopleState} from '@/stores/people' import {useProvisionState} from '@/stores/provision' +import {useSettingsEmailState} from '@/stores/settings-email' +import {useSettingsPhoneState} from '@/stores/settings-phone' import {useSettingsContactsState} from '@/stores/settings-contacts' import {useTeamsState} from '@/stores/teams' +import {useUsersState} from '@/stores/users' import {useWaitingState} from '@/stores/waiting' import {useRouterState} from '@/stores/router' import * as Util from '@/constants/router' import { + getConvoState, onChatInboxSynced, onGetInboxConvsUnboxed, onGetInboxUnverifiedConvs, @@ -57,7 +61,6 @@ import { onIncomingInboxUIItem, handleConvoEngineIncoming, onRouteChanged as onConvoRouteChanged, - onTeamBuildingFinished as onConvoTeamBuildingFinished, syncBadgeState, syncGregorExplodingModes, } from '@/stores/convostate' @@ -90,7 +93,7 @@ export const onEngineConnected = () => { ignorePromise(registerUIs()) } useConfigState.getState().dispatch.onEngineConnected() - storeRegistry.getState('daemon').dispatch.startHandshake() + useDaemonState.getState().dispatch.startHandshake() { const notifyCtl = async () => { try { @@ -120,61 +123,7 @@ export const onEngineDisconnected = () => { await logger.dump() } ignorePromise(f()) - storeRegistry.getState('daemon').dispatch.setError(new Error('Disconnected')) -} - -// Initialize team building callbacks. Not ideal but keeping all the existing logic for now. -export const initTeamBuildingCallbacks = () => { - const commonCallbacks = { - onAddMembersWizardPushMembers: (members: Array) => { - useTeamsState.getState().dispatch.addMembersWizardPushMembers(members) - }, - } - - const namespaces: Array = ['chat', 'crypto', 'teams', 'people'] - for (const namespace of namespaces) { - const store = createTBStore(namespace) - const currentState = store.getState() - store.setState({ - dispatch: { - ...currentState.dispatch, - defer: { - ...currentState.dispatch.defer, - ...commonCallbacks, - ...(namespace === 'chat' - ? { - onFinishedTeamBuildingChat: users => { - onConvoTeamBuildingFinished(users) - }, - } - : {}), - ...(namespace === 'crypto' - ? { - onFinishedTeamBuildingCrypto: users => { - const visible = Util.getVisibleScreen() - const visibleParams = - visible?.name === 'cryptoTeamBuilder' - ? (visible.params as {teamBuilderNonce?: string} | undefined) - : undefined - const teamBuilderUsers = [...users].map(({serviceId, username}) => ({serviceId, username})) - Util.clearModals() - Util.navigateAppend( - { - name: CryptoRoutes.encryptTab, - params: { - teamBuilderNonce: visibleParams?.teamBuilderNonce, - teamBuilderUsers, - }, - }, - true - ) - }, - } - : {}), - }, - }, - }) - } + useDaemonState.getState().dispatch.setError(new Error('Disconnected')) } export const initSharedSubscriptions = () => { @@ -236,45 +185,41 @@ export const initSharedSubscriptions = () => { // Re-get info about our account if you log in/we're done handshaking/became reachable if (s.gregorReachable === T.RPCGen.Reachable.yes) { // not in waiting state - if (storeRegistry.getState('daemon').handshakeWaiters.size === 0) { - ignorePromise(storeRegistry.getState('daemon').dispatch.loadDaemonBootstrapStatus()) + if (useDaemonState.getState().handshakeWaiters.size === 0) { + ignorePromise(useDaemonState.getState().dispatch.loadDaemonBootstrapStatus()) } - storeRegistry.getState('teams').dispatch.eagerLoadTeams() + useTeamsState.getState().dispatch.eagerLoadTeams() } } if (s.installerRanCount !== old.installerRanCount) { - storeRegistry.getState('fs').dispatch.checkKbfsDaemonRpcStatus() + useFSState.getState().dispatch.checkKbfsDaemonRpcStatus() } if (s.loggedIn !== old.loggedIn) { if (s.loggedIn) { - ignorePromise(storeRegistry.getState('daemon').dispatch.loadDaemonBootstrapStatus()) - storeRegistry.getState('fs').dispatch.checkKbfsDaemonRpcStatus() + ignorePromise(useDaemonState.getState().dispatch.loadDaemonBootstrapStatus()) + useFSState.getState().dispatch.checkKbfsDaemonRpcStatus() } else { clearSignupEmail() clearSignupDeviceNameDraft() } - storeRegistry - .getState('daemon') - .dispatch.loadDaemonAccounts( - s.configuredAccounts.length, - s.loggedIn, - useConfigState.getState().dispatch.refreshAccounts - ) + useDaemonState.getState().dispatch.loadDaemonAccounts( + s.configuredAccounts.length, + s.loggedIn, + useConfigState.getState().dispatch.refreshAccounts + ) if (!s.loggedInCausedbyStartup) { ignorePromise(useConfigState.getState().dispatch.refreshAccounts()) } } if (s.revokedTrigger !== old.revokedTrigger) { - storeRegistry - .getState('daemon') - .dispatch.loadDaemonAccounts( - s.configuredAccounts.length, - s.loggedIn, - useConfigState.getState().dispatch.refreshAccounts - ) + useDaemonState.getState().dispatch.loadDaemonAccounts( + s.configuredAccounts.length, + s.loggedIn, + useConfigState.getState().dispatch.refreshAccounts + ) } if (s.configuredAccounts !== old.configuredAccounts) { @@ -283,12 +228,12 @@ export const initSharedSubscriptions = () => { name: account.username, })) if (updates.length > 0) { - storeRegistry.getState('users').dispatch.updates(updates) + useUsersState.getState().dispatch.updates(updates) } } if (s.active !== old.active) { - const cs = storeRegistry.getConvoState(getSelectedConversation()) + const cs = getConvoState(getSelectedConversation()) cs.dispatch.markThreadAsRead() } }) @@ -298,7 +243,7 @@ export const initSharedSubscriptions = () => { useDaemonState.subscribe((s, old) => { if (s.handshakeVersion !== old.handshakeVersion) { useDarkModeState.getState().dispatch.loadDarkPrefs() - storeRegistry.getState('chat').dispatch.loadStaticConfig() + useChatState.getState().dispatch.loadStaticConfig() const configState = useConfigState.getState() s.dispatch.loadDaemonAccounts( configState.configuredAccounts.length, @@ -326,7 +271,7 @@ export const initSharedSubscriptions = () => { configDispatch.setHTTPSrvInfo(bootstrap.httpSrvInfo.address, bootstrap.httpSrvInfo.token) } - storeRegistry.getState('chat').dispatch.updateUserReacjis(userReacjis) + useChatState.getState().dispatch.updateUserReacjis(userReacjis) } } @@ -388,16 +333,16 @@ export const initSharedSubscriptions = () => { Util.getTab(prev) === Tabs.fsTab && next && Util.getTab(next) !== Tabs.fsTab && - storeRegistry.getState('fs').criticalUpdate + useFSState.getState().criticalUpdate ) { - const {dispatch} = storeRegistry.getState('fs') + const {dispatch} = useFSState.getState() dispatch.setCriticalUpdate(false) } const fsRrouteNames = ['fsRoot', 'barePreview'] const wasScreen = fsRrouteNames.includes(Util.getVisibleScreen(prev)?.name ?? '') const isScreen = fsRrouteNames.includes(Util.getVisibleScreen(next)?.name ?? '') if (wasScreen !== isScreen) { - const {dispatch} = storeRegistry.getState('fs') + const {dispatch} = useFSState.getState() if (wasScreen) { dispatch.userOut() } else { @@ -411,11 +356,11 @@ export const initSharedSubscriptions = () => { } if (prev && Util.getTab(prev) === Tabs.peopleTab && next && Util.getTab(next) !== Tabs.peopleTab) { - storeRegistry.getState('people').dispatch.markViewed() + usePeopleState.getState().dispatch.markViewed() } if (prev && Util.getTab(prev) === Tabs.teamsTab && next && Util.getTab(next) !== Tabs.teamsTab) { - storeRegistry.getState('teams').dispatch.clearNavBadges() + useTeamsState.getState().dispatch.clearNavBadges() } // Clear "check your inbox" in settings when you leave the settings tab @@ -424,15 +369,14 @@ export const initSharedSubscriptions = () => { Util.getTab(prev) === Tabs.settingsTab && next && Util.getTab(next) !== Tabs.settingsTab && - storeRegistry.getState('settings-email').addedEmail + useSettingsEmailState.getState().addedEmail ) { - storeRegistry.getState('settings-email').dispatch.resetAddedEmail() + useSettingsEmailState.getState().dispatch.resetAddedEmail() } onConvoRouteChanged(prev, next) }) ) - initTeamBuildingCallbacks() } // This is to defer loading stores we don't need immediately. @@ -543,7 +487,7 @@ export const _onEngineIncoming = (action: EngineGen.Actions) => { { const emailAddress = action.payload.params.emailAddress if (emailAddress) { - storeRegistry.getState('settings-email').dispatch.notifyEmailVerified(emailAddress) + useSettingsEmailState.getState().dispatch.notifyEmailVerified(emailAddress) } clearSignupEmail() } @@ -563,14 +507,12 @@ export const _onEngineIncoming = (action: EngineGen.Actions) => { break case 'keybase.1.NotifyPhoneNumber.phoneNumbersChanged': { const {list} = action.payload.params - storeRegistry - .getState('settings-phone') - .dispatch.notifyPhoneNumberPhoneNumbersChanged(list ?? undefined) + useSettingsPhoneState.getState().dispatch.notifyPhoneNumberPhoneNumbersChanged(list ?? undefined) break } case 'keybase.1.NotifyEmailAddress.emailsChanged': { const list = action.payload.params.list ?? [] - storeRegistry.getState('settings-email').dispatch.notifyEmailAddressEmailsChanged(list) + useSettingsEmailState.getState().dispatch.notifyEmailAddressEmailsChanged(list) break } case 'chat.1.chatUi.chatInboxFailed': diff --git a/shared/constants/router.tsx b/shared/constants/router.tsx index 8f6bd41f608e..0c13e3cdd193 100644 --- a/shared/constants/router.tsx +++ b/shared/constants/router.tsx @@ -22,7 +22,6 @@ import {isMobile} from './platform' import {ignorePromise, shallowEqual} from './utils' import {registerDebugClear} from '@/util/debug' import {makeUUID} from '@/util/uuid' -import {storeRegistry} from '@/stores/store-registry' import * as Meta from './chat/meta' import * as Strings from './strings' import logger from '@/logger' @@ -86,6 +85,11 @@ const uiParticipantsToParticipantInfo = ( return participantInfo } +const getConvoState = (conversationIDKey: T.Chat.ConversationIDKey) => { + const {getConvoState} = require('@/stores/convostate') as typeof ConvoStateType + return getConvoState(conversationIDKey) +} + export const getRootState = (): NavState | undefined => { if (!navigationRef.isReady()) return return navigationRef.getRootState() @@ -435,7 +439,7 @@ export const createConversation = ( const participantInfo = uiParticipantsToParticipantInfo(uiConv.participants ?? []) if (participantInfo.all.length > 0) { - storeRegistry.getConvoState(conversationIDKey).dispatch.setParticipants(participantInfo) + getConvoState(conversationIDKey).dispatch.setParticipants(participantInfo) } navigateToThread(conversationIDKey, 'justCreated', highlightMessageID) @@ -711,7 +715,7 @@ export const navigateToThread = ( threadSearchQuery?: string, createConversationError?: T.Chat.CreateConversationError ) => { - storeRegistry.getConvoState(conversationIDKey).dispatch.prepareToNavigateToThread(highlightMessageID) + getConvoState(conversationIDKey).dispatch.prepareToNavigateToThread(highlightMessageID) if (reason === 'navChanged') { return diff --git a/shared/crypto/routes.tsx b/shared/crypto/routes.tsx index 782e2402ddab..4911a1156b07 100644 --- a/shared/crypto/routes.tsx +++ b/shared/crypto/routes.tsx @@ -3,6 +3,7 @@ import * as C from '@/constants' import * as Crypto from '@/constants/crypto' import {HeaderLeftButton, type HeaderBackButtonProps} from '@/common-adapters/header-buttons' import cryptoTeamBuilder from '../team-building/page' +import {TeamBuilderScreen} from '../team-building/page' import type {StaticScreenProps} from '@react-navigation/core' import {defineRouteMap} from '@/constants/types/router' import type { @@ -72,16 +73,29 @@ const VerifyOutputScreen = React.lazy(async () => { } }) -const CryptoTeamBuilderScreen = React.lazy(async () => { - const {default: teamBuilder} = await import('../team-building/page') - const TeamBuilderScreen = teamBuilder.screen - return { - default: (p: StaticScreenProps) => { - const {teamBuilderNonce: _teamBuilderNonce, teamBuilderUsers: _teamBuilderUsers, ...params} = p.route.params - return - }, - } -}) +const CryptoTeamBuilderScreen = (p: StaticScreenProps) => { + const {teamBuilderNonce, teamBuilderUsers: _teamBuilderUsers, ...params} = p.route.params + return ( + { + const nextTeamBuilderUsers = [...users].map(({serviceId, username}) => ({serviceId, username})) + C.Router2.clearModals() + C.Router2.navigateAppend( + { + name: Crypto.encryptTab, + params: { + teamBuilderNonce, + teamBuilderUsers: nextTeamBuilderUsers, + }, + }, + true + ) + }} + /> + ) +} export const newRoutes = defineRouteMap({ [Crypto.decryptTab]: { diff --git a/shared/desktop/renderer/remote-event-handler.desktop.tsx b/shared/desktop/renderer/remote-event-handler.desktop.tsx index 538dc0da2f1e..06461158f6d4 100644 --- a/shared/desktop/renderer/remote-event-handler.desktop.tsx +++ b/shared/desktop/renderer/remote-event-handler.desktop.tsx @@ -5,13 +5,15 @@ import * as Tabs from '@/constants/tabs' import {RPCError} from '@/util/errors' import {ignorePromise} from '@/constants/utils' import {navigateAppend, navigateToThread, previewConversation, switchTab} from '@/constants/router' -import {storeRegistry} from '@/stores/store-registry' import {onEngineConnected, onEngineDisconnected} from '@/constants/init/index.desktop' import {emitDeepLink} from '@/router-v2/linking' import {isPathSaltpackEncrypted, isPathSaltpackSigned} from '@/util/path' import type HiddenString from '@/util/hidden-string' +import {useChatState} from '@/stores/chat' import {useConfigState} from '@/stores/config' +import {useFSState} from '@/stores/fs' import {usePinentryState} from '@/stores/pinentry' +import {useTrackerState} from '@/stores/tracker' import logger from '@/logger' import {makeUUID} from '@/util/uuid' @@ -73,7 +75,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { break } case RemoteGen.inboxRefresh: { - ignorePromise(storeRegistry.getState('chat').dispatch.inboxRefresh('widgetRefresh')) + ignorePromise(useChatState.getState().dispatch.inboxRefresh('widgetRefresh')) break } case RemoteGen.engineConnection: { @@ -90,15 +92,15 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { break } case RemoteGen.setCriticalUpdate: { - storeRegistry.getState('fs').dispatch.setCriticalUpdate(action.payload.critical) + useFSState.getState().dispatch.setCriticalUpdate(action.payload.critical) break } case RemoteGen.userFileEditsLoad: { - storeRegistry.getState('fs').dispatch.userFileEditsLoad() + useFSState.getState().dispatch.userFileEditsLoad() break } case RemoteGen.openFilesFromWidget: { - storeRegistry.getState('fs').dispatch.defer.openFilesFromWidgetDesktop?.(action.payload.path) + useFSState.getState().dispatch.defer.openFilesFromWidgetDesktop?.(action.payload.path) break } case RemoteGen.saltpackFileOpen: { @@ -114,7 +116,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { break } case RemoteGen.openPathInSystemFileManager: { - storeRegistry.getState('fs').dispatch.defer.openPathInSystemFileManagerDesktop?.(action.payload.path) + useFSState.getState().dispatch.defer.openPathInSystemFileManagerDesktop?.(action.payload.path) break } case RemoteGen.unlockFoldersSubmitPaperKey: { @@ -142,19 +144,19 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { break } case RemoteGen.trackerChangeFollow: { - storeRegistry.getState('tracker').dispatch.changeFollow(action.payload.guiID, action.payload.follow) + useTrackerState.getState().dispatch.changeFollow(action.payload.guiID, action.payload.follow) break } case RemoteGen.trackerIgnore: { - storeRegistry.getState('tracker').dispatch.ignore(action.payload.guiID) + useTrackerState.getState().dispatch.ignore(action.payload.guiID) break } case RemoteGen.trackerCloseTracker: { - storeRegistry.getState('tracker').dispatch.closeTracker(action.payload.guiID) + useTrackerState.getState().dispatch.closeTracker(action.payload.guiID) break } case RemoteGen.trackerLoad: { - storeRegistry.getState('tracker').dispatch.load(action.payload) + useTrackerState.getState().dispatch.load(action.payload) break } case RemoteGen.link: diff --git a/shared/login/reset/account-reset.test.ts b/shared/login/reset/account-reset.test.ts index 5d6831f479dd..ee8c2f489b7d 100644 --- a/shared/login/reset/account-reset.test.ts +++ b/shared/login/reset/account-reset.test.ts @@ -13,8 +13,8 @@ jest.mock('@/constants/router', () => { } }) -jest.mock('@/stores/store-registry', () => ({ - storeRegistry: { +jest.mock('@/stores/provision', () => ({ + useProvisionState: { getState: jest.fn(() => ({ dispatch: { startProvision: mockStartProvision, diff --git a/shared/login/reset/account-reset.tsx b/shared/login/reset/account-reset.tsx index a56402cc4cd0..64391b2cc4f0 100644 --- a/shared/login/reset/account-reset.tsx +++ b/shared/login/reset/account-reset.tsx @@ -4,7 +4,7 @@ import * as T from '@/constants/types' import {ignorePromise} from '@/constants/utils' import logger from '@/logger' import {consumeKeyed, registerKeyed} from '@/stores/flow-handles' -import {storeRegistry} from '@/stores/store-registry' +import {useProvisionState} from '@/stores/provision' import {RPCError} from '@/util/errors' type EnterResetPipelineParams = { @@ -38,7 +38,7 @@ export const enterResetPipeline = ({onError, password = '', username}: EnterRese const resetKey = registerResetPrompt((action: T.RPCGen.ResetPromptResponse) => { response.result(action) if (action === T.RPCGen.ResetPromptResponse.confirmReset) { - storeRegistry.getState('provision').dispatch.startProvision(username, true) + useProvisionState.getState().dispatch.startProvision(username, true) } else { navUpToScreen('login') } diff --git a/shared/stores/store-registry.tsx b/shared/stores/store-registry.tsx deleted file mode 100644 index 1636bdd86945..000000000000 --- a/shared/stores/store-registry.tsx +++ /dev/null @@ -1,133 +0,0 @@ -// used to allow non-circular cross-calls between stores -// ONLY for zustand stores -import type * as T from '@/constants/types' -import type * as ConvoStateType from '@/stores/convostate' -import type {ConvoState} from '@/stores/convostate' -import type {State as ChatState, useChatState} from '@/stores/chat' -import type {State as DaemonState, useDaemonState} from '@/stores/daemon' -import type {State as FSState, useFSState} from '@/stores/fs' -import type {State as PeopleState, usePeopleState} from '@/stores/people' -import type {State as ProvisionState, useProvisionState} from '@/stores/provision' -import type {State as PushState, usePushState} from '@/stores/push' -import type { - State as RecoverPasswordState, - useState as useRecoverPasswordState, -} from '@/stores/recover-password' -import type {State as SettingsEmailState, useSettingsEmailState} from '@/stores/settings-email' -import type {State as SettingsPhoneState, useSettingsPhoneState} from '@/stores/settings-phone' -import type {State as TeamsState, useTeamsState} from '@/stores/teams' -import type {State as TrackerState, useTrackerState} from '@/stores/tracker' -import type {State as UsersState, useUsersState} from '@/stores/users' - -type StoreName = - | 'chat' - | 'daemon' - | 'fs' - | 'people' - | 'provision' - | 'push' - | 'recover-password' - | 'settings-email' - | 'settings-phone' - | 'teams' - | 'tracker' - | 'users' - -type StoreStates = { - chat: ChatState - daemon: DaemonState - fs: FSState - people: PeopleState - provision: ProvisionState - push: PushState - 'recover-password': RecoverPasswordState - 'settings-email': SettingsEmailState - 'settings-phone': SettingsPhoneState - teams: TeamsState - tracker: TrackerState - users: UsersState -} - -type StoreHooks = { - chat: typeof useChatState - daemon: typeof useDaemonState - fs: typeof useFSState - people: typeof usePeopleState - provision: typeof useProvisionState - push: typeof usePushState - 'recover-password': typeof useRecoverPasswordState - 'settings-email': typeof useSettingsEmailState - 'settings-phone': typeof useSettingsPhoneState - teams: typeof useTeamsState - tracker: typeof useTrackerState - users: typeof useUsersState -} - -class StoreRegistry { - getStore(storeName: T): StoreHooks[T] { - /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-return */ - switch (storeName) { - case 'chat': { - const {useChatState} = require('@/stores/chat') - return useChatState - } - case 'daemon': { - const {useDaemonState} = require('@/stores/daemon') - return useDaemonState - } - case 'fs': { - const {useFSState} = require('@/stores/fs') - return useFSState - } - case 'people': { - const {usePeopleState} = require('@/stores/people') - return usePeopleState - } - case 'provision': { - const {useProvisionState} = require('@/stores/provision') - return useProvisionState - } - case 'push': { - const {usePushState} = require('@/stores/push') - return usePushState - } - case 'recover-password': { - const {useState} = require('@/stores/recover-password') - return useState - } - case 'settings-email': { - const {useSettingsEmailState} = require('@/stores/settings-email') - return useSettingsEmailState - } - case 'settings-phone': { - const {useSettingsPhoneState} = require('@/stores/settings-phone') - return useSettingsPhoneState - } - case 'teams': { - const {useTeamsState} = require('@/stores/teams') - return useTeamsState - } - case 'tracker': { - const {useTrackerState} = require('@/stores/tracker') - return useTrackerState - } - case 'users': { - const {useUsersState} = require('@/stores/users') - return useUsersState - } - default: - throw new Error(`Unknown store: ${storeName}`) - } - } - - getState(storeName: T): StoreStates[T] { - return this.getStore(storeName).getState() as StoreStates[T] - } - - getConvoState(id: T.Chat.ConversationIDKey): ConvoState { - const {getConvoState} = require('@/stores/convostate') as typeof ConvoStateType - return getConvoState(id) - } -} - -export const storeRegistry = new StoreRegistry() diff --git a/shared/stores/team-building.tsx b/shared/stores/team-building.tsx index adea76255453..b4f98fafaed8 100644 --- a/shared/stores/team-building.tsx +++ b/shared/stores/team-building.tsx @@ -46,13 +46,7 @@ export type State = Store & { cancelTeamBuilding: () => void changeSendNotification: (sendNotification: boolean) => void closeTeamBuilding: () => void - defer: { - onAddMembersWizardPushMembers: (members: Array) => void - onFinishedTeamBuildingChat: (users: ReadonlySet) => void - onFinishedTeamBuildingCrypto: (users: ReadonlySet) => void - } fetchUserRecs: () => void - finishTeamBuilding: () => void finishedTeamBuilding: () => void removeUsersFromTeamSoFar: (users: Array) => void resetState: () => void @@ -252,6 +246,17 @@ const interestingPersonToUser = (person: T.RPCGen.InterestingPerson): T.TB.User } const createSlice: Z.ImmerStateCreator = (set, get) => { + const resetStatePreservingSelection = () => { + const {namespace, selectedRole, sendNotification, teamSoFar} = get() + set(() => ({ + ...initialStore, + namespace, + selectedRole, + sendNotification, + teamSoFar, + })) + } + const dispatch: State['dispatch'] = { addUsersToTeamSoFar: users => { set(s => { @@ -294,17 +299,6 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { navigateUp() } }, - defer: { - onAddMembersWizardPushMembers: (_members: Array) => { - throw new Error('onAddMembersWizardPushMembers not properly initialized') - }, - onFinishedTeamBuildingChat: (_users: ReadonlySet) => { - throw new Error('onFinishedTeamBuildingChat not properly initialized') - }, - onFinishedTeamBuildingCrypto: (_users: ReadonlySet) => { - throw new Error('onFinishedTeamBuildingCrypto not properly initialized') - }, - }, fetchUserRecs: () => { const includeContacts = get().namespace === 'chat' const f = async () => { @@ -335,47 +329,8 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { } ignorePromise(f()) }, - finishTeamBuilding: () => { - set(s => { - s.error = '' - }) - const {namespace, selectedRole, sendNotification, teamSoFar} = get() - if (namespace !== 'teams') { - get().dispatch.closeTeamBuilding() - return - } - const members = [...teamSoFar].map(user => ({assertion: user.id, role: 'writer'} as const)) - get().dispatch.closeTeamBuilding() - get().dispatch.defer.onAddMembersWizardPushMembers(members) - set(() => ({ - ...initialStore, - namespace, - selectedRole, - sendNotification, - teamSoFar, - })) - }, finishedTeamBuilding: () => { - const {teamSoFar, selectedRole, sendNotification, namespace} = get() - set(() => ({ - ...initialStore, - namespace, - selectedRole, - sendNotification, - teamSoFar, - })) - switch (namespace) { - case 'crypto': { - get().dispatch.defer.onFinishedTeamBuildingCrypto(teamSoFar) - break - } - case 'chat': { - get().dispatch.defer.onFinishedTeamBuildingChat(teamSoFar) - break - } - default: - } - get().dispatch.closeTeamBuilding() + resetStatePreservingSelection() }, removeUsersFromTeamSoFar: users => { set(s => { diff --git a/shared/stores/tests/team-building.test.ts b/shared/stores/tests/team-building.test.ts index 8196000f21b3..8f09a3f0b1ea 100644 --- a/shared/stores/tests/team-building.test.ts +++ b/shared/stores/tests/team-building.test.ts @@ -64,30 +64,3 @@ test('finishedTeamBuilding clears transient state but preserves namespace and se expect(store.getState().searchQuery).toBe('') expect(store.getState().searchResults.size).toBe(0) }) - -test('finishTeamBuilding snapshots selected members before close reset', () => { - const store = createTBStore('teams') - const alice = makeUser('alice') - const pushedMembers: Array> = [] - - store.setState(s => ({ - ...s, - dispatch: { - ...s.dispatch, - closeTeamBuilding: () => { - store.getState().dispatch.resetState() - }, - defer: { - ...s.dispatch.defer, - onAddMembersWizardPushMembers: members => { - pushedMembers.push(members) - }, - }, - }, - })) - - store.getState().dispatch.addUsersToTeamSoFar([alice]) - store.getState().dispatch.finishTeamBuilding() - - expect(pushedMembers).toEqual([[{assertion: 'alice', role: 'writer'}]]) -}) diff --git a/shared/team-building/container.tsx b/shared/team-building/container.tsx index 638180724d06..a5af7c716b0f 100644 --- a/shared/team-building/container.tsx +++ b/shared/team-building/container.tsx @@ -2,11 +2,17 @@ import type * as T from '@/constants/types' import TeamBuilding from '.' export default TeamBuilding -export type TeamBuilderProps = Partial<{ +type RouteParams = { namespace: T.TB.AllowedNamespace teamID?: string filterServices?: Array goButtonLabel?: T.TB.GoButtonLabel - title: string + title?: string recommendedHideYourself?: boolean -}> +} + +export type TeamBuilderRouteParams = RouteParams + +export type TeamBuilderProps = RouteParams & { + onFinishTeamBuilding?: () => void +} diff --git a/shared/team-building/index.tsx b/shared/team-building/index.tsx index ed6bc15e7c6d..a2e5e20496d2 100644 --- a/shared/team-building/index.tsx +++ b/shared/team-building/index.tsx @@ -53,6 +53,8 @@ const findUserById = (users: ReadonlyArray | undefined, userId: strin const shouldShowContactsBanner = (filterServices: ReadonlyArray | undefined) => Kb.Styles.isMobile && (!filterServices || filterServices.includes('phone')) +const noop = () => {} + const useTeamBuildingData = (searchString: string, selectedService: T.TB.ServiceIdWithContact) => { const {searchResults, error, rawTeamSoFar, userRecs} = TB.useTBContext( C.useShallow(s => ({ @@ -72,6 +74,7 @@ const useTeamBuildingData = (searchString: string, selectedService: T.TB.Service } const useTeamBuildingActions = ({ + onFinishTeamBuilding, namespace, searchString, selectedService, @@ -82,6 +85,7 @@ const useTeamBuildingActions = ({ setSearchString, setSelectedService, }: { + onFinishTeamBuilding?: () => void namespace: T.TB.AllowedNamespace searchString: string selectedService: T.TB.ServiceIdWithContact @@ -97,8 +101,6 @@ const useTeamBuildingActions = ({ cancelTeamBuilding, dispatchSearch, fetchUserRecs, - finishTeamBuilding, - finishedTeamBuilding, removeUsersFromTeamSoFar, } = TB.useTBContext( C.useShallow(s => ({ @@ -106,8 +108,6 @@ const useTeamBuildingActions = ({ cancelTeamBuilding: s.dispatch.cancelTeamBuilding, dispatchSearch: s.dispatch.search, fetchUserRecs: s.dispatch.fetchUserRecs, - finishTeamBuilding: s.dispatch.finishTeamBuilding, - finishedTeamBuilding: s.dispatch.finishedTeamBuilding, removeUsersFromTeamSoFar: s.dispatch.removeUsersFromTeamSoFar, })) ) @@ -157,7 +157,7 @@ const useTeamBuildingActions = ({ onAdd, onChangeService, onChangeText, - onFinishTeamBuilding: namespace === 'teams' ? finishTeamBuilding : finishedTeamBuilding, + onFinishTeamBuilding: onFinishTeamBuilding ?? noop, onRemove: (userId: string) => { removeUsersFromTeamSoFar([userId]) }, @@ -175,11 +175,18 @@ type OwnProps = { teamID?: string filterServices?: Array goButtonLabel?: T.TB.GoButtonLabel + onFinishTeamBuilding?: () => void title?: string recommendedHideYourself?: boolean } -const TeamBuilding = ({namespace, teamID, filterServices, goButtonLabel = 'Start'}: OwnProps) => { +const TeamBuilding = ({ + namespace, + teamID, + filterServices, + goButtonLabel = 'Start', + onFinishTeamBuilding: onFinishTeamBuildingProp, +}: OwnProps) => { const [focusInputCounter, setFocusInputCounter] = React.useState(0) const [enterInputCounter, setEnterInputCounter] = React.useState(0) const [highlightedIndex, setHighlightedIndex] = React.useState(0) @@ -205,12 +212,13 @@ const TeamBuilding = ({namespace, teamID, filterServices, goButtonLabel = 'Start onAdd, onChangeService, onChangeText, - onFinishTeamBuilding, + onFinishTeamBuilding: finishTeamBuilding, onRemove, onSearchForMore, search, } = useTeamBuildingActions({ namespace, + onFinishTeamBuilding: onFinishTeamBuildingProp, searchString, selectedService, setFocusInputCounter, @@ -287,7 +295,7 @@ const TeamBuilding = ({namespace, teamID, filterServices, goButtonLabel = 'Start teamSoFar={teamSoFar} onChangeText={onChangeText} onSearchForMore={onSearchForMore} - onFinishTeamBuilding={onFinishTeamBuilding} + onFinishTeamBuilding={finishTeamBuilding} /> {waitingForCreate && ( @@ -304,7 +312,7 @@ const TeamBuilding = ({namespace, teamID, filterServices, goButtonLabel = 'Start onDownArrowKeyDown={onDownArrowKeyDown} onUpArrowKeyDown={onUpArrowKeyDown} onEnterKeyDown={onEnterKeyDown} - onFinishTeamBuilding={onFinishTeamBuilding} + onFinishTeamBuilding={finishTeamBuilding} onRemove={onRemove} teamSoFar={teamSoFar} searchString={searchString} diff --git a/shared/team-building/page.tsx b/shared/team-building/page.tsx index 8465a8ffb82e..969675e9fadd 100644 --- a/shared/team-building/page.tsx +++ b/shared/team-building/page.tsx @@ -5,6 +5,7 @@ import * as React from 'react' import * as T from '@/constants/types' import * as C from '@/constants' import {ModalTitle as TeamsModalTitle} from '../teams/common' +import type {TeamBuilderRouteParams} from './container' import {TBProvider, useTBContext} from '@/stores/team-building' import {useModalHeaderState} from '@/stores/modal-header' @@ -50,27 +51,30 @@ const TBHeaderRight = ({ const HeaderRightUpdater = ({ namespace, goButtonLabel, + onFinishTeamBuilding, }: { namespace: T.TB.AllowedNamespace goButtonLabel?: string + onFinishTeamBuilding?: () => void }) => { const navigation = useNavigation() const hasTeamSoFar = useTBContext(s => s.teamSoFar.size > 0) - const finishTeamBuilding = useTBContext(s => s.dispatch.finishTeamBuilding) - const finishedTeamBuilding = useTBContext(s => s.dispatch.finishedTeamBuilding) React.useEffect(() => { if (!Kb.Styles.isMobile) return if (namespace !== 'teams' && namespace !== 'chat' && namespace !== 'crypto') return - const onFinish = namespace === 'teams' ? finishTeamBuilding : finishedTeamBuilding + const enabled = hasTeamSoFar && !!onFinishTeamBuilding + if (!onFinishTeamBuilding) { + useModalHeaderState.setState({actionEnabled: false, onAction: undefined}) + } if (Kb.Styles.isIOS) { const label = namespace === 'teams' ? 'Add' : (goButtonLabel ?? 'Start') navigation.setOptions({ - unstable_headerRightItems: hasTeamSoFar - ? () => [{label, onPress: onFinish, type: 'button' as const}] + unstable_headerRightItems: enabled + ? () => [{label, onPress: onFinishTeamBuilding, type: 'button' as const}] : () => [], } as object) } else { - useModalHeaderState.setState({actionEnabled: hasTeamSoFar, onAction: onFinish}) + useModalHeaderState.setState({actionEnabled: enabled, onAction: onFinishTeamBuilding}) } return () => { if (Kb.Styles.isIOS) { @@ -79,20 +83,33 @@ const HeaderRightUpdater = ({ useModalHeaderState.setState({actionEnabled: false, onAction: undefined}) } } - }, [namespace, hasTeamSoFar, finishTeamBuilding, finishedTeamBuilding, goButtonLabel, navigation]) + }, [namespace, hasTeamSoFar, goButtonLabel, navigation, onFinishTeamBuilding]) return null } // Calls resetState when the screen is removed (e.g. default cancel button pressed) -const CancelOnRemove = () => { +const CancelOnRemove = ({ + skipResetOnRemoveRef, +}: { + skipResetOnRemoveRef: React.MutableRefObject +}) => { const navigation = useNavigation() const resetState = useTBContext(s => s.dispatch.resetState) - React.useEffect(() => navigation.addListener('beforeRemove', resetState), [navigation, resetState]) + React.useEffect( + () => + navigation.addListener('beforeRemove', () => { + if (skipResetOnRemoveRef.current) return + resetState() + }), + [navigation, resetState, skipResetOnRemoveRef] + ) return null } const styles = Kb.Styles.styleSheetCreate(() => ({hide: {opacity: 0}}) as const) +type OwnProps = StaticScreenProps + const getOptions = ({route}: OwnProps) => { const namespace = route.params.namespace const title = typeof route.params.title === 'string' ? route.params.title : '' @@ -142,17 +159,58 @@ const getOptions = ({route}: OwnProps) => { } const Building = React.lazy(async () => import('./container')) -type OwnProps = StaticScreenProps> +export type TeamBuilderScreenProps = StaticScreenProps & { + onComplete?: (users: ReadonlySet) => void +} + +const ScreenBody = ({ + onComplete, + routeParams, +}: { + onComplete?: (users: ReadonlySet) => void + routeParams: TeamBuilderRouteParams +}) => { + const {goButtonLabel, namespace} = routeParams + const teamSoFar = useTBContext(s => s.teamSoFar) + const closeTeamBuilding = useTBContext(s => s.dispatch.closeTeamBuilding) + const finishedTeamBuilding = useTBContext(s => s.dispatch.finishedTeamBuilding) + const skipResetOnRemoveRef = React.useRef(false) + + const onFinishTeamBuilding = React.useCallback(() => { + if (!teamSoFar.size) return + const users = new Set(teamSoFar) + skipResetOnRemoveRef.current = true + finishedTeamBuilding() + closeTeamBuilding() + onComplete?.(users) + setTimeout(() => { + skipResetOnRemoveRef.current = false + }, 0) + }, [closeTeamBuilding, finishedTeamBuilding, onComplete, teamSoFar]) + + return ( + <> + + + + + ) +} -const Screen = (p: OwnProps) => ( +export const TeamBuilderScreen = (p: TeamBuilderScreenProps) => ( - - - + ) export default { getOptions, - screen: Screen, + screen: TeamBuilderScreen, } diff --git a/shared/teams/routes.tsx b/shared/teams/routes.tsx index fb0597135f45..8ab0123e9f69 100644 --- a/shared/teams/routes.tsx +++ b/shared/teams/routes.tsx @@ -8,10 +8,21 @@ import {ModalTitle} from './common' import {HeaderLeftButton} from '@/common-adapters/header-buttons' import contactRestricted from '../team-building/contact-restricted.page' import teamsTeamBuilder from '../team-building/page' +import {TeamBuilderScreen} from '../team-building/page' import {useModalHeaderState} from '@/stores/modal-header' import teamsRootGetOptions from './get-options' import {defineRouteMap} from '@/constants/types/router' +const TeamsTeamBuilderScreen = (p: Parameters[0]) => ( + { + const members = [...users].map(user => ({assertion: user.id, role: 'writer'} as const)) + Teams.useTeamsState.getState().dispatch.addMembersWizardPushMembers(members) + }} + /> +) + const AddToChannelsHeaderTitle = ({teamID}: {teamID: T.Teams.TeamID}) => { const title = useModalHeaderState(s => s.title) return @@ -345,5 +356,8 @@ export const newModalRoutes = defineRouteMap({ headerTitle: () => , }, }), - teamsTeamBuilder, + teamsTeamBuilder: { + ...teamsTeamBuilder, + screen: TeamsTeamBuilderScreen, + }, })