@oxyhq/services 19.1.3 → 20.0.0
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/android/build.gradle +21 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/so/oxy/identity/OxyIdentityModule.kt +84 -0
- package/android/src/main/java/so/oxy/identity/OxyIdentityProvider.kt +77 -0
- package/android/src/main/java/so/oxy/identity/OxyIdentityStore.kt +76 -0
- package/expo-module.config.json +9 -0
- package/ios/OxyIdentity.podspec +28 -0
- package/ios/OxyIdentityModule.swift +33 -0
- package/lib/commonjs/ui/components/FollowButton.js +1 -2
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/SettingsIcon.js +1 -2
- package/lib/commonjs/ui/components/SettingsIcon.js.map +1 -1
- package/lib/commonjs/ui/components/logo/LogoIcon.js +0 -2
- package/lib/commonjs/ui/components/logo/LogoIcon.js.map +1 -1
- package/lib/commonjs/ui/components/logo/LogoText.js +0 -2
- package/lib/commonjs/ui/components/logo/LogoText.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +9 -8
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +1 -3
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useLanguageManagement.js +109 -28
- package/lib/commonjs/ui/hooks/useLanguageManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +1 -3
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/screens/LanguageSelectorScreen.js +151 -88
- package/lib/commonjs/ui/screens/LanguageSelectorScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PreferencesScreen.js +5 -6
- package/lib/commonjs/ui/screens/PreferencesScreen.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +1 -1
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/SettingsIcon.js +1 -1
- package/lib/module/ui/components/SettingsIcon.js.map +1 -1
- package/lib/module/ui/components/logo/LogoIcon.js +0 -1
- package/lib/module/ui/components/logo/LogoIcon.js.map +1 -1
- package/lib/module/ui/components/logo/LogoText.js +0 -1
- package/lib/module/ui/components/logo/LogoText.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +9 -8
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +1 -3
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/useLanguageManagement.js +107 -29
- package/lib/module/ui/hooks/useLanguageManagement.js.map +1 -1
- package/lib/module/ui/hooks/useSessionManagement.js +1 -3
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
- package/lib/module/ui/screens/LanguageSelectorScreen.js +153 -91
- package/lib/module/ui/screens/LanguageSelectorScreen.js.map +1 -1
- package/lib/module/ui/screens/PreferencesScreen.js +5 -6
- package/lib/module/ui/screens/PreferencesScreen.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +2 -2
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyPayButton.d.ts +1 -1
- package/lib/typescript/commonjs/ui/components/OxyPayButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SettingsIcon.d.ts +1 -1
- package/lib/typescript/commonjs/ui/components/SettingsIcon.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/logo/LogoIcon.d.ts +1 -1
- package/lib/typescript/commonjs/ui/components/logo/LogoIcon.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/logo/LogoText.d.ts +1 -1
- package/lib/typescript/commonjs/ui/components/logo/LogoText.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts +1 -2
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +3 -0
- package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts +2 -1
- package/lib/typescript/commonjs/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useLanguageManagement.d.ts +52 -8
- package/lib/typescript/commonjs/ui/hooks/useLanguageManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts +1 -2
- package/lib/typescript/commonjs/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/PreferencesScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/FileListSection.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/FileListSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
- package/lib/typescript/commonjs/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts +2 -2
- package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyPayButton.d.ts +1 -1
- package/lib/typescript/module/ui/components/OxyPayButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SettingsIcon.d.ts +1 -1
- package/lib/typescript/module/ui/components/SettingsIcon.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/logo/LogoIcon.d.ts +1 -1
- package/lib/typescript/module/ui/components/logo/LogoIcon.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/logo/LogoText.d.ts +1 -1
- package/lib/typescript/module/ui/components/logo/LogoText.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts +1 -2
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +3 -0
- package/lib/typescript/module/ui/context/oxyContextTypes.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts +2 -1
- package/lib/typescript/module/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useLanguageManagement.d.ts +52 -8
- package/lib/typescript/module/ui/hooks/useLanguageManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts +1 -2
- package/lib/typescript/module/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/LanguageSelectorScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/PreferencesScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/FileListSection.d.ts +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/FileListSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts +1 -1
- package/lib/typescript/module/ui/screens/fileManagement/hooks/useFileUploadState.d.ts.map +1 -1
- package/package.json +31 -18
- package/plugins/withSharedIdentityProvider.js +83 -0
- package/plugins/withSharedIdentityReader.js +46 -0
- package/src/ui/components/FollowButton.tsx +6 -5
- package/src/ui/components/OxyPayButton.tsx +1 -1
- package/src/ui/components/SettingsIcon.tsx +2 -1
- package/src/ui/components/logo/LogoIcon.tsx +2 -1
- package/src/ui/components/logo/LogoText.tsx +2 -1
- package/src/ui/context/OxyContext.tsx +13 -11
- package/src/ui/context/hooks/useAuthOperations.ts +1 -5
- package/src/ui/context/oxyContextTypes.ts +3 -0
- package/src/ui/hooks/useFollow.ts +1 -1
- package/src/ui/hooks/useLanguageManagement.ts +144 -42
- package/src/ui/hooks/useSessionManagement.ts +1 -4
- package/src/ui/screens/FeedbackScreen.tsx +1 -1
- package/src/ui/screens/LanguageSelectorScreen.tsx +195 -104
- package/src/ui/screens/PreferencesScreen.tsx +4 -6
- package/src/ui/screens/fileManagement/FileListSection.tsx +1 -1
- package/src/ui/screens/fileManagement/hooks/useFileUploadState.ts +1 -1
|
@@ -1,110 +1,162 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import { View, StyleSheet, ScrollView } from 'react-native';
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { View, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
|
3
3
|
import type { BaseScreenProps } from '../types/navigation';
|
|
4
4
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
5
|
-
import { normalizeTheme } from '@oxyhq/core';
|
|
6
5
|
import { Ionicons } from '@expo/vector-icons';
|
|
7
6
|
import { toast } from '@oxyhq/bloom';
|
|
8
7
|
import { H1, Text } from '@oxyhq/bloom/typography';
|
|
9
8
|
import { SearchInput } from '@oxyhq/bloom/search-input';
|
|
10
9
|
import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
|
|
10
|
+
import {
|
|
11
|
+
SUPPORTED_LANGUAGES,
|
|
12
|
+
getNativeLanguageName,
|
|
13
|
+
type SupportedLanguage,
|
|
14
|
+
} from '@oxyhq/core';
|
|
11
15
|
import Header from '../components/Header';
|
|
12
16
|
import { useI18n } from '../hooks/useI18n';
|
|
13
|
-
import { SUPPORTED_LANGUAGES } from '@oxyhq/core';
|
|
14
17
|
import { useOxy } from '../context/OxyContext';
|
|
18
|
+
import { addLocale, removeLocale, setPrimaryLocale } from '../hooks/useLanguageManagement';
|
|
19
|
+
import { useUpdateProfile } from '../hooks/mutations/useAccountMutations';
|
|
15
20
|
|
|
16
21
|
interface LanguageSelectorScreenProps extends BaseScreenProps { }
|
|
17
22
|
|
|
18
|
-
// Size (in dp) of the circular
|
|
19
|
-
const
|
|
20
|
-
//
|
|
21
|
-
const
|
|
23
|
+
// Size (in dp) of the circular chip rendered as each row's leading icon.
|
|
24
|
+
const CHIP_SIZE = 40;
|
|
25
|
+
// Trailing remove-affordance icon size (dp).
|
|
26
|
+
const REMOVE_ICON_SIZE = 24;
|
|
27
|
+
|
|
28
|
+
/** O(1) catalog lookup by canonical locale code. */
|
|
29
|
+
const CATALOG_BY_CODE: ReadonlyMap<string, SupportedLanguage> = new Map(
|
|
30
|
+
SUPPORTED_LANGUAGES.map((entry) => [entry.code, entry]),
|
|
31
|
+
);
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
|
-
* LanguageSelectorScreen -
|
|
34
|
+
* LanguageSelectorScreen — multi-select over the supported BCP-47 locales.
|
|
25
35
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
36
|
+
* The user picks one or more locales; the FIRST (primary) drives the app's UI
|
|
37
|
+
* language. When signed in the ordered selection is persisted to the account
|
|
38
|
+
* (`updateProfile({ languages })`, primary first) and every Oxy app follows it;
|
|
39
|
+
* when signed out a single local override is stored on-device.
|
|
30
40
|
*/
|
|
31
41
|
const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
|
|
32
42
|
goBack,
|
|
33
43
|
onClose,
|
|
34
|
-
theme,
|
|
35
44
|
}) => {
|
|
36
|
-
|
|
37
|
-
// on the ACTIVE account's profile — the active session IS that account — so
|
|
38
|
-
// gate the server sync on the active account.
|
|
39
|
-
const { user, currentLanguage, setLanguage, oxyServices, isAuthenticated } = useOxy();
|
|
45
|
+
const { user, isAuthenticated, currentLanguage, currentLanguages, setLanguage } = useOxy();
|
|
40
46
|
const { t } = useI18n();
|
|
41
47
|
const bloomTheme = useTheme();
|
|
42
|
-
const
|
|
43
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
48
|
+
const updateProfile = useUpdateProfile();
|
|
44
49
|
const [query, setQuery] = useState('');
|
|
50
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
45
51
|
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
let serverSyncFailed = false;
|
|
56
|
-
|
|
57
|
-
// If signed in, persist preference to backend user settings
|
|
58
|
-
if (isAuthenticated && user?.id) {
|
|
59
|
-
try {
|
|
60
|
-
await oxyServices.updateProfile({ language: languageId });
|
|
61
|
-
} catch (e: unknown) {
|
|
62
|
-
// Server sync failed, but we'll save locally anyway
|
|
63
|
-
serverSyncFailed = true;
|
|
64
|
-
if (__DEV__) {
|
|
65
|
-
console.warn('Failed to sync language to server (will save locally only):', e instanceof Error ? e.message : e);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
52
|
+
// The current ordered selection: the account locales when signed in, else
|
|
53
|
+
// the single guest/device locale. Always non-empty (`currentLanguage`
|
|
54
|
+
// resolves to the device or fallback locale).
|
|
55
|
+
const selected = useMemo<string[]>(
|
|
56
|
+
() => (currentLanguages.length > 0 ? currentLanguages : [currentLanguage]),
|
|
57
|
+
[currentLanguages, currentLanguage],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const selectedSet = useMemo(() => new Set(selected), [selected]);
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
const selectedEntries = useMemo<SupportedLanguage[]>(
|
|
63
|
+
() =>
|
|
64
|
+
selected
|
|
65
|
+
.map((code) => CATALOG_BY_CODE.get(code))
|
|
66
|
+
.filter((entry): entry is SupportedLanguage => entry !== undefined),
|
|
67
|
+
[selected],
|
|
68
|
+
);
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
// Available = supported locales not already selected, filtered by the query
|
|
71
|
+
// (matches English name + native name).
|
|
72
|
+
const availableEntries = useMemo<SupportedLanguage[]>(() => {
|
|
73
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
74
|
+
return SUPPORTED_LANGUAGES.filter((entry) => {
|
|
75
|
+
if (selectedSet.has(entry.code)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (!normalizedQuery) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
entry.name.toLowerCase().includes(normalizedQuery) ||
|
|
83
|
+
entry.nativeName.toLowerCase().includes(normalizedQuery)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
}, [query, selectedSet]);
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
toast.success(t('language.changed', { lang: selectedLang?.name || languageId }));
|
|
88
|
+
const isBusy = isSubmitting || updateProfile.isPending;
|
|
77
89
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
90
|
+
// Persist the next ordered selection. Signed-in accounts write the full
|
|
91
|
+
// ordered array; guests store only the primary locally.
|
|
92
|
+
const commitSelection = useCallback(
|
|
93
|
+
async (next: string[], announceCode: string): Promise<void> => {
|
|
94
|
+
if (isBusy || next.length === 0) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
setIsSubmitting(true);
|
|
98
|
+
try {
|
|
99
|
+
if (isAuthenticated && user?.id) {
|
|
100
|
+
await updateProfile.mutateAsync({ languages: next });
|
|
101
|
+
} else {
|
|
102
|
+
await setLanguage(next[0]);
|
|
103
|
+
}
|
|
104
|
+
toast.success(t('language.changed', { lang: getNativeLanguageName(announceCode) }));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// `useUpdateProfile` already surfaces a toast on failure; only the
|
|
107
|
+
// local (guest) path needs its own error toast.
|
|
108
|
+
if (!(isAuthenticated && user?.id)) {
|
|
109
|
+
toast.error(t('language.saveFailed'));
|
|
110
|
+
}
|
|
111
|
+
if (__DEV__) {
|
|
112
|
+
console.error('Error saving language preference:', error);
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
setIsSubmitting(false);
|
|
81
116
|
}
|
|
117
|
+
},
|
|
118
|
+
[isAuthenticated, user?.id, updateProfile, setLanguage, t, isBusy],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Adding a locale: signed-in appends (non-primary); a guest can only hold a
|
|
122
|
+
// single locale, so the tapped locale becomes their primary.
|
|
123
|
+
const handleAdd = useCallback(
|
|
124
|
+
(code: string) => {
|
|
125
|
+
const next = isAuthenticated && user?.id ? addLocale(selected, code) : [code];
|
|
126
|
+
void commitSelection(next, code);
|
|
127
|
+
},
|
|
128
|
+
[isAuthenticated, user?.id, selected, commitSelection],
|
|
129
|
+
);
|
|
82
130
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (
|
|
131
|
+
const handleSetPrimary = useCallback(
|
|
132
|
+
(code: string) => {
|
|
133
|
+
if (selected[0] === code) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
void commitSelection(setPrimaryLocale(selected, code), code);
|
|
137
|
+
},
|
|
138
|
+
[selected, commitSelection],
|
|
139
|
+
);
|
|
86
140
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
141
|
+
const handleRemove = useCallback(
|
|
142
|
+
(code: string) => {
|
|
143
|
+
if (selected.length <= 1) {
|
|
144
|
+
return;
|
|
91
145
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
);
|
|
107
|
-
}, [query]);
|
|
146
|
+
const next = removeLocale(selected, code);
|
|
147
|
+
const announceCode = next[0] ?? code;
|
|
148
|
+
void commitSelection(next, announceCode);
|
|
149
|
+
},
|
|
150
|
+
[selected, commitSelection],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const renderChip = (entry: SupportedLanguage) => (
|
|
154
|
+
<View className="bg-fill-secondary items-center justify-center" style={styles.chip}>
|
|
155
|
+
<Text className="text-text-secondary" style={styles.chipLabel}>
|
|
156
|
+
{entry.language.toUpperCase()}
|
|
157
|
+
</Text>
|
|
158
|
+
</View>
|
|
159
|
+
);
|
|
108
160
|
|
|
109
161
|
return (
|
|
110
162
|
<View className="flex-1 bg-bg">
|
|
@@ -120,10 +172,8 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
|
|
|
120
172
|
className="flex-1"
|
|
121
173
|
contentContainerClassName="px-screen-margin pt-space-24 pb-space-24"
|
|
122
174
|
showsVerticalScrollIndicator={false}
|
|
123
|
-
removeClippedSubviews={true}
|
|
124
175
|
keyboardShouldPersistTaps="handled"
|
|
125
176
|
>
|
|
126
|
-
{/* Big Title */}
|
|
127
177
|
<View className="mb-space-16">
|
|
128
178
|
<H1 className="text-text" style={styles.bigTitle}>
|
|
129
179
|
{t('language.title')}
|
|
@@ -135,7 +185,6 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
|
|
|
135
185
|
) : null}
|
|
136
186
|
</View>
|
|
137
187
|
|
|
138
|
-
{/* Search / filter */}
|
|
139
188
|
<View className="mb-space-16">
|
|
140
189
|
<SearchInput
|
|
141
190
|
value={query}
|
|
@@ -145,31 +194,45 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
|
|
|
145
194
|
/>
|
|
146
195
|
</View>
|
|
147
196
|
|
|
148
|
-
{/*
|
|
149
|
-
<SettingsListGroup>
|
|
150
|
-
{
|
|
151
|
-
const
|
|
197
|
+
{/* Selected locales — ordered, primary first. */}
|
|
198
|
+
<SettingsListGroup title={t('language.selected')}>
|
|
199
|
+
{selectedEntries.map((entry, index) => {
|
|
200
|
+
const isPrimary = index === 0;
|
|
201
|
+
const canRemove = selected.length > 1;
|
|
152
202
|
return (
|
|
153
203
|
<SettingsListItem
|
|
154
|
-
key={
|
|
155
|
-
icon={
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
204
|
+
key={entry.code}
|
|
205
|
+
icon={renderChip(entry)}
|
|
206
|
+
title={entry.nativeName}
|
|
207
|
+
description={entry.name}
|
|
208
|
+
onPress={isPrimary ? undefined : () => handleSetPrimary(entry.code)}
|
|
209
|
+
disabled={isBusy}
|
|
210
|
+
accessibilityLabel={
|
|
211
|
+
isPrimary
|
|
212
|
+
? `${entry.name} — ${t('language.primary')}`
|
|
213
|
+
: entry.name
|
|
162
214
|
}
|
|
163
|
-
title={language.name}
|
|
164
|
-
description={language.nativeName}
|
|
165
|
-
onPress={() => handleLanguageSelect(language.id)}
|
|
166
215
|
rightElement={
|
|
167
|
-
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
216
|
+
isPrimary ? (
|
|
217
|
+
<View className="bg-fill-secondary rounded-full px-space-8 py-space-2">
|
|
218
|
+
<Text className="text-primary" style={styles.primaryBadge}>
|
|
219
|
+
{t('language.primary')}
|
|
220
|
+
</Text>
|
|
221
|
+
</View>
|
|
222
|
+
) : canRemove ? (
|
|
223
|
+
<TouchableOpacity
|
|
224
|
+
onPress={() => handleRemove(entry.code)}
|
|
225
|
+
disabled={isBusy}
|
|
226
|
+
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
|
227
|
+
accessibilityRole="button"
|
|
228
|
+
accessibilityLabel={`${t('language.remove')} ${entry.name}`}
|
|
229
|
+
>
|
|
230
|
+
<Ionicons
|
|
231
|
+
name="close-circle"
|
|
232
|
+
size={REMOVE_ICON_SIZE}
|
|
233
|
+
color={bloomTheme.colors.textSecondary}
|
|
234
|
+
/>
|
|
235
|
+
</TouchableOpacity>
|
|
173
236
|
) : undefined
|
|
174
237
|
}
|
|
175
238
|
showChevron={false}
|
|
@@ -177,6 +240,29 @@ const LanguageSelectorScreen: React.FC<LanguageSelectorScreenProps> = ({
|
|
|
177
240
|
);
|
|
178
241
|
})}
|
|
179
242
|
</SettingsListGroup>
|
|
243
|
+
|
|
244
|
+
{/* Available locales. */}
|
|
245
|
+
<SettingsListGroup title={t('language.available')}>
|
|
246
|
+
{availableEntries.map((entry) => (
|
|
247
|
+
<SettingsListItem
|
|
248
|
+
key={entry.code}
|
|
249
|
+
icon={renderChip(entry)}
|
|
250
|
+
title={entry.nativeName}
|
|
251
|
+
description={entry.name}
|
|
252
|
+
onPress={() => handleAdd(entry.code)}
|
|
253
|
+
disabled={isBusy}
|
|
254
|
+
accessibilityLabel={entry.name}
|
|
255
|
+
rightElement={
|
|
256
|
+
<Ionicons
|
|
257
|
+
name="add-circle-outline"
|
|
258
|
+
size={REMOVE_ICON_SIZE}
|
|
259
|
+
color={bloomTheme.colors.textSecondary}
|
|
260
|
+
/>
|
|
261
|
+
}
|
|
262
|
+
showChevron={false}
|
|
263
|
+
/>
|
|
264
|
+
))}
|
|
265
|
+
</SettingsListGroup>
|
|
180
266
|
</ScrollView>
|
|
181
267
|
</View>
|
|
182
268
|
);
|
|
@@ -193,13 +279,18 @@ const styles = StyleSheet.create({
|
|
|
193
279
|
fontSize: 16,
|
|
194
280
|
lineHeight: 22,
|
|
195
281
|
},
|
|
196
|
-
|
|
197
|
-
width:
|
|
198
|
-
height:
|
|
199
|
-
borderRadius:
|
|
282
|
+
chip: {
|
|
283
|
+
width: CHIP_SIZE,
|
|
284
|
+
height: CHIP_SIZE,
|
|
285
|
+
borderRadius: CHIP_SIZE / 2,
|
|
286
|
+
},
|
|
287
|
+
chipLabel: {
|
|
288
|
+
fontSize: 13,
|
|
289
|
+
fontWeight: '600',
|
|
200
290
|
},
|
|
201
|
-
|
|
202
|
-
fontSize:
|
|
291
|
+
primaryBadge: {
|
|
292
|
+
fontSize: 12,
|
|
293
|
+
fontWeight: '600',
|
|
203
294
|
},
|
|
204
295
|
});
|
|
205
296
|
|
|
@@ -4,6 +4,7 @@ import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list'
|
|
|
4
4
|
import { Switch } from '@oxyhq/bloom/switch';
|
|
5
5
|
import { useTheme } from '@oxyhq/bloom/theme';
|
|
6
6
|
import type { UserPreferences } from '@oxyhq/core';
|
|
7
|
+
import { getNativeLanguageName } from '@oxyhq/core';
|
|
7
8
|
import type { BaseScreenProps } from '../types/navigation';
|
|
8
9
|
import Header from '../components/Header';
|
|
9
10
|
import { SettingsIcon } from '../components/SettingsIcon';
|
|
@@ -112,12 +113,9 @@ const PreferencesScreen: React.FC<BaseScreenProps> = ({
|
|
|
112
113
|
const nextTheme: ThemePreference =
|
|
113
114
|
THEME_ORDER[(THEME_ORDER.indexOf(themePref) + 1) % THEME_ORDER.length] ?? 'system';
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
return locale;
|
|
120
|
-
}, [prefs?.language, locale]);
|
|
116
|
+
// The active UI locale is driven by the account's primary language when
|
|
117
|
+
// signed in (else the device locale) — surfaced here via the i18n `locale`.
|
|
118
|
+
const languageDescription = useMemo(() => getNativeLanguageName(locale), [locale]);
|
|
121
119
|
|
|
122
120
|
const isSaving = updateMutation.isPending;
|
|
123
121
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { Ionicons } from '@expo/vector-icons';
|
|
11
11
|
import type { FileMetadata } from '@oxyhq/core';
|
|
12
12
|
import { SettingsListGroup, SettingsListItem } from '@oxyhq/bloom/settings-list';
|
|
13
|
-
import { useTheme } from '@oxyhq/bloom/theme';
|
|
13
|
+
import type { useTheme } from '@oxyhq/bloom/theme';
|
|
14
14
|
import { fileManagementStyles } from '../../components/fileManagement/styles';
|
|
15
15
|
|
|
16
16
|
type ThemeColors = ReturnType<typeof useTheme>['colors'];
|
|
@@ -3,7 +3,7 @@ import { useCallback, useRef, useState } from 'react';
|
|
|
3
3
|
import { toast } from '@oxyhq/bloom';
|
|
4
4
|
import type { AssetUploadInput, FileMetadata } from '@oxyhq/core';
|
|
5
5
|
import { useFileStore } from '../../../stores/fileStore';
|
|
6
|
-
import { useUploadFile } from '../../../hooks/mutations/useAccountMutations';
|
|
6
|
+
import type { useUploadFile } from '../../../hooks/mutations/useAccountMutations';
|
|
7
7
|
import { convertDocumentPickerAssetToFile, formatFileSize } from '../../../utils/fileManagement';
|
|
8
8
|
import {
|
|
9
9
|
type PendingUploadFile,
|