Language settings updates, new primary language setting (#1471)

* move content languages to screen

* add dropdown library, style primary lang select

* update settings button

* show selected langauges in button

* use primary language in translator link

* update copy

* lint
zio/stable
Eric Bailey 2023-09-21 13:33:19 -05:00 committed by GitHub
parent 335061f763
commit 8a5f9cd43d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 272 additions and 14 deletions

View File

@ -44,6 +44,7 @@
"@react-native-community/blur": "^4.3.0", "@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "7.2.0", "@react-native-community/datetimepicker": "7.2.0",
"@react-native-menu/menu": "^0.8.0", "@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.4.10",
"@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2", "@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6", "@react-navigation/native": "^6.1.6",
@ -130,6 +131,7 @@
"react-native-ios-context-menu": "^1.15.3", "react-native-ios-context-menu": "^1.15.3",
"react-native-linear-gradient": "^2.6.2", "react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "6.1.4", "react-native-pager-view": "6.1.4",
"react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.4.2", "react-native-reanimated": "^3.4.2",
"react-native-root-siblings": "^4.1.1", "react-native-root-siblings": "^4.1.1",

View File

@ -46,6 +46,7 @@ import {ModerationScreen} from './view/screens/Moderation'
import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists'
import {NotFoundScreen} from './view/screens/NotFound' import {NotFoundScreen} from './view/screens/NotFound'
import {SettingsScreen} from './view/screens/Settings' import {SettingsScreen} from './view/screens/Settings'
import {LanguageSettingsScreen} from './view/screens/LanguageSettings'
import {ProfileScreen} from './view/screens/Profile' import {ProfileScreen} from './view/screens/Profile'
import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' import {ProfileFollowersScreen} from './view/screens/ProfileFollowers'
import {ProfileFollowsScreen} from './view/screens/ProfileFollows' import {ProfileFollowsScreen} from './view/screens/ProfileFollows'
@ -118,6 +119,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
component={SettingsScreen} component={SettingsScreen}
options={{title: title('Settings')}} options={{title: title('Settings')}}
/> />
<Stack.Screen
name="LanguageSettings"
component={LanguageSettingsScreen}
options={{title: title('Language Settings')}}
/>
<Stack.Screen <Stack.Screen
name="Profile" name="Profile"
component={ProfileScreen} component={ProfileScreen}

View File

@ -10,6 +10,7 @@ export type CommonNavigatorParams = {
ModerationMutedAccounts: undefined ModerationMutedAccounts: undefined
ModerationBlockedAccounts: undefined ModerationBlockedAccounts: undefined
Settings: undefined Settings: undefined
LanguageSettings: undefined
Profile: {name: string; hideBackButton?: boolean} Profile: {name: string; hideBackButton?: boolean}
ProfileFollowers: {name: string} ProfileFollowers: {name: string}
ProfileFollows: {name: string} ProfileFollows: {name: string}

View File

@ -79,8 +79,8 @@ export function isPostInLanguage(
return bcp47Match.basicFilter(lang, targetLangs).length > 0 return bcp47Match.basicFilter(lang, targetLangs).length > 0
} }
export function getTranslatorLink(text: string): string { export function getTranslatorLink(text: string, lang: string): string {
return `https://translate.google.com/?sl=auto&text=${encodeURIComponent( return `https://translate.google.com/?sl=auto&tl=${lang}&text=${encodeURIComponent(
text, text,
)}` )}`
} }

View File

