This commit is contained in:
Zhiyuan Zheng 2021-02-10 00:40:44 +01:00
parent c46888acab
commit a40a645337
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
31 changed files with 593 additions and 558 deletions

View File

@ -20,12 +20,10 @@ jobs:
expo-username: ${{ secrets.EXPO_USERNAME }}
expo-token: ${{ secrets.EXPO_TOKEN }}
- name: -- Step 4 -- Install node dependencies
run: yarn install
- name: -- Step 5 -- Install native dependencies
run: npx pod-install
- name: -- Step 6 -- Install ruby dependencies
- name: -- Step 5 -- Install ruby dependencies
run: bundle install
- name: -- Step 7 -- Run fastlane
- name: -- Step 6 -- Run fastlane
env:
TOOOT_ENVIRONMENT: development
SENTRY_ORGANIZATION: ${{ secrets.SENTRY_ORGANIZATION }}

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "src/modules/react-native-image-viewing"]
path = src/modules/react-native-image-viewing
url = https://github.com/xmflsct/react-native-image-viewing.git

View File

@ -54,19 +54,16 @@
"react-native-feather": "^1.0.2",
"react-native-gesture-handler": "~1.9.0",
"react-native-htmlview": "^0.16.0",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-reanimated": "^2.0.0-rc.2",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.17.1",
"react-native-shared-element": "^0.7.0",
"react-native-svg": "12.1.0",
"react-native-swipe-list-view": "^3.2.6",
"react-native-tab-view": "^2.15.2",
"react-native-tab-view-viewpager-adapter": "^1.1.0",
"react-native-toast-message": "^1.4.3",
"react-native-unimodules": "~0.12.0",
"react-navigation-shared-element": "^3.0.0",
"react-query": "^3.6.0",
"react-query": "^3.8.2",
"react-redux": "^7.2.2",
"react-timeago": "^5.2.0",
"reconnecting-websocket": "^4.4.0",

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(

View File

@ -8595,18 +8595,6 @@ react-native-htmlview@^0.16.0:
entities "^1.1.1"
htmlparser2-without-node-native "^3.9.2"
react-native-image-pan-zoom@^2.1.12:
version "2.1.12"
resolved "https://registry.yarnpkg.com/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz#eb98bf56fb5610379bdbfdb63219cc1baca98fd2"
integrity sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==
react-native-image-zoom-viewer@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-3.0.1.tgz#a2bd5fb3bda15e0686ce88fcde8576726495d7fb"
integrity sha512-la6s5DNSuq4GCRLsi5CZ29FPjgTpdCuGIRdO5T9rUrAtxrlpBPhhSnHrbmPVxsdtOUvxHacTh2Gfa9+RraMZQA==
dependencies:
react-native-image-pan-zoom "^2.1.12"
react-native-iphone-x-helper@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
@ -8638,11 +8626,6 @@ react-native-screens@~2.17.1:
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.17.1.tgz#c3c0ac750af48741c5b1635511e6af2a27b74309"
integrity sha512-B4gD5e4csvlVwlhf+RNqjQZ9mHTwe/iL3rXondgZxnKz4oW0QAmtLnLRKOrYVxoaJaF9Fy7jhjo//24/472APQ==
react-native-shared-element@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/react-native-shared-element/-/react-native-shared-element-0.7.0.tgz#c5e02eb372f6e38e48eb1030fd59be8f3d8c7a1f"
integrity sha512-TJTGwQceABYete+vH3ahNSgzVzXz7X2SPv3thT91gdcFCrm7ht7IKXBXJiYjOA+4TfdqnGEAWkspCy80oEnOgw==
react-native-svg@12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-12.1.0.tgz#acfe48c35cd5fca3d5fd767abae0560c36cfc03d"
@ -8732,13 +8715,6 @@ react-native@~0.63.4:
use-subscription "^1.0.0"
whatwg-fetch "^3.0.0"
react-navigation-shared-element@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-navigation-shared-element/-/react-navigation-shared-element-3.0.0.tgz#3dfa6a71f138e4ceb457a91aea3ef2a8ab2a6a32"
integrity sha512-nSmbgf4+hKSZU1Rzi5Bm1Is5mt0Z+xY5sXeTx4t5KMnmNW4lz5M83p9oTW6mKDoXUeCocUfeVwbCmsAjlyTLQw==
dependencies:
hoist-non-react-statics "^3.3.2"
react-navigation-stack@^2.10.2:
version "2.10.2"
resolved "https://registry.yarnpkg.com/react-navigation-stack/-/react-navigation-stack-2.10.2.tgz#9b6d38503496f9ef519acc6cad270ec0ace4ccbd"
@ -8755,10 +8731,10 @@ react-navigation@*, react-navigation@^4.4.3:
"@react-navigation/core" "^3.7.9"
"@react-navigation/native" "^3.8.3"
react-query@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.6.0.tgz#3da307a4a4cb6ea95f9c95b7e50b5281d5244e4d"
integrity sha512-39ptLt4qaKO1DE+ta6SpPutweEgDvUQj/KlebC+okJ9Nxbs5ExxKV8RYlLeop6vdDFyiMmwYrt1POiF8oWGh1A==
react-query@^3.8.2:
version "3.8.2"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.8.2.tgz#e2dac76b5d9b3465d854f4ca040a35ba4bcae1ae"
integrity sha512-Ha8+WZLIHOUkKhqE4+2RZZB03WvEWmNhN4WIIw3zWVCtR7blo2n2TXtu+d3YhaY1o6dt+sHT+J+zNV8IzR583w==
dependencies:
"@babel/runtime" "^7.5.5"
match-sorter "^6.0.2"