diff --git a/shared/app/index.native.tsx b/shared/app/index.native.tsx index 4e2aae6b48eb..ba51cb977b5d 100644 --- a/shared/app/index.native.tsx +++ b/shared/app/index.native.tsx @@ -2,7 +2,7 @@ import * as C from '@/constants' import {useConfigState} from '@/constants/config' import * as Kb from '@/common-adapters' import * as React from 'react' -import {useDeepLinksState} from '@/constants/deeplinks' +import {handleAppLink} from '@/constants/deeplinks' import Main from './main.native' import {KeyboardProvider} from 'react-native-keyboard-controller' import Animated, {ReducedMotionConfig, ReduceMotion} from 'react-native-reanimated' @@ -18,9 +18,9 @@ import ServiceDecoration from '@/common-adapters/markdown/service-decoration' import {useUnmountAll} from '@/util/debug-react' import {darkModeSupported, guiConfig} from 'react-native-kb' import {install} from 'react-native-kb' -import {useEngineState} from '@/constants/engine' import * as DarkMode from '@/constants/darkmode' -import {initPlatformListener} from '@/constants/platform-specific' +import {onEngineConnected, onEngineDisconnected} from '@/constants/platform-specific/shared' +import {initPlatformListener} from '@/constants/init' enableFreeze(true) setServiceDecoration(ServiceDecoration) @@ -107,7 +107,6 @@ const StoreHelper = (p: {children: React.ReactNode}): React.ReactNode => { const {children} = p useDarkHookup() useKeyboardHookup() - const handleAppLink = useDeepLinksState(s => s.dispatch.handleAppLink) React.useEffect(() => { const linkingSub = Linking.addEventListener('url', ({url}: {url: string}) => { @@ -116,7 +115,7 @@ const StoreHelper = (p: {children: React.ReactNode}): React.ReactNode => { return () => { linkingSub.remove() } - }, [handleAppLink]) + }, []) return children } @@ -136,9 +135,9 @@ const useInit = () => { const {batch} = C.useWaitingState.getState().dispatch const eng = makeEngine(batch, c => { if (c) { - useEngineState.getState().dispatch.onEngineConnected() + onEngineConnected() } else { - useEngineState.getState().dispatch.onEngineDisconnected() + onEngineDisconnected() } }) initPlatformListener() diff --git a/shared/chat/conversation/input-area/location-popup.d.ts b/shared/chat/conversation/input-area/location-popup.d.ts new file mode 100644 index 000000000000..251868e41272 --- /dev/null +++ b/shared/chat/conversation/input-area/location-popup.d.ts @@ -0,0 +1,3 @@ +import type * as React from 'react' +declare const LocationPopup: () => React.ReactNode +export default LocationPopup diff --git a/shared/chat/conversation/input-area/location-popup.desktop.tsx b/shared/chat/conversation/input-area/location-popup.desktop.tsx new file mode 100644 index 000000000000..c07558b9f77f --- /dev/null +++ b/shared/chat/conversation/input-area/location-popup.desktop.tsx @@ -0,0 +1,2 @@ +const LocationPopup = () => null +export default LocationPopup diff --git a/shared/chat/conversation/input-area/location-popup.tsx b/shared/chat/conversation/input-area/location-popup.native.tsx similarity index 73% rename from shared/chat/conversation/input-area/location-popup.tsx rename to shared/chat/conversation/input-area/location-popup.native.tsx index fff570f758c7..1225ddfcd0eb 100644 --- a/shared/chat/conversation/input-area/location-popup.tsx +++ b/shared/chat/conversation/input-area/location-popup.native.tsx @@ -2,10 +2,52 @@ import * as C from '@/constants' import * as Chat from '@/constants/chat2' import * as React from 'react' import {useConfigState} from '@/constants/config' +import logger from '@/logger' import * as Kb from '@/common-adapters' import * as T from '@/constants/types' import LocationMap from '@/chat/location-map' import {useCurrentUserState} from '@/constants/current-user' +import {requestLocationPermission} from '@/constants/platform-specific' +import * as ExpoLocation from 'expo-location' + +const useWatchPosition = (conversationIDKey: T.Chat.ConversationIDKey) => { + const updateLastCoord = Chat.useChatState(s => s.dispatch.updateLastCoord) + const setCommandStatusInfo = Chat.useChatContext(s => s.dispatch.setCommandStatusInfo) + React.useEffect(() => { + let unsub = () => {} + logger.info('[location] perms check due to map') + const f = async () => { + try { + await requestLocationPermission(T.RPCChat.UIWatchPositionPerm.base) + const sub = await ExpoLocation.watchPositionAsync( + {accuracy: ExpoLocation.LocationAccuracy.Highest}, + (location: ExpoLocation.LocationObject) => { + const coord = { + accuracy: Math.floor(location.coords.accuracy ?? 0), + lat: location.coords.latitude, + lon: location.coords.longitude, + } + updateLastCoord(coord) + } + ) + unsub = () => sub.remove() + } catch (_error) { + const error = _error as {message?: string} + logger.info('failed to get location: ' + error.message) + setCommandStatusInfo({ + actions: [T.RPCChat.UICommandStatusActionTyp.appsettings], + displayText: `Failed to access location. ${error.message}`, + displayType: T.RPCChat.UICommandStatusDisplayTyp.error, + }) + } + } + + C.ignorePromise(f()) + return () => { + unsub() + } + }, [conversationIDKey, updateLastCoord, setCommandStatusInfo]) +} const LocationPopup = () => { const conversationIDKey = Chat.useChatContext(s => s.id) @@ -27,17 +69,7 @@ const LocationPopup = () => { sendMessage(duration ? `/location live ${duration}` : '/location') } - React.useEffect(() => { - let unwatch: undefined | (() => void) - C.PlatformSpecific.watchPositionForMap(conversationIDKey) - .then(unsub => { - unwatch = unsub - }) - .catch(() => {}) - return () => { - unwatch?.() - } - }, [conversationIDKey]) + useWatchPosition(conversationIDKey) const width = Math.ceil(Kb.Styles.dimensionWidth) const height = Math.ceil(Kb.Styles.dimensionHeight - 320) diff --git a/shared/chat/conversation/input-area/normal2/moremenu-popup.tsx b/shared/chat/conversation/input-area/normal2/moremenu-popup.native.tsx similarity index 100% rename from shared/chat/conversation/input-area/normal2/moremenu-popup.tsx rename to shared/chat/conversation/input-area/normal2/moremenu-popup.native.tsx diff --git a/shared/chat/conversation/input-area/normal2/platform-input.native.tsx b/shared/chat/conversation/input-area/normal2/platform-input.native.tsx index 4194566d7d99..134412c3846e 100644 --- a/shared/chat/conversation/input-area/normal2/platform-input.native.tsx +++ b/shared/chat/conversation/input-area/normal2/platform-input.native.tsx @@ -5,7 +5,7 @@ import * as React from 'react' import AudioRecorder from '@/chat/audio/audio-recorder.native' import FilePickerPopup from '../filepicker-popup' import {onHWKeyPressed, removeOnHWKeyPressed} from 'react-native-kb' -import MoreMenuPopup from './moremenu-popup' +import MoreMenuPopup from './moremenu-popup.native' import SetExplodingMessagePicker from './set-explode-popup' import Typing from './typing' import type * as ImagePicker from 'expo-image-picker' diff --git a/shared/common-adapters/markdown/service-decoration.tsx b/shared/common-adapters/markdown/service-decoration.tsx index 9951adafed94..90879a58e0b2 100644 --- a/shared/common-adapters/markdown/service-decoration.tsx +++ b/shared/common-adapters/markdown/service-decoration.tsx @@ -1,7 +1,7 @@ import * as T from '@/constants/types' import * as React from 'react' import * as C from '@/constants' -import {useDeepLinksState} from '@/constants/deeplinks' +import {handleAppLink} from '@/constants/deeplinks' import * as Styles from '@/styles' import Channel from './channel' import KbfsPath from '@/fs/common/kbfs-path' @@ -29,10 +29,9 @@ type KeybaseLinkProps = { } const KeybaseLink = (props: KeybaseLinkProps) => { - const handleAppLink = useDeepLinksState(s => s.dispatch.handleAppLink) const onClick = React.useCallback(() => { handleAppLink(props.link) - }, [handleAppLink, props.link]) + }, [props.link]) return ( = (set, get) => { } const onClick = () => { - storeRegistry.getState('config').dispatch.showMain() + useConfigState.getState().dispatch.showMain() storeRegistry.getState('chat').dispatch.navigateToInbox() get().dispatch.navigateToThread('desktopNotification') } const onClose = () => {} logger.info('invoking NotifyPopup for chat notification') - const sound = storeRegistry.getState('config').notifySound + const sound = useConfigState.getState().notifySound const cleanBody = body.replaceAll(/!>(.*?) = (set, get) => { blockConversation: reportUser => { const f = async () => { storeRegistry.getState('chat').dispatch.navigateToInbox() - storeRegistry.getState('config').dispatch.dynamic.persistRoute?.() + useConfigState.getState().dispatch.dynamic.persistRoute?.() await T.RPCChat.localSetConversationStatusLocalRpcPromise({ conversationID: get().getConvID(), identifyBehavior: T.RPCGen.TLFIdentifyBehavior.chatGui, @@ -1754,7 +1755,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { }, markTeamAsRead: teamID => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { logger.info('bail on not logged in') return } @@ -1765,7 +1766,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { }, markThreadAsRead: force => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { logger.info('mark read bail on not logged in') return } @@ -2703,7 +2704,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { } const conversationIDKey = get().id const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { logger.info('mark unread bail on not logged in') return } diff --git a/shared/constants/chat2/index.tsx b/shared/constants/chat2/index.tsx index b0991b30975f..79cc8a00fedf 100644 --- a/shared/constants/chat2/index.tsx +++ b/shared/constants/chat2/index.tsx @@ -18,6 +18,7 @@ import isEqual from 'lodash/isEqual' import {bodyToJSON} from '../rpc-utils' import {navigateAppend, navUpToScreen, switchTab} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useCurrentUserState} from '../current-user' import {useWaitingState} from '../waiting' import * as S from '../strings' @@ -527,7 +528,7 @@ export const useChatState = Z.createZustand((set, get) => { inboxRefresh: reason => { const f = async () => { const {username} = useCurrentUserState.getState() - const {loggedIn} = storeRegistry.getState('config') + const {loggedIn} = useConfigState.getState() if (!loggedIn || !username) { return } @@ -1566,12 +1567,13 @@ export const useChatState = Z.createZustand((set, get) => { const first = resultMetas[0] if (!first) { if (p.reason === 'appLink') { - storeRegistry - .getState('deeplinks') - .dispatch.setLinkError( - "We couldn't find this team chat channel. Please check that you're a member of the team and the channel exists." - ) - navigateAppend('keybaseLinkError') + navigateAppend({ + props: { + error: + "We couldn't find this team chat channel. Please check that you're a member of the team and the channel exists.", + }, + selected: 'keybaseLinkError', + }) return } else { return @@ -1595,12 +1597,13 @@ export const useChatState = Z.createZustand((set, get) => { error.code === T.RPCGen.StatusCode.scteamnotfound && reason === 'appLink' ) { - storeRegistry - .getState('deeplinks') - .dispatch.setLinkError( - "We couldn't find this team. Please check that you're a member of the team and the channel exists." - ) - navigateAppend('keybaseLinkError') + navigateAppend({ + props: { + error: + "We couldn't find this team. Please check that you're a member of the team and the channel exists.", + }, + selected: 'keybaseLinkError', + }) return } else { throw error @@ -1757,7 +1760,7 @@ export const useChatState = Z.createZustand((set, get) => { unboxRows: (ids, force) => { // We want to unbox rows that have scroll into view const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } diff --git a/shared/constants/config/index.tsx b/shared/constants/config/index.tsx index 1501ce8e40d7..f52577a959cc 100644 --- a/shared/constants/config/index.tsx +++ b/shared/constants/config/index.tsx @@ -13,8 +13,6 @@ import {defaultUseNativeFrame, isMobile} from '../platform' import {type CommonResponseHandler} from '@/engine/types' import {invalidPasswordErrorString} from './util' import {navigateAppend} from '../router2/util' -import {storeRegistry} from '../store-registry' -import {useWhatsNewState} from '../whats-new' type Store = T.Immutable<{ forceSmallNav: boolean @@ -151,8 +149,6 @@ export interface State extends Store { openAppSettings?: () => void openAppStore?: () => void onEngineConnectedDesktop?: () => void - onEngineIncomingDesktop?: (action: EngineGen.Actions) => void - onEngineIncomingNative?: (action: EngineGen.Actions) => void persistRoute?: (path?: ReadonlyArray) => void setNavigatorExistsNative?: () => void showMainNative?: () => void @@ -265,9 +261,6 @@ export const useConfigState = Z.createZustand((set, get) => { set(s => { s.allowAnimatedEmojis = allowAnimatedEmojis }) - - const lastSeenItem = goodState.find(i => i.item.category === 'whatsNewLastSeenVersion') - useWhatsNewState.getState().dispatch.updateLastSeen(lastSeenItem) } const updateRuntimeStats = (stats?: T.RPCGen.RuntimeStats) => { @@ -305,8 +298,6 @@ export const useConfigState = Z.createZustand((set, get) => { }, dumpLogsNative: undefined, onEngineConnectedDesktop: undefined, - onEngineIncomingDesktop: undefined, - onEngineIncomingNative: undefined, onFilePickerError: undefined, openAppSettings: undefined, openAppStore: undefined, @@ -420,7 +411,7 @@ export const useConfigState = Z.createZustand((set, get) => { 'keybase.1.provisionUi.DisplayAndPromptSecret': cancelOnCallback, 'keybase.1.provisionUi.PromptNewDeviceName': (_, response) => { cancelOnCallback(undefined, response) - storeRegistry.getState('provision').dispatch.dynamic.setUsername?.(username) + navigateAppend({props: {username}, selected: 'username'}) }, 'keybase.1.provisionUi.chooseDevice': cancelOnCallback, 'keybase.1.provisionUi.chooseGPGMethod': cancelOnCallback, diff --git a/shared/constants/deeplinks/index.tsx b/shared/constants/deeplinks/index.tsx index def8e1b6cc84..97d432d954eb 100644 --- a/shared/constants/deeplinks/index.tsx +++ b/shared/constants/deeplinks/index.tsx @@ -1,7 +1,6 @@ import * as Crypto from '../crypto/util' import * as Tabs from '../tabs' import {isPathSaltpackEncrypted, isPathSaltpackSigned} from '@/util/path' -import * as Z from '@/util/zustand' import * as EngineGen from '@/actions/engine-gen-gen' import type HiddenString from '@/util/hidden-string' import URL from 'url-parse' @@ -9,11 +8,10 @@ import logger from '@/logger' import * as T from '@/constants/types' import {navigateAppend, switchTab} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useCryptoState} from '../crypto' +import {useConfigState} from '../config' const prefix = 'keybase://' -type Store = T.Immutable<{ - keybaseLinkError: string -}> export const linkFromConvAndMessage = (conv: string, messageID: number) => `${prefix}chat/${conv}/${messageID}` @@ -40,23 +38,6 @@ const validTeamnamePart = (s: string): boolean => { } const validTeamname = (s: string) => s.split('.').every(validTeamnamePart) - -const initialStore: Store = { - keybaseLinkError: '', -} - -export interface State extends Store { - dispatch: { - handleAppLink: (link: string) => void - handleKeybaseLink: (link: string) => void - handleSaltPackOpen: (_path: string | HiddenString) => void - onEngineIncomingImpl: (action: EngineGen.Actions) => void - resetState: 'default' - setLinkError: (e: string) => void - } -} - -export const useDeepLinksState = Z.createZustand((set, get) => { const handleShowUserProfileLink = (username: string) => { switchTab(Tabs.peopleTab) storeRegistry.getState('profile').dispatch.showUserProfile(username) @@ -129,209 +110,197 @@ export const useDeepLinksState = Z.createZustand((set, get) => { ) } - const dispatch: State['dispatch'] = { - handleAppLink: link => { - if (link.startsWith('keybase://')) { - get().dispatch.handleKeybaseLink(link.replace('keybase://', '')) +export const handleAppLink = (link: string) => { + if (link.startsWith('keybase://')) { + handleKeybaseLink(link.replace('keybase://', '')) + return + } else { + // Normal deeplink + const url = new URL(link) + const username = urlToUsername(url) + if (username === 'phone-app') { + const phoneState = storeRegistry.getState('settings-phone') + const phones = (phoneState as {phones?: Map}).phones + if (!phones || phones.size > 0) { return - } else { - // Normal deeplink - const url = new URL(link) - const username = urlToUsername(url) - if (username === 'phone-app') { - const phones = storeRegistry.getState('settings-phone').phones - if (!phones || phones.size > 0) { - return - } - switchTab(Tabs.settingsTab) - navigateAppend('settingsAddPhone') - } else if (username && username !== 'app') { - handleShowUserProfileLink(username) - return + } + switchTab(Tabs.settingsTab) + navigateAppend('settingsAddPhone') + } else if (username && username !== 'app') { + handleShowUserProfileLink(username) + return + } + const teamLink = urlToTeamDeepLink(url) + if (teamLink) { + handleTeamPageLink(teamLink.teamName, teamLink.action) + return + } + } +} + +export const handleKeybaseLink = (link: string) => { + if (!link) return + const error = + "We couldn't read this link. The link might be bad, or your Keybase app might be out of date and needs to be updated." + const parts = link.split('/') + // List guaranteed to contain at least one elem. + switch (parts[0]) { + case 'profile': + if (parts[1] === 'new-proof' && (parts.length === 3 || parts.length === 4)) { + parts.length === 4 && + parts[3] && + storeRegistry.getState('profile').dispatch.showUserProfile(parts[3]) + storeRegistry.getState('profile').dispatch.addProof(parts[2]!, 'appLink') + return + } else if (parts[1] === 'show' && parts.length === 3) { + // Username is basically a team name part, we can use the same logic to + // validate deep link. + const username = parts[2]! + if (username.length && validTeamnamePart(username)) { + return handleShowUserProfileLink(username) } - const teamLink = urlToTeamDeepLink(url) - if (teamLink) { - handleTeamPageLink(teamLink.teamName, teamLink.action) - return + } + break + // Fall-through + case 'private': + case 'public': + case 'team': + try { + const decoded = decodeURIComponent(link) + switchTab(Tabs.fsTab) + storeRegistry + .getState('router') + .dispatch.navigateAppend({props: {path: `/keybase/${decoded}`}, selected: 'fsRoot'}) + return + } catch { + logger.warn("Coudn't decode KBFS URI") + return + } + case 'convid': + if (parts.length === 2) { + const conversationIDKey = parts[1] + if (conversationIDKey) { + storeRegistry.getConvoState(conversationIDKey).dispatch.navigateToThread('navChanged') } + return } - }, - handleKeybaseLink: link => { - if (!link) return - const error = - "We couldn't read this link. The link might be bad, or your Keybase app might be out of date and needs to be updated." - const parts = link.split('/') - // List guaranteed to contain at least one elem. - switch (parts[0]) { - case 'profile': - if (parts[1] === 'new-proof' && (parts.length === 3 || parts.length === 4)) { - parts.length === 4 && - parts[3] && - storeRegistry.getState('profile').dispatch.showUserProfile(parts[3]) - storeRegistry.getState('profile').dispatch.addProof(parts[2]!, 'appLink') + break + case 'chat': + if (parts.length === 2 || parts.length === 3) { + if (parts[1]!.includes('#')) { + const teamChat = parts[1]!.split('#') + if (teamChat.length !== 2) { + navigateAppend({props: {error}, selected: 'keybaseLinkError'}) return - } else if (parts[1] === 'show' && parts.length === 3) { - // Username is basically a team name part, we can use the same logic to - // validate deep link. - const username = parts[2]! - if (username.length && validTeamnamePart(username)) { - return handleShowUserProfileLink(username) - } } - break - // Fall-through - case 'private': - case 'public': - case 'team': - try { - const decoded = decodeURIComponent(link) - switchTab(Tabs.fsTab) - storeRegistry - .getState('router') - .dispatch.navigateAppend({props: {path: `/keybase/${decoded}`}, selected: 'fsRoot'}) - return - } catch { - logger.warn("Coudn't decode KBFS URI") - return - } - case 'convid': - if (parts.length === 2) { - const conversationIDKey = parts[1] - if (conversationIDKey) { - storeRegistry.getConvoState(conversationIDKey).dispatch.navigateToThread('navChanged') - } + const [teamname, channelname] = teamChat + const _highlightMessageID = parseInt(parts[2]!, 10) + if (_highlightMessageID < 0) { + logger.warn(`invalid chat message id: ${_highlightMessageID}`) return } - break - case 'chat': - if (parts.length === 2 || parts.length === 3) { - if (parts[1]!.includes('#')) { - const teamChat = parts[1]!.split('#') - if (teamChat.length !== 2) { - get().dispatch.setLinkError(error) - navigateAppend('keybaseLinkError') - return - } - const [teamname, channelname] = teamChat - const _highlightMessageID = parseInt(parts[2]!, 10) - if (_highlightMessageID < 0) { - logger.warn(`invalid chat message id: ${_highlightMessageID}`) - return - } - const highlightMessageID = T.Chat.numberToMessageID(_highlightMessageID) - const {previewConversation} = storeRegistry.getState('chat').dispatch - previewConversation({ - channelname, - highlightMessageID, - reason: 'appLink', - teamname, - }) - return - } else { - const highlightMessageID = parseInt(parts[2]!, 10) - if (highlightMessageID < 0) { - logger.warn(`invalid chat message id: ${highlightMessageID}`) - return - } - const {previewConversation} = storeRegistry.getState('chat').dispatch - previewConversation({ - highlightMessageID: T.Chat.numberToMessageID(highlightMessageID), - participants: parts[1]!.split(','), - reason: 'appLink', - }) - return - } - } - break - case 'team-page': // keybase://team-page/{team_name}/{manage_settings,add_or_invite}? - if (parts.length >= 2) { - const teamName = parts[1]! - if (teamName.length && validTeamname(teamName)) { - const actionPart = parts[2] - const action = isTeamPageAction(actionPart) ? actionPart : undefined - handleTeamPageLink(teamName, action) - return - } - } - break - case 'incoming-share': - // android needs to render first when coming back - setTimeout(() => { - navigateAppend('incomingShareNew') - }, 500) - return - case 'team-invite-link': - storeRegistry.getState('teams').dispatch.openInviteLink(parts[1] ?? '', parts[2] || '') - return - case 'settingsPushPrompt': - navigateAppend('settingsPushPrompt') + const highlightMessageID = T.Chat.numberToMessageID(_highlightMessageID) + const {previewConversation} = storeRegistry.getState('chat').dispatch + previewConversation({ + channelname, + highlightMessageID, + reason: 'appLink', + teamname, + }) return - case Tabs.teamsTab: - switchTab(Tabs.teamsTab) - return - case Tabs.fsTab: - switchTab(Tabs.fsTab) - return - case Tabs.chatTab: - switchTab(Tabs.chatTab) - return - case Tabs.peopleTab: - switchTab(Tabs.peopleTab) + } else { + const highlightMessageID = parseInt(parts[2]!, 10) + if (highlightMessageID < 0) { + logger.warn(`invalid chat message id: ${highlightMessageID}`) + return + } + const {previewConversation} = storeRegistry.getState('chat').dispatch + previewConversation({ + highlightMessageID: T.Chat.numberToMessageID(highlightMessageID), + participants: parts[1]!.split(','), + reason: 'appLink', + }) return - case Tabs.settingsTab: - switchTab(Tabs.settingsTab) + } + } + break + case 'team-page': // keybase://team-page/{team_name}/{manage_settings,add_or_invite}? + if (parts.length >= 2) { + const teamName = parts[1]! + if (teamName.length && validTeamname(teamName)) { + const actionPart = parts[2] + const action = isTeamPageAction(actionPart) ? actionPart : undefined + handleTeamPageLink(teamName, action) return - default: - // Fall through to the error return below. + } } - get().dispatch.setLinkError(error) - navigateAppend('keybaseLinkError') - }, - handleSaltPackOpen: _path => { - const path = typeof _path === 'string' ? _path : _path.stringValue() + break + case 'incoming-share': + // android needs to render first when coming back + setTimeout(() => { + navigateAppend('incomingShareNew') + }, 500) + return + case 'team-invite-link': + storeRegistry.getState('teams').dispatch.openInviteLink(parts[1] ?? '', parts[2] || '') + return + case 'settingsPushPrompt': + navigateAppend('settingsPushPrompt') + return + case Tabs.teamsTab: + switchTab(Tabs.teamsTab) + return + case Tabs.fsTab: + switchTab(Tabs.fsTab) + return + case Tabs.chatTab: + switchTab(Tabs.chatTab) + return + case Tabs.peopleTab: + switchTab(Tabs.peopleTab) + return + case Tabs.settingsTab: + switchTab(Tabs.settingsTab) + return + default: + // Fall through to the error return below. + } + navigateAppend({props: {error}, selected: 'keybaseLinkError'}) +} - if (!storeRegistry.getState('config').loggedIn) { - console.warn('Tried to open a saltpack file before being logged in') - return - } - let operation: T.Crypto.Operations | undefined - if (isPathSaltpackEncrypted(path)) { - operation = Crypto.Operations.Decrypt - } else if (isPathSaltpackSigned(path)) { - operation = Crypto.Operations.Verify - } else { - logger.warn( - 'Deeplink received saltpack file path not ending in ".encrypted.saltpack" or ".signed.saltpack"' - ) - return - } - storeRegistry.getState('crypto').dispatch.onSaltpackOpenFile(operation, path) - switchTab(Tabs.cryptoTab) - }, +export const handleSaltPackOpen = (_path: string | HiddenString) => { + const path = typeof _path === 'string' ? _path : _path.stringValue() - onEngineIncomingImpl: action => { - switch (action.type) { - case EngineGen.keybase1NotifyServiceHandleKeybaseLink: { - const {link, deferred} = action.payload.params - if (deferred && !link.startsWith('keybase://team-invite-link/')) { - return - } - get().dispatch.handleKeybaseLink(link) - break - } - default: - } - }, - resetState: 'default', - setLinkError: e => { - set(s => { - s.keybaseLinkError = e - }) - }, + if (!useConfigState.getState().loggedIn) { + console.warn('Tried to open a saltpack file before being logged in') + return + } + let operation: T.Crypto.Operations | undefined + if (isPathSaltpackEncrypted(path)) { + operation = Crypto.Operations.Decrypt + } else if (isPathSaltpackSigned(path)) { + operation = Crypto.Operations.Verify + } else { + logger.warn( + 'Deeplink received saltpack file path not ending in ".encrypted.saltpack" or ".signed.saltpack"' + ) + return } - return { - ...initialStore, - dispatch, + useCryptoState.getState().dispatch.onSaltpackOpenFile(operation, path) + switchTab(Tabs.cryptoTab) +} + +export const onEngineIncomingImpl = (action: EngineGen.Actions) => { + switch (action.type) { + case EngineGen.keybase1NotifyServiceHandleKeybaseLink: { + const {link, deferred} = action.payload.params + if (deferred && !link.startsWith('keybase://team-invite-link/')) { + return + } + handleKeybaseLink(link) + break + } + default: } -}) +} diff --git a/shared/constants/deeplinks/util.tsx b/shared/constants/deeplinks/util.tsx index 7d1d3078ac54..e5f2d279fcc6 100644 --- a/shared/constants/deeplinks/util.tsx +++ b/shared/constants/deeplinks/util.tsx @@ -1,11 +1,11 @@ import * as EngineGen from '@/actions/engine-gen-gen' -import {storeRegistry} from '../store-registry' +import {onEngineIncomingImpl} from './index' export const onEngineIncoming = (action: EngineGen.Actions) => { switch (action.type) { case EngineGen.keybase1NotifyServiceHandleKeybaseLink: { - storeRegistry.getState('deeplinks').dispatch.onEngineIncomingImpl(action) + onEngineIncomingImpl(action) } break default: diff --git a/shared/constants/engine/index.tsx b/shared/constants/engine/index.tsx deleted file mode 100644 index 167add4ebbe4..000000000000 --- a/shared/constants/engine/index.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type * as EngineGen from '@/actions/engine-gen-gen' -import * as Z from '@/util/zustand' -import * as ChatUtil from '../chat2/util' -import * as NotifUtil from '../notifications/util' -import * as PeopleUtil from '../people/util' -import * as PinentryUtil from '../pinentry/util' -import {onEngineIncoming as onEngineIncomingShared} from '../platform-specific/shared' -import {storeRegistry} from '../store-registry' -import {ignorePromise} from '../utils' -import * as TrackerUtil from '../tracker2/util' -import * as UnlockFoldersUtil from '../unlock-folders/util' -import logger from '@/logger' - -type Store = object -const initialStore: Store = {} - -export interface State extends Store { - dispatch: { - onEngineConnected: () => void - onEngineDisconnected: () => void - onEngineIncoming: (action: EngineGen.Actions) => void - resetState: () => void - } -} - -export const useEngineState = Z.createZustand(set => { - let incomingTimeout: NodeJS.Timeout - const dispatch: State['dispatch'] = { - onEngineConnected: () => { - ChatUtil.onEngineConnected() - storeRegistry.getState('config').dispatch.onEngineConnected() - storeRegistry.getState('daemon').dispatch.startHandshake() - NotifUtil.onEngineConnected() - PeopleUtil.onEngineConnected() - PinentryUtil.onEngineConnected() - TrackerUtil.onEngineConnected() - UnlockFoldersUtil.onEngineConnected() - }, - onEngineDisconnected: () => { - const f = async () => { - await logger.dump() - } - ignorePromise(f()) - storeRegistry.getState('daemon').dispatch.setError(new Error('Disconnected')) - }, - onEngineIncoming: action => { - // defer a frame so its more like before - incomingTimeout = setTimeout(() => { - // we delegate to these utils so we don't need to load stores that we don't need yet - onEngineIncomingShared(action) - }, 0) - }, - resetState: () => { - set(s => ({...s, ...initialStore, dispatch: s.dispatch})) - clearTimeout(incomingTimeout) - }, - } - return { - ...initialStore, - dispatch, - } -}) diff --git a/shared/constants/fs/index.tsx b/shared/constants/fs/index.tsx index 16e08313c52b..afccb5d7f7bc 100644 --- a/shared/constants/fs/index.tsx +++ b/shared/constants/fs/index.tsx @@ -15,6 +15,7 @@ import isEqual from 'lodash/isEqual' import {settingsFsTab} from '../settings/util' import {navigateAppend, navigateUp} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useCurrentUserState} from '../current-user' export {makeActionForOpenPathInFilesTab} from './util' @@ -1582,7 +1583,7 @@ export const useFSState = Z.createZustand((set, get) => { favoritesLoad: () => { const f = async () => { try { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } const results = await T.RPCGen.SimpleFSSimpleFSListFavoritesRpcPromise() @@ -2217,7 +2218,7 @@ export const useFSState = Z.createZustand((set, get) => { src: { PathType: T.RPCGen.PathType.local, local: T.FS.getNormalizedLocalPath( - storeRegistry.getState('config').incomingShareUseOriginal + useConfigState.getState().incomingShareUseOriginal ? originalPath : scaledPath || originalPath ), diff --git a/shared/constants/fs/platform-specific.desktop.tsx b/shared/constants/fs/platform-specific.desktop.tsx index b9e372eded7c..71bee000e649 100644 --- a/shared/constants/fs/platform-specific.desktop.tsx +++ b/shared/constants/fs/platform-specific.desktop.tsx @@ -9,7 +9,7 @@ import KB2 from '@/util/electron.desktop' import {uint8ArrayToHex} from 'uint8array-extras' import {useFSState} from '.' import {navigateAppend} from '../router2/util' -import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' const {openPathInFinder, openURL, getPathType, selectFilesToUploadDialog} = KB2.functions const {darwinCopyToKBFSTempUploadFile, relaunchApp, uninstallKBFSDialog, uninstallDokanDialog} = KB2.functions @@ -133,7 +133,7 @@ const onInstallCachedDokan = async () => { } const initPlatformSpecific = () => { - storeRegistry.getStore('config').subscribe((s, old) => { + useConfigState.subscribe((s, old) => { if (s.appFocused === old.appFocused) return useFSState.getState().dispatch.onChangedFocus(s.appFocused) }) @@ -274,7 +274,7 @@ const initPlatformSpecific = () => { }) s.dispatch.dynamic.openFilesFromWidgetDesktop = wrapErrors((path: T.FS.Path) => { - storeRegistry.getState('config').dispatch.showMain() + useConfigState.getState().dispatch.showMain() if (path) { Constants.makeActionForOpenPathInFilesTab(path) } else { diff --git a/shared/constants/git/index.tsx b/shared/constants/git/index.tsx index ffe070ae3f3f..497136984873 100644 --- a/shared/constants/git/index.tsx +++ b/shared/constants/git/index.tsx @@ -6,6 +6,7 @@ import * as dateFns from 'date-fns' import * as Z from '@/util/zustand' import debounce from 'lodash/debounce' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' const parseRepos = (results: ReadonlyArray) => { const errors: Array = [] @@ -105,7 +106,7 @@ export const useGitState = Z.createZustand((set, get) => { async () => { const results = await T.RPCGen.gitGetAllGitMetadataRpcPromise(undefined, S.waitingKeyGitLoading) const {errors, repos} = parseRepos(results || []) - const {setGlobalError} = storeRegistry.getState('config').dispatch + const {setGlobalError} = useConfigState.getState().dispatch errors.forEach(e => setGlobalError(e)) set(s => { s.idToInfo = repos diff --git a/shared/constants/init.d.ts b/shared/constants/init.d.ts new file mode 100644 index 000000000000..aa0eb1800753 --- /dev/null +++ b/shared/constants/init.d.ts @@ -0,0 +1,5 @@ +import type * as EngineGen from '@/actions/engine-gen-gen' + +export declare function initPlatformListener(): void +export declare function onEngineIncoming(action: EngineGen.Actions): void + diff --git a/shared/constants/init.desktop.tsx b/shared/constants/init.desktop.tsx new file mode 100644 index 000000000000..910c0f076402 --- /dev/null +++ b/shared/constants/init.desktop.tsx @@ -0,0 +1,275 @@ +import * as Chat from './chat2' +import {ignorePromise} from './utils' +import {useActiveState} from './active' +import {useConfigState} from './config' +import * as ConfigConstants from './config' +import {useDaemonState} from './daemon' +import {useFSState} from './fs' +import {useProfileState} from './profile' +import {useRouterState} from './router2' +import * as EngineGen from '@/actions/engine-gen-gen' +import * as T from './types' +import InputMonitor from './platform-specific/input-monitor.desktop' +import KB2 from '@/util/electron.desktop' +import logger from '@/logger' +import type {RPCError} from '@/util/errors' +import {getEngine} from '@/engine' +import {isLinux, isWindows} from './platform.desktop' +import {kbfsNotification} from './platform-specific/kbfs-notifications' +import {skipAppFocusActions} from '@/local-debug.desktop' +import NotifyPopup from '@/util/notify-popup' +import {noKBFSFailReason} from './config/util' +import {initSharedSubscriptions} from './platform-specific/shared' +import {wrapErrors} from '@/util/debug' +import {dumpLogs} from './platform-specific/index.desktop' + +const {showMainWindow, activeChanged, requestWindowsStartService} = KB2.functions +const {quitApp, exitApp, setOpenAtLogin, copyToClipboard} = KB2.functions + +const maybePauseVideos = () => { + const {appFocused} = useConfigState.getState() + const videos = document.querySelectorAll('video') + const allVideos = Array.from(videos) + + allVideos.forEach(v => { + if (appFocused) { + if (v.hasAttribute('data-focus-paused')) { + if (v.paused) { + v.play() + .then(() => {}) + .catch(() => {}) + } + } + } else { + // only pause looping videos + if (!v.paused && v.hasAttribute('loop') && v.hasAttribute('autoplay')) { + v.setAttribute('data-focus-paused', 'true') + v.pause() + } + } + }) +} + +export const onEngineIncoming = (action: EngineGen.Actions) => { + switch (action.type) { + case EngineGen.keybase1LogsendPrepareLogsend: { + const f = async () => { + const response = action.payload.response + try { + await dumpLogs() + } finally { + response.result() + } + } + ignorePromise(f()) + break + } + case EngineGen.keybase1NotifyAppExit: + console.log('App exit requested') + exitApp?.(0) + break + case EngineGen.keybase1NotifyFSFSActivity: + kbfsNotification(action.payload.params.notification, NotifyPopup) + break + case EngineGen.keybase1NotifyPGPPgpKeyInSecretStoreFile: { + const f = async () => { + try { + await T.RPCGen.pgpPgpStorageDismissRpcPromise() + } catch (err) { + console.warn('Error in sending pgpPgpStorageDismissRpc:', err) + } + } + ignorePromise(f()) + break + } + case EngineGen.keybase1NotifyServiceShutdown: { + const {code} = action.payload.params + if (isWindows && code !== (T.RPCGen.ExitCode.restart as number)) { + console.log('Quitting due to service shutdown with code: ', code) + // Quit just the app, not the service + quitApp?.() + } + break + } + + case EngineGen.keybase1LogUiLog: { + const {params} = action.payload + const {level, text} = params + logger.info('keybase.1.logUi.log:', params.text.data) + if (level >= T.RPCGen.LogLevel.error) { + NotifyPopup(text.data) + } + break + } + + case EngineGen.keybase1NotifySessionClientOutOfDate: { + const {upgradeTo, upgradeURI, upgradeMsg} = action.payload.params + const body = upgradeMsg || `Please update to ${upgradeTo} by going to ${upgradeURI}` + NotifyPopup('Client out of date!', {body}, 60 * 60) + // This is from the API server. Consider notifications from server always critical. + useConfigState + .getState() + .dispatch.setOutOfDate({critical: true, message: upgradeMsg, outOfDate: true, updating: false}) + break + } + default: + } +} + +export const initPlatformListener = () => { + useConfigState.setState(s => { + s.dispatch.dynamic.dumpLogsNative = dumpLogs + s.dispatch.dynamic.showMainNative = wrapErrors(() => showMainWindow?.()) + s.dispatch.dynamic.copyToClipboard = wrapErrors((s: string) => copyToClipboard?.(s)) + s.dispatch.dynamic.onEngineConnectedDesktop = wrapErrors(() => { + // Introduce ourselves to the service + const f = async () => { + await T.RPCGen.configHelloIAmRpcPromise({details: KB2.constants.helloDetails}) + } + ignorePromise(f()) + }) + + }) + + useConfigState.subscribe((s, old) => { + if (s.loggedIn !== old.loggedIn) { + s.dispatch.osNetworkStatusChanged(navigator.onLine, 'notavailable', true) + } + + if (s.appFocused !== old.appFocused) { + maybePauseVideos() + } + + if (s.openAtLogin !== old.openAtLogin) { + const {openAtLogin} = s + const f = async () => { + if (__DEV__) { + console.log('onSetOpenAtLogin disabled for dev mode') + return + } else { + await T.RPCGen.configGuiSetValueRpcPromise({ + path: ConfigConstants.openAtLoginKey, + value: {b: openAtLogin, isNull: false}, + }) + } + if (isLinux || isWindows) { + const enabled = + (await T.RPCGen.ctlGetOnLoginStartupRpcPromise()) === T.RPCGen.OnLoginStartupStatus.enabled + if (enabled !== openAtLogin) { + try { + await T.RPCGen.ctlSetOnLoginStartupRpcPromise({enabled: openAtLogin}) + } catch (error_) { + const error = error_ as RPCError + logger.warn(`Error in sending ctlSetOnLoginStartup: ${error.message}`) + } + } + } else { + logger.info(`Login item settings changed! now ${openAtLogin ? 'on' : 'off'}`) + await setOpenAtLogin?.(openAtLogin) + } + } + ignorePromise(f()) + } + }) + + const handleWindowFocusEvents = () => { + const handle = (appFocused: boolean) => { + if (skipAppFocusActions) { + console.log('Skipping app focus actions!') + } else { + useConfigState.getState().dispatch.changedFocus(appFocused) + } + } + window.addEventListener('focus', () => handle(true)) + window.addEventListener('blur', () => handle(false)) + } + handleWindowFocusEvents() + + const setupReachabilityWatcher = () => { + window.addEventListener('online', () => + useConfigState.getState().dispatch.osNetworkStatusChanged(true, 'notavailable') + ) + window.addEventListener('offline', () => + useConfigState.getState().dispatch.osNetworkStatusChanged(false, 'notavailable') + ) + } + setupReachabilityWatcher() + + useDaemonState.subscribe((s, old) => { + if (s.handshakeVersion !== old.handshakeVersion) { + if (!isWindows) return + + const f = async () => { + const waitKey = 'pipeCheckFail' + const version = s.handshakeVersion + const {wait} = s.dispatch + wait(waitKey, version, true) + try { + logger.info('Checking RPC ownership') + if (KB2.functions.winCheckRPCOwnership) { + await KB2.functions.winCheckRPCOwnership() + } + wait(waitKey, version, false) + } catch (error_) { + // error will be logged in bootstrap check + getEngine().reset() + const error = error_ as RPCError + wait(waitKey, version, false, error.message || 'windows pipe owner fail', true) + } + } + ignorePromise(f()) + } + + if (s.handshakeState !== old.handshakeState && s.handshakeState === 'done') { + useConfigState.getState().dispatch.setStartupDetails({ + conversation: Chat.noConversationIDKey, + followUser: '', + link: '', + tab: undefined, + }) + } + }) + + if (isLinux) { + useConfigState.getState().dispatch.initUseNativeFrame() + } + useConfigState.getState().dispatch.initNotifySound() + useConfigState.getState().dispatch.initForceSmallNav() + useConfigState.getState().dispatch.initOpenAtLogin() + useConfigState.getState().dispatch.initAppUpdateLoop() + + useProfileState.setState(s => { + s.dispatch.editAvatar = () => { + useRouterState + .getState() + .dispatch.navigateAppend({props: {image: undefined}, selected: 'profileEditAvatar'}) + } + }) + + const initializeInputMonitor = () => { + const inputMonitor = new InputMonitor() + inputMonitor.notifyActive = (userActive: boolean) => { + if (skipAppFocusActions) { + console.log('Skipping app focus actions!') + } else { + useActiveState.getState().dispatch.setActive(userActive) + // let node thread save file + activeChanged?.(Date.now(), userActive) + } + } + } + initializeInputMonitor() + + useDaemonState.setState(s => { + s.dispatch.onRestartHandshakeNative = () => { + const {handshakeFailedReason} = useDaemonState.getState() + if (isWindows && handshakeFailedReason === noKBFSFailReason) { + requestWindowsStartService?.() + } + } + }) + + initSharedSubscriptions() + + ignorePromise(useFSState.getState().dispatch.setupSubscriptions()) +} diff --git a/shared/constants/init.native.tsx b/shared/constants/init.native.tsx new file mode 100644 index 000000000000..31ec34f7d372 --- /dev/null +++ b/shared/constants/init.native.tsx @@ -0,0 +1,506 @@ +import {ignorePromise, neverThrowPromiseFunc, timeoutPromise} from './utils' +import {useChatState} from './chat2' +import {useConfigState} from './config' +import {useCurrentUserState} from './current-user' +import {useDaemonState} from './daemon' +import {useDarkModeState} from './darkmode' +import {useFSState} from './fs' +import {useProfileState} from './profile' +import {useRouterState} from './router2' +import {useSettingsContactsState} from './settings-contacts' +import * as T from './types' +import * as Clipboard from 'expo-clipboard' +import * as EngineGen from '@/actions/engine-gen-gen' +import * as ExpoLocation from 'expo-location' +import * as ExpoTaskManager from 'expo-task-manager' +import * as Tabs from './tabs' +import * as NetInfo from '@react-native-community/netinfo' +import NotifyPopup from '@/util/notify-popup' +import logger from '@/logger' +import {Alert, Linking} from 'react-native' +import {isAndroid} from './platform.native' +import {wrapErrors} from '@/util/debug' +import {getTab, getVisiblePath, logState} from './router2' +import {launchImageLibraryAsync} from '@/util/expo-image-picker.native' +import {setupAudioMode} from '@/util/audio.native' +import { + androidOpenSettings, + fsCacheDir, + fsDownloadDir, + androidAppColorSchemeChanged, + guiConfig, + shareListenersRegistered, +} from 'react-native-kb' +import {initPushListener, getStartupDetailsFromInitialPush} from './platform-specific/push.native' +import {initSharedSubscriptions} from './platform-specific/shared' +import type {ImageInfo} from '@/util/expo-image-picker.native' +import {noConversationIDKey} from './types/chat2/common' +import {getSelectedConversation} from './chat2/common' +import {getConvoState} from './chat2/convostate' +import {requestLocationPermission, showShareActionSheet} from './platform-specific/index.native' + +const loadStartupDetails = async () => { + const [routeState, initialUrl, push] = await Promise.all([ + neverThrowPromiseFunc(async () => { + try { + const config = JSON.parse(guiConfig) as {ui?: {routeState2?: string}} | undefined + return Promise.resolve(config?.ui?.routeState2 ?? '') + } catch { + return Promise.resolve('') + } + }), + neverThrowPromiseFunc(async () => Linking.getInitialURL()), + neverThrowPromiseFunc(getStartupDetailsFromInitialPush), + ] as const) + + // Clear last value to be extra safe bad things don't hose us forever + try { + await T.RPCGen.configGuiSetValueRpcPromise({ + path: 'ui.routeState2', + value: {isNull: false, s: ''}, + }) + } catch {} + + let conversation: T.Chat.ConversationIDKey | undefined + let followUser = '' + let link = '' + let tab = '' + + // Top priority, push + if (push) { + logger.info('initialState: push', push.startupConversation, push.startupFollowUser) + conversation = push.startupConversation + followUser = push.startupFollowUser ?? '' + } else if (initialUrl) { + // Second priority, deep link + link = initialUrl + } else if (routeState) { + // Last priority, saved from last session + try { + const item = JSON.parse(routeState) as + | undefined + | {param?: {selectedConversationIDKey?: unknown}; routeName?: string} + if (item) { + const _convo = item.param?.selectedConversationIDKey || undefined + if (typeof _convo === 'string') { + conversation = _convo + logger.info('initialState: routeState', conversation) + } + const _rn = item.routeName || undefined + if (typeof _rn === 'string') { + tab = _rn as unknown as typeof tab + } + } + } catch { + logger.info('initialState: routeState parseFail') + conversation = undefined + tab = '' + } + } + + // never allow this case + if (tab === 'blank') { + tab = '' + } + + useConfigState.getState().dispatch.setStartupDetails({ + conversation: conversation ?? noConversationIDKey, + followUser, + link, + tab: tab as Tabs.Tab, + }) +} + +const locationTaskName = 'background-location-task' +let locationRefs = 0 +let madeBackgroundTask = false + +const ensureBackgroundTask = () => { + if (madeBackgroundTask) return + madeBackgroundTask = true + + ExpoTaskManager.defineTask(locationTaskName, async ({data, error}) => { + if (error) { + // check `error.message` for more details. + return Promise.resolve() + } + + if (!data) { + return Promise.resolve() + } + const d = data as {locations?: Array} + const locations = d.locations + if (!locations?.length) { + return Promise.resolve() + } + const pos = locations.at(-1) + const coord = { + accuracy: Math.floor(pos?.coords.accuracy ?? 0), + lat: pos?.coords.latitude ?? 0, + lon: pos?.coords.longitude ?? 0, + } + + useChatState.getState().dispatch.updateLastCoord(coord) + return Promise.resolve() + }) +} + +const setPermissionDeniedCommandStatus = (conversationIDKey: T.Chat.ConversationIDKey, text: string) => { + getConvoState(conversationIDKey).dispatch.setCommandStatusInfo({ + actions: [T.RPCChat.UICommandStatusActionTyp.appsettings], + displayText: text, + displayType: T.RPCChat.UICommandStatusDisplayTyp.error, + }) +} + +const onChatWatchPosition = async (action: EngineGen.Chat1ChatUiChatWatchPositionPayload) => { + const response = action.payload.response + response.result(0) + try { + await requestLocationPermission(action.payload.params.perm) + } catch (_error) { + const error = _error as {message?: string} + logger.info('failed to get location perms: ' + error.message) + setPermissionDeniedCommandStatus( + T.Chat.conversationIDToKey(action.payload.params.convID), + `Failed to access location. ${error.message}` + ) + } + + locationRefs++ + + if (locationRefs === 1) { + try { + logger.info( + '[location] location watch start due to ', + T.Chat.conversationIDToKey(action.payload.params.convID) + ) + ensureBackgroundTask() + await ExpoLocation.startLocationUpdatesAsync(locationTaskName, { + deferredUpdatesDistance: 65, + pausesUpdatesAutomatically: true, + showsBackgroundLocationIndicator: true, + }) + logger.info('[location] start success') + } catch { + logger.info('[location] start failed') + locationRefs-- + } + } +} + +const onChatClearWatch = async () => { + locationRefs-- + if (locationRefs <= 0) { + try { + logger.info('[location] end start') + ensureBackgroundTask() + await ExpoLocation.stopLocationUpdatesAsync(locationTaskName) + logger.info('[location] end success') + } catch { + logger.info('[location] end failed') + } + } +} + +export const onEngineIncoming = (action: EngineGen.Actions) => { + switch (action.type) { + case EngineGen.chat1ChatUiTriggerContactSync: + useSettingsContactsState.getState().dispatch.manageContactsCache() + break + case EngineGen.keybase1LogUiLog: { + const {params} = action.payload + const {level, text} = params + logger.info('keybase.1.logUi.log:', params.text.data) + if (level >= T.RPCGen.LogLevel.error) { + NotifyPopup(text.data) + } + break + } + case EngineGen.chat1ChatUiChatWatchPosition: + ignorePromise(onChatWatchPosition(action)) + break + case EngineGen.chat1ChatUiChatClearWatch: + ignorePromise(onChatClearWatch()) + break + default: + } +} + +export const initPlatformListener = () => { + let _lastPersist = '' + useConfigState.setState(s => { + s.dispatch.dynamic.persistRoute = wrapErrors((path?: ReadonlyArray) => { + const f = async () => { + if (!useConfigState.getState().startup.loaded) { + return + } + let param = {} + let routeName = Tabs.peopleTab + if (path) { + const cur = getTab() + if (cur) { + routeName = cur + } + const ap = getVisiblePath() + ap.some(r => { + if (r.name === 'chatConversation') { + const rParams = r.params as undefined | {conversationIDKey?: T.Chat.ConversationIDKey} + param = {selectedConversationIDKey: rParams?.conversationIDKey} + return true + } + return false + }) + } + const s = JSON.stringify({param, routeName}) + // don't keep rewriting + if (_lastPersist === s) { + return + } + _lastPersist = s + + await T.RPCGen.configGuiSetValueRpcPromise({ + path: 'ui.routeState2', + value: {isNull: false, s}, + }) + } + ignorePromise(f()) + }) + + }) + + useConfigState.subscribe((s, old) => { + if (s.mobileAppState === old.mobileAppState) return + let appFocused: boolean + let logState: T.RPCGen.MobileAppState + switch (s.mobileAppState) { + case 'active': + appFocused = true + logState = T.RPCGen.MobileAppState.foreground + break + case 'background': + appFocused = false + logState = T.RPCGen.MobileAppState.background + break + case 'inactive': + appFocused = false + logState = T.RPCGen.MobileAppState.inactive + break + default: + appFocused = false + logState = T.RPCGen.MobileAppState.foreground + } + + logger.info(`setting app state on service to: ${logState}`) + s.dispatch.changedFocus(appFocused) + + if (appFocused && old.mobileAppState !== 'active') { + const {dispatch} = getConvoState(getSelectedConversation()) + dispatch.loadMoreMessages({reason: 'foregrounding'}) + dispatch.markThreadAsRead() + } + }) + + useConfigState.setState(s => { + s.dispatch.dynamic.copyToClipboard = wrapErrors((s: string) => { + Clipboard.setStringAsync(s) + .then(() => {}) + .catch(() => {}) + }) + }) + + const configureAndroidCacheDir = () => { + if (isAndroid && fsCacheDir && fsDownloadDir) { + ignorePromise( + T.RPCChat.localConfigureFileAttachmentDownloadLocalRpcPromise({ + // Android's cache dir is (when I tried) [app]/cache but Go side uses + // [app]/.cache by default, which can't be used for sharing to other apps. + cacheDirOverride: fsCacheDir, + downloadDirOverride: fsDownloadDir, + }) + .then(() => {}) + .catch((e: unknown) => { + logger.error(`[Android cache override] Failed to configure: ${String(e)}`) + }) + ) + } else if (isAndroid) { + logger.warn( + `[Android cache override] Missing dirs - cacheDir: ${fsCacheDir}, downloadDir: ${fsDownloadDir}` + ) + } + } + + useDaemonState.subscribe((s, old) => { + const versionChanged = s.handshakeVersion !== old.handshakeVersion + const stateChanged = s.handshakeState !== old.handshakeState + const justBecameReady = stateChanged && s.handshakeState === 'done' && old.handshakeState !== 'done' + + if (versionChanged || justBecameReady) { + configureAndroidCacheDir() + } + }) + + useConfigState.setState(s => { + s.dispatch.dynamic.onFilePickerError = wrapErrors((error: Error) => { + Alert.alert('Error', String(error)) + }) + s.dispatch.dynamic.openAppStore = wrapErrors(() => { + Linking.openURL( + isAndroid + ? 'http://play.google.com/store/apps/details?id=io.keybase.ossifrage' + : 'https://itunes.apple.com/us/app/keybase-crypto-for-everyone/id1044461770?mt=8' + ).catch(() => {}) + }) + }) + + useProfileState.setState(s => { + s.dispatch.editAvatar = () => { + const f = async () => { + try { + const result = await launchImageLibraryAsync('photo') + const first = result.assets?.reduce((acc, a) => { + if (!acc && (a.type === 'image' || a.type === 'video')) { + return a as ImageInfo + } + return acc + }, undefined) + if (!result.canceled && first) { + useRouterState + .getState() + .dispatch.navigateAppend({props: {image: first}, selected: 'profileEditAvatar'}) + } + } catch (error) { + useConfigState.getState().dispatch.filePickerError(new Error(String(error))) + } + } + ignorePromise(f()) + } + }) + + useConfigState.subscribe((s, old) => { + if (s.loggedIn === old.loggedIn) return + const f = async () => { + const {type} = await NetInfo.fetch() + s.dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type, true) + } + ignorePromise(f()) + }) + + useConfigState.subscribe((s, old) => { + if (s.networkStatus === old.networkStatus) return + const type = s.networkStatus?.type + if (!type) return + const f = async () => { + try { + await T.RPCGen.appStateUpdateMobileNetStateRpcPromise({state: type}) + } catch (err) { + console.warn('Error sending mobileNetStateUpdate', err) + } + } + ignorePromise(f()) + }) + + useConfigState.setState(s => { + s.dispatch.dynamic.showShareActionSheet = wrapErrors( + (filePath: string, message: string, mimeType: string) => { + const f = async () => { + await showShareActionSheet({filePath, message, mimeType}) + } + ignorePromise(f()) + } + ) + }) + + useConfigState.subscribe((s, old) => { + if (s.mobileAppState === old.mobileAppState) return + if (s.mobileAppState === 'active') { + // only reload on foreground + useSettingsContactsState.getState().dispatch.loadContactPermissions() + } + }) + + // Location + if (isAndroid) { + useDarkModeState.subscribe((s, old) => { + if (s.darkModePreference === old.darkModePreference) return + androidAppColorSchemeChanged(s.darkModePreference) + }) + } + + // we call this when we're logged in. + let calledShareListenersRegistered = false + + useRouterState.subscribe((s, old) => { + const next = s.navState + const prev = old.navState + if (next === prev) return + const f = async () => { + await timeoutPromise(1000) + const path = getVisiblePath() + useConfigState.getState().dispatch.dynamic.persistRoute?.(path) + } + + if (!calledShareListenersRegistered && logState().loggedIn) { + calledShareListenersRegistered = true + shareListenersRegistered() + } + ignorePromise(f()) + }) + + // Start this immediately instead of waiting so we can do more things in parallel + ignorePromise(loadStartupDetails()) + initPushListener() + + NetInfo.addEventListener(({type}) => { + useConfigState.getState().dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type) + }) + + const initAudioModes = () => { + ignorePromise(setupAudioMode(false)) + } + initAudioModes() + + if (isAndroid) { + const daemonState = useDaemonState.getState() + if (daemonState.handshakeState === 'done' || daemonState.handshakeVersion > 0) { + configureAndroidCacheDir() + } + } + + useConfigState.setState(s => { + s.dispatch.dynamic.openAppSettings = wrapErrors(() => { + const f = async () => { + if (isAndroid) { + androidOpenSettings() + } else { + const settingsURL = 'app-settings:' + const can = await Linking.canOpenURL(settingsURL) + if (can) { + await Linking.openURL(settingsURL) + } else { + logger.warn('Unable to open app settings') + } + } + } + ignorePromise(f()) + }) + + }) + + useRouterState.setState(s => { + s.dispatch.dynamic.tabLongPress = wrapErrors((tab: string) => { + if (tab !== Tabs.peopleTab) return + const accountRows = useConfigState.getState().configuredAccounts + const current = useCurrentUserState.getState().username + const row = accountRows.find(a => a.username !== current && a.hasStoredSecret) + if (row) { + useConfigState.getState().dispatch.setUserSwitching(true) + useConfigState.getState().dispatch.login(row.username, '') + } + }) + }) + + initSharedSubscriptions() + + ignorePromise(useFSState.getState().dispatch.setupSubscriptions()) +} diff --git a/shared/constants/notifications/index.tsx b/shared/constants/notifications/index.tsx index c619e197f9c4..de0802fff654 100644 --- a/shared/constants/notifications/index.tsx +++ b/shared/constants/notifications/index.tsx @@ -5,6 +5,7 @@ import {isMobile} from '../platform' import isEqual from 'lodash/isEqual' import * as Tabs from '../tabs' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useCurrentUserState} from '../current-user' export type BadgeType = 'regular' | 'update' | 'error' | 'uploading' @@ -103,16 +104,16 @@ export const useNotifState = Z.createZustand((set, get) => { onEngineIncomingImpl: action => { switch (action.type) { case EngineGen.keybase1NotifyAuditRootAuditError: - storeRegistry - .getState('config') + useConfigState + .getState() .dispatch.setGlobalError( new Error(`Keybase is buggy, please report this: ${action.payload.params.message}`) ) break case EngineGen.keybase1NotifyAuditBoxAuditError: - storeRegistry - .getState('config') + useConfigState + .getState() .dispatch.setGlobalError( new Error( `Keybase had a problem loading a team, please report this with \`keybase log send\`: ${action.payload.params.message}` @@ -121,7 +122,7 @@ export const useNotifState = Z.createZustand((set, get) => { break case EngineGen.keybase1NotifyBadgesBadgeState: { const badgeState = action.payload.params.badgeState - storeRegistry.getState('config').dispatch.setBadgeState(badgeState) + useConfigState.getState().dispatch.setBadgeState(badgeState) const counts = badgeStateToBadgeCounts(badgeState) if (!isMobile && shouldTriggerTlfLoad(badgeState)) { diff --git a/shared/constants/people/index.tsx b/shared/constants/people/index.tsx index a6f559bccc9e..7599680c75f4 100644 --- a/shared/constants/people/index.tsx +++ b/shared/constants/people/index.tsx @@ -9,7 +9,7 @@ import type {IconType} from '@/common-adapters/icon.constants-gen' // do NOT pul import {isMobile} from '../platform' import type {e164ToDisplay as e164ToDisplayType} from '@/util/phone-numbers' import debounce from 'lodash/debounce' -import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useFollowerState} from '../followers' import {RPCError, isNetworkErr} from '../utils' @@ -388,7 +388,7 @@ export const usePeopleState = Z.createZustand((set, get) => { logger.info( 'getPeopleData: appFocused:', 'loggedIn', - storeRegistry.getState('config').loggedIn, + useConfigState.getState().loggedIn, 'action', {markViewed, numFollowSuggestionsWanted} ) diff --git a/shared/constants/platform-specific/index.d.ts b/shared/constants/platform-specific/index.d.ts index 7eefb9fc101e..a5c165a8b3c6 100644 --- a/shared/constants/platform-specific/index.d.ts +++ b/shared/constants/platform-specific/index.d.ts @@ -15,5 +15,4 @@ export declare function saveAttachmentToCameraRoll(fileURL: string, mimeType: st export declare function requestLocationPermission(mode: T.RPCChat.UIWatchPositionPerm): Promise export declare function watchPositionForMap(conversationIDKey: T.Chat.ConversationIDKey): Promise<() => void> -export declare function initPlatformListener(): void export declare function requestPermissionsToWrite(): Promise diff --git a/shared/constants/platform-specific/index.desktop.tsx b/shared/constants/platform-specific/index.desktop.tsx index eef15f0e977b..72e46a605293 100644 --- a/shared/constants/platform-specific/index.desktop.tsx +++ b/shared/constants/platform-specific/index.desktop.tsx @@ -1,33 +1,17 @@ -import * as Chat from '../chat2' import {ignorePromise} from '../utils' -import {useActiveState} from '../active' import {useConfigState} from '../config' -import * as ConfigConstants from '../config' -import {useDaemonState} from '../daemon' -import {useFSState} from '../fs' import {usePinentryState} from '../pinentry' -import {useProfileState} from '../profile' -import {useRouterState} from '../router2' -import * as EngineGen from '@/actions/engine-gen-gen' import * as RemoteGen from '@/actions/remote-gen' import * as T from '../types' -import InputMonitor from './input-monitor.desktop' import KB2 from '@/util/electron.desktop' import logger from '@/logger' import {RPCError} from '@/util/errors' -import {getEngine} from '@/engine' -import {isLinux, isWindows} from '../platform.desktop' -import {kbfsNotification} from './kbfs-notifications' -import {skipAppFocusActions} from '@/local-debug.desktop' -import NotifyPopup from '@/util/notify-popup' -import {noKBFSFailReason} from '@/constants/config/util' -import {initSharedSubscriptions} from './shared' import {switchTab} from '../router2/util' import {storeRegistry} from '../store-registry' -import {wrapErrors} from '@/util/debug' +import {onEngineConnected, onEngineDisconnected} from '@/constants/platform-specific/shared' +import {handleAppLink, handleSaltPackOpen} from '../deeplinks' -const {showMainWindow, activeChanged, requestWindowsStartService, dumpNodeLogger} = KB2.functions -const {quitApp, exitApp, setOpenAtLogin, ctlQuit, copyToClipboard} = KB2.functions +const {ctlQuit, dumpNodeLogger} = KB2.functions export const requestPermissionsToWrite = async () => { return Promise.resolve(true) @@ -43,30 +27,6 @@ export async function saveAttachmentToCameraRoll() { export const requestLocationPermission = async () => Promise.resolve() export const watchPositionForMap = async () => Promise.resolve(() => {}) -const maybePauseVideos = () => { - const {appFocused} = useConfigState.getState() - const videos = document.querySelectorAll('video') - const allVideos = Array.from(videos) - - allVideos.forEach(v => { - if (appFocused) { - if (v.hasAttribute('data-focus-paused')) { - if (v.paused) { - v.play() - .then(() => {}) - .catch(() => {}) - } - } - } else { - // only pause looping videos - if (!v.paused && v.hasAttribute('loop') && v.hasAttribute('autoplay')) { - v.setAttribute('data-focus-paused', 'true') - v.pause() - } - } - }) -} - export const dumpLogs = async (reason?: string) => { await logger.dump() await (dumpNodeLogger?.() ?? Promise.resolve([])) @@ -76,229 +36,6 @@ export const dumpLogs = async (reason?: string) => { } } -export const initPlatformListener = () => { - useConfigState.setState(s => { - s.dispatch.dynamic.dumpLogsNative = dumpLogs - s.dispatch.dynamic.showMainNative = wrapErrors(() => showMainWindow?.()) - s.dispatch.dynamic.copyToClipboard = wrapErrors((s: string) => copyToClipboard?.(s)) - s.dispatch.dynamic.onEngineConnectedDesktop = wrapErrors(() => { - // Introduce ourselves to the service - const f = async () => { - await T.RPCGen.configHelloIAmRpcPromise({details: KB2.constants.helloDetails}) - } - ignorePromise(f()) - }) - - s.dispatch.dynamic.onEngineIncomingDesktop = wrapErrors((action: EngineGen.Actions) => { - switch (action.type) { - case EngineGen.keybase1LogsendPrepareLogsend: { - const f = async () => { - const response = action.payload.response - try { - await dumpLogs() - } finally { - response.result() - } - } - ignorePromise(f()) - break - } - case EngineGen.keybase1NotifyAppExit: - console.log('App exit requested') - exitApp?.(0) - break - case EngineGen.keybase1NotifyFSFSActivity: - kbfsNotification(action.payload.params.notification, NotifyPopup) - break - case EngineGen.keybase1NotifyPGPPgpKeyInSecretStoreFile: { - const f = async () => { - try { - await T.RPCGen.pgpPgpStorageDismissRpcPromise() - } catch (err) { - console.warn('Error in sending pgpPgpStorageDismissRpc:', err) - } - } - ignorePromise(f()) - break - } - case EngineGen.keybase1NotifyServiceShutdown: { - const {code} = action.payload.params - if (isWindows && code !== (T.RPCGen.ExitCode.restart as number)) { - console.log('Quitting due to service shutdown with code: ', code) - // Quit just the app, not the service - quitApp?.() - } - break - } - - case EngineGen.keybase1LogUiLog: { - const {params} = action.payload - const {level, text} = params - logger.info('keybase.1.logUi.log:', params.text.data) - if (level >= T.RPCGen.LogLevel.error) { - NotifyPopup(text.data) - } - break - } - - case EngineGen.keybase1NotifySessionClientOutOfDate: { - const {upgradeTo, upgradeURI, upgradeMsg} = action.payload.params - const body = upgradeMsg || `Please update to ${upgradeTo} by going to ${upgradeURI}` - NotifyPopup('Client out of date!', {body}, 60 * 60) - // This is from the API server. Consider notifications from server always critical. - useConfigState - .getState() - .dispatch.setOutOfDate({critical: true, message: upgradeMsg, outOfDate: true, updating: false}) - break - } - default: - } - }) - }) - - useConfigState.subscribe((s, old) => { - if (s.loggedIn !== old.loggedIn) { - s.dispatch.osNetworkStatusChanged(navigator.onLine, 'notavailable', true) - } - - if (s.appFocused !== old.appFocused) { - maybePauseVideos() - } - - if (s.openAtLogin !== old.openAtLogin) { - const {openAtLogin} = s - const f = async () => { - if (__DEV__) { - console.log('onSetOpenAtLogin disabled for dev mode') - return - } else { - await T.RPCGen.configGuiSetValueRpcPromise({ - path: ConfigConstants.openAtLoginKey, - value: {b: openAtLogin, isNull: false}, - }) - } - if (isLinux || isWindows) { - const enabled = - (await T.RPCGen.ctlGetOnLoginStartupRpcPromise()) === T.RPCGen.OnLoginStartupStatus.enabled - if (enabled !== openAtLogin) { - try { - await T.RPCGen.ctlSetOnLoginStartupRpcPromise({enabled: openAtLogin}) - } catch (error_) { - const error = error_ as RPCError - logger.warn(`Error in sending ctlSetOnLoginStartup: ${error.message}`) - } - } - } else { - logger.info(`Login item settings changed! now ${openAtLogin ? 'on' : 'off'}`) - await setOpenAtLogin?.(openAtLogin) - } - } - ignorePromise(f()) - } - }) - - const handleWindowFocusEvents = () => { - const handle = (appFocused: boolean) => { - if (skipAppFocusActions) { - console.log('Skipping app focus actions!') - } else { - useConfigState.getState().dispatch.changedFocus(appFocused) - } - } - window.addEventListener('focus', () => handle(true)) - window.addEventListener('blur', () => handle(false)) - } - handleWindowFocusEvents() - - const setupReachabilityWatcher = () => { - window.addEventListener('online', () => - useConfigState.getState().dispatch.osNetworkStatusChanged(true, 'notavailable') - ) - window.addEventListener('offline', () => - useConfigState.getState().dispatch.osNetworkStatusChanged(false, 'notavailable') - ) - } - setupReachabilityWatcher() - - useDaemonState.subscribe((s, old) => { - if (s.handshakeVersion !== old.handshakeVersion) { - if (!isWindows) return - - const f = async () => { - const waitKey = 'pipeCheckFail' - const version = s.handshakeVersion - const {wait} = s.dispatch - wait(waitKey, version, true) - try { - logger.info('Checking RPC ownership') - if (KB2.functions.winCheckRPCOwnership) { - await KB2.functions.winCheckRPCOwnership() - } - wait(waitKey, version, false) - } catch (error_) { - // error will be logged in bootstrap check - getEngine().reset() - const error = error_ as RPCError - wait(waitKey, version, false, error.message || 'windows pipe owner fail', true) - } - } - ignorePromise(f()) - } - - if (s.handshakeState !== old.handshakeState && s.handshakeState === 'done') { - useConfigState.getState().dispatch.setStartupDetails({ - conversation: Chat.noConversationIDKey, - followUser: '', - link: '', - tab: undefined, - }) - } - }) - - if (isLinux) { - useConfigState.getState().dispatch.initUseNativeFrame() - } - useConfigState.getState().dispatch.initNotifySound() - useConfigState.getState().dispatch.initForceSmallNav() - useConfigState.getState().dispatch.initOpenAtLogin() - useConfigState.getState().dispatch.initAppUpdateLoop() - - useProfileState.setState(s => { - s.dispatch.editAvatar = () => { - useRouterState - .getState() - .dispatch.navigateAppend({props: {image: undefined}, selected: 'profileEditAvatar'}) - } - }) - - const initializeInputMonitor = () => { - const inputMonitor = new InputMonitor() - inputMonitor.notifyActive = (userActive: boolean) => { - if (skipAppFocusActions) { - console.log('Skipping app focus actions!') - } else { - useActiveState.getState().dispatch.setActive(userActive) - // let node thread save file - activeChanged?.(Date.now(), userActive) - } - } - } - initializeInputMonitor() - - useDaemonState.setState(s => { - s.dispatch.onRestartHandshakeNative = () => { - const {handshakeFailedReason} = useDaemonState.getState() - if (isWindows && handshakeFailedReason === noKBFSFailReason) { - requestWindowsStartService?.() - } - } - }) - - initSharedSubscriptions() - - ignorePromise(useFSState.getState().dispatch.setupSubscriptions()) -} - const updateApp = () => { const f = async () => { await T.RPCGen.configStartUpdateIfNeededRpcPromise() @@ -333,9 +70,9 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { } case RemoteGen.engineConnection: { if (action.payload.connected) { - storeRegistry.getState('engine').dispatch.onEngineConnected() + onEngineConnected() } else { - storeRegistry.getState('engine').dispatch.onEngineDisconnected() + onEngineDisconnected() } break } @@ -356,7 +93,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { break } case RemoteGen.saltpackFileOpen: { - storeRegistry.getState('deeplinks').dispatch.handleSaltPackOpen(action.payload.path) + handleSaltPackOpen(action.payload.path) break } case RemoteGen.pinentryOnCancel: { @@ -414,7 +151,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => { case RemoteGen.link: { const {link} = action.payload - storeRegistry.getState('deeplinks').dispatch.handleAppLink(link) + handleAppLink(link) } break case RemoteGen.installerRan: diff --git a/shared/constants/platform-specific/index.native.tsx b/shared/constants/platform-specific/index.native.tsx index b3b45046629c..3f0a71e13dfc 100644 --- a/shared/constants/platform-specific/index.native.tsx +++ b/shared/constants/platform-specific/index.native.tsx @@ -1,48 +1,11 @@ -import {ignorePromise, neverThrowPromiseFunc, timeoutPromise} from '../utils' -import {useChatState} from '../chat2' -import {getConvoState} from '../chat2/convostate' -import {useConfigState} from '../config' -import {useCurrentUserState} from '../current-user' -import {useDaemonState} from '../daemon' -import {useDarkModeState} from '../darkmode' -import {useFSState} from '../fs' -import {useProfileState} from '../profile' -import {useRouterState} from '../router2' -import {useSettingsContactsState} from '../settings-contacts' import * as T from '../types' -import * as Clipboard from 'expo-clipboard' -import * as EngineGen from '@/actions/engine-gen-gen' import * as ExpoLocation from 'expo-location' -import * as ExpoTaskManager from 'expo-task-manager' import * as MediaLibrary from 'expo-media-library' -import * as Tabs from '../tabs' -import * as NetInfo from '@react-native-community/netinfo' -import NotifyPopup from '@/util/notify-popup' import {addNotificationRequest} from 'react-native-kb' import logger from '@/logger' -import {Alert, Linking, ActionSheetIOS} from 'react-native' +import {ActionSheetIOS} from 'react-native' import {isIOS, isAndroid} from '../platform.native' -import {wrapErrors} from '@/util/debug' -import {getTab, getVisiblePath, logState} from '../router2' -import {launchImageLibraryAsync} from '@/util/expo-image-picker.native' -import {setupAudioMode} from '@/util/audio.native' -import { - androidOpenSettings, - androidShare, - androidShareText, - androidUnlink, - fsCacheDir, - fsDownloadDir, - androidAppColorSchemeChanged, - guiConfig, - shareListenersRegistered, -} from 'react-native-kb' -import {initPushListener, getStartupDetailsFromInitialPush} from './push.native' -import {initSharedSubscriptions} from './shared' -import type {ImageInfo} from '@/util/expo-image-picker.native' -import {noConversationIDKey} from '@/constants/types/chat2/common' -import {getSelectedConversation} from '@/constants/chat2/common' -import {storeRegistry} from '../store-registry' +import {androidShare, androidShareText, androidUnlink} from 'react-native-kb' export const requestPermissionsToWrite = async () => { if (isAndroid) { @@ -146,505 +109,3 @@ export const showShareActionSheet = async (options: { } } } - -const loadStartupDetails = async () => { - const [routeState, initialUrl, push] = await Promise.all([ - neverThrowPromiseFunc(async () => { - try { - const config = JSON.parse(guiConfig) as {ui?: {routeState2?: string}} | undefined - return Promise.resolve(config?.ui?.routeState2 ?? '') - } catch { - return Promise.resolve('') - } - }), - neverThrowPromiseFunc(async () => Linking.getInitialURL()), - neverThrowPromiseFunc(getStartupDetailsFromInitialPush), - ] as const) - - // Clear last value to be extra safe bad things don't hose us forever - try { - await T.RPCGen.configGuiSetValueRpcPromise({ - path: 'ui.routeState2', - value: {isNull: false, s: ''}, - }) - } catch {} - - let conversation: T.Chat.ConversationIDKey | undefined - let followUser = '' - let link = '' - let tab = '' - - // Top priority, push - if (push) { - logger.info('initialState: push', push.startupConversation, push.startupFollowUser) - conversation = push.startupConversation - followUser = push.startupFollowUser ?? '' - } else if (initialUrl) { - // Second priority, deep link - link = initialUrl - } else if (routeState) { - // Last priority, saved from last session - try { - const item = JSON.parse(routeState) as - | undefined - | {param?: {selectedConversationIDKey?: unknown}; routeName?: string} - if (item) { - const _convo = item.param?.selectedConversationIDKey || undefined - if (typeof _convo === 'string') { - conversation = _convo - logger.info('initialState: routeState', conversation) - } - const _rn = item.routeName || undefined - if (typeof _rn === 'string') { - tab = _rn as unknown as typeof tab - } - } - } catch { - logger.info('initialState: routeState parseFail') - conversation = undefined - tab = '' - } - } - - // never allow this case - if (tab === 'blank') { - tab = '' - } - - useConfigState.getState().dispatch.setStartupDetails({ - conversation: conversation ?? noConversationIDKey, - followUser, - link, - tab: tab as Tabs.Tab, - }) -} - -const setPermissionDeniedCommandStatus = (conversationIDKey: T.Chat.ConversationIDKey, text: string) => { - getConvoState(conversationIDKey).dispatch.setCommandStatusInfo({ - actions: [T.RPCChat.UICommandStatusActionTyp.appsettings], - displayText: text, - displayType: T.RPCChat.UICommandStatusDisplayTyp.error, - }) -} - -const onChatWatchPosition = async (action: EngineGen.Chat1ChatUiChatWatchPositionPayload) => { - const response = action.payload.response - response.result(0) - try { - await requestLocationPermission(action.payload.params.perm) - } catch (_error) { - const error = _error as {message?: string} - logger.info('failed to get location perms: ' + error.message) - setPermissionDeniedCommandStatus( - T.Chat.conversationIDToKey(action.payload.params.convID), - `Failed to access location. ${error.message}` - ) - } - - locationRefs++ - - if (locationRefs === 1) { - try { - logger.info( - '[location] location watch start due to ', - T.Chat.conversationIDToKey(action.payload.params.convID) - ) - ensureBackgroundTask() - await ExpoLocation.startLocationUpdatesAsync(locationTaskName, { - deferredUpdatesDistance: 65, - pausesUpdatesAutomatically: true, - showsBackgroundLocationIndicator: true, - }) - logger.info('[location] start success') - } catch { - logger.info('[location] start failed') - locationRefs-- - } - } -} - -const onChatClearWatch = async () => { - locationRefs-- - if (locationRefs <= 0) { - try { - logger.info('[location] end start') - ensureBackgroundTask() - await ExpoLocation.stopLocationUpdatesAsync(locationTaskName) - logger.info('[location] end success') - } catch { - logger.info('[location] end failed') - } - } -} - -const locationTaskName = 'background-location-task' -let locationRefs = 0 -let madeBackgroundTask = false - -const ensureBackgroundTask = () => { - if (madeBackgroundTask) return - madeBackgroundTask = true - - ExpoTaskManager.defineTask(locationTaskName, async ({data, error}) => { - if (error) { - // check `error.message` for more details. - return Promise.resolve() - } - - if (!data) { - return Promise.resolve() - } - const d = data as {locations?: Array} - const locations = d.locations - if (!locations?.length) { - return Promise.resolve() - } - const pos = locations.at(-1) - const coord = { - accuracy: Math.floor(pos?.coords.accuracy ?? 0), - lat: pos?.coords.latitude ?? 0, - lon: pos?.coords.longitude ?? 0, - } - - useChatState.getState().dispatch.updateLastCoord(coord) - return Promise.resolve() - }) -} - -export const watchPositionForMap = async (conversationIDKey: T.Chat.ConversationIDKey) => { - try { - logger.info('[location] perms check due to map') - await requestLocationPermission(T.RPCChat.UIWatchPositionPerm.base) - } catch (_error) { - const error = _error as {message?: string} - logger.info('failed to get location perms: ' + error.message) - setPermissionDeniedCommandStatus(conversationIDKey, `Failed to access location. ${error.message}`) - return () => {} - } - - try { - const sub = await ExpoLocation.watchPositionAsync( - {accuracy: ExpoLocation.LocationAccuracy.Highest}, - location => { - const coord = { - accuracy: Math.floor(location.coords.accuracy ?? 0), - lat: location.coords.latitude, - lon: location.coords.longitude, - } - useChatState.getState().dispatch.updateLastCoord(coord) - } - ) - return () => sub.remove() - } catch (_error) { - const error = _error as {message?: string} - logger.info('failed to get location: ' + error.message) - setPermissionDeniedCommandStatus(conversationIDKey, `Failed to access location. ${error.message}`) - return () => {} - } -} - -export const initPlatformListener = () => { - let _lastPersist = '' - useConfigState.setState(s => { - s.dispatch.dynamic.persistRoute = wrapErrors((path?: ReadonlyArray) => { - const f = async () => { - if (!useConfigState.getState().startup.loaded) { - return - } - let param = {} - let routeName = Tabs.peopleTab - if (path) { - const cur = getTab() - if (cur) { - routeName = cur - } - const ap = getVisiblePath() - ap.some(r => { - if (r.name === 'chatConversation') { - const rParams = r.params as undefined | {conversationIDKey?: T.Chat.ConversationIDKey} - param = {selectedConversationIDKey: rParams?.conversationIDKey} - return true - } - return false - }) - } - const s = JSON.stringify({param, routeName}) - // don't keep rewriting - if (_lastPersist === s) { - return - } - _lastPersist = s - - await T.RPCGen.configGuiSetValueRpcPromise({ - path: 'ui.routeState2', - value: {isNull: false, s}, - }) - } - ignorePromise(f()) - }) - - s.dispatch.dynamic.onEngineIncomingNative = wrapErrors((action: EngineGen.Actions) => { - switch (action.type) { - default: - } - }) - }) - - useConfigState.subscribe((s, old) => { - if (s.mobileAppState === old.mobileAppState) return - let appFocused: boolean - let logState: T.RPCGen.MobileAppState - switch (s.mobileAppState) { - case 'active': - appFocused = true - logState = T.RPCGen.MobileAppState.foreground - break - case 'background': - appFocused = false - logState = T.RPCGen.MobileAppState.background - break - case 'inactive': - appFocused = false - logState = T.RPCGen.MobileAppState.inactive - break - default: - appFocused = false - logState = T.RPCGen.MobileAppState.foreground - } - - logger.info(`setting app state on service to: ${logState}`) - s.dispatch.changedFocus(appFocused) - - if (appFocused && old.mobileAppState !== 'active') { - const {dispatch} = storeRegistry.getConvoState(getSelectedConversation()) - dispatch.loadMoreMessages({reason: 'foregrounding'}) - dispatch.markThreadAsRead() - } - }) - - useConfigState.setState(s => { - s.dispatch.dynamic.copyToClipboard = wrapErrors((s: string) => { - Clipboard.setStringAsync(s) - .then(() => {}) - .catch(() => {}) - }) - }) - - const configureAndroidCacheDir = () => { - if (isAndroid && fsCacheDir && fsDownloadDir) { - ignorePromise( - T.RPCChat.localConfigureFileAttachmentDownloadLocalRpcPromise({ - // Android's cache dir is (when I tried) [app]/cache but Go side uses - // [app]/.cache by default, which can't be used for sharing to other apps. - cacheDirOverride: fsCacheDir, - downloadDirOverride: fsDownloadDir, - }) - .then(() => {}) - .catch((e: unknown) => { - logger.error(`[Android cache override] Failed to configure: ${String(e)}`) - }) - ) - } else if (isAndroid) { - logger.warn( - `[Android cache override] Missing dirs - cacheDir: ${fsCacheDir}, downloadDir: ${fsDownloadDir}` - ) - } - } - - useDaemonState.subscribe((s, old) => { - const versionChanged = s.handshakeVersion !== old.handshakeVersion - const stateChanged = s.handshakeState !== old.handshakeState - const justBecameReady = stateChanged && s.handshakeState === 'done' && old.handshakeState !== 'done' - - if (versionChanged || justBecameReady) { - configureAndroidCacheDir() - } - }) - - useConfigState.setState(s => { - s.dispatch.dynamic.onFilePickerError = wrapErrors((error: Error) => { - Alert.alert('Error', String(error)) - }) - s.dispatch.dynamic.openAppStore = wrapErrors(() => { - Linking.openURL( - isAndroid - ? 'http://play.google.com/store/apps/details?id=io.keybase.ossifrage' - : 'https://itunes.apple.com/us/app/keybase-crypto-for-everyone/id1044461770?mt=8' - ).catch(() => {}) - }) - }) - - useProfileState.setState(s => { - s.dispatch.editAvatar = () => { - const f = async () => { - try { - const result = await launchImageLibraryAsync('photo') - const first = result.assets?.reduce((acc, a) => { - if (!acc && (a.type === 'image' || a.type === 'video')) { - return a as ImageInfo - } - return acc - }, undefined) - if (!result.canceled && first) { - useRouterState - .getState() - .dispatch.navigateAppend({props: {image: first}, selected: 'profileEditAvatar'}) - } - } catch (error) { - useConfigState.getState().dispatch.filePickerError(new Error(String(error))) - } - } - ignorePromise(f()) - } - }) - - useConfigState.subscribe((s, old) => { - if (s.loggedIn === old.loggedIn) return - const f = async () => { - const {type} = await NetInfo.fetch() - s.dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type, true) - } - ignorePromise(f()) - }) - - useConfigState.subscribe((s, old) => { - if (s.networkStatus === old.networkStatus) return - const type = s.networkStatus?.type - if (!type) return - const f = async () => { - try { - await T.RPCGen.appStateUpdateMobileNetStateRpcPromise({state: type}) - } catch (err) { - console.warn('Error sending mobileNetStateUpdate', err) - } - } - ignorePromise(f()) - }) - - useConfigState.setState(s => { - s.dispatch.dynamic.showShareActionSheet = wrapErrors( - (filePath: string, message: string, mimeType: string) => { - const f = async () => { - await showShareActionSheet({filePath, message, mimeType}) - } - ignorePromise(f()) - } - ) - }) - - useConfigState.subscribe((s, old) => { - if (s.mobileAppState === old.mobileAppState) return - if (s.mobileAppState === 'active') { - // only reload on foreground - useSettingsContactsState.getState().dispatch.loadContactPermissions() - } - }) - - // Location - if (isAndroid) { - useDarkModeState.subscribe((s, old) => { - if (s.darkModePreference === old.darkModePreference) return - androidAppColorSchemeChanged(s.darkModePreference) - }) - } - - // we call this when we're logged in. - let calledShareListenersRegistered = false - - useRouterState.subscribe((s, old) => { - const next = s.navState - const prev = old.navState - if (next === prev) return - const f = async () => { - await timeoutPromise(1000) - const path = getVisiblePath() - useConfigState.getState().dispatch.dynamic.persistRoute?.(path) - } - - if (!calledShareListenersRegistered && logState().loggedIn) { - calledShareListenersRegistered = true - shareListenersRegistered() - } - ignorePromise(f()) - }) - - // Start this immediately instead of waiting so we can do more things in parallel - ignorePromise(loadStartupDetails()) - initPushListener() - - NetInfo.addEventListener(({type}) => { - useConfigState.getState().dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type) - }) - - const initAudioModes = () => { - ignorePromise(setupAudioMode(false)) - } - initAudioModes() - - if (isAndroid) { - const daemonState = useDaemonState.getState() - if (daemonState.handshakeState === 'done' || daemonState.handshakeVersion > 0) { - configureAndroidCacheDir() - } - } - - useConfigState.setState(s => { - s.dispatch.dynamic.openAppSettings = wrapErrors(() => { - const f = async () => { - if (isAndroid) { - androidOpenSettings() - } else { - const settingsURL = 'app-settings:' - const can = await Linking.canOpenURL(settingsURL) - if (can) { - await Linking.openURL(settingsURL) - } else { - logger.warn('Unable to open app settings') - } - } - } - ignorePromise(f()) - }) - - s.dispatch.dynamic.onEngineIncomingNative = wrapErrors((action: EngineGen.Actions) => { - switch (action.type) { - case EngineGen.chat1ChatUiTriggerContactSync: - useSettingsContactsState.getState().dispatch.manageContactsCache() - break - case EngineGen.keybase1LogUiLog: { - const {params} = action.payload - const {level, text} = params - logger.info('keybase.1.logUi.log:', params.text.data) - if (level >= T.RPCGen.LogLevel.error) { - NotifyPopup(text.data) - } - break - } - case EngineGen.chat1ChatUiChatWatchPosition: - ignorePromise(onChatWatchPosition(action)) - break - case EngineGen.chat1ChatUiChatClearWatch: - ignorePromise(onChatClearWatch()) - break - default: - } - }) - }) - - useRouterState.setState(s => { - s.dispatch.dynamic.tabLongPress = wrapErrors((tab: string) => { - if (tab !== Tabs.peopleTab) return - const accountRows = useConfigState.getState().configuredAccounts - const current = useCurrentUserState.getState().username - const row = accountRows.find(a => a.username !== current && a.hasStoredSecret) - if (row) { - useConfigState.getState().dispatch.setUserSwitching(true) - useConfigState.getState().dispatch.login(row.username, '') - } - }) - }) - - initSharedSubscriptions() - - ignorePromise(useFSState.getState().dispatch.setupSubscriptions()) -} diff --git a/shared/constants/platform-specific/push.native.tsx b/shared/constants/platform-specific/push.native.tsx index 87936d03820d..25f3ed270787 100644 --- a/shared/constants/platform-specific/push.native.tsx +++ b/shared/constants/platform-specific/push.native.tsx @@ -10,6 +10,7 @@ import { removeAllPendingNotificationRequests, } from 'react-native-kb' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useLogoutState} from '../logout' type DataCommon = { @@ -162,7 +163,7 @@ const getStartupDetailsFromInitialPush = async () => { export const initPushListener = () => { // Permissions - storeRegistry.getStore('config').subscribe((s, old) => { + useConfigState.subscribe((s, old) => { if (s.mobileAppState === old.mobileAppState) return // Only recheck on foreground, not background if (s.mobileAppState !== 'active') { @@ -184,7 +185,7 @@ export const initPushListener = () => { }) let lastCount = -1 - storeRegistry.getStore('config').subscribe((s, old) => { + useConfigState.subscribe((s, old) => { if (s.badgeState === old.badgeState) return if (!s.badgeState) return const count = s.badgeState.bigTeamBadgeCount + s.badgeState.smallTeamBadgeCount @@ -221,7 +222,7 @@ export const initPushListener = () => { if (isAndroid) { RNEmitter.addListener('onShareData', (evt: {text?: string; localPaths?: Array}) => { - const {setAndroidShare} = storeRegistry.getState('config').dispatch + const {setAndroidShare} = useConfigState.getState().dispatch const text = evt.text const urls = evt.localPaths diff --git a/shared/constants/platform-specific/shared.tsx b/shared/constants/platform-specific/shared.tsx index ed3a135c1de9..417cddd4cc44 100644 --- a/shared/constants/platform-specific/shared.tsx +++ b/shared/constants/platform-specific/shared.tsx @@ -21,6 +21,7 @@ import * as GitUtil from '../git/util' import * as NotifUtil from '../notifications/util' import * as PeopleUtil from '../people/util' import * as PinentryUtil from '../pinentry/util' +import {useProvisionState} from '../provision' import {storeRegistry} from '../store-registry' import {useSettingsContactsState} from '../settings-contacts' import * as SettingsUtil from '../settings/util' @@ -30,10 +31,32 @@ import * as TeamsUtil from '../teams/util' import * as TrackerUtil from '../tracker2/util' import * as UnlockFoldersUtil from '../unlock-folders/util' import * as UsersUtil from '../users/util' +import {useWhatsNewState} from '../whats-new' +import {onEngineIncoming as onEngineIncomingPlatform} from '../init' let _emitStartupOnLoadDaemonConnectedOnce = false +export const onEngineConnected = () => { + ChatUtil.onEngineConnected() + useConfigState.getState().dispatch.onEngineConnected() + storeRegistry.getState('daemon').dispatch.startHandshake() + NotifUtil.onEngineConnected() + PeopleUtil.onEngineConnected() + PinentryUtil.onEngineConnected() + TrackerUtil.onEngineConnected() + UnlockFoldersUtil.onEngineConnected() +} + +export const onEngineDisconnected = () => { + const f = async () => { + await logger.dump() + } + ignorePromise(f()) + storeRegistry.getState('daemon').dispatch.setError(new Error('Disconnected')) +} + export const initSharedSubscriptions = () => { + useConfigState.subscribe((s, old) => { if (s.loadOnStartPhase !== old.loadOnStartPhase) { if (s.loadOnStartPhase === 'startupOrReloginButNotInARush') { @@ -115,10 +138,10 @@ export const initSharedSubscriptions = () => { .dispatch.loadDaemonAccounts( s.configuredAccounts.length, s.loggedIn, - storeRegistry.getState('config').dispatch.refreshAccounts + useConfigState.getState().dispatch.refreshAccounts ) if (!s.loggedInCausedbyStartup) { - ignorePromise(storeRegistry.getState('config').dispatch.refreshAccounts()) + ignorePromise(useConfigState.getState().dispatch.refreshAccounts()) } } @@ -134,7 +157,7 @@ export const initSharedSubscriptions = () => { .dispatch.loadDaemonAccounts( s.configuredAccounts.length, s.loggedIn, - storeRegistry.getState('config').dispatch.refreshAccounts + useConfigState.getState().dispatch.refreshAccounts ) } @@ -147,17 +170,22 @@ export const initSharedSubscriptions = () => { storeRegistry.getState('users').dispatch.updates(updates) } } + + if (s.gregorPushState !== old.gregorPushState) { + const lastSeenItem = s.gregorPushState.find(i => i.item.category === 'whatsNewLastSeenVersion') + useWhatsNewState.getState().dispatch.updateLastSeen(lastSeenItem) + } }) useDaemonState.subscribe((s, old) => { if (s.handshakeVersion !== old.handshakeVersion) { useDarkModeState.getState().dispatch.loadDarkPrefs() storeRegistry.getState('chat').dispatch.loadStaticConfig() - const configState = storeRegistry.getState('config') + const configState = useConfigState.getState() s.dispatch.loadDaemonAccounts( configState.configuredAccounts.length, configState.loggedIn, - storeRegistry.getState('config').dispatch.refreshAccounts + useConfigState.getState().dispatch.refreshAccounts ) } @@ -167,7 +195,7 @@ export const initSharedSubscriptions = () => { const {deviceID, deviceName, loggedIn, uid, username, userReacjis} = bootstrap useCurrentUserState.getState().dispatch.setBootstrap({deviceID, deviceName, uid, username}) - const configDispatch = storeRegistry.getState('config').dispatch + const configDispatch = useConfigState.getState().dispatch if (username) { configDispatch.setDefaultUsername(username) } @@ -188,9 +216,26 @@ export const initSharedSubscriptions = () => { if (s.handshakeState === 'done') { if (!_emitStartupOnLoadDaemonConnectedOnce) { _emitStartupOnLoadDaemonConnectedOnce = true - storeRegistry.getState('config').dispatch.loadOnStart('connectedToDaemonForFirstTime') + useConfigState.getState().dispatch.loadOnStart('connectedToDaemonForFirstTime') + } + } + } + }) + + useProvisionState.subscribe((s, old) => { + if (s.startProvisionTrigger !== old.startProvisionTrigger) { + useConfigState.getState().dispatch.setLoginError() + useConfigState.getState().dispatch.resetRevokedSelf() + const f = async () => { + // If we're logged in, we're coming from the user switcher; log out first to prevent the service from getting out of sync with the GUI about our logged-in-ness + if (useConfigState.getState().loggedIn) { + await T.RPCGen.loginLogoutRpcPromise( + {force: false, keepSecrets: true}, + 'config:loginAsOther' + ) } } + ignorePromise(f()) } }) } @@ -201,9 +246,8 @@ export const onEngineIncoming = (action: EngineGen.Actions) => { AvatarUtil.onEngineIncoming(action) BotsUtil.onEngineIncoming(action) ChatUtil.onEngineIncoming(action) - storeRegistry.getState('config').dispatch.dynamic.onEngineIncomingDesktop?.(action) - storeRegistry.getState('config').dispatch.dynamic.onEngineIncomingNative?.(action) - storeRegistry.getState('config').dispatch.onEngineIncoming(action) + onEngineIncomingPlatform(action) + useConfigState.getState().dispatch.onEngineIncoming(action) DeepLinksUtil.onEngineIncoming(action) DevicesUtil.onEngineIncoming(action) FollowerUtil.onEngineIncoming(action) diff --git a/shared/constants/profile/index.tsx b/shared/constants/profile/index.tsx index 5e8eaa1d4cf1..d99952fa3cdb 100644 --- a/shared/constants/profile/index.tsx +++ b/shared/constants/profile/index.tsx @@ -10,7 +10,7 @@ import {fixCrop} from '@/util/crop' import {clearModals, navigateAppend, navigateUp} from '../router2/util' import {storeRegistry} from '../store-registry' import {useCurrentUserState} from '../current-user' -import {navToProfile} from '../router2' +import {navToProfile} from '../router2/util' type ProveGenericParams = { logoBlack: T.Tracker.SiteIconSet @@ -408,12 +408,13 @@ export const useProfileState = Z.createZustand((set, get) => { s.errorCode = error.code }) if (error.code === T.RPCGen.StatusCode.scgeneric && reason === 'appLink') { - storeRegistry - .getState('deeplinks') - .dispatch.setLinkError( - "We couldn't find a valid service for proofs in this link. The link might be bad, or your Keybase app might be out of date and need to be updated." - ) - navigateAppend('keybaseLinkError') + navigateAppend({ + props: { + error: + "We couldn't find a valid service for proofs in this link. The link might be bad, or your Keybase app might be out of date and need to be updated.", + }, + selected: 'keybaseLinkError', + }) } } if (genericService) { diff --git a/shared/constants/provision/index.tsx b/shared/constants/provision/index.tsx index f5d82672bde0..e948638d1e95 100644 --- a/shared/constants/provision/index.tsx +++ b/shared/constants/provision/index.tsx @@ -1,6 +1,6 @@ -import * as C from '..' import * as T from '../types' -import {ignorePromise} from '../utils' +import {ignorePromise, wrapErrors} from '../utils' +import {waitingKeyProvision, waitingKeyProvisionForgotUsername} from '../strings' import * as Z from '@/util/zustand' import {RPCError} from '@/util/errors' import {isMobile} from '../platform' @@ -9,7 +9,6 @@ import isEqual from 'lodash/isEqual' import {rpcDeviceToDevice} from '../rpc-utils' import {invalidPasswordErrorString} from '@/constants/config/util' import {clearModals, navigateAppend} from '../router2/util' -import {storeRegistry} from '../store-registry' import {useWaitingState} from '../waiting' export type Device = { @@ -83,6 +82,7 @@ type Store = T.Immutable<{ inlineError?: RPCError passphrase: string provisionStep: number + startProvisionTrigger: number username: string }> const initialStore: Store = { @@ -100,6 +100,7 @@ const initialStore: Store = { inlineError: undefined, passphrase: '', provisionStep: 0, + startProvisionTrigger: 0, username: '', } @@ -122,15 +123,15 @@ export interface State extends Store { } export const useProvisionState = Z.createZustand((set, get) => { - const _cancel = C.wrapErrors((ignoreWarning?: boolean) => { - useWaitingState.getState().dispatch.clear(C.waitingKeyProvision) + const _cancel = wrapErrors((ignoreWarning?: boolean) => { + useWaitingState.getState().dispatch.clear(waitingKeyProvision) if (!ignoreWarning) { console.log('Provision: cancel called while not overloaded') } }) // add a new value to submit and clear things behind - const _updateAutoSubmit = C.wrapErrors((step: Store['autoSubmit'][0]) => { + const _updateAutoSubmit = wrapErrors((step: Store['autoSubmit'][0]) => { set(s => { const idx = s.autoSubmit.findIndex(a => a.type === step.type) if (idx !== -1) { @@ -140,16 +141,7 @@ export const useProvisionState = Z.createZustand((set, get) => { }) }) - const _setUsername = C.wrapErrors((username: string, restart: boolean = true) => { - set(s => { - s.username = username - s.autoSubmit = [{type: 'username'}] - }) - if (restart) { - get().dispatch.restartProvisioning() - } - }) - const _setPassphrase = C.wrapErrors((passphrase: string, restart: boolean = true) => { + const _setPassphrase = wrapErrors((passphrase: string, restart: boolean = true) => { set(s => { s.passphrase = passphrase }) @@ -159,7 +151,7 @@ export const useProvisionState = Z.createZustand((set, get) => { } }) - const _setDeviceName = C.wrapErrors((name: string, restart: boolean = true) => { + const _setDeviceName = wrapErrors((name: string, restart: boolean = true) => { set(s => { s.deviceName = name }) @@ -169,7 +161,7 @@ export const useProvisionState = Z.createZustand((set, get) => { } }) - const _submitDeviceSelect = C.wrapErrors((name: string, restart: boolean = true) => { + const _submitDeviceSelect = wrapErrors((name: string, restart: boolean = true) => { const devices = get().devices const selectedDevice = devices.find(d => d.name === name) if (!selectedDevice) { @@ -184,7 +176,7 @@ export const useProvisionState = Z.createZustand((set, get) => { } }) - const _submitTextCode = C.wrapErrors((_code: string) => { + const _submitTextCode = wrapErrors((_code: string) => { console.log('Provision, unwatched submitTextCode called') get().dispatch.restartProvisioning() }) @@ -205,7 +197,7 @@ export const useProvisionState = Z.createZustand((set, get) => { let cancelled = false const setupCancel = (response: CommonResponseHandler) => { set(s => { - s.dispatch.dynamic.cancel = C.wrapErrors(() => { + s.dispatch.dynamic.cancel = wrapErrors(() => { set(s => { s.dispatch.dynamic.cancel = _cancel }) @@ -232,7 +224,7 @@ export const useProvisionState = Z.createZustand((set, get) => { set(s => { s.error = previousErr s.codePageIncomingTextCode = phrase - s.dispatch.dynamic.submitTextCode = C.wrapErrors((code: string) => { + s.dispatch.dynamic.submitTextCode = wrapErrors((code: string) => { set(s => { s.dispatch.dynamic.submitTextCode = _submitTextCode }) @@ -260,13 +252,13 @@ export const useProvisionState = Z.createZustand((set, get) => { }, incomingCallMap: { 'keybase.1.provisionUi.DisplaySecretExchanged': () => { - useWaitingState.getState().dispatch.increment(C.waitingKeyProvision) + useWaitingState.getState().dispatch.increment(waitingKeyProvision) }, 'keybase.1.provisionUi.ProvisioneeSuccess': () => {}, 'keybase.1.provisionUi.ProvisionerSuccess': () => {}, }, params: undefined, - waitingKey: C.waitingKeyProvision, + waitingKey: waitingKeyProvision, }) } catch { } finally { @@ -274,7 +266,6 @@ export const useProvisionState = Z.createZustand((set, get) => { s.dispatch.dynamic.cancel = _cancel s.dispatch.dynamic.setDeviceName = _setDeviceName s.dispatch.dynamic.setPassphrase = _setPassphrase - s.dispatch.dynamic.setUsername = _setUsername s.dispatch.dynamic.submitDeviceSelect = _submitDeviceSelect s.dispatch.dynamic.submitTextCode = _submitTextCode }) @@ -287,7 +278,15 @@ export const useProvisionState = Z.createZustand((set, get) => { cancel: _cancel, setDeviceName: _setDeviceName, setPassphrase: _setPassphrase, - setUsername: _setUsername, + setUsername: wrapErrors((username: string, restart: boolean = true) => { + set(s => { + s.username = username + s.autoSubmit = [{type: 'username'}] + }) + if (restart) { + get().dispatch.restartProvisioning() + } + }), submitDeviceSelect: _submitDeviceSelect, submitTextCode: _submitTextCode, }, @@ -297,7 +296,7 @@ export const useProvisionState = Z.createZustand((set, get) => { try { await T.RPCGen.accountRecoverUsernameWithEmailRpcPromise( {email}, - C.waitingKeyProvisionForgotUsername + waitingKeyProvisionForgotUsername ) set(s => { s.forgotUsernameResult = 'success' @@ -315,7 +314,7 @@ export const useProvisionState = Z.createZustand((set, get) => { try { await T.RPCGen.accountRecoverUsernameWithPhoneRpcPromise( {phone}, - C.waitingKeyProvisionForgotUsername + waitingKeyProvisionForgotUsername ) set(s => { s.forgotUsernameResult = 'success' @@ -367,7 +366,7 @@ export const useProvisionState = Z.createZustand((set, get) => { // Make cancel set the flag and cancel the current rpc const setupCancel = (response: CommonResponseHandler) => { set(s => { - s.dispatch.dynamic.cancel = C.wrapErrors(() => { + s.dispatch.dynamic.cancel = wrapErrors(() => { set(s => { s.dispatch.dynamic.cancel = _cancel }) @@ -398,7 +397,7 @@ export const useProvisionState = Z.createZustand((set, get) => { set(s => { s.error = previousErr s.codePageIncomingTextCode = phrase - s.dispatch.dynamic.submitTextCode = C.wrapErrors((code: string) => { + s.dispatch.dynamic.submitTextCode = wrapErrors((code: string) => { set(s => { s.dispatch.dynamic.submitTextCode = _submitTextCode }) @@ -419,7 +418,7 @@ export const useProvisionState = Z.createZustand((set, get) => { set(s => { s.error = errorMessage s.existingDevices = T.castDraft(existingDevices ?? []) - s.dispatch.dynamic.setDeviceName = C.wrapErrors((name: string) => { + s.dispatch.dynamic.setDeviceName = wrapErrors((name: string) => { set(s => { s.dispatch.dynamic.setDeviceName = _setDeviceName }) @@ -444,7 +443,7 @@ export const useProvisionState = Z.createZustand((set, get) => { set(s => { s.error = '' s.devices = devices - s.dispatch.dynamic.submitDeviceSelect = C.wrapErrors((device: string) => { + s.dispatch.dynamic.submitDeviceSelect = wrapErrors((device: string) => { set(s => { s.dispatch.dynamic.submitDeviceSelect = _submitDeviceSelect }) @@ -473,7 +472,7 @@ export const useProvisionState = Z.createZustand((set, get) => { // Service asking us again due to an error? set(s => { s.error = retryLabel === invalidPasswordErrorString ? 'Incorrect password.' : retryLabel - s.dispatch.dynamic.setPassphrase = C.wrapErrors((passphrase: string) => { + s.dispatch.dynamic.setPassphrase = wrapErrors((passphrase: string) => { set(s => { s.dispatch.dynamic.setPassphrase = _setPassphrase }) @@ -503,7 +502,7 @@ export const useProvisionState = Z.createZustand((set, get) => { incomingCallMap: { 'keybase.1.loginUi.displayPrimaryPaperKey': () => {}, 'keybase.1.provisionUi.DisplaySecretExchanged': () => { - useWaitingState.getState().dispatch.increment(C.waitingKeyProvision) + useWaitingState.getState().dispatch.increment(waitingKeyProvision) }, 'keybase.1.provisionUi.ProvisioneeSuccess': () => {}, 'keybase.1.provisionUi.ProvisionerSuccess': () => {}, @@ -516,7 +515,7 @@ export const useProvisionState = Z.createZustand((set, get) => { paperKey: '', username, }, - waitingKey: C.waitingKeyProvision, + waitingKey: waitingKeyProvision, }) get().dispatch.resetState() } catch (_finalError) { @@ -552,23 +551,11 @@ export const useProvisionState = Z.createZustand((set, get) => { }, startProvision: (name = '', fromReset = false) => { get().dispatch.dynamic.cancel?.(true) - storeRegistry.getState('config').dispatch.setLoginError() - storeRegistry.getState('config').dispatch.resetRevokedSelf() - set(s => { + s.startProvisionTrigger++ s.username = name }) - const f = async () => { - // If we're logged in, we're coming from the user switcher; log out first to prevent the service from getting out of sync with the GUI about our logged-in-ness - if (storeRegistry.getState('config').loggedIn) { - await T.RPCGen.loginLogoutRpcPromise( - {force: false, keepSecrets: true}, - C.waitingKeyConfigLoginAsOther - ) - } - navigateAppend({props: {fromReset}, selected: 'username'}) - } - ignorePromise(f()) + navigateAppend({props: {fromReset}, selected: 'username'}) }, } diff --git a/shared/constants/push.native.tsx b/shared/constants/push.native.tsx index 59272b34c86c..9e7995bfade6 100644 --- a/shared/constants/push.native.tsx +++ b/shared/constants/push.native.tsx @@ -3,6 +3,7 @@ import * as S from './strings' import {ignorePromise, neverThrowPromiseFunc, timeoutPromise} from './utils' import {navigateAppend, navUpToScreen, switchTab} from './router2/util' import {storeRegistry} from './store-registry' +import {useConfigState} from './config' import {useCurrentUserState} from './current-user' import {useLogoutState} from './logout' import {useWaitingState} from './waiting' @@ -163,7 +164,7 @@ export const usePushState = Z.createZustand((set, get) => { } break case 'settings.contacts': - if (storeRegistry.getState('config').loggedIn) { + if (useConfigState.getState().loggedIn) { switchTab(Tabs.peopleTab) navUpToScreen('peopleRoot') } @@ -222,13 +223,13 @@ export const usePushState = Z.createZustand((set, get) => { const shownPushPrompt = await askNativeIfSystemPushPromptHasBeenShown() if (shownPushPrompt) { // we've already shown the prompt, take them to settings - storeRegistry.getState('config').dispatch.dynamic.openAppSettings?.() + useConfigState.getState().dispatch.dynamic.openAppSettings?.() get().dispatch.showPermissionsPrompt({persistSkip: true, show: false}) return } } try { - storeRegistry.getState('config').dispatch.dynamic.openAppSettings?.() + useConfigState.getState().dispatch.dynamic.openAppSettings?.() const {increment} = useWaitingState.getState().dispatch increment(S.waitingKeyPushPermissionsRequesting) await requestPermissionsFromNative() @@ -295,7 +296,7 @@ export const usePushState = Z.createZustand((set, get) => { // permissions checker finishes after the routeToInitialScreen is done. if ( p.show && - storeRegistry.getState('config').loggedIn && + useConfigState.getState().loggedIn && storeRegistry.getState('daemon').handshakeState === 'done' && !get().justSignedUp && !get().hasPermissions diff --git a/shared/constants/recover-password/index.tsx b/shared/constants/recover-password/index.tsx index 47d6eec523fa..1012961c6f45 100644 --- a/shared/constants/recover-password/index.tsx +++ b/shared/constants/recover-password/index.tsx @@ -8,6 +8,7 @@ import {type Device} from '../provision' import {rpcDeviceToDevice} from '../rpc-utils' import {clearModals, navigateAppend, navigateUp} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' type Store = T.Immutable<{ devices: Array @@ -230,7 +231,7 @@ export const useState = Z.createZustand((set, get) => { storeRegistry .getState('router') .dispatch.navigateAppend( - storeRegistry.getState('config').loggedIn + useConfigState.getState().loggedIn ? 'recoverPasswordErrorModal' : 'recoverPasswordError', true diff --git a/shared/constants/settings-chat/index.tsx b/shared/constants/settings-chat/index.tsx index 24b2b4a1024f..615ba7a1981f 100644 --- a/shared/constants/settings-chat/index.tsx +++ b/shared/constants/settings-chat/index.tsx @@ -2,7 +2,7 @@ import * as T from '../types' import {ignorePromise} from '../utils' import * as S from '../strings' import * as Z from '@/util/zustand' -import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' export type ChatUnfurlState = { unfurlMode?: T.RPCChat.UnfurlMode @@ -49,7 +49,7 @@ export const useSettingsChatState = Z.createZustand((set, get) => { const dispatch: State['dispatch'] = { contactSettingsRefresh: () => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } try { @@ -67,7 +67,7 @@ export const useSettingsChatState = Z.createZustand((set, get) => { }, contactSettingsSaved: (enabled, indirectFollowees, teamsEnabled, teamsList) => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } @@ -100,7 +100,7 @@ export const useSettingsChatState = Z.createZustand((set, get) => { resetState: 'default', unfurlSettingsRefresh: () => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } try { @@ -128,7 +128,7 @@ export const useSettingsChatState = Z.createZustand((set, get) => { s.unfurl = T.castDraft({unfurlError: undefined, unfurlMode, unfurlWhitelist}) }) const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } try { diff --git a/shared/constants/settings-contacts.native.tsx b/shared/constants/settings-contacts.native.tsx index 2471a0051cbb..971a5bfdfab5 100644 --- a/shared/constants/settings-contacts.native.tsx +++ b/shared/constants/settings-contacts.native.tsx @@ -1,6 +1,6 @@ -import * as C from '.' import * as Contacts from 'expo-contacts' import {ignorePromise} from './utils' +import {importContactsWaitingKey} from './strings' import * as T from './types' import * as Z from '@/util/zustand' import {addNotificationRequest} from 'react-native-kb' @@ -11,7 +11,7 @@ import {getDefaultCountryCode} from 'react-native-kb' import {getE164} from './settings-phone' import {pluralize} from '@/util/string' import {navigateAppend} from './router2/util' -import {storeRegistry} from './store-registry' +import {useConfigState} from './config' import {useCurrentUserState} from './current-user' import {useWaitingState} from './waiting' @@ -89,7 +89,7 @@ export const useSettingsContactsState = Z.createZustand((set, get) => { } await T.RPCGen.configGuiSetValueRpcPromise( {path: importContactsConfigKey(username), value: {b: enable, isNull: false}}, - C.importContactsWaitingKey + importContactsWaitingKey ) get().dispatch.loadContactImportEnabled() } @@ -102,7 +102,7 @@ export const useSettingsContactsState = Z.createZustand((set, get) => { }, loadContactImportEnabled: () => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } const username = useCurrentUserState.getState().username @@ -114,7 +114,7 @@ export const useSettingsContactsState = Z.createZustand((set, get) => { try { const value = await T.RPCGen.configGuiGetValueRpcPromise( {path: importContactsConfigKey(username)}, - C.importContactsWaitingKey + importContactsWaitingKey ) enabled = !!value.b && !value.isNull } catch (error) { @@ -244,7 +244,7 @@ export const useSettingsContactsState = Z.createZustand((set, get) => { requestPermissions: (thenToggleImportOn?: boolean, fromSettings?: boolean) => { const f = async () => { const {decrement, increment} = useWaitingState.getState().dispatch - increment(C.importContactsWaitingKey) + increment(importContactsWaitingKey) const status = (await Contacts.requestPermissionsAsync()).status if (status === Contacts.PermissionStatus.GRANTED && thenToggleImportOn) { @@ -253,7 +253,7 @@ export const useSettingsContactsState = Z.createZustand((set, get) => { set(s => { s.permissionStatus = status }) - decrement(C.importContactsWaitingKey) + decrement(importContactsWaitingKey) } ignorePromise(f()) }, diff --git a/shared/constants/settings/index.tsx b/shared/constants/settings/index.tsx index 8832c7ff5792..16ffc111cdcf 100644 --- a/shared/constants/settings/index.tsx +++ b/shared/constants/settings/index.tsx @@ -10,6 +10,7 @@ import * as Tabs from '../tabs' import logger from '@/logger' import {clearModals, navigateAppend, switchTab} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useCurrentUserState} from '../current-user' import {useWaitingState} from '../waiting' import {processorProfileInProgressKey, traceInProgressKey} from './util' @@ -59,7 +60,7 @@ export const useSettingsState = Z.createZustand(set => { return } - if (maybeLoadAppLinkOnce || !storeRegistry.getState('config').startup.link.endsWith('/phone-app')) { + if (maybeLoadAppLinkOnce || !useConfigState.getState().startup.link.endsWith('/phone-app')) { return } maybeLoadAppLinkOnce = true @@ -102,7 +103,7 @@ export const useSettingsState = Z.createZustand(set => { } await T.RPCGen.loginAccountDeleteRpcPromise({passphrase}, S.waitingKeySettingsGeneric) - storeRegistry.getState('config').dispatch.setJustDeletedSelf(username) + useConfigState.getState().dispatch.setJustDeletedSelf(username) clearModals() navigateAppend(Tabs.loginTab) } @@ -110,7 +111,7 @@ export const useSettingsState = Z.createZustand(set => { }, loadLockdownMode: () => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } try { @@ -142,7 +143,7 @@ export const useSettingsState = Z.createZustand(set => { }, loadSettings: () => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } try { @@ -226,7 +227,7 @@ export const useSettingsState = Z.createZustand(set => { }, setLockdownMode: enabled => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } try { diff --git a/shared/constants/signup/index.tsx b/shared/constants/signup/index.tsx index d5526f28da51..dfa015b6319a 100644 --- a/shared/constants/signup/index.tsx +++ b/shared/constants/signup/index.tsx @@ -10,6 +10,7 @@ import {RPCError} from '@/util/errors' import {isValidEmail, isValidName, isValidUsername} from '@/util/simple-validators' import {navigateAppend, navigateUp} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' type Store = T.Immutable<{ devicename: string @@ -255,7 +256,7 @@ export const useSignupState = Z.createZustand((set, get) => { }) const f = async () => { // If we're logged in, we're coming from the user switcher; log out first to prevent the service from getting out of sync with the GUI about our logged-in-ness - if (storeRegistry.getState('config').loggedIn) { + if (useConfigState.getState().loggedIn) { await T.RPCGen.loginLogoutRpcPromise({force: false, keepSecrets: true}) } try { diff --git a/shared/constants/store-registry.tsx b/shared/constants/store-registry.tsx index 354d01a3fa1b..41e7718be585 100644 --- a/shared/constants/store-registry.tsx +++ b/shared/constants/store-registry.tsx @@ -9,12 +9,8 @@ import type {State as ArchiveState, useArchiveState} from './archive' import type {State as AutoResetState, useAutoResetState} from './autoreset' import type {State as BotsState, useBotsState} from './bots' import type {State as ChatState, useChatState} from './chat2' -import type {State as ConfigState, useConfigState} from './config' -import type {State as CryptoState, useCryptoState} from './crypto' import type {State as DaemonState, useDaemonState} from './daemon' -import type {State as DeepLinksState, useDeepLinksState} from './deeplinks' import type {State as DevicesState, useDevicesState} from './devices' -import type {State as EngineState, useEngineState} from './engine' import type {State as FSState, useFSState} from './fs' import type {State as GitState, useGitState} from './git' import type {State as NotificationsState, useNotifState} from './notifications' @@ -42,12 +38,8 @@ type StoreName = | 'autoreset' | 'bots' | 'chat' - | 'config' - | 'crypto' | 'daemon' - | 'deeplinks' | 'devices' - | 'engine' | 'fs' | 'git' | 'notifications' @@ -75,12 +67,8 @@ type StoreStates = { autoreset: AutoResetState bots: BotsState chat: ChatState - config: ConfigState - crypto: CryptoState daemon: DaemonState - deeplinks: DeepLinksState devices: DevicesState - engine: EngineState fs: FSState git: GitState notifications: NotificationsState @@ -109,12 +97,8 @@ type StoreHooks = { autoreset: typeof useAutoResetState bots: typeof useBotsState chat: typeof useChatState - config: typeof useConfigState - crypto: typeof useCryptoState daemon: typeof useDaemonState - deeplinks: typeof useDeepLinksState devices: typeof useDevicesState - engine: typeof useEngineState fs: typeof useFSState git: typeof useGitState notifications: typeof useNotifState @@ -161,30 +145,14 @@ class StoreRegistry { const {useChatState} = require('./chat2') return useChatState } - case 'config': { - const {useConfigState} = require('./config') - return useConfigState - } - case 'crypto': { - const {useCryptoState} = require('./crypto') - return useCryptoState - } case 'daemon': { const {useDaemonState} = require('./daemon') return useDaemonState } - case 'deeplinks': { - const {useDeepLinksState} = require('./deeplinks') - return useDeepLinksState - } case 'devices': { const {useDevicesState} = require('./devices') return useDevicesState } - case 'engine': { - const {useEngineState} = require('./engine') - return useEngineState - } case 'fs': { const {useFSState} = require('./fs') return useFSState diff --git a/shared/constants/team-building/index.tsx b/shared/constants/team-building/index.tsx index 0d838f7c2332..ddef3357d5dc 100644 --- a/shared/constants/team-building/index.tsx +++ b/shared/constants/team-building/index.tsx @@ -14,6 +14,7 @@ import {registerDebugClear} from '@/util/debug' import {searchWaitingKey} from './utils' import {navigateUp} from '../router2/util' import {storeRegistry} from '../store-registry' +import {useCryptoState} from '../crypto' export {allServices, selfToUser, searchWaitingKey} from './utils' type Store = T.Immutable<{ @@ -360,7 +361,7 @@ const createSlice: Z.ImmerStateCreator = (set, get) => { const {finishedTeam, namespace} = get() switch (namespace) { case 'crypto': { - storeRegistry.getState('crypto').dispatch.onTeamBuildingFinished(finishedTeam) + useCryptoState.getState().dispatch.onTeamBuildingFinished(finishedTeam) break } case 'chat2': { diff --git a/shared/constants/teams/index.tsx b/shared/constants/teams/index.tsx index 3c99b98e7e04..748e2fc01423 100644 --- a/shared/constants/teams/index.tsx +++ b/shared/constants/teams/index.tsx @@ -20,6 +20,7 @@ import {mapGetEnsureValue} from '@/util/map' import {bodyToJSON} from '../rpc-utils' import {fixCrop} from '@/util/crop' import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' import {useCurrentUserState} from '../current-user' import * as Util from './util' import {getTab} from '../router2/util' @@ -1792,7 +1793,7 @@ export const useTeamsState = Z.createZustand((set, get) => { const f = async () => { const username = useCurrentUserState.getState().username - const loggedIn = storeRegistry.getState('config').loggedIn + const loggedIn = useConfigState.getState().loggedIn if (!username || !loggedIn) { logger.warn('getTeams while logged out') return @@ -2308,7 +2309,7 @@ export const useTeamsState = Z.createZustand((set, get) => { break case EngineGen.keybase1NotifyBadgesBadgeState: { const {badgeState} = action.payload.params - const loggedIn = storeRegistry.getState('config').loggedIn + const loggedIn = useConfigState.getState().loggedIn if (loggedIn) { const deletedTeams = badgeState.deletedTeams || [] const newTeams = new Set(badgeState.newTeams || []) @@ -2539,14 +2540,14 @@ export const useTeamsState = Z.createZustand((set, get) => { const convID = T.Chat.keyToConversationID(conversationIDKey) await T.RPCChat.localJoinConversationByIDLocalRpcPromise({convID}, waitingKey) } catch (error) { - storeRegistry.getState('config').dispatch.setGlobalError(error) + useConfigState.getState().dispatch.setGlobalError(error) } } else { try { const convID = T.Chat.keyToConversationID(conversationIDKey) await T.RPCChat.localLeaveConversationLocalRpcPromise({convID}, waitingKey) } catch (error) { - storeRegistry.getState('config').dispatch.setGlobalError(error) + useConfigState.getState().dispatch.setGlobalError(error) } } } @@ -2659,14 +2660,14 @@ export const useTeamsState = Z.createZustand((set, get) => { teamID, }) } catch (payload) { - storeRegistry.getState('config').dispatch.setGlobalError(payload) + useConfigState.getState().dispatch.setGlobalError(payload) } } if (ignoreAccessRequests !== settings.ignoreAccessRequests) { try { await T.RPCGen.teamsSetTarsDisabledRpcPromise({disabled: settings.ignoreAccessRequests, teamID}) } catch (payload) { - storeRegistry.getState('config').dispatch.setGlobalError(payload) + useConfigState.getState().dispatch.setGlobalError(payload) } } if (publicityAnyMember !== settings.publicityAnyMember) { @@ -2676,7 +2677,7 @@ export const useTeamsState = Z.createZustand((set, get) => { teamID, }) } catch (payload) { - storeRegistry.getState('config').dispatch.setGlobalError(payload) + useConfigState.getState().dispatch.setGlobalError(payload) } } if (publicityMember !== settings.publicityMember) { @@ -2686,14 +2687,14 @@ export const useTeamsState = Z.createZustand((set, get) => { teamID, }) } catch (payload) { - storeRegistry.getState('config').dispatch.setGlobalError(payload) + useConfigState.getState().dispatch.setGlobalError(payload) } } if (publicityTeam !== settings.publicityTeam) { try { await T.RPCGen.teamsSetTeamShowcaseRpcPromise({isShowcased: settings.publicityTeam, teamID}) } catch (payload) { - storeRegistry.getState('config').dispatch.setGlobalError(payload) + useConfigState.getState().dispatch.setGlobalError(payload) } } } diff --git a/shared/constants/tracker2/index.tsx b/shared/constants/tracker2/index.tsx index 4d4e32101536..82d96edd041e 100644 --- a/shared/constants/tracker2/index.tsx +++ b/shared/constants/tracker2/index.tsx @@ -291,13 +291,13 @@ export const useTrackerState = Z.createZustand((set, get) => { } else if (error.code === T.RPCGen.StatusCode.scnotfound) { // we're on the profile page for a user that does not exist. Currently the only way // to get here is with an invalid link or deeplink. - storeRegistry - .getState('deeplinks') - .dispatch.setLinkError( - `You followed a profile link for a user (${assertion}) that does not exist.` - ) navigateUp() - navigateAppend('keybaseLinkError') + navigateAppend({ + props: { + error: `You followed a profile link for a user (${assertion}) that does not exist.`, + }, + selected: 'keybaseLinkError', + }) } // hooked into reloadable logger.error(`Error loading profile: ${error.message}`) diff --git a/shared/constants/unlock-folders/index.tsx b/shared/constants/unlock-folders/index.tsx index dc4a1d43ca00..9cc11c3b8352 100644 --- a/shared/constants/unlock-folders/index.tsx +++ b/shared/constants/unlock-folders/index.tsx @@ -3,8 +3,7 @@ import * as T from '../types' import * as Z from '@/util/zustand' import logger from '@/logger' import {getEngine} from '@/engine/require' -import type {State as ConfigStore} from '../config' -import {storeRegistry} from '../store-registry' +import {useConfigState, type State as ConfigStore} from '../config' type Store = T.Immutable<{ devices: ConfigStore['unlockFoldersDevices'] @@ -39,7 +38,7 @@ export const useUnlockFoldersState = Z.createZustand((set, _get) => { case EngineGen.keybase1RekeyUIRefresh: { const {problemSetDevices} = action.payload.params logger.info('Asked for rekey') - storeRegistry.getState('config').dispatch.openUnlockFolders(problemSetDevices.devices ?? []) + useConfigState.getState().dispatch.openUnlockFolders(problemSetDevices.devices ?? []) break } case EngineGen.keybase1RekeyUIDelegateRekeyUI: { @@ -49,7 +48,7 @@ export const useUnlockFoldersState = Z.createZustand((set, _get) => { dangling: true, incomingCallMap: { 'keybase.1.rekeyUI.refresh': ({problemSetDevices}) => { - storeRegistry.getState('config').dispatch.openUnlockFolders(problemSetDevices.devices ?? []) + useConfigState.getState().dispatch.openUnlockFolders(problemSetDevices.devices ?? []) }, 'keybase.1.rekeyUI.rekeySendEvent': () => {}, // ignored debug call from daemon }, diff --git a/shared/constants/wallets/index.tsx b/shared/constants/wallets/index.tsx index b6d0b28eff96..3843d7919ea1 100644 --- a/shared/constants/wallets/index.tsx +++ b/shared/constants/wallets/index.tsx @@ -2,7 +2,7 @@ import * as T from '../types' import {ignorePromise} from '../utils' import * as Z from '@/util/zustand' import {loadAccountsWaitingKey} from './utils' -import {storeRegistry} from '../store-registry' +import {useConfigState} from '../config' export {loadAccountsWaitingKey} from './utils' @@ -33,7 +33,7 @@ export const useState = Z.createZustand((set, get) => { const dispatch: State['dispatch'] = { load: () => { const f = async () => { - if (!storeRegistry.getState('config').loggedIn) { + if (!useConfigState.getState().loggedIn) { return } const res = await T.RPCStellar.localGetWalletAccountsLocalRpcPromise(undefined, [ diff --git a/shared/deeplinks/error.tsx b/shared/deeplinks/error.tsx index ed8a9cda94c5..6450b1b88b1f 100644 --- a/shared/deeplinks/error.tsx +++ b/shared/deeplinks/error.tsx @@ -1,6 +1,5 @@ import * as C from '@/constants' import * as Kb from '@/common-adapters' -import {useDeepLinksState} from '@/constants/deeplinks' type KeybaseLinkErrorBodyProps = { message: string @@ -21,9 +20,9 @@ export const KeybaseLinkErrorBody = (props: KeybaseLinkErrorBodyProps) => { ) } -const KeybaseLinkError = () => { - const deepError = useDeepLinksState(s => s.keybaseLinkError) - const message = deepError +const LinkError = (props: {error?: string}) => { + const error = props.error ?? 'Invalid page! (sorry)' + const message = error const isError = true const navigateUp = C.useRouterState(s => s.dispatch.navigateUp) const onClose = () => navigateUp() @@ -48,4 +47,6 @@ const styles = Kb.Styles.styleSheetCreate(() => ({ }), })) -export default KeybaseLinkError +type OwnProps = C.ViewPropsToPageProps +const Screen = (p: OwnProps) => +export default Screen diff --git a/shared/desktop/renderer/main2.desktop.tsx b/shared/desktop/renderer/main2.desktop.tsx index 1f5a194af580..52b136dabe68 100644 --- a/shared/desktop/renderer/main2.desktop.tsx +++ b/shared/desktop/renderer/main2.desktop.tsx @@ -17,7 +17,7 @@ import type {default as NewMainType} from '../../app/main.desktop' import {setServiceDecoration} from '@/common-adapters/markdown/react' import ServiceDecoration from '@/common-adapters/markdown/service-decoration' import {useDarkModeState} from '@/constants/darkmode' -import {initPlatformListener} from '@/constants/platform-specific' +import {initPlatformListener} from '@/constants/init' setServiceDecoration(ServiceDecoration) const {ipcRendererOn, requestWindowsStartService, appStartedUp} = KB2.functions diff --git a/shared/engine/index-impl.tsx b/shared/engine/index-impl.tsx index 14f389095d1d..ac22a8e6b371 100644 --- a/shared/engine/index-impl.tsx +++ b/shared/engine/index-impl.tsx @@ -12,7 +12,7 @@ import {printOutstandingRPCs} from '@/local-debug' import {resetClient, createClient, rpcLog, type CreateClientType, type PayloadType} from './index.platform' import {type RPCError, convertToError} from '@/util/errors' import type * as EngineGen from '../actions/engine-gen-gen' -import type * as EngineConst from '@/constants/engine' +import type * as PlatSpecType from '@/constants/platform-specific/shared' // delay incoming to stop react from queueing too many setState calls and stopping rendering // only while debugging for now @@ -47,6 +47,7 @@ class Engine { _listenersAreReady: boolean = false _emitWaiting: (changes: BatchParams) => void + _incomingTimeout: NodeJS.Timeout | undefined _queuedChanges: Array<{error?: RPCError; increment: boolean; key: WaitingKey}> = [] dispatchWaitingAction = (key: WaitingKey, waiting: boolean, error?: RPCError) => { @@ -68,8 +69,15 @@ class Engine { this._onConnectedCB = onConnected // the node engine doesn't do this and we don't want to pull in any reqs if (allowIncomingCalls) { - const {useEngineState} = require('@/constants/engine') as typeof EngineConst - this._engineConstantsIncomingCall = useEngineState.getState().dispatch.onEngineIncoming + this._engineConstantsIncomingCall = (action: EngineGen.Actions) => { + // defer a frame so its more like before + this._incomingTimeout = setTimeout(() => { + // we delegate to these utils so we don't need to load stores that we don't need yet + const {onEngineIncoming: onEngineIncomingShared} = + require('@/constants/platform-specific/shared') as typeof PlatSpecType + onEngineIncomingShared(action) + }, 0) + } } this._emitWaiting = emitWaiting this._rpcClient = createClient( diff --git a/shared/provision/username-or-email.tsx b/shared/provision/username-or-email.tsx index 0ead30e44502..abf8e1800685 100644 --- a/shared/provision/username-or-email.tsx +++ b/shared/provision/username-or-email.tsx @@ -10,7 +10,7 @@ import UserCard from '@/login/user-card' import {SignupScreen, errorBanner} from '@/signup/common' import {useProvisionState} from '@/constants/provision' -type OwnProps = {fromReset?: boolean} +type OwnProps = {fromReset?: boolean; username?: string} const decodeInlineError = (inlineRPCError: RPCError | undefined) => { let inlineError = '' @@ -59,7 +59,12 @@ const UsernameOrEmailContainer = (op: OwnProps) => { }, [_setUsername, waiting] ) - const [username, setUsername] = React.useState(_username) + const [username, setUsername] = React.useState(op.username ?? _username) + React.useEffect(() => { + if (op.username && op.username !== _username) { + _setUsername?.(op.username) + } + }, [op.username, _username, _setUsername]) const onSubmit = React.useCallback(() => { _onSubmit(username) }, [_onSubmit, username]) diff --git a/shared/router-v2/hooks.native.tsx b/shared/router-v2/hooks.native.tsx index cba0b4cfaad0..bce95fef44e4 100644 --- a/shared/router-v2/hooks.native.tsx +++ b/shared/router-v2/hooks.native.tsx @@ -3,7 +3,7 @@ import * as Chat from '@/constants/chat2' import {useConfigState} from '@/constants/config' import * as Tabs from '@/constants/tabs' import * as React from 'react' -import {useDeepLinksState} from '@/constants/deeplinks' +import {handleAppLink} from '@/constants/deeplinks' import {Linking} from 'react-native' import {useColorScheme} from 'react-native' import {usePushState} from '@/constants/push' @@ -109,7 +109,7 @@ export const useInitialState = (loggedInLoaded: boolean) => { } if (url && isValidLink(url)) { - setTimeout(() => url && useDeepLinksState.getState().dispatch.handleAppLink(url), 1) + setTimeout(() => url && handleAppLink(url), 1) } else if (startupFollowUser && !startupConversation) { url = `keybase://profile/show/${startupFollowUser}`