Basic posting done

Except hidden text, layout and sensitive attachments
This commit is contained in:
Zhiyuan Zheng 2020-11-19 22:45:26 +01:00
parent c0d7f379b3
commit aecb5c5b3d
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
7 changed files with 271 additions and 8 deletions

View File

@ -13,7 +13,7 @@ setConsole({
if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render')
whyDidYouRender(React)
// whyDidYouRender(React)
}
const App: React.FC = () => (

View File

@ -41,8 +41,9 @@ const client = async ({
// } catch (error) {
// return Promise.reject('ky error: ' + error.json())
// }
console.log('upload done')
if (response.ok) {
console.log('returning ok')
return Promise.resolve({
headers: response.headers,
body: await response.json()

View File

@ -10,6 +10,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useNavigation } from '@react-navigation/native'
import store from 'src/stacks/common/store'
import PostMain from './PostToot/PostMain'
import client from 'src/api/client'
@ -52,6 +53,13 @@ export type PostState = {
| '604800'
| string
}
attachments: {
id: string
url: string
preview_url: string
description: string
}[]
visibility: 'public' | 'unlisted' | 'private' | 'direct'
}
export type PostAction =
@ -79,6 +87,25 @@ export type PostAction =
type: 'poll'
payload: PostState['poll']
}
| {
type: 'attachments/add'
payload: {
id: string
url: string
preview_url: string
description: string
}
}
| {
type: 'attachments/remove'
payload: {
id: string
}
}
| {
type: 'visibility'
payload: PostState['visibility']
}
const postInitialState: PostState = {
text: {
@ -101,7 +128,11 @@ const postInitialState: PostState = {
},
multiple: false,
expire: '86400'
}
},
attachments: [],
visibility: store.getState().instanceInfo.localAccount.locked
? 'private'
: 'public'
}
const postReducer = (state: PostState, action: PostAction): PostState => {
switch (action.type) {
@ -117,6 +148,15 @@ const postReducer = (state: PostState, action: PostAction): PostState => {
return { ...state, emojis: action.payload }
case 'poll':
return { ...state, poll: action.payload }
case 'attachments/add':
return { ...state, attachments: state.attachments.concat(action.payload) }
case 'attachments/remove':
return {
...state,
attachments: state.attachments.filter(a => a.id !== action.payload.id)
}
case 'visibility':
return { ...state, visibility: action.payload }
default:
throw new Error('Unexpected action')
}
@ -159,13 +199,11 @@ const PostToot: React.FC = () => {
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',
@ -254,6 +292,6 @@ const PostToot: React.FC = () => {
</KeyboardAvoidingView>
)
}
;(PostMain as any).whyDidYouRender = true
// ;(PostMain as any).whyDidYouRender = true
export default PostToot

View File

@ -0,0 +1,66 @@
import React, { Dispatch } from 'react'
import { Image, StyleSheet, View } from 'react-native'
import { Feather } from '@expo/vector-icons'
import { PostAction, PostState } from '../PostToot'
export interface Props {
postState: PostState
postDispatch: Dispatch<PostAction>
}
const PostAttachments: React.FC<Props> = ({ postState, postDispatch }) => {
return (
<View style={styles.base}>
{postState.attachments.map((attachment, index) => (
<View key={index} style={styles.imageContainer}>
<Image
style={styles.image}
source={{ uri: attachment.preview_url }}
/>
<Feather
name='edit'
size={24}
color='white'
style={styles.buttonEdit}
/>
<Feather
name='trash-2'
size={24}
color='white'
style={styles.buttonRemove}
onPress={() =>
postDispatch({ type: 'attachments/remove', payload: attachment })
}
/>
</View>
))}
</View>
)
}
const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row',
backgroundColor: 'lightgreen'
},
imageContainer: {
flexBasis: 100
},
image: {
flex: 1
},
buttonEdit: {
position: 'absolute',
top: 0,
left: 0
},
buttonRemove: {
position: 'absolute',
top: 0,
right: 0
}
})
export default PostAttachments

View File

@ -6,6 +6,7 @@ import React, {
useState
} from 'react'
import {
ActionSheetIOS,
Keyboard,
Pressable,
StyleSheet,
@ -15,6 +16,7 @@ import {
} from 'react-native'
import { useQuery } from 'react-query'
import { Feather } from '@expo/vector-icons'
import * as ImagePicker from 'expo-image-picker'
import { debounce, differenceWith, isEqual } from 'lodash'
import Autolinker from 'src/modules/autolinker'
@ -23,6 +25,8 @@ import PostPoll from './PostPoll'
import PostSuggestions from './PostSuggestions'
import { emojisFetch } from 'src/stacks/common/emojisFetch'
import { PostAction, PostState } from 'src/stacks/Shared/PostToot'
import addAttachments from './addAttachments'
import PostAttachments from './PostAttachments'
export interface Props {
postState: PostState
@ -30,6 +34,15 @@ export interface Props {
}
const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
useEffect(() => {
;(async () => {
const { status } = await ImagePicker.requestCameraRollPermissionsAsync()
if (status !== 'granted') {
alert('Sorry, we need camera roll permissions to make this work!')
}
})()
}, [])
const [editorMinHeight, setEditorMinHeight] = useState(0)
const { data: emojisData } = useQuery(['Emojis'], emojisFetch)
@ -139,6 +152,19 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
})
}, [])
const getVisibilityIcon = () => {
switch (postState.visibility) {
case 'public':
return 'globe'
case 'unlisted':
return 'unlock'
case 'private':
return 'lock'
case 'direct':
return 'mail'
}
}
return (
<View style={{ flex: 1 }}>
<TextInput
@ -170,6 +196,11 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
>
<Text>{postState.text.formatted}</Text>
</TextInput>
{postState.attachments.length > 0 && (
<View style={styles.attachments}>
<PostAttachments postState={postState} postDispatch={postDispatch} />
</View>
)}
{postState.poll.active && (
<View style={styles.poll}>
<PostPoll postState={postState} postDispatch={postDispatch} />
@ -198,7 +229,13 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
<></>
)}
<Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}>
<Feather name='paperclip' size={24} />
<Feather
name='paperclip'
size={24}
onPress={async () =>
await addAttachments({ postState, postDispatch })
}
/>
<Feather
name='bar-chart-2'
size={24}
@ -209,7 +246,34 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
})
}
/>
<Feather name='eye-off' size={24} />
<Feather
name={getVisibilityIcon()}
size={24}
onPress={() =>
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['公开', '不公开', '仅关注着', '私信', '取消'],
cancelButtonIndex: 4
},
buttonIndex => {
switch (buttonIndex) {
case 0:
postDispatch({ type: 'visibility', payload: 'public' })
break
case 1:
postDispatch({ type: 'visibility', payload: 'unlisted' })
break
case 2:
postDispatch({ type: 'visibility', payload: 'private' })
break
case 3:
postDispatch({ type: 'visibility', payload: 'direct' })
break
}
}
)
}
/>
<Feather
name='smile'
size={24}
@ -237,6 +301,10 @@ const styles = StyleSheet.create({
textInput: {
backgroundColor: 'gray'
},
attachments: {
flex: 1,
height: 100
},
poll: {
flex: 1,
height: 100

View File

@ -0,0 +1,87 @@
import { Dispatch } from 'react'
import { ActionSheetIOS, Alert } from 'react-native'
import * as ImagePicker from 'expo-image-picker'
import { ImageInfo } from 'expo-image-picker/build/ImagePicker.types'
import { PostAction, PostState } from '../PostToot'
import client from 'src/api/client'
const uploadAttachment = async (uri: ImageInfo['uri']) => {
const filename = uri.split('/').pop()
const match = /\.(\w+)$/.exec(filename!)
const type = match ? `image/${match[1]}` : `image`
const formData = new FormData()
formData.append('file', { uri: uri, name: filename, type: type })
return client({
method: 'post',
instance: 'local',
endpoint: 'media',
body: formData
})
.then(res => {
if (res.body.id && res.body.type !== 'unknown') {
console.log('url: ' + res.body.preview_url)
return Promise.resolve({
id: res.body.id,
url: res.body.url,
preview_url: res.body.preview_url,
description: res.body.description
})
} else {
Alert.alert('上传失败', '', [
{
text: '返回重试'
}
])
return Promise.reject()
}
})
.catch(() => {
Alert.alert('上传失败', '', [
{
text: '返回重试'
}
])
return Promise.reject()
})
}
const addAttachments = async ({
postState,
postDispatch
}: {
postState: PostState
postDispatch: Dispatch<PostAction>
}) => {
ActionSheetIOS.showActionSheetWithOptions(
{
options: ['从相册选取', '现照', '取消'],
cancelButtonIndex: 2
},
async buttonIndex => {
if (buttonIndex === 0) {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
exif: false
})
if (!result.cancelled) {
const response = await uploadAttachment(result.uri)
if (response.id) {
postDispatch({
type: 'attachments/add',
payload: response
})
}
}
} else if (buttonIndex === 1) {
// setResult(Math.floor(Math.random() * 100) + 1)
}
}
)
}
export default addAttachments

View File

@ -8,6 +8,9 @@ const preloadedState = {
local: 'social.xmflsct.com',
localToken: 'qjzJ0IjvZ1apsn0_wBkGcdjKgX7Dao9KEPhGwggPwAo',
localAccountId: '1',
localAccount: {
locked: false
},
remote: 'mastodon.social'
}
}