Bluesky app fork with some witchin' additions 💫

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 change

authored by

Paul Frazee and committed by
GitHub
0b43d728 23056daa

+111 -14
+9 -1
src/components/AppLanguageDropdown.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' 4 + import {useQueryClient} from '@tanstack/react-query' 4 5 5 6 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 6 7 import {APP_LANGUAGES} from '#/locale/languages' 7 8 import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 9 + import {resetPostsFeedQueries} from '#/state/queries/post-feed' 8 10 import {atoms as a, useTheme} from '#/alf' 9 11 import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 10 12 11 13 export function AppLanguageDropdown() { 12 14 const t = useTheme() 13 15 16 + const queryClient = useQueryClient() 14 17 const langPrefs = useLanguagePrefs() 15 18 const setLangPrefs = useLanguagePrefsApi() 16 19 const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage) ··· 21 24 if (sanitizedLang !== value) { 22 25 setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 23 26 } 27 + setLangPrefs.setPrimaryLanguage(value) 28 + setLangPrefs.setContentLanguage(value) 29 + 30 + // reset feeds to refetch content 31 + resetPostsFeedQueries(queryClient) 24 32 }, 25 - [sanitizedLang, setLangPrefs], 33 + [sanitizedLang, setLangPrefs, queryClient], 26 34 ) 27 35 28 36 return (
+9 -1
src/components/AppLanguageDropdown.web.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 + import {useQueryClient} from '@tanstack/react-query' 3 4 4 5 import {sanitizeAppLanguageSetting} from '#/locale/helpers' 5 6 import {APP_LANGUAGES} from '#/locale/languages' 6 7 import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences' 8 + import {resetPostsFeedQueries} from '#/state/queries/post-feed' 7 9 import {atoms as a, useTheme} from '#/alf' 8 10 import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron' 9 11 import {Text} from '#/components/Typography' ··· 11 13 export function AppLanguageDropdown() { 12 14 const t = useTheme() 13 15 16 + const queryClient = useQueryClient() 14 17 const langPrefs = useLanguagePrefs() 15 18 const setLangPrefs = useLanguagePrefsApi() 16 19 ··· 24 27 if (sanitizedLang !== value) { 25 28 setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) 26 29 } 30 + setLangPrefs.setPrimaryLanguage(value) 31 + setLangPrefs.setContentLanguage(value) 32 + 33 + // reset feeds to refetch content 34 + resetPostsFeedQueries(queryClient) 27 35 }, 28 - [sanitizedLang, setLangPrefs], 36 + [sanitizedLang, setLangPrefs, queryClient], 29 37 ) 30 38 31 39 return (
+71 -10
src/lib/api/feed/custom.ts
··· 1 1 import { 2 2 AppBskyFeedDefs, 3 3 AppBskyFeedGetFeed as GetCustomFeed, 4 + AtpAgent, 4 5 } from '@atproto/api' 5 - import {FeedAPI, FeedAPIResponse} from './types' 6 - import {getAgent} from '#/state/session' 6 + 7 7 import {getContentLanguages} from '#/state/preferences/languages' 8 + import {getAgent} from '#/state/session' 9 + import {FeedAPI, FeedAPIResponse} from './types' 8 10 9 11 export class CustomFeedAPI implements FeedAPI { 10 12 constructor(public params: GetCustomFeed.QueryParams) {} ··· 29 31 limit: number 30 32 }): Promise<FeedAPIResponse> { 31 33 const contentLangs = getContentLanguages().join(',') 32 - const res = await getAgent().app.bsky.feed.getFeed( 33 - { 34 - ...this.params, 35 - cursor, 36 - limit, 37 - }, 38 - {headers: {'Accept-Language': contentLangs}}, 39 - ) 34 + const agent = getAgent() 35 + const res = agent.session 36 + ? await getAgent().app.bsky.feed.getFeed( 37 + { 38 + ...this.params, 39 + cursor, 40 + limit, 41 + }, 42 + {headers: {'Accept-Language': contentLangs}}, 43 + ) 44 + : await loggedOutFetch({...this.params, cursor, limit}) 40 45 if (res.success) { 41 46 // NOTE 42 47 // some custom feeds fail to enforce the pagination limit ··· 55 60 } 56 61 } 57 62 } 63 + 64 + // HACK 65 + // we want feeds to give language-specific results immediately when a 66 + // logged-out user changes their language. this comes with two problems: 67 + // 1. not all languages have content, and 68 + // 2. our public caching layer isnt correctly busting against the accept-language header 69 + // for now we handle both of these with a manual workaround 70 + // -prf 71 + async function loggedOutFetch({ 72 + feed, 73 + limit, 74 + cursor, 75 + }: { 76 + feed: string 77 + limit: number 78 + cursor?: string 79 + }) { 80 + let contentLangs = getContentLanguages().join(',') 81 + 82 + // manually construct fetch call so we can add the `lang` cache-busting param 83 + let res = await AtpAgent.fetch!( 84 + `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 85 + cursor ? `&cursor=${cursor}` : '' 86 + }&limit=${limit}&lang=${contentLangs}`, 87 + 'GET', 88 + {'Accept-Language': contentLangs}, 89 + undefined, 90 + ) 91 + if (res.body?.feed?.length) { 92 + return { 93 + success: true, 94 + data: res.body, 95 + } 96 + } 97 + 98 + // no data, try again with language headers removed 99 + res = await AtpAgent.fetch!( 100 + `https://api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${feed}${ 101 + cursor ? `&cursor=${cursor}` : '' 102 + }&limit=${limit}`, 103 + 'GET', 104 + {'Accept-Language': ''}, 105 + undefined, 106 + ) 107 + if (res.body?.feed?.length) { 108 + return { 109 + success: true, 110 + data: res.body, 111 + } 112 + } 113 + 114 + return { 115 + success: false, 116 + data: {feed: []}, 117 + } 118 + }
+7 -1
src/state/preferences/languages.tsx
··· 1 1 import React from 'react' 2 + 3 + import {AppLanguage} from '#/locale/languages' 2 4 import * as persisted from '#/state/persisted' 3 - import {AppLanguage} from '#/locale/languages' 4 5 5 6 type SetStateCb = ( 6 7 s: persisted.Schema['languagePrefs'], ··· 9 10 type ApiContext = { 10 11 setPrimaryLanguage: (code2: string) => void 11 12 setPostLanguage: (commaSeparatedLangCodes: string) => void 13 + setContentLanguage: (code2: string) => void 12 14 toggleContentLanguage: (code2: string) => void 13 15 togglePostLanguage: (code2: string) => void 14 16 savePostLanguageToHistory: () => void ··· 21 23 const apiContext = React.createContext<ApiContext>({ 22 24 setPrimaryLanguage: (_: string) => {}, 23 25 setPostLanguage: (_: string) => {}, 26 + setContentLanguage: (_: string) => {}, 24 27 toggleContentLanguage: (_: string) => {}, 25 28 togglePostLanguage: (_: string) => {}, 26 29 savePostLanguageToHistory: () => {}, ··· 52 55 }, 53 56 setPostLanguage(commaSeparatedLangCodes: string) { 54 57 setStateWrapped(s => ({...s, postLanguage: commaSeparatedLangCodes})) 58 + }, 59 + setContentLanguage(code2: string) { 60 + setStateWrapped(s => ({...s, contentLanguages: [code2]})) 55 61 }, 56 62 toggleContentLanguage(code2: string) { 57 63 setStateWrapped(s => {
+8
src/state/queries/post-feed.ts
··· 459 459 } 460 460 } 461 461 462 + export function resetPostsFeedQueries(queryClient: QueryClient, timeout = 0) { 463 + setTimeout(() => { 464 + queryClient.resetQueries({ 465 + predicate: query => query.queryKey[0] === RQKEY_ROOT, 466 + }) 467 + }, timeout) 468 + } 469 + 462 470 export function resetProfilePostsQueries( 463 471 queryClient: QueryClient, 464 472 did: string,
+7 -1
src/view/shell/NavSignupCard.tsx
··· 48 48 </Text> 49 49 </View> 50 50 51 - <View style={{flexDirection: 'row', paddingTop: 12, gap: 8}}> 51 + <View 52 + style={{ 53 + flexDirection: 'row', 54 + flexWrap: 'wrap', 55 + paddingTop: 12, 56 + gap: 8, 57 + }}> 52 58 <Button 53 59 onPress={showCreateAccount} 54 60 accessibilityHint={_(msg`Sign up`)}