@ethora/chat-component-rn 26.8.3 → 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/CHANGELOG.md +4 -0
- 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/ReduxWrapper.js +43 -6
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- 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/components/Modals/UserSettingsModal/UserSettingsModal.js +136 -3
- package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
- package/lib/commonjs/helpers/keyboardLayout.js +23 -1
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/preferencesStorage.js +44 -0
- package/lib/commonjs/helpers/preferencesStorage.js.map +1 -0
- package/lib/commonjs/i18n/strings.js +42 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/main.js +13 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/roomStore/chatSettingsSlice.js +17 -4
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/commonjs/services/pushRegistration.js +13 -2
- package/lib/commonjs/services/pushRegistration.js.map +1 -1
- package/lib/commonjs/theme/theme.js +28 -4
- package/lib/commonjs/theme/theme.js.map +1 -1
- package/lib/commonjs/version.js +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/ReduxWrapper.js +47 -10
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- 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/components/Modals/UserSettingsModal/UserSettingsModal.js +139 -6
- package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
- package/lib/module/helpers/keyboardLayout.js +20 -0
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/preferencesStorage.js +36 -0
- package/lib/module/helpers/preferencesStorage.js.map +1 -0
- package/lib/module/i18n/strings.js +42 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/main.js +1 -0
- package/lib/module/main.js.map +1 -1
- package/lib/module/roomStore/chatSettingsSlice.js +14 -3
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/services/pushRegistration.js +13 -2
- package/lib/module/services/pushRegistration.js.map +1 -1
- package/lib/module/theme/theme.js +28 -3
- package/lib/module/theme/theme.js.map +1 -1
- package/lib/module/version.js +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/ReduxWrapper.d.ts.map +1 -1
- 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/components/Modals/UserSettingsModal/UserSettingsModal.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/lib/typescript/helpers/preferencesStorage.d.ts +11 -0
- package/lib/typescript/helpers/preferencesStorage.d.ts.map +1 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +2 -0
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts +12 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
- package/lib/typescript/services/pushRegistration.d.ts.map +1 -1
- package/lib/typescript/theme/theme.d.ts +17 -3
- package/lib/typescript/theme/theme.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +23 -1
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/package.json +2 -2
- 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/ReduxWrapper.tsx +52 -11
- 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/components/Modals/UserSettingsModal/UserSettingsModal.tsx +157 -4
- package/src/helpers/keyboardLayout.ts +26 -0
- package/src/helpers/preferencesStorage.ts +40 -0
- package/src/i18n/strings.ts +42 -0
- package/src/main.ts +2 -0
- package/src/roomStore/chatSettingsSlice.ts +24 -2
- package/src/services/pushRegistration.ts +15 -2
- package/src/theme/theme.ts +39 -5
- package/src/types/types.ts +24 -1
- package/src/version.ts +1 -1
|
@@ -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
|
-
{
|
|
493
|
-
<
|
|
494
|
-
|
|
495
|
-
|
|
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
|
+
};
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
-
import React, {useMemo} from 'react';
|
|
2
|
-
import {
|
|
1
|
+
import React, {useEffect, useMemo, useRef} from 'react';
|
|
2
|
+
import {useColorScheme} from 'react-native';
|
|
3
|
+
import {Provider, useDispatch, useSelector} from 'react-redux';
|
|
3
4
|
import {KeyboardProvider} from 'react-native-keyboard-controller';
|
|
4
5
|
import {SafeAreaProvider} from 'react-native-safe-area-context';
|
|
5
6
|
import {ThemeProvider} from 'styled-components/native';
|
|
6
|
-
import {resolveTheme} from '../../theme/theme';
|
|
7
|
+
import {applyThemePreference, resolveTheme} from '../../theme/theme';
|
|
7
8
|
import {store} from '../../roomStore';
|
|
8
|
-
import {
|
|
9
|
+
import type {RootState} from '../../roomStore';
|
|
10
|
+
import {
|
|
11
|
+
setConfig,
|
|
12
|
+
setPushEnabled,
|
|
13
|
+
setThemePreference,
|
|
14
|
+
} from '../../roomStore/chatSettingsSlice';
|
|
15
|
+
import {loadPreferences} from '../../helpers/preferencesStorage';
|
|
9
16
|
import {ConfigUser, IConfig, MessageProps} from '../../types/types';
|
|
10
17
|
import {XmppProvider} from '../../context/xmppProvider';
|
|
11
18
|
import {MessageNotificationProvider} from '../../context/MessageNotificationContext';
|
|
@@ -53,6 +60,42 @@ const ConfigEnabler: React.FC<{config?: IConfig}> = ({config}) => {
|
|
|
53
60
|
return null;
|
|
54
61
|
};
|
|
55
62
|
|
|
63
|
+
const ChatThemeProvider: React.FC<{config?: IConfig; children: React.ReactNode}> = ({
|
|
64
|
+
config,
|
|
65
|
+
children,
|
|
66
|
+
}) => {
|
|
67
|
+
const dispatch = useDispatch();
|
|
68
|
+
const storeConfig = useSelector((s: RootState) => s.chatSettingStore.config);
|
|
69
|
+
const preference = useSelector(
|
|
70
|
+
(s: RootState) => s.chatSettingStore.themePreference,
|
|
71
|
+
);
|
|
72
|
+
const systemDark = useColorScheme() === 'dark';
|
|
73
|
+
const effective = applyThemePreference(storeConfig ?? config, preference);
|
|
74
|
+
const theme = useMemo(
|
|
75
|
+
() => resolveTheme(effective, systemDark),
|
|
76
|
+
[effective, systemDark],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const hydrated = useRef(false);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (hydrated.current) {return;}
|
|
82
|
+
hydrated.current = true;
|
|
83
|
+
loadPreferences().then((prefs) => {
|
|
84
|
+
if (prefs.theme) {dispatch(setThemePreference(prefs.theme));}
|
|
85
|
+
if (typeof prefs.pushEnabled === 'boolean') {
|
|
86
|
+
dispatch(setPushEnabled(prefs.pushEnabled));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}, [dispatch]);
|
|
90
|
+
|
|
91
|
+
const onThemeChange = config?.eventHandlers?.onThemeChange;
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (preference && onThemeChange) {onThemeChange(preference, theme.dark);}
|
|
94
|
+
}, [preference, theme.dark, onThemeChange]);
|
|
95
|
+
|
|
96
|
+
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
|
|
97
|
+
};
|
|
98
|
+
|
|
56
99
|
export const ReduxWrapper: React.FC<ChatWrapperProps> = React.memo(
|
|
57
100
|
({...props}) => {
|
|
58
101
|
const memoizedConfig = useMemo(() => {
|
|
@@ -70,13 +113,11 @@ export const ReduxWrapper: React.FC<ChatWrapperProps> = React.memo(
|
|
|
70
113
|
// KeyboardAvoidingView under the same flag).
|
|
71
114
|
const ownKeyboardHandling = !memoizedConfig?.disableKeyboardAvoidingView;
|
|
72
115
|
|
|
73
|
-
// Light/dark palette (config.dark + config.darkColors
|
|
74
|
-
// components read it via `({ theme }) => theme.surface`;
|
|
75
|
-
// styles via the `useTheme()` hook.
|
|
76
|
-
const theme = useMemo(() => resolveTheme(memoizedConfig), [memoizedConfig]);
|
|
77
|
-
|
|
116
|
+
// Light/dark palette (config.dark + config.darkColors + the Settings
|
|
117
|
+
// choice). Styled components read it via `({ theme }) => theme.surface`;
|
|
118
|
+
// plain RN styles via the `useTheme()` hook.
|
|
78
119
|
const tree = (
|
|
79
|
-
<
|
|
120
|
+
<ChatThemeProvider config={memoizedConfig}>
|
|
80
121
|
<XmppProvider config={memoizedConfig} isVisible={props.isVisible}>
|
|
81
122
|
<ToastProvider>
|
|
82
123
|
<MessageNotificationProvider config={memoizedConfig}>
|
|
@@ -84,7 +125,7 @@ export const ReduxWrapper: React.FC<ChatWrapperProps> = React.memo(
|
|
|
84
125
|
</MessageNotificationProvider>
|
|
85
126
|
</ToastProvider>
|
|
86
127
|
</XmppProvider>
|
|
87
|
-
</
|
|
128
|
+
</ChatThemeProvider>
|
|
88
129
|
);
|
|
89
130
|
|
|
90
131
|
return (
|
|
@@ -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 {
|
|
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
|