1
0
mirror of https://github.com/tooot-app/app synced 2025-04-15 10:47:46 +02:00

Emoji done

This commit is contained in:
Zhiyuan Zheng 2020-12-03 22:03:06 +01:00
parent d59fabd47f
commit 5866d016bc
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
15 changed files with 599 additions and 373 deletions

View File

@ -12,6 +12,7 @@ import {
import { useSafeAreaInsets } from 'react-native-safe-area-context' import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import Button from './Button'
export interface Props { export interface Props {
children: React.ReactNode children: React.ReactNode
@ -85,12 +86,10 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
style={[styles.handle, { backgroundColor: theme.background }]} style={[styles.handle, { backgroundColor: theme.background }]}
/> />
{children} {children}
<Pressable <Button
onPress={() => closeModal.start(() => handleDismiss())} onPress={() => closeModal.start(() => handleDismiss())}
style={[styles.cancel, { borderColor: theme.primary }]} text='取消'
> />
<Text style={[styles.text, { color: theme.primary }]}></Text>
</Pressable>
</Animated.View> </Animated.View>
</View> </View>
</Modal> </Modal>
@ -111,17 +110,6 @@ const styles = StyleSheet.create({
height: StyleConstants.Spacing.S / 2, height: StyleConstants.Spacing.S / 2,
borderRadius: 100, borderRadius: 100,
top: -StyleConstants.Spacing.M * 2 top: -StyleConstants.Spacing.M * 2
},
cancel: {
padding: StyleConstants.Spacing.S,
marginLeft: StyleConstants.Spacing.L,
marginRight: StyleConstants.Spacing.L,
borderWidth: 1,
borderRadius: 100
},
text: {
fontSize: StyleConstants.Font.Size.L,
textAlign: 'center'
} }
}) })

View File

@ -1,38 +1,74 @@
import { Feather } from '@expo/vector-icons'
import React from 'react' import React from 'react'
import { Pressable, StyleSheet, Text } from 'react-native' import { Pressable, StyleSheet, Text } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
export interface Props { type PropsBase = {
onPress: () => void onPress: () => void
text: string disabled?: boolean
fontSize?: 'S' | 'M' | 'L' buttonSize?: 'S' | 'M'
} }
const Button: React.FC<Props> = ({ onPress, text, fontSize = 'M' }) => { export interface PropsText extends PropsBase {
text: string
icon?: string
size?: 'S' | 'M' | 'L'
}
export interface PropsIcon extends PropsBase {
text?: string
icon: string
size?: 'S' | 'M' | 'L'
}
const Button: React.FC<PropsText | PropsIcon> = ({
onPress,
disabled = false,
buttonSize = 'M',
text,
icon,
size = 'M'
}) => {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<Pressable <Pressable
onPress={onPress} {...(!disabled && { onPress })}
style={[styles.button, { borderColor: theme.primary }]} style={[
styles.button,
{
borderColor: disabled ? theme.secondary : theme.primary,
paddingTop: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'],
paddingBottom: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS']
}
]}
> >
<Text {icon ? (
style={[ <Feather
styles.text, name={icon}
{ color: theme.primary, fontSize: StyleConstants.Font.Size[fontSize] } size={StyleConstants.Font.Size[size]}
]} color={disabled ? theme.secondary : theme.primary}
> />
{text} ) : (
</Text> <Text
style={[
styles.text,
{
color: disabled ? theme.secondary : theme.primary,
fontSize: StyleConstants.Font.Size[size]
}
]}
>
{text}
</Text>
)}
</Pressable> </Pressable>
) )
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
paddingTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.S,
paddingLeft: StyleConstants.Spacing.M, paddingLeft: StyleConstants.Spacing.M,
paddingRight: StyleConstants.Spacing.M, paddingRight: StyleConstants.Spacing.M,
borderWidth: 1, borderWidth: 1,

View File

