diff --git a/lib/selectors/thread-selectors.js b/lib/selectors/thread-selectors.js --- a/lib/selectors/thread-selectors.js +++ b/lib/selectors/thread-selectors.js @@ -255,6 +255,19 @@ .map(([id]) => id), ); +const unreadFarcasterThreadIDsSelector: ( + state: BaseAppState<>, +) => $ReadOnlyArray = createSelector( + (state: BaseAppState<>) => state.threadStore.threadInfos, + (threadInfos: RawThreadInfos): $ReadOnlyArray => + Object.entries(threadInfos) + .filter( + ([, threadInfo]) => + !!threadInfo.farcaster && !!threadInfo.currentUser.unread, + ) + .map(([id]) => id), +); + const unreadCount: (state: BaseAppState<>) => number = createSelector( (state: BaseAppState<>) => state.threadStore.threadInfos, (threadInfos: RawThreadInfos): number => @@ -596,4 +609,5 @@ thickRawThreadInfosSelector, unreadThickThreadIDsSelector, getChildThreads, + unreadFarcasterThreadIDsSelector, }; diff --git a/lib/types/notif-types.js b/lib/types/notif-types.js --- a/lib/types/notif-types.js +++ b/lib/types/notif-types.js @@ -60,6 +60,9 @@ +unreadCount?: number, +threadID: string, +encryptionFailed?: '1', + +badge?: string, + +badgeOnly?: '1', + +farcasterBadge?: '1', }; export type PlainTextWebNotification = $ReadOnly<{ diff --git a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java --- a/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java +++ b/native/android/app/src/main/java/app/comm/android/notifications/CommNotificationsHandler.java @@ -58,6 +58,7 @@ private static final String KEYSERVER_ID_KEY = "keyserverID"; private static final String SENDER_DEVICE_ID_KEY = "senderDeviceID"; private static final String MESSAGE_TYPE_KEY = "type"; + private static final String FARCASTER_BADGE_KEY = "farcasterBadge"; private static final String CHANNEL_ID = "default"; private static final long[] VIBRATION_SPEC = {500, 500}; private static final Map NOTIF_PRIORITY_VERBOSE = @@ -68,6 +69,7 @@ private static final String MMKV_KEY_SEPARATOR = "."; private static final String MMKV_KEYSERVER_PREFIX = "KEYSERVER"; private static final String MMKV_UNREAD_COUNT_SUFFIX = "UNREAD_COUNT"; + private static final String MMKV_FARCASTER_KEY = "FARCASTER"; private Bitmap displayableNotificationLargeIcon; private NotificationManager notificationManager; private LocalBroadcastManager localBroadcastManager; @@ -301,6 +303,22 @@ senderKeyserverUnreadCountKey, senderKeyserverUnreadCount); } + if ("1".equals(message.getData().get(FARCASTER_BADGE_KEY)) && + message.getData().get(BADGE_KEY) != null) { + String farcasterBadge = message.getData().get(BADGE_KEY); + try { + int farcasterBadgeCount = Integer.parseInt(farcasterBadge); + String farcasterUnreadCountKey = String.join( + MMKV_KEY_SEPARATOR, + MMKV_KEYSERVER_PREFIX, + MMKV_FARCASTER_KEY, + MMKV_UNREAD_COUNT_SUFFIX); + CommMMKV.setInt(farcasterUnreadCountKey, farcasterBadgeCount); + } catch (NumberFormatException e) { + Log.w("COMM", "Invalid Farcaster badge count", e); + } + } + if (message.getData().get(SENDER_DEVICE_ID_KEY) != null && message.getData().get(THREAD_ID_KEY) != null && message.getData().get(RESCIND_KEY) != null) { diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h @@ -116,15 +116,15 @@ virtual jsi::Value isNotificationsSessionInitializedWithDevices( jsi::Runtime &rt, jsi::Array deviceIDs) override; - virtual jsi::Value updateKeyserverDataInNotifStorage( + virtual jsi::Value updateDataInNotifStorage( jsi::Runtime &rt, - jsi::Array keyserversData) override; - virtual jsi::Value removeKeyserverDataFromNotifStorage( + jsi::Array data) override; + virtual jsi::Value removeDataFromNotifStorage( jsi::Runtime &rt, - jsi::Array keyserverIDsToDelete) override; - virtual jsi::Value getKeyserverDataFromNotifStorage( + jsi::Array idsToDelete) override; + virtual jsi::Value getDataFromNotifStorage( jsi::Runtime &rt, - jsi::Array keyserverIDs) override; + jsi::Array ids) override; virtual jsi::Value updateUnreadThickThreadsInNotifsStorage( jsi::Runtime &rt, jsi::Array unreadThickThreadIDs) override; diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp --- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp +++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp @@ -1328,26 +1328,26 @@ }); } -jsi::Value CommCoreModule::updateKeyserverDataInNotifStorage( +jsi::Value CommCoreModule::updateDataInNotifStorage( jsi::Runtime &rt, - jsi::Array keyserversData) { - - std::vector> keyserversDataCpp; - for (auto idx = 0; idx < keyserversData.size(rt); idx++) { - auto data = keyserversData.getValueAtIndex(rt, idx).asObject(rt); - std::string keyserverID = data.getProperty(rt, "id").asString(rt).utf8(rt); - std::string keyserverUnreadCountKey = - "KEYSERVER." + keyserverID + ".UNREAD_COUNT"; - int unreadCount = data.getProperty(rt, "unreadCount").asNumber(); - keyserversDataCpp.push_back({keyserverUnreadCountKey, unreadCount}); + jsi::Array data) { + + std::vector> dataVectorCpp; + for (auto idx = 0; idx < data.size(rt); idx++) { + auto dataItem = data.getValueAtIndex(rt, idx).asObject(rt); + std::string id = dataItem.getProperty(rt, "id").asString(rt).utf8(rt); + std::string storageKey = "KEYSERVER." + id + ".UNREAD_COUNT"; + + int unreadCount = dataItem.getProperty(rt, "unreadCount").asNumber(); + dataVectorCpp.push_back({storageKey, unreadCount}); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { - for (const auto &keyserverData : keyserversDataCpp) { - CommMMKV::setInt(keyserverData.first, keyserverData.second); + for (const auto &dataItem : dataVectorCpp) { + CommMMKV::setInt(dataItem.first, dataItem.second); } } catch (const std::exception &e) { error = e.what(); @@ -1363,23 +1363,22 @@ }); } -jsi::Value CommCoreModule::removeKeyserverDataFromNotifStorage( +jsi::Value CommCoreModule::removeDataFromNotifStorage( jsi::Runtime &rt, - jsi::Array keyserverIDsToDelete) { - std::vector keyserverIDsToDeleteCpp{}; - for (auto idx = 0; idx < keyserverIDsToDelete.size(rt); idx++) { - std::string keyserverID = - keyserverIDsToDelete.getValueAtIndex(rt, idx).asString(rt).utf8(rt); - std::string keyserverUnreadCountKey = - "KEYSERVER." + keyserverID + ".UNREAD_COUNT"; - keyserverIDsToDeleteCpp.push_back(keyserverUnreadCountKey); + jsi::Array idsToDelete) { + std::vector keysToDeleteCpp{}; + for (auto idx = 0; idx < idsToDelete.size(rt); idx++) { + std::string id = + idsToDelete.getValueAtIndex(rt, idx).asString(rt).utf8(rt); + std::string storageKey = "KEYSERVER." + id + ".UNREAD_COUNT"; + keysToDeleteCpp.push_back(storageKey); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; try { - CommMMKV::removeKeys(keyserverIDsToDeleteCpp); + CommMMKV::removeKeys(keysToDeleteCpp); } catch (const std::exception &e) { error = e.what(); } @@ -1394,65 +1393,64 @@ }); } -jsi::Value CommCoreModule::getKeyserverDataFromNotifStorage( +jsi::Value CommCoreModule::getDataFromNotifStorage( jsi::Runtime &rt, - jsi::Array keyserverIDs) { - std::vector keyserverIDsCpp{}; - for (auto idx = 0; idx < keyserverIDs.size(rt); idx++) { - std::string keyserverID = - keyserverIDs.getValueAtIndex(rt, idx).asString(rt).utf8(rt); - keyserverIDsCpp.push_back(keyserverID); + jsi::Array ids) { + std::vector idsCpp{}; + for (auto idx = 0; idx < ids.size(rt); idx++) { + std::string id = + ids.getValueAtIndex(rt, idx).asString(rt).utf8(rt); + idsCpp.push_back(id); } return createPromiseAsJSIValue( rt, [=](jsi::Runtime &innerRt, std::shared_ptr promise) { std::string error; - std::vector> keyserversDataVector{}; + std::vector> dataVector{}; try { - for (const auto &keyserverID : keyserverIDsCpp) { - std::string keyserverUnreadCountKey = - "KEYSERVER." + keyserverID + ".UNREAD_COUNT"; + for (const auto &id : idsCpp) { + std::string storageKey = "KEYSERVER." + id + ".UNREAD_COUNT"; std::optional unreadCount = - CommMMKV::getInt(keyserverUnreadCountKey, -1); + CommMMKV::getInt(storageKey, -1); if (!unreadCount.has_value()) { continue; } - keyserversDataVector.push_back({keyserverID, unreadCount.value()}); + dataVector.push_back({id, unreadCount.value()}); } } catch (const std::exception &e) { error = e.what(); } - auto keyserversDataVectorPtr = + auto dataVectorPtr = std::make_shared>>( - std::move(keyserversDataVector)); + std::move(dataVector)); this->jsInvoker_->invokeAsync( - [&innerRt, keyserversDataVectorPtr, error, promise]() { + [&innerRt, dataVectorPtr, error, promise]() { if (error.size()) { promise->reject(error); return; } - size_t numKeyserversData = keyserversDataVectorPtr->size(); - jsi::Array jsiKeyserversData = - jsi::Array(innerRt, numKeyserversData); + size_t numData = dataVectorPtr->size(); + jsi::Array jsiData = + jsi::Array(innerRt, numData); size_t writeIdx = 0; - for (const auto &keyserverData : *keyserversDataVectorPtr) { - jsi::Object jsiKeyserverData = jsi::Object(innerRt); - jsiKeyserverData.setProperty( - innerRt, "id", keyserverData.first); - jsiKeyserverData.setProperty( - innerRt, "unreadCount", keyserverData.second); - jsiKeyserversData.setValueAtIndex( - innerRt, writeIdx++, jsiKeyserverData); + for (const auto &dataItem : *dataVectorPtr) { + jsi::Object jsiDataItem = jsi::Object(innerRt); + jsiDataItem.setProperty( + innerRt, "id", dataItem.first); + jsiDataItem.setProperty( + innerRt, "unreadCount", dataItem.second); + jsiData.setValueAtIndex( + innerRt, writeIdx++, jsiDataItem); } - promise->resolve(std::move(jsiKeyserversData)); + promise->resolve(std::move(jsiData)); }); }); } diff --git a/native/cpp/CommonCpp/_generated/commJSI-generated.cpp b/native/cpp/CommonCpp/_generated/commJSI-generated.cpp --- a/native/cpp/CommonCpp/_generated/commJSI-generated.cpp +++ b/native/cpp/CommonCpp/_generated/commJSI-generated.cpp @@ -66,14 +66,14 @@ static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isNotificationsSessionInitializedWithDevices(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { return static_cast(&turboModule)->isNotificationsSessionInitializedWithDevices(rt, args[0].asObject(rt).asArray(rt)); } -static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateKeyserverDataInNotifStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { - return static_cast(&turboModule)->updateKeyserverDataInNotifStorage(rt, args[0].asObject(rt).asArray(rt)); +static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateDataInNotifStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->updateDataInNotifStorage(rt, args[0].asObject(rt).asArray(rt)); } -static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeKeyserverDataFromNotifStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { - return static_cast(&turboModule)->removeKeyserverDataFromNotifStorage(rt, args[0].asObject(rt).asArray(rt)); +static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeDataFromNotifStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->removeDataFromNotifStorage(rt, args[0].asObject(rt).asArray(rt)); } -static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getKeyserverDataFromNotifStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { - return static_cast(&turboModule)->getKeyserverDataFromNotifStorage(rt, args[0].asObject(rt).asArray(rt)); +static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getDataFromNotifStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { + return static_cast(&turboModule)->getDataFromNotifStorage(rt, args[0].asObject(rt).asArray(rt)); } static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateUnreadThickThreadsInNotifsStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) { return static_cast(&turboModule)->updateUnreadThickThreadsInNotifsStorage(rt, args[0].asObject(rt).asArray(rt)); @@ -270,9 +270,9 @@ methodMap_["isNotificationsSessionInitialized"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isNotificationsSessionInitialized}; methodMap_["isDeviceNotificationsSessionInitialized"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isDeviceNotificationsSessionInitialized}; methodMap_["isNotificationsSessionInitializedWithDevices"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isNotificationsSessionInitializedWithDevices}; - methodMap_["updateKeyserverDataInNotifStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateKeyserverDataInNotifStorage}; - methodMap_["removeKeyserverDataFromNotifStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeKeyserverDataFromNotifStorage}; - methodMap_["getKeyserverDataFromNotifStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getKeyserverDataFromNotifStorage}; + methodMap_["updateDataInNotifStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateDataInNotifStorage}; + methodMap_["removeDataFromNotifStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeDataFromNotifStorage}; + methodMap_["getDataFromNotifStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getDataFromNotifStorage}; methodMap_["updateUnreadThickThreadsInNotifsStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateUnreadThickThreadsInNotifsStorage}; methodMap_["getUnreadThickThreadIDsFromNotifsStorage"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getUnreadThickThreadIDsFromNotifsStorage}; methodMap_["initializeContentOutboundSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentOutboundSession}; diff --git a/native/cpp/CommonCpp/_generated/commJSI.h b/native/cpp/CommonCpp/_generated/commJSI.h --- a/native/cpp/CommonCpp/_generated/commJSI.h +++ b/native/cpp/CommonCpp/_generated/commJSI.h @@ -37,9 +37,9 @@ virtual jsi::Value isNotificationsSessionInitialized(jsi::Runtime &rt) = 0; virtual jsi::Value isDeviceNotificationsSessionInitialized(jsi::Runtime &rt, jsi::String deviceID) = 0; virtual jsi::Value isNotificationsSessionInitializedWithDevices(jsi::Runtime &rt, jsi::Array deviceIDs) = 0; - virtual jsi::Value updateKeyserverDataInNotifStorage(jsi::Runtime &rt, jsi::Array keyserversData) = 0; - virtual jsi::Value removeKeyserverDataFromNotifStorage(jsi::Runtime &rt, jsi::Array keyserverIDsToDelete) = 0; - virtual jsi::Value getKeyserverDataFromNotifStorage(jsi::Runtime &rt, jsi::Array keyserverIDs) = 0; + virtual jsi::Value updateDataInNotifStorage(jsi::Runtime &rt, jsi::Array data) = 0; + virtual jsi::Value removeDataFromNotifStorage(jsi::Runtime &rt, jsi::Array idsToDelete) = 0; + virtual jsi::Value getDataFromNotifStorage(jsi::Runtime &rt, jsi::Array ids) = 0; virtual jsi::Value updateUnreadThickThreadsInNotifsStorage(jsi::Runtime &rt, jsi::Array unreadThickThreadIDs) = 0; virtual jsi::Value getUnreadThickThreadIDsFromNotifsStorage(jsi::Runtime &rt) = 0; virtual jsi::Value initializeContentOutboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional oneTimeKey, jsi::String deviceID) = 0; @@ -254,29 +254,29 @@ return bridging::callFromJs( rt, &T::isNotificationsSessionInitializedWithDevices, jsInvoker_, instance_, std::move(deviceIDs)); } - jsi::Value updateKeyserverDataInNotifStorage(jsi::Runtime &rt, jsi::Array keyserversData) override { + jsi::Value updateDataInNotifStorage(jsi::Runtime &rt, jsi::Array data) override { static_assert( - bridging::getParameterCount(&T::updateKeyserverDataInNotifStorage) == 2, - "Expected updateKeyserverDataInNotifStorage(...) to have 2 parameters"); + bridging::getParameterCount(&T::updateDataInNotifStorage) == 2, + "Expected updateDataInNotifStorage(...) to have 2 parameters"); return bridging::callFromJs( - rt, &T::updateKeyserverDataInNotifStorage, jsInvoker_, instance_, std::move(keyserversData)); + rt, &T::updateDataInNotifStorage, jsInvoker_, instance_, std::move(data)); } - jsi::Value removeKeyserverDataFromNotifStorage(jsi::Runtime &rt, jsi::Array keyserverIDsToDelete) override { + jsi::Value removeDataFromNotifStorage(jsi::Runtime &rt, jsi::Array idsToDelete) override { static_assert( - bridging::getParameterCount(&T::removeKeyserverDataFromNotifStorage) == 2, - "Expected removeKeyserverDataFromNotifStorage(...) to have 2 parameters"); + bridging::getParameterCount(&T::removeDataFromNotifStorage) == 2, + "Expected removeDataFromNotifStorage(...) to have 2 parameters"); return bridging::callFromJs( - rt, &T::removeKeyserverDataFromNotifStorage, jsInvoker_, instance_, std::move(keyserverIDsToDelete)); + rt, &T::removeDataFromNotifStorage, jsInvoker_, instance_, std::move(idsToDelete)); } - jsi::Value getKeyserverDataFromNotifStorage(jsi::Runtime &rt, jsi::Array keyserverIDs) override { + jsi::Value getDataFromNotifStorage(jsi::Runtime &rt, jsi::Array ids) override { static_assert( - bridging::getParameterCount(&T::getKeyserverDataFromNotifStorage) == 2, - "Expected getKeyserverDataFromNotifStorage(...) to have 2 parameters"); + bridging::getParameterCount(&T::getDataFromNotifStorage) == 2, + "Expected getDataFromNotifStorage(...) to have 2 parameters"); return bridging::callFromJs( - rt, &T::getKeyserverDataFromNotifStorage, jsInvoker_, instance_, std::move(keyserverIDs)); + rt, &T::getDataFromNotifStorage, jsInvoker_, instance_, std::move(ids)); } jsi::Value updateUnreadThickThreadsInNotifsStorage(jsi::Runtime &rt, jsi::Array unreadThickThreadIDs) override { static_assert( diff --git a/native/database/sqlite-api.js b/native/database/sqlite-api.js --- a/native/database/sqlite-api.js +++ b/native/database/sqlite-api.js @@ -59,7 +59,7 @@ const promises = []; if (keyserversToRemoveFromNotifsStore.length > 0) { promises.push( - commCoreModule.removeKeyserverDataFromNotifStorage( + commCoreModule.removeDataFromNotifStorage( keyserversToRemoveFromNotifsStore, ), ); diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -24,12 +24,14 @@ NSString *const encryptionKeyLabel = @"encryptionKey"; NSString *const needsSilentBadgeUpdateKey = @"needsSilentBadgeUpdate"; NSString *const notificationIdKey = @"notificationId"; +NSString *const farcasterBadgeKey = @"farcasterBadge"; // Those and future MMKV-related constants should match // similar constants in CommNotificationsHandler.java const std::string mmkvKeySeparator = "."; const std::string mmkvKeyserverPrefix = "KEYSERVER"; const std::string mmkvUnreadCountSuffix = "UNREAD_COUNT"; +const std::string mmkvFarcasterKey = "FARCASTER"; // The context for this constant can be found here: // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28 @@ -529,6 +531,16 @@ senderKeyserverUnreadCountKey, senderKeyserverUnreadCount); } + if (content.userInfo[farcasterBadgeKey] && + [content.userInfo[farcasterBadgeKey] isEqualToString:@"1"] && + content.userInfo[@"badge"]) { + int farcasterBadgeCount = [content.userInfo[@"badge"] intValue]; + std::string farcasterUnreadCountKey = joinStrings( + mmkvKeySeparator, + {mmkvKeyserverPrefix, mmkvFarcasterKey, mmkvUnreadCountSuffix}); + comm::CommMMKV::setInt(farcasterUnreadCountKey, farcasterBadgeCount); + } + if (content.userInfo[senderDeviceIDKey] && content.userInfo[threadIDKey] && [self isRescind:content.userInfo]) { comm::CommMMKV::removeElementFromStringSet( @@ -541,7 +553,7 @@ std::string([content.userInfo[threadIDKey] UTF8String])); } - // calculate unread counts from keyservers + // calculate unread counts from keyservers and Farcaster int totalUnreadCount = 0; std::vector allKeys = comm::CommMMKV::getAllKeys(); for (const auto &key : allKeys) { diff --git a/native/push/push-handler.react.js b/native/push/push-handler.react.js --- a/native/push/push-handler.react.js +++ b/native/push/push-handler.react.js @@ -362,9 +362,9 @@ let unreadThickThreadIDs: $ReadOnlyArray; try { [queriedKeyserverData, unreadThickThreadIDs] = await Promise.all([ - commCoreModule.getKeyserverDataFromNotifStorage(notifsStorageQueries), + commCoreModule.getDataFromNotifStorage(notifsStorageQueries), handleUnreadThickThreadIDsInNotifsStorage, - commCoreModule.updateKeyserverDataInNotifStorage(notifStorageUpdates), + commCoreModule.updateDataInNotifStorage(notifStorageUpdates), ]); } catch (e) { if (__DEV__) { @@ -386,6 +386,21 @@ } totalUnreadCount += unreadThickThreadIDs.length; + + let farcasterUnreadCount = 0; + try { + const farcasterData = await commCoreModule.getDataFromNotifStorage([ + 'FARCASTER', + ]); + if (farcasterData.length > 0) { + farcasterUnreadCount = farcasterData[0].unreadCount; + } + } catch (e) { + console.error('Failed to get Farcaster unread count:', e); + } + + totalUnreadCount += farcasterUnreadCount; + if (Platform.OS === 'ios') { CommIOSNotifications.setBadgesCount(totalUnreadCount); } else if (Platform.OS === 'android') { @@ -398,9 +413,7 @@ this.props.thinThreadsUnreadCount, ); try { - await commCoreModule.removeKeyserverDataFromNotifStorage( - keyserversDataToRemove, - ); + await commCoreModule.removeDataFromNotifStorage(keyserversDataToRemove); } catch (e) { if (__DEV__) { Alert.alert( diff --git a/native/schema/CommCoreModuleSchema.js b/native/schema/CommCoreModuleSchema.js --- a/native/schema/CommCoreModuleSchema.js +++ b/native/schema/CommCoreModuleSchema.js @@ -79,14 +79,14 @@ +isNotificationsSessionInitializedWithDevices: ( deviceIDs: $ReadOnlyArray, ) => Promise<{ +[deviceID: string]: boolean }>; - +updateKeyserverDataInNotifStorage: ( - keyserversData: $ReadOnlyArray<{ +id: string, +unreadCount: number }>, + +updateDataInNotifStorage: ( + data: $ReadOnlyArray<{ +id: string, +unreadCount: number }>, ) => Promise; - +removeKeyserverDataFromNotifStorage: ( - keyserverIDsToDelete: $ReadOnlyArray, + +removeDataFromNotifStorage: ( + idsToDelete: $ReadOnlyArray, ) => Promise; - +getKeyserverDataFromNotifStorage: ( - keyserverIDs: $ReadOnlyArray, + +getDataFromNotifStorage: ( + ids: $ReadOnlyArray, ) => Promise<$ReadOnlyArray<{ +id: string, +unreadCount: number }>>; +updateUnreadThickThreadsInNotifsStorage: ( unreadThickThreadIDs: $ReadOnlyArray, diff --git a/services/tunnelbroker/src/notifs/generic_client.rs b/services/tunnelbroker/src/notifs/generic_client.rs --- a/services/tunnelbroker/src/notifs/generic_client.rs +++ b/services/tunnelbroker/src/notifs/generic_client.rs @@ -66,9 +66,18 @@ #[derive(Clone, Debug, Serialize)] pub struct GenericNotifPayload { - pub title: String, - pub body: String, - pub thread_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub body: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub thread_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub badge: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub badge_only: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub farcaster_badge: Option, } enum APNsTopic { @@ -135,36 +144,66 @@ apns_collapse_id: None, }; - let payload = json!({ + let mut payload_obj = json!({ "aps": { - "alert": { - "title": self.title, - "body": self.body, - }, - "thread-id": self.thread_id, - "sound": "default", "mutable-content": 1 }, }); + // Only add alert if we have title or body + if self.title.is_some() || self.body.is_some() { + payload_obj["aps"]["alert"] = json!({ + "title": self.title.unwrap_or_default(), + "body": self.body.unwrap_or_default(), + }); + payload_obj["aps"]["sound"] = json!("default"); + } + + if let Some(thread_id) = self.thread_id { + payload_obj["aps"]["thread-id"] = json!(thread_id); + } + if let Some(badge) = self.badge { + payload_obj["badge"] = json!(badge); + } + if let Some(badge_only) = self.badge_only { + payload_obj["badgeOnly"] = json!(if badge_only { "1" } else { "0" }); + } + if let Some(farcaster_badge) = self.farcaster_badge { + payload_obj["farcasterBadge"] = + json!(if farcaster_badge { "1" } else { "0" }); + } + APNsNotif { device_token: device_token.to_string(), headers, - payload: serde_json::to_string(&payload).unwrap(), + payload: serde_json::to_string(&payload_obj).unwrap(), } } fn into_fcm(self, device_token: &str) -> FCMMessage { use super::fcm::firebase_message::{AndroidConfig, AndroidMessagePriority}; - let data = json!({ + let mut data = json!({ "id": uuid::Uuid::new_v4().to_string(), - "title": self.title, - "body": self.body, - "threadID": self.thread_id, - "badgeOnly": "0", + "badgeOnly": if self.badge_only.unwrap_or(false) { "1" } else { "0" }, }); + if let Some(title) = self.title { + data["title"] = json!(title); + } + if let Some(body) = self.body { + data["body"] = json!(body); + } + if let Some(thread_id) = self.thread_id { + data["threadID"] = json!(thread_id); + } + if let Some(badge) = self.badge { + data["badge"] = json!(badge); + } + if let Some(farcaster_badge) = self.farcaster_badge { + data["farcasterBadge"] = json!(if farcaster_badge { "1" } else { "0" }); + } + FCMMessage { data, token: device_token.to_string(), @@ -177,13 +216,30 @@ fn into_web_push(self, device_token: &str) -> WebPushNotif { use crate::notifs::web_push::WebPushNotif; - let payload = json!({ + let mut payload = json!({ "id": uuid::Uuid::new_v4().to_string(), - "title": self.title, - "body": self.body, - "threadID": self.thread_id, }); + if let Some(title) = self.title { + payload["title"] = json!(title); + } + if let Some(body) = self.body { + payload["body"] = json!(body); + } + if let Some(thread_id) = self.thread_id { + payload["threadID"] = json!(thread_id); + } + if let Some(badge) = self.badge { + payload["badge"] = json!(badge); + } + if let Some(badge_only) = self.badge_only { + payload["badgeOnly"] = json!(if badge_only { "1" } else { "0" }); + } + if let Some(farcaster_badge) = self.farcaster_badge { + payload["farcasterBadge"] = + json!(if farcaster_badge { "1" } else { "0" }); + } + WebPushNotif { device_token: device_token.to_string(), payload: serde_json::to_string(&payload).unwrap(), @@ -191,11 +247,27 @@ } fn into_wns(self, device_token: &str) -> WNSNotif { - let payload = json!({ - "title": self.title, - "body": self.body, - "threadID": self.thread_id, - }); + let mut payload = json!({}); + + if let Some(title) = self.title { + payload["title"] = json!(title); + } + if let Some(body) = self.body { + payload["body"] = json!(body); + } + if let Some(thread_id) = self.thread_id { + payload["threadID"] = json!(thread_id); + } + if let Some(badge) = self.badge { + payload["badge"] = json!(badge); + } + if let Some(badge_only) = self.badge_only { + payload["badgeOnly"] = json!(if badge_only { "1" } else { "0" }); + } + if let Some(farcaster_badge) = self.farcaster_badge { + payload["farcasterBadge"] = + json!(if farcaster_badge { "1" } else { "0" }); + } WNSNotif { device_token: device_token.to_string(), diff --git a/services/tunnelbroker/src/token_distributor/notif_utils.rs b/services/tunnelbroker/src/token_distributor/notif_utils.rs --- a/services/tunnelbroker/src/token_distributor/notif_utils.rs +++ b/services/tunnelbroker/src/token_distributor/notif_utils.rs @@ -58,9 +58,12 @@ }; Some(GenericNotifPayload { - title: trim_text(title, 100), - body: trim_text(&body, 300), - thread_id: format!("FARCASTER#{}", conversation_id), + title: Some(trim_text(title, 100)), + body: Some(trim_text(&body, 300)), + thread_id: Some(format!("FARCASTER#{}", conversation_id)), + badge: None, + badge_only: None, + farcaster_badge: None, }) } diff --git a/services/tunnelbroker/src/token_distributor/token_connection.rs b/services/tunnelbroker/src/token_distributor/token_connection.rs --- a/services/tunnelbroker/src/token_distributor/token_connection.rs +++ b/services/tunnelbroker/src/token_distributor/token_connection.rs @@ -6,7 +6,9 @@ use crate::database::DatabaseClient; use crate::farcaster::FarcasterClient; use crate::log::redact_sensitive_data; -use crate::notifs::{GenericNotifClient, NotifRecipientDescriptor}; +use crate::notifs::{ + GenericNotifClient, GenericNotifPayload, NotifRecipientDescriptor, +}; use crate::token_distributor::config::TokenDistributorConfig; use crate::token_distributor::error::TokenConnectionError; use crate::token_distributor::notif_utils::prepare_notif_payload; @@ -341,7 +343,7 @@ debug!("Processing refresh-self-direct-casts-inbox message"); } FarcasterPayload::Unseen { .. } => { - if let Err(e) = self.handle_unseen_message(&mut client).await { + if let Err(e) = self.handle_unseen_message(&farcaster_msg, &mut client).await { info!( metricType = "TokenDistributor_ConnectionFailure", metricValue = 1, @@ -667,8 +669,24 @@ async fn handle_unseen_message( &self, + farcaster_msg: &FarcasterMessage, client: &mut ChainedInterceptedServicesAuthClient, ) -> Result<(), TokenConnectionError> { + let inbox_count = if let Some(data_str) = &farcaster_msg.data { + match serde_json::from_str::(data_str) { + Ok(data_json) => data_json + .get("inboxCount") + .and_then(|v| v.as_u64()) + .map(|v| v as i32), + Err(e) => { + warn!("Failed to parse unseen message data: {}", e); + None + } + } + } else { + None + }; + let conversations = self .farcaster_client .fetch_inbox(&self.token_info.user_id) @@ -707,6 +725,38 @@ let recipient_devices = self.get_self_user_device_list(client).await?; + if let Some(count) = inbox_count { + for (device_id, platform_details) in &recipient_devices { + let Ok(platform) = platform_details.device_type().try_into() else { + continue; + }; + + let target = NotifRecipientDescriptor { + platform, + device_id: device_id.to_string(), + }; + + // Send badge-only notification using GenericNotifPayload + let badge_payload = GenericNotifPayload { + title: None, + body: None, + thread_id: None, + badge: Some(count.to_string()), + badge_only: Some(true), + farcaster_badge: Some(true), + }; + + if let Err(err) = + self.notif_client.send_notif(badge_payload, target).await + { + if !err.is_invalid_token() { + tracing::error!("Failed to send Farcaster badge notif: {:?}", err); + } + } + } + } + + // Send inbox status messages to all devices for (device_id, _) in &recipient_devices { self .message_sender diff --git a/web/push-notif/badge-handler.react.js b/web/push-notif/badge-handler.react.js --- a/web/push-notif/badge-handler.react.js +++ b/web/push-notif/badge-handler.react.js @@ -5,6 +5,7 @@ import { allConnectionInfosSelector } from 'lib/selectors/keyserver-selectors.js'; import { thinThreadsUnreadCountSelector, + unreadFarcasterThreadIDsSelector, unreadThickThreadIDsSelector, } from 'lib/selectors/thread-selectors.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; @@ -25,13 +26,16 @@ const { socketState: tunnelbrokerSocketState } = useTunnelbroker(); const currentUnreadThickThreadIDs = useSelector(unreadThickThreadIDsSelector); + const unreadFarcasterThreadIDs = useSelector( + unreadFarcasterThreadIDsSelector, + ); React.useEffect(() => { void (async () => { const unreadCountUpdates: { [keyserverID: string]: number, } = {}; - const unreadCountQueries: Array = []; + const unreadCountQueries: Array = ['FARCASTER']; for (const keyserverID in thinThreadsUnreadCount) { if (connection[keyserverID]?.status !== 'connected') { @@ -40,6 +44,7 @@ } unreadCountUpdates[keyserverID] = thinThreadsUnreadCount[keyserverID]; } + unreadCountUpdates['FARCASTER'] = unreadFarcasterThreadIDs.length; let queriedUnreadCounts: { +[keyserverID: string]: ?number } = {}; let unreadThickThreadIDs: $ReadOnlyArray = []; @@ -76,6 +81,7 @@ } totalUnreadCount += unreadThickThreadIDs.length; + document.title = getTitle(totalUnreadCount); electron?.setBadge(totalUnreadCount === 0 ? null : totalUnreadCount); })(); @@ -84,6 +90,7 @@ currentUnreadThickThreadIDs, thinThreadsUnreadCount, connection, + unreadFarcasterThreadIDs.length, ]); } diff --git a/web/push-notif/service-worker.js b/web/push-notif/service-worker.js --- a/web/push-notif/service-worker.js +++ b/web/push-notif/service-worker.js @@ -14,6 +14,7 @@ WEB_NOTIFS_SERVICE_UTILS_KEY, type WebNotifsServiceUtilsData, type WebNotifDecryptionError, + updateNotifsUnreadCountStorage, } from './notif-crypto-utils.js'; import { persistAuthMetadata } from './services-client.js'; import { localforageConfig } from '../shared-worker/utils/constants.js'; @@ -100,6 +101,14 @@ event.waitUntil( (async () => { + if (data.farcasterBadge === '1' && data.badgeOnly === '1') { + const farcasterBadgeCount = parseInt(data.badge, 10) || 0; + await updateNotifsUnreadCountStorage({ + FARCASTER: farcasterBadgeCount, + }); + return; + } + let plainTextData: PlainTextWebNotification; let decryptionResult: PlainTextWebNotification | WebNotifDecryptionError;