diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/alf/atoms.ts | 6 | ||||
-rw-r--r-- | src/components/Dialog/index.tsx | 4 | ||||
-rw-r--r-- | src/components/Dialog/types.ts | 1 | ||||
-rw-r--r-- | src/components/Prompt.tsx | 91 | ||||
-rw-r--r-- | src/components/dialogs/MutedWords.tsx | 33 | ||||
-rw-r--r-- | src/lib/hooks/useOTAUpdate.ts | 52 | ||||
-rw-r--r-- | src/state/modals/index.tsx | 13 | ||||
-rw-r--r-- | src/view/com/composer/Composer.tsx | 43 | ||||
-rw-r--r-- | src/view/com/feeds/FeedSourceCard.tsx | 254 | ||||
-rw-r--r-- | src/view/com/modals/Confirm.tsx | 132 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 6 | ||||
-rw-r--r-- | src/view/com/modals/Modal.web.tsx | 5 | ||||
-rw-r--r-- | src/view/com/posts/FeedErrorMessage.tsx | 89 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeader.tsx | 47 | ||||
-rw-r--r-- | src/view/com/profile/ProfileMenu.tsx | 86 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 43 | ||||
-rw-r--r-- | src/view/screens/AppPasswords.tsx | 36 | ||||
-rw-r--r-- | src/view/screens/ProfileList.tsx | 198 | ||||
-rw-r--r-- | src/view/screens/Storybook/Dialogs.tsx | 2 |
19 files changed, 538 insertions, 603 deletions
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index fff3a4d8b..6d73cd307 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -111,6 +111,12 @@ export const atoms = { flex_row: { flexDirection: 'row', }, + flex_col_reverse: { + flexDirection: 'column-reverse', + }, + flex_row_reverse: { + flexDirection: 'row-reverse', + }, flex_wrap: { flexWrap: 'wrap', }, diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx index f0e7b7e82..5f6edeac7 100644 --- a/src/components/Dialog/index.tsx +++ b/src/components/Dialog/index.tsx @@ -75,6 +75,7 @@ export function Outer({ control, onClose, nativeOptions, + testID, }: React.PropsWithChildren<DialogOuterProps>) { const t = useTheme() const sheet = React.useRef<BottomSheet>(null) @@ -145,7 +146,8 @@ export function Outer({ accessibilityViewIsModal // Android importantForAccessibility="yes" - style={[a.absolute, a.inset_0]}> + style={[a.absolute, a.inset_0]} + testID={testID}> <BottomSheet enableDynamicSizing={!hasSnapPoints} enablePanDownToClose diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index 4fc60ec39..9e7ad3c04 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -46,6 +46,7 @@ export type DialogOuterProps = { sheet?: Omit<BottomSheetProps, 'children'> } webOptions?: {} + testID?: string } type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 3b245c440..9ffa9e7d8 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -1,11 +1,11 @@ import React from 'react' -import {View, PressableProps} from 'react-native' +import {View} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {Text} from '#/components/Typography' -import {Button} from '#/components/Button' +import {Button, ButtonColor, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -22,8 +22,10 @@ const Context = React.createContext<{ export function Outer({ children, control, + testID, }: React.PropsWithChildren<{ control: Dialog.DialogOuterProps['control'] + testID?: string }>) { const {gtMobile} = useBreakpoints() const titleId = React.useId() @@ -35,7 +37,7 @@ export function Outer({ ) return ( - <Dialog.Outer control={control}> + <Dialog.Outer control={control} testID={testID}> <Context.Provider value={context}> <Dialog.Handle /> @@ -80,7 +82,9 @@ export function Actions({children}: React.PropsWithChildren<{}>) { a.w_full, a.gap_sm, a.justify_end, - gtMobile ? [a.flex_row] : [a.flex_col, a.pt_md, a.pb_4xl], + gtMobile + ? [a.flex_row, a.flex_row_reverse, a.justify_start] + : [a.flex_col, a.pt_md, a.pb_4xl], ]}> {children} </View> @@ -89,18 +93,29 @@ export function Actions({children}: React.PropsWithChildren<{}>) { export function Cancel({ children, -}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { + cta, +}: React.PropsWithChildren<{ + /** + * Optional i18n string, used in lieu of `children` for simple buttons. If + * undefined (and `children` is undefined), it will default to "Cancel". + */ + cta?: string +}>) { const {_} = useLingui() const {gtMobile} = useBreakpoints() const {close} = Dialog.useDialogContext() + const onPress = React.useCallback(() => { + close() + }, [close]) + return ( <Button variant="solid" color="secondary" size={gtMobile ? 'small' : 'medium'} - label={_(msg`Cancel`)} - onPress={() => close()}> - {children} + label={cta || _(msg`Cancel`)} + onPress={onPress}> + {children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>} </Button> ) } @@ -108,22 +123,70 @@ export function Cancel({ export function Action({ children, onPress, -}: React.PropsWithChildren<{onPress?: () => void}>) { + color = 'primary', + cta, + testID, +}: React.PropsWithChildren<{ + onPress: () => void + color?: ButtonColor + /** + * Optional i18n string, used in lieu of `children` for simple buttons. If + * undefined (and `children` is undefined), it will default to "Confirm". + */ + cta?: string + testID?: string +}>) { const {_} = useLingui() const {gtMobile} = useBreakpoints() const {close} = Dialog.useDialogContext() const handleOnPress = React.useCallback(() => { close() - onPress?.() + onPress() }, [close, onPress]) + return ( <Button variant="solid" - color="primary" + color={color} size={gtMobile ? 'small' : 'medium'} - label={_(msg`Confirm`)} - onPress={handleOnPress}> - {children} + label={cta || _(msg`Confirm`)} + onPress={handleOnPress} + testID={testID}> + {children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>} </Button> ) } + +export function Basic({ + control, + title, + description, + cancelButtonCta, + confirmButtonCta, + onConfirm, + confirmButtonColor, +}: React.PropsWithChildren<{ + control: Dialog.DialogOuterProps['control'] + title: string + description: string + cancelButtonCta?: string + confirmButtonCta?: string + onConfirm: () => void + confirmButtonColor?: ButtonColor +}>) { + return ( + <Outer control={control} testID="confirmModal"> + <Title>{title}</Title> + <Description>{description}</Description> + <Actions> + <Action + cta={confirmButtonCta} + onPress={onConfirm} + color={confirmButtonColor} + testID="confirmBtn" + /> + <Cancel cta={cancelButtonCta} /> + </Actions> + </Outer> + ) +} diff --git a/src/components/dialogs/MutedWords.tsx b/src/components/dialogs/MutedWords.tsx index 5f23a7883..dcdaccea3 100644 --- a/src/components/dialogs/MutedWords.tsx +++ b/src/components/dialogs/MutedWords.tsx @@ -277,29 +277,16 @@ function MutedWordRow({ return ( <> - <Prompt.Outer control={control}> - <Prompt.Title> - <Trans>Are you sure?</Trans> - </Prompt.Title> - <Prompt.Description> - <Trans> - This will delete {word.value} from your muted words. You can always - add it back later. - </Trans> - </Prompt.Description> - <Prompt.Actions> - <Prompt.Cancel> - <ButtonText> - <Trans>Nevermind</Trans> - </ButtonText> - </Prompt.Cancel> - <Prompt.Action onPress={remove}> - <ButtonText> - <Trans>Remove</Trans> - </ButtonText> - </Prompt.Action> - </Prompt.Actions> - </Prompt.Outer> + <Prompt.Basic + control={control} + title={_(msg`Are you sure?`)} + description={_( + msg`This will delete ${word.value} from your muted words. You can always add it back later.`, + )} + onConfirm={remove} + confirmButtonCta={_(msg`Remove`)} + confirmButtonColor="negative" + /> <View style={[ diff --git a/src/lib/hooks/useOTAUpdate.ts b/src/lib/hooks/useOTAUpdate.ts index 53eab300e..d35179256 100644 --- a/src/lib/hooks/useOTAUpdate.ts +++ b/src/lib/hooks/useOTAUpdate.ts @@ -2,25 +2,9 @@ import * as Updates from 'expo-updates' import {useCallback, useEffect} from 'react' import {AppState} from 'react-native' import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' -import {t} from '@lingui/macro' export function useOTAUpdate() { - const {openModal} = useModalControls() - // HELPER FUNCTIONS - const showUpdatePopup = useCallback(() => { - openModal({ - name: 'confirm', - title: t`Update Available`, - message: t`A new version of the app is available. Please update to continue using the app.`, - onPressConfirm: async () => { - Updates.reloadAsync().catch(err => { - throw err - }) - }, - }) - }, [openModal]) const checkForUpdate = useCallback(async () => { logger.debug('useOTAUpdate: Checking for update...') try { @@ -32,32 +16,26 @@ export function useOTAUpdate() { } // Otherwise fetch the update in the background, so even if the user rejects switching to latest version it will be done automatically on next relaunch. await Updates.fetchUpdateAsync() - // show a popup modal - showUpdatePopup() } catch (e) { logger.error('useOTAUpdate: Error while checking for update', { message: e, }) } - }, [showUpdatePopup]) - const updateEventListener = useCallback( - (event: Updates.UpdateEvent) => { - logger.debug('useOTAUpdate: Listening for update...') - if (event.type === Updates.UpdateEventType.ERROR) { - logger.error('useOTAUpdate: Error while listening for update', { - message: event.message, - }) - } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { - // Handle no update available - // do nothing - } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) { - // Handle update available - // open modal, ask for user confirmation, and reload the app - showUpdatePopup() - } - }, - [showUpdatePopup], - ) + }, []) + const updateEventListener = useCallback((event: Updates.UpdateEvent) => { + logger.debug('useOTAUpdate: Listening for update...') + if (event.type === Updates.UpdateEventType.ERROR) { + logger.error('useOTAUpdate: Error while listening for update', { + message: event.message, + }) + } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { + // Handle no update available + // do nothing + } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) { + // Handle update available + // open modal, ask for user confirmation, and reload the app + } + }, []) useEffect(() => { // ADD EVENT LISTENERS diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx index 691add005..a18f6c87c 100644 --- a/src/state/modals/index.tsx +++ b/src/state/modals/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api' -import {StyleProp, ViewStyle} from 'react-native' import {Image as RNImage} from 'react-native-image-crop-picker' import {ImageModel} from '#/state/models/media/image' @@ -9,17 +8,6 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {EmbedPlayerSource} from '#/lib/strings/embed-player' import {ThreadgateSetting} from '../queries/threadgate' -export interface ConfirmModal { - name: 'confirm' - title: string - message: string | (() => JSX.Element) - onPressConfirm: () => void | Promise<void> - onPressCancel?: () => void | Promise<void> - confirmBtnText?: string - confirmBtnStyle?: StyleProp<ViewStyle> - cancelBtnText?: string -} - export interface EditProfileModal { name: 'edit-profile' profile: AppBskyActorDefs.ProfileViewDetailed @@ -225,7 +213,6 @@ export type Modal = | InviteCodesModal // Generic - | ConfirmModal | LinkWarningModal | EmbedConsentModal | InAppBrowserConsentModal diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 2855d4232..ef965b271 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -49,7 +49,7 @@ import {SuggestedLanguage} from './select-language/SuggestedLanguage' import {insertMentionAt} from 'lib/strings/mention-manip' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useModals, useModalControls} from '#/state/modals' +import {useModals} from '#/state/modals' import {useRequireAltTextEnabled} from '#/state/preferences' import { useLanguagePrefs, @@ -63,6 +63,8 @@ import {emitPostCreated} from '#/state/events' import {ThreadgateSetting} from '#/state/queries/threadgate' import {logger} from '#/logger' import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' +import * as Prompt from '#/components/Prompt' +import {useDialogStateControlContext} from 'state/dialogs' type Props = ComposerOpts export const ComposePost = observer(function ComposePost({ @@ -76,8 +78,7 @@ export const ComposePost = observer(function ComposePost({ }: Props) { const {currentAccount} = useSession() const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) - const {isModalActive, activeModals} = useModals() - const {openModal, closeModal} = useModalControls() + const {isModalActive} = useModals() const {closeComposer} = useComposerControls() const {track} = useAnalytics() const pal = usePalette('default') @@ -87,6 +88,9 @@ export const ComposePost = observer(function ComposePost({ const langPrefs = useLanguagePrefs() const setLangPrefs = useLanguagePrefsApi() const textInput = useRef<TextInputRef>(null) + const discardPromptControl = Prompt.usePromptControl() + const {closeAllDialogs} = useDialogStateControlContext() + const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isProcessing, setIsProcessing] = useState(false) const [processingState, setProcessingState] = useState('') @@ -134,27 +138,21 @@ export const ComposePost = observer(function ComposePost({ const onPressCancel = useCallback(() => { if (graphemeLength > 0 || !gallery.isEmpty) { - if (activeModals.some(modal => modal.name === 'confirm')) { - closeModal() - } + closeAllDialogs() if (Keyboard) { Keyboard.dismiss() } - openModal({ - name: 'confirm', - title: _(msg`Discard draft`), - onPressConfirm: onClose, - onPressCancel: () => { - closeModal() - }, - message: _(msg`Are you sure you'd like to discard this draft?`), - confirmBtnText: _(msg`Discard`), - confirmBtnStyle: {backgroundColor: colors.red4}, - }) + discardPromptControl.open() } else { onClose() } - }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _]) + }, [ + graphemeLength, + gallery.isEmpty, + closeAllDialogs, + discardPromptControl, + onClose, + ]) // android back button useEffect(() => { if (!isAndroid) { @@ -488,6 +486,15 @@ export const ComposePost = observer(function ComposePost({ <CharProgress count={graphemeLength} /> </View> </View> + + <Prompt.Basic + control={discardPromptControl} + title={_(msg`Discard draft?`)} + description={_(msg`Are you sure you'd like to discard this draft?`)} + onConfirm={onClose} + confirmButtonCta={_(msg`Discard`)} + confirmButtonColor="negative" + /> </KeyboardAvoidingView> ) }) diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 4af62f6f6..9300b4159 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -11,7 +11,6 @@ import {AtUri} from '@atproto/api' import * as Toast from 'view/com/util/Toast' import {sanitizeHandle} from 'lib/strings/handles' import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import { @@ -24,6 +23,7 @@ import { import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {useTheme} from '#/alf' +import * as Prompt from '#/components/Prompt' import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' export function FeedSourceCard({ @@ -85,8 +85,8 @@ export function FeedSourceCardLoaded({ const t = useTheme() const pal = usePalette('default') const {_} = useLingui() + const removePromptControl = Prompt.usePromptControl() const navigation = useNavigationDeduped() - const {openModal} = useModalControls() const {isPending: isSavePending, mutateAsync: saveFeed} = useSaveFeedMutation() @@ -96,40 +96,45 @@ export function FeedSourceCardLoaded({ const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || '')) + const onSave = React.useCallback(async () => { + if (!feed) return + + try { + if (pinOnSave) { + await pinFeed({uri: feed.uri}) + } else { + await saveFeed({uri: feed.uri}) + } + Toast.show(_(msg`Added to my feeds`)) + } catch (e) { + Toast.show(_(msg`There was an issue contacting your server`)) + logger.error('Failed to save feed', {message: e}) + } + }, [_, feed, pinFeed, pinOnSave, saveFeed]) + + const onUnsave = React.useCallback(async () => { + if (!feed) return + + try { + await removeFeed({uri: feed.uri}) + // await item.unsave() + Toast.show(_(msg`Removed from my feeds`)) + } catch (e) { + Toast.show(_(msg`There was an issue contacting your server`)) + logger.error('Failed to unsave feed', {message: e}) + } + }, [_, feed, removeFeed]) + const onToggleSaved = React.useCallback(async () => { // Only feeds can be un/saved, lists are handled elsewhere if (feed?.type !== 'feed') return if (isSaved) { - openModal({ - name: 'confirm', - title: _(msg`Remove from my feeds`), - message: _(msg`Remove ${feed?.displayName} from my feeds?`), - onPressConfirm: async () => { - try { - await removeFeed({uri: feed.uri}) - // await item.unsave() - Toast.show(_(msg`Removed from my feeds`)) - } catch (e) { - Toast.show(_(msg`There was an issue contacting your server`)) - logger.error('Failed to unsave feed', {message: e}) - } - }, - }) + removePromptControl.open() } else { - try { - if (pinOnSave) { - await pinFeed({uri: feed.uri}) - } else { - await saveFeed({uri: feed.uri}) - } - Toast.show(_(msg`Added to my feeds`)) - } catch (e) { - Toast.show(_(msg`There was an issue contacting your server`)) - logger.error('Failed to save feed', {message: e}) - } + await onSave() } - }, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed]) + }, [feed?.type, isSaved, removePromptControl, onSave]) /* * LOAD STATE @@ -167,25 +172,7 @@ export function FeedSourceCardLoaded({ accessibilityRole="button" accessibilityLabel={_(msg`Remove from my feeds`)} accessibilityHint="" - onPress={() => { - openModal({ - name: 'confirm', - title: _(msg`Remove from my feeds`), - message: _(msg`Remove this feed from my feeds?`), - onPressConfirm: async () => { - try { - await removeFeed({uri: feedUri}) - // await item.unsave() - Toast.show(_(msg`Removed from my feeds`)) - } catch (e) { - Toast.show( - _(msg`There was an issue contacting your server`), - ) - logger.error('Failed to unsave feed', {message: e}) - } - }, - }) - }} + onPress={onToggleSaved} hitSlop={15} style={styles.btn}> <FontAwesomeIcon @@ -199,89 +186,104 @@ export function FeedSourceCardLoaded({ ) return ( - <Pressable - testID={`feed-${feed.displayName}`} - accessibilityRole="button" - style={[styles.container, pal.border, style]} - onPress={() => { - if (feed.type === 'feed') { - navigation.push('ProfileFeed', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) - } else if (feed.type === 'list') { - navigation.push('ProfileList', { - name: feed.creatorDid, - rkey: new AtUri(feed.uri).rkey, - }) - } - }} - key={feed.uri}> - <View style={[styles.headerContainer]}> - <View style={[s.mr10]}> - <UserAvatar type="algo" size={36} avatar={feed.avatar} /> - </View> - <View style={[styles.headerTextContainer]}> - <Text style={[pal.text, s.bold]} numberOfLines={3}> - {feed.displayName} - </Text> - <Text style={[pal.textLight]} numberOfLines={3}> - {feed.type === 'feed' ? ( - <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> - ) : ( - <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> - )} - </Text> - </View> - - {showSaveBtn && feed.type === 'feed' && ( - <View style={[s.justifyCenter]}> - <Pressable - testID={`feed-${feed.displayName}-toggleSave`} - disabled={isSavePending || isPinPending || isRemovePending} - accessibilityRole="button" - accessibilityLabel={ - isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`) - } - accessibilityHint="" - onPress={onToggleSaved} - hitSlop={15} - style={styles.btn}> - {isSaved ? ( - <FontAwesomeIcon - icon={['far', 'trash-can']} - size={19} - color={pal.colors.icon} - /> + <> + <Pressable + testID={`feed-${feed.displayName}`} + accessibilityRole="button" + style={[styles.container, pal.border, style]} + onPress={() => { + if (feed.type === 'feed') { + navigation.push('ProfileFeed', { + name: feed.creatorDid, + rkey: new AtUri(feed.uri).rkey, + }) + } else if (feed.type === 'list') { + navigation.push('ProfileList', { + name: feed.creatorDid, + rkey: new AtUri(feed.uri).rkey, + }) + } + }} + key={feed.uri}> + <View style={[styles.headerContainer]}> + <View style={[s.mr10]}> + <UserAvatar type="algo" size={36} avatar={feed.avatar} /> + </View> + <View style={[styles.headerTextContainer]}> + <Text style={[pal.text, s.bold]} numberOfLines={3}> + {feed.displayName} + </Text> + <Text style={[pal.textLight]} numberOfLines={3}> + {feed.type === 'feed' ? ( + <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> ) : ( - <FontAwesomeIcon - icon="plus" - size={18} - color={pal.colors.link} - /> + <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> )} - </Pressable> + </Text> </View> - )} - </View> - {showDescription && feed.description ? ( - <RichText - style={[t.atoms.text_contrast_high, styles.description]} - value={feed.description} - numberOfLines={3} - /> - ) : null} + {showSaveBtn && feed.type === 'feed' && ( + <View style={[s.justifyCenter]}> + <Pressable + testID={`feed-${feed.displayName}-toggleSave`} + disabled={isSavePending || isPinPending || isRemovePending} + accessibilityRole="button" + accessibilityLabel={ + isSaved + ? _(msg`Remove from my feeds`) + : _(msg`Add to my feeds`) + } + accessibilityHint="" + onPress={onToggleSaved} + hitSlop={15} + style={styles.btn}> + {isSaved ? ( + <FontAwesomeIcon + icon={['far', 'trash-can']} + size={19} + color={pal.colors.icon} + /> + ) : ( + <FontAwesomeIcon + icon="plus" + size={18} + color={pal.colors.link} + /> + )} + </Pressable> + </View> + )} + </View> + + {showDescription && feed.description ? ( + <RichText + style={[t.atoms.text_contrast_high, styles.description]} + value={feed.description} + numberOfLines={3} + /> + ) : null} - {showLikes && feed.type === 'feed' ? ( - <Text type="sm-medium" style={[pal.text, pal.textLight]}> - <Trans> - Liked by {feed.likeCount || 0}{' '} - {pluralize(feed.likeCount || 0, 'user')} - </Trans> - </Text> - ) : null} - </Pressable> + {showLikes && feed.type === 'feed' ? ( + <Text type="sm-medium" style={[pal.text, pal.textLight]}> + <Trans> + Liked by {feed.likeCount || 0}{' '} + {pluralize(feed.likeCount || 0, 'user')} + </Trans> + </Text> + ) : null} + </Pressable> + + <Prompt.Basic + control={removePromptControl} + title={_(msg`Remove from my feeds?`)} + description={_( + msg`Are you sure you want to remove ${feed.displayName} from your feeds?`, + )} + onConfirm={onUnsave} + confirmButtonCta={_(msg`Remove`)} + confirmButtonColor="negative" + /> + </> ) } diff --git a/src/view/com/modals/Confirm.tsx b/src/view/com/modals/Confirm.tsx deleted file mode 100644 index 307897fb8..000000000 --- a/src/view/com/modals/Confirm.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import React, {useState} from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {Text} from '../util/text/Text' -import {s, colors} from 'lib/styles' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {cleanError} from 'lib/strings/errors' -import {usePalette} from 'lib/hooks/usePalette' -import {isWeb} from 'platform/detection' -import {useLingui} from '@lingui/react' -import {Trans, msg} from '@lingui/macro' -import type {ConfirmModal} from '#/state/modals' -import {useModalControls} from '#/state/modals' - -export const snapPoints = ['50%'] - -export function Component({ - title, - message, - onPressConfirm, - onPressCancel, - confirmBtnText, - confirmBtnStyle, - cancelBtnText, -}: ConfirmModal) { - const pal = usePalette('default') - const {_} = useLingui() - const {closeModal} = useModalControls() - const [isProcessing, setIsProcessing] = useState<boolean>(false) - const [error, setError] = useState<string>('') - const onPress = async () => { - setError('') - setIsProcessing(true) - try { - await onPressConfirm() - closeModal() - return - } catch (e: any) { - setError(cleanError(e)) - setIsProcessing(false) - } - } - return ( - <View testID="confirmModal" style={[pal.view, styles.container]}> - <Text type="title-xl" style={[pal.text, styles.title]}> - {title} - </Text> - {typeof message === 'string' ? ( - <Text type="xl" style={[pal.textLight, styles.description]}> - {message} - </Text> - ) : ( - message() - )} - {error ? ( - <View style={s.mt10}> - <ErrorMessage message={error} /> - </View> - ) : undefined} - <View style={s.flex1} /> - {isProcessing ? ( - <View style={[styles.btn, s.mt10]}> - <ActivityIndicator /> - </View> - ) : ( - <TouchableOpacity - testID="confirmBtn" - onPress={onPress} - style={[styles.btn, confirmBtnStyle]} - accessibilityRole="button" - accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))} - accessibilityHint=""> - <Text style={[s.white, s.bold, s.f18]}> - {confirmBtnText ?? <Trans context="action">Confirm</Trans>} - </Text> - </TouchableOpacity> - )} - {onPressCancel === undefined ? null : ( - <TouchableOpacity - testID="cancelBtn" - onPress={onPressCancel} - style={[styles.btnCancel, s.mt10]} - accessibilityRole="button" - accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))} - accessibilityHint=""> - <Text type="button-lg" style={pal.textLight}> - {cancelBtnText ?? <Trans context="action">Cancel</Trans>} - </Text> - </TouchableOpacity> - )} - </View> - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 10, - paddingBottom: isWeb ? 0 : 60, - }, - title: { - textAlign: 'center', - marginBottom: 12, - }, - description: { - textAlign: 'center', - paddingHorizontal: 22, - marginBottom: 10, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - marginTop: 22, - marginHorizontal: 44, - backgroundColor: colors.blue3, - }, - btnCancel: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - marginHorizontal: 20, - }, -}) diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 100444ff5..e03879c1d 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -6,7 +6,6 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop' import {usePalette} from 'lib/hooks/usePalette' import {useModals, useModalControls} from '#/state/modals' -import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as RepostModal from './Repost' import * as SelfLabelModal from './SelfLabel' @@ -66,10 +65,7 @@ export function ModalsContainer() { let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS let element - if (activeModal?.name === 'confirm') { - snapPoints = ConfirmModal.snapPoints - element = <ConfirmModal.Component {...activeModal} /> - } else if (activeModal?.name === 'edit-profile') { + if (activeModal?.name === 'edit-profile') { snapPoints = EditProfileModal.snapPoints element = <EditProfileModal.Component {...activeModal} /> } else if (activeModal?.name === 'report') { diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 0ced894a1..d72b7e485 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -7,7 +7,6 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' import {useModals, useModalControls} from '#/state/modals' import type {Modal as ModalIface} from '#/state/modals' -import * as ConfirmModal from './Confirm' import * as EditProfileModal from './EditProfile' import * as ReportModal from './report/Modal' import * as AppealLabelModal from './AppealLabel' @@ -78,9 +77,7 @@ function Modal({modal}: {modal: ModalIface}) { } let element - if (modal.name === 'confirm') { - element = <ConfirmModal.Component {...modal} /> - } else if (modal.name === 'edit-profile') { + if (modal.name === 'edit-profile') { element = <EditProfileModal.Component {...modal} /> } else if (modal.name === 'report') { element = <ReportModal.Component {...modal} /> diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index 6d99c32f1..c52090f97 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -9,13 +9,13 @@ import {usePalette} from 'lib/hooks/usePalette' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from 'lib/routes/types' import {logger} from '#/logger' -import {useModalControls} from '#/state/modals' import {msg as msgLingui, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {FeedDescriptor} from '#/state/queries/post-feed' import {EmptyState} from '../util/EmptyState' import {cleanError} from '#/lib/strings/errors' import {useRemoveFeedMutation} from '#/state/queries/preferences' +import * as Prompt from '#/components/Prompt' export enum KnownError { Block = 'Block', @@ -118,35 +118,29 @@ function FeedgenErrorMessage({ ) const [_, uri] = feedDesc.split('|') const [ownerDid] = safeParseFeedgenUri(uri) - const {openModal, closeModal} = useModalControls() + const removePromptControl = Prompt.usePromptControl() const {mutateAsync: removeFeed} = useRemoveFeedMutation() const onViewProfile = React.useCallback(() => { navigation.navigate('Profile', {name: ownerDid}) }, [navigation, ownerDid]) + const onPressRemoveFeed = React.useCallback(() => { + removePromptControl.open() + }, [removePromptControl]) + const onRemoveFeed = React.useCallback(async () => { - openModal({ - name: 'confirm', - title: _l(msgLingui`Remove feed`), - message: _l(msgLingui`Remove this feed from your saved feeds?`), - async onPressConfirm() { - try { - await removeFeed({uri}) - } catch (err) { - Toast.show( - _l( - msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`, - ), - ) - logger.error('Failed to remove feed', {message: err}) - } - }, - onPressCancel() { - closeModal() - }, - }) - }, [openModal, closeModal, uri, removeFeed, _l]) + try { + await removeFeed({uri}) + } catch (err) { + Toast.show( + _l( + msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`, + ), + ) + logger.error('Failed to remove feed', {message: err}) + } + }, [uri, removeFeed, _l]) const cta = React.useMemo(() => { switch (knownError) { @@ -179,27 +173,38 @@ function FeedgenErrorMessage({ }, [knownError, onViewProfile, onRemoveFeed, _l]) return ( - <View - style={[ - pal.border, - pal.viewLight, - { - borderTopWidth: 1, - paddingHorizontal: 20, - paddingVertical: 18, - gap: 12, - }, - ]}> - <Text style={pal.text}>{msg}</Text> + <> + <View + style={[ + pal.border, + pal.viewLight, + { + borderTopWidth: 1, + paddingHorizontal: 20, + paddingVertical: 18, + gap: 12, + }, + ]}> + <Text style={pal.text}>{msg}</Text> - {rawError?.message && ( - <Text style={pal.textLight}> - <Trans>Message from server: {rawError.message}</Trans> - </Text> - )} + {rawError?.message && ( + <Text style={pal.textLight}> + <Trans>Message from server: {rawError.message}</Trans> + </Text> + )} - {cta} - </View> + {cta} + </View> + + <Prompt.Basic + control={removePromptControl} + title={_l(msgLingui`Remove feed?`)} + description={_l(msgLingui`Remove this feed from your saved feeds`)} + onConfirm={onPressRemoveFeed} + confirmButtonCta={_l(msgLingui`Remove`)} + confirmButtonColor="negative" + /> + </> ) } diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index a11fe8374..75e06eb9b 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -52,6 +52,7 @@ import {LabelInfo} from '../util/moderation/LabelInfo' import {useProfileShadow} from 'state/cache/profile-shadow' import {atoms as a} from '#/alf' import {ProfileMenu} from 'view/com/profile/ProfileMenu' +import * as Prompt from '#/components/Prompt' let ProfileHeaderLoading = (_props: {}): React.ReactNode => { const pal = usePalette('default') @@ -104,6 +105,7 @@ let ProfileHeader = ({ const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [__, queueUnblock] = useProfileBlockMutationQueue(profile) + const unblockPromptControl = Prompt.usePromptControl() const moderation = useMemo( () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], @@ -176,27 +178,18 @@ let ProfileHeader = ({ }) }, [track, openModal, profile]) - const onPressUnblockAccount = React.useCallback(() => { + const unblockAccount = React.useCallback(async () => { track('ProfileHeader:UnblockAccountButtonClicked') - openModal({ - name: 'confirm', - title: _(msg`Unblock Account`), - message: _( - msg`The account will be able to interact with you after unblocking.`, - ), - onPressConfirm: async () => { - try { - await queueUnblock() - Toast.show(_(msg`Account unblocked`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to unblock account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, - }) - }, [_, openModal, queueUnblock, track]) + try { + await queueUnblock() + Toast.show(_(msg`Account unblocked`)) + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to unblock account', {message: e}) + Toast.show(_(msg`There was an issue! ${e.toString()}`)) + } + } + }, [_, queueUnblock, track]) const isMe = React.useMemo( () => currentAccount?.did === profile.did, @@ -242,7 +235,7 @@ let ProfileHeader = ({ profile.viewer?.blockingByList ? null : ( <TouchableOpacity testID="unblockBtn" - onPress={onPressUnblockAccount} + onPress={() => unblockPromptControl.open()} style={[styles.btn, styles.mainBtn, pal.btn]} accessibilityRole="button" accessibilityLabel={_(msg`Unblock`)} @@ -475,6 +468,18 @@ let ProfileHeader = ({ /> </View> </TouchableWithoutFeedback> + <Prompt.Basic + control={unblockPromptControl} + title={_(msg`Unblock Account?`)} + description={_( + msg`The account will be able to interact with you after unblocking.`, + )} + onConfirm={unblockAccount} + confirmButtonCta={ + profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) + } + confirmButtonColor="negative" + /> </View> ) } diff --git a/src/view/com/profile/ProfileMenu.tsx b/src/view/com/profile/ProfileMenu.tsx index d79e1891d..ef102daf9 100644 --- a/src/view/com/profile/ProfileMenu.tsx +++ b/src/view/com/profile/ProfileMenu.tsx @@ -33,6 +33,7 @@ import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/Per import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' import {logger} from '#/logger' import {Shadow} from 'state/cache/types' +import * as Prompt from '#/components/Prompt' let ProfileMenu = ({ profile, @@ -53,6 +54,8 @@ let ProfileMenu = ({ const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const [, queueUnfollow] = useProfileFollowMutationQueue(profile) + const blockPromptControl = Prompt.usePromptControl() + const invalidateProfileQuery = React.useCallback(() => { queryClient.invalidateQueries({ queryKey: profileQueryKey(profile.did), @@ -102,49 +105,31 @@ let ProfileMenu = ({ } }, [profile.viewer?.muted, track, queueUnmute, _, queueMute]) - const onPressBlockAccount = React.useCallback(async () => { + const blockAccount = React.useCallback(async () => { if (profile.viewer?.blocking) { track('ProfileHeader:UnblockAccountButtonClicked') - openModal({ - name: 'confirm', - title: _(msg`Unblock Account`), - message: _( - msg`The account will be able to interact with you after unblocking.`, - ), - onPressConfirm: async () => { - try { - await queueUnblock() - Toast.show(_(msg`Account unblocked`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to unblock account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, - }) + try { + await queueUnblock() + Toast.show(_(msg`Account unblocked`)) + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to unblock account', {message: e}) + Toast.show(_(msg`There was an issue! ${e.toString()}`)) + } + } } else { track('ProfileHeader:BlockAccountButtonClicked') - openModal({ - name: 'confirm', - title: _(msg`Block Account`), - message: _( - msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, - ), - onPressConfirm: async () => { - try { - await queueBlock() - Toast.show(_(msg`Account blocked`)) - } catch (e: any) { - if (e?.name !== 'AbortError') { - logger.error('Failed to block account', {message: e}) - Toast.show(_(msg`There was an issue! ${e.toString()}`)) - } - } - }, - }) + try { + await queueBlock() + Toast.show(_(msg`Account blocked`)) + } catch (e: any) { + if (e?.name !== 'AbortError') { + logger.error('Failed to block account', {message: e}) + Toast.show(_(msg`There was an issue! ${e.toString()}`)) + } + } } - }, [profile.viewer?.blocking, track, openModal, _, queueUnblock, queueBlock]) + }, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock]) const onPressUnfollowAccount = React.useCallback(async () => { track('ProfileHeader:UnfollowButtonClicked') @@ -268,7 +253,7 @@ let ProfileMenu = ({ ? _(msg`Unblock Account`) : _(msg`Block Account`) } - onPress={onPressBlockAccount}> + onPress={() => blockPromptControl.open()}> <Menu.ItemText> {profile.viewer?.blocking ? ( <Trans>Unblock Account</Trans> @@ -299,6 +284,29 @@ let ProfileMenu = ({ )} </Menu.Outer> </Menu.Root> + + <Prompt.Basic + control={blockPromptControl} + title={ + profile.viewer?.blocking + ? _(msg`Unblock Account?`) + : _(msg`Block Account?`) + } + description={ + profile.viewer?.blocking + ? _( + msg`The account will be able to interact with you after unblocking.`, + ) + : _( + msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, + ) + } + onConfirm={blockAccount} + confirmButtonCta={ + profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) + } + confirmButtonColor="negative" + /> </EventStopper> ) } diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 3c1a736f3..84a047c40 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -14,6 +14,8 @@ import {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' import * as Toast from '../Toast' import {EventStopper} from '../EventStopper' +import {useDialogControl} from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' import {useModalControls} from '#/state/modals' import {makeProfileLink} from '#/lib/routes/links' import {CommonNavigatorParams} from '#/lib/routes/types' @@ -81,6 +83,8 @@ let PostDropdownBtn = ({ const openLink = useOpenLink() const navigation = useNavigation() const {mutedWordsDialogControl} = useGlobalDialogsControlContext() + const deletePromptControl = useDialogControl() + const hidePromptControl = useDialogControl() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) @@ -257,16 +261,7 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownHideBtn" label={_(msg`Hide post`)} - onPress={() => { - openModal({ - name: 'confirm', - title: _(msg`Hide this post?`), - message: _( - msg`This will hide this post from your feeds.`, - ), - onPressConfirm: onHidePost, - }) - }}> + onPress={hidePromptControl.open}> <Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText> <Menu.ItemIcon icon={EyeSlash} position="right" /> </Menu.Item> @@ -298,14 +293,7 @@ let PostDropdownBtn = ({ <Menu.Item testID="postDropdownDeleteBtn" label={_(msg`Delete post`)} - onPress={() => { - openModal({ - name: 'confirm', - title: _(msg`Delete this post?`), - message: _(msg`Are you sure? This cannot be undone.`), - onPressConfirm: onDeletePost, - }) - }}> + onPress={deletePromptControl.open}> <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> <Menu.ItemIcon icon={Trash} position="right" /> </Menu.Item> @@ -335,6 +323,25 @@ let PostDropdownBtn = ({ </Menu.Group> </Menu.Outer> </Menu.Root> + + <Prompt.Basic + control={deletePromptControl} + title={_(msg`Delete this post?`)} + description={_( + msg`If you remove this post, you won't be able to recover it.`, + )} + onConfirm={onDeletePost} + confirmButtonCta={_(msg`Delete`)} + confirmButtonColor="negative" + /> + + <Prompt.Basic + control={hidePromptControl} + title={_(msg`Hide this post?`)} + description={_(msg`This post will be hidden from feeds.`)} + onConfirm={onHidePost} + confirmButtonCta={_(msg`Hide`)} + /> </EventStopper> ) } diff --git a/src/view/screens/AppPasswords.tsx b/src/view/screens/AppPasswords.tsx index dc439c367..800216169 100644 --- a/src/view/screens/AppPasswords.tsx +++ b/src/view/screens/AppPasswords.tsx @@ -29,6 +29,8 @@ import { } from '#/state/queries/app-passwords' import {ErrorScreen} from '../com/util/error/ErrorScreen' import {cleanError} from '#/lib/strings/errors' +import * as Prompt from '#/components/Prompt' +import {useDialogControl} from '#/components/Dialog' type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> export function AppPasswords({}: Props) { @@ -212,23 +214,18 @@ function AppPassword({ }) { const pal = usePalette('default') const {_} = useLingui() - const {openModal} = useModalControls() + const control = useDialogControl() const {contentLanguages} = useLanguagePrefs() const deleteMutation = useAppPasswordDeleteMutation() const onDelete = React.useCallback(async () => { - openModal({ - name: 'confirm', - title: _(msg`Delete app password`), - message: _( - msg`Are you sure you want to delete the app password "${name}"?`, - ), - async onPressConfirm() { - await deleteMutation.mutateAsync({name}) - Toast.show(_(msg`App password deleted`)) - }, - }) - }, [deleteMutation, openModal, name, _]) + await deleteMutation.mutateAsync({name}) + Toast.show(_(msg`App password deleted`)) + }, [deleteMutation, name, _]) + + const onPress = React.useCallback(() => { + control.open() + }, [control]) const primaryLocale = contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' @@ -237,7 +234,7 @@ function AppPassword({ <TouchableOpacity testID={testID} style={[styles.item, pal.border]} - onPress={onDelete} + onPress={onPress} accessibilityRole="button" accessibilityLabel={_(msg`Delete app password`)} accessibilityHint=""> @@ -260,6 +257,17 @@ function AppPassword({ </Text> </View> <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> + + <Prompt.Basic + control={control} + title={_(msg`Delete app password?`)} + description={_( + msg`Are you sure you want to delete the app password "${name}"?`, + )} + onConfirm={onDelete} + confirmButtonCta={_(msg`Delete`)} + confirmButtonColor="negative" + /> </TouchableOpacity> ) } diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 9e98757ef..95046e5b4 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -61,6 +61,8 @@ import {logger} from '#/logger' import {useAnalytics} from '#/lib/analytics/analytics' import {listenSoftReset} from '#/state/events' import {atoms as a, useTheme} from '#/alf' +import * as Prompt from '#/components/Prompt' +import {useDialogControl} from '#/components/Dialog' const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_MOD = ['About'] @@ -234,7 +236,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const {_} = useLingui() const navigation = useNavigation<NavigationProp>() const {currentAccount} = useSession() - const {openModal, closeModal} = useModalControls() + const {openModal} = useModalControls() const listMuteMutation = useListMuteMutation() const listBlockMutation = useListBlockMutation() const listDeleteMutation = useListDeleteMutation() @@ -251,6 +253,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() const {track} = useAnalytics() + const deleteListPromptControl = useDialogControl() + const subscribeMutePromptControl = useDialogControl() + const subscribeBlockPromptControl = useDialogControl() + const isPinned = preferences?.feeds?.pinned?.includes(list.uri) const isSaved = preferences?.feeds?.saved?.includes(list.uri) @@ -269,32 +275,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { } }, [list.uri, isPinned, pinFeed, unpinFeed, _]) - const onSubscribeMute = useCallback(() => { - openModal({ - name: 'confirm', - title: _(msg`Mute these accounts?`), - message: _( - msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, - ), - confirmBtnText: _(msg`Mute this List`), - async onPressConfirm() { - try { - await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) - Toast.show(_(msg`List muted`)) - track('Lists:Mute') - } catch { - Toast.show( - _( - msg`There was an issue. Please check your internet connection and try again.`, - ), - ) - } - }, - onPressCancel() { - closeModal() - }, - }) - }, [openModal, closeModal, list, listMuteMutation, track, _]) + const onSubscribeMute = useCallback(async () => { + try { + await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) + Toast.show(_(msg`List muted`)) + track('Lists:Mute') + } catch { + Toast.show( + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), + ) + } + }, [list, listMuteMutation, track, _]) const onUnsubscribeMute = useCallback(async () => { try { @@ -310,32 +303,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { } }, [list, listMuteMutation, track, _]) - const onSubscribeBlock = useCallback(() => { - openModal({ - name: 'confirm', - title: _(msg`Block these accounts?`), - message: _( - msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, - ), - confirmBtnText: _(msg`Block this List`), - async onPressConfirm() { - try { - await listBlockMutation.mutateAsync({uri: list.uri, block: true}) - Toast.show(_(msg`List blocked`)) - track('Lists:Block') - } catch { - Toast.show( - _( - msg`There was an issue. Please check your internet connection and try again.`, - ), - ) - } - }, - onPressCancel() { - closeModal() - }, - }) - }, [openModal, closeModal, list, listBlockMutation, track, _]) + const onSubscribeBlock = useCallback(async () => { + try { + await listBlockMutation.mutateAsync({uri: list.uri, block: true}) + Toast.show(_(msg`List blocked`)) + track('Lists:Block') + } catch { + Toast.show( + _( + msg`There was an issue. Please check your internet connection and try again.`, + ), + ) + } + }, [list, listBlockMutation, track, _]) const onUnsubscribeBlock = useCallback(async () => { try { @@ -358,34 +338,26 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { }) }, [openModal, list]) - const onPressDelete = useCallback(() => { - openModal({ - name: 'confirm', - title: _(msg`Delete List`), - message: _(msg`Are you sure?`), - async onPressConfirm() { - await listDeleteMutation.mutateAsync({uri: list.uri}) - - if (isSaved || isPinned) { - const {saved, pinned} = preferences!.feeds - - setSavedFeeds({ - saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved, - pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned, - }) - } + const onPressDelete = useCallback(async () => { + await listDeleteMutation.mutateAsync({uri: list.uri}) - Toast.show(_(msg`List deleted`)) - track('Lists:Delete') - if (navigation.canGoBack()) { - navigation.goBack() - } else { - navigation.navigate('Home') - } - }, - }) + if (isSaved || isPinned) { + const {saved, pinned} = preferences!.feeds + + setSavedFeeds({ + saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved, + pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned, + }) + } + + Toast.show(_(msg`List deleted`)) + track('Lists:Delete') + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.navigate('Home') + } }, [ - openModal, list, listDeleteMutation, navigation, @@ -443,7 +415,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { items.push({ testID: 'listHeaderDropdownDeleteBtn', label: _(msg`Delete List`), - onPress: onPressDelete, + onPress: deleteListPromptControl.open, icon: { ios: { name: 'trash', @@ -489,7 +461,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { items.push({ testID: 'listHeaderDropdownMuteBtn', label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`), - onPress: isMuting ? onUnsubscribeMute : onSubscribeMute, + onPress: isMuting + ? onUnsubscribeMute + : subscribeMutePromptControl.open, icon: { ios: { name: isMuting ? 'eye' : 'eye.slash', @@ -504,7 +478,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { items.push({ testID: 'listHeaderDropdownBlockBtn', label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`), - onPress: isBlocking ? onUnsubscribeBlock : onSubscribeBlock, + onPress: isBlocking + ? onUnsubscribeBlock + : subscribeBlockPromptControl.open, icon: { ios: { name: 'person.fill.xmark', @@ -517,24 +493,24 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { } return items }, [ - isOwner, - onPressShare, - onPressEdit, - onPressDelete, - onPressReport, _, + onPressShare, + isOwner, isModList, isPinned, - unpinFeed, + isCurateList, + onPressEdit, + deleteListPromptControl.open, + onPressReport, isPending, + unpinFeed, list.uri, - isCurateList, - isMuting, isBlocking, + isMuting, onUnsubscribeMute, - onSubscribeMute, + subscribeMutePromptControl.open, onUnsubscribeBlock, - onSubscribeBlock, + subscribeBlockPromptControl.open, ]) const subscribeDropdownItems: DropdownItem[] = useMemo(() => { @@ -542,7 +518,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { { testID: 'subscribeDropdownMuteBtn', label: _(msg`Mute accounts`), - onPress: onSubscribeMute, + onPress: subscribeMutePromptControl.open, icon: { ios: { name: 'speaker.slash', @@ -554,7 +530,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { { testID: 'subscribeDropdownBlockBtn', label: _(msg`Block accounts`), - onPress: onSubscribeBlock, + onPress: subscribeBlockPromptControl.open, icon: { ios: { name: 'person.fill.xmark', @@ -564,7 +540,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { }, }, ] - }, [onSubscribeMute, onSubscribeBlock, _]) + }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open]) return ( <ProfileSubpageHeader @@ -620,6 +596,38 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) { <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} /> </View> </NativeDropdown> + + <Prompt.Basic + control={deleteListPromptControl} + title={_(msg`Delete this list?`)} + description={_( + msg`If you delete this list, you won't be able to recover it.`, + )} + onConfirm={onPressDelete} + confirmButtonCta={_(msg`Delete`)} + confirmButtonColor="negative" + /> + + <Prompt.Basic + control={subscribeMutePromptControl} + title={_(msg`Mute these accounts?`)} + description={_( + msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, + )} + onConfirm={onSubscribeMute} + confirmButtonCta={_(msg`Mute list`)} + /> + + <Prompt.Basic + control={subscribeBlockPromptControl} + title={_(msg`Block these accounts?`)} + description={_( + msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, + )} + onConfirm={onSubscribeBlock} + confirmButtonCta={_(msg`Block list`)} + confirmButtonColor="negative" + /> </ProfileSubpageHeader> ) } diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index 09be124db..c2eaf19ac 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -68,7 +68,7 @@ export function Dialogs() { </Prompt.Description> <Prompt.Actions> <Prompt.Cancel>Cancel</Prompt.Cancel> - <Prompt.Action>Confirm</Prompt.Action> + <Prompt.Action onPress={() => {}}>Confirm</Prompt.Action> </Prompt.Actions> </Prompt.Outer> |