diff --git a/native/android/app/CMakeLists.txt b/native/android/app/CMakeLists.txt
index d388abbfa..d5673b409 100644
--- a/native/android/app/CMakeLists.txt
+++ b/native/android/app/CMakeLists.txt
@@ -1,248 +1,249 @@
 # For more information about using CMake with Android Studio, read the
 # documentation: https://d.android.com/studio/projects/add-native-code.html
 project(comm CXX C)
 
 set(CMAKE_CXX_STANDARD 17)
 
 # C0103 is a naming convention, but the variable names which need to be set
 # are determined by the upstream project
 # cmake-lint: disable=C0103
 # Disable line length as some paths are hard to reduce without becoming cryptic
 # cmake-lint: disable=C0301
 
 # Sets the minimum version of CMake required to build the native library.
 cmake_minimum_required(VERSION 3.18)
 
 # Creates and names a library, sets it as either STATIC
 # or SHARED, and provides the relative paths to its source code.
 # You can define multiple libraries, and CMake builds them for you.
 # Gradle automatically packages shared libraries with your APK.
 set(PACKAGE_NAME "comm_jni_module")
 
 find_library(log-lib log)
 find_package(fbjni REQUIRED CONFIG)
 
 set(BUILD_TESTING OFF)
 set(HAVE_SYMBOLIZE OFF)
 set(WITH_GTEST OFF CACHE BOOL "Use googletest" FORCE)
 set(WITH_GFLAGS OFF CACHE BOOL "Use gflags" FORCE)
 
 # General
 set(_third_party_dir ${CMAKE_CURRENT_SOURCE_DIR}/build/third-party-ndk)
 set(_android_build_dir build/${CMAKE_ANDROID_ARCH_ABI})
 
 include(FetchContent)
 
 if(CMAKE_ANDROID_ARCH_ABI STREQUAL arm64-v8a)
   set(Rust_CARGO_TARGET aarch64-linux-android)
 elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL x86_64)
   set(Rust_CARGO_TARGET x86_64-linux-android)
 elseif(CMAKE_ANDROID_ARCH_ABI STREQUAL armeabi-v7a)
   set(Rust_CARGO_TARGET armv7-linux-androideabi)
 endif()
 
 string(TOLOWER ${CMAKE_HOST_SYSTEM_NAME} CMAKE_HOST_SYSTEM_NAME_LOWER)
 set(_toolchain_path
   "$ENV{ANDROID_HOME}/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/${CMAKE_HOST_SYSTEM_NAME_LOWER}-x86_64/bin"
 )
 if(EXISTS "${_toolchain_path}/${Rust_CARGO_TARGET}-ar")
   set(AR "${_toolchain_path}/${Rust_CARGO_TARGET}-ar")
 else()
   set(AR "${_toolchain_path}/llvm-ar")
 endif()
 
 FetchContent_Declare(
   Corrosion
   GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
   GIT_TAG v0.2.1
 )
 
 FetchContent_MakeAvailable(Corrosion)
 
 include(../../../shared/cmake/corrosion-cxx.cmake)
 
 add_library_rust(PATH ../../native_rust_library NAMESPACE comm)
 
 # We're updating parameters below for Cmake's find_OpenSSL() function
 set(OPENSSL_ROOT_DIR
   "${_third_party_dir}/openssl/openssl-${OPENSSL_VERSION}/${_android_build_dir}"
 )
 list(APPEND CMAKE_FIND_ROOT_PATH "${OPENSSL_ROOT_DIR}")
 
 # Override HAVE_EXECINFO_H in glog's CMakeLists.txt
 if(
   CMAKE_ANDROID_ARCH_ABI STREQUAL arm64-v8a OR
   CMAKE_ANDROID_ARCH_ABI STREQUAL armeabi-v7a
 )
   set(HAVE_EXECINFO_H OFF CACHE BOOL "Whether platform has execinfo.h")
 endif()
 
 add_subdirectory(${_third_party_dir}/glog/glog-${GLOG_VERSION}/)
 
 file(GLOB LIBRN_DIR "${REACT_NATIVE_SO_DIR}/${ANDROID_ABI}")
 if (NOT LIBRN_DIR)
   # If /${ANDROID_ABI} dir not found, then ${REACT_NATIVE_SO_DIR} is probably:
   # ReactAndroid/build/react-ndk/exported
   file(GLOB LIBRN_DIR "${REACT_NATIVE_SO_DIR}")
 endif ()
 
 include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/folly-target.cmake)
 include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/openssl-target.cmake)
 
 add_subdirectory(../../node_modules/olm ./build)
 
 set(_node_modules_dir ${CMAKE_CURRENT_SOURCE_DIR}/../../node_modules)
 set(_react_native_dir ${_node_modules_dir}/react-native)
 
 add_subdirectory(../../cpp/CommonCpp/
   ${CMAKE_CURRENT_BINARY_DIR}/build/CommonCpp
   EXCLUDE_FROM_ALL
 )
 
 file(GLOB SQLCIPHER
   "${_node_modules_dir}/@commapp/sqlcipher-amalgamation/src/*.c"
 )
 
 # Add files which aren't a part of comm-tools
 list(APPEND ANDROID_NATIVE_CODE
   "./src/cpp/CommSecureStore.cpp"
   "./src/cpp/DatabaseInitializerJNIHelper.cpp"
   "./src/cpp/GlobalDBSingleton.cpp"
   "./src/cpp/Logger.cpp"
   "./src/cpp/MessageOperationsUtilitiesJNIHelper.cpp"
   "./src/cpp/PlatformSpecificTools.cpp"
   "./src/cpp/TerminateApp.cpp"
   "./src/cpp/ThreadOperationsJNIHelper.cpp"
   "./src/cpp/jsiInstaller.cpp"
   "./src/cpp/NotificationsCryptoModuleJNIHelper.cpp"
   "./src/cpp/StaffUtilsJNIHelper.cpp"
   "./src/cpp/AESCrypto.cpp"
   "./src/cpp/CommServicesAuthMetadataEmitter.cpp"
   "./src/cpp/CommMMKV.cpp"
+  "./src/cpp/CommMMKVJNIHelper.cpp"
   "./src/cpp/NotificationsInboundKeysProvider.cpp"
 )
 
 list(APPEND GENERATED_NATIVE_CODE
   "../../cpp/CommonCpp/_generated/commJSI-generated.cpp"
   "../../cpp/CommonCpp/_generated/utilsJSI-generated.cpp"
   "../../cpp/CommonCpp/_generated/rustJSI-generated.cpp"
 )
 list(APPEND RUST_NATIVE_CODE
   "../../native_rust_library/RustCallback.cpp"
   "../../native_rust_library/RustAESCrypto.cpp"
   "../../native_rust_library/RustCSAMetadataEmitter.cpp"
   "../../native_rust_library/RustSecureStore.cpp"
   "../../native_rust_library/RustBackupExecutor.cpp"
 )
 file(GLOB CRYPTO_NATIVE_CODE "../../cpp/CommonCpp/CryptoTools/*.cpp")
 file(GLOB DB_NATIVE_CODE "../../cpp/CommonCpp/DatabaseManagers/*.cpp")
 file(GLOB DB_ENTITIES_NATIVE_CODE "../../cpp/CommonCpp/DatabaseManagers/entities/*.cpp")
 file(GLOB_RECURSE MODULE_NATIVE_CODE "../../cpp/CommonCpp/NativeModules/**/*.cpp")
 file(GLOB MODULE_ROOT_NATIVE_CODE "../../cpp/CommonCpp/NativeModules/*.cpp")
 file(GLOB NOTIFICATIONS_NATIVE_CODE "../../cpp/CommonCpp/Notifications/**/*.cpp")
 
 add_library(
   # Sets the name of the library
   ${PACKAGE_NAME}
 
   # Sets the library as a shared library
   SHARED
 
   # React dependencies
   ${_react_native_dir}/ReactCommon/jsi/jsi/jsi.cpp
   ${_react_native_dir}/ReactCommon/jsi/jsi/JSIDynamic.cpp
   ${_react_native_dir}/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/ReactCommon/CallInvokerHolder.cpp
   ${_react_native_dir}/ReactCommon/react/nativemodule/core/ReactCommon/TurboModule.cpp
   ${_react_native_dir}/ReactCommon/react/bridging/LongLivedObject.cpp
   ${_react_native_dir}/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleUtils.cpp
 
   # Third party dependencies
   ${SQLCIPHER}
 
   # comm code
   ${ANDROID_NATIVE_CODE}
   ${GENERATED_NATIVE_CODE}
   ${CRYPTO_NATIVE_CODE}
   ${DB_NATIVE_CODE}
   ${DB_ENTITIES_NATIVE_CODE}
   ${MODULE_NATIVE_CODE}
   ${MODULE_ROOT_NATIVE_CODE}
   ${TOOLS_NATIVE_CODE}
   ${NOTIFICATIONS_NATIVE_CODE}
   ${RUST_NATIVE_CODE}
 )
 
 set(BUILD_DIR ${CMAKE_SOURCE_DIR}/build)
 
 target_include_directories(
   ${PACKAGE_NAME}
   PRIVATE
 
   # React Native
   ${_react_native_dir}/React
   ${_react_native_dir}/React/Base
   ${_react_native_dir}/ReactCommon
   ${_react_native_dir}/ReactCommon/jsi
   ${_react_native_dir}/ReactCommon/callinvoker
   ${_react_native_dir}/ReactAndroid/src/main/java/com/facebook/react/turbomodule/core/jni/ReactCommon
 
   # SQLCipher amalgamation
   ${_node_modules_dir}/@commapp/sqlcipher-amalgamation/src
 
   # symlinked React Native headers
   ../headers
 
   # comm android specific code
   ./src/cpp
 
   # comm native mutual code
   ../../cpp/CommonCpp/
   ../../cpp/CommonCpp/NativeModules
   ../../cpp/CommonCpp/NativeModules/InternalModules
   ../../cpp/CommonCpp/NativeModules/PersistentStorageUtilities
   ../../cpp/CommonCpp/NativeModules/PersistentStorageUtilities/BackupOperationsUtilities
   ../../cpp/CommonCpp/NativeModules/PersistentStorageUtilities/DataStores
   ../../cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities
   ../../cpp/CommonCpp/NativeModules/PersistentStorageUtilities/MessageOperationsUtilities
   ../../cpp/CommonCpp/NativeModules/PersistentStorageUtilities/MessageOperationsUtilities/MessageSpecs
   ../../cpp/CommonCpp/DatabaseManagers
   ../../cpp/CommonCpp/Notifications
   ../../cpp/CommonCpp/Notifications/BackgroundDataStorage
 
   # native rust library
   ${native_rust_library_include_dir}
 )
 
 add_definitions(
   # SQLCipher
   -DSQLITE_THREADSAFE=0
   -DSQLITE_HAS_CODEC
   -DSQLITE_TEMP_STORE=2
   -DSQLCIPHER_CRYPTO_OPENSSL
   -DSQLITE_ENABLE_SESSION
   -DSQLITE_ENABLE_PREUPDATE_HOOK
   -DSQLITE_ENABLE_FTS5
 )
 
 target_link_libraries(
   ${PACKAGE_NAME}
   fbjni::fbjni
   android
   ${log-lib}
   Folly::folly
   glog::glog
   olm
   openssl-crypto
   openssl-ssl
   comm::native_rust_library
   comm-tools
 )
 
 # add a dummy library which is required by CallInvokerHolderImpl.java
 add_library(
   turbomodulejsijni
   # Sets the library as a shared library.
   SHARED
   # Provides a relative path to your source file(s).
   ./src/cpp/dummy.cpp
 )
diff --git a/native/android/app/src/cpp/CommMMKVJNIHelper.cpp b/native/android/app/src/cpp/CommMMKVJNIHelper.cpp
new file mode 100644
index 000000000..c8bd7a314
--- /dev/null
+++ b/native/android/app/src/cpp/CommMMKVJNIHelper.cpp
@@ -0,0 +1,15 @@
+#include <Tools/CommMMKV.h>
+#include <Tools/CommMMKVJNIHelper.h>
+
+namespace comm {
+std::string CommMMKVJNIHelper::notifsStorageUnreadThickThreadsKey(
+    facebook::jni::alias_ref<CommMMKVJNIHelper> jThis) {
+  return CommMMKV::notifsStorageUnreadThickThreadsKey;
+}
+
+void CommMMKVJNIHelper::registerNatives() {
+  javaClassStatic()->registerNatives({makeNativeMethod(
+      "notifsStorageUnreadThickThreadsKey",
+      CommMMKVJNIHelper::notifsStorageUnreadThickThreadsKey)});
+}
+} // namespace comm
diff --git a/native/android/app/src/cpp/jsiInstaller.cpp b/native/android/app/src/cpp/jsiInstaller.cpp
index 14f38dee7..6aac93440 100644
--- a/native/android/app/src/cpp/jsiInstaller.cpp
+++ b/native/android/app/src/cpp/jsiInstaller.cpp
@@ -1,80 +1,82 @@
 #include <CallInvokerHolder.h>
 #include <fbjni/fbjni.h>
 #include <jsi/jsi.h>
 
 #include <InternalModules/DatabaseInitializerJNIHelper.h>
 #include <InternalModules/GlobalDBSingletonJNIHelper.h>
 #include <NativeModules/CommConstants.h>
 #include <NativeModules/CommCoreModule.h>
 #include <NativeModules/CommRustModule.h>
 #include <NativeModules/CommUtilsModule.h>
 #include <Notifications/BackgroundDataStorage/NotificationsCryptoModuleJNIHelper.h>
 #include <PersistentStorageUtilities/MessageOperationsUtilities/MessageOperationsUtilitiesJNIHelper.h>
 #include <PersistentStorageUtilities/ThreadOperationsUtilities/ThreadOperationsJNIHelper.h>
+#include <Tools/CommMMKVJNIHelper.h>
 #include <Tools/StaffUtilsJNIHelper.h>
 
 namespace jni = facebook::jni;
 namespace jsi = facebook::jsi;
 namespace react = facebook::react;
 
 class CommHybrid : public jni::HybridClass<CommHybrid> {
 public:
   static auto constexpr kJavaDescriptor = "Lapp/comm/android/fbjni/CommHybrid;";
 
   static void initHybrid(
       jni::alias_ref<jhybridobject> jThis,
       jlong jsContext,
       jni::alias_ref<react::CallInvokerHolder::javaobject>
           jsCallInvokerHolder) {
     jsi::Runtime *rt = (jsi::Runtime *)jsContext;
     auto jsCallInvoker = jsCallInvokerHolder->cthis()->getCallInvoker();
     std::shared_ptr<comm::CommCoreModule> coreNativeModule =
         std::make_shared<comm::CommCoreModule>(jsCallInvoker);
     std::shared_ptr<comm::CommUtilsModule> utilsNativeModule =
         std::make_shared<comm::CommUtilsModule>(jsCallInvoker);
     std::shared_ptr<comm::CommRustModule> rustNativeModule =
         std::make_shared<comm::CommRustModule>(jsCallInvoker);
     std::shared_ptr<comm::CommConstants> nativeConstants =
         std::make_shared<comm::CommConstants>();
 
     if (rt != nullptr) {
       rt->global().setProperty(
           *rt,
           jsi::PropNameID::forAscii(*rt, "CommCoreModule"),
           jsi::Object::createFromHostObject(*rt, coreNativeModule));
       rt->global().setProperty(
           *rt,
           jsi::PropNameID::forAscii(*rt, "CommUtilsModule"),
           jsi::Object::createFromHostObject(*rt, utilsNativeModule));
       rt->global().setProperty(
           *rt,
           jsi::PropNameID::forAscii(*rt, "CommRustModule"),
           jsi::Object::createFromHostObject(*rt, rustNativeModule));
       rt->global().setProperty(
           *rt,
           jsi::PropNameID::forAscii(*rt, "CommConstants"),
           jsi::Object::createFromHostObject(*rt, nativeConstants));
     }
   }
 
   static void registerNatives() {
     javaClassStatic()->registerNatives({
         makeNativeMethod("initHybrid", CommHybrid::initHybrid),
     });
   }
 
 private:
   friend HybridBase;
 };
 
 JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *) {
   return jni::initialize(vm, [] {
     CommHybrid::registerNatives();
     comm::ThreadOperationsJNIHelper::registerNatives();
     comm::MessageOperationsUtilitiesJNIHelper::registerNatives();
     comm::GlobalDBSingletonJNIHelper::registerNatives();
     comm::DatabaseInitializerJNIHelper::registerNatives();
     comm::NotificationsCryptoModuleJNIHelper::registerNatives();
     comm::StaffUtilsJNIHelper::registerNatives();
+    comm::CommMMKVJNIHelper::registerNatives();
   });
 }
diff --git a/native/android/app/src/main/java/app/comm/android/fbjni/CommMMKV.java b/native/android/app/src/main/java/app/comm/android/fbjni/CommMMKV.java
index b9b61dc5c..6130f0ef5 100644
--- a/native/android/app/src/main/java/app/comm/android/fbjni/CommMMKV.java
+++ b/native/android/app/src/main/java/app/comm/android/fbjni/CommMMKV.java
@@ -1,190 +1,192 @@
 package app.comm.android.fbjni;
 
 import android.util.Log;
 import app.comm.android.MainApplication;
 import app.comm.android.fbjni.CommSecureStore;
 import app.comm.android.fbjni.PlatformSpecificTools;
 import com.tencent.mmkv.MMKV;
 import java.util.Base64;
 import java.util.Set;
 
 public class CommMMKV {
   private static final int MMKV_ENCRYPTION_KEY_SIZE = 16;
   private static final int MMKV_ID_SIZE = 8;
 
   private static final String SECURE_STORE_MMKV_ENCRYPTION_KEY_ID =
       "comm.mmkvEncryptionKey";
   private static final String SECURE_STORE_MMKV_IDENTIFIER_KEY_ID =
       "comm.mmkvID";
 
   private static String mmkvEncryptionKey;
   private static String mmkvIdentifier;
 
+  public static native String notifsStorageUnreadThickThreadsKey();
+
   private static MMKV getMMKVInstance(String mmkvID, String encryptionKey) {
     MMKV mmkv = MMKV.mmkvWithID(mmkvID, MMKV.MULTI_PROCESS_MODE, encryptionKey);
     if (mmkv == null) {
       throw new RuntimeException("Failed to instantiate MMKV object.");
     }
     return mmkv;
   }
 
   private static void assignInitializationData() {
     byte[] encryptionKeyBytes = PlatformSpecificTools.generateSecureRandomBytes(
         MMKV_ENCRYPTION_KEY_SIZE);
     byte[] identifierBytes =
         PlatformSpecificTools.generateSecureRandomBytes(MMKV_ID_SIZE);
     String encryptionKey = Base64.getEncoder()
                                .encodeToString(encryptionKeyBytes)
                                .substring(0, MMKV_ENCRYPTION_KEY_SIZE);
     String identifier = Base64.getEncoder()
                             .encodeToString(identifierBytes)
                             .substring(0, MMKV_ID_SIZE);
     CommSecureStore.set(SECURE_STORE_MMKV_ENCRYPTION_KEY_ID, encryptionKey);
     CommSecureStore.set(SECURE_STORE_MMKV_IDENTIFIER_KEY_ID, identifier);
     mmkvEncryptionKey = encryptionKey;
     mmkvIdentifier = identifier;
   }
 
   public static void initialize() {
     if (mmkvEncryptionKey != null && mmkvIdentifier != null) {
       return;
     }
 
     synchronized (CommMMKV.class) {
       if (mmkvEncryptionKey != null && mmkvIdentifier != null) {
         return;
       }
 
       String encryptionKey = null, identifier = null;
       try {
         encryptionKey =
             CommSecureStore.get(SECURE_STORE_MMKV_ENCRYPTION_KEY_ID);
         identifier = CommSecureStore.get(SECURE_STORE_MMKV_IDENTIFIER_KEY_ID);
       } catch (Exception e) {
         Log.w("COMM", "Failed to get MMKV keys from CommSecureStore", e);
       }
 
       if (encryptionKey == null || identifier == null) {
         assignInitializationData();
       } else {
         mmkvEncryptionKey = encryptionKey;
         mmkvIdentifier = identifier;
       }
 
       MMKV.initialize(MainApplication.getMainApplicationContext());
       getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey);
     }
   }
 
   public static void lock() {
     initialize();
     getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey).lock();
   }
 
   public static void unlock() {
     getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey).unlock();
   }
 
   public static void clearSensitiveData() {
     initialize();
     synchronized (mmkvEncryptionKey) {
       getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey).clearAll();
       boolean storageRemoved = MMKV.removeStorage(mmkvIdentifier);
       if (!storageRemoved) {
         throw new RuntimeException("Failed to remove MMKV storage.");
       }
       assignInitializationData();
       MMKV.initialize(MainApplication.getMainApplicationContext());
       getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey);
     }
   }
 
   public static boolean setString(String key, String value) {
     initialize();
     return getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey)
         .encode(key, value);
   }
 
   public static String getString(String key) {
     initialize();
     return getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey).decodeString(key);
   }
 
   public static boolean setInt(String key, int value) {
     initialize();
     return getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey)
         .encode(key, value);
   }
 
   public static Integer getInt(String key, int noValue) {
     initialize();
     int value = getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey)
                     .decodeInt(key, noValue);
     if (value == noValue) {
       return null;
     }
     return value;
   }
 
   public static String[] getAllKeys() {
     initialize();
     return getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey).allKeys();
   }
 
   public static void removeKeys(String[] keys) {
     initialize();
     getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey)
         .removeValuesForKeys(keys);
   }
 
   public static void addElementToStringSet(String setKey, String element) {
     initialize();
     MMKV mmkv = getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey);
     mmkv.lock();
     try {
       Set<String> stringSet = mmkv.decodeStringSet(setKey);
       if (stringSet != null) {
         stringSet.add(element);
       } else {
         stringSet = Set.of(element);
       }
       mmkv.encode(setKey, stringSet);
     } finally {
       mmkv.unlock();
     }
   }
 
   public static void removeElementFromStringSet(String setKey, String element) {
     initialize();
     MMKV mmkv = getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey);
     mmkv.lock();
     try {
       Set<String> stringSet = mmkv.decodeStringSet(setKey);
       if (stringSet == null) {
         return;
       }
       stringSet.remove(element);
       mmkv.encode(setKey, stringSet);
     } finally {
       mmkv.unlock();
     }
   }
 
   public static String[] getStringSet(String setKey) {
     initialize();
     Set<String> stringSet = getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey)
                                 .decodeStringSet(setKey);
     if (stringSet == null) {
       return new String[0];
     }
 
     return stringSet.toArray(new String[stringSet.size()]);
   }
 
   public static boolean setStringSet(String key, String[] elements) {
     initialize();
     Set<String> stringSet = Set.of(elements);
     return getMMKVInstance(mmkvIdentifier, mmkvEncryptionKey)
         .encode(key, stringSet);
   }
 }
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
index 99112256c..589738b31 100644
--- 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
@@ -1,694 +1,696 @@
 package app.comm.android.notifications;
 
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
 import android.util.JsonReader;
 import android.util.Log;
 import androidx.core.app.NotificationCompat;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.ProcessLifecycleOwner;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import app.comm.android.ExpoUtils;
 import app.comm.android.MainActivity;
 import app.comm.android.R;
 import app.comm.android.aescrypto.AESCryptoModuleCompat;
 import app.comm.android.commservices.CommAndroidServicesClient;
 import app.comm.android.fbjni.CommMMKV;
 import app.comm.android.fbjni.CommSecureStore;
 import app.comm.android.fbjni.GlobalDBSingleton;
 import app.comm.android.fbjni.MessageOperationsUtilities;
 import app.comm.android.fbjni.NetworkModule;
 import app.comm.android.fbjni.NotificationsCryptoModule;
 import app.comm.android.fbjni.StaffUtils;
 import app.comm.android.fbjni.ThreadOperations;
 import com.google.firebase.messaging.FirebaseMessagingService;
 import com.google.firebase.messaging.RemoteMessage;
 import java.io.File;
 import java.io.IOException;
 import java.lang.OutOfMemoryError;
 import java.lang.StringBuilder;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Map;
 import me.leolin.shortcutbadger.ShortcutBadger;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 public class CommNotificationsHandler extends FirebaseMessagingService {
   private static final String BADGE_KEY = "badge";
   private static final String BADGE_ONLY_KEY = "badgeOnly";
   private static final String SET_UNREAD_STATUS_KEY = "setUnreadStatus";
   private static final String NOTIF_ID_KEY = "id";
   private static final String ENCRYPTED_PAYLOAD_KEY = "encryptedPayload";
   private static final String ENCRYPTION_FAILED_KEY = "encryptionFailed";
   private static final String BLOB_HASH_KEY = "blobHash";
   private static final String BLOB_HOLDER_KEY = "blobHolder";
   private static final String AES_ENCRYPTION_KEY_LABEL = "encryptionKey";
   private static final String GROUP_NOTIF_IDS_KEY = "groupNotifIDs";
   private static final String COLLAPSE_ID_KEY = "collapseKey";
   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 CHANNEL_ID = "default";
   private static final long[] VIBRATION_SPEC = {500, 500};
   private static final Map<Integer, String> NOTIF_PRIORITY_VERBOSE =
       Map.of(0, "UNKNOWN", 1, "HIGH", 2, "NORMAL");
 
   // Those and future MMKV-related constants should match
   // similar constants in NotificationService.mm
   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_UNREAD_THICK_THREADS =
-      "NOTIFS.UNREAD_THICK_THREADS";
   private Bitmap displayableNotificationLargeIcon;
   private NotificationManager notificationManager;
   private LocalBroadcastManager localBroadcastManager;
   private AESCryptoModuleCompat aesCryptoModule;
 
   public static final String RESCIND_KEY = "rescind";
   public static final String RESCIND_ID_KEY = "rescindID";
   public static final String TITLE_KEY = "title";
   public static final String PREFIX_KEY = "prefix";
   public static final String BODY_KEY = "body";
   public static final String MESSAGE_INFOS_KEY = "messageInfos";
   public static final String THREAD_ID_KEY = "threadID";
 
   public static final String TOKEN_EVENT = "TOKEN_EVENT";
   public static final String MESSAGE_EVENT = "MESSAGE_EVENT";
 
   @Override
   public void onCreate() {
     super.onCreate();
     CommSecureStore.getInstance().initialize(
         ExpoUtils.createExpoSecureStoreSupplier(this.getApplicationContext()));
     notificationManager = (NotificationManager)this.getSystemService(
         Context.NOTIFICATION_SERVICE);
     localBroadcastManager = LocalBroadcastManager.getInstance(this);
     displayableNotificationLargeIcon = BitmapFactory.decodeResource(
         this.getApplicationContext().getResources(), R.mipmap.ic_launcher);
     aesCryptoModule = new AESCryptoModuleCompat();
   }
 
   @Override
   public void onNewToken(String token) {
     Intent intent = new Intent(TOKEN_EVENT);
     intent.putExtra("token", token);
     localBroadcastManager.sendBroadcast(intent);
   }
 
   @Override
   public void onMessageReceived(RemoteMessage message) {
     handleAlteredNotificationPriority(message);
 
     if (StaffUtils.isStaffRelease() &&
         message.getData().get(KEYSERVER_ID_KEY) == null &&
         message.getData().get(SENDER_DEVICE_ID_KEY) == null) {
       displayErrorMessageNotification(
           "Received notification without keyserver ID nor sender device ID",
           "Missing keyserver ID.",
           null);
       return;
     }
 
     if (message.getData().get(ENCRYPTED_PAYLOAD_KEY) != null) {
       try {
         message = this.olmDecryptRemoteMessage(message);
       } catch (JSONException e) {
         Log.w("COMM", "Malformed notification JSON payload.", e);
         return;
       } catch (IllegalStateException e) {
         Log.w("COMM", "Android notification type violation.", e);
         return;
       } catch (Exception e) {
         Log.w("COMM", "Notification decryption failure.", e);
         return;
       }
     }
 
     if (StaffUtils.isStaffRelease() &&
         "1".equals(message.getData().get(ENCRYPTION_FAILED_KEY))) {
       displayErrorMessageNotification(
           "Notification encryption failed on the keyserver. Please investigate",
           "Unencrypted notification",
           null);
     }
 
     if ("1".equals(message.getData().get(ENCRYPTION_FAILED_KEY))) {
       Log.w("COMM", "Received erroneously unencrypted notification.");
     }
 
     String rescind = message.getData().get(RESCIND_KEY);
     if ("true".equals(rescind) &&
         android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
       handleNotificationRescind(message);
     }
 
     try {
       handleUnreadCountUpdate(message);
     } catch (Exception e) {
       Log.w("COMM", "Unread count update failure.", e);
     }
 
     String badgeOnly = message.getData().get(BADGE_ONLY_KEY);
     if ("1".equals(badgeOnly)) {
       return;
     }
 
     if (message.getData().get(MESSAGE_INFOS_KEY) != null) {
       handleMessageInfosPersistence(message);
     }
 
     if (message.getData().get(BLOB_HASH_KEY) != null &&
         message.getData().get(AES_ENCRYPTION_KEY_LABEL) != null &&
         message.getData().get(BLOB_HOLDER_KEY) != null) {
       handleLargeNotification(message);
     }
 
     Intent intent = new Intent(MESSAGE_EVENT);
     intent.putExtra(
         "message", serializeMessageDataForIntentAttachment(message));
     localBroadcastManager.sendBroadcast(intent);
 
     if (this.isAppInForeground()) {
       return;
     }
     this.displayNotification(message);
   }
 
   private void handleAlteredNotificationPriority(RemoteMessage message) {
     if (!StaffUtils.isStaffRelease()) {
       return;
     }
 
     int originalPriority = message.getOriginalPriority();
     int priority = message.getPriority();
 
     String priorityName = NOTIF_PRIORITY_VERBOSE.get(priority);
     String originalPriorityName = NOTIF_PRIORITY_VERBOSE.get(originalPriority);
 
     if (priorityName == null || originalPriorityName == null) {
       // Technically this will never happen as
       // it would violate FCM documentation
       return;
     }
 
     if (priority != originalPriority) {
       displayErrorMessageNotification(
           "System changed notification priority from " + priorityName + " to " +
               originalPriorityName,
           "Notification priority altered.",
           null);
     }
   }
 
   private boolean isAppInForeground() {
     return ProcessLifecycleOwner.get().getLifecycle().getCurrentState() ==
         Lifecycle.State.RESUMED;
   }
 
   private boolean notificationGroupingSupported() {
     // Comm doesn't support notification grouping for clients running
     // Android versions older than 23
     return android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.M;
   }
 
   private void handleNotificationRescind(RemoteMessage message) {
     String setUnreadStatus = message.getData().get(SET_UNREAD_STATUS_KEY);
     String threadID = message.getData().get(THREAD_ID_KEY);
     if ("true".equals(setUnreadStatus)) {
       File sqliteFile =
           this.getApplicationContext().getDatabasePath("comm.sqlite");
       if (sqliteFile.exists()) {
         GlobalDBSingleton.scheduleOrRun(() -> {
           ThreadOperations.updateSQLiteUnreadStatus(
               sqliteFile.getPath(), threadID, false);
         });
       } else {
         Log.w(
             "COMM",
             "Database not existing yet. Skipping thread status update.");
       }
     }
     String rescindID = message.getData().get(RESCIND_ID_KEY);
     boolean groupSummaryPresent = false;
     boolean threadGroupPresent = false;
 
     for (StatusBarNotification notification :
          notificationManager.getActiveNotifications()) {
       String tag = notification.getTag();
       Bundle data = notification.getNotification().extras;
 
       boolean thinThreadRescind =
           tag != null && rescindID != null && tag.equals(rescindID);
 
       boolean thickThreadRescind = tag != null && data != null &&
           threadID.equals(data.getString("threadID"));
 
       boolean isGroupMember =
           threadID.equals(notification.getNotification().getGroup());
 
       boolean isGroupSummary =
           (notification.getNotification().flags &
            Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY;
 
       if (thinThreadRescind || thickThreadRescind) {
         notificationManager.cancel(notification.getTag(), notification.getId());
       } else if (
           isGroupMember && isGroupSummary && StaffUtils.isStaffRelease()) {
         groupSummaryPresent = true;
         removeNotificationFromGroupSummary(threadID, rescindID, notification);
       } else if (isGroupMember && isGroupSummary) {
         groupSummaryPresent = true;
       } else if (isGroupMember) {
         threadGroupPresent = true;
       } else if (isGroupSummary && StaffUtils.isStaffRelease()) {
         checkForUnmatchedRescind(threadID, rescindID, notification);
       }
     }
 
     if (groupSummaryPresent && !threadGroupPresent) {
       notificationManager.cancel(threadID, threadID.hashCode());
     }
   }
 
   private void handleUnreadCountUpdate(RemoteMessage message) {
     if (message.getData().get(KEYSERVER_ID_KEY) != null &&
         message.getData().get(BADGE_KEY) != null) {
       String badge = message.getData().get(BADGE_KEY);
       String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY);
       String senderKeyserverUnreadCountKey = String.join(
           MMKV_KEY_SEPARATOR,
           MMKV_KEYSERVER_PREFIX,
           senderKeyserverID,
           MMKV_UNREAD_COUNT_SUFFIX);
 
       int senderKeyserverUnreadCount;
       try {
         senderKeyserverUnreadCount = Integer.parseInt(badge);
       } catch (NumberFormatException e) {
         Log.w("COMM", "Invalid badge count", e);
         return;
       }
       CommMMKV.setInt(
           senderKeyserverUnreadCountKey, senderKeyserverUnreadCount);
     }
 
     if (message.getData().get(SENDER_DEVICE_ID_KEY) != null &&
         message.getData().get(THREAD_ID_KEY) != null &&
         message.getData().get(RESCIND_KEY) != null) {
       CommMMKV.removeElementFromStringSet(
-          MMKV_UNREAD_THICK_THREADS, message.getData().get(THREAD_ID_KEY));
+          CommMMKV.notifsStorageUnreadThickThreadsKey(),
+          message.getData().get(THREAD_ID_KEY));
     } else if (
         message.getData().get(SENDER_DEVICE_ID_KEY) != null &&
         message.getData().get(THREAD_ID_KEY) != null) {
       CommMMKV.addElementToStringSet(
-          MMKV_UNREAD_THICK_THREADS, message.getData().get(THREAD_ID_KEY));
+          CommMMKV.notifsStorageUnreadThickThreadsKey(),
+          message.getData().get(THREAD_ID_KEY));
     }
 
     int totalUnreadCount = 0;
     String[] allKeys = CommMMKV.getAllKeys();
     for (String key : allKeys) {
 
       if (!key.startsWith(MMKV_KEYSERVER_PREFIX) ||
           !key.endsWith(MMKV_UNREAD_COUNT_SUFFIX)) {
         continue;
       }
 
       Integer unreadCount = CommMMKV.getInt(key, -1);
       if (unreadCount == null) {
         continue;
       }
 
       totalUnreadCount += unreadCount;
     }
 
