From dab09369cb165b382a6d6a9a457102f4c98588ba Mon Sep 17 00:00:00 2001 From: xmflsct Date: Sat, 3 Dec 2022 23:49:14 +0100 Subject: [PATCH] Add trending tags in search landing page --- src/api/handleError.ts | 15 ++--- src/api/instance.ts | 40 +++++------ src/components/Hashtag.tsx | 5 +- src/helpers/features.json | 5 ++ src/i18n/en/screens/tabs.json | 3 + src/screens/Tabs/Shared/Search.tsx | 104 ++++++++++++++++++----------- src/utils/queryHooks/trends.ts | 42 ++++++++++++ 7 files changed, 135 insertions(+), 79 deletions(-) create mode 100644 src/utils/queryHooks/trends.ts diff --git a/src/api/handleError.ts b/src/api/handleError.ts index 6f984b78..efbff696 100644 --- a/src/api/handleError.ts +++ b/src/api/handleError.ts @@ -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() } } diff --git a/src/api/instance.ts b/src/api/instance.ts index 6df9974d..0126e4b1 100644 --- a/src/api/instance.ts +++ b/src/api/instance.ts @@ -13,10 +13,7 @@ export type Params = { } headers?: { [key: string]: string } body?: FormData - extras?: Omit< - AxiosRequestConfig, - 'method' | 'url' | 'params' | 'headers' | 'data' - > + extras?: Omit } export type InstanceResponse = { @@ -35,9 +32,7 @@ const apiInstance = async ({ }: Params): Promise> => { 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 ({ 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 ({ 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 ({ .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, diff --git a/src/components/Hashtag.tsx b/src/components/Hashtag.tsx index 721dd73f..2d05edc9 100644 --- a/src/components/Hashtag.tsx +++ b/src/components/Hashtag.tsx @@ -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 = ({ 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(0) diff --git a/src/helpers/features.json b/src/helpers/features.json index 147b20a4..3a466762 100644 --- a/src/helpers/features.json +++ b/src/helpers/features.json @@ -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, diff --git a/src/i18n/en/screens/tabs.json b/src/i18n/en/screens/tabs.json index 808dd98b..8f49b544 100644 --- a/src/i18n/en/screens/tabs.json +++ b/src/i18n/en/screens/tabs.json @@ -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": { diff --git a/src/screens/Tabs/Shared/Search.tsx b/src/screens/Tabs/Shared/Search.tsx index c5a7cd43..090d696e 100644 --- a/src/screens/Tabs/Shared/Search.tsx +++ b/src/screens/Tabs/Shared/Search.tsx @@ -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> } : { headerLeft: () => null }), headerTitle: () => { - const onChangeText = debounce((text: string) => navigation.setParams({ text }), 1000, { - trailing: true - }) return ( > 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> ) } }) - }, [mode]) + }, [text, mode]) + + const trendsTags = useTrendsQuery({ type: 'tags' }) const mapKeyToTranslations = { accounts: t('shared.search.sections.accounts'), @@ -119,21 +122,16 @@ const TabSharedSearch: React.FC> } }) - const listEmpty = useMemo(() => { + const listEmpty = () => { return ( - - - {status === 'loading' ? ( - - - - ) : ( - <> + + {status === 'loading' ? ( + + + + ) : ( + <> + > }} /> - + {t('shared.search.empty.advanced.header')} @@ -171,25 +172,37 @@ const TabSharedSearch: React.FC> {' '} {t('shared.search.empty.advanced.example.accountLink')} - - )} - + + + + {t('shared.search.empty.trending.tags')} + + + {trendsTags.data?.map((tag, index) => { + const hashtag = tag as Mastodon.Tag + return ( + + {index !== 0 ? : null} + navigation.setParams({ text: `#${hashtag.name}` })} + /> + + ) + })} + + + )} ) - }, [status]) - - const listItem = useCallback(({ item, section }: { item: any; section: any }) => { - switch (section.title) { - case 'accounts': - return - case 'hashtags': - return - case 'statuses': - return - default: - return null - } - }, []) + } return ( > > { + switch (section.title) { + case 'accounts': + return + case 'hashtags': + return + case 'statuses': + return + default: + return null + } + }} stickySectionHeadersEnabled sections={data || []} - ListEmptyComponent={listEmpty} + ListEmptyComponent={listEmpty()} keyboardShouldPersistTaps='always' renderSectionHeader={({ section: { translation } }) => ( ) => { + 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({ + method: 'get', + url: trendsNewPath ? 'trends/tags' : 'trends' + }).then(res => res.body) + case 'statuses': + return apiInstance({ + method: 'get', + url: 'trends/tags' + }).then(res => res.body) + } +} + +const useTrendsQuery = ({ + options, + ...queryKeyParams +}: QueryKeyTrends[1] & { + options?: UseQueryOptions +}) => { + const queryKey: QueryKeyTrends = ['Trends', { ...queryKeyParams }] + return useQuery(queryKey, queryFunction, options) +} + +export { useTrendsQuery }