@ -58,7 +58,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
height: StyleConstants.Avatar.L, height: StyleConstants.Avatar.L,
marginTop: StyleConstants.Spacing.M, marginTop: StyleConstants.Spacing.M,
borderWidth: 0.5, borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6 borderRadius: 6
}, },
left: { left: {

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
import { Button, StyleSheet, Text, TextInput, View } from 'react-native' import { StyleSheet, Text, TextInput, View } from 'react-native'
import { useQuery } from 'react-query' import { useQuery } from 'react-query'
import { debounce } from 'lodash' import { debounce } from 'lodash'
@ -13,6 +13,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import Button from 'src/components/Button'
const Login: React.FC = () => { const Login: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
@ -145,9 +146,9 @@ const Login: React.FC = () => {
returnKeyType='go' returnKeyType='go'
/> />
<Button <Button
title={t('content.login.button')}
disabled={!data?.uri}
onPress={async () => await createApplication()} onPress={async () => await createApplication()}
text={t('content.login.button')}
disabled={!data?.uri}
/> />
{isSuccess && data && data.uri && ( {isSuccess && data && data.uri && (
<View> <View>

View File

@ -208,13 +208,13 @@ const styles = StyleSheet.create({
}, },
account_types: { marginLeft: StyleConstants.Spacing.S }, account_types: { marginLeft: StyleConstants.Spacing.S },
fields: { fields: {
borderTopWidth: 0.5, borderTopWidth: StyleSheet.hairlineWidth,
marginBottom: StyleConstants.Spacing.M marginBottom: StyleConstants.Spacing.M
}, },
field: { field: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
borderBottomWidth: 0.5, borderBottomWidth: StyleSheet.hairlineWidth,
paddingTop: StyleConstants.Spacing.S, paddingTop: StyleConstants.Spacing.S,
paddingBottom: StyleConstants.Spacing.S paddingBottom: StyleConstants.Spacing.S
}, },

View File

@ -5,7 +5,7 @@ import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { store } from 'src/store' import { store } from 'src/store'
import PostMain from './Compose/PostMain' import ComposeRoot from './Compose/Root'
import client from 'src/api/client' import client from 'src/api/client'
import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice' import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice'
import { HeaderLeft, HeaderRight } from 'src/components/Header' import { HeaderLeft, HeaderRight } from 'src/components/Header'
@ -105,7 +105,7 @@ export type PostAction =
const postInitialState: PostState = { const postInitialState: PostState = {
text: { text: {
count: 0, count: 500,
raw: '', raw: '',
formatted: undefined formatted: undefined
}, },
@ -285,13 +285,15 @@ const Compose: React.FC = () => {
<HeaderRight <HeaderRight
onPress={async () => tootPost()} onPress={async () => tootPost()}
text='发嘟嘟' text='发嘟嘟'
disabled={postState.text.raw.length < 1} disabled={
postState.text.raw.length < 1 || postState.text.count < 0
}
/> />
) )
}} }}
> >
{() => ( {() => (
<PostMain postState={postState} postDispatch={postDispatch} /> <ComposeRoot postState={postState} postDispatch={postDispatch} />
)} )}
</Stack.Screen> </Stack.Screen>
</Stack.Navigator> </Stack.Navigator>

View File

@ -0,0 +1,147 @@
import { Feather } from '@expo/vector-icons'
import React, { Dispatch } from 'react'
import {
ActionSheetIOS,
Keyboard,
Pressable,
StyleSheet,
Text,
TextInput
} from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, PostState } from '../Compose'
import addAttachments from './addAttachments'
export interface Props {
textInputRef: React.RefObject<TextInput>
postState: PostState
postDispatch: Dispatch<PostAction>
}
const ComposeActions: React.FC<Props> = ({
textInputRef,
postState,
postDispatch
}) => {
const { theme } = useTheme()
const getVisibilityIcon = () => {
switch (postState.visibility) {
case 'public':
return 'globe'
case 'unlisted':
return 'unlock'
case 'private':
return 'lock'
case 'direct':
return 'mail'
}
}
return (
<Pressable
style={[
styles.additions,
{ backgroundColor: theme.background, borderTopColor: theme.border }
]}
onPress={() => Keyboard.dismiss()}
>
<Feather
name='aperture'
size={24}
color={postState.poll.active ? theme.secondary : theme.primary}
onPress={async () =>
!postState.poll.active &&
(await addAttachments({ postState, postDispatch }))
}
/>
<Feather
name='bar-chart-2'
size={24}
color={
postState.attachments.length > 0 ? theme.secondary : theme.primary
}
onPress={() => {
if (postState.attachments.length === 0) {
postDispatch({
type: 'poll',
payload: { ...postState.poll, active: !postState.poll.active }
})
}
if (postState.poll.active) {
textInputRef.current?.focus()
}
}}
/>
<Feather
name={getVisibilityIcon()}
size={24}
color={theme.primary}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
cancelButtonIndex: 4
},
buttonIndex => {
switch (buttonIndex) {
case 0:
postDispatch({ type: 'visibility', payload: 'public' })
break
case 1:
postDispatch({ type: 'visibility', payload: 'unlisted' })
break
case 2:
postDispatch({ type: 'visibility', payload: 'private' })
break
case 3:
postDispatch({ type: 'visibility', payload: 'direct' })
break
}
}
)
}
/>
<Feather
name='smile'
size={24}
color={postState.emojis?.length ? theme.primary : theme.secondary}
onPress={() => {
if (postState.emojis?.length && postState.overlay === null) {
Keyboard.dismiss()
postDispatch({ type: 'overlay', payload: 'emojis' })
}
if (postState.overlay === 'emojis') {
postDispatch({ type: 'overlay', payload: null })
}
}}
/>
<Text
style={[
styles.count,
{ color: postState.text.count < 0 ? theme.error : theme.primary }
]}
>
{postState.text.count}
</Text>
</Pressable>
)
}
const styles = StyleSheet.create({
additions: {
height: 45,
borderTopWidth: StyleSheet.hairlineWidth,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center'
},
count: {
textAlign: 'center',
fontSize: StyleConstants.Font.Size.M,
fontWeight: StyleConstants.Font.Weight.Bold
}
})
export default ComposeActions

