diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 528b9f4cd..3cf08ec33 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -297,7 +297,16 @@ func serve(cctx *cli.Context) error { e.GET("/settings/threads", server.WebGeneric) e.GET("/settings/external-embeds", server.WebGeneric) e.GET("/settings/accessibility", server.WebGeneric) + e.GET("/settings/runes", server.WebGeneric) + e.GET("/settings/runes/menus", server.WebGeneric) + e.GET("/settings/runes/badges", server.WebGeneric) + e.GET("/settings/runes/impressions", server.WebGeneric) + e.GET("/settings/runes/usability", server.WebGeneric) + e.GET("/settings/runes/display", server.WebGeneric) + e.GET("/settings/runes/infrastructure", server.WebGeneric) + e.GET("/settings/runes/other-additions", server.WebGeneric) e.GET("/settings/appearance", server.WebGeneric) + e.GET("/settings/appearance/color-theme", server.WebGeneric) e.GET("/settings/account", server.WebGeneric) e.GET("/settings/automation-label", server.WebGeneric) e.GET("/settings/privacy-and-security", server.WebGeneric) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index f92360e75..3f127d751 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -104,6 +104,7 @@ import {AccessibilitySettingsScreen} from '#/screens/Settings/AccessibilitySetti import {AccountSettingsScreen} from '#/screens/Settings/AccountSettings' import {ActivityPrivacySettingsScreen} from '#/screens/Settings/ActivityPrivacySettings' import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings' +import {AppearanceColorThemeSettingsScreen} from '#/screens/Settings/AppearanceSettings/ColorThemeSettings' import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings' import {AppPasswordsScreen} from '#/screens/Settings/AppPasswords' import {AutomationLabelSettingsScreen} from '#/screens/Settings/AutomationLabelSettings' @@ -128,6 +129,13 @@ import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/Not import {PetLabelSettingsScreen} from '#/screens/Settings/PetLabelSettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {RunesSettingsScreen} from '#/screens/Settings/RunesSettings' +import {RunesBadgesSettingsScreen} from '#/screens/Settings/RunesSettings/BadgesSettings' +import {RunesDisplaySettingsScreen} from '#/screens/Settings/RunesSettings/DisplaySettings' +import {RunesImpressionsSettingsScreen} from '#/screens/Settings/RunesSettings/ImpressionsSettings' +import {RunesInfrastructureSettingsScreen} from '#/screens/Settings/RunesSettings/InfrastructureSettings' +import {RunesMenusSettingsScreen} from '#/screens/Settings/RunesSettings/MenusSettings' +import {RunesOtherAdditionsSettingsScreen} from '#/screens/Settings/RunesSettings/OtherAdditionsSettings' +import {RunesUsabilitySettingsScreen} from '#/screens/Settings/RunesSettings/UsabilitySettings' import {SettingsScreen} from '#/screens/Settings/Settings' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' import { @@ -424,6 +432,41 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + RunesMenusSettingsScreen} + options={{title: title(msg`Menus`), requireAuth: true}} + /> + RunesBadgesSettingsScreen} + options={{title: title(msg`Badges`), requireAuth: true}} + /> + RunesImpressionsSettingsScreen} + options={{title: title(msg`Impressions`), requireAuth: true}} + /> + RunesUsabilitySettingsScreen} + options={{title: title(msg`Usability`), requireAuth: true}} + /> + RunesDisplaySettingsScreen} + options={{title: title(msg`Display`), requireAuth: true}} + /> + RunesInfrastructureSettingsScreen} + options={{title: title(msg`Infrastructure`), requireAuth: true}} + /> + RunesOtherAdditionsSettingsScreen} + options={{title: title(msg`Other additions`), requireAuth: true}} + /> AppearanceSettingsScreen} @@ -432,6 +475,14 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { requireAuth: true, }} /> + AppearanceColorThemeSettingsScreen} + options={{ + title: title(msg`Color Theme`), + requireAuth: true, + }} + /> AccountSettingsScreen} diff --git a/src/components/icons/Eclipse.tsx b/src/components/icons/Eclipse.tsx new file mode 100644 index 000000000..b87ec662d --- /dev/null +++ b/src/components/icons/Eclipse.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Eclipse_Stroke2_Corner0_Rounded = createSinglePathSVG({ + 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', +}) diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 354e53b97..099d6a665 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -50,7 +50,15 @@ export type CommonNavigatorParams = { PreferencesExternalEmbeds: undefined AccessibilitySettings: undefined AppearanceSettings: undefined + AppearanceColorThemeSettings: undefined RunesSettings: undefined + RunesMenusSettings: undefined + RunesBadgesSettings: undefined + RunesImpressionsSettings: undefined + RunesUsabilitySettings: undefined + RunesDisplaySettings: undefined + RunesInfrastructureSettings: undefined + RunesOtherAdditionsSettings: undefined AccountSettings: undefined AutomationLabelSettings: undefined PetLabelSettings: undefined diff --git a/src/routes.ts b/src/routes.ts index 670215b71..f07dc05a5 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -49,7 +49,15 @@ export const router = new Router({ PreferencesExternalEmbeds: '/settings/external-embeds', AccessibilitySettings: '/settings/accessibility', RunesSettings: '/settings/runes', + RunesMenusSettings: '/settings/runes/menus', + RunesBadgesSettings: '/settings/runes/badges', + RunesImpressionsSettings: '/settings/runes/impressions', + RunesUsabilitySettings: '/settings/runes/usability', + RunesDisplaySettings: '/settings/runes/display', + RunesInfrastructureSettings: '/settings/runes/infrastructure', + RunesOtherAdditionsSettings: '/settings/runes/other-additions', AppearanceSettings: '/settings/appearance', + AppearanceColorThemeSettings: '/settings/appearance/color-theme', SavedFeeds: '/settings/saved-feeds', AccountSettings: '/settings/account', AutomationLabelSettings: '/settings/automation-label', diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx index adac73b5e..2cbadb19d 100644 --- a/src/screens/Settings/AboutSettings.tsx +++ b/src/screens/Settings/AboutSettings.tsx @@ -8,11 +8,12 @@ import {Trans} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {useMutation} from '@tanstack/react-query' -import {STATUS_PAGE_URL} from '#/lib/constants' +import {HELP_DESK_URL, STATUS_PAGE_URL} from '#/lib/constants' import {type CommonNavigatorParams} from '#/lib/routes/types' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' import {BroomSparkle_Stroke2_Corner2_Rounded as BroomSparkleIcon} from '#/components/icons/BroomSparkle' +import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' import {CodeLines_Stroke2_Corner2_Rounded as CodeLinesIcon} from '#/components/icons/CodeLines' import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe' import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/icons/Newspaper' @@ -102,6 +103,12 @@ export function AboutSettingsScreen({}: Props) { Status Page + + + + Source code + + diff --git a/src/screens/Settings/AccessibilitySettings.tsx b/src/screens/Settings/AccessibilitySettings.tsx index d5819fa4d..e10280008 100644 --- a/src/screens/Settings/AccessibilitySettings.tsx +++ b/src/screens/Settings/AccessibilitySettings.tsx @@ -1,8 +1,13 @@ -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' +import {useState} from 'react' +import {View} from 'react-native' +import {Trans, useLingui} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' +import { + DEFAULT_ALT_TEXT_AI_MODEL, + DEFAULT_ALT_TEXT_AI_PROMPT, +} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' import {type CommonNavigatorParams} from '#/lib/routes/types' import { useHapticsDisabled, @@ -14,20 +19,35 @@ import { useLargeAltBadgeEnabled, useSetLargeAltBadgeEnabled, } from '#/state/preferences/large-alt-badge' +import { + useOpenRouterApiKey, + useOpenRouterConfigured, + useOpenRouterModel, + useOpenRouterPrompt, + useSetOpenRouterApiKey, + useSetOpenRouterModel, + useSetOpenRouterPrompt, +} from '#/state/preferences/openrouter' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' import * as Toggle from '#/components/forms/Toggle' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' import {Haptic_Stroke2_Corner2_Rounded as HapticIcon} from '#/components/icons/Haptic' +import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' import * as Layout from '#/components/Layout' -import {IS_NATIVE} from '#/env' +import {InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' +import {IS_NATIVE, IS_WEB} from '#/env' type Props = NativeStackScreenProps< CommonNavigatorParams, 'AccessibilitySettings' > export function AccessibilitySettingsScreen({}: Props) { - const {_} = useLingui() + const {t: l} = useLingui() const requireAltTextEnabled = useRequireAltTextEnabled() const setRequireAltTextEnabled = useSetRequireAltTextEnabled() @@ -36,6 +56,12 @@ export function AccessibilitySettingsScreen({}: Props) { const largeAltBadgeEnabled = useLargeAltBadgeEnabled() const setLargeAltBadgeEnabled = useSetLargeAltBadgeEnabled() + const setOpenRouterApiKeyControl = Dialog.useDialogControl() + const openRouterConfigured = useOpenRouterConfigured() + const openRouterModel = useOpenRouterModel() + const setOpenRouterModelControl = Dialog.useDialogControl() + const setOpenRouterPromptControl = Dialog.useDialogControl() + return ( @@ -56,7 +82,7 @@ export function AccessibilitySettingsScreen({}: Props) { setRequireAltTextEnabled(value)} style={[a.w_full]}> @@ -67,7 +93,7 @@ export function AccessibilitySettingsScreen({}: Props) { setLargeAltBadgeEnabled(value)} style={[a.w_full]}> @@ -87,7 +113,7 @@ export function AccessibilitySettingsScreen({}: Props) { setHapticsDisabled(value)} style={[a.w_full]}> @@ -99,8 +125,294 @@ export function AccessibilitySettingsScreen({}: Props) { )} + + + + + OpenRouter API Key + + setOpenRouterApiKeyControl.open()} + /> + + + + + + Set your OpenRouter API key to enable AI-powered alt text + generation for images in the composer. Get an API key at{' '} + + openrouter.ai + + + + + + {openRouterConfigured && ( + + + + {`OpenRouter Model`} + + setOpenRouterModelControl.open()} + /> + + )} + + {openRouterConfigured && ( + + + + Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} + + Search models + + + + + )} + + {openRouterConfigured && ( + + + + Alt Text Prompt + + setOpenRouterPromptControl.open()} + /> + + )} + + {openRouterConfigured && ( + + + + Customize the prompt sent to the AI model when generating alt + text. Leave empty to use the default prompt. + + + + )} + + + + ) } + +function OpenRouterApiKeyDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const apiKey = useOpenRouterApiKey() + const [value, setValue] = useState(apiKey ?? '') + const setApiKey = useSetOpenRouterApiKey() + + return ( + setValue(apiKey ?? '')}> + + + + + OpenRouter API Key + + + + + { + setApiKey(value.trim() || undefined) + control.close() + }} + accessibilityHint={l`Enter your OpenRouter API key for AI alt text generation`} + defaultValue={apiKey ?? ''} + secureTextEntry + /> + + + + + + + + + + ) +} + +function OpenRouterModelDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const model = useOpenRouterModel() + const [value, setValue] = useState(model ?? '') + const setModel = useSetOpenRouterModel() + + return ( + setValue(model ?? '')}> + + + + + OpenRouter Model + + + + + { + setModel(value.trim() || undefined) + control.close() + }} + accessibilityHint={l`Enter the model ID to use for alt text generation`} + defaultValue={model ?? ''} + /> + + + + + + + + + + ) +} + +function OpenRouterPromptDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const prompt = useOpenRouterPrompt() + const [value, setValue] = useState(prompt ?? '') + const setPrompt = useSetOpenRouterPrompt() + + return ( + setValue(prompt ?? '')}> + + + + + Alt Text Prompt + + + + + + + + + + + + + + + ) +} + +const styles = { + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, +} diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx index ea2614e64..05b32647f 100644 --- a/src/screens/Settings/AppearanceSettings.tsx +++ b/src/screens/Settings/AppearanceSettings.tsx @@ -1,5 +1,4 @@ -import {useCallback, useMemo} from 'react' -import {Pressable, View} from 'react-native' +import {useCallback} from 'react' import Animated, { FadeInUp, FadeOutUp, @@ -14,7 +13,6 @@ import { type CommonNavigatorParams, type NativeStackScreenProps, } from '#/lib/routes/types' -import {type Schema} from '#/state/persisted' import { useEnableSquareAvatars, useSetEnableSquareAvatars, @@ -26,30 +24,13 @@ import { import {useKawaiiMode, useSetKawaiiMode} from '#/state/preferences/kawaii' import {useSetThemePrefs, useThemePrefs} from '#/state/shell' import {SettingsListItem as AppIconSettingsListItem} from '#/screens/Settings/AppIconSettings/SettingsListItem' +import {ItemTextWithSubtitle} from '#/screens/Settings/NotificationSettings/components/ItemTextWithSubtitle' import {type Alf, atoms as a, native, useAlf, useTheme} from '#/alf' -import { - BLACKSKY_PALETTE, - BLUESKY_PALETTE, - CATPPUCIN_PALETTE, - DEER_PALETTE, - DEFAULT_PALETTE, - EVERGARDEN_PALETTE, - KITTY_PALETTE, - REDDWARF_PALETTE, - ZEPPELIN_PALETTE, -} from '#/alf/themes' -import {getMaterial3Colors} from '#/alf/util/material3Theme' -import {useMaterialYouPalette} from '#/alf/util/materialYou' import * as SegmentedControl from '#/components/forms/SegmentedControl' -import {Slider} from '#/components/forms/Slider' import * as Toggle from '#/components/forms/Toggle' import {Circle_And_Square_Stroke1_Corner0_Rounded_Filled as SquareIcon} from '#/components/icons/CircleAndSquare' import {ColorPalette_Stroke2_Corner0_Rounded as ColorPaletteIcon} from '#/components/icons/ColorPalette' import {type Props as SVGIconProps} from '#/components/icons/common' -import { - Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, - Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, -} from '#/components/icons/Heart2' import {Moon_Stroke2_Corner0_Rounded as MoonIcon} from '#/components/icons/Moon' import {Phone_Stroke2_Corner0_Rounded as PhoneIcon} from '#/components/icons/Phone' import {Sparkle_Stroke2_Corner0_Rounded as SparkleIcon} from '#/components/icons/Sparkle' @@ -58,49 +39,17 @@ import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleC import * as Layout from '#/components/Layout' import {Text} from '#/components/Typography' import {IS_ANDROID, IS_INTERNAL, IS_NATIVE} from '#/env' +import {getColorSchemeLabel, useColorSchemes} from './AppearanceSettings/shared' import * as SettingsList from './components/SettingsList' type Props = NativeStackScreenProps -type ColorSchemeName = - | 'witchsky' - | 'bluesky' - | 'blacksky' - | 'deer' - | 'zeppelin' - | 'kitty' - | 'reddwarf' - | 'catppuccin' - | 'evergarden' - | 'material3' - -type ColorSchemeOption = { - name: ColorSchemeName - label: string - primary: string -} - export function AppearanceSettingsScreen({}: Props) { const {_} = useLingui() const {fonts} = useAlf() - const t = useTheme() - const { - colorMode, - colorScheme, - darkTheme, - hue, - material3Accent, - material3Style, - } = useThemePrefs() - const { - setColorMode, - setColorScheme, - setDarkTheme, - setHue, - setMaterial3Accent, - setMaterial3Style, - } = useSetThemePrefs() + const {colorMode, colorScheme, darkTheme} = useThemePrefs() + const {setColorMode, setDarkTheme} = useSetThemePrefs() const kawaiiMode = useKawaiiMode() const setKawaiiMode = useSetKawaiiMode() @@ -111,11 +60,8 @@ export function AppearanceSettingsScreen({}: Props) { const enableSquareButtons = useEnableSquareButtons() const setEnableSquareButtons = useSetEnableSquareButtons() - const material3Palette = useMaterialYouPalette() - const cachedScheme = useMemo( - () => getMaterial3Colors(material3Palette), - [material3Palette], - ) + const colorSchemes = useColorSchemes() + const colorSchemeLabel = getColorSchemeLabel(colorSchemes, colorScheme) const onChangeAppearance = useCallback( (value: 'light' | 'system' | 'dark') => { @@ -124,13 +70,6 @@ export function AppearanceSettingsScreen({}: Props) { [setColorMode], ) - const onChangeScheme = useCallback( - (value: ColorSchemeName) => { - setColorScheme(value) - }, - [setColorScheme], - ) - const onChangeDarkTheme = useCallback( (value: 'dim' | 'dark') => { setDarkTheme(value) @@ -152,59 +91,6 @@ export function AppearanceSettingsScreen({}: Props) { [fonts], ) - const colorSchemes: ColorSchemeOption[] = [ - { - name: 'witchsky', - label: _(msg`Witchsky`), - primary: DEFAULT_PALETTE.primary_500, - }, - { - name: 'bluesky', - label: _(msg`Bluesky`), - primary: BLUESKY_PALETTE.primary_500, - }, - { - name: 'blacksky', - label: _(msg`Blacksky`), - primary: BLACKSKY_PALETTE.primary_500, - }, - { - name: 'deer', - label: _(msg`Deer`), - primary: DEER_PALETTE.primary_500, - }, - { - name: 'zeppelin', - label: _(msg`Zeppelin`), - primary: ZEPPELIN_PALETTE.primary_500, - }, - { - name: 'kitty', - label: _(msg`Kitty`), - primary: KITTY_PALETTE.primary_500, - }, - { - name: 'reddwarf', - label: _(msg`Red Dwarf`), - primary: REDDWARF_PALETTE.primary_500, - }, - { - name: 'catppuccin', - label: _(msg`Catppuccin`), - primary: CATPPUCIN_PALETTE.primary_500, - }, - { - name: 'evergarden', - label: _(msg`Evergarden`), - primary: EVERGARDEN_PALETTE.primary_500, - }, - { - name: 'material3', - label: _(msg`Material You`), - primary: cachedScheme.regular.primary_500, - }, - ] - return ( @@ -263,99 +149,16 @@ export function AppearanceSettingsScreen({}: Props) { )} - + - - Color Theme - - - - Choose which color scheme to use: - - - {colorScheme === 'material3' && !IS_ANDROID && ( - <> - - Accent hue: - - { - setMaterial3Accent(hueToHex(v)) - setHue(0) - }} - minimumValue={0} - maximumValue={360} - step={1} - debounceFull={true} - /> - - - Style: - - - {MATERIAL3_STYLE_OPTIONS.map(({name, label}) => { - const isSelected = material3Style === name - return ( - setMaterial3Style(name)} - style={[ - a.flex_1, - a.rounded_sm, - a.align_center, - a.px_sm, - a.py_sm, - a.border, - {minWidth: '22%'}, - { - borderColor: isSelected - ? t.palette.primary_500 - : t.atoms.border_contrast_low.borderColor, - borderWidth: 2, - backgroundColor: isSelected - ? t.palette.primary_100 - : t.atoms.bg.backgroundColor, - }, - ]}> - - {label} - - - ) - })} - - - )} - {colorScheme !== 'material3' && ( - <> - - Hue shift the colors: - - - - )} - - + Color Theme} + subtitleText={colorSchemeLabel} + /> + @@ -472,72 +275,6 @@ export function AppearanceSettingsScreen({}: Props) { ) } -function ColorSchemeGrid({ - schemes, - selectedScheme, - onSchemeChange, -}: { - schemes: ColorSchemeOption[] - selectedScheme: ColorSchemeName - onSchemeChange: (scheme: ColorSchemeName) => void -}) { - const t = useTheme() - return ( - - {schemes.map(({name, label, primary}) => { - const isSelected = selectedScheme === name - const HeartIcon = isSelected ? HeartIconFilled : HeartIconOutline - return ( - onSchemeChange(name)} - style={[ - a.flex_1, - a.rounded_md, - a.overflow_hidden, - {minWidth: '30%'}, - a.border, - { - borderColor: isSelected - ? primary - : t.atoms.border_contrast_low.borderColor, - borderWidth: 2, - }, - ]}> - - - - - {label} - - - - - - ) - })} - - ) -} - export function AppearanceToggleButtonGroup({ title, description, @@ -593,65 +330,3 @@ export function AppearanceToggleButtonGroup({ ) } - -const MATERIAL3_STYLE_OPTIONS: { - name: Schema['material3Style'] - label: string -}[] = [ - {name: 'TONAL_SPOT', label: 'Tonal Spot'}, - {name: 'VIBRANT', label: 'Vibrant'}, - {name: 'EXPRESSIVE', label: 'Expressive'}, - {name: 'SPRITZ', label: 'Spritz'}, - {name: 'RAINBOW', label: 'Rainbow'}, - {name: 'FRUIT_SALAD', label: 'Fruit Salad'}, - {name: 'CONTENT', label: 'Content'}, - {name: 'MONOCHROMATIC', label: 'Mono'}, -] - -function hueToHex(hue: number): string { - const h = hue / 60 - const x = 1 - Math.abs((h % 2) - 1) - let r = 0, - g = 0, - b = 0 - if (h < 1) { - r = 1 - g = x - } else if (h < 2) { - r = x - g = 1 - } else if (h < 3) { - g = 1 - b = x - } else if (h < 4) { - g = x - b = 1 - } else if (h < 5) { - r = x - b = 1 - } else { - r = 1 - b = x - } - const toHex = (v: number) => - Math.round(v * 255) - .toString(16) - .padStart(2, '0') - return `#${toHex(r)}${toHex(g)}${toHex(b)}` -} - -function hexToHue(hex: string): number { - const r = parseInt(hex.slice(1, 3), 16) / 255 - const g = parseInt(hex.slice(3, 5), 16) / 255 - const b = parseInt(hex.slice(5, 7), 16) / 255 - const max = Math.max(r, g, b) - const min = Math.min(r, g, b) - const d = max - min - if (d === 0) return 0 - let h = 0 - if (max === r) h = ((g - b) / d) % 6 - else if (max === g) h = (b - r) / d + 2 - else h = (r - g) / d + 4 - h = Math.round(h * 60) - return h < 0 ? h + 360 : h -} diff --git a/src/screens/Settings/AppearanceSettings/ColorThemeSettings.tsx b/src/screens/Settings/AppearanceSettings/ColorThemeSettings.tsx new file mode 100644 index 000000000..886313652 --- /dev/null +++ b/src/screens/Settings/AppearanceSettings/ColorThemeSettings.tsx @@ -0,0 +1,144 @@ +import {useCallback} from 'react' +import {Pressable, View} from 'react-native' +import {Trans} from '@lingui/react/macro' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' + +import {type CommonNavigatorParams} from '#/lib/routes/types' +import {useSetThemePrefs, useThemePrefs} from '#/state/shell' +import {atoms as a, useTheme} from '#/alf' +import {Slider} from '#/components/forms/Slider' +import * as Layout from '#/components/Layout' +import {Text} from '#/components/Typography' +import {IS_ANDROID} from '#/env' +import * as SettingsList from '../components/SettingsList' +import { + ColorSchemeGrid, + type ColorSchemeName, + hexToHue, + hueToHex, + MATERIAL3_STYLE_OPTIONS, + useColorSchemes, +} from './shared' + +type Props = NativeStackScreenProps< + CommonNavigatorParams, + 'AppearanceColorThemeSettings' +> + +export function AppearanceColorThemeSettingsScreen({}: Props) { + const t = useTheme() + const colorSchemes = useColorSchemes() + + const {colorScheme, hue, material3Accent, material3Style} = useThemePrefs() + const {setColorScheme, setHue, setMaterial3Accent, setMaterial3Style} = + useSetThemePrefs() + + const onChangeScheme = useCallback( + (value: ColorSchemeName) => { + setColorScheme(value) + }, + [setColorScheme], + ) + + return ( + + + + + + Color Theme + + + + + + + + + {colorScheme === 'material3' && !IS_ANDROID ? ( + <> + + Accent hue: + + { + setMaterial3Accent(hueToHex(value)) + setHue(0) + }} + minimumValue={0} + maximumValue={360} + step={1} + debounceFull={true} + /> + + + Style: + + + {MATERIAL3_STYLE_OPTIONS.map(({name, label}) => { + const isSelected = material3Style === name + + return ( + setMaterial3Style(name)} + style={[ + a.flex_1, + a.rounded_sm, + a.align_center, + a.px_sm, + a.py_sm, + a.border, + {minWidth: '22%'}, + { + borderColor: isSelected + ? t.palette.primary_500 + : t.atoms.border_contrast_low.borderColor, + borderWidth: 2, + backgroundColor: isSelected + ? t.palette.primary_100 + : t.atoms.bg.backgroundColor, + }, + ]}> + + {label} + + + ) + })} + + + ) : ( + <> + + Hue shift the colors: + + + + )} + + + + + ) +} diff --git a/src/screens/Settings/AppearanceSettings/shared.tsx b/src/screens/Settings/AppearanceSettings/shared.tsx new file mode 100644 index 000000000..0ac8cebaf --- /dev/null +++ b/src/screens/Settings/AppearanceSettings/shared.tsx @@ -0,0 +1,252 @@ +import {useMemo} from 'react' +import {Pressable, View} from 'react-native' +import {msg} from '@lingui/core/macro' +import {useLingui} from '@lingui/react' + +import {type Schema} from '#/state/persisted' +import {atoms as a, useTheme} from '#/alf' +import { + BLACKSKY_PALETTE, + BLUESKY_PALETTE, + CATPPUCIN_PALETTE, + DEER_PALETTE, + DEFAULT_PALETTE, + EVERGARDEN_PALETTE, + KITTY_PALETTE, + REDDWARF_PALETTE, + ZEPPELIN_PALETTE, +} from '#/alf/themes' +import {getMaterial3Colors} from '#/alf/util/material3Theme' +import {useMaterialYouPalette} from '#/alf/util/materialYou' +import { + Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled, + Heart2_Stroke2_Corner0_Rounded as HeartIconOutline, +} from '#/components/icons/Heart2' +import {Text} from '#/components/Typography' + +export type ColorSchemeName = + | 'witchsky' + | 'bluesky' + | 'blacksky' + | 'deer' + | 'zeppelin' + | 'kitty' + | 'reddwarf' + | 'catppuccin' + | 'evergarden' + | 'material3' + +export type ColorSchemeOption = { + name: ColorSchemeName + label: string + primary: string +} + +export function useColorSchemes() { + const {_} = useLingui() + const material3Palette = useMaterialYouPalette() + const cachedScheme = useMemo( + () => getMaterial3Colors(material3Palette), + [material3Palette], + ) + + return useMemo( + () => [ + { + name: 'witchsky', + label: _(msg`Witchsky`), + primary: DEFAULT_PALETTE.primary_500, + }, + { + name: 'bluesky', + label: _(msg`Bluesky`), + primary: BLUESKY_PALETTE.primary_500, + }, + { + name: 'blacksky', + label: _(msg`Blacksky`), + primary: BLACKSKY_PALETTE.primary_500, + }, + { + name: 'deer', + label: _(msg`Deer`), + primary: DEER_PALETTE.primary_500, + }, + { + name: 'zeppelin', + label: _(msg`Zeppelin`), + primary: ZEPPELIN_PALETTE.primary_500, + }, + { + name: 'kitty', + label: _(msg`Kitty`), + primary: KITTY_PALETTE.primary_500, + }, + { + name: 'reddwarf', + label: _(msg`Red Dwarf`), + primary: REDDWARF_PALETTE.primary_500, + }, + { + name: 'catppuccin', + label: _(msg`Catppuccin`), + primary: CATPPUCIN_PALETTE.primary_500, + }, + { + name: 'evergarden', + label: _(msg`Evergarden`), + primary: EVERGARDEN_PALETTE.primary_500, + }, + { + name: 'material3', + label: _(msg`Material You`), + primary: cachedScheme.regular.primary_500, + }, + ], + [_, cachedScheme], + ) +} + +export function getColorSchemeLabel( + colorSchemes: ColorSchemeOption[], + colorScheme: ColorSchemeName, +) { + return ( + colorSchemes.find(scheme => scheme.name === colorScheme)?.label ?? + colorScheme + ) +} + +export function ColorSchemeGrid({ + schemes, + selectedScheme, + onSchemeChange, +}: { + schemes: ColorSchemeOption[] + selectedScheme: ColorSchemeName + onSchemeChange: (scheme: ColorSchemeName) => void +}) { + const t = useTheme() + + return ( + + {schemes.map(({name, label, primary}) => { + const isSelected = selectedScheme === name + const HeartIcon = isSelected ? HeartIconFilled : HeartIconOutline + + return ( + onSchemeChange(name)} + style={[ + a.flex_1, + a.rounded_md, + a.overflow_hidden, + {minWidth: '30%'}, + a.border, + { + borderColor: isSelected + ? primary + : t.atoms.border_contrast_low.borderColor, + borderWidth: 2, + }, + ]}> + + + + + {label} + + + + + + ) + })} + + ) +} + +export const MATERIAL3_STYLE_OPTIONS: { + name: Schema['material3Style'] + label: string +}[] = [ + {name: 'TONAL_SPOT', label: 'Tonal Spot'}, + {name: 'VIBRANT', label: 'Vibrant'}, + {name: 'EXPRESSIVE', label: 'Expressive'}, + {name: 'SPRITZ', label: 'Spritz'}, + {name: 'RAINBOW', label: 'Rainbow'}, + {name: 'FRUIT_SALAD', label: 'Fruit Salad'}, + {name: 'CONTENT', label: 'Content'}, + {name: 'MONOCHROMATIC', label: 'Mono'}, +] + +export function hueToHex(hue: number): string { + const h = hue / 60 + const x = 1 - Math.abs((h % 2) - 1) + let r = 0, + g = 0, + b = 0 + if (h < 1) { + r = 1 + g = x + } else if (h < 2) { + r = x + g = 1 + } else if (h < 3) { + g = 1 + b = x + } else if (h < 4) { + g = x + b = 1 + } else if (h < 5) { + r = x + b = 1 + } else { + r = 1 + b = x + } + + const toHex = (v: number) => + Math.round(v * 255) + .toString(16) + .padStart(2, '0') + return `#${toHex(r)}${toHex(g)}${toHex(b)}` +} + +export function hexToHue(hex: string): number { + const r = parseInt(hex.slice(1, 3), 16) / 255 + const g = parseInt(hex.slice(3, 5), 16) / 255 + const b = parseInt(hex.slice(5, 7), 16) / 255 + const max = Math.max(r, g, b) + const min = Math.min(r, g, b) + const d = max - min + + if (d === 0) return 0 + + let h = 0 + if (max === r) h = ((g - b) / d) % 6 + else if (max === g) h = (b - r) / d + 2 + else h = (r - g) / d + 4 + + h = Math.round(h * 60) + return h < 0 ? h + 360 : h +} diff --git a/src/screens/Settings/RunesSettings.tsx b/src/screens/Settings/RunesSettings.tsx deleted file mode 100644 index 737571935..000000000 --- a/src/screens/Settings/RunesSettings.tsx +++ /dev/null @@ -1,2094 +0,0 @@ -import {useState} from 'react' -import {View} from 'react-native' -import {isDid} from '@atproto/api' -import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' -import {msg} from '@lingui/core/macro' -import {useLingui} from '@lingui/react' -import {Trans} from '@lingui/react/macro' -import {type NativeStackScreenProps} from '@react-navigation/native-stack' - -import { - APPVIEW_DID_PROXY, - DEFAULT_ALT_TEXT_AI_MODEL, - DEFAULT_ALT_TEXT_AI_PROMPT, -} from '#/lib/constants' -import {usePalette} from '#/lib/hooks/usePalette' -import {type CommonNavigatorParams} from '#/lib/routes/types' -import {dynamicActivate} from '#/locale/i18n' -import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web' -import {type AppLanguage} from '#/locale/languages' -import * as persisted from '#/state/persisted' -import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' -import { - useConstellationInstance, - useSetConstellationInstance, -} from '#/state/preferences/constellation-instance' -import { - useCustomAppViewDid, - useSetCustomAppViewDid, -} from '#/state/preferences/custom-appview-did' -import { - useDeerVerificationEnabled, - useDeerVerificationTrusted, - useSetDeerVerificationEnabled, -} from '#/state/preferences/deer-verification' -import { - useDirectFetchRecords, - useSetDirectFetchRecords, -} from '#/state/preferences/direct-fetch-records' -import { - useDisableComposerPrompt, - useSetDisableComposerPrompt, -} from '#/state/preferences/disable-composer-prompt' -import { - useDisableFollowedByMetrics, - useSetDisableFollowedByMetrics, -} from '#/state/preferences/disable-followed-by-metrics' -import { - useDisableFollowersMetrics, - useSetDisableFollowersMetrics, -} from '#/state/preferences/disable-followers-metrics' -import { - useDisableFollowingMetrics, - useSetDisableFollowingMetrics, -} from '#/state/preferences/disable-following-metrics' -import { - useDisableLikesMetrics, - useSetDisableLikesMetrics, -} from '#/state/preferences/disable-likes-metrics' -import { - useDisablePostsMetrics, - useSetDisablePostsMetrics, -} from '#/state/preferences/disable-posts-metrics' -import { - useDisableQuotesMetrics, - useSetDisableQuotesMetrics, -} from '#/state/preferences/disable-quotes-metrics' -import { - useDisableReplyMetrics, - useSetDisableReplyMetrics, -} from '#/state/preferences/disable-reply-metrics' -import { - useDisableRepostsMetrics, - useSetDisableRepostsMetrics, -} from '#/state/preferences/disable-reposts-metrics' -import { - useDisableSavesMetrics, - useSetDisableSavesMetrics, -} from '#/state/preferences/disable-saves-metrics' -import { - useDisableVerifyEmailReminder, - useSetDisableVerifyEmailReminder, -} from '#/state/preferences/disable-verify-email-reminder' -import { - useDisableViaRepostNotification, - useSetDisableViaRepostNotification, -} from '#/state/preferences/disable-via-repost-notification' -import { - useDiscoverContextEnabled, - useSetDiscoverContextEnabled, -} from '#/state/preferences/discover-context-enabled' -import { - useSetShowExternalShareButtons, - useShowExternalShareButtons, -} from '#/state/preferences/external-share-buttons' -import { - useFaviconService, - useSetFaviconService, -} from '#/state/preferences/favicon-service' -import { - useHideFeedsPromoTab, - useSetHideFeedsPromoTab, -} from '#/state/preferences/hide-feeds-promo-tab' -import { - useHideSimilarAccountsRecomm, - useSetHideSimilarAccountsRecomm, -} from '#/state/preferences/hide-similar-accounts-recommendations' -import { - useHideUnreplyablePosts, - useSetHideUnreplyablePosts, -} from '#/state/preferences/hide-unreplyable-posts' -import { - useHighQualityImages, - useSetHighQualityImages, -} from '#/state/preferences/high-quality-images' -import { - useImageCdnHost, - useSetImageCdnHost, -} from '#/state/preferences/image-cdn-host' -import {useModerationOpts} from '#/state/preferences/moderation-opts' -import { - useNoAppLabelers, - useSetNoAppLabelers, -} from '#/state/preferences/no-app-labelers' -import { - useNoDiscoverFallback, - useSetNoDiscoverFallback, -} from '#/state/preferences/no-discover-fallback' -import { - useOpenRouterApiKey, - useOpenRouterConfigured, - useOpenRouterModel, - useOpenRouterPrompt, - useSetOpenRouterApiKey, - useSetOpenRouterModel, - useSetOpenRouterPrompt, -} from '#/state/preferences/openrouter' -import { - usePdsLabelEnabled, - usePdsLabelHideBskyPds, - useSetPdsLabelEnabled, - useSetPdsLabelHideBskyPds, -} from '#/state/preferences/pds-label' -import { - usePlcDirectory, - useSetPlcDirectory, -} from '#/state/preferences/plc-directory' -import { - usePostReplacement, - useSetPostReplacement, -} from '#/state/preferences/post-name-replacement' -import { - useRepostCarouselEnabled, - useSetRepostCarouselEnabled, -} from '#/state/preferences/repost-carousel-enabled' -import { - useSetShowFollowsYouBadge, - useShowFollowsYouBadge, -} from '#/state/preferences/show-follows-you-badge' -import { - useSetShowLinkInHandle, - useShowLinkInHandle, -} from '#/state/preferences/show-link-in-handle.tsx' -import { - useSetShowLinkInHandleOnlyOnWorkingLinks, - useShowLinkInHandleOnlyOnWorkingLinks, -} from '#/state/preferences/show-link-in-handle-only-on-working-links' -import { - useLibreTranslateInstance, - useSetLibreTranslateInstance, - useSetTranslationServicePreference, - useTranslationServicePreference, -} from '#/state/preferences/translation-service-preference' -import { - useHandleInLinks, - useSetHandleInLinks, -} from '#/state/preferences/use-handle-in-links' -import {useProfilesQuery} from '#/state/queries/profile' -import {findService, useDidDocument} from '#/state/queries/resolve-identity' -import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' -import * as SettingsList from '#/screens/Settings/components/SettingsList' -import {atoms as a, useBreakpoints} from '#/alf' -import {Admonition} from '#/components/Admonition' -import {Button, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import * as Toggle from '#/components/forms/Toggle' -import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' -import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' -import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' -import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' -import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' -import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' -import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' -import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' -import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' -import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' -import * as Layout from '#/components/Layout' -import {InlineLinkText} from '#/components/Link' -import {Text} from '#/components/Typography' -import {IS_WEB} from '#/env' -import { - useAutoLikeOnRepost, - useSetAutoLikeOnRepost, -} from '../../state/preferences/auto-like-on-repost.tsx' -import {SearchProfileCard} from '../Search/components/SearchProfileCard' - -type Props = NativeStackScreenProps - -function ConstellationInstanceDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const constellationInstance = useConstellationInstance() - const [url, setUrl] = useState(constellationInstance ?? '') - const setConstellationInstance = useSetConstellationInstance() - - const submit = () => { - setConstellationInstance(url) - control.close() - } - - const shouldDisable = () => { - try { - return !new URL(url).hostname.includes('.') - } catch (e) { - return true - } - } - - return ( - setUrl(constellationInstance ?? '')}> - - - - - Constellations instance URL - - - - - { - setUrl(value) - }} - placeholder={persisted.defaults.constellationInstance} - placeholderTextColor={pal.colors.textLight} - onSubmitEditing={submit} - accessibilityHint={_( - msg`Input the url of the constellations instance to use`, - )} - defaultValue={constellationInstance} - /> - - - - - - - - - - ) -} - -function CustomAppViewDidDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const [customAppViewDid] = useCustomAppViewDid() - const [did, setDid] = useState(customAppViewDid ?? '') - const setCustomAppViewDid = useSetCustomAppViewDid() - - const doc = useDidDocument({did}) - const bskyAppViewService = - doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') - - const submit = () => { - if (did.length === 0) { - control.close(() => { - setCustomAppViewDid(undefined) - }) - return - } - if (!bskyAppViewService?.serviceEndpoint) return - control.close(() => { - setCustomAppViewDid(did) - }) - } - - return ( - setDid(customAppViewDid ?? '')}> - - - - - Custom AppView Proxy DID - - - - - { - setDid(value) - }} - placeholder={ - APPVIEW_DID_PROXY?.substring(0, APPVIEW_DID_PROXY.indexOf('#')) || - `did:web:api.bsky.app` - } - placeholderTextColor={pal.colors.textLight} - onSubmitEditing={submit} - accessibilityHint={_( - msg`Input the DID of the AppView to proxy requests through`, - )} - isInvalid={ - !!did && !bskyAppViewService?.serviceEndpoint && !doc.isLoading - } - defaultValue={customAppViewDid ?? ''} - /> - - {did && !isDid(did) && ( - - - - )} - - {did && (did.includes('#') || did.includes('?')) && ( - - - - )} - - {doc.isError && ( - - - - )} - - {doc.data && - !bskyAppViewService && - (doc.data as {message?: string}).message && ( - - - - )} - - {doc.data && !bskyAppViewService && ( - - - - )} - - {bskyAppViewService && ( - - {JSON.stringify(bskyAppViewService, null, 2)} - - )} - - - - - - - - - - ) -} - -function FaviconServiceDialog({control}: {control: Dialog.DialogControlProps}) { - const pal = usePalette('default') - const {_} = useLingui() - - const faviconService = useFaviconService() - const [url, setUrl] = useState(faviconService ?? '') - const [inputVersion, setInputVersion] = useState(0) - const setFaviconService = useSetFaviconService() - - const updateInputValue = (nextUrl: string) => { - setUrl(nextUrl) - setInputVersion(v => v + 1) - } - - const submit = () => { - setFaviconService(url.trim()) - control.close() - } - - const shouldDisable = () => { - return url.length > 0 && !url.includes('(pds)') - } - - const presets = [ - 'https://twenty-icons.com/(pds)', - 'https://favicon.im/(pds)?larger=true&throw-error-on-404=true', - 'https://favicon.blueat.net/(pds)?larger=true&throw-error-on-404=true', - ] - - return ( - updateInputValue(faviconService ?? '')}> - - - - - Favicon Service URL - - - - (pds) is replaced with the domain of an account's host. - - - - - - setUrl(value)} - placeholder={persisted.defaults.faviconService} - placeholderTextColor={pal.colors.textLight} - onSubmitEditing={submit} - accessibilityHint={_( - msg`Enter the favicon service URL with (pds) as placeholder`, - )} - defaultValue={url} - /> - - - {presets.map(preset => ( - - ))} - - - - - - - - - - - ) -} - -function LibreTranslateInstanceDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const libreTranslateInstance = useLibreTranslateInstance() - const [url, setUrl] = useState(libreTranslateInstance ?? '') - const setLibreTranslateInstance = useSetLibreTranslateInstance() - - const submit = () => { - setLibreTranslateInstance(url) - control.close() - } - - const shouldDisable = () => { - try { - return !new URL(url).hostname.includes('.') - } catch (e) { - return true - } - } - - return ( - setUrl(libreTranslateInstance ?? '')}> - - - - - LibreTranslate instance URL - - - - - { - setUrl(value) - }} - placeholder={persisted.defaults.libreTranslateInstance} - placeholderTextColor={pal.colors.textLight} - onSubmitEditing={submit} - accessibilityHint={_( - msg`Input the url of the LibreTranslate instance to use`, - )} - defaultValue={libreTranslateInstance} - /> - - - - - - - - - - ) -} - -function ImageCdnHostDialog({control}: {control: Dialog.DialogControlProps}) { - const pal = usePalette('default') - const {_} = useLingui() - - const imageCdnHost = useImageCdnHost() - const [url, setUrl] = useState(imageCdnHost ?? '') - const setImageCdnHost = useSetImageCdnHost() - - const submit = () => { - const trimmedUrl = url.trim() - if (!trimmedUrl) { - control.close(() => { - setImageCdnHost(undefined) - }) - return - } - - control.close(() => { - try { - setImageCdnHost(new URL(trimmedUrl).origin) - } catch { - setImageCdnHost(trimmedUrl) - } - }) - } - - const isReset = url.trim().length === 0 - - const shouldDisable = () => { - if (isReset) return false - - try { - return !new URL(url).hostname.includes('.') - } catch (e) { - return true - } - } - - return ( - setUrl(imageCdnHost ?? '')}> - - - - - Image CDN URL - - - - - { - setUrl(value) - }} - placeholder={persisted.defaults.imageCdnHost} - placeholderTextColor={pal.colors.textLight} - onSubmitEditing={submit} - accessibilityHint={_(msg`Input the URL of the image CDN to use`)} - defaultValue={imageCdnHost} - /> - - - - - - - - - - ) -} - -function PlcDirectoryDialog({control}: {control: Dialog.DialogControlProps}) { - const pal = usePalette('default') - const {_} = useLingui() - - const plcDirectory = usePlcDirectory() - const [url, setUrl] = useState(plcDirectory ?? '') - const setPlcDirectory = useSetPlcDirectory() - - const submit = () => { - const trimmedUrl = url.trim() - if (!trimmedUrl) { - control.close(() => { - setPlcDirectory(undefined) - }) - return - } - - control.close(() => { - try { - setPlcDirectory(new URL(trimmedUrl).origin) - } catch { - setPlcDirectory(trimmedUrl) - } - }) - } - - const isReset = url.trim().length === 0 - - const shouldDisable = () => { - if (isReset) return false - - try { - const nextUrl = new URL(url) - return nextUrl.protocol !== 'https:' && nextUrl.protocol !== 'http:' - } catch { - return true - } - } - - return ( - setUrl(plcDirectory ?? '')}> - - - - - PLC Directory URL - - - - - { - setUrl(value) - }} - placeholder={persisted.defaults.plcDirectory} - placeholderTextColor={pal.colors.textLight} - onSubmitEditing={submit} - accessibilityHint={_( - msg`Input the URL of the PLC directory to use`, - )} - defaultValue={plcDirectory} - /> - - - - - - - - - - ) -} -function PostReplacementDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_, i18n} = useLingui() - - const postReplacement = usePostReplacement() - const setPostReplacement = useSetPostReplacement() - - const [singular, setSingular] = useState(postReplacement.postName) - const [plural, setPlural] = useState(postReplacement.postsName) - const [pluralManuallyEdited, setPluralManuallyEdited] = useState(false) - - const submit = async () => { - setPostReplacement({ - enabled: singular.trim().toLowerCase() !== 'post', - postName: singular, - postsName: plural, - }) - - // Force reload the i18n messages to apply the replacement immediately - const locale = i18n.locale - await (IS_WEB - ? dynamicActivateWeb(locale as AppLanguage) - : dynamicActivate(locale as AppLanguage)) - - control.close() - } - - const handleSingularChange = (value: string) => { - setSingular(value) - if (!pluralManuallyEdited) { - setPlural(value + 's') - } - } - - const handlePluralChange = (value: string) => { - setPlural(value) - setPluralManuallyEdited(true) - } - - const handlePresetSelect = (singularForm: string, pluralForm: string) => { - setSingular(singularForm) - setPlural(pluralForm) - setPluralManuallyEdited(false) - } - - const shouldDisable = () => { - return !singular.trim() || !plural.trim() - } - - return ( - { - setSingular(postReplacement.postName) - setPlural(postReplacement.postsName) - setPluralManuallyEdited(false) - }}> - - - - - Custom post phrase - - - - - - - - {[ - {singular: 'post', plural: 'posts'}, - {singular: 'skeet', plural: 'skeets'}, - {singular: 'note', plural: 'notes'}, - {singular: 'woot', plural: 'woots'}, - {singular: 'toot', plural: 'toots'}, - {singular: 'silly', plural: 'sillies'}, - ].map(preset => ( - - ))} - - - - - - - - - - - - - ) -} - -function TrustedVerifiersDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const {_} = useLingui() - - return ( - - - - - - Trusted Verifiers - - - - - - - - - ) -} - -const TrustedVerifiers = (): React.ReactNode => { - const trusted = useDeerVerificationTrusted() - const moderationOpts = useModerationOpts() - - const results = useProfilesQuery({ - handles: Array.from(trusted), - }) - - const {gtMobile} = useBreakpoints() - - return ( - results.data && - moderationOpts !== undefined && ( - - {results.data.profiles.map(profile => ( - - ))} - - ) - ) -} - -function OpenRouterApiKeyDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const apiKey = useOpenRouterApiKey() - const [value, setValue] = useState(apiKey ?? '') - const setApiKey = useSetOpenRouterApiKey() - - const submit = () => { - setApiKey(value.trim() || undefined) - control.close() - } - - return ( - setValue(apiKey ?? '')}> - - - - - OpenRouter API Key - - - - - - - - - - - - - - - ) -} - -function OpenRouterModelDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const model = useOpenRouterModel() - const [value, setValue] = useState(model ?? '') - const setModel = useSetOpenRouterModel() - - const submit = () => { - setModel(value.trim() || undefined) - control.close() - } - - return ( - setValue(model ?? '')}> - - - - - OpenRouter Model - - - - - - - - - - - - - - - ) -} - -function OpenRouterPromptDialog({ - control, -}: { - control: Dialog.DialogControlProps -}) { - const pal = usePalette('default') - const {_} = useLingui() - - const prompt = useOpenRouterPrompt() - const [value, setValue] = useState(prompt ?? '') - const setPrompt = useSetOpenRouterPrompt() - - const submit = () => { - setPrompt(value.trim() || undefined) - control.close() - } - - return ( - setValue(prompt ?? '')}> - - - - - Alt Text Prompt - - - - - - - - - - - - - - - ) -} - -export function RunesSettingsScreen({}: Props) { - const {_} = useLingui() - - const goLinksEnabled = useGoLinksEnabled() - const setGoLinksEnabled = useSetGoLinksEnabled() - - const directFetchRecords = useDirectFetchRecords() - const setDirectFetchRecords = useSetDirectFetchRecords() - - const showExternalShareButtons = useShowExternalShareButtons() - const setShowExternalShareButtons = useSetShowExternalShareButtons() - - const noAppLabelers = useNoAppLabelers() - const setNoAppLabelers = useSetNoAppLabelers() - - const noDiscoverFallback = useNoDiscoverFallback() - const setNoDiscoverFallback = useSetNoDiscoverFallback() - - const highQualityImages = useHighQualityImages() - const setHighQualityImages = useSetHighQualityImages() - const imageCdnHost = useImageCdnHost() - const plcDirectory = usePlcDirectory() - - const hideFeedsPromoTab = useHideFeedsPromoTab() - const setHideFeedsPromoTab = useSetHideFeedsPromoTab() - - const disableViaRepostNotification = useDisableViaRepostNotification() - const setDisableViaRepostNotification = useSetDisableViaRepostNotification() - - const disableComposerPrompt = useDisableComposerPrompt() - const setDisableComposerPrompt = useSetDisableComposerPrompt() - - const discoverContextEnabled = useDiscoverContextEnabled() - const setDiscoverContextEnabled = useSetDiscoverContextEnabled() - - const disableLikesMetrics = useDisableLikesMetrics() - const setDisableLikesMetrics = useSetDisableLikesMetrics() - - const disableRepostsMetrics = useDisableRepostsMetrics() - const setDisableRepostsMetrics = useSetDisableRepostsMetrics() - - const disableQuotesMetrics = useDisableQuotesMetrics() - const setDisableQuotesMetrics = useSetDisableQuotesMetrics() - - const disableSavesMetrics = useDisableSavesMetrics() - const setDisableSavesMetrics = useSetDisableSavesMetrics() - - const disableReplyMetrics = useDisableReplyMetrics() - const setDisableReplyMetrics = useSetDisableReplyMetrics() - - const disableFollowersMetrics = useDisableFollowersMetrics() - const setDisableFollowersMetrics = useSetDisableFollowersMetrics() - - const disableFollowingMetrics = useDisableFollowingMetrics() - const setDisableFollowingMetrics = useSetDisableFollowingMetrics() - - const disableFollowedByMetrics = useDisableFollowedByMetrics() - const setDisableFollowedByMetrics = useSetDisableFollowedByMetrics() - - const disablePostsMetrics = useDisablePostsMetrics() - const setDisablePostsMetrics = useSetDisablePostsMetrics() - - const hideSimilarAccountsRecomm = useHideSimilarAccountsRecomm() - const setHideSimilarAccountsRecomm = useSetHideSimilarAccountsRecomm() - - const hideUnreplyablePosts = useHideUnreplyablePosts() - const setHideUnreplyablePosts = useSetHideUnreplyablePosts() - - const disableVerifyEmailReminder = useDisableVerifyEmailReminder() - const setDisableVerifyEmailReminder = useSetDisableVerifyEmailReminder() - - const constellationInstance = useConstellationInstance() - const setConstellationInstanceControl = Dialog.useDialogControl() - - const setTrustedVerifiersDialogControl = Dialog.useDialogControl() - - const deerVerificationEnabled = useDeerVerificationEnabled() - const setDeerVerificationEnabled = useSetDeerVerificationEnabled() - - const pdsLabelEnabled = usePdsLabelEnabled() - const setPdsLabelEnabled = useSetPdsLabelEnabled() - const pdsLabelHideBskyPds = usePdsLabelHideBskyPds() - const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds() - - const repostCarouselEnabled = useRepostCarouselEnabled() - const setRepostCarouselEnabled = useSetRepostCarouselEnabled() - - const showFollowsYouBadge = useShowFollowsYouBadge() - const setShowFollowsYouBadge = useSetShowFollowsYouBadge() - - const showLinkInHandle = useShowLinkInHandle() - const setShowLinkInHandle = useSetShowLinkInHandle() - const showLinkInHandleOnlyOnWorkingLinks = - useShowLinkInHandleOnlyOnWorkingLinks() - const setShowLinkInHandleOnlyOnWorkingLinks = - useSetShowLinkInHandleOnlyOnWorkingLinks() - - const handleInLinks = useHandleInLinks() - const setHandleInLinks = useSetHandleInLinks() - - const translationServicePreference = useTranslationServicePreference() - const setTranslationServicePreference = useSetTranslationServicePreference() - - const setLibreTranslateInstanceControl = Dialog.useDialogControl() - - const setImageCdnHostControl = Dialog.useDialogControl() - - const setPlcDirectoryControl = Dialog.useDialogControl() - - const setPostReplacementDialogControl = Dialog.useDialogControl() - - const setOpenRouterApiKeyControl = Dialog.useDialogControl() - const openRouterModel = useOpenRouterModel() - const setOpenRouterModelControl = Dialog.useDialogControl() - const setOpenRouterPromptControl = Dialog.useDialogControl() - const openRouterConfigured = useOpenRouterConfigured() - - const autoLikeOnRepost = useAutoLikeOnRepost() - const setAutoLikeOnRepost = useSetAutoLikeOnRepost() - - const [customAppViewDid] = useCustomAppViewDid() - const setCustomAppViewDidControl = Dialog.useDialogControl() - - const setFaviconServiceControl = Dialog.useDialogControl() - - return ( - - - - - - Runes - - - - - - - - - - Redirects - - setGoLinksEnabled(value)} - style={[a.w_full]}> - - Redirect through go.bsky.app - - - - setHandleInLinks(value)} - style={[a.w_full]}> - - Use handles in profile links instead of DIDs - - - - - - - - - Visibility - - setDirectFetchRecords(value)} - style={[a.w_full]}> - - - Fetch records directly from PDS to see contents of blocked and - detached quotes - - - - - - - - - - Bridging and Fediverse - - setShowExternalShareButtons(value)} - style={[a.w_full]}> - - - Show "Open original post" and "Open post in PDSls" buttons - - - - - - - - - - Verification - - setDeerVerificationEnabled(value)} - style={[a.w_full]}> - - - Select your own set of trusted verifiers, and operate as a - verifier - - - - - - - - - - May slow down the client or fail to find all labels. Revoke and - grant trust in the meatball menu on a profile.{' '} - {deerVerificationEnabled - ? 'You currently' - : 'If enabled, you would'}{' '} - trust the following verifiers: - - - - - - - - {`Trusted Verifiers`} - - setTrustedVerifiersDialogControl.open()} - /> - - - - - - {`Constellation Instance`} - - setConstellationInstanceControl.open()} - /> - - - - - Constellation is used to supplement AppView responses for custom - verifications and nuclear block bypass, via backlinks. Current - instance:\u00A0 - - {constellationInstance} - - - - - - - - - - - {`Custom post phrase`} - - setPostReplacementDialogControl.open()} - /> - - - - - - Tweaks - - setPdsLabelEnabled(value)} - style={[a.w_full]}> - - - Show a PDS badge next to the display name on profiles - - - - - {pdsLabelEnabled && ( - setPdsLabelHideBskyPds(value)} - style={[a.w_full]}> - - Hide PDS badge for Bluesky-hosted accounts - - - - )} - - setRepostCarouselEnabled(value)} - style={[a.w_full]}> - - Combine reposts into a horizontal carousel - - - - - setShowLinkInHandle(value)} - style={[a.w_full]}> - - - On non-bsky.social handles, show a link to that URL - - - - - {showLinkInHandle && ( - setShowLinkInHandleOnlyOnWorkingLinks(value)} - style={[a.w_full]}> - - Only show URL on handles with working links - - - - )} - - setNoDiscoverFallback(value)} - style={[a.w_full]}> - - Do not fall back to discover feed - - - - - setHighQualityImages(value)} - style={[a.w_full]}> - - Display images in higher quality - - - - - - Images will be served as PNG instead of WEBP. Images will take - longer to load and use more bandwidth. - - - setAutoLikeOnRepost(value)} - style={[a.w_full]}> - - Auto-like what you repost - - - - setHideFeedsPromoTab(value)} - style={[a.w_full]}> - - - Hide "Feeds ✨" tab when only one feed is selected - - - - - - setDisableViaRepostNotification(value)} - style={[a.w_full]}> - - Disable via repost notifications - - - - - - Forcefully disables the notifications other people receive when - you like/repost a post someone else has reposted for privacy. - - - - setHideSimilarAccountsRecomm(value)} - style={[a.w_full]}> - - Hide similar accounts recommendations - - - - - setHideUnreplyablePosts(value)} - style={[a.w_full]}> - - Hide posts that cannot be replied to from feeds - - - - - - Hides posts from feeds where replies are disabled (e.g. due to - postgates or other restrictions). Does not affect thread views. - - - - setDisableComposerPrompt(value)} - style={[a.w_full]}> - - Disable composer prompt - - - - - setDisableVerifyEmailReminder(value)} - style={[a.w_full]}> - - Disable verify email reminder - - - - - - This only gets rid of the reminder on app launch, useful if your - PDS does not have email verification setup.\u00A0 This does NOT - give access to features locked behind email verification. - - - - setDiscoverContextEnabled(value)} - style={[a.w_full]}> - - Show debug context for posts in Discover feed - - - - - - {pdsLabelEnabled && ( - - - - Favicon service - - setFaviconServiceControl.open()} - /> - - )} - - - - - - - Post Translation Provider - - - setTranslationServicePreference('google')} - style={[a.w_full]}> - - Use Google Translate - - - - - setTranslationServicePreference('kagi')} - style={[a.w_full]}> - - Use Kagi Translate - - - - - setTranslationServicePreference('papago')} - style={[a.w_full]}> - - Use Naver Papago - - - - - setTranslationServicePreference('libreTranslate')} - style={[a.w_full]}> - - Use LibreTranslate - - - - - - {translationServicePreference === 'libreTranslate' && ( - - - - {`LibreTranslate Instance`} - - setLibreTranslateInstanceControl.open()} - /> - - )} - - - - - - - OpenRouter API Key - - setOpenRouterApiKeyControl.open()} - /> - - - - - - Set your OpenRouter API key to enable AI-powered alt text - generation for images in the composer. Get an API key at{' '} - - openrouter.ai - - - - - - {openRouterConfigured && ( - - - - {`OpenRouter Model`} - - setOpenRouterModelControl.open()} - /> - - )} - - {openRouterConfigured && ( - - - - Current model: {openRouterModel ?? DEFAULT_ALT_TEXT_AI_MODEL}.{' '} - - Search models - - - - - )} - - {openRouterConfigured && ( - - - - Alt Text Prompt - - setOpenRouterPromptControl.open()} - /> - - )} - - {openRouterConfigured && ( - - - - Customize the prompt sent to the AI model when generating alt - text. Leave empty to use the default prompt. - - - - )} - - - - - - - Metrics - - - setDisableLikesMetrics(value)} - style={[a.w_full]}> - - Disable likes metrics - - - - - setDisableRepostsMetrics(value)} - style={[a.w_full]}> - - Disable reposts metrics - - - - - setDisableQuotesMetrics(value)} - style={[a.w_full]}> - - Disable quotes metrics - - - - - setDisableSavesMetrics(value)} - style={[a.w_full]}> - - Disable saves metrics - - - - - setDisableReplyMetrics(value)} - style={[a.w_full]}> - - Disable reply metrics - - - - - setDisableFollowersMetrics(value)} - style={[a.w_full]}> - - Disable followers metrics - - - - - setDisableFollowingMetrics(value)} - style={[a.w_full]}> - - Disable following metrics - - - - - setDisableFollowedByMetrics(value)} - style={[a.w_full]}> - - Disable "followed by" metrics - - - - - setShowFollowsYouBadge(value)} - style={[a.w_full]}> - - Show "Follows you" badge - - - - - setDisablePostsMetrics(value)} - style={[a.w_full]}> - - Disable post counts metrics - - - - - - - - - - - {`Image CDN`} - - setImageCdnHostControl.open()} - /> - - - - - Override the CDN host for all images. Current:  - - {imageCdnHost} - - - - - - - - - {`PLC Directory`} - - setPlcDirectoryControl.open()} - /> - - - - - Override the PLC directory used to resolve DIDs. Current:  - - {plcDirectory} - - - - - - - - - - - {`Custom AppView DID`} - - setCustomAppViewDidControl.open()} - /> - - - - - - - - Labelers - - setNoAppLabelers(value)} - style={[a.w_full]}> - - Do not declare any default app labelers - - - - - - - - Restart the app after changing this setting. - - - - - - Some App Views will default to using an app labeler if you have - no labelers, so consider subscribing to at least one labeler if - you have issues. - - - - - - - App labelers are mandatory top-level labelers that can perform - "takedowns". This setting does not influence geolocation-based - labelers. - - - - - - - - - - - - - - - - - - ) -} - -const styles = { - textInput: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 14, - paddingVertical: 10, - fontSize: 16, - }, -} diff --git a/src/screens/Settings/RunesSettings/BadgesSettings.tsx b/src/screens/Settings/RunesSettings/BadgesSettings.tsx new file mode 100644 index 000000000..bee2ea366 --- /dev/null +++ b/src/screens/Settings/RunesSettings/BadgesSettings.tsx @@ -0,0 +1,303 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' +import {Trans, useLingui} from '@lingui/react/macro' + +import {usePalette} from '#/lib/hooks/usePalette' +import * as persisted from '#/state/persisted' +import { + useDeerVerificationEnabled, + useDeerVerificationTrusted, + useSetDeerVerificationEnabled, +} from '#/state/preferences/deer-verification' +import { + useFaviconService, + useSetFaviconService, +} from '#/state/preferences/favicon-service' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import { + usePdsLabelEnabled, + usePdsLabelHideBskyPds, + useSetPdsLabelEnabled, + useSetPdsLabelHideBskyPds, +} from '#/state/preferences/pds-label' +import {useProfilesQuery} from '#/state/queries/profile' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a, useBreakpoints} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' +import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' +import {Text} from '#/components/Typography' +import {IS_WEB} from '#/env' +import {SearchProfileCard} from '../../Search/components/SearchProfileCard' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesBadgesSettingsScreen() { + const {t: l} = useLingui() + + const deerVerificationEnabled = useDeerVerificationEnabled() + const setDeerVerificationEnabled = useSetDeerVerificationEnabled() + const setTrustedVerifiersDialogControl = Dialog.useDialogControl() + + const pdsLabelEnabled = usePdsLabelEnabled() + const setPdsLabelEnabled = useSetPdsLabelEnabled() + const pdsLabelHideBskyPds = usePdsLabelHideBskyPds() + const setPdsLabelHideBskyPds = useSetPdsLabelHideBskyPds() + const setFaviconServiceControl = Dialog.useDialogControl() + + return ( + + + + + Verification + + setDeerVerificationEnabled(value)} + style={[a.w_full]}> + + + Select your own set of trusted verifiers, and operate as a + verifier + + + + + + + + + + May slow down the client or fail to find all labels. Revoke and + grant trust in the meatball menu on a profile.{' '} + {deerVerificationEnabled + ? 'You currently' + : 'If enabled, you would'}{' '} + trust the following verifiers: + + + + + + + + {`Trusted Verifiers`} + + setTrustedVerifiersDialogControl.open()} + /> + + + + + + + + PDS badges + + setPdsLabelEnabled(value)} + style={[a.w_full]}> + + Show a PDS badge next to the display name on profiles + + + + {pdsLabelEnabled && ( + setPdsLabelHideBskyPds(value)} + style={[a.w_full]}> + + Hide PDS badge for Bluesky-hosted accounts + + + + )} + + + {pdsLabelEnabled && ( + + + + Favicon service + + setFaviconServiceControl.open()} + /> + + )} + + + + + ) +} + +function FaviconServiceDialog({control}: {control: Dialog.DialogControlProps}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const faviconService = useFaviconService() + const [url, setUrl] = useState(faviconService ?? '') + const [inputVersion, setInputVersion] = useState(0) + const setFaviconService = useSetFaviconService() + + const updateInputValue = (nextUrl: string) => { + setUrl(nextUrl) + setInputVersion(version => version + 1) + } + + const presets = [ + 'https://twenty-icons.com/(pds)', + 'https://favicon.im/(pds)?larger=true&throw-error-on-404=true', + 'https://favicon.blueat.net/(pds)?larger=true&throw-error-on-404=true', + ] + + return ( + updateInputValue(faviconService ?? '')}> + + + + + Favicon Service URL + + + + (pds) is replaced with the domain of an account's host. + + + + + + { + setFaviconService(url.trim()) + control.close() + }} + accessibilityHint={l`Enter the favicon service URL with (pds) as placeholder`} + defaultValue={url} + /> + + + {presets.map(preset => ( + + ))} + + + + + + + + + + + ) +} + +function TrustedVerifiersDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const {t: l} = useLingui() + + return ( + + + + + + Trusted Verifiers + + + + + + + + + ) +} + +function TrustedVerifiers() { + const trusted = useDeerVerificationTrusted() + const moderationOpts = useModerationOpts() + const {gtMobile} = useBreakpoints() + + const results = useProfilesQuery({ + handles: Array.from(trusted), + }) + + if (!results.data || moderationOpts === undefined) { + return null + } + + return ( + + {results.data.profiles.map(profile => ( + + ))} + + ) +} + +const styles = { + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, +} diff --git a/src/screens/Settings/RunesSettings/DisplaySettings.tsx b/src/screens/Settings/RunesSettings/DisplaySettings.tsx new file mode 100644 index 000000000..7d211419d --- /dev/null +++ b/src/screens/Settings/RunesSettings/DisplaySettings.tsx @@ -0,0 +1,231 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {Trans, useLingui} from '@lingui/react/macro' + +import {usePalette} from '#/lib/hooks/usePalette' +import {dynamicActivate} from '#/locale/i18n' +import {dynamicActivate as dynamicActivateWeb} from '#/locale/i18n.web' +import {type AppLanguage} from '#/locale/languages' +import { + useHighQualityImages, + useSetHighQualityImages, +} from '#/state/preferences/high-quality-images' +import { + usePostReplacement, + useSetPostReplacement, +} from '#/state/preferences/post-name-replacement' +import { + useRepostCarouselEnabled, + useSetRepostCarouselEnabled, +} from '#/state/preferences/repost-carousel-enabled' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {Image_Stroke2_Corner0_Rounded as ImageIcon} from '#/components/icons/Image' +import {Pencil_Stroke2_Corner0_Rounded as PencilIcon} from '#/components/icons/Pencil' +import {Repost_Stroke2_Corner3_Rounded as RepostIcon} from '#/components/icons/Repost' +import {Text} from '#/components/Typography' +import {IS_WEB} from '#/env' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesDisplaySettingsScreen() { + const {t: l} = useLingui() + + const repostCarouselEnabled = useRepostCarouselEnabled() + const setRepostCarouselEnabled = useSetRepostCarouselEnabled() + + const highQualityImages = useHighQualityImages() + const setHighQualityImages = useSetHighQualityImages() + + const setPostReplacementDialogControl = Dialog.useDialogControl() + + return ( + + setRepostCarouselEnabled(value)}> + + + + Combine reposts into a horizontal carousel + + + + + setHighQualityImages(value)}> + + + + Display images in higher quality + + + + + + + + Images will be served as PNG instead of WEBP. Images will take + longer to load and use more bandwidth. + + + + + + + {`Custom post phrase`} + + setPostReplacementDialogControl.open()} + /> + + + + ) +} + +function PostReplacementDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l, i18n} = useLingui() + + const postReplacement = usePostReplacement() + const setPostReplacement = useSetPostReplacement() + + const [singular, setSingular] = useState(postReplacement.postName) + const [plural, setPlural] = useState(postReplacement.postsName) + const [pluralManuallyEdited, setPluralManuallyEdited] = useState(false) + + const submit = async () => { + setPostReplacement({ + enabled: singular.trim().toLowerCase() !== 'post', + postName: singular, + postsName: plural, + }) + + const locale = i18n.locale + await (IS_WEB + ? dynamicActivateWeb(locale as AppLanguage) + : dynamicActivate(locale as AppLanguage)) + + control.close() + } + + const handleSingularChange = (value: string) => { + setSingular(value) + if (!pluralManuallyEdited) { + setPlural(`${value}s`) + } + } + + const handlePluralChange = (value: string) => { + setPlural(value) + setPluralManuallyEdited(true) + } + + const handlePresetSelect = (singularForm: string, pluralForm: string) => { + setSingular(singularForm) + setPlural(pluralForm) + setPluralManuallyEdited(false) + } + + return ( + { + setSingular(postReplacement.postName) + setPlural(postReplacement.postsName) + setPluralManuallyEdited(false) + }}> + + + + Custom post phrase + + + + + + {[ + {singular: 'post', plural: 'posts'}, + {singular: 'skeet', plural: 'skeets'}, + {singular: 'note', plural: 'notes'}, + {singular: 'woot', plural: 'woots'}, + {singular: 'toot', plural: 'toots'}, + {singular: 'silly', plural: 'sillies'}, + ].map(preset => ( + + ))} + + + + + + + + + + + + ) +} + +const styles = { + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, +} diff --git a/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx b/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx new file mode 100644 index 000000000..7a90adac0 --- /dev/null +++ b/src/screens/Settings/RunesSettings/ImpressionsSettings.tsx @@ -0,0 +1,199 @@ +import {Trans, useLingui} from '@lingui/react/macro' + +import { + useDisableFollowedByMetrics, + useSetDisableFollowedByMetrics, +} from '#/state/preferences/disable-followed-by-metrics' +import { + useDisableFollowersMetrics, + useSetDisableFollowersMetrics, +} from '#/state/preferences/disable-followers-metrics' +import { + useDisableFollowingMetrics, + useSetDisableFollowingMetrics, +} from '#/state/preferences/disable-following-metrics' +import { + useDisableLikesMetrics, + useSetDisableLikesMetrics, +} from '#/state/preferences/disable-likes-metrics' +import { + useDisablePostsMetrics, + useSetDisablePostsMetrics, +} from '#/state/preferences/disable-posts-metrics' +import { + useDisableQuotesMetrics, + useSetDisableQuotesMetrics, +} from '#/state/preferences/disable-quotes-metrics' +import { + useDisableReplyMetrics, + useSetDisableReplyMetrics, +} from '#/state/preferences/disable-reply-metrics' +import { + useDisableRepostsMetrics, + useSetDisableRepostsMetrics, +} from '#/state/preferences/disable-reposts-metrics' +import { + useDisableSavesMetrics, + useSetDisableSavesMetrics, +} from '#/state/preferences/disable-saves-metrics' +import { + useSetShowFollowsYouBadge, + useShowFollowsYouBadge, +} from '#/state/preferences/show-follows-you-badge' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesImpressionsSettingsScreen() { + const {t: l} = useLingui() + + const disableLikesMetrics = useDisableLikesMetrics() + const setDisableLikesMetrics = useSetDisableLikesMetrics() + const disableRepostsMetrics = useDisableRepostsMetrics() + const setDisableRepostsMetrics = useSetDisableRepostsMetrics() + const disableQuotesMetrics = useDisableQuotesMetrics() + const setDisableQuotesMetrics = useSetDisableQuotesMetrics() + const disableSavesMetrics = useDisableSavesMetrics() + const setDisableSavesMetrics = useSetDisableSavesMetrics() + const disableReplyMetrics = useDisableReplyMetrics() + const setDisableReplyMetrics = useSetDisableReplyMetrics() + const disableFollowersMetrics = useDisableFollowersMetrics() + const setDisableFollowersMetrics = useSetDisableFollowersMetrics() + const disableFollowingMetrics = useDisableFollowingMetrics() + const setDisableFollowingMetrics = useSetDisableFollowingMetrics() + const disableFollowedByMetrics = useDisableFollowedByMetrics() + const setDisableFollowedByMetrics = useSetDisableFollowedByMetrics() + const disablePostsMetrics = useDisablePostsMetrics() + const setDisablePostsMetrics = useSetDisablePostsMetrics() + const showFollowsYouBadge = useShowFollowsYouBadge() + const setShowFollowsYouBadge = useSetShowFollowsYouBadge() + + return ( + + + + + Posts + + setDisableLikesMetrics(value)} + style={[a.w_full]}> + + Remove likes counts + + + + setDisableRepostsMetrics(value)} + style={[a.w_full]}> + + Remove reposts counts + + + + setDisableQuotesMetrics(value)} + style={[a.w_full]}> + + Remove quotes counts + + + + setDisableSavesMetrics(value)} + style={[a.w_full]}> + + Remove saves counts + + + + setDisableReplyMetrics(value)} + style={[a.w_full]}> + + Remove reply counts + + + + + + + + Profiles + + setDisableFollowersMetrics(value)} + style={[a.w_full]}> + + Remove followers counts + + + + setDisableFollowingMetrics(value)} + style={[a.w_full]}> + + Remove following counts + + + + setDisableFollowedByMetrics(value)} + style={[a.w_full]}> + + Remove "followed by" avatars + + + + setShowFollowsYouBadge(value)} + style={[a.w_full]}> + + Show "Follows you" badge + + + + setDisablePostsMetrics(value)} + style={[a.w_full]}> + + Remove post counts + + + + + + ) +} diff --git a/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx b/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx new file mode 100644 index 000000000..ec2706e03 --- /dev/null +++ b/src/screens/Settings/RunesSettings/InfrastructureSettings.tsx @@ -0,0 +1,702 @@ +import {useState} from 'react' +import {View} from 'react-native' +import {isDid} from '@atproto/api' +import {Trans, useLingui} from '@lingui/react/macro' + +import {APPVIEW_DID_PROXY} from '#/lib/constants' +import {usePalette} from '#/lib/hooks/usePalette' +import * as persisted from '#/state/persisted' +import { + useConstellationInstance, + useSetConstellationInstance, +} from '#/state/preferences/constellation-instance' +import { + useCustomAppViewDid, + useSetCustomAppViewDid, +} from '#/state/preferences/custom-appview-did' +import { + useImageCdnHost, + useSetImageCdnHost, +} from '#/state/preferences/image-cdn-host' +import { + useNoAppLabelers, + useSetNoAppLabelers, +} from '#/state/preferences/no-app-labelers' +import { + usePlcDirectory, + useSetPlcDirectory, +} from '#/state/preferences/plc-directory' +import { + useLibreTranslateInstance, + useSetLibreTranslateInstance, + useSetTranslationServicePreference, + useTranslationServicePreference, +} from '#/state/preferences/translation-service-preference' +import {findService, useDidDocument} from '#/state/queries/resolve-identity' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as Toggle from '#/components/forms/Toggle' +import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' +import {RaisingHand4Finger_Stroke2_Corner0_Rounded as RaisingHandIcon} from '#/components/icons/RaisingHand' +import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' +import {InlineLinkText} from '#/components/Link' +import {Text} from '#/components/Typography' +import {IS_WEB} from '#/env' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesInfrastructureSettingsScreen() { + const {t: l} = useLingui() + + const translationServicePreference = useTranslationServicePreference() + const setTranslationServicePreference = useSetTranslationServicePreference() + const setLibreTranslateInstanceControl = Dialog.useDialogControl() + + const imageCdnHost = useImageCdnHost() + const setImageCdnHostControl = Dialog.useDialogControl() + + const plcDirectory = usePlcDirectory() + const setPlcDirectoryControl = Dialog.useDialogControl() + + const constellationInstance = useConstellationInstance() + const setConstellationInstanceControl = Dialog.useDialogControl() + + const [customAppViewDid] = useCustomAppViewDid() + const setCustomAppViewDidControl = Dialog.useDialogControl() + + const noAppLabelers = useNoAppLabelers() + const setNoAppLabelers = useSetNoAppLabelers() + + return ( + + + + + Post Translation Provider + + setTranslationServicePreference('google')} + style={[a.w_full]}> + + Use Google Translate + + + + setTranslationServicePreference('kagi')} + style={[a.w_full]}> + + Use Kagi Translate + + + + setTranslationServicePreference('papago')} + style={[a.w_full]}> + + Use Naver Papago + + + + setTranslationServicePreference('libreTranslate')} + style={[a.w_full]}> + + Use LibreTranslate + + + + + + {translationServicePreference === 'libreTranslate' && ( + + + + {`LibreTranslate Instance`} + + setLibreTranslateInstanceControl.open()} + /> + + )} + + + + + + + {`Image CDN`} + + setImageCdnHostControl.open()} + /> + + + + + Override the CDN host for all images. Current:  + + {imageCdnHost} + + + + + + + + + {`PLC Directory`} + + setPlcDirectoryControl.open()} + /> + + + + + Override the PLC directory used to resolve DIDs. Current:  + + {plcDirectory} + + + + + + + + + {`Constellation Instance`} + + setConstellationInstanceControl.open()} + /> + + + + + Constellation is used to supplement AppView responses for custom + verifications and nuclear block bypass, via backlinks. Current + instance:  + + {constellationInstance} + + + + + + + + + + + {`Custom AppView DID`} + + setCustomAppViewDidControl.open()} + /> + + + + + setNoAppLabelers(value)}> + + + + Do not declare any default app labelers + + + + + + + + Restart the app after changing this setting. + + + + + + Some App Views will default to using an app labeler if you have no + labelers, so consider subscribing to at least one labeler if you + have issues. + + + + + + + App labelers are mandatory top-level labelers that can perform + "takedowns". This setting does not influence geolocation-based + labelers. + + + + + + + + + + + ) +} + +function ConstellationInstanceDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const constellationInstance = useConstellationInstance() + const [url, setUrl] = useState(constellationInstance ?? '') + const setConstellationInstance = useSetConstellationInstance() + + const submit = () => { + setConstellationInstance(url) + control.close() + } + + return ( + setUrl(constellationInstance ?? '')}> + + + + + Constellations instance URL + + + + + + + + + + + + + + + ) +} + +function CustomAppViewDidDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const [customAppViewDid] = useCustomAppViewDid() + const [did, setDid] = useState(customAppViewDid ?? '') + const setCustomAppViewDid = useSetCustomAppViewDid() + + const doc = useDidDocument({did}) + const bskyAppViewService = + doc.data && findService(doc.data, '#bsky_appview', 'BskyAppView') + + const submit = () => { + if (did.length === 0) { + control.close(() => { + setCustomAppViewDid(undefined) + }) + return + } + if (!bskyAppViewService?.serviceEndpoint) return + control.close(() => { + setCustomAppViewDid(did) + }) + } + + return ( + setDid(customAppViewDid ?? '')}> + + + + + Custom AppView Proxy DID + + + + + + + {did && !isDid(did) && ( + + + + )} + + {did && (did.includes('#') || did.includes('?')) && ( + + + + )} + + {doc.isError && ( + + + + )} + + {doc.data && + !bskyAppViewService && + (doc.data as {message?: string}).message && ( + + + + )} + + {doc.data && !bskyAppViewService && ( + + + + )} + + {bskyAppViewService && ( + + {JSON.stringify(bskyAppViewService, null, 2)} + + )} + + + + + + + + + + ) +} + +function LibreTranslateInstanceDialog({ + control, +}: { + control: Dialog.DialogControlProps +}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const libreTranslateInstance = useLibreTranslateInstance() + const [url, setUrl] = useState(libreTranslateInstance ?? '') + const setLibreTranslateInstance = useSetLibreTranslateInstance() + + return ( + setUrl(libreTranslateInstance ?? '')}> + + + + + LibreTranslate instance URL + + + + + { + setLibreTranslateInstance(url) + control.close() + }} + accessibilityHint={l`Input the url of the LibreTranslate instance to use`} + defaultValue={libreTranslateInstance} + /> + + + + + + + + + + ) +} + +function ImageCdnHostDialog({control}: {control: Dialog.DialogControlProps}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const imageCdnHost = useImageCdnHost() + const [url, setUrl] = useState(imageCdnHost ?? '') + const setImageCdnHost = useSetImageCdnHost() + const isReset = url.trim().length === 0 + + const submit = () => { + const trimmedUrl = url.trim() + if (!trimmedUrl) { + control.close(() => { + setImageCdnHost(undefined) + }) + return + } + + control.close(() => { + try { + setImageCdnHost(new URL(trimmedUrl).origin) + } catch { + setImageCdnHost(trimmedUrl) + } + }) + } + + return ( + setUrl(imageCdnHost ?? '')}> + + + + + Image CDN URL + + + + + + + + + + + + + + + ) +} + +function PlcDirectoryDialog({control}: {control: Dialog.DialogControlProps}) { + const pal = usePalette('default') + const {t: l} = useLingui() + + const plcDirectory = usePlcDirectory() + const [url, setUrl] = useState(plcDirectory ?? '') + const setPlcDirectory = useSetPlcDirectory() + const isReset = url.trim().length === 0 + + const submit = () => { + const trimmedUrl = url.trim() + if (!trimmedUrl) { + control.close(() => { + setPlcDirectory(undefined) + }) + return + } + + control.close(() => { + try { + setPlcDirectory(new URL(trimmedUrl).origin) + } catch { + setPlcDirectory(trimmedUrl) + } + }) + } + + return ( + setUrl(plcDirectory ?? '')}> + + + + + PLC Directory URL + + + + + + + + + + + + + + + ) +} + +function isValidHostnameUrl(url: string) { + try { + return new URL(url).hostname.includes('.') + } catch { + return false + } +} + +function isValidPlcDirectoryUrl(url: string) { + try { + const nextUrl = new URL(url) + return nextUrl.protocol === 'https:' || nextUrl.protocol === 'http:' + } catch { + return false + } +} + +const styles = { + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 14, + paddingVertical: 10, + fontSize: 16, + }, +} diff --git a/src/screens/Settings/RunesSettings/MenusSettings.tsx b/src/screens/Settings/RunesSettings/MenusSettings.tsx new file mode 100644 index 000000000..ed16933c8 --- /dev/null +++ b/src/screens/Settings/RunesSettings/MenusSettings.tsx @@ -0,0 +1,102 @@ +import {Trans, useLingui} from '@lingui/react/macro' + +import { + useSetShowExternalShareButtons, + useShowExternalShareButtons, +} from '#/state/preferences/external-share-buttons' +import { + useSetShowLinkInHandle, + useShowLinkInHandle, +} from '#/state/preferences/show-link-in-handle.tsx' +import { + useSetShowLinkInHandleOnlyOnWorkingLinks, + useShowLinkInHandleOnlyOnWorkingLinks, +} from '#/state/preferences/show-link-in-handle-only-on-working-links' +import { + useHandleInLinks, + useSetHandleInLinks, +} from '#/state/preferences/use-handle-in-links' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import * as Toggle from '#/components/forms/Toggle' +import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight' +import {At_Stroke2_Corner2_Rounded as AtIcon} from '#/components/icons/At' +import {ChainLink_Stroke2_Corner0_Rounded as ChainLinkIcon} from '#/components/icons/ChainLink' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesMenusSettingsScreen() { + const {t: l} = useLingui() + + const handleInLinks = useHandleInLinks() + const setHandleInLinks = useSetHandleInLinks() + + const showExternalShareButtons = useShowExternalShareButtons() + const setShowExternalShareButtons = useSetShowExternalShareButtons() + + const showLinkInHandle = useShowLinkInHandle() + const setShowLinkInHandle = useSetShowLinkInHandle() + const showLinkInHandleOnlyOnWorkingLinks = + useShowLinkInHandleOnlyOnWorkingLinks() + const setShowLinkInHandleOnlyOnWorkingLinks = + useSetShowLinkInHandleOnlyOnWorkingLinks() + + return ( + + setHandleInLinks(value)}> + + + + Use handles in profile links instead of DIDs + + + + + setShowExternalShareButtons(value)}> + + + + + Show "Open original post" and "Open post in PDSls" buttons + + + + + + setShowLinkInHandle(value)}> + + + + On non-bsky.social handles, show a link to that URL + + + + + {showLinkInHandle && ( + setShowLinkInHandleOnlyOnWorkingLinks(value)}> + + + + Only show URL on handles with working links + + + + + )} + + ) +} diff --git a/src/screens/Settings/RunesSettings/OtherAdditionsSettings.tsx b/src/screens/Settings/RunesSettings/OtherAdditionsSettings.tsx new file mode 100644 index 000000000..6338ee191 --- /dev/null +++ b/src/screens/Settings/RunesSettings/OtherAdditionsSettings.tsx @@ -0,0 +1,104 @@ +import {Trans, useLingui} from '@lingui/react/macro' + +import { + useAutoLikeOnRepost, + useSetAutoLikeOnRepost, +} from '#/state/preferences/auto-like-on-repost.tsx' +import { + useDirectFetchRecords, + useSetDirectFetchRecords, +} from '#/state/preferences/direct-fetch-records' +import { + useDisableViaRepostNotification, + useSetDisableViaRepostNotification, +} from '#/state/preferences/disable-via-repost-notification' +import { + useDiscoverContextEnabled, + useSetDiscoverContextEnabled, +} from '#/state/preferences/discover-context-enabled' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import * as Toggle from '#/components/forms/Toggle' +import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesOtherAdditionsSettingsScreen() { + const {t: l} = useLingui() + + const directFetchRecords = useDirectFetchRecords() + const setDirectFetchRecords = useSetDirectFetchRecords() + + const autoLikeOnRepost = useAutoLikeOnRepost() + const setAutoLikeOnRepost = useSetAutoLikeOnRepost() + + const disableViaRepostNotification = useDisableViaRepostNotification() + const setDisableViaRepostNotification = useSetDisableViaRepostNotification() + + const discoverContextEnabled = useDiscoverContextEnabled() + const setDiscoverContextEnabled = useSetDiscoverContextEnabled() + + return ( + + + + + Extras + + setDirectFetchRecords(value)} + style={[a.w_full]}> + + + Fetch records directly from PDS to see contents of blocked and + detached quotes + + + + + setAutoLikeOnRepost(value)} + style={[a.w_full]}> + + Auto-like what you repost + + + + setDisableViaRepostNotification(value)} + style={[a.w_full]}> + + Disable via repost notifications + + + + + + Forcefully disables the notifications other people receive when you + like/repost a post someone else has reposted for privacy. + + + setDiscoverContextEnabled(value)} + style={[a.w_full]}> + + Show debug context for posts in Discover feed + + + + + + ) +} diff --git a/src/screens/Settings/RunesSettings/UsabilitySettings.tsx b/src/screens/Settings/RunesSettings/UsabilitySettings.tsx new file mode 100644 index 000000000..5ee852009 --- /dev/null +++ b/src/screens/Settings/RunesSettings/UsabilitySettings.tsx @@ -0,0 +1,159 @@ +import {Trans, useLingui} from '@lingui/react/macro' + +import {useGoLinksEnabled, useSetGoLinksEnabled} from '#/state/preferences' +import { + useDisableComposerPrompt, + useSetDisableComposerPrompt, +} from '#/state/preferences/disable-composer-prompt' +import { + useDisableVerifyEmailReminder, + useSetDisableVerifyEmailReminder, +} from '#/state/preferences/disable-verify-email-reminder' +import { + useHideFeedsPromoTab, + useSetHideFeedsPromoTab, +} from '#/state/preferences/hide-feeds-promo-tab' +import { + useHideSimilarAccountsRecomm, + useSetHideSimilarAccountsRecomm, +} from '#/state/preferences/hide-similar-accounts-recommendations' +import { + useHideUnreplyablePosts, + useSetHideUnreplyablePosts, +} from '#/state/preferences/hide-unreplyable-posts' +import { + useNoDiscoverFallback, + useSetNoDiscoverFallback, +} from '#/state/preferences/no-discover-fallback' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {atoms as a} from '#/alf' +import {Admonition} from '#/components/Admonition' +import * as Toggle from '#/components/forms/Toggle' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +export function RunesUsabilitySettingsScreen() { + const {t: l} = useLingui() + + const goLinksEnabled = useGoLinksEnabled() + const setGoLinksEnabled = useSetGoLinksEnabled() + + const noDiscoverFallback = useNoDiscoverFallback() + const setNoDiscoverFallback = useSetNoDiscoverFallback() + + const hideFeedsPromoTab = useHideFeedsPromoTab() + const setHideFeedsPromoTab = useSetHideFeedsPromoTab() + + const hideSimilarAccountsRecomm = useHideSimilarAccountsRecomm() + const setHideSimilarAccountsRecomm = useSetHideSimilarAccountsRecomm() + + const hideUnreplyablePosts = useHideUnreplyablePosts() + const setHideUnreplyablePosts = useSetHideUnreplyablePosts() + + const disableComposerPrompt = useDisableComposerPrompt() + const setDisableComposerPrompt = useSetDisableComposerPrompt() + + const disableVerifyEmailReminder = useDisableVerifyEmailReminder() + const setDisableVerifyEmailReminder = useSetDisableVerifyEmailReminder() + + return ( + + + + + Debloating + + setGoLinksEnabled(value)} + style={[a.w_full]}> + + Redirect through go.bsky.app + + + + setNoDiscoverFallback(value)} + style={[a.w_full]}> + + Do not fall back to discover feed + + + + setHideFeedsPromoTab(value)} + style={[a.w_full]}> + + Hide "Feeds ✨" tab when only one feed is selected + + + + setHideSimilarAccountsRecomm(value)} + style={[a.w_full]}> + + Hide similar accounts recommendations + + + + setHideUnreplyablePosts(value)} + style={[a.w_full]}> + + Hide posts that cannot be replied to from feeds + + + + + + Hides posts from feeds where replies are disabled (e.g. due to + postgates or other restrictions). Does not affect thread views. + + + setDisableComposerPrompt(value)} + style={[a.w_full]}> + + Disable composer prompt + + + + setDisableVerifyEmailReminder(value)} + style={[a.w_full]}> + + Disable verify email reminder + + + + + + This only gets rid of the reminder on app launch, useful if your PDS + does not have email verification setup.  This does NOT give + access to features locked behind email verification. + + + + + ) +} diff --git a/src/screens/Settings/RunesSettings/components/RunesScreenLayout.tsx b/src/screens/Settings/RunesSettings/components/RunesScreenLayout.tsx new file mode 100644 index 000000000..c655a1c4e --- /dev/null +++ b/src/screens/Settings/RunesSettings/components/RunesScreenLayout.tsx @@ -0,0 +1,27 @@ +import {type ReactNode} from 'react' + +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import * as Layout from '#/components/Layout' + +export function RunesScreenLayout({ + titleText, + children, +}: { + titleText: string + children: ReactNode +}) { + return ( + + + + + {titleText} + + + + + {children} + + + ) +} diff --git a/src/screens/Settings/RunesSettings/index.tsx b/src/screens/Settings/RunesSettings/index.tsx new file mode 100644 index 000000000..ec654e25c --- /dev/null +++ b/src/screens/Settings/RunesSettings/index.tsx @@ -0,0 +1,74 @@ +import {Trans, useLingui} from '@lingui/react/macro' +import {type NativeStackScreenProps} from '@react-navigation/native-stack' + +import {type CommonNavigatorParams} from '#/lib/routes/types' +import * as SettingsList from '#/screens/Settings/components/SettingsList' +import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' +import {DotGrid3x1_Stroke2_Corner0_Rounded as EllipsisIcon} from '#/components/icons/DotGrid' +import {Eye_Stroke2_Corner0_Rounded as VisibilityIcon} from '#/components/icons/Eye' +import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' +import {Lab_Stroke2_Corner0_Rounded as BeakerIcon} from '#/components/icons/Lab' +import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' +import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' +import {RunesScreenLayout} from './components/RunesScreenLayout' + +type Props = NativeStackScreenProps + +export function RunesSettingsScreen({}: Props) { + const {t: l} = useLingui() + + return ( + + + + + Menus + + + + + + Badges + + + + + + Impressions + + + + + + Usability + + + + + + Display + + + + + + Infrastructure + + + + + + Other additions + + + + ) +} diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index e60cd934e..2ca6f9de8 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -1,12 +1,11 @@ import {useState} from 'react' -import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' +import {Alert, LayoutAnimation, Pressable, View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' -import {HELP_DESK_URL} from '#/lib/constants' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates' import { @@ -38,14 +37,13 @@ import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/co import {useDialogControl} from '#/components/Dialog' import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount' import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility' -import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom' import {Bell_Stroke2_Corner0_Rounded as NotificationIcon} from '#/components/icons/Bell' import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components/icons/BubbleInfo' import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron' -import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion' import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets' import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts' import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' +import {Eclipse_Stroke2_Corner0_Rounded as EclipseIcon} from '#/components/icons/Eclipse' import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock' import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' @@ -190,7 +188,7 @@ export function SettingsScreen({}: Props) { - Moderation + Moderation and content filters Appearance - - - - Runes - - @@ -249,16 +241,12 @@ export function SettingsScreen({}: Props) { Languages - void Linking.openURL(HELP_DESK_URL)} - label={l`Code`} - accessibilityHint={l`Opens code repository in browser`}> - + + - Source code + Runes - - +