Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at main 251 lines 7.8 kB view raw
1import {useEffect} from 'react' 2import Animated, { 3 Easing, 4 useAnimatedStyle, 5 useSharedValue, 6 withSequence, 7 withTiming, 8} from 'react-native-reanimated' 9import {msg} from '@lingui/core/macro' 10import {useLingui} from '@lingui/react' 11import {Trans} from '@lingui/react/macro' 12 13import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants' 14import {codeToLanguageName} from '#/locale/helpers' 15import { 16 toPostLanguages, 17 useLanguagePrefs, 18 useLanguagePrefsApi, 19} from '#/state/preferences/languages' 20import {atoms as a, useTheme} from '#/alf' 21import {Button, type ButtonProps} from '#/components/Button' 22import * as Dialog from '#/components/Dialog' 23import {LanguageSelectDialog} from '#/components/dialogs/LanguageSelectDialog' 24import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron' 25import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' 26import * as Menu from '#/components/Menu' 27import {Text} from '#/components/Typography' 28import {useAnalytics} from '#/analytics' 29 30export function PostLanguageSelect({ 31 currentLanguages: currentLanguagesProp, 32 onSelectLanguage, 33 nudgeAt = 0, 34}: { 35 currentLanguages?: string[] 36 onSelectLanguage?: (language: string) => void 37 /** 38 * Timestamp (ms) of the last honored language-detection nudge. Each 39 * time this changes, the button flashes a transient hint and fades. 40 * The parent rate-limits updates, so successive detector firings inside 41 * the cooldown won't re-flash. The initial `0` on mount is intentionally 42 * ignored. 43 */ 44 nudgeAt?: number 45}) { 46 const {_} = useLingui() 47 const langPrefs = useLanguagePrefs() 48 const setLangPrefs = useLanguagePrefsApi() 49 const languageDialogControl = Dialog.useDialogControl() 50 51 const dedupedHistory = Array.from( 52 new Set([...langPrefs.postLanguageHistory, langPrefs.postLanguage]), 53 ) 54 55 const currentLanguages = 56 currentLanguagesProp ?? toPostLanguages(langPrefs.postLanguage) 57 58 const onSelectLanguages = (languages: string[]) => { 59 let langsString = languages.join(',') 60 if (!langsString) { 61 langsString = langPrefs.primaryLanguage 62 } 63 setLangPrefs.setPostLanguage(langsString) 64 onSelectLanguage?.(langsString) 65 } 66 67 if ( 68 dedupedHistory.length === 1 && 69 dedupedHistory[0] === langPrefs.postLanguage 70 ) { 71 return ( 72 <> 73 <LanguageBtn onPress={languageDialogControl.open} nudgeAt={nudgeAt} /> 74 <LanguageSelectDialog 75 titleText={<Trans>Choose post languages</Trans>} 76 subtitleText={ 77 <Trans>Select up to 3 languages used in this post</Trans> 78 } 79 control={languageDialogControl} 80 currentLanguages={currentLanguages} 81 onSelectLanguages={onSelectLanguages} 82 maxLanguages={3} 83 /> 84 </> 85 ) 86 } 87 88 return ( 89 <> 90 <Menu.Root> 91 <Menu.Trigger label={_(msg`Select post language`)}> 92 {({props}) => ( 93 <LanguageBtn 94 currentLanguages={currentLanguages} 95 nudgeAt={nudgeAt} 96 {...props} 97 /> 98 )} 99 </Menu.Trigger> 100 <Menu.Outer> 101 <Menu.Group> 102 {dedupedHistory.map(historyItem => { 103 const langCodes = historyItem.split(',') 104 const langName = langCodes 105 .map(code => codeToLanguageName(code, langPrefs.appLanguage)) 106 .join(' + ') 107 return ( 108 <Menu.Item 109 key={historyItem} 110 label={_(msg`Select ${langName}`)} 111 onPress={() => { 112 setLangPrefs.setPostLanguage(historyItem) 113 onSelectLanguage?.(historyItem) 114 }}> 115 <Menu.ItemText>{langName}</Menu.ItemText> 116 <Menu.ItemRadio 117 selected={currentLanguages.includes(historyItem)} 118 /> 119 </Menu.Item> 120 ) 121 })} 122 </Menu.Group> 123 <Menu.Divider /> 124 <Menu.Item 125 label={_(msg`More languages...`)} 126 onPress={languageDialogControl.open}> 127 <Menu.ItemText> 128 <Trans>More languages...</Trans> 129 </Menu.ItemText> 130 <Menu.ItemIcon icon={ChevronRightIcon} /> 131 </Menu.Item> 132 </Menu.Outer> 133 </Menu.Root> 134 135 <LanguageSelectDialog 136 titleText={<Trans>Choose post languages</Trans>} 137 subtitleText={<Trans>Select up to 3 languages used in this post</Trans>} 138 control={languageDialogControl} 139 currentLanguages={currentLanguages} 140 onSelectLanguages={onSelectLanguages} 141 maxLanguages={3} 142 /> 143 </> 144 ) 145} 146 147const PULSE_FADE_IN_MS = 300 148const PULSE_FADE_OUT_MS = 500 149 150function LanguageBtn({ 151 currentLanguages: currentLanguagesProp, 152 nudgeAt = 0, 153 ...props 154}: Omit<ButtonProps, 'label' | 'children'> & { 155 currentLanguages?: string[] 156 nudgeAt?: number 157}) { 158 const t = useTheme() 159 const ax = useAnalytics() 160 const {_} = useLingui() 161 const langPrefs = useLanguagePrefs() 162 163 const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) 164 const currentLanguages = currentLanguagesProp ?? postLanguagesPref 165 166 /* 167 * Stays at 0 when idle; each nudge runs two pulses with a faster 168 * fade-in and slower fade-out, ease-in-out throughout. Reassigning 169 * `value` cancels any prior sequence, so rapid re-nudges cleanly 170 * restart. 171 */ 172 const nudgePulse = useSharedValue(0) 173 useEffect(() => { 174 if (nudgeAt === 0) return 175 const easing = Easing.inOut(Easing.quad) 176 const fadeIn = {duration: PULSE_FADE_IN_MS, easing} 177 const fadeOut = {duration: PULSE_FADE_OUT_MS, easing} 178 nudgePulse.value = withSequence( 179 withTiming(1, fadeIn), 180 withTiming(0, fadeOut), 181 withTiming(1, fadeIn), 182 withTiming(0, fadeOut), 183 ) 184 }, [nudgeAt, nudgePulse]) 185 const pulseStyle = useAnimatedStyle(() => ({ 186 opacity: nudgePulse.value, 187 })) 188 189 return ( 190 <Button 191 testID="selectLangBtn" 192 size="small" 193 hitSlop={LANG_DROPDOWN_HITSLOP} 194 label={_( 195 msg({ 196 message: `Post language selection`, 197 comment: `Accessibility label for button that opens dialog to choose post language settings`, 198 }), 199 )} 200 accessibilityHint={_(msg`Opens post language settings`)} 201 style={[a.mr_xs, a.overflow_hidden]} 202 {...props} 203 onPress={e => { 204 props.onPress?.(e) 205 ax.metric('composer:language:langSelectorPressed', { 206 wasNudged: nudgeAt > 0, 207 }) 208 }}> 209 {({pressed, hovered}) => { 210 const color = 211 pressed || hovered ? t.palette.primary_300 : t.palette.primary_500 212 return ( 213 <> 214 <Animated.View 215 pointerEvents="none" 216 style={[ 217 a.absolute, 218 { 219 top: 0, 220 right: 0, 221 bottom: 0, 222 left: 0, 223 backgroundColor: t.atoms.bg_contrast_50.backgroundColor, 224 }, 225 pulseStyle, 226 ]} 227 /> 228 {currentLanguages.length > 0 ? ( 229 <Text 230 style={[ 231 {color}, 232 a.font_semi_bold, 233 a.text_sm, 234 a.leading_snug, 235 {maxWidth: 100}, 236 ]} 237 numberOfLines={1} 238 maxFontSizeMultiplier={1.5}> 239 {currentLanguages 240 .map(lang => codeToLanguageName(lang, langPrefs.appLanguage)) 241 .join(', ')} 242 </Text> 243 ) : ( 244 <GlobeIcon size="xs" style={{color}} /> 245 )} 246 </> 247 ) 248 }} 249 </Button> 250 ) 251}