diff options
Diffstat (limited to 'src/view/com/util/forms')
-rw-r--r-- | src/view/com/util/forms/DateInput.tsx | 32 | ||||
-rw-r--r-- | src/view/com/util/forms/NativeDropdown.tsx | 3 | ||||
-rw-r--r-- | src/view/com/util/forms/NativeDropdown.web.tsx | 12 | ||||
-rw-r--r-- | src/view/com/util/forms/PostDropdownBtn.tsx | 421 | ||||
-rw-r--r-- | src/view/com/util/forms/SelectableBtn.tsx | 1 |
5 files changed, 277 insertions, 192 deletions
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx index c5f0afc8f..0104562aa 100644 --- a/src/view/com/util/forms/DateInput.tsx +++ b/src/view/com/util/forms/DateInput.tsx @@ -1,8 +1,5 @@ import React, {useState, useCallback} from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' -import DateTimePicker, { - DateTimePickerEvent, -} from '@react-native-community/datetimepicker' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' import {getLocales} from 'expo-localization' +import DatePicker from 'react-native-date-picker' const LOCALE = getLocales()[0] @@ -43,11 +41,9 @@ export function DateInput(props: Props) { }, [props.handleAsUTC]) const onChangeInternal = useCallback( - (event: DateTimePickerEvent, date: Date | undefined) => { + (date: Date) => { setShow(false) - if (date) { - props.onChange(date) - } + props.onChange(date) }, [setShow, props], ) @@ -56,6 +52,10 @@ export function DateInput(props: Props) { setShow(true) }, [setShow]) + const onCancel = useCallback(() => { + setShow(false) + }, []) + return ( <View> {isAndroid && ( @@ -80,15 +80,17 @@ export function DateInput(props: Props) { </Button> )} {(isIOS || show) && ( - <DateTimePicker - testID={props.testID ? `${props.testID}-datepicker` : undefined} + <DatePicker + timeZoneOffsetInMinutes={0} + modal={isAndroid} + open={isAndroid} + theme={theme.colorScheme} + date={props.value} + onDateChange={onChangeInternal} + onConfirm={onChangeInternal} + onCancel={onCancel} mode="date" - timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined} - display="spinner" - // @ts-ignore applies in iOS only -prf - themeVariant={theme.colorScheme} - value={props.value} - onChange={onChangeInternal} + testID={props.testID ? `${props.testID}-datepicker` : undefined} accessibilityLabel={props.accessibilityLabel} accessibilityHint={props.accessibilityHint} accessibilityLabelledBy={props.accessibilityLabelledBy} diff --git a/src/view/com/util/forms/NativeDropdown.tsx b/src/view/com/util/forms/NativeDropdown.tsx index 082285064..0a47569f2 100644 --- a/src/view/com/util/forms/NativeDropdown.tsx +++ b/src/view/com/util/forms/NativeDropdown.tsx @@ -1,7 +1,7 @@ import React from 'react' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as DropdownMenu from 'zeego/dropdown-menu' -import {Pressable, StyleSheet, Platform, View} from 'react-native' +import {Pressable, StyleSheet, Platform, View, ViewStyle} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' import {usePalette} from 'lib/hooks/usePalette' @@ -151,6 +151,7 @@ type Props = { testID?: string accessibilityLabel?: string accessibilityHint?: string + triggerStyle?: ViewStyle } /* The `NativeDropdown` function uses native iOS and Android dropdown menus. diff --git a/src/view/com/util/forms/NativeDropdown.web.tsx b/src/view/com/util/forms/NativeDropdown.web.tsx index 9e9888ad8..94591d393 100644 --- a/src/view/com/util/forms/NativeDropdown.web.tsx +++ b/src/view/com/util/forms/NativeDropdown.web.tsx @@ -1,7 +1,7 @@ import React from 'react' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import * as DropdownMenu from '@radix-ui/react-dropdown-menu' -import {Pressable, StyleSheet, View, Text} from 'react-native' +import {Pressable, StyleSheet, View, Text, ViewStyle} from 'react-native' import {IconProp} from '@fortawesome/fontawesome-svg-core' import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' import {usePalette} from 'lib/hooks/usePalette' @@ -21,6 +21,7 @@ export const DropdownMenuItem = (props: ItemProps & {testID?: string}) => { return ( <DropdownMenu.Item + className="nativeDropdown-item" {...props} style={StyleSheet.flatten([ styles.item, @@ -52,6 +53,7 @@ type Props = { testID?: string accessibilityLabel?: string accessibilityHint?: string + triggerStyle?: ViewStyle } export function NativeDropdown({ @@ -60,6 +62,7 @@ export function NativeDropdown({ testID, accessibilityLabel, accessibilityHint, + triggerStyle, }: React.PropsWithChildren<Props>) { const pal = usePalette('default') const theme = useTheme() @@ -119,7 +122,8 @@ export function NativeDropdown({ accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} onPress={() => setOpen(o => !o)} - hitSlop={HITSLOP_10}> + hitSlop={HITSLOP_10} + style={triggerStyle}> {children} </Pressable> </DropdownMenu.Trigger> @@ -232,6 +236,10 @@ const styles = StyleSheet.create({ paddingLeft: 12, paddingRight: 12, borderRadius: 8, + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif', + outline: 0, + border: 0, }, itemTitle: { fontSize: 16, diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index e56c88d2c..d04672c63 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,36 +1,50 @@ import React, {memo} from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' -import Clipboard from '@react-native-clipboard/clipboard' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {Pressable, PressableProps, StyleProp, ViewStyle} from 'react-native' import { AppBskyActorDefs, AppBskyFeedPost, AtUri, RichText as RichTextAPI, } from '@atproto/api' -import {toShareUrl} from 'lib/strings/url-helpers' -import {useTheme} from 'lib/ThemeContext' -import {shareUrl} from 'lib/sharing' -import { - NativeDropdown, - DropdownItem as NativeDropdownItem, -} from './NativeDropdown' -import * as Toast from '../Toast' -import {EventStopper} from '../EventStopper' -import {useModalControls} from '#/state/modals' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import Clipboard from '@react-native-clipboard/clipboard' +import {useNavigation} from '@react-navigation/native' + import {makeProfileLink} from '#/lib/routes/links' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {richTextToString} from '#/lib/strings/rich-text-helpers' import {getTranslatorLink} from '#/locale/helpers' -import {usePostDeleteMutation} from '#/state/queries/post' +import {logger} from '#/logger' +import {isWeb} from '#/platform/detection' import {useMutedThreads, useToggleThreadMute} from '#/state/muted-threads' import {useLanguagePrefs} from '#/state/preferences' import {useHiddenPosts, useHiddenPostsApi} from '#/state/preferences' import {useOpenLink} from '#/state/preferences/in-app-browser' -import {logger} from '#/logger' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {usePostDeleteMutation} from '#/state/queries/post' import {useSession} from '#/state/session' -import {isWeb} from '#/platform/detection' -import {richTextToString} from '#/lib/strings/rich-text-helpers' +import {getCurrentRoute} from 'lib/routes/helpers' +import {shareUrl} from 'lib/sharing' +import {toShareUrl} from 'lib/strings/url-helpers' +import {useTheme} from 'lib/ThemeContext' +import {atoms as a, useTheme as useAlf} from '#/alf' +import {useDialogControl} from '#/components/Dialog' +import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' +import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' +import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' +import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' +import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import * as Menu from '#/components/Menu' +import * as Prompt from '#/components/Prompt' +import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' +import {EventStopper} from '../EventStopper' +import * as Toast from '../Toast' let PostDropdownBtn = ({ testID, @@ -40,7 +54,7 @@ let PostDropdownBtn = ({ record, richText, style, - showAppealLabelItem, + hitSlop, }: { testID: string postAuthor: AppBskyActorDefs.ProfileViewBasic @@ -49,13 +63,13 @@ let PostDropdownBtn = ({ record: AppBskyFeedPost.Record richText: RichTextAPI style?: StyleProp<ViewStyle> - showAppealLabelItem?: boolean + hitSlop?: PressableProps['hitSlop'] }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() + const alf = useAlf() const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl - const {openModal} = useModalControls() const langPrefs = useLanguagePrefs() const mutedThreads = useMutedThreads() const toggleThreadMute = useToggleThreadMute() @@ -63,11 +77,18 @@ let PostDropdownBtn = ({ const hiddenPosts = useHiddenPosts() const {hidePost} = useHiddenPostsApi() const openLink = useOpenLink() + const navigation = useNavigation() + const {mutedWordsDialogControl} = useGlobalDialogsControlContext() + const reportDialogControl = useReportDialogControl() + const deletePromptControl = useDialogControl() + const hidePromptControl = useDialogControl() + const loggedOutWarningPromptControl = useDialogControl() const rootUri = record.reply?.root?.uri || postUri const isThreadMuted = mutedThreads.includes(rootUri) const isPostHidden = hiddenPosts && hiddenPosts.includes(postUri) const isAuthor = postAuthor.did === currentAccount?.did + const href = React.useMemo(() => { const urip = new AtUri(postUri) return makeProfileLink(postAuthor, 'post', urip.rkey) @@ -82,13 +103,38 @@ let PostDropdownBtn = ({ postDeleteMutation.mutateAsync({uri: postUri}).then( () => { Toast.show(_(msg`Post deleted`)) + + const route = getCurrentRoute(navigation.getState()) + if (route.name === 'PostThread') { + const params = route.params as CommonNavigatorParams['PostThread'] + if ( + currentAccount && + isAuthor && + (params.name === currentAccount.handle || + params.name === currentAccount.did) + ) { + const currentHref = makeProfileLink(postAuthor, 'post', params.rkey) + if (currentHref === href && navigation.canGoBack()) { + navigation.goBack() + } + } + } }, e => { logger.error('Failed to delete post', {message: e}) Toast.show(_(msg`Failed to delete post, please try again`)) }, ) - }, [postUri, postDeleteMutation, _]) + }, [ + navigation, + postUri, + postDeleteMutation, + postAuthor, + currentAccount, + isAuthor, + href, + _, + ]) const onToggleThreadMute = React.useCallback(() => { try { @@ -120,159 +166,186 @@ let PostDropdownBtn = ({ hidePost({uri: postUri}) }, [postUri, hidePost]) - const dropdownItems: NativeDropdownItem[] = [ - { - label: _(msg`Translate`), - onPress() { - onOpenTranslate() - }, - testID: 'postDropdownTranslateBtn', - icon: { - ios: { - name: 'character.book.closed', - }, - android: 'ic_menu_sort_alphabetically', - web: 'language', - }, - }, - { - label: _(msg`Copy post text`), - onPress() { - onCopyPostText() - }, - testID: 'postDropdownCopyTextBtn', - icon: { - ios: { - name: 'doc.on.doc', - }, - android: 'ic_menu_edit', - web: ['far', 'paste'], - }, - }, - { - label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`), - onPress() { - const url = toShareUrl(href) - shareUrl(url) - }, - testID: 'postDropdownShareBtn', - icon: { - ios: { - name: 'square.and.arrow.up', - }, - android: 'ic_menu_share', - web: 'share', - }, - }, - hasSession && { - label: 'separator', - }, - hasSession && { - label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`), - onPress() { - onToggleThreadMute() - }, - testID: 'postDropdownMuteThreadBtn', - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_lock_silent_mode', - web: 'comment-slash', - }, - }, - hasSession && - !isAuthor && - !isPostHidden && { - label: _(msg`Hide post`), - onPress() { - openModal({ - name: 'confirm', - title: _(msg`Hide this post?`), - message: _(msg`This will hide this post from your feeds.`), - onPressConfirm: onHidePost, - }) - }, - testID: 'postDropdownHideBtn', - icon: { - ios: { - name: 'eye.slash', - }, - android: 'ic_menu_delete', - web: ['far', 'eye-slash'], - }, - }, - { - label: 'separator', - }, - !isAuthor && - hasSession && { - label: _(msg`Report post`), - onPress() { - openModal({ - name: 'report', - uri: postUri, - cid: postCid, - }) - }, - testID: 'postDropdownReportBtn', - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }, - isAuthor && { - label: _(msg`Delete post`), - onPress() { - openModal({ - name: 'confirm', - title: _(msg`Delete this post?`), - message: _(msg`Are you sure? This cannot be undone.`), - onPressConfirm: onDeletePost, - }) - }, - testID: 'postDropdownDeleteBtn', - icon: { - ios: { - name: 'trash', - }, - android: 'ic_menu_delete', - web: ['far', 'trash-can'], - }, - }, - showAppealLabelItem && { - label: 'separator', - }, - showAppealLabelItem && { - label: _(msg`Appeal content warning`), - onPress() { - openModal({name: 'appeal-label', uri: postUri, cid: postCid}) - }, - testID: 'postDropdownAppealBtn', - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }, - ].filter(Boolean) as NativeDropdownItem[] + const shouldShowLoggedOutWarning = React.useMemo(() => { + return !!postAuthor.labels?.find( + label => label.val === '!no-unauthenticated', + ) + }, [postAuthor]) + + const onSharePost = React.useCallback(() => { + const url = toShareUrl(href) + shareUrl(url) + }, [href]) return ( - <EventStopper> - <NativeDropdown - testID={testID} - items={dropdownItems} - accessibilityLabel={_(msg`More post options`)} - accessibilityHint=""> - <View style={style}> - <FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} /> - </View> - </NativeDropdown> + <EventStopper onKeyDown={false}> + <Menu.Root> + <Menu.Trigger label={_(msg`Open post options menu`)}> + {({props, state}) => { + return ( + <Pressable + {...props} + hitSlop={hitSlop} + testID={testID} + style={[ + style, + a.rounded_full, + (state.hovered || state.pressed) && [ + alf.atoms.bg_contrast_50, + ], + ]}> + <FontAwesomeIcon + icon="ellipsis" + size={20} + color={defaultCtrlColor} + style={{pointerEvents: 'none'}} + /> + </Pressable> + ) + }} + </Menu.Trigger> + + <Menu.Outer> + <Menu.Group> + <Menu.Item + testID="postDropdownTranslateBtn" + label={_(msg`Translate`)} + onPress={onOpenTranslate}> + <Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText> + <Menu.ItemIcon icon={Translate} position="right" /> + </Menu.Item> + + <Menu.Item + testID="postDropdownCopyTextBtn" + label={_(msg`Copy post text`)} + onPress={onCopyPostText}> + <Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText> + <Menu.ItemIcon icon={ClipboardIcon} position="right" /> + </Menu.Item> + + <Menu.Item + testID="postDropdownShareBtn" + label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} + onPress={() => { + if (shouldShowLoggedOutWarning) { + loggedOutWarningPromptControl.open() + } else { + onSharePost() + } + }}> + <Menu.ItemText> + {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} + </Menu.ItemText> + <Menu.ItemIcon icon={Share} position="right" /> + </Menu.Item> + </Menu.Group> + + {hasSession && ( + <> + <Menu.Divider /> + + <Menu.Group> + <Menu.Item + testID="postDropdownMuteThreadBtn" + label={ + isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`) + } + onPress={onToggleThreadMute}> + <Menu.ItemText> + {isThreadMuted + ? _(msg`Unmute thread`) + : _(msg`Mute thread`)} + </Menu.ItemText> + <Menu.ItemIcon + icon={isThreadMuted ? Unmute : Mute} + position="right" + /> + </Menu.Item> + + <Menu.Item + testID="postDropdownMuteWordsBtn" + label={_(msg`Mute words & tags`)} + onPress={() => mutedWordsDialogControl.open()}> + <Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText> + <Menu.ItemIcon icon={Filter} position="right" /> + </Menu.Item> + + {!isAuthor && !isPostHidden && ( + <Menu.Item + testID="postDropdownHideBtn" + label={_(msg`Hide post`)} + onPress={hidePromptControl.open}> + <Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText> + <Menu.ItemIcon icon={EyeSlash} position="right" /> + </Menu.Item> + )} + </Menu.Group> + </> + )} + + <Menu.Divider /> + + <Menu.Group> + {!isAuthor && ( + <Menu.Item + testID="postDropdownReportBtn" + label={_(msg`Report post`)} + onPress={() => reportDialogControl.open()}> + <Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText> + <Menu.ItemIcon icon={Warning} position="right" /> + </Menu.Item> + )} + + {isAuthor && ( + <Menu.Item + testID="postDropdownDeleteBtn" + label={_(msg`Delete post`)} + onPress={deletePromptControl.open}> + <Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText> + <Menu.ItemIcon icon={Trash} position="right" /> + </Menu.Item> + )} + </Menu.Group> + </Menu.Outer> + </Menu.Root> + + <Prompt.Basic + control={deletePromptControl} + title={_(msg`Delete this post?`)} + description={_( + msg`If you remove this post, you won't be able to recover it.`, + )} + onConfirm={onDeletePost} + confirmButtonCta={_(msg`Delete`)} + confirmButtonColor="negative" + /> + + <Prompt.Basic + control={hidePromptControl} + title={_(msg`Hide this post?`)} + description={_(msg`This post will be hidden from feeds.`)} + onConfirm={onHidePost} + confirmButtonCta={_(msg`Hide`)} + /> + + <ReportDialog + control={reportDialogControl} + params={{ + type: 'post', + uri: postUri, + cid: postCid, + }} + /> + + <Prompt.Basic + control={loggedOutWarningPromptControl} + title={_(msg`Note about sharing`)} + description={_( + msg`This post is only visible to logged-in users. It won't be visible to people who aren't logged in.`, + )} + onConfirm={onSharePost} + confirmButtonCta={_(msg`Share anyway`)} + /> </EventStopper> ) } diff --git a/src/view/com/util/forms/SelectableBtn.tsx b/src/view/com/util/forms/SelectableBtn.tsx index f09d063a1..e577e155d 100644 --- a/src/view/com/util/forms/SelectableBtn.tsx +++ b/src/view/com/util/forms/SelectableBtn.tsx @@ -57,6 +57,7 @@ const styles = StyleSheet.create({ btn: { flexDirection: 'row', justifyContent: 'center', + flexGrow: 1, borderWidth: 1, borderLeftWidth: 0, paddingHorizontal: 10, |