forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2
3import {type AppLanguage} from '#/locale/languages'
4import * as persisted from '#/state/persisted'
5import {AnalyticsContext, utils} from '#/analytics'
6
7type SetStateCb = (
8 s: persisted.Schema['languagePrefs'],
9) => persisted.Schema['languagePrefs']
10type StateContext = persisted.Schema['languagePrefs']
11type ApiContext = {
12 setPrimaryLanguage: (code2: string) => void
13 setPostLanguage: (commaSeparatedLangCodes: string) => void
14 setContentLanguage: (code2: string) => void
15 toggleContentLanguage: (code2: string) => void
16 togglePostLanguage: (code2: string) => void
17 savePostLanguageToHistory: () => void
18 setAppLanguage: (code2: AppLanguage) => void
19}
20
21const stateContext = React.createContext<StateContext>(
22 persisted.defaults.languagePrefs,
23)
24stateContext.displayName = 'LanguagePrefsStateContext'
25const apiContext = React.createContext<ApiContext>({
26 setPrimaryLanguage: (_: string) => {},
27 setPostLanguage: (_: string) => {},
28 setContentLanguage: (_: string) => {},
29 toggleContentLanguage: (_: string) => {},
30 togglePostLanguage: (_: string) => {},
31 savePostLanguageToHistory: () => {},
32 setAppLanguage: (_: AppLanguage) => {},
33})
34apiContext.displayName = 'LanguagePrefsApiContext'
35
36export function Provider({children}: React.PropsWithChildren<{}>) {
37 const [state, setState] = React.useState(persisted.get('languagePrefs'))
38
39 const setStateWrapped = React.useCallback(
40 (fn: SetStateCb) => {
41 const s = fn(persisted.get('languagePrefs'))
42 setState(s)
43 persisted.write('languagePrefs', s)
44 },
45 [setState],
46 )
47
48 React.useEffect(() => {
49 return persisted.onUpdate('languagePrefs', nextLanguagePrefs => {
50 setState(nextLanguagePrefs)
51 })
52 }, [setStateWrapped])
53
54 const api = React.useMemo(
55 () => ({
56 setPrimaryLanguage(code2: string) {
57 setStateWrapped(s => ({...s, primaryLanguage: code2}))
58 },
59 setPostLanguage(commaSeparatedLangCodes: string) {
60 setStateWrapped(s => ({...s, postLanguage: commaSeparatedLangCodes}))
61 },
62 setContentLanguage(code2: string) {
63 setStateWrapped(s => ({...s, contentLanguages: [code2]}))
64 },
65 toggleContentLanguage(code2: string) {
66 setStateWrapped(s => {
67 const exists = s.contentLanguages.includes(code2)
68 const next = exists
69 ? s.contentLanguages.filter(lang => lang !== code2)
70 : s.contentLanguages.concat(code2)
71 return {
72 ...s,
73 contentLanguages: next,
74 }
75 })
76 },
77 togglePostLanguage(code2: string) {
78 setStateWrapped(s => {
79 const exists = hasPostLanguage(state.postLanguage, code2)
80 let next = s.postLanguage
81
82 if (exists) {
83 next = toPostLanguages(s.postLanguage)
84 .filter(lang => lang !== code2)
85 .join(',')
86 } else {
87 // sort alphabetically for deterministic comparison in context menu
88 next = toPostLanguages(s.postLanguage)
89 .concat([code2])
90 .sort((a, b) => a.localeCompare(b))
91 .join(',')
92 }
93
94 return {
95 ...s,
96 postLanguage: next,
97 }
98 })
99 },
100 /**
101 * Saves whatever language codes are currently selected into a history array,
102 * which is then used to populate the language selector menu.
103 */
104 savePostLanguageToHistory() {
105 // filter out duplicate `this.postLanguage` if exists, and prepend
106 // value to start of array
107 setStateWrapped(s => ({
108 ...s,
109 postLanguageHistory: [s.postLanguage]
110 .concat(
111 s.postLanguageHistory.filter(
112 commaSeparatedLangCodes =>
113 commaSeparatedLangCodes !== s.postLanguage,
114 ),
115 )
116 .slice(0, 6),
117 }))
118 },
119 setAppLanguage(code2: AppLanguage) {
120 setStateWrapped(s => ({...s, appLanguage: code2}))
121 },
122 }),
123 [state, setStateWrapped],
124 )
125
126 return (
127 <stateContext.Provider value={state}>
128 <apiContext.Provider value={api}>
129 <AnalyticsContext
130 metadata={utils.useMeta({
131 preferences: {
132 appLanguage: state.appLanguage,
133 contentLanguages: state.contentLanguages,
134 },
135 })}>
136 {children}
137 </AnalyticsContext>
138 </apiContext.Provider>
139 </stateContext.Provider>
140 )
141}
142
143export function useLanguagePrefs() {
144 return React.useContext(stateContext)
145}
146
147export function useLanguagePrefsApi() {
148 return React.useContext(apiContext)
149}
150
151export function getContentLanguages() {
152 return persisted.get('languagePrefs').contentLanguages
153}
154
155/**
156 * Be careful with this. It's used for the PWI home screen so that users can
157 * select a UI language and have it apply to the fetched Discover feed.
158 *
159 * We only support BCP-47 two-letter codes here, hence the split.
160 */
161export function getAppLanguageAsContentLanguage() {
162 return persisted.get('languagePrefs').appLanguage.split('-')[0]
163}
164
165export function toPostLanguages(postLanguage: string): string[] {
166 // filter out empty strings if exist
167 return postLanguage.split(',').filter(Boolean)
168}
169
170export function fromPostLanguages(languages: string[]): string {
171 return languages.filter(Boolean).join(',')
172}
173
174export function hasPostLanguage(postLanguage: string, code2: string): boolean {
175 return toPostLanguages(postLanguage).includes(code2)
176}