mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Basic posting done
Except hidden text, layout and sensitive attachments
This commit is contained in:
		
							
								
								
									
										2
									
								
								App.tsx
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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 = () => ( | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										66
									
								
								src/stacks/Shared/PostToot/PostAttachments.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/stacks/Shared/PostToot/PostAttachments.tsx
									
									
									
									
									
										Normal 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 | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										87
									
								
								src/stacks/Shared/PostToot/addAttachments.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/stacks/Shared/PostToot/addAttachments.ts
									
									
									
									
									
										Normal 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 | ||||
| @@ -8,6 +8,9 @@ const preloadedState = { | ||||
|     local: 'social.xmflsct.com', | ||||
|     localToken: 'qjzJ0IjvZ1apsn0_wBkGcdjKgX7Dao9KEPhGwggPwAo', | ||||
|     localAccountId: '1', | ||||
|     localAccount: { | ||||
|       locked: false | ||||
|     }, | ||||
|     remote: 'mastodon.social' | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user