mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Preparing for upgrading expo SDK
This commit is contained in:
		
							
								
								
									
										16
									
								
								App.tsx
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								App.tsx
									
									
									
									
									
								
							| @@ -16,14 +16,14 @@ setConsole({ | ||||
|   error: console.warn | ||||
| }) | ||||
|  | ||||
| // if (__DEV__) { | ||||
| //   const whyDidYouRender = require('@welldone-software/why-did-you-render') | ||||
| //   whyDidYouRender(React, { | ||||
| //     trackAllPureComponents: true, | ||||
| //     trackHooks: true, | ||||
| //     hotReloadBufferMs: 1000 | ||||
| //   }) | ||||
| // } | ||||
| if (__DEV__) { | ||||
|   const whyDidYouRender = require('@welldone-software/why-did-you-render') | ||||
|   whyDidYouRender(React, { | ||||
|     trackAllPureComponents: true, | ||||
|     trackHooks: true, | ||||
|     hotReloadBufferMs: 1000 | ||||
|   }) | ||||
| } | ||||
|  | ||||
| const App: React.FC = () => { | ||||
|   return ( | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/@types/mastodon.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								src/@types/mastodon.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -267,6 +267,12 @@ declare namespace Mastodon { | ||||
|     'reading:expand:spoilers'?: boolean | ||||
|   } | ||||
|  | ||||
|   type Results = { | ||||
|     accounts?: Account[] | ||||
|     statuses?: Status[] | ||||
|     hashtags?: Tag[] | ||||
|   } | ||||
|  | ||||
|   type Status = { | ||||
|     // Base | ||||
|     id: string | ||||
|   | ||||
| @@ -79,4 +79,8 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ButtonRow | ||||
| export default React.memo(ButtonRow, (prev, next) => { | ||||
|   let skipUpdate = true | ||||
|   skipUpdate = prev.disabled === next.disabled | ||||
|   return skipUpdate | ||||
| }) | ||||
|   | ||||
| @@ -38,4 +38,9 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default HeaderLeft | ||||
| export default React.memo(HeaderLeft, (prev, next) => { | ||||
|   let skipUpdate = true | ||||
|   skipUpdate = prev.text === next.text | ||||
|   skipUpdate = prev.icon === next.icon | ||||
|   return skipUpdate | ||||
| }) | ||||
|   | ||||
| @@ -60,4 +60,10 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default HeaderRight | ||||
| export default React.memo(HeaderRight, (prev, next) => { | ||||
|   let skipUpdate = true | ||||
|   skipUpdate = prev.disabled === next.disabled | ||||
|   skipUpdate = prev.text === next.text | ||||
|   skipUpdate = prev.icon === next.icon | ||||
|   return skipUpdate | ||||
| }) | ||||
|   | ||||
| @@ -123,4 +123,8 @@ const styles = StyleSheet.create({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default MenuRow | ||||
| export default React.memo(MenuRow, (prev, next) => { | ||||
|   let skipUpdate = true | ||||
|   skipUpdate = prev.content === next.content | ||||
|   return skipUpdate | ||||
| }) | ||||
|   | ||||
| @@ -15,7 +15,7 @@ import HeaderDefaultActionsStatus from './HeaderDefault/ActionsStatus' | ||||
| import HeaderDefaultActionsDomain from './HeaderDefault/ActionsDomain' | ||||
|  | ||||
| export interface Props { | ||||
|   queryKey: App.QueryKey | ||||
|   queryKey?: App.QueryKey | ||||
|   status: Mastodon.Status | ||||
| } | ||||
|  | ||||
| @@ -83,11 +83,13 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => { | ||||
|             @{account} | ||||
|           </Text> | ||||
|         </View> | ||||
|         {queryKey && ( | ||||
|           <Pressable | ||||
|             style={styles.action} | ||||
|             onPress={onPressAction} | ||||
|             children={pressableAction} | ||||
|           /> | ||||
|         )} | ||||
|       </View> | ||||
|  | ||||
|       <View style={styles.meta}> | ||||
| @@ -116,6 +118,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => { | ||||
|         )} | ||||
|       </View> | ||||
|  | ||||
|       {queryKey && ( | ||||
|         <BottomSheet | ||||
|           visible={modalVisible} | ||||
|           handleDismiss={() => setBottomSheetVisible(false)} | ||||
| @@ -145,6 +148,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({ queryKey, status }) => { | ||||
|             /> | ||||
|           )} | ||||
|         </BottomSheet> | ||||
|       )} | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -37,13 +37,12 @@ export type ComposeState = { | ||||
|     formatted: ReactNode | ||||
|     selection: { start: number; end: number } | ||||
|   } | ||||
|   tag: | ||||
|     | { | ||||
|   tag?: { | ||||
|     type: 'url' | 'accounts' | 'hashtags' | ||||
|     text: string | ||||
|     offset: number | ||||
|     length: number | ||||
|   } | ||||
|     | undefined | ||||
|   emoji: { | ||||
|     active: boolean | ||||
|     emojis: { title: string; data: Mastodon.Emoji[] }[] | undefined | ||||
| @@ -69,7 +68,7 @@ export type ComposeState = { | ||||
|       | string | ||||
|   } | ||||
|   attachments: { sensitive: boolean; uploads: Mastodon.Attachment[] } | ||||
|   attachmentUploadProgress: { progress: number; aspect?: number } | undefined | ||||
|   attachmentUploadProgress?: { progress: number; aspect?: number } | ||||
|   visibility: 'public' | 'unlisted' | 'private' | 'direct' | ||||
|   replyToStatus?: Mastodon.Status | ||||
| } | ||||
| @@ -93,7 +92,7 @@ export type PostAction = | ||||
|     } | ||||
|   | { | ||||
|       type: 'poll' | ||||
|       payload: ComposeState['poll'] | ||||
|       payload: Partial<ComposeState['poll']> | ||||
|     } | ||||
|   | { | ||||
|       type: 'attachments' | ||||
| @@ -209,7 +208,8 @@ const composeExistingState = ({ | ||||
|           raw: replyPlaceholder, | ||||
|           formatted: undefined, | ||||
|           selection: { start: 0, end: 0 } | ||||
|         } | ||||
|         }, | ||||
|         replyToStatus: incomingStatus.reblog || incomingStatus | ||||
|       } | ||||
|   } | ||||
| } | ||||
| @@ -224,7 +224,7 @@ const postReducer = (state: ComposeState, action: PostAction): ComposeState => { | ||||
|     case 'emoji': | ||||
|       return { ...state, emoji: action.payload } | ||||
|     case 'poll': | ||||
|       return { ...state, poll: action.payload } | ||||
|       return { ...state, poll: { ...state.poll, ...action.payload } } | ||||
|     case 'attachments': | ||||
|       return { | ||||
|         ...state, | ||||
|   | ||||
| @@ -1,13 +1,6 @@ | ||||
| import { Feather } from '@expo/vector-icons' | ||||
| import React, { Dispatch, useCallback, useMemo } from 'react' | ||||
| import { | ||||
|   ActionSheetIOS, | ||||
|   Keyboard, | ||||
|   Pressable, | ||||
|   StyleSheet, | ||||
|   Text, | ||||
|   TextInput | ||||
| } from 'react-native' | ||||
| import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
| import { PostAction, ComposeState } from '../Compose' | ||||
| @@ -26,19 +19,6 @@ const ComposeActions: React.FC<Props> = ({ | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const getVisibilityIcon = () => { | ||||
|     switch (composeState.visibility) { | ||||
|       case 'public': | ||||
|         return 'globe' | ||||
|       case 'unlisted': | ||||
|         return 'unlock' | ||||
|       case 'private': | ||||
|         return 'lock' | ||||
|       case 'direct': | ||||
|         return 'mail' | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const attachmentColor = useMemo(() => { | ||||
|     if (composeState.poll.active) return theme.disabled | ||||
|     if (composeState.attachmentUploadProgress) return theme.primary | ||||
| @@ -99,6 +79,54 @@ const ComposeActions: React.FC<Props> = ({ | ||||
|     composeState.attachmentUploadProgress | ||||
|   ]) | ||||
|  | ||||
|   const visibilityIcon = useMemo(() => { | ||||
|     switch (composeState.visibility) { | ||||
|       case 'public': | ||||
|         return 'globe' | ||||
|       case 'unlisted': | ||||
|         return 'unlock' | ||||
|       case 'private': | ||||
|         return 'lock' | ||||
|       case 'direct': | ||||
|         return 'mail' | ||||
|     } | ||||
|   }, [composeState.visibility]) | ||||
|   const visibilityOnPress = useCallback( | ||||
|     () => | ||||
|       ActionSheetIOS.showActionSheetWithOptions( | ||||
|         { | ||||
|           options: ['公开', '不公开', '仅关注着', '私信', '取消'], | ||||
|           cancelButtonIndex: 4 | ||||
|         }, | ||||
|         buttonIndex => { | ||||
|           switch (buttonIndex) { | ||||
|             case 0: | ||||
|               composeDispatch({ type: 'visibility', payload: 'public' }) | ||||
|               break | ||||
|             case 1: | ||||
|               composeDispatch({ type: 'visibility', payload: 'unlisted' }) | ||||
|               break | ||||
|             case 2: | ||||
|               composeDispatch({ type: 'visibility', payload: 'private' }) | ||||
|               break | ||||
|             case 3: | ||||
|               composeDispatch({ type: 'visibility', payload: 'direct' }) | ||||
|               break | ||||
|           } | ||||
|         } | ||||
|       ), | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   const spoilerOnPress = useCallback( | ||||
|     () => | ||||
|       composeDispatch({ | ||||
|         type: 'spoiler', | ||||
|         payload: { active: !composeState.spoiler.active } | ||||
|       }), | ||||
|     [composeState.spoiler.active] | ||||
|   ) | ||||
|  | ||||
|   const emojiColor = useMemo(() => { | ||||
|     if (!composeState.emoji.emojis) return theme.disabled | ||||
|     if (composeState.emoji.active) { | ||||
| @@ -124,12 +152,11 @@ const ComposeActions: React.FC<Props> = ({ | ||||
|   }, [composeState.emoji.active, composeState.emoji.emojis]) | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
|     <View | ||||
|       style={[ | ||||
|         styles.additions, | ||||
|         { backgroundColor: theme.background, borderTopColor: theme.border } | ||||
|       ]} | ||||
|       onPress={() => Keyboard.dismiss()} | ||||
|     > | ||||
|       <Feather | ||||
|         name='aperture' | ||||
| @@ -144,44 +171,16 @@ const ComposeActions: React.FC<Props> = ({ | ||||
|         onPress={pollOnPress} | ||||
|       /> | ||||
|       <Feather | ||||
|         name={getVisibilityIcon()} | ||||
|         name={visibilityIcon} | ||||
|         size={24} | ||||
|         color={theme.secondary} | ||||
|         onPress={() => | ||||
|           ActionSheetIOS.showActionSheetWithOptions( | ||||
|             { | ||||
|               options: ['公开', '不公开', '仅关注着', '私信', '取消'], | ||||
|               cancelButtonIndex: 4 | ||||
|             }, | ||||
|             buttonIndex => { | ||||
|               switch (buttonIndex) { | ||||
|                 case 0: | ||||
|                   composeDispatch({ type: 'visibility', payload: 'public' }) | ||||
|                   break | ||||
|                 case 1: | ||||
|                   composeDispatch({ type: 'visibility', payload: 'unlisted' }) | ||||
|                   break | ||||
|                 case 2: | ||||
|                   composeDispatch({ type: 'visibility', payload: 'private' }) | ||||
|                   break | ||||
|                 case 3: | ||||
|                   composeDispatch({ type: 'visibility', payload: 'direct' }) | ||||
|                   break | ||||
|               } | ||||
|             } | ||||
|           ) | ||||
|         } | ||||
|         onPress={visibilityOnPress} | ||||
|       /> | ||||
|       <Feather | ||||
|         name='alert-triangle' | ||||
|         size={24} | ||||
|         color={composeState.spoiler.active ? theme.primary : theme.secondary} | ||||
|         onPress={() => | ||||
|           composeDispatch({ | ||||
|             type: 'spoiler', | ||||
|             payload: { active: !composeState.spoiler.active } | ||||
|           }) | ||||
|         } | ||||
|         onPress={spoilerOnPress} | ||||
|       /> | ||||
|       <Feather | ||||
|         name='smile' | ||||
| @@ -189,7 +188,7 @@ const ComposeActions: React.FC<Props> = ({ | ||||
|         color={emojiColor} | ||||
|         onPress={emojiOnPress} | ||||
|       /> | ||||
|     </Pressable> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { Dispatch } from 'react' | ||||
| import React, { Dispatch, useCallback, useMemo } from 'react' | ||||
| import { | ||||
|   Image, | ||||
|   Pressable, | ||||
| @@ -20,37 +20,15 @@ export interface Props { | ||||
|   composeDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const ComposeEmojis: React.FC<Props> = ({ | ||||
| const SingleEmoji = ({ | ||||
|   emoji, | ||||
|   textInputRef, | ||||
|   composeState, | ||||
|   composeDispatch | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <SectionList | ||||
|         horizontal | ||||
|         keyboardShouldPersistTaps='handled' | ||||
|         sections={composeState.emoji.emojis!} | ||||
|         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={() => { | ||||
| }: { emoji: Mastodon.Emoji } & Props) => { | ||||
|   const onPress = useCallback(() => { | ||||
|     updateText({ | ||||
|                         origin: textInputRef.current?.isFocused() | ||||
|                           ? 'text' | ||||
|                           : 'spoiler', | ||||
|       origin: textInputRef.current?.isFocused() ? 'text' : 'spoiler', | ||||
|       composeState, | ||||
|       composeDispatch, | ||||
|       newText: `:${emoji.shortcode}:`, | ||||
| @@ -60,17 +38,52 @@ const ComposeEmojis: React.FC<Props> = ({ | ||||
|       type: 'emoji', | ||||
|       payload: { ...composeState.emoji, active: false } | ||||
|     }) | ||||
|                     }} | ||||
|                   > | ||||
|                     <Image source={{ uri: emoji.url }} style={styles.emoji} /> | ||||
|                   </Pressable> | ||||
|                 ))} | ||||
|               </View> | ||||
|   }, []) | ||||
|   const children = useMemo( | ||||
|     () => <Image source={{ uri: emoji.url }} style={styles.emoji} />, | ||||
|     [] | ||||
|   ) | ||||
|           } else { | ||||
|             return null | ||||
|           } | ||||
|         }} | ||||
|   return ( | ||||
|     <Pressable key={emoji.shortcode} onPress={onPress} children={children} /> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const ComposeEmojis: React.FC<Props> = ({ ...props }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const listHeader = useCallback( | ||||
|     ({ section: { title } }) => ( | ||||
|       <Text style={[styles.group, { color: theme.secondary }]}>{title}</Text> | ||||
|     ), | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   const emojiList = useCallback( | ||||
|     section => | ||||
|       section.data.map((emoji: Mastodon.Emoji) => ( | ||||
|         <SingleEmoji key={emoji.shortcode} emoji={emoji} {...props} /> | ||||
|       )), | ||||
|     [] | ||||
|   ) | ||||
|   const listItem = useCallback( | ||||
|     ({ section, index }) => | ||||
|       index === 0 ? ( | ||||
|         <View key={section.title} style={styles.emojis}> | ||||
|           {emojiList(section)} | ||||
|         </View> | ||||
|       ) : null, | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <SectionList | ||||
|         horizontal | ||||
|         keyboardShouldPersistTaps='handled' | ||||
|         sections={props.composeState.emoji.emojis!} | ||||
|         keyExtractor={item => item.shortcode} | ||||
|         renderSectionHeader={listHeader} | ||||
|         renderItem={listItem} | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { Dispatch, useEffect, useState } from 'react' | ||||
| import React, { Dispatch, useCallback, useEffect, useState } from 'react' | ||||
| import { ActionSheetIOS, StyleSheet, TextInput, View } from 'react-native' | ||||
| import { Feather } from '@expo/vector-icons' | ||||
|  | ||||
| @@ -9,11 +9,14 @@ import { ButtonRow } from 'src/components/Button' | ||||
| import { MenuContainer, MenuRow } from 'src/components/Menu' | ||||
|  | ||||
| export interface Props { | ||||
|   composeState: ComposeState | ||||
|   poll: ComposeState['poll'] | ||||
|   composeDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
| const ComposePoll: React.FC<Props> = ({ | ||||
|   poll: { total, options, multiple, expire }, | ||||
|   composeDispatch | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const expireMapping: { [key: string]: string } = { | ||||
| @@ -31,24 +34,42 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|     setFirstRender(false) | ||||
|   }, []) | ||||
|  | ||||
|   const minusOnPress = useCallback( | ||||
|     () => | ||||
|       total > 2 && | ||||
|       composeDispatch({ | ||||
|         type: 'poll', | ||||
|         payload: { total: total - 1 } | ||||
|       }), | ||||
|     [total] | ||||
|   ) | ||||
|   console.log('total: ', total) | ||||
|   const plusOnPress = useCallback(() => { | ||||
|     total < 4 && | ||||
|       composeDispatch({ | ||||
|         type: 'poll', | ||||
|         payload: { total: total + 1 } | ||||
|       }) | ||||
|   }, [total]) | ||||
|  | ||||
|   return ( | ||||
|     <View style={[styles.base, { borderColor: theme.border }]}> | ||||
|       <View style={styles.options}> | ||||
|         {[...Array(composeState.poll.total)].map((e, i) => { | ||||
|           const restOptions = Object.keys(composeState.poll.options).filter( | ||||
|             o => parseInt(o) !== i && parseInt(o) < composeState.poll.total | ||||
|         {[...Array(total)].map((e, i) => { | ||||
|           const restOptions = Object.keys(options).filter( | ||||
|             o => parseInt(o) !== i && parseInt(o) < total | ||||
|           ) | ||||
|           let hasConflict = false | ||||
|           restOptions.forEach(o => { | ||||
|             // @ts-ignore | ||||
|             if (composeState.poll.options[o] === composeState.poll.options[i]) { | ||||
|             if (options[o] === options[i]) { | ||||
|               hasConflict = true | ||||
|             } | ||||
|           }) | ||||
|           return ( | ||||
|             <View key={i} style={styles.option}> | ||||
|               <Feather | ||||
|                 name={composeState.poll.multiple ? 'square' : 'circle'} | ||||
|                 name={multiple ? 'square' : 'circle'} | ||||
|                 size={StyleConstants.Font.Size.L} | ||||
|                 color={theme.secondary} | ||||
|               /> | ||||
| @@ -65,14 +86,11 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|                 placeholderTextColor={theme.secondary} | ||||
|                 maxLength={50} | ||||
|                 // @ts-ignore | ||||
|                 value={composeState.poll.options[i]} | ||||
|                 value={options[i]} | ||||
|                 onChangeText={e => | ||||
|                   composeDispatch({ | ||||
|                     type: 'poll', | ||||
|                     payload: { | ||||
|                       ...composeState.poll, | ||||
|                       options: { ...composeState.poll.options, [i]: e } | ||||
|                     } | ||||
|                     payload: { options: { ...options, [i]: e } } | ||||
|                   }) | ||||
|                 } | ||||
|               /> | ||||
| @@ -83,35 +101,23 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|       <View style={styles.controlAmount}> | ||||
|         <View style={styles.firstButton}> | ||||
|           <ButtonRow | ||||
|             onPress={() => | ||||
|               composeState.poll.total > 2 && | ||||
|               composeDispatch({ | ||||
|                 type: 'poll', | ||||
|                 payload: { ...composeState.poll, total: composeState.poll.total - 1 } | ||||
|               }) | ||||
|             } | ||||
|             onPress={minusOnPress} | ||||
|             icon='minus' | ||||
|             disabled={!(composeState.poll.total > 2)} | ||||
|             disabled={!(total > 2)} | ||||
|             buttonSize='S' | ||||
|           /> | ||||
|         </View> | ||||
|         <ButtonRow | ||||
|           onPress={() => | ||||
|             composeState.poll.total < 4 && | ||||
|             composeDispatch({ | ||||
|               type: 'poll', | ||||
|               payload: { ...composeState.poll, total: composeState.poll.total + 1 } | ||||
|             }) | ||||
|           } | ||||
|           onPress={plusOnPress} | ||||
|           icon='plus' | ||||
|           disabled={!(composeState.poll.total < 4)} | ||||
|           disabled={!(total < 4)} | ||||
|           buttonSize='S' | ||||
|         /> | ||||
|       </View> | ||||
|       <MenuContainer> | ||||
|         <MenuRow | ||||
|           title='可选项' | ||||
|           content={composeState.poll.multiple ? '多选' : '单选'} | ||||
|           content={multiple ? '多选' : '单选'} | ||||
|           onPress={() => | ||||
|             ActionSheetIOS.showActionSheetWithOptions( | ||||
|               { | ||||
| @@ -122,7 +128,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|                 index < 2 && | ||||
|                 composeDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { ...composeState.poll, multiple: index === 1 } | ||||
|                   payload: { multiple: index === 1 } | ||||
|                 }) | ||||
|             ) | ||||
|           } | ||||
| @@ -130,7 +136,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|         /> | ||||
|         <MenuRow | ||||
|           title='有效期' | ||||
|           content={expireMapping[composeState.poll.expire]} | ||||
|           content={expireMapping[expire]} | ||||
|           onPress={() => | ||||
|             ActionSheetIOS.showActionSheetWithOptions( | ||||
|               { | ||||
| @@ -141,10 +147,7 @@ const ComposePoll: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|                 index < 7 && | ||||
|                 composeDispatch({ | ||||
|                   type: 'poll', | ||||
|                   payload: { | ||||
|                     ...composeState.poll, | ||||
|                     expire: Object.keys(expireMapping)[index] | ||||
|                   } | ||||
|                   payload: { expire: Object.keys(expireMapping)[index] } | ||||
|                 }) | ||||
|             ) | ||||
|           } | ||||
|   | ||||
							
								
								
									
										46
									
								
								src/screens/Shared/Compose/Reply.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/screens/Shared/Compose/Reply.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import React, { Dispatch, RefObject } from 'react' | ||||
| import { StyleSheet, Text, TextInput, View } from 'react-native' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
|  | ||||
| import TimelineAttachment from 'src/components/Timelines/Timeline/Shared/Attachment' | ||||
| import TimelineAvatar from 'src/components/Timelines/Timeline/Shared/Avatar' | ||||
| import TimelineCard from 'src/components/Timelines/Timeline/Shared/Card' | ||||
| import TimelineContent from 'src/components/Timelines/Timeline/Shared/Content' | ||||
| import TimelineHeaderDefault from 'src/components/Timelines/Timeline/Shared/HeaderDefault' | ||||
|  | ||||
| export interface Props { | ||||
|   replyToStatus: Mastodon.Status | ||||
| } | ||||
|  | ||||
| const ComposeReply: React.FC<Props> = ({ replyToStatus }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   return ( | ||||
|     <View style={styles.status}> | ||||
|       <TimelineAvatar account={replyToStatus.account} /> | ||||
|       <View style={styles.details}> | ||||
|         <TimelineHeaderDefault status={replyToStatus} /> | ||||
|         {replyToStatus.content.length > 0 && ( | ||||
|           <TimelineContent status={replyToStatus} /> | ||||
|         )} | ||||
|         {replyToStatus.media_attachments.length > 0 && ( | ||||
|           <TimelineAttachment status={replyToStatus} width={200} /> | ||||
|         )} | ||||
|         {replyToStatus.card && <TimelineCard card={replyToStatus.card} />} | ||||
|       </View> | ||||
|     </View> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   status: { | ||||
|     flex: 1, | ||||
|     flexDirection: 'row' | ||||
|   }, | ||||
|   details: { | ||||
|     flex: 1 | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default React.memo(ComposeReply, () => true) | ||||
| @@ -1,5 +1,12 @@ | ||||
| import { forEach, groupBy, sortBy } from 'lodash' | ||||
| import React, { Dispatch, useEffect, useMemo, useRef } from 'react' | ||||
| import React, { | ||||
|   Dispatch, | ||||
|   RefObject, | ||||
|   useCallback, | ||||
|   useEffect, | ||||
|   useMemo, | ||||
|   useRef | ||||
| } from 'react' | ||||
| import { | ||||
|   View, | ||||
|   ActivityIndicator, | ||||
| @@ -22,6 +29,7 @@ import ComposeActions from './Actions' | ||||
| import ComposeAttachments from './Attachments' | ||||
| import ComposeEmojis from './Emojis' | ||||
| import ComposePoll from './Poll' | ||||
| import ComposeReply from './Reply' | ||||
| import ComposeSpoilerInput from './SpoilerInput' | ||||
| import ComposeTextInput from './TextInput' | ||||
| import updateText from './updateText' | ||||
| @@ -32,13 +40,91 @@ export interface Props { | ||||
|   composeDispatch: Dispatch<PostAction> | ||||
| } | ||||
|  | ||||
| const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
| const ListItem = React.memo( | ||||
|   ({ | ||||
|     item, | ||||
|     composeState, | ||||
|     composeDispatch, | ||||
|     textInputRef | ||||
|   }: { | ||||
|     item: Mastodon.Account & Mastodon.Tag | ||||
|     composeState: ComposeState | ||||
|     composeDispatch: Dispatch<PostAction> | ||||
|     textInputRef: RefObject<TextInput> | ||||
|   }) => { | ||||
|     const { theme } = useTheme() | ||||
|     const onPress = useCallback(() => { | ||||
|       const focusedInput = textInputRef.current?.isFocused() | ||||
|         ? 'text' | ||||
|         : 'spoiler' | ||||
|       updateText({ | ||||
|         origin: focusedInput, | ||||
|         composeState: { | ||||
|           ...composeState, | ||||
|           [focusedInput]: { | ||||
|             ...composeState[focusedInput], | ||||
|             selection: { | ||||
|               start: composeState.tag!.offset, | ||||
|               end: composeState.tag!.offset + composeState.tag!.text.length + 1 | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         composeDispatch, | ||||
|         newText: item.acct ? `@${item.acct}` : `#${item.name}`, | ||||
|         type: 'suggestion' | ||||
|       }) | ||||
|     }, []) | ||||
|     const children = useMemo( | ||||
|       () => | ||||
|         item.acct ? ( | ||||
|           <View style={[styles.account, { borderBottomColor: theme.border }]}> | ||||
|             <Image source={{ uri: item.avatar }} style={styles.accountAvatar} /> | ||||
|             <View> | ||||
|               <Text style={[styles.accountName, { color: theme.primary }]}> | ||||
|                 {item.emojis?.length ? ( | ||||
|                   <Emojis | ||||
|                     content={item.display_name || item.username} | ||||
|                     emojis={item.emojis} | ||||
|                     size={StyleConstants.Font.Size.S} | ||||
|                   /> | ||||
|                 ) : ( | ||||
|                   item.display_name || item.username | ||||
|                 )} | ||||
|               </Text> | ||||
|               <Text style={[styles.accountAccount, { color: theme.primary }]}> | ||||
|                 @{item.acct} | ||||
|               </Text> | ||||
|             </View> | ||||
|           </View> | ||||
|         ) : ( | ||||
|           <View style={[styles.hashtag, { borderBottomColor: theme.border }]}> | ||||
|             <Text style={[styles.hashtagText, { color: theme.primary }]}> | ||||
|               #{item.name} | ||||
|             </Text> | ||||
|           </View> | ||||
|         ), | ||||
|       [] | ||||
|     ) | ||||
|     return ( | ||||
|       <Pressable | ||||
|         key={item.url} | ||||
|         onPress={onPress} | ||||
|         style={styles.suggestion} | ||||
|         children={children} | ||||
|       /> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|   const { isFetching, isSuccess, data, refetch } = useQuery( | ||||
|     [ | ||||
|       'Search', | ||||
|       { type: composeState.tag?.type, term: composeState.tag?.text.substring(1) } | ||||
|       { | ||||
|         type: composeState.tag?.type, | ||||
|         term: composeState.tag?.text.substring(1) | ||||
|       } | ||||
|     ], | ||||
|     searchFetch, | ||||
|     { enabled: false } | ||||
| @@ -86,15 +172,8 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|     } | ||||
|   }, [isFetching]) | ||||
|  | ||||
|   const listHeader = useMemo(() => { | ||||
|     return ( | ||||
|     <View style={styles.base}> | ||||
|       <ProgressViewIOS | ||||
|         progress={composeState.attachmentUploadProgress?.progress || 0} | ||||
|         progressViewStyle='bar' | ||||
|       /> | ||||
|       <FlatList | ||||
|         keyboardShouldPersistTaps='handled' | ||||
|         ListHeaderComponent={ | ||||
|       <> | ||||
|         {composeState.spoiler.active ? ( | ||||
|           <ComposeSpoilerInput | ||||
| @@ -108,10 +187,12 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|           textInputRef={textInputRef} | ||||
|         /> | ||||
|       </> | ||||
|         } | ||||
|         ListFooterComponent={ | ||||
|           <> | ||||
|             {composeState.emoji.active && ( | ||||
|     ) | ||||
|   }, [composeState.spoiler.active, composeState.text.formatted]) | ||||
|  | ||||
|   const listFooterEmojis = useMemo( | ||||
|     () => | ||||
|       composeState.emoji.active && ( | ||||
|         <View style={styles.emojis}> | ||||
|           <ComposeEmojis | ||||
|             textInputRef={textInputRef} | ||||
| @@ -119,9 +200,12 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|             composeDispatch={composeDispatch} | ||||
|           /> | ||||
|         </View> | ||||
|             )} | ||||
|  | ||||
|             {(composeState.attachments.uploads.length > 0 || | ||||
|       ), | ||||
|     [composeState.emoji.active] | ||||
|   ) | ||||
|   const listFooterAttachments = useMemo( | ||||
|     () => | ||||
|       (composeState.attachments.uploads.length > 0 || | ||||
|         composeState.attachmentUploadProgress) && ( | ||||
|         <View style={styles.attachments}> | ||||
|           <ComposeAttachments | ||||
| @@ -129,99 +213,101 @@ const ComposeRoot: React.FC<Props> = ({ composeState, composeDispatch }) => { | ||||
|             composeDispatch={composeDispatch} | ||||
|           /> | ||||
|         </View> | ||||
|             )} | ||||
|  | ||||
|             {composeState.poll.active && ( | ||||
|       ), | ||||
|     [composeState.attachments.uploads, composeState.attachmentUploadProgress] | ||||
|   ) | ||||
|   // const listFooterPoll = useMemo( | ||||
|   //   () => | ||||
|   //     composeState.poll.active && ( | ||||
|   //       <View style={styles.poll}> | ||||
|   //         <ComposePoll | ||||
|   //           poll={composeState.poll} | ||||
|   //           composeDispatch={composeDispatch} | ||||
|   //         /> | ||||
|   //       </View> | ||||
|   //     ), | ||||
|   //   [ | ||||
|   //     composeState.poll.active, | ||||
|   //     composeState.poll.total, | ||||
|   //     composeState.poll.options['0'], | ||||
|   //     composeState.poll.options['1'], | ||||
|   //     composeState.poll.options['2'], | ||||
|   //     composeState.poll.options['3'], | ||||
|   //     composeState.poll.multiple, | ||||
|   //     composeState.poll.expire | ||||
|   //   ] | ||||
|   // ) | ||||
|   const listFooterPoll = () => | ||||
|     composeState.poll.active && ( | ||||
|       <View style={styles.poll}> | ||||
|         <ComposePoll | ||||
|                   composeState={composeState} | ||||
|           poll={composeState.poll} | ||||
|           composeDispatch={composeDispatch} | ||||
|         /> | ||||
|       </View> | ||||
|             )} | ||||
|     ) | ||||
|   const listFooterReply = useMemo( | ||||
|     () => | ||||
|       composeState.replyToStatus && ( | ||||
|         <View style={styles.replyTo}> | ||||
|           <ComposeReply replyToStatus={composeState.replyToStatus} /> | ||||
|         </View> | ||||
|       ), | ||||
|     [] | ||||
|   ) | ||||
|   const listFooter = useMemo(() => { | ||||
|     return ( | ||||
|       <> | ||||
|         {listFooterEmojis} | ||||
|         {listFooterAttachments} | ||||
|         {listFooterPoll()} | ||||
|         {listFooterReply} | ||||
|       </> | ||||
|         } | ||||
|     ) | ||||
|   }, [ | ||||
|     composeState.emoji.active, | ||||
|     composeState.attachments.uploads, | ||||
|     composeState.attachmentUploadProgress, | ||||
|     composeState.poll.active, | ||||
|     composeState.poll.total, | ||||
|     composeState.poll.options['0'], | ||||
|     composeState.poll.options['1'], | ||||
|     composeState.poll.options['2'], | ||||
|     composeState.poll.options['3'], | ||||
|     composeState.poll.multiple, | ||||
|     composeState.poll.expire | ||||
|   ]) | ||||
|  | ||||
|   const listKey = useCallback( | ||||
|     (item: Mastodon.Account | Mastodon.Tag) => item.url, | ||||
|     [isSuccess] | ||||
|   ) | ||||
|   const listItem = useCallback( | ||||
|     ({ item }) => | ||||
|       isSuccess ? ( | ||||
|         <ListItem | ||||
|           item={item} | ||||
|           composeState={composeState} | ||||
|           composeDispatch={composeDispatch} | ||||
|           textInputRef={textInputRef} | ||||
|         /> | ||||
|       ) : null, | ||||
|     [isSuccess] | ||||
|   ) | ||||
|   return ( | ||||
|     <View style={styles.base}> | ||||
|       <ProgressViewIOS | ||||
|         progress={composeState.attachmentUploadProgress?.progress || 0} | ||||
|         progressViewStyle='bar' | ||||
|       /> | ||||
|       <FlatList | ||||
|         keyboardShouldPersistTaps='handled' | ||||
|         ListHeaderComponent={listHeader} | ||||
|         ListFooterComponent={listFooter} | ||||
|         ListEmptyComponent={listEmpty} | ||||
|         data={composeState.tag && isSuccess ? data[composeState.tag.type] : []} | ||||
|         renderItem={({ item, index }) => ( | ||||
|           <Pressable | ||||
|             key={index} | ||||
|             onPress={() => { | ||||
|               const focusedInput = textInputRef.current?.isFocused() | ||||
|                 ? 'text' | ||||
|                 : 'spoiler' | ||||
|               updateText({ | ||||
|                 origin: focusedInput, | ||||
|                 composeState: { | ||||
|                   ...composeState, | ||||
|                   [focusedInput]: { | ||||
|                     ...composeState[focusedInput], | ||||
|                     selection: { | ||||
|                       start: composeState.tag!.offset, | ||||
|                       end: | ||||
|                         composeState.tag!.offset + composeState.tag!.text.length + 1 | ||||
|                     } | ||||
|                   } | ||||
|                 }, | ||||
|                 composeDispatch, | ||||
|                 newText: item.acct ? `@${item.acct}` : `#${item.name}`, | ||||
|                 type: 'suggestion' | ||||
|               }) | ||||
|             }} | ||||
|             style={styles.suggestion} | ||||
|           > | ||||
|             {item.acct ? ( | ||||
|               <View | ||||
|                 style={[ | ||||
|                   styles.account, | ||||
|                   { borderBottomColor: theme.border }, | ||||
|                   index === 0 && { | ||||
|                     borderTopWidth: StyleSheet.hairlineWidth, | ||||
|                     borderTopColor: theme.border | ||||
|                   } | ||||
|                 ]} | ||||
|               > | ||||
|                 <Image | ||||
|                   source={{ uri: item.avatar }} | ||||
|                   style={styles.accountAvatar} | ||||
|                 /> | ||||
|                 <View> | ||||
|                   <Text style={[styles.accountName, { color: theme.primary }]}> | ||||
|                     {item.emojis.length ? ( | ||||
|                       <Emojis | ||||
|                         content={item.display_name || item.username} | ||||
|                         emojis={item.emojis} | ||||
|                         size={StyleConstants.Font.Size.S} | ||||
|                       /> | ||||
|                     ) : ( | ||||
|                       item.display_name || item.username | ||||
|                     )} | ||||
|                   </Text> | ||||
|                   <Text | ||||
|                     style={[styles.accountAccount, { color: theme.primary }]} | ||||
|                   > | ||||
|                     @{item.acct} | ||||
|                   </Text> | ||||
|                 </View> | ||||
|               </View> | ||||
|             ) : ( | ||||
|               <View | ||||
|                 style={[ | ||||
|                   styles.hashtag, | ||||
|                   { borderBottomColor: theme.border }, | ||||
|                   index === 0 && { | ||||
|                     borderTopWidth: StyleSheet.hairlineWidth, | ||||
|                     borderTopColor: theme.border | ||||
|                   } | ||||
|                 ]} | ||||
|               > | ||||
|                 <Text style={[styles.hashtagText, { color: theme.primary }]}> | ||||
|                   #{item.name} | ||||
|                 </Text> | ||||
|               </View> | ||||
|             )} | ||||
|           </Pressable> | ||||
|         )} | ||||
|         data={data} | ||||
|         keyExtractor={listKey} | ||||
|         renderItem={listItem} | ||||
|       /> | ||||
|       <ComposeActions | ||||
|         textInputRef={textInputRef} | ||||
| @@ -245,6 +331,10 @@ const styles = StyleSheet.create({ | ||||
|     flex: 1, | ||||
|     padding: StyleConstants.Spacing.Global.PagePadding | ||||
|   }, | ||||
|   replyTo: { | ||||
|     flex: 1, | ||||
|     padding: StyleConstants.Spacing.Global.PagePadding | ||||
|   }, | ||||
|   suggestion: { | ||||
|     flex: 1 | ||||
|   }, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { Dispatch, RefObject } from 'react' | ||||
| import React, { Dispatch } from 'react' | ||||
| import { StyleSheet, Text, TextInput } from 'react-native' | ||||
| import { StyleConstants } from 'src/utils/styles/constants' | ||||
| import { useTheme } from 'src/utils/styles/ThemeManager' | ||||
| @@ -8,13 +8,11 @@ import formatText from './formatText' | ||||
| export interface Props { | ||||
|   composeState: ComposeState | ||||
|   composeDispatch: Dispatch<PostAction> | ||||
|   // textInputRef: RefObject<TextInput> | ||||
| } | ||||
|  | ||||
| const ComposeSpoilerInput: React.FC<Props> = ({ | ||||
|   composeState, | ||||
|   composeDispatch, | ||||
|   // textInputRef | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,10 @@ const ComposeTextInput: React.FC<Props> = ({ | ||||
|           selection: { start, end } | ||||
|         } | ||||
|       }) => { | ||||
|         composeDispatch({ type: 'text', payload: { selection: { start, end } } }) | ||||
|         composeDispatch({ | ||||
|           type: 'text', | ||||
|           payload: { selection: { start, end } } | ||||
|         }) | ||||
|       }} | ||||
|       ref={textInputRef} | ||||
|       scrollEnabled | ||||
| @@ -70,5 +73,6 @@ const styles = StyleSheet.create({ | ||||
| export default React.memo( | ||||
|   ComposeTextInput, | ||||
|   (prev, next) => | ||||
|     prev.composeState.text.raw === next.composeState.text.raw && | ||||
|     prev.composeState.text.formatted === next.composeState.text.formatted | ||||
| ) | ||||
|   | ||||
| @@ -19,5 +19,5 @@ export const searchFetch = async ( | ||||
|     url: 'search', | ||||
|     params: { type, q: term, limit } | ||||
|   }) | ||||
|   return Promise.resolve(res.body) | ||||
|   return Promise.resolve(res.body[type]) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user