Skip to content

Commit

Permalink
Move language preferences to new persistence + context (#1837)
Browse files Browse the repository at this point in the history
  • Loading branch information
pfrazee authored Nov 8, 2023
1 parent e75b2d5 commit 5843e21
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 190 deletions.
13 changes: 8 additions & 5 deletions src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {TestCtrls} from 'view/com/testing/TestCtrls'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'

SplashScreen.preventAutoHideAsync()

Expand Down Expand Up @@ -80,11 +81,13 @@ function App() {

return (
<ShellStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<InnerApp />
</InvitesStateProvider>
</MutedThreadsProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<InnerApp />
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
)
}
Expand Down
13 changes: 8 additions & 5 deletions src/App.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {queryClient} from 'lib/react-query'
import {Provider as ShellStateProvider} from 'state/shell'
import {Provider as MutedThreadsProvider} from 'state/muted-threads'
import {Provider as InvitesStateProvider} from 'state/invites'
import {Provider as PrefsStateProvider} from 'state/preferences'

const InnerApp = observer(function AppImpl() {
const colorMode = useColorMode()
Expand Down Expand Up @@ -70,11 +71,13 @@ function App() {

return (
<ShellStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<InnerApp />
</InvitesStateProvider>
</MutedThreadsProvider>
<PrefsStateProvider>
<MutedThreadsProvider>
<InvitesStateProvider>
<InnerApp />
</InvitesStateProvider>
</MutedThreadsProvider>
</PrefsStateProvider>
</ShellStateProvider>
)
}
Expand Down
136 changes: 2 additions & 134 deletions src/state/models/ui/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {isObj, hasProp} from 'lib/type-guards'
import {RootStoreModel} from '../root-store'
import {ModerationOpts} from '@atproto/api'
import {DEFAULT_FEEDS} from 'lib/constants'
import {deviceLocales} from 'platform/detection'
import {getAge} from 'lib/strings/time'
import {FeedTuner} from 'lib/api/feed-manip'
import {LANGUAGES} from '../../../locale/languages'
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
export type LabelPreference = APILabelPreference | 'show'
Expand All @@ -34,9 +33,6 @@ const LABEL_GROUPS = [
'impersonation',
]
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']

interface LegacyPreferences {
Expand All @@ -62,10 +58,6 @@ 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
contentLabels = new LabelPreferencesModel()
savedFeeds: string[] = []
pinnedFeeds: string[] = []
Expand Down Expand Up @@ -103,10 +95,6 @@ export class PreferencesModel {

serialize() {
return {
primaryLanguage: this.primaryLanguage,
contentLanguages: this.contentLanguages,
postLanguage: this.postLanguage,
postLanguageHistory: this.postLanguageHistory,
contentLabels: this.contentLabels,
savedFeeds: this.savedFeeds,
pinnedFeeds: this.pinnedFeeds,
Expand All @@ -120,44 +108,6 @@ 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') &&
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
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
Object.assign(this.contentLabels, v.contentLabels)
Expand Down Expand Up @@ -262,9 +212,6 @@ export class PreferencesModel {
try {
runInAction(() => {
this.contentLabels = new LabelPreferencesModel()
this.contentLanguages = deviceLocales
this.postLanguage = deviceLocales ? deviceLocales.join(',') : 'en'
this.postLanguageHistory = DEFAULT_LANG_CODES
this.savedFeeds = []
this.pinnedFeeds = []
})
Expand All @@ -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
// =

Expand Down Expand Up @@ -599,17 +471,13 @@ export class PreferencesModel {
}
}

setPrimaryLanguage(lang: string) {
this.primaryLanguage = lang
}

getFeedTuners(
feedType: 'home' | 'following' | 'author' | 'custom' | 'list' | 'likes',
) {
if (feedType === 'custom') {
return [
FeedTuner.dedupReposts,
FeedTuner.preferredLangOnly(this.contentLanguages),
FeedTuner.preferredLangOnly(getContentLanguages()),
]
}
if (feedType === 'list') {
Expand Down
8 changes: 8 additions & 0 deletions src/state/preferences/index.tsx
Original file line number Diff line number Diff line change
@@ -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>
}
122 changes: 122 additions & 0 deletions src/state/preferences/languages.tsx
Original file line number Diff line number Diff line change
@@ -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),
}))
}
Loading

0 comments on commit 5843e21

Please sign in to comment.