diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 6 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 1 | ||||
-rw-r--r-- | src/locale/helpers.ts | 4 | ||||
-rw-r--r-- | src/routes.ts | 1 | ||||
-rw-r--r-- | src/state/models/ui/preferences.ts | 15 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 5 | ||||
-rw-r--r-- | src/view/com/post/Post.tsx | 5 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 5 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | src/view/screens/LanguageSettings.tsx | 205 | ||||
-rw-r--r-- | src/view/screens/Settings.tsx | 17 |
11 files changed, 252 insertions, 14 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 604fca2b9..a247c72dd 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -46,6 +46,7 @@ import {ModerationScreen} from './view/screens/Moderation' import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' import {NotFoundScreen} from './view/screens/NotFound' import {SettingsScreen} from './view/screens/Settings' +import {LanguageSettingsScreen} from './view/screens/LanguageSettings' import {ProfileScreen} from './view/screens/Profile' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowsScreen} from './view/screens/ProfileFollows' @@ -119,6 +120,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { options={{title: title('Settings')}} /> <Stack.Screen + name="LanguageSettings" + component={LanguageSettingsScreen} + options={{title: title('Language Settings')}} + /> + <Stack.Screen name="Profile" component={ProfileScreen} options={({route}) => ({ diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index e2867a707..35a379d48 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -10,6 +10,7 @@ export type CommonNavigatorParams = { ModerationMutedAccounts: undefined ModerationBlockedAccounts: undefined Settings: undefined + LanguageSettings: undefined Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} ProfileFollows: {name: string} diff --git a/src/locale/helpers.ts b/src/locale/helpers.ts index 3ada8ef0e..65db32f2f 100644 --- a/src/locale/helpers.ts +++ b/src/locale/helpers.ts @@ -79,8 +79,8 @@ export function isPostInLanguage( return bcp47Match.basicFilter(lang, targetLangs).length > 0 } -export function getTranslatorLink(text: string): string { - return `https://translate.google.com/?sl=auto&text=${encodeURIComponent( +export function getTranslatorLink(text: string, lang: string): string { + return `https://translate.google.com/?sl=auto&tl=${lang}&text=${encodeURIComponent( text, )}` } diff --git a/src/routes.ts b/src/routes.ts index 35266d85b..7049d60ff 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -6,6 +6,7 @@ export const router = new Router({ Feeds: '/feeds', Notifications: '/notifications', Settings: '/settings', + LanguageSettings: '/settings/language', Moderation: '/moderation', ModerationMuteLists: '/moderation/mute-lists', ModerationMutedAccounts: '/moderation/muted-accounts', diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 5e07685ca..5ae391670 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -44,6 +44,7 @@ export class LabelPreferencesModel { export class PreferencesModel { adultContentEnabled = false + primaryLanguage: string = deviceLocales[0] || 'en' contentLanguages: string[] = deviceLocales || [] postLanguage: string = deviceLocales[0] || 'en' postLanguageHistory: string[] = DEFAULT_LANG_CODES @@ -78,6 +79,7 @@ export class PreferencesModel { serialize() { return { + primaryLanguage: this.primaryLanguage, contentLanguages: this.contentLanguages, postLanguage: this.postLanguage, postLanguageHistory: this.postLanguageHistory, @@ -105,6 +107,15 @@ export class PreferencesModel { */ hydrate(v: unknown) { if (isObj(v)) { + if ( + hasProp(v, 'primaryLanguage') && + typeof v.primaryLanguage === 'string' + ) { + this.primaryLanguage = v.primaryLanguage + } else { + // default to the device languages + this.primaryLanguage = deviceLocales[0] || 'en' + } // check if content languages in preferences exist, otherwise default to device languages if ( hasProp(v, 'contentLanguages') && @@ -542,6 +553,10 @@ export class PreferencesModel { this.requireAltTextEnabled = !this.requireAltTextEnabled } + setPrimaryLanguage(lang: string) { + this.primaryLanguage = lang + } + getFeedTuners( feedType: 'home' | 'following' | 'author' | 'custom' | 'likes', ) { diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index fc0684698..b98ec805e 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -75,7 +75,10 @@ export const PostThreadItem = observer(function PostThreadItem({ }, [item.post.uri, item.post.author]) const repostsTitle = 'Reposts of this post' - const translatorUrl = getTranslatorLink(record?.text || '') + const translatorUrl = getTranslatorLink( + record?.text || '', + store.preferences.primaryLanguage, + ) const needsTranslation = useMemo( () => store.preferences.contentLanguages.length > 0 && diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index d7559e3c4..d5191cf4e 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -115,7 +115,10 @@ const PostLoaded = observer(function PostLoadedImpl({ replyAuthorDid = urip.hostname } - const translatorUrl = getTranslatorLink(record?.text || '') + const translatorUrl = getTranslatorLink( + record?.text || '', + store.preferences.primaryLanguage, + ) const onPressReply = React.useCallback(() => { store.shell.openComposer({ diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 23d8546bc..71be3969a 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -64,7 +64,10 @@ export const FeedItem = observer(function FeedItemImpl({ const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) return urip.hostname }, [record?.reply]) - const translatorUrl = getTranslatorLink(record?.text || '') + const translatorUrl = getTranslatorLink( + record?.text || '', + store.preferences.primaryLanguage, + ) const onPressReply = React.useCallback(() => { track('FeedItem:PostReply') diff --git a/src/view/index.ts b/src/view/index.ts index 07848aa8f..1e6f27419 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -97,6 +97,7 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark' import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' +import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' export function setup() { library.add( @@ -197,5 +198,6 @@ export function setup() { faTrashCan, faX, faXmark, + faChevronDown, ) } diff --git a/src/view/screens/LanguageSettings.tsx b/src/view/screens/LanguageSettings.tsx new file mode 100644 index 000000000..8b952a564 --- /dev/null +++ b/src/view/screens/LanguageSettings.tsx @@ -0,0 +1,205 @@ +import React from 'react' +import {StyleSheet, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {Text} from '../com/util/text/Text' +import {useStores} from 'state/index' +import {s} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' +import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' +import {ViewHeader} from 'view/com/util/ViewHeader' +import {CenteredView} from 'view/com/util/Views' +import {Button} from 'view/com/util/forms/Button' +import { + FontAwesomeIcon, + FontAwesomeIconStyle, +} from '@fortawesome/react-native-fontawesome' +import {useAnalytics} from 'lib/analytics/analytics' +import {useFocusEffect} from '@react-navigation/native' +import {LANGUAGES} from 'lib/../locale/languages' +import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> + +export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( + _: Props, +) { + const pal = usePalette('default') + const store = useStores() + const {isTabletOrDesktop} = useWebMediaQueries() + const {screen, track} = useAnalytics() + + useFocusEffect( + React.useCallback(() => { + screen('Settings') + store.shell.setMinimalShellMode(false) + }, [screen, store]), + ) + + const onPressContentLanguages = React.useCallback(() => { + track('Settings:ContentlanguagesButtonClicked') + store.shell.openModal({name: 'content-languages-settings'}) + }, [track, store]) + + const onChangePrimaryLanguage = React.useCallback( + (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { + store.preferences.setPrimaryLanguage(value) + }, + [store.preferences], + ) + + const myLanguages = React.useMemo(() => { + return ( + store.preferences.contentLanguages + .map(lang => LANGUAGES.find(l => l.code2 === lang)) + .filter(Boolean) + // @ts-ignore + .map(l => l.name) + .join(', ') + ) + }, [store.preferences.contentLanguages]) + + return ( + <CenteredView + style={[ + pal.view, + pal.border, + styles.container, + isTabletOrDesktop && styles.desktopContainer, + ]}> + <ViewHeader title="Language Settings" showOnDesktop /> + + <View style={{paddingTop: 20, paddingHorizontal: 20}}> + <View style={{paddingBottom: 20}}> + <Text type="title-sm" style={[pal.text, s.pb5]}> + Primary Language + </Text> + <Text style={[pal.text, s.pb10]}> + Select your preferred language for translations in your feed. + </Text> + + <View style={{position: 'relative'}}> + <RNPickerSelect + value={store.preferences.primaryLanguage} + onValueChange={onChangePrimaryLanguage} + items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ + label: l.name, + value: l.code2, + key: l.code2 + l.code3, + }))} + style={{ + inputAndroid: { + backgroundColor: pal.viewLight.backgroundColor, + color: pal.text.color, + fontSize: 14, + letterSpacing: 0.5, + fontWeight: '500', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 24, + }, + inputIOS: { + backgroundColor: pal.viewLight.backgroundColor, + color: pal.text.color, + fontSize: 14, + letterSpacing: 0.5, + fontWeight: '500', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 24, + }, + inputWeb: { + // @ts-ignore web only + cursor: 'pointer', + '-moz-appearance': 'none', + '-webkit-appearance': 'none', + appearance: 'none', + outline: 0, + borderWidth: 0, + backgroundColor: pal.viewLight.backgroundColor, + color: pal.text.color, + fontSize: 14, + letterSpacing: 0.5, + fontWeight: '500', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 24, + }, + }} + /> + + <View + style={{ + position: 'absolute', + top: 1, + right: 1, + bottom: 1, + width: 40, + backgroundColor: pal.viewLight.backgroundColor, + borderRadius: 24, + pointerEvents: 'none', + alignItems: 'center', + justifyContent: 'center', + }}> + <FontAwesomeIcon + icon="chevron-down" + style={pal.text as FontAwesomeIconStyle} + /> + </View> + </View> + </View> + + <View + style={{ + height: 1, + backgroundColor: pal.border.borderColor, + marginBottom: 20, + }} + /> + + <View style={{paddingBottom: 20}}> + <Text type="title-sm" style={[pal.text, s.pb5]}> + Content Languages + </Text> + <Text style={[pal.text, s.pb10]}> + Select which languages you want your subscribed feeds to include. If + none are selected, all languages will be shown. + </Text> + + <Button + type="default" + onPress={onPressContentLanguages} + style={styles.button}> + <FontAwesomeIcon + icon={myLanguages.length ? 'check' : 'plus'} + style={pal.text as FontAwesomeIconStyle} + /> + <Text + type="button" + style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]} + numberOfLines={1}> + {myLanguages.length ? myLanguages : 'Select languages'} + </Text> + </Button> + </View> + </View> + </CenteredView> + ) +}) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: 90, + }, + desktopContainer: { + borderLeftWidth: 1, + borderRightWidth: 1, + paddingBottom: 40, + }, + button: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, +}) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 1ff5f58ff..4783f3353 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -145,10 +145,9 @@ export const SettingsScreen = withAuthRequired( store.shell.openModal({name: 'invite-codes'}) }, [track, store]) - const onPressContentLanguages = React.useCallback(() => { - track('Settings:ContentlanguagesButtonClicked') - store.shell.openModal({name: 'content-languages-settings'}) - }, [track, store]) + const onPressLanguageSettings = React.useCallback(() => { + navigation.navigate('LanguageSettings') + }, [navigation]) const onPressSignout = React.useCallback(() => { track('Settings:SignOutButtonClicked') @@ -456,12 +455,12 @@ export const SettingsScreen = withAuthRequired( </Text> </TouchableOpacity> <TouchableOpacity - testID="contentLanguagesBtn" + testID="languageSettingsBtn" style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} - onPress={isSwitching ? undefined : onPressContentLanguages} + onPress={isSwitching ? undefined : onPressLanguageSettings} accessibilityRole="button" - accessibilityHint="Content languages" - accessibilityLabel="Opens configurable content language settings"> + accessibilityHint="Language settings" + accessibilityLabel="Opens configurable language settings"> <View style={[styles.iconContainer, pal.btn]}> <FontAwesomeIcon icon="language" @@ -469,7 +468,7 @@ export const SettingsScreen = withAuthRequired( /> </View> <Text type="lg" style={pal.text}> - Content languages + Languages </Text> </TouchableOpacity> <TouchableOpacity |