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>
zio/stable
Hailey 2024-03-12 16:56:14 -07:00 committed by GitHub
parent 090b35e52e
commit 9f2f7f221c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 540 additions and 605 deletions

View File

@ -111,6 +111,12 @@ export const atoms = {
flex_row: { flex_row: {
flexDirection: 'row', flexDirection: 'row',
}, },
flex_col_reverse: {
flexDirection: 'column-reverse',
},
flex_row_reverse: {
flexDirection: 'row-reverse',
},
flex_wrap: { flex_wrap: {
flexWrap: 'wrap', flexWrap: 'wrap',
}, },

View File

@ -75,6 +75,7 @@ export function Outer({
control, control,
onClose, onClose,
nativeOptions, nativeOptions,
testID,
}: React.PropsWithChildren<DialogOuterProps>) { }: React.PropsWithChildren<DialogOuterProps>) {
const t = useTheme() const t = useTheme()
const sheet = React.useRef<BottomSheet>(null) const sheet = React.useRef<BottomSheet>(null)
@ -145,7 +146,8 @@ export function Outer({
accessibilityViewIsModal accessibilityViewIsModal
// Android // Android
importantForAccessibility="yes" importantForAccessibility="yes"
style={[a.absolute, a.inset_0]}> style={[a.absolute, a.inset_0]}
testID={testID}>
<BottomSheet <BottomSheet
enableDynamicSizing={!hasSnapPoints} enableDynamicSizing={!hasSnapPoints}
enablePanDownToClose enablePanDownToClose

View File

@ -46,6 +46,7 @@ export type DialogOuterProps = {
sheet?: Omit<BottomSheetProps, 'children'> sheet?: Omit<BottomSheetProps, 'children'>
} }
webOptions?: {} webOptions?: {}
testID?: string
} }
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T

View File

