1
0
mirror of https://github.com/tooot-app/app synced 2025-06-05 22:19:13 +02:00
This commit is contained in:
Zhiyuan Zheng
2021-02-10 00:40:44 +01:00
parent c46888acab
commit a40a645337
31 changed files with 593 additions and 558 deletions

View File

@ -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>

View File

@ -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
}
}

View File

@ -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

View File

@ -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}
/>
)

View File

@ -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({

View File

@ -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}

View File

@ -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()

View File

@ -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>
>()

View File

@ -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>
>()

View File

@ -1,4 +1,4 @@
export default {
heading: 'Direct messages',
heading: 'Direct Messages',
content: {}
}

View File

@ -14,7 +14,8 @@ export default {
}
}
},
settings: '$t(meSettings:heading)',
accountSettings: 'Account Settings',
appSettings: '$t(meSettings:heading)',
logout: {
button: 'Log out',
alert: {

View File

@ -14,7 +14,8 @@ export default {
}
}
},
settings: '$t(meSettings:heading)',
accountSettings: '账户设置',
appSettings: '$t(meSettings:heading)',
logout: {
button: '退出当前账号',
alert: {

View File

@ -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

View 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

View File

@ -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}

View File

@ -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

View File

@ -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} />,
[]

View File

@ -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 (
<>

View File

@ -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 (

View File

@ -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

View File

@ -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

View File

@ -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')}

View File

@ -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(() => {

View File

@ -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>

View File

@ -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()
}}
/>

View File

@ -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(

View File

@ -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(