@ethora/chat-component-rn 26.8.3 → 26.8.4
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/MainComponents/ReduxWrapper.js +43 -6
- package/lib/commonjs/components/MainComponents/ReduxWrapper.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/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/MainComponents/ReduxWrapper.js +47 -10
- package/lib/module/components/MainComponents/ReduxWrapper.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/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/MainComponents/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.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/MainComponents/ReduxWrapper.tsx +52 -11
- package/src/components/Modals/UserSettingsModal/UserSettingsModal.tsx +157 -4
- 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,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 (
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
|
|
3
3
|
import React, { useCallback, useMemo } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Switch,
|
|
7
|
+
Text,
|
|
8
|
+
TouchableOpacity,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
6
12
|
import { ModalContainerFullScreen } from '../styledModalComponents';
|
|
7
13
|
import ModalHeaderComponent from '../ModalHeaderComponent';
|
|
8
14
|
import { ArowDownIcon } from '../../../assets/icons';
|
|
9
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
setActiveModal,
|
|
17
|
+
setPushEnabled,
|
|
18
|
+
setThemePreference,
|
|
19
|
+
} from '../../../roomStore/chatSettingsSlice';
|
|
20
|
+
import type { RootState } from '../../../roomStore';
|
|
10
21
|
import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
|
|
11
22
|
import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
12
23
|
import { chatTextStyle } from '../../../helpers/typography';
|
|
24
|
+
import { savePreferences } from '../../../helpers/preferencesStorage';
|
|
13
25
|
import { useT } from '../../../i18n/useT';
|
|
14
26
|
import { useTheme } from '../../../hooks/useTheme';
|
|
15
|
-
import type { ChatTheme } from '../../../theme/theme';
|
|
27
|
+
import type { ChatTheme, ThemePreference } from '../../../theme/theme';
|
|
16
28
|
|
|
17
29
|
interface UserSettingsModalProps {
|
|
18
30
|
handleCloseModal: any;
|
|
19
31
|
}
|
|
20
32
|
|
|
33
|
+
const APPEARANCES: ThemePreference[] = ['light', 'dark', 'system'];
|
|
34
|
+
|
|
21
35
|
const UserSettingsModal: React.FC<UserSettingsModalProps> = ({
|
|
22
36
|
handleCloseModal,
|
|
23
37
|
}) => {
|
|
@@ -27,6 +41,28 @@ const UserSettingsModal: React.FC<UserSettingsModalProps> = ({
|
|
|
27
41
|
const theme = useTheme();
|
|
28
42
|
const styles = useMemo(() => createStyles(theme), [theme]);
|
|
29
43
|
|
|
44
|
+
const themePreference = useSelector(
|
|
45
|
+
(s: RootState) => s.chatSettingStore.themePreference
|
|
46
|
+
);
|
|
47
|
+
const pushEnabled = useSelector(
|
|
48
|
+
(s: RootState) => s.chatSettingStore.pushEnabled
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// What the picker shows before the user ever touched it: the host's
|
|
52
|
+
// config.dark, so the selected segment matches what is on screen.
|
|
53
|
+
const appearance: ThemePreference =
|
|
54
|
+
themePreference ??
|
|
55
|
+
(config?.dark === 'system' ? 'system' : config?.dark ? 'dark' : 'light');
|
|
56
|
+
|
|
57
|
+
const appearanceLabels: Record<ThemePreference, string> = useMemo(
|
|
58
|
+
() => ({
|
|
59
|
+
light: t('settings.appearance.light'),
|
|
60
|
+
dark: t('settings.appearance.dark'),
|
|
61
|
+
system: t('settings.appearance.system'),
|
|
62
|
+
}),
|
|
63
|
+
[t]
|
|
64
|
+
);
|
|
65
|
+
|
|
30
66
|
const options = useMemo(
|
|
31
67
|
() => [
|
|
32
68
|
{ label: t('settings.manageData.title'), key: MODAL_TYPES.MANAGE_DATA },
|
|
@@ -42,6 +78,31 @@ const UserSettingsModal: React.FC<UserSettingsModalProps> = ({
|
|
|
42
78
|
[dispatch]
|
|
43
79
|
);
|
|
44
80
|
|
|
81
|
+
const handleAppearance = useCallback(
|
|
82
|
+
(pref: ThemePreference) => {
|
|
83
|
+
dispatch(setThemePreference(pref));
|
|
84
|
+
savePreferences({ theme: pref });
|
|
85
|
+
},
|
|
86
|
+
[dispatch]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// The SDK acts on the flag itself (pushRegistration releases / restores
|
|
90
|
+
// the device-token registrations); the host is told so it can mirror
|
|
91
|
+
// the choice on any native push side it owns.
|
|
92
|
+
const onPushNotificationsToggle = config?.eventHandlers?.onPushNotificationsToggle;
|
|
93
|
+
const handlePush = useCallback(
|
|
94
|
+
(enabled: boolean) => {
|
|
95
|
+
dispatch(setPushEnabled(enabled));
|
|
96
|
+
savePreferences({ pushEnabled: enabled });
|
|
97
|
+
if (onPushNotificationsToggle) {
|
|
98
|
+
Promise.resolve(onPushNotificationsToggle(enabled)).catch((err) =>
|
|
99
|
+
console.warn('[settings] onPushNotificationsToggle failed', err)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
[dispatch, onPushNotificationsToggle]
|
|
104
|
+
);
|
|
105
|
+
|
|
45
106
|
return (
|
|
46
107
|
<ModalContainerFullScreen style={styles.screen}>
|
|
47
108
|
<ModalHeaderComponent
|
|
@@ -53,6 +114,51 @@ const UserSettingsModal: React.FC<UserSettingsModalProps> = ({
|
|
|
53
114
|
* hairline dividers — the rows are separate destinations, and the
|
|
54
115
|
* design gives each its own surface. */}
|
|
55
116
|
<View style={styles.body}>
|
|
117
|
+
{!config?.settings?.hideAppearance ? (
|
|
118
|
+
<View style={[styles.card, styles.stackCard]} testID="settings-appearance">
|
|
119
|
+
<Text style={styles.label}>{t('settings.appearance.title')}</Text>
|
|
120
|
+
<View style={styles.segmented} accessibilityRole="radiogroup">
|
|
121
|
+
{APPEARANCES.map((pref) => {
|
|
122
|
+
const selected = pref === appearance;
|
|
123
|
+
return (
|
|
124
|
+
<TouchableOpacity
|
|
125
|
+
key={pref}
|
|
126
|
+
testID={`settings-appearance-${pref}`}
|
|
127
|
+
accessibilityRole="radio"
|
|
128
|
+
accessibilityState={{ selected }}
|
|
129
|
+
activeOpacity={0.7}
|
|
130
|
+
style={[styles.segment, selected && styles.segmentSelected]}
|
|
131
|
+
onPress={() => handleAppearance(pref)}
|
|
132
|
+
>
|
|
133
|
+
<Text
|
|
134
|
+
style={[styles.segmentLabel, selected && styles.segmentLabelSelected]}
|
|
135
|
+
>
|
|
136
|
+
{appearanceLabels[pref]}
|
|
137
|
+
</Text>
|
|
138
|
+
</TouchableOpacity>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</View>
|
|
142
|
+
</View>
|
|
143
|
+
) : null}
|
|
144
|
+
|
|
145
|
+
{!config?.settings?.hidePushToggle ? (
|
|
146
|
+
<View style={styles.card} testID="settings-notifications">
|
|
147
|
+
<View style={styles.switchText}>
|
|
148
|
+
<Text style={styles.label}>{t('settings.notifications.push')}</Text>
|
|
149
|
+
<Text style={styles.hint}>{t('settings.notifications.pushHint')}</Text>
|
|
150
|
+
</View>
|
|
151
|
+
<Switch
|
|
152
|
+
testID="settings-push-switch"
|
|
153
|
+
value={pushEnabled !== false}
|
|
154
|
+
onValueChange={handlePush}
|
|
155
|
+
trackColor={{ true: theme.primary, false: theme.surfaceSecondary }}
|
|
156
|
+
thumbColor={theme.surface}
|
|
157
|
+
ios_backgroundColor={theme.surfaceSecondary}
|
|
158
|
+
/>
|
|
159
|
+
</View>
|
|
160
|
+
) : null}
|
|
161
|
+
|
|
56
162
|
{options.map((option) => (
|
|
57
163
|
<TouchableOpacity
|
|
58
164
|
key={option.key}
|
|
@@ -98,11 +204,58 @@ const createStyles = (theme: ChatTheme) => StyleSheet.create({
|
|
|
98
204
|
shadowRadius: 12,
|
|
99
205
|
elevation: 3,
|
|
100
206
|
},
|
|
207
|
+
// Label above the control instead of beside it: three segments do not
|
|
208
|
+
// fit next to a label on narrow phones.
|
|
209
|
+
stackCard: {
|
|
210
|
+
flexDirection: 'column',
|
|
211
|
+
alignItems: 'stretch',
|
|
212
|
+
gap: 14,
|
|
213
|
+
},
|
|
101
214
|
label: {
|
|
102
215
|
fontSize: 17,
|
|
103
216
|
fontWeight: '500',
|
|
104
217
|
color: theme.text,
|
|
105
218
|
},
|
|
219
|
+
hint: {
|
|
220
|
+
marginTop: 4,
|
|
221
|
+
fontSize: 13,
|
|
222
|
+
lineHeight: 18,
|
|
223
|
+
color: theme.textSecondary,
|
|
224
|
+
},
|
|
225
|
+
switchText: {
|
|
226
|
+
flex: 1,
|
|
227
|
+
paddingRight: 16,
|
|
228
|
+
},
|
|
229
|
+
segmented: {
|
|
230
|
+
flexDirection: 'row',
|
|
231
|
+
backgroundColor: theme.surfaceSecondary,
|
|
232
|
+
borderRadius: 12,
|
|
233
|
+
padding: 3,
|
|
234
|
+
gap: 3,
|
|
235
|
+
},
|
|
236
|
+
segment: {
|
|
237
|
+
flex: 1,
|
|
238
|
+
alignItems: 'center',
|
|
239
|
+
justifyContent: 'center',
|
|
240
|
+
paddingVertical: 9,
|
|
241
|
+
borderRadius: 9,
|
|
242
|
+
},
|
|
243
|
+
segmentSelected: {
|
|
244
|
+
backgroundColor: theme.surface,
|
|
245
|
+
shadowColor: theme.shadow,
|
|
246
|
+
shadowOffset: { width: 0, height: 1 },
|
|
247
|
+
shadowOpacity: 0.08,
|
|
248
|
+
shadowRadius: 3,
|
|
249
|
+
elevation: 2,
|
|
250
|
+
},
|
|
251
|
+
segmentLabel: {
|
|
252
|
+
fontSize: 15,
|
|
253
|
+
fontWeight: '500',
|
|
254
|
+
color: theme.textSecondary,
|
|
255
|
+
},
|
|
256
|
+
segmentLabelSelected: {
|
|
257
|
+
color: theme.text,
|
|
258
|
+
},
|
|
106
259
|
chevron: {
|
|
107
260
|
transform: [{ rotate: '-90deg' }],
|
|
108
261
|
},
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** @format */
|
|
2
|
+
|
|
3
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
4
|
+
import type { ThemePreference } from '../theme/theme';
|
|
5
|
+
|
|
6
|
+
const KEY = '@ethora/preferences';
|
|
7
|
+
|
|
8
|
+
/** Small, unencrypted, per-device user preferences set from Settings. */
|
|
9
|
+
export interface StoredPreferences {
|
|
10
|
+
theme?: ThemePreference;
|
|
11
|
+
pushEnabled?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const loadPreferences = async (): Promise<StoredPreferences> => {
|
|
15
|
+
try {
|
|
16
|
+
const raw = await AsyncStorage.getItem(KEY);
|
|
17
|
+
return raw ? (JSON.parse(raw) as StoredPreferences) : {};
|
|
18
|
+
} catch {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const savePreferences = async (
|
|
24
|
+
patch: StoredPreferences
|
|
25
|
+
): Promise<void> => {
|
|
26
|
+
try {
|
|
27
|
+
const current = await loadPreferences();
|
|
28
|
+
await AsyncStorage.setItem(KEY, JSON.stringify({ ...current, ...patch }));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.warn('[preferences] save failed', error);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const clearPreferences = async (): Promise<void> => {
|
|
35
|
+
try {
|
|
36
|
+
await AsyncStorage.removeItem(KEY);
|
|
37
|
+
} catch {
|
|
38
|
+
/* ignore */
|
|
39
|
+
}
|
|
40
|
+
};
|
package/src/i18n/strings.ts
CHANGED
|
@@ -239,6 +239,13 @@ const en: UiStringTable = {
|
|
|
239
239
|
'settings.blockedUsers.label': 'Blocked Users',
|
|
240
240
|
'settings.blockedUsers.description': 'Users you have blocked cannot send you messages or see when you are online.',
|
|
241
241
|
'settings.blockedUsers.emptyState': "You haven't blocked anyone yet.",
|
|
242
|
+
'settings.appearance.title': 'Appearance',
|
|
243
|
+
'settings.appearance.light': 'Light',
|
|
244
|
+
'settings.appearance.dark': 'Dark',
|
|
245
|
+
'settings.appearance.system': 'System',
|
|
246
|
+
'settings.notifications.title': 'Notifications',
|
|
247
|
+
'settings.notifications.push': 'Push notifications',
|
|
248
|
+
'settings.notifications.pushHint': 'New messages and calls wake this device even when the app is closed.',
|
|
242
249
|
'settings.visibility.title': 'Visibility',
|
|
243
250
|
'settings.visibility.profileLabel': 'Profile Visibility',
|
|
244
251
|
'settings.visibility.open': 'Open (default)',
|
|
@@ -490,6 +497,13 @@ const fr: UiStringTable = {
|
|
|
490
497
|
'settings.blockedUsers.label': 'Utilisateurs bloqués',
|
|
491
498
|
'settings.blockedUsers.description': "Les utilisateurs que vous avez bloqués ne peuvent pas vous envoyer de messages ni voir quand vous êtes en ligne.",
|
|
492
499
|
'settings.blockedUsers.emptyState': "Vous n'avez encore bloqué personne.",
|
|
500
|
+
'settings.appearance.title': 'Apparence',
|
|
501
|
+
'settings.appearance.light': 'Clair',
|
|
502
|
+
'settings.appearance.dark': 'Sombre',
|
|
503
|
+
'settings.appearance.system': 'Système',
|
|
504
|
+
'settings.notifications.title': 'Notifications',
|
|
505
|
+
'settings.notifications.push': 'Notifications push',
|
|
506
|
+
'settings.notifications.pushHint': 'Les nouveaux messages et appels réveillent cet appareil même quand l\'application est fermée.',
|
|
493
507
|
'settings.visibility.title': 'Visibilité',
|
|
494
508
|
'settings.visibility.profileLabel': 'Visibilité du profil',
|
|
495
509
|
'settings.visibility.open': 'Ouvert (par défaut)',
|
|
@@ -741,6 +755,13 @@ const es: UiStringTable = {
|
|
|
741
755
|
'settings.blockedUsers.label': 'Usuarios bloqueados',
|
|
742
756
|
'settings.blockedUsers.description': 'Los usuarios que hayas bloqueado no podrán enviarte mensajes ni ver cuándo estás en línea.',
|
|
743
757
|
'settings.blockedUsers.emptyState': 'Todavía no has bloqueado a nadie.',
|
|
758
|
+
'settings.appearance.title': 'Apariencia',
|
|
759
|
+
'settings.appearance.light': 'Claro',
|
|
760
|
+
'settings.appearance.dark': 'Oscuro',
|
|
761
|
+
'settings.appearance.system': 'Sistema',
|
|
762
|
+
'settings.notifications.title': 'Notificaciones',
|
|
763
|
+
'settings.notifications.push': 'Notificaciones push',
|
|
764
|
+
'settings.notifications.pushHint': 'Los mensajes y llamadas nuevos despiertan este dispositivo incluso con la aplicación cerrada.',
|
|
744
765
|
'settings.visibility.title': 'Visibilidad',
|
|
745
766
|
'settings.visibility.profileLabel': 'Visibilidad del perfil',
|
|
746
767
|
'settings.visibility.open': 'Abierto (predeterminado)',
|
|
@@ -992,6 +1013,13 @@ const pt: UiStringTable = {
|
|
|
992
1013
|
'settings.blockedUsers.label': 'Utilizadores bloqueados',
|
|
993
1014
|
'settings.blockedUsers.description': 'Os utilizadores que bloqueou não lhe podem enviar mensagens nem ver quando está online.',
|
|
994
1015
|
'settings.blockedUsers.emptyState': 'Ainda não bloqueou ninguém.',
|
|
1016
|
+
'settings.appearance.title': 'Aparência',
|
|
1017
|
+
'settings.appearance.light': 'Claro',
|
|
1018
|
+
'settings.appearance.dark': 'Escuro',
|
|
1019
|
+
'settings.appearance.system': 'Sistema',
|
|
1020
|
+
'settings.notifications.title': 'Notificações',
|
|
1021
|
+
'settings.notifications.push': 'Notificações push',
|
|
1022
|
+
'settings.notifications.pushHint': 'Novas mensagens e chamadas acordam este dispositivo mesmo com o aplicativo fechado.',
|
|
995
1023
|
'settings.visibility.title': 'Visibilidade',
|
|
996
1024
|
'settings.visibility.profileLabel': 'Visibilidade do perfil',
|
|
997
1025
|
'settings.visibility.open': 'Aberto (predefinição)',
|
|
@@ -1243,6 +1271,13 @@ const ht: UiStringTable = {
|
|
|
1243
1271
|
'settings.blockedUsers.label': 'Itilizatè bloke',
|
|
1244
1272
|
'settings.blockedUsers.description': 'Itilizatè ou bloke yo pa ka voye mesaj ba ou ni wè lè ou an liy.',
|
|
1245
1273
|
'settings.blockedUsers.emptyState': 'Ou poko bloke pèsonn.',
|
|
1274
|
+
'settings.appearance.title': 'Aparans',
|
|
1275
|
+
'settings.appearance.light': 'Klè',
|
|
1276
|
+
'settings.appearance.dark': 'Fonse',
|
|
1277
|
+
'settings.appearance.system': 'Sistèm',
|
|
1278
|
+
'settings.notifications.title': 'Notifikasyon',
|
|
1279
|
+
'settings.notifications.push': 'Notifikasyon push',
|
|
1280
|
+
'settings.notifications.pushHint': 'Nouvo mesaj ak apèl reveye aparèy sa a menm lè aplikasyon an fèmen.',
|
|
1246
1281
|
'settings.visibility.title': 'Vizibilite',
|
|
1247
1282
|
'settings.visibility.profileLabel': 'Vizibilite pwofil la',
|
|
1248
1283
|
'settings.visibility.open': 'Louvri (default)',
|
|
@@ -1494,6 +1529,13 @@ const zh: UiStringTable = {
|
|
|
1494
1529
|
'settings.blockedUsers.label': '已屏蔽用户',
|
|
1495
1530
|
'settings.blockedUsers.description': '被您屏蔽的用户无法向您发送消息,也无法看到您何时在线。',
|
|
1496
1531
|
'settings.blockedUsers.emptyState': '您还没有屏蔽任何人。',
|
|
1532
|
+
'settings.appearance.title': '外观',
|
|
1533
|
+
'settings.appearance.light': '浅色',
|
|
1534
|
+
'settings.appearance.dark': '深色',
|
|
1535
|
+
'settings.appearance.system': '跟随系统',
|
|
1536
|
+
'settings.notifications.title': '通知',
|
|
1537
|
+
'settings.notifications.push': '推送通知',
|
|
1538
|
+
'settings.notifications.pushHint': '即使应用已关闭,新消息和来电也会唤醒此设备。',
|
|
1497
1539
|
'settings.visibility.title': '可见性',
|
|
1498
1540
|
'settings.visibility.profileLabel': '资料可见性',
|
|
1499
1541
|
'settings.visibility.open': '公开(默认)',
|
package/src/main.ts
CHANGED
|
@@ -121,3 +121,5 @@ export {
|
|
|
121
121
|
useTheme,
|
|
122
122
|
} from './theme';
|
|
123
123
|
export type { ChatTheme, ChatThemeColors, ChatThemeOverrides } from './theme';
|
|
124
|
+
export type { ThemePreference } from './theme/theme';
|
|
125
|
+
export { setThemePreference, setPushEnabled } from './roomStore/chatSettingsSlice';
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { Iso639_1Codes } from '../types/models/language.model';
|
|
13
13
|
import type { TranslateMode } from '../utils/translateModePolicy';
|
|
14
14
|
import { secureUserStorage } from '../helpers/secureUserStorage';
|
|
15
|
+
import { applyThemePreference, type ThemePreference } from '../theme/theme';
|
|
15
16
|
|
|
16
17
|
export interface ChatState {
|
|
17
18
|
user: User;
|
|
@@ -29,6 +30,14 @@ export interface ChatState {
|
|
|
29
30
|
* See utils/translateModePolicy.
|
|
30
31
|
*/
|
|
31
32
|
translateMode?: TranslateMode;
|
|
33
|
+
/**
|
|
34
|
+
* Appearance picked in Settings (light / dark / system). Persisted per
|
|
35
|
+
* device; folded into `config.dark` by setConfig / setThemePreference so
|
|
36
|
+
* every consumer of `config` sees the effective value.
|
|
37
|
+
*/
|
|
38
|
+
themePreference?: ThemePreference;
|
|
39
|
+
/** Push notifications toggle from Settings. `undefined` = on. */
|
|
40
|
+
pushEnabled?: boolean;
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export const unpackAndTransform = (input?: User): User => {
|
|
@@ -126,11 +135,22 @@ const reducers = {
|
|
|
126
135
|
};
|
|
127
136
|
}
|
|
128
137
|
},
|
|
138
|
+
setThemePreference: (
|
|
139
|
+
state: WritableDraft<ChatState>,
|
|
140
|
+
action: PayloadAction<ThemePreference | undefined>
|
|
141
|
+
) => {
|
|
142
|
+
state.themePreference = action.payload;
|
|
143
|
+
state.config = applyThemePreference(state.config as IConfig | undefined, action.payload) as any;
|
|
144
|
+
},
|
|
145
|
+
setPushEnabled: (state: WritableDraft<ChatState>, action: PayloadAction<boolean>) => {
|
|
146
|
+
state.pushEnabled = action.payload;
|
|
147
|
+
},
|
|
129
148
|
setConfig: (state: WritableDraft<ChatState>, action: PayloadAction<IConfig | undefined>) => {
|
|
130
149
|
// Cast away immer's WritableDraft — IConfig contains callable
|
|
131
150
|
// fields (eventHandlers, customComponent) that confuse the draft
|
|
132
|
-
// type inference but are inert at write-time.
|
|
133
|
-
|
|
151
|
+
// type inference but are inert at write-time. The user's in-app
|
|
152
|
+
// appearance choice survives a host re-dispatch.
|
|
153
|
+
state.config = applyThemePreference(action.payload, state.themePreference) as any;
|
|
134
154
|
},
|
|
135
155
|
setActiveModal: (state: WritableDraft<ChatState>, action: PayloadAction<ModalType | undefined>) => {
|
|
136
156
|
state.activeModal = action.payload;
|
|
@@ -220,6 +240,8 @@ export const {
|
|
|
220
240
|
setStoreClient,
|
|
221
241
|
setLangSource,
|
|
222
242
|
setTranslateMode,
|
|
243
|
+
setThemePreference,
|
|
244
|
+
setPushEnabled,
|
|
223
245
|
} = chatSlice.actions;
|
|
224
246
|
|
|
225
247
|
export default chatSlice.reducer;
|
|
@@ -70,7 +70,10 @@ const readSession = (): Session => {
|
|
|
70
70
|
xmppUsername: String(user?.xmppUsername || ''),
|
|
71
71
|
projectName: String(config?.projectName || '').trim(),
|
|
72
72
|
gatewayUrl: String(config?.pushNotifications?.apiUrl || '').trim(),
|
|
73
|
-
|
|
73
|
+
// Off by config (host) or by the user's Settings toggle.
|
|
74
|
+
pushDisabled:
|
|
75
|
+
config?.pushNotifications?.enabled === false ||
|
|
76
|
+
slice?.pushEnabled === false,
|
|
74
77
|
};
|
|
75
78
|
};
|
|
76
79
|
|
|
@@ -233,7 +236,17 @@ const ensureWatching = (): void => {
|
|
|
233
236
|
hookIdentity = null;
|
|
234
237
|
return;
|
|
235
238
|
}
|
|
236
|
-
if (session.pushDisabled
|
|
239
|
+
if (session.pushDisabled) {
|
|
240
|
+
// Switched off (Settings toggle or config): take the device off the
|
|
241
|
+
// backend but keep the tokens, so switching back on re-registers
|
|
242
|
+
// without the host doing anything. `registered` empties at once,
|
|
243
|
+
// so later store updates do not repeat the release.
|
|
244
|
+
if (registered.size > 0) {
|
|
245
|
+
releasePushRegistration().catch(() => undefined);
|
|
246
|
+
}
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (!isReady(session)) {
|
|
237
250
|
return;
|
|
238
251
|
}
|
|
239
252
|
runTokenHook(session);
|
package/src/theme/theme.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** @format */
|
|
2
2
|
|
|
3
|
+
import { Appearance } from 'react-native';
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Chat theme.
|
|
5
7
|
*
|
|
@@ -161,7 +163,10 @@ export const DARK_THEME: ChatThemeColors = {
|
|
|
161
163
|
/** The subset of IConfig the theme depends on (kept structural so the
|
|
162
164
|
* helpers can be called with partial configs in tests). */
|
|
163
165
|
export interface ThemeConfigInput {
|
|
164
|
-
dark
|
|
166
|
+
/** `true` forces dark, `'system'` follows the OS appearance (pass the
|
|
167
|
+
* current scheme as `systemDark` to resolveTheme / isDarkTheme; omitted,
|
|
168
|
+
* the OS scheme is read directly). */
|
|
169
|
+
dark?: boolean | 'system';
|
|
165
170
|
darkColors?: ChatThemeOverrides;
|
|
166
171
|
colors?: {
|
|
167
172
|
primary?: string;
|
|
@@ -179,8 +184,16 @@ export interface ThemeConfigInput {
|
|
|
179
184
|
backgroundChat?: { color?: string };
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
/** The OS appearance right now — the fallback when a caller has no hook. */
|
|
188
|
+
// Optional chaining on `Appearance` itself: tests that mock react-native
|
|
189
|
+
// partially leave it undefined, and a missing module must read as light.
|
|
190
|
+
const systemIsDark = (): boolean => Appearance?.getColorScheme?.() === 'dark';
|
|
191
|
+
|
|
192
|
+
export const isDarkTheme = (
|
|
193
|
+
config?: ThemeConfigInput | null,
|
|
194
|
+
systemDark: boolean = systemIsDark()
|
|
195
|
+
): boolean =>
|
|
196
|
+
config?.dark === 'system' ? systemDark : config?.dark === true;
|
|
184
197
|
|
|
185
198
|
const pick = <T>(...values: (T | undefined | null | '')[]): T | undefined =>
|
|
186
199
|
values.find((v) => v !== undefined && v !== null && v !== '') as
|
|
@@ -196,8 +209,11 @@ const stripUndefined = (o: ChatThemeOverrides): ChatThemeOverrides =>
|
|
|
196
209
|
* Build the effective palette for a config. Pure; memoize at the call
|
|
197
210
|
* site (useTheme does).
|
|
198
211
|
*/
|
|
199
|
-
export const resolveTheme = (
|
|
200
|
-
|
|
212
|
+
export const resolveTheme = (
|
|
213
|
+
config?: ThemeConfigInput | null,
|
|
214
|
+
systemDark?: boolean
|
|
215
|
+
): ChatTheme => {
|
|
216
|
+
if (isDarkTheme(config, systemDark)) {
|
|
201
217
|
const overrides = stripUndefined(config?.darkColors ?? {});
|
|
202
218
|
const primary = pick(overrides.primary, DARK_THEME.primary)!;
|
|
203
219
|
return {
|
|
@@ -241,3 +257,21 @@ export const resolveTheme = (config?: ThemeConfigInput | null): ChatTheme => {
|
|
|
241
257
|
statusBarStyle: 'dark-content',
|
|
242
258
|
};
|
|
243
259
|
};
|
|
260
|
+
|
|
261
|
+
/** In-app appearance choice from Settings; `undefined` = follow the host's config.dark. */
|
|
262
|
+
export type ThemePreference = 'light' | 'dark' | 'system';
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Fold the user's in-app appearance choice into a config: the choice wins
|
|
266
|
+
* over the host's `dark` (a host that wants to lock the theme hides the
|
|
267
|
+
* setting with `settings.hideAppearance`). Returns the same object when
|
|
268
|
+
* nothing changes, so memoised consumers do not re-render.
|
|
269
|
+
*/
|
|
270
|
+
export const applyThemePreference = <T extends { dark?: boolean | 'system' }>(
|
|
271
|
+
config: T | undefined,
|
|
272
|
+
preference?: ThemePreference
|
|
273
|
+
): T | undefined => {
|
|
274
|
+
if (!preference || !config) {return config;}
|
|
275
|
+
const dark = preference === 'system' ? ('system' as const) : preference === 'dark';
|
|
276
|
+
return config.dark === dark ? config : { ...config, dark };
|
|
277
|
+
};
|
package/src/types/types.ts
CHANGED
|
@@ -386,8 +386,10 @@ export interface IConfig {
|
|
|
386
386
|
* not applied — pass brand colours via `darkColors` instead.
|
|
387
387
|
*
|
|
388
388
|
* config: { dark: true, darkColors: { primary: '#7C3AED', surface: '#111' } }
|
|
389
|
+
*
|
|
390
|
+
* `'system'` follows the device appearance (light/dark) automatically.
|
|
389
391
|
*/
|
|
390
|
-
dark?: boolean;
|
|
392
|
+
dark?: boolean | 'system';
|
|
391
393
|
/** Per-entry overrides of the dark palette; see ChatThemeColors. */
|
|
392
394
|
darkColors?: ChatThemeOverrides;
|
|
393
395
|
/** Configurable font family / weights for the chat UI. See TypographyConfig. */
|
|
@@ -787,8 +789,29 @@ export interface IConfig {
|
|
|
787
789
|
firebaseConfig?: FBConfig;
|
|
788
790
|
};
|
|
789
791
|
|
|
792
|
+
/** Settings screen switches. */
|
|
793
|
+
settings?: {
|
|
794
|
+
/** Hide the Light / Dark / System appearance picker (host locks the theme via `dark`). */
|
|
795
|
+
hideAppearance?: boolean;
|
|
796
|
+
/** Hide the push-notifications toggle. */
|
|
797
|
+
hidePushToggle?: boolean;
|
|
798
|
+
};
|
|
799
|
+
|
|
790
800
|
// ----- event hooks -----
|
|
791
801
|
eventHandlers?: {
|
|
802
|
+
/**
|
|
803
|
+
* Fired when the user picks an appearance in Settings (and once on
|
|
804
|
+
* mount with the persisted choice). `isDark` is the resolved value
|
|
805
|
+
* for 'system'. Hosts use it to sync their own chrome (status bar).
|
|
806
|
+
*/
|
|
807
|
+
onThemeChange?: (preference: 'light' | 'dark' | 'system', isDark: boolean) => void;
|
|
808
|
+
/**
|
|
809
|
+
* Fired when the user toggles push notifications in Settings. The SDK
|
|
810
|
+
* itself stops delivering: off releases every device-token
|
|
811
|
+
* registration on the backend (tokens are kept), on registers them
|
|
812
|
+
* again. A host that also owns native push should mirror the choice.
|
|
813
|
+
*/
|
|
814
|
+
onPushNotificationsToggle?: (enabled: boolean) => void | Promise<void>;
|
|
792
815
|
onMessageSent?: (event: {
|
|
793
816
|
message: string;
|
|
794
817
|
roomJID: string;
|
package/src/version.ts
CHANGED