[Settings] Thread prefs revamp (#5772)
* thread preferences screen * minor tweaks * more spacing * replace gate with IS_INTERNAL * [Settings] Following feed prefs revamp (#5773) * gated new settings screen * Following feed prefs * Update src/screens/Settings/FollowingFeedPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Update src/screens/Settings/FollowingFeedPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * replace pref following feed gate * Update src/screens/Settings/FollowingFeedPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * use "Experimental" as the header --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] External media prefs revamp (#5774) * gated new settings screen * external media prefs revamp * replace gate ext media embeds * Update src/screens/Settings/ExternalMediaPreferences.tsx Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * add imports for translation * alternate list style on native --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] Languages revamp (partial) (#5775) * language settings (lazy restyle) * replace gate * fix text determining flex space * [Settings] App passwords revamp (#5777) * rework app passwords screen * Apply surfdude's copy changes Thanks @surfdude29! Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * format * replace gate * use admonition for input error and animate --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] Change handle dialog (#5781) * new change handle dialog * animations native only * overflow hidden on togglebutton animation * add a low-contrast border * extract out copybutton * finish change handle dialog * invalidate query on success * web fixes * error message for rate limit exceeded * typo * em dash! Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * another em dash Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * set maxwidth of suffixtext * Copy tweak Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * [Settings] Notifs settings revamp (#5884) * rename, move, and restyle notif settings * bold "experimental:" --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M13.5 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 6a1 1 0 0 0 0 2v2.64q-.319.374-.711.8l-.129.142c-.312.342-.649.711-.974 1.092-.731.857-1.488 1.866-1.89 2.99A4.9 4.9 0 0 0 4 17.298 4.7 4.7 0 0 0 8.702 22h6.596A4.7 4.7 0 0 0 20 17.298a4.8 4.8 0 0 0-.297-1.634c-.401-1.124-1.157-2.133-1.89-2.99-.324-.38-.66-.75-.973-1.092h0l-.129-.141c-.26-.286-.5-.55-.711-.8V8a1 1 0 1 0 0-2H8Zm2 5.35V8h4v3.35l.22.275c.306.383.661.777 1.013 1.163l.13.143h0c.315.345.628.688.93 1.042.372.435.704.861.974 1.28l-.159.025c-.845.13-1.838.242-2.581.222-.842-.022-1.475-.217-2.227-.454l-.027-.008c-.746-.235-1.61-.507-2.746-.538-.743-.02-1.617.064-2.38.165q.26-.342.56-.692c.302-.354.615-.697.93-1.042l.13-.143c.352-.386.707-.78 1.014-1.163L10 11.35Zm7.41 5.905q.316-.048.586-.095.004.07.004.138A2.7 2.7 0 0 1 15.298 20H8.702A2.7 2.7 0 0 1 6 17.298q0-.213.039-.434c.236-.043.53-.093.853-.142.845-.13 1.837-.242 2.581-.222.842.022 1.475.217 2.227.454l.027.008c.746.235 1.61.507 2.746.538.931.024 2.07-.113 2.937-.245Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
+3
-3
@@ -55,7 +55,6 @@ import {ModerationModlistsScreen} from '#/view/screens/ModerationModlists'
|
|||||||
import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'
|
import {ModerationMutedAccounts} from '#/view/screens/ModerationMutedAccounts'
|
||||||
import {NotFoundScreen} from '#/view/screens/NotFound'
|
import {NotFoundScreen} from '#/view/screens/NotFound'
|
||||||
import {NotificationsScreen} from '#/view/screens/Notifications'
|
import {NotificationsScreen} from '#/view/screens/Notifications'
|
||||||
import {NotificationsSettingsScreen} from '#/view/screens/NotificationsSettings'
|
|
||||||
import {PostThreadScreen} from '#/view/screens/PostThread'
|
import {PostThreadScreen} from '#/view/screens/PostThread'
|
||||||
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
import {PreferencesExternalEmbeds} from '#/view/screens/PreferencesExternalEmbeds'
|
||||||
import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed'
|
import {PreferencesFollowingFeed} from '#/view/screens/PreferencesFollowingFeed'
|
||||||
@@ -87,6 +86,7 @@ import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy'
|
|||||||
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
import {ProfileKnownFollowersScreen} from '#/screens/Profile/KnownFollowers'
|
||||||
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLikedBy'
|
||||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||||
|
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||||
import {
|
import {
|
||||||
StarterPackScreen,
|
StarterPackScreen,
|
||||||
StarterPackScreenShort,
|
StarterPackScreenShort,
|
||||||
@@ -378,8 +378,8 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="NotificationsSettings"
|
name="NotificationSettings"
|
||||||
getComponent={() => NotificationsSettingsScreen}
|
getComponent={() => NotificationSettingsScreen}
|
||||||
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
options={{title: title(msg`Notification settings`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
|||||||
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
import {Text as BaseText, TextProps} from '#/components/Typography'
|
import {Text as BaseText, TextProps} from '#/components/Typography'
|
||||||
|
|
||||||
const colors = {
|
export const colors = {
|
||||||
warning: {
|
warning: {
|
||||||
light: '#DFBC00',
|
light: '#DFBC00',
|
||||||
dark: '#BFAF1F',
|
dark: '#BFAF1F',
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ export function SuffixText({
|
|||||||
<Text
|
<Text
|
||||||
accessibilityLabel={label}
|
accessibilityLabel={label}
|
||||||
accessibilityHint={accessibilityHint}
|
accessibilityHint={accessibilityHint}
|
||||||
|
numberOfLines={1}
|
||||||
style={[
|
style={[
|
||||||
a.z_20,
|
a.z_20,
|
||||||
a.pr_sm,
|
a.pr_sm,
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Beaker_Stroke2_Corner2_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M13.5 4a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM10 5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM8 6a1 1 0 0 0 0 2v2.64q-.319.374-.711.8l-.129.142c-.312.342-.649.711-.974 1.092-.731.857-1.488 1.866-1.89 2.99A4.9 4.9 0 0 0 4 17.298 4.7 4.7 0 0 0 8.702 22h6.596A4.7 4.7 0 0 0 20 17.298c0-.575-.114-1.122-.297-1.634-.401-1.124-1.157-2.133-1.89-2.99-.324-.38-.66-.75-.973-1.092h0l-.129-.141c-.26-.286-.5-.55-.711-.8V8a1 1 0 1 0 0-2H8Zm2 5.35V8h4v3.35l.22.275c.306.383.661.777 1.013 1.163l.13.143h0c.315.345.628.688.93 1.042.372.435.704.861.974 1.28l-.159.025c-.845.13-1.838.242-2.581.222-.842-.022-1.475-.217-2.227-.454l-.027-.008c-.746-.235-1.61-.507-2.746-.538-.743-.02-1.617.064-2.38.165q.26-.342.56-.692c.302-.354.615-.697.93-1.042l.13-.143c.352-.386.707-.78 1.014-1.163L10 11.35Zm7.41 5.905q.316-.048.586-.095.004.07.004.138A2.7 2.7 0 0 1 15.298 20H8.702A2.7 2.7 0 0 1 6 17.298q0-.213.039-.434c.236-.043.53-.093.853-.142.845-.13 1.837-.242 2.581-.222.842.022 1.475.217 2.227.454l.027.008c.746.235 1.61.507 2.746.538.931.024 2.07-.113 2.937-.245Z',
|
||||||
|
})
|
||||||
@@ -48,7 +48,7 @@ export type CommonNavigatorParams = {
|
|||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
MessagesConversation: {conversation: string; embed?: string}
|
MessagesConversation: {conversation: string; embed?: string}
|
||||||
MessagesSettings: undefined
|
MessagesSettings: undefined
|
||||||
NotificationsSettings: undefined
|
NotificationSettings: undefined
|
||||||
Feeds: undefined
|
Feeds: undefined
|
||||||
Start: {name: string; rkey: string}
|
Start: {name: string; rkey: string}
|
||||||
StarterPack: {name: string; rkey: string; new?: boolean}
|
StarterPack: {name: string; rkey: string; new?: boolean}
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@ export const router = new Router({
|
|||||||
Search: '/search',
|
Search: '/search',
|
||||||
Feeds: '/feeds',
|
Feeds: '/feeds',
|
||||||
Notifications: '/notifications',
|
Notifications: '/notifications',
|
||||||
NotificationsSettings: '/notifications/settings',
|
NotificationSettings: '/notifications/settings',
|
||||||
Settings: '/settings',
|
Settings: '/settings',
|
||||||
Lists: '/lists',
|
Lists: '/lists',
|
||||||
// moderation
|
// moderation
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ import React from 'react'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog'
|
import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog'
|
||||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
@@ -25,6 +22,7 @@ import {PencilLine_Stroke2_Corner2_Rounded as PencilIcon} from '#/components/ico
|
|||||||
import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner2_Rounded} from '#/components/icons/Trash'
|
||||||
import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
|
import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {ChangeHandleDialog} from './components/ChangeHandleDialog'
|
||||||
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
||||||
@@ -32,10 +30,9 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const {data: profile} = useProfileQuery({did: currentAccount?.did})
|
|
||||||
const {openModal} = useModalControls()
|
const {openModal} = useModalControls()
|
||||||
const birthdayControl = useDialogControl()
|
const birthdayControl = useDialogControl()
|
||||||
|
const changeHandleControl = useDialogControl()
|
||||||
const exportCarControl = useDialogControl()
|
const exportCarControl = useDialogControl()
|
||||||
const deactivateAccountControl = useDialogControl()
|
const deactivateAccountControl = useDialogControl()
|
||||||
|
|
||||||
@@ -117,26 +114,12 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
label={_(msg`Handle`)}
|
label={_(msg`Handle`)}
|
||||||
onPress={() =>
|
accessibilityHint={_(msg`Open change handle dialog`)}
|
||||||
openModal({
|
onPress={() => changeHandleControl.open()}>
|
||||||
name: 'change-handle',
|
|
||||||
onChanged() {
|
|
||||||
if (currentAccount) {
|
|
||||||
// refresh my profile
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: RQKEY_PROFILE(currentAccount.did),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}>
|
|
||||||
<SettingsList.ItemIcon icon={AtIcon} />
|
<SettingsList.ItemIcon icon={AtIcon} />
|
||||||
<SettingsList.ItemText>
|
<SettingsList.ItemText>
|
||||||
<Trans>Handle</Trans>
|
<Trans>Handle</Trans>
|
||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
{profile && (
|
|
||||||
<SettingsList.BadgeText>@{profile.handle}</SettingsList.BadgeText>
|
|
||||||
)}
|
|
||||||
<SettingsList.Chevron />
|
<SettingsList.Chevron />
|
||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
<SettingsList.Divider />
|
<SettingsList.Divider />
|
||||||
@@ -173,6 +156,7 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
</Layout.Content>
|
</Layout.Content>
|
||||||
|
|
||||||
<BirthDateSettingsDialog control={birthdayControl} />
|
<BirthDateSettingsDialog control={birthdayControl} />
|
||||||
|
<ChangeHandleDialog control={changeHandleControl} />
|
||||||
<ExportCarDialog control={exportCarControl} />
|
<ExportCarDialog control={exportCarControl} />
|
||||||
<DeactivateAccountDialog control={deactivateAccountControl} />
|
<DeactivateAccountDialog control={deactivateAccountControl} />
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import React, {useCallback} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
LayoutAnimationConfig,
|
||||||
|
LinearTransition,
|
||||||
|
StretchOutY,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {ComAtprotoServerListAppPasswords} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {
|
||||||
|
useAppPasswordDeleteMutation,
|
||||||
|
useAppPasswordsQuery,
|
||||||
|
} from '#/state/queries/app-passwords'
|
||||||
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Admonition, colors} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||||
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {AddAppPasswordDialog} from './components/AddAppPasswordDialog'
|
||||||
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
||||||
|
export function AppPasswordsScreen({}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {data: appPasswords, error} = useAppPasswordsQuery()
|
||||||
|
const createAppPasswordControl = useDialogControl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="AppPasswordsScreen">
|
||||||
|
<Layout.Header title={_(msg`App Passwords`)} />
|
||||||
|
<Layout.Content>
|
||||||
|
{error ? (
|
||||||
|
<ErrorScreen
|
||||||
|
title={_(msg`Oops!`)}
|
||||||
|
message={_(msg`There was an issue fetching your app passwords`)}
|
||||||
|
details={cleanError(error)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SettingsList.Container>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<Admonition type="tip" style={[a.flex_1]}>
|
||||||
|
<Trans>
|
||||||
|
Use app passwords to sign in to other Bluesky clients without
|
||||||
|
giving full access to your account or password.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</SettingsList.Item>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Add App Password`)}
|
||||||
|
size="large"
|
||||||
|
color="primary"
|
||||||
|
variant="solid"
|
||||||
|
onPress={() => createAppPasswordControl.open()}
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<ButtonIcon icon={PlusIcon} />
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Add App Password</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</SettingsList.Item>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<LayoutAnimationConfig skipEntering skipExiting>
|
||||||
|
{appPasswords ? (
|
||||||
|
appPasswords.length > 0 ? (
|
||||||
|
<View style={[a.overflow_hidden]}>
|
||||||
|
{appPasswords.map(appPassword => (
|
||||||
|
<Animated.View
|
||||||
|
key={appPassword.name}
|
||||||
|
style={a.w_full}
|
||||||
|
entering={FadeIn}
|
||||||
|
exiting={isWeb ? FadeOut : StretchOutY}
|
||||||
|
layout={LinearTransition.delay(150)}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<AppPasswordCard appPassword={appPassword} />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Animated.View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
icon="growth"
|
||||||
|
message={_(msg`No app passwords yet`)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
a.py_4xl,
|
||||||
|
]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</LayoutAnimationConfig>
|
||||||
|
</SettingsList.Container>
|
||||||
|
)}
|
||||||
|
</Layout.Content>
|
||||||
|
|
||||||
|
<AddAppPasswordDialog
|
||||||
|
control={createAppPasswordControl}
|
||||||
|
passwords={appPasswords?.map(p => p.name) || []}
|
||||||
|
/>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppPasswordCard({
|
||||||
|
appPassword,
|
||||||
|
}: {
|
||||||
|
appPassword: ComAtprotoServerListAppPasswords.AppPassword
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {i18n, _} = useLingui()
|
||||||
|
const deleteControl = Prompt.usePromptControl()
|
||||||
|
const {mutateAsync: deleteMutation} = useAppPasswordDeleteMutation()
|
||||||
|
|
||||||
|
const onDelete = useCallback(async () => {
|
||||||
|
await deleteMutation({name: appPassword.name})
|
||||||
|
Toast.show(_(msg`App password deleted`))
|
||||||
|
}, [deleteMutation, appPassword.name, _])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.border,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.px_md,
|
||||||
|
a.py_sm,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_between,
|
||||||
|
a.align_start,
|
||||||
|
a.w_full,
|
||||||
|
a.gap_sm,
|
||||||
|
]}>
|
||||||
|
<View style={[a.gap_xs]}>
|
||||||
|
<Text style={[t.atoms.text, a.text_md, a.font_bold]}>
|
||||||
|
{appPassword.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={[t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>
|
||||||
|
Created{' '}
|
||||||
|
{i18n.date(appPassword.createdAt, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'numeric',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Delete app password`)}
|
||||||
|
variant="ghost"
|
||||||
|
color="negative"
|
||||||
|
size="small"
|
||||||
|
style={[a.bg_transparent]}
|
||||||
|
onPress={() => deleteControl.open()}>
|
||||||
|
<ButtonIcon icon={TrashIcon} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
{appPassword.privileged && (
|
||||||
|
<View style={[a.flex_row, a.gap_sm, a.align_center, a.mt_md]}>
|
||||||
|
<WarningIcon style={[{color: colors.warning[t.scheme]}]} />
|
||||||
|
<Text style={t.atoms.text_contrast_high}>
|
||||||
|
<Trans>Allows access to direct messages</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Prompt.Basic
|
||||||
|
control={deleteControl}
|
||||||
|
title={_(msg`Delete app password?`)}
|
||||||
|
description={_(
|
||||||
|
msg`Are you sure you want to delete the app password "${appPassword.name}"?`,
|
||||||
|
)}
|
||||||
|
onConfirm={onDelete}
|
||||||
|
confirmButtonCta={_(msg`Delete`)}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import React, {Fragment} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
|
import {
|
||||||
|
EmbedPlayerSource,
|
||||||
|
externalEmbedLabels,
|
||||||
|
} from '#/lib/strings/embed-player'
|
||||||
|
import {
|
||||||
|
useExternalEmbedsPrefs,
|
||||||
|
useSetExternalEmbedPref,
|
||||||
|
} from '#/state/preferences'
|
||||||
|
import {atoms as a, native} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<
|
||||||
|
CommonNavigatorParams,
|
||||||
|
'PreferencesExternalEmbeds'
|
||||||
|
>
|
||||||
|
export function ExternalMediaPreferencesScreen({}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="externalMediaPreferencesScreen">
|
||||||
|
<Layout.Header title={_(msg`External Media Preferences`)} />
|
||||||
|
<Layout.Content>
|
||||||
|
<SettingsList.Container>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<Admonition type="info" style={[a.flex_1]}>
|
||||||
|
<Trans>
|
||||||
|
External media may allow websites to collect information about
|
||||||
|
you and your device. No information is sent or requested until
|
||||||
|
you press the "play" button.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</SettingsList.Item>
|
||||||
|
<SettingsList.Group iconInset={false}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Enable media players for</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<View style={[a.mt_sm, a.w_full]}>
|
||||||
|
{native(<SettingsList.Divider style={[a.my_0]} />)}
|
||||||
|
{Object.entries(externalEmbedLabels)
|
||||||
|
// TODO: Remove special case when we disable the old integration.
|
||||||
|
.filter(([key]) => key !== 'tenor')
|
||||||
|
.map(([key, label]) => (
|
||||||
|
<Fragment key={key}>
|
||||||
|
<PrefSelector
|
||||||
|
source={key as EmbedPlayerSource}
|
||||||
|
label={label}
|
||||||
|
key={key}
|
||||||
|
/>
|
||||||
|
{native(<SettingsList.Divider style={[a.my_0]} />)}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</SettingsList.Group>
|
||||||
|
</SettingsList.Container>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PrefSelector({
|
||||||
|
source,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
source: EmbedPlayerSource
|
||||||
|
label: string
|
||||||
|
}) {
|
||||||
|
const setExternalEmbedPref = useSetExternalEmbedPref()
|
||||||
|
const sources = useExternalEmbedsPrefs()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toggle.Item
|
||||||
|
name={label}
|
||||||
|
label={label}
|
||||||
|
type="checkbox"
|
||||||
|
value={sources?.[source] === 'show'}
|
||||||
|
onChange={() =>
|
||||||
|
setExternalEmbedPref(
|
||||||
|
source,
|
||||||
|
sources?.[source] === 'show' ? 'hide' : 'show',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.py_md,
|
||||||
|
native([a.justify_between, a.flex_row_reverse]),
|
||||||
|
]}>
|
||||||
|
<Toggle.Platform />
|
||||||
|
<Toggle.LabelText style={[a.text_md]}>{label}</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
useSetFeedViewPreferencesMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
|
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
||||||
|
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
||||||
|
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<
|
||||||
|
CommonNavigatorParams,
|
||||||
|
'PreferencesFollowingFeed'
|
||||||
|
>
|
||||||
|
export function FollowingFeedPreferencesScreen({}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate: setFeedViewPref, variables} =
|
||||||
|
useSetFeedViewPreferencesMutation()
|
||||||
|
|
||||||
|
const showReplies = !(
|
||||||
|
variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
|
||||||
|
)
|
||||||
|
|
||||||
|
const showReposts = !(
|
||||||
|
variables?.hideReposts ?? preferences?.feedViewPrefs?.hideReposts
|
||||||
|
)
|
||||||
|
|
||||||
|
const showQuotePosts = !(
|
||||||
|
variables?.hideQuotePosts ?? preferences?.feedViewPrefs?.hideQuotePosts
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergeFeedEnabled = Boolean(
|
||||||
|
variables?.lab_mergeFeedEnabled ??
|
||||||
|
preferences?.feedViewPrefs?.lab_mergeFeedEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="followingFeedPreferencesScreen">
|
||||||
|
<Layout.Header title={_(msg`Following Feed Preferences`)} />
|
||||||
|
<Layout.Content>
|
||||||
|
<SettingsList.Container>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<Admonition type="tip" style={[a.flex_1]}>
|
||||||
|
<Trans>These settings only apply to the Following feed.</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</SettingsList.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="show-replies"
|
||||||
|
label={_(msg`Show replies`)}
|
||||||
|
value={showReplies}
|
||||||
|
onChange={value =>
|
||||||
|
setFeedViewPref({
|
||||||
|
hideReplies: !value,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<SettingsList.ItemIcon icon={BubblesIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Show replies</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="show-reposts"
|
||||||
|
label={_(msg`Show reposts`)}
|
||||||
|
value={showReposts}
|
||||||
|
onChange={value =>
|
||||||
|
setFeedViewPref({
|
||||||
|
hideReposts: !value,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<SettingsList.ItemIcon icon={RepostIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Show reposts</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="show-quotes"
|
||||||
|
label={_(msg`Show quote posts`)}
|
||||||
|
value={showQuotePosts}
|
||||||
|
onChange={value =>
|
||||||
|
setFeedViewPref({
|
||||||
|
hideQuotePosts: !value,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<SettingsList.ItemIcon icon={QuoteIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Show quote posts</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Toggle.Item>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<SettingsList.Group>
|
||||||
|
<SettingsList.ItemIcon icon={BeakerIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Experimental</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="merge-feed"
|
||||||
|
label={_(
|
||||||
|
msg`Show samples of your saved feeds in your Following feed`,
|
||||||
|
)}
|
||||||
|
value={mergeFeedEnabled}
|
||||||
|
onChange={value =>
|
||||||
|
setFeedViewPref({
|
||||||
|
lab_mergeFeedEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={[a.w_full, a.gap_md]}>
|
||||||
|
<Toggle.LabelText style={[a.flex_1]}>
|
||||||
|
<Trans>
|
||||||
|
Show samples of your saved feeds in your Following feed
|
||||||
|
</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</Toggle.Item>
|
||||||
|
</SettingsList.Group>
|
||||||
|
</SettingsList.Container>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import React, {useCallback, useMemo} from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
|
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||||
|
import {useModalControls} from '#/state/modals'
|
||||||
|
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
|
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
|
||||||
|
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
|
||||||
|
export function LanguageSettingsScreen({}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const langPrefs = useLanguagePrefs()
|
||||||
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
|
||||||
|
const onPressContentLanguages = useCallback(() => {
|
||||||
|
openModal({name: 'content-languages-settings'})
|
||||||
|
}, [openModal])
|
||||||
|
|
||||||
|
const onChangePrimaryLanguage = useCallback(
|
||||||
|
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
|
||||||
|
if (!value) return
|
||||||
|
if (langPrefs.primaryLanguage !== value) {
|
||||||
|
setLangPrefs.setPrimaryLanguage(value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[langPrefs, setLangPrefs],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onChangeAppLanguage = useCallback(
|
||||||
|
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
|
||||||
|
if (!value) return
|
||||||
|
if (langPrefs.appLanguage !== value) {
|
||||||
|
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[langPrefs, setLangPrefs],
|
||||||
|
)
|
||||||
|
|
||||||
|
const myLanguages = useMemo(() => {
|
||||||
|
return (
|
||||||
|
langPrefs.contentLanguages
|
||||||
|
.map(lang => LANGUAGES.find(l => l.code2 === lang))
|
||||||
|
.filter(Boolean)
|
||||||
|
// @ts-ignore
|
||||||
|
.map(l => l.name)
|
||||||
|
.join(', ')
|
||||||
|
)
|
||||||
|
}, [langPrefs.contentLanguages])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="PreferencesLanguagesScreen">
|
||||||
|
<Layout.Header title={_(msg`Languages`)} />
|
||||||
|
<Layout.Content>
|
||||||
|
<SettingsList.Container>
|
||||||
|
<SettingsList.Group iconInset={false}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>App Language</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<View style={[a.gap_md, a.w_full]}>
|
||||||
|
<Text style={[a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Select your app language for the default text to display in
|
||||||
|
the app.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.relative, web([a.w_full, {maxWidth: 400}])]}>
|
||||||
|
<RNPickerSelect
|
||||||
|
placeholder={{}}
|
||||||
|
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
|
||||||
|
onValueChange={onChangeAppLanguage}
|
||||||
|
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
|
||||||
|
label: l.name,
|
||||||
|
value: l.code2,
|
||||||
|
key: l.code2,
|
||||||
|
}))}
|
||||||
|
style={{
|
||||||
|
inputAndroid: {
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
color: t.atoms.text.color,
|
||||||
|
fontSize: 14,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
fontWeight: a.font_bold.fontWeight,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
},
|
||||||
|
inputIOS: {
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
color: t.atoms.text.color,
|
||||||
|
fontSize: 14,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
fontWeight: a.font_bold.fontWeight,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
},
|
||||||
|
inputWeb: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
// @ts-ignore web only
|
||||||
|
'-moz-appearance': 'none',
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
appearance: 'none',
|
||||||
|
outline: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
color: t.atoms.text.color,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
fontWeight: a.font_bold.fontWeight,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.rounded_xs,
|
||||||
|
a.pointer_events_none,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{
|
||||||
|
top: 1,
|
||||||
|
right: 1,
|
||||||
|
bottom: 1,
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<ChevronDownIcon style={[t.atoms.text]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SettingsList.Group>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<SettingsList.Group iconInset={false}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Primary Language</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<View style={[a.gap_md, a.w_full]}>
|
||||||
|
<Text style={[a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Select your preferred language for translations in your feed.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.relative, web([a.w_full, {maxWidth: 400}])]}>
|
||||||
|
<RNPickerSelect
|
||||||
|
placeholder={{}}
|
||||||
|
value={langPrefs.primaryLanguage}
|
||||||
|
onValueChange={onChangePrimaryLanguage}
|
||||||
|
items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
|
||||||
|
label: l.name,
|
||||||
|
value: l.code2,
|
||||||
|
key: l.code2 + l.code3,
|
||||||
|
}))}
|
||||||
|
style={{
|
||||||
|
inputAndroid: {
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
color: t.atoms.text.color,
|
||||||
|
fontSize: 14,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
fontWeight: a.font_bold.fontWeight,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
},
|
||||||
|
inputIOS: {
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
color: t.atoms.text.color,
|
||||||
|
fontSize: 14,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
fontWeight: a.font_bold.fontWeight,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
},
|
||||||
|
inputWeb: {
|
||||||
|
flex: 1,
|
||||||
|
width: '100%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
// @ts-ignore web only
|
||||||
|
'-moz-appearance': 'none',
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
appearance: 'none',
|
||||||
|
outline: 0,
|
||||||
|
borderWidth: 0,
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
color: t.atoms.text.color,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
fontWeight: a.font_bold.fontWeight,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 1,
|
||||||
|
right: 1,
|
||||||
|
bottom: 1,
|
||||||
|
width: 40,
|
||||||
|
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||||
|
borderRadius: a.rounded_xs.borderRadius,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
<ChevronDownIcon style={t.atoms.text} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SettingsList.Group>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<SettingsList.Group iconInset={false}>
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Content Languages</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
<Text style={[a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Select which languages you want your subscribed feeds to
|
||||||
|
include. If none are selected, all languages will be shown.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
label={_(msg`Select content languages`)}
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
variant="solid"
|
||||||
|
onPress={onPressContentLanguages}
|
||||||
|
style={[a.justify_start, web({maxWidth: 400})]}>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={myLanguages.length > 0 ? CheckIcon : PlusIcon}
|
||||||
|
/>
|
||||||
|
<ButtonText
|
||||||
|
style={[t.atoms.text, a.text_md, a.flex_1, a.text_left]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{myLanguages.length > 0
|
||||||
|
? myLanguages
|
||||||
|
: _(msg`Select languages`)}
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</SettingsList.Group>
|
||||||
|
</SettingsList.Container>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Text} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
|
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
||||||
|
import {useNotificationSettingsMutation} from '#/state/queries/notifications/settings'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Error} from '#/components/Error'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'>
|
||||||
|
export function NotificationSettingsScreen({}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
|
||||||
|
const serverPriority = data?.pages.at(0)?.priority
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: onChangePriority,
|
||||||
|
isPending: isMutationPending,
|
||||||
|
variables,
|
||||||
|
} = useNotificationSettingsMutation()
|
||||||
|
|
||||||
|
const priority = isMutationPending
|
||||||
|
? variables[0] === 'enabled'
|
||||||
|
: serverPriority
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen>
|
||||||
|
<Layout.Header title={_(msg`Notification Settings`)} />
|
||||||
|
<Layout.Content>
|
||||||
|
{isQueryError ? (
|
||||||
|
<Error
|
||||||
|
title={_(msg`Oops!`)}
|
||||||
|
message={_(msg`Something went wrong!`)}
|
||||||
|
onRetry={refetch}
|
||||||
|
sideBorders={false}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SettingsList.Container>
|
||||||
|
<SettingsList.Group>
|
||||||
|
<SettingsList.ItemIcon icon={BeakerIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Notification filters</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Group
|
||||||
|
label={_(msg`Priority notifications`)}
|
||||||
|
type="checkbox"
|
||||||
|
values={priority ? ['enabled'] : []}
|
||||||
|
onChange={onChangePriority}
|
||||||
|
disabled={typeof priority !== 'boolean' || isMutationPending}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="enabled"
|
||||||
|
label={_(msg`Enable priority notifications`)}
|
||||||
|
style={[a.flex_1, a.justify_between]}>
|
||||||
|
<Toggle.LabelText>
|
||||||
|
<Trans>Enable priority notifications</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
{!data ? <Loader size="md" /> : <Toggle.Platform />}
|
||||||
|
</Toggle.Item>
|
||||||
|
</Toggle.Group>
|
||||||
|
</SettingsList.Group>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<Admonition type="warning" style={[a.flex_1]}>
|
||||||
|
<Trans>
|
||||||
|
<Text style={[a.font_bold]}>Experimental:</Text> When this
|
||||||
|
preference is enabled, you'll only receive reply and quote
|
||||||
|
notifications from users you follow. We'll continue to add
|
||||||
|
more controls here over time.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</SettingsList.Item>
|
||||||
|
</SettingsList.Container>
|
||||||
|
)}
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ export function SettingsScreen({}: Props) {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.px_xl,
|
a.px_xl,
|
||||||
|
a.pt_md,
|
||||||
a.pb_md,
|
a.pb_md,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.gap_2xs,
|
a.gap_2xs,
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
useSetThreadViewPreferencesMutation,
|
||||||
|
} from '#/state/queries/preferences'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
|
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
||||||
|
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
|
||||||
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
||||||
|
export function ThreadPreferencesScreen({}: Props) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutate: setThreadViewPrefs, variables} =
|
||||||
|
useSetThreadViewPreferencesMutation()
|
||||||
|
|
||||||
|
const sortReplies = variables?.sort ?? preferences?.threadViewPrefs?.sort
|
||||||
|
|
||||||
|
const prioritizeFollowedUsers = Boolean(
|
||||||
|
variables?.prioritizeFollowedUsers ??
|
||||||
|
preferences?.threadViewPrefs?.prioritizeFollowedUsers,
|
||||||
|
)
|
||||||
|
const treeViewEnabled = Boolean(
|
||||||
|
variables?.lab_treeViewEnabled ??
|
||||||
|
preferences?.threadViewPrefs?.lab_treeViewEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout.Screen testID="threadPreferencesScreen">
|
||||||
|
<Layout.Header title={_(msg`Thread Preferences`)} />
|
||||||
|
<Layout.Content>
|
||||||
|
<SettingsList.Container>
|
||||||
|
<SettingsList.Group>
|
||||||
|
<SettingsList.ItemIcon icon={BubblesIcon} />
|
||||||
|
<SettingsList.ItemText>Sort replies</SettingsList.ItemText>
|
||||||
|
<View style={[a.w_full, a.gap_md]}>
|
||||||
|
<Text style={[a.flex_1, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Sort replies to the same post by:</Trans>
|
||||||
|
</Text>
|
||||||
|
<Toggle.Group
|
||||||
|
label={_(msg`Sort replies by`)}
|
||||||
|
type="radio"
|
||||||
|
values={sortReplies ? [sortReplies] : []}
|
||||||
|
onChange={values => setThreadViewPrefs({sort: values[0]})}>
|
||||||
|
<View style={[a.gap_sm, a.flex_1]}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="oldest"
|
||||||
|
label={_(msg`Oldest replies first`)}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText>
|
||||||
|
<Trans>Oldest replies first</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="newest"
|
||||||
|
label={_(msg`Newest replies first`)}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText>
|
||||||
|
<Trans>Newest replies first</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="most-likes"
|
||||||
|
label={_(msg`Most-liked replies first`)}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText>
|
||||||
|
<Trans>Most-liked first</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Toggle.Item
|
||||||
|
name="random"
|
||||||
|
label={_(msg`Random (aka "Poster's Roulette")`)}>
|
||||||
|
<Toggle.Radio />
|
||||||
|
<Toggle.LabelText>
|
||||||
|
<Trans>Random (aka "Poster's Roulette")</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
|
</Toggle.Group>
|
||||||
|
</View>
|
||||||
|
</SettingsList.Group>
|
||||||
|
<SettingsList.Group>
|
||||||
|
<SettingsList.ItemIcon icon={PersonGroupIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Prioritize your Follows</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="prioritize-follows"
|
||||||
|
label={_(msg`Prioritize your Follows`)}
|
||||||
|
value={prioritizeFollowedUsers}
|
||||||
|
onChange={value =>
|
||||||
|
setThreadViewPrefs({
|
||||||
|
prioritizeFollowedUsers: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={[a.w_full, a.gap_md]}>
|
||||||
|
<Toggle.LabelText style={[a.flex_1]}>
|
||||||
|
<Trans>
|
||||||
|
Show replies by people you follow before all other replies.
|
||||||
|
</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</Toggle.Item>
|
||||||
|
</SettingsList.Group>
|
||||||
|
<SettingsList.Divider />
|
||||||
|
<SettingsList.Group>
|
||||||
|
<SettingsList.ItemIcon icon={BeakerIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Experimental</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="threaded-mode"
|
||||||
|
label={_(msg`Threaded mode`)}
|
||||||
|
value={treeViewEnabled}
|
||||||
|
onChange={value =>
|
||||||
|
setThreadViewPrefs({
|
||||||
|
lab_treeViewEnabled: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
style={[a.w_full, a.gap_md]}>
|
||||||
|
<Toggle.LabelText style={[a.flex_1]}>
|
||||||
|
<Trans>Show replies in a threaded view</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</Toggle.Item>
|
||||||
|
</SettingsList.Group>
|
||||||
|
</SettingsList.Container>
|
||||||
|
</Layout.Content>
|
||||||
|
</Layout.Screen>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import React, {useEffect, useMemo, useState} from 'react'
|
||||||
|
import {useWindowDimensions, View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
LayoutAnimationConfig,
|
||||||
|
LinearTransition,
|
||||||
|
SlideInRight,
|
||||||
|
SlideOutLeft,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {ComAtprotoServerCreateAppPassword} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useMutation} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
import {useAppPasswordCreateMutation} from '#/state/queries/app-passwords'
|
||||||
|
import {atoms as a, native, useTheme} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import * as TextInput from '#/components/forms/TextField'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron'
|
||||||
|
import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {CopyButton} from './CopyButton'
|
||||||
|
|
||||||
|
export function AddAppPasswordDialog({
|
||||||
|
control,
|
||||||
|
passwords,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogControlProps
|
||||||
|
passwords: string[]
|
||||||
|
}) {
|
||||||
|
const {height} = useWindowDimensions()
|
||||||
|
return (
|
||||||
|
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<CreateDialogInner passwords={passwords} />
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CreateDialogInner({passwords}: {passwords: string[]}) {
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const autogeneratedName = useRandomName()
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [privileged, setPrivileged] = useState(false)
|
||||||
|
const {
|
||||||
|
mutateAsync: actuallyCreateAppPassword,
|
||||||
|
error: apiError,
|
||||||
|
data,
|
||||||
|
} = useAppPasswordCreateMutation()
|
||||||
|
|
||||||
|
const regexFailError = useMemo(
|
||||||
|
() =>
|
||||||
|
new DisplayableError(
|
||||||
|
_(
|
||||||
|
msg`App password names can only contain letters, numbers, spaces, dashes, and underscores`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
[_],
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: createAppPassword,
|
||||||
|
error: validationError,
|
||||||
|
isPending,
|
||||||
|
} = useMutation<
|
||||||
|
ComAtprotoServerCreateAppPassword.AppPassword,
|
||||||
|
Error | DisplayableError
|
||||||
|
>({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const chosenName = name.trim() || autogeneratedName
|
||||||
|
if (chosenName.length < 4) {
|
||||||
|
throw new DisplayableError(
|
||||||
|
_(msg`App password names must be at least 4 characters long`),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (passwords.find(p => p === chosenName)) {
|
||||||
|
throw new DisplayableError(_(msg`App password name must be unique`))
|
||||||
|
}
|
||||||
|
return await actuallyCreateAppPassword({name: chosenName, privileged})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const [hasBeenCopied, setHasBeenCopied] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasBeenCopied) {
|
||||||
|
const timeout = setTimeout(() => setHasBeenCopied(false), 100)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [hasBeenCopied])
|
||||||
|
|
||||||
|
const error =
|
||||||
|
validationError || (!name.match(/^[a-zA-Z0-9-_ ]*$/) && regexFailError)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.ScrollableInner label={_(msg`Add app password`)}>
|
||||||
|
<View style={[native(a.pt_md)]}>
|
||||||
|
<LayoutAnimationConfig skipEntering skipExiting>
|
||||||
|
{!data ? (
|
||||||
|
<Animated.View
|
||||||
|
style={[a.gap_lg]}
|
||||||
|
exiting={native(SlideOutLeft)}
|
||||||
|
key={0}>
|
||||||
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
|
<Trans>Add App Password</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Please enter a unique name for this app password or use our
|
||||||
|
randomly generated one.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
<TextInput.Root isInvalid={!!error}>
|
||||||
|
<Dialog.Input
|
||||||
|
label={_(msg`App Password`)}
|
||||||
|
placeholder={autogeneratedName}
|
||||||
|
onChangeText={setName}
|
||||||
|
returnKeyType="done"
|
||||||
|
onSubmitEditing={() => createAppPassword()}
|
||||||
|
blurOnSubmit
|
||||||
|
autoCorrect={false}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</TextInput.Root>
|
||||||
|
</View>
|
||||||
|
{error instanceof DisplayableError && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<Admonition type="error">{error.message}</Admonition>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Animated.View
|
||||||
|
style={[a.gap_lg]}
|
||||||
|
layout={native(LinearTransition)}>
|
||||||
|
<Toggle.Item
|
||||||
|
name="privileged"
|
||||||
|
type="checkbox"
|
||||||
|
label={_(msg`Allow access to your direct messages`)}
|
||||||
|
value={privileged}
|
||||||
|
onChange={setPrivileged}
|
||||||
|
style={[a.flex_1]}>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
<Toggle.LabelText
|
||||||
|
style={[a.font_normal, a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>Allow access to your direct messages</Trans>
|
||||||
|
</Toggle.LabelText>
|
||||||
|
</Toggle.Item>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Next`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
style={[a.flex_1]}
|
||||||
|
onPress={() => createAppPassword()}
|
||||||
|
disabled={isPending}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={ChevronRight} />
|
||||||
|
</Button>
|
||||||
|
{!!apiError ||
|
||||||
|
(error && !(error instanceof DisplayableError) && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<Admonition type="error">
|
||||||
|
<Trans>
|
||||||
|
Failed to create app password. Please try again.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</Animated.View>
|
||||||
|
))}
|
||||||
|
</Animated.View>
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<Animated.View
|
||||||
|
style={[a.gap_lg]}
|
||||||
|
entering={isWeb ? FadeIn.delay(200) : SlideInRight}
|
||||||
|
key={1}>
|
||||||
|
<Text style={[a.text_2xl, a.font_bold]}>
|
||||||
|
<Trans>Here is your app password!</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
Use this to sign into the other app along with your handle.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<CopyButton
|
||||||
|
value={data.password}
|
||||||
|
label={_(msg`Copy App Password`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary">
|
||||||
|
<ButtonText>{data.password}</ButtonText>
|
||||||
|
<ButtonIcon icon={CopyIcon} />
|
||||||
|
</CopyButton>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.leading_snug,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
<Trans>
|
||||||
|
For security reasons, you won't be able to view this again. If
|
||||||
|
you lose this app password, you'll need to generate a new one.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Done`)}
|
||||||
|
size="large"
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
style={[a.flex_1]}
|
||||||
|
onPress={() => control.close()}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</LayoutAnimationConfig>
|
||||||
|
</View>
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DisplayableError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'DisplayableError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useRandomName() {
|
||||||
|
return useState(
|
||||||
|
() => shadesOfBlue[Math.floor(Math.random() * shadesOfBlue.length)],
|
||||||
|
)[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadesOfBlue: string[] = [
|
||||||
|
'AliceBlue',
|
||||||
|
'Aqua',
|
||||||
|
'Aquamarine',
|
||||||
|
'Azure',
|
||||||
|
'BabyBlue',
|
||||||
|
'Blue',
|
||||||
|
'BlueViolet',
|
||||||
|
'CadetBlue',
|
||||||
|
'CornflowerBlue',
|
||||||
|
'Cyan',
|
||||||
|
'DarkBlue',
|
||||||
|
'DarkCyan',
|
||||||
|
'DarkSlateBlue',
|
||||||
|
'DeepSkyBlue',
|
||||||
|
'DodgerBlue',
|
||||||
|
'ElectricBlue',
|
||||||
|
'LightBlue',
|
||||||
|
'LightCyan',
|
||||||
|
'LightSkyBlue',
|
||||||
|
'LightSteelBlue',
|
||||||
|
'MediumAquaMarine',
|
||||||
|
'MediumBlue',
|
||||||
|
'MediumSlateBlue',
|
||||||
|
'MidnightBlue',
|
||||||
|
'Navy',
|
||||||
|
'PowderBlue',
|
||||||
|
'RoyalBlue',
|
||||||
|
'SkyBlue',
|
||||||
|
'SlateBlue',
|
||||||
|
'SteelBlue',
|
||||||
|
'Teal',
|
||||||
|
'Turquoise',
|
||||||
|
]
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
import React, {useCallback, useMemo, useState} from 'react'
|
||||||
|
import {useWindowDimensions, View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
LayoutAnimationConfig,
|
||||||
|
LinearTransition,
|
||||||
|
SlideInLeft,
|
||||||
|
SlideInRight,
|
||||||
|
SlideOutLeft,
|
||||||
|
SlideOutRight,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {createFullHandle, validateHandle} from '#/lib/strings/handles'
|
||||||
|
import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
|
||||||
|
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||||
|
import {useServiceQuery} from '#/state/queries/service'
|
||||||
|
import {useAgent, useSession} from '#/state/session'
|
||||||
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
|
import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import * as TextField from '#/components/forms/TextField'
|
||||||
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
|
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
|
||||||
|
import {At_Stroke2_Corner0_Rounded as AtIcon} from '#/components/icons/At'
|
||||||
|
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
|
import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4'
|
||||||
|
import {InlineLinkText} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
import {CopyButton} from './CopyButton'
|
||||||
|
|
||||||
|
export function ChangeHandleDialog({
|
||||||
|
control,
|
||||||
|
}: {
|
||||||
|
control: Dialog.DialogControlProps
|
||||||
|
}) {
|
||||||
|
const {height} = useWindowDimensions()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
|
||||||
|
<ChangeHandleDialogInner />
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangeHandleDialogInner() {
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const agent = useAgent()
|
||||||
|
const {
|
||||||
|
data: serviceInfo,
|
||||||
|
error: serviceInfoError,
|
||||||
|
refetch,
|
||||||
|
} = useServiceQuery(agent.serviceUrl.toString())
|
||||||
|
|
||||||
|
const [page, setPage] = useState<'provided-handle' | 'own-handle'>(
|
||||||
|
'provided-handle',
|
||||||
|
)
|
||||||
|
|
||||||
|
const cancelButton = useCallback(
|
||||||
|
() => (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Cancel`)}
|
||||||
|
onPress={() => control.close()}
|
||||||
|
size="small"
|
||||||
|
color="primary"
|
||||||
|
variant="ghost"
|
||||||
|
style={[a.rounded_full]}>
|
||||||
|
<ButtonText style={[a.text_md]}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
[control, _],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={_(msg`Change Handle`)}
|
||||||
|
style={[a.overflow_hidden]}
|
||||||
|
header={
|
||||||
|
<Dialog.Header renderLeft={cancelButton}>
|
||||||
|
<Dialog.HeaderText>
|
||||||
|
<Trans>Change Handle</Trans>
|
||||||
|
</Dialog.HeaderText>
|
||||||
|
</Dialog.Header>
|
||||||
|
}
|
||||||
|
contentContainerStyle={[a.pt_0, a.px_0]}>
|
||||||
|
<View style={[a.flex_1, a.pt_lg, a.px_xl]}>
|
||||||
|
{serviceInfoError ? (
|
||||||
|
<ErrorScreen
|
||||||
|
title={_(msg`Oops!`)}
|
||||||
|
message={_(msg`There was an issue fetching your service info`)}
|
||||||
|
details={cleanError(serviceInfoError)}
|
||||||
|
onPressTryAgain={refetch}
|
||||||
|
/>
|
||||||
|
) : serviceInfo ? (
|
||||||
|
<LayoutAnimationConfig skipEntering skipExiting>
|
||||||
|
{page === 'provided-handle' ? (
|
||||||
|
<Animated.View
|
||||||
|
key={page}
|
||||||
|
entering={native(SlideInLeft)}
|
||||||
|
exiting={native(SlideOutLeft)}>
|
||||||
|
<ProvidedHandlePage
|
||||||
|
serviceInfo={serviceInfo}
|
||||||
|
goToOwnHandle={() => setPage('own-handle')}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<Animated.View
|
||||||
|
key={page}
|
||||||
|
entering={native(SlideInRight)}
|
||||||
|
exiting={native(SlideOutRight)}>
|
||||||
|
<OwnHandlePage
|
||||||
|
goToServiceHandle={() => setPage('provided-handle')}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</LayoutAnimationConfig>
|
||||||
|
) : (
|
||||||
|
<View style={[a.flex_1, a.justify_center, a.align_center, a.py_4xl]}>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProvidedHandlePage({
|
||||||
|
serviceInfo,
|
||||||
|
goToOwnHandle,
|
||||||
|
}: {
|
||||||
|
serviceInfo: ComAtprotoServerDescribeServer.OutputSchema
|
||||||
|
goToOwnHandle: () => void
|
||||||
|
}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const [subdomain, setSubdomain] = useState('')
|
||||||
|
const agent = useAgent()
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: changeHandle,
|
||||||
|
isPending,
|
||||||
|
error,
|
||||||
|
isSuccess,
|
||||||
|
} = useUpdateHandleMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
if (currentAccount) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: RQKEY_PROFILE(currentAccount.did),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
agent.resumeSession(agent.session!).then(() => control.close())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const host = serviceInfo.availableUserDomains[0]
|
||||||
|
|
||||||
|
const validation = useMemo(
|
||||||
|
() => validateHandle(subdomain, host),
|
||||||
|
[subdomain, host],
|
||||||
|
)
|
||||||
|
|
||||||
|
const isTooLong = subdomain.length > 18
|
||||||
|
const isInvalid =
|
||||||
|
isTooLong ||
|
||||||
|
!validation.handleChars ||
|
||||||
|
!validation.hyphenStartOrEnd ||
|
||||||
|
!validation.totalLength
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutAnimationConfig skipEntering>
|
||||||
|
<View style={[a.flex_1, a.gap_md]}>
|
||||||
|
{isSuccess && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<SuccessMessage text={_(msg`Handle changed!`)} />
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<ChangeHandleError error={error} />
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Animated.View
|
||||||
|
layout={native(LinearTransition)}
|
||||||
|
style={[a.flex_1, a.gap_md]}>
|
||||||
|
<View>
|
||||||
|
<TextField.LabelText>
|
||||||
|
<Trans>New handle</Trans>
|
||||||
|
</TextField.LabelText>
|
||||||
|
<TextField.Root isInvalid={isInvalid}>
|
||||||
|
<TextField.Icon icon={AtIcon} />
|
||||||
|
<Dialog.Input
|
||||||
|
editable={!isPending}
|
||||||
|
defaultValue={subdomain}
|
||||||
|
onChangeText={text => setSubdomain(text)}
|
||||||
|
label={_(msg`New handle`)}
|
||||||
|
placeholder={_(msg`e.g. alice`)}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
<TextField.SuffixText label={host} style={[{maxWidth: '40%'}]}>
|
||||||
|
{host}
|
||||||
|
</TextField.SuffixText>
|
||||||
|
</TextField.Root>
|
||||||
|
</View>
|
||||||
|
<Text>
|
||||||
|
<Trans>
|
||||||
|
Your full handle will be{' '}
|
||||||
|
<Text style={[a.font_bold]}>
|
||||||
|
@{createFullHandle(subdomain, host)}
|
||||||
|
</Text>
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Save new handle`)}
|
||||||
|
variant="solid"
|
||||||
|
size="large"
|
||||||
|
color={validation.overall && !isTooLong ? 'primary' : 'secondary'}
|
||||||
|
disabled={!validation.overall && !isTooLong}
|
||||||
|
onPress={() => {
|
||||||
|
if (validation.overall && !isTooLong) {
|
||||||
|
changeHandle({handle: createFullHandle(subdomain, host)})
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{isPending ? (
|
||||||
|
<ButtonIcon icon={Loader} />
|
||||||
|
) : (
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Save</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Text style={[a.leading_snug]}>
|
||||||
|
<Trans>
|
||||||
|
If you have your own domain, you can use that as your handle. This
|
||||||
|
lets you self-verify your identity –{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`learn more`)}
|
||||||
|
to="https://bsky.social/about/blog/4-28-2023-domain-handle-tutorial"
|
||||||
|
style={[a.font_bold]}
|
||||||
|
disableMismatchWarning>
|
||||||
|
learn more
|
||||||
|
</InlineLinkText>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
label={_(msg`I have my own domain`)}
|
||||||
|
variant="outline"
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
onPress={goToOwnHandle}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>I have my own domain</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={ArrowRightIcon} />
|
||||||
|
</Button>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</LayoutAnimationConfig>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const [dnsPanel, setDNSPanel] = useState(true)
|
||||||
|
const [domain, setDomain] = useState('')
|
||||||
|
const agent = useAgent()
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const fetchDid = useFetchDid()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: changeHandle,
|
||||||
|
isPending,
|
||||||
|
error,
|
||||||
|
isSuccess,
|
||||||
|
} = useUpdateHandleMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
if (currentAccount) {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: RQKEY_PROFILE(currentAccount.did),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
agent.resumeSession(agent.session!).then(() => control.close())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: verify,
|
||||||
|
isPending: isVerifyPending,
|
||||||
|
isSuccess: isVerified,
|
||||||
|
error: verifyError,
|
||||||
|
reset: resetVerification,
|
||||||
|
} = useMutation<true, Error | DidMismatchError>({
|
||||||
|
mutationKey: ['verify-handle', domain],
|
||||||
|
mutationFn: async () => {
|
||||||
|
const did = await fetchDid(domain)
|
||||||
|
if (did !== currentAccount?.did) {
|
||||||
|
throw new DidMismatchError(did)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_1, a.gap_lg]}>
|
||||||
|
{isSuccess && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<SuccessMessage text={_(msg`Handle changed!`)} />
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<ChangeHandleError error={error} />
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
{verifyError && (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<Admonition type="error">
|
||||||
|
{verifyError instanceof DidMismatchError ? (
|
||||||
|
<Trans>
|
||||||
|
Wrong DID returned from server. Received: {verifyError.did}
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Failed to verify handle. Please try again.</Trans>
|
||||||
|
)}
|
||||||
|
</Admonition>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Animated.View
|
||||||
|
layout={native(LinearTransition)}
|
||||||
|
style={[a.flex_1, a.gap_md, a.overflow_hidden]}>
|
||||||
|
<View>
|
||||||
|
<TextField.LabelText>
|
||||||
|
<Trans>Enter the domain you want to use</Trans>
|
||||||
|
</TextField.LabelText>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Icon icon={AtIcon} />
|
||||||
|
<Dialog.Input
|
||||||
|
label={_(msg`New handle`)}
|
||||||
|
placeholder={_(msg`e.g. alice.com`)}
|
||||||
|
editable={!isPending}
|
||||||
|
defaultValue={domain}
|
||||||
|
onChangeText={text => {
|
||||||
|
setDomain(text)
|
||||||
|
resetVerification()
|
||||||
|
}}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
</View>
|
||||||
|
<ToggleButton.Group
|
||||||
|
label={_(msg`Choose domain verification method`)}
|
||||||
|
values={[dnsPanel ? 'dns' : 'file']}
|
||||||
|
onChange={values => setDNSPanel(values[0] === 'dns')}>
|
||||||
|
<ToggleButton.Button name="dns" label={_(msg`DNS Panel`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>DNS Panel</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="file" label={_(msg`No DNS Panel`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>No DNS Panel</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
</ToggleButton.Group>
|
||||||
|
{dnsPanel ? (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<Trans>Add the following DNS record to your domain:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Text style={[t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Host:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.py_xs]}>
|
||||||
|
<CopyButton
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
value="_atproto"
|
||||||
|
label={_(msg`Copy host`)}
|
||||||
|
hoverStyle={[a.bg_transparent]}
|
||||||
|
hitSlop={HITSLOP_10}>
|
||||||
|
<Text style={[a.text_md, a.flex_1]}>_atproto</Text>
|
||||||
|
<ButtonIcon icon={CopyIcon} />
|
||||||
|
</CopyButton>
|
||||||
|
</View>
|
||||||
|
<Text style={[a.mt_xs, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Type:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.py_xs]}>
|
||||||
|
<Text style={[a.text_md]}>TXT</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[a.mt_xs, t.atoms.text_contrast_medium]}>
|
||||||
|
<Trans>Value:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.py_xs]}>
|
||||||
|
<CopyButton
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
value={'did=' + currentAccount?.did}
|
||||||
|
label={_(msg`Copy TXT record value`)}
|
||||||
|
hoverStyle={[a.bg_transparent]}
|
||||||
|
hitSlop={HITSLOP_10}>
|
||||||
|
<Text style={[a.text_md, a.flex_1]}>
|
||||||
|
did={currentAccount?.did}
|
||||||
|
</Text>
|
||||||
|
<ButtonIcon icon={CopyIcon} />
|
||||||
|
</CopyButton>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text>
|
||||||
|
<Trans>This should create a domain record at:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md]}>_atproto.{domain}</Text>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text>
|
||||||
|
<Trans>Upload a text file to:</Trans>
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.p_md,
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<Text style={[a.text_md]}>
|
||||||
|
https://{domain}/.well-known/atproto-did
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text>
|
||||||
|
<Trans>That contains the following:</Trans>
|
||||||
|
</Text>
|
||||||
|
<CopyButton
|
||||||
|
value={currentAccount?.did ?? ''}
|
||||||
|
label={_(msg`Copy DID`)}
|
||||||
|
size="large"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
style={[a.px_md, a.border, t.atoms.border_contrast_low]}>
|
||||||
|
<Text style={[a.text_md, a.flex_1]}>{currentAccount?.did}</Text>
|
||||||
|
<ButtonIcon icon={CopyIcon} />
|
||||||
|
</CopyButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
{isVerified && (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn}
|
||||||
|
exiting={FadeOut}
|
||||||
|
layout={native(LinearTransition)}>
|
||||||
|
<SuccessMessage text={_(msg`Domain verified!`)} />
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Animated.View layout={native(LinearTransition)}>
|
||||||
|
<Button
|
||||||
|
label={
|
||||||
|
isVerified
|
||||||
|
? _(msg`Update to ${domain}`)
|
||||||
|
: dnsPanel
|
||||||
|
? _(msg`Verify DNS Record`)
|
||||||
|
: _(msg`Verify Text File`)
|
||||||
|
}
|
||||||
|
variant="solid"
|
||||||
|
size="large"
|
||||||
|
color={domain.trim().length > 0 ? 'primary' : 'secondary'}
|
||||||
|
disabled={domain.trim().length === 0}
|
||||||
|
onPress={() => {
|
||||||
|
if (isVerified) {
|
||||||
|
changeHandle({handle: domain})
|
||||||
|
} else {
|
||||||
|
verify()
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{isPending || isVerifyPending ? (
|
||||||
|
<ButtonIcon icon={Loader} />
|
||||||
|
) : (
|
||||||
|
<ButtonText>
|
||||||
|
{isVerified ? (
|
||||||
|
<Trans>Update to {domain}</Trans>
|
||||||
|
) : dnsPanel ? (
|
||||||
|
<Trans>Verify DNS Record</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Verify Text File</Trans>
|
||||||
|
)}
|
||||||
|
</ButtonText>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Animated.View>
|
||||||
|
<Animated.View layout={native(LinearTransition)}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Use default provider`)}
|
||||||
|
accessibilityHint={_(msg`Go back to previous page`)}
|
||||||
|
onPress={goToServiceHandle}
|
||||||
|
style={[a.p_0, a.justify_start]}>
|
||||||
|
<ButtonText style={[{color: t.palette.primary_500}, a.text_left]}>
|
||||||
|
<Trans>Nevermind, create a handle for me</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DidMismatchError extends Error {
|
||||||
|
did: string
|
||||||
|
constructor(did: string) {
|
||||||
|
super('DID mismatch')
|
||||||
|
this.name = 'DidMismatchError'
|
||||||
|
this.did = did
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChangeHandleError({error}: {error: unknown}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
let message = _(msg`Failed to change handle. Please try again.`)
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
if (error.message.startsWith('Handle already taken')) {
|
||||||
|
message = _(msg`Handle already taken. Please try a different one.`)
|
||||||
|
} else if (error.message === 'Reserved handle') {
|
||||||
|
message = _(msg`This handle is reserved. Please try a different one.`)
|
||||||
|
} else if (error.message === 'Handle too long') {
|
||||||
|
message = _(msg`Handle too long. Please try a shorter one.`)
|
||||||
|
} else if (error.message === 'Input/handle must be a valid handle') {
|
||||||
|
message = _(msg`Invalid handle. Please try a different one.`)
|
||||||
|
} else if (error.message === 'Rate Limit Exceeded') {
|
||||||
|
message = _(
|
||||||
|
msg`Rate limit exceeded – you've tried to change your handle too many times in a short period. Please wait a minute before trying again.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Admonition type="error">{message}</Admonition>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SuccessMessage({text}: {text: string}) {
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
|
const t = useTheme()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.gap_md,
|
||||||
|
a.flex_row,
|
||||||
|
a.justify_center,
|
||||||
|
a.align_center,
|
||||||
|
gtMobile ? a.px_md : a.px_sm,
|
||||||
|
a.py_xs,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{height: 20, width: 20},
|
||||||
|
a.rounded_full,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
{backgroundColor: t.palette.positive_600},
|
||||||
|
]}>
|
||||||
|
<CheckIcon fill={t.palette.white} size="xs" />
|
||||||
|
</View>
|
||||||
|
<Text style={[a.text_md]}>{text}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import React, {useCallback, useEffect, useState} from 'react'
|
||||||
|
import {GestureResponderEvent, View} from 'react-native'
|
||||||
|
import Animated, {FadeOutUp, ZoomIn} from 'react-native-reanimated'
|
||||||
|
import * as Clipboard from 'expo-clipboard'
|
||||||
|
import {Trans} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonProps} from '#/components/Button'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function CopyButton({
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
onPress: onPressProp,
|
||||||
|
...props
|
||||||
|
}: ButtonProps & {value: string}) {
|
||||||
|
const [hasBeenCopied, setHasBeenCopied] = useState(false)
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasBeenCopied) {
|
||||||
|
const timeout = setTimeout(() => setHasBeenCopied(false), 100)
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [hasBeenCopied])
|
||||||
|
|
||||||
|
const onPress = useCallback(
|
||||||
|
(evt: GestureResponderEvent) => {
|
||||||
|
Clipboard.setStringAsync(value)
|
||||||
|
setHasBeenCopied(true)
|
||||||
|
onPressProp?.(evt)
|
||||||
|
},
|
||||||
|
[value, onPressProp],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.relative]}>
|
||||||
|
{hasBeenCopied && (
|
||||||
|
<Animated.View
|
||||||
|
entering={ZoomIn.duration(100)}
|
||||||
|
exiting={FadeOutUp.duration(2000)}
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{bottom: '100%', right: 0},
|
||||||
|
a.justify_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.z_10,
|
||||||
|
a.pb_sm,
|
||||||
|
]}
|
||||||
|
pointerEvents="none">
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.font_bold,
|
||||||
|
a.text_right,
|
||||||
|
a.text_md,
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
]}>
|
||||||
|
<Trans>Copied!</Trans>
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
style={[a.flex_1, a.justify_between, style]}
|
||||||
|
onPress={onPress}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import React, {useContext, useMemo} from 'react'
|
|||||||
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
|
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||||
import * as Button from '#/components/Button'
|
import * as Button from '#/components/Button'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon} from '#/components/icons/Chevron'
|
||||||
import {Link, LinkProps} from '#/components/Link'
|
import {Link, LinkProps} from '#/components/Link'
|
||||||
@@ -17,7 +17,7 @@ const ItemContext = React.createContext({
|
|||||||
const Portal = createPortalGroup()
|
const Portal = createPortalGroup()
|
||||||
|
|
||||||
export function Container({children}: {children: React.ReactNode}) {
|
export function Container({children}: {children: React.ReactNode}) {
|
||||||
return <View style={[a.flex_1, a.py_lg]}>{children}</View>
|
return <View style={[a.flex_1, a.py_md]}>{children}</View>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,11 +241,17 @@ export function ItemText({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Divider() {
|
export function Divider({style}: ViewStyleProp) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[a.border_t, t.atoms.border_contrast_medium, a.w_full, a.my_sm]}
|
style={[
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_medium,
|
||||||
|
a.w_full,
|
||||||
|
a.my_sm,
|
||||||
|
style,
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ export function useFetchHandle() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateHandleMutation() {
|
export function useUpdateHandleMutation(opts?: {
|
||||||
|
onSuccess?: (handle: string) => void
|
||||||
|
}) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ export function useUpdateHandleMutation() {
|
|||||||
await agent.updateHandle({handle})
|
await agent.updateHandle({handle})
|
||||||
},
|
},
|
||||||
onSuccess(_data, variables) {
|
onSuccess(_data, variables) {
|
||||||
|
opts?.onSuccess?.(variables.handle)
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: fetchHandleQueryKey(variables.handle),
|
queryKey: fetchHandleQueryKey(variables.handle),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
|
|||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
|
||||||
export function useNotificationsSettingsMutation() {
|
export function useNotificationSettingsMutation() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
@@ -28,14 +29,17 @@ import {Text} from '#/view/com/util/text/Text'
|
|||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
|
import {AppPasswordsScreen as NewAppPasswordsScreen} from '#/screens/Settings/AppPasswords'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
|
||||||
export function AppPasswords({}: Props) {
|
export function AppPasswords(props: Props) {
|
||||||
return (
|
return IS_INTERNAL ? (
|
||||||
|
<NewAppPasswordsScreen {...props} />
|
||||||
|
) : (
|
||||||
<Layout.Screen testID="AppPasswordsScreen">
|
<Layout.Screen testID="AppPasswordsScreen">
|
||||||
<AppPasswordsInner />
|
<AppPasswordsInner />
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
||||||
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
@@ -22,11 +23,20 @@ import {Button} from '#/view/com/util/forms/Button'
|
|||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
import {CenteredView} from '#/view/com/util/Views'
|
||||||
|
import {LanguageSettingsScreen as NewLanguageSettingsScreen} from '#/screens/Settings/LanguageSettings'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
|
||||||
|
|
||||||
export function LanguageSettingsScreen(_props: Props) {
|
export function LanguageSettingsScreen(props: Props) {
|
||||||
|
return IS_INTERNAL ? (
|
||||||
|
<NewLanguageSettingsScreen {...props} />
|
||||||
|
) : (
|
||||||
|
<LegacyLanguageSettingsScreen {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyLanguageSettingsScreen(_props: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View} from 'react-native'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {AllNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
|
||||||
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
|
|
||||||
import {useNotificationsSettingsMutation} from '#/state/queries/notifications/settings'
|
|
||||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
|
||||||
import {Admonition} from '#/components/Admonition'
|
|
||||||
import {Error} from '#/components/Error'
|
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
|
||||||
import * as Layout from '#/components/Layout'
|
|
||||||
import {Loader} from '#/components/Loader'
|
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationsSettings'>
|
|
||||||
export function NotificationsSettingsScreen({}: Props) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
const t = useTheme()
|
|
||||||
|
|
||||||
const {data, isError: isQueryError, refetch} = useNotificationFeedQuery()
|
|
||||||
const serverPriority = data?.pages.at(0)?.priority
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutate: onChangePriority,
|
|
||||||
isPending: isMutationPending,
|
|
||||||
variables,
|
|
||||||
} = useNotificationsSettingsMutation()
|
|
||||||
|
|
||||||
const priority = isMutationPending
|
|
||||||
? variables[0] === 'enabled'
|
|
||||||
: serverPriority
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Layout.Screen>
|
|
||||||
<ScrollView stickyHeaderIndices={[0]}>
|
|
||||||
<ViewHeader
|
|
||||||
title={_(msg`Notification Settings`)}
|
|
||||||
showOnDesktop
|
|
||||||
showBorder
|
|
||||||
/>
|
|
||||||
{isQueryError ? (
|
|
||||||
<Error
|
|
||||||
title={_(msg`Oops!`)}
|
|
||||||
message={_(msg`Something went wrong!`)}
|
|
||||||
onRetry={refetch}
|
|
||||||
sideBorders={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<View style={[a.p_lg, a.gap_md]}>
|
|
||||||
<Text style={[a.text_lg, a.font_bold]}>
|
|
||||||
<FontAwesomeIcon icon="flask" style={t.atoms.text} />{' '}
|
|
||||||
<Trans>Notification filters</Trans>
|
|
||||||
</Text>
|
|
||||||
<Toggle.Group
|
|
||||||
label={_(msg`Priority notifications`)}
|
|
||||||
type="checkbox"
|
|
||||||
values={priority ? ['enabled'] : []}
|
|
||||||
onChange={onChangePriority}
|
|
||||||
disabled={typeof priority !== 'boolean' || isMutationPending}>
|
|
||||||
<View>
|
|
||||||
<Toggle.Item
|
|
||||||
name="enabled"
|
|
||||||
label={_(msg`Enable priority notifications`)}
|
|
||||||
style={[a.justify_between, a.py_sm]}>
|
|
||||||
<Toggle.LabelText>
|
|
||||||
<Trans>Enable priority notifications</Trans>
|
|
||||||
</Toggle.LabelText>
|
|
||||||
{!data ? <Loader size="md" /> : <Toggle.Platform />}
|
|
||||||
</Toggle.Item>
|
|
||||||
</View>
|
|
||||||
</Toggle.Group>
|
|
||||||
<Admonition type="warning" style={[a.mt_sm]}>
|
|
||||||
<Trans>
|
|
||||||
Experimental: When this preference is enabled, you'll only
|
|
||||||
receive reply and quote notifications from users you follow.
|
|
||||||
We'll continue to add more controls here over time.
|
|
||||||
</Trans>
|
|
||||||
</Admonition>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</Layout.Screen>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ import {StyleSheet, View} from 'react-native'
|
|||||||
import {Trans} from '@lingui/macro'
|
import {Trans} from '@lingui/macro'
|
||||||
import {useFocusEffect} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
@@ -19,6 +20,7 @@ import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
|
|||||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPreferences'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
@@ -26,7 +28,15 @@ type Props = NativeStackScreenProps<
|
|||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
'PreferencesExternalEmbeds'
|
'PreferencesExternalEmbeds'
|
||||||
>
|
>
|
||||||
export function PreferencesExternalEmbeds({}: Props) {
|
export function PreferencesExternalEmbeds(props: Props) {
|
||||||
|
return IS_INTERNAL ? (
|
||||||
|
<ExternalMediaPreferencesScreen {...props} />
|
||||||
|
) : (
|
||||||
|
<LegacyPreferencesExternalEmbeds {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyPreferencesExternalEmbeds({}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const {isTabletOrMobile} = useWebMediaQueries()
|
const {isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
@@ -16,6 +17,7 @@ import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
|
|||||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
@@ -23,7 +25,15 @@ type Props = NativeStackScreenProps<
|
|||||||
CommonNavigatorParams,
|
CommonNavigatorParams,
|
||||||
'PreferencesFollowingFeed'
|
'PreferencesFollowingFeed'
|
||||||
>
|
>
|
||||||
export function PreferencesFollowingFeed({}: Props) {
|
export function PreferencesFollowingFeed(props: Props) {
|
||||||
|
return IS_INTERNAL ? (
|
||||||
|
<FollowingFeedPreferencesScreen {...props} />
|
||||||
|
) : (
|
||||||
|
<LegacyPreferencesFollowingFeed {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyPreferencesFollowingFeed({}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {isTabletOrMobile} = useWebMediaQueries()
|
const {isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {IS_INTERNAL} from '#/lib/app-info'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||||
@@ -17,11 +18,20 @@ import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
|
|||||||
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
import {SimpleViewHeader} from '#/view/com/util/SimpleViewHeader'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {ScrollView} from '#/view/com/util/Views'
|
import {ScrollView} from '#/view/com/util/Views'
|
||||||
|
import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
|
||||||
export function PreferencesThreads({}: Props) {
|
export function PreferencesThreads(props: Props) {
|
||||||
|
return IS_INTERNAL ? (
|
||||||
|
<ThreadPreferencesScreen {...props} />
|
||||||
|
) : (
|
||||||
|
<LegacyPreferencesThreads {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyPreferencesThreads({}: Props) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {isTabletOrMobile} = useWebMediaQueries()
|
const {isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
|||||||
Reference in New Issue
Block a user