Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

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

feat: add individual Runes & Color Themes settings screens

- revert Moderation label back to Moderation and content filters
- give Runes a new icon and move it to replace Help link
- move Source code link to About page
- move OpenRouter configuration to the Accessibility settings screen (and update that screen's useLingui usage)

+2729 -2463
+9
bskyweb/cmd/bskyweb/server.go
··· 297 297 e.GET("/settings/threads", server.WebGeneric) 298 298 e.GET("/settings/external-embeds", server.WebGeneric) 299 299 e.GET("/settings/accessibility", server.WebGeneric) 300 + e.GET("/settings/runes", server.WebGeneric) 301 + e.GET("/settings/runes/menus", server.WebGeneric) 302 + e.GET("/settings/runes/badges", server.WebGeneric) 303 + e.GET("/settings/runes/impressions", server.WebGeneric) 304 + e.GET("/settings/runes/usability", server.WebGeneric) 305 + e.GET("/settings/runes/display", server.WebGeneric) 306 + e.GET("/settings/runes/infrastructure", server.WebGeneric) 307 + e.GET("/settings/runes/other-additions", server.WebGeneric) 300 308 e.GET("/settings/appearance", server.WebGeneric) 309 + e.GET("/settings/appearance/color-theme", server.WebGeneric) 301 310 e.GET("/settings/account", server.WebGeneric) 302 311 e.GET("/settings/automation-label", server.WebGeneric) 303 312 e.GET("/settings/privacy-and-security", server.WebGeneric)
+51
src/Navigation.tsx
··· 104 104 import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings' 105 105 import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings' 106 106 import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' 107 + import {AppearanceColorThemeSettingsScreen} from '#/screens/Settings/AppearanceSettings/ColorThemeSettings' 107 108 import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' 108 109 import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' 109 110 import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings' ··· 128 129 import {PetLabelSettingsScreen} from '#/screens/Settings/PetLabelSettings' 129 130 import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' 130 131 import {RunesSettingsScreen} from '#/screens/Settings/RunesSettings' 132 + import {RunesBadgesSettingsScreen} from '#/screens/Settings/RunesSettings/BadgesSettings' 133 + import {RunesDisplaySettingsScreen} from '#/screens/Settings/RunesSettings/DisplaySettings' 134 + import {RunesImpressionsSettingsScreen} from '#/screens/Settings/RunesSettings/ImpressionsSettings' 135 + import {RunesInfrastructureSettingsScreen} from '#/screens/Settings/RunesSettings/InfrastructureSettings' 136 + import {RunesMenusSettingsScreen} from '#/screens/Settings/RunesSettings/MenusSettings' 137 + import {RunesOtherAdditionsSettingsScreen} from '#/screens/Settings/RunesSettings/OtherAdditionsSettings' 138 + import {RunesUsabilitySettingsScreen} from '#/screens/Settings/RunesSettings/UsabilitySettings' 131 139 import {SettingsScreen} from '#/screens/Settings/Settings' 132 140 import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' 133 141 import { ··· 425 433 }} 426 434 /> 427 435 <Stack.Screen 436 + name="RunesMenusSettings" 437 + getComponent={() => RunesMenusSettingsScreen} 438 + options={{title: title(msg`Menus`), requireAuth: true}} 439 + /> 440 + <Stack.Screen 441 + name="RunesBadgesSettings" 442 + getComponent={() => RunesBadgesSettingsScreen} 443 + options={{title: title(msg`Badges`), requireAuth: true}} 444 + /> 445 + <Stack.Screen 446 + name="RunesImpressionsSettings" 447 + getComponent={() => RunesImpressionsSettingsScreen} 448 + options={{title: title(msg`Impressions`), requireAuth: true}} 449 + /> 450 + <Stack.Screen 451 + name="RunesUsabilitySettings" 452 + getComponent={() => RunesUsabilitySettingsScreen} 453 + options={{title: title(msg`Usability`), requireAuth: true}} 454 + /> 455 + <Stack.Screen 456 + name="RunesDisplaySettings" 457 + getComponent={() => RunesDisplaySettingsScreen} 458 + options={{title: title(msg`Display`), requireAuth: true}} 459 + /> 460 + <Stack.Screen 461 + name="RunesInfrastructureSettings" 462 + getComponent={() => RunesInfrastructureSettingsScreen} 463 + options={{title: title(msg`Infrastructure`), requireAuth: true}} 464 + /> 465 + <Stack.Screen 466 + name="RunesOtherAdditionsSettings" 467 + getComponent={() => RunesOtherAdditionsSettingsScreen} 468 + options={{title: title(msg`Other additions`), requireAuth: true}} 469 + /> 470 + <Stack.Screen 428 471 name="AppearanceSettings" 429 472 getComponent={() => AppearanceSettingsScreen} 430 473 options={{ 431 474 title: title(msg`Appearance`), 475 + requireAuth: true, 476 + }} 477 + /> 478 + <Stack.Screen 479 + name="AppearanceColorThemeSettings" 480 + getComponent={() => AppearanceColorThemeSettingsScreen} 481 + options={{ 482 + title: title(msg`Color Theme`), 432 483 requireAuth: true, 433 484 }} 434 485 />
+5
src/components/icons/Eclipse.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Eclipse_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + path: 'M 12,2 C 6.4887879,2 2,6.488788 2,12 2,17.511171 6.4887822,22 12,22 17.511177,22 22,17.511177 22,12 22,6.4887823 17.511171,2 12,2 Z m 0.429688,2.0273437 c 4.080562,0.2161476 7.32682,3.4623676 7.542968,7.5429693 -0.966626,0.825301 -2.194066,1.289061 -3.474609,1.289062 -1.421572,0 -2.783842,-0.56314 -3.789063,-1.568359 -1.005221,-1.005223 -1.56836,-2.367409 -1.568359,-3.7890629 0,-1.2805185 0.463778,-2.5080046 1.289063,-3.4746094 z M 9.9199219,4.3066406 C 9.4395095,5.29846 9.1347656,6.3791148 9.1347656,7.5019531 c 0,1.9528738 0.775302,3.8260839 2.1562504,5.2070309 1.380947,1.380948 3.254182,2.156249 5.207031,2.15625 1.122858,0 2.203507,-0.304748 3.195312,-0.785156 C 18.777224,17.485465 15.702321,19.994141 12,19.994141 7.5729806,19.994141 4.0058594,16.426976 4.0058594,12 c 0,-3.7023745 2.5086455,-6.7772417 5.9140625,-7.6933594 z', 5 + })
+8
src/lib/routes/types.ts
··· 50 50 PreferencesExternalEmbeds: undefined 51 51 AccessibilitySettings: undefined 52 52 AppearanceSettings: undefined 53 + AppearanceColorThemeSettings: undefined 53 54 RunesSettings: undefined 55 + RunesMenusSettings: undefined 56 + RunesBadgesSettings: undefined 57 + RunesImpressionsSettings: undefined 58 + RunesUsabilitySettings: undefined 59 + RunesDisplaySettings: undefined 60 + RunesInfrastructureSettings: undefined 61 + RunesOtherAdditionsSettings: undefined 54 62 AccountSettings: undefined 55 63 AutomationLabelSettings: undefined 56 64 PetLabelSettings: undefined
+8
src/routes.ts
··· 49 49 PreferencesExternalEmbeds: '/settings/external-embeds', 50 50 AccessibilitySettings: '/settings/accessibility', 51 51 RunesSettings: '/settings/runes', 52 + RunesMenusSettings: '/settings/runes/menus', 53 + RunesBadgesSettings: '/settings/runes/badges', 54 + RunesImpressionsSettings: '/settings/runes/impressions', 55 + RunesUsabilitySettings: '/settings/runes/usability', 56 + RunesDisplaySettings: '/settings/runes/display', 57 + RunesInfrastructureSettings: '/settings/runes/infrastructure', 58 + RunesOtherAdditionsSettings: '/settings/runes/other-additions', 52 59 AppearanceSettings: '/settings/appearance', 60 + AppearanceColorThemeSettings: '/settings/appearance/color-theme', 53 61 SavedFeeds: '/settings/saved-feeds', 54 62 AccountSettings: '/settings/account', 55 63 AutomationLabelSettings: '/settings/automation-label',
+8 -1
src/screens/Settings/AboutSettings.tsx
··· 8 8 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 9 9 import {useMutation} from '@tanstack/react-query' 10 10 11 - import {STATUS_PAGE_URL} from '#/lib/constants' 11 + import {HELP_DESK_URL, STATUS_PAGE_URL} from '#/lib/constants' 12 12 import {type CommonNavigatorParams} from '#/lib/routes/types' 13 13 import * as SettingsList from '#/screens/Settings/components/SettingsList' 14 14 import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' 15 15 import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/components/icons/BroomSparkle' 16 + import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' 16 17 import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines' 17 18 import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' 18 19 import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper' ··· 100 101 <SettingsList.ItemIcon icon={GlobeIcon} /> 101 102 <SettingsList.ItemText> 102 103 <Trans>Status Page</Trans> 104 + </SettingsList.ItemText> 105 + </SettingsList.LinkItem> 106 + <SettingsList.LinkItem to={HELP_DESK_URL} label={_(msg`Source code`)}> 107 + <SettingsList.ItemIcon icon={CodeBracketsIcon} /> 108 + <SettingsList.ItemText> 109 + <Trans>Source code</Trans> 103 110 </SettingsList.ItemText> 104 111 </SettingsList.LinkItem> 105 112 <SettingsList.Divider />
+320 -8
src/screens/Settings/AccessibilitySettings.tsx
··· 1 - import {msg} from '@lingui/core/macro' 2 - import {useLingui} from '@lingui/react' 3 - import {Trans} from '@lingui/react/macro' 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {Trans, useLingui} from '@lingui/react/macro' 4 4 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 5 6 + import { 7 + DEFAULT_ALT_TEXT_AI_MODEL, 8 + DEFAULT_ALT_TEXT_AI_PROMPT, 9 + } from '#/lib/constants' 10 + import {usePalette} from '#/lib/hooks/usePalette' 6 11 import {type CommonNavigatorParams} from '#/lib/routes/types' 7 12 import { 8 13 useHapticsDisabled, ··· 14 19 useLargeAltBadgeEnabled, 15 20 useSetLargeAltBadgeEnabled, 16 21 } from '#/state/preferences/large-alt-badge' 22 + import { 23 + useOpenRouterApiKey, 24 + useOpenRouterConfigured, 25 + useOpenRouterModel, 26 + useOpenRouterPrompt, 27 + useSetOpenRouterApiKey, 28 + useSetOpenRouterModel, 29 + useSetOpenRouterPrompt, 30 + } from '#/state/preferences/openrouter' 17 31 import * as SettingsList from '#/screens/Settings/components/SettingsList' 18 32 import {atoms as a} from '#/alf' 33 + import {Admonition} from '#/components/Admonition' 34 + import {Button, ButtonText} from '#/components/Button' 35 + import * as Dialog from '#/components/Dialog' 19 36 import * as Toggle from '#/components/forms/Toggle' 20 37 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 21 38 import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' 39 + import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 22 40 import * as Layout from '#/components/Layout' 23 - import {IS_NATIVE} from '#/env' 41 + import {InlineLinkText} from '#/components/Link' 42 + import {Text} from '#/components/Typography' 43 + import {IS_NATIVE, IS_WEB} from '#/env' 24 44 25 45 type Props = NativeStackScreenProps< 26 46 CommonNavigatorParams, 27 47 'AccessibilitySettings' 28 48 > 29 49 export function AccessibilitySettingsScreen({}: Props) { 30 - const {_} = useLingui() 50 + const {t: l} = useLingui() 31 51 32 52 const requireAltTextEnabled = useRequireAltTextEnabled() 33 53 const setRequireAltTextEnabled = useSetRequireAltTextEnabled() ··· 36 56 const largeAltBadgeEnabled = useLargeAltBadgeEnabled() 37 57 const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() 38 58 59 + const setOpenRouterApiKeyControl = Dialog.useDialogControl() 60 + const openRouterConfigured = useOpenRouterConfigured() 61 + const openRouterModel = useOpenRouterModel() 62 + const setOpenRouterModelControl = Dialog.useDialogControl() 63 + const setOpenRouterPromptControl = Dialog.useDialogControl() 64 + 39 65 return ( 40 66 <Layout.Screen> 41 67 <Layout.Header.Outer> ··· 56 82 </SettingsList.ItemText> 57 83 <Toggle.Item 58 84 name="require_alt_text" 59 - label={_(msg`Require alt text before posting`)} 85 + label={l`Require alt text before posting`} 60 86 value={requireAltTextEnabled ?? false} 61 87 onChange={value => setRequireAltTextEnabled(value)} 62 88 style={[a.w_full]}> ··· 67 93 </Toggle.Item> 68 94 <Toggle.Item 69 95 name="large_alt_badge" 70 - label={_(msg`Display larger alt text badges`)} 96 + label={l`Display larger alt text badges`} 71 97 value={!!largeAltBadgeEnabled} 72 98 onChange={value => setLargeAltBadgeEnabled(value)} 73 99 style={[a.w_full]}> ··· 87 113 </SettingsList.ItemText> 88 114 <Toggle.Item 89 115 name="haptics" 90 - label={_(msg`Disable haptic feedback`)} 116 + label={l`Disable haptic feedback`} 91 117 value={hapticsDisabled ?? false} 92 118 onChange={value => setHapticsDisabled(value)} 93 119 style={[a.w_full]}> ··· 99 125 </SettingsList.Group> 100 126 </> 101 127 )} 128 + 129 + <SettingsList.Item> 130 + <SettingsList.ItemIcon icon={BeakerIcon} /> 131 + <SettingsList.ItemText> 132 + <Trans>OpenRouter API Key</Trans> 133 + </SettingsList.ItemText> 134 + <SettingsList.BadgeButton 135 + label={openRouterConfigured ? l`Change` : l`Set`} 136 + onPress={() => setOpenRouterApiKeyControl.open()} 137 + /> 138 + </SettingsList.Item> 139 + 140 + <SettingsList.Item> 141 + <Admonition type="info" style={[a.flex_1]}> 142 + <Trans> 143 + Set your OpenRouter API key to enable AI-powered alt text 144 + generation for images in the composer. Get an API key at{' '} 145 + <InlineLinkText 146 + to="https://openrouter.ai" 147 + label="openrouter.ai"> 148 + openrouter.ai 149 + </InlineLinkText> 150 + </Trans> 151 + </Admonition> 152 + </SettingsList.Item> 153 + 154 + {openRouterConfigured && ( 155 + <SettingsList.Item> 156 + <SettingsList.ItemIcon icon={BeakerIcon} /> 157 + <SettingsList.ItemText> 158 + <Trans>{`OpenRouter Model`}</Trans> 159 + </SettingsList.ItemText> 160 + <SettingsList.BadgeButton 161 + label={l`Change`} 162 + onPress={() => setOpenRouterModelControl.open()} 163 + /> 164 + </SettingsList.Item> 165 + )} 166 + 167 + {openRouterConfigured && ( 168 + <SettingsList.Item> 169 + <Admonition type="info" style={[a.flex_1]}> 170 + <Trans> 171 + Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} 172 + <InlineLinkText 173 + to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular" 174 + label="openrouter.ai"> 175 + Search models 176 + </InlineLinkText> 177 + </Trans> 178 + </Admonition> 179 + </SettingsList.Item> 180 + )} 181 + 182 + {openRouterConfigured && ( 183 + <SettingsList.Item> 184 + <SettingsList.ItemIcon icon={BeakerIcon} /> 185 + <SettingsList.ItemText> 186 + <Trans>Alt Text Prompt</Trans> 187 + </SettingsList.ItemText> 188 + <SettingsList.BadgeButton 189 + label={l`Change`} 190 + onPress={() => setOpenRouterPromptControl.open()} 191 + /> 192 + </SettingsList.Item> 193 + )} 194 + 195 + {openRouterConfigured && ( 196 + <SettingsList.Item> 197 + <Admonition type="info" style={[a.flex_1]}> 198 + <Trans> 199 + Customize the prompt sent to the AI model when generating alt 200 + text. Leave empty to use the default prompt. 201 + </Trans> 202 + </Admonition> 203 + </SettingsList.Item> 204 + )} 205 + 206 + <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} /> 207 + <OpenRouterModelDialog control={setOpenRouterModelControl} /> 208 + <OpenRouterPromptDialog control={setOpenRouterPromptControl} /> 102 209 </SettingsList.Container> 103 210 </Layout.Content> 104 211 </Layout.Screen> 105 212 ) 106 213 } 214 + 215 + function OpenRouterApiKeyDialog({ 216 + control, 217 + }: { 218 + control: Dialog.DialogControlProps 219 + }) { 220 + const pal = usePalette('default') 221 + const {t: l} = useLingui() 222 + 223 + const apiKey = useOpenRouterApiKey() 224 + const [value, setValue] = useState(apiKey ?? '') 225 + const setApiKey = useSetOpenRouterApiKey() 226 + 227 + return ( 228 + <Dialog.Outer 229 + control={control} 230 + nativeOptions={{preventExpansion: true}} 231 + onClose={() => setValue(apiKey ?? '')}> 232 + <Dialog.Handle /> 233 + <Dialog.ScrollableInner label={l`OpenRouter API Key`}> 234 + <View style={[a.gap_sm, a.pb_lg]}> 235 + <Text style={[a.text_2xl, a.font_bold]}> 236 + <Trans>OpenRouter API Key</Trans> 237 + </Text> 238 + </View> 239 + 240 + <View style={a.gap_lg}> 241 + <Dialog.Input 242 + label="API Key" 243 + autoFocus 244 + style={[styles.textInput, pal.border, pal.text]} 245 + onChangeText={setValue} 246 + placeholder="sk-or-..." 247 + placeholderTextColor={pal.colors.textLight} 248 + onSubmitEditing={() => { 249 + setApiKey(value.trim() || undefined) 250 + control.close() 251 + }} 252 + accessibilityHint={l`Enter your OpenRouter API key for AI alt text generation`} 253 + defaultValue={apiKey ?? ''} 254 + secureTextEntry 255 + /> 256 + 257 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 258 + <Button 259 + label={l`Save`} 260 + size="large" 261 + onPress={() => { 262 + setApiKey(value.trim() || undefined) 263 + control.close() 264 + }} 265 + variant="solid" 266 + color="primary"> 267 + <ButtonText> 268 + <Trans>Save</Trans> 269 + </ButtonText> 270 + </Button> 271 + </View> 272 + </View> 273 + 274 + <Dialog.Close /> 275 + </Dialog.ScrollableInner> 276 + </Dialog.Outer> 277 + ) 278 + } 279 + 280 + function OpenRouterModelDialog({ 281 + control, 282 + }: { 283 + control: Dialog.DialogControlProps 284 + }) { 285 + const pal = usePalette('default') 286 + const {t: l} = useLingui() 287 + 288 + const model = useOpenRouterModel() 289 + const [value, setValue] = useState(model ?? '') 290 + const setModel = useSetOpenRouterModel() 291 + 292 + return ( 293 + <Dialog.Outer 294 + control={control} 295 + nativeOptions={{preventExpansion: true}} 296 + onClose={() => setValue(model ?? '')}> 297 + <Dialog.Handle /> 298 + <Dialog.ScrollableInner label={l`OpenRouter Model`}> 299 + <View style={[a.gap_sm, a.pb_lg]}> 300 + <Text style={[a.text_2xl, a.font_bold]}> 301 + <Trans>OpenRouter Model</Trans> 302 + </Text> 303 + </View> 304 + 305 + <View style={a.gap_lg}> 306 + <Dialog.Input 307 + label="Model" 308 + autoFocus 309 + style={[styles.textInput, pal.border, pal.text]} 310 + onChangeText={setValue} 311 + placeholder={DEFAULT_ALT_TEXT_AI_MODEL} 312 + placeholderTextColor={pal.colors.textLight} 313 + onSubmitEditing={() => { 314 + setModel(value.trim() || undefined) 315 + control.close() 316 + }} 317 + accessibilityHint={l`Enter the model ID to use for alt text generation`} 318 + defaultValue={model ?? ''} 319 + /> 320 + 321 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 322 + <Button 323 + label={l`Save`} 324 + size="large" 325 + onPress={() => { 326 + setModel(value.trim() || undefined) 327 + control.close() 328 + }} 329 + variant="solid" 330 + color="primary"> 331 + <ButtonText> 332 + <Trans>Save</Trans> 333 + </ButtonText> 334 + </Button> 335 + </View> 336 + </View> 337 + 338 + <Dialog.Close /> 339 + </Dialog.ScrollableInner> 340 + </Dialog.Outer> 341 + ) 342 + } 343 + 344 + function OpenRouterPromptDialog({ 345 + control, 346 + }: { 347 + control: Dialog.DialogControlProps 348 + }) { 349 + const pal = usePalette('default') 350 + const {t: l} = useLingui() 351 + 352 + const prompt = useOpenRouterPrompt() 353 + const [value, setValue] = useState(prompt ?? '') 354 + const setPrompt = useSetOpenRouterPrompt() 355 + 356 + return ( 357 + <Dialog.Outer 358 + control={control} 359 + nativeOptions={{preventExpansion: true}} 360 + onClose={() => setValue(prompt ?? '')}> 361 + <Dialog.Handle /> 362 + <Dialog.ScrollableInner label={l`Alt Text Prompt`}> 363 + <View style={[a.gap_sm, a.pb_lg]}> 364 + <Text style={[a.text_2xl, a.font_bold]}> 365 + <Trans>Alt Text Prompt</Trans> 366 + </Text> 367 + </View> 368 + 369 + <View style={a.gap_lg}> 370 + <Dialog.Input 371 + label="Prompt" 372 + multiline 373 + numberOfLines={6} 374 + style={[ 375 + styles.textInput, 376 + pal.border, 377 + pal.text, 378 + {minHeight: 120, textAlignVertical: 'top'}, 379 + ]} 380 + onChangeText={setValue} 381 + placeholder={DEFAULT_ALT_TEXT_AI_PROMPT} 382 + placeholderTextColor={pal.colors.textLight} 383 + accessibilityHint={l`Enter a custom prompt for AI alt text generation`} 384 + defaultValue={prompt ?? ''} 385 + /> 386 + 387 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 388 + <Button 389 + label={l`Save`} 390 + size="large" 391 + onPress={() => { 392 + setPrompt(value.trim() || undefined) 393 + control.close() 394 + }} 395 + variant="solid" 396 + color="primary"> 397 + <ButtonText> 398 + <Trans>Save</Trans> 399 + </ButtonText> 400 + </Button> 401 + </View> 402 + </View> 403 + 404 + <Dialog.Close /> 405 + </Dialog.ScrollableInner> 406 + </Dialog.Outer> 407 + ) 408 + } 409 + 410 + const styles = { 411 + textInput: { 412 + borderWidth: 1, 413 + borderRadius: 6, 414 + paddingHorizontal: 14, 415 + paddingVertical: 10, 416 + fontSize: 16, 417 + }, 418 + }
+16 -341
src/screens/Settings/AppearanceSettings.tsx
··· 1 - import {useCallback, useMemo} from 'react' 2 - import {Pressable, View} from 'react-native' 1 + import {useCallback} from 'react' 3 2 import Animated, { 4 3 FadeInUp, 5 4 FadeOutUp, ··· 14 13 type CommonNavigatorParams, 15 14 type NativeStackScreenProps, 16 15 } from '#/lib/routes/types' 17 - import {type Schema} from '#/state/persisted' 18 16 import { 19 17 useEnableSquareAvatars, 20 18 useSetEnableSquareAvatars, ··· 26 24 import {useKawaiiMode, useSetKawaiiMode} from '#/state/preferences/kawaii' 27 25 import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 28 26 import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' 27 + import {ItemTextWithSubtitle} from '#/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle' 29 28 import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' 30 - import { 31 - BLACKSKY_PALETTE, 32 - BLUESKY_PALETTE, 33 - CATPPUCIN_PALETTE, 34 - DEER_PALETTE, 35 - DEFAULT_PALETTE, 36 - EVERGARDEN_PALETTE, 37 - KITTY_PALETTE, 38 - REDDWARF_PALETTE, 39 - ZEPPELIN_PALETTE, 40 - } from '#/alf/themes' 41 - import {getMaterial3Colors} from '#/alf/util/material3Theme' 42 - import {useMaterialYouPalette} from '#/alf/util/materialYou' 43 29 import * as SegmentedControl from '#/components/forms/SegmentedControl' 44 - import {Slider} from '#/components/forms/Slider' 45 30 import * as Toggle from '#/components/forms/Toggle' 46 31 import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as SquareIcon} from '#/components/icons/CircleAndSquare' 47 32 import {ColorPalette_Stroke2_Corner0_Rounded as ColorPaletteIcon} from '#/components/icons/ColorPalette' 48 33 import {type Props as SVGIconProps} from '#/components/icons/common' 49 - import { 50 - Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, 51 - Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, 52 - } from '#/components/icons/Heart2' 53 34 import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' 54 35 import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' 55 36 import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' ··· 58 39 import * as Layout from '#/components/Layout' 59 40 import {Text} from '#/components/Typography' 60 41 import {IS_ANDROID, IS_INTERNAL, IS_NATIVE} from '#/env' 42 + import {getColorSchemeLabel, useColorSchemes} from './AppearanceSettings/shared' 61 43 import * as SettingsList from './components/SettingsList' 62 44 63 45 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'> 64 46 65 - type ColorSchemeName = 66 - | 'witchsky' 67 - | 'bluesky' 68 - | 'blacksky' 69 - | 'deer' 70 - | 'zeppelin' 71 - | 'kitty' 72 - | 'reddwarf' 73 - | 'catppuccin' 74 - | 'evergarden' 75 - | 'material3' 76 - 77 - type ColorSchemeOption = { 78 - name: ColorSchemeName 79 - label: string 80 - primary: string 81 - } 82 - 83 47 export function AppearanceSettingsScreen({}: Props) { 84 48 const {_} = useLingui() 85 49 const {fonts} = useAlf() 86 - const t = useTheme() 87 50 88 - const { 89 - colorMode, 90 - colorScheme, 91 - darkTheme, 92 - hue, 93 - material3Accent, 94 - material3Style, 95 - } = useThemePrefs() 96 - const { 97 - setColorMode, 98 - setColorScheme, 99 - setDarkTheme, 100 - setHue, 101 - setMaterial3Accent, 102 - setMaterial3Style, 103 - } = useSetThemePrefs() 51 + const {colorMode, colorScheme, darkTheme} = useThemePrefs() 52 + const {setColorMode, setDarkTheme} = useSetThemePrefs() 104 53 105 54 const kawaiiMode = useKawaiiMode() 106 55 const setKawaiiMode = useSetKawaiiMode() ··· 111 60 const enableSquareButtons = useEnableSquareButtons() 112 61 const setEnableSquareButtons = useSetEnableSquareButtons() 113 62 114 - const material3Palette = useMaterialYouPalette() 115 - const cachedScheme = useMemo( 116 - () => getMaterial3Colors(material3Palette), 117 - [material3Palette], 118 - ) 63 + const colorSchemes = useColorSchemes() 64 + const colorSchemeLabel = getColorSchemeLabel(colorSchemes, colorScheme) 119 65 120 66 const onChangeAppearance = useCallback( 121 67 (value: 'light' | 'system' | 'dark') => { ··· 124 70 [setColorMode], 125 71 ) 126 72 127 - const onChangeScheme = useCallback( 128 - (value: ColorSchemeName) => { 129 - setColorScheme(value) 130 - }, 131 - [setColorScheme], 132 - ) 133 - 134 73 const onChangeDarkTheme = useCallback( 135 74 (value: 'dim' | 'dark') => { 136 75 setDarkTheme(value) ··· 152 91 [fonts], 153 92 ) 154 93 155 - const colorSchemes: ColorSchemeOption[] = [ 156 - { 157 - name: 'witchsky', 158 - label: _(msg`Witchsky`), 159 - primary: DEFAULT_PALETTE.primary_500, 160 - }, 161 - { 162 - name: 'bluesky', 163 - label: _(msg`Bluesky`), 164 - primary: BLUESKY_PALETTE.primary_500, 165 - }, 166 - { 167 - name: 'blacksky', 168 - label: _(msg`Blacksky`), 169 - primary: BLACKSKY_PALETTE.primary_500, 170 - }, 171 - { 172 - name: 'deer', 173 - label: _(msg`Deer`), 174 - primary: DEER_PALETTE.primary_500, 175 - }, 176 - { 177 - name: 'zeppelin', 178 - label: _(msg`Zeppelin`), 179 - primary: ZEPPELIN_PALETTE.primary_500, 180 - }, 181 - { 182 - name: 'kitty', 183 - label: _(msg`Kitty`), 184 - primary: KITTY_PALETTE.primary_500, 185 - }, 186 - { 187 - name: 'reddwarf', 188 - label: _(msg`Red Dwarf`), 189 - primary: REDDWARF_PALETTE.primary_500, 190 - }, 191 - { 192 - name: 'catppuccin', 193 - label: _(msg`Catppuccin`), 194 - primary: CATPPUCIN_PALETTE.primary_500, 195 - }, 196 - { 197 - name: 'evergarden', 198 - label: _(msg`Evergarden`), 199 - primary: EVERGARDEN_PALETTE.primary_500, 200 - }, 201 - { 202 - name: 'material3', 203 - label: _(msg`Material You`), 204 - primary: cachedScheme.regular.primary_500, 205 - }, 206 - ] 207 - 208 94 return ( 209 95 <LayoutAnimationConfig skipExiting skipEntering> 210 96 <Layout.Screen testID="preferencesThreadsScreen"> ··· 263 149 </Animated.View> 264 150 )} 265 151 266 - <SettingsList.Group> 152 + <SettingsList.LinkItem 153 + to="/settings/appearance/color-theme" 154 + label={_(msg`Color theme settings`)} 155 + contentContainerStyle={[a.align_start]}> 267 156 <SettingsList.ItemIcon icon={ColorPaletteIcon} /> 268 - <SettingsList.ItemText> 269 - <Trans>Color Theme</Trans> 270 - </SettingsList.ItemText> 271 - <View style={[a.w_full, a.gap_md]}> 272 - <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 273 - <Trans>Choose which color scheme to use:</Trans> 274 - </Text> 275 - <ColorSchemeGrid 276 - schemes={colorSchemes} 277 - selectedScheme={colorScheme} 278 - onSchemeChange={onChangeScheme} 279 - /> 280 - {colorScheme === 'material3' && !IS_ANDROID && ( 281 - <> 282 - <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 283 - <Trans>Accent hue:</Trans> 284 - </Text> 285 - <Slider 286 - value={hexToHue(material3Accent)} 287 - onValueChange={v => { 288 - setMaterial3Accent(hueToHex(v)) 289 - setHue(0) 290 - }} 291 - minimumValue={0} 292 - maximumValue={360} 293 - step={1} 294 - debounceFull={true} 295 - /> 296 - 297 - <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 298 - <Trans>Style:</Trans> 299 - </Text> 300 - <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 301 - {MATERIAL3_STYLE_OPTIONS.map(({name, label}) => { 302 - const isSelected = material3Style === name 303 - return ( 304 - <Pressable 305 - accessibilityRole="button" 306 - key={name} 307 - onPress={() => setMaterial3Style(name)} 308 - style={[ 309 - a.flex_1, 310 - a.rounded_sm, 311 - a.align_center, 312 - a.px_sm, 313 - a.py_sm, 314 - a.border, 315 - {minWidth: '22%'}, 316 - { 317 - borderColor: isSelected 318 - ? t.palette.primary_500 319 - : t.atoms.border_contrast_low.borderColor, 320 - borderWidth: 2, 321 - backgroundColor: isSelected 322 - ? t.palette.primary_100 323 - : t.atoms.bg.backgroundColor, 324 - }, 325 - ]}> 326 - <Text 327 - style={[ 328 - a.text_xs, 329 - a.font_bold, 330 - isSelected 331 - ? {color: t.palette.primary_500} 332 - : t.atoms.text, 333 - ]}> 334 - {label} 335 - </Text> 336 - </Pressable> 337 - ) 338 - })} 339 - </View> 340 - </> 341 - )} 342 - {colorScheme !== 'material3' && ( 343 - <> 344 - <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 345 - <Trans>Hue shift the colors:</Trans> 346 - </Text> 347 - <Slider 348 - value={hue} 349 - onValueChange={setHue} 350 - minimumValue={0} 351 - maximumValue={360} 352 - step={1} 353 - debounceFull={true} 354 - /> 355 - </> 356 - )} 357 - </View> 358 - </SettingsList.Group> 157 + <ItemTextWithSubtitle 158 + titleText={<Trans>Color Theme</Trans>} 159 + subtitleText={colorSchemeLabel} 160 + /> 161 + </SettingsList.LinkItem> 359 162 360 163 <Animated.View layout={native(LinearTransition)}> 361 164 <SettingsList.Divider /> ··· 472 275 ) 473 276 } 474 277 475 - function ColorSchemeGrid({ 476 - schemes, 477 - selectedScheme, 478 - onSchemeChange, 479 - }: { 480 - schemes: ColorSchemeOption[] 481 - selectedScheme: ColorSchemeName 482 - onSchemeChange: (scheme: ColorSchemeName) => void 483 - }) { 484 - const t = useTheme() 485 - return ( 486 - <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 487 - {schemes.map(({name, label, primary}) => { 488 - const isSelected = selectedScheme === name 489 - const HeartIcon = isSelected ? HeartIconFilled : HeartIconOutline 490 - return ( 491 - <Pressable 492 - accessibilityRole="button" 493 - key={name} 494 - onPress={() => onSchemeChange(name)} 495 - style={[ 496 - a.flex_1, 497 - a.rounded_md, 498 - a.overflow_hidden, 499 - {minWidth: '30%'}, 500 - a.border, 501 - { 502 - borderColor: isSelected 503 - ? primary 504 - : t.atoms.border_contrast_low.borderColor, 505 - borderWidth: 2, 506 - }, 507 - ]}> 508 - <View 509 - style={[ 510 - a.p_sm, 511 - a.gap_xs, 512 - {backgroundColor: t.atoms.bg.backgroundColor}, 513 - ]}> 514 - <View 515 - style={[ 516 - a.w_full, 517 - a.rounded_xs, 518 - {backgroundColor: primary, height: 24}, 519 - ]} 520 - /> 521 - <View 522 - style={[ 523 - a.flex_row, 524 - a.align_center, 525 - a.justify_center, 526 - a.gap_xs, 527 - ]}> 528 - <Text style={[a.text_sm, a.font_bold, t.atoms.text]}> 529 - {label} 530 - </Text> 531 - <HeartIcon size="xs" style={[{color: primary}]} /> 532 - </View> 533 - </View> 534 - </Pressable> 535 - ) 536 - })} 537 - </View> 538 - ) 539 - } 540 - 541 278 export function AppearanceToggleButtonGroup<T extends string>({ 542 279 title, 543 280 description, ··· 593 330 </> 594 331 ) 595 332 } 596 - 597 - const MATERIAL3_STYLE_OPTIONS: { 598 - name: Schema['material3Style'] 599 - label: string 600 - }[] = [ 601 - {name: 'TONAL_SPOT', label: 'Tonal Spot'}, 602 - {name: 'VIBRANT', label: 'Vibrant'}, 603 - {name: 'EXPRESSIVE', label: 'Expressive'}, 604 - {name: 'SPRITZ', label: 'Spritz'}, 605 - {name: 'RAINBOW', label: 'Rainbow'}, 606 - {name: 'FRUIT_SALAD', label: 'Fruit Salad'}, 607 - {name: 'CONTENT', label: 'Content'}, 608 - {name: 'MONOCHROMATIC', label: 'Mono'}, 609 - ] 610 - 611 - function hueToHex(hue: number): string { 612 - const h = hue / 60 613 - const x = 1 - Math.abs((h % 2) - 1) 614 - let r = 0, 615 - g = 0, 616 - b = 0 617 - if (h < 1) { 618 - r = 1 619 - g = x 620 - } else if (h < 2) { 621 - r = x 622 - g = 1 623 - } else if (h < 3) { 624 - g = 1 625 - b = x 626 - } else if (h < 4) { 627 - g = x 628 - b = 1 629 - } else if (h < 5) { 630 - r = x 631 - b = 1 632 - } else { 633 - r = 1 634 - b = x 635 - } 636 - const toHex = (v: number) => 637 - Math.round(v * 255) 638 - .toString(16) 639 - .padStart(2, '0') 640 - return `#${toHex(r)}${toHex(g)}${toHex(b)}` 641 - } 642 - 643 - function hexToHue(hex: string): number { 644 - const r = parseInt(hex.slice(1, 3), 16) / 255 645 - const g = parseInt(hex.slice(3, 5), 16) / 255 646 - const b = parseInt(hex.slice(5, 7), 16) / 255 647 - const max = Math.max(r, g, b) 648 - const min = Math.min(r, g, b) 649 - const d = max - min 650 - if (d === 0) return 0 651 - let h = 0 652 - if (max === r) h = ((g - b) / d) % 6 653 - else if (max === g) h = (b - r) / d + 2 654 - else h = (r - g) / d + 4 655 - h = Math.round(h * 60) 656 - return h < 0 ? h + 360 : h 657 - }
+144
src/screens/Settings/AppearanceSettings/ColorThemeSettings.tsx
··· 1 + import {useCallback} from 'react' 2 + import {Pressable, View} from 'react-native' 3 + import {Trans} from '@lingui/react/macro' 4 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 5 + 6 + import {type CommonNavigatorParams} from '#/lib/routes/types' 7 + import {useSetThemePrefs, useThemePrefs} from '#/state/shell' 8 + import {atoms as a, useTheme} from '#/alf' 9 + import {Slider} from '#/components/forms/Slider' 10 + import * as Layout from '#/components/Layout' 11 + import {Text} from '#/components/Typography' 12 + import {IS_ANDROID} from '#/env' 13 + import * as SettingsList from '../components/SettingsList' 14 + import { 15 + ColorSchemeGrid, 16 + type ColorSchemeName, 17 + hexToHue, 18 + hueToHex, 19 + MATERIAL3_STYLE_OPTIONS, 20 + useColorSchemes, 21 + } from './shared' 22 + 23 + type Props = NativeStackScreenProps< 24 + CommonNavigatorParams, 25 + 'AppearanceColorThemeSettings' 26 + > 27 + 28 + export function AppearanceColorThemeSettingsScreen({}: Props) { 29 + const t = useTheme() 30 + const colorSchemes = useColorSchemes() 31 + 32 + const {colorScheme, hue, material3Accent, material3Style} = useThemePrefs() 33 + const {setColorScheme, setHue, setMaterial3Accent, setMaterial3Style} = 34 + useSetThemePrefs() 35 + 36 + const onChangeScheme = useCallback( 37 + (value: ColorSchemeName) => { 38 + setColorScheme(value) 39 + }, 40 + [setColorScheme], 41 + ) 42 + 43 + return ( 44 + <Layout.Screen> 45 + <Layout.Header.Outer> 46 + <Layout.Header.BackButton /> 47 + <Layout.Header.Content> 48 + <Layout.Header.TitleText> 49 + <Trans>Color Theme</Trans> 50 + </Layout.Header.TitleText> 51 + </Layout.Header.Content> 52 + <Layout.Header.Slot /> 53 + </Layout.Header.Outer> 54 + <Layout.Content> 55 + <SettingsList.Container> 56 + <View style={[a.gap_md, a.px_lg, a.py_sm]}> 57 + <ColorSchemeGrid 58 + schemes={colorSchemes} 59 + selectedScheme={colorScheme} 60 + onSchemeChange={onChangeScheme} 61 + /> 62 + {colorScheme === 'material3' && !IS_ANDROID ? ( 63 + <> 64 + <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 65 + <Trans>Accent hue:</Trans> 66 + </Text> 67 + <Slider 68 + value={hexToHue(material3Accent)} 69 + onValueChange={value => { 70 + setMaterial3Accent(hueToHex(value)) 71 + setHue(0) 72 + }} 73 + minimumValue={0} 74 + maximumValue={360} 75 + step={1} 76 + debounceFull={true} 77 + /> 78 + 79 + <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 80 + <Trans>Style:</Trans> 81 + </Text> 82 + <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 83 + {MATERIAL3_STYLE_OPTIONS.map(({name, label}) => { 84 + const isSelected = material3Style === name 85 + 86 + return ( 87 + <Pressable 88 + accessibilityRole="button" 89 + key={name} 90 + onPress={() => setMaterial3Style(name)} 91 + style={[ 92 + a.flex_1, 93 + a.rounded_sm, 94 + a.align_center, 95 + a.px_sm, 96 + a.py_sm, 97 + a.border, 98 + {minWidth: '22%'}, 99 + { 100 + borderColor: isSelected 101 + ? t.palette.primary_500 102 + : t.atoms.border_contrast_low.borderColor, 103 + borderWidth: 2, 104 + backgroundColor: isSelected 105 + ? t.palette.primary_100 106 + : t.atoms.bg.backgroundColor, 107 + }, 108 + ]}> 109 + <Text 110 + style={[ 111 + a.text_xs, 112 + a.font_bold, 113 + isSelected 114 + ? {color: t.palette.primary_500} 115 + : t.atoms.text, 116 + ]}> 117 + {label} 118 + </Text> 119 + </Pressable> 120 + ) 121 + })} 122 + </View> 123 + </> 124 + ) : ( 125 + <> 126 + <Text style={[a.flex_1, t.atoms.text_contrast_medium]}> 127 + <Trans>Hue shift the colors:</Trans> 128 + </Text> 129 + <Slider 130 + value={hue} 131 + onValueChange={setHue} 132 + minimumValue={0} 133 + maximumValue={360} 134 + step={1} 135 + debounceFull={true} 136 + /> 137 + </> 138 + )} 139 + </View> 140 + </SettingsList.Container> 141 + </Layout.Content> 142 + </Layout.Screen> 143 + ) 144 + }
+252
src/screens/Settings/AppearanceSettings/shared.tsx
··· 1 + import {useMemo} from 'react' 2 + import {Pressable, View} from 'react-native' 3 + import {msg} from '@lingui/core/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {type Schema} from '#/state/persisted' 7 + import {atoms as a, useTheme} from '#/alf' 8 + import { 9 + BLACKSKY_PALETTE, 10 + BLUESKY_PALETTE, 11 + CATPPUCIN_PALETTE, 12 + DEER_PALETTE, 13 + DEFAULT_PALETTE, 14 + EVERGARDEN_PALETTE, 15 + KITTY_PALETTE, 16 + REDDWARF_PALETTE, 17 + ZEPPELIN_PALETTE, 18 + } from '#/alf/themes' 19 + import {getMaterial3Colors} from '#/alf/util/material3Theme' 20 + import {useMaterialYouPalette} from '#/alf/util/materialYou' 21 + import { 22 + Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, 23 + Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, 24 + } from '#/components/icons/Heart2' 25 + import {Text} from '#/components/Typography' 26 + 27 + export type ColorSchemeName = 28 + | 'witchsky' 29 + | 'bluesky' 30 + | 'blacksky' 31 + | 'deer' 32 + | 'zeppelin' 33 + | 'kitty' 34 + | 'reddwarf' 35 + | 'catppuccin' 36 + | 'evergarden' 37 + | 'material3' 38 + 39 + export type ColorSchemeOption = { 40 + name: ColorSchemeName 41 + label: string 42 + primary: string 43 + } 44 + 45 + export function useColorSchemes() { 46 + const {_} = useLingui() 47 + const material3Palette = useMaterialYouPalette() 48 + const cachedScheme = useMemo( 49 + () => getMaterial3Colors(material3Palette), 50 + [material3Palette], 51 + ) 52 + 53 + return useMemo<ColorSchemeOption[]>( 54 + () => [ 55 + { 56 + name: 'witchsky', 57 + label: _(msg`Witchsky`), 58 + primary: DEFAULT_PALETTE.primary_500, 59 + }, 60 + { 61 + name: 'bluesky', 62 + label: _(msg`Bluesky`), 63 + primary: BLUESKY_PALETTE.primary_500, 64 + }, 65 + { 66 + name: 'blacksky', 67 + label: _(msg`Blacksky`), 68 + primary: BLACKSKY_PALETTE.primary_500, 69 + }, 70 + { 71 + name: 'deer', 72 + label: _(msg`Deer`), 73 + primary: DEER_PALETTE.primary_500, 74 + }, 75 + { 76 + name: 'zeppelin', 77 + label: _(msg`Zeppelin`), 78 + primary: ZEPPELIN_PALETTE.primary_500, 79 + }, 80 + { 81 + name: 'kitty', 82 + label: _(msg`Kitty`), 83 + primary: KITTY_PALETTE.primary_500, 84 + }, 85 + { 86 + name: 'reddwarf', 87 + label: _(msg`Red Dwarf`), 88 + primary: REDDWARF_PALETTE.primary_500, 89 + }, 90 + { 91 + name: 'catppuccin', 92 + label: _(msg`Catppuccin`), 93 + primary: CATPPUCIN_PALETTE.primary_500, 94 + }, 95 + { 96 + name: 'evergarden', 97 + label: _(msg`Evergarden`), 98 + primary: EVERGARDEN_PALETTE.primary_500, 99 + }, 100 + { 101 + name: 'material3', 102 + label: _(msg`Material You`), 103 + primary: cachedScheme.regular.primary_500, 104 + }, 105 + ], 106 + [_, cachedScheme], 107 + ) 108 + } 109 + 110 + export function getColorSchemeLabel( 111 + colorSchemes: ColorSchemeOption[], 112 + colorScheme: ColorSchemeName, 113 + ) { 114 + return ( 115 + colorSchemes.find(scheme => scheme.name === colorScheme)?.label ?? 116 + colorScheme 117 + ) 118 + } 119 + 120 + export function ColorSchemeGrid({ 121 + schemes, 122 + selectedScheme, 123 + onSchemeChange, 124 + }: { 125 + schemes: ColorSchemeOption[] 126 + selectedScheme: ColorSchemeName 127 + onSchemeChange: (scheme: ColorSchemeName) => void 128 + }) { 129 + const t = useTheme() 130 + 131 + return ( 132 + <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 133 + {schemes.map(({name, label, primary}) => { 134 + const isSelected = selectedScheme === name 135 + const HeartIcon = isSelected ? HeartIconFilled : HeartIconOutline 136 + 137 + return ( 138 + <Pressable 139 + accessibilityRole="button" 140 + key={name} 141 + onPress={() => onSchemeChange(name)} 142 + style={[ 143 + a.flex_1, 144 + a.rounded_md, 145 + a.overflow_hidden, 146 + {minWidth: '30%'}, 147 + a.border, 148 + { 149 + borderColor: isSelected 150 + ? primary 151 + : t.atoms.border_contrast_low.borderColor, 152 + borderWidth: 2, 153 + }, 154 + ]}> 155 + <View 156 + style={[ 157 + a.p_sm, 158 + a.gap_xs, 159 + {backgroundColor: t.atoms.bg.backgroundColor}, 160 + ]}> 161 + <View 162 + style={[ 163 + a.w_full, 164 + a.rounded_xs, 165 + {backgroundColor: primary, height: 24}, 166 + ]} 167 + /> 168 + <View 169 + style={[ 170 + a.flex_row, 171 + a.align_center, 172 + a.justify_center, 173 + a.gap_xs, 174 + ]}> 175 + <Text style={[a.text_sm, a.font_bold, t.atoms.text]}> 176 + {label} 177 + </Text> 178 + <HeartIcon size="xs" style={[{color: primary}]} /> 179 + </View> 180 + </View> 181 + </Pressable> 182 + ) 183 + })} 184 + </View> 185 + ) 186 + } 187 + 188 + export const MATERIAL3_STYLE_OPTIONS: { 189 + name: Schema['material3Style'] 190 + label: string 191 + }[] = [ 192 + {name: 'TONAL_SPOT', label: 'Tonal Spot'}, 193 + {name: 'VIBRANT', label: 'Vibrant'}, 194 + {name: 'EXPRESSIVE', label: 'Expressive'}, 195 + {name: 'SPRITZ', label: 'Spritz'}, 196 + {name: 'RAINBOW', label: 'Rainbow'}, 197 + {name: 'FRUIT_SALAD', label: 'Fruit Salad'}, 198 + {name: 'CONTENT', label: 'Content'}, 199 + {name: 'MONOCHROMATIC', label: 'Mono'}, 200 + ] 201 + 202 + export function hueToHex(hue: number): string { 203 + const h = hue / 60 204 + const x = 1 - Math.abs((h % 2) - 1) 205 + let r = 0, 206 + g = 0, 207 + b = 0 208 + if (h < 1) { 209 + r = 1 210 + g = x 211 + } else if (h < 2) { 212 + r = x 213 + g = 1 214 + } else if (h < 3) { 215 + g = 1 216 + b = x 217 + } else if (h < 4) { 218 + g = x 219 + b = 1 220 + } else if (h < 5) { 221 + r = x 222 + b = 1 223 + } else { 224 + r = 1 225 + b = x 226 + } 227 + 228 + const toHex = (v: number) => 229 + Math.round(v * 255) 230 + .toString(16) 231 + .padStart(2, '0') 232 + return `#${toHex(r)}${toHex(g)}${toHex(b)}` 233 + } 234 + 235 + export function hexToHue(hex: string): number { 236 + const r = parseInt(hex.slice(1, 3), 16) / 255 237 + const g = parseInt(hex.slice(3, 5), 16) / 255 238 + const b = parseInt(hex.slice(5, 7), 16) / 255 239 + const max = Math.max(r, g, b) 240 + const min = Math.min(r, g, b) 241 + const d = max - min 242 + 243 + if (d === 0) return 0 244 + 245 + let h = 0 246 + if (max === r) h = ((g - b) / d) % 6 247 + else if (max === g) h = (b - r) / d + 2 248 + else h = (r - g) / d + 4 249 + 250 + h = Math.round(h * 60) 251 + return h < 0 ? h + 360 : h 252 + }
-2094
src/screens/Settings/RunesSettings.tsx
··· 1 - import {useState} from 'react' 2 - import {View} from 'react-native' 3 - import {isDid} from '@atproto/api' 4 - import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 5 - import {msg} from '@lingui/core/macro' 6 - import {useLingui} from '@lingui/react' 7 - import {Trans} from '@lingui/react/macro' 8 - import {type NativeStackScreenProps} from '@react-navigation/native-stack' 9 - 10 - import { 11 - APPVIEW_DID_PROXY, 12 - DEFAULT_ALT_TEXT_AI_MODEL, 13 - DEFAULT_ALT_TEXT_AI_PROMPT, 14 - } from '#/lib/constants' 15 - import {usePalette} from '#/lib/hooks/usePalette' 16 - import {type CommonNavigatorParams} from '#/lib/routes/types' 17 - import {dynamicActivate} from '#/locale/i18n' 18 - import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web' 19 - import {type AppLanguage} from '#/locale/languages' 20 - import * as persisted from '#/state/persisted' 21 - import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' 22 - import { 23 - useConstellationInstance, 24 - useSetConstellationInstance, 25 - } from '#/state/preferences/constellation-instance' 26 - import { 27 - useCustomAppViewDid, 28 - useSetCustomAppViewDid, 29 - } from '#/state/preferences/custom-appview-did' 30 - import { 31 - useDeerVerificationEnabled, 32 - useDeerVerificationTrusted, 33 - useSetDeerVerificationEnabled, 34 - } from '#/state/preferences/deer-verification' 35 - import { 36 - useDirectFetchRecords, 37 - useSetDirectFetchRecords, 38 - } from '#/state/preferences/direct-fetch-records' 39 - import { 40 - useDisableComposerPrompt, 41 - useSetDisableComposerPrompt, 42 - } from '#/state/preferences/disable-composer-prompt' 43 - import { 44 - useDisableFollowedByMetrics, 45 - useSetDisableFollowedByMetrics, 46 - } from '#/state/preferences/disable-followed-by-metrics' 47 - import { 48 - useDisableFollowersMetrics, 49 - useSetDisableFollowersMetrics, 50 - } from '#/state/preferences/disable-followers-metrics' 51 - import { 52 - useDisableFollowingMetrics, 53 - useSetDisableFollowingMetrics, 54 - } from '#/state/preferences/disable-following-metrics' 55 - import { 56 - useDisableLikesMetrics, 57 - useSetDisableLikesMetrics, 58 - } from '#/state/preferences/disable-likes-metrics' 59 - import { 60 - useDisablePostsMetrics, 61 - useSetDisablePostsMetrics, 62 - } from '#/state/preferences/disable-posts-metrics' 63 - import { 64 - useDisableQuotesMetrics, 65 - useSetDisableQuotesMetrics, 66 - } from '#/state/preferences/disable-quotes-metrics' 67 - import { 68 - useDisableReplyMetrics, 69 - useSetDisableReplyMetrics, 70 - } from '#/state/preferences/disable-reply-metrics' 71 - import { 72 - useDisableRepostsMetrics, 73 - useSetDisableRepostsMetrics, 74 - } from '#/state/preferences/disable-reposts-metrics' 75 - import { 76 - useDisableSavesMetrics, 77 - useSetDisableSavesMetrics, 78 - } from '#/state/preferences/disable-saves-metrics' 79 - import { 80 - useDisableVerifyEmailReminder, 81 - useSetDisableVerifyEmailReminder, 82 - } from '#/state/preferences/disable-verify-email-reminder' 83 - import { 84 - useDisableViaRepostNotification, 85 - useSetDisableViaRepostNotification, 86 - } from '#/state/preferences/disable-via-repost-notification' 87 - import { 88 - useDiscoverContextEnabled, 89 - useSetDiscoverContextEnabled, 90 - } from '#/state/preferences/discover-context-enabled' 91 - import { 92 - useSetShowExternalShareButtons, 93 - useShowExternalShareButtons, 94 - } from '#/state/preferences/external-share-buttons' 95 - import { 96 - useFaviconService, 97 - useSetFaviconService, 98 - } from '#/state/preferences/favicon-service' 99 - import { 100 - useHideFeedsPromoTab, 101 - useSetHideFeedsPromoTab, 102 - } from '#/state/preferences/hide-feeds-promo-tab' 103 - import { 104 - useHideSimilarAccountsRecomm, 105 - useSetHideSimilarAccountsRecomm, 106 - } from '#/state/preferences/hide-similar-accounts-recommendations' 107 - import { 108 - useHideUnreplyablePosts, 109 - useSetHideUnreplyablePosts, 110 - } from '#/state/preferences/hide-unreplyable-posts' 111 - import { 112 - useHighQualityImages, 113 - useSetHighQualityImages, 114 - } from '#/state/preferences/high-quality-images' 115 - import { 116 - useImageCdnHost, 117 - useSetImageCdnHost, 118 - } from '#/state/preferences/image-cdn-host' 119 - import {useModerationOpts} from '#/state/preferences/moderation-opts' 120 - import { 121 - useNoAppLabelers, 122 - useSetNoAppLabelers, 123 - } from '#/state/preferences/no-app-labelers' 124 - import { 125 - useNoDiscoverFallback, 126 - useSetNoDiscoverFallback, 127 - } from '#/state/preferences/no-discover-fallback' 128 - import { 129 - useOpenRouterApiKey, 130 - useOpenRouterConfigured, 131 - useOpenRouterModel, 132 - useOpenRouterPrompt, 133 - useSetOpenRouterApiKey, 134 - useSetOpenRouterModel, 135 - useSetOpenRouterPrompt, 136 - } from '#/state/preferences/openrouter' 137 - import { 138 - usePdsLabelEnabled, 139 - usePdsLabelHideBskyPds, 140 - useSetPdsLabelEnabled, 141 - useSetPdsLabelHideBskyPds, 142 - } from '#/state/preferences/pds-label' 143 - import { 144 - usePlcDirectory, 145 - useSetPlcDirectory, 146 - } from '#/state/preferences/plc-directory' 147 - import { 148 - usePostReplacement, 149 - useSetPostReplacement, 150 - } from '#/state/preferences/post-name-replacement' 151 - import { 152 - useRepostCarouselEnabled, 153 - useSetRepostCarouselEnabled, 154 - } from '#/state/preferences/repost-carousel-enabled' 155 - import { 156 - useSetShowFollowsYouBadge, 157 - useShowFollowsYouBadge, 158 - } from '#/state/preferences/show-follows-you-badge' 159 - import { 160 - useSetShowLinkInHandle, 161 - useShowLinkInHandle, 162 - } from '#/state/preferences/show-link-in-handle.tsx' 163 - import { 164 - useSetShowLinkInHandleOnlyOnWorkingLinks, 165 - useShowLinkInHandleOnlyOnWorkingLinks, 166 - } from '#/state/preferences/show-link-in-handle-only-on-working-links' 167 - import { 168 - useLibreTranslateInstance, 169 - useSetLibreTranslateInstance, 170 - useSetTranslationServicePreference, 171 - useTranslationServicePreference, 172 - } from '#/state/preferences/translation-service-preference' 173 - import { 174 - useHandleInLinks, 175 - useSetHandleInLinks, 176 - } from '#/state/preferences/use-handle-in-links' 177 - import {useProfilesQuery} from '#/state/queries/profile' 178 - import {findService, useDidDocument} from '#/state/queries/resolve-identity' 179 - import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 180 - import * as SettingsList from '#/screens/Settings/components/SettingsList' 181 - import {atoms as a, useBreakpoints} from '#/alf' 182 - import {Admonition} from '#/components/Admonition' 183 - import {Button, ButtonText} from '#/components/Button' 184 - import * as Dialog from '#/components/Dialog' 185 - import * as Toggle from '#/components/forms/Toggle' 186 - import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' 187 - import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 188 - import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 189 - import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 190 - import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 191 - import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 192 - import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' 193 - import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' 194 - import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 195 - import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 196 - import * as Layout from '#/components/Layout' 197 - import {InlineLinkText} from '#/components/Link' 198 - import {Text} from '#/components/Typography' 199 - import {IS_WEB} from '#/env' 200 - import { 201 - useAutoLikeOnRepost, 202 - useSetAutoLikeOnRepost, 203 - } from '../../state/preferences/auto-like-on-repost.tsx' 204 - import {SearchProfileCard} from '../Search/components/SearchProfileCard' 205 - 206 - type Props = NativeStackScreenProps<CommonNavigatorParams> 207 - 208 - function ConstellationInstanceDialog({ 209 - control, 210 - }: { 211 - control: Dialog.DialogControlProps 212 - }) { 213 - const pal = usePalette('default') 214 - const {_} = useLingui() 215 - 216 - const constellationInstance = useConstellationInstance() 217 - const [url, setUrl] = useState(constellationInstance ?? '') 218 - const setConstellationInstance = useSetConstellationInstance() 219 - 220 - const submit = () => { 221 - setConstellationInstance(url) 222 - control.close() 223 - } 224 - 225 - const shouldDisable = () => { 226 - try { 227 - return !new URL(url).hostname.includes('.') 228 - } catch (e) { 229 - return true 230 - } 231 - } 232 - 233 - return ( 234 - <Dialog.Outer 235 - control={control} 236 - nativeOptions={{preventExpansion: true}} 237 - onClose={() => setUrl(constellationInstance ?? '')}> 238 - <Dialog.Handle /> 239 - <Dialog.ScrollableInner label={_(msg`Constellations instance URL`)}> 240 - <View style={[a.gap_sm, a.pb_lg]}> 241 - <Text style={[a.text_2xl, a.font_bold]}> 242 - <Trans>Constellations instance URL</Trans> 243 - </Text> 244 - </View> 245 - 246 - <View style={a.gap_lg}> 247 - <Dialog.Input 248 - label="Text input field" 249 - autoFocus 250 - style={[styles.textInput, pal.border, pal.text]} 251 - onChangeText={value => { 252 - setUrl(value) 253 - }} 254 - placeholder={persisted.defaults.constellationInstance} 255 - placeholderTextColor={pal.colors.textLight} 256 - onSubmitEditing={submit} 257 - accessibilityHint={_( 258 - msg`Input the url of the constellations instance to use`, 259 - )} 260 - defaultValue={constellationInstance} 261 - /> 262 - 263 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 264 - <Button 265 - label={_(msg`Save`)} 266 - size="large" 267 - onPress={() => void submit()} 268 - variant="solid" 269 - color="primary" 270 - disabled={shouldDisable()}> 271 - <ButtonText> 272 - <Trans>Save</Trans> 273 - </ButtonText> 274 - </Button> 275 - </View> 276 - </View> 277 - 278 - <Dialog.Close /> 279 - </Dialog.ScrollableInner> 280 - </Dialog.Outer> 281 - ) 282 - } 283 - 284 - function CustomAppViewDidDialog({ 285 - control, 286 - }: { 287 - control: Dialog.DialogControlProps 288 - }) { 289 - const pal = usePalette('default') 290 - const {_} = useLingui() 291 - 292 - const [customAppViewDid] = useCustomAppViewDid() 293 - const [did, setDid] = useState(customAppViewDid ?? '') 294 - const setCustomAppViewDid = useSetCustomAppViewDid() 295 - 296 - const doc = useDidDocument({did}) 297 - const bskyAppViewService = 298 - doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') 299 - 300 - const submit = () => { 301 - if (did.length === 0) { 302 - control.close(() => { 303 - setCustomAppViewDid(undefined) 304 - }) 305 - return 306 - } 307 - if (!bskyAppViewService?.serviceEndpoint) return 308 - control.close(() => { 309 - setCustomAppViewDid(did) 310 - }) 311 - } 312 - 313 - return ( 314 - <Dialog.Outer 315 - control={control} 316 - nativeOptions={{preventExpansion: true}} 317 - onClose={() => setDid(customAppViewDid ?? '')}> 318 - <Dialog.Handle /> 319 - <Dialog.ScrollableInner label={_(msg`Custom AppView Proxy DID`)}> 320 - <View style={[a.gap_sm, a.pb_lg]}> 321 - <Text style={[a.text_2xl, a.font_bold]}> 322 - <Trans>Custom AppView Proxy DID</Trans> 323 - </Text> 324 - </View> 325 - 326 - <View style={a.gap_lg}> 327 - <Dialog.Input 328 - label="Text input field" 329 - autoFocus 330 - style={[styles.textInput, pal.border, pal.text]} 331 - onChangeText={value => { 332 - setDid(value) 333 - }} 334 - placeholder={ 335 - APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) || 336 - `did:web:api.bsky.app` 337 - } 338 - placeholderTextColor={pal.colors.textLight} 339 - onSubmitEditing={submit} 340 - accessibilityHint={_( 341 - msg`Input the DID of the AppView to proxy requests through`, 342 - )} 343 - isInvalid={ 344 - !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading 345 - } 346 - defaultValue={customAppViewDid ?? ''} 347 - /> 348 - 349 - {did && !isDid(did) && ( 350 - <View> 351 - <ErrorMessage message={_(msg`must enter a DID`)} /> 352 - </View> 353 - )} 354 - 355 - {did && (did.includes('#') || did.includes('?')) && ( 356 - <View> 357 - <ErrorMessage message={_(msg`don't include the service id`)} /> 358 - </View> 359 - )} 360 - 361 - {doc.isError && ( 362 - <View> 363 - <ErrorMessage 364 - message={ 365 - doc.error.message || _(msg`document resolution failure`) 366 - } 367 - /> 368 - </View> 369 - )} 370 - 371 - {doc.data && 372 - !bskyAppViewService && 373 - (doc.data as {message?: string}).message && ( 374 - <View> 375 - <ErrorMessage 376 - message={(doc.data as {message: string}).message} 377 - /> 378 - </View> 379 - )} 380 - 381 - {doc.data && !bskyAppViewService && ( 382 - <View> 383 - <ErrorMessage 384 - message={_(msg`document doesn't contain #bsky_appview service`)} 385 - /> 386 - </View> 387 - )} 388 - 389 - {bskyAppViewService && ( 390 - <Text style={[a.text_sm, a.leading_snug]}> 391 - {JSON.stringify(bskyAppViewService, null, 2)} 392 - </Text> 393 - )} 394 - 395 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 396 - <Button 397 - label={_(msg`Save`)} 398 - size="large" 399 - onPress={() => void submit()} 400 - variant="solid" 401 - color={did.length > 0 ? 'primary' : 'secondary'} 402 - disabled={ 403 - did.length !== 0 && !bskyAppViewService?.serviceEndpoint 404 - }> 405 - <ButtonText> 406 - {did.length > 0 ? <Trans>Save</Trans> : <Trans>Reset</Trans>} 407 - </ButtonText> 408 - </Button> 409 - </View> 410 - </View> 411 - 412 - <Dialog.Close /> 413 - </Dialog.ScrollableInner> 414 - </Dialog.Outer> 415 - ) 416 - } 417 - 418 - function FaviconServiceDialog({control}: {control: Dialog.DialogControlProps}) { 419 - const pal = usePalette('default') 420 - const {_} = useLingui() 421 - 422 - const faviconService = useFaviconService() 423 - const [url, setUrl] = useState(faviconService ?? '') 424 - const [inputVersion, setInputVersion] = useState(0) 425 - const setFaviconService = useSetFaviconService() 426 - 427 - const updateInputValue = (nextUrl: string) => { 428 - setUrl(nextUrl) 429 - setInputVersion(v => v + 1) 430 - } 431 - 432 - const submit = () => { 433 - setFaviconService(url.trim()) 434 - control.close() 435 - } 436 - 437 - const shouldDisable = () => { 438 - return url.length > 0 && !url.includes('(pds)') 439 - } 440 - 441 - const presets = [ 442 - 'https://twenty-icons.com/(pds)', 443 - 'https://favicon.im/(pds)?larger=true&throw-error-on-404=true', 444 - 'https://favicon.blueat.net/(pds)?larger=true&throw-error-on-404=true', 445 - ] 446 - 447 - return ( 448 - <Dialog.Outer 449 - control={control} 450 - nativeOptions={{preventExpansion: true}} 451 - onClose={() => updateInputValue(faviconService ?? '')}> 452 - <Dialog.Handle /> 453 - <Dialog.ScrollableInner label={_(msg`Favicon Service URL`)}> 454 - <View style={[a.gap_sm, a.pb_lg]}> 455 - <Text style={[a.text_2xl, a.font_bold]}> 456 - <Trans>Favicon Service URL</Trans> 457 - </Text> 458 - <Text style={[a.text_sm, {color: pal.colors.textLight}]}> 459 - <Trans> 460 - (pds) is replaced with the domain of an account's host. 461 - </Trans> 462 - </Text> 463 - </View> 464 - 465 - <View style={a.gap_lg}> 466 - <Dialog.Input 467 - key={`favicon-service-input-${inputVersion}`} 468 - label="Text input field" 469 - autoFocus 470 - style={[styles.textInput, pal.border, pal.text]} 471 - onChangeText={value => setUrl(value)} 472 - placeholder={persisted.defaults.faviconService} 473 - placeholderTextColor={pal.colors.textLight} 474 - onSubmitEditing={submit} 475 - accessibilityHint={_( 476 - msg`Enter the favicon service URL with (pds) as placeholder`, 477 - )} 478 - defaultValue={url} 479 - /> 480 - 481 - <View style={[a.flex_row, a.flex_wrap, a.mb_xs]}> 482 - {presets.map(preset => ( 483 - <Button 484 - key={preset} 485 - variant="ghost" 486 - color="primary" 487 - label={preset} 488 - style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} 489 - onPress={() => updateInputValue(preset)}> 490 - <ButtonText>{preset}</ButtonText> 491 - </Button> 492 - ))} 493 - </View> 494 - 495 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 496 - <Button 497 - label={_(msg`Save`)} 498 - size="large" 499 - onPress={() => void submit()} 500 - variant="solid" 501 - color="primary" 502 - disabled={shouldDisable()}> 503 - <ButtonText> 504 - <Trans>Save</Trans> 505 - </ButtonText> 506 - </Button> 507 - </View> 508 - </View> 509 - 510 - <Dialog.Close /> 511 - </Dialog.ScrollableInner> 512 - </Dialog.Outer> 513 - ) 514 - } 515 - 516 - function LibreTranslateInstanceDialog({ 517 - control, 518 - }: { 519 - control: Dialog.DialogControlProps 520 - }) { 521 - const pal = usePalette('default') 522 - const {_} = useLingui() 523 - 524 - const libreTranslateInstance = useLibreTranslateInstance() 525 - const [url, setUrl] = useState(libreTranslateInstance ?? '') 526 - const setLibreTranslateInstance = useSetLibreTranslateInstance() 527 - 528 - const submit = () => { 529 - setLibreTranslateInstance(url) 530 - control.close() 531 - } 532 - 533 - const shouldDisable = () => { 534 - try { 535 - return !new URL(url).hostname.includes('.') 536 - } catch (e) { 537 - return true 538 - } 539 - } 540 - 541 - return ( 542 - <Dialog.Outer 543 - control={control} 544 - nativeOptions={{preventExpansion: true}} 545 - onClose={() => setUrl(libreTranslateInstance ?? '')}> 546 - <Dialog.Handle /> 547 - <Dialog.ScrollableInner label={_(msg`LibreTranslate instance URL`)}> 548 - <View style={[a.gap_sm, a.pb_lg]}> 549 - <Text style={[a.text_2xl, a.font_bold]}> 550 - <Trans>LibreTranslate instance URL</Trans> 551 - </Text> 552 - </View> 553 - 554 - <View style={a.gap_lg}> 555 - <Dialog.Input 556 - label="Text input field" 557 - autoFocus 558 - style={[styles.textInput, pal.border, pal.text]} 559 - onChangeText={value => { 560 - setUrl(value) 561 - }} 562 - placeholder={persisted.defaults.libreTranslateInstance} 563 - placeholderTextColor={pal.colors.textLight} 564 - onSubmitEditing={submit} 565 - accessibilityHint={_( 566 - msg`Input the url of the LibreTranslate instance to use`, 567 - )} 568 - defaultValue={libreTranslateInstance} 569 - /> 570 - 571 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 572 - <Button 573 - label={_(msg`Save`)} 574 - size="large" 575 - onPress={() => void submit()} 576 - variant="solid" 577 - color="primary" 578 - disabled={shouldDisable()}> 579 - <ButtonText> 580 - <Trans>Save</Trans> 581 - </ButtonText> 582 - </Button> 583 - </View> 584 - </View> 585 - 586 - <Dialog.Close /> 587 - </Dialog.ScrollableInner> 588 - </Dialog.Outer> 589 - ) 590 - } 591 - 592 - function ImageCdnHostDialog({control}: {control: Dialog.DialogControlProps}) { 593 - const pal = usePalette('default') 594 - const {_} = useLingui() 595 - 596 - const imageCdnHost = useImageCdnHost() 597 - const [url, setUrl] = useState(imageCdnHost ?? '') 598 - const setImageCdnHost = useSetImageCdnHost() 599 - 600 - const submit = () => { 601 - const trimmedUrl = url.trim() 602 - if (!trimmedUrl) { 603 - control.close(() => { 604 - setImageCdnHost(undefined) 605 - }) 606 - return 607 - } 608 - 609 - control.close(() => { 610 - try { 611 - setImageCdnHost(new URL(trimmedUrl).origin) 612 - } catch { 613 - setImageCdnHost(trimmedUrl) 614 - } 615 - }) 616 - } 617 - 618 - const isReset = url.trim().length === 0 619 - 620 - const shouldDisable = () => { 621 - if (isReset) return false 622 - 623 - try { 624 - return !new URL(url).hostname.includes('.') 625 - } catch (e) { 626 - return true 627 - } 628 - } 629 - 630 - return ( 631 - <Dialog.Outer 632 - control={control} 633 - nativeOptions={{preventExpansion: true}} 634 - onClose={() => setUrl(imageCdnHost ?? '')}> 635 - <Dialog.Handle /> 636 - <Dialog.ScrollableInner label={_(msg`Image CDN URL`)}> 637 - <View style={[a.gap_sm, a.pb_lg]}> 638 - <Text style={[a.text_2xl, a.font_bold]}> 639 - <Trans>Image CDN URL</Trans> 640 - </Text> 641 - </View> 642 - 643 - <View style={a.gap_lg}> 644 - <Dialog.Input 645 - label="Text input field" 646 - autoFocus 647 - style={[styles.textInput, pal.border, pal.text]} 648 - onChangeText={value => { 649 - setUrl(value) 650 - }} 651 - placeholder={persisted.defaults.imageCdnHost} 652 - placeholderTextColor={pal.colors.textLight} 653 - onSubmitEditing={submit} 654 - accessibilityHint={_(msg`Input the URL of the image CDN to use`)} 655 - defaultValue={imageCdnHost} 656 - /> 657 - 658 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 659 - <Button 660 - label={isReset ? _(msg`Reset`) : _(msg`Save`)} 661 - size="large" 662 - onPress={() => void submit()} 663 - variant="solid" 664 - color={isReset ? 'secondary' : 'primary'} 665 - disabled={shouldDisable()}> 666 - <ButtonText> 667 - {isReset ? <Trans>Reset</Trans> : <Trans>Save</Trans>} 668 - </ButtonText> 669 - </Button> 670 - </View> 671 - </View> 672 - 673 - <Dialog.Close /> 674 - </Dialog.ScrollableInner> 675 - </Dialog.Outer> 676 - ) 677 - } 678 - 679 - function PlcDirectoryDialog({control}: {control: Dialog.DialogControlProps}) { 680 - const pal = usePalette('default') 681 - const {_} = useLingui() 682 - 683 - const plcDirectory = usePlcDirectory() 684 - const [url, setUrl] = useState(plcDirectory ?? '') 685 - const setPlcDirectory = useSetPlcDirectory() 686 - 687 - const submit = () => { 688 - const trimmedUrl = url.trim() 689 - if (!trimmedUrl) { 690 - control.close(() => { 691 - setPlcDirectory(undefined) 692 - }) 693 - return 694 - } 695 - 696 - control.close(() => { 697 - try { 698 - setPlcDirectory(new URL(trimmedUrl).origin) 699 - } catch { 700 - setPlcDirectory(trimmedUrl) 701 - } 702 - }) 703 - } 704 - 705 - const isReset = url.trim().length === 0 706 - 707 - const shouldDisable = () => { 708 - if (isReset) return false 709 - 710 - try { 711 - const nextUrl = new URL(url) 712 - return nextUrl.protocol !== 'https:' && nextUrl.protocol !== 'http:' 713 - } catch { 714 - return true 715 - } 716 - } 717 - 718 - return ( 719 - <Dialog.Outer 720 - control={control} 721 - nativeOptions={{preventExpansion: true}} 722 - onClose={() => setUrl(plcDirectory ?? '')}> 723 - <Dialog.Handle /> 724 - <Dialog.ScrollableInner label={_(msg`PLC Directory URL`)}> 725 - <View style={[a.gap_sm, a.pb_lg]}> 726 - <Text style={[a.text_2xl, a.font_bold]}> 727 - <Trans>PLC Directory URL</Trans> 728 - </Text> 729 - </View> 730 - 731 - <View style={a.gap_lg}> 732 - <Dialog.Input 733 - label="Text input field" 734 - autoFocus 735 - style={[styles.textInput, pal.border, pal.text]} 736 - onChangeText={value => { 737 - setUrl(value) 738 - }} 739 - placeholder={persisted.defaults.plcDirectory} 740 - placeholderTextColor={pal.colors.textLight} 741 - onSubmitEditing={submit} 742 - accessibilityHint={_( 743 - msg`Input the URL of the PLC directory to use`, 744 - )} 745 - defaultValue={plcDirectory} 746 - /> 747 - 748 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 749 - <Button 750 - label={isReset ? _(msg`Reset`) : _(msg`Save`)} 751 - size="large" 752 - onPress={() => void submit()} 753 - variant="solid" 754 - color={isReset ? 'secondary' : 'primary'} 755 - disabled={shouldDisable()}> 756 - <ButtonText> 757 - {isReset ? <Trans>Reset</Trans> : <Trans>Save</Trans>} 758 - </ButtonText> 759 - </Button> 760 - </View> 761 - </View> 762 - 763 - <Dialog.Close /> 764 - </Dialog.ScrollableInner> 765 - </Dialog.Outer> 766 - ) 767 - } 768 - function PostReplacementDialog({ 769 - control, 770 - }: { 771 - control: Dialog.DialogControlProps 772 - }) { 773 - const pal = usePalette('default') 774 - const {_, i18n} = useLingui() 775 - 776 - const postReplacement = usePostReplacement() 777 - const setPostReplacement = useSetPostReplacement() 778 - 779 - const [singular, setSingular] = useState(postReplacement.postName) 780 - const [plural, setPlural] = useState(postReplacement.postsName) 781 - const [pluralManuallyEdited, setPluralManuallyEdited] = useState(false) 782 - 783 - const submit = async () => { 784 - setPostReplacement({ 785 - enabled: singular.trim().toLowerCase() !== 'post', 786 - postName: singular, 787 - postsName: plural, 788 - }) 789 - 790 - // Force reload the i18n messages to apply the replacement immediately 791 - const locale = i18n.locale 792 - await (IS_WEB 793 - ? dynamicActivateWeb(locale as AppLanguage) 794 - : dynamicActivate(locale as AppLanguage)) 795 - 796 - control.close() 797 - } 798 - 799 - const handleSingularChange = (value: string) => { 800 - setSingular(value) 801 - if (!pluralManuallyEdited) { 802 - setPlural(value + 's') 803 - } 804 - } 805 - 806 - const handlePluralChange = (value: string) => { 807 - setPlural(value) 808 - setPluralManuallyEdited(true) 809 - } 810 - 811 - const handlePresetSelect = (singularForm: string, pluralForm: string) => { 812 - setSingular(singularForm) 813 - setPlural(pluralForm) 814 - setPluralManuallyEdited(false) 815 - } 816 - 817 - const shouldDisable = () => { 818 - return !singular.trim() || !plural.trim() 819 - } 820 - 821 - return ( 822 - <Dialog.Outer 823 - control={control} 824 - nativeOptions={{preventExpansion: true}} 825 - onClose={() => { 826 - setSingular(postReplacement.postName) 827 - setPlural(postReplacement.postsName) 828 - setPluralManuallyEdited(false) 829 - }}> 830 - <Dialog.Handle /> 831 - <Dialog.ScrollableInner label={_(msg`Custom post phrase`)}> 832 - <View style={[a.gap_sm, a.pb_lg]}> 833 - <Text style={[a.text_2xl, a.font_bold]}> 834 - <Trans>Custom post phrase</Trans> 835 - </Text> 836 - </View> 837 - 838 - <View style={a.gap_lg}> 839 - <Dialog.Input 840 - label="Singular form" 841 - autoFocus 842 - style={[styles.textInput, pal.border, pal.text]} 843 - onChangeText={handleSingularChange} 844 - placeholder="skeet" 845 - placeholderTextColor={pal.colors.textLight} 846 - accessibilityHint={_(msg`Input the singular form (e.g., "skeet")`)} 847 - value={singular} 848 - /> 849 - 850 - <View style={[a.flex_row, a.flex_wrap, a.mb_xs]}> 851 - {[ 852 - {singular: 'post', plural: 'posts'}, 853 - {singular: 'skeet', plural: 'skeets'}, 854 - {singular: 'note', plural: 'notes'}, 855 - {singular: 'woot', plural: 'woots'}, 856 - {singular: 'toot', plural: 'toots'}, 857 - {singular: 'silly', plural: 'sillies'}, 858 - ].map(preset => ( 859 - <Button 860 - key={preset.singular} 861 - variant="ghost" 862 - color="primary" 863 - label={preset.singular} 864 - style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} 865 - onPress={() => 866 - handlePresetSelect(preset.singular, preset.plural) 867 - }> 868 - <ButtonText>{preset.singular}</ButtonText> 869 - </Button> 870 - ))} 871 - </View> 872 - 873 - <Dialog.Input 874 - label="Plural form" 875 - style={[styles.textInput, pal.border, pal.text]} 876 - onChangeText={handlePluralChange} 877 - placeholder="skeets" 878 - placeholderTextColor={pal.colors.textLight} 879 - accessibilityHint={_(msg`Input the plural form (e.g., "skeets")`)} 880 - value={plural} 881 - /> 882 - 883 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 884 - <Button 885 - label={_(msg`Save`)} 886 - size="large" 887 - onPress={() => void submit()} 888 - variant="solid" 889 - color="primary" 890 - disabled={shouldDisable()}> 891 - <ButtonText> 892 - <Trans>Save</Trans> 893 - </ButtonText> 894 - </Button> 895 - </View> 896 - </View> 897 - 898 - <Dialog.Close /> 899 - </Dialog.ScrollableInner> 900 - </Dialog.Outer> 901 - ) 902 - } 903 - 904 - function TrustedVerifiersDialog({ 905 - control, 906 - }: { 907 - control: Dialog.DialogControlProps 908 - }) { 909 - const {_} = useLingui() 910 - 911 - return ( 912 - <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 913 - <Dialog.Handle /> 914 - <Dialog.ScrollableInner label={_(msg`Trusted Verifiers`)}> 915 - <View style={[a.gap_sm, a.pb_lg]}> 916 - <Text style={[a.text_2xl, a.font_bold]}> 917 - <Trans>Trusted Verifiers</Trans> 918 - </Text> 919 - </View> 920 - 921 - <TrustedVerifiers /> 922 - 923 - <Dialog.Close /> 924 - </Dialog.ScrollableInner> 925 - </Dialog.Outer> 926 - ) 927 - } 928 - 929 - const TrustedVerifiers = (): React.ReactNode => { 930 - const trusted = useDeerVerificationTrusted() 931 - const moderationOpts = useModerationOpts() 932 - 933 - const results = useProfilesQuery({ 934 - handles: Array.from(trusted), 935 - }) 936 - 937 - const {gtMobile} = useBreakpoints() 938 - 939 - return ( 940 - results.data && 941 - moderationOpts !== undefined && ( 942 - <View style={[gtMobile ? a.pl_md : a.pl_sm, a.pb_sm]}> 943 - {results.data.profiles.map(profile => ( 944 - <SearchProfileCard 945 - key={profile.did} 946 - profile={profile as ProfileViewBasic} 947 - moderationOpts={moderationOpts} 948 - /> 949 - ))} 950 - </View> 951 - ) 952 - ) 953 - } 954 - 955 - function OpenRouterApiKeyDialog({ 956 - control, 957 - }: { 958 - control: Dialog.DialogControlProps 959 - }) { 960 - const pal = usePalette('default') 961 - const {_} = useLingui() 962 - 963 - const apiKey = useOpenRouterApiKey() 964 - const [value, setValue] = useState(apiKey ?? '') 965 - const setApiKey = useSetOpenRouterApiKey() 966 - 967 - const submit = () => { 968 - setApiKey(value.trim() || undefined) 969 - control.close() 970 - } 971 - 972 - return ( 973 - <Dialog.Outer 974 - control={control} 975 - nativeOptions={{preventExpansion: true}} 976 - onClose={() => setValue(apiKey ?? '')}> 977 - <Dialog.Handle /> 978 - <Dialog.ScrollableInner label={_(msg`OpenRouter API Key`)}> 979 - <View style={[a.gap_sm, a.pb_lg]}> 980 - <Text style={[a.text_2xl, a.font_bold]}> 981 - <Trans>OpenRouter API Key</Trans> 982 - </Text> 983 - </View> 984 - 985 - <View style={a.gap_lg}> 986 - <Dialog.Input 987 - label="API Key" 988 - autoFocus 989 - style={[styles.textInput, pal.border, pal.text]} 990 - onChangeText={setValue} 991 - placeholder="sk-or-..." 992 - placeholderTextColor={pal.colors.textLight} 993 - onSubmitEditing={submit} 994 - accessibilityHint={_( 995 - msg`Enter your OpenRouter API key for AI alt text generation`, 996 - )} 997 - defaultValue={apiKey ?? ''} 998 - secureTextEntry 999 - /> 1000 - 1001 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 1002 - <Button 1003 - label={_(msg`Save`)} 1004 - size="large" 1005 - onPress={() => void submit()} 1006 - variant="solid" 1007 - color="primary"> 1008 - <ButtonText> 1009 - <Trans>Save</Trans> 1010 - </ButtonText> 1011 - </Button> 1012 - </View> 1013 - </View> 1014 - 1015 - <Dialog.Close /> 1016 - </Dialog.ScrollableInner> 1017 - </Dialog.Outer> 1018 - ) 1019 - } 1020 - 1021 - function OpenRouterModelDialog({ 1022 - control, 1023 - }: { 1024 - control: Dialog.DialogControlProps 1025 - }) { 1026 - const pal = usePalette('default') 1027 - const {_} = useLingui() 1028 - 1029 - const model = useOpenRouterModel() 1030 - const [value, setValue] = useState(model ?? '') 1031 - const setModel = useSetOpenRouterModel() 1032 - 1033 - const submit = () => { 1034 - setModel(value.trim() || undefined) 1035 - control.close() 1036 - } 1037 - 1038 - return ( 1039 - <Dialog.Outer 1040 - control={control} 1041 - nativeOptions={{preventExpansion: true}} 1042 - onClose={() => setValue(model ?? '')}> 1043 - <Dialog.Handle /> 1044 - <Dialog.ScrollableInner label={_(msg`OpenRouter Model`)}> 1045 - <View style={[a.gap_sm, a.pb_lg]}> 1046 - <Text style={[a.text_2xl, a.font_bold]}> 1047 - <Trans>OpenRouter Model</Trans> 1048 - </Text> 1049 - </View> 1050 - 1051 - <View style={a.gap_lg}> 1052 - <Dialog.Input 1053 - label="Model" 1054 - autoFocus 1055 - style={[styles.textInput, pal.border, pal.text]} 1056 - onChangeText={setValue} 1057 - placeholder={DEFAULT_ALT_TEXT_AI_MODEL} 1058 - placeholderTextColor={pal.colors.textLight} 1059 - onSubmitEditing={submit} 1060 - accessibilityHint={_( 1061 - msg`Enter the model ID to use for alt text generation`, 1062 - )} 1063 - defaultValue={model ?? ''} 1064 - /> 1065 - 1066 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 1067 - <Button 1068 - label={_(msg`Save`)} 1069 - size="large" 1070 - onPress={() => void submit()} 1071 - variant="solid" 1072 - color="primary"> 1073 - <ButtonText> 1074 - <Trans>Save</Trans> 1075 - </ButtonText> 1076 - </Button> 1077 - </View> 1078 - </View> 1079 - 1080 - <Dialog.Close /> 1081 - </Dialog.ScrollableInner> 1082 - </Dialog.Outer> 1083 - ) 1084 - } 1085 - 1086 - function OpenRouterPromptDialog({ 1087 - control, 1088 - }: { 1089 - control: Dialog.DialogControlProps 1090 - }) { 1091 - const pal = usePalette('default') 1092 - const {_} = useLingui() 1093 - 1094 - const prompt = useOpenRouterPrompt() 1095 - const [value, setValue] = useState(prompt ?? '') 1096 - const setPrompt = useSetOpenRouterPrompt() 1097 - 1098 - const submit = () => { 1099 - setPrompt(value.trim() || undefined) 1100 - control.close() 1101 - } 1102 - 1103 - return ( 1104 - <Dialog.Outer 1105 - control={control} 1106 - nativeOptions={{preventExpansion: true}} 1107 - onClose={() => setValue(prompt ?? '')}> 1108 - <Dialog.Handle /> 1109 - <Dialog.ScrollableInner label={_(msg`Alt Text Prompt`)}> 1110 - <View style={[a.gap_sm, a.pb_lg]}> 1111 - <Text style={[a.text_2xl, a.font_bold]}> 1112 - <Trans>Alt Text Prompt</Trans> 1113 - </Text> 1114 - </View> 1115 - 1116 - <View style={a.gap_lg}> 1117 - <Dialog.Input 1118 - label="Prompt" 1119 - multiline 1120 - numberOfLines={6} 1121 - style={[ 1122 - styles.textInput, 1123 - pal.border, 1124 - pal.text, 1125 - {minHeight: 120, textAlignVertical: 'top'}, 1126 - ]} 1127 - onChangeText={setValue} 1128 - placeholder={DEFAULT_ALT_TEXT_AI_PROMPT} 1129 - placeholderTextColor={pal.colors.textLight} 1130 - accessibilityHint={_( 1131 - msg`Enter a custom prompt for AI alt text generation`, 1132 - )} 1133 - defaultValue={prompt ?? ''} 1134 - /> 1135 - 1136 - <View style={IS_WEB && [a.flex_row, a.justify_end]}> 1137 - <Button 1138 - label={_(msg`Save`)} 1139 - size="large" 1140 - onPress={() => void submit()} 1141 - variant="solid" 1142 - color="primary"> 1143 - <ButtonText> 1144 - <Trans>Save</Trans> 1145 - </ButtonText> 1146 - </Button> 1147 - </View> 1148 - </View> 1149 - 1150 - <Dialog.Close /> 1151 - </Dialog.ScrollableInner> 1152 - </Dialog.Outer> 1153 - ) 1154 - } 1155 - 1156 - export function RunesSettingsScreen({}: Props) { 1157 - const {_} = useLingui() 1158 - 1159 - const goLinksEnabled = useGoLinksEnabled() 1160 - const setGoLinksEnabled = useSetGoLinksEnabled() 1161 - 1162 - const directFetchRecords = useDirectFetchRecords() 1163 - const setDirectFetchRecords = useSetDirectFetchRecords() 1164 - 1165 - const showExternalShareButtons = useShowExternalShareButtons() 1166 - const setShowExternalShareButtons = useSetShowExternalShareButtons() 1167 - 1168 - const noAppLabelers = useNoAppLabelers() 1169 - const setNoAppLabelers = useSetNoAppLabelers() 1170 - 1171 - const noDiscoverFallback = useNoDiscoverFallback() 1172 - const setNoDiscoverFallback = useSetNoDiscoverFallback() 1173 - 1174 - const highQualityImages = useHighQualityImages() 1175 - const setHighQualityImages = useSetHighQualityImages() 1176 - const imageCdnHost = useImageCdnHost() 1177 - const plcDirectory = usePlcDirectory() 1178 - 1179 - const hideFeedsPromoTab = useHideFeedsPromoTab() 1180 - const setHideFeedsPromoTab = useSetHideFeedsPromoTab() 1181 - 1182 - const disableViaRepostNotification = useDisableViaRepostNotification() 1183 - const setDisableViaRepostNotification = useSetDisableViaRepostNotification() 1184 - 1185 - const disableComposerPrompt = useDisableComposerPrompt() 1186 - const setDisableComposerPrompt = useSetDisableComposerPrompt() 1187 - 1188 - const discoverContextEnabled = useDiscoverContextEnabled() 1189 - const setDiscoverContextEnabled = useSetDiscoverContextEnabled() 1190 - 1191 - const disableLikesMetrics = useDisableLikesMetrics() 1192 - const setDisableLikesMetrics = useSetDisableLikesMetrics() 1193 - 1194 - const disableRepostsMetrics = useDisableRepostsMetrics() 1195 - const setDisableRepostsMetrics = useSetDisableRepostsMetrics() 1196 - 1197 - const disableQuotesMetrics = useDisableQuotesMetrics() 1198 - const setDisableQuotesMetrics = useSetDisableQuotesMetrics() 1199 - 1200 - const disableSavesMetrics = useDisableSavesMetrics() 1201 - const setDisableSavesMetrics = useSetDisableSavesMetrics() 1202 - 1203 - const disableReplyMetrics = useDisableReplyMetrics() 1204 - const setDisableReplyMetrics = useSetDisableReplyMetrics() 1205 - 1206 - const disableFollowersMetrics = useDisableFollowersMetrics() 1207 - const setDisableFollowersMetrics = useSetDisableFollowersMetrics() 1208 - 1209 - const disableFollowingMetrics = useDisableFollowingMetrics() 1210 - const setDisableFollowingMetrics = useSetDisableFollowingMetrics() 1211 - 1212 - const disableFollowedByMetrics = useDisableFollowedByMetrics() 1213 - const setDisableFollowedByMetrics = useSetDisableFollowedByMetrics() 1214 - 1215 - const disablePostsMetrics = useDisablePostsMetrics() 1216 - const setDisablePostsMetrics = useSetDisablePostsMetrics() 1217 - 1218 - const hideSimilarAccountsRecomm = useHideSimilarAccountsRecomm() 1219 - const setHideSimilarAccountsRecomm = useSetHideSimilarAccountsRecomm() 1220 - 1221 - const hideUnreplyablePosts = useHideUnreplyablePosts() 1222 - const setHideUnreplyablePosts = useSetHideUnreplyablePosts() 1223 - 1224 - const disableVerifyEmailReminder = useDisableVerifyEmailReminder() 1225 - const setDisableVerifyEmailReminder = useSetDisableVerifyEmailReminder() 1226 - 1227 - const constellationInstance = useConstellationInstance() 1228 - const setConstellationInstanceControl = Dialog.useDialogControl() 1229 - 1230 - const setTrustedVerifiersDialogControl = Dialog.useDialogControl() 1231 - 1232 - const deerVerificationEnabled = useDeerVerificationEnabled() 1233 - const setDeerVerificationEnabled = useSetDeerVerificationEnabled() 1234 - 1235 - const pdsLabelEnabled = usePdsLabelEnabled() 1236 - const setPdsLabelEnabled = useSetPdsLabelEnabled() 1237 - const pdsLabelHideBskyPds = usePdsLabelHideBskyPds() 1238 - const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds() 1239 - 1240 - const repostCarouselEnabled = useRepostCarouselEnabled() 1241 - const setRepostCarouselEnabled = useSetRepostCarouselEnabled() 1242 - 1243 - const showFollowsYouBadge = useShowFollowsYouBadge() 1244 - const setShowFollowsYouBadge = useSetShowFollowsYouBadge() 1245 - 1246 - const showLinkInHandle = useShowLinkInHandle() 1247 - const setShowLinkInHandle = useSetShowLinkInHandle() 1248 - const showLinkInHandleOnlyOnWorkingLinks = 1249 - useShowLinkInHandleOnlyOnWorkingLinks() 1250 - const setShowLinkInHandleOnlyOnWorkingLinks = 1251 - useSetShowLinkInHandleOnlyOnWorkingLinks() 1252 - 1253 - const handleInLinks = useHandleInLinks() 1254 - const setHandleInLinks = useSetHandleInLinks() 1255 - 1256 - const translationServicePreference = useTranslationServicePreference() 1257 - const setTranslationServicePreference = useSetTranslationServicePreference() 1258 - 1259 - const setLibreTranslateInstanceControl = Dialog.useDialogControl() 1260 - 1261 - const setImageCdnHostControl = Dialog.useDialogControl() 1262 - 1263 - const setPlcDirectoryControl = Dialog.useDialogControl() 1264 - 1265 - const setPostReplacementDialogControl = Dialog.useDialogControl() 1266 - 1267 - const setOpenRouterApiKeyControl = Dialog.useDialogControl() 1268 - const openRouterModel = useOpenRouterModel() 1269 - const setOpenRouterModelControl = Dialog.useDialogControl() 1270 - const setOpenRouterPromptControl = Dialog.useDialogControl() 1271 - const openRouterConfigured = useOpenRouterConfigured() 1272 - 1273 - const autoLikeOnRepost = useAutoLikeOnRepost() 1274 - const setAutoLikeOnRepost = useSetAutoLikeOnRepost() 1275 - 1276 - const [customAppViewDid] = useCustomAppViewDid() 1277 - const setCustomAppViewDidControl = Dialog.useDialogControl() 1278 - 1279 - const setFaviconServiceControl = Dialog.useDialogControl() 1280 - 1281 - return ( 1282 - <Layout.Screen> 1283 - <Layout.Header.Outer> 1284 - <Layout.Header.BackButton /> 1285 - <Layout.Header.Content> 1286 - <Layout.Header.TitleText> 1287 - <Trans>Runes</Trans> 1288 - </Layout.Header.TitleText> 1289 - </Layout.Header.Content> 1290 - <Layout.Header.Slot /> 1291 - </Layout.Header.Outer> 1292 - <Layout.Content> 1293 - <SettingsList.Container> 1294 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1295 - <SettingsList.ItemIcon icon={AtomIcon} /> 1296 - <SettingsList.ItemText> 1297 - <Trans>Redirects</Trans> 1298 - </SettingsList.ItemText> 1299 - <Toggle.Item 1300 - name="use_go_links" 1301 - label={_(msg`Redirect through go.bsky.app`)} 1302 - value={goLinksEnabled ?? false} 1303 - onChange={value => setGoLinksEnabled(value)} 1304 - style={[a.w_full]}> 1305 - <Toggle.LabelText style={[a.flex_1]}> 1306 - <Trans>Redirect through go.bsky.app</Trans> 1307 - </Toggle.LabelText> 1308 - <Toggle.Platform /> 1309 - </Toggle.Item> 1310 - <Toggle.Item 1311 - name="use_handle_in_links" 1312 - label={_( 1313 - msg`Use handles in profile links instead of DIDs (requires restart)`, 1314 - )} 1315 - value={handleInLinks ?? false} 1316 - onChange={value => setHandleInLinks(value)} 1317 - style={[a.w_full]}> 1318 - <Toggle.LabelText style={[a.flex_1]}> 1319 - <Trans>Use handles in profile links instead of DIDs</Trans> 1320 - </Toggle.LabelText> 1321 - <Toggle.Platform /> 1322 - </Toggle.Item> 1323 - </SettingsList.Group> 1324 - 1325 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1326 - <SettingsList.ItemIcon icon={VisibilityIcon} /> 1327 - <SettingsList.ItemText> 1328 - <Trans>Visibility</Trans> 1329 - </SettingsList.ItemText> 1330 - <Toggle.Item 1331 - name="direct_fetch_records" 1332 - label={_( 1333 - msg`Fetch records directly from PDS to see through quote blocks`, 1334 - )} 1335 - value={directFetchRecords} 1336 - onChange={value => setDirectFetchRecords(value)} 1337 - style={[a.w_full]}> 1338 - <Toggle.LabelText style={[a.flex_1]}> 1339 - <Trans> 1340 - Fetch records directly from PDS to see contents of blocked and 1341 - detached quotes 1342 - </Trans> 1343 - </Toggle.LabelText> 1344 - <Toggle.Platform /> 1345 - </Toggle.Item> 1346 - </SettingsList.Group> 1347 - 1348 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1349 - <SettingsList.ItemIcon icon={ChainLinkIcon} /> 1350 - <SettingsList.ItemText> 1351 - <Trans>Bridging and Fediverse</Trans> 1352 - </SettingsList.ItemText> 1353 - <Toggle.Item 1354 - name="external_share_buttons" 1355 - label={_( 1356 - msg`Show "Open original post" and "Open post in PDSls" buttons`, 1357 - )} 1358 - value={showExternalShareButtons} 1359 - onChange={value => setShowExternalShareButtons(value)} 1360 - style={[a.w_full]}> 1361 - <Toggle.LabelText style={[a.flex_1]}> 1362 - <Trans> 1363 - Show "Open original post" and "Open post in PDSls" buttons 1364 - </Trans> 1365 - </Toggle.LabelText> 1366 - <Toggle.Platform /> 1367 - </Toggle.Item> 1368 - </SettingsList.Group> 1369 - 1370 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1371 - <SettingsList.ItemIcon icon={VerifiedIcon} /> 1372 - <SettingsList.ItemText> 1373 - <Trans>Verification</Trans> 1374 - </SettingsList.ItemText> 1375 - <Toggle.Item 1376 - name="custom_verifications" 1377 - label={_( 1378 - msg`Select your own set of trusted verifiers, and operate as a verifier`, 1379 - )} 1380 - value={deerVerificationEnabled} 1381 - onChange={value => setDeerVerificationEnabled(value)} 1382 - style={[a.w_full]}> 1383 - <Toggle.LabelText style={[a.flex_1]}> 1384 - <Trans> 1385 - Select your own set of trusted verifiers, and operate as a 1386 - verifier 1387 - </Trans> 1388 - </Toggle.LabelText> 1389 - <Toggle.Platform /> 1390 - </Toggle.Item> 1391 - </SettingsList.Group> 1392 - 1393 - <SettingsList.Item> 1394 - <Admonition type="warning" style={[a.flex_1]}> 1395 - <Trans> 1396 - May slow down the client or fail to find all labels. Revoke and 1397 - grant trust in the meatball menu on a profile.{' '} 1398 - {deerVerificationEnabled 1399 - ? 'You currently' 1400 - : 'If enabled, you would'}{' '} 1401 - trust the following verifiers: 1402 - </Trans> 1403 - </Admonition> 1404 - </SettingsList.Item> 1405 - 1406 - <SettingsList.Item> 1407 - <SettingsList.ItemIcon icon={VerifiedIcon} /> 1408 - <SettingsList.ItemText> 1409 - <Trans>{`Trusted Verifiers`}</Trans> 1410 - </SettingsList.ItemText> 1411 - <SettingsList.BadgeButton 1412 - label={_(msg`View`)} 1413 - onPress={() => setTrustedVerifiersDialogControl.open()} 1414 - /> 1415 - </SettingsList.Item> 1416 - 1417 - <SettingsList.Item> 1418 - <SettingsList.ItemIcon icon={StarIcon} /> 1419 - <SettingsList.ItemText> 1420 - <Trans>{`Constellation Instance`}</Trans> 1421 - </SettingsList.ItemText> 1422 - <SettingsList.BadgeButton 1423 - label={_(msg`Change`)} 1424 - onPress={() => setConstellationInstanceControl.open()} 1425 - /> 1426 - </SettingsList.Item> 1427 - <SettingsList.Item> 1428 - <Admonition type="info" style={[a.flex_1]}> 1429 - <Trans> 1430 - Constellation is used to supplement AppView responses for custom 1431 - verifications and nuclear block bypass, via backlinks. Current 1432 - instance:\u00A0 1433 - <InlineLinkText 1434 - to={constellationInstance} 1435 - label={constellationInstance}> 1436 - {constellationInstance} 1437 - </InlineLinkText> 1438 - </Trans> 1439 - </Admonition> 1440 - </SettingsList.Item> 1441 - 1442 - <SettingsList.Divider /> 1443 - 1444 - <SettingsList.Item> 1445 - <SettingsList.ItemIcon icon={PencilIcon} /> 1446 - <SettingsList.ItemText> 1447 - <Trans>{`Custom post phrase`}</Trans> 1448 - </SettingsList.ItemText> 1449 - <SettingsList.BadgeButton 1450 - label={_(msg`Change`)} 1451 - onPress={() => setPostReplacementDialogControl.open()} 1452 - /> 1453 - </SettingsList.Item> 1454 - 1455 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1456 - <SettingsList.ItemIcon icon={PaintRollerIcon} /> 1457 - <SettingsList.ItemText> 1458 - <Trans>Tweaks</Trans> 1459 - </SettingsList.ItemText> 1460 - <Toggle.Item 1461 - name="pds_label_badge" 1462 - label={_( 1463 - msg`Show a PDS badge next to the display name on profiles`, 1464 - )} 1465 - value={pdsLabelEnabled} 1466 - onChange={value => setPdsLabelEnabled(value)} 1467 - style={[a.w_full]}> 1468 - <Toggle.LabelText style={[a.flex_1]}> 1469 - <Trans> 1470 - Show a PDS badge next to the display name on profiles 1471 - </Trans> 1472 - </Toggle.LabelText> 1473 - <Toggle.Platform /> 1474 - </Toggle.Item> 1475 - {pdsLabelEnabled && ( 1476 - <Toggle.Item 1477 - name="pds_label_hide_bsky" 1478 - label={_(msg`Hide PDS badge for Bluesky-hosted accounts`)} 1479 - value={pdsLabelHideBskyPds} 1480 - onChange={value => setPdsLabelHideBskyPds(value)} 1481 - style={[a.w_full]}> 1482 - <Toggle.LabelText style={[a.flex_1]}> 1483 - <Trans>Hide PDS badge for Bluesky-hosted accounts</Trans> 1484 - </Toggle.LabelText> 1485 - <Toggle.Platform /> 1486 - </Toggle.Item> 1487 - )} 1488 - 1489 - <Toggle.Item 1490 - name="repost_carousel" 1491 - label={_(msg`Combine reposts into a horizontal carousel`)} 1492 - value={repostCarouselEnabled} 1493 - onChange={value => setRepostCarouselEnabled(value)} 1494 - style={[a.w_full]}> 1495 - <Toggle.LabelText style={[a.flex_1]}> 1496 - <Trans>Combine reposts into a horizontal carousel</Trans> 1497 - </Toggle.LabelText> 1498 - <Toggle.Platform /> 1499 - </Toggle.Item> 1500 - 1501 - <Toggle.Item 1502 - name="show_link_in_handle" 1503 - label={_( 1504 - msg`On non-bsky.social handles, show a link to that URL`, 1505 - )} 1506 - value={showLinkInHandle} 1507 - onChange={value => setShowLinkInHandle(value)} 1508 - style={[a.w_full]}> 1509 - <Toggle.LabelText style={[a.flex_1]}> 1510 - <Trans> 1511 - On non-bsky.social handles, show a link to that URL 1512 - </Trans> 1513 - </Toggle.LabelText> 1514 - <Toggle.Platform /> 1515 - </Toggle.Item> 1516 - {showLinkInHandle && ( 1517 - <Toggle.Item 1518 - name="show_link_in_handle_only_on_working_links" 1519 - label={_(msg`Only show URL on handles with working links`)} 1520 - value={showLinkInHandleOnlyOnWorkingLinks} 1521 - onChange={value => setShowLinkInHandleOnlyOnWorkingLinks(value)} 1522 - style={[a.w_full]}> 1523 - <Toggle.LabelText style={[a.flex_1]}> 1524 - <Trans>Only show URL on handles with working links</Trans> 1525 - </Toggle.LabelText> 1526 - <Toggle.Platform /> 1527 - </Toggle.Item> 1528 - )} 1529 - 1530 - <Toggle.Item 1531 - name="no_discover_fallback" 1532 - label={_(msg`Do not fall back to discover feed`)} 1533 - value={noDiscoverFallback} 1534 - onChange={value => setNoDiscoverFallback(value)} 1535 - style={[a.w_full]}> 1536 - <Toggle.LabelText style={[a.flex_1]}> 1537 - <Trans>Do not fall back to discover feed</Trans> 1538 - </Toggle.LabelText> 1539 - <Toggle.Platform /> 1540 - </Toggle.Item> 1541 - 1542 - <Toggle.Item 1543 - name="high_quality_images" 1544 - label={_(msg`Display images in higher quality`)} 1545 - value={highQualityImages} 1546 - onChange={value => setHighQualityImages(value)} 1547 - style={[a.w_full]}> 1548 - <Toggle.LabelText style={[a.flex_1]}> 1549 - <Trans>Display images in higher quality</Trans> 1550 - </Toggle.LabelText> 1551 - <Toggle.Platform /> 1552 - </Toggle.Item> 1553 - <Admonition type="info" style={[a.flex_1]}> 1554 - <Trans> 1555 - Images will be served as PNG instead of WEBP. Images will take 1556 - longer to load and use more bandwidth. 1557 - </Trans> 1558 - </Admonition> 1559 - <Toggle.Item 1560 - name="auto_like_on_repost" 1561 - label={_(msg`Auto-like what you repost`)} 1562 - value={autoLikeOnRepost} 1563 - onChange={value => setAutoLikeOnRepost(value)} 1564 - style={[a.w_full]}> 1565 - <Toggle.LabelText style={[a.flex_1]}> 1566 - <Trans>Auto-like what you repost</Trans> 1567 - </Toggle.LabelText> 1568 - <Toggle.Platform /> 1569 - </Toggle.Item> 1570 - <Toggle.Item 1571 - name="hide_feeds_promo_tab" 1572 - label={_(msg`Hide "Feeds ✨" tab when only one feed is selected`)} 1573 - value={hideFeedsPromoTab} 1574 - onChange={value => setHideFeedsPromoTab(value)} 1575 - style={[a.w_full]}> 1576 - <Toggle.LabelText style={[a.flex_1]}> 1577 - <Trans> 1578 - Hide "Feeds ✨" tab when only one feed is selected 1579 - </Trans> 1580 - </Toggle.LabelText> 1581 - <Toggle.Platform /> 1582 - </Toggle.Item> 1583 - 1584 - <Toggle.Item 1585 - name="disable_via_repost_notification" 1586 - label={_(msg`Disable via repost notifications`)} 1587 - value={disableViaRepostNotification} 1588 - onChange={value => setDisableViaRepostNotification(value)} 1589 - style={[a.w_full]}> 1590 - <Toggle.LabelText style={[a.flex_1]}> 1591 - <Trans>Disable via repost notifications</Trans> 1592 - </Toggle.LabelText> 1593 - <Toggle.Platform /> 1594 - </Toggle.Item> 1595 - <Admonition type="info" style={[a.flex_1]}> 1596 - <Trans> 1597 - Forcefully disables the notifications other people receive when 1598 - you like/repost a post someone else has reposted for privacy. 1599 - </Trans> 1600 - </Admonition> 1601 - 1602 - <Toggle.Item 1603 - name="hide_similar_accounts_recommendations" 1604 - label={_(msg`Hide similar accounts recommendations`)} 1605 - value={hideSimilarAccountsRecomm} 1606 - onChange={value => setHideSimilarAccountsRecomm(value)} 1607 - style={[a.w_full]}> 1608 - <Toggle.LabelText style={[a.flex_1]}> 1609 - <Trans>Hide similar accounts recommendations</Trans> 1610 - </Toggle.LabelText> 1611 - <Toggle.Platform /> 1612 - </Toggle.Item> 1613 - 1614 - <Toggle.Item 1615 - name="hide_unreplyable_posts" 1616 - label={_(msg`Hide posts that cannot be replied to from feeds`)} 1617 - value={hideUnreplyablePosts} 1618 - onChange={value => setHideUnreplyablePosts(value)} 1619 - style={[a.w_full]}> 1620 - <Toggle.LabelText style={[a.flex_1]}> 1621 - <Trans>Hide posts that cannot be replied to from feeds</Trans> 1622 - </Toggle.LabelText> 1623 - <Toggle.Platform /> 1624 - </Toggle.Item> 1625 - <Admonition type="info" style={[a.flex_1]}> 1626 - <Trans> 1627 - Hides posts from feeds where replies are disabled (e.g. due to 1628 - postgates or other restrictions). Does not affect thread views. 1629 - </Trans> 1630 - </Admonition> 1631 - 1632 - <Toggle.Item 1633 - name="disable_composer_prompt" 1634 - label={_(msg`Disable composer prompt`)} 1635 - value={disableComposerPrompt} 1636 - onChange={value => setDisableComposerPrompt(value)} 1637 - style={[a.w_full]}> 1638 - <Toggle.LabelText style={[a.flex_1]}> 1639 - <Trans>Disable composer prompt</Trans> 1640 - </Toggle.LabelText> 1641 - <Toggle.Platform /> 1642 - </Toggle.Item> 1643 - 1644 - <Toggle.Item 1645 - name="disable_verify_email_reminder" 1646 - label={_(msg`Disable verify email reminder`)} 1647 - value={disableVerifyEmailReminder} 1648 - onChange={value => setDisableVerifyEmailReminder(value)} 1649 - style={[a.w_full]}> 1650 - <Toggle.LabelText style={[a.flex_1]}> 1651 - <Trans>Disable verify email reminder</Trans> 1652 - </Toggle.LabelText> 1653 - <Toggle.Platform /> 1654 - </Toggle.Item> 1655 - <Admonition type="warning" style={[a.flex_1]}> 1656 - <Trans> 1657 - This only gets rid of the reminder on app launch, useful if your 1658 - PDS does not have email verification setup.\u00A0 This does NOT 1659 - give access to features locked behind email verification. 1660 - </Trans> 1661 - </Admonition> 1662 - 1663 - <Toggle.Item 1664 - name="discover_context" 1665 - label={_(msg`Show debug context for posts in Discover feed`)} 1666 - value={discoverContextEnabled} 1667 - onChange={value => setDiscoverContextEnabled(value)} 1668 - style={[a.w_full]}> 1669 - <Toggle.LabelText style={[a.flex_1]}> 1670 - <Trans>Show debug context for posts in Discover feed</Trans> 1671 - </Toggle.LabelText> 1672 - <Toggle.Platform /> 1673 - </Toggle.Item> 1674 - </SettingsList.Group> 1675 - 1676 - {pdsLabelEnabled && ( 1677 - <SettingsList.Item> 1678 - <SettingsList.ItemIcon icon={StarIcon} /> 1679 - <SettingsList.ItemText> 1680 - <Trans>Favicon service</Trans> 1681 - </SettingsList.ItemText> 1682 - <SettingsList.BadgeButton 1683 - label={_(msg`Change`)} 1684 - onPress={() => setFaviconServiceControl.open()} 1685 - /> 1686 - </SettingsList.Item> 1687 - )} 1688 - 1689 - <SettingsList.Divider /> 1690 - 1691 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1692 - <SettingsList.ItemIcon icon={EarthIcon} /> 1693 - <SettingsList.ItemText> 1694 - <Trans>Post Translation Provider</Trans> 1695 - </SettingsList.ItemText> 1696 - 1697 - <Toggle.Item 1698 - name="service_google" 1699 - label={_(msg`Use Google Translate`)} 1700 - value={translationServicePreference === 'google'} 1701 - onChange={() => setTranslationServicePreference('google')} 1702 - style={[a.w_full]}> 1703 - <Toggle.LabelText style={[a.flex_1]}> 1704 - <Trans>Use Google Translate</Trans> 1705 - </Toggle.LabelText> 1706 - <Toggle.Radio /> 1707 - </Toggle.Item> 1708 - 1709 - <Toggle.Item 1710 - name="service_kagi" 1711 - label={_(msg`Use Kagi Translate`)} 1712 - value={translationServicePreference === 'kagi'} 1713 - onChange={() => setTranslationServicePreference('kagi')} 1714 - style={[a.w_full]}> 1715 - <Toggle.LabelText style={[a.flex_1]}> 1716 - <Trans>Use Kagi Translate</Trans> 1717 - </Toggle.LabelText> 1718 - <Toggle.Radio /> 1719 - </Toggle.Item> 1720 - 1721 - <Toggle.Item 1722 - name="service_papago" 1723 - label={_(msg`Use Naver Papago`)} 1724 - value={translationServicePreference === 'papago'} 1725 - onChange={() => setTranslationServicePreference('papago')} 1726 - style={[a.w_full]}> 1727 - <Toggle.LabelText style={[a.flex_1]}> 1728 - <Trans>Use Naver Papago</Trans> 1729 - </Toggle.LabelText> 1730 - <Toggle.Radio /> 1731 - </Toggle.Item> 1732 - 1733 - <Toggle.Item 1734 - name="service_libreTranslate" 1735 - label={_(msg`Use LibreTranslate`)} 1736 - value={translationServicePreference === 'libreTranslate'} 1737 - onChange={() => setTranslationServicePreference('libreTranslate')} 1738 - style={[a.w_full]}> 1739 - <Toggle.LabelText style={[a.flex_1]}> 1740 - <Trans>Use LibreTranslate</Trans> 1741 - </Toggle.LabelText> 1742 - <Toggle.Radio /> 1743 - </Toggle.Item> 1744 - </SettingsList.Group> 1745 - 1746 - {translationServicePreference === 'libreTranslate' && ( 1747 - <SettingsList.Item> 1748 - <SettingsList.ItemIcon icon={EarthIcon} /> 1749 - <SettingsList.ItemText> 1750 - <Trans>{`LibreTranslate Instance`}</Trans> 1751 - </SettingsList.ItemText> 1752 - <SettingsList.BadgeButton 1753 - label={_(msg`Change`)} 1754 - onPress={() => setLibreTranslateInstanceControl.open()} 1755 - /> 1756 - </SettingsList.Item> 1757 - )} 1758 - 1759 - <SettingsList.Divider /> 1760 - 1761 - <SettingsList.Item> 1762 - <SettingsList.ItemIcon icon={BeakerIcon} /> 1763 - <SettingsList.ItemText> 1764 - <Trans>OpenRouter API Key</Trans> 1765 - </SettingsList.ItemText> 1766 - <SettingsList.BadgeButton 1767 - label={openRouterConfigured ? _(msg`Change`) : _(msg`Set`)} 1768 - onPress={() => setOpenRouterApiKeyControl.open()} 1769 - /> 1770 - </SettingsList.Item> 1771 - 1772 - <SettingsList.Item> 1773 - <Admonition type="info" style={[a.flex_1]}> 1774 - <Trans> 1775 - Set your OpenRouter API key to enable AI-powered alt text 1776 - generation for images in the composer. Get an API key at{' '} 1777 - <InlineLinkText 1778 - to="https://openrouter.ai" 1779 - label="openrouter.ai"> 1780 - openrouter.ai 1781 - </InlineLinkText> 1782 - </Trans> 1783 - </Admonition> 1784 - </SettingsList.Item> 1785 - 1786 - {openRouterConfigured && ( 1787 - <SettingsList.Item> 1788 - <SettingsList.ItemIcon icon={BeakerIcon} /> 1789 - <SettingsList.ItemText> 1790 - <Trans>{`OpenRouter Model`}</Trans> 1791 - </SettingsList.ItemText> 1792 - <SettingsList.BadgeButton 1793 - label={_(msg`Change`)} 1794 - onPress={() => setOpenRouterModelControl.open()} 1795 - /> 1796 - </SettingsList.Item> 1797 - )} 1798 - 1799 - {openRouterConfigured && ( 1800 - <SettingsList.Item> 1801 - <Admonition type="info" style={[a.flex_1]}> 1802 - <Trans> 1803 - Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} 1804 - <InlineLinkText 1805 - to="https://openrouter.ai/models?fmt=cards&input_modalities=image&order=most-popular" 1806 - label="openrouter.ai"> 1807 - Search models 1808 - </InlineLinkText> 1809 - </Trans> 1810 - </Admonition> 1811 - </SettingsList.Item> 1812 - )} 1813 - 1814 - {openRouterConfigured && ( 1815 - <SettingsList.Item> 1816 - <SettingsList.ItemIcon icon={BeakerIcon} /> 1817 - <SettingsList.ItemText> 1818 - <Trans>Alt Text Prompt</Trans> 1819 - </SettingsList.ItemText> 1820 - <SettingsList.BadgeButton 1821 - label={_(msg`Change`)} 1822 - onPress={() => setOpenRouterPromptControl.open()} 1823 - /> 1824 - </SettingsList.Item> 1825 - )} 1826 - 1827 - {openRouterConfigured && ( 1828 - <SettingsList.Item> 1829 - <Admonition type="info" style={[a.flex_1]}> 1830 - <Trans> 1831 - Customize the prompt sent to the AI model when generating alt 1832 - text. Leave empty to use the default prompt. 1833 - </Trans> 1834 - </Admonition> 1835 - </SettingsList.Item> 1836 - )} 1837 - 1838 - <SettingsList.Divider /> 1839 - 1840 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 1841 - <SettingsList.ItemIcon icon={VisibilityIcon} /> 1842 - <SettingsList.ItemText> 1843 - <Trans>Metrics</Trans> 1844 - </SettingsList.ItemText> 1845 - 1846 - <Toggle.Item 1847 - name="disable_likes_metrics" 1848 - label={_(msg`Disable likes metrics`)} 1849 - value={disableLikesMetrics} 1850 - onChange={value => setDisableLikesMetrics(value)} 1851 - style={[a.w_full]}> 1852 - <Toggle.LabelText style={[a.flex_1]}> 1853 - <Trans>Disable likes metrics</Trans> 1854 - </Toggle.LabelText> 1855 - <Toggle.Platform /> 1856 - </Toggle.Item> 1857 - 1858 - <Toggle.Item 1859 - name="disable_reposts_metrics" 1860 - label={_(msg`Disable reposts metrics`)} 1861 - value={disableRepostsMetrics} 1862 - onChange={value => setDisableRepostsMetrics(value)} 1863 - style={[a.w_full]}> 1864 - <Toggle.LabelText style={[a.flex_1]}> 1865 - <Trans>Disable reposts metrics</Trans> 1866 - </Toggle.LabelText> 1867 - <Toggle.Platform /> 1868 - </Toggle.Item> 1869 - 1870 - <Toggle.Item 1871 - name="disable_quotes_metrics" 1872 - label={_(msg`Disable quotes metrics`)} 1873 - value={disableQuotesMetrics} 1874 - onChange={value => setDisableQuotesMetrics(value)} 1875 - style={[a.w_full]}> 1876 - <Toggle.LabelText style={[a.flex_1]}> 1877 - <Trans>Disable quotes metrics</Trans> 1878 - </Toggle.LabelText> 1879 - <Toggle.Platform /> 1880 - </Toggle.Item> 1881 - 1882 - <Toggle.Item 1883 - name="disable_saves_metrics" 1884 - label={_(msg`Disable saves metrics`)} 1885 - value={disableSavesMetrics} 1886 - onChange={value => setDisableSavesMetrics(value)} 1887 - style={[a.w_full]}> 1888 - <Toggle.LabelText style={[a.flex_1]}> 1889 - <Trans>Disable saves metrics</Trans> 1890 - </Toggle.LabelText> 1891 - <Toggle.Platform /> 1892 - </Toggle.Item> 1893 - 1894 - <Toggle.Item 1895 - name="disable_reply_metrics" 1896 - label={_(msg`Disable reply metrics`)} 1897 - value={disableReplyMetrics} 1898 - onChange={value => setDisableReplyMetrics(value)} 1899 - style={[a.w_full]}> 1900 - <Toggle.LabelText style={[a.flex_1]}> 1901 - <Trans>Disable reply metrics</Trans> 1902 - </Toggle.LabelText> 1903 - <Toggle.Platform /> 1904 - </Toggle.Item> 1905 - 1906 - <Toggle.Item 1907 - name="disable_followers_metrics" 1908 - label={_(msg`Disable followers metrics`)} 1909 - value={disableFollowersMetrics} 1910 - onChange={value => setDisableFollowersMetrics(value)} 1911 - style={[a.w_full]}> 1912 - <Toggle.LabelText style={[a.flex_1]}> 1913 - <Trans>Disable followers metrics</Trans> 1914 - </Toggle.LabelText> 1915 - <Toggle.Platform /> 1916 - </Toggle.Item> 1917 - 1918 - <Toggle.Item 1919 - name="disable_following_metrics" 1920 - label={_(msg`Disable following metrics`)} 1921 - value={disableFollowingMetrics} 1922 - onChange={value => setDisableFollowingMetrics(value)} 1923 - style={[a.w_full]}> 1924 - <Toggle.LabelText style={[a.flex_1]}> 1925 - <Trans>Disable following metrics</Trans> 1926 - </Toggle.LabelText> 1927 - <Toggle.Platform /> 1928 - </Toggle.Item> 1929 - 1930 - <Toggle.Item 1931 - name="disable_followed_by_metrics" 1932 - label={_(msg`Disable "followed by" metrics`)} 1933 - value={disableFollowedByMetrics} 1934 - onChange={value => setDisableFollowedByMetrics(value)} 1935 - style={[a.w_full]}> 1936 - <Toggle.LabelText style={[a.flex_1]}> 1937 - <Trans>Disable "followed by" metrics</Trans> 1938 - </Toggle.LabelText> 1939 - <Toggle.Platform /> 1940 - </Toggle.Item> 1941 - 1942 - <Toggle.Item 1943 - name="show_follows_you_badge" 1944 - label={_(msg`Show "Follows you" badge`)} 1945 - value={showFollowsYouBadge} 1946 - onChange={value => setShowFollowsYouBadge(value)} 1947 - style={[a.w_full]}> 1948 - <Toggle.LabelText style={[a.flex_1]}> 1949 - <Trans>Show "Follows you" badge</Trans> 1950 - </Toggle.LabelText> 1951 - <Toggle.Platform /> 1952 - </Toggle.Item> 1953 - 1954 - <Toggle.Item 1955 - name="disable_posts_metrics" 1956 - label={_(msg`Disable post counts metrics`)} 1957 - value={disablePostsMetrics} 1958 - onChange={value => setDisablePostsMetrics(value)} 1959 - style={[a.w_full]}> 1960 - <Toggle.LabelText style={[a.flex_1]}> 1961 - <Trans>Disable post counts metrics</Trans> 1962 - </Toggle.LabelText> 1963 - <Toggle.Platform /> 1964 - </Toggle.Item> 1965 - </SettingsList.Group> 1966 - 1967 - <SettingsList.Divider /> 1968 - 1969 - <SettingsList.Item> 1970 - <SettingsList.ItemIcon icon={EarthIcon} /> 1971 - <SettingsList.ItemText> 1972 - <Trans>{`Image CDN`}</Trans> 1973 - </SettingsList.ItemText> 1974 - <SettingsList.BadgeButton 1975 - label={_(msg`Change`)} 1976 - onPress={() => setImageCdnHostControl.open()} 1977 - /> 1978 - </SettingsList.Item> 1979 - <SettingsList.Item> 1980 - <Admonition type="info" style={[a.flex_1]}> 1981 - <Trans> 1982 - Override the CDN host for all images. Current:  1983 - <InlineLinkText to={imageCdnHost} label={imageCdnHost}> 1984 - {imageCdnHost} 1985 - </InlineLinkText> 1986 - </Trans> 1987 - </Admonition> 1988 - </SettingsList.Item> 1989 - 1990 - <SettingsList.Item> 1991 - <SettingsList.ItemIcon icon={EarthIcon} /> 1992 - <SettingsList.ItemText> 1993 - <Trans>{`PLC Directory`}</Trans> 1994 - </SettingsList.ItemText> 1995 - <SettingsList.BadgeButton 1996 - label={_(msg`Change`)} 1997 - onPress={() => setPlcDirectoryControl.open()} 1998 - /> 1999 - </SettingsList.Item> 2000 - <SettingsList.Item> 2001 - <Admonition type="info" style={[a.flex_1]}> 2002 - <Trans> 2003 - Override the PLC directory used to resolve DIDs. Current:  2004 - <InlineLinkText to={plcDirectory} label={plcDirectory}> 2005 - {plcDirectory} 2006 - </InlineLinkText> 2007 - </Trans> 2008 - </Admonition> 2009 - </SettingsList.Item> 2010 - 2011 - <SettingsList.Divider /> 2012 - 2013 - <SettingsList.Item> 2014 - <SettingsList.ItemIcon icon={StarIcon} /> 2015 - <SettingsList.ItemText> 2016 - <Trans>{`Custom AppView DID`}</Trans> 2017 - </SettingsList.ItemText> 2018 - <SettingsList.BadgeButton 2019 - label={customAppViewDid ? _(msg`Change`) : _(msg`Set`)} 2020 - onPress={() => setCustomAppViewDidControl.open()} 2021 - /> 2022 - </SettingsList.Item> 2023 - 2024 - <SettingsList.Divider /> 2025 - 2026 - <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 2027 - <SettingsList.ItemIcon icon={RaisingHandIcon} /> 2028 - <SettingsList.ItemText> 2029 - <Trans>Labelers</Trans> 2030 - </SettingsList.ItemText> 2031 - <Toggle.Item 2032 - name="no_app_labelers" 2033 - label={_(msg`Do not declare any app labelers`)} 2034 - value={noAppLabelers} 2035 - onChange={value => setNoAppLabelers(value)} 2036 - style={[a.w_full]}> 2037 - <Toggle.LabelText style={[a.flex_1]}> 2038 - <Trans>Do not declare any default app labelers</Trans> 2039 - </Toggle.LabelText> 2040 - <Toggle.Platform /> 2041 - </Toggle.Item> 2042 - </SettingsList.Group> 2043 - 2044 - <SettingsList.Item> 2045 - <Admonition type="warning" style={[a.flex_1]}> 2046 - <Trans>Restart the app after changing this setting.</Trans> 2047 - </Admonition> 2048 - </SettingsList.Item> 2049 - <SettingsList.Item> 2050 - <Admonition type="tip" style={[a.flex_1]}> 2051 - <Trans> 2052 - Some App Views will default to using an app labeler if you have 2053 - no labelers, so consider subscribing to at least one labeler if 2054 - you have issues. 2055 - </Trans> 2056 - </Admonition> 2057 - </SettingsList.Item> 2058 - <SettingsList.Item> 2059 - <Admonition type="info" style={[a.flex_1]}> 2060 - <Trans> 2061 - App labelers are mandatory top-level labelers that can perform 2062 - "takedowns". This setting does not influence geolocation-based 2063 - labelers. 2064 - </Trans> 2065 - </Admonition> 2066 - </SettingsList.Item> 2067 - </SettingsList.Container> 2068 - </Layout.Content> 2069 - <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 2070 - <CustomAppViewDidDialog control={setCustomAppViewDidControl} /> 2071 - <FaviconServiceDialog control={setFaviconServiceControl} /> 2072 - <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 2073 - <LibreTranslateInstanceDialog 2074 - control={setLibreTranslateInstanceControl} 2075 - /> 2076 - <ImageCdnHostDialog control={setImageCdnHostControl} /> 2077 - <PlcDirectoryDialog control={setPlcDirectoryControl} /> 2078 - <PostReplacementDialog control={setPostReplacementDialogControl} /> 2079 - <OpenRouterApiKeyDialog control={setOpenRouterApiKeyControl} /> 2080 - <OpenRouterModelDialog control={setOpenRouterModelControl} /> 2081 - <OpenRouterPromptDialog control={setOpenRouterPromptControl} /> 2082 - </Layout.Screen> 2083 - ) 2084 - } 2085 - 2086 - const styles = { 2087 - textInput: { 2088 - borderWidth: 1, 2089 - borderRadius: 6, 2090 - paddingHorizontal: 14, 2091 - paddingVertical: 10, 2092 - fontSize: 16, 2093 - }, 2094 - }
+303
src/screens/Settings/RunesSettings/BadgesSettings.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 4 + import {Trans, useLingui} from '@lingui/react/macro' 5 + 6 + import {usePalette} from '#/lib/hooks/usePalette' 7 + import * as persisted from '#/state/persisted' 8 + import { 9 + useDeerVerificationEnabled, 10 + useDeerVerificationTrusted, 11 + useSetDeerVerificationEnabled, 12 + } from '#/state/preferences/deer-verification' 13 + import { 14 + useFaviconService, 15 + useSetFaviconService, 16 + } from '#/state/preferences/favicon-service' 17 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 18 + import { 19 + usePdsLabelEnabled, 20 + usePdsLabelHideBskyPds, 21 + useSetPdsLabelEnabled, 22 + useSetPdsLabelHideBskyPds, 23 + } from '#/state/preferences/pds-label' 24 + import {useProfilesQuery} from '#/state/queries/profile' 25 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 26 + import {atoms as a, useBreakpoints} from '#/alf' 27 + import {Admonition} from '#/components/Admonition' 28 + import {Button, ButtonText} from '#/components/Button' 29 + import * as Dialog from '#/components/Dialog' 30 + import * as Toggle from '#/components/forms/Toggle' 31 + import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 32 + import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 33 + import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 34 + import {Text} from '#/components/Typography' 35 + import {IS_WEB} from '#/env' 36 + import {SearchProfileCard} from '../../Search/components/SearchProfileCard' 37 + import {RunesScreenLayout} from './components/RunesScreenLayout' 38 + 39 + export function RunesBadgesSettingsScreen() { 40 + const {t: l} = useLingui() 41 + 42 + const deerVerificationEnabled = useDeerVerificationEnabled() 43 + const setDeerVerificationEnabled = useSetDeerVerificationEnabled() 44 + const setTrustedVerifiersDialogControl = Dialog.useDialogControl() 45 + 46 + const pdsLabelEnabled = usePdsLabelEnabled() 47 + const setPdsLabelEnabled = useSetPdsLabelEnabled() 48 + const pdsLabelHideBskyPds = usePdsLabelHideBskyPds() 49 + const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds() 50 + const setFaviconServiceControl = Dialog.useDialogControl() 51 + 52 + return ( 53 + <RunesScreenLayout titleText={l`Badges`}> 54 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 55 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 56 + <SettingsList.ItemText> 57 + <Trans>Verification</Trans> 58 + </SettingsList.ItemText> 59 + <Toggle.Item 60 + name="custom_verifications" 61 + label={l`Select your own set of trusted verifiers, and operate as a verifier`} 62 + value={deerVerificationEnabled} 63 + onChange={value => setDeerVerificationEnabled(value)} 64 + style={[a.w_full]}> 65 + <Toggle.LabelText style={[a.flex_1]}> 66 + <Trans> 67 + Select your own set of trusted verifiers, and operate as a 68 + verifier 69 + </Trans> 70 + </Toggle.LabelText> 71 + <Toggle.Platform /> 72 + </Toggle.Item> 73 + </SettingsList.Group> 74 + 75 + <SettingsList.Item> 76 + <Admonition type="warning" style={[a.flex_1]}> 77 + <Trans> 78 + May slow down the client or fail to find all labels. Revoke and 79 + grant trust in the meatball menu on a profile.{' '} 80 + {deerVerificationEnabled 81 + ? 'You currently' 82 + : 'If enabled, you would'}{' '} 83 + trust the following verifiers: 84 + </Trans> 85 + </Admonition> 86 + </SettingsList.Item> 87 + 88 + <SettingsList.Item> 89 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 90 + <SettingsList.ItemText> 91 + <Trans>{`Trusted Verifiers`}</Trans> 92 + </SettingsList.ItemText> 93 + <SettingsList.BadgeButton 94 + label={l`View`} 95 + onPress={() => setTrustedVerifiersDialogControl.open()} 96 + /> 97 + </SettingsList.Item> 98 + 99 + <SettingsList.Divider /> 100 + 101 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 102 + <SettingsList.ItemIcon icon={PaintRollerIcon} /> 103 + <SettingsList.ItemText> 104 + <Trans>PDS badges</Trans> 105 + </SettingsList.ItemText> 106 + <Toggle.Item 107 + name="pds_label_badge" 108 + label={l`Show a PDS badge next to the display name on profiles`} 109 + value={pdsLabelEnabled} 110 + onChange={value => setPdsLabelEnabled(value)} 111 + style={[a.w_full]}> 112 + <Toggle.LabelText style={[a.flex_1]}> 113 + <Trans>Show a PDS badge next to the display name on profiles</Trans> 114 + </Toggle.LabelText> 115 + <Toggle.Platform /> 116 + </Toggle.Item> 117 + {pdsLabelEnabled && ( 118 + <Toggle.Item 119 + name="pds_label_hide_bsky" 120 + label={l`Hide PDS badge for Bluesky-hosted accounts`} 121 + value={pdsLabelHideBskyPds} 122 + onChange={value => setPdsLabelHideBskyPds(value)} 123 + style={[a.w_full]}> 124 + <Toggle.LabelText style={[a.flex_1]}> 125 + <Trans>Hide PDS badge for Bluesky-hosted accounts</Trans> 126 + </Toggle.LabelText> 127 + <Toggle.Platform /> 128 + </Toggle.Item> 129 + )} 130 + </SettingsList.Group> 131 + 132 + {pdsLabelEnabled && ( 133 + <SettingsList.Item> 134 + <SettingsList.ItemIcon icon={StarIcon} /> 135 + <SettingsList.ItemText> 136 + <Trans>Favicon service</Trans> 137 + </SettingsList.ItemText> 138 + <SettingsList.BadgeButton 139 + label={l`Change`} 140 + onPress={() => setFaviconServiceControl.open()} 141 + /> 142 + </SettingsList.Item> 143 + )} 144 + 145 + <FaviconServiceDialog control={setFaviconServiceControl} /> 146 + <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 147 + </RunesScreenLayout> 148 + ) 149 + } 150 + 151 + function FaviconServiceDialog({control}: {control: Dialog.DialogControlProps}) { 152 + const pal = usePalette('default') 153 + const {t: l} = useLingui() 154 + 155 + const faviconService = useFaviconService() 156 + const [url, setUrl] = useState(faviconService ?? '') 157 + const [inputVersion, setInputVersion] = useState(0) 158 + const setFaviconService = useSetFaviconService() 159 + 160 + const updateInputValue = (nextUrl: string) => { 161 + setUrl(nextUrl) 162 + setInputVersion(version => version + 1) 163 + } 164 + 165 + const presets = [ 166 + 'https://twenty-icons.com/(pds)', 167 + 'https://favicon.im/(pds)?larger=true&throw-error-on-404=true', 168 + 'https://favicon.blueat.net/(pds)?larger=true&throw-error-on-404=true', 169 + ] 170 + 171 + return ( 172 + <Dialog.Outer 173 + control={control} 174 + nativeOptions={{preventExpansion: true}} 175 + onClose={() => updateInputValue(faviconService ?? '')}> 176 + <Dialog.Handle /> 177 + <Dialog.ScrollableInner label={l`Favicon Service URL`}> 178 + <View style={[a.gap_sm, a.pb_lg]}> 179 + <Text style={[a.text_2xl, a.font_bold]}> 180 + <Trans>Favicon Service URL</Trans> 181 + </Text> 182 + <Text style={[a.text_sm, {color: pal.colors.textLight}]}> 183 + <Trans> 184 + (pds) is replaced with the domain of an account&apos;s host. 185 + </Trans> 186 + </Text> 187 + </View> 188 + 189 + <View style={a.gap_lg}> 190 + <Dialog.Input 191 + key={`favicon-service-input-${inputVersion}`} 192 + label="Text input field" 193 + autoFocus 194 + style={[styles.textInput, pal.border, pal.text]} 195 + onChangeText={setUrl} 196 + placeholder={persisted.defaults.faviconService} 197 + placeholderTextColor={pal.colors.textLight} 198 + onSubmitEditing={() => { 199 + setFaviconService(url.trim()) 200 + control.close() 201 + }} 202 + accessibilityHint={l`Enter the favicon service URL with (pds) as placeholder`} 203 + defaultValue={url} 204 + /> 205 + 206 + <View style={[a.flex_row, a.flex_wrap, a.mb_xs]}> 207 + {presets.map(preset => ( 208 + <Button 209 + key={preset} 210 + variant="ghost" 211 + color="primary" 212 + label={preset} 213 + style={[a.px_sm, a.py_xs, a.rounded_sm, a.gap_sm]} 214 + onPress={() => updateInputValue(preset)}> 215 + <ButtonText>{preset}</ButtonText> 216 + </Button> 217 + ))} 218 + </View> 219 + 220 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 221 + <Button 222 + label={l`Save`} 223 + size="large" 224 + onPress={() => { 225 + setFaviconService(url.trim()) 226 + control.close() 227 + }} 228 + variant="solid" 229 + color="primary" 230 + disabled={url.length > 0 && !url.includes('(pds)')}> 231 + <ButtonText> 232 + <Trans>Save</Trans> 233 + </ButtonText> 234 + </Button> 235 + </View> 236 + </View> 237 + 238 + <Dialog.Close /> 239 + </Dialog.ScrollableInner> 240 + </Dialog.Outer> 241 + ) 242 + } 243 + 244 + function TrustedVerifiersDialog({ 245 + control, 246 + }: { 247 + control: Dialog.DialogControlProps 248 + }) { 249 + const {t: l} = useLingui() 250 + 251 + return ( 252 + <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 253 + <Dialog.Handle /> 254 + <Dialog.ScrollableInner label={l`Trusted Verifiers`}> 255 + <View style={[a.gap_sm, a.pb_lg]}> 256 + <Text style={[a.text_2xl, a.font_bold]}> 257 + <Trans>Trusted Verifiers</Trans> 258 + </Text> 259 + </View> 260 + 261 + <TrustedVerifiers /> 262 + 263 + <Dialog.Close /> 264 + </Dialog.ScrollableInner> 265 + </Dialog.Outer> 266 + ) 267 + } 268 + 269 + function TrustedVerifiers() { 270 + const trusted = useDeerVerificationTrusted() 271 + const moderationOpts = useModerationOpts() 272 + const {gtMobile} = useBreakpoints() 273 + 274 + const results = useProfilesQuery({ 275 + handles: Array.from(trusted), 276 + }) 277 + 278 + if (!results.data || moderationOpts === undefined) { 279 + return null 280 + } 281 + 282 + return ( 283 + <View style={[gtMobile ? a.pl_md : a.pl_sm, a.pb_sm]}> 284 + {results.data.profiles.map(profile => ( 285 + <SearchProfileCard 286 + key={profile.did} 287 + profile={profile as ProfileViewBasic} 288 + moderationOpts={moderationOpts} 289 + /> 290 + ))} 291 + </View> 292 + ) 293 + } 294 + 295 + const styles = { 296 + textInput: { 297 + borderWidth: 1, 298 + borderRadius: 6, 299 + paddingHorizontal: 14, 300 + paddingVertical: 10, 301 + fontSize: 16, 302 + }, 303 + }
+231
src/screens/Settings/RunesSettings/DisplaySettings.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {Trans, useLingui} from '@lingui/react/macro' 4 + 5 + import {usePalette} from '#/lib/hooks/usePalette' 6 + import {dynamicActivate} from '#/locale/i18n' 7 + import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web' 8 + import {type AppLanguage} from '#/locale/languages' 9 + import { 10 + useHighQualityImages, 11 + useSetHighQualityImages, 12 + } from '#/state/preferences/high-quality-images' 13 + import { 14 + usePostReplacement, 15 + useSetPostReplacement, 16 + } from '#/state/preferences/post-name-replacement' 17 + import { 18 + useRepostCarouselEnabled, 19 + useSetRepostCarouselEnabled, 20 + } from '#/state/preferences/repost-carousel-enabled' 21 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 22 + import {atoms as a} from '#/alf' 23 + import {Admonition} from '#/components/Admonition' 24 + import {Button, ButtonText} from '#/components/Button' 25 + import * as Dialog from '#/components/Dialog' 26 + import * as Toggle from '#/components/forms/Toggle' 27 + import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' 28 + import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' 29 + import {Repost_Stroke2_Corner3_Rounded as RepostIcon} from '#/components/icons/Repost' 30 + import {Text} from '#/components/Typography' 31 + import {IS_WEB} from '#/env' 32 + import {RunesScreenLayout} from './components/RunesScreenLayout' 33 + 34 + export function RunesDisplaySettingsScreen() { 35 + const {t: l} = useLingui() 36 + 37 + const repostCarouselEnabled = useRepostCarouselEnabled() 38 + const setRepostCarouselEnabled = useSetRepostCarouselEnabled() 39 + 40 + const highQualityImages = useHighQualityImages() 41 + const setHighQualityImages = useSetHighQualityImages() 42 + 43 + const setPostReplacementDialogControl = Dialog.useDialogControl() 44 + 45 + return ( 46 + <RunesScreenLayout titleText={l`Display`}> 47 + <Toggle.Item 48 + name="repost_carousel" 49 + label={l`Combine reposts into a horizontal carousel`} 50 + value={repostCarouselEnabled} 51 + onChange={value => setRepostCarouselEnabled(value)}> 52 + <SettingsList.Item> 53 + <SettingsList.ItemIcon icon={RepostIcon} /> 54 + <SettingsList.ItemText> 55 + <Trans>Combine reposts into a horizontal carousel</Trans> 56 + </SettingsList.ItemText> 57 + <Toggle.Platform /> 58 + </SettingsList.Item> 59 + </Toggle.Item> 60 + <Toggle.Item 61 + name="high_quality_images" 62 + label={l`Display images in higher quality`} 63 + value={highQualityImages} 64 + onChange={value => setHighQualityImages(value)}> 65 + <SettingsList.Item> 66 + <SettingsList.ItemIcon icon={ImageIcon} /> 67 + <SettingsList.ItemText> 68 + <Trans>Display images in higher quality</Trans> 69 + </SettingsList.ItemText> 70 + <Toggle.Platform /> 71 + </SettingsList.Item> 72 + </Toggle.Item> 73 + <SettingsList.Item> 74 + <Admonition type="info" style={[a.flex_1]}> 75 + <Trans> 76 + Images will be served as PNG instead of WEBP. Images will take 77 + longer to load and use more bandwidth. 78 + </Trans> 79 + </Admonition> 80 + </SettingsList.Item> 81 + <SettingsList.Item> 82 + <SettingsList.ItemIcon icon={PencilIcon} /> 83 + <SettingsList.ItemText> 84 + <Trans>{`Custom post phrase`}</Trans> 85 + </SettingsList.ItemText> 86 + <SettingsList.BadgeButton 87 + label={l`Change`} 88 + onPress={() => setPostReplacementDialogControl.open()} 89 + /> 90 + </SettingsList.Item> 91 + <PostReplacementDialog control={setPostReplacementDialogControl} /> 92 + </RunesScreenLayout> 93 + ) 94 + } 95 + 96 + function PostReplacementDialog({ 97 + control, 98 + }: { 99 + control: Dialog.DialogControlProps 100 + }) { 101 + const pal = usePalette('default') 102 + const {t: l, i18n} = useLingui() 103 + 104 + const postReplacement = usePostReplacement() 105 + const setPostReplacement = useSetPostReplacement() 106 + 107 + const [singular, setSingular] = useState(postReplacement.postName) 108 + const [plural, setPlural] = useState(postReplacement.postsName) 109 + const [pluralManuallyEdited, setPluralManuallyEdited] = useState(false) 110 + 111 + const submit = async () => { 112 + setPostReplacement({ 113 + enabled: singular.trim().toLowerCase() !== 'post', 114 + postName: singular, 115 + postsName: plural, 116 + }) 117 + 118 + const locale = i18n.locale 119 + await (IS_WEB 120 + ? dynamicActivateWeb(locale as AppLanguage) 121 + : dynamicActivate(locale as AppLanguage)) 122 + 123 + control.close() 124 + } 125 + 126 + const handleSingularChange = (value: string) => { 127 + setSingular(value) 128 + if (!pluralManuallyEdited) { 129 + setPlural(`${value}s`) 130 + } 131 + } 132 + 133 + const handlePluralChange = (value: string) => { 134 + setPlural(value) 135 + setPluralManuallyEdited(true) 136 + } 137 + 138 + const handlePresetSelect = (singularForm: string, pluralForm: string) => { 139 + setSingular(singularForm) 140 + setPlural(pluralForm) 141 + setPluralManuallyEdited(false) 142 + } 143 + 144 + return ( 145 + <Dialog.Outer 146 + control={control} 147 + nativeOptions={{preventExpansion: true}} 148 + onClose={() => { 149 + setSingular(postReplacement.postName) 150 + setPlural(postReplacement.postsName) 151 + setPluralManuallyEdited(false) 152 + }}> 153 + <Dialog.Handle /> 154 + <Dialog.ScrollableInner label={l`Custom post phrase`}> 155 + <Text style={[a.text_2xl, a.font_bold, a.pb_lg]}> 156 + <Trans>Custom post phrase</Trans> 157 + </Text> 158 + 159 + <View style={a.gap_lg}> 160 + <Dialog.Input 161 + label="Singular form" 162 + autoFocus 163 + style={[styles.textInput, pal.border, pal.text]} 164 + onChangeText={handleSingularChange} 165 + placeholder="skeet" 166 + placeholderTextColor={pal.colors.textLight} 167 + accessibilityHint={l`Input the singular form (e.g., "skeet")`} 168 + value={singular} 169 + /> 170 + <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> 171 + {[ 172 + {singular: 'post', plural: 'posts'}, 173 + {singular: 'skeet', plural: 'skeets'}, 174 + {singular: 'note', plural: 'notes'}, 175 + {singular: 'woot', plural: 'woots'}, 176 + {singular: 'toot', plural: 'toots'}, 177 + {singular: 'silly', plural: 'sillies'}, 178 + ].map(preset => ( 179 + <Button 180 + key={preset.singular} 181 + variant="ghost" 182 + color="primary" 183 + label={preset.singular} 184 + style={[a.px_sm, a.py_xs, a.rounded_sm]} 185 + onPress={() => 186 + handlePresetSelect(preset.singular, preset.plural) 187 + }> 188 + <ButtonText>{preset.singular}</ButtonText> 189 + </Button> 190 + ))} 191 + </View> 192 + <Dialog.Input 193 + label="Plural form" 194 + style={[styles.textInput, pal.border, pal.text, a.mt_lg]} 195 + onChangeText={handlePluralChange} 196 + placeholder="skeets" 197 + placeholderTextColor={pal.colors.textLight} 198 + accessibilityHint={l`Input the plural form (e.g., "skeets")`} 199 + value={plural} 200 + /> 201 + 202 + <View style={IS_WEB && [a.flex_row, a.justify_end, a.pt_lg]}> 203 + <Button 204 + label={l`Save`} 205 + size="large" 206 + onPress={() => void submit()} 207 + variant="solid" 208 + color="primary" 209 + disabled={!singular.trim() || !plural.trim()}> 210 + <ButtonText> 211 + <Trans>Save</Trans> 212 + </ButtonText> 213 + </Button> 214 + </View> 215 + </View> 216 + 217 + <Dialog.Close /> 218 + </Dialog.ScrollableInner> 219 + </Dialog.Outer> 220 + ) 221 + } 222 + 223 + const styles = { 224 + textInput: { 225 + borderWidth: 1, 226 + borderRadius: 6, 227 + paddingHorizontal: 14, 228 + paddingVertical: 10, 229 + fontSize: 16, 230 + }, 231 + }
+199
src/screens/Settings/RunesSettings/ImpressionsSettings.tsx
··· 1 + import {Trans, useLingui} from '@lingui/react/macro' 2 + 3 + import { 4 + useDisableFollowedByMetrics, 5 + useSetDisableFollowedByMetrics, 6 + } from '#/state/preferences/disable-followed-by-metrics' 7 + import { 8 + useDisableFollowersMetrics, 9 + useSetDisableFollowersMetrics, 10 + } from '#/state/preferences/disable-followers-metrics' 11 + import { 12 + useDisableFollowingMetrics, 13 + useSetDisableFollowingMetrics, 14 + } from '#/state/preferences/disable-following-metrics' 15 + import { 16 + useDisableLikesMetrics, 17 + useSetDisableLikesMetrics, 18 + } from '#/state/preferences/disable-likes-metrics' 19 + import { 20 + useDisablePostsMetrics, 21 + useSetDisablePostsMetrics, 22 + } from '#/state/preferences/disable-posts-metrics' 23 + import { 24 + useDisableQuotesMetrics, 25 + useSetDisableQuotesMetrics, 26 + } from '#/state/preferences/disable-quotes-metrics' 27 + import { 28 + useDisableReplyMetrics, 29 + useSetDisableReplyMetrics, 30 + } from '#/state/preferences/disable-reply-metrics' 31 + import { 32 + useDisableRepostsMetrics, 33 + useSetDisableRepostsMetrics, 34 + } from '#/state/preferences/disable-reposts-metrics' 35 + import { 36 + useDisableSavesMetrics, 37 + useSetDisableSavesMetrics, 38 + } from '#/state/preferences/disable-saves-metrics' 39 + import { 40 + useSetShowFollowsYouBadge, 41 + useShowFollowsYouBadge, 42 + } from '#/state/preferences/show-follows-you-badge' 43 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 44 + import {atoms as a} from '#/alf' 45 + import * as Toggle from '#/components/forms/Toggle' 46 + import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 47 + import {RunesScreenLayout} from './components/RunesScreenLayout' 48 + 49 + export function RunesImpressionsSettingsScreen() { 50 + const {t: l} = useLingui() 51 + 52 + const disableLikesMetrics = useDisableLikesMetrics() 53 + const setDisableLikesMetrics = useSetDisableLikesMetrics() 54 + const disableRepostsMetrics = useDisableRepostsMetrics() 55 + const setDisableRepostsMetrics = useSetDisableRepostsMetrics() 56 + const disableQuotesMetrics = useDisableQuotesMetrics() 57 + const setDisableQuotesMetrics = useSetDisableQuotesMetrics() 58 + const disableSavesMetrics = useDisableSavesMetrics() 59 + const setDisableSavesMetrics = useSetDisableSavesMetrics() 60 + const disableReplyMetrics = useDisableReplyMetrics() 61 + const setDisableReplyMetrics = useSetDisableReplyMetrics() 62 + const disableFollowersMetrics = useDisableFollowersMetrics() 63 + const setDisableFollowersMetrics = useSetDisableFollowersMetrics() 64 + const disableFollowingMetrics = useDisableFollowingMetrics() 65 + const setDisableFollowingMetrics = useSetDisableFollowingMetrics() 66 + const disableFollowedByMetrics = useDisableFollowedByMetrics() 67 + const setDisableFollowedByMetrics = useSetDisableFollowedByMetrics() 68 + const disablePostsMetrics = useDisablePostsMetrics() 69 + const setDisablePostsMetrics = useSetDisablePostsMetrics() 70 + const showFollowsYouBadge = useShowFollowsYouBadge() 71 + const setShowFollowsYouBadge = useSetShowFollowsYouBadge() 72 + 73 + return ( 74 + <RunesScreenLayout titleText={l`Impressions`}> 75 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 76 + <SettingsList.ItemIcon icon={VisibilityIcon} /> 77 + <SettingsList.ItemText> 78 + <Trans>Posts</Trans> 79 + </SettingsList.ItemText> 80 + <Toggle.Item 81 + name="disable_likes_metrics" 82 + label={l`Remove likes counts`} 83 + value={disableLikesMetrics} 84 + onChange={value => setDisableLikesMetrics(value)} 85 + style={[a.w_full]}> 86 + <Toggle.LabelText style={[a.flex_1]}> 87 + <Trans>Remove likes counts</Trans> 88 + </Toggle.LabelText> 89 + <Toggle.Platform /> 90 + </Toggle.Item> 91 + <Toggle.Item 92 + name="disable_reposts_metrics" 93 + label={l`Remove reposts counts`} 94 + value={disableRepostsMetrics} 95 + onChange={value => setDisableRepostsMetrics(value)} 96 + style={[a.w_full]}> 97 + <Toggle.LabelText style={[a.flex_1]}> 98 + <Trans>Remove reposts counts</Trans> 99 + </Toggle.LabelText> 100 + <Toggle.Platform /> 101 + </Toggle.Item> 102 + <Toggle.Item 103 + name="disable_quotes_metrics" 104 + label={l`Remove quotes counts`} 105 + value={disableQuotesMetrics} 106 + onChange={value => setDisableQuotesMetrics(value)} 107 + style={[a.w_full]}> 108 + <Toggle.LabelText style={[a.flex_1]}> 109 + <Trans>Remove quotes counts</Trans> 110 + </Toggle.LabelText> 111 + <Toggle.Platform /> 112 + </Toggle.Item> 113 + <Toggle.Item 114 + name="disable_saves_metrics" 115 + label={l`Remove saves counts`} 116 + value={disableSavesMetrics} 117 + onChange={value => setDisableSavesMetrics(value)} 118 + style={[a.w_full]}> 119 + <Toggle.LabelText style={[a.flex_1]}> 120 + <Trans>Remove saves counts</Trans> 121 + </Toggle.LabelText> 122 + <Toggle.Platform /> 123 + </Toggle.Item> 124 + <Toggle.Item 125 + name="disable_reply_metrics" 126 + label={l`Remove reply counts`} 127 + value={disableReplyMetrics} 128 + onChange={value => setDisableReplyMetrics(value)} 129 + style={[a.w_full]}> 130 + <Toggle.LabelText style={[a.flex_1]}> 131 + <Trans>Remove reply counts</Trans> 132 + </Toggle.LabelText> 133 + <Toggle.Platform /> 134 + </Toggle.Item> 135 + </SettingsList.Group> 136 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 137 + <SettingsList.ItemIcon icon={VisibilityIcon} /> 138 + <SettingsList.ItemText> 139 + <Trans>Profiles</Trans> 140 + </SettingsList.ItemText> 141 + <Toggle.Item 142 + name="disable_followers_metrics" 143 + label={l`Remove followers counts`} 144 + value={disableFollowersMetrics} 145 + onChange={value => setDisableFollowersMetrics(value)} 146 + style={[a.w_full]}> 147 + <Toggle.LabelText style={[a.flex_1]}> 148 + <Trans>Remove followers counts</Trans> 149 + </Toggle.LabelText> 150 + <Toggle.Platform /> 151 + </Toggle.Item> 152 + <Toggle.Item 153 + name="disable_following_metrics" 154 + label={l`Remove following counts`} 155 + value={disableFollowingMetrics} 156 + onChange={value => setDisableFollowingMetrics(value)} 157 + style={[a.w_full]}> 158 + <Toggle.LabelText style={[a.flex_1]}> 159 + <Trans>Remove following counts</Trans> 160 + </Toggle.LabelText> 161 + <Toggle.Platform /> 162 + </Toggle.Item> 163 + <Toggle.Item 164 + name="disable_followed_by_metrics" 165 + label={l`Remove "followed by" counts`} 166 + value={disableFollowedByMetrics} 167 + onChange={value => setDisableFollowedByMetrics(value)} 168 + style={[a.w_full]}> 169 + <Toggle.LabelText style={[a.flex_1]}> 170 + <Trans>Remove "followed by" avatars</Trans> 171 + </Toggle.LabelText> 172 + <Toggle.Platform /> 173 + </Toggle.Item> 174 + <Toggle.Item 175 + name="show_follows_you_badge" 176 + label={l`Show "Follows you" badge`} 177 + value={showFollowsYouBadge} 178 + onChange={value => setShowFollowsYouBadge(value)} 179 + style={[a.w_full]}> 180 + <Toggle.LabelText style={[a.flex_1]}> 181 + <Trans>Show "Follows you" badge</Trans> 182 + </Toggle.LabelText> 183 + <Toggle.Platform /> 184 + </Toggle.Item> 185 + <Toggle.Item 186 + name="disable_posts_metrics" 187 + label={l`Remove post counts`} 188 + value={disablePostsMetrics} 189 + onChange={value => setDisablePostsMetrics(value)} 190 + style={[a.w_full]}> 191 + <Toggle.LabelText style={[a.flex_1]}> 192 + <Trans>Remove post counts</Trans> 193 + </Toggle.LabelText> 194 + <Toggle.Platform /> 195 + </Toggle.Item> 196 + </SettingsList.Group> 197 + </RunesScreenLayout> 198 + ) 199 + }
+702
src/screens/Settings/RunesSettings/InfrastructureSettings.tsx
··· 1 + import {useState} from 'react' 2 + import {View} from 'react-native' 3 + import {isDid} from '@atproto/api' 4 + import {Trans, useLingui} from '@lingui/react/macro' 5 + 6 + import {APPVIEW_DID_PROXY} from '#/lib/constants' 7 + import {usePalette} from '#/lib/hooks/usePalette' 8 + import * as persisted from '#/state/persisted' 9 + import { 10 + useConstellationInstance, 11 + useSetConstellationInstance, 12 + } from '#/state/preferences/constellation-instance' 13 + import { 14 + useCustomAppViewDid, 15 + useSetCustomAppViewDid, 16 + } from '#/state/preferences/custom-appview-did' 17 + import { 18 + useImageCdnHost, 19 + useSetImageCdnHost, 20 + } from '#/state/preferences/image-cdn-host' 21 + import { 22 + useNoAppLabelers, 23 + useSetNoAppLabelers, 24 + } from '#/state/preferences/no-app-labelers' 25 + import { 26 + usePlcDirectory, 27 + useSetPlcDirectory, 28 + } from '#/state/preferences/plc-directory' 29 + import { 30 + useLibreTranslateInstance, 31 + useSetLibreTranslateInstance, 32 + useSetTranslationServicePreference, 33 + useTranslationServicePreference, 34 + } from '#/state/preferences/translation-service-preference' 35 + import {findService, useDidDocument} from '#/state/queries/resolve-identity' 36 + import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 37 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 38 + import {atoms as a} from '#/alf' 39 + import {Admonition} from '#/components/Admonition' 40 + import {Button, ButtonText} from '#/components/Button' 41 + import * as Dialog from '#/components/Dialog' 42 + import * as Toggle from '#/components/forms/Toggle' 43 + import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 44 + import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' 45 + import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 46 + import {InlineLinkText} from '#/components/Link' 47 + import {Text} from '#/components/Typography' 48 + import {IS_WEB} from '#/env' 49 + import {RunesScreenLayout} from './components/RunesScreenLayout' 50 + 51 + export function RunesInfrastructureSettingsScreen() { 52 + const {t: l} = useLingui() 53 + 54 + const translationServicePreference = useTranslationServicePreference() 55 + const setTranslationServicePreference = useSetTranslationServicePreference() 56 + const setLibreTranslateInstanceControl = Dialog.useDialogControl() 57 + 58 + const imageCdnHost = useImageCdnHost() 59 + const setImageCdnHostControl = Dialog.useDialogControl() 60 + 61 + const plcDirectory = usePlcDirectory() 62 + const setPlcDirectoryControl = Dialog.useDialogControl() 63 + 64 + const constellationInstance = useConstellationInstance() 65 + const setConstellationInstanceControl = Dialog.useDialogControl() 66 + 67 + const [customAppViewDid] = useCustomAppViewDid() 68 + const setCustomAppViewDidControl = Dialog.useDialogControl() 69 + 70 + const noAppLabelers = useNoAppLabelers() 71 + const setNoAppLabelers = useSetNoAppLabelers() 72 + 73 + return ( 74 + <RunesScreenLayout titleText={l`Infrastructure`}> 75 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 76 + <SettingsList.ItemIcon icon={EarthIcon} /> 77 + <SettingsList.ItemText> 78 + <Trans>Post Translation Provider</Trans> 79 + </SettingsList.ItemText> 80 + <Toggle.Item 81 + name="service_google" 82 + label={l`Use Google Translate`} 83 + value={translationServicePreference === 'google'} 84 + onChange={() => setTranslationServicePreference('google')} 85 + style={[a.w_full]}> 86 + <Toggle.LabelText style={[a.flex_1]}> 87 + <Trans>Use Google Translate</Trans> 88 + </Toggle.LabelText> 89 + <Toggle.Radio /> 90 + </Toggle.Item> 91 + <Toggle.Item 92 + name="service_kagi" 93 + label={l`Use Kagi Translate`} 94 + value={translationServicePreference === 'kagi'} 95 + onChange={() => setTranslationServicePreference('kagi')} 96 + style={[a.w_full]}> 97 + <Toggle.LabelText style={[a.flex_1]}> 98 + <Trans>Use Kagi Translate</Trans> 99 + </Toggle.LabelText> 100 + <Toggle.Radio /> 101 + </Toggle.Item> 102 + <Toggle.Item 103 + name="service_papago" 104 + label={l`Use Naver Papago`} 105 + value={translationServicePreference === 'papago'} 106 + onChange={() => setTranslationServicePreference('papago')} 107 + style={[a.w_full]}> 108 + <Toggle.LabelText style={[a.flex_1]}> 109 + <Trans>Use Naver Papago</Trans> 110 + </Toggle.LabelText> 111 + <Toggle.Radio /> 112 + </Toggle.Item> 113 + <Toggle.Item 114 + name="service_libreTranslate" 115 + label={l`Use LibreTranslate`} 116 + value={translationServicePreference === 'libreTranslate'} 117 + onChange={() => setTranslationServicePreference('libreTranslate')} 118 + style={[a.w_full]}> 119 + <Toggle.LabelText style={[a.flex_1]}> 120 + <Trans>Use LibreTranslate</Trans> 121 + </Toggle.LabelText> 122 + <Toggle.Radio /> 123 + </Toggle.Item> 124 + </SettingsList.Group> 125 + 126 + {translationServicePreference === 'libreTranslate' && ( 127 + <SettingsList.Item> 128 + <SettingsList.ItemIcon icon={EarthIcon} /> 129 + <SettingsList.ItemText> 130 + <Trans>{`LibreTranslate Instance`}</Trans> 131 + </SettingsList.ItemText> 132 + <SettingsList.BadgeButton 133 + label={l`Change`} 134 + onPress={() => setLibreTranslateInstanceControl.open()} 135 + /> 136 + </SettingsList.Item> 137 + )} 138 + 139 + <SettingsList.Divider /> 140 + 141 + <SettingsList.Item> 142 + <SettingsList.ItemIcon icon={EarthIcon} /> 143 + <SettingsList.ItemText> 144 + <Trans>{`Image CDN`}</Trans> 145 + </SettingsList.ItemText> 146 + <SettingsList.BadgeButton 147 + label={l`Change`} 148 + onPress={() => setImageCdnHostControl.open()} 149 + /> 150 + </SettingsList.Item> 151 + <SettingsList.Item> 152 + <Admonition type="info" style={[a.flex_1]}> 153 + <Trans> 154 + Override the CDN host for all images. Current:&nbsp; 155 + <InlineLinkText to={imageCdnHost} label={imageCdnHost}> 156 + {imageCdnHost} 157 + </InlineLinkText> 158 + </Trans> 159 + </Admonition> 160 + </SettingsList.Item> 161 + 162 + <SettingsList.Item> 163 + <SettingsList.ItemIcon icon={EarthIcon} /> 164 + <SettingsList.ItemText> 165 + <Trans>{`PLC Directory`}</Trans> 166 + </SettingsList.ItemText> 167 + <SettingsList.BadgeButton 168 + label={l`Change`} 169 + onPress={() => setPlcDirectoryControl.open()} 170 + /> 171 + </SettingsList.Item> 172 + <SettingsList.Item> 173 + <Admonition type="info" style={[a.flex_1]}> 174 + <Trans> 175 + Override the PLC directory used to resolve DIDs. Current:&nbsp; 176 + <InlineLinkText to={plcDirectory} label={plcDirectory}> 177 + {plcDirectory} 178 + </InlineLinkText> 179 + </Trans> 180 + </Admonition> 181 + </SettingsList.Item> 182 + 183 + <SettingsList.Item> 184 + <SettingsList.ItemIcon icon={StarIcon} /> 185 + <SettingsList.ItemText> 186 + <Trans>{`Constellation Instance`}</Trans> 187 + </SettingsList.ItemText> 188 + <SettingsList.BadgeButton 189 + label={l`Change`} 190 + onPress={() => setConstellationInstanceControl.open()} 191 + /> 192 + </SettingsList.Item> 193 + <SettingsList.Item> 194 + <Admonition type="info" style={[a.flex_1]}> 195 + <Trans> 196 + Constellation is used to supplement AppView responses for custom 197 + verifications and nuclear block bypass, via backlinks. Current 198 + instance:&nbsp; 199 + <InlineLinkText 200 + to={constellationInstance} 201 + label={constellationInstance}> 202 + {constellationInstance} 203 + </InlineLinkText> 204 + </Trans> 205 + </Admonition> 206 + </SettingsList.Item> 207 + 208 + <SettingsList.Divider /> 209 + 210 + <SettingsList.Item> 211 + <SettingsList.ItemIcon icon={StarIcon} /> 212 + <SettingsList.ItemText> 213 + <Trans>{`Custom AppView DID`}</Trans> 214 + </SettingsList.ItemText> 215 + <SettingsList.BadgeButton 216 + label={customAppViewDid ? l`Change` : l`Set`} 217 + onPress={() => setCustomAppViewDidControl.open()} 218 + /> 219 + </SettingsList.Item> 220 + 221 + <SettingsList.Divider /> 222 + 223 + <Toggle.Item 224 + name="no_app_labelers" 225 + label={l`Do not declare any app labelers`} 226 + value={noAppLabelers} 227 + onChange={value => setNoAppLabelers(value)}> 228 + <SettingsList.Item> 229 + <SettingsList.ItemIcon icon={RaisingHandIcon} /> 230 + <SettingsList.ItemText> 231 + <Trans>Do not declare any default app labelers</Trans> 232 + </SettingsList.ItemText> 233 + <Toggle.Platform /> 234 + </SettingsList.Item> 235 + </Toggle.Item> 236 + 237 + <SettingsList.Item> 238 + <Admonition type="warning" style={[a.flex_1]}> 239 + <Trans>Restart the app after changing this setting.</Trans> 240 + </Admonition> 241 + </SettingsList.Item> 242 + <SettingsList.Item> 243 + <Admonition type="tip" style={[a.flex_1]}> 244 + <Trans> 245 + Some App Views will default to using an app labeler if you have no 246 + labelers, so consider subscribing to at least one labeler if you 247 + have issues. 248 + </Trans> 249 + </Admonition> 250 + </SettingsList.Item> 251 + <SettingsList.Item> 252 + <Admonition type="info" style={[a.flex_1]}> 253 + <Trans> 254 + App labelers are mandatory top-level labelers that can perform 255 + "takedowns". This setting does not influence geolocation-based 256 + labelers. 257 + </Trans> 258 + </Admonition> 259 + </SettingsList.Item> 260 + 261 + <ConstellationInstanceDialog control={setConstellationInstanceControl} /> 262 + <CustomAppViewDidDialog control={setCustomAppViewDidControl} /> 263 + <LibreTranslateInstanceDialog 264 + control={setLibreTranslateInstanceControl} 265 + /> 266 + <ImageCdnHostDialog control={setImageCdnHostControl} /> 267 + <PlcDirectoryDialog control={setPlcDirectoryControl} /> 268 + </RunesScreenLayout> 269 + ) 270 + } 271 + 272 + function ConstellationInstanceDialog({ 273 + control, 274 + }: { 275 + control: Dialog.DialogControlProps 276 + }) { 277 + const pal = usePalette('default') 278 + const {t: l} = useLingui() 279 + 280 + const constellationInstance = useConstellationInstance() 281 + const [url, setUrl] = useState(constellationInstance ?? '') 282 + const setConstellationInstance = useSetConstellationInstance() 283 + 284 + const submit = () => { 285 + setConstellationInstance(url) 286 + control.close() 287 + } 288 + 289 + return ( 290 + <Dialog.Outer 291 + control={control} 292 + nativeOptions={{preventExpansion: true}} 293 + onClose={() => setUrl(constellationInstance ?? '')}> 294 + <Dialog.Handle /> 295 + <Dialog.ScrollableInner label={l`Constellations instance URL`}> 296 + <View style={[a.gap_sm, a.pb_lg]}> 297 + <Text style={[a.text_2xl, a.font_bold]}> 298 + <Trans>Constellations instance URL</Trans> 299 + </Text> 300 + </View> 301 + 302 + <View style={a.gap_lg}> 303 + <Dialog.Input 304 + label="Text input field" 305 + autoFocus 306 + style={[styles.textInput, pal.border, pal.text]} 307 + onChangeText={setUrl} 308 + placeholder={persisted.defaults.constellationInstance} 309 + placeholderTextColor={pal.colors.textLight} 310 + onSubmitEditing={submit} 311 + accessibilityHint={l`Input the url of the constellations instance to use`} 312 + defaultValue={constellationInstance} 313 + /> 314 + 315 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 316 + <Button 317 + label={l`Save`} 318 + size="large" 319 + onPress={() => void submit()} 320 + variant="solid" 321 + color="primary" 322 + disabled={!isValidHostnameUrl(url)}> 323 + <ButtonText> 324 + <Trans>Save</Trans> 325 + </ButtonText> 326 + </Button> 327 + </View> 328 + </View> 329 + 330 + <Dialog.Close /> 331 + </Dialog.ScrollableInner> 332 + </Dialog.Outer> 333 + ) 334 + } 335 + 336 + function CustomAppViewDidDialog({ 337 + control, 338 + }: { 339 + control: Dialog.DialogControlProps 340 + }) { 341 + const pal = usePalette('default') 342 + const {t: l} = useLingui() 343 + 344 + const [customAppViewDid] = useCustomAppViewDid() 345 + const [did, setDid] = useState(customAppViewDid ?? '') 346 + const setCustomAppViewDid = useSetCustomAppViewDid() 347 + 348 + const doc = useDidDocument({did}) 349 + const bskyAppViewService = 350 + doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') 351 + 352 + const submit = () => { 353 + if (did.length === 0) { 354 + control.close(() => { 355 + setCustomAppViewDid(undefined) 356 + }) 357 + return 358 + } 359 + if (!bskyAppViewService?.serviceEndpoint) return 360 + control.close(() => { 361 + setCustomAppViewDid(did) 362 + }) 363 + } 364 + 365 + return ( 366 + <Dialog.Outer 367 + control={control} 368 + nativeOptions={{preventExpansion: true}} 369 + onClose={() => setDid(customAppViewDid ?? '')}> 370 + <Dialog.Handle /> 371 + <Dialog.ScrollableInner label={l`Custom AppView Proxy DID`}> 372 + <View style={[a.gap_sm, a.pb_lg]}> 373 + <Text style={[a.text_2xl, a.font_bold]}> 374 + <Trans>Custom AppView Proxy DID</Trans> 375 + </Text> 376 + </View> 377 + 378 + <View style={a.gap_lg}> 379 + <Dialog.Input 380 + label="Text input field" 381 + autoFocus 382 + style={[styles.textInput, pal.border, pal.text]} 383 + onChangeText={setDid} 384 + placeholder={ 385 + APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) || 386 + 'did:web:api.bsky.app' 387 + } 388 + placeholderTextColor={pal.colors.textLight} 389 + onSubmitEditing={submit} 390 + accessibilityHint={l`Input the DID of the AppView to proxy requests through`} 391 + isInvalid={ 392 + !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading 393 + } 394 + defaultValue={customAppViewDid ?? ''} 395 + /> 396 + 397 + {did && !isDid(did) && ( 398 + <View> 399 + <ErrorMessage message={l`must enter a DID`} /> 400 + </View> 401 + )} 402 + 403 + {did && (did.includes('#') || did.includes('?')) && ( 404 + <View> 405 + <ErrorMessage message={l`don't include the service id`} /> 406 + </View> 407 + )} 408 + 409 + {doc.isError && ( 410 + <View> 411 + <ErrorMessage 412 + message={doc.error.message || l`document resolution failure`} 413 + /> 414 + </View> 415 + )} 416 + 417 + {doc.data && 418 + !bskyAppViewService && 419 + (doc.data as {message?: string}).message && ( 420 + <View> 421 + <ErrorMessage 422 + message={(doc.data as {message: string}).message} 423 + /> 424 + </View> 425 + )} 426 + 427 + {doc.data && !bskyAppViewService && ( 428 + <View> 429 + <ErrorMessage 430 + message={l`document doesn't contain #bsky_appview service`} 431 + /> 432 + </View> 433 + )} 434 + 435 + {bskyAppViewService && ( 436 + <Text style={[a.text_sm, a.leading_snug]}> 437 + {JSON.stringify(bskyAppViewService, null, 2)} 438 + </Text> 439 + )} 440 + 441 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 442 + <Button 443 + label={l`Save`} 444 + size="large" 445 + onPress={() => void submit()} 446 + variant="solid" 447 + color={did.length > 0 ? 'primary' : 'secondary'} 448 + disabled={ 449 + did.length !== 0 && !bskyAppViewService?.serviceEndpoint 450 + }> 451 + <ButtonText> 452 + {did.length > 0 ? <Trans>Save</Trans> : <Trans>Reset</Trans>} 453 + </ButtonText> 454 + </Button> 455 + </View> 456 + </View> 457 + 458 + <Dialog.Close /> 459 + </Dialog.ScrollableInner> 460 + </Dialog.Outer> 461 + ) 462 + } 463 + 464 + function LibreTranslateInstanceDialog({ 465 + control, 466 + }: { 467 + control: Dialog.DialogControlProps 468 + }) { 469 + const pal = usePalette('default') 470 + const {t: l} = useLingui() 471 + 472 + const libreTranslateInstance = useLibreTranslateInstance() 473 + const [url, setUrl] = useState(libreTranslateInstance ?? '') 474 + const setLibreTranslateInstance = useSetLibreTranslateInstance() 475 + 476 + return ( 477 + <Dialog.Outer 478 + control={control} 479 + nativeOptions={{preventExpansion: true}} 480 + onClose={() => setUrl(libreTranslateInstance ?? '')}> 481 + <Dialog.Handle /> 482 + <Dialog.ScrollableInner label={l`LibreTranslate instance URL`}> 483 + <View style={[a.gap_sm, a.pb_lg]}> 484 + <Text style={[a.text_2xl, a.font_bold]}> 485 + <Trans>LibreTranslate instance URL</Trans> 486 + </Text> 487 + </View> 488 + 489 + <View style={a.gap_lg}> 490 + <Dialog.Input 491 + label="Text input field" 492 + autoFocus 493 + style={[styles.textInput, pal.border, pal.text]} 494 + onChangeText={setUrl} 495 + placeholder={persisted.defaults.libreTranslateInstance} 496 + placeholderTextColor={pal.colors.textLight} 497 + onSubmitEditing={() => { 498 + setLibreTranslateInstance(url) 499 + control.close() 500 + }} 501 + accessibilityHint={l`Input the url of the LibreTranslate instance to use`} 502 + defaultValue={libreTranslateInstance} 503 + /> 504 + 505 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 506 + <Button 507 + label={l`Save`} 508 + size="large" 509 + onPress={() => { 510 + setLibreTranslateInstance(url) 511 + control.close() 512 + }} 513 + variant="solid" 514 + color="primary" 515 + disabled={!isValidHostnameUrl(url)}> 516 + <ButtonText> 517 + <Trans>Save</Trans> 518 + </ButtonText> 519 + </Button> 520 + </View> 521 + </View> 522 + 523 + <Dialog.Close /> 524 + </Dialog.ScrollableInner> 525 + </Dialog.Outer> 526 + ) 527 + } 528 + 529 + function ImageCdnHostDialog({control}: {control: Dialog.DialogControlProps}) { 530 + const pal = usePalette('default') 531 + const {t: l} = useLingui() 532 + 533 + const imageCdnHost = useImageCdnHost() 534 + const [url, setUrl] = useState(imageCdnHost ?? '') 535 + const setImageCdnHost = useSetImageCdnHost() 536 + const isReset = url.trim().length === 0 537 + 538 + const submit = () => { 539 + const trimmedUrl = url.trim() 540 + if (!trimmedUrl) { 541 + control.close(() => { 542 + setImageCdnHost(undefined) 543 + }) 544 + return 545 + } 546 + 547 + control.close(() => { 548 + try { 549 + setImageCdnHost(new URL(trimmedUrl).origin) 550 + } catch { 551 + setImageCdnHost(trimmedUrl) 552 + } 553 + }) 554 + } 555 + 556 + return ( 557 + <Dialog.Outer 558 + control={control} 559 + nativeOptions={{preventExpansion: true}} 560 + onClose={() => setUrl(imageCdnHost ?? '')}> 561 + <Dialog.Handle /> 562 + <Dialog.ScrollableInner label={l`Image CDN URL`}> 563 + <View style={[a.gap_sm, a.pb_lg]}> 564 + <Text style={[a.text_2xl, a.font_bold]}> 565 + <Trans>Image CDN URL</Trans> 566 + </Text> 567 + </View> 568 + 569 + <View style={a.gap_lg}> 570 + <Dialog.Input 571 + label="Text input field" 572 + autoFocus 573 + style={[styles.textInput, pal.border, pal.text]} 574 + onChangeText={setUrl} 575 + placeholder={persisted.defaults.imageCdnHost} 576 + placeholderTextColor={pal.colors.textLight} 577 + onSubmitEditing={submit} 578 + accessibilityHint={l`Input the URL of the image CDN to use`} 579 + defaultValue={imageCdnHost} 580 + /> 581 + 582 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 583 + <Button 584 + label={isReset ? l`Reset` : l`Save`} 585 + size="large" 586 + onPress={() => void submit()} 587 + variant="solid" 588 + color={isReset ? 'secondary' : 'primary'} 589 + disabled={!isReset && !isValidHostnameUrl(url)}> 590 + <ButtonText> 591 + {isReset ? <Trans>Reset</Trans> : <Trans>Save</Trans>} 592 + </ButtonText> 593 + </Button> 594 + </View> 595 + </View> 596 + 597 + <Dialog.Close /> 598 + </Dialog.ScrollableInner> 599 + </Dialog.Outer> 600 + ) 601 + } 602 + 603 + function PlcDirectoryDialog({control}: {control: Dialog.DialogControlProps}) { 604 + const pal = usePalette('default') 605 + const {t: l} = useLingui() 606 + 607 + const plcDirectory = usePlcDirectory() 608 + const [url, setUrl] = useState(plcDirectory ?? '') 609 + const setPlcDirectory = useSetPlcDirectory() 610 + const isReset = url.trim().length === 0 611 + 612 + const submit = () => { 613 + const trimmedUrl = url.trim() 614 + if (!trimmedUrl) { 615 + control.close(() => { 616 + setPlcDirectory(undefined) 617 + }) 618 + return 619 + } 620 + 621 + control.close(() => { 622 + try { 623 + setPlcDirectory(new URL(trimmedUrl).origin) 624 + } catch { 625 + setPlcDirectory(trimmedUrl) 626 + } 627 + }) 628 + } 629 + 630 + return ( 631 + <Dialog.Outer 632 + control={control} 633 + nativeOptions={{preventExpansion: true}} 634 + onClose={() => setUrl(plcDirectory ?? '')}> 635 + <Dialog.Handle /> 636 + <Dialog.ScrollableInner label={l`PLC Directory URL`}> 637 + <View style={[a.gap_sm, a.pb_lg]}> 638 + <Text style={[a.text_2xl, a.font_bold]}> 639 + <Trans>PLC Directory URL</Trans> 640 + </Text> 641 + </View> 642 + 643 + <View style={a.gap_lg}> 644 + <Dialog.Input 645 + label="Text input field" 646 + autoFocus 647 + style={[styles.textInput, pal.border, pal.text]} 648 + onChangeText={setUrl} 649 + placeholder={persisted.defaults.plcDirectory} 650 + placeholderTextColor={pal.colors.textLight} 651 + onSubmitEditing={submit} 652 + accessibilityHint={l`Input the URL of the PLC directory to use`} 653 + defaultValue={plcDirectory} 654 + /> 655 + 656 + <View style={IS_WEB && [a.flex_row, a.justify_end]}> 657 + <Button 658 + label={isReset ? l`Reset` : l`Save`} 659 + size="large" 660 + onPress={() => void submit()} 661 + variant="solid" 662 + color={isReset ? 'secondary' : 'primary'} 663 + disabled={!isReset && !isValidPlcDirectoryUrl(url)}> 664 + <ButtonText> 665 + {isReset ? <Trans>Reset</Trans> : <Trans>Save</Trans>} 666 + </ButtonText> 667 + </Button> 668 + </View> 669 + </View> 670 + 671 + <Dialog.Close /> 672 + </Dialog.ScrollableInner> 673 + </Dialog.Outer> 674 + ) 675 + } 676 + 677 + function isValidHostnameUrl(url: string) { 678 + try { 679 + return new URL(url).hostname.includes('.') 680 + } catch { 681 + return false 682 + } 683 + } 684 + 685 + function isValidPlcDirectoryUrl(url: string) { 686 + try { 687 + const nextUrl = new URL(url) 688 + return nextUrl.protocol === 'https:' || nextUrl.protocol === 'http:' 689 + } catch { 690 + return false 691 + } 692 + } 693 + 694 + const styles = { 695 + textInput: { 696 + borderWidth: 1, 697 + borderRadius: 6, 698 + paddingHorizontal: 14, 699 + paddingVertical: 10, 700 + fontSize: 16, 701 + }, 702 + }
+102
src/screens/Settings/RunesSettings/MenusSettings.tsx
··· 1 + import {Trans, useLingui} from '@lingui/react/macro' 2 + 3 + import { 4 + useSetShowExternalShareButtons, 5 + useShowExternalShareButtons, 6 + } from '#/state/preferences/external-share-buttons' 7 + import { 8 + useSetShowLinkInHandle, 9 + useShowLinkInHandle, 10 + } from '#/state/preferences/show-link-in-handle.tsx' 11 + import { 12 + useSetShowLinkInHandleOnlyOnWorkingLinks, 13 + useShowLinkInHandleOnlyOnWorkingLinks, 14 + } from '#/state/preferences/show-link-in-handle-only-on-working-links' 15 + import { 16 + useHandleInLinks, 17 + useSetHandleInLinks, 18 + } from '#/state/preferences/use-handle-in-links' 19 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 20 + import * as Toggle from '#/components/forms/Toggle' 21 + import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight' 22 + import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' 23 + import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' 24 + import {RunesScreenLayout} from './components/RunesScreenLayout' 25 + 26 + export function RunesMenusSettingsScreen() { 27 + const {t: l} = useLingui() 28 + 29 + const handleInLinks = useHandleInLinks() 30 + const setHandleInLinks = useSetHandleInLinks() 31 + 32 + const showExternalShareButtons = useShowExternalShareButtons() 33 + const setShowExternalShareButtons = useSetShowExternalShareButtons() 34 + 35 + const showLinkInHandle = useShowLinkInHandle() 36 + const setShowLinkInHandle = useSetShowLinkInHandle() 37 + const showLinkInHandleOnlyOnWorkingLinks = 38 + useShowLinkInHandleOnlyOnWorkingLinks() 39 + const setShowLinkInHandleOnlyOnWorkingLinks = 40 + useSetShowLinkInHandleOnlyOnWorkingLinks() 41 + 42 + return ( 43 + <RunesScreenLayout titleText={l`Menus`}> 44 + <Toggle.Item 45 + name="use_handle_in_links" 46 + label={l`Use handles in profile links instead of DIDs (requires restart)`} 47 + value={handleInLinks ?? false} 48 + onChange={value => setHandleInLinks(value)}> 49 + <SettingsList.Item> 50 + <SettingsList.ItemIcon icon={AtIcon} /> 51 + <SettingsList.ItemText> 52 + <Trans>Use handles in profile links instead of DIDs</Trans> 53 + </SettingsList.ItemText> 54 + <Toggle.Platform /> 55 + </SettingsList.Item> 56 + </Toggle.Item> 57 + <Toggle.Item 58 + name="external_share_buttons" 59 + label={l`Show "Open original post" and "Open post in PDSls" buttons`} 60 + value={showExternalShareButtons} 61 + onChange={value => setShowExternalShareButtons(value)}> 62 + <SettingsList.Item> 63 + <SettingsList.ItemIcon icon={ArrowShareRightIcon} /> 64 + <SettingsList.ItemText> 65 + <Trans> 66 + Show "Open original post" and "Open post in PDSls" buttons 67 + </Trans> 68 + </SettingsList.ItemText> 69 + <Toggle.Platform /> 70 + </SettingsList.Item> 71 + </Toggle.Item> 72 + <Toggle.Item 73 + name="show_link_in_handle" 74 + label={l`On non-bsky.social handles, show a link to that URL`} 75 + value={showLinkInHandle} 76 + onChange={value => setShowLinkInHandle(value)}> 77 + <SettingsList.Item> 78 + <SettingsList.ItemIcon icon={ChainLinkIcon} /> 79 + <SettingsList.ItemText> 80 + <Trans>On non-bsky.social handles, show a link to that URL</Trans> 81 + </SettingsList.ItemText> 82 + <Toggle.Platform /> 83 + </SettingsList.Item> 84 + </Toggle.Item> 85 + {showLinkInHandle && ( 86 + <Toggle.Item 87 + name="show_link_in_handle_only_on_working_links" 88 + label={l`Only show URL on handles with working links`} 89 + value={showLinkInHandleOnlyOnWorkingLinks} 90 + onChange={value => setShowLinkInHandleOnlyOnWorkingLinks(value)}> 91 + <SettingsList.Item> 92 + <SettingsList.ItemIcon icon={ChainLinkIcon} /> 93 + <SettingsList.ItemText> 94 + <Trans>Only show URL on handles with working links</Trans> 95 + </SettingsList.ItemText> 96 + <Toggle.Platform /> 97 + </SettingsList.Item> 98 + </Toggle.Item> 99 + )} 100 + </RunesScreenLayout> 101 + ) 102 + }
+104
src/screens/Settings/RunesSettings/OtherAdditionsSettings.tsx
··· 1 + import {Trans, useLingui} from '@lingui/react/macro' 2 + 3 + import { 4 + useAutoLikeOnRepost, 5 + useSetAutoLikeOnRepost, 6 + } from '#/state/preferences/auto-like-on-repost.tsx' 7 + import { 8 + useDirectFetchRecords, 9 + useSetDirectFetchRecords, 10 + } from '#/state/preferences/direct-fetch-records' 11 + import { 12 + useDisableViaRepostNotification, 13 + useSetDisableViaRepostNotification, 14 + } from '#/state/preferences/disable-via-repost-notification' 15 + import { 16 + useDiscoverContextEnabled, 17 + useSetDiscoverContextEnabled, 18 + } from '#/state/preferences/discover-context-enabled' 19 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 20 + import {atoms as a} from '#/alf' 21 + import {Admonition} from '#/components/Admonition' 22 + import * as Toggle from '#/components/forms/Toggle' 23 + import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 24 + import {RunesScreenLayout} from './components/RunesScreenLayout' 25 + 26 + export function RunesOtherAdditionsSettingsScreen() { 27 + const {t: l} = useLingui() 28 + 29 + const directFetchRecords = useDirectFetchRecords() 30 + const setDirectFetchRecords = useSetDirectFetchRecords() 31 + 32 + const autoLikeOnRepost = useAutoLikeOnRepost() 33 + const setAutoLikeOnRepost = useSetAutoLikeOnRepost() 34 + 35 + const disableViaRepostNotification = useDisableViaRepostNotification() 36 + const setDisableViaRepostNotification = useSetDisableViaRepostNotification() 37 + 38 + const discoverContextEnabled = useDiscoverContextEnabled() 39 + const setDiscoverContextEnabled = useSetDiscoverContextEnabled() 40 + 41 + return ( 42 + <RunesScreenLayout titleText={l`Other additions`}> 43 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 44 + <SettingsList.ItemIcon icon={VisibilityIcon} /> 45 + <SettingsList.ItemText> 46 + <Trans>Extras</Trans> 47 + </SettingsList.ItemText> 48 + <Toggle.Item 49 + name="direct_fetch_records" 50 + label={l`Fetch records directly from PDS to see through quote blocks`} 51 + value={directFetchRecords} 52 + onChange={value => setDirectFetchRecords(value)} 53 + style={[a.w_full]}> 54 + <Toggle.LabelText style={[a.flex_1]}> 55 + <Trans> 56 + Fetch records directly from PDS to see contents of blocked and 57 + detached quotes 58 + </Trans> 59 + </Toggle.LabelText> 60 + <Toggle.Platform /> 61 + </Toggle.Item> 62 + <Toggle.Item 63 + name="auto_like_on_repost" 64 + label={l`Auto-like what you repost`} 65 + value={autoLikeOnRepost} 66 + onChange={value => setAutoLikeOnRepost(value)} 67 + style={[a.w_full]}> 68 + <Toggle.LabelText style={[a.flex_1]}> 69 + <Trans>Auto-like what you repost</Trans> 70 + </Toggle.LabelText> 71 + <Toggle.Platform /> 72 + </Toggle.Item> 73 + <Toggle.Item 74 + name="disable_via_repost_notification" 75 + label={l`Disable via repost notifications`} 76 + value={disableViaRepostNotification} 77 + onChange={value => setDisableViaRepostNotification(value)} 78 + style={[a.w_full]}> 79 + <Toggle.LabelText style={[a.flex_1]}> 80 + <Trans>Disable via repost notifications</Trans> 81 + </Toggle.LabelText> 82 + <Toggle.Platform /> 83 + </Toggle.Item> 84 + <Admonition type="info" style={[a.flex_1]}> 85 + <Trans> 86 + Forcefully disables the notifications other people receive when you 87 + like/repost a post someone else has reposted for privacy. 88 + </Trans> 89 + </Admonition> 90 + <Toggle.Item 91 + name="discover_context" 92 + label={l`Show debug context for posts in Discover feed`} 93 + value={discoverContextEnabled} 94 + onChange={value => setDiscoverContextEnabled(value)} 95 + style={[a.w_full]}> 96 + <Toggle.LabelText style={[a.flex_1]}> 97 + <Trans>Show debug context for posts in Discover feed</Trans> 98 + </Toggle.LabelText> 99 + <Toggle.Platform /> 100 + </Toggle.Item> 101 + </SettingsList.Group> 102 + </RunesScreenLayout> 103 + ) 104 + }
+159
src/screens/Settings/RunesSettings/UsabilitySettings.tsx
··· 1 + import {Trans, useLingui} from '@lingui/react/macro' 2 + 3 + import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' 4 + import { 5 + useDisableComposerPrompt, 6 + useSetDisableComposerPrompt, 7 + } from '#/state/preferences/disable-composer-prompt' 8 + import { 9 + useDisableVerifyEmailReminder, 10 + useSetDisableVerifyEmailReminder, 11 + } from '#/state/preferences/disable-verify-email-reminder' 12 + import { 13 + useHideFeedsPromoTab, 14 + useSetHideFeedsPromoTab, 15 + } from '#/state/preferences/hide-feeds-promo-tab' 16 + import { 17 + useHideSimilarAccountsRecomm, 18 + useSetHideSimilarAccountsRecomm, 19 + } from '#/state/preferences/hide-similar-accounts-recommendations' 20 + import { 21 + useHideUnreplyablePosts, 22 + useSetHideUnreplyablePosts, 23 + } from '#/state/preferences/hide-unreplyable-posts' 24 + import { 25 + useNoDiscoverFallback, 26 + useSetNoDiscoverFallback, 27 + } from '#/state/preferences/no-discover-fallback' 28 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 29 + import {atoms as a} from '#/alf' 30 + import {Admonition} from '#/components/Admonition' 31 + import * as Toggle from '#/components/forms/Toggle' 32 + import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 33 + import {RunesScreenLayout} from './components/RunesScreenLayout' 34 + 35 + export function RunesUsabilitySettingsScreen() { 36 + const {t: l} = useLingui() 37 + 38 + const goLinksEnabled = useGoLinksEnabled() 39 + const setGoLinksEnabled = useSetGoLinksEnabled() 40 + 41 + const noDiscoverFallback = useNoDiscoverFallback() 42 + const setNoDiscoverFallback = useSetNoDiscoverFallback() 43 + 44 + const hideFeedsPromoTab = useHideFeedsPromoTab() 45 + const setHideFeedsPromoTab = useSetHideFeedsPromoTab() 46 + 47 + const hideSimilarAccountsRecomm = useHideSimilarAccountsRecomm() 48 + const setHideSimilarAccountsRecomm = useSetHideSimilarAccountsRecomm() 49 + 50 + const hideUnreplyablePosts = useHideUnreplyablePosts() 51 + const setHideUnreplyablePosts = useSetHideUnreplyablePosts() 52 + 53 + const disableComposerPrompt = useDisableComposerPrompt() 54 + const setDisableComposerPrompt = useSetDisableComposerPrompt() 55 + 56 + const disableVerifyEmailReminder = useDisableVerifyEmailReminder() 57 + const setDisableVerifyEmailReminder = useSetDisableVerifyEmailReminder() 58 + 59 + return ( 60 + <RunesScreenLayout titleText={l`Usability`}> 61 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 62 + <SettingsList.ItemIcon icon={PaintRollerIcon} /> 63 + <SettingsList.ItemText> 64 + <Trans>Debloating</Trans> 65 + </SettingsList.ItemText> 66 + <Toggle.Item 67 + name="use_go_links" 68 + label={l`Redirect through go.bsky.app`} 69 + value={goLinksEnabled ?? false} 70 + onChange={value => setGoLinksEnabled(value)} 71 + style={[a.w_full]}> 72 + <Toggle.LabelText style={[a.flex_1]}> 73 + <Trans>Redirect through go.bsky.app</Trans> 74 + </Toggle.LabelText> 75 + <Toggle.Platform /> 76 + </Toggle.Item> 77 + <Toggle.Item 78 + name="no_discover_fallback" 79 + label={l`Do not fall back to discover feed`} 80 + value={noDiscoverFallback} 81 + onChange={value => setNoDiscoverFallback(value)} 82 + style={[a.w_full]}> 83 + <Toggle.LabelText style={[a.flex_1]}> 84 + <Trans>Do not fall back to discover feed</Trans> 85 + </Toggle.LabelText> 86 + <Toggle.Platform /> 87 + </Toggle.Item> 88 + <Toggle.Item 89 + name="hide_feeds_promo_tab" 90 + label={l`Hide "Feeds ✨" tab when only one feed is selected`} 91 + value={hideFeedsPromoTab} 92 + onChange={value => setHideFeedsPromoTab(value)} 93 + style={[a.w_full]}> 94 + <Toggle.LabelText style={[a.flex_1]}> 95 + <Trans>Hide "Feeds ✨" tab when only one feed is selected</Trans> 96 + </Toggle.LabelText> 97 + <Toggle.Platform /> 98 + </Toggle.Item> 99 + <Toggle.Item 100 + name="hide_similar_accounts_recommendations" 101 + label={l`Hide similar accounts recommendations`} 102 + value={hideSimilarAccountsRecomm} 103 + onChange={value => setHideSimilarAccountsRecomm(value)} 104 + style={[a.w_full]}> 105 + <Toggle.LabelText style={[a.flex_1]}> 106 + <Trans>Hide similar accounts recommendations</Trans> 107 + </Toggle.LabelText> 108 + <Toggle.Platform /> 109 + </Toggle.Item> 110 + <Toggle.Item 111 + name="hide_unreplyable_posts" 112 + label={l`Hide posts that cannot be replied to from feeds`} 113 + value={hideUnreplyablePosts} 114 + onChange={value => setHideUnreplyablePosts(value)} 115 + style={[a.w_full]}> 116 + <Toggle.LabelText style={[a.flex_1]}> 117 + <Trans>Hide posts that cannot be replied to from feeds</Trans> 118 + </Toggle.LabelText> 119 + <Toggle.Platform /> 120 + </Toggle.Item> 121 + <Admonition type="info" style={[a.flex_1]}> 122 + <Trans> 123 + Hides posts from feeds where replies are disabled (e.g. due to 124 + postgates or other restrictions). Does not affect thread views. 125 + </Trans> 126 + </Admonition> 127 + <Toggle.Item 128 + name="disable_composer_prompt" 129 + label={l`Disable composer prompt`} 130 + value={disableComposerPrompt} 131 + onChange={value => setDisableComposerPrompt(value)} 132 + style={[a.w_full]}> 133 + <Toggle.LabelText style={[a.flex_1]}> 134 + <Trans>Disable composer prompt</Trans> 135 + </Toggle.LabelText> 136 + <Toggle.Platform /> 137 + </Toggle.Item> 138 + <Toggle.Item 139 + name="disable_verify_email_reminder" 140 + label={l`Disable verify email reminder`} 141 + value={disableVerifyEmailReminder} 142 + onChange={value => setDisableVerifyEmailReminder(value)} 143 + style={[a.w_full]}> 144 + <Toggle.LabelText style={[a.flex_1]}> 145 + <Trans>Disable verify email reminder</Trans> 146 + </Toggle.LabelText> 147 + <Toggle.Platform /> 148 + </Toggle.Item> 149 + <Admonition type="warning" style={[a.flex_1]}> 150 + <Trans> 151 + This only gets rid of the reminder on app launch, useful if your PDS 152 + does not have email verification setup.&nbsp; This does NOT give 153 + access to features locked behind email verification. 154 + </Trans> 155 + </Admonition> 156 + </SettingsList.Group> 157 + </RunesScreenLayout> 158 + ) 159 + }
+27
src/screens/Settings/RunesSettings/components/RunesScreenLayout.tsx
··· 1 + import {type ReactNode} from 'react' 2 + 3 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 4 + import * as Layout from '#/components/Layout' 5 + 6 + export function RunesScreenLayout({ 7 + titleText, 8 + children, 9 + }: { 10 + titleText: string 11 + children: ReactNode 12 + }) { 13 + return ( 14 + <Layout.Screen> 15 + <Layout.Header.Outer> 16 + <Layout.Header.BackButton /> 17 + <Layout.Header.Content> 18 + <Layout.Header.TitleText>{titleText}</Layout.Header.TitleText> 19 + </Layout.Header.Content> 20 + <Layout.Header.Slot /> 21 + </Layout.Header.Outer> 22 + <Layout.Content> 23 + <SettingsList.Container>{children}</SettingsList.Container> 24 + </Layout.Content> 25 + </Layout.Screen> 26 + ) 27 + }
+74
src/screens/Settings/RunesSettings/index.tsx
··· 1 + import {Trans, useLingui} from '@lingui/react/macro' 2 + import {type NativeStackScreenProps} from '@react-navigation/native-stack' 3 + 4 + import {type CommonNavigatorParams} from '#/lib/routes/types' 5 + import * as SettingsList from '#/screens/Settings/components/SettingsList' 6 + import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' 7 + import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' 8 + import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' 9 + import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 10 + import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' 11 + import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 12 + import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 13 + import {RunesScreenLayout} from './components/RunesScreenLayout' 14 + 15 + type Props = NativeStackScreenProps<CommonNavigatorParams> 16 + 17 + export function RunesSettingsScreen({}: Props) { 18 + const {t: l} = useLingui() 19 + 20 + return ( 21 + <RunesScreenLayout titleText={l`Runes`}> 22 + <SettingsList.LinkItem to="/settings/runes/menus" label={l`Menus`}> 23 + <SettingsList.ItemIcon icon={EllipsisIcon} /> 24 + <SettingsList.ItemText> 25 + <Trans>Menus</Trans> 26 + </SettingsList.ItemText> 27 + </SettingsList.LinkItem> 28 + <SettingsList.LinkItem to="/settings/runes/badges" label={l`Badges`}> 29 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 30 + <SettingsList.ItemText> 31 + <Trans>Badges</Trans> 32 + </SettingsList.ItemText> 33 + </SettingsList.LinkItem> 34 + <SettingsList.LinkItem 35 + to="/settings/runes/impressions" 36 + label={l`Impressions`}> 37 + <SettingsList.ItemIcon icon={VisibilityIcon} /> 38 + <SettingsList.ItemText> 39 + <Trans>Impressions</Trans> 40 + </SettingsList.ItemText> 41 + </SettingsList.LinkItem> 42 + <SettingsList.LinkItem 43 + to="/settings/runes/usability" 44 + label={l`Usability`}> 45 + <SettingsList.ItemIcon icon={AtomIcon} /> 46 + <SettingsList.ItemText> 47 + <Trans>Usability</Trans> 48 + </SettingsList.ItemText> 49 + </SettingsList.LinkItem> 50 + <SettingsList.LinkItem to="/settings/runes/display" label={l`Display`}> 51 + <SettingsList.ItemIcon icon={PaintRollerIcon} /> 52 + <SettingsList.ItemText> 53 + <Trans>Display</Trans> 54 + </SettingsList.ItemText> 55 + </SettingsList.LinkItem> 56 + <SettingsList.LinkItem 57 + to="/settings/runes/infrastructure" 58 + label={l`Infrastructure`}> 59 + <SettingsList.ItemIcon icon={EarthIcon} /> 60 + <SettingsList.ItemText> 61 + <Trans>Infrastructure</Trans> 62 + </SettingsList.ItemText> 63 + </SettingsList.LinkItem> 64 + <SettingsList.LinkItem 65 + to="/settings/runes/other-additions" 66 + label={l`Other additions`}> 67 + <SettingsList.ItemIcon icon={BeakerIcon} /> 68 + <SettingsList.ItemText> 69 + <Trans>Other additions</Trans> 70 + </SettingsList.ItemText> 71 + </SettingsList.LinkItem> 72 + </RunesScreenLayout> 73 + ) 74 + }
+7 -19
src/screens/Settings/Settings.tsx
··· 1 1 import {useState} from 'react' 2 - import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' 2 + import {Alert, LayoutAnimation, Pressable, View} from 'react-native' 3 3 import {useReducedMotion} from 'react-native-reanimated' 4 4 import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' 5 5 import {Trans, useLingui} from '@lingui/react/macro' 6 6 import {useNavigation} from '@react-navigation/native' 7 7 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 8 8 9 - import {HELP_DESK_URL} from '#/lib/constants' 10 9 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 11 10 import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' 12 11 import { ··· 38 37 import {useDialogControl} from '#/components/Dialog' 39 38 import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' 40 39 import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' 41 - import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' 42 40 import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' 43 41 import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' 44 42 import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' 45 - import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' 46 43 import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' 47 44 import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts' 48 45 import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 46 + import {Eclipse_Stroke2_Corner0_Rounded as EclipseIcon} from '#/components/icons/Eclipse' 49 47 import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' 50 48 import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' 51 49 import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' ··· 190 188 <SettingsList.LinkItem to="/moderation" label={l`Moderation`}> 191 189 <SettingsList.ItemIcon icon={HandIcon} /> 192 190 <SettingsList.ItemText> 193 - <Trans>Moderation</Trans> 191 + <Trans>Moderation and content filters</Trans> 194 192 </SettingsList.ItemText> 195 193 </SettingsList.LinkItem> 196 194 <SettingsList.LinkItem ··· 229 227 <Trans>Appearance</Trans> 230 228 </SettingsList.ItemText> 231 229 </SettingsList.LinkItem> 232 - <SettingsList.LinkItem to="/settings/runes" label={l`Runes`}> 233 - <SettingsList.ItemIcon icon={AtomIcon} /> 234 - <SettingsList.ItemText> 235 - <Trans>Runes</Trans> 236 - </SettingsList.ItemText> 237 - </SettingsList.LinkItem> 238 230 <SettingsList.LinkItem 239 231 to="/settings/accessibility" 240 232 label={l`Accessibility`}> ··· 249 241 <Trans>Languages</Trans> 250 242 </SettingsList.ItemText> 251 243 </SettingsList.LinkItem> 252 - <SettingsList.PressableItem 253 - onPress={() => void Linking.openURL(HELP_DESK_URL)} 254 - label={l`Code`} 255 - accessibilityHint={l`Opens code repository in browser`}> 256 - <SettingsList.ItemIcon icon={CircleQuestionIcon} /> 244 + <SettingsList.LinkItem to="/settings/runes" label={l`Runes`}> 245 + <SettingsList.ItemIcon icon={EclipseIcon} /> 257 246 <SettingsList.ItemText> 258 - <Trans>Source code</Trans> 247 + <Trans>Runes</Trans> 259 248 </SettingsList.ItemText> 260 - <SettingsList.Chevron /> 261 - </SettingsList.PressableItem> 249 + </SettingsList.LinkItem> 262 250 <SettingsList.LinkItem to="/settings/about" label={l`About`}> 263 251 <SettingsList.ItemIcon icon={BubbleInfoIcon} /> 264 252 <SettingsList.ItemText>