Posting using formData

This commit is contained in:
Zhiyuan Zheng 2020-11-17 23:57:23 +01:00
parent 5cf6eaa8d9
commit c0d7f379b3
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
8 changed files with 345 additions and 66 deletions

View File

@ -11,6 +11,11 @@ setConsole({
error: console.warn error: console.warn
}) })
if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React)
}
const App: React.FC = () => ( const App: React.FC = () => (
<ReactQueryCacheProvider queryCache={queryCache}> <ReactQueryCacheProvider queryCache={queryCache}>
<Index /> <Index />

View File

@ -1,10 +1,16 @@
module.exports = function (api) { module.exports = function (api) {
api.cache(true) api.cache(true)
return { return {
presets: ['babel-preset-expo'], presets: [
'babel-preset-expo',
// {
// runtime: 'automatic',
// development: process.env.NODE_ENV === 'development',
// importSource: '@welldone-software/why-did-you-render'
// }
],
plugins: [ plugins: [
['@babel/plugin-proposal-optional-chaining'], ['@babel/plugin-proposal-optional-chaining'],
// ['babel-plugin-typescript-to-proptypes'],
[ [
'module-resolver', 'module-resolver',
{ {

View File

@ -20,6 +20,7 @@
"expo": "~39.0.4", "expo": "~39.0.4",
"expo-app-auth": "~9.2.0", "expo-app-auth": "~9.2.0",
"expo-av": "~8.6.0", "expo-av": "~8.6.0",
"expo-image-picker": "~9.1.1",
"expo-secure-store": "~9.2.0", "expo-secure-store": "~9.2.0",
"expo-splash-screen": "~0.6.1", "expo-splash-screen": "~0.6.1",
"expo-status-bar": "~1.0.2", "expo-status-bar": "~1.0.2",
@ -48,12 +49,14 @@
"@babel/core": "~7.12.3", "@babel/core": "~7.12.3",
"@babel/plugin-proposal-optional-chaining": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@types/lodash": "^4.14.164", "@types/lodash": "^4.14.164",
"@types/node": "^14.14.7",
"@types/react": "~16.9.35", "@types/react": "~16.9.35",
"@types/react-dom": "^16.9.9", "@types/react-dom": "^16.9.9",
"@types/react-native": "~0.63.2", "@types/react-native": "~0.63.2",
"@types/react-native-htmlview": "^0.12.2", "@types/react-native-htmlview": "^0.12.2",
"@types/react-navigation": "^3.4.0", "@types/react-navigation": "^3.4.0",
"@types/react-redux": "^7.1.11", "@types/react-redux": "^7.1.11",
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
"babel-plugin-module-resolver": "^4.0.0", "babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-typescript-to-proptypes": "^1.4.1", "babel-plugin-typescript-to-proptypes": "^1.4.1",
"typescript": "~3.9.2" "typescript": "~3.9.2"

View File

@ -18,28 +18,29 @@ const client = async ({
query?: { query?: {
[key: string]: string | number | boolean [key: string]: string | number | boolean
} }
body?: object body?: FormData
}): Promise<any> => { }): Promise<any> => {
const state: RootState['instanceInfo'] = store.getState().instanceInfo const state: RootState['instanceInfo'] = store.getState().instanceInfo
let response let response
try { // try {
response = await ky(endpoint, { response = await ky(endpoint, {
method: method, method: method,
prefixUrl: `https://${state[instance]}/api/${version}`, prefixUrl: `https://${state[instance]}/api/${version}`,
searchParams: query, searchParams: query,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...headers, ...headers,
...(instance === 'local' && { ...(instance === 'local' && {
Authorization: `Bearer ${state.localToken}` Authorization: `Bearer ${state.localToken}`
}) })
}, },
...(body && { json: body }) ...(body && { body: body }),
}) throwHttpErrors: false
} catch (error) { })
return Promise.reject('ky error: ' + error) // } catch (error) {
} // return Promise.reject('ky error: ' + error.json())
// }
if (response.ok) { if (response.ok) {
return Promise.resolve({ return Promise.resolve({
@ -47,8 +48,9 @@ const client = async ({
body: await response.json() body: await response.json()
}) })
} else { } else {
console.error(response.status + ': ' + response.statusText) const errorResponse = await response.json()
return Promise.reject({ body: response.statusText }) console.error(response.status + ': ' + errorResponse.error)
return Promise.reject({ body: errorResponse.error })
} }
} }

View File

@ -31,6 +31,27 @@ export type PostState = {
} }
| undefined | undefined
emojis: mastodon.Emoji[] | undefined emojis: mastodon.Emoji[] | undefined
poll: {
active: boolean
total: number
options: {
'1': string
'2': string
'3': string
'4': string
[key: string]: string
}
multiple: boolean
expire:
| '300'
| '1800'
| '3600'
| '21600'
| '86400'
| '259200'
| '604800'
| string
}
} }
export type PostAction = export type PostAction =
@ -54,6 +75,10 @@ export type PostAction =
type: 'emojis' type: 'emojis'
payload: PostState['emojis'] payload: PostState['emojis']
} }
| {
type: 'poll'
payload: PostState['poll']
}
const postInitialState: PostState = { const postInitialState: PostState = {
text: { text: {
@ -64,7 +89,19 @@ const postInitialState: PostState = {
selection: { start: 0, end: 0 }, selection: { start: 0, end: 0 },
overlay: null, overlay: null,
tag: undefined, tag: undefined,
emojis: undefined emojis: undefined,
poll: {
active: false,
total: 2,
options: {
'1': '',
'2': '',
'3': '',
'4': ''
},
multiple: false,
expire: '86400'
}
} }
const postReducer = (state: PostState, action: PostAction): PostState => { const postReducer = (state: PostState, action: PostAction): PostState => {
switch (action.type) { switch (action.type) {
@ -78,6 +115,8 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
return { ...state, tag: action.payload } return { ...state, tag: action.payload }
case 'emojis': case 'emojis':
return { ...state, emojis: action.payload } return { ...state, emojis: action.payload }
case 'poll':
return { ...state, poll: action.payload }
default: default:
throw new Error('Unexpected action') throw new Error('Unexpected action')
} }
@ -106,6 +145,72 @@ const PostToot: React.FC = () => {
const [postState, postDispatch] = useReducer(postReducer, postInitialState) const [postState, postDispatch] = useReducer(postReducer, postInitialState)
const tootPost = async () => {
if (postState.text.count < 0) {
Alert.alert('字数超限', '', [
{
text: '返回继续编辑'
}
])
} else {
const formData = new FormData()
formData.append('status', postState.text.raw)
if (postState.poll.active) {
Object.values(postState.poll.options)
.filter(e => e.length)
.forEach(e => {
console.log(e)
formData.append('poll[options][]', e)
})
formData.append('poll[expires_in]', postState.poll.expire)
formData.append('poll[multiple]', postState.poll.multiple.toString())
}
console.log(formData)
client({
method: 'post',
instance: 'local',
endpoint: 'statuses',
headers: {
'Idempotency-Key': Date.now().toString() + Math.random().toString()
},
body: formData
})
.then(
res => {
if (res.body.id) {
Alert.alert('发布成功', '', [
{
text: '好的',
onPress: () => navigation.goBack()
}
])
} else {
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
}
},
error => {
Alert.alert('发布失败', error.body, [
{
text: '返回重试'
}
])
}
)
.catch(() => {
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
})
}
}
return ( return (
<KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}> <KeyboardAvoidingView behavior='padding' style={{ flex: 1 }}>
<SafeAreaView <SafeAreaView
@ -134,42 +239,7 @@ const PostToot: React.FC = () => {
), ),
headerCenter: () => <></>, headerCenter: () => <></>,
headerRight: () => ( headerRight: () => (
<Pressable <Pressable onPress={async () => tootPost()}>
onPress={async () => {
if (postState.text.count < 0) {
Alert.alert('字数超限', '', [
{
text: '返回继续编辑'
}
])
} else {
const res = await client({
method: 'post',
instance: 'local',
endpoint: 'statuses',
headers: {
'Idempotency-Key':
Date.now().toString() + Math.random().toString()
},
query: { status: postState.text.raw }
})
if (res.body.id) {
Alert.alert('发布成功', '', [
{
text: '好的',
onPress: () => navigation.goBack()
}
])
} else {
Alert.alert('发布失败', '', [
{
text: '返回重试'
}
])
}
}
}}
>
<Text></Text> <Text></Text>
</Pressable> </Pressable>
) )
@ -184,5 +254,6 @@ const PostToot: React.FC = () => {
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }
;(PostMain as any).whyDidYouRender = true
export default PostToot export default PostToot

View File

@ -13,15 +13,16 @@ import {
TextInput, TextInput,
View View
} from 'react-native' } from 'react-native'
import { useQuery } from 'react-query'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import { debounce, differenceWith, isEqual } from 'lodash' import { debounce, differenceWith, isEqual } from 'lodash'
import Autolinker from 'src/modules/autolinker' import Autolinker from 'src/modules/autolinker'
import PostSuggestions from './PostSuggestions'
import PostEmojis from './PostEmojis' import PostEmojis from './PostEmojis'
import { useQuery } from 'react-query' import PostPoll from './PostPoll'
import PostSuggestions from './PostSuggestions'
import { emojisFetch } from 'src/stacks/common/emojisFetch' import { emojisFetch } from 'src/stacks/common/emojisFetch'
import { PostAction, PostState } from '../PostToot' import { PostAction, PostState } from 'src/stacks/Shared/PostToot'
export interface Props { export interface Props {
postState: PostState postState: PostState
@ -169,8 +170,13 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
> >
<Text>{postState.text.formatted}</Text> <Text>{postState.text.formatted}</Text>
</TextInput> </TextInput>
{postState.poll.active && (
<View style={styles.poll}>
<PostPoll postState={postState} postDispatch={postDispatch} />
</View>
)}
{postState.overlay === 'suggestions' ? ( {postState.overlay === 'suggestions' ? (
<View style={[styles.suggestions]}> <View style={styles.suggestions}>
<PostSuggestions <PostSuggestions
onChangeText={onChangeText} onChangeText={onChangeText}
postState={postState} postState={postState}
@ -181,7 +187,7 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
<></> <></>
)} )}
{postState.overlay === 'emojis' ? ( {postState.overlay === 'emojis' ? (
<View style={[styles.emojis]}> <View style={styles.emojis}>
<PostEmojis <PostEmojis
onChangeText={onChangeText} onChangeText={onChangeText}
postState={postState} postState={postState}
@ -193,7 +199,16 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
)} )}
<Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}> <Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}>
<Feather name='paperclip' size={24} /> <Feather name='paperclip' size={24} />
<Feather name='bar-chart-2' size={24} /> <Feather
name='bar-chart-2'
size={24}
onPress={() =>
postDispatch({
type: 'poll',
payload: { ...postState.poll, active: !postState.poll.active }
})
}
/>
<Feather name='eye-off' size={24} /> <Feather name='eye-off' size={24} />
<Feather <Feather
name='smile' name='smile'
@ -211,6 +226,10 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
) )
} }
// (PostSuggestions as any).whyDidYouRender = true,
// (PostPoll as any).whyDidYouRender = true,
// (PostEmojis as any).whyDidYouRender = true
const styles = StyleSheet.create({ const styles = StyleSheet.create({
main: { main: {
flex: 1 flex: 1
@ -218,6 +237,10 @@ const styles = StyleSheet.create({
textInput: { textInput: {
backgroundColor: 'gray' backgroundColor: 'gray'
}, },
poll: {
flex: 1,
height: 100
},
suggestions: { suggestions: {
flex: 1, flex: 1,
backgroundColor: 'lightyellow' backgroundColor: 'lightyellow'

View File

@ -0,0 +1,144 @@
import React, { Dispatch, useState } from 'react'
import {
ActionSheetIOS,
Pressable,
StyleSheet,
Text,
TextInput,
View
} from 'react-native'
import { Feather } from '@expo/vector-icons'
import { PostAction, PostState } from '../PostToot'
export interface Props {
postState: PostState
postDispatch: Dispatch<PostAction>
}
const PostPoll: React.FC<Props> = ({ postState, postDispatch }) => {
const expireMapping: { [key: string]: string } = {
'300': '5分钟',
'1800': '30分钟',
'3600': '1小时',
'21600': '6小时',
'86400': '1天',
'259200': '3天',
'604800': '7天'
}
return (
<View style={styles.base}>
{[...Array(postState.poll.total)].map((e, i) => (
<View key={i} style={styles.option}>
{postState.poll.multiple ? (
<Feather name='square' size={20} />
) : (
<Feather name='circle' size={20} />
)}
<TextInput
style={styles.textInput}
maxLength={50}
value={postState.poll.options[i]}
onChangeText={e =>
postDispatch({
type: 'poll',
payload: {
...postState.poll,
options: { ...postState.poll.options, [i]: e }
}
})
}
/>
</View>
))}
<View style={styles.totalControl}>
<Feather
name='minus'
size={20}
color={postState.poll.total > 2 ? 'black' : 'grey'}
onPress={() =>
postState.poll.total > 2 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, total: postState.poll.total - 1 }
})
}
/>
<Feather
name='plus'
size={20}
color={postState.poll.total < 4 ? 'black' : 'grey'}
onPress={() =>
postState.poll.total < 4 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, total: postState.poll.total + 1 }
})
}
/>
</View>
<Pressable
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['单选', '多选', '取消'],
cancelButtonIndex: 2
},
index =>
index < 2 &&
postDispatch({
type: 'poll',
payload: { ...postState.poll, multiple: index === 1 }
})
)
}
>
<Text>{postState.poll.multiple ? '多选' : '单选'}</Text>
</Pressable>
<Pressable
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: [...Object.values(expireMapping), '取消'],
cancelButtonIndex: 7
},
index =>
index < 7 &&
postDispatch({
type: 'poll',
payload: {
...postState.poll,
expire: Object.keys(expireMapping)[index]
}
})
)
}
>
<Text>{expireMapping[postState.poll.expire]}</Text>
</Pressable>
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
backgroundColor: 'green'
},
option: {
height: 30,
margin: 5,
flexDirection: 'row'
},
textInput: {
flex: 1,
backgroundColor: 'white'
},
totalControl: {
alignSelf: 'flex-end',
flexDirection: 'row'
}
})
export default PostPoll

View File

@ -1363,6 +1363,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.164.tgz#52348bcf909ac7b4c1bcbeda5c23135176e5dfa0" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.164.tgz#52348bcf909ac7b4c1bcbeda5c23135176e5dfa0"
integrity sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg== integrity sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg==
"@types/node@^14.14.7":
version "14.14.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d"
integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.3" version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
@ -1454,6 +1459,13 @@
invariant "^2.2.4" invariant "^2.2.4"
lodash "^4.5.0" lodash "^4.5.0"
"@welldone-software/why-did-you-render@^6.0.0-rc.1":
version "6.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@welldone-software/why-did-you-render/-/why-did-you-render-6.0.0-rc.1.tgz#b0e92edb2e34e7af695cca1822844f02018d9814"
integrity sha512-qQe5w89tYnYtwRqlhdF33ivWjsQlGXkan5lFzNwpAoMEUFIbDuwvFiBUAbE76Lfz63GabSaf1vyuCusgJ7Rtqg==
dependencies:
lodash "^4"
abort-controller@^3.0.0: abort-controller@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@ -2700,6 +2712,14 @@ expo-font@~8.3.0:
fbjs "1.0.0" fbjs "1.0.0"
fontfaceobserver "^2.1.0" fontfaceobserver "^2.1.0"
expo-image-picker@~9.1.1:
version "9.1.1"
resolved "https://registry.yarnpkg.com/expo-image-picker/-/expo-image-picker-9.1.1.tgz#42abe9deb595fa9f9d8ac0d2ba8aad2cd6f69581"
integrity sha512-Etz2OQhRflfx+xFbSdma8QLZsnV/yq0M/yqYlsi3/RLiWAQYM/D/VmRfDDPiG10gm+KX3Xb5iKplNjPrWeTuQg==
dependencies:
expo-permissions "~9.3.0"
uuid "7.0.2"
expo-keep-awake@~8.3.0: expo-keep-awake@~8.3.0:
version "8.3.0" version "8.3.0"
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-8.3.0.tgz#11bb8073dfe453259926855c81d9f35db03a79b9" resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-8.3.0.tgz#11bb8073dfe453259926855c81d9f35db03a79b9"
@ -3845,7 +3865,7 @@ lodash.throttle@^4.1.1:
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.0: lodash@^4, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.6.0:
version "4.17.20" version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@ -5980,6 +6000,11 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@7.0.2:
version "7.0.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6"
integrity sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw==
uuid@^3.3.2, uuid@^3.4.0: uuid@^3.3.2, uuid@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"