View File

@ -9,7 +9,7 @@ export interface Props {
postDispatch: Dispatch<PostAction> postDispatch: Dispatch<PostAction>
} }
const PostAttachments: React.FC<Props> = ({ postState, postDispatch }) => { const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
{postState.attachments.map((attachment, index) => ( {postState.attachments.map((attachment, index) => (
@ -63,4 +63,4 @@ const styles = StyleSheet.create({
} }
}) })
export default PostAttachments export default ComposeAttachments

View File

@ -0,0 +1,108 @@
import { forEach, groupBy, sortBy } from 'lodash'
import React, { Dispatch } from 'react'
import {
Image,
Pressable,
SectionList,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, PostState } from '../Compose'
import updateText from './updateText'
export interface Props {
textInputRef: React.RefObject<TextInput>
onChangeText: any
postState: PostState
postDispatch: Dispatch<PostAction>
}
const ComposeEmojis: React.FC<Props> = ({
textInputRef,
onChangeText,
postState,
postDispatch
}) => {
const { theme } = useTheme()
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []
forEach(
groupBy(sortBy(postState.emojis, ['category', 'shortcode']), 'category'),
(value, key) => sortedEmojis.push({ title: key, data: value })
)
return (
<View style={styles.base}>
<SectionList
horizontal
sections={sortedEmojis}
keyExtractor={item => item.shortcode}
renderSectionHeader={({ section: { title } }) => (
<Text style={[styles.group, { color: theme.secondary }]}>
{title}
</Text>
)}
renderItem={({ section, index }) => {
if (index === 0) {
return (
<View key={section.title} style={styles.emojis}>
{section.data.map(emoji => (
<Pressable
key={emoji.shortcode}
onPress={() => {
updateText({
onChangeText,
postState,
newText: `:${emoji.shortcode}:`
})
textInputRef.current?.focus()
postDispatch({ type: 'overlay', payload: null })
}}
>
<Image source={{ uri: emoji.url }} style={styles.emoji} />
</Pressable>
))}
</View>
)
} else {
return null
}
}}
/>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
height: 260
},
group: {
position: 'absolute',
left: StyleConstants.Spacing.L,
fontSize: StyleConstants.Font.Size.S
},
emojis: {
flex: 1,
flexWrap: 'wrap',
marginTop: StyleConstants.Spacing.M,
marginLeft: StyleConstants.Spacing.M
},
emoji: {
width: 32,
height: 32,
padding: StyleConstants.Spacing.S,
margin: StyleConstants.Spacing.S
}
})
export default ComposeEmojis

View File

@ -0,0 +1,186 @@
import React, { Dispatch, useEffect, useState } from 'react'
import {
ActionSheetIOS,
Pressable,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { Feather } from '@expo/vector-icons'
import { PostAction, PostState } from '../Compose'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants'
import Button from 'src/components/Button'
import { MenuContainer, MenuRow } from 'src/components/Menu'
export interface Props {
postState: PostState
postDispatch: Dispatch<PostAction>
}
const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
const { theme } = useTheme()
const expireMapping: { [key: string]: string } = {
'300': '5分钟',
'1800': '30分钟',
'3600': '1小时',
'21600': '6小时',
'86400': '1天',
'259200': '3天',
'604800': '7天'
}
const [firstRender, setFirstRender] = useState(true)
useEffect(() => {
setFirstRender(false)
}, [])
return (
<View style={[styles.base, { borderColor: theme.border }]}>
<View style={styles.options}>
{[...Array(postState.poll.total)].map((e, i) => (
<View key={i} style={styles.option}>
<Feather
name={postState.poll.multiple ? 'square' : 'circle'}
size={StyleConstants.Font.Size.L}
color={theme.secondary}
/>
<TextInput
{...(i === 0 && firstRender && { autoFocus: true })}
style={[
styles.textInput,
{ borderColor: theme.border, color: theme.primary }
]}
placeholder={`选项 ${i}`}
placeholderTextColor={theme.secondary}
maxLength={50}
value={postState.poll.options[i]}
onChangeText={e =>
postDispatch({
type: 'poll',
payload: {
...postState.poll,
options: { ...postState.poll.options, [i]: e }
}
})
}
/>
</View>
))}
</View>
<View style={styles.controlAmount}>
<View style={styles.firstButton}>
<Button
onPress={() =>
postState.poll.total > 2 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, total: postState.poll.total - 1 }
})
}
icon='minus'
disabled={!(postState.poll.total > 2)}
buttonSize='S'
/>
</View>
<Button
onPress={() =>
postState.poll.total < 4 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, total: postState.poll.total + 1 }
})
}
icon='plus'
disabled={!(postState.poll.total < 4)}
buttonSize='S'
/>
</View>
<MenuContainer>
<MenuRow
title='可选项'
content={postState.poll.multiple ? '多选' : '单选'}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['单选', '多选', '取消'],
cancelButtonIndex: 2
},
index =>
index < 2 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, multiple: index === 1 }
})
)
}
iconBack='chevron-right'
/>
<MenuRow
title='有效期'
content={expireMapping[postState.poll.expire]}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: [...Object.values(expireMapping), '取消'],
cancelButtonIndex: 7
},
index =>
index < 7 &&
postDispatch({
type: 'poll',
payload: {
...postState.poll,
expire: Object.keys(expireMapping)[index]
}
})
)
}
iconBack='chevron-right'
/>
</MenuContainer>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6
},
options: {
marginTop: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S
},
option: {
marginLeft: StyleConstants.Spacing.M,
marginRight: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.S,
flexDirection: 'row',
alignItems: 'center'
},
textInput: {
flex: 1,
padding: StyleConstants.Spacing.S,
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 6,
fontSize: StyleConstants.Font.Size.M,
marginLeft: StyleConstants.Spacing.S
},
controlAmount: {
flex: 1,
flexDirection: 'row',
justifyContent: 'flex-end',
marginRight: StyleConstants.Spacing.M,
marginBottom: StyleConstants.Spacing.M
},
firstButton: {
marginRight: StyleConstants.Spacing.S
}
})
export default ComposePoll

