This commit is contained in:
xmflsct 2023-01-26 23:07:13 +01:00
parent e8eb62e2d0
commit 653b588c29
18 changed files with 555 additions and 207 deletions

114
src/components/Filter.tsx Normal file
View File

@ -0,0 +1,114 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Fragment } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { View } from 'react-native'
import { TouchableNativeFeedback } from 'react-native-gesture-handler'
import Icon from './Icon'
import CustomText from './Text'
export type Props = {
onPress: () => void
filter: Mastodon.Filter<'v2'>
button?: React.ReactNode
}
export const Filter: React.FC<Props> = ({ onPress, filter, button }) => {
const { t } = useTranslation(['common', 'screenTabs'])
const { colors } = useTheme()
return (
<TouchableNativeFeedback onPress={onPress}>
<View
style={{
paddingVertical: StyleConstants.Spacing.S,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundDefault
}}
>
<View style={{ flex: 1 }}>
<CustomText
fontStyle='M'
children={filter.title}
style={{ color: colors.primaryDefault }}
numberOfLines={1}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginVertical: StyleConstants.Spacing.XS
}}
>
{filter.expires_at && new Date() > new Date(filter.expires_at) ? (
<CustomText
fontStyle='S'
fontWeight='Bold'
children={t('screenTabs:me.preferencesFilters.expired')}
style={{ color: colors.red, marginRight: StyleConstants.Spacing.M }}
/>
) : null}
{filter.keywords?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.keywords', {
count: filter.keywords.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.keywords?.length && filter.statuses?.length ? (
<CustomText
children={t('common:separator')}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.statuses?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.statuses', {
count: filter.statuses.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
</View>
<CustomText
style={{ color: colors.secondary }}
children={
<Trans
ns='screenTabs'
i18nKey='me.preferencesFilters.context'
components={[
<>
{filter.context.map((c, index) => (
<Fragment key={index}>
<CustomText
style={{
color: colors.secondary,
textDecorationColor: colors.disabled,
textDecorationLine: 'underline',
textDecorationStyle: 'solid'
}}
children={t(`screenTabs:me.preferencesFilters.contexts.${c}`)}
/>
<CustomText children={t('common:separator')} />
</Fragment>
))}
</>
]}
/>
}
/>
</View>
{button || (
<Icon
name='chevron-right'
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
style={{ marginLeft: 8 }}
/>
)}
</View>
</TouchableNativeFeedback>
)
}

View File

@ -22,7 +22,7 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
const onPress = () => {
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
navigation.push('Tab-Shared-Hashtag', { tag_name: hashtag.name })
}
const padding = StyleConstants.Spacing.Global.PagePadding

View File

