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

Use context to provide compose state and dispatch

This commit is contained in:
Zhiyuan Zheng
2020-12-11 00:29:22 +01:00
parent c114176ee4
commit 0fa9f87f66
15 changed files with 244 additions and 307 deletions

16
App.tsx
View File

@ -16,14 +16,14 @@ setConsole({
error: console.warn error: console.warn
}) })
if (__DEV__) { // if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render') // const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React, { // whyDidYouRender(React, {
trackAllPureComponents: true, // trackAllPureComponents: true,
trackHooks: true, // trackHooks: true,
hotReloadBufferMs: 1000 // hotReloadBufferMs: 1000
}) // })
} // }
const App: React.FC = () => { const App: React.FC = () => {
return ( return (

View File

@ -1,11 +1,22 @@
import React, { ReactNode, useEffect, useReducer, useState } from 'react' import React, {
createContext,
createRef,
Dispatch,
ReactNode,
RefObject,
useEffect,
useReducer,
useRef,
useState
} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Alert, Alert,
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
StyleSheet, StyleSheet,
Text Text,
TextInput
} from 'react-native' } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack' import { createNativeStackNavigator } from 'react-native-screens/native-stack'
@ -67,10 +78,17 @@ export type ComposeState = {
| '604800' | '604800'
| string | string
} }
attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] } attachments: {
sensitive: boolean
uploads: (Mastodon.Attachment & { local_url?: string })[]
}
attachmentUploadProgress?: { progress: number; aspect?: number } attachmentUploadProgress?: { progress: number; aspect?: number }
visibility: 'public' | 'unlisted' | 'private' | 'direct' visibility: 'public' | 'unlisted' | 'private' | 'direct'
replyToStatus?: Mastodon.Status replyToStatus?: Mastodon.Status
textInputFocus: {
current: 'text' | 'spoiler'
refs: { text: RefObject<TextInput>; spoiler: RefObject<TextInput> }
}
} }
export type PostAction = export type PostAction =
@ -110,6 +128,10 @@ export type PostAction =
type: 'visibility' type: 'visibility'
payload: ComposeState['visibility'] payload: ComposeState['visibility']
} }
| {
type: 'textInputFocus'
payload: Partial<ComposeState['textInputFocus']>
}
const composeInitialState: ComposeState = { const composeInitialState: ComposeState = {
spoiler: { spoiler: {
@ -145,7 +167,11 @@ const composeInitialState: ComposeState = {
getLocalAccountPreferences(store.getState())[ getLocalAccountPreferences(store.getState())[
'posting:default:visibility' 'posting:default:visibility'
] || 'public', ] || 'public',
replyToStatus: undefined replyToStatus: undefined,
textInputFocus: {
current: 'text',
refs: { text: createRef(), spoiler: createRef() }
}
} }
const composeExistingState = ({ const composeExistingState = ({
type, type,
@ -244,11 +270,22 @@ const postReducer = (state: ComposeState, action: PostAction): ComposeState => {
} }
case 'visibility': case 'visibility':
return { ...state, visibility: action.payload } return { ...state, visibility: action.payload }
case 'textInputFocus':
return {
...state,
textInputFocus: { ...state.textInputFocus, ...action.payload }
}
default: default:
throw new Error('Unexpected action') throw new Error('Unexpected action')
} }
} }
type ContextType = {
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
export const ComposeContext = createContext<ContextType>({} as ContextType)
export interface Props { export interface Props {
route: { route: {
params: params:
@ -298,14 +335,14 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
case 'edit': case 'edit':
if (params.incomingStatus.spoiler_text) { if (params.incomingStatus.spoiler_text) {
formatText({ formatText({
origin: 'spoiler', textInput: 'spoiler',
composeDispatch, composeDispatch,
content: params.incomingStatus.spoiler_text, content: params.incomingStatus.spoiler_text,
disableDebounce: true disableDebounce: true
}) })
} }
formatText({ formatText({
origin: 'text', textInput: 'text',
composeDispatch, composeDispatch,
content: params.incomingStatus.text!, content: params.incomingStatus.text!,
disableDebounce: true disableDebounce: true
@ -313,7 +350,7 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
break break
case 'reply': case 'reply':
formatText({ formatText({
origin: 'text', textInput: 'text',
composeDispatch, composeDispatch,
content: `@${ content: `@${
params.incomingStatus.reblog params.incomingStatus.reblog
@ -429,6 +466,8 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
const totalTextCount = const totalTextCount =
(composeState.spoiler.active ? composeState.spoiler.count : 0) + (composeState.spoiler.active ? composeState.spoiler.count : 0) +
composeState.text.count composeState.text.count
// doesn't work
const rawCount = composeState.text.raw.length
return ( return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
@ -475,18 +514,17 @@ const Compose: React.FC<Props> = ({ route: { params } }) => {
<HeaderRight <HeaderRight
onPress={async () => tootPost()} onPress={async () => tootPost()}
text='发嘟嘟' text='发嘟嘟'
disabled={ disabled={rawCount < 1 || totalTextCount > 500}
composeState.text.raw.length < 1 || totalTextCount > 500
}
/> />
) )
}} }}
> >
{() => ( {() => (
<ComposeRoot <ComposeContext.Provider
composeState={composeState} value={{ composeState, composeDispatch }}
composeDispatch={composeDispatch} >
/> <ComposeRoot />
</ComposeContext.Provider>
)} )}
</Stack.Screen> </Stack.Screen>
</Stack.Navigator> </Stack.Navigator>
@ -502,4 +540,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(Compose, () => true) export default Compose

View File

@ -1,22 +1,13 @@
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import React, { Dispatch, useCallback, useMemo } from 'react' import React, { useCallback, useContext, useMemo } from 'react'
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native' import { ActionSheetIOS, StyleSheet, View } 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'
import { PostAction, ComposeState } from '../Compose' import { ComposeContext } from '../Compose'
import addAttachments from './addAttachments' import addAttachments from './addAttachments'
export interface Props { const ComposeActions: React.FC = () => {
textInputRef: React.RefObject<TextInput> const { composeState, composeDispatch } = useContext(ComposeContext)
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const ComposeActions: React.FC<Props> = ({
textInputRef,
composeState,
composeDispatch
}) => {
const { theme } = useTheme() const { theme } = useTheme()
const attachmentColor = useMemo(() => { const attachmentColor = useMemo(() => {
@ -71,7 +62,7 @@ const ComposeActions: React.FC<Props> = ({
}) })
} }
if (composeState.poll.active) { if (composeState.poll.active) {
textInputRef.current?.focus() composeState.textInputFocus.refs.text.current?.focus()
} }
}, [ }, [
composeState.poll.active, composeState.poll.active,

View File

@ -1,4 +1,4 @@
import React, { Dispatch, useCallback } from 'react' import React, { useCallback, useContext } from 'react'
import { import {
FlatList, FlatList,
Image, Image,
@ -8,7 +8,7 @@ import {
View View
} from 'react-native' } from 'react-native'
import { PostAction, ComposeState } from '../Compose' import { ComposeContext } from '../Compose'
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'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
@ -19,12 +19,8 @@ import { Feather } from '@expo/vector-icons'
const DEFAULT_HEIGHT = 200 const DEFAULT_HEIGHT = 200
export interface Props { const ComposeAttachments: React.FC = () => {
composeState: ComposeState const { composeState, composeDispatch } = useContext(ComposeContext)
composeDispatch: Dispatch<PostAction>
}
const ComposeAttachments: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
@ -106,7 +102,8 @@ const ComposeAttachments: React.FC<Props> = ({ composeState, composeDispatch })
style={styles.progressContainer} style={styles.progressContainer}
visible={composeState.attachmentUploadProgress === undefined} visible={composeState.attachmentUploadProgress === undefined}
width={ width={
(composeState.attachmentUploadProgress?.aspect || 3 / 2) * DEFAULT_HEIGHT (composeState.attachmentUploadProgress?.aspect || 3 / 2) *
DEFAULT_HEIGHT
} }
height={200} height={200}
> >

View File

@ -349,4 +349,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(ComposeEditAttachment, () => true) export default ComposeEditAttachment

View File

@ -1,34 +1,22 @@
import React, { Dispatch, useCallback, useMemo } from 'react' import React, { useCallback, useContext, useMemo } from 'react'
import { import {
Image, Image,
Pressable, Pressable,
SectionList, SectionList,
StyleSheet, StyleSheet,
Text, Text,
TextInput,
View View
} from 'react-native' } 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'
import { PostAction, ComposeState } from '../Compose' import { ComposeContext } from '../Compose'
import updateText from './updateText' import updateText from './updateText'
export interface Props { const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => {
textInputRef: React.RefObject<TextInput> const { composeState, composeDispatch } = useContext(ComposeContext)
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const SingleEmoji = ({
emoji,
textInputRef,
composeState,
composeDispatch
}: { emoji: Mastodon.Emoji } & Props) => {
const onPress = useCallback(() => { const onPress = useCallback(() => {
updateText({ updateText({
origin: textInputRef.current?.isFocused() ? 'text' : 'spoiler',
composeState, composeState,
composeDispatch, composeDispatch,
newText: `:${emoji.shortcode}:`, newText: `:${emoji.shortcode}:`,
@ -48,7 +36,8 @@ const SingleEmoji = ({
) )
} }
const ComposeEmojis: React.FC<Props> = ({ ...props }) => { const ComposeEmojis: React.FC = () => {
const { composeState } = useContext(ComposeContext)
const { theme } = useTheme() const { theme } = useTheme()
const listHeader = useCallback( const listHeader = useCallback(
@ -61,7 +50,7 @@ const ComposeEmojis: React.FC<Props> = ({ ...props }) => {
const emojiList = useCallback( const emojiList = useCallback(
section => section =>
section.data.map((emoji: Mastodon.Emoji) => ( section.data.map((emoji: Mastodon.Emoji) => (
<SingleEmoji key={emoji.shortcode} emoji={emoji} {...props} /> <SingleEmoji key={emoji.shortcode} emoji={emoji} />
)), )),
[] []
) )
@ -80,7 +69,7 @@ const ComposeEmojis: React.FC<Props> = ({ ...props }) => {
<SectionList <SectionList
horizontal horizontal
keyboardShouldPersistTaps='handled' keyboardShouldPersistTaps='handled'
sections={props.composeState.emoji.emojis!} sections={composeState.emoji.emojis!}
keyExtractor={item => item.shortcode} keyExtractor={item => item.shortcode}
renderSectionHeader={listHeader} renderSectionHeader={listHeader}
renderItem={listItem} renderItem={listItem}
@ -116,4 +105,4 @@ const styles = StyleSheet.create({
} }
}) })
export default ComposeEmojis export default React.memo(ComposeEmojis, () => true)

View File

@ -1,22 +1,20 @@
import React, { Dispatch, useCallback, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native' import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { PostAction, ComposeState } from '../Compose' import { ComposeContext } from '../Compose'
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 { ButtonRow } from 'src/components/Button' import { ButtonRow } from 'src/components/Button'
import { MenuContainer, MenuRow } from 'src/components/Menu' import { MenuContainer, MenuRow } from 'src/components/Menu'
export interface Props { const ComposePoll: React.FC = () => {
poll: ComposeState['poll'] const {
composeDispatch: Dispatch<PostAction> composeState: {
} poll: { total, options, multiple, expire }
},
const ComposePoll: React.FC<Props> = ({ composeDispatch
poll: { total, options, multiple, expire }, } = useContext(ComposeContext)
composeDispatch
}) => {
const { theme } = useTheme() const { theme } = useTheme()
const expireMapping: { [key: string]: string } = { const expireMapping: { [key: string]: string } = {
@ -34,24 +32,6 @@ const ComposePoll: React.FC<Props> = ({
setFirstRender(false) setFirstRender(false)
}, []) }, [])
const minusOnPress = useCallback(
() =>
total > 2 &&
composeDispatch({
type: 'poll',
payload: { total: total - 1 }
}),
[total]
)
console.log('total: ', total)
const plusOnPress = useCallback(() => {
total < 4 &&
composeDispatch({
type: 'poll',
payload: { total: total + 1 }
})
}, [total])
return ( return (
<View style={[styles.base, { borderColor: theme.border }]}> <View style={[styles.base, { borderColor: theme.border }]}>
<View style={styles.options}> <View style={styles.options}>
@ -101,14 +81,28 @@ const ComposePoll: React.FC<Props> = ({
<View style={styles.controlAmount}> <View style={styles.controlAmount}>
<View style={styles.firstButton}> <View style={styles.firstButton}>
<ButtonRow <ButtonRow
onPress={minusOnPress} key={total + 'minus'}
onPress={() => {
total > 2 &&
composeDispatch({
type: 'poll',
payload: { total: total - 1 }
})
}}
icon='minus' icon='minus'
disabled={!(total > 2)} disabled={!(total > 2)}
buttonSize='S' buttonSize='S'
/> />
</View> </View>
<ButtonRow <ButtonRow
onPress={plusOnPress} key={total + 'plus'}
onPress={() => {
total < 4 &&
composeDispatch({
type: 'poll',
payload: { total: total + 1 }
})
}}
icon='plus' icon='plus'
disabled={!(total < 4)} disabled={!(total < 4)}
buttonSize='S' buttonSize='S'

View File

@ -1,6 +1,5 @@
import React, { Dispatch, RefObject } from 'react' import React, { useContext } from 'react'
import { StyleSheet, Text, TextInput, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import TimelineAttachment from 'src/components/Timelines/Timeline/Shared/Attachment' import TimelineAttachment from 'src/components/Timelines/Timeline/Shared/Attachment'
@ -8,26 +7,26 @@ import TimelineAvatar from 'src/components/Timelines/Timeline/Shared/Avatar'
import TimelineCard from 'src/components/Timelines/Timeline/Shared/Card' import TimelineCard from 'src/components/Timelines/Timeline/Shared/Card'
import TimelineContent from 'src/components/Timelines/Timeline/Shared/Content' import TimelineContent from 'src/components/Timelines/Timeline/Shared/Content'
import TimelineHeaderDefault from 'src/components/Timelines/Timeline/Shared/HeaderDefault' import TimelineHeaderDefault from 'src/components/Timelines/Timeline/Shared/HeaderDefault'
import { ComposeContext } from '../Compose'
export interface Props { const ComposeReply: React.FC = () => {
replyToStatus: Mastodon.Status const {
} composeState: { replyToStatus }
} = useContext(ComposeContext)
const ComposeReply: React.FC<Props> = ({ replyToStatus }) => {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
<View style={styles.status}> <View style={styles.status}>
<TimelineAvatar account={replyToStatus.account} /> <TimelineAvatar account={replyToStatus!.account} />
<View style={styles.details}> <View style={styles.details}>
<TimelineHeaderDefault status={replyToStatus} /> <TimelineHeaderDefault status={replyToStatus!} />
{replyToStatus.content.length > 0 && ( {replyToStatus!.content.length > 0 && (
<TimelineContent status={replyToStatus} /> <TimelineContent status={replyToStatus!} />
)} )}
{replyToStatus.media_attachments.length > 0 && ( {replyToStatus!.media_attachments.length > 0 && (
<TimelineAttachment status={replyToStatus} width={200} /> <TimelineAttachment status={replyToStatus!} width={200} />
)} )}
{replyToStatus.card && <TimelineCard card={replyToStatus.card} />} {replyToStatus!.card && <TimelineCard card={replyToStatus!.card} />}
</View> </View>
</View> </View>
) )

View File

@ -3,6 +3,7 @@ import React, {
Dispatch, Dispatch,
RefObject, RefObject,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef useRef
@ -24,21 +25,12 @@ import { emojisFetch } from 'src/utils/fetches/emojisFetch'
import { searchFetch } from 'src/utils/fetches/searchFetch' import { searchFetch } from 'src/utils/fetches/searchFetch'
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'
import { PostAction, ComposeState } from '../Compose' import { PostAction, ComposeState, ComposeContext } from '../Compose'
import ComposeActions from './Actions' import ComposeActions from './Actions'
import ComposeAttachments from './Attachments'
import ComposeEmojis from './Emojis'
import ComposePoll from './Poll'
import ComposeReply from './Reply'
import ComposeSpoilerInput from './SpoilerInput'
import ComposeTextInput from './TextInput'
import updateText from './updateText' import updateText from './updateText'
import * as Permissions from 'expo-permissions' import * as Permissions from 'expo-permissions'
import ComposeRootFooter from './Root/Footer'
export interface Props { import ComposeRootHeader from './Root/Header'
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const ListItem = React.memo( const ListItem = React.memo(
({ ({
@ -58,7 +50,6 @@ const ListItem = React.memo(
? 'text' ? 'text'
: 'spoiler' : 'spoiler'
updateText({ updateText({
origin: focusedInput,
composeState: { composeState: {
...composeState, ...composeState,
[focusedInput]: { [focusedInput]: {
@ -117,7 +108,9 @@ const ListItem = React.memo(
() => true () => true
) )
const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { const ComposeRoot: React.FC = () => {
const { composeState, composeDispatch } = useContext(ComposeContext)
const { isFetching, isSuccess, data, refetch } = useQuery( const { isFetching, isSuccess, data, refetch } = useQuery(
[ [
'Search', 'Search',
@ -172,112 +165,6 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
} }
}, [isFetching]) }, [isFetching])
const listHeader = useMemo(() => {
return (
<>
{composeState.spoiler.active ? (
<ComposeSpoilerInput
composeState={composeState}
composeDispatch={composeDispatch}
/>
) : null}
<ComposeTextInput
composeState={composeState}
composeDispatch={composeDispatch}
textInputRef={textInputRef}
/>
</>
)
}, [composeState.spoiler.active, composeState.text.formatted])
const listFooterEmojis = useMemo(
() =>
composeState.emoji.active && (
<View style={styles.emojis}>
<ComposeEmojis
textInputRef={textInputRef}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
),
[composeState.emoji.active]
)
const listFooterAttachments = useMemo(
() =>
(composeState.attachments.uploads.length > 0 ||
composeState.attachmentUploadProgress) && (
<View style={styles.attachments}>
<ComposeAttachments
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
),
[composeState.attachments.uploads, composeState.attachmentUploadProgress]
)
// const listFooterPoll = useMemo(
// () =>
// composeState.poll.active && (
// <View style={styles.poll}>
// <ComposePoll
// poll={composeState.poll}
// composeDispatch={composeDispatch}
// />
// </View>
// ),
// [
// composeState.poll.active,
// composeState.poll.total,
// composeState.poll.options['0'],
// composeState.poll.options['1'],
// composeState.poll.options['2'],
// composeState.poll.options['3'],
// composeState.poll.multiple,
// composeState.poll.expire
// ]
// )
const listFooterPoll = () =>
composeState.poll.active && (
<View style={styles.poll}>
<ComposePoll
poll={composeState.poll}
composeDispatch={composeDispatch}
/>
</View>
)
const listFooterReply = useMemo(
() =>
composeState.replyToStatus && (
<View style={styles.replyTo}>
<ComposeReply replyToStatus={composeState.replyToStatus} />
</View>
),
[]
)
const listFooter = useMemo(() => {
return (
<>
{listFooterEmojis}
{listFooterAttachments}
{listFooterPoll()}
{listFooterReply}
</>
)
}, [
composeState.emoji.active,
composeState.attachments.uploads,
composeState.attachmentUploadProgress,
composeState.poll.active,
composeState.poll.total,
composeState.poll.options['0'],
composeState.poll.options['1'],
composeState.poll.options['2'],
composeState.poll.options['3'],
composeState.poll.multiple,
composeState.poll.expire
])
const listKey = useCallback( const listKey = useCallback(
(item: Mastodon.Account | Mastodon.Tag) => item.url, (item: Mastodon.Account | Mastodon.Tag) => item.url,
[isSuccess] [isSuccess]
@ -302,18 +189,14 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
/> />
<FlatList <FlatList
keyboardShouldPersistTaps='handled' keyboardShouldPersistTaps='handled'
ListHeaderComponent={listHeader} ListHeaderComponent={<ComposeRootHeader textInputRef={textInputRef} />}
ListFooterComponent={listFooter} ListFooterComponent={<ComposeRootFooter textInputRef={textInputRef} />}
ListEmptyComponent={listEmpty} ListEmptyComponent={listEmpty}
data={data} data={data}
keyExtractor={listKey} keyExtractor={listKey}
renderItem={listItem} renderItem={listItem}
/> />
<ComposeActions <ComposeActions />
textInputRef={textInputRef}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View> </View>
) )
} }
@ -323,18 +206,6 @@ const styles = StyleSheet.create({
flex: 1 flex: 1
}, },
contentView: { flex: 1 }, contentView: { flex: 1 },
attachments: {
flex: 1
},
poll: {
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
},
replyTo: {
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
},
suggestion: { suggestion: {
flex: 1 flex: 1
}, },
@ -374,9 +245,6 @@ const styles = StyleSheet.create({
fontSize: StyleConstants.Font.Size.S, fontSize: StyleConstants.Font.Size.S,
fontWeight: StyleConstants.Font.Weight.Bold, fontWeight: StyleConstants.Font.Weight.Bold,
marginBottom: StyleConstants.Spacing.XS marginBottom: StyleConstants.Spacing.XS
},
emojis: {
flex: 1
} }
}) })

View File

@ -0,0 +1,64 @@
import React, { useContext } from 'react'
import { StyleSheet, TextInput, View } from 'react-native'
import { StyleConstants } from 'src/utils/styles/constants'
import { ComposeContext } from '../../Compose'
import ComposeAttachments from '../Attachments'
import ComposeEmojis from '../Emojis'
import ComposePoll from '../Poll'
import ComposeReply from '../Reply'
export interface Props {
textInputRef: React.RefObject<TextInput>
}
const ComposeRootFooter: React.FC<Props> = ({ textInputRef }) => {
const { composeState, composeDispatch } = useContext(ComposeContext)
return (
<>
{composeState.emoji.active && (
<View style={styles.emojis}>
<ComposeEmojis textInputRef={textInputRef} />
</View>
)}
{(composeState.attachments.uploads.length > 0 ||
composeState.attachmentUploadProgress) && (
<View style={styles.attachments}>
<ComposeAttachments />
</View>
)}
{composeState.poll.active && (
<View style={styles.poll} key='poll'>
<ComposePoll />
</View>
)}
{composeState.replyToStatus && (
<View style={styles.reply}>
<ComposeReply />
</View>
)}
</>
)
}
const styles = StyleSheet.create({
emojis: {
flex: 1
},
attachments: {
flex: 1
},
poll: {
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
},
reply: {
flex: 1,
padding: StyleConstants.Spacing.Global.PagePadding
}
})
export default ComposeRootFooter

View File

@ -0,0 +1,17 @@
import React, { useContext } from 'react'
import { ComposeContext } from '../../Compose'
import ComposeSpoilerInput from '../SpoilerInput'
import ComposeTextInput from '../TextInput'
const ComposeRootHeader: React.FC = () => {
const { composeState } = useContext(ComposeContext)
return (
<>
{composeState.spoiler.active ? <ComposeSpoilerInput /> : null}
<ComposeTextInput />
</>
)
}
export default ComposeRootHeader

View File

@ -1,19 +1,12 @@
import React, { Dispatch } from 'react' import React, { useContext } from 'react'
import { StyleSheet, Text, TextInput } from 'react-native' import { StyleSheet, Text, TextInput } 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'
import { PostAction, ComposeState } from '../Compose' import { ComposeContext } from '../Compose'
import formatText from './formatText' import formatText from './formatText'
export interface Props { const ComposeSpoilerInput: React.FC = () => {
composeState: ComposeState const { composeState, composeDispatch } = useContext(ComposeContext)
composeDispatch: Dispatch<PostAction>
}
const ComposeSpoilerInput: React.FC<Props> = ({
composeState,
composeDispatch,
}) => {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
@ -34,7 +27,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
placeholderTextColor={theme.secondary} placeholderTextColor={theme.secondary}
onChangeText={content => onChangeText={content =>
formatText({ formatText({
origin: 'spoiler', textInput: 'spoiler',
composeDispatch, composeDispatch,
content content
}) })
@ -49,7 +42,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
payload: { selection: { start, end } } payload: { selection: { start, end } }
}) })
}} }}
// ref={textInputRef} ref={composeState.textInputFocus.refs.spoiler}
scrollEnabled scrollEnabled
> >
<Text>{composeState.spoiler.formatted}</Text> <Text>{composeState.spoiler.formatted}</Text>
@ -68,8 +61,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo( export default ComposeSpoilerInput
ComposeSpoilerInput,
(prev, next) =>
prev.composeState.spoiler.formatted === next.composeState.spoiler.formatted
)

View File

@ -1,21 +1,12 @@
import React, { Dispatch, RefObject } from 'react' import React, { useContext } from 'react'
import { StyleSheet, Text, TextInput } from 'react-native' import { StyleSheet, Text, TextInput } 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'
import { PostAction, ComposeState } from '../Compose' import { ComposeContext } from '../Compose'
import formatText from './formatText' import formatText from './formatText'
export interface Props { const ComposeTextInput: React.FC = () => {
composeState: ComposeState const { composeState, composeDispatch } = useContext(ComposeContext)
composeDispatch: Dispatch<PostAction>
textInputRef: RefObject<TextInput>
}
const ComposeTextInput: React.FC<Props> = ({
composeState,
composeDispatch,
textInputRef
}) => {
const { theme } = useTheme() const { theme } = useTheme()
return ( return (
@ -36,11 +27,17 @@ const ComposeTextInput: React.FC<Props> = ({
placeholderTextColor={theme.secondary} placeholderTextColor={theme.secondary}
onChangeText={content => onChangeText={content =>
formatText({ formatText({
origin: 'text', textInput: 'text',
composeDispatch, composeDispatch,
content content
}) })
} }
onFocus={() =>
composeDispatch({
type: 'textInputFocus',
payload: { current: 'text' }
})
}
onSelectionChange={({ onSelectionChange={({
nativeEvent: { nativeEvent: {
selection: { start, end } selection: { start, end }
@ -51,7 +48,7 @@ const ComposeTextInput: React.FC<Props> = ({
payload: { selection: { start, end } } payload: { selection: { start, end } }
}) })
}} }}
ref={textInputRef} ref={composeState.textInputFocus.refs.text}
scrollEnabled scrollEnabled
> >
<Text>{composeState.text.formatted}</Text> <Text>{composeState.text.formatted}</Text>
@ -70,9 +67,4 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo( export default ComposeTextInput
ComposeTextInput,
(prev, next) =>
prev.composeState.text.raw === next.composeState.text.raw &&
prev.composeState.text.formatted === next.composeState.text.formatted
)

View File

@ -7,7 +7,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, ComposeState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
export interface Params { export interface Params {
origin: 'text' | 'spoiler' textInput: ComposeState['textInputFocus']['current']
composeDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
content: string content: string
refetch?: (options?: RefetchOptions | undefined) => Promise<any> refetch?: (options?: RefetchOptions | undefined) => Promise<any>
@ -33,7 +33,7 @@ const debouncedSuggestions = debounce(
let prevTags: ComposeState['tag'][] = [] let prevTags: ComposeState['tag'][] = []
const formatText = ({ const formatText = ({
origin, textInput,
composeDispatch, composeDispatch,
content, content,
disableDebounce = false disableDebounce = false
@ -108,7 +108,7 @@ const formatText = ({
contentLength = contentLength + _content.length contentLength = contentLength + _content.length
composeDispatch({ composeDispatch({
type: origin, type: textInput,
payload: { payload: {
count: contentLength, count: contentLength,
raw: content, raw: content,

View File

@ -3,25 +3,24 @@ import { PostAction, ComposeState } from '../Compose'
import formatText from './formatText' import formatText from './formatText'
const updateText = ({ const updateText = ({
origin,
composeState, composeState,
composeDispatch, composeDispatch,
newText, newText,
type type
}: { }: {
origin: 'text' | 'spoiler'
composeState: ComposeState composeState: ComposeState
composeDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
newText: string newText: string
type: 'emoji' | 'suggestion' type: 'emoji' | 'suggestion'
}) => { }) => {
if (composeState[origin].raw.length) { const textInput = composeState.textInputFocus.current
const contentFront = composeState[origin].raw.slice( if (composeState[textInput].raw.length) {
const contentFront = composeState[textInput].raw.slice(
0, 0,
composeState[origin].selection.start composeState[textInput].selection.start
) )
const contentRear = composeState[origin].raw.slice( const contentRear = composeState[textInput].raw.slice(
composeState[origin].selection.end composeState[textInput].selection.end
) )
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1)) const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
@ -32,14 +31,14 @@ const updateText = ({
}${newText}${whiteSpaceRear ? '' : ' '}` }${newText}${whiteSpaceRear ? '' : ' '}`
formatText({ formatText({
origin, textInput,
composeDispatch, composeDispatch,
content: [contentFront, newTextWithSpace, contentRear].join(''), content: [contentFront, newTextWithSpace, contentRear].join(''),
disableDebounce: true disableDebounce: true
}) })
} else { } else {
formatText({ formatText({
origin, textInput,
composeDispatch, composeDispatch,
content: `${newText} `, content: `${newText} `,
disableDebounce: true disableDebounce: true