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
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
17 changed files with 420 additions and 253 deletions

View File

@ -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
}

View File

@ -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<Props> = ({ queryKey, status }) => {
const navigation = useNavigation()
const { theme } = useTheme()
const iconColor = theme.secondary
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(
() =>
mutateAction({

View File

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

View File

@ -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<Props> = ({
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<Props> = ({
/>
<MenuRow
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'
title='删除并重发'
title='删除并重新编辑'
/>
<MenuRow
onPress={() => {

View File

@ -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<PostState['spoiler']>
payload: Partial<ComposeState['spoiler']>
}
| {
type: 'text'
payload: Partial<PostState['text']>
payload: Partial<ComposeState['text']>
}
| {
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<PostState['attachments']>
payload: Partial<ComposeState['attachments']>
}
| {
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<Props> = ({ 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 (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
@ -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
}
/>
)
}}
>
{() => (
<ComposeRoot postState={postState} postDispatch={postDispatch} />
<ComposeRoot
composeState={composeState}
composeDispatch={composeDispatch}
/>
)}
</Stack.Screen>
</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'
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<TextInput>
postState: PostState
postDispatch: Dispatch<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const ComposeActions: React.FC<Props> = ({
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<Props> = ({
}
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 (
<Pressable
@ -156,16 +156,16 @@ const ComposeActions: React.FC<Props> = ({
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<Props> = ({
<Feather
name='alert-triangle'
size={24}
color={postState.spoiler.active ? theme.primary : theme.secondary}
color={composeState.spoiler.active ? theme.primary : theme.secondary}
onPress={() =>
postDispatch({
composeDispatch({
type: 'spoiler',
payload: { active: !postState.spoiler.active }
payload: { active: !composeState.spoiler.active }
})
}
/>

View File

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

View File

@ -36,14 +36,14 @@ export interface Props {
route: {
params: {
attachment: Mastodon.Attachment & { local_url: string }
postDispatch: Dispatch<PostAction>
composeDispatch: Dispatch<PostAction>
}
}
}
const ComposeEditAttachment: React.FC<Props> = ({
route: {
params: { attachment, postDispatch }
params: { attachment, composeDispatch }
}
}) => {
const navigation = useNavigation()
@ -72,7 +72,7 @@ const ComposeEditAttachment: React.FC<Props> = ({
}
}
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 { 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<TextInput>
postState: PostState
postDispatch: Dispatch<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const ComposeEmojis: React.FC<Props> = ({
textInputRef,
postState,
postDispatch
composeState,
composeDispatch
}) => {
const { theme } = useTheme()
@ -32,7 +32,7 @@ const ComposeEmojis: React.FC<Props> = ({
<SectionList
horizontal
keyboardShouldPersistTaps='handled'
sections={postState.emoji.emojis!}
sections={composeState.emoji.emojis!}
keyExtractor={item => item.shortcode}
renderSectionHeader={({ section: { title } }) => (
<Text style={[styles.group, { color: theme.secondary }]}>
@ -51,14 +51,14 @@ const ComposeEmojis: React.FC<Props> = ({
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 }
})
}}
>

View File

@ -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<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => {
const { theme } = useTheme()
const expireMapping: { [key: string]: string } = {
@ -34,21 +34,21 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
return (
<View style={[styles.base, { borderColor: theme.border }]}>
<View style={styles.options}>
{[...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 (
<View key={i} style={styles.option}>
<Feather
name={postState.poll.multiple ? 'square' : 'circle'}
name={composeState.poll.multiple ? 'square' : 'circle'}
size={StyleConstants.Font.Size.L}
color={theme.secondary}
/>
@ -65,13 +65,13 @@ const ComposePoll: React.FC<Props> = ({ 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<Props> = ({ postState, postDispatch }) => {
<View style={styles.firstButton}>
<ButtonRow
onPress={() =>
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'
/>
</View>
<ButtonRow
onPress={() =>
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'
/>
</View>
<MenuContainer>
<MenuRow
title='可选项'
content={postState.poll.multiple ? '多选' : '单选'}
content={composeState.poll.multiple ? '多选' : '单选'}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
@ -120,9 +120,9 @@ const ComposePoll: React.FC<Props> = ({ 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<Props> = ({ postState, postDispatch }) => {
/>
<MenuRow
title='有效期'
content={expireMapping[postState.poll.expire]}
content={expireMapping[composeState.poll.expire]}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
@ -139,10 +139,10 @@ const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => {
},
index =>
index < 7 &&
postDispatch({
composeDispatch({
type: 'poll',
payload: {
...postState.poll,
...composeState.poll,
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 { 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<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}
const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => {
const ComposeRoot: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ postState, postDispatch }) => {
return (
<View style={styles.base}>
<ProgressViewIOS
progress={postState.attachmentUploadProgress?.progress || 0}
progress={composeState.attachmentUploadProgress?.progress || 0}
progressViewStyle='bar'
/>
<FlatList
keyboardShouldPersistTaps='handled'
ListHeaderComponent={
<>
{postState.spoiler.active ? (
{composeState.spoiler.active ? (
<ComposeSpoilerInput
postState={postState}
postDispatch={postDispatch}
composeState={composeState}
composeDispatch={composeDispatch}
/>
) : null}
<ComposeTextInput
postState={postState}
postDispatch={postDispatch}
composeState={composeState}
composeDispatch={composeDispatch}
textInputRef={textInputRef}
/>
</>
}
ListFooterComponent={
<>
{postState.emoji.active && (
{composeState.emoji.active && (
<View style={styles.emojis}>
<ComposeEmojis
textInputRef={textInputRef}
postState={postState}
postDispatch={postDispatch}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)}
{(postState.attachments.uploads.length > 0 ||
postState.attachmentUploadProgress) && (
{(composeState.attachments.uploads.length > 0 ||
composeState.attachmentUploadProgress) && (
<View style={styles.attachments}>
<ComposeAttachments
postState={postState}
postDispatch={postDispatch}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)}
{postState.poll.active && (
{composeState.poll.active && (
<View style={styles.poll}>
<ComposePoll
postState={postState}
postDispatch={postDispatch}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)}
</>
}
ListEmptyComponent={listEmpty}
data={postState.tag && isSuccess ? data[postState.tag.type] : []}
data={composeState.tag && isSuccess ? data[composeState.tag.type] : []}
renderItem={({ item, index }) => (
<Pressable
key={index}
@ -152,18 +152,18 @@ const ComposeRoot: React.FC<Props> = ({ 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<Props> = ({ postState, postDispatch }) => {
/>
<ComposeActions
textInputRef={textInputRef}
postState={postState}
postDispatch={postDispatch}
composeState={composeState}
composeDispatch={composeDispatch}
/>
</View>
)

View File

@ -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<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
// textInputRef: RefObject<TextInput>
}
const ComposeSpoilerInput: React.FC<Props> = ({
postState,
postDispatch,
composeState,
composeDispatch,
// textInputRef
}) => {
const { theme } = useTheme()
@ -37,7 +37,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
onChangeText={content =>
formatText({
origin: 'spoiler',
postDispatch,
composeDispatch,
content
})
}
@ -46,7 +46,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
selection: { start, end }
}
}) => {
postDispatch({
composeDispatch({
type: 'spoiler',
payload: { selection: { start, end } }
})
@ -54,7 +54,7 @@ const ComposeSpoilerInput: React.FC<Props> = ({
// ref={textInputRef}
scrollEnabled
>
<Text>{postState.spoiler.formatted}</Text>
<Text>{composeState.spoiler.formatted}</Text>
</TextInput>
)
}
@ -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
)

View File

@ -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<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
textInputRef: RefObject<TextInput>
}
const ComposeTextInput: React.FC<Props> = ({
postState,
postDispatch,
composeState,
composeDispatch,
textInputRef
}) => {
const { theme } = useTheme()
@ -37,7 +37,7 @@ const ComposeTextInput: React.FC<Props> = ({
onChangeText={content =>
formatText({
origin: 'text',
postDispatch,
composeDispatch,
content
})
}
@ -46,12 +46,12 @@ const ComposeTextInput: React.FC<Props> = ({
selection: { start, end }
}
}) => {
postDispatch({ type: 'text', payload: { selection: { start, end } } })
composeDispatch({ type: 'text', payload: { selection: { start, end } } })
}}
ref={textInputRef}
scrollEnabled
>
<Text>{postState.text.formatted}</Text>
<Text>{composeState.text.formatted}</Text>
</TextInput>
)
}
@ -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
)

View File

@ -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<ImageInfo>
postState: PostState
postDispatch: Dispatch<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}) => {
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<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
}): Promise<any> => {
ActionSheetIOS.showActionSheetWithOptions(
{

View File

@ -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<PostAction>
composeDispatch: Dispatch<PostAction>
content: string
refetch?: (options?: RefetchOptions | undefined) => Promise<any>
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,

View File

@ -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<PostAction>
composeState: ComposeState
composeDispatch: Dispatch<PostAction>
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
})

View File

@ -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]}`
}