diff --git a/go/libkb/secret_store.go b/go/libkb/secret_store.go index 8427edc54ca5..4eb7645138a6 100644 --- a/go/libkb/secret_store.go +++ b/go/libkb/secret_store.go @@ -117,19 +117,22 @@ func GetConfiguredAccountsFromProvisionedUsernames(m MetaContext, s SecretStoreA allUsernames = append(allUsernames, currentUsername) } + // Build UIDs first so we can attach them to each account + uids := make([]keybase1.UID, len(allUsernames)) + for idx, username := range allUsernames { + uids[idx] = GetUIDByNormalizedUsername(m.G(), username) + } + accounts := make(map[NormalizedUsername]keybase1.ConfiguredAccount) - for _, username := range allUsernames { + for idx, username := range allUsernames { accounts[username] = keybase1.ConfiguredAccount{ Username: username.String(), IsCurrent: username.Eq(currentUsername), + Uid: uids[idx], } } // Get the full names - uids := make([]keybase1.UID, len(allUsernames)) - for idx, username := range allUsernames { - uids[idx] = GetUIDByNormalizedUsername(m.G(), username) - } usernamePackages, err := m.G().UIDMapper.MapUIDsToUsernamePackages(m.Ctx(), m.G(), uids, time.Hour*24, time.Second*10, false) if err != nil { diff --git a/go/protocol/keybase1/login.go b/go/protocol/keybase1/login.go index ebf1333733b4..a73fca4efa2b 100644 --- a/go/protocol/keybase1/login.go +++ b/go/protocol/keybase1/login.go @@ -15,6 +15,7 @@ type ConfiguredAccount struct { Fullname FullName `codec:"fullname" json:"fullname"` HasStoredSecret bool `codec:"hasStoredSecret" json:"hasStoredSecret"` IsCurrent bool `codec:"isCurrent" json:"isCurrent"` + Uid UID `codec:"uid" json:"uid"` } func (o ConfiguredAccount) DeepCopy() ConfiguredAccount { @@ -23,6 +24,7 @@ func (o ConfiguredAccount) DeepCopy() ConfiguredAccount { Fullname: o.Fullname.DeepCopy(), HasStoredSecret: o.HasStoredSecret, IsCurrent: o.IsCurrent, + Uid: o.Uid.DeepCopy(), } } diff --git a/protocol/avdl/keybase1/login.avdl b/protocol/avdl/keybase1/login.avdl index 2ee7f816283a..d46df3e0ce30 100644 --- a/protocol/avdl/keybase1/login.avdl +++ b/protocol/avdl/keybase1/login.avdl @@ -9,6 +9,7 @@ protocol login { FullName fullname; boolean hasStoredSecret; boolean isCurrent; + UID uid; } /** diff --git a/protocol/json/keybase1/login.json b/protocol/json/keybase1/login.json index ec5d4da54935..20bc3bf201b5 100644 --- a/protocol/json/keybase1/login.json +++ b/protocol/json/keybase1/login.json @@ -26,6 +26,10 @@ { "type": "boolean", "name": "isCurrent" + }, + { + "type": "UID", + "name": "uid" } ] } diff --git a/shared/constants/init/push-listener.native.tsx b/shared/constants/init/push-listener.native.tsx index 122dae3977af..633be79c58b1 100644 --- a/shared/constants/init/push-listener.native.tsx +++ b/shared/constants/init/push-listener.native.tsx @@ -37,6 +37,7 @@ type DataNewMessageSilent2 = DataCommon & { } type DataFollow = DataCommon & { type: 'follow' + targetUID?: string username?: string } type DataChatExtension = DataCommon & { @@ -73,6 +74,8 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { const data = _n as PushN const userInteraction = !!data.userInteraction + const dataUid = data as {uid?: string; targetUID?: string} + const forUid = dataUid.uid switch (data.type) { case 'chat.readmessage': { @@ -86,6 +89,7 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { return data.convID ? { conversationIDKey: T.Chat.stringToConversationIDKey(data.convID), + forUid, membersType: anyToConversationMembersType(data.t), type: 'chat.newmessage', unboxPayload: data.m || '', @@ -108,6 +112,7 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { case 'follow': return data.username ? { + forUid: forUid ?? dataUid.targetUID, type: 'follow', userInteraction, username: data.username, @@ -117,6 +122,7 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { return data.convID ? { conversationIDKey: T.Chat.stringToConversationIDKey(data.convID), + forUid, type: 'chat.extension', } : undefined @@ -213,6 +219,26 @@ export const initPushListener = () => { usePushState.getState().dispatch.initialPermissionsCheck() + // When current-user.uid changes, run pending push if it was for this account + storeRegistry.getStore('current-user').subscribe((s, old) => { + if (s.uid === old.uid) return + const pushState = storeRegistry.getState('push') + const pending = pushState.pendingPushNotification + if (!pending || !('forUid' in pending)) return + const forUid = (pending as {forUid?: string}).forUid + if (!forUid || forUid !== s.uid) return + pushState.dispatch.clearPendingPushNotification() + pushState.dispatch.handlePush(pending) + }) + + // Clear pending push on logout + storeRegistry.getStore('config').subscribe((s, old) => { + if (s.loggedIn === old.loggedIn) return + if (!s.loggedIn) { + storeRegistry.getState('push').dispatch.clearPendingPushNotification() + } + }) + const listenNative = async () => { const RNEmitter = getNativeEmitter() diff --git a/shared/constants/types/config.tsx b/shared/constants/types/config.tsx index 837ec64ebb5e..9b4451c2bccf 100644 --- a/shared/constants/types/config.tsx +++ b/shared/constants/types/config.tsx @@ -8,5 +8,6 @@ export type DaemonHandshakeState = 'starting' | 'waitingForWaiters' | 'done' export type ConfiguredAccount = { fullname?: string hasStoredSecret: boolean + uid?: string username: string } diff --git a/shared/constants/types/push.tsx b/shared/constants/types/push.tsx index 5cbaf7eeb54c..69d1d63e3730 100644 --- a/shared/constants/types/push.tsx +++ b/shared/constants/types/push.tsx @@ -14,19 +14,22 @@ export type PushNotification = } | { conversationIDKey: ChatTypes.ConversationIDKey + forUid?: string membersType?: RPCChatTypes.ConversationMembersType type: 'chat.newmessage' unboxPayload: string userInteraction: boolean } | { + forUid?: string type: 'follow' userInteraction: boolean username: string } | { - type: 'chat.extension' conversationIDKey: ChatTypes.ConversationIDKey + forUid?: string + type: 'chat.extension' } | { type: 'settings.contacts' diff --git a/shared/constants/types/rpc-gen.tsx b/shared/constants/types/rpc-gen.tsx index 8160728a8afb..6694069ec171 100644 --- a/shared/constants/types/rpc-gen.tsx +++ b/shared/constants/types/rpc-gen.tsx @@ -2788,7 +2788,7 @@ export type ComponentResult = {readonly name: String; readonly status: Status; r export type Confidence = {readonly usernameVerifiedVia: UsernameVerificationType; readonly proofs?: ReadonlyArray | null; readonly other: String} export type Config = {readonly serverURI: String; readonly socketFile: String; readonly label: String; readonly runMode: String; readonly gpgExists: Boolean; readonly gpgPath: String; readonly version: String; readonly path: String; readonly binaryRealpath: String; readonly configPath: String; readonly versionShort: String; readonly versionFull: String; readonly isAutoForked: Boolean; readonly forkType: ForkType} export type ConfigValue = {readonly isNull: Boolean; readonly b?: Boolean | null; readonly i?: Int | null; readonly f?: Double | null; readonly s?: String | null; readonly o?: String | null} -export type ConfiguredAccount = {readonly username: String; readonly fullname: FullName; readonly hasStoredSecret: Boolean; readonly isCurrent: Boolean} +export type ConfiguredAccount = {readonly username: String; readonly fullname: FullName; readonly hasStoredSecret: Boolean; readonly isCurrent: Boolean; readonly uid: UID} export type ConfirmResult = {readonly identityConfirmed: Boolean; readonly remoteConfirmed: Boolean; readonly expiringLocal: Boolean; readonly autoConfirmed: Boolean} export type ConflictGeneration = Int export type ConflictState = {conflictStateType: ConflictStateType.normalview; normalview: FolderNormalView} | {conflictStateType: ConflictStateType.manualresolvinglocalview; manualresolvinglocalview: FolderConflictManualResolvingLocalView} diff --git a/shared/desktop/CHANGELOG.txt b/shared/desktop/CHANGELOG.txt index 170335b00409..94f072196ab0 100644 --- a/shared/desktop/CHANGELOG.txt +++ b/shared/desktop/CHANGELOG.txt @@ -1,4 +1,4 @@ -• On iOS quicky share to recent conversations +• On iOS quickly share to recent conversations • Emoji 16 support • iOS HEIC Avatar support • Better sharing/push support diff --git a/shared/stores/daemon.tsx b/shared/stores/daemon.tsx index 2b27c950c916..49fbf2a62ff5 100644 --- a/shared/stores/daemon.tsx +++ b/shared/stores/daemon.tsx @@ -30,7 +30,11 @@ const initialStore: Store = { export interface State extends Store { dispatch: { - loadDaemonAccounts: (configuredAccountsLength: number, loggedIn: boolean, refreshAccounts: () => Promise) => void + loadDaemonAccounts: ( + configuredAccountsLength: number, + loggedIn: boolean, + refreshAccounts: () => Promise + ) => void loadDaemonBootstrapStatus: () => Promise resetState: () => void setError: (e?: Error) => void @@ -117,7 +121,11 @@ export const useDaemonState = Z.createZustand('daemon', (set, get) => { daemonHandshakeDone: () => { get().dispatch.setState('done') }, - loadDaemonAccounts: (configuredAccountsLength: number, loggedIn: boolean, refreshAccounts: () => Promise) => { + loadDaemonAccounts: ( + configuredAccountsLength: number, + loggedIn: boolean, + refreshAccounts: () => Promise + ) => { const f = async () => { const version = get().handshakeVersion if (configuredAccountsLength) { diff --git a/shared/stores/push.d.ts b/shared/stores/push.d.ts index e73902fd1183..dc9d73e331e8 100644 --- a/shared/stores/push.d.ts +++ b/shared/stores/push.d.ts @@ -4,6 +4,7 @@ import type {UseBoundStore, StoreApi} from 'zustand' type Store = T.Immutable<{ hasPermissions: boolean justSignedUp: boolean + pendingPushNotification?: T.Push.PushNotification showPushPrompt: boolean token: string }> @@ -14,6 +15,7 @@ export type State = Store & { onGetDaemonHandshakeState?: () => T.Config.DaemonHandshakeState } checkPermissions: () => Promise + clearPendingPushNotification: () => void deleteToken: (version: number) => void handlePush: (notification: T.Push.PushNotification) => void initialPermissionsCheck: () => void diff --git a/shared/stores/push.native.tsx b/shared/stores/push.native.tsx index 3eec6daa9e97..a24138259299 100644 --- a/shared/stores/push.native.tsx +++ b/shared/stores/push.native.tsx @@ -24,6 +24,7 @@ export const tokenType = isIOS ? (isDevApplePushToken ? 'appledev' : 'apple') : const initialStore: Store = { hasPermissions: true, justSignedUp: false, + pendingPushNotification: undefined, showPushPrompt: false, token: '', } @@ -114,6 +115,11 @@ export const usePushState = Z.createZustand('push', (set, get) => { return false } }, + clearPendingPushNotification: () => { + set(s => { + s.pendingPushNotification = undefined + }) + }, defer: { onGetDaemonHandshakeState: () => { throw new Error('onGetDaemonHandshakeState not implemented') @@ -148,6 +154,31 @@ export const usePushState = Z.createZustand('push', (set, get) => { handlePush: notification => { const f = async () => { try { + const forUid = 'forUid' in notification ? notification.forUid : undefined + + if (forUid) { + const currentUid = storeRegistry.getState('current-user').uid + if (forUid !== currentUid) { + const {configuredAccounts, dispatch: configDispatch} = storeRegistry.getState('config') + const account = configuredAccounts.find(acc => acc.uid === forUid) + if (!account) { + logger.info('[Push] notification forUid not in configured accounts, skipping') + return + } + if (!account.hasStoredSecret) { + logger.info('[Push] account has no stored secret, cannot switch') + return + } + logger.info('[Push] switching to account for notification tap') + set(s => { + s.pendingPushNotification = notification + }) + configDispatch.setUserSwitching(true) + configDispatch.login(account.username, '') + return + } + } + switch (notification.type) { case 'chat.readmessage': if (notification.badges === 0) { @@ -182,8 +213,7 @@ export const usePushState = Z.createZustand('push', (set, get) => { if (__DEV__) { console.error(e) } - - logger.error('[Push] unhandled!!') + logger.error('[Push] unhandled', e) } } ignorePromise(f())