Skip to content

Commit 7c506c1

Browse files
committed
feat: introduce host blacklist hook
1 parent 6f8c82b commit 7c506c1

2 files changed

Lines changed: 110 additions & 5 deletions

File tree

examples/SampleApp/src/screens/ThreadScreen.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
useTheme,
1212
useTranslationContext,
1313
useTypingString,
14+
useClosingPortalHostBlacklist,
15+
PortalWhileClosingView,
1416
} from 'stream-chat-react-native';
1517
import { useStateStore } from 'stream-chat-react-native';
1618

@@ -161,7 +163,12 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
161163
onAlsoSentToChannelHeaderPress={onAlsoSentToChannelHeaderPress}
162164
messageId={targetedMessageIdFromParams}
163165
>
164-
<ThreadHeader thread={thread} />
166+
<PortalWhileClosingView
167+
portalHostName='overlay-header'
168+
portalName='channel-header'
169+
>
170+
<ThreadHeader thread={thread} />
171+
</PortalWhileClosingView>
165172
<Thread
166173
onThreadDismount={onThreadDismount}
167174
shouldUseFlashList={messageListImplementation === 'flashlist'}

package/src/state-store/message-overlay-store.ts

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback } from 'react';
1+
import { useCallback, useEffect, useRef } from 'react';
22

33
import { makeMutable, type SharedValue } from 'react-native-reanimated';
44

@@ -8,6 +8,7 @@ import { useSyncExternalStore } from 'use-sync-external-store/shim';
88
import { useStateStore } from '../hooks';
99

1010
type OverlayState = {
11+
closingPortalHostBlacklist: string[];
1112
id: string | undefined;
1213
closing: boolean;
1314
};
@@ -22,6 +23,7 @@ type ClosingPortalLayoutsState = {
2223
};
2324

2425
const DefaultState = {
26+
closingPortalHostBlacklist: [],
2527
closing: false,
2628
id: undefined,
2729
};
@@ -30,6 +32,8 @@ const DefaultClosingPortalLayoutsState: ClosingPortalLayoutsState = {
3032
};
3133

3234
let closingPortalLayoutRegistrationCounter = 0;
35+
let closingPortalHostBlacklistRegistrationCounter = 0;
36+
let closingPortalHostBlacklistStack: Array<{ hostNames: string[]; id: string }> = [];
3337

