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
parent
090b35e52e
commit
9f2f7f221c
|
@ -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',
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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={[
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue