mirror of
				https://github.com/tooot-app/app
				synced 2025-06-05 22:19:13 +02:00 
			
		
		
		
	Updates
This commit is contained in:
		| @@ -1,5 +1,4 @@ | ||||
| import client from '@api/client' | ||||
| import { HeaderCenter, HeaderLeft } from '@components/Header' | ||||
| import { toast, toastConfig } from '@components/toast' | ||||
| import { | ||||
|   NavigationContainer, | ||||
| @@ -21,11 +20,11 @@ import * as Analytics from 'expo-firebase-analytics' | ||||
| import React, { createRef, useCallback, useEffect, useRef } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Platform, StatusBar } from 'react-native' | ||||
| import { createNativeStackNavigator } from 'react-native-screens/native-stack' | ||||
| import Toast from 'react-native-toast-message' | ||||
| import { createSharedElementStackNavigator } from 'react-navigation-shared-element' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
|  | ||||
| const Stack = createSharedElementStackNavigator<Nav.RootStackParamList>() | ||||
| const Stack = createNativeStackNavigator<Nav.RootStackParamList>() | ||||
|  | ||||
| export interface Props { | ||||
|   localCorrupt?: string | ||||
| @@ -133,11 +132,7 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|         onReady={navigationContainerOnReady} | ||||
|         onStateChange={navigationContainerOnStateChange} | ||||
|       > | ||||
|         <Stack.Navigator | ||||
|           mode='modal' | ||||
|           initialRouteName='Screen-Tabs' | ||||
|           screenOptions={{ cardStyle: { backgroundColor: theme.background } }} | ||||
|         > | ||||
|         <Stack.Navigator initialRouteName='Screen-Tabs'> | ||||
|           <Stack.Screen | ||||
|             name='Screen-Tabs' | ||||
|             component={ScreenTabs} | ||||
| @@ -148,80 +143,31 @@ const Screens: React.FC<Props> = ({ localCorrupt }) => { | ||||
|             name='Screen-Actions' | ||||
|             component={ScreenActions} | ||||
|             options={{ | ||||
|               headerShown: false, | ||||
|               cardStyle: { backgroundColor: 'transparent' }, | ||||
|               cardStyleInterpolator: ({ current: { progress } }) => ({ | ||||
|                 cardStyle: { | ||||
|                   opacity: progress.interpolate({ | ||||
|                     inputRange: [0, 1], | ||||
|                     outputRange: [0, 1] | ||||
|                   }) | ||||
|                 } | ||||
|               }) | ||||
|               stackPresentation: 'transparentModal', | ||||
|               stackAnimation: 'fade' | ||||
|             }} | ||||
|           /> | ||||
|           <Stack.Screen | ||||
|             name='Screen-Announcements' | ||||
|             component={ScreenAnnouncements} | ||||
|             options={{ | ||||
|               gestureEnabled: false, | ||||
|               headerTitle: t('sharedAnnouncements:heading'), | ||||
|               ...(Platform.OS === 'android' && { | ||||
|                 headerCenter: () => ( | ||||
|                   <HeaderCenter content={t('sharedAnnouncements:heading')} /> | ||||
|                 ) | ||||
|               }), | ||||
|               headerTransparent: true, | ||||
|               headerLeft: () => ( | ||||
|                 <HeaderLeft | ||||
|                   content='X' | ||||
|                   native={false} | ||||
|                   onPress={() => navigationRef.current?.goBack()} | ||||
|                 /> | ||||
|               ), | ||||
|               animationTypeForReplace: 'pop', | ||||
|               cardStyle: { backgroundColor: 'transparent' }, | ||||
|               cardStyleInterpolator: ({ current: { progress } }) => ({ | ||||
|                 cardStyle: { | ||||
|                   opacity: progress.interpolate({ | ||||
|                     inputRange: [0, 1], | ||||
|                     outputRange: [0, 1] | ||||
|                   }) | ||||
|                 } | ||||
|               }) | ||||
|               stackPresentation: 'transparentModal', | ||||
|               stackAnimation: 'fade' | ||||
|             }} | ||||
|           /> | ||||
|           <Stack.Screen | ||||
|             name='Screen-Compose' | ||||
|             component={ScreenCompose} | ||||
|             options={{ gestureEnabled: false, headerShown: false }} | ||||
|             options={{ | ||||
|               stackPresentation: 'fullScreenModal' | ||||
|             }} | ||||
|           /> | ||||
|           <Stack.Screen | ||||
|             name='Screen-ImagesViewer' | ||||
|             component={ScreenImagesViewer} | ||||
|             options={{ | ||||
|               gestureEnabled: false, | ||||
|               headerTransparent: true, | ||||
|               headerLeft: () => ( | ||||
|                 <HeaderLeft | ||||
|                   content='X' | ||||
|                   native={false} | ||||
|                   onPress={() => navigationRef.current?.goBack()} | ||||
|                 /> | ||||
|               ), | ||||
|               cardStyle: { backgroundColor: 'transparent' }, | ||||
|               cardStyleInterpolator: ({ current: { progress } }) => ({ | ||||
|                 cardStyle: { | ||||
|                   opacity: progress.interpolate({ | ||||
|                     inputRange: [0, 1], | ||||
|                     outputRange: [0, 1] | ||||
|                   }) | ||||
|                 } | ||||
|               }) | ||||
|             }} | ||||
|             sharedElements={route => { | ||||
|               const { imageIndex, imageUrls } = route.params | ||||
|               return [{ id: `image.${imageUrls[imageIndex].url}` }] | ||||
|               stackPresentation: 'fullScreenModal', | ||||
|               stackAnimation: 'fade' | ||||
|             }} | ||||
|           /> | ||||
|         </Stack.Navigator> | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { | ||||
|   updateLocalNotification | ||||
| } from '@utils/slices/instancesSlice' | ||||
| import { useEffect, useRef } from 'react' | ||||
| import { InfiniteData, useQueryClient } from 'react-query' | ||||
| import { useQueryClient } from 'react-query' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
| import ReconnectingWebSocket from 'reconnecting-websocket' | ||||
|  | ||||
| @@ -17,7 +17,12 @@ const useWebsocket = ({ | ||||
| }) => { | ||||
|   const queryClient = useQueryClient() | ||||
|   const dispatch = useDispatch() | ||||
|   const localInstance = useSelector(getLocalInstance) | ||||
|   const localInstance = useSelector( | ||||
|     getLocalInstance, | ||||
|     (prev, next) => | ||||
|       prev?.urls.streaming_api === next?.urls.streaming_api && | ||||
|       prev?.token === next?.token | ||||
|   ) | ||||
|  | ||||
|   const rws = useRef<ReconnectingWebSocket>() | ||||
|   useEffect(() => { | ||||
| @@ -40,16 +45,7 @@ const useWebsocket = ({ | ||||
|               'Timeline', | ||||
|               { page: 'Notifications' } | ||||
|             ] | ||||
|             const queryData = queryClient.getQueryData(queryKey) | ||||
|             queryData !== undefined && | ||||
|               queryClient.setQueryData< | ||||
|                 InfiniteData<Mastodon.Notification[]> | undefined | ||||
|               >(queryKey, old => { | ||||
|                 if (old) { | ||||
|                   old.pages[0].unshift(payload) | ||||
|                   return old | ||||
|                 } | ||||
|               }) | ||||
|             queryClient.invalidateQueries(queryKey) | ||||
|             break | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import React, { useCallback, useMemo, useState } from 'react' | ||||
| import { Pressable, StyleProp, StyleSheet, ViewStyle } from 'react-native' | ||||
| import { Blurhash } from 'react-native-blurhash' | ||||
| import FastImage, { ImageStyle } from 'react-native-fast-image' | ||||
| import { SharedElement } from 'react-navigation-shared-element' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
|  | ||||
| export interface Props { | ||||
| @@ -73,23 +72,12 @@ const GracefullyImage = React.memo( | ||||
|     const children = useCallback(() => { | ||||
|       return ( | ||||
|         <> | ||||
|           {sharedElement ? ( | ||||
|             <SharedElement id={`image.${sharedElement}`} style={[styles.image]}> | ||||
|               <FastImage | ||||
|                 source={{ uri: sourceUri }} | ||||
|                 style={[styles.image, imageStyle]} | ||||
|                 onLoad={onLoad} | ||||
|                 onError={onError} | ||||
|               /> | ||||
|             </SharedElement> | ||||
|           ) : ( | ||||
|             <FastImage | ||||
|               source={{ uri: sourceUri }} | ||||
|               style={[styles.image, imageStyle]} | ||||
|               onLoad={onLoad} | ||||
|               onError={onError} | ||||
|             /> | ||||
|           )} | ||||
|           <FastImage | ||||
|             source={{ uri: sourceUri }} | ||||
|             style={[styles.image, imageStyle]} | ||||
|             onLoad={onLoad} | ||||
|             onError={onError} | ||||
|           /> | ||||
|           {blurhash && | ||||
|           (hidden || !(previewLoaded || originalLoaded || remoteLoaded)) ? ( | ||||
|             <Blurhash | ||||
|   | ||||
| @@ -5,16 +5,20 @@ import { StyleSheet, Text } from 'react-native' | ||||
|  | ||||
| export interface Props { | ||||
|   content: string | ||||
|   inverted?: boolean | ||||
| } | ||||
|  | ||||
| // Used for Android mostly | ||||
| const HeaderCenter = React.memo( | ||||
|   ({ content }: Props) => { | ||||
|   ({ content, inverted = false }: Props) => { | ||||
|     const { theme } = useTheme() | ||||
|  | ||||
|     return ( | ||||
|       <Text | ||||
|         style={[styles.text, { color: theme.primary }]} | ||||
|         style={[ | ||||
|           styles.text, | ||||
|           { color: inverted ? theme.primaryOverlay : theme.primary } | ||||
|         ]} | ||||
|         children={content} | ||||
|       /> | ||||
|     ) | ||||
|   | ||||
| @@ -29,7 +29,7 @@ const ComponentInstance: React.FC<Props> = ({ | ||||
|   const { t } = useTranslation('componentInstance') | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const localInstances = useSelector(getLocalInstances) | ||||
|   const localInstances = useSelector(getLocalInstances, () => true) | ||||
|   const [instanceDomain, setInstanceDomain] = useState<string>() | ||||
|  | ||||
|   const instanceQuery = useInstanceQuery({ | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import ComponentSeparator from '@components/Separator' | ||||
| import { useScrollToTop } from '@react-navigation/native' | ||||
| import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { getLocalActiveIndex } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useEffect, useMemo, useRef } from 'react' | ||||
| @@ -12,6 +13,7 @@ import Animated, { | ||||
|   withTiming | ||||
| } from 'react-native-reanimated' | ||||
| import { InfiniteData, useQueryClient } from 'react-query' | ||||
| import { useSelector } from 'react-redux' | ||||
| import TimelineConversation from './Timeline/Conversation' | ||||
| import TimelineDefault from './Timeline/Default' | ||||
| import TimelineEmpty from './Timeline/Empty' | ||||
| @@ -40,6 +42,8 @@ const Timeline: React.FC<Props> = ({ | ||||
|   disableInfinity = false, | ||||
|   customProps | ||||
| }) => { | ||||
|   // Update timeline when account switched | ||||
|   useSelector(getLocalActiveIndex) | ||||
|   const queryKeyParams = { | ||||
|     page, | ||||
|     ...(hashtag && { hashtag }), | ||||
| @@ -218,7 +222,7 @@ const Timeline: React.FC<Props> = ({ | ||||
|         ref={flRef} | ||||
|         windowSize={8} | ||||
|         data={flattenData} | ||||
|         initialNumToRender={3} | ||||
|         initialNumToRender={6} | ||||
|         maxToRenderPerBatch={3} | ||||
|         style={styles.flatList} | ||||
|         renderItem={renderItem} | ||||
|   | ||||
| @@ -58,7 +58,10 @@ const TimelineConversation: React.FC<Props> = ({ | ||||
|   queryKey, | ||||
|   highlighted = false | ||||
| }) => { | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const localAccount = useSelector( | ||||
|     getLocalAccount, | ||||
|     (prev, next) => prev?.id === next?.id | ||||
|   ) | ||||
|   const { theme } = useTheme() | ||||
|  | ||||
|   const queryClient = useQueryClient() | ||||
|   | ||||
| @@ -36,7 +36,10 @@ const TimelineDefault: React.FC<Props> = ({ | ||||
|   disableOnPress = false | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const localAccount = useSelector( | ||||
|     getLocalAccount, | ||||
|     (prev, next) => prev?.id === next?.id | ||||
|   ) | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.TabLocalStackParamList> | ||||
|   >() | ||||
|   | ||||
| @@ -29,7 +29,10 @@ const TimelineNotifications: React.FC<Props> = ({ | ||||
|   highlighted = false | ||||
| }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const localAccount = useSelector( | ||||
|     getLocalAccount, | ||||
|     (prev, next) => prev?.id === next?.id | ||||
|   ) | ||||
|   const navigation = useNavigation< | ||||
|     StackNavigationProp<Nav.TabLocalStackParamList> | ||||
|   >() | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| export default { | ||||
|   heading: 'Direct messages', | ||||
|   heading: 'Direct Messages', | ||||
|   content: {} | ||||
| } | ||||
|   | ||||
| @@ -14,7 +14,8 @@ export default { | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     settings: '$t(meSettings:heading)', | ||||
|     accountSettings: 'Account Settings', | ||||
|     appSettings: '$t(meSettings:heading)', | ||||
|     logout: { | ||||
|       button: 'Log out', | ||||
|       alert: { | ||||
|   | ||||
| @@ -14,7 +14,8 @@ export default { | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     settings: '$t(meSettings:heading)', | ||||
|     accountSettings: '账户设置', | ||||
|     appSettings: '$t(meSettings:heading)', | ||||
|     logout: { | ||||
|       button: '退出当前账号', | ||||
|       alert: { | ||||
|   | ||||
| @@ -1,32 +1,7 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import { StackScreenProps } from '@react-navigation/stack' | ||||
| import { getLocalAccount, getLocalUrl } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useMemo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Dimensions, StyleSheet, View } from 'react-native' | ||||
| import { | ||||
|   PanGestureHandler, | ||||
|   State, | ||||
|   TapGestureHandler | ||||
| } from 'react-native-gesture-handler' | ||||
| import Animated, { | ||||
|   Extrapolate, | ||||
|   interpolate, | ||||
|   runOnJS, | ||||
|   useAnimatedGestureHandler, | ||||
|   useAnimatedStyle, | ||||
|   useSharedValue, | ||||
|   withTiming | ||||
| } from 'react-native-reanimated' | ||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||
| import { useSelector } from 'react-redux' | ||||
| import ActionsAccount from './Actions/Account' | ||||
| import ActionsDomain from './Actions/Domain' | ||||
| import ActionsShare from './Actions/Share' | ||||
| import ActionsStatus from './Actions/Status' | ||||
| import React from 'react' | ||||
| import { SafeAreaProvider } from 'react-native-safe-area-context' | ||||
| import ScreenActionsRoot from './Actions/Root' | ||||
|  | ||||
| export type ScreenAccountProp = StackScreenProps< | ||||
|   Nav.RootStackParamList, | ||||
| @@ -34,188 +9,14 @@ export type ScreenAccountProp = StackScreenProps< | ||||
| > | ||||
|  | ||||
| const ScreenActions = React.memo( | ||||
|   ({ route: { params }, navigation }: ScreenAccountProp) => { | ||||
|     const { t } = useTranslation() | ||||
|  | ||||
|     const localAccount = useSelector(getLocalAccount) | ||||
|     let sameAccount = false | ||||
|     switch (params.type) { | ||||
|       case 'status': | ||||
|         sameAccount = localAccount?.id === params.status.account.id | ||||
|         break | ||||
|       case 'account': | ||||
|         sameAccount = localAccount?.id === params.account.id | ||||
|         break | ||||
|     } | ||||
|  | ||||
|     const localDomain = useSelector(getLocalUrl) | ||||
|     let sameDomain = true | ||||
|     let statusDomain: string | ||||
|     switch (params.type) { | ||||
|       case 'status': | ||||
|         statusDomain = params.status.uri | ||||
|           ? params.status.uri.split(new RegExp(/\/\/(.*?)\//))[1] | ||||
|           : '' | ||||
|         sameDomain = localDomain === statusDomain | ||||
|         break | ||||
|     } | ||||
|  | ||||
|     const { theme } = useTheme() | ||||
|     const insets = useSafeAreaInsets() | ||||
|  | ||||
|     const DEFAULT_VALUE = 350 | ||||
|     const screenHeight = Dimensions.get('screen').height | ||||
|     const panY = useSharedValue(DEFAULT_VALUE) | ||||
|     useEffect(() => { | ||||
|       panY.value = withTiming(0) | ||||
|     }, []) | ||||
|     const styleTop = useAnimatedStyle(() => { | ||||
|       return { | ||||
|         bottom: interpolate( | ||||
|           panY.value, | ||||
|           [0, screenHeight], | ||||
|           [0, -screenHeight], | ||||
|           Extrapolate.CLAMP | ||||
|         ) | ||||
|       } | ||||
|     }) | ||||
|     const dismiss = useCallback(() => { | ||||
|       panY.value = withTiming(DEFAULT_VALUE) | ||||
|       navigation.goBack() | ||||
|     }, []) | ||||
|     const onGestureEvent = useAnimatedGestureHandler({ | ||||
|       onActive: ({ translationY }) => { | ||||
|         panY.value = translationY | ||||
|       }, | ||||
|       onEnd: ({ velocityY }) => { | ||||
|         if (velocityY > 500) { | ||||
|           runOnJS(dismiss)() | ||||
|         } else { | ||||
|           panY.value = withTiming(0) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     const actions = useMemo(() => { | ||||
|       switch (params.type) { | ||||
|         case 'status': | ||||
|           return ( | ||||
|             <> | ||||
|               {!sameAccount && ( | ||||
|                 <ActionsAccount | ||||
|                   queryKey={params.queryKey} | ||||
|                   account={params.status.account} | ||||
|                   dismiss={dismiss} | ||||
|                 /> | ||||
|               )} | ||||
|               {sameAccount && params.status && ( | ||||
|                 <ActionsStatus | ||||
|                   navigation={navigation} | ||||
|                   queryKey={params.queryKey} | ||||
|                   status={params.status} | ||||
|                   dismiss={dismiss} | ||||
|                 /> | ||||
|               )} | ||||
|               {!sameDomain && statusDomain && ( | ||||
|                 <ActionsDomain | ||||
|                   queryKey={params.queryKey} | ||||
|                   domain={statusDomain} | ||||
|                   dismiss={dismiss} | ||||
|                 /> | ||||
|               )} | ||||
|               <ActionsShare | ||||
|                 url={params.status.url || params.status.uri} | ||||
|                 type={params.type} | ||||
|                 dismiss={dismiss} | ||||
|               /> | ||||
|             </> | ||||
|           ) | ||||
|         case 'account': | ||||
|           return ( | ||||
|             <> | ||||
|               {!sameAccount && ( | ||||
|                 <ActionsAccount account={params.account} dismiss={dismiss} /> | ||||
|               )} | ||||
|               <ActionsShare | ||||
|                 url={params.account.url} | ||||
|                 type={params.type} | ||||
|                 dismiss={dismiss} | ||||
|               /> | ||||
|             </> | ||||
|           ) | ||||
|       } | ||||
|     }, []) | ||||
|  | ||||
|   (props: ScreenAccountProp) => { | ||||
|     return ( | ||||
|       <Animated.View style={{ flex: 1 }}> | ||||
|         <TapGestureHandler | ||||
|           onHandlerStateChange={({ nativeEvent }) => { | ||||
|             if (nativeEvent.state === State.ACTIVE) { | ||||
|               dismiss() | ||||
|             } | ||||
|           }} | ||||
|         > | ||||
|           <Animated.View | ||||
|             style={[ | ||||
|               styles.overlay, | ||||
|               { backgroundColor: theme.backgroundOverlay } | ||||
|             ]} | ||||
|           > | ||||
|             <PanGestureHandler onGestureEvent={onGestureEvent}> | ||||
|               <Animated.View | ||||
|                 style={[ | ||||
|                   styles.container, | ||||
|                   styleTop, | ||||
|                   { | ||||
|                     backgroundColor: theme.background, | ||||
|                     paddingBottom: insets.bottom || StyleConstants.Spacing.L | ||||
|                   } | ||||
|                 ]} | ||||
|               > | ||||
|                 <View | ||||
|                   style={[ | ||||
|                     styles.handle, | ||||
|                     { backgroundColor: theme.primaryOverlay } | ||||
|                   ]} | ||||
|                 /> | ||||
|                 {actions} | ||||
|                 <Button | ||||
|                   type='text' | ||||
|                   content={t('common:buttons.cancel')} | ||||
|                   onPress={() => { | ||||
|                     analytics('bottomsheet_cancel') | ||||
|                     // dismiss() | ||||
|                   }} | ||||
|                   style={styles.button} | ||||
|                 /> | ||||
|               </Animated.View> | ||||
|             </PanGestureHandler> | ||||
|           </Animated.View> | ||||
|         </TapGestureHandler> | ||||
|       </Animated.View> | ||||
|       <SafeAreaProvider> | ||||
|         <ScreenActionsRoot {...props} /> | ||||
|       </SafeAreaProvider> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   overlay: { | ||||
|     flex: 1, | ||||
|     justifyContent: 'flex-end' | ||||
|   }, | ||||
|   container: { | ||||
|     paddingTop: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   handle: { | ||||
|     alignSelf: 'center', | ||||
|     width: StyleConstants.Spacing.S * 8, | ||||
|     height: StyleConstants.Spacing.S / 2, | ||||
|     borderRadius: 100, | ||||
|     top: -StyleConstants.Spacing.M * 2 | ||||
|   }, | ||||
|   button: { | ||||
|     marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2 | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ScreenActions | ||||
|   | ||||
							
								
								
									
										224
									
								
								src/screens/Actions/Root.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								src/screens/Actions/Root.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,224 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import { StackScreenProps } from '@react-navigation/stack' | ||||
| import { getLocalAccount, getLocalUrl } from '@utils/slices/instancesSlice' | ||||
| import { StyleConstants } from '@utils/styles/constants' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useMemo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { Dimensions, StyleSheet, View } from 'react-native' | ||||
| import { | ||||
|   PanGestureHandler, | ||||
|   State, | ||||
|   TapGestureHandler | ||||
| } from 'react-native-gesture-handler' | ||||
| import Animated, { | ||||
|   Extrapolate, | ||||
|   interpolate, | ||||
|   runOnJS, | ||||
|   useAnimatedGestureHandler, | ||||
|   useAnimatedStyle, | ||||
|   useSharedValue, | ||||
|   withTiming | ||||
| } from 'react-native-reanimated' | ||||
| import { useSafeAreaInsets } from 'react-native-safe-area-context' | ||||
| import { useSelector } from 'react-redux' | ||||
| import ActionsAccount from './Account' | ||||
| import ActionsDomain from './Domain' | ||||
| import ActionsShare from './Share' | ||||
| import ActionsStatus from './Status' | ||||
|  | ||||
| export type ScreenAccountProp = StackScreenProps< | ||||
|   Nav.RootStackParamList, | ||||
|   'Screen-Actions' | ||||
| > | ||||
|  | ||||
| const ScreenActionsRoot = React.memo( | ||||
|   ({ route: { params }, navigation }: ScreenAccountProp) => { | ||||
|     const { t } = useTranslation() | ||||
|  | ||||
|     const localAccount = useSelector( | ||||
|       getLocalAccount, | ||||
|       (prev, next) => prev?.id === next?.id | ||||
|     ) | ||||
|     let sameAccount = false | ||||
|     switch (params.type) { | ||||
|       case 'status': | ||||
|         sameAccount = localAccount?.id === params.status.account.id | ||||
|         break | ||||
|       case 'account': | ||||
|         sameAccount = localAccount?.id === params.account.id | ||||
|         break | ||||
|     } | ||||
|  | ||||
|     const localDomain = useSelector(getLocalUrl) | ||||
|     let sameDomain = true | ||||
|     let statusDomain: string | ||||
|     switch (params.type) { | ||||
|       case 'status': | ||||
|         statusDomain = params.status.uri | ||||
|           ? params.status.uri.split(new RegExp(/\/\/(.*?)\//))[1] | ||||
|           : '' | ||||
|         sameDomain = localDomain === statusDomain | ||||
|         break | ||||
|     } | ||||
|  | ||||
|     const { theme } = useTheme() | ||||
|     const insets = useSafeAreaInsets() | ||||
|  | ||||
|     const DEFAULT_VALUE = 350 | ||||
|     const screenHeight = Dimensions.get('screen').height | ||||
|     const panY = useSharedValue(DEFAULT_VALUE) | ||||
|     useEffect(() => { | ||||
|       panY.value = withTiming(0) | ||||
|     }, []) | ||||
|     const styleTop = useAnimatedStyle(() => { | ||||
|       return { | ||||
|         bottom: interpolate( | ||||
|           panY.value, | ||||
|           [0, screenHeight], | ||||
|           [0, -screenHeight], | ||||
|           Extrapolate.CLAMP | ||||
|         ) | ||||
|       } | ||||
|     }) | ||||
|     const dismiss = useCallback(() => { | ||||
|       panY.value = withTiming(DEFAULT_VALUE) | ||||
|       navigation.goBack() | ||||
|     }, []) | ||||
|     const onGestureEvent = useAnimatedGestureHandler({ | ||||
|       onActive: ({ translationY }) => { | ||||
|         panY.value = translationY | ||||
|       }, | ||||
|       onEnd: ({ velocityY }) => { | ||||
|         if (velocityY > 500) { | ||||
|           runOnJS(dismiss)() | ||||
|         } else { | ||||
|           panY.value = withTiming(0) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     const actions = useMemo(() => { | ||||
|       switch (params.type) { | ||||
|         case 'status': | ||||
|           return ( | ||||
|             <> | ||||
|               {!sameAccount && ( | ||||
|                 <ActionsAccount | ||||
|                   queryKey={params.queryKey} | ||||
|                   account={params.status.account} | ||||
|                   dismiss={dismiss} | ||||
|                 /> | ||||
|               )} | ||||
|               {sameAccount && params.status && ( | ||||
|                 <ActionsStatus | ||||
|                   navigation={navigation} | ||||
|                   queryKey={params.queryKey} | ||||
|                   status={params.status} | ||||
|                   dismiss={dismiss} | ||||
|                 /> | ||||
|               )} | ||||
|               {!sameDomain && statusDomain && ( | ||||
|                 <ActionsDomain | ||||
|                   queryKey={params.queryKey} | ||||
|                   domain={statusDomain} | ||||
|                   dismiss={dismiss} | ||||
|                 /> | ||||
|               )} | ||||
|               <ActionsShare | ||||
|                 url={params.status.url || params.status.uri} | ||||
|                 type={params.type} | ||||
|                 dismiss={dismiss} | ||||
|               /> | ||||
|             </> | ||||
|           ) | ||||
|         case 'account': | ||||
|           return ( | ||||
|             <> | ||||
|               {!sameAccount && ( | ||||
|                 <ActionsAccount account={params.account} dismiss={dismiss} /> | ||||
|               )} | ||||
|               <ActionsShare | ||||
|                 url={params.account.url} | ||||
|                 type={params.type} | ||||
|                 dismiss={dismiss} | ||||
|               /> | ||||
|             </> | ||||
|           ) | ||||
|       } | ||||
|     }, []) | ||||
|  | ||||
|     return ( | ||||
|       <Animated.View style={{ flex: 1 }}> | ||||
|         <TapGestureHandler | ||||
|           onHandlerStateChange={({ nativeEvent }) => { | ||||
|             if (nativeEvent.state === State.ACTIVE) { | ||||
|               dismiss() | ||||
|             } | ||||
|           }} | ||||
|         > | ||||
|           <Animated.View | ||||
|             style={[ | ||||
|               styles.overlay, | ||||
|               { backgroundColor: theme.backgroundOverlay } | ||||
|             ]} | ||||
|           > | ||||
|             <PanGestureHandler onGestureEvent={onGestureEvent}> | ||||
|               <Animated.View | ||||
|                 style={[ | ||||
|                   styles.container, | ||||
|                   styleTop, | ||||
|                   { | ||||
|                     backgroundColor: theme.background, | ||||
|                     paddingBottom: insets.bottom || StyleConstants.Spacing.L | ||||
|                   } | ||||
|                 ]} | ||||
|               > | ||||
|                 <View | ||||
|                   style={[ | ||||
|                     styles.handle, | ||||
|                     { backgroundColor: theme.primaryOverlay } | ||||
|                   ]} | ||||
|                 /> | ||||
|                 {actions} | ||||
|                 <Button | ||||
|                   type='text' | ||||
|                   content={t('common:buttons.cancel')} | ||||
|                   onPress={() => { | ||||
|                     analytics('bottomsheet_cancel') | ||||
|                     // dismiss() | ||||
|                   }} | ||||
|                   style={styles.button} | ||||
|                 /> | ||||
|               </Animated.View> | ||||
|             </PanGestureHandler> | ||||
|           </Animated.View> | ||||
|         </TapGestureHandler> | ||||
|       </Animated.View> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   overlay: { | ||||
|     flex: 1, | ||||
|     justifyContent: 'flex-end' | ||||
|   }, | ||||
|   container: { | ||||
|     paddingTop: StyleConstants.Spacing.M | ||||
|   }, | ||||
|   handle: { | ||||
|     alignSelf: 'center', | ||||
|     width: StyleConstants.Spacing.S * 8, | ||||
|     height: StyleConstants.Spacing.S / 2, | ||||
|     borderRadius: 100, | ||||
|     top: -StyleConstants.Spacing.M * 2 | ||||
|   }, | ||||
|   button: { | ||||
|     marginHorizontal: StyleConstants.Spacing.Global.PagePadding * 2 | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ScreenActionsRoot | ||||
| @@ -1,6 +1,7 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import Button from '@components/Button' | ||||
| import haptics from '@components/haptics' | ||||
| import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import { ParseHTML } from '@components/Parse' | ||||
| import RelativeTime from '@components/RelativeTime' | ||||
| import { BlurView } from '@react-native-community/blur' | ||||
| @@ -203,7 +204,29 @@ const ScreenAnnouncements: React.FC<ScreenAnnouncementsProp> = ({ | ||||
|       style={styles.base} | ||||
|       reducedTransparencyFallbackColor={theme.background} | ||||
|     > | ||||
|       <SafeAreaView style={styles.base}> | ||||
|       <SafeAreaView style={styles.base} edges={['bottom']}> | ||||
|         <View | ||||
|           style={{ | ||||
|             flex: 1, | ||||
|             flexDirection: 'row', | ||||
|             justifyContent: 'space-between', | ||||
|             alignItems: 'center' | ||||
|           }} | ||||
|         > | ||||
|           <HeaderLeft | ||||
|             content='X' | ||||
|             native={false} | ||||
|             onPress={() => navigation.goBack()} | ||||
|           /> | ||||
|           <HeaderCenter content={t('sharedAnnouncements:heading')} /> | ||||
|           <View style={{ opacity: 0 }}> | ||||
|             <HeaderRight | ||||
|               content='MoreHorizontal' | ||||
|               native={false} | ||||
|               onPress={() => {}} | ||||
|             /> | ||||
|           </View> | ||||
|         </View> | ||||
|         <FlatList | ||||
|           horizontal | ||||
|           data={query.data} | ||||
|   | ||||
| @@ -75,7 +75,12 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({ | ||||
|     setHasKeyboard(false) | ||||
|   } | ||||
|  | ||||
|   // const draft = useSelector(getLocalDraft, () => true) | ||||
|   const localAccount = useSelector(getLocalAccount, (prev, next) => | ||||
|     prev?.preferences && next?.preferences | ||||
|       ? prev?.preferences['posting:default:visibility'] === | ||||
|         next?.preferences['posting:default:visibility'] | ||||
|       : true | ||||
|   ) | ||||
|   const initialReducerState = useMemo(() => { | ||||
|     if (params) { | ||||
|       return composeParseState(params) | ||||
| @@ -92,7 +97,6 @@ const ScreenCompose: React.FC<ScreenComposeProp> = ({ | ||||
|     } | ||||
|   }, []) | ||||
|  | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const [composeState, composeDispatch] = useReducer( | ||||
|     composeReducer, | ||||
|     initialReducerState | ||||
|   | ||||
| @@ -18,12 +18,12 @@ const SingleEmoji = ({ emoji }: { emoji: Mastodon.Emoji }) => { | ||||
|       newText: `:${emoji.shortcode}:`, | ||||
|       type: 'emoji' | ||||
|     }) | ||||
|     composeDispatch({ | ||||
|       type: 'emoji', | ||||
|       payload: { ...composeState.emoji, active: false } | ||||
|     }) | ||||
|     // composeDispatch({ | ||||
|     //   type: 'emoji', | ||||
|     //   payload: { ...composeState.emoji, active: false } | ||||
|     // }) | ||||
|     haptics('Success') | ||||
|   }, []) | ||||
|   }, [composeState]) | ||||
|   const children = useMemo( | ||||
|     () => <FastImage source={{ uri: emoji.url }} style={styles.emoji} />, | ||||
|     [] | ||||
|   | ||||
| @@ -14,7 +14,10 @@ import ComposeTextInput from './Header/TextInput' | ||||
| const ComposeRootHeader: React.FC = () => { | ||||
|   const { composeState } = useContext(ComposeContext) | ||||
|   const localActiveIndex = useSelector(getLocalActiveIndex) | ||||
|   const localInstances = useSelector(getLocalInstances) | ||||
|   const localInstances = useSelector( | ||||
|     getLocalInstances, | ||||
|     (prev, next) => prev.length === next.length | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|   | ||||
| @@ -11,7 +11,10 @@ const ComposePostingAs = React.memo( | ||||
|     const { t } = useTranslation('sharedCompose') | ||||
|     const { theme } = useTheme() | ||||
|  | ||||
|     const localAccount = useSelector(getLocalAccount) | ||||
|     const localAccount = useSelector( | ||||
|       getLocalAccount, | ||||
|       (prev, next) => prev?.acct === next?.acct | ||||
|     ) | ||||
|     const localUri = useSelector(getLocalUri) | ||||
|  | ||||
|     return ( | ||||
|   | ||||
| @@ -1,17 +1,21 @@ | ||||
| import analytics from '@components/analytics' | ||||
| import { HeaderRight } from '@components/Header' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import { StackScreenProps } from '@react-navigation/stack' | ||||
| import CameraRoll from '@react-native-community/cameraroll' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { PermissionsAndroid, Platform, Share } from 'react-native' | ||||
| import FastImage from 'react-native-fast-image' | ||||
| import ImageViewer from 'react-native-image-zoom-viewer' | ||||
| import { SharedElement } from 'react-navigation-shared-element' | ||||
| import { HeaderCenter, HeaderLeft, HeaderRight } from '@components/Header' | ||||
| import { toast } from '@components/toast' | ||||
| import { useActionSheet } from '@expo/react-native-action-sheet' | ||||
| import CameraRoll from '@react-native-community/cameraroll' | ||||
| import { StackScreenProps } from '@react-navigation/stack' | ||||
| import ImageView from '@root/modules/react-native-image-viewing/src/index' | ||||
| import { findIndex } from 'lodash' | ||||
| import React, { useCallback, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { | ||||
|   PermissionsAndroid, | ||||
|   Platform, | ||||
|   Share, | ||||
|   StyleSheet, | ||||
|   View | ||||
| } from 'react-native' | ||||
| import { SafeAreaView } from 'react-native-safe-area-context' | ||||
|  | ||||
| export type ScreenImagesViewerProp = StackScreenProps< | ||||
|   Nav.RootStackParamList, | ||||
| @@ -24,7 +28,6 @@ const ScreenImagesViewer = ({ | ||||
|   }, | ||||
|   navigation | ||||
| }: ScreenImagesViewerProp) => { | ||||
|   const { theme } = useTheme() | ||||
|   const [currentIndex, setCurrentIndex] = useState( | ||||
|     findIndex(imageUrls, ['imageIndex', imageIndex]) | ||||
|   ) | ||||
| @@ -95,45 +98,53 @@ const ScreenImagesViewer = ({ | ||||
|     ) | ||||
|   }, [currentIndex]) | ||||
|  | ||||
|   useEffect( | ||||
|     () => | ||||
|       navigation.setOptions({ | ||||
|         headerTitle: `${currentIndex + 1} / ${imageUrls.length}`, | ||||
|         headerTintColor: theme.primaryOverlay, | ||||
|         headerRight: () => ( | ||||
|           <HeaderRight | ||||
|             content='MoreHorizontal' | ||||
|             native={false} | ||||
|             onPress={onPress} | ||||
|           /> | ||||
|         ) | ||||
|       }), | ||||
|   const HeaderComponent = useCallback( | ||||
|     () => ( | ||||
|       <View | ||||
|         style={{ | ||||
|           flex: 1, | ||||
|           flexDirection: 'row', | ||||
|           justifyContent: 'space-between', | ||||
|           alignItems: 'center' | ||||
|         }} | ||||
|       > | ||||
|         <HeaderLeft | ||||
|           content='X' | ||||
|           native={false} | ||||
|           onPress={() => navigation.goBack()} | ||||
|         /> | ||||
|         <HeaderCenter | ||||
|           inverted | ||||
|           content={`${currentIndex + 1} / ${imageUrls.length}`} | ||||
|         /> | ||||
|         <HeaderRight | ||||
|           content='MoreHorizontal' | ||||
|           native={false} | ||||
|           onPress={onPress} | ||||
|         /> | ||||
|       </View> | ||||
|     ), | ||||
|     [currentIndex] | ||||
|   ) | ||||
|  | ||||
|   const renderImage = useCallback( | ||||
|     prop => ( | ||||
|       <SharedElement id={`imageFail.${imageUrls[imageIndex].url}`}> | ||||
|         <FastImage {...prop} /> | ||||
|       </SharedElement> | ||||
|     ), | ||||
|     [] | ||||
|   ) | ||||
|  | ||||
|   return ( | ||||
|     <ImageViewer | ||||
|       index={imageIndex} | ||||
|       imageUrls={imageUrls} | ||||
|       enableSwipeDown | ||||
|       useNativeDriver | ||||
|       swipeDownThreshold={100} | ||||
|       renderIndicator={() => <></>} | ||||
|       saveToLocalByLongPress={false} | ||||
|       onSwipeDown={() => navigation.goBack()} | ||||
|       onChange={index => index && setCurrentIndex(index)} | ||||
|       renderImage={renderImage} | ||||
|     /> | ||||
|     <SafeAreaView style={styles.base} edges={['top']}> | ||||
|       <ImageView | ||||
|         images={imageUrls.map(urls => ({ uri: urls.url }))} | ||||
|         imageIndex={imageIndex} | ||||
|         onImageIndexChange={index => setCurrentIndex(index)} | ||||
|         onRequestClose={() => navigation.goBack()} | ||||
|         HeaderComponent={HeaderComponent} | ||||
|       /> | ||||
|     </SafeAreaView> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| const styles = StyleSheet.create({ | ||||
|   base: { | ||||
|     flex: 1, | ||||
|     backgroundColor: 'black' | ||||
|   } | ||||
| }) | ||||
|  | ||||
| export default ScreenImagesViewer | ||||
|   | ||||
| @@ -7,17 +7,18 @@ import { | ||||
| } from '@react-navigation/bottom-tabs' | ||||
| import { NavigatorScreenParams } from '@react-navigation/native' | ||||
| import { StackScreenProps } from '@react-navigation/stack' | ||||
| import { useTimelineQuery } from '@utils/queryHooks/timeline' | ||||
| import { | ||||
|   getLocalAccount, | ||||
|   getLocalActiveIndex, | ||||
|   getLocalInstances, | ||||
|   getLocalNotification | ||||
|   getLocalNotification, | ||||
|   updateLocalNotification | ||||
| } from '@utils/slices/instancesSlice' | ||||
| import { useTheme } from '@utils/styles/ThemeManager' | ||||
| import React, { useCallback, useEffect, useMemo } from 'react' | ||||
| import React, { useCallback, useMemo } from 'react' | ||||
| import { Platform } from 'react-native' | ||||
| import FastImage from 'react-native-fast-image' | ||||
| import { useSelector } from 'react-redux' | ||||
| import { useDispatch, useSelector } from 'react-redux' | ||||
| import TabLocal from './Tabs/Local' | ||||
| import TabMe from './Tabs/Me' | ||||
| import TabNotifications from './Tabs/Notifications' | ||||
| @@ -38,143 +39,171 @@ export type ScreenTabsProp = StackScreenProps< | ||||
|  | ||||
| const Tab = createBottomTabNavigator<Nav.ScreenTabsStackParamList>() | ||||
|  | ||||
| const ScreenTabs: React.FC<ScreenTabsProp> = ({ navigation }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const localActiveIndex = useSelector(getLocalActiveIndex) | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
| const ScreenTabs = React.memo( | ||||
|   ({ navigation }: ScreenTabsProp) => { | ||||
|     const { theme } = useTheme() | ||||
|     const dispatch = useDispatch() | ||||
|     const localActiveIndex = useSelector(getLocalActiveIndex) | ||||
|     const localAccount = useSelector( | ||||
|       getLocalAccount, | ||||
|       (prev, next) => prev?.avatarStatic === next?.avatarStatic | ||||
|     ) | ||||
|  | ||||
|   const screenOptions = useCallback( | ||||
|     ({ route }): BottomTabNavigationOptions => ({ | ||||
|       tabBarIcon: ({ | ||||
|         focused, | ||||
|         color, | ||||
|         size | ||||
|       }: { | ||||
|         focused: boolean | ||||
|         color: string | ||||
|         size: number | ||||
|       }) => { | ||||
|         switch (route.name) { | ||||
|           case 'Tab-Local': | ||||
|             return <Icon name='Home' size={size} color={color} /> | ||||
|           case 'Tab-Public': | ||||
|             return <Icon name='Globe' size={size} color={color} /> | ||||
|           case 'Tab-Compose': | ||||
|             return <Icon name='Plus' size={size} color={color} /> | ||||
|           case 'Tab-Notifications': | ||||
|             return <Icon name='Bell' size={size} color={color} /> | ||||
|           case 'Tab-Me': | ||||
|             return localActiveIndex !== null ? ( | ||||
|               <FastImage | ||||
|                 source={{ uri: localAccount?.avatarStatic }} | ||||
|                 style={{ | ||||
|                   width: size, | ||||
|                   height: size, | ||||
|                   borderRadius: size, | ||||
|                   borderWidth: focused ? 2 : 0, | ||||
|                   borderColor: focused ? theme.secondary : color | ||||
|                 }} | ||||
|               /> | ||||
|             ) : ( | ||||
|               <Icon | ||||
|                 name={focused ? 'Meh' : 'Smile'} | ||||
|                 size={size} | ||||
|                 color={!focused ? theme.secondary : color} | ||||
|               /> | ||||
|             ) | ||||
|           default: | ||||
|             return <Icon name='AlertOctagon' size={size} color={color} /> | ||||
|         } | ||||
|       } | ||||
|     }), | ||||
|     [localActiveIndex, localAccount] | ||||
|   ) | ||||
|   const tabBarOptions = useMemo( | ||||
|     () => ({ | ||||
|       activeTintColor: theme.primary, | ||||
|       inactiveTintColor: | ||||
|         localActiveIndex !== null ? theme.secondary : theme.disabled, | ||||
|       showLabel: false, | ||||
|       ...(Platform.OS === 'android' && { keyboardHidesTabBar: true }) | ||||
|     }), | ||||
|     [theme, localActiveIndex] | ||||
|   ) | ||||
|   const localListeners = useCallback( | ||||
|     () => ({ | ||||
|       tabPress: (e: any) => { | ||||
|         if (!(localActiveIndex !== null)) { | ||||
|           e.preventDefault() | ||||
|         } | ||||
|       } | ||||
|     }), | ||||
|     [localActiveIndex] | ||||
|   ) | ||||
|   const composeListeners = useMemo( | ||||
|     () => ({ | ||||
|       tabPress: (e: any) => { | ||||
|         e.preventDefault() | ||||
|         if (localActiveIndex !== null) { | ||||
|           haptics('Light') | ||||
|           navigation.navigate('Screen-Compose') | ||||
|         } | ||||
|       } | ||||
|     }), | ||||
|     [localActiveIndex] | ||||
|   ) | ||||
|   const composeComponent = useCallback(() => null, []) | ||||
|   const notificationsListeners = useCallback( | ||||
|     () => ({ | ||||
|       tabPress: (e: any) => { | ||||
|         if (!(localActiveIndex !== null)) { | ||||
|           e.preventDefault() | ||||
|         } | ||||
|       } | ||||
|     }), | ||||
|     [localActiveIndex] | ||||
|   ) | ||||
|  | ||||
|   // On launch check if there is any unread noficiations | ||||
|   useWebsocket({ stream: 'user', event: 'notification' }) | ||||
|   const localNotification = useSelector(getLocalNotification) | ||||
|  | ||||
|   return ( | ||||
|     <Tab.Navigator | ||||
|       initialRouteName={localActiveIndex !== null ? 'Tab-Local' : 'Tab-Me'} | ||||
|       screenOptions={screenOptions} | ||||
|       tabBarOptions={tabBarOptions} | ||||
|     > | ||||
|       <Tab.Screen | ||||
|         name='Tab-Local' | ||||
|         component={TabLocal} | ||||
|         listeners={localListeners} | ||||
|       /> | ||||
|       <Tab.Screen name='Tab-Public' component={TabPublic} /> | ||||
|       <Tab.Screen | ||||
|         name='Tab-Compose' | ||||
|         component={composeComponent} | ||||
|         listeners={composeListeners} | ||||
|       /> | ||||
|       <Tab.Screen | ||||
|         name='Tab-Notifications' | ||||
|         component={TabNotifications} | ||||
|         listeners={notificationsListeners} | ||||
|         options={{ | ||||
|           tabBarBadge: localNotification?.latestTime | ||||
|             ? !localNotification.readTime || | ||||
|               new Date(localNotification.readTime) < | ||||
|                 new Date(localNotification.latestTime) | ||||
|               ? '' | ||||
|               : undefined | ||||
|             : undefined, | ||||
|           tabBarBadgeStyle: { | ||||
|             transform: [{ scale: 0.5 }], | ||||
|             backgroundColor: theme.red | ||||
|     const screenOptions = useCallback( | ||||
|       ({ route }): BottomTabNavigationOptions => ({ | ||||
|         tabBarIcon: ({ | ||||
|           focused, | ||||
|           color, | ||||
|           size | ||||
|         }: { | ||||
|           focused: boolean | ||||
|           color: string | ||||
|           size: number | ||||
|         }) => { | ||||
|           switch (route.name) { | ||||
|             case 'Tab-Local': | ||||
|               return <Icon name='Home' size={size} color={color} /> | ||||
|             case 'Tab-Public': | ||||
|               return <Icon name='Globe' size={size} color={color} /> | ||||
|             case 'Tab-Compose': | ||||
|               return <Icon name='Plus' size={size} color={color} /> | ||||
|             case 'Tab-Notifications': | ||||
|               return <Icon name='Bell' size={size} color={color} /> | ||||
|             case 'Tab-Me': | ||||
|               return localActiveIndex !== null ? ( | ||||
|                 <FastImage | ||||
|                   source={{ uri: localAccount?.avatarStatic }} | ||||
|                   style={{ | ||||
|                     width: size, | ||||
|                     height: size, | ||||
|                     borderRadius: size, | ||||
|                     borderWidth: focused ? 2 : 0, | ||||
|                     borderColor: focused ? theme.secondary : color | ||||
|                   }} | ||||
|                 /> | ||||
|               ) : ( | ||||
|                 <Icon | ||||
|                   name={focused ? 'Meh' : 'Smile'} | ||||
|                   size={size} | ||||
|                   color={!focused ? theme.secondary : color} | ||||
|                 /> | ||||
|               ) | ||||
|             default: | ||||
|               return <Icon name='AlertOctagon' size={size} color={color} /> | ||||
|           } | ||||
|         }} | ||||
|       /> | ||||
|       <Tab.Screen name='Tab-Me' component={TabMe} /> | ||||
|     </Tab.Navigator> | ||||
|   ) | ||||
| } | ||||
|         } | ||||
|       }), | ||||
|       [localActiveIndex, localAccount?.avatarStatic] | ||||
|     ) | ||||
|     const tabBarOptions = useMemo( | ||||
|       () => ({ | ||||
|         activeTintColor: theme.primary, | ||||
|         inactiveTintColor: | ||||
|           localActiveIndex !== null ? theme.secondary : theme.disabled, | ||||
|         showLabel: false, | ||||
|         ...(Platform.OS === 'android' && { keyboardHidesTabBar: true }) | ||||
|       }), | ||||
|       [theme, localActiveIndex] | ||||
|     ) | ||||
|     const localListeners = useCallback( | ||||
|       () => ({ | ||||
|         tabPress: (e: any) => { | ||||
|           if (!(localActiveIndex !== null)) { | ||||
|             e.preventDefault() | ||||
|           } | ||||
|         } | ||||
|       }), | ||||
|       [localActiveIndex] | ||||
|     ) | ||||
|     const composeListeners = useMemo( | ||||
|       () => ({ | ||||
|         tabPress: (e: any) => { | ||||
|           e.preventDefault() | ||||
|           if (localActiveIndex !== null) { | ||||
|             haptics('Light') | ||||
|             navigation.navigate('Screen-Compose') | ||||
|           } | ||||
|         } | ||||
|       }), | ||||
|       [localActiveIndex] | ||||
|     ) | ||||
|     const composeComponent = useCallback(() => null, []) | ||||
|     const notificationsListeners = useCallback( | ||||
|       () => ({ | ||||
|         tabPress: (e: any) => { | ||||
|           if (!(localActiveIndex !== null)) { | ||||
|             e.preventDefault() | ||||
|           } | ||||
|         } | ||||
|       }), | ||||
|       [localActiveIndex] | ||||
|     ) | ||||
|  | ||||
|     // On launch check if there is any unread noficiations | ||||
|     useTimelineQuery({ | ||||
|       page: 'Notifications', | ||||
|       options: { | ||||
|         notifyOnChangeProps: [], | ||||
|         select: data => { | ||||
|           if (data.pages[0].length) { | ||||
|             dispatch( | ||||
|               updateLocalNotification({ | ||||
|                 latestTime: data.pages[0][0].created_at | ||||
|               }) | ||||
|             ) | ||||
|           } | ||||
|           return data | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     useWebsocket({ stream: 'user', event: 'notification' }) | ||||
|     const localNotification = useSelector( | ||||
|       getLocalNotification, | ||||
|       (prev, next) => | ||||
|         prev?.readTime === next?.readTime && | ||||
|         prev?.latestTime === next?.latestTime | ||||
|     ) | ||||
|  | ||||
|     return ( | ||||
|       <Tab.Navigator | ||||
|         initialRouteName={localActiveIndex !== null ? 'Tab-Local' : 'Tab-Me'} | ||||
|         screenOptions={screenOptions} | ||||
|         tabBarOptions={tabBarOptions} | ||||
|       > | ||||
|         <Tab.Screen | ||||
|           name='Tab-Local' | ||||
|           component={TabLocal} | ||||
|           listeners={localListeners} | ||||
|         /> | ||||
|         <Tab.Screen name='Tab-Public' component={TabPublic} /> | ||||
|         <Tab.Screen | ||||
|           name='Tab-Compose' | ||||
|           component={composeComponent} | ||||
|           listeners={composeListeners} | ||||
|         /> | ||||
|         <Tab.Screen | ||||
|           name='Tab-Notifications' | ||||
|           component={TabNotifications} | ||||
|           listeners={notificationsListeners} | ||||
|           options={{ | ||||
|             tabBarBadge: localNotification?.latestTime | ||||
|               ? !localNotification.readTime || | ||||
|                 new Date(localNotification.readTime) < | ||||
|                   new Date(localNotification.latestTime) | ||||
|                 ? '' | ||||
|                 : undefined | ||||
|               : undefined, | ||||
|             tabBarBadgeStyle: { | ||||
|               transform: [{ scale: 0.5 }], | ||||
|               backgroundColor: theme.red | ||||
|             } | ||||
|           }} | ||||
|         /> | ||||
|         <Tab.Screen name='Tab-Me' component={TabMe} /> | ||||
|       </Tab.Navigator> | ||||
|     ) | ||||
|   }, | ||||
|   () => true | ||||
| ) | ||||
|  | ||||
| export default ScreenTabs | ||||
|   | ||||
| @@ -44,7 +44,7 @@ const Collections: React.FC = () => { | ||||
|         onPress={() => navigation.navigate('Tab-Me-Bookmarks')} | ||||
|       /> | ||||
|       <MenuRow | ||||
|         iconFront='Star' | ||||
|         iconFront='Heart' | ||||
|         iconBack='ChevronRight' | ||||
|         title={t('content.collections.favourites')} | ||||
|         onPress={() => navigation.navigate('Tab-Me-Favourites')} | ||||
|   | ||||
| @@ -10,7 +10,10 @@ export interface Props { | ||||
| } | ||||
|  | ||||
| const MyInfo: React.FC<Props> = ({ setData }) => { | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const localAccount = useSelector( | ||||
|     getLocalAccount, | ||||
|     (prev, next) => prev?.id === next?.id | ||||
|   ) | ||||
|   const { data } = useAccountQuery({ id: localAccount!.id }) | ||||
|  | ||||
|   useEffect(() => { | ||||
|   | ||||
| @@ -1,26 +1,32 @@ | ||||
| import { MenuContainer, MenuRow } from '@components/Menu' | ||||
| import { useNavigation } from '@react-navigation/native' | ||||
| import { getLocalUrl } from '@utils/slices/instancesSlice' | ||||
| import * as WebBrowser from 'expo-web-browser' | ||||
| import React from 'react' | ||||
| import { useTranslation } from 'react-i18next' | ||||
| import { useSelector } from 'react-redux' | ||||
|  | ||||
| const Settings: React.FC = () => { | ||||
|   const { t } = useTranslation('meRoot') | ||||
|   const navigation = useNavigation() | ||||
|  | ||||
|   const [loadingState, setLoadingState] = React.useState(false) | ||||
|   React.useEffect(() => { | ||||
|     const timer = setTimeout(() => { | ||||
|       setLoadingState(!loadingState) | ||||
|     }, 5000) | ||||
|     return () => clearTimeout(timer) | ||||
|   }, [loadingState]) | ||||
|   const localUrl = useSelector(getLocalUrl) | ||||
|  | ||||
|   return ( | ||||
|     <MenuContainer> | ||||
|       {/* <MenuRow | ||||
|         iconFront='User' | ||||
|         iconBack='ExternalLink' | ||||
|         title={t('content.accountSettings')} | ||||
|         onPress={() => | ||||
|           localUrl && | ||||
|           WebBrowser.openBrowserAsync(`https://${localUrl}/settings/profile`) | ||||
|         } | ||||
|       /> */} | ||||
|       <MenuRow | ||||
|         iconFront='Settings' | ||||
|         iconBack='ChevronRight' | ||||
|         title={t('content.settings')} | ||||
|         title={t('content.appSettings')} | ||||
|         onPress={() => navigation.navigate('Tab-Me-Settings')} | ||||
|       /> | ||||
|     </MenuContainer> | ||||
|   | ||||
| @@ -45,8 +45,8 @@ const AccountButton: React.FC<Props> = ({ instance, disabled = false }) => { | ||||
|       onPress={() => { | ||||
|         haptics('Light') | ||||
|         analytics('switch_existing_press') | ||||
|         queryClient.clear() | ||||
|         dispatch(updateLocalActiveIndex(instance)) | ||||
|         queryClient.clear() | ||||
|         navigation.goBack() | ||||
|       }} | ||||
|     /> | ||||
|   | ||||
| @@ -21,7 +21,9 @@ export interface Props { | ||||
| } | ||||
|  | ||||
| const AccountInformation: React.FC<Props> = ({ account, myInfo = false }) => { | ||||
|   const ownAccount = account?.id === useSelector(getLocalAccount)?.id | ||||
|   const ownAccount = | ||||
|     account?.id === | ||||
|     useSelector(getLocalAccount, (prev, next) => prev?.id === next?.id)?.id | ||||
|   const { mode, theme } = useTheme() | ||||
|  | ||||
|   const animation = useCallback( | ||||
|   | ||||
| @@ -14,7 +14,10 @@ export interface Props { | ||||
|  | ||||
| const AccountInformationAccount: React.FC<Props> = ({ account, myInfo }) => { | ||||
|   const { theme } = useTheme() | ||||
|   const localAccount = useSelector(getLocalAccount) | ||||
|   const localAccount = useSelector( | ||||
|     getLocalAccount, | ||||
|     (prev, next) => prev?.acct === next?.acct | ||||
|   ) | ||||
|   const localUri = useSelector(getLocalUri) | ||||
|  | ||||
|   const movedStyle = useMemo( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user