diff --git a/package.json b/package.json index f9275a96..96fc1cb6 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "native": "220603", "major": 4, "minor": 1, - "patch": 1, + "patch": 2, "expo": "45.0.0" }, "description": "tooot app for Mastodon", diff --git a/src/Screens.tsx b/src/Screens.tsx index b5e1c889..f83cf37c 100644 --- a/src/Screens.tsx +++ b/src/Screens.tsx @@ -271,8 +271,9 @@ const Screens: React.FC = ({ localCorrupt }) => { return ( { + queryClient.refetchQueries(['Relationship', { id: accountId }]) const theParams = params as MutationVarsTimelineUpdateAccountProperty displayMessage({ theme, type: 'success', message: t('common:message.success.message', { - function: t(`account.${theParams.payload.property}.action`) + function: t(`account.${theParams.payload.property}.action`, { + ...(typeof theParams.payload.currentValue === 'boolean' && { + context: theParams.payload.currentValue.toString() + }) + }) }) }) }, @@ -47,7 +54,11 @@ const contextMenuAccount = ({ theme, type: 'error', message: t('common:message.error.message', { - function: t(`account.${theParams.payload.property}.action`) + function: t(`account.${theParams.payload.property}.action`, { + ...(typeof theParams.payload.currentValue === 'boolean' && { + context: theParams.payload.currentValue.toString() + }) + }) }), ...(err.status && typeof err.status === 'number' && @@ -70,93 +81,70 @@ const contextMenuAccount = ({ ) const ownAccount = instanceAccount?.id === accountId + const { data: relationship } = useRelationshipQuery({ + id: accountId, + options: { enabled: type === 'account' } + }) + if (!ownAccount) { - switch (Platform.OS) { - case 'ios': - actions.push({ - id: 'account', - title: t('account.title'), - inlineChildren: true, - actions: [ - { - id: 'account-mute', - title: t('account.mute.action'), - systemIcon: 'eye.slash' - }, - { - id: 'account-block', - title: t('account.block.action'), - systemIcon: 'xmark.circle', - destructive: true - }, - { - id: 'account-reports', - title: t('account.reports.action'), - systemIcon: 'flag', - destructive: true - } - ] - }) - break - default: - actions.push( - { - id: 'account-mute', - title: t('account.mute.action'), - systemIcon: 'eye.slash' - }, - { - id: 'account-block', - title: t('account.block.action'), - systemIcon: 'xmark.circle', - destructive: true - }, - { - id: 'account-reports', - title: t('account.reports.action'), - systemIcon: 'flag', - destructive: true - } - ) - break - } + actions.push( + { + id: 'account-mute', + title: t('account.mute.action', { + context: (relationship?.muting || false).toString() + }), + systemIcon: 'eye.slash' + }, + { + id: 'account-block', + title: t('account.block.action', { + context: (relationship?.blocking || false).toString() + }), + systemIcon: 'xmark.circle', + destructive: true + }, + { + id: 'account-reports', + title: t('account.reports.action'), + systemIcon: 'flag', + destructive: true + } + ) } - return (id: string) => { - switch (id) { - case 'account-mute': - analytics('timeline_shared_headeractions_account_mute_press', { - page: queryKey && queryKey[1].page - }) - mutateion.mutate({ - type: 'updateAccountProperty', - queryKey, - id: accountId, - payload: { property: 'mute' } - }) - break - case 'account-block': - analytics('timeline_shared_headeractions_account_block_press', { - page: queryKey && queryKey[1].page - }) - mutateion.mutate({ - type: 'updateAccountProperty', - queryKey, - id: accountId, - payload: { property: 'block' } - }) - break - case 'account-report': - analytics('timeline_shared_headeractions_account_reports_press', { - page: queryKey && queryKey[1].page - }) - mutateion.mutate({ - type: 'updateAccountProperty', - queryKey, - id: accountId, - payload: { property: 'reports' } - }) - break + return (index: number) => { + if (actions[index].id === 'account-mute') { + analytics('timeline_shared_headeractions_account_mute_press', { + page: queryKey && queryKey[1].page + }) + mutateion.mutate({ + type: 'updateAccountProperty', + queryKey, + id: accountId, + payload: { property: 'mute', currentValue: relationship?.muting } + }) + } + if (actions[index].id === 'account-block') { + analytics('timeline_shared_headeractions_account_block_press', { + page: queryKey && queryKey[1].page + }) + mutateion.mutate({ + type: 'updateAccountProperty', + queryKey, + id: accountId, + payload: { property: 'block', currentValue: relationship?.blocking } + }) + } + if (actions[index].id === 'account-report') { + analytics('timeline_shared_headeractions_account_reports_press', { + page: queryKey && queryKey[1].page + }) + mutateion.mutate({ + type: 'updateAccountProperty', + queryKey, + id: accountId, + payload: { property: 'reports' } + }) } } } diff --git a/src/components/ContextMenu/instance.ts b/src/components/ContextMenu/instance.ts index 676bc8ad..3e0cd165 100644 --- a/src/components/ContextMenu/instance.ts +++ b/src/components/ContextMenu/instance.ts @@ -71,36 +71,38 @@ const contextMenuInstance = ({ } } - return (id: string) => { - switch (id) { - case 'instance-block': - analytics('timeline_shared_headeractions_domain_block_press', { - page: queryKey[1].page - }) - Alert.alert( - t('instance.block.alert.title', { instance }), - t('instance.block.alert.message'), - [ - { - text: t('instance.block.alert.buttons.confirm'), - style: 'destructive', - onPress: () => { - analytics( - 'timeline_shared_headeractions_domain_block_confirm', - { page: queryKey && queryKey[1].page } - ) - mutation.mutate({ - type: 'domainBlock', - queryKey, - domain: instance - }) - } - }, - { - text: t('common:buttons.cancel') + return (index: number) => { + if ( + actions[index].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 + }) + Alert.alert( + t('instance.block.alert.title', { instance }), + t('instance.block.alert.message'), + [ + { + text: t('instance.block.alert.buttons.confirm'), + style: 'destructive', + onPress: () => { + analytics('timeline_shared_headeractions_domain_block_confirm', { + page: queryKey && queryKey[1].page + }) + mutation.mutate({ + type: 'domainBlock', + queryKey, + domain: instance + }) } - ] - ) + }, + { + text: t('common:buttons.cancel') + } + ] + ) } } } diff --git a/src/components/ContextMenu/share.ts b/src/components/ContextMenu/share.ts index 4f7b25ec..7d797847 100644 --- a/src/components/ContextMenu/share.ts +++ b/src/components/ContextMenu/share.ts @@ -18,19 +18,17 @@ const contextMenuShare = ({ actions, type, url }: Props) => { systemIcon: 'square.and.arrow.up' }) - return (id: string) => { - switch (id) { - case 'share': - analytics('timeline_shared_headeractions_share_press') - switch (Platform.OS) { - case 'ios': - Share.share({ url }) - break - case 'android': - Share.share({ message: url }) - break - } - break + return (index: number) => { + if (actions[index].id === 'share') { + analytics('timeline_shared_headeractions_share_press') + switch (Platform.OS) { + case 'ios': + Share.share({ url }) + break + case 'android': + Share.share({ message: url }) + break + } } } } diff --git a/src/components/ContextMenu/status.ts b/src/components/ContextMenu/status.ts index 8867b01a..5579a390 100644 --- a/src/components/ContextMenu/status.ts +++ b/src/components/ContextMenu/status.ts @@ -15,7 +15,7 @@ import { } from '@utils/slices/instancesSlice' import { useTheme } from '@utils/styles/ThemeManager' import { useTranslation } from 'react-i18next' -import { Alert, Platform } from 'react-native' +import { Alert } from 'react-native' import { ContextMenuAction } from 'react-native-context-menu-view' import { useQueryClient } from 'react-query' import { useSelector } from 'react-redux' @@ -88,7 +88,7 @@ const contextMenuStatus = ({ }, { id: 'status-mute', - title: t('status.mute.action-muted', { + title: t('status.mute.action', { context: status.muted.toString() }), systemIcon: status.muted ? 'speaker' : 'speaker.slash' @@ -107,178 +107,161 @@ const contextMenuStatus = ({ if (status.visibility === 'public' || status.visibility === 'unlisted') { accountMenuItems.push({ id: 'status-pin', - title: t('status.pin.action-pinned', { + title: t('status.pin.action', { context: status.pinned.toString() }), systemIcon: status.pinned ? 'pin.slash' : 'pin' }) } - switch (Platform.OS) { - case 'ios': - actions.push({ - id: 'status', - title: t('status.title'), - inlineChildren: true, - actions: accountMenuItems - }) - break - default: - actions.push(...accountMenuItems) - break - } + actions.push(...accountMenuItems) } - return async (id: string) => { - switch (id) { - case '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({ + return async (index: number) => { + 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') + } + ] + ) + } + 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, - rootQueryKey, id: status.id }) - } - }, - { - text: t('common:buttons.cancel') - } - ] - ) - break - case '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 }) - .then(res => { - navigation.navigate('Screen-Compose', { - type: 'deleteEdit', - incomingStatus: res.body as Mastodon.Status, - ...(replyToStatus && { replyToStatus }), - queryKey - }) - }) - } - }, - { - text: t('common:buttons.cancel') + }) } - ] - ) - break - case 'status-mute': - analytics('timeline_shared_headeractions_status_mute_press', { - page: queryKey && queryKey[1].page - }) - mutation.mutate({ - type: 'updateStatusProperty', - queryKey, - rootQueryKey, - id: status.id, - payload: { - property: 'muted', - currentValue: status.muted, - propertyCount: undefined, - countValue: undefined + }, + { + text: t('common:buttons.cancel') } - }) - break - case 'status-edit': - analytics('timeline_shared_headeractions_status_edit_press', { - 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) + ] + ) + } + if (actions[index].id === 'status-mute') { + analytics('timeline_shared_headeractions_status_mute_press', { + page: queryKey && queryKey[1].page + }) + mutation.mutate({ + type: 'updateStatusProperty', + queryKey, + rootQueryKey, + id: status.id, + payload: { + property: 'muted', + currentValue: status.muted, + propertyCount: undefined, + countValue: undefined } - apiInstance<{ - id: Mastodon.Status['id'] - text: NonNullable - spoiler_text: Mastodon.Status['spoiler_text'] - }>({ + }) + } + if (actions[index].id === 'status-edit') { + analytics('timeline_shared_headeractions_status_edit_press', { + 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.id}/source` - }).then(res => { - navigation.navigate('Screen-Compose', { - type: 'edit', - incomingStatus: { - ...status, - text: res.body.text, - spoiler_text: res.body.spoiler_text - }, - ...(replyToStatus && { replyToStatus }), - queryKey, - rootQueryKey - }) - }) - break - case 'status-pin': - // Also note that reblogs cannot be pinned. - analytics('timeline_shared_headeractions_status_pin_press', { - page: queryKey && queryKey[1].page - }) - mutation.mutate({ - type: 'updateStatusProperty', + url: `statuses/${status.in_reply_to_id}` + }).then(res => res.body) + } + apiInstance<{ + id: Mastodon.Status['id'] + text: NonNullable + spoiler_text: Mastodon.Status['spoiler_text'] + }>({ + method: 'get', + url: `statuses/${status.id}/source` + }).then(res => { + navigation.navigate('Screen-Compose', { + type: 'edit', + incomingStatus: { + ...status, + text: res.body.text, + spoiler_text: res.body.spoiler_text + }, + ...(replyToStatus && { replyToStatus }), queryKey, - rootQueryKey, - id: status.id, - payload: { - property: 'pinned', - currentValue: status.pinned, - propertyCount: undefined, - countValue: undefined - } + rootQueryKey }) - break + }) + } + if (actions[index].id === 'status-pin') { + // Also note that reblogs cannot be pinned. + analytics('timeline_shared_headeractions_status_pin_press', { + page: queryKey && queryKey[1].page + }) + mutation.mutate({ + type: 'updateStatusProperty', + queryKey, + rootQueryKey, + id: status.id, + payload: { + property: 'pinned', + currentValue: status.pinned, + propertyCount: undefined, + countValue: undefined + } + }) } } } diff --git a/src/components/RelativeTime.tsx b/src/components/RelativeTime.tsx index 7dfd3c6a..5b1589f4 100644 --- a/src/components/RelativeTime.tsx +++ b/src/components/RelativeTime.tsx @@ -3,11 +3,10 @@ import { FormattedRelativeTime } from 'react-intl' import { AppState } from 'react-native' export interface Props { - type: 'past' | 'future' time: string | number } -const RelativeTime: React.FC = ({ type, time }) => { +const RelativeTime: React.FC = ({ time }) => { const [now, setNow] = useState(new Date().getTime()) useEffect(() => { const appStateListener = AppState.addEventListener('change', state => { @@ -21,9 +20,7 @@ const RelativeTime: React.FC = ({ type, time }) => { return ( ) diff --git a/src/components/Timeline/Shared/ContextMenu.tsx b/src/components/Timeline/Shared/ContextMenu.tsx index f11af4b4..856e1835 100644 --- a/src/components/Timeline/Shared/ContextMenu.tsx +++ b/src/components/Timeline/Shared/ContextMenu.tsx @@ -47,6 +47,7 @@ const TimelineContextMenu: React.FC = ({ }) const accountOnPress = contextMenuAccount({ actions, + type: 'status', queryKey, rootQueryKey, id: status.account.id @@ -62,14 +63,14 @@ const TimelineContextMenu: React.FC = ({ { + onPress={({ nativeEvent: { index } }) => { for (const on of [ shareOnPress, statusOnPress, accountOnPress, instanceOnPress ]) { - on && on(id) + on && on(index) } }} children={children} diff --git a/src/components/Timeline/Shared/HeaderShared/Created.tsx b/src/components/Timeline/Shared/HeaderShared/Created.tsx index 9830d72a..8ce2a56b 100644 --- a/src/components/Timeline/Shared/HeaderShared/Created.tsx +++ b/src/components/Timeline/Shared/HeaderShared/Created.tsx @@ -32,7 +32,7 @@ const HeaderSharedCreated = React.memo( /> ) : ( - + )} {edited_at ? ( diff --git a/src/components/Timeline/Shared/Poll.tsx b/src/components/Timeline/Shared/Poll.tsx index fddc8a8d..a44c44fd 100644 --- a/src/components/Timeline/Shared/Poll.tsx +++ b/src/components/Timeline/Shared/Poll.tsx @@ -269,7 +269,7 @@ const TimelinePoll: React.FC = ({ )) }, [theme, allOptions]) - const pollVoteCounts = useMemo(() => { + const pollVoteCounts = () => { if (poll.voters_count !== null) { return ( t('shared.poll.meta.count.voters', { count: poll.voters_count }) + ' • ' @@ -279,9 +279,9 @@ const TimelinePoll: React.FC = ({ t('shared.poll.meta.count.votes', { count: poll.votes_count }) + ' • ' ) } - }, [poll.voters_count, poll.votes_count]) + } - const pollExpiration = useMemo(() => { + const pollExpiration = () => { if (poll.expired) { return t('shared.poll.meta.expiration.expired') } else { @@ -289,12 +289,12 @@ const TimelinePoll: React.FC = ({ return ( ]} + components={[]} /> ) } } - }, [theme, i18n.language, poll.expired, poll.expires_at]) + } return ( @@ -312,8 +312,8 @@ const TimelinePoll: React.FC = ({ fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }} > - {pollVoteCounts} - {pollExpiration} + {pollVoteCounts()} + {pollExpiration()} diff --git a/src/i18n/en/components/contextMenu.json b/src/i18n/en/components/contextMenu.json index 538c7a4f..cd410020 100644 --- a/src/i18n/en/components/contextMenu.json +++ b/src/i18n/en/components/contextMenu.json @@ -3,10 +3,12 @@ "account": { "title": "User actions", "mute": { - "action": "Mute user" + "action_false": "Mute user", + "action_true": "Unmute user" }, "block": { - "action": "Block user" + "action_false": "Block user", + "action_true": "Unblock user" }, "reports": { "action": "Report user" @@ -59,12 +61,12 @@ } }, "mute": { - "action-muted_false": "Mute toot and replies", - "action-muted_true": "Unmute toot and replies" + "action_false": "Mute toot and replies", + "action_true": "Unmute toot and replies" }, "pin": { - "action-pinned_false": "Pin toot", - "action-pinned_true": "Unpin toot" + "action_false": "Pin toot", + "action_true": "Unpin toot" } } } \ No newline at end of file diff --git a/src/i18n/en/components/timeline.json b/src/i18n/en/components/timeline.json index 6a3341a1..f622a093 100644 --- a/src/i18n/en/components/timeline.json +++ b/src/i18n/en/components/timeline.json @@ -139,7 +139,7 @@ }, "expiration": { "expired": "Vote expired", - "until": "Expires in <0 />" + "until": "Expires <0 />" } } } diff --git a/src/screens/Announcements.tsx b/src/screens/Announcements.tsx index 2efc34a6..e38df48c 100644 --- a/src/screens/Announcements.tsx +++ b/src/screens/Announcements.tsx @@ -93,7 +93,7 @@ const ScreenAnnouncements: React.FC< + ]} /> diff --git a/src/screens/Tabs/Shared/Account.tsx b/src/screens/Tabs/Shared/Account.tsx index a0e15f88..d9562b42 100644 --- a/src/screens/Tabs/Shared/Account.tsx +++ b/src/screens/Tabs/Shared/Account.tsx @@ -99,7 +99,8 @@ const TabSharedAccount: React.FC< customProps={{ renderItem, onScroll, - ListHeaderComponent + ListHeaderComponent, + maintainVisibleContentPosition: undefined }} /> diff --git a/src/screens/Tabs/Shared/Root.tsx b/src/screens/Tabs/Shared/Root.tsx index 61ae95f0..ec2ff51a 100644 --- a/src/screens/Tabs/Shared/Root.tsx +++ b/src/screens/Tabs/Shared/Root.tsx @@ -63,15 +63,16 @@ const TabSharedRoot = ({ }) const accountOnPress = contextMenuAccount({ actions, + type: 'account', id: account.id }) return ( { - shareOnPress(id) - accountOnPress(id) + onPress={({ nativeEvent: { index } }) => { + shareOnPress(index) + accountOnPress(index) }} dropdownMenuMode > diff --git a/src/utils/queryHooks/timeline.ts b/src/utils/queryHooks/timeline.ts index cc30c82c..d7e1b131 100644 --- a/src/utils/queryHooks/timeline.ts +++ b/src/utils/queryHooks/timeline.ts @@ -314,6 +314,7 @@ export type MutationVarsTimelineUpdateAccountProperty = { id: Mastodon.Account['id'] payload: { property: 'mute' | 'block' | 'reports' + currentValue?: boolean } } @@ -383,7 +384,9 @@ const mutationFunction = async (params: MutationVarsTimeline) => { case 'mute': return apiInstance({ method: 'post', - url: `accounts/${params.id}/${params.payload.property}` + url: `accounts/${params.id}/${ + params.payload.currentValue ? 'un' : '' + }${params.payload.property}` }) case 'reports': return apiInstance({