@ -6,6 +6,7 @@ export const router = new Router({
Feeds: '/feeds', Feeds: '/feeds',
Notifications: '/notifications', Notifications: '/notifications',
Settings: '/settings', Settings: '/settings',
LanguageSettings: '/settings/language',
Moderation: '/moderation', Moderation: '/moderation',
ModerationMuteLists: '/moderation/mute-lists', ModerationMuteLists: '/moderation/mute-lists',
ModerationMutedAccounts: '/moderation/muted-accounts', ModerationMutedAccounts: '/moderation/muted-accounts',

View File

@ -44,6 +44,7 @@ export class LabelPreferencesModel {
export class PreferencesModel { export class PreferencesModel {
adultContentEnabled = false adultContentEnabled = false
primaryLanguage: string = deviceLocales[0] || 'en'
contentLanguages: string[] = deviceLocales || [] contentLanguages: string[] = deviceLocales || []
postLanguage: string = deviceLocales[0] || 'en' postLanguage: string = deviceLocales[0] || 'en'
postLanguageHistory: string[] = DEFAULT_LANG_CODES postLanguageHistory: string[] = DEFAULT_LANG_CODES
@ -78,6 +79,7 @@ export class PreferencesModel {
serialize() { serialize() {
return { return {
primaryLanguage: this.primaryLanguage,
contentLanguages: this.contentLanguages, contentLanguages: this.contentLanguages,
postLanguage: this.postLanguage, postLanguage: this.postLanguage,
postLanguageHistory: this.postLanguageHistory, postLanguageHistory: this.postLanguageHistory,
@ -105,6 +107,15 @@ export class PreferencesModel {
*/ */
hydrate(v: unknown) { hydrate(v: unknown) {
if (isObj(v)) { 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 // check if content languages in preferences exist, otherwise default to device languages
if ( if (
hasProp(v, 'contentLanguages') && hasProp(v, 'contentLanguages') &&
@ -542,6 +553,10 @@ export class PreferencesModel {
this.requireAltTextEnabled = !this.requireAltTextEnabled this.requireAltTextEnabled = !this.requireAltTextEnabled
} }
setPrimaryLanguage(lang: string) {
this.primaryLanguage = lang
}
getFeedTuners( getFeedTuners(
feedType: 'home' | 'following' | 'author' | 'custom' | 'likes', feedType: 'home' | 'following' | 'author' | 'custom' | 'likes',
) { ) {

View File

@ -75,7 +75,10 @@ export const PostThreadItem = observer(function PostThreadItem({
}, [item.post.uri, item.post.author]) }, [item.post.uri, item.post.author])
const repostsTitle = 'Reposts of this post' const repostsTitle = 'Reposts of this post'
const translatorUrl = getTranslatorLink(record?.text || '') const translatorUrl = getTranslatorLink(
record?.text || '',
store.preferences.primaryLanguage,
)
const needsTranslation = useMemo( const needsTranslation = useMemo(
() => () =>
store.preferences.contentLanguages.length > 0 && store.preferences.contentLanguages.length > 0 &&

View File

@ -115,7 +115,10 @@ const PostLoaded = observer(function PostLoadedImpl({
replyAuthorDid = urip.hostname replyAuthorDid = urip.hostname
} }
const translatorUrl = getTranslatorLink(record?.text || '') const translatorUrl = getTranslatorLink(
record?.text || '',
store.preferences.primaryLanguage,
)
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
store.shell.openComposer({ store.shell.openComposer({

View File

@ -64,7 +64,10 @@ export const FeedItem = observer(function FeedItemImpl({
const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri)
return urip.hostname return urip.hostname
}, [record?.reply]) }, [record?.reply])
const translatorUrl = getTranslatorLink(record?.text || '') const translatorUrl = getTranslatorLink(
record?.text || '',
store.preferences.primaryLanguage,
)
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {
track('FeedItem:PostReply') track('FeedItem:PostReply')

View File

@ -97,6 +97,7 @@ import {faUserXmark} from '@fortawesome/free-solid-svg-icons/faUserXmark'
import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash'
import {faX} from '@fortawesome/free-solid-svg-icons/faX' import {faX} from '@fortawesome/free-solid-svg-icons/faX'
import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark'
import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown'
export function setup() { export function setup() {
library.add( library.add(
@ -197,5 +198,6 @@ export function setup() {
faTrashCan, faTrashCan,
faX, faX,
faXmark, faXmark,
faChevronDown,
) )
} }

View File

@ -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,
},
})

View File

@ -145,10 +145,9 @@ export const SettingsScreen = withAuthRequired(
store.shell.openModal({name: 'invite-codes'}) store.shell.openModal({name: 'invite-codes'})
}, [track, store]) }, [track, store])
const onPressContentLanguages = React.useCallback(() => { const onPressLanguageSettings = React.useCallback(() => {
track('Settings:ContentlanguagesButtonClicked') navigation.navigate('LanguageSettings')
store.shell.openModal({name: 'content-languages-settings'}) }, [navigation])
}, [track, store])
const onPressSignout = React.useCallback(() => { const onPressSignout = React.useCallback(() => {
track('Settings:SignOutButtonClicked') track('Settings:SignOutButtonClicked')
@ -456,12 +455,12 @@ export const SettingsScreen = withAuthRequired(
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
testID="contentLanguagesBtn" testID="languageSettingsBtn"
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
onPress={isSwitching ? undefined : onPressContentLanguages} onPress={isSwitching ? undefined : onPressLanguageSettings}
accessibilityRole="button" accessibilityRole="button"
accessibilityHint="Content languages" accessibilityHint="Language settings"
accessibilityLabel="Opens configurable content language settings"> accessibilityLabel="Opens configurable language settings">
<View style={[styles.iconContainer, pal.btn]}> <View style={[styles.iconContainer, pal.btn]}>
<FontAwesomeIcon <FontAwesomeIcon
icon="language" icon="language"
@ -469,7 +468,7 @@ export const SettingsScreen = withAuthRequired(
/> />
</View> </View>
<Text type="lg" style={pal.text}> <Text type="lg" style={pal.text}>
Content languages Languages
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity

View File

@ -3374,6 +3374,16 @@
resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-0.8.0.tgz#dbf227c2081e5ffd3d2073ee68ecc84cf8639727" resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-0.8.0.tgz#dbf227c2081e5ffd3d2073ee68ecc84cf8639727"
integrity sha512-kxiT6ySZsDbBvNWovrKVAfs4AQvAytKIf0f8KQLkVO6eNYMUmONBQPzi6onTTbVujXtZHambo7qr/PcedaR8Tg== integrity sha512-kxiT6ySZsDbBvNWovrKVAfs4AQvAytKIf0f8KQLkVO6eNYMUmONBQPzi6onTTbVujXtZHambo7qr/PcedaR8Tg==
"@react-native-picker/picker@2.4.10":
version "2.4.10"
resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-2.4.10.tgz#339c7bfc6e1d9a5e934122eaaa7767dc1c5fb725"
integrity sha512-EvAlHmPEPOwvbP6Pjg/gtDV3XJzIjIxr10fXFNlX5r9HeHw582G1Zt2o8FLyB718nOttgj8HYUTGxvhu4N65sQ==
"@react-native-picker/picker@^1.8.3":
version "1.16.8"
resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-1.16.8.tgz#2126ca54d4a5a3e9ea5e3f39ad1e6643f8e4b3d4"
integrity sha512-pacdQDX6V6EmjF+HoiIh6u++qx4mTK0WnhgUHRc01B+Qt5eoeUwseBqmqfTSXTx/aHDEd6PiIw7UGvKgFoqgFQ==
"@react-native/assets-registry@^0.72.0": "@react-native/assets-registry@^0.72.0":
version "0.72.0" version "0.72.0"
resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.72.0.tgz#c82a76a1d86ec0c3907be76f7faf97a32bbed05d" resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.72.0.tgz#c82a76a1d86ec0c3907be76f7faf97a32bbed05d"
@ -15769,6 +15779,14 @@ react-native-pager-view@6.1.4:
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.4.tgz#3a63ebd1b72f81991157ea552bb9c887e529bc8c" resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.4.tgz#3a63ebd1b72f81991157ea552bb9c887e529bc8c"
integrity sha512-fmTwgGwPxGCBusKAq7gHzm+s1Yp0qh5rKPoQszaCuxrb+76KgK4Qe82jJNPUp2xTZOKSw+FbJU2QahF8ncTl+w== integrity sha512-fmTwgGwPxGCBusKAq7gHzm+s1Yp0qh5rKPoQszaCuxrb+76KgK4Qe82jJNPUp2xTZOKSw+FbJU2QahF8ncTl+w==
react-native-picker-select@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/react-native-picker-select/-/react-native-picker-select-8.1.0.tgz#667a5442f783f4bcfd3f65880c6926155fd2c39c"
integrity sha512-iLsLv2OEWpXnQMDYJS6du5Cl1HTHy887n60Yp5OOiMny0TDB9w5CfxTUYWtpsvJJrUa/Yrv+1NMQiJy7IA4ETw==
dependencies:
"@react-native-picker/picker" "^1.8.3"
lodash.isequal "^4.5.0"
react-native-progress@bluesky-social/react-native-progress: react-native-progress@bluesky-social/react-native-progress:
version "5.0.0" version "5.0.0"
resolved "https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4" resolved "https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"