This commit is contained in:
Zhiyuan Zheng 2021-03-15 00:18:44 +01:00
parent 7e1916989d
commit e6adbf6986
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
21 changed files with 258 additions and 227 deletions

View File

@ -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)

View File

@ -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 = {

View File

@ -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<Props> = ({
<TimelineFullConversation queryKey={queryKey} status={actualStatus} />
</View>
<TimelineActionsUsers status={actualStatus} highlighted={highlighted} />
{queryKey && !disableDetails && (
<View
style={{

View File

@ -300,7 +300,8 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 4
minHeight: StyleConstants.Font.Size.L + StyleConstants.Spacing.S * 4,
marginHorizontal: StyleConstants.Spacing.S
}
})

View File

@ -0,0 +1,90 @@
import analytics from '@components/analytics'
import { useNavigation } from '@react-navigation/native'
import { StackNavigationProp } from '@react-navigation/stack'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, Text, View } from 'react-native'
export interface Props {
status: Mastodon.Status
highlighted: boolean
}
const TimelineActionsUsers = React.memo(
({ status, highlighted }: Props) => {
if (!highlighted) {
return null
}
const { t } = useTranslation('componentTimeline')
const { theme } = useTheme()
const navigation = useNavigation<
StackNavigationProp<Nav.TabLocalStackParamList>
>()
return (
<View style={styles.base}>
{status.reblogs_count > 0 ? (
<Text
style={[styles.text, { color: theme.secondary }]}
onPress={() => {
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
})}
</Text>
) : null}
{status.favourites_count > 0 ? (
<Text
style={[styles.text, { color: theme.secondary }]}
onPress={() => {
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
})}
</Text>
) : null}
</View>
)
},
(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

View File

@ -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,

View File

@ -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'

View File

@ -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)'
}
}
}

View File

@ -1,8 +0,0 @@
export default {
heading: {
segments: {
left: 'Following',
right: 'Followers'
}
}
}

View File

@ -0,0 +1,12 @@
export default {
heading: {
accounts: {
following: 'Following {{count}}',
followers: '{{count}} followers'
},
statuses: {
reblogged_by: '{{count}} boosted',
favourited_by: '{{count}} favourited'
}
}
}

View File

@ -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,

View File

@ -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: '显示敏感内容'

View File

@ -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)'
}
}
}

View File

@ -1,8 +0,0 @@
export default {
heading: {
segments: {
left: '关注中',
right: '关注者'
}
}
}

View File

@ -0,0 +1,12 @@
export default {
heading: {
accounts: {
following: '关注 {{count}} 人',
followers: '被 {{count}} 人关注'
},
statuses: {
reblogged_by: '{{count}} 人转嘟',
favourited_by: '{{count}} 人收藏'
}
}
}

View File

@ -48,15 +48,17 @@ const AccountInformationStats: React.FC<Props> = ({ account, myInfo }) => {
<Text
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]}
children={t('content.summary.following_count', {
count: account.following_count || 0
count: account.following_count
})}
onPress={() => {
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<Props> = ({ account, myInfo }) => {
<Text
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
children={t('content.summary.followers_count', {
count: account.followers_count || 0
count: account.followers_count
})}
onPress={() => {
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
})
}}
/>

View File

@ -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: () => (
<View style={styles.segmentsContainer}>
<SegmentedControl
appearance={mode}
values={[
t('heading.segments.left'),
t('heading.segments.right')
]}
selectedIndex={segment}
onChange={({ nativeEvent }) =>
setSegment(nativeEvent.selectedSegmentIndex)
}
/>
</View>
)
})
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 <RelationshipsList id={account.id} type={route.key} />
}
return (
<TabView
lazy
swipeEnabled
renderScene={renderScene}
renderTabBar={() => 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

View File

@ -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<Props> = ({ 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<FlatList<Mastodon.Account>>(null)
const keyExtractor = useCallback(({ id }) => id, [])
const renderItem = useCallback(
({ item }) => <ComponentAccount account={item} origin='relationship' />,
[]
)
const onEndReached = useCallback(
() => !isFetchingNextPage && fetchNextPage(),
[isFetchingNextPage]
)
const refreshControl = useMemo(
() => (
<RefreshControl refreshing={isFetching} onRefresh={() => refetch()} />
),
[isFetching]
)
useScrollToTop(flRef)
return (
<FlatList
ref={flRef}
windowSize={11}
data={flattenData}
initialNumToRender={5}
maxToRenderPerBatch={5}
style={styles.flatList}
renderItem={renderItem}
onEndReached={onEndReached}
keyExtractor={keyExtractor}
onEndReachedThreshold={0.75}
refreshControl={refreshControl}
ItemSeparatorComponent={ComponentSeparator}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 2
}}
/>
)
}
const styles = StyleSheet.create({
flatList: {
minHeight: '100%'
}
})
export default RelationshipsList

View File

@ -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 }) => <ComponentAccount account={item} origin='relationship' />,
[]
)
const onEndReached = useCallback(
() => hasNextPage && !isFetchingNextPage && fetchNextPage(),
[hasNextPage, isFetchingNextPage]
)
return (
<FlatList
windowSize={11}
data={flattenData}
initialNumToRender={5}
maxToRenderPerBatch={5}
style={styles.flatList}
renderItem={renderItem}
onEndReached={onEndReached}
onEndReachedThreshold={0.75}
ItemSeparatorComponent={ComponentSeparator}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 2
}}
/>
)
},
() => true
)
const styles = StyleSheet.create({
flatList: {
minHeight: '100%'
}
})
export default TabSharedUsers

View File

@ -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<BaseScreens, 'Tab-Shared-Toot'>
export type SharedUsersProp = StackScreenProps<BaseScreens, 'Tab-Shared-Users'>
const sharedScreens = (
Stack: TypedNavigator<
BaseScreens,
@ -133,14 +130,6 @@ const sharedScreens = (
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>,
<Stack.Screen
key='Tab-Shared-Relationships'
name='Tab-Shared-Relationships'
component={TabSharedRelationships}
options={({ navigation }: SharedRelationshipsProp) => ({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>,
<Stack.Screen
key='Tab-Shared-Search'
name='Tab-Shared-Search'
@ -210,6 +199,20 @@ const sharedScreens = (
}),
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>,
<Stack.Screen
key='Tab-Shared-Users'
name='Tab-Shared-Users'
component={TabSharedUsers}
options={({
navigation,
route: {
params: { reference, type, count }
}
}: SharedUsersProp) => ({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />,
headerTitle: t(`sharedUsers:heading.${reference}.${type}`, { count })
})}
/>
]
}

View File

@ -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<Mastodon.Account[]>({
method: 'get',
url: `accounts/${id}/${type}`,
url: `${reference}/${id}/${type}`,
params
})
}
const useRelationshipsQuery = <TData = Mastodon.Account[]>({
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 }