Improve the language behaviors around the PWI (#3545)
* Handle leftnav overflow with longer languages' copy * Update the language dropdown to set ALL language prefs * Add hackfix to language cachebusting on PWI * Reset feeds on language changezio/stable
parent
23056daa29
commit
0b43d728e4
|
@ -1,16 +1,19 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
|
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||||
import {APP_LANGUAGES} from '#/locale/languages'
|
import {APP_LANGUAGES} from '#/locale/languages'
|
||||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||||
|
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||||
|
|
||||||
export function AppLanguageDropdown() {
|
export function AppLanguageDropdown() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
|
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
|
||||||
|
@ -21,8 +24,13 @@ export function AppLanguageDropdown() {
|
||||||
if (sanitizedLang !== value) {
|
if (sanitizedLang !== value) {
|
||||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||||
}
|
}
|
||||||
|
setLangPrefs.setPrimaryLanguage(value)
|
||||||
|
setLangPrefs.setContentLanguage(value)
|
||||||
|
|
||||||
|
// reset feeds to refetch content
|
||||||
|
resetPostsFeedQueries(queryClient)
|
||||||
},
|
},
|
||||||
[sanitizedLang, setLangPrefs],
|
[sanitizedLang, setLangPrefs, queryClient],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||||
import {APP_LANGUAGES} from '#/locale/languages'
|
import {APP_LANGUAGES} from '#/locale/languages'
|
||||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||||
|
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
@ -11,6 +13,7 @@ import {Text} from '#/components/Typography'
|
||||||
export function AppLanguageDropdown() {
|
export function AppLanguageDropdown() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
|
|
||||||
|
@ -24,8 +27,13 @@ export function AppLanguageDropdown() {
|
||||||
if (sanitizedLang !== value) {
|
if (sanitizedLang !== value) {
|
||||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||||
}
|
}
|
||||||
|
setLangPrefs.setPrimaryLanguage(value)
|
||||||
|
setLangPrefs.setContentLanguage(value)
|
||||||
|
|
||||||
|
// reset feeds to refetch content
|
||||||
|
resetPostsFeedQueries(queryClient)
|
||||||
},
|
},
|
||||||
[sanitizedLang, setLangPrefs],
|
[sanitizedLang, setLangPrefs, queryClient],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import {
|
import {
|
||||||
AppBskyFeedDefs,
|
AppBskyFeedDefs,
|
||||||
AppBskyFeedGetFeed as GetCustomFeed,
|
AppBskyFeedGetFeed as GetCustomFeed,
|
||||||
|
AtpAgent,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {FeedAPI, FeedAPIResponse} from './types'
|
|
||||||
import {getAgent} from '#/state/session'
|
|
||||||
import {getContentLanguages} from '#/state/preferences/languages'
|
import {getContentLanguages} from '#/state/preferences/languages'
|
||||||
|
import {getAgent} from '#/state/session'
|
||||||
|
import {FeedAPI, FeedAPIResponse} from './types'
|
||||||
|
|
||||||
export class CustomFeedAPI implements FeedAPI {
|
export class CustomFeedAPI implements FeedAPI {
|
||||||
constructor(public params: GetCustomFeed.QueryParams) {}
|
constructor(public params: GetCustomFeed.QueryParams) {}
|
||||||
|
@ -29,7 +31,9 @@ export class CustomFeedAPI implements FeedAPI {
|
||||||
limit: number
|
limit: number
|
||||||
}): Promise<FeedAPIResponse> {
|
}): Promise<FeedAPIResponse> {
|
||||||
const contentLangs = getContentLanguages().join(',')
|
const contentLangs = getContentLanguages().join(',')
|
||||||
const res = await getAgent().app.bsky.feed.getFeed(
|
const agent = getAgent()
|
||||||
|
const res = agent.session
|
||||||
|
? await getAgent().app.bsky.feed.getFeed(
|
||||||
{
|
{
|
||||||
...this.params,
|
...this.params,
|
||||||
cursor,
|
cursor,
|
||||||
|
@ -37,6 +41,7 @@ export class CustomFeedAPI implements FeedAPI {
|
||||||
},
|
},
|
||||||
{headers: {'Accept-Language': contentLangs}},
|
{headers: {'Accept-Language': contentLangs}},
|
||||||
)
|
)
|
||||||
|
: await loggedOutFetch({...this.params, cursor, limit})
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
// NOTE
|
// NOTE
|
||||||
// some custom feeds fail to enforce the pagination limit
|
// some custom feeds fail to enforce the pagination limit
|
||||||
|
@ -55,3 +60,59 @@ export class CustomFeedAPI implements FeedAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HACK
|
||||||
|
// we want feeds to give language-specific results immediately when a
|
||||||
|
// logged-out user changes their language. this comes with two problems:
|
||||||
|
// 1. not all languages have content, and
|
||||||
|
// 2. our public caching layer isnt correctly busting against the accept-language header
|
||||||
|
// for now we handle both of these with a manual workaround
|
||||||
|
// -prf
|
||||||
|
async function loggedOutFetch({
|
||||||
|
feed,
|
||||||
|
limit,
|
||||||
|
cursor,
|
||||||
|
}: {
|
||||||
|
feed: string
|
||||||
|
limit: number
|
||||||
|
cursor?: string
|
||||||
|
}) {
|
||||||
|
let contentLangs = getContentLanguages().join(',')
|
||||||
|
|
||||||
|
// manually construct fetch call so we can add the `lang` cache-busting param
|
||||||
|
let res = await AtpAgent.fetch!(
|
||||||
|
`https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
|
||||||
|
cursor ? `&cursor=${cursor}` : ''
|
||||||
|
}&limit=${limit}&lang=${contentLangs}`,
|
||||||
|
'GET',
|
||||||
|
{'Accept-Language': contentLangs},
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
if (res.body?.feed?.length) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: res.body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no data, try again with language headers removed
|
||||||
|
res = await AtpAgent.fetch!(
|
||||||
|
`https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${
|
||||||
|
cursor ? `&cursor=${cursor}` : ''
|
||||||
|
}&limit=${limit}`,
|
||||||
|
'GET',
|
||||||
|
{'Accept-Language': ''},
|
||||||
|
undefined,
|
||||||
|
)
|
||||||
|
if (res.body?.feed?.length) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: res.body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
data: {feed: []},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import * as persisted from '#/state/persisted'
|
|
||||||
import {AppLanguage} from '#/locale/languages'
|
import {AppLanguage} from '#/locale/languages'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
|
||||||
type SetStateCb = (
|
type SetStateCb = (
|
||||||
s: persisted.Schema['languagePrefs'],
|
s: persisted.Schema['languagePrefs'],
|
||||||
|
@ -9,6 +10,7 @@ type StateContext = persisted.Schema['languagePrefs']
|
||||||
type ApiContext = {
|
type ApiContext = {
|
||||||
setPrimaryLanguage: (code2: string) => void
|
setPrimaryLanguage: (code2: string) => void
|
||||||
setPostLanguage: (commaSeparatedLangCodes: string) => void
|
setPostLanguage: (commaSeparatedLangCodes: string) => void
|
||||||
|
setContentLanguage: (code2: string) => void
|
||||||
toggleContentLanguage: (code2: string) => void
|
toggleContentLanguage: (code2: string) => void
|
||||||
togglePostLanguage: (code2: string) => void
|
togglePostLanguage: (code2: string) => void
|
||||||
savePostLanguageToHistory: () => void
|
savePostLanguageToHistory: () => void
|
||||||
|
@ -21,6 +23,7 @@ const stateContext = React.createContext<StateContext>(
|
||||||
const apiContext = React.createContext<ApiContext>({
|
const apiContext = React.createContext<ApiContext>({
|
||||||
setPrimaryLanguage: (_: string) => {},
|
setPrimaryLanguage: (_: string) => {},
|
||||||
setPostLanguage: (_: string) => {},
|
setPostLanguage: (_: string) => {},
|
||||||
|
setContentLanguage: (_: string) => {},
|
||||||
toggleContentLanguage: (_: string) => {},
|
toggleContentLanguage: (_: string) => {},
|
||||||
togglePostLanguage: (_: string) => {},
|
togglePostLanguage: (_: string) => {},
|
||||||
savePostLanguageToHistory: () => {},
|
savePostLanguageToHistory: () => {},
|
||||||
|
@ -53,6 +56,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
setPostLanguage(commaSeparatedLangCodes: string) {
|
setPostLanguage(commaSeparatedLangCodes: string) {
|
||||||
setStateWrapped(s => ({...s, postLanguage: commaSeparatedLangCodes}))
|
setStateWrapped(s => ({...s, postLanguage: commaSeparatedLangCodes}))
|
||||||
},
|
},
|
||||||
|
setContentLanguage(code2: string) {
|
||||||
|
setStateWrapped(s => ({...s, contentLanguages: [code2]}))
|
||||||
|
},
|
||||||
toggleContentLanguage(code2: string) {
|
toggleContentLanguage(code2: string) {
|
||||||
setStateWrapped(s => {
|
setStateWrapped(s => {
|
||||||
const exists = s.contentLanguages.includes(code2)
|
const exists = s.contentLanguages.includes(code2)
|
||||||
|
|
|
@ -459,6 +459,14 @@ function assertSomePostsPassModeration(feed: AppBskyFeedDefs.FeedViewPost[]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resetPostsFeedQueries(queryClient: QueryClient, timeout = 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
queryClient.resetQueries({
|
||||||
|
predicate: query => query.queryKey[0] === RQKEY_ROOT,
|
||||||
|
})
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
export function resetProfilePostsQueries(
|
export function resetProfilePostsQueries(
|
||||||
queryClient: QueryClient,
|
queryClient: QueryClient,
|
||||||
did: string,
|
did: string,
|
||||||
|
|
|
@ -48,7 +48,13 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{flexDirection: 'row', paddingTop: 12, gap: 8}}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
paddingTop: 12,
|
||||||
|
gap: 8,
|
||||||
|
}}>
|
||||||
<Button
|
<Button
|
||||||
onPress={showCreateAccount}
|
onPress={showCreateAccount}
|
||||||
accessibilityHint={_(msg`Sign up`)}
|
accessibilityHint={_(msg`Sign up`)}
|
||||||
|
|
Loading…
Reference in New Issue