View File

@ -1,44 +0,0 @@
import React, { Dispatch } from 'react'
import { Image, Pressable } from 'react-native'
import { PostAction, PostState } from '../Compose'
import updateText from './updateText'
export interface Props {
onChangeText: any
postState: PostState
postDispatch: Dispatch<PostAction>
}
const PostEmojis: React.FC<Props> = ({
onChangeText,
postState,
postDispatch
}) => {
return (
<>
{postState.emojis?.map((emoji, index) => (
<Pressable
key={index}
onPress={() => {
updateText({
onChangeText,
postState,
newText: `:${emoji.shortcode}:`
})
postDispatch({ type: 'overlay', payload: null })
}}
>
<Image
key={index}
source={{ uri: emoji.url }}
style={{ width: 24, height: 24 }}
/>
</Pressable>
))}
</>
)
}
export default PostEmojis

View File

@ -1,144 +0,0 @@
import React, { Dispatch, useState } from 'react'
import {
ActionSheetIOS,
Pressable,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { Feather } from '@expo/vector-icons'
import { PostAction, PostState } from '../Compose'
export interface Props {
postState: PostState
postDispatch: Dispatch<PostAction>
}
const PostPoll: React.FC<Props> = ({ postState, postDispatch }) => {
const expireMapping: { [key: string]: string } = {
'300': '5分钟',
'1800': '30分钟',
'3600': '1小时',
'21600': '6小时',
'86400': '1天',
'259200': '3天',
'604800': '7天'
}
return (
<View style={styles.base}>
{[...Array(postState.poll.total)].map((e, i) => (
<View key={i} style={styles.option}>
{postState.poll.multiple ? (
<Feather name='square' size={20} />
) : (
<Feather name='circle' size={20} />
)}
<TextInput
style={styles.textInput}
maxLength={50}
value={postState.poll.options[i]}
onChangeText={e =>
postDispatch({
type: 'poll',
payload: {
...postState.poll,
options: { ...postState.poll.options, [i]: e }
}
})
}
/>
</View>
))}
<View style={styles.totalControl}>
<Feather
name='minus'
size={20}
color={postState.poll.total > 2 ? 'black' : 'grey'}
onPress={() =>
postState.poll.total > 2 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, total: postState.poll.total - 1 }
})
}
/>
<Feather
name='plus'
size={20}
color={postState.poll.total < 4 ? 'black' : 'grey'}
onPress={() =>
postState.poll.total < 4 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, total: postState.poll.total + 1 }
})
}
/>
</View>
<Pressable
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['单选', '多选', '取消'],
cancelButtonIndex: 2
},
index =>
index < 2 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, multiple: index === 1 }
})
)
}
>
<Text>{postState.poll.multiple ? '多选' : '单选'}</Text>
</Pressable>
<Pressable
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: [...Object.values(expireMapping), '取消'],
cancelButtonIndex: 7
},
index =>
index < 7 &&
postDispatch({
type: 'poll',
payload: {
...postState.poll,
expire: Object.keys(expireMapping)[index]
}
})
)
}
>
<Text>{expireMapping[postState.poll.expire]}</Text>
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
backgroundColor: 'green'
},
option: {
height: 30,
margin: 5,
flexDirection: 'row'
},
textInput: {
flex: 1,
backgroundColor: 'white'
},
totalControl: {
alignSelf: 'flex-end',
flexDirection: 'row'
}
})
export default PostPoll

