Add trending tags in search landing page

This commit is contained in:
xmflsct 2022-12-03 23:49:14 +01:00
parent 8a7e78485d
commit dab09369cb
7 changed files with 135 additions and 79 deletions

View File

@ -7,30 +7,23 @@ const handleError = (error: any) => {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
console.error(
ctx.bold(' API instance '),
ctx.bold(' API '),
ctx.bold('response'),
error.response.status,
error?.response.data?.error || error?.response.message || 'Unknown error'
)
return Promise.reject({
status: error?.response.status,
message:
error?.response.data?.error ||
error?.response.message ||
'Unknown error'
message: error?.response.data?.error || error?.response.message || 'Unknown error'
})
} else if (error?.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
console.error(ctx.bold(' API instance '), ctx.bold('request'), error)
console.error(ctx.bold(' API '), ctx.bold('request'), error)
return Promise.reject()
} else {
console.error(
ctx.bold(' API instance '),
ctx.bold('internal'),
error?.message
)
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
return Promise.reject()
}
}

View File

@ -13,10 +13,7 @@ export type Params = {
}
headers?: { [key: string]: string }
body?: FormData
extras?: Omit<
AxiosRequestConfig,
'method' | 'url' | 'params' | 'headers' | 'data'
>
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
}
export type InstanceResponse<T = unknown> = {
@ -35,9 +32,7 @@ const apiInstance = async <T = unknown>({
}: Params): Promise<InstanceResponse<T>> => {
const { store } = require('@root/store')
const state = store.getState() as RootState
const instanceActive = state.instances.instances.findIndex(
instance => instance.active
)
const instanceActive = state.instances.instances.findIndex(instance => instance.active)
let domain
let token
@ -45,21 +40,19 @@ const apiInstance = async <T = unknown>({
domain = state.instances.instances[instanceActive].url
token = state.instances.instances[instanceActive].token
} else {
console.warn(
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
)
console.warn(ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided')
return Promise.reject()
}
console.log(
ctx.bgGreen.bold(' API instance ') +
' ' +
domain +
' ' +
method +
ctx.green(' -> ') +
`/${url}` +
(params ? ctx.green(' -> ') : ''),
' ' +
domain +
' ' +
method +
ctx.green(' -> ') +
`/${url}` +
(params ? ctx.green(' -> ') : ''),
params ? params : ''
)
@ -70,10 +63,7 @@ const apiInstance = async <T = unknown>({
url,
params,
headers: {
'Content-Type':
body && body instanceof FormData
? 'multipart/form-data'
: 'application/json',
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
Accept: '*/*',
...userAgent,
...headers,
@ -87,10 +77,10 @@ const apiInstance = async <T = unknown>({
.then(response => {
let prev
let next
if (response.headers.link) {
const headersLinks = li.parse(response.headers.link)
prev = headersLinks.prev?.match(/_id=([0-9]*)/)[1]
next = headersLinks.next?.match(/_id=([0-9]*)/)[1]
if (response.headers?.link) {
const headersLinks = li.parse(response.headers?.link)
prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1]
next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1]
}
return Promise.resolve({
body: response.data,

View File

@ -3,9 +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 { sumBy } from 'lodash'
import React, { useCallback, useState } from 'react'
import { Dimensions, Pressable, View } from 'react-native'
import { Dimensions, Pressable } from 'react-native'
import Sparkline from './Sparkline'
import CustomText from './Text'
@ -22,7 +21,7 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
}, [])
const padding = StyleConstants.Spacing.S * 1.5
const padding = StyleConstants.Spacing.Global.PagePadding
const width = Dimensions.get('window').width / 4
const [height, setHeight] = useState<number>(0)

View File

@ -24,6 +24,11 @@
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "trends_new_path",
"version": 3.5,
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
},
{
"feature": "follow_tags",
"version": 4.0,

View File

@ -350,6 +350,9 @@
"statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)",
"accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)"
}
},
"trending": {
"tags": "Trending tags"
}
},
"sections": {

View File

@ -6,10 +6,11 @@ import CustomText from '@components/Text'
import TimelineDefault from '@components/Timeline/Default'
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
import { useSearchQuery } from '@utils/queryHooks/search'
import { useTrendsQuery } from '@utils/queryHooks/trends'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { debounce } from 'lodash'
import React, { useCallback, useEffect, useMemo } from 'react'
import React, { useEffect } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import {
KeyboardAvoidingView,
@ -38,9 +39,6 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
}
: { headerLeft: () => null }),
headerTitle: () => {
const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, {
trailing: true
})
return (
<View
style={{
@ -67,7 +65,10 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
paddingLeft: StyleConstants.Spacing.XS
}}
autoFocus
onChangeText={onChangeText}
value={text}
onChangeText={debounce((text: string) => navigation.setParams({ text }), 1000, {
trailing: true
})}
autoCapitalize='none'
autoCorrect={false}
clearButtonMode='never'
@ -81,7 +82,9 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
)
}
})
}, [mode])
}, [text, mode])
const trendsTags = useTrendsQuery({ type: 'tags' })
const mapKeyToTranslations = {
accounts: t('shared.search.sections.accounts'),
@ -119,21 +122,16 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
}
})
const listEmpty = useMemo(() => {
const listEmpty = () => {
return (
<View
style={{
marginVertical: StyleConstants.Spacing.Global.PagePadding,
alignItems: 'center'
}}
>
<View>
{status === 'loading' ? (
<View style={{ flex: 1, alignItems: 'center' }}>
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View>
) : (
<>
<View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}>
{status === 'loading' ? (
<View style={{ flex: 1, alignItems: 'center' }}>
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
</View>
) : (
<>
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
<CustomText
fontStyle='S'
style={{
@ -148,7 +146,10 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
}}
/>
</CustomText>
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
<CustomText
style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}
fontWeight='Bold'
>
{t('shared.search.empty.advanced.header')}
</CustomText>
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
@ -171,25 +172,37 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
{' '}
{t('shared.search.empty.advanced.example.accountLink')}
</CustomText>
</>
)}
</View>
</View>
<CustomText
style={{
color: colors.primaryDefault,
marginTop: StyleConstants.Spacing.M,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
}}
fontWeight='Bold'
>
{t('shared.search.empty.trending.tags')}
</CustomText>
<View>
{trendsTags.data?.map((tag, index) => {
const hashtag = tag as Mastodon.Tag
return (
<React.Fragment key={index}>
{index !== 0 ? <ComponentSeparator /> : null}
<ComponentHashtag
hashtag={hashtag}
onPress={() => navigation.setParams({ text: `#${hashtag.name}` })}
/>
</React.Fragment>
)
})}
</View>
</>
)}
</View>
)
}, [status])
const listItem = useCallback(({ item, section }: { item: any; section: any }) => {
switch (section.title) {
case 'accounts':
return <ComponentAccount account={item} />
case 'hashtags':
return <ComponentHashtag hashtag={item} />
case 'statuses':
return <TimelineDefault item={item} disableDetails />
default:
return null
}
}, [])
}
return (
<KeyboardAvoidingView
@ -198,10 +211,21 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
>
<SectionList
style={{ minHeight: '100%' }}
renderItem={listItem}
renderItem={({ item, section }: { item: any; section: any }) => {
switch (section.title) {
case 'accounts':
return <ComponentAccount account={item} />
case 'hashtags':
return <ComponentHashtag hashtag={item} />
case 'statuses':
return <TimelineDefault item={item} disableDetails />
default:
return null
}
}}
stickySectionHeadersEnabled
sections={data || []}
ListEmptyComponent={listEmpty}
ListEmptyComponent={listEmpty()}
keyboardShouldPersistTaps='always'
renderSectionHeader={({ section: { translation } }) => (
<View

View File

@ -0,0 +1,42 @@
import apiInstance from '@api/instance'
import { store } from '@root/store'
import { checkInstanceFeature } from '@utils/slices/instancesSlice'
import { AxiosError } from 'axios'
import { QueryFunctionContext, useQuery, UseQueryOptions } from 'react-query'
export type QueryKeyTrends = ['Trends', { type: 'tags' | 'statuses' | 'links' }]
const queryFunction = ({ queryKey }: QueryFunctionContext<QueryKeyTrends>) => {
const trendsNewPath = checkInstanceFeature('trends_new_path')(store.getState())
if (!trendsNewPath && queryKey[1].type !== 'tags') {
return []
}
const { type } = queryKey[1]
switch (type) {
case 'tags':
return apiInstance<Mastodon.Tag[]>({
method: 'get',
url: trendsNewPath ? 'trends/tags' : 'trends'
}).then(res => res.body)
case 'statuses':
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: 'trends/tags'
}).then(res => res.body)
}
}
const useTrendsQuery = ({
options,
...queryKeyParams
}: QueryKeyTrends[1] & {
options?: UseQueryOptions<Mastodon.Tag[] | Mastodon.Status[], AxiosError>
}) => {
const queryKey: QueryKeyTrends = ['Trends', { ...queryKeyParams }]
return useQuery(queryKey, queryFunction, options)
}
export { useTrendsQuery }