Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

ALF confirmation dialogs (Dialogs Pt. 3) (#3143)

* Improve a11y on ios

* Format

* Remove android

* Fix android

* ALF confirmation dialog

* Use ALF for Delete Post confirmation

organize

diff

fix text

minimize

change copy

alternative confirm prompt

revert type changes

add ButtonColor param

* small adjustment to buttons in prompt

* full width below gtmobile

* update hide post dialog

* space out dialogs

* update dialogs for lists

* add example

* add to app passwords

* Revert some changes

* use sharedvalue for `importantForAccessibility`

* add back `isOpen`

* fix some more types

* small adjustment to buttons in prompt

* full width below gtmobile

* update the rest of the prompts

rm old confirm modal

rm update prompt

feed error prompt

feed source card and profile block/unblock

composer discard

* Update src/view/screens/AppPasswords.tsx

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* lint

* How about a default

* Reverse reverse

* Port over confirm dialogs

* Add some comments

* Remove unused file

* complete merge

* add testID where needed

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

authored by

Hailey
surfdude29
Eric Bailey
and committed by
GitHub
9f2f7f22 090b35e5

+536 -601
+6
src/alf/atoms.ts
··· 111 111 flex_row: { 112 112 flexDirection: 'row', 113 113 }, 114 + flex_col_reverse: { 115 + flexDirection: 'column-reverse', 116 + }, 117 + flex_row_reverse: { 118 + flexDirection: 'row-reverse', 119 + }, 114 120 flex_wrap: { 115 121 flexWrap: 'wrap', 116 122 },
+3 -1
src/components/Dialog/index.tsx
··· 75 75 control, 76 76 onClose, 77 77 nativeOptions, 78 + testID, 78 79 }: React.PropsWithChildren<DialogOuterProps>) { 79 80 const t = useTheme() 80 81 const sheet = React.useRef<BottomSheet>(null) ··· 145 146 accessibilityViewIsModal 146 147 // Android 147 148 importantForAccessibility="yes" 148 - style={[a.absolute, a.inset_0]}> 149 + style={[a.absolute, a.inset_0]} 150 + testID={testID}> 149 151 <BottomSheet 150 152 enableDynamicSizing={!hasSnapPoints} 151 153 enablePanDownToClose
+1
src/components/Dialog/types.ts
··· 46 46 sheet?: Omit<BottomSheetProps, 'children'> 47 47 } 48 48 webOptions?: {} 49 + testID?: string 49 50 } 50 51 51 52 type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
+77 -14
src/components/Prompt.tsx
··· 1 1 import React from 'react' 2 - import {View, PressableProps} from 'react-native' 2 + import {View} from 'react-native' 3 3 import {msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {useTheme, atoms as a, useBreakpoints} from '#/alf' 7 7 import {Text} from '#/components/Typography' 8 - import {Button} from '#/components/Button' 8 + import {Button, ButtonColor, ButtonText} from '#/components/Button' 9 9 10 10 import * as Dialog from '#/components/Dialog' 11 11 ··· 22 22 export function Outer({ 23 23 children, 24 24 control, 25 + testID, 25 26 }: React.PropsWithChildren<{ 26 27 control: Dialog.DialogOuterProps['control'] 28 + testID?: string 27 29 }>) { 28 30 const {gtMobile} = useBreakpoints() 29 31 const titleId = React.useId() ··· 35 37 ) 36 38 37 39 return ( 38 - <Dialog.Outer control={control}> 40 + <Dialog.Outer control={control} testID={testID}> 39 41 <Context.Provider value={context}> 40 42 <Dialog.Handle /> 41 43 ··· 80 82 a.w_full, 81 83 a.gap_sm, 82 84 a.justify_end, 83 - gtMobile ? [a.flex_row] : [a.flex_col, a.pt_md, a.pb_4xl], 85 + gtMobile 86 + ? [a.flex_row, a.flex_row_reverse, a.justify_start] 87 + : [a.flex_col, a.pt_md, a.pb_4xl], 84 88 ]}> 85 89 {children} 86 90 </View> ··· 89 93 90 94 export function Cancel({ 91 95 children, 92 - }: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { 96 + cta, 97 + }: React.PropsWithChildren<{ 98 + /** 99 + * Optional i18n string, used in lieu of `children` for simple buttons. If 100 + * undefined (and `children` is undefined), it will default to "Cancel". 101 + */ 102 + cta?: string 103 + }>) { 93 104 const {_} = useLingui() 94 105 const {gtMobile} = useBreakpoints() 95 106 const {close} = Dialog.useDialogContext() 107 + const onPress = React.useCallback(() => { 108 + close() 109 + }, [close]) 110 + 96 111 return ( 97 112 <Button 98 113 variant="solid" 99 114 color="secondary" 100 115 size={gtMobile ? 'small' : 'medium'} 101 - label={_(msg`Cancel`)} 102 - onPress={() => close()}> 103 - {children} 116 + label={cta || _(msg`Cancel`)} 117 + onPress={onPress}> 118 + {children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>} 104 119 </Button> 105 120 ) 106 121 } ··· 108 123 export function Action({ 109 124 children, 110 125 onPress, 111 - }: React.PropsWithChildren<{onPress?: () => void}>) { 126 + color = 'primary', 127 + cta, 128 + testID, 129 + }: React.PropsWithChildren<{ 130 + onPress: () => void 131 + color?: ButtonColor 132 + /** 133 + * Optional i18n string, used in lieu of `children` for simple buttons. If 134 + * undefined (and `children` is undefined), it will default to "Confirm". 135 + */ 136 + cta?: string 137 + testID?: string 138 + }>) { 112 139 const {_} = useLingui() 113 140 const {gtMobile} = useBreakpoints() 114 141 const {close} = Dialog.useDialogContext() 115 142 const handleOnPress = React.useCallback(() => { 116 143 close() 117 - onPress?.() 144 + onPress() 118 145 }, [close, onPress]) 146 + 119 147 return ( 120 148 <Button 121 149 variant="solid" 122 - color="primary" 150 + color={color} 123 151 size={gtMobile ? 'small' : 'medium'} 124 - label={_(msg`Confirm`)} 125 - onPress={handleOnPress}> 126 - {children} 152 + label={cta || _(msg`Confirm`)} 153 + onPress={handleOnPress} 154 + testID={testID}> 155 + {children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>} 127 156 </Button> 128 157 ) 129 158 } 159 + 160 + export function Basic({ 161 + control, 162 + title, 163 + description, 164 + cancelButtonCta, 165 + confirmButtonCta, 166 + onConfirm, 167 + confirmButtonColor, 168 + }: React.PropsWithChildren<{ 169 + control: Dialog.DialogOuterProps['control'] 170 + title: string 171 + description: string 172 + cancelButtonCta?: string 173 + confirmButtonCta?: string 174 + onConfirm: () => void 175 + confirmButtonColor?: ButtonColor 176 + }>) { 177 + return ( 178 + <Outer control={control} testID="confirmModal"> 179 + <Title>{title}</Title> 180 + <Description>{description}</Description> 181 + <Actions> 182 + <Action 183 + cta={confirmButtonCta} 184 + onPress={onConfirm} 185 + color={confirmButtonColor} 186 + testID="confirmBtn" 187 + /> 188 + <Cancel cta={cancelButtonCta} /> 189 + </Actions> 190 + </Outer> 191 + ) 192 + }
+10 -23
src/components/dialogs/MutedWords.tsx
··· 277 277 278 278 return ( 279 279 <> 280 - <Prompt.Outer control={control}> 281 - <Prompt.Title> 282 - <Trans>Are you sure?</Trans> 283 - </Prompt.Title> 284 - <Prompt.Description> 285 - <Trans> 286 - This will delete {word.value} from your muted words. You can always 287 - add it back later. 288 - </Trans> 289 - </Prompt.Description> 290 - <Prompt.Actions> 291 - <Prompt.Cancel> 292 - <ButtonText> 293 - <Trans>Nevermind</Trans> 294 - </ButtonText> 295 - </Prompt.Cancel> 296 - <Prompt.Action onPress={remove}> 297 - <ButtonText> 298 - <Trans>Remove</Trans> 299 - </ButtonText> 300 - </Prompt.Action> 301 - </Prompt.Actions> 302 - </Prompt.Outer> 280 + <Prompt.Basic 281 + control={control} 282 + title={_(msg`Are you sure?`)} 283 + description={_( 284 + msg`This will delete ${word.value} from your muted words. You can always add it back later.`, 285 + )} 286 + onConfirm={remove} 287 + confirmButtonCta={_(msg`Remove`)} 288 + confirmButtonColor="negative" 289 + /> 303 290 304 291 <View 305 292 style={[
+15 -37
src/lib/hooks/useOTAUpdate.ts
··· 2 2 import {useCallback, useEffect} from 'react' 3 3 import {AppState} from 'react-native' 4 4 import {logger} from '#/logger' 5 - import {useModalControls} from '#/state/modals' 6 - import {t} from '@lingui/macro' 7 5 8 6 export function useOTAUpdate() { 9 - const {openModal} = useModalControls() 10 - 11 7 // HELPER FUNCTIONS 12 - const showUpdatePopup = useCallback(() => { 13 - openModal({ 14 - name: 'confirm', 15 - title: t`Update Available`, 16 - message: t`A new version of the app is available. Please update to continue using the app.`, 17 - onPressConfirm: async () => { 18 - Updates.reloadAsync().catch(err => { 19 - throw err 20 - }) 21 - }, 22 - }) 23 - }, [openModal]) 24 8 const checkForUpdate = useCallback(async () => { 25 9 logger.debug('useOTAUpdate: Checking for update...') 26 10 try { ··· 32 16 } 33 17 // 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. 34 18 await Updates.fetchUpdateAsync() 35 - // show a popup modal 36 - showUpdatePopup() 37 19 } catch (e) { 38 20 logger.error('useOTAUpdate: Error while checking for update', { 39 21 message: e, 40 22 }) 41 23 } 42 - }, [showUpdatePopup]) 43 - const updateEventListener = useCallback( 44 - (event: Updates.UpdateEvent) => { 45 - logger.debug('useOTAUpdate: Listening for update...') 46 - if (event.type === Updates.UpdateEventType.ERROR) { 47 - logger.error('useOTAUpdate: Error while listening for update', { 48 - message: event.message, 49 - }) 50 - } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { 51 - // Handle no update available 52 - // do nothing 53 - } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) { 54 - // Handle update available 55 - // open modal, ask for user confirmation, and reload the app 56 - showUpdatePopup() 57 - } 58 - }, 59 - [showUpdatePopup], 60 - ) 24 + }, []) 25 + const updateEventListener = useCallback((event: Updates.UpdateEvent) => { 26 + logger.debug('useOTAUpdate: Listening for update...') 27 + if (event.type === Updates.UpdateEventType.ERROR) { 28 + logger.error('useOTAUpdate: Error while listening for update', { 29 + message: event.message, 30 + }) 31 + } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { 32 + // Handle no update available 33 + // do nothing 34 + } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) { 35 + // Handle update available 36 + // open modal, ask for user confirmation, and reload the app 37 + } 38 + }, []) 61 39 62 40 useEffect(() => { 63 41 // ADD EVENT LISTENERS
-13
src/state/modals/index.tsx
··· 1 1 import React from 'react' 2 2 import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api' 3 - import {StyleProp, ViewStyle} from 'react-native' 4 3 import {Image as RNImage} from 'react-native-image-crop-picker' 5 4 6 5 import {ImageModel} from '#/state/models/media/image' ··· 8 7 import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' 9 8 import {EmbedPlayerSource} from '#/lib/strings/embed-player' 10 9 import {ThreadgateSetting} from '../queries/threadgate' 11 - 12 - export interface ConfirmModal { 13 - name: 'confirm' 14 - title: string 15 - message: string | (() => JSX.Element) 16 - onPressConfirm: () => void | Promise<void> 17 - onPressCancel?: () => void | Promise<void> 18 - confirmBtnText?: string 19 - confirmBtnStyle?: StyleProp<ViewStyle> 20 - cancelBtnText?: string 21 - } 22 10 23 11 export interface EditProfileModal { 24 12 name: 'edit-profile' ··· 225 213 | InviteCodesModal 226 214 227 215 // Generic 228 - | ConfirmModal 229 216 | LinkWarningModal 230 217 | EmbedConsentModal 231 218 | InAppBrowserConsentModal
+25 -18
src/view/com/composer/Composer.tsx
··· 49 49 import {insertMentionAt} from 'lib/strings/mention-manip' 50 50 import {Trans, msg} from '@lingui/macro' 51 51 import {useLingui} from '@lingui/react' 52 - import {useModals, useModalControls} from '#/state/modals' 52 + import {useModals} from '#/state/modals' 53 53 import {useRequireAltTextEnabled} from '#/state/preferences' 54 54 import { 55 55 useLanguagePrefs, ··· 63 63 import {ThreadgateSetting} from '#/state/queries/threadgate' 64 64 import {logger} from '#/logger' 65 65 import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' 66 + import * as Prompt from '#/components/Prompt' 67 + import {useDialogStateControlContext} from 'state/dialogs' 66 68 67 69 type Props = ComposerOpts 68 70 export const ComposePost = observer(function ComposePost({ ··· 76 78 }: Props) { 77 79 const {currentAccount} = useSession() 78 80 const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) 79 - const {isModalActive, activeModals} = useModals() 80 - const {openModal, closeModal} = useModalControls() 81 + const {isModalActive} = useModals() 81 82 const {closeComposer} = useComposerControls() 82 83 const {track} = useAnalytics() 83 84 const pal = usePalette('default') ··· 87 88 const langPrefs = useLanguagePrefs() 88 89 const setLangPrefs = useLanguagePrefsApi() 89 90 const textInput = useRef<TextInputRef>(null) 91 + const discardPromptControl = Prompt.usePromptControl() 92 + const {closeAllDialogs} = useDialogStateControlContext() 93 + 90 94 const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) 91 95 const [isProcessing, setIsProcessing] = useState(false) 92 96 const [processingState, setProcessingState] = useState('') ··· 134 138 135 139 const onPressCancel = useCallback(() => { 136 140 if (graphemeLength > 0 || !gallery.isEmpty) { 137 - if (activeModals.some(modal => modal.name === 'confirm')) { 138 - closeModal() 139 - } 141 + closeAllDialogs() 140 142 if (Keyboard) { 141 143 Keyboard.dismiss() 142 144 } 143 - openModal({ 144 - name: 'confirm', 145 - title: _(msg`Discard draft`), 146 - onPressConfirm: onClose, 147 - onPressCancel: () => { 148 - closeModal() 149 - }, 150 - message: _(msg`Are you sure you'd like to discard this draft?`), 151 - confirmBtnText: _(msg`Discard`), 152 - confirmBtnStyle: {backgroundColor: colors.red4}, 153 - }) 145 + discardPromptControl.open() 154 146 } else { 155 147 onClose() 156 148 } 157 - }, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _]) 149 + }, [ 150 + graphemeLength, 151 + gallery.isEmpty, 152 + closeAllDialogs, 153 + discardPromptControl, 154 + onClose, 155 + ]) 158 156 // android back button 159 157 useEffect(() => { 160 158 if (!isAndroid) { ··· 488 486 <CharProgress count={graphemeLength} /> 489 487 </View> 490 488 </View> 489 + 490 + <Prompt.Basic 491 + control={discardPromptControl} 492 + title={_(msg`Discard draft?`)} 493 + description={_(msg`Are you sure you'd like to discard this draft?`)} 494 + onConfirm={onClose} 495 + confirmButtonCta={_(msg`Discard`)} 496 + confirmButtonColor="negative" 497 + /> 491 498 </KeyboardAvoidingView> 492 499 ) 493 500 })
+128 -126
src/view/com/feeds/FeedSourceCard.tsx
··· 11 11 import * as Toast from 'view/com/util/Toast' 12 12 import {sanitizeHandle} from 'lib/strings/handles' 13 13 import {logger} from '#/logger' 14 - import {useModalControls} from '#/state/modals' 15 14 import {Trans, msg} from '@lingui/macro' 16 15 import {useLingui} from '@lingui/react' 17 16 import { ··· 24 23 import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' 25 24 import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 26 25 import {useTheme} from '#/alf' 26 + import * as Prompt from '#/components/Prompt' 27 27 import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' 28 28 29 29 export function FeedSourceCard({ ··· 85 85 const t = useTheme() 86 86 const pal = usePalette('default') 87 87 const {_} = useLingui() 88 + const removePromptControl = Prompt.usePromptControl() 88 89 const navigation = useNavigationDeduped() 89 - const {openModal} = useModalControls() 90 90 91 91 const {isPending: isSavePending, mutateAsync: saveFeed} = 92 92 useSaveFeedMutation() ··· 96 96 97 97 const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || '')) 98 98 99 + const onSave = React.useCallback(async () => { 100 + if (!feed) return 101 + 102 + try { 103 + if (pinOnSave) { 104 + await pinFeed({uri: feed.uri}) 105 + } else { 106 + await saveFeed({uri: feed.uri}) 107 + } 108 + Toast.show(_(msg`Added to my feeds`)) 109 + } catch (e) { 110 + Toast.show(_(msg`There was an issue contacting your server`)) 111 + logger.error('Failed to save feed', {message: e}) 112 + } 113 + }, [_, feed, pinFeed, pinOnSave, saveFeed]) 114 + 115 + const onUnsave = React.useCallback(async () => { 116 + if (!feed) return 117 + 118 + try { 119 + await removeFeed({uri: feed.uri}) 120 + // await item.unsave() 121 + Toast.show(_(msg`Removed from my feeds`)) 122 + } catch (e) { 123 + Toast.show(_(msg`There was an issue contacting your server`)) 124 + logger.error('Failed to unsave feed', {message: e}) 125 + } 126 + }, [_, feed, removeFeed]) 127 + 99 128 const onToggleSaved = React.useCallback(async () => { 100 129 // Only feeds can be un/saved, lists are handled elsewhere 101 130 if (feed?.type !== 'feed') return 102 131 103 132 if (isSaved) { 104 - openModal({ 105 - name: 'confirm', 106 - title: _(msg`Remove from my feeds`), 107 - message: _(msg`Remove ${feed?.displayName} from my feeds?`), 108 - onPressConfirm: async () => { 109 - try { 110 - await removeFeed({uri: feed.uri}) 111 - // await item.unsave() 112 - Toast.show(_(msg`Removed from my feeds`)) 113 - } catch (e) { 114 - Toast.show(_(msg`There was an issue contacting your server`)) 115 - logger.error('Failed to unsave feed', {message: e}) 116 - } 117 - }, 118 - }) 133 + removePromptControl.open() 119 134 } else { 120 - try { 121 - if (pinOnSave) { 122 - await pinFeed({uri: feed.uri}) 123 - } else { 124 - await saveFeed({uri: feed.uri}) 125 - } 126 - Toast.show(_(msg`Added to my feeds`)) 127 - } catch (e) { 128 - Toast.show(_(msg`There was an issue contacting your server`)) 129 - logger.error('Failed to save feed', {message: e}) 130 - } 135 + await onSave() 131 136 } 132 - }, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed]) 137 + }, [feed?.type, isSaved, removePromptControl, onSave]) 133 138 134 139 /* 135 140 * LOAD STATE ··· 167 172 accessibilityRole="button" 168 173 accessibilityLabel={_(msg`Remove from my feeds`)} 169 174 accessibilityHint="" 170 - onPress={() => { 171 - openModal({ 172 - name: 'confirm', 173 - title: _(msg`Remove from my feeds`), 174 - message: _(msg`Remove this feed from my feeds?`), 175 - onPressConfirm: async () => { 176 - try { 177 - await removeFeed({uri: feedUri}) 178 - // await item.unsave() 179 - Toast.show(_(msg`Removed from my feeds`)) 180 - } catch (e) { 181 - Toast.show( 182 - _(msg`There was an issue contacting your server`), 183 - ) 184 - logger.error('Failed to unsave feed', {message: e}) 185 - } 186 - }, 187 - }) 188 - }} 175 + onPress={onToggleSaved} 189 176 hitSlop={15} 190 177 style={styles.btn}> 191 178 <FontAwesomeIcon ··· 199 186 ) 200 187 201 188 return ( 202 - <Pressable 203 - testID={`feed-${feed.displayName}`} 204 - accessibilityRole="button" 205 - style={[styles.container, pal.border, style]} 206 - onPress={() => { 207 - if (feed.type === 'feed') { 208 - navigation.push('ProfileFeed', { 209 - name: feed.creatorDid, 210 - rkey: new AtUri(feed.uri).rkey, 211 - }) 212 - } else if (feed.type === 'list') { 213 - navigation.push('ProfileList', { 214 - name: feed.creatorDid, 215 - rkey: new AtUri(feed.uri).rkey, 216 - }) 217 - } 218 - }} 219 - key={feed.uri}> 220 - <View style={[styles.headerContainer]}> 221 - <View style={[s.mr10]}> 222 - <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 223 - </View> 224 - <View style={[styles.headerTextContainer]}> 225 - <Text style={[pal.text, s.bold]} numberOfLines={3}> 226 - {feed.displayName} 227 - </Text> 228 - <Text style={[pal.textLight]} numberOfLines={3}> 229 - {feed.type === 'feed' ? ( 230 - <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 231 - ) : ( 232 - <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 233 - )} 234 - </Text> 235 - </View> 236 - 237 - {showSaveBtn && feed.type === 'feed' && ( 238 - <View style={[s.justifyCenter]}> 239 - <Pressable 240 - testID={`feed-${feed.displayName}-toggleSave`} 241 - disabled={isSavePending || isPinPending || isRemovePending} 242 - accessibilityRole="button" 243 - accessibilityLabel={ 244 - isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`) 245 - } 246 - accessibilityHint="" 247 - onPress={onToggleSaved} 248 - hitSlop={15} 249 - style={styles.btn}> 250 - {isSaved ? ( 251 - <FontAwesomeIcon 252 - icon={['far', 'trash-can']} 253 - size={19} 254 - color={pal.colors.icon} 255 - /> 189 + <> 190 + <Pressable 191 + testID={`feed-${feed.displayName}`} 192 + accessibilityRole="button" 193 + style={[styles.container, pal.border, style]} 194 + onPress={() => { 195 + if (feed.type === 'feed') { 196 + navigation.push('ProfileFeed', { 197 + name: feed.creatorDid, 198 + rkey: new AtUri(feed.uri).rkey, 199 + }) 200 + } else if (feed.type === 'list') { 201 + navigation.push('ProfileList', { 202 + name: feed.creatorDid, 203 + rkey: new AtUri(feed.uri).rkey, 204 + }) 205 + } 206 + }} 207 + key={feed.uri}> 208 + <View style={[styles.headerContainer]}> 209 + <View style={[s.mr10]}> 210 + <UserAvatar type="algo" size={36} avatar={feed.avatar} /> 211 + </View> 212 + <View style={[styles.headerTextContainer]}> 213 + <Text style={[pal.text, s.bold]} numberOfLines={3}> 214 + {feed.displayName} 215 + </Text> 216 + <Text style={[pal.textLight]} numberOfLines={3}> 217 + {feed.type === 'feed' ? ( 218 + <Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 256 219 ) : ( 257 - <FontAwesomeIcon 258 - icon="plus" 259 - size={18} 260 - color={pal.colors.link} 261 - /> 220 + <Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans> 262 221 )} 263 - </Pressable> 222 + </Text> 264 223 </View> 265 - )} 266 - </View> 224 + 225 + {showSaveBtn && feed.type === 'feed' && ( 226 + <View style={[s.justifyCenter]}> 227 + <Pressable 228 + testID={`feed-${feed.displayName}-toggleSave`} 229 + disabled={isSavePending || isPinPending || isRemovePending} 230 + accessibilityRole="button" 231 + accessibilityLabel={ 232 + isSaved 233 + ? _(msg`Remove from my feeds`) 234 + : _(msg`Add to my feeds`) 235 + } 236 + accessibilityHint="" 237 + onPress={onToggleSaved} 238 + hitSlop={15} 239 + style={styles.btn}> 240 + {isSaved ? ( 241 + <FontAwesomeIcon 242 + icon={['far', 'trash-can']} 243 + size={19} 244 + color={pal.colors.icon} 245 + /> 246 + ) : ( 247 + <FontAwesomeIcon 248 + icon="plus" 249 + size={18} 250 + color={pal.colors.link} 251 + /> 252 + )} 253 + </Pressable> 254 + </View> 255 + )} 256 + </View> 267 257 268 - {showDescription && feed.description ? ( 269 - <RichText 270 - style={[t.atoms.text_contrast_high, styles.description]} 271 - value={feed.description} 272 - numberOfLines={3} 273 - /> 274 - ) : null} 258 + {showDescription && feed.description ? ( 259 + <RichText 260 + style={[t.atoms.text_contrast_high, styles.description]} 261 + value={feed.description} 262 + numberOfLines={3} 263 + /> 264 + ) : null} 265 + 266 + {showLikes && feed.type === 'feed' ? ( 267 + <Text type="sm-medium" style={[pal.text, pal.textLight]}> 268 + <Trans> 269 + Liked by {feed.likeCount || 0}{' '} 270 + {pluralize(feed.likeCount || 0, 'user')} 271 + </Trans> 272 + </Text> 273 + ) : null} 274 + </Pressable> 275 275 276 - {showLikes && feed.type === 'feed' ? ( 277 - <Text type="sm-medium" style={[pal.text, pal.textLight]}> 278 - <Trans> 279 - Liked by {feed.likeCount || 0}{' '} 280 - {pluralize(feed.likeCount || 0, 'user')} 281 - </Trans> 282 - </Text> 283 - ) : null} 284 - </Pressable> 276 + <Prompt.Basic 277 + control={removePromptControl} 278 + title={_(msg`Remove from my feeds?`)} 279 + description={_( 280 + msg`Are you sure you want to remove ${feed.displayName} from your feeds?`, 281 + )} 282 + onConfirm={onUnsave} 283 + confirmButtonCta={_(msg`Remove`)} 284 + confirmButtonColor="negative" 285 + /> 286 + </> 285 287 ) 286 288 } 287 289
-132
src/view/com/modals/Confirm.tsx
··· 1 - import React, {useState} from 'react' 2 - import { 3 - ActivityIndicator, 4 - StyleSheet, 5 - TouchableOpacity, 6 - View, 7 - } from 'react-native' 8 - import {Text} from '../util/text/Text' 9 - import {s, colors} from 'lib/styles' 10 - import {ErrorMessage} from '../util/error/ErrorMessage' 11 - import {cleanError} from 'lib/strings/errors' 12 - import {usePalette} from 'lib/hooks/usePalette' 13 - import {isWeb} from 'platform/detection' 14 - import {useLingui} from '@lingui/react' 15 - import {Trans, msg} from '@lingui/macro' 16 - import type {ConfirmModal} from '#/state/modals' 17 - import {useModalControls} from '#/state/modals' 18 - 19 - export const snapPoints = ['50%'] 20 - 21 - export function Component({ 22 - title, 23 - message, 24 - onPressConfirm, 25 - onPressCancel, 26 - confirmBtnText, 27 - confirmBtnStyle, 28 - cancelBtnText, 29 - }: ConfirmModal) { 30 - const pal = usePalette('default') 31 - const {_} = useLingui() 32 - const {closeModal} = useModalControls() 33 - const [isProcessing, setIsProcessing] = useState<boolean>(false) 34 - const [error, setError] = useState<string>('') 35 - const onPress = async () => { 36 - setError('') 37 - setIsProcessing(true) 38 - try { 39 - await onPressConfirm() 40 - closeModal() 41 - return 42 - } catch (e: any) { 43 - setError(cleanError(e)) 44 - setIsProcessing(false) 45 - } 46 - } 47 - return ( 48 - <View testID="confirmModal" style={[pal.view, styles.container]}> 49 - <Text type="title-xl" style={[pal.text, styles.title]}> 50 - {title} 51 - </Text> 52 - {typeof message === 'string' ? ( 53 - <Text type="xl" style={[pal.textLight, styles.description]}> 54 - {message} 55 - </Text> 56 - ) : ( 57 - message() 58 - )} 59 - {error ? ( 60 - <View style={s.mt10}> 61 - <ErrorMessage message={error} /> 62 - </View> 63 - ) : undefined} 64 - <View style={s.flex1} /> 65 - {isProcessing ? ( 66 - <View style={[styles.btn, s.mt10]}> 67 - <ActivityIndicator /> 68 - </View> 69 - ) : ( 70 - <TouchableOpacity 71 - testID="confirmBtn" 72 - onPress={onPress} 73 - style={[styles.btn, confirmBtnStyle]} 74 - accessibilityRole="button" 75 - accessibilityLabel={_(msg({message: 'Confirm', context: 'action'}))} 76 - accessibilityHint=""> 77 - <Text style={[s.white, s.bold, s.f18]}> 78 - {confirmBtnText ?? <Trans context="action">Confirm</Trans>} 79 - </Text> 80 - </TouchableOpacity> 81 - )} 82 - {onPressCancel === undefined ? null : ( 83 - <TouchableOpacity 84 - testID="cancelBtn" 85 - onPress={onPressCancel} 86 - style={[styles.btnCancel, s.mt10]} 87 - accessibilityRole="button" 88 - accessibilityLabel={_(msg({message: 'Cancel', context: 'action'}))} 89 - accessibilityHint=""> 90 - <Text type="button-lg" style={pal.textLight}> 91 - {cancelBtnText ?? <Trans context="action">Cancel</Trans>} 92 - </Text> 93 - </TouchableOpacity> 94 - )} 95 - </View> 96 - ) 97 - } 98 - 99 - const styles = StyleSheet.create({ 100 - container: { 101 - flex: 1, 102 - padding: 10, 103 - paddingBottom: isWeb ? 0 : 60, 104 - }, 105 - title: { 106 - textAlign: 'center', 107 - marginBottom: 12, 108 - }, 109 - description: { 110 - textAlign: 'center', 111 - paddingHorizontal: 22, 112 - marginBottom: 10, 113 - }, 114 - btn: { 115 - flexDirection: 'row', 116 - alignItems: 'center', 117 - justifyContent: 'center', 118 - borderRadius: 32, 119 - padding: 14, 120 - marginTop: 22, 121 - marginHorizontal: 44, 122 - backgroundColor: colors.blue3, 123 - }, 124 - btnCancel: { 125 - flexDirection: 'row', 126 - alignItems: 'center', 127 - justifyContent: 'center', 128 - borderRadius: 32, 129 - padding: 14, 130 - marginHorizontal: 20, 131 - }, 132 - })
+1 -5
src/view/com/modals/Modal.tsx
··· 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 7 8 8 import {useModals, useModalControls} from '#/state/modals' 9 - import * as ConfirmModal from './Confirm' 10 9 import * as EditProfileModal from './EditProfile' 11 10 import * as RepostModal from './Repost' 12 11 import * as SelfLabelModal from './SelfLabel' ··· 66 65 67 66 let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS 68 67 let element 69 - if (activeModal?.name === 'confirm') { 70 - snapPoints = ConfirmModal.snapPoints 71 - element = <ConfirmModal.Component {...activeModal} /> 72 - } else if (activeModal?.name === 'edit-profile') { 68 + if (activeModal?.name === 'edit-profile') { 73 69 snapPoints = EditProfileModal.snapPoints 74 70 element = <EditProfileModal.Component {...activeModal} /> 75 71 } else if (activeModal?.name === 'report') {
+1 -4
src/view/com/modals/Modal.web.tsx
··· 7 7 8 8 import {useModals, useModalControls} from '#/state/modals' 9 9 import type {Modal as ModalIface} from '#/state/modals' 10 - import * as ConfirmModal from './Confirm' 11 10 import * as EditProfileModal from './EditProfile' 12 11 import * as ReportModal from './report/Modal' 13 12 import * as AppealLabelModal from './AppealLabel' ··· 78 77 } 79 78 80 79 let element 81 - if (modal.name === 'confirm') { 82 - element = <ConfirmModal.Component {...modal} /> 83 - } else if (modal.name === 'edit-profile') { 80 + if (modal.name === 'edit-profile') { 84 81 element = <EditProfileModal.Component {...modal} /> 85 82 } else if (modal.name === 'report') { 86 83 element = <ReportModal.Component {...modal} />
+47 -42
src/view/com/posts/FeedErrorMessage.tsx
··· 9 9 import {useNavigation} from '@react-navigation/native' 10 10 import {NavigationProp} from 'lib/routes/types' 11 11 import {logger} from '#/logger' 12 - import {useModalControls} from '#/state/modals' 13 12 import {msg as msgLingui, Trans} from '@lingui/macro' 14 13 import {useLingui} from '@lingui/react' 15 14 import {FeedDescriptor} from '#/state/queries/post-feed' 16 15 import {EmptyState} from '../util/EmptyState' 17 16 import {cleanError} from '#/lib/strings/errors' 18 17 import {useRemoveFeedMutation} from '#/state/queries/preferences' 18 + import * as Prompt from '#/components/Prompt' 19 19 20 20 export enum KnownError { 21 21 Block = 'Block', ··· 118 118 ) 119 119 const [_, uri] = feedDesc.split('|') 120 120 const [ownerDid] = safeParseFeedgenUri(uri) 121 - const {openModal, closeModal} = useModalControls() 121 + const removePromptControl = Prompt.usePromptControl() 122 122 const {mutateAsync: removeFeed} = useRemoveFeedMutation() 123 123 124 124 const onViewProfile = React.useCallback(() => { 125 125 navigation.navigate('Profile', {name: ownerDid}) 126 126 }, [navigation, ownerDid]) 127 127 128 + const onPressRemoveFeed = React.useCallback(() => { 129 + removePromptControl.open() 130 + }, [removePromptControl]) 131 + 128 132 const onRemoveFeed = React.useCallback(async () => { 129 - openModal({ 130 - name: 'confirm', 131 - title: _l(msgLingui`Remove feed`), 132 - message: _l(msgLingui`Remove this feed from your saved feeds?`), 133 - async onPressConfirm() { 134 - try { 135 - await removeFeed({uri}) 136 - } catch (err) { 137 - Toast.show( 138 - _l( 139 - msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`, 140 - ), 141 - ) 142 - logger.error('Failed to remove feed', {message: err}) 143 - } 144 - }, 145 - onPressCancel() { 146 - closeModal() 147 - }, 148 - }) 149 - }, [openModal, closeModal, uri, removeFeed, _l]) 133 + try { 134 + await removeFeed({uri}) 135 + } catch (err) { 136 + Toast.show( 137 + _l( 138 + msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`, 139 + ), 140 + ) 141 + logger.error('Failed to remove feed', {message: err}) 142 + } 143 + }, [uri, removeFeed, _l]) 150 144 151 145 const cta = React.useMemo(() => { 152 146 switch (knownError) { ··· 179 173 }, [knownError, onViewProfile, onRemoveFeed, _l]) 180 174 181 175 return ( 182 - <View 183 - style={[ 184 - pal.border, 185 - pal.viewLight, 186 - { 187 - borderTopWidth: 1, 188 - paddingHorizontal: 20, 189 - paddingVertical: 18, 190 - gap: 12, 191 - }, 192 - ]}> 193 - <Text style={pal.text}>{msg}</Text> 176 + <> 177 + <View 178 + style={[ 179 + pal.border, 180 + pal.viewLight, 181 + { 182 + borderTopWidth: 1, 183 + paddingHorizontal: 20, 184 + paddingVertical: 18, 185 + gap: 12, 186 + }, 187 + ]}> 188 + <Text style={pal.text}>{msg}</Text> 189 + 190 + {rawError?.message && ( 191 + <Text style={pal.textLight}> 192 + <Trans>Message from server: {rawError.message}</Trans> 193 + </Text> 194 + )} 194 195 195 - {rawError?.message && ( 196 - <Text style={pal.textLight}> 197 - <Trans>Message from server: {rawError.message}</Trans> 198 - </Text> 199 - )} 196 + {cta} 197 + </View> 200 198 201 - {cta} 202 - </View> 199 + <Prompt.Basic 200 + control={removePromptControl} 201 + title={_l(msgLingui`Remove feed?`)} 202 + description={_l(msgLingui`Remove this feed from your saved feeds`)} 203 + onConfirm={onPressRemoveFeed} 204 + confirmButtonCta={_l(msgLingui`Remove`)} 205 + confirmButtonColor="negative" 206 + /> 207 + </> 203 208 ) 204 209 } 205 210
+26 -21
src/view/com/profile/ProfileHeader.tsx
··· 52 52 import {useProfileShadow} from 'state/cache/profile-shadow' 53 53 import {atoms as a} from '#/alf' 54 54 import {ProfileMenu} from 'view/com/profile/ProfileMenu' 55 + import * as Prompt from '#/components/Prompt' 55 56 56 57 let ProfileHeaderLoading = (_props: {}): React.ReactNode => { 57 58 const pal = usePalette('default') ··· 104 105 const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) 105 106 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) 106 107 const [__, queueUnblock] = useProfileBlockMutationQueue(profile) 108 + const unblockPromptControl = Prompt.usePromptControl() 107 109 const moderation = useMemo( 108 110 () => moderateProfile(profile, moderationOpts), 109 111 [profile, moderationOpts], ··· 176 178 }) 177 179 }, [track, openModal, profile]) 178 180 179 - const onPressUnblockAccount = React.useCallback(() => { 181 + const unblockAccount = React.useCallback(async () => { 180 182 track('ProfileHeader:UnblockAccountButtonClicked') 181 - openModal({ 182 - name: 'confirm', 183 - title: _(msg`Unblock Account`), 184 - message: _( 185 - msg`The account will be able to interact with you after unblocking.`, 186 - ), 187 - onPressConfirm: async () => { 188 - try { 189 - await queueUnblock() 190 - Toast.show(_(msg`Account unblocked`)) 191 - } catch (e: any) { 192 - if (e?.name !== 'AbortError') { 193 - logger.error('Failed to unblock account', {message: e}) 194 - Toast.show(_(msg`There was an issue! ${e.toString()}`)) 195 - } 196 - } 197 - }, 198 - }) 199 - }, [_, openModal, queueUnblock, track]) 183 + try { 184 + await queueUnblock() 185 + Toast.show(_(msg`Account unblocked`)) 186 + } catch (e: any) { 187 + if (e?.name !== 'AbortError') { 188 + logger.error('Failed to unblock account', {message: e}) 189 + Toast.show(_(msg`There was an issue! ${e.toString()}`)) 190 + } 191 + } 192 + }, [_, queueUnblock, track]) 200 193 201 194 const isMe = React.useMemo( 202 195 () => currentAccount?.did === profile.did, ··· 242 235 profile.viewer?.blockingByList ? null : ( 243 236 <TouchableOpacity 244 237 testID="unblockBtn" 245 - onPress={onPressUnblockAccount} 238 + onPress={() => unblockPromptControl.open()} 246 239 style={[styles.btn, styles.mainBtn, pal.btn]} 247 240 accessibilityRole="button" 248 241 accessibilityLabel={_(msg`Unblock`)} ··· 475 468 /> 476 469 </View> 477 470 </TouchableWithoutFeedback> 471 + <Prompt.Basic 472 + control={unblockPromptControl} 473 + title={_(msg`Unblock Account?`)} 474 + description={_( 475 + msg`The account will be able to interact with you after unblocking.`, 476 + )} 477 + onConfirm={unblockAccount} 478 + confirmButtonCta={ 479 + profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) 480 + } 481 + confirmButtonColor="negative" 482 + /> 478 483 </View> 479 484 ) 480 485 }
+47 -39
src/view/com/profile/ProfileMenu.tsx
··· 33 33 import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2' 34 34 import {logger} from '#/logger' 35 35 import {Shadow} from 'state/cache/types' 36 + import * as Prompt from '#/components/Prompt' 36 37 37 38 let ProfileMenu = ({ 38 39 profile, ··· 52 53 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile) 53 54 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) 54 55 const [, queueUnfollow] = useProfileFollowMutationQueue(profile) 56 + 57 + const blockPromptControl = Prompt.usePromptControl() 55 58 56 59 const invalidateProfileQuery = React.useCallback(() => { 57 60 queryClient.invalidateQueries({ ··· 102 105 } 103 106 }, [profile.viewer?.muted, track, queueUnmute, _, queueMute]) 104 107 105 - const onPressBlockAccount = React.useCallback(async () => { 108 + const blockAccount = React.useCallback(async () => { 106 109 if (profile.viewer?.blocking) { 107 110 track('ProfileHeader:UnblockAccountButtonClicked') 108 - openModal({ 109 - name: 'confirm', 110 - title: _(msg`Unblock Account`), 111 - message: _( 112 - msg`The account will be able to interact with you after unblocking.`, 113 - ), 114 - onPressConfirm: async () => { 115 - try { 116 - await queueUnblock() 117 - Toast.show(_(msg`Account unblocked`)) 118 - } catch (e: any) { 119 - if (e?.name !== 'AbortError') { 120 - logger.error('Failed to unblock account', {message: e}) 121 - Toast.show(_(msg`There was an issue! ${e.toString()}`)) 122 - } 123 - } 124 - }, 125 - }) 111 + try { 112 + await queueUnblock() 113 + Toast.show(_(msg`Account unblocked`)) 114 + } catch (e: any) { 115 + if (e?.name !== 'AbortError') { 116 + logger.error('Failed to unblock account', {message: e}) 117 + Toast.show(_(msg`There was an issue! ${e.toString()}`)) 118 + } 119 + } 126 120 } else { 127 121 track('ProfileHeader:BlockAccountButtonClicked') 128 - openModal({ 129 - name: 'confirm', 130 - title: _(msg`Block Account`), 131 - message: _( 132 - msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 133 - ), 134 - onPressConfirm: async () => { 135 - try { 136 - await queueBlock() 137 - Toast.show(_(msg`Account blocked`)) 138 - } catch (e: any) { 139 - if (e?.name !== 'AbortError') { 140 - logger.error('Failed to block account', {message: e}) 141 - Toast.show(_(msg`There was an issue! ${e.toString()}`)) 142 - } 143 - } 144 - }, 145 - }) 122 + try { 123 + await queueBlock() 124 + Toast.show(_(msg`Account blocked`)) 125 + } catch (e: any) { 126 + if (e?.name !== 'AbortError') { 127 + logger.error('Failed to block account', {message: e}) 128 + Toast.show(_(msg`There was an issue! ${e.toString()}`)) 129 + } 130 + } 146 131 } 147 - }, [profile.viewer?.blocking, track, openModal, _, queueUnblock, queueBlock]) 132 + }, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock]) 148 133 149 134 const onPressUnfollowAccount = React.useCallback(async () => { 150 135 track('ProfileHeader:UnfollowButtonClicked') ··· 268 253 ? _(msg`Unblock Account`) 269 254 : _(msg`Block Account`) 270 255 } 271 - onPress={onPressBlockAccount}> 256 + onPress={() => blockPromptControl.open()}> 272 257 <Menu.ItemText> 273 258 {profile.viewer?.blocking ? ( 274 259 <Trans>Unblock Account</Trans> ··· 299 284 )} 300 285 </Menu.Outer> 301 286 </Menu.Root> 287 + 288 + <Prompt.Basic 289 + control={blockPromptControl} 290 + title={ 291 + profile.viewer?.blocking 292 + ? _(msg`Unblock Account?`) 293 + : _(msg`Block Account?`) 294 + } 295 + description={ 296 + profile.viewer?.blocking 297 + ? _( 298 + msg`The account will be able to interact with you after unblocking.`, 299 + ) 300 + : _( 301 + msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 302 + ) 303 + } 304 + onConfirm={blockAccount} 305 + confirmButtonCta={ 306 + profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) 307 + } 308 + confirmButtonColor="negative" 309 + /> 302 310 </EventStopper> 303 311 ) 304 312 }
+25 -18
src/view/com/util/forms/PostDropdownBtn.tsx
··· 14 14 import {shareUrl} from 'lib/sharing' 15 15 import * as Toast from '../Toast' 16 16 import {EventStopper} from '../EventStopper' 17 + import {useDialogControl} from '#/components/Dialog' 18 + import * as Prompt from '#/components/Prompt' 17 19 import {useModalControls} from '#/state/modals' 18 20 import {makeProfileLink} from '#/lib/routes/links' 19 21 import {CommonNavigatorParams} from '#/lib/routes/types' ··· 81 83 const openLink = useOpenLink() 82 84 const navigation = useNavigation() 83 85 const {mutedWordsDialogControl} = useGlobalDialogsControlContext() 86 + const deletePromptControl = useDialogControl() 87 + const hidePromptControl = useDialogControl() 84 88 85 89 const rootUri = record.reply?.root?.uri || postUri 86 90 const isThreadMuted = mutedThreads.includes(rootUri) ··· 257 261 <Menu.Item 258 262 testID="postDropdownHideBtn" 259 263 label={_(msg`Hide post`)} 260 - onPress={() => { 261 - openModal({ 262 - name: 'confirm', 263 - title: _(msg`Hide this post?`), 264 - message: _( 265 - msg`This will hide this post from your feeds.`, 266 - ), 267 - onPressConfirm: onHidePost, 268 - }) 269 - }}> 264 + onPress={hidePromptControl.open}> 270 265 <Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText> 271 266 <Menu.ItemIcon icon={EyeSlash} position="right" /> 272 267 </Menu.Item> ··· 298 293 <Menu.Item 299 294 testID="postDropdownDeleteBtn" 300 295 label={_(msg`Delete post`)} 301 - onPress={() => { 302 - openModal({ 303 - name: 'confirm', 304 - title: _(msg`Delete this post?`), 305 - message: _(msg`Are you sure? This cannot be undone.`), 306 - onPressConfirm: onDeletePost, 307 - }) 308 - }}> 296 + onPress={deletePromptControl.open}> 309 297 <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> 310 298 <Menu.ItemIcon icon={Trash} position="right" /> 311 299 </Menu.Item> ··· 335 323 </Menu.Group> 336 324 </Menu.Outer> 337 325 </Menu.Root> 326 + 327 + <Prompt.Basic 328 + control={deletePromptControl} 329 + title={_(msg`Delete this post?`)} 330 + description={_( 331 + msg`If you remove this post, you won't be able to recover it.`, 332 + )} 333 + onConfirm={onDeletePost} 334 + confirmButtonCta={_(msg`Delete`)} 335 + confirmButtonColor="negative" 336 + /> 337 + 338 + <Prompt.Basic 339 + control={hidePromptControl} 340 + title={_(msg`Hide this post?`)} 341 + description={_(msg`This post will be hidden from feeds.`)} 342 + onConfirm={onHidePost} 343 + confirmButtonCta={_(msg`Hide`)} 344 + /> 338 345 </EventStopper> 339 346 ) 340 347 }
+22 -14
src/view/screens/AppPasswords.tsx
··· 29 29 } from '#/state/queries/app-passwords' 30 30 import {ErrorScreen} from '../com/util/error/ErrorScreen' 31 31 import {cleanError} from '#/lib/strings/errors' 32 + import * as Prompt from '#/components/Prompt' 33 + import {useDialogControl} from '#/components/Dialog' 32 34 33 35 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> 34 36 export function AppPasswords({}: Props) { ··· 212 214 }) { 213 215 const pal = usePalette('default') 214 216 const {_} = useLingui() 215 - const {openModal} = useModalControls() 217 + const control = useDialogControl() 216 218 const {contentLanguages} = useLanguagePrefs() 217 219 const deleteMutation = useAppPasswordDeleteMutation() 218 220 219 221 const onDelete = React.useCallback(async () => { 220 - openModal({ 221 - name: 'confirm', 222 - title: _(msg`Delete app password`), 223 - message: _( 224 - msg`Are you sure you want to delete the app password "${name}"?`, 225 - ), 226 - async onPressConfirm() { 227 - await deleteMutation.mutateAsync({name}) 228 - Toast.show(_(msg`App password deleted`)) 229 - }, 230 - }) 231 - }, [deleteMutation, openModal, name, _]) 222 + await deleteMutation.mutateAsync({name}) 223 + Toast.show(_(msg`App password deleted`)) 224 + }, [deleteMutation, name, _]) 225 + 226 + const onPress = React.useCallback(() => { 227 + control.open() 228 + }, [control]) 232 229 233 230 const primaryLocale = 234 231 contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' ··· 237 234 <TouchableOpacity 238 235 testID={testID} 239 236 style={[styles.item, pal.border]} 240 - onPress={onDelete} 237 + onPress={onPress} 241 238 accessibilityRole="button" 242 239 accessibilityLabel={_(msg`Delete app password`)} 243 240 accessibilityHint=""> ··· 260 257 </Text> 261 258 </View> 262 259 <FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> 260 + 261 + <Prompt.Basic 262 + control={control} 263 + title={_(msg`Delete app password?`)} 264 + description={_( 265 + msg`Are you sure you want to delete the app password "${name}"?`, 266 + )} 267 + onConfirm={onDelete} 268 + confirmButtonCta={_(msg`Delete`)} 269 + confirmButtonColor="negative" 270 + /> 263 271 </TouchableOpacity> 264 272 ) 265 273 }
+101 -93
src/view/screens/ProfileList.tsx
··· 61 61 import {useAnalytics} from '#/lib/analytics/analytics' 62 62 import {listenSoftReset} from '#/state/events' 63 63 import {atoms as a, useTheme} from '#/alf' 64 + import * as Prompt from '#/components/Prompt' 65 + import {useDialogControl} from '#/components/Dialog' 64 66 65 67 const SECTION_TITLES_CURATE = ['Posts', 'About'] 66 68 const SECTION_TITLES_MOD = ['About'] ··· 234 236 const {_} = useLingui() 235 237 const navigation = useNavigation<NavigationProp>() 236 238 const {currentAccount} = useSession() 237 - const {openModal, closeModal} = useModalControls() 239 + const {openModal} = useModalControls() 238 240 const listMuteMutation = useListMuteMutation() 239 241 const listBlockMutation = useListBlockMutation() 240 242 const listDeleteMutation = useListDeleteMutation() ··· 250 252 const {data: preferences} = usePreferencesQuery() 251 253 const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() 252 254 const {track} = useAnalytics() 255 + 256 + const deleteListPromptControl = useDialogControl() 257 + const subscribeMutePromptControl = useDialogControl() 258 + const subscribeBlockPromptControl = useDialogControl() 253 259 254 260 const isPinned = preferences?.feeds?.pinned?.includes(list.uri) 255 261 const isSaved = preferences?.feeds?.saved?.includes(list.uri) ··· 269 275 } 270 276 }, [list.uri, isPinned, pinFeed, unpinFeed, _]) 271 277 272 - const onSubscribeMute = useCallback(() => { 273 - openModal({ 274 - name: 'confirm', 275 - title: _(msg`Mute these accounts?`), 276 - message: _( 277 - msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, 278 - ), 279 - confirmBtnText: _(msg`Mute this List`), 280 - async onPressConfirm() { 281 - try { 282 - await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) 283 - Toast.show(_(msg`List muted`)) 284 - track('Lists:Mute') 285 - } catch { 286 - Toast.show( 287 - _( 288 - msg`There was an issue. Please check your internet connection and try again.`, 289 - ), 290 - ) 291 - } 292 - }, 293 - onPressCancel() { 294 - closeModal() 295 - }, 296 - }) 297 - }, [openModal, closeModal, list, listMuteMutation, track, _]) 278 + const onSubscribeMute = useCallback(async () => { 279 + try { 280 + await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) 281 + Toast.show(_(msg`List muted`)) 282 + track('Lists:Mute') 283 + } catch { 284 + Toast.show( 285 + _( 286 + msg`There was an issue. Please check your internet connection and try again.`, 287 + ), 288 + ) 289 + } 290 + }, [list, listMuteMutation, track, _]) 298 291 299 292 const onUnsubscribeMute = useCallback(async () => { 300 293 try { ··· 310 303 } 311 304 }, [list, listMuteMutation, track, _]) 312 305 313 - const onSubscribeBlock = useCallback(() => { 314 - openModal({ 315 - name: 'confirm', 316 - title: _(msg`Block these accounts?`), 317 - message: _( 318 - msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 319 - ), 320 - confirmBtnText: _(msg`Block this List`), 321 - async onPressConfirm() { 322 - try { 323 - await listBlockMutation.mutateAsync({uri: list.uri, block: true}) 324 - Toast.show(_(msg`List blocked`)) 325 - track('Lists:Block') 326 - } catch { 327 - Toast.show( 328 - _( 329 - msg`There was an issue. Please check your internet connection and try again.`, 330 - ), 331 - ) 332 - } 333 - }, 334 - onPressCancel() { 335 - closeModal() 336 - }, 337 - }) 338 - }, [openModal, closeModal, list, listBlockMutation, track, _]) 306 + const onSubscribeBlock = useCallback(async () => { 307 + try { 308 + await listBlockMutation.mutateAsync({uri: list.uri, block: true}) 309 + Toast.show(_(msg`List blocked`)) 310 + track('Lists:Block') 311 + } catch { 312 + Toast.show( 313 + _( 314 + msg`There was an issue. Please check your internet connection and try again.`, 315 + ), 316 + ) 317 + } 318 + }, [list, listBlockMutation, track, _]) 339 319 340 320 const onUnsubscribeBlock = useCallback(async () => { 341 321 try { ··· 358 338 }) 359 339 }, [openModal, list]) 360 340 361 - const onPressDelete = useCallback(() => { 362 - openModal({ 363 - name: 'confirm', 364 - title: _(msg`Delete List`), 365 - message: _(msg`Are you sure?`), 366 - async onPressConfirm() { 367 - await listDeleteMutation.mutateAsync({uri: list.uri}) 341 + const onPressDelete = useCallback(async () => { 342 + await listDeleteMutation.mutateAsync({uri: list.uri}) 368 343 369 - if (isSaved || isPinned) { 370 - const {saved, pinned} = preferences!.feeds 344 + if (isSaved || isPinned) { 345 + const {saved, pinned} = preferences!.feeds 371 346 372 - setSavedFeeds({ 373 - saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved, 374 - pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned, 375 - }) 376 - } 347 + setSavedFeeds({ 348 + saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved, 349 + pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned, 350 + }) 351 + } 377 352 378 - Toast.show(_(msg`List deleted`)) 379 - track('Lists:Delete') 380 - if (navigation.canGoBack()) { 381 - navigation.goBack() 382 - } else { 383 - navigation.navigate('Home') 384 - } 385 - }, 386 - }) 353 + Toast.show(_(msg`List deleted`)) 354 + track('Lists:Delete') 355 + if (navigation.canGoBack()) { 356 + navigation.goBack() 357 + } else { 358 + navigation.navigate('Home') 359 + } 387 360 }, [ 388 - openModal, 389 361 list, 390 362 listDeleteMutation, 391 363 navigation, ··· 443 415 items.push({ 444 416 testID: 'listHeaderDropdownDeleteBtn', 445 417 label: _(msg`Delete List`), 446 - onPress: onPressDelete, 418 + onPress: deleteListPromptControl.open, 447 419 icon: { 448 420 ios: { 449 421 name: 'trash', ··· 489 461 items.push({ 490 462 testID: 'listHeaderDropdownMuteBtn', 491 463 label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`), 492 - onPress: isMuting ? onUnsubscribeMute : onSubscribeMute, 464 + onPress: isMuting 465 + ? onUnsubscribeMute 466 + : subscribeMutePromptControl.open, 493 467 icon: { 494 468 ios: { 495 469 name: isMuting ? 'eye' : 'eye.slash', ··· 504 478 items.push({ 505 479 testID: 'listHeaderDropdownBlockBtn', 506 480 label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`), 507 - onPress: isBlocking ? onUnsubscribeBlock : onSubscribeBlock, 481 + onPress: isBlocking 482 + ? onUnsubscribeBlock 483 + : subscribeBlockPromptControl.open, 508 484 icon: { 509 485 ios: { 510 486 name: 'person.fill.xmark', ··· 517 493 } 518 494 return items 519 495 }, [ 496 + _, 497 + onPressShare, 520 498 isOwner, 521 - onPressShare, 499 + isModList, 500 + isPinned, 501 + isCurateList, 522 502 onPressEdit, 523 - onPressDelete, 503 + deleteListPromptControl.open, 524 504 onPressReport, 525 - _, 526 - isModList, 527 - isPinned, 505 + isPending, 528 506 unpinFeed, 529 - isPending, 530 507 list.uri, 531 - isCurateList, 532 - isMuting, 533 508 isBlocking, 509 + isMuting, 534 510 onUnsubscribeMute, 535 - onSubscribeMute, 511 + subscribeMutePromptControl.open, 536 512 onUnsubscribeBlock, 537 - onSubscribeBlock, 513 + subscribeBlockPromptControl.open, 538 514 ]) 539 515 540 516 const subscribeDropdownItems: DropdownItem[] = useMemo(() => { ··· 542 518 { 543 519 testID: 'subscribeDropdownMuteBtn', 544 520 label: _(msg`Mute accounts`), 545 - onPress: onSubscribeMute, 521 + onPress: subscribeMutePromptControl.open, 546 522 icon: { 547 523 ios: { 548 524 name: 'speaker.slash', ··· 554 530 { 555 531 testID: 'subscribeDropdownBlockBtn', 556 532 label: _(msg`Block accounts`), 557 - onPress: onSubscribeBlock, 533 + onPress: subscribeBlockPromptControl.open, 558 534 icon: { 559 535 ios: { 560 536 name: 'person.fill.xmark', ··· 564 540 }, 565 541 }, 566 542 ] 567 - }, [onSubscribeMute, onSubscribeBlock, _]) 543 + }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open]) 568 544 569 545 return ( 570 546 <ProfileSubpageHeader ··· 620 596 <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} /> 621 597 </View> 622 598 </NativeDropdown> 599 + 600 + <Prompt.Basic 601 + control={deleteListPromptControl} 602 + title={_(msg`Delete this list?`)} 603 + description={_( 604 + msg`If you delete this list, you won't be able to recover it.`, 605 + )} 606 + onConfirm={onPressDelete} 607 + confirmButtonCta={_(msg`Delete`)} 608 + confirmButtonColor="negative" 609 + /> 610 + 611 + <Prompt.Basic 612 + control={subscribeMutePromptControl} 613 + title={_(msg`Mute these accounts?`)} 614 + description={_( 615 + msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, 616 + )} 617 + onConfirm={onSubscribeMute} 618 + confirmButtonCta={_(msg`Mute list`)} 619 + /> 620 + 621 + <Prompt.Basic 622 + control={subscribeBlockPromptControl} 623 + title={_(msg`Block these accounts?`)} 624 + description={_( 625 + msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, 626 + )} 627 + onConfirm={onSubscribeBlock} 628 + confirmButtonCta={_(msg`Block list`)} 629 + confirmButtonColor="negative" 630 + /> 623 631 </ProfileSubpageHeader> 624 632 ) 625 633 }
+1 -1
src/view/screens/Storybook/Dialogs.tsx
··· 68 68 </Prompt.Description> 69 69 <Prompt.Actions> 70 70 <Prompt.Cancel>Cancel</Prompt.Cancel> 71 - <Prompt.Action>Confirm</Prompt.Action> 71 + <Prompt.Action onPress={() => {}}>Confirm</Prompt.Action> 72 72 </Prompt.Actions> 73 73 </Prompt.Outer> 74 74