@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,15 @@
1
+ import React, { ReactNode } from 'react';
2
+ interface SwipeBackLayerProps {
3
+ /** Called once the screen has left (swipe, back button, hardware back). */
4
+ onClose: () => void;
5
+ /** Receives the animated close, to hand to the screen's own back button. */
6
+ children: (close: () => void) => ReactNode;
7
+ }
8
+ /**
9
+ * Full-screen modal layer with the same navigation feel as RoomStack: it
10
+ * slides in from the right, and a swipe from the left edge drags it back
11
+ * out over whatever is underneath (the room or the list).
12
+ */
13
+ export declare const SwipeBackLayer: React.FC<SwipeBackLayerProps>;
14
+ export {};
15
+ //# sourceMappingURL=SwipeBackLayer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SwipeBackLayer.d.ts","sourceRoot":"","sources":["../../../../../src/components/Modals/Modal/SwipeBackLayer.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,SAAS,EAAkC,MAAM,OAAO,CAAC;AAyBzE,UAAU,mBAAmB;IAC3B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC;CAC5C;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,EAAE,CAAC,mBAAmB,CAqGxD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ProfileHeader.d.ts","sourceRoot":"","sources":["../../../../../src/components/Modals/ProfileHeader/ProfileHeader.tsx"],"names":[],"mappings":"AAAA,cAAc;AAEd,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EACL,QAAQ,EAER,SAAS,EAGT,SAAS,EAGV,MAAM,cAAc,CAAC;AAKtB,yCAAyC;AACzC,eAAO,MAAM,WAAW,MAAM,CAAC;AAC/B,2DAA2D;AAC3D,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,qEAAqE;AACrE,eAAO,MAAM,YAAY,QAAiC,CAAC;AAyC3D,wEAAwE;AACxE,eAAO,MAAM,aAAa,eAAmB,CAAC;AAyC9C,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC;IACxB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB;;;IAMzB,uEAAuE;;CAG1E,CAAC;AAYF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAChC,WAAW,GAAG;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sEAAsE;IACtE,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACjC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACtC,CAyKF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAClC,WAAW,GAAG;IACZ,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACnC,CAyGF,CAAC"}
1
+ {"version":3,"file":"ProfileHeader.d.ts","sourceRoot":"","sources":["../../../../../src/components/Modals/ProfileHeader/ProfileHeader.tsx"],"names":[],"mappings":"AAAA,cAAc;AAEd,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EACL,QAAQ,EAGR,SAAS,EAGT,SAAS,EAGV,MAAM,cAAc,CAAC;AAKtB,yCAAyC;AACzC,eAAO,MAAM,WAAW,MAAM,CAAC;AAC/B,2DAA2D;AAC3D,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,qEAAqE;AACrE,eAAO,MAAM,YAAY,QAAiC,CAAC;AA+C3D,wEAAwE;AACxE,eAAO,MAAM,aAAa,eAAmB,CAAC;AAyC9C,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC;IACxB,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB;;;IAMzB,uEAAuE;;CAG1E,CAAC;AAYF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAChC,WAAW,GAAG;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,sEAAsE;IACtE,cAAc,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACjC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACtC,CAsLF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAClC,WAAW,GAAG;IACZ,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACnC,CA0GF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"UserProfileModal.d.ts","sourceRoot":"","sources":["../../../../../src/components/Modals/UserProfileModal/UserProfileModal.tsx"],"names":[],"mappings":"AAAA,cAAc;AAEd,OAAO,KAA4D,MAAM,OAAO,CAAC;AAgEjF,KAAK,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;AAEjD,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,GAAG,IAAI,KAAG,MAOnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM;IAChC,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,KAAG,MAAM,EAOT,CAAC;AAEF,UAAU,qBAAqB;IAC7B,gBAAgB,EAAE,GAAG,CAAC;CACvB;AAED,QAAA,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA6erD,CAAC;AAqIF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"UserProfileModal.d.ts","sourceRoot":"","sources":["../../../../../src/components/Modals/UserProfileModal/UserProfileModal.tsx"],"names":[],"mappings":"AAAA,cAAc;AAEd,OAAO,KAA4D,MAAM,OAAO,CAAC;AA+DjF,KAAK,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;AAEjD,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,GAAG,IAAI,KAAG,MAOnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM;IAChC,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,KAAG,MAAM,EAOT,CAAC;AAEF,UAAU,qBAAqB;IAC7B,gBAAgB,EAAE,GAAG,CAAC;CACvB;AAED,QAAA,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAifrD,CAAC;AAqIF,eAAe,gBAAgB,CAAC"}
@@ -5,7 +5,13 @@ type KeyboardLayoutArgs = {
5
5
  keyboardVisible?: boolean;
6
6
  };
7
7
  export declare const ANDROID_INPUT_DOCK_GAP = 12;
8
+ export declare const IOS_KEYBOARD_DOCK_GAP = 4;
8
9
  export declare const getKeyboardVerticalOffset: ({ platform, configuredOffset, bottomInset, }: KeyboardLayoutArgs) => number;
10
+ export declare const getKeyboardAvoidingOffset: ({ configuredOffset, }: Pick<KeyboardLayoutArgs, "configuredOffset">) => number;
11
+ export declare const getInputDockKeyboardPadding: ({ platform, inputDockPaddingBottom, }: {
12
+ platform: string;
13
+ inputDockPaddingBottom: number;
14
+ }) => number;
9
15
  type InputDockPaddingArgs = Omit<KeyboardLayoutArgs, 'configuredOffset'> & {
10
16
  configuredPadding?: number;
11
17
  hostOwnsLayout?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"keyboardLayout.d.ts","sourceRoot":"","sources":["../../../src/helpers/keyboardLayout.ts"],"names":[],"mappings":"AAAA,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,yBAAyB,GAAI,8CAIvC,kBAAkB,KAAG,MACmC,CAAC;AAE5D,KAAK,oBAAoB,GAAG,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,GAAG;IAIzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAK3B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,gFAMvC,oBAAoB,KAAG,MAQzB,CAAC"}
