diff --git a/src/components/ContextMenu/account.ts b/src/components/ContextMenu/account.ts index 4fe00925..cd5e4793 100644 --- a/src/components/ContextMenu/account.ts +++ b/src/components/ContextMenu/account.ts @@ -131,6 +131,9 @@ const contextMenuAccount = ({ actions, type, queryKey, rootQueryKey, id: account } return (index: number) => { + if (typeof index !== 'number' || !actions[index]) { + return // For Android + } if (actions[index].id === 'account-mute') { analytics('timeline_shared_headeractions_account_mute_press', { page: queryKey && queryKey[1].page diff --git a/src/components/ContextMenu/instance.ts b/src/components/ContextMenu/instance.ts index 862a09e6..92df562e 100644 --- a/src/components/ContextMenu/instance.ts +++ b/src/components/ContextMenu/instance.ts @@ -1,9 +1,6 @@ import analytics from '@components/analytics' import { displayMessage } from '@components/Message' -import { - QueryKeyTimeline, - useTimelineMutation -} from '@utils/queryHooks/timeline' +import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline' import { getInstanceUrl } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import { useTranslation } from 'react-i18next' @@ -19,12 +16,7 @@ export interface Props { rootQueryKey?: QueryKeyTimeline } -const contextMenuInstance = ({ - actions, - status, - queryKey, - rootQueryKey -}: Props) => { +const contextMenuInstance = ({ actions, status, queryKey, rootQueryKey }: Props) => { const { t } = useTranslation('componentContextMenu') const { theme } = useTheme() @@ -72,10 +64,12 @@ const contextMenuInstance = ({ } return (index: number) => { + if (typeof index !== 'number' || !actions[index]) { + return // For Android + } if ( actions[index].id === 'instance-block' || - (actions[index].id === 'instance' && - actions[index].actions?.[0].id === 'instance-block') + (actions[index].id === 'instance' && actions[index].actions?.[0].id === 'instance-block') ) { analytics('timeline_shared_headeractions_domain_block_press', { page: queryKey[1].page diff --git a/src/components/ContextMenu/share.ts b/src/components/ContextMenu/share.ts index 5d7052c6..03401876 100644 --- a/src/components/ContextMenu/share.ts +++ b/src/components/ContextMenu/share.ts @@ -25,7 +25,8 @@ const contextMenuShare = ({ copiableContent, actions, type, url }: Props) => { title: t(`share.${type}.action`), systemIcon: 'square.and.arrow.up' }) - Platform.OS !== 'android' && type === 'status' && + Platform.OS !== 'android' && + type === 'status' && actions.push({ id: 'copy', title: t(`copy.action`), @@ -34,6 +35,9 @@ const contextMenuShare = ({ copiableContent, actions, type, url }: Props) => { }) return (index: number) => { + if (typeof index !== 'number' || !actions[index]) { + return // For Android + } if (actions[index].id === 'copy') { analytics('timeline_shared_headeractions_copy_press') Clipboard.setString(copiableContent?.current.content || '') diff --git a/src/components/ContextMenu/status.ts b/src/components/ContextMenu/status.ts index 5caac672..8e98f7fc 100644 --- a/src/components/ContextMenu/status.ts +++ b/src/components/ContextMenu/status.ts @@ -9,10 +9,7 @@ import { QueryKeyTimeline, useTimelineMutation } from '@utils/queryHooks/timeline' -import { - checkInstanceFeature, - getInstanceAccount -} from '@utils/slices/instancesSlice' +import { checkInstanceFeature, getInstanceAccount } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' @@ -27,16 +24,8 @@ export interface Props { rootQueryKey?: QueryKeyTimeline } -const contextMenuStatus = ({ - actions, - status, - queryKey, - rootQueryKey -}: Props) => { - const navigation = - useNavigation< - NativeStackNavigationProp - >() +const contextMenuStatus = ({ actions, status, queryKey, rootQueryKey }: Props) => { + const navigation = useNavigation>() const { theme } = useTheme() const { t } = useTranslation('componentContextMenu') @@ -44,8 +33,7 @@ const contextMenuStatus = ({ const mutation = useTimelineMutation({ onMutate: true, onError: (err: any, params, oldData) => { - const theFunction = (params as MutationVarsTimelineUpdateStatusProperty) - .payload + const theFunction = (params as MutationVarsTimelineUpdateStatusProperty).payload ? (params as MutationVarsTimelineUpdateStatusProperty).payload.property : 'delete' displayMessage({ @@ -59,17 +47,14 @@ const contextMenuStatus = ({ err.data && err.data.error && typeof err.data.error === 'string' && { - description: err.data.error - }) + description: err.data.error + }) }) queryClient.setQueryData(queryKey, oldData) } }) - const instanceAccount = useSelector( - getInstanceAccount, - (prev, next) => prev.id === next.id - ) + const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id) const ownAccount = instanceAccount?.id === status?.account?.id if (ownAccount) { @@ -118,83 +103,75 @@ const contextMenuStatus = ({ } return async (index: number) => { + if (typeof index !== 'number' || !actions[index]) { + return // For Android + } if (actions[index].id === 'status-delete') { analytics('timeline_shared_headeractions_status_delete_press', { page: queryKey && queryKey[1].page }) - Alert.alert( - t('status.delete.alert.title'), - t('status.delete.alert.message'), - [ - { - text: t('status.delete.alert.buttons.confirm'), - style: 'destructive', - onPress: async () => { - analytics('timeline_shared_headeractions_status_delete_confirm', { - page: queryKey && queryKey[1].page - }) - mutation.mutate({ - type: 'deleteItem', - source: 'statuses', - queryKey, - rootQueryKey, - id: status.id - }) - } - }, - { - text: t('common:buttons.cancel') + Alert.alert(t('status.delete.alert.title'), t('status.delete.alert.message'), [ + { + text: t('status.delete.alert.buttons.confirm'), + style: 'destructive', + onPress: async () => { + analytics('timeline_shared_headeractions_status_delete_confirm', { + page: queryKey && queryKey[1].page + }) + mutation.mutate({ + type: 'deleteItem', + source: 'statuses', + queryKey, + rootQueryKey, + id: status.id + }) } - ] - ) + }, + { + text: t('common:buttons.cancel') + } + ]) } if (actions[index].id === 'status-delete-edit') { analytics('timeline_shared_headeractions_status_deleteedit_press', { page: queryKey && queryKey[1].page }) - Alert.alert( - t('status.deleteEdit.alert.title'), - t('status.deleteEdit.alert.message'), - [ - { - text: t('status.deleteEdit.alert.buttons.confirm'), - style: 'destructive', - onPress: async () => { - analytics( - 'timeline_shared_headeractions_status_deleteedit_confirm', - { - page: queryKey && queryKey[1].page - } - ) - let replyToStatus: Mastodon.Status | undefined = undefined - if (status.in_reply_to_id) { - replyToStatus = await apiInstance({ - method: 'get', - url: `statuses/${status.in_reply_to_id}` - }).then(res => res.body) - } - mutation - .mutateAsync({ - type: 'deleteItem', - source: 'statuses', - queryKey, - id: status.id - }) - .then(res => { - navigation.navigate('Screen-Compose', { - type: 'deleteEdit', - incomingStatus: res.body as Mastodon.Status, - ...(replyToStatus && { replyToStatus }), - queryKey - }) - }) + Alert.alert(t('status.deleteEdit.alert.title'), t('status.deleteEdit.alert.message'), [ + { + text: t('status.deleteEdit.alert.buttons.confirm'), + style: 'destructive', + onPress: async () => { + analytics('timeline_shared_headeractions_status_deleteedit_confirm', { + page: queryKey && queryKey[1].page + }) + let replyToStatus: Mastodon.Status | undefined = undefined + if (status.in_reply_to_id) { + replyToStatus = await apiInstance({ + method: 'get', + url: `statuses/${status.in_reply_to_id}` + }).then(res => res.body) } - }, - { - text: t('common:buttons.cancel') + mutation + .mutateAsync({ + type: 'deleteItem', + source: 'statuses', + queryKey, + id: status.id + }) + .then(res => { + navigation.navigate('Screen-Compose', { + type: 'deleteEdit', + incomingStatus: res.body as Mastodon.Status, + ...(replyToStatus && { replyToStatus }), + queryKey + }) + }) } - ] - ) + }, + { + text: t('common:buttons.cancel') + } + ]) } if (actions[index].id === 'status-mute') { analytics('timeline_shared_headeractions_status_mute_press', { diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 18d88803..e861298a 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -157,15 +157,6 @@ const TimelineDefault: React.FC = ({ return disableOnPress ? ( {main()} - ) : Platform.OS === 'android' ? ( - {}} - > - {main()} - ) : ( { queryKey }) + const { showActionSheetWithOptions } = useActionSheet() + return ( @@ -82,25 +85,26 @@ const TimelineHeaderDefault = ({ queryKey, status, highlighted }: Props) => { null} - > - { - for (const on of [shareOnPress, statusOnPress, accountOnPress, instanceOnPress]) { - on && on(index) + onPress={() => + showActionSheetWithOptions( + { + options: actions.map(action => action.title), + cancelButtonIndex: 999, + destructiveButtonIndex: actions + .map((action, index) => (action.destructive ? index : 999)) + .filter(num => num !== 999) + }, + index => { + if (index !== undefined) { + for (const on of [shareOnPress, statusOnPress, accountOnPress, instanceOnPress]) { + on && on(index) + } + } } - }} - children={ - - } - /> + ) + } + > + ) : null} diff --git a/src/components/Timeline/Shared/HeaderNotification.android.tsx b/src/components/Timeline/Shared/HeaderNotification.android.tsx index dc94a54d..de62abd0 100644 --- a/src/components/Timeline/Shared/HeaderNotification.android.tsx +++ b/src/components/Timeline/Shared/HeaderNotification.android.tsx @@ -4,12 +4,13 @@ import contextMenuShare from '@components/ContextMenu/share' import contextMenuStatus from '@components/ContextMenu/status' import Icon from '@components/Icon' import { RelationshipIncoming, RelationshipOutgoing } from '@components/Relationship' +import { useActionSheet } from '@expo/react-native-action-sheet' import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useMemo } from 'react' import { Pressable, View } from 'react-native' -import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view' +import { ContextMenuAction } from 'react-native-context-menu-view' import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedApplication from './HeaderShared/Application' import HeaderSharedCreated from './HeaderShared/Created' @@ -57,6 +58,8 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => { queryKey }) + const { showActionSheetWithOptions } = useActionSheet() + const actions = useMemo(() => { switch (notification.type) { case 'follow': @@ -68,29 +71,34 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => { return ( null} - children={ - { - for (const on of [ - shareOnPress, - statusOnPress, - accountOnPress, - instanceOnPress - ]) { - on && on(index) + onPress={() => + showActionSheetWithOptions( + { + options: contextMenuActions.map(action => action.title), + cancelButtonIndex: 999, + destructiveButtonIndex: contextMenuActions + .map((action, index) => (action.destructive ? index : 999)) + .filter(num => num !== 999) + }, + index => { + if (index !== undefined) { + for (const on of [ + shareOnPress, + statusOnPress, + accountOnPress, + instanceOnPress + ]) { + on && on(index) + } } - }} - children={ - } + ) + } + children={ + } />