This commit is contained in:
xmflsct 2022-12-10 20:19:18 +01:00
parent 357c4039cb
commit bdbacf579e
15 changed files with 193 additions and 108 deletions

View File

@ -3,8 +3,8 @@ import { StackNavigationProp } from '@react-navigation/stack'
import { TabLocalStackParamList } from '@utils/navigation/navigators'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
import { Dimensions, Pressable } from 'react-native'
import React, { PropsWithChildren, useCallback, useState } from 'react'
import { Dimensions, Pressable, View } from 'react-native'
import Sparkline from './Sparkline'
import CustomText from './Text'
@ -13,7 +13,11 @@ export interface Props {
onPress?: () => void
}
const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress }) => {
const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({
hashtag,
onPress: customOnPress,
children
}) => {
const { colors } = useTheme()
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
@ -31,15 +35,11 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
style={{
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding
}}
onPress={customOnPress || onPress}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => setHeight(height - padding * 2 - 1)}
>
<CustomText
fontStyle='M'
@ -52,11 +52,22 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
>
#{hashtag.name}
</CustomText>
<Sparkline
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
width={width}
height={height}
/>
<View
style={{ flexDirection: 'row', alignItems: 'center' }}
onLayout={({
nativeEvent: {
layout: { height }
}
}) => setHeight(height)}
>
<Sparkline
data={hashtag.history.map(h => parseInt(h.uses)).reverse()}
width={width}
height={height}
margin={children ? StyleConstants.Spacing.S : undefined}
/>
{children}
</View>
</Pressable>
)
}

View File

