Refine structure

This commit is contained in:
Zhiyuan Zheng 2021-01-04 18:29:02 +01:00
parent 811964e10f
commit dcb36a682d
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
21 changed files with 561 additions and 382 deletions

39
src/@types/app.d.ts vendored
View File

@ -17,39 +17,20 @@ declare namespace App {
} }
declare namespace QueryKey { declare namespace QueryKey {
type Account = [ type Account = ['Account', { id: Mastodon.Account['id'] }]
'Account',
{
id: Mastodon.Account['id']
}
]
type Announcements = [ type Announcements = ['Announcements', { showAll?: boolean }]
'Announcements',
{
showAll?: boolean
}
]
type Application = [ type Application = ['Application', { instanceDomain: string }]
'Application',
{
instanceDomain: string
}
]
type Instance = [ type Instance = ['Instance', { instanceDomain: string }]
'Instance',
{
instanceDomain: string
}
]
type Relationship = [ type Relationship = ['Relationship', { id: Mastodon.Account['id'] }]
'Relationship',
{ type Relationships = [
id: Mastodon.Account['id'] 'Relationships',
} 'following' | 'followers',
{ id: Mastodon.Account['id'] }
] ]
type Search = [ type Search = [

View File

@ -0,0 +1,66 @@
import { ParseEmojis } from '@components/Parse'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
export interface Props {
account: Mastodon.Account
}
const ComponentAccount: React.FC<Props> = ({ account }) => {
const navigation = useNavigation()
const { theme } = useTheme()
return (
<Pressable
style={[styles.itemDefault, styles.itemAccount]}
onPress={() => {
navigation.push('Screen-Shared-Account', { account })
}}
>
<Image
source={{ uri: account.avatar_static }}
style={styles.itemAccountAvatar}
/>
<View>
<Text numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
size='S'
fontBold
/>
</Text>
<Text
numberOfLines={1}
style={[styles.itemAccountAcct, { color: theme.secondary }]}
>
@{account.acct}
</Text>
</View>
</Pressable>
)
}
const styles = StyleSheet.create({
itemDefault: {
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.M
},
itemAccount: {
flexDirection: 'row',
alignItems: 'center'
},
itemAccountAvatar: {
alignSelf: 'flex-start',
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
marginRight: StyleConstants.Spacing.S
},
itemAccountAcct: { marginTop: StyleConstants.Spacing.XS }
})
export default ComponentAccount

View File

@ -27,7 +27,8 @@ const renderNode = ({
navigation, navigation,
mentions, mentions,
tags, tags,
showFullLink showFullLink,
disableDetails
}: { }: {
theme: any theme: any
node: any node: any
@ -37,6 +38,7 @@ const renderNode = ({
mentions?: Mastodon.Mention[] mentions?: Mastodon.Mention[]
tags?: Mastodon.Tag[] tags?: Mastodon.Tag[]
showFullLink: boolean showFullLink: boolean
disableDetails: boolean
}) => { }) => {
if (node.name == 'a') { if (node.name == 'a') {
const classes = node.attribs.class const classes = node.attribs.class
@ -52,9 +54,10 @@ const renderNode = ({
}} }}
onPress={() => { onPress={() => {
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
navigation.push('Screen-Shared-Hashtag', { !disableDetails &&
hashtag: tag[1] || tag[2] navigation.push('Screen-Shared-Hashtag', {
}) hashtag: tag[1] || tag[2]
})
}} }}
> >
{node.children[0].data} {node.children[0].data}
@ -72,6 +75,7 @@ const renderNode = ({
}} }}
onPress={() => { onPress={() => {
accountIndex !== -1 && accountIndex !== -1 &&
!disableDetails &&
navigation.push('Screen-Shared-Account', { navigation.push('Screen-Shared-Account', {
account: mentions[accountIndex] account: mentions[accountIndex]
}) })
@ -96,7 +100,7 @@ const renderNode = ({
...StyleConstants.FontStyle[size] ...StyleConstants.FontStyle[size]
}} }}
onPress={async () => onPress={async () =>
!shouldBeTag !disableDetails && !shouldBeTag
? await openLink(href) ? await openLink(href)
: navigation.push('Screen-Shared-Hashtag', { : navigation.push('Screen-Shared-Hashtag', {
hashtag: content.substring(1) hashtag: content.substring(1)
@ -132,6 +136,7 @@ export interface Props {
showFullLink?: boolean showFullLink?: boolean
numberOfLines?: number numberOfLines?: number
expandHint?: string expandHint?: string
disableDetails?: boolean
} }
const ParseHTML: React.FC<Props> = ({ const ParseHTML: React.FC<Props> = ({
@ -142,7 +147,8 @@ const ParseHTML: React.FC<Props> = ({
tags, tags,
showFullLink = false, showFullLink = false,
numberOfLines = 10, numberOfLines = 10,
expandHint = '全文' expandHint = '全文',
disableDetails = false
}) => { }) => {
const navigation = useNavigation() const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
@ -157,7 +163,8 @@ const ParseHTML: React.FC<Props> = ({
navigation, navigation,
mentions, mentions,
tags, tags,
showFullLink showFullLink,
disableDetails
}), }),
[] []
) )

View File

@ -15,7 +15,7 @@ export interface Props {
const RelationshipIncoming: React.FC<Props> = ({ id }) => { const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const { t } = useTranslation() const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }] const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback( const fireMutation = useCallback(

View File

@ -14,7 +14,7 @@ export interface Props {
const RelationshipOutgoing: React.FC<Props> = ({ id }) => { const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
const { t } = useTranslation() const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }] const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
const query = useQuery(relationshipQueryKey, relationshipFetch) const query = useQuery(relationshipQueryKey, relationshipFetch)
const queryClient = useQueryClient() const queryClient = useQueryClient()

View File

@ -0,0 +1,31 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet, View } from 'react-native'
export interface Props {
extraMarginLeft?: number
extraMarginRight?: number
}
const ComponentSeparator = React.memo(
({ extraMarginLeft = 0, extraMarginRight = 0 }: Props) => {
const { theme } = useTheme()
return (
<View
style={{
borderTopColor: theme.border,
borderTopWidth: StyleSheet.hairlineWidth,
marginLeft:
StyleConstants.Spacing.Global.PagePadding + extraMarginLeft,
marginRight:
StyleConstants.Spacing.Global.PagePadding + extraMarginRight
}}
/>
)
},
() => true
)
export default ComponentSeparator

View File

@ -1,18 +1,18 @@
import ComponentSeparator from '@components/Separator'
import TimelineConversation from '@components/Timelines/Timeline/Conversation'
import TimelineDefault from '@components/Timelines/Timeline/Default'
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
import TimelineEnd from '@root/components/Timelines/Timeline/End'
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
import { useScrollToTop } from '@react-navigation/native'
import { timelineFetch } from '@utils/fetches/timelineFetch'
import { updateNotification } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback, useEffect, useMemo, useRef } from 'react' import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { RefreshControl, StyleSheet } from 'react-native' import { RefreshControl, StyleSheet } from 'react-native'
import { InfiniteData, useInfiniteQuery } from 'react-query'
import TimelineNotifications from '@components/Timelines/Timeline/Notifications'
import TimelineDefault from '@components/Timelines/Timeline/Default'
import TimelineConversation from '@components/Timelines/Timeline/Conversation'
import { timelineFetch } from '@utils/fetches/timelineFetch'
import TimelineSeparator from '@components/Timelines/Timeline/Separator'
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
import TimelineEnd from '@components/Timelines/Timeline/Shared/End'
import { useScrollToTop } from '@react-navigation/native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { InfiniteData, useInfiniteQuery } from 'react-query'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { updateNotification } from '@root/utils/slices/instancesSlice'
export type TimelineData = export type TimelineData =
| InfiniteData<{ | InfiniteData<{
@ -130,6 +130,10 @@ const Timeline: React.FC<Props> = ({
item={item} item={item}
queryKey={queryKey} queryKey={queryKey}
index={index} index={index}
{...(queryKey[0] === 'RemotePublic' && {
disableDetails: true,
disableOnPress: true
})}
{...(flattenPinnedLength && {...(flattenPinnedLength &&
flattenPinnedLength[0] && { flattenPinnedLength[0] && {
pinnedLength: flattenPinnedLength[0] pinnedLength: flattenPinnedLength[0]
@ -143,8 +147,13 @@ const Timeline: React.FC<Props> = ({
) )
const ItemSeparatorComponent = useCallback( const ItemSeparatorComponent = useCallback(
({ leadingItem }) => ( ({ leadingItem }) => (
<TimelineSeparator <ComponentSeparator
{...(toot === leadingItem.id && { highlighted: true })} {...(toot === leadingItem.id
? { extraMarginLeft: 0 }
: {
extraMarginLeft:
StyleConstants.Avatar.M + StyleConstants.Spacing.S
})}
/> />
), ),
[] []

View File

@ -15,10 +15,12 @@ import { useSelector } from 'react-redux'
export interface Props { export interface Props {
item: Mastodon.Status item: Mastodon.Status
queryKey: QueryKey.Timeline queryKey?: QueryKey.Timeline
index: number index: number
pinnedLength?: number pinnedLength?: number
highlighted?: boolean highlighted?: boolean
disableDetails?: boolean
disableOnPress?: boolean
} }
// When the poll is long // When the poll is long
@ -27,17 +29,18 @@ const TimelineDefault: React.FC<Props> = ({
queryKey, queryKey,
index, index,
pinnedLength, pinnedLength,
highlighted = false highlighted = false,
disableDetails = false,
disableOnPress = false
}) => { }) => {
const localAccountId = useSelector(getLocalAccountId) const localAccountId = useSelector(getLocalAccountId)
const isRemotePublic = queryKey[0] === 'RemotePublic'
const navigation = useNavigation() const navigation = useNavigation()
let actualStatus = item.reblog ? item.reblog : item let actualStatus = item.reblog ? item.reblog : item
const onPress = useCallback( const onPress = useCallback(
() => () =>
!isRemotePublic && !disableOnPress &&
!highlighted && !highlighted &&
navigation.push('Screen-Shared-Toot', { navigation.push('Screen-Shared-Toot', {
toot: actualStatus toot: actualStatus
@ -55,11 +58,11 @@ const TimelineDefault: React.FC<Props> = ({
<View style={styles.header}> <View style={styles.header}>
<TimelineAvatar <TimelineAvatar
{...(!isRemotePublic && { queryKey })} queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account} account={actualStatus.account}
/> />
<TimelineHeaderDefault <TimelineHeaderDefault
{...(!isRemotePublic && { queryKey })} queryKey={disableOnPress ? undefined : queryKey}
status={actualStatus} status={actualStatus}
sameAccount={actualStatus.account.id === localAccountId} sameAccount={actualStatus.account.id === localAccountId}
/> />
@ -74,9 +77,13 @@ const TimelineDefault: React.FC<Props> = ({
}} }}
> >
{actualStatus.content.length > 0 && ( {actualStatus.content.length > 0 && (
<TimelineContent status={actualStatus} highlighted={highlighted} /> <TimelineContent
status={actualStatus}
highlighted={highlighted}
disableDetails={disableDetails}
/>
)} )}
{actualStatus.poll && ( {queryKey && actualStatus.poll && (
<TimelinePoll <TimelinePoll
queryKey={queryKey} queryKey={queryKey}
poll={actualStatus.poll} poll={actualStatus.poll}
@ -84,13 +91,15 @@ const TimelineDefault: React.FC<Props> = ({
sameAccount={actualStatus.account.id === localAccountId} sameAccount={actualStatus.account.id === localAccountId}
/> />
)} )}
{actualStatus.media_attachments.length > 0 && ( {!disableDetails && actualStatus.media_attachments.length > 0 && (
<TimelineAttachment status={actualStatus} /> <TimelineAttachment status={actualStatus} />
)} )}
{actualStatus.card && <TimelineCard card={actualStatus.card} />} {!disableDetails && actualStatus.card && (
<TimelineCard card={actualStatus.card} />
)}
</View> </View>
{!isRemotePublic && ( {queryKey && !disableDetails && (
<View <View
style={{ style={{
paddingLeft: highlighted paddingLeft: highlighted

View File

@ -23,7 +23,6 @@ const TimelineEnd: React.FC<Props> = ({ hasNextPage }) => {
i18nKey='timeline:shared.end.message' // optional -> fallbacks to defaults if not provided i18nKey='timeline:shared.end.message' // optional -> fallbacks to defaults if not provided
components={[ components={[
<Icon <Icon
inline
name='Coffee' name='Coffee'
size={StyleConstants.Font.Size.S} size={StyleConstants.Font.Size.S}
color={theme.secondary} color={theme.secondary}

View File

@ -1,38 +0,0 @@
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { useTheme } from '@utils/styles/ThemeManager'
import { StyleConstants } from '@utils/styles/constants'
export interface Props {
highlighted?: boolean
}
const TimelineSeparator: React.FC<Props> = ({ highlighted = false }) => {
const { theme } = useTheme()
return (
<View
style={[
styles.base,
{
borderTopColor: theme.border,
marginLeft: highlighted
? StyleConstants.Spacing.Global.PagePadding
: StyleConstants.Spacing.Global.PagePadding +
StyleConstants.Avatar.M +
StyleConstants.Spacing.S
}
]}
/>
)
}
const styles = StyleSheet.create({
base: {
borderTopWidth: StyleSheet.hairlineWidth,
marginRight: StyleConstants.Spacing.Global.PagePadding
}
})
export default TimelineSeparator

View File

@ -8,12 +8,14 @@ export interface Props {
status: Mastodon.Status status: Mastodon.Status
numberOfLines?: number numberOfLines?: number
highlighted?: boolean highlighted?: boolean
disableDetails?: boolean
} }
const TimelineContent: React.FC<Props> = ({ const TimelineContent: React.FC<Props> = ({
status, status,
numberOfLines, numberOfLines,
highlighted = false highlighted = false,
disableDetails = false
}) => { }) => {
const { t } = useTranslation('timeline') const { t } = useTranslation('timeline')
@ -29,6 +31,7 @@ const TimelineContent: React.FC<Props> = ({
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={999} numberOfLines={999}
disableDetails={disableDetails}
/> />
</View> </View>
<ParseHTML <ParseHTML
@ -39,6 +42,7 @@ const TimelineContent: React.FC<Props> = ({
tags={status.tags} tags={status.tags}
numberOfLines={0} numberOfLines={0}
expandHint={t('shared.content.expandHint')} expandHint={t('shared.content.expandHint')}
disableDetails={disableDetails}
/> />
</> </>
) : ( ) : (
@ -49,6 +53,7 @@ const TimelineContent: React.FC<Props> = ({
mentions={status.mentions} mentions={status.mentions}
tags={status.tags} tags={status.tags}
numberOfLines={numberOfLines} numberOfLines={numberOfLines}
disableDetails={disableDetails}
/> />
)} )}
</> </>

View File

@ -1,9 +1,11 @@
import client from '@api/client' import client from '@api/client'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast' import { toast } from '@components/toast'
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 { findIndex } from 'lodash'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
@ -33,14 +35,24 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
const oldData = queryClient.getQueryData(queryKey) const oldData = queryClient.getQueryData(queryKey)
haptics('Success') haptics('Success')
queryClient.setQueryData(queryKey, (old: any) => queryClient.setQueryData<TimelineData>(queryKey, old => {
old.pages.map((paging: any) => ({ let tootIndex = -1
toots: paging.toots.filter( const pageIndex = findIndex(old?.pages, page => {
(toot: Mastodon.Conversation) => toot.id !== conversation.id const tempIndex = findIndex(page.toots, ['id', conversation.id])
), if (tempIndex >= 0) {
pointer: paging.pointer tootIndex = tempIndex
})) return true
) } else {
return false
}
})
if (pageIndex >= 0 && tootIndex >= 0) {
old!.pages[pageIndex].toots.splice(tootIndex, 1)
}
return old
})
return oldData return oldData
}, },

View File

@ -7,8 +7,8 @@ export default {
created_at: '加入时间:{{date}}', created_at: '加入时间:{{date}}',
summary: { summary: {
statuses_count: '{{count}} 条嘟文', statuses_count: '{{count}} 条嘟文',
followers_count: '关注 {{count}} 人', following_count: '关注 {{count}} 人',
following_count: '被 {{count}} 人关注' followers_count: '被 {{count}} 人关注'
}, },
segments: { segments: {
left: '所有嘟嘟', left: '所有嘟嘟',

View File

@ -1,3 +1,4 @@
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@root/utils/styles/constants' import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager' import { useTheme } from '@root/utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient' import { LinearGradient } from 'expo-linear-gradient'
@ -13,6 +14,7 @@ export interface Props {
} }
const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => { const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation('sharedAccount') const { t } = useTranslation('sharedAccount')
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient) const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
@ -55,10 +57,17 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
shimmerColors={theme.shimmer} shimmerColors={theme.shimmer}
> >
<Text <Text
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]} style={[styles.stat, { color: theme.primary, textAlign: 'right' }]}
onPress={() =>
account &&
navigation.push('Screen-Shared-Relationships', {
account,
initialType: 'following'
})
}
> >
{t('content.summary.followers_count', { {t('content.summary.following_count', {
count: account?.followers_count || 0 count: account?.following_count || 0
})} })}
</Text> </Text>
</ShimmerPlaceholder> </ShimmerPlaceholder>
@ -70,10 +79,17 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
shimmerColors={theme.shimmer} shimmerColors={theme.shimmer}
> >
<Text <Text
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]} style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
onPress={() =>
account &&
navigation.push('Screen-Shared-Relationships', {
account,
initialType: 'followers'
})
}
> >
{t('content.summary.following_count', { {t('content.summary.followers_count', {
count: account?.following_count || 0 count: account?.followers_count || 0
})} })}
</Text> </Text>
</ShimmerPlaceholder> </ShimmerPlaceholder>

View File

@ -1,13 +1,8 @@
import TimelineAttachment from '@components/Timelines/Timeline/Shared/Attachment'
import TimelineAvatar from '@components/Timelines/Timeline/Shared/Avatar'
import TimelineCard from '@components/Timelines/Timeline/Shared/Card'
import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
import TimelineHeaderDefault from '@components/Timelines/Timeline/Shared/HeaderDefault'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import ComposeContext from './utils/createContext' import ComposeContext from './utils/createContext'
import React, { useContext } from 'react' import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import TimelineDefault from '@root/components/Timelines/Timeline/Default'
const ComposeReply: React.FC = () => { const ComposeReply: React.FC = () => {
const { const {
@ -16,32 +11,20 @@ const ComposeReply: React.FC = () => {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<View style={[styles.status, { borderTopColor: theme.border }]}> <View style={[styles.base, { borderTopColor: theme.border }]}>
<TimelineAvatar account={replyToStatus!.account} /> <TimelineDefault
<View style={styles.details}> item={replyToStatus!}
<TimelineHeaderDefault status={replyToStatus!} sameAccount={false} /> index={0}
{replyToStatus!.content.length > 0 && ( disableDetails
<TimelineContent status={replyToStatus!} /> disableOnPress
)} />
{replyToStatus!.media_attachments.length > 0 && (
<TimelineAttachment status={replyToStatus!} />
)}
{replyToStatus!.card && <TimelineCard card={replyToStatus!.card} />}
</View>
</View> </View>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
status: { base: {
flex: 1, borderTopWidth: StyleSheet.hairlineWidth
flexDirection: 'row',
borderTopWidth: StyleSheet.hairlineWidth,
paddingTop: StyleConstants.Spacing.Global.PagePadding,
margin: StyleConstants.Spacing.Global.PagePadding
},
details: {
flex: 1
} }
}) })

View File

@ -0,0 +1,81 @@
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 { Dimensions, StyleSheet, View } from 'react-native'
import { TabView } from 'react-native-tab-view'
import RelationshipsList from './Relationships/List'
export interface Props {
route: {
params: {
account: Mastodon.Account
initialType: 'following' | 'followers'
}
}
}
const ScreenSharedRelationships: React.FC<Props> = ({
route: {
params: { account, initialType }
}
}) => {
console.log(account.id)
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={['关注中', '关注者']}
selectedIndex={segment}
onChange={({ nativeEvent }) =>
setSegment(nativeEvent.selectedSegmentIndex)
}
/>
</View>
)
})
return updateHeaderRight()
}, [])
const routes: { key: Props['route']['params']['initialType'] }[] = [
{ key: 'following' },
{ key: 'followers' }
]
const renderScene = ({
route
}: {
route: {
key: Props['route']['params']['initialType']
}
}) => {
return <RelationshipsList id={account.id} type={route.key} />
}
return (
<TabView
lazy
swipeEnabled
renderScene={renderScene}
renderTabBar={() => null}
initialLayout={{ width: Dimensions.get('window').width }}
navigationState={{ index: segment, routes }}
onIndexChange={index => setSegment(index)}
/>
)
}
const styles = StyleSheet.create({
segmentsContainer: {
flexBasis: '60%'
}
})
export default React.memo(ScreenSharedRelationships, () => true)

View File

@ -0,0 +1,98 @@
import ComponentAccount from '@components/Account'
import ComponentSeparator from '@components/Separator'
import TimelineEmpty from '@components/Timelines/Timeline/Empty'
import TimelineEnd from '@root/components/Timelines/Timeline/End'
import { useScrollToTop } from '@react-navigation/native'
import { relationshipsFetch } from '@utils/fetches/relationshipsFetch'
import React, { useCallback, useMemo, useRef } from 'react'
import { RefreshControl, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler'
import { useInfiniteQuery } from 'react-query'
export interface Props {
id: Mastodon.Account['id']
type: 'following' | 'followers'
}
const RelationshipsList: React.FC<Props> = ({ id, type }) => {
const queryKey: QueryKey.Relationships = ['Relationships', type, { id }]
const {
status,
data,
isFetching,
refetch,
hasNextPage,
fetchNextPage,
isFetchingNextPage
} = useInfiniteQuery(queryKey, relationshipsFetch, {
getNextPageParam: lastPage => {
return lastPage.length
? {
direction: 'next',
id: lastPage[lastPage.length - 1].id
}
: undefined
}
})
const flattenData = data?.pages ? data.pages.flatMap(d => [...d]) : []
const flRef = useRef<FlatList<any>>(null)
const keyExtractor = useCallback(({ id }) => id, [])
const renderItem = useCallback(
({ item }) => <ComponentAccount account={item} />,
[]
)
const flItemEmptyComponent = useMemo(
() => <TimelineEmpty status={status} refetch={refetch} />,
[status]
)
const onEndReached = useCallback(
() => !isFetchingNextPage && fetchNextPage(),
[isFetchingNextPage]
)
const ListFooterComponent = useCallback(
() => <TimelineEnd hasNextPage={hasNextPage} />,
[hasNextPage]
)
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}
ListFooterComponent={ListFooterComponent}
ListEmptyComponent={flItemEmptyComponent}
refreshControl={refreshControl}
ItemSeparatorComponent={ComponentSeparator}
maintainVisibleContentPosition={{
minIndexForVisible: 0,
autoscrollToTopThreshold: 2
}}
/>
)
}
const styles = StyleSheet.create({
flatList: {
minHeight: '100%'
}
})
export default RelationshipsList

View File

@ -1,24 +1,22 @@
import { HeaderRight } from '@components/Header'
import Icon from '@components/Icon'
import { ParseEmojis, ParseHTML } from '@components/Parse'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import ComponentAccount from '@root/components/Account'
import ComponentSeparator from '@root/components/Separator'
import TimelineDefault from '@root/components/Timelines/Timeline/Default'
import { searchFetch } from '@utils/fetches/searchFetch' import { searchFetch } from '@utils/fetches/searchFetch'
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, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { import {
Image,
KeyboardAvoidingView, KeyboardAvoidingView,
Pressable, Pressable,
SectionList, SectionList,
StyleSheet, StyleSheet,
Text, Text,
TextInput,
View View
} from 'react-native' } from 'react-native'
import { Chase } from 'react-native-animated-spinkit' import { Chase } from 'react-native-animated-spinkit'
import { SafeAreaView } from 'react-native-safe-area-context' import { TextInput } from 'react-native-gesture-handler'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
const ScreenSharedSearch: React.FC = () => { const ScreenSharedSearch: React.FC = () => {
@ -31,6 +29,42 @@ const ScreenSharedSearch: React.FC = () => {
{ enabled: false } { enabled: false }
) )
useEffect(() => {
const updateHeaderRight = () =>
navigation.setOptions({
headerCenter: () => (
<View style={styles.searchBar}>
<Text
style={{ ...StyleConstants.FontStyle.M, color: theme.primary }}
>
</Text>
<TextInput
style={[
styles.textInput,
{
color: theme.primary
}
]}
autoFocus
onChangeText={onChangeText}
autoCapitalize='none'
autoCorrect={false}
clearButtonMode='never'
keyboardType='web-search'
onSubmitEditing={({ nativeEvent: { text } }) =>
setSearchTerm(text)
}
placeholder={'些什么'}
placeholderTextColor={theme.secondary}
returnKeyType='go'
/>
</View>
)
})
return updateHeaderRight()
}, [])
const [setctionData, setSectionData] = useState< const [setctionData, setSectionData] = useState<
{ title: string; data: any }[] { title: string; data: any }[]
>([]) >([])
@ -74,48 +108,50 @@ const ScreenSharedSearch: React.FC = () => {
const listEmpty = useMemo( const listEmpty = useMemo(
() => ( () => (
<View style={styles.emptyBase}> <View style={styles.emptyBase}>
{status === 'loading' ? ( <View>
<View style={styles.loading}> {status === 'loading' ? (
<Chase <View style={styles.loading}>
size={StyleConstants.Font.Size.M * 1.25} <Chase
color={theme.secondary} size={StyleConstants.Font.Size.M * 1.25}
/> color={theme.secondary}
</View> />
) : ( </View>
<> ) : (
<Text <>
style={[ <Text
styles.emptyDefault, style={[
styles.emptyFontSize, styles.emptyDefault,
{ color: theme.primary } styles.emptyFontSize,
]} { color: theme.primary }
> ]}
<Text style={styles.emptyFontBold}></Text> >
<Text style={styles.emptyFontBold}></Text> <Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text> <Text style={styles.emptyFontBold}></Text>
</Text> <Text style={styles.emptyFontBold}></Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> </Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> </Text>
<Text style={{ color: theme.secondary }}>@username@domain</Text> <Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
{' '} <Text style={{ color: theme.secondary }}>@username@domain</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> </Text>
<Text style={{ color: theme.secondary }}>#example</Text> <Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
{' '} <Text style={{ color: theme.secondary }}>#example</Text>
</Text> {' '}
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> </Text>
<Text style={{ color: theme.secondary }}>URL</Text> <Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
{' '} <Text style={{ color: theme.secondary }}>URL</Text>
</Text> {' '}
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}> </Text>
<Text style={{ color: theme.secondary }}>URL</Text> <Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
{' '} <Text style={{ color: theme.secondary }}>URL</Text>
</Text> {' '}
</> </Text>
)} </>
)}
</View>
</View> </View>
), ),
[status] [status]
@ -123,10 +159,7 @@ const ScreenSharedSearch: React.FC = () => {
const sectionHeader = useCallback( const sectionHeader = useCallback(
({ section: { title } }) => ( ({ section: { title } }) => (
<View <View
style={[ style={[styles.sectionHeader, { backgroundColor: theme.background }]}
styles.sectionHeader,
{ borderBottomColor: theme.border, backgroundColor: theme.background }
]}
> >
<Text style={[styles.sectionHeaderText, { color: theme.primary }]}> <Text style={[styles.sectionHeaderText, { color: theme.primary }]}>
{title} {title}
@ -152,43 +185,10 @@ const ScreenSharedSearch: React.FC = () => {
) : null, ) : null,
[searchTerm] [searchTerm]
) )
const listItem = useCallback(({ item, section }) => { const listItem = useCallback(({ item, section, index }) => {
switch (section.title) { switch (section.title) {
case 'accounts': case 'accounts':
return ( return <ComponentAccount account={item} />
<Pressable
style={[
styles.itemDefault,
styles.itemAccount,
{ borderBottomColor: theme.border }
]}
onPress={() => {
navigation.goBack()
navigation.push('Screen-Shared-Account', { account: item })
}}
>
<Image
source={{ uri: item.avatar_static }}
style={styles.itemAccountAvatar}
/>
<View>
<Text numberOfLines={1}>
<ParseEmojis
content={item.display_name || item.username}
emojis={item.emojis}
size='S'
fontBold
/>
</Text>
<Text
numberOfLines={1}
style={[styles.itemAccountAcct, { color: theme.secondary }]}
>
@{item.acct}
</Text>
</View>
</Pressable>
)
case 'hashtags': case 'hashtags':
return ( return (
<Pressable <Pressable
@ -206,50 +206,7 @@ const ScreenSharedSearch: React.FC = () => {
</Pressable> </Pressable>
) )
case 'statuses': case 'statuses':
return ( return <TimelineDefault item={item} index={index} disableDetails />
<Pressable
style={[
styles.itemDefault,
styles.itemAccount,
{ borderBottomColor: theme.border }
]}
onPress={() => {
navigation.goBack()
navigation.push('Screen-Shared-Toot', { toot: item })
}}
>
<Image
source={{ uri: item.account.avatar_static }}
style={styles.itemAccountAvatar}
/>
<View>
<Text numberOfLines={1}>
<ParseEmojis
content={item.account.display_name || item.account.username}
emojis={item.account.emojis}
size='S'
fontBold
/>
</Text>
<Text
numberOfLines={1}
style={[styles.itemAccountAcct, { color: theme.secondary }]}
>
@{item.account.acct}
</Text>
{item.content && (
<View style={styles.itemStatus}>
<ParseHTML
content={item.content}
size='M'
emojis={item.emojis}
numberOfLines={2}
/>
</View>
)}
</View>
</Pressable>
)
default: default:
return null return null
} }
@ -257,100 +214,42 @@ const ScreenSharedSearch: React.FC = () => {
return ( return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}> <SectionList
<View style={styles.searchBar}> style={styles.base}
<View renderItem={listItem}
style={[styles.searchField, { borderBottomColor: theme.secondary }]} stickySectionHeadersEnabled
> sections={setctionData}
<Icon ListEmptyComponent={listEmpty}
name='Search' keyboardShouldPersistTaps='always'
color={theme.primary} renderSectionHeader={sectionHeader}
size={StyleConstants.Font.Size.M} renderSectionFooter={sectionFooter}
style={styles.searchIcon} keyExtractor={(item, index) => item + index}
/> SectionSeparatorComponent={ComponentSeparator}
<TextInput ItemSeparatorComponent={ComponentSeparator}
style={[ />
styles.textInput,
{
color: theme.primary
}
]}
autoFocus
onChangeText={onChangeText}
autoCapitalize='none'
autoCorrect={false}
clearButtonMode='never'
keyboardType='web-search'
onSubmitEditing={({ nativeEvent: { text } }) =>
setSearchTerm(text)
}
placeholder={'搜索些什么'}
placeholderTextColor={theme.secondary}
returnKeyType='go'
/>
</View>
<View style={styles.searchCancel}>
<HeaderRight
type='text'
content='取消'
onPress={() => navigation.goBack()}
/>
</View>
</View>
<SectionList
style={styles.base}
renderItem={listItem}
stickySectionHeadersEnabled
sections={setctionData}
ListEmptyComponent={listEmpty}
keyboardShouldPersistTaps='always'
renderSectionHeader={sectionHeader}
renderSectionFooter={sectionFooter}
keyExtractor={(item, index) => item + index}
/>
</SafeAreaView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
base: { base: {
flex: 1, minHeight: '100%'
padding: StyleConstants.Spacing.Global.PagePadding,
paddingTop: 0
}, },
searchBar: { searchBar: {
padding: StyleConstants.Spacing.Global.PagePadding, flexBasis: '80%',
paddingBottom: 0,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center' alignItems: 'center'
}, },
searchField: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderBottomWidth: 1.25
},
searchIcon: {
marginLeft: StyleConstants.Spacing.S
},
searchCancel: {
paddingHorizontal: StyleConstants.Spacing.S,
marginLeft: StyleConstants.Spacing.S
},
textInput: { textInput: {
flex: 1,
padding: StyleConstants.Spacing.S,
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.S paddingLeft: StyleConstants.Spacing.XS,
marginBottom:
(StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2
}, },
emptyBase: { emptyBase: {
marginTop: StyleConstants.Spacing.M, marginVertical: StyleConstants.Spacing.Global.PagePadding,
marginLeft: // paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
StyleConstants.Spacing.S + alignItems: 'center'
StyleConstants.Spacing.M +
StyleConstants.Spacing.S
}, },
loading: { flex: 1, alignItems: 'center' }, loading: { flex: 1, alignItems: 'center' },
emptyFontSize: { ...StyleConstants.FontStyle.S }, emptyFontSize: { ...StyleConstants.FontStyle.S },
@ -364,8 +263,7 @@ const styles = StyleSheet.create({
marginBottom: StyleConstants.Spacing.S marginBottom: StyleConstants.Spacing.S
}, },
sectionHeader: { sectionHeader: {
padding: StyleConstants.Spacing.M, padding: StyleConstants.Spacing.M
borderBottomWidth: StyleSheet.hairlineWidth
}, },
sectionHeaderText: { sectionHeaderText: {
...StyleConstants.FontStyle.M, ...StyleConstants.FontStyle.M,
@ -383,23 +281,8 @@ const styles = StyleSheet.create({
padding: StyleConstants.Spacing.S * 1.5, padding: StyleConstants.Spacing.S * 1.5,
borderBottomWidth: StyleSheet.hairlineWidth borderBottomWidth: StyleSheet.hairlineWidth
}, },
itemAccount: {
flexDirection: 'row',
alignItems: 'center'
},
itemAccountAvatar: {
alignSelf: 'flex-start',
width: StyleConstants.Avatar.S,
height: StyleConstants.Avatar.S,
borderRadius: 6,
marginRight: StyleConstants.Spacing.S
},
itemAccountAcct: { marginTop: StyleConstants.Spacing.XS },
itemHashtag: { itemHashtag: {
...StyleConstants.FontStyle.M ...StyleConstants.FontStyle.M
},
itemStatus: {
marginTop: StyleConstants.Spacing.S
} }
}) })

View File

@ -1,15 +1,15 @@
import { HeaderLeft } from '@root/components/Header' import { HeaderLeft, HeaderRight } from '@components/Header'
import ScreenSharedAccount from '@screens/Shared/Account' import ScreenSharedAccount from '@screens/Shared/Account'
import ScreenSharedAnnouncements from '@root/screens/Shared/Announcements' import ScreenSharedAnnouncements from '@screens/Shared/Announcements'
import ScreenSharedHashtag from '@screens/Shared/Hashtag' import ScreenSharedHashtag from '@screens/Shared/Hashtag'
import ScreenSharedImagesViewer from '@screens/Shared/ImagesViewer'
import ScreenSharedRelationships from '@screens/Shared/Relationships'
import ScreenSharedToot from '@screens/Shared/Toot' import ScreenSharedToot from '@screens/Shared/Toot'
import Compose from '@screens/Shared/Compose' import Compose from '@screens/Shared/Compose'
import ComposeEditAttachment from '@screens/Shared/Compose/EditAttachment'
import ScreenSharedSearch from '@screens/Shared/Search' import ScreenSharedSearch from '@screens/Shared/Search'
import React from 'react' import React from 'react'
import { Text } from 'react-native'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ScreenSharedImagesViewer from './ImagesViewer' import { View } from 'react-native'
const sharedScreens = (Stack: any) => { const sharedScreens = (Stack: any) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -60,9 +60,9 @@ const sharedScreens = (Stack: any) => {
key='Screen-Shared-Search' key='Screen-Shared-Search'
name='Screen-Shared-Search' name='Screen-Shared-Search'
component={ScreenSharedSearch} component={ScreenSharedSearch}
options={{ options={({ navigation }: any) => ({
stackPresentation: 'modal' headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
}} })}
/>, />,
<Stack.Screen <Stack.Screen
key='Screen-Shared-Announcements' key='Screen-Shared-Announcements'
@ -81,6 +81,15 @@ const sharedScreens = (Stack: any) => {
stackPresentation: 'transparentModal', stackPresentation: 'transparentModal',
stackAnimation: 'none' stackAnimation: 'none'
}} }}
/>,
<Stack.Screen
key='Screen-Shared-Relationships'
name='Screen-Shared-Relationships'
component={ScreenSharedRelationships}
options={({ route, navigation }: any) => ({
title: route.params.account.display_name || route.params.account.name,
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/> />
] ]
} }

View File

@ -0,0 +1,28 @@
import client from '@api/client'
export const relationshipsFetch = async ({
queryKey,
pageParam
}: {
queryKey: QueryKey.Relationships
pageParam?: { direction: 'next'; id: Mastodon.Status['id'] }
}): Promise<Mastodon.Account[]> => {
const [_, type, { id }] = queryKey
let params: { [key: string]: string } = {}
if (pageParam) {
switch (pageParam.direction) {
case 'next':
params.max_id = pageParam.id
break
}
}
const res = await client({
method: 'get',
instance: 'local',
url: `accounts/${id}/${type}`,
params
})
return Promise.resolve(res.body)
}

View File

@ -3,7 +3,7 @@ const Base = 4
export const StyleConstants = { export const StyleConstants = {
Font: { Font: {
Size: { S: 14, M: 16, L: 18 }, Size: { S: 14, M: 16, L: 18 },
LineHeight: { S: 18, M: 22, L: 30 }, LineHeight: { S: 18, M: 22, L: 26 },
Weight: { Bold: '600' as '600' } Weight: { Bold: '600' as '600' }
}, },
FontStyle: { FontStyle: {
@ -21,5 +21,5 @@ export const StyleConstants = {
Global: { PagePadding: Base * 4 } Global: { PagePadding: Base * 4 }
}, },
Avatar: { S: 36, M: 52, L: 104 } Avatar: { S: 40, M: 52, L: 104 }
} }