Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Language settings updates, new primary language setting (#1471)

* move content languages to screen

* add dropdown library, style primary lang select

* update settings button

* show selected langauges in button

* use primary language in translator link

* update copy

* lint

authored by

Eric Bailey and committed by
GitHub
8a5f9cd4 335061f7

+272 -14
+2
package.json
··· 44 44 "@react-native-community/blur": "^4.3.0", 45 45 "@react-native-community/datetimepicker": "7.2.0", 46 46 "@react-native-menu/menu": "^0.8.0", 47 + "@react-native-picker/picker": "2.4.10", 47 48 "@react-navigation/bottom-tabs": "^6.5.7", 48 49 "@react-navigation/drawer": "^6.6.2", 49 50 "@react-navigation/native": "^6.1.6", ··· 130 131 "react-native-ios-context-menu": "^1.15.3", 131 132 "react-native-linear-gradient": "^2.6.2", 132 133 "react-native-pager-view": "6.1.4", 134 + "react-native-picker-select": "^8.1.0", 133 135 "react-native-progress": "bluesky-social/react-native-progress", 134 136 "react-native-reanimated": "^3.4.2", 135 137 "react-native-root-siblings": "^4.1.1",
+6
src/Navigation.tsx
··· 46 46 import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' 47 47 import {NotFoundScreen} from './view/screens/NotFound' 48 48 import {SettingsScreen} from './view/screens/Settings' 49 + import {LanguageSettingsScreen} from './view/screens/LanguageSettings' 49 50 import {ProfileScreen} from './view/screens/Profile' 50 51 import {ProfileFollowersScreen} from './view/screens/ProfileFollowers' 51 52 import {ProfileFollowsScreen} from './view/screens/ProfileFollows' ··· 117 118 name="Settings" 118 119 component={SettingsScreen} 119 120 options={{title: title('Settings')}} 121 + /> 122 + <Stack.Screen 123 + name="LanguageSettings" 124 + component={LanguageSettingsScreen} 125 + options={{title: title('Language Settings')}} 120 126 /> 121 127 <Stack.Screen 122 128 name="Profile"
+1
src/lib/routes/types.ts
··· 10 10 ModerationMutedAccounts: undefined 11 11 ModerationBlockedAccounts: undefined 12 12 Settings: undefined 13 + LanguageSettings: undefined 13 14 Profile: {name: string; hideBackButton?: boolean} 14 15 ProfileFollowers: {name: string} 15 16 ProfileFollows: {name: string}
+2 -2
src/locale/helpers.ts
··· 79 79 return bcp47Match.basicFilter(lang, targetLangs).length > 0 80 80 } 81 81 82 - export function getTranslatorLink(text: string): string { 83 - return `https://translate.google.com/?sl=auto&text=${encodeURIComponent( 82 + export function getTranslatorLink(text: string, lang: string): string { 83 + return `https://translate.google.com/?sl=auto&tl=${lang}&text=${encodeURIComponent( 84 84 text, 85 85 )}` 86 86 }
+1
src/routes.ts
··· 6 6 Feeds: '/feeds', 7 7 Notifications: '/notifications', 8 8 Settings: '/settings', 9 + LanguageSettings: '/settings/language', 9 10 Moderation: '/moderation', 10 11 ModerationMuteLists: '/moderation/mute-lists', 11 12 ModerationMutedAccounts: '/moderation/muted-accounts',
+15
src/state/models/ui/preferences.ts
··· 44 44 45 45 export class PreferencesModel { 46 46 adultContentEnabled = false 47 + primaryLanguage: string = deviceLocales[0] || 'en' 47 48 contentLanguages: string[] = deviceLocales || [] 48 49 postLanguage: string = deviceLocales[0] || 'en' 49 50 postLanguageHistory: string[] = DEFAULT_LANG_CODES ··· 78 79 79 80 serialize() { 80 81 return { 82 + primaryLanguage: this.primaryLanguage, 81 83 contentLanguages: this.contentLanguages, 82 84 postLanguage: this.postLanguage, 83 85 postLanguageHistory: this.postLanguageHistory, ··· 105 107 */ 106 108 hydrate(v: unknown) { 107 109 if (isObj(v)) { 110 + if ( 111 + hasProp(v, 'primaryLanguage') && 112 + typeof v.primaryLanguage === 'string' 113 + ) { 114 + this.primaryLanguage = v.primaryLanguage 115 + } else { 116 + // default to the device languages 117 + this.primaryLanguage = deviceLocales[0] || 'en' 118 + } 108 119 // check if content languages in preferences exist, otherwise default to device languages 109 120 if ( 110 121 hasProp(v, 'contentLanguages') && ··· 540 551 541 552 toggleRequireAltTextEnabled() { 542 553 this.requireAltTextEnabled = !this.requireAltTextEnabled 554 + } 555 + 556 + setPrimaryLanguage(lang: string) { 557 + this.primaryLanguage = lang 543 558 } 544 559 545 560 getFeedTuners(
+4 -1
src/view/com/post-thread/PostThreadItem.tsx
··· 75 75 }, [item.post.uri, item.post.author]) 76 76 const repostsTitle = 'Reposts of this post' 77 77 78 - const translatorUrl = getTranslatorLink(record?.text || '') 78 + const translatorUrl = getTranslatorLink( 79 + record?.text || '', 80 + store.preferences.primaryLanguage, 81 + ) 79 82 const needsTranslation = useMemo( 80 83 () => 81 84 store.preferences.contentLanguages.length > 0 &&
+4 -1
src/view/com/post/Post.tsx
··· 115 115 replyAuthorDid = urip.hostname 116 116 } 117 117 118 - const translatorUrl = getTranslatorLink(record?.text || '') 118 + const translatorUrl = getTranslatorLink( 119 + record?.text || '', 120 + store.preferences.primaryLanguage, 121 + ) 119 122 120 123 const onPressReply = React.useCallback(() => { 121 124 store.shell.openComposer({
+4 -1
src/view/com/posts/FeedItem.tsx
··· 64 64 const urip = new AtUri(record.reply.parent?.uri || record.reply.root.uri) 65 65 return urip.hostname 66 66 }, [record?.reply]) 67 - const translatorUrl = getTranslatorLink(record?.text || '') 67 + const translatorUrl = getTranslatorLink( 68 + record?.text || '', 69 + store.preferences.primaryLanguage, 70 + ) 68 71 69 72 const onPressReply = React.useCallback(() => { 70 73 track('FeedItem:PostReply')
+2
src/view/index.ts
··· 97 97 import {faUsersSlash} from '@fortawesome/free-solid-svg-icons/faUsersSlash' 98 98 import {faX} from '@fortawesome/free-solid-svg-icons/faX' 99 99 import {faXmark} from '@fortawesome/free-solid-svg-icons/faXmark' 100 + import {faChevronDown} from '@fortawesome/free-solid-svg-icons/faChevronDown' 100 101 101 102 export function setup() { 102 103 library.add( ··· 197 198 faTrashCan, 198 199 faX, 199 200 faXmark, 201 + faChevronDown, 200 202 ) 201 203 }
+205
src/view/screens/LanguageSettings.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet, View} from 'react-native' 3 + import {observer} from 'mobx-react-lite' 4 + import {Text} from '../com/util/text/Text' 5 + import {useStores} from 'state/index' 6 + import {s} from 'lib/styles' 7 + import {usePalette} from 'lib/hooks/usePalette' 8 + import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 9 + import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' 10 + import {ViewHeader} from 'view/com/util/ViewHeader' 11 + import {CenteredView} from 'view/com/util/Views' 12 + import {Button} from 'view/com/util/forms/Button' 13 + import { 14 + FontAwesomeIcon, 15 + FontAwesomeIconStyle, 16 + } from '@fortawesome/react-native-fontawesome' 17 + import {useAnalytics} from 'lib/analytics/analytics' 18 + import {useFocusEffect} from '@react-navigation/native' 19 + import {LANGUAGES} from 'lib/../locale/languages' 20 + import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select' 21 + 22 + type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'> 23 + 24 + export const LanguageSettingsScreen = observer(function LanguageSettingsImpl( 25 + _: Props, 26 + ) { 27 + const pal = usePalette('default') 28 + const store = useStores() 29 + const {isTabletOrDesktop} = useWebMediaQueries() 30 + const {screen, track} = useAnalytics() 31 + 32 + useFocusEffect( 33 + React.useCallback(() => { 34 + screen('Settings') 35 + store.shell.setMinimalShellMode(false) 36 + }, [screen, store]), 37 + ) 38 + 39 + const onPressContentLanguages = React.useCallback(() => { 40 + track('Settings:ContentlanguagesButtonClicked') 41 + store.shell.openModal({name: 'content-languages-settings'}) 42 + }, [track, store]) 43 + 44 + const onChangePrimaryLanguage = React.useCallback( 45 + (value: Parameters<PickerSelectProps['onValueChange']>[0]) => { 46 + store.preferences.setPrimaryLanguage(value) 47 + }, 48 + [store.preferences], 49 + ) 50 + 51 + const myLanguages = React.useMemo(() => { 52 + return ( 53 + store.preferences.contentLanguages 54 + .map(lang => LANGUAGES.find(l => l.code2 === lang)) 55 + .filter(Boolean) 56 + // @ts-ignore 57 + .map(l => l.name) 58 + .join(', ') 59 + ) 60 + }, [store.preferences.contentLanguages]) 61 + 62 + return ( 63 + <CenteredView 64 + style={[ 65 + pal.view, 66 + pal.border, 67 + styles.container, 68 + isTabletOrDesktop && styles.desktopContainer, 69 + ]}> 70 + <ViewHeader title="Language Settings" showOnDesktop /> 71 + 72 + <View style={{paddingTop: 20, paddingHorizontal: 20}}> 73 + <View style={{paddingBottom: 20}}> 74 + <Text type="title-sm" style={[pal.text, s.pb5]}> 75 + Primary Language 76 + </Text> 77 + <Text style={[pal.text, s.pb10]}> 78 + Select your preferred language for translations in your feed. 79 + </Text> 80 + 81 + <View style={{position: 'relative'}}> 82 + <RNPickerSelect 83 + value={store.preferences.primaryLanguage} 84 + onValueChange={onChangePrimaryLanguage} 85 + items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({ 86 + label: l.name, 87 + value: l.code2, 88 + key: l.code2 + l.code3, 89 + }))} 90 + style={{ 91 + inputAndroid: { 92 + backgroundColor: pal.viewLight.backgroundColor, 93 + color: pal.text.color, 94 + fontSize: 14, 95 + letterSpacing: 0.5, 96 + fontWeight: '500', 97 + paddingHorizontal: 14, 98 + paddingVertical: 8, 99 + borderRadius: 24, 100 + }, 101 + inputIOS: { 102 + backgroundColor: pal.viewLight.backgroundColor, 103 + color: pal.text.color, 104 + fontSize: 14, 105 + letterSpacing: 0.5, 106 + fontWeight: '500', 107 + paddingHorizontal: 14, 108 + paddingVertical: 8, 109 + borderRadius: 24, 110 + }, 111 + inputWeb: { 112 + // @ts-ignore web only 113 + cursor: 'pointer', 114 + '-moz-appearance': 'none', 115 + '-webkit-appearance': 'none', 116 + appearance: 'none', 117 + outline: 0, 118 + borderWidth: 0, 119 + backgroundColor: pal.viewLight.backgroundColor, 120 + color: pal.text.color, 121 + fontSize: 14, 122 + letterSpacing: 0.5, 123 + fontWeight: '500', 124 + paddingHorizontal: 14, 125 + paddingVertical: 8, 126 + borderRadius: 24, 127 + }, 128 + }} 129 + /> 130 + 131 + <View 132 + style={{ 133 + position: 'absolute', 134 + top: 1, 135 + right: 1, 136 + bottom: 1, 137 + width: 40, 138 + backgroundColor: pal.viewLight.backgroundColor, 139 + borderRadius: 24, 140 + pointerEvents: 'none', 141 + alignItems: 'center', 142 + justifyContent: 'center', 143 + }}> 144 + <FontAwesomeIcon 145 + icon="chevron-down" 146 + style={pal.text as FontAwesomeIconStyle} 147 + /> 148 + </View> 149 + </View> 150 + </View> 151 + 152 + <View 153 + style={{ 154 + height: 1, 155 + backgroundColor: pal.border.borderColor, 156 + marginBottom: 20, 157 + }} 158 + /> 159 + 160 + <View style={{paddingBottom: 20}}> 161 + <Text type="title-sm" style={[pal.text, s.pb5]}> 162 + Content Languages 163 + </Text> 164 + <Text style={[pal.text, s.pb10]}> 165 + Select which languages you want your subscribed feeds to include. If 166 + none are selected, all languages will be shown. 167 + </Text> 168 + 169 + <Button 170 + type="default" 171 + onPress={onPressContentLanguages} 172 + style={styles.button}> 173 + <FontAwesomeIcon 174 + icon={myLanguages.length ? 'check' : 'plus'} 175 + style={pal.text as FontAwesomeIconStyle} 176 + /> 177 + <Text 178 + type="button" 179 + style={[pal.text, {flexShrink: 1, overflow: 'hidden'}]} 180 + numberOfLines={1}> 181 + {myLanguages.length ? myLanguages : 'Select languages'} 182 + </Text> 183 + </Button> 184 + </View> 185 + </View> 186 + </CenteredView> 187 + ) 188 + }) 189 + 190 + const styles = StyleSheet.create({ 191 + container: { 192 + flex: 1, 193 + paddingBottom: 90, 194 + }, 195 + desktopContainer: { 196 + borderLeftWidth: 1, 197 + borderRightWidth: 1, 198 + paddingBottom: 40, 199 + }, 200 + button: { 201 + flexDirection: 'row', 202 + alignItems: 'center', 203 + gap: 8, 204 + }, 205 + })
+8 -9
src/view/screens/Settings.tsx
··· 145 145 store.shell.openModal({name: 'invite-codes'}) 146 146 }, [track, store]) 147 147 148 - const onPressContentLanguages = React.useCallback(() => { 149 - track('Settings:ContentlanguagesButtonClicked') 150 - store.shell.openModal({name: 'content-languages-settings'}) 151 - }, [track, store]) 148 + const onPressLanguageSettings = React.useCallback(() => { 149 + navigation.navigate('LanguageSettings') 150 + }, [navigation]) 152 151 153 152 const onPressSignout = React.useCallback(() => { 154 153 track('Settings:SignOutButtonClicked') ··· 456 455 </Text> 457 456 </TouchableOpacity> 458 457 <TouchableOpacity 459 - testID="contentLanguagesBtn" 458 + testID="languageSettingsBtn" 460 459 style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]} 461 - onPress={isSwitching ? undefined : onPressContentLanguages} 460 + onPress={isSwitching ? undefined : onPressLanguageSettings} 462 461 accessibilityRole="button" 463 - accessibilityHint="Content languages" 464 - accessibilityLabel="Opens configurable content language settings"> 462 + accessibilityHint="Language settings" 463 + accessibilityLabel="Opens configurable language settings"> 465 464 <View style={[styles.iconContainer, pal.btn]}> 466 465 <FontAwesomeIcon 467 466 icon="language" ··· 469 468 /> 470 469 </View> 471 470 <Text type="lg" style={pal.text}> 472 - Content languages 471 + Languages 473 472 </Text> 474 473 </TouchableOpacity> 475 474 <TouchableOpacity
+18
yarn.lock
··· 3374 3374 resolved "https://registry.yarnpkg.com/@react-native-menu/menu/-/menu-0.8.0.tgz#dbf227c2081e5ffd3d2073ee68ecc84cf8639727" 3375 3375 integrity sha512-kxiT6ySZsDbBvNWovrKVAfs4AQvAytKIf0f8KQLkVO6eNYMUmONBQPzi6onTTbVujXtZHambo7qr/PcedaR8Tg== 3376 3376 3377 + "@react-native-picker/picker@2.4.10": 3378 + version "2.4.10" 3379 + resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-2.4.10.tgz#339c7bfc6e1d9a5e934122eaaa7767dc1c5fb725" 3380 + integrity sha512-EvAlHmPEPOwvbP6Pjg/gtDV3XJzIjIxr10fXFNlX5r9HeHw582G1Zt2o8FLyB718nOttgj8HYUTGxvhu4N65sQ== 3381 + 3382 + "@react-native-picker/picker@^1.8.3": 3383 + version "1.16.8" 3384 + resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-1.16.8.tgz#2126ca54d4a5a3e9ea5e3f39ad1e6643f8e4b3d4" 3385 + integrity sha512-pacdQDX6V6EmjF+HoiIh6u++qx4mTK0WnhgUHRc01B+Qt5eoeUwseBqmqfTSXTx/aHDEd6PiIw7UGvKgFoqgFQ== 3386 + 3377 3387 "@react-native/assets-registry@^0.72.0": 3378 3388 version "0.72.0" 3379 3389 resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.72.0.tgz#c82a76a1d86ec0c3907be76f7faf97a32bbed05d" ··· 15768 15778 version "6.1.4" 15769 15779 resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.1.4.tgz#3a63ebd1b72f81991157ea552bb9c887e529bc8c" 15770 15780 integrity sha512-fmTwgGwPxGCBusKAq7gHzm+s1Yp0qh5rKPoQszaCuxrb+76KgK4Qe82jJNPUp2xTZOKSw+FbJU2QahF8ncTl+w== 15781 + 15782 + react-native-picker-select@^8.1.0: 15783 + version "8.1.0" 15784 + resolved "https://registry.yarnpkg.com/react-native-picker-select/-/react-native-picker-select-8.1.0.tgz#667a5442f783f4bcfd3f65880c6926155fd2c39c" 15785 + integrity sha512-iLsLv2OEWpXnQMDYJS6du5Cl1HTHy887n60Yp5OOiMny0TDB9w5CfxTUYWtpsvJJrUa/Yrv+1NMQiJy7IA4ETw== 15786 + dependencies: 15787 + "@react-native-picker/picker" "^1.8.3" 15788 + lodash.isequal "^4.5.0" 15771 15789 15772 15790 react-native-progress@bluesky-social/react-native-progress: 15773 15791 version "5.0.0"