mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Remove most React memorization
Though added memo for timeline components making them (almost) pure
This commit is contained in:
		| @@ -32,7 +32,7 @@ | ||||
|     "@react-native-community/blur": "^4.3.0", | ||||
|     "@react-native-community/netinfo": "9.3.7", | ||||
|     "@react-native-community/segmented-control": "^2.2.2", | ||||
|     "@react-native-menu/menu": "^0.7.2", | ||||
|     "@react-native-menu/menu": "^0.7.3", | ||||
|     "@react-navigation/bottom-tabs": "^6.5.2", | ||||
|     "@react-navigation/native": "^6.1.1", | ||||
|     "@react-navigation/native-stack": "^6.9.7", | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								src/App.tsx
									
									
									
									
									
								
							| @@ -17,10 +17,7 @@ import { | ||||
|   setAccount, | ||||
|   setGlobalStorage | ||||
| } from '@utils/storage/actions' | ||||
| import { | ||||
|   hasMigratedFromAsyncStorage, | ||||
|   migrateFromAsyncStorage | ||||
| } from '@utils/storage/migrations/toMMKV' | ||||
| import { migrateFromAsyncStorage, versionStorageGlobal } from '@utils/storage/migrations/toMMKV' | ||||
| import ThemeManager from '@utils/styles/ThemeManager' | ||||
| import * as Localization from 'expo-localization' | ||||
| import * as SplashScreen from 'expo-splash-screen' | ||||
| @@ -51,17 +48,15 @@ const App: React.FC = () => { | ||||
|   const [appIsReady, setAppIsReady] = useState(false) | ||||
|   const [localCorrupt, setLocalCorrupt] = useState<string>() | ||||
|  | ||||
|   const [hasMigrated, setHasMigrated] = useState(hasMigratedFromAsyncStorage) | ||||
|   const [hasMigrated, setHasMigrated] = useState<boolean>(versionStorageGlobal !== undefined) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const prepare = async () => { | ||||
|       if (!hasMigrated && !hasMigratedFromAsyncStorage) { | ||||
|       if (!hasMigrated) { | ||||
|         try { | ||||
|           await migrateFromAsyncStorage() | ||||
|           setHasMigrated(true) | ||||
|         } catch (e) { | ||||
|           // TODO: fall back to AsyncStorage? Wipe storage clean and use MMKV? Crash app? | ||||
|         } | ||||
|         } catch {} | ||||
|       } else { | ||||
|         log('log', 'App', 'loading from MMKV') | ||||
|         const account = getGlobalStorage.string('account.active') | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import Icon from '@components/Icon' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useMemo, useState } from 'react' | ||||
| import React, { useState } from 'react' | ||||
| import { AccessibilityProps, Pressable, StyleProp, View, ViewStyle } from 'react-native' | ||||
| import { Flow } from 'react-native-animated-spinkit' | ||||
| import CustomText from './Text' | ||||
| @@ -48,18 +48,16 @@ const Button: React.FC<Props> = ({ | ||||
|   overlay = false, | ||||
|   onPress | ||||
| }) => { | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const loadingSpinkit = useMemo( | ||||
|     () => ( | ||||
|   const loadingSpinkit = () => | ||||
|     loading ? ( | ||||
|       <View style={{ position: 'absolute' }}> | ||||
|         <Flow size={StyleConstants.Font.Size[size]} color={colors.secondary} /> | ||||
|       </View> | ||||
|     ), | ||||
|     [theme] | ||||
|   ) | ||||
|     ) : null | ||||
|  | ||||
|   const mainColor = useMemo(() => { | ||||
|   const mainColor = () => { | ||||
|     if (selected) { | ||||
|       return colors.blue | ||||
|     } else if (overlay) { | ||||
| @@ -73,29 +71,21 @@ const Button: React.FC<Props> = ({ | ||||
|         return colors.primaryDefault | ||||
|       } | ||||
|     } | ||||
|   }, [theme, disabled, loading, selected]) | ||||
|  | ||||
|   const colorBackground = useMemo(() => { | ||||
|     if (overlay) { | ||||
|       return colors.backgroundOverlayInvert | ||||
|     } else { | ||||
|       return colors.backgroundDefault | ||||
|   } | ||||
|   }, [theme]) | ||||
|  | ||||
|   const children = useMemo(() => { | ||||
|   const children = () => { | ||||
|     switch (type) { | ||||
|       case 'icon': | ||||
|         return ( | ||||
|           <> | ||||
|             <Icon | ||||
|               name={content} | ||||
|               color={mainColor} | ||||
|               color={mainColor()} | ||||
|               strokeWidth={strokeWidth} | ||||
|               style={{ opacity: loading ? 0 : 1 }} | ||||
|               size={StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1)} | ||||
|             /> | ||||
|             {loading ? loadingSpinkit : null} | ||||
|             {loadingSpinkit()} | ||||
|           </> | ||||
|         ) | ||||
|       case 'text': | ||||
| @@ -103,7 +93,7 @@ const Button: React.FC<Props> = ({ | ||||
|           <> | ||||
|             <CustomText | ||||
|               style={{ | ||||
|                 color: mainColor, | ||||
|                 color: mainColor(), | ||||
|                 fontSize: StyleConstants.Font.Size[size] * (size === 'L' ? 1.25 : 1), | ||||
|                 opacity: loading ? 0 : 1 | ||||
|               }} | ||||
| @@ -111,11 +101,11 @@ const Button: React.FC<Props> = ({ | ||||
|               children={content} | ||||
|               testID='text' | ||||
|             /> | ||||
|             {loading ? loadingSpinkit : null} | ||||
|             {loadingSpinkit()} | ||||
|           </> | ||||
|         ) | ||||
|     } | ||||
|   }, [theme, content, loading, disabled]) | ||||
|   } | ||||
|  | ||||
|   const [layoutHeight, setLayoutHeight] = useState<number | undefined>() | ||||
|  | ||||
| @@ -136,8 +126,8 @@ const Button: React.FC<Props> = ({ | ||||
|           justifyContent: 'center', | ||||
|           alignItems: 'center', | ||||
|           borderWidth: overlay ? 0 : 1, | ||||
|           borderColor: mainColor, | ||||
|           backgroundColor: colorBackground, | ||||
|           borderColor: mainColor(), | ||||
|           backgroundColor: overlay ? colors.backgroundOverlayInvert : colors.backgroundDefault, | ||||
|           paddingVertical: StyleConstants.Spacing[spacing], | ||||
|           paddingHorizontal: StyleConstants.Spacing[spacing] + StyleConstants.Spacing.XS, | ||||
|           width: round && layoutHeight ? layoutHeight : undefined | ||||
| @@ -149,7 +139,7 @@ const Button: React.FC<Props> = ({ | ||||
|       })} | ||||
|       testID='base' | ||||
|       onPress={onPress} | ||||
|       children={children} | ||||
|       children={children()} | ||||
|       disabled={selected || disabled || loading} | ||||
|     /> | ||||
|   ) | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useMemo, useState } from 'react' | ||||
| import React, { useState } from 'react' | ||||
| import { | ||||
|   AccessibilityProps, | ||||
|   Image, | ||||
| @@ -65,7 +65,7 @@ const GracefullyImage = ({ | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const blurhashView = useMemo(() => { | ||||
|   const blurhashView = () => { | ||||
|     if (hidden || !imageLoaded) { | ||||
|       if (blurhash) { | ||||
|         return <Blurhash decodeAsync blurhash={blurhash} style={styles.placeholder} /> | ||||
| @@ -75,7 +75,7 @@ const GracefullyImage = ({ | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   }, [hidden, imageLoaded]) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
| @@ -98,7 +98,7 @@ const GracefullyImage = ({ | ||||
|         style={[{ flex: 1 }, imageStyle]} | ||||
|         onLoad={onLoad} | ||||
|       /> | ||||
|       {blurhashView} | ||||
|       {blurhashView()} | ||||
|     </Pressable> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { StackNavigationProp } from '@react-navigation/stack' | ||||
| import { TabLocalStackParamList } from '@utils/navigation/navigators' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { PropsWithChildren, useCallback, useState } from 'react' | ||||
| import React, { PropsWithChildren, useState } from 'react' | ||||
| import { Dimensions, Pressable, View } from 'react-native' | ||||
| import Sparkline from './Sparkline' | ||||
| import CustomText from './Text' | ||||
| @@ -21,9 +21,9 @@ const ComponentHashtag: React.FC<PropsWithChildren & Props> = ({ | ||||
|   const { colors } = useTheme() | ||||
|   const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|   const onPress = () => { | ||||
|     navigation.push('Tab-Shared-Hashtag', { hashtag: hashtag.name }) | ||||
|   }, []) | ||||
|   } | ||||
|  | ||||
|   const padding = StyleConstants.Spacing.Global.PagePadding | ||||
|   const width = Dimensions.get('window').width / 4 | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import Icon from '@components/Icon' | ||||
| import CustomText from '@components/Text' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useMemo } from 'react' | ||||
| import React from 'react' | ||||
| import { Pressable } from 'react-native' | ||||
|  | ||||
| export interface Props { | ||||
| @@ -21,9 +21,9 @@ const HeaderLeft: React.FC<Props> = ({ | ||||
|   background = false, | ||||
|   onPress | ||||
| }) => { | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const children = useMemo(() => { | ||||
|   const children = () => { | ||||
|     switch (type) { | ||||
|       case 'icon': | ||||
|         return ( | ||||
| @@ -35,31 +35,23 @@ const HeaderLeft: React.FC<Props> = ({ | ||||
|         ) | ||||
|       case 'text': | ||||
|         return ( | ||||
|           <CustomText | ||||
|             fontStyle='M' | ||||
|             style={{ color: colors.primaryDefault }} | ||||
|             children={content} | ||||
|           /> | ||||
|           <CustomText fontStyle='M' style={{ color: colors.primaryDefault }} children={content} /> | ||||
|         ) | ||||
|     } | ||||
|   }, [theme]) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
|       onPress={onPress} | ||||
|       children={children} | ||||
|       children={children()} | ||||
|       style={{ | ||||
|         flexDirection: 'row', | ||||
|         justifyContent: 'center', | ||||
|         alignItems: 'center', | ||||
|         backgroundColor: background | ||||
|           ? colors.backgroundOverlayDefault | ||||
|           : undefined, | ||||
|         backgroundColor: background ? colors.backgroundOverlayDefault : undefined, | ||||
|         minHeight: 44, | ||||
|         minWidth: 44, | ||||
|         marginLeft: native | ||||
|           ? -StyleConstants.Spacing.S | ||||
|           : StyleConstants.Spacing.S, | ||||
|         marginLeft: native ? -StyleConstants.Spacing.S : StyleConstants.Spacing.S, | ||||
|         ...(type === 'icon' && { | ||||
|           borderRadius: 100 | ||||
|         }), | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import Icon from '@components/Icon' | ||||
| import CustomText from '@components/Text' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useMemo } from 'react' | ||||
| import React from 'react' | ||||
| import { AccessibilityProps, Pressable, View } from 'react-native' | ||||
| import { Flow } from 'react-native-animated-spinkit' | ||||
|  | ||||
| @@ -40,16 +40,14 @@ const HeaderRight: React.FC<Props> = ({ | ||||
| }) => { | ||||
|   const { colors, theme } = useTheme() | ||||
|  | ||||
|   const loadingSpinkit = useMemo( | ||||
|     () => ( | ||||
|   const loadingSpinkit = () => | ||||
|     loading ? ( | ||||
|       <View style={{ position: 'absolute' }}> | ||||
|         <Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> | ||||
|       </View> | ||||
|     ), | ||||
|     [theme] | ||||
|   ) | ||||
|     ) : null | ||||
|  | ||||
|   const children = useMemo(() => { | ||||
|   const children = () => { | ||||
|     switch (type) { | ||||
|       case 'icon': | ||||
|         return ( | ||||
| @@ -60,7 +58,7 @@ const HeaderRight: React.FC<Props> = ({ | ||||
|               size={StyleConstants.Spacing.M * 1.25} | ||||
|               color={disabled ? colors.secondary : destructive ? colors.red : colors.primaryDefault} | ||||
|             /> | ||||
|             {loading && loadingSpinkit} | ||||
|             {loadingSpinkit()} | ||||
|           </> | ||||
|         ) | ||||
|       case 'text': | ||||
| @@ -79,11 +77,11 @@ const HeaderRight: React.FC<Props> = ({ | ||||
|               }} | ||||
|               children={content} | ||||
|             /> | ||||
|             {loading && loadingSpinkit} | ||||
|             {loadingSpinkit()} | ||||
|           </> | ||||
|         ) | ||||
|     } | ||||
|   }, [theme, loading, disabled]) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <Pressable | ||||
| @@ -92,7 +90,7 @@ const HeaderRight: React.FC<Props> = ({ | ||||
|       accessibilityRole='button' | ||||
|       accessibilityState={accessibilityState} | ||||
|       onPress={onPress} | ||||
|       children={children} | ||||
|       children={children()} | ||||
|       disabled={disabled || loading} | ||||
|       style={{ | ||||
|         flexDirection: 'row', | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { useAccessibility } from '@utils/accessibility/AccessibilityManager' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { ColorDefinitions } from '@utils/styles/themes' | ||||
| import React, { useMemo } from 'react' | ||||
| import React from 'react' | ||||
| import { View } from 'react-native' | ||||
| import { Flow } from 'react-native-animated-spinkit' | ||||
| import { State, Switch, TapGestureHandler } from 'react-native-gesture-handler' | ||||
| @@ -47,15 +47,6 @@ const MenuRow: React.FC<Props> = ({ | ||||
|   const { colors, theme } = useTheme() | ||||
|   const { screenReaderEnabled } = useAccessibility() | ||||
|  | ||||
|   const loadingSpinkit = useMemo( | ||||
|     () => ( | ||||
|       <View style={{ position: 'absolute' }}> | ||||
|         <Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> | ||||
|       </View> | ||||
|     ), | ||||
|     [theme] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <View | ||||
|       style={{ minHeight: 50 }} | ||||
| @@ -157,7 +148,11 @@ const MenuRow: React.FC<Props> = ({ | ||||
|                     style={{ marginLeft: 8, opacity: loading ? 0 : 1 }} | ||||
|                   /> | ||||
|                 ) : null} | ||||
|                 {loading && loadingSpinkit} | ||||
|                 {loading ? ( | ||||
|                   <View style={{ position: 'absolute' }}> | ||||
|                     <Flow size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> | ||||
|                   </View> | ||||
|                 ) : null} | ||||
|               </View> | ||||
|             ) : null} | ||||
|           </View> | ||||
|   | ||||
| @@ -20,8 +20,14 @@ export interface Props { | ||||
|   style?: TextStyle | ||||
| } | ||||
|  | ||||
| const ParseEmojis = React.memo( | ||||
|   ({ content, emojis, size = 'M', adaptiveSize = false, fontBold = false, style }: Props) => { | ||||
| const ParseEmojis: React.FC<Props> = ({ | ||||
|   content, | ||||
|   emojis, | ||||
|   size = 'M', | ||||
|   adaptiveSize = false, | ||||
|   fontBold = false, | ||||
|   style | ||||
| }) => { | ||||
|   if (!content) return null | ||||
|  | ||||
|   const { reduceMotionEnabled } = useAccessibility() | ||||
| @@ -93,8 +99,6 @@ const ParseEmojis = React.memo( | ||||
|       )} | ||||
|     </CustomText> | ||||
|   ) | ||||
|   }, | ||||
|   (prev, next) => prev.content === next.content && prev.style?.color === next.style?.color | ||||
| ) | ||||
| } | ||||
|  | ||||
| export default ParseEmojis | ||||
|   | ||||
| @@ -34,8 +34,7 @@ export interface Props { | ||||
|   setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>> | ||||
| } | ||||
|  | ||||
| const ParseHTML = React.memo( | ||||
|   ({ | ||||
| const ParseHTML: React.FC<Props> = ({ | ||||
|   content, | ||||
|   size = 'M', | ||||
|   textStyles, | ||||
| @@ -50,7 +49,7 @@ const ParseHTML = React.memo( | ||||
|   disableDetails = false, | ||||
|   selectable = false, | ||||
|   setSpoilerExpanded | ||||
|   }: Props) => { | ||||
| }) => { | ||||
|   const [adaptiveFontsize] = useGlobalStorage.number('app.font_size') | ||||
|   const adaptedFontsize = adaptiveScale( | ||||
|     StyleConstants.Font.Size[size], | ||||
| @@ -143,8 +142,7 @@ const ParseHTML = React.memo( | ||||
|               } | ||||
|               if (classes.includes('mention') && mentions?.length) { | ||||
|                 const mentionIndex = mentions.findIndex(mention => mention.url === href) | ||||
|                   const paramsAccount = (params as { account: Mastodon.Account } | undefined) | ||||
|                     ?.account | ||||
|                 const paramsAccount = (params as { account: Mastodon.Account } | undefined)?.account | ||||
|                 const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id | ||||
|                 return ( | ||||
|                   <Text | ||||
| @@ -263,8 +261,6 @@ const ParseHTML = React.memo( | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
|   }, | ||||
|   (prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis) | ||||
| ) | ||||
| } | ||||
|  | ||||
| export default ParseHTML | ||||
|   | ||||
| @@ -115,4 +115,4 @@ const TimelineConversation: React.FC<Props> = ({ conversation, queryKey, highlig | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TimelineConversation | ||||
| export default React.memo(TimelineConversation, () => true) | ||||
|   | ||||
| @@ -221,4 +221,4 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TimelineDefault | ||||
| export default React.memo(TimelineDefault, () => true) | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import { QueryKeyTimeline } from '@utils/queryHooks/timeline' | ||||
| import { useAccountStorage } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { Fragment, useCallback, useState } from 'react' | ||||
| import React, { Fragment, useState } from 'react' | ||||
| import { Pressable, View } from 'react-native' | ||||
| import * as ContextMenu from 'zeego/context-menu' | ||||
| import StatusContext from './Shared/Context' | ||||
| @@ -53,14 +53,6 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => { | ||||
|   const { colors } = useTheme() | ||||
|   const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>() | ||||
|  | ||||
|   const onPress = useCallback(() => { | ||||
|     notification.status && | ||||
|       navigation.push('Tab-Shared-Toot', { | ||||
|         toot: notification.status, | ||||
|         rootQueryKey: queryKey | ||||
|       }) | ||||
|   }, []) | ||||
|  | ||||
|   const main = () => { | ||||
|     return ( | ||||
|       <> | ||||
| @@ -159,7 +151,13 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => { | ||||
|               backgroundColor: colors.backgroundDefault, | ||||
|               paddingBottom: notification.status ? 0 : StyleConstants.Spacing.Global.PagePadding | ||||
|             }} | ||||
|             onPress={onPress} | ||||
|             onPress={() => | ||||
|               notification.status && | ||||
|               navigation.push('Tab-Shared-Toot', { | ||||
|                 toot: notification.status, | ||||
|                 rootQueryKey: queryKey | ||||
|               }) | ||||
|             } | ||||
|             onLongPress={() => {}} | ||||
|             children={main()} | ||||
|           /> | ||||
| @@ -187,4 +185,4 @@ const TimelineNotifications: React.FC<Props> = ({ notification, queryKey }) => { | ||||
|   ) | ||||
| } | ||||
|  | ||||
| export default TimelineNotifications | ||||
| export default React.memo(TimelineNotifications, () => true) | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import { useAccountStorage } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { uniqBy } from 'lodash' | ||||
| import React, { useCallback, useContext, useMemo } from 'react' | ||||
| import React, { useContext } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Pressable, StyleSheet, View } from 'react-native' | ||||
| import StatusContext from './Context' | ||||
| @@ -76,7 +76,7 @@ const TimelineActions: React.FC = () => { | ||||
|   }) | ||||
|  | ||||
|   const [accountId] = useAccountStorage.string('auth.account.id') | ||||
|   const onPressReply = useCallback(() => { | ||||
|   const onPressReply = () => { | ||||
|     const accts = uniqBy( | ||||
|       ([status.account] as Mastodon.Account[] & Mastodon.Mention[]) | ||||
|         .concat(status.mentions) | ||||
| @@ -89,9 +89,9 @@ const TimelineActions: React.FC = () => { | ||||
|       accts, | ||||
|       queryKey | ||||
|     }) | ||||
|   }, [status.replies_count]) | ||||
|   } | ||||
|   const { showActionSheetWithOptions } = useActionSheet() | ||||
|   const onPressReblog = useCallback(() => { | ||||
|   const onPressReblog = () => { | ||||
|     if (!status.reblogged) { | ||||
|       showActionSheetWithOptions( | ||||
|         { | ||||
| @@ -157,8 +157,8 @@ const TimelineActions: React.FC = () => { | ||||
|         } | ||||
|       }) | ||||
|     } | ||||
|   }, [status.reblogged, status.reblogs_count]) | ||||
|   const onPressFavourite = useCallback(() => { | ||||
|   } | ||||
|   const onPressFavourite = () => { | ||||
|     mutation.mutate({ | ||||
|       type: 'updateStatusProperty', | ||||
|       queryKey, | ||||
| @@ -172,8 +172,8 @@ const TimelineActions: React.FC = () => { | ||||
|         countValue: status.favourites_count | ||||
|       } | ||||
|     }) | ||||
|   }, [status.favourited, status.favourites_count]) | ||||
|   const onPressBookmark = useCallback(() => { | ||||
|   } | ||||
|   const onPressBookmark = () => { | ||||
|     mutation.mutate({ | ||||
|       type: 'updateStatusProperty', | ||||
|       queryKey, | ||||
| @@ -187,10 +187,9 @@ const TimelineActions: React.FC = () => { | ||||
|         countValue: undefined | ||||
|       } | ||||
|     }) | ||||
|   }, [status.bookmarked]) | ||||
|   } | ||||
|  | ||||
|   const childrenReply = useMemo( | ||||
|     () => ( | ||||
|   const childrenReply = () => ( | ||||
|     <> | ||||
|       <Icon name='MessageCircle' color={iconColor} size={StyleConstants.Font.Size.L} /> | ||||
|       {status.replies_count > 0 ? ( | ||||
| @@ -205,10 +204,8 @@ const TimelineActions: React.FC = () => { | ||||
|         </CustomText> | ||||
|       ) : null} | ||||
|     </> | ||||
|     ), | ||||
|     [status.replies_count] | ||||
|   ) | ||||
|   const childrenReblog = useMemo(() => { | ||||
|   const childrenReblog = () => { | ||||
|     const color = (state: boolean) => (state ? colors.green : colors.secondary) | ||||
|     const disabled = | ||||
|       status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount) | ||||
| @@ -236,8 +233,8 @@ const TimelineActions: React.FC = () => { | ||||
|         ) : null} | ||||
|       </> | ||||
|     ) | ||||
|   }, [status.reblogged, status.reblogs_count]) | ||||
|   const childrenFavourite = useMemo(() => { | ||||
|   } | ||||
|   const childrenFavourite = () => { | ||||
|     const color = (state: boolean) => (state ? colors.red : colors.secondary) | ||||
|     return ( | ||||
|       <> | ||||
| @@ -256,13 +253,13 @@ const TimelineActions: React.FC = () => { | ||||
|         ) : null} | ||||
|       </> | ||||
|     ) | ||||
|   }, [status.favourited, status.favourites_count]) | ||||
|   const childrenBookmark = useMemo(() => { | ||||
|   } | ||||
|   const childrenBookmark = () => { | ||||
|     const color = (state: boolean) => (state ? colors.yellow : colors.secondary) | ||||
|     return ( | ||||
|       <Icon name='Bookmark' color={color(status.bookmarked)} size={StyleConstants.Font.Size.L} /> | ||||
|     ) | ||||
|   }, [status.bookmarked]) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <View style={{ flexDirection: 'row' }}> | ||||
| @@ -275,7 +272,7 @@ const TimelineActions: React.FC = () => { | ||||
|           : { accessibilityLabel: '' })} | ||||
|         style={styles.action} | ||||
|         onPress={onPressReply} | ||||
|         children={childrenReply} | ||||
|         children={childrenReply()} | ||||
|       /> | ||||
|  | ||||
|       <Pressable | ||||
| @@ -289,7 +286,7 @@ const TimelineActions: React.FC = () => { | ||||
|           : { accessibilityLabel: '' })} | ||||
|         style={styles.action} | ||||
|         onPress={onPressReblog} | ||||
|         children={childrenReblog} | ||||
|         children={childrenReblog()} | ||||
|         disabled={ | ||||
|           status.visibility === 'direct' || (status.visibility === 'private' && !ownAccount) | ||||
|         } | ||||
| @@ -306,7 +303,7 @@ const TimelineActions: React.FC = () => { | ||||
|           : { accessibilityLabel: '' })} | ||||
|         style={styles.action} | ||||
|         onPress={onPressFavourite} | ||||
|         children={childrenFavourite} | ||||
|         children={childrenFavourite()} | ||||
|       /> | ||||
|  | ||||
|       <Pressable | ||||
| @@ -320,7 +317,7 @@ const TimelineActions: React.FC = () => { | ||||
|           : { accessibilityLabel: '' })} | ||||
|         style={styles.action} | ||||
|         onPress={onPressBookmark} | ||||
|         children={childrenBookmark} | ||||
|         children={childrenBookmark()} | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import updateStatusProperty from '@utils/queryHooks/timeline/updateStatusPropert | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { maxBy } from 'lodash' | ||||
| import React, { useCallback, useContext, useMemo, useState } from 'react' | ||||
| import React, { useContext, useState } from 'react' | ||||
| import { Trans, useTranslation } from 'react-i18next' | ||||
| import { Pressable, View } from 'react-native' | ||||
| import StatusContext from './Context' | ||||
| @@ -73,7 +73,7 @@ const TimelinePoll: React.FC = () => { | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   const pollButton = useMemo(() => { | ||||
|   const pollButton = () => { | ||||
|     if (!poll.expired) { | ||||
|       if (!ownAccount && !poll.voted) { | ||||
|         return ( | ||||
| @@ -127,17 +127,14 @@ const TimelinePoll: React.FC = () => { | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|   }, [theme, poll.expired, poll.voted, allOptions, mutation.isLoading]) | ||||
|   } | ||||
|  | ||||
|   const isSelected = useCallback( | ||||
|     (index: number): string => | ||||
|   const isSelected = (index: number): string => | ||||
|     allOptions[index] | ||||
|       ? `Check${poll.multiple ? 'Square' : 'Circle'}` | ||||
|         : `${poll.multiple ? 'Square' : 'Circle'}`, | ||||
|     [allOptions] | ||||
|   ) | ||||
|       : `${poll.multiple ? 'Square' : 'Circle'}` | ||||
|  | ||||
|   const pollBodyDisallow = useMemo(() => { | ||||
|   const pollBodyDisallow = () => { | ||||
|     const maxValue = maxBy(poll.options, option => option.votes_count)?.votes_count | ||||
|     return poll.options.map((option, index) => ( | ||||
|       <View key={index} style={{ flex: 1, paddingVertical: StyleConstants.Spacing.S }}> | ||||
| @@ -191,8 +188,8 @@ const TimelinePoll: React.FC = () => { | ||||
|         /> | ||||
|       </View> | ||||
|     )) | ||||
|   }, [theme, poll.options]) | ||||
|   const pollBodyAllow = useMemo(() => { | ||||
|   } | ||||
|   const pollBodyAllow = () => { | ||||
|     return poll.options.map((option, index) => ( | ||||
|       <Pressable | ||||
|         key={index} | ||||
| @@ -229,7 +226,7 @@ const TimelinePoll: React.FC = () => { | ||||
|         </View> | ||||
|       </Pressable> | ||||
|     )) | ||||
|   }, [theme, allOptions]) | ||||
|   } | ||||
|  | ||||
|   const pollVoteCounts = () => { | ||||
|     if (poll.voters_count !== null) { | ||||
| @@ -263,7 +260,7 @@ const TimelinePoll: React.FC = () => { | ||||
|  | ||||
|   return ( | ||||
|     <View style={{ marginTop: StyleConstants.Spacing.M }}> | ||||
|       {poll.expired || poll.voted ? pollBodyDisallow : pollBodyAllow} | ||||
|       {poll.expired || poll.voted ? pollBodyDisallow() : pollBodyAllow()} | ||||
|       <View | ||||
|         style={{ | ||||
|           flex: 1, | ||||
| @@ -272,7 +269,7 @@ const TimelinePoll: React.FC = () => { | ||||
|           marginTop: StyleConstants.Spacing.XS | ||||
|         }} | ||||
|       > | ||||
|         {pollButton} | ||||
|         {pollButton()} | ||||
|         <CustomText fontStyle='S' style={{ flexShrink: 1, color: colors.secondary }}> | ||||
|           {pollVoteCounts()} | ||||
|           {pollExpiration()} | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { useGlobalStorageListener } from '@utils/storage/actions' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { RefObject, useCallback, useRef } from 'react' | ||||
| import React, { RefObject, useRef } from 'react' | ||||
| import { FlatList, FlatListProps, Platform, RefreshControl } from 'react-native' | ||||
| import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' | ||||
| import TimelineEmpty from './Empty' | ||||
| @@ -56,11 +56,6 @@ const Timeline: React.FC<Props> = ({ | ||||
|  | ||||
|   const flattenData = data?.pages ? data.pages?.flatMap(page => [...page.body]) : [] | ||||
|  | ||||
|   const onEndReached = useCallback( | ||||
|     () => !disableInfinity && !isFetchingNextPage && fetchNextPage(), | ||||
|     [isFetchingNextPage] | ||||
|   ) | ||||
|  | ||||
|   const flRef = useRef<FlatList>(null) | ||||
|  | ||||
|   const scrollY = useSharedValue(0) | ||||
| @@ -120,7 +115,7 @@ const Timeline: React.FC<Props> = ({ | ||||
|         data={flattenData} | ||||
|         initialNumToRender={6} | ||||
|         maxToRenderPerBatch={3} | ||||
|         onEndReached={onEndReached} | ||||
|         onEndReached={() => !disableInfinity && !isFetchingNextPage && fetchNextPage()} | ||||
|         onEndReachedThreshold={0.75} | ||||
|         ListFooterComponent={ | ||||
|           <TimelineFooter queryKey={queryKey} disableInfinity={disableInfinity} /> | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { RootStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect } from 'react' | ||||
| import React, { useEffect } from 'react' | ||||
| import { Dimensions, StyleSheet, View } from 'react-native' | ||||
| import { PanGestureHandler, State, TapGestureHandler } from 'react-native-gesture-handler' | ||||
| import Animated, { | ||||
| @@ -34,9 +34,8 @@ const ScreenActions = ({ | ||||
|       bottom: interpolate(panY.value, [0, screenHeight], [0, -screenHeight], Extrapolate.CLAMP) | ||||
|     } | ||||
|   }) | ||||
|   const dismiss = useCallback(() => { | ||||
|     navigation.goBack() | ||||
|   }, []) | ||||
|   const dismiss = () => navigation.goBack() | ||||
|  | ||||
|   const onGestureEvent = useAnimatedGestureHandler({ | ||||
|     onActive: ({ translationY }) => { | ||||
|       panY.value = translationY | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { RootStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { useAnnouncementMutation, useAnnouncementQuery } from '@utils/queryHooks/announcement' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useState } from 'react' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import { Trans, useTranslation } from 'react-i18next' | ||||
| import { | ||||
|   Dimensions, | ||||
| @@ -56,8 +56,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'> | ||||
|     } | ||||
|   }, [query.data]) | ||||
|  | ||||
|   const renderItem = useCallback( | ||||
|     ({ item, index }: { item: Mastodon.Announcement; index: number }) => ( | ||||
|   const renderItem = ({ item, index }: { item: Mastodon.Announcement; index: number }) => ( | ||||
|     <View | ||||
|       key={index} | ||||
|       style={{ | ||||
| @@ -181,23 +180,16 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'> | ||||
|         /> | ||||
|       </View> | ||||
|     </View> | ||||
|     ), | ||||
|     [mode] | ||||
|   ) | ||||
|  | ||||
|   const onMomentumScrollEnd = useCallback( | ||||
|     ({ | ||||
|   const onMomentumScrollEnd = ({ | ||||
|     nativeEvent: { | ||||
|       contentOffset: { x }, | ||||
|       layoutMeasurement: { width } | ||||
|     } | ||||
|     }: NativeSyntheticEvent<NativeScrollEvent>) => { | ||||
|       setIndex(Math.floor(x / width)) | ||||
|     }, | ||||
|     [] | ||||
|   ) | ||||
|   }: NativeSyntheticEvent<NativeScrollEvent>) => setIndex(Math.floor(x / width)) | ||||
|  | ||||
|   const ListEmptyComponent = useCallback(() => { | ||||
|   const ListEmptyComponent = () => { | ||||
|     return ( | ||||
|       <View | ||||
|         style={{ | ||||
| @@ -209,7 +201,7 @@ const ScreenAnnouncements: React.FC<RootStackScreenProps<'Screen-Announcements'> | ||||
|         <Circle size={StyleConstants.Font.Size.L} color={colors.secondary} /> | ||||
|       </View> | ||||
|     ) | ||||
|   }, []) | ||||
|   } | ||||
|  | ||||
|   return Platform.OS === 'ios' ? ( | ||||
|     <BlurView | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useContext, useMemo } from 'react' | ||||
| import React, { useContext } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Keyboard, Pressable, StyleSheet, View } from 'react-native' | ||||
| import ComposeContext from '../utils/createContext' | ||||
| @@ -18,7 +18,7 @@ const ComposeActions: React.FC = () => { | ||||
|   const { t } = useTranslation(['common', 'screenCompose']) | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const attachmentColor = useMemo(() => { | ||||
|   const attachmentColor = () => { | ||||
|     if (composeState.poll.active) return colors.disabled | ||||
|  | ||||
|     if (composeState.attachments.uploads.length) { | ||||
| @@ -26,7 +26,7 @@ const ComposeActions: React.FC = () => { | ||||
|     } else { | ||||
|       return colors.secondary | ||||
|     } | ||||
|   }, [composeState.poll.active, composeState.attachments.uploads]) | ||||
|   } | ||||
|   const attachmentOnPress = () => { | ||||
|     if (composeState.poll.active) return | ||||
|  | ||||
| @@ -35,7 +35,7 @@ const ComposeActions: React.FC = () => { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const pollColor = useMemo(() => { | ||||
|   const pollColor = () => { | ||||
|     if (composeState.attachments.uploads.length) return colors.disabled | ||||
|  | ||||
|     if (composeState.poll.active) { | ||||
| @@ -43,7 +43,7 @@ const ComposeActions: React.FC = () => { | ||||
|     } else { | ||||
|       return colors.secondary | ||||
|     } | ||||
|   }, [composeState.poll.active, composeState.attachments.uploads]) | ||||
|   } | ||||
|   const pollOnPress = () => { | ||||
|     if (!composeState.attachments.uploads.length) { | ||||
|       layoutAnimation() | ||||
| @@ -57,7 +57,7 @@ const ComposeActions: React.FC = () => { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const visibilityIcon = useMemo(() => { | ||||
|   const visibilityIcon = () => { | ||||
|     switch (composeState.visibility) { | ||||
|       case 'public': | ||||
|         return 'Globe' | ||||
| @@ -68,7 +68,7 @@ const ComposeActions: React.FC = () => { | ||||
|       case 'direct': | ||||
|         return 'Mail' | ||||
|     } | ||||
|   }, [composeState.visibility]) | ||||
|   } | ||||
|   const visibilityOnPress = () => { | ||||
|     if (!composeState.visibilityLock) { | ||||
|       showActionSheetWithOptions( | ||||
| @@ -116,7 +116,7 @@ const ComposeActions: React.FC = () => { | ||||
|   } | ||||
|  | ||||
|   const { emojisState, emojisDispatch } = useContext(EmojisContext) | ||||
|   const emojiColor = useMemo(() => { | ||||
|   const emojiColor = () => { | ||||
|     if (!emojis.current?.length) return colors.disabled | ||||
|  | ||||
|     if (emojisState.targetIndex !== -1) { | ||||
| @@ -124,7 +124,7 @@ const ComposeActions: React.FC = () => { | ||||
|     } else { | ||||
|       return colors.secondary | ||||
|     } | ||||
|   }, [emojis.current?.length, emojisState.targetIndex]) | ||||
|   } | ||||
|   const emojiOnPress = () => { | ||||
|     if (emojisState.targetIndex === -1) { | ||||
|       Keyboard.dismiss() | ||||
| @@ -159,7 +159,7 @@ const ComposeActions: React.FC = () => { | ||||
|         }} | ||||
|         style={styles.button} | ||||
|         onPress={attachmentOnPress} | ||||
|         children={<Icon name='Camera' size={24} color={attachmentColor} />} | ||||
|         children={<Icon name='Camera' size={24} color={attachmentColor()} />} | ||||
|       /> | ||||
|       <Pressable | ||||
|         accessibilityRole='button' | ||||
| @@ -171,7 +171,7 @@ const ComposeActions: React.FC = () => { | ||||
|         }} | ||||
|         style={styles.button} | ||||
|         onPress={pollOnPress} | ||||
|         children={<Icon name='BarChart2' size={24} color={pollColor} />} | ||||
|         children={<Icon name='BarChart2' size={24} color={pollColor()} />} | ||||
|       /> | ||||
|       <Pressable | ||||
|         accessibilityRole='button' | ||||
| @@ -183,7 +183,7 @@ const ComposeActions: React.FC = () => { | ||||
|         onPress={visibilityOnPress} | ||||
|         children={ | ||||
|           <Icon | ||||
|             name={visibilityIcon} | ||||
|             name={visibilityIcon()} | ||||
|             size={24} | ||||
|             color={composeState.visibilityLock ? colors.disabled : colors.secondary} | ||||
|           /> | ||||
| @@ -213,7 +213,7 @@ const ComposeActions: React.FC = () => { | ||||
|         }} | ||||
|         style={styles.button} | ||||
|         onPress={emojiOnPress} | ||||
|         children={<Icon name='Smile' size={24} color={emojiColor} />} | ||||
|         children={<Icon name='Smile' size={24} color={emojiColor()} />} | ||||
|       /> | ||||
|     </View> | ||||
|   ) | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import { useNavigation } from '@react-navigation/native' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import layoutAnimation from '@utils/styles/layoutAnimation' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { RefObject, useCallback, useContext, useEffect, useMemo, useRef } from 'react' | ||||
| import React, { RefObject, useContext, useEffect, useRef } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { FlatList, Pressable, StyleSheet, View } from 'react-native' | ||||
| import { Circle } from 'react-native-animated-spinkit' | ||||
| @@ -32,16 +32,13 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|  | ||||
|   const flatListRef = useRef<FlatList>(null) | ||||
|  | ||||
|   const sensitiveOnPress = useCallback( | ||||
|     () => | ||||
|   const sensitiveOnPress = () => | ||||
|     composeDispatch({ | ||||
|       type: 'attachments/sensitive', | ||||
|       payload: { sensitive: !composeState.attachments.sensitive } | ||||
|       }), | ||||
|     [composeState.attachments.sensitive] | ||||
|   ) | ||||
|     }) | ||||
|  | ||||
|   const calculateWidth = useCallback((item: ExtendedAttachment) => { | ||||
|   const calculateWidth = (item: ExtendedAttachment) => { | ||||
|     if (item.local) { | ||||
|       return ((item.local.width || 100) / (item.local.height || 100)) * DEFAULT_HEIGHT | ||||
|     } else { | ||||
| @@ -59,9 +56,9 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|         return DEFAULT_HEIGHT | ||||
|       } | ||||
|     } | ||||
|   }, []) | ||||
|   } | ||||
|  | ||||
|   const snapToOffsets = useMemo(() => { | ||||
|   const snapToOffsets = () => { | ||||
|     const attachmentsOffsets = composeState.attachments.uploads.map((_, index) => { | ||||
|       let currentOffset = 0 | ||||
|       Array.from(Array(index).keys()).map( | ||||
| @@ -81,19 +78,19 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|             StyleConstants.Spacing.Global.PagePadding | ||||
|         ] | ||||
|       : attachmentsOffsets | ||||
|   }, [composeState.attachments.uploads.length]) | ||||
|   } | ||||
|   let prevOffsets = useRef<number[]>() | ||||
|   useEffect(() => { | ||||
|     if (snapToOffsets.length > (prevOffsets.current ? prevOffsets.current.length : 0)) { | ||||
|     const snap = snapToOffsets() | ||||
|     if (snap.length > (prevOffsets.current ? prevOffsets.current.length : 0)) { | ||||
|       flatListRef.current?.scrollToOffset({ | ||||
|         offset: snapToOffsets[snapToOffsets.length - 2] + snapToOffsets[snapToOffsets.length - 1] | ||||
|         offset: snap[snapToOffsets.length - 2] + snap[snapToOffsets.length - 1] | ||||
|       }) | ||||
|     } | ||||
|     prevOffsets.current = snapToOffsets | ||||
|     prevOffsets.current = snap | ||||
|   }, [snapToOffsets, prevOffsets.current]) | ||||
|  | ||||
|   const renderAttachment = useCallback( | ||||
|     ({ item, index }: { item: ExtendedAttachment; index: number }) => { | ||||
|   const renderAttachment = ({ item, index }: { item: ExtendedAttachment; index: number }) => { | ||||
|     return ( | ||||
|       <View | ||||
|         key={index} | ||||
| @@ -189,52 +186,8 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|         )} | ||||
|       </View> | ||||
|     ) | ||||
|     }, | ||||
|     [] | ||||
|   ) | ||||
|   } | ||||
|  | ||||
|   const listFooter = useMemo( | ||||
|     () => ( | ||||
|       <Pressable | ||||
|         accessible | ||||
|         accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')} | ||||
|         style={{ | ||||
|           height: DEFAULT_HEIGHT, | ||||
|           marginLeft: StyleConstants.Spacing.Global.PagePadding, | ||||
|           marginTop: StyleConstants.Spacing.Global.PagePadding, | ||||
|           marginBottom: StyleConstants.Spacing.Global.PagePadding, | ||||
|           width: DEFAULT_HEIGHT, | ||||
|           backgroundColor: colors.backgroundOverlayInvert | ||||
|         }} | ||||
|         onPress={async () => { | ||||
|           await chooseAndUploadAttachment({ | ||||
|             composeDispatch, | ||||
|             showActionSheetWithOptions | ||||
|           }) | ||||
|         }} | ||||
|       > | ||||
|         <Button | ||||
|           type='icon' | ||||
|           content='UploadCloud' | ||||
|           spacing='M' | ||||
|           round | ||||
|           overlay | ||||
|           onPress={async () => { | ||||
|             await chooseAndUploadAttachment({ | ||||
|               composeDispatch, | ||||
|               showActionSheetWithOptions | ||||
|             }) | ||||
|           }} | ||||
|           style={{ | ||||
|             position: 'absolute', | ||||
|             top: (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2, | ||||
|             left: (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2 | ||||
|           }} | ||||
|         /> | ||||
|       </Pressable> | ||||
|     ), | ||||
|     [] | ||||
|   ) | ||||
|   return ( | ||||
|     <View | ||||
|       style={{ | ||||
| @@ -276,13 +229,54 @@ const ComposeAttachments: React.FC<Props> = ({ accessibleRefAttachments }) => { | ||||
|         pagingEnabled={false} | ||||
|         snapToAlignment='center' | ||||
|         renderItem={renderAttachment} | ||||
|         snapToOffsets={snapToOffsets} | ||||
|         snapToOffsets={snapToOffsets()} | ||||
|         keyboardShouldPersistTaps='always' | ||||
|         showsHorizontalScrollIndicator={false} | ||||
|         data={composeState.attachments.uploads} | ||||
|         keyExtractor={item => item.local?.uri || item.remote?.url || Math.random().toString()} | ||||
|         ListFooterComponent={ | ||||
|           composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? listFooter : null | ||||
|           composeState.attachments.uploads.length < MAX_MEDIA_ATTACHMENTS ? ( | ||||
|             <Pressable | ||||
|               accessible | ||||
|               accessibilityLabel={t('content.root.footer.attachments.upload.accessibilityLabel')} | ||||
|               style={{ | ||||
|                 height: DEFAULT_HEIGHT, | ||||
|                 marginLeft: StyleConstants.Spacing.Global.PagePadding, | ||||
|                 marginTop: StyleConstants.Spacing.Global.PagePadding, | ||||
|                 marginBottom: StyleConstants.Spacing.Global.PagePadding, | ||||
|                 width: DEFAULT_HEIGHT, | ||||
|                 backgroundColor: colors.backgroundOverlayInvert | ||||
|               }} | ||||
|               onPress={async () => { | ||||
|                 await chooseAndUploadAttachment({ | ||||
|                   composeDispatch, | ||||
|                   showActionSheetWithOptions | ||||
|                 }) | ||||
|               }} | ||||
|             > | ||||
|               <Button | ||||
|                 type='icon' | ||||
|                 content='UploadCloud' | ||||
|                 spacing='M' | ||||
|                 round | ||||
|                 overlay | ||||
|                 onPress={async () => { | ||||
|                   await chooseAndUploadAttachment({ | ||||
|                     composeDispatch, | ||||
|                     showActionSheetWithOptions | ||||
|                   }) | ||||
|                 }} | ||||
|                 style={{ | ||||
|                   position: 'absolute', | ||||
|                   top: | ||||
|                     (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / | ||||
|                     2, | ||||
|                   left: | ||||
|                     (DEFAULT_HEIGHT - StyleConstants.Spacing.M * 2 - StyleConstants.Font.Size.M) / 2 | ||||
|                 }} | ||||
|               /> | ||||
|             </Pressable> | ||||
|           ) : null | ||||
|         } | ||||
|       /> | ||||
|     </View> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import ComponentSeparator from '@components/Separator' | ||||
| import { useSearchQuery } from '@utils/queryHooks/search' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useContext, useEffect, useMemo, useRef } from 'react' | ||||
| import React, { useContext, useEffect, useRef } from 'react' | ||||
| import { AccessibilityInfo, findNodeHandle, FlatList, View } from 'react-native' | ||||
| import { Circle } from 'react-native-animated-spinkit' | ||||
| import ComposePosting from '../Posting' | ||||
| @@ -53,29 +53,22 @@ const ComposeRoot = () => { | ||||
|     } | ||||
|   }, [composeState.tag]) | ||||
|  | ||||
|   const listEmpty = useMemo(() => { | ||||
|     if (isFetching) { | ||||
|       return ( | ||||
|         <View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}> | ||||
|           <Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> | ||||
|         </View> | ||||
|       ) | ||||
|     } | ||||
|   }, [isFetching]) | ||||
|  | ||||
|   const Footer = useMemo( | ||||
|     () => <ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} />, | ||||
|     [accessibleRefAttachments.current] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <View style={{ flex: 1 }}> | ||||
|       <FlatList | ||||
|         renderItem={({ item }) => <ComposeRootSuggestion item={item} />} | ||||
|         ListEmptyComponent={listEmpty} | ||||
|         ListEmptyComponent={ | ||||
|           isFetching ? ( | ||||
|             <View key='listEmpty' style={{ flex: 1, alignItems: 'center' }}> | ||||
|               <Circle size={StyleConstants.Font.Size.M * 1.25} color={colors.secondary} /> | ||||
|             </View> | ||||
|           ) : null | ||||
|         } | ||||
|         keyboardShouldPersistTaps='always' | ||||
|         ListHeaderComponent={ComposeRootHeader} | ||||
|         ListFooterComponent={Footer} | ||||
|         ListFooterComponent={ | ||||
|           <ComposeRootFooter accessibleRefAttachments={accessibleRefAttachments} /> | ||||
|         } | ||||
|         ItemSeparatorComponent={ComponentSeparator} | ||||
|         // @ts-ignore | ||||
|         data={data ? data[mapSchemaToType()] : undefined} | ||||
|   | ||||
| @@ -21,7 +21,7 @@ import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import * as StoreReview from 'expo-store-review' | ||||
| import { filter } from 'lodash' | ||||
| import React, { useCallback, useEffect, useMemo, useReducer, useState } from 'react' | ||||
| import React, { useEffect, useMemo, useReducer, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Alert, Keyboard, Platform } from 'react-native' | ||||
| import ComposeDraftsList, { removeDraft } from './DraftsList' | ||||
| @@ -202,8 +202,74 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({ | ||||
|     return () => (autoSave ? clearInterval(autoSave) : undefined) | ||||
|   }, [composeState]) | ||||
|  | ||||
|   const headerLeft = useCallback( | ||||
|     () => ( | ||||
|   const headerRightDisabled = () => { | ||||
|     if (totalTextCount > maxTootChars) { | ||||
|       return true | ||||
|     } | ||||
|     if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) { | ||||
|       return true | ||||
|     } | ||||
|     if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) { | ||||
|       return true | ||||
|     } | ||||
|     return false | ||||
|   } | ||||
|   const mutateTimeline = useTimelineMutation({ onMutate: true }) | ||||
|  | ||||
|   const inputProps: EmojisState['inputProps'] = [ | ||||
|     { | ||||
|       value: [ | ||||
|         composeState.text.raw, | ||||
|         content => { | ||||
|           formatText({ textInput: 'text', composeDispatch, content }) | ||||
|         } | ||||
|       ], | ||||
|       selection: [ | ||||
|         composeState.text.selection, | ||||
|         selection => composeDispatch({ type: 'text', payload: { selection } }) | ||||
|       ], | ||||
|       isFocused: composeState.textInputFocus.isFocused.text, | ||||
|       maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0), | ||||
|       ref: composeState.textInputFocus.refs.text | ||||
|     }, | ||||
|     { | ||||
|       value: [ | ||||
|         composeState.spoiler.raw, | ||||
|         content => formatText({ textInput: 'spoiler', composeDispatch, content }) | ||||
|       ], | ||||
|       selection: [ | ||||
|         composeState.spoiler.selection, | ||||
|         selection => composeDispatch({ type: 'spoiler', payload: { selection } }) | ||||
|       ], | ||||
|       isFocused: composeState.textInputFocus.isFocused.spoiler, | ||||
|       maxLength: maxTootChars - composeState.text.count, | ||||
|       ref: composeState.textInputFocus.refs.spoiler | ||||
|     } | ||||
|   ] | ||||
|  | ||||
|   return ( | ||||
|     <ComponentEmojis | ||||
|       inputProps={inputProps} | ||||
|       customButton | ||||
|       customBehavior={Platform.OS === 'ios' ? 'padding' : undefined} | ||||
|       customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']} | ||||
|     > | ||||
|       <ComposeContext.Provider value={{ composeState, composeDispatch }}> | ||||
|         <Stack.Navigator initialRouteName='Screen-Compose-Root'> | ||||
|           <Stack.Screen | ||||
|             name='Screen-Compose-Root' | ||||
|             component={ComposeRoot} | ||||
|             options={{ | ||||
|               title: `${totalTextCount} / ${maxTootChars}`, | ||||
|               headerTitleStyle: { | ||||
|                 fontWeight: | ||||
|                   totalTextCount > maxTootChars | ||||
|                     ? StyleConstants.Font.Weight.Bold | ||||
|                     : StyleConstants.Font.Weight.Normal, | ||||
|                 fontSize: StyleConstants.Font.Size.M | ||||
|               }, | ||||
|               headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary, | ||||
|               headerLeft: () => ( | ||||
|                 <HeaderLeft | ||||
|                   type='text' | ||||
|                   content={t('common:buttons.cancel')} | ||||
| @@ -237,23 +303,7 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({ | ||||
|                   }} | ||||
|                 /> | ||||
|               ), | ||||
|     [composeState] | ||||
|   ) | ||||
|   const headerRightDisabled = useMemo(() => { | ||||
|     if (totalTextCount > maxTootChars) { | ||||
|       return true | ||||
|     } | ||||
|     if (composeState.attachments.uploads.filter(upload => upload.uploading).length > 0) { | ||||
|       return true | ||||
|     } | ||||
|     if (composeState.attachments.uploads.length === 0 && composeState.text.raw.length === 0) { | ||||
|       return true | ||||
|     } | ||||
|     return false | ||||
|   }, [totalTextCount, composeState.attachments.uploads, composeState.text.raw]) | ||||
|   const mutateTimeline = useTimelineMutation({ onMutate: true }) | ||||
|   const headerRight = useCallback( | ||||
|     () => ( | ||||
|               headerRight: () => ( | ||||
|                 <HeaderRight | ||||
|                   type='text' | ||||
|                   content={t( | ||||
| @@ -276,7 +326,9 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({ | ||||
|                         if (Platform.OS === 'ios' && Platform.constants.osVersion === '13.3') { | ||||
|                           // https://github.com/tooot-app/app/issues/59 | ||||
|                         } else { | ||||
|                 const currentCount = getGlobalStorage.number('app.count_till_store_review') | ||||
|                           const currentCount = getGlobalStorage.number( | ||||
|                             'app.count_till_store_review' | ||||
|                           ) | ||||
|                           if (currentCount === 10) { | ||||
|                             StoreReview?.isAvailableAsync() | ||||
|                               .then(() => StoreReview.requestReview()) | ||||
| @@ -332,78 +384,18 @@ const ScreenCompose: React.FC<RootStackScreenProps<'Screen-Compose'>> = ({ | ||||
|                           haptics('Error') | ||||
|                           handleError({ message: 'Posting error', captureResponse: true }) | ||||
|                           composeDispatch({ type: 'posting', payload: false }) | ||||
|                 Alert.alert(t('screenCompose:heading.right.alert.default.title'), undefined, [ | ||||
|                   { text: t('screenCompose:heading.right.alert.default.button') } | ||||
|                 ]) | ||||
|                           Alert.alert( | ||||
|                             t('screenCompose:heading.right.alert.default.title'), | ||||
|                             undefined, | ||||
|                             [{ text: t('screenCompose:heading.right.alert.default.button') }] | ||||
|                           ) | ||||
|                         } | ||||
|                       }) | ||||
|                   }} | ||||
|                   loading={composeState.posting} | ||||
|         disabled={headerRightDisabled} | ||||
|                   disabled={headerRightDisabled()} | ||||
|                 /> | ||||
|     ), | ||||
|     [totalTextCount, composeState] | ||||
|               ) | ||||
|  | ||||
|   const headerContent = useMemo(() => { | ||||
|     return `${totalTextCount} / ${maxTootChars}` | ||||
|   }, [totalTextCount, maxTootChars, composeState.dirty]) | ||||
|  | ||||
|   const inputProps: EmojisState['inputProps'] = [ | ||||
|     { | ||||
|       value: [ | ||||
|         composeState.text.raw, | ||||
|         content => { | ||||
|           formatText({ textInput: 'text', composeDispatch, content }) | ||||
|         } | ||||
|       ], | ||||
|       selection: [ | ||||
|         composeState.text.selection, | ||||
|         selection => composeDispatch({ type: 'text', payload: { selection } }) | ||||
|       ], | ||||
|       isFocused: composeState.textInputFocus.isFocused.text, | ||||
|       maxLength: maxTootChars - (composeState.spoiler.active ? composeState.spoiler.count : 0), | ||||
|       ref: composeState.textInputFocus.refs.text | ||||
|     }, | ||||
|     { | ||||
|       value: [ | ||||
|         composeState.spoiler.raw, | ||||
|         content => formatText({ textInput: 'spoiler', composeDispatch, content }) | ||||
|       ], | ||||
|       selection: [ | ||||
|         composeState.spoiler.selection, | ||||
|         selection => composeDispatch({ type: 'spoiler', payload: { selection } }) | ||||
|       ], | ||||
|       isFocused: composeState.textInputFocus.isFocused.spoiler, | ||||
|       maxLength: maxTootChars - composeState.text.count, | ||||
|       ref: composeState.textInputFocus.refs.spoiler | ||||
|     } | ||||
|   ] | ||||
|  | ||||
|   return ( | ||||
|     <ComponentEmojis | ||||
|       inputProps={inputProps} | ||||
|       customButton | ||||
|       customBehavior={Platform.OS === 'ios' ? 'padding' : undefined} | ||||
|       customEdges={hasKeyboard ? ['top'] : ['top', 'bottom']} | ||||
|     > | ||||
|       <ComposeContext.Provider value={{ composeState, composeDispatch }}> | ||||
|         <Stack.Navigator initialRouteName='Screen-Compose-Root'> | ||||
|           <Stack.Screen | ||||
|             name='Screen-Compose-Root' | ||||
|             component={ComposeRoot} | ||||
|             options={{ | ||||
|               title: headerContent, | ||||
|               headerTitleStyle: { | ||||
|                 fontWeight: | ||||
|                   totalTextCount > maxTootChars | ||||
|                     ? StyleConstants.Font.Weight.Bold | ||||
|                     : StyleConstants.Font.Weight.Normal, | ||||
|                 fontSize: StyleConstants.Font.Size.M | ||||
|               }, | ||||
|               headerTintColor: totalTextCount > maxTootChars ? colors.red : colors.secondary, | ||||
|               headerLeft, | ||||
|               headerRight | ||||
|             }} | ||||
|           /> | ||||
|           <Stack.Screen | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { androidActionSheetStyles } from '@utils/helpers/androidActionSheetStyles' | ||||
| import { RootStackScreenProps } from '@utils/navigation/navigators' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useState } from 'react' | ||||
| import React, { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { | ||||
|   Dimensions, | ||||
| @@ -40,14 +40,41 @@ const ScreenImagesViewer = ({ | ||||
|  | ||||
|   const insets = useSafeAreaInsets() | ||||
|  | ||||
|   const { mode, colors } = useTheme() | ||||
|   const { colors } = useTheme() | ||||
|   const { t } = useTranslation(['common', 'screenImageViewer']) | ||||
|  | ||||
|   const initialIndex = imageUrls.findIndex(image => image.id === id) | ||||
|   const [currentIndex, setCurrentIndex] = useState(initialIndex) | ||||
|  | ||||
|   const { showActionSheetWithOptions } = useActionSheet() | ||||
|   const onPress = useCallback(() => { | ||||
|  | ||||
|   const isZoomed = useSharedValue(false) | ||||
|  | ||||
|   return ( | ||||
|     <View style={{ backgroundColor: 'black' }}> | ||||
|       <StatusBar hidden /> | ||||
|       <View | ||||
|         style={{ | ||||
|           flexDirection: 'row', | ||||
|           justifyContent: 'space-between', | ||||
|           alignItems: 'center', | ||||
|           marginTop: insets.top, | ||||
|           position: 'absolute', | ||||
|           width: '100%', | ||||
|           zIndex: 999 | ||||
|         }} | ||||
|       > | ||||
|         <HeaderLeft content='X' native={false} background onPress={() => navigation.goBack()} /> | ||||
|         {!hideCounter ? ( | ||||
|           <HeaderCenter inverted content={`${currentIndex + 1} / ${imageUrls.length}`} /> | ||||
|         ) : null} | ||||
|         <HeaderRight | ||||
|           accessibilityLabel={t('screenImageViewer:content.actions.accessibilityLabel')} | ||||
|           accessibilityHint={t('screenImageViewer:content.actions.accessibilityHint')} | ||||
|           content='MoreHorizontal' | ||||
|           native={false} | ||||
|           background | ||||
|           onPress={() => | ||||
|             showActionSheetWithOptions( | ||||
|               { | ||||
|                 options: [ | ||||
| @@ -76,100 +103,7 @@ const ScreenImagesViewer = ({ | ||||
|                 } | ||||
|               } | ||||
|             ) | ||||
|   }, [currentIndex]) | ||||
|  | ||||
|   const isZoomed = useSharedValue(false) | ||||
|  | ||||
|   const renderItem = React.useCallback( | ||||
|     ({ | ||||
|       item | ||||
|     }: { | ||||
|       item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0] | ||||
|     }) => { | ||||
|       const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT | ||||
|       const imageRatio = item.width && item.height ? item.width / item.height : 1 | ||||
|       const imageWidth = item.width || 100 | ||||
|       const imageHeight = item.height || 100 | ||||
|  | ||||
|       const maxWidthScale = item.width ? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0 | ||||
|       const maxHeightScale = item.height ? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4 : 0 | ||||
|       const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4]) | ||||
|  | ||||
|       return ( | ||||
|         <Zoom | ||||
|           onZoomBegin={() => (isZoomed.value = true)} | ||||
|           onZoomEnd={() => (isZoomed.value = false)} | ||||
|           maximumZoomScale={max > 8 ? 8 : max} | ||||
|           simultaneousGesture={Gesture.Fling() | ||||
|             .direction(Directions.DOWN) | ||||
|             .onStart(() => { | ||||
|               if (isZoomed.value === false) { | ||||
|                 runOnJS(navigation.goBack)() | ||||
|           } | ||||
|             })} | ||||
|           children={ | ||||
|             <View | ||||
|               style={{ | ||||
|                 width: WINDOW_WIDTH, | ||||
|                 height: WINDOW_HEIGHT, | ||||
|                 flexDirection: 'row', | ||||
|                 alignItems: 'center', | ||||
|                 justifyContent: 'center' | ||||
|               }} | ||||
|             > | ||||
|               <GracefullyImage | ||||
|                 uri={{ preview: item.preview_url, remote: item.remote_url, original: item.url }} | ||||
|                 dimension={{ | ||||
|                   width: | ||||
|                     screenRatio > imageRatio | ||||
|                       ? (WINDOW_HEIGHT / imageHeight) * imageWidth | ||||
|                       : WINDOW_WIDTH, | ||||
|                   height: | ||||
|                     screenRatio > imageRatio | ||||
|                       ? WINDOW_HEIGHT | ||||
|                       : (WINDOW_WIDTH / imageWidth) * imageHeight | ||||
|                 }} | ||||
|               /> | ||||
|             </View> | ||||
|           } | ||||
|         /> | ||||
|       ) | ||||
|     }, | ||||
|     [isZoomed.value] | ||||
|   ) | ||||
|  | ||||
|   const onViewableItemsChanged = useCallback( | ||||
|     ({ viewableItems }: { viewableItems: ViewToken[] }) => { | ||||
|       setCurrentIndex(viewableItems[0]?.index || 0) | ||||
|     }, | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <View style={{ backgroundColor: 'black' }}> | ||||
|       <StatusBar hidden /> | ||||
|       <View | ||||
|         style={{ | ||||
|           flexDirection: 'row', | ||||
|           justifyContent: 'space-between', | ||||
|           alignItems: 'center', | ||||
|           marginTop: insets.top, | ||||
|           position: 'absolute', | ||||
|           width: '100%', | ||||
|           zIndex: 999 | ||||
|         }} | ||||
|       > | ||||
|         <HeaderLeft content='X' native={false} background onPress={() => navigation.goBack()} /> | ||||
|         {!hideCounter ? ( | ||||
|           <HeaderCenter inverted content={`${currentIndex + 1} / ${imageUrls.length}`} /> | ||||
|         ) : null} | ||||
|         <HeaderRight | ||||
|           accessibilityLabel={t('screenImageViewer:content.actions.accessibilityLabel')} | ||||
|           accessibilityHint={t('screenImageViewer:content.actions.accessibilityHint')} | ||||
|           content='MoreHorizontal' | ||||
|           native={false} | ||||
|           background | ||||
|           onPress={onPress} | ||||
|         /> | ||||
|       </View> | ||||
|       <LongPressGestureHandler | ||||
| @@ -211,8 +145,71 @@ const ScreenImagesViewer = ({ | ||||
|           pagingEnabled | ||||
|           horizontal | ||||
|           keyExtractor={item => item.id} | ||||
|           renderItem={renderItem} | ||||
|           onViewableItemsChanged={onViewableItemsChanged} | ||||
|           renderItem={({ | ||||
|             item | ||||
|           }: { | ||||
|             item: RootStackScreenProps<'Screen-ImagesViewer'>['route']['params']['imageUrls'][0] | ||||
|           }) => { | ||||
|             const screenRatio = WINDOW_WIDTH / WINDOW_HEIGHT | ||||
|             const imageRatio = item.width && item.height ? item.width / item.height : 1 | ||||
|             const imageWidth = item.width || 100 | ||||
|             const imageHeight = item.height || 100 | ||||
|  | ||||
|             const maxWidthScale = item.width | ||||
|               ? (item.width / WINDOW_WIDTH / PixelRatio.get()) * 4 | ||||
|               : 0 | ||||
|             const maxHeightScale = item.height | ||||
|               ? (item.height / WINDOW_WIDTH / PixelRatio.get()) * 4 | ||||
|               : 0 | ||||
|             const max = Math.max.apply(Math, [maxWidthScale, maxHeightScale, 4]) | ||||
|  | ||||
|             return ( | ||||
|               <Zoom | ||||
|                 onZoomBegin={() => (isZoomed.value = true)} | ||||
|                 onZoomEnd={() => (isZoomed.value = false)} | ||||
|                 maximumZoomScale={max > 8 ? 8 : max} | ||||
|                 simultaneousGesture={Gesture.Fling() | ||||
|                   .direction(Directions.DOWN) | ||||
|                   .onStart(() => { | ||||
|                     if (isZoomed.value === false) { | ||||
|                       runOnJS(navigation.goBack)() | ||||
|                     } | ||||
|                   })} | ||||
|                 children={ | ||||
|                   <View | ||||
|                     style={{ | ||||
|                       width: WINDOW_WIDTH, | ||||
|                       height: WINDOW_HEIGHT, | ||||
|                       flexDirection: 'row', | ||||
|                       alignItems: 'center', | ||||
|                       justifyContent: 'center' | ||||
|                     }} | ||||
|                   > | ||||
|                     <GracefullyImage | ||||
|                       uri={{ | ||||
|                         preview: item.preview_url, | ||||
|                         remote: item.remote_url, | ||||
|                         original: item.url | ||||
|                       }} | ||||
|                       dimension={{ | ||||
|                         width: | ||||
|                           screenRatio > imageRatio | ||||
|                             ? (WINDOW_HEIGHT / imageHeight) * imageWidth | ||||
|                             : WINDOW_WIDTH, | ||||
|                         height: | ||||
|                           screenRatio > imageRatio | ||||
|                             ? WINDOW_HEIGHT | ||||
|                             : (WINDOW_WIDTH / imageWidth) * imageHeight | ||||
|                       }} | ||||
|                     /> | ||||
|                   </View> | ||||
|                 } | ||||
|               /> | ||||
|             ) | ||||
|           }} | ||||
|           onViewableItemsChanged={({ viewableItems }: { viewableItems: ViewToken[] }) => { | ||||
|             setCurrentIndex(viewableItems[0]?.index || 0) | ||||
|           }} | ||||
|           viewabilityConfig={{ | ||||
|             itemVisiblePercentThreshold: 50 | ||||
|           }} | ||||
|   | ||||
| @@ -6,8 +6,8 @@ import { TabLocalStackParamList } from '@utils/navigation/navigators' | ||||
| import { useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback } from 'react' | ||||
| import { Dimensions, ListRenderItem, Pressable, View } from 'react-native' | ||||
| import React from 'react' | ||||
| import { Dimensions, Pressable, View } from 'react-native' | ||||
| import { FlatList } from 'react-native-gesture-handler' | ||||
| import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated' | ||||
|  | ||||
| @@ -39,8 +39,25 @@ const AccountAttachments: React.FC<Props> = ({ account }) => { | ||||
|         .splice(0, DISPLAY_AMOUNT) | ||||
|     : [] | ||||
|  | ||||
|   const renderItem = useCallback<ListRenderItem<Mastodon.Status>>( | ||||
|     ({ item, index }) => { | ||||
|   const styleContainer = useAnimatedStyle(() => { | ||||
|     if (flattenData.length) { | ||||
|       return { | ||||
|         height: withTiming(width + StyleConstants.Spacing.Global.PagePadding * 2), | ||||
|         paddingVertical: StyleConstants.Spacing.Global.PagePadding, | ||||
|         borderTopWidth: 1, | ||||
|         borderTopColor: colors.border | ||||
|       } | ||||
|     } else { | ||||
|       return {} | ||||
|     } | ||||
|   }, [flattenData.length]) | ||||
|  | ||||
|   return ( | ||||
|     <Animated.View style={[{ flex: 1 }, styleContainer]}> | ||||
|       <FlatList | ||||
|         horizontal | ||||
|         data={flattenData} | ||||
|         renderItem={({ item, index }) => { | ||||
|           if (index === DISPLAY_AMOUNT - 1) { | ||||
|             return ( | ||||
|               <Pressable | ||||
| @@ -72,7 +89,8 @@ const AccountAttachments: React.FC<Props> = ({ account }) => { | ||||
|             return ( | ||||
|               <GracefullyImage | ||||
|                 uri={{ | ||||
|               original: item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url, | ||||
|                   original: | ||||
|                     item.media_attachments[0]?.preview_url || item.media_attachments[0]?.url, | ||||
|                   remote: item.media_attachments[0]?.remote_url | ||||
|                 }} | ||||
|                 blurhash={ | ||||
| @@ -84,29 +102,7 @@ const AccountAttachments: React.FC<Props> = ({ account }) => { | ||||
|               /> | ||||
|             ) | ||||
|           } | ||||
|     }, | ||||
|     [account] | ||||
|   ) | ||||
|  | ||||
|   const styleContainer = useAnimatedStyle(() => { | ||||
|     if (flattenData.length) { | ||||
|       return { | ||||
|         height: withTiming(width + StyleConstants.Spacing.Global.PagePadding * 2), | ||||
|         paddingVertical: StyleConstants.Spacing.Global.PagePadding, | ||||
|         borderTopWidth: 1, | ||||
|         borderTopColor: colors.border | ||||
|       } | ||||
|     } else { | ||||
|       return {} | ||||
|     } | ||||
|   }, [flattenData.length]) | ||||
|  | ||||
|   return ( | ||||
|     <Animated.View style={[{ flex: 1 }, styleContainer]}> | ||||
|       <FlatList | ||||
|         horizontal | ||||
|         data={flattenData} | ||||
|         renderItem={renderItem} | ||||
|         }} | ||||
|         showsHorizontalScrollIndicator={false} | ||||
|       /> | ||||
|     </Animated.View> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { ParseEmojis } from '@components/Parse' | ||||
| import CustomText from '@components/Text' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useMemo } from 'react' | ||||
| import React from 'react' | ||||
| import { View } from 'react-native' | ||||
| import { PlaceholderLine } from 'rn-placeholder' | ||||
|  | ||||
| @@ -13,21 +13,6 @@ export interface Props { | ||||
| const AccountInformationName: React.FC<Props> = ({ account }) => { | ||||
|   const { colors } = useTheme() | ||||
|  | ||||
|   const movedContent = useMemo(() => { | ||||
|     if (account?.moved) { | ||||
|       return ( | ||||
|         <View style={{ marginLeft: StyleConstants.Spacing.S }}> | ||||
|           <ParseEmojis | ||||
|             content={account.moved.display_name || account.moved.username} | ||||
|             emojis={account.moved.emojis} | ||||
|             size='L' | ||||
|             fontBold | ||||
|           /> | ||||
|         </View> | ||||
|       ) | ||||
|     } | ||||
|   }, [account?.moved]) | ||||
|  | ||||
|   return ( | ||||
|     <View | ||||
|       style={{ | ||||
| @@ -51,7 +36,16 @@ const AccountInformationName: React.FC<Props> = ({ account }) => { | ||||
|               fontBold | ||||
|             /> | ||||
|           </CustomText> | ||||
|           {movedContent} | ||||
|           {account.moved ? ( | ||||
|             <View style={{ marginLeft: StyleConstants.Spacing.S }}> | ||||
|               <ParseEmojis | ||||
|                 content={account.moved.display_name || account.moved.username} | ||||
|                 emojis={account.moved.emojis} | ||||
|                 size='L' | ||||
|                 fontBold | ||||
|               /> | ||||
|             </View> | ||||
|           ) : null} | ||||
|         </> | ||||
|       ) : ( | ||||
|         <PlaceholderLine | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import { SearchResult } from '@utils/queryHooks/search' | ||||
| import { QueryKeyUsers, useUsersQuery } from '@utils/queryHooks/users' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useState } from 'react' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { View } from 'react-native' | ||||
| import { Circle, Flow } from 'react-native-animated-spinkit' | ||||
| @@ -41,11 +41,6 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = | ||||
|   }) | ||||
|   const flattenData = data?.pages ? data.pages.flatMap(page => [...page.body]) : [] | ||||
|  | ||||
|   const onEndReached = useCallback( | ||||
|     () => hasNextPage && !isFetchingNextPage && fetchNextPage(), | ||||
|     [hasNextPage, isFetchingNextPage] | ||||
|   ) | ||||
|  | ||||
|   const [isSearching, setIsSearching] = useState(false) | ||||
|  | ||||
|   return ( | ||||
| @@ -90,7 +85,7 @@ const TabSharedUsers: React.FC<TabSharedStackScreenProps<'Tab-Shared-Users'>> = | ||||
|           children={<Flow size={StyleConstants.Font.Size.L} color={colors.secondary} />} | ||||
|         /> | ||||
|       )} | ||||
|       onEndReached={onEndReached} | ||||
|       onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()} | ||||
|       onEndReachedThreshold={0.75} | ||||
|       ItemSeparatorComponent={ComponentSeparator} | ||||
|       ListEmptyComponent={ | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs' | ||||
| import { RootStackScreenProps, ScreenTabsStackParamList } from '@utils/navigation/navigators' | ||||
| import { getGlobalStorage, useAccountStorage, useGlobalStorage } from '@utils/storage/actions' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| import React from 'react' | ||||
| import { Platform } from 'react-native' | ||||
| import TabLocal from './Local' | ||||
| import TabMe from './Me' | ||||
| @@ -20,31 +20,6 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => { | ||||
|   const [accountActive] = useGlobalStorage.string('account.active') | ||||
|   const [avatarStatic] = useAccountStorage.string('auth.account.avatar_static') | ||||
|  | ||||
|   const composeListeners = useMemo( | ||||
|     () => ({ | ||||
|       tabPress: (e: any) => { | ||||
|         e.preventDefault() | ||||
|         haptics('Light') | ||||
|         navigation.navigate('Screen-Compose') | ||||
|       } | ||||
|     }), | ||||
|     [] | ||||
|   ) | ||||
|   const composeComponent = useCallback(() => null, []) | ||||
|  | ||||
|   const meListeners = useMemo( | ||||
|     () => ({ | ||||
|       tabLongPress: () => { | ||||
|         haptics('Light') | ||||
|         //@ts-ignore | ||||
|         navigation.navigate('Tab-Me', { screen: 'Tab-Me-Root' }) | ||||
|         //@ts-ignore | ||||
|         navigation.navigate('Tab-Me', { screen: 'Tab-Me-Switch' }) | ||||
|       } | ||||
|     }), | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <Tab.Navigator | ||||
|       initialRouteName={accountActive ? getGlobalStorage.string('app.prev_tab') : 'Tab-Me'} | ||||
| @@ -97,9 +72,32 @@ const ScreenTabs = ({ navigation }: RootStackScreenProps<'Screen-Tabs'>) => { | ||||
|     > | ||||
|       <Tab.Screen name='Tab-Local' component={TabLocal} /> | ||||
|       <Tab.Screen name='Tab-Public' component={TabPublic} /> | ||||
|       <Tab.Screen name='Tab-Compose' component={composeComponent} listeners={composeListeners} /> | ||||
|       <Tab.Screen | ||||
|         name='Tab-Compose' | ||||
|         listeners={{ | ||||
|           tabPress: e => { | ||||
|             e.preventDefault() | ||||
|             haptics('Light') | ||||
|             navigation.navigate('Screen-Compose') | ||||
|           } | ||||
|         }} | ||||
|       > | ||||
|         {() => null} | ||||
|       </Tab.Screen> | ||||
|       <Tab.Screen name='Tab-Notifications' component={TabNotifications} /> | ||||
|       <Tab.Screen name='Tab-Me' component={TabMe} listeners={meListeners} /> | ||||
|       <Tab.Screen | ||||
|         name='Tab-Me' | ||||
|         component={TabMe} | ||||
|         listeners={{ | ||||
|           tabLongPress: () => { | ||||
|             haptics('Light') | ||||
|             //@ts-ignore | ||||
|             navigation.navigate('Tab-Me', { screen: 'Tab-Me-Root' }) | ||||
|             //@ts-ignore | ||||
|             navigation.navigate('Tab-Me', { screen: 'Tab-Me-Switch' }) | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|     </Tab.Navigator> | ||||
|   ) | ||||
| } | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { themes } from '@utils/styles/themes' | ||||
| import * as Linking from 'expo-linking' | ||||
| import { addScreenshotListener } from 'expo-screen-capture' | ||||
| import React, { useCallback, useEffect, useState } from 'react' | ||||
| import React, { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { IntlProvider } from 'react-intl' | ||||
| import { Alert, Platform, StatusBar } from 'react-native' | ||||
| @@ -90,7 +90,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|   useEmojisQuery({ options: { enabled: !!accountActive } }) | ||||
|  | ||||
|   // Callbacks | ||||
|   const navigationContainerOnStateChange = useCallback(() => { | ||||
|   const navigationContainerOnStateChange = () => { | ||||
|     const currentRoute = navigationRef.getCurrentRoute() | ||||
|  | ||||
|     const matchTabName = currentRoute?.name?.match(/(Tab-.*)-Root/) | ||||
| @@ -98,7 +98,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|       // @ts-ignore | ||||
|       setGlobalStorage('app.prev_tab', matchTabName[1]) | ||||
|     } | ||||
|   }, []) | ||||
|   } | ||||
|  | ||||
|   // Deep linking for compose | ||||
|   const [deeplinked, setDeeplinked] = useState(false) | ||||
| @@ -128,8 +128,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|   }, [accounts, accountActive, deeplinked]) | ||||
|  | ||||
|   // Share Extension | ||||
|   const handleShare = useCallback( | ||||
|     ( | ||||
|   const handleShare = ( | ||||
|     item?: | ||||
|       | { | ||||
|           data: { mimeType: string; data: string }[] | ||||
| @@ -228,9 +227,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|     }, | ||||
|     [] | ||||
|   ) | ||||
|   } | ||||
|   useEffect(() => { | ||||
|     ShareMenu.getInitialShare(handleShare) | ||||
|   }, []) | ||||
|   | ||||
| @@ -13,6 +13,8 @@ export type GlobalV0 = { | ||||
|   // number | ||||
|   'app.count_till_store_review'?: number | ||||
|   'app.font_size'?: -1 | 0 | 1 | 2 | 3 | ||||
|   'version.global': number | ||||
|   'version.account': number | ||||
|   // boolean | ||||
|   'app.auto_play_gifv'?: boolean | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import log from '@utils/startup/log' | ||||
| import { secureStorage, storage } from '@utils/storage' | ||||
| import { MMKV } from 'react-native-mmkv' | ||||
|  | ||||
| export const hasMigratedFromAsyncStorage = storage.global.getBoolean('hasMigratedFromAsyncStorage') | ||||
| export const versionStorageGlobal = storage.global.getNumber('version.global') | ||||
|  | ||||
| export async function migrateFromAsyncStorage(): Promise<void> { | ||||
|   log('log', 'Migration', 'Migrating...') | ||||
| @@ -107,7 +107,7 @@ export async function migrateFromAsyncStorage(): Promise<void> { | ||||
|     throw error | ||||
|   } | ||||
|  | ||||
|   storage.global.set('hasMigratedFromAsyncStorage', true) | ||||
|   storage.global.set('version.global', 0) | ||||
|  | ||||
|   const end = global.performance.now() | ||||
|   log('log', 'Migration', `Migrated in ${end - start}ms`) | ||||
|   | ||||
| @@ -19,7 +19,7 @@ const ManageThemeContext = createContext<ContextType>({ | ||||
|  | ||||
| export const useTheme = () => useContext(ManageThemeContext) | ||||
|  | ||||
| const useColorSchemeDelay = (delay = 500) => { | ||||
| const useColorSchemeDelay = (delay = 50) => { | ||||
|   const [colorScheme, setColorScheme] = React.useState(Appearance.getColorScheme()) | ||||
|   const onColorSchemeChange = React.useCallback( | ||||
|     throttle( | ||||
|   | ||||
							
								
								
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -2922,13 +2922,13 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@react-native-menu/menu@npm:^0.7.2": | ||||
|   version: 0.7.2 | ||||
|   resolution: "@react-native-menu/menu@npm:0.7.2" | ||||
| "@react-native-menu/menu@npm:^0.7.3": | ||||
|   version: 0.7.3 | ||||
|   resolution: "@react-native-menu/menu@npm:0.7.3" | ||||
|   peerDependencies: | ||||
|     react: "*" | ||||
|     react-native: "*" | ||||
|   checksum: b82402d183a58427cf0cc0dd617a81019e559d828b370f37d2df2690254dfa1e53f4da228e8c63e53d7c29423fd939f9a4b0c2b2930dc7d6cc5fdc5858b523f6 | ||||
|   checksum: 419b2e500c49248b3cc2202ceda7cdb35008dfc3a4d67becb5a1276a9d306c654eccfa041a1d040b97ebb188478e565c658fa5dee1d7db834a786d67e32461e1 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| @@ -11232,7 +11232,7 @@ __metadata: | ||||
|     "@react-native-community/blur": ^4.3.0 | ||||
|     "@react-native-community/netinfo": 9.3.7 | ||||
|     "@react-native-community/segmented-control": ^2.2.2 | ||||
|     "@react-native-menu/menu": ^0.7.2 | ||||
|     "@react-native-menu/menu": ^0.7.3 | ||||
|     "@react-navigation/bottom-tabs": ^6.5.2 | ||||
|     "@react-navigation/native": ^6.1.1 | ||||
|     "@react-navigation/native-stack": ^6.9.7 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user