Skip to content

Commit bed5d86

Browse files
committed
feat: introduce teleport aware view
1 parent 7309488 commit bed5d86

File tree

5 files changed

+207
-103
lines changed

5 files changed

+207
-103
lines changed

package/src/components/MessageInput/MessageInput.tsx

Lines changed: 110 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,13 @@ 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';
5960
import { AudioRecorderManagerState } from '../../state-store/audio-recorder-manager';
6061
import { MessageInputHeightState } from '../../state-store/message-input-height-store';
6162
import { primitives } from '../../theme';
6263
import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput';
6364
import { CreatePoll } from '../Poll/CreatePollContent';
65+
import { PortalWhileClosingView } from '../UIComponents/PortalWhileClosingView';
6466
import { SafeAreaViewWrapper } from '../UIComponents/SafeAreaViewWrapper';
6567

6668
const useStyles = () => {
@@ -356,117 +358,124 @@ const MessageInputWithContext = (props: MessageInputPropsWithContext) => {
356358
() => ({ micPositionX, micPositionY }),
357359
[micPositionX, micPositionY],
358360
);
359-
360361
return (
361362
<MicPositionProvider value={micPositionContextValue}>
362-
{/* TODO V9: Think of a better way to do this without so much re-layouting. */}
363-
<Animated.View
364-
layout={LinearTransition.duration(200)}
365-
onLayout={({
366-
nativeEvent: {
367-
layout: { height: newHeight },
368-
},
369-
}) =>
370-
messageInputHeightStore.setHeight(
371-
messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight,
372-
)
373-
} // BOTTOM OFFSET is the position of the input from the bottom of the screen
374-
style={
375-
messageInputFloating
376-
? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper]
377-
: [
378-
styles.wrapper,
379-
{
380-
borderTopWidth: 1,
381-
backgroundColor: semantics.composerBg,
382-
borderColor: semantics.borderCoreDefault,
383-
// paddingBottom: BOTTOM_OFFSET,
384-
paddingBottom:
385-
selectedPicker && !isKeyboardVisible
386-
? attachmentPickerBottomSheetHeight - bottomInset + BOTTOM_OFFSET
387-
: BOTTOM_OFFSET,
388-
},
389-
wrapper,
390-
]
391-
}
392-
>
393-
{Input ? (
394-
<Input additionalTextInputProps={additionalTextInputProps} getUsers={getUsers} />
395-
) : (
396-
<View style={[styles.container, container]}>
397-
<MessageComposerLeadingView />
398-
<Animated.View
399-
layout={LinearTransition.duration(200)}
400-
style={[
401-
styles.inputBoxWrapper,
402-
messageInputFloating ? [styles.shadow, inputFloatingContainer] : null,
403-
inputBoxWrapper,
404-
isFocused ? focusedInputBoxContainer : null,
405-
]}
406-
>
407-
<View style={[styles.inputBoxContainer, inputBoxContainer]}>
408-
{recordingStatus === 'stopped' ? (
409-
<AudioRecordingPreview />
410-
) : micLocked ? (
411-
<AudioRecordingInProgress />
412-
) : null}
413-
414-
<MessageInputHeaderView />
415-
363+
<Animated.View layout={LinearTransition.duration(200)}>
364+
<PortalWhileClosingView
365+
placeholderHeight={height}
366+
portalHostName='overlay-composer'
367+
portalName='message-input-composer'
368+
>
369+
<View
370+
onLayout={({
371+
nativeEvent: {
372+
layout: { height: newHeight },
373+
},
374+
}) => {
375+
setOverlayComposerH(newHeight);
376+
messageInputHeightStore.setHeight(
377+
messageInputFloating ? newHeight + BOTTOM_OFFSET : newHeight,
378+
);
379+
}}
380+
style={
381+
messageInputFloating
382+
? [styles.wrapper, styles.floatingWrapper, { bottom: BOTTOM_OFFSET }, floatingWrapper]
383+
: [
384+
styles.wrapper,
385+
{
386+
borderTopWidth: 1,
387+
backgroundColor: semantics.composerBg,
388+
borderColor: semantics.borderCoreDefault,
389+
// paddingBottom: BOTTOM_OFFSET,
390+
paddingBottom:
391+
selectedPicker && !isKeyboardVisible
392+
? attachmentPickerBottomSheetHeight - bottomInset + BOTTOM_OFFSET
393+
: BOTTOM_OFFSET,
394+
},
395+
wrapper,
396+
]
397+
}
398+
>
399+
{Input ? (
400+
<Input additionalTextInputProps={additionalTextInputProps} getUsers={getUsers} />
401+
) : (
402+
<View style={[styles.container, container]}>
403+
<MessageComposerLeadingView />
416404
<Animated.View
417-
style={[styles.inputContainer, inputContainer]}
418405
layout={LinearTransition.duration(200)}
406+
style={[
407+
styles.inputBoxWrapper,
408+
messageInputFloating ? [styles.shadow, inputFloatingContainer] : null,
409+
inputBoxWrapper,
410+
isFocused ? focusedInputBoxContainer : null,
411+
]}
419412
>
420-
{!isRecordingStateIdle ? (
421-
<AudioRecorder slideToCancelStyle={slideToCancelAnimatedStyle} />
422-
) : (
423-
<>
424-
<MessageInputLeadingView />
425-
426-
<Animated.View
427-
style={styles.autocompleteInputContainer}
428-
layout={LinearTransition.duration(200)}
429-
>
430-
<AutoCompleteInput
431-
TextInputComponent={TextInputComponent}
432-
{...additionalTextInputProps}
433-
/>
434-
</Animated.View>
435-
</>
436-
)}
437-
438-
<MessageInputTrailingView />
413+
<View style={[styles.inputBoxContainer, inputBoxContainer]}>
414+
{recordingStatus === 'stopped' ? (
415+
<AudioRecordingPreview />
416+
) : micLocked ? (
417+
<AudioRecordingInProgress />
418+
) : null}
419+
420+
<MessageInputHeaderView />
421+
422+
<Animated.View
423+
style={[styles.inputContainer, inputContainer]}
424+
layout={LinearTransition.duration(200)}
425+
>
426+
{!isRecordingStateIdle ? (
427+
<AudioRecorder slideToCancelStyle={slideToCancelAnimatedStyle} />
428+
) : (
429+
<>
430+
<MessageInputLeadingView />
431+
432+
<Animated.View
433+
style={styles.autocompleteInputContainer}
434+
layout={LinearTransition.duration(200)}
435+
>
436+
<AutoCompleteInput
437+
TextInputComponent={TextInputComponent}
438+
{...additionalTextInputProps}
439+
/>
440+
</Animated.View>
441+
</>
442+
)}
443+
444+
<MessageInputTrailingView />
445+
</Animated.View>
446+
</View>
439447
</Animated.View>
440448
</View>
441-
</Animated.View>
449+
)}
450+
<ShowThreadMessageInChannelButton threadList={threadList} />
451+
{!isRecordingStateIdle ? (
452+
<View
453+
style={[
454+
styles.audioLockIndicatorWrapper,
455+
{
456+
bottom: messageInputFloating ? 0 : 16,
457+
},
458+
]}
459+
>
460+
<AudioRecordingLockIndicator
461+
messageInputHeight={height}
462+
micLocked={micLocked}
463+
style={lockIndicatorAnimatedStyle}
464+
/>
465+
</View>
466+
) : (
467+
<MessageComposerTrailingView />
468+
)}
469+
470+
<View
471+
style={[styles.suggestionsListContainer, { bottom: height }, suggestionListContainer]}
472+
>
473+
<AutoCompleteSuggestionList />
474+
</View>
442475
</View>
443-
)}
444-
<ShowThreadMessageInChannelButton threadList={threadList} />
476+
</PortalWhileClosingView>
445477
</Animated.View>
446478

447-
{!isRecordingStateIdle ? (
448-
<View
449-
style={[
450-
styles.audioLockIndicatorWrapper,
451-
{
452-
bottom: messageInputFloating ? 0 : 16,
453-
},
454-
]}
455-
>
456-
<AudioRecordingLockIndicator
457-
messageInputHeight={height}
458-
micLocked={micLocked}
459-
style={lockIndicatorAnimatedStyle}
460-
/>
461-
</View>
462-
) : (
463-
<MessageComposerTrailingView />
464-
)}
465-
466-
<View style={[styles.suggestionsListContainer, { bottom: height }, suggestionListContainer]}>
467-
<AutoCompleteSuggestionList />
468-
</View>
469-
470479
{showPollCreationDialog ? (
471480
<View style={styles.pollModalWrapper}>
472481
<Modal
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import React, { ReactNode, useMemo, useState } from 'react';
2+
import { View } from 'react-native';
3+
4+
import { Portal } from 'react-native-teleport';
5+
6+
import { useOverlayController } from '../../state-store';
7+
8+
type PortalWhileClosingViewProps = {
9+
children: ReactNode;
10+
placeholderHeight?: number;
11+
portalHostName: string;
12+
portalName: string;
13+
};
14+
15+
export const PortalWhileClosingView = ({
16+
children,
17+
placeholderHeight,
18+
portalHostName,
19+
portalName,
20+
}: PortalWhileClosingViewProps) => {
21+
const { closing } = useOverlayController();
22+
const [measuredHeight, setMeasuredHeight] = useState(0);
23+
const shouldMeasure = placeholderHeight == null;
24+
25+
const resolvedPlaceholderHeight = useMemo(
26+
() => placeholderHeight ?? measuredHeight,
27+
[placeholderHeight, measuredHeight],
28+
);
29+
30+
return (
31+
<>
32+
<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+
)}
44+
</Portal>
45+
{closing && resolvedPlaceholderHeight > 0 ? (
46+
<View pointerEvents='none' style={{ height: resolvedPlaceholderHeight, width: '100%' }} />
47+
) : null}
48+
</>
49+
);
50+
};

package/src/components/UIComponents/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './StreamBottomSheetModalFlatList';
33
export * from './ImageBackground';
44
export * from './Spinner';
55
export * from './SwipableWrapper';
6+
export * from './PortalWhileClosingView';

package/src/contexts/overlayContext/MessageOverlayHostLayer.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export const MessageOverlayHostLayer = () => {
2828
const messageH = useSharedValue<Rect>(undefined);
2929
const topH = useSharedValue<Rect>(undefined);
3030
const bottomH = useSharedValue<Rect>(undefined);
31+
const composerH = useSharedValue(0);
3132
const closeCorrectionY = useSharedValue(0);
3233

3334
const topInset = insets.top;
@@ -44,6 +45,7 @@ export const MessageOverlayHostLayer = () => {
4445
const maxY = screenH - bottomInset - padding;
4546

4647
const backdrop = useSharedValue(0);
48+
const closeCoverOpacity = useSharedValue(0);
4749

4850
useEffect(
4951
() =>
@@ -63,14 +65,17 @@ export const MessageOverlayHostLayer = () => {
6365
setBottomH: (rect) => {
6466
bottomH.value = rect;
6567
},
68+
setComposerH: (height) => {
69+
composerH.value = height;
70+
},
6671
setMessageH: (rect) => {
6772
messageH.value = rect;
6873
},
6974
setTopH: (rect) => {
7075
topH.value = rect;
7176
},
7277
}),
73-
[bottomH, closeCorrectionY, messageH, topH],
78+
[bottomH, closeCorrectionY, composerH, messageH, topH],
7479
);
7580

7681
useEffect(() => {
@@ -80,11 +85,18 @@ export const MessageOverlayHostLayer = () => {
8085
runOnJS(finalizeCloseOverlay)();
8186
}
8287
});
83-
}, [isActive, closing, backdrop]);
88+
closeCoverOpacity.value = withSpring(closing ? 1 : 0, { duration: DURATION });
89+
}, [isActive, closing, backdrop, closeCoverOpacity]);
8490

