mirror of https://github.com/tooot-app/app
Add trending tags in search landing page
This commit is contained in:
parent
8a7e78485d
commit
dab09369cb
|
@ -7,30 +7,23 @@ const handleError = (error: any) => {
|
||||||
// The request was made and the server responded with a status code
|
// The request was made and the server responded with a status code
|
||||||
// that falls out of the range of 2xx
|
// that falls out of the range of 2xx
|
||||||
console.error(
|
console.error(
|
||||||
ctx.bold(' API instance '),
|
ctx.bold(' API '),
|
||||||
ctx.bold('response'),
|
ctx.bold('response'),
|
||||||
error.response.status,
|
error.response.status,
|
||||||
error?.response.data?.error || error?.response.message || 'Unknown error'
|
error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||||
)
|
)
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
status: error?.response.status,
|
status: error?.response.status,
|
||||||
message:
|
message: error?.response.data?.error || error?.response.message || 'Unknown error'
|
||||||
error?.response.data?.error ||
|
|
||||||
error?.response.message ||
|
|
||||||
'Unknown error'
|
|
||||||
})
|
})
|
||||||
} else if (error?.request) {
|
} else if (error?.request) {
|
||||||
// The request was made but no response was received
|
// The request was made but no response was received
|
||||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||||
// http.ClientRequest in node.js
|
// 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()
|
return Promise.reject()
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(ctx.bold(' API '), ctx.bold('internal'), error?.message)
|
||||||
ctx.bold(' API instance '),
|
|
||||||
ctx.bold('internal'),
|
|
||||||
error?.message
|
|
||||||
)
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,7 @@ export type Params = {
|
||||||
}
|
}
|
||||||
headers?: { [key: string]: string }
|
headers?: { [key: string]: string }
|
||||||
body?: FormData
|
body?: FormData
|
||||||
extras?: Omit<
|
extras?: Omit<AxiosRequestConfig, 'method' | 'url' | 'params' | 'headers' | 'data'>
|
||||||
AxiosRequestConfig,
|
|
||||||
'method' | 'url' | 'params' | 'headers' | 'data'
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type InstanceResponse<T = unknown> = {
|
export type InstanceResponse<T = unknown> = {
|
||||||
|
@ -35,9 +32,7 @@ const apiInstance = async <T = unknown>({
|
||||||
}: Params): Promise<InstanceResponse<T>> => {
|
}: Params): Promise<InstanceResponse<T>> => {
|
||||||
const { store } = require('@root/store')
|
const { store } = require('@root/store')
|
||||||
const state = store.getState() as RootState
|
const state = store.getState() as RootState
|
||||||
const instanceActive = state.instances.instances.findIndex(
|
const instanceActive = state.instances.instances.findIndex(instance => instance.active)
|
||||||
instance => instance.active
|
|
||||||
)
|
|
||||||
|
|
||||||
let domain
|
let domain
|
||||||
let token
|
let token
|
||||||
|
@ -45,21 +40,19 @@ const apiInstance = async <T = unknown>({
|
||||||
domain = state.instances.instances[instanceActive].url
|
domain = state.instances.instances[instanceActive].url
|
||||||
token = state.instances.instances[instanceActive].token
|
token = state.instances.instances[instanceActive].token
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided')
|
||||||
ctx.bgRed.white.bold(' API ') + ' ' + 'No instance domain is provided'
|
|
||||||
)
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
ctx.bgGreen.bold(' API instance ') +
|
ctx.bgGreen.bold(' API instance ') +
|
||||||
' ' +
|
' ' +
|
||||||
domain +
|
domain +
|
||||||
' ' +
|
' ' +
|
||||||
method +
|
method +
|
||||||
ctx.green(' -> ') +
|
ctx.green(' -> ') +
|
||||||
`/${url}` +
|
`/${url}` +
|
||||||
(params ? ctx.green(' -> ') : ''),
|
(params ? ctx.green(' -> ') : ''),
|
||||||
params ? params : ''
|
params ? params : ''
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -70,10 +63,7 @@ const apiInstance = async <T = unknown>({
|
||||||
url,
|
url,
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type':
|
'Content-Type': body && body instanceof FormData ? 'multipart/form-data' : 'application/json',
|
||||||
body && body instanceof FormData
|
|
||||||
? 'multipart/form-data'
|
|
||||||
: 'application/json',
|
|
||||||
Accept: '*/*',
|
Accept: '*/*',
|
||||||
...userAgent,
|
...userAgent,
|
||||||
...headers,
|
...headers,
|
||||||
|
@ -87,10 +77,10 @@ const apiInstance = async <T = unknown>({
|
||||||
.then(response => {
|
.then(response => {
|
||||||
let prev
|
let prev
|
||||||
let next
|
let next
|
||||||
if (response.headers.link) {
|
if (response.headers?.link) {
|
||||||
const headersLinks = li.parse(response.headers.link)
|
const headersLinks = li.parse(response.headers?.link)
|
||||||
prev = headersLinks.prev?.match(/_id=([0-9]*)/)[1]
|
prev = headersLinks.prev?.match(/_id=([0-9]*)/)?.[1]
|
||||||
next = headersLinks.next?.match(/_id=([0-9]*)/)[1]
|
next = headersLinks.next?.match(/_id=([0-9]*)/)?.[1]
|
||||||
}
|
}
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
body: response.data,
|
body: response.data,
|
||||||
|
|
|
@ -3,9 +3,8 @@ import { StackNavigationProp } from '@react-navigation/stack'
|
||||||
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
import { TabLocalStackParamList } from '@utils/navigation/navigators'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { sumBy } from 'lodash'
|
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { Dimensions, Pressable, View } from 'react-native'
|
import { Dimensions, Pressable } from 'react-native'
|
||||||
import Sparkline from './Sparkline'
|
import Sparkline from './Sparkline'
|
||||||
import CustomText from './Text'
|
import CustomText from './Text'
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ const ComponentHashtag: React.FC<Props> = ({ hashtag, onPress: customOnPress })
|
||||||
navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name })
|
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 width = Dimensions.get('window').width / 4
|
||||||
const [height, setHeight] = useState<number>(0)
|
const [height, setHeight] = useState<number>(0)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,11 @@
|
||||||
"version": 3.5,
|
"version": 3.5,
|
||||||
"reference": "https://github.com/mastodon/mastodon/releases/tag/v3.5.0"
|
"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",
|
"feature": "follow_tags",
|
||||||
"version": 4.0,
|
"version": 4.0,
|
||||||
|
|
|
@ -350,6 +350,9 @@
|
||||||
"statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)",
|
"statusLink": "$t(shared.search.header.prefix) $t(shared.search.sections.statuses)",
|
||||||
"accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)"
|
"accountLink": "$t(shared.search.header.prefix) $t(shared.search.sections.accounts)"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"trending": {
|
||||||
|
"tags": "Trending tags"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
|
|
|
@ -6,10 +6,11 @@ import CustomText from '@components/Text'
|
||||||
import TimelineDefault from '@components/Timeline/Default'
|
import TimelineDefault from '@components/Timeline/Default'
|
||||||
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
import { TabSharedStackScreenProps } from '@utils/navigation/navigators'
|
||||||
import { useSearchQuery } from '@utils/queryHooks/search'
|
import { useSearchQuery } from '@utils/queryHooks/search'
|
||||||
|
import { useTrendsQuery } from '@utils/queryHooks/trends'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
@ -38,9 +39,6 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||||
}
|
}
|
||||||
: { headerLeft: () => null }),
|
: { headerLeft: () => null }),
|
||||||
headerTitle: () => {
|
headerTitle: () => {
|
||||||
const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, {
|
|
||||||
trailing: true
|
|
||||||
})
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
@ -67,7 +65,10 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||||
paddingLeft: StyleConstants.Spacing.XS
|
paddingLeft: StyleConstants.Spacing.XS
|
||||||
}}
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
onChangeText={onChangeText}
|
value={text}
|
||||||
|
onChangeText={debounce((text: string) => navigation.setParams({ text }), 1000, {
|
||||||
|
trailing: true
|
||||||
|
})}
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
clearButtonMode='never'
|
clearButtonMode='never'
|
||||||
|
@ -81,7 +82,9 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [mode])
|
}, [text, mode])
|
||||||
|
|
||||||
|
const trendsTags = useTrendsQuery({ type: 'tags' })
|
||||||
|
|
||||||
const mapKeyToTranslations = {
|
const mapKeyToTranslations = {
|
||||||
accounts: t('shared.search.sections.accounts'),
|
accounts: t('shared.search.sections.accounts'),
|
||||||
|
@ -119,21 +122,16 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const listEmpty = useMemo(() => {
|
const listEmpty = () => {
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={{ paddingVertical: StyleConstants.Spacing.Global.PagePadding }}>
|
||||||
style={{
|
{status === 'loading' ? (
|
||||||
marginVertical: StyleConstants.Spacing.Global.PagePadding,
|
<View style={{ flex: 1, alignItems: 'center' }}>
|
||||||
alignItems: 'center'
|
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
||||||
}}
|
</View>
|
||||||
>
|
) : (
|
||||||
<View>
|
<>
|
||||||
{status === 'loading' ? (
|
<View style={{ paddingHorizontal: StyleConstants.Spacing.Global.PagePadding }}>
|
||||||
<View style={{ flex: 1, alignItems: 'center' }}>
|
|
||||||
<Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CustomText
|
<CustomText
|
||||||
fontStyle='S'
|
fontStyle='S'
|
||||||
style={{
|
style={{
|
||||||
|
@ -148,7 +146,10 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CustomText>
|
</CustomText>
|
||||||
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
<CustomText
|
||||||
|
style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}
|
||||||
|
fontWeight='Bold'
|
||||||
|
>
|
||||||
{t('shared.search.empty.advanced.header')}
|
{t('shared.search.empty.advanced.header')}
|
||||||
</CustomText>
|
</CustomText>
|
||||||
<CustomText style={[styles.emptyAdvanced, { color: colors.primaryDefault }]}>
|
<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')}
|
{t('shared.search.empty.advanced.example.accountLink')}
|
||||||
</CustomText>
|
</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>
|
</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 (
|
return (
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
|
@ -198,10 +211,21 @@ const TabSharedSearch: React.FC<TabSharedStackScreenProps<'Tab-Shared-Search'>>
|
||||||
>
|
>
|
||||||
<SectionList
|
<SectionList
|
||||||
style={{ minHeight: '100%' }}
|
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
|
stickySectionHeadersEnabled
|
||||||
sections={data || []}
|
sections={data || []}
|
||||||
ListEmptyComponent={listEmpty}
|
ListEmptyComponent={listEmpty()}
|
||||||
keyboardShouldPersistTaps='always'
|
keyboardShouldPersistTaps='always'
|
||||||
renderSectionHeader={({ section: { translation } }) => (
|
renderSectionHeader={({ section: { translation } }) => (
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -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 }
|
Loading…
Reference in New Issue