mirror of
https://github.com/tooot-app/app
synced 2025-05-09 22:42:22 +02:00
Fix #613
This commit is contained in:
parent
e8eb62e2d0
commit
653b588c29
114
src/components/Filter.tsx
Normal file
114
src/components/Filter.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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}
|
||||
|
97
src/components/contextMenu/hashtag.ts
Normal file
97
src/components/contextMenu/hashtag.ts
Normal 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
|
@ -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'
|
||||
}
|
||||
])
|
||||
|
||||
|
@ -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 ..."
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
@ -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 })}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
132
src/screens/Tabs/Shared/Filter.tsx
Normal file
132
src/screens/Tabs/Shared/Filter.tsx
Normal 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
|
@ -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} />
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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'>
|
||||
|
@ -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 }
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user