diff --git a/App.tsx b/App.tsx index cd44b8ae..c5dc9b95 100644 --- a/App.tsx +++ b/App.tsx @@ -13,7 +13,7 @@ setConsole({ if (__DEV__) { const whyDidYouRender = require('@welldone-software/why-did-you-render') - whyDidYouRender(React) + // whyDidYouRender(React) } const App: React.FC = () => ( diff --git a/src/api/client.ts b/src/api/client.ts index 8d149e8e..5da63d94 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -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() diff --git a/src/stacks/Shared/PostToot.tsx b/src/stacks/Shared/PostToot.tsx index 35d415a1..23890aba 100644 --- a/src/stacks/Shared/PostToot.tsx +++ b/src/stacks/Shared/PostToot.tsx @@ -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 = () => { ) } -;(PostMain as any).whyDidYouRender = true +// ;(PostMain as any).whyDidYouRender = true export default PostToot diff --git a/src/stacks/Shared/PostToot/PostAttachments.tsx b/src/stacks/Shared/PostToot/PostAttachments.tsx new file mode 100644 index 00000000..9948a319 --- /dev/null +++ b/src/stacks/Shared/PostToot/PostAttachments.tsx @@ -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 +} + +const PostAttachments: React.FC = ({ postState, postDispatch }) => { + return ( + + {postState.attachments.map((attachment, index) => ( + + + + + postDispatch({ type: 'attachments/remove', payload: attachment }) + } + /> + + ))} + + ) +} + +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 diff --git a/src/stacks/Shared/PostToot/PostMain.tsx b/src/stacks/Shared/PostToot/PostMain.tsx index 2d49dd18..3171f42a 100644 --- a/src/stacks/Shared/PostToot/PostMain.tsx +++ b/src/stacks/Shared/PostToot/PostMain.tsx @@ -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 = ({ 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 = ({ postState, postDispatch }) => { }) }, []) + const getVisibilityIcon = () => { + switch (postState.visibility) { + case 'public': + return 'globe' + case 'unlisted': + return 'unlock' + case 'private': + return 'lock' + case 'direct': + return 'mail' + } + } + return ( = ({ postState, postDispatch }) => { > {postState.text.formatted} + {postState.attachments.length > 0 && ( + + + + )} {postState.poll.active && ( @@ -198,7 +229,13 @@ const PostMain: React.FC = ({ postState, postDispatch }) => { <> )} Keyboard.dismiss()}> - + + await addAttachments({ postState, postDispatch }) + } + /> = ({ postState, postDispatch }) => { }) } /> - + + 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 + } + } + ) + } + /> { + 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 +}) => { + 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 diff --git a/src/stacks/common/store.ts b/src/stacks/common/store.ts index 4dfafff8..01d6de9d 100644 --- a/src/stacks/common/store.ts +++ b/src/stacks/common/store.ts @@ -8,6 +8,9 @@ const preloadedState = { local: 'social.xmflsct.com', localToken: 'qjzJ0IjvZ1apsn0_wBkGcdjKgX7Dao9KEPhGwggPwAo', localAccountId: '1', + localAccount: { + locked: false + }, remote: 'mastodon.social' } }