1
0
mirror of https://github.com/tooot-app/app synced 2025-03-06 12:37:44 +01:00

My lists are done

This commit is contained in:
Zhiyuan Zheng 2020-11-22 00:46:23 +01:00
parent 1ad67e67ac
commit 6a5d9e7fb8
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
34 changed files with 865 additions and 296 deletions

View File

@ -37,6 +37,7 @@
"react-native-render-html": "^4.2.4",
"react-native-safe-area-context": "3.1.4",
"react-native-screens": "~2.10.1",
"react-native-shimmer-placeholder": "^2.0.6",
"react-native-toast-message": "^1.3.4",
"react-native-web": "~0.13.7",
"react-native-webview": "10.7.0",

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

@ -11,6 +11,9 @@ declare namespace App {
| 'Account_Default'
| 'Account_All'
| 'Account_Media'
| 'Conversations'
| 'Bookmarks'
| 'Favourites'
type QueryKey = [
Pages,

View File

@ -136,6 +136,13 @@ declare namespace Mastodon {
blurhash: string
}
type Conversation = {
id: string
accounts: Account[]
unread: boolean
last_status?: Status
}
type Emoji = {
// Base
shortcode: string
@ -151,6 +158,11 @@ declare namespace Mastodon {
verified_at?: string
}
type List = {
id: string
title: string
}
type Mention = {
// Base
id: string

View File

@ -57,7 +57,6 @@ export const Index: React.FC = () => {
<Tab.Screen name='Screen-Public' component={ScreenPublic} />
<Tab.Screen
name='Screen-Post'
component={() => <></>}
listeners={({ navigation, route }) => ({
tabPress: e => {
e.preventDefault()
@ -70,8 +69,13 @@ export const Index: React.FC = () => {
})
}
})}
>
{() => <></>}
</Tab.Screen>
<Tab.Screen
name='Screen-Notifications'
component={ScreenNotifications}
/>
<Tab.Screen name='Screen-Notifications' component={ScreenNotifications} />
<Tab.Screen name='Screen-Me' component={ScreenMe} />
</Tab.Navigator>

5
src/components/Menu.tsx Normal file
View File

@ -0,0 +1,5 @@
import MenuContainer from './Menu/Container'
import MenuHeader from './Menu/Header'
import MenuItem from './Menu/Item'
export { MenuContainer, MenuHeader, MenuItem }

View File

@ -0,0 +1,8 @@
import React from 'react'
import { View } from 'react-native'
const MenuContainer: React.FC = ({ ...props }) => {
return <View>{props.children}</View>
}
export default MenuContainer

View File

@ -0,0 +1,20 @@
import React from 'react'
import { StyleSheet, Text } from 'react-native'
export interface Props {
heading: string
}
const MenuHeader: React.FC<Props> = ({ heading }) => {
return <Text style={styles.header}>{heading}</Text>
}
const styles = StyleSheet.create({
header: {
marginTop: 12,
paddingLeft: 12,
paddingRight: 12
}
})
export default MenuHeader

View File

@ -0,0 +1,70 @@
import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons'
export interface Props {
icon?: string
title: string
navigateTo?: string
navigateToParams?: {}
}
const Core: React.FC<Props> = ({ icon, title, navigateTo }) => {
return (
<View style={styles.core}>
{icon && <Feather name={icon} size={24} style={styles.iconLeading} />}
<Text>{title}</Text>
{navigateTo && (
<Feather
name='chevron-right'
size={24}
color='lightgray'
style={styles.iconNavigation}
/>
)}
</View>
)
}
const MenuItem: React.FC<Props> = ({ ...props }) => {
const navigation = useNavigation()
return props.navigateTo ? (
<Pressable
style={styles.base}
onPress={() => {
navigation.navigate(props.navigateTo!, props.navigateToParams)
}}
>
<Core {...props} />
</Pressable>
) : (
<View style={styles.base}>
<Core {...props} />
</View>
)
}
const styles = StyleSheet.create({
base: {
height: 50,
borderBottomColor: 'lightgray',
borderBottomWidth: 1
},
core: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 12,
paddingRight: 12
},
iconLeading: {
marginRight: 8
},
iconNavigation: {
marginLeft: 'auto'
}
})
export default MenuItem

View File

@ -14,7 +14,7 @@ const Avatar: React.FC<Props> = ({ uri, id }) => {
<Pressable
style={styles.avatar}
onPress={() => {
navigation.navigate('Account', {
navigation.navigate('Screen-Shared-Account', {
id: id
})
}}

View File

@ -0,0 +1,66 @@
import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import relativeTime from 'src/utils/relativeTime'
import Emojis from './Emojis'
export interface Props {
account: Mastodon.Account
created_at?: Mastodon.Status['created_at']
}
const HeaderConversation: React.FC<Props> = ({ account, created_at }) => {
return (
<View>
<View style={styles.nameAndDate}>
<View style={styles.name}>
{account.emojis ? (
<Emojis
content={account.display_name || account.username}
emojis={account.emojis}
dimension={14}
/>
) : (
<Text numberOfLines={1}>
{account.display_name || account.username}
</Text>
)}
</View>
{created_at && (
<View>
<Text style={styles.created_at}>{relativeTime(created_at)}</Text>
</View>
)}
</View>
<Text style={styles.account} numberOfLines={1}>
@{account.acct}
</Text>
</View>
)
}
const styles = StyleSheet.create({
nameAndDate: {
width: '100%',
flexDirection: 'row',
justifyContent: 'space-between'
},
name: {
flexDirection: 'row',
marginRight: 8,
fontWeight: '900'
},
created_at: {
fontSize: 12,
lineHeight: 12,
marginTop: 8,
marginBottom: 8,
marginRight: 8
},
account: {
lineHeight: 14,
flexShrink: 1
}
})
export default HeaderConversation

View File

@ -45,7 +45,9 @@ const TootNotification: React.FC<Props> = ({ notification, queryKey }) => {
/>
<Pressable
onPress={() =>
navigation.navigate('Toot', { toot: notification.id })
navigation.navigate('Screen-Shared-Toot', {
toot: notification.id
})
}
>
{notification.status ? (

View File

@ -0,0 +1,73 @@
import React, { useMemo } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import Avatar from './Status/Avatar'
import HeaderConversation from './Status/HeaderConversation'
import Content from './Status/Content'
export interface Props {
item: Mastodon.Conversation
}
// Unread and mark as unread
const TimelineConversation: React.FC<Props> = ({ item }) => {
const navigation = useNavigation()
const statusView = useMemo(() => {
return (
<View style={styles.statusView}>
<View style={styles.status}>
<Avatar uri={item.accounts[0].avatar} id={item.accounts[0].id} />
<View style={styles.details}>
<HeaderConversation
account={item.accounts[0]}
created_at={item.last_status?.created_at}
/>
{/* Can pass toot info to next page to speed up performance */}
<Pressable
onPress={() =>
item.last_status &&
navigation.navigate('Screen-Shared-Toot', {
toot: item.last_status.id
})
}
>
{item.last_status ? (
<Content
content={item.last_status.content}
emojis={item.last_status.emojis}
mentions={item.last_status.mentions}
spoiler_text={item.last_status.spoiler_text}
// tags={actualStatus.tags}
// style={{ flex: 1 }}
/>
) : (
<></>
)}
</Pressable>
</View>
</View>
</View>
)
}, [item])
return statusView
}
const styles = StyleSheet.create({
statusView: {
flex: 1,
flexDirection: 'column',
padding: 12
},
status: {
flex: 1,
flexDirection: 'row'
},
details: {
flex: 1,
flexGrow: 1
}
})
export default TimelineConversation

View File

@ -12,23 +12,23 @@ import Card from './Status/Card'
import ActionsStatus from './Status/ActionsStatus'
export interface Props {
status: Mastodon.Status
item: Mastodon.Status
queryKey: App.QueryKey
}
const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
const navigation = useNavigation()
let actualStatus = status.reblog ? status.reblog : status
let actualStatus = item.reblog ? item.reblog : item
const statusView = useMemo(() => {
return (
<View style={styles.statusView}>
{status.reblog && (
{item.reblog && (
<Actioned
action='reblog'
name={status.account.display_name || status.account.username}
emojis={status.account.emojis}
name={item.account.display_name || item.account.username}
emojis={item.account.emojis}
/>
)}
<View style={styles.status}>
@ -47,13 +47,15 @@ const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
}
emojis={actualStatus.account.emojis}
account={actualStatus.account.acct}
created_at={status.created_at}
application={status.application}
created_at={item.created_at}
application={item.application}
/>
{/* Can pass toot info to next page to speed up performance */}
<Pressable
onPress={() =>
navigation.navigate('Toot', { toot: actualStatus.id })
navigation.navigate('Screen-Shared-Toot', {
toot: actualStatus.id
})
}
>
{actualStatus.content ? (
@ -83,7 +85,7 @@ const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
</View>
</View>
)
}, [status])
}, [item])
return statusView
}
@ -104,4 +106,4 @@ const styles = StyleSheet.create({
}
})
export default StatusInTimeline
export default TimelineDefault

View File

@ -2,14 +2,66 @@ import React from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import ScreenMeRoot from 'src/screens/Me/Root'
import ScreenMeConversations from './Me/Cconversations'
import ScreenMeBookmarks from './Me/Bookmarks'
import ScreenMeFavourites from './Me/Favourites'
import ScreenMeLists from './Me/Lists'
import sharedScreens from 'src/screens/Shared/sharedScreens'
import ScreenMeListsList from './Me/Root/Lists/List'
const Stack = createNativeStackNavigator()
const ScreenMe: React.FC = () => {
return (
<Stack.Navigator>
<Stack.Screen name='Screen-Me-Root' component={ScreenMeRoot} />
<Stack.Navigator
screenOptions={{
headerTitle: 'test'
}}
>
<Stack.Screen
name='Screen-Me-Root'
component={ScreenMeRoot}
options={{
headerTranslucent: true,
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
headerCenter: () => <></>
}}
/>
<Stack.Screen
name='Screen-Me-Conversations'
component={ScreenMeConversations}
options={{
headerTitle: '对话'
}}
/>
<Stack.Screen
name='Screen-Me-Bookmarks'
component={ScreenMeBookmarks}
options={{
headerTitle: '书签'
}}
/>
<Stack.Screen
name='Screen-Me-Favourites'
component={ScreenMeFavourites}
options={{
headerTitle: '书签'
}}
/>
<Stack.Screen
name='Screen-Me-Lists'
component={ScreenMeLists}
options={{
headerTitle: '书签'
}}
/>
<Stack.Screen
name='Screen-Me-Lists-List'
component={ScreenMeListsList}
options={{
headerTitle: '书签'
}}
/>
{sharedScreens(Stack)}
</Stack.Navigator>

View File

@ -0,0 +1,9 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
const ScreenMeBookmarks: React.FC = () => {
return <Timeline page='Bookmarks' />
}
export default ScreenMeBookmarks

View File

@ -0,0 +1,9 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
const ScreenMeConversations: React.FC = () => {
return <Timeline page='Conversations' />
}
export default ScreenMeConversations

View File

@ -0,0 +1,9 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
const ScreenMeFavourites: React.FC = () => {
return <Timeline page='Favourites' />
}
export default ScreenMeFavourites

41
src/screens/Me/Lists.tsx Normal file
View File

@ -0,0 +1,41 @@
import React from 'react'
import { ActivityIndicator, Text } from 'react-native'
import { useQuery } from 'react-query'
import { MenuContainer, MenuHeader, MenuItem } from 'src/components/Menu'
import { listsFetch } from 'src/utils/fetches/listsFetch'
const ScreenMeLists: React.FC = () => {
const { status, data } = useQuery(['Lists'], listsFetch)
let lists
switch (status) {
case 'loading':
lists = <ActivityIndicator />
break
case 'error':
lists = <Text></Text>
break
case 'success':
lists = data?.map(d => (
<MenuItem
icon='list'
title={d.title}
navigateTo='Screen-Me-Lists-List'
navigateToParams={{
list: d.id
}}
/>
))
break
}
return (
<MenuContainer>
<MenuHeader heading='我的列表' />
{lists}
</MenuContainer>
)
}
export default ScreenMeLists

View File

@ -1,17 +1,33 @@
import React from 'react'
import { View } from 'react-native'
import { ScrollView } from 'react-native'
import { useSelector } from 'react-redux'
import { RootState } from 'src/store'
import { RootState, store } from 'src/store'
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
import Login from './Root/Login'
import MyInfo from './Root/MyInfo'
import MyCollections from './Root/MyCollections'
import Settings from './Root/Settings'
const ScreenMeRoot: React.FC = () => {
const localRegistered = useSelector(
(state: RootState) => state.instances.local.url
)
return <View>{localRegistered ? <></> : <Login />}</View>
return (
<ScrollView>
{localRegistered ? (
<MyInfo id={getLocalAccountId(store.getState())!} />
) : (
<Login />
)}
{localRegistered && (
<MyCollections id={getLocalAccountId(store.getState())!} />
)}
<Settings />
</ScrollView>
)
}
export default ScreenMeRoot

View File

@ -0,0 +1,23 @@
import React from 'react'
import Timeline from 'src/screens/Timelines/Timeline'
// Show remote hashtag? Only when private, show local version?
export interface Props {
route: {
params: {
list: string
}
}
}
const ScreenMeListsList: React.FC<Props> = ({
route: {
params: { list }
}
}) => {
return <Timeline page='List' list={list} />
}
export default ScreenMeListsList

View File

@ -76,7 +76,6 @@ const Login: React.FC = () => {
clientSecret: applicationData?.clientSecret,
scopes: ['read', 'write', 'follow', 'push'],
redirectUri: 'exp://127.0.0.1:19000'
// usePKCE: false
},
{
authorizationEndpoint: `https://${instance}/oauth/authorize`

View File

@ -0,0 +1,20 @@
import React from 'react'
import { MenuContainer, MenuHeader, MenuItem } from 'src/components/Menu'
export interface Props {
id: Mastodon.Account['id']
}
const MyInfo: React.FC<Props> = ({ id }) => {
return (
<MenuContainer>
<MenuHeader heading='我的东西' />
<MenuItem icon='mail' title='私信' navigateTo='Screen-Me-Conversations' />
<MenuItem icon='bookmark' title='书签' navigateTo='Screen-Me-Bookmarks' />
<MenuItem icon='star' title='喜欢' navigateTo='Screen-Me-Favourites' />
<MenuItem icon='list' title='列表' navigateTo='Screen-Me-Lists' />
</MenuContainer>
)
}
export default MyInfo

View File

@ -0,0 +1,23 @@
import React from 'react'
import { useQuery } from 'react-query'
import { accountFetch } from 'src/utils/fetches/accountFetch'
import AccountHeader from 'src/screens/Shared/Account/Header'
import AccountInformation from 'src/screens/Shared/Account/Information'
export interface Props {
id: Mastodon.Account['id']
}
const MyInfo: React.FC<Props> = ({ id }) => {
const { data } = useQuery(['Account', { id }], accountFetch)
return (
<>
<AccountHeader uri={data?.header} limitHeight />
<AccountInformation account={data} />
</>
)
}
export default MyInfo

View File

@ -0,0 +1,13 @@
import React from 'react'
import { MenuContainer, MenuHeader, MenuItem } from 'src/components/Menu'
const Settings: React.FC = () => {
return (
<MenuContainer>
<MenuHeader heading='设置' />
<MenuItem icon='settings' title='设置' navigateTo='Local' />
</MenuContainer>
)
}
export default Settings

View File

@ -1,185 +1,16 @@
import React, { useEffect, useRef, useState } from 'react'
import {
Dimensions,
FlatList,
Image,
ScrollView,
StyleSheet,
Text,
View
} from 'react-native'
import SegmentedControl from '@react-native-community/segmented-control'
import { Feather } from '@expo/vector-icons'
import React from 'react'
import { ScrollView } from 'react-native'
// import * as relationshipsSlice from 'src/stacks/common/relationshipsSlice'
import ParseContent from 'src/components/ParseContent'
import Timeline from 'src/screens/Timelines/Timeline'
import { useQuery } from 'react-query'
import { accountFetch } from '../../utils/fetches/accountFetch'
import AccountToots from './Account/Toots'
import AccountHeader from './Account/Header'
import AccountInformation from './Account/Information'
// Moved account example: https://m.cmx.im/web/accounts/27812
const Header = ({
uri,
size
}: {
uri: string
size: { width: number; height: number }
}) => {
if (uri) {
const heightRatio = size ? size.height / size.width : 1 / 2
return (
<Image
source={{ uri: uri }}
style={[
styles.header,
{
height:
Dimensions.get('window').width *
(heightRatio > 0.5 ? 1 / 2 : heightRatio)
}
]}
/>
)
} else {
return (
<View
style={[
styles.header,
{ height: Dimensions.get('window').width * (1 / 3) }
]}
/>
)
}
}
const Information = ({
account,
emojis
}: {
account: Mastodon.Account
emojis: Mastodon.Emoji[]
}) => {
return (
<View style={styles.information}>
{/* <Text>Moved or not: {account.moved}</Text> */}
<Image source={{ uri: account.avatar }} style={styles.avatar} />
<Text style={styles.display_name}>
{account.display_name || account.username}
{account.bot && (
<Feather name='hard-drive' style={styles.display_name} />
)}
</Text>
<Text style={styles.account}>
{account.acct}
{account.locked && <Feather name='lock' />}
</Text>
{account.fields &&
account.fields.map((field, index) => (
<View key={index} style={{ flex: 1, flexDirection: 'row' }}>
<Text style={{ width: '30%', alignSelf: 'center' }}>
<ParseContent content={field.name} emojis={emojis} showFullLink />{' '}
{field.verified_at && <Feather name='check-circle' />}
</Text>
<Text style={{ width: '70%' }}>
<ParseContent
content={field.value}
emojis={emojis}
showFullLink
/>
</Text>
</View>
))}
{account.note && <ParseContent content={account.note} emojis={emojis} />}
<Text>
{' '}
{new Date(account.created_at).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</Text>
<Text>Toots: {account.statuses_count}</Text>
<Text>Followers: {account.followers_count}</Text>
<Text>Following: {account.following_count}</Text>
</View>
)
}
const Toots = ({ account }: { account: string }) => {
const [segment, setSegment] = useState(0)
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
false
)
const horizontalPaging = useRef<any>()
const pages: ['Account_Default', 'Account_All', 'Account_Media'] = [
'Account_Default',
'Account_All',
'Account_Media'
]
return (
<>
<SegmentedControl
values={['嘟嘟', '嘟嘟和回复', '媒体']}
selectedIndex={segment}
onChange={({ nativeEvent }) => {
setSegmentManuallyTriggered(true)
setSegment(nativeEvent.selectedSegmentIndex)
horizontalPaging.current.scrollToIndex({
index: nativeEvent.selectedSegmentIndex
})
}}
style={{ width: '100%', height: 30 }}
/>
<FlatList
style={{ width: Dimensions.get('window').width, height: '100%' }}
data={pages}
keyExtractor={page => page}
renderItem={({ item, index }) => {
return (
<View style={{ width: Dimensions.get('window').width }}>
<Timeline
key={index}
page={item}
account={account}
disableRefresh
scrollEnabled={false}
/>
</View>
)
}}
ref={horizontalPaging}
bounces={false}
getItemLayout={(data, index) => ({
length: Dimensions.get('window').width,
offset: Dimensions.get('window').width * index,
index
})}
horizontal
onMomentumScrollEnd={() => {
setSegmentManuallyTriggered(false)
}}
onScroll={({ nativeEvent }) =>
!segmentManuallyTriggered &&
setSegment(
nativeEvent.contentOffset.x <= Dimensions.get('window').width / 3
? 0
: 1
)
}
pagingEnabled
showsHorizontalScrollIndicator={false}
/>
</>
)
}
export interface Props {
route: {
params: {
@ -193,68 +24,17 @@ const ScreenSharedAccount: React.FC<Props> = ({
params: { id }
}
}) => {
const { isLoading, isFetchingMore, isError, isSuccess, data } = useQuery(
['Account', { id }],
accountFetch
)
const { data } = useQuery(['Account', { id }], accountFetch)
// const stateRelationships = useSelector(relationshipsState)
interface isHeaderImageSize {
width: number
height: number
}
const [headerImageSize, setHeaderImageSize] = useState<
isHeaderImageSize | undefined
>(undefined)
useEffect(() => {
if (isSuccess && data.header) {
Image.getSize(data.header, (width, height) => {
setHeaderImageSize({ width, height })
})
} else {
setHeaderImageSize({ width: 3, height: 1 })
}
}, [data, isSuccess])
// add emoji support
return isSuccess && headerImageSize ? (
<ScrollView>
{headerImageSize && (
<Header
uri={data.header}
size={{
width: headerImageSize.width,
height: headerImageSize.height
}}
/>
)}
<Information account={data} emojis={data.emojis} />
<Toots account={id} />
return (
<ScrollView bounces={false}>
<AccountHeader uri={data?.header} />
<AccountInformation account={data} />
<AccountToots id={id} />
</ScrollView>
) : (
<></>
)
}
const styles = StyleSheet.create({
header: {
width: '100%',
backgroundColor: 'gray'
},
information: { marginTop: -30, paddingLeft: 12, paddingRight: 12 },
avatar: {
width: 90,
height: 90
},
display_name: {
fontSize: 18,
fontWeight: 'bold',
marginTop: 12
},
account: {
marginTop: 4
}
})
export default ScreenSharedAccount

View File

@ -0,0 +1,61 @@
import React, { useEffect, useRef } from 'react'
import { Animated, Dimensions, Image, StyleSheet } from 'react-native'
export interface Props {
uri: Mastodon.Account['header']
limitHeight?: boolean
}
const limitRatio = 0.4
const AccountHeader: React.FC<Props> = ({ uri, limitHeight = false }) => {
console.log(uri)
useEffect(() => {
if (uri) {
if (uri.includes('/headers/original/missing.png')) {
animateNewSize(limitRatio)
} else {
Image.getSize(
uri,
(width, height) => {
animateNewSize(limitHeight ? limitRatio : height / width)
},
() => {
animateNewSize(limitRatio)
}
)
}
} else {
animateNewSize(limitRatio)
}
}, [uri])
const windowWidth = Dimensions.get('window').width
const imageHeight = useRef(new Animated.Value(windowWidth * limitRatio))
.current
const animateNewSize = (ratio: number) => {
Animated.timing(imageHeight, {
toValue: windowWidth * ratio,
duration: 350,
useNativeDriver: false
}).start()
}
return (
<Animated.View style={[styles.imageContainer, { height: imageHeight }]}>
<Image source={{ uri: uri }} style={styles.image} />
</Animated.View>
)
}
const styles = StyleSheet.create({
imageContainer: {
backgroundColor: 'lightgray'
},
image: {
width: '100%',
height: '100%'
}
})
export default AccountHeader

View File

@ -0,0 +1,95 @@
import React, { useState } from 'react'
import { Image, StyleSheet, Text, View } from 'react-native'
import ShimmerPlaceholder from 'react-native-shimmer-placeholder'
import { Feather } from '@expo/vector-icons'
import ParseContent from 'src/components/ParseContent'
export interface Props {
account: Mastodon.Account | undefined
}
const AccountInformation: React.FC<Props> = ({ account }) => {
const [avatarLoaded, setAvatarLoaded] = useState(false)
// add emoji support
return (
<View style={styles.information}>
{/* <Text>Moved or not: {account.moved}</Text> */}
<ShimmerPlaceholder visible={avatarLoaded} width={90} height={90}>
<Image
source={{ uri: account?.avatar }}
style={styles.avatar}
onLoadEnd={() => setAvatarLoaded(true)}
/>
</ShimmerPlaceholder>
<Text style={styles.display_name}>
{account?.display_name || account?.username}
{account?.bot && (
<Feather name='hard-drive' style={styles.display_name} />
)}
</Text>
<Text style={styles.account}>
{account?.acct}
{account?.locked && <Feather name='lock' />}
</Text>
{account?.fields &&
account.fields.map((field, index) => (
<View key={index} style={{ flex: 1, flexDirection: 'row' }}>
<Text style={{ width: '30%', alignSelf: 'center' }}>
<ParseContent
content={field.name}
emojis={account.emojis}
showFullLink
/>{' '}
{field.verified_at && <Feather name='check-circle' />}
</Text>
<Text style={{ width: '70%' }}>
<ParseContent
content={field.value}
emojis={account.emojis}
showFullLink
/>
</Text>
</View>
))}
{account?.note && (
<ParseContent content={account.note} emojis={account.emojis} />
)}
{account?.created_at && (
<Text>
{' '}
{new Date(account.created_at).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</Text>
)}
<Text>Toots: {account?.statuses_count}</Text>
<Text>Followers: {account?.followers_count}</Text>
<Text>Following: {account?.following_count}</Text>
</View>
)
}
const styles = StyleSheet.create({
information: { marginTop: -30, paddingLeft: 12, paddingRight: 12 },
avatar: {
width: 90,
height: 90
},
display_name: {
fontSize: 18,
fontWeight: 'bold',
marginTop: 12
},
account: {
marginTop: 4
}
})
export default AccountInformation

View File

@ -0,0 +1,81 @@
import React, { useRef, useState } from 'react'
import { Dimensions, FlatList, View } from 'react-native'
import SegmentedControl from '@react-native-community/segmented-control'
import Timeline from 'src/screens/Timelines/Timeline'
export interface Props {
id: Mastodon.Account['id']
}
const AccountToots: React.FC<Props> = ({ id }) => {
const [segment, setSegment] = useState(0)
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
false
)
const horizontalPaging = useRef<any>()
const pages: ['Account_Default', 'Account_All', 'Account_Media'] = [
'Account_Default',
'Account_All',
'Account_Media'
]
return (
<>
<SegmentedControl
values={['嘟嘟', '嘟嘟和回复', '媒体']}
selectedIndex={segment}
onChange={({ nativeEvent }) => {
setSegmentManuallyTriggered(true)
setSegment(nativeEvent.selectedSegmentIndex)
horizontalPaging.current.scrollToIndex({
index: nativeEvent.selectedSegmentIndex
})
}}
style={{ width: '100%', height: 30 }}
/>
<FlatList
style={{ width: Dimensions.get('window').width, height: '100%' }}
data={pages}
keyExtractor={page => page}
renderItem={({ item, index }) => {
return (
<View style={{ width: Dimensions.get('window').width }}>
<Timeline
key={index}
page={item}
account={id}
disableRefresh
scrollEnabled={false}
/>
</View>
)
}}
ref={horizontalPaging}
bounces={false}
getItemLayout={(data, index) => ({
length: Dimensions.get('window').width,
offset: Dimensions.get('window').width * index,
index
})}
horizontal
onMomentumScrollEnd={() => {
setSegmentManuallyTriggered(false)
}}
onScroll={({ nativeEvent }) =>
!segmentManuallyTriggered &&
setSegment(
nativeEvent.contentOffset.x <= Dimensions.get('window').width / 3
? 0
: 1
)
}
pagingEnabled
showsHorizontalScrollIndicator={false}
/>
</>
)
}
export default AccountToots

View File

@ -5,8 +5,27 @@ import ScreenSharedHashtag from 'src/screens/Shared/Hashtag'
import ScreenSharedToot from 'src/screens/Shared/Toot'
import ScreenSharedWebview from 'src/screens/Shared/Webview'
import PostToot from 'src/screens/Shared/Compose'
import { TypedNavigator } from '@react-navigation/native'
import { NativeStackNavigationOptions } from 'react-native-screens/lib/typescript'
import {
NativeStackNavigationEventMap,
NativeStackNavigatorProps
} from 'react-native-screens/lib/typescript/types'
const sharedScreens = (Stack: any) => {
const sharedScreens = (
Stack: TypedNavigator<
Record<string, object | undefined>,
any,
NativeStackNavigationOptions,
NativeStackNavigationEventMap,
({
initialRouteName,
children,
screenOptions,
...rest
}: NativeStackNavigatorProps) => JSX.Element
>
) => {
return [
<Stack.Screen
key='Screen-Shared-Account'
@ -15,7 +34,7 @@ const sharedScreens = (Stack: any) => {
options={{
headerTranslucent: true,
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
headerCenter: () => {}
headerCenter: () => <></>
}}
/>,
<Stack.Screen

View File

@ -3,7 +3,8 @@ import { ActivityIndicator, AppState, FlatList, Text, View } from 'react-native'
import { setFocusHandler, useInfiniteQuery } from 'react-query'
import StatusInNotifications from 'src/components/StatusInNotifications'
import StatusInTimeline from 'src/components/StatusInTimeline'
import TimelineDefault from 'src/components/TimelineDefault'
import TimelineConversation from 'src/components/TimelineConversation'
import { timelineFetch } from 'src/utils/fetches/timelineFetch'
// Opening nesting hashtag pages
@ -37,10 +38,7 @@ const Timeline: React.FC<Props> = ({
return () => AppState.removeEventListener('change', handleAppStateChange)
})
const queryKey: App.QueryKey = [
page,
{ page, hashtag, list, toot, account }
]
const queryKey: App.QueryKey = [page, { page, hashtag, list, toot, account }]
const {
isLoading,
isFetchingMore,
@ -50,9 +48,7 @@ const Timeline: React.FC<Props> = ({
fetchMore
} = useInfiniteQuery(queryKey, timelineFetch)
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
// if (page==='Toot'){
// console.log(data)
// }
// const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
let content
if (!isSuccess) {
@ -67,18 +63,30 @@ const Timeline: React.FC<Props> = ({
scrollEnabled={scrollEnabled} // For timeline in Account view
data={flattenData}
keyExtractor={({ id }) => id}
renderItem={({ item, index, separators }) =>
page === 'Notifications' ? (
<StatusInNotifications
key={index}
notification={item}
queryKey={queryKey}
/>
) : (
<StatusInTimeline key={index} status={item} queryKey={queryKey} />
)
}
// {...(state.pointer && { initialScrollIndex: state.pointer })}
renderItem={({ item, index, separators }) => {
switch (page) {
case 'Conversations':
return <TimelineConversation key={index} item={item} />
case 'Notifications':
return (
<StatusInNotifications
key={index}
notification={item}
queryKey={queryKey}
/>
)
default:
return (
<TimelineDefault
key={index}
item={item}
queryKey={queryKey}
/>
)
}
}}
// require getItemLayout
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
{...(!disableRefresh && {
onRefresh: () =>
fetchMore(

View File

@ -1,4 +1,4 @@
import { configureStore } from '@reduxjs/toolkit'
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import { persistReducer, persistStore } from 'redux-persist'
import createSecureStore from 'redux-persist-expo-securestore'
@ -14,7 +14,12 @@ const instancesPersistConfig = {
const store = configureStore({
reducer: {
instances: persistReducer(instancesPersistConfig, instancesSlice)
}
},
middleware: getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['persist/PERSIST']
}
})
})
let persistor = persistStore(store)

View File

@ -0,0 +1,10 @@
import client from 'src/api/client'
export const listsFetch = async () => {
const res = await client({
method: 'get',
instance: 'local',
endpoint: 'lists'
})
return Promise.resolve(res.body)
}

View File

@ -47,7 +47,7 @@ export const timelineFetch = async (
endpoint: 'timelines/home',
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'Local':
query.local = 'true'
@ -57,7 +57,7 @@ export const timelineFetch = async (
endpoint: 'timelines/public',
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'LocalPublic':
res = await client({
@ -66,7 +66,7 @@ export const timelineFetch = async (
endpoint: 'timelines/public',
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'RemotePublic':
res = await client({
@ -75,7 +75,7 @@ export const timelineFetch = async (
endpoint: 'timelines/public',
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'Notifications':
res = await client({
@ -84,7 +84,7 @@ export const timelineFetch = async (
endpoint: 'notifications',
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'Account_Default':
res = await client({
@ -105,7 +105,7 @@ export const timelineFetch = async (
}
})
toots = uniqBy([...toots, ...res.body], 'id')
return Promise.resolve({ toots: toots })
return Promise.resolve({ toots: toots, pointer: null })
case 'Account_All':
res = await client({
@ -114,7 +114,7 @@ export const timelineFetch = async (
endpoint: `accounts/${account}/statuses`,
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'Account_Media':
res = await client({
@ -125,7 +125,7 @@ export const timelineFetch = async (
only_media: 'true'
}
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
case 'Hashtag':
res = await client({
@ -134,18 +134,43 @@ export const timelineFetch = async (
endpoint: `timelines/tag/${hashtag}`,
query
})
return Promise.resolve({ toots: res.body })
return Promise.resolve({ toots: res.body, pointer: null })
// case 'List':
// res = await client({
// method: 'get',
// instance: 'local',
// endpoint: `timelines/list/${list}`,
// query
// })
// return {
// toots: res.body
// }
case 'Conversations':
res = await client({
method: 'get',
instance: 'local',
endpoint: `conversations`,
query
})
return Promise.resolve({ toots: res.body, pointer: null })
case 'Bookmarks':
res = await client({
method: 'get',
instance: 'local',
endpoint: `bookmarks`,
query
})
return Promise.resolve({ toots: res.body, pointer: null })
case 'Favourites':
res = await client({
method: 'get',
instance: 'local',
endpoint: `favourites`,
query
})
return Promise.resolve({ toots: res.body, pointer: null })
case 'List':
res = await client({
method: 'get',
instance: 'local',
endpoint: `timelines/list/${list}`,
query
})
return Promise.resolve({ toots: res.body, pointer: null })
case 'Toot':
const current = await client({
@ -168,6 +193,6 @@ export const timelineFetch = async (
})
default:
console.error('First time fetching timeline error')
console.error('Page is not provided')
}
}

View File

@ -5039,6 +5039,11 @@ react-native-screens@~2.10.1:
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.10.1.tgz#06d22fae87ef0ce51c616c34a199726db1403b95"
integrity sha512-Z2kKSk4AwWRQNCBmTjViuBQK0/Lx0jc25TZptn/2gKYUCOuVRvCekoA26u0Tsb3BIQ8tWDsZW14OwDlFUXW1aw==
react-native-shimmer-placeholder@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/react-native-shimmer-placeholder/-/react-native-shimmer-placeholder-2.0.6.tgz#a6626d955945edb1aa01f8863f3e039a738d53d1"
integrity sha512-eq0Jxi/j/WseijfSeNjoAsaz1164XUCDvKpG/+My+c5YeVMfjTnl9SwoVAIr9uOpuDXXia67j8xME+eFJZvBXw==
react-native-toast-message@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-native-toast-message/-/react-native-toast-message-1.3.4.tgz#19ea1c5d3ad8e9d12f7550c4719a9e0258dca6bd"