8591
const backdropStyle = useAnimatedStyle(() => ({
8692
opacity: backdrop.value,
8793
}));
94+
const closeCoverStyle = useAnimatedStyle(() => ({
95+
opacity: closeCoverOpacity.value,
96+
}));
97+
const composerSlotStyle = useAnimatedStyle(() => ({
98+
height: composerH.value,
99+
}));
88100

89101
const messageShiftY = useDerivedValue(() => {
90102
if (!messageH.value || !topH.value || !bottomH.value) return 0;
@@ -246,12 +258,39 @@ export const MessageOverlayHostLayer = () => {
246258
<PortalHost name='bottom-item' style={StyleSheet.absoluteFillObject} />
247259
</Animated.View>
248260
</View>
261+
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>
249272
</View>
250273
</GestureDetector>
251274
);
252275
};
253276

254277
const styles = StyleSheet.create({
278+
overlayComposerSlot: {
279+
bottom: 0,
280+
elevation: 20,
281+
left: 0,
282+
position: 'absolute',
283+
right: 0,
284+
width: '100%',
285+
zIndex: 20,
286+
},
287+
overlayHeaderSlot: {
288+
...StyleSheet.absoluteFillObject,
289+
justifyContent: 'flex-start',
290+
left: 0,
291+
position: 'absolute',
292+
right: 0,
293+
},
255294
shadow3: {
256295
overflow: 'visible',
257296
...Platform.select({

0 commit comments

Comments
 (0)