tooot/src/screens/Shared/Compose.tsx

381 lines
10 KiB
TypeScript
Raw Normal View History

2020-11-15 23:33:01 +01:00
import React, { ReactNode, useEffect, useReducer, useState } from 'react'
2020-12-06 23:51:13 +01:00
import {
ActivityIndicator,
Alert,
Keyboard,
KeyboardAvoidingView,
StyleSheet,
Text
} from 'react-native'
2020-11-15 23:33:01 +01:00
import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useNavigation } from '@react-navigation/native'
2020-12-06 23:51:13 +01:00
import sha256 from 'crypto-js/sha256'
2020-11-21 13:32:29 +01:00
import { store } from 'src/store'
2020-12-03 22:03:06 +01:00
import ComposeRoot from './Compose/Root'
2020-11-15 22:33:09 +01:00
import client from 'src/api/client'
2020-11-21 13:19:05 +01:00
import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice'
2020-12-03 01:28:56 +01:00
import { HeaderLeft, HeaderRight } from 'src/components/Header'
2020-12-06 23:51:13 +01:00
import { StyleConstants } from 'src/utils/styles/constants'
import { useTheme } from 'src/utils/styles/ThemeManager'
2020-11-15 20:29:43 +01:00
const Stack = createNativeStackNavigator()
2020-11-15 22:33:09 +01:00
export type PostState = {
2020-12-06 23:51:13 +01:00
spoiler: {
active: boolean
count: number
raw: string
formatted: ReactNode
selection: { start: number; end: number }
}
2020-11-15 22:33:09 +01:00
text: {
count: number
raw: string
formatted: ReactNode
2020-12-06 23:51:13 +01:00
selection: { start: number; end: number }
2020-11-15 22:33:09 +01:00
}
tag:
| {
type: 'url' | 'accounts' | 'hashtags'
text: string
offset: number
}
| undefined
2020-12-04 01:17:10 +01:00
emoji: {
active: boolean
emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined
}
2020-11-17 23:57:23 +01:00
poll: {
active: boolean
total: number
options: {
2020-12-06 22:32:36 +01:00
'0': string | undefined
'1': string | undefined
'2': string | undefined
'3': string | undefined
2020-11-17 23:57:23 +01:00
}
multiple: boolean
expire:
| '300'
| '1800'
| '3600'
| '21600'
| '86400'
| '259200'
| '604800'
| string
}
2020-12-07 00:23:26 +01:00
attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] }
2020-12-06 21:42:19 +01:00
attachmentUploadProgress: { progress: number; aspect?: number } | undefined
visibility: 'public' | 'unlisted' | 'private' | 'direct'
2020-11-15 22:33:09 +01:00
}
export type PostAction =
| {
2020-12-06 23:51:13 +01:00
type: 'spoiler'
payload: Partial<PostState['spoiler']>
2020-11-15 22:33:09 +01:00
}
| {
2020-12-06 23:51:13 +01:00
type: 'text'
payload: Partial<PostState['text']>
2020-11-15 22:33:09 +01:00
}
| {
type: 'tag'
payload: PostState['tag']
}
| {
2020-12-04 01:17:10 +01:00
type: 'emoji'
payload: PostState['emoji']
2020-11-15 22:33:09 +01:00
}
2020-11-17 23:57:23 +01:00
| {
type: 'poll'
payload: PostState['poll']
}
| {
type: 'attachments'
2020-12-07 00:23:26 +01:00
payload: Partial<PostState['attachments']>
}
| {
type: 'attachmentUploadProgress'
payload: PostState['attachmentUploadProgress']
}
2020-12-06 16:06:38 +01:00
| {
type: 'attachmentEdit'
payload: Mastodon.Attachment & { local_url?: string }
}
| {
type: 'visibility'
payload: PostState['visibility']
}
2020-11-15 22:33:09 +01:00
const postInitialState: PostState = {
2020-12-06 23:51:13 +01:00
spoiler: {
active: false,
count: 0,
raw: '',
formatted: undefined,
selection: { start: 0, end: 0 }
},
2020-11-15 22:33:09 +01:00
text: {
2020-12-06 23:51:13 +01:00
count: 0,
2020-11-15 22:33:09 +01:00
raw: '',
2020-12-06 23:51:13 +01:00
formatted: undefined,
selection: { start: 0, end: 0 }
2020-11-15 22:33:09 +01:00
},
tag: undefined,
2020-12-04 01:17:10 +01:00
emoji: { active: false, emojis: undefined },
2020-11-17 23:57:23 +01:00
poll: {
active: false,
total: 2,
options: {
2020-12-06 23:51:13 +01:00
'0': undefined,
'1': undefined,
'2': undefined,
2020-12-06 23:51:13 +01:00
'3': undefined
2020-11-17 23:57:23 +01:00
},
multiple: false,
expire: '86400'
},
2020-12-07 00:23:26 +01:00
attachments: { sensitive: false, uploads: [] },
attachmentUploadProgress: undefined,
visibility:
getLocalAccountPreferences(store.getState())[
'posting:default:visibility'
] || 'public'
2020-11-15 22:33:09 +01:00
}
const postReducer = (state: PostState, action: PostAction): PostState => {
switch (action.type) {
2020-12-06 23:51:13 +01:00
case 'spoiler':
return { ...state, spoiler: { ...state.spoiler, ...action.payload } }
2020-11-15 22:33:09 +01:00
case 'text':
return { ...state, text: { ...state.text, ...action.payload } }
case 'tag':
return { ...state, tag: action.payload }
2020-12-04 01:17:10 +01:00
case 'emoji':
return { ...state, emoji: action.payload }
2020-11-17 23:57:23 +01:00
case 'poll':
return { ...state, poll: action.payload }
case 'attachments':
2020-12-07 00:23:26 +01:00
return {
...state,
attachments: { ...state.attachments, ...action.payload }
}
case 'attachmentUploadProgress':
return { ...state, attachmentUploadProgress: action.payload }
2020-12-06 16:06:38 +01:00
case 'attachmentEdit':
return {
...state,
2020-12-07 00:23:26 +01:00
attachments: {
...state.attachments,
uploads: state.attachments.uploads.map(upload =>
upload.id === action.payload.id ? action.payload : upload
)
}
2020-12-06 16:06:38 +01:00
}
case 'visibility':
return { ...state, visibility: action.payload }
2020-11-15 22:33:09 +01:00
default:
throw new Error('Unexpected action')
}
}
2020-11-22 00:57:43 +01:00
const Compose: React.FC = () => {
2020-12-06 23:51:13 +01:00
const { theme } = useTheme()
const navigation = useNavigation()
2020-12-06 23:51:13 +01:00
const [isSubmitting, setIsSubmitting] = useState(false)
2020-11-15 23:33:01 +01:00
const [hasKeyboard, setHasKeyboard] = useState(false)
useEffect(() => {
Keyboard.addListener('keyboardWillShow', _keyboardDidShow)
Keyboard.addListener('keyboardWillHide', _keyboardDidHide)
// cleanup function
return () => {
Keyboard.removeListener('keyboardWillShow', _keyboardDidShow)
Keyboard.removeListener('keyboardWillHide', _keyboardDidHide)
}
}, [])
const _keyboardDidShow = () => {
setHasKeyboard(true)
}
const _keyboardDidHide = () => {
setHasKeyboard(false)
}
2020-11-15 22:33:09 +01:00
const [postState, postDispatch] = useReducer(postReducer, postInitialState)
2020-11-17 23:57:23 +01:00
const tootPost = async () => {
2020-12-06 23:51:13 +01:00
setIsSubmitting(true)
2020-11-17 23:57:23 +01:00
if (postState.text.count < 0) {
Alert.alert('字数超限', '', [
{
text: '返回继续编辑'
}
])
} else {
const formData = new FormData()
2020-12-06 23:51:13 +01:00
if (postState.spoiler.active) {
formData.append('spoiler_text', postState.spoiler.raw)
}
2020-11-17 23:57:23 +01:00
formData.append('status', postState.text.raw)
2020-11-17 23:57:23 +01:00
if (postState.poll.active) {
Object.values(postState.poll.options)
.filter(e => e?.length)
.forEach(e => formData.append('poll[options][]', e!))
2020-11-17 23:57:23 +01:00
formData.append('poll[expires_in]', postState.poll.expire)
formData.append('poll[multiple]', postState.poll.multiple.toString())
}
2020-12-07 00:23:26 +01:00
if (postState.attachments.uploads.length) {
formData.append('sensitive', postState.attachments.sensitive.toString())
postState.attachments.uploads.forEach(e =>
formData.append('media_ids[]', e!.id)
2020-11-20 01:41:46 +01:00
)
}
2020-11-20 01:41:46 +01:00
formData.append('visibility', postState.visibility)
2020-11-17 23:57:23 +01:00
client({
method: 'post',
instance: 'local',
url: 'statuses',
2020-11-17 23:57:23 +01:00
headers: {
2020-12-06 23:51:13 +01:00
'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 +
2020-12-07 00:23:26 +01:00
postState.attachments.sensitive +
postState.attachments.uploads.map(upload => upload.id) +
2020-12-06 23:51:13 +01:00
postState.visibility
).toString()
2020-11-17 23:57:23 +01:00
},
body: formData
})
.then(
res => {
if (res.body.id) {
2020-12-06 23:51:13 +01:00
setIsSubmitting(false)
2020-11-17 23:57:23 +01:00
Alert.alert('发布成功', '', [
{
text: '好的',
2020-11-20 01:41:46 +01:00
onPress: () => {
// clear homepage cache
navigation.goBack()
}
2020-11-17 23:57:23 +01:00
}
])
} else {
2020-12-06 23:51:13 +01:00
setIsSubmitting(false)
2020-11-17 23:57:23 +01:00
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
}
},
error => {
2020-12-06 23:51:13 +01:00
setIsSubmitting(false)
2020-11-17 23:57:23 +01:00
Alert.alert('发布失败', error.body, [
{
text: '返回重试'
}
])
}
)
.catch(() => {
2020-12-06 23:51:13 +01:00
setIsSubmitting(false)
2020-11-17 23:57:23 +01:00
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
})
}
}
2020-12-06 23:51:13 +01:00
const totalTextCount =
(postState.spoiler.active ? postState.spoiler.count : 0) +
postState.text.count
return (
2020-11-15 23:33:01 +01:00
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView
style={{ flex: 1 }}
edges={hasKeyboard ? ['left', 'right'] : ['left', 'right', 'bottom']}
>
<Stack.Navigator>
<Stack.Screen
name='PostMain'
options={{
headerLeft: () => (
2020-12-03 01:28:56 +01:00
<HeaderLeft
2020-11-15 23:33:01 +01:00
onPress={() =>
Alert.alert('确认取消编辑?', '', [
{ text: '继续编辑', style: 'cancel' },
2020-11-15 22:33:09 +01:00
{
2020-11-15 23:33:01 +01:00
text: '退出编辑',
style: 'destructive',
2020-11-15 22:33:09 +01:00
onPress: () => navigation.goBack()
}
])
}
2020-12-03 01:28:56 +01:00
text='退出编辑'
/>
2020-11-15 23:33:01 +01:00
),
2020-12-06 23:51:13 +01:00
headerCenter: () => (
<Text
style={[
styles.count,
{
color:
totalTextCount > 500 ? theme.error : theme.secondary
}
]}
>
{totalTextCount} / 500
</Text>
),
headerRight: () =>
isSubmitting ? (
<ActivityIndicator />
) : (
<HeaderRight
onPress={async () => tootPost()}
text='发嘟嘟'
disabled={
postState.text.raw.length < 1 || totalTextCount > 500
}
/>
)
2020-11-15 23:33:01 +01:00
}}
>
2020-12-03 01:28:56 +01:00
{() => (
2020-12-03 22:03:06 +01:00
<ComposeRoot postState={postState} postDispatch={postDispatch} />
2020-11-15 23:33:01 +01:00
)}
</Stack.Screen>
</Stack.Navigator>
</SafeAreaView>
</KeyboardAvoidingView>
)
}
2020-12-06 23:51:13 +01:00
const styles = StyleSheet.create({
count: {
textAlign: 'center',
fontSize: StyleConstants.Font.Size.M
}
})
2020-11-22 00:57:43 +01:00
export default Compose