diff --git a/src/@types/mastodon.d.ts b/src/@types/mastodon.d.ts index 3c6ae887..945d4740 100644 --- a/src/@types/mastodon.d.ts +++ b/src/@types/mastodon.d.ts @@ -299,9 +299,9 @@ declare namespace Mastodon { url?: string in_reply_to_id?: string in_reply_to_account_id?: string - reblog: Status - poll: Poll - card: Card + reblog?: Status + poll?: Poll + card?: Card language?: string text?: string } diff --git a/src/components/Timelines/Timeline/Shared/Actions.tsx b/src/components/Timelines/Timeline/Shared/Actions.tsx index 4d829fa8..c6634dc2 100644 --- a/src/components/Timelines/Timeline/Shared/Actions.tsx +++ b/src/components/Timelines/Timeline/Shared/Actions.tsx @@ -7,6 +7,8 @@ import client from 'src/api/client' import { useTheme } from 'src/utils/styles/ThemeManager' import { toast } from 'src/components/toast' import { StyleConstants } from 'src/utils/styles/constants' +import { useNavigation } from '@react-navigation/native' +import getCurrentTab from 'src/utils/getCurrentTab' const fireMutation = async ({ id, @@ -47,6 +49,7 @@ export interface Props { } const TimelineActions: React.FC = ({ queryKey, status }) => { + const navigation = useNavigation() const { theme } = useTheme() const iconColor = theme.secondary const iconColorAction = (state: boolean) => @@ -85,7 +88,15 @@ const TimelineActions: React.FC = ({ queryKey, status }) => { } }) - const onPressReply = useCallback(() => {}, []) + const onPressReply = useCallback(() => { + navigation.navigate(getCurrentTab(navigation), { + screen: 'Screen-Shared-Compose', + params: { + type: 'reply', + incomingStatus: status + } + }) + }, []) const onPressReblog = useCallback( () => mutateAction({ diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx index dcf36d4d..b219fca1 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderDefault.tsx @@ -41,7 +41,7 @@ const TimelineHeaderDefault: React.FC = ({ queryKey, status }) => { const onPressAction = useCallback(() => setBottomSheetVisible(true), []) const onPressApplication = useCallback(() => { - navigation.navigate('Webview', { + navigation.navigate('Screen-Shared-Webview', { uri: status.application!.website }) }, []) diff --git a/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus.tsx b/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus.tsx index 0e702c49..d74ecde8 100644 --- a/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus.tsx +++ b/src/components/Timelines/Timeline/Shared/HeaderDefault/ActionsStatus.tsx @@ -1,8 +1,11 @@ +import { useNavigation } from '@react-navigation/native' import React from 'react' +import { Alert } from 'react-native' import { useMutation, useQueryCache } from 'react-query' import client from 'src/api/client' import { MenuContainer, MenuHeader, MenuRow } from 'src/components/Menu' import { toast } from 'src/components/toast' +import getCurrentTab from 'src/utils/getCurrentTab' const fireMutation = async ({ id, @@ -62,6 +65,7 @@ const HeaderDefaultActionsStatus: React.FC = ({ status, setBottomSheetVisible }) => { + const navigation = useNavigation() const queryCache = useQueryCache() const [mutateAction] = useMutation(fireMutation, { onMutate: ({ id, type, stateKey, prevState }) => { @@ -119,10 +123,38 @@ const HeaderDefaultActionsStatus: React.FC = ({ /> { - 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' - title='删除并重发' + title='删除并重新编辑' /> { diff --git a/src/screens/Shared/Compose.tsx b/src/screens/Shared/Compose.tsx index 92f3d617..fcd519ad 100644 --- a/src/screens/Shared/Compose.tsx +++ b/src/screens/Shared/Compose.tsx @@ -19,10 +19,11 @@ import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice' import { HeaderLeft, HeaderRight } from 'src/components/Header' import { StyleConstants } from 'src/utils/styles/constants' import { useTheme } from 'src/utils/styles/ThemeManager' +import formatText from './Compose/formatText' const Stack = createNativeStackNavigator() -export type PostState = { +export type ComposeState = { spoiler: { active: boolean count: number @@ -70,36 +71,37 @@ export type PostState = { attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] } attachmentUploadProgress: { progress: number; aspect?: number } | undefined visibility: 'public' | 'unlisted' | 'private' | 'direct' + replyToStatus?: Mastodon.Status } export type PostAction = | { type: 'spoiler' - payload: Partial + payload: Partial } | { type: 'text' - payload: Partial + payload: Partial } | { type: 'tag' - payload: PostState['tag'] + payload: ComposeState['tag'] } | { type: 'emoji' - payload: PostState['emoji'] + payload: ComposeState['emoji'] } | { type: 'poll' - payload: PostState['poll'] + payload: ComposeState['poll'] } | { type: 'attachments' - payload: Partial + payload: Partial } | { type: 'attachmentUploadProgress' - payload: PostState['attachmentUploadProgress'] + payload: ComposeState['attachmentUploadProgress'] } | { type: 'attachmentEdit' @@ -107,10 +109,10 @@ export type PostAction = } | { type: 'visibility' - payload: PostState['visibility'] + payload: ComposeState['visibility'] } -const postInitialState: PostState = { +const composeInitialState: ComposeState = { spoiler: { active: false, count: 0, @@ -143,9 +145,75 @@ const postInitialState: PostState = { visibility: getLocalAccountPreferences(store.getState())[ '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) { case 'spoiler': return { ...state, spoiler: { ...state.spoiler, ...action.payload } } @@ -181,11 +249,20 @@ const postReducer = (state: PostState, action: PostAction): PostState => { } } -const Compose: React.FC = () => { - const { theme } = useTheme() - const navigation = useNavigation() +export interface Props { + route: { + params: + | { + type?: 'reply' | 'edit' + incomingStatus: Mastodon.Status + } + | undefined + } +} - const [isSubmitting, setIsSubmitting] = useState(false) +const Compose: React.FC = ({ route: { params } }) => { + const navigation = useNavigation() + const { theme } = useTheme() const [hasKeyboard, setHasKeyboard] = useState(false) useEffect(() => { @@ -205,11 +282,53 @@ const Compose: React.FC = () => { 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 () => { setIsSubmitting(true) - if (postState.text.count < 0) { + if (composeState.text.count < 0) { Alert.alert('字数超限', '', [ { text: '返回继续编辑' @@ -218,28 +337,31 @@ const Compose: React.FC = () => { } else { const formData = new FormData() - if (postState.spoiler.active) { - formData.append('spoiler_text', postState.spoiler.raw) + if (composeState.spoiler.active) { + formData.append('spoiler_text', composeState.spoiler.raw) } - formData.append('status', postState.text.raw) + formData.append('status', composeState.text.raw) - if (postState.poll.active) { - Object.values(postState.poll.options) + if (composeState.poll.active) { + Object.values(composeState.poll.options) .filter(e => e?.length) .forEach(e => formData.append('poll[options][]', e!)) - formData.append('poll[expires_in]', postState.poll.expire) - formData.append('poll[multiple]', postState.poll.multiple.toString()) + formData.append('poll[expires_in]', composeState.poll.expire) + formData.append('poll[multiple]', composeState.poll.multiple.toString()) } - if (postState.attachments.uploads.length) { - formData.append('sensitive', postState.attachments.sensitive.toString()) - postState.attachments.uploads.forEach(e => + if (composeState.attachments.uploads.length) { + formData.append( + 'sensitive', + composeState.attachments.sensitive.toString() + ) + composeState.attachments.uploads.forEach(e => formData.append('media_ids[]', e!.id) ) } - formData.append('visibility', postState.visibility) + formData.append('visibility', composeState.visibility) client({ method: 'post', @@ -247,17 +369,17 @@ const Compose: React.FC = () => { url: 'statuses', headers: { 'Idempotency-Key': sha256( - postState.spoiler.raw + - postState.text.raw + - postState.poll.options['0'] + - postState.poll.options['1'] + - postState.poll.options['2'] + - postState.poll.options['3'] + - postState.poll.multiple + - postState.poll.expire + - postState.attachments.sensitive + - postState.attachments.uploads.map(upload => upload.id) + - postState.visibility + composeState.spoiler.raw + + composeState.text.raw + + composeState.poll.options['0'] + + composeState.poll.options['1'] + + composeState.poll.options['2'] + + composeState.poll.options['3'] + + composeState.poll.multiple + + composeState.poll.expire + + composeState.attachments.sensitive + + composeState.attachments.uploads.map(upload => upload.id) + + composeState.visibility ).toString() }, body: formData @@ -305,8 +427,8 @@ const Compose: React.FC = () => { } const totalTextCount = - (postState.spoiler.active ? postState.spoiler.count : 0) + - postState.text.count + (composeState.spoiler.active ? composeState.spoiler.count : 0) + + composeState.text.count return ( @@ -354,14 +476,17 @@ const Compose: React.FC = () => { onPress={async () => tootPost()} text='发嘟嘟' disabled={ - postState.text.raw.length < 1 || totalTextCount > 500 + composeState.text.raw.length < 1 || totalTextCount > 500 } /> ) }} > {() => ( - + )} @@ -377,4 +502,4 @@ const styles = StyleSheet.create({ } }) -export default Compose +export default React.memo(Compose, () => true) diff --git a/src/screens/Shared/Compose/Actions.tsx b/src/screens/Shared/Compose/Actions.tsx index 5b3a7cb7..93c81a9d 100644 --- a/src/screens/Shared/Compose/Actions.tsx +++ b/src/screens/Shared/Compose/Actions.tsx @@ -10,24 +10,24 @@ import { } from 'react-native' import { StyleConstants } from 'src/utils/styles/constants' import { useTheme } from 'src/utils/styles/ThemeManager' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import addAttachments from './addAttachments' export interface Props { textInputRef: React.RefObject - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch } const ComposeActions: React.FC = ({ textInputRef, - postState, - postDispatch + composeState, + composeDispatch }) => { const { theme } = useTheme() const getVisibilityIcon = () => { - switch (postState.visibility) { + switch (composeState.visibility) { case 'public': return 'globe' case 'unlisted': @@ -40,88 +40,88 @@ const ComposeActions: React.FC = ({ } const attachmentColor = useMemo(() => { - if (postState.poll.active) return theme.disabled - if (postState.attachmentUploadProgress) return theme.primary + if (composeState.poll.active) return theme.disabled + if (composeState.attachmentUploadProgress) return theme.primary - if (postState.attachments.uploads.length) { + if (composeState.attachments.uploads.length) { return theme.primary } else { return theme.secondary } }, [ - postState.poll.active, - postState.attachments.uploads, - postState.attachmentUploadProgress + composeState.poll.active, + composeState.attachments.uploads, + composeState.attachmentUploadProgress ]) const attachmentOnPress = useCallback(async () => { - if (postState.poll.active) return - if (postState.attachmentUploadProgress) return + if (composeState.poll.active) return + if (composeState.attachmentUploadProgress) return - if (!postState.attachments.uploads.length) { - return await addAttachments({ postState, postDispatch }) + if (!composeState.attachments.uploads.length) { + return await addAttachments({ composeState, composeDispatch }) } }, [ - postState.poll.active, - postState.attachments.uploads, - postState.attachmentUploadProgress + composeState.poll.active, + composeState.attachments.uploads, + composeState.attachmentUploadProgress ]) const pollColor = useMemo(() => { - if (postState.attachments.uploads.length) return theme.disabled - if (postState.attachmentUploadProgress) return theme.disabled + if (composeState.attachments.uploads.length) return theme.disabled + if (composeState.attachmentUploadProgress) return theme.disabled - if (postState.poll.active) { + if (composeState.poll.active) { return theme.primary } else { return theme.secondary } }, [ - postState.poll.active, - postState.attachments.uploads, - postState.attachmentUploadProgress + composeState.poll.active, + composeState.attachments.uploads, + composeState.attachmentUploadProgress ]) const pollOnPress = useCallback(() => { if ( - !postState.attachments.uploads.length && - !postState.attachmentUploadProgress + !composeState.attachments.uploads.length && + !composeState.attachmentUploadProgress ) { - postDispatch({ + composeDispatch({ 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() } }, [ - postState.poll.active, - postState.attachments.uploads, - postState.attachmentUploadProgress + composeState.poll.active, + composeState.attachments.uploads, + composeState.attachmentUploadProgress ]) const emojiColor = useMemo(() => { - if (!postState.emoji.emojis) return theme.disabled - if (postState.emoji.active) { + if (!composeState.emoji.emojis) return theme.disabled + if (composeState.emoji.active) { return theme.primary } else { return theme.secondary } - }, [postState.emoji.active, postState.emoji.emojis]) + }, [composeState.emoji.active, composeState.emoji.emojis]) const emojiOnPress = useCallback(() => { - if (postState.emoji.emojis) { - if (postState.emoji.active) { - postDispatch({ + if (composeState.emoji.emojis) { + if (composeState.emoji.active) { + composeDispatch({ type: 'emoji', - payload: { ...postState.emoji, active: false } + payload: { ...composeState.emoji, active: false } }) } else { - postDispatch({ + composeDispatch({ 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 ( = ({ buttonIndex => { switch (buttonIndex) { case 0: - postDispatch({ type: 'visibility', payload: 'public' }) + composeDispatch({ type: 'visibility', payload: 'public' }) break case 1: - postDispatch({ type: 'visibility', payload: 'unlisted' }) + composeDispatch({ type: 'visibility', payload: 'unlisted' }) break case 2: - postDispatch({ type: 'visibility', payload: 'private' }) + composeDispatch({ type: 'visibility', payload: 'private' }) break case 3: - postDispatch({ type: 'visibility', payload: 'direct' }) + composeDispatch({ type: 'visibility', payload: 'direct' }) break } } @@ -175,11 +175,11 @@ const ComposeActions: React.FC = ({ - postDispatch({ + composeDispatch({ type: 'spoiler', - payload: { active: !postState.spoiler.active } + payload: { active: !composeState.spoiler.active } }) } /> diff --git a/src/screens/Shared/Compose/Attachments.tsx b/src/screens/Shared/Compose/Attachments.tsx index 0ec9b221..c5586eb5 100644 --- a/src/screens/Shared/Compose/Attachments.tsx +++ b/src/screens/Shared/Compose/Attachments.tsx @@ -8,7 +8,7 @@ import { View } from 'react-native' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import { StyleConstants } from 'src/utils/styles/constants' import { useTheme } from 'src/utils/styles/ThemeManager' import { useNavigation } from '@react-navigation/native' @@ -20,11 +20,11 @@ import { Feather } from '@expo/vector-icons' const DEFAULT_HEIGHT = 200 export interface Props { - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch } -const ComposeAttachments: React.FC = ({ postState, postDispatch }) => { +const ComposeAttachments: React.FC = ({ composeState, composeDispatch }) => { const { theme } = useTheme() const navigation = useNavigation() @@ -73,10 +73,10 @@ const ComposeAttachments: React.FC = ({ postState, postDispatch }) => { - postDispatch({ + composeDispatch({ type: 'attachments', payload: { - uploads: postState.attachments.uploads.filter( + uploads: composeState.attachments.uploads.filter( e => e.id !== item.id ) } @@ -89,7 +89,7 @@ const ComposeAttachments: React.FC = ({ postState, postDispatch }) => { onPress={() => navigation.navigate('Screen-Shared-Compose-EditAttachment', { attachment: item, - postDispatch + composeDispatch }) } styles={styles.edit} @@ -104,15 +104,15 @@ const ComposeAttachments: React.FC = ({ postState, postDispatch }) => { return ( - {postState.attachments.uploads.length > 0 && - postState.attachments.uploads[0].type === 'image' && - postState.attachments.uploads.length < 4 && ( + {composeState.attachments.uploads.length > 0 && + composeState.attachments.uploads[0].type === 'image' && + composeState.attachments.uploads.length < 4 && ( = ({ postState, postDispatch }) => { backgroundColor: theme.border }} onPress={async () => - await addAttachments({ postState, postDispatch }) + await addAttachments({ composeState, composeDispatch }) } > - await addAttachments({ postState, postDispatch }) + await addAttachments({ composeState, composeDispatch }) } styles={{ top: @@ -144,21 +144,21 @@ const ComposeAttachments: React.FC = ({ postState, postDispatch }) => { )} ) - }, [postState.attachmentUploadProgress, postState.attachments.uploads]) + }, [composeState.attachmentUploadProgress, composeState.attachments.uploads]) return ( - postDispatch({ + composeDispatch({ type: 'attachments', - payload: { sensitive: !postState.attachments.sensitive } + payload: { sensitive: !composeState.attachments.sensitive } }) } > @@ -169,8 +169,8 @@ const ComposeAttachments: React.FC = ({ postState, postDispatch }) => { + composeDispatch: Dispatch } } } const ComposeEditAttachment: React.FC = ({ route: { - params: { attachment, postDispatch } + params: { attachment, composeDispatch } } }) => { const navigation = useNavigation() @@ -72,7 +72,7 @@ const ComposeEditAttachment: React.FC = ({ } } if (needUpdate) { - postDispatch({ type: 'attachmentEdit', payload: attachment }) + composeDispatch({ type: 'attachmentEdit', payload: attachment }) } }) diff --git a/src/screens/Shared/Compose/Emojis.tsx b/src/screens/Shared/Compose/Emojis.tsx index ff4b1c7b..4027540e 100644 --- a/src/screens/Shared/Compose/Emojis.tsx +++ b/src/screens/Shared/Compose/Emojis.tsx @@ -11,19 +11,19 @@ import { import { StyleConstants } from 'src/utils/styles/constants' import { useTheme } from 'src/utils/styles/ThemeManager' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import updateText from './updateText' export interface Props { textInputRef: React.RefObject - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch } const ComposeEmojis: React.FC = ({ textInputRef, - postState, - postDispatch + composeState, + composeDispatch }) => { const { theme } = useTheme() @@ -32,7 +32,7 @@ const ComposeEmojis: React.FC = ({ item.shortcode} renderSectionHeader={({ section: { title } }) => ( @@ -51,14 +51,14 @@ const ComposeEmojis: React.FC = ({ origin: textInputRef.current?.isFocused() ? 'text' : 'spoiler', - postState, - postDispatch, + composeState, + composeDispatch, newText: `:${emoji.shortcode}:`, type: 'emoji' }) - postDispatch({ + composeDispatch({ type: 'emoji', - payload: { ...postState.emoji, active: false } + payload: { ...composeState.emoji, active: false } }) }} > diff --git a/src/screens/Shared/Compose/Poll.tsx b/src/screens/Shared/Compose/Poll.tsx index 4f5cc9d7..4ec70107 100644 --- a/src/screens/Shared/Compose/Poll.tsx +++ b/src/screens/Shared/Compose/Poll.tsx @@ -2,18 +2,18 @@ import React, { Dispatch, useEffect, useState } from 'react' import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native' import { Feather } from '@expo/vector-icons' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import { useTheme } from 'src/utils/styles/ThemeManager' import { StyleConstants } from 'src/utils/styles/constants' import { ButtonRow } from 'src/components/Button' import { MenuContainer, MenuRow } from 'src/components/Menu' export interface Props { - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch } -const ComposePoll: React.FC = ({ postState, postDispatch }) => { +const ComposePoll: React.FC = ({ composeState, composeDispatch }) => { const { theme } = useTheme() const expireMapping: { [key: string]: string } = { @@ -34,21 +34,21 @@ const ComposePoll: React.FC = ({ postState, postDispatch }) => { return ( - {[...Array(postState.poll.total)].map((e, i) => { - const restOptions = Object.keys(postState.poll.options).filter( - o => parseInt(o) !== i && parseInt(o) < postState.poll.total + {[...Array(composeState.poll.total)].map((e, i) => { + const restOptions = Object.keys(composeState.poll.options).filter( + o => parseInt(o) !== i && parseInt(o) < composeState.poll.total ) let hasConflict = false restOptions.forEach(o => { // @ts-ignore - if (postState.poll.options[o] === postState.poll.options[i]) { + if (composeState.poll.options[o] === composeState.poll.options[i]) { hasConflict = true } }) return ( @@ -65,13 +65,13 @@ const ComposePoll: React.FC = ({ postState, postDispatch }) => { placeholderTextColor={theme.secondary} maxLength={50} // @ts-ignore - value={postState.poll.options[i]} + value={composeState.poll.options[i]} onChangeText={e => - postDispatch({ + composeDispatch({ type: 'poll', payload: { - ...postState.poll, - options: { ...postState.poll.options, [i]: e } + ...composeState.poll, + options: { ...composeState.poll.options, [i]: e } } }) } @@ -84,34 +84,34 @@ const ComposePoll: React.FC = ({ postState, postDispatch }) => { - postState.poll.total > 2 && - postDispatch({ + composeState.poll.total > 2 && + composeDispatch({ type: 'poll', - payload: { ...postState.poll, total: postState.poll.total - 1 } + payload: { ...composeState.poll, total: composeState.poll.total - 1 } }) } icon='minus' - disabled={!(postState.poll.total > 2)} + disabled={!(composeState.poll.total > 2)} buttonSize='S' /> - postState.poll.total < 4 && - postDispatch({ + composeState.poll.total < 4 && + composeDispatch({ type: 'poll', - payload: { ...postState.poll, total: postState.poll.total + 1 } + payload: { ...composeState.poll, total: composeState.poll.total + 1 } }) } icon='plus' - disabled={!(postState.poll.total < 4)} + disabled={!(composeState.poll.total < 4)} buttonSize='S' /> ActionSheetIOS.showActionSheetWithOptions( { @@ -120,9 +120,9 @@ const ComposePoll: React.FC = ({ postState, postDispatch }) => { }, index => index < 2 && - postDispatch({ + composeDispatch({ type: 'poll', - payload: { ...postState.poll, multiple: index === 1 } + payload: { ...composeState.poll, multiple: index === 1 } }) ) } @@ -130,7 +130,7 @@ const ComposePoll: React.FC = ({ postState, postDispatch }) => { /> ActionSheetIOS.showActionSheetWithOptions( { @@ -139,10 +139,10 @@ const ComposePoll: React.FC = ({ postState, postDispatch }) => { }, index => index < 7 && - postDispatch({ + composeDispatch({ type: 'poll', payload: { - ...postState.poll, + ...composeState.poll, expire: Object.keys(expireMapping)[index] } }) diff --git a/src/screens/Shared/Compose/Root.tsx b/src/screens/Shared/Compose/Root.tsx index af0a1b4f..eabf3af6 100644 --- a/src/screens/Shared/Compose/Root.tsx +++ b/src/screens/Shared/Compose/Root.tsx @@ -17,7 +17,7 @@ import { emojisFetch } from 'src/utils/fetches/emojisFetch' import { searchFetch } from 'src/utils/fetches/searchFetch' import { StyleConstants } from 'src/utils/styles/constants' import { useTheme } from 'src/utils/styles/ThemeManager' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import ComposeActions from './Actions' import ComposeAttachments from './Attachments' import ComposeEmojis from './Emojis' @@ -28,26 +28,26 @@ import updateText from './updateText' import * as Permissions from 'expo-permissions' export interface Props { - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch } -const ComposeRoot: React.FC = ({ postState, postDispatch }) => { +const ComposeRoot: React.FC = ({ composeState, composeDispatch }) => { const { theme } = useTheme() const { isFetching, isSuccess, data, refetch } = useQuery( [ 'Search', - { type: postState.tag?.type, term: postState.tag?.text.substring(1) } + { type: composeState.tag?.type, term: composeState.tag?.text.substring(1) } ], searchFetch, { enabled: false } ) useEffect(() => { - if (postState.tag?.text) { + if (composeState.tag?.text) { refetch() } - }, [postState.tag?.text]) + }, [composeState.tag?.text]) useEffect(() => { ;(async () => { @@ -71,9 +71,9 @@ const ComposeRoot: React.FC = ({ postState, postDispatch }) => { groupBy(sortBy(emojisData, ['category', 'shortcode']), 'category'), (value, key) => sortedEmojis.push({ title: key, data: value }) ) - postDispatch({ + composeDispatch({ type: 'emoji', - payload: { ...postState.emoji, emojis: sortedEmojis } + payload: { ...composeState.emoji, emojis: sortedEmojis } }) } }, [emojisData]) @@ -89,60 +89,60 @@ const ComposeRoot: React.FC = ({ postState, postDispatch }) => { return ( - {postState.spoiler.active ? ( + {composeState.spoiler.active ? ( ) : null} } ListFooterComponent={ <> - {postState.emoji.active && ( + {composeState.emoji.active && ( )} - {(postState.attachments.uploads.length > 0 || - postState.attachmentUploadProgress) && ( + {(composeState.attachments.uploads.length > 0 || + composeState.attachmentUploadProgress) && ( )} - {postState.poll.active && ( + {composeState.poll.active && ( )} } ListEmptyComponent={listEmpty} - data={postState.tag && isSuccess ? data[postState.tag.type] : []} + data={composeState.tag && isSuccess ? data[composeState.tag.type] : []} renderItem={({ item, index }) => ( = ({ postState, postDispatch }) => { : 'spoiler' updateText({ origin: focusedInput, - postState: { - ...postState, + composeState: { + ...composeState, [focusedInput]: { - ...postState[focusedInput], + ...composeState[focusedInput], selection: { - start: postState.tag!.offset, + start: composeState.tag!.offset, 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}`, type: 'suggestion' }) @@ -225,8 +225,8 @@ const ComposeRoot: React.FC = ({ postState, postDispatch }) => { /> ) diff --git a/src/screens/Shared/Compose/SpoilerInput.tsx b/src/screens/Shared/Compose/SpoilerInput.tsx index 2fe45f89..006dec33 100644 --- a/src/screens/Shared/Compose/SpoilerInput.tsx +++ b/src/screens/Shared/Compose/SpoilerInput.tsx @@ -2,18 +2,18 @@ import React, { Dispatch, RefObject } from 'react' import { 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 { PostAction, ComposeState } from '../Compose' import formatText from './formatText' export interface Props { - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch // textInputRef: RefObject } const ComposeSpoilerInput: React.FC = ({ - postState, - postDispatch, + composeState, + composeDispatch, // textInputRef }) => { const { theme } = useTheme() @@ -37,7 +37,7 @@ const ComposeSpoilerInput: React.FC = ({ onChangeText={content => formatText({ origin: 'spoiler', - postDispatch, + composeDispatch, content }) } @@ -46,7 +46,7 @@ const ComposeSpoilerInput: React.FC = ({ selection: { start, end } } }) => { - postDispatch({ + composeDispatch({ type: 'spoiler', payload: { selection: { start, end } } }) @@ -54,7 +54,7 @@ const ComposeSpoilerInput: React.FC = ({ // ref={textInputRef} scrollEnabled > - {postState.spoiler.formatted} + {composeState.spoiler.formatted} ) } @@ -73,5 +73,5 @@ const styles = StyleSheet.create({ export default React.memo( ComposeSpoilerInput, (prev, next) => - prev.postState.spoiler.formatted === next.postState.spoiler.formatted + prev.composeState.spoiler.formatted === next.composeState.spoiler.formatted ) diff --git a/src/screens/Shared/Compose/TextInput.tsx b/src/screens/Shared/Compose/TextInput.tsx index 46c68185..d6444b59 100644 --- a/src/screens/Shared/Compose/TextInput.tsx +++ b/src/screens/Shared/Compose/TextInput.tsx @@ -2,18 +2,18 @@ import React, { Dispatch, RefObject } from 'react' import { 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 { PostAction, ComposeState } from '../Compose' import formatText from './formatText' export interface Props { - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch textInputRef: RefObject } const ComposeTextInput: React.FC = ({ - postState, - postDispatch, + composeState, + composeDispatch, textInputRef }) => { const { theme } = useTheme() @@ -37,7 +37,7 @@ const ComposeTextInput: React.FC = ({ onChangeText={content => formatText({ origin: 'text', - postDispatch, + composeDispatch, content }) } @@ -46,12 +46,12 @@ const ComposeTextInput: React.FC = ({ selection: { start, end } } }) => { - postDispatch({ type: 'text', payload: { selection: { start, end } } }) + composeDispatch({ type: 'text', payload: { selection: { start, end } } }) }} ref={textInputRef} scrollEnabled > - {postState.text.formatted} + {composeState.text.formatted} ) } @@ -70,5 +70,5 @@ const styles = StyleSheet.create({ export default React.memo( ComposeTextInput, (prev, next) => - prev.postState.text.formatted === next.postState.text.formatted + prev.composeState.text.formatted === next.composeState.text.formatted ) diff --git a/src/screens/Shared/Compose/addAttachments.ts b/src/screens/Shared/Compose/addAttachments.ts index df55ccac..611347c8 100644 --- a/src/screens/Shared/Compose/addAttachments.ts +++ b/src/screens/Shared/Compose/addAttachments.ts @@ -2,18 +2,18 @@ import { Dispatch } from 'react' import { ActionSheetIOS, Alert } from 'react-native' import * as ImagePicker from 'expo-image-picker' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import client from 'src/api/client' import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types' const uploadAttachment = async ({ result, - postState, - postDispatch + composeState, + composeDispatch }: { result: NonNullable - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch }) => { const formData = new FormData() // @ts-ignore @@ -30,7 +30,7 @@ const uploadAttachment = async ({ url: 'media', body: formData, onUploadProgress: p => { - postDispatch({ + composeDispatch({ type: 'attachmentUploadProgress', payload: { progress: p.loaded / p.total, @@ -40,15 +40,15 @@ const uploadAttachment = async ({ } }) .then(({ body }: { body: Mastodon.Attachment & { local_url: string } }) => { - postDispatch({ + composeDispatch({ type: 'attachmentUploadProgress', payload: undefined }) if (body.id) { body.local_url = result.uri - postDispatch({ + composeDispatch({ type: 'attachments', - payload: { uploads: postState.attachments.uploads.concat([body]) } + payload: { uploads: composeState.attachments.uploads.concat([body]) } }) return Promise.resolve() } else { @@ -56,7 +56,7 @@ const uploadAttachment = async ({ { text: '返回重试', onPress: () => - postDispatch({ + composeDispatch({ type: 'attachmentUploadProgress', payload: undefined }) @@ -70,7 +70,7 @@ const uploadAttachment = async ({ { text: '返回重试', onPress: () => - postDispatch({ + composeDispatch({ type: 'attachmentUploadProgress', payload: undefined }) @@ -83,8 +83,8 @@ const uploadAttachment = async ({ const addAttachments = async ({ ...params }: { - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch }): Promise => { ActionSheetIOS.showActionSheetWithOptions( { diff --git a/src/screens/Shared/Compose/formatText.tsx b/src/screens/Shared/Compose/formatText.tsx index d923453e..910741ae 100644 --- a/src/screens/Shared/Compose/formatText.tsx +++ b/src/screens/Shared/Compose/formatText.tsx @@ -4,11 +4,11 @@ import { Text } from 'react-native' import { RefetchOptions } from 'react-query/types/core/query' import Autolinker from 'src/modules/autolinker' import { useTheme } from 'src/utils/styles/ThemeManager' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' export interface Params { origin: 'text' | 'spoiler' - postDispatch: Dispatch + composeDispatch: Dispatch content: string refetch?: (options?: RefetchOptions | undefined) => Promise disableDebounce?: boolean @@ -25,8 +25,8 @@ const TagText = ({ text }: { text: string }) => { } const debouncedSuggestions = debounce( - (postDispatch, tag) => { - postDispatch({ type: 'tag', payload: tag }) + (composeDispatch, tag) => { + composeDispatch({ type: 'tag', payload: tag }) }, 500, { @@ -34,15 +34,15 @@ const debouncedSuggestions = debounce( } ) -let prevTags: PostState['tag'][] = [] +let prevTags: ComposeState['tag'][] = [] const formatText = ({ origin, - postDispatch, + composeDispatch, content, disableDebounce = false }: Params) => { - const tags: PostState['tag'][] = [] + const tags: ComposeState['tag'][] = [] Autolinker.link(content, { email: false, phone: false, @@ -74,11 +74,11 @@ const formatText = ({ const changedTag = differenceWith(tags, prevTags, isEqual) if (changedTag.length && !disableDebounce) { if (changedTag[0]!.type !== 'url') { - debouncedSuggestions(postDispatch, changedTag[0]) + debouncedSuggestions(composeDispatch, changedTag[0]) } } else { debouncedSuggestions.cancel() - postDispatch({ type: 'tag', payload: undefined }) + composeDispatch({ type: 'tag', payload: undefined }) } prevTags = tags let _content = content @@ -107,7 +107,7 @@ const formatText = ({ children.push(_content) contentLength = contentLength + _content.length - postDispatch({ + composeDispatch({ type: origin, payload: { count: contentLength, diff --git a/src/screens/Shared/Compose/updateText.ts b/src/screens/Shared/Compose/updateText.ts index e32b2a43..46fa7104 100644 --- a/src/screens/Shared/Compose/updateText.ts +++ b/src/screens/Shared/Compose/updateText.ts @@ -1,27 +1,27 @@ import { Dispatch } from 'react' -import { PostAction, PostState } from '../Compose' +import { PostAction, ComposeState } from '../Compose' import formatText from './formatText' const updateText = ({ origin, - postState, - postDispatch, + composeState, + composeDispatch, newText, type }: { origin: 'text' | 'spoiler' - postState: PostState - postDispatch: Dispatch + composeState: ComposeState + composeDispatch: Dispatch newText: string type: 'emoji' | 'suggestion' }) => { - if (postState[origin].raw.length) { - const contentFront = postState[origin].raw.slice( + if (composeState[origin].raw.length) { + const contentFront = composeState[origin].raw.slice( 0, - postState[origin].selection.start + composeState[origin].selection.start ) - const contentRear = postState[origin].raw.slice( - postState[origin].selection.end + const contentRear = composeState[origin].raw.slice( + composeState[origin].selection.end ) const whiteSpaceFront = /\s/g.test(contentFront.slice(-1)) @@ -33,14 +33,14 @@ const updateText = ({ formatText({ origin, - postDispatch, + composeDispatch, content: [contentFront, newTextWithSpace, contentRear].join(''), disableDebounce: true }) } else { formatText({ origin, - postDispatch, + composeDispatch, content: `${newText} `, disableDebounce: true }) diff --git a/src/utils/getCurrentTab.ts b/src/utils/getCurrentTab.ts index 0b359ffc..c7692d5b 100644 --- a/src/utils/getCurrentTab.ts +++ b/src/utils/getCurrentTab.ts @@ -1,8 +1,7 @@ const getCurrentTab = (navigation: any) => { - const { - length, - [length - 1]: last - } = navigation.dangerouslyGetState().history + const { length, [length - 1]: last } = + navigation.dangerouslyGetState().history || + navigation.dangerouslyGetParent()?.dangerouslyGetState().history return `Screen-${last.key.split(new RegExp(/Screen-(.*?)-/))[1]}` }