Show pinned posts in Account page

This commit is contained in:
Zhiyuan Zheng 2020-12-14 23:44:57 +01:00
parent 177afe1dd1
commit fe1ca72a3e
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
5 changed files with 226 additions and 126 deletions

View File

@ -65,13 +65,14 @@ const Timeline: React.FC<Props> = ({
})
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
const flattenPinnedLength = data ? data.flatMap(d => [d?.pinnedLength]) : []
const flRef = useRef<FlatList>(null)
useEffect(() => {
if (toot && isSuccess) {
setTimeout(() => {
flRef.current?.scrollToIndex({
index: flattenPointer[0],
index: flattenPointer[0]!,
viewOffset: 100
})
}, 500)
@ -79,7 +80,7 @@ const Timeline: React.FC<Props> = ({
}, [isSuccess])
const flKeyExtrator = useCallback(({ id }) => id, [])
const flRenderItem = useCallback(({ item }) => {
const flRenderItem = useCallback(({ item, index }) => {
switch (page) {
case 'Conversations':
return <TimelineConversation item={item} queryKey={queryKey} />
@ -90,6 +91,11 @@ const Timeline: React.FC<Props> = ({
<TimelineDefault
item={item}
queryKey={queryKey}
index={index}
{...(flattenPinnedLength &&
flattenPinnedLength[0] && {
pinnedLength: flattenPinnedLength[0]
})}
{...(toot && toot.id === item.id && { highlighted: true })}
/>
)

View File

@ -16,6 +16,8 @@ import { StyleConstants } from '@utils/styles/constants'
export interface Props {
item: Mastodon.Status
queryKey: App.QueryKey
index: number
pinnedLength?: number
highlighted?: boolean
}
@ -23,6 +25,8 @@ export interface Props {
const TimelineDefault: React.FC<Props> = ({
item,
queryKey,
index,
pinnedLength,
highlighted = false
}) => {
const isRemotePublic = queryKey[0] === 'RemotePublic'
@ -72,9 +76,11 @@ const TimelineDefault: React.FC<Props> = ({
return (
<View style={styles.statusView}>
{item.reblog && (
{item.reblog ? (
<TimelineActioned action='reblog' account={item.account} />
)}
) : pinnedLength && index < pinnedLength ? (
<TimelineActioned action='pinned' account={item.account} />
) : null}
<View style={styles.header}>
<TimelineAvatar

View File

@ -8,13 +8,12 @@ import { StyleConstants } from '@utils/styles/constants'
export interface Props {
account: Mastodon.Account
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog'
action: 'favourite' | 'follow' | 'mention' | 'poll' | 'reblog' | 'pinned'
notification?: boolean
}
const TimelineActioned: React.FC<Props> = ({
account,
action,
notification = false
}) => {
@ -25,6 +24,17 @@ const TimelineActioned: React.FC<Props> = ({
let icon
let content
switch (action) {
case 'pinned':
icon = (
<Feather
name='anchor'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
)
content = `置顶`
break
case 'favourite':
icon = (
<Feather

View File

@ -1,5 +1,5 @@
import React, { useState } from 'react'
import { Image, StyleSheet, Text, View } from 'react-native'
import React, { createRef, useEffect, useState } from 'react'
import { Animated, Image, StyleSheet, Text, View } from 'react-native'
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
import { Feather } from '@expo/vector-icons'
@ -18,10 +18,37 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
const { theme } = useTheme()
const [avatarLoaded, setAvatarLoaded] = useState(false)
const shimmerAvatarRef = createRef<ShimmerPlaceholder>()
const shimmerNameRef = createRef<ShimmerPlaceholder>()
const shimmerAccountRef = createRef<ShimmerPlaceholder>()
const shimmerCreatedRef = createRef<ShimmerPlaceholder>()
const shimmerStatTootRef = createRef<ShimmerPlaceholder>()
const shimmerStatFolloingRef = createRef<ShimmerPlaceholder>()
const shimmerStatFollowerRef = createRef<ShimmerPlaceholder>()
useEffect(() => {
const informationAnimated = Animated.stagger(400, [
Animated.parallel([
shimmerAvatarRef.current!.getAnimated(),
shimmerNameRef.current!.getAnimated(),
shimmerAccountRef.current!.getAnimated(),
shimmerCreatedRef.current!.getAnimated(),
shimmerStatTootRef.current!.getAnimated(),
shimmerStatFolloingRef.current!.getAnimated(),
shimmerStatFollowerRef.current!.getAnimated()
])
])
Animated.loop(informationAnimated).start()
}, [])
return (
<View style={styles.information}>
{/* <Text>Moved or not: {account.moved}</Text> */}
<ShimmerPlaceholder visible={avatarLoaded} width={90} height={90}>
<ShimmerPlaceholder
ref={shimmerAvatarRef}
visible={avatarLoaded}
width={StyleConstants.Avatar.L}
height={StyleConstants.Avatar.L}
>
<Image
source={{ uri: account?.avatar }}
style={styles.avatar}
@ -29,52 +56,68 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
/>
</ShimmerPlaceholder>
<View style={styles.display_name}>
{account?.emojis ? (
<Emojis
content={account?.display_name || account?.username}
emojis={account.emojis}
size={StyleConstants.Font.Size.L}
fontBold={true}
/>
) : (
<ShimmerPlaceholder
ref={shimmerNameRef}
visible={account !== undefined}
width={StyleConstants.Font.Size.L * 8}
height={StyleConstants.Font.Size.L}
style={styles.display_name}
>
<View>
{account?.emojis ? (
<Emojis
content={account?.display_name || account?.username}
emojis={account.emojis}
size={StyleConstants.Font.Size.L}
fontBold={true}
/>
) : (
<Text
style={{
color: theme.primary,
fontSize: StyleConstants.Font.Size.L,
fontWeight: StyleConstants.Font.Weight.Bold
}}
>
{account?.display_name || account?.username}
</Text>
)}
</View>
</ShimmerPlaceholder>
<ShimmerPlaceholder
ref={shimmerAccountRef}
visible={account !== undefined}
width={StyleConstants.Font.Size.M * 8}
height={StyleConstants.Font.Size.M}
style={{ marginBottom: StyleConstants.Spacing.L }}
>
<View style={styles.account}>
<Text
style={{
color: theme.primary,
fontSize: StyleConstants.Font.Size.L,
fontWeight: StyleConstants.Font.Weight.Bold
color: theme.secondary,
fontSize: StyleConstants.Font.Size.M
}}
selectable
>
{account?.display_name || account?.username}
@{account?.acct}
</Text>
)}
</View>
<View style={styles.account}>
<Text
style={{
color: theme.secondary,
fontSize: StyleConstants.Font.Size.M
}}
selectable
>
@{account?.acct}
</Text>
{account?.locked && (
<Feather
name='lock'
style={styles.account_types}
color={theme.secondary}
/>
)}
{account?.bot && (
<Feather
name='hard-drive'
style={styles.account_types}
color={theme.secondary}
/>
)}
</View>
{account?.locked && (
<Feather
name='lock'
style={styles.account_types}
color={theme.secondary}
/>
)}
{account?.bot && (
<Feather
name='hard-drive'
style={styles.account_types}
color={theme.secondary}
/>
)}
</View>
</ShimmerPlaceholder>
{account?.fields && account.fields.length > 0 && (
<View style={[styles.fields, { borderTopColor: theme.border }]}>
@ -84,15 +127,7 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
style={[styles.field, { borderBottomColor: theme.border }]}
>
<View
style={{
flexBasis: '30%',
alignItems: 'center',
justifyContent: 'center',
borderRightWidth: 1,
borderRightColor: theme.border,
paddingLeft: StyleConstants.Spacing.S,
paddingRight: StyleConstants.Spacing.S
}}
style={[styles.fieldLeft, { borderRightColor: theme.border }]}
>
<ParseContent
content={field.name}
@ -105,18 +140,11 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
name='check-circle'
size={StyleConstants.Font.Size.M}
color={theme.primary}
style={styles.fieldCheck}
/>
)}
</View>
<View
style={{
flexBasis: '70%',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: StyleConstants.Spacing.S,
paddingRight: StyleConstants.Spacing.S
}}
>
<View style={styles.fieldRight}>
<ParseContent
content={field.value}
size={'M'}
@ -139,48 +167,78 @@ const AccountInformation: React.FC<Props> = ({ account }) => {
</View>
)}
<View style={styles.created_at}>
<Feather
name='calendar'
size={StyleConstants.Font.Size.M + 2}
color={theme.secondary}
style={styles.created_at_icon}
/>
<Text
style={{
color: theme.secondary,
fontSize: StyleConstants.Font.Size.M
}}
>
{t(
'content.created_at',
{
<ShimmerPlaceholder
ref={shimmerCreatedRef}
visible={account !== undefined}
width={StyleConstants.Font.Size.S * 8}
height={StyleConstants.Font.Size.S}
style={{ marginBottom: StyleConstants.Spacing.M }}
>
<View style={styles.created_at}>
<Feather
name='calendar'
size={StyleConstants.Font.Size.S}
color={theme.secondary}
style={styles.created_at_icon}
/>
<Text
style={{
color: theme.secondary,
fontSize: StyleConstants.Font.Size.S
}}
>
{t('content.created_at', {
date: new Date(account?.created_at!).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
} || null
)}
</Text>
</View>
})}
</Text>
</View>
</ShimmerPlaceholder>
<View style={styles.summary}>
<Text style={{ color: theme.primary }}>
{t('content.summary.statuses_count', {
count: account?.statuses_count || 0
})}
</Text>
<Text style={{ color: theme.primary, textAlign: 'center' }}>
{t('content.summary.followers_count', {
count: account?.followers_count || 0
})}
</Text>
<Text style={{ color: theme.primary, textAlign: 'right' }}>
{t('content.summary.following_count', {
count: account?.following_count || 0
})}
</Text>
<View style={styles.stats}>
<ShimmerPlaceholder
ref={shimmerStatTootRef}
visible={account !== undefined}
width={StyleConstants.Font.Size.S * 5}
height={StyleConstants.Font.Size.S}
>
<Text style={[styles.stat, { color: theme.primary }]}>
{t('content.summary.statuses_count', {
count: account?.statuses_count || 0
})}
</Text>
</ShimmerPlaceholder>
<ShimmerPlaceholder
ref={shimmerStatFolloingRef}
visible={account !== undefined}
width={StyleConstants.Font.Size.S * 5}
height={StyleConstants.Font.Size.S}
>
<Text
style={[styles.stat, { color: theme.primary, textAlign: 'center' }]}
>
{t('content.summary.followers_count', {
count: account?.followers_count || 0
})}
</Text>
</ShimmerPlaceholder>
<ShimmerPlaceholder
ref={shimmerStatFollowerRef}
visible={account !== undefined}
width={StyleConstants.Font.Size.S * 5}
height={StyleConstants.Font.Size.S}
>
<Text
style={[styles.stat, { color: theme.primary, textAlign: 'right' }]}
>
{t('content.summary.following_count', {
count: account?.following_count || 0
})}
</Text>
</ShimmerPlaceholder>
</View>
</View>
)
@ -203,8 +261,7 @@ const styles = StyleSheet.create({
},
account: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.L
alignItems: 'center'
},
account_types: { marginLeft: StyleConstants.Spacing.S },
fields: {
@ -218,21 +275,40 @@ const styles = StyleSheet.create({
paddingTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.S
},
fieldLeft: {
flexBasis: '30%',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRightWidth: 1,
paddingLeft: StyleConstants.Spacing.S,
paddingRight: StyleConstants.Spacing.S
},
fieldCheck: { marginLeft: StyleConstants.Spacing.XS },
fieldRight: {
flexBasis: '70%',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: StyleConstants.Spacing.S,
paddingRight: StyleConstants.Spacing.S
},
note: {
marginBottom: StyleConstants.Spacing.L
},
created_at: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: StyleConstants.Spacing.M
alignItems: 'center'
},
created_at_icon: {
marginRight: StyleConstants.Spacing.XS
},
summary: {
stats: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between'
},
stat: {
fontSize: StyleConstants.Font.Size.S
}
})

View File

@ -25,7 +25,11 @@ export const timelineFetch = async (
direction: 'prev' | 'next'
id: string
}
) => {
): Promise<{
toots: Mastodon.Status[]
pointer?: number
pinnedLength?: number
}> => {
let res
if (pagination && pagination.id) {
@ -47,7 +51,7 @@ export const timelineFetch = async (
url: 'timelines/home',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Local':
params.local = 'true'
@ -57,7 +61,7 @@ export const timelineFetch = async (
url: 'timelines/public',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'LocalPublic':
res = await client({
@ -66,7 +70,7 @@ export const timelineFetch = async (
url: 'timelines/public',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'RemotePublic':
res = await client({
@ -75,7 +79,7 @@ export const timelineFetch = async (
url: 'timelines/public',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Notifications':
res = await client({
@ -84,7 +88,7 @@ export const timelineFetch = async (
url: 'notifications',
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Account_Default':
res = await client({
@ -95,6 +99,7 @@ export const timelineFetch = async (
pinned: 'true'
}
})
const pinnedLength = res.body.length
let toots: Mastodon.Status[] = res.body
res = await client({
method: 'get',
@ -105,7 +110,7 @@ export const timelineFetch = async (
}
})
toots = uniqBy([...toots, ...res.body], 'id')
return Promise.resolve({ toots: toots, pointer: null })
return Promise.resolve({ toots: toots, pinnedLength })
case 'Account_All':
res = await client({
@ -114,7 +119,7 @@ export const timelineFetch = async (
url: `accounts/${account}/statuses`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Account_Media':
res = await client({
@ -125,7 +130,7 @@ export const timelineFetch = async (
only_media: 'true'
}
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Hashtag':
res = await client({
@ -134,7 +139,7 @@ export const timelineFetch = async (
url: `timelines/tag/${hashtag}`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Conversations':
res = await client({
@ -149,7 +154,7 @@ export const timelineFetch = async (
(b: Mastodon.Conversation) => b.id !== pagination.id
)
}
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Bookmarks':
res = await client({
@ -158,7 +163,7 @@ export const timelineFetch = async (
url: `bookmarks`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Favourites':
res = await client({
@ -167,7 +172,7 @@ export const timelineFetch = async (
url: `favourites`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'List':
res = await client({
@ -176,7 +181,7 @@ export const timelineFetch = async (
url: `timelines/list/${list}`,
params
})
return Promise.resolve({ toots: res.body, pointer: null })
return Promise.resolve({ toots: res.body })
case 'Toot':
res = await client({
@ -188,8 +193,5 @@ export const timelineFetch = async (
toots: [...res.body.ancestors, toot, ...res.body.descendants],
pointer: res.body.ancestors.length
})
default:
console.error('Page is not provided')
}
}