forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 💫
1import {useState} from 'react'
2import {View} from 'react-native'
3import {isDid} from '@atproto/api'
4import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
5import {msg} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7import {Trans} from '@lingui/react/macro'
8import {type NativeStackScreenProps} from '@react-navigation/native-stack'
9
10import {
11 APPVIEW_DID_PROXY,
12 DEFAULT_ALT_TEXT_AI_MODEL,
13 DEFAULT_ALT_TEXT_AI_PROMPT,
14} from '#/lib/constants'
15import {usePalette} from '#/lib/hooks/usePalette'
16import {type CommonNavigatorParams} from '#/lib/routes/types'
17import {dynamicActivate} from '#/locale/i18n'
18import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web'
19import {type AppLanguage} from '#/locale/languages'
20import * as persisted from '#/state/persisted'
21import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences'
22import {
23 useConstellationInstance,
24 useSetConstellationInstance,
25} from '#/state/preferences/constellation-instance'
26import {
27 useCustomAppViewDid,
28 useSetCustomAppViewDid,
29} from '#/state/preferences/custom-appview-did'
30import {
31 useDeerVerificationEnabled,
32 useDeerVerificationTrusted,
33 useSetDeerVerificationEnabled,
34} from '#/state/preferences/deer-verification'
35import {
36 useDirectFetchRecords,
37 useSetDirectFetchRecords,
38} from '#/state/preferences/direct-fetch-records'
39import {
40 useDisableComposerPrompt,
41 useSetDisableComposerPrompt,
42} from '#/state/preferences/disable-composer-prompt'
43import {
44 useDisableFollowedByMetrics,
45 useSetDisableFollowedByMetrics,
46} from '#/state/preferences/disable-followed-by-metrics'
47import {
48 useDisableFollowersMetrics,
49 useSetDisableFollowersMetrics,
50} from '#/state/preferences/disable-followers-metrics'
51import {
52 useDisableFollowingMetrics,
53 useSetDisableFollowingMetrics,
54} from '#/state/preferences/disable-following-metrics'
55import {
56 useDisableLikesMetrics,
57 useSetDisableLikesMetrics,
58} from '#/state/preferences/disable-likes-metrics'
59import {
60 useDisablePostsMetrics,
61 useSetDisablePostsMetrics,
62} from '#/state/preferences/disable-posts-metrics'
63import {
64 useDisableQuotesMetrics,
65 useSetDisableQuotesMetrics,
66} from '#/state/preferences/disable-quotes-metrics'
67import {
68 useDisableReplyMetrics,
69 useSetDisableReplyMetrics,
70} from '#/state/preferences/disable-reply-metrics'
71import {
72 useDisableRepostsMetrics,
73 useSetDisableRepostsMetrics,
74} from '#/state/preferences/disable-reposts-metrics'
75import {
76 useDisableSavesMetrics,
77 useSetDisableSavesMetrics,
78} from '#/state/preferences/disable-saves-metrics'
79import {
80 useDisableVerifyEmailReminder,
81 useSetDisableVerifyEmailReminder,
82} from '#/state/preferences/disable-verify-email-reminder'
83import {
84 useDisableViaRepostNotification,
85 useSetDisableViaRepostNotification,
86} from '#/state/preferences/disable-via-repost-notification'
87import {
88 useDiscoverContextEnabled,
89 useSetDiscoverContextEnabled,
90} from '#/state/preferences/discover-context-enabled'
91import {
92 useSetShowExternalShareButtons,
93 useShowExternalShareButtons,
94} from '#/state/preferences/external-share-buttons'
95import {
96 useHideFeedsPromoTab,
97 useSetHideFeedsPromoTab,
98} from '#/state/preferences/hide-feeds-promo-tab'
99import {
100 useHideSimilarAccountsRecomm,
101 useSetHideSimilarAccountsRecomm,
102} from '#/state/preferences/hide-similar-accounts-recommendations'
103import {
104 useHideUnreplyablePosts,
105 useSetHideUnreplyablePosts,
106} from '#/state/preferences/hide-unreplyable-posts'
107import {
108 useHighQualityImages,
109 useSetHighQualityImages,
110} from '#/state/preferences/high-quality-images'
111import {
112 useImageCdnHost,
113 useSetImageCdnHost,
114} from '#/state/preferences/image-cdn-host'
115import {useModerationOpts} from '#/state/preferences/moderation-opts'
116import {
117 useNoAppLabelers,
118 useSetNoAppLabelers,
119} from '#/state/preferences/no-app-labelers'
120import {
121 useNoDiscoverFallback,
122 useSetNoDiscoverFallback,
123} from '#/state/preferences/no-discover-fallback'
124import {
125 useOpenRouterApiKey,
126 useOpenRouterConfigured,
127 useOpenRouterModel,
128 useOpenRouterPrompt,
129 useSetOpenRouterApiKey,
130 useSetOpenRouterModel,
131 useSetOpenRouterPrompt,
132} from '#/state/preferences/openrouter'
133import {
134 usePdsLabelEnabled,
135 usePdsLabelHideBskyPds,
136 useSetPdsLabelEnabled,
137 useSetPdsLabelHideBskyPds,
138} from '#/state/preferences/pds-label'
139import {
140 usePostReplacement,
141 useSetPostReplacement,
142} from '#/state/preferences/post-name-replacement'
143import {
144 useRepostCarouselEnabled,
145 useSetRepostCarouselEnabled,
146} from '#/state/preferences/repost-carousel-enabled'
147import {
148 useSetShowFollowsYouBadge,
149 useShowFollowsYouBadge,
150} from '#/state/preferences/show-follows-you-badge'
151import {
152 useSetShowLinkInHandle,
153 useShowLinkInHandle,
154} from '#/state/preferences/show-link-in-handle.tsx'
155import {
156 useLibreTranslateInstance,
157 useSetLibreTranslateInstance,
158 useSetTranslationServicePreference,
159 useTranslationServicePreference,
160} from '#/state/preferences/translation-service-preference'
161import {
162 useHandleInLinks,
163 useSetHandleInLinks,
164} from '#/state/preferences/use-handle-in-links'
165import {useProfilesQuery} from '#/state/queries/profile'
166import {findService, useDidDocument} from '#/state/queries/resolve-identity'
167import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
168import * as SettingsList from '#/screens/Settings/components/SettingsList'
169import {atoms as a, useBreakpoints} from '#/alf'
170import {Admonition} from '#/components/Admonition'
171import {Button, ButtonText} from '#/components/Button'
172import * as Dialog from '#/components/Dialog'
173import * as Toggle from '#/components/forms/Toggle'
174import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
175import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink'
176import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye'
177import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
178import {Lab_Stroke2_Corner0_Rounded as _BeakerIcon} from '#/components/icons/Lab'
179import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller'
180import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil'
181import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand'
182import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star'
183import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
184import * as Layout from '#/components/Layout'
185import {InlineLinkText} from '#/components/Link'
186import {Text} from '#/components/Typography'
187import {IS_WEB} from '#/env'
188import {
189 useAutoLikeOnRepost,
190 useSetAutoLikeOnRepost,
191} from '../../state/preferences/auto-like-on-repost.tsx'
192import {SearchProfileCard} from '../Search/components/SearchProfileCard'
193
194type Props = NativeStackScreenProps<CommonNavigatorParams>
195
196function ConstellationInstanceDialog({
197 control,
198}: {
199 control: Dialog.DialogControlProps
200}) {
201 const pal = usePalette('default')
202 const {_} = useLingui()
203
204 const constellationInstance = useConstellationInstance()
205 const [url, setUrl] = useState(constellationInstance ?? '')
206 const setConstellationInstance = useSetConstellationInstance()
207
208 const submit = () => {
209 setConstellationInstance(url)
210 control.close()
211 }
212
213 const shouldDisable = () => {
214 try {
215 return !new URL(url).hostname.includes('.')
216 } catch (e) {
217 return true
218 }
219 }
220
221 return (
222 <Dialog.Outer
223 control={control}
224 nativeOptions={{preventExpansion: true}}
225 onClose={() => setUrl(constellationInstance ?? '')}>
226 <Dialog.Handle />
227 <Dialog.ScrollableInner label={_(msg`Constellations instance URL`)}>
228 <View style={[a.gap_sm, a.pb_lg]}>
229 <Text style={[a.text_2xl, a.font_bold]}>
230 <Trans>Constellations instance URL</Trans>
231 </Text>
232 </View>
233
234 <View style={a.gap_lg}>
235 <Dialog.Input
236 label="Text input field"
237 autoFocus
238 style={[styles.textInput, pal.border, pal.text]}
239 onChangeText={value => {
240 setUrl(value)
241 }}
242 placeholder={persisted.defaults.constellationInstance}
243 placeholderTextColor={pal.colors.textLight}
244 onSubmitEditing={submit}
245 accessibilityHint={_(
246 msg`Input the url of the constellations instance to use`,
247 )}
248 defaultValue={constellationInstance}
249 />
250
251 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
252 <Button
253 label={_(msg`Save`)}
254 size="large"
255 onPress={submit}
256 variant="solid"
257 color="primary"
258 disabled={shouldDisable()}>
259 <ButtonText>
260 <Trans>Save</Trans>
261 </ButtonText>
262 </Button>
263 </View>
264 </View>
265
266 <Dialog.Close />
267 </Dialog.ScrollableInner>
268 </Dialog.Outer>
269 )
270}
271
272function CustomAppViewDidDialog({
273 control,
274}: {
275 control: Dialog.DialogControlProps
276}) {
277 const pal = usePalette('default')
278 const {_} = useLingui()
279
280 const [customAppViewDid] = useCustomAppViewDid()
281 const [did, setDid] = useState(customAppViewDid ?? '')
282 const setCustomAppViewDid = useSetCustomAppViewDid()
283
284 const doc = useDidDocument({did})
285 const bskyAppViewService =
286 doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView')
287
288 const submit = () => {
289 if (did.length === 0) {
290 control.close(() => {
291 setCustomAppViewDid(undefined)
292 })
293 return
294 }
295 if (!bskyAppViewService?.serviceEndpoint) return
296 control.close(() => {
297 setCustomAppViewDid(did)
298 })
299 }
300
301 return (
302 <Dialog.Outer
303 control={control}
304 nativeOptions={{preventExpansion: true}}
305 onClose={() => setDid(customAppViewDid ?? '')}>
306 <Dialog.Handle />
307 <Dialog.ScrollableInner label={_(msg`Custom AppView Proxy DID`)}>
308 <View style={[a.gap_sm, a.pb_lg]}>
309 <Text style={[a.text_2xl, a.font_bold]}>
310 <Trans>Custom AppView Proxy DID</Trans>
311 </Text>
312 </View>
313
314 <View style={a.gap_lg}>
315 <Dialog.Input
316 label="Text input field"
317 autoFocus
318 style={[styles.textInput, pal.border, pal.text]}
319 onChangeText={value => {
320 setDid(value)
321 }}
322 placeholder={
323 APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) ||
324 `did:web:api.bsky.app`
325 }
326 placeholderTextColor={pal.colors.textLight}
327 onSubmitEditing={submit}
328 accessibilityHint={_(
329 msg`Input the DID of the AppView to proxy requests through`,
330 )}
331 isInvalid={
332 !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading
333 }
334 defaultValue={customAppViewDid ?? ''}
335 />
336
337 {did && !isDid(did) && (
338 <View>
339 <ErrorMessage message={_(msg`must enter a DID`)} />
340 </View>
341 )}
342
343 {did && (did.includes('#') || did.includes('?')) && (
344 <View>
345 <ErrorMessage message={_(msg`don't include the service id`)} />
346 </View>
347 )}
348
349 {doc.isError && (
350 <View>
351 <ErrorMessage
352 message={
353 doc.error.message || _(msg`document resolution failure`)
354 }
355 />
356 </View>
357 )}
358
359 {doc.data &&
360 !bskyAppViewService &&
361 (doc.data as {message?: string}).message && (
362 <View>
363 <ErrorMessage
364 message={(doc.data as {message: string}).message}
365 />
366 </View>
367 )}
368
369 {doc.data && !bskyAppViewService && (
370 <View>
371 <ErrorMessage
372 message={_(msg`document doesn't contain #bsky_appview service`)}
373 />
374 </View>
375 )}
376
377 {bskyAppViewService && (
378 <Text style={[a.text_sm, a.leading_snug]}>
379 {JSON.stringify(bskyAppViewService, null, 2)}
380 </Text>
381 )}
382
383 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
384 <Button
385 label={_(msg`Save`)}
386 size="large"
387 onPress={submit}
388 variant="solid"
389 color={did.length > 0 ? 'primary' : 'secondary'}
390 disabled={
391 did.length !== 0 && !bskyAppViewService?.serviceEndpoint
392 }>
393 <ButtonText>
394 {did.length > 0 ? <Trans>Save</Trans> : <Trans>Reset</Trans>}
395 </ButtonText>
396 </Button>
397 </View>
398 </View>
399
400 <Dialog.Close />
401 </Dialog.ScrollableInner>
402 </Dialog.Outer>
403 )
404}
405
406function LibreTranslateInstanceDialog({
407 control,
408}: {
409 control: Dialog.DialogControlProps
410}) {
411 const pal = usePalette('default')
412 const {_} = useLingui()
413
414 const libreTranslateInstance = useLibreTranslateInstance()
415 const [url, setUrl] = useState(libreTranslateInstance ?? '')
416 const setLibreTranslateInstance = useSetLibreTranslateInstance()
417
418 const submit = () => {
419 setLibreTranslateInstance(url)
420 control.close()
421 }
422
423 const shouldDisable = () => {
424 try {
425 return !new URL(url).hostname.includes('.')
426 } catch (e) {
427 return true
428 }
429 }
430
431 return (
432 <Dialog.Outer
433 control={control}
434 nativeOptions={{preventExpansion: true}}
435 onClose={() => setUrl(libreTranslateInstance ?? '')}>
436 <Dialog.Handle />
437 <Dialog.ScrollableInner label={_(msg`LibreTranslate instance URL`)}>
438 <View style={[a.gap_sm, a.pb_lg]}>
439 <Text style={[a.text_2xl, a.font_bold]}>
440 <Trans>LibreTranslate instance URL</Trans>
441 </Text>
442 </View>
443
444 <View style={a.gap_lg}>
445 <Dialog.Input
446 label="Text input field"
447 autoFocus
448 style={[styles.textInput, pal.border, pal.text]}
449 onChangeText={value => {
450 setUrl(value)
451 }}
452 placeholder={persisted.defaults.libreTranslateInstance}
453 placeholderTextColor={pal.colors.textLight}
454 onSubmitEditing={submit}
455 accessibilityHint={_(
456 msg`Input the url of the LibreTranslate instance to use`,
457 )}
458 defaultValue={libreTranslateInstance}
459 />
460
461 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
462 <Button
463 label={_(msg`Save`)}
464 size="large"
465 onPress={submit}
466 variant="solid"
467 color="primary"
468 disabled={shouldDisable()}>
469 <ButtonText>
470 <Trans>Save</Trans>
471 </ButtonText>
472 </Button>
473 </View>
474 </View>
475
476 <Dialog.Close />
477 </Dialog.ScrollableInner>
478 </Dialog.Outer>
479 )
480}
481
482function ImageCdnHostDialog({control}: {control: Dialog.DialogControlProps}) {
483 const pal = usePalette('default')
484 const {_} = useLingui()
485
486 const imageCdnHost = useImageCdnHost()
487 const [url, setUrl] = useState(imageCdnHost ?? '')
488 const setImageCdnHost = useSetImageCdnHost()
489
490 const submit = () => {
491 try {
492 setImageCdnHost(new URL(url).origin)
493 } catch {
494 setImageCdnHost(url)
495 }
496 control.close()
497 }
498
499 const shouldDisable = () => {
500 try {
501 return !new URL(url).hostname.includes('.')
502 } catch (e) {
503 return true
504 }
505 }
506
507 return (
508 <Dialog.Outer
509 control={control}
510 nativeOptions={{preventExpansion: true}}
511 onClose={() => setUrl(imageCdnHost ?? '')}>
512 <Dialog.Handle />
513 <Dialog.ScrollableInner label={_(msg`Image CDN URL`)}>
514 <View style={[a.gap_sm, a.pb_lg]}>
515 <Text style={[a.text_2xl, a.font_bold]}>
516 <Trans>Image CDN URL</Trans>
517 </Text>
518 </View>
519
520 <View style={a.gap_lg}>
521 <Dialog.Input
522 label="Text input field"
523 autoFocus
524 style={[styles.textInput, pal.border, pal.text]}
525 onChangeText={value => {
526 setUrl(value)
527 }}
528 placeholder={persisted.defaults.imageCdnHost}
529 placeholderTextColor={pal.colors.textLight}
530 onSubmitEditing={submit}
531 accessibilityHint={_(msg`Input the URL of the image CDN to use`)}
532 defaultValue={imageCdnHost}
533 />
534
535 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
536 <Button
537 label={_(msg`Save`)}
538 size="large"
539 onPress={submit}
540 variant="solid"
541 color="primary"
542 disabled={shouldDisable()}>
543 <ButtonText>
544 <Trans>Save</Trans>
545 </ButtonText>
546 </Button>
547 </View>
548 </View>
549
550 <Dialog.Close />
551 </Dialog.ScrollableInner>
552 </Dialog.Outer>
553 )
554}
555
556function PostReplacementDialog({
557 control,
558}: {
559 control: Dialog.DialogControlProps
560}) {
561 const pal = usePalette('default')
562 const {_, i18n} = useLingui()
563
564 const postReplacement = usePostReplacement()
565 const setPostReplacement = useSetPostReplacement()
566
567 const [singular, setSingular] = useState(postReplacement.postName)
568 const [plural, setPlural] = useState(postReplacement.postsName)
569 const [pluralManuallyEdited, setPluralManuallyEdited] = useState(false)
570
571 const submit = async () => {
572 setPostReplacement({
573 enabled: singular.trim().toLowerCase() !== 'post',
574 postName: singular,
575 postsName: plural,
576 })
577
578 // Force reload the i18n messages to apply the replacement immediately
579 const locale = i18n.locale
580 await (IS_WEB
581 ? dynamicActivateWeb(locale as AppLanguage)
582 : dynamicActivate(locale as AppLanguage))
583
584 control.close()
585 }
586
587 const handleSingularChange = (value: string) => {
588 setSingular(value)
589 if (!pluralManuallyEdited) {
590 setPlural(value + 's')
591 }
592 }
593
594 const handlePluralChange = (value: string) => {
595 setPlural(value)
596 setPluralManuallyEdited(true)
597 }
598
599 const handlePresetSelect = (singularForm: string, pluralForm: string) => {
600 setSingular(singularForm)
601 setPlural(pluralForm)
602 setPluralManuallyEdited(false)
603 }
604
605 const shouldDisable = () => {
606 return !singular.trim() || !plural.trim()
607 }
608
609 return (
610 <Dialog.Outer
611 control={control}
612 nativeOptions={{preventExpansion: true}}
613 onClose={() => {
614 setSingular(postReplacement.postName)
615 setPlural(postReplacement.postsName)
616 setPluralManuallyEdited(false)
617 }}>
618 <Dialog.Handle />
619 <Dialog.ScrollableInner label={_(msg`Custom post phrase`)}>
620 <View style={[a.gap_sm, a.pb_lg]}>
621 <Text style={[a.text_2xl, a.font_bold]}>
622 <Trans>Custom post phrase</Trans>
623 </Text>
624 </View>
625
626 <View style={a.gap_lg}>
627 <Dialog.Input
628 label="Singular form"
629 autoFocus
630 style={[styles.textInput, pal.border, pal.text]}
631 onChangeText={handleSingularChange}
632 placeholder="skeet"
633 placeholderTextColor={pal.colors.textLight}
634 accessibilityHint={_(msg`Input the singular form (e.g., "skeet")`)}
635 value={singular}
636 />
637
638 <View style={[a.flex_row, a.flex_wrap, a.mb_xs]}>
639 {[
640 {singular: 'post', plural: 'posts'},
641 {singular: 'skeet', plural: 'skeets'},
642 {singular: 'note', plural: 'notes'},
643 {singular: 'woot', plural: 'woots'},
644 {singular: 'toot', plural: 'toots'},
645 {singular: 'silly', plural: 'sillies'},
646 ].map(preset => (
647 <Button
648 key={preset.singular}
649 variant="ghost"
650 color="primary"
651 label={preset.singular}
652 style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]}
653 onPress={() =>
654 handlePresetSelect(preset.singular, preset.plural)
655 }>
656 <ButtonText>{preset.singular}</ButtonText>
657 </Button>
658 ))}
659 </View>
660
661 <Dialog.Input
662 label="Plural form"
663 style={[styles.textInput, pal.border, pal.text]}
664 onChangeText={handlePluralChange}
665 placeholder="skeets"
666 placeholderTextColor={pal.colors.textLight}
667 accessibilityHint={_(msg`Input the plural form (e.g., "skeets")`)}
668 value={plural}
669 />
670
671 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
672 <Button
673 label={_(msg`Save`)}
674 size="large"
675 onPress={submit}
676 variant="solid"
677 color="primary"
678 disabled={shouldDisable()}>
679 <ButtonText>
680 <Trans>Save</Trans>
681 </ButtonText>
682 </Button>
683 </View>
684 </View>
685
686 <Dialog.Close />
687 </Dialog.ScrollableInner>
688 </Dialog.Outer>
689 )
690}
691
692function TrustedVerifiersDialog({
693 control,
694}: {
695 control: Dialog.DialogControlProps
696}) {
697 const {_} = useLingui()
698
699 return (
700 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
701 <Dialog.Handle />
702 <Dialog.ScrollableInner label={_(msg`Trusted Verifiers`)}>
703 <View style={[a.gap_sm, a.pb_lg]}>
704 <Text style={[a.text_2xl, a.font_bold]}>
705 <Trans>Trusted Verifiers</Trans>
706 </Text>
707 </View>
708
709 <TrustedVerifiers />
710
711 <Dialog.Close />
712 </Dialog.ScrollableInner>
713 </Dialog.Outer>
714 )
715}
716
717const TrustedVerifiers = (): React.ReactNode => {
718 const trusted = useDeerVerificationTrusted()
719 const moderationOpts = useModerationOpts()
720
721 const results = useProfilesQuery({
722 handles: Array.from(trusted),
723 })
724
725 const {gtMobile} = useBreakpoints()
726
727 return (
728 results.data &&
729 moderationOpts !== undefined && (
730 <View style={[gtMobile ? a.pl_md : a.pl_sm, a.pb_sm]}>
731 {results.data.profiles.map(profile => (
732 <SearchProfileCard
733 key={profile.did}
734 profile={profile as ProfileViewBasic}
735 moderationOpts={moderationOpts}
736 />
737 ))}
738 </View>
739 )
740 )
741}
742
743function OpenRouterApiKeyDialog({
744 control,
745}: {
746 control: Dialog.DialogControlProps
747}) {
748 const pal = usePalette('default')
749 const {_} = useLingui()
750
751 const apiKey = useOpenRouterApiKey()
752 const [value, setValue] = useState(apiKey ?? '')
753 const setApiKey = useSetOpenRouterApiKey()
754
755 const submit = () => {
756 setApiKey(value.trim() || undefined)
757 control.close()
758 }
759
760 return (
761 <Dialog.Outer
762 control={control}
763 nativeOptions={{preventExpansion: true}}
764 onClose={() => setValue(apiKey ?? '')}>
765 <Dialog.Handle />
766 <Dialog.ScrollableInner label={_(msg`OpenRouter API Key`)}>
767 <View style={[a.gap_sm, a.pb_lg]}>
768 <Text style={[a.text_2xl, a.font_bold]}>
769 <Trans>OpenRouter API Key</Trans>
770 </Text>
771 </View>
772
773 <View style={a.gap_lg}>
774 <Dialog.Input
775 label="API Key"
776 autoFocus
777 style={[styles.textInput, pal.border, pal.text]}
778 onChangeText={setValue}
779 placeholder="sk-or-..."
780 placeholderTextColor={pal.colors.textLight}
781 onSubmitEditing={submit}
782 accessibilityHint={_(
783 msg`Enter your OpenRouter API key for AI alt text generation`,
784 )}
785 defaultValue={apiKey ?? ''}
786 secureTextEntry
787 />
788
789 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
790 <Button
791 label={_(msg`Save`)}
792 size="large"
793 onPress={submit}
794 variant="solid"
795 color="primary">
796 <ButtonText>
797 <Trans>Save</Trans>
798 </ButtonText>
799 </Button>
800 </View>
801 </View>
802
803 <Dialog.Close />
804 </Dialog.ScrollableInner>
805 </Dialog.Outer>
806 )
807}
808
809function OpenRouterModelDialog({
810 control,
811}: {
812 control: Dialog.DialogControlProps
813}) {
814 const pal = usePalette('default')
815 const {_} = useLingui()
816
817 const model = useOpenRouterModel()
818 const [value, setValue] = useState(model ?? '')
819 const setModel = useSetOpenRouterModel()
820
821 const submit = () => {
822 setModel(value.trim() || undefined)
823 control.close()
824 }
825
826 return (
827 <Dialog.Outer
828 control={control}
829 nativeOptions={{preventExpansion: true}}
830 onClose={() => setValue(model ?? '')}>
831 <Dialog.Handle />
832 <Dialog.ScrollableInner label={_(msg`OpenRouter Model`)}>
833 <View style={[a.gap_sm, a.pb_lg]}>
834 <Text style={[a.text_2xl, a.font_bold]}>
835 <Trans>OpenRouter Model</Trans>
836 </Text>
837 </View>
838
839 <View style={a.gap_lg}>
840 <Dialog.Input
841 label="Model"
842 autoFocus
843 style={[styles.textInput, pal.border, pal.text]}
844 onChangeText={setValue}
845 placeholder={DEFAULT_ALT_TEXT_AI_MODEL}
846 placeholderTextColor={pal.colors.textLight}
847 onSubmitEditing={submit}
848 accessibilityHint={_(
849 msg`Enter the model ID to use for alt text generation`,
850 )}
851 defaultValue={model ?? ''}
852 />
853
854 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
855 <Button
856 label={_(msg`Save`)}
857 size="large"
858 onPress={submit}
859 variant="solid"
860 color="primary">
861 <ButtonText>
862 <Trans>Save</Trans>
863 </ButtonText>
864 </Button>
865 </View>
866 </View>
867
868 <Dialog.Close />
869 </Dialog.ScrollableInner>
870 </Dialog.Outer>
871 )
872}
873
874function OpenRouterPromptDialog({
875 control,
876}: {
877 control: Dialog.DialogControlProps
878}) {
879 const pal = usePalette('default')
880 const {_} = useLingui()
881
882 const prompt = useOpenRouterPrompt()
883 const [value, setValue] = useState(prompt ?? '')
884 const setPrompt = useSetOpenRouterPrompt()
885
886 const submit = () => {
887 setPrompt(value.trim() || undefined)
888 control.close()
889 }
890
891 return (
892 <Dialog.Outer
893 control={control}
894 nativeOptions={{preventExpansion: true}}
895 onClose={() => setValue(prompt ?? '')}>
896 <Dialog.Handle />
897 <Dialog.ScrollableInner label={_(msg`Alt Text Prompt`)}>
898 <View style={[a.gap_sm, a.pb_lg]}>
899 <Text style={[a.text_2xl, a.font_bold]}>
900 <Trans>Alt Text Prompt</Trans>
901 </Text>
902 </View>
903
904 <View style={a.gap_lg}>
905 <Dialog.Input
906 label="Prompt"
907 multiline
908 numberOfLines={6}
909 style={[
910 styles.textInput,
911 pal.border,
912 pal.text,
913 {minHeight: 120, textAlignVertical: 'top'},
914 ]}
915 onChangeText={setValue}
916 placeholder={DEFAULT_ALT_TEXT_AI_PROMPT}
917 placeholderTextColor={pal.colors.textLight}
918 accessibilityHint={_(
919 msg`Enter a custom prompt for AI alt text generation`,
920 )}
921 defaultValue={prompt ?? ''}
922 />
923
924 <View style={IS_WEB && [a.flex_row, a.justify_end]}>
925 <Button
926 label={_(msg`Save`)}
927 size="large"
928 onPress={submit}
929 variant="solid"
930 color="primary">
931 <ButtonText>
932 <Trans>Save</Trans>
933 </ButtonText>
934 </Button>
935 </View>
936 </View>
937
938 <Dialog.Close />
939 </Dialog.ScrollableInner>
940 </Dialog.Outer>
941 )
942}
943
944export function RunesSettingsScreen({}: Props) {
945 const {_} = useLingui()
946
947 const goLinksEnabled = useGoLinksEnabled()
948 const setGoLinksEnabled = useSetGoLinksEnabled()
949
950 const directFetchRecords = useDirectFetchRecords()
951 const setDirectFetchRecords = useSetDirectFetchRecords()
952
953 const showExternalShareButtons = useShowExternalShareButtons()
954 const setShowExternalShareButtons = useSetShowExternalShareButtons()
955
956 const noAppLabelers = useNoAppLabelers()
957 const setNoAppLabelers = useSetNoAppLabelers()
958
959 const noDiscoverFallback = useNoDiscoverFallback()
960 const setNoDiscoverFallback = useSetNoDiscoverFallback()
961
962 const highQualityImages = useHighQualityImages()
963 const setHighQualityImages = useSetHighQualityImages()
964 const imageCdnHost = useImageCdnHost()
965
966 const hideFeedsPromoTab = useHideFeedsPromoTab()
967 const setHideFeedsPromoTab = useSetHideFeedsPromoTab()
968
969 const disableViaRepostNotification = useDisableViaRepostNotification()
970 const setDisableViaRepostNotification = useSetDisableViaRepostNotification()
971
972 const disableComposerPrompt = useDisableComposerPrompt()
973 const setDisableComposerPrompt = useSetDisableComposerPrompt()
974
975 const discoverContextEnabled = useDiscoverContextEnabled()
976 const setDiscoverContextEnabled = useSetDiscoverContextEnabled()
977
978 const disableLikesMetrics = useDisableLikesMetrics()
979 const setDisableLikesMetrics = useSetDisableLikesMetrics()
980
981 const disableRepostsMetrics = useDisableRepostsMetrics()
982 const setDisableRepostsMetrics = useSetDisableRepostsMetrics()
983
984 const disableQuotesMetrics = useDisableQuotesMetrics()
985 const setDisableQuotesMetrics = useSetDisableQuotesMetrics()
986
987 const disableSavesMetrics = useDisableSavesMetrics()
988 const setDisableSavesMetrics = useSetDisableSavesMetrics()
989
990 const disableReplyMetrics = useDisableReplyMetrics()
991 const setDisableReplyMetrics = useSetDisableReplyMetrics()
992
993 const disableFollowersMetrics = useDisableFollowersMetrics()
994 const setDisableFollowersMetrics = useSetDisableFollowersMetrics()
995
996 const disableFollowingMetrics = useDisableFollowingMetrics()
997 const setDisableFollowingMetrics = useSetDisableFollowingMetrics()
998
999 const disableFollowedByMetrics = useDisableFollowedByMetrics()
1000 const setDisableFollowedByMetrics = useSetDisableFollowedByMetrics()
1001
1002 const disablePostsMetrics = useDisablePostsMetrics()
1003 const setDisablePostsMetrics = useSetDisablePostsMetrics()
1004
1005 const hideSimilarAccountsRecomm = useHideSimilarAccountsRecomm()
1006 const setHideSimilarAccountsRecomm = useSetHideSimilarAccountsRecomm()
1007
1008 const hideUnreplyablePosts = useHideUnreplyablePosts()
1009 const setHideUnreplyablePosts = useSetHideUnreplyablePosts()
1010
1011 const disableVerifyEmailReminder = useDisableVerifyEmailReminder()
1012 const setDisableVerifyEmailReminder = useSetDisableVerifyEmailReminder()
1013
1014 const constellationInstance = useConstellationInstance()
1015 const setConstellationInstanceControl = Dialog.useDialogControl()
1016
1017 const setTrustedVerifiersDialogControl = Dialog.useDialogControl()
1018
1019 const deerVerificationEnabled = useDeerVerificationEnabled()
1020 const setDeerVerificationEnabled = useSetDeerVerificationEnabled()
1021
1022 const pdsLabelEnabled = usePdsLabelEnabled()
1023 const setPdsLabelEnabled = useSetPdsLabelEnabled()
1024 const pdsLabelHideBskyPds = usePdsLabelHideBskyPds()
1025 const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds()
1026
1027 const repostCarouselEnabled = useRepostCarouselEnabled()
1028 const setRepostCarouselEnabled = useSetRepostCarouselEnabled()
1029
1030 const showFollowsYouBadge = useShowFollowsYouBadge()
1031 const setShowFollowsYouBadge = useSetShowFollowsYouBadge()
1032
1033 const showLinkInHandle = useShowLinkInHandle()
1034 const setShowLinkInHandle = useSetShowLinkInHandle()
1035
1036 const handleInLinks = useHandleInLinks()
1037 const setHandleInLinks = useSetHandleInLinks()
1038
1039 const translationServicePreference = useTranslationServicePreference()
1040 const setTranslationServicePreference = useSetTranslationServicePreference()
1041
1042 const setLibreTranslateInstanceControl = Dialog.useDialogControl()
1043
1044 const setImageCdnHostControl = Dialog.useDialogControl()
1045
1046 const setPostReplacementDialogControl = Dialog.useDialogControl()
1047
1048 const setOpenRouterApiKeyControl = Dialog.useDialogControl()
1049 const openRouterModel = useOpenRouterModel()
1050 const setOpenRouterModelControl = Dialog.useDialogControl()
1051 const setOpenRouterPromptControl = Dialog.useDialogControl()
1052 const openRouterConfigured = useOpenRouterConfigured()
1053
1054 const autoLikeOnRepost = useAutoLikeOnRepost()
1055 const setAutoLikeOnRepost = useSetAutoLikeOnRepost()
1056
1057 const [customAppViewDid] = useCustomAppViewDid()
1058 const setCustomAppViewDidControl = Dialog.useDialogControl()
1059
1060 return (
1061 <Layout.Screen>
1062 <Layout.Header.Outer>
1063 <Layout.Header.BackButton />
1064 <Layout.Header.Content>
1065 <Layout.Header.TitleText>
1066 <Trans>Runes</Trans>
1067 </Layout.Header.TitleText>
1068 </Layout.Header.Content>
1069 <Layout.Header.Slot />
1070 </Layout.Header.Outer>
1071 <Layout.Content>
1072 <SettingsList.Container>
1073 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1074 <SettingsList.ItemIcon icon={AtomIcon} />
1075 <SettingsList.ItemText>
1076 <Trans>Redirects</Trans>
1077 </SettingsList.ItemText>
1078 <Toggle.Item
1079 name="use_go_links"
1080 label={_(msg`Redirect through go.bsky.app`)}
1081 value={goLinksEnabled ?? false}
1082 onChange={value => setGoLinksEnabled(value)}
1083 style={[a.w_full]}>
1084 <Toggle.LabelText style={[a.flex_1]}>
1085 <Trans>Redirect through go.bsky.app</Trans>
1086 </Toggle.LabelText>
1087 <Toggle.Platform />
1088 </Toggle.Item>
1089 <Toggle.Item
1090 name="use_handle_in_links"
1091 label={_(
1092 msg`Use handles in profile links instead of DIDs (requires restart)`,
1093 )}
1094 value={handleInLinks ?? false}
1095 onChange={value => setHandleInLinks(value)}
1096 style={[a.w_full]}>
1097 <Toggle.LabelText style={[a.flex_1]}>
1098 <Trans>Use handles in profile links instead of DIDs</Trans>
1099 </Toggle.LabelText>
1100 <Toggle.Platform />
1101 </Toggle.Item>
1102 </SettingsList.Group>
1103
1104 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1105 <SettingsList.ItemIcon icon={VisibilityIcon} />
1106 <SettingsList.ItemText>
1107 <Trans>Visibility</Trans>
1108 </SettingsList.ItemText>
1109 <Toggle.Item
1110 name="direct_fetch_records"
1111 label={_(
1112 msg`Fetch records directly from PDS to see through quote blocks`,
1113 )}
1114 value={directFetchRecords}
1115 onChange={value => setDirectFetchRecords(value)}
1116 style={[a.w_full]}>
1117 <Toggle.LabelText style={[a.flex_1]}>
1118 <Trans>
1119 Fetch records directly from PDS to see contents of blocked and
1120 detached quotes
1121 </Trans>
1122 </Toggle.LabelText>
1123 <Toggle.Platform />
1124 </Toggle.Item>
1125 </SettingsList.Group>
1126
1127 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1128 <SettingsList.ItemIcon icon={ChainLinkIcon} />
1129 <SettingsList.ItemText>
1130 <Trans>Bridging and Fediverse</Trans>
1131 </SettingsList.ItemText>
1132 <Toggle.Item
1133 name="external_share_buttons"
1134 label={_(
1135 msg`Show "Open original post" and "Open post in PDSls" buttons`,
1136 )}
1137 value={showExternalShareButtons}
1138 onChange={value => setShowExternalShareButtons(value)}
1139 style={[a.w_full]}>
1140 <Toggle.LabelText style={[a.flex_1]}>
1141 <Trans>
1142 Show "Open original post" and "Open post in PDSls" buttons
1143 </Trans>
1144 </Toggle.LabelText>
1145 <Toggle.Platform />
1146 </Toggle.Item>
1147 </SettingsList.Group>
1148
1149 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1150 <SettingsList.ItemIcon icon={VerifiedIcon} />
1151 <SettingsList.ItemText>
1152 <Trans>Verification</Trans>
1153 </SettingsList.ItemText>
1154 <Toggle.Item
1155 name="custom_verifications"
1156 label={_(
1157 msg`Select your own set of trusted verifiers, and operate as a verifier`,
1158 )}
1159 value={deerVerificationEnabled}
1160 onChange={value => setDeerVerificationEnabled(value)}
1161 style={[a.w_full]}>
1162 <Toggle.LabelText style={[a.flex_1]}>
1163 <Trans>
1164 Select your own set of trusted verifiers, and operate as a
1165 verifier
1166 </Trans>
1167 </Toggle.LabelText>
1168 <Toggle.Platform />
1169 </Toggle.Item>
1170 </SettingsList.Group>
1171
1172 <SettingsList.Item>
1173 <Admonition type="warning" style={[a.flex_1]}>
1174 <Trans>
1175 May slow down the client or fail to find all labels. Revoke and
1176 grant trust in the meatball menu on a profile.{' '}
1177 {deerVerificationEnabled
1178 ? 'You currently'
1179 : 'If enabled, you would'}{' '}
1180 trust the following verifiers:
1181 </Trans>
1182 </Admonition>
1183 </SettingsList.Item>
1184
1185 <SettingsList.Item>
1186 <SettingsList.ItemIcon icon={VerifiedIcon} />
1187 <SettingsList.ItemText>
1188 <Trans>{`Trusted Verifiers`}</Trans>
1189 </SettingsList.ItemText>
1190 <SettingsList.BadgeButton
1191 label={_(msg`View`)}
1192 onPress={() => setTrustedVerifiersDialogControl.open()}
1193 />
1194 </SettingsList.Item>
1195
1196 <SettingsList.Item>
1197 <SettingsList.ItemIcon icon={StarIcon} />
1198 <SettingsList.ItemText>
1199 <Trans>{`Constellation Instance`}</Trans>
1200 </SettingsList.ItemText>
1201 <SettingsList.BadgeButton
1202 label={_(msg`Change`)}
1203 onPress={() => setConstellationInstanceControl.open()}
1204 />
1205 </SettingsList.Item>
1206 <SettingsList.Item>
1207 <Admonition type="info" style={[a.flex_1]}>
1208 <Trans>
1209 Constellation is used to supplement AppView responses for custom
1210 verifications and nuclear block bypass, via backlinks. Current
1211 instance:\u00A0
1212 <InlineLinkText
1213 to={constellationInstance}
1214 label={constellationInstance}>
1215 {constellationInstance}
1216 </InlineLinkText>
1217 </Trans>
1218 </Admonition>
1219 </SettingsList.Item>
1220
1221 <SettingsList.Divider />
1222
1223 <SettingsList.Item>
1224 <SettingsList.ItemIcon icon={PencilIcon} />
1225 <SettingsList.ItemText>
1226 <Trans>{`Custom post phrase`}</Trans>
1227 </SettingsList.ItemText>
1228 <SettingsList.BadgeButton
1229 label={_(msg`Change`)}
1230 onPress={() => setPostReplacementDialogControl.open()}
1231 />
1232 </SettingsList.Item>
1233
1234 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1235 <SettingsList.ItemIcon icon={PaintRollerIcon} />
1236 <SettingsList.ItemText>
1237 <Trans>Tweaks</Trans>
1238 </SettingsList.ItemText>
1239 <Toggle.Item
1240 name="pds_label_badge"
1241 label={_(
1242 msg`Show a PDS badge next to the display name on profiles`,
1243 )}
1244 value={pdsLabelEnabled}
1245 onChange={value => setPdsLabelEnabled(value)}
1246 style={[a.w_full]}>
1247 <Toggle.LabelText style={[a.flex_1]}>
1248 <Trans>
1249 Show a PDS badge next to the display name on profiles
1250 </Trans>
1251 </Toggle.LabelText>
1252 <Toggle.Platform />
1253 </Toggle.Item>
1254 {pdsLabelEnabled && (
1255 <Toggle.Item
1256 name="pds_label_hide_bsky"
1257 label={_(msg`Hide PDS badge for Bluesky-hosted accounts`)}
1258 value={pdsLabelHideBskyPds}
1259 onChange={value => setPdsLabelHideBskyPds(value)}
1260 style={[a.w_full]}>
1261 <Toggle.LabelText style={[a.flex_1]}>
1262 <Trans>Hide PDS badge for Bluesky-hosted accounts</Trans>
1263 </Toggle.LabelText>
1264 <Toggle.Platform />
1265 </Toggle.Item>
1266 )}
1267
1268 <Toggle.Item
1269 name="repost_carousel"
1270 label={_(msg`Combine reposts into a horizontal carousel`)}
1271 value={repostCarouselEnabled}
1272 onChange={value => setRepostCarouselEnabled(value)}
1273 style={[a.w_full]}>
1274 <Toggle.LabelText style={[a.flex_1]}>
1275 <Trans>Combine reposts into a horizontal carousel</Trans>
1276 </Toggle.LabelText>
1277 <Toggle.Platform />
1278 </Toggle.Item>
1279
1280 <Toggle.Item
1281 name="show_link_in_handle"
1282 label={_(
1283 msg`On non-bsky.social handles, show a link to that URL`,
1284 )}
1285 value={showLinkInHandle}
1286 onChange={value => setShowLinkInHandle(value)}
1287 style={[a.w_full]}>
1288 <Toggle.LabelText style={[a.flex_1]}>
1289 <Trans>
1290 On non-bsky.social handles, show a link to that URL
1291 </Trans>
1292 </Toggle.LabelText>
1293 <Toggle.Platform />
1294 </Toggle.Item>
1295
1296 <Toggle.Item
1297 name="no_discover_fallback"
1298 label={_(msg`Do not fall back to discover feed`)}
1299 value={noDiscoverFallback}
1300 onChange={value => setNoDiscoverFallback(value)}
1301 style={[a.w_full]}>
1302 <Toggle.LabelText style={[a.flex_1]}>
1303 <Trans>Do not fall back to discover feed</Trans>
1304 </Toggle.LabelText>
1305 <Toggle.Platform />
1306 </Toggle.Item>
1307
1308 <Toggle.Item
1309 name="high_quality_images"
1310 label={_(msg`Display images in higher quality`)}
1311 value={highQualityImages}
1312 onChange={value => setHighQualityImages(value)}
1313 style={[a.w_full]}>
1314 <Toggle.LabelText style={[a.flex_1]}>
1315 <Trans>Display images in higher quality</Trans>
1316 </Toggle.LabelText>
1317 <Toggle.Platform />
1318 </Toggle.Item>
1319 <Admonition type="info" style={[a.flex_1]}>
1320 <Trans>
1321 Images will be served as PNG instead of JPEG. Images will take
1322 longer to load and use more bandwidth.
1323 </Trans>
1324 </Admonition>
1325 <Toggle.Item
1326 name="auto_like_on_repost"
1327 label={_(msg`Auto-like what you repost`)}
1328 value={autoLikeOnRepost}
1329 onChange={value => setAutoLikeOnRepost(value)}
1330 style={[a.w_full]}>
1331 <Toggle.LabelText style={[a.flex_1]}>
1332 <Trans>Auto-like what you repost</Trans>
1333 </Toggle.LabelText>
1334 <Toggle.Platform />
1335 </Toggle.Item>
1336 <Toggle.Item
1337 name="hide_feeds_promo_tab"
1338 label={_(msg`Hide "Feeds ✨" tab when only one feed is selected`)}
1339 value={hideFeedsPromoTab}
1340 onChange={value => setHideFeedsPromoTab(value)}
1341 style={[a.w_full]}>
1342 <Toggle.LabelText style={[a.flex_1]}>
1343 <Trans>
1344 Hide "Feeds ✨" tab when only one feed is selected
1345 </Trans>
1346 </Toggle.LabelText>
1347 <Toggle.Platform />
1348 </Toggle.Item>
1349
1350 <Toggle.Item
1351 name="disable_via_repost_notification"
1352 label={_(msg`Disable via repost notifications`)}
1353 value={disableViaRepostNotification}
1354 onChange={value => setDisableViaRepostNotification(value)}
1355 style={[a.w_full]}>
1356 <Toggle.LabelText style={[a.flex_1]}>
1357 <Trans>Disable via repost notifications</Trans>
1358 </Toggle.LabelText>
1359 <Toggle.Platform />
1360 </Toggle.Item>
1361 <Admonition type="info" style={[a.flex_1]}>
1362 <Trans>
1363 Forcefully disables the notifications other people receive when
1364 you like/repost a post someone else has reposted for privacy.
1365 </Trans>
1366 </Admonition>
1367
1368 <Toggle.Item
1369 name="hide_similar_accounts_recommendations"
1370 label={_(msg`Hide similar accounts recommendations`)}
1371 value={hideSimilarAccountsRecomm}
1372 onChange={value => setHideSimilarAccountsRecomm(value)}
1373 style={[a.w_full]}>
1374 <Toggle.LabelText style={[a.flex_1]}>
1375 <Trans>Hide similar accounts recommendations</Trans>
1376 </Toggle.LabelText>
1377 <Toggle.Platform />
1378 </Toggle.Item>
1379
1380 <Toggle.Item
1381 name="hide_unreplyable_posts"
1382 label={_(msg`Hide posts that cannot be replied to from feeds`)}
1383 value={hideUnreplyablePosts}
1384 onChange={value => setHideUnreplyablePosts(value)}
1385 style={[a.w_full]}>
1386 <Toggle.LabelText style={[a.flex_1]}>
1387 <Trans>Hide posts that cannot be replied to from feeds</Trans>
1388 </Toggle.LabelText>
1389 <Toggle.Platform />
1390 </Toggle.Item>
1391 <Admonition type="info" style={[a.flex_1]}>
1392 <Trans>
1393 Hides posts from feeds where replies are disabled (e.g. due to
1394 postgates or other restrictions). Does not affect thread views.
1395 </Trans>
1396 </Admonition>
1397
1398 <Toggle.Item
1399 name="disable_composer_prompt"
1400 label={_(msg`Disable composer prompt`)}
1401 value={disableComposerPrompt}
1402 onChange={value => setDisableComposerPrompt(value)}
1403 style={[a.w_full]}>
1404 <Toggle.LabelText style={[a.flex_1]}>
1405 <Trans>Disable composer prompt</Trans>
1406 </Toggle.LabelText>
1407 <Toggle.Platform />
1408 </Toggle.Item>
1409
1410 <Toggle.Item
1411 name="disable_verify_email_reminder"
1412 label={_(msg`Disable verify email reminder`)}
1413 value={disableVerifyEmailReminder}
1414 onChange={value => setDisableVerifyEmailReminder(value)}
1415 style={[a.w_full]}>
1416 <Toggle.LabelText style={[a.flex_1]}>
1417 <Trans>Disable verify email reminder</Trans>
1418 </Toggle.LabelText>
1419 <Toggle.Platform />
1420 </Toggle.Item>
1421 <Admonition type="warning" style={[a.flex_1]}>
1422 <Trans>
1423 This only gets rid of the reminder on app launch, useful if your
1424 PDS does not have email verification setup.\u00A0 This does NOT
1425 give access to features locked behind email verification.
1426 </Trans>
1427 </Admonition>
1428
1429 <Toggle.Item
1430 name="discover_context"
1431 label={_(msg`Show debug context for posts in Discover feed`)}
1432 value={discoverContextEnabled}
1433 onChange={value => setDiscoverContextEnabled(value)}
1434 style={[a.w_full]}>
1435 <Toggle.LabelText style={[a.flex_1]}>
1436 <Trans>Show debug context for posts in Discover feed</Trans>
1437 </Toggle.LabelText>
1438 <Toggle.Platform />
1439 </Toggle.Item>
1440 </SettingsList.Group>
1441
1442 <SettingsList.Divider />
1443
1444 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1445 <SettingsList.ItemIcon icon={EarthIcon} />
1446 <SettingsList.ItemText>
1447 <Trans>Post Translation Provider</Trans>
1448 </SettingsList.ItemText>
1449
1450 <Toggle.Item
1451 name="service_google"
1452 label={_(msg`Use Google Translate`)}
1453 value={translationServicePreference === 'google'}
1454 onChange={() => setTranslationServicePreference('google')}
1455 style={[a.w_full]}>
1456 <Toggle.LabelText style={[a.flex_1]}>
1457 <Trans>Use Google Translate</Trans>
1458 </Toggle.LabelText>
1459 <Toggle.Radio />
1460 </Toggle.Item>
1461
1462 <Toggle.Item
1463 name="service_kagi"
1464 label={_(msg`Use Kagi Translate`)}
1465 value={translationServicePreference === 'kagi'}
1466 onChange={() => setTranslationServicePreference('kagi')}
1467 style={[a.w_full]}>
1468 <Toggle.LabelText style={[a.flex_1]}>
1469 <Trans>Use Kagi Translate</Trans>
1470 </Toggle.LabelText>
1471 <Toggle.Radio />
1472 </Toggle.Item>
1473
1474 <Toggle.Item
1475 name="service_papago"
1476 label={_(msg`Use Naver Papago`)}
1477 value={translationServicePreference === 'papago'}
1478 onChange={() => setTranslationServicePreference('papago')}
1479 style={[a.w_full]}>
1480 <Toggle.LabelText style={[a.flex_1]}>
1481 <Trans>Use Naver Papago</Trans>
1482 </Toggle.LabelText>
1483 <Toggle.Radio />
1484 </Toggle.Item>
1485
1486 <Toggle.Item
1487 name="service_libreTranslate"
1488 label={_(msg`Use LibreTranslate`)}
1489 value={translationServicePreference === 'libreTranslate'}
1490 onChange={() => setTranslationServicePreference('libreTranslate')}
1491 style={[a.w_full]}>
1492 <Toggle.LabelText style={[a.flex_1]}>
1493 <Trans>Use LibreTranslate</Trans>
1494 </Toggle.LabelText>
1495 <Toggle.Radio />
1496 </Toggle.Item>
1497 </SettingsList.Group>
1498
1499 {translationServicePreference === 'libreTranslate' && (
1500 <SettingsList.Item>
1501 <SettingsList.ItemIcon icon={EarthIcon} />
1502 <SettingsList.ItemText>
1503 <Trans>{`LibreTranslate Instance`}</Trans>
1504 </SettingsList.ItemText>
1505 <SettingsList.BadgeButton
1506 label={_(msg`Change`)}
1507 onPress={() => setLibreTranslateInstanceControl.open()}
1508 />
1509 </SettingsList.Item>
1510 )}
1511
1512 <SettingsList.Divider />
1513
1514 <SettingsList.Item>
1515 <SettingsList.ItemIcon icon={_BeakerIcon} />
1516 <SettingsList.ItemText>
1517 <Trans>OpenRouter API Key</Trans>
1518 </SettingsList.ItemText>
1519 <SettingsList.BadgeButton
1520 label={openRouterConfigured ? _(msg`Change`) : _(msg`Set`)}
1521 onPress={() => setOpenRouterApiKeyControl.open()}
1522 />
1523 </SettingsList.Item>
1524
1525 <SettingsList.Item>
1526 <Admonition type="info" style={[a.flex_1]}>
1527 <Trans>
1528 Set your OpenRouter API key to enable AI-powered alt text
1529 generation for images in the composer. Get an API key at{' '}
1530 <InlineLinkText
1531 to="https://openrouter.ai"
1532 label="openrouter.ai">
1533 openrouter.ai
1534 </InlineLinkText>
1535 </Trans>
1536 </Admonition>
1537 </SettingsList.Item>
1538
1539 {openRouterConfigured && (
1540 <SettingsList.Item>
1541 <SettingsList.ItemIcon icon={_BeakerIcon} />
1542 <SettingsList.ItemText>
1543 <Trans>{`OpenRouter Model`}</Trans>
1544 </SettingsList.ItemText>
1545 <SettingsList.BadgeButton
1546 label={_(msg`Change`)}
1547 onPress={() => setOpenRouterModelControl.open()}
1548 />
1549 </SettingsList.Item>
1550 )}
1551
1552 {openRouterConfigured && (
1553 <SettingsList.Item>
1554 <Admonition type="info" style={[a.flex_1]}>
1555 <Trans>
1556 Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '}
1557 <InlineLinkText
1558 to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular"
1559 label="openrouter.ai">
1560 Search models
1561 </InlineLinkText>
1562 </Trans>
1563 </Admonition>
1564 </SettingsList.Item>
1565 )}
1566
1567 {openRouterConfigured && (
1568 <SettingsList.Item>
1569 <SettingsList.ItemIcon icon={_BeakerIcon} />
1570 <SettingsList.ItemText>
1571 <Trans>Alt Text Prompt</Trans>
1572 </SettingsList.ItemText>
1573 <SettingsList.BadgeButton
1574 label={_(msg`Change`)}
1575 onPress={() => setOpenRouterPromptControl.open()}
1576 />
1577 </SettingsList.Item>
1578 )}
1579
1580 {openRouterConfigured && (
1581 <SettingsList.Item>
1582 <Admonition type="info" style={[a.flex_1]}>
1583 <Trans>
1584 Customize the prompt sent to the AI model when generating alt
1585 text. Leave empty to use the default prompt.
1586 </Trans>
1587 </Admonition>
1588 </SettingsList.Item>
1589 )}
1590
1591 <SettingsList.Divider />
1592
1593 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1594 <SettingsList.ItemIcon icon={VisibilityIcon} />
1595 <SettingsList.ItemText>
1596 <Trans>Metrics</Trans>
1597 </SettingsList.ItemText>
1598
1599 <Toggle.Item
1600 name="disable_likes_metrics"
1601 label={_(msg`Disable likes metrics`)}
1602 value={disableLikesMetrics}
1603 onChange={value => setDisableLikesMetrics(value)}
1604 style={[a.w_full]}>
1605 <Toggle.LabelText style={[a.flex_1]}>
1606 <Trans>Disable likes metrics</Trans>
1607 </Toggle.LabelText>
1608 <Toggle.Platform />
1609 </Toggle.Item>
1610
1611 <Toggle.Item
1612 name="disable_reposts_metrics"
1613 label={_(msg`Disable reposts metrics`)}
1614 value={disableRepostsMetrics}
1615 onChange={value => setDisableRepostsMetrics(value)}
1616 style={[a.w_full]}>
1617 <Toggle.LabelText style={[a.flex_1]}>
1618 <Trans>Disable reposts metrics</Trans>
1619 </Toggle.LabelText>
1620 <Toggle.Platform />
1621 </Toggle.Item>
1622
1623 <Toggle.Item
1624 name="disable_quotes_metrics"
1625 label={_(msg`Disable quotes metrics`)}
1626 value={disableQuotesMetrics}
1627 onChange={value => setDisableQuotesMetrics(value)}
1628 style={[a.w_full]}>
1629 <Toggle.LabelText style={[a.flex_1]}>
1630 <Trans>Disable quotes metrics</Trans>
1631 </Toggle.LabelText>
1632 <Toggle.Platform />
1633 </Toggle.Item>
1634
1635 <Toggle.Item
1636 name="disable_saves_metrics"
1637 label={_(msg`Disable saves metrics`)}
1638 value={disableSavesMetrics}
1639 onChange={value => setDisableSavesMetrics(value)}
1640 style={[a.w_full]}>
1641 <Toggle.LabelText style={[a.flex_1]}>
1642 <Trans>Disable saves metrics</Trans>
1643 </Toggle.LabelText>
1644 <Toggle.Platform />
1645 </Toggle.Item>
1646
1647 <Toggle.Item
1648 name="disable_reply_metrics"
1649 label={_(msg`Disable reply metrics`)}
1650 value={disableReplyMetrics}
1651 onChange={value => setDisableReplyMetrics(value)}
1652 style={[a.w_full]}>
1653 <Toggle.LabelText style={[a.flex_1]}>
1654 <Trans>Disable reply metrics</Trans>
1655 </Toggle.LabelText>
1656 <Toggle.Platform />
1657 </Toggle.Item>
1658
1659 <Toggle.Item
1660 name="disable_followers_metrics"
1661 label={_(msg`Disable followers metrics`)}
1662 value={disableFollowersMetrics}
1663 onChange={value => setDisableFollowersMetrics(value)}
1664 style={[a.w_full]}>
1665 <Toggle.LabelText style={[a.flex_1]}>
1666 <Trans>Disable followers metrics</Trans>
1667 </Toggle.LabelText>
1668 <Toggle.Platform />
1669 </Toggle.Item>
1670
1671 <Toggle.Item
1672 name="disable_following_metrics"
1673 label={_(msg`Disable following metrics`)}
1674 value={disableFollowingMetrics}
1675 onChange={value => setDisableFollowingMetrics(value)}
1676 style={[a.w_full]}>
1677 <Toggle.LabelText style={[a.flex_1]}>
1678 <Trans>Disable following metrics</Trans>
1679 </Toggle.LabelText>
1680 <Toggle.Platform />
1681 </Toggle.Item>
1682
1683 <Toggle.Item
1684 name="disable_followed_by_metrics"
1685 label={_(msg`Disable "followed by" metrics`)}
1686 value={disableFollowedByMetrics}
1687 onChange={value => setDisableFollowedByMetrics(value)}
1688 style={[a.w_full]}>
1689 <Toggle.LabelText style={[a.flex_1]}>
1690 <Trans>Disable "followed by" metrics</Trans>
1691 </Toggle.LabelText>
1692 <Toggle.Platform />
1693 </Toggle.Item>
1694
1695 <Toggle.Item
1696 name="show_follows_you_badge"
1697 label={_(msg`Show "Follows you" badge`)}
1698 value={showFollowsYouBadge}
1699 onChange={value => setShowFollowsYouBadge(value)}
1700 style={[a.w_full]}>
1701 <Toggle.LabelText style={[a.flex_1]}>
1702 <Trans>Show "Follows you" badge</Trans>
1703 </Toggle.LabelText>
1704 <Toggle.Platform />
1705 </Toggle.Item>
1706
1707 <Toggle.Item
1708 name="disable_posts_metrics"
1709 label={_(msg`Disable post counts metrics`)}
1710 value={disablePostsMetrics}
1711 onChange={value => setDisablePostsMetrics(value)}
1712 style={[a.w_full]}>
1713 <Toggle.LabelText style={[a.flex_1]}>
1714 <Trans>Disable post counts metrics</Trans>
1715 </Toggle.LabelText>
1716 <Toggle.Platform />
1717 </Toggle.Item>
1718 </SettingsList.Group>
1719
1720 <SettingsList.Divider />
1721
1722 <SettingsList.Item>
1723 <SettingsList.ItemIcon icon={EarthIcon} />
1724 <SettingsList.ItemText>
1725 <Trans>{`Image CDN`}</Trans>
1726 </SettingsList.ItemText>
1727 <SettingsList.BadgeButton
1728 label={_(msg`Change`)}
1729 onPress={() => setImageCdnHostControl.open()}
1730 />
1731 </SettingsList.Item>
1732 <SettingsList.Item>
1733 <Admonition type="info" style={[a.flex_1]}>
1734 <Trans>
1735 Override the CDN host for all images. Current:
1736 <InlineLinkText to={imageCdnHost} label={imageCdnHost}>
1737 {imageCdnHost}
1738 </InlineLinkText>
1739 </Trans>
1740 </Admonition>
1741 </SettingsList.Item>
1742
1743 <SettingsList.Divider />
1744
1745 <SettingsList.Item>
1746 <SettingsList.ItemIcon icon={StarIcon} />
1747 <SettingsList.ItemText>
1748 <Trans>{`Custom AppView DID`}</Trans>
1749 </SettingsList.ItemText>
1750 <SettingsList.BadgeButton
1751 label={customAppViewDid ? _(msg`Change`) : _(msg`Set`)}
1752 onPress={() => setCustomAppViewDidControl.open()}
1753 />
1754 </SettingsList.Item>
1755
1756 <SettingsList.Divider />
1757
1758 <SettingsList.Group contentContainerStyle={[a.gap_sm]}>
1759 <SettingsList.ItemIcon icon={RaisingHandIcon} />
1760 <SettingsList.ItemText>
1761 <Trans>Labelers</Trans>
1762 </SettingsList.ItemText>
1763 <Toggle.Item
1764 name="no_app_labelers"
1765 label={_(msg`Do not declare any app labelers`)}
1766 value={noAppLabelers}
1767 onChange={value => setNoAppLabelers(value)}
1768 style={[a.w_full]}>
1769 <Toggle.LabelText style={[a.flex_1]}>
1770 <Trans>Do not declare any default app labelers</Trans>
1771 </Toggle.LabelText>
1772 <Toggle.Platform />
1773 </Toggle.Item>
1774 </SettingsList.Group>
1775
1776 <SettingsList.Item>
1777 <Admonition type="warning" style={[a.flex_1]}>
1778 <Trans>Restart the app after changing this setting.</Trans>
1779 </Admonition>
1780 </SettingsList.Item>
1781 <SettingsList.Item>
1782 <Admonition type="tip" style={[a.flex_1]}>
1783 <Trans>
1784 Some App Views will default to using an app labeler if you have
1785 no labelers, so consider subscribing to at least one labeler if
1786 you have issues.
1787 </Trans>
1788 </Admonition>
1789 </SettingsList.Item>
1790 <SettingsList.Item>
1791 <Admonition type="info" style={[a.flex_1]}>
1792 <Trans>
1793 App labelers are mandatory top-level labelers that can perform
1794 "takedowns". This setting does not influence geolocation-based
1795 labelers.
1796 </Trans>
1797 </Admonition>
1798 </SettingsList.Item>
1799 </SettingsList.Container>
1800 </Layout.Content>
1801 <ConstellationInstanceDialog control={setConstellationInstanceControl} />
1802 <CustomAppViewDidDialog control={setCustomAppViewDidControl} />
1803 <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} />
1804 <LibreTranslateInstanceDialog
1805 control={setLibreTranslateInstanceControl}
1806 />
1807 <ImageCdnHostDialog control={setImageCdnHostControl} />
1808 <PostReplacementDialog control={setPostReplacementDialogControl} />
1809 <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} />
1810 <OpenRouterModelDialog control={setOpenRouterModelControl} />
1811 <OpenRouterPromptDialog control={setOpenRouterPromptControl} />
1812 </Layout.Screen>
1813 )
1814}
1815
1816const styles = {
1817 textInput: {
1818 borderWidth: 1,
1819 borderRadius: 6,
1820 paddingHorizontal: 14,
1821 paddingVertical: 10,
1822 fontSize: 16,
1823 },
1824}