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

View File

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

View File

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

View File

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

View File

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