mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
My lists are done
This commit is contained in:
@ -37,6 +37,7 @@
|
|||||||
"react-native-render-html": "^4.2.4",
|
"react-native-render-html": "^4.2.4",
|
||||||
"react-native-safe-area-context": "3.1.4",
|
"react-native-safe-area-context": "3.1.4",
|
||||||
"react-native-screens": "~2.10.1",
|
"react-native-screens": "~2.10.1",
|
||||||
|
"react-native-shimmer-placeholder": "^2.0.6",
|
||||||
"react-native-toast-message": "^1.3.4",
|
"react-native-toast-message": "^1.3.4",
|
||||||
"react-native-web": "~0.13.7",
|
"react-native-web": "~0.13.7",
|
||||||
"react-native-webview": "10.7.0",
|
"react-native-webview": "10.7.0",
|
||||||
|
3
src/@types/app.d.ts
vendored
3
src/@types/app.d.ts
vendored
@ -11,6 +11,9 @@ declare namespace App {
|
|||||||
| 'Account_Default'
|
| 'Account_Default'
|
||||||
| 'Account_All'
|
| 'Account_All'
|
||||||
| 'Account_Media'
|
| 'Account_Media'
|
||||||
|
| 'Conversations'
|
||||||
|
| 'Bookmarks'
|
||||||
|
| 'Favourites'
|
||||||
|
|
||||||
type QueryKey = [
|
type QueryKey = [
|
||||||
Pages,
|
Pages,
|
||||||
|
12
src/@types/mastodon.d.ts
vendored
12
src/@types/mastodon.d.ts
vendored
@ -136,6 +136,13 @@ declare namespace Mastodon {
|
|||||||
blurhash: string
|
blurhash: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Conversation = {
|
||||||
|
id: string
|
||||||
|
accounts: Account[]
|
||||||
|
unread: boolean
|
||||||
|
last_status?: Status
|
||||||
|
}
|
||||||
|
|
||||||
type Emoji = {
|
type Emoji = {
|
||||||
// Base
|
// Base
|
||||||
shortcode: string
|
shortcode: string
|
||||||
@ -151,6 +158,11 @@ declare namespace Mastodon {
|
|||||||
verified_at?: string
|
verified_at?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type List = {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
type Mention = {
|
type Mention = {
|
||||||
// Base
|
// Base
|
||||||
id: string
|
id: string
|
||||||
|
@ -57,7 +57,6 @@ export const Index: React.FC = () => {
|
|||||||
<Tab.Screen name='Screen-Public' component={ScreenPublic} />
|
<Tab.Screen name='Screen-Public' component={ScreenPublic} />
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name='Screen-Post'
|
name='Screen-Post'
|
||||||
component={() => <></>}
|
|
||||||
listeners={({ navigation, route }) => ({
|
listeners={({ navigation, route }) => ({
|
||||||
tabPress: e => {
|
tabPress: e => {
|
||||||
e.preventDefault()
|
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.Screen name='Screen-Me' component={ScreenMe} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|
||||||
|
5
src/components/Menu.tsx
Normal file
5
src/components/Menu.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import MenuContainer from './Menu/Container'
|
||||||
|
import MenuHeader from './Menu/Header'
|
||||||
|
import MenuItem from './Menu/Item'
|
||||||
|
|
||||||
|
export { MenuContainer, MenuHeader, MenuItem }
|
8
src/components/Menu/Container.tsx
Normal file
8
src/components/Menu/Container.tsx
Normal 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
|
20
src/components/Menu/Header.tsx
Normal file
20
src/components/Menu/Header.tsx
Normal 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
|
70
src/components/Menu/Item.tsx
Normal file
70
src/components/Menu/Item.tsx
Normal 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
|
@ -14,7 +14,7 @@ const Avatar: React.FC<Props> = ({ uri, id }) => {
|
|||||||
<Pressable
|
<Pressable
|
||||||
style={styles.avatar}
|
style={styles.avatar}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
navigation.navigate('Account', {
|
navigation.navigate('Screen-Shared-Account', {
|
||||||
id: id
|
id: id
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
66
src/components/Status/HeaderConversation.tsx
Normal file
66
src/components/Status/HeaderConversation.tsx
Normal 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
|
@ -45,7 +45,9 @@ const TootNotification: React.FC<Props> = ({ notification, queryKey }) => {
|
|||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Toot', { toot: notification.id })
|
navigation.navigate('Screen-Shared-Toot', {
|
||||||
|
toot: notification.id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{notification.status ? (
|
{notification.status ? (
|
||||||
|
73
src/components/TimelineConversation.tsx
Normal file
73
src/components/TimelineConversation.tsx
Normal 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
|
@ -12,23 +12,23 @@ import Card from './Status/Card'
|
|||||||
import ActionsStatus from './Status/ActionsStatus'
|
import ActionsStatus from './Status/ActionsStatus'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
status: Mastodon.Status
|
item: Mastodon.Status
|
||||||
queryKey: App.QueryKey
|
queryKey: App.QueryKey
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
|
const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
let actualStatus = status.reblog ? status.reblog : status
|
let actualStatus = item.reblog ? item.reblog : item
|
||||||
|
|
||||||
const statusView = useMemo(() => {
|
const statusView = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.statusView}>
|
<View style={styles.statusView}>
|
||||||
{status.reblog && (
|
{item.reblog && (
|
||||||
<Actioned
|
<Actioned
|
||||||
action='reblog'
|
action='reblog'
|
||||||
name={status.account.display_name || status.account.username}
|
name={item.account.display_name || item.account.username}
|
||||||
emojis={status.account.emojis}
|
emojis={item.account.emojis}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<View style={styles.status}>
|
<View style={styles.status}>
|
||||||
@ -47,13 +47,15 @@ const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
|
|||||||
}
|
}
|
||||||
emojis={actualStatus.account.emojis}
|
emojis={actualStatus.account.emojis}
|
||||||
account={actualStatus.account.acct}
|
account={actualStatus.account.acct}
|
||||||
created_at={status.created_at}
|
created_at={item.created_at}
|
||||||
application={status.application}
|
application={item.application}
|
||||||
/>
|
/>
|
||||||
{/* Can pass toot info to next page to speed up performance */}
|
{/* Can pass toot info to next page to speed up performance */}
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Toot', { toot: actualStatus.id })
|
navigation.navigate('Screen-Shared-Toot', {
|
||||||
|
toot: actualStatus.id
|
||||||
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{actualStatus.content ? (
|
{actualStatus.content ? (
|
||||||
@ -83,7 +85,7 @@ const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}, [status])
|
}, [item])
|
||||||
|
|
||||||
return statusView
|
return statusView
|
||||||
}
|
}
|
||||||
@ -104,4 +106,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default StatusInTimeline
|
export default TimelineDefault
|
@ -2,14 +2,66 @@ import React from 'react'
|
|||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
|
||||||
import ScreenMeRoot from 'src/screens/Me/Root'
|
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 sharedScreens from 'src/screens/Shared/sharedScreens'
|
||||||
|
import ScreenMeListsList from './Me/Root/Lists/List'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
const ScreenMe: React.FC = () => {
|
const ScreenMe: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator>
|
<Stack.Navigator
|
||||||
<Stack.Screen name='Screen-Me-Root' component={ScreenMeRoot} />
|
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)}
|
{sharedScreens(Stack)}
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
|
9
src/screens/Me/Bookmarks.tsx
Normal file
9
src/screens/Me/Bookmarks.tsx
Normal 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
|
9
src/screens/Me/Cconversations.tsx
Normal file
9
src/screens/Me/Cconversations.tsx
Normal 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
|
9
src/screens/Me/Favourites.tsx
Normal file
9
src/screens/Me/Favourites.tsx
Normal 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
41
src/screens/Me/Lists.tsx
Normal 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
|
@ -1,17 +1,33 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { View } from 'react-native'
|
import { ScrollView } from 'react-native'
|
||||||
import { useSelector } from 'react-redux'
|
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 Login from './Root/Login'
|
||||||
|
import MyInfo from './Root/MyInfo'
|
||||||
|
import MyCollections from './Root/MyCollections'
|
||||||
|
import Settings from './Root/Settings'
|
||||||
|
|
||||||
const ScreenMeRoot: React.FC = () => {
|
const ScreenMeRoot: React.FC = () => {
|
||||||
const localRegistered = useSelector(
|
const localRegistered = useSelector(
|
||||||
(state: RootState) => state.instances.local.url
|
(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
|
export default ScreenMeRoot
|
||||||
|
23
src/screens/Me/Root/Lists/List.tsx
Normal file
23
src/screens/Me/Root/Lists/List.tsx
Normal 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
|
@ -76,7 +76,6 @@ const Login: React.FC = () => {
|
|||||||
clientSecret: applicationData?.clientSecret,
|
clientSecret: applicationData?.clientSecret,
|
||||||
scopes: ['read', 'write', 'follow', 'push'],
|
scopes: ['read', 'write', 'follow', 'push'],
|
||||||
redirectUri: 'exp://127.0.0.1:19000'
|
redirectUri: 'exp://127.0.0.1:19000'
|
||||||
// usePKCE: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authorizationEndpoint: `https://${instance}/oauth/authorize`
|
authorizationEndpoint: `https://${instance}/oauth/authorize`
|
||||||
|
20
src/screens/Me/Root/MyCollections.tsx
Normal file
20
src/screens/Me/Root/MyCollections.tsx
Normal 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
|
23
src/screens/Me/Root/MyInfo.tsx
Normal file
23
src/screens/Me/Root/MyInfo.tsx
Normal 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
|
13
src/screens/Me/Root/Settings.tsx
Normal file
13
src/screens/Me/Root/Settings.tsx
Normal 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
|
@ -1,185 +1,16 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React from 'react'
|
||||||
import {
|
import { ScrollView } from 'react-native'
|
||||||
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 * as relationshipsSlice from 'src/stacks/common/relationshipsSlice'
|
// 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 { useQuery } from 'react-query'
|
||||||
import { accountFetch } from '../../utils/fetches/accountFetch'
|
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
|
// 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 {
|
export interface Props {
|
||||||
route: {
|
route: {
|
||||||
params: {
|
params: {
|
||||||
@ -193,68 +24,17 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||||||
params: { id }
|
params: { id }
|
||||||
}
|
}
|
||||||
}) => {
|
}) => {
|
||||||
const { isLoading, isFetchingMore, isError, isSuccess, data } = useQuery(
|
const { data } = useQuery(['Account', { id }], accountFetch)
|
||||||
['Account', { id }],
|
|
||||||
accountFetch
|
|
||||||
)
|
|
||||||
|
|
||||||
// const stateRelationships = useSelector(relationshipsState)
|
// const stateRelationships = useSelector(relationshipsState)
|
||||||
interface isHeaderImageSize {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
const [headerImageSize, setHeaderImageSize] = useState<
|
|
||||||
isHeaderImageSize | undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
if (isSuccess && data.header) {
|
<ScrollView bounces={false}>
|
||||||
Image.getSize(data.header, (width, height) => {
|
<AccountHeader uri={data?.header} />
|
||||||
setHeaderImageSize({ width, height })
|
<AccountInformation account={data} />
|
||||||
})
|
<AccountToots id={id} />
|
||||||
} 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} />
|
|
||||||
</ScrollView>
|
</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
|
export default ScreenSharedAccount
|
||||||
|
61
src/screens/Shared/Account/Header.tsx
Normal file
61
src/screens/Shared/Account/Header.tsx
Normal 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
|
95
src/screens/Shared/Account/Information.tsx
Normal file
95
src/screens/Shared/Account/Information.tsx
Normal 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
|
81
src/screens/Shared/Account/Toots.tsx
Normal file
81
src/screens/Shared/Account/Toots.tsx
Normal 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
|
@ -5,8 +5,27 @@ import ScreenSharedHashtag from 'src/screens/Shared/Hashtag'
|
|||||||
import ScreenSharedToot from 'src/screens/Shared/Toot'
|
import ScreenSharedToot from 'src/screens/Shared/Toot'
|
||||||
import ScreenSharedWebview from 'src/screens/Shared/Webview'
|
import ScreenSharedWebview from 'src/screens/Shared/Webview'
|
||||||
import PostToot from 'src/screens/Shared/Compose'
|
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 [
|
return [
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
key='Screen-Shared-Account'
|
key='Screen-Shared-Account'
|
||||||
@ -15,7 +34,7 @@ const sharedScreens = (Stack: any) => {
|
|||||||
options={{
|
options={{
|
||||||
headerTranslucent: true,
|
headerTranslucent: true,
|
||||||
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
|
||||||
headerCenter: () => {}
|
headerCenter: () => <></>
|
||||||
}}
|
}}
|
||||||
/>,
|
/>,
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
|
@ -3,7 +3,8 @@ import { ActivityIndicator, AppState, FlatList, Text, View } from 'react-native'
|
|||||||
import { setFocusHandler, useInfiniteQuery } from 'react-query'
|
import { setFocusHandler, useInfiniteQuery } from 'react-query'
|
||||||
|
|
||||||
import StatusInNotifications from 'src/components/StatusInNotifications'
|
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'
|
import { timelineFetch } from 'src/utils/fetches/timelineFetch'
|
||||||
|
|
||||||
// Opening nesting hashtag pages
|
// Opening nesting hashtag pages
|
||||||
@ -37,10 +38,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
return () => AppState.removeEventListener('change', handleAppStateChange)
|
return () => AppState.removeEventListener('change', handleAppStateChange)
|
||||||
})
|
})
|
||||||
|
|
||||||
const queryKey: App.QueryKey = [
|
const queryKey: App.QueryKey = [page, { page, hashtag, list, toot, account }]
|
||||||
page,
|
|
||||||
{ page, hashtag, list, toot, account }
|
|
||||||
]
|
|
||||||
const {
|
const {
|
||||||
isLoading,
|
isLoading,
|
||||||
isFetchingMore,
|
isFetchingMore,
|
||||||
@ -50,9 +48,7 @@ const Timeline: React.FC<Props> = ({
|
|||||||
fetchMore
|
fetchMore
|
||||||
} = useInfiniteQuery(queryKey, timelineFetch)
|
} = useInfiniteQuery(queryKey, timelineFetch)
|
||||||
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
|
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
|
||||||
// if (page==='Toot'){
|
// const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
|
||||||
// console.log(data)
|
|
||||||
// }
|
|
||||||
|
|
||||||
let content
|
let content
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
@ -67,18 +63,30 @@ const Timeline: React.FC<Props> = ({
|
|||||||
scrollEnabled={scrollEnabled} // For timeline in Account view
|
scrollEnabled={scrollEnabled} // For timeline in Account view
|
||||||
data={flattenData}
|
data={flattenData}
|
||||||
keyExtractor={({ id }) => id}
|
keyExtractor={({ id }) => id}
|
||||||
renderItem={({ item, index, separators }) =>
|
renderItem={({ item, index, separators }) => {
|
||||||
page === 'Notifications' ? (
|
switch (page) {
|
||||||
|
case 'Conversations':
|
||||||
|
return <TimelineConversation key={index} item={item} />
|
||||||
|
case 'Notifications':
|
||||||
|
return (
|
||||||
<StatusInNotifications
|
<StatusInNotifications
|
||||||
key={index}
|
key={index}
|
||||||
notification={item}
|
notification={item}
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
<StatusInTimeline key={index} status={item} queryKey={queryKey} />
|
default:
|
||||||
|
return (
|
||||||
|
<TimelineDefault
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
queryKey={queryKey}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// {...(state.pointer && { initialScrollIndex: state.pointer })}
|
}}
|
||||||
|
// require getItemLayout
|
||||||
|
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
|
||||||
{...(!disableRefresh && {
|
{...(!disableRefresh && {
|
||||||
onRefresh: () =>
|
onRefresh: () =>
|
||||||
fetchMore(
|
fetchMore(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { configureStore } from '@reduxjs/toolkit'
|
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
|
||||||
import { persistReducer, persistStore } from 'redux-persist'
|
import { persistReducer, persistStore } from 'redux-persist'
|
||||||
import createSecureStore from 'redux-persist-expo-securestore'
|
import createSecureStore from 'redux-persist-expo-securestore'
|
||||||
|
|
||||||
@ -14,7 +14,12 @@ const instancesPersistConfig = {
|
|||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
instances: persistReducer(instancesPersistConfig, instancesSlice)
|
instances: persistReducer(instancesPersistConfig, instancesSlice)
|
||||||
|
},
|
||||||
|
middleware: getDefaultMiddleware({
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: ['persist/PERSIST']
|
||||||
}
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
let persistor = persistStore(store)
|
let persistor = persistStore(store)
|
||||||
|
10
src/utils/fetches/listsFetch.ts
Normal file
10
src/utils/fetches/listsFetch.ts
Normal 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)
|
||||||
|
}
|
@ -47,7 +47,7 @@ export const timelineFetch = async (
|
|||||||
endpoint: 'timelines/home',
|
endpoint: 'timelines/home',
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'Local':
|
case 'Local':
|
||||||
query.local = 'true'
|
query.local = 'true'
|
||||||
@ -57,7 +57,7 @@ export const timelineFetch = async (
|
|||||||
endpoint: 'timelines/public',
|
endpoint: 'timelines/public',
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'LocalPublic':
|
case 'LocalPublic':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -66,7 +66,7 @@ export const timelineFetch = async (
|
|||||||
endpoint: 'timelines/public',
|
endpoint: 'timelines/public',
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'RemotePublic':
|
case 'RemotePublic':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -75,7 +75,7 @@ export const timelineFetch = async (
|
|||||||
endpoint: 'timelines/public',
|
endpoint: 'timelines/public',
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'Notifications':
|
case 'Notifications':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -84,7 +84,7 @@ export const timelineFetch = async (
|
|||||||
endpoint: 'notifications',
|
endpoint: 'notifications',
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'Account_Default':
|
case 'Account_Default':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -105,7 +105,7 @@ export const timelineFetch = async (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
toots = uniqBy([...toots, ...res.body], 'id')
|
toots = uniqBy([...toots, ...res.body], 'id')
|
||||||
return Promise.resolve({ toots: toots })
|
return Promise.resolve({ toots: toots, pointer: null })
|
||||||
|
|
||||||
case 'Account_All':
|
case 'Account_All':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -114,7 +114,7 @@ export const timelineFetch = async (
|
|||||||
endpoint: `accounts/${account}/statuses`,
|
endpoint: `accounts/${account}/statuses`,
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'Account_Media':
|
case 'Account_Media':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -125,7 +125,7 @@ export const timelineFetch = async (
|
|||||||
only_media: 'true'
|
only_media: 'true'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
case 'Hashtag':
|
case 'Hashtag':
|
||||||
res = await client({
|
res = await client({
|
||||||
@ -134,18 +134,43 @@ export const timelineFetch = async (
|
|||||||
endpoint: `timelines/tag/${hashtag}`,
|
endpoint: `timelines/tag/${hashtag}`,
|
||||||
query
|
query
|
||||||
})
|
})
|
||||||
return Promise.resolve({ toots: res.body })
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
|
|
||||||
// case 'List':
|
case 'Conversations':
|
||||||
// res = await client({
|
res = await client({
|
||||||
// method: 'get',
|
method: 'get',
|
||||||
// instance: 'local',
|
instance: 'local',
|
||||||
// endpoint: `timelines/list/${list}`,
|
endpoint: `conversations`,
|
||||||
// query
|
query
|
||||||
// })
|
})
|
||||||
// return {
|
return Promise.resolve({ toots: res.body, pointer: null })
|
||||||
// toots: res.body
|
|
||||||
// }
|
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':
|
case 'Toot':
|
||||||
const current = await client({
|
const current = await client({
|
||||||
@ -168,6 +193,6 @@ export const timelineFetch = async (
|
|||||||
})
|
})
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.error('First time fetching timeline error')
|
console.error('Page is not provided')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.10.1.tgz#06d22fae87ef0ce51c616c34a199726db1403b95"
|
||||||
integrity sha512-Z2kKSk4AwWRQNCBmTjViuBQK0/Lx0jc25TZptn/2gKYUCOuVRvCekoA26u0Tsb3BIQ8tWDsZW14OwDlFUXW1aw==
|
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:
|
react-native-toast-message@^1.3.4:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/react-native-toast-message/-/react-native-toast-message-1.3.4.tgz#19ea1c5d3ad8e9d12f7550c4719a9e0258dca6bd"
|
||||||
|
Reference in New Issue
Block a user