mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Emoji done
This commit is contained in:
		| @@ -12,6 +12,7 @@ import { | ||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import Button from './Button' | ||||
|  | ||||
| export interface Props { | ||||
|   children: React.ReactNode | ||||
| @@ -85,12 +86,10 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => { | ||||
|             style={[styles.handle, { backgroundColor: theme.background }]} | ||||
|           /> | ||||
|           {children} | ||||
|           <Pressable | ||||
|           <Button | ||||
|             onPress={() => closeModal.start(() => handleDismiss())} | ||||
|             style={[styles.cancel, { borderColor: theme.primary }]} | ||||
|           > | ||||
|             <Text style={[styles.text, { color: theme.primary }]}>取消</Text> | ||||
|           </Pressable> | ||||
|             text='取消' | ||||
|           /> | ||||
|         </Animated.View> | ||||
|       </View> | ||||
|     </Modal> | ||||
| @@ -111,17 +110,6 @@ const styles = StyleSheet.create({ | ||||
|     height: StyleConstants.Spacing.S / 2, | ||||
|     borderRadius: 100, | ||||
|     top: -StyleConstants.Spacing.M * 2 | ||||
|   }, | ||||
|   cancel: { | ||||
|     padding: StyleConstants.Spacing.S, | ||||
|     marginLeft: StyleConstants.Spacing.L, | ||||
|     marginRight: StyleConstants.Spacing.L, | ||||
|     borderWidth: 1, | ||||
|     borderRadius: 100 | ||||
|   }, | ||||
|   text: { | ||||
|     fontSize: StyleConstants.Font.Size.L, | ||||
|     textAlign: 'center' | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
| @@ -1,38 +1,74 @@ | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import React from 'react' | ||||
| import { Pressable, StyleSheet, Text } from 'react-native' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
|  | ||||
| export interface Props { | ||||
| type PropsBase = { | ||||
|   onPress: () => void | ||||
|   text: string | ||||
|   fontSize?: 'S' | 'M' | 'L' | ||||
|   disabled?: boolean | ||||
|   buttonSize?: 'S' | 'M' | ||||
| } | ||||
|  | ||||
| const Button: React.FC<Props> = ({ onPress, text, fontSize = 'M' }) => { | ||||
| export interface PropsText extends PropsBase { | ||||
|   text: string | ||||
|   icon?: string | ||||
|   size?: 'S' | 'M' | 'L' | ||||
| } | ||||
|  | ||||
| export interface PropsIcon extends PropsBase { | ||||
|   text?: string | ||||
|   icon: string | ||||
|   size?: 'S' | 'M' | 'L' | ||||
| } | ||||
|  | ||||
| const Button: React.FC<PropsText | PropsIcon> = ({ | ||||
|   onPress, | ||||
|   disabled = false, | ||||
|   buttonSize = 'M', | ||||
|   text, | ||||
|   icon, | ||||
|   size = 'M' | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
|       onPress={onPress} | ||||
|       style={[styles.button, { borderColor: theme.primary }]} | ||||
|       {...(!disabled && { onPress })} | ||||
|       style={[ | ||||
|         styles.button, | ||||
|         { | ||||
|           borderColor: disabled ? theme.secondary : theme.primary, | ||||
|           paddingTop: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'], | ||||
|           paddingBottom: StyleConstants.Spacing[buttonSize === 'M' ? 'S' : 'XS'] | ||||
|         } | ||||
|       ]} | ||||
|     > | ||||
|       {icon ? ( | ||||
|         <Feather | ||||
|           name={icon} | ||||
|           size={StyleConstants.Font.Size[size]} | ||||
|           color={disabled ? theme.secondary : theme.primary} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <Text | ||||
|           style={[ | ||||
|             styles.text, | ||||
|           { color: theme.primary, fontSize: StyleConstants.Font.Size[fontSize] } | ||||
|             { | ||||
|               color: disabled ? theme.secondary : theme.primary, | ||||
|               fontSize: StyleConstants.Font.Size[size] | ||||
|             } | ||||
|           ]} | ||||
|         > | ||||
|           {text} | ||||
|         </Text> | ||||
|       )} | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   button: { | ||||
|     paddingTop: StyleConstants.Spacing.S, | ||||
|     paddingBottom: StyleConstants.Spacing.S, | ||||
|     paddingLeft: StyleConstants.Spacing.M, | ||||
|     paddingRight: StyleConstants.Spacing.M, | ||||
|     borderWidth: 1, | ||||
|   | ||||
| @@ -58,7 +58,7 @@ const styles = StyleSheet.create({ | ||||
|     flexDirection: 'row', | ||||
|     height: StyleConstants.Avatar.L, | ||||
|     marginTop: StyleConstants.Spacing.M, | ||||
|     borderWidth: 0.5, | ||||
|     borderWidth: StyleSheet.hairlineWidth, | ||||
|     borderRadius: 6 | ||||
|   }, | ||||
|   left: { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import React, { useCallback, useEffect, useState } from 'react' | ||||
| import { Button, StyleSheet, Text, TextInput, View } from 'react-native' | ||||
| import { StyleSheet, Text, TextInput, View } from 'react-native' | ||||
| import { useQuery } from 'react-query' | ||||
| import { debounce } from 'lodash' | ||||
|  | ||||
| @@ -13,6 +13,7 @@ import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
|  | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import Button from 'src/components/Button' | ||||
|  | ||||
| const Login: React.FC = () => { | ||||
|   const { t } = useTranslation('meRoot') | ||||
| @@ -145,9 +146,9 @@ const Login: React.FC = () => { | ||||
|         returnKeyType='go' | ||||
|       /> | ||||
|       <Button | ||||
|         title={t('content.login.button')} | ||||
|         disabled={!data?.uri} | ||||
|         onPress={async () => await createApplication()} | ||||
|         text={t('content.login.button')} | ||||
|         disabled={!data?.uri} | ||||
|       /> | ||||
|       {isSuccess && data && data.uri && ( | ||||
|         <View> | ||||
|   | ||||
| @@ -208,13 +208,13 @@ const styles = StyleSheet.create({ | ||||
|   }, | ||||
|   account_types: { marginLeft: StyleConstants.Spacing.S }, | ||||
|   fields: { | ||||
|     borderTopWidth: 0.5, | ||||
|     borderTopWidth: StyleSheet.hairlineWidth, | ||||
|     marginBottom: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   field: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     borderBottomWidth: 0.5, | ||||
|     borderBottomWidth: StyleSheet.hairlineWidth, | ||||
|     paddingTop: StyleConstants.Spacing.S, | ||||
|     paddingBottom: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
|  | ||||
| import { store } from 'src/store' | ||||
| import PostMain from './Compose/PostMain' | ||||
| import ComposeRoot from './Compose/Root' | ||||
| import client from 'src/api/client' | ||||
| import { getLocalAccountPreferences } from 'src/utils/slices/instancesSlice' | ||||
| import { HeaderLeft, HeaderRight } from 'src/components/Header' | ||||
| @@ -105,7 +105,7 @@ export type PostAction = | ||||
|  | ||||
| const postInitialState: PostState = { | ||||
|   text: { | ||||
|     count: 0, | ||||
|     count: 500, | ||||
|     raw: '', | ||||
|     formatted: undefined | ||||
|   }, | ||||
| @@ -285,13 +285,15 @@ const Compose: React.FC = () => { | ||||
|                 <HeaderRight | ||||
|                   onPress={async () => tootPost()} | ||||
|                   text='发嘟嘟' | ||||
|                   disabled={postState.text.raw.length < 1} | ||||
|                   disabled={ | ||||
|                     postState.text.raw.length < 1 || postState.text.count < 0 | ||||
|                   } | ||||
|                 /> | ||||
|               ) | ||||
|             }} | ||||
|           > | ||||
|             {() => ( | ||||
|               <PostMain postState={postState} postDispatch={postDispatch} /> | ||||
|               <ComposeRoot postState={postState} postDispatch={postDispatch} /> | ||||
|             )} | ||||
|           </Stack.Screen> | ||||
|         </Stack.Navigator> | ||||
|   | ||||
							
								
								
									
										147
									
								
								src/screens/Shared/Compose/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/screens/Shared/Compose/Actions.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import React, { Dispatch } from 'react' | ||||
| import { | ||||
|   ActionSheetIOS, | ||||
|   Keyboard, | ||||
|   Pressable, | ||||
|   StyleSheet, | ||||
|   Text, | ||||
|   TextInput | ||||
| } from 'react-native' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
| import { PostAction, PostState } from '../Compose' | ||||
| import addAttachments from './addAttachments' | ||||
|  | ||||
| export interface Props { | ||||
|   textInputRef: React.RefObject<TextInput> | ||||
|   postState: PostState | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const ComposeActions: React.FC<Props> = ({ | ||||
|   textInputRef, | ||||
|   postState, | ||||
|   postDispatch | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const getVisibilityIcon = () => { | ||||
|     switch (postState.visibility) { | ||||
|       case 'public': | ||||
|         return 'globe' | ||||
|       case 'unlisted': | ||||
|         return 'unlock' | ||||
|       case 'private': | ||||
|         return 'lock' | ||||
|       case 'direct': | ||||
|         return 'mail' | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
|       style={[ | ||||
|         styles.additions, | ||||
|         { backgroundColor: theme.background, borderTopColor: theme.border } | ||||
|       ]} | ||||
|       onPress={() => Keyboard.dismiss()} | ||||
|     > | ||||
|       <Feather | ||||
|         name='aperture' | ||||
|         size={24} | ||||
|         color={postState.poll.active ? theme.secondary : theme.primary} | ||||
|         onPress={async () => | ||||
|           !postState.poll.active && | ||||
|           (await addAttachments({ postState, postDispatch })) | ||||
|         } | ||||
|       /> | ||||
|       <Feather | ||||
|         name='bar-chart-2' | ||||
|         size={24} | ||||
|         color={ | ||||
|           postState.attachments.length > 0 ? theme.secondary : theme.primary | ||||
|         } | ||||
|         onPress={() => { | ||||
|           if (postState.attachments.length === 0) { | ||||
|             postDispatch({ | ||||
|               type: 'poll', | ||||
|               payload: { ...postState.poll, active: !postState.poll.active } | ||||
|             }) | ||||
|           } | ||||
|           if (postState.poll.active) { | ||||
|             textInputRef.current?.focus() | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       <Feather | ||||
|         name={getVisibilityIcon()} | ||||
|         size={24} | ||||
|         color={theme.primary} | ||||
|         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} | ||||
|         color={postState.emojis?.length ? theme.primary : theme.secondary} | ||||
|         onPress={() => { | ||||
|           if (postState.emojis?.length && postState.overlay === null) { | ||||
|             Keyboard.dismiss() | ||||
|             postDispatch({ type: 'overlay', payload: 'emojis' }) | ||||
|           } | ||||
|           if (postState.overlay === 'emojis') { | ||||
|             postDispatch({ type: 'overlay', payload: null }) | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       <Text | ||||
|         style={[ | ||||
|           styles.count, | ||||
|           { color: postState.text.count < 0 ? theme.error : theme.primary } | ||||
|         ]} | ||||
|       > | ||||
|         {postState.text.count} | ||||
|       </Text> | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   additions: { | ||||
|     height: 45, | ||||
|     borderTopWidth: StyleSheet.hairlineWidth, | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'space-around', | ||||
|     alignItems: 'center' | ||||
|   }, | ||||
|   count: { | ||||
|     textAlign: 'center', | ||||
|     fontSize: StyleConstants.Font.Size.M, | ||||
|     fontWeight: StyleConstants.Font.Weight.Bold | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ComposeActions | ||||
| @@ -9,7 +9,7 @@ export interface Props { | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
| 
 | ||||
| const PostAttachments: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
| const ComposeAttachments: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       {postState.attachments.map((attachment, index) => ( | ||||
| @@ -63,4 +63,4 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| export default PostAttachments | ||||
| export default ComposeAttachments | ||||
							
								
								
									
										108
									
								
								src/screens/Shared/Compose/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/screens/Shared/Compose/Emojis.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| import { forEach, groupBy, sortBy } from 'lodash' | ||||
| import React, { Dispatch } from 'react' | ||||
| import { | ||||
|   Image, | ||||
|   Pressable, | ||||
|   SectionList, | ||||
|   StyleSheet, | ||||
|   Text, | ||||
|   TextInput, | ||||
|   View | ||||
| } from 'react-native' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
|  | ||||
| import { PostAction, PostState } from '../Compose' | ||||
| import updateText from './updateText' | ||||
|  | ||||
| export interface Props { | ||||
|   textInputRef: React.RefObject<TextInput> | ||||
|   onChangeText: any | ||||
|   postState: PostState | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const ComposeEmojis: React.FC<Props> = ({ | ||||
|   textInputRef, | ||||
|   onChangeText, | ||||
|   postState, | ||||
|   postDispatch | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = [] | ||||
|   forEach( | ||||
|     groupBy(sortBy(postState.emojis, ['category', 'shortcode']), 'category'), | ||||
|     (value, key) => sortedEmojis.push({ title: key, data: value }) | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <SectionList | ||||
|         horizontal | ||||
|         sections={sortedEmojis} | ||||
|         keyExtractor={item => item.shortcode} | ||||
|         renderSectionHeader={({ section: { title } }) => ( | ||||
|           <Text style={[styles.group, { color: theme.secondary }]}> | ||||
|             {title} | ||||
|           </Text> | ||||
|         )} | ||||
|         renderItem={({ section, index }) => { | ||||
|           if (index === 0) { | ||||
|             return ( | ||||
|               <View key={section.title} style={styles.emojis}> | ||||
|                 {section.data.map(emoji => ( | ||||
|                   <Pressable | ||||
|                     key={emoji.shortcode} | ||||
|                     onPress={() => { | ||||
|                       updateText({ | ||||
|                         onChangeText, | ||||
|                         postState, | ||||
|                         newText: `:${emoji.shortcode}:` | ||||
|                       }) | ||||
|                       textInputRef.current?.focus() | ||||
|                       postDispatch({ type: 'overlay', payload: null }) | ||||
|                     }} | ||||
|                   > | ||||
|                     <Image source={{ uri: emoji.url }} style={styles.emoji} /> | ||||
|                   </Pressable> | ||||
|                 ))} | ||||
|               </View> | ||||
|             ) | ||||
|           } else { | ||||
|             return null | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   base: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     flexWrap: 'wrap', | ||||
|     justifyContent: 'space-around', | ||||
|     height: 260 | ||||
|   }, | ||||
|   group: { | ||||
|     position: 'absolute', | ||||
|     left: StyleConstants.Spacing.L, | ||||
|     fontSize: StyleConstants.Font.Size.S | ||||
|   }, | ||||
|   emojis: { | ||||
|     flex: 1, | ||||
|     flexWrap: 'wrap', | ||||
|     marginTop: StyleConstants.Spacing.M, | ||||
|     marginLeft: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   emoji: { | ||||
|     width: 32, | ||||
|     height: 32, | ||||
|     padding: StyleConstants.Spacing.S, | ||||
|     margin: StyleConstants.Spacing.S | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ComposeEmojis | ||||
							
								
								
									
										186
									
								
								src/screens/Shared/Compose/Poll.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/screens/Shared/Compose/Poll.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,186 @@ | ||||
| import React, { Dispatch, useEffect, useState } from 'react' | ||||
| import { | ||||
|   ActionSheetIOS, | ||||
|   Pressable, | ||||
|   StyleSheet, | ||||
|   Text, | ||||
|   TextInput, | ||||
|   View | ||||
| } from 'react-native' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
|  | ||||
| import { PostAction, PostState } from '../Compose' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import Button from 'src/components/Button' | ||||
| import { MenuContainer, MenuRow } from 'src/components/Menu' | ||||
|  | ||||
| export interface Props { | ||||
|   postState: PostState | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const ComposePoll: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const expireMapping: { [key: string]: string } = { | ||||
|     '300': '5分钟', | ||||
|     '1800': '30分钟', | ||||
|     '3600': '1小时', | ||||
|     '21600': '6小时', | ||||
|     '86400': '1天', | ||||
|     '259200': '3天', | ||||
|     '604800': '7天' | ||||
|   } | ||||
|  | ||||
|   const [firstRender, setFirstRender] = useState(true) | ||||
|   useEffect(() => { | ||||
|     setFirstRender(false) | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <View style={[styles.base, { borderColor: theme.border }]}> | ||||
|       <View style={styles.options}> | ||||
|         {[...Array(postState.poll.total)].map((e, i) => ( | ||||
|           <View key={i} style={styles.option}> | ||||
|             <Feather | ||||
|               name={postState.poll.multiple ? 'square' : 'circle'} | ||||
|               size={StyleConstants.Font.Size.L} | ||||
|               color={theme.secondary} | ||||
|             /> | ||||
|             <TextInput | ||||
|               {...(i === 0 && firstRender && { autoFocus: true })} | ||||
|               style={[ | ||||
|                 styles.textInput, | ||||
|                 { borderColor: theme.border, color: theme.primary } | ||||
|               ]} | ||||
|               placeholder={`选项 ${i}`} | ||||
|               placeholderTextColor={theme.secondary} | ||||
|               maxLength={50} | ||||
|               value={postState.poll.options[i]} | ||||
|               onChangeText={e => | ||||
|                 postDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { | ||||
|                     ...postState.poll, | ||||
|                     options: { ...postState.poll.options, [i]: e } | ||||
|                   } | ||||
|                 }) | ||||
|               } | ||||
|             /> | ||||
|           </View> | ||||
|         ))} | ||||
|       </View> | ||||
|       <View style={styles.controlAmount}> | ||||
|         <View style={styles.firstButton}> | ||||
|           <Button | ||||
|             onPress={() => | ||||
|               postState.poll.total > 2 && | ||||
|               postDispatch({ | ||||
|                 type: 'poll', | ||||
|                 payload: { ...postState.poll, total: postState.poll.total - 1 } | ||||
|               }) | ||||
|             } | ||||
|             icon='minus' | ||||
|             disabled={!(postState.poll.total > 2)} | ||||
|             buttonSize='S' | ||||
|           /> | ||||
|         </View> | ||||
|         <Button | ||||
|           onPress={() => | ||||
|             postState.poll.total < 4 && | ||||
|             postDispatch({ | ||||
|               type: 'poll', | ||||
|               payload: { ...postState.poll, total: postState.poll.total + 1 } | ||||
|             }) | ||||
|           } | ||||
|           icon='plus' | ||||
|           disabled={!(postState.poll.total < 4)} | ||||
|           buttonSize='S' | ||||
|         /> | ||||
|       </View> | ||||
|       <MenuContainer> | ||||
|         <MenuRow | ||||
|           title='可选项' | ||||
|           content={postState.poll.multiple ? '多选' : '单选'} | ||||
|           onPress={() => | ||||
|             ActionSheetIOS.showActionSheetWithOptions( | ||||
|               { | ||||
|                 options: ['单选', '多选', '取消'], | ||||
|                 cancelButtonIndex: 2 | ||||
|               }, | ||||
|               index => | ||||
|                 index < 2 && | ||||
|                 postDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { ...postState.poll, multiple: index === 1 } | ||||
|                 }) | ||||
|             ) | ||||
|           } | ||||
|           iconBack='chevron-right' | ||||
|         /> | ||||
|         <MenuRow | ||||
|           title='有效期' | ||||
|           content={expireMapping[postState.poll.expire]} | ||||
|           onPress={() => | ||||
|             ActionSheetIOS.showActionSheetWithOptions( | ||||
|               { | ||||
|                 options: [...Object.values(expireMapping), '取消'], | ||||
|                 cancelButtonIndex: 7 | ||||
|               }, | ||||
|               index => | ||||
|                 index < 7 && | ||||
|                 postDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { | ||||
|                     ...postState.poll, | ||||
|                     expire: Object.keys(expireMapping)[index] | ||||
|                   } | ||||
|                 }) | ||||
|             ) | ||||
|           } | ||||
|           iconBack='chevron-right' | ||||
|         /> | ||||
|       </MenuContainer> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   base: { | ||||
|     flex: 1, | ||||
|     borderWidth: StyleSheet.hairlineWidth, | ||||
|     borderRadius: 6 | ||||
|   }, | ||||
|   options: { | ||||
|     marginTop: StyleConstants.Spacing.M, | ||||
|     marginBottom: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   option: { | ||||
|     marginLeft: StyleConstants.Spacing.M, | ||||
|     marginRight: StyleConstants.Spacing.M, | ||||
|     marginBottom: StyleConstants.Spacing.S, | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center' | ||||
|   }, | ||||
|   textInput: { | ||||
|     flex: 1, | ||||
|     padding: StyleConstants.Spacing.S, | ||||
|     borderWidth: StyleSheet.hairlineWidth, | ||||
|     borderRadius: 6, | ||||
|     fontSize: StyleConstants.Font.Size.M, | ||||
|     marginLeft: StyleConstants.Spacing.S | ||||
|   }, | ||||
|   controlAmount: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'flex-end', | ||||
|     marginRight: StyleConstants.Spacing.M, | ||||
|     marginBottom: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   firstButton: { | ||||
|     marginRight: StyleConstants.Spacing.S | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ComposePoll | ||||
| @@ -1,44 +0,0 @@ | ||||
| import React, { Dispatch } from 'react' | ||||
| import { Image, Pressable } from 'react-native' | ||||
|  | ||||
| import { PostAction, PostState } from '../Compose' | ||||
| import updateText from './updateText' | ||||
|  | ||||
| export interface Props { | ||||
|   onChangeText: any | ||||
|   postState: PostState | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const PostEmojis: React.FC<Props> = ({ | ||||
|   onChangeText, | ||||
|   postState, | ||||
|   postDispatch | ||||
| }) => { | ||||
|   return ( | ||||
|     <> | ||||
|       {postState.emojis?.map((emoji, index) => ( | ||||
|         <Pressable | ||||
|           key={index} | ||||
|           onPress={() => { | ||||
|             updateText({ | ||||
|               onChangeText, | ||||
|               postState, | ||||
|               newText: `:${emoji.shortcode}:` | ||||
|             }) | ||||
|  | ||||
|             postDispatch({ type: 'overlay', payload: null }) | ||||
|           }} | ||||
|         > | ||||
|           <Image | ||||
|             key={index} | ||||
|             source={{ uri: emoji.url }} | ||||
|             style={{ width: 24, height: 24 }} | ||||
|           /> | ||||
|         </Pressable> | ||||
|       ))} | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default PostEmojis | ||||
| @@ -1,144 +0,0 @@ | ||||
| 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 '../Compose' | ||||
|  | ||||
| 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 | ||||
| @@ -3,6 +3,7 @@ import React, { | ||||
|   Dispatch, | ||||
|   useCallback, | ||||
|   useEffect, | ||||
|   useRef, | ||||
|   useState | ||||
| } from 'react' | ||||
| import { | ||||
| @@ -21,20 +22,24 @@ import * as ImagePicker from 'expo-image-picker' | ||||
| import { debounce, differenceWith, isEqual } from 'lodash' | ||||
| 
 | ||||
| import Autolinker from 'src/modules/autolinker' | ||||
| import PostEmojis from './PostEmojis' | ||||
| import PostPoll from './PostPoll' | ||||
| import PostSuggestions from './PostSuggestions' | ||||
| import ComposeEmojis from './Emojis' | ||||
| import ComposePoll from './Poll' | ||||
| import ComposeSuggestions from './Suggestions' | ||||
| import { emojisFetch } from 'src/utils/fetches/emojisFetch' | ||||
| import { PostAction, PostState } from 'src/screens/Shared/Compose' | ||||
| import addAttachments from './addAttachments' | ||||
| import PostAttachments from './PostAttachments' | ||||
| import ComposeAttachments from './Attachments' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import ComposeActions from './Actions' | ||||
| 
 | ||||
| export interface Props { | ||||
|   postState: PostState | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
| 
 | ||||
| const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
| const ComposeRoot: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|   const { theme } = useTheme() | ||||
|   useEffect(() => { | ||||
|     ;(async () => { | ||||
|       const { status } = await ImagePicker.requestCameraRollPermissionsAsync() | ||||
| @@ -44,8 +49,6 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|     })() | ||||
|   }, []) | ||||
| 
 | ||||
|   const [editorMinHeight, setEditorMinHeight] = useState(0) | ||||
| 
 | ||||
|   const { data: emojisData } = useQuery(['Emojis'], emojisFetch) | ||||
|   useEffect(() => { | ||||
|     if (emojisData && emojisData.length) { | ||||
| @@ -153,33 +156,49 @@ 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' | ||||
|   const textInputRef = useRef<TextInput>(null) | ||||
| 
 | ||||
|   const renderOverlay = (overlay: PostState['overlay']) => { | ||||
|     switch (overlay) { | ||||
|       case 'emojis': | ||||
|         return ( | ||||
|           <View style={styles.emojis}> | ||||
|             <ComposeEmojis | ||||
|               textInputRef={textInputRef} | ||||
|               onChangeText={onChangeText} | ||||
|               postState={postState} | ||||
|               postDispatch={postDispatch} | ||||
|             /> | ||||
|           </View> | ||||
|         ) | ||||
|       case 'suggestions': | ||||
|         return ( | ||||
|           <View style={styles.suggestions}> | ||||
|             <ComposeSuggestions | ||||
|               onChangeText={onChangeText} | ||||
|               postState={postState} | ||||
|               postDispatch={postDispatch} | ||||
|             /> | ||||
|           </View> | ||||
|         ) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <ScrollView | ||||
|         style={styles.contentView} | ||||
|         style={[styles.contentView]} | ||||
|         alwaysBounceVertical={false} | ||||
|         keyboardDismissMode='interactive' | ||||
|         // child touch event not picked up
 | ||||
|         keyboardShouldPersistTaps='always' | ||||
|       > | ||||
|         <TextInput | ||||
|           style={[ | ||||
|             styles.textInput | ||||
|             // {
 | ||||
|             //   flex: postState.overlay ? 0 : 1,
 | ||||
|             //   minHeight: editorMinHeight + 14
 | ||||
|             // }
 | ||||
|             styles.textInput, | ||||
|             { | ||||
|               color: theme.primary | ||||
|             } | ||||
|           ]} | ||||
|           autoCapitalize='none' | ||||
|           autoCorrect={false} | ||||
| @@ -187,10 +206,8 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|           enablesReturnKeyAutomatically | ||||
|           multiline | ||||
|           placeholder='想说点什么' | ||||
|           placeholderTextColor={theme.secondary} | ||||
|           onChangeText={content => onChangeText({ content })} | ||||
|           onContentSizeChange={({ nativeEvent }) => { | ||||
|             setEditorMinHeight(nativeEvent.contentSize.height) | ||||
|           }} | ||||
|           onSelectionChange={({ | ||||
|             nativeEvent: { | ||||
|               selection: { start, end } | ||||
| @@ -198,13 +215,17 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|           }) => { | ||||
|             postDispatch({ type: 'selection', payload: { start, end } }) | ||||
|           }} | ||||
|           ref={textInputRef} | ||||
|           scrollEnabled | ||||
|         > | ||||
|           <Text>{postState.text.formatted}</Text> | ||||
|         </TextInput> | ||||
| 
 | ||||
|         {renderOverlay(postState.overlay)} | ||||
| 
 | ||||
|         {postState.attachments.length > 0 && ( | ||||
|           <View style={styles.attachments}> | ||||
|             <PostAttachments | ||||
|             <ComposeAttachments | ||||
|               postState={postState} | ||||
|               postDispatch={postDispatch} | ||||
|             /> | ||||
| @@ -212,94 +233,15 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => { | ||||
|         )} | ||||
|         {postState.poll.active && ( | ||||
|           <View style={styles.poll}> | ||||
|             <PostPoll postState={postState} postDispatch={postDispatch} /> | ||||
|             <ComposePoll postState={postState} postDispatch={postDispatch} /> | ||||
|           </View> | ||||
|         )} | ||||
|         {postState.overlay === 'suggestions' ? ( | ||||
|           <View style={styles.suggestions}> | ||||
|             <PostSuggestions | ||||
|               onChangeText={onChangeText} | ||||
|               postState={postState} | ||||
|               postDispatch={postDispatch} | ||||
|             /> | ||||
|           </View> | ||||
|         ) : ( | ||||
|           <></> | ||||
|         )} | ||||
|         {postState.overlay === 'emojis' ? ( | ||||
|           <View style={styles.emojis}> | ||||
|             <PostEmojis | ||||
|               onChangeText={onChangeText} | ||||
|               postState={postState} | ||||
|               postDispatch={postDispatch} | ||||
|             /> | ||||
|           </View> | ||||
|         ) : ( | ||||
|           <></> | ||||
|         )} | ||||
|       </ScrollView> | ||||
|       <Pressable style={styles.additions} onPress={() => Keyboard.dismiss()}> | ||||
|         <Feather | ||||
|           name='paperclip' | ||||
|           size={24} | ||||
|           color={postState.poll.active ? 'gray' : 'black'} | ||||
|           onPress={async () => | ||||
|             !postState.poll.active && | ||||
|             (await addAttachments({ postState, postDispatch })) | ||||
|           } | ||||
|       <ComposeActions | ||||
|         textInputRef={textInputRef} | ||||
|         postState={postState} | ||||
|         postDispatch={postDispatch} | ||||
|       /> | ||||
|         <Feather | ||||
|           name='bar-chart-2' | ||||
|           size={24} | ||||
|           color={postState.attachments.length > 0 ? 'gray' : 'black'} | ||||
|           onPress={() => | ||||
|             postState.attachments.length === 0 && | ||||
|             postDispatch({ | ||||
|               type: 'poll', | ||||
|               payload: { ...postState.poll, active: !postState.poll.active } | ||||
|             }) | ||||
|           } | ||||
|         /> | ||||
|         <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} | ||||
|           color={postState.emojis?.length ? 'black' : 'white'} | ||||
|           onPress={() => { | ||||
|             if (postState.emojis?.length && postState.overlay === null) { | ||||
|               postDispatch({ type: 'overlay', payload: 'emojis' }) | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|         <Text>{postState.text.count}</Text> | ||||
|       </Pressable> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
| @@ -308,36 +250,29 @@ const styles = StyleSheet.create({ | ||||
|   base: { | ||||
|     flex: 1 | ||||
|   }, | ||||
|   contentView: { | ||||
|     flex: 1, | ||||
|     backgroundColor: 'gray' | ||||
|   }, | ||||
|   contentView: { flex: 1 }, | ||||
|   textInput: { | ||||
|     backgroundColor: 'lightgray', | ||||
|     paddingBottom: 20 | ||||
|     fontSize: StyleConstants.Font.Size.M, | ||||
|     marginTop: StyleConstants.Spacing.S, | ||||
|     marginBottom: StyleConstants.Spacing.M, | ||||
|     paddingLeft: StyleConstants.Spacing.Global.PagePadding, | ||||
|     paddingRight: StyleConstants.Spacing.Global.PagePadding | ||||
|   }, | ||||
|   attachments: { | ||||
|     flex: 1, | ||||
|     height: 100 | ||||
|   }, | ||||
|   poll: { | ||||
|     height: 100 | ||||
|     flex: 1, | ||||
|     padding: StyleConstants.Spacing.Global.PagePadding | ||||
|   }, | ||||
|   suggestions: { | ||||
|     flex: 1, | ||||
|     backgroundColor: 'lightyellow' | ||||
|   }, | ||||
|   emojis: { | ||||
|     flex: 1, | ||||
|     backgroundColor: 'lightblue' | ||||
|   }, | ||||
|   additions: { | ||||
|     height: 44, | ||||
|     backgroundColor: 'red', | ||||
|     flexDirection: 'row', | ||||
|     justifyContent: 'space-around', | ||||
|     alignItems: 'center' | ||||
|     flex: 1 | ||||
|   } | ||||
| }) | ||||
| 
 | ||||
| export default PostMain | ||||
| export default ComposeRoot | ||||
| @@ -46,7 +46,7 @@ export interface Props { | ||||
|   postDispatch: Dispatch<PostAction> | ||||
| } | ||||
| 
 | ||||
| const PostSuggestions: React.FC<Props> = ({ | ||||
| const ComposeSuggestions: React.FC<Props> = ({ | ||||
|   onChangeText, | ||||
|   postState, | ||||
|   postDispatch | ||||
| @@ -94,4 +94,4 @@ const PostSuggestions: React.FC<Props> = ({ | ||||
|   return content | ||||
| } | ||||
| 
 | ||||
| export default PostSuggestions | ||||
| export default ComposeSuggestions | ||||
| @@ -9,16 +9,27 @@ const updateText = ({ | ||||
|   postState: PostState | ||||
|   newText: string | ||||
| }) => { | ||||
|   if (postState.text.raw.length) { | ||||
|     const contentFront = postState.text.raw.slice(0, postState.selection.start) | ||||
|     const contentRear = postState.text.raw.slice(postState.selection.end) | ||||
|  | ||||
|     const whiteSpaceFront = /\s/g.test(contentFront.slice(-1)) | ||||
|     const whiteSpaceRear = /\s/g.test(contentRear.slice(-1)) | ||||
|  | ||||
|     const newTextWithSpace = `${whiteSpaceFront ? '' : ' '}${newText}${ | ||||
|       whiteSpaceRear ? '' : ' ' | ||||
|     }` | ||||
|  | ||||
|     onChangeText({ | ||||
|     content: postState.text.raw | ||||
|       ? [ | ||||
|           postState.text.raw.slice(0, postState.selection.start), | ||||
|           newText, | ||||
|           postState.text.raw.slice(postState.selection.end) | ||||
|         ].join('') | ||||
|       : newText, | ||||
|       content: [contentFront, newTextWithSpace, contentRear].join(''), | ||||
|       disableDebounce: true | ||||
|     }) | ||||
|   } else { | ||||
|     onChangeText({ | ||||
|       content: `${newText} `, | ||||
|       disableDebounce: true | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default updateText | ||||
|   | ||||
		Reference in New Issue
	
	Block a user