diff --git a/package.json b/package.json index c6c7bfa9..67747791 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@react-native-community/blur": "^4.3.0", "@react-native-community/datetimepicker": "7.2.0", "@react-native-menu/menu": "^0.8.0", + "@react-native-picker/picker": "2.4.10", "@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/drawer": "^6.6.2", "@react-navigation/native": "^6.1.6", @@ -130,6 +131,7 @@ "react-native-ios-context-menu": "^1.15.3", "react-native-linear-gradient": "^2.6.2", "react-native-pager-view": "6.1.4", + "react-native-picker-select": "^8.1.0", "react-native-progress": "bluesky-social/react-native-progress", "react-native-reanimated": "^3.4.2", "react-native-root-siblings": "^4.1.1", diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 604fca2b..a247c72d 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' @@ -118,6 +119,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { component={SettingsScreen} options={{title: title('Settings')}} /> + 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 35266d85..7049d60f 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 5e07685c..5ae39167 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 fc068469..b98ec805 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 d7559e3c..d5191cf4 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 23d8546b..71be3969 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 07848aa8..1e6f2741 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 00000000..8b952a56 --- /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 + +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[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 ( + + + + + + + Primary Language + + + Select your preferred language for translations in your feed. + + + + 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, + }, + }} + /> + + + + + + + + + + + + Content Languages + + + Select which languages you want your subscribed feeds to include. If + none are selected, all languages will be shown. + + + + + + + ) +}) + +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 1ff5f58f..4783f335 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( + accessibilityHint="Language settings" + accessibilityLabel="Opens configurable language settings"> - Content languages + Languages