3438
type OverlaySharedValueController = {
3539
incrementCloseCorrectionY: (deltaY: number) => void;
@@ -69,7 +73,11 @@ export const bumpOverlayLayoutRevision = (closeCorrectionDeltaY = 0) => {
6973

7074
export const openOverlay = (id: string) => {
7175
sharedValueController?.resetCloseCorrectionY();
72-
overlayStore.partialNext({ closing: false, id });
76+
overlayStore.partialNext({
77+
closing: false,
78+
closingPortalHostBlacklist: getCurrentClosingPortalHostBlacklist(),
79+
id,
80+
});
7381
};
7482

7583
export const closeOverlay = () => {
@@ -90,7 +98,10 @@ export const scheduleActionOnClose = (action: () => void | Promise<void>) => {
9098
};
9199

92100
export const finalizeCloseOverlay = () => {
93-
overlayStore.partialNext(DefaultState);
101+
overlayStore.next({
102+
...DefaultState,
103+
closingPortalHostBlacklist: getCurrentClosingPortalHostBlacklist(),
104+
});
94105
sharedValueController?.reset();
95106
};
96107

@@ -102,6 +113,43 @@ const closingPortalLayoutsStore = new StateStore<ClosingPortalLayoutsState>(
102113
export const createClosingPortalLayoutRegistrationId = () =>
103114
`closing-portal-layout-${closingPortalLayoutRegistrationCounter++}`;
104115

116+
const getCurrentClosingPortalHostBlacklist = () =>
117+
closingPortalHostBlacklistStack[closingPortalHostBlacklistStack.length - 1]?.hostNames ?? [];
118+
119+
const syncClosingPortalHostBlacklist = () => {
120+
overlayStore.partialNext({
121+
closingPortalHostBlacklist: getCurrentClosingPortalHostBlacklist(),
122+
});
123+
};
124+
125+
const createClosingPortalHostBlacklistRegistrationId = () =>
126+
`closing-portal-host-blacklist-${closingPortalHostBlacklistRegistrationCounter++}`;
127+
128+
const setClosingPortalHostBlacklist = (id: string, hostNames: string[]) => {
129+
const existingEntryIndex = closingPortalHostBlacklistStack.findIndex((entry) => entry.id === id);
130+
131+
if (existingEntryIndex === -1) {
132+
closingPortalHostBlacklistStack = [...closingPortalHostBlacklistStack, { hostNames, id }];
133+
} else {
134+
closingPortalHostBlacklistStack = closingPortalHostBlacklistStack.map((entry, index) =>
135+
index === existingEntryIndex ? { ...entry, hostNames } : entry,
136+
);
137+
}
138+
139+
syncClosingPortalHostBlacklist();
140+
};
141+
142+
const clearClosingPortalHostBlacklist = (id: string) => {
143+
const nextBlacklistStack = closingPortalHostBlacklistStack.filter((entry) => entry.id !== id);
144+
145+
if (nextBlacklistStack.length === closingPortalHostBlacklistStack.length) {
146+
return;
147+
}
148+
149+
closingPortalHostBlacklistStack = nextBlacklistStack;
150+
syncClosingPortalHostBlacklist();
151+
};
152+
105153
export const setClosingPortalLayout = (hostName: string, id: string, layout: Rect) => {
106154
const { layouts } = closingPortalLayoutsStore.getLatestValue();
107155
const hostEntries = layouts[hostName] ?? [];
@@ -170,16 +218,66 @@ const overlayClosingSelector = (nextState: OverlayState) => ({
170218
closing: nextState.closing,
171219
});
172220

221+
const closingPortalHostBlacklistSelector = (nextState: OverlayState) => ({
222+
closingPortalHostBlacklist: nextState.closingPortalHostBlacklist,
223+
});
224+
173225
export const useIsOverlayClosing = () => {
174226
return useStateStore(overlayStore, overlayClosingSelector).closing;
175227
};
176228

229+
export const useClosingPortalHostBlacklistState = () => {
230+
return useStateStore(overlayStore, closingPortalHostBlacklistSelector).closingPortalHostBlacklist;
231+
};
232+
177233
export const useShouldTeleportToClosingPortal = (hostName: string, id: string) => {
178234
const closing = useIsOverlayClosing();
235+
const closingPortalHostBlacklist = useClosingPortalHostBlacklistState();
179236
const closingPortalLayouts = useClosingPortalLayouts();
180237
const hostEntries = closingPortalLayouts[hostName];
181238

182-
return !!closing && hostEntries?.[hostEntries.length - 1]?.id === id;
239+
return (
240+
closing &&
241+
!closingPortalHostBlacklist.includes(hostName) &&
242+
hostEntries?.[hostEntries.length - 1]?.id === id
243+
);
244+
};
245+
246+
/**
247+
* Registers a screen-level blacklist of closing portal hosts that should not render while this hook is active.
248+
*
249+
* The blacklist uses stack semantics:
250+
* - mounting/enabling a new instance makes its blacklist active
251+
* - unmounting/disabling restores the previous active blacklist automatically
252+
*
253+
* This keeps stacked screens predictable without requiring previous screens to rerun effects when the top screen
254+
* disappears.
255+
*/
256+
export const useClosingPortalHostBlacklist = (hostNames: string[], enabled = true) => {
257+
const registrationIdRef = useRef<string | null>(null);
258+
259+
if (!registrationIdRef.current) {
260+
registrationIdRef.current = createClosingPortalHostBlacklistRegistrationId();
261+
}
262+
263+
const registrationId = registrationIdRef.current;
264+
const serializedNormalizedHostNames = JSON.stringify([...new Set(hostNames)]);
265+
266+
useEffect(() => {
267+
if (!enabled) {
268+
clearClosingPortalHostBlacklist(registrationId);
269+
return;
270+
}
271+
272+
setClosingPortalHostBlacklist(
273+
registrationId,
274+
JSON.parse(serializedNormalizedHostNames) as string[],
275+
);
276+
277+
return () => {
278+
clearClosingPortalHostBlacklist(registrationId);
279+
};
280+
}, [enabled, registrationId, serializedNormalizedHostNames]);
183281
};
184282

185283
/**

0 commit comments

Comments
 (0)