1
0
mirror of https://github.com/tooot-app/app synced 2025-04-22 22:27:37 +02:00

Partial fix #495

Added list removing users
This commit is contained in:
xmflsct 2022-12-02 00:13:59 +01:00
parent 0cc1cdd4b6
commit f619d1bb6a
13 changed files with 309 additions and 80 deletions

View File

@ -4,39 +4,47 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators' import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { PropsWithChildren } from 'react'
import { Pressable, View } from 'react-native' import { Pressable, View } from 'react-native'
import GracefullyImage from './GracefullyImage' import GracefullyImage from './GracefullyImage'
import CustomText from './Text' import CustomText from './Text'
export interface Props { export interface Props {
account: Mastodon.Account account: Mastodon.Account
onPress?: () => void Component?: typeof View | typeof Pressable
props?: {}
} }
const ComponentAccount: React.FC<Props> = ({ account, onPress: customOnPress }) => { const ComponentAccount: React.FC<PropsWithChildren & Props> = ({
account,
Component,
props,
children
}) => {
const { colors } = useTheme() const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = useCallback(() => navigation.push('Tab-Shared-Account', { account }), []) if (!props) {
props = { onPress: () => navigation.push('Tab-Shared-Account', { account }) }
}
return ( return React.createElement(
<Pressable Component || Pressable,
accessibilityRole='button' {
style={{ ...props,
style: {
flex: 1, flex: 1,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding, paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.M, paddingVertical: StyleConstants.Spacing.M,
flexDirection: 'row', flexDirection: 'row',
alignSelf: 'flex-start', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}} }
onPress={customOnPress || onPress} },
> <View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<GracefullyImage <GracefullyImage
uri={{ original: account.avatar, static: account.avatar_static }} uri={{ original: account.avatar, static: account.avatar_static }}
style={{ style={{
alignSelf: 'flex-start',
width: StyleConstants.Avatar.S, width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S, height: StyleConstants.Avatar.S,
borderRadius: 6, borderRadius: 6,
@ -62,7 +70,8 @@ const ComponentAccount: React.FC<Props> = ({ account, onPress: customOnPress })
@{account.acct} @{account.acct}
</CustomText> </CustomText>
</View> </View>
</Pressable> </View>,
children
) )
} }

View File

@ -57,7 +57,7 @@ const TimelineEmpty = React.memo(
style={{ style={{
marginTop: StyleConstants.Spacing.S, marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L, marginBottom: StyleConstants.Spacing.L,
color: colors.primaryDefault color: colors.secondary
}} }}
> >
{t('empty.success.message')} {t('empty.success.message')}

View File

@ -46,17 +46,20 @@
"language": { "language": {
"name": "Language" "name": "Language"
}, },
"lists": { "list": {
"name": "Lists" "name": "List: {{list}}"
},
"listAccounts": {
"name": "Users in list: {{list}}"
}, },
"listAdd": { "listAdd": {
"name": "Add a List" "name": "Add a List"
}, },
"listEdit": { "listEdit": {
"name": "Edit List" "name": "Edit List Details"
}, },
"list": { "lists": {
"name": "List: {{list}}" "name": "Lists"
}, },
"push": { "push": {
"name": "Push Notification" "name": "Push Notification"
@ -93,7 +96,13 @@
"XXL": "XXL" "XXL": "XXL"
} }
}, },
"listAccounts": {
"heading": "Manage users",
"error": "Delete user from list",
"empty": "No user added in this list"
},
"listEdit": { "listEdit": {
"heading": "Edit list details",
"title": "Title", "title": "Title",
"repliesPolicy": { "repliesPolicy": {
"heading": "Show replies to:", "heading": "Show replies to:",

View File

@ -44,7 +44,7 @@ const ComposeRootSuggestion: React.FC<Props> = ({ item }) => {
} }
return item.acct ? ( return item.acct ? (
<ComponentAccount account={item} onPress={onPress} /> <ComponentAccount account={item} props={{ onPress }} />
) : ( ) : (
<ComponentHashtag hashtag={item} onPress={onPress} origin='suggestion' /> <ComponentHashtag hashtag={item} onPress={onPress} origin='suggestion' />
) )

View File

@ -8,8 +8,9 @@ import TabMeBookmarks from './Me/Bookmarks'
import TabMeConversations from './Me/Cconversations' import TabMeConversations from './Me/Cconversations'
import TabMeFavourites from './Me/Favourites' import TabMeFavourites from './Me/Favourites'
import TabMeList from './Me/List' import TabMeList from './Me/List'
import TabMeListEdit from './Me/ListEdit' import TabMeListAccounts from './Me/List/Accounts'
import TabMeLists from './Me/Lists' import TabMeListEdit from './Me/List/Edit'
import TabMeListList from './Me/List/List'
import TabMeProfile from './Me/Profile' import TabMeProfile from './Me/Profile'
import TabMePush from './Me/Push' import TabMePush from './Me/Push'
import TabMeRoot from './Me/Root' import TabMeRoot from './Me/Root'
@ -86,21 +87,28 @@ const TabMe = React.memo(
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} /> headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})} })}
/> />
<Stack.Screen
name='Tab-Me-List-Accounts'
component={TabMeListAccounts}
options={({ navigation, route: { params } }) => ({
title: t('me.stacks.listAccounts.name', { list: params.title }),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.listsAdd.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen <Stack.Screen
name='Tab-Me-List-Edit' name='Tab-Me-List-Edit'
component={TabMeListEdit} component={TabMeListEdit}
options={{ options={{
gestureEnabled: false, gestureEnabled: false,
presentation: 'modal', presentation: 'modal'
title: t('me.stacks.listsAdd.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.listsAdd.name')} />
})
}} }}
/> />
<Stack.Screen <Stack.Screen
name='Tab-Me-Lists' name='Tab-Me-List-List'
component={TabMeLists} component={TabMeListList}
options={({ navigation }: any) => ({ options={({ navigation }: any) => ({
title: t('me.stacks.lists.name'), title: t('me.stacks.lists.name'),
...(Platform.OS === 'android' && { ...(Platform.OS === 'android' && {

View File

@ -0,0 +1,99 @@
import ComponentAccount from '@components/Account'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import CustomText from '@components/Text'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import {
QueryKeyListAccounts,
useListAccountsMutation,
useListAccountsQuery
} from '@utils/queryHooks/lists'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList, View } from 'react-native'
const TabMeListAccounts: React.FC<TabMeStackScreenProps<'Tab-Me-List-Accounts'>> = ({
route: { params }
}) => {
const { colors, theme } = useTheme()
const { t } = useTranslation('screenTabs')
const queryKey: QueryKeyListAccounts = ['ListAccounts', { id: params.id }]
const { data, refetch, fetchNextPage, hasNextPage } = useListAccountsQuery({
...queryKey[1],
options: {
getNextPageParam: lastPage =>
lastPage?.links?.next && {
max_id: lastPage.links.next
}
}
})
const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : []
const mutation = useListAccountsMutation({
onSuccess: () => {
haptics('Light')
refetch()
},
onError: () => {
displayMessage({
theme,
type: 'error',
message: t('common:message.error.message', {
function: t('me.listAccounts.error')
})
})
}
})
return (
<FlatList
data={flattenData}
renderItem={({ item, index }) => (
<ComponentAccount
key={index}
account={item}
Component={View}
children={
<Button
type='icon'
content='X'
round
onPress={() =>
mutation.mutate({ type: 'delete', payload: { id: params.id, accounts: [item.id] } })
}
/>
}
/>
)}
ListEmptyComponent={
<View
style={{
flex: 1,
minHeight: '100%',
justifyContent: 'center',
alignItems: 'center'
}}
>
<CustomText
fontStyle='M'
style={{
marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.L,
color: colors.secondary
}}
>
{t('me.listAccounts.empty')}
</CustomText>
</View>
}
onEndReached={() => hasNextPage && fetchNextPage()}
/>
)
}
export default TabMeListAccounts

View File

@ -1,6 +1,6 @@
import { EmojisState } from '@components/Emojis/helpers/EmojisContext' import { EmojisState } from '@components/Emojis/helpers/EmojisContext'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header' import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header'
import ComponentInput from '@components/Input' import ComponentInput from '@components/Input'
import { displayMessage, Message } from '@components/Message' import { displayMessage, Message } from '@components/Message'
import Selections from '@components/Selections' import Selections from '@components/Selections'
@ -12,7 +12,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert, ScrollView, TextInput } from 'react-native' import { Alert, Platform, ScrollView, TextInput } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
@ -83,6 +83,16 @@ const TabMeListEdit: React.FC<TabMeStackScreenProps<'Tab-Me-List-Edit'>> = ({
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
title: params.type === 'add' ? t('me.stacks.listAdd.name') : t('me.stacks.listEdit.name'),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter
content={
params.type === 'add' ? t('me.stacks.listAdd.name') : t('me.stacks.listEdit.name')
}
/>
)
}),
headerLeft: () => ( headerLeft: () => (
<HeaderLeft <HeaderLeft
content='X' content='X'

View File

@ -5,7 +5,7 @@ import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const TabMeLists: React.FC<TabMeStackScreenProps<'Tab-Me-Lists'>> = ({ navigation }) => { const TabMeListList: React.FC<TabMeStackScreenProps<'Tab-Me-List-List'>> = ({ navigation }) => {
const { data } = useListsQuery({}) const { data } = useListsQuery({})
const { t } = useTranslation('screenTabs') const { t } = useTranslation('screenTabs')
@ -23,17 +23,17 @@ const TabMeLists: React.FC<TabMeStackScreenProps<'Tab-Me-Lists'>> = ({ navigatio
return ( return (
<MenuContainer> <MenuContainer>
{data?.map((d: Mastodon.List, i: number) => ( {data?.map((params, index) => (
<MenuRow <MenuRow
key={i} key={index}
iconFront='List' iconFront='List'
iconBack='ChevronRight' iconBack='ChevronRight'
title={d.title} title={params.title}
onPress={() => navigation.navigate('Tab-Me-List', d)} onPress={() => navigation.navigate('Tab-Me-List', params)}
/> />
))} ))}
</MenuContainer> </MenuContainer>
) )
} }
export default TabMeLists export default TabMeListList

View File

@ -9,9 +9,9 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native'
import { useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
import { menuListAccounts, menuListDelete, menuListEdit } from './menus'
const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
navigation, navigation,
@ -40,6 +40,10 @@ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
}) })
useEffect(() => { useEffect(() => {
const listAccounts = menuListAccounts({ params })
const listEdit = menuListEdit({ params, key })
const listDelete = menuListDelete({ params, mutation })
navigation.setOptions({ navigation.setOptions({
headerRight: () => ( headerRight: () => (
<DropdownMenu.Root> <DropdownMenu.Root>
@ -52,41 +56,24 @@ const TabMeList: React.FC<TabMeStackScreenProps<'Tab-Me-List'>> = ({
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Content> <DropdownMenu.Content>
<DropdownMenu.Item <DropdownMenu.Group>
key='list-edit' <DropdownMenu.Item key={listAccounts.key} onSelect={listAccounts.onSelect}>
onSelect={() => <DropdownMenu.ItemTitle children={listAccounts.title} />
navigation.navigate('Tab-Me-List-Edit', { <DropdownMenu.ItemIcon iosIconName={listAccounts.icon} />
type: 'edit', </DropdownMenu.Item>
payload: params, </DropdownMenu.Group>
key
})
}
>
<DropdownMenu.ItemTitle children={t('me.stacks.listEdit.name')} />
<DropdownMenu.ItemIcon iosIconName='square.and.pencil' />
</DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Group>
key='list-delete' <DropdownMenu.Item key={listEdit.key} onSelect={listEdit.onSelect}>
destructive <DropdownMenu.ItemTitle children={listEdit.title} />
onSelect={() => <DropdownMenu.ItemIcon iosIconName={listEdit.icon} />
Alert.alert( </DropdownMenu.Item>
t('me.listDelete.confirm.title', { list: params.title.slice(0, 6) }),
t('me.listDelete.confirm.message'), <DropdownMenu.Item key={listDelete.key} destructive onSelect={listDelete.onSelect}>
[ <DropdownMenu.ItemTitle children={listDelete.title} />
{ <DropdownMenu.ItemIcon iosIconName={listDelete.icon} />
text: t('common:buttons.delete'), </DropdownMenu.Item>
style: 'destructive', </DropdownMenu.Group>
onPress: () => mutation.mutate({ type: 'delete', payload: params })
},
{ text: t('common:buttons.cancel') }
]
)
}
>
<DropdownMenu.ItemTitle children={t('me.listDelete.heading')} />
<DropdownMenu.ItemIcon iosIconName='trash' />
</DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
) )

View File

@ -0,0 +1,48 @@
import navigationRef from '@helpers/navigationRef'
import i18next from 'i18next'
import { Alert } from 'react-native'
import { UseMutationResult } from 'react-query'
export const menuListAccounts = ({ params }: { params: Mastodon.List }) => ({
key: 'list-accounts',
onSelect: () => navigationRef.navigate<any>('Tab-Me-List-Accounts', params),
title: i18next.t('screenTabs:me.listAccounts.heading'),
icon: 'person.crop.circle.fill.badge.checkmark'
})
export const menuListEdit = ({ params, key }: { params: Mastodon.List; key: string }) => ({
key: 'list-edit',
onSelect: () =>
navigationRef.navigate<any>('Tab-Me-List-Edit', {
type: 'edit',
payload: params,
key
}),
title: i18next.t('screenTabs:me.listEdit.heading'),
icon: 'square.and.pencil'
})
export const menuListDelete = ({
params,
mutation
}: {
params: Mastodon.List
mutation: UseMutationResult<any, any, unknown, unknown>
}) => ({
key: 'list-delete',
onSelect: () =>
Alert.alert(
i18next.t('screenTabs:me.listDelete.confirm.title', { list: params.title.slice(0, 6) }),
i18next.t('screenTabs:me.listDelete.confirm.message'),
[
{
text: i18next.t('common:buttons.delete'),
style: 'destructive',
onPress: () => mutation.mutate({ type: 'delete', payload: params })
},
{ text: i18next.t('common:buttons.cancel') }
]
),
title: i18next.t('screenTabs:me.listDelete.heading'),
icon: 'trash'
})

View File

@ -85,7 +85,7 @@ const Collections: React.FC = () => {
iconFront='List' iconFront='List'
iconBack='ChevronRight' iconBack='ChevronRight'
title={t('me.stacks.lists.name')} title={t('me.stacks.lists.name')}
onPress={() => navigation.navigate('Tab-Me-Lists')} onPress={() => navigation.navigate('Tab-Me-List-List')}
/> />
) : null} ) : null}
{mePage.announcements.shown ? ( {mePage.announcements.shown ? (

View File

@ -139,6 +139,7 @@ export type TabMeStackParamList = {
'Tab-Me-Conversations': undefined 'Tab-Me-Conversations': undefined
'Tab-Me-Favourites': undefined 'Tab-Me-Favourites': undefined
'Tab-Me-List': Mastodon.List 'Tab-Me-List': Mastodon.List
'Tab-Me-List-Accounts': Omit<Mastodon.List, 'replies_policy'>
'Tab-Me-List-Edit': 'Tab-Me-List-Edit':
| { | {
type: 'add' type: 'add'
@ -148,7 +149,7 @@ export type TabMeStackParamList = {
payload: Mastodon.List payload: Mastodon.List
key: string // To update title after successful mutation key: string // To update title after successful mutation
} }
'Tab-Me-Lists': undefined 'Tab-Me-List-List': undefined
'Tab-Me-Profile': undefined 'Tab-Me-Profile': undefined
'Tab-Me-Push': undefined 'Tab-Me-Push': undefined
'Tab-Me-Settings': undefined 'Tab-Me-Settings': undefined

View File

@ -1,6 +1,14 @@
import apiInstance from '@api/instance' import apiInstance, { InstanceResponse } from '@api/instance'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useMutation, UseMutationOptions, useQuery, UseQueryOptions } from 'react-query' import {
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from 'react-query'
export type QueryKeyLists = ['Lists'] export type QueryKeyLists = ['Lists']
@ -53,10 +61,10 @@ const mutationFunction = async (params: MutationVarsLists) => {
body body
}).then(res => res.body) }).then(res => res.body)
case 'delete': case 'delete':
return apiInstance({ return apiInstance<{}>({
method: 'delete', method: 'delete',
url: `lists/${params.payload.id}` url: `lists/${params.payload.id}`
}) }).then(res => res.body)
} }
} }
@ -66,4 +74,54 @@ const useListsMutation = (
return useMutation(mutationFunction, options) return useMutation(mutationFunction, options)
} }
export { useListsQuery, useListsMutation } /* ----- */
export type QueryKeyListAccounts = ['ListAccounts', { id: Mastodon.List['id'] }]
const accountsQueryFunction = async ({
queryKey,
pageParam
}: QueryFunctionContext<QueryKeyListAccounts>) => {
const { id } = queryKey[1]
return await apiInstance<Mastodon.Account[]>({
method: 'get',
url: `lists/${id}/accounts`,
params: { ...pageParam, limit: 40 }
})
}
const useListAccountsQuery = ({
options,
...queryKeyParams
}: QueryKeyListAccounts[1] & {
options?: UseInfiniteQueryOptions<InstanceResponse<Mastodon.Account[]>, AxiosError>
}) => {
const queryKey: QueryKeyListAccounts = ['ListAccounts', queryKeyParams]
return useInfiniteQuery(queryKey, accountsQueryFunction, options)
}
type AccountsMutationVarsLists = {
type: 'add' | 'delete'
payload: Pick<Mastodon.List, 'id'> & { accounts: Mastodon.Account['id'][] }
}
const accountsMutationFunction = async (params: AccountsMutationVarsLists) => {
const body = new FormData()
for (const account of params.payload.accounts) {
body.append('account_ids[]', account)
}
return apiInstance<{}>({
method: params.type === 'add' ? 'post' : 'delete',
url: `lists/${params.payload.id}/accounts`,
body
}).then(res => res.body)
}
const useListAccountsMutation = (
options: UseMutationOptions<Mastodon.List, AxiosError, AccountsMutationVarsLists>
) => {
return useMutation(accountsMutationFunction, options)
}
export { useListsQuery, useListsMutation, useListAccountsQuery, useListAccountsMutation }