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
This commit is contained in:
parent
335061f763
commit
8a5f9cd43d
13 changed files with 272 additions and 14 deletions
|
@ -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')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LanguageSettings"
|
||||
component={LanguageSettingsScreen}
|
||||
options={{title: title('Language Settings')}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Profile"
|
||||
component={ProfileScreen}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
)}`
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
) {
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
205
src/view/screens/LanguageSettings.tsx
Normal file
205
src/view/screens/LanguageSettings.tsx
Normal 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,
|
||||
},
|
||||
})
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue