From d12f6876fe7be12d452d844d16793a81869e6f87 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:35:32 +0200 Subject: [PATCH 1/4] fix: widen endPoll type (#3833) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR is the UI SDK counterpart for [this change](https://github.com/GetStream/stream-chat-js/pull/1895) in the LLC. Linear ticket: https://linear.app/stream/issue/REACT-1199/add-optimistic-updates-to-polls-and-everywhere-else-where-possible-for ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- examples/ExpoMessaging/package.json | 2 +- examples/SampleApp/ios/Podfile.lock | 4 ++-- examples/SampleApp/package.json | 2 +- package/package.json | 2 +- package/src/components/Poll/hooks/usePollState.ts | 2 +- yarn.lock | 14 +++++++------- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index db1c4b4629..63f68072b6 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -51,7 +51,7 @@ "react-native-teleport": "^1.1.12", "react-native-web": "^0.21.2", "react-native-worklets": "0.11.1", - "stream-chat": "^9.51.0", + "stream-chat": "^9.54.0", "stream-chat-expo": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index d223b78687..4de2187ad1 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -2640,7 +2640,7 @@ PODS: - SDWebImage/Core (~> 5.17) - SocketRocket (0.7.1): - ReactNativeDependencies - - stream-chat-react-native (9.8.0): + - stream-chat-react-native (9.9.2): - hermes-engine - RCTRequired - RCTTypeSafety @@ -3223,7 +3223,7 @@ SPEC CHECKSUMS: SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377 SocketRocket: 37aec555668fb852ec12e3c0de59a86ca58f0871 - stream-chat-react-native: de5922b681460d780a4d47c58b2b024622954aa7 + stream-chat-react-native: bb3a2c7f8949f4ebb735d83f549f43ae37c3ae7b Teleport: c42718cf1f14dd53b250c0fa29b9235a42f861ec Yoga: d1c536142c5ff8ec8cd856ab2a7c227a1d875c8e diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 796e919724..452ae975d6 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -64,7 +64,7 @@ "react-native-teleport": "^1.1.12", "react-native-video": "^6.19.2", "react-native-worklets": "^0.12.1", - "stream-chat": "^9.51.0", + "stream-chat": "^9.54.0", "stream-chat-react-native": "workspace:^", "stream-chat-react-native-core": "workspace:^" }, diff --git a/package/package.json b/package/package.json index bbf7e55b30..57f350122a 100644 --- a/package/package.json +++ b/package/package.json @@ -78,7 +78,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^2.0.0", - "stream-chat": "^9.51.0", + "stream-chat": "^9.54.0", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { diff --git a/package/src/components/Poll/hooks/usePollState.ts b/package/src/components/Poll/hooks/usePollState.ts index 78d7b2ff8c..26fd646569 100644 --- a/package/src/components/Poll/hooks/usePollState.ts +++ b/package/src/components/Poll/hooks/usePollState.ts @@ -39,7 +39,7 @@ export type UsePollStateSelectorReturnType = { export type UsePollStateReturnType = UsePollStateSelectorReturnType & { addComment: (answerText: string) => Promise; addOption: (optionText: string) => Promise; - endVote: () => Promise; + endVote: () => Promise<(APIResponse & UpdatePollAPIResponse) | undefined>; }; const selector = (nextValue: PollState): UsePollStateSelectorReturnType => ({ diff --git a/yarn.lock b/yarn.lock index 42801841f4..fa63934f8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7006,7 +7006,7 @@ __metadata: react-native-teleport: "npm:^1.1.12" react-native-web: "npm:^0.21.2" react-native-worklets: "npm:0.11.1" - stream-chat: "npm:^9.51.0" + stream-chat: "npm:^9.54.0" stream-chat-expo: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:6.0.3" @@ -18412,7 +18412,7 @@ __metadata: react-native-teleport: "npm:^1.1.12" react-native-video: "npm:^6.19.2" react-native-worklets: "npm:^0.12.1" - stream-chat: "npm:^9.51.0" + stream-chat: "npm:^9.54.0" stream-chat-react-native: "workspace:^" stream-chat-react-native-core: "workspace:^" typescript: "npm:6.0.3" @@ -19164,7 +19164,7 @@ __metadata: react-native-worklets: "npm:^0.12.1" react-test-renderer: "npm:19.2.3" rimraf: "npm:^6.0.1" - stream-chat: "npm:^9.51.0" + stream-chat: "npm:^9.54.0" typescript: "npm:6.0.3" use-sync-external-store: "npm:^1.5.0" uuid: "npm:^11.1.0" @@ -19238,9 +19238,9 @@ __metadata: languageName: unknown linkType: soft -"stream-chat@npm:^9.51.0": - version: 9.51.0 - resolution: "stream-chat@npm:9.51.0" +"stream-chat@npm:^9.54.0": + version: 9.54.0 + resolution: "stream-chat@npm:9.54.0" dependencies: "@types/jsonwebtoken": "npm:^9.0.8" "@types/ws": "npm:^8.18.1" @@ -19256,7 +19256,7 @@ __metadata: built: true husky: built: true - checksum: 10c0/a2888b1dad9496f8ba35e5afb44f88a6ce6b64785780b0244eba349ee10c52594f43c08a16a23924b14640a50849ce835773f016435725378bf0bbcfe0d636ed + checksum: 10c0/f8c02372ffa672e759970223811667b8263bbded048a85adbdd6c3ed60a16f72c882d9700538b94a3148a8e4bfa259eafae5effdc0d8e7ffd2525e61f27a190e languageName: node linkType: hard From 02850238f01bd0b28bc4c45c174f8c13563c7b6e Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:12:30 +0200 Subject: [PATCH 2/4] fix: bump flashlist in sample apps (#3839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal This PR bumps the version of FlashList in our sample apps. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- examples/ExpoMessaging/package.json | 2 +- examples/SampleApp/package.json | 2 +- package/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/ExpoMessaging/package.json b/examples/ExpoMessaging/package.json index 63f68072b6..5eb853cec1 100644 --- a/examples/ExpoMessaging/package.json +++ b/examples/ExpoMessaging/package.json @@ -18,7 +18,7 @@ "@react-native-community/netinfo": "12.0.1", "@react-native-firebase/app": "^24.0.0", "@react-native-firebase/messaging": "^24.0.0", - "@shopify/flash-list": "^2.3.2", + "@shopify/flash-list": "^2.3.3", "expo": "^57.0.7", "expo-asset": "~57.0.6", "expo-audio": "patch:expo-audio@npm%3A57.0.2#~/.yarn/patches/expo-audio-npm-57.0.2-7a40cb0b0a.patch", diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 452ae975d6..4ccf1e6633 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -43,7 +43,7 @@ "@react-navigation/drawer": "^7.13.2", "@react-navigation/native": "^7.3.11", "@react-navigation/native-stack": "^7.18.3", - "@shopify/flash-list": "^2.3.2", + "@shopify/flash-list": "^2.3.3", "dayjs": "1.11.13", "emoji-mart": "^5.6.0", "lodash.mergewith": "^4.6.2", diff --git a/package/package.json b/package/package.json index 57f350122a..e6a75f1c56 100644 --- a/package/package.json +++ b/package/package.json @@ -115,7 +115,7 @@ "@react-native-community/netinfo": "^12.0.1", "@react-native/babel-preset": "0.87.0", "@react-native/jest-preset": "0.87.0", - "@shopify/flash-list": "^2.3.2", + "@shopify/flash-list": "^2.3.3", "@stream-io/typescript-config": "workspace:^", "@testing-library/jest-native": "^5.4.3", "@testing-library/react-native": "13.2.0", diff --git a/yarn.lock b/yarn.lock index fa63934f8d..fd910f97f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5951,14 +5951,14 @@ __metadata: languageName: node linkType: hard -"@shopify/flash-list@npm:^2.3.2": - version: 2.3.2 - resolution: "@shopify/flash-list@npm:2.3.2" +"@shopify/flash-list@npm:^2.3.3": + version: 2.3.3 + resolution: "@shopify/flash-list@npm:2.3.3" peerDependencies: "@babel/runtime": "*" react: "*" react-native: "*" - checksum: 10c0/3e58913506b1dc7f02dd0696c478fd963ede413d9d7121705a08ff505d5c21e611813ce9370068238b8983ff582ed7b8ee94f68e94f99a1ac8f600c01fff118e + checksum: 10c0/c94af22f8b034d9f24e8ea3628ac20597cdb00f8c250d24a8e7adb86bc2a41cf10234c2fd87315bf7a34211db87d7eba40bddda610847cb6afedd2d6a1a3ffe5 languageName: node linkType: hard @@ -6971,7 +6971,7 @@ __metadata: "@react-native-firebase/messaging": "npm:^24.0.0" "@rnx-kit/metro-config": "npm:^2.1.2" "@rnx-kit/metro-resolver-symlinks": "npm:^0.2.6" - "@shopify/flash-list": "npm:^2.3.2" + "@shopify/flash-list": "npm:^2.3.3" "@stream-io/typescript-config": "workspace:^" "@types/react": "npm:~19.2.10" expo: "npm:^57.0.7" @@ -18387,7 +18387,7 @@ __metadata: "@react-navigation/native": "npm:^7.3.11" "@react-navigation/native-stack": "npm:^7.18.3" "@rnx-kit/metro-config": "npm:^2.1.0" - "@shopify/flash-list": "npm:^2.3.2" + "@shopify/flash-list": "npm:^2.3.3" "@stream-io/typescript-config": "workspace:^" "@types/lodash.mergewith": "npm:^4.6.9" "@types/react": "npm:^19.2.0" @@ -19119,7 +19119,7 @@ __metadata: "@react-native-community/netinfo": "npm:^12.0.1" "@react-native/babel-preset": "npm:0.87.0" "@react-native/jest-preset": "npm:0.87.0" - "@shopify/flash-list": "npm:^2.3.2" + "@shopify/flash-list": "npm:^2.3.3" "@stream-io/typescript-config": "workspace:^" "@testing-library/jest-native": "npm:^5.4.3" "@testing-library/react-native": "npm:13.2.0" From 0ab5ecbae06681ca9b93d23621046e7b08b7ef6b Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:01:53 +0200 Subject: [PATCH 3/4] fix: bump java runner to 21 (#3841) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal Linear ticket: https://linear.app/stream/issue/RN-441/update-java-runner-image-to-v21 ## ๐Ÿ›  Implementation details So it would appear the java 17 runner image has reached EOL and is no longer available. It was anyway due an update (and should've been done ages ago) but we never did this for the sample app. ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- .github/workflows/sample-distribution.yml | 2 +- .../android/gradle/gradle-daemon-jvm.properties | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 examples/SampleApp/android/gradle/gradle-daemon-jvm.properties diff --git a/.github/workflows/sample-distribution.yml b/.github/workflows/sample-distribution.yml index 363f9efd8e..67097ad844 100644 --- a/.github/workflows/sample-distribution.yml +++ b/.github/workflows/sample-distribution.yml @@ -80,7 +80,7 @@ jobs: - uses: actions/setup-java@v5 with: distribution: 'zulu' - java-version: '17' + java-version: '21' check-latest: true - name: Setup Android SDK uses: amyu/setup-android@v5 diff --git a/examples/SampleApp/android/gradle/gradle-daemon-jvm.properties b/examples/SampleApp/android/gradle/gradle-daemon-jvm.properties deleted file mode 100644 index 6c1139ec06..0000000000 --- a/examples/SampleApp/android/gradle/gradle-daemon-jvm.properties +++ /dev/null @@ -1,12 +0,0 @@ -#This file is generated by updateDaemonJvm -toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect -toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect -toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect -toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect -toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect -toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect -toolchainVersion=21 From 4de08ed5cf34dfbd8812ba837111d938ac13eca9 Mon Sep 17 00:00:00 2001 From: Ivan Sekovanikj <31964049+isekovanic@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:27:07 +0200 Subject: [PATCH 4/4] fix: ctx menu failure and imperative closure edge cases (#3844) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ๐ŸŽฏ Goal Linear ticket: https://linear.app/stream/issue/RN-442/request-failures-and-closure-edge-cases-in-ctx-menu This PR resolves 2 UI issues with the contextual menu: 1. Errors on actions get all subsequent actions stuck (this happens because errors will not clear the action queue and the pending action will remain first, so if it continues failing (i.e improper permissions or something) it will block all actions following it from running as it will simply throw mid for cycle. 2. The hardware back button (or back gesture) on Android keep the context menu open and since it overlays all of the screens it essentially keeps the opaque background while not actually closing the overlay. ## ๐Ÿ›  Implementation details ## ๐ŸŽจ UI Changes
iOS
Before After
Android
Before After
## ๐Ÿงช Testing ## โ˜‘๏ธ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android --- .../MessageOverlayHostLayer.tsx | 14 ++++ .../MessageOverlayHostLayer.test.tsx | 59 +++++++++++++- .../__tests__/message-overlay-store.test.tsx | 80 +++++++++++++++++++ .../src/state-store/message-overlay-store.ts | 13 ++- 4 files changed, 161 insertions(+), 5 deletions(-) diff --git a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx index 96e3d523db..d5b1a2029e 100644 --- a/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx +++ b/package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useRef } from 'react'; import { + BackHandler, I18nManager, Platform, Pressable, @@ -101,6 +102,19 @@ export const MessageOverlayHostLayer = () => { } }, [isActive, overlayOpenHint, announce]); + useEffect(() => { + if (!isActive) { + return; + } + + const subscription = BackHandler.addEventListener('hardwareBackPress', () => { + closeOverlay(); + return true; + }); + + return () => subscription.remove(); + }, [isActive]); + // When orientation changes, we dismiss the overlay. While it is possible // (and I have a working PoC of) a repositioning solution, it is pretty // complex and probably bug prone. This is anyway how the iOS native contextual diff --git a/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx b/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx index 8d0cb06efe..299b17f8a9 100644 --- a/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx +++ b/package/src/contexts/overlayContext/__tests__/MessageOverlayHostLayer.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, Text } from 'react-native'; +import { BackHandler, StyleSheet, Text } from 'react-native'; import Animated from 'react-native-reanimated'; @@ -179,6 +179,63 @@ describe('MessageOverlayHostLayer', () => { expect(overlayStore.getLatestValue().closing).toBe(true); }); + it('closes the overlay on Android back press instead of letting navigation handle it', () => { + type BackPressListener = Parameters[1]; + const listeners: BackPressListener[] = []; + const addEventListenerSpy = jest + .spyOn(BackHandler, 'addEventListener') + .mockImplementation((_eventName, handler) => { + listeners.push(handler); + return { + remove: () => { + listeners.splice(listeners.indexOf(handler), 1); + }, + }; + }); + const pressBack = () => + listeners[listeners.length - 1]?.({ timeStamp: Date.now(), type: 'hardwareBackPress' }); + + render( + + + , + ); + + // Nothing is registered while the overlay is closed, so back goes to navigation. + expect(listeners).toHaveLength(0); + + act(() => { + openOverlay('message-1'); + }); + + let handled: boolean | null | undefined; + act(() => { + handled = pressBack(); + }); + flushAnimationFrameQueue(); + + expect(handled).toBe(true); + expect(overlayStore.getLatestValue().closing).toBe(true); + + // Still consumed while the close animation runs, so the screen doesn't pop underneath. + act(() => { + handled = pressBack(); + }); + flushAnimationFrameQueue(); + expect(handled).toBe(true); + expect(overlayStore.getLatestValue().closing).toBe(true); + + act(() => { + finalizeCloseOverlay(); + }); + + expect(listeners).toHaveLength(0); + // One registration for the whole open -> closing -> closed cycle. + expect(addEventListenerSpy).toHaveBeenCalledTimes(1); + + addEventListenerSpy.mockRestore(); + }); + it('positions and translates the top, message, and bottom hosts using the registered rects', () => { const renderTree = () => ( diff --git a/package/src/state-store/__tests__/message-overlay-store.test.tsx b/package/src/state-store/__tests__/message-overlay-store.test.tsx index b18aef459a..cdd8c903eb 100644 --- a/package/src/state-store/__tests__/message-overlay-store.test.tsx +++ b/package/src/state-store/__tests__/message-overlay-store.test.tsx @@ -9,6 +9,7 @@ import { finalizeCloseOverlay, openOverlay, overlayStore, + scheduleActionOnClose, setClosingPortalLayout, useClosingPortalHostBlacklist, useClosingPortalHostBlacklistState, @@ -28,6 +29,14 @@ const flushAnimationFrameQueue = () => { }); }; +// Runs `fn` and lets the async close-action flush (`await action()` per queued action) settle. +const actAndFlushPromises = async (fn: () => void) => { + await act(async () => { + fn(); + await Promise.resolve(); + }); +}; + const BlacklistRegistrar = ({ enabled = true, hostNames, @@ -125,6 +134,77 @@ describe('message overlay store portal hooks', () => { second.unmount(); }); + it('keeps running queued actions when one of them fails and does not rerun it on the next close', async () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const failingAction = jest.fn(() => Promise.reject(new Error('action failed'))); + const throwingAction = jest.fn(() => { + throw new Error('sync action failed'); + }); + const followingAction = jest.fn(); + + act(() => { + openOverlay('message-1'); + }); + scheduleActionOnClose(failingAction); + scheduleActionOnClose(throwingAction); + scheduleActionOnClose(followingAction); + + await actAndFlushPromises(finalizeCloseOverlay); + + expect(failingAction).toHaveBeenCalledTimes(1); + expect(throwingAction).toHaveBeenCalledTimes(1); + expect(followingAction).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toHaveBeenCalledTimes(2); + + const nextAction = jest.fn(); + + act(() => { + openOverlay('message-2'); + }); + scheduleActionOnClose(nextAction); + + await actAndFlushPromises(finalizeCloseOverlay); + + expect(nextAction).toHaveBeenCalledTimes(1); + expect(failingAction).toHaveBeenCalledTimes(1); + expect(throwingAction).toHaveBeenCalledTimes(1); + + consoleWarnSpy.mockRestore(); + }); + + it('defers actions scheduled during a flush to the next close instead of running them twice', async () => { + let resolveSlowAction: () => void = () => {}; + const slowAction = jest.fn( + () => + new Promise((resolve) => { + resolveSlowAction = resolve; + }), + ); + const laterAction = jest.fn(); + + act(() => { + openOverlay('message-1'); + }); + scheduleActionOnClose(slowAction); + + await actAndFlushPromises(finalizeCloseOverlay); + expect(slowAction).toHaveBeenCalledTimes(1); + + // The overlay reopens and an action is scheduled while the first flush is still awaiting. + act(() => { + openOverlay('message-2'); + }); + scheduleActionOnClose(laterAction); + + await actAndFlushPromises(resolveSlowAction); + expect(laterAction).not.toHaveBeenCalled(); + + await actAndFlushPromises(finalizeCloseOverlay); + + expect(laterAction).toHaveBeenCalledTimes(1); + expect(slowAction).toHaveBeenCalledTimes(1); + }); + it('does not enter closing when closeOverlay is called without an active overlay id', () => { act(() => { closeOverlay(); diff --git a/package/src/state-store/message-overlay-store.ts b/package/src/state-store/message-overlay-store.ts index 9fcd969efc..4334fbb421 100644 --- a/package/src/state-store/message-overlay-store.ts +++ b/package/src/state-store/message-overlay-store.ts @@ -216,12 +216,17 @@ const actionQueueSelector = (nextState: OverlayState) => ({ active: !!nextState. // integrations causing UI issues. overlayStore.subscribeWithSelector(actionQueueSelector, async ({ active }) => { if (!active) { + const actions = actionQueue; + actionQueue = []; + // flush the queue - for (const action of actionQueue) { - await action(); + for (const action of actions) { + try { + await action(); + } catch (error) { + console.warn('A message overlay action failed after the overlay closed:', error); + } } - - actionQueue = []; } });