@ -1,11 +1,11 @@
import React from 'react' import React from 'react'
import {View, PressableProps} from 'react-native' import {View} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {useTheme, atoms as a, useBreakpoints} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {Button} from '#/components/Button' import {Button, ButtonColor, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@ -22,8 +22,10 @@ const Context = React.createContext<{
export function Outer({ export function Outer({
children, children,
control, control,
testID,
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
testID?: string
}>) { }>) {
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const titleId = React.useId() const titleId = React.useId()
@ -35,7 +37,7 @@ export function Outer({
) )
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control} testID={testID}>
<Context.Provider value={context}> <Context.Provider value={context}>
<Dialog.Handle /> <Dialog.Handle />
@ -80,7 +82,9 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
a.w_full, a.w_full,
a.gap_sm, a.gap_sm,
a.justify_end, 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} {children}
</View> </View>
@ -89,18 +93,29 @@ export function Actions({children}: React.PropsWithChildren<{}>) {
export function Cancel({ export function Cancel({
children, 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 {_} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext() const {close} = Dialog.useDialogContext()
const onPress = React.useCallback(() => {
close()
}, [close])
return ( return (
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size={gtMobile ? 'small' : 'medium'} size={gtMobile ? 'small' : 'medium'}
label={_(msg`Cancel`)} label={cta || _(msg`Cancel`)}
onPress={() => close()}> onPress={onPress}>
{children} {children ? children : <ButtonText>{cta || _(msg`Cancel`)}</ButtonText>}
</Button> </Button>
) )
} }
@ -108,22 +123,70 @@ export function Cancel({
export function Action({ export function Action({
children, children,
onPress, 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 {_} = useLingui()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext() const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => { const handleOnPress = React.useCallback(() => {
close() close()
onPress?.() onPress()
}, [close, onPress]) }, [close, onPress])
return ( return (
<Button <Button
variant="solid" variant="solid"
color="primary" color={color}
size={gtMobile ? 'small' : 'medium'} size={gtMobile ? 'small' : 'medium'}
label={_(msg`Confirm`)} label={cta || _(msg`Confirm`)}
onPress={handleOnPress}> onPress={handleOnPress}
{children} testID={testID}>
{children ? children : <ButtonText>{cta || _(msg`Confirm`)}</ButtonText>}
</Button> </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>
)
}

View File

@ -277,29 +277,16 @@ function MutedWordRow({
return ( return (
<> <>
<Prompt.Outer control={control}> <Prompt.Basic
<Prompt.Title> control={control}
<Trans>Are you sure?</Trans> title={_(msg`Are you sure?`)}
</Prompt.Title> description={_(
<Prompt.Description> msg`This will delete ${word.value} from your muted words. You can always add it back later.`,
<Trans> )}
This will delete {word.value} from your muted words. You can always onConfirm={remove}
add it back later. confirmButtonCta={_(msg`Remove`)}
</Trans> confirmButtonColor="negative"
</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>
<View <View
style={[ style={[

View File

@ -2,25 +2,9 @@ import * as Updates from 'expo-updates'
import {useCallback, useEffect} from 'react' import {useCallback, useEffect} from 'react'
import {AppState} from 'react-native' import {AppState} from 'react-native'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {t} from '@lingui/macro'
export function useOTAUpdate() { export function useOTAUpdate() {
const {openModal} = useModalControls()
// HELPER FUNCTIONS // 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 () => { const checkForUpdate = useCallback(async () => {
logger.debug('useOTAUpdate: Checking for update...') logger.debug('useOTAUpdate: Checking for update...')
try { 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. // 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() await Updates.fetchUpdateAsync()
// show a popup modal
showUpdatePopup()
} catch (e) { } catch (e) {
logger.error('useOTAUpdate: Error while checking for update', { logger.error('useOTAUpdate: Error while checking for update', {
message: e, message: e,
}) })
} }
}, [showUpdatePopup]) }, [])
const updateEventListener = useCallback( const updateEventListener = useCallback((event: Updates.UpdateEvent) => {
(event: Updates.UpdateEvent) => { logger.debug('useOTAUpdate: Listening for update...')
logger.debug('useOTAUpdate: Listening for update...') if (event.type === Updates.UpdateEventType.ERROR) {
if (event.type === Updates.UpdateEventType.ERROR) { logger.error('useOTAUpdate: Error while listening for update', {
logger.error('useOTAUpdate: Error while listening for update', { message: event.message,
message: event.message, })
}) } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
} else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { // Handle no update available
// Handle no update available // do nothing
// do nothing } else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) {
} else if (event.type === Updates.UpdateEventType.UPDATE_AVAILABLE) { // Handle update available
// Handle update available // open modal, ask for user confirmation, and reload the app
// open modal, ask for user confirmation, and reload the app }
showUpdatePopup() }, [])
}
},
[showUpdatePopup],
)
useEffect(() => { useEffect(() => {
// ADD EVENT LISTENERS // ADD EVENT LISTENERS

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api'
import {StyleProp, ViewStyle} from 'react-native'
import {Image as RNImage} from 'react-native-image-crop-picker' import {Image as RNImage} from 'react-native-image-crop-picker'
import {ImageModel} from '#/state/models/media/image' 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 {EmbedPlayerSource} from '#/lib/strings/embed-player'
import {ThreadgateSetting} from '../queries/threadgate' 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 { export interface EditProfileModal {
name: 'edit-profile' name: 'edit-profile'
profile: AppBskyActorDefs.ProfileViewDetailed profile: AppBskyActorDefs.ProfileViewDetailed
@ -225,7 +213,6 @@ export type Modal =
| InviteCodesModal | InviteCodesModal
// Generic // Generic
| ConfirmModal
| LinkWarningModal | LinkWarningModal
| EmbedConsentModal | EmbedConsentModal
| InAppBrowserConsentModal | InAppBrowserConsentModal

View File

@ -49,7 +49,7 @@ import {SuggestedLanguage} from './select-language/SuggestedLanguage'
import {insertMentionAt} from 'lib/strings/mention-manip' import {insertMentionAt} from 'lib/strings/mention-manip'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModals, useModalControls} from '#/state/modals' import {useModals} from '#/state/modals'
import {useRequireAltTextEnabled} from '#/state/preferences' import {useRequireAltTextEnabled} from '#/state/preferences'
import { import {
useLanguagePrefs, useLanguagePrefs,
@ -63,6 +63,8 @@ import {emitPostCreated} from '#/state/events'
import {ThreadgateSetting} from '#/state/queries/threadgate' import {ThreadgateSetting} from '#/state/queries/threadgate'
import {logger} from '#/logger' import {logger} from '#/logger'
import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo' import {ComposerReplyTo} from 'view/com/composer/ComposerReplyTo'
import * as Prompt from '#/components/Prompt'
import {useDialogStateControlContext} from 'state/dialogs'
type Props = ComposerOpts type Props = ComposerOpts
export const ComposePost = observer(function ComposePost({ export const ComposePost = observer(function ComposePost({
@ -76,8 +78,7 @@ export const ComposePost = observer(function ComposePost({
}: Props) { }: Props) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {data: currentProfile} = useProfileQuery({did: currentAccount!.did}) const {data: currentProfile} = useProfileQuery({did: currentAccount!.did})
const {isModalActive, activeModals} = useModals() const {isModalActive} = useModals()
const {openModal, closeModal} = useModalControls()
const {closeComposer} = useComposerControls() const {closeComposer} = useComposerControls()
const {track} = useAnalytics() const {track} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
@ -87,6 +88,9 @@ export const ComposePost = observer(function ComposePost({
const langPrefs = useLanguagePrefs() const langPrefs = useLanguagePrefs()
const setLangPrefs = useLanguagePrefsApi() const setLangPrefs = useLanguagePrefsApi()
const textInput = useRef<TextInputRef>(null) const textInput = useRef<TextInputRef>(null)
const discardPromptControl = Prompt.usePromptControl()
const {closeAllDialogs} = useDialogStateControlContext()
const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true}) const [isKeyboardVisible] = useIsKeyboardVisible({iosUseWillEvents: true})
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [processingState, setProcessingState] = useState('') const [processingState, setProcessingState] = useState('')
@ -134,27 +138,21 @@ export const ComposePost = observer(function ComposePost({
const onPressCancel = useCallback(() => { const onPressCancel = useCallback(() => {
if (graphemeLength > 0 || !gallery.isEmpty) { if (graphemeLength > 0 || !gallery.isEmpty) {
if (activeModals.some(modal => modal.name === 'confirm')) { closeAllDialogs()
closeModal()
}
if (Keyboard) { if (Keyboard) {
Keyboard.dismiss() Keyboard.dismiss()
} }
openModal({ discardPromptControl.open()
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},
})
} else { } else {
onClose() onClose()
} }
}, [openModal, closeModal, activeModals, onClose, graphemeLength, gallery, _]) }, [
graphemeLength,
gallery.isEmpty,
closeAllDialogs,
discardPromptControl,
onClose,
])
// android back button // android back button
useEffect(() => { useEffect(() => {
if (!isAndroid) { if (!isAndroid) {
@ -488,6 +486,15 @@ export const ComposePost = observer(function ComposePost({
<CharProgress count={graphemeLength} /> <CharProgress count={graphemeLength} />
</View> </View>
</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> </KeyboardAvoidingView>
) )
}) })

View File

@ -11,7 +11,6 @@ import {AtUri} from '@atproto/api'
import * as Toast from 'view/com/util/Toast' import * as Toast from 'view/com/util/Toast'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import { import {
@ -24,6 +23,7 @@ import {
import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed' import {useFeedSourceInfoQuery, FeedSourceInfo} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {useTheme} from '#/alf' import {useTheme} from '#/alf'
import * as Prompt from '#/components/Prompt'
import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped' import {useNavigationDeduped} from 'lib/hooks/useNavigationDeduped'
export function FeedSourceCard({ export function FeedSourceCard({
@ -85,8 +85,8 @@ export function FeedSourceCardLoaded({
const t = useTheme() const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const removePromptControl = Prompt.usePromptControl()
const navigation = useNavigationDeduped() const navigation = useNavigationDeduped()
const {openModal} = useModalControls()
const {isPending: isSavePending, mutateAsync: saveFeed} = const {isPending: isSavePending, mutateAsync: saveFeed} =
useSaveFeedMutation() useSaveFeedMutation()
@ -96,40 +96,45 @@ export function FeedSourceCardLoaded({
const isSaved = Boolean(preferences?.feeds?.saved?.includes(feed?.uri || '')) 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 () => { const onToggleSaved = React.useCallback(async () => {
// Only feeds can be un/saved, lists are handled elsewhere // Only feeds can be un/saved, lists are handled elsewhere
if (feed?.type !== 'feed') return if (feed?.type !== 'feed') return
if (isSaved) { if (isSaved) {
openModal({ removePromptControl.open()
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})
}
},
})
} else { } else {
try { await onSave()
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})
}
} }
}, [isSaved, openModal, feed, removeFeed, saveFeed, _, pinOnSave, pinFeed]) }, [feed?.type, isSaved, removePromptControl, onSave])
/* /*
* LOAD STATE * LOAD STATE
@ -167,25 +172,7 @@ export function FeedSourceCardLoaded({
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Remove from my feeds`)} accessibilityLabel={_(msg`Remove from my feeds`)}
accessibilityHint="" accessibilityHint=""
onPress={() => { onPress={onToggleSaved}
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})
}
},
})
}}
hitSlop={15} hitSlop={15}
style={styles.btn}> style={styles.btn}>
<FontAwesomeIcon <FontAwesomeIcon
@ -199,89 +186,104 @@ export function FeedSourceCardLoaded({
) )
return ( return (
<Pressable <>
testID={`feed-${feed.displayName}`} <Pressable
accessibilityRole="button" testID={`feed-${feed.displayName}`}
style={[styles.container, pal.border, style]} accessibilityRole="button"
onPress={() => { style={[styles.container, pal.border, style]}
if (feed.type === 'feed') { onPress={() => {
navigation.push('ProfileFeed', { if (feed.type === 'feed') {
name: feed.creatorDid, navigation.push('ProfileFeed', {
rkey: new AtUri(feed.uri).rkey, name: feed.creatorDid,
}) rkey: new AtUri(feed.uri).rkey,
} else if (feed.type === 'list') { })
navigation.push('ProfileList', { } else if (feed.type === 'list') {
name: feed.creatorDid, navigation.push('ProfileList', {
rkey: new AtUri(feed.uri).rkey, name: feed.creatorDid,
}) rkey: new AtUri(feed.uri).rkey,
} })
}} }
key={feed.uri}> }}
<View style={[styles.headerContainer]}> key={feed.uri}>
<View style={[s.mr10]}> <View style={[styles.headerContainer]}>
<UserAvatar type="algo" size={36} avatar={feed.avatar} /> <View style={[s.mr10]}>
</View> <UserAvatar type="algo" size={36} avatar={feed.avatar} />
<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}
/>
) : (
<FontAwesomeIcon
icon="plus"
size={18}
color={pal.colors.link}
/>
)}
</Pressable>
</View> </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}
/>
) : (
<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>
<Prompt.Basic
control={removePromptControl}
title={_(msg`Remove from my feeds?`)}
description={_(
msg`Are you sure you want to remove ${feed.displayName} from your feeds?`,
)} )}
</View> onConfirm={onUnsave}
confirmButtonCta={_(msg`Remove`)}
{showDescription && feed.description ? ( confirmButtonColor="negative"
<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>
) )
} }

View File

@ -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,
},
})

View File

@ -6,7 +6,6 @@ import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useModals, useModalControls} from '#/state/modals' import {useModals, useModalControls} from '#/state/modals'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as RepostModal from './Repost' import * as RepostModal from './Repost'
import * as SelfLabelModal from './SelfLabel' import * as SelfLabelModal from './SelfLabel'
@ -66,10 +65,7 @@ export function ModalsContainer() {
let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS let snapPoints: (string | number)[] = DEFAULT_SNAPPOINTS
let element let element
if (activeModal?.name === 'confirm') { if (activeModal?.name === 'edit-profile') {
snapPoints = ConfirmModal.snapPoints
element = <ConfirmModal.Component {...activeModal} />
} else if (activeModal?.name === 'edit-profile') {
snapPoints = EditProfileModal.snapPoints snapPoints = EditProfileModal.snapPoints
element = <EditProfileModal.Component {...activeModal} /> element = <EditProfileModal.Component {...activeModal} />
} else if (activeModal?.name === 'report') { } else if (activeModal?.name === 'report') {

View File

@ -7,7 +7,6 @@ import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {useModals, useModalControls} from '#/state/modals' import {useModals, useModalControls} from '#/state/modals'
import type {Modal as ModalIface} from '#/state/modals' import type {Modal as ModalIface} from '#/state/modals'
import * as ConfirmModal from './Confirm'
import * as EditProfileModal from './EditProfile' import * as EditProfileModal from './EditProfile'
import * as ReportModal from './report/Modal' import * as ReportModal from './report/Modal'
import * as AppealLabelModal from './AppealLabel' import * as AppealLabelModal from './AppealLabel'
@ -78,9 +77,7 @@ function Modal({modal}: {modal: ModalIface}) {
} }
let element let element
if (modal.name === 'confirm') { if (modal.name === 'edit-profile') {
element = <ConfirmModal.Component {...modal} />
} else if (modal.name === 'edit-profile') {
element = <EditProfileModal.Component {...modal} /> element = <EditProfileModal.Component {...modal} />
} else if (modal.name === 'report') { } else if (modal.name === 'report') {
element = <ReportModal.Component {...modal} /> element = <ReportModal.Component {...modal} />

View File

@ -9,13 +9,13 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {msg as msgLingui, Trans} from '@lingui/macro' import {msg as msgLingui, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {FeedDescriptor} from '#/state/queries/post-feed' import {FeedDescriptor} from '#/state/queries/post-feed'
import {EmptyState} from '../util/EmptyState' import {EmptyState} from '../util/EmptyState'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useRemoveFeedMutation} from '#/state/queries/preferences' import {useRemoveFeedMutation} from '#/state/queries/preferences'
import * as Prompt from '#/components/Prompt'
export enum KnownError { export enum KnownError {
Block = 'Block', Block = 'Block',
@ -118,35 +118,29 @@ function FeedgenErrorMessage({
) )
const [_, uri] = feedDesc.split('|') const [_, uri] = feedDesc.split('|')
const [ownerDid] = safeParseFeedgenUri(uri) const [ownerDid] = safeParseFeedgenUri(uri)
const {openModal, closeModal} = useModalControls() const removePromptControl = Prompt.usePromptControl()
const {mutateAsync: removeFeed} = useRemoveFeedMutation() const {mutateAsync: removeFeed} = useRemoveFeedMutation()
const onViewProfile = React.useCallback(() => { const onViewProfile = React.useCallback(() => {
navigation.navigate('Profile', {name: ownerDid}) navigation.navigate('Profile', {name: ownerDid})
}, [navigation, ownerDid]) }, [navigation, ownerDid])
const onPressRemoveFeed = React.useCallback(() => {
removePromptControl.open()
}, [removePromptControl])
const onRemoveFeed = React.useCallback(async () => { const onRemoveFeed = React.useCallback(async () => {
openModal({ try {
name: 'confirm', await removeFeed({uri})
title: _l(msgLingui`Remove feed`), } catch (err) {
message: _l(msgLingui`Remove this feed from your saved feeds?`), Toast.show(
async onPressConfirm() { _l(
try { msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`,
await removeFeed({uri}) ),
} catch (err) { )
Toast.show( logger.error('Failed to remove feed', {message: err})
_l( }
msgLingui`There was an an issue removing this feed. Please check your internet connection and try again.`, }, [uri, removeFeed, _l])
),
)
logger.error('Failed to remove feed', {message: err})
}
},
onPressCancel() {
closeModal()
},
})
}, [openModal, closeModal, uri, removeFeed, _l])
const cta = React.useMemo(() => { const cta = React.useMemo(() => {
switch (knownError) { switch (knownError) {
@ -179,27 +173,38 @@ function FeedgenErrorMessage({
}, [knownError, onViewProfile, onRemoveFeed, _l]) }, [knownError, onViewProfile, onRemoveFeed, _l])
return ( return (
<View <>
style={[ <View
pal.border, style={[
pal.viewLight, pal.border,
{ pal.viewLight,
borderTopWidth: 1, {
paddingHorizontal: 20, borderTopWidth: 1,
paddingVertical: 18, paddingHorizontal: 20,
gap: 12, paddingVertical: 18,
}, gap: 12,
]}> },
<Text style={pal.text}>{msg}</Text> ]}>
<Text style={pal.text}>{msg}</Text>
{rawError?.message && ( {rawError?.message && (
<Text style={pal.textLight}> <Text style={pal.textLight}>
<Trans>Message from server: {rawError.message}</Trans> <Trans>Message from server: {rawError.message}</Trans>
</Text> </Text>
)} )}
{cta} {cta}
</View> </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"
/>
</>
) )
} }

View File

@ -52,6 +52,7 @@ import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow' import {useProfileShadow} from 'state/cache/profile-shadow'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ProfileMenu} from 'view/com/profile/ProfileMenu' import {ProfileMenu} from 'view/com/profile/ProfileMenu'
import * as Prompt from '#/components/Prompt'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => { let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const pal = usePalette('default') const pal = usePalette('default')
@ -104,6 +105,7 @@ let ProfileHeader = ({
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false) const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile) const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
const [__, queueUnblock] = useProfileBlockMutationQueue(profile) const [__, queueUnblock] = useProfileBlockMutationQueue(profile)
const unblockPromptControl = Prompt.usePromptControl()
const moderation = useMemo( const moderation = useMemo(
() => moderateProfile(profile, moderationOpts), () => moderateProfile(profile, moderationOpts),
[profile, moderationOpts], [profile, moderationOpts],
@ -176,27 +178,18 @@ let ProfileHeader = ({
}) })
}, [track, openModal, profile]) }, [track, openModal, profile])
const onPressUnblockAccount = React.useCallback(() => { const unblockAccount = React.useCallback(async () => {
track('ProfileHeader:UnblockAccountButtonClicked') track('ProfileHeader:UnblockAccountButtonClicked')
openModal({ try {
name: 'confirm', await queueUnblock()
title: _(msg`Unblock Account`), Toast.show(_(msg`Account unblocked`))
message: _( } catch (e: any) {
msg`The account will be able to interact with you after unblocking.`, if (e?.name !== 'AbortError') {
), logger.error('Failed to unblock account', {message: e})
onPressConfirm: async () => { Toast.show(_(msg`There was an issue! ${e.toString()}`))
try { }
await queueUnblock() }
Toast.show(_(msg`Account unblocked`)) }, [_, queueUnblock, track])
} 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])
const isMe = React.useMemo( const isMe = React.useMemo(
() => currentAccount?.did === profile.did, () => currentAccount?.did === profile.did,
@ -242,7 +235,7 @@ let ProfileHeader = ({
profile.viewer?.blockingByList ? null : ( profile.viewer?.blockingByList ? null : (
<TouchableOpacity <TouchableOpacity
testID="unblockBtn" testID="unblockBtn"
onPress={onPressUnblockAccount} onPress={() => unblockPromptControl.open()}
style={[styles.btn, styles.mainBtn, pal.btn]} style={[styles.btn, styles.mainBtn, pal.btn]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Unblock`)} accessibilityLabel={_(msg`Unblock`)}
@ -475,6 +468,18 @@ let ProfileHeader = ({
/> />
</View> </View>
</TouchableWithoutFeedback> </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> </View>
) )
} }

View File

@ -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 {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Shadow} from 'state/cache/types' import {Shadow} from 'state/cache/types'
import * as Prompt from '#/components/Prompt'
let ProfileMenu = ({ let ProfileMenu = ({
profile, profile,
@ -53,6 +54,8 @@ let ProfileMenu = ({
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
const [, queueUnfollow] = useProfileFollowMutationQueue(profile) const [, queueUnfollow] = useProfileFollowMutationQueue(profile)
const blockPromptControl = Prompt.usePromptControl()
const invalidateProfileQuery = React.useCallback(() => { const invalidateProfileQuery = React.useCallback(() => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: profileQueryKey(profile.did), queryKey: profileQueryKey(profile.did),
@ -102,49 +105,31 @@ let ProfileMenu = ({
} }
}, [profile.viewer?.muted, track, queueUnmute, _, queueMute]) }, [profile.viewer?.muted, track, queueUnmute, _, queueMute])
const onPressBlockAccount = React.useCallback(async () => { const blockAccount = React.useCallback(async () => {
if (profile.viewer?.blocking) { if (profile.viewer?.blocking) {
track('ProfileHeader:UnblockAccountButtonClicked') track('ProfileHeader:UnblockAccountButtonClicked')
openModal({ try {
name: 'confirm', await queueUnblock()
title: _(msg`Unblock Account`), Toast.show(_(msg`Account unblocked`))
message: _( } catch (e: any) {
msg`The account will be able to interact with you after unblocking.`, if (e?.name !== 'AbortError') {
), logger.error('Failed to unblock account', {message: e})
onPressConfirm: async () => { 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 { } else {
track('ProfileHeader:BlockAccountButtonClicked') track('ProfileHeader:BlockAccountButtonClicked')
openModal({ try {
name: 'confirm', await queueBlock()
title: _(msg`Block Account`), Toast.show(_(msg`Account blocked`))
message: _( } catch (e: any) {
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, if (e?.name !== 'AbortError') {
), logger.error('Failed to block account', {message: e})
onPressConfirm: async () => { 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 () => { const onPressUnfollowAccount = React.useCallback(async () => {
track('ProfileHeader:UnfollowButtonClicked') track('ProfileHeader:UnfollowButtonClicked')
@ -268,7 +253,7 @@ let ProfileMenu = ({
? _(msg`Unblock Account`) ? _(msg`Unblock Account`)
: _(msg`Block Account`) : _(msg`Block Account`)
} }
onPress={onPressBlockAccount}> onPress={() => blockPromptControl.open()}>
<Menu.ItemText> <Menu.ItemText>
{profile.viewer?.blocking ? ( {profile.viewer?.blocking ? (
<Trans>Unblock Account</Trans> <Trans>Unblock Account</Trans>
@ -299,6 +284,29 @@ let ProfileMenu = ({
)} )}
</Menu.Outer> </Menu.Outer>
</Menu.Root> </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> </EventStopper>
) )
} }

View File

@ -14,6 +14,8 @@ import {useTheme} from 'lib/ThemeContext'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import * as Toast from '../Toast' import * as Toast from '../Toast'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {useDialogControl} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {CommonNavigatorParams} from '#/lib/routes/types' import {CommonNavigatorParams} from '#/lib/routes/types'
@ -81,6 +83,8 @@ let PostDropdownBtn = ({
const openLink = useOpenLink() const openLink = useOpenLink()
const navigation = useNavigation() const navigation = useNavigation()
const {mutedWordsDialogControl} = useGlobalDialogsControlContext() const {mutedWordsDialogControl} = useGlobalDialogsControlContext()
const deletePromptControl = useDialogControl()
const hidePromptControl = useDialogControl()
const rootUri = record.reply?.root?.uri || postUri const rootUri = record.reply?.root?.uri || postUri
const isThreadMuted = mutedThreads.includes(rootUri) const isThreadMuted = mutedThreads.includes(rootUri)
@ -257,16 +261,7 @@ let PostDropdownBtn = ({
<Menu.Item <Menu.Item
testID="postDropdownHideBtn" testID="postDropdownHideBtn"
label={_(msg`Hide post`)} label={_(msg`Hide post`)}
onPress={() => { onPress={hidePromptControl.open}>
openModal({
name: 'confirm',
title: _(msg`Hide this post?`),
message: _(
msg`This will hide this post from your feeds.`,
),
onPressConfirm: onHidePost,
})
}}>
<Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText> <Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText>
<Menu.ItemIcon icon={EyeSlash} position="right" /> <Menu.ItemIcon icon={EyeSlash} position="right" />
</Menu.Item> </Menu.Item>
@ -298,14 +293,7 @@ let PostDropdownBtn = ({
<Menu.Item <Menu.Item
testID="postDropdownDeleteBtn" testID="postDropdownDeleteBtn"
label={_(msg`Delete post`)} label={_(msg`Delete post`)}
onPress={() => { onPress={deletePromptControl.open}>
openModal({
name: 'confirm',
title: _(msg`Delete this post?`),
message: _(msg`Are you sure? This cannot be undone.`),
onPressConfirm: onDeletePost,
})
}}>
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
<Menu.ItemIcon icon={Trash} position="right" /> <Menu.ItemIcon icon={Trash} position="right" />
</Menu.Item> </Menu.Item>
@ -335,6 +323,25 @@ let PostDropdownBtn = ({
</Menu.Group> </Menu.Group>
</Menu.Outer> </Menu.Outer>
</Menu.Root> </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> </EventStopper>
) )
} }

View File

@ -29,6 +29,8 @@ import {
} from '#/state/queries/app-passwords' } from '#/state/queries/app-passwords'
import {ErrorScreen} from '../com/util/error/ErrorScreen' import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import * as Prompt from '#/components/Prompt'
import {useDialogControl} from '#/components/Dialog'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'> type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppPasswords'>
export function AppPasswords({}: Props) { export function AppPasswords({}: Props) {
@ -212,23 +214,18 @@ function AppPassword({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls() const control = useDialogControl()
const {contentLanguages} = useLanguagePrefs() const {contentLanguages} = useLanguagePrefs()
const deleteMutation = useAppPasswordDeleteMutation() const deleteMutation = useAppPasswordDeleteMutation()
const onDelete = React.useCallback(async () => { const onDelete = React.useCallback(async () => {
openModal({ await deleteMutation.mutateAsync({name})
name: 'confirm', Toast.show(_(msg`App password deleted`))
title: _(msg`Delete app password`), }, [deleteMutation, name, _])
message: _(
msg`Are you sure you want to delete the app password "${name}"?`, const onPress = React.useCallback(() => {
), control.open()
async onPressConfirm() { }, [control])
await deleteMutation.mutateAsync({name})
Toast.show(_(msg`App password deleted`))
},
})
}, [deleteMutation, openModal, name, _])
const primaryLocale = const primaryLocale =
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
@ -237,7 +234,7 @@ function AppPassword({
<TouchableOpacity <TouchableOpacity
testID={testID} testID={testID}
style={[styles.item, pal.border]} style={[styles.item, pal.border]}
onPress={onDelete} onPress={onPress}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={_(msg`Delete app password`)} accessibilityLabel={_(msg`Delete app password`)}
accessibilityHint=""> accessibilityHint="">
@ -260,6 +257,17 @@ function AppPassword({
</Text> </Text>
</View> </View>
<FontAwesomeIcon icon={['far', 'trash-can']} style={styles.trashIcon} /> <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> </TouchableOpacity>
) )
} }

View File

@ -61,6 +61,8 @@ import {logger} from '#/logger'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a, useTheme} from '#/alf' 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_CURATE = ['Posts', 'About']
const SECTION_TITLES_MOD = ['About'] const SECTION_TITLES_MOD = ['About']
@ -234,7 +236,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
const {_} = useLingui() const {_} = useLingui()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {openModal, closeModal} = useModalControls() const {openModal} = useModalControls()
const listMuteMutation = useListMuteMutation() const listMuteMutation = useListMuteMutation()
const listBlockMutation = useListBlockMutation() const listBlockMutation = useListBlockMutation()
const listDeleteMutation = useListDeleteMutation() const listDeleteMutation = useListDeleteMutation()
@ -251,6 +253,10 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
const {track} = useAnalytics() const {track} = useAnalytics()
const deleteListPromptControl = useDialogControl()
const subscribeMutePromptControl = useDialogControl()
const subscribeBlockPromptControl = useDialogControl()
const isPinned = preferences?.feeds?.pinned?.includes(list.uri) const isPinned = preferences?.feeds?.pinned?.includes(list.uri)
const isSaved = preferences?.feeds?.saved?.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, _]) }, [list.uri, isPinned, pinFeed, unpinFeed, _])
const onSubscribeMute = useCallback(() => { const onSubscribeMute = useCallback(async () => {
openModal({ try {
name: 'confirm', await listMuteMutation.mutateAsync({uri: list.uri, mute: true})
title: _(msg`Mute these accounts?`), Toast.show(_(msg`List muted`))
message: _( track('Lists:Mute')
msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`, } catch {
), Toast.show(
confirmBtnText: _(msg`Mute this List`), _(
async onPressConfirm() { msg`There was an issue. Please check your internet connection and try again.`,
try { ),
await listMuteMutation.mutateAsync({uri: list.uri, mute: true}) )
Toast.show(_(msg`List muted`)) }
track('Lists:Mute') }, [list, listMuteMutation, track, _])
} catch {
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
}
},
onPressCancel() {
closeModal()
},
})
}, [openModal, closeModal, list, listMuteMutation, track, _])
const onUnsubscribeMute = useCallback(async () => { const onUnsubscribeMute = useCallback(async () => {
try { try {
@ -310,32 +303,19 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
} }
}, [list, listMuteMutation, track, _]) }, [list, listMuteMutation, track, _])
const onSubscribeBlock = useCallback(() => { const onSubscribeBlock = useCallback(async () => {
openModal({ try {
name: 'confirm', await listBlockMutation.mutateAsync({uri: list.uri, block: true})
title: _(msg`Block these accounts?`), Toast.show(_(msg`List blocked`))
message: _( track('Lists:Block')
msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`, } catch {
), Toast.show(
confirmBtnText: _(msg`Block this List`), _(
async onPressConfirm() { msg`There was an issue. Please check your internet connection and try again.`,
try { ),
await listBlockMutation.mutateAsync({uri: list.uri, block: true}) )
Toast.show(_(msg`List blocked`)) }
track('Lists:Block') }, [list, listBlockMutation, track, _])
} catch {
Toast.show(
_(
msg`There was an issue. Please check your internet connection and try again.`,
),
)
}
},
onPressCancel() {
closeModal()
},
})
}, [openModal, closeModal, list, listBlockMutation, track, _])
const onUnsubscribeBlock = useCallback(async () => { const onUnsubscribeBlock = useCallback(async () => {
try { try {
@ -358,34 +338,26 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
}) })
}, [openModal, list]) }, [openModal, list])
const onPressDelete = useCallback(() => { const onPressDelete = useCallback(async () => {
openModal({ await listDeleteMutation.mutateAsync({uri: list.uri})
name: 'confirm',
title: _(msg`Delete List`),
message: _(msg`Are you sure?`),
async onPressConfirm() {
await listDeleteMutation.mutateAsync({uri: list.uri})
if (isSaved || isPinned) { if (isSaved || isPinned) {
const {saved, pinned} = preferences!.feeds const {saved, pinned} = preferences!.feeds
setSavedFeeds({ setSavedFeeds({
saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved, saved: isSaved ? saved.filter(uri => uri !== list.uri) : saved,
pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned, pinned: isPinned ? pinned.filter(uri => uri !== list.uri) : pinned,
}) })
} }
Toast.show(_(msg`List deleted`)) Toast.show(_(msg`List deleted`))
track('Lists:Delete') track('Lists:Delete')
if (navigation.canGoBack()) { if (navigation.canGoBack()) {
navigation.goBack() navigation.goBack()
} else { } else {
navigation.navigate('Home') navigation.navigate('Home')
} }
},
})
}, [ }, [
openModal,
list, list,
listDeleteMutation, listDeleteMutation,
navigation, navigation,
@ -443,7 +415,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
items.push({ items.push({
testID: 'listHeaderDropdownDeleteBtn', testID: 'listHeaderDropdownDeleteBtn',
label: _(msg`Delete List`), label: _(msg`Delete List`),
onPress: onPressDelete, onPress: deleteListPromptControl.open,
icon: { icon: {
ios: { ios: {
name: 'trash', name: 'trash',
@ -489,7 +461,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
items.push({ items.push({
testID: 'listHeaderDropdownMuteBtn', testID: 'listHeaderDropdownMuteBtn',
label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`), label: isMuting ? _(msg`Un-mute list`) : _(msg`Mute list`),
onPress: isMuting ? onUnsubscribeMute : onSubscribeMute, onPress: isMuting
? onUnsubscribeMute
: subscribeMutePromptControl.open,
icon: { icon: {
ios: { ios: {
name: isMuting ? 'eye' : 'eye.slash', name: isMuting ? 'eye' : 'eye.slash',
@ -504,7 +478,9 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
items.push({ items.push({
testID: 'listHeaderDropdownBlockBtn', testID: 'listHeaderDropdownBlockBtn',
label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`), label: isBlocking ? _(msg`Un-block list`) : _(msg`Block list`),
onPress: isBlocking ? onUnsubscribeBlock : onSubscribeBlock, onPress: isBlocking
? onUnsubscribeBlock
: subscribeBlockPromptControl.open,
icon: { icon: {
ios: { ios: {
name: 'person.fill.xmark', name: 'person.fill.xmark',
@ -517,24 +493,24 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
} }
return items return items
}, [ }, [
isOwner,
onPressShare,
onPressEdit,
onPressDelete,
onPressReport,
_, _,
onPressShare,
isOwner,
isModList, isModList,
isPinned, isPinned,
unpinFeed,
isPending,
list.uri,
isCurateList, isCurateList,
isMuting, onPressEdit,
deleteListPromptControl.open,
onPressReport,
isPending,
unpinFeed,
list.uri,
isBlocking, isBlocking,
isMuting,
onUnsubscribeMute, onUnsubscribeMute,
onSubscribeMute, subscribeMutePromptControl.open,
onUnsubscribeBlock, onUnsubscribeBlock,
onSubscribeBlock, subscribeBlockPromptControl.open,
]) ])
const subscribeDropdownItems: DropdownItem[] = useMemo(() => { const subscribeDropdownItems: DropdownItem[] = useMemo(() => {
@ -542,7 +518,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
{ {
testID: 'subscribeDropdownMuteBtn', testID: 'subscribeDropdownMuteBtn',
label: _(msg`Mute accounts`), label: _(msg`Mute accounts`),
onPress: onSubscribeMute, onPress: subscribeMutePromptControl.open,
icon: { icon: {
ios: { ios: {
name: 'speaker.slash', name: 'speaker.slash',
@ -554,7 +530,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
{ {
testID: 'subscribeDropdownBlockBtn', testID: 'subscribeDropdownBlockBtn',
label: _(msg`Block accounts`), label: _(msg`Block accounts`),
onPress: onSubscribeBlock, onPress: subscribeBlockPromptControl.open,
icon: { icon: {
ios: { ios: {
name: 'person.fill.xmark', name: 'person.fill.xmark',
@ -564,7 +540,7 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
}, },
}, },
] ]
}, [onSubscribeMute, onSubscribeBlock, _]) }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open])
return ( return (
<ProfileSubpageHeader <ProfileSubpageHeader
@ -620,6 +596,38 @@ function Header({rkey, list}: {rkey: string; list: AppBskyGraphDefs.ListView}) {
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} /> <FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} />
</View> </View>
</NativeDropdown> </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> </ProfileSubpageHeader>
) )
} }

View File

@ -68,7 +68,7 @@ export function Dialogs() {
</Prompt.Description> </Prompt.Description>
<Prompt.Actions> <Prompt.Actions>
<Prompt.Cancel>Cancel</Prompt.Cancel> <Prompt.Cancel>Cancel</Prompt.Cancel>
<Prompt.Action>Confirm</Prompt.Action> <Prompt.Action onPress={() => {}}>Confirm</Prompt.Action>
</Prompt.Actions> </Prompt.Actions>
</Prompt.Outer> </Prompt.Outer>