@ -1,7 +1,6 @@
import { useTheme } from '@utils/styles/ThemeManager'
import { maxBy, minBy } from 'lodash'
import React from 'react'
import { Platform } from 'react-native'
import Svg, { G, Path } from 'react-native-svg'
export interface Props {
@ -69,7 +68,7 @@ const Sparkline: React.FC<Props> = ({ data, width, height, margin = 0 }) => {
const fillPoints = linePoints.concat(closePolyPoints)
return (
<Svg height={Platform.OS !== 'android' ? 'auto' : 24} width={width}>
<Svg height={height} width={width} style={{ marginRight: margin }}>
<G>
<Path d={'M' + fillPoints.join(' ')} fill={colors.blue} fillOpacity={0.1} />
<Path

View File

@ -5,6 +5,7 @@
"cancel": "Cancel",
"discard": "Discard",
"continue": "Continue",
"create": "Create",
"delete": "Delete",
"done": "Done"
},

View File

@ -40,6 +40,9 @@
"favourites": {
"name": "Favourites"
},
"followedTags": {
"name": "Followed hashtags"
},
"fontSize": {
"name": "Toot Font Size"
},
@ -53,7 +56,7 @@
"name": "Users in list: {{list}}"
},
"listAdd": {
"name": "Add a List"
"name": "Create a List"
},
"listEdit": {
"name": "Edit List Details"

View File

@ -1,12 +1,12 @@
import { HeaderCenter, HeaderLeft } from '@components/Header'
import { HeaderLeft } from '@components/Header'
import { createNativeStackNavigator } from '@react-navigation/native-stack'
import { TabMeStackParamList } from '@utils/navigation/navigators'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Platform } from 'react-native'
import TabMeBookmarks from './Me/Bookmarks'
import TabMeConversations from './Me/Cconversations'
import TabMeFavourites from './Me/Favourites'
import TabMeFollowedTags from './Me/FollowedTags'
import TabMeList from './Me/List'
import TabMeListAccounts from './Me/List/Accounts'
import TabMeListEdit from './Me/List/Edit'
@ -42,9 +42,6 @@ const TabMe = React.memo(
component={TabMeBookmarks}
options={({ navigation }: any) => ({
title: t('me.stacks.bookmarks.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.bookmarks.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -53,9 +50,6 @@ const TabMe = React.memo(
component={TabMeConversations}
options={({ navigation }: any) => ({
title: t('me.stacks.conversations.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.conversations.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -64,9 +58,14 @@ const TabMe = React.memo(
component={TabMeFavourites}
options={({ navigation }: any) => ({
title: t('me.stacks.favourites.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.favourites.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
<Stack.Screen
name='Tab-Me-FollowedTags'
component={TabMeFollowedTags}
options={({ navigation }: any) => ({
title: t('me.stacks.followedTags.name'),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -75,15 +74,6 @@ const TabMe = React.memo(
component={TabMeList}
options={({ route, navigation }: any) => ({
title: t('me.stacks.list.name', { list: route.params.title }),
...(Platform.OS === 'android' && {
headerCenter: () => (
<HeaderCenter
content={t('me.stacks.list.name', {
list: route.params.title
})}
/>
)
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -92,9 +82,6 @@ const TabMe = React.memo(
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)} />
})}
/>
@ -111,9 +98,6 @@ const TabMe = React.memo(
component={TabMeListList}
options={({ navigation }: any) => ({
title: t('me.stacks.lists.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.lists.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -130,9 +114,6 @@ const TabMe = React.memo(
component={TabMePush}
options={({ navigation }) => ({
title: t('me.stacks.push.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.push.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>
@ -149,9 +130,6 @@ const TabMe = React.memo(
component={TabMeSettingsFontsize}
options={({ navigation }: any) => ({
title: t('me.stacks.fontSize.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.fontSize.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -160,9 +138,6 @@ const TabMe = React.memo(
component={TabMeSettingsLanguage}
options={({ navigation }: any) => ({
title: t('me.stacks.language.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.language.name')} />
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.pop(1)} />
})}
/>
@ -173,9 +148,6 @@ const TabMe = React.memo(
presentation: 'modal',
headerShown: true,
title: t('me.stacks.switch.name'),
...(Platform.OS === 'android' && {
headerCenter: () => <HeaderCenter content={t('me.stacks.switch.name')} />
}),
headerLeft: () => (
<HeaderLeft content='ChevronDown' onPress={() => navigation.goBack()} />
)

View File

@ -0,0 +1,71 @@
import Button from '@components/Button'
import haptics from '@components/haptics'
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 React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { FlatList } from 'react-native-gesture-handler'
const TabMeFollowedTags: React.FC<TabMeStackScreenProps<'Tab-Me-FollowedTags'>> = ({
navigation
}) => {
const { t } = useTranslation('screenTabs')
const { data, fetchNextPage, refetch } = useFollowedTagsQuery()
const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : []
useEffect(() => {
if (flattenData.length === 0) {
navigation.goBack()
}
}, [flattenData.length])
const mutation = useTagsMutation({
onSuccess: () => {
haptics('Light')
refetch()
},
onError: (err: any, { to }) => {
displayMessage({
type: 'error',
message: t('common:message.error.message', {
function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow')
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
return (
<FlatList
style={{ flex: 1 }}
data={flattenData}
renderItem={({ item }) => (
<ComponentHashtag
hashtag={item}
onPress={() => {}}
children={
<Button
type='text'
content={t('shared.hashtag.unfollow')}
onPress={() => mutation.mutate({ tag: item.name, to: !item.following })}
/>
}
/>
)}
onEndReached={() => fetchNextPage()}
onEndReachedThreshold={0.5}
ItemSeparatorComponent={ComponentSeparator}
/>
)
}
export default TabMeFollowedTags

View File

@ -7,14 +7,14 @@ import { useTranslation } from 'react-i18next'
const TabMeListList: React.FC<TabMeStackScreenProps<'Tab-Me-List-List'>> = ({ navigation }) => {
const { data } = useListsQuery({})
const { t } = useTranslation('screenTabs')
const { t } = useTranslation()
useEffect(() => {
navigation.setOptions({
headerRight: () => (
<HeaderRight
accessibilityLabel={t('me.stacks.listAdd.name')}
content='Plus'
type='text'
content={t('common:buttons.create')}
onPress={() => navigation.navigate('Tab-Me-List-Edit', { type: 'add' })}
/>
)

View File

@ -3,6 +3,7 @@ import { useNavigation } from '@react-navigation/native'
import { useAppDispatch } from '@root/store'
import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
import { useListsQuery } from '@utils/queryHooks/lists'
import { useFollowedTagsQuery } from '@utils/queryHooks/tags'
import { getInstanceMePage, updateInstanceMePage } from '@utils/slices/instancesSlice'
import { getInstancePush } from '@utils/slices/instancesSlice'
import React, { useEffect } from 'react'
@ -16,39 +17,40 @@ const Collections: React.FC = () => {
const dispatch = useAppDispatch()
const mePage = useSelector(getInstanceMePage)
const listsQuery = useListsQuery({
useFollowedTagsQuery({
options: {
notifyOnChangeProps: ['data']
onSuccess: data =>
dispatch(
updateInstanceMePage({
followedTags: { shown: !!data?.pages?.[0].body?.length }
})
)
}
})
useEffect(() => {
if (listsQuery.isSuccess) {
dispatch(
updateInstanceMePage({
lists: { shown: listsQuery.data?.length ? true : false }
})
)
useListsQuery({
options: {
onSuccess: data =>
dispatch(
updateInstanceMePage({
lists: { shown: !!data?.length }
})
)
}
}, [listsQuery.isSuccess, listsQuery.data?.length])
const announcementsQuery = useAnnouncementQuery({
})
useAnnouncementQuery({
showAll: true,
options: {
notifyOnChangeProps: ['data']
onSuccess: data =>
dispatch(
updateInstanceMePage({
announcements: {
shown: !!data?.length ? true : false,
unread: data?.filter(announcement => !announcement.read).length
}
})
)
}
})
useEffect(() => {
if (announcementsQuery.data) {
dispatch(
updateInstanceMePage({
announcements: {
shown: announcementsQuery.data.length ? true : false,
unread: announcementsQuery.data.filter(announcement => !announcement.read).length
}
})
)
}
}, [announcementsQuery.data])
const instancePush = useSelector(getInstancePush, (prev, next) => prev?.global === next?.global)
@ -80,6 +82,14 @@ const Collections: React.FC = () => {
onPress={() => navigation.navigate('Tab-Me-List-List')}
/>
) : null}
{mePage.followedTags.shown ? (
<MenuRow
iconFront='Hash'
iconBack='ChevronRight'
title={t('me.stacks.followedTags.name')}
onPress={() => navigation.navigate('Tab-Me-FollowedTags')}
/>
) : null}
{mePage.announcements.shown ? (
<MenuRow
iconFront='Clipboard'

View File

@ -3,11 +3,11 @@ import { HeaderLeft, HeaderRight } from '@components/Header'
import { displayMessage } from '@components/Message'
import Timeline from '@components/Timeline'
import TimelineDefault from '@components/Timeline/Default'
import { useQueryClient } from '@tanstack/react-query'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyFollowedTags, useTagsMutation, useTagsQuery } from '@utils/queryHooks/tags'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
@ -27,7 +27,6 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
const queryKey: QueryKeyTimeline = ['Timeline', { page: 'Hashtag', hashtag }]
const { theme } = useTheme()
const { t } = useTranslation('screenTabs')
const canFollowTags = useSelector(checkInstanceFeature('follow_tags'))
@ -35,14 +34,16 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
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({
theme,
type: 'error',
message: t('common:message.error.message', {
function: to ? t('shared.hashtag.follow') : t('shared.hashtag.unfollow')
@ -68,7 +69,7 @@ const TabSharedHashtag: React.FC<TabSharedStackScreenProps<'Tab-Shared-Hashtag'>
content={data?.following ? t('shared.hashtag.unfollow') : t('shared.hashtag.follow')}
onPress={() =>
typeof data?.following === 'boolean' &&
mutation.mutate({ tag: hashtag, type: 'follow', to: !data.following })
mutation.mutate({ tag: hashtag, to: !data.following })
}
/>
)

View File

@ -135,6 +135,7 @@ const instancesMigration = {
instances: state.instances.map(instance => {
return {
...instance,
mePage: { ...instance.mePage, followedTags: { shown: false } },
push: {
...instance.push,
global: instance.push.global.value,

View File

@ -47,6 +47,7 @@ export type InstanceV11 = {
}
}
mePage: {
followedTags: { shown: boolean }
lists: { shown: boolean }
announcements: { shown: boolean; unread: number }
}

View File

@ -143,6 +143,7 @@ export type TabMeStackParamList = {
'Tab-Me-Bookmarks': undefined
'Tab-Me-Conversations': undefined
'Tab-Me-Favourites': undefined
'Tab-Me-FollowedTags': undefined
'Tab-Me-List': Mastodon.List
'Tab-Me-List-Accounts': Omit<Mastodon.List, 'replies_policy'>
'Tab-Me-List-Edit':

View File

@ -1,32 +1,40 @@
import apiInstance from '@api/instance'
import apiInstance, { InstanceResponse } from '@api/instance'
import { AxiosError } from 'axios'
import {
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from '@tanstack/react-query'
import { infinitePageParams } from './utils'
type QueryKeyFollowedTags = ['FollowedTags']
const useFollowedTagsQuery = ({
options
}: {
options?: UseQueryOptions<Mastodon.Tag, AxiosError>
}) => {
export type QueryKeyFollowedTags = ['FollowedTags']
const useFollowedTagsQuery = (
params: {
options?: Omit<
UseInfiniteQueryOptions<InstanceResponse<Mastodon.Tag[]>, AxiosError>,
'getPreviousPageParam' | 'getNextPageParam'
>
} | void
) => {
const queryKey: QueryKeyFollowedTags = ['FollowedTags']
return useQuery(
return useInfiniteQuery(
queryKey,
async ({ pageParam }: QueryFunctionContext<QueryKeyFollowedTags>) => {
const params: { [key: string]: string } = { ...pageParam }
const res = await apiInstance<Mastodon.Tag>({ method: 'get', url: `followed_tags`, params })
return res.body
return await apiInstance<Mastodon.Tag[]>({ method: 'get', url: `followed_tags`, params })
},
options
{
...params?.options,
...infinitePageParams
}
)
}
type QueryKeyTags = ['Tags', { tag: string }]
export type QueryKeyTags = ['Tags', { tag: string }]
const queryFunction = async ({ queryKey }: QueryFunctionContext<QueryKeyTags>) => {
const { tag } = queryKey[1]
@ -43,15 +51,12 @@ const useTagsQuery = ({
return useQuery(queryKey, queryFunction, options)
}
type MutationVarsAnnouncement = { tag: string; type: 'follow'; to: boolean }
const mutationFunction = async ({ tag, type, to }: MutationVarsAnnouncement) => {
switch (type) {
case 'follow':
return apiInstance<{}>({
method: 'post',
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
})
}
type MutationVarsAnnouncement = { tag: string; to: boolean }
const mutationFunction = async ({ tag, to }: MutationVarsAnnouncement) => {
return apiInstance<{}>({
method: 'post',
url: `tags/${tag}/${to ? 'follow' : 'unfollow'}`
})
}
const useTagsMutation = (options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>) => {
return useMutation(mutationFunction, options)

View File

@ -0,0 +1,8 @@
import { InstanceResponse } from '@api/instance'
export const infinitePageParams = {
getPreviousPageParam: (firstPage: InstanceResponse<any>) =>
firstPage.links?.prev && { since_id: firstPage.links.next },
getNextPageParam: (lastPage: InstanceResponse<any>) =>
lastPage.links?.next && { max_id: lastPage.links.next }
}

View File

@ -107,6 +107,7 @@ const addInstance = createAsyncThunk(
},
timelinesLookback: {},
mePage: {
followedTags: { shown: false },
lists: { shown: false },
announcements: { shown: false, unread: 0 }
},