View File

@ -3,6 +3,7 @@ import React, {
Dispatch, Dispatch,
useCallback, useCallback,
useEffect, useEffect,
useRef,
useState useState
} from 'react' } from 'react'
import { import {
@ -21,20 +22,24 @@ import * as ImagePicker from 'expo-image-picker'
import { debounce, differenceWith, isEqual } from 'lodash' import { debounce, differenceWith, isEqual } from 'lodash'
import Autolinker from 'src/modules/autolinker' import Autolinker from 'src/modules/autolinker'
import PostEmojis from './PostEmojis' import ComposeEmojis from './Emojis'
import PostPoll from './PostPoll' import ComposePoll from './Poll'
import PostSuggestions from './PostSuggestions' import ComposeSuggestions from './Suggestions'
import { emojisFetch } from 'src/utils/fetches/emojisFetch' import { emojisFetch } from 'src/utils/fetches/emojisFetch'
import { PostAction, PostState } from 'src/screens/Shared/Compose' import { PostAction, PostState } from 'src/screens/Shared/Compose'
import addAttachments from './addAttachments' import addAttachments from './addAttachments'
import PostAttachments from './PostAttachments' import ComposeAttachments from './Attachments'
import { useTheme } from 'src/utils/styles/ThemeManager'
import { StyleConstants } from 'src/utils/styles/constants'
import ComposeActions from './Actions'
export interface Props { export interface Props {
postState: PostState postState: PostState
postDispatch: Dispatch<PostAction> postDispatch: Dispatch<PostAction>
} }
const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
const { theme } = useTheme()
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
const { status } = await ImagePicker.requestCameraRollPermissionsAsync() const { status } = await ImagePicker.requestCameraRollPermissionsAsync()
@ -44,8 +49,6 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
})() })()
}, []) }, [])
const [editorMinHeight, setEditorMinHeight] = useState(0)
const { data: emojisData } = useQuery(['Emojis'], emojisFetch) const { data: emojisData } = useQuery(['Emojis'], emojisFetch)
useEffect(() => { useEffect(() => {
if (emojisData && emojisData.length) { if (emojisData && emojisData.length) {
@ -153,33 +156,49 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
}) })
}, []) }, [])
const getVisibilityIcon = () => { const textInputRef = useRef<TextInput>(null)
switch (postState.visibility) {
case 'public': const renderOverlay = (overlay: PostState['overlay']) => {
return 'globe' switch (overlay) {
case 'unlisted': case 'emojis':
return 'unlock' return (
case 'private': <View style={styles.emojis}>
return 'lock' <ComposeEmojis
case 'direct': textInputRef={textInputRef}
return 'mail' onChangeText={onChangeText}
postState={postState}
postDispatch={postDispatch}
/>
</View>
)
case 'suggestions':
return (
<View style={styles.suggestions}>
<ComposeSuggestions
onChangeText={onChangeText}
postState={postState}
postDispatch={postDispatch}
/>
</View>
)
} }
} }
return ( return (
<View style={styles.base}> <View style={styles.base}>
<ScrollView <ScrollView
style={styles.contentView} style={[styles.contentView]}
alwaysBounceVertical={false} alwaysBounceVertical={false}
keyboardDismissMode='interactive' keyboardDismissMode='interactive'
// child touch event not picked up
keyboardShouldPersistTaps='always'
> >
<TextInput <TextInput
style={[ style={[
styles.textInput styles.textInput,
// { {
// flex: postState.overlay ? 0 : 1, color: theme.primary
// minHeight: editorMinHeight + 14 }
// }
]} ]}
autoCapitalize='none' autoCapitalize='none'
autoCorrect={false} autoCorrect={false}
@ -187,10 +206,8 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
enablesReturnKeyAutomatically enablesReturnKeyAutomatically
multiline multiline
placeholder='想说点什么' placeholder='想说点什么'
placeholderTextColor={theme.secondary}
onChangeText={content => onChangeText({ content })} onChangeText={content => onChangeText({ content })}
onContentSizeChange={({ nativeEvent }) => {
setEditorMinHeight(nativeEvent.contentSize.height)
}}
onSelectionChange={({ onSelectionChange={({
nativeEvent: { nativeEvent: {
selection: { start, end } selection: { start, end }
@ -198,13 +215,17 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
}) => { }) => {
postDispatch({ type: 'selection', payload: { start, end } }) postDispatch({ type: 'selection', payload: { start, end } })
}} }}
ref={textInputRef}
scrollEnabled scrollEnabled
> >
<Text>{postState.text.formatted}</Text> <Text>{postState.text.formatted}</Text>
</TextInput> </TextInput>
{renderOverlay(postState.overlay)}
{postState.attachments.length > 0 && ( {postState.attachments.length > 0 && (
<View style={styles.attachments}> <View style={styles.attachments}>
<PostAttachments <ComposeAttachments
postState={postState} postState={postState}
postDispatch={postDispatch} postDispatch={postDispatch}
/> />
@ -212,94 +233,15 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
)} )}
{postState.poll.active && ( {postState.poll.active && (
<View style={styles.poll}> <View style={styles.poll}>
<PostPoll postState={postState} postDispatch={postDispatch} /> <ComposePoll postState={postState} postDispatch={postDispatch} />
</View> </View>
)} )}
{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>
) : (
<></>
)}
</ScrollView> </ScrollView>
<Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}> <ComposeActions
<Feather textInputRef={textInputRef}
name='paperclip' postState={postState}
size={24} postDispatch={postDispatch}
color={postState.poll.active ? 'gray' : 'black'} />
onPress={async () =>
!postState.poll.active &&
(await addAttachments({ postState, postDispatch }))
}
/>
<Feather
name='bar-chart-2'
size={24}
color={postState.attachments.length > 0 ? 'gray' : 'black'}
onPress={() =>
postState.attachments.length === 0 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, active: !postState.poll.active }
})
}
/>
<Feather
name={getVisibilityIcon()}
size={24}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
cancelButtonIndex: 4
},
buttonIndex => {
switch (buttonIndex) {
case 0:
postDispatch({ type: 'visibility', payload: 'public' })
break
case 1:
postDispatch({ type: 'visibility', payload: 'unlisted' })
break
case 2:
postDispatch({ type: 'visibility', payload: 'private' })
break
case 3:
postDispatch({ type: 'visibility', payload: 'direct' })
break
}
}
)
}
/>
<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> </View>
) )
} }
@ -308,36 +250,29 @@ const styles = StyleSheet.create({
base: { base: {
flex: 1 flex: 1
}, },
contentView: { contentView: { flex: 1 },
flex: 1,
backgroundColor: 'gray'
},
textInput: { textInput: {
backgroundColor: 'lightgray', fontSize: StyleConstants.Font.Size.M,
paddingBottom: 20 marginTop: StyleConstants.Spacing.S,
marginBottom: StyleConstants.Spacing.M,
paddingLeft: StyleConstants.Spacing.Global.PagePadding,
paddingRight: StyleConstants.Spacing.Global.PagePadding
}, },
attachments: { attachments: {
flex: 1, flex: 1,
height: 100 height: 100
}, },
poll: { poll: {
height: 100 flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
}, },
suggestions: { suggestions: {
flex: 1, flex: 1,
backgroundColor: 'lightyellow' backgroundColor: 'lightyellow'
}, },
emojis: { emojis: {
flex: 1, flex: 1
backgroundColor: 'lightblue'
},
additions: {
height: 44,
backgroundColor: 'red',
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'center'
} }
}) })
export default PostMain export default ComposeRoot

