Skip to content

Commit efb686c

Browse files
authored
fix: translated into viewport PortalWhileClosingView (#3512)
## 🎯 Goal THis PR addresses a very peculiar issue we have with our `PortalWhileClosingView`. Namely, any time the `PortalWhileClosingView` is rendered some place else than the actual viewport (a common example would be the JS `stack` version of `react-navigation`, where the entire screen is first rendered offscreen and then translated into view) our previous setup would take layout measurements wrong (as it would do it on mount initially, while the view is completely offset). This would of course resolve itself if `onLayout` is ever invoked again on the `children` property of `PortalWhileClosingView`, however this is in no way guaranteed (as even the initial `onLayout` happens offscreen). Instead, we change the logic to only measure when: - The overlay actually opens - `onLayout` is invoked while the overlay is open We don't particularly care about any other scenario anyway. This is additionally a slight performance boost as well, as for example children with very heavy layout animations (for example animating `scale` directly on some `interpolation`, or basically anything which calls `onLayout` very often). We do pay the cost on initial teleport, but it's negligible and does not at all stress any of the threads out. ## 🛠 Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ 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
1 parent 7992186 commit efb686c

File tree

2 files changed

+58
-34
lines changed

2 files changed

+58
-34
lines changed

package/src/components/UIComponents/PortalWhileClosingView.tsx

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactNode, useEffect, useRef } from 'react';
1+
import React, { ReactNode, useEffect, useMemo, useRef } from 'react';
22
import { Platform, View } from 'react-native';
33

44
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated';
@@ -11,6 +11,7 @@ import {
1111
createClosingPortalLayoutRegistrationId,
1212
setClosingPortalLayout,
1313
useShouldTeleportToClosingPortal,
14+
useHasActiveId,
1415
} from '../../state-store';
1516

1617
type PortalWhileClosingViewProps = {
@@ -67,54 +68,26 @@ export const PortalWhileClosingView = ({
6768
portalHostName,
6869
portalName,
6970
}: PortalWhileClosingViewProps) => {
70-
const containerRef = useRef<View | null>(null);
7171
const registrationIdRef = useRef<string | null>(null);
72-
const placeholderLayout = useSharedValue({ h: 0, w: 0 });
73-
const insets = useSafeAreaInsets();
7472

7573
if (!registrationIdRef.current) {
7674
registrationIdRef.current = createClosingPortalLayoutRegistrationId();
7775
}
7876

7977
const registrationId = registrationIdRef.current;
80-
const shouldTeleport = useShouldTeleportToClosingPortal(portalHostName, registrationId);
81-
82-
const syncPortalLayout = useStableCallback(() => {
83-
containerRef.current?.measureInWindow((x, y, width, height) => {
84-
const absolute = {
85-
x,
86-
y: y + (Platform.OS === 'android' ? insets.top : 0),
87-
};
88-
89-
if (!width || !height) {
90-
return;
91-
}
9278

93-
placeholderLayout.value = { h: height, w: width };
94-
95-
setClosingPortalLayout(portalHostName, registrationId, {
96-
...absolute,
97-
h: height,
98-
w: width,
99-
});
100-
});
101-
});
79+
const { syncPortalLayout, containerRef, placeholderLayout } = useSyncingApi(
80+
portalHostName,
81+
registrationId,
82+
);
83+
const shouldTeleport = useShouldTeleportToClosingPortal(portalHostName, registrationId);
10284

10385
useEffect(() => {
10486
return () => {
10587
clearClosingPortalLayout(portalHostName, registrationId);
10688
};
10789
}, [portalHostName, registrationId]);
10890

109-
useEffect(() => {
110-
// Measure once after mount and layout settle.
111-
requestAnimationFrame(() => {
112-
requestAnimationFrame(() => {
113-
syncPortalLayout();
114-
});
115-
});
116-
}, [insets.top, portalHostName, syncPortalLayout]);
117-
11891
const placeholderStyle = useAnimatedStyle(() => ({
11992
height: placeholderLayout.value.h,
12093
width: placeholderLayout.value.w > 0 ? placeholderLayout.value.w : '100%',
@@ -137,3 +110,46 @@ export const PortalWhileClosingView = ({
137110
</>
138111
);
139112
};
113+
114+
const useSyncingApi = (portalHostName: string, registrationId: string) => {
115+
const containerRef = useRef<View | null>(null);
116+
const placeholderLayout = useSharedValue({ h: 0, w: 0 });
117+
const insets = useSafeAreaInsets();
118+
const hasActiveId = useHasActiveId();
119+
120+
const syncPortalLayout = useStableCallback(() => {
121+
if (!hasActiveId) {
122+
return;
123+
}
124+
125+
containerRef.current?.measureInWindow((x, y, width, height) => {
126+
const absolute = {
127+
x,
128+
y: y + (Platform.OS === 'android' ? insets.top : 0),
129+
};
130+
131+
if (!width || !height) {
132+
return;
133+
}
134+
135+
placeholderLayout.value = { h: height, w: width };
136+
137+
setClosingPortalLayout(portalHostName, registrationId, {
138+
...absolute,
139+
h: height,
140+
w: width,
141+
});
142+
});
143+
});
144+
145+
useEffect(() => {
146+
if (hasActiveId) {
147+
syncPortalLayout();
148+
}
149+
}, [insets.bottom, hasActiveId, syncPortalLayout]);
150+
151+
return useMemo(
152+
() => ({ syncPortalLayout, containerRef, placeholderLayout }),
153+
[placeholderLayout, syncPortalLayout],
154+
);
155+
};

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,11 @@ export const useIsOverlayActive = (id: string) => {
342342

343343
return useStateStore(overlayStore, messageOverlaySelector);
344344
};
345+
346+
export const activeIdSelector = (nextState: OverlayState) => ({ id: nextState.id });
347+
348+
export const useHasActiveId = () => {
349+
const { id } = useStateStore(overlayStore, activeIdSelector);
350+
351+
return !!id;
352+
};

0 commit comments

Comments
 (0)