1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00

Fix safe area view and keyboard view

This commit is contained in:
Zhiyuan Zheng
2020-11-15 23:33:01 +01:00
parent 188b97fbd8
commit 5cf6eaa8d9
3 changed files with 245 additions and 257 deletions

View File

@ -7,7 +7,6 @@ import React from 'react'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import store from 'src/stacks/common/store' import store from 'src/stacks/common/store'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import Toast from 'react-native-toast-message' import Toast from 'react-native-toast-message'
import { StatusBar } from 'expo-status-bar' import { StatusBar } from 'expo-status-bar'
@ -25,66 +24,64 @@ export const Index: React.FC = () => {
<Provider store={store}> <Provider store={store}>
<StatusBar style='auto' /> <StatusBar style='auto' />
<SafeAreaProvider> <NavigationContainer>
<NavigationContainer> <Tab.Navigator
<Tab.Navigator screenOptions={({ route }) => ({
screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => {
tabBarIcon: ({ focused, color, size }) => { let name: string
let name: string switch (route.name) {
switch (route.name) { case 'Local':
case 'Local': name = 'home'
name = 'home' break
break case 'Public':
case 'Public': name = 'globe'
name = 'globe' break
break case 'PostRoot':
case 'PostRoot': name = 'plus'
name = 'plus' break
break case 'Notifications':
case 'Notifications': name = 'bell'
name = 'bell' break
break case 'Me':
case 'Me': name = focused ? 'smile' : 'meh'
name = focused ? 'smile' : 'meh' break
break default:
default: name = 'alert-octagon'
name = 'alert-octagon' break
break }
} return <Feather name={name} size={size} color={color} />
return <Feather name={name} size={size} color={color} /> }
})}
tabBarOptions={{
activeTintColor: 'black',
inactiveTintColor: 'gray',
showLabel: false
}}
>
<Tab.Screen name='Local' component={Local} />
<Tab.Screen name='Public' component={Public} />
<Tab.Screen
name='PostRoot'
component={PostRoot}
listeners={({ navigation, route }) => ({
tabPress: e => {
e.preventDefault()
const {
length,
[length - 1]: last
} = navigation.dangerouslyGetState().history
navigation.navigate(last.key.split(new RegExp(/(.*?)-/))[1], {
screen: 'PostToot'
})
} }
})} })}
tabBarOptions={{ />
activeTintColor: 'black', <Tab.Screen name='Notifications' component={Notifications} />
inactiveTintColor: 'gray', <Tab.Screen name='Me' component={Me} />
showLabel: false </Tab.Navigator>
}}
>
<Tab.Screen name='Local' component={Local} />
<Tab.Screen name='Public' component={Public} />
<Tab.Screen
name='PostRoot'
component={PostRoot}
listeners={({ navigation, route }) => ({
tabPress: e => {
e.preventDefault()
const {
length,
[length - 1]: last
} = navigation.dangerouslyGetState().history
navigation.navigate(last.key.split(new RegExp(/(.*?)-/))[1], {
screen: 'PostToot'
})
}
})}
/>
<Tab.Screen name='Notifications' component={Notifications} />
<Tab.Screen name='Me' component={Me} />
</Tab.Navigator>
<Toast ref={(ref: any) => Toast.setRef(ref)} /> <Toast ref={(ref: any) => Toast.setRef(ref)} />
</NavigationContainer> </NavigationContainer>
</SafeAreaProvider>
</Provider> </Provider>
) )
} }

View File

@ -1,5 +1,12 @@
import React, { ReactNode, useReducer } from 'react' import React, { ReactNode, useEffect, useReducer, useState } from 'react'
import { Alert, Pressable, Text } from 'react-native' import {
Alert,
Keyboard,
KeyboardAvoidingView,
Pressable,
Text
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
@ -24,11 +31,6 @@ export type PostState = {
} }
| undefined | undefined
emojis: mastodon.Emoji[] | undefined emojis: mastodon.Emoji[] | undefined
height: {
view: number
editor: number
keyboard: number
}
} }
export type PostAction = export type PostAction =
@ -52,10 +54,6 @@ export type PostAction =
type: 'emojis' type: 'emojis'
payload: PostState['emojis'] payload: PostState['emojis']
} }
| {
type: 'height'
payload: Partial<PostState['height']>
}
const postInitialState: PostState = { const postInitialState: PostState = {
text: { text: {
@ -66,12 +64,7 @@ const postInitialState: PostState = {
selection: { start: 0, end: 0 }, selection: { start: 0, end: 0 },
overlay: null, overlay: null,
tag: undefined, tag: undefined,
emojis: undefined, emojis: undefined
height: {
view: 0,
editor: 0,
keyboard: 0
}
} }
const postReducer = (state: PostState, action: PostAction): PostState => { const postReducer = (state: PostState, action: PostAction): PostState => {
switch (action.type) { switch (action.type) {
@ -85,8 +78,6 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
return { ...state, tag: action.payload } return { ...state, tag: action.payload }
case 'emojis': case 'emojis':
return { ...state, emojis: action.payload } return { ...state, emojis: action.payload }
case 'height':
return { ...state, height: { ...state.height, ...action.payload } }
default: default:
throw new Error('Unexpected action') throw new Error('Unexpected action')
} }
@ -95,77 +86,102 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
const PostToot: React.FC = () => { const PostToot: React.FC = () => {
const navigation = useNavigation() const navigation = useNavigation()
const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => {
Keyboard.addListener('keyboardWillShow', _keyboardDidShow)
Keyboard.addListener('keyboardWillHide', _keyboardDidHide)
// cleanup function
return () => {
Keyboard.removeListener('keyboardWillShow', _keyboardDidShow)
Keyboard.removeListener('keyboardWillHide', _keyboardDidHide)
}
}, [])
const _keyboardDidShow = () => {
setHasKeyboard(true)
}
const _keyboardDidHide = () => {
setHasKeyboard(false)
}
const [postState, postDispatch] = useReducer(postReducer, postInitialState) const [postState, postDispatch] = useReducer(postReducer, postInitialState)
return ( return (
<Stack.Navigator> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<Stack.Screen <SafeAreaView
name='PostMain' style={{ flex: 1 }}
options={{ edges={hasKeyboard ? ['left', 'right'] : ['left', 'right', 'bottom']}
headerLeft: () => ( >
<Pressable <Stack.Navigator>
onPress={() => <Stack.Screen
Alert.alert('确认取消编辑?', '', [ name='PostMain'
{ text: '继续编辑', style: 'cancel' }, options={{
{ headerLeft: () => (
text: '退出编辑', <Pressable
style: 'destructive', onPress={() =>
onPress: () => navigation.goBack() Alert.alert('确认取消编辑?', '', [
} { text: '继续编辑', style: 'cancel' },
])
}
>
<Text>退</Text>
</Pressable>
),
headerCenter: () => <></>,
headerRight: () => (
<Pressable
onPress={async () => {
if (postState.text.count < 0) {
Alert.alert('字数超限', '', [
{
text: '返回继续编辑'
}
])
} else {
const res = await client({
method: 'post',
instance: 'local',
endpoint: 'statuses',
headers: {
'Idempotency-Key':
Date.now().toString() + Math.random().toString()
},
query: { status: postState.text.raw }
})
if (res.body.id) {
Alert.alert('发布成功', '', [
{ {
text: '好的', text: '退出编辑',
style: 'destructive',
onPress: () => navigation.goBack() onPress: () => navigation.goBack()
} }
]) ])
} else {
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
} }
} >
}} <Text>退</Text>
> </Pressable>
<Text></Text> ),
</Pressable> headerCenter: () => <></>,
) headerRight: () => (
}} <Pressable
> onPress={async () => {
{props => ( if (postState.text.count < 0) {
<PostMain postState={postState} postDispatch={postDispatch} /> Alert.alert('字数超限', '', [
)} {
</Stack.Screen> text: '返回继续编辑'
</Stack.Navigator> }
])
} else {
const res = await client({
method: 'post',
instance: 'local',
endpoint: 'statuses',
headers: {
'Idempotency-Key':
Date.now().toString() + Math.random().toString()
},
query: { status: postState.text.raw }
})
if (res.body.id) {
Alert.alert('发布成功', '', [
{
text: '好的',
onPress: () => navigation.goBack()
}
])
} else {
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
}
}
}}
>
<Text></Text>
</Pressable>
)
}}
>
{props => (
<PostMain postState={postState} postDispatch={postDispatch} />
)}
</Stack.Screen>
</Stack.Navigator>
</SafeAreaView>
</KeyboardAvoidingView>
) )
} }

View File

@ -1,4 +1,10 @@
import React, { createElement, Dispatch, useCallback, useEffect } from 'react' import React, {
createElement,
Dispatch,
useCallback,
useEffect,
useState
} from 'react'
import { import {
Keyboard, Keyboard,
Pressable, Pressable,
@ -7,7 +13,6 @@ import {
TextInput, TextInput,
View View
} from 'react-native' } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { debounce, differenceWith, isEqual } from 'lodash' import { debounce, differenceWith, isEqual } from 'lodash'
@ -24,27 +29,7 @@ export interface Props {
} }
const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
useEffect(() => { const [editorMinHeight, setEditorMinHeight] = useState(0)
Keyboard.addListener('keyboardDidShow', _keyboardDidShow)
Keyboard.addListener('keyboardDidHide', _keyboardDidHide)
return () => {
Keyboard.removeListener('keyboardDidShow', _keyboardDidShow)
Keyboard.removeListener('keyboardDidHide', _keyboardDidHide)
}
}, [])
const _keyboardDidShow = (props: any) => {
postDispatch({
type: 'height',
payload: { keyboard: props.endCoordinates.height }
})
}
const _keyboardDidHide = () => {
postDispatch({
type: 'height',
payload: { keyboard: 0 }
})
}
const { data: emojisData } = useQuery(['Emojis'], emojisFetch) const { data: emojisData } = useQuery(['Emojis'], emojisFetch)
useEffect(() => { useEffect(() => {
@ -54,15 +39,10 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
}, [emojisData]) }, [emojisData])
const debouncedSuggestions = useCallback( const debouncedSuggestions = useCallback(
debounce( debounce(tag => {
tag => postDispatch({ type: 'overlay', payload: 'suggestions' })
(() => { postDispatch({ type: 'tag', payload: tag })
console.log(tag) }, 500),
postDispatch({ type: 'overlay', payload: 'suggestions' })
postDispatch({ type: 'tag', payload: tag })
})(),
300
),
[] []
) )
let prevTags: PostState['tag'][] = [] let prevTags: PostState['tag'][] = []
@ -87,7 +67,6 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
newType = 'url' newType = 'url'
break break
} }
// @ts-ignore
tags.push({ tags.push({
type: newType, type: newType,
text: props.getMatchedText(), text: props.getMatchedText(),
@ -99,7 +78,18 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
const changedTag = differenceWith(prevTags, tags, isEqual) const changedTag = differenceWith(prevTags, tags, isEqual)
// quick delete causes flicking of suggestion box // quick delete causes flicking of suggestion box
if (changedTag.length && tags.length && !disableDebounce) { if (
changedTag.length > 0 &&
tags.length > 0 &&
content.length > 0 &&
!disableDebounce
) {
console.log('changedTag length')
console.log(changedTag.length)
console.log('tags length')
console.log(tags.length)
console.log('changed Tag')
console.log(changedTag)
if (changedTag[0]!.type !== 'url') { if (changedTag[0]!.type !== 'url') {
debouncedSuggestions(changedTag[0]) debouncedSuggestions(changedTag[0])
} }
@ -147,92 +137,77 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
} }
}) })
}, []) }, [])
return ( return (
<SafeAreaView <View style={{ flex: 1 }}>
style={styles.main} <TextInput
edges={['left', 'right', 'bottom']} style={[
onLayout={({ nativeEvent }) => styles.textInput,
postDispatch({ {
type: 'height', flex: postState.overlay ? 0 : 1,
payload: { view: nativeEvent.layout.height } minHeight: editorMinHeight + 14
}) }
} ]}
> autoCapitalize='none'
<View autoCorrect={false}
style={{ height: postState.height.view - postState.height.keyboard }} autoFocus
enablesReturnKeyAutomatically
multiline
placeholder='想说点什么'
onChangeText={content => onChangeText({ content })}
onContentSizeChange={({ nativeEvent }) => {
setEditorMinHeight(nativeEvent.contentSize.height)
}}
onSelectionChange={({
nativeEvent: {
selection: { start, end }
}
}) => {
postDispatch({ type: 'selection', payload: { start, end } })
}}
scrollEnabled
> >
<TextInput <Text>{postState.text.formatted}</Text>
style={[ </TextInput>
styles.textInput, {postState.overlay === 'suggestions' ? (
{ <View style={[styles.suggestions]}>
flex: postState.overlay ? 0 : 1, <PostSuggestions
minHeight: postState.height.editor + 14 onChangeText={onChangeText}
} postState={postState}
]} postDispatch={postDispatch}
autoCapitalize='none'
autoCorrect={false}
autoFocus
enablesReturnKeyAutomatically
multiline
placeholder='想说点什么'
onChangeText={content => onChangeText({ content })}
onContentSizeChange={({ nativeEvent }) => {
postDispatch({
type: 'height',
payload: { editor: nativeEvent.contentSize.height }
})
}}
onSelectionChange={({
nativeEvent: {
selection: { start, end }
}
}) => {
postDispatch({ type: 'selection', payload: { start, end } })
}}
scrollEnabled
>
<Text>{postState.text.formatted}</Text>
</TextInput>
{postState.overlay === 'suggestions' ? (
<View style={[styles.suggestions]}>
<PostSuggestions
onChangeText={onChangeText}
postState={postState}
postDispatch={postDispatch}
/>
</View>
) : (
<></>
)}
{postState.overlay === 'emojis' ? (
<View style={[styles.emojis]}>
<PostEmojis
onChangeText={onChangeText}
postState={postState}
postDispatch={postDispatch}
/>
</View>
) : (
<></>
)}
<Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}>
<Feather name='paperclip' size={24} />
<Feather name='bar-chart-2' size={24} />
<Feather name='eye-off' size={24} />
<Feather
name='smile'
size={24}
color={postState.emojis?.length ? 'black' : 'white'}
onPress={() => {
if (postState.emojis?.length && postState.overlay === null) {
postDispatch({ type: 'overlay', payload: 'emojis' })
}
}}
/> />
<Text>{postState.text.count}</Text> </View>
</Pressable> ) : (
</View> <></>
</SafeAreaView> )}
{postState.overlay === 'emojis' ? (
<View style={[styles.emojis]}>
<PostEmojis
onChangeText={onChangeText}
postState={postState}
postDispatch={postDispatch}
/>
</View>
) : (
<></>
)}
<Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}>
<Feather name='paperclip' size={24} />
<Feather name='bar-chart-2' size={24} />
<Feather name='eye-off' size={24} />
<Feather
name='smile'
size={24}
color={postState.emojis?.length ? 'black' : 'white'}
onPress={() => {
if (postState.emojis?.length && postState.overlay === null) {
postDispatch({ type: 'overlay', payload: 'emojis' })
}
}}
/>
<Text>{postState.text.count}</Text>
</Pressable>
</View>
) )
} }