mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Updates
This commit is contained in:
		| @@ -14,6 +14,7 @@ | |||||||
|     "@react-native-community/masked-view": "0.1.10", |     "@react-native-community/masked-view": "0.1.10", | ||||||
|     "@react-native-community/netinfo": "^5.9.7", |     "@react-native-community/netinfo": "^5.9.7", | ||||||
|     "@react-native-community/segmented-control": "2.2.1", |     "@react-native-community/segmented-control": "2.2.1", | ||||||
|  |     "@react-native-community/viewpager": "4.2.0", | ||||||
|     "@react-navigation/bottom-tabs": "^5.11.2", |     "@react-navigation/bottom-tabs": "^5.11.2", | ||||||
|     "@react-navigation/native": "^5.8.10", |     "@react-navigation/native": "^5.8.10", | ||||||
|     "@reduxjs/toolkit": "^1.5.0", |     "@reduxjs/toolkit": "^1.5.0", | ||||||
| @@ -61,6 +62,7 @@ | |||||||
|     "react-native-shimmer-placeholder": "^2.0.6", |     "react-native-shimmer-placeholder": "^2.0.6", | ||||||
|     "react-native-svg": "12.1.0", |     "react-native-svg": "12.1.0", | ||||||
|     "react-native-tab-view": "^2.15.2", |     "react-native-tab-view": "^2.15.2", | ||||||
|  |     "react-native-tab-view-viewpager-adapter": "^1.1.0", | ||||||
|     "react-native-toast-message": "^1.4.2", |     "react-native-toast-message": "^1.4.2", | ||||||
|     "react-navigation": "^4.4.3", |     "react-navigation": "^4.4.3", | ||||||
|     "react-query": "^3.5.6", |     "react-query": "^3.5.6", | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								src/@types/react-navigation.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -14,8 +14,18 @@ declare namespace Nav { | |||||||
|     'Screen-Shared-Announcements': { showAll?: boolean } |     'Screen-Shared-Announcements': { showAll?: boolean } | ||||||
|     'Screen-Shared-Compose': |     'Screen-Shared-Compose': | ||||||
|       | { |       | { | ||||||
|           type?: 'reply' | 'conversation' | 'edit' |           type: 'reply' | 'conversation' | 'edit' | ||||||
|           incomingStatus: Mastodon.Status |           incomingStatus: Mastodon.Status | ||||||
|  |           queryKey?: [ | ||||||
|  |             'Timeline', | ||||||
|  |             { | ||||||
|  |               page: App.Pages | ||||||
|  |               hashtag?: Mastodon.Tag['name'] | ||||||
|  |               list?: Mastodon.List['id'] | ||||||
|  |               toot?: Mastodon.Status['id'] | ||||||
|  |               account?: Mastodon.Account['id'] | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|         } |         } | ||||||
|       | undefined |       | undefined | ||||||
|     'Screen-Shared-Hashtag': { |     'Screen-Shared-Hashtag': { | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| import React from 'react' | import React, { useRef } from 'react' | ||||||
| import { Dimensions, Modal, StyleSheet, View } from 'react-native' | import { Dimensions, Modal, StyleSheet, View } from 'react-native' | ||||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||||
| import { useTheme } from '@utils/styles/ThemeManager' | import { useTheme } from '@utils/styles/ThemeManager' | ||||||
| import { StyleConstants } from '@utils/styles/constants' | import { StyleConstants } from '@utils/styles/constants' | ||||||
| import Button from '@components/Button' | import Button from '@components/Button' | ||||||
| import { PanGestureHandler } from 'react-native-gesture-handler' | import { | ||||||
|  |   PanGestureHandler, | ||||||
|  |   State, | ||||||
|  |   TapGestureHandler | ||||||
|  | } from 'react-native-gesture-handler' | ||||||
| import Animated, { | import Animated, { | ||||||
|   Extrapolate, |   Extrapolate, | ||||||
|   interpolate, |   interpolate, | ||||||
| @@ -55,33 +59,44 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => { | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Modal animated animationType='fade' visible={visible} transparent> |     <Modal animated animationType='fade' visible={visible} transparent> | ||||||
|       <PanGestureHandler onGestureEvent={onGestureEvent}> |       <TapGestureHandler | ||||||
|  |         onHandlerStateChange={({ nativeEvent }) => { | ||||||
|  |           if (nativeEvent.state === State.ACTIVE) { | ||||||
|  |             callDismiss() | ||||||
|  |           } | ||||||
|  |         }} | ||||||
|  |       > | ||||||
|         <Animated.View |         <Animated.View | ||||||
|           style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]} |           style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]} | ||||||
|         > |         > | ||||||
|           <Animated.View |           <PanGestureHandler onGestureEvent={onGestureEvent}> | ||||||
|             style={[ |             <Animated.View | ||||||
|               styles.container, |               style={[ | ||||||
|               styleTop, |                 styles.container, | ||||||
|               { |                 styleTop, | ||||||
|                 backgroundColor: theme.background, |                 { | ||||||
|                 paddingBottom: insets.bottom || StyleConstants.Spacing.L |                   backgroundColor: theme.background, | ||||||
|               } |                   paddingBottom: insets.bottom || StyleConstants.Spacing.L | ||||||
|             ]} |                 } | ||||||
|           > |               ]} | ||||||
|             <View |             > | ||||||
|               style={[styles.handle, { backgroundColor: theme.primaryOverlay }]} |               <View | ||||||
|             /> |                 style={[ | ||||||
|             {children} |                   styles.handle, | ||||||
|             <Button |                   { backgroundColor: theme.primaryOverlay } | ||||||
|               type='text' |                 ]} | ||||||
|               content='取消' |               /> | ||||||
|               onPress={() => handleDismiss()} |               {children} | ||||||
|               style={styles.button} |               <Button | ||||||
|             /> |                 type='text' | ||||||
|           </Animated.View> |                 content='取消' | ||||||
|  |                 onPress={() => handleDismiss()} | ||||||
|  |                 style={styles.button} | ||||||
|  |               /> | ||||||
|  |             </Animated.View> | ||||||
|  |           </PanGestureHandler> | ||||||
|         </Animated.View> |         </Animated.View> | ||||||
|       </PanGestureHandler> |       </TapGestureHandler> | ||||||
|     </Modal> |     </Modal> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import Icon from '@components/Icon' | import Icon from '@components/Icon' | ||||||
| import { StyleConstants } from '@utils/styles/constants' | import { StyleConstants } from '@utils/styles/constants' | ||||||
|  | import layoutAnimation from '@utils/styles/layoutAnimation' | ||||||
| import { useTheme } from '@utils/styles/ThemeManager' | import { useTheme } from '@utils/styles/ThemeManager' | ||||||
| import React, { useMemo } from 'react' | import React, { useEffect, useMemo, useRef } from 'react' | ||||||
| import { Pressable, StyleSheet, Text, View } from 'react-native' | import { Pressable, StyleSheet, Text, View } from 'react-native' | ||||||
| import { Chase } from 'react-native-animated-spinkit' | import { Chase } from 'react-native-animated-spinkit' | ||||||
|  |  | ||||||
| @@ -24,6 +25,15 @@ const HeaderRight: React.FC<Props> = ({ | |||||||
| }) => { | }) => { | ||||||
|   const { theme } = useTheme() |   const { theme } = useTheme() | ||||||
|  |  | ||||||
|  |   const mounted = useRef(false) | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (mounted.current) { | ||||||
|  |       layoutAnimation() | ||||||
|  |     } else { | ||||||
|  |       mounted.current = true | ||||||
|  |     } | ||||||
|  |   }, [content, loading, disabled]) | ||||||
|  |  | ||||||
|   const loadingSpinkit = useMemo( |   const loadingSpinkit = useMemo( | ||||||
|     () => ( |     () => ( | ||||||
|       <View style={{ position: 'absolute' }}> |       <View style={{ position: 'absolute' }}> | ||||||
|   | |||||||
| @@ -3,8 +3,9 @@ import { StyleConstants } from '@utils/styles/constants' | |||||||
| import { useTheme } from '@utils/styles/ThemeManager' | import { useTheme } from '@utils/styles/ThemeManager' | ||||||
| import { ColorDefinitions } from '@utils/styles/themes' | import { ColorDefinitions } from '@utils/styles/themes' | ||||||
| import React, { useMemo } from 'react' | import React, { useMemo } from 'react' | ||||||
| import { Pressable, StyleSheet, Switch, Text, View } from 'react-native' | import { StyleSheet, Switch, Text, View } from 'react-native' | ||||||
| import { Chase } from 'react-native-animated-spinkit' | import { Chase } from 'react-native-animated-spinkit' | ||||||
|  | import { State, TapGestureHandler } from 'react-native-gesture-handler' | ||||||
|  |  | ||||||
| export interface Props { | export interface Props { | ||||||
|   iconFront?: any |   iconFront?: any | ||||||
| @@ -54,81 +55,86 @@ const MenuRow: React.FC<Props> = ({ | |||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Pressable |     <View style={styles.base}> | ||||||
|       style={styles.base} |       <TapGestureHandler | ||||||
|       onPress={onPress} |         onHandlerStateChange={({ nativeEvent }) => { | ||||||
|       disabled={loading} |           if (nativeEvent.state === State.ACTIVE) { | ||||||
|       testID='base' |             if (!loading) { | ||||||
|     > |               onPress && onPress() | ||||||
|       <View style={styles.core}> |             } | ||||||
|         <View style={styles.front}> |           } | ||||||
|           {iconFront && ( |         }} | ||||||
|             <Icon |       > | ||||||
|               name={iconFront} |         <View style={styles.core}> | ||||||
|               size={StyleConstants.Font.Size.L} |           <View style={styles.front}> | ||||||
|               color={theme[iconFrontColor]} |             {iconFront && ( | ||||||
|               style={styles.iconFront} |               <Icon | ||||||
|             /> |                 name={iconFront} | ||||||
|           )} |                 size={StyleConstants.Font.Size.L} | ||||||
|           <View style={styles.main}> |                 color={theme[iconFrontColor]} | ||||||
|             <Text |                 style={styles.iconFront} | ||||||
|               style={[styles.title, { color: theme.primary }]} |  | ||||||
|               numberOfLines={1} |  | ||||||
|             > |  | ||||||
|               {title} |  | ||||||
|             </Text> |  | ||||||
|             {description ? ( |  | ||||||
|               <Text style={[styles.description, { color: theme.secondary }]}> |  | ||||||
|                 {description} |  | ||||||
|               </Text> |  | ||||||
|             ) : null} |  | ||||||
|           </View> |  | ||||||
|         </View> |  | ||||||
|  |  | ||||||
|         {(content && content.length) || |  | ||||||
|         switchValue !== undefined || |  | ||||||
|         iconBack ? ( |  | ||||||
|           <View style={styles.back}> |  | ||||||
|             {content && content.length ? ( |  | ||||||
|               <> |  | ||||||
|                 <Text |  | ||||||
|                   style={[ |  | ||||||
|                     styles.content, |  | ||||||
|                     { |  | ||||||
|                       color: theme.secondary, |  | ||||||
|                       opacity: !iconBack && loading ? 0 : 1 |  | ||||||
|                     } |  | ||||||
|                   ]} |  | ||||||
|                   numberOfLines={1} |  | ||||||
|                 > |  | ||||||
|                   {content} |  | ||||||
|                 </Text> |  | ||||||
|                 {loading && !iconBack && loadingSpinkit} |  | ||||||
|               </> |  | ||||||
|             ) : null} |  | ||||||
|             {switchValue !== undefined ? ( |  | ||||||
|               <Switch |  | ||||||
|                 value={switchValue} |  | ||||||
|                 onValueChange={switchOnValueChange} |  | ||||||
|                 disabled={switchDisabled} |  | ||||||
|                 trackColor={{ true: theme.blue, false: theme.disabled }} |  | ||||||
|               /> |               /> | ||||||
|             ) : null} |             )} | ||||||
|             {iconBack ? ( |             <View style={styles.main}> | ||||||
|               <> |               <Text | ||||||
|                 <Icon |                 style={[styles.title, { color: theme.primary }]} | ||||||
|                   name={iconBack} |                 numberOfLines={1} | ||||||
|                   size={StyleConstants.Font.Size.L} |               > | ||||||
|                   color={theme[iconBackColor]} |                 {title} | ||||||
|                   style={[styles.iconBack, { opacity: loading ? 0 : 1 }]} |               </Text> | ||||||
|                 /> |               {description ? ( | ||||||
|                 {loading && loadingSpinkit} |                 <Text style={[styles.description, { color: theme.secondary }]}> | ||||||
|               </> |                   {description} | ||||||
|             ) : null} |                 </Text> | ||||||
|  |               ) : null} | ||||||
|  |             </View> | ||||||
|           </View> |           </View> | ||||||
|         ) : null} |  | ||||||
|       </View> |           {(content && content.length) || | ||||||
|     </Pressable> |           switchValue !== undefined || | ||||||
|  |           iconBack ? ( | ||||||
|  |             <View style={styles.back}> | ||||||
|  |               {content && content.length ? ( | ||||||
|  |                 <> | ||||||
|  |                   <Text | ||||||
|  |                     style={[ | ||||||
|  |                       styles.content, | ||||||
|  |                       { | ||||||
|  |                         color: theme.secondary, | ||||||
|  |                         opacity: !iconBack && loading ? 0 : 1 | ||||||
|  |                       } | ||||||
|  |                     ]} | ||||||
|  |                     numberOfLines={1} | ||||||
|  |                   > | ||||||
|  |                     {content} | ||||||
|  |                   </Text> | ||||||
|  |                   {loading && !iconBack && loadingSpinkit} | ||||||
|  |                 </> | ||||||
|  |               ) : null} | ||||||
|  |               {switchValue !== undefined ? ( | ||||||
|  |                 <Switch | ||||||
|  |                   value={switchValue} | ||||||
|  |                   onValueChange={switchOnValueChange} | ||||||
|  |                   disabled={switchDisabled} | ||||||
|  |                   trackColor={{ true: theme.blue, false: theme.disabled }} | ||||||
|  |                 /> | ||||||
|  |               ) : null} | ||||||
|  |               {iconBack ? ( | ||||||
|  |                 <> | ||||||
|  |                   <Icon | ||||||
|  |                     name={iconBack} | ||||||
|  |                     size={StyleConstants.Font.Size.L} | ||||||
|  |                     color={theme[iconBackColor]} | ||||||
|  |                     style={[styles.iconBack, { opacity: loading ? 0 : 1 }]} | ||||||
|  |                   /> | ||||||
|  |                   {loading && loadingSpinkit} | ||||||
|  |                 </> | ||||||
|  |               ) : null} | ||||||
|  |             </View> | ||||||
|  |           ) : null} | ||||||
|  |         </View> | ||||||
|  |       </TapGestureHandler> | ||||||
|  |     </View> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react' | |||||||
| import { Dimensions, Platform, StyleSheet, View } from 'react-native' | import { Dimensions, Platform, StyleSheet, View } from 'react-native' | ||||||
| import { createNativeStackNavigator } from 'react-native-screens/native-stack' | import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||||
| import { TabView } from 'react-native-tab-view' | import { TabView } from 'react-native-tab-view' | ||||||
|  | import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter' | ||||||
| import { useSelector } from 'react-redux' | import { useSelector } from 'react-redux' | ||||||
|  |  | ||||||
| const Stack = createNativeStackNavigator< | const Stack = createNativeStackNavigator< | ||||||
| @@ -53,21 +54,6 @@ const Timelines: React.FC<Props> = ({ name, content }) => { | |||||||
|     [localActiveIndex] |     [localActiveIndex] | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   const screenComponent = useCallback( |  | ||||||
|     () => ( |  | ||||||
|       <TabView |  | ||||||
|         lazy |  | ||||||
|         swipeEnabled |  | ||||||
|         renderScene={renderScene} |  | ||||||
|         renderTabBar={() => null} |  | ||||||
|         onIndexChange={index => setSegment(index)} |  | ||||||
|         navigationState={{ index: segment, routes }} |  | ||||||
|         initialLayout={{ width: Dimensions.get('window').width }} |  | ||||||
|       /> |  | ||||||
|     ), |  | ||||||
|     [segment, localActiveIndex] |  | ||||||
|   ) |  | ||||||
|  |  | ||||||
|   const screenOptions = useMemo(() => { |   const screenOptions = useMemo(() => { | ||||||
|     if (localActiveIndex === null) { |     if (localActiveIndex === null) { | ||||||
|       if (name === 'Public') { |       if (name === 'Public') { | ||||||
| @@ -102,6 +88,8 @@ const Timelines: React.FC<Props> = ({ name, content }) => { | |||||||
|     } |     } | ||||||
|   }, [localActiveIndex, mode, segment]) |   }, [localActiveIndex, mode, segment]) | ||||||
|  |  | ||||||
|  |   const renderPager = useCallback(props => <ViewPagerAdapter {...props} />, []) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Stack.Navigator |     <Stack.Navigator | ||||||
|       screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }} |       screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }} | ||||||
| @@ -109,9 +97,21 @@ const Timelines: React.FC<Props> = ({ name, content }) => { | |||||||
|       <Stack.Screen |       <Stack.Screen | ||||||
|         // @ts-ignore |         // @ts-ignore | ||||||
|         name={`Screen-${name}-Root`} |         name={`Screen-${name}-Root`} | ||||||
|         component={screenComponent} |  | ||||||
|         options={screenOptions} |         options={screenOptions} | ||||||
|       /> |       > | ||||||
|  |         {() => ( | ||||||
|  |           <TabView | ||||||
|  |             lazy | ||||||
|  |             swipeEnabled | ||||||
|  |             renderPager={renderPager} | ||||||
|  |             renderScene={renderScene} | ||||||
|  |             renderTabBar={() => null} | ||||||
|  |             onIndexChange={index => setSegment(index)} | ||||||
|  |             navigationState={{ index: segment, routes }} | ||||||
|  |             initialLayout={{ width: Dimensions.get('window').width }} | ||||||
|  |           /> | ||||||
|  |         )} | ||||||
|  |       </Stack.Screen> | ||||||
|  |  | ||||||
|       {sharedScreens(Stack)} |       {sharedScreens(Stack)} | ||||||
|     </Stack.Navigator> |     </Stack.Navigator> | ||||||
|   | |||||||
| @@ -14,9 +14,7 @@ import { FlatList } from 'react-native-gesture-handler' | |||||||
| import { useDispatch } from 'react-redux' | import { useDispatch } from 'react-redux' | ||||||
| import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' | import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' | ||||||
| import { findIndex } from 'lodash' | import { findIndex } from 'lodash' | ||||||
| import CustomRefreshControl from '@components/CustomRefreshControl' |  | ||||||
| import { InfiniteData, useQueryClient } from 'react-query' | import { InfiniteData, useQueryClient } from 'react-query' | ||||||
| import { useTheme } from '@utils/styles/ThemeManager' |  | ||||||
|  |  | ||||||
| export interface Props { | export interface Props { | ||||||
|   page: App.Pages |   page: App.Pages | ||||||
| @@ -37,8 +35,6 @@ const Timeline: React.FC<Props> = ({ | |||||||
|   disableRefresh = false, |   disableRefresh = false, | ||||||
|   disableInfinity = false |   disableInfinity = false | ||||||
| }) => { | }) => { | ||||||
|   const { theme } = useTheme() |  | ||||||
|  |  | ||||||
|   const queryKeyParams = { |   const queryKeyParams = { | ||||||
|     page, |     page, | ||||||
|     ...(hashtag && { hashtag }), |     ...(hashtag && { hashtag }), | ||||||
| @@ -212,6 +208,7 @@ const Timeline: React.FC<Props> = ({ | |||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <FlatList |     <FlatList | ||||||
|  |       bounces={!disableRefresh} | ||||||
|       ref={flRef} |       ref={flRef} | ||||||
|       windowSize={11} |       windowSize={11} | ||||||
|       data={flattenData} |       data={flattenData} | ||||||
|   | |||||||
| @@ -103,7 +103,8 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => { | |||||||
|     () => |     () => | ||||||
|       navigation.navigate('Screen-Shared-Compose', { |       navigation.navigate('Screen-Shared-Compose', { | ||||||
|         type: 'reply', |         type: 'reply', | ||||||
|         incomingStatus: status |         incomingStatus: status, | ||||||
|  |         queryKey | ||||||
|       }), |       }), | ||||||
|     [] |     [] | ||||||
|   ) |   ) | ||||||
|   | |||||||
| @@ -10,6 +10,32 @@ export interface Props { | |||||||
|   card: Mastodon.Card |   card: Mastodon.Card | ||||||
| } | } | ||||||
|  |  | ||||||
|  | type CancelPromise = ((reason?: Error) => void) | undefined | ||||||
|  | type ImageSize = { width: number; height: number } | ||||||
|  | interface ImageSizeOperation { | ||||||
|  |   start: () => Promise<ImageSize> | ||||||
|  |   cancel: CancelPromise | ||||||
|  | } | ||||||
|  | const getImageSize = (uri: string): ImageSizeOperation => { | ||||||
|  |   let cancel: CancelPromise | ||||||
|  |   const start = (): Promise<ImageSize> => | ||||||
|  |     new Promise<{ width: number; height: number }>((resolve, reject) => { | ||||||
|  |       cancel = reject | ||||||
|  |       Image.getSize( | ||||||
|  |         uri, | ||||||
|  |         (width, height) => { | ||||||
|  |           cancel = undefined | ||||||
|  |           resolve({ width, height }) | ||||||
|  |         }, | ||||||
|  |         error => { | ||||||
|  |           reject(error) | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |   return { start, cancel } | ||||||
|  | } | ||||||
|  |  | ||||||
| const TimelineCard: React.FC<Props> = ({ card }) => { | const TimelineCard: React.FC<Props> = ({ card }) => { | ||||||
|   const { theme } = useTheme() |   const { theme } = useTheme() | ||||||
|  |  | ||||||
| @@ -19,6 +45,26 @@ const TimelineCard: React.FC<Props> = ({ card }) => { | |||||||
|       Image.getSize(card.image, () => setImageLoaded(true)) |       Image.getSize(card.image, () => setImageLoaded(true)) | ||||||
|     } |     } | ||||||
|   }, []) |   }, []) | ||||||
|  |   useEffect(() => { | ||||||
|  |     let cancel: CancelPromise | ||||||
|  |     const sideEffect = async (): Promise<void> => { | ||||||
|  |       try { | ||||||
|  |         const operation = getImageSize(card.image) | ||||||
|  |         cancel = operation.cancel | ||||||
|  |         await operation.start() | ||||||
|  |       } catch (error) { | ||||||
|  |         if (__DEV__) console.warn(error) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (card.image) { | ||||||
|  |       sideEffect() | ||||||
|  |     } | ||||||
|  |     return () => { | ||||||
|  |       if (cancel) { | ||||||
|  |         cancel() | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|   const cardVisual = useMemo(() => { |   const cardVisual = useMemo(() => { | ||||||
|     if (imageLoaded) { |     if (imageLoaded) { | ||||||
|       return <Image source={{ uri: card.image }} style={styles.image} /> |       return <Image source={{ uri: card.image }} style={styles.image} /> | ||||||
|   | |||||||
| @@ -96,7 +96,8 @@ const HeaderActionsStatus: React.FC<Props> = ({ | |||||||
|                   if (res.id) { |                   if (res.id) { | ||||||
|                     navigation.navigate('Screen-Shared-Compose', { |                     navigation.navigate('Screen-Shared-Compose', { | ||||||
|                       type: 'edit', |                       type: 'edit', | ||||||
|                       incomingStatus: res |                       incomingStatus: res, | ||||||
|  |                       queryKey | ||||||
|                     }) |                     }) | ||||||
|                   } |                   } | ||||||
|                 } |                 } | ||||||
|   | |||||||
| @@ -23,7 +23,6 @@ const ScreenMeRoot: React.FC<StackScreenProps< | |||||||
| >> = ({ route: { params }, navigation }) => { | >> = ({ route: { params }, navigation }) => { | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (params && params.navigateAway) { |     if (params && params.navigateAway) { | ||||||
|       console.log('oops') |  | ||||||
|       navigation.navigate(params.navigateAway) |       navigation.navigate(params.navigateAway) | ||||||
|     } |     } | ||||||
|   }, [params]) |   }, [params]) | ||||||
|   | |||||||
| @@ -43,8 +43,8 @@ const AccountHeader: React.FC<Props> = ({ account, limitHeight = false }) => { | |||||||
|   }, [account]) |   }, [account]) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Animated.Image |     <Animated.View | ||||||
|       source={{ uri: account?.header }} |       // source={{ uri: account?.header }} | ||||||
|       style={[styleHeight, { backgroundColor: theme.disabled }]} |       style={[styleHeight, { backgroundColor: theme.disabled }]} | ||||||
|     /> |     /> | ||||||
|   ) |   ) | ||||||
|   | |||||||
| @@ -1,10 +1,11 @@ | |||||||
| import Timeline from '@components/Timelines/Timeline' | import Timeline from '@components/Timelines/Timeline' | ||||||
| import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs' | import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs' | ||||||
| import { StyleConstants } from '@utils/styles/constants' | import { StyleConstants } from '@utils/styles/constants' | ||||||
| import React, { useContext } from 'react' | import React, { useCallback, useContext } from 'react' | ||||||
| import { Dimensions, StyleSheet } from 'react-native' | import { Dimensions, StyleSheet } from 'react-native' | ||||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||||
| import { TabView } from 'react-native-tab-view' | import { TabView } from 'react-native-tab-view' | ||||||
|  | import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter' | ||||||
| import AccountContext from './utils/createContext' | import AccountContext from './utils/createContext' | ||||||
|  |  | ||||||
| export interface Props { | export interface Props { | ||||||
| @@ -32,10 +33,13 @@ const AccountToots: React.FC<Props> = ({ id }) => { | |||||||
|     return <Timeline page={route.key} account={id} disableRefresh /> |     return <Timeline page={route.key} account={id} disableRefresh /> | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const renderPager = useCallback(props => <ViewPagerAdapter {...props} />, []) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <TabView |     <TabView | ||||||
|       lazy |       lazy | ||||||
|       swipeEnabled |       swipeEnabled | ||||||
|  |       renderPager={renderPager} | ||||||
|       renderScene={renderScene} |       renderScene={renderScene} | ||||||
|       renderTabBar={() => null} |       renderTabBar={() => null} | ||||||
|       initialLayout={{ width: Dimensions.get('window').width }} |       initialLayout={{ width: Dimensions.get('window').width }} | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| import { HeaderLeft, HeaderRight } from '@components/Header' | import { HeaderLeft, HeaderRight } from '@components/Header' | ||||||
| import haptics from '@root/components/haptics' | import haptics from '@root/components/haptics' | ||||||
| import { store } from '@root/store' | import { store } from '@root/store' | ||||||
| import layoutAnimation from '@root/utils/styles/layoutAnimation' |  | ||||||
| import formatText from '@screens/Shared/Compose/formatText' | import formatText from '@screens/Shared/Compose/formatText' | ||||||
| import ComposeRoot from '@screens/Shared/Compose/Root' | import ComposeRoot from '@screens/Shared/Compose/Root' | ||||||
| import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||||
| @@ -58,11 +57,8 @@ const Compose: React.FC<SharedComposeProp> = ({ | |||||||
|   const localAccount = getLocalAccount(store.getState()) |   const localAccount = getLocalAccount(store.getState()) | ||||||
|   const [composeState, composeDispatch] = useReducer( |   const [composeState, composeDispatch] = useReducer( | ||||||
|     composeReducer, |     composeReducer, | ||||||
|     params?.type && params?.incomingStatus |     params | ||||||
|       ? composeParseState({ |       ? composeParseState(params) | ||||||
|           type: params.type, |  | ||||||
|           incomingStatus: params.incomingStatus |  | ||||||
|         }) |  | ||||||
|       : { |       : { | ||||||
|           ...composeInitialState, |           ...composeInitialState, | ||||||
|           visibility: |           visibility: | ||||||
| @@ -72,7 +68,6 @@ const Compose: React.FC<SharedComposeProp> = ({ | |||||||
|               : 'public' |               : 'public' | ||||||
|         } |         } | ||||||
|   ) |   ) | ||||||
|   const [isSubmitting, setIsSubmitting] = useState(false) |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     switch (params?.type) { |     switch (params?.type) { | ||||||
| @@ -163,8 +158,8 @@ const Compose: React.FC<SharedComposeProp> = ({ | |||||||
|         type='text' |         type='text' | ||||||
|         content={params?.type ? postButtonText[params.type] : '发嘟嘟'} |         content={params?.type ? postButtonText[params.type] : '发嘟嘟'} | ||||||
|         onPress={() => { |         onPress={() => { | ||||||
|           layoutAnimation() |           composeDispatch({ type: 'posting', payload: true }) | ||||||
|           setIsSubmitting(true) |  | ||||||
|           composePost(params, composeState) |           composePost(params, composeState) | ||||||
|             .then(() => { |             .then(() => { | ||||||
|               haptics('Success') |               haptics('Success') | ||||||
| @@ -173,27 +168,15 @@ const Compose: React.FC<SharedComposeProp> = ({ | |||||||
|                 { page: 'Following' } |                 { page: 'Following' } | ||||||
|               ] |               ] | ||||||
|               queryClient.invalidateQueries(queryKey) |               queryClient.invalidateQueries(queryKey) | ||||||
|               if ( |  | ||||||
|                 params?.type && |               if (params?.queryKey && params.queryKey[1].page === 'Toot') { | ||||||
|                 (params.type === 'reply' || params.type === 'conversation') |                 queryClient.invalidateQueries(params.queryKey) | ||||||
|               ) { |  | ||||||
|                 queryClient.invalidateQueries( |  | ||||||
|                   [ |  | ||||||
|                     'Toot', |  | ||||||
|                     { |  | ||||||
|                       toot: params.incomingStatus.reblog |  | ||||||
|                         ? params.incomingStatus.reblog.id |  | ||||||
|                         : params.incomingStatus.id |  | ||||||
|                     } |  | ||||||
|                   ], |  | ||||||
|                   { exact: true, active: true } |  | ||||||
|                 ) |  | ||||||
|               } |               } | ||||||
|               navigation.goBack() |               navigation.goBack() | ||||||
|             }) |             }) | ||||||
|             .catch(() => { |             .catch(() => { | ||||||
|               haptics('Error') |               haptics('Error') | ||||||
|               setIsSubmitting(false) |               composeDispatch({ type: 'posting', payload: false }) | ||||||
|               Alert.alert('发布失败', '', [ |               Alert.alert('发布失败', '', [ | ||||||
|                 { |                 { | ||||||
|                   text: '返回重试' |                   text: '返回重试' | ||||||
| @@ -201,11 +184,11 @@ const Compose: React.FC<SharedComposeProp> = ({ | |||||||
|               ]) |               ]) | ||||||
|             }) |             }) | ||||||
|         }} |         }} | ||||||
|         loading={isSubmitting} |         loading={composeState.posting} | ||||||
|         disabled={composeState.text.raw.length < 1 || totalTextCount > 500} |         disabled={composeState.text.raw.length < 1 || totalTextCount > 500} | ||||||
|       /> |       /> | ||||||
|     ), |     ), | ||||||
|     [isSubmitting, totalTextCount, composeState] |     [totalTextCount, composeState] | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								src/screens/Shared/Compose/Posting.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/screens/Shared/Compose/Posting.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | import React, { useContext } from 'react' | ||||||
|  | import { Modal, StyleSheet, View } from 'react-native' | ||||||
|  | import { useTheme } from '@utils/styles/ThemeManager' | ||||||
|  | import ComposeContext from './utils/createContext' | ||||||
|  | import { Chase } from 'react-native-animated-spinkit' | ||||||
|  | import { StyleConstants } from '@utils/styles/constants' | ||||||
|  |  | ||||||
|  | const ComposePosting = React.memo( | ||||||
|  |   () => { | ||||||
|  |     const { composeState } = useContext(ComposeContext) | ||||||
|  |     const { theme } = useTheme() | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |       <Modal | ||||||
|  |         transparent | ||||||
|  |         animationType='fade' | ||||||
|  |         visible={composeState.posting} | ||||||
|  |         children={ | ||||||
|  |           <View | ||||||
|  |             style={[styles.base, { backgroundColor: theme.backgroundOverlay }]} | ||||||
|  |             children={ | ||||||
|  |               <Chase | ||||||
|  |                 size={StyleConstants.Font.Size.L * 2} | ||||||
|  |                 color={theme.primaryOverlay} | ||||||
|  |               /> | ||||||
|  |             } | ||||||
|  |           /> | ||||||
|  |         } | ||||||
|  |       /> | ||||||
|  |     ) | ||||||
|  |   }, | ||||||
|  |   () => true | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const styles = StyleSheet.create({ | ||||||
|  |   base: { | ||||||
|  |     flex: 1, | ||||||
|  |     justifyContent: 'center', | ||||||
|  |     alignItems: 'center' | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | export default ComposePosting | ||||||
| @@ -7,6 +7,7 @@ import React, { useCallback, useContext, useEffect, useMemo } from 'react' | |||||||
| import { View, FlatList, StyleSheet } from 'react-native' | import { View, FlatList, StyleSheet } from 'react-native' | ||||||
| import { Chase } from 'react-native-animated-spinkit' | import { Chase } from 'react-native-animated-spinkit' | ||||||
| import ComposeActions from './Actions' | import ComposeActions from './Actions' | ||||||
|  | import ComposePosting from './Posting' | ||||||
| import ComposeRootFooter from './Root/Footer' | import ComposeRootFooter from './Root/Footer' | ||||||
| import ComposeRootHeader from './Root/Header' | import ComposeRootHeader from './Root/Header' | ||||||
| import ComposeRootSuggestion from './Root/Suggestion' | import ComposeRootSuggestion from './Root/Suggestion' | ||||||
| @@ -89,6 +90,7 @@ const ComposeRoot: React.FC = () => { | |||||||
|         keyExtractor={({ item }) => item.acct || item.name} |         keyExtractor={({ item }) => item.acct || item.name} | ||||||
|       /> |       /> | ||||||
|       <ComposeActions /> |       <ComposeActions /> | ||||||
|  |       <ComposePosting /> | ||||||
|     </View> |     </View> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,8 @@ | |||||||
| import { createRef } from "react" | import { createRef } from 'react' | ||||||
| import { ComposeState } from "./types" | import { ComposeState } from './types' | ||||||
|  |  | ||||||
| const composeInitialState: ComposeState = { | const composeInitialState: ComposeState = { | ||||||
|  |   posting: false, | ||||||
|   spoiler: { |   spoiler: { | ||||||
|     active: false, |     active: false, | ||||||
|     count: 0, |     count: 0, | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| import client from '@root/api/client' | import client from '@root/api/client' | ||||||
| import { Props } from '@screens/Shared/Compose' |  | ||||||
| import { ComposeState } from '@screens/Shared/Compose/utils/types' | import { ComposeState } from '@screens/Shared/Compose/utils/types' | ||||||
|  | import { SharedComposeProp } from '@screens/Shared/sharedScreens' | ||||||
| import * as Crypto from 'expo-crypto' | import * as Crypto from 'expo-crypto' | ||||||
|  |  | ||||||
| const composePost = async ( | const composePost = async ( | ||||||
|   params: Props['route']['params'], |   params: SharedComposeProp['route']['params'], | ||||||
|   composeState: ComposeState |   composeState: ComposeState | ||||||
| ) => { | ) => { | ||||||
|   const formData = new FormData() |   const formData = new FormData() | ||||||
| @@ -43,7 +43,7 @@ const composePost = async ( | |||||||
|   return client<Mastodon.Status>({ |   return client<Mastodon.Status>({ | ||||||
|     method: 'post', |     method: 'post', | ||||||
|     instance: 'local', |     instance: 'local', | ||||||
|     url: 'statuses', |     url: 'statusess', | ||||||
|     headers: { |     headers: { | ||||||
|       'Idempotency-Key': await Crypto.digestStringAsync( |       'Idempotency-Key': await Crypto.digestStringAsync( | ||||||
|         Crypto.CryptoDigestAlgorithm.SHA256, |         Crypto.CryptoDigestAlgorithm.SHA256, | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ const composeReducer = ( | |||||||
|   action: ComposeAction |   action: ComposeAction | ||||||
| ): ComposeState => { | ): ComposeState => { | ||||||
|   switch (action.type) { |   switch (action.type) { | ||||||
|  |     case 'posting': | ||||||
|  |       return { ...state, posting: action.payload } | ||||||
|     case 'spoiler': |     case 'spoiler': | ||||||
|       return { ...state, spoiler: { ...state.spoiler, ...action.payload } } |       return { ...state, spoiler: { ...state.spoiler, ...action.payload } } | ||||||
|     case 'text': |     case 'text': | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								src/screens/Shared/Compose/utils/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								src/screens/Shared/Compose/utils/types.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,7 @@ export type ExtendedAttachment = { | |||||||
| } | } | ||||||
|  |  | ||||||
| export type ComposeState = { | export type ComposeState = { | ||||||
|  |   posting: boolean | ||||||
|   spoiler: { |   spoiler: { | ||||||
|     active: boolean |     active: boolean | ||||||
|     count: number |     count: number | ||||||
| @@ -62,6 +63,10 @@ export type ComposeState = { | |||||||
| } | } | ||||||
|  |  | ||||||
| export type ComposeAction = | export type ComposeAction = | ||||||
|  |   | { | ||||||
|  |       type: 'posting' | ||||||
|  |       payload: ComposeState['posting'] | ||||||
|  |     } | ||||||
|   | { |   | { | ||||||
|       type: 'spoiler' |       type: 'spoiler' | ||||||
|       payload: Partial<ComposeState['spoiler']> |       payload: Partial<ComposeState['spoiler']> | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ const ScreenSharedRelationships: React.FC<SharedRelationshipsProp> = ({ | |||||||
|         ) |         ) | ||||||
|       }) |       }) | ||||||
|     return updateHeaderRight() |     return updateHeaderRight() | ||||||
|   }, []) |   }, [segment, mode]) | ||||||
|  |  | ||||||
|   const routes: { |   const routes: { | ||||||
|     key: SharedRelationshipsProp['route']['params']['initialType'] |     key: SharedRelationshipsProp['route']['params']['initialType'] | ||||||
| @@ -55,9 +55,9 @@ const ScreenSharedRelationships: React.FC<SharedRelationshipsProp> = ({ | |||||||
|       swipeEnabled |       swipeEnabled | ||||||
|       renderScene={renderScene} |       renderScene={renderScene} | ||||||
|       renderTabBar={() => null} |       renderTabBar={() => null} | ||||||
|       initialLayout={{ width: Dimensions.get('window').width }} |  | ||||||
|       navigationState={{ index: segment, routes }} |  | ||||||
|       onIndexChange={index => setSegment(index)} |       onIndexChange={index => setSegment(index)} | ||||||
|  |       navigationState={{ index: segment, routes }} | ||||||
|  |       initialLayout={{ width: Dimensions.get('window').width }} | ||||||
|     /> |     /> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1727,6 +1727,11 @@ | |||||||
|   resolved "https://registry.yarnpkg.com/@react-native-community/segmented-control/-/segmented-control-2.2.1.tgz#5ca418d78c5f6051353c9586918458713b88a83c" |   resolved "https://registry.yarnpkg.com/@react-native-community/segmented-control/-/segmented-control-2.2.1.tgz#5ca418d78c5f6051353c9586918458713b88a83c" | ||||||
|   integrity sha512-BzxFbI9Iqv+31yVqEvCTzJYmwb8jOMTf/UPuC4Hj176tmEPqBpuDaGH+rkAFg1miOco3/43RQxiAZO+mkY40Fg== |   integrity sha512-BzxFbI9Iqv+31yVqEvCTzJYmwb8jOMTf/UPuC4Hj176tmEPqBpuDaGH+rkAFg1miOco3/43RQxiAZO+mkY40Fg== | ||||||
|  |  | ||||||
|  | "@react-native-community/viewpager@4.2.0": | ||||||
|  |   version "4.2.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/@react-native-community/viewpager/-/viewpager-4.2.0.tgz#11a4453715eb45cb0f732c98666ce168b800dfa4" | ||||||
|  |   integrity sha512-tptvkyStulE9Jv/LVYSldvydAq3DVOwsfqmy3mTh3NWl1LZwE4gZwZ455jiRmW5StsJ3Q/Od/GGsN0FI8gHAXQ== | ||||||
|  |  | ||||||
| "@react-navigation/bottom-tabs@^5.11.2": | "@react-navigation/bottom-tabs@^5.11.2": | ||||||
|   version "5.11.2" |   version "5.11.2" | ||||||
|   resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.2.tgz#5b541612fcecdea2a5024a4028da35e4a727bde6" |   resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.2.tgz#5b541612fcecdea2a5024a4028da35e4a727bde6" | ||||||
| @@ -8280,6 +8285,11 @@ react-native-svg@12.1.0: | |||||||
|     css-select "^2.1.0" |     css-select "^2.1.0" | ||||||
|     css-tree "^1.0.0-alpha.39" |     css-tree "^1.0.0-alpha.39" | ||||||
|  |  | ||||||
|  | react-native-tab-view-viewpager-adapter@^1.1.0: | ||||||
|  |   version "1.1.0" | ||||||
|  |   resolved "https://registry.yarnpkg.com/react-native-tab-view-viewpager-adapter/-/react-native-tab-view-viewpager-adapter-1.1.0.tgz#d6e085ed1c91a13e714d87395d428f8afc2b3377" | ||||||
|  |   integrity sha512-KLKw+Ay41xAl5F2eRscJd7DHN8JLx06E2lTQJrYAxtBcNKDKfrcYC3wmq7t72DAOBe/6niF0h44c/t/D/AoXsA== | ||||||
|  |  | ||||||
| react-native-tab-view@^2.15.2: | react-native-tab-view@^2.15.2: | ||||||
|   version "2.15.2" |   version "2.15.2" | ||||||
|   resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-2.15.2.tgz#4bc7832d33a119306614efee667509672a7ee64e" |   resolved "https://registry.yarnpkg.com/react-native-tab-view/-/react-native-tab-view-2.15.2.tgz#4bc7832d33a119306614efee667509672a7ee64e" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user