Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 215 lines 6.0 kB view raw
1import {useEffect, useState} from 'react' 2import {Text as RNText, View} from 'react-native' 3import {parseLanguage} from '@atproto/api' 4import {msg} from '@lingui/core/macro' 5import {useLingui} from '@lingui/react' 6import {Trans} from '@lingui/react/macro' 7import lande from 'lande' 8 9import {code3ToCode2Strict, codeToLanguageName} from '#/locale/helpers' 10import {useLanguagePrefs} from '#/state/preferences/languages' 11import {atoms as a, useTheme} from '#/alf' 12import {Button, ButtonText} from '#/components/Button' 13import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 14import {Text} from '#/components/Typography' 15 16// fallbacks for safari 17const onIdle = 18 globalThis.requestIdleCallback || ((cb: () => void) => setTimeout(cb, 1)) 19const cancelIdle = globalThis.cancelIdleCallback || clearTimeout 20 21export function SuggestedLanguage({ 22 text, 23 replyToLanguages: replyToLanguagesProp, 24 currentLanguages, 25 onAcceptSuggestedLanguage, 26}: { 27 text: string 28 /** 29 * All languages associated with the post being replied to. 30 */ 31 replyToLanguages: string[] 32 /** 33 * All languages currently selected for the post being composed. 34 */ 35 currentLanguages: string[] 36 /** 37 * Called when the user accepts a suggested language. We only pass a single 38 * language here. If the post being replied to has multiple languages, we 39 * only suggest the first one. 40 */ 41 onAcceptSuggestedLanguage: (language: string | null) => void 42}) { 43 const langPrefs = useLanguagePrefs() 44 const replyToLanguages = replyToLanguagesProp 45 .map(lang => cleanUpLanguage(lang)) 46 .filter(Boolean) as string[] 47 const [hasInteracted, setHasInteracted] = useState(false) 48 const [suggestedLanguage, setSuggestedLanguage] = useState< 49 string | undefined 50 >(undefined) 51 52 useEffect(() => { 53 if (text.length > 0 && !hasInteracted) { 54 setHasInteracted(true) 55 } 56 }, [text, hasInteracted]) 57 58 useEffect(() => { 59 const textTrimmed = text.trim() 60 61 // Don't run the language model on small posts, the results are likely 62 // to be inaccurate anyway. 63 if (textTrimmed.length < 40) { 64 setSuggestedLanguage(undefined) 65 return 66 } 67 68 const idle = onIdle(() => { 69 setSuggestedLanguage(guessLanguage(textTrimmed)) 70 }) 71 72 return () => cancelIdle(idle) 73 }, [text]) 74 75 /* 76 * We've detected a language, and the user hasn't already selected it. 77 */ 78 const hasLanguageSuggestion = 79 suggestedLanguage && !currentLanguages.includes(suggestedLanguage) 80 /* 81 * We have not detected a different language, and the user is not already 82 * using or has not already selected one of the languages of the post they 83 * are replying to. 84 */ 85 const hasSuggestedReplyLanguage = 86 !hasInteracted && 87 !suggestedLanguage && 88 replyToLanguages.length && 89 !replyToLanguages.some(l => currentLanguages.includes(l)) 90 91 if (hasLanguageSuggestion) { 92 const suggestedLanguageName = codeToLanguageName( 93 suggestedLanguage, 94 langPrefs.appLanguage, 95 ) 96 97 return ( 98 <LanguageSuggestionButton 99 label={ 100 <RNText> 101 <Trans> 102 Are you writing in{' '} 103 <Text style={[a.font_bold]}>{suggestedLanguageName}</Text>? 104 </Trans> 105 </RNText> 106 } 107 value={suggestedLanguage} 108 onAccept={onAcceptSuggestedLanguage} 109 /> 110 ) 111 } else if (hasSuggestedReplyLanguage) { 112 const suggestedLanguageName = codeToLanguageName( 113 replyToLanguages[0], 114 langPrefs.appLanguage, 115 ) 116 117 return ( 118 <LanguageSuggestionButton 119 label={ 120 <RNText> 121 <Trans> 122 The post you're replying to was marked as being written in{' '} 123 {suggestedLanguageName} by its author. Would you like to reply in{' '} 124 <Text style={[a.font_bold]}>{suggestedLanguageName}</Text>? 125 </Trans> 126 </RNText> 127 } 128 value={replyToLanguages[0]} 129 onAccept={onAcceptSuggestedLanguage} 130 /> 131 ) 132 } else { 133 return null 134 } 135} 136 137function LanguageSuggestionButton({ 138 label, 139 value, 140 onAccept, 141}: { 142 label: React.ReactNode 143 value: string 144 onAccept: (language: string | null) => void 145}) { 146 const t = useTheme() 147 const {_} = useLingui() 148 149 return ( 150 <View style={[a.px_lg, a.py_sm]}> 151 <View 152 style={[ 153 a.gap_md, 154 a.border, 155 a.flex_row, 156 a.align_center, 157 a.rounded_sm, 158 a.p_md, 159 a.pl_lg, 160 t.atoms.bg, 161 t.atoms.border_contrast_low, 162 ]}> 163 <EarthIcon /> 164 <View style={[a.flex_1]}> 165 <Text 166 style={[ 167 a.leading_snug, 168 { 169 maxWidth: 400, 170 }, 171 ]}> 172 {label} 173 </Text> 174 </View> 175 176 <Button 177 size="small" 178 color="secondary" 179 onPress={() => onAccept(value)} 180 label={_(msg`Accept this language suggestion`)}> 181 <ButtonText> 182 <Trans>Yes</Trans> 183 </ButtonText> 184 </Button> 185 </View> 186 </View> 187 ) 188} 189 190/** 191 * This function is using the lande language model to attempt to detect the language 192 * We want to only make suggestions when we feel a high degree of certainty 193 * The magic numbers are based on debugging sessions against some test strings 194 */ 195function guessLanguage(text: string): string | undefined { 196 const scores = lande(text).filter(([_lang, value]) => value >= 0.0002) 197 // if the model has multiple items with a score higher than 0.0002, it isn't certain enough 198 if (scores.length !== 1) { 199 return undefined 200 } 201 const [lang, value] = scores[0] 202 // if the model doesn't give a score of 0.97 or above, it isn't certain enough 203 if (value < 0.97) { 204 return undefined 205 } 206 return code3ToCode2Strict(lang) 207} 208 209function cleanUpLanguage(text: string | undefined): string | undefined { 210 if (!text) { 211 return undefined 212 } 213 214 return parseLanguage(text)?.language 215}