@ -69,7 +69,7 @@ const ParseHTML: React.FC<Props> = ({
const [followedTags] = useAccountStorage.object('followed_tags')
const MAX_ALLOWED_LINES = 30
const MAX_ALLOWED_LINES = 35
const [totalLines, setTotalLines] = useState<number>()
const [expanded, setExpanded] = useState(highlighted)
@ -147,7 +147,7 @@ const ParseHTML: React.FC<Props> = ({
tag?.length &&
!disableDetails &&
!sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
navigation.push('Tab-Shared-Hashtag', { tag_name: tag })
}
children={children}
/>
@ -203,9 +203,7 @@ const ParseHTML: React.FC<Props> = ({
onPress={async () => {
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
hashtag: content.substring(1)
})
navigation.push('Tab-Shared-Hashtag', { tag_name: content.substring(1) })
} else {
await openLink(href, navigation)
}

View File

@ -137,8 +137,8 @@ const TimelinePoll: React.FC = () => {
marginRight: StyleConstants.Spacing.S
}}
name={
`${poll.own_votes?.includes(index) ? 'Check' : ''}${
poll.multiple ? 'Square' : 'Circle'
`${poll.own_votes?.includes(index) ? 'check-' : ''}${
poll.multiple ? 'square' : 'circle'
}` as any
}
size={StyleConstants.Font.Size.M}

View File

@ -0,0 +1,97 @@
import haptics from '@components/haptics'
import { displayMessage } from '@components/Message'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackParamList } from '@utils/navigation/navigators'
import { QueryKeyFollowedTags, useTagMutation, useTagQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { useTranslation } from 'react-i18next'
const menuHashtag = ({
tag_name,
queryKey
}: {
tag_name: Mastodon.Tag['name']
queryKey?: QueryKeyTimeline
}): ContextMenu => {
const navigation =
useNavigation<NativeStackNavigationProp<TabSharedStackParamList, any, undefined>>()
const { t } = useTranslation(['common', 'componentContextMenu'])
const canFollowTags = featureCheck('follow_tags')
const queryClient = useQueryClient()
const {
data,
isFetching: tagIsFetching,
refetch: tagRefetch
} = useTagQuery({ tag_name, options: { enabled: canFollowTags } })
const tagMutation = useTagMutation({
onSuccess: () => {
haptics('Light')
tagRefetch()
const queryKeyFollowedTags: QueryKeyFollowedTags = ['FollowedTags']
queryClient.invalidateQueries({ queryKey: queryKeyFollowedTags })
},
onError: (err: any, { to }) => {
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: t('componentContextMenu:hashtag.follow', {
defaultValue: 'false',
context: to.toString()
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
const menus: ContextMenu = []
menus.push([
{
type: 'item',
key: 'hashtag-follow',
props: {
onSelect: () =>
typeof data?.following === 'boolean' &&
tagMutation.mutate({ tag_name, to: !data.following }),
disabled: tagIsFetching,
destructive: false,
hidden: !canFollowTags
},
title: t('componentContextMenu:hashtag.follow.action', {
defaultValue: 'false',
context: (data?.following || false).toString()
}),
icon: data?.following ? 'rectangle.stack.badge.minus' : 'rectangle.stack.badge.plus'
},
{
type: 'item',
key: 'hashtag-filter',
props: {
onSelect: () =>
navigation.navigate('Tab-Shared-Filter', { source: 'hashtag', tag_name, queryKey }),
disabled: false,
destructive: false,
hidden: !canFollowTags
},
title: t('componentContextMenu:hashtag.filter.action'),
icon: 'line.3.horizontal.decrease'
}
])
return menus
}
export default menuHashtag

View File

@ -231,6 +231,22 @@ const menuStatus = ({
context: (status.pinned || false).toString()
}),
icon: status.pinned ? 'pin.slash' : 'pin'
},
{
type: 'item',
key: 'status-filter',
props: {
// @ts-ignore
onSelect: () => navigation.navigate('Tab-Shared-Filter', { source: 'status', status, queryKey }),
disabled: false,
destructive: false,
hidden: !('filtered' in status)
},
title: t('componentContextMenu:status.filter.action', {
defaultValue: 'false',
context: (!!status.filtered?.length).toString()
}),
icon: status.pinned ? 'rectangle.badge.checkmark' : 'rectangle.badge.xmark'
}
])

View File

@ -6,7 +6,7 @@
"action_false": "Follow user",
"action_true": "Unfollow user"
},
"inLists": "Lists containing user",
"inLists": "Lists containing user ...",
"showBoosts": {
"action_false": "Show user's boosts",
"action_true": "Hide users's boosts"
@ -16,12 +16,12 @@
"action_true": "Unmute user"
},
"followAs": {
"trigger": "Follow as...",
"trigger": "Follow as ...",
"succeed_default": "Now following @{{target}} with @{{source}}",
"succeed_locked": "Sent follow request to @{{target}} with {{source}}, pending approval",
"failed": "Follow as"
},
"blockReport": "Block and report...",
"blockReport": "Block and report",
"block": {
"action_false": "Block user",
"action_true": "Unblock user",
@ -54,6 +54,15 @@
}
}
},
"hashtag": {
"follow": {
"action_false": "Follow",
"action_true": "Unfollow",
},
"filter": {
"action": "Filter hashtag ..."
}
},
"share": {
"status": {
"action": "Share toot"
@ -88,6 +97,10 @@
"pin": {
"action_false": "Pin toot",
"action_true": "Unpin toot"
},
"filter": {
"action_false": "Filter toot ...",
"action_true": "Manage filters ..."
}
}
}

View File

@ -399,9 +399,9 @@
"attachments": {
"name": "<0 /><1>'s media</1>"
},
"hashtag": {
"follow": "Follow",
"unfollow": "Unfollow"
"filter": {
"name": "Add to filter",
"existed": "Existed in these filters"
},
"history": {
"name": "Edit History"

View File

@ -4,7 +4,7 @@ import ComponentHashtag from '@components/Hashtag'
import { displayMessage } from '@components/Message'
import ComponentSeparator from '@components/Separator'
import { TabMeStackScreenProps } from '@utils/navigation/navigators'
import { useFollowedTagsQuery, useTagsMutation } from '@utils/queryHooks/tags'
import { useFollowedTagsQuery, useTagMutation } from '@utils/queryHooks/tags'
import { flattenPages } from '@utils/queryHooks/utils'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -13,7 +13,7 @@ import { FlatList } from 'react-native-gesture-handler'
const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>> = ({
navigation
}) => {
const { t } = useTranslation(['common', 'screenTabs'])
const { t } = useTranslation(['common', 'screenTabs', 'componentContextMenu'])
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
const flattenData = flattenPages(data)
@ -24,7 +24,7 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
}
}, [flattenData.length])
const mutation = useTagsMutation({
const mutation = useTagMutation({
onSuccess: () => {
haptics('Light')
refetch()
@ -33,9 +33,10 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: to
? t('screenTabs:shared.hashtag.follow')
: t('screenTabs:shared.hashtag.unfollow')
function: t('componentContextMenu:hashtag.follow.action', {
defaultValue: 'false',
context: to.toString()
})
}),
...(err.status &&
typeof err.status === 'number' &&
@ -58,8 +59,11 @@ const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>>
children={
<Button
type='text'
content={t('screenTabs:shared.hashtag.unfollow')}
onPress={() => mutation.mutate({ tag: item.name, to: !item.following })}
content={t('componentContextMenu:hashtag.follow.action', {
defaultValue: 'fase',
context: 'false'
})}
onPress={() => mutation.mutate({ tag_name: item.name, to: !item.following })}
/>
}
/>

View File

@ -1,15 +1,15 @@
import { Filter } from '@components/Filter'
import { HeaderLeft, HeaderRight } from '@components/Header'
import Icon from '@components/Icon'
import ComponentSeparator from '@components/Separator'
import CustomText from '@components/Text'
import apiInstance from '@utils/api/instance'
import { TabMePreferencesStackScreenProps } from '@utils/navigation/navigators'
import { useFiltersQuery } from '@utils/queryHooks/filters'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { Fragment, useEffect } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Pressable, TouchableNativeFeedback, View } from 'react-native'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, View } from 'react-native'
import { SwipeListView } from 'react-native-swipe-list-view'
const TabMePreferencesFilters: React.FC<
@ -39,6 +39,7 @@ const TabMePreferencesFilters: React.FC<
return (
<SwipeListView
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
renderHiddenItem={({ item }) => (
<Pressable
style={{
@ -65,98 +66,10 @@ const TabMePreferencesFilters: React.FC<
filter.expires_at ? new Date().getTime() - new Date(filter.expires_at).getTime() : 1
)}
renderItem={({ item: filter }) => (
<TouchableNativeFeedback
<Filter
filter={filter}
onPress={() => navigation.navigate('Tab-Me-Preferences-Filter', { type: 'edit', filter })}
>
<View
style={{
padding: StyleConstants.Spacing.Global.PagePadding,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.backgroundDefault
}}
>
<View style={{ flex: 1 }}>
<CustomText
fontStyle='M'
children={filter.title}
style={{ color: colors.primaryDefault }}
numberOfLines={1}
/>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginVertical: StyleConstants.Spacing.XS
}}
>
{filter.expires_at && new Date() > new Date(filter.expires_at) ? (
<CustomText
fontStyle='S'
fontWeight='Bold'
children={t('screenTabs:me.preferencesFilters.expired')}
style={{ color: colors.red, marginRight: StyleConstants.Spacing.M }}
/>
) : null}
{filter.keywords?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.keywords', {
count: filter.keywords.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.keywords?.length && filter.statuses?.length ? (
<CustomText
children={t('common:separator')}
style={{ color: colors.primaryDefault }}
/>
) : null}
{filter.statuses?.length ? (
<CustomText
children={t('screenTabs:me.preferencesFilters.statuses', {
count: filter.statuses.length
})}
style={{ color: colors.primaryDefault }}
/>
) : null}
</View>
<CustomText
style={{ color: colors.secondary }}
children={
<Trans
ns='screenTabs'
i18nKey='me.preferencesFilters.context'
components={[
<>
{filter.context.map((c, index) => (
<Fragment key={index}>
<CustomText
style={{
color: colors.secondary,
textDecorationColor: colors.disabled,
textDecorationLine: 'underline',
textDecorationStyle: 'solid'
}}
children={t(`screenTabs:me.preferencesFilters.contexts.${c}`)}
/>
<CustomText children={t('common:separator')} />
</Fragment>
))}
</>
]}
/>
}
/>
</View>
<Icon
name='chevron-right'
size={StyleConstants.Font.Size.L}
color={colors.primaryDefault}
style={{ marginLeft: 8 }}
/>
</View>
</TouchableNativeFeedback>
/>
)}
ItemSeparatorComponent={ComponentSeparator}
/>

View File

@ -27,15 +27,13 @@ const TabSharedAccountInLists: React.FC<
navigation.setOptions({
presentation: 'modal',
title: t('screenTabs:shared.accountInLists.name', { username: account.username }),
headerRight: () => {
return (
<HeaderRight
type='text'
content={t('common:buttons.done')}
onPress={() => navigation.pop(1)}
/>
)
}
headerRight: () => (
<HeaderRight
type='text'
content={t('common:buttons.done')}
onPress={() => navigation.pop(1)}
/>
)
})
}, [])
@ -66,11 +64,11 @@ const TabSharedAccountInLists: React.FC<
<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 }} />
SectionSeparatorComponent={props =>
props.leadingItem && props.trailingSection ? (
<View style={{ height: StyleConstants.Spacing.XL }} />
) : null
}}
}
renderSectionHeader={({ section: { title, data } }) =>
data.length ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }} children={title} />
@ -117,7 +115,7 @@ const TabSharedAccountInLists: React.FC<
title={item.title}
/>
)}
></SectionList>
/>
)
}

View File

@ -0,0 +1,132 @@
import Button from '@components/Button'
import { Filter } from '@components/Filter'
import { HeaderRight } from '@components/Header'
import Hr from '@components/Hr'
import CustomText from '@components/Text'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps, useNavState } from '@utils/navigation/navigators'
import { useFilterMutation, useFiltersQuery } from '@utils/queryHooks/filters'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { SectionList, View } from 'react-native'
const TabSharedFilter: React.FC<TabSharedStackScreenProps<'Tab-Shared-Filter'>> = ({
navigation,
route: { params }
}) => {
if (!featureCheck('filter_server_side')) {
navigation.goBack()
return null
}
const { colors } = useTheme()
const { t } = useTranslation(['common', 'screenTabs'])
useEffect(() => {
navigation.setOptions({
title: t('screenTabs:shared.filter.name'),
headerRight: () => (
<HeaderRight
type='text'
content={t('common:buttons.done')}
onPress={() => navigation.goBack()}
/>
)
})
}, [])
const { data, isFetching, refetch } = useFiltersQuery<'v2'>({ version: 'v2' })
const sections = [
{
id: 'add',
data:
data?.filter(filter => {
switch (params.source) {
case 'hashtag':
return !filter.keywords.find(keyword => keyword.keyword === `#${params.tag_name}`)
case 'status':
return !filter.statuses.find(({ status_id }) => status_id === params.status.id)
}
}) || []
},
{
id: 'remove',
title: t('screenTabs:shared.filter.existed'),
data:
data?.filter(filter => {
switch (params.source) {
case 'hashtag':
return !!filter.keywords.find(keyword => keyword.keyword === `#${params.tag_name}`)
case 'status':
return !!filter.statuses.find(({ status_id }) => status_id === params.status.id)
}
}) || []
}
]
const mutation = useFilterMutation()
const navState = useNavState()
return (
<SectionList
contentContainerStyle={{ padding: StyleConstants.Spacing.Global.PagePadding }}
sections={sections}
SectionSeparatorComponent={props =>
props.leadingItem && props.trailingSection ? (
<View style={{ height: StyleConstants.Spacing.XL }} />
) : null
}
renderSectionHeader={({ section: { title, data } }) =>
title && data.length ? (
<CustomText fontStyle='S' style={{ color: colors.secondary }} children={title} />
) : null
}
ItemSeparatorComponent={Hr}
renderItem={({ item, section }) => (
<Filter
filter={item}
button={
<Button
type='icon'
content={section.id === 'add' ? 'plus' : 'minus'}
round
disabled={isFetching}
onPress={() => {
if (section.id === 'add' || section.id === 'remove') {
switch (params.source) {
case 'status':
mutation
.mutateAsync({
source: 'status',
filter: item,
action: section.id,
status: params.status
})
.then(() => refetch())
break
case 'hashtag':
mutation
.mutateAsync({
source: 'keyword',
filter: item,
action: section.id,
keyword: `#${params.tag_name}`
})
.then(() => refetch())
break
}
}
}}
/>
}
onPress={() => {}}
/>
)}
/>
)
}
export default TabSharedFilter

