diff --git a/README.md b/README.md index baa9c50d..7071cd32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # [tooot](https://tooot.app/) app for Mastodon -[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push?style=flat-square)](LICENSE) ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build?style=flat-square) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app?style=flat-square) ![GitHub package.json version](https://img.shields.io/github/package-json/v/tooot-app/app?style=flat-square) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app?style=flat-square) +[![GPL-3.0](https://img.shields.io/github/license/tooot-app/push?style=flat-square)](LICENSE) ![GitHub issues](https://img.shields.io/github/issues/tooot-app/app?style=flat-square) ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/tooot-app/app?include_prereleases&style=flat-square) ![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/tooot-app/app?style=flat-square) + +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/tooot-app/app/build?style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/candidate?label=build%20candidate&style=flat-square) ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/tooot-app/app/build/release?label=build%20release&style=flat-square) diff --git a/src/@types/react-navigation.d.ts b/src/@types/react-navigation.d.ts index 1cdd1168..3d6798b5 100644 --- a/src/@types/react-navigation.d.ts +++ b/src/@types/react-navigation.d.ts @@ -84,15 +84,24 @@ declare namespace Nav { 'Tab-Shared-Hashtag': { hashtag: Mastodon.Tag['name'] } - 'Tab-Shared-Relationships': { - account: Mastodon.Account - initialType: 'following' | 'followers' - } 'Tab-Shared-Search': { text: string | undefined } 'Tab-Shared-Toot': { toot: Mastodon.Status rootQueryKey: any } + 'Tab-Shared-Users': + | { + reference: 'accounts' + id: Mastodon.Account['id'] + type: 'following' | 'followers' + count: number + } + | { + reference: 'statuses' + id: Mastodon.Status['id'] + type: 'reblogged_by' | 'favourited_by' + count: number + } } type TabLocalStackParamList = { diff --git a/src/components/Timeline/Default.tsx b/src/components/Timeline/Default.tsx index 8f1bae30..e9e738e7 100644 --- a/src/components/Timeline/Default.tsx +++ b/src/components/Timeline/Default.tsx @@ -17,6 +17,7 @@ import { uniqBy } from 'lodash' import React, { useCallback } from 'react' import { Pressable, StyleSheet, View } from 'react-native' import { useSelector } from 'react-redux' +import TimelineActionsUsers from './Shared/ActionsUsers' import TimelineFullConversation from './Shared/FullConversation' export interface Props { @@ -128,6 +129,8 @@ const TimelineDefault: React.FC = ({ + + {queryKey && !disableDetails && ( { + if (!highlighted) { + return null + } + + const { t } = useTranslation('componentTimeline') + const { theme } = useTheme() + const navigation = useNavigation< + StackNavigationProp + >() + + return ( + + {status.reblogs_count > 0 ? ( + { + analytics('timeline_shared_actionsusers_press_boosted', { + count: status.reblogs_count + }) + navigation.push('Tab-Shared-Users', { + reference: 'statuses', + id: status.id, + type: 'reblogged_by', + count: status.reblogs_count + }) + }} + > + {t('shared.actionsUsers.reblogged_by', { + count: status.reblogs_count + })} + + ) : null} + {status.favourites_count > 0 ? ( + { + analytics('timeline_shared_actionsusers_press_boosted', { + count: status.favourites_count + }) + navigation.push('Tab-Shared-Users', { + reference: 'statuses', + id: status.id, + type: 'favourited_by', + count: status.favourites_count + }) + }} + > + {t('shared.actionsUsers.favourited_by', { + count: status.favourites_count + })} + + ) : null} + + ) + }, + (prev, next) => + prev.status.reblogs_count === next.status.reblogs_count && + prev.status.favourites_count === next.status.favourites_count +) + +const styles = StyleSheet.create({ + base: { + flexDirection: 'row' + }, + pressable: { margin: StyleConstants.Spacing.M }, + text: { + ...StyleConstants.FontStyle.S, + padding: StyleConstants.Spacing.S * 1.5, + paddingLeft: 0, + marginRight: StyleConstants.Spacing.S + } +}) + +export default TimelineActionsUsers diff --git a/src/i18n/en/_all.ts b/src/i18n/en/_all.ts index 8db9aaaf..4fc87212 100644 --- a/src/i18n/en/_all.ts +++ b/src/i18n/en/_all.ts @@ -22,9 +22,9 @@ export default { sharedAnnouncements: require('./screens/sharedAnnouncements').default, sharedAttachments: require('./screens/sharedAttachments').default, sharedCompose: require('./screens/sharedCompose').default, - sharedRelationships: require('./screens/sharedRelationships').default, sharedSearch: require('./screens/sharedSearch').default, sharedToot: require('./screens/sharedToot').default, + sharedUsers: require('./screens/sharedUsers').default, componentInstance: require('./components/instance').default, componentParse: require('./components/parse').default, diff --git a/src/i18n/en/components/timeline.ts b/src/i18n/en/components/timeline.ts index 4a7aeb56..b1faeccb 100644 --- a/src/i18n/en/components/timeline.ts +++ b/src/i18n/en/components/timeline.ts @@ -39,6 +39,10 @@ export default { function: 'Bookmark toot' } }, + actionsUsers: { + reblogged_by: '$t(sharedUsers:heading.statuses.reblogged_by)', + favourited_by: '$t(sharedUsers:heading.statuses.favourited_by)' + }, attachment: { sensitive: { button: 'Show sensitive media' diff --git a/src/i18n/en/screens/sharedAccount.ts b/src/i18n/en/screens/sharedAccount.ts index fe7fe665..a8bd00df 100644 --- a/src/i18n/en/screens/sharedAccount.ts +++ b/src/i18n/en/screens/sharedAccount.ts @@ -4,8 +4,8 @@ export default { created_at: 'Registered: {{date}}', summary: { statuses_count: '{{count}} toots', - following_count: 'Following {{count}}', - followers_count: '{{count}} followers' + following_count: '$t(sharedUsers:heading.accounts.following)', + followers_count: '$t(sharedUsers:heading.accounts.followers)' } } } diff --git a/src/i18n/en/screens/sharedRelationships.ts b/src/i18n/en/screens/sharedRelationships.ts deleted file mode 100644 index 24a7b3e3..00000000 --- a/src/i18n/en/screens/sharedRelationships.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default { - heading: { - segments: { - left: 'Following', - right: 'Followers' - } - } -} diff --git a/src/i18n/en/screens/sharedUsers.ts b/src/i18n/en/screens/sharedUsers.ts new file mode 100644 index 00000000..ac5cfba4 --- /dev/null +++ b/src/i18n/en/screens/sharedUsers.ts @@ -0,0 +1,12 @@ +export default { + heading: { + accounts: { + following: 'Following {{count}}', + followers: '{{count}} followers' + }, + statuses: { + reblogged_by: '{{count}} boosted', + favourited_by: '{{count}} favourited' + } + } +} diff --git a/src/i18n/zh-Hans/_all.ts b/src/i18n/zh-Hans/_all.ts index 8db9aaaf..4fc87212 100644 --- a/src/i18n/zh-Hans/_all.ts +++ b/src/i18n/zh-Hans/_all.ts @@ -22,9 +22,9 @@ export default { sharedAnnouncements: require('./screens/sharedAnnouncements').default, sharedAttachments: require('./screens/sharedAttachments').default, sharedCompose: require('./screens/sharedCompose').default, - sharedRelationships: require('./screens/sharedRelationships').default, sharedSearch: require('./screens/sharedSearch').default, sharedToot: require('./screens/sharedToot').default, + sharedUsers: require('./screens/sharedUsers').default, componentInstance: require('./components/instance').default, componentParse: require('./components/parse').default, diff --git a/src/i18n/zh-Hans/components/timeline.ts b/src/i18n/zh-Hans/components/timeline.ts index 817afa19..6d7871e1 100644 --- a/src/i18n/zh-Hans/components/timeline.ts +++ b/src/i18n/zh-Hans/components/timeline.ts @@ -39,6 +39,10 @@ export default { function: '收藏嘟文' } }, + actionsUsers: { + reblogged_by: '$t(sharedUsers:heading.statuses.reblogged_by)', + favourited_by: '$t(sharedUsers:heading.statuses.favourited_by)' + }, attachment: { sensitive: { button: '显示敏感内容' diff --git a/src/i18n/zh-Hans/screens/sharedAccount.ts b/src/i18n/zh-Hans/screens/sharedAccount.ts index 3d2caa6a..96624804 100644 --- a/src/i18n/zh-Hans/screens/sharedAccount.ts +++ b/src/i18n/zh-Hans/screens/sharedAccount.ts @@ -4,8 +4,8 @@ export default { created_at: '注册时间:{{date}}', summary: { statuses_count: '{{count}} 条嘟文', - following_count: '关注 {{count}} 人', - followers_count: '被 {{count}} 人关注' + following_count: '$t(sharedUsers:heading.accounts.following)', + followers_count: '$t(sharedUsers:heading.accounts.followers)' } } } diff --git a/src/i18n/zh-Hans/screens/sharedRelationships.ts b/src/i18n/zh-Hans/screens/sharedRelationships.ts deleted file mode 100644 index 64f5c40e..00000000 --- a/src/i18n/zh-Hans/screens/sharedRelationships.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default { - heading: { - segments: { - left: '关注中', - right: '关注者' - } - } -} diff --git a/src/i18n/zh-Hans/screens/sharedUsers.ts b/src/i18n/zh-Hans/screens/sharedUsers.ts new file mode 100644 index 00000000..60450284 --- /dev/null +++ b/src/i18n/zh-Hans/screens/sharedUsers.ts @@ -0,0 +1,12 @@ +export default { + heading: { + accounts: { + following: '关注 {{count}} 人', + followers: '被 {{count}} 人关注' + }, + statuses: { + reblogged_by: '{{count}} 人转嘟', + favourited_by: '{{count}} 人收藏' + } + } +} diff --git a/src/screens/Tabs/Shared/Account/Information/Stats.tsx b/src/screens/Tabs/Shared/Account/Information/Stats.tsx index 504c3fb1..08777a3c 100644 --- a/src/screens/Tabs/Shared/Account/Information/Stats.tsx +++ b/src/screens/Tabs/Shared/Account/Information/Stats.tsx @@ -48,15 +48,17 @@ const AccountInformationStats: React.FC = ({ account, myInfo }) => { { analytics('account_stats_following_press', { count: account.following_count }) - navigation.push('Tab-Shared-Relationships', { - account, - initialType: 'following' + navigation.push('Tab-Shared-Users', { + reference: 'accounts', + id: account.id, + type: 'following', + count: account.following_count }) }} /> @@ -73,15 +75,17 @@ const AccountInformationStats: React.FC = ({ account, myInfo }) => { { analytics('account_stats_followers_press', { count: account.followers_count }) - navigation.push('Tab-Shared-Relationships', { - account, - initialType: 'followers' + navigation.push('Tab-Shared-Users', { + reference: 'accounts', + id: account.id, + type: 'followers', + count: account.followers_count }) }} /> diff --git a/src/screens/Tabs/Shared/Relationships.tsx b/src/screens/Tabs/Shared/Relationships.tsx deleted file mode 100644 index 725bb132..00000000 --- a/src/screens/Tabs/Shared/Relationships.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import SegmentedControl from '@react-native-community/segmented-control' -import { useNavigation } from '@react-navigation/native' -import { useTheme } from '@utils/styles/ThemeManager' -import React, { useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { Dimensions, StyleSheet, View } from 'react-native' -import { TabView } from 'react-native-tab-view' -import RelationshipsList from './Relationships/List' -import { SharedRelationshipsProp } from './sharedScreens' - -const TabSharedRelationships = React.memo( - ({ - route: { - params: { account, initialType } - } - }: SharedRelationshipsProp) => { - const { t } = useTranslation('sharedRelationships') - const { mode } = useTheme() - const navigation = useNavigation() - - const [segment, setSegment] = useState(initialType === 'following' ? 0 : 1) - useEffect(() => { - const updateHeaderRight = () => - navigation.setOptions({ - headerCenter: () => ( - - - setSegment(nativeEvent.selectedSegmentIndex) - } - /> - - ) - }) - return updateHeaderRight() - }, [segment, mode]) - - const routes: { - key: SharedRelationshipsProp['route']['params']['initialType'] - }[] = [{ key: 'following' }, { key: 'followers' }] - - const renderScene = ({ - route - }: { - route: { - key: SharedRelationshipsProp['route']['params']['initialType'] - } - }) => { - return - } - - return ( - null} - onIndexChange={index => setSegment(index)} - navigationState={{ index: segment, routes }} - initialLayout={{ width: Dimensions.get('screen').width }} - /> - ) - }, - () => true -) - -const styles = StyleSheet.create({ - segmentsContainer: { - flexBasis: '60%' - } -}) - -export default TabSharedRelationships diff --git a/src/screens/Tabs/Shared/Relationships/List.tsx b/src/screens/Tabs/Shared/Relationships/List.tsx deleted file mode 100644 index bd460352..00000000 --- a/src/screens/Tabs/Shared/Relationships/List.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import ComponentAccount from '@components/Account' -import ComponentSeparator from '@components/Separator' -import { useScrollToTop } from '@react-navigation/native' -import { - QueryKeyRelationships, - useRelationshipsQuery -} from '@utils/queryHooks/relationships' -import React, { useCallback, useMemo, useRef } from 'react' -import { RefreshControl, StyleSheet } from 'react-native' -import { FlatList } from 'react-native-gesture-handler' - -export interface Props { - id: Mastodon.Account['id'] - type: 'following' | 'followers' -} - -const RelationshipsList: React.FC = ({ id, type }) => { - const queryKey: QueryKeyRelationships = ['Relationships', { type, id }] - const { - data, - isFetching, - refetch, - fetchNextPage, - isFetchingNextPage - } = useRelationshipsQuery({ - ...queryKey[1], - options: { - getPreviousPageParam: firstPage => - firstPage.links?.prev && { since_id: firstPage.links.next }, - getNextPageParam: lastPage => - lastPage.links?.next && { max_id: lastPage.links.next } - } - }) - const flattenData = data?.pages ? data.pages.flatMap(d => [...d.body]) : [] - - const flRef = useRef>(null) - - const keyExtractor = useCallback(({ id }) => id, []) - const renderItem = useCallback( - ({ item }) => , - [] - ) - const onEndReached = useCallback( - () => !isFetchingNextPage && fetchNextPage(), - [isFetchingNextPage] - ) - const refreshControl = useMemo( - () => ( - refetch()} /> - ), - [isFetching] - ) - - useScrollToTop(flRef) - - return ( - - ) -} - -const styles = StyleSheet.create({ - flatList: { - minHeight: '100%' - } -}) - -export default RelationshipsList diff --git a/src/screens/Tabs/Shared/Users.tsx b/src/screens/Tabs/Shared/Users.tsx new file mode 100644 index 00000000..c26770f6 --- /dev/null +++ b/src/screens/Tabs/Shared/Users.tsx @@ -0,0 +1,66 @@ +import ComponentAccount from '@components/Account' +import ComponentSeparator from '@components/Separator' +import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users' +import React, { useCallback } from 'react' +import { StyleSheet } from 'react-native' +import { FlatList } from 'react-native-gesture-handler' +import { SharedUsersProp } from './sharedScreens' + +const TabSharedUsers = React.memo( + ({ route: { params } }: SharedUsersProp) => { + const queryKey: QueryKeyUsers = ['Users', params] + const { + data, + hasNextPage, + fetchNextPage, + isFetchingNextPage + } = useUsersQuery({ + ...queryKey[1], + options: { + getPreviousPageParam: firstPage => + firstPage.links?.prev && { since_id: firstPage.links.next }, + getNextPageParam: lastPage => + lastPage.links?.next && { max_id: lastPage.links.next } + } + }) + const flattenData = data?.pages + ? data.pages.flatMap(page => [...page.body]) + : [] + + const renderItem = useCallback( + ({ item }) => , + [] + ) + const onEndReached = useCallback( + () => hasNextPage && !isFetchingNextPage && fetchNextPage(), + [hasNextPage, isFetchingNextPage] + ) + + return ( + + ) + }, + () => true +) + +const styles = StyleSheet.create({ + flatList: { + minHeight: '100%' + } +}) + +export default TabSharedUsers diff --git a/src/screens/Tabs/Shared/sharedScreens.tsx b/src/screens/Tabs/Shared/sharedScreens.tsx index 0de44ca1..d7491706 100644 --- a/src/screens/Tabs/Shared/sharedScreens.tsx +++ b/src/screens/Tabs/Shared/sharedScreens.tsx @@ -5,9 +5,9 @@ import { StackScreenProps } from '@react-navigation/stack' import TabSharedAccount from '@screens/Tabs/Shared/Account' import TabSharedAttachments from '@screens/Tabs/Shared/Attachments' import TabSharedHashtag from '@screens/Tabs/Shared/Hashtag' -import TabSharedRelationships from '@screens/Tabs/Shared/Relationships' import TabSharedSearch from '@screens/Tabs/Shared/Search' import TabSharedToot from '@screens/Tabs/Shared/Toot' +import TabSharedUsers from '@screens/Tabs/Shared/Users' import { StyleConstants } from '@utils/styles/constants' import { useTheme } from '@utils/styles/ThemeManager' import { debounce } from 'lodash' @@ -41,11 +41,6 @@ export type SharedHashtagProp = StackScreenProps< 'Tab-Shared-Hashtag' > -export type SharedRelationshipsProp = StackScreenProps< - BaseScreens, - 'Tab-Shared-Relationships' -> - export type SharedSearchProp = StackScreenProps< BaseScreens, 'Tab-Shared-Search' @@ -53,6 +48,8 @@ export type SharedSearchProp = StackScreenProps< export type SharedTootProp = StackScreenProps +export type SharedUsersProp = StackScreenProps + const sharedScreens = ( Stack: TypedNavigator< BaseScreens, @@ -133,14 +130,6 @@ const sharedScreens = ( headerLeft: () => navigation.goBack()} /> })} />, - ({ - headerLeft: () => navigation.goBack()} /> - })} - />, navigation.goBack()} /> })} + />, + ({ + headerLeft: () => navigation.goBack()} />, + headerTitle: t(`sharedUsers:heading.${reference}.${type}`, { count }) + })} /> ] } diff --git a/src/utils/queryHooks/relationships.ts b/src/utils/queryHooks/users.ts similarity index 58% rename from src/utils/queryHooks/relationships.ts rename to src/utils/queryHooks/users.ts index 301cdf14..276f7652 100644 --- a/src/utils/queryHooks/relationships.ts +++ b/src/utils/queryHooks/users.ts @@ -2,46 +2,46 @@ import apiInstance from '@api/instance' import { AxiosError } from 'axios' import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query' -export type QueryKeyRelationships = [ - 'Relationships', - { type: 'following' | 'followers'; id: Mastodon.Account['id'] } +export type QueryKeyUsers = [ + 'Users', + Nav.TabSharedStackParamList['Tab-Shared-Users'] ] const queryFunction = ({ queryKey, pageParam }: { - queryKey: QueryKeyRelationships + queryKey: QueryKeyUsers pageParam?: { [key: string]: string } }) => { - const { type, id } = queryKey[1] + const { reference, id, type } = queryKey[1] let params: { [key: string]: string } = { ...pageParam } return apiInstance({ method: 'get', - url: `accounts/${id}/${type}`, + url: `${reference}/${id}/${type}`, params }) } -const useRelationshipsQuery = ({ +const useUsersQuery = ({ options, ...queryKeyParams -}: QueryKeyRelationships[1] & { +}: QueryKeyUsers[1] & { options?: UseInfiniteQueryOptions< { body: Mastodon.Account[] links?: { prev?: string; next?: string } }, AxiosError, - TData + { + body: Mastodon.Account[] + links?: { prev?: string; next?: string } + } > }) => { - const queryKey: QueryKeyRelationships = [ - 'Relationships', - { ...queryKeyParams } - ] + const queryKey: QueryKeyUsers = ['Users', { ...queryKeyParams }] return useInfiniteQuery(queryKey, queryFunction, options) } -export { useRelationshipsQuery } +export { useUsersQuery }