diff options
Diffstat (limited to 'src/screens')
-rw-r--r-- | src/screens/Moderation/index.tsx | 131 | ||||
-rw-r--r-- | src/screens/Settings/AccountSettings.tsx | 2 | ||||
-rw-r--r-- | src/screens/Settings/components/DisableEmail2FADialog.tsx | 201 | ||||
-rw-r--r-- | src/screens/Settings/components/Email2FAToggle.tsx | 2 | ||||
-rw-r--r-- | src/screens/Settings/components/ExportCarDialog.tsx | 110 |
5 files changed, 313 insertions, 133 deletions
diff --git a/src/screens/Moderation/index.tsx b/src/screens/Moderation/index.tsx index d5a2daffd..5f340cd56 100644 --- a/src/screens/Moderation/index.tsx +++ b/src/screens/Moderation/index.tsx @@ -1,13 +1,11 @@ import React from 'react' import {Linking, View} from 'react-native' import {useSafeAreaFrame} from 'react-native-safe-area-context' -import {ComAtprotoLabelDefs} from '@atproto/api' import {LABELS} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useFocusEffect} from '@react-navigation/native' -import {IS_INTERNAL} from '#/lib/app-info' import {getLabelingServiceTitle} from '#/lib/moderation' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {logger} from '#/logger' @@ -18,11 +16,6 @@ import { UsePreferencesQueryResponse, usePreferencesSetAdultContentMutation, } from '#/state/queries/preferences' -import { - useProfileQuery, - useProfileUpdateMutation, -} from '#/state/queries/profile' -import {useSession} from '#/state/session' import {isNonConfigurableModerationAuthority} from '#/state/session/additional-moderation-authorities' import {useSetMinimalShellMode} from '#/state/shell' import {ViewHeader} from '#/view/com/util/ViewHeader' @@ -469,131 +462,7 @@ export function ModerationScreenInner({ })} </View> )} - - {!IS_INTERNAL && ( - <> - <Text - style={[ - a.text_md, - a.font_bold, - a.pt_2xl, - a.pb_md, - t.atoms.text_contrast_high, - ]}> - <Trans>Logged-out visibility</Trans> - </Text> - - <PwiOptOut /> - </> - )} - <View style={{height: 200}} /> </ScrollView> ) } - -function PwiOptOut() { - const t = useTheme() - const {_} = useLingui() - const {currentAccount} = useSession() - const {data: profile} = useProfileQuery({did: currentAccount?.did}) - const updateProfile = useProfileUpdateMutation() - - const isOptedOut = - profile?.labels?.some(l => l.val === '!no-unauthenticated') || false - const canToggle = profile && !updateProfile.isPending - - const onToggleOptOut = React.useCallback(() => { - if (!profile) { - return - } - let wasAdded = false - updateProfile.mutate({ - profile, - updates: existing => { - // create labels attr if needed - existing.labels = ComAtprotoLabelDefs.isSelfLabels(existing.labels) - ? existing.labels - : { - $type: 'com.atproto.label.defs#selfLabels', - values: [], - } - - // toggle the label - const hasLabel = existing.labels.values.some( - l => l.val === '!no-unauthenticated', - ) - if (hasLabel) { - wasAdded = false - existing.labels.values = existing.labels.values.filter( - l => l.val !== '!no-unauthenticated', - ) - } else { - wasAdded = true - existing.labels.values.push({val: '!no-unauthenticated'}) - } - - // delete if no longer needed - if (existing.labels.values.length === 0) { - delete existing.labels - } - return existing - }, - checkCommitted: res => { - const exists = !!res.data.labels?.some( - l => l.val === '!no-unauthenticated', - ) - return exists === wasAdded - }, - }) - }, [updateProfile, profile]) - - return ( - <View style={[a.pt_sm]}> - <View style={[a.flex_row, a.align_center, a.justify_between, a.gap_lg]}> - <Toggle.Item - disabled={!canToggle} - value={isOptedOut} - onChange={onToggleOptOut} - name="logged_out_visibility" - style={a.flex_1} - label={_( - msg`Discourage apps from showing my account to logged-out users`, - )}> - <Toggle.Switch /> - <Toggle.LabelText style={[a.text_md, a.flex_1]}> - <Trans> - Discourage apps from showing my account to logged-out users - </Trans> - </Toggle.LabelText> - </Toggle.Item> - - {updateProfile.isPending && <Loader />} - </View> - - <View style={[a.pt_md, a.gap_md, {paddingLeft: 38}]}> - <Text style={[a.leading_snug, t.atoms.text_contrast_high]}> - <Trans> - Bluesky will not show your profile and posts to logged-out users. - Other apps may not honor this request. This does not make your - account private. - </Trans> - </Text> - <Text style={[a.font_bold, a.leading_snug, t.atoms.text_contrast_high]}> - <Trans> - Note: Bluesky is an open and public network. This setting only - limits the visibility of your content on the Bluesky app and - website, and other apps may not respect this setting. Your content - may still be shown to logged-out users by other apps and websites. - </Trans> - </Text> - - <InlineLinkText - label={_(msg`Learn more about what is public on Bluesky.`)} - to="https://blueskyweb.zendesk.com/hc/en-us/articles/15835264007693-Data-Privacy"> - <Trans>Learn more about what is public on Bluesky.</Trans> - </InlineLinkText> - </View> - </View> - ) -} diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index f34810a68..35c5f3aa0 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -6,7 +6,6 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams} from '#/lib/routes/types' import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' -import {ExportCarDialog} from '#/view/screens/Settings/ExportCarDialog' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {useDialogControl} from '#/components/Dialog' @@ -24,6 +23,7 @@ import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/ico import * as Layout from '#/components/Layout' import {ChangeHandleDialog} from './components/ChangeHandleDialog' import {DeactivateAccountDialog} from './components/DeactivateAccountDialog' +import {ExportCarDialog} from './components/ExportCarDialog' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'> export function AccountSettingsScreen({}: Props) { diff --git a/src/screens/Settings/components/DisableEmail2FADialog.tsx b/src/screens/Settings/components/DisableEmail2FADialog.tsx new file mode 100644 index 000000000..1378759b0 --- /dev/null +++ b/src/screens/Settings/components/DisableEmail2FADialog.tsx @@ -0,0 +1,201 @@ +import React, {useState} from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {isNative} from '#/platform/detection' +import {useAgent, useSession} from '#/state/session' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' +import {Loader} from '#/components/Loader' +import {P, Text} from '#/components/Typography' + +enum Stages { + Email, + ConfirmCode, +} + +export function DisableEmail2FADialog({ + control, +}: { + control: Dialog.DialogOuterProps['control'] +}) { + const {_} = useLingui() + const t = useTheme() + const {gtMobile} = useBreakpoints() + const {currentAccount} = useSession() + const agent = useAgent() + + const [stage, setStage] = useState<Stages>(Stages.Email) + const [confirmationCode, setConfirmationCode] = useState<string>('') + const [isProcessing, setIsProcessing] = useState<boolean>(false) + const [error, setError] = useState<string>('') + + const onSendEmail = async () => { + setError('') + setIsProcessing(true) + try { + await agent.com.atproto.server.requestEmailUpdate() + setStage(Stages.ConfirmCode) + } catch (e) { + setError(cleanError(String(e))) + } finally { + setIsProcessing(false) + } + } + + const onConfirmDisable = async () => { + setError('') + setIsProcessing(true) + try { + if (currentAccount?.email) { + await agent.com.atproto.server.updateEmail({ + email: currentAccount!.email, + token: confirmationCode.trim(), + emailAuthFactor: false, + }) + await agent.resumeSession(agent.session!) + Toast.show(_(msg`Email 2FA disabled`)) + } + control.close() + } catch (e) { + const errMsg = String(e) + if (errMsg.includes('Token is invalid')) { + setError(_(msg`Invalid 2FA confirmation code.`)) + } else { + setError(cleanError(errMsg)) + } + } finally { + setIsProcessing(false) + } + } + + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + <Dialog.ScrollableInner + accessibilityDescribedBy="dialog-description" + accessibilityLabelledBy="dialog-title"> + <View style={[a.relative, a.gap_md, a.w_full]}> + <Text + nativeID="dialog-title" + style={[a.text_2xl, a.font_bold, t.atoms.text]}> + <Trans>Disable Email 2FA</Trans> + </Text> + <P nativeID="dialog-description"> + {stage === Stages.ConfirmCode ? ( + <Trans> + An email has been sent to{' '} + {currentAccount?.email || '(no email)'}. It includes a + confirmation code which you can enter below. + </Trans> + ) : ( + <Trans> + To disable the email 2FA method, please verify your access to + the email address. + </Trans> + )} + </P> + + {error ? <ErrorMessage message={error} /> : undefined} + + {stage === Stages.Email ? ( + <View + style={[ + a.gap_sm, + gtMobile && [a.flex_row, a.justify_end, a.gap_md], + ]}> + <Button + testID="sendEmailButton" + variant="solid" + color="primary" + size={gtMobile ? 'small' : 'large'} + onPress={onSendEmail} + label={_(msg`Send verification email`)} + disabled={isProcessing}> + <ButtonText> + <Trans>Send verification email</Trans> + </ButtonText> + {isProcessing && <ButtonIcon icon={Loader} />} + </Button> + <Button + testID="haveCodeButton" + variant="ghost" + color="primary" + size={gtMobile ? 'small' : 'large'} + onPress={() => setStage(Stages.ConfirmCode)} + label={_(msg`I have a code`)} + disabled={isProcessing}> + <ButtonText> + <Trans>I have a code</Trans> + </ButtonText> + </Button> + </View> + ) : stage === Stages.ConfirmCode ? ( + <View> + <View style={[a.mb_md]}> + <TextField.LabelText> + <Trans>Confirmation code</Trans> + </TextField.LabelText> + <TextField.Root> + <TextField.Icon icon={Lock} /> + <Dialog.Input + testID="confirmationCode" + label={_(msg`Confirmation code`)} + autoCapitalize="none" + autoFocus + autoCorrect={false} + autoComplete="off" + value={confirmationCode} + onChangeText={setConfirmationCode} + onSubmitEditing={onConfirmDisable} + editable={!isProcessing} + /> + </TextField.Root> + </View> + <View + style={[ + a.gap_sm, + gtMobile && [a.flex_row, a.justify_end, a.gap_md], + ]}> + <Button + testID="resendCodeBtn" + variant="ghost" + color="primary" + size={gtMobile ? 'small' : 'large'} + onPress={onSendEmail} + label={_(msg`Resend email`)} + disabled={isProcessing}> + <ButtonText> + <Trans>Resend email</Trans> + </ButtonText> + </Button> + <Button + testID="confirmBtn" + variant="solid" + color="primary" + size={gtMobile ? 'small' : 'large'} + onPress={onConfirmDisable} + label={_(msg`Confirm`)} + disabled={isProcessing}> + <ButtonText> + <Trans>Confirm</Trans> + </ButtonText> + {isProcessing && <ButtonIcon icon={Loader} />} + </Button> + </View> + </View> + ) : undefined} + + {!gtMobile && isNative && <View style={{height: 40}} />} + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} diff --git a/src/screens/Settings/components/Email2FAToggle.tsx b/src/screens/Settings/components/Email2FAToggle.tsx index 85ae89dea..a74f9fce7 100644 --- a/src/screens/Settings/components/Email2FAToggle.tsx +++ b/src/screens/Settings/components/Email2FAToggle.tsx @@ -4,9 +4,9 @@ import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' import {useAgent, useSession} from '#/state/session' -import {DisableEmail2FADialog} from '#/view/screens/Settings/DisableEmail2FADialog' import {useDialogControl} from '#/components/Dialog' import * as Prompt from '#/components/Prompt' +import {DisableEmail2FADialog} from './DisableEmail2FADialog' import * as SettingsList from './SettingsList' export function Email2FAToggle() { diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx new file mode 100644 index 000000000..2de3895d3 --- /dev/null +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -0,0 +1,110 @@ +import React from 'react' +import {View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {saveBytesToDisk} from '#/lib/media/manip' +import {logger} from '#/logger' +import {useAgent} from '#/state/session' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useTheme} from '#/alf' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import {Download_Stroke2_Corner0_Rounded as DownloadIcon} from '#/components/icons/Download' +import {InlineLinkText} from '#/components/Link' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' + +export function ExportCarDialog({ + control, +}: { + control: Dialog.DialogOuterProps['control'] +}) { + const {_} = useLingui() + const t = useTheme() + const agent = useAgent() + const [loading, setLoading] = React.useState(false) + + const download = React.useCallback(async () => { + if (!agent.session) { + return // shouldnt ever happen + } + try { + setLoading(true) + const did = agent.session.did + const downloadRes = await agent.com.atproto.sync.getRepo({did}) + const saveRes = await saveBytesToDisk( + 'repo.car', + downloadRes.data, + downloadRes.headers['content-type'], + ) + + if (saveRes) { + Toast.show(_(msg`File saved successfully!`)) + } + } catch (e) { + logger.error('Error occurred while downloading CAR file', {message: e}) + Toast.show(_(msg`Error occurred while saving file`), 'xmark') + } finally { + setLoading(false) + control.close() + } + }, [_, control, agent]) + + return ( + <Dialog.Outer control={control}> + <Dialog.Handle /> + <Dialog.ScrollableInner + accessibilityDescribedBy="dialog-description" + accessibilityLabelledBy="dialog-title"> + <View style={[a.relative, a.gap_lg, a.w_full]}> + <Text nativeID="dialog-title" style={[a.text_2xl, a.font_bold]}> + <Trans>Export My Data</Trans> + </Text> + <Text nativeID="dialog-description" style={[a.text_sm]}> + <Trans> + Your account repository, containing all public data records, can + be downloaded as a "CAR" file. This file does not include media + embeds, such as images, or your private data, which must be + fetched separately. + </Trans> + </Text> + + <Button + variant="solid" + color="primary" + size="large" + label={_(msg`Download CAR file`)} + disabled={loading} + onPress={download}> + <ButtonIcon icon={DownloadIcon} /> + <ButtonText> + <Trans>Download CAR file</Trans> + </ButtonText> + {loading && <ButtonIcon icon={Loader} />} + </Button> + + <Text + style={[ + t.atoms.text_contrast_medium, + a.text_sm, + a.leading_snug, + a.flex_1, + ]}> + <Trans> + This feature is in beta. You can read more about repository + exports in{' '} + <InlineLinkText + label={_(msg`View blogpost for more details`)} + to="https://docs.bsky.app/blog/repo-export" + style={[a.text_sm]}> + this blogpost + </InlineLinkText> + . + </Trans> + </Text> + </View> + </Dialog.ScrollableInner> + </Dialog.Outer> + ) +} |