Bluesky app fork with some witchin' additions ๐Ÿ’ซ

SUPER AMAZING (translation) SETTINGS & README ADDITION

Add Naver Papago and LibreTranslate (including Instance selection!) Post Translation Engines/Providers
Update README to include new features and TODOs
Cleared up and slightly improved the Experiments settings page

xan.lol cbab49f0 9e44a1b3

verified
+226 -36
+7 -15
README.md
··· 21 21 - Embed player works with [stream.place](https://stream.place/) links! 22 22 - Open skeets in PDSls and original pages of bridged posts 23 23 - You can redraft skeets 24 - - Better defaults (alt text required ๐Ÿ˜‰) 24 + - Better defaults (alt text required ๐Ÿ˜‰ autoplay off ๐Ÿซจ) 25 25 - More unique repost icons 26 26 - Can download videos 27 27 - 'Mutuals' in place of 'Following' when relevant ··· 50 50 - Toggle similar account recommendations 51 51 - Toggle to make all user avatars square (like labelers) 52 52 - Toggle for more square-ish UI (still slightly rounded) 53 + - Toggle to remove the composer prompt at the top of the Following & Discover feeds 54 + - Change post translation provider (between Google, Kagi, Papago, and LibreTranslate) 53 55 54 56 #### Metrics 55 57 ··· 64 66 - following 65 67 - & who someone's followed by 66 68 67 - #### Gates 68 - 69 - - Toggle for an alternate share icon 70 - - Toggle to show feed context for debugging 71 - - Toggle to hide the 'show latest' button 72 - - Toggle to make reply button open thread from feeds 73 - - More may be available in developer mode? Often less ๐Ÿคท 74 - - (Accessible by holding the version in the About settings screen) 75 - 76 69 ## Upcoming or wishful features 77 70 78 71 - Better OpenGraph support for sharing profiles & skeets (including videos & fixing quotes) ··· 83 76 ### TODO: Xan 84 77 85 78 - [ ] Setup App Linking for Android (.well-known w/ app package fingerprint) 86 - - [ ] Automatic PDS detection like other social-app forks (fallback/email addresses to use witchsky.social) 79 + - [ ] Fallback/email addresses to use witchsky.social in Automatic PDS detection 87 80 - [ ] Change followed accounts [on onboarding](https://github.com/blacksky-algorithms/blacksky.community/commit/e36ee43efb4999f070860d7f70122e45b28c1e2b) 88 - - [ ] Join date & switch accounts from composer from a fork like [deer.aylac.top](https://github.com/ayla6/deer-social-test) 81 + - [ ] Join date & switch accounts in composer from a fork like [deer.aylac.top](https://github.com/ayla6/deer-social-test) 89 82 - [ ] Visual replies indicator like the [Firmament userstyle](https://witchsky.app/profile/did:plc:jwhxcrf5uvl3vyw7nurecgt5/post/3m4rr3vzmak2a) (and likes?) 90 - - [ ] Additional translation service providers + setting (Deepl, Kagi) 91 83 - [ ] Put DeerSettings into separate subpages 92 84 - [ ] After subpages for options, add [Outlinks page](https://witchsky.app/profile/did:plc:q7suwaz53ztc4mbiqyygbn43/post/3m5zjhhshic2g) & 93 85 - [ ] ShareMenuItems.tsx, ShareMenuItems.web.tsx 94 86 - [ ] For profile meatball button, Open profile in PDSls & Open bridged OG fedi account page 95 87 - [ ] ProfileMenu.tsx 96 - - [ ] Witchsky PDS and .social site (list good songs containing 'bitch' in their titles) 88 + - [ ] Witchsky PDS and .social site (list good songs containing 'bitch' in their titles for related site) 97 89 98 90 ### Even more wishful or far off 99 91 ··· 101 93 - [ ] Submit releases to the Google Play Store and iOS App Store 102 94 - [ ] Move from [Cloudflare Pages](https://pages.cloudflare.com/) to [wisp.place](https://wisp.place/) (needs serverless for embeds) 103 95 - [ ] Toggle between handle and DID in share links 104 - - [ ] Move TOS and privacy policy to Jollywhoppers website? 96 + - [ ] Move TOS and privacy policy to Jollywhoppers website 105 97 - [ ] Ignore `!no-unauthenticated` labels 106 98 - [ ] Material 3 Expressive theming on Android (Liquid **ass on iOS) 107 99
+10 -1
src/lib/hooks/useTranslate.ts
··· 1 1 import {useCallback} from 'react' 2 2 import * as IntentLauncher from 'expo-intent-launcher' 3 3 4 - import {getTranslatorLink, getTranslatorLinkKagi} from '#/locale/helpers' 4 + import { 5 + getTranslatorLink, 6 + getTranslatorLinkKagi, 7 + getTranslatorLinkLibreTranslate, 8 + getTranslatorLinkPapago, 9 + } from '#/locale/helpers' 5 10 import {useTranslationServicePreference} from '#/state/preferences/translation-service-preference' 6 11 import {IS_ANDROID} from '#/env' 7 12 import {useOpenLink} from './useOpenLink' ··· 19 24 // it is a mystery https://www.youtube.com/watch?v=fq3abPnEEGE 20 25 if (translationServicePreference == 'kagi') { 21 26 translateUrl = getTranslatorLinkKagi(text, language) 27 + } else if (translationServicePreference == 'papago') { 28 + translateUrl = getTranslatorLinkPapago(text, language) 29 + } else if (translationServicePreference == 'libreTranslate') { 30 + translateUrl = getTranslatorLinkLibreTranslate(text, language) 22 31 } else { 23 32 translateUrl = getTranslatorLink(text, language) 24 33 }
+17
src/locale/helpers.ts
··· 3 3 import lande from 'lande' 4 4 5 5 import {hasProp} from '#/lib/type-guards' 6 + import * as persisted from '#/state/persisted' 6 7 import { 7 8 AppLanguage, 8 9 type Language, ··· 138 139 return `https://translate.kagi.com/?from=auto&to=${lang}&text=${encodeURIComponent( 139 140 text, 140 141 )}` 142 + } 143 + 144 + export function getTranslatorLinkPapago(text: string, lang: string): string { 145 + return `https://papago.naver.com/?sk=auto&tk=${lang}&st=${encodeURIComponent( 146 + text, 147 + )}` 148 + } 149 + 150 + export function getTranslatorLinkLibreTranslate( 151 + text: string, 152 + lang: string, 153 + ): string { 154 + const instance = 155 + persisted.get('libreTranslateInstance') ?? 156 + persisted.defaults.libreTranslateInstance! 157 + return `${instance}?source=auto&target=${lang}&q=${encodeURIComponent(text)}` 141 158 } 142 159 143 160 /**
+141 -18
src/screens/Settings/DeerSettings.tsx
··· 108 108 useShowLinkInHandle, 109 109 } from '#/state/preferences/show-link-in-handle.tsx' 110 110 import { 111 + useLibreTranslateInstance, 112 + useSetLibreTranslateInstance, 111 113 useSetTranslationServicePreference, 112 114 useTranslationServicePreference, 113 115 } from '#/state/preferences/translation-service-preference' ··· 128 130 import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 129 131 import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 130 132 import * as Layout from '#/components/Layout' 133 + import {InlineLinkText} from '#/components/Link' 131 134 import {Text} from '#/components/Typography' 132 135 import {IS_WEB} from '#/env' 133 136 import {SearchProfileCard} from '../Search/components/SearchProfileCard' ··· 142 145 const pal = usePalette('default') 143 146 const {_} = useLingui() 144 147 145 - const [url, setUrl] = useState('') 148 + const constellationInstance = useConstellationInstance() 149 + const [url, setUrl] = useState(constellationInstance ?? '') 146 150 const setConstellationInstance = useSetConstellationInstance() 147 151 148 152 const submit = () => { ··· 162 166 <Dialog.Outer 163 167 control={control} 164 168 nativeOptions={{preventExpansion: true}} 165 - onClose={() => setUrl('')}> 169 + onClose={() => setUrl(constellationInstance ?? '')}> 166 170 <Dialog.Handle /> 167 171 <Dialog.ScrollableInner label={_(msg`Constellations instance URL`)}> 168 172 <View style={[a.gap_sm, a.pb_lg]}> ··· 185 189 accessibilityHint={_( 186 190 msg`Input the url of the constellations instance to use`, 187 191 )} 192 + defaultValue={constellationInstance} 193 + /> 194 + 195 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 196 + <Button 197 + label={_(msg`Save`)} 198 + size="large" 199 + onPress={submit} 200 + variant="solid" 201 + color="primary" 202 + disabled={shouldDisable()}> 203 + <ButtonText> 204 + <Trans>Save</Trans> 205 + </ButtonText> 206 + </Button> 207 + </View> 208 + </View> 209 + 210 + <Dialog.Close /> 211 + </Dialog.ScrollableInner> 212 + </Dialog.Outer> 213 + ) 214 + } 215 + 216 + function LibreTranslateInstanceDialog({ 217 + control, 218 + }: { 219 + control: Dialog.DialogControlProps 220 + }) { 221 + const pal = usePalette('default') 222 + const {_} = useLingui() 223 + 224 + const libreTranslateInstance = useLibreTranslateInstance() 225 + const [url, setUrl] = useState(libreTranslateInstance ?? '') 226 + const setLibreTranslateInstance = useSetLibreTranslateInstance() 227 + 228 + const submit = () => { 229 + setLibreTranslateInstance(url) 230 + control.close() 231 + } 232 + 233 + const shouldDisable = () => { 234 + try { 235 + return !new URL(url).hostname.includes('.') 236 + } catch (e) { 237 + return true 238 + } 239 + } 240 + 241 + return ( 242 + <Dialog.Outer 243 + control={control} 244 + nativeOptions={{preventExpansion: true}} 245 + onClose={() => setUrl(libreTranslateInstance ?? '')}> 246 + <Dialog.Handle /> 247 + <Dialog.ScrollableInner label={_(msg`LibreTranslate instance URL`)}> 248 + <View style={[a.gap_sm, a.pb_lg]}> 249 + <Text style={[a.text_2xl, a.font_bold]}> 250 + <Trans>LibreTranslate instance URL</Trans> 251 + </Text> 252 + </View> 253 + 254 + <View style={a.gap_lg}> 255 + <Dialog.Input 256 + label="Text input field" 257 + autoFocus 258 + style={[styles.textInput, pal.border, pal.text]} 259 + onChangeText={value => { 260 + setUrl(value) 261 + }} 262 + placeholder={persisted.defaults.libreTranslateInstance} 263 + placeholderTextColor={pal.colors.textLight} 264 + onSubmitEditing={submit} 265 + accessibilityHint={_( 266 + msg`Input the url of the LibreTranslate instance to use`, 267 + )} 268 + defaultValue={libreTranslateInstance} 188 269 /> 189 270 190 271 <View style={IS_WEB && [a.flex_row, a.justify_end]}> ··· 342 423 const translationServicePreference = useTranslationServicePreference() 343 424 const setTranslationServicePreference = useSetTranslationServicePreference() 344 425 426 + const setLibreTranslateInstanceControl = Dialog.useDialogControl() 427 + 345 428 return ( 346 429 <Layout.Screen> 347 430 <Layout.Header.Outer> ··· 480 563 <Trans> 481 564 Constellation is used to supplement AppView responses for custom 482 565 verifications and nuclear block bypass, via backlinks. Current 483 - instance: {constellationInstance} 566 + instance: 567 + <InlineLinkText 568 + to={constellationInstance} 569 + label={constellationInstance}> 570 + {constellationInstance} 571 + </InlineLinkText> 484 572 </Trans> 485 573 </Admonition> 486 574 </SettingsList.Item> 575 + 576 + <SettingsList.Divider /> 487 577 488 578 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 489 579 <SettingsList.ItemIcon icon={PaintRollerIcon} /> ··· 643 733 </Admonition> 644 734 </SettingsList.Group> 645 735 736 + <SettingsList.Divider /> 737 + 646 738 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 647 739 <SettingsList.ItemIcon icon={EarthIcon} /> 648 740 <SettingsList.ItemText> 649 - <Trans>Translation Engine</Trans> 741 + <Trans>Post Translation Engine</Trans> 650 742 </SettingsList.ItemText> 651 743 652 - <Admonition type="info" style={[a.flex_1]}> 653 - <Trans>Choose the engine to use when translating posts.</Trans> 654 - </Admonition> 655 - 656 744 <Toggle.Item 657 745 name="service_google" 658 746 label={_(msg`Use Google Translate`)} ··· 676 764 </Toggle.LabelText> 677 765 <Toggle.Radio /> 678 766 </Toggle.Item> 767 + 768 + <Toggle.Item 769 + name="service_papago" 770 + label={_(msg`Use Naver Papago`)} 771 + value={translationServicePreference === 'papago'} 772 + onChange={() => setTranslationServicePreference('papago')} 773 + style={[a.w_full]}> 774 + <Toggle.LabelText style={[a.flex_1]}> 775 + <Trans>Use Naver Papago</Trans> 776 + </Toggle.LabelText> 777 + <Toggle.Radio /> 778 + </Toggle.Item> 779 + 780 + <Toggle.Item 781 + name="service_libreTranslate" 782 + label={_(msg`Use LibreTranslate`)} 783 + value={translationServicePreference === 'libreTranslate'} 784 + onChange={() => setTranslationServicePreference('libreTranslate')} 785 + style={[a.w_full]}> 786 + <Toggle.LabelText style={[a.flex_1]}> 787 + <Trans>Use LibreTranslate</Trans> 788 + </Toggle.LabelText> 789 + <Toggle.Radio /> 790 + </Toggle.Item> 679 791 </SettingsList.Group> 680 792 793 + {translationServicePreference === 'libreTranslate' && ( 794 + <SettingsList.Item> 795 + <SettingsList.ItemIcon icon={EarthIcon} /> 796 + <SettingsList.ItemText> 797 + <Trans>{`LibreTranslate Instance`}</Trans> 798 + </SettingsList.ItemText> 799 + <SettingsList.BadgeButton 800 + label={_(msg`Change`)} 801 + onPress={() => setLibreTranslateInstanceControl.open()} 802 + /> 803 + </SettingsList.Item> 804 + )} 805 + 806 + <SettingsList.Divider /> 807 + 681 808 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 682 809 <SettingsList.ItemIcon icon={VisibilityIcon} /> 683 810 <SettingsList.ItemText> ··· 698 825 699 826 <Toggle.Item 700 827 name="disable_reposts_metrics" 701 - label={_(msg`Disable reskeets metrics`)} 828 + label={_(msg`Disable reskeet metrics`)} 702 829 value={disableRepostsMetrics} 703 830 onChange={value => setDisableRepostsMetrics(value)} 704 831 style={[a.w_full]}> 705 832 <Toggle.LabelText style={[a.flex_1]}> 706 - <Trans>Disable reskeets metrics</Trans> 833 + <Trans>Disable reskeet metrics</Trans> 707 834 </Toggle.LabelText> 708 835 <Toggle.Platform /> 709 836 </Toggle.Item> ··· 793 920 </Toggle.Item> 794 921 </SettingsList.Group> 795 922 796 - <SettingsList.Item> 797 - <Admonition type="warning" style={[a.flex_1]}> 798 - <Trans> 799 - These settings might summon nasal demons! Restart the app after 800 - changing if anything breaks. 801 - </Trans> 802 - </Admonition> 803 - </SettingsList.Item> 923 + <SettingsList.Divider /> 804 924 805 925 <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 806 926 <SettingsList.ItemIcon icon={RaisingHandIcon} /> ··· 847 967 </Layout.Content> 848 968 <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 849 969 <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 970 + <LibreTranslateInstanceDialog 971 + control={setLibreTranslateInstanceControl} 972 + /> 850 973 </Layout.Screen> 851 974 ) 852 975 }
+8 -1
src/state/persisted/schema.ts
··· 172 172 173 173 showExternalShareButtons: z.boolean().optional(), 174 174 175 - translationServicePreference: z.enum(['google', 'kagi']), 175 + translationServicePreference: z.enum([ 176 + 'google', 177 + 'kagi', 178 + 'papago', 179 + 'libreTranslate', 180 + ]), 181 + libreTranslateInstance: z.string().optional(), 176 182 177 183 /** @deprecated */ 178 184 mutedThreads: z.array(z.string()), ··· 284 290 hideUnreplyablePosts: false, 285 291 showExternalShareButtons: false, 286 292 translationServicePreference: 'google', 293 + libreTranslateInstance: 'https://libretranslate.com/', 287 294 } 288 295 289 296 export function tryParse(rawData: string): Schema | undefined {
+43 -1
src/state/preferences/translation-service-preference.tsx
··· 4 4 5 5 type StateContext = persisted.Schema['translationServicePreference'] 6 6 type SetContext = (v: persisted.Schema['translationServicePreference']) => void 7 + type InstanceStateContext = persisted.Schema['libreTranslateInstance'] 8 + type SetInstanceContext = ( 9 + v: persisted.Schema['libreTranslateInstance'], 10 + ) => void 7 11 8 12 const stateContext = React.createContext<StateContext>( 9 13 persisted.defaults.translationServicePreference, ··· 11 15 const setContext = React.createContext<SetContext>( 12 16 (_: persisted.Schema['translationServicePreference']) => {}, 13 17 ) 18 + const instanceStateContext = React.createContext<InstanceStateContext>( 19 + persisted.defaults.libreTranslateInstance, 20 + ) 21 + const setInstanceContext = React.createContext<SetInstanceContext>( 22 + (_: persisted.Schema['libreTranslateInstance']) => {}, 23 + ) 14 24 15 25 export function Provider({children}: React.PropsWithChildren<{}>) { 16 26 const [state, setState] = React.useState( 17 27 persisted.get('translationServicePreference'), 18 28 ) 29 + const [instanceState, setInstanceState] = React.useState( 30 + persisted.get('libreTranslateInstance'), 31 + ) 19 32 20 33 const setStateWrapped = React.useCallback( 21 34 ( ··· 30 43 [setState], 31 44 ) 32 45 46 + const setInstanceStateWrapped = React.useCallback( 47 + (libreTranslateInstance: persisted.Schema['libreTranslateInstance']) => { 48 + setInstanceState(libreTranslateInstance) 49 + persisted.write('libreTranslateInstance', libreTranslateInstance) 50 + }, 51 + [setInstanceState], 52 + ) 53 + 33 54 React.useEffect(() => { 34 55 return persisted.onUpdate( 35 56 'translationServicePreference', ··· 39 60 ) 40 61 }, [setStateWrapped]) 41 62 63 + React.useEffect(() => { 64 + return persisted.onUpdate('libreTranslateInstance', nextInstance => { 65 + setInstanceState(nextInstance) 66 + }) 67 + }, [setInstanceStateWrapped]) 68 + 42 69 return ( 43 70 <stateContext.Provider value={state}> 44 71 <setContext.Provider value={setStateWrapped}> 45 - {children} 72 + <instanceStateContext.Provider value={instanceState}> 73 + <setInstanceContext.Provider value={setInstanceStateWrapped}> 74 + {children} 75 + </setInstanceContext.Provider> 76 + </instanceStateContext.Provider> 46 77 </setContext.Provider> 47 78 </stateContext.Provider> 48 79 ) ··· 55 86 export function useSetTranslationServicePreference() { 56 87 return React.useContext(setContext) 57 88 } 89 + 90 + export function useLibreTranslateInstance() { 91 + return ( 92 + React.useContext(instanceStateContext) ?? 93 + persisted.defaults.libreTranslateInstance! 94 + ) 95 + } 96 + 97 + export function useSetLibreTranslateInstance() { 98 + return React.useContext(setInstanceContext) 99 + }