diff --git a/go/bind/keybase.go b/go/bind/keybase.go index 46a598215680..eaea3948f68d 100644 --- a/go/bind/keybase.go +++ b/go/bind/keybase.go @@ -70,7 +70,7 @@ func log(format string, args ...interface{}) { } type PushNotifier interface { - LocalNotification(ident string, msg string, badgeCount int, soundName string, convID string, typ string) + LocalNotification(ident string, title string, msg string, badgeCount int, soundName string, convID string, typ string) DisplayChatNotification(notification *ChatNotification) } @@ -288,6 +288,7 @@ func Init(homeDir, mobileSharedHome, logFile, runModeStr string, kbCtx = libkb.NewGlobalContext() kbCtx.Init() kbCtx.SetProofServices(externals.NewProofServices(kbCtx)) + kbCtx.AddLogoutHook(accountCacheLogoutHook{}, "notifications/accountCache") var suffix string if isIPad { @@ -688,7 +689,7 @@ func pushPendingMessageFailure(obrs []chat1.OutboxRecord, pusher PushNotifier) { for _, obr := range obrs { if topicType := obr.Msg.ClientHeader.Conv.TopicType; obr.Msg.IsBadgableType() && topicType == chat1.TopicType_CHAT { kbCtx.Log.Debug("pushPendingMessageFailure: pushing convID: %s", obr.ConvID) - pusher.LocalNotification("failedpending", + pusher.LocalNotification("failedpending", "", "Heads up! Your message hasn't sent yet, tap here to retry.", -1, "default", obr.ConvID.String(), "chat.failedpending") return diff --git a/go/bind/notifications.go b/go/bind/notifications.go index 3e1a9ddbbc2b..c4788a9ca82e 100644 --- a/go/bind/notifications.go +++ b/go/bind/notifications.go @@ -6,8 +6,11 @@ import ( "fmt" "regexp" "runtime" + "strconv" + "sync" "time" + lru "github.com/hashicorp/golang-lru" "github.com/keybase/client/go/chat" "github.com/keybase/client/go/chat/globals" "github.com/keybase/client/go/chat/storage" @@ -20,6 +23,42 @@ import ( "github.com/kyokomi/emoji" ) +var ( + seenNotificationsMtx sync.Mutex + seenNotifications, _ = lru.New(100) + + multipleAccountsMtx sync.Mutex + multipleAccountsCached *bool +) + +// accountCacheLogoutHook implements libkb.LogoutHook. It clears the cached +// result of hasMultipleLoggedInAccounts so that the next background +// notification recomputes it against the post-logout account list. +type accountCacheLogoutHook struct{} + +func (accountCacheLogoutHook) OnLogout(_ libkb.MetaContext) error { + multipleAccountsMtx.Lock() + multipleAccountsCached = nil + multipleAccountsMtx.Unlock() + return nil +} + +func hasMultipleLoggedInAccounts(ctx context.Context) bool { + multipleAccountsMtx.Lock() + defer multipleAccountsMtx.Unlock() + if multipleAccountsCached != nil { + return *multipleAccountsCached + } + users, err := kbCtx.GetUsersWithStoredSecrets(ctx) + if err != nil { + // Don't cache on error; retry next time. + return false + } + result := len(users) > 1 + multipleAccountsCached = &result + return result +} + type Person struct { KeybaseUsername string KeybaseAvatar string @@ -47,6 +86,8 @@ type ChatNotification struct { IsPlaintext bool SoundName string BadgeCount int + // Title is the notification title, e.g. "username@keybase" + Title string } func HandlePostTextReply(strConvID, tlfName string, intMessageID int, body string) (err error) { @@ -106,6 +147,23 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str return libkb.LoginRequiredError{} } mp := chat.NewMobilePush(gc) + // Dedupe by convID||msgID + dupKey := strConvID + "||" + strconv.Itoa(intMessageID) + // Optimistic early-exit: check under the mutex so that if another goroutine + // is currently in the display+add critical section below, we wait for it to + // finish and then see the cache entry rather than proceeding with redundant work. + seenNotificationsMtx.Lock() + _, isDup := seenNotifications.Get(dupKey) + seenNotificationsMtx.Unlock() + if isDup { + // Cancel any duplicate visible notifications + if len(pushID) > 0 { + mp.AckNotificationSuccess(ctx, []string{pushID}) + } + kbCtx.Log.CDebugf(ctx, "HandleBackgroundNotification: duplicate notification convID=%s msgID=%d", strConvID, intMessageID) + // Return nil (not an error) so Android does not treat this as failure and show a fallback notification. + return nil + } uid := gregor1.UID(kbCtx.Env.GetUID().ToBytes()) convID, err := chat1.MakeConvID(strConvID) if err != nil { @@ -119,6 +177,11 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str return err } + currentUsername := string(kbCtx.Env.GetUsername()) + title := "Keybase" + if hasMultipleLoggedInAccounts(ctx) { + title = fmt.Sprintf("%s@keybase", currentUsername) + } chatNotification := ChatNotification{ IsPlaintext: displayPlaintext, Message: &Message{ @@ -131,10 +194,12 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str TopicName: conv.Info.TopicName, TlfName: conv.Info.TlfName, IsGroupConversation: len(conv.Info.Participants) > 2, - ConversationName: utils.FormatConversationName(conv.Info, string(kbCtx.Env.GetUsername())), + ConversationName: utils.FormatConversationName(conv.Info, currentUsername), SoundName: soundName, BadgeCount: badgeCount, + Title: title, } + kbCtx.Log.CDebugf(ctx, "HandleBackgroundNotification: title=%s", chatNotification.Title) msgUnboxed, err := mp.UnboxPushNotification(ctx, uid, convID, membersType, body) if err == nil && msgUnboxed.IsValid() { @@ -195,6 +260,22 @@ func HandleBackgroundNotification(strConvID, body, serverMessageBody, sender str // only display and ack this notification if we actually have something to display if pusher != nil && (len(chatNotification.Message.Plaintext) > 0 || len(chatNotification.Message.ServerMessage) > 0) { + // Lock and check if we've already processed this notification. + seenNotificationsMtx.Lock() + defer seenNotificationsMtx.Unlock() + if _, ok := seenNotifications.Get(dupKey); ok { + // Cancel any duplicate visible notifications + if len(pushID) > 0 { + mp.AckNotificationSuccess(ctx, []string{pushID}) + } + kbCtx.Log.CDebugf(ctx, "HandleBackgroundNotification: duplicate notification convID=%s msgID=%d", strConvID, intMessageID) + // Return nil (not an error) so Android does not treat this as failure and show a fallback notification. + return nil + } + // Add to cache before displaying so that any concurrent goroutine that + // reaches the second check while DisplayChatNotification is running will + // see the entry and bail out rather than displaying a duplicate. + seenNotifications.Add(dupKey, struct{}{}) pusher.DisplayChatNotification(&chatNotification) if len(pushID) > 0 { mp.AckNotificationSuccess(ctx, []string{pushID}) diff --git a/go/libkb/secret_store.go b/go/libkb/secret_store.go index 942d66748968..b64a2114b850 100644 --- a/go/libkb/secret_store.go +++ b/go/libkb/secret_store.go @@ -118,19 +118,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 82da5f157e02..5d5a27dc615e 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/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt b/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt index 2d071a144bfa..b3c8f2a4d9fa 100644 --- a/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt +++ b/shared/android/app/src/main/java/io/keybase/ossifrage/KBPushNotifier.kt @@ -122,6 +122,7 @@ class KBPushNotifier internal constructor(private val context: Context, private val convData = ConvData(chatNotification.convID, chatNotification.tlfName ?: "", chatNotification.message.id) val builder = NotificationCompat.Builder(context, KeybasePushNotificationListenerService.CHAT_CHANNEL_ID) .setSmallIcon(R.drawable.ic_notif) + .setContentTitle(chatNotification.title ?: "") .setContentIntent(pending_intent) .setAutoCancel(true) var notificationDefaults = NotificationCompat.DEFAULT_LIGHTS or NotificationCompat.DEFAULT_VIBRATE @@ -228,9 +229,9 @@ class KBPushNotifier internal constructor(private val context: Context, private notificationManager.notify(uniqueTag, 0, builder.build()) } - override fun localNotification(ident: String, msg: String, badgeCount: Long, soundName: String, convID: String, + override fun localNotification(ident: String, title: String, msg: String, badgeCount: Long, soundName: String, convID: String, typ: String) { - genericNotification(ident, "", msg, bundle, KeybasePushNotificationListenerService.GENERAL_CHANNEL_ID) + genericNotification(ident, title, msg, bundle, KeybasePushNotificationListenerService.GENERAL_CHANNEL_ID) } companion object { diff --git a/shared/constants/daemon/index.tsx b/shared/constants/daemon/index.tsx index 08f0bc225ced..6b203f82ea33 100644 --- a/shared/constants/daemon/index.tsx +++ b/shared/constants/daemon/index.tsx @@ -226,14 +226,14 @@ export const useDaemonState = Z.createZustand((set, get) => { const usernameToFullname: {[username: string]: string} = {} configuredAccounts.forEach(account => { - const {username, isCurrent, fullname, hasStoredSecret} = account + const {username, isCurrent, fullname, hasStoredSecret, uid} = account if (username === defaultUsername) { existingDefaultFound = true } if (isCurrent) { currentName = account.username } - nextConfiguredAccounts.push({hasStoredSecret, username}) + nextConfiguredAccounts.push({hasStoredSecret, uid, username}) usernameToFullname[username] = fullname }) if (!existingDefaultFound) { diff --git a/shared/constants/platform-specific/push.native.tsx b/shared/constants/platform-specific/push.native.tsx index 295d3927bb84..cc2f7724ac86 100644 --- a/shared/constants/platform-specific/push.native.tsx +++ b/shared/constants/platform-specific/push.native.tsx @@ -34,6 +34,7 @@ type DataNewMessageSilent2 = DataCommon & { } type DataFollow = DataCommon & { type: 'follow' + targetUID?: string username?: string } type DataChatExtension = DataCommon & { @@ -70,6 +71,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': { @@ -83,6 +86,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 || '', @@ -105,6 +109,7 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { case 'follow': return data.username ? { + forUid: forUid ?? dataUid.targetUID, type: 'follow', userInteraction, username: data.username, @@ -114,6 +119,7 @@ const normalizePush = (_n?: object): T.Push.PushNotification | undefined => { return data.convID ? { conversationIDKey: T.Chat.stringToConversationIDKey(data.convID), + forUid, type: 'chat.extension', } : undefined @@ -199,6 +205,31 @@ export const initPushListener = () => { storeRegistry.getState('push').dispatch.initialPermissionsCheck() + // Second half of the account-switch-for-notification flow: when the uid + // changes (i.e. login completes after a switch initiated in handlePush in + // constants/push.native.tsx), replay the pending notification for the new + // account if it was addressed to them. + storeRegistry.getStore('current-user').subscribe((s, old) => { + if (s.uid === old.uid) return + const pushState = storeRegistry.getState('push') + const pending = pushState.pendingPushNotification + if (!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, but not during an account switch — the switch + // flow sets userSwitching=true before triggering logout, and the pending + // notification must survive until the new account finishes bootstrapping. + storeRegistry.getStore('config').subscribe((s, old) => { + if (s.loggedIn === old.loggedIn) return + if (!s.loggedIn && !s.userSwitching) { + storeRegistry.getState('push').dispatch.clearPendingPushNotification() + } + }) + const listenNative = async () => { const RNEmitter = getNativeEmitter() diff --git a/shared/constants/push.d.ts b/shared/constants/push.d.ts index ae8fd435e57a..c0d151a5b59b 100644 --- a/shared/constants/push.d.ts +++ b/shared/constants/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 }> @@ -11,6 +12,7 @@ type Store = T.Immutable<{ export type State = Store & { dispatch: { checkPermissions: () => Promise + clearPendingPushNotification: () => void deleteToken: (version: number) => void handlePush: (notification: T.Push.PushNotification) => void initialPermissionsCheck: () => void diff --git a/shared/constants/push.desktop.tsx b/shared/constants/push.desktop.tsx index b3fed7e84595..7d4de613e78e 100644 --- a/shared/constants/push.desktop.tsx +++ b/shared/constants/push.desktop.tsx @@ -15,6 +15,7 @@ export const usePushState = Z.createZustand(() => { checkPermissions: async () => { return Promise.resolve(false) }, + clearPendingPushNotification: () => {}, deleteToken: () => {}, handlePush: () => {}, initialPermissionsCheck: () => {}, diff --git a/shared/constants/push.native.tsx b/shared/constants/push.native.tsx index 605401cfb53f..fccd7a284c0f 100644 --- a/shared/constants/push.native.tsx +++ b/shared/constants/push.native.tsx @@ -22,6 +22,7 @@ export const tokenType = isIOS ? (isDevApplePushToken ? 'appledev' : 'apple') : const initialStore: Store = { hasPermissions: true, justSignedUp: false, + pendingPushNotification: undefined, showPushPrompt: false, token: '', } @@ -112,6 +113,11 @@ export const usePushState = Z.createZustand((set, get) => { return false } }, + clearPendingPushNotification: () => { + set(s => { + s.pendingPushNotification = undefined + }) + }, deleteToken: version => { const f = async () => { const waitKey = 'push:deleteToken' @@ -141,6 +147,35 @@ export const usePushState = Z.createZustand((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 + } + // Store the notification and trigger an account switch. We do NOT + // process the notification here — execution continues once the uid + // changes, picked up by the subscriber in + // constants/platform-specific/push.native.tsx (initPushListener). + 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) { @@ -176,8 +211,7 @@ export const usePushState = Z.createZustand((set, get) => { if (__DEV__) { console.error(e) } - - logger.error('[Push] unhandled!!') + logger.error('[Push] unhandled', e) } } ignorePromise(f()) diff --git a/shared/constants/types/config.tsx b/shared/constants/types/config.tsx index e001a72201e9..2075de03c896 100644 --- a/shared/constants/types/config.tsx +++ b/shared/constants/types/config.tsx @@ -9,6 +9,7 @@ export type OutOfDate = { export type DaemonHandshakeState = 'starting' | 'waitingForWaiters' | 'done' export type ConfiguredAccount = { hasStoredSecret: boolean + uid: string username: string } // 'notavailable' is the desktop default diff --git a/shared/constants/types/push.tsx b/shared/constants/types/push.tsx index 8b35307b1543..22e5166d6ee1 100644 --- a/shared/constants/types/push.tsx +++ b/shared/constants/types/push.tsx @@ -16,19 +16,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/ios/Keybase/Fs.swift b/shared/ios/Keybase/Fs.swift index 7b6463e49ace..59192c0920e4 100644 --- a/shared/ios/Keybase/Fs.swift +++ b/shared/ios/Keybase/Fs.swift @@ -1,12 +1,15 @@ import Foundation @objc class FsHelper: NSObject { - @objc func setupFs(_ skipLogFile: Bool, setupSharedHome shouldSetupSharedHome: Bool) -> [String: String] { + @objc func setupFs(_ skipLogFile: Bool, setupSharedHome shouldSetupSharedHome: Bool) -> [String: + String] + { let setupFsStartTime = CFAbsoluteTimeGetCurrent() NSLog("setupFs: starting") var home = NSHomeDirectory() - let sharedURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.keybase") + let sharedURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.keybase") var sharedHome = sharedURL?.relativePath ?? "" home = setupAppHome(home: home, sharedHome: sharedHome) @@ -18,9 +21,12 @@ import Foundation // Put logs in a subdir that is entirely background readable let oldLogURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] .appendingPathComponent("Keybase") - let serviceLogFile = skipLogFile ? "" : oldLogURL - .appendingPathComponent("logs") - .appendingPathComponent("ios.log").path + let serviceLogFile = + skipLogFile + ? "" + : oldLogURL + .appendingPathComponent("logs") + .appendingPathComponent("ios.log").path if !skipLogFile { // cleanup old log files @@ -45,12 +51,14 @@ import Foundation "kbfs_sync_cache", "kbfs_settings", "synced_tlf_config", - "logs" + "logs", ].forEach { - createBackgroundReadableDirectory(path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) + createBackgroundReadableDirectory( + path: appKeybaseURL.appendingPathComponent($0).path, setAllFiles: true) } // Mark avatars, which are in the caches dir - createBackgroundReadableDirectory(path: oldLogURL.appendingPathComponent("avatars").path, setAllFiles: true) + createBackgroundReadableDirectory( + path: oldLogURL.appendingPathComponent("avatars").path, setAllFiles: true) let setupFsElapsed = CFAbsoluteTimeGetCurrent() - setupFsStartTime NSLog("setupFs: completed in %.3f seconds", setupFsElapsed) @@ -58,7 +66,7 @@ import Foundation return [ "home": home, "sharedHome": sharedHome, - "logFile": serviceLogFile + "logFile": serviceLogFile, ] } @@ -82,9 +90,12 @@ import Foundation // directory accessible as long as the user has unlocked the phone once. The // files are still stored on the disk encrypted (note for the chat database, // it means we are encrypting it twice), and are inaccessible otherwise. - let noProt = [FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication] + let noProt = [ + FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication + ] NSLog("creating background readable directory: path: \(path) setAllFiles: \(setAllFiles)") - _ = try? fm.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: noProt) + _ = try? fm.createDirectory( + atPath: path, withIntermediateDirectories: true, attributes: noProt) do { try fm.setAttributes(noProt, ofItemAtPath: path) } catch { @@ -111,7 +122,9 @@ import Foundation } } let dirElapsed = CFAbsoluteTimeGetCurrent() - dirStartTime - NSLog("createBackgroundReadableDirectory completed for: \(path), processed \(fileCount) files, total: %.3f seconds", dirElapsed) + NSLog( + "createBackgroundReadableDirectory completed for: \(path), processed \(fileCount) files, total: %.3f seconds", + dirElapsed) } else { NSLog("Error creating enumerator for path: \(path)") } @@ -188,7 +201,8 @@ import Foundation _ = addSkipBackupAttribute(to: sharedKeybasePath) guard maybeMigrateDirectory(source: appKeybasePath, dest: sharedKeybasePath), - maybeMigrateDirectory(source: appEraseableKVPath, dest: sharedEraseableKVPath) else { + maybeMigrateDirectory(source: appEraseableKVPath, dest: sharedEraseableKVPath) + else { return home } diff --git a/shared/ios/Keybase/MainThreadWatchdog.swift b/shared/ios/Keybase/MainThreadWatchdog.swift index b6fff1a9265c..add9b53437d2 100644 --- a/shared/ios/Keybase/MainThreadWatchdog.swift +++ b/shared/ios/Keybase/MainThreadWatchdog.swift @@ -170,7 +170,6 @@ class MainThreadWatchdog { private func captureAndLogStackTrace() { gMainStackReady = false guard let tid = mainThreadPthread else { - NSLog("[Startup] Watchdog: main thread pthread not captured") DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: main thread pthread not captured") } @@ -183,7 +182,6 @@ class MainThreadWatchdog { Thread.sleep(forTimeInterval: 0.01) } guard gMainStackReady else { - NSLog("[Startup] Watchdog: stack capture timed out") DispatchQueue.main.async { [weak self] in self?.writeLog("Watchdog: stack capture timed out") } return diff --git a/shared/ios/Keybase/Pusher.swift b/shared/ios/Keybase/Pusher.swift index 75650447a745..d6c6b84ff37d 100644 --- a/shared/ios/Keybase/Pusher.swift +++ b/shared/ios/Keybase/Pusher.swift @@ -1,17 +1,22 @@ import Foundation -import UserNotifications import Keybasego +import UserNotifications class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { - func localNotification(_ ident: String?, msg: String?, badgeCount: Int, soundName: String?, convID: String?, typ: String?) { + func localNotification( + _ ident: String?, title: String?, msg: String?, badgeCount: Int, soundName: String?, + convID: String?, typ: String? + ) { let content = UNMutableNotificationContent() if let soundName = soundName { content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName)) } content.badge = (badgeCount >= 0) ? NSNumber(value: badgeCount) : nil + content.title = title ?? "" content.body = msg ?? "" content.userInfo = ["convID": convID ?? "", "type": typ ?? ""] - let request = UNNotificationRequest(identifier: ident ?? UUID().uuidString, content: content, trigger: nil) + let request = UNNotificationRequest( + identifier: ident ?? UUID().uuidString, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) { error in if let error = error { NSLog("local notification failed: %@", error.localizedDescription) @@ -27,12 +32,17 @@ class PushNotifier: NSObject, Keybasego.KeybasePushNotifierProtocol { if notification.isPlaintext && !message.plaintext.isEmpty { let username = message.from?.keybaseUsername ?? "" let convName = notification.conversationName - msg = (username == convName || convName.isEmpty) + msg = + (username == convName || convName.isEmpty) ? "\(username): \(message.plaintext)" : "\(username) (\(convName)): \(message.plaintext)" } else { msg = message.serverMessage } - localNotification(ident, msg: msg, badgeCount: notification.badgeCount, soundName: notification.soundName, convID: notification.convID, typ: "chat.newmessage") + let title = notification.title + NSLog("PushNotifier display: title=%@", title ?? "") + localNotification( + ident, title: title, msg: msg, badgeCount: notification.badgeCount, + soundName: notification.soundName, convID: notification.convID, typ: "chat.newmessage") } } diff --git a/shared/ios/Keybase/ShareIntentDonatorImpl.swift b/shared/ios/Keybase/ShareIntentDonatorImpl.swift index c5bde51cd30a..dd9f1aaa1261 100644 --- a/shared/ios/Keybase/ShareIntentDonatorImpl.swift +++ b/shared/ios/Keybase/ShareIntentDonatorImpl.swift @@ -51,7 +51,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto NSLog("ShareIntentDonator: donateShareConversations: empty conversations array") return } - NSLog("ShareIntentDonator: donateShareConversations: donating %d conversations", conversations.count) + NSLog( + "ShareIntentDonator: donateShareConversations: donating %d conversations", conversations.count + ) donateConversations(conversations) } @@ -97,7 +99,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto } /// Loads avatar URL(s) and composites them. Apple requires a non-nil image for share sheet suggestions. - private func loadAvatars(urls: [URL], intent: INSendMessageIntent, completion: @escaping () -> Void) { + private func loadAvatars( + urls: [URL], intent: INSendMessageIntent, completion: @escaping () -> Void + ) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in let images = urls.compactMap { (try? Data(contentsOf: $0)).flatMap { UIImage(data: $0) } } if let combined = self?.compositeAvatarImages(images), let data = combined.pngData() { @@ -129,7 +133,8 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let size: CGFloat = 192 let circleSize = size * 0.65 let leftRect = CGRect(origin: .zero, size: CGSize(width: circleSize, height: circleSize)) - let rightRect = CGRect(x: size - circleSize, y: size - circleSize, width: circleSize, height: circleSize) + let rightRect = CGRect( + x: size - circleSize, y: size - circleSize, width: circleSize, height: circleSize) let fullRect = CGRect(origin: .zero, size: CGSize(width: size, height: size)) let format = UIGraphicsImageRendererFormat() format.opaque = false @@ -142,7 +147,8 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let scale = max(size / imgSize.width, size / imgSize.height) let scaledW = imgSize.width * scale let scaledH = imgSize.height * scale - let drawRect = CGRect(x: (size - scaledW) / 2, y: (size - scaledH) / 2, width: scaledW, height: scaledH) + let drawRect = CGRect( + x: (size - scaledW) / 2, y: (size - scaledH) / 2, width: scaledW, height: scaledH) self.drawImageInCircle(img, in: fullRect, drawRect: drawRect, context: cgContext) } else { self.drawImageInCircle(images[0], in: leftRect, drawRect: leftRect, context: cgContext) @@ -152,7 +158,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto } /// Draws an image clipped to an oval. Uses aspect fill when drawRect differs from clip rect. - private func drawImageInCircle(_ image: UIImage, in clipRect: CGRect, drawRect: CGRect, context: CGContext) { + private func drawImageInCircle( + _ image: UIImage, in clipRect: CGRect, drawRect: CGRect, context: CGContext + ) { context.saveGState() UIBezierPath(ovalIn: clipRect).addClip() image.draw(in: drawRect) @@ -163,7 +171,9 @@ class ShareIntentDonatorImpl: NSObject, Keybasego.KeybaseShareIntentDonatorProto let interaction = INInteraction(intent: intent, response: nil) interaction.donate { error in if let error = error { - NSLog("ShareIntentDonator: donateIntent failed for %@: %@", intent.conversationIdentifier ?? "?", error.localizedDescription) + NSLog( + "ShareIntentDonator: donateIntent failed for %@: %@", + intent.conversationIdentifier ?? "?", error.localizedDescription) } } } diff --git a/shared/ios/KeybaseShare/ShareViewController.swift b/shared/ios/KeybaseShare/ShareViewController.swift index 163e8a5e7668..041e53163cec 100644 --- a/shared/ios/KeybaseShare/ShareViewController.swift +++ b/shared/ios/KeybaseShare/ShareViewController.swift @@ -8,10 +8,10 @@ import Foundation import Intents -import UIKit -import MobileCoreServices -import Keybasego import KBCommon +import Keybasego +import MobileCoreServices +import UIKit @objc(ShareViewController) public class ShareViewController: UIViewController { @@ -33,7 +33,11 @@ public class ShareViewController: UIViewController { let sel = #selector(UIApplication.open(_:options:completionHandler:)) if r.responds(to: sel) { let imp = r.method(for: sel) - typealias Func = @convention(c) (AnyObject, Selector, URL, [UIApplication.OpenExternalURLOptionsKey: Any], ((Bool) -> Void)?) -> Void + typealias Func = + @convention(c) ( + AnyObject, Selector, URL, [UIApplication.OpenExternalURLOptionsKey: Any], + ((Bool) -> Void)? + ) -> Void let f = unsafeBitCast(imp, to: Func.self) f(r, sel, url, [:], nil) return @@ -54,7 +58,9 @@ public class ShareViewController: UIViewController { } func showProgressView() { - let alertController = UIAlertController(title: "Working on it", message: "\n\nPreparing content for sharing into Keybase.", preferredStyle: .alert) + let alertController = UIAlertController( + title: "Working on it", message: "\n\nPreparing content for sharing into Keybase.", + preferredStyle: .alert) alert = alertController let spinner = UIActivityIndicatorView(style: .medium) spinner.translatesAutoresizingMaskIntoConstraints = false @@ -62,7 +68,7 @@ public class ShareViewController: UIViewController { alertController.view.addSubview(spinner) NSLayoutConstraint.activate([ spinner.centerXAnchor.constraint(equalTo: alertController.view.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: alertController.view.centerYAnchor, constant: -8) + spinner.centerYAnchor.constraint(equalTo: alertController.view.centerYAnchor, constant: -8), ]) present(alertController, animated: true, completion: nil) } @@ -76,9 +82,10 @@ public class ShareViewController: UIViewController { if let intent = extensionContext?.intent as? INSendMessageIntent { selectedConvID = intent.conversationIdentifier } - let itemArrs = extensionContext?.inputItems.compactMap { - ($0 as? NSExtensionItem)?.attachments - } ?? [] + let itemArrs = + extensionContext?.inputItems.compactMap { + ($0 as? NSExtensionItem)?.attachments + } ?? [] weak var weakSelf = self iph = ItemProviderHelper(forShare: true, withItems: itemArrs) { diff --git a/shared/login/relogin/index.desktop.tsx b/shared/login/relogin/index.desktop.tsx index bdca6732bbad..847e375c9b83 100644 --- a/shared/login/relogin/index.desktop.tsx +++ b/shared/login/relogin/index.desktop.tsx @@ -32,7 +32,7 @@ const Login = (props: Props) => { } const userRows = props.users - .concat({hasStoredSecret: false, username: other}) + .concat({hasStoredSecret: false, uid: '', username: other}) .map(u => ) const selectedIdx = props.users.findIndex(u => u.username === props.selectedUser)