diff --git a/README.md b/README.md
index 67323d9a49..077582b73e 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
[](https://www.npmjs.com/package/stream-chat-react-native)
[](https://github.com/GetStream/stream-chat-react-native/actions)
[](https://getstream.io/chat/docs/sdk/reactnative)
-
+
diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock
index 2f4353d0cf..08f8124803 100644
--- a/examples/ExpoMessaging/yarn.lock
+++ b/examples/ExpoMessaging/yarn.lock
@@ -7427,9 +7427,24 @@ stream-chat-react-native-core@7.0.0:
uid ""
stream-chat@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0.tgz#cb22dcb8b7f070c623a13b6b75b212d560534d6c"
- integrity sha512-I4+/DEp7dP3WBgRmqHaLswL+Y2fyQkUWJhYBS5zx4bpu1cYM6WEir9HYjToDNuJjltqa/FFIEF/tMPWr7iTc0A==
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.1.1.tgz#c81ffa84a7ca579d9812065bc159010191b59090"
+ integrity sha512-7Y23aIVQMppNZgRj/rTFwIx9pszxgDcS99idkSXJSgdV8C7FlyDtiF1yQSdP0oiNFAt7OUP/xSqmbJTljrm24Q==
+ dependencies:
+ "@types/jsonwebtoken" "^9.0.8"
+ "@types/ws" "^8.5.14"
+ axios "^1.6.0"
+ base64-js "^1.5.1"
+ form-data "^4.0.0"
+ isomorphic-ws "^5.0.0"
+ jsonwebtoken "^9.0.2"
+ linkifyjs "^4.2.0"
+ ws "^8.18.1"
+
+stream-chat@^9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.2.0.tgz#f3109891ca27f17b6fd0aa6ebcf66be12df1f88c"
+ integrity sha512-inz3CA5tuqqSrla7qjRTCKs+coRKOYROWf0wEWYgbCu0tAUuiBTRtu1PJL1isEXIaPLiWi00BuRrBEIFon9Kng==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock
index 5da03a4244..a054cfd1b8 100644
--- a/examples/SampleApp/ios/Podfile.lock
+++ b/examples/SampleApp/ios/Podfile.lock
@@ -2578,91 +2578,91 @@ SPEC CHECKSUMS:
hermes-engine: b417d2b2aee3b89b58e63e23a51e02be91dc876d
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
- op-sqlite: 2e34a191af7e843608357671c94a6e2befd4b986
+ op-sqlite: c33561ea312a2ae38aae032fd3a42635dc6b57e8
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
- RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
+ RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
RCTDeprecation: b2eecf2d60216df56bc5e6be5f063826d3c1ee35
RCTRequired: 78522de7dc73b81f3ed7890d145fa341f5bb32ea
RCTTypeSafety: c135dd2bf50402d87fd12884cbad5d5e64850edd
React: b229c49ed5898dab46d60f61ed5a0bfa2ee2fadb
React-callinvoker: 2ac508e92c8bd9cf834cc7d7787d94352e4af58f
React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a
- React-Core: 325b4f6d9162ae8b9a6ff42fe78e260eb124180d
- React-CoreModules: 558041e5258f70cd1092f82778d07b8b2ff01897
- React-cxxreact: 8fff17cbe76e6a8f9991b59552e1235429f9c74b
+ React-Core: 13cdd1558d0b3f6d9d5a22e14d89150280e79f02
+ React-CoreModules: b07a6744f48305405e67c845ebf481b6551b712a
+ React-cxxreact: 1055a86c66ac35b4e80bd5fb766aed5f494dfff4
React-debug: c76e92776a86622209279fe6d24a0147584444ed
- React-defaultsnativemodule: 111fb1efc95c2bd0ee18e38e9f7b57d678e6f932
- React-domnativemodule: d5154a815306fd6050ee9346a1490d2fb17eb0e5
- React-Fabric: 51ac32f0a6790b1d3b14d90c6870e5ce5bb3854a
- React-FabricComponents: 1094d6a3c2566b3c56951331c44d7d3960570ac8
- React-FabricImage: 6b210ad3c72704a9ad60dde66c397ce6257333f4
+ React-defaultsnativemodule: c2e3ac39909241374c3322eb2be33f4c15fe6be4
+ React-domnativemodule: 240b3c95b5300cc6537594e73ebc6e8e77585b74
+ React-Fabric: 3b403ca25f74d54454b31d1d2627050e0777d42c
+ React-FabricComponents: 154740cfcd57943709a9d0343769d17173c0ac9c
+ React-FabricImage: 0863e39cea98f3ca2f8c3d92984660795cec84ae
React-featureflags: efb93a998907e4ad5b88f6ed77cc140914d5c36d
- React-featureflagsnativemodule: a74b09429c2e7a57412d78cc159ab86ae4f15db9
- React-graphics: 17ef0ee3ef4a4c1774cc82f1f477ecef4d67c73f
- React-hermes: a9a0c8377627b5506ef9a7b6f60a805c306e3f51
- React-idlecallbacksnativemodule: 0711ec5eb53c7f790641fa00e5f6ec0355d3159b
- React-ImageManager: 23b4701408390428724f0e0ebb2cbed7b37c2b24
- React-jserrorhandler: e21b438ef8b99ea8bf070ff35f00bc0215b5f769
- React-jsi: f3f51595cc4c089037b536368f016d4742bf9cf7
- React-jsiexecutor: cca6c232db461e2fd213a11e9364cfa6fdaa20eb
- React-jsinspector: 8a3c2637b84ebec478f46a43432a522d7489410f
- React-jsinspectortracing: ee0215d2db753cc10f45fc9aa86557718d0b16fb
- React-jsitracing: 258be1fd259141f6aa43012c20c70ebc02e32087
- React-logger: 018826bfd51b9f18e87f67db1590bc510ad20664
- React-Mapbuffer: 9fbb496e7d6f7c34d5e617365ee778bf96d14eae
- React-microtasksnativemodule: 36adde22631838680d1be62776e8ccb83186c06a
- react-native-blob-util: d03eaad9fd1bbe90bd0eedb5bad3333215976086
- react-native-cameraroll: 10054f480dfd6e0bd02fdf08fb6d82f80b362575
- react-native-document-picker: 78c262a7f9f77df2380378aa4b3413b8646ce91b
- react-native-image-picker: 4c10603e9b253c7f48f296034c04a09fe9c8d427
- react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
- react-native-safe-area-context: 02e0f487c16ccf1acc8a666bed2318ceeb5dc14c
- react-native-video: 16b5c395d05f8af23e16bfe3dc0794a5514c882f
- React-NativeModulesApple: ec44c21ae0bbb5f9a2df72db00294e33a00e07f0
- React-perflogger: 9e8d3c0dc0194eb932162812a168aa5dc662f418
- React-performancetimeline: 350424518f433dd43f063dc5f2cf3195c1a5b60f
+ React-featureflagsnativemodule: 51116d72aafea30860f315702d17eb76bbb725a3
+ React-graphics: 91d9920451f633d64d31948da3ba0377b6eda8de
+ React-hermes: 71186f872c932e4574d5feb3ed754dda63a0b3bd
+ React-idlecallbacksnativemodule: 19bf1fa4b2b66fe1898ac1d185129cdcc3221c7c
+ React-ImageManager: 7dc7bfca8e9ecb9a7436b8a89a143a193ef5adcf
+ React-jserrorhandler: d8640792495ac2d78e73acbcc77a8439d1eedfef
+ React-jsi: 0775a66820496769ad83e629f0f5cce621a57fc7
+ React-jsiexecutor: 2cf5ba481386803f3c88b85c63fa102cba5d769e
+ React-jsinspector: d1d9f215c7431b286acc12e83cdf0d90c265f0ed
+ React-jsinspectortracing: c4c1cceb9a9c266ce849c82332e35cc57ee9dae9
+ React-jsitracing: 267618eec9c362658a4587c5ddcfb41b2e00c403
+ React-logger: 795cd5055782db394f187f9db0477d4b25b44291
+ React-Mapbuffer: 0df2a235bd0182f5cbed6c5f095e66deca12e335
+ React-microtasksnativemodule: b31e56a980634f383221bfefd5111d04c14c110b
+ react-native-blob-util: 875bbeee07e4ada135e4edf9fc7b22acf8d9721d
+ react-native-cameraroll: cdc91c4c953d1a18aa3ce88b5a25698025c8c4d2
+ react-native-document-picker: 19be73c0423e4bc886cef74ec282eff750698013
+ react-native-image-picker: 1c620a65f900a47d6d12ec94874c6a1820ebea7d
+ react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
+ react-native-safe-area-context: 0b43456abcaaa3c8323bbfafe9c5f0f9511219d2
+ react-native-video: a225b4d4d3286f3253dc7b00a62e7c8e59d04d51
+ React-NativeModulesApple: b74b4e3004104429461593fe460ad790cc4928c2
+ React-perflogger: ab51b7592532a0ea45bf6eed7e6cae14a368b678
+ React-performancetimeline: 37192fd1019c3b3b597a877dff12f3af68305c34
React-RCTActionSheet: 592674cf61142497e0e820688f5a696e41bf16dd
- React-RCTAnimation: e6d669872f9b3b4ab9527aab283b7c49283236b7
- React-RCTAppDelegate: de2343fe08be4c945d57e0ecce44afcc7dd8fc03
- React-RCTBlob: 3e2dce94c56218becc4b32b627fc2293149f798d
- React-RCTFabric: adad07a08efb186bc1046041207527927524170d
- React-RCTFBReactNativeSpec: d10ca5e0ccbfeac8c047361fedf8e4ac653887b6
- React-RCTImage: dc04b176c022d12a8f55ae7a7279b1e091066ae0
- React-RCTLinking: 88f5e37fe4f26fbc80791aa2a5f01baf9b9a3fd5
- React-RCTNetwork: f213693565efbd698b8e9c18d700a514b49c0c8e
- React-RCTSettings: a2d32a90c45a3575568cad850abc45924999b8a5
- React-RCTText: 54cdcd1cbf6f6a91dc6317f5d2c2b7fc3f6bf7a0
- React-RCTVibration: 11dae0e7f577b5807bb7d31e2e881eb46f854fd4
+ React-RCTAnimation: 8fbb8dba757b49c78f4db403133ab6399a4ce952
+ React-RCTAppDelegate: 7f88baa8cb4e5d6c38bb4d84339925c70c9ac864
+ React-RCTBlob: f89b162d0fe6b570a18e755eb16cbe356d3c6d17
+ React-RCTFabric: f2151588dc1dc884b34b8660d72ef5237aa4b10e
+ React-RCTFBReactNativeSpec: 8c29630c2f379c729300e4c1e540f3d1b78d1936
+ React-RCTImage: ccac9969940f170503857733f9a5f63578e106e1
+ React-RCTLinking: d82427bbf18415a3732105383dff119131cadd90
+ React-RCTNetwork: 12ad4d0fbde939e00251ca5ca890da2e6825cc3c
+ React-RCTSettings: e7865bf9f455abf427da349c855f8644b5c39afa
+ React-RCTText: 2cdfd88745059ec3202a0842ea75a956c7d6f27d
+ React-RCTVibration: a3a1458e6230dfd64b3768ebc0a4aac430d9d508
React-rendererconsistency: aa476d937c91886dd8b2ddde3191c775585ae47a
- React-rendererdebug: df10d858ac7709b9c8349d952474b0746092c690
+ React-rendererdebug: 5a2219e0ceb78f4ffe9ee2d80fa260bb5bac50b2
React-rncore: 517c6c3647d45de81a7920b6959adf14fed2a5a5
- React-RuntimeApple: 6922a0861c3fc4c7d544fc7d1d5cb38c779d1264
- React-RuntimeCore: 41a95876d16630ce00946eaaee7ffd5222242b44
+ React-RuntimeApple: 40809bf5975c265b990dec2725f2cfb61f1afc75
+ React-RuntimeCore: 375c2645e924fdca875918f07ed987653c517edc
React-runtimeexecutor: a188df372373baf5066e6e229177836488799f80
- React-RuntimeHermes: f2ca409c03c36bb3dcbf61bdfa2636501f9faebd
- React-runtimescheduler: 7ae10fa81428c2479e0a5534943dacb8e34c9d52
+ React-RuntimeHermes: 2de8d61ec25d950ae4aebcab1a895e0bb8b18c95
+ React-runtimescheduler: e8b49a60eca68a3513c259879a352ed010fed255
React-timing: e56b95cb12c6fb9146be7ba3d671cf6b5d17b2e0
- React-utils: 6eabecc0e7d7bcf21b6b33357bc1fe8ae13c7c4c
- ReactAppDependencyProvider: a1fb08dfdc7ebc387b2e54cfc9decd283ed821d8
- ReactCodegen: 0f8899ac1bad260bf3b362ee848ef67a70b5a306
- ReactCommon: a30b578194de911fbe1698efb8247bfe4cb6abff
- RNAudioRecorderPlayer: 11df0c7b614e9767ef24d896465c3a758c592de7
- RNCAsyncStorage: 849b77e6ab3eb838361a902b492993056924faab
- RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87
- RNFBApp: b5626640e0f4b4fe5be4f44375df703c0d62ee4b
- RNFBMessaging: 8e38f5ca846497f8a9c91d33f311c00ca52d119c
- RNGestureHandler: f7b3a72c099e1e29b5b81688678bc9108d44057c
- RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168
- RNReactNativeHapticFeedback: eb5395b503c7a8f10de5e6722ef8afd3c61bc4f5
- RNReanimated: fe5c52894886953248a81a10b2a9b6eeb5398d61
- RNScreens: fc78b9b5a1274426d7a59b7d07c272bba13604fa
- RNShare: dcef43a8864fcc114fd582edba7832a906fd318d
- RNSVG: 71e35e78add645b84b52b0c6f203f91028e1ab5e
+ React-utils: 8ad62100a8780798a380b769e968c4764bad1f4b
+ ReactAppDependencyProvider: f2e81d80afd71a8058589e19d8a134243fa53f17
+ ReactCodegen: 299e99fc57c93edc7c5396ef1a39a3a4d494f25d
+ ReactCommon: c8fdbc582b98a07daf201cd95c1da75dd029f3ee
+ RNAudioRecorderPlayer: 224c7de87722938aedce04000d09baa633148f5b
+ RNCAsyncStorage: dac011cac81189c2b3b8654f3db97d2b6362d165
+ RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8
+ RNFBApp: 60366dd9d6bb01327607e1561a32508592d76db9
+ RNFBMessaging: 9465c2e3adb5e02cae8d40048306a30aea7f55cf
+ RNGestureHandler: 0a16f3f13829c01268ae55610a40b57b713c8161
+ RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9
+ RNReactNativeHapticFeedback: a49e613d48d721c99cad9689a490554104c22154
+ RNReanimated: c9f295fb1679867288d238bfaf3ea39225c95e1b
+ RNScreens: 77f93ec55b749c49549b447527ebf78e990125f3
+ RNShare: 12d13ebc179faf22534c605d17b2c2fa40191850
+ RNSVG: 05776cf3f0d52d3f8e7ebee34b2189da7b8638ff
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- stream-chat-react-native: 655e616ec1738f03ab9796eb12693bb1a91bb65e
+ stream-chat-react-native: ba870d69df921790816046ea3cdf32fbd5973a73
Yoga: be02ca501b03c79d7027a6bbbd0a8db985034f11
PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d
diff --git a/examples/SampleApp/src/screens/ChannelScreen.tsx b/examples/SampleApp/src/screens/ChannelScreen.tsx
index f34685141a..3e2c2added 100644
--- a/examples/SampleApp/src/screens/ChannelScreen.tsx
+++ b/examples/SampleApp/src/screens/ChannelScreen.tsx
@@ -124,19 +124,23 @@ export const ChannelScreen: React.FC = ({
useEffect(() => {
const initChannel = async () => {
- if (!chatClient || !channelId) {
+ if (!chatClient || !channelId || channelFromProp) {
return;
}
const newChannel = chatClient?.channel('messaging', channelId);
- if (!newChannel?.initialized) {
- await newChannel?.watch();
+ try {
+ if (!newChannel?.initialized) {
+ await newChannel?.watch();
+ }
+ } catch(error) {
+ console.log('An error has occurred while watching the channel: ', error);
}
setChannel(newChannel);
};
initChannel();
- }, [channelId, chatClient]);
+ }, [channelFromProp, channelId, chatClient]);
useFocusEffect(() => {
setSelectedThread(undefined);
diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock
index 04687f5183..33d8f8a708 100644
--- a/examples/SampleApp/yarn.lock
+++ b/examples/SampleApp/yarn.lock
@@ -7568,9 +7568,24 @@ stream-chat-react-native-core@7.0.0:
uid ""
stream-chat@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0.tgz#cb22dcb8b7f070c623a13b6b75b212d560534d6c"
- integrity sha512-I4+/DEp7dP3WBgRmqHaLswL+Y2fyQkUWJhYBS5zx4bpu1cYM6WEir9HYjToDNuJjltqa/FFIEF/tMPWr7iTc0A==
+ version "9.0.1"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.1.tgz#5c556f7212811b0216db745478e2c7dd8b72162a"
+ integrity sha512-v5jPrvFeZ+mT1r+4Xbw6o/rYe36BYoMVKEscBR12zj+edXS3sw1xgbrbxOfNPPSNKQF6euXya/TmzxC5whisgQ==
+ dependencies:
+ "@types/jsonwebtoken" "^9.0.8"
+ "@types/ws" "^8.5.14"
+ axios "^1.6.0"
+ base64-js "^1.5.1"
+ form-data "^4.0.0"
+ isomorphic-ws "^5.0.0"
+ jsonwebtoken "^9.0.2"
+ linkifyjs "^4.2.0"
+ ws "^8.18.1"
+
+stream-chat@^9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.2.0.tgz#f3109891ca27f17b6fd0aa6ebcf66be12df1f88c"
+ integrity sha512-inz3CA5tuqqSrla7qjRTCKs+coRKOYROWf0wEWYgbCu0tAUuiBTRtu1PJL1isEXIaPLiWi00BuRrBEIFon9Kng==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
diff --git a/examples/TypeScriptMessaging/ios/Podfile.lock b/examples/TypeScriptMessaging/ios/Podfile.lock
index d9be70d483..fed861c54d 100644
--- a/examples/TypeScriptMessaging/ios/Podfile.lock
+++ b/examples/TypeScriptMessaging/ios/Podfile.lock
@@ -2323,88 +2323,88 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
- DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
+ DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6
FBLazyVector: 6fe148afcef2e3213e484758e3459609d40d57f5
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
- glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
+ glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8
hermes-engine: b417d2b2aee3b89b58e63e23a51e02be91dc876d
- op-sqlite: 2e34a191af7e843608357671c94a6e2befd4b986
- RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
+ op-sqlite: c33561ea312a2ae38aae032fd3a42635dc6b57e8
+ RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
RCTDeprecation: b2eecf2d60216df56bc5e6be5f063826d3c1ee35
RCTRequired: 78522de7dc73b81f3ed7890d145fa341f5bb32ea
RCTTypeSafety: c135dd2bf50402d87fd12884cbad5d5e64850edd
React: b229c49ed5898dab46d60f61ed5a0bfa2ee2fadb
React-callinvoker: 2ac508e92c8bd9cf834cc7d7787d94352e4af58f
React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a
- React-Core: 325b4f6d9162ae8b9a6ff42fe78e260eb124180d
- React-CoreModules: 558041e5258f70cd1092f82778d07b8b2ff01897
- React-cxxreact: 8fff17cbe76e6a8f9991b59552e1235429f9c74b
+ React-Core: 13cdd1558d0b3f6d9d5a22e14d89150280e79f02
+ React-CoreModules: b07a6744f48305405e67c845ebf481b6551b712a
+ React-cxxreact: 1055a86c66ac35b4e80bd5fb766aed5f494dfff4
React-debug: 0a5fcdbacc6becba0521e910c1bcfdb20f32a3f6
- React-defaultsnativemodule: 618dc50a0fad41b489997c3eb7aba3a74479fd14
- React-domnativemodule: 7ba599afb6c2a7ec3eb6450153e2efe0b8747e9a
- React-Fabric: 252112089d2c63308f4cbfade4010b6606db67d1
- React-FabricComponents: 3c0f75321680d14d124438ab279c64ec2a3d13c4
- React-FabricImage: 728b8061cdec2857ca885fd605ee03ad43ffca98
+ React-defaultsnativemodule: 4bb28fc97fee5be63a9ebf8f7a435cfe8ba69459
+ React-domnativemodule: b36a11c2597243d7563985028c51ece988d8ae33
+ React-Fabric: afc561718f25b2cd800b709d934101afe376a12c
+ React-FabricComponents: f4e0a4e18a27bf6d39cbf2a0b42f37a92fa4e37f
+ React-FabricImage: 37d8e8b672eda68a19d71143eb65148084efb325
React-featureflags: 19682e02ef5861d96b992af16a19109c3dfc1200
- React-featureflagsnativemodule: 23528c7e7d50782b7ef0804168ba40bbaf1e86ab
- React-graphics: fefe48f71bfe6f48fd037f59e8277b12e91b6be1
- React-hermes: a9a0c8377627b5506ef9a7b6f60a805c306e3f51
- React-idlecallbacksnativemodule: 7e2b6a3b70e042f89cd91dbd73c479bb39a72a7e
- React-ImageManager: e3300996ac2e2914bf821f71e2f2c92ae6e62ae2
- React-jserrorhandler: fa75876c662e5d7e79d6efc763fc9f4c88e26986
- React-jsi: f3f51595cc4c089037b536368f016d4742bf9cf7
- React-jsiexecutor: cca6c232db461e2fd213a11e9364cfa6fdaa20eb
- React-jsinspector: 2bd4c9fddf189d6ec2abf4948461060502582bef
- React-jsinspectortracing: a417d8a0ad481edaa415734b4dac81e3e5ee7dc6
- React-jsitracing: 1ff7172c5b0522cbf6c98d82bdbb160e49b5804e
- React-logger: 018826bfd51b9f18e87f67db1590bc510ad20664
- React-Mapbuffer: 3c11cee7737609275c7b66bd0b1de475f094cedf
- React-microtasksnativemodule: 843f352b32aacbe13a9c750190d34df44c3e6c2c
- react-native-blob-util: f82bbc6f071231ae76e1c03b77290de1781df313
- react-native-document-picker: 8663632f183816c420ea0c462711d1abc19ac936
- react-native-image-picker: df2b20cdfa981f7288f4019774d9baa26a4772c1
- react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
- react-native-safe-area-context: 7e513d737b0b5c1d10bbe0e5fcc9f925a7be144c
- react-native-video: b0fc63469ac8cab3bae8e0e7368ba940e425f8a4
- React-NativeModulesApple: 88433b6946778bea9c153e27b671de15411bf225
- React-perflogger: 9e8d3c0dc0194eb932162812a168aa5dc662f418
- React-performancetimeline: 5a2d6efef52bdcefac079c7baa30934978acd023
+ React-featureflagsnativemodule: d7cddf6d907b4e5ab84f9e744b7e88461656e48c
+ React-graphics: b0f78580cdaf5800d25437e3d41cc6c3d83b7aea
+ React-hermes: 71186f872c932e4574d5feb3ed754dda63a0b3bd
+ React-idlecallbacksnativemodule: dd2af19cdd3bc55149d17a2409ed72b694dfbe9c
+ React-ImageManager: a77dde8d5aa6a2b6962c702bf3a47695ef0aa32b
+ React-jserrorhandler: 9c14e89f12d5904257a79aaf84a70cd2e5ac07ba
+ React-jsi: 0775a66820496769ad83e629f0f5cce621a57fc7
+ React-jsiexecutor: 2cf5ba481386803f3c88b85c63fa102cba5d769e
+ React-jsinspector: 8052d532bb7a98b6e021755674659802fb140cc5
+ React-jsinspectortracing: bdd8fd0adcb4813663562e7874c5842449df6d8a
+ React-jsitracing: 2bab3bf55de3d04baf205def375fa6643c47c794
+ React-logger: 795cd5055782db394f187f9db0477d4b25b44291
+ React-Mapbuffer: 0502faf46cab8fb89cfc7bf3e6c6109b6ef9b5de
+ React-microtasksnativemodule: 663bc64e3a96c5fc91081923ae7481adc1359a78
+ react-native-blob-util: fa67658b21ee53bf62a54741a74c441c0e3f2c90
+ react-native-document-picker: 5cb1c6615796389f4f1b7fe2e4f103e38e4d6398
+ react-native-image-picker: 0452fc9efc21101946746663bf34e0db5741f00c
+ react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
+ react-native-safe-area-context: 9c33120e9eac7741a5364cc2d9f74665049b76b3
+ react-native-video: c95c10cdac8541f74bd92194dd6a8137ebe6a19d
+ React-NativeModulesApple: 16fbd5b040ff6c492dacc361d49e63cba7a6a7a1
+ React-perflogger: ab51b7592532a0ea45bf6eed7e6cae14a368b678
+ React-performancetimeline: bc2e48198ec814d578ac8401f65d78a574358203
React-RCTActionSheet: 592674cf61142497e0e820688f5a696e41bf16dd
- React-RCTAnimation: e6d669872f9b3b4ab9527aab283b7c49283236b7
- React-RCTAppDelegate: de2343fe08be4c945d57e0ecce44afcc7dd8fc03
- React-RCTBlob: 3e2dce94c56218becc4b32b627fc2293149f798d
- React-RCTFabric: cac2c033381d79a5956e08550b0220cb2d78ea93
- React-RCTFBReactNativeSpec: d10ca5e0ccbfeac8c047361fedf8e4ac653887b6
- React-RCTImage: dc04b176c022d12a8f55ae7a7279b1e091066ae0
- React-RCTLinking: 88f5e37fe4f26fbc80791aa2a5f01baf9b9a3fd5
- React-RCTNetwork: f213693565efbd698b8e9c18d700a514b49c0c8e
- React-RCTSettings: a2d32a90c45a3575568cad850abc45924999b8a5
- React-RCTText: 54cdcd1cbf6f6a91dc6317f5d2c2b7fc3f6bf7a0
- React-RCTVibration: 11dae0e7f577b5807bb7d31e2e881eb46f854fd4
+ React-RCTAnimation: 8fbb8dba757b49c78f4db403133ab6399a4ce952
+ React-RCTAppDelegate: 7f88baa8cb4e5d6c38bb4d84339925c70c9ac864
+ React-RCTBlob: f89b162d0fe6b570a18e755eb16cbe356d3c6d17
+ React-RCTFabric: 8ad6d875abe6e87312cef90e4b15ef7f6bed72e6
+ React-RCTFBReactNativeSpec: 8c29630c2f379c729300e4c1e540f3d1b78d1936
+ React-RCTImage: ccac9969940f170503857733f9a5f63578e106e1
+ React-RCTLinking: d82427bbf18415a3732105383dff119131cadd90
+ React-RCTNetwork: 12ad4d0fbde939e00251ca5ca890da2e6825cc3c
+ React-RCTSettings: e7865bf9f455abf427da349c855f8644b5c39afa
+ React-RCTText: 2cdfd88745059ec3202a0842ea75a956c7d6f27d
+ React-RCTVibration: a3a1458e6230dfd64b3768ebc0a4aac430d9d508
React-rendererconsistency: 64e897e00d2568fd8dfe31e2496f80e85c0aaad1
- React-rendererdebug: 41ce452460c44bba715d9e41d5493a96de277764
+ React-rendererdebug: a3f6d3ae7d2fa0035885026756281c07ee32479e
React-rncore: 58748c2aa445f56b99e5118dad0aedb51c40ce9f
- React-RuntimeApple: 7785ed0d8ae54da65a88736bb63ca97608a6d933
- React-RuntimeCore: 6029ea70bc77f98cfd43ebe69217f14e93ba1f12
+ React-RuntimeApple: f0fda7bacabd32daa099cfda8f07466c30acd149
+ React-RuntimeCore: 683ee0b6a76d4b4bf6fbf83a541895b4887cc636
React-runtimeexecutor: a188df372373baf5066e6e229177836488799f80
- React-RuntimeHermes: a264609c28b796edfffc8ae4cb8fad1773ab948b
- React-runtimescheduler: 23ec3a1e0fb1ec752d1a9c1fb15258c30bfc7222
+ React-RuntimeHermes: 907c8e9bec13ea6466b94828c088c24590d4d0b6
+ React-runtimescheduler: a2e2a39125dd6426b5d8b773f689d660cd7c5f60
React-timing: bb220a53a795ed57976a4855c521f3de2f298fe5
- React-utils: 3b054aaebe658fc710a8d239d0e4b9fd3e0b78f9
- ReactAppDependencyProvider: a1fb08dfdc7ebc387b2e54cfc9decd283ed821d8
- ReactCodegen: 008c319179d681a6a00966edfc67fda68f9fbb2e
- ReactCommon: 0c097b53f03d6bf166edbcd0915da32f3015dd90
- RNAudioRecorderPlayer: 11df0c7b614e9767ef24d896465c3a758c592de7
- RNCClipboard: f13dd3ceae005858e137ae9e70f3c414e174ff81
- RNGestureHandler: 8b1080a6db0be82dbca18550d6212b885bfab6b2
- RNReactNativeHapticFeedback: 66c6b0cf19f5d9dae8be36b2336e1fe2a2e42566
- RNReanimated: 6383cd0d805e48768b97bd65bcb1d06f0e69ab8e
- RNScreens: 0d4cb9afe052607ad0aa71f645a88bb7c7f2e64c
- RNShare: 56b5431c60e1e9ee167191f4f327471af1c2941a
- RNSVG: 8126581b369adf6a0004b6a6cab1a55e3002d5b0
+ React-utils: 300d8bbb6555dcffaca71e7a0663201b5c7edbbc
+ ReactAppDependencyProvider: f2e81d80afd71a8058589e19d8a134243fa53f17
+ ReactCodegen: a63a0ab6ae824aef2e8c744981edd718b16eb9f2
+ ReactCommon: 3d39389f8e2a2157d5c999f8fba57bd1c8f226f0
+ RNAudioRecorderPlayer: 224c7de87722938aedce04000d09baa633148f5b
+ RNCClipboard: 7659a79c651d0e889bbd533dcc8bc8ff1e98ed70
+ RNGestureHandler: 9b05fab9a0b48fe48c968de7dbb9ca38a2b4f7ab
+ RNReactNativeHapticFeedback: 5fdbbaedabc1698dc3bb2a72105fadf63136a451
+ RNReanimated: ff71ce0443c71c8a77501ace7b9738ede83cfa55
+ RNScreens: 991214b4e69016c1ae32830d9cea31c9c9422367
+ RNShare: 6b1ee93f4fce1346c7c299c3107af20af05120b4
+ RNSVG: 8588ee1ca9b2e6fd2c99466e35b3db0e9f81bb40
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
- stream-chat-react-native: ad7b0924fab4b907f3ac17210322f8b435affe80
+ stream-chat-react-native: 65658c637042b7223460891d7be6491d46d6cf0c
Yoga: afd04ff05ebe0121a00c468a8a3c8080221cb14c
PODFILE CHECKSUM: 6b7a4b74915b42bfe4ffddaf67cbf5e7a2bfeab3
diff --git a/examples/TypeScriptMessaging/yarn.lock b/examples/TypeScriptMessaging/yarn.lock
index 7076242572..74b4ebdc7c 100644
--- a/examples/TypeScriptMessaging/yarn.lock
+++ b/examples/TypeScriptMessaging/yarn.lock
@@ -6954,9 +6954,24 @@ stream-chat-react-native-core@7.0.0:
uid ""
stream-chat@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0.tgz#cb22dcb8b7f070c623a13b6b75b212d560534d6c"
- integrity sha512-I4+/DEp7dP3WBgRmqHaLswL+Y2fyQkUWJhYBS5zx4bpu1cYM6WEir9HYjToDNuJjltqa/FFIEF/tMPWr7iTc0A==
+ version "9.1.1"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.1.1.tgz#c81ffa84a7ca579d9812065bc159010191b59090"
+ integrity sha512-7Y23aIVQMppNZgRj/rTFwIx9pszxgDcS99idkSXJSgdV8C7FlyDtiF1yQSdP0oiNFAt7OUP/xSqmbJTljrm24Q==
+ dependencies:
+ "@types/jsonwebtoken" "^9.0.8"
+ "@types/ws" "^8.5.14"
+ axios "^1.6.0"
+ base64-js "^1.5.1"
+ form-data "^4.0.0"
+ isomorphic-ws "^5.0.0"
+ jsonwebtoken "^9.0.2"
+ linkifyjs "^4.2.0"
+ ws "^8.18.1"
+
+stream-chat@^9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.2.0.tgz#f3109891ca27f17b6fd0aa6ebcf66be12df1f88c"
+ integrity sha512-inz3CA5tuqqSrla7qjRTCKs+coRKOYROWf0wEWYgbCu0tAUuiBTRtu1PJL1isEXIaPLiWi00BuRrBEIFon9Kng==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
diff --git a/package/package.json b/package/package.json
index 4af7491795..8025fec589 100644
--- a/package/package.json
+++ b/package/package.json
@@ -77,7 +77,7 @@
"path": "0.12.7",
"react-native-markdown-package": "1.8.2",
"react-native-url-polyfill": "^1.3.0",
- "stream-chat": "^9.0.0",
+ "stream-chat": "^9.2.0",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
@@ -149,10 +149,11 @@
"react-test-renderer": "19.0.0",
"rimraf": "^6.0.1",
"typescript": "5.8.2",
- "typescript-eslint": "^8.25.0",
+ "typescript-eslint": "^8.29.0",
"uuid": "^11.1.0"
},
"resolutions": {
- "@types/react": "^19.0.0"
+ "@types/react": "^19.0.0",
+ "@babel/runtime": "^7.26.9"
}
}
diff --git a/package/src/__tests__/offline-support/offline-feature.js b/package/src/__tests__/offline-support/offline-feature.js
index ce995646de..5bc46f9ca6 100644
--- a/package/src/__tests__/offline-support/offline-feature.js
+++ b/package/src/__tests__/offline-support/offline-feature.js
@@ -13,8 +13,11 @@ import { useChannelsContext } from '../../contexts/channelsContext/ChannelsConte
import { getOrCreateChannelApi } from '../../mock-builders/api/getOrCreateChannel';
import { queryChannelsApi } from '../../mock-builders/api/queryChannels';
import { useMockedApis } from '../../mock-builders/api/useMockedApis';
+import dispatchChannelDeletedEvent from '../../mock-builders/event/channelDeleted';
+import dispatchChannelHiddenEvent from '../../mock-builders/event/channelHidden';
import dispatchChannelTruncatedEvent from '../../mock-builders/event/channelTruncated';
import dispatchChannelUpdatedEvent from '../../mock-builders/event/channelUpdated';
+import dispatchChannelVisibleEvent from '../../mock-builders/event/channelVisible';
import dispatchConnectionChangedEvent from '../../mock-builders/event/connectionChanged';
import dispatchMemberAddedEvent from '../../mock-builders/event/memberAdded';
import dispatchMemberRemovedEvent from '../../mock-builders/event/memberRemoved';
@@ -23,6 +26,7 @@ import dispatchMessageNewEvent from '../../mock-builders/event/messageNew';
import dispatchMessageReadEvent from '../../mock-builders/event/messageRead';
import dispatchMessageUpdatedEvent from '../../mock-builders/event/messageUpdated';
import dispatchNotificationAddedToChannel from '../../mock-builders/event/notificationAddedToChannel';
+import dispatchNotificationMarkUnread from '../../mock-builders/event/notificationMarkUnread';
import dispatchNotificationMessageNewEvent from '../../mock-builders/event/notificationMessageNew';
import dispatchNotificationRemovedFromChannel from '../../mock-builders/event/notificationRemovedFromChannel';
import dispatchReactionDeletedEvent from '../../mock-builders/event/reactionDeleted';
@@ -124,8 +128,10 @@ export const Generic = () => {
const createChannel = (messagesOverride) => {
const id = uuidv4();
const cid = `messaging:${id}`;
- const begin = getRandomInt(0, allUsers.length - 2); // begin shouldn't be the end of users.length
- const end = getRandomInt(begin + 1, allUsers.length - 1);
+ // always guarantee at least 2 members for ease of use; cases that need to test specific behaviour
+ // for 1 or 0 member channels should explicitly generate them.
+ const begin = getRandomInt(0, allUsers.length - 3); // begin shouldn't be the end of users.length
+ const end = getRandomInt(begin + 2, allUsers.length - 1);
const usersForMembers = allUsers.slice(begin, end);
const members = usersForMembers.map((user) =>
generateMember({
@@ -133,6 +139,8 @@ export const Generic = () => {
user,
}),
);
+ members.push(generateMember({ cid, user: chatClient.user }));
+
const messages =
messagesOverride ||
Array(10)
@@ -162,8 +170,9 @@ export const Generic = () => {
});
const reads = members.map((member) => ({
+ cid,
last_read: new Date(new Date().setDate(new Date().getDate() - getRandomInt(0, 20))),
- unread_messages: getRandomInt(0, messages.length),
+ unread_messages: 0,
user: member.user,
}));
@@ -176,21 +185,23 @@ export const Generic = () => {
id,
members,
messages,
+ read: reads,
});
};
beforeEach(async () => {
jest.clearAllMocks();
+ chatClient = await getTestClientWithUser({ id: 'dan' });
allUsers = Array(20).fill(1).map(generateUser);
+ allUsers.push(chatClient.user);
allMessages = [];
allMembers = [];
allReactions = [];
allReads = [];
+
channels = Array(10)
.fill(1)
.map(() => createChannel());
-
- chatClient = await getTestClientWithUser({ id: 'dan' });
await BetterSqlite.openDB();
BetterSqlite.dropAllTables();
});
@@ -282,12 +293,7 @@ export const Generic = () => {
);
readsRows.forEach((row) =>
expect(
- allReads.filter(
- (r) =>
- r.last_read === row.lastRead &&
- r.user.id === row.userId &&
- r.unread_messages === row.unreadMessages,
- ),
+ allReads.filter((r) => r.user.id === row.userId && r.cid === row.cid),
).toHaveLength(1),
);
});
@@ -316,6 +322,7 @@ export const Generic = () => {
await act(() => dispatchConnectionChangedEvent(chatClient, false));
// await waiter();
await act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(async () => {
expect(screen.getByTestId('channel-list')).toBeTruthy();
@@ -328,8 +335,10 @@ export const Generic = () => {
renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+
await waitFor(async () => {
- act(() => dispatchConnectionChangedEvent(chatClient));
expect(screen.getByTestId('channel-list')).toBeTruthy();
await expectAllChannelsWithStateToBeInDB(screen.queryAllByLabelText);
});
@@ -342,8 +351,11 @@ export const Generic = () => {
renderComponent();
- await waitFor(() => {
+ await waitFor(async () => {
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(
+ async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true),
+ );
expect(screen.getByTestId('channel-list')).toBeTruthy();
expect(screen.getByTestId(emptyChannel.cid)).toBeTruthy();
expect(chatClient.hydrateActiveChannels).toHaveBeenCalled();
@@ -356,18 +368,151 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const targetChannel = channels[0].channel;
const newMessage = generateMessage({
- cid: channels[0].channel.cid,
+ cid: targetChannel.cid,
user: generateUser(),
});
- act(() => dispatchMessageNewEvent(chatClient, newMessage, channels[0].channel));
+ act(() => dispatchMessageNewEvent(chatClient, newMessage, targetChannel));
await waitFor(async () => {
const messagesRows = await BetterSqlite.selectFromTable('messages');
- const matchingRows = messagesRows.filter((m) => m.id === newMessage.id);
- expect(matchingRows.length).toBe(1);
- expect(matchingRows[0].id).toBe(newMessage.id);
+ const readRows = await BetterSqlite.selectFromTable('reads');
+ const matchingMessageRows = messagesRows.filter((m) => m.id === newMessage.id);
+ const matchingReadRows = readRows.filter(
+ (r) => targetChannel.cid === r.cid && chatClient.userID === r.userId,
+ );
+
+ expect(matchingMessageRows.length).toBe(1);
+ expect(matchingMessageRows[0].id).toBe(newMessage.id);
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(1);
+ });
+ });
+
+ it('should correctly handle multiple new messages and add them to the database', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const targetChannel = channels[0].channel;
+
+ // check if the reads state is correct first
+ await waitFor(async () => {
+ const readRows = await BetterSqlite.selectFromTable('reads');
+ const matchingReadRows = readRows.filter(
+ (r) => targetChannel.cid === r.cid && chatClient.userID === r.userId,
+ );
+
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(0);
+ });
+
+ const newMessages = [
+ generateMessage({
+ cid: targetChannel.cid,
+ user: generateUser(),
+ }),
+ generateMessage({
+ cid: targetChannel.cid,
+ user: generateUser(),
+ }),
+ generateMessage({
+ cid: targetChannel.cid,
+ user: generateUser(),
+ }),
+ ];
+
+ newMessages.forEach((newMessage) => {
+ act(() => dispatchMessageNewEvent(chatClient, newMessage, targetChannel));
+ });
+
+ await waitFor(async () => {
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const readRows = await BetterSqlite.selectFromTable('reads');
+ const matchingMessageRows = messagesRows.filter((m) =>
+ newMessages.some((newMessage) => newMessage.id === m.id),
+ );
+ const matchingReadRows = readRows.filter(
+ (r) => targetChannel.cid === r.cid && chatClient.userID === r.userId,
+ );
+
+ expect(matchingMessageRows.length).toBe(3);
+ newMessages.forEach((newMessage) => {
+ expect(
+ matchingMessageRows.some(
+ (matchingMessageRow) => matchingMessageRow.id === newMessage.id,
+ ),
+ ).toBe(true);
+ });
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(3);
+ });
+ });
+
+ it('should correctly handle multiple new messages from our own user', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const targetChannel = channels[0].channel;
+
+ // check if the reads state is correct first
+ await waitFor(async () => {
+ const readRows = await BetterSqlite.selectFromTable('reads');
+ const matchingReadRows = readRows.filter(
+ (r) => targetChannel.cid === r.cid && chatClient.userID === r.userId,
+ );
+
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(0);
+ });
+
+ const newMessages = [
+ generateMessage({
+ cid: targetChannel.cid,
+ user: chatClient.user,
+ }),
+ generateMessage({
+ cid: targetChannel.cid,
+ user: chatClient.user,
+ }),
+ generateMessage({
+ cid: targetChannel.cid,
+ user: chatClient.user,
+ }),
+ ];
+
+ newMessages.forEach((newMessage) => {
+ act(() => dispatchMessageNewEvent(chatClient, newMessage, targetChannel));
+ });
+
+ await waitFor(async () => {
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const readRows = await BetterSqlite.selectFromTable('reads');
+ const matchingMessageRows = messagesRows.filter((m) =>
+ newMessages.some((newMessage) => newMessage.id === m.id),
+ );
+ const matchingReadRows = readRows.filter(
+ (r) => targetChannel.cid === r.cid && chatClient.userID === r.userId,
+ );
+
+ expect(matchingMessageRows.length).toBe(3);
+ newMessages.forEach((newMessage) => {
+ expect(
+ matchingMessageRows.some(
+ (matchingMessageRow) => matchingMessageRow.id === newMessage.id,
+ ),
+ ).toBe(true);
+ });
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(0);
});
});
@@ -375,8 +520,11 @@ export const Generic = () => {
useMockedApis(chatClient, [queryChannelsApi(channels)]);
renderComponent();
- await waitFor(() => {
+ await waitFor(async () => {
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(
+ async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true),
+ );
expect(screen.getByTestId('channel-list')).toBeTruthy();
});
@@ -399,6 +547,7 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const updatedMessage = { ...channels[0].messages[0] };
@@ -420,6 +569,7 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const removedChannel = channels[getRandomInt(0, channels.length - 1)].channel;
act(() => dispatchNotificationRemovedFromChannel(chatClient, removedChannel));
@@ -441,11 +591,122 @@ export const Generic = () => {
});
});
+ it('should remove the channel from DB if the channel is deleted', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const removedChannel = channels[getRandomInt(0, channels.length - 1)].channel;
+ act(() => dispatchChannelDeletedEvent(chatClient, removedChannel));
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(removedChannel.cid)).toBeFalsy();
+ await expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const channelsRows = await BetterSqlite.selectFromTable('channels');
+ const matchingRows = channelsRows.filter((c) => c.id === removedChannel.id);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === removedChannel.cid);
+
+ expect(matchingRows.length).toBe(0);
+ expect(matchingMessagesRows.length).toBe(0);
+ });
+ });
+
+ it('should correctly mark the channel as hidden in the db', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const hiddenChannel = channels[getRandomInt(0, channels.length - 1)].channel;
+ act(() => dispatchChannelHiddenEvent(chatClient, hiddenChannel));
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(hiddenChannel.cid)).toBeFalsy();
+ await expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const channelsRows = await BetterSqlite.selectFromTable('channels');
+ const matchingRows = channelsRows.filter((c) => c.id === hiddenChannel.id);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === hiddenChannel.cid);
+
+ expect(matchingRows.length).toBe(1);
+ expect(matchingRows[0].hidden).toBeTruthy();
+ expect(matchingMessagesRows.length).toBe(
+ chatClient.activeChannels[hiddenChannel.cid].state.messages.length,
+ );
+ });
+ });
+
+ it('should correctly mark the channel as visible if it was hidden before in the db', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const hiddenChannel = channels[getRandomInt(0, channels.length - 1)].channel;
+ // first, we mark it as hidden
+ act(() => dispatchChannelHiddenEvent(chatClient, hiddenChannel));
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(hiddenChannel.cid)).toBeFalsy();
+ await expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const channelsRows = await BetterSqlite.selectFromTable('channels');
+ const matchingRows = channelsRows.filter((c) => c.id === hiddenChannel.id);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === hiddenChannel.cid);
+
+ expect(matchingRows.length).toBe(1);
+ expect(matchingRows[0].hidden).toBeTruthy();
+ expect(matchingMessagesRows.length).toBe(
+ chatClient.activeChannels[hiddenChannel.cid].state.messages.length,
+ );
+ });
+
+ // then, we make it visible after waiting for everything to finish
+ act(() => dispatchChannelVisibleEvent(chatClient, hiddenChannel));
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(hiddenChannel.cid)).toBeFalsy();
+ await expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const channelsRows = await BetterSqlite.selectFromTable('channels');
+ const matchingRows = channelsRows.filter((c) => c.id === hiddenChannel.id);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === hiddenChannel.cid);
+
+ expect(matchingRows.length).toBe(1);
+ expect(matchingRows[0].hidden).toBeFalsy();
+ expect(matchingMessagesRows.length).toBe(
+ chatClient.activeChannels[hiddenChannel.cid].state.messages.length,
+ );
+ });
+ });
+
it('should add the channel to DB when user is added as member', async () => {
useMockedApis(chatClient, [queryChannelsApi(channels)]);
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const newChannel = createChannel();
@@ -476,11 +737,130 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const channelToTruncate = channels[getRandomInt(0, channels.length - 1)].channel;
act(() => dispatchChannelTruncatedEvent(chatClient, channelToTruncate));
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(channelToTruncate.cid)).toBeTruthy();
+ expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === channelToTruncate.cid);
+
+ const readsRows = await BetterSqlite.selectFromTable('reads');
+ const matchingReadRows = readsRows.filter(
+ (r) => r.userId === chatClient.userID && r.cid === channelToTruncate.cid,
+ );
+
+ expect(matchingMessagesRows.length).toBe(0);
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(0);
+ });
+ });
+
+ it('should truncate the correct messages if channel.truncated arrives with truncated_at', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const channelResponse = channels[getRandomInt(0, channels.length - 1)];
+ const channelToTruncate = channelResponse.channel;
+ const messages = channelResponse.messages;
+ messages.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
+ // truncate at the middle
+ const truncatedAt = messages[Number(messages.length / 2)].created_at;
+ act(() =>
+ dispatchChannelTruncatedEvent(chatClient, {
+ ...channelToTruncate,
+ truncated_at: truncatedAt,
+ }),
+ );
+
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(channelToTruncate.cid)).toBeTruthy();
+ expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === channelToTruncate.cid);
+
+ const readsRows = await BetterSqlite.selectFromTable('reads');
+ const matchingReadRows = readsRows.filter(
+ (r) => r.userId === chatClient.userID && r.cid === channelToTruncate.cid,
+ );
+
+ const messagesLeft = messages.length / 2 - 1;
+
+ expect(matchingMessagesRows.length).toBe(messagesLeft);
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(messagesLeft);
+ });
+ });
+
+ it('should gracefully handle a truncated_at date before each message', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const channelResponse = channels[getRandomInt(0, channels.length - 1)];
+ const channelToTruncate = channelResponse.channel;
+ const truncatedAt = new Date(0).toISOString();
+ act(() =>
+ dispatchChannelTruncatedEvent(chatClient, {
+ ...channelToTruncate,
+ truncated_at: truncatedAt,
+ }),
+ );
+
+ await waitFor(async () => {
+ const channelIdsOnUI = screen
+ .queryAllByLabelText('list-item')
+ .map((node) => node._fiber.pendingProps.testID);
+ expect(channelIdsOnUI.includes(channelToTruncate.cid)).toBeTruthy();
+ expectCIDsOnUIToBeInDB(screen.queryAllByLabelText);
+
+ const messagesRows = await BetterSqlite.selectFromTable('messages');
+ const matchingMessagesRows = messagesRows.filter((m) => m.cid === channelToTruncate.cid);
+
+ expect(matchingMessagesRows.length).toBe(channelResponse.messages.length);
+ });
+ });
+
+ it('should gracefully handle a truncated_at date after each message', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const channelResponse = channels[getRandomInt(0, channels.length - 1)];
+ const channelToTruncate = channelResponse.channel;
+ const messages = channelResponse.messages;
+ const latestTimestamp = Math.max(...messages.map((m) => new Date(m.created_at).getTime()));
+ // truncate at the middle
+ const truncatedAt = new Date(latestTimestamp + 1).toISOString();
+ act(() =>
+ dispatchChannelTruncatedEvent(chatClient, {
+ ...channelToTruncate,
+ truncated_at: truncatedAt,
+ }),
+ );
+
await waitFor(async () => {
const channelIdsOnUI = screen
.queryAllByLabelText('list-item')
@@ -499,6 +879,7 @@ export const Generic = () => {
useMockedApis(chatClient, [queryChannelsApi(channels)]);
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
@@ -539,11 +920,161 @@ export const Generic = () => {
});
});
+ it('should correctly add multiple reactions to the DB', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMessage =
+ targetChannel.messages[getRandomInt(0, targetChannel.messages.length - 1)];
+ const reactionMember =
+ targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+ const someOtherMember = targetChannel.members.filter(
+ (member) => reactionMember.user.id !== member.user.id,
+ )[getRandomInt(0, targetChannel.members.length - 2)];
+
+ const newReactions = [
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ }),
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: someOtherMember.user,
+ }),
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'love',
+ user: reactionMember.user,
+ }),
+ ];
+ const messageWithNewReactionBase = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions],
+ };
+ const newLatestReactions = [];
+
+ newReactions.forEach((newReaction) => {
+ newLatestReactions.push(newReaction);
+ const messageWithNewReaction = {
+ ...messageWithNewReactionBase,
+ latest_reactions: [...messageWithNewReactionBase.latest_reactions, ...newLatestReactions],
+ };
+ act(() =>
+ dispatchReactionNewEvent(
+ chatClient,
+ newReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+ });
+
+ const finalReactionCount =
+ messageWithNewReactionBase.latest_reactions.length +
+ newReactions.filter(
+ (newReaction) =>
+ !messageWithNewReactionBase.latest_reactions.some(
+ (initialReaction) =>
+ initialReaction.type === newReaction.type &&
+ initialReaction.user.id === newReaction.user.id,
+ ),
+ ).length;
+
+ await waitFor(async () => {
+ const reactionsRows = await BetterSqlite.selectFromTable('reactions');
+ const matchingReactionsRows = reactionsRows.filter(
+ (r) => r.messageId === messageWithNewReactionBase.id,
+ );
+
+ expect(matchingReactionsRows.length).toBe(finalReactionCount);
+ newReactions.forEach((newReaction) => {
+ expect(
+ matchingReactionsRows.filter(
+ (reaction) =>
+ reaction.type === newReaction.type && reaction.userId === newReaction.user.id,
+ ).length,
+ ).toBe(1);
+ });
+ });
+ });
+
+ it('should gracefully handle multiple reaction.new events of the same type for the same user', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMessage =
+ targetChannel.messages[getRandomInt(0, targetChannel.messages.length - 1)];
+ const reactionMember =
+ targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+
+ const newReactions = [
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ }),
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ }),
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ }),
+ ];
+ const messageWithNewReactionBase = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions],
+ };
+ const newLatestReactions = [];
+
+ newReactions.forEach((newReaction) => {
+ newLatestReactions.push(newReaction);
+ const messageWithNewReaction = {
+ ...messageWithNewReactionBase,
+ latest_reactions: [...messageWithNewReactionBase.latest_reactions, ...newLatestReactions],
+ };
+ act(() =>
+ dispatchReactionNewEvent(
+ chatClient,
+ newReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+ });
+
+ await waitFor(async () => {
+ const reactionsRows = await BetterSqlite.selectFromTable('reactions');
+ const matchingReactionsRows = reactionsRows.filter(
+ (r) =>
+ r.type === 'wow' &&
+ r.userId === reactionMember.user.id &&
+ r.messageId === messageWithNewReactionBase.id,
+ );
+
+ expect(matchingReactionsRows.length).toBe(1);
+ });
+ });
+
it('should remove a reaction from DB when reaction is deleted', async () => {
useMockedApis(chatClient, [queryChannelsApi(channels)]);
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
@@ -597,6 +1128,7 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
@@ -629,24 +1161,301 @@ export const Generic = () => {
});
});
- it('should add a member to DB when a new member is added to channel', async () => {
+ it('should correctly upsert reactions when enforce_unique is true', async () => {
useMockedApis(chatClient, [queryChannelsApi(channels)]);
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMessage =
+ targetChannel.messages[getRandomInt(0, targetChannel.messages.length - 1)];
+ const reactionMember =
+ targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+
+ const newReactions = [
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ }),
+ generateReaction({
+ message_id: targetMessage.id,
+ type: 'love',
+ user: reactionMember.user,
+ }),
+ ];
+ const messageWithNewReactionBase = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions],
+ };
+ const newLatestReactions = [];
+
+ newReactions.forEach((newReaction) => {
+ newLatestReactions.push(newReaction);
+ const messageWithNewReaction = {
+ ...messageWithNewReactionBase,
+ latest_reactions: [...messageWithNewReactionBase.latest_reactions, ...newLatestReactions],
+ };
+ act(() =>
+ dispatchReactionNewEvent(
+ chatClient,
+ newReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+ });
+
+ await waitFor(async () => {
+ const reactionsRows = await BetterSqlite.selectFromTable('reactions');
+ const matchingReactionsRows = reactionsRows.filter(
+ (r) =>
+ r.messageId === messageWithNewReactionBase.id && r.userId === reactionMember.user.id,
+ );
+
+ expect(matchingReactionsRows.length).toBe(2);
+ newReactions.forEach((newReaction) => {
+ expect(
+ matchingReactionsRows.filter(
+ (reaction) =>
+ reaction.type === newReaction.type && reaction.userId === newReaction.user.id,
+ ).length,
+ ).toBe(1);
+ });
+ });
+
+ const uniqueReaction = generateReaction({
+ message_id: targetMessage.id,
+ type: 'like',
+ user: reactionMember.user,
+ });
+ const messageWithNewReaction = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions, uniqueReaction],
+ };
+
+ act(() =>
+ dispatchReactionUpdatedEvent(
+ chatClient,
+ uniqueReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+
+ await waitFor(async () => {
+ const reactionsRows = await BetterSqlite.selectFromTable('reactions');
+ const matchingReactionsRows = reactionsRows.filter(
+ (r) =>
+ r.type === uniqueReaction.type &&
+ r.userId === reactionMember.user.id &&
+ r.messageId === messageWithNewReaction.id,
+ );
+
+ expect(matchingReactionsRows.length).toBe(1);
+ });
+ });
+
+ it('should also update the corresponding message.reaction_groups with reaction.new', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMessage =
+ targetChannel.messages[getRandomInt(0, targetChannel.messages.length - 1)];
+ const reactionMember =
+ targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+
+ const newReaction = generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ });
+ const newDate = new Date().toISOString();
+ // the actual content of the reaction_groups does not matter, as we just want to know if it updates to it
+ // anything impossible given the scenarios is fine
+ const messageWithNewReaction = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions, newReaction],
+ reaction_groups: {
+ ...targetMessage.reaction_groups,
+ [newReaction.type]: {
+ count: 999,
+ first_reaction_at: newDate,
+ last_reaction_at: newDate,
+ sum_scores: 999,
+ },
+ },
+ };
+
+ act(() =>
+ dispatchReactionNewEvent(
+ chatClient,
+ newReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+
+ await waitFor(async () => {
+ const messageRows = await BetterSqlite.selectFromTable('messages');
+ const messageWithReactionRow = messageRows.filter(
+ (m) => m.id === messageWithNewReaction.id,
+ )[0];
+
+ const reactionGroups = JSON.parse(messageWithReactionRow.reactionGroups);
+
+ expect(reactionGroups[newReaction.type]?.count).toBe(999);
+ expect(reactionGroups[newReaction.type]?.sum_scores).toBe(999);
+ expect(reactionGroups[newReaction.type]?.first_reaction_at).toBe(newDate);
+ expect(reactionGroups[newReaction.type]?.last_reaction_at).toBe(newDate);
+ });
+ });
+
+ it('should also update the corresponding message.reaction_groups with reaction.updated', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMessage =
+ targetChannel.messages[getRandomInt(0, targetChannel.messages.length - 1)];
+ const reactionMember =
+ targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+
+ const newReaction = generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ });
+ const newDate = new Date().toISOString();
+ const messageWithNewReaction = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions, newReaction],
+ reaction_groups: {
+ ...targetMessage.reaction_groups,
+ [newReaction.type]: {
+ count: 999,
+ first_reaction_at: newDate,
+ last_reaction_at: newDate,
+ sum_scores: 999,
+ },
+ },
+ };
+
+ act(() =>
+ dispatchReactionUpdatedEvent(
+ chatClient,
+ newReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+
+ await waitFor(async () => {
+ const messageRows = await BetterSqlite.selectFromTable('messages');
+ const messageWithReactionRow = messageRows.filter(
+ (m) => m.id === messageWithNewReaction.id,
+ )[0];
+
+ const reactionGroups = JSON.parse(messageWithReactionRow.reactionGroups);
+
+ expect(reactionGroups[newReaction.type]?.count).toBe(999);
+ expect(reactionGroups[newReaction.type]?.sum_scores).toBe(999);
+ expect(reactionGroups[newReaction.type]?.first_reaction_at).toBe(newDate);
+ expect(reactionGroups[newReaction.type]?.last_reaction_at).toBe(newDate);
+ });
+ });
+
+ it('should also update the corresponding message.reaction_groups with reaction.deleted', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMessage =
+ targetChannel.messages[getRandomInt(0, targetChannel.messages.length - 1)];
+ const reactionMember =
+ targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+
+ const newReaction = generateReaction({
+ message_id: targetMessage.id,
+ type: 'wow',
+ user: reactionMember.user,
+ });
+ const newDate = new Date().toISOString();
+ const messageWithNewReaction = {
+ ...targetMessage,
+ latest_reactions: [...targetMessage.latest_reactions, newReaction],
+ reaction_groups: {
+ ...targetMessage.reaction_groups,
+ [newReaction.type]: {
+ count: 999,
+ first_reaction_at: newDate,
+ last_reaction_at: newDate,
+ sum_scores: 999,
+ },
+ },
+ };
+
+ act(() =>
+ dispatchReactionDeletedEvent(
+ chatClient,
+ newReaction,
+ messageWithNewReaction,
+ targetChannel.channel,
+ ),
+ );
+
+ await waitFor(async () => {
+ const messageRows = await BetterSqlite.selectFromTable('messages');
+ const messageWithReactionRow = messageRows.filter(
+ (m) => m.id === messageWithNewReaction.id,
+ )[0];
+
+ const reactionGroups = JSON.parse(messageWithReactionRow.reactionGroups);
+
+ expect(reactionGroups[newReaction.type]?.count).toBe(999);
+ expect(reactionGroups[newReaction.type]?.sum_scores).toBe(999);
+ expect(reactionGroups[newReaction.type]?.first_reaction_at).toBe(newDate);
+ expect(reactionGroups[newReaction.type]?.last_reaction_at).toBe(newDate);
+ });
+ });
+
+ it('should add a member to DB when a new member is added to channel', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+ renderComponent();
+
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+
+ const oldMemberCount = targetChannel.channel.member_count;
const newMember = generateMember();
act(() => dispatchMemberAddedEvent(chatClient, newMember, targetChannel.channel));
await waitFor(async () => {
const membersRows = await BetterSqlite.selectFromTable('members');
+ const channelRows = await BetterSqlite.selectFromTable('channels');
const matchingMembersRows = membersRows.filter(
(m) => m.cid === targetChannel.channel.cid && m.userId === newMember.user_id,
);
+ const targetChannelFromDb = channelRows.filter(
+ (c) => c.cid === targetChannel.channel.cid,
+ )[0];
expect(matchingMembersRows.length).toBe(1);
+ expect(targetChannelFromDb.memberCount).toBe(oldMemberCount + 1);
});
});
@@ -655,19 +1464,26 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+ const oldMemberCount = targetChannel.channel.member_count;
act(() => dispatchMemberRemovedEvent(chatClient, targetMember, targetChannel.channel));
await waitFor(async () => {
const membersRows = await BetterSqlite.selectFromTable('members');
+ const channelRows = await BetterSqlite.selectFromTable('channels');
const matchingMembersRows = membersRows.filter(
(m) => m.cid === targetChannel.channel.cid && m.userId === targetMember.user_id,
);
+ const targetChannelFromDb = channelRows.filter(
+ (c) => c.cid === targetChannel.channel.cid,
+ )[0];
expect(matchingMembersRows.length).toBe(0);
+ expect(targetChannelFromDb.memberCount).toBe(oldMemberCount - 1);
});
});
@@ -676,6 +1492,7 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
@@ -702,6 +1519,7 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
@@ -727,12 +1545,19 @@ export const Generic = () => {
renderComponent();
act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
const targetChannel = channels[getRandomInt(0, channels.length - 1)];
const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+ const readTimestamp = new Date().toISOString();
+
act(() => {
- dispatchMessageReadEvent(chatClient, targetMember.user, targetChannel.channel);
+ dispatchMessageReadEvent(chatClient, targetMember.user, targetChannel.channel, {
+ first_unread_message_id: '123',
+ last_read: readTimestamp,
+ last_read_message_id: '321',
+ });
});
await waitFor(async () => {
@@ -743,6 +1568,54 @@ export const Generic = () => {
expect(matchingReadRows.length).toBe(1);
expect(matchingReadRows[0].unreadMessages).toBe(0);
+ expect(matchingReadRows[0].lastReadMessageId).toBe('321');
+ // FIXME: Currently missing from the DB, uncomment when added.
+ // expect(matchingReadRows[0].firstUnreadMessageId).toBe('123');
+ expect(matchingReadRows[0].lastRead).toBe(readTimestamp);
+ });
+ });
+
+ it('should update reads in DB when a channel is marked as unread', async () => {
+ useMockedApis(chatClient, [queryChannelsApi(channels)]);
+
+ renderComponent();
+ act(() => dispatchConnectionChangedEvent(chatClient));
+ await act(async () => await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true));
+ await waitFor(() => expect(screen.getByTestId('channel-list')).toBeTruthy());
+ const targetChannel = channels[getRandomInt(0, channels.length - 1)];
+ const targetMember = targetChannel.members[getRandomInt(0, targetChannel.members.length - 1)];
+
+ chatClient.userID = targetMember.user.id;
+ chatClient.user = targetMember.user;
+
+ const readTimestamp = new Date().toISOString();
+
+ act(() => {
+ dispatchNotificationMarkUnread(
+ chatClient,
+ targetChannel.channel,
+ {
+ first_unread_message_id: '123',
+ last_read: readTimestamp,
+ last_read_message_id: '321',
+ unread_messages: 5,
+ },
+ targetMember.user,
+ );
+ });
+
+ await waitFor(async () => {
+ const readsRows = await BetterSqlite.selectFromTable('reads');
+ const matchingReadRows = readsRows.filter(
+ (r) => r.userId === targetMember.user_id && r.cid === targetChannel.cid,
+ );
+
+ expect(matchingReadRows.length).toBe(1);
+ expect(matchingReadRows[0].unreadMessages).toBe(5);
+ expect(matchingReadRows[0].lastReadMessageId).toBe('321');
+ // FIXME: Currently missing from the DB, uncomment when added.
+ // expect(matchingReadRows[0].firstUnreadMessageId).toBe('123');
+ expect(matchingReadRows[0].lastRead).toBe(readTimestamp);
});
});
});
diff --git a/package/src/__tests__/offline-support/optimistic-update.js b/package/src/__tests__/offline-support/optimistic-update.js
index 68f65bef7e..7cf6fc00b8 100644
--- a/package/src/__tests__/offline-support/optimistic-update.js
+++ b/package/src/__tests__/offline-support/optimistic-update.js
@@ -7,11 +7,12 @@ import { v4 as uuidv4 } from 'uuid';
import { Channel } from '../../components/Channel/Channel';
import { Chat } from '../../components/Chat/Chat';
-import { MessagesContext } from '../../contexts';
+import { MessageInputContext, MessagesContext } from '../../contexts';
import { deleteMessageApi } from '../../mock-builders/api/deleteMessage';
import { deleteReactionApi } from '../../mock-builders/api/deleteReaction';
import { erroredDeleteApi, erroredPostApi } from '../../mock-builders/api/error';
import { getOrCreateChannelApi } from '../../mock-builders/api/getOrCreateChannel';
+import { sendMessageApi } from '../../mock-builders/api/sendMessage';
import { sendReactionApi } from '../../mock-builders/api/sendReaction';
import { useMockedApis } from '../../mock-builders/api/useMockedApis';
import dispatchConnectionChangedEvent from '../../mock-builders/event/connectionChanged';
@@ -112,6 +113,7 @@ export const OptimisticUpdates = () => {
channels: [channelResponse],
isLatestMessagesSet: true,
});
+ chatClient.wsConnection = { isHealthy: true, onlineStatusChanged: jest.fn() };
});
afterEach(() => {
@@ -177,7 +179,7 @@ export const OptimisticUpdates = () => {
});
});
- it('pending task should be cleared if deleteMessage request is succesful', async () => {
+ it('pending task should be cleared if deleteMessage request is successful', async () => {
const message = generateMessage();
render(
@@ -237,7 +239,7 @@ export const OptimisticUpdates = () => {
});
});
- it('pending task should be cleared if sendReaction request is succesful', async () => {
+ it('pending task should be cleared if sendReaction request is successful', async () => {
const reaction = generateReaction();
const targetMessage = channel.state.messages[0];
@@ -264,6 +266,69 @@ export const OptimisticUpdates = () => {
});
});
+ describe('send message', () => {
+ it('pending task should exist if sendMessage request fails', async () => {
+ const newMessage = generateMessage();
+
+ render(
+
+
+ {
+ useMockedApis(chatClient, [erroredPostApi()]);
+ try {
+ await sendMessage({ customMessageData: newMessage });
+ } catch (e) {
+ // do nothing
+ }
+ }}
+ context={MessageInputContext}
+ >
+
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
+ await waitFor(async () => {
+ const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks');
+ const pendingTaskType = pendingTasksRows?.[0]?.type;
+ const pendingTaskPayload = JSON.parse(pendingTasksRows?.[0]?.payload || '{}');
+ expect(pendingTaskType).toBe('send-message');
+ expect(pendingTaskPayload[0].id).toEqual(newMessage.id);
+ expect(pendingTaskPayload[0].text).toEqual(newMessage.text);
+ });
+ });
+
+ it('pending task should be cleared if sendMessage request is successful', async () => {
+ const newMessage = generateMessage();
+
+ // initialValue is needed as a prop to trick the message input ctx into thinking
+ // we are sending a message.
+ render(
+
+
+ {
+ useMockedApis(chatClient, [sendMessageApi(newMessage)]);
+ await sendMessage({ customMessageData: newMessage });
+ }}
+ context={MessageInputContext}
+ >
+
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
+
+ await waitFor(async () => {
+ const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks');
+ expect(pendingTasksRows.length).toBe(0);
+ });
+ });
+ });
+
describe('delete reaction', () => {
it('pending task should exist if deleteReaction request fails', async () => {
const reaction = generateReaction();
@@ -298,7 +363,7 @@ export const OptimisticUpdates = () => {
});
});
- it('pending task should be cleared if deleteReaction request is succesful', async () => {
+ it('pending task should be cleared if deleteReaction request is successful', async () => {
const reaction = generateReaction();
const targetMessage = channel.state.messages[0];
@@ -326,52 +391,97 @@ export const OptimisticUpdates = () => {
});
});
- it('pending task should be executed after connection is recovered', async () => {
- const message = channel.state.messages[0];
- const reaction = generateReaction();
-
- render(
-
-
- {
- useMockedApis(chatClient, [erroredDeleteApi()]);
- try {
- await deleteMessage(reaction);
- } catch (e) {
- // do nothing
- }
-
- useMockedApis(chatClient, [erroredPostApi()]);
- try {
- await sendReaction(reaction.type, message.id);
- } catch (e) {
- // do nothing
- }
- }}
- context={MessagesContext}
- >
-
-
-
- ,
- );
- await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
+ describe('pending task execution', () => {
+ it('pending task should be executed after connection is recovered', async () => {
+ const message = channel.state.messages[0];
+ const reaction = generateReaction();
+
+ render(
+
+
+ {
+ useMockedApis(chatClient, [erroredDeleteApi()]);
+ try {
+ await deleteMessage(message);
+ } catch (e) {
+ // do nothing
+ }
+
+ useMockedApis(chatClient, [erroredPostApi()]);
+ try {
+ await sendReaction(reaction.type, message.id);
+ } catch (e) {
+ // do nothing
+ }
+ }}
+ context={MessagesContext}
+ >
+
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
+
+ await waitFor(async () => {
+ const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks');
+
+ expect(pendingTasksRows.length).toBe(2);
+ });
+
+ const deleteMessageSpy = jest.spyOn(chatClient, '_deleteMessage').mockImplementation();
+ const sendReactionSpy = jest.spyOn(channel, '_sendReaction').mockImplementation();
- await waitFor(async () => {
- const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks');
+ act(() => dispatchConnectionChangedEvent(chatClient, true));
- expect(pendingTasksRows.length).toBe(2);
+ await waitFor(() => {
+ expect(deleteMessageSpy).toHaveBeenCalled();
+ expect(sendReactionSpy).toHaveBeenCalled();
+ });
});
- jest.spyOn(chatClient, 'deleteMessage').mockImplementation();
- jest.spyOn(channel, 'sendReaction').mockImplementation();
+ // This is a separate test so CallbackEffectWithContext does not need to be modified in order
+ // to accept multiple contexts. It can be improved in the future.
+ it('send message pending task should be executed after connection is recovered', async () => {
+ const newMessage = generateMessage();
- act(() => dispatchConnectionChangedEvent(chatClient, true));
+ // initialValue is needed as a prop to trick the message input ctx into thinking
+ // we are sending a message.
+ render(
+
+
+ {
+ useMockedApis(chatClient, [erroredPostApi()]);
+ try {
+ await sendMessage({ customMessageData: newMessage });
+ } catch (e) {
+ // do nothing
+ }
+ }}
+ context={MessageInputContext}
+ >
+
+
+
+ ,
+ );
+ await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
+
+ await waitFor(async () => {
+ const pendingTasksRows = await BetterSqlite.selectFromTable('pendingTasks');
+
+ expect(pendingTasksRows.length).toBe(1);
+ });
- await waitFor(() => {
- expect(chatClient.deleteMessage).toHaveBeenCalled();
- expect(channel.sendReaction).toHaveBeenCalled();
+ const sendMessageSpy = jest.spyOn(channel, '_sendMessage').mockImplementation();
+
+ act(() => dispatchConnectionChangedEvent(chatClient, true));
+
+ await waitFor(() => {
+ expect(sendMessageSpy).toHaveBeenCalled();
+ });
});
});
});
diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx
index d8200a5fcd..8d25180e7c 100644
--- a/package/src/components/Channel/Channel.tsx
+++ b/package/src/components/Channel/Channel.tsx
@@ -95,9 +95,7 @@ import * as dbApi from '../../store/apis';
import { ChannelUnreadState, FileTypes } from '../../types/types';
import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
import { compressedImageURI } from '../../utils/compressImage';
-import { DBSyncManager } from '../../utils/DBSyncManager';
import { patchMessageTextCommand } from '../../utils/patchMessageTextCommand';
-import { removeReactionFromLocalState } from '../../utils/removeReactionFromLocalState';
import { removeReservedFields } from '../../utils/removeReservedFields';
import {
defaultEmojiSearchIndex,
@@ -260,7 +258,7 @@ export type ChannelPropsWithContext = Pick &
| 'StickyHeader'
>
> &
- Pick &
+ Pick &
Partial<
Omit<
InputMessageInputContextValue,
@@ -672,6 +670,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
UploadProgressIndicator = UploadProgressIndicatorDefault,
UrlPreview = CardDefault,
VideoThumbnail = VideoThumbnailDefault,
+ isOnline,
} = props;
const { thread: threadProps, threadInstance } = threadFromProps;
@@ -1039,14 +1038,6 @@ const ChannelWithContext = (props: PropsWithChildren) =
syncingChannelRef.current = true;
setError(false);
- if (channelMessagesState?.messages) {
- await channel?.watch({
- messages: {
- limit: channelMessagesState.messages.length + 30,
- },
- });
- }
-
const parseMessage = (message: LocalMessage) =>
({
...message,
@@ -1056,6 +1047,14 @@ const ChannelWithContext = (props: PropsWithChildren) =
}) as unknown as MessageResponse;
try {
+ if (channelMessagesState?.messages) {
+ await channel?.watch({
+ messages: {
+ limit: channelMessagesState.messages.length + 30,
+ },
+ });
+ }
+
if (!thread) {
copyChannelState();
@@ -1102,12 +1101,14 @@ const ChannelWithContext = (props: PropsWithChildren) =
};
let connectionChangedSubscription: ReturnType;
- if (enableOfflineSupport) {
- connectionChangedSubscription = DBSyncManager.onSyncStatusChange((statusChanged) => {
- if (statusChanged) {
- connectionChangedHandler();
- }
- });
+ if (enableOfflineSupport && client.offlineDb) {
+ connectionChangedSubscription = client.offlineDb.syncManager.onSyncStatusChange(
+ (statusChanged) => {
+ if (statusChanged) {
+ connectionChangedHandler();
+ }
+ },
+ );
} else {
connectionChangedSubscription = client.on('connection.changed', (event) => {
if (event.online) {
@@ -1118,8 +1119,7 @@ const ChannelWithContext = (props: PropsWithChildren) =
return () => {
connectionChangedSubscription.unsubscribe();
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [enableOfflineSupport, shouldSyncChannel]);
+ }, [enableOfflineSupport, client, shouldSyncChannel]);
// In case the channel is disconnected which may happen when channel is deleted,
// underlying js client throws an error. Following function ensures that Channel component
@@ -1346,7 +1346,33 @@ const ChannelWithContext = (props: PropsWithChildren) =
const sendMessageRequest = useStableCallback(
async (message: MessageResponse, retrying?: boolean) => {
+ let failedMessageUpdated = false;
+ const handleFailedMessage = async () => {
+ if (!failedMessageUpdated) {
+ const updatedMessage = {
+ ...message,
+ cid: channel.cid,
+ status: MessageStatusTypes.FAILED,
+ };
+ updateMessage(updatedMessage);
+ threadInstance?.upsertReplyLocally?.({ message: updatedMessage });
+ optimisticallyUpdatedNewMessages.delete(message.id);
+
+ if (enableOfflineSupport) {
+ await dbApi.updateMessage({
+ message: updatedMessage,
+ });
+ }
+
+ failedMessageUpdated = true;
+ }
+ };
+
try {
+ if (!isOnline) {
+ await handleFailedMessage();
+ }
+
const updatedMessage = await uploadPendingAttachments(message);
const extraFields = omit(updatedMessage, [
'__html',
@@ -1386,39 +1412,33 @@ const ChannelWithContext = (props: PropsWithChildren) =
} as StreamMessage;
let messageResponse = {} as SendMessageAPIResponse;
+
if (doSendMessageRequest) {
messageResponse = await doSendMessageRequest(channel?.cid || '', messageData);
} else if (channel) {
messageResponse = await channel.sendMessage(messageData);
}
- if (messageResponse.message) {
- messageResponse.message.status = MessageStatusTypes.RECEIVED;
+ if (messageResponse?.message) {
+ const newMessageResponse = {
+ ...messageResponse.message,
+ status: MessageStatusTypes.RECEIVED,
+ };
if (enableOfflineSupport) {
await dbApi.updateMessage({
- message: { ...messageResponse.message, cid: channel.cid },
+ message: { ...newMessageResponse, cid: channel.cid },
});
}
if (retrying) {
- replaceMessage(message, messageResponse.message);
+ replaceMessage(message, newMessageResponse);
} else {
- updateMessage(messageResponse.message, {}, true);
+ updateMessage(newMessageResponse, {}, true);
}
}
} catch (err) {
console.log(err);
- message.status = MessageStatusTypes.FAILED;
- const updatedMessage = { ...message, cid: channel.cid };
- updateMessage(updatedMessage);
- threadInstance?.upsertReplyLocally?.({ message: updatedMessage });
- optimisticallyUpdatedNewMessages.delete(message.id);
-
- if (enableOfflineSupport) {
- await dbApi.updateMessage({
- message: { ...message, cid: channel.cid },
- });
- }
+ await handleFailedMessage();
}
},
);
@@ -1512,10 +1532,8 @@ const ChannelWithContext = (props: PropsWithChildren) =
}
}
- if (enableOfflineSupport) {
- await dbApi.deleteMessage({
- id: message.id,
- });
+ if (client.offlineDb) {
+ await client.offlineDb.handleRemoveMessage({ messageId: message.id });
}
},
);
@@ -1533,79 +1551,49 @@ const ChannelWithContext = (props: PropsWithChildren) =
{ enforce_unique: enforceUniqueReaction },
];
- if (!enableOfflineSupport) {
- await channel.sendReaction(...payload);
- return;
- }
+ if (enableOfflineSupport) {
+ await addReactionToLocalState({
+ channel,
+ enforceUniqueReaction,
+ messageId,
+ reactionType: type,
+ user: client.user,
+ });
- addReactionToLocalState({
- channel,
- enforceUniqueReaction,
- messageId,
- reactionType: type,
- user: client.user,
- });
+ copyMessagesStateFromChannel(channel);
+ }
- copyMessagesStateFromChannel(channel);
+ const sendReactionResponse = await channel.sendReaction(...payload);
- const sendReactionResponse = await DBSyncManager.queueTask({
- client,
- task: {
- channelId: channel.id,
- channelType: channel.type,
- messageId,
- payload,
- type: 'send-reaction',
- },
- });
if (sendReactionResponse?.message) {
threadInstance?.upsertReplyLocally?.({ message: sendReactionResponse.message });
}
});
const deleteMessage: MessagesContextValue['deleteMessage'] = useStableCallback(
- async (message) => {
+ async (message, hardDelete = false) => {
if (!channel.id) {
throw new Error('Channel has not been initialized yet');
}
- if (!enableOfflineSupport) {
- if (message.status === MessageStatusTypes.FAILED) {
- await removeMessage(message);
- return;
- }
- await client.deleteMessage(message.id);
+ if (message.status === MessageStatusTypes.FAILED) {
+ await removeMessage(message);
return;
}
+ const updatedMessage = {
+ ...message,
+ cid: channel.cid,
+ deleted_at: new Date(),
+ type: 'deleted' as MessageLabel,
+ };
+ updateMessage(updatedMessage);
- if (message.status === MessageStatusTypes.FAILED) {
- await DBSyncManager.dropPendingTasks({ messageId: message.id });
- await removeMessage(message);
- } else {
- const updatedMessage = {
- ...message,
- cid: channel.cid,
- deleted_at: new Date(),
- type: 'deleted' as MessageLabel,
- };
- updateMessage(updatedMessage);
-
- threadInstance?.upsertReplyLocally({ message: updatedMessage });
-
- const data = await DBSyncManager.queueTask({
- client,
- task: {
- channelId: channel.id,
- channelType: channel.type,
- messageId: message.id,
- payload: [message.id],
- type: 'delete-message',
- },
- });
+ threadInstance?.upsertReplyLocally({ message: updatedMessage });
- if (data?.message) {
- updateMessage({ ...data.message });
- }
+ const data = await client.deleteMessage(message.id, hardDelete);
+
+ if (data?.message) {
+ updateMessage({ ...data.message });
}
},
);
@@ -1618,30 +1606,18 @@ const ChannelWithContext = (props: PropsWithChildren) =
const payload: Parameters = [messageId, type];
- if (!enableOfflineSupport) {
- await channel.deleteReaction(...payload);
- return;
- }
-
- removeReactionFromLocalState({
- channel,
- messageId,
- reactionType: type,
- user: client.user,
- });
+ if (enableOfflineSupport) {
+ channel.state.removeReaction({
+ created_at: '',
+ message_id: messageId,
+ type,
+ updated_at: '',
+ });
- copyMessagesStateFromChannel(channel);
+ copyMessagesStateFromChannel(channel);
+ }
- await DBSyncManager.queueTask({
- client,
- task: {
- channelId: channel.id,
- channelType: channel.type,
- messageId,
- payload,
- type: 'delete-reaction',
- },
- });
+ await channel.deleteReaction(...payload);
},
);
@@ -2050,7 +2026,7 @@ export type ChannelProps = Partial) => {
- const { client, enableOfflineSupport, isMessageAIGenerated } = useChatContext();
+ const { client, enableOfflineSupport, isOnline, isMessageAIGenerated } = useChatContext();
const { t } = useTranslationContext();
const threadFromProps = props?.thread;
@@ -2082,6 +2058,7 @@ export const Channel = (props: PropsWithChildren) => {
shouldSyncChannel={shouldSyncChannel}
{...{
isMessageAIGenerated,
+ isOnline,
setThreadMessages,
thread,
threadMessages,
diff --git a/package/src/components/ChannelList/ChannelList.tsx b/package/src/components/ChannelList/ChannelList.tsx
index 083af190b8..d141040f76 100644
--- a/package/src/components/ChannelList/ChannelList.tsx
+++ b/package/src/components/ChannelList/ChannelList.tsx
@@ -19,7 +19,6 @@ import {
ChannelsProvider,
} from '../../contexts/channelsContext/ChannelsContext';
import { useChatContext } from '../../contexts/chatContext/ChatContext';
-import { upsertCidsForQuery } from '../../store/apis/upsertCidsForQuery';
import type { ChannelListEventListenerOptions } from '../../types/types';
import { ChannelPreviewMessenger } from '../ChannelPreview/ChannelPreviewMessenger';
import { EmptyStateIndicator as EmptyStateIndicatorDefault } from '../Indicators/EmptyStateIndicator';
@@ -321,7 +320,7 @@ export const ChannelList = (props: ChannelListProps) => {
]);
useEffect(() => {
- channelManager.setOptions({ abortInFlightQuery: true, lockChannelOrder });
+ channelManager.setOptions({ abortInFlightQuery: false, lockChannelOrder });
}, [channelManager, lockChannelOrder]);
useEffect(() => {
@@ -343,7 +342,6 @@ export const ChannelList = (props: ChannelListProps) => {
refreshing,
refreshList,
reloadList,
- staticChannelsActive,
} = usePaginatedChannels({
channelManager,
enableOfflineSupport,
@@ -358,26 +356,6 @@ export const ChannelList = (props: ChannelListProps) => {
setChannels: channelManager.setChannels,
});
- const channelIdsStr = channels?.reduce((acc, channel) => `${acc}${channel.cid}`, '');
-
- useEffect(() => {
- if (
- channels == null ||
- !channelListInitialized ||
- staticChannelsActive ||
- !enableOfflineSupport
- ) {
- return;
- }
-
- upsertCidsForQuery({
- cids: channels.map((c) => c.cid),
- filters,
- sort,
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [channelIdsStr, staticChannelsActive]);
-
const channelsContext = useCreateChannelsContext({
additionalFlatListProps,
channelListInitialized,
diff --git a/package/src/components/ChannelList/hooks/usePaginatedChannels.ts b/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
index 7c75acbbdd..4fd5b1ffaf 100644
--- a/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
+++ b/package/src/components/ChannelList/hooks/usePaginatedChannels.ts
@@ -13,17 +13,8 @@ import { useChatContext } from '../../../contexts/chatContext/ChatContext';
import { useStateStore } from '../../../hooks';
import { useIsMountedRef } from '../../../hooks/useIsMountedRef';
-import { getChannelsForFilterSort } from '../../../store/apis/getChannelsForFilterSort';
-
-import { ONE_SECOND_IN_MS } from '../../../utils/date';
-import { DBSyncManager } from '../../../utils/DBSyncManager';
import { MAX_QUERY_CHANNELS_LIMIT } from '../utils';
-const waitSeconds = (seconds: number) =>
- new Promise((resolve) => {
- setTimeout(resolve, seconds * ONE_SECOND_IN_MS);
- });
-
type Parameters = {
channelManager: ChannelManager;
enableOfflineSupport: boolean;
@@ -37,7 +28,6 @@ const DEFAULT_OPTIONS = {
message_limit: 10,
};
-const MAX_NUMBER_OF_RETRIES = 3;
const RETRY_INTERVAL_IN_MS = 5000;
type QueryType = 'queryLocalDB' | 'reload' | 'refresh' | 'loadChannels';
@@ -48,6 +38,7 @@ const selector = (nextValue: ChannelManagerState) =>
({
channelListInitialized: nextValue.initialized,
channels: nextValue.channels,
+ error: nextValue.error,
pagination: nextValue.pagination,
}) as const;
@@ -56,16 +47,14 @@ export const usePaginatedChannels = ({
enableOfflineSupport,
filters = {},
options = DEFAULT_OPTIONS,
- setForceUpdate,
sort = {},
}: Parameters) => {
- const [error, setError] = useState(undefined);
const [staticChannelsActive, setStaticChannelsActive] = useState(false);
const [activeQueryType, setActiveQueryType] = useState('queryLocalDB');
const activeChannels = useActiveChannelsRefContext();
const isMountedRef = useIsMountedRef();
const { client } = useChatContext();
- const { channelListInitialized, channels, pagination } =
+ const { channelListInitialized, channels, pagination, error } =
useStateStore(channelManager?.state, selector) ?? {};
const hasNextPage = pagination?.hasNext;
@@ -77,7 +66,6 @@ export const usePaginatedChannels = ({
const queryChannels: QueryChannels = async (
queryType: QueryType = 'loadChannels',
- retryCount = 0,
): Promise => {
if (!client || !isMountedRef.current) {
return;
@@ -106,7 +94,6 @@ export const usePaginatedChannels = ({
filtersRef.current = filters;
sortRef.current = sort;
isQueryingRef.current = true;
- setError(undefined);
activeRequestId.current++;
const currentRequestId = activeRequestId.current;
setActiveQueryType(queryType);
@@ -138,27 +125,12 @@ export const usePaginatedChannels = ({
isQueryingRef.current = false;
} catch (err: unknown) {
isQueryingRef.current = false;
- await waitSeconds(2);
if (isQueryStale()) {
return;
}
- // querying.current check is needed in order to make sure the next query call doesnt flick an error
- // state and then succeed (reconnect case)
- if (retryCount === MAX_NUMBER_OF_RETRIES && !isQueryingRef.current) {
- setActiveQueryType(null);
- console.warn(err);
-
- setError(
- new Error(
- `Maximum number of retries reached in queryChannels. Last error message is: ${err}`,
- ),
- );
- return;
- }
-
- return queryChannels(queryType, retryCount + 1);
+ console.warn(err);
}
setActiveQueryType(null);
@@ -198,71 +170,15 @@ export const usePaginatedChannels = ({
const sortStr = useMemo(() => JSON.stringify(sort), [sort]);
useEffect(() => {
- const loadOfflineChannels = async () => {
- if (!client?.user?.id) {
- return;
- }
-
- try {
- const channelsFromDB = await getChannelsForFilterSort({
- currentUserId: client.user.id,
- filters,
- sort,
- });
-
- if (channelsFromDB) {
- const offlineChannels = client.hydrateActiveChannels(channelsFromDB, {
- offlineMode: true,
- skipInitialization: [], // passing empty array will clear out the existing messages from channel state, this removes the possibility of duplicate messages
- });
-
- channelManager.setChannels(offlineChannels);
- setStaticChannelsActive(true);
- }
- } catch (e) {
- console.warn('Failed to get channels from database: ', e);
- return false;
- }
-
- setActiveQueryType(null);
-
- return true;
- };
-
- let listener: ReturnType;
- if (enableOfflineSupport) {
- // Any time DB is synced, we need to update the UI with local DB channels first,
- // and then call queryChannels to ensure any new channels are added to UI.
- listener = DBSyncManager.onSyncStatusChange(async (syncStatus) => {
- if (syncStatus) {
- const loadingChannelsSucceeded = await loadOfflineChannels();
- if (loadingChannelsSucceeded) {
- await reloadList();
- setForceUpdate((u) => u + 1);
- }
- }
- });
- // On start, load the channels from local db.
- loadOfflineChannels().then((success) => {
- // If db is already synced (sync api and pending api calls), then
- // right away call queryChannels.
- if (success) {
- const dbSyncStatus = DBSyncManager.getSyncStatus();
- if (dbSyncStatus) {
- reloadList();
- }
- }
- });
- } else {
- listener = client.on('connection.changed', async (event) => {
+ const listener: ReturnType = client.on(
+ 'connection.changed',
+ async (event) => {
if (event.online) {
await refreshList();
- setForceUpdate((u) => u + 1);
}
- });
-
- reloadList();
- }
+ },
+ );
+ reloadList();
return () => listener?.unsubscribe?.();
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts b/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts
index 366271361b..9dd063d088 100644
--- a/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts
+++ b/package/src/components/ChannelPreview/hooks/useIsChannelMuted.ts
@@ -17,11 +17,29 @@ export const useIsChannelMuted = (channel: Channel) => {
useEffect(() => {
const handleEvent = () => {
+ const newMuteStatus = channel.muteStatus();
+ if (
+ newMuteStatus.muted === muted.muted &&
+ newMuteStatus.createdAt?.getTime?.() === muted.createdAt?.getTime?.() &&
+ newMuteStatus.expiresAt?.getTime?.() === muted.expiresAt?.getTime?.()
+ ) {
+ return;
+ }
+
setMuted(channel.muteStatus());
};
- client.on('notification.channel_mutes_updated', handleEvent);
- return () => client.off('notification.channel_mutes_updated', handleEvent);
+ const listeners = [
+ client.on('notification.channel_mutes_updated', handleEvent),
+ client.on('health.check', (event) => {
+ if (event.me) {
+ handleEvent();
+ }
+ }),
+ ];
+ return () => {
+ listeners.forEach((listener) => listener.unsubscribe());
+ };
}, [channel, client, muted]);
return muted ?? defaultMuteStatus;
diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx
index d8fe2e261b..cbb6b8f1a7 100644
--- a/package/src/components/Chat/Chat.tsx
+++ b/package/src/components/Chat/Chat.tsx
@@ -1,15 +1,13 @@
import React, { PropsWithChildren, useEffect, useState } from 'react';
import { Image, Platform } from 'react-native';
-import type { Channel, StreamChat } from 'stream-chat';
+import { Channel, OfflineDBState } from 'stream-chat';
import { useAppSettings } from './hooks/useAppSettings';
import { useCreateChatContext } from './hooks/useCreateChatContext';
import { useIsOnline } from './hooks/useIsOnline';
import { useMutedUsers } from './hooks/useMutedUsers';
-import { useSyncDatabase } from './hooks/useSyncDatabase';
-
import { ChannelsStateProvider } from '../../contexts/channelsStateContext/ChannelsStateContext';
import { ChatContextValue, ChatProvider } from '../../contexts/chatContext/ChatContext';
import { useDebugContext } from '../../contexts/debugContext/DebugContext';
@@ -20,13 +18,13 @@ import {
DEFAULT_USER_LANGUAGE,
TranslationProvider,
} from '../../contexts/translationContext/TranslationContext';
+import { useStateStore } from '../../hooks';
import { useStreami18n } from '../../hooks/useStreami18n';
import init from '../../init';
import { NativeHandlers } from '../../native';
-import { SqliteClient } from '../../store/SqliteClient';
+import { OfflineDB } from '../../store/OfflineDB';
-import { DBSyncManager } from '../../utils/DBSyncManager';
import type { Streami18n } from '../../utils/i18n/Streami18n';
import { version } from '../../version.json';
@@ -134,6 +132,12 @@ export type ChatProps = Pick &
style?: DeepPartial;
};
+const selector = (nextValue: OfflineDBState) =>
+ ({
+ initialized: nextValue.initialized,
+ userId: nextValue.userId,
+ }) as const;
+
const ChatWithContext = (props: PropsWithChildren) => {
const {
children,
@@ -157,13 +161,8 @@ const ChatWithContext = (props: PropsWithChildren) => {
*/
const { connectionRecovering, isOnline } = useIsOnline(client, closeConnectionOnBackground);
- const [initialisedDatabaseConfig, setInitialisedDatabaseConfig] = useState<{
- initialised: boolean;
- userID?: string;
- }>({
- initialised: false,
- userID: client.userID,
- });
+ const { initialized: offlineDbInitialized, userId: offlineDbUserId } =
+ useStateStore(client.offlineDb?.state, selector) ?? {};
/**
* Setup muted user listener
@@ -212,30 +211,18 @@ const ChatWithContext = (props: PropsWithChildren) => {
return;
}
- const initializeDatabase = () => {
- // This acts as a lock for some very rare occurrences of concurrency
- // issues we've encountered before with the QuickSqliteClient being
- // uninitialized before it's being invoked.
- setInitialisedDatabaseConfig({ initialised: false, userID });
- SqliteClient.initializeDatabase()
- .then(async () => {
- setInitialisedDatabaseConfig({ initialised: true, userID });
- await DBSyncManager.init(client as unknown as StreamChat);
- })
- .catch((error) => {
- console.log('Error Initializing DB:', error);
- });
- };
-
- initializeDatabase();
+ const initializeDatabase = async () => {
+ if (!client.offlineDb) {
+ client.setOfflineDBApi(new OfflineDB({ client }));
+ }
- return () => {
- if (userID && enableOfflineSupport) {
- SqliteClient.closeDB();
+ if (client.offlineDb) {
+ await client.offlineDb.init(userID);
}
};
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [userID, enableOfflineSupport]);
+
+ initializeDatabase();
+ }, [userID, enableOfflineSupport, client]);
useEffect(() => {
if (!client) {
@@ -251,12 +238,7 @@ const ChatWithContext = (props: PropsWithChildren) => {
};
}, [client]);
- // In case something went wrong, make sure to also unsubscribe the listener
- // on unmount if it exists to prevent a memory leak.
- useEffect(() => () => DBSyncManager.connectionChangedListener?.unsubscribe(), []);
-
- const initialisedDatabase =
- initialisedDatabaseConfig.initialised && userID === initialisedDatabaseConfig.userID;
+ const initialisedDatabase = !!offlineDbInitialized && userID === offlineDbUserId;
const appSettings = useAppSettings(client, isOnline, enableOfflineSupport, initialisedDatabase);
@@ -273,12 +255,6 @@ const ChatWithContext = (props: PropsWithChildren) => {
setActiveChannel,
});
- useSyncDatabase({
- client,
- enableOfflineSupport,
- initialisedDatabase,
- });
-
if (userID && enableOfflineSupport && !initialisedDatabase) {
// if user id has been set and offline support is enabled, we need to wait for database to be initialised
return LoadingIndicator ? : null;
diff --git a/package/src/components/Chat/__tests__/Chat.test.js b/package/src/components/Chat/__tests__/Chat.test.js
index ca5b1172ff..44d1c049db 100644
--- a/package/src/components/Chat/__tests__/Chat.test.js
+++ b/package/src/components/Chat/__tests__/Chat.test.js
@@ -11,7 +11,6 @@ import { useTranslationContext } from '../../../contexts/translationContext/Tran
import dispatchConnectionChangedEvent from '../../../mock-builders/event/connectionChanged';
import dispatchConnectionRecoveredEvent from '../../../mock-builders/event/connectionRecovered';
import { getTestClient, getTestClientWithUser, setUser } from '../../../mock-builders/mock';
-import { DBSyncManager } from '../../../utils/DBSyncManager';
import { Streami18n } from '../../../utils/i18n/Streami18n';
import { Chat } from '../Chat';
@@ -132,10 +131,6 @@ describe('ChatContext', () => {
});
describe('TranslationContext', () => {
- beforeEach(() => {
- jest.spyOn(DBSyncManager, 'init');
- });
-
afterEach(() => {
jest.clearAllMocks();
cleanup();
@@ -240,10 +235,14 @@ describe('TranslationContext', () => {
let unsubscribeSpy;
let listenersAfterInitialMount;
+ const initSpy = jest.spyOn(chatClientWithUser.offlineDb.syncManager, 'init');
await waitFor(() => {
// the unsubscribe fn changes during init(), so we keep a reference to the spy
- unsubscribeSpy = jest.spyOn(DBSyncManager.connectionChangedListener, 'unsubscribe');
+ unsubscribeSpy = jest.spyOn(
+ chatClientWithUser.offlineDb.syncManager.connectionChangedListener,
+ 'unsubscribe',
+ );
listenersAfterInitialMount = chatClientWithUser.listeners['connection.changed'];
});
@@ -251,8 +250,8 @@ describe('TranslationContext', () => {
rerender();
await waitFor(() => {
- expect(DBSyncManager.init).toHaveBeenCalledTimes(2);
- expect(unsubscribeSpy).toHaveBeenCalledTimes(2);
+ expect(initSpy).toHaveBeenCalledTimes(1);
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(0);
expect(chatClientWithUser.listeners['connection.changed'].length).toBe(
listenersAfterInitialMount.length,
);
@@ -267,10 +266,14 @@ describe('TranslationContext', () => {
let unsubscribeSpy;
let listenersAfterInitialMount;
+ const initSpy = jest.spyOn(chatClientWithUser.offlineDb.syncManager, 'init');
await waitFor(() => {
// the unsubscribe fn changes during init(), so we keep a reference to the spy
- unsubscribeSpy = jest.spyOn(DBSyncManager.connectionChangedListener, 'unsubscribe');
+ unsubscribeSpy = jest.spyOn(
+ chatClientWithUser.offlineDb.syncManager.connectionChangedListener,
+ 'unsubscribe',
+ );
listenersAfterInitialMount = chatClientWithUser.listeners['connection.changed'];
});
@@ -282,7 +285,7 @@ describe('TranslationContext', () => {
rerender();
await waitFor(() => {
- expect(DBSyncManager.init).toHaveBeenCalledTimes(2);
+ expect(initSpy).toHaveBeenCalledTimes(2);
expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
expect(chatClientWithUser.listeners['connection.changed'].length).toBe(
listenersAfterInitialMount.length,
@@ -297,9 +300,14 @@ describe('TranslationContext', () => {
const { rerender } = render();
let unsubscribeSpy;
+ const initSpy = jest.spyOn(chatClientWithUser.offlineDb.syncManager, 'init');
+
await waitFor(() => {
// the unsubscribe fn changes during init(), so we keep a reference to the spy
- unsubscribeSpy = jest.spyOn(DBSyncManager.connectionChangedListener, 'unsubscribe');
+ unsubscribeSpy = jest.spyOn(
+ chatClientWithUser.offlineDb.syncManager.connectionChangedListener,
+ 'unsubscribe',
+ );
});
const listenersAfterInitialMount = chatClientWithUser.listeners['connection.changed'];
@@ -308,8 +316,8 @@ describe('TranslationContext', () => {
rerender();
await waitFor(() => {
- expect(DBSyncManager.init).toHaveBeenCalledTimes(1);
- expect(unsubscribeSpy).toHaveBeenCalledTimes(1);
+ expect(initSpy).toHaveBeenCalledTimes(1);
+ expect(unsubscribeSpy).toHaveBeenCalledTimes(0);
expect(chatClientWithUser.listeners['connection.changed'].length).toBe(
listenersAfterInitialMount.length,
);
diff --git a/package/src/components/Chat/hooks/handleEventToSyncDB.ts b/package/src/components/Chat/hooks/handleEventToSyncDB.ts
deleted file mode 100644
index 8f90918a46..0000000000
--- a/package/src/components/Chat/hooks/handleEventToSyncDB.ts
+++ /dev/null
@@ -1,290 +0,0 @@
-import type { Event, StreamChat } from 'stream-chat';
-
-import { deleteChannel } from '../../../store/apis/deleteChannel';
-import { deleteMember } from '../../../store/apis/deleteMember';
-import { deleteMessagesForChannel } from '../../../store/apis/deleteMessagesForChannel';
-import { updateMessage } from '../../../store/apis/updateMessage';
-import { updatePollMessage } from '../../../store/apis/updatePollMessage';
-import { upsertChannelData } from '../../../store/apis/upsertChannelData';
-import { upsertChannelDataFromChannel } from '../../../store/apis/upsertChannelDataFromChannel';
-import { upsertChannels } from '../../../store/apis/upsertChannels';
-import { upsertMembers } from '../../../store/apis/upsertMembers';
-import { upsertMessages } from '../../../store/apis/upsertMessages';
-import { upsertReads } from '../../../store/apis/upsertReads';
-import { createSelectQuery } from '../../../store/sqlite-utils/createSelectQuery';
-import { SqliteClient } from '../../../store/SqliteClient';
-import { PreparedQueries } from '../../../store/types';
-
-export const handleEventToSyncDB = async (event: Event, client: StreamChat, flush?: boolean) => {
- const { type } = event;
-
- // This function is used to guard the queries that require channel to be present in the db first
- // If channel is not present in the db, we first fetch the channel data from the channel object
- // and then add the queries with a channel create query first
- const queriesWithChannelGuard = async (
- createQueries: (flushOverride?: boolean) => Promise,
- ) => {
- const cid = event.cid || event.channel?.cid;
-
- if (!cid) {
- return await createQueries(flush);
- }
- const channels = await SqliteClient.executeSql.apply(
- null,
- createSelectQuery('channels', ['cid'], {
- cid,
- }),
- );
- // a channel is not present in the db, we first fetch the channel data from the channel object.
- // this can happen for example when a message.new event is received for a channel that is not in the db due to a channel being hidden.
- if (channels.length === 0) {
- const channel =
- event.channel_type && event.channel_id
- ? client.channel(event.channel_type, event.channel_id)
- : undefined;
- if (channel && channel.initialized && !channel.disconnected) {
- const channelQuery = await upsertChannelDataFromChannel({
- channel,
- flush,
- });
- if (channelQuery) {
- const createdQueries = await createQueries(false);
- const newQueries = [...channelQuery, ...createdQueries];
- if (flush !== false) {
- await SqliteClient.executeSqlBatch(newQueries);
- }
- return newQueries;
- } else {
- console.warn(
- `Couldnt create channel queries on ${type} event for an initialized channel that is not in DB, skipping event`,
- { event },
- );
- return [];
- }
- } else {
- console.warn(
- `Received ${type} event for a non initialized channel that is not in DB, skipping event`,
- { event },
- );
- return [];
- }
- }
- return createQueries(flush);
- };
-
- if (type === 'message.read' || type === 'notification.mark_read') {
- const cid = event.cid;
- const user = event.user;
- if (user?.id && cid) {
- return await queriesWithChannelGuard((flushOverride) =>
- upsertReads({
- cid,
- flush: flushOverride,
- reads: [
- {
- last_read: event.received_at as string,
- last_read_message_id: event.last_read_message_id,
- unread_messages: 0,
- user,
- },
- ],
- }),
- );
- }
- }
-
- if (type === 'notification.mark_unread') {
- const cid = event.cid;
- const user = event.user;
- if (user?.id && cid) {
- return await queriesWithChannelGuard((flushOverride) =>
- upsertReads({
- cid,
- flush: flushOverride,
- reads: [
- {
- last_read: event.received_at as string,
- last_read_message_id: event.last_read_message_id,
- unread_messages: event.unread_messages,
- user,
- },
- ],
- }),
- );
- }
- }
-
- if (type === 'message.new') {
- const { cid, message, user } = event;
-
- if (message && (!message.parent_id || message.show_in_channel)) {
- return await queriesWithChannelGuard(async (flushOverride) => {
- let queries = await upsertMessages({
- flush: flushOverride,
- messages: [message],
- });
- if (cid && client.user && client.user.id !== user?.id) {
- const userId = client.user.id;
- const channel = client.activeChannels[cid];
- if (channel) {
- const ownReads = channel.state.read[userId];
- const unreadCount = channel.countUnread();
- const upsertReadsQueries = await upsertReads({
- cid,
- flush: flushOverride,
- reads: [
- {
- last_read: ownReads.last_read.toString() as string,
- last_read_message_id: ownReads.last_read_message_id,
- unread_messages: unreadCount,
- user: client.user,
- },
- ],
- });
- queries = [...queries, ...upsertReadsQueries];
- }
- }
- return queries;
- });
- }
- }
-
- if (type === 'message.updated' || type === 'message.deleted') {
- const message = event.message;
- if (message && !message.parent_id) {
- // Update only if it exists, otherwise event could be related
- // to a message which is not in database.
- return await queriesWithChannelGuard((flushOverride) =>
- updateMessage({
- flush: flushOverride,
- message,
- }),
- );
- }
- }
-
- if (type === 'reaction.updated') {
- const message = event.message;
- if (message && event.reaction) {
- // We update the entire message to make sure we also update reaction_groups
- return await queriesWithChannelGuard((flushOverride) =>
- updateMessage({
- flush: flushOverride,
- message,
- }),
- );
- }
- }
-
- if (type === 'reaction.new' || type === 'reaction.deleted') {
- const message = event.message;
- if (message && !message.parent_id) {
- // Here we are relying on the fact message.latest_reactions always includes
- // the new reaction. So we first delete all the existing reactions and populate
- // the reactions table with message.latest_reactions
- return await queriesWithChannelGuard((flushOverride) =>
- updateMessage({
- flush: flushOverride,
- message,
- }),
- );
- }
- }
-
- if (
- type === 'channel.updated' ||
- type === 'channel.visible' ||
- type === 'notification.added_to_channel' ||
- type === 'notification.message_new'
- ) {
- if (event.channel) {
- return upsertChannelData({
- channel: event.channel,
- flush,
- });
- }
- }
-
- if (
- type === 'channel.hidden' ||
- type === 'channel.deleted' ||
- type === 'notification.removed_from_channel'
- ) {
- if (event.channel) {
- return deleteChannel({
- cid: event.channel.cid,
- flush,
- });
- }
- }
-
- if (type === 'channel.truncated') {
- if (event.channel) {
- return deleteMessagesForChannel({
- cid: event.channel.cid,
- flush,
- });
- }
- }
-
- if (type === 'channels.queried') {
- if (event.queriedChannels?.channels?.length) {
- return upsertChannels({
- channels: event.queriedChannels?.channels,
- flush,
- isLatestMessagesSet: event.queriedChannels?.isLatestMessageSet,
- });
- }
- }
-
- if (type === 'member.added' || type === 'member.updated') {
- const member = event.member;
- const cid = event.cid;
- if (member && cid) {
- return await queriesWithChannelGuard((flushOverride) =>
- upsertMembers({
- cid,
- flush: flushOverride,
- members: [member],
- }),
- );
- }
- }
-
- if (type === 'member.removed') {
- const member = event.member;
- const cid = event.cid;
- if (member && cid) {
- return await queriesWithChannelGuard((flushOverride) =>
- deleteMember({
- cid,
- flush: flushOverride,
- member,
- }),
- );
- }
- }
-
- if (
- [
- 'poll.closed',
- 'poll.updated',
- 'poll.vote_casted',
- 'poll.vote_changed',
- 'poll.vote_removed',
- ].includes(type)
- ) {
- const { poll, poll_vote, type } = event;
- if (poll) {
- return updatePollMessage({
- eventType: type,
- flush,
- poll,
- poll_vote,
- userID: client?.userID || '',
- });
- }
- }
-
- return [];
-};
diff --git a/package/src/components/Chat/hooks/useAppSettings.ts b/package/src/components/Chat/hooks/useAppSettings.ts
index ce61f8f0df..eae91005fa 100644
--- a/package/src/components/Chat/hooks/useAppSettings.ts
+++ b/package/src/components/Chat/hooks/useAppSettings.ts
@@ -3,7 +3,6 @@ import { useEffect, useRef, useState } from 'react';
import type { AppSettingsAPIResponse, StreamChat } from 'stream-chat';
import { useIsMountedRef } from '../../../hooks/useIsMountedRef';
-import * as dbApi from '../../../store/apis';
export const useAppSettings = (
client: StreamChat,
@@ -17,6 +16,10 @@ export const useAppSettings = (
const isMounted = useIsMountedRef();
useEffect(() => {
+ if (fetchedAppSettings.current) {
+ return;
+ }
+
const fetchAppSettings = () => {
if (appSettingsPromise.current) {
return appSettingsPromise.current;
@@ -24,43 +27,16 @@ export const useAppSettings = (
appSettingsPromise.current = client.getAppSettings();
return appSettingsPromise.current;
};
- /**
- * Fetches app settings from the backend when offline support is disabled.
- */
- const enforceAppSettingsWithoutOfflineSupport = async () => {
- if (!client.userID) {
- return;
- }
- try {
- const appSettings = await fetchAppSettings();
- if (isMounted.current) {
- setAppSettings(appSettings);
- fetchedAppSettings.current = true;
- }
- } catch (error: unknown) {
- if (error instanceof Error) {
- console.error(`An error occurred while getting app settings: ${error}`);
- }
- }
- };
+ const enforceAppSettings = async () => {
+ if (!client.userID) return;
- /**
- * Fetches app settings from the local database when offline support is enabled if internet is off else fetches from the backend.
- * Note: We need to set the app settings from the local database when offline as the client will not have the app settings in memory. For this we store it for the `client.userID`.
- *
- * TODO: Remove client.userID usage for offline support case.
- */
- const enforceAppSettingsWithOfflineSupport = async () => {
- if (!client.userID) {
- return;
- }
- if (!initialisedDatabase) {
- return;
- }
+ if (enableOfflineSupport && !initialisedDatabase) return;
+
+ const userId = client.userID as string;
- if (!isOnline) {
- const appSettings = await dbApi.getAppSettings({ currentUserId: client.userID });
+ if (!isOnline && client.offlineDb) {
+ const appSettings = await client.offlineDb.getAppSettings({ userId });
setAppSettings(appSettings);
return;
}
@@ -70,10 +46,10 @@ export const useAppSettings = (
if (isMounted.current && appSettings) {
setAppSettings(appSettings);
fetchedAppSettings.current = true;
- await dbApi.upsertAppSettings({
- appSettings,
- currentUserId: client.userID as string,
- });
+ client.offlineDb?.executeQuerySafely(
+ (db) => db.upsertAppSettings({ appSettings, userId }),
+ { method: 'upsertAppSettings' },
+ );
}
} catch (error: unknown) {
if (error instanceof Error) {
@@ -82,21 +58,8 @@ export const useAppSettings = (
}
};
- async function enforeAppSettings() {
- if (fetchedAppSettings.current) {
- return;
- }
-
- if (enableOfflineSupport) {
- await enforceAppSettingsWithOfflineSupport();
- } else {
- await enforceAppSettingsWithoutOfflineSupport();
- }
- }
-
- enforeAppSettings();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [client, isOnline, initialisedDatabase]);
+ enforceAppSettings();
+ }, [client, isOnline, initialisedDatabase, isMounted, enableOfflineSupport]);
return appSettings;
};
diff --git a/package/src/components/Chat/hooks/useSyncDatabase.ts b/package/src/components/Chat/hooks/useSyncDatabase.ts
index 10401a2f07..22645d7f37 100644
--- a/package/src/components/Chat/hooks/useSyncDatabase.ts
+++ b/package/src/components/Chat/hooks/useSyncDatabase.ts
@@ -1,25 +1,19 @@
-import { useEffect } from 'react';
-
import type { StreamChat } from 'stream-chat';
-import { handleEventToSyncDB } from './handleEventToSyncDB';
-
type Params = {
client: StreamChat;
enableOfflineSupport: boolean;
initialisedDatabase: boolean;
};
-export const useSyncDatabase = ({ client, enableOfflineSupport, initialisedDatabase }: Params) => {
- useEffect(() => {
- let listener: ReturnType | undefined;
- if (enableOfflineSupport && initialisedDatabase) {
- listener = client?.on((event) => handleEventToSyncDB(event, client));
- }
-
- return () => {
- listener?.unsubscribe();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [client, initialisedDatabase]);
-};
+/**
+ * @deprecated
+ * With the recent rework of the Offline Support feature, the handling of events has been moved
+ * to the stream-chat client instead of the SDK. This hook now does nothing and you can safely
+ * remove it from your code if you were using it. It will be removed in the next major release.
+ * @param client
+ * @param initialisedDatabase
+ */
+// @ts-ignore
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const useSyncDatabase = ({ client, initialisedDatabase }: Params) => {};
diff --git a/package/src/components/MessageMenu/MessageUserReactions.tsx b/package/src/components/MessageMenu/MessageUserReactions.tsx
index 8f53b5c6c8..4132e1a608 100644
--- a/package/src/components/MessageMenu/MessageUserReactions.tsx
+++ b/package/src/components/MessageMenu/MessageUserReactions.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
@@ -80,6 +80,12 @@ export const MessageUserReactions = (props: MessageUserReactionsProps) => {
setSelectedReaction(reactionType);
};
+ useEffect(() => {
+ if (selectedReaction && reactionTypes.length > 0 && !reactionTypes.includes(selectedReaction)) {
+ setSelectedReaction(reactionTypes[0]);
+ }
+ }, [reactionTypes, selectedReaction]);
+
const messageReactions = useMemo(
() =>
reactionTypes.reduce((acc, reaction) => {
diff --git a/package/src/components/MessageMenu/hooks/useFetchReactions.ts b/package/src/components/MessageMenu/hooks/useFetchReactions.ts
index c91411c637..b7f068847b 100644
--- a/package/src/components/MessageMenu/hooks/useFetchReactions.ts
+++ b/package/src/components/MessageMenu/hooks/useFetchReactions.ts
@@ -3,7 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { LocalMessage, ReactionResponse, ReactionSort } from 'stream-chat';
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
-import { getReactionsForFilterSort } from '../../../store/apis/getReactionsforFilterSort';
export type UseFetchReactionParams = {
limit?: number;
@@ -28,25 +27,10 @@ export const useFetchReactions = ({
const sortString = useMemo(() => JSON.stringify(sort), [sort]);
const fetchReactions = useCallback(async () => {
- const loadOfflineReactions = async () => {
- if (!messageId) {
- return;
- }
- const reactionsFromDB = await getReactionsForFilterSort({
- currentMessageId: messageId,
- filters: reactionType ? { type: reactionType } : {},
- sort,
- });
- if (reactionsFromDB) {
- setReactions(reactionsFromDB);
- setLoading(false);
- }
- };
-
- const loadOnlineReactions = async () => {
- if (!messageId) {
- return;
- }
+ if (!messageId) {
+ return;
+ }
+ try {
const response = await client.queryReactions(
messageId,
reactionType ? { type: reactionType } : {},
@@ -54,38 +38,87 @@ export const useFetchReactions = ({
{ limit, next },
);
if (response) {
+ setReactions((prevReactions) =>
+ next ? [...prevReactions, ...response.reactions] : response.reactions,
+ );
setNext(response.next);
- setReactions((prevReactions) => [...prevReactions, ...response.reactions]);
setLoading(false);
}
- };
-
- try {
- // TODO: Threads are not supported for the offline use case as we don't store the thread messages currently, and this will change in the future.
- if (enableOfflineSupport && !message?.parent_id) {
- await loadOfflineReactions();
- } else {
- await loadOnlineReactions();
- }
} catch (error) {
console.log('Error fetching reactions: ', error);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [client, messageId, reactionType, sortString, next, enableOfflineSupport]);
+ }, [client, messageId, reactionType, sortString, next, limit, enableOfflineSupport]);
const loadNextPage = useCallback(async () => {
if (next) {
await fetchReactions();
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [fetchReactions]);
+ }, [fetchReactions, next]);
useEffect(() => {
setReactions([]);
setNext(undefined);
fetchReactions();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [messageId, reactionType, sortString]);
+ }, [fetchReactions, messageId, reactionType, sortString]);
+
+ useEffect(() => {
+ const listeners: ReturnType[] = [];
+ listeners.push(
+ client.on('offline_reactions.queried', (event) => {
+ const { offlineReactions } = event;
+ if (offlineReactions) {
+ setReactions(offlineReactions);
+ setLoading(false);
+ setNext(undefined);
+ }
+ }),
+ );
+
+ listeners.push(
+ client.on('reaction.new', (event) => {
+ const { reaction } = event;
+
+ if (reaction && reaction.type === reactionType) {
+ setReactions((prevReactions) => [reaction, ...prevReactions]);
+ }
+ }),
+ );
+
+ listeners.push(
+ client.on('reaction.updated', (event) => {
+ const { reaction } = event;
+
+ if (reaction) {
+ if (reaction.type === reactionType) {
+ setReactions((prevReactions) => [reaction, ...prevReactions]);
+ } else {
+ setReactions((prevReactions) =>
+ prevReactions.filter((r) => r.user_id !== reaction.user_id),
+ );
+ }
+ }
+ }),
+ );
+
+ listeners.push(
+ client.on('reaction.deleted', (event) => {
+ const { reaction } = event;
+
+ if (reaction && reaction.type === reactionType) {
+ setReactions((prevReactions) =>
+ prevReactions.filter((r) => r.user_id !== reaction.user_id),
+ );
+ }
+ }),
+ );
+
+ return () => {
+ listeners.forEach((listener) => {
+ listener?.unsubscribe();
+ });
+ };
+ }, [client, reactionType]);
return { loading, loadNextPage, reactions };
};
diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx
index 2442ca713c..624e1c77ca 100644
--- a/package/src/contexts/messagesContext/MessagesContext.tsx
+++ b/package/src/contexts/messagesContext/MessagesContext.tsx
@@ -107,7 +107,7 @@ export type MessagesContextValue = Pick;
- deleteMessage: (message: LocalMessage) => Promise;
+ deleteMessage: (message: LocalMessage, hardDelete?: boolean) => Promise;
deleteReaction: (type: string, messageId: string) => Promise;
/** Should keyboard be dismissed when messaged is touched */
diff --git a/package/src/mock-builders/event/channelVisible.js b/package/src/mock-builders/event/channelVisible.js
new file mode 100644
index 0000000000..c74df7eed3
--- /dev/null
+++ b/package/src/mock-builders/event/channelVisible.js
@@ -0,0 +1,7 @@
+export default (client, channel = {}) => {
+ client.dispatchEvent({
+ channel,
+ cid: channel.cid,
+ type: 'channel.visible',
+ });
+};
diff --git a/package/src/mock-builders/event/memberAdded.js b/package/src/mock-builders/event/memberAdded.js
index 853a6f7b8a..b9281f98ef 100644
--- a/package/src/mock-builders/event/memberAdded.js
+++ b/package/src/mock-builders/event/memberAdded.js
@@ -1,8 +1,10 @@
export default (client, member, channel = {}) => {
client.dispatchEvent({
- channel,
+ channel_id: channel.id,
+ channel_type: channel.type,
cid: channel.cid,
member,
type: 'member.added',
+ user: member.user,
});
};
diff --git a/package/src/mock-builders/event/memberRemoved.js b/package/src/mock-builders/event/memberRemoved.js
index a7ed76556a..174f7758c0 100644
--- a/package/src/mock-builders/event/memberRemoved.js
+++ b/package/src/mock-builders/event/memberRemoved.js
@@ -4,5 +4,6 @@ export default (client, member, channel = {}) => {
cid: channel.cid,
member,
type: 'member.removed',
+ user: member.user,
});
};
diff --git a/package/src/mock-builders/event/memberUpdated.js b/package/src/mock-builders/event/memberUpdated.js
index e4562a553d..a337633f57 100644
--- a/package/src/mock-builders/event/memberUpdated.js
+++ b/package/src/mock-builders/event/memberUpdated.js
@@ -4,5 +4,6 @@ export default (client, member, channel = {}) => {
cid: channel.cid,
member,
type: 'member.updated',
+ user: member.user,
});
};
diff --git a/package/src/mock-builders/event/messageNew.js b/package/src/mock-builders/event/messageNew.js
index 391e2896a0..0453a41d52 100644
--- a/package/src/mock-builders/event/messageNew.js
+++ b/package/src/mock-builders/event/messageNew.js
@@ -6,5 +6,6 @@ export default (client, newMessage, channel = {}) => {
cid: channel.cid,
message: newMessage,
type: 'message.new',
+ ...(newMessage.user ? { user: newMessage.user } : {}),
});
};
diff --git a/package/src/mock-builders/event/messageRead.js b/package/src/mock-builders/event/messageRead.js
index 75fa523cfe..9edbab30f2 100644
--- a/package/src/mock-builders/event/messageRead.js
+++ b/package/src/mock-builders/event/messageRead.js
@@ -1,10 +1,13 @@
-export default (client, user, channel = {}) => {
+export default (client, user, channel = {}, payload = {}) => {
+ const newDate = new Date();
const event = {
channel,
cid: channel.cid,
- received_at: new Date(),
+ created_at: newDate,
+ received_at: newDate,
type: 'message.read',
user,
+ ...payload,
};
client.dispatchEvent(event);
diff --git a/package/src/mock-builders/event/notificationMarkUnread.js b/package/src/mock-builders/event/notificationMarkUnread.js
index 862802cf18..50dd0255c7 100644
--- a/package/src/mock-builders/event/notificationMarkUnread.js
+++ b/package/src/mock-builders/event/notificationMarkUnread.js
@@ -1,7 +1,10 @@
export default (client, channel = {}, payload = {}, user = {}) => {
+ const newDate = new Date();
client.dispatchEvent({
channel,
cid: channel.cid,
+ created_at: newDate,
+ received_at: newDate,
type: 'notification.mark_unread',
user,
...payload,
diff --git a/package/src/mock-builders/generator/channel.ts b/package/src/mock-builders/generator/channel.ts
index 8fde5071fb..542a2f285a 100644
--- a/package/src/mock-builders/generator/channel.ts
+++ b/package/src/mock-builders/generator/channel.ts
@@ -97,10 +97,20 @@ export const generateChannelResponse = (
channel?: Record;
id?: string;
messages?: Record[];
+ members?: Record[];
+ read?: Record[];
type?: string;
- } = { channel: {}, id: uuidv4(), messages: [], type: 'messaging' },
+ } = { channel: {}, id: uuidv4(), members: [], messages: [], read: [], type: 'messaging' },
) => {
- const { channel = {}, id = uuidv4(), messages = [], type = 'messaging', ...rest } = customValues;
+ const {
+ channel = {},
+ id = uuidv4(),
+ messages = [],
+ members = [],
+ read,
+ type = 'messaging',
+ ...rest
+ } = customValues;
const defaults = getChannelDefaults();
return {
@@ -110,12 +120,14 @@ export const generateChannelResponse = (
cid: `${type}:${id}`,
...channel,
id,
+ member_count: members.length,
type,
user: generateUser(),
},
},
- members: [],
+ members,
messages,
+ read,
...rest,
};
};
diff --git a/package/src/store/OfflineDB.ts b/package/src/store/OfflineDB.ts
new file mode 100644
index 0000000000..ee99feac04
--- /dev/null
+++ b/package/src/store/OfflineDB.ts
@@ -0,0 +1,93 @@
+import { AbstractOfflineDB, StreamChat } from 'stream-chat';
+import type {
+ DBGetAppSettingsType,
+ DBGetChannelsForQueryType,
+ DBGetChannelsType,
+ DBGetLastSyncedAtType,
+ DBUpsertAppSettingsType,
+ DBUpsertUserSyncStatusType,
+} from 'stream-chat';
+
+import * as api from './apis';
+import { SqliteClient } from './SqliteClient';
+
+export class OfflineDB extends AbstractOfflineDB {
+ constructor({ client }: { client: StreamChat }) {
+ super({ client });
+ }
+
+ upsertCidsForQuery = api.upsertCidsForQuery;
+
+ upsertChannels = api.upsertChannels;
+
+ // TODO: Rename currentUserId -> userId in the next major version as it is technically breaking.
+ upsertUserSyncStatus = ({ userId, lastSyncedAt, execute }: DBUpsertUserSyncStatusType) =>
+ api.upsertUserSyncStatus({ currentUserId: userId, execute, lastSyncedAt });
+
+ // TODO: Rename currentUserId -> userId in the next major version as it is technically breaking.
+ upsertAppSettings = ({ appSettings, userId, execute }: DBUpsertAppSettingsType) =>
+ api.upsertAppSettings({ appSettings, currentUserId: userId, execute });
+
+ upsertPoll = api.upsertPoll;
+
+ upsertChannelData = api.upsertChannelData;
+
+ upsertReads = api.upsertReads;
+
+ upsertMessages = api.upsertMessages;
+
+ upsertMembers = api.upsertMembers;
+
+ updateMessage = api.updateMessage;
+
+ // TODO: Rename currentUserId -> userId in the next major version as it is technically breaking.
+ getChannels = ({ cids, userId }: DBGetChannelsType) =>
+ api.getChannels({ channelIds: cids, currentUserId: userId });
+
+ // TODO: Rename currentUserId -> userId in the next major version as it is technically breaking.
+ getChannelsForQuery = ({ userId, filters, sort }: DBGetChannelsForQueryType) =>
+ api.getChannelsForFilterSort({ currentUserId: userId, filters, sort });
+
+ getAllChannelCids = api.getAllChannelIds;
+
+ // TODO: Rename currentUserId -> userId in the next major version as it is technically breaking.
+ getLastSyncedAt = ({ userId }: DBGetLastSyncedAtType) =>
+ api.getLastSyncedAt({ currentUserId: userId });
+
+ getAppSettings = ({ userId }: DBGetAppSettingsType) =>
+ api.getAppSettings({ currentUserId: userId });
+
+ getReactions = api.getReactionsForFilterSort;
+
+ addPendingTask = api.addPendingTask;
+
+ deletePendingTask = api.deletePendingTask;
+
+ deleteReaction = api.deleteReaction;
+
+ deleteMember = api.deleteMember;
+
+ deleteChannel = api.deleteChannel;
+
+ deleteMessagesForChannel = api.deleteMessagesForChannel;
+
+ dropPendingTasks = api.dropPendingTasks;
+
+ hardDeleteMessage = api.deleteMessage;
+
+ softDeleteMessage = api.softDeleteMessage;
+
+ getPendingTasks = api.getPendingTasks;
+
+ updateReaction = api.updateReaction;
+
+ insertReaction = api.insertReaction;
+
+ channelExists = api.channelExists;
+
+ resetDB = SqliteClient.resetDB;
+
+ executeSqlBatch = SqliteClient.executeSqlBatch;
+
+ initializeDB = SqliteClient.initializeDatabase;
+}
diff --git a/package/src/store/SqliteClient.ts b/package/src/store/SqliteClient.ts
index 50ff2f1870..d507c7a3d7 100644
--- a/package/src/store/SqliteClient.ts
+++ b/package/src/store/SqliteClient.ts
@@ -28,7 +28,7 @@ import type { PreparedBatchQueries, PreparedQueries, Scalar, Table } from './typ
* This way usage @op-engineering/op-sqlite package is scoped to a single class/file.
*/
export class SqliteClient {
- static dbVersion = 8;
+ static dbVersion = 9;
static dbName = DB_NAME;
static dbLocation = DB_LOCATION;
@@ -96,6 +96,7 @@ export class SqliteClient {
});
await this.db.executeBatch(finalQueries);
} catch (e) {
+ this.db?.execute('ROLLBACK');
this.logger?.('error', 'SqlBatch queries failed', {
error: e,
queries,
@@ -164,6 +165,7 @@ export class SqliteClient {
await SqliteClient.dropTables();
await SqliteClient.updateUserPragmaVersion(SqliteClient.dbVersion);
}
+
SqliteClient.logger?.('info', 'create tables if not exists', {
tables: Object.keys(tables),
});
@@ -176,6 +178,8 @@ export class SqliteClient {
);
await SqliteClient.executeSqlBatch(q);
+
+ return true;
} catch (e) {
console.log('Error initializing DB', e);
this.logger?.('error', 'Error initializing DB', {
@@ -183,6 +187,8 @@ export class SqliteClient {
dbname: SqliteClient.dbName,
error: e,
});
+
+ return false;
}
};
diff --git a/package/src/store/apis/addPendingTask.ts b/package/src/store/apis/addPendingTask.ts
index 25207fc30a..5d2cbc8290 100644
--- a/package/src/store/apis/addPendingTask.ts
+++ b/package/src/store/apis/addPendingTask.ts
@@ -1,8 +1,9 @@
+import type { PendingTask } from 'stream-chat';
+
import { mapTaskToStorable } from '../mappers/mapTaskToStorable';
import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { createUpsertQuery } from '../sqlite-utils/createUpsertQuery';
import { SqliteClient } from '../SqliteClient';
-import type { PendingTask } from '../types';
/*
* addPendingTask - Adds a pending task to the database
diff --git a/package/src/store/apis/channelExists.ts b/package/src/store/apis/channelExists.ts
new file mode 100644
index 0000000000..6c4a264b63
--- /dev/null
+++ b/package/src/store/apis/channelExists.ts
@@ -0,0 +1,14 @@
+import { SqliteClient } from '../SqliteClient';
+
+export const channelExists = async ({ cid }: { cid: string }) => {
+ const channels = await SqliteClient.executeSql(
+ 'SELECT EXISTS(SELECT 1 FROM channels WHERE cid = ?)',
+ [cid],
+ );
+
+ SqliteClient.logger?.('info', 'channelExists', {
+ cid,
+ });
+
+ return channels.length > 0;
+};
diff --git a/package/src/store/apis/deleteChannel.ts b/package/src/store/apis/deleteChannel.ts
index 32e2986525..bbf4d85319 100644
--- a/package/src/store/apis/deleteChannel.ts
+++ b/package/src/store/apis/deleteChannel.ts
@@ -1,17 +1,23 @@
import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { SqliteClient } from '../SqliteClient';
-export const deleteChannel = async ({ cid, flush = true }: { cid: string; flush?: boolean }) => {
+export const deleteChannel = async ({
+ cid,
+ execute = true,
+}: {
+ cid: string;
+ execute?: boolean;
+}) => {
const query = createDeleteQuery('channels', {
cid,
});
SqliteClient.logger?.('info', 'deleteChannel', {
cid,
- flush,
+ execute,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSql.apply(null, query);
}
diff --git a/package/src/store/apis/deleteMember.ts b/package/src/store/apis/deleteMember.ts
index b43bae049e..30c60315dc 100644
--- a/package/src/store/apis/deleteMember.ts
+++ b/package/src/store/apis/deleteMember.ts
@@ -5,12 +5,12 @@ import { SqliteClient } from '../SqliteClient';
export const deleteMember = async ({
cid,
- flush = true,
+ execute = true,
member,
}: {
cid: string;
member: ChannelMemberResponse;
- flush?: boolean;
+ execute?: boolean;
}) => {
const query = createDeleteQuery('members', {
cid,
@@ -19,11 +19,11 @@ export const deleteMember = async ({
SqliteClient.logger?.('info', 'deleteMember', {
cid,
- flush,
+ execute,
userId: member.user_id,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSql.apply(null, query);
}
diff --git a/package/src/store/apis/deleteMessage.ts b/package/src/store/apis/deleteMessage.ts
index 4d4b1acaae..df209c373a 100644
--- a/package/src/store/apis/deleteMessage.ts
+++ b/package/src/store/apis/deleteMessage.ts
@@ -1,19 +1,29 @@
import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { SqliteClient } from '../SqliteClient';
-export const deleteMessage = async ({ flush = true, id }: { id: string; flush?: boolean }) => {
- const query = createDeleteQuery('messages', {
- id,
- });
+export const deleteMessage = async ({ execute = true, id }: { id: string; execute?: boolean }) => {
+ const queries = [];
+
+ queries.push(
+ createDeleteQuery('reactions', {
+ messageId: id,
+ }),
+ );
+
+ queries.push(
+ createDeleteQuery('messages', {
+ id,
+ }),
+ );
SqliteClient.logger?.('info', 'deleteMessage', {
- flush,
+ execute,
id,
});
- if (flush) {
- await SqliteClient.executeSql.apply(null, query);
+ if (execute) {
+ await SqliteClient.executeSqlBatch(queries);
}
- return [query];
+ return queries;
};
diff --git a/package/src/store/apis/deleteMessagesForChannel.ts b/package/src/store/apis/deleteMessagesForChannel.ts
index 0be52db13e..99de66178a 100644
--- a/package/src/store/apis/deleteMessagesForChannel.ts
+++ b/package/src/store/apis/deleteMessagesForChannel.ts
@@ -1,23 +1,27 @@
-import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { SqliteClient } from '../SqliteClient';
export const deleteMessagesForChannel = async ({
cid,
- flush = true,
+ truncated_at,
+ execute = true,
}: {
cid: string;
- flush?: boolean;
+ truncated_at?: string;
+ execute?: boolean;
}) => {
- const query = createDeleteQuery('messages', {
- cid,
- });
+ const timestamp = truncated_at ? new Date(truncated_at).toISOString() : new Date().toISOString();
+ const query: [string, (string | number)[]] = [
+ `DELETE FROM messages WHERE cid = ? AND createdAt <= ?`,
+ [cid, timestamp],
+ ];
SqliteClient.logger?.('info', 'deleteMessagesForChannel', {
cid,
- flush,
+ execute,
+ truncated_at,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSql.apply(null, query);
}
diff --git a/package/src/store/apis/deleteReaction.ts b/package/src/store/apis/deleteReaction.ts
index 646d735357..b00789c6a8 100644
--- a/package/src/store/apis/deleteReaction.ts
+++ b/package/src/store/apis/deleteReaction.ts
@@ -1,32 +1,52 @@
+import { FormatMessageResponse, MessageResponse, ReactionResponse } from 'stream-chat';
+
import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
+import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
import { SqliteClient } from '../SqliteClient';
+import { PreparedQueries } from '../types';
export const deleteReaction = async ({
- flush = true,
- messageId,
- reactionType,
- userId,
+ execute = true,
+ message,
+ reaction,
}: {
- messageId: string;
- reactionType: string;
- userId: string;
- flush?: boolean;
+ reaction: ReactionResponse;
+ message?: MessageResponse | FormatMessageResponse;
+ execute?: boolean;
}) => {
- const query = createDeleteQuery('reactions', {
- messageId,
- type: reactionType,
- userId,
- });
+ const queries: PreparedQueries[] = [];
+
+ if (!message) {
+ return [];
+ }
+
+ queries.push(
+ createDeleteQuery('reactions', {
+ messageId: reaction.message_id,
+ type: reaction.type,
+ userId: reaction.user_id,
+ }),
+ );
+
+ const stringifiedNewReactionGroups = JSON.stringify(message.reaction_groups);
+
+ queries.push(
+ createUpdateQuery(
+ 'messages',
+ {
+ reactionGroups: stringifiedNewReactionGroups,
+ },
+ { id: message.id },
+ ),
+ );
SqliteClient.logger?.('info', 'deleteReaction', {
- messageId,
- type: reactionType,
- userId,
+ reaction,
});
- if (flush) {
- await SqliteClient.executeSql.apply(null, query);
+ if (execute) {
+ await SqliteClient.executeSqlBatch(queries);
}
- return [query];
+ return queries;
};
diff --git a/package/src/store/apis/deleteReactions.ts b/package/src/store/apis/deleteReactions.ts
index 4715679171..61a949b483 100644
--- a/package/src/store/apis/deleteReactions.ts
+++ b/package/src/store/apis/deleteReactions.ts
@@ -2,20 +2,20 @@ import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { SqliteClient } from '../SqliteClient';
export const deleteReactionsForMessage = async ({
- flush = true,
+ execute = true,
messageId,
}: {
messageId: string;
- flush?: boolean;
+ execute?: boolean;
}) => {
const query = createDeleteQuery('reactions', {
messageId,
});
console.log('deleteReactionsForMessage', {
- flush,
+ execute,
messageId,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSql.apply(null, query);
}
diff --git a/package/src/store/apis/dropPendingTasks.ts b/package/src/store/apis/dropPendingTasks.ts
new file mode 100644
index 0000000000..f9aaa2be1e
--- /dev/null
+++ b/package/src/store/apis/dropPendingTasks.ts
@@ -0,0 +1,32 @@
+import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
+import { SqliteClient } from '../SqliteClient';
+
+/**
+ * dropPendingTasks - Drops all pending tasks from the DB given a specific messageId.
+ * Useful for when we do some message actions on a failed message and then we decide to
+ * delete it afterwards, removing it from the state.
+ *
+ * @param {Object} param
+ * @param {string} param.messageId The messageId for which we want to remove the pending tasks.
+ * @param {boolean} param.execute Whether we should immediately execute the query or return it as a prepared one.
+ *
+ * @return {() => void} - A function that can be called to remove the task from the database
+ */
+export const dropPendingTasks = async ({
+ messageId,
+ execute = true,
+}: {
+ messageId: string;
+ execute?: boolean;
+}) => {
+ const queries = [createDeleteQuery('pendingTasks', { messageId })];
+ SqliteClient.logger?.('info', 'dropPendingTasks', {
+ messageId,
+ });
+
+ if (execute) {
+ await SqliteClient.executeSqlBatch(queries);
+ }
+
+ return queries;
+};
diff --git a/package/src/store/apis/getChannelMessages.ts b/package/src/store/apis/getChannelMessages.ts
index d7d34b3614..5302045e23 100644
--- a/package/src/store/apis/getChannelMessages.ts
+++ b/package/src/store/apis/getChannelMessages.ts
@@ -25,7 +25,7 @@ export const getChannelMessages = async ({
const messageIds = messageRows.map(({ id }) => id);
// Populate the message reactions.
- const reactionRows = await selectReactionsForMessages(messageIds);
+ const reactionRows = await selectReactionsForMessages(messageIds, null);
const messageIdVsReactions: Record[]> = {};
reactionRows.forEach((reaction) => {
if (!messageIdVsReactions[reaction.messageId]) {
diff --git a/package/src/store/apis/getChannels.ts b/package/src/store/apis/getChannels.ts
index 1c06f3c453..72a94e1d33 100644
--- a/package/src/store/apis/getChannels.ts
+++ b/package/src/store/apis/getChannels.ts
@@ -25,19 +25,22 @@ export const getChannels = async ({
currentUserId: string;
}): Promise[]> => {
SqliteClient.logger?.('info', 'getChannels', { channelIds, currentUserId });
- const channels = await selectChannels({ channelIds });
- const cidVsMembers = await getMembers({ channelIds });
- const cidVsReads = await getReads({ channelIds });
- const cidVsMessages = await getChannelMessages({
- channelIds,
- currentUserId,
- });
+ const [channels, cidVsMembers, cidVsReads, cidVsMessages] = await Promise.all([
+ selectChannels({ channelIds }),
+ getMembers({ channelIds }),
+ getReads({ channelIds }),
+ getChannelMessages({
+ channelIds,
+ currentUserId,
+ }),
+ ]);
// Enrich the channels with state
return channels.map((c) => ({
...mapStorableToChannel(c),
members: cidVsMembers[c.cid] || [],
+ membership: (cidVsMembers[c.cid] || []).find((member) => member.user_id === currentUserId),
messages: cidVsMessages[c.cid] || [],
pinned_messages: [],
read: cidVsReads[c.cid] || [],
diff --git a/package/src/store/apis/getLastSyncedAt.ts b/package/src/store/apis/getLastSyncedAt.ts
index f4f81d0e2c..27fbb1e412 100644
--- a/package/src/store/apis/getLastSyncedAt.ts
+++ b/package/src/store/apis/getLastSyncedAt.ts
@@ -5,7 +5,7 @@ export const getLastSyncedAt = async ({
currentUserId,
}: {
currentUserId: string;
-}): Promise => {
+}): Promise => {
SqliteClient.logger?.('info', 'getLastSyncedAt', { currentUserId });
const result = await SqliteClient.executeSql.apply(
null,
@@ -14,8 +14,5 @@ export const getLastSyncedAt = async ({
}),
);
- if (typeof result[0]?.lastSyncedAt === 'number') {
- return result[0]?.lastSyncedAt;
- }
- return undefined;
+ return result[0]?.lastSyncedAt;
};
diff --git a/package/src/store/apis/getReactionsforFilterSort.ts b/package/src/store/apis/getReactionsforFilterSort.ts
index ef7592ce0c..629ec450a5 100644
--- a/package/src/store/apis/getReactionsforFilterSort.ts
+++ b/package/src/store/apis/getReactionsforFilterSort.ts
@@ -10,15 +10,18 @@ import { SqliteClient } from '../SqliteClient';
* @param currentMessageId The message ID for which reactions are to be fetched.
* @param filters The filters to be applied while fetching reactions.
* @param sort The sort to be applied while fetching reactions.
+ * @param limit The limit of how many reactions should be returned.
*/
export const getReactionsForFilterSort = async ({
- currentMessageId,
+ messageId,
filters,
sort,
+ limit,
}: {
- currentMessageId: string;
- filters?: ReactionFilters;
+ messageId: string;
+ filters?: Pick;
sort?: ReactionSort;
+ limit?: number;
}): Promise => {
if (!filters && !sort) {
console.warn('Please provide the query (filters/sort) to fetch channels from DB');
@@ -27,7 +30,7 @@ export const getReactionsForFilterSort = async ({
SqliteClient.logger?.('info', 'getReactionsForFilterSort', { filters, sort });
- const reactions = await selectReactionsForMessages([currentMessageId]);
+ const reactions = await selectReactionsForMessages([messageId], limit, filters, sort);
if (!reactions) {
return null;
@@ -37,7 +40,5 @@ export const getReactionsForFilterSort = async ({
return [];
}
- const filteredReactions = reactions.filter((reaction) => reaction.type === filters?.type);
-
- return getReactions({ reactions: filteredReactions });
+ return getReactions({ reactions });
};
diff --git a/package/src/store/apis/index.ts b/package/src/store/apis/index.ts
index ae2740705a..a3201a5c69 100644
--- a/package/src/store/apis/index.ts
+++ b/package/src/store/apis/index.ts
@@ -8,11 +8,14 @@ export * from './getAppSettings';
export * from './getChannelMessages';
export * from './getChannels';
export * from './getChannelsForFilterSort';
+export * from './getReactionsforFilterSort';
export * from './getLastSyncedAt';
export * from './getMembers';
export * from './getReads';
export * from './updateMessage';
export * from './updateReaction';
+export * from './insertReaction';
+export * from './deleteReaction';
export * from './upsertAppSettings';
export * from './upsertChannelData';
export * from './upsertChannels';
@@ -21,4 +24,10 @@ export * from './upsertUserSyncStatus';
export * from './upsertMembers';
export * from './upsertMessages';
export * from './upsertReads';
-export * from './updatePollMessage';
+export * from './upsertPoll';
+export * from './addPendingTask';
+export * from './deletePendingTask';
+export * from './getPendingTasks';
+export * from './softDeleteMessage';
+export * from './channelExists';
+export * from './dropPendingTasks';
diff --git a/package/src/store/apis/insertReaction.ts b/package/src/store/apis/insertReaction.ts
index 1ec6eb4111..da1c2006af 100644
--- a/package/src/store/apis/insertReaction.ts
+++ b/package/src/store/apis/insertReaction.ts
@@ -7,13 +7,13 @@ import { SqliteClient } from '../SqliteClient';
import type { PreparedQueries } from '../types';
export const insertReaction = async ({
- flush = true,
+ execute = true,
message,
reaction,
}: {
message: MessageResponse | LocalMessage;
reaction: ReactionResponse;
- flush?: boolean;
+ execute?: boolean;
}) => {
const queries: PreparedQueries[] = [];
@@ -34,11 +34,11 @@ export const insertReaction = async ({
);
SqliteClient.logger?.('info', 'insertReaction', {
- flush,
+ execute,
reaction: storableReaction,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(queries);
}
diff --git a/package/src/store/apis/queries/selectMessagesForChannels.ts b/package/src/store/apis/queries/selectMessagesForChannels.ts
index c020ec2376..71bf431f10 100644
--- a/package/src/store/apis/queries/selectMessagesForChannels.ts
+++ b/package/src/store/apis/queries/selectMessagesForChannels.ts
@@ -30,7 +30,7 @@ export const selectMessagesForChannels = async (
*,
ROW_NUMBER() OVER (
PARTITION BY cid
- ORDER BY cast(strftime('%s', createdAt) AS INTEGER) DESC
+ ORDER BY createdAt DESC
) RowNum
FROM messages
WHERE cid in (${questionMarks})
@@ -39,7 +39,7 @@ export const selectMessagesForChannels = async (
users b
ON b.id = a.userId
WHERE RowNum < 200
- ORDER BY cast(strftime('%s', a.createdAt) AS INTEGER) ASC`,
+ ORDER BY a.createdAt ASC`,
cids,
);
diff --git a/package/src/store/apis/queries/selectReactionsForMessages.ts b/package/src/store/apis/queries/selectReactionsForMessages.ts
index 67f2feb219..02908582ee 100644
--- a/package/src/store/apis/queries/selectReactionsForMessages.ts
+++ b/package/src/store/apis/queries/selectReactionsForMessages.ts
@@ -1,3 +1,5 @@
+import type { ReactionFilters, ReactionSort } from 'stream-chat';
+
import { tables } from '../../schema';
import { SqliteClient } from '../../SqliteClient';
import type { TableRowJoinedUser } from '../../types';
@@ -5,9 +7,15 @@ import type { TableRowJoinedUser } from '../../types';
/**
* Fetches reactions for a message from the database for messageIds.
* @param messageIds The message IDs for which reactions are to be fetched.
+ * @param limit The limit of how many reactions should be returned.
+ * @param filters A ReactionFilter for the reactions we want to fetch. Only type is currently supported.
+ * @param sort A sort for reactions to be used when querying. Custom data is currently not supported for sorting.
*/
export const selectReactionsForMessages = async (
messageIds: string[],
+ limit: number | null = 25,
+ filters?: Pick,
+ sort?: ReactionSort,
): Promise[]> => {
const questionMarks = Array(messageIds.length).fill('?').join(',');
const reactionsColumnNames = Object.keys(tables.reactions.columns)
@@ -16,6 +24,15 @@ export const selectReactionsForMessages = async (
const userColumnNames = Object.keys(tables.users.columns)
.map((name) => `'${name}', b.${name}`)
.join(', ');
+ const filterValue = filters?.type
+ ? [typeof filters.type === 'string' ? filters.type : filters.type.$eq]
+ : [];
+ const createdAtSort = Array.isArray(sort)
+ ? sort.find((s) => !!s.created_at)?.created_at
+ : sort?.created_at;
+ const orderByClause = createdAtSort
+ ? `ORDER BY cast(strftime('%s', a.createdAt) AS INTEGER) ${createdAtSort === 1 ? 'ASC' : 'DESC'}`
+ : '';
SqliteClient.logger?.('info', 'selectReactionsForMessages', {
messageIds,
@@ -33,8 +50,10 @@ export const selectReactionsForMessages = async (
LEFT JOIN
users b
ON b.id = a.userId
- WHERE a.messageId in (${questionMarks})`,
- messageIds,
+ WHERE a.messageId in (${questionMarks}) ${filters?.type ? `AND a.type = ?` : ''}
+ ${orderByClause}
+ ${limit ? 'LIMIT ?' : ''}`,
+ [...messageIds, ...filterValue, ...(limit ? [limit] : [])],
);
return result.map((r) => JSON.parse(r.value));
diff --git a/package/src/store/apis/softDeleteMessage.ts b/package/src/store/apis/softDeleteMessage.ts
new file mode 100644
index 0000000000..e88964f358
--- /dev/null
+++ b/package/src/store/apis/softDeleteMessage.ts
@@ -0,0 +1,32 @@
+import { MessageLabel } from 'stream-chat';
+
+import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
+import { SqliteClient } from '../SqliteClient';
+
+export const softDeleteMessage = async ({
+ execute = true,
+ id,
+}: {
+ id: string;
+ execute?: boolean;
+}) => {
+ const query = createUpdateQuery(
+ 'messages',
+ {
+ deletedAt: new Date().toISOString(),
+ type: 'deleted' as MessageLabel,
+ },
+ { id },
+ );
+
+ SqliteClient.logger?.('info', 'softDeleteMessage', {
+ execute,
+ id,
+ });
+
+ if (execute) {
+ await SqliteClient.executeSql.apply(null, query);
+ }
+
+ return [query];
+};
diff --git a/package/src/store/apis/updateMessage.ts b/package/src/store/apis/updateMessage.ts
index 290a6fc794..3de2d8ba84 100644
--- a/package/src/store/apis/updateMessage.ts
+++ b/package/src/store/apis/updateMessage.ts
@@ -3,7 +3,6 @@ import type { LocalMessage, MessageResponse } from 'stream-chat';
import { mapMessageToStorable } from '../mappers/mapMessageToStorable';
import { mapReactionToStorable } from '../mappers/mapReactionToStorable';
import { mapUserToStorable } from '../mappers/mapUserToStorable';
-import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { createSelectQuery } from '../sqlite-utils/createSelectQuery';
import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
import { createUpsertQuery } from '../sqlite-utils/createUpsertQuery';
@@ -11,11 +10,11 @@ import { SqliteClient } from '../SqliteClient';
import type { PreparedQueries } from '../types';
export const updateMessage = async ({
- flush = true,
+ execute = true,
message,
}: {
message: MessageResponse | LocalMessage;
- flush?: boolean;
+ execute?: boolean;
}) => {
const queries: PreparedQueries[] = [];
@@ -48,12 +47,6 @@ export const updateMessage = async ({
queries.push(createUpsertQuery('users', storableUser));
}
- queries.push(
- createDeleteQuery('reactions', {
- messageId: message.id,
- }),
- );
-
const latestReactions = message.latest_reactions || [];
const ownReactions = message.own_reactions || [];
@@ -77,7 +70,7 @@ export const updateMessage = async ({
users: storableUsers,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(queries);
}
diff --git a/package/src/store/apis/updatePollMessage.ts b/package/src/store/apis/updatePollMessage.ts
deleted file mode 100644
index 1263a4f9bb..0000000000
--- a/package/src/store/apis/updatePollMessage.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { isVoteAnswer, PollAnswer, PollResponse, PollVote } from 'stream-chat';
-
-import { mapPollToStorable } from '../mappers/mapPollToStorable';
-import { mapStorableToPoll } from '../mappers/mapStorableToPoll';
-import { createSelectQuery } from '../sqlite-utils/createSelectQuery';
-import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
-import { SqliteClient } from '../SqliteClient';
-import type { PreparedQueries, TableRow } from '../types';
-
-export const updatePollMessage = async ({
- eventType,
- flush = true,
- poll,
- poll_vote,
- userID,
-}: {
- eventType: string;
- poll: PollResponse;
- userID: string;
- flush?: boolean;
- poll_vote?: PollVote | PollAnswer;
-}) => {
- const queries: PreparedQueries[] = [];
-
- const pollsFromDB = await SqliteClient.executeSql.apply(
- null,
- createSelectQuery('poll', ['*'], {
- id: poll.id,
- }),
- );
-
- for (const pollFromDB of pollsFromDB) {
- const serializedPoll = mapStorableToPoll(pollFromDB as unknown as TableRow<'poll'>);
- const { latest_answers = [], own_votes = [] } = serializedPoll;
- let newOwnVotes = own_votes;
- if (poll_vote && poll_vote.user?.id === userID) {
- newOwnVotes =
- eventType === 'poll.vote_removed'
- ? newOwnVotes.filter((vote) => vote.id !== poll_vote.id)
- : [poll_vote, ...newOwnVotes.filter((vote) => vote.id !== poll_vote.id)];
- }
- let newLatestAnswers = latest_answers;
- if (poll_vote && isVoteAnswer(poll_vote)) {
- newLatestAnswers =
- eventType === 'poll.vote_removed'
- ? newLatestAnswers.filter((answer) => answer.id !== poll_vote?.id)
- : [poll_vote, ...newLatestAnswers.filter((answer) => answer.id !== poll_vote?.id)];
- }
-
- const storablePoll = mapPollToStorable({
- ...poll,
- latest_answers: newLatestAnswers,
- own_votes: newOwnVotes,
- });
-
- queries.push(
- createUpdateQuery('poll', storablePoll, {
- id: poll.id,
- }),
- );
- SqliteClient.logger?.('info', 'updatePoll', {
- poll: storablePoll,
- });
- }
-
- if (flush) {
- SqliteClient.executeSqlBatch(queries);
- }
-
- return queries;
-};
diff --git a/package/src/store/apis/updateReaction.ts b/package/src/store/apis/updateReaction.ts
index ddd6ad9e50..ef919bb5c6 100644
--- a/package/src/store/apis/updateReaction.ts
+++ b/package/src/store/apis/updateReaction.ts
@@ -3,19 +3,20 @@ import type { LocalMessage, MessageResponse, ReactionResponse } from 'stream-cha
import { mapMessageToStorable } from '../mappers/mapMessageToStorable';
import { mapReactionToStorable } from '../mappers/mapReactionToStorable';
import { mapUserToStorable } from '../mappers/mapUserToStorable';
+import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery';
import { createUpsertQuery } from '../sqlite-utils/createUpsertQuery';
import { SqliteClient } from '../SqliteClient';
import type { PreparedQueries } from '../types';
export const updateReaction = async ({
- flush = true,
+ execute = true,
message,
reaction,
}: {
message: MessageResponse | LocalMessage;
reaction: ReactionResponse;
- flush?: boolean;
+ execute?: boolean;
}) => {
const queries: PreparedQueries[] = [];
let storableUser: ReturnType | undefined;
@@ -28,11 +29,12 @@ export const updateReaction = async ({
const storableReaction = mapReactionToStorable(reaction);
queries.push(
- createUpdateQuery('reactions', storableReaction, {
+ createDeleteQuery('reactions', {
messageId: reaction.message_id,
userId: reaction.user_id,
}),
);
+ queries.push(createUpsertQuery('reactions', storableReaction));
let updatedReactionGroups: string | undefined;
if (message.reaction_groups) {
@@ -43,12 +45,12 @@ export const updateReaction = async ({
SqliteClient.logger?.('info', 'updateReaction', {
addedUser: storableUser,
- flush,
+ execute,
updatedReaction: storableReaction,
updatedReactionGroups,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(queries);
}
diff --git a/package/src/store/apis/upsertAppSettings.ts b/package/src/store/apis/upsertAppSettings.ts
index fd43e0e69f..dd51536958 100644
--- a/package/src/store/apis/upsertAppSettings.ts
+++ b/package/src/store/apis/upsertAppSettings.ts
@@ -6,25 +6,29 @@ import { SqliteClient } from '../SqliteClient';
export const upsertAppSettings = async ({
appSettings,
currentUserId,
- flush = true,
+ execute = true,
}: {
appSettings: AppSettingsAPIResponse;
currentUserId: string;
- flush?: boolean;
+ execute?: boolean;
}) => {
const storableAppSettings = JSON.stringify(appSettings);
- const query = createUpsertQuery('userSyncStatus', {
- appSettings: storableAppSettings,
- userId: currentUserId,
- });
+ const queries = [
+ createUpsertQuery('userSyncStatus', {
+ appSettings: storableAppSettings,
+ userId: currentUserId,
+ }),
+ ];
SqliteClient.logger?.('info', 'upsertAppSettings', {
appSettings: storableAppSettings,
- flush,
+ execute,
userId: currentUserId,
});
- if (flush) {
- await SqliteClient.executeSql.apply(null, query);
+ if (execute) {
+ await SqliteClient.executeSqlBatch(queries);
}
+
+ return queries;
};
diff --git a/package/src/store/apis/upsertChannelData.ts b/package/src/store/apis/upsertChannelData.ts
index b551171032..79ae222907 100644
--- a/package/src/store/apis/upsertChannelData.ts
+++ b/package/src/store/apis/upsertChannelData.ts
@@ -6,19 +6,19 @@ import { SqliteClient } from '../SqliteClient';
export const upsertChannelData = async ({
channel,
- flush = true,
+ execute = true,
}: {
channel: ChannelResponse;
- flush?: boolean;
+ execute?: boolean;
}) => {
const storableChannel = mapChannelDataToStorable(channel);
const query = createUpsertQuery('channels', storableChannel);
SqliteClient.logger?.('info', 'upsertChannelData', {
channel: storableChannel,
- flush,
+ execute,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch([query]);
}
diff --git a/package/src/store/apis/upsertChannelDataFromChannel.ts b/package/src/store/apis/upsertChannelDataFromChannel.ts
index c65d1ca126..002395ed1f 100644
--- a/package/src/store/apis/upsertChannelDataFromChannel.ts
+++ b/package/src/store/apis/upsertChannelDataFromChannel.ts
@@ -6,17 +6,17 @@ import { SqliteClient } from '../SqliteClient';
export const upsertChannelDataFromChannel = async ({
channel,
- flush = true,
+ execute = true,
}: {
channel: Channel;
- flush?: boolean;
+ execute?: boolean;
}) => {
const storableChannel = mapChannelToStorable(channel);
if (!storableChannel) {
return;
}
const query = createUpsertQuery('channels', storableChannel);
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch([query]);
}
diff --git a/package/src/store/apis/upsertChannels.ts b/package/src/store/apis/upsertChannels.ts
index 31a158197a..26cc87bf77 100644
--- a/package/src/store/apis/upsertChannels.ts
+++ b/package/src/store/apis/upsertChannels.ts
@@ -1,6 +1,5 @@
-import type { ChannelAPIResponse, ChannelFilters, ChannelSort } from 'stream-chat';
+import type { ChannelAPIResponse, ChannelMemberResponse } from 'stream-chat';
-import { upsertCidsForQuery } from './upsertCidsForQuery';
import { upsertMembers } from './upsertMembers';
import { upsertMessages } from './upsertMessages';
@@ -13,16 +12,12 @@ import type { PreparedQueries } from '../types';
export const upsertChannels = async ({
channels,
- filters,
- flush = true,
+ execute = true,
isLatestMessagesSet,
- sort,
}: {
channels: ChannelAPIResponse[];
- filters?: ChannelFilters;
- flush?: boolean;
+ execute?: boolean;
isLatestMessagesSet?: boolean;
- sort?: ChannelSort;
}) => {
// Update the database only if the query is provided.
let queries: PreparedQueries[] = [];
@@ -33,25 +28,20 @@ export const upsertChannels = async ({
channelIds,
});
- if (filters || sort) {
- queries = queries.concat(
- await upsertCidsForQuery({
- cids: channelIds,
- filters,
- flush: false,
- sort,
- }),
- );
- }
-
for (const channel of channels) {
queries.push(createUpsertQuery('channels', mapChannelDataToStorable(channel.channel)));
- const { members, messages, read } = channel;
+ const { members, membership, messages, read } = channel;
+ if (
+ membership &&
+ !members.includes((m: ChannelMemberResponse) => m.user?.id === membership.user?.id)
+ ) {
+ members.push({ ...membership, user_id: membership.user?.id });
+ }
queries = queries.concat(
await upsertMembers({
cid: channel.channel.cid,
- flush: false,
+ execute: false,
members,
}),
);
@@ -60,7 +50,7 @@ export const upsertChannels = async ({
queries = queries.concat(
await upsertReads({
cid: channel.channel.cid,
- flush: false,
+ execute: false,
reads: read,
}),
);
@@ -69,14 +59,14 @@ export const upsertChannels = async ({
if (isLatestMessagesSet) {
queries = queries.concat(
await upsertMessages({
- flush: false,
+ execute: false,
messages,
}),
);
}
}
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(queries);
}
diff --git a/package/src/store/apis/upsertCidsForQuery.ts b/package/src/store/apis/upsertCidsForQuery.ts
index 4bc4b784c5..3d388a2978 100644
--- a/package/src/store/apis/upsertCidsForQuery.ts
+++ b/package/src/store/apis/upsertCidsForQuery.ts
@@ -8,12 +8,12 @@ import { SqliteClient } from '../SqliteClient';
export const upsertCidsForQuery = async ({
cids,
filters,
- flush = true,
+ execute = true,
sort,
}: {
cids: string[];
filters?: ChannelFilters;
- flush?: boolean;
+ execute?: boolean;
sort?: ChannelSort;
}) => {
// Update the database only if the query is provided.
@@ -26,11 +26,11 @@ export const upsertCidsForQuery = async ({
SqliteClient.logger?.('info', 'upsertCidsForQuery', {
cids: cidsString,
- flush,
+ execute,
id,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSql.apply(null, query);
}
diff --git a/package/src/store/apis/upsertMembers.ts b/package/src/store/apis/upsertMembers.ts
index 7047390979..6a48205417 100644
--- a/package/src/store/apis/upsertMembers.ts
+++ b/package/src/store/apis/upsertMembers.ts
@@ -8,12 +8,12 @@ import type { PreparedQueries } from '../types';
export const upsertMembers = async ({
cid,
- flush = true,
+ execute = true,
members,
}: {
cid: string;
members: ChannelMemberResponse[];
- flush?: boolean;
+ execute?: boolean;
}) => {
const queries: PreparedQueries[] = [];
@@ -34,12 +34,12 @@ export const upsertMembers = async ({
SqliteClient.logger?.('info', 'upsertMembers', {
cid,
- flush,
+ execute,
storableMembers,
storableUsers,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(queries);
}
diff --git a/package/src/store/apis/upsertMessages.ts b/package/src/store/apis/upsertMessages.ts
index 7bcc007a51..84a13e22aa 100644
--- a/package/src/store/apis/upsertMessages.ts
+++ b/package/src/store/apis/upsertMessages.ts
@@ -8,11 +8,11 @@ import { createUpsertQuery } from '../sqlite-utils/createUpsertQuery';
import { SqliteClient } from '../SqliteClient';
export const upsertMessages = async ({
- flush = true,
+ execute = true,
messages,
}: {
messages: MessageResponse[];
- flush?: boolean;
+ execute?: boolean;
}) => {
const storableMessages: Array> = [];
const storableUsers: Array> = [];
@@ -45,14 +45,14 @@ export const upsertMessages = async ({
];
SqliteClient.logger?.('info', 'upsertMessages', {
- flush,
+ execute,
messages: storableMessages,
polls: storablePolls,
reactions: storableReactions,
users: storableUsers,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(finalQueries);
}
diff --git a/package/src/store/apis/upsertPoll.ts b/package/src/store/apis/upsertPoll.ts
new file mode 100644
index 0000000000..0cd43900fc
--- /dev/null
+++ b/package/src/store/apis/upsertPoll.ts
@@ -0,0 +1,29 @@
+import { PollResponse } from 'stream-chat';
+
+import { mapPollToStorable } from '../mappers/mapPollToStorable';
+import { createUpsertQuery } from '../sqlite-utils/createUpsertQuery';
+import { SqliteClient } from '../SqliteClient';
+import type { PreparedQueries } from '../types';
+
+export const upsertPoll = async ({
+ execute = true,
+ poll,
+}: {
+ poll: PollResponse;
+ execute?: boolean;
+}) => {
+ const queries: PreparedQueries[] = [];
+
+ const storablePoll = mapPollToStorable(poll);
+
+ queries.push(createUpsertQuery('poll', storablePoll));
+ SqliteClient.logger?.('info', 'upsertPoll', {
+ poll: storablePoll,
+ });
+
+ if (execute) {
+ await SqliteClient.executeSqlBatch(queries);
+ }
+
+ return queries;
+};
diff --git a/package/src/store/apis/upsertReads.ts b/package/src/store/apis/upsertReads.ts
index 7d3fbd13cb..92d4c15145 100644
--- a/package/src/store/apis/upsertReads.ts
+++ b/package/src/store/apis/upsertReads.ts
@@ -8,12 +8,12 @@ import type { PreparedQueries } from '../types';
export const upsertReads = async ({
cid,
- flush = true,
+ execute = true,
reads,
}: {
cid: string;
reads: ReadResponse[];
- flush?: boolean;
+ execute?: boolean;
}) => {
const queries: PreparedQueries[] = [];
@@ -31,12 +31,12 @@ export const upsertReads = async ({
queries.push(...storableReads.map((storableRead) => createUpsertQuery('reads', storableRead)));
SqliteClient.logger?.('info', 'upsertReads', {
- flush,
+ execute,
reads: storableReads,
users: storableUsers,
});
- if (flush) {
+ if (execute) {
await SqliteClient.executeSqlBatch(queries);
}
diff --git a/package/src/store/apis/upsertUserSyncStatus.ts b/package/src/store/apis/upsertUserSyncStatus.ts
index f4473d8d60..9e19cfc531 100644
--- a/package/src/store/apis/upsertUserSyncStatus.ts
+++ b/package/src/store/apis/upsertUserSyncStatus.ts
@@ -4,19 +4,27 @@ import { SqliteClient } from '../SqliteClient';
export const upsertUserSyncStatus = async ({
currentUserId,
lastSyncedAt,
+ execute = true,
}: {
currentUserId: string;
lastSyncedAt: string;
+ execute?: boolean;
}) => {
- const query = createUpsertQuery('userSyncStatus', {
- lastSyncedAt,
- userId: currentUserId,
- });
+ const queries = [
+ createUpsertQuery('userSyncStatus', {
+ lastSyncedAt,
+ userId: currentUserId,
+ }),
+ ];
SqliteClient.logger?.('info', 'upsertUserSyncStatus', {
lastSyncedAt,
userId: currentUserId,
});
- await SqliteClient.executeSql.apply(null, query);
+ if (execute) {
+ await SqliteClient.executeSqlBatch(queries);
+ }
+
+ return queries;
};
diff --git a/package/src/store/mappers/mapMemberToStorable.ts b/package/src/store/mappers/mapMemberToStorable.ts
index ed1b492daf..1b90e4bc29 100644
--- a/package/src/store/mappers/mapMemberToStorable.ts
+++ b/package/src/store/mappers/mapMemberToStorable.ts
@@ -12,6 +12,7 @@ export const mapMemberToStorable = ({
member: ChannelMemberResponse;
}): TableRow<'members'> => {
const {
+ archived_at,
banned,
channel_role,
created_at,
@@ -23,9 +24,11 @@ export const mapMemberToStorable = ({
shadow_banned,
updated_at,
user_id,
+ pinned_at,
} = member;
return {
+ archivedAt: mapDateTimeToStorable(archived_at),
banned,
channelRole: channel_role,
cid,
@@ -34,6 +37,7 @@ export const mapMemberToStorable = ({
invited,
inviteRejectedAt: invite_rejected_at,
isModerator: is_moderator,
+ pinnedAt: mapDateTimeToStorable(pinned_at),
role,
shadowBanned: shadow_banned,
updatedAt: mapDateTimeToStorable(updated_at),
diff --git a/package/src/store/mappers/mapReadToStorable.ts b/package/src/store/mappers/mapReadToStorable.ts
index 0e261be04d..42e6f01bbd 100644
--- a/package/src/store/mappers/mapReadToStorable.ts
+++ b/package/src/store/mappers/mapReadToStorable.ts
@@ -11,11 +11,12 @@ export const mapReadToStorable = ({
cid: string;
read: ReadResponse;
}): TableRow<'reads'> => {
- const { last_read, unread_messages, user } = read;
+ const { last_read, unread_messages, user, last_read_message_id } = read;
return {
cid,
lastRead: mapDateTimeToStorable(last_read),
+ lastReadMessageId: last_read_message_id,
unreadMessages: unread_messages,
userId: user?.id,
};
diff --git a/package/src/store/mappers/mapStorableToMember.ts b/package/src/store/mappers/mapStorableToMember.ts
index ee5233547f..35c26259b4 100644
--- a/package/src/store/mappers/mapStorableToMember.ts
+++ b/package/src/store/mappers/mapStorableToMember.ts
@@ -8,6 +8,7 @@ export const mapStorableToMember = (
memberRow: TableRowJoinedUser<'members'>,
): ChannelMemberResponse => {
const {
+ archivedAt,
banned,
channelRole,
createdAt,
@@ -15,6 +16,7 @@ export const mapStorableToMember = (
invited,
inviteRejectedAt,
isModerator,
+ pinnedAt,
role,
shadowBanned,
updatedAt,
@@ -23,6 +25,7 @@ export const mapStorableToMember = (
} = memberRow;
return {
+ archived_at: archivedAt,
banned,
channel_role: channelRole,
created_at: createdAt,
@@ -30,6 +33,7 @@ export const mapStorableToMember = (
invite_rejected_at: inviteRejectedAt,
invited,
is_moderator: isModerator,
+ pinned_at: pinnedAt,
role,
shadow_banned: shadowBanned,
updated_at: updatedAt,
diff --git a/package/src/store/mappers/mapStorableToRead.ts b/package/src/store/mappers/mapStorableToRead.ts
index 448aecea5b..17c8fe1ddc 100644
--- a/package/src/store/mappers/mapStorableToRead.ts
+++ b/package/src/store/mappers/mapStorableToRead.ts
@@ -5,10 +5,11 @@ import { mapStorableToUser } from './mapStorableToUser';
import type { TableRowJoinedUser } from '../types';
export const mapStorableToRead = (row: TableRowJoinedUser<'reads'>): ReadResponse => {
- const { lastRead, unreadMessages, user } = row;
+ const { lastRead, unreadMessages, user, lastReadMessageId } = row;
return {
last_read: lastRead,
+ last_read_message_id: lastReadMessageId,
unread_messages: unreadMessages,
user: mapStorableToUser(user),
};
diff --git a/package/src/store/mappers/mapStorableToTask.ts b/package/src/store/mappers/mapStorableToTask.ts
index cf55a66418..c7eff1c117 100644
--- a/package/src/store/mappers/mapStorableToTask.ts
+++ b/package/src/store/mappers/mapStorableToTask.ts
@@ -1,4 +1,6 @@
-import type { PendingTask, TableRowJoinedUser } from '../types';
+import { PendingTask } from 'stream-chat';
+
+import type { TableRowJoinedUser } from '../types';
export const mapStorableToTask = (row: TableRowJoinedUser<'pendingTasks'>): PendingTask => {
const { channelId, channelType, id, messageId, type } = row;
diff --git a/package/src/store/mappers/mapTaskToStorable.ts b/package/src/store/mappers/mapTaskToStorable.ts
index 3f448370ce..3baf24fceb 100644
--- a/package/src/store/mappers/mapTaskToStorable.ts
+++ b/package/src/store/mappers/mapTaskToStorable.ts
@@ -1,4 +1,4 @@
-import type { PendingTask } from '../types';
+import type { PendingTask } from 'stream-chat';
export const mapTaskToStorable = (task: PendingTask) => ({
...task,
diff --git a/package/src/store/schema.ts b/package/src/store/schema.ts
index bcfbf723f1..43b451e5e8 100644
--- a/package/src/store/schema.ts
+++ b/package/src/store/schema.ts
@@ -1,6 +1,4 @@
-import type { MessageLabel, Role } from 'stream-chat';
-
-import type { PendingTaskTypes } from './types';
+import type { MessageLabel, PendingTaskTypes, Role } from 'stream-chat';
import type { ValueOf } from '../types/types';
@@ -64,6 +62,7 @@ export const tables: Tables = {
},
members: {
columns: {
+ archivedAt: 'TEXT',
banned: 'BOOLEAN DEFAULT FALSE',
channelRole: 'TEXT',
cid: 'TEXT NOT NULL',
@@ -72,6 +71,7 @@ export const tables: Tables = {
invited: 'BOOLEAN',
inviteRejectedAt: 'TEXT',
isModerator: 'BOOLEAN',
+ pinnedAt: 'TEXT',
role: 'TEXT',
shadowBanned: 'BOOLEAN DEFAULT FALSE',
updatedAt: 'TEXT',
@@ -270,6 +270,7 @@ export type Schema = {
updatedAt?: string;
};
members: {
+ archivedAt?: string;
cid: string;
banned?: boolean;
channelRole?: Role;
@@ -282,6 +283,7 @@ export type Schema = {
shadowBanned?: boolean;
updatedAt?: string;
userId?: string;
+ pinnedAt?: string;
};
messages: {
attachments: string;
diff --git a/package/src/utils/DBSyncManager.ts b/package/src/utils/DBSyncManager.ts
deleted file mode 100644
index d4daffe65b..0000000000
--- a/package/src/utils/DBSyncManager.ts
+++ /dev/null
@@ -1,242 +0,0 @@
-import type { AxiosError } from 'axios';
-import dayjs from 'dayjs';
-import type { APIErrorResponse, StreamChat } from 'stream-chat';
-
-import { handleEventToSyncDB } from '../components/Chat/hooks/handleEventToSyncDB';
-import { getAllChannelIds, getLastSyncedAt, upsertUserSyncStatus } from '../store/apis';
-
-import { addPendingTask } from '../store/apis/addPendingTask';
-
-import { deletePendingTask } from '../store/apis/deletePendingTask';
-import { getPendingTasks } from '../store/apis/getPendingTasks';
-import { SqliteClient } from '../store/SqliteClient';
-import type { PendingTask } from '../store/types';
-
-/**
- * DBSyncManager has the responsibility to sync the channel states
- * within local database whenever possible.
- *
- * Components can get the current sync status using DBSyncManager.getCurrentStatus().
- * Or components can attach a listener for status change as following:
- *
- * ```tsx
- * useEffect(() => {
- * const unsubscribe = DBSyncManager.onSyncStatusChange((syncStatus) => {
- * if (syncStatus) {
- * doSomething();
- * }
- * })
- *
- * return () => unsubscribe();
- * })
- * ```
- */
-const restBeforeNextTask = () => new Promise((resolve) => setTimeout(resolve, 500));
-
-export class DBSyncManager {
- static syncStatus = false;
- static listeners: Array<(status: boolean) => void> = [];
- static client: StreamChat | null = null;
- static connectionChangedListener: { unsubscribe: () => void } | null = null;
-
- /**
- * Returns weather channel states in local DB are synced with backend or not.
- * @returns boolean
- */
- static getSyncStatus = () => this.syncStatus;
-
- /**
- * Initializes the DBSyncManager. This function should be called only once
- * throughout the lifetime of SDK.
- *
- * @param client
- */
- static init = async (client: StreamChat) => {
- try {
- this.client = client;
- // If the websocket connection is already active, then straightaway
- // call the sync api and also execute pending api calls.
- // Otherwise wait for `connection.changed` event.
- if (client.user?.id && client.wsConnection?.isHealthy) {
- await this.syncAndExecutePendingTasks();
- this.syncStatus = true;
- this.listeners.forEach((l) => l(true));
- }
-
- // If a listener has already been registered, unsubscribe from it so
- // that it can be reinstated. This can happen if we reconnect with a
- // different user or the component invoking the init() function gets
- // unmounted and then remounted again. This part of the code makes
- // sure the stale listener doesn't produce a memory leak.
- if (this.connectionChangedListener) {
- this.connectionChangedListener.unsubscribe();
- }
-
- this.connectionChangedListener = this.client.on('connection.changed', async (event) => {
- if (event.online) {
- await this.syncAndExecutePendingTasks();
- this.syncStatus = true;
- this.listeners.forEach((l) => l(true));
- } else {
- this.syncStatus = false;
- this.listeners.forEach((l) => l(false));
- }
- });
- } catch (error) {
- console.log('Error in DBSyncManager.init: ', error);
- }
- };
-
- /**
- * Subscribes a listener for sync status change.
- *
- * @param listener {function}
- * @returns {function} to unsubscribe the listener.
- */
- static onSyncStatusChange = (listener: (status: boolean) => void) => {
- this.listeners.push(listener);
-
- return {
- unsubscribe: () => {
- this.listeners = this.listeners.filter((el) => el !== listener);
- },
- };
- };
-
- static sync = async (client: StreamChat) => {
- if (!this.client?.user) {
- return;
- }
- const cids = await getAllChannelIds();
- // If there are no channels, then there is no need to sync.
- if (cids.length === 0) {
- return;
- }
-
- const lastSyncedAt = await getLastSyncedAt({
- currentUserId: this.client.user.id,
- });
-
- if (lastSyncedAt) {
- const lastSyncedAtDate = new Date(lastSyncedAt);
- const lastSyncedAtDayJs = dayjs(lastSyncedAtDate);
- const nowDayJs = dayjs();
- const diff = nowDayJs.diff(lastSyncedAtDayJs, 'days');
- if (diff > 30) {
- // stream backend will send an error if we try to sync after 30 days.
- // In that case reset the entire DB and start fresh.
- await SqliteClient.resetDB();
- } else {
- try {
- const result = await this.client.sync(cids, lastSyncedAtDate.toISOString());
- const queryPromises = result.events.map(
- async (event) => await handleEventToSyncDB(event, client),
- );
- const queriesArray = await Promise.all(queryPromises);
- const queries = queriesArray.flat();
-
- if (queries.length) {
- await SqliteClient.executeSqlBatch(queries);
- }
- } catch (e) {
- // Error will be raised by the sync API if there are too many events.
- // In that case reset the entire DB and start fresh.
- await SqliteClient.resetDB();
- }
- }
- }
- await upsertUserSyncStatus({
- currentUserId: this.client.user.id,
- lastSyncedAt: new Date().toString(),
- });
- };
-
- static syncAndExecutePendingTasks = async () => {
- if (!this.client) {
- return;
- }
-
- await this.executePendingTasks(this.client);
- await this.sync(this.client);
- };
-
- static queueTask = async ({ client, task }: { client: StreamChat; task: PendingTask }) => {
- const removeFromApi = await addPendingTask(task);
-
- let response;
- try {
- response = await this.executeTask({ client, task });
- } catch (e) {
- if ((e as AxiosError)?.response?.data?.code === 4) {
- // Error code 16 - message already exists
- // ignore
- } else {
- throw e;
- }
- }
-
- await removeFromApi();
-
- return response;
- };
-
- static executeTask = async ({ client, task }: { client: StreamChat; task: PendingTask }) => {
- const channel = client.channel(task.channelType, task.channelId);
-
- if (task.type === 'send-reaction') {
- return await channel.sendReaction(...task.payload);
- }
-
- if (task.type === 'delete-reaction') {
- return await channel.deleteReaction(...task.payload);
- }
-
- if (task.type === 'delete-message') {
- return await client.deleteMessage(...task.payload);
- }
-
- throw new Error('Invalid task type');
- };
-
- static executePendingTasks = async (client: StreamChat) => {
- const queue = await getPendingTasks();
- for (const task of queue) {
- if (!task.id) {
- continue;
- }
-
- try {
- await this.executeTask({
- client,
- task,
- });
- } catch (e) {
- if ((e as AxiosError)?.response?.data?.code === 4) {
- // Error code 16 - message already exists
- // ignore
- } else {
- throw e;
- }
- }
-
- await deletePendingTask({
- id: task.id,
- });
- await restBeforeNextTask();
- }
- };
-
- static dropPendingTasks = async (conditions: { messageId: string }) => {
- const tasks = await getPendingTasks(conditions);
-
- for (const task of tasks) {
- if (!task.id) {
- continue;
- }
-
- await deletePendingTask({
- id: task.id,
- });
- }
- };
-}
diff --git a/package/src/utils/addReactionToLocalState.ts b/package/src/utils/addReactionToLocalState.ts
index 694dc42b21..bf99946fd2 100644
--- a/package/src/utils/addReactionToLocalState.ts
+++ b/package/src/utils/addReactionToLocalState.ts
@@ -1,9 +1,8 @@
import type { Channel, ReactionResponse, UserResponse } from 'stream-chat';
-import { updateReaction } from '../store/apis';
-import { insertReaction } from '../store/apis/insertReaction';
+import { insertReaction, updateReaction } from '../store/apis';
-export const addReactionToLocalState = ({
+export const addReactionToLocalState = async ({
channel,
enforceUniqueReaction,
messageId,
@@ -32,74 +31,21 @@ export const addReactionToLocalState = ({
};
const hasOwnReaction = message.own_reactions && message.own_reactions.length > 0;
- if (!message.own_reactions) {
- message.own_reactions = [];
- }
-
- if (!message.latest_reactions) {
- message.latest_reactions = [];
- }
- if (enforceUniqueReaction) {
- const currentReaction = message.own_reactions[0];
- message.own_reactions = [];
- if (!message.latest_reactions) {
- message.latest_reactions = [];
- }
- message.latest_reactions = message.latest_reactions.filter((r) => r.user_id !== user.id);
+ const messageWithReaction = channel.state.addReaction(reaction, undefined, enforceUniqueReaction);
- if (
- currentReaction &&
- message.reaction_groups &&
- message.reaction_groups[currentReaction.type] &&
- message.reaction_groups[currentReaction.type].count > 0 &&
- message.reaction_groups[currentReaction.type].sum_scores > 0
- ) {
- message.reaction_groups[currentReaction.type].count =
- message.reaction_groups[currentReaction.type].count - 1;
- message.reaction_groups[currentReaction.type].sum_scores =
- message.reaction_groups[currentReaction.type].sum_scores - 1;
- }
-
- if (!message.reaction_groups) {
- message.reaction_groups = {
- [reactionType]: {
- count: 1,
- first_reaction_at: new Date().toISOString(),
- last_reaction_at: new Date().toISOString(),
- sum_scores: 1,
- },
- };
- } else {
- if (!message.reaction_groups[reactionType]) {
- message.reaction_groups[reactionType] = {
- count: 1,
- first_reaction_at: new Date().toISOString(),
- last_reaction_at: new Date().toISOString(),
- sum_scores: 1,
- };
- } else {
- message.reaction_groups[reactionType] = {
- ...message.reaction_groups[reactionType],
- count: message.reaction_groups[reactionType].count + 1,
- last_reaction_at: new Date().toISOString(),
- sum_scores: message.reaction_groups[reactionType].sum_scores + 1,
- };
- }
- }
+ if (!messageWithReaction) {
+ return;
}
- message.own_reactions = [...message.own_reactions, reaction];
- message.latest_reactions = [...message.latest_reactions, reaction];
-
if (enforceUniqueReaction && hasOwnReaction) {
- updateReaction({
- message,
+ await updateReaction({
+ message: messageWithReaction,
reaction,
});
} else {
- insertReaction({
- message,
+ await insertReaction({
+ message: messageWithReaction,
reaction,
});
}
diff --git a/package/src/utils/removeReactionFromLocalState.ts b/package/src/utils/removeReactionFromLocalState.ts
index b5cdba3913..2caf00be1a 100644
--- a/package/src/utils/removeReactionFromLocalState.ts
+++ b/package/src/utils/removeReactionFromLocalState.ts
@@ -1,7 +1,5 @@
import type { Channel, UserResponse } from 'stream-chat';
-import { deleteReaction } from '../store/apis/deleteReaction';
-
export const removeReactionFromLocalState = ({
channel,
messageId,
@@ -48,10 +46,4 @@ export const removeReactionFromLocalState = ({
delete message.reaction_groups[reactionType];
}
}
-
- deleteReaction({
- messageId,
- reactionType,
- userId: user.id,
- });
};
diff --git a/package/yarn.lock b/package/yarn.lock
index d52740d3f6..9547afacf7 100644
--- a/package/yarn.lock
+++ b/package/yarn.lock
@@ -2112,16 +2112,16 @@
dependencies:
"@types/yargs-parser" "*"
-"@typescript-eslint/eslint-plugin@8.25.0":
- version "8.25.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz#5e1d56f067e5808fa82d1b75bced82396e868a14"
- integrity sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==
+"@typescript-eslint/eslint-plugin@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.0.tgz#151c4878700a5ad229ce6713d2674d58b626b3d9"
+ integrity sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==
dependencies:
"@eslint-community/regexpp" "^4.10.0"
- "@typescript-eslint/scope-manager" "8.25.0"
- "@typescript-eslint/type-utils" "8.25.0"
- "@typescript-eslint/utils" "8.25.0"
- "@typescript-eslint/visitor-keys" "8.25.0"
+ "@typescript-eslint/scope-manager" "8.29.0"
+ "@typescript-eslint/type-utils" "8.29.0"
+ "@typescript-eslint/utils" "8.29.0"
+ "@typescript-eslint/visitor-keys" "8.29.0"
graphemer "^1.4.0"
ignore "^5.3.1"
natural-compare "^1.4.0"
@@ -2143,15 +2143,15 @@
semver "^7.3.7"
tsutils "^3.21.0"
-"@typescript-eslint/parser@8.25.0":
- version "8.25.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.25.0.tgz#58fb81c7b7a35184ba17583f3d7ac6c4f3d95be8"
- integrity sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==
+"@typescript-eslint/parser@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.29.0.tgz#b98841e0a8099728cb8583da92326fcb7f5be1d2"
+ integrity sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==
dependencies:
- "@typescript-eslint/scope-manager" "8.25.0"
- "@typescript-eslint/types" "8.25.0"
- "@typescript-eslint/typescript-estree" "8.25.0"
- "@typescript-eslint/visitor-keys" "8.25.0"
+ "@typescript-eslint/scope-manager" "8.29.0"
+ "@typescript-eslint/types" "8.29.0"
+ "@typescript-eslint/typescript-estree" "8.29.0"
+ "@typescript-eslint/visitor-keys" "8.29.0"
debug "^4.3.4"
"@typescript-eslint/parser@^5.30.5":
@@ -2180,6 +2180,14 @@
"@typescript-eslint/types" "8.25.0"
"@typescript-eslint/visitor-keys" "8.25.0"
+"@typescript-eslint/scope-manager@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.29.0.tgz#8fd9872823aef65ff71d3f6d1ec9316ace0b6bf3"
+ integrity sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==
+ dependencies:
+ "@typescript-eslint/types" "8.29.0"
+ "@typescript-eslint/visitor-keys" "8.29.0"
+
"@typescript-eslint/type-utils@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a"
@@ -2190,13 +2198,13 @@
debug "^4.3.4"
tsutils "^3.21.0"
-"@typescript-eslint/type-utils@8.25.0":
- version "8.25.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz#ee0d2f67c80af5ae74b5d6f977e0f8ded0059677"
- integrity sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==
+"@typescript-eslint/type-utils@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.29.0.tgz#98dcfd1193cb4e2b2d0294a8656ce5eb58c443a9"
+ integrity sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==
dependencies:
- "@typescript-eslint/typescript-estree" "8.25.0"
- "@typescript-eslint/utils" "8.25.0"
+ "@typescript-eslint/typescript-estree" "8.29.0"
+ "@typescript-eslint/utils" "8.29.0"
debug "^4.3.4"
ts-api-utils "^2.0.1"
@@ -2210,6 +2218,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.25.0.tgz#f91512c2f532b1d6a8826cadd0b0e5cd53cf97e0"
integrity sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==
+"@typescript-eslint/types@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.29.0.tgz#65add70ab4ef66beaa42a5addf87dab2b05b1f33"
+ integrity sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==
+
"@typescript-eslint/typescript-estree@5.62.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b"
@@ -2237,6 +2250,20 @@
semver "^7.6.0"
ts-api-utils "^2.0.1"
+"@typescript-eslint/typescript-estree@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.29.0.tgz#d201a4f115327ec90496307c9958262285065b00"
+ integrity sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==
+ dependencies:
+ "@typescript-eslint/types" "8.29.0"
+ "@typescript-eslint/visitor-keys" "8.29.0"
+ debug "^4.3.4"
+ fast-glob "^3.3.2"
+ is-glob "^4.0.3"
+ minimatch "^9.0.4"
+ semver "^7.6.0"
+ ts-api-utils "^2.0.1"
+
"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
@@ -2251,7 +2278,17 @@
eslint-scope "^5.1.1"
semver "^7.3.7"
-"@typescript-eslint/utils@8.25.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
+"@typescript-eslint/utils@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.29.0.tgz#d6d22b19c8c4812a874f00341f686b45b9fe895f"
+ integrity sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.4.0"
+ "@typescript-eslint/scope-manager" "8.29.0"
+ "@typescript-eslint/types" "8.29.0"
+ "@typescript-eslint/typescript-estree" "8.29.0"
+
+"@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.25.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.25.0.tgz#3ea2f9196a915ef4daa2c8eafd44adbd7d56d08a"
integrity sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==
@@ -2277,6 +2314,14 @@
"@typescript-eslint/types" "8.25.0"
eslint-visitor-keys "^4.2.0"
+"@typescript-eslint/visitor-keys@8.29.0":
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.29.0.tgz#2356336c9efdc3597ffcd2aa1ce95432852b743d"
+ integrity sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==
+ dependencies:
+ "@typescript-eslint/types" "8.29.0"
+ eslint-visitor-keys "^4.2.0"
+
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@@ -4380,9 +4425,9 @@ glob-stream@^8.0.0:
streamx "^2.12.5"
glob@^11.0.0:
- version "11.0.1"
- resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.1.tgz#1c3aef9a59d680e611b53dcd24bb8639cef064d9"
- integrity sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==
+ version "11.0.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.2.tgz#3261e3897bbc603030b041fd77ba636022d51ce0"
+ integrity sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==
dependencies:
foreground-child "^3.1.0"
jackspeak "^4.0.1"
@@ -5827,9 +5872,9 @@ lru-cache@^10.2.0:
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
lru-cache@^11.0.0:
- version "11.0.2"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.0.2.tgz#fbd8e7cf8211f5e7e5d91905c415a3f55755ca39"
- integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==
+ version "11.1.0"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117"
+ integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==
lru-cache@^5.1.1:
version "5.1.1"
@@ -7772,10 +7817,10 @@ statuses@~1.5.0:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
-stream-chat@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.0.0.tgz#cb22dcb8b7f070c623a13b6b75b212d560534d6c"
- integrity sha512-I4+/DEp7dP3WBgRmqHaLswL+Y2fyQkUWJhYBS5zx4bpu1cYM6WEir9HYjToDNuJjltqa/FFIEF/tMPWr7iTc0A==
+stream-chat@^9.2.0:
+ version "9.2.0"
+ resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-9.2.0.tgz#f3109891ca27f17b6fd0aa6ebcf66be12df1f88c"
+ integrity sha512-inz3CA5tuqqSrla7qjRTCKs+coRKOYROWf0wEWYgbCu0tAUuiBTRtu1PJL1isEXIaPLiWi00BuRrBEIFon9Kng==
dependencies:
"@types/jsonwebtoken" "^9.0.8"
"@types/ws" "^8.5.14"
@@ -8209,14 +8254,14 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
-typescript-eslint@^8.25.0:
- version "8.25.0"
- resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.25.0.tgz#73047c157cd70ee93cf2f9243f1599d21cf60239"
- integrity sha512-TxRdQQLH4g7JkoFlYG3caW5v1S6kEkz8rqt80iQJZUYPq1zD1Ra7HfQBJJ88ABRaMvHAXnwRvRB4V+6sQ9xN5Q==
+typescript-eslint@^8.29.0:
+ version "8.29.0"
+ resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.29.0.tgz#fc059b4c840889e5180dd822594eb46fa4619093"
+ integrity sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==
dependencies:
- "@typescript-eslint/eslint-plugin" "8.25.0"
- "@typescript-eslint/parser" "8.25.0"
- "@typescript-eslint/utils" "8.25.0"
+ "@typescript-eslint/eslint-plugin" "8.29.0"
+ "@typescript-eslint/parser" "8.29.0"
+ "@typescript-eslint/utils" "8.29.0"
typescript@5.8.2, typescript@^5.0.4:
version "5.8.2"