diff --git a/src/components/Menu/Container.tsx b/src/components/Menu/Container.tsx index 0a7910c0..10c942eb 100644 --- a/src/components/Menu/Container.tsx +++ b/src/components/Menu/Container.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { StyleSheet, View } from 'react-native' +import { View } from 'react-native' import { StyleConstants } from '@utils/styles/constants' export interface Props { @@ -7,14 +7,16 @@ export interface Props { } const MenuContainer: React.FC = ({ children }) => { - return {children} + return ( + + {children} + + ) } -const styles = StyleSheet.create({ - base: { - paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, - marginBottom: StyleConstants.Spacing.Global.PagePadding - } -}) - export default MenuContainer diff --git a/src/components/Menu/Row.tsx b/src/components/Menu/Row.tsx index e5027ff1..3953e5c9 100644 --- a/src/components/Menu/Row.tsx +++ b/src/components/Menu/Row.tsx @@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { ColorDefinitions } from '@utils/styles/themes' import React, { useMemo } from 'react' -import { View } from 'react-native' +import { Text, View } from 'react-native' import { Flow } from 'react-native-animated-spinkit' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' @@ -65,6 +65,7 @@ const MenuRow: React.FC = ({ > { + if (typeof iconBack !== 'string') return // Let icon back handles the gesture if (nativeEvent.state === State.ACTIVE && !loading) { if (screenReaderEnabled && switchOnValueChange) { switchOnValueChange() @@ -79,12 +80,13 @@ const MenuRow: React.FC = ({ style={{ flex: 1, flexDirection: 'row', - paddingTop: StyleConstants.Spacing.S + justifyContent: 'space-between', + marginTop: StyleConstants.Spacing.S }} > = ({ queryKey, rootQueryKey, status url: status.url || status.uri }) const mAccount = menuAccount({ + type: 'status', openChange, - id: status.account.id, + account: status.account, queryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey }) diff --git a/src/components/Timeline/Shared/HeaderDefault.tsx b/src/components/Timeline/Shared/HeaderDefault.tsx index 7a649026..3e0e7102 100644 --- a/src/components/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timeline/Shared/HeaderDefault.tsx @@ -45,8 +45,9 @@ const TimelineHeaderDefault: React.FC = ({ copiableContent }) const mAccount = menuAccount({ + type: 'status', openChange, - id: status.account.id, + account: status.account, queryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey }) diff --git a/src/components/Timeline/Shared/HeaderNotification.tsx b/src/components/Timeline/Shared/HeaderNotification.tsx index 4e1b7854..b29589d8 100644 --- a/src/components/Timeline/Shared/HeaderNotification.tsx +++ b/src/components/Timeline/Shared/HeaderNotification.tsx @@ -31,8 +31,9 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => { url: notification.status?.url || notification.status?.uri }) const mAccount = menuAccount({ + type: 'status', openChange, - id: notification.status?.account.id, + account: notification.status?.account, queryKey }) const mStatus = menuStatus({ status: notification.status, queryKey }) diff --git a/src/components/contextMenu/account.tsx b/src/components/contextMenu/account.tsx index 3e0dc319..34b52dce 100644 --- a/src/components/contextMenu/account.tsx +++ b/src/components/contextMenu/account.tsx @@ -1,5 +1,8 @@ import haptics from '@components/haptics' import { displayMessage } from '@components/Message' +import { useNavigation } from '@react-navigation/native' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { TabSharedStackParamList } from '@utils/navigation/navigators' import { QueryKeyRelationship, useRelationshipMutation, @@ -19,25 +22,29 @@ import { useQueryClient } from 'react-query' import { useSelector } from 'react-redux' const menuAccount = ({ + type, openChange, - id, + account, queryKey, rootQueryKey }: { + type: 'status' | 'account' // Where the action is coming from openChange: boolean - id?: Mastodon.Account['id'] + account?: Pick queryKey?: QueryKeyTimeline rootQueryKey?: QueryKeyTimeline }): ContextMenu[][] => { - if (!id) return [] + if (!account) return [] + const navigation = + useNavigation>() const { theme } = useTheme() const { t } = useTranslation('componentContextMenu') const menus: ContextMenu[][] = [[]] const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id) - const ownAccount = instanceAccount?.id === id + const ownAccount = instanceAccount?.id === account.id const [enabled, setEnabled] = useState(openChange) useEffect(() => { @@ -45,12 +52,12 @@ const menuAccount = ({ setEnabled(true) } }, [openChange, enabled]) - const { data, isFetching } = useRelationshipQuery({ id, options: { enabled } }) + const { data, isFetching } = useRelationshipQuery({ id: account.id, options: { enabled } }) const queryClient = useQueryClient() const timelineMutation = useTimelineMutation({ onSuccess: (_, params) => { - queryClient.refetchQueries(['Relationship', { id }]) + queryClient.refetchQueries(['Relationship', { id: account.id }]) const theParams = params as MutationVarsTimelineUpdateAccountProperty displayMessage({ theme, @@ -90,7 +97,7 @@ const menuAccount = ({ rootQueryKey && queryClient.invalidateQueries(rootQueryKey) } }) - const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] + const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: account.id }] const relationshipMutation = useRelationshipMutation({ onSuccess: (res, { payload: { action } }) => { haptics('Success') @@ -118,14 +125,14 @@ const menuAccount = ({ } }) - if (!ownAccount && Platform.OS !== 'android') { + if (!ownAccount && Platform.OS !== 'android' && type !== 'account') { menus[0].push({ key: 'account-following', item: { onSelect: () => data && relationshipMutation.mutate({ - id, + id: account.id, type: 'outgoing', payload: { action: 'follow', state: !data?.requested ? data.following : true } }), @@ -146,6 +153,17 @@ const menuAccount = ({ }) } if (!ownAccount) { + menus[0].push({ + key: 'account-list', + item: { + onSelect: () => navigation.navigate('Tab-Shared-Account-In-Lists', { account }), + disabled: Platform.OS !== 'android' ? !data || isFetching : false, + destructive: false, + hidden: isFetching ? false : !data?.following + }, + title: t('account.inLists'), + icon: 'checklist' + }) menus[0].push({ key: 'account-mute', item: { @@ -153,7 +171,7 @@ const menuAccount = ({ timelineMutation.mutate({ type: 'updateAccountProperty', queryKey, - id, + id: account.id, payload: { property: 'mute', currentValue: data?.muting } }), disabled: Platform.OS !== 'android' ? !data || isFetching : false, @@ -176,7 +194,7 @@ const menuAccount = ({ timelineMutation.mutate({ type: 'updateAccountProperty', queryKey, - id, + id: account.id, payload: { property: 'block', currentValue: data?.blocking } }), disabled: Platform.OS !== 'android' ? !data || isFetching : false, @@ -195,13 +213,13 @@ const menuAccount = ({ timelineMutation.mutate({ type: 'updateAccountProperty', queryKey, - id, + id: account.id, payload: { property: 'reports' } }) timelineMutation.mutate({ type: 'updateAccountProperty', queryKey, - id, + id: account.id, payload: { property: 'block', currentValue: false } }) }, diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 14ce79e7..8a4d2329 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -5,7 +5,8 @@ "cancel": "Cancel", "discard": "Discard", "continue": "Continue", - "delete": "Delete" + "delete": "Delete", + "done": "Done" }, "customEmoji": { "accessibilityLabel": "Custom emoji {{emoji}}" diff --git a/src/i18n/en/components/contextMenu.json b/src/i18n/en/components/contextMenu.json index 4ce19a8b..08b201d7 100644 --- a/src/i18n/en/components/contextMenu.json +++ b/src/i18n/en/components/contextMenu.json @@ -6,6 +6,7 @@ "action_false": "Follow user", "action_true": "Unfollow user" }, + "inLists": "Manage user of lists", "mute": { "action_false": "Mute user", "action_true": "Unmute user" diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index 828ff2e2..808dd98b 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -320,6 +320,11 @@ }, "suspended": "Account suspended by the moderators of your server" }, + "accountInLists": { + "name": "Lists of @{{username}}", + "inLists": "In lists", + "notInLists": "Other lists" + }, "attachments": { "name": "<0 /><1>\"s media" }, diff --git a/src/screens/Tabs/Shared/Account.tsx b/src/screens/Tabs/Shared/Account.tsx index 38202a93..c36ee564 100644 --- a/src/screens/Tabs/Shared/Account.tsx +++ b/src/screens/Tabs/Shared/Account.tsx @@ -11,7 +11,7 @@ import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import React, { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import { StyleSheet, Text, View } from 'react-native' +import { Text, View } from 'react-native' import { useSharedValue } from 'react-native-reanimated' import { useIsFetching } from 'react-query' import * as DropdownMenu from 'zeego/dropdown-menu' @@ -30,21 +30,10 @@ const TabSharedAccount: React.FC const { colors, mode } = useTheme() const mShare = menuShare({ type: 'account', url: account.url }) - const mAccount = menuAccount({ openChange: true, id: account.id }) + const mAccount = menuAccount({ type: 'account', openChange: true, account }) useEffect(() => { navigation.setOptions({ headerRight: () => { - // const shareOnPress = contextMenuShare({ - // actions, - // type: 'account', - // url: account.url - // }) - // const accountOnPress = contextMenuAccount({ - // actions, - // type: 'account', - // id: account.id - // }) - return ( @@ -107,7 +96,7 @@ const TabSharedAccount: React.FC const ListHeaderComponent = useMemo(() => { return ( <> - + {!data?.suspended && fetchedTimeline.current ? ( @@ -129,7 +118,10 @@ const TabSharedAccount: React.FC break } }} - style={styles.segmentsContainer} + style={{ + marginTop: StyleConstants.Spacing.M, + marginHorizontal: StyleConstants.Spacing.Global.PagePadding + }} /> ) : null} {data?.suspended ? ( @@ -178,14 +170,4 @@ const TabSharedAccount: React.FC ) } -const styles = StyleSheet.create({ - header: { - borderBottomWidth: 1 - }, - segmentsContainer: { - marginTop: StyleConstants.Spacing.M, - marginHorizontal: StyleConstants.Spacing.Global.PagePadding - } -}) - export default TabSharedAccount diff --git a/src/screens/Tabs/Shared/AccountInLists.tsx b/src/screens/Tabs/Shared/AccountInLists.tsx new file mode 100644 index 00000000..4cc2900f --- /dev/null +++ b/src/screens/Tabs/Shared/AccountInLists.tsx @@ -0,0 +1,119 @@ +import Button from '@components/Button' +import haptics from '@components/haptics' +import { HeaderRight } from '@components/Header' +import { MenuRow } from '@components/Menu' +import CustomText from '@components/Text' +import { TabSharedStackScreenProps } from '@utils/navigation/navigators' +import { useAccountInListsQuery } from '@utils/queryHooks/account' +import { useListAccountsMutation, useListsQuery } from '@utils/queryHooks/lists' +import { StyleConstants } from '@utils/styles/constants' +import { useTheme } from '@utils/styles/ThemeManager' +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { SectionList, Text, View } from 'react-native' + +const TabSharedAccountInLists: React.FC< + TabSharedStackScreenProps<'Tab-Shared-Account-In-Lists'> +> = ({ + navigation, + route: { + params: { account } + } +}) => { + const { colors } = useTheme() + const { t } = useTranslation('screenTabs') + + useEffect(() => { + navigation.setOptions({ + presentation: 'modal', + title: t('shared.accountInLists.name', { username: account.username }), + headerLeft: () => null, + headerRight: () => { + return ( + navigation.pop(1)} + /> + ) + } + }) + }, []) + + const listsQuery = useListsQuery({}) + const accountInListsQuery = useAccountInListsQuery({ id: account.id }) + + const sections = [ + { id: 'in', title: t('shared.accountInLists.inLists'), data: accountInListsQuery.data || [] }, + { + id: 'out', + title: t('shared.accountInLists.notInLists'), + data: + listsQuery.data?.filter( + ({ id }) => !accountInListsQuery.data?.filter(d => d.id === id).length + ) || [] + } + ] + + const listAccountsMutation = useListAccountsMutation({}) + + return ( + { + return props.leadingItem && props.trailingSection ? ( + + ) : null + }} + renderSectionHeader={({ section: { title, data } }) => + data.length ? ( + + ) : null + } + renderItem={({ index, item, section }) => ( + { + switch (section.id) { + case 'in': + listAccountsMutation + .mutateAsync({ + type: 'delete', + payload: { id: item.id, accounts: [account.id] } + }) + .then(() => { + haptics('Light') + accountInListsQuery.refetch() + }) + break + case 'out': + listAccountsMutation + .mutateAsync({ + type: 'add', + payload: { id: item.id, accounts: [account.id] } + }) + .then(() => { + haptics('Light') + accountInListsQuery.refetch() + }) + break + } + }} + /> + } + title={item.title} + /> + )} + > + ) +} + +export default TabSharedAccountInLists diff --git a/src/screens/Tabs/Shared/index.tsx b/src/screens/Tabs/Shared/index.tsx index 46a7e25b..131079dc 100644 --- a/src/screens/Tabs/Shared/index.tsx +++ b/src/screens/Tabs/Shared/index.tsx @@ -16,6 +16,7 @@ import { debounce } from 'lodash' import React from 'react' import { Trans, useTranslation } from 'react-i18next' import { Platform, TextInput, View } from 'react-native' +import TabSharedAccountInLists from './AccountInLists' const TabShared = ({ Stack }: { Stack: ReturnType }) => { const { colors, mode } = useTheme() @@ -48,6 +49,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType + + + } 'Tab-Shared-Attachments': { account: Mastodon.Account } 'Tab-Shared-Hashtag': { hashtag: Mastodon.Tag['name'] diff --git a/src/utils/queryHooks/account.ts b/src/utils/queryHooks/account.ts index 45ab11df..7187b437 100644 --- a/src/utils/queryHooks/account.ts +++ b/src/utils/queryHooks/account.ts @@ -4,7 +4,7 @@ import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query' export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }] -const queryFunction = ({ queryKey }: QueryFunctionContext) => { +const accountQueryFunction = ({ queryKey }: QueryFunctionContext) => { const { id } = queryKey[1] return apiInstance({ @@ -20,7 +20,30 @@ const useAccountQuery = ({ options?: UseQueryOptions }) => { const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }] - return useQuery(queryKey, queryFunction, options) + return useQuery(queryKey, accountQueryFunction, options) } -export { useAccountQuery } +/* ----- */ + +export type QueryKeyAccountInLists = ['AccountInLists', { id: Mastodon.Account['id'] }] + +const accountInListsQueryFunction = ({ queryKey }: QueryFunctionContext) => { + const { id } = queryKey[1] + + return apiInstance({ + method: 'get', + url: `accounts/${id}/lists` + }).then(res => res.body) +} + +const useAccountInListsQuery = ({ + options, + ...queryKeyParams +}: QueryKeyAccount[1] & { + options?: UseQueryOptions +}) => { + const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }] + return useQuery(queryKey, accountInListsQueryFunction, options) +} + +export { useAccountQuery, useAccountInListsQuery }