View File

@ -1,85 +1,69 @@
import haptics from '@components/haptics'
import menuHashtag from '@components/contextMenu/hashtag'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import { useQueryClient } from '@tanstack/react-query'
import { featureCheck } from '@utils/helpers/featureCheck'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { QueryKeyFollowedTags, useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import React, { Fragment, useEffect } from 'react'
import * as DropdownMenu from 'zeego/dropdown-menu'
const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>> = ({
navigation,
route: {
params: { hashtag }
params: { tag_name }
}
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', tag_name }]
const canFollowTags = featureCheck('follow_tags')
const canFilterTag = featureCheck('filter_server_side')
const mHashtag = menuHashtag({ tag_name, queryKey })
useEffect(() => {
navigation.setOptions({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
title: `#${decodeURIComponent(hashtag)}`
title: `#${decodeURIComponent(tag_name)}`
})
navigation.setParams({ queryKey: queryKey })
}, [])
const { t } = useTranslation(['common', 'screenTabs'])
const canFollowTags = featureCheck('follow_tags')
const { data, isFetching, refetch } = useTagsQuery({
tag: hashtag,
options: { enabled: canFollowTags }
})
const queryClient = useQueryClient()
const mutation = useTagsMutation({
onSuccess: () => {
haptics('Success')
refetch()
const queryKeyFollowedTags: QueryKeyFollowedTags = ['FollowedTags']
queryClient.invalidateQueries({ queryKey: queryKeyFollowedTags })
},
onError: (err: any, { to }) => {
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: to
? t('screenTabs:shared.hashtag.follow')
: t('screenTabs:shared.hashtag.unfollow')
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
useEffect(() => {
if (!canFollowTags) return
if (!canFollowTags && !canFilterTag) return
navigation.setOptions({
headerRight: () => (
<HeaderRight
loading={isFetching || mutation.isLoading}
type='text'
content={
data?.following
? t('screenTabs:shared.hashtag.unfollow')
: t('screenTabs:shared.hashtag.follow')
}
onPress={() =>
typeof data?.following === 'boolean' &&
mutation.mutate({ tag: hashtag, to: !data.following })
}
/>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<HeaderRight content='more-horizontal' onPress={() => {}} />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
{[mHashtag].map((menu, i) => (
<Fragment key={i}>
{menu.map((group, index) => (
<DropdownMenu.Group key={index}>
{group.map(item => {
switch (item.type) {
case 'item':
return (
<DropdownMenu.Item key={item.key} {...item.props}>
<DropdownMenu.ItemTitle children={item.title} />
{item.icon ? (
<DropdownMenu.ItemIcon ios={{ name: item.icon }} />
) : null}
</DropdownMenu.Item>
)
}
})}
</DropdownMenu.Group>
))}
</Fragment>
))}
</DropdownMenu.Content>
</DropdownMenu.Root>
)
})
}, [canFollowTags, data?.following, isFetching])
}, [mHashtag])
return <Timeline queryKey={queryKey} />
}

View File

@ -9,6 +9,7 @@ import TabSharedSearch from '@screens/Tabs/Shared/Search'
import TabSharedToot from '@screens/Tabs/Shared/Toot'
import TabSharedUsers from '@screens/Tabs/Shared/Users'
import React from 'react'
import TabSharedFilter from './Filter'
const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNavigator> }) => {
return (
@ -28,6 +29,12 @@ const TabShared = ({ Stack }: { Stack: ReturnType<typeof createNativeStackNaviga
name='Tab-Shared-Attachments'
component={TabSharedAttachments}
/>
<Stack.Screen
key='Tab-Shared-Filter'
name='Tab-Shared-Filter'
component={TabSharedFilter}
options={{ presentation: 'modal' }}
/>
<Stack.Screen
key='Tab-Shared-Hashtag'
name='Tab-Shared-Hashtag'

View File

@ -99,7 +99,10 @@ export type TabSharedStackParamList = {
}
'Tab-Shared-Account-In-Lists': { account: Pick<Mastodon.Account, 'id' | 'username'> }
'Tab-Shared-Attachments': { account: Mastodon.Account; queryKey?: QueryKeyTimeline }
'Tab-Shared-Hashtag': { hashtag: Mastodon.Tag['name']; queryKey?: QueryKeyTimeline }
'Tab-Shared-Filter':
| { source: 'status'; status: Mastodon.Status }
| { source: 'hashtag'; tag_name: Mastodon.Tag['name'] }
'Tab-Shared-Hashtag': { tag_name: Mastodon.Tag['name']; queryKey?: QueryKeyTimeline }
'Tab-Shared-History': { status: Mastodon.Status; detectedLanguage: string }
'Tab-Shared-Report': {
account: Pick<Mastodon.Account, 'id' | 'acct' | 'username' | 'url'>

View File

@ -1,4 +1,5 @@
import { QueryFunctionContext, useQuery, UseQueryOptions } from '@tanstack/react-query'
import haptics from '@components/haptics'
import { QueryFunctionContext, useMutation, useQuery, UseQueryOptions } from '@tanstack/react-query'
import apiInstance from '@utils/api/instance'
import { AxiosError } from 'axios'
@ -28,6 +29,73 @@ const useFilterQuery = ({
})
}
/* ----- */
type MutationVarsFilter = { filter: Mastodon.Filter<'v2'> } & (
| { source: 'status'; action: 'add'; status: Mastodon.Status }
| { source: 'status'; action: 'remove'; status: Mastodon.Status }
| { source: 'keyword'; action: 'add'; keyword: string }
| { source: 'keyword'; action: 'remove'; keyword: string }
)
const mutationFunction = async (params: MutationVarsFilter) => {
switch (params.source) {
case 'status':
switch (params.action) {
case 'add':
return apiInstance({
method: 'post',
version: 'v2',
url: `filters/${params.filter.id}/statuses`,
body: { status_id: params.status.id }
})
case 'remove':
for (const status of params.filter.statuses) {
if (status.status_id === params.status.id) {
await apiInstance({
method: 'delete',
version: 'v2',
url: `filters/statuses/${status.id}`
})
}
}
return Promise.resolve()
}
break
case 'keyword':
switch (params.action) {
case 'add':
return apiInstance({
method: 'post',
version: 'v2',
url: `filters/${params.filter.id}/keywords`,
body: { keyword: params.keyword, whole_word: true }
})
case 'remove':
for (const keyword of params.filter.keywords) {
if (keyword.keyword === params.keyword) {
await apiInstance({
method: 'delete',
version: 'v2',
url: `filters/keywords/${keyword.id}`
})
}
}
return Promise.resolve()
}
break
}
}
const useFilterMutation = () => {
return useMutation<any, AxiosError, MutationVarsFilter>(mutationFunction, {
onSuccess: () => haptics('Light'),
onError: () => haptics('Error')
})
}
/* ----- */
export type QueryKeyFilters = ['Filters', { version: 'v1' | 'v2' }]
const filtersQueryFunction = async <T extends 'v1' | 'v2' = 'v1'>({
@ -54,4 +122,4 @@ const useFiltersQuery = <T extends 'v1' | 'v2' = 'v1'>(params?: {
})
}
export { useFilterQuery, useFiltersQuery }
export { useFilterQuery, useFilterMutation, useFiltersQuery }

View File

@ -60,32 +60,33 @@ const useFollowedTagsQuery = (
)
}
export type QueryKeyTags = ['Tags', { tag: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => {
const { tag } = queryKey[1]
export type QueryKeyTag = ['Tag', { tag_name: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTag>) => {
const { tag_name } = queryKey[1]
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag}` })
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `tags/${tag_name}` })
return res.body
}
const useTagsQuery = ({
options,
...queryKeyParams
}: QueryKeyTags[1] & {
const useTagQuery = ({
tag_name,
options
}: {
tag_name: Mastodon.Tag['name']
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
}) => {
const queryKey: QueryKeyTags = ['Tags', { ...queryKeyParams }]
const queryKey: QueryKeyTag = ['Tag', { tag_name }]
return useQuery(queryKey, queryFunction, options)
}
type MutationVarsAnnouncement = { tag: string; to: boolean }
const mutationFunction = async ({ tag, to }: MutationVarsAnnouncement) => {
type MutationVarsTag = { tag_name: Mastodon.Tag['name']; to: boolean }
const mutationFunction = async ({ tag_name, to }: MutationVarsTag) => {
return apiInstance<{}>({
method: 'post',
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
url: `tags/${tag_name}/${to ? 'follow' : 'unfollow'}`
})
}
const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => {
const useTagMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsTag>) => {
return useMutation(mutationFunction, options)
}
export { useFollowedTagsQuery, useTagsQuery, useTagsMutation }
export { useFollowedTagsQuery, useTagQuery, useTagMutation }

View File

@ -33,7 +33,7 @@ export type QueryKeyTimeline = [
}
| {
page: 'Hashtag'
hashtag: Mastodon.Tag['name']
tag_name: Mastodon.Tag['name']
}
| {
page: 'List'
@ -219,7 +219,7 @@ export const queryFunctionTimeline = async ({
case 'Hashtag':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `timelines/tag/${page.hashtag}`,
url: `timelines/tag/${page.tag_name}`,
params
})