diff options
Diffstat (limited to 'src/view/com/modals')
-rw-r--r-- | src/view/com/modals/Confirm.tsx | 7 | ||||
-rw-r--r-- | src/view/com/modals/DeleteAccount.tsx | 210 | ||||
-rw-r--r-- | src/view/com/modals/EditProfile.tsx | 51 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 33 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 18 | ||||
-rw-r--r-- | src/view/com/modals/ReportAccount.tsx | 30 | ||||
-rw-r--r-- | src/view/com/modals/ReportPost.tsx | 37 | ||||
-rw-r--r-- | src/view/com/modals/ServerInput.tsx | 13 | ||||
-rw-r--r-- | src/view/com/modals/crop-image/CropImage.web.tsx | 8 |
9 files changed, 349 insertions, 58 deletions
diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx index 3e2ad6eea..60c104f99 100644 --- a/src/view/com/modals/Confirm.tsx +++ b/src/view/com/modals/Confirm.tsx @@ -7,9 +7,10 @@ import { } from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors, gradients} from '../../lib/styles' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' export const snapPoints = ['50%'] @@ -33,7 +34,7 @@ export function Component({ store.shell.closeModal() return } catch (e: any) { - setError(e.toString()) + setError(cleanError(e)) setIsProcessing(false) } } diff --git a/src/view/com/modals/DeleteAccount.tsx b/src/view/com/modals/DeleteAccount.tsx new file mode 100644 index 000000000..de29e728d --- /dev/null +++ b/src/view/com/modals/DeleteAccount.tsx @@ -0,0 +1,210 @@ +import React from 'react' +import { + ActivityIndicator, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native' +import {BottomSheetTextInput} from '@gorhom/bottom-sheet' +import LinearGradient from 'react-native-linear-gradient' +import * as Toast from '../util/Toast' +import {Text} from '../util/text/Text' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' +import {usePalette} from 'lib/hooks/usePalette' +import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' + +export const snapPoints = ['60%'] + +export function Component({}: {}) { + const pal = usePalette('default') + const store = useStores() + const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false) + const [confirmCode, setConfirmCode] = React.useState<string>('') + const [password, setPassword] = React.useState<string>('') + const [isProcessing, setIsProcessing] = React.useState<boolean>(false) + const [error, setError] = React.useState<string>('') + const onPressSendEmail = async () => { + setError('') + setIsProcessing(true) + try { + await store.api.com.atproto.account.requestDelete() + setIsEmailSent(true) + } catch (e: any) { + setError(cleanError(e)) + } + setIsProcessing(false) + } + const onPressConfirmDelete = async () => { + setError('') + setIsProcessing(true) + try { + await store.api.com.atproto.account.delete({ + did: store.me.did, + password, + token: confirmCode, + }) + Toast.show('Your account has been deleted') + store.nav.tab.fixedTabReset() + store.session.clear() + store.shell.closeModal() + } catch (e: any) { + setError(cleanError(e)) + } + setIsProcessing(false) + } + const onCancel = () => { + store.shell.closeModal() + } + return ( + <View + style={[styles.container, {backgroundColor: pal.colors.backgroundLight}]}> + <View style={[styles.innerContainer, pal.view]}> + <Text type="title-xl" style={[styles.title, pal.text]}> + Delete account + </Text> + {!isEmailSent ? ( + <> + <Text type="lg" style={[styles.description, pal.text]}> + For security reasons, we'll need to send a confirmation code to + your email. + </Text> + {error ? ( + <View style={s.mt10}> + <ErrorMessage message={error} /> + </View> + ) : undefined} + {isProcessing ? ( + <View style={[styles.btn, s.mt10]}> + <ActivityIndicator /> + </View> + ) : ( + <> + <TouchableOpacity + style={styles.mt20} + onPress={onPressSendEmail}> + <LinearGradient + colors={[ + gradients.blueLight.start, + gradients.blueLight.end, + ]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.btn]}> + <Text type="button-lg" style={[s.white, s.bold]}> + Send email + </Text> + </LinearGradient> + </TouchableOpacity> + <TouchableOpacity + style={[styles.btn, s.mt10]} + onPress={onCancel}> + <Text type="button-lg" style={pal.textLight}> + Cancel + </Text> + </TouchableOpacity> + </> + )} + </> + ) : ( + <> + <Text type="lg" style={styles.description}> + Check your inbox for an email with the confirmation code to enter + below: + </Text> + <BottomSheetTextInput + style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]} + placeholder="Confirmation code" + placeholderTextColor={pal.textLight.color} + value={confirmCode} + onChangeText={setConfirmCode} + /> + <Text type="lg" style={styles.description}> + Please enter your password as well: + </Text> + <BottomSheetTextInput + style={[styles.textInput, pal.borderDark, pal.text]} + placeholder="Password" + placeholderTextColor={pal.textLight.color} + secureTextEntry + value={password} + onChangeText={setPassword} + /> + {error ? ( + <View style={styles.mt20}> + <ErrorMessage message={error} /> + </View> + ) : undefined} + {isProcessing ? ( + <View style={[styles.btn, s.mt10]}> + <ActivityIndicator /> + </View> + ) : ( + <> + <TouchableOpacity + style={[styles.btn, styles.evilBtn, styles.mt20]} + onPress={onPressConfirmDelete}> + <Text type="button-lg" style={[s.white, s.bold]}> + Delete my account + </Text> + </TouchableOpacity> + <TouchableOpacity + style={[styles.btn, s.mt10]} + onPress={onCancel}> + <Text type="button-lg" style={pal.textLight}> + Cancel + </Text> + </TouchableOpacity> + </> + )} + </> + )} + </View> + </View> + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + innerContainer: { + paddingBottom: 20, + }, + title: { + textAlign: 'center', + marginTop: 12, + marginBottom: 12, + }, + description: { + textAlign: 'center', + paddingHorizontal: 22, + marginBottom: 10, + }, + mt20: { + marginTop: 20, + }, + mb20: { + marginBottom: 20, + }, + textInput: { + borderWidth: 1, + borderRadius: 6, + paddingHorizontal: 16, + paddingVertical: 12, + fontSize: 20, + marginHorizontal: 20, + }, + btn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderRadius: 32, + padding: 14, + marginHorizontal: 20, + }, + evilBtn: { + backgroundColor: colors.red4, + }, +}) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 12b72a399..add75e89e 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -11,18 +11,17 @@ import {ScrollView, TextInput} from './util' import {PickedMedia} from '../util/images/image-crop-picker/ImageCropPicker' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' -import {useStores} from '../../../state' -import {ProfileViewModel} from '../../../state/models/profile-view' -import {s, colors, gradients} from '../../lib/styles' -import { - enforceLen, - MAX_DISPLAY_NAME, - MAX_DESCRIPTION, -} from '../../../lib/strings' -import {isNetworkError} from '../../../lib/errors' -import {compressIfNeeded} from '../../../lib/images' +import {useStores} from 'state/index' +import {ProfileViewModel} from 'state/models/profile-view' +import {s, colors, gradients} from 'lib/styles' +import {enforceLen} from 'lib/strings/helpers' +import {MAX_DISPLAY_NAME, MAX_DESCRIPTION} from 'lib/constants' +import {compressIfNeeded} from 'lib/images' import {UserBanner} from '../util/UserBanner' import {UserAvatar} from '../util/UserAvatar' +import {usePalette} from 'lib/hooks/usePalette' +import {useAnalytics} from 'lib/analytics' +import {cleanError, isNetworkError} from 'lib/strings/errors' export const snapPoints = ['80%'] @@ -35,6 +34,9 @@ export function Component({ }) { const store = useStores() const [error, setError] = useState<string>('') + const pal = usePalette('default') + const {track} = useAnalytics() + const [isProcessing, setProcessing] = useState<boolean>(false) const [displayName, setDisplayName] = useState<string>( profileView.displayName || '', @@ -54,24 +56,27 @@ export function Component({ store.shell.closeModal() } const onSelectNewAvatar = async (img: PickedMedia) => { + track('EditProfile:AvatarSelected') try { - const finalImg = await compressIfNeeded(img, 300000) - setNewUserAvatar(finalImg) + const finalImg = await compressIfNeeded(img, 1000000) + setNewUserAvatar({mediaType: 'photo', ...finalImg}) setUserAvatar(finalImg.path) } catch (e: any) { - setError(e.message || e.toString()) + setError(cleanError(e)) } } const onSelectNewBanner = async (img: PickedMedia) => { + track('EditProfile:BannerSelected') try { - const finalImg = await compressIfNeeded(img, 500000) - setNewUserBanner(finalImg) + const finalImg = await compressIfNeeded(img, 1000000) + setNewUserBanner({mediaType: 'photo', ...finalImg}) setUserBanner(finalImg.path) } catch (e: any) { - setError(e.message || e.toString()) + setError(cleanError(e)) } } const onPressSave = async () => { + track('EditProfile:Save') setProcessing(true) if (error) { setError('') @@ -94,7 +99,7 @@ export function Component({ 'Failed to save your profile. Check your internet connection and try again.', ) } else { - setError(e.message) + setError(cleanError(e)) } } setProcessing(false) @@ -103,13 +108,13 @@ export function Component({ return ( <View style={s.flex1}> <ScrollView style={styles.inner}> - <Text style={styles.title}>Edit my profile</Text> + <Text style={[styles.title, pal.text]}>Edit my profile</Text> <View style={styles.photos}> <UserBanner banner={userBanner} onSelectNewBanner={onSelectNewBanner} /> - <View style={styles.avi}> + <View style={[styles.avi, {borderColor: pal.colors.background}]}> <UserAvatar size={80} avatar={userAvatar} @@ -127,7 +132,7 @@ export function Component({ <View> <Text style={styles.label}>Display Name</Text> <TextInput - style={styles.textInput} + style={[styles.textInput, pal.text]} placeholder="e.g. Alice Roberts" placeholderTextColor={colors.gray4} value={displayName} @@ -135,9 +140,9 @@ export function Component({ /> </View> <View style={s.pb10}> - <Text style={styles.label}>Description</Text> + <Text style={[styles.label, pal.text]}>Description</Text> <TextInput - style={[styles.textArea]} + style={[styles.textArea, pal.text]} placeholder="e.g. Artist, dog-lover, and memelord." placeholderTextColor={colors.gray4} multiline @@ -162,7 +167,7 @@ export function Component({ )} <TouchableOpacity style={s.mt5} onPress={onPressCancel}> <View style={[styles.btn]}> - <Text style={[s.black, s.bold]}>Cancel</Text> + <Text style={[s.black, s.bold, pal.text]}>Cancel</Text> </View> </TouchableOpacity> </ScrollView> diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index d0b40d56d..2529d0d5b 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -2,23 +2,26 @@ import React, {useRef, useEffect} from 'react' import {View} from 'react-native' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' -import {useStores} from '../../../state' +import {useStores} from 'state/index' import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' -import * as models from '../../../state/models/shell-ui' +import * as models from 'state/models/shell-ui' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ServerInputModal from './ServerInput' import * as ReportPostModal from './ReportPost' import * as ReportAccountModal from './ReportAccount' +import * as DeleteAccountModal from './DeleteAccount' +import {usePalette} from 'lib/hooks/usePalette' +import {StyleSheet} from 'react-native' const CLOSED_SNAPPOINTS = ['10%'] export const Modal = observer(function Modal() { const store = useStores() const bottomSheetRef = useRef<BottomSheet>(null) - + const pal = usePalette('default') const onBottomSheetChange = (snapPoint: number) => { if (snapPoint === -1) { store.shell.closeModal() @@ -62,10 +65,21 @@ export const Modal = observer(function Modal() { ) } else if (store.shell.activeModal?.name === 'report-post') { snapPoints = ReportPostModal.snapPoints - element = <ReportPostModal.Component /> + element = ( + <ReportPostModal.Component + {...(store.shell.activeModal as models.ReportPostModal)} + /> + ) } else if (store.shell.activeModal?.name === 'report-account') { snapPoints = ReportAccountModal.snapPoints - element = <ReportAccountModal.Component /> + element = ( + <ReportAccountModal.Component + {...(store.shell.activeModal as models.ReportAccountModal)} + /> + ) + } else if (store.shell.activeModal?.name === 'delete-account') { + snapPoints = DeleteAccountModal.snapPoints + element = <DeleteAccountModal.Component /> } else { element = <View /> } @@ -80,8 +94,17 @@ export const Modal = observer(function Modal() { backdropComponent={ store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined } + handleIndicatorStyle={{backgroundColor: pal.text.color}} + handleStyle={[styles.handle, pal.view]} onChange={onBottomSheetChange}> {element} </BottomSheet> ) }) + +const styles = StyleSheet.create({ + handle: { + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + }, +}) diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 44ea95f07..3c6551093 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -1,10 +1,10 @@ import React from 'react' import {TouchableWithoutFeedback, StyleSheet, View} from 'react-native' import {observer} from 'mobx-react-lite' -import {useStores} from '../../../state' -import {usePalette} from '../../lib/hooks/usePalette' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' -import * as models from '../../../state/models/shell-ui' +import * as models from 'state/models/shell-ui' import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' @@ -48,9 +48,17 @@ export const Modal = observer(function Modal() { /> ) } else if (store.shell.activeModal?.name === 'report-post') { - element = <ReportPostModal.Component /> + element = ( + <ReportPostModal.Component + {...(store.shell.activeModal as models.ReportPostModal)} + /> + ) } else if (store.shell.activeModal?.name === 'report-account') { - element = <ReportAccountModal.Component /> + element = ( + <ReportAccountModal.Component + {...(store.shell.activeModal as models.ReportAccountModal)} + /> + ) } else if (store.shell.activeModal?.name === 'crop-image') { element = ( <CropImageModal.Component diff --git a/src/view/com/modals/ReportAccount.tsx b/src/view/com/modals/ReportAccount.tsx index 1385d5711..377a32838 100644 --- a/src/view/com/modals/ReportAccount.tsx +++ b/src/view/com/modals/ReportAccount.tsx @@ -5,12 +5,15 @@ import { TouchableOpacity, View, } from 'react-native' +import {ComAtprotoReportReasonType} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' -import {useStores} from '../../../state' -import {s, colors, gradients} from '../../lib/styles' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup' import {Text} from '../util/text/Text' +import * as Toast from '../util/Toast' import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' const ITEMS: RadioGroupItem[] = [ {key: 'spam', label: 'Spam or excessive repeat posts'}, @@ -20,7 +23,7 @@ const ITEMS: RadioGroupItem[] = [ export const snapPoints = ['50%'] -export function Component() { +export function Component({did}: {did: string}) { const store = useStores() const [isProcessing, setIsProcessing] = useState<boolean>(false) const [error, setError] = useState<string>('') @@ -28,13 +31,30 @@ export function Component() { const onSelectIssue = (v: string) => setIssue(v) const onPress = async () => { setError('') + if (!issue) { + return + } setIsProcessing(true) try { - // TODO + // NOTE: we should update the lexicon of reasontype to include more options -prf + let reasonType = ComAtprotoReportReasonType.OTHER + if (issue === 'spam') { + reasonType = ComAtprotoReportReasonType.SPAM + } + const reason = ITEMS.find(item => item.key === issue)?.label || '' + await store.api.com.atproto.report.create({ + reasonType, + reason, + subject: { + $type: 'com.atproto.repo.repoRef', + did, + }, + }) + Toast.show("Thank you for your report! We'll look into it promptly.") store.shell.closeModal() return } catch (e: any) { - setError(e.toString()) + setError(cleanError(e)) setIsProcessing(false) } } diff --git a/src/view/com/modals/ReportPost.tsx b/src/view/com/modals/ReportPost.tsx index 8a3a1f758..3d47e7ef0 100644 --- a/src/view/com/modals/ReportPost.tsx +++ b/src/view/com/modals/ReportPost.tsx @@ -5,12 +5,15 @@ import { TouchableOpacity, View, } from 'react-native' +import {ComAtprotoReportReasonType} from '@atproto/api' import LinearGradient from 'react-native-linear-gradient' -import {useStores} from '../../../state' -import {s, colors, gradients} from '../../lib/styles' +import {useStores} from 'state/index' +import {s, colors, gradients} from 'lib/styles' import {RadioGroup, RadioGroupItem} from '../util/forms/RadioGroup' import {Text} from '../util/text/Text' +import * as Toast from '../util/Toast' import {ErrorMessage} from '../util/error/ErrorMessage' +import {cleanError} from 'lib/strings/errors' const ITEMS: RadioGroupItem[] = [ {key: 'spam', label: 'Spam or excessive repeat posts'}, @@ -21,7 +24,13 @@ const ITEMS: RadioGroupItem[] = [ export const snapPoints = ['50%'] -export function Component() { +export function Component({ + postUri, + postCid, +}: { + postUri: string + postCid: string +}) { const store = useStores() const [isProcessing, setIsProcessing] = useState<boolean>(false) const [error, setError] = useState<string>('') @@ -29,13 +38,31 @@ export function Component() { const onSelectIssue = (v: string) => setIssue(v) const onPress = async () => { setError('') + if (!issue) { + return + } setIsProcessing(true) try { - // TODO + // NOTE: we should update the lexicon of reasontype to include more options -prf + let reasonType = ComAtprotoReportReasonType.OTHER + if (issue === 'spam') { + reasonType = ComAtprotoReportReasonType.SPAM + } + const reason = ITEMS.find(item => item.key === issue)?.label || '' + await store.api.com.atproto.report.create({ + reasonType, + reason, + subject: { + $type: 'com.atproto.repo.recordRef', + uri: postUri, + cid: postCid, + }, + }) + Toast.show("Thank you for your report! We'll look into it promptly.") store.shell.closeModal() return } catch (e: any) { - setError(e.toString()) + setError(cleanError(e)) setIsProcessing(false) } } diff --git a/src/view/com/modals/ServerInput.tsx b/src/view/com/modals/ServerInput.tsx index dde836719..5a9a4cfed 100644 --- a/src/view/com/modals/ServerInput.tsx +++ b/src/view/com/modals/ServerInput.tsx @@ -6,14 +6,10 @@ import { } from '@fortawesome/react-native-fontawesome' import {ScrollView, TextInput} from './util' import {Text} from '../util/text/Text' -import {useStores} from '../../../state' -import {s, colors} from '../../lib/styles' -import { - LOCAL_DEV_SERVICE, - STAGING_SERVICE, - PROD_SERVICE, -} from '../../../state/index' -import {LOGIN_INCLUDE_DEV_SERVERS} from '../../../build-flags' +import {useStores} from 'state/index' +import {s, colors} from 'lib/styles' +import {LOCAL_DEV_SERVICE, STAGING_SERVICE, PROD_SERVICE} from 'state/index' +import {LOGIN_INCLUDE_DEV_SERVERS} from 'lib/build-flags' export const snapPoints = ['80%'] @@ -37,6 +33,7 @@ export function Component({onSelect}: {onSelect: (url: string) => void}) { {LOGIN_INCLUDE_DEV_SERVERS ? ( <> <TouchableOpacity + testID="localDevServerButton" style={styles.btn} onPress={() => doSelect(LOCAL_DEV_SERVICE)}> <Text style={styles.btnText}>Local dev server</Text> diff --git a/src/view/com/modals/crop-image/CropImage.web.tsx b/src/view/com/modals/crop-image/CropImage.web.tsx index 1f234c4a6..e43f37397 100644 --- a/src/view/com/modals/crop-image/CropImage.web.tsx +++ b/src/view/com/modals/crop-image/CropImage.web.tsx @@ -5,10 +5,10 @@ import {Slider} from '@miblanchard/react-native-slider' import LinearGradient from 'react-native-linear-gradient' import {Text} from '../../util/text/Text' import {PickedMedia} from '../../util/images/image-crop-picker/types' -import {s, gradients} from '../../../lib/styles' -import {useStores} from '../../../../state' -import {usePalette} from '../../../lib/hooks/usePalette' -import {SquareIcon, RectWideIcon, RectTallIcon} from '../../../lib/icons' +import {s, gradients} from 'lib/styles' +import {useStores} from 'state/index' +import {usePalette} from 'lib/hooks/usePalette' +import {SquareIcon, RectWideIcon, RectTallIcon} from 'lib/icons' enum AspectRatio { Square = 'square', |