@oxyhq/services
Version:
215 lines (207 loc) • 6.74 kB
JavaScript
"use strict";
import React, { useState, useMemo, useCallback } from 'react';
import { View, Text, StyleSheet, ScrollView, Platform } from 'react-native';
import { useThemeStyles } from "../hooks/useThemeStyles.js";
import { useColorScheme } from "../hooks/useColorScheme.js";
import { normalizeTheme } from "../utils/themeUtils.js";
import { Ionicons } from '@expo/vector-icons';
import { toast } from '../../lib/sonner';
import { Header, GroupedSection } from "../components/index.js";
import { useI18n } from "../hooks/useI18n.js";
import { SUPPORTED_LANGUAGES } from '@oxyhq/core';
import { useOxy } from "../context/OxyContext.js";
import { fontFamilies } from "../styles/fonts.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
/**
* LanguageSelectorScreen - Optimized for performance
*
* Performance optimizations:
* - useMemo for language items to prevent recreation on every render
* - useCallback for handlers to prevent unnecessary re-renders
* - Memoized current language section
*/
const LanguageSelectorScreen = ({
goBack,
onClose,
theme
}) => {
// Use useOxy() hook for OxyContext values
const {
user,
currentLanguage,
setLanguage,
oxyServices,
isAuthenticated
} = useOxy();
const {
t
} = useI18n();
const colorScheme = useColorScheme();
const normalizedTheme = normalizeTheme(theme);
const themeStyles = useThemeStyles(normalizedTheme, colorScheme);
const themeColors = themeStyles.colors;
const [isLoading, setIsLoading] = useState(false);
// Memoize the language select handler to prevent recreation on every render
const handleLanguageSelect = useCallback(async languageId => {
if (languageId === currentLanguage || isLoading) {
return; // Already selected or loading
}
setIsLoading(true);
try {
let serverSyncFailed = false;
// If signed in, persist preference to backend user settings
if (isAuthenticated && user?.id) {
try {
await oxyServices.updateProfile({
language: languageId
});
} catch (e) {
// Server sync failed, but we'll save locally anyway
serverSyncFailed = true;
if (__DEV__) {
console.warn('Failed to sync language to server (will save locally only):', e?.message || e);
}
}
}
// Always persist locally for immediate UX and for guests
await setLanguage(languageId);
const selectedLang = SUPPORTED_LANGUAGES.find(lang => lang.id === languageId);
// Show success message (language is saved locally regardless of server sync)
toast.success(t('language.changed', {
lang: selectedLang?.name || languageId
}));
// Log server sync failure only in dev mode (user experience is still good - saved locally)
if (serverSyncFailed && __DEV__) {
console.warn('Language saved locally but server sync failed');
}
setIsLoading(false);
// Close the bottom sheet if possible; otherwise, go back
if (onClose) onClose();else goBack?.();
} catch (error) {
// Only show error if local storage also failed
if (__DEV__) {
console.error('Error saving language preference:', error);
}
toast.error('Failed to save language preference');
setIsLoading(false);
}
}, [currentLanguage, isLoading, isAuthenticated, user?.id, oxyServices, setLanguage, t, onClose, goBack]);
// Memoize language items to prevent recreation on every render
const languageItems = useMemo(() => SUPPORTED_LANGUAGES.map(language => {
const isSelected = currentLanguage === language.id;
return {
id: language.id,
title: language.name,
subtitle: language.nativeName,
customIcon: /*#__PURE__*/_jsx(View, {
style: [styles.languageFlag, {
backgroundColor: `${language.color}15`
}],
children: /*#__PURE__*/_jsx(Text, {
style: styles.flagEmoji,
children: language.flag
})
}),
iconColor: language.color,
selected: isSelected,
onPress: () => handleLanguageSelect(language.id),
customContent: isSelected ? /*#__PURE__*/_jsx(Ionicons, {
name: "checkmark-circle",
size: 24,
color: themeColors.tint
}) : undefined
};
}), [currentLanguage, handleLanguageSelect, themeColors]);
return /*#__PURE__*/_jsxs(View, {
style: [styles.container, {
backgroundColor: theme === 'dark' ? '#000000' : '#F5F5F5'
}],
children: [/*#__PURE__*/_jsx(Header, {
title: "",
subtitle: "",
theme: normalizedTheme,
onBack: onClose || goBack,
variant: "minimal",
elevation: "none"
}), /*#__PURE__*/_jsxs(ScrollView, {
style: styles.content,
contentContainerStyle: styles.contentContainer,
showsVerticalScrollIndicator: false,
removeClippedSubviews: true,
children: [/*#__PURE__*/_jsxs(View, {
style: styles.titleContainer,
children: [/*#__PURE__*/_jsx(Text, {
style: [styles.bigTitle, {
color: themeColors.text
}],
children: t('language.title')
}), t('language.subtitle') && /*#__PURE__*/_jsx(Text, {
style: [styles.bigSubtitle, {
color: themeColors.secondaryText
}],
children: t('language.subtitle')
})]
}), /*#__PURE__*/_jsx(View, {
style: styles.sectionContainer,
children: /*#__PURE__*/_jsx(View, {
style: [styles.materialCard, {
backgroundColor: themeColors.card
}],
children: /*#__PURE__*/_jsx(GroupedSection, {
items: languageItems
})
})
})]
})]
});
};
const styles = StyleSheet.create({
container: {
flex: 1
},
content: {
flex: 1
},
contentContainer: {
padding: 16,
paddingTop: 24
},
titleContainer: {
marginBottom: 32,
paddingTop: 0
},
bigTitle: {
fontSize: 34,
fontWeight: Platform.OS === 'web' ? 'bold' : undefined,
fontFamily: fontFamilies.interBold,
lineHeight: 40,
marginBottom: 8,
letterSpacing: -0.5
},
bigSubtitle: {
fontSize: 16,
lineHeight: 22,
opacity: 0.7,
marginTop: 4
},
sectionContainer: {
marginBottom: 8
},
materialCard: {
borderRadius: 12,
overflow: 'hidden'
},
languageFlag: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: 'center',
justifyContent: 'center'
},
flagEmoji: {
fontSize: 20
}
});
// Export memoized component to prevent unnecessary re-renders
export default /*#__PURE__*/React.memo(LanguageSelectorScreen);
//# sourceMappingURL=LanguageSelectorScreen.js.map