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

View File

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

View File

@ -14,7 +14,7 @@ export interface Props {
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }]
const relationshipQueryKey: QueryKey.Relationship = ['Relationship', { id }]
const query = useQuery(relationshipQueryKey, relationshipFetch)
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 { 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 { InfiniteData, useInfiniteQuery } from 'react-query'
import { useDispatch } from 'react-redux'
import { updateNotification } from '@root/utils/slices/instancesSlice'
export type TimelineData =
| InfiniteData<{
@ -130,6 +130,10 @@ const Timeline: React.FC<Props> = ({
item={item}
queryKey={queryKey}
index={index}
{...(queryKey[0] === 'RemotePublic' && {
disableDetails: true,
disableOnPress: true
})}
{...(flattenPinnedLength &&
flattenPinnedLength[0] && {
pinnedLength: flattenPinnedLength[0]
@ -143,8 +147,13 @@ const Timeline: React.FC<Props> = ({
)
const ItemSeparatorComponent = useCallback(
({ leadingItem }) => (
<TimelineSeparator
{...(toot === leadingItem.id && { highlighted: true })}
<ComponentSeparator
{...(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 {
item: Mastodon.Status
queryKey: QueryKey.Timeline
queryKey?: QueryKey.Timeline
index: number
pinnedLength?: number
highlighted?: boolean
disableDetails?: boolean
disableOnPress?: boolean
}
// When the poll is long
@ -27,17 +29,18 @@ const TimelineDefault: React.FC<Props> = ({
queryKey,
index,
pinnedLength,
highlighted = false
highlighted = false,
disableDetails = false,
disableOnPress = false
}) => {
const localAccountId = useSelector(getLocalAccountId)
const isRemotePublic = queryKey[0] === 'RemotePublic'
const navigation = useNavigation()
let actualStatus = item.reblog ? item.reblog : item
const onPress = useCallback(
() =>
!isRemotePublic &&
!disableOnPress &&
!highlighted &&
navigation.push('Screen-Shared-Toot', {
toot: actualStatus
@ -55,11 +58,11 @@ const TimelineDefault: React.FC<Props> = ({
<View style={styles.header}>
<TimelineAvatar
{...(!isRemotePublic && { queryKey })}
queryKey={disableOnPress ? undefined : queryKey}
account={actualStatus.account}
/>
<TimelineHeaderDefault
{...(!isRemotePublic && { queryKey })}
queryKey={disableOnPress ? undefined : queryKey}
status={actualStatus}
sameAccount={actualStatus.account.id === localAccountId}
/>
@ -74,9 +77,13 @@ const TimelineDefault: React.FC<Props> = ({
}}
>
{actualStatus.content.length > 0 && (
<TimelineContent status={actualStatus} highlighted={highlighted} />
<TimelineContent
status={actualStatus}
highlighted={highlighted}
disableDetails={disableDetails}
/>
)}
{actualStatus.poll && (
{queryKey && actualStatus.poll && (
<TimelinePoll
queryKey={queryKey}
poll={actualStatus.poll}
@ -84,13 +91,15 @@ const TimelineDefault: React.FC<Props> = ({
sameAccount={actualStatus.account.id === localAccountId}
/>
)}
{actualStatus.media_attachments.length > 0 && (
{!disableDetails && actualStatus.media_attachments.length > 0 && (
<TimelineAttachment status={actualStatus} />
)}
{actualStatus.card && <TimelineCard card={actualStatus.card} />}
{!disableDetails && actualStatus.card && (
<TimelineCard card={actualStatus.card} />
)}
</View>
{!isRemotePublic && (
{queryKey && !disableDetails && (
<View
style={{
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
components={[
<Icon
inline
name='Coffee'
size={StyleConstants.Font.Size.S}
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
numberOfLines?: number
highlighted?: boolean
disableDetails?: boolean
}
const TimelineContent: React.FC<Props> = ({
status,
numberOfLines,
highlighted = false
highlighted = false,
disableDetails = false
}) => {
const { t } = useTranslation('timeline')
@ -29,6 +31,7 @@ const TimelineContent: React.FC<Props> = ({
mentions={status.mentions}
tags={status.tags}
numberOfLines={999}
disableDetails={disableDetails}
/>
</View>
<ParseHTML
@ -39,6 +42,7 @@ const TimelineContent: React.FC<Props> = ({
tags={status.tags}
numberOfLines={0}
expandHint={t('shared.content.expandHint')}
disableDetails={disableDetails}
/>
</>
) : (
@ -49,6 +53,7 @@ const TimelineContent: React.FC<Props> = ({
mentions={status.mentions}
tags={status.tags}
numberOfLines={numberOfLines}
disableDetails={disableDetails}
/>
)}
</>

View File

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

View File

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

View File

@ -1,3 +1,4 @@
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
@ -13,6 +14,7 @@ export interface Props {
}
const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
const navigation = useNavigation()
const { theme } = useTheme()
const { t } = useTranslation('sharedAccount')
const ShimmerPlaceholder = createShimmerPlaceholder(LinearGradient)
@ -55,10 +57,17 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
shimmerColors={theme.shimmer}
>
<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', {
count: account?.followers_count || 0
{t('content.summary.following_count', {
count: account?.following_count || 0
})}
</Text>
</ShimmerPlaceholder>
@ -70,10 +79,17 @@ const AccountInformationStats = forwardRef<any, Props>(({ account }, ref) => {
shimmerColors={theme.shimmer}
>
<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', {
count: account?.following_count || 0
{t('content.summary.followers_count', {
count: account?.followers_count || 0
})}
</Text>
</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 ComposeContext from './utils/createContext'
import React, { useContext } from 'react'
import { StyleSheet, View } from 'react-native'
import TimelineDefault from '@root/components/Timelines/Timeline/Default'
const ComposeReply: React.FC = () => {
const {
@ -16,32 +11,20 @@ const ComposeReply: React.FC = () => {
const { theme } = useTheme()
return (
<View style={[styles.status, { borderTopColor: theme.border }]}>
<TimelineAvatar account={replyToStatus!.account} />
<View style={styles.details}>
<TimelineHeaderDefault status={replyToStatus!} sameAccount={false} />
{replyToStatus!.content.length > 0 && (
<TimelineContent status={replyToStatus!} />
)}
{replyToStatus!.media_attachments.length > 0 && (
<TimelineAttachment status={replyToStatus!} />
)}
{replyToStatus!.card && <TimelineCard card={replyToStatus!.card} />}
</View>
<View style={[styles.base, { borderTopColor: theme.border }]}>
<TimelineDefault
item={replyToStatus!}
index={0}
disableDetails
disableOnPress
/>
</View>
)
}
const styles = StyleSheet.create({
status: {
flex: 1,
flexDirection: 'row',
borderTopWidth: StyleSheet.hairlineWidth,
paddingTop: StyleConstants.Spacing.Global.PagePadding,
margin: StyleConstants.Spacing.Global.PagePadding
},
details: {
flex: 1
base: {
borderTopWidth: StyleSheet.hairlineWidth
}
})

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 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 { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { debounce } from 'lodash'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import {
Image,
KeyboardAvoidingView,
Pressable,
SectionList,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
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'
const ScreenSharedSearch: React.FC = () => {
@ -31,6 +29,42 @@ const ScreenSharedSearch: React.FC = () => {
{ 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<
{ title: string; data: any }[]
>([])
@ -74,48 +108,50 @@ const ScreenSharedSearch: React.FC = () => {
const listEmpty = useMemo(
() => (
<View style={styles.emptyBase}>
{status === 'loading' ? (
<View style={styles.loading}>
<Chase
size={StyleConstants.Font.Size.M * 1.25}
color={theme.secondary}
/>
</View>
) : (
<>
<Text
style={[
styles.emptyDefault,
styles.emptyFontSize,
{ color: theme.primary }
]}
>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
</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 style={{ color: theme.secondary }}>#example</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
</>
)}
<View>
{status === 'loading' ? (
<View style={styles.loading}>
<Chase
size={StyleConstants.Font.Size.M * 1.25}
color={theme.secondary}
/>
</View>
) : (
<>
<Text
style={[
styles.emptyDefault,
styles.emptyFontSize,
{ color: theme.primary }
]}
>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
<Text style={styles.emptyFontBold}></Text>
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
</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 style={{ color: theme.secondary }}>#example</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
<Text style={[styles.emptyAdvanced, { color: theme.primary }]}>
<Text style={{ color: theme.secondary }}>URL</Text>
{' '}
</Text>
</>
)}
</View>
</View>
),
[status]
@ -123,10 +159,7 @@ const ScreenSharedSearch: React.FC = () => {
const sectionHeader = useCallback(
({ section: { title } }) => (
<View
style={[
styles.sectionHeader,
{ borderBottomColor: theme.border, backgroundColor: theme.background }
]}
style={[styles.sectionHeader, { backgroundColor: theme.background }]}
>
<Text style={[styles.sectionHeaderText, { color: theme.primary }]}>
{title}
@ -152,43 +185,10 @@ const ScreenSharedSearch: React.FC = () => {
) : null,
[searchTerm]
)
const listItem = useCallback(({ item, section }) => {
const listItem = useCallback(({ item, section, index }) => {
switch (section.title) {
case 'accounts':
return (
<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>
)
return <ComponentAccount account={item} />
case 'hashtags':
return (
<Pressable
@ -206,50 +206,7 @@ const ScreenSharedSearch: React.FC = () => {
</Pressable>
)
case 'statuses':
return (
<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>
)
return <TimelineDefault item={item} index={index} disableDetails />
default:
return null
}
@ -257,100 +214,42 @@ const ScreenSharedSearch: React.FC = () => {
return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView style={{ flex: 1 }} edges={['bottom']}>
<View style={styles.searchBar}>
<View
style={[styles.searchField, { borderBottomColor: theme.secondary }]}
>
<Icon
name='Search'
color={theme.primary}
size={StyleConstants.Font.Size.M}
style={styles.searchIcon}
/>
<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>
<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>
<SectionList
style={styles.base}
renderItem={listItem}
stickySectionHeadersEnabled
sections={setctionData}
ListEmptyComponent={listEmpty}
keyboardShouldPersistTaps='always'
renderSectionHeader={sectionHeader}
renderSectionFooter={sectionFooter}
keyExtractor={(item, index) => item + index}
SectionSeparatorComponent={ComponentSeparator}
ItemSeparatorComponent={ComponentSeparator}
/>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding,
paddingTop: 0
minHeight: '100%'
},
searchBar: {
padding: StyleConstants.Spacing.Global.PagePadding,
paddingBottom: 0,
flexBasis: '80%',
flexDirection: 'row',
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: {
flex: 1,
padding: StyleConstants.Spacing.S,
...StyleConstants.FontStyle.M,
marginRight: StyleConstants.Spacing.S
paddingLeft: StyleConstants.Spacing.XS,
marginBottom:
(StyleConstants.Font.LineHeight.M - StyleConstants.Font.Size.M) / 2
},
emptyBase: {
marginTop: StyleConstants.Spacing.M,
marginLeft:
StyleConstants.Spacing.S +
StyleConstants.Spacing.M +
StyleConstants.Spacing.S
marginVertical: StyleConstants.Spacing.Global.PagePadding,
// paddingHorizontal: StyleConstants.Spacing.Global.PagePadding
alignItems: 'center'
},
loading: { flex: 1, alignItems: 'center' },
emptyFontSize: { ...StyleConstants.FontStyle.S },
@ -364,8 +263,7 @@ const styles = StyleSheet.create({
marginBottom: StyleConstants.Spacing.S
},
sectionHeader: {
padding: StyleConstants.Spacing.M,
borderBottomWidth: StyleSheet.hairlineWidth
padding: StyleConstants.Spacing.M
},
sectionHeaderText: {
...StyleConstants.FontStyle.M,
@ -383,23 +281,8 @@ const styles = StyleSheet.create({
padding: StyleConstants.Spacing.S * 1.5,
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: {
...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 ScreenSharedAnnouncements from '@root/screens/Shared/Announcements'
import ScreenSharedAnnouncements from '@screens/Shared/Announcements'
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 Compose from '@screens/Shared/Compose'
import ComposeEditAttachment from '@screens/Shared/Compose/EditAttachment'
import ScreenSharedSearch from '@screens/Shared/Search'
import React from 'react'
import { Text } from 'react-native'
import { useTranslation } from 'react-i18next'
import ScreenSharedImagesViewer from './ImagesViewer'
import { View } from 'react-native'
const sharedScreens = (Stack: any) => {
const { t } = useTranslation()
@ -60,9 +60,9 @@ const sharedScreens = (Stack: any) => {
key='Screen-Shared-Search'
name='Screen-Shared-Search'
component={ScreenSharedSearch}
options={{
stackPresentation: 'modal'
}}
options={({ navigation }: any) => ({
headerLeft: () => <HeaderLeft onPress={() => navigation.goBack()} />
})}
/>,
<Stack.Screen
key='Screen-Shared-Announcements'
@ -81,6 +81,15 @@ const sharedScreens = (Stack: any) => {
stackPresentation: 'transparentModal',
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 = {
Font: {
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' }
},
FontStyle: {
@ -21,5 +21,5 @@ export const StyleConstants = {
Global: { PagePadding: Base * 4 }
},
Avatar: { S: 36, M: 52, L: 104 }
Avatar: { S: 40, M: 52, L: 104 }
}