Move language preferences to new persistence + context (#1837)

zio/stable
Paul Frazee 2023-11-08 09:38:28 -08:00 committed by GitHub
parent e75b2d508b
commit 5843e212c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 233 additions and 190 deletions

View File

@ -24,6 +24,7 @@ import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@ -80,11 +81,13 @@ function App() {
return ( return (
<ShellStateProvider> <ShellStateProvider>
<MutedThreadsProvider> <PrefsStateProvider>
<InvitesStateProvider> <MutedThreadsProvider>
<InnerApp /> <InvitesStateProvider>
</InvitesStateProvider> <InnerApp />
</MutedThreadsProvider> </InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
) )
} }

View File

@ -19,6 +19,7 @@ import {queryClient} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell' import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as MutedThreadsProvider} from 'state/muted-threads' import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites' import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'
const InnerApp = observer(function AppImpl() { const InnerApp = observer(function AppImpl() {
const colorMode = useColorMode() const colorMode = useColorMode()
@ -70,11 +71,13 @@ function App() {
return ( return (
<ShellStateProvider> <ShellStateProvider>
<MutedThreadsProvider> <PrefsStateProvider>
<InvitesStateProvider> <MutedThreadsProvider>
<InnerApp /> <InvitesStateProvider>
</InvitesStateProvider> <InnerApp />
</MutedThreadsProvider> </InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider> </ShellStateProvider>
) )
} }

View File

@ -10,11 +10,10 @@ import {isObj, hasProp} from 'lib/type-guards'
import {RootStoreModel} from '../root-store' import {RootStoreModel} from '../root-store'
import {ModerationOpts} from '@atproto/api' import {ModerationOpts} from '@atproto/api'
import {DEFAULT_FEEDS} from 'lib/constants' import {DEFAULT_FEEDS} from 'lib/constants'
import {deviceLocales} from 'platform/detection'
import {getAge} from 'lib/strings/time' import {getAge} from 'lib/strings/time'
import {FeedTuner} from 'lib/api/feed-manip' import {FeedTuner} from 'lib/api/feed-manip'
import {LANGUAGES} from '../../../locale/languages'
import {logger} from '#/logger' import {logger} from '#/logger'
import {getContentLanguages} from '#/state/preferences/languages'
// TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf // TEMP we need to permanently convert 'show' to 'ignore', for now we manually convert -prf
export type LabelPreference = APILabelPreference | 'show' export type LabelPreference = APILabelPreference | 'show'
@ -34,9 +33,6 @@ const LABEL_GROUPS = [
'impersonation', 'impersonation',
] ]
const VISIBILITY_VALUES = ['ignore', 'warn', 'hide'] const VISIBILITY_VALUES = ['ignore', 'warn', 'hide']
const DEFAULT_LANG_CODES = (deviceLocales || [])
.concat(['en', 'ja', 'pt', 'de'])
.slice(0, 6)
const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random'] const THREAD_SORT_VALUES = ['oldest', 'newest', 'most-likes', 'random']
interface LegacyPreferences { interface LegacyPreferences {
@ -62,10 +58,6 @@ export class LabelPreferencesModel {
export class PreferencesModel { export class PreferencesModel {
adultContentEnabled = false adultContentEnabled = false
primaryLanguage: string = deviceLocales[0] || 'en'
contentLanguages: string[] = deviceLocales || []
postLanguage: string = deviceLocales[0] || 'en'
postLanguageHistory: string[] = DEFAULT_LANG_CODES
contentLabels = new LabelPreferencesModel() contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = [] savedFeeds: string[] = []
pinnedFeeds: string[] = [] pinnedFeeds: string[] = []
@ -103,10 +95,6 @@ export class PreferencesModel {
serialize() { serialize() {
return { return {
primaryLanguage: this.primaryLanguage,
contentLanguages: this.contentLanguages,
postLanguage: this.postLanguage,
postLanguageHistory: this.postLanguageHistory,
contentLabels: this.contentLabels, contentLabels: this.contentLabels,
savedFeeds: this.savedFeeds, savedFeeds: this.savedFeeds,
pinnedFeeds: this.pinnedFeeds, pinnedFeeds: this.pinnedFeeds,
@ -120,44 +108,6 @@ 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
if (
hasProp(v, 'contentLanguages') &&
Array.isArray(v.contentLanguages) &&
typeof v.contentLanguages.every(item => typeof item === 'string')
) {
this.contentLanguages = v.contentLanguages
} else {
// default to the device languages
this.contentLanguages = deviceLocales
}
if (hasProp(v, 'postLanguage') && typeof v.postLanguage === 'string') {
this.postLanguage = v.postLanguage
} else {
// default to the device languages
this.postLanguage = deviceLocales[0] || 'en'
}
if (
hasProp(v, 'postLanguageHistory') &&
Array.isArray(v.postLanguageHistory) &&
typeof v.postLanguageHistory.every(item => typeof item === 'string')
) {
this.postLanguageHistory = v.postLanguageHistory
.concat(DEFAULT_LANG_CODES)
.slice(0, 6)
} else {
// default to a starter set
this.postLanguageHistory = DEFAULT_LANG_CODES
}
// check if content labels in preferences exist, then hydrate // check if content labels in preferences exist, then hydrate
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') { if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
Object.assign(this.contentLabels, v.contentLabels) Object.assign(this.contentLabels, v.contentLabels)
@ -262,9 +212,6 @@ export class PreferencesModel {
try { try {
runInAction(() => { runInAction(() => {
this.contentLabels = new LabelPreferencesModel() this.contentLabels = new LabelPreferencesModel()
this.contentLanguages = deviceLocales
this.postLanguage = deviceLocales ? deviceLocales.join(',') : 'en'
this.postLanguageHistory = DEFAULT_LANG_CODES
this.savedFeeds = [] this.savedFeeds = []
this.pinnedFeeds = [] this.pinnedFeeds = []
}) })
@ -276,81 +223,6 @@ export class PreferencesModel {
} }
} }
// languages
// =
hasContentLanguage(code2: string) {
return this.contentLanguages.includes(code2)
}
toggleContentLanguage(code2: string) {
if (this.hasContentLanguage(code2)) {
this.contentLanguages = this.contentLanguages.filter(
lang => lang !== code2,
)
} else {
this.contentLanguages = this.contentLanguages.concat([code2])
}
}
/**
* A getter that splits `this.postLanguage` into an array of strings.
*
* This was previously the main field on this model, but now we're
* concatenating lang codes to make multi-selection a little better.
*/
get postLanguages() {
// filter out empty strings if exist
return this.postLanguage.split(',').filter(Boolean)
}
hasPostLanguage(code2: string) {
return this.postLanguages.includes(code2)
}
togglePostLanguage(code2: string) {
if (this.hasPostLanguage(code2)) {
this.postLanguage = this.postLanguages
.filter(lang => lang !== code2)
.join(',')
} else {
// sort alphabetically for deterministic comparison in context menu
this.postLanguage = this.postLanguages
.concat([code2])
.sort((a, b) => a.localeCompare(b))
.join(',')
}
}
setPostLanguage(commaSeparatedLangCodes: string) {
this.postLanguage = commaSeparatedLangCodes
}
/**
* Saves whatever language codes are currently selected into a history array,
* which is then used to populate the language selector menu.
*/
savePostLanguageToHistory() {
// filter out duplicate `this.postLanguage` if exists, and prepend
// value to start of array
this.postLanguageHistory = [this.postLanguage]
.concat(
this.postLanguageHistory.filter(
commaSeparatedLangCodes =>
commaSeparatedLangCodes !== this.postLanguage,
),
)
.slice(0, 6)
}
getReadablePostLanguages() {
const all = this.postLanguages.map(code2 => {
const lang = LANGUAGES.find(l => l.code2 === code2)
return lang ? lang.name : code2
})
return all.join(', ')
}
// moderation // moderation
// = // =
@ -599,17 +471,13 @@ export class PreferencesModel {
} }
} }
setPrimaryLanguage(lang: string) {
this.primaryLanguage = lang
}
getFeedTuners( getFeedTuners(
feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes', feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes',
) { ) {
if (feedType === 'custom') { if (feedType === 'custom') {
return [ return [
FeedTuner.dedupReposts, FeedTuner.dedupReposts,
FeedTuner.preferredLangOnly(this.contentLanguages), FeedTuner.preferredLangOnly(getContentLanguages()),
] ]
} }
if (feedType === 'list') { if (feedType === 'list') {

View File

@ -0,0 +1,8 @@
import React from 'react'
import {Provider as LanguagesProvider} from './languages'
export {useLanguagePrefs, useSetLanguagePrefs} from './languages'
export function Provider({children}: React.PropsWithChildren<{}>) {
return <LanguagesProvider>{children}</LanguagesProvider>
}

View File

@ -0,0 +1,122 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type SetStateCb = (
v: persisted.Schema['languagePrefs'],
) => persisted.Schema['languagePrefs']
type StateContext = persisted.Schema['languagePrefs']
type SetContext = (fn: SetStateCb) => void
const stateContext = React.createContext<StateContext>(
persisted.defaults.languagePrefs,
)
const setContext = React.createContext<SetContext>((_: SetStateCb) => {})
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('languagePrefs'))
const setStateWrapped = React.useCallback(
(fn: SetStateCb) => {
const v = fn(persisted.get('languagePrefs'))
setState(v)
persisted.write('languagePrefs', v)
},
[setState],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('languagePrefs'))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
export function useLanguagePrefs() {
return React.useContext(stateContext)
}
export function useSetLanguagePrefs() {
return React.useContext(setContext)
}
export function getContentLanguages() {
return persisted.get('languagePrefs').contentLanguages
}
export function toggleContentLanguage(
state: StateContext,
setState: SetContext,
code2: string,
) {
if (state.contentLanguages.includes(code2)) {
setState(v => ({
...v,
contentLanguages: v.contentLanguages.filter(lang => lang !== code2),
}))
} else {
setState(v => ({
...v,
contentLanguages: v.contentLanguages.concat(code2),
}))
}
}
export function toPostLanguages(postLanguage: string): string[] {
// filter out empty strings if exist
return postLanguage.split(',').filter(Boolean)
}
export function hasPostLanguage(postLanguage: string, code2: string): boolean {
return toPostLanguages(postLanguage).includes(code2)
}
export function togglePostLanguage(
state: StateContext,
setState: SetContext,
code2: string,
) {
if (hasPostLanguage(state.postLanguage, code2)) {
setState(v => ({
...v,
postLanguage: toPostLanguages(v.postLanguage)
.filter(lang => lang !== code2)
.join(','),
}))
} else {
// sort alphabetically for deterministic comparison in context menu
setState(v => ({
...v,
postLanguage: toPostLanguages(v.postLanguage)
.concat([code2])
.sort((a, b) => a.localeCompare(b))
.join(','),
}))
}
}
/**
* Saves whatever language codes are currently selected into a history array,
* which is then used to populate the language selector menu.
*/
export function savePostLanguageToHistory(setState: SetContext) {
// filter out duplicate `this.postLanguage` if exists, and prepend
// value to start of array
setState(v => ({
...v,
postLanguageHistory: [v.postLanguage]
.concat(
v.postLanguageHistory.filter(
commaSeparatedLangCodes => commaSeparatedLangCodes !== v.postLanguage,
),
)
.slice(0, 6),
}))
}

View File

@ -50,6 +50,12 @@ import {SelectLangBtn} from './select-language/SelectLangBtn'
import {EmojiPickerButton} from './text-input/web/EmojiPicker.web' import {EmojiPickerButton} from './text-input/web/EmojiPicker.web'
import {insertMentionAt} from 'lib/strings/mention-manip' import {insertMentionAt} from 'lib/strings/mention-manip'
import {useRequireAltTextEnabled} from '#/state/shell' import {useRequireAltTextEnabled} from '#/state/shell'
import {
useLanguagePrefs,
useSetLanguagePrefs,
toPostLanguages,
savePostLanguageToHistory,
} from '#/state/preferences/languages'
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@ -63,6 +69,8 @@ export const ComposePost = observer(function ComposePost({
const {isDesktop, isMobile} = useWebMediaQueries() const {isDesktop, isMobile} = useWebMediaQueries()
const store = useStores() const store = useStores()
const requireAltTextEnabled = useRequireAltTextEnabled() const requireAltTextEnabled = useRequireAltTextEnabled()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useSetLanguagePrefs()
const textInput = useRef<TextInputRef>(null) const textInput = useRef<TextInputRef>(null)
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
@ -212,7 +220,7 @@ export const ComposePost = observer(function ComposePost({
labels, labels,
onStateChange: setProcessingState, onStateChange: setProcessingState,
knownHandles: autocompleteView.knownHandles, knownHandles: autocompleteView.knownHandles,
langs: store.preferences.postLanguages, langs: toPostLanguages(langPrefs.postLanguage),
}) })
} catch (e: any) { } catch (e: any) {
if (extLink) { if (extLink) {
@ -234,7 +242,7 @@ export const ComposePost = observer(function ComposePost({
if (!replyTo) { if (!replyTo) {
store.me.mainFeed.onPostCreated() store.me.mainFeed.onPostCreated()
} }
store.preferences.savePostLanguageToHistory() savePostLanguageToHistory(setLangPrefs)
onPost?.() onPost?.()
onClose() onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`) Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)

View File

@ -15,10 +15,18 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {codeToLanguageName} from '../../../../locale/helpers' import {codeToLanguageName} from '../../../../locale/helpers'
import {
useLanguagePrefs,
useSetLanguagePrefs,
toPostLanguages,
hasPostLanguage,
} from '#/state/preferences/languages'
export const SelectLangBtn = observer(function SelectLangBtn() { export const SelectLangBtn = observer(function SelectLangBtn() {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useSetLanguagePrefs()
const onPressMore = useCallback(async () => { const onPressMore = useCallback(async () => {
if (isNative) { if (isNative) {
@ -29,8 +37,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
store.shell.openModal({name: 'post-languages-settings'}) store.shell.openModal({name: 'post-languages-settings'})
}, [store]) }, [store])
const postLanguagesPref = store.preferences.postLanguages const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
const postLanguagePref = store.preferences.postLanguage
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(() => {
let arr: DropdownItemButton[] = [] let arr: DropdownItemButton[] = []
@ -49,13 +56,14 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
arr.push({ arr.push({
icon: icon:
langCodes.every(code => store.preferences.hasPostLanguage(code)) && langCodes.every(code =>
langCodes.length === postLanguagesPref.length hasPostLanguage(langPrefs.postLanguage, code),
) && langCodes.length === postLanguagesPref.length
? ['fas', 'circle-dot'] ? ['fas', 'circle-dot']
: ['far', 'circle'], : ['far', 'circle'],
label: langName, label: langName,
onPress() { onPress() {
store.preferences.setPostLanguage(commaSeparatedLangCodes) setLangPrefs(v => ({...v, postLanguage: commaSeparatedLangCodes}))
}, },
}) })
} }
@ -65,11 +73,11 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
* Re-join here after sanitization bc postLanguageHistory is an array of * Re-join here after sanitization bc postLanguageHistory is an array of
* comma-separated strings too * comma-separated strings too
*/ */
add(postLanguagePref) add(langPrefs.postLanguage)
} }
// comma-separted strings of lang codes that have been used in the past // comma-separted strings of lang codes that have been used in the past
for (const lang of store.preferences.postLanguageHistory) { for (const lang of langPrefs.postLanguageHistory) {
add(lang) add(lang)
} }
@ -82,7 +90,7 @@ export const SelectLangBtn = observer(function SelectLangBtn() {
onPress: onPressMore, onPress: onPressMore,
}, },
] ]
}, [store.preferences, onPressMore, postLanguagePref, postLanguagesPref]) }, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref])
return ( return (
<DropdownButton <DropdownButton

View File

@ -9,11 +9,18 @@ import {deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {LanguageToggle} from './LanguageToggle' import {LanguageToggle} from './LanguageToggle'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {
useLanguagePrefs,
useSetLanguagePrefs,
toggleContentLanguage,
} from '#/state/preferences/languages'
export const snapPoints = ['100%'] export const snapPoints = ['100%']
export function Component({}: {}) { export function Component({}: {}) {
const store = useStores() const store = useStores()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useSetLanguagePrefs()
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const onPressDone = React.useCallback(() => { const onPressDone = React.useCallback(() => {
@ -29,23 +36,23 @@ export function Component({}: {}) {
// sort so that device & selected languages are on top, then alphabetically // sort so that device & selected languages are on top, then alphabetically
langs.sort((a, b) => { langs.sort((a, b) => {
const hasA = const hasA =
store.preferences.hasContentLanguage(a.code2) || langPrefs.contentLanguages.includes(a.code2) ||
deviceLocales.includes(a.code2) deviceLocales.includes(a.code2)
const hasB = const hasB =
store.preferences.hasContentLanguage(b.code2) || langPrefs.contentLanguages.includes(b.code2) ||
deviceLocales.includes(b.code2) deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name) if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1 if (hasA) return -1
return 1 return 1
}) })
return langs return langs
}, [store]) }, [langPrefs])
const onPress = React.useCallback( const onPress = React.useCallback(
(code2: string) => { (code2: string) => {
store.preferences.toggleContentLanguage(code2) toggleContentLanguage(langPrefs, setLangPrefs, code2)
}, },
[store], [langPrefs, setLangPrefs],
) )
return ( return (

View File

@ -3,7 +3,7 @@ import {StyleSheet} from 'react-native'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {useStores} from 'state/index' import {useLanguagePrefs, toPostLanguages} from '#/state/preferences/languages'
export const LanguageToggle = observer(function LanguageToggleImpl({ export const LanguageToggle = observer(function LanguageToggleImpl({
code2, code2,
@ -17,17 +17,17 @@ export const LanguageToggle = observer(function LanguageToggleImpl({
langType: 'contentLanguages' | 'postLanguages' langType: 'contentLanguages' | 'postLanguages'
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const langPrefs = useLanguagePrefs()
const isSelected = store.preferences[langType].includes(code2) const values =
langType === 'contentLanguages'
? langPrefs.contentLanguages
: toPostLanguages(langPrefs.postLanguage)
const isSelected = values.includes(code2)
// enforce a max of 3 selections for post languages // enforce a max of 3 selections for post languages
let isDisabled = false let isDisabled = false
if ( if (langType === 'postLanguages' && values.length >= 3 && !isSelected) {
langType === 'postLanguages' &&
store.preferences[langType].length >= 3 &&
!isSelected
) {
isDisabled = true isDisabled = true
} }

View File

@ -10,11 +10,19 @@ import {deviceLocales} from 'platform/detection'
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages' import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton' import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
import {ToggleButton} from 'view/com/util/forms/ToggleButton' import {ToggleButton} from 'view/com/util/forms/ToggleButton'
import {
useLanguagePrefs,
useSetLanguagePrefs,
hasPostLanguage,
togglePostLanguage,
} from '#/state/preferences/languages'
export const snapPoints = ['100%'] export const snapPoints = ['100%']
export const Component = observer(function PostLanguagesSettingsImpl() { export const Component = observer(function PostLanguagesSettingsImpl() {
const store = useStores() const store = useStores()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useSetLanguagePrefs()
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const onPressDone = React.useCallback(() => { const onPressDone = React.useCallback(() => {
@ -30,23 +38,23 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
// sort so that device & selected languages are on top, then alphabetically // sort so that device & selected languages are on top, then alphabetically
langs.sort((a, b) => { langs.sort((a, b) => {
const hasA = const hasA =
store.preferences.hasPostLanguage(a.code2) || hasPostLanguage(langPrefs.postLanguage, a.code2) ||
deviceLocales.includes(a.code2) deviceLocales.includes(a.code2)
const hasB = const hasB =
store.preferences.hasPostLanguage(b.code2) || hasPostLanguage(langPrefs.postLanguage, b.code2) ||
deviceLocales.includes(b.code2) deviceLocales.includes(b.code2)
if (hasA === hasB) return a.name.localeCompare(b.name) if (hasA === hasB) return a.name.localeCompare(b.name)
if (hasA) return -1 if (hasA) return -1
return 1 return 1
}) })
return langs return langs
}, [store]) }, [langPrefs])
const onPress = React.useCallback( const onPress = React.useCallback(
(code2: string) => { (code2: string) => {
store.preferences.togglePostLanguage(code2) togglePostLanguage(langPrefs, setLangPrefs, code2)
}, },
[store], [langPrefs, setLangPrefs],
) )
return ( return (
@ -70,14 +78,11 @@ export const Component = observer(function PostLanguagesSettingsImpl() {
</Text> </Text>
<ScrollView style={styles.scrollContainer}> <ScrollView style={styles.scrollContainer}>
{languages.map(lang => { {languages.map(lang => {
const isSelected = store.preferences.hasPostLanguage(lang.code2) const isSelected = hasPostLanguage(langPrefs.postLanguage, lang.code2)
// enforce a max of 3 selections for post languages // enforce a max of 3 selections for post languages
let isDisabled = false let isDisabled = false
if ( if (langPrefs.postLanguage.split(',').length >= 3 && !isSelected) {
store.preferences.postLanguage.split(',').length >= 3 &&
!isSelected
) {
isDisabled = true isDisabled = true
} }

View File

@ -38,6 +38,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {MAX_POST_LINES} from 'lib/constants' import {MAX_POST_LINES} from 'lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
export const PostThreadItem = observer(function PostThreadItem({ export const PostThreadItem = observer(function PostThreadItem({
item, item,
@ -54,6 +55,7 @@ export const PostThreadItem = observer(function PostThreadItem({
const store = useStores() const store = useStores()
const mutedThreads = useMutedThreads() const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute() const toggleThreadMute = useToggleThreadMute()
const langPrefs = useLanguagePrefs()
const [deleted, setDeleted] = React.useState(false) const [deleted, setDeleted] = React.useState(false)
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = React.useState(
countLines(item.richText?.text) >= MAX_POST_LINES, countLines(item.richText?.text) >= MAX_POST_LINES,
@ -85,15 +87,15 @@ export const PostThreadItem = observer(function PostThreadItem({
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
store.preferences.primaryLanguage, langPrefs.primaryLanguage,
) )
const needsTranslation = useMemo( const needsTranslation = useMemo(
() => () =>
Boolean( Boolean(
store.preferences.primaryLanguage && langPrefs.primaryLanguage &&
!isPostInLanguage(item.post, [store.preferences.primaryLanguage]), !isPostInLanguage(item.post, [langPrefs.primaryLanguage]),
), ),
[item.post, store.preferences.primaryLanguage], [item.post, langPrefs.primaryLanguage],
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {

View File

@ -34,6 +34,7 @@ import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
export const Post = observer(function PostImpl({ export const Post = observer(function PostImpl({
view, view,
@ -109,6 +110,7 @@ const PostLoaded = observer(function PostLoadedImpl({
const store = useStores() const store = useStores()
const mutedThreads = useMutedThreads() const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute() const toggleThreadMute = useToggleThreadMute()
const langPrefs = useLanguagePrefs()
const [limitLines, setLimitLines] = React.useState( const [limitLines, setLimitLines] = React.useState(
countLines(item.richText?.text) >= MAX_POST_LINES, countLines(item.richText?.text) >= MAX_POST_LINES,
) )
@ -125,7 +127,7 @@ const PostLoaded = observer(function PostLoadedImpl({
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
store.preferences.primaryLanguage, langPrefs.primaryLanguage,
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {

View File

@ -34,6 +34,7 @@ import {MAX_POST_LINES} from 'lib/constants'
import {countLines} from 'lib/strings/helpers' import {countLines} from 'lib/strings/helpers'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads'
import {useLanguagePrefs} from '#/state/preferences'
export const FeedItem = observer(function FeedItemImpl({ export const FeedItem = observer(function FeedItemImpl({
item, item,
@ -50,6 +51,7 @@ export const FeedItem = observer(function FeedItemImpl({
showReplyLine?: boolean showReplyLine?: boolean
}) { }) {
const store = useStores() const store = useStores()
const langPrefs = useLanguagePrefs()
const pal = usePalette('default') const pal = usePalette('default')
const mutedThreads = useMutedThreads() const mutedThreads = useMutedThreads()
const toggleThreadMute = useToggleThreadMute() const toggleThreadMute = useToggleThreadMute()
@ -75,7 +77,7 @@ export const FeedItem = observer(function FeedItemImpl({
}, [record?.reply]) }, [record?.reply])
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
store.preferences.primaryLanguage, langPrefs.primaryLanguage,
) )
const onPressReply = React.useCallback(() => { const onPressReply = React.useCallback(() => {

View File

@ -17,6 +17,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useLanguagePrefs} from '#/state/preferences'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export const AppPasswords = withAuthRequired( export const AppPasswords = withAuthRequired(
@ -161,6 +162,7 @@ function AppPassword({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const {contentLanguages} = useLanguagePrefs()
const onDelete = React.useCallback(async () => { const onDelete = React.useCallback(async () => {
store.shell.openModal({ store.shell.openModal({
@ -174,8 +176,6 @@ function AppPassword({
}) })
}, [store, name]) }, [store, name])
const {contentLanguages} = store.preferences
const primaryLocale = const primaryLocale =
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'

View File

@ -19,6 +19,7 @@ import {useFocusEffect} from '@react-navigation/native'
import {LANGUAGES} from 'lib/../locale/languages' import {LANGUAGES} from 'lib/../locale/languages'
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useLanguagePrefs, useSetLanguagePrefs} from '#/state/preferences'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
@ -27,6 +28,8 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const langPrefs = useLanguagePrefs()
const setLangPrefs = useSetLanguagePrefs()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const {screen, track} = useAnalytics() const {screen, track} = useAnalytics()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
@ -45,21 +48,23 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
const onChangePrimaryLanguage = React.useCallback( const onChangePrimaryLanguage = React.useCallback(
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => { (value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
store.preferences.setPrimaryLanguage(value) if (langPrefs.primaryLanguage !== value) {
setLangPrefs(v => ({...v, primaryLanguage: value}))
}
}, },
[store.preferences], [langPrefs, setLangPrefs],
) )
const myLanguages = React.useMemo(() => { const myLanguages = React.useMemo(() => {
return ( return (
store.preferences.contentLanguages langPrefs.contentLanguages
.map(lang => LANGUAGES.find(l => l.code2 === lang)) .map(lang => LANGUAGES.find(l => l.code2 === lang))
.filter(Boolean) .filter(Boolean)
// @ts-ignore // @ts-ignore
.map(l => l.name) .map(l => l.name)
.join(', ') .join(', ')
) )
}, [store.preferences.contentLanguages]) }, [langPrefs.contentLanguages])
return ( return (
<CenteredView <CenteredView
@ -82,7 +87,7 @@ export const LanguageSettingsScreen = observer(function LanguageSettingsImpl(
<View style={{position: 'relative'}}> <View style={{position: 'relative'}}>
<RNPickerSelect <RNPickerSelect
value={store.preferences.primaryLanguage} value={langPrefs.primaryLanguage}
onValueChange={onChangePrimaryLanguage} onValueChange={onChangePrimaryLanguage}
items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
label: l.name, label: l.name,