1
+ {"version":3,"file":"keyboardLayout.d.ts","sourceRoot":"","sources":["../../../src/helpers/keyboardLayout.ts"],"names":[],"mappings":"AAAA,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAGzC,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,eAAO,MAAM,yBAAyB,GAAI,8CAIvC,kBAAkB,KAAG,MACmC,CAAC;AAO5D,eAAO,MAAM,yBAAyB,GAAI,uBAEvC,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,KAAG,MAA0B,CAAC;AAK7E,eAAO,MAAM,2BAA2B,GAAI,uCAGzC;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB,EAAE,MAAM,CAAC;CAChC,KAAG,MAGwB,CAAC;AAE7B,KAAK,oBAAoB,GAAG,IAAI,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,GAAG;IAIzE,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAK3B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,gFAMvC,oBAAoB,KAAG,MAQzB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethora/chat-component-rn",
3
- "version": "26.8.4",
3
+ "version": "26.8.5",
4
4
  "description": "Drop-in XMPP chat component for React Native / Expo. <Chat config={...}/> and you're done.",
5
5
  "main": "index.js",
6
6
  "module": "lib/module/main.js",
@@ -9,6 +9,7 @@ import { BurgerMenuIcon } from '../../assets/icons';
9
9
  import Button from '../styled/Button';
10
10
  import { useTheme } from '../../hooks/useTheme';
11
11
  import { useT } from '../../i18n/useT';
12
+ import { resolveHeaderHeight } from '../../helpers/headerLayout';
12
13
 
13
14
  interface HeaderRoomListProps {
14
15
  setDrawerOpen: () => void;
@@ -20,6 +21,9 @@ export const HeaderRoomList: FC<HeaderRoomListProps> = ({ setDrawerOpen }) => {
20
21
  const t = useT();
21
22
  const insets = useSafeAreaInsets();
22
23
  const topInset = config?.headerLayout?.safeAreaTop ? insets.top : 0;
24
+ // Same fixed band as ChatHeader / modal headers: the safe-area inset is
25
+ // padded on top and the row (buttons + title) centers in the band below it.
26
+ const headerHeight = resolveHeaderHeight(config?.headerLayout?.height) + topInset;
23
27
 
24
28
  const modalUser: any = selectedUser ?? user;
25
29
 
@@ -45,7 +49,7 @@ export const HeaderRoomList: FC<HeaderRoomListProps> = ({ setDrawerOpen }) => {
45
49
  style={[
46
50
  styles.headerContainer,
47
51
  { backgroundColor: theme.surface, shadowColor: theme.shadow },
48
- topInset ? { paddingTop: 16 + topInset } : null,
52
+ { height: headerHeight, paddingTop: topInset },
49
53
  ]}
50
54
  >
51
55
  {!config?.disableRoomMenu && config?.headerMenu ? (
@@ -66,7 +70,7 @@ export const HeaderRoomList: FC<HeaderRoomListProps> = ({ setDrawerOpen }) => {
66
70
  ) : (
67
71
  <View style={styles.leftContainer} />
68
72
  )}
69
- <View style={styles.centerContainer}>
73
+ <View style={[styles.centerContainer, { top: topInset }]}>
70
74
  {config?.headerLogo ? (
71
75
  HeaderLogo
72
76
  ) : (
@@ -95,9 +99,6 @@ const styles = StyleSheet.create({
95
99
  flexDirection: 'row',
96
100
  alignItems: 'center',
97
101
  paddingHorizontal: 16,
98
- paddingTop: 16,
99
- paddingBottom: 12,
100
- minHeight: 56,
101
102
  justifyContent: 'space-between',
102
103
  borderBottomLeftRadius: 20,
103
104
  borderBottomRightRadius: 20,
@@ -121,6 +122,7 @@ const styles = StyleSheet.create({
121
122
  position: 'absolute',
122
123
  left: 0,
123
124
  right: 0,
125
+ bottom: 0,
124
126
  alignItems: 'center',
125
127
  justifyContent: 'center',
126
128
  },
@@ -48,6 +48,7 @@ import {
48
48
  KeyboardAvoidingView,
49
49
  KeyboardStickyView,
50
50
  } from 'react-native-keyboard-controller';
51
+ import { KeyboardInputDock } from './KeyboardInputDock';
51
52
  import useComposing from '../../hooks/useComposing';
52
53
  import { store } from '../../roomStore';
53
54
  import {
@@ -58,6 +59,8 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
58
59
  import {
59
60
  getInputDockPaddingBottom,
60
61
  getKeyboardVerticalOffset,
62
+ getKeyboardAvoidingOffset,
63
+ getInputDockKeyboardPadding,
61
64
  } from '../../helpers/keyboardLayout';
62
65
 
63
66
  interface ChatRoomProps {
@@ -357,6 +360,13 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
357
360
  configuredPadding: configWithEventHandlers?.inputDockPaddingBottom,
358
361
  hostOwnsLayout: !!configWithEventHandlers?.disableKeyboardAvoidingView,
359
362
  });
363
+ const keyboardAvoidingOffset = getKeyboardAvoidingOffset({
364
+ configuredOffset: configWithEventHandlers?.keyboardVerticalOffset ?? 0,
365
+ });
366
+ const inputDockKeyboardPadding = getInputDockKeyboardPadding({
367
+ platform: Platform.OS,
368
+ inputDockPaddingBottom,
369
+ });
360
370
 
361
371
  // Keyboard avoidance is delegated to react-native-keyboard-controller's
362
372
  // KeyboardAvoidingView. When the HOST app supplies its own keyboard
@@ -394,16 +404,26 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
394
404
  : View;
395
405
  const keyboardWrapperProps = avoidKeyboard
396
406
  ? {
397
- style: { flex: 1 },
407
+ // The dock's colour: the keyboard-height padding below the dock is
408
+ // this view's own box, and it shows around the keyboard's rounded
409
+ // top corners — it must read as a continuation of the dock.
410
+ style: { flex: 1, backgroundColor: theme.surface },
398
411
  behavior: 'padding' as const,
399
- keyboardVerticalOffset,
412
+ keyboardVerticalOffset: keyboardAvoidingOffset,
400
413
  }
401
414
  : { style: { flex: 1 } };
402
415
  // Input dock: a plain View normally; under the sticky strategy it becomes
403
416
  // a KeyboardStickyView so it (and only it) lifts with the keyboard.
417
+ // Under the avoiding-view strategy the dock's safe-area padding collapses
418
+ // while the keyboard is open (KeyboardInputDock), so the composer is glued
419
+ // to the keyboard.
420
+ const collapsingDock =
421
+ avoidKeyboard && inputDockKeyboardPadding !== inputDockPaddingBottom;
404
422
  const InputDockTag: React.ComponentType<any> = stickyInput
405
423
  ? KeyboardStickyView
406
- : View;
424
+ : collapsingDock
425
+ ? KeyboardInputDock
426
+ : View;
407
427
  const inputDockProps: any = {
408
428
  // The dock carries the composer's white surface all the way to the
409
429
  // bottom edge, so it needs the same rounded top and upward shadow —
@@ -422,6 +442,12 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
422
442
  ...(stickyInput
423
443
  ? { offset: { closed: 0, opened: keyboardVerticalOffset } }
424
444
  : {}),
445
+ ...(collapsingDock
446
+ ? {
447
+ closedPadding: inputDockPaddingBottom,
448
+ openedPadding: inputDockKeyboardPadding,
449
+ }
450
+ : {}),
425
451
  };
426
452
 
427
453
  return (
@@ -1,6 +1,7 @@
1
1
  import React, {FC, useEffect, useMemo, useRef, useState} from 'react';
2
2
  import {useDispatch, useSelector} from 'react-redux';
3
3
  import ChatRoom from './ChatRoom';
4
+ import {RoomStack, RoomStackHandle} from './RoomStack';
4
5
  import {
5
6
  setActiveModal,
6
7
  setConfig,
@@ -37,7 +38,7 @@ import {ModalWrapper} from '../Modals/ModalWrapper/ModalWrapper';
37
38
  import {useChatSettingState} from '../../hooks/useChatSettingState';
38
39
  import {useTheme} from '../../hooks/useTheme';
39
40
  import { usePendingNotification } from '../../hooks/usePendingNotification';
40
- import {DeviceEventEmitter, Pressable, Text, View} from 'react-native';
41
+ import {DeviceEventEmitter, Keyboard, Pressable, Text, View} from 'react-native';
41
42
  import {pushLog as devPushLog} from '../../utils/devLogger';
42
43
  import {normalizeRoomJid} from '../../helpers/normalizeRoomJid';
43
44
  import {buildSeedRoom} from '../../helpers/buildSeedRoom';
@@ -70,6 +71,7 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
70
71
 
71
72
  usePendingNotification();
72
73
  const theme = useTheme();
74
+ const roomStackRef = useRef<RoomStackHandle>(null);
73
75
 
74
76
  const [isInited, setInited] = useState(false);
75
77
  const [showModal, setShowModal] = useState(false);
@@ -123,6 +125,14 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
123
125
  dispatch(setDeleteModal({isDeleteModal: false}));
124
126
  };
125
127
 
128
+ // A modal (chat profile, user profile, ...) covers the chat, so the
129
+ // composer's keyboard must not stay open on top of it.
130
+ useEffect(() => {
131
+ if (activeModal) {
132
+ Keyboard.dismiss();
133
+ }
134
+ }, [activeModal]);
135
+
126
136
  // A host drives the reader's language from OUTSIDE the component through
127
137
  // `config.translates.readerLocale` (their own switcher, or the testbed's
128
138
  // Setup tab). Nothing was syncing it into `langSource`, so pinning it only
@@ -425,6 +435,12 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
425
435
  // the list. With `roomJID` or `disableRooms`, skip the list entirely.
426
436
  const showRoomList =
427
437
  !config?.disableRooms && !roomJID && !activeRoomJID;
438
+ // List mode: the room is pushed over the list and can be swiped back
439
+ // (RoomStack). With `roomJID` / `disableRooms` there is no list to return to.
440
+ const listMode = !config?.disableRooms && !roomJID;
441
+ const backToList = () => {
442
+ dispatch(setCurrentRoom({ roomJID: '' }));
443
+ };
428
444
 
429
445
  return (
430
446
  <>
@@ -489,10 +505,35 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
489
505
  Android. Wraps only the chat area — the global Modal /
490
506
  ModalWrapper below stay above it. */}
491
507
  <InteractionsOverlayProvider>
492
- {showRoomList ? (
493
- <RoomList
494
- chats={Object.values(rooms)}
495
- onRoomClick={handleChangeChat}
508
+ {listMode ? (
509
+ <RoomStack
510
+ ref={roomStackRef}
511
+ onBack={backToList}
512
+ roomBackground={theme.chatBackground}
513
+ // An open modal covers the room: back must not pull the
514
+ // room out from under it.
515
+ hardwareBack={!activeModal}
516
+ list={
517
+ <RoomList
518
+ chats={Object.values(rooms)}
519
+ onRoomClick={handleChangeChat}
520
+ />
521
+ }
522
+ room={
523
+ showRoomList ? null : (
524
+ <ChatWrapperBox
525
+ style={{
526
+ ...MainComponentStyles,
527
+ }}>
528
+ <ChatRoom
529
+ CustomMessageComponent={
530
+ CustomMessageComponent || Message
531
+ }
532
+ handleBackClick={() => roomStackRef.current?.pop()}
533
+ />
534
+ </ChatWrapperBox>
535
+ )
536
+ }
496
537
  />
497
538
  ) : (
498
539
  <ChatWrapperBox
@@ -501,13 +542,6 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
501
542
  }}>
502
543
  <ChatRoom
503
544
  CustomMessageComponent={CustomMessageComponent || Message}
504
- handleBackClick={
505
- roomJID || config?.disableRooms
506
- ? undefined
507
- : () => {
508
- dispatch(setCurrentRoom({ roomJID: '' }));
509
- }
510
- }
511
545
  />
512
546
  </ChatWrapperBox>
513
547
  )}
@@ -0,0 +1,47 @@
1
+ import React, { FC } from 'react';
2
+ import { ViewProps } from 'react-native';
3
+ import Animated, {
4
+ interpolate,
5
+ useAnimatedStyle,
6
+ } from 'react-native-reanimated';
7
+ import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller';
8
+
9
+ interface KeyboardInputDockProps extends ViewProps {
10
+ // Bottom padding while the keyboard is closed (the safe-area inset).
11
+ closedPadding: number;
12
+ // Bottom padding while the keyboard is open.
13
+ openedPadding: number;
14
+ }
15
+
16
+ /**
17
+ * Input dock for the KeyboardAvoidingView strategy on iOS. The dock's
18
+ * safe-area padding is only needed while the keyboard is closed; with the
19
+ * keyboard open it collapses in sync with the keyboard animation, so the
20
+ * composer sits right on top of the keyboard instead of leaving a band of
21
+ * dock (or chat background) between them.
22
+ */
23
+ export const KeyboardInputDock: FC<KeyboardInputDockProps> = ({
24
+ closedPadding,
25
+ openedPadding,
26
+ style,
27
+ children,
28
+ ...rest
29
+ }) => {
30
+ const { progress } = useReanimatedKeyboardAnimation();
31
+ const animatedStyle = useAnimatedStyle(
32
+ () => ({
33
+ paddingBottom: interpolate(
34
+ progress.value,
35
+ [0, 1],
36
+ [closedPadding, openedPadding]
37
+ ),
38
+ }),
39
+ [closedPadding, openedPadding]
40
+ );
41
+
42
+ return (
43
+ <Animated.View {...rest} style={[style, animatedStyle]}>
44
+ {children}
45
+ </Animated.View>
46
+ );
47
+ };
@@ -0,0 +1,236 @@
1
+ import React, {
2
+ forwardRef,
3
+ ReactNode,
4
+ useCallback,
5
+ useEffect,
6
+ useImperativeHandle,
7
+ useRef,
8
+ } from 'react';
9
+ import {
10
+ BackHandler,
11
+ Keyboard,
12
+ LayoutChangeEvent,
13
+ StyleSheet,
14
+ useWindowDimensions,
15
+ View,
16
+ } from 'react-native';
17
+ import Animated, {
18
+ Easing,
19
+ interpolate,
20
+ runOnJS,
21
+ useAnimatedStyle,
22
+ useSharedValue,
23
+ withTiming,
24
+ } from 'react-native-reanimated';
25
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
26
+
27
+ // Width of the left-edge strip that starts the back swipe.
28
+ export const EDGE_WIDTH = 28;
29
+ // Released past this share of the width (or faster than the velocity
30
+ // below), the swipe completes; otherwise the room settles back.
31
+ export const COMPLETE_DISTANCE = 0.35;
32
+ export const COMPLETE_VELOCITY = 600;
33
+ // How far the list underneath is shifted left while fully covered.
34
+ const LIST_PARALLAX = 0.3;
35
+ export const LIST_DIM = 0.25;
36
+
37
+ export const PUSH = { duration: 320, easing: Easing.bezier(0.2, 0.8, 0.2, 1) };
38
+ export const POP = { duration: 240, easing: Easing.out(Easing.cubic) };
39
+
40
+ export interface RoomStackHandle {
41
+ /** Slide the room out, then call `onBack`. */
42
+ pop: () => void;
43
+ }
44
+
45
+ interface RoomStackProps {
46
+ /** The room list — stays mounted under the room so it is there to reveal. */
47
+ list: ReactNode;
48
+ /** The open room, or null while only the list is shown. */
49
+ room: ReactNode | null;
50
+ /** Called once the room has left the screen (swipe, back button, pop()). */
51
+ onBack: () => void;
52
+ /** Background behind the room's content. */
53
+ roomBackground?: string;
54
+ /** Android hardware back / system back gesture returns to the list. */
55
+ hardwareBack?: boolean;
56
+ }
57
+
58
+ /**
59
+ * Two-layer stack for the list → room navigation: the room slides in over
60
+ * the list, and a swipe from the left edge drags it back out, following the
61
+ * finger, with the list parallaxing in underneath (the iOS navigation feel).
62
+ */
63
+ export const RoomStack = forwardRef<RoomStackHandle, RoomStackProps>(
64
+ ({ list, room, onBack, roomBackground, hardwareBack = true }, ref) => {
65
+ const { width: windowWidth } = useWindowDimensions();
66
+ const width = useSharedValue(windowWidth);
67
+ const hasRoom = !!room;
68
+ // Room's horizontal offset: 0 = covering the list, `width` = off-screen.
69
+ // A room already open on the first render is shown in place.
70
+ const x = useSharedValue(hasRoom ? 0 : windowWidth);
71
+ const closing = useSharedValue(false);
72
+
73
+ const isFirstRender = useRef(true);
74
+ useEffect(() => {
75
+ if (isFirstRender.current) {
76
+ isFirstRender.current = false;
77
+ return;
78
+ }
79
+ closing.value = false;
80
+ if (hasRoom) {
81
+ x.value = width.value;
82
+ x.value = withTiming(0, PUSH);
83
+ } else {
84
+ x.value = width.value;
85
+ }
86
+ }, [hasRoom]); // eslint-disable-line react-hooks/exhaustive-deps
87
+
88
+ const onBackRef = useRef(onBack);
89
+ onBackRef.current = onBack;
90
+ const finish = useCallback(() => onBackRef.current(), []);
91
+
92
+ const pop = useCallback(() => {
93
+ if (closing.value) {
94
+ return;
95
+ }
96
+ closing.value = true;
97
+ Keyboard.dismiss();
98
+ x.value = withTiming(width.value, POP, finished => {
99
+ if (finished) {
100
+ runOnJS(finish)();
101
+ }
102
+ });
103
+ }, [finish]); // eslint-disable-line react-hooks/exhaustive-deps
104
+
105
+ useImperativeHandle(ref, () => ({ pop }), [pop]);
106
+
107
+ useEffect(() => {
108
+ if (!hasRoom || !hardwareBack) {
109
+ return undefined;
110
+ }
111
+ const sub = BackHandler.addEventListener('hardwareBackPress', () => {
112
+ pop();
113
+ return true;
114
+ });
115
+ return () => sub.remove();
116
+ }, [hasRoom, hardwareBack, pop]);
117
+
118
+ const dismissKeyboard = useCallback(() => Keyboard.dismiss(), []);
119
+
120
+ const swipeBack = Gesture.Pan()
121
+ .hitSlop({ left: 0, width: EDGE_WIDTH })
122
+ .activeOffsetX(12)
123
+ .failOffsetY([-16, 16])
124
+ .onStart(() => {
125
+ runOnJS(dismissKeyboard)();
126
+ })
127
+ .onUpdate(e => {
128
+ if (closing.value) {
129
+ return;
130
+ }
131
+ x.value = Math.min(Math.max(e.translationX, 0), width.value);
132
+ })
133
+ .onEnd(e => {
134
+ if (closing.value) {
135
+ return;
136
+ }
137
+ const complete =
138
+ x.value > width.value * COMPLETE_DISTANCE ||
139
+ e.velocityX > COMPLETE_VELOCITY;
140
+ if (complete) {
141
+ closing.value = true;
142
+ x.value = withTiming(width.value, POP, finished => {
143
+ if (finished) {
144
+ runOnJS(finish)();
145
+ }
146
+ });
147
+ } else {
148
+ x.value = withTiming(0, POP);
149
+ }
150
+ });
151
+
152
+ const onLayout = useCallback((e: LayoutChangeEvent) => {
153
+ const next = e.nativeEvent.layout.width;
154
+ if (next > 0 && next !== width.value) {
155
+ // Keep an off-screen room off-screen when the container resizes.
156
+ if (x.value === width.value) {
157
+ x.value = next;
158
+ }
159
+ width.value = next;
160
+ }
161
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
162
+
163
+ const roomStyle = useAnimatedStyle(() => ({
164
+ transform: [{ translateX: x.value }],
165
+ }));
166
+ // 1 while the room fully covers the list, 0 once it is gone.
167
+ const listStyle = useAnimatedStyle(() => {
168
+ const covered = 1 - Math.min(Math.max(x.value / width.value, 0), 1);
169
+ return {
170
+ transform: [{ translateX: -width.value * LIST_PARALLAX * covered }],
171
+ };
172
+ });
173
+ const dimStyle = useAnimatedStyle(() => ({
174
+ opacity: interpolate(x.value, [0, width.value], [LIST_DIM, 0]),
175
+ }));
176
+
177
+ return (
178
+ <View style={styles.container} onLayout={onLayout}>
179
+ <Animated.View
180
+ style={[styles.layer, listStyle]}
181
+ pointerEvents={hasRoom ? 'none' : 'auto'}
182
+ accessibilityElementsHidden={hasRoom}
183
+ importantForAccessibility={hasRoom ? 'no-hide-descendants' : 'auto'}
184
+ >
185
+ {list}
186
+ </Animated.View>
187
+ {hasRoom && (
188
+ <>
189
+ <Animated.View
190
+ pointerEvents="none"
191
+ style={[styles.layer, styles.dim, dimStyle]}
192
+ />
193
+ <GestureDetector gesture={swipeBack}>
194
+ <Animated.View
195
+ style={[
196
+ styles.layer,
197
+ styles.room,
198
+ { backgroundColor: roomBackground },
199
+ roomStyle,
200
+ ]}
201
+ >
202
+ {room}
203
+ </Animated.View>
204
+ </GestureDetector>
205
+ </>
206
+ )}
207
+ </View>
208
+ );
209
+ }
210
+ );
211
+
212
+ RoomStack.displayName = 'RoomStack';
213
+
214
+ const styles = StyleSheet.create({
215
+ container: {
216
+ flex: 1,
217
+ width: '100%',
218
+ overflow: 'hidden',
219
+ },
220
+ layer: {
221
+ position: 'absolute',
222
+ top: 0,
223
+ right: 0,
224
+ bottom: 0,
225
+ left: 0,
226
+ },
227
+ dim: {
228
+ backgroundColor: '#000',
229
+ },
230
+ room: {
231
+ shadowColor: '#000',
232
+ shadowOffset: { width: -4, height: 0 },
233
+ shadowOpacity: 0.18,
234
+ shadowRadius: 12,
235
+ },
236
+ });
@@ -510,6 +510,10 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
510
510
  ]}
511
511
  onLayout={(e) => setViewportHeight(e.nativeEvent.layout.height)}
512
512
  showsVerticalScrollIndicator={false}
513
+ // Magnet: a header released part-way settles fully open or fully
514
+ // collapsed; past the collapsed point the content scrolls freely.
515
+ snapToOffsets={[0, collapseDistance]}
516
+ snapToEnd={false}
513
517
  keyboardShouldPersistTaps="handled"
514
518
  scrollEventThrottle={16}
515
519
  onScroll={Animated.event(
@@ -2,9 +2,13 @@ import React from 'react';
2
2
  import { ModalBackground } from '../styledModalComponents';
3
3
  import { ModalType } from '../../../types/types';
4
4
  import { useDispatch } from 'react-redux';
5
- import { setActiveModal } from '../../../roomStore/chatSettingsSlice';
5
+ import {
6
+ setActiveModal,
7
+ setSelectedUser,
8
+ } from '../../../roomStore/chatSettingsSlice';
6
9
  import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
7
10
  import { MODAL_COMPONENTS } from '../../../helpers/constants/MODAL_COMPONENTS';
11
+ import { SwipeBackLayer } from './SwipeBackLayer';
8
12
 
9
13
  interface ModalProps {
10
14
  children?: React.ReactNode;
@@ -44,6 +48,33 @@ const Modal: React.FC<ModalProps> = ({ children, modal, setOpenModal }) => {
44
48
  return <>{renderModalContent()}{children}</>;
45
49
  }
46
50
 
51
+ // The profile screens are full-screen pages rather than dialogs: they
52
+ // slide in and can be swiped back from the left edge, like a room. One
53
+ // layer serves both, so going chat profile → member profile swaps the
54
+ // page in place instead of replaying the entrance.
55
+ if (modal === MODAL_TYPES.PROFILE || modal === MODAL_TYPES.CHAT_PROFILE) {
56
+ const ProfileComponent = MODAL_COMPONENTS[modal];
57
+ if (ProfileComponent) {
58
+ return (
59
+ <SwipeBackLayer
60
+ onClose={() => {
61
+ // The viewed member is cleared only once the page is gone, so
62
+ // it doesn't flip to the user's own profile mid-slide.
63
+ dispatch(setSelectedUser(undefined));
64
+ setOpenModal();
65
+ }}
66
+ >
67
+ {(close) => (
68
+ <>
69
+ <ProfileComponent handleCloseModal={close} />
70
+ {children}
71
+ </>
72
+ )}
73
+ </SwipeBackLayer>
74
+ );
75
+ }
76
+ }
77
+
47
78
  return (
48
79
  modal && (
49
80
  <ModalBackground