diff --git a/examples/SampleApp/App.tsx b/examples/SampleApp/App.tsx index 56a64bcaf9..771d8db352 100644 --- a/examples/SampleApp/App.tsx +++ b/examples/SampleApp/App.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { DevSettings, LogBox, Platform, useColorScheme } from 'react-native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native'; -import { createStackNavigator } from '@react-navigation/stack'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { Chat, @@ -92,8 +92,8 @@ notifee.onBackgroundEvent(async ({ detail, type }) => { }); const Drawer = createDrawerNavigator(); -const Stack = createStackNavigator(); -const UserSelectorStack = createStackNavigator(); +const Stack = createNativeStackNavigator(); +const UserSelectorStack = createNativeStackNavigator(); const App = () => { const { chatClient, isConnecting, loginUser, logout, switchUser } = useChatClient(); const [messageListImplementation, setMessageListImplementation] = useState< @@ -163,7 +163,9 @@ const App = () => { messageListImplementationStoredValue?.id as MessageListImplementationConfigItem['id'], ); setMessageListMode(messageListModeStoredValue?.mode as MessageListModeConfigItem['mode']); - setMessageListPruning(messageListPruningStoredValue?.value as MessageListPruningConfigItem['value']); + setMessageListPruning( + messageListPruningStoredValue?.value as MessageListPruningConfigItem['value'], + ); }; getMessageListConfig(); return () => { diff --git a/examples/SampleApp/android/app/src/main/AndroidManifest.xml b/examples/SampleApp/android/app/src/main/AndroidManifest.xml index b8d46ef536..a4d1d53dfc 100644 --- a/examples/SampleApp/android/app/src/main/AndroidManifest.xml +++ b/examples/SampleApp/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + + diff --git a/examples/SampleApp/index.js b/examples/SampleApp/index.js index e48125176c..80c27dd351 100644 --- a/examples/SampleApp/index.js +++ b/examples/SampleApp/index.js @@ -1,3 +1,4 @@ +import './src/utils/bootstrapBackgroundMessageHandler'; import 'react-native-gesture-handler'; import { AppRegistry } from 'react-native'; import { enableScreens } from 'react-native-screens'; diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index d2f38c4db0..171a4084a1 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -155,6 +155,67 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) + - NitroModules (0.31.3): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - NitroSound (0.2.9): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - NitroModules + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - op-sqlite (14.1.4): - boost - DoubleConversion @@ -2632,8 +2693,6 @@ PODS: - React-perflogger (= 0.80.2) - React-utils (= 0.80.2) - SocketRocket - - RNAudioRecorderPlayer (3.6.13): - - React-Core - RNCAsyncStorage (2.2.0): - boost - DoubleConversion @@ -3119,6 +3178,8 @@ DEPENDENCIES: - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - NitroModules (from `../node_modules/react-native-nitro-modules`) + - NitroSound (from `../node_modules/react-native-nitro-sound`) - "op-sqlite (from `../node_modules/@op-engineering/op-sqlite`)" - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -3194,7 +3255,6 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - RNAudioRecorderPlayer (from `../node_modules/react-native-audio-recorder-player`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - RNFastImage (from `../node_modules/react-native-fast-image`) - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" @@ -3252,6 +3312,10 @@ EXTERNAL SOURCES: hermes-engine: :podspec: "../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" :tag: hermes-2025-07-24-RNv0.80.2-5c7dbc0a78cb2d2a8bc81c41c617c3abecf209ff + NitroModules: + :path: "../node_modules/react-native-nitro-modules" + NitroSound: + :path: "../node_modules/react-native-nitro-sound" op-sqlite: :path: "../node_modules/@op-engineering/op-sqlite" RCT-Folly: @@ -3400,8 +3464,6 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" - RNAudioRecorderPlayer: - :path: "../node_modules/react-native-audio-recorder-player" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" RNFastImage: @@ -3455,100 +3517,101 @@ SPEC CHECKSUMS: hermes-engine: bbc1152da7d2d40f9e59c28acc6576fcf5d28e2a libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - op-sqlite: a7e46cfdaebeef219fd0e939332967af9fe6d406 + NitroModules: 8849240f6ee6d3b295514112437e3b09e855cb67 + NitroSound: fe46960c89410e62e05e9a709d8bf28a8202d1b3 + op-sqlite: b9a4028bef60145d7b98fbbc4341c83420cdcfd5 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f + RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: 300c5eb91114d4339b0bb39505d0f4824d7299b7 RCTRequired: e0446b01093475b7082fbeee5d1ef4ad1fe20ac4 RCTTypeSafety: cb974efcdc6695deedf7bf1eb942f2a0603a063f React: e7a4655b09d0e17e54be188cc34c2f3e2087318a React-callinvoker: 62192daaa2f30c3321fc531e4f776f7b09cf892b React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: b23cdaaa9d76389d958c06af3c57aa6ad611c542 - React-CoreModules: 8e0f562e5695991e455abbebe1e968af71d52553 - React-cxxreact: 6ccbe0cc2c652b29409b14b23cfb3cd74e084691 + React-Core: c400b068fdb6172177f3b3fae00c10d1077244d7 + React-CoreModules: 8e911a5a504b45824374eec240a78de7a6db8ca2 + React-cxxreact: 06a91f55ac5f842219d6ca47e0f77187a5b5f4ac React-debug: ab52e07f7148480ea61c5e9b68408d749c6e2b8f - React-defaultsnativemodule: 291d2b0a93c399056121f4f0acc7f46d155a38ec - React-domnativemodule: c4968302e857bd422df8eec50a3cd4d078bd4ac0 - React-Fabric: 7e3ba48433b87a416052c4077d5965aff64cb8c9 - React-FabricComponents: 21de255cf52232644d12d3288cced1f0c519b25d - React-FabricImage: 15a0961a0ab34178f1c803aa0a7d28f21322ffc3 - React-featureflags: 4e5dad365d57e3c3656447dfdad790f75878d9f4 - React-featureflagsnativemodule: 5eac59389131c2b87d165dac4094b5e86067fabb - React-graphics: 2f9b3db89f156afd793da99f23782f400f58c1ee - React-hermes: cc8c77acee1406c258622cd8abbee9049f6b5761 - React-idlecallbacksnativemodule: 1d7e1f73b624926d16db956e87c4885ef485664a - React-ImageManager: 8b6066f6638fba7d4a94fbd0b39b477ea8aced58 - React-jserrorhandler: e5a4626d65b0eda9a11c43a9f14d0423d8a7080d - React-jsi: ea5c640ea63c127080f158dac7f4f393d13d415c - React-jsiexecutor: cf7920f82e46fe9a484c15c9f31e67d7179aa826 - React-jsinspector: 094e3cb99952a0024fa977fa04706e68747cba18 - React-jsinspectorcdp: dca545979146e3ecbdc999c0789dab55beecc140 - React-jsinspectornetwork: 0a105fe74b0b1a93f70409d955c8a0556dc17c59 - React-jsinspectortracing: 76088dd78a2de3ea637a860cdf39a6d9c2637d6b - React-jsitooling: a2e1e87382aae2294bc94a6e9682b9bc83da1d36 - React-jsitracing: 45827be59e673f4c54175c150891301138846906 - React-logger: 7cfc7b1ae1f8e5fe5097f9c746137cc3a8fad4ce - React-Mapbuffer: 8f620d1794c6b59a8c3862c3ae820a2e9e6c9bb0 - React-microtasksnativemodule: dcf5321c9a41659a6718df8a5f202af1577c6825 - react-native-blob-util: a511afccff6511544ebf56928e6afdf837b037a7 - react-native-cameraroll: 8c3ba9b6f511cf645778de19d5039b61d922fdfb - react-native-document-picker: b37cf6660ad9087b782faa78a1e67687fac15bfd - react-native-geolocation: b7f68b8c04e36ee669c630dbc48dd42cf93a0a41 - react-native-image-picker: 9bceb747cd6cde22a3416ffdc819d11b5b11f156 - react-native-maps: 9febd31278b35cd21e4fad2cf6fa708993be5dab - react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 - react-native-safe-area-context: 32293dc61d1b92ccf892499ab6f8acfd609f9aef - react-native-video: 4da16bfca01a02aa2095e40683d74f2d6563207c - React-NativeModulesApple: 342e280bb9fc2aa5f61f6c257b309a86b995e12d + React-defaultsnativemodule: fab7bf1b5ce1f3ed252aa4e949ec48a8df67d624 + React-domnativemodule: 735fa5238cceebceeecc18f9f4321016461178cf + React-Fabric: c75719fc8818049c3cf9071f0619af988b020c9f + React-FabricComponents: 74a381cc0dfaf2ec3ee29f39ef8533a7fd864b83 + React-FabricImage: 9a3ff143b1ac42e077c0b33ec790f3674ace5783 + React-featureflags: e1eca0727563a61c919131d57bbd0019c3bdb0f0 + React-featureflagsnativemodule: 692211fd48283b2ddee3817767021010e2f1788e + React-graphics: 759b02bde621f12426c1dc1ae2498aaa6b441cd7 + React-hermes: b6e33fcd21aa7523dc76e62acd7a547e68c28a5b + React-idlecallbacksnativemodule: 731552efc0815fc9d65a6931da55e722b1b3a397 + React-ImageManager: 2c510a480f2c358f56a82df823c66d5700949c96 + React-jserrorhandler: 411e18cbdcbdf546f8f8707091faeb00703527c1 + React-jsi: 3fde19aaf675c0607a0824c4d6002a4943820fd9 + React-jsiexecutor: 4f898228240cf261a02568e985dfa7e1d7ad1dfb + React-jsinspector: 2f0751e6a4fb840f4ed325384d0795a9e9afaf39 + React-jsinspectorcdp: 71c48944d97f5f20e8e144e419ddf04ffa931e93 + React-jsinspectornetwork: 102f347669b278644cc9bb4ebf2f90422bd5ccef + React-jsinspectortracing: 0f6f2ec7f3faa9dc73d591b24b460141612515eb + React-jsitooling: b557f8e12efdaf16997e43b0d07dbd8a3fce3a5b + React-jsitracing: f9a77561d99c0cd053a8230bab4829b100903949 + React-logger: ea80169d826e0cd112fa4d68f58b2b3b968f1ecb + React-Mapbuffer: 230c34b1cabd1c4815726c711b9df221c3d3fbfb + React-microtasksnativemodule: 29d62f132e4aba34ebb7f2b936dde754eb08971b + react-native-blob-util: cbd6b292d0f558f09dce85e6afe68074cd031f3e + react-native-cameraroll: 00057cc0ec595fdbdf282ecfb931d484b240565f + react-native-document-picker: c5fa18e9fc47b34cfbab3b0a4447d0df918a5621 + react-native-geolocation: eb39c815c9b58ddc3efb552cafdd4b035e4cf682 + react-native-image-picker: e479ec8884df9d99a62c1f53f2307055ad43ea85 + react-native-maps: ee1e65647460c3d41e778071be5eda10e3da6225 + react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac + react-native-safe-area-context: 7fd4c2c8023da8e18eaa3424cb49d52f626debee + react-native-video: 71973843c2c9ac154c54f95a5a408fd8b041790e + React-NativeModulesApple: d061f458c3febdf0ac99b1b0faf23b7305974b25 React-oscompat: 56d6de59f9ae95cd006a1c40be2cde83bc06a4e1 - React-perflogger: 4008bd05a8b6c157b06608c0ea0b8bd5d9c5e6c9 - React-performancetimeline: 3ac316a346fe3d48801a746b754dd8f5b5146838 + React-perflogger: 0633844e495d8b34798c9bf0cb32ce315f1d5c9f + React-performancetimeline: 53bdf62ff49a9b0c4bd4d66329fdcf28d77c1c9d React-RCTActionSheet: 49138012280ec3bbb35193d8d09adb8bc61c982e - React-RCTAnimation: ebfe7c62016d4c17b56b2cab3a221908ae46288d - React-RCTAppDelegate: 0108657ba9a19f6a1cd62dcd19c2c0485b3fc251 - React-RCTBlob: 6cc309d1623f3c2679125a04a7425685b7219e6b - React-RCTFabric: 04d1cf11ee3747a699260492e319e92649d7ac88 - React-RCTFBReactNativeSpec: ff3e37e2456afc04211334e86d07bf20488df0ae - React-RCTImage: bb98a59aeed953a48be3f917b9b745b213b340ab - React-RCTLinking: d6e9795d4d75d154c1dd821fd0746cc3e05d6670 - React-RCTNetwork: 5c8a7a2dd26728323189362f149e788548ac72bc - React-RCTRuntime: 52b28e281aba881e1f94ee8b16611823b730d1c5 - React-RCTSettings: b6a02d545ce10dd936b39914b32674db6e865307 - React-RCTText: c7d9232da0e9b5082a99a617483d9164a9cd46e9 - React-RCTVibration: fe636c985c1bf25e4a5b5b4d9315a3b882468a72 + React-RCTAnimation: c7ed4a9d5a4e43c9b10f68bb43cd238c4a2e7e89 + React-RCTAppDelegate: ea2ab6f4aef1489f72025b7128d8ab645b40eafb + React-RCTBlob: c052799460b245e1fffe3d1dddea36fa88e998a0 + React-RCTFabric: fad6230640c42fb8cda29b1d0759f7a1fb8cc677 + React-RCTFBReactNativeSpec: ffb22c3ee3d359ae9245ca94af203845da9371ec + React-RCTImage: 59fc2571f4f109a77139924f5babee8f9cd639c9 + React-RCTLinking: a045cb58c08188dce6c6f4621de105114b1b16ce + React-RCTNetwork: fc7115a2f5e15ae0aa05e9a9be726817feefb482 + React-RCTRuntime: c69b86dc60dcc7297318097fc60bd8e40b050f74 + React-RCTSettings: 30d7dd7eae66290467a1e72bf42d927fa78c3884 + React-RCTText: 755d59284e66c7d33bb4f0ccc428fe69110c3e74 + React-RCTVibration: ffe019e588815df226f6f8ccdc65979f8b2bc440 React-rendererconsistency: aba18fa58a4d037004f6bed6bb201eb368016c56 - React-renderercss: b490bd53486a6bae1e9809619735d1f2b2cabd7f - React-rendererdebug: 8db25b276b64d5a1dbf05677de0c4ff1039d5184 + React-renderercss: c7c140782f5f21103b638abfde7b3f11d6a5fd7e + React-rendererdebug: 111519052db9610f1b93baf7350c800621df3d0c React-rncore: 22f344c7f9109b68c3872194b0b5081ca1aee655 - React-RuntimeApple: 19bdabedda0eeb70acb71e68bfc42d61bbcbacd9 - React-RuntimeCore: 11bf03bdbd6e72857481c46d0f4eb9c19b14c754 + React-RuntimeApple: 30d20d804a216eb297ccc9ce1dc9e931738c03b1 + React-RuntimeCore: 6e1facd50e0b7aed1bc36b090015f33133958bb6 React-runtimeexecutor: b35de9cb7f5d19c66ea9b067235f95b947697ba5 - React-RuntimeHermes: d8f736d0a2d38233c7ec7bd36040eb9b0a3ccd8c - React-runtimescheduler: 0c95966d030c8ebbebddaab49630cda2889ca073 + React-RuntimeHermes: 222268a5931a23f095565c4d60e2673c04e2178a + React-runtimescheduler: aea93219348ba3069fe6c7685a84fe17d3a4b4ee React-timing: 42e8212c479d1e956d3024be0a07f205a2e34d9d - React-utils: a185f723baa0c525c361e6c281a846d919623dbe - ReactAppDependencyProvider: 8df342c127fd0c1e30e8b9f71ff814c22414a7c0 - ReactCodegen: 4928682e20747464165effacc170019a18da953c - ReactCommon: ec1cdf708729338070f8c4ad746768a782fd9eb1 - RNAudioRecorderPlayer: 5d5aac7a0e0f159861736ef2b433770342da7197 - RNCAsyncStorage: f30b3a83064e28b0fc46f1fbd3834589ed64c7b9 - RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87 - RNFBApp: db9c2e6d36fe579ab19b82c0a4a417ff7569db7e - RNFBMessaging: de62448d205095171915d622ed5fb45c2be5e075 - RNGestureHandler: b2fccd493292b4904794460fa80d76a8f29df961 - RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 - RNReactNativeHapticFeedback: d39b9a5b334ce26f49ca6abe9eea8b3938532aee - RNReanimated: be0bc51a01858c195f6df763ec2334b8bfe6f408 - RNScreens: 6a2d1ff4d263d29d3d3db9f3c19aad2f99fdd162 - RNShare: 9d801eafd9ae835f51bcae6b5c8de9bf3389075b - RNSVG: bc7ccfe884848ac924d2279d9025d41b5f05cb0c - RNWorklets: 18d2a9a10588e4d51f42116f19e650d296ab8dbc + React-utils: 0ebf25dd4eb1b5497933f4d8923b862d0fe9566f + ReactAppDependencyProvider: 6c9197c1f6643633012ab646d2bfedd1b0d25989 + ReactCodegen: f8ae44cfcb65af88f409f4b094b879591232f12c + ReactCommon: a237545f08f598b8b37dc3a9163c1c4b85817b0b + RNCAsyncStorage: afe7c3711dc256e492aa3a50dcac43eecebd0ae5 + RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 + RNFBApp: df5caad9f64b6bc87f8a0b110e6bc411fb00a12b + RNFBMessaging: 6586f18ab3411aeb3349088c19fe54283d39e529 + RNGestureHandler: 4d36eb583264375d9f7ece09a2efd918ebc85605 + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 + RNReactNativeHapticFeedback: 8bd4a2ba7c3daeb5d2acfceb8b61743f203076d0 + RNReanimated: 408767d090bcbfe3877cfbcc9dc9d29f5e878203 + RNScreens: 5ca475eb30f4362c5808f3ff4a1c7b786bcd878e + RNShare: 083f05646b9534305999cf20452bd87ca0e8b0b0 + RNSVG: fd433fe5da0f7fee8c78f5865f29ab37321dbd7f + RNWorklets: 7d34d4c80edec50bb1eec6bd034e7686db26da8e SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: f42e234640869e0eafcdd354441414ad1818b9fe + stream-chat-react-native: 7a042480d22a8a87aaee6186bf2f1013af017d3a Yoga: ce248fb32065c9b00451491b06607f1c25b2f1ed PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index b0903a998d..3f71a2523a 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -36,19 +36,20 @@ "@react-navigation/bottom-tabs": "7.3.14", "@react-navigation/drawer": "7.4.1", "@react-navigation/native": "^7.1.10", - "@react-navigation/stack": "^7.3.3", + "@react-navigation/native-stack": "^7.6.2", "@shopify/flash-list": "^2.1.0", "emoji-mart": "^5.6.0", "lodash.mergewith": "^4.6.2", "react": "19.1.0", "react-native": "^0.80.2", - "react-native-audio-recorder-player": "^3.6.13", "react-native-blob-util": "^0.22.2", "react-native-fast-image": "^8.6.3", "react-native-gesture-handler": "^2.26.0", "react-native-haptic-feedback": "^2.3.3", "react-native-image-picker": "^8.2.1", "react-native-maps": "1.20.1", + "react-native-nitro-modules": "^0.31.3", + "react-native-nitro-sound": "^0.2.9", "react-native-reanimated": "^4.0.1", "react-native-safe-area-context": "^5.4.1", "react-native-screens": "^4.11.1", diff --git a/examples/SampleApp/src/components/ChannelPreview.tsx b/examples/SampleApp/src/components/ChannelPreview.tsx index a14ea3e49b..2cfb300d47 100644 --- a/examples/SampleApp/src/components/ChannelPreview.tsx +++ b/examples/SampleApp/src/components/ChannelPreview.tsx @@ -20,7 +20,7 @@ import { useAppOverlayContext } from '../context/AppOverlayContext'; import { useBottomSheetOverlayContext } from '../context/BottomSheetOverlayContext'; import { useChannelInfoOverlayContext } from '../context/ChannelInfoOverlayContext'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { StackNavigatorParamList } from '../types'; import { ChannelState } from 'stream-chat'; @@ -49,7 +49,7 @@ const styles = StyleSheet.create({ }, }); -type ChannelListScreenNavigationProp = StackNavigationProp< +type ChannelListScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'ChannelListScreen' >; diff --git a/examples/SampleApp/src/components/ChatScreenHeader.tsx b/examples/SampleApp/src/components/ChatScreenHeader.tsx index 1f1ce4807c..902de89b62 100644 --- a/examples/SampleApp/src/components/ChatScreenHeader.tsx +++ b/examples/SampleApp/src/components/ChatScreenHeader.tsx @@ -10,7 +10,7 @@ import { useAppContext } from '../context/AppContext'; import { NewDirectMessageIcon } from '../icons/NewDirectMessageIcon'; import type { DrawerNavigationProp } from '@react-navigation/drawer'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { DrawerNavigatorParamList, StackNavigatorParamList } from '../types'; import { NetworkDownIndicator } from './NetworkDownIndicator'; @@ -25,7 +25,7 @@ const styles = StyleSheet.create({ type ChatScreenHeaderNavigationProp = CompositeNavigationProp< DrawerNavigationProp, - StackNavigationProp + NativeStackNavigationProp >; export const ChatScreenHeader: React.FC<{ title?: string }> = ({ title = 'Stream Chat' }) => { diff --git a/examples/SampleApp/src/components/MenuDrawer.tsx b/examples/SampleApp/src/components/MenuDrawer.tsx index 7b7f7030af..ab27232a1a 100644 --- a/examples/SampleApp/src/components/MenuDrawer.tsx +++ b/examples/SampleApp/src/components/MenuDrawer.tsx @@ -82,7 +82,7 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => { return ( - setSecretMenuPressCounter(c => c + 1)} style={[styles.userRow]}> + setSecretMenuPressCounter((c) => c + 1)} style={[styles.userRow]}> { - + navigation.navigate('NewDirectMessagingScreen')} + onPress={() => + navigation.navigate('HomeScreen', { + screen: 'NewDirectMessagingScreen', + }) + } style={styles.menuItem} > @@ -120,7 +128,11 @@ export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => { navigation.navigate('NewGroupChannelAddMemberScreen')} + onPress={() => + navigation.navigate('HomeScreen', { + screen: 'NewGroupChannelAddMemberScreen', + }) + } style={styles.menuItem} > diff --git a/examples/SampleApp/src/components/ScreenHeader.tsx b/examples/SampleApp/src/components/ScreenHeader.tsx index aa17025d0a..a36e48a285 100644 --- a/examples/SampleApp/src/components/ScreenHeader.tsx +++ b/examples/SampleApp/src/components/ScreenHeader.tsx @@ -9,7 +9,7 @@ import { ChannelsUnreadCountBadge } from './UnreadCountBadge'; import { GoBack } from '../icons/GoBack'; import type { DrawerNavigationProp } from '@react-navigation/drawer'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { DrawerNavigatorParamList, StackNavigatorParamList } from '../types'; @@ -52,7 +52,7 @@ const styles = StyleSheet.create({ type ScreenHeaderNavigationProp = CompositeNavigationProp< DrawerNavigationProp, - StackNavigationProp + NativeStackNavigationProp >; export const BackButton: React.FC<{ diff --git a/examples/SampleApp/src/context/ChannelInfoOverlayContext.tsx b/examples/SampleApp/src/context/ChannelInfoOverlayContext.tsx index c089af0978..3327b79977 100644 --- a/examples/SampleApp/src/context/ChannelInfoOverlayContext.tsx +++ b/examples/SampleApp/src/context/ChannelInfoOverlayContext.tsx @@ -1,12 +1,12 @@ import React, { useContext, useState } from 'react'; import { ChannelState } from 'stream-chat'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { ChannelContextValue } from 'stream-chat-react-native'; import type { StackNavigatorParamList } from '../types'; -export type ChannelListScreenNavigationProp = StackNavigationProp< +export type ChannelListScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'ChannelListScreen' >; diff --git a/examples/SampleApp/src/context/UserInfoOverlayContext.tsx b/examples/SampleApp/src/context/UserInfoOverlayContext.tsx index da5ebdb4a5..6c9edb3eb2 100644 --- a/examples/SampleApp/src/context/UserInfoOverlayContext.tsx +++ b/examples/SampleApp/src/context/UserInfoOverlayContext.tsx @@ -1,19 +1,17 @@ import React, { useContext, useState } from 'react'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { ChannelState } from 'stream-chat'; import type { ChannelContextValue } from 'stream-chat-react-native'; import type { StackNavigatorParamList } from '../types'; -type GroupChannelDetailsScreenNavigationProp = StackNavigationProp< +type GroupChannelDetailsScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'GroupChannelDetailsScreen' >; -export type UserInfoOverlayData = Partial< - Pick -> & { +export type UserInfoOverlayData = Partial> & { member?: ChannelState['members'][0]; navigation?: GroupChannelDetailsScreenNavigationProp; }; diff --git a/examples/SampleApp/src/hooks/useChatClient.ts b/examples/SampleApp/src/hooks/useChatClient.ts index 7e11e8235a..3db2874e1b 100644 --- a/examples/SampleApp/src/hooks/useChatClient.ts +++ b/examples/SampleApp/src/hooks/useChatClient.ts @@ -1,6 +1,10 @@ import { useEffect, useRef, useState } from 'react'; import { StreamChat, PushProvider } from 'stream-chat'; -import { getMessaging, AuthorizationStatus } from '@react-native-firebase/messaging'; +import { + getMessaging, + AuthorizationStatus, + FirebaseMessagingTypes, +} from '@react-native-firebase/messaging'; import notifee from '@notifee/react-native'; import { SqliteClient } from 'stream-chat-react-native'; import { USER_TOKENS, USERS } from '../ChatUsers'; @@ -11,6 +15,30 @@ import { PermissionsAndroid, Platform } from 'react-native'; const messaging = getMessaging(); +const displayNotification = async ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, + channelId: string, +) => { + const { stream, ...rest } = remoteMessage.data ?? {}; + const data = { + ...rest, + ...((stream as unknown as Record | undefined) ?? {}), // extract and merge stream object if present + }; + if (data.body && data.title) { + await notifee.displayNotification({ + android: { + channelId, + pressAction: { + id: 'default', + }, + }, + body: data.body as string, + title: data.title as string, + data, + }); + } +}; + // Request Push Notification permission from device. const requestNotificationPermission = async () => { const authStatus = await messaging.requestPermission(); @@ -94,34 +122,12 @@ export const useChatClient = () => { }); // show notifications when on foreground const unsubscribeForegroundMessageReceive = messaging.onMessage(async (remoteMessage) => { - const { stream, ...rest } = remoteMessage.data ?? {}; - const data = { - ...rest, - ...((stream as unknown as Record | undefined) ?? {}), // extract and merge stream object if present - }; const channelId = await notifee.createChannel({ id: 'foreground', name: 'Foreground Messages', }); - // create the android channel to send the notification to - // display the notification on foreground - const notification = remoteMessage.notification ?? {}; - const body = (data.body ?? notification.body) as string; - const title = (data.title ?? notification.title) as string; - - if (body && title) { - await notifee.displayNotification({ - android: { - channelId, - pressAction: { - id: 'default', - }, - }, - body, - title, - data, - }); - } + + await displayNotification(remoteMessage, channelId); }); unsubscribePushListenersRef.current = () => { diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx index 1b059bf3f8..fbd35467ce 100644 --- a/examples/SampleApp/src/screens/ChannelScreen.tsx +++ b/examples/SampleApp/src/screens/ChannelScreen.tsx @@ -18,7 +18,7 @@ import { MessageActionsParams, } from 'stream-chat-react-native'; import { Platform, Pressable, StyleSheet, View } from 'react-native'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useAppContext } from '../context/AppContext'; @@ -34,7 +34,7 @@ import { MessageLocation } from '../components/LocationSharing/MessageLocation.t import { useStreamChatContext } from '../context/StreamChatContext.tsx'; import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx'; -export type ChannelScreenNavigationProp = StackNavigationProp< +export type ChannelScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'ChannelScreen' >; diff --git a/examples/SampleApp/src/screens/ChatScreen.tsx b/examples/SampleApp/src/screens/ChatScreen.tsx index 938cf70193..2bf7288d1b 100644 --- a/examples/SampleApp/src/screens/ChatScreen.tsx +++ b/examples/SampleApp/src/screens/ChatScreen.tsx @@ -8,7 +8,7 @@ import { MentionsScreen } from './MentionsScreen'; import { BottomTabs } from '../components/BottomTabs'; import type { RouteProp } from '@react-navigation/native'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { BottomTabNavigatorParamList, StackNavigatorParamList } from '../types'; import { DraftsScreen } from './DraftScreen'; @@ -16,7 +16,10 @@ import { RemindersScreen } from './RemindersScreen'; const Tab = createBottomTabNavigator(); -type ChatScreenNavigationProp = StackNavigationProp; +type ChatScreenNavigationProp = NativeStackNavigationProp< + StackNavigatorParamList, + 'MessagingScreen' +>; type ChatScreenRouteProp = RouteProp; type Props = { diff --git a/examples/SampleApp/src/screens/DraftScreen.tsx b/examples/SampleApp/src/screens/DraftScreen.tsx index 82b69dcde7..4f8d70ed54 100644 --- a/examples/SampleApp/src/screens/DraftScreen.tsx +++ b/examples/SampleApp/src/screens/DraftScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StackNavigationProp } from '@react-navigation/stack'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { BottomTabNavigatorParamList } from '../types'; import { StyleSheet, View } from 'react-native'; import { useTheme } from 'stream-chat-react-native'; @@ -7,7 +7,7 @@ import { ChatScreenHeader } from '../components/ChatScreenHeader'; import { DraftsList } from '../components/DraftsList'; export type DraftsScreenProps = { - navigation: StackNavigationProp; + navigation: NativeStackNavigationProp; }; export const DraftsScreen: React.FC = () => { diff --git a/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx b/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx index 2bddc63cb2..0746bd158d 100644 --- a/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx +++ b/examples/SampleApp/src/screens/GroupChannelDetailsScreen.tsx @@ -35,7 +35,7 @@ import { Picture } from '../icons/Picture'; import { RemoveUser } from '../icons/RemoveUser'; import { getUserActivityStatus } from '../utils/getUserActivityStatus'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { Channel, UserResponse } from 'stream-chat'; import type { StackNavigatorParamList } from '../types'; @@ -127,7 +127,7 @@ type GroupChannelDetailsProps = { route: GroupChannelDetailsRouteProp; }; -type GroupChannelDetailsScreenNavigationProp = StackNavigationProp< +type GroupChannelDetailsScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'GroupChannelDetailsScreen' >; diff --git a/examples/SampleApp/src/screens/MapScreen.tsx b/examples/SampleApp/src/screens/MapScreen.tsx index 062bacce09..d05c942d1d 100644 --- a/examples/SampleApp/src/screens/MapScreen.tsx +++ b/examples/SampleApp/src/screens/MapScreen.tsx @@ -1,4 +1,4 @@ -import { StackNavigationProp } from '@react-navigation/stack'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { StackNavigatorParamList } from '../types'; import { RouteProp } from '@react-navigation/native'; import { @@ -17,7 +17,10 @@ import MapView, { MapMarker, Marker } from 'react-native-maps'; import { SharedLocationResponse, StreamChat } from 'stream-chat'; import { useStreamChatContext } from '../context/StreamChatContext'; -export type MapScreenNavigationProp = StackNavigationProp; +export type MapScreenNavigationProp = NativeStackNavigationProp< + StackNavigatorParamList, + 'MapScreen' +>; export type MapScreenRouteProp = RouteProp; export type MapScreenProps = { navigation: MapScreenNavigationProp; diff --git a/examples/SampleApp/src/screens/MentionsScreen.tsx b/examples/SampleApp/src/screens/MentionsScreen.tsx index 11c01155bd..4071463037 100644 --- a/examples/SampleApp/src/screens/MentionsScreen.tsx +++ b/examples/SampleApp/src/screens/MentionsScreen.tsx @@ -8,7 +8,7 @@ import { MessageSearchList } from '../components/MessageSearch/MessageSearchList import { usePaginatedSearchedMessages } from '../hooks/usePaginatedSearchedMessages'; import { useScrollToTop } from '@react-navigation/native'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { BottomTabNavigatorParamList } from '../types'; import { useAppContext } from '../context/AppContext'; @@ -44,7 +44,7 @@ const EmptyMentionsSearchIndicator = () => { }; export type MentionsScreenProps = { - navigation: StackNavigationProp; + navigation: NativeStackNavigationProp; }; export const MentionsScreen: React.FC = () => { diff --git a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx index 228129e300..7ecb9e9992 100644 --- a/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx +++ b/examples/SampleApp/src/screens/NewDirectMessagingScreen.tsx @@ -18,7 +18,7 @@ import { UserSearchResults } from '../components/UserSearch/UserSearchResults'; import { useAppContext } from '../context/AppContext'; import { useUserSearchContext } from '../context/UserSearchContext'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { Channel as StreamChatChannel } from 'stream-chat'; import { NewDirectMessagingSendButton } from '../components/NewDirectMessagingSendButton'; @@ -104,7 +104,7 @@ const EmptyMessagesIndicator = () => { ); }; -export type NewDirectMessagingScreenNavigationProp = StackNavigationProp< +export type NewDirectMessagingScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'NewDirectMessagingScreen' >; diff --git a/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx b/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx index 7e3e54b868..eb963ed8ce 100644 --- a/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx +++ b/examples/SampleApp/src/screens/NewGroupChannelAddMemberScreen.tsx @@ -8,7 +8,7 @@ import { UserSearchResults } from '../components/UserSearch/UserSearchResults'; import { useAppContext } from '../context/AppContext'; import { useUserSearchContext } from '../context/UserSearchContext'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { StackNavigatorParamList } from '../types'; @@ -63,7 +63,7 @@ const RightArrowButton: React.FC = (props) => { ); }; -export type NewGroupChannelAddMemberScreenNavigationProp = StackNavigationProp< +export type NewGroupChannelAddMemberScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'NewGroupChannelAddMemberScreen' >; diff --git a/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx b/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx index 8aeaee3541..6a3a039338 100644 --- a/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx +++ b/examples/SampleApp/src/screens/NewGroupChannelAssignNameScreen.tsx @@ -9,7 +9,7 @@ import { UserSearchResults } from '../components/UserSearch/UserSearchResults'; import { useAppContext } from '../context/AppContext'; import { useUserSearchContext } from '../context/UserSearchContext'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { StackNavigatorParamList } from '../types'; @@ -69,7 +69,7 @@ const ConfirmButton: React.FC = (props) => { ); }; -type NewGroupChannelAssignNameScreenNavigationProp = StackNavigationProp< +type NewGroupChannelAssignNameScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'NewGroupChannelAssignNameScreen' >; diff --git a/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx b/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx index 367a1bc08f..2ac1be0d2a 100644 --- a/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx +++ b/examples/SampleApp/src/screens/OneOnOneChannelDetailScreen.tsx @@ -25,7 +25,7 @@ import { Pin } from '../icons/Pin'; import { getUserActivityStatus } from '../utils/getUserActivityStatus'; import type { RouteProp } from '@react-navigation/native'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { StackNavigatorParamList } from '../types'; @@ -106,7 +106,7 @@ type OneOnOneChannelDetailScreenRouteProp = RouteProp< 'OneOnOneChannelDetailScreen' >; -type OneOnOneChannelDetailScreenNavigationProp = StackNavigationProp< +type OneOnOneChannelDetailScreenNavigationProp = NativeStackNavigationProp< StackNavigatorParamList, 'OneOnOneChannelDetailScreen' >; diff --git a/examples/SampleApp/src/screens/RemindersScreen.tsx b/examples/SampleApp/src/screens/RemindersScreen.tsx index df4e0a5545..4e38d8fe80 100644 --- a/examples/SampleApp/src/screens/RemindersScreen.tsx +++ b/examples/SampleApp/src/screens/RemindersScreen.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { StackNavigationProp } from '@react-navigation/stack'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { BottomTabNavigatorParamList } from '../types'; import { StyleSheet, View } from 'react-native'; import { ChatScreenHeader } from '../components/ChatScreenHeader'; @@ -8,7 +8,7 @@ import { useTheme } from 'stream-chat-react-native'; import { RemindersList } from '../components/Reminders/RemindersList'; export type RemindersScreenProps = { - navigation: StackNavigationProp; + navigation: NativeStackNavigationProp; }; export const RemindersScreen: React.FC = () => { diff --git a/examples/SampleApp/src/screens/ThreadListScreen.tsx b/examples/SampleApp/src/screens/ThreadListScreen.tsx index 9415d6a4bf..c62c9a63d0 100644 --- a/examples/SampleApp/src/screens/ThreadListScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadListScreen.tsx @@ -3,7 +3,7 @@ import { StyleSheet, View } from 'react-native'; import { useTheme, ThreadList } from 'stream-chat-react-native'; import { ChatScreenHeader } from '../components/ChatScreenHeader'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { BottomTabNavigatorParamList } from '../types'; import { useNavigation, useIsFocused } from '@react-navigation/native'; @@ -24,7 +24,7 @@ const styles = StyleSheet.create({ }); export type ThreadsScreenProps = { - navigation: StackNavigationProp; + navigation: NativeStackNavigationProp; }; export const ThreadListScreen: React.FC = () => { diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx index dcba48e518..ddf81d1ec4 100644 --- a/examples/SampleApp/src/screens/ThreadScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadScreen.tsx @@ -23,7 +23,7 @@ import { useCreateDraftFocusEffect } from '../utils/useCreateDraftFocusEffect.ts import { MessageReminderHeader } from '../components/Reminders/MessageReminderHeader.tsx'; import { channelMessageActions } from '../utils/messageActions.tsx'; import { useStreamChatContext } from '../context/StreamChatContext.tsx'; -import { StackNavigationProp } from '@react-navigation/stack'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { CustomAttachmentPickerSelectionBar } from '../components/AttachmentPickerSelectionBar.tsx'; import { MessageLocation } from '../components/LocationSharing/MessageLocation.tsx'; @@ -35,7 +35,10 @@ const styles = StyleSheet.create({ }, }); -type ThreadScreenNavigationProp = StackNavigationProp; +type ThreadScreenNavigationProp = NativeStackNavigationProp< + StackNavigatorParamList, + 'ThreadScreen' +>; type ThreadScreenRouteProp = RouteProp; type ThreadScreenProps = { diff --git a/examples/SampleApp/src/screens/UserSelectorScreen.tsx b/examples/SampleApp/src/screens/UserSelectorScreen.tsx index a99893c605..0448c4ead6 100644 --- a/examples/SampleApp/src/screens/UserSelectorScreen.tsx +++ b/examples/SampleApp/src/screens/UserSelectorScreen.tsx @@ -18,7 +18,7 @@ import { StreamLogo } from '../icons/StreamLogo'; import { Settings } from '../icons/Settings'; import AsyncStore from '../utils/AsyncStore'; -import type { StackNavigationProp } from '@react-navigation/stack'; +import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { UserSelectorParamList } from '../types'; @@ -78,7 +78,7 @@ const styles = StyleSheet.create({ }, }); -export type UserSelectorScreenNavigationProp = StackNavigationProp< +export type UserSelectorScreenNavigationProp = NativeStackNavigationProp< UserSelectorParamList, 'UserSelectorScreen' >; diff --git a/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts b/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts new file mode 100644 index 0000000000..211c8e8316 --- /dev/null +++ b/examples/SampleApp/src/utils/bootstrapBackgroundMessageHandler.ts @@ -0,0 +1,71 @@ +import { LoginConfig } from '../types'; +import AsyncStore from './AsyncStore'; +import { + FirebaseMessagingTypes, + setBackgroundMessageHandler, +} from '@react-native-firebase/messaging'; +import { DeliveredMessageConfirmation, StreamChat } from 'stream-chat'; +import notifee from '@notifee/react-native'; +import { getMessaging } from '@react-native-firebase/messaging'; + +const messaging = getMessaging(); + +const displayNotification = async ( + remoteMessage: FirebaseMessagingTypes.RemoteMessage, + channelId: string, +) => { + const { stream, ...rest } = remoteMessage.data ?? {}; + const data = { + ...rest, + ...((stream as unknown as Record | undefined) ?? {}), // extract and merge stream object if present + }; + if (data.body && data.title) { + await notifee.displayNotification({ + android: { + channelId, + pressAction: { + id: 'default', + }, + }, + body: data.body as string, + title: data.title as string, + data, + }); + } +}; + +setBackgroundMessageHandler(messaging, async (remoteMessage) => { + try { + const loginConfig = await AsyncStore.getItem( + '@stream-rn-sampleapp-login-config', + null, + ); + if (!loginConfig) { + return; + } + const chatClient = StreamChat.getInstance(loginConfig.apiKey); + await chatClient._setToken({ id: loginConfig.userId }, loginConfig.userToken); + + const notification = remoteMessage.data; + + const deliverMessageConfirmation = [ + { + cid: notification?.cid, + id: notification?.id, + }, + ]; + + await chatClient?.markChannelsDelivered({ + latest_delivered_messages: deliverMessageConfirmation as DeliveredMessageConfirmation[], + }); + // create the android channel to send the notification to + const channelId = await notifee.createChannel({ + id: 'chat-messages', + name: 'Chat Messages', + }); + // display the notification + await displayNotification(remoteMessage, channelId); + } catch (error) { + console.error(error); + } +}); diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index b95479fc13..10b8d177e4 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -2325,6 +2325,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.4.1.tgz#a3c247aceab35f75dd0aa4bfa85d2be5a4508688" integrity sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg== +"@react-native-community/slider@^5.0.1": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-5.1.0.tgz#e44d4c66a57732c33e625ea8471aa0a1e8c5ad1c" + integrity sha512-79nzA7eXPgM6UMo5ZZ/rrJu8Gai0aRctN6stBHneQYh5Qst98hYD3piY+RQHJhDuCCrpQL85YwGVU4FdDOE3cA== + "@react-native-documents/picker@^10.1.3": version "10.1.3" resolved "https://registry.yarnpkg.com/@react-native-documents/picker/-/picker-10.1.3.tgz#105d2376dd488a36861073218a9470227d2f5100" @@ -2562,6 +2567,25 @@ dependencies: color "^4.2.3" +"@react-navigation/elements@^2.8.1": + version "2.8.1" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.8.1.tgz#4df96e0219a2ed0be20d5452297835e632aaa6f9" + integrity sha512-MLmuS5kPAeAFFOylw89WGjgEFBqGj/KBK6ZrFrAOqLnTqEzk52/SO1olb5GB00k6ZUCDZKJOp1BrLXslxE6TgQ== + dependencies: + color "^4.2.3" + use-latest-callback "^0.2.4" + use-sync-external-store "^1.5.0" + +"@react-navigation/native-stack@^7.6.2": + version "7.6.2" + resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.6.2.tgz#884efb57d4d8b9be4d743deb71e6ae223b610460" + integrity sha512-CB6chGNLwJYiyOeyCNUKx33yT7XJSwRZIeKHf4S1vs+Oqu3u9zMnvGUIsesNgbgX0xy16gBqYsrWgr0ZczBTtA== + dependencies: + "@react-navigation/elements" "^2.8.1" + color "^4.2.3" + sf-symbols-typescript "^2.1.0" + warn-once "^0.1.1" + "@react-navigation/native@^7.1.10": version "7.1.10" resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.10.tgz#768f674f7c09b6a57215762052aa62a7dc107402" @@ -2580,14 +2604,6 @@ dependencies: nanoid "^3.3.11" -"@react-navigation/stack@^7.3.3": - version "7.3.3" - resolved "https://registry.yarnpkg.com/@react-navigation/stack/-/stack-7.3.3.tgz#e5c9e054fa7d3e224e659fc7b3e0da2773624f3c" - integrity sha512-TIUiQKSFYjVwRAT5pFn1jIk4aqPMgeh5eTXzsxgnAxrqrmE9E1o1XaZj+aeqBhTeBKxPTaRSjzsayD2/cue0mA== - dependencies: - "@react-navigation/elements" "^2.4.3" - color "^4.2.3" - "@rnx-kit/metro-config@^2.1.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@rnx-kit/metro-config/-/metro-config-2.1.0.tgz#df71ae13bd6a9b822e200c9dce9155812f9c04fa" @@ -5359,11 +5375,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -hyochan-welcome@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hyochan-welcome/-/hyochan-welcome-1.0.1.tgz#a949de8bc3c1e18fe096016bc273aa191c844971" - integrity sha512-WRZNH5grESkOXP/r7xc7TMhO9cUqxaJIuZcQDAjzHWs6viGP+sWtVbiBigxc9YVRrw3hnkESQWwzqg+oOga65A== - i18next@^25.2.1: version "25.2.1" resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.2.1.tgz#23cf8794904f551f577558d93c84b0fb6cd489a2" @@ -7480,13 +7491,6 @@ react-is@^19.1.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.1.0.tgz#805bce321546b7e14c084989c77022351bbdd11b" integrity sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg== -react-native-audio-recorder-player@^3.6.13: - version "3.6.13" - resolved "https://registry.yarnpkg.com/react-native-audio-recorder-player/-/react-native-audio-recorder-player-3.6.13.tgz#cfb98ef2582a02e1de4a539ccf93981805b39211" - integrity sha512-8IaNxets8as8qfITPh4GgXdW2PhrlGNqYD/41rMeEWRxGGv5KQEttC2uQWkDc7yA7/0GeBN6AFgatK2S7cLZQw== - dependencies: - hyochan-welcome "^1.0.0" - react-native-blob-util@^0.22.2: version "0.22.2" resolved "https://registry.yarnpkg.com/react-native-blob-util/-/react-native-blob-util-0.22.2.tgz#818c4b90a0af37fcc0a659fd63c67ac57e8ea275" @@ -7559,6 +7563,18 @@ react-native-markdown-package@1.8.2: react-native-lightbox "^0.7.0" simple-markdown "^0.7.1" +react-native-nitro-modules@^0.31.3: + version "0.31.3" + resolved "https://registry.yarnpkg.com/react-native-nitro-modules/-/react-native-nitro-modules-0.31.3.tgz#19d26c3c677921687a42ed9c9943050034b3aebe" + integrity sha512-jGHBfSTzSo6eXcb0X4/N1sfYdHm2E+koE2fifLZ3gtArD+3ZeThyFvERhB2fzd4JOqtcIJL/5VLIjxlbjISQ/g== + +react-native-nitro-sound@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/react-native-nitro-sound/-/react-native-nitro-sound-0.2.9.tgz#e456600c3bf3128645cfd30642d80d02f64c98e6" + integrity sha512-shlDKr5riTqqhnh42H0NynZmtzrR6JWqZ7m1yVBfY9B43d4DvD2MdhhLjpvaZwnMefGM5gYrVPN/6YBjcF1RMg== + dependencies: + "@react-native-community/slider" "^5.0.1" + react-native-reanimated@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.0.1.tgz#6cb8bca007baa18d75e0ef8b03e969d2777cd5e8" @@ -7988,6 +8004,11 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sf-symbols-typescript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-2.1.0.tgz#50a2d7b36edd6809606f0b0a36322fc1fdd7cfde" + integrity sha512-ezT7gu/SHTPIOEEoG6TF+O0m5eewl0ZDAO4AtdBi5HjsrUI6JdCG17+Q8+aKp0heM06wZKApRCn5olNbs0Wb/A== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -8663,6 +8684,11 @@ use-latest-callback@^0.2.3: resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.3.tgz#2d644d3063040b9bc2d4c55bb525a13ae3de9e16" integrity sha512-7vI3fBuyRcP91pazVboc4qu+6ZqM8izPWX9k7cRnT8hbD5svslcknsh3S9BUhaK11OmgTV4oWZZVSeQAiV53SQ== +use-latest-callback@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.6.tgz#e5ea752808c86219acc179ace0ae3c1203255e77" + integrity sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg== + use-sync-external-store@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" @@ -8711,7 +8737,7 @@ walker@^1.0.7, walker@^1.0.8: dependencies: makeerror "1.0.12" -warn-once@0.1.1, warn-once@^0.1.0: +warn-once@0.1.1, warn-once@^0.1.0, warn-once@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== diff --git a/package/native-package/package.json b/package/native-package/package.json index dfeb2a6006..cb9f70e376 100644 --- a/package/native-package/package.json +++ b/package/native-package/package.json @@ -30,6 +30,7 @@ "@stream-io/flat-list-mvcp": ">=0.10.3", "react-native": ">=0.73.0", "react-native-audio-recorder-player": ">=3.6.13", + "react-native-nitro-sound": ">=0.2.9", "react-native-blob-util": ">=0.21.1", "react-native-haptic-feedback": ">=2.3.0", "react-native-image-picker": ">=7.1.2", @@ -61,6 +62,9 @@ "react-native-audio-recorder-player": { "optional": true }, + "react-native-nitro-sound": { + "optional": true + }, "react-native-video": { "optional": true }, diff --git a/package/native-package/src/optionalDependencies/Audio.ts b/package/native-package/src/optionalDependencies/Audio.ts index 60c04adf03..8db7d3fa3d 100644 --- a/package/native-package/src/optionalDependencies/Audio.ts +++ b/package/native-package/src/optionalDependencies/Audio.ts @@ -7,11 +7,14 @@ import { AVEncoderAudioQualityIOSType, AVEncodingOption, AVModeIOSOption, + AVModeIOSOptionNitroSound, OutputFormatAndroidType, RNCLIRecordingOptions as RecordingOptions, } from 'stream-chat-react-native-core'; let AudioRecorderPackage; +let AudioRecorderPackageAudioRecorderPlayer; +let AudioRecorderPackageNitroSound; let audioRecorderPlayer; let RNBlobUtil; @@ -23,11 +26,26 @@ try { } try { - AudioRecorderPackage = require('react-native-audio-recorder-player').default; - audioRecorderPlayer = new AudioRecorderPackage(); - audioRecorderPlayer.setSubscriptionDuration(Platform.OS === 'android' ? 0.1 : 0.06); + AudioRecorderPackageNitroSound = require('react-native-nitro-sound'); +} catch (e) { + console.log( + 'The package react-native-audio-recorder-player is deprecated. Please install react-native-nitro-sound instead.', + ); +} + +try { + AudioRecorderPackageAudioRecorderPlayer = require('react-native-audio-recorder-player').default; } catch (e) { - console.log('react-native-audio-recorder-player is not installed.'); + // do nothing +} + +AudioRecorderPackage = AudioRecorderPackageNitroSound || AudioRecorderPackageAudioRecorderPlayer; +if (AudioRecorderPackageNitroSound) { + audioRecorderPlayer = AudioRecorderPackageNitroSound.createSound(); + audioRecorderPlayer.setSubscriptionDuration(Platform.OS === 'android' ? 0.1 : 0.06); +} else if (AudioRecorderPackageAudioRecorderPlayer) { + audioRecorderPlayer = new AudioRecorderPackageAudioRecorderPlayer(); + audioRecorderPlayer.setSubscriptionDuration(Platform.OS === 'android' ? 0.1 : 0.06); } const verifyAndroidPermissions = async () => { @@ -91,13 +109,18 @@ class _Audio { audioRecordingConfiguration: AudioRecordingConfiguration = { options: { audioSet: { + // Android specific properties AudioEncoderAndroid: AudioEncoderAndroidType.AAC, AudioSourceAndroid: AudioSourceAndroidType.MIC, + OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS, + + // iOS specific properties AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high, AVFormatIDKeyIOS: AVEncodingOption.aac, - AVModeIOS: AVModeIOSOption.spokenaudio, + AVModeIOS: AudioRecorderPackageNitroSound + ? AVModeIOSOptionNitroSound.spokenaudio + : AVModeIOSOption.spokenaudio, AVNumberOfChannelsKeyIOS: 2, - OutputFormatAndroid: OutputFormatAndroidType.AAC_ADTS, }, isMeteringEnabled: true, }, @@ -134,7 +157,7 @@ class _Audio { ios: 'sound.aac', }); const recording = await audioRecorderPlayer.startRecorder( - path, + AudioRecorderPackageAudioRecorderPlayer ? path : undefined, this.audioRecordingConfiguration.options.audioSet, this.audioRecordingConfiguration.options.isMeteringEnabled, ); diff --git a/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx b/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx index 7857068d49..bb98581f67 100644 --- a/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx +++ b/package/src/components/ChannelPreview/ChannelPreviewStatus.tsx @@ -3,7 +3,8 @@ import { StyleSheet, Text, View } from 'react-native'; import { ChannelPreviewProps } from './ChannelPreview'; import type { ChannelPreviewMessengerPropsWithContext } from './ChannelPreviewMessenger'; -import { MessageReadStatus } from './hooks/useLatestMessagePreview'; + +import { MessageDeliveryStatus } from './hooks/useMessageDeliveryStatus'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { useTranslationContext } from '../../contexts/translationContext/TranslationContext'; @@ -38,7 +39,7 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => { }, } = useTheme(); - const created_at = latestMessagePreview.messageObject?.created_at; + const created_at = latestMessagePreview?.created_at; const latestMessageDate = created_at ? new Date(created_at) : new Date(); const formattedDate = useMemo( @@ -55,9 +56,11 @@ export const ChannelPreviewStatus = (props: ChannelPreviewStatusProps) => { return ( - {status === MessageReadStatus.READ ? ( + {status === MessageDeliveryStatus.READ ? ( - ) : status === MessageReadStatus.UNREAD ? ( + ) : status === MessageDeliveryStatus.DELIVERED ? ( + + ) : status === MessageDeliveryStatus.SENT ? ( ) : null} diff --git a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts index a902844f59..8222e746f7 100644 --- a/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts +++ b/package/src/components/ChannelPreview/hooks/useChannelPreviewData.ts @@ -1,7 +1,7 @@ import { type SetStateAction, useEffect, useMemo, useState } from 'react'; import throttle from 'lodash/throttle'; -import type { Channel, ChannelState, Event, MessageResponse, StreamChat } from 'stream-chat'; +import type { Channel, Event, LocalMessage, MessageResponse, StreamChat } from 'stream-chat'; import { useIsChannelMuted } from './useIsChannelMuted'; @@ -16,7 +16,7 @@ const setLastMessageThrottleOptions = { leading: true, trailing: true }; const refreshUnreadCountThrottleTimeout = 400; const refreshUnreadCountThrottleOptions = setLastMessageThrottleOptions; -type LastMessageType = ReturnType | MessageResponse; +type LastMessageType = LocalMessage | MessageResponse; export const useChannelPreviewData = ( channel: Channel, @@ -172,7 +172,11 @@ export const useChannelPreviewData = ( return () => listeners.forEach((l) => l.unsubscribe()); }, [channel, refreshUnreadCount, forceUpdate, channelListForceUpdate, setLastMessage]); - const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, lastMessage); + const latestMessagePreview = useLatestMessagePreview( + channel, + forceUpdate, + lastMessage as LocalMessage, + ); return { latestMessagePreview, muted, unread }; }; diff --git a/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts b/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts index 548b1f6094..c731344c7b 100644 --- a/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts +++ b/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts @@ -4,9 +4,8 @@ import { TFunction } from 'i18next'; import type { AttachmentManagerState, Channel, - ChannelState, DraftMessage, - MessageResponse, + LocalMessage, PollState, PollVote, StreamChat, @@ -14,6 +13,8 @@ import type { UserResponse, } from 'stream-chat'; +import { MessageDeliveryStatus, useMessageDeliveryStatus } from './useMessageDeliveryStatus'; + import { useChatContext } from '../../../contexts/chatContext/ChatContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; @@ -22,16 +23,14 @@ import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage'; import { stringifyMessage } from '../../../utils/utils'; -type LatestMessage = ReturnType | MessageResponse; - export type LatestMessagePreview = { - messageObject: LatestMessage | undefined; + messageObject: LocalMessage | undefined; previews: { bold: boolean; text: string; draft?: boolean; }[]; - status: number; + status?: MessageDeliveryStatus; created_at?: string | Date; }; @@ -48,7 +47,7 @@ const selector = (nextValue: PollState): LatestMessagePreviewSelectorReturnType }); const getMessageSenderName = ( - message: LatestMessage | undefined, + message: LocalMessage | undefined, currentUserId: string | undefined, t: (key: string) => string, membersLength: number, @@ -87,7 +86,7 @@ const getLatestMessageDisplayText = ( channel: Channel, client: StreamChat, draftMessage: DraftMessage | undefined, - message: LatestMessage | undefined, + message: LocalMessage | undefined, t: (key: string) => string, pollState: LatestMessagePreviewSelectorReturnType | undefined, ) => { @@ -194,47 +193,23 @@ export enum MessageReadStatus { NOT_SENT_BY_CURRENT_USER = 0, UNREAD = 1, READ = 2, + DELIVERED = 3, } -const getLatestMessageReadStatus = ( - channel: Channel, - client: StreamChat, - message: LatestMessage | undefined, - readEvents: boolean, -): MessageReadStatus => { - const currentUserId = client.userID; - if (!message || currentUserId !== message.user?.id || readEvents === false) { - return MessageReadStatus.NOT_SENT_BY_CURRENT_USER; - } - - const readList = { ...channel.state.read }; - if (currentUserId) { - delete readList[currentUserId]; - } - - const messageUpdatedAt = message.updated_at - ? typeof message.updated_at === 'string' - ? new Date(message.updated_at) - : message.updated_at - : undefined; - - return Object.values(readList).some( - ({ last_read }) => messageUpdatedAt && messageUpdatedAt < last_read, - ) - ? MessageReadStatus.READ - : MessageReadStatus.UNREAD; -}; - const getLatestMessagePreview = (params: { channel: Channel; client: StreamChat; draftMessage?: DraftMessage; pollState: LatestMessagePreviewSelectorReturnType | undefined; - readEvents: boolean; + /** + * @deprecated This parameter is no longer used and will be removed in the next major release. + */ + readEvents?: boolean; + lastMessage?: LocalMessage; + status?: MessageDeliveryStatus; t: TFunction; - lastMessage?: ReturnType | MessageResponse; }) => { - const { channel, client, draftMessage, lastMessage, pollState, readEvents, t } = params; + const { channel, client, draftMessage, lastMessage, pollState, status, t } = params; const messages = channel.state.messages; @@ -248,7 +223,7 @@ const getLatestMessagePreview = (params: { text: t('Nothing yet...'), }, ], - status: MessageReadStatus.NOT_SENT_BY_CURRENT_USER, + status: MessageDeliveryStatus.NOT_SENT_BY_CURRENT_USER, }; } @@ -260,7 +235,7 @@ const getLatestMessagePreview = (params: { created_at: message?.created_at, messageObject: message, previews: getLatestMessageDisplayText(channel, client, draftMessage, message, t, pollState), - status: getLatestMessageReadStatus(channel, client, message, readEvents), + status, }; }; @@ -275,6 +250,9 @@ const stateSelector = (state: AttachmentManagerState) => ({ /** * Hook to set the display preview for latest message on channel. * + * FIXME: This hook is very poorly implemented and needs to be refactored with granular hooks + * to avoid unnecessary re-renders and to make the code more readable. + * * @param {*} channel Channel object * * @returns {object} latest message preview e.g.. { text: 'this was last message ...', created_at: '11/12/2020', messageObject: { originalMessageObject } } @@ -282,7 +260,7 @@ const stateSelector = (state: AttachmentManagerState) => ({ export const useLatestMessagePreview = ( channel: Channel, forceUpdate: number, - lastMessage?: ReturnType | MessageResponse, + lastMessage?: LocalMessage, ) => { const { client } = useChatContext(); const { t } = useTranslationContext(); @@ -328,7 +306,11 @@ export const useLatestMessagePreview = ( return read_events; }, [channelConfigExists, channel]); - const readStatus = getLatestMessageReadStatus(channel, client, translatedLastMessage, readEvents); + const { status } = useMessageDeliveryStatus({ + channel, + isReadEventsEnabled: readEvents, + lastMessage: lastMessage as LocalMessage, + }); const pollId = lastMessage?.poll_id ?? ''; const poll = client.polls.fromState(pollId); @@ -343,16 +325,15 @@ export const useLatestMessagePreview = ( draftMessage, lastMessage: translatedLastMessage, pollState, - readEvents, + status, t, }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [ channelLastMessageString, + status, draftMessage, forceUpdate, - readEvents, - readStatus, latestVotesByOption, createdBy, name, diff --git a/package/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts b/package/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts new file mode 100644 index 0000000000..dd89b9a665 --- /dev/null +++ b/package/src/components/ChannelPreview/hooks/useMessageDeliveryStatus.ts @@ -0,0 +1,103 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { Channel, Event, LocalMessage, MessageResponse } from 'stream-chat'; + +import { useChatContext } from '../../../contexts/chatContext/ChatContext'; + +export enum MessageDeliveryStatus { + NOT_SENT_BY_CURRENT_USER = 'not_sent_by_current_user', + DELIVERED = 'delivered', + READ = 'read', + SENT = 'sent', +} + +type MessageDeliveryStatusProps = { + channel: Channel; + lastMessage: LocalMessage; + isReadEventsEnabled: boolean; +}; + +export const useMessageDeliveryStatus = ({ + channel, + lastMessage, + isReadEventsEnabled = true, +}: MessageDeliveryStatusProps) => { + const { client } = useChatContext(); + const [status, setStatus] = useState(undefined); + + const isOwnMessage = useCallback( + (message: LocalMessage | MessageResponse) => + client.user && message && message.user?.id === client.user.id, + [client], + ); + + useEffect(() => { + if (!lastMessage) { + setStatus(undefined); + } + + if (!isReadEventsEnabled) { + setStatus(MessageDeliveryStatus.NOT_SENT_BY_CURRENT_USER); + return; + } + + if (!lastMessage?.created_at || !isOwnMessage(lastMessage)) { + return; + } + + const msgRef = { + msgId: lastMessage.id, + timestampMs: new Date(lastMessage.created_at).getTime(), + }; + + const readerOfMessage = channel.messageReceiptsTracker.readersForMessage(msgRef); + const deliveredForMessage = channel.messageReceiptsTracker.deliveredForMessage(msgRef); + + setStatus( + readerOfMessage.length > 1 || + (readerOfMessage.length === 1 && readerOfMessage[0].id !== client.user?.id) + ? MessageDeliveryStatus.READ + : deliveredForMessage.length > 1 || + (deliveredForMessage.length === 1 && deliveredForMessage[0].id !== client.user?.id) + ? MessageDeliveryStatus.DELIVERED + : MessageDeliveryStatus.SENT, + ); + }, [channel, client.user?.id, isOwnMessage, isReadEventsEnabled, lastMessage]); + + useEffect(() => { + const handleMessageNew = (event: Event) => { + // the last message is not mine, so do not show the delivery status + if (event.message && !isOwnMessage(event.message)) { + return setStatus(undefined); + } + return setStatus(MessageDeliveryStatus.SENT); + }; + const { unsubscribe } = channel.on('message.new', handleMessageNew); + return unsubscribe; + }, [channel, isOwnMessage]); + + useEffect(() => { + if (!isOwnMessage(lastMessage)) return; + const handleMessageDelivered = (event: Event) => { + if ( + event.user?.id !== client.user?.id && + lastMessage && + lastMessage.id === event.last_delivered_message_id + ) + setStatus(MessageDeliveryStatus.DELIVERED); + }; + + const handleMarkRead = (event: Event) => { + if (event.user?.id !== client.user?.id) setStatus(MessageDeliveryStatus.READ); + }; + + const listeners = [ + channel.on('message.delivered', handleMessageDelivered), + channel.on('message.read', handleMarkRead), + ]; + + return () => listeners.forEach((l) => l.unsubscribe()); + }, [channel, client, isOwnMessage, lastMessage]); + + return { status }; +}; diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 6a677616ae..699d10cdd5 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -6,6 +6,8 @@ import type { Attachment, LocalMessage, UserResponse } from 'stream-chat'; import { useCreateMessageContext } from './hooks/useCreateMessageContext'; import { useMessageActionHandlers } from './hooks/useMessageActionHandlers'; import { useMessageActions } from './hooks/useMessageActions'; +import { useMessageDeliveredData } from './hooks/useMessageDeliveryData'; +import { useMessageReadData } from './hooks/useMessageReadData'; import { useProcessReactions } from './hooks/useProcessReactions'; import { messageActions as defaultMessageActions } from './utils/messageActions'; @@ -46,7 +48,6 @@ import { MessageStatusTypes, } from '../../utils/utils'; import type { Thumbnail } from '../Attachment/utils/buildGallery/types'; -import { getReadState } from '../MessageList/utils/getReadState'; export type TouchableEmitter = | 'fileAttachment' @@ -143,10 +144,18 @@ export type MessagePropsWithContext = Pick< Partial< Omit< MessageContextValue, - 'groupStyles' | 'handleReaction' | 'message' | 'isMessageAIGenerated' | 'readBy' + | 'groupStyles' + | 'handleReaction' + | 'message' + | 'isMessageAIGenerated' + | 'deliveredToCount' + | 'readBy' > > & - Pick & + Pick< + MessageContextValue, + 'groupStyles' | 'message' | 'isMessageAIGenerated' | 'readBy' | 'deliveredToCount' + > & Pick< MessagesContextValue, | 'sendReaction' @@ -221,6 +230,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { chatContext, deleteMessage: deleteMessageFromContext, deleteReaction, + deliveredToCount, dismissKeyboard, dismissKeyboardOnMessageTouch, enableLongPress = true, @@ -626,6 +636,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { actionsEnabled, alignment, channel, + deliveredToCount, dismissOverlay, files: attachments.files, goToMessage, @@ -767,6 +778,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWithContext) => { const { chatContext: { mutedUsers: prevMutedUsers }, + deliveredToCount: prevDeliveredBy, goToMessage: prevGoToMessage, groupStyles: prevGroupStyles, isAttachmentEqual, @@ -781,6 +793,7 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit } = prevProps; const { chatContext: { mutedUsers: nextMutedUsers }, + deliveredToCount: nextDeliveredBy, goToMessage: nextGoToMessage, groupStyles: nextGroupStyles, isTargetedMessage: nextIsTargetedMessage, @@ -793,6 +806,11 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit t: nextT, } = nextProps; + const deliveredByEqual = prevDeliveredBy === nextDeliveredBy; + if (!deliveredByEqual) { + return false; + } + const readByEqual = prevReadBy === nextReadBy; if (!readByEqual) { return false; @@ -957,13 +975,14 @@ export type MessageProps = Partial< */ export const Message = (props: MessageProps) => { const { message } = props; - const { channel, enforceUniqueReaction, members, read } = useChannelContext(); + const { channel, enforceUniqueReaction, members } = useChannelContext(); const chatContext = useChatContext(); const { dismissKeyboard } = useKeyboardContext(); const messagesContext = useMessagesContext(); const { openThread } = useThreadContext(); const { t } = useTranslationContext(); - const readBy = useMemo(() => getReadState(message, read), [message, read]); + const readBy = useMessageReadData({ message }); + const deliveredToCount = useMessageDeliveredData({ message }); const { setQuotedMessage, setEditingState } = useMessageComposerAPIContext(); return ( @@ -972,6 +991,7 @@ export const Message = (props: MessageProps) => { {...{ channel, chatContext, + deliveredToCount, dismissKeyboard, enforceUniqueReaction, members, diff --git a/package/src/components/Message/MessageSimple/MessageBubble.tsx b/package/src/components/Message/MessageSimple/MessageBubble.tsx index be813308dc..773cfd8183 100644 --- a/package/src/components/Message/MessageSimple/MessageBubble.tsx +++ b/package/src/components/Message/MessageSimple/MessageBubble.tsx @@ -101,6 +101,7 @@ export const SwipableMessageBubble = React.memo( ); const SWIPABLE_THRESHOLD = 25; + const MINIMUM_DISTANCE = 8; const triggerHaptic = NativeHandlers.triggerHaptic; @@ -120,14 +121,17 @@ export const SwipableMessageBubble = React.memo( const xDiff = Math.abs(event.changedTouches[0].x - touchStart.value.x); const yDiff = Math.abs(event.changedTouches[0].y - touchStart.value.y); const isHorizontalPanning = xDiff > yDiff; + const hasMinimumDistance = xDiff > MINIMUM_DISTANCE || yDiff > MINIMUM_DISTANCE; - if (isHorizontalPanning) { + // Only activate if there's significant horizontal movement + if (isHorizontalPanning && hasMinimumDistance) { state.activate(); isSwiping.value = true; if (!shouldRenderSwipeableWrapper) { runOnJS(setShouldRenderAnimatedWrapper)(isSwiping.value); } - } else { + } else if (hasMinimumDistance) { + // If there's significant movement but not horizontal, fail the gesture state.fail(); } }) diff --git a/package/src/components/Message/MessageSimple/MessageStatus.tsx b/package/src/components/Message/MessageSimple/MessageStatus.tsx index 1552c775a5..f133e9e510 100644 --- a/package/src/components/Message/MessageSimple/MessageStatus.tsx +++ b/package/src/components/Message/MessageSimple/MessageStatus.tsx @@ -14,12 +14,13 @@ import { MessageStatusTypes } from '../../../utils/utils'; export type MessageStatusPropsWithContext = Pick< MessageContextValue, - 'message' | 'readBy' | 'threadList' ->; + 'deliveredToCount' | 'message' | 'readBy' | 'threadList' +> & { + channelMembersCount: number; +}; const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { - const { channel } = useChannelContext(); - const { message, readBy, threadList } = props; + const { channelMembersCount, deliveredToCount, message, readBy, threadList } = props; const { theme: { @@ -30,68 +31,73 @@ const MessageStatusWithContext = (props: MessageStatusPropsWithContext) => { }, } = useTheme(); - if (message.status === MessageStatusTypes.SENDING) { - return ( - - - ); - } - - if (threadList || message.status === MessageStatusTypes.FAILED) { + if (threadList || message.status === MessageStatusTypes.FAILED || message.type === 'error') { return null; } - if (readBy) { - const members = channel?.state.members; - const otherMembers = Object.values(members).filter( - (member) => member.user_id !== message.user?.id, - ); - const hasOtherMembersGreaterThanOne = otherMembers.length > 1; - const hasReadByGreaterThanOne = typeof readBy === 'number' && readBy > 1; - const shouldDisplayReadByCount = hasOtherMembersGreaterThanOne && hasReadByGreaterThanOne; - const countOfReadBy = - typeof readBy === 'number' && hasOtherMembersGreaterThanOne ? readBy - 1 : 0; - const showDoubleCheck = hasReadByGreaterThanOne || readBy === true; - - return ( - - {shouldDisplayReadByCount ? ( - - {countOfReadBy} - - ) : null} - {message.type !== 'error' ? ( - showDoubleCheck ? ( - - ) : ( - - ) - ) : null} - - ); - } - - if (message.status === MessageStatusTypes.RECEIVED && message.type !== 'ephemeral') { - return ( - - - - ); - } - - return null; + const hasReadByGreaterThanOne = typeof readBy === 'number' && readBy > 1; + + // Variables to determine the status of the message + const read = hasReadByGreaterThanOne || readBy === true; + const delivered = deliveredToCount > 1; + const sending = message.status === MessageStatusTypes.SENDING; + const sent = + message.status === MessageStatusTypes.RECEIVED && + !delivered && + !read && + message.type !== 'ephemeral'; + + const isGroupChannel = channelMembersCount > 2; + + const shouldDisplayReadByCount = isGroupChannel && hasReadByGreaterThanOne; + const countOfReadBy = typeof readBy === 'number' && shouldDisplayReadByCount ? readBy - 1 : 0; + + return ( + + {shouldDisplayReadByCount ? ( + + {countOfReadBy} + + ) : null} + {read ? ( + + ) : delivered ? ( + + ) : sending ? ( + + ); }; const areEqual = ( prevProps: MessageStatusPropsWithContext, nextProps: MessageStatusPropsWithContext, ) => { - const { message: prevMessage, readBy: prevReadBy, threadList: prevThreadList } = prevProps; - const { message: nextMessage, readBy: nextReadBy, threadList: nextThreadList } = nextProps; + const { + deliveredToCount: prevDeliveredBy, + message: prevMessage, + readBy: prevReadBy, + threadList: prevThreadList, + channelMembersCount: prevChannelMembersCount, + } = prevProps; + const { + deliveredToCount: nextDeliveredBy, + message: nextMessage, + readBy: nextReadBy, + threadList: nextThreadList, + channelMembersCount: nextChannelMembersCount, + } = nextProps; + + const deliveredByEqual = prevDeliveredBy === nextDeliveredBy; + if (!deliveredByEqual) { + return false; + } const threadListEqual = prevThreadList === nextThreadList; if (!threadListEqual) { @@ -103,6 +109,11 @@ const areEqual = ( return false; } + const channelMembersCountEqual = prevChannelMembersCount === nextChannelMembersCount; + if (!channelMembersCountEqual) { + return false; + } + const messageEqual = prevMessage.status === nextMessage.status && prevMessage.type === nextMessage.type; if (!messageEqual) { @@ -120,9 +131,17 @@ const MemoizedMessageStatus = React.memo( export type MessageStatusProps = Partial; export const MessageStatus = (props: MessageStatusProps) => { - const { message, readBy, threadList } = useMessageContext(); + const { channel } = useChannelContext(); + const { deliveredToCount, message, readBy, threadList } = useMessageContext(); + + const channelMembersCount = Object.keys(channel?.state.members).length; - return ; + return ( + + ); }; MessageStatus.displayName = 'MessageStatus{messageSimple{status}}'; diff --git a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx index 28b0ac70ca..6f5c892713 100644 --- a/package/src/components/Message/MessageSimple/MessageTextContainer.tsx +++ b/package/src/components/Message/MessageSimple/MessageTextContainer.tsx @@ -71,7 +71,7 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon }, } = theme; - const translatedMessage = useTranslatedMessage(message) as LocalMessage; + const translatedMessage = useTranslatedMessage(message); if (!message.text) { return null; @@ -94,7 +94,7 @@ const MessageTextContainerWithContext = (props: MessageTextContainerPropsWithCon ...markdownStyles, ...(onlyEmojis ? onlyEmojiMarkdown : {}), }, - message: translatedMessage, + message: translatedMessage as LocalMessage, messageOverlay, messageTextNumberOfLines, onLongPress, diff --git a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js index 85c4ab790f..36958bfe38 100644 --- a/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js +++ b/package/src/components/Message/MessageSimple/__tests__/MessageStatus.test.js @@ -42,6 +42,8 @@ describe('MessageStatus', () => { chatClient = await getTestClientWithUser(user1); useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); channel = chatClient.channel('messaging', mockedChannel.id); + + channel.state.members = Object.fromEntries(members.map((member) => [member.user_id, member])); }); afterEach(cleanup); @@ -56,33 +58,19 @@ describe('MessageStatus', () => { , ); - it('should render message status with delivered container', async () => { - const user = generateUser(); - const message = generateMessage({ user }); - - const { getByTestId } = renderMessageStatus({ - lastReceivedId: message.id, - message: { ...message, status: 'received' }, - }); - - await waitFor(() => { - expect(getByTestId('delivered-container')).toBeTruthy(); - }); - }); - it('should render message status with read by container', async () => { const user = generateUser(); const message = generateMessage({ user }); const readBy = 2; - const { getByTestId, getByText, rerender, toJSON } = renderMessageStatus({ - lastReceivedId: message.id, + const { getByLabelText, getByText, rerender, toJSON } = renderMessageStatus({ + deliveredToCount: 2, message, readBy, }); await waitFor(() => { - expect(getByTestId('read-by-container')).toBeTruthy(); + expect(getByLabelText('Read by count')).toBeTruthy(); expect(getByText((readBy - 1).toString())).toBeTruthy(); }); @@ -105,21 +93,29 @@ describe('MessageStatus', () => { await waitFor(() => { expect(toJSON()).toMatchSnapshot(); - expect(getByTestId('read-by-container')).toBeTruthy(); + expect(getByLabelText('Read by count')).toBeTruthy(); expect(getByText((readBy - 1).toString())).toBeTruthy(); }); }); - it('should render message status with sending container', async () => { - const user = generateUser(); - const message = generateMessage({ user }); - - const { getByTestId } = renderMessageStatus({ - message: { ...message, status: 'sending' }, - }); - - await waitFor(() => { - expect(getByTestId('sending-container')).toBeTruthy(); - }); - }); + it.each([ + [1, 1, 'sending', 'Sending'], + [2, 2, 'received', 'Read'], + [1, 1, 'received', 'Sent'], + [2, 1, 'received', 'Delivered'], + ])( + 'should render message status with %s container when deliveredToCount is %s and readBy is %s and status is %s', + async (deliveredToCount, readBy, status, accessibilityLabel) => { + const user = generateUser(); + const message = generateMessage({ user }); + const { getByLabelText } = renderMessageStatus({ + deliveredToCount, + message: { ...message, status }, + readBy, + }); + await waitFor(() => { + expect(getByLabelText(accessibilityLabel)).toBeTruthy(); + }); + }, + ); }); diff --git a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap index 164ee4ead7..605e11641c 100644 --- a/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap +++ b/package/src/components/Message/MessageSimple/__tests__/__snapshots__/MessageStatus.test.js.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`MessageStatus should render message status with read by container 1`] = ` 1 { + const { channel } = useChannelContext(); + const { client } = useChatContext(); + const calculate = useCallback(() => { + if (!message.created_at) { + return 0; + } + const messageRef = { + msgId: message.id, + timestampMs: new Date(message.created_at).getTime(), + }; + return channel.messageReceiptsTracker.deliveredForMessage(messageRef).length; + }, [channel, message]); + + const [deliveredToCount, setDeliveredToCount] = useState(calculate()); + + useEffect(() => { + const { unsubscribe } = channel.on('message.delivered', (event: Event) => { + /** + * An optimization to only re-calculate if the event is received by a different user. + */ + if (event.user?.id !== client.user?.id) { + setDeliveredToCount(calculate()); + } + }); + return unsubscribe; + }, [channel, calculate, client.user?.id]); + + return deliveredToCount; +}; diff --git a/package/src/components/Message/hooks/useMessageReadData.ts b/package/src/components/Message/hooks/useMessageReadData.ts new file mode 100644 index 0000000000..8e0086a232 --- /dev/null +++ b/package/src/components/Message/hooks/useMessageReadData.ts @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { Event, LocalMessage } from 'stream-chat'; + +import { useChannelContext } from '../../../contexts/channelContext/ChannelContext'; +import { useChatContext } from '../../../contexts/chatContext/ChatContext'; + +export const useMessageReadData = ({ message }: { message: LocalMessage }) => { + const { channel } = useChannelContext(); + const { client } = useChatContext(); + const calculate = useCallback(() => { + if (!message.created_at) { + return 0; + } + const messageRef = { + msgId: message.id, + timestampMs: new Date(message.created_at).getTime(), + }; + + return channel.messageReceiptsTracker.readersForMessage(messageRef).length; + }, [channel, message]); + + const [readBy, setReadBy] = useState(calculate()); + + useEffect(() => { + const { unsubscribe } = channel.on('message.read', (event: Event) => { + /** + * An optimization to only re-calculate if the event is received by a different user. + */ + if (event.user?.id !== client.user?.id) { + setReadBy(calculate()); + } + }); + return unsubscribe; + }, [channel, calculate, client.user?.id]); + + return readBy; +}; diff --git a/package/src/components/MessageList/utils/getReadState.ts b/package/src/components/MessageList/utils/getReadState.ts deleted file mode 100644 index 6b7821ca48..0000000000 --- a/package/src/components/MessageList/utils/getReadState.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ChannelState, LocalMessage } from 'stream-chat'; - -/** - * Get the number of users who have read the message - * @param message - The message to get the read state for - * @param read - The read state of the channel - * @returns The number of users who have read the message - */ -export const getReadState = (message: LocalMessage, read?: ChannelState['read']) => { - if (!read) { - return 0; - } - - const readState = Object.values(read).reduce((acc, readState) => { - if (!readState.last_read) { - return acc; - } - - if (message.created_at && message.created_at < readState.last_read) { - return acc + 1; - } - - return acc; - }, 0); - - return readState; -}; diff --git a/package/src/components/ThreadList/ThreadList.tsx b/package/src/components/ThreadList/ThreadList.tsx index 3d5bca18ff..9fd3355074 100644 --- a/package/src/components/ThreadList/ThreadList.tsx +++ b/package/src/components/ThreadList/ThreadList.tsx @@ -40,7 +40,7 @@ export const DefaultThreadListEmptyPlaceholder = () => ; export const DefaultThreadListLoadingNextIndicator = () => ; -const DefaultThreadListItem = (props: { item: Thread }) => ; +const renderItem = (props: { item: Thread }) => ; const ThreadListComponent = () => { const { @@ -73,7 +73,7 @@ const ThreadListComponent = () => { ListEmptyComponent={ThreadListEmptyPlaceholder} ListFooterComponent={isLoadingNext ? ThreadListLoadingMoreIndicator : null} onEndReached={loadMore} - renderItem={DefaultThreadListItem} + renderItem={renderItem} testID='thread-flatlist' {...additionalFlatListProps} /> diff --git a/package/src/components/index.ts b/package/src/components/index.ts index 094c1599ce..108f0471c8 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -157,7 +157,8 @@ export * from './MessageList/TypingIndicatorContainer'; export * from './MessageList/utils/getDateSeparators'; export * from './MessageList/utils/getGroupStyles'; export * from './MessageList/utils/getLastReceivedMessage'; -export * from './MessageList/utils/getReadState'; +export * from './Message/hooks/useMessageDeliveryData'; +export * from './Message/hooks/useMessageReadData'; export * from './MessageMenu/MessageActionList'; export * from './MessageMenu/MessageActionListItem'; diff --git a/package/src/contexts/messageContext/MessageContext.tsx b/package/src/contexts/messageContext/MessageContext.tsx index 824e217a40..70c44865b3 100644 --- a/package/src/contexts/messageContext/MessageContext.tsx +++ b/package/src/contexts/messageContext/MessageContext.tsx @@ -88,8 +88,10 @@ export type MessageContextValue = { /** The images attached to a message */ otherAttachments: Attachment[]; reactions: ReactionSummary[]; - /** Whether or not the message has been read by the current user */ + /** Read count of the message */ readBy: number | boolean; + /** Delivery count of the message */ + deliveredToCount: number; /** React set state function to set the state of `isEditedMessageOpen` */ setIsEditedMessageOpen: React.Dispatch>; /** diff --git a/package/src/hooks/useTranslatedMessage.ts b/package/src/hooks/useTranslatedMessage.ts index 628c6ef99a..a0684516a5 100644 --- a/package/src/hooks/useTranslatedMessage.ts +++ b/package/src/hooks/useTranslatedMessage.ts @@ -1,10 +1,10 @@ -import type { LocalMessage, MessageResponse, TranslationLanguages } from 'stream-chat'; +import type { LocalMessage, TranslationLanguages } from 'stream-chat'; import { useTranslationContext } from '../contexts/translationContext/TranslationContext'; type TranslationKey = `${TranslationLanguages}_text`; -export const useTranslatedMessage = (message?: MessageResponse | LocalMessage) => { +export const useTranslatedMessage = (message?: LocalMessage) => { const { userLanguage } = useTranslationContext(); const translationKey: TranslationKey = `${userLanguage}_text`; diff --git a/package/src/store/mappers/mapReadToStorable.ts b/package/src/store/mappers/mapReadToStorable.ts index 42e6f01bbd..b6f3b8007d 100644 --- a/package/src/store/mappers/mapReadToStorable.ts +++ b/package/src/store/mappers/mapReadToStorable.ts @@ -11,10 +11,19 @@ export const mapReadToStorable = ({ cid: string; read: ReadResponse; }): TableRow<'reads'> => { - const { last_read, unread_messages, user, last_read_message_id } = read; + const { + last_read, + unread_messages, + user, + last_read_message_id, + last_delivered_at, + last_delivered_message_id, + } = read; return { cid, + lastDeliveredAt: mapDateTimeToStorable(last_delivered_at), + lastDeliveredMessageId: last_delivered_message_id, lastRead: mapDateTimeToStorable(last_read), lastReadMessageId: last_read_message_id, unreadMessages: unread_messages, diff --git a/package/src/store/mappers/mapStorableToRead.ts b/package/src/store/mappers/mapStorableToRead.ts index 17c8fe1ddc..b758151bc3 100644 --- a/package/src/store/mappers/mapStorableToRead.ts +++ b/package/src/store/mappers/mapStorableToRead.ts @@ -5,9 +5,18 @@ import { mapStorableToUser } from './mapStorableToUser'; import type { TableRowJoinedUser } from '../types'; export const mapStorableToRead = (row: TableRowJoinedUser<'reads'>): ReadResponse => { - const { lastRead, unreadMessages, user, lastReadMessageId } = row; + const { + lastRead, + unreadMessages, + user, + lastReadMessageId, + lastDeliveredAt, + lastDeliveredMessageId, + } = row; return { + last_delivered_at: lastDeliveredAt, + last_delivered_message_id: lastDeliveredMessageId, last_read: lastRead, last_read_message_id: lastReadMessageId, unread_messages: unreadMessages, diff --git a/package/src/store/schema.ts b/package/src/store/schema.ts index 21de2afed4..b21f1dd9ba 100644 --- a/package/src/store/schema.ts +++ b/package/src/store/schema.ts @@ -266,6 +266,8 @@ export const tables: Tables = { reads: { columns: { cid: 'TEXT NOT NULL', + lastDeliveredAt: 'TEXT', + lastDeliveredMessageId: 'TEXT', lastRead: 'TEXT NOT NULL', lastReadMessageId: 'TEXT', unreadMessages: 'INTEGER DEFAULT 0', @@ -468,6 +470,8 @@ export type Schema = { lastReadMessageId?: string; unreadMessages?: number; userId?: string; + lastDeliveredAt?: string; + lastDeliveredMessageId?: string; }; reminders: { channelCid: string; diff --git a/package/src/types/types.ts b/package/src/types/types.ts index 68a5b8a4ec..f6f36837a9 100644 --- a/package/src/types/types.ts +++ b/package/src/types/types.ts @@ -437,6 +437,7 @@ export enum AVEncodingOption { wav = 'wav', } +// TODO: Remove this enum when audio recorder player is removed completely export enum AVModeIOSOption { gamechat = 'gamechat', measurement = 'measurement', @@ -448,15 +449,19 @@ export enum AVModeIOSOption { voiceprompt = 'voiceprompt', } -export type AVModeIOSType = - | AVModeIOSOption.gamechat - | AVModeIOSOption.measurement - | AVModeIOSOption.movieplayback - | AVModeIOSOption.spokenaudio - | AVModeIOSOption.videochat - | AVModeIOSOption.videorecording - | AVModeIOSOption.voicechat - | AVModeIOSOption.voiceprompt; +// TODO: Change it to AVModeIOSOption when audio recorder player is removed completely +export enum AVModeIOSOptionNitroSound { + gamechat = 'gameChatAudio', + measurement = 'measurement', + movieplayback = 'moviePlayback', + spokenaudio = 'spokenAudio', + videochat = 'videoChat', + videorecording = 'videoRecording', + voicechat = 'voiceChat', + voiceprompt = 'voicePrompt', +} + +export type AVModeIOSType = AVModeIOSOption | AVModeIOSOptionNitroSound; export enum AVEncoderAudioQualityIOSType { min = 0, diff --git a/package/src/utils/utils.ts b/package/src/utils/utils.ts index 928893f9b6..120056db43 100644 --- a/package/src/utils/utils.ts +++ b/package/src/utils/utils.ts @@ -41,6 +41,7 @@ export const ProgressIndicatorTypes: { }); export const MessageStatusTypes = { + DELIVERED: 'delivered', FAILED: 'failed', RECEIVED: 'received', SENDING: 'sending',