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

Basic reply and re-edit done

This commit is contained in:
Zhiyuan Zheng
2020-12-07 12:31:40 +01:00
parent 69289e8d40
commit 8d715f4324
17 changed files with 420 additions and 253 deletions

View File

@ -299,9 +299,9 @@ declare namespace Mastodon {
url?: string url?: string
in_reply_to_id?: string in_reply_to_id?: string
in_reply_to_account_id?: string in_reply_to_account_id?: string
reblog: Status reblog?: Status
poll: Poll poll?: Poll
card: Card card?: Card
language?: string language?: string
text?: string text?: string
} }

View File

@ -7,6 +7,8 @@ import client from 'src/api/client'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import { toast } from 'src/components/toast' import { toast } from 'src/components/toast'
import { StyleConstants } from 'src/utils/styles/constants' import { StyleConstants } from 'src/utils/styles/constants'
import { useNavigation } from '@react-navigation/native'
import getCurrentTab from 'src/utils/getCurrentTab'
const fireMutation = async ({ const fireMutation = async ({
id, id,
@ -47,6 +49,7 @@ export interface Props {
} }
const TimelineActions: React.FC<Props> = ({ queryKey, status }) => { const TimelineActions: React.FC<Props> = ({ queryKey, status }) => {
const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
const iconColor = theme.secondary const iconColor = theme.secondary
const iconColorAction = (state: boolean) => const iconColorAction = (state: boolean) =>
@ -85,7 +88,15 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status }) => {
} }
}) })
const onPressReply = useCallback(() => {}, []) const onPressReply = useCallback(() => {
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose',
params: {
type: 'reply',
incomingStatus: status
}
})
}, [])
const onPressReblog = useCallback( const onPressReblog = useCallback(
() => () =>
mutateAction({ mutateAction({

View File

@ -41,7 +41,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => {
const onPressAction = useCallback(() => setBottomSheetVisible(true), []) const onPressAction = useCallback(() => setBottomSheetVisible(true), [])
const onPressApplication = useCallback(() => { const onPressApplication = useCallback(() => {
navigation.navigate('Webview', { navigation.navigate('Screen-Shared-Webview', {
uri: status.application!.website uri: status.application!.website
}) })
}, []) }, [])

View File

@ -1,8 +1,11 @@
import { useNavigation } from '@react-navigation/native'
import React from 'react' import React from 'react'
import { Alert } from 'react-native'
import { useMutation, useQueryCache } from 'react-query' import { useMutation, useQueryCache } from 'react-query'
import client from 'src/api/client' import client from 'src/api/client'
import { MenuContainer, MenuHeader, MenuRow } from 'src/components/Menu' import { MenuContainer, MenuHeader, MenuRow } from 'src/components/Menu'
import { toast } from 'src/components/toast' import { toast } from 'src/components/toast'
import getCurrentTab from 'src/utils/getCurrentTab'
const fireMutation = async ({ const fireMutation = async ({
id, id,
@ -62,6 +65,7 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
status, status,
setBottomSheetVisible setBottomSheetVisible
}) => { }) => {
const navigation = useNavigation()
const queryCache = useQueryCache() const queryCache = useQueryCache()
const [mutateAction] = useMutation(fireMutation, { const [mutateAction] = useMutation(fireMutation, {
onMutate: ({ id, type, stateKey, prevState }) => { onMutate: ({ id, type, stateKey, prevState }) => {
@ -119,10 +123,38 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
console.warn('功能未开发') Alert.alert(
'确认删除嘟嘟?',
'你确定要删除这条嘟文并重新编辑它吗?所有相关的转嘟和喜欢都会被清除,回复将会失去关联。',
[
{ text: '取消', style: 'cancel' },
{
text: '删除并重新编辑',
style: 'destructive',
onPress: async () => {
await client({
method: 'delete',
instance: 'local',
url: `statuses/${status.id}`
})
.then(res => {
queryCache.invalidateQueries(queryKey)
setBottomSheetVisible(false)
navigation.navigate(getCurrentTab(navigation), {
screen: 'Screen-Shared-Compose',
params: { type: 'edit', incomingStatus: res.body }
})
})
.catch(() => {
toast({ type: 'error', content: '删除失败' })
})
}
}
]
)
}} }}
iconFront='trash' iconFront='trash'
title='删除并重' title='删除并重新编辑'
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {

View File

@ -19,10 +19,11 @@ import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice'
import { HeaderLeft, HeaderRight } from 'src/components/Header' import { HeaderLeft, HeaderRight } from 'src/components/Header'
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 formatText from './Compose/formatText'
const Stack = createNativeStackNavigator() const Stack = createNativeStackNavigator()
export type PostState = { export type ComposeState = {
spoiler: { spoiler: {
active: boolean active: boolean
count: number count: number
@ -70,36 +71,37 @@ export type PostState = {
attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] } attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] }
attachmentUploadProgress: { progress: number; aspect?: number } | undefined attachmentUploadProgress: { progress: number; aspect?: number } | undefined
visibility: 'public' | 'unlisted' | 'private' | 'direct' visibility: 'public' | 'unlisted' | 'private' | 'direct'
replyToStatus?: Mastodon.Status
} }
export type PostAction = export type PostAction =
| { | {
type: 'spoiler' type: 'spoiler'
payload: Partial<PostState['spoiler']> payload: Partial<ComposeState['spoiler']>
} }
| { | {
type: 'text' type: 'text'
payload: Partial<PostState['text']> payload: Partial<ComposeState['text']>
} }
| { | {
type: 'tag' type: 'tag'
payload: PostState['tag'] payload: ComposeState['tag']
} }
| { | {
type: 'emoji' type: 'emoji'
payload: PostState['emoji'] payload: ComposeState['emoji']
} }
| { | {
type: 'poll' type: 'poll'
payload: PostState['poll'] payload: ComposeState['poll']
} }
| { | {
type: 'attachments' type: 'attachments'
payload: Partial<PostState['attachments']> payload: Partial<ComposeState['attachments']>
} }
| { | {
type: 'attachmentUploadProgress' type: 'attachmentUploadProgress'
payload: PostState['attachmentUploadProgress'] payload: ComposeState['attachmentUploadProgress']
} }
| { | {
type: 'attachmentEdit' type: 'attachmentEdit'
@ -107,10 +109,10 @@ export type PostAction =
} }
| { | {
type: 'visibility' type: 'visibility'
payload: PostState['visibility'] payload: ComposeState['visibility']
} }
const postInitialState: PostState = { const composeInitialState: ComposeState = {
spoiler: { spoiler: {
active: false, active: false,
count: 0, count: 0,
@ -143,9 +145,75 @@ const postInitialState: PostState = {
visibility: visibility:
getLocalAccountPreferences(store.getState())[ getLocalAccountPreferences(store.getState())[
'posting:default:visibility' 'posting:default:visibility'
] || 'public' ] || 'public',
replyToStatus: undefined
} }
const postReducer = (state: PostState, action: PostAction): PostState => { const composeExistingState = ({
type,
incomingStatus
}: {
type: 'reply' | 'edit'
incomingStatus: Mastodon.Status
}): ComposeState => {
switch (type) {
case 'edit':
return {
...composeInitialState,
...(incomingStatus.spoiler_text?.length && {
spoiler: {
active: true,
count: incomingStatus.spoiler_text.length,
raw: incomingStatus.spoiler_text,
formatted: incomingStatus.spoiler_text,
selection: { start: 0, end: 0 }
}
}),
text: {
count: incomingStatus.text!.length,
raw: incomingStatus.text!,
formatted: undefined,
selection: { start: 0, end: 0 }
},
...(incomingStatus.poll && {
poll: {
active: true,
total: incomingStatus.poll.options.length,
options: {
'0': incomingStatus.poll.options[0].title || undefined,
'1': incomingStatus.poll.options[1].title || undefined,
'2': incomingStatus.poll.options[2].title || undefined,
'3': incomingStatus.poll.options[3].title || undefined
},
multiple: incomingStatus.poll.multiple,
expire: '86400' // !!!
}
}),
...(incomingStatus.media_attachments && {
attachments: {
sensitive: incomingStatus.sensitive,
uploads: incomingStatus.media_attachments
}
}),
visibility: incomingStatus.visibility
}
case 'reply':
const replyPlaceholder = `@${
incomingStatus.reblog
? incomingStatus.reblog.account.acct
: incomingStatus.account.acct
} `
return {
...composeInitialState,
text: {
count: replyPlaceholder.length,
raw: replyPlaceholder,
formatted: undefined,
selection: { start: 0, end: 0 }
}
}
}
}
const postReducer = (state: ComposeState, action: PostAction): ComposeState => {
switch (action.type) { switch (action.type) {
case 'spoiler': case 'spoiler':
return { ...state, spoiler: { ...state.spoiler, ...action.payload } } return { ...state, spoiler: { ...state.spoiler, ...action.payload } }
@ -181,11 +249,20 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
} }
} }
const Compose: React.FC = () => { export interface Props {
const { theme } = useTheme() route: {
const navigation = useNavigation() params:
| {
type?: 'reply' | 'edit'
incomingStatus: Mastodon.Status
}
| undefined
}
}
const [isSubmitting, setIsSubmitting] = useState(false) const Compose: React.FC<Props> = ({ route: { params } }) => {
const navigation = useNavigation()
const { theme } = useTheme()
const [hasKeyboard, setHasKeyboard] = useState(false) const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => { useEffect(() => {
@ -205,11 +282,53 @@ const Compose: React.FC = () => {
setHasKeyboard(false) setHasKeyboard(false)
} }
const [postState, postDispatch] = useReducer(postReducer, postInitialState) const [composeState, composeDispatch] = useReducer(
postReducer,
params?.type && params?.incomingStatus
? composeExistingState({
type: params.type,
incomingStatus: params.incomingStatus
})
: composeInitialState
)
const [isSubmitting, setIsSubmitting] = useState(false)
useEffect(() => {
switch (params?.type) {
case 'edit':
if (params.incomingStatus.spoiler_text) {
formatText({
origin: 'spoiler',
composeDispatch,
content: params.incomingStatus.spoiler_text,
disableDebounce: true
})
}
formatText({
origin: 'text',
composeDispatch,
content: params.incomingStatus.text!,
disableDebounce: true
})
break
case 'reply':
formatText({
origin: 'text',
composeDispatch,
content: `@${
params.incomingStatus.reblog
? params.incomingStatus.reblog.account.acct
: params.incomingStatus.account.acct
} `,
disableDebounce: true
})
break
}
}, [params?.type])
const tootPost = async () => { const tootPost = async () => {
setIsSubmitting(true) setIsSubmitting(true)
if (postState.text.count < 0) { if (composeState.text.count < 0) {
Alert.alert('字数超限', '', [ Alert.alert('字数超限', '', [
{ {
text: '返回继续编辑' text: '返回继续编辑'
@ -218,28 +337,31 @@ const Compose: React.FC = () => {
} else { } else {
const formData = new FormData() const formData = new FormData()
if (postState.spoiler.active) { if (composeState.spoiler.active) {
formData.append('spoiler_text', postState.spoiler.raw) formData.append('spoiler_text', composeState.spoiler.raw)
} }
formData.append('status', postState.text.raw) formData.append('status', composeState.text.raw)
if (postState.poll.active) { if (composeState.poll.active) {
Object.values(postState.poll.options) Object.values(composeState.poll.options)
.filter(e => e?.length) .filter(e => e?.length)
.forEach(e => formData.append('poll[options][]', e!)) .forEach(e => formData.append('poll[options][]', e!))
formData.append('poll[expires_in]', postState.poll.expire) formData.append('poll[expires_in]', composeState.poll.expire)
formData.append('poll[multiple]', postState.poll.multiple.toString()) formData.append('poll[multiple]', composeState.poll.multiple.toString())
} }
if (postState.attachments.uploads.length) { if (composeState.attachments.uploads.length) {
formData.append('sensitive', postState.attachments.sensitive.toString()) formData.append(
postState.attachments.uploads.forEach(e => 'sensitive',
composeState.attachments.sensitive.toString()
)
composeState.attachments.uploads.forEach(e =>
formData.append('media_ids[]', e!.id) formData.append('media_ids[]', e!.id)
) )
} }
formData.append('visibility', postState.visibility) formData.append('visibility', composeState.visibility)
client({ client({
method: 'post', method: 'post',
@ -247,17 +369,17 @@ const Compose: React.FC = () => {
url: 'statuses', url: 'statuses',
headers: { headers: {
'Idempotency-Key': sha256( 'Idempotency-Key': sha256(
postState.spoiler.raw + composeState.spoiler.raw +
postState.text.raw + composeState.text.raw +
postState.poll.options['0'] + composeState.poll.options['0'] +
postState.poll.options['1'] + composeState.poll.options['1'] +
postState.poll.options['2'] + composeState.poll.options['2'] +
postState.poll.options['3'] + composeState.poll.options['3'] +
postState.poll.multiple + composeState.poll.multiple +
postState.poll.expire + composeState.poll.expire +
postState.attachments.sensitive + composeState.attachments.sensitive +
postState.attachments.uploads.map(upload => upload.id) + composeState.attachments.uploads.map(upload => upload.id) +
postState.visibility composeState.visibility
).toString() ).toString()
}, },
body: formData body: formData
@ -305,8 +427,8 @@ const Compose: React.FC = () => {
} }
const totalTextCount = const totalTextCount =
(postState.spoiler.active ? postState.spoiler.count : 0) + (composeState.spoiler.active ? composeState.spoiler.count : 0) +
postState.text.count composeState.text.count
return ( return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
@ -354,14 +476,17 @@ const Compose: React.FC = () => {
onPress={async () => tootPost()} onPress={async () => tootPost()}
text='发嘟嘟' text='发嘟嘟'
disabled={ disabled={
postState.text.raw.length < 1 || totalTextCount > 500 composeState.text.raw.length < 1 || totalTextCount > 500
} }
/> />
) )
}} }}
> >
{() => ( {() => (
<ComposeRoot postState={postState} postDispatch={postDispatch} /> <ComposeRoot
composeState={composeState}
composeDispatch={composeDispatch}
/>
)} )}
</Stack.Screen> </Stack.Screen>
</Stack.Navigator> </Stack.Navigator>
@ -377,4 +502,4 @@ const styles = StyleSheet.create({
} }
}) })
export default Compose export default React.memo(Compose, () => true)

View File

@ -10,24 +10,24 @@ import {
} 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, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import addAttachments from './addAttachments' import addAttachments from './addAttachments'
export interface Props { export interface Props {
textInputRef: React.RefObject<TextInput> textInputRef: React.RefObject<TextInput>
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
} }
const ComposeActions: React.FC<Props> = ({ const ComposeActions: React.FC<Props> = ({
textInputRef, textInputRef,
postState, composeState,
postDispatch composeDispatch
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
const getVisibilityIcon = () => { const getVisibilityIcon = () => {
switch (postState.visibility) { switch (composeState.visibility) {
case 'public': case 'public':
return 'globe' return 'globe'
case 'unlisted': case 'unlisted':
@ -40,88 +40,88 @@ const ComposeActions: React.FC<Props> = ({
} }
const attachmentColor = useMemo(() => { const attachmentColor = useMemo(() => {
if (postState.poll.active) return theme.disabled if (composeState.poll.active) return theme.disabled
if (postState.attachmentUploadProgress) return theme.primary if (composeState.attachmentUploadProgress) return theme.primary
if (postState.attachments.uploads.length) { if (composeState.attachments.uploads.length) {
return theme.primary return theme.primary
} else { } else {
return theme.secondary return theme.secondary
} }
}, [ }, [
postState.poll.active, composeState.poll.active,
postState.attachments.uploads, composeState.attachments.uploads,
postState.attachmentUploadProgress composeState.attachmentUploadProgress
]) ])
const attachmentOnPress = useCallback(async () => { const attachmentOnPress = useCallback(async () => {
if (postState.poll.active) return if (composeState.poll.active) return
if (postState.attachmentUploadProgress) return if (composeState.attachmentUploadProgress) return
if (!postState.attachments.uploads.length) { if (!composeState.attachments.uploads.length) {
return await addAttachments({ postState, postDispatch }) return await addAttachments({ composeState, composeDispatch })
} }
}, [ }, [
postState.poll.active, composeState.poll.active,
postState.attachments.uploads, composeState.attachments.uploads,
postState.attachmentUploadProgress composeState.attachmentUploadProgress
]) ])
const pollColor = useMemo(() => { const pollColor = useMemo(() => {
if (postState.attachments.uploads.length) return theme.disabled if (composeState.attachments.uploads.length) return theme.disabled
if (postState.attachmentUploadProgress) return theme.disabled if (composeState.attachmentUploadProgress) return theme.disabled
if (postState.poll.active) { if (composeState.poll.active) {
return theme.primary return theme.primary
} else { } else {
return theme.secondary return theme.secondary
} }
}, [ }, [
postState.poll.active, composeState.poll.active,
postState.attachments.uploads, composeState.attachments.uploads,
postState.attachmentUploadProgress composeState.attachmentUploadProgress
]) ])
const pollOnPress = useCallback(() => { const pollOnPress = useCallback(() => {
if ( if (
!postState.attachments.uploads.length && !composeState.attachments.uploads.length &&
!postState.attachmentUploadProgress !composeState.attachmentUploadProgress
) { ) {
postDispatch({ composeDispatch({
type: 'poll', type: 'poll',
payload: { ...postState.poll, active: !postState.poll.active } payload: { ...composeState.poll, active: !composeState.poll.active }
}) })
} }
if (postState.poll.active) { if (composeState.poll.active) {
textInputRef.current?.focus() textInputRef.current?.focus()
} }
}, [ }, [
postState.poll.active, composeState.poll.active,
postState.attachments.uploads, composeState.attachments.uploads,
postState.attachmentUploadProgress composeState.attachmentUploadProgress
]) ])
const emojiColor = useMemo(() => { const emojiColor = useMemo(() => {
if (!postState.emoji.emojis) return theme.disabled if (!composeState.emoji.emojis) return theme.disabled
if (postState.emoji.active) { if (composeState.emoji.active) {
return theme.primary return theme.primary
} else { } else {
return theme.secondary return theme.secondary
} }
}, [postState.emoji.active, postState.emoji.emojis]) }, [composeState.emoji.active, composeState.emoji.emojis])
const emojiOnPress = useCallback(() => { const emojiOnPress = useCallback(() => {
if (postState.emoji.emojis) { if (composeState.emoji.emojis) {
if (postState.emoji.active) { if (composeState.emoji.active) {
postDispatch({ composeDispatch({
type: 'emoji', type: 'emoji',
payload: { ...postState.emoji, active: false } payload: { ...composeState.emoji, active: false }
}) })
} else { } else {
postDispatch({ composeDispatch({
type: 'emoji', type: 'emoji',
payload: { ...postState.emoji, active: true } payload: { ...composeState.emoji, active: true }
}) })
} }
} }
}, [postState.emoji.active, postState.emoji.emojis]) }, [composeState.emoji.active, composeState.emoji.emojis])
return ( return (
<Pressable <Pressable
@ -156,16 +156,16 @@ const ComposeActions: React.FC<Props> = ({
buttonIndex => { buttonIndex => {
switch (buttonIndex) { switch (buttonIndex) {
case 0: case 0:
postDispatch({ type: 'visibility', payload: 'public' }) composeDispatch({ type: 'visibility', payload: 'public' })
break break
case 1: case 1:
postDispatch({ type: 'visibility', payload: 'unlisted' }) composeDispatch({ type: 'visibility', payload: 'unlisted' })
break break
case 2: case 2:
postDispatch({ type: 'visibility', payload: 'private' }) composeDispatch({ type: 'visibility', payload: 'private' })
break break
case 3: case 3:
postDispatch({ type: 'visibility', payload: 'direct' }) composeDispatch({ type: 'visibility', payload: 'direct' })
break break
} }
} }
@ -175,11 +175,11 @@ const ComposeActions: React.FC<Props> = ({
<Feather <Feather
name='alert-triangle' name='alert-triangle'
size={24} size={24}
color={postState.spoiler.active ? theme.primary : theme.secondary} color={composeState.spoiler.active ? theme.primary : theme.secondary}
onPress={() => onPress={() =>
postDispatch({ composeDispatch({
type: 'spoiler', type: 'spoiler',
payload: { active: !postState.spoiler.active } payload: { active: !composeState.spoiler.active }
}) })
} }
/> />

View File

@ -8,7 +8,7 @@ import {
View View
} from 'react-native' } from 'react-native'
import { PostAction, PostState } from '../Compose' import { PostAction, ComposeState } 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'
@ -20,11 +20,11 @@ import { Feather } from '@expo/vector-icons'
const DEFAULT_HEIGHT = 200 const DEFAULT_HEIGHT = 200
export interface Props { export interface Props {
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
} }
const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => { const ComposeAttachments: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { theme } = useTheme() const { theme } = useTheme()
const navigation = useNavigation() const navigation = useNavigation()
@ -73,10 +73,10 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
<ButtonRound <ButtonRound
icon='x' icon='x'
onPress={() => onPress={() =>
postDispatch({ composeDispatch({
type: 'attachments', type: 'attachments',
payload: { payload: {
uploads: postState.attachments.uploads.filter( uploads: composeState.attachments.uploads.filter(
e => e.id !== item.id e => e.id !== item.id
) )
} }
@ -89,7 +89,7 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
onPress={() => onPress={() =>
navigation.navigate('Screen-Shared-Compose-EditAttachment', { navigation.navigate('Screen-Shared-Compose-EditAttachment', {
attachment: item, attachment: item,
postDispatch composeDispatch
}) })
} }
styles={styles.edit} styles={styles.edit}
@ -104,15 +104,15 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
return ( return (
<ShimmerPlaceholder <ShimmerPlaceholder
style={styles.progressContainer} style={styles.progressContainer}
visible={postState.attachmentUploadProgress === undefined} visible={composeState.attachmentUploadProgress === undefined}
width={ width={
(postState.attachmentUploadProgress?.aspect || 3 / 2) * DEFAULT_HEIGHT (composeState.attachmentUploadProgress?.aspect || 3 / 2) * DEFAULT_HEIGHT
} }
height={200} height={200}
> >
{postState.attachments.uploads.length > 0 && {composeState.attachments.uploads.length > 0 &&
postState.attachments.uploads[0].type === 'image' && composeState.attachments.uploads[0].type === 'image' &&
postState.attachments.uploads.length < 4 && ( composeState.attachments.uploads.length < 4 && (
<Pressable <Pressable
style={{ style={{
width: DEFAULT_HEIGHT, width: DEFAULT_HEIGHT,
@ -120,13 +120,13 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
backgroundColor: theme.border backgroundColor: theme.border
}} }}
onPress={async () => onPress={async () =>
await addAttachments({ postState, postDispatch }) await addAttachments({ composeState, composeDispatch })
} }
> >
<ButtonRound <ButtonRound
icon='upload-cloud' icon='upload-cloud'
onPress={async () => onPress={async () =>
await addAttachments({ postState, postDispatch }) await addAttachments({ composeState, composeDispatch })
} }
styles={{ styles={{
top: top:
@ -144,21 +144,21 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
)} )}
</ShimmerPlaceholder> </ShimmerPlaceholder>
) )
}, [postState.attachmentUploadProgress, postState.attachments.uploads]) }, [composeState.attachmentUploadProgress, composeState.attachments.uploads])
return ( return (
<View style={styles.base}> <View style={styles.base}>
<Pressable <Pressable
style={styles.sensitive} style={styles.sensitive}
onPress={() => onPress={() =>
postDispatch({ composeDispatch({
type: 'attachments', type: 'attachments',
payload: { sensitive: !postState.attachments.sensitive } payload: { sensitive: !composeState.attachments.sensitive }
}) })
} }
> >
<Feather <Feather
name={postState.attachments.sensitive ? 'check-circle' : 'circle'} name={composeState.attachments.sensitive ? 'check-circle' : 'circle'}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme.primary} color={theme.primary}
/> />
@ -169,8 +169,8 @@ const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
<View style={styles.imageContainer}> <View style={styles.imageContainer}>
<FlatList <FlatList
horizontal horizontal
extraData={postState.attachments.uploads.length} extraData={composeState.attachments.uploads.length}
data={postState.attachments.uploads} data={composeState.attachments.uploads}
renderItem={renderAttachment} renderItem={renderAttachment}
ListFooterComponent={listFooter} ListFooterComponent={listFooter}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}

View File

@ -36,14 +36,14 @@ export interface Props {
route: { route: {
params: { params: {
attachment: Mastodon.Attachment & { local_url: string } attachment: Mastodon.Attachment & { local_url: string }
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
} }
} }
} }
const ComposeEditAttachment: React.FC<Props> = ({ const ComposeEditAttachment: React.FC<Props> = ({
route: { route: {
params: { attachment, postDispatch } params: { attachment, composeDispatch }
} }
}) => { }) => {
const navigation = useNavigation() const navigation = useNavigation()
@ -72,7 +72,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
} }
} }
if (needUpdate) { if (needUpdate) {
postDispatch({ type: 'attachmentEdit', payload: attachment }) composeDispatch({ type: 'attachmentEdit', payload: attachment })
} }
}) })

View File

@ -11,19 +11,19 @@ import {
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, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import updateText from './updateText' import updateText from './updateText'
export interface Props { export interface Props {
textInputRef: React.RefObject<TextInput> textInputRef: React.RefObject<TextInput>
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
} }
const ComposeEmojis: React.FC<Props> = ({ const ComposeEmojis: React.FC<Props> = ({
textInputRef, textInputRef,
postState, composeState,
postDispatch composeDispatch
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
@ -32,7 +32,7 @@ const ComposeEmojis: React.FC<Props> = ({
<SectionList <SectionList
horizontal horizontal
keyboardShouldPersistTaps='handled' keyboardShouldPersistTaps='handled'
sections={postState.emoji.emojis!} sections={composeState.emoji.emojis!}
keyExtractor={item => item.shortcode} keyExtractor={item => item.shortcode}
renderSectionHeader={({ section: { title } }) => ( renderSectionHeader={({ section: { title } }) => (
<Text style={[styles.group, { color: theme.secondary }]}> <Text style={[styles.group, { color: theme.secondary }]}>
@ -51,14 +51,14 @@ const ComposeEmojis: React.FC<Props> = ({
origin: textInputRef.current?.isFocused() origin: textInputRef.current?.isFocused()
? 'text' ? 'text'
: 'spoiler', : 'spoiler',
postState, composeState,
postDispatch, composeDispatch,
newText: `:${emoji.shortcode}:`, newText: `:${emoji.shortcode}:`,
type: 'emoji' type: 'emoji'
}) })
postDispatch({ composeDispatch({
type: 'emoji', type: 'emoji',
payload: { ...postState.emoji, active: false } payload: { ...composeState.emoji, active: false }
}) })
}} }}
> >

View File

@ -2,18 +2,18 @@ import React, { Dispatch, 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, PostState } from '../Compose' import { PostAction, ComposeState } 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 { export interface Props {
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
} }
const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => { const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { theme } = useTheme() const { theme } = useTheme()
const expireMapping: { [key: string]: string } = { const expireMapping: { [key: string]: string } = {
@ -34,21 +34,21 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
return ( return (
<View style={[styles.base, { borderColor: theme.border }]}> <View style={[styles.base, { borderColor: theme.border }]}>
<View style={styles.options}> <View style={styles.options}>
{[...Array(postState.poll.total)].map((e, i) => { {[...Array(composeState.poll.total)].map((e, i) => {
const restOptions = Object.keys(postState.poll.options).filter( const restOptions = Object.keys(composeState.poll.options).filter(
o => parseInt(o) !== i && parseInt(o) < postState.poll.total o => parseInt(o) !== i && parseInt(o) < composeState.poll.total
) )
let hasConflict = false let hasConflict = false
restOptions.forEach(o => { restOptions.forEach(o => {
// @ts-ignore // @ts-ignore
if (postState.poll.options[o] === postState.poll.options[i]) { if (composeState.poll.options[o] === composeState.poll.options[i]) {
hasConflict = true hasConflict = true
} }
}) })
return ( return (
<View key={i} style={styles.option}> <View key={i} style={styles.option}>
<Feather <Feather
name={postState.poll.multiple ? 'square' : 'circle'} name={composeState.poll.multiple ? 'square' : 'circle'}
size={StyleConstants.Font.Size.L} size={StyleConstants.Font.Size.L}
color={theme.secondary} color={theme.secondary}
/> />
@ -65,13 +65,13 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
placeholderTextColor={theme.secondary} placeholderTextColor={theme.secondary}
maxLength={50} maxLength={50}
// @ts-ignore // @ts-ignore
value={postState.poll.options[i]} value={composeState.poll.options[i]}
onChangeText={e => onChangeText={e =>
postDispatch({ composeDispatch({
type: 'poll', type: 'poll',
payload: { payload: {
...postState.poll, ...composeState.poll,
options: { ...postState.poll.options, [i]: e } options: { ...composeState.poll.options, [i]: e }
} }
}) })
} }
@ -84,34 +84,34 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
<View style={styles.firstButton}> <View style={styles.firstButton}>
<ButtonRow <ButtonRow
onPress={() => onPress={() =>
postState.poll.total > 2 && composeState.poll.total > 2 &&
postDispatch({ composeDispatch({
type: 'poll', type: 'poll',
payload: { ...postState.poll, total: postState.poll.total - 1 } payload: { ...composeState.poll, total: composeState.poll.total - 1 }
}) })
} }
icon='minus' icon='minus'
disabled={!(postState.poll.total > 2)} disabled={!(composeState.poll.total > 2)}
buttonSize='S' buttonSize='S'
/> />
</View> </View>
<ButtonRow <ButtonRow
onPress={() => onPress={() =>
postState.poll.total < 4 && composeState.poll.total < 4 &&
postDispatch({ composeDispatch({
type: 'poll', type: 'poll',
payload: { ...postState.poll, total: postState.poll.total + 1 } payload: { ...composeState.poll, total: composeState.poll.total + 1 }
}) })
} }
icon='plus' icon='plus'
disabled={!(postState.poll.total < 4)} disabled={!(composeState.poll.total < 4)}
buttonSize='S' buttonSize='S'
/> />
</View> </View>
<MenuContainer> <MenuContainer>
<MenuRow <MenuRow
title='可选项' title='可选项'
content={postState.poll.multiple ? '多选' : '单选'} content={composeState.poll.multiple ? '多选' : '单选'}
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( ActionSheetIOS.showActionSheetWithOptions(
{ {
@ -120,9 +120,9 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
}, },
index => index =>
index < 2 && index < 2 &&
postDispatch({ composeDispatch({
type: 'poll', type: 'poll',
payload: { ...postState.poll, multiple: index === 1 } payload: { ...composeState.poll, multiple: index === 1 }
}) })
) )
} }
@ -130,7 +130,7 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
/> />
<MenuRow <MenuRow
title='有效期' title='有效期'
content={expireMapping[postState.poll.expire]} content={expireMapping[composeState.poll.expire]}
onPress={() => onPress={() =>
ActionSheetIOS.showActionSheetWithOptions( ActionSheetIOS.showActionSheetWithOptions(
{ {
@ -139,10 +139,10 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
}, },
index => index =>
index < 7 && index < 7 &&
postDispatch({ composeDispatch({
type: 'poll', type: 'poll',
payload: { payload: {
...postState.poll, ...composeState.poll,
expire: Object.keys(expireMapping)[index] expire: Object.keys(expireMapping)[index]
} }
}) })

View File

@ -17,7 +17,7 @@ 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, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import ComposeActions from './Actions' import ComposeActions from './Actions'
import ComposeAttachments from './Attachments' import ComposeAttachments from './Attachments'
import ComposeEmojis from './Emojis' import ComposeEmojis from './Emojis'
@ -28,26 +28,26 @@ import updateText from './updateText'
import * as Permissions from 'expo-permissions' import * as Permissions from 'expo-permissions'
export interface Props { export interface Props {
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
} }
const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => { const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { theme } = useTheme() const { theme } = useTheme()
const { isFetching, isSuccess, data, refetch } = useQuery( const { isFetching, isSuccess, data, refetch } = useQuery(
[ [
'Search', 'Search',
{ type: postState.tag?.type, term: postState.tag?.text.substring(1) } { type: composeState.tag?.type, term: composeState.tag?.text.substring(1) }
], ],
searchFetch, searchFetch,
{ enabled: false } { enabled: false }
) )
useEffect(() => { useEffect(() => {
if (postState.tag?.text) { if (composeState.tag?.text) {
refetch() refetch()
} }
}, [postState.tag?.text]) }, [composeState.tag?.text])
useEffect(() => { useEffect(() => {
;(async () => { ;(async () => {
@ -71,9 +71,9 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'), groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'),
(value, key) => sortedEmojis.push({ title: key, data: value }) (value, key) => sortedEmojis.push({ title: key, data: value })
) )
postDispatch({ composeDispatch({
type: 'emoji', type: 'emoji',
payload: { ...postState.emoji, emojis: sortedEmojis } payload: { ...composeState.emoji, emojis: sortedEmojis }
}) })
} }
}, [emojisData]) }, [emojisData])
@ -89,60 +89,60 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
return ( return (
<View style={styles.base}> <View style={styles.base}>
<ProgressViewIOS <ProgressViewIOS
progress={postState.attachmentUploadProgress?.progress || 0} progress={composeState.attachmentUploadProgress?.progress || 0}
progressViewStyle='bar' progressViewStyle='bar'
/> />
<FlatList <FlatList
keyboardShouldPersistTaps='handled' keyboardShouldPersistTaps='handled'
ListHeaderComponent={ ListHeaderComponent={
<> <>
{postState.spoiler.active ? ( {composeState.spoiler.active ? (
<ComposeSpoilerInput <ComposeSpoilerInput
postState={postState} composeState={composeState}
postDispatch={postDispatch} composeDispatch={composeDispatch}
/> />
) : null} ) : null}
<ComposeTextInput <ComposeTextInput
postState={postState} composeState={composeState}
postDispatch={postDispatch} composeDispatch={composeDispatch}
textInputRef={textInputRef} textInputRef={textInputRef}
/> />
</> </>
} }
ListFooterComponent={ ListFooterComponent={
<> <>
{postState.emoji.active && ( {composeState.emoji.active && (
<View style={styles.emojis}> <View style={styles.emojis}>
<ComposeEmojis <ComposeEmojis
textInputRef={textInputRef} textInputRef={textInputRef}
postState={postState} composeState={composeState}
postDispatch={postDispatch} composeDispatch={composeDispatch}
/> />
</View> </View>
)} )}
{(postState.attachments.uploads.length > 0 || {(composeState.attachments.uploads.length > 0 ||
postState.attachmentUploadProgress) && ( composeState.attachmentUploadProgress) && (
<View style={styles.attachments}> <View style={styles.attachments}>
<ComposeAttachments <ComposeAttachments
postState={postState} composeState={composeState}
postDispatch={postDispatch} composeDispatch={composeDispatch}
/> />
</View> </View>
)} )}
{postState.poll.active && ( {composeState.poll.active && (
<View style={styles.poll}> <View style={styles.poll}>
<ComposePoll <ComposePoll
postState={postState} composeState={composeState}
postDispatch={postDispatch} composeDispatch={composeDispatch}
/> />
</View> </View>
)} )}
</> </>
} }
ListEmptyComponent={listEmpty} ListEmptyComponent={listEmpty}
data={postState.tag && isSuccess ? data[postState.tag.type] : []} data={composeState.tag && isSuccess ? data[composeState.tag.type] : []}
renderItem={({ item, index }) => ( renderItem={({ item, index }) => (
<Pressable <Pressable
key={index} key={index}
@ -152,18 +152,18 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
: 'spoiler' : 'spoiler'
updateText({ updateText({
origin: focusedInput, origin: focusedInput,
postState: { composeState: {
...postState, ...composeState,
[focusedInput]: { [focusedInput]: {
...postState[focusedInput], ...composeState[focusedInput],
selection: { selection: {
start: postState.tag!.offset, start: composeState.tag!.offset,
end: end:
postState.tag!.offset + postState.tag!.text.length + 1 composeState.tag!.offset + composeState.tag!.text.length + 1
} }
} }
}, },
postDispatch, composeDispatch,
newText: item.acct ? `@${item.acct}` : `#${item.name}`, newText: item.acct ? `@${item.acct}` : `#${item.name}`,
type: 'suggestion' type: 'suggestion'
}) })
@ -225,8 +225,8 @@ const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
/> />
<ComposeActions <ComposeActions
textInputRef={textInputRef} textInputRef={textInputRef}
postState={postState} composeState={composeState}
postDispatch={postDispatch} composeDispatch={composeDispatch}
/> />
</View> </View>
) )

View File

@ -2,18 +2,18 @@ import React, { Dispatch, RefObject } 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, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import formatText from './formatText' import formatText from './formatText'
export interface Props { export interface Props {
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
// textInputRef: RefObject<TextInput> // textInputRef: RefObject<TextInput>
} }
const ComposeSpoilerInput: React.FC<Props> = ({ const ComposeSpoilerInput: React.FC<Props> = ({
postState, composeState,
postDispatch, composeDispatch,
// textInputRef // textInputRef
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
@ -37,7 +37,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
onChangeText={content => onChangeText={content =>
formatText({ formatText({
origin: 'spoiler', origin: 'spoiler',
postDispatch, composeDispatch,
content content
}) })
} }
@ -46,7 +46,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
selection: { start, end } selection: { start, end }
} }
}) => { }) => {
postDispatch({ composeDispatch({
type: 'spoiler', type: 'spoiler',
payload: { selection: { start, end } } payload: { selection: { start, end } }
}) })
@ -54,7 +54,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
// ref={textInputRef} // ref={textInputRef}
scrollEnabled scrollEnabled
> >
<Text>{postState.spoiler.formatted}</Text> <Text>{composeState.spoiler.formatted}</Text>
</TextInput> </TextInput>
) )
} }
@ -73,5 +73,5 @@ const styles = StyleSheet.create({
export default React.memo( export default React.memo(
ComposeSpoilerInput, ComposeSpoilerInput,
(prev, next) => (prev, next) =>
prev.postState.spoiler.formatted === next.postState.spoiler.formatted prev.composeState.spoiler.formatted === next.composeState.spoiler.formatted
) )

View File

@ -2,18 +2,18 @@ import React, { Dispatch, RefObject } 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, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import formatText from './formatText' import formatText from './formatText'
export interface Props { export interface Props {
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
textInputRef: RefObject<TextInput> textInputRef: RefObject<TextInput>
} }
const ComposeTextInput: React.FC<Props> = ({ const ComposeTextInput: React.FC<Props> = ({
postState, composeState,
postDispatch, composeDispatch,
textInputRef textInputRef
}) => { }) => {
const { theme } = useTheme() const { theme } = useTheme()
@ -37,7 +37,7 @@ const ComposeTextInput: React.FC<Props> = ({
onChangeText={content => onChangeText={content =>
formatText({ formatText({
origin: 'text', origin: 'text',
postDispatch, composeDispatch,
content content
}) })
} }
@ -46,12 +46,12 @@ const ComposeTextInput: React.FC<Props> = ({
selection: { start, end } selection: { start, end }
} }
}) => { }) => {
postDispatch({ type: 'text', payload: { selection: { start, end } } }) composeDispatch({ type: 'text', payload: { selection: { start, end } } })
}} }}
ref={textInputRef} ref={textInputRef}
scrollEnabled scrollEnabled
> >
<Text>{postState.text.formatted}</Text> <Text>{composeState.text.formatted}</Text>
</TextInput> </TextInput>
) )
} }
@ -70,5 +70,5 @@ const styles = StyleSheet.create({
export default React.memo( export default React.memo(
ComposeTextInput, ComposeTextInput,
(prev, next) => (prev, next) =>
prev.postState.text.formatted === next.postState.text.formatted prev.composeState.text.formatted === next.composeState.text.formatted
) )

View File

@ -2,18 +2,18 @@ import { Dispatch } from 'react'
import { ActionSheetIOS, Alert } from 'react-native' import { ActionSheetIOS, Alert } from 'react-native'
import * as ImagePicker from 'expo-image-picker' import * as ImagePicker from 'expo-image-picker'
import { PostAction, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import client from 'src/api/client' import client from 'src/api/client'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
const uploadAttachment = async ({ const uploadAttachment = async ({
result, result,
postState, composeState,
postDispatch composeDispatch
}: { }: {
result: NonNullable<ImageInfo> result: NonNullable<ImageInfo>
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
}) => { }) => {
const formData = new FormData() const formData = new FormData()
// @ts-ignore // @ts-ignore
@ -30,7 +30,7 @@ const uploadAttachment = async ({
url: 'media', url: 'media',
body: formData, body: formData,
onUploadProgress: p => { onUploadProgress: p => {
postDispatch({ composeDispatch({
type: 'attachmentUploadProgress', type: 'attachmentUploadProgress',
payload: { payload: {
progress: p.loaded / p.total, progress: p.loaded / p.total,
@ -40,15 +40,15 @@ const uploadAttachment = async ({
} }
}) })
.then(({ body }: { body: Mastodon.Attachment & { local_url: string } }) => { .then(({ body }: { body: Mastodon.Attachment & { local_url: string } }) => {
postDispatch({ composeDispatch({
type: 'attachmentUploadProgress', type: 'attachmentUploadProgress',
payload: undefined payload: undefined
}) })
if (body.id) { if (body.id) {
body.local_url = result.uri body.local_url = result.uri
postDispatch({ composeDispatch({
type: 'attachments', type: 'attachments',
payload: { uploads: postState.attachments.uploads.concat([body]) } payload: { uploads: composeState.attachments.uploads.concat([body]) }
}) })
return Promise.resolve() return Promise.resolve()
} else { } else {
@ -56,7 +56,7 @@ const uploadAttachment = async ({
{ {
text: '返回重试', text: '返回重试',
onPress: () => onPress: () =>
postDispatch({ composeDispatch({
type: 'attachmentUploadProgress', type: 'attachmentUploadProgress',
payload: undefined payload: undefined
}) })
@ -70,7 +70,7 @@ const uploadAttachment = async ({
{ {
text: '返回重试', text: '返回重试',
onPress: () => onPress: () =>
postDispatch({ composeDispatch({
type: 'attachmentUploadProgress', type: 'attachmentUploadProgress',
payload: undefined payload: undefined
}) })
@ -83,8 +83,8 @@ const uploadAttachment = async ({
const addAttachments = async ({ const addAttachments = async ({
...params ...params
}: { }: {
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
}): Promise<any> => { }): Promise<any> => {
ActionSheetIOS.showActionSheetWithOptions( ActionSheetIOS.showActionSheetWithOptions(
{ {

View File

@ -4,11 +4,11 @@ import { Text } from 'react-native'
import { RefetchOptions } from 'react-query/types/core/query' import { RefetchOptions } from 'react-query/types/core/query'
import Autolinker from 'src/modules/autolinker' import Autolinker from 'src/modules/autolinker'
import { useTheme } from 'src/utils/styles/ThemeManager' import { useTheme } from 'src/utils/styles/ThemeManager'
import { PostAction, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
export interface Params { export interface Params {
origin: 'text' | 'spoiler' origin: 'text' | 'spoiler'
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
content: string content: string
refetch?: (options?: RefetchOptions | undefined) => Promise<any> refetch?: (options?: RefetchOptions | undefined) => Promise<any>
disableDebounce?: boolean disableDebounce?: boolean
@ -25,8 +25,8 @@ const TagText = ({ text }: { text: string }) => {
} }
const debouncedSuggestions = debounce( const debouncedSuggestions = debounce(
(postDispatch, tag) => { (composeDispatch, tag) => {
postDispatch({ type: 'tag', payload: tag }) composeDispatch({ type: 'tag', payload: tag })
}, },
500, 500,
{ {
@ -34,15 +34,15 @@ const debouncedSuggestions = debounce(
} }
) )
let prevTags: PostState['tag'][] = [] let prevTags: ComposeState['tag'][] = []
const formatText = ({ const formatText = ({
origin, origin,
postDispatch, composeDispatch,
content, content,
disableDebounce = false disableDebounce = false
}: Params) => { }: Params) => {
const tags: PostState['tag'][] = [] const tags: ComposeState['tag'][] = []
Autolinker.link(content, { Autolinker.link(content, {
email: false, email: false,
phone: false, phone: false,
@ -74,11 +74,11 @@ const formatText = ({
const changedTag = differenceWith(tags, prevTags, isEqual) const changedTag = differenceWith(tags, prevTags, isEqual)
if (changedTag.length && !disableDebounce) { if (changedTag.length && !disableDebounce) {
if (changedTag[0]!.type !== 'url') { if (changedTag[0]!.type !== 'url') {
debouncedSuggestions(postDispatch, changedTag[0]) debouncedSuggestions(composeDispatch, changedTag[0])
} }
} else { } else {
debouncedSuggestions.cancel() debouncedSuggestions.cancel()
postDispatch({ type: 'tag', payload: undefined }) composeDispatch({ type: 'tag', payload: undefined })
} }
prevTags = tags prevTags = tags
let _content = content let _content = content
@ -107,7 +107,7 @@ const formatText = ({
children.push(_content) children.push(_content)
contentLength = contentLength + _content.length contentLength = contentLength + _content.length
postDispatch({ composeDispatch({
type: origin, type: origin,
payload: { payload: {
count: contentLength, count: contentLength,

View File

@ -1,27 +1,27 @@
import { Dispatch } from 'react' import { Dispatch } from 'react'
import { PostAction, PostState } from '../Compose' import { PostAction, ComposeState } from '../Compose'
import formatText from './formatText' import formatText from './formatText'
const updateText = ({ const updateText = ({
origin, origin,
postState, composeState,
postDispatch, composeDispatch,
newText, newText,
type type
}: { }: {
origin: 'text' | 'spoiler' origin: 'text' | 'spoiler'
postState: PostState composeState: ComposeState
postDispatch: Dispatch<PostAction> composeDispatch: Dispatch<PostAction>
newText: string newText: string
type: 'emoji' | 'suggestion' type: 'emoji' | 'suggestion'
}) => { }) => {
if (postState[origin].raw.length) { if (composeState[origin].raw.length) {
const contentFront = postState[origin].raw.slice( const contentFront = composeState[origin].raw.slice(
0, 0,
postState[origin].selection.start composeState[origin].selection.start
) )
const contentRear = postState[origin].raw.slice( const contentRear = composeState[origin].raw.slice(
postState[origin].selection.end composeState[origin].selection.end
) )
const whiteSpaceFront = /\s/g.test(contentFront.slice(-1)) const whiteSpaceFront = /\s/g.test(contentFront.slice(-1))
@ -33,14 +33,14 @@ const updateText = ({
formatText({ formatText({
origin, origin,
postDispatch, composeDispatch,
content: [contentFront, newTextWithSpace, contentRear].join(''), content: [contentFront, newTextWithSpace, contentRear].join(''),
disableDebounce: true disableDebounce: true
}) })
} else { } else {
formatText({ formatText({
origin, origin,
postDispatch, composeDispatch,
content: `${newText} `, content: `${newText} `,
disableDebounce: true disableDebounce: true
}) })

View File

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