-    totalUnreadCount += CommMMKV.getStringSet(MMKV_UNREAD_THICK_THREADS).length;
+    totalUnreadCount +=
+        CommMMKV.getStringSet(CommMMKV.notifsStorageUnreadThickThreadsKey())
+            .length;
 
     if (totalUnreadCount > 0) {
       ShortcutBadger.applyCount(this, totalUnreadCount);
     } else {
       ShortcutBadger.removeCount(this);
     }
   }
 
   private void handleMessageInfosPersistence(RemoteMessage message) {
     String rawMessageInfosString = message.getData().get(MESSAGE_INFOS_KEY);
     File sqliteFile =
         this.getApplicationContext().getDatabasePath("comm.sqlite");
     if (rawMessageInfosString != null && sqliteFile.exists()) {
       GlobalDBSingleton.scheduleOrRun(() -> {
         MessageOperationsUtilities.storeMessageInfos(
             sqliteFile.getPath(), rawMessageInfosString);
       });
     } else if (rawMessageInfosString != null) {
       Log.w("COMM", "Database not existing yet. Skipping notification");
     }
   }
 
   private void handleLargeNotification(RemoteMessage message) {
     String blobHash = message.getData().get(BLOB_HASH_KEY);
     String blobHolder = message.getData().get(BLOB_HOLDER_KEY);
     try {
       byte[] largePayload =
           CommAndroidServicesClient.getInstance().getBlobSync(blobHash);
       message = aesDecryptRemoteMessage(message, largePayload);
       handleMessageInfosPersistence(message);
     } catch (Exception e) {
       Log.w("COMM", "Failure when handling large notification.", e);
     }
     CommAndroidServicesClient.getInstance().scheduleDeferredBlobDeletion(
         blobHash, blobHolder, this.getApplicationContext());
   }
 
   private void addToThreadGroupAndDisplay(
       String notificationID,
       NotificationCompat.Builder notificationBuilder,
       String threadID) {
 
     notificationBuilder.setGroup(threadID).setGroupAlertBehavior(
         NotificationCompat.GROUP_ALERT_CHILDREN);
 
     NotificationCompat.Builder groupSummaryNotificationBuilder =
         new NotificationCompat.Builder(this.getApplicationContext())
             .setChannelId(CHANNEL_ID)
             .setSmallIcon(R.drawable.notif_icon)
             .setContentIntent(
                 this.createStartMainActivityAction(threadID, threadID))
             .setGroup(threadID)
             .setGroupSummary(true)
             .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
 
     if (StaffUtils.isStaffRelease()) {
       ArrayList<String> groupNotifIDs =
           recordNotificationInGroupSummary(threadID, notificationID);
 
       String notificationSummaryBody =
           "Notif IDs: " + String.join(System.lineSeparator(), groupNotifIDs);
 
       Bundle data = new Bundle();
       data.putStringArrayList(GROUP_NOTIF_IDS_KEY, groupNotifIDs);
 
       groupSummaryNotificationBuilder
           .setContentTitle("Summary for thread id " + threadID)
           .setExtras(data)
           .setStyle(new NotificationCompat.BigTextStyle().bigText(
               notificationSummaryBody))
           .setAutoCancel(false);
     } else {
       groupSummaryNotificationBuilder.setAutoCancel(true);
     }
 
     notificationManager.notify(
         notificationID, notificationID.hashCode(), notificationBuilder.build());
     notificationManager.notify(
         threadID, threadID.hashCode(), groupSummaryNotificationBuilder.build());
   }
 
   private void displayNotification(RemoteMessage message) {
     if (message.getData().get(RESCIND_KEY) != null) {
       // don't attempt to display rescinds
       return;
     }
     String id = message.getData().get(NOTIF_ID_KEY);
     String collapseKey = message.getData().get(COLLAPSE_ID_KEY);
     String notificationID = id;
 
     if (collapseKey != null) {
       notificationID = collapseKey;
     }
 
     String title = message.getData().get(TITLE_KEY);
     String prefix = message.getData().get(PREFIX_KEY);
     String body = message.getData().get(BODY_KEY);
     String threadID = message.getData().get(THREAD_ID_KEY);
 
     if (prefix != null) {
       body = prefix + " " + body;
     }
 
     Bundle data = new Bundle();
     data.putString(THREAD_ID_KEY, threadID);
 
     NotificationCompat.Builder notificationBuilder =
         new NotificationCompat.Builder(this.getApplicationContext())
             .setDefaults(Notification.DEFAULT_ALL)
             .setContentText(body)
             .setExtras(data)
             .setChannelId(CHANNEL_ID)
             .setVibrate(VIBRATION_SPEC)
             .setSmallIcon(R.drawable.notif_icon)
             .setLargeIcon(displayableNotificationLargeIcon)
             .setAutoCancel(true);
 
     if (title != null) {
       notificationBuilder.setContentTitle(title);
     }
 
     if (threadID != null) {
       notificationBuilder.setContentIntent(
           this.createStartMainActivityAction(id, threadID));
     }
 
     if (!this.notificationGroupingSupported() || threadID == null) {
       notificationManager.notify(
           notificationID,
           notificationID.hashCode(),
           notificationBuilder.build());
       return;
     }
     this.addToThreadGroupAndDisplay(
         notificationID, notificationBuilder, threadID);
   }
 
   private PendingIntent
   createStartMainActivityAction(String notificationID, String threadID) {
     Intent intent =
         new Intent(this.getApplicationContext(), MainActivity.class);
     intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
     intent.putExtra("threadID", threadID);
 
     return PendingIntent.getActivity(
         this.getApplicationContext(),
         notificationID.hashCode(),
         intent,
         PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
   }
 
   private RemoteMessage updateRemoteMessageWithDecryptedPayload(
       RemoteMessage message,
       String decryptedSerializedPayload)
       throws JSONException, IllegalStateException {
     JSONObject decryptedPayload = new JSONObject(decryptedSerializedPayload);
 
     ((Iterable<String>)() -> decryptedPayload.keys())
         .forEach(payloadFieldName -> {
           if (decryptedPayload.optJSONArray(payloadFieldName) != null ||
               decryptedPayload.optJSONObject(payloadFieldName) != null) {
             throw new IllegalStateException(
                 "Notification payload JSON is not {[string]: string} type.");
           }
           String payloadFieldValue =
               decryptedPayload.optString(payloadFieldName);
           message.getData().put(payloadFieldName, payloadFieldValue);
         });
     return message;
   }
 
   private RemoteMessage olmDecryptRemoteMessage(RemoteMessage message)
       throws JSONException, IllegalStateException, NumberFormatException {
     String encryptedSerializedPayload =
         message.getData().get(ENCRYPTED_PAYLOAD_KEY);
 
     String decryptedSerializedPayload;
     if (message.getData().get(KEYSERVER_ID_KEY) != null) {
       String senderKeyserverID = message.getData().get(KEYSERVER_ID_KEY);
       decryptedSerializedPayload = NotificationsCryptoModule.decrypt(
           senderKeyserverID,
           encryptedSerializedPayload,
           NotificationsCryptoModule.olmEncryptedTypeMessage());
     } else if (message.getData().get(SENDER_DEVICE_ID_KEY) != null) {
       String senderDeviceID = message.getData().get(SENDER_DEVICE_ID_KEY);
       String messageTypeString = message.getData().get(MESSAGE_TYPE_KEY);
       int messageType = Integer.parseInt(messageTypeString);
       decryptedSerializedPayload = NotificationsCryptoModule.peerDecrypt(
           senderDeviceID, encryptedSerializedPayload, messageType);
     } else {
       throw new RuntimeException(
           "Received notification without keyserver ID nor sender device ID.");
     }
 
     return updateRemoteMessageWithDecryptedPayload(
         message, decryptedSerializedPayload);
   }
 
   private RemoteMessage
   aesDecryptRemoteMessage(RemoteMessage message, byte[] blob)
       throws JSONException, IllegalStateException {
     String aesEncryptionKey = message.getData().get(AES_ENCRYPTION_KEY_LABEL);
     // On the keyserver AES key is generated as raw bytes
     // so to send it in JSON it is encoded to Base64 string.
     byte[] aesEncryptionKeyBytes = Base64.getDecoder().decode(aesEncryptionKey);
     // On the keyserver notification is a string so it is
     // first encoded into UTF8 bytes. Therefore bytes
     // obtained from blob decryption are correct UTF8 bytes.
     String decryptedSerializedPayload = new String(
         aesCryptoModule.decrypt(aesEncryptionKeyBytes, blob),
         StandardCharsets.UTF_8);
 
     return updateRemoteMessageWithDecryptedPayload(
         message, decryptedSerializedPayload);
   }
 
   private Bundle
   serializeMessageDataForIntentAttachment(RemoteMessage message) {
     Bundle bundle = new Bundle();
     message.getData().forEach(bundle::putString);
     return bundle;
   }
 
   private void displayErrorMessageNotification(
       String errorMessage,
       String errorTitle,
       String largeErrorData) {
 
     NotificationCompat.Builder errorNotificationBuilder =
         new NotificationCompat.Builder(this.getApplicationContext())
             .setDefaults(Notification.DEFAULT_ALL)
             .setChannelId(CHANNEL_ID)
             .setSmallIcon(R.drawable.notif_icon)
             .setLargeIcon(displayableNotificationLargeIcon);
 
     if (errorMessage != null) {
       errorNotificationBuilder.setContentText(errorMessage);
     }
 
     if (errorTitle != null) {
       errorNotificationBuilder.setContentTitle(errorTitle);
     }
 
     if (largeErrorData != null) {
       errorNotificationBuilder.setStyle(
           new NotificationCompat.BigTextStyle().bigText(largeErrorData));
     }
 
     notificationManager.notify(
         errorMessage,
         errorMessage.hashCode(),
         errorNotificationBuilder.build());
   }
 
   private boolean
   isGroupSummary(StatusBarNotification notification, String threadID) {
     boolean isAnySummary = (notification.getNotification().flags &
                             Notification.FLAG_GROUP_SUMMARY) != 0;
     if (threadID == null) {
       return isAnySummary;
     }
     return isAnySummary &&
         threadID.equals(notification.getNotification().getGroup());
   }
 
   private ArrayList<String>
   recordNotificationInGroupSummary(String threadID, String notificationID) {
     ArrayList<String> groupNotifIDs =
         Arrays.stream(notificationManager.getActiveNotifications())
             .filter(notif -> isGroupSummary(notif, threadID))
             .findFirst()
             .map(
                 notif
                 -> notif.getNotification().extras.getStringArrayList(
                     GROUP_NOTIF_IDS_KEY))
             .orElse(new ArrayList<>());
 
     groupNotifIDs.add(notificationID);
     return groupNotifIDs;
   }
 
   private void removeNotificationFromGroupSummary(
       String threadID,
       String notificationID,
       StatusBarNotification groupSummaryNotification) {
     ArrayList<String> groupNotifIDs =
         groupSummaryNotification.getNotification().extras.getStringArrayList(
             GROUP_NOTIF_IDS_KEY);
     if (groupNotifIDs == null) {
       displayErrorMessageNotification(
           "Empty summary notif for thread ID " + threadID,
           "Empty Summary Notif",
           "Summary notification for thread ID " + threadID +
               " had empty body when rescinding " + notificationID);
     }
 
     boolean notificationRemoved =
         groupNotifIDs.removeIf(notifID -> notifID.equals(notificationID));
 
     if (!notificationRemoved) {
       displayErrorMessageNotification(
           "Notif with ID " + notificationID + " not in " + threadID,
           "Unrecorded Notif",
           "Rescinded notification with id " + notificationID +
               " not found in group summary for thread id " + threadID);
       return;
     }
 
     String notificationSummaryBody =
         "Notif IDs: " + String.join(System.lineSeparator(), groupNotifIDs);
 
     Bundle data = new Bundle();
     data.putStringArrayList(GROUP_NOTIF_IDS_KEY, groupNotifIDs);
 
     NotificationCompat.Builder groupSummaryNotificationBuilder =
         new NotificationCompat.Builder(this.getApplicationContext())
             .setChannelId(CHANNEL_ID)
             .setSmallIcon(R.drawable.notif_icon)
             .setContentIntent(
                 this.createStartMainActivityAction(threadID, threadID))
             .setContentTitle("Summary for thread id " + threadID)
             .setExtras(data)
             .setStyle(new NotificationCompat.BigTextStyle().bigText(
                 notificationSummaryBody))
             .setGroup(threadID)
             .setGroupSummary(true)
             .setAutoCancel(false)
             .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN);
 
     notificationManager.notify(
         threadID, threadID.hashCode(), groupSummaryNotificationBuilder.build());
   }
 
   private void checkForUnmatchedRescind(
       String threadID,
       String notificationID,
       StatusBarNotification anySummaryNotification) {
     ArrayList<String> anyGroupNotifIDs =
         anySummaryNotification.getNotification().extras.getStringArrayList(
             GROUP_NOTIF_IDS_KEY);
     if (anyGroupNotifIDs == null) {
       return;
     }
 
     String groupID = anySummaryNotification.getNotification().getGroup();
     for (String notifID : anyGroupNotifIDs) {
       if (!notificationID.equals(notifID)) {
         continue;
       }
 
       displayErrorMessageNotification(
           "Summary for thread id " + groupID + "has " + notifID,
           "Rescind Mismatch",
           "Summary notif for thread id " + groupID + " contains notif id " +
               notifID + " which was received in rescind with thread id " +
               threadID);
     }
   }
 }
diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp
index 1cc58b00f..a5853460e 100644
--- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp
+++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.cpp
@@ -1,3142 +1,3211 @@
 #include "CommCoreModule.h"
 #include "../Notifications/BackgroundDataStorage/NotificationsCryptoModule.h"
 #include "BaseDataStore.h"
 #include "CommServicesAuthMetadataEmitter.h"
 #include "DatabaseManager.h"
 #include "InternalModules/GlobalDBSingleton.h"
 #include "InternalModules/RustPromiseManager.h"
 #include "NativeModuleUtils.h"
 #include "TerminateApp.h"
 
 #include <ReactCommon/TurboModuleUtils.h>
 #include <folly/dynamic.h>
 #include <folly/json.h>
 #include <future>
 
 #include "JSIRust.h"
 #include "lib.rs.h"
 #include <algorithm>
 #include <string>
 
 namespace comm {
 
 using namespace facebook::react;
 
 jsi::Value CommCoreModule::getDraft(jsi::Runtime &rt, jsi::String key) {
   std::string keyStr = key.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string draftStr;
           try {
             draftStr = DatabaseManager::getQueryExecutor().getDraft(keyStr);
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             jsi::String draft = jsi::String::createFromUtf8(innerRt, draftStr);
             promise->resolve(std::move(draft));
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::updateDraft(
     jsi::Runtime &rt,
     jsi::String key,
     jsi::String text) {
   std::string keyStr = key.utf8(rt);
   std::string textStr = text.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().updateDraft(keyStr, textStr);
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(true);
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::moveDraft(
     jsi::Runtime &rt,
     jsi::String oldKey,
     jsi::String newKey) {
   std::string oldKeyStr = oldKey.utf8(rt);
   std::string newKeyStr = newKey.utf8(rt);
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=]() {
           std::string error;
           bool result = false;
           try {
             result = DatabaseManager::getQueryExecutor().moveDraft(
                 oldKeyStr, newKeyStr);
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(result);
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getClientDBStore(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::vector<Draft> draftsVector;
           std::vector<Thread> threadsVector;
           std::vector<MessageEntity> messagesVector;
           std::vector<MessageStoreThread> messageStoreThreadsVector;
           std::vector<Report> reportStoreVector;
           std::vector<UserInfo> userStoreVector;
           std::vector<KeyserverInfo> keyserverStoreVector;
           std::vector<CommunityInfo> communityStoreVector;
           std::vector<IntegrityThreadHash> integrityStoreVector;
           std::vector<SyncedMetadataEntry> syncedMetadataStoreVector;
           std::vector<AuxUserInfo> auxUserStoreVector;
           std::vector<ThreadActivityEntry> threadActivityStoreVector;
           std::vector<EntryInfo> entryStoreVector;
           std::vector<LocalMessageInfo> messageStoreLocalMessageInfosVector;
           try {
             draftsVector = DatabaseManager::getQueryExecutor().getAllDrafts();
             messagesVector =
                 DatabaseManager::getQueryExecutor().getInitialMessages();
             threadsVector = DatabaseManager::getQueryExecutor().getAllThreads();
             messageStoreThreadsVector =
                 DatabaseManager::getQueryExecutor().getAllMessageStoreThreads();
             reportStoreVector =
                 DatabaseManager::getQueryExecutor().getAllReports();
             userStoreVector = DatabaseManager::getQueryExecutor().getAllUsers();
             keyserverStoreVector =
                 DatabaseManager::getQueryExecutor().getAllKeyservers();
             communityStoreVector =
                 DatabaseManager::getQueryExecutor().getAllCommunities();
             integrityStoreVector = DatabaseManager::getQueryExecutor()
                                        .getAllIntegrityThreadHashes();
             syncedMetadataStoreVector =
                 DatabaseManager::getQueryExecutor().getAllSyncedMetadata();
             auxUserStoreVector =
                 DatabaseManager::getQueryExecutor().getAllAuxUserInfos();
             threadActivityStoreVector = DatabaseManager::getQueryExecutor()
                                             .getAllThreadActivityEntries();
             entryStoreVector =
                 DatabaseManager::getQueryExecutor().getAllEntries();
             messageStoreLocalMessageInfosVector =
                 DatabaseManager::getQueryExecutor()
                     .getAllMessageStoreLocalMessageInfos();
           } catch (std::system_error &e) {
             error = e.what();
           }
           auto draftsVectorPtr =
               std::make_shared<std::vector<Draft>>(std::move(draftsVector));
           auto messagesVectorPtr = std::make_shared<std::vector<MessageEntity>>(
               std::move(messagesVector));
           auto threadsVectorPtr =
               std::make_shared<std::vector<Thread>>(std::move(threadsVector));
           auto messageStoreThreadsVectorPtr =
               std::make_shared<std::vector<MessageStoreThread>>(
                   std::move(messageStoreThreadsVector));
           auto reportStoreVectorPtr = std::make_shared<std::vector<Report>>(
               std::move(reportStoreVector));
           auto userStoreVectorPtr = std::make_shared<std::vector<UserInfo>>(
               std::move(userStoreVector));
           auto keyserveStoreVectorPtr =
               std::make_shared<std::vector<KeyserverInfo>>(
                   std::move(keyserverStoreVector));
           auto communityStoreVectorPtr =
               std::make_shared<std::vector<CommunityInfo>>(
                   std::move(communityStoreVector));
           auto integrityStoreVectorPtr =
               std::make_shared<std::vector<IntegrityThreadHash>>(
                   std::move(integrityStoreVector));
           auto syncedMetadataStoreVectorPtr =
               std::make_shared<std::vector<SyncedMetadataEntry>>(
                   std::move(syncedMetadataStoreVector));
           auto auxUserStoreVectorPtr =
               std::make_shared<std::vector<AuxUserInfo>>(
                   std::move(auxUserStoreVector));
           auto threadActivityStoreVectorPtr =
               std::make_shared<std::vector<ThreadActivityEntry>>(
                   std::move(threadActivityStoreVector));
           auto entryStoreVectorPtr = std::make_shared<std::vector<EntryInfo>>(
               std::move(entryStoreVector));
           auto messageStoreLocalMessageInfosVectorPtr =
               std::make_shared<std::vector<LocalMessageInfo>>(
                   std::move(messageStoreLocalMessageInfosVector));
           this->jsInvoker_->invokeAsync([&innerRt,
                                          draftsVectorPtr,
                                          messagesVectorPtr,
                                          threadsVectorPtr,
                                          messageStoreThreadsVectorPtr,
                                          reportStoreVectorPtr,
                                          userStoreVectorPtr,
                                          keyserveStoreVectorPtr,
                                          communityStoreVectorPtr,
                                          integrityStoreVectorPtr,
                                          syncedMetadataStoreVectorPtr,
                                          auxUserStoreVectorPtr,
                                          threadActivityStoreVectorPtr,
                                          entryStoreVectorPtr,
                                          messageStoreLocalMessageInfosVectorPtr,
                                          error,
                                          promise,
                                          draftStore = this->draftStore,
                                          threadStore = this->threadStore,
                                          messageStore = this->messageStore,
                                          reportStore = this->reportStore,
                                          userStore = this->userStore,
                                          keyserverStore = this->keyserverStore,
                                          communityStore = this->communityStore,
                                          integrityStore = this->integrityStore,
                                          syncedMetadataStore =
                                              this->syncedMetadataStore,
                                          auxUserStore = this->auxUserStore,
                                          threadActivityStore =
                                              this->threadActivityStore,
                                          entryStore = this->entryStore]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             jsi::Array jsiDrafts =
                 draftStore.parseDBDataStore(innerRt, draftsVectorPtr);
             jsi::Array jsiMessages =
                 messageStore.parseDBDataStore(innerRt, messagesVectorPtr);
             jsi::Array jsiThreads =
                 threadStore.parseDBDataStore(innerRt, threadsVectorPtr);
             jsi::Array jsiMessageStoreThreads =
                 messageStore.parseDBMessageStoreThreads(
                     innerRt, messageStoreThreadsVectorPtr);
             jsi::Array jsiReportStore =
                 reportStore.parseDBDataStore(innerRt, reportStoreVectorPtr);
             jsi::Array jsiUserStore =
                 userStore.parseDBDataStore(innerRt, userStoreVectorPtr);
             jsi::Array jsiKeyserverStore = keyserverStore.parseDBDataStore(
                 innerRt, keyserveStoreVectorPtr);
             jsi::Array jsiCommunityStore = communityStore.parseDBDataStore(
                 innerRt, communityStoreVectorPtr);
             jsi::Array jsiIntegrityStore = integrityStore.parseDBDataStore(
                 innerRt, integrityStoreVectorPtr);
             jsi::Array jsiSyncedMetadataStore =
                 syncedMetadataStore.parseDBDataStore(
                     innerRt, syncedMetadataStoreVectorPtr);
             jsi::Array jsiAuxUserStore =
                 auxUserStore.parseDBDataStore(innerRt, auxUserStoreVectorPtr);
             jsi::Array jsiThreadActivityStore =
                 threadActivityStore.parseDBDataStore(
                     innerRt, threadActivityStoreVectorPtr);
             jsi::Array jsiEntryStore =
                 entryStore.parseDBDataStore(innerRt, entryStoreVectorPtr);
             jsi::Array jsiMessageStoreLocalMessageInfos =
                 messageStore.parseDBMessageStoreLocalMessageInfos(
                     innerRt, messageStoreLocalMessageInfosVectorPtr);
 
             auto jsiClientDBStore = jsi::Object(innerRt);
             jsiClientDBStore.setProperty(innerRt, "messages", jsiMessages);
             jsiClientDBStore.setProperty(innerRt, "threads", jsiThreads);
             jsiClientDBStore.setProperty(innerRt, "drafts", jsiDrafts);
             jsiClientDBStore.setProperty(
                 innerRt, "messageStoreThreads", jsiMessageStoreThreads);
             jsiClientDBStore.setProperty(innerRt, "reports", jsiReportStore);
             jsiClientDBStore.setProperty(innerRt, "users", jsiUserStore);
             jsiClientDBStore.setProperty(
                 innerRt, "keyservers", jsiKeyserverStore);
             jsiClientDBStore.setProperty(
                 innerRt, "communities", jsiCommunityStore);
             jsiClientDBStore.setProperty(
                 innerRt, "integrityThreadHashes", jsiIntegrityStore);
             jsiClientDBStore.setProperty(
                 innerRt, "syncedMetadata", jsiSyncedMetadataStore);
             jsiClientDBStore.setProperty(
                 innerRt, "auxUserInfos", jsiAuxUserStore);
             jsiClientDBStore.setProperty(
                 innerRt, "threadActivityEntries", jsiThreadActivityStore);
             jsiClientDBStore.setProperty(innerRt, "entries", jsiEntryStore);
             jsiClientDBStore.setProperty(
                 innerRt,
                 "messageStoreLocalMessageInfos",
                 jsiMessageStoreLocalMessageInfos);
 
             promise->resolve(std::move(jsiClientDBStore));
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::removeAllDrafts(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().removeAllDrafts();
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(jsi::Value::undefined());
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Array CommCoreModule::getInitialMessagesSync(jsi::Runtime &rt) {
   auto messagesVector =
       NativeModuleUtils::runSyncOrThrowJSError<std::vector<MessageEntity>>(
           rt, []() {
             return DatabaseManager::getQueryExecutor().getInitialMessages();
           });
   auto messagesVectorPtr =
       std::make_shared<std::vector<MessageEntity>>(std::move(messagesVector));
   jsi::Array jsiMessages =
       this->messageStore.parseDBDataStore(rt, messagesVectorPtr);
   return jsiMessages;
 }
 
 void CommCoreModule::processMessageStoreOperationsSync(
     jsi::Runtime &rt,
     jsi::Array operations) {
   return this->messageStore.processStoreOperationsSync(
       rt, std::move(operations));
 }
 
 jsi::Array CommCoreModule::getAllThreadsSync(jsi::Runtime &rt) {
   auto threadsVector =
       NativeModuleUtils::runSyncOrThrowJSError<std::vector<Thread>>(rt, []() {
         return DatabaseManager::getQueryExecutor().getAllThreads();
       });
 
   auto threadsVectorPtr =
       std::make_shared<std::vector<Thread>>(std::move(threadsVector));
   jsi::Array jsiThreads =
       this->threadStore.parseDBDataStore(rt, threadsVectorPtr);
 
   return jsiThreads;
 }
 
 void CommCoreModule::processThreadStoreOperationsSync(
     jsi::Runtime &rt,
     jsi::Array operations) {
   this->threadStore.processStoreOperationsSync(rt, std::move(operations));
 }
 
 void CommCoreModule::processReportStoreOperationsSync(
     jsi::Runtime &rt,
     jsi::Array operations) {
   this->reportStore.processStoreOperationsSync(rt, std::move(operations));
 }
 
 template <typename T>
 void CommCoreModule::appendDBStoreOps(
     jsi::Runtime &rt,
     jsi::Object &operations,
     const char *key,
     T &store,
     std::shared_ptr<std::vector<std::unique_ptr<DBOperationBase>>>
         &destination) {
   auto opsObject = operations.getProperty(rt, key);
   if (opsObject.isObject()) {
     auto ops = store.createOperations(rt, opsObject.asObject(rt).asArray(rt));
     std::move(
         std::make_move_iterator(ops.begin()),
         std::make_move_iterator(ops.end()),
         std::back_inserter(*destination));
   }
 }
 
 jsi::Value CommCoreModule::processDBStoreOperations(
     jsi::Runtime &rt,
     jsi::Object operations) {
   std::string createOperationsError;
 
   auto storeOpsPtr =
       std::make_shared<std::vector<std::unique_ptr<DBOperationBase>>>();
   try {
     this->appendDBStoreOps(
         rt, operations, "draftStoreOperations", this->draftStore, storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "threadStoreOperations",
         this->threadStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "messageStoreOperations",
         this->messageStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "reportStoreOperations",
         this->reportStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt, operations, "userStoreOperations", this->userStore, storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "keyserverStoreOperations",
         this->keyserverStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "communityStoreOperations",
         this->communityStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "integrityStoreOperations",
         this->integrityStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "syncedMetadataStoreOperations",
         this->syncedMetadataStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "auxUserStoreOperations",
         this->auxUserStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "threadActivityStoreOperations",
         this->threadActivityStore,
         storeOpsPtr);
     this->appendDBStoreOps(
         rt, operations, "entryStoreOperations", this->entryStore, storeOpsPtr);
     this->appendDBStoreOps(
         rt,
         operations,
         "messageSearchStoreOperations",
         this->messageSearchStore,
         storeOpsPtr);
   } catch (std::runtime_error &e) {
     createOperationsError = e.what();
   }
 
   std::vector<OutboundP2PMessage> messages;
   try {
     auto messagesJSIObj = operations.getProperty(rt, "outboundP2PMessages");
 
     if (messagesJSIObj.isObject()) {
       auto messagesJSI = messagesJSIObj.asObject(rt).asArray(rt);
       for (size_t idx = 0; idx < messagesJSI.size(rt); idx++) {
         jsi::Object msgObj = messagesJSI.getValueAtIndex(rt, idx).asObject(rt);
 
         std::string messageID =
             msgObj.getProperty(rt, "messageID").asString(rt).utf8(rt);
         std::string deviceID =
             msgObj.getProperty(rt, "deviceID").asString(rt).utf8(rt);
         std::string userID =
             msgObj.getProperty(rt, "userID").asString(rt).utf8(rt);
         std::string timestamp =
             msgObj.getProperty(rt, "timestamp").asString(rt).utf8(rt);
         std::string plaintext =
             msgObj.getProperty(rt, "plaintext").asString(rt).utf8(rt);
         std::string ciphertext =
             msgObj.getProperty(rt, "ciphertext").asString(rt).utf8(rt);
         std::string status =
             msgObj.getProperty(rt, "status").asString(rt).utf8(rt);
         bool supports_auto_retry =
             msgObj.getProperty(rt, "supportsAutoRetry").asBool();
 
         OutboundP2PMessage outboundMessage{
             messageID,
             deviceID,
             userID,
             timestamp,
             plaintext,
             ciphertext,
             status,
             supports_auto_retry};
         messages.push_back(outboundMessage);
       }
     }
 
   } catch (std::runtime_error &e) {
     createOperationsError = e.what();
   }
 
   return facebook::react::createPromiseAsJSIValue(
       rt,
       [=](jsi::Runtime &innerRt,
           std::shared_ptr<facebook::react::Promise> promise) {
         taskType job = [=]() {
           std::string error = createOperationsError;
 
           if (!error.size()) {
             try {
               DatabaseManager::getQueryExecutor().beginTransaction();
               for (const auto &operation : *storeOpsPtr) {
                 operation->execute();
               }
               if (messages.size() > 0) {
                 DatabaseManager::getQueryExecutor().addOutboundP2PMessages(
                     messages);
               }
               DatabaseManager::getQueryExecutor().captureBackupLogs();
               DatabaseManager::getQueryExecutor().commitTransaction();
             } catch (std::system_error &e) {
               error = e.what();
               DatabaseManager::getQueryExecutor().rollbackTransaction();
             }
           }
 
           if (!error.size()) {
             ::triggerBackupFileUpload();
           }
 
           this->jsInvoker_->invokeAsync([=]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 void CommCoreModule::terminate(jsi::Runtime &rt) {
   TerminateApp::terminate();
 }
 
 const std::string
 getAccountDataKey(const std::string secureStoreAccountDataKey) {
   folly::Optional<std::string> storedSecretKey =
       CommSecureStore::get(secureStoreAccountDataKey);
   if (!storedSecretKey.hasValue()) {
     storedSecretKey = crypto::Tools::generateRandomString(64);
     CommSecureStore::set(secureStoreAccountDataKey, storedSecretKey.value());
   }
   return storedSecretKey.value();
 }
 
 void CommCoreModule::persistCryptoModules(
     bool persistContentModule,
     std::optional<std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
         maybeUpdatedNotifsCryptoModule) {
   std::string storedSecretKey = getAccountDataKey(secureStoreAccountDataKey);
 
   if (!persistContentModule && !maybeUpdatedNotifsCryptoModule.has_value()) {
     return;
   }
 
   crypto::Persist newContentPersist;
   if (persistContentModule) {
     newContentPersist = this->contentCryptoModule->storeAsB64(storedSecretKey);
   }
 
   std::promise<void> persistencePromise;
   std::future<void> persistenceFuture = persistencePromise.get_future();
   GlobalDBSingleton::instance.scheduleOrRunCancellable(
       [=, &persistencePromise]() {
         try {
           DatabaseManager::getQueryExecutor().beginTransaction();
           if (persistContentModule) {
             DatabaseManager::getQueryExecutor().storeOlmPersistData(
                 DatabaseManager::getQueryExecutor().getContentAccountID(),
                 newContentPersist);
           }
           if (maybeUpdatedNotifsCryptoModule.has_value()) {
             NotificationsCryptoModule::persistNotificationsAccount(
                 maybeUpdatedNotifsCryptoModule.value().first,
                 maybeUpdatedNotifsCryptoModule.value().second,
                 true);
           }
           DatabaseManager::getQueryExecutor().commitTransaction();
           persistencePromise.set_value();
         } catch (std::system_error &e) {
           DatabaseManager::getQueryExecutor().rollbackTransaction();
           persistencePromise.set_exception(std::make_exception_ptr(e));
         }
       });
   persistenceFuture.get();
 }
 
 jsi::Value CommCoreModule::initializeCryptoAccount(jsi::Runtime &rt) {
   folly::Optional<std::string> storedSecretKey =
       CommSecureStore::get(this->secureStoreAccountDataKey);
   if (!storedSecretKey.hasValue()) {
     storedSecretKey = crypto::Tools::generateRandomString(64);
     CommSecureStore::set(
         this->secureStoreAccountDataKey, storedSecretKey.value());
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=]() {
           crypto::Persist contentPersist;
           crypto::Persist notifsPersist;
           std::string error;
           try {
             std::optional<std::string> contentAccountData =
                 DatabaseManager::getQueryExecutor().getOlmPersistAccountData(
                     DatabaseManager::getQueryExecutor().getContentAccountID());
             if (contentAccountData.has_value()) {
               contentPersist.account = crypto::OlmBuffer(
                   contentAccountData->begin(), contentAccountData->end());
               // handle sessions data
               std::vector<OlmPersistSession> sessionsData =
                   DatabaseManager::getQueryExecutor()
                       .getOlmPersistSessionsData();
               for (OlmPersistSession &sessionsDataItem : sessionsData) {
                 crypto::OlmBuffer sessionDataBuffer(
                     sessionsDataItem.session_data.begin(),
                     sessionsDataItem.session_data.end());
                 crypto::SessionPersist sessionPersist{
                     sessionDataBuffer, sessionsDataItem.version};
                 contentPersist.sessions.insert(std::make_pair(
                     sessionsDataItem.target_device_id, sessionPersist));
               }
             }
 
             std::optional<std::string> notifsAccountData =
                 DatabaseManager::getQueryExecutor().getOlmPersistAccountData(
                     DatabaseManager::getQueryExecutor().getNotifsAccountID());
 
             if (notifsAccountData.has_value()) {
               notifsPersist.account = crypto::OlmBuffer(
                   notifsAccountData->begin(), notifsAccountData->end());
             }
 
           } catch (std::system_error &e) {
             error = e.what();
           }
 
           this->cryptoThread->scheduleTask([=]() {
             std::string error;
             this->contentCryptoModule.reset(new crypto::CryptoModule(
                 this->publicCryptoAccountID,
                 storedSecretKey.value(),
                 contentPersist));
 
             std::optional<
                 std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
                 maybeNotifsCryptoAccountToPersist;
 
             if (!NotificationsCryptoModule::
                     isNotificationsAccountInitialized()) {
               maybeNotifsCryptoAccountToPersist = {
                   std::make_shared<crypto::CryptoModule>(
                       this->notifsCryptoAccountID,
                       storedSecretKey.value(),
                       notifsPersist),
                   storedSecretKey.value()};
             }
 
             try {
               this->persistCryptoModules(
                   contentPersist.isEmpty(), maybeNotifsCryptoAccountToPersist);
             } catch (const std::exception &e) {
               error = e.what();
             }
 
             this->jsInvoker_->invokeAsync([=]() {
               if (error.size()) {
                 promise->reject(error);
                 return;
               }
             });
 
             this->jsInvoker_->invokeAsync(
                 [=]() { promise->resolve(jsi::Value::undefined()); });
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getUserPublicKey(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string primaryKeysResult;
           std::string notificationsKeysResult;
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             error = "user has not been initialized";
           } else {
             primaryKeysResult = this->contentCryptoModule->getIdentityKeys();
             notificationsKeysResult =
                 NotificationsCryptoModule::getIdentityKeys();
           }
 
           std::string notificationsCurve25519Cpp, notificationsEd25519Cpp,
               blobPayloadCpp, signatureCpp, primaryCurve25519Cpp,
               primaryEd25519Cpp;
 
           if (!error.size()) {
             folly::dynamic parsedPrimary;
             try {
               parsedPrimary = folly::parseJson(primaryKeysResult);
             } catch (const folly::json::parse_error &e) {
               error =
                   "parsing identity keys failed with: " + std::string(e.what());
             }
             if (!error.size()) {
               primaryCurve25519Cpp = parsedPrimary["curve25519"].asString();
               primaryEd25519Cpp = parsedPrimary["ed25519"].asString();
 
               folly::dynamic parsedNotifications;
               try {
                 parsedNotifications = folly::parseJson(notificationsKeysResult);
               } catch (const folly::json::parse_error &e) {
                 error = "parsing notifications keys failed with: " +
                     std::string(e.what());
               }
               if (!error.size()) {
                 notificationsCurve25519Cpp =
                     parsedNotifications["curve25519"].asString();
                 notificationsEd25519Cpp =
                     parsedNotifications["ed25519"].asString();
 
                 folly::dynamic blobPayloadJSON = folly::dynamic::object(
                     "primaryIdentityPublicKeys",
                     folly::dynamic::object("ed25519", primaryEd25519Cpp)(
                         "curve25519", primaryCurve25519Cpp))(
                     "notificationIdentityPublicKeys",
                     folly::dynamic::object("ed25519", notificationsEd25519Cpp)(
                         "curve25519", notificationsCurve25519Cpp));
 
                 blobPayloadCpp = folly::toJson(blobPayloadJSON);
                 signatureCpp =
                     this->contentCryptoModule->signMessage(blobPayloadCpp);
               }
             }
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
 
             auto primaryCurve25519{
                 jsi::String::createFromUtf8(innerRt, primaryCurve25519Cpp)};
             auto primaryEd25519{
                 jsi::String::createFromUtf8(innerRt, primaryEd25519Cpp)};
             auto jsiPrimaryIdentityPublicKeys = jsi::Object(innerRt);
             jsiPrimaryIdentityPublicKeys.setProperty(
                 innerRt, "ed25519", primaryEd25519);
             jsiPrimaryIdentityPublicKeys.setProperty(
                 innerRt, "curve25519", primaryCurve25519);
 
             auto notificationsCurve25519{jsi::String::createFromUtf8(
                 innerRt, notificationsCurve25519Cpp)};
             auto notificationsEd25519{
                 jsi::String::createFromUtf8(innerRt, notificationsEd25519Cpp)};
             auto jsiNotificationIdentityPublicKeys = jsi::Object(innerRt);
             jsiNotificationIdentityPublicKeys.setProperty(
                 innerRt, "ed25519", notificationsEd25519);
             jsiNotificationIdentityPublicKeys.setProperty(
                 innerRt, "curve25519", notificationsCurve25519);
 
             auto blobPayload{
                 jsi::String::createFromUtf8(innerRt, blobPayloadCpp)};
             auto signature{jsi::String::createFromUtf8(innerRt, signatureCpp)};
 
             auto jsiClientPublicKeys = jsi::Object(innerRt);
             jsiClientPublicKeys.setProperty(
                 innerRt,
                 "primaryIdentityPublicKeys",
                 jsiPrimaryIdentityPublicKeys);
             jsiClientPublicKeys.setProperty(
                 innerRt,
                 "notificationIdentityPublicKeys",
                 jsiNotificationIdentityPublicKeys);
             jsiClientPublicKeys.setProperty(
                 innerRt, "blobPayload", blobPayload);
             jsiClientPublicKeys.setProperty(innerRt, "signature", signature);
             promise->resolve(std::move(jsiClientPublicKeys));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Object parseOLMOneTimeKeys(jsi::Runtime &rt, std::string oneTimeKeysBlob) {
   folly::dynamic parsedOneTimeKeys = folly::parseJson(oneTimeKeysBlob);
 
   auto jsiOneTimeKeysInner = jsi::Object(rt);
 
   for (auto &kvPair : parsedOneTimeKeys["curve25519"].items()) {
     jsiOneTimeKeysInner.setProperty(
         rt,
         kvPair.first.asString().c_str(),
         jsi::String::createFromUtf8(rt, kvPair.second.asString()));
   }
 
   auto jsiOneTimeKeys = jsi::Object(rt);
   jsiOneTimeKeys.setProperty(rt, "curve25519", jsiOneTimeKeysInner);
 
   return jsiOneTimeKeys;
 }
 
 std::string parseOLMPrekey(std::string prekeyBlob) {
   folly::dynamic parsedPrekey;
   try {
     parsedPrekey = folly::parseJson(prekeyBlob);
   } catch (const folly::json::parse_error &e) {
     throw std::runtime_error(
         "parsing prekey failed with: " + std::string(e.what()));
   }
 
   folly::dynamic innerObject = parsedPrekey["curve25519"];
   if (!innerObject.isObject()) {
     throw std::runtime_error("parsing prekey failed: inner object malformed");
   }
 
   if (innerObject.values().begin() == innerObject.values().end()) {
     throw std::runtime_error("parsing prekey failed: prekey missing");
   }
 
   return parsedPrekey["curve25519"].values().begin()->asString();
 }
 
 jsi::Object parseOneTimeKeysResult(
     jsi::Runtime &rt,
     std::string contentOneTimeKeysBlob,
     std::string notifOneTimeKeysBlob) {
   auto contentOneTimeKeys = parseOLMOneTimeKeys(rt, contentOneTimeKeysBlob);
   auto notifOneTimeKeys = parseOLMOneTimeKeys(rt, notifOneTimeKeysBlob);
   auto jsiOneTimeKeysResult = jsi::Object(rt);
   jsiOneTimeKeysResult.setProperty(
       rt, "contentOneTimeKeys", contentOneTimeKeys);
   jsiOneTimeKeysResult.setProperty(
       rt, "notificationsOneTimeKeys", notifOneTimeKeys);
 
   return jsiOneTimeKeysResult;
 }
 
 jsi::Object parseEncryptedData(
     jsi::Runtime &rt,
     const crypto::EncryptedData &encryptedData) {
   auto encryptedDataJSI = jsi::Object(rt);
   auto message =
       std::string{encryptedData.message.begin(), encryptedData.message.end()};
   auto messageJSI = jsi::String::createFromUtf8(rt, message);
   encryptedDataJSI.setProperty(rt, "message", messageJSI);
   encryptedDataJSI.setProperty(
       rt, "messageType", static_cast<int>(encryptedData.messageType));
   if (encryptedData.sessionVersion.has_value()) {
     encryptedDataJSI.setProperty(
         rt,
         "sessionVersion",
         static_cast<int>(encryptedData.sessionVersion.value()));
   }
   return encryptedDataJSI;
 }
 
 jsi::Value
 CommCoreModule::getOneTimeKeys(jsi::Runtime &rt, double oneTimeKeysAmount) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string contentResult;
           std::string notifResult;
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
           try {
             contentResult =
                 this->contentCryptoModule->getOneTimeKeysForPublishing(
                     oneTimeKeysAmount);
             std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>
                 notifsCryptoModuleWithPicklingKey =
                     NotificationsCryptoModule::fetchNotificationsAccount()
                         .value();
             notifResult = notifsCryptoModuleWithPicklingKey.first
                               ->getOneTimeKeysForPublishing(oneTimeKeysAmount);
             this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(
                 parseOneTimeKeysResult(innerRt, contentResult, notifResult));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::validateAndUploadPrekeys(
     jsi::Runtime &rt,
     jsi::String authUserID,
     jsi::String authDeviceID,
     jsi::String authAccessToken) {
   auto authUserIDRust = jsiStringToRustString(authUserID, rt);
   auto authDeviceIDRust = jsiStringToRustString(authDeviceID, rt);
   auto authAccessTokenRust = jsiStringToRustString(authAccessToken, rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::optional<std::string> maybeContentPrekeyToUpload;
           std::optional<std::string> maybeNotifsPrekeyToUpload;
 
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
 
           std::optional<
               std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
               notifsCryptoModuleWithPicklingKey;
           try {
             notifsCryptoModuleWithPicklingKey =
                 NotificationsCryptoModule::fetchNotificationsAccount();
             maybeContentPrekeyToUpload =
                 this->contentCryptoModule->validatePrekey();
             maybeNotifsPrekeyToUpload =
                 notifsCryptoModuleWithPicklingKey.value()
                     .first->validatePrekey();
             this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey);
 
             if (!maybeContentPrekeyToUpload.has_value()) {
               maybeContentPrekeyToUpload =
                   this->contentCryptoModule->getUnpublishedPrekey();
             }
             if (!maybeNotifsPrekeyToUpload.has_value()) {
               maybeNotifsPrekeyToUpload =
                   notifsCryptoModuleWithPicklingKey.value()
                       .first->getUnpublishedPrekey();
             }
           } catch (const std::exception &e) {
             error = e.what();
           }
 
           if (error.size()) {
             this->jsInvoker_->invokeAsync(
                 [=, &innerRt]() { promise->reject(error); });
             return;
           }
 
           if (!maybeContentPrekeyToUpload.has_value() &&
               !maybeNotifsPrekeyToUpload.has_value()) {
             this->jsInvoker_->invokeAsync(
                 [=]() { promise->resolve(jsi::Value::undefined()); });
             return;
           }
 
           std::string contentPrekeyToUpload;
           if (maybeContentPrekeyToUpload.has_value()) {
             contentPrekeyToUpload = maybeContentPrekeyToUpload.value();
           } else {
             contentPrekeyToUpload = this->contentCryptoModule->getPrekey();
           }
 
           std::string notifsPrekeyToUpload;
           if (maybeNotifsPrekeyToUpload.has_value()) {
             notifsPrekeyToUpload = maybeNotifsPrekeyToUpload.value();
           } else {
             notifsPrekeyToUpload =
                 notifsCryptoModuleWithPicklingKey.value().first->getPrekey();
           }
 
           std::string prekeyUploadError;
 
           try {
             std::string contentPrekeySignature =
                 this->contentCryptoModule->getPrekeySignature();
             std::string notifsPrekeySignature =
                 notifsCryptoModuleWithPicklingKey.value()
                     .first->getPrekeySignature();
 
             try {
               std::promise<folly::dynamic> prekeyPromise;
               std::future<folly::dynamic> prekeyFuture =
                   prekeyPromise.get_future();
               RustPromiseManager::CPPPromiseInfo promiseInfo = {
                   std::move(prekeyPromise)};
               auto currentID = RustPromiseManager::instance.addPromise(
                   std::move(promiseInfo));
               auto contentPrekeyToUploadRust =
                   rust::String(parseOLMPrekey(contentPrekeyToUpload));
               auto prekeySignatureRust = rust::string(contentPrekeySignature);
               auto notifsPrekeyToUploadRust =
                   rust::String(parseOLMPrekey(notifsPrekeyToUpload));
               auto notificationsPrekeySignatureRust =
                   rust::string(notifsPrekeySignature);
               ::identityRefreshUserPrekeys(
                   authUserIDRust,
                   authDeviceIDRust,
                   authAccessTokenRust,
                   contentPrekeyToUploadRust,
                   prekeySignatureRust,
                   notifsPrekeyToUploadRust,
                   notificationsPrekeySignatureRust,
                   currentID);
               prekeyFuture.get();
             } catch (const std::exception &e) {
               prekeyUploadError = e.what();
             }
 
             if (!prekeyUploadError.size()) {
               this->contentCryptoModule->markPrekeyAsPublished();
               notifsCryptoModuleWithPicklingKey.value()
                   .first->markPrekeyAsPublished();
               this->persistCryptoModules(
                   true, notifsCryptoModuleWithPicklingKey);
             }
           } catch (std::exception &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([=]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             if (prekeyUploadError.size()) {
               promise->reject(prekeyUploadError);
               return;
             }
             promise->resolve(jsi::Value::undefined());
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::validateAndGetPrekeys(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string contentPrekey, notifPrekey, contentPrekeySignature,
               notifPrekeySignature;
           std::optional<std::string> contentPrekeyBlob;
           std::optional<std::string> notifPrekeyBlob;
 
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
 
           std::optional<
               std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
               notifsCryptoModuleWithPicklingKey;
           try {
             notifsCryptoModuleWithPicklingKey =
                 NotificationsCryptoModule::fetchNotificationsAccount();
             contentPrekeyBlob = this->contentCryptoModule->validatePrekey();
             if (!contentPrekeyBlob) {
               contentPrekeyBlob =
                   this->contentCryptoModule->getUnpublishedPrekey();
             }
             if (!contentPrekeyBlob) {
               contentPrekeyBlob = this->contentCryptoModule->getPrekey();
             }
 
             notifPrekeyBlob = notifsCryptoModuleWithPicklingKey.value()
                                   .first->validatePrekey();
             if (!notifPrekeyBlob) {
               notifPrekeyBlob = notifsCryptoModuleWithPicklingKey.value()
                                     .first->getUnpublishedPrekey();
             }
             if (!notifPrekeyBlob) {
               notifPrekeyBlob =
                   notifsCryptoModuleWithPicklingKey.value().first->getPrekey();
             }
             this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey);
 
             contentPrekeySignature =
                 this->contentCryptoModule->getPrekeySignature();
             notifPrekeySignature = notifsCryptoModuleWithPicklingKey.value()
                                        .first->getPrekeySignature();
 
             contentPrekey = parseOLMPrekey(contentPrekeyBlob.value());
             notifPrekey = parseOLMPrekey(notifPrekeyBlob.value());
           } catch (const std::exception &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto contentPrekeyJSI =
                 jsi::String::createFromUtf8(innerRt, contentPrekey);
             auto contentPrekeySignatureJSI =
                 jsi::String::createFromUtf8(innerRt, contentPrekeySignature);
             auto notifPrekeyJSI =
                 jsi::String::createFromUtf8(innerRt, notifPrekey);
             auto notifPrekeySignatureJSI =
                 jsi::String::createFromUtf8(innerRt, notifPrekeySignature);
 
             auto signedPrekeysJSI = jsi::Object(innerRt);
             signedPrekeysJSI.setProperty(
                 innerRt, "contentPrekey", contentPrekeyJSI);
             signedPrekeysJSI.setProperty(
                 innerRt, "contentPrekeySignature", contentPrekeySignatureJSI);
             signedPrekeysJSI.setProperty(
                 innerRt, "notifPrekey", notifPrekeyJSI);
             signedPrekeysJSI.setProperty(
                 innerRt, "notifPrekeySignature", notifPrekeySignatureJSI);
 
             promise->resolve(std::move(signedPrekeysJSI));
           });
         };
 
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::initializeNotificationsSession(
     jsi::Runtime &rt,
     jsi::String identityKeys,
     jsi::String prekey,
     jsi::String prekeySignature,
     std::optional<jsi::String> oneTimeKey,
     jsi::String keyserverID) {
   auto identityKeysCpp{identityKeys.utf8(rt)};
   auto prekeyCpp{prekey.utf8(rt)};
   auto prekeySignatureCpp{prekeySignature.utf8(rt)};
   auto keyserverIDCpp{keyserverID.utf8(rt)};
 
   std::optional<std::string> oneTimeKeyCpp;
   if (oneTimeKey) {
     oneTimeKeyCpp = oneTimeKey->utf8(rt);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           crypto::EncryptedData result;
           std::optional<
               std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
               notifsCryptoModuleWithPicklingKey;
           try {
             notifsCryptoModuleWithPicklingKey =
                 NotificationsCryptoModule::fetchNotificationsAccount();
             std::optional<crypto::OlmBuffer> oneTimeKeyBuffer;
             if (oneTimeKeyCpp) {
               oneTimeKeyBuffer = crypto::OlmBuffer(
                   oneTimeKeyCpp->begin(), oneTimeKeyCpp->end());
             }
 
             notifsCryptoModuleWithPicklingKey.value()
                 .first->initializeOutboundForSendingSession(
                     keyserverIDCpp,
                     std::vector<uint8_t>(
                         identityKeysCpp.begin(), identityKeysCpp.end()),
                     std::vector<uint8_t>(prekeyCpp.begin(), prekeyCpp.end()),
                     std::vector<uint8_t>(
                         prekeySignatureCpp.begin(), prekeySignatureCpp.end()),
                     oneTimeKeyBuffer);
 
             result = notifsCryptoModuleWithPicklingKey.value().first->encrypt(
                 keyserverIDCpp,
                 NotificationsCryptoModule::initialEncryptedMessageContent);
 
             std::shared_ptr<crypto::Session> keyserverNotificationsSession =
                 notifsCryptoModuleWithPicklingKey.value()
                     .first->getSessionByDeviceId(keyserverIDCpp);
 
             NotificationsCryptoModule::persistNotificationsSession(
                 keyserverIDCpp, keyserverNotificationsSession);
 
             // Session is removed from the account since it is persisted
             // at different location that the account after serialization
             notifsCryptoModuleWithPicklingKey.value()
                 .first->removeSessionByDeviceId(keyserverIDCpp);
             this->persistCryptoModules(
                 false, notifsCryptoModuleWithPicklingKey);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(jsi::String::createFromUtf8(
                 innerRt,
                 std::string{result.message.begin(), result.message.end()}));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::isNotificationsSessionInitialized(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           bool result;
           try {
             result =
                 NotificationsCryptoModule::isNotificationsSessionInitialized(
                     "Comm");
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(result);
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::isDeviceNotificationsSessionInitialized(
     jsi::Runtime &rt,
     jsi::String deviceID) {
   auto deviceIDCpp{deviceID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
 
           std::string error;
           bool result;
           try {
             result = NotificationsCryptoModule::
                 isDeviceNotificationsSessionInitialized(deviceIDCpp);
           } catch (const std::exception &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(result);
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::isNotificationsSessionInitializedWithDevices(
     jsi::Runtime &rt,
     jsi::Array deviceIDs) {
   std::vector<std::string> deviceIDsCpp;
   for (auto idx = 0; idx < deviceIDs.size(rt); idx++) {
     std::string deviceIDCpp =
         deviceIDs.getValueAtIndex(rt, idx).asString(rt).utf8(rt);
     deviceIDsCpp.push_back(deviceIDCpp);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
 
           std::string error;
           std::vector<std::pair<std::string, bool>> result;
 
           try {
             result = NotificationsCryptoModule::
                 isNotificationsSessionInitializedWithDevices(deviceIDsCpp);
           } catch (const std::exception &e) {
             error = e.what();
           }
 
           auto resultPtr =
               std::make_shared<std::vector<std::pair<std::string, bool>>>(
                   std::move(result));
 
           this->jsInvoker_->invokeAsync(
               [&innerRt, resultPtr, error, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                   return;
                 }
 
                 jsi::Object jsiResult = jsi::Object(innerRt);
                 for (const auto &deviceResult : *resultPtr) {
                   jsiResult.setProperty(
                       innerRt, deviceResult.first.c_str(), deviceResult.second);
                 }
                 promise->resolve(std::move(jsiResult));
               });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::updateKeyserverDataInNotifStorage(
     jsi::Runtime &rt,
     jsi::Array keyserversData) {
 
   std::vector<std::pair<std::string, int>> 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});
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         try {
           for (const auto &keyserverData : keyserversDataCpp) {
             CommMMKV::setInt(keyserverData.first, keyserverData.second);
           }
         } catch (const std::exception &e) {
           error = e.what();
         }
 
         this->jsInvoker_->invokeAsync([=, &innerRt]() {
           if (error.size()) {
             promise->reject(error);
             return;
           }
           promise->resolve(jsi::Value::undefined());
         });
       });
 }
 
 jsi::Value CommCoreModule::removeKeyserverDataFromNotifStorage(
     jsi::Runtime &rt,
     jsi::Array keyserverIDsToDelete) {
   std::vector<std::string> 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);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         try {
           CommMMKV::removeKeys(keyserverIDsToDeleteCpp);
         } catch (const std::exception &e) {
           error = e.what();
         }
 
         this->jsInvoker_->invokeAsync([=, &innerRt]() {
           if (error.size()) {
             promise->reject(error);
             return;
           }
           promise->resolve(jsi::Value::undefined());
         });
       });
 }
 
 jsi::Value CommCoreModule::getKeyserverDataFromNotifStorage(
     jsi::Runtime &rt,
     jsi::Array keyserverIDs) {
   std::vector<std::string> 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);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         std::vector<std::pair<std::string, int>> keyserversDataVector{};
 
         try {
           for (const auto &keyserverID : keyserverIDsCpp) {
             std::string keyserverUnreadCountKey =
                 "KEYSERVER." + keyserverID + ".UNREAD_COUNT";
             std::optional<int> unreadCount =
                 CommMMKV::getInt(keyserverUnreadCountKey, -1);
 
             if (!unreadCount.has_value()) {
               continue;
             }
 
             keyserversDataVector.push_back({keyserverID, unreadCount.value()});
           }
         } catch (const std::exception &e) {
           error = e.what();
         }
 
         auto keyserversDataVectorPtr =
             std::make_shared<std::vector<std::pair<std::string, int>>>(
                 std::move(keyserversDataVector));
 
         this->jsInvoker_->invokeAsync(
             [&innerRt, keyserversDataVectorPtr, error, promise]() {
               if (error.size()) {
                 promise->reject(error);
                 return;
               }
 
               size_t numKeyserversData = keyserversDataVectorPtr->size();
               jsi::Array jsiKeyserversData =
                   jsi::Array(innerRt, numKeyserversData);
               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);
               }
 
               promise->resolve(std::move(jsiKeyserversData));
             });
       });
 }
 
+jsi::Value CommCoreModule::updateUnreadThickThreadsInNotifsStorage(
+    jsi::Runtime &rt,
+    jsi::Array unreadThickThreadIDs) {
+  std::vector<std::string> unreadThickThreadIDsCpp{};
+  for (auto idx = 0; idx < unreadThickThreadIDs.size(rt); idx++) {
+    std::string thickThreadID =
+        unreadThickThreadIDs.getValueAtIndex(rt, idx).asString(rt).utf8(rt);
+    unreadThickThreadIDsCpp.push_back(thickThreadID);
+  }
+
+  return createPromiseAsJSIValue(
+      rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
+        std::string error;
+        try {
+          CommMMKV::setStringSet(
+              CommMMKV::notifsStorageUnreadThickThreadsKey,
+              unreadThickThreadIDsCpp);
+        } catch (const std::exception &e) {
+          error = e.what();
+        }
+
+        this->jsInvoker_->invokeAsync([=, &innerRt]() {
+          if (error.size()) {
+            promise->reject(error);
+            return;
+          }
+          promise->resolve(jsi::Value::undefined());
+        });
+      });
+}
+
+jsi::Value
+CommCoreModule::getUnreadThickThreadIDsFromNotifsStorage(jsi::Runtime &rt) {
+  return createPromiseAsJSIValue(
+      rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
+        std::string error;
+        std::vector<std::string> unreadThickThreadIDs{};
+        try {
+          unreadThickThreadIDs = CommMMKV::getStringSet(
+              CommMMKV::notifsStorageUnreadThickThreadsKey);
+        } catch (const std::exception &e) {
+          error = e.what();
+        }
+
+        auto unreadThreadThickThreadIDsPtr =
+            std::make_shared<std::vector<std::string>>(
+                std::move(unreadThickThreadIDs));
+
+        this->jsInvoker_->invokeAsync([=, &innerRt]() {
+          if (error.size()) {
+            promise->reject(error);
+            return;
+          }
+
+          jsi::Array jsiUnreadThickThreadIDs =
+              jsi::Array(innerRt, unreadThreadThickThreadIDsPtr->size());
+          size_t writeIdx = 0;
+
+          for (const auto &thickThreadID : *unreadThreadThickThreadIDsPtr) {
+            jsi::String jsiThickThreadID =
+                jsi::String::createFromUtf8(innerRt, thickThreadID);
+            jsiUnreadThickThreadIDs.setValueAtIndex(
+                innerRt, writeIdx++, jsiThickThreadID);
+          }
+          promise->resolve(std::move(jsiUnreadThickThreadIDs));
+        });
+      });
+}
+
 jsi::Value CommCoreModule::initializeContentOutboundSession(
     jsi::Runtime &rt,
     jsi::String identityKeys,
     jsi::String prekey,
     jsi::String prekeySignature,
     std::optional<jsi::String> oneTimeKey,
     jsi::String deviceID) {
   auto identityKeysCpp{identityKeys.utf8(rt)};
   auto prekeyCpp{prekey.utf8(rt)};
   auto prekeySignatureCpp{prekeySignature.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
 
   std::optional<std::string> oneTimeKeyCpp;
   if (oneTimeKey) {
     oneTimeKeyCpp = oneTimeKey->utf8(rt);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           crypto::EncryptedData initialEncryptedData;
           int sessionVersion;
           try {
             std::optional<crypto::OlmBuffer> oneTimeKeyBuffer;
             if (oneTimeKeyCpp) {
               oneTimeKeyBuffer = crypto::OlmBuffer(
                   oneTimeKeyCpp->begin(), oneTimeKeyCpp->end());
             }
             sessionVersion =
                 this->contentCryptoModule->initializeOutboundForSendingSession(
                     deviceIDCpp,
                     std::vector<uint8_t>(
                         identityKeysCpp.begin(), identityKeysCpp.end()),
                     std::vector<uint8_t>(prekeyCpp.begin(), prekeyCpp.end()),
                     std::vector<uint8_t>(
                         prekeySignatureCpp.begin(), prekeySignatureCpp.end()),
                     oneTimeKeyBuffer);
 
             const std::string initMessage = "{\"type\": \"init\"}";
             initialEncryptedData =
                 contentCryptoModule->encrypt(deviceIDCpp, initMessage);
 
             this->persistCryptoModules(true, std::nullopt);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto initialEncryptedDataJSI =
                 parseEncryptedData(innerRt, initialEncryptedData);
             auto outboundSessionCreationResultJSI = jsi::Object(innerRt);
             outboundSessionCreationResultJSI.setProperty(
                 innerRt, "encryptedData", initialEncryptedDataJSI);
             outboundSessionCreationResultJSI.setProperty(
                 innerRt, "sessionVersion", sessionVersion);
 
             promise->resolve(std::move(outboundSessionCreationResultJSI));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::initializeContentInboundSession(
     jsi::Runtime &rt,
     jsi::String identityKeys,
     jsi::Object encryptedDataJSI,
     jsi::String deviceID,
     double sessionVersion,
     bool overwrite) {
   auto identityKeysCpp{identityKeys.utf8(rt)};
   size_t messageType =
       std::lround(encryptedDataJSI.getProperty(rt, "messageType").asNumber());
   std::string encryptedMessageCpp =
       encryptedDataJSI.getProperty(rt, "message").asString(rt).utf8(rt);
   auto deviceIDCpp{deviceID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string decryptedMessage;
           try {
             this->contentCryptoModule->initializeInboundForReceivingSession(
                 deviceIDCpp,
                 std::vector<uint8_t>(
                     encryptedMessageCpp.begin(), encryptedMessageCpp.end()),
                 std::vector<uint8_t>(
                     identityKeysCpp.begin(), identityKeysCpp.end()),
                 static_cast<int>(sessionVersion),
                 overwrite);
             crypto::EncryptedData encryptedData{
                 std::vector<uint8_t>(
                     encryptedMessageCpp.begin(), encryptedMessageCpp.end()),
                 messageType};
             decryptedMessage =
                 this->contentCryptoModule->decrypt(deviceIDCpp, encryptedData);
             this->persistCryptoModules(true, std::nullopt);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(
                 jsi::String::createFromUtf8(innerRt, decryptedMessage));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::isContentSessionInitialized(
     jsi::Runtime &rt,
     jsi::String deviceID) {
   auto deviceIDCpp{deviceID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           bool result;
 
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
 
           try {
             result = this->contentCryptoModule->hasSessionFor(deviceIDCpp);
           } catch (const std::exception &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(result);
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::initializeNotificationsOutboundSession(
     jsi::Runtime &rt,
     jsi::String identityKeys,
     jsi::String prekey,
     jsi::String prekeySignature,
     std::optional<jsi::String> oneTimeKey,
     jsi::String deviceID) {
   auto identityKeysCpp{identityKeys.utf8(rt)};
   auto prekeyCpp{prekey.utf8(rt)};
   auto prekeySignatureCpp{prekeySignature.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
 
   std::optional<std::string> oneTimeKeyCpp;
   if (oneTimeKey) {
     oneTimeKeyCpp = oneTimeKey->utf8(rt);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           crypto::EncryptedData result;
           std::optional<
               std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
               notifsCryptoModuleWithPicklingKey;
           try {
             notifsCryptoModuleWithPicklingKey =
                 NotificationsCryptoModule::fetchNotificationsAccount();
             std::optional<crypto::OlmBuffer> oneTimeKeyBuffer;
             if (oneTimeKeyCpp) {
               oneTimeKeyBuffer = crypto::OlmBuffer(
                   oneTimeKeyCpp->begin(), oneTimeKeyCpp->end());
             }
             notifsCryptoModuleWithPicklingKey.value()
                 .first->initializeOutboundForSendingSession(
                     deviceIDCpp,
                     std::vector<uint8_t>(
                         identityKeysCpp.begin(), identityKeysCpp.end()),
                     std::vector<uint8_t>(prekeyCpp.begin(), prekeyCpp.end()),
                     std::vector<uint8_t>(
                         prekeySignatureCpp.begin(), prekeySignatureCpp.end()),
                     oneTimeKeyBuffer);
 
             result = notifsCryptoModuleWithPicklingKey.value().first->encrypt(
                 deviceIDCpp,
                 NotificationsCryptoModule::initialEncryptedMessageContent);
 
             std::shared_ptr<crypto::Session> peerNotificationsSession =
                 notifsCryptoModuleWithPicklingKey.value()
                     .first->getSessionByDeviceId(deviceIDCpp);
 
             NotificationsCryptoModule::persistDeviceNotificationsSession(
                 deviceIDCpp, peerNotificationsSession);
 
             // Session is removed from the account since it is persisted
             // at different location that the account after serialization
             notifsCryptoModuleWithPicklingKey.value()
                 .first->removeSessionByDeviceId(deviceIDCpp);
             this->persistCryptoModules(
                 false, notifsCryptoModuleWithPicklingKey);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto initialEncryptedDataJSI = parseEncryptedData(innerRt, result);
             promise->resolve(std::move(initialEncryptedDataJSI));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::encrypt(
     jsi::Runtime &rt,
     jsi::String message,
     jsi::String deviceID) {
   auto messageCpp{message.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           crypto::EncryptedData encryptedMessage;
           try {
             encryptedMessage =
                 contentCryptoModule->encrypt(deviceIDCpp, messageCpp);
             this->persistCryptoModules(true, std::nullopt);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto encryptedDataJSI =
                 parseEncryptedData(innerRt, encryptedMessage);
             promise->resolve(std::move(encryptedDataJSI));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::encryptNotification(
     jsi::Runtime &rt,
     jsi::String payload,
     jsi::String deviceID) {
   auto payloadCpp{payload.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           crypto::EncryptedData result;
           try {
             result =
                 NotificationsCryptoModule::encrypt(deviceIDCpp, payloadCpp);
           } catch (const std::exception &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto encryptedDataJSI = parseEncryptedData(innerRt, result);
             promise->resolve(std::move(encryptedDataJSI));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::encryptAndPersist(
     jsi::Runtime &rt,
     jsi::String message,
     jsi::String deviceID,
     jsi::String messageID) {
   auto messageCpp{message.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
   auto messageIDCpp{messageID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           crypto::EncryptedData encryptedMessage;
           try {
             encryptedMessage =
                 contentCryptoModule->encrypt(deviceIDCpp, messageCpp);
 
             std::string storedSecretKey =
                 getAccountDataKey(secureStoreAccountDataKey);
             crypto::Persist newContentPersist =
                 this->contentCryptoModule->storeAsB64(storedSecretKey);
 
             std::promise<void> persistencePromise;
             std::future<void> persistenceFuture =
                 persistencePromise.get_future();
             GlobalDBSingleton::instance.scheduleOrRunCancellable(
                 [=, &persistencePromise]() {
                   try {
 
                     folly::dynamic jsonObject = folly::dynamic::object;
                     std::string messageStr(
                         encryptedMessage.message.begin(),
                         encryptedMessage.message.end());
                     jsonObject["message"] = messageStr;
                     jsonObject["messageType"] = encryptedMessage.messageType;
                     std::string ciphertext = folly::toJson(jsonObject);
 
                     DatabaseManager::getQueryExecutor().beginTransaction();
                     DatabaseManager::getQueryExecutor()
                         .setCiphertextForOutboundP2PMessage(
                             messageIDCpp, deviceIDCpp, ciphertext);
                     DatabaseManager::getQueryExecutor().storeOlmPersistData(
                         DatabaseManager::getQueryExecutor()
                             .getContentAccountID(),
                         newContentPersist);
                     DatabaseManager::getQueryExecutor().commitTransaction();
                     persistencePromise.set_value();
                   } catch (std::system_error &e) {
                     DatabaseManager::getQueryExecutor().rollbackTransaction();
                     persistencePromise.set_exception(
                         std::make_exception_ptr(e));
                   }
                 });
             persistenceFuture.get();
 
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto encryptedDataJSI =
                 parseEncryptedData(innerRt, encryptedMessage);
             promise->resolve(std::move(encryptedDataJSI));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::decrypt(
     jsi::Runtime &rt,
     jsi::Object encryptedDataJSI,
     jsi::String deviceID) {
   size_t messageType =
       std::lround(encryptedDataJSI.getProperty(rt, "messageType").asNumber());
   std::string message =
       encryptedDataJSI.getProperty(rt, "message").asString(rt).utf8(rt);
   auto deviceIDCpp{deviceID.utf8(rt)};
 
   std::optional<int> sessionVersion;
   if (encryptedDataJSI.hasProperty(rt, "sessionVersion")) {
     sessionVersion = std::lround(
         encryptedDataJSI.getProperty(rt, "sessionVersion").asNumber());
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string decryptedMessage;
           try {
             crypto::EncryptedData encryptedData{
                 std::vector<uint8_t>(message.begin(), message.end()),
                 messageType,
                 sessionVersion};
             decryptedMessage =
                 this->contentCryptoModule->decrypt(deviceIDCpp, encryptedData);
             this->persistCryptoModules(true, std::nullopt);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(
                 jsi::String::createFromUtf8(innerRt, decryptedMessage));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::decryptAndPersist(
     jsi::Runtime &rt,
     jsi::Object encryptedDataJSI,
     jsi::String deviceID,
     jsi::String userID,
     jsi::String messageID) {
   size_t messageType =
       std::lround(encryptedDataJSI.getProperty(rt, "messageType").asNumber());
   std::string message =
       encryptedDataJSI.getProperty(rt, "message").asString(rt).utf8(rt);
 
   std::optional<int> sessionVersion;
   if (encryptedDataJSI.hasProperty(rt, "sessionVersion")) {
     sessionVersion = std::lround(
         encryptedDataJSI.getProperty(rt, "sessionVersion").asNumber());
   }
 
   auto deviceIDCpp{deviceID.utf8(rt)};
   auto messageIDCpp{messageID.utf8(rt)};
   auto userIDCpp{userID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string decryptedMessage;
           try {
             crypto::EncryptedData encryptedData{
                 std::vector<uint8_t>(message.begin(), message.end()),
                 messageType,
                 sessionVersion};
             decryptedMessage =
                 this->contentCryptoModule->decrypt(deviceIDCpp, encryptedData);
 
             std::string storedSecretKey =
                 getAccountDataKey(secureStoreAccountDataKey);
             crypto::Persist newContentPersist =
                 this->contentCryptoModule->storeAsB64(storedSecretKey);
 
             std::promise<void> persistencePromise;
             std::future<void> persistenceFuture =
                 persistencePromise.get_future();
             GlobalDBSingleton::instance.scheduleOrRunCancellable(
                 [=, &persistencePromise]() {
                   try {
                     InboundP2PMessage message{
                         messageIDCpp,
                         deviceIDCpp,
                         decryptedMessage,
                         "decrypted",
                         userIDCpp};
 
                     DatabaseManager::getQueryExecutor().beginTransaction();
                     DatabaseManager::getQueryExecutor().addInboundP2PMessage(
                         message);
                     DatabaseManager::getQueryExecutor().storeOlmPersistData(
                         DatabaseManager::getQueryExecutor()
                             .getContentAccountID(),
                         newContentPersist);
                     DatabaseManager::getQueryExecutor().commitTransaction();
                     persistencePromise.set_value();
                   } catch (std::system_error &e) {
                     DatabaseManager::getQueryExecutor().rollbackTransaction();
                     persistencePromise.set_exception(
                         std::make_exception_ptr(e));
                   }
                 });
             persistenceFuture.get();
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(
                 jsi::String::createFromUtf8(innerRt, decryptedMessage));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::signMessage(jsi::Runtime &rt, jsi::String message) {
   std::string messageStr = message.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string signature;
           try {
             signature = this->contentCryptoModule->signMessage(messageStr);
           } catch (const std::exception &e) {
             error = "signing message failed with: " + std::string(e.what());
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
 
             auto jsiSignature{jsi::String::createFromUtf8(innerRt, signature)};
             promise->resolve(std::move(jsiSignature));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::verifySignature(
     jsi::Runtime &rt,
     jsi::String publicKey,
     jsi::String message,
     jsi::String signature) {
   std::string keyStr = publicKey.utf8(rt);
   std::string messageStr = message.utf8(rt);
   std::string signatureStr = signature.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           try {
             crypto::CryptoModule::verifySignature(
                 keyStr, messageStr, signatureStr);
           } catch (const std::exception &e) {
             error = "verifying signature failed with: " + std::string(e.what());
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(jsi::Value::undefined());
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 CommCoreModule::CommCoreModule(
     std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
     : facebook::react::CommCoreModuleSchemaCxxSpecJSI(jsInvoker),
       cryptoThread(std::make_unique<WorkerThread>("crypto")),
       draftStore(jsInvoker),
       threadStore(jsInvoker),
       messageStore(jsInvoker),
       reportStore(jsInvoker),
       userStore(jsInvoker),
       keyserverStore(jsInvoker),
       communityStore(jsInvoker),
       integrityStore(jsInvoker),
       syncedMetadataStore(jsInvoker),
       auxUserStore(jsInvoker),
       threadActivityStore(jsInvoker),
       entryStore(jsInvoker),
       messageSearchStore(jsInvoker) {
   GlobalDBSingleton::instance.enableMultithreading();
 }
 
 double CommCoreModule::getCodeVersion(jsi::Runtime &rt) {
   return this->codeVersion;
 }
 
 jsi::Value CommCoreModule::setNotifyToken(jsi::Runtime &rt, jsi::String token) {
   auto notifyToken{token.utf8(rt)};
   return createPromiseAsJSIValue(
       rt,
       [this,
        notifyToken](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, notifyToken, promise]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().setNotifyToken(notifyToken);
           } catch (std::system_error &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::clearNotifyToken(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [this](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, promise]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().clearNotifyToken();
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 };
 
 jsi::Value
 CommCoreModule::stampSQLiteDBUserID(jsi::Runtime &rt, jsi::String userID) {
   auto currentUserID{userID.utf8(rt)};
   return createPromiseAsJSIValue(
       rt,
       [this,
        currentUserID](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, promise, currentUserID]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().stampSQLiteDBUserID(
                 currentUserID);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getSQLiteStampedUserID(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [this](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, &innerRt, promise]() {
           std::string error;
           std::string result;
           try {
             result =
                 DatabaseManager::getQueryExecutor().getSQLiteStampedUserID();
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([&innerRt, error, result, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::String::createFromUtf8(innerRt, result));
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::clearSensitiveData(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [this](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         GlobalDBSingleton::instance.setTasksCancelled(true);
         taskType job = [this, promise]() {
           std::string error;
           try {
             this->innerClearCommServicesAuthMetadata();
             DatabaseManager::clearSensitiveData();
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
           GlobalDBSingleton::instance.scheduleOrRun(
               []() { GlobalDBSingleton::instance.setTasksCancelled(false); });
         };
         GlobalDBSingleton::instance.scheduleOrRun(job);
       });
 }
 
 bool CommCoreModule::checkIfDatabaseNeedsDeletion(jsi::Runtime &rt) {
   return DatabaseManager::checkIfDatabaseNeedsDeletion();
 }
 
 void CommCoreModule::reportDBOperationsFailure(jsi::Runtime &rt) {
   DatabaseManager::reportDBOperationsFailure();
 }
 
 jsi::Value CommCoreModule::computeBackupKey(
     jsi::Runtime &rt,
     jsi::String password,
     jsi::String backupID) {
   std::string passwordStr = password.utf8(rt);
   std::string backupIDStr = backupID.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::array<::std::uint8_t, 32> backupKey;
           try {
             backupKey = compute_backup_key(passwordStr, backupIDStr);
           } catch (const std::exception &e) {
             error = std::string{"Failed to compute backup key: "} + e.what();
           }
 
           this->jsInvoker_->invokeAsync([=, &innerRt]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             auto size = backupKey.size();
             auto arrayBuffer =
                 innerRt.global()
                     .getPropertyAsFunction(innerRt, "ArrayBuffer")
                     .callAsConstructor(innerRt, {static_cast<double>(size)})
                     .asObject(innerRt)
                     .getArrayBuffer(innerRt);
             auto bufferPtr = arrayBuffer.data(innerRt);
             memcpy(bufferPtr, backupKey.data(), size);
             promise->resolve(std::move(arrayBuffer));
           });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::generateRandomString(jsi::Runtime &rt, double size) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::string randomString;
           try {
             randomString =
                 crypto::Tools::generateRandomString(static_cast<size_t>(size));
           } catch (const std::exception &e) {
             error = "Failed to generate random string for size " +
                 std::to_string(size) + ": " + e.what();
           }
 
           this->jsInvoker_->invokeAsync(
               [&innerRt, error, randomString, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                 } else {
                   jsi::String jsiRandomString =
                       jsi::String::createFromUtf8(innerRt, randomString);
                   promise->resolve(std::move(jsiRandomString));
                 }
               });
         };
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value CommCoreModule::setCommServicesAuthMetadata(
     jsi::Runtime &rt,
     jsi::String userID,
     jsi::String deviceID,
     jsi::String accessToken) {
   auto userIDStr{userID.utf8(rt)};
   auto deviceIDStr{deviceID.utf8(rt)};
   auto accessTokenStr{accessToken.utf8(rt)};
   return createPromiseAsJSIValue(
       rt,
       [this, userIDStr, deviceIDStr, accessTokenStr](
           jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         try {
           this->innerSetCommServicesAuthMetadata(
               userIDStr, deviceIDStr, accessTokenStr);
         } catch (const std::exception &e) {
           error = e.what();
         }
         this->jsInvoker_->invokeAsync([error, promise]() {
           if (error.size()) {
             promise->reject(error);
           } else {
             promise->resolve(jsi::Value::undefined());
           }
         });
       });
 }
 
 void CommCoreModule::innerSetCommServicesAuthMetadata(
     std::string userID,
     std::string deviceID,
     std::string accessToken) {
   CommSecureStore::set(CommSecureStore::userID, userID);
   CommSecureStore::set(CommSecureStore::deviceID, deviceID);
   CommSecureStore::set(CommSecureStore::commServicesAccessToken, accessToken);
   CommServicesAuthMetadataEmitter::sendAuthMetadataToJS(accessToken, userID);
 }
 
 jsi::Value CommCoreModule::getCommServicesAuthMetadata(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [this](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         std::string userID;
         std::string deviceID;
         std::string accessToken;
         try {
           folly::Optional<std::string> userIDOpt =
               CommSecureStore::get(CommSecureStore::userID);
           if (userIDOpt.hasValue()) {
             userID = userIDOpt.value();
           }
           folly::Optional<std::string> deviceIDOpt =
               CommSecureStore::get(CommSecureStore::deviceID);
           if (deviceIDOpt.hasValue()) {
             deviceID = deviceIDOpt.value();
           }
           folly::Optional<std::string> accessTokenOpt =
               CommSecureStore::get(CommSecureStore::commServicesAccessToken);
           if (accessTokenOpt.hasValue()) {
             accessToken = accessTokenOpt.value();
           }
         } catch (const std::exception &e) {
           error = e.what();
         }
         this->jsInvoker_->invokeAsync(
             [&innerRt, error, userID, deviceID, accessToken, promise]() {
               if (error.size()) {
                 promise->reject(error);
               } else {
                 auto authMetadata = jsi::Object(innerRt);
                 if (!userID.empty()) {
                   authMetadata.setProperty(
                       innerRt,
                       "userID",
                       jsi::String::createFromUtf8(innerRt, userID));
                 }
                 if (!deviceID.empty()) {
                   authMetadata.setProperty(
                       innerRt,
                       "deviceID",
                       jsi::String::createFromUtf8(innerRt, deviceID));
                 }
                 if (!accessToken.empty()) {
                   authMetadata.setProperty(
                       innerRt,
                       "accessToken",
                       jsi::String::createFromUtf8(innerRt, accessToken));
                 }
                 promise->resolve(std::move(authMetadata));
               }
             });
       });
 }
 
 jsi::Value CommCoreModule::clearCommServicesAuthMetadata(jsi::Runtime &rt) {
   return this->setCommServicesAuthMetadata(
       rt,
       jsi::String::createFromUtf8(rt, ""),
       jsi::String::createFromUtf8(rt, ""),
       jsi::String::createFromUtf8(rt, ""));
 }
 
 void CommCoreModule::innerClearCommServicesAuthMetadata() {
   return this->innerSetCommServicesAuthMetadata("", "", "");
 }
 
 jsi::Value CommCoreModule::setCommServicesAccessToken(
     jsi::Runtime &rt,
     jsi::String accessToken) {
   auto accessTokenStr{accessToken.utf8(rt)};
   return createPromiseAsJSIValue(
       rt,
       [this, accessTokenStr](
           jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         try {
           CommSecureStore::set(
               CommSecureStore::commServicesAccessToken, accessTokenStr);
         } catch (const std::exception &e) {
           error = e.what();
         }
         this->jsInvoker_->invokeAsync([error, promise]() {
           if (error.size()) {
             promise->reject(error);
           } else {
             promise->resolve(jsi::Value::undefined());
           }
         });
       });
 }
 
 jsi::Value CommCoreModule::clearCommServicesAccessToken(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [this](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         std::string error;
         try {
           CommSecureStore::set(CommSecureStore::commServicesAccessToken, "");
         } catch (const std::exception &e) {
           error = e.what();
         }
         this->jsInvoker_->invokeAsync([error, promise]() {
           if (error.size()) {
             promise->reject(error);
           } else {
             promise->resolve(jsi::Value::undefined());
           }
         });
       });
 }
 
 void CommCoreModule::startBackupHandler(jsi::Runtime &rt) {
   try {
     ::startBackupHandler();
   } catch (const std::exception &e) {
     throw jsi::JSError(rt, e.what());
   }
 }
 
 void CommCoreModule::stopBackupHandler(jsi::Runtime &rt) {
   try {
     ::stopBackupHandler();
   } catch (const std::exception &e) {
     throw jsi::JSError(rt, e.what());
   }
 }
 
 jsi::Value CommCoreModule::createNewBackupInternal(
     jsi::Runtime &rt,
     std::string backupSecret,
     std::string backupMessage) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         this->cryptoThread->scheduleTask([=, &innerRt]() {
           std::string error;
 
           std::string backupID;
           try {
             backupID = crypto::Tools::generateRandomString(32);
           } catch (const std::exception &e) {
             error = "Failed to generate backupID";
           }
 
           std::string pickleKey;
           std::string pickledAccount;
           if (!error.size()) {
             try {
               pickleKey = crypto::Tools::generateRandomString(64);
               crypto::Persist persist =
                   this->contentCryptoModule->storeAsB64(pickleKey);
               pickledAccount =
                   std::string(persist.account.begin(), persist.account.end());
             } catch (const std::exception &e) {
               error = "Failed to pickle crypto account";
             }
           }
 
           if (!error.size()) {
             auto currentID = RustPromiseManager::instance.addPromise(
                 {promise, this->jsInvoker_, innerRt});
             ::createBackup(
                 rust::string(backupID),
                 rust::string(backupSecret),
                 rust::string(pickleKey),
                 rust::string(pickledAccount),
                 rust::string(backupMessage),
                 currentID);
           } else {
             this->jsInvoker_->invokeAsync(
                 [=, &innerRt]() { promise->reject(error); });
           }
         });
       });
 }
 
 jsi::Value
 CommCoreModule::createNewBackup(jsi::Runtime &rt, jsi::String backupSecret) {
   std::string backupSecretStr = backupSecret.utf8(rt);
   return createNewBackupInternal(rt, backupSecretStr, "");
 }
 
 jsi::Value CommCoreModule::createNewSIWEBackup(
     jsi::Runtime &rt,
     jsi::String backupSecret,
     jsi::String siweBackupMsg) {
   std::string backupSecretStr = backupSecret.utf8(rt);
   std::string siweBackupMsgStr = siweBackupMsg.utf8(rt);
   return createNewBackupInternal(rt, backupSecretStr, siweBackupMsgStr);
 }
 
 jsi::Value CommCoreModule::restoreBackupInternal(
     jsi::Runtime &rt,
     std::string backupSecret,
     std::string backupID,
     std::string maxVersion) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         auto currentID = RustPromiseManager::instance.addPromise(
             {promise, this->jsInvoker_, innerRt});
         ::restoreBackup(
             rust::string(backupSecret),
             rust::string(backupID),
             rust::string(maxVersion),
             currentID);
       });
 }
 
 jsi::Value CommCoreModule::restoreBackup(
     jsi::Runtime &rt,
     jsi::String backupSecret,
     jsi::String maxVersion) {
   std::string backupSecretStr = backupSecret.utf8(rt);
   std::string maxVersionStr = maxVersion.utf8(rt);
   return restoreBackupInternal(rt, backupSecretStr, "", maxVersionStr);
 }
 
 jsi::Value CommCoreModule::restoreSIWEBackup(
     jsi::Runtime &rt,
     jsi::String backupSecret,
     jsi::String backupID,
     jsi::String maxVersion) {
   std::string backupSecretStr = backupSecret.utf8(rt);
   std::string backupIDStr = backupID.utf8(rt);
   std::string maxVersionStr = maxVersion.utf8(rt);
   return restoreBackupInternal(rt, backupSecretStr, backupIDStr, maxVersionStr);
 }
 
 jsi::Value CommCoreModule::restoreBackupData(
     jsi::Runtime &rt,
     jsi::String backupID,
     jsi::String backupDataKey,
     jsi::String backupLogDataKey,
     jsi::String maxVersion) {
   std::string backupIDStr = backupID.utf8(rt);
   std::string backupDataKeyStr = backupDataKey.utf8(rt);
   std::string backupLogDataKeyStr = backupLogDataKey.utf8(rt);
   std::string maxVersionStr = maxVersion.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         auto currentID = RustPromiseManager::instance.addPromise(
             {promise, this->jsInvoker_, innerRt});
         ::restoreBackupData(
             rust::string(backupIDStr),
             rust::string(backupDataKeyStr),
             rust::string(backupLogDataKeyStr),
             rust::string(maxVersionStr),
             currentID);
       });
 }
 
 jsi::Value
 CommCoreModule::retrieveBackupKeys(jsi::Runtime &rt, jsi::String backupSecret) {
   std::string backupSecretStr = backupSecret.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         auto currentID = RustPromiseManager::instance.addPromise(
             {promise, this->jsInvoker_, innerRt});
         ::retrieveBackupKeys(rust::string(backupSecretStr), currentID);
       });
 }
 
 jsi::Value CommCoreModule::retrieveLatestSIWEBackupData(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         auto currentID = RustPromiseManager::instance.addPromise(
             {promise, this->jsInvoker_, innerRt});
         ::retrieveLatestSIWEBackupData(currentID);
       });
 }
 
 jsi::Value CommCoreModule::setSIWEBackupSecrets(
     jsi::Runtime &rt,
     jsi::Object siweBackupSecrets) {
   std::string message =
       siweBackupSecrets.getProperty(rt, "message").asString(rt).utf8(rt);
   std::string signature =
       siweBackupSecrets.getProperty(rt, "signature").asString(rt).utf8(rt);
   folly::dynamic backupSecretsJSON =
       folly::dynamic::object("message", message)("signature", signature);
   std::string backupSecrets = folly::toJson(backupSecretsJSON);
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, promise, backupSecrets]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().setMetadata(
                 "siweBackupSecrets", backupSecrets);
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getSIWEBackupSecrets(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [this](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, &innerRt, promise]() {
           std::string error;
           std::string backupSecrets;
           try {
             backupSecrets = DatabaseManager::getQueryExecutor().getMetadata(
                 "siweBackupSecrets");
           } catch (const std::exception &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync(
               [&innerRt, error, backupSecrets, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                 } else if (!backupSecrets.size()) {
                   promise->resolve(jsi::Value::undefined());
                 } else {
                   folly::dynamic backupSecretsJSON =
                       folly::parseJson(backupSecrets);
                   std::string message = backupSecretsJSON["message"].asString();
                   std::string signature =
                       backupSecretsJSON["signature"].asString();
 
                   auto siweBackupSecrets = jsi::Object(innerRt);
                   siweBackupSecrets.setProperty(
                       innerRt,
                       "message",
                       jsi::String::createFromUtf8(innerRt, message));
                   siweBackupSecrets.setProperty(
                       innerRt,
                       "signature",
                       jsi::String::createFromUtf8(innerRt, signature));
                   promise->resolve(std::move(siweBackupSecrets));
                 }
               });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getAllInboundP2PMessages(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::vector<InboundP2PMessage> messages;
 
           try {
             messages =
                 DatabaseManager::getQueryExecutor().getAllInboundP2PMessage();
 
           } catch (std::system_error &e) {
             error = e.what();
           }
           auto messagesPtr = std::make_shared<std::vector<InboundP2PMessage>>(
               std::move(messages));
 
           this->jsInvoker_->invokeAsync(
               [&innerRt, messagesPtr, error, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                   return;
                 }
 
                 jsi::Array jsiMessages =
                     jsi::Array(innerRt, messagesPtr->size());
                 size_t writeIdx = 0;
                 for (const InboundP2PMessage &msg : *messagesPtr) {
                   jsi::Object jsiMsg = jsi::Object(innerRt);
                   jsiMsg.setProperty(innerRt, "messageID", msg.message_id);
                   jsiMsg.setProperty(
                       innerRt, "senderDeviceID", msg.sender_device_id);
                   jsiMsg.setProperty(innerRt, "plaintext", msg.plaintext);
                   jsiMsg.setProperty(innerRt, "status", msg.status);
                   jsiMsg.setProperty(
                       innerRt, "senderUserID", msg.sender_user_id);
                   jsiMessages.setValueAtIndex(innerRt, writeIdx++, jsiMsg);
                 }
 
                 promise->resolve(std::move(jsiMessages));
               });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value
 CommCoreModule::removeInboundP2PMessages(jsi::Runtime &rt, jsi::Array ids) {
   std::vector<std::string> msgIDsCPP{};
   for (auto idx = 0; idx < ids.size(rt); idx++) {
     std::string msgID = ids.getValueAtIndex(rt, idx).asString(rt).utf8(rt);
     msgIDsCPP.push_back(msgID);
   }
 
   return createPromiseAsJSIValue(
       rt,
       [this,
        msgIDsCPP](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [this, promise, msgIDsCPP]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().removeInboundP2PMessages(
                 msgIDsCPP);
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value
 CommCoreModule::getOutboundP2PMessagesByID(jsi::Runtime &rt, jsi::Array ids) {
   std::vector<std::string> msgIDsCPP{};
   for (auto idx = 0; idx < ids.size(rt); idx++) {
     std::string msgID = ids.getValueAtIndex(rt, idx).asString(rt).utf8(rt);
     msgIDsCPP.push_back(msgID);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::vector<OutboundP2PMessage> messages;
 
           try {
             messages =
                 DatabaseManager::getQueryExecutor().getOutboundP2PMessagesByID(
                     msgIDsCPP);
 
           } catch (std::system_error &e) {
             error = e.what();
           }
           auto messagesPtr = std::make_shared<std::vector<OutboundP2PMessage>>(
               std::move(messages));
 
           this->jsInvoker_->invokeAsync(
               [&innerRt, messagesPtr, error, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                   return;
                 }
 
                 jsi::Array jsiMessages =
                     jsi::Array(innerRt, messagesPtr->size());
                 size_t writeIdx = 0;
                 for (const OutboundP2PMessage &msg : *messagesPtr) {
                   jsi::Object jsiMsg = jsi::Object(innerRt);
                   jsiMsg.setProperty(innerRt, "messageID", msg.message_id);
                   jsiMsg.setProperty(innerRt, "deviceID", msg.device_id);
                   jsiMsg.setProperty(innerRt, "userID", msg.user_id);
                   jsiMsg.setProperty(innerRt, "timestamp", msg.timestamp);
                   jsiMsg.setProperty(innerRt, "plaintext", msg.plaintext);
                   jsiMsg.setProperty(innerRt, "ciphertext", msg.ciphertext);
                   jsiMsg.setProperty(innerRt, "status", msg.status);
                   jsiMsg.setProperty(
                       innerRt, "supportsAutoRetry", msg.supports_auto_retry);
                   jsiMessages.setValueAtIndex(innerRt, writeIdx++, jsiMsg);
                 }
 
                 promise->resolve(std::move(jsiMessages));
               });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getAllOutboundP2PMessages(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::vector<OutboundP2PMessage> messages;
 
           try {
             messages =
                 DatabaseManager::getQueryExecutor().getAllOutboundP2PMessages();
 
           } catch (std::system_error &e) {
             error = e.what();
           }
           auto messagesPtr = std::make_shared<std::vector<OutboundP2PMessage>>(
               std::move(messages));
 
           this->jsInvoker_->invokeAsync(
               [&innerRt, messagesPtr, error, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                   return;
                 }
 
                 jsi::Array jsiMessages =
                     jsi::Array(innerRt, messagesPtr->size());
                 size_t writeIdx = 0;
                 for (const OutboundP2PMessage &msg : *messagesPtr) {
                   jsi::Object jsiMsg = jsi::Object(innerRt);
                   jsiMsg.setProperty(innerRt, "messageID", msg.message_id);
                   jsiMsg.setProperty(innerRt, "deviceID", msg.device_id);
                   jsiMsg.setProperty(innerRt, "userID", msg.user_id);
                   jsiMsg.setProperty(innerRt, "timestamp", msg.timestamp);
                   jsiMsg.setProperty(innerRt, "plaintext", msg.plaintext);
                   jsiMsg.setProperty(innerRt, "ciphertext", msg.ciphertext);
                   jsiMsg.setProperty(innerRt, "status", msg.status);
                   jsiMsg.setProperty(
                       innerRt, "supportsAutoRetry", msg.supports_auto_retry);
                   jsiMessages.setValueAtIndex(innerRt, writeIdx++, jsiMsg);
                 }
 
                 promise->resolve(std::move(jsiMessages));
               });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::markOutboundP2PMessageAsSent(
     jsi::Runtime &rt,
     jsi::String messageID,
     jsi::String deviceID) {
   auto messageIDCpp{messageID.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().markOutboundP2PMessageAsSent(
                 messageIDCpp, deviceIDCpp);
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::removeOutboundP2PMessage(
     jsi::Runtime &rt,
     jsi::String messageID,
     jsi::String deviceID) {
   auto messageIDCpp{messageID.utf8(rt)};
   auto deviceIDCpp{deviceID.utf8(rt)};
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=]() {
           std::string error;
           try {
             DatabaseManager::getQueryExecutor().removeOutboundP2PMessage(
                 messageIDCpp, deviceIDCpp);
           } catch (std::system_error &e) {
             error = e.what();
           }
           this->jsInvoker_->invokeAsync([error, promise]() {
             if (error.size()) {
               promise->reject(error);
             } else {
               promise->resolve(jsi::Value::undefined());
             }
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::resetOutboundP2PMessagesForDevice(
     jsi::Runtime &rt,
     jsi::String deviceID) {
   std::string deviceIDCpp{deviceID.utf8(rt)};
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::vector<std::string> messageIDs;
 
           try {
             DatabaseManager::getQueryExecutor().beginTransaction();
             messageIDs = DatabaseManager::getQueryExecutor()
                              .resetOutboundP2PMessagesForDevice(deviceIDCpp);
             DatabaseManager::getQueryExecutor().commitTransaction();
           } catch (std::system_error &e) {
             error = e.what();
             DatabaseManager::getQueryExecutor().rollbackTransaction();
           }
 
           auto messageIDsPtr =
               std::make_shared<std::vector<std::string>>(std::move(messageIDs));
 
           this->jsInvoker_->invokeAsync(
               [&innerRt, messageIDsPtr, error, promise]() {
                 if (error.size()) {
                   promise->reject(error);
                   return;
                 }
 
                 jsi::Array jsiMessageIDs =
                     jsi::Array(innerRt, messageIDsPtr->size());
                 size_t writeIdx = 0;
                 for (const std::string &id : *messageIDsPtr) {
                   jsi::String jsiString =
                       jsi::String::createFromUtf8(innerRt, id);
                   jsiMessageIDs.setValueAtIndex(innerRt, writeIdx++, jsiString);
                 }
 
                 promise->resolve(std::move(jsiMessageIDs));
               });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::getSyncedDatabaseVersion(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::vector<SyncedMetadataEntry> syncedMetadataStoreVector;
           try {
             syncedMetadataStoreVector =
                 DatabaseManager::getQueryExecutor().getAllSyncedMetadata();
           } catch (std::system_error &e) {
             error = e.what();
           }
           std::string version;
           for (auto &entry : syncedMetadataStoreVector) {
             if (entry.name == "db_version") {
               version = entry.data;
             }
           }
 
           this->jsInvoker_->invokeAsync([&innerRt, error, promise, version]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             jsi::String jsiVersion =
                 jsi::String::createFromUtf8(innerRt, version);
             promise->resolve(std::move(jsiVersion));
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::markPrekeysAsPublished(jsi::Runtime &rt) {
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
 
           if (this->contentCryptoModule == nullptr ||
               !NotificationsCryptoModule::isNotificationsAccountInitialized()) {
             this->jsInvoker_->invokeAsync([=, &innerRt]() {
               promise->reject("user has not been initialized");
             });
             return;
           }
 
           std::optional<
               std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
               notifsCryptoModuleWithPicklingKey;
           try {
             notifsCryptoModuleWithPicklingKey =
                 NotificationsCryptoModule::fetchNotificationsAccount();
             this->contentCryptoModule->markPrekeyAsPublished();
             notifsCryptoModuleWithPicklingKey.value()
                 .first->markPrekeyAsPublished();
             this->persistCryptoModules(true, notifsCryptoModuleWithPicklingKey);
           } catch (std::exception &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([=]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
             promise->resolve(jsi::Value::undefined());
           });
         };
 
         this->cryptoThread->scheduleTask(job);
       });
 }
 
 jsi::Value
 CommCoreModule::getRelatedMessages(jsi::Runtime &rt, jsi::String messageID) {
   std::string messageIDStr = messageID.utf8(rt);
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::shared_ptr<std::vector<MessageEntity>> messages;
           try {
             messages = std::make_shared<std::vector<MessageEntity>>(
                 DatabaseManager::getQueryExecutor().getRelatedMessages(
                     messageIDStr));
           } catch (std::system_error &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([&innerRt,
                                          error,
                                          promise,
                                          messages,
                                          messageStore = this->messageStore]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
 
             jsi::Array jsiMessages =
                 messageStore.parseDBDataStore(innerRt, messages);
             promise->resolve(std::move(jsiMessages));
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 jsi::Value CommCoreModule::searchMessages(
     jsi::Runtime &rt,
     jsi::String query,
     jsi::String threadID,
     std::optional<jsi::String> timestampCursor,
     std::optional<jsi::String> messageIDCursor) {
   std::string queryStr = query.utf8(rt);
   std::string threadIDStr = threadID.utf8(rt);
 
   std::optional<std::string> timestampCursorCpp;
   if (timestampCursor) {
     timestampCursorCpp = timestampCursor->utf8(rt);
   }
 
   std::optional<std::string> messageIDCursorCpp;
   if (messageIDCursor) {
     messageIDCursorCpp = messageIDCursor->utf8(rt);
   }
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::shared_ptr<std::vector<MessageEntity>> messages;
           try {
             messages = std::make_shared<std::vector<MessageEntity>>(
                 DatabaseManager::getQueryExecutor().searchMessages(
                     queryStr,
                     threadIDStr,
                     timestampCursorCpp,
                     messageIDCursorCpp));
           } catch (std::system_error &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([&innerRt,
                                          error,
                                          promise,
                                          messages,
                                          messageStore = this->messageStore]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
 
             jsi::Array jsiMessages =
                 messageStore.parseDBDataStore(innerRt, messages);
             promise->resolve(std::move(jsiMessages));
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 };
 
 jsi::Value CommCoreModule::fetchMessages(
     jsi::Runtime &rt,
     jsi::String threadID,
     double limit,
     double offset) {
   std::string threadIDCpp = threadID.utf8(rt);
   int limitInt = std::lround(limit);
   int offsetInt = std::lround(offset);
 
   return createPromiseAsJSIValue(
       rt, [=](jsi::Runtime &innerRt, std::shared_ptr<Promise> promise) {
         taskType job = [=, &innerRt]() {
           std::string error;
           std::shared_ptr<std::vector<MessageEntity>> messages;
           try {
             messages = std::make_shared<std::vector<MessageEntity>>(
                 DatabaseManager::getQueryExecutor().fetchMessages(
                     threadIDCpp, limitInt, offsetInt));
           } catch (std::system_error &e) {
             error = e.what();
           }
 
           this->jsInvoker_->invokeAsync([&innerRt,
                                          error,
                                          promise,
                                          messages,
                                          messageStore = this->messageStore]() {
             if (error.size()) {
               promise->reject(error);
               return;
             }
 
             jsi::Array jsiMessages =
                 messageStore.parseDBDataStore(innerRt, messages);
             promise->resolve(std::move(jsiMessages));
           });
         };
         GlobalDBSingleton::instance.scheduleOrRunCancellable(
             job, promise, this->jsInvoker_);
       });
 }
 
 } // namespace comm
diff --git a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h
index 3ec63f61b..8203a493e 100644
--- a/native/cpp/CommonCpp/NativeModules/CommCoreModule.h
+++ b/native/cpp/CommonCpp/NativeModules/CommCoreModule.h
@@ -1,283 +1,288 @@
 #pragma once
 
 #include "../CryptoTools/CryptoModule.h"
 #include "../DatabaseManagers/entities/Message.h"
 #include "../Tools/CommMMKV.h"
 #include "../Tools/CommSecureStore.h"
 #include "../Tools/WorkerThread.h"
 #include "../_generated/commJSI.h"
 #include "PersistentStorageUtilities/DataStores/AuxUserStore.h"
 #include "PersistentStorageUtilities/DataStores/CommunityStore.h"
 #include "PersistentStorageUtilities/DataStores/DraftStore.h"
 #include "PersistentStorageUtilities/DataStores/EntryStore.h"
 #include "PersistentStorageUtilities/DataStores/IntegrityStore.h"
 #include "PersistentStorageUtilities/DataStores/KeyserverStore.h"
 #include "PersistentStorageUtilities/DataStores/MessageSearchStore.h"
 #include "PersistentStorageUtilities/DataStores/MessageStore.h"
 #include "PersistentStorageUtilities/DataStores/ReportStore.h"
 #include "PersistentStorageUtilities/DataStores/SyncedMetadataStore.h"
 #include "PersistentStorageUtilities/DataStores/ThreadActivityStore.h"
 #include "PersistentStorageUtilities/DataStores/ThreadStore.h"
 #include "PersistentStorageUtilities/DataStores/UserStore.h"
 #include <ReactCommon/TurboModuleUtils.h>
 #include <jsi/jsi.h>
 #include <memory>
 #include <string>
 
 namespace comm {
 
 namespace jsi = facebook::jsi;
 
 class CommCoreModule : public facebook::react::CommCoreModuleSchemaCxxSpecJSI {
   const int codeVersion{384};
   std::unique_ptr<WorkerThread> cryptoThread;
 
   const std::string secureStoreAccountDataKey = "cryptoAccountDataKey";
   const std::string publicCryptoAccountID = "publicCryptoAccountID";
   std::unique_ptr<crypto::CryptoModule> contentCryptoModule;
   const std::string notifsCryptoAccountID = "notifsCryptoAccountID";
 
   DraftStore draftStore;
   ThreadStore threadStore;
   MessageStore messageStore;
   ReportStore reportStore;
   UserStore userStore;
   KeyserverStore keyserverStore;
   CommunityStore communityStore;
   IntegrityStore integrityStore;
   SyncedMetadataStore syncedMetadataStore;
   AuxUserStore auxUserStore;
   ThreadActivityStore threadActivityStore;
   EntryStore entryStore;
   MessageSearchStore messageSearchStore;
 
   void persistCryptoModules(
       bool persistContentModule,
       std::optional<
           std::pair<std::shared_ptr<crypto::CryptoModule>, std::string>>
           maybeUpdatedNotifsCryptoModule);
   jsi::Value createNewBackupInternal(
       jsi::Runtime &rt,
       std::string backupSecret,
       std::string backupMessage);
   jsi::Value restoreBackupInternal(
       jsi::Runtime &rt,
       std::string backupSecret,
       std::string backupID,
       std::string maxVersion);
 
   virtual jsi::Value getDraft(jsi::Runtime &rt, jsi::String key) override;
   virtual jsi::Value
   updateDraft(jsi::Runtime &rt, jsi::String key, jsi::String text) override;
   virtual jsi::Value
   moveDraft(jsi::Runtime &rt, jsi::String oldKey, jsi::String newKey) override;
   virtual jsi::Value getClientDBStore(jsi::Runtime &rt) override;
   virtual jsi::Value removeAllDrafts(jsi::Runtime &rt) override;
   virtual jsi::Array getInitialMessagesSync(jsi::Runtime &rt) override;
   virtual void processReportStoreOperationsSync(
       jsi::Runtime &rt,
       jsi::Array operations) override;
   virtual void processMessageStoreOperationsSync(
       jsi::Runtime &rt,
       jsi::Array operations) override;
   virtual jsi::Array getAllThreadsSync(jsi::Runtime &rt) override;
   virtual void processThreadStoreOperationsSync(
       jsi::Runtime &rt,
       jsi::Array operations) override;
   virtual jsi::Value
   processDBStoreOperations(jsi::Runtime &rt, jsi::Object operations) override;
   template <typename T>
   void appendDBStoreOps(
       jsi::Runtime &rt,
       jsi::Object &operations,
       const char *key,
       T &store,
       std::shared_ptr<std::vector<std::unique_ptr<DBOperationBase>>>
           &destination);
   virtual jsi::Value initializeCryptoAccount(jsi::Runtime &rt) override;
   virtual jsi::Value getUserPublicKey(jsi::Runtime &rt) override;
   virtual jsi::Value
   getOneTimeKeys(jsi::Runtime &rt, double oneTimeKeysAmount) override;
   virtual jsi::Value validateAndUploadPrekeys(
       jsi::Runtime &rt,
       jsi::String authUserID,
       jsi::String authDeviceID,
       jsi::String authAccessToken) override;
   virtual jsi::Value validateAndGetPrekeys(jsi::Runtime &rt) override;
   virtual jsi::Value initializeNotificationsSession(
       jsi::Runtime &rt,
       jsi::String identityKeys,
       jsi::String prekey,
       jsi::String prekeySignature,
       std::optional<jsi::String> oneTimeKey,
       jsi::String keyserverID) override;
   virtual jsi::Value
   isNotificationsSessionInitialized(jsi::Runtime &rt) override;
   virtual jsi::Value isDeviceNotificationsSessionInitialized(
       jsi::Runtime &rt,
       jsi::String deviceID) override;
   virtual jsi::Value isNotificationsSessionInitializedWithDevices(
       jsi::Runtime &rt,
       jsi::Array deviceIDs) override;
   virtual jsi::Value updateKeyserverDataInNotifStorage(
       jsi::Runtime &rt,
       jsi::Array keyserversData) override;
   virtual jsi::Value removeKeyserverDataFromNotifStorage(
       jsi::Runtime &rt,
       jsi::Array keyserverIDsToDelete) override;
   virtual jsi::Value getKeyserverDataFromNotifStorage(
       jsi::Runtime &rt,
       jsi::Array keyserverIDs) override;
+  virtual jsi::Value updateUnreadThickThreadsInNotifsStorage(
+      jsi::Runtime &rt,
+      jsi::Array unreadThickThreadIDs) override;
+  virtual jsi::Value
+  getUnreadThickThreadIDsFromNotifsStorage(jsi::Runtime &rt) override;
   virtual jsi::Value initializeContentOutboundSession(
       jsi::Runtime &rt,
       jsi::String identityKeys,
       jsi::String prekey,
       jsi::String prekeySignature,
       std::optional<jsi::String> oneTimeKey,
       jsi::String deviceID) override;
   virtual jsi::Value initializeContentInboundSession(
       jsi::Runtime &rt,
       jsi::String identityKeys,
       jsi::Object encryptedDataJSI,
       jsi::String deviceID,
       double sessionVersion,
       bool overwrite) override;
   virtual jsi::Value
   isContentSessionInitialized(jsi::Runtime &rt, jsi::String deviceID) override;
   virtual jsi::Value initializeNotificationsOutboundSession(
       jsi::Runtime &rt,
       jsi::String identityKeys,
       jsi::String prekey,
       jsi::String prekeySignature,
       std::optional<jsi::String> oneTimeKey,
       jsi::String deviceID) override;
   virtual jsi::Value
   encrypt(jsi::Runtime &rt, jsi::String message, jsi::String deviceID) override;
   virtual jsi::Value encryptNotification(
       jsi::Runtime &rt,
       jsi::String payload,
       jsi::String deviceID) override;
   virtual jsi::Value encryptAndPersist(
       jsi::Runtime &rt,
       jsi::String message,
       jsi::String deviceID,
       jsi::String messageID) override;
   virtual jsi::Value decrypt(
       jsi::Runtime &rt,
       jsi::Object encryptedDataJSI,
       jsi::String deviceID) override;
   virtual jsi::Value decryptAndPersist(
       jsi::Runtime &rt,
       jsi::Object encryptedDataJSI,
       jsi::String deviceID,
       jsi::String userID,
       jsi::String messageID) override;
   virtual jsi::Value
   signMessage(jsi::Runtime &rt, jsi::String message) override;
   virtual jsi::Value verifySignature(
       jsi::Runtime &rt,
       jsi::String publicKey,
       jsi::String message,
       jsi::String signature) override;
   virtual void terminate(jsi::Runtime &rt) override;
   virtual double getCodeVersion(jsi::Runtime &rt) override;
   virtual jsi::Value
   setNotifyToken(jsi::Runtime &rt, jsi::String token) override;
   virtual jsi::Value clearNotifyToken(jsi::Runtime &rt) override;
   virtual jsi::Value
   stampSQLiteDBUserID(jsi::Runtime &rt, jsi::String userID) override;
   virtual jsi::Value getSQLiteStampedUserID(jsi::Runtime &rt) override;
   virtual jsi::Value clearSensitiveData(jsi::Runtime &rt) override;
   virtual bool checkIfDatabaseNeedsDeletion(jsi::Runtime &rt) override;
   virtual void reportDBOperationsFailure(jsi::Runtime &rt) override;
   virtual jsi::Value computeBackupKey(
       jsi::Runtime &rt,
       jsi::String password,
       jsi::String backupID) override;
   virtual jsi::Value
   generateRandomString(jsi::Runtime &rt, double size) override;
   virtual jsi::Value setCommServicesAuthMetadata(
       jsi::Runtime &rt,
       jsi::String userID,
       jsi::String deviceID,
       jsi::String accessToken) override;
   virtual void innerSetCommServicesAuthMetadata(
       std::string userID,
       std::string deviceID,
       std::string accessToken);
   virtual jsi::Value getCommServicesAuthMetadata(jsi::Runtime &rt) override;
   virtual jsi::Value clearCommServicesAuthMetadata(jsi::Runtime &rt) override;
   virtual void innerClearCommServicesAuthMetadata();
   virtual jsi::Value setCommServicesAccessToken(
       jsi::Runtime &rt,
       jsi::String accessToken) override;
   virtual jsi::Value clearCommServicesAccessToken(jsi::Runtime &rt) override;
   virtual void startBackupHandler(jsi::Runtime &rt) override;
   virtual void stopBackupHandler(jsi::Runtime &rt) override;
   virtual jsi::Value
   createNewBackup(jsi::Runtime &rt, jsi::String backupSecret) override;
   virtual jsi::Value createNewSIWEBackup(
       jsi::Runtime &rt,
       jsi::String backupSecret,
       jsi::String siweBackupMsg) override;
   virtual jsi::Value restoreBackup(
       jsi::Runtime &rt,
       jsi::String backupSecret,
       jsi::String maxVersion) override;
   virtual jsi::Value restoreSIWEBackup(
       jsi::Runtime &rt,
       jsi::String backupSecret,
       jsi::String backupID,
       jsi::String maxVersion) override;
   virtual jsi::Value restoreBackupData(
       jsi::Runtime &rt,
       jsi::String backupID,
       jsi::String backupDataKey,
       jsi::String backupLogDataKey,
       jsi::String maxVersion) override;
   virtual jsi::Value
   retrieveBackupKeys(jsi::Runtime &rt, jsi::String backupSecret) override;
   virtual jsi::Value setSIWEBackupSecrets(
       jsi::Runtime &rt,
       jsi::Object siweBackupSecrets) override;
   virtual jsi::Value retrieveLatestSIWEBackupData(jsi::Runtime &rt) override;
   virtual jsi::Value getSIWEBackupSecrets(jsi::Runtime &rt) override;
   virtual jsi::Value getAllInboundP2PMessages(jsi::Runtime &rt) override;
   virtual jsi::Value
   removeInboundP2PMessages(jsi::Runtime &rt, jsi::Array ids) override;
   virtual jsi::Value
   getOutboundP2PMessagesByID(jsi::Runtime &rt, jsi::Array ids) override;
   virtual jsi::Value getAllOutboundP2PMessages(jsi::Runtime &rt) override;
   virtual jsi::Value markOutboundP2PMessageAsSent(
       jsi::Runtime &rt,
       jsi::String messageID,
       jsi::String deviceID) override;
   virtual jsi::Value removeOutboundP2PMessage(
       jsi::Runtime &rt,
       jsi::String messageID,
       jsi::String deviceID) override;
   virtual jsi::Value resetOutboundP2PMessagesForDevice(
       jsi::Runtime &rt,
       jsi::String deviceID) override;
 
   virtual jsi::Value getSyncedDatabaseVersion(jsi::Runtime &rt) override;
   virtual jsi::Value markPrekeysAsPublished(jsi::Runtime &rt) override;
   virtual jsi::Value
   getRelatedMessages(jsi::Runtime &rt, jsi::String messageID) override;
   virtual jsi::Value searchMessages(
       jsi::Runtime &rt,
       jsi::String query,
       jsi::String threadID,
       std::optional<jsi::String> timestampCursor,
       std::optional<jsi::String> messageIDCursor) override;
   virtual jsi::Value fetchMessages(
       jsi::Runtime &rt,
       jsi::String threadID,
       double limit,
       double offset) override;
 
 public:
   CommCoreModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker);
 };
 
 } // namespace comm
diff --git a/native/cpp/CommonCpp/Tools/CommMMKV.h b/native/cpp/CommonCpp/Tools/CommMMKV.h
index 7e7e80642..d90c02c11 100644
--- a/native/cpp/CommonCpp/Tools/CommMMKV.h
+++ b/native/cpp/CommonCpp/Tools/CommMMKV.h
@@ -1,48 +1,51 @@
 #pragma once
 
 #include <optional>
 #include <string>
 #include <vector>
 
 namespace comm {
 class CommMMKV {
 
 public:
   static void initialize();
   static void clearSensitiveData();
   static bool setString(std::string key, std::string value);
   static std::optional<std::string> getString(std::string key);
 
   static bool setInt(std::string key, int value);
 
   // MMKV API can't return null when we try to get integer that
   // doesn't exist. It allows us to set default value that is
   // returned instead in case the integer isn't present. The
   // developer should pass as `noValue` the value that they
   // know should never be set under certain key. Implementation
   // will pass `noValue` as default value and return `std::nullopt`
   // in case MMKV returns default value.
   static std::optional<int> getInt(std::string key, int noValue);
 
   static std::vector<std::string> getAllKeys();
   static void removeKeys(const std::vector<std::string> &keys);
 
   static void addElementToStringSet(std::string setKey, std::string element);
   static void
   removeElementFromStringSet(std::string setKey, std::string element);
   static std::vector<std::string> getStringSet(std::string setKey);
   static bool
   setStringSet(std::string key, const std::vector<std::string> &elements);
 
+  inline static const std::string notifsStorageUnreadThickThreadsKey =
+      "NOTIFS.UNREAD_THICK_THREADS";
+
   class InitFromNSEForbiddenError : public std::runtime_error {
   public:
     using std::runtime_error::runtime_error;
   };
 
   class ScopedCommMMKVLock {
   public:
     ScopedCommMMKVLock();
     ~ScopedCommMMKVLock();
   };
 };
 } // namespace comm
diff --git a/native/cpp/CommonCpp/Tools/CommMMKVJNIHelper.h b/native/cpp/CommonCpp/Tools/CommMMKVJNIHelper.h
new file mode 100644
index 000000000..84bf1b106
--- /dev/null
+++ b/native/cpp/CommonCpp/Tools/CommMMKVJNIHelper.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <fbjni/fbjni.h>
+
+namespace comm {
+class CommMMKVJNIHelper : public facebook::jni::JavaClass<CommMMKVJNIHelper> {
+public:
+  static auto constexpr kJavaDescriptor = "Lapp/comm/android/fbjni/CommMMKV;";
+  static std::string notifsStorageUnreadThickThreadsKey(
+      facebook::jni::alias_ref<CommMMKVJNIHelper> jThis);
+  static void registerNatives();
+};
+} // namespace comm
diff --git a/native/cpp/CommonCpp/_generated/commJSI-generated.cpp b/native/cpp/CommonCpp/_generated/commJSI-generated.cpp
index a927b3547..3dab23a6c 100644
--- a/native/cpp/CommonCpp/_generated/commJSI-generated.cpp
+++ b/native/cpp/CommonCpp/_generated/commJSI-generated.cpp
@@ -1,321 +1,329 @@
 /**
  * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
  *
  * Do not edit this file as changes may cause incorrect behavior and will be lost
  * once the code is regenerated.
  *
  * @generated by codegen project: GenerateModuleH.js
  */
 
 #include "commJSI.h"
 
 namespace facebook {
 namespace react {
 
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getDraft(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getDraft(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateDraft(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->updateDraft(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_moveDraft(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->moveDraft(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getClientDBStore(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getClientDBStore(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeAllDrafts(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->removeAllDrafts(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getInitialMessagesSync(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getInitialMessagesSync(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processMessageStoreOperationsSync(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->processMessageStoreOperationsSync(rt, args[0].asObject(rt).asArray(rt));
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getAllThreadsSync(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getAllThreadsSync(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processReportStoreOperationsSync(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->processReportStoreOperationsSync(rt, args[0].asObject(rt).asArray(rt));
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processThreadStoreOperationsSync(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->processThreadStoreOperationsSync(rt, args[0].asObject(rt).asArray(rt));
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processDBStoreOperations(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->processDBStoreOperations(rt, args[0].asObject(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeCryptoAccount(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->initializeCryptoAccount(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getUserPublicKey(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getUserPublicKey(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getOneTimeKeys(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getOneTimeKeys(rt, args[0].asNumber());
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_validateAndGetPrekeys(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->validateAndGetPrekeys(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_validateAndUploadPrekeys(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->validateAndUploadPrekeys(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeNotificationsSession(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->initializeNotificationsSession(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt), args[3].isNull() || args[3].isUndefined() ? std::nullopt : std::make_optional(args[3].asString(rt)), args[4].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isNotificationsSessionInitialized(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->isNotificationsSessionInitialized(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isDeviceNotificationsSessionInitialized(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->isDeviceNotificationsSessionInitialized(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isNotificationsSessionInitializedWithDevices(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&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<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->updateKeyserverDataInNotifStorage(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<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->removeKeyserverDataFromNotifStorage(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<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getKeyserverDataFromNotifStorage(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<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->updateUnreadThickThreadsInNotifsStorage(rt, args[0].asObject(rt).asArray(rt));
+}
+static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getUnreadThickThreadIDsFromNotifsStorage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
+  return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getUnreadThickThreadIDsFromNotifsStorage(rt);
+}
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentOutboundSession(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->initializeContentOutboundSession(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt), args[3].isNull() || args[3].isUndefined() ? std::nullopt : std::make_optional(args[3].asString(rt)), args[4].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentInboundSession(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->initializeContentInboundSession(rt, args[0].asString(rt), args[1].asObject(rt), args[2].asString(rt), args[3].asNumber(), args[4].asBool());
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isContentSessionInitialized(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->isContentSessionInitialized(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeNotificationsOutboundSession(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->initializeNotificationsOutboundSession(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt), args[3].isNull() || args[3].isUndefined() ? std::nullopt : std::make_optional(args[3].asString(rt)), args[4].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encrypt(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->encrypt(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encryptNotification(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->encryptNotification(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encryptAndPersist(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->encryptAndPersist(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decrypt(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->decrypt(rt, args[0].asObject(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decryptAndPersist(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->decryptAndPersist(rt, args[0].asObject(rt), args[1].asString(rt), args[2].asString(rt), args[3].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_signMessage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->signMessage(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_verifySignature(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->verifySignature(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getCodeVersion(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getCodeVersion(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_terminate(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->terminate(rt);
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setNotifyToken(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->setNotifyToken(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearNotifyToken(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->clearNotifyToken(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_stampSQLiteDBUserID(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->stampSQLiteDBUserID(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getSQLiteStampedUserID(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getSQLiteStampedUserID(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearSensitiveData(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->clearSensitiveData(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_checkIfDatabaseNeedsDeletion(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->checkIfDatabaseNeedsDeletion(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_reportDBOperationsFailure(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->reportDBOperationsFailure(rt);
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_computeBackupKey(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->computeBackupKey(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_generateRandomString(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->generateRandomString(rt, args[0].asNumber());
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setCommServicesAuthMetadata(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->setCommServicesAuthMetadata(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getCommServicesAuthMetadata(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getCommServicesAuthMetadata(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearCommServicesAuthMetadata(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->clearCommServicesAuthMetadata(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setCommServicesAccessToken(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->setCommServicesAccessToken(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearCommServicesAccessToken(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->clearCommServicesAccessToken(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_startBackupHandler(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->startBackupHandler(rt);
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_stopBackupHandler(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->stopBackupHandler(rt);
   return jsi::Value::undefined();
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_createNewBackup(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->createNewBackup(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_createNewSIWEBackup(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->createNewSIWEBackup(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_restoreBackup(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->restoreBackup(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_restoreSIWEBackup(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->restoreSIWEBackup(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_restoreBackupData(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->restoreBackupData(rt, args[0].asString(rt), args[1].asString(rt), args[2].asString(rt), args[3].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_retrieveBackupKeys(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->retrieveBackupKeys(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_retrieveLatestSIWEBackupData(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->retrieveLatestSIWEBackupData(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setSIWEBackupSecrets(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->setSIWEBackupSecrets(rt, args[0].asObject(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getSIWEBackupSecrets(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getSIWEBackupSecrets(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getAllInboundP2PMessages(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getAllInboundP2PMessages(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeInboundP2PMessages(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->removeInboundP2PMessages(rt, args[0].asObject(rt).asArray(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getOutboundP2PMessagesByID(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getOutboundP2PMessagesByID(rt, args[0].asObject(rt).asArray(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getAllOutboundP2PMessages(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getAllOutboundP2PMessages(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_markOutboundP2PMessageAsSent(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->markOutboundP2PMessageAsSent(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeOutboundP2PMessage(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->removeOutboundP2PMessage(rt, args[0].asString(rt), args[1].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_resetOutboundP2PMessagesForDevice(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->resetOutboundP2PMessagesForDevice(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getSyncedDatabaseVersion(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getSyncedDatabaseVersion(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_markPrekeysAsPublished(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->markPrekeysAsPublished(rt);
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getRelatedMessages(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->getRelatedMessages(rt, args[0].asString(rt));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_searchMessages(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->searchMessages(rt, args[0].asString(rt), args[1].asString(rt), args[2].isNull() || args[2].isUndefined() ? std::nullopt : std::make_optional(args[2].asString(rt)), args[3].isNull() || args[3].isUndefined() ? std::nullopt : std::make_optional(args[3].asString(rt)));
 }
 static jsi::Value __hostFunction_CommCoreModuleSchemaCxxSpecJSI_fetchMessages(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
   return static_cast<CommCoreModuleSchemaCxxSpecJSI *>(&turboModule)->fetchMessages(rt, args[0].asString(rt), args[1].asNumber(), args[2].asNumber());
 }
 
 CommCoreModuleSchemaCxxSpecJSI::CommCoreModuleSchemaCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
   : TurboModule("CommTurboModule", jsInvoker) {
   methodMap_["getDraft"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getDraft};
   methodMap_["updateDraft"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateDraft};
   methodMap_["moveDraft"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_moveDraft};
   methodMap_["getClientDBStore"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getClientDBStore};
   methodMap_["removeAllDrafts"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeAllDrafts};
   methodMap_["getInitialMessagesSync"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getInitialMessagesSync};
   methodMap_["processMessageStoreOperationsSync"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processMessageStoreOperationsSync};
   methodMap_["getAllThreadsSync"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getAllThreadsSync};
   methodMap_["processReportStoreOperationsSync"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processReportStoreOperationsSync};
   methodMap_["processThreadStoreOperationsSync"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processThreadStoreOperationsSync};
   methodMap_["processDBStoreOperations"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_processDBStoreOperations};
   methodMap_["initializeCryptoAccount"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeCryptoAccount};
   methodMap_["getUserPublicKey"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getUserPublicKey};
   methodMap_["getOneTimeKeys"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getOneTimeKeys};
   methodMap_["validateAndGetPrekeys"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_validateAndGetPrekeys};
   methodMap_["validateAndUploadPrekeys"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_validateAndUploadPrekeys};
   methodMap_["initializeNotificationsSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeNotificationsSession};
   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_["updateUnreadThickThreadsInNotifsStorage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_updateUnreadThickThreadsInNotifsStorage};
+  methodMap_["getUnreadThickThreadIDsFromNotifsStorage"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getUnreadThickThreadIDsFromNotifsStorage};
   methodMap_["initializeContentOutboundSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentOutboundSession};
   methodMap_["initializeContentInboundSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeContentInboundSession};
   methodMap_["isContentSessionInitialized"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_isContentSessionInitialized};
   methodMap_["initializeNotificationsOutboundSession"] = MethodMetadata {5, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_initializeNotificationsOutboundSession};
   methodMap_["encrypt"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encrypt};
   methodMap_["encryptNotification"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encryptNotification};
   methodMap_["encryptAndPersist"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_encryptAndPersist};
   methodMap_["decrypt"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decrypt};
   methodMap_["decryptAndPersist"] = MethodMetadata {4, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_decryptAndPersist};
   methodMap_["signMessage"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_signMessage};
   methodMap_["verifySignature"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_verifySignature};
   methodMap_["getCodeVersion"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getCodeVersion};
   methodMap_["terminate"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_terminate};
   methodMap_["setNotifyToken"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setNotifyToken};
   methodMap_["clearNotifyToken"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearNotifyToken};
   methodMap_["stampSQLiteDBUserID"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_stampSQLiteDBUserID};
   methodMap_["getSQLiteStampedUserID"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getSQLiteStampedUserID};
   methodMap_["clearSensitiveData"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearSensitiveData};
   methodMap_["checkIfDatabaseNeedsDeletion"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_checkIfDatabaseNeedsDeletion};
   methodMap_["reportDBOperationsFailure"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_reportDBOperationsFailure};
   methodMap_["computeBackupKey"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_computeBackupKey};
   methodMap_["generateRandomString"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_generateRandomString};
   methodMap_["setCommServicesAuthMetadata"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setCommServicesAuthMetadata};
   methodMap_["getCommServicesAuthMetadata"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getCommServicesAuthMetadata};
   methodMap_["clearCommServicesAuthMetadata"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearCommServicesAuthMetadata};
   methodMap_["setCommServicesAccessToken"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setCommServicesAccessToken};
   methodMap_["clearCommServicesAccessToken"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_clearCommServicesAccessToken};
   methodMap_["startBackupHandler"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_startBackupHandler};
   methodMap_["stopBackupHandler"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_stopBackupHandler};
   methodMap_["createNewBackup"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_createNewBackup};
   methodMap_["createNewSIWEBackup"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_createNewSIWEBackup};
   methodMap_["restoreBackup"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_restoreBackup};
   methodMap_["restoreSIWEBackup"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_restoreSIWEBackup};
   methodMap_["restoreBackupData"] = MethodMetadata {4, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_restoreBackupData};
   methodMap_["retrieveBackupKeys"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_retrieveBackupKeys};
   methodMap_["retrieveLatestSIWEBackupData"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_retrieveLatestSIWEBackupData};
   methodMap_["setSIWEBackupSecrets"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_setSIWEBackupSecrets};
   methodMap_["getSIWEBackupSecrets"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getSIWEBackupSecrets};
   methodMap_["getAllInboundP2PMessages"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getAllInboundP2PMessages};
   methodMap_["removeInboundP2PMessages"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeInboundP2PMessages};
   methodMap_["getOutboundP2PMessagesByID"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getOutboundP2PMessagesByID};
   methodMap_["getAllOutboundP2PMessages"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getAllOutboundP2PMessages};
   methodMap_["markOutboundP2PMessageAsSent"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_markOutboundP2PMessageAsSent};
   methodMap_["removeOutboundP2PMessage"] = MethodMetadata {2, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_removeOutboundP2PMessage};
   methodMap_["resetOutboundP2PMessagesForDevice"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_resetOutboundP2PMessagesForDevice};
   methodMap_["getSyncedDatabaseVersion"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getSyncedDatabaseVersion};
   methodMap_["markPrekeysAsPublished"] = MethodMetadata {0, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_markPrekeysAsPublished};
   methodMap_["getRelatedMessages"] = MethodMetadata {1, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_getRelatedMessages};
   methodMap_["searchMessages"] = MethodMetadata {4, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_searchMessages};
   methodMap_["fetchMessages"] = MethodMetadata {3, __hostFunction_CommCoreModuleSchemaCxxSpecJSI_fetchMessages};
 }
 
 
 } // namespace react
 } // namespace facebook
diff --git a/native/cpp/CommonCpp/_generated/commJSI.h b/native/cpp/CommonCpp/_generated/commJSI.h
index b19843d60..5d3055b83 100644
--- a/native/cpp/CommonCpp/_generated/commJSI.h
+++ b/native/cpp/CommonCpp/_generated/commJSI.h
@@ -1,710 +1,728 @@
 /**
  * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
  *
  * Do not edit this file as changes may cause incorrect behavior and will be lost
  * once the code is regenerated.
  *
  * @generated by codegen project: GenerateModuleH.js
  */
 
 #pragma once
 
 #include <ReactCommon/TurboModule.h>
 #include <react/bridging/Bridging.h>
 
 namespace facebook {
 namespace react {
 
 class JSI_EXPORT CommCoreModuleSchemaCxxSpecJSI : public TurboModule {
 protected:
   CommCoreModuleSchemaCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);
 
 public:
   virtual jsi::Value getDraft(jsi::Runtime &rt, jsi::String key) = 0;
   virtual jsi::Value updateDraft(jsi::Runtime &rt, jsi::String key, jsi::String text) = 0;
   virtual jsi::Value moveDraft(jsi::Runtime &rt, jsi::String oldKey, jsi::String newKey) = 0;
   virtual jsi::Value getClientDBStore(jsi::Runtime &rt) = 0;
   virtual jsi::Value removeAllDrafts(jsi::Runtime &rt) = 0;
   virtual jsi::Array getInitialMessagesSync(jsi::Runtime &rt) = 0;
   virtual void processMessageStoreOperationsSync(jsi::Runtime &rt, jsi::Array operations) = 0;
   virtual jsi::Array getAllThreadsSync(jsi::Runtime &rt) = 0;
   virtual void processReportStoreOperationsSync(jsi::Runtime &rt, jsi::Array operations) = 0;
   virtual void processThreadStoreOperationsSync(jsi::Runtime &rt, jsi::Array operations) = 0;
   virtual jsi::Value processDBStoreOperations(jsi::Runtime &rt, jsi::Object operations) = 0;
   virtual jsi::Value initializeCryptoAccount(jsi::Runtime &rt) = 0;
   virtual jsi::Value getUserPublicKey(jsi::Runtime &rt) = 0;
   virtual jsi::Value getOneTimeKeys(jsi::Runtime &rt, double oneTimeKeysAmount) = 0;
   virtual jsi::Value validateAndGetPrekeys(jsi::Runtime &rt) = 0;
   virtual jsi::Value validateAndUploadPrekeys(jsi::Runtime &rt, jsi::String authUserID, jsi::String authDeviceID, jsi::String authAccessToken) = 0;
   virtual jsi::Value initializeNotificationsSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional<jsi::String> oneTimeKey, jsi::String keyserverID) = 0;
   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 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<jsi::String> oneTimeKey, jsi::String deviceID) = 0;
   virtual jsi::Value initializeContentInboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::Object encryptedContent, jsi::String deviceID, double sessionVersion, bool overwrite) = 0;
   virtual jsi::Value isContentSessionInitialized(jsi::Runtime &rt, jsi::String deviceID) = 0;
   virtual jsi::Value initializeNotificationsOutboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional<jsi::String> oneTimeKey, jsi::String deviceID) = 0;
   virtual jsi::Value encrypt(jsi::Runtime &rt, jsi::String message, jsi::String deviceID) = 0;
   virtual jsi::Value encryptNotification(jsi::Runtime &rt, jsi::String payload, jsi::String deviceID) = 0;
   virtual jsi::Value encryptAndPersist(jsi::Runtime &rt, jsi::String message, jsi::String deviceID, jsi::String messageID) = 0;
   virtual jsi::Value decrypt(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID) = 0;
   virtual jsi::Value decryptAndPersist(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID, jsi::String userID, jsi::String messageID) = 0;
   virtual jsi::Value signMessage(jsi::Runtime &rt, jsi::String message) = 0;
   virtual jsi::Value verifySignature(jsi::Runtime &rt, jsi::String publicKey, jsi::String message, jsi::String signature) = 0;
   virtual double getCodeVersion(jsi::Runtime &rt) = 0;
   virtual void terminate(jsi::Runtime &rt) = 0;
   virtual jsi::Value setNotifyToken(jsi::Runtime &rt, jsi::String token) = 0;
   virtual jsi::Value clearNotifyToken(jsi::Runtime &rt) = 0;
   virtual jsi::Value stampSQLiteDBUserID(jsi::Runtime &rt, jsi::String userID) = 0;
   virtual jsi::Value getSQLiteStampedUserID(jsi::Runtime &rt) = 0;
   virtual jsi::Value clearSensitiveData(jsi::Runtime &rt) = 0;
   virtual bool checkIfDatabaseNeedsDeletion(jsi::Runtime &rt) = 0;
   virtual void reportDBOperationsFailure(jsi::Runtime &rt) = 0;
   virtual jsi::Value computeBackupKey(jsi::Runtime &rt, jsi::String password, jsi::String backupID) = 0;
   virtual jsi::Value generateRandomString(jsi::Runtime &rt, double size) = 0;
   virtual jsi::Value setCommServicesAuthMetadata(jsi::Runtime &rt, jsi::String userID, jsi::String deviceID, jsi::String accessToken) = 0;
   virtual jsi::Value getCommServicesAuthMetadata(jsi::Runtime &rt) = 0;
   virtual jsi::Value clearCommServicesAuthMetadata(jsi::Runtime &rt) = 0;
   virtual jsi::Value setCommServicesAccessToken(jsi::Runtime &rt, jsi::String accessToken) = 0;
   virtual jsi::Value clearCommServicesAccessToken(jsi::Runtime &rt) = 0;
   virtual void startBackupHandler(jsi::Runtime &rt) = 0;
   virtual void stopBackupHandler(jsi::Runtime &rt) = 0;
   virtual jsi::Value createNewBackup(jsi::Runtime &rt, jsi::String backupSecret) = 0;
   virtual jsi::Value createNewSIWEBackup(jsi::Runtime &rt, jsi::String backupSecret, jsi::String siweBackupMsg) = 0;
   virtual jsi::Value restoreBackup(jsi::Runtime &rt, jsi::String backupSecret, jsi::String maxVersion) = 0;
   virtual jsi::Value restoreSIWEBackup(jsi::Runtime &rt, jsi::String backupSecret, jsi::String backupID, jsi::String maxVersion) = 0;
   virtual jsi::Value restoreBackupData(jsi::Runtime &rt, jsi::String backupID, jsi::String backupDataKey, jsi::String backupLogDataKey, jsi::String maxVersion) = 0;
   virtual jsi::Value retrieveBackupKeys(jsi::Runtime &rt, jsi::String backupSecret) = 0;
   virtual jsi::Value retrieveLatestSIWEBackupData(jsi::Runtime &rt) = 0;
   virtual jsi::Value setSIWEBackupSecrets(jsi::Runtime &rt, jsi::Object siweBackupSecrets) = 0;
   virtual jsi::Value getSIWEBackupSecrets(jsi::Runtime &rt) = 0;
   virtual jsi::Value getAllInboundP2PMessages(jsi::Runtime &rt) = 0;
   virtual jsi::Value removeInboundP2PMessages(jsi::Runtime &rt, jsi::Array ids) = 0;
   virtual jsi::Value getOutboundP2PMessagesByID(jsi::Runtime &rt, jsi::Array ids) = 0;
   virtual jsi::Value getAllOutboundP2PMessages(jsi::Runtime &rt) = 0;
   virtual jsi::Value markOutboundP2PMessageAsSent(jsi::Runtime &rt, jsi::String messageID, jsi::String deviceID) = 0;
   virtual jsi::Value removeOutboundP2PMessage(jsi::Runtime &rt, jsi::String messageID, jsi::String deviceID) = 0;
   virtual jsi::Value resetOutboundP2PMessagesForDevice(jsi::Runtime &rt, jsi::String deviceID) = 0;
   virtual jsi::Value getSyncedDatabaseVersion(jsi::Runtime &rt) = 0;
   virtual jsi::Value markPrekeysAsPublished(jsi::Runtime &rt) = 0;
   virtual jsi::Value getRelatedMessages(jsi::Runtime &rt, jsi::String messageID) = 0;
   virtual jsi::Value searchMessages(jsi::Runtime &rt, jsi::String query, jsi::String threadID, std::optional<jsi::String> timestampCursor, std::optional<jsi::String> messageIDCursor) = 0;
   virtual jsi::Value fetchMessages(jsi::Runtime &rt, jsi::String threadID, double limit, double offset) = 0;
 
 };
 
 template <typename T>
 class JSI_EXPORT CommCoreModuleSchemaCxxSpec : public TurboModule {
 public:
   jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propName) override {
     return delegate_.get(rt, propName);
   }
 
 protected:
   CommCoreModuleSchemaCxxSpec(std::shared_ptr<CallInvoker> jsInvoker)
     : TurboModule("CommTurboModule", jsInvoker),
       delegate_(static_cast<T*>(this), jsInvoker) {}
 
 private:
   class Delegate : public CommCoreModuleSchemaCxxSpecJSI {
   public:
     Delegate(T *instance, std::shared_ptr<CallInvoker> jsInvoker) :
       CommCoreModuleSchemaCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {}
 
     jsi::Value getDraft(jsi::Runtime &rt, jsi::String key) override {
       static_assert(
           bridging::getParameterCount(&T::getDraft) == 2,
           "Expected getDraft(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getDraft, jsInvoker_, instance_, std::move(key));
     }
     jsi::Value updateDraft(jsi::Runtime &rt, jsi::String key, jsi::String text) override {
       static_assert(
           bridging::getParameterCount(&T::updateDraft) == 3,
           "Expected updateDraft(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::updateDraft, jsInvoker_, instance_, std::move(key), std::move(text));
     }
     jsi::Value moveDraft(jsi::Runtime &rt, jsi::String oldKey, jsi::String newKey) override {
       static_assert(
           bridging::getParameterCount(&T::moveDraft) == 3,
           "Expected moveDraft(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::moveDraft, jsInvoker_, instance_, std::move(oldKey), std::move(newKey));
     }
     jsi::Value getClientDBStore(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getClientDBStore) == 1,
           "Expected getClientDBStore(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getClientDBStore, jsInvoker_, instance_);
     }
     jsi::Value removeAllDrafts(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::removeAllDrafts) == 1,
           "Expected removeAllDrafts(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::removeAllDrafts, jsInvoker_, instance_);
     }
     jsi::Array getInitialMessagesSync(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getInitialMessagesSync) == 1,
           "Expected getInitialMessagesSync(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Array>(
           rt, &T::getInitialMessagesSync, jsInvoker_, instance_);
     }
     void processMessageStoreOperationsSync(jsi::Runtime &rt, jsi::Array operations) override {
       static_assert(
           bridging::getParameterCount(&T::processMessageStoreOperationsSync) == 2,
           "Expected processMessageStoreOperationsSync(...) to have 2 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::processMessageStoreOperationsSync, jsInvoker_, instance_, std::move(operations));
     }
     jsi::Array getAllThreadsSync(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getAllThreadsSync) == 1,
           "Expected getAllThreadsSync(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Array>(
           rt, &T::getAllThreadsSync, jsInvoker_, instance_);
     }
     void processReportStoreOperationsSync(jsi::Runtime &rt, jsi::Array operations) override {
       static_assert(
           bridging::getParameterCount(&T::processReportStoreOperationsSync) == 2,
           "Expected processReportStoreOperationsSync(...) to have 2 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::processReportStoreOperationsSync, jsInvoker_, instance_, std::move(operations));
     }
     void processThreadStoreOperationsSync(jsi::Runtime &rt, jsi::Array operations) override {
       static_assert(
           bridging::getParameterCount(&T::processThreadStoreOperationsSync) == 2,
           "Expected processThreadStoreOperationsSync(...) to have 2 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::processThreadStoreOperationsSync, jsInvoker_, instance_, std::move(operations));
     }
     jsi::Value processDBStoreOperations(jsi::Runtime &rt, jsi::Object operations) override {
       static_assert(
           bridging::getParameterCount(&T::processDBStoreOperations) == 2,
           "Expected processDBStoreOperations(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::processDBStoreOperations, jsInvoker_, instance_, std::move(operations));
     }
     jsi::Value initializeCryptoAccount(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::initializeCryptoAccount) == 1,
           "Expected initializeCryptoAccount(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::initializeCryptoAccount, jsInvoker_, instance_);
     }
     jsi::Value getUserPublicKey(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getUserPublicKey) == 1,
           "Expected getUserPublicKey(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getUserPublicKey, jsInvoker_, instance_);
     }
     jsi::Value getOneTimeKeys(jsi::Runtime &rt, double oneTimeKeysAmount) override {
       static_assert(
           bridging::getParameterCount(&T::getOneTimeKeys) == 2,
           "Expected getOneTimeKeys(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getOneTimeKeys, jsInvoker_, instance_, std::move(oneTimeKeysAmount));
     }
     jsi::Value validateAndGetPrekeys(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::validateAndGetPrekeys) == 1,
           "Expected validateAndGetPrekeys(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::validateAndGetPrekeys, jsInvoker_, instance_);
     }
     jsi::Value validateAndUploadPrekeys(jsi::Runtime &rt, jsi::String authUserID, jsi::String authDeviceID, jsi::String authAccessToken) override {
       static_assert(
           bridging::getParameterCount(&T::validateAndUploadPrekeys) == 4,
           "Expected validateAndUploadPrekeys(...) to have 4 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::validateAndUploadPrekeys, jsInvoker_, instance_, std::move(authUserID), std::move(authDeviceID), std::move(authAccessToken));
     }
     jsi::Value initializeNotificationsSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional<jsi::String> oneTimeKey, jsi::String keyserverID) override {
       static_assert(
           bridging::getParameterCount(&T::initializeNotificationsSession) == 6,
           "Expected initializeNotificationsSession(...) to have 6 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::initializeNotificationsSession, jsInvoker_, instance_, std::move(identityKeys), std::move(prekey), std::move(prekeySignature), std::move(oneTimeKey), std::move(keyserverID));
     }
     jsi::Value isNotificationsSessionInitialized(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::isNotificationsSessionInitialized) == 1,
           "Expected isNotificationsSessionInitialized(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::isNotificationsSessionInitialized, jsInvoker_, instance_);
     }
     jsi::Value isDeviceNotificationsSessionInitialized(jsi::Runtime &rt, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::isDeviceNotificationsSessionInitialized) == 2,
           "Expected isDeviceNotificationsSessionInitialized(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::isDeviceNotificationsSessionInitialized, jsInvoker_, instance_, std::move(deviceID));
     }
     jsi::Value isNotificationsSessionInitializedWithDevices(jsi::Runtime &rt, jsi::Array deviceIDs) override {
       static_assert(
           bridging::getParameterCount(&T::isNotificationsSessionInitializedWithDevices) == 2,
           "Expected isNotificationsSessionInitializedWithDevices(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::isNotificationsSessionInitializedWithDevices, jsInvoker_, instance_, std::move(deviceIDs));
     }
     jsi::Value updateKeyserverDataInNotifStorage(jsi::Runtime &rt, jsi::Array keyserversData) override {
       static_assert(
           bridging::getParameterCount(&T::updateKeyserverDataInNotifStorage) == 2,
           "Expected updateKeyserverDataInNotifStorage(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::updateKeyserverDataInNotifStorage, jsInvoker_, instance_, std::move(keyserversData));
     }
     jsi::Value removeKeyserverDataFromNotifStorage(jsi::Runtime &rt, jsi::Array keyserverIDsToDelete) override {
       static_assert(
           bridging::getParameterCount(&T::removeKeyserverDataFromNotifStorage) == 2,
           "Expected removeKeyserverDataFromNotifStorage(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::removeKeyserverDataFromNotifStorage, jsInvoker_, instance_, std::move(keyserverIDsToDelete));
     }
     jsi::Value getKeyserverDataFromNotifStorage(jsi::Runtime &rt, jsi::Array keyserverIDs) override {
       static_assert(
           bridging::getParameterCount(&T::getKeyserverDataFromNotifStorage) == 2,
           "Expected getKeyserverDataFromNotifStorage(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getKeyserverDataFromNotifStorage, jsInvoker_, instance_, std::move(keyserverIDs));
     }
+    jsi::Value updateUnreadThickThreadsInNotifsStorage(jsi::Runtime &rt, jsi::Array unreadThickThreadIDs) override {
+      static_assert(
+          bridging::getParameterCount(&T::updateUnreadThickThreadsInNotifsStorage) == 2,
+          "Expected updateUnreadThickThreadsInNotifsStorage(...) to have 2 parameters");
+
+      return bridging::callFromJs<jsi::Value>(
+          rt, &T::updateUnreadThickThreadsInNotifsStorage, jsInvoker_, instance_, std::move(unreadThickThreadIDs));
+    }
+    jsi::Value getUnreadThickThreadIDsFromNotifsStorage(jsi::Runtime &rt) override {
+      static_assert(
+          bridging::getParameterCount(&T::getUnreadThickThreadIDsFromNotifsStorage) == 1,
+          "Expected getUnreadThickThreadIDsFromNotifsStorage(...) to have 1 parameters");
+
+      return bridging::callFromJs<jsi::Value>(
+          rt, &T::getUnreadThickThreadIDsFromNotifsStorage, jsInvoker_, instance_);
+    }
     jsi::Value initializeContentOutboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional<jsi::String> oneTimeKey, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::initializeContentOutboundSession) == 6,
           "Expected initializeContentOutboundSession(...) to have 6 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::initializeContentOutboundSession, jsInvoker_, instance_, std::move(identityKeys), std::move(prekey), std::move(prekeySignature), std::move(oneTimeKey), std::move(deviceID));
     }
     jsi::Value initializeContentInboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::Object encryptedContent, jsi::String deviceID, double sessionVersion, bool overwrite) override {
       static_assert(
           bridging::getParameterCount(&T::initializeContentInboundSession) == 6,
           "Expected initializeContentInboundSession(...) to have 6 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::initializeContentInboundSession, jsInvoker_, instance_, std::move(identityKeys), std::move(encryptedContent), std::move(deviceID), std::move(sessionVersion), std::move(overwrite));
     }
     jsi::Value isContentSessionInitialized(jsi::Runtime &rt, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::isContentSessionInitialized) == 2,
           "Expected isContentSessionInitialized(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::isContentSessionInitialized, jsInvoker_, instance_, std::move(deviceID));
     }
     jsi::Value initializeNotificationsOutboundSession(jsi::Runtime &rt, jsi::String identityKeys, jsi::String prekey, jsi::String prekeySignature, std::optional<jsi::String> oneTimeKey, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::initializeNotificationsOutboundSession) == 6,
           "Expected initializeNotificationsOutboundSession(...) to have 6 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::initializeNotificationsOutboundSession, jsInvoker_, instance_, std::move(identityKeys), std::move(prekey), std::move(prekeySignature), std::move(oneTimeKey), std::move(deviceID));
     }
     jsi::Value encrypt(jsi::Runtime &rt, jsi::String message, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::encrypt) == 3,
           "Expected encrypt(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::encrypt, jsInvoker_, instance_, std::move(message), std::move(deviceID));
     }
     jsi::Value encryptNotification(jsi::Runtime &rt, jsi::String payload, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::encryptNotification) == 3,
           "Expected encryptNotification(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::encryptNotification, jsInvoker_, instance_, std::move(payload), std::move(deviceID));
     }
     jsi::Value encryptAndPersist(jsi::Runtime &rt, jsi::String message, jsi::String deviceID, jsi::String messageID) override {
       static_assert(
           bridging::getParameterCount(&T::encryptAndPersist) == 4,
           "Expected encryptAndPersist(...) to have 4 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::encryptAndPersist, jsInvoker_, instance_, std::move(message), std::move(deviceID), std::move(messageID));
     }
     jsi::Value decrypt(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::decrypt) == 3,
           "Expected decrypt(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::decrypt, jsInvoker_, instance_, std::move(encryptedData), std::move(deviceID));
     }
     jsi::Value decryptAndPersist(jsi::Runtime &rt, jsi::Object encryptedData, jsi::String deviceID, jsi::String userID, jsi::String messageID) override {
       static_assert(
           bridging::getParameterCount(&T::decryptAndPersist) == 5,
           "Expected decryptAndPersist(...) to have 5 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::decryptAndPersist, jsInvoker_, instance_, std::move(encryptedData), std::move(deviceID), std::move(userID), std::move(messageID));
     }
     jsi::Value signMessage(jsi::Runtime &rt, jsi::String message) override {
       static_assert(
           bridging::getParameterCount(&T::signMessage) == 2,
           "Expected signMessage(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::signMessage, jsInvoker_, instance_, std::move(message));
     }
     jsi::Value verifySignature(jsi::Runtime &rt, jsi::String publicKey, jsi::String message, jsi::String signature) override {
       static_assert(
           bridging::getParameterCount(&T::verifySignature) == 4,
           "Expected verifySignature(...) to have 4 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::verifySignature, jsInvoker_, instance_, std::move(publicKey), std::move(message), std::move(signature));
     }
     double getCodeVersion(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getCodeVersion) == 1,
           "Expected getCodeVersion(...) to have 1 parameters");
 
       return bridging::callFromJs<double>(
           rt, &T::getCodeVersion, jsInvoker_, instance_);
     }
     void terminate(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::terminate) == 1,
           "Expected terminate(...) to have 1 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::terminate, jsInvoker_, instance_);
     }
     jsi::Value setNotifyToken(jsi::Runtime &rt, jsi::String token) override {
       static_assert(
           bridging::getParameterCount(&T::setNotifyToken) == 2,
           "Expected setNotifyToken(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::setNotifyToken, jsInvoker_, instance_, std::move(token));
     }
     jsi::Value clearNotifyToken(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::clearNotifyToken) == 1,
           "Expected clearNotifyToken(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::clearNotifyToken, jsInvoker_, instance_);
     }
     jsi::Value stampSQLiteDBUserID(jsi::Runtime &rt, jsi::String userID) override {
       static_assert(
           bridging::getParameterCount(&T::stampSQLiteDBUserID) == 2,
           "Expected stampSQLiteDBUserID(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::stampSQLiteDBUserID, jsInvoker_, instance_, std::move(userID));
     }
     jsi::Value getSQLiteStampedUserID(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getSQLiteStampedUserID) == 1,
           "Expected getSQLiteStampedUserID(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getSQLiteStampedUserID, jsInvoker_, instance_);
     }
     jsi::Value clearSensitiveData(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::clearSensitiveData) == 1,
           "Expected clearSensitiveData(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::clearSensitiveData, jsInvoker_, instance_);
     }
     bool checkIfDatabaseNeedsDeletion(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::checkIfDatabaseNeedsDeletion) == 1,
           "Expected checkIfDatabaseNeedsDeletion(...) to have 1 parameters");
 
       return bridging::callFromJs<bool>(
           rt, &T::checkIfDatabaseNeedsDeletion, jsInvoker_, instance_);
     }
     void reportDBOperationsFailure(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::reportDBOperationsFailure) == 1,
           "Expected reportDBOperationsFailure(...) to have 1 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::reportDBOperationsFailure, jsInvoker_, instance_);
     }
     jsi::Value computeBackupKey(jsi::Runtime &rt, jsi::String password, jsi::String backupID) override {
       static_assert(
           bridging::getParameterCount(&T::computeBackupKey) == 3,
           "Expected computeBackupKey(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::computeBackupKey, jsInvoker_, instance_, std::move(password), std::move(backupID));
     }
     jsi::Value generateRandomString(jsi::Runtime &rt, double size) override {
       static_assert(
           bridging::getParameterCount(&T::generateRandomString) == 2,
           "Expected generateRandomString(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::generateRandomString, jsInvoker_, instance_, std::move(size));
     }
     jsi::Value setCommServicesAuthMetadata(jsi::Runtime &rt, jsi::String userID, jsi::String deviceID, jsi::String accessToken) override {
       static_assert(
           bridging::getParameterCount(&T::setCommServicesAuthMetadata) == 4,
           "Expected setCommServicesAuthMetadata(...) to have 4 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::setCommServicesAuthMetadata, jsInvoker_, instance_, std::move(userID), std::move(deviceID), std::move(accessToken));
     }
     jsi::Value getCommServicesAuthMetadata(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getCommServicesAuthMetadata) == 1,
           "Expected getCommServicesAuthMetadata(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getCommServicesAuthMetadata, jsInvoker_, instance_);
     }
     jsi::Value clearCommServicesAuthMetadata(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::clearCommServicesAuthMetadata) == 1,
           "Expected clearCommServicesAuthMetadata(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::clearCommServicesAuthMetadata, jsInvoker_, instance_);
     }
     jsi::Value setCommServicesAccessToken(jsi::Runtime &rt, jsi::String accessToken) override {
       static_assert(
           bridging::getParameterCount(&T::setCommServicesAccessToken) == 2,
           "Expected setCommServicesAccessToken(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::setCommServicesAccessToken, jsInvoker_, instance_, std::move(accessToken));
     }
     jsi::Value clearCommServicesAccessToken(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::clearCommServicesAccessToken) == 1,
           "Expected clearCommServicesAccessToken(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::clearCommServicesAccessToken, jsInvoker_, instance_);
     }
     void startBackupHandler(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::startBackupHandler) == 1,
           "Expected startBackupHandler(...) to have 1 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::startBackupHandler, jsInvoker_, instance_);
     }
     void stopBackupHandler(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::stopBackupHandler) == 1,
           "Expected stopBackupHandler(...) to have 1 parameters");
 
       return bridging::callFromJs<void>(
           rt, &T::stopBackupHandler, jsInvoker_, instance_);
     }
     jsi::Value createNewBackup(jsi::Runtime &rt, jsi::String backupSecret) override {
       static_assert(
           bridging::getParameterCount(&T::createNewBackup) == 2,
           "Expected createNewBackup(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::createNewBackup, jsInvoker_, instance_, std::move(backupSecret));
     }
     jsi::Value createNewSIWEBackup(jsi::Runtime &rt, jsi::String backupSecret, jsi::String siweBackupMsg) override {
       static_assert(
           bridging::getParameterCount(&T::createNewSIWEBackup) == 3,
           "Expected createNewSIWEBackup(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::createNewSIWEBackup, jsInvoker_, instance_, std::move(backupSecret), std::move(siweBackupMsg));
     }
     jsi::Value restoreBackup(jsi::Runtime &rt, jsi::String backupSecret, jsi::String maxVersion) override {
       static_assert(
           bridging::getParameterCount(&T::restoreBackup) == 3,
           "Expected restoreBackup(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::restoreBackup, jsInvoker_, instance_, std::move(backupSecret), std::move(maxVersion));
     }
     jsi::Value restoreSIWEBackup(jsi::Runtime &rt, jsi::String backupSecret, jsi::String backupID, jsi::String maxVersion) override {
       static_assert(
           bridging::getParameterCount(&T::restoreSIWEBackup) == 4,
           "Expected restoreSIWEBackup(...) to have 4 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::restoreSIWEBackup, jsInvoker_, instance_, std::move(backupSecret), std::move(backupID), std::move(maxVersion));
     }
     jsi::Value restoreBackupData(jsi::Runtime &rt, jsi::String backupID, jsi::String backupDataKey, jsi::String backupLogDataKey, jsi::String maxVersion) override {
       static_assert(
           bridging::getParameterCount(&T::restoreBackupData) == 5,
           "Expected restoreBackupData(...) to have 5 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::restoreBackupData, jsInvoker_, instance_, std::move(backupID), std::move(backupDataKey), std::move(backupLogDataKey), std::move(maxVersion));
     }
     jsi::Value retrieveBackupKeys(jsi::Runtime &rt, jsi::String backupSecret) override {
       static_assert(
           bridging::getParameterCount(&T::retrieveBackupKeys) == 2,
           "Expected retrieveBackupKeys(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::retrieveBackupKeys, jsInvoker_, instance_, std::move(backupSecret));
     }
     jsi::Value retrieveLatestSIWEBackupData(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::retrieveLatestSIWEBackupData) == 1,
           "Expected retrieveLatestSIWEBackupData(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::retrieveLatestSIWEBackupData, jsInvoker_, instance_);
     }
     jsi::Value setSIWEBackupSecrets(jsi::Runtime &rt, jsi::Object siweBackupSecrets) override {
       static_assert(
           bridging::getParameterCount(&T::setSIWEBackupSecrets) == 2,
           "Expected setSIWEBackupSecrets(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::setSIWEBackupSecrets, jsInvoker_, instance_, std::move(siweBackupSecrets));
     }
     jsi::Value getSIWEBackupSecrets(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getSIWEBackupSecrets) == 1,
           "Expected getSIWEBackupSecrets(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getSIWEBackupSecrets, jsInvoker_, instance_);
     }
     jsi::Value getAllInboundP2PMessages(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getAllInboundP2PMessages) == 1,
           "Expected getAllInboundP2PMessages(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getAllInboundP2PMessages, jsInvoker_, instance_);
     }
     jsi::Value removeInboundP2PMessages(jsi::Runtime &rt, jsi::Array ids) override {
       static_assert(
           bridging::getParameterCount(&T::removeInboundP2PMessages) == 2,
           "Expected removeInboundP2PMessages(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::removeInboundP2PMessages, jsInvoker_, instance_, std::move(ids));
     }
     jsi::Value getOutboundP2PMessagesByID(jsi::Runtime &rt, jsi::Array ids) override {
       static_assert(
           bridging::getParameterCount(&T::getOutboundP2PMessagesByID) == 2,
           "Expected getOutboundP2PMessagesByID(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getOutboundP2PMessagesByID, jsInvoker_, instance_, std::move(ids));
     }
     jsi::Value getAllOutboundP2PMessages(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getAllOutboundP2PMessages) == 1,
           "Expected getAllOutboundP2PMessages(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getAllOutboundP2PMessages, jsInvoker_, instance_);
     }
     jsi::Value markOutboundP2PMessageAsSent(jsi::Runtime &rt, jsi::String messageID, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::markOutboundP2PMessageAsSent) == 3,
           "Expected markOutboundP2PMessageAsSent(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::markOutboundP2PMessageAsSent, jsInvoker_, instance_, std::move(messageID), std::move(deviceID));
     }
     jsi::Value removeOutboundP2PMessage(jsi::Runtime &rt, jsi::String messageID, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::removeOutboundP2PMessage) == 3,
           "Expected removeOutboundP2PMessage(...) to have 3 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::removeOutboundP2PMessage, jsInvoker_, instance_, std::move(messageID), std::move(deviceID));
     }
     jsi::Value resetOutboundP2PMessagesForDevice(jsi::Runtime &rt, jsi::String deviceID) override {
       static_assert(
           bridging::getParameterCount(&T::resetOutboundP2PMessagesForDevice) == 2,
           "Expected resetOutboundP2PMessagesForDevice(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::resetOutboundP2PMessagesForDevice, jsInvoker_, instance_, std::move(deviceID));
     }
     jsi::Value getSyncedDatabaseVersion(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::getSyncedDatabaseVersion) == 1,
           "Expected getSyncedDatabaseVersion(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getSyncedDatabaseVersion, jsInvoker_, instance_);
     }
     jsi::Value markPrekeysAsPublished(jsi::Runtime &rt) override {
       static_assert(
           bridging::getParameterCount(&T::markPrekeysAsPublished) == 1,
           "Expected markPrekeysAsPublished(...) to have 1 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::markPrekeysAsPublished, jsInvoker_, instance_);
     }
     jsi::Value getRelatedMessages(jsi::Runtime &rt, jsi::String messageID) override {
       static_assert(
           bridging::getParameterCount(&T::getRelatedMessages) == 2,
           "Expected getRelatedMessages(...) to have 2 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::getRelatedMessages, jsInvoker_, instance_, std::move(messageID));
     }
     jsi::Value searchMessages(jsi::Runtime &rt, jsi::String query, jsi::String threadID, std::optional<jsi::String> timestampCursor, std::optional<jsi::String> messageIDCursor) override {
       static_assert(
           bridging::getParameterCount(&T::searchMessages) == 5,
           "Expected searchMessages(...) to have 5 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::searchMessages, jsInvoker_, instance_, std::move(query), std::move(threadID), std::move(timestampCursor), std::move(messageIDCursor));
     }
     jsi::Value fetchMessages(jsi::Runtime &rt, jsi::String threadID, double limit, double offset) override {
       static_assert(
           bridging::getParameterCount(&T::fetchMessages) == 4,
           "Expected fetchMessages(...) to have 4 parameters");
 
       return bridging::callFromJs<jsi::Value>(
           rt, &T::fetchMessages, jsInvoker_, instance_, std::move(threadID), std::move(limit), std::move(offset));
     }
 
   private:
     T *instance_;
   };
 
   Delegate delegate_;
 };
 
 } // namespace react
 } // namespace facebook
diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm
index 53d8ad147..39a947244 100644
--- a/native/ios/NotificationService/NotificationService.mm
+++ b/native/ios/NotificationService/NotificationService.mm
@@ -1,953 +1,954 @@
 #import "NotificationService.h"
 #import "AESCryptoModuleObjCCompat.h"
 #import "CommIOSServicesClient.h"
 #import "CommMMKV.h"
 #import "Logger.h"
 #import "NotificationsCryptoModule.h"
 #import "StaffUtils.h"
 #import "TemporaryMessageStorage.h"
 #import <mach/mach.h>
 #include <iterator>
 #include <sstream>
 
 NSString *const backgroundNotificationTypeKey = @"backgroundNotifType";
 NSString *const messageInfosKey = @"messageInfos";
 NSString *const threadIDKey = @"threadID";
 NSString *const encryptedPayloadKey = @"encryptedPayload";
 NSString *const encryptionFailedKey = @"encryptionFailed";
 NSString *const collapseIDKey = @"collapseID";
 NSString *const keyserverIDKey = @"keyserverID";
 NSString *const senderDeviceIDKey = @"senderDeviceID";
 NSString *const messageTypeKey = @"type";
 NSString *const blobHashKey = @"blobHash";
 NSString *const blobHolderKey = @"blobHolder";
 NSString *const encryptionKeyLabel = @"encryptionKey";
 NSString *const needsSilentBadgeUpdateKey = @"needsSilentBadgeUpdate";
 NSString *const notificationIdKey = @"notificationId";
 
 // 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 unreadThickThreads = "NOTIFS.UNREAD_THICK_THREADS";
 
 // The context for this constant can be found here:
 // https://linear.app/comm/issue/ENG-3074#comment-bd2f5e28
 int64_t const notificationRemovalDelay = (int64_t)(0.1 * NSEC_PER_SEC);
 // Apple gives us about 30 seconds to process single notification,
 // se we let any semaphore wait for at most 20 seconds
 int64_t const semaphoreAwaitTimeLimit = (int64_t)(20 * NSEC_PER_SEC);
 
 CFStringRef newMessageInfosDarwinNotification =
     CFSTR("app.comm.darwin_new_message_infos");
 
 // Implementation below was inspired by the
 // following discussion with Apple staff member:
 // https://developer.apple.com/forums/thread/105088
 size_t getMemoryUsageInBytes() {
   task_vm_info_data_t vmInfo;
   mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
 
   kern_return_t result =
       task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count);
 
   if (result != KERN_SUCCESS) {
     return -1;
   }
 
   size_t memory_usage = static_cast<size_t>(vmInfo.phys_footprint);
   return memory_usage;
 }
 
 std::string joinStrings(
     const std::string &separator,
     const std::vector<std::string> &array) {
   std::ostringstream joinedStream;
   std::copy(
       array.begin(),
       array.end(),
       std::ostream_iterator<std::string>(joinedStream, separator.c_str()));
   std::string joined = joinedStream.str();
   return joined.empty() ? joined : joined.substr(0, joined.size() - 1);
 }
 
 @interface NotificationService ()
 
 @property(strong) NSMutableDictionary *contentHandlers;
 @property(strong) NSMutableDictionary *contents;
 
 @end
 
 @implementation NotificationService
 
 - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request
                    withContentHandler:
                        (void (^)(UNNotificationContent *_Nonnull))
                            contentHandler {
   // Set-up methods are idempotent
   [NotificationService setUpNSEProcess];
   [self setUpNSEInstance];
 
   NSString *contentHandlerKey = [request.identifier copy];
   UNMutableNotificationContent *content = [request.content mutableCopy];
   [self putContent:content withHandler:contentHandler forKey:contentHandlerKey];
 
   UNNotificationContent *publicUserContent = content;
 
   // Step 1: notification decryption.
   std::unique_ptr<comm::NotificationsCryptoModule::BaseStatefulDecryptResult>
       statefulDecryptResultPtr;
   BOOL decryptionExecuted = NO;
 
   if ([self shouldBeDecrypted:content.userInfo]) {
     std::optional<std::string> notifID;
     NSString *objcNotifID = content.userInfo[@"id"];
     if (objcNotifID) {
       notifID = std::string([objcNotifID UTF8String]);
     }
 
     std::string decryptErrorMessage;
     try {
       @try {
         statefulDecryptResultPtr = [self decryptContentInPlace:content];
         decryptionExecuted = YES;
       } @catch (NSException *e) {
         decryptErrorMessage = "NSE: Received Obj-C exception: " +
             std::string([e.name UTF8String]) +
             " during notification decryption.";
         if (notifID.has_value()) {
           decryptErrorMessage += " Notif ID: " + notifID.value();
         }
       }
     } catch (const std::exception &e) {
       decryptErrorMessage =
           "NSE: Received C++ exception: " + std::string(e.what()) +
           " during notification decryption.";
       if (notifID.has_value()) {
         decryptErrorMessage += " Notif ID: " + notifID.value();
       }
     }
 
     if (decryptErrorMessage.size()) {
       NSString *errorMessage =
           [NSString stringWithUTF8String:decryptErrorMessage.c_str()];
       if (notifID.has_value() &&
           [self isAppShowingNotificationWith:
                     [NSString stringWithCString:notifID.value().c_str()
                                        encoding:NSUTF8StringEncoding]]) {
         errorMessage = [errorMessage
             stringByAppendingString:@" App shows notif with this ID."];
       }
 
       [self callContentHandlerForKey:contentHandlerKey
                       onErrorMessage:errorMessage
                withPublicUserContent:[[UNNotificationContent alloc] init]];
       return;
     }
   }
 
   NSMutableArray *errorMessages = [[NSMutableArray alloc] init];
 
   if (comm::StaffUtils::isStaffRelease() &&
       [self shouldAlertUnencryptedNotification:content.userInfo]) {
     [errorMessages addObject:
                        @"Notification encryption failed on the keyserver. "
                        @"Please investigate!"];
   }
   if ([self shouldAlertUnencryptedNotification:content.userInfo]) {
     comm::Logger::log("NSE: Received erroneously unencrypted notification.");
   }
 
   // Step 2: notification persistence in a temporary storage
   std::string persistErrorMessage;
   try {
     @try {
       [self persistMessagePayload:content.userInfo];
     } @catch (NSException *e) {
       persistErrorMessage =
           "Obj-C exception: " + std::string([e.name UTF8String]) +
           " during notification persistence.";
     }
   } catch (const std::exception &e) {
     persistErrorMessage = "C++ exception: " + std::string(e.what()) +
         " during notification persistence.";
   }
 
   if (persistErrorMessage.size()) {
     [errorMessages
         addObject:[NSString stringWithUTF8String:persistErrorMessage.c_str()]];
   }
 
   // Step 3: Cumulative unread count calculation
   std::string unreadCountCalculationError;
   try {
     @try {
       [self calculateTotalUnreadCountInPlace:content];
     } @catch (NSException *e) {
       unreadCountCalculationError =
           "Obj-C exception: " + std::string([e.name UTF8String]) +
           " during unread count calculation.";
     }
   } catch (const std::exception &e) {
     unreadCountCalculationError = "C++ exception: " + std::string(e.what()) +
         " during unread count calculation.";
   }
 
   if (unreadCountCalculationError.size() &&
       comm::StaffUtils::isStaffRelease()) {
     [errorMessages
         addObject:[NSString stringWithUTF8String:unreadCountCalculationError
                                                      .c_str()]];
   }
 
   // Step 4: (optional) rescind read notifications
 
   // Message payload persistence is a higher priority task, so it has
   // to happen prior to potential notification center clearing.
   if ([self isRescind:content.userInfo]) {
     std::string rescindErrorMessage;
     try {
       @try {
         if (content.userInfo[notificationIdKey]) {
           // thin thread rescind
           [self removeNotificationsWithCondition:^BOOL(
                     UNNotification *_Nonnull notif) {
             return [content.userInfo[notificationIdKey]
                 isEqualToString:notif.request.content.userInfo[@"id"]];
           }];
         } else if (content.userInfo[threadIDKey]) {
           // thick thread rescind
           [self removeNotificationsWithCondition:^BOOL(
                     UNNotification *_Nonnull notif) {
             return [content.userInfo[threadIDKey]
                 isEqualToString:notif.request.content.userInfo[threadIDKey]];
           }];
         }
       } @catch (NSException *e) {
         rescindErrorMessage =
             "Obj-C exception: " + std::string([e.name UTF8String]) +
             " during notification rescind.";
       }
     } catch (const std::exception &e) {
       rescindErrorMessage = "C++ exception: " + std::string(e.what()) +
           " during notification rescind.";
     }
 
     if (rescindErrorMessage.size()) {
       [errorMessages
           addObject:[NSString
                         stringWithUTF8String:persistErrorMessage.c_str()]];
     }
 
     publicUserContent = [[UNNotificationContent alloc] init];
   }
 
   // Step 5: (optional) execute notification coalescing
   if ([self isCollapsible:content.userInfo]) {
     std::string coalescingErrorMessage;
     try {
       @try {
         [self displayLocalNotificationFromContent:content
                                    forCollapseKey:content
                                                       .userInfo[collapseIDKey]];
       } @catch (NSException *e) {
         coalescingErrorMessage =
             "Obj-C exception: " + std::string([e.name UTF8String]) +
             " during notification coalescing.";
       }
     } catch (const std::exception &e) {
       coalescingErrorMessage = "C++ exception: " + std::string(e.what()) +
           " during notification coalescing.";
     }
 
     if (coalescingErrorMessage.size()) {
       [errorMessages
           addObject:[NSString
                         stringWithUTF8String:coalescingErrorMessage.c_str()]];
       // Even if we fail to execute coalescing then public users
       // should still see the original message.
       publicUserContent = content;
     } else {
       publicUserContent = [[UNNotificationContent alloc] init];
     }
   }
 
   // Step 6: (optional) create empty notification that
   // only provides badge count.
 
   // For notifs that only contain badge update the
   // server sets BODY to "ENCRYPTED" for internal
   // builds for debugging purposes. So instead of
   // letting such notif go through, we construct
   // another notif that doesn't have a body.
   if (content.userInfo[needsSilentBadgeUpdateKey]) {
     publicUserContent = [self getBadgeOnlyContentFor:content];
   }
 
   // Step 7: (optional) download notification paylaod
   // from blob service in case it is large notification
   if ([self isLargeNotification:content.userInfo]) {
     std::string processLargeNotificationError;
     try {
       @try {
         [self fetchAndPersistLargeNotifPayload:content];
       } @catch (NSException *e) {
         processLargeNotificationError =
             "Obj-C exception: " + std::string([e.name UTF8String]) +
             " during large notification processing.";
       }
     } catch (const std::exception &e) {
       processLargeNotificationError =
           "C++ exception: " + std::string(e.what()) +
           " during large notification processing.";
     }
 
     if (processLargeNotificationError.size()) {
       [errorMessages
           addObject:[NSString stringWithUTF8String:processLargeNotificationError
                                                        .c_str()]];
     }
   }
 
   // Step 8: notify main app that there is data
   // to transfer to SQLite and redux.
   [self sendNewMessageInfosNotification];
 
   if (NSString *currentMemoryEventMessage =
           [NotificationService getAndSetMemoryEventMessage:nil]) {
     [errorMessages addObject:currentMemoryEventMessage];
   }
 
   if (errorMessages.count) {
     NSString *cumulatedErrorMessage = [@"NSE: Received "
         stringByAppendingString:[errorMessages componentsJoinedByString:@" "]];
     [self callContentHandlerForKey:contentHandlerKey
                     onErrorMessage:cumulatedErrorMessage
              withPublicUserContent:publicUserContent];
     return;
   }
 
   [self callContentHandlerForKey:contentHandlerKey
                      withContent:publicUserContent];
 
   if (decryptionExecuted) {
     comm::NotificationsCryptoModule::flushState(
         std::move(statefulDecryptResultPtr));
   }
 }
 
 - (void)serviceExtensionTimeWillExpire {
   // Called just before the extension will be terminated by the system.
   // Use this as an opportunity to deliver your "best attempt" at modified
   // content, otherwise the original push payload will be used.
   NSMutableArray<void (^)(UNNotificationContent *_Nonnull)> *allHandlers =
       [[NSMutableArray alloc] init];
   NSMutableArray<UNNotificationContent *> *allContents =
       [[NSMutableArray alloc] init];
 
   @synchronized(self.contentHandlers) {
     for (NSString *key in self.contentHandlers) {
       [allHandlers addObject:self.contentHandlers[key]];
       [allContents addObject:self.contents[key]];
     }
 
     [self.contentHandlers removeAllObjects];
     [self.contents removeAllObjects];
   }
 
   for (int i = 0; i < allContents.count; i++) {
     UNNotificationContent *content = allContents[i];
     void (^handler)(UNNotificationContent *_Nonnull) = allHandlers[i];
 
     if ([self isRescind:content.userInfo]) {
       // If we get to this place it means we were unable to
       // remove relevant notification from notification center in
       // in time given to NSE to process notification.
       // It is an extremely unlikely to happen.
       if (!comm::StaffUtils::isStaffRelease()) {
         handler([[UNNotificationContent alloc] init]);
         continue;
       }
 
       NSString *errorMessage =
           @"NSE: Exceeded time limit to rescind a notification.";
       UNNotificationContent *errorContent =
           [self buildContentForError:errorMessage];
       handler(errorContent);
       continue;
     }
 
     if ([self isCollapsible:content.userInfo]) {
       // If we get to this place it means we were unable to
       // execute notification coalescing with local notification
       // mechanism in time given to NSE to process notification.
       if (!comm::StaffUtils::isStaffRelease()) {
         handler(content);
         continue;
       }
 
       NSString *errorMessage =
           @"NSE: Exceeded time limit to collapse a notitication.";
       UNNotificationContent *errorContent =
           [self buildContentForError:errorMessage];
       handler(errorContent);
       continue;
     }
 
     if ([self shouldBeDecrypted:content.userInfo] &&
         !content.userInfo[@"successfullyDecrypted"]) {
       // If we get to this place it means we were unable to
       // decrypt encrypted notification content in time
       // given to NSE to process notification.
       if (!comm::StaffUtils::isStaffRelease()) {
         handler([[UNNotificationContent alloc] init]);
         continue;
       }
 
       NSString *errorMessage =
           @"NSE: Exceeded time limit to decrypt a notification.";
       UNNotificationContent *errorContent =
           [self buildContentForError:errorMessage];
       handler(errorContent);
       continue;
     }
 
     // At this point we know that the content is at least
     // correctly decrypted so we can display it to the user.
     // Another operation, like persistence, had failed.
     if (content.userInfo[needsSilentBadgeUpdateKey]) {
       UNNotificationContent *badgeOnlyContent =
           [self getBadgeOnlyContentFor:content];
       handler(badgeOnlyContent);
       continue;
     }
 
     handler(content);
   }
 }
 
 - (void)removeNotificationsWithCondition:
     (BOOL (^)(UNNotification *_Nonnull))condition {
   dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
 
   void (^delayedSemaphorePostCallback)() = ^() {
     dispatch_time_t timeToPostSemaphore =
         dispatch_time(DISPATCH_TIME_NOW, notificationRemovalDelay);
     dispatch_after(timeToPostSemaphore, dispatch_get_main_queue(), ^{
       dispatch_semaphore_signal(semaphore);
     });
   };
 
   [UNUserNotificationCenter.currentNotificationCenter
       getDeliveredNotificationsWithCompletionHandler:^(
           NSArray<UNNotification *> *_Nonnull notifications) {
         NSMutableArray<NSString *> *notificationsToRemove =
             [[NSMutableArray alloc] init];
         for (UNNotification *notif in notifications) {
           if (condition(notif)) {
             [notificationsToRemove addObject:notif.request.identifier];
           }
         }
         [UNUserNotificationCenter.currentNotificationCenter
             removeDeliveredNotificationsWithIdentifiers:notificationsToRemove];
         delayedSemaphorePostCallback();
       }];
 
   dispatch_semaphore_wait(
       semaphore, dispatch_time(DISPATCH_TIME_NOW, semaphoreAwaitTimeLimit));
 }
 
 - (void)displayLocalNotificationFromContent:(UNNotificationContent *)content
                              forCollapseKey:(NSString *)collapseKey {
   UNMutableNotificationContent *localNotifContent =
       [[UNMutableNotificationContent alloc] init];
 
   localNotifContent.title = content.title;
   localNotifContent.body = content.body;
   localNotifContent.badge = content.badge;
   localNotifContent.userInfo = content.userInfo;
 
   UNNotificationRequest *localNotifRequest =
       [UNNotificationRequest requestWithIdentifier:collapseKey
                                            content:localNotifContent
                                            trigger:nil];
 
   [self displayLocalNotificationFor:localNotifRequest];
 }
 
 - (void)persistMessagePayload:(NSDictionary *)payload {
   if (payload[messageInfosKey]) {
     TemporaryMessageStorage *temporaryStorage =
         [[TemporaryMessageStorage alloc] init];
     [temporaryStorage writeMessage:payload[messageInfosKey]];
     return;
   }
 
   if (![self isRescind:payload]) {
     return;
   }
 
   NSError *jsonError = nil;
   NSData *binarySerializedRescindPayload =
       [NSJSONSerialization dataWithJSONObject:payload
                                       options:0
                                         error:&jsonError];
   if (jsonError) {
     comm::Logger::log(
         "NSE: Failed to serialize rescind payload. Details: " +
         std::string([jsonError.localizedDescription UTF8String]));
     return;
   }
 
   NSString *serializedRescindPayload =
       [[NSString alloc] initWithData:binarySerializedRescindPayload
                             encoding:NSUTF8StringEncoding];
 
   TemporaryMessageStorage *temporaryRescindsStorage =
       [[TemporaryMessageStorage alloc] initForRescinds];
   [temporaryRescindsStorage writeMessage:serializedRescindPayload];
 }
 
 - (BOOL)isRescind:(NSDictionary *)payload {
   return payload[backgroundNotificationTypeKey] &&
       [payload[backgroundNotificationTypeKey] isEqualToString:@"CLEAR"];
 }
 
 - (void)calculateTotalUnreadCountInPlace:
     (UNMutableNotificationContent *)content {
 
   if (content.userInfo[keyserverIDKey] && content.badge) {
     std::string senderKeyserverID =
         std::string([content.userInfo[keyserverIDKey] UTF8String]);
     std::string senderKeyserverUnreadCountKey = joinStrings(
         mmkvKeySeparator,
         {mmkvKeyserverPrefix, senderKeyserverID, mmkvUnreadCountSuffix});
 
     int senderKeyserverUnreadCount = [content.badge intValue];
     comm::CommMMKV::setInt(
         senderKeyserverUnreadCountKey, senderKeyserverUnreadCount);
   }
 
   if (content.userInfo[senderDeviceIDKey] && content.userInfo[threadIDKey] &&
       [self isRescind:content.userInfo]) {
     comm::CommMMKV::removeElementFromStringSet(
-        unreadThickThreads,
+        comm::CommMMKV::notifsStorageUnreadThickThreadsKey,
         std::string([content.userInfo[threadIDKey] UTF8String]));
   } else if (
       content.userInfo[senderDeviceIDKey] && content.userInfo[threadIDKey]) {
     comm::CommMMKV::addElementToStringSet(
-        unreadThickThreads,
+        comm::CommMMKV::notifsStorageUnreadThickThreadsKey,
         std::string([content.userInfo[threadIDKey] UTF8String]));
   }
 
   // calculate unread counts from keyservers
   int totalUnreadCount = 0;
   std::vector<std::string> allKeys = comm::CommMMKV::getAllKeys();
   for (const auto &key : allKeys) {
     if (key.size() <
             mmkvKeyserverPrefix.size() + mmkvUnreadCountSuffix.size() ||
         key.compare(0, mmkvKeyserverPrefix.size(), mmkvKeyserverPrefix) ||
         key.compare(
             key.size() - mmkvUnreadCountSuffix.size(),
             mmkvUnreadCountSuffix.size(),
             mmkvUnreadCountSuffix)) {
       continue;
     }
 
     std::optional<int> unreadCount = comm::CommMMKV::getInt(key, -1);
     if (!unreadCount.has_value()) {
       continue;
     }
     totalUnreadCount += unreadCount.value();
   }
 
   // calculate unread counts from thick threads
-  totalUnreadCount += comm::CommMMKV::getStringSet(unreadThickThreads).size();
+  totalUnreadCount += comm::CommMMKV::getStringSet(
+                          comm::CommMMKV::notifsStorageUnreadThickThreadsKey)
+                          .size();
 
   content.badge = @(totalUnreadCount);
 }
 
 - (void)fetchAndPersistLargeNotifPayload:
     (UNMutableNotificationContent *)content {
   NSString *blobHash = content.userInfo[blobHashKey];
 
   NSData *encryptionKey = [[NSData alloc]
       initWithBase64EncodedString:content.userInfo[encryptionKeyLabel]
                           options:0];
 
   __block NSError *fetchError = nil;
   NSData *largePayloadBinary =
       [CommIOSServicesClient.sharedInstance getBlobSync:blobHash
                                              orSetError:&fetchError];
 
   if (fetchError) {
     comm::Logger::log(
         "Failed to fetch notif payload from blob service. Details: " +
         std::string([fetchError.localizedDescription UTF8String]));
     return;
   }
 
   NSDictionary *largePayload =
       [NotificationService aesDecryptAndParse:largePayloadBinary
                                       withKey:encryptionKey];
   [self persistMessagePayload:largePayload];
   [CommIOSServicesClient.sharedInstance
       storeBlobForDeletionWithHash:blobHash
                          andHolder:content.userInfo[blobHolderKey]];
 }
 
 - (BOOL)isCollapsible:(NSDictionary *)payload {
   return payload[collapseIDKey];
 }
 
 - (BOOL)isLargeNotification:(NSDictionary *)payload {
   return payload[blobHashKey] && payload[encryptionKeyLabel] &&
       payload[blobHolderKey];
 }
 
 - (UNNotificationContent *)getBadgeOnlyContentFor:
     (UNNotificationContent *)content {
   UNMutableNotificationContent *badgeOnlyContent =
       [[UNMutableNotificationContent alloc] init];
   badgeOnlyContent.badge = content.badge;
   return badgeOnlyContent;
 }
 
 - (void)sendNewMessageInfosNotification {
   CFNotificationCenterPostNotification(
       CFNotificationCenterGetDarwinNotifyCenter(),
       newMessageInfosDarwinNotification,
       (__bridge const void *)(self),
       nil,
       TRUE);
 }
 
 - (BOOL)shouldBeDecrypted:(NSDictionary *)payload {
   return payload[encryptedPayloadKey];
 }
 
 - (BOOL)shouldAlertUnencryptedNotification:(NSDictionary *)payload {
   return payload[encryptionFailedKey] &&
       [payload[encryptionFailedKey] isEqualToString:@"1"];
 }
 
 - (std::unique_ptr<comm::NotificationsCryptoModule::BaseStatefulDecryptResult>)
     decryptContentInPlace:(UNMutableNotificationContent *)content {
   std::string encryptedData =
       std::string([content.userInfo[encryptedPayloadKey] UTF8String]);
 
   std::unique_ptr<comm::NotificationsCryptoModule::BaseStatefulDecryptResult>
       decryptResult;
   if (content.userInfo[keyserverIDKey]) {
     std::string senderKeyserverID =
         std::string([content.userInfo[keyserverIDKey] UTF8String]);
     decryptResult = comm::NotificationsCryptoModule::statefulDecrypt(
         senderKeyserverID,
         encryptedData,
         comm::NotificationsCryptoModule::olmEncryptedTypeMessage);
   } else if (
       content.userInfo[senderDeviceIDKey] && content.userInfo[messageTypeKey]) {
     std::string senderDeviceID =
         std::string([content.userInfo[senderDeviceIDKey] UTF8String]);
     size_t messageType = [content.userInfo[messageTypeKey] intValue];
     decryptResult = comm::NotificationsCryptoModule::statefulPeerDecrypt(
         senderDeviceID, encryptedData, messageType);
   } else {
     throw std::runtime_error(
         "Received notification without keyserver ID nor sender device ID.");
   }
 
   NSString *decryptedSerializedPayload =
       [NSString stringWithUTF8String:decryptResult->getDecryptedData().c_str()];
 
   NSDictionary *decryptedPayload = [NSJSONSerialization
       JSONObjectWithData:[decryptedSerializedPayload
                              dataUsingEncoding:NSUTF8StringEncoding]
                  options:0
                    error:nil];
 
   NSMutableDictionary *mutableUserInfo = [content.userInfo mutableCopy];
 
   NSMutableDictionary *mutableAps = nil;
   if (mutableUserInfo[@"aps"]) {
     mutableAps = [mutableUserInfo[@"aps"] mutableCopy];
   }
 
   NSString *body = decryptedPayload[@"merged"];
   if (body) {
     content.body = body;
     if (mutableAps && mutableAps[@"alert"]) {
       mutableAps[@"alert"] = body;
     }
   } else {
     mutableUserInfo[needsSilentBadgeUpdateKey] = @(YES);
   }
 
   NSString *threadID = decryptedPayload[threadIDKey];
   if (threadID) {
     content.threadIdentifier = threadID;
     mutableUserInfo[threadIDKey] = threadID;
     if (mutableAps) {
       mutableAps[@"thread-id"] = threadID;
     }
   }
 
   NSString *badgeStr = decryptedPayload[@"badge"];
   if (badgeStr) {
     NSNumber *badge = @([badgeStr intValue]);
     content.badge = badge;
     if (mutableAps) {
       mutableAps[@"badge"] = badge;
     }
   }
 
   // The rest have been already decrypted and handled.
   static NSArray<NSString *> *handledKeys =
       @[ @"merged", @"badge", threadIDKey ];
 
   for (NSString *payloadKey in decryptedPayload) {
     if ([handledKeys containsObject:payloadKey]) {
       continue;
     }
     mutableUserInfo[payloadKey] = decryptedPayload[payloadKey];
   }
 
   if (mutableAps) {
     mutableUserInfo[@"aps"] = mutableAps;
   }
   [mutableUserInfo removeObjectForKey:encryptedPayloadKey];
   mutableUserInfo[@"successfullyDecrypted"] = @(YES);
   content.userInfo = mutableUserInfo;
 
   return decryptResult;
 }
 
 // Apple documentation for NSE does not explicitly state
 // that single NSE instance will be used by only one thread
 // at a time. Even though UNNotificationServiceExtension API
 // suggests that it could be the case we don't trust it
 // and keep a synchronized collection of handlers and contents.
 // We keep reports of events that strongly suggest there is
 // parallelism in notifications processing. In particular we
 // have see notifications not being decrypted when access
 // to encryption keys had not been correctly implemented.
 // Similar behaviour is adopted by other apps such as Signal,
 // Telegram or Element.
 
 - (void)setUpNSEInstance {
   @synchronized(self) {
     if (self.contentHandlers) {
       return;
     }
     self.contentHandlers = [[NSMutableDictionary alloc] init];
     self.contents = [[NSMutableDictionary alloc] init];
   }
 }
 
 - (void)putContent:(UNNotificationContent *)content
        withHandler:(void (^)(UNNotificationContent *_Nonnull))handler
             forKey:(NSString *)key {
   @synchronized(self.contentHandlers) {
     [self.contentHandlers setObject:handler forKey:key];
     [self.contents setObject:content forKey:key];
   }
 }
 
 - (void)callContentHandlerForKey:(NSString *)key
                      withContent:(UNNotificationContent *)content {
   void (^handler)(UNNotificationContent *_Nonnull);
 
   @synchronized(self.contentHandlers) {
     handler = [self.contentHandlers objectForKey:key];
     [self.contentHandlers removeObjectForKey:key];
     [self.contents removeObjectForKey:key];
   }
 
   if (!handler) {
     return;
   }
   handler(content);
 }
 
 - (UNNotificationContent *)buildContentForError:(NSString *)error {
   UNMutableNotificationContent *content =
       [[UNMutableNotificationContent alloc] init];
   content.body = error;
   return content;
 }
 
 - (void)callContentHandlerForKey:(NSString *)key
                   onErrorMessage:(NSString *)errorMessage
            withPublicUserContent:(UNNotificationContent *)publicUserContent {
   comm::Logger::log(std::string([errorMessage UTF8String]));
 
   if (comm::StaffUtils::isStaffRelease()) {
     NSString *errorNotifId = [@"error_for_" stringByAppendingString:key];
     UNNotificationContent *content = [self buildContentForError:errorMessage];
     UNNotificationRequest *localNotifRequest =
         [UNNotificationRequest requestWithIdentifier:errorNotifId
                                              content:content
                                              trigger:nil];
     [self displayLocalNotificationFor:localNotifRequest];
   }
 
   [self callContentHandlerForKey:key withContent:publicUserContent];
 }
 
 - (void)displayLocalNotificationFor:(UNNotificationRequest *)localNotifRequest {
   // We must wait until local notif display completion
   // handler returns. Context:
   // https://developer.apple.com/forums/thread/108340?answerId=331640022#331640022
 
   dispatch_semaphore_t localNotifDisplaySemaphore =
       dispatch_semaphore_create(0);
 
   __block NSError *localNotifDisplayError = nil;
   [UNUserNotificationCenter.currentNotificationCenter
       addNotificationRequest:localNotifRequest
        withCompletionHandler:^(NSError *_Nullable error) {
          if (error) {
            localNotifDisplayError = error;
          }
          dispatch_semaphore_signal(localNotifDisplaySemaphore);
        }];
 
   dispatch_semaphore_wait(
       localNotifDisplaySemaphore,
       dispatch_time(DISPATCH_TIME_NOW, semaphoreAwaitTimeLimit));
 
   if (localNotifDisplayError) {
     throw std::runtime_error(
         std::string([localNotifDisplayError.localizedDescription UTF8String]));
   }
 }
 
 - (BOOL)isAppShowingNotificationWith:(NSString *)identifier {
   dispatch_semaphore_t getAllDeliveredNotifsSemaphore =
       dispatch_semaphore_create(0);
 
   __block BOOL foundNotification = NO;
   [UNUserNotificationCenter.currentNotificationCenter
       getDeliveredNotificationsWithCompletionHandler:^(
           NSArray<UNNotification *> *_Nonnull notifications) {
         for (UNNotification *notif in notifications) {
           if (notif.request.content.userInfo[@"id"] &&
               [notif.request.content.userInfo[@"id"]
                   isEqualToString:identifier]) {
             foundNotification = YES;
             break;
           }
         }
         dispatch_semaphore_signal(getAllDeliveredNotifsSemaphore);
       }];
 
   dispatch_semaphore_wait(
       getAllDeliveredNotifsSemaphore,
       dispatch_time(DISPATCH_TIME_NOW, semaphoreAwaitTimeLimit));
 
   return foundNotification;
 }
 
 // Monitor memory usage
 + (NSString *)getAndSetMemoryEventMessage:(NSString *)message {
   static NSString *memoryEventMessage = nil;
   static NSLock *memoryEventLock = [[NSLock alloc] init];
 
   @try {
     if (![memoryEventLock tryLock]) {
       return nil;
     }
     NSString *currentMemoryEventMessage =
         memoryEventMessage ? [memoryEventMessage copy] : nil;
     memoryEventMessage = [message copy];
     return currentMemoryEventMessage;
   } @finally {
     [memoryEventLock unlock];
   }
 }
 
 + (dispatch_source_t)registerForMemoryEvents {
   dispatch_source_t memorySource = dispatch_source_create(
       DISPATCH_SOURCE_TYPE_MEMORYPRESSURE,
       0L,
       DISPATCH_MEMORYPRESSURE_CRITICAL,
       dispatch_get_main_queue());
 
   dispatch_block_t eventHandler = ^{
     NSString *criticalMemoryEventMessage = [NSString
         stringWithFormat:
             @"NSE: Received CRITICAL memory event. Memory usage: %ld bytes",
             getMemoryUsageInBytes()];
 
     comm::Logger::log(std::string([criticalMemoryEventMessage UTF8String]));
     if (!comm::StaffUtils::isStaffRelease()) {
       // If it is not a staff release we don't set
       // memoryEventMessage variable since it will
       // not be displayed to the client anyway
       return;
     }
 
     [NotificationService
         getAndSetMemoryEventMessage:criticalMemoryEventMessage];
   };
 
   dispatch_source_set_event_handler(memorySource, eventHandler);
   dispatch_activate(memorySource);
   return memorySource;
 }
 
 // AES Cryptography
 static AESCryptoModuleObjCCompat *_aesCryptoModule = nil;
 
 + (AESCryptoModuleObjCCompat *)processLocalAESCryptoModule {
   return _aesCryptoModule;
 }
 
 + (NSDictionary *)aesDecryptAndParse:(NSData *)sealedData
                              withKey:(NSData *)key {
   NSError *decryptError = nil;
   NSInteger destinationLength =
       [[NotificationService processLocalAESCryptoModule]
           decryptedLength:sealedData];
 
   NSMutableData *destination = [NSMutableData dataWithLength:destinationLength];
   [[NotificationService processLocalAESCryptoModule]
       decryptWithKey:key
           sealedData:sealedData
          destination:destination
            withError:&decryptError];
 
   if (decryptError) {
     comm::Logger::log(
         "NSE: Notification aes decryption failure. Details: " +
         std::string([decryptError.localizedDescription UTF8String]));
     return nil;
   }
 
   NSString *decryptedSerializedPayload =
       [[NSString alloc] initWithData:destination encoding:NSUTF8StringEncoding];
 
   return [NSJSONSerialization
       JSONObjectWithData:[decryptedSerializedPayload
                              dataUsingEncoding:NSUTF8StringEncoding]
                  options:0
                    error:nil];
 }
 
 // Process-local initialization code NSE may use different threads and instances
 // of this class to process notifs, but it usually keeps the same process for
 // extended period of time. Objects that can be initialized once and reused on
 // each notif should be declared in a method below to avoid wasting resources
 
 + (void)setUpNSEProcess {
   static dispatch_source_t memoryEventSource;
   static dispatch_once_t onceToken;
 
   dispatch_once(&onceToken, ^{
     _aesCryptoModule = [[AESCryptoModuleObjCCompat alloc] init];
     memoryEventSource = [NotificationService registerForMemoryEvents];
   });
 }
 
 @end
diff --git a/native/schema/CommCoreModuleSchema.js b/native/schema/CommCoreModuleSchema.js
index 3a4d561a6..3f7a376e0 100644
--- a/native/schema/CommCoreModuleSchema.js
+++ b/native/schema/CommCoreModuleSchema.js
@@ -1,239 +1,245 @@
 // @flow
 
 'use strict';
 
 import { TurboModuleRegistry } from 'react-native';
 import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport.js';
 
 import type { ClientDBMessageStoreOperation } from 'lib/ops/message-store-ops.js';
 import type { ClientDBReportStoreOperation } from 'lib/ops/report-store-ops.js';
 import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js';
 import type {
   OneTimeKeysResult,
   SignedPrekeys,
   ClientPublicKeys,
   EncryptedData,
   OutboundSessionCreationResult,
 } from 'lib/types/crypto-types.js';
 import type { ClientDBMessageInfo } from 'lib/types/message-types.js';
 import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js';
 import type {
   InboundP2PMessage,
   OutboundP2PMessage,
 } from 'lib/types/sqlite-types.js';
 import type {
   ClientDBStore,
   ClientDBStoreOperations,
 } from 'lib/types/store-ops-types';
 import type { ClientDBThreadInfo } from 'lib/types/thread-types.js';
 
 type CommServicesAuthMetadata = {
   +userID?: ?string,
   +deviceID?: ?string,
   +accessToken?: ?string,
 };
 
 interface Spec extends TurboModule {
   +getDraft: (key: string) => Promise<string>;
   +updateDraft: (key: string, text: string) => Promise<boolean>;
   +moveDraft: (oldKey: string, newKey: string) => Promise<boolean>;
   +getClientDBStore: () => Promise<ClientDBStore>;
   +removeAllDrafts: () => Promise<void>;
   +getInitialMessagesSync: () => $ReadOnlyArray<ClientDBMessageInfo>;
   +processMessageStoreOperationsSync: (
     operations: $ReadOnlyArray<ClientDBMessageStoreOperation>,
   ) => void;
   +getAllThreadsSync: () => $ReadOnlyArray<ClientDBThreadInfo>;
   +processReportStoreOperationsSync: (
     operations: $ReadOnlyArray<ClientDBReportStoreOperation>,
   ) => void;
   +processThreadStoreOperationsSync: (
     operations: $ReadOnlyArray<ClientDBThreadStoreOperation>,
   ) => void;
   +processDBStoreOperations: (operations: Object) => Promise<void>;
   +initializeCryptoAccount: () => Promise<string>;
   +getUserPublicKey: () => Promise<ClientPublicKeys>;
   +getOneTimeKeys: (oneTimeKeysAmount: number) => Promise<OneTimeKeysResult>;
   +validateAndGetPrekeys: () => Promise<SignedPrekeys>;
   +validateAndUploadPrekeys: (
     authUserID: string,
     authDeviceID: string,
     authAccessToken: string,
   ) => Promise<void>;
   +initializeNotificationsSession: (
     identityKeys: string,
     prekey: string,
     prekeySignature: string,
     oneTimeKey: ?string,
     keyserverID: string,
   ) => Promise<string>;
   +isNotificationsSessionInitialized: () => Promise<boolean>;
   +isDeviceNotificationsSessionInitialized: (
     deviceID: string,
   ) => Promise<boolean>;
   +isNotificationsSessionInitializedWithDevices: (
     deviceIDs: $ReadOnlyArray<string>,
   ) => Promise<{ +[deviceID: string]: boolean }>;
   +updateKeyserverDataInNotifStorage: (
     keyserversData: $ReadOnlyArray<{ +id: string, +unreadCount: number }>,
   ) => Promise<void>;
   +removeKeyserverDataFromNotifStorage: (
     keyserverIDsToDelete: $ReadOnlyArray<string>,
   ) => Promise<void>;
   +getKeyserverDataFromNotifStorage: (
     keyserverIDs: $ReadOnlyArray<string>,
   ) => Promise<$ReadOnlyArray<{ +id: string, +unreadCount: number }>>;
+  +updateUnreadThickThreadsInNotifsStorage: (
+    unreadThickThreadIDs: $ReadOnlyArray<string>,
+  ) => Promise<void>;
+  +getUnreadThickThreadIDsFromNotifsStorage: () => Promise<
+    $ReadOnlyArray<string>,
+  >;
   +initializeContentOutboundSession: (
     identityKeys: string,
     prekey: string,
     prekeySignature: string,
     oneTimeKey: ?string,
     deviceID: string,
   ) => Promise<OutboundSessionCreationResult>;
   +initializeContentInboundSession: (
     identityKeys: string,
     encryptedContent: Object,
     deviceID: string,
     sessionVersion: number,
     overwrite: boolean,
   ) => Promise<string>;
   +isContentSessionInitialized: (deviceID: string) => Promise<boolean>;
   +initializeNotificationsOutboundSession: (
     identityKeys: string,
     prekey: string,
     prekeySignature: string,
     oneTimeKey: ?string,
     deviceID: string,
   ) => Promise<EncryptedData>;
   +encrypt: (message: string, deviceID: string) => Promise<EncryptedData>;
   +encryptNotification: (
     payload: string,
     deviceID: string,
   ) => Promise<EncryptedData>;
   +encryptAndPersist: (
     message: string,
     deviceID: string,
     messageID: string,
   ) => Promise<EncryptedData>;
   +decrypt: (encryptedData: Object, deviceID: string) => Promise<string>;
   +decryptAndPersist: (
     encryptedData: Object,
     deviceID: string,
     userID: string,
     messageID: string,
   ) => Promise<string>;
   +signMessage: (message: string) => Promise<string>;
   +verifySignature: (
     publicKey: string,
     message: string,
     signature: string,
   ) => Promise<void>;
   +getCodeVersion: () => number;
   +terminate: () => void;
   +setNotifyToken: (token: string) => Promise<void>;
   +clearNotifyToken: () => Promise<void>;
   +stampSQLiteDBUserID: (userID: string) => Promise<void>;
   +getSQLiteStampedUserID: () => Promise<string>;
   +clearSensitiveData: () => Promise<void>;
   +checkIfDatabaseNeedsDeletion: () => boolean;
   +reportDBOperationsFailure: () => void;
   +computeBackupKey: (password: string, backupID: string) => Promise<Object>;
   +generateRandomString: (size: number) => Promise<string>;
   +setCommServicesAuthMetadata: (
     userID: string,
     deviceID: string,
     accessToken: string,
   ) => Promise<void>;
   +getCommServicesAuthMetadata: () => Promise<CommServicesAuthMetadata>;
   +clearCommServicesAuthMetadata: () => Promise<void>;
   +setCommServicesAccessToken: (accessToken: string) => Promise<void>;
   +clearCommServicesAccessToken: () => Promise<void>;
   +startBackupHandler: () => void;
   +stopBackupHandler: () => void;
   +createNewBackup: (backupSecret: string) => Promise<void>;
   +createNewSIWEBackup: (
     backupSecret: string,
     siweBackupMsg: string,
   ) => Promise<void>;
   +restoreBackup: (backupSecret: string, maxVersion: string) => Promise<string>;
   +restoreSIWEBackup: (
     backupSecret: string,
     backupID: string,
     maxVersion: string,
   ) => Promise<string>;
   +restoreBackupData: (
     backupID: string,
     backupDataKey: string,
     backupLogDataKey: string,
     maxVersion: string,
   ) => Promise<void>;
   +retrieveBackupKeys: (backupSecret: string) => Promise<string>;
   +retrieveLatestSIWEBackupData: () => Promise<string>;
   +setSIWEBackupSecrets: (siweBackupSecrets: Object) => Promise<void>;
   +getSIWEBackupSecrets: () => Promise<?Object>;
   +getAllInboundP2PMessages: () => Promise<Array<InboundP2PMessage>>;
   +removeInboundP2PMessages: (ids: $ReadOnlyArray<string>) => Promise<void>;
   +getOutboundP2PMessagesByID: (
     ids: $ReadOnlyArray<string>,
   ) => Promise<Array<OutboundP2PMessage>>;
   +getAllOutboundP2PMessages: () => Promise<Array<OutboundP2PMessage>>;
   +markOutboundP2PMessageAsSent: (
     messageID: string,
     deviceID: string,
   ) => Promise<void>;
   +removeOutboundP2PMessage: (
     messageID: string,
     deviceID: string,
   ) => Promise<void>;
   +resetOutboundP2PMessagesForDevice: (
     deviceID: string,
   ) => Promise<Array<string>>;
   +getSyncedDatabaseVersion: () => Promise<string>;
   +markPrekeysAsPublished: () => Promise<void>;
   +getRelatedMessages: (
     messageID: string,
   ) => Promise<Array<ClientDBMessageInfo>>;
   +searchMessages: (
     query: string,
     threadID: string,
     timestampCursor: ?string,
     messageIDCursor: ?string,
   ) => Promise<Array<ClientDBMessageInfo>>;
   +fetchMessages: (
     threadID: string,
     limit: number,
     offset: number,
   ) => Promise<Array<ClientDBMessageInfo>>;
 }
 
 export interface CoreModuleSpec extends Spec {
   +computeBackupKey: (
     password: string,
     backupID: string,
   ) => Promise<ArrayBuffer>;
   +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise<string>;
   +decryptAndPersist: (
     encryptedData: EncryptedData,
     deviceID: string,
     userID: string,
     messageID: string,
   ) => Promise<string>;
   +initializeContentInboundSession: (
     identityKeys: string,
     encryptedContent: EncryptedData,
     deviceID: string,
     sessionVersion: number,
     overwrite: boolean,
   ) => Promise<string>;
   +setSIWEBackupSecrets: (
     siweBackupSecrets: SIWEBackupSecrets,
   ) => Promise<void>;
   +getSIWEBackupSecrets: () => Promise<?SIWEBackupSecrets>;
   +processDBStoreOperations: (
     operations: ClientDBStoreOperations,
   ) => Promise<void>;
 }
 
 export default (TurboModuleRegistry.getEnforcing<Spec>(
   'CommTurboModule',
 ): Spec);