mirror of https://github.com/tooot-app/app
Refine structure
This commit is contained in:
parent
811964e10f
commit
dcb36a682d
|
@ -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 = [
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
|
@ -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
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: '所有嘟嘟',
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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()} />
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue