1
0
mirror of https://github.com/tooot-app/app synced 2025-02-27 09:07:51 +01:00
This commit is contained in:
xmflsct 2022-12-03 15:50:15 +01:00
parent a3a0bf523f
commit 29fd36a581
14 changed files with 223 additions and 57 deletions

View File

@ -1,5 +1,5 @@
import React from 'react' import React from 'react'
import { StyleSheet, View } from 'react-native' import { View } from 'react-native'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
export interface Props { export interface Props {
@ -7,14 +7,16 @@ export interface Props {
} }
const MenuContainer: React.FC<Props> = ({ children }) => { const MenuContainer: React.FC<Props> = ({ children }) => {
return <View style={styles.base}>{children}</View> return (
<View
style={{
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}}
>
{children}
</View>
)
} }
const styles = StyleSheet.create({
base: {
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
marginBottom: StyleConstants.Spacing.Global.PagePadding
}
})
export default MenuContainer export default MenuContainer

View File

@ -5,7 +5,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { ColorDefinitions } from '@utils/styles/themes' import { ColorDefinitions } from '@utils/styles/themes'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { View } from 'react-native' import { Text, View } from 'react-native'
import { Flow } from 'react-native-animated-spinkit' import { Flow } from 'react-native-animated-spinkit'
import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler'
@ -65,6 +65,7 @@ const MenuRow: React.FC<Props> = ({
> >
<TapGestureHandler <TapGestureHandler
onHandlerStateChange={async ({ nativeEvent }) => { onHandlerStateChange={async ({ nativeEvent }) => {
if (typeof iconBack !== 'string') return // Let icon back handles the gesture
if (nativeEvent.state === State.ACTIVE && !loading) { if (nativeEvent.state === State.ACTIVE && !loading) {
if (screenReaderEnabled && switchOnValueChange) { if (screenReaderEnabled && switchOnValueChange) {
switchOnValueChange() switchOnValueChange()
@ -79,12 +80,13 @@ const MenuRow: React.FC<Props> = ({
style={{ style={{
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
paddingTop: StyleConstants.Spacing.S justifyContent: 'space-between',
marginTop: StyleConstants.Spacing.S
}} }}
> >
<View <View
style={{ style={{
flexGrow: 3, flex: 3,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}} }}

View File

@ -28,8 +28,9 @@ const TimelineHeaderAndroid: React.FC<Props> = ({ queryKey, rootQueryKey, status
url: status.url || status.uri url: status.url || status.uri
}) })
const mAccount = menuAccount({ const mAccount = menuAccount({
type: 'status',
openChange, openChange,
id: status.account.id, account: status.account,
queryKey queryKey
}) })
const mStatus = menuStatus({ status, queryKey, rootQueryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey })

View File

@ -45,8 +45,9 @@ const TimelineHeaderDefault: React.FC<Props> = ({
copiableContent copiableContent
}) })
const mAccount = menuAccount({ const mAccount = menuAccount({
type: 'status',
openChange, openChange,
id: status.account.id, account: status.account,
queryKey queryKey
}) })
const mStatus = menuStatus({ status, queryKey, rootQueryKey }) const mStatus = menuStatus({ status, queryKey, rootQueryKey })

View File

@ -31,8 +31,9 @@ const TimelineHeaderNotification = ({ queryKey, notification }: Props) => {
url: notification.status?.url || notification.status?.uri url: notification.status?.url || notification.status?.uri
}) })
const mAccount = menuAccount({ const mAccount = menuAccount({
type: 'status',
openChange, openChange,
id: notification.status?.account.id, account: notification.status?.account,
queryKey queryKey
}) })
const mStatus = menuStatus({ status: notification.status, queryKey }) const mStatus = menuStatus({ status: notification.status, queryKey })

View File

@ -1,5 +1,8 @@
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { displayMessage } from '@components/Message' 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 { import {
QueryKeyRelationship, QueryKeyRelationship,
useRelationshipMutation, useRelationshipMutation,
@ -19,25 +22,29 @@ import { useQueryClient } from 'react-query'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
const menuAccount = ({ const menuAccount = ({
type,
openChange, openChange,
id, account,
queryKey, queryKey,
rootQueryKey rootQueryKey
}: { }: {
type: 'status' | 'account' // Where the action is coming from
openChange: boolean openChange: boolean
id?: Mastodon.Account['id'] account?: Pick<Mastodon.Account, 'id' | 'username'>
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
rootQueryKey?: QueryKeyTimeline rootQueryKey?: QueryKeyTimeline
}): ContextMenu[][] => { }): ContextMenu[][] => {
if (!id) return [] if (!account) return []
const navigation =
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation('componentContextMenu') const { t } = useTranslation('componentContextMenu')
const menus: ContextMenu[][] = [[]] const menus: ContextMenu[][] = [[]]
const instanceAccount = useSelector(getInstanceAccount, (prev, next) => prev.id === next.id) 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) const [enabled, setEnabled] = useState(openChange)
useEffect(() => { useEffect(() => {
@ -45,12 +52,12 @@ const menuAccount = ({
setEnabled(true) setEnabled(true)
} }
}, [openChange, enabled]) }, [openChange, enabled])
const { data, isFetching } = useRelationshipQuery({ id, options: { enabled } }) const { data, isFetching } = useRelationshipQuery({ id: account.id, options: { enabled } })
const queryClient = useQueryClient() const queryClient = useQueryClient()
const timelineMutation = useTimelineMutation({ const timelineMutation = useTimelineMutation({
onSuccess: (_, params) => { onSuccess: (_, params) => {
queryClient.refetchQueries(['Relationship', { id }]) queryClient.refetchQueries(['Relationship', { id: account.id }])
const theParams = params as MutationVarsTimelineUpdateAccountProperty const theParams = params as MutationVarsTimelineUpdateAccountProperty
displayMessage({ displayMessage({
theme, theme,
@ -90,7 +97,7 @@ const menuAccount = ({
rootQueryKey && queryClient.invalidateQueries(rootQueryKey) rootQueryKey && queryClient.invalidateQueries(rootQueryKey)
} }
}) })
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id: account.id }]
const relationshipMutation = useRelationshipMutation({ const relationshipMutation = useRelationshipMutation({
onSuccess: (res, { payload: { action } }) => { onSuccess: (res, { payload: { action } }) => {
haptics('Success') haptics('Success')
@ -118,14 +125,14 @@ const menuAccount = ({
} }
}) })
if (!ownAccount && Platform.OS !== 'android') { if (!ownAccount && Platform.OS !== 'android' && type !== 'account') {
menus[0].push({ menus[0].push({
key: 'account-following', key: 'account-following',
item: { item: {
onSelect: () => onSelect: () =>
data && data &&
relationshipMutation.mutate({ relationshipMutation.mutate({
id, id: account.id,
type: 'outgoing', type: 'outgoing',
payload: { action: 'follow', state: !data?.requested ? data.following : true } payload: { action: 'follow', state: !data?.requested ? data.following : true }
}), }),
@ -146,6 +153,17 @@ const menuAccount = ({
}) })
} }
if (!ownAccount) { 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({ menus[0].push({
key: 'account-mute', key: 'account-mute',
item: { item: {
@ -153,7 +171,7 @@ const menuAccount = ({
timelineMutation.mutate({ timelineMutation.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',
queryKey, queryKey,
id, id: account.id,
payload: { property: 'mute', currentValue: data?.muting } payload: { property: 'mute', currentValue: data?.muting }
}), }),
disabled: Platform.OS !== 'android' ? !data || isFetching : false, disabled: Platform.OS !== 'android' ? !data || isFetching : false,
@ -176,7 +194,7 @@ const menuAccount = ({
timelineMutation.mutate({ timelineMutation.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',
queryKey, queryKey,
id, id: account.id,
payload: { property: 'block', currentValue: data?.blocking } payload: { property: 'block', currentValue: data?.blocking }
}), }),
disabled: Platform.OS !== 'android' ? !data || isFetching : false, disabled: Platform.OS !== 'android' ? !data || isFetching : false,
@ -195,13 +213,13 @@ const menuAccount = ({
timelineMutation.mutate({ timelineMutation.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',
queryKey, queryKey,
id, id: account.id,
payload: { property: 'reports' } payload: { property: 'reports' }
}) })
timelineMutation.mutate({ timelineMutation.mutate({
type: 'updateAccountProperty', type: 'updateAccountProperty',
queryKey, queryKey,
id, id: account.id,
payload: { property: 'block', currentValue: false } payload: { property: 'block', currentValue: false }
}) })
}, },

View File

@ -5,7 +5,8 @@
"cancel": "Cancel", "cancel": "Cancel",
"discard": "Discard", "discard": "Discard",
"continue": "Continue", "continue": "Continue",
"delete": "Delete" "delete": "Delete",
"done": "Done"
}, },
"customEmoji": { "customEmoji": {
"accessibilityLabel": "Custom emoji {{emoji}}" "accessibilityLabel": "Custom emoji {{emoji}}"

View File

@ -6,6 +6,7 @@
"action_false": "Follow user", "action_false": "Follow user",
"action_true": "Unfollow user" "action_true": "Unfollow user"
}, },
"inLists": "Manage user of lists",
"mute": { "mute": {
"action_false": "Mute user", "action_false": "Mute user",
"action_true": "Unmute user" "action_true": "Unmute user"

View File

@ -320,6 +320,11 @@
}, },
"suspended": "Account suspended by the moderators of your server" "suspended": "Account suspended by the moderators of your server"
}, },
"accountInLists": {
"name": "Lists of @{{username}}",
"inLists": "In lists",
"notInLists": "Other lists"
},
"attachments": { "attachments": {
"name": "<0 /><1>\"s media</1>" "name": "<0 /><1>\"s media</1>"
}, },

View File

@ -11,7 +11,7 @@ import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useMemo, useRef, useState } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' 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 { useSharedValue } from 'react-native-reanimated'
import { useIsFetching } from 'react-query' import { useIsFetching } from 'react-query'
import * as DropdownMenu from 'zeego/dropdown-menu' import * as DropdownMenu from 'zeego/dropdown-menu'
@ -30,21 +30,10 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
const { colors, mode } = useTheme() const { colors, mode } = useTheme()
const mShare = menuShare({ type: 'account', url: account.url }) 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(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerRight: () => { headerRight: () => {
// const shareOnPress = contextMenuShare({
// actions,
// type: 'account',
// url: account.url
// })
// const accountOnPress = contextMenuAccount({
// actions,
// type: 'account',
// id: account.id
// })
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger> <DropdownMenu.Trigger>
@ -107,7 +96,7 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
const ListHeaderComponent = useMemo(() => { const ListHeaderComponent = useMemo(() => {
return ( return (
<> <>
<View style={[styles.header, { borderBottomColor: colors.border }]}> <View style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}>
<AccountHeader account={data} /> <AccountHeader account={data} />
<AccountInformation account={data} /> <AccountInformation account={data} />
{!data?.suspended && fetchedTimeline.current ? ( {!data?.suspended && fetchedTimeline.current ? (
@ -129,7 +118,10 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
break break
} }
}} }}
style={styles.segmentsContainer} style={{
marginTop: StyleConstants.Spacing.M,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
/> />
) : null} ) : null}
{data?.suspended ? ( {data?.suspended ? (
@ -178,14 +170,4 @@ const TabSharedAccount: React.FC<TabSharedStackScreenProps<'Tab-Shared-Account'>
) )
} }
const styles = StyleSheet.create({
header: {
borderBottomWidth: 1
},
segmentsContainer: {
marginTop: StyleConstants.Spacing.M,
marginHorizontal: StyleConstants.Spacing.Global.PagePadding
}
})
export default TabSharedAccount export default TabSharedAccount

View File

@ -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 (
<HeaderRight
type='text'
content={t('common:buttons.done')}
onPress={() => 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 (
<SectionList
style={{ padding: StyleConstants.Spacing.Global.PagePadding }}
sections={sections}
SectionSeparatorComponent={props => {
return props.leadingItem && props.trailingSection ? (
<View style={{ flex: 1, height: StyleConstants.Spacing.Global.PagePadding * 2 }} />
) : null
}}
renderSectionHeader={({ section: { title, data } }) =>
data.length ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }} children={title} />
) : null
}
renderItem={({ index, item, section }) => (
<MenuRow
key={index}
iconFront='List'
content={
<Button
type='icon'
content={section.id === 'in' ? 'Minus' : 'Plus'}
round
disabled={accountInListsQuery.isFetching}
onPress={() => {
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}
/>
)}
></SectionList>
)
}
export default TabSharedAccountInLists

View File

@ -16,6 +16,7 @@ import { debounce } from 'lodash'
import React from 'react' import React from 'react'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { Platform, TextInput, View } from 'react-native' import { Platform, TextInput, View } from 'react-native'
import TabSharedAccountInLists from './AccountInLists'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => { const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
const { colors, mode } = useTheme() const { colors, mode } = useTheme()
@ -48,6 +49,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNaviga
}} }}
/> />
<Stack.Screen
key='Tab-Shared-Account-In-Lists'
name='Tab-Shared-Account-In-Lists'
component={TabSharedAccountInLists}
/>
<Stack.Screen <Stack.Screen
key='Tab-Shared-Attachments' key='Tab-Shared-Attachments'
name='Tab-Shared-Attachments' name='Tab-Shared-Attachments'

View File

@ -92,6 +92,9 @@ export type TabSharedStackParamList = {
'Tab-Shared-Account': { 'Tab-Shared-Account': {
account: Mastodon.Account | Mastodon.Mention account: Mastodon.Account | Mastodon.Mention
} }
'Tab-Shared-Account-In-Lists': {
account: Pick<Mastodon.Account, 'id' | 'username'>
}
'Tab-Shared-Attachments': { account: Mastodon.Account } 'Tab-Shared-Attachments': { account: Mastodon.Account }
'Tab-Shared-Hashtag': { 'Tab-Shared-Hashtag': {
hashtag: Mastodon.Tag['name'] hashtag: Mastodon.Tag['name']

View File

@ -4,7 +4,7 @@ import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query'
export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }] export type QueryKeyAccount = ['Account', { id: Mastodon.Account['id'] }]
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => { const accountQueryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyAccount>) => {
const { id } = queryKey[1] const { id } = queryKey[1]
return apiInstance<Mastodon.Account>({ return apiInstance<Mastodon.Account>({
@ -20,7 +20,30 @@ const useAccountQuery = ({
options?: UseQueryOptions<Mastodon.Account, AxiosError> options?: UseQueryOptions<Mastodon.Account, AxiosError>
}) => { }) => {
const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }] 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<QueryKeyAccount>) => {
const { id } = queryKey[1]
return apiInstance<Mastodon.List[]>({
method: 'get',
url: `accounts/${id}/lists`
}).then(res => res.body)
}
const useAccountInListsQuery = ({
options,
...queryKeyParams
}: QueryKeyAccount[1] & {
options?: UseQueryOptions<Mastodon.List[], AxiosError>
}) => {
const queryKey: QueryKeyAccount = ['Account', { ...queryKeyParams }]
return useQuery(queryKey, accountInListsQueryFunction, options)
}
export { useAccountQuery, useAccountInListsQuery }