@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.
Files changed (71) hide show
  1. package/lib/commonjs/components/Header/HeaderRoomList.js +12 -7
  2. package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
  3. package/lib/commonjs/components/MainComponents/ChatRoom.js +23 -3
  4. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatWrapper.js +40 -9
  6. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/KeyboardInputDock.js +40 -0
  8. package/lib/commonjs/components/MainComponents/KeyboardInputDock.js.map +1 -0
  9. package/lib/commonjs/components/MainComponents/RoomStack.js +202 -0
  10. package/lib/commonjs/components/MainComponents/RoomStack.js.map +1 -0
  11. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +6 -1
  12. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  13. package/lib/commonjs/components/Modals/Modal/Modal.js +24 -0
  14. package/lib/commonjs/components/Modals/Modal/Modal.js.map +1 -1
  15. package/lib/commonjs/components/Modals/Modal/SwipeBackLayer.js +146 -0
  16. package/lib/commonjs/components/Modals/Modal/SwipeBackLayer.js.map +1 -0
  17. package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js +28 -10
  18. package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -1
  19. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +9 -3
  20. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  21. package/lib/commonjs/helpers/keyboardLayout.js +23 -1
  22. package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
  23. package/lib/module/components/Header/HeaderRoomList.js +12 -7
  24. package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
  25. package/lib/module/components/MainComponents/ChatRoom.js +24 -4
  26. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  27. package/lib/module/components/MainComponents/ChatWrapper.js +41 -10
  28. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  29. package/lib/module/components/MainComponents/KeyboardInputDock.js +33 -0
  30. package/lib/module/components/MainComponents/KeyboardInputDock.js.map +1 -0
  31. package/lib/module/components/MainComponents/RoomStack.js +198 -0
  32. package/lib/module/components/MainComponents/RoomStack.js.map +1 -0
  33. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +6 -1
  34. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  35. package/lib/module/components/Modals/Modal/Modal.js +25 -1
  36. package/lib/module/components/Modals/Modal/Modal.js.map +1 -1
  37. package/lib/module/components/Modals/Modal/SwipeBackLayer.js +140 -0
  38. package/lib/module/components/Modals/Modal/SwipeBackLayer.js.map +1 -0
  39. package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js +29 -11
  40. package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -1
  41. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +10 -4
  42. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  43. package/lib/module/helpers/keyboardLayout.js +20 -0
  44. package/lib/module/helpers/keyboardLayout.js.map +1 -1
  45. package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
  46. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  47. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  48. package/lib/typescript/components/MainComponents/KeyboardInputDock.d.ts +16 -0
  49. package/lib/typescript/components/MainComponents/KeyboardInputDock.d.ts.map +1 -0
  50. package/lib/typescript/components/MainComponents/RoomStack.d.ts +37 -0
  51. package/lib/typescript/components/MainComponents/RoomStack.d.ts.map +1 -0
  52. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  53. package/lib/typescript/components/Modals/Modal/Modal.d.ts.map +1 -1
  54. package/lib/typescript/components/Modals/Modal/SwipeBackLayer.d.ts +15 -0
  55. package/lib/typescript/components/Modals/Modal/SwipeBackLayer.d.ts.map +1 -0
  56. package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts.map +1 -1
  57. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  58. package/lib/typescript/helpers/keyboardLayout.d.ts +6 -0
  59. package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
  60. package/package.json +1 -1
  61. package/src/components/Header/HeaderRoomList.tsx +7 -5
  62. package/src/components/MainComponents/ChatRoom.tsx +29 -3
  63. package/src/components/MainComponents/ChatWrapper.tsx +46 -12
  64. package/src/components/MainComponents/KeyboardInputDock.tsx +47 -0
  65. package/src/components/MainComponents/RoomStack.tsx +236 -0
  66. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -0
  67. package/src/components/Modals/Modal/Modal.tsx +32 -1
  68. package/src/components/Modals/Modal/SwipeBackLayer.tsx +170 -0
  69. package/src/components/Modals/ProfileHeader/ProfileHeader.tsx +29 -8
  70. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +6 -3
  71. 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.28)';
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
- <Image
253
- testID={`${testIDPrefix}-hero-image`}
254
- source={{ uri: imageUri as string }}
255
- style={StyleSheet.absoluteFill}
256
- resizeMode="cover"
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: 'rgba(0,0,0,0.35)',
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
- }, [dispatch, handleCloseModal]);
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