Lots of updates

This commit is contained in:
Zhiyuan Zheng 2020-11-24 00:18:47 +01:00
parent fba1d0d531
commit 8200375c92
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
23 changed files with 378 additions and 152 deletions

View File

@ -16,9 +16,18 @@ import ScreenMe from 'src/screens/Me'
import { themes } from 'src/utils/styles/themes'
import { useTheme } from 'src/utils/styles/ThemeManager'
import getCurrentTab from 'src/utils/getCurrentTab'
enableScreens()
const Tab = createBottomTabNavigator()
const Tab = createBottomTabNavigator<RootStackParamList>()
export type RootStackParamList = {
'Screen-Local': undefined
'Screen-Public': { publicTab: boolean }
'Screen-Post': undefined
'Screen-Notifications': undefined
'Screen-Me': undefined
}
export const Index: React.FC = () => {
const { mode, theme } = useTheme()
@ -65,11 +74,7 @@ export const Index: React.FC = () => {
listeners={({ navigation, route }) => ({
tabPress: e => {
e.preventDefault()
const {
length,
[length - 1]: last
} = navigation.dangerouslyGetState().history
navigation.navigate(last.key.split(new RegExp(/(.*)-/))[1], {
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose'
})
}

View File

@ -0,0 +1,74 @@
import React from 'react'
import {
Alert,
AlertButton,
AlertOptions,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
export interface Props {
text: string
destructive?: boolean
alertOption?: {
title: string
message?: string | undefined
buttons?: AlertButton[] | undefined
options?: AlertOptions | undefined
}
}
const Core: React.FC<Props> = ({ text, destructive = false }) => {
const { theme } = useTheme()
return (
<View style={styles.core}>
<Text style={{ color: destructive ? theme.dangerous : theme.primary }}>
{text}
</Text>
</View>
)
}
const MenuButton: React.FC<Props> = ({ ...props }) => {
const { theme } = useTheme()
return (
<Pressable
style={[styles.base, { borderBottomColor: theme.separator }]}
onPress={() =>
props.alertOption &&
Alert.alert(
props.alertOption.title,
props.alertOption.message,
props.alertOption.buttons,
props.alertOption.options
)
}
>
<Core {...props} />
</Pressable>
)
}
const styles = StyleSheet.create({
base: {
height: 50,
borderBottomWidth: 1
},
core: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingLeft: constants.GLOBAL_PAGE_PADDING,
paddingRight: constants.GLOBAL_PAGE_PADDING
}
})
export default MenuButton

View File

@ -1,8 +1,24 @@
import React from 'react'
import { View } from 'react-native'
import { StyleSheet, View } from 'react-native'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
const MenuContainer: React.FC = ({ ...props }) => {
return <View>{props.children}</View>
const { theme } = useTheme()
return (
<View style={[styles.base, { borderTopColor: theme.separator }]}>
{props.children}
</View>
)
}
const styles = StyleSheet.create({
base: {
borderTopWidth: 1,
marginBottom: constants.SPACING_M
}
})
export default MenuContainer

View File

@ -4,6 +4,8 @@ import { useNavigation } from '@react-navigation/native'
import { Feather } from '@expo/vector-icons'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
export interface Props {
icon?: string
title: string
@ -16,8 +18,17 @@ 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>
{icon && (
<Feather
name={icon}
size={constants.FONT_SIZE_M + 2}
style={styles.iconLeading}
color={theme.primary}
/>
)}
<Text style={{ color: theme.primary, fontSize: constants.FONT_SIZE_M }}>
{title}
</Text>
{navigateTo && (
<Feather
name='chevron-right'
@ -31,11 +42,12 @@ const Core: React.FC<Props> = ({ icon, title, navigateTo }) => {
}
const MenuItem: React.FC<Props> = ({ ...props }) => {
const { theme } = useTheme()
const navigation = useNavigation()
return props.navigateTo ? (
<Pressable
style={styles.base}
style={[styles.base, { borderBottomColor: theme.separator }]}
onPress={() => {
navigation.navigate(props.navigateTo!, props.navigateToParams)
}}
@ -52,15 +64,14 @@ const MenuItem: React.FC<Props> = ({ ...props }) => {
const styles = StyleSheet.create({
base: {
height: 50,
borderBottomColor: 'lightgray',
borderBottomWidth: 1
},
core: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 12,
paddingRight: 12
paddingLeft: constants.GLOBAL_PAGE_PADDING,
paddingRight: constants.GLOBAL_PAGE_PADDING
},
iconLeading: {
marginRight: 8

View File

@ -0,0 +1,8 @@
import React from 'react'
import { Text } from 'react-native'
const PleaseLogin = () => {
return <Text></Text>
}
export default PleaseLogin

View File

@ -7,9 +7,12 @@ import { Feather } from '@expo/vector-icons'
import Timeline from './Timelines/Timeline'
import sharedScreens from 'src/screens/Shared/sharedScreens'
import { InstancesState } from 'src/utils/slices/instancesSlice'
import { RootState } from 'src/store'
import { getRemoteUrl, InstancesState } from 'src/utils/slices/instancesSlice'
import { RootState, store } from 'src/store'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { useNavigation } from '@react-navigation/native'
import getCurrentTab from 'src/utils/getCurrentTab'
import PleaseLogin from './PleaseLogin'
const Stack = createNativeStackNavigator()
@ -25,22 +28,24 @@ const Page = ({
{localRegistered || page === 'RemotePublic' ? (
<Timeline page={page} />
) : (
<Text></Text>
<PleaseLogin />
)}
</View>
)
}
export interface Props {
name: string
name: 'Screen-Local-Root' | 'Screen-Public-Root'
content: { title: string; page: App.Pages }[]
}
const Timelines: React.FC<Props> = ({ name, content }) => {
const navigation = useNavigation()
const { theme } = useTheme()
const localRegistered = useSelector(
(state: RootState) => state.instances.local.url
)
const publicDomain = getRemoteUrl(store.getState())
const [segment, setSegment] = useState(0)
const [renderHeader, setRenderHeader] = useState(false)
const [segmentManuallyTriggered, setSegmentManuallyTriggered] = useState(
@ -59,58 +64,80 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
<Stack.Screen
name={name}
options={{
headerRight: () =>
renderHeader ? (
<Feather name='search' size={24} color={theme.secondary} />
) : null,
headerCenter: () =>
renderHeader ? (
<SegmentedControl
values={[content[0].title, content[1].title]}
selectedIndex={segment}
onChange={({ nativeEvent }) => {
setSegmentManuallyTriggered(true)
setSegment(nativeEvent.selectedSegmentIndex)
horizontalPaging.current.scrollToIndex({
index: nativeEvent.selectedSegmentIndex
})
}}
style={{ width: 150, height: 30 }}
/>
) : null
headerTitle: name === 'Screen-Public-Root' ? publicDomain : '',
...(renderHeader &&
localRegistered && {
headerCenter: () => (
<SegmentedControl
values={[content[0].title, content[1].title]}
selectedIndex={segment}
onChange={({ nativeEvent }) => {
setSegmentManuallyTriggered(true)
setSegment(nativeEvent.selectedSegmentIndex)
horizontalPaging.current.scrollToIndex({
index: nativeEvent.selectedSegmentIndex
})
}}
style={{ width: 150, height: 30 }}
/>
),
headerRight: () => (
<Feather
name='search'
size={24}
color={theme.secondary}
onPress={() => {
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Search'
})
}}
/>
)
})
}}
>
{() => (
<FlatList
style={{ width: Dimensions.get('window').width, height: '100%' }}
data={content}
extraData={localRegistered}
keyExtractor={({ page }) => page}
renderItem={({ item, index }) => (
<Page key={index} item={item} localRegistered={localRegistered} />
)}
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 / 2
? 0
: 1
)
}
pagingEnabled
showsHorizontalScrollIndicator={false}
/>
)}
{() => {
return (
<FlatList
style={{ width: Dimensions.get('window').width, height: '100%' }}
data={content}
extraData={localRegistered}
keyExtractor={({ page }) => page}
renderItem={({ item, index }) => {
if (!localRegistered && index === 0) {
return null
}
return (
<Page
key={index}
item={item}
localRegistered={localRegistered}
/>
)
}}
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 / 2
? 0
: 1
)
}
pagingEnabled
showsHorizontalScrollIndicator={false}
/>
)
}}
</Stack.Screen>
{sharedScreens(Stack)}

View File

@ -13,7 +13,8 @@ const TimelineSeparator = () => {
const styles = StyleSheet.create({
base: {
borderTopWidth: 1,
marginLeft: constants.SPACING_M + constants.AVATAR_S + constants.SPACING_S
marginLeft: constants.SPACING_M + constants.AVATAR_S + constants.SPACING_S,
marginRight: constants.SPACING_M
}
})

View File

@ -2,6 +2,8 @@ import React from 'react'
import { Image, StyleSheet, Text } from 'react-native'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
const regexEmoji = new RegExp(/(:[a-z0-9_]+:)/)
export interface Props {
@ -23,7 +25,7 @@ const Emojis: React.FC<Props> = ({
fontSize: size,
lineHeight: size + 2,
color: theme.primary,
...(fontBold && { fontWeight: 'bold' })
...(fontBold && { fontWeight: constants.FONT_WEIGHT_BOLD })
},
image: {
width: size,

View File

@ -5,7 +5,7 @@ import Timelines from 'src/components/Timelines'
const ScreenLocal: React.FC = () => {
return (
<Timelines
name='Local'
name='Screen-Local-Root'
content={[
{ title: '关注', page: 'Following' },
{ title: '本站', page: 'Local' }

View File

@ -8,24 +8,30 @@ import ScreenMeFavourites from './Me/Favourites'
import ScreenMeLists from './Me/Lists'
import sharedScreens from 'src/screens/Shared/sharedScreens'
import ScreenMeListsList from './Me/Root/Lists/List'
import { useSelector } from 'react-redux'
import { RootState } from 'src/store'
const Stack = createNativeStackNavigator()
const ScreenMe: React.FC = () => {
const localRegistered = useSelector(
(state: RootState) => state.instances.local.url
)
return (
<Stack.Navigator
screenOptions={{
headerTitle: 'test'
}}
>
<Stack.Navigator>
<Stack.Screen
name='Screen-Me-Root'
component={ScreenMeRoot}
options={{
headerTranslucent: true,
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
headerCenter: () => <></>
}}
options={
localRegistered
? {
headerTranslucent: true,
headerStyle: { backgroundColor: 'rgba(255, 255, 255, 0)' },
headerCenter: () => <></>
}
: { headerTitle: '我的长毛象' }
}
/>
<Stack.Screen
name='Screen-Me-Conversations'

View File

@ -9,6 +9,7 @@ import Login from './Root/Login'
import MyInfo from './Root/MyInfo'
import MyCollections from './Root/MyCollections'
import Settings from './Root/Settings'
import Logout from './Root/Logout'
const ScreenMeRoot: React.FC = () => {
const localRegistered = useSelector(
@ -26,6 +27,7 @@ const ScreenMeRoot: React.FC = () => {
<MyCollections id={getLocalAccountId(store.getState())!} />
)}
<Settings />
{localRegistered && <Logout />}
</ScrollView>
)
}

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react'
import { Button, Text, TextInput, View } from 'react-native'
import { Button, StyleSheet, Text, TextInput, View } from 'react-native'
import { useQuery } from 'react-query'
import { debounce } from 'lodash'
@ -9,8 +9,12 @@ import * as AuthSession from 'expo-auth-session'
import { useDispatch } from 'react-redux'
import { updateLocal } from 'src/utils/slices/instancesSlice'
import { useNavigation } from '@react-navigation/native'
import { useTheme } from 'src/utils/styles/ThemeManager'
import constants from 'src/utils/styles/constants'
const Login: React.FC = () => {
const { theme } = useTheme()
const navigation = useNavigation()
const dispatch = useDispatch()
const [instance, setInstance] = useState('')
@ -109,23 +113,34 @@ const Login: React.FC = () => {
}
)
dispatch(updateLocal({ url: instance, token: accessToken }))
navigation.navigate('Local')
navigation.navigate('Screen-Local-Root')
}
})()
}, [response])
return (
<View>
<View style={styles.base}>
<TextInput
style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
style={{
height: 50,
color: theme.primary,
borderColor: theme.border,
borderWidth: 1,
padding: constants.SPACING_M
}}
onChangeText={onChangeText}
autoCapitalize='none'
autoCorrect={false}
autoFocus
clearButtonMode='unless-editing'
keyboardType='url'
textContentType='URL'
onSubmitEditing={async () =>
isSuccess && data && data.uri && (await createApplication())
}
placeholder='输入服务器'
placeholderTextColor='#888888'
placeholderTextColor={theme.secondary}
returnKeyType='go'
/>
<Button
title='登录'
@ -134,11 +149,17 @@ const Login: React.FC = () => {
/>
{isSuccess && data && data.uri && (
<View>
<Text>{data.title}</Text>
<Text style={{ color: theme.primary }}>{data.title}</Text>
</View>
)}
</View>
)
}
const styles = StyleSheet.create({
base: {
padding: constants.GLOBAL_PAGE_PADDING
}
})
export default Login

View File

@ -0,0 +1,46 @@
import React from 'react'
import { useDispatch } from 'react-redux'
import { updateLocal } from 'src/utils/slices/instancesSlice'
import MenuButton from 'src/components/Menu/Button'
import { MenuContainer } from 'src/components/Menu'
import { useNavigation } from '@react-navigation/native'
const Logout: React.FC = () => {
const dispatch = useDispatch()
const navigation = useNavigation()
const alertOption = {
title: '确认退出登录?',
message: '退出登录后,需要重新认证账号',
buttons: [
{
text: '退出登录',
style: 'destructive' as const,
onPress: () => {
dispatch(updateLocal({}))
navigation.navigate('Screen-Public', {
screen: 'Screen-Public-Root',
params: { publicTab: true }
})
}
},
{
text: '取消',
style: 'cancel' as const
}
]
}
return (
<MenuContainer>
<MenuButton
text='退出当前账号'
destructive={true}
alertOption={alertOption}
/>
</MenuContainer>
)
}
export default Logout

View File

@ -1,5 +1,5 @@
import React from 'react'
import { MenuContainer, MenuHeader, MenuItem } from 'src/components/Menu'
import { MenuContainer, MenuItem } from 'src/components/Menu'
export interface Props {
id: Mastodon.Account['id']
@ -8,7 +8,6 @@ export interface Props {
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' />

View File

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

View File

@ -1,36 +1,25 @@
import React, { useEffect, useState } from 'react'
import React from 'react'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { Feather } from '@expo/vector-icons'
import Timeline from 'src/components/Timelines/Timeline'
import sharedScreens from 'src/screens/Shared/sharedScreens'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { useSelector } from 'react-redux'
import { RootState } from 'src/store'
import PleaseLogin from 'src/components/PleaseLogin'
const Stack = createNativeStackNavigator()
const ScreenNotifications: React.FC = () => {
const { theme } = useTheme()
const [renderHeader, setRenderHeader] = useState(false)
useEffect(() => {
const nbr = setTimeout(() => setRenderHeader(true), 50)
return
}, [])
const localRegistered = useSelector(
(state: RootState) => state.instances.local.url
)
return (
<Stack.Navigator
screenOptions={{
headerRight: () =>
renderHeader ? (
<Feather name='search' size={24} color={theme.secondary} />
) : null,
headerTitle: '通知',
headerLargeTitle: true
}}
>
<Stack.Screen name='Notifications'>
{() => <Timeline page='Notifications' />}
<Stack.Navigator screenOptions={{ headerTitle: '通知' }}>
<Stack.Screen name='Screen-Notifications-Root'>
{() =>
localRegistered ? <Timeline page='Notifications' /> : <PleaseLogin />
}
</Stack.Screen>
{sharedScreens(Stack)}

View File

@ -5,7 +5,7 @@ import Timelines from 'src/components/Timelines'
const ScreenPublic: React.FC = () => {
return (
<Timelines
name='Public'
name='Screen-Public-Root'
content={[
{ title: '跨站', page: 'LocalPublic' },
{ title: '他站', page: 'RemotePublic' }

View File

@ -0,0 +1,7 @@
import React from 'react'
const ScreenSharedSearch: React.FC = () => {
return <></>
}
export default ScreenSharedSearch

View File

@ -5,27 +5,9 @@ import ScreenSharedHashtag from 'src/screens/Shared/Hashtag'
import ScreenSharedToot from 'src/screens/Shared/Toot'
import ScreenSharedWebview from 'src/screens/Shared/Webview'
import Compose 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'
import ScreenSharedSearch from './Search'
const sharedScreens = (
Stack: TypedNavigator<
Record<string, object | undefined>,
any,
NativeStackNavigationOptions,
NativeStackNavigationEventMap,
({
initialRouteName,
children,
screenOptions,
...rest
}: NativeStackNavigatorProps) => JSX.Element
>
) => {
const sharedScreens = (Stack: any) => {
return [
<Stack.Screen
key='Screen-Shared-Account'
@ -68,6 +50,14 @@ const sharedScreens = (
options={{
stackPresentation: 'fullScreenModal'
}}
/>,
<Stack.Screen
key='Screen-Shared-Search'
name='Screen-Shared-Search'
component={ScreenSharedSearch}
options={{
stackPresentation: 'modal'
}}
/>
]
}

View File

@ -0,0 +1,9 @@
const getCurrentTab = (navigation: any) => {
const {
length,
[length - 1]: last
} = navigation.dangerouslyGetState().history
return `Screen-${last.key.split(new RegExp(/Screen-(.*?)-/))[1]}`
}
export default getCurrentTab

View File

@ -17,15 +17,34 @@ export type InstancesState = {
}
}
const initialStateLocal = {
url: undefined,
token: undefined,
account: {
id: undefined,
preferences: {
'posting:default:visibility': undefined,
'posting:default:sensitive': undefined,
'posting:default:language': undefined,
'reading:expand:media': undefined,
'reading:expand:spoilers': undefined
}
}
}
export const updateLocal = createAsyncThunk(
'instances/updateLocal',
async ({
url,
token
}: {
url: InstancesState['local']['url']
token: InstancesState['local']['token']
url?: InstancesState['local']['url']
token?: InstancesState['local']['token']
}) => {
if (!url || !token) {
return initialStateLocal
}
const {
body: { id }
} = await client({
@ -58,22 +77,9 @@ export const updateLocal = createAsyncThunk(
const instancesSlice = createSlice({
name: 'instances',
initialState: {
local: {
url: undefined,
token: undefined,
account: {
id: undefined,
preferences: {
'posting:default:visibility': undefined,
'posting:default:sensitive': undefined,
'posting:default:language': undefined,
'reading:expand:media': undefined,
'reading:expand:spoilers': undefined
}
}
},
local: initialStateLocal,
remote: {
url: 'mastodon.social'
url: 'm.cmx.im'
}
} as InstancesState,
reducers: {},
@ -85,6 +91,7 @@ const instancesSlice = createSlice({
})
export const getLocalUrl = (state: RootState) => state.instances.local.url
export const getRemoteUrl = (state: RootState) => state.instances.remote.url
export const getLocalAccountId = (state: RootState) =>
state.instances.local.account.id
export const getLocalAccountPreferences = (state: RootState) =>

View File

@ -3,6 +3,8 @@ export default {
FONT_SIZE_M: 14,
FONT_SIZE_L: 18,
FONT_WEIGHT_BOLD: '600',
SPACING_XS: 4,
SPACING_S: 8,
SPACING_M: 16,

View File

@ -7,6 +7,7 @@ export type ColorDefinitions =
| 'link'
| 'border'
| 'separator'
| 'dangerous'
const themeColors: {
[key in ColorDefinitions]: {
@ -37,6 +38,10 @@ const themeColors: {
separator: {
light: 'rgba(0, 0, 0, 0.1)',
dark: 'rgba(255, 255, 255, 0.1)'
},
dangerous: {
light: 'rgb(255, 59, 48)',
dark: 'rgb(255, 69, 58)'
}
}