Skip to content

Commit 77ef97e

Browse files
committed
feat: PoC for teleportation aware views
1 parent bed5d86 commit 77ef97e

File tree

6 files changed

+167
-64
lines changed

6 files changed

+167
-64
lines changed

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
useTranslationContext,
1616
MessageActionsParams,
1717
ChannelAvatar,
18+
PortalWhileClosingView,
1819
} from 'stream-chat-react-native';
1920
import { Platform, Pressable, StyleSheet, View } from 'react-native';
2021
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
@@ -236,7 +237,12 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
236237
thread={selectedThread}
237238
maximumMessageLimit={messageListPruning}
238239
>
239-
<ChannelHeader channel={channel} />
240+
<PortalWhileClosingView
241+
portalHostName='overlay-header'
242+
portalName='channel-header'
243+
>
244+
<ChannelHeader channel={channel} />
245+
</PortalWhileClosingView>
240246
{messageListImplementation === 'flashlist' ? (
241247
<MessageFlashList
242248
onThreadSelect={onThreadSelect}

package/src/components/MessageInput/MessageInput.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@ import {
5656
import { useAttachmentPickerState } from '../../hooks/useAttachmentPickerState';
5757
import { useKeyboardVisibility } from '../../hooks/useKeyboardVisibility';
5858
import { useStateStore } from '../../hooks/useStateStore';
59-
import { setOverlayComposerH } from '../../state-store';
6059
import { AudioRecorderManagerState } from '../../state-store/audio-recorder-manager';
6160
import { MessageInputHeightState } from '../../state-store/message-input-height-store';
6261
import { primitives } from '../../theme';
@@ -362,7 +361,6 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
362361
<MicPositionProvider value={micPositionContextValue}>
363362
<Animated.View layout={LinearTransition.duration(200)}>
364363
<PortalWhileClosingView
365-
placeholderHeight={height}
366364
portalHostName='overlay-composer'
367365
portalName='message-input-composer'
368366
>
@@ -372,7 +370,6 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
372370
layout: { height: newHeight },
373371
},
374372
}) => {
375-
setOverlayComposerH(newHeight);
376373
messageInputHeightStore.setHeight(
377374
messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight,
378375
);

package/src/components/UIComponents/PortalWhileClosingView.tsx

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

4+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
45
import { Portal } from 'react-native-teleport';
56

6-
import { useOverlayController } from '../../state-store';
7+
import { setClosingPortalLayout, useOverlayController } from '../../state-store';
78

89
type PortalWhileClosingViewProps = {
910
children: ReactNode;
10-
placeholderHeight?: number;
1111
portalHostName: string;
1212
portalName: string;
1313
};
1414

1515
export const PortalWhileClosingView = ({
1616
children,
17-
placeholderHeight,
1817
portalHostName,
1918
portalName,
2019
}: PortalWhileClosingViewProps) => {
2120
const { closing } = useOverlayController();
21+
const containerRef = useRef<View | null>(null);
22+
const absolutePositionRef = useRef<{ x: number; y: number } | null>(null);
23+
const measuredSizeRef = useRef<{ h: number; w: number }>({ h: 0, w: 0 });
2224
const [measuredHeight, setMeasuredHeight] = useState(0);
23-
const shouldMeasure = placeholderHeight == null;
25+
const [measuredWidth, setMeasuredWidth] = useState(0);
26+
const insets = useSafeAreaInsets();
2427

25-
const resolvedPlaceholderHeight = useMemo(
26-
() => placeholderHeight ?? measuredHeight,
27-
[placeholderHeight, measuredHeight],
28-
);
28+
const resolvedPlaceholderHeight = useMemo(() => measuredHeight, [measuredHeight]);
29+
30+
useEffect(() => {
31+
let cancelled = false;
32+
33+
const measureAbsolutePosition = () => {
34+
containerRef.current?.measureInWindow((x, y) => {
35+
if (cancelled) return;
36+
const absolute = {
37+
x,
38+
y: y + (Platform.OS === 'android' ? insets.top : 0),
39+
};
40+
41+
absolutePositionRef.current = absolute;
42+
43+
const { h, w } = measuredSizeRef.current;
44+
if (!w || !h) return;
45+
46+
setClosingPortalLayout(portalHostName, {
47+
...absolute,
48+
h,
49+
w,
50+
});
51+
});
52+
};
53+
54+
// Measure once after mount and layout settle.
55+
requestAnimationFrame(() => {
56+
requestAnimationFrame(measureAbsolutePosition);
57+
});
58+
59+
return () => {
60+
cancelled = true;
61+
};
62+
}, [insets.top, portalHostName]);
2963

3064
return (
3165
<>
3266
<Portal hostName={closing ? portalHostName : undefined} name={portalName}>
33-
{shouldMeasure ? (
34-
<View
35-
onLayout={(event) => {
36-
setMeasuredHeight(event.nativeEvent.layout.height);
37-
}}
38-
>
39-
{children}
40-
</View>
41-
) : (
42-
children
43-
)}
67+
<View
68+
collapsable={false}
69+
ref={containerRef}
70+
onLayout={(event) => {
71+
const { height, width } = event.nativeEvent.layout;
72+
setMeasuredHeight(height);
73+
setMeasuredWidth(width);
74+
measuredSizeRef.current = { h: height, w: width };
75+
76+
const absolute = absolutePositionRef.current;
77+
if (!absolute) return;
78+
79+
setClosingPortalLayout(portalHostName, { ...absolute, h: height, w: width });
80+
}}
81+
>
82+
{children}
83+
</View>
4484
</Portal>
4585
{closing && resolvedPlaceholderHeight > 0 ? (
46-
<View pointerEvents='none' style={{ height: resolvedPlaceholderHeight, width: '100%' }} />
86+
<View
87+
pointerEvents='none'
88+
style={{ height: resolvedPlaceholderHeight, width: measuredWidth || '100%' }}
89+
/>
4790
) : null}
4891
</>
4992
);

package/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ export * from './UIComponents/StreamBottomSheetModalFlatList';
183183
export * from './UIComponents/ImageBackground';
184184
export * from './UIComponents/Spinner';
185185
export * from './UIComponents/SwipableWrapper';
186+
export * from './UIComponents/PortalWhileClosingView';
186187

187188
export * from './Thread/Thread';
188189
export * from './Thread/components/ThreadFooterComponent';

package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx

Lines changed: 55 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
22
import { Platform, Pressable, StyleSheet, useWindowDimensions, View } from 'react-native';
33
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
44
import Animated, {
5+
SharedValue,
56
clamp,
67
runOnJS,
78
useAnimatedStyle,
@@ -17,18 +18,56 @@ import {
1718
finalizeCloseOverlay,
1819
registerOverlaySharedValueController,
1920
Rect,
21+
useClosingPortalLayouts,
2022
useOverlayController,
2123
} from '../../state-store';
2224

2325
const DURATION = 300;
26+
const ClosingPortalHostSlot = ({
27+
closeCoverOpacity,
28+
hostName,
29+
rect,
30+
}: {
31+
closeCoverOpacity: SharedValue<number>;
32+
hostName: string;
33+
rect: Exclude<Rect, undefined>;
34+
}) => {
35+
const layout = useSharedValue<Rect>(rect);
36+
37+
useEffect(() => {
38+
layout.value = rect;
39+
}, [layout, rect]);
40+
41+
const style = useAnimatedStyle(() => {
42+
const value = layout.value;
43+
if (!value) return { opacity: closeCoverOpacity.value };
44+
45+
return {
46+
height: value.h,
47+
left: value.x,
48+
opacity: closeCoverOpacity.value,
49+
top: value.y,
50+
width: value.w,
51+
};
52+
});
53+
54+
console.log('RENDERING: ', hostName);
55+
56+
return (
57+
<Animated.View pointerEvents='box-none' style={[styles.overlayClosingSlot, style]}>
58+
<PortalHost name={hostName} style={StyleSheet.absoluteFillObject} />
59+
</Animated.View>
60+
);
61+
};
62+
2463
export const MessageOverlayHostLayer = () => {
2564
const { id, closing } = useOverlayController();
65+
const closingPortalLayouts = useClosingPortalLayouts();
2666
const insets = useSafeAreaInsets();
2767
const { height: screenH } = useWindowDimensions();
2868
const messageH = useSharedValue<Rect>(undefined);
2969
const topH = useSharedValue<Rect>(undefined);
3070
const bottomH = useSharedValue<Rect>(undefined);
31-
const composerH = useSharedValue(0);
3271
const closeCorrectionY = useSharedValue(0);
3372

3473
const topInset = insets.top;
@@ -65,17 +104,14 @@ export const MessageOverlayHostLayer = () => {
65104
setBottomH: (rect) => {
66105
bottomH.value = rect;
67106
},
68-
setComposerH: (height) => {
69-
composerH.value = height;
70-
},
71107
setMessageH: (rect) => {
72108
messageH.value = rect;
73109
},
74110
setTopH: (rect) => {
75111
topH.value = rect;
76112
},
77113
}),
78-
[bottomH, closeCorrectionY, composerH, messageH, topH],
114+
[bottomH, closeCorrectionY, messageH, topH],
79115
);
80116

81117
useEffect(() => {
@@ -91,12 +127,6 @@ export const MessageOverlayHostLayer = () => {
91127
const backdropStyle = useAnimatedStyle(() => ({
92128
opacity: backdrop.value,
93129
}));
94-
const closeCoverStyle = useAnimatedStyle(() => ({
95-
opacity: closeCoverOpacity.value,
96-
}));
97-
const composerSlotStyle = useAnimatedStyle(() => ({
98-
height: composerH.value,
99-
}));
100130

101131
const messageShiftY = useDerivedValue(() => {
102132
if (!messageH.value || !topH.value || !bottomH.value) return 0;
@@ -231,6 +261,8 @@ export const MessageOverlayHostLayer = () => {
231261
runOnJS(closeOverlay)();
232262
});
233263

264+
console.log('TEST', Object.entries(closingPortalLayouts));
265+
234266
return (
235267
<GestureDetector gesture={tap}>
236268
<View pointerEvents='box-none' style={StyleSheet.absoluteFill}>
@@ -259,38 +291,28 @@ export const MessageOverlayHostLayer = () => {
259291
</Animated.View>
260292
</View>
261293

262-
<Animated.View pointerEvents='box-none' style={[styles.overlayHeaderSlot, closeCoverStyle]}>
263-
<PortalHost name='overlay-header' style={StyleSheet.absoluteFillObject} />
264-
</Animated.View>
265-
266-
<Animated.View
267-
pointerEvents='box-none'
268-
style={[styles.overlayComposerSlot, closeCoverStyle, composerSlotStyle]}
269-
>
270-
<PortalHost name='overlay-composer' style={StyleSheet.absoluteFillObject} />
271-
</Animated.View>
294+
{Object.entries(closingPortalLayouts).map(([hostName, rect]) => {
295+
if (!rect) return null;
296+
return (
297+
<ClosingPortalHostSlot
298+
closeCoverOpacity={closeCoverOpacity}
299+
hostName={hostName}
300+
key={hostName}
301+
rect={rect}
302+
/>
303+
);
304+
})}
272305
</View>
273306
</GestureDetector>
274307
);
275308
};
276309

277310
const styles = StyleSheet.create({
278-
overlayComposerSlot: {
279-
bottom: 0,
311+
overlayClosingSlot: {
280312
elevation: 20,
281-
left: 0,
282313
position: 'absolute',
283-
right: 0,
284-
width: '100%',
285314
zIndex: 20,
286315
},
287-
overlayHeaderSlot: {
288-
...StyleSheet.absoluteFillObject,
289-
justifyContent: 'flex-start',
290-
left: 0,
291-
position: 'absolute',
292-
right: 0,
293-
},
294316
shadow3: {
295317
overflow: 'visible',
296318
...Platform.select({

0 commit comments

Comments
 (0)