View File

@ -46,7 +46,7 @@ export interface Props {
postDispatch: Dispatch<PostAction> postDispatch: Dispatch<PostAction>
} }
const PostSuggestions: React.FC<Props> = ({ const ComposeSuggestions: React.FC<Props> = ({
onChangeText, onChangeText,
postState, postState,
postDispatch postDispatch
@ -94,4 +94,4 @@ const PostSuggestions: React.FC<Props> = ({
return content return content
} }
export default PostSuggestions export default ComposeSuggestions

View File

@ -9,16 +9,27 @@ const updateText = ({
postState: PostState postState: PostState
newText: string newText: string
}) => { }) => {
onChangeText({ if (postState.text.raw.length) {
content: postState.text.raw const contentFront = postState.text.raw.slice(0, postState.selection.start)
? [ const contentRear = postState.text.raw.slice(postState.selection.end)
postState.text.raw.slice(0, postState.selection.start),
newText, const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
postState.text.raw.slice(postState.selection.end) const whiteSpaceRear = /\s/g.test(contentRear.slice(-1))
].join('')
: newText, const newTextWithSpace = `${whiteSpaceFront ? '' : ' '}${newText}${
disableDebounce: true whiteSpaceRear ? '' : ' '
}) }`
onChangeText({
content: [contentFront, newTextWithSpace, contentRear].join(''),
disableDebounce: true
})
} else {
onChangeText({
content: `${newText} `,
disableDebounce: true
})
}
} }
export default updateText export default updateText