diff --git a/App.tsx b/App.tsx index dfe2d708..8ccbd27c 100644 --- a/App.tsx +++ b/App.tsx @@ -5,7 +5,8 @@ import { Provider } from 'react-redux' import ThemeManager from 'src/utils/styles/ThemeManager' import { Index } from 'src/Index' -import { store } from 'src/store' +import { persistor, store } from 'src/store' +import { PersistGate } from 'redux-persist/integration/react' const queryCache = new QueryCache() @@ -30,7 +31,16 @@ const App: React.FC = () => { - + + {bootstrapped => { + if (bootstrapped) { + require('src/i18n/i18n') + return + } else { + return <> + } + }} + diff --git a/app.config.ts b/app.config.ts index 467aa7e4..b50bdd32 100644 --- a/app.config.ts +++ b/app.config.ts @@ -13,11 +13,22 @@ export default (): ExpoConfig => ({ developmentClient: { silentLaunch: true }, scheme: 'mastodonct', ios: { + infoPlist: { + CFBundleAllowMixedLocalizations: true + }, splash: { image: './assets/splash.png', resizeMode: 'contain', backgroundColor: '#ffffff' } }, - assetBundlePatterns: ['**/*'] + locales: { + zh: { + CFBundleDisplayName: '我的嘟嘟' + }, + en: { + CFBundleDisplayName: 'My Toots' + } + }, + assetBundlePatterns: ['assets/*'] }) diff --git a/package.json b/package.json index eaadcd04..e05e00a3 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,16 @@ "expo-auth-session": "~2.0.0", "expo-av": "~8.6.0", "expo-image-picker": "~9.1.1", + "expo-localization": "^9.0.0", "expo-secure-store": "~9.2.0", "expo-splash-screen": "~0.6.1", "expo-status-bar": "~1.0.2", + "i18next": "^19.8.4", "ky": "^0.24.0", "lodash": "^4.17.20", "react": "16.13.1", "react-dom": "16.13.1", + "react-i18next": "^11.7.3", "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz", "react-native-appearance": "~0.3.3", "react-native-collapsible": "^1.5.3", @@ -60,4 +63,4 @@ "typescript": "~3.9.2" }, "private": true -} \ No newline at end of file +} diff --git a/src/components/Menu/Container.tsx b/src/components/Menu/Container.tsx index 4a87f0d9..0caa5d78 100644 --- a/src/components/Menu/Container.tsx +++ b/src/components/Menu/Container.tsx @@ -4,11 +4,24 @@ import { useTheme } from 'src/utils/styles/ThemeManager' import constants from 'src/utils/styles/constants' -const MenuContainer: React.FC = ({ ...props }) => { +export interface Props { + children: React.ReactNode + marginTop?: boolean +} + +const MenuContainer: React.FC = ({ ...props }) => { const { theme } = useTheme() return ( - + {props.children} ) @@ -17,7 +30,7 @@ const MenuContainer: React.FC = ({ ...props }) => { const styles = StyleSheet.create({ base: { borderTopWidth: 1, - marginBottom: constants.SPACING_M + marginBottom: constants.GLOBAL_PAGE_PADDING } }) diff --git a/src/components/Menu/Item.tsx b/src/components/Menu/Item.tsx index ba70c828..39f6b412 100644 --- a/src/components/Menu/Item.tsx +++ b/src/components/Menu/Item.tsx @@ -1,61 +1,82 @@ import React from 'react' import { Pressable, StyleSheet, Text, View } from 'react-native' -import { useNavigation } from '@react-navigation/native' import { Feather } from '@expo/vector-icons' import { useTheme } from 'src/utils/styles/ThemeManager' import constants from 'src/utils/styles/constants' +import { ColorDefinitions } from 'src/utils/styles/themes' export interface Props { - icon?: string + iconFront?: string + iconFrontColor?: ColorDefinitions title: string - navigateTo?: string - navigateToParams?: {} + content?: string + iconBack?: 'chevron-right' | 'check' + iconBackColor?: ColorDefinitions + onPress?: () => void } -const Core: React.FC = ({ icon, title, navigateTo }) => { +const Core: React.FC = ({ + iconFront, + iconFrontColor, + title, + content, + iconBack, + iconBackColor +}) => { const { theme } = useTheme() + iconFrontColor = iconFrontColor || 'primary' + iconBackColor = iconBackColor || 'secondary' return ( - {icon && ( - - )} - - {title} - - {navigateTo && ( - - )} + + {iconFront && ( + + )} + + {title} + + + + {content && ( + + {content} + + )} + {iconBack && ( + + )} + ) } const MenuItem: React.FC = ({ ...props }) => { const { theme } = useTheme() - const navigation = useNavigation() - return props.navigateTo ? ( + return props.onPress ? ( { - navigation.navigate(props.navigateTo!, props.navigateToParams) - }} + onPress={props.onPress} > ) : ( - + ) @@ -69,15 +90,31 @@ const styles = StyleSheet.create({ core: { flex: 1, flexDirection: 'row', - alignItems: 'center', paddingLeft: constants.GLOBAL_PAGE_PADDING, paddingRight: constants.GLOBAL_PAGE_PADDING }, - iconLeading: { + front: { + flex: 1, + flexDirection: 'row', + alignItems: 'center' + }, + back: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'center' + }, + iconFront: { marginRight: 8 }, - iconNavigation: { - marginLeft: 'auto' + text: { + fontSize: constants.FONT_SIZE_M + }, + content: { + fontSize: constants.FONT_SIZE_M + }, + iconBack: { + marginLeft: 8 } }) diff --git a/src/components/Timelines.tsx b/src/components/Timelines.tsx index d7b0edcb..c6a84963 100644 --- a/src/components/Timelines.tsx +++ b/src/components/Timelines.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef, useState } from 'react' -import { Dimensions, FlatList, StyleSheet, Text, View } from 'react-native' +import { Dimensions, FlatList, StyleSheet, View } from 'react-native' import SegmentedControl from '@react-native-community/segmented-control' import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { useSelector } from 'react-redux' @@ -7,8 +7,11 @@ import { Feather } from '@expo/vector-icons' import Timeline from './Timelines/Timeline' import sharedScreens from 'src/screens/Shared/sharedScreens' -import { getRemoteUrl, InstancesState } from 'src/utils/slices/instancesSlice' -import { RootState, store } from 'src/store' +import { + getLocalUrl, + getRemoteUrl, + InstancesState +} from 'src/utils/slices/instancesSlice' import { useTheme } from 'src/utils/styles/ThemeManager' import { useNavigation } from '@react-navigation/native' import getCurrentTab from 'src/utils/getCurrentTab' @@ -42,15 +45,10 @@ export interface Props { const Timelines: React.FC = ({ name, content }) => { const navigation = useNavigation() const { theme } = useTheme() - const localRegistered = useSelector( - (state: RootState) => state.instances.local.url - ) - const publicDomain = getRemoteUrl(store.getState()) + const localRegistered = useSelector(getLocalUrl) + const publicDomain = useSelector(getRemoteUrl) const [segment, setSegment] = useState(0) const [renderHeader, setRenderHeader] = useState(false) - const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState( - false - ) useEffect(() => { const nbr = setTimeout(() => setRenderHeader(true), 50) @@ -60,8 +58,6 @@ const Timelines: React.FC = ({ name, content }) => { const horizontalPaging = useRef(null!) const onChangeSegment = useCallback(({ nativeEvent }) => { - setSegmentManuallyTriggered(true) - setSegment(nativeEvent.selectedSegmentIndex) horizontalPaging.current.scrollToIndex({ index: nativeEvent.selectedSegmentIndex }) @@ -90,13 +86,8 @@ const Timelines: React.FC = ({ name, content }) => { }, [localRegistered] ) - const flOnMomentumScrollEnd = useCallback( - () => setSegmentManuallyTriggered(false), - [] - ) const flOnScroll = useCallback( ({ nativeEvent }) => - !segmentManuallyTriggered && setSegment( nativeEvent.contentOffset.x <= Dimensions.get('window').width / 2 ? 0 @@ -114,12 +105,13 @@ const Timelines: React.FC = ({ name, content }) => { ...(renderHeader && localRegistered && { headerCenter: () => ( - + + + ), headerRight: () => ( = ({ name, content }) => { keyExtractor={flKeyExtrator} getItemLayout={flGetItemLayout} showsHorizontalScrollIndicator={false} - onMomentumScrollEnd={flOnMomentumScrollEnd} /> ) }} @@ -159,6 +150,9 @@ const Timelines: React.FC = ({ name, content }) => { } const styles = StyleSheet.create({ + segmentsContainer: { + flexBasis: '60%' + }, flatList: { width: Dimensions.get('window').width, height: '100%' diff --git a/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx b/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx index b3a7f1b3..bc7a5e22 100644 --- a/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx +++ b/src/components/Timelines/Timeline/Shared/ActionsStatus.tsx @@ -8,17 +8,15 @@ import { Text, View } from 'react-native' -import Toast from 'react-native-toast-message' import { useMutation, useQueryCache } from 'react-query' import { Feather } from '@expo/vector-icons' -import { findIndex } from 'lodash' import client from 'src/api/client' import { getLocalAccountId } from 'src/utils/slices/instancesSlice' -import { store } from 'src/store' import { useTheme } from 'src/utils/styles/ThemeManager' import constants from 'src/utils/styles/constants' import { toast } from 'src/components/toast' +import { useSelector } from 'react-redux' const fireMutation = async ({ id, @@ -87,7 +85,7 @@ const ActionsStatus: React.FC = ({ queryKey, status }) => { const iconColorAction = (state: boolean) => state ? theme.primary : theme.secondary - const localAccountId = getLocalAccountId(store.getState()) + const localAccountId = useSelector(getLocalAccountId) const [bottomSheetVisible, setBottomSheetVisible] = useState(false) const queryCache = useQueryCache() diff --git a/src/components/Timelines/Timeline/Shared/Avatar.tsx b/src/components/Timelines/Timeline/Shared/Avatar.tsx index 6f820cee..2aadf714 100644 --- a/src/components/Timelines/Timeline/Shared/Avatar.tsx +++ b/src/components/Timelines/Timeline/Shared/Avatar.tsx @@ -34,7 +34,7 @@ const styles = StyleSheet.create({ image: { width: '100%', height: '100%', - borderRadius: 8 + borderRadius: 6 } }) diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx index 06c15be5..4f414491 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx @@ -2,19 +2,18 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { Pressable, StyleSheet, Text, View } from 'react-native' import { useNavigation } from '@react-navigation/native' import { Feather } from '@expo/vector-icons' -import Toast from 'react-native-toast-message' import { useMutation, useQueryCache } from 'react-query' import Emojis from './Emojis' import relativeTime from 'src/utils/relativeTime' import client from 'src/api/client' import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice' -import { store } from 'src/store' import { useTheme } from 'src/utils/styles/ThemeManager' import constants from 'src/utils/styles/constants' import BottomSheet from 'src/components/BottomSheet' import BottomSheetRow from 'src/components/BottomSheet/Row' import { toast } from 'src/components/toast' +import { useSelector } from 'react-redux' const fireMutation = async ({ id, @@ -115,8 +114,8 @@ const HeaderDefault: React.FC = ({ const { theme } = useTheme() const navigation = useNavigation() - const localAccountId = getLocalAccountId(store.getState()) - const localDomain = getLocalUrl(store.getState()) + const localAccountId = useSelector(getLocalAccountId) + const localDomain = useSelector(getLocalUrl) const [since, setSince] = useState(relativeTime(created_at)) const [modalVisible, setModalVisible] = useState(false) diff --git a/src/i18n/en/_all.ts b/src/i18n/en/_all.ts new file mode 100644 index 00000000..5f745620 --- /dev/null +++ b/src/i18n/en/_all.ts @@ -0,0 +1,3 @@ +export default { + common: require('./common').default +} diff --git a/src/i18n/en/common.ts b/src/i18n/en/common.ts new file mode 100644 index 00000000..43602fbe --- /dev/null +++ b/src/i18n/en/common.ts @@ -0,0 +1,31 @@ +export default { + headers: { + local: { + segments: { + left: 'Following', + right: 'Local' + } + }, + public: { + segments: { + left: 'Federated', + right: 'Others' + } + }, + notifications: 'Notifications', + me: { + root: 'My Mastodon', + conversations: 'Messages', + bookmarks: 'Booksmarks', + favourites: 'Favourites', + lists: { + root: 'Lists', + list: 'List {{list}}' + }, + settings: { + root: 'Settings', + language: 'Language' + } + } + } +} diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts new file mode 100644 index 00000000..f138cd83 --- /dev/null +++ b/src/i18n/i18n.ts @@ -0,0 +1,50 @@ +import i18next from 'i18next' +import { initReactI18next } from 'react-i18next' +import * as Localization from 'expo-localization' + +import zh from 'src/i18n/zh/_all' +import en from 'src/i18n/en/_all' +import { + changeLanguage, + getSettingsLanguage +} from 'src/utils/slices/settingsSlice' +import { store } from 'src/store' + console.log(store.getState()) +if (!getSettingsLanguage(store.getState())) { + console.log('No default locale of app') + const deviceLocal = Localization.locale + if (deviceLocal.startsWith('zh')) { + store.dispatch(changeLanguage('zh')) + } else { + store.dispatch(changeLanguage('en')) + } +} + +i18next.use(initReactI18next).init({ + lng: getSettingsLanguage(store.getState()), + fallbackLng: 'en', + supportedLngs: ['zh', 'en'], + nonExplicitSupportedLngs: true, + + ns: ['common'], + defaultNS: 'common', + + resources: { + zh: zh, + en: en + }, + + saveMissing: true, + missingKeyHandler: (lng, ns, key, fallbackValue) => { + console.warn('i18n missing: ' + ns + ' : ' + key) + }, + + // react options + interpolation: { + escapeValue: false + }, + + debug: true +}) + +export default i18next diff --git a/src/i18n/zh/_all.ts b/src/i18n/zh/_all.ts new file mode 100644 index 00000000..e67184ea --- /dev/null +++ b/src/i18n/zh/_all.ts @@ -0,0 +1,4 @@ +export default { + common: require('./common').default, + settings: require('./settings').default +} diff --git a/src/i18n/zh/common.ts b/src/i18n/zh/common.ts new file mode 100644 index 00000000..e703cd6e --- /dev/null +++ b/src/i18n/zh/common.ts @@ -0,0 +1,31 @@ +export default { + headers: { + local: { + segments: { + left: '我的关注', + right: '本站嘟嘟' + } + }, + public: { + segments: { + left: '跨站关注', + right: '外站嘟嘟' + } + }, + notifications: '我的通知', + me: { + root: '我的长毛象', + conversations: '私信', + bookmarks: '书签', + favourites: '喜欢', + lists: { + root: '列表', + list: '列表 {{list}}' + }, + settings: { + root: '设置', + language: '语言' + } + } + } +} diff --git a/src/i18n/zh/settings.ts b/src/i18n/zh/settings.ts new file mode 100644 index 00000000..12ff856e --- /dev/null +++ b/src/i18n/zh/settings.ts @@ -0,0 +1,11 @@ +export default { + content: { + language: { + title: '切换语言', + options: { + zh: '简体中文', + en: 'English' + } + } + } +} diff --git a/src/screens/Local.tsx b/src/screens/Local.tsx index daac16de..53822067 100644 --- a/src/screens/Local.tsx +++ b/src/screens/Local.tsx @@ -1,14 +1,17 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import Timelines from 'src/components/Timelines' const ScreenLocal: React.FC = () => { + const { t } = useTranslation() + return ( ) diff --git a/src/screens/Me.tsx b/src/screens/Me.tsx index b028088c..12644965 100644 --- a/src/screens/Me.tsx +++ b/src/screens/Me.tsx @@ -1,5 +1,6 @@ import React from 'react' import { createNativeStackNavigator } from 'react-native-screens/native-stack' +import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import ScreenMeRoot from 'src/screens/Me/Root' @@ -16,6 +17,7 @@ import { RootState } from 'src/store' const Stack = createNativeStackNavigator() const ScreenMe: React.FC = () => { + const { t } = useTranslation() const localRegistered = useSelector( (state: RootState) => state.instances.local.url ) @@ -32,49 +34,49 @@ const ScreenMe: React.FC = () => { headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' }, headerCenter: () => <> } - : { headerTitle: '我的长毛象' } + : { headerTitle: t('headers.me.root') } } /> ({ - headerTitle: `列表:${route.params.title}` + headerTitle: t('headers.me.lists.list', { list: route.params.title }) })} /> diff --git a/src/screens/Me/Lists.tsx b/src/screens/Me/Lists.tsx index 63e5d5d5..58bc3f32 100644 --- a/src/screens/Me/Lists.tsx +++ b/src/screens/Me/Lists.tsx @@ -1,3 +1,4 @@ +import { useNavigation } from '@react-navigation/native' import React from 'react' import { ActivityIndicator, Text } from 'react-native' import { useQuery } from 'react-query' @@ -6,6 +7,7 @@ import { MenuContainer, MenuItem } from 'src/components/Menu' import { listsFetch } from 'src/utils/fetches/listsFetch' const ScreenMeLists: React.FC = () => { + const navigation = useNavigation() const { status, data } = useQuery(['Lists'], listsFetch) let lists @@ -20,13 +22,14 @@ const ScreenMeLists: React.FC = () => { lists = data?.map((d: Mastodon.List, i: number) => ( + navigation.navigate('Screen-Me-Lists-List', { + list: d.id, + title: d.title + }) + } /> )) break diff --git a/src/screens/Me/Root.tsx b/src/screens/Me/Root.tsx index 9204fdf9..acee40e6 100644 --- a/src/screens/Me/Root.tsx +++ b/src/screens/Me/Root.tsx @@ -2,8 +2,7 @@ import React from 'react' import { ScrollView } from 'react-native' import { useSelector } from 'react-redux' -import { RootState, store } from 'src/store' -import { getLocalAccountId } from 'src/utils/slices/instancesSlice' +import { getLocalUrl } from 'src/utils/slices/instancesSlice' import Login from './Root/Login' import MyInfo from './Root/MyInfo' @@ -12,20 +11,12 @@ import Settings from './Root/Settings' import Logout from './Root/Logout' const ScreenMeRoot: React.FC = () => { - const localRegistered = useSelector( - (state: RootState) => state.instances.local.url - ) + const localRegistered = useSelector(getLocalUrl) return ( - {localRegistered ? ( - - ) : ( - - )} - {localRegistered && ( - - )} + {localRegistered ? : } + {localRegistered && } {localRegistered && } diff --git a/src/screens/Me/Root/MyCollections.tsx b/src/screens/Me/Root/MyCollections.tsx index 710b717e..59c4cdf5 100644 --- a/src/screens/Me/Root/MyCollections.tsx +++ b/src/screens/Me/Root/MyCollections.tsx @@ -1,17 +1,35 @@ +import { useNavigation } from '@react-navigation/native' import React from 'react' +import { useTranslation } from 'react-i18next' + import { MenuContainer, MenuItem } from 'src/components/Menu' -export interface Props { - id: Mastodon.Account['id'] -} +const MyInfo: React.FC = () => { + const { t } = useTranslation() + const navigation = useNavigation() -const MyInfo: React.FC = ({ id }) => { return ( - - - - + navigation.navigate('Screen-Me-Conversations')} + /> + navigation.navigate('Screen-Me-Bookmarks')} + /> + navigation.navigate('Screen-Me-Favourites')} + /> + navigation.navigate('Screen-Me-Lists')} + /> ) } diff --git a/src/screens/Me/Root/MyInfo.tsx b/src/screens/Me/Root/MyInfo.tsx index ed12371f..b779b26e 100644 --- a/src/screens/Me/Root/MyInfo.tsx +++ b/src/screens/Me/Root/MyInfo.tsx @@ -4,13 +4,12 @@ import { useQuery } from 'react-query' import { accountFetch } from 'src/utils/fetches/accountFetch' import AccountHeader from 'src/screens/Shared/Account/Header' import AccountInformation from 'src/screens/Shared/Account/Information' +import { useSelector } from 'react-redux' +import { getLocalAccountId } from 'src/utils/slices/instancesSlice' -export interface Props { - id: Mastodon.Account['id'] -} - -const MyInfo: React.FC = ({ id }) => { - const { data } = useQuery(['Account', { id }], accountFetch) +const MyInfo: React.FC = () => { + const localAccountId = useSelector(getLocalAccountId) + const { data } = useQuery(['Account', { id: localAccountId }], accountFetch) return ( <> diff --git a/src/screens/Me/Root/Settings.tsx b/src/screens/Me/Root/Settings.tsx index c20521ec..37cc1f79 100644 --- a/src/screens/Me/Root/Settings.tsx +++ b/src/screens/Me/Root/Settings.tsx @@ -1,10 +1,20 @@ +import { useNavigation } from '@react-navigation/native' import React from 'react' +import { useTranslation } from 'react-i18next' + import { MenuContainer, MenuItem } from 'src/components/Menu' const Settings: React.FC = () => { + const { t } = useTranslation() + const navigation = useNavigation() + return ( - + navigation.navigate('Screen-Me-Settings')} + /> ) } diff --git a/src/screens/Me/Settings.tsx b/src/screens/Me/Settings.tsx index 83128404..95c8fed4 100644 --- a/src/screens/Me/Settings.tsx +++ b/src/screens/Me/Settings.tsx @@ -1,9 +1,53 @@ import React from 'react' +import { useTranslation } from 'react-i18next' +import { ActionSheetIOS } from 'react-native' +import { useDispatch, useSelector } from 'react-redux' -import { MenuContainer } from 'src/components/Menu' +import { MenuContainer, MenuItem } from 'src/components/Menu' +import { + changeLanguage, + getSettingsLanguage +} from 'src/utils/slices/settingsSlice' const ScreenMeSettings: React.FC = () => { - return + const { t, i18n } = useTranslation('settings') + const language = useSelector(getSettingsLanguage) + const dispatch = useDispatch() + console.log(i18n.language) + + return ( + + + ActionSheetIOS.showActionSheetWithOptions( + { + options: [ + t('settings:content.language.options.zh'), + t('settings:content.language.options.en'), + '取消' + ], + cancelButtonIndex: 2 + }, + buttonIndex => { + switch (buttonIndex) { + case 0: + dispatch(changeLanguage('zh')) + i18n.changeLanguage('zh') + break + case 1: + dispatch(changeLanguage('en')) + i18n.changeLanguage('en') + break + } + } + ) + } + /> + + ) } export default ScreenMeSettings diff --git a/src/screens/Notifications.tsx b/src/screens/Notifications.tsx index 461a2e52..6e390b9b 100644 --- a/src/screens/Notifications.tsx +++ b/src/screens/Notifications.tsx @@ -6,16 +6,20 @@ import sharedScreens from 'src/screens/Shared/sharedScreens' import { useSelector } from 'react-redux' import { RootState } from 'src/store' import PleaseLogin from 'src/components/PleaseLogin' +import { useTranslation } from 'react-i18next' const Stack = createNativeStackNavigator() const ScreenNotifications: React.FC = () => { + const { t } = useTranslation() const localRegistered = useSelector( (state: RootState) => state.instances.local.url ) return ( - + {() => localRegistered ? : diff --git a/src/screens/Public.tsx b/src/screens/Public.tsx index 1e14fa84..b3aa72e2 100644 --- a/src/screens/Public.tsx +++ b/src/screens/Public.tsx @@ -1,14 +1,17 @@ import React from 'react' +import { useTranslation } from 'react-i18next' import Timelines from 'src/components/Timelines' const ScreenPublic: React.FC = () => { + const { t } = useTranslation() + return ( ) diff --git a/src/store.ts b/src/store.ts index 9839c5ad..f77c3aa9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3,6 +3,7 @@ import { persistReducer, persistStore } from 'redux-persist' import createSecureStore from 'redux-persist-expo-securestore' import instancesSlice from 'src/utils/slices/instancesSlice' +import settingsSlice from 'src/utils/slices/settingsSlice' const secureStorage = createSecureStore() @@ -11,9 +12,15 @@ const instancesPersistConfig = { storage: secureStorage } +const settingsPersistConfig = { + key: 'settings', + storage: secureStorage +} + const store = configureStore({ reducer: { - instances: persistReducer(instancesPersistConfig, instancesSlice) + instances: persistReducer(instancesPersistConfig, instancesSlice), + settings: persistReducer(settingsPersistConfig, settingsSlice) }, middleware: getDefaultMiddleware({ serializableCheck: { diff --git a/src/utils/slices/settingsSlice.ts b/src/utils/slices/settingsSlice.ts new file mode 100644 index 00000000..047fddc1 --- /dev/null +++ b/src/utils/slices/settingsSlice.ts @@ -0,0 +1,77 @@ +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit' + +import { RootState } from 'src/store' +// import client from 'src/api/client' + +export type SettingsState = { + language: 'zh' | 'en' | undefined +} + +const initialState = { + language: undefined +} + +// export const updateLocal = createAsyncThunk( +// 'instances/updateLocal', +// async ({ +// url, +// token +// }: { +// url?: InstancesState['local']['url'] +// token?: InstancesState['local']['token'] +// }) => { +// if (!url || !token) { +// return initialStateLocal +// } + +// const { +// body: { id } +// } = await client({ +// method: 'get', +// instance: 'remote', +// instanceUrl: url, +// endpoint: `accounts/verify_credentials`, +// headers: { Authorization: `Bearer ${token}` } +// }) + +// const { body: preferences } = await client({ +// method: 'get', +// instance: 'remote', +// instanceUrl: url, +// endpoint: `preferences`, +// headers: { Authorization: `Bearer ${token}` } +// }) + +// return { +// url, +// token, +// account: { +// id, +// preferences +// } +// } +// } +// ) + +const settingsSlice = createSlice({ + name: 'settings', + initialState: initialState as SettingsState, + reducers: { + changeLanguage: ( + state, + action: PayloadAction> + ) => { + state.language = action.payload + } + } + // extraReducers: builder => { + // builder.addCase(updateLocal.fulfilled, (state, action) => { + // state.local = action.payload + // }) + // } +}) + +export const getSettingsLanguage = (state: RootState) => state.settings.language + +export const { changeLanguage } = settingsSlice.actions +export default settingsSlice.reducer diff --git a/src/utils/styles/constants.ts b/src/utils/styles/constants.ts index 68daaec3..ae15cd0f 100644 --- a/src/utils/styles/constants.ts +++ b/src/utils/styles/constants.ts @@ -11,7 +11,7 @@ export default { SPACING_L: 24, SPACING_XL: 40, - GLOBAL_PAGE_PADDING: 16, // SPACING_M + GLOBAL_PAGE_PADDING: 24, // SPACING_M GLOBAL_SPACING_BASE: 8, // SPACING_S AVATAR_S: 52, diff --git a/yarn.lock b/yarn.lock index 3ae41fb8..81e338f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -958,7 +958,7 @@ pirates "^4.0.0" source-map-support "^0.5.16" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4": version "7.12.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== @@ -1481,6 +1481,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/webpack-env@^1.16.0": + version "1.16.0" + resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.0.tgz#8c0a9435dfa7b3b1be76562f3070efb3f92637b4" + integrity sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw== + "@types/yargs-parser@*": version "15.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" @@ -2763,6 +2768,13 @@ expo-linking@~1.0.4: qs "^6.5.0" url-parse "^1.4.4" +expo-localization@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/expo-localization/-/expo-localization-9.0.0.tgz#1a61582f9ffb3cd28879a917dec347c444bff54e" + integrity sha512-jG84LA9yyQpLcSOKvR/WoIm0IHZ4tZQma//AJ9/DgBZC5zSB/aDbcjmCStQvqCePz8EMy1CKxRd+UlKYPLEiVw== + dependencies: + rtl-detect "^1.0.2" + expo-location@~9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/expo-location/-/expo-location-9.0.1.tgz#adf93b8adf5e9dcf9570cba1d66c8e3831329156" @@ -3261,6 +3273,13 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +html-parse-stringify2@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify2/-/html-parse-stringify2-2.0.1.tgz#dc5670b7292ca158b7bc916c9a6735ac8872834a" + integrity sha1-3FZwtyksoVi3vJFsmmc1rIhyg0o= + dependencies: + void-elements "^2.0.1" + htmlparser2-without-node-native@^3.9.2: version "3.9.2" resolved "https://registry.yarnpkg.com/htmlparser2-without-node-native/-/htmlparser2-without-node-native-3.9.2.tgz#b3ed050d877d0ff3465969e339877b7f9f6631f6" @@ -3285,6 +3304,13 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +i18next@^19.8.4: + version "19.8.4" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-19.8.4.tgz#447718f2a26319b8debdbcc6fbc1a9761be7316b" + integrity sha512-FfVPNWv+felJObeZ6DSXZkj9QM1Ivvh7NcFCgA8XPtJWHz0iXVa9BUy+QY8EPrCLE+vWgDfV/sc96BgXVo6HAA== + dependencies: + "@babel/runtime" "^7.12.0" + iconv-lite@^0.4.17: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -4937,6 +4963,14 @@ react-dom@16.13.1: prop-types "^15.6.2" scheduler "^0.19.1" +react-i18next@^11.7.3: + version "11.7.3" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.7.3.tgz#256461c46baf5b3208c3c6860ca4e569fc7ed053" + integrity sha512-7sYZqVZgdaS9Z0ZH6nuJFErCD0zz5wK3jR4/xCrWjZcxHHF3GRu7BXdicbSPprZV4ZYz7LJzxxMHO7dg5Qb70A== + dependencies: + "@babel/runtime" "^7.3.1" + html-parse-stringify2 "2.0.1" + react-is@^16.12.0, react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -5305,6 +5339,11 @@ rsvp@^4.8.4: resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== +rtl-detect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.2.tgz#8eca316f5c6563d54df4e406171dd7819adda67f" + integrity sha512-5X1422hvphzg2a/bo4tIDbjFjbJUOaPZwqE6dnyyxqwFqfR+tBcvfqapJr0o0VygATVCGKiODEewhZtKF+90AA== + run-async@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -6081,6 +6120,11 @@ vlq@^1.0.0: resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== +void-elements@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"