@ethora/chat-component-rn 26.8.4 → 26.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/Header/HeaderRoomList.js +12 -7
- package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +23 -3
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +40 -9
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/KeyboardInputDock.js +40 -0
- package/lib/commonjs/components/MainComponents/KeyboardInputDock.js.map +1 -0
- package/lib/commonjs/components/MainComponents/RoomStack.js +202 -0
- package/lib/commonjs/components/MainComponents/RoomStack.js.map +1 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +6 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/Modal/Modal.js +24 -0
- package/lib/commonjs/components/Modals/Modal/Modal.js.map +1 -1
- package/lib/commonjs/components/Modals/Modal/SwipeBackLayer.js +146 -0
- package/lib/commonjs/components/Modals/Modal/SwipeBackLayer.js.map +1 -0
- package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js +28 -10
- package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +9 -3
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/commonjs/helpers/keyboardLayout.js +23 -1
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/components/Header/HeaderRoomList.js +12 -7
- package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +24 -4
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +41 -10
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/KeyboardInputDock.js +33 -0
- package/lib/module/components/MainComponents/KeyboardInputDock.js.map +1 -0
- package/lib/module/components/MainComponents/RoomStack.js +198 -0
- package/lib/module/components/MainComponents/RoomStack.js.map +1 -0
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +6 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/Modal/Modal.js +25 -1
- package/lib/module/components/Modals/Modal/Modal.js.map +1 -1
- package/lib/module/components/Modals/Modal/SwipeBackLayer.js +140 -0
- package/lib/module/components/Modals/Modal/SwipeBackLayer.js.map +1 -0
- package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js +29 -11
- package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +10 -4
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/module/helpers/keyboardLayout.js +20 -0
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/KeyboardInputDock.d.ts +16 -0
- package/lib/typescript/components/MainComponents/KeyboardInputDock.d.ts.map +1 -0
- package/lib/typescript/components/MainComponents/RoomStack.d.ts +37 -0
- package/lib/typescript/components/MainComponents/RoomStack.d.ts.map +1 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/Modal/Modal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/Modal/SwipeBackLayer.d.ts +15 -0
- package/lib/typescript/components/Modals/Modal/SwipeBackLayer.d.ts.map +1 -0
- package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts +6 -0
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Header/HeaderRoomList.tsx +7 -5
- package/src/components/MainComponents/ChatRoom.tsx +29 -3
- package/src/components/MainComponents/ChatWrapper.tsx +46 -12
- package/src/components/MainComponents/KeyboardInputDock.tsx +47 -0
- package/src/components/MainComponents/RoomStack.tsx +236 -0
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -0
- package/src/components/Modals/Modal/Modal.tsx +32 -1
- package/src/components/Modals/Modal/SwipeBackLayer.tsx +170 -0
- package/src/components/Modals/ProfileHeader/ProfileHeader.tsx +29 -8
- package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +6 -3
- package/src/helpers/keyboardLayout.ts +26 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BackHandler,
|
|
4
|
+
Keyboard,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
useWindowDimensions,
|
|
7
|
+
View,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import Animated, {
|
|
10
|
+
interpolate,
|
|
11
|
+
runOnJS,
|
|
12
|
+
useAnimatedStyle,
|
|
13
|
+
useSharedValue,
|
|
14
|
+
withTiming,
|
|
15
|
+
} from 'react-native-reanimated';
|
|
16
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
17
|
+
import {
|
|
18
|
+
COMPLETE_DISTANCE,
|
|
19
|
+
COMPLETE_VELOCITY,
|
|
20
|
+
EDGE_WIDTH,
|
|
21
|
+
LIST_DIM,
|
|
22
|
+
POP,
|
|
23
|
+
PUSH,
|
|
24
|
+
} from '../../MainComponents/RoomStack';
|
|
25
|
+
|
|
26
|
+
interface SwipeBackLayerProps {
|
|
27
|
+
/** Called once the screen has left (swipe, back button, hardware back). */
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
/** Receives the animated close, to hand to the screen's own back button. */
|
|
30
|
+
children: (close: () => void) => ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Full-screen modal layer with the same navigation feel as RoomStack: it
|
|
35
|
+
* slides in from the right, and a swipe from the left edge drags it back
|
|
36
|
+
* out over whatever is underneath (the room or the list).
|
|
37
|
+
*/
|
|
38
|
+
export const SwipeBackLayer: React.FC<SwipeBackLayerProps> = ({
|
|
39
|
+
onClose,
|
|
40
|
+
children,
|
|
41
|
+
}) => {
|
|
42
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
43
|
+
const width = useSharedValue(windowWidth);
|
|
44
|
+
const x = useSharedValue(windowWidth);
|
|
45
|
+
const closing = useSharedValue(false);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
x.value = withTiming(0, PUSH);
|
|
49
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
50
|
+
|
|
51
|
+
const onCloseRef = useRef(onClose);
|
|
52
|
+
onCloseRef.current = onClose;
|
|
53
|
+
const finish = useCallback(() => onCloseRef.current(), []);
|
|
54
|
+
|
|
55
|
+
const close = useCallback(() => {
|
|
56
|
+
if (closing.value) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
closing.value = true;
|
|
60
|
+
Keyboard.dismiss();
|
|
61
|
+
x.value = withTiming(width.value, POP, finished => {
|
|
62
|
+
if (finished) {
|
|
63
|
+
runOnJS(finish)();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}, [finish]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
70
|
+
close();
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
return () => sub.remove();
|
|
74
|
+
}, [close]);
|
|
75
|
+
|
|
76
|
+
const dismissKeyboard = useCallback(() => Keyboard.dismiss(), []);
|
|
77
|
+
|
|
78
|
+
const swipeBack = Gesture.Pan()
|
|
79
|
+
.hitSlop({ left: 0, width: EDGE_WIDTH })
|
|
80
|
+
.activeOffsetX(12)
|
|
81
|
+
.failOffsetY([-16, 16])
|
|
82
|
+
.onStart(() => {
|
|
83
|
+
runOnJS(dismissKeyboard)();
|
|
84
|
+
})
|
|
85
|
+
.onUpdate(e => {
|
|
86
|
+
if (closing.value) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
x.value = Math.min(Math.max(e.translationX, 0), width.value);
|
|
90
|
+
})
|
|
91
|
+
.onEnd(e => {
|
|
92
|
+
if (closing.value) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const complete =
|
|
96
|
+
x.value > width.value * COMPLETE_DISTANCE ||
|
|
97
|
+
e.velocityX > COMPLETE_VELOCITY;
|
|
98
|
+
if (complete) {
|
|
99
|
+
closing.value = true;
|
|
100
|
+
x.value = withTiming(width.value, POP, finished => {
|
|
101
|
+
if (finished) {
|
|
102
|
+
runOnJS(finish)();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
x.value = withTiming(0, POP);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const screenStyle = useAnimatedStyle(() => ({
|
|
111
|
+
transform: [{ translateX: x.value }],
|
|
112
|
+
}));
|
|
113
|
+
const dimStyle = useAnimatedStyle(() => ({
|
|
114
|
+
opacity: interpolate(x.value, [0, width.value], [LIST_DIM, 0]),
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<View
|
|
119
|
+
pointerEvents="box-none"
|
|
120
|
+
style={styles.host}
|
|
121
|
+
onLayout={e => {
|
|
122
|
+
const next = e.nativeEvent.layout.width;
|
|
123
|
+
if (next > 0) {
|
|
124
|
+
width.value = next;
|
|
125
|
+
}
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
<Animated.View
|
|
129
|
+
pointerEvents="none"
|
|
130
|
+
style={[styles.fill, styles.dim, dimStyle]}
|
|
131
|
+
/>
|
|
132
|
+
<GestureDetector gesture={swipeBack}>
|
|
133
|
+
<Animated.View style={[styles.fill, styles.screen, screenStyle]}>
|
|
134
|
+
{children(close)}
|
|
135
|
+
</Animated.View>
|
|
136
|
+
</GestureDetector>
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const styles = StyleSheet.create({
|
|
142
|
+
host: {
|
|
143
|
+
position: 'absolute',
|
|
144
|
+
top: 0,
|
|
145
|
+
right: 0,
|
|
146
|
+
bottom: 0,
|
|
147
|
+
left: 0,
|
|
148
|
+
zIndex: 1000,
|
|
149
|
+
overflow: 'hidden',
|
|
150
|
+
},
|
|
151
|
+
fill: {
|
|
152
|
+
position: 'absolute',
|
|
153
|
+
top: 0,
|
|
154
|
+
right: 0,
|
|
155
|
+
bottom: 0,
|
|
156
|
+
left: 0,
|
|
157
|
+
},
|
|
158
|
+
dim: {
|
|
159
|
+
backgroundColor: '#000',
|
|
160
|
+
},
|
|
161
|
+
// Same box the shared ModalBackground gives its content.
|
|
162
|
+
screen: {
|
|
163
|
+
justifyContent: 'center',
|
|
164
|
+
alignItems: 'center',
|
|
165
|
+
shadowColor: '#000',
|
|
166
|
+
shadowOffset: { width: -4, height: 0 },
|
|
167
|
+
shadowOpacity: 0.18,
|
|
168
|
+
shadowRadius: 12,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
@@ -4,6 +4,7 @@ import React from 'react';
|
|
|
4
4
|
import {
|
|
5
5
|
Animated,
|
|
6
6
|
Image,
|
|
7
|
+
Platform,
|
|
7
8
|
StyleProp,
|
|
8
9
|
StyleSheet,
|
|
9
10
|
Text,
|
|
@@ -39,7 +40,13 @@ const COLLAPSE_RAMP = 48;
|
|
|
39
40
|
*/
|
|
40
41
|
const BLUR_INTENSITY = 92;
|
|
41
42
|
/** Darkening applied on top of the blur (and used alone without it). */
|
|
42
|
-
const FROST_DIM = 'rgba(0,0,0,0.
|
|
43
|
+
const FROST_DIM = 'rgba(0,0,0,0.5)';
|
|
44
|
+
/**
|
|
45
|
+
* Blur of the picture itself when expo-blur is not in the binary: RN's own
|
|
46
|
+
* `Image.blurRadius`, so the collapsed header is still frosted rather than
|
|
47
|
+
* just dimmed. Android caps the radius at 25.
|
|
48
|
+
*/
|
|
49
|
+
const IMAGE_BLUR_RADIUS = Platform.OS === 'android' ? 25 : 40;
|
|
43
50
|
/** Height of the readability gradient under the name and buttons. */
|
|
44
51
|
const SCRIM_HEIGHT = 220;
|
|
45
52
|
/** Bands the gradient is built from — enough that no step is visible. */
|
|
@@ -249,12 +256,25 @@ export const ProfileHero: React.FC<
|
|
|
249
256
|
]}
|
|
250
257
|
>
|
|
251
258
|
{onPhoto ? (
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
<>
|
|
260
|
+
<Image
|
|
261
|
+
testID={`${testIDPrefix}-hero-image`}
|
|
262
|
+
source={{ uri: imageUri as string }}
|
|
263
|
+
style={StyleSheet.absoluteFill}
|
|
264
|
+
resizeMode="cover"
|
|
265
|
+
/>
|
|
266
|
+
{/* Without expo-blur the frost is a blurred copy of the same
|
|
267
|
+
* picture, inside the same transform so the two line up as
|
|
268
|
+
* one fades into the other. */}
|
|
269
|
+
{!BlurView && (
|
|
270
|
+
<Animated.Image
|
|
271
|
+
source={{ uri: imageUri as string }}
|
|
272
|
+
style={[StyleSheet.absoluteFill, { opacity: collapsed }]}
|
|
273
|
+
resizeMode="cover"
|
|
274
|
+
blurRadius={IMAGE_BLUR_RADIUS}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
</>
|
|
258
278
|
) : (
|
|
259
279
|
<View style={styles.initialsWrap}>
|
|
260
280
|
<Text
|
|
@@ -395,6 +415,7 @@ export const ProfileTopBar: React.FC<
|
|
|
395
415
|
source={{ uri: imageUri as string }}
|
|
396
416
|
style={StyleSheet.absoluteFill}
|
|
397
417
|
resizeMode="cover"
|
|
418
|
+
blurRadius={BlurView ? 0 : IMAGE_BLUR_RADIUS}
|
|
398
419
|
/>
|
|
399
420
|
<FrostLayer />
|
|
400
421
|
</>
|
|
@@ -499,7 +520,7 @@ const styles = StyleSheet.create({
|
|
|
499
520
|
backgroundColor: FROST_DIM,
|
|
500
521
|
},
|
|
501
522
|
frostFallback: {
|
|
502
|
-
backgroundColor:
|
|
523
|
+
backgroundColor: FROST_DIM,
|
|
503
524
|
},
|
|
504
525
|
// Only over a photo: keeps white text readable without tinting the flat
|
|
505
526
|
// fallback colour.
|
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
setActiveFile,
|
|
40
40
|
setActiveModal,
|
|
41
41
|
setLangSource,
|
|
42
|
-
setSelectedUser,
|
|
43
42
|
} from '../../../roomStore/chatSettingsSlice';
|
|
44
43
|
import { addRoomViaApi, setCurrentRoom } from '../../../roomStore/roomsSlice';
|
|
45
44
|
import { runLogoutFlow } from '../../Menu/HeaderRoomListMenu';
|
|
@@ -183,10 +182,10 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
|
|
|
183
182
|
};
|
|
184
183
|
}, [isOwnProfile, user?.token]);
|
|
185
184
|
|
|
185
|
+
// The modal host clears `selectedUser` once the page has slid away.
|
|
186
186
|
const handleBackClick = useCallback(() => {
|
|
187
|
-
dispatch(setSelectedUser(undefined));
|
|
188
187
|
handleCloseModal();
|
|
189
|
-
}, [
|
|
188
|
+
}, [handleCloseModal]);
|
|
190
189
|
|
|
191
190
|
// Same teardown the room-list menu runs (XMPP, redux, persisted slices,
|
|
192
191
|
// AsyncStorage), including the host's confirm copy and callbacks.
|
|
@@ -473,6 +472,10 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
|
|
|
473
472
|
]}
|
|
474
473
|
onLayout={(e) => setViewportHeight(e.nativeEvent.layout.height)}
|
|
475
474
|
showsVerticalScrollIndicator={false}
|
|
475
|
+
// Magnet: a header released part-way settles fully open or fully
|
|
476
|
+
// collapsed; past the collapsed point the content scrolls freely.
|
|
477
|
+
snapToOffsets={[0, collapseDistance]}
|
|
478
|
+
snapToEnd={false}
|
|
476
479
|
scrollEventThrottle={16}
|
|
477
480
|
onScroll={Animated.event(
|
|
478
481
|
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
|
|
@@ -6,6 +6,9 @@ type KeyboardLayoutArgs = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export const ANDROID_INPUT_DOCK_GAP = 12;
|
|
9
|
+
// Bottom padding the dock keeps while the iOS keyboard is open, so the
|
|
10
|
+
// composer is glued to the keyboard without touching its edge.
|
|
11
|
+
export const IOS_KEYBOARD_DOCK_GAP = 4;
|
|
9
12
|
|
|
10
13
|
export const getKeyboardVerticalOffset = ({
|
|
11
14
|
platform,
|
|
@@ -14,6 +17,29 @@ export const getKeyboardVerticalOffset = ({
|
|
|
14
17
|
}: KeyboardLayoutArgs): number =>
|
|
15
18
|
configuredOffset + (platform === 'ios' ? bottomInset : 0);
|
|
16
19
|
|
|
20
|
+
// Offset for the KeyboardAvoidingView strategy. Unlike KeyboardStickyView
|
|
21
|
+
// (where a positive `opened` offset pushes the dock DOWN into the keyboard),
|
|
22
|
+
// a positive value here ADDS bottom padding above the keyboard — so the
|
|
23
|
+
// safe-area inset must not be added: the avoiding view already ends exactly
|
|
24
|
+
// at the keyboard's top edge.
|
|
25
|
+
export const getKeyboardAvoidingOffset = ({
|
|
26
|
+
configuredOffset = 0,
|
|
27
|
+
}: Pick<KeyboardLayoutArgs, 'configuredOffset'>): number => configuredOffset;
|
|
28
|
+
|
|
29
|
+
// Dock bottom padding while the keyboard is open (KeyboardAvoidingView
|
|
30
|
+
// strategy). On iOS the safe-area padding collapses to a small gap; Android
|
|
31
|
+
// keeps its fixed gap.
|
|
32
|
+
export const getInputDockKeyboardPadding = ({
|
|
33
|
+
platform,
|
|
34
|
+
inputDockPaddingBottom,
|
|
35
|
+
}: {
|
|
36
|
+
platform: string;
|
|
37
|
+
inputDockPaddingBottom: number;
|
|
38
|
+
}): number =>
|
|
39
|
+
platform === 'ios'
|
|
40
|
+
? Math.min(inputDockPaddingBottom, IOS_KEYBOARD_DOCK_GAP)
|
|
41
|
+
: inputDockPaddingBottom;
|
|
42
|
+
|
|
17
43
|
type InputDockPaddingArgs = Omit<KeyboardLayoutArgs, 'configuredOffset'> & {
|
|
18
44
|
// Explicit `inputDockPaddingBottom` config value, verbatim on both
|
|
19
45
|
// platforms when provided (0 allowed). Takes priority over every other
|