1. Added more notification types
2. Use `react-native-reanimated` v2
This commit is contained in:
Zhiyuan Zheng 2021-01-04 10:50:24 +01:00
parent dceaf8d25c
commit e9ea0ed71e
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
47 changed files with 956 additions and 700 deletions

View File

@ -6,6 +6,7 @@ import { resetLocal } from '@root/utils/slices/instancesSlice'
import ThemeManager from '@utils/styles/ThemeManager'
import chalk from 'chalk'
import * as Analytics from 'expo-firebase-analytics'
import { Audio } from 'expo-av'
import * as SplashScreen from 'expo-splash-screen'
import React, { useCallback, useEffect, useState } from 'react'
import { enableScreens } from 'react-native-screens'
@ -51,6 +52,12 @@ Sentry.init({
startingLog('log', 'initializing react-query')
const queryClient = new QueryClient()
startingLog('log', 'setting audio playback default options')
Audio.setAudioModeAsync({
playsInSilentModeIOS: true,
interruptionModeIOS: 1
})
startingLog('log', 'initializing native screen')
enableScreens()

View File

@ -57,5 +57,8 @@ export default (): ExpoConfig => ({
measurementId: 'G-3J0FS8WV5J'
}
}
},
experiments: {
turboModules: true
}
})

View File

@ -3,7 +3,7 @@ module.exports = function (api) {
return {
presets: ['babel-preset-expo'],
plugins: [
['@babel/plugin-proposal-optional-chaining'],
'@babel/plugin-proposal-optional-chaining',
[
'module-resolver',
{
@ -17,7 +17,8 @@ module.exports = function (api) {
'@utils': './src/utils'
}
}
]
],
'react-native-reanimated/plugin'
]
}
}

View File

@ -10,12 +10,12 @@
},
"dependencies": {
"@react-native-community/masked-view": "0.1.10",
"@react-native-community/netinfo": "^5.9.9",
"@react-native-community/netinfo": "^5.9.7",
"@react-native-community/segmented-control": "2.2.1",
"@react-native-community/slider": "3.0.3",
"@react-navigation/bottom-tabs": "^5.11.2",
"@react-navigation/native": "^5.8.10",
"@reduxjs/toolkit": "^1.5.0",
"@sharcoux/slider": "^5.0.1",
"axios": "^0.21.1",
"buffer": "^6.0.3",
"expo": "^40.0.0",
@ -53,7 +53,7 @@
"react-native-gesture-handler": "~1.8.0",
"react-native-htmlview": "^0.16.0",
"react-native-image-zoom-viewer": "^3.0.1",
"react-native-reanimated": "~1.13.0",
"react-native-reanimated": "2.0.0-rc.0",
"react-native-safe-area-context": "3.1.9",
"react-native-screens": "~2.15.0",
"react-native-shimmer-placeholder": "^2.0.6",
@ -116,4 +116,4 @@
]
},
"private": true
}
}

View File

@ -303,7 +303,14 @@ declare namespace Mastodon {
type Notification = {
// Base
id: string
type: 'follow' | 'mention' | 'reblog' | 'favourite' | 'poll'
type:
| 'follow'
| 'follow_request'
| 'mention'
| 'reblog'
| 'favourite'
| 'poll'
| 'status'
created_at: string
account: Account

View File

@ -1,14 +1,16 @@
import client from '@api/client'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { toast, toastConfig } from '@components/toast'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import {
NavigationContainer,
NavigationContainerRef
} from '@react-navigation/native'
import ScreenLocal from '@screens/Local'
import ScreenPublic from '@screens/Public'
import ScreenNotifications from '@screens/Notifications'
import ScreenMe from '@screens/Me'
import ScreenNotifications from '@screens/Notifications'
import ScreenPublic from '@screens/Public'
import { timelineFetch } from '@utils/fetches/timelineFetch'
import {
getLocalNotification,
@ -18,13 +20,12 @@ import {
} from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { themes } from '@utils/styles/themes'
import { toast, toastConfig } from '@components/toast'
import * as Analytics from 'expo-firebase-analytics'
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
import { StatusBar } from 'react-native'
import Toast from 'react-native-toast-message'
import { useDispatch, useSelector } from 'react-redux'
import { useInfiniteQuery } from 'react-query'
import { useDispatch, useSelector } from 'react-redux'
const Tab = createBottomTabNavigator<RootStackParamList>()
@ -214,12 +215,13 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
}),
[localInstance]
)
const tabScreenComposeListeners = useCallback(
({ navigation }) => ({
const tabScreenComposeListeners = useMemo(
() => ({
tabPress: (e: any) => {
e.preventDefault()
if (localInstance) {
navigation.navigate('Screen-Shared-Compose')
haptics('Medium')
navigationRef.current?.navigate('Screen-Shared-Compose')
}
}
}),

View File

@ -1,16 +1,19 @@
import React, { useEffect, useRef } from 'react'
import {
Animated,
Dimensions,
Modal,
PanResponder,
StyleSheet,
View
} from 'react-native'
import React from 'react'
import { Dimensions, Modal, StyleSheet, View } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTheme } from '@utils/styles/ThemeManager'
import { StyleConstants } from '@utils/styles/constants'
import Button from '@components/Button'
import { PanGestureHandler } from 'react-native-gesture-handler'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
export interface Props {
children: React.ReactNode
@ -22,76 +25,63 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
const { theme } = useTheme()
const insets = useSafeAreaInsets()
const panY = useRef(new Animated.Value(Dimensions.get('screen').height))
.current
const top = panY.interpolate({
inputRange: [-1, 0, 1],
outputRange: [0, 0, 1]
})
const resetModal = Animated.timing(panY, {
toValue: 0,
duration: 300,
useNativeDriver: false
})
const closeModal = Animated.timing(panY, {
toValue: Dimensions.get('screen').height,
duration: 350,
useNativeDriver: false
})
const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: Animated.event([null, { dy: panY }], {
useNativeDriver: false
}),
onPanResponderRelease: (e, gs) => {
if (gs.dy > 0 && gs.vy > 1) {
return closeModal.start(() => handleDismiss())
} else if (gs.dy === 0 && gs.vy === 0) {
return closeModal.start(() => handleDismiss())
}
return resetModal.start()
}
})
).current
useEffect(() => {
if (visible) {
resetModal.start()
const screenHeight = Dimensions.get('screen').height
const panY = useSharedValue(0)
const styleTop = useAnimatedStyle(() => {
return {
top: interpolate(
panY.value,
[0, screenHeight],
[0, screenHeight],
Extrapolate.CLAMP
)
}
}, [visible])
})
const callDismiss = () => {
handleDismiss()
}
const onGestureEvent = useAnimatedGestureHandler({
onActive: ({ translationY }) => {
panY.value = translationY
},
onEnd: ({ velocityY }) => {
if (velocityY > 500) {
runOnJS(callDismiss)()
} else {
panY.value = withTiming(0)
}
}
})
return (
<Modal animated animationType='fade' visible={visible} transparent>
<View
style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]}
{...panResponder.panHandlers}
>
<PanGestureHandler onGestureEvent={onGestureEvent}>
<Animated.View
style={[
styles.container,
{
top,
backgroundColor: theme.background,
paddingBottom: insets.bottom || StyleConstants.Spacing.L
}
]}
style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]}
>
<View
style={[styles.handle, { backgroundColor: theme.background }]}
/>
{children}
<Button
type='text'
content='取消'
onPress={() => closeModal.start(() => handleDismiss())}
style={styles.button}
/>
<Animated.View
style={[
styles.container,
styleTop,
{
backgroundColor: theme.background,
paddingBottom: insets.bottom || StyleConstants.Spacing.L
}
]}
>
<View
style={[styles.handle, { backgroundColor: theme.primaryOverlay }]}
/>
{children}
<Button
type='text'
content='取消'
onPress={() => handleDismiss()}
style={styles.button}
/>
</Animated.View>
</Animated.View>
</View>
</PanGestureHandler>
</Modal>
)
}

View File

@ -2,7 +2,7 @@ import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useEffect, useMemo } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import {
Pressable,
StyleProp,
@ -12,6 +12,7 @@ import {
ViewStyle
} from 'react-native'
import { Chase } from 'react-native-animated-spinkit'
import Animated from 'react-native-reanimated'
export interface Props {
style?: StyleProp<ViewStyle>
@ -48,7 +49,14 @@ const Button: React.FC<Props> = ({
}) => {
const { theme } = useTheme()
useEffect(() => layoutAnimation(), [content, loading, disabled])
const mounted = useRef(false)
useEffect(() => {
if (mounted.current) {
layoutAnimation()
} else {
mounted.current = true
}
}, [content, loading, disabled])
const loadingSpinkit = useMemo(
() => (
@ -139,24 +147,26 @@ const Button: React.FC<Props> = ({
}
return (
<Pressable
style={[
styles.button,
{
borderWidth: overlay ? 0 : 1,
borderColor: colorBorder,
backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal:
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
},
customStyle
]}
testID='base'
onPress={onPress}
children={children}
disabled={disabled || loading}
/>
<Animated.View>
<Pressable
style={[
styles.button,
{
borderWidth: overlay ? 0 : 1,
borderColor: colorBorder,
backgroundColor: colorBackground,
paddingVertical: StyleConstants.Spacing[spacing],
paddingHorizontal:
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
},
customStyle
]}
testID='base'
onPress={onPress}
children={children}
disabled={disabled || loading}
/>
</Animated.View>
)
}

View File

@ -6,6 +6,7 @@ export interface Props {
name: string
size: number
color: string
fill?: string
strokeWidth?: number
inline?: boolean // When used in line of text, need to drag it down
style?: StyleProp<ViewStyle>
@ -15,6 +16,7 @@ const Icon: React.FC<Props> = ({
name,
size,
color,
fill,
strokeWidth = 2,
inline = false,
style
@ -36,6 +38,7 @@ const Icon: React.FC<Props> = ({
width: size,
height: size,
color,
fill,
strokeWidth
})}
</View>

View File

@ -27,7 +27,8 @@ const ParseEmojis: React.FC<Props> = ({
},
image: {
width: StyleConstants.Font.Size[size],
height: StyleConstants.Font.Size[size]
height: StyleConstants.Font.Size[size],
marginBottom: -StyleConstants.Font.Size[size] * 0.125
}
})
@ -50,7 +51,7 @@ const ParseEmojis: React.FC<Props> = ({
{/* When emoji starts a paragraph, lineHeight will break */}
{i === 0 ? <Text> </Text> : null}
<Image
resizeMode='contain'
// resizeMode='contain'
source={{ uri: emojis[emojiIndex].url }}
style={[styles.image]}
/>

View File

@ -3,12 +3,18 @@ import openLink from '@components/openLink'
import ParseEmojis from '@components/Parse/Emojis'
import { useNavigation } from '@react-navigation/native'
import { StyleConstants } from '@utils/styles/constants'
import layoutAnimation from '@utils/styles/layoutAnimation'
import { useTheme } from '@utils/styles/ThemeManager'
import { LinearGradient } from 'expo-linear-gradient'
import React, { useCallback, useMemo, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import HTMLView from 'react-native-htmlview'
import Animated, {
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
withTiming
} from 'react-native-reanimated'
// Prevent going to the same hashtag multiple times
const renderNode = ({
@ -170,6 +176,27 @@ const ParseHTML: React.FC<Props> = ({
const [allowExpand, setAllowExpand] = useState(false)
const [showAllText, setShowAllText] = useState(false)
const viewHeight = useDerivedValue(() => {
if (allowExpand) {
if (showAllText) {
return heightOriginal as number
} else {
return heightTruncated as number
}
} else {
return heightOriginal as number
}
}, [heightOriginal, heightTruncated, allowExpand, showAllText])
const ViewHeight = useAnimatedStyle(() => {
return {
height: allowExpand
? showAllText
? withTiming(viewHeight.value)
: withTiming(viewHeight.value)
: undefined
}
})
const calNumberOfLines = useMemo(() => {
if (numberOfLines === 0) {
// For spoilers without calculation
@ -179,11 +206,7 @@ const ParseHTML: React.FC<Props> = ({
if (!heightTruncated) {
return numberOfLines
} else {
if (allowExpand && !showAllText) {
return numberOfLines
} else {
return undefined
}
return undefined
}
} else {
return undefined
@ -215,22 +238,21 @@ const ParseHTML: React.FC<Props> = ({
return (
<View>
<Text
style={{
...StyleConstants.FontStyle[size],
color: theme.primary,
overflow: 'hidden'
}}
children={children}
numberOfLines={calNumberOfLines}
onLayout={onLayout}
/>
<Animated.View style={[ViewHeight, { overflow: 'hidden' }]}>
<Text
style={{
...StyleConstants.FontStyle[size],
color: theme.primary,
height: allowExpand ? heightOriginal : undefined
}}
children={children}
numberOfLines={calNumberOfLines}
onLayout={onLayout}
/>
</Animated.View>
{allowExpand ? (
<Pressable
onPress={() => {
layoutAnimation()
setShowAllText(!showAllText)
}}
onPress={() => setShowAllText(!showAllText)}
style={{ marginTop: showAllText ? 0 : -lineHeight * 1.25 }}
>
<LinearGradient

View File

@ -0,0 +1,4 @@
import RelationshipIncoming from '@components/Relationship/Incoming'
import RelationshipOutgoing from '@components/Relationship/Outgoing'
export { RelationshipIncoming, RelationshipOutgoing }

View File

@ -0,0 +1,86 @@
import client from '@api/client'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { toast } from '@components/toast'
import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
export interface Props {
id: Mastodon.Account['id']
}
const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }]
const queryClient = useQueryClient()
const fireMutation = useCallback(
({ type }: { type: 'authorize' | 'reject' }) => {
return client({
method: 'post',
instance: 'local',
url: `follow_requests/${id}/${type}`
})
},
[]
)
const mutation = useMutation(fireMutation, {
onSuccess: ({ body }) => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, body)
queryClient.invalidateQueries(['Notifications', {}])
},
onError: (err: any, { type }) => {
haptics('Error')
toast({
type: 'error',
message: t('common:toastMessage.error.message', {
function: t(`relationship:${type}.function`)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
return (
<View style={styles.base}>
<Button
round
type='icon'
content='X'
loading={mutation.isLoading}
onPress={() => mutation.mutate({ type: 'reject' })}
/>
<Button
round
type='icon'
content='Check'
loading={mutation.isLoading}
onPress={() => mutation.mutate({ type: 'authorize' })}
style={styles.approve}
/>
</View>
)
}
const styles = StyleSheet.create({
base: {
flexShrink: 1,
flexDirection: 'row'
},
approve: {
marginLeft: StyleConstants.Spacing.M
}
})
export default RelationshipIncoming

View File

@ -0,0 +1,111 @@
import client from '@api/client'
import Button from '@components/Button'
import haptics from '@components/haptics'
import { toast } from '@components/toast'
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useMutation, useQuery, useQueryClient } from 'react-query'
export interface Props {
id: Mastodon.Account['id']
}
const RelationshipOutgoing: React.FC<Props> = ({ id }) => {
const { t } = useTranslation()
const relationshipQueryKey = ['Relationship', { id }]
const query = useQuery(relationshipQueryKey, relationshipFetch)
const queryClient = useQueryClient()
const fireMutation = useCallback(
({ type, state }: { type: 'follow' | 'block'; state: boolean }) => {
return client({
method: 'post',
instance: 'local',
url: `accounts/${id}/${state ? 'un' : ''}${type}`
})
},
[]
)
const mutation = useMutation(fireMutation, {
onSuccess: ({ body }) => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, body)
},
onError: (err: any, { type }) => {
haptics('Error')
toast({
type: 'error',
message: t('common:toastMessage.error.message', {
function: t(`relationship:${type}.function`)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
let content: string
let onPress: () => void
if (query.isError) {
content = t('relationship:button.error')
onPress = () => {}
} else {
if (query.data?.blocked_by) {
content = t('relationship:button.blocked_by')
onPress = () => null
} else {
if (query.data?.blocking) {
content = t('relationship:button.blocking')
onPress = () =>
mutation.mutate({
type: 'block',
state: query.data?.blocking
})
} else {
if (query.data?.following) {
content = t('relationship:button.following')
onPress = () =>
mutation.mutate({
type: 'follow',
state: query.data?.following
})
} else {
if (query.data?.requested) {
content = t('relationship:button.requested')
onPress = () =>
mutation.mutate({
type: 'follow',
state: query.data?.requested
})
} else {
content = t('relationship:button.default')
onPress = () =>
mutation.mutate({
type: 'follow',
state: false
})
}
}
}
}
}
return (
<Button
type='text'
content={content}
onPress={onPress}
loading={query.isLoading || mutation.isLoading}
disabled={query.isError || query.data?.blocked_by}
/>
)
}
export default RelationshipOutgoing

View File

@ -1,21 +1,20 @@
import React, { useCallback, useState } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import SegmentedControl from '@react-native-community/segmented-control'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { useSelector } from 'react-redux'
import { HeaderRight } from '@components/Header'
import Timeline from '@components/Timelines/Timeline'
import SegmentedControl from '@react-native-community/segmented-control'
import { useNavigation } from '@react-navigation/native'
import sharedScreens from '@screens/Shared/sharedScreens'
import { getLocalUrl, getRemoteUrl } from '@utils/slices/instancesSlice'
import { useTheme } from '@utils/styles/ThemeManager'
import { useNavigation } from '@react-navigation/native'
import { HeaderRight } from './Header'
import React, { useCallback, useState } from 'react'
import { Dimensions, StyleSheet, View } from 'react-native'
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
import { TabView } from 'react-native-tab-view'
import { useSelector } from 'react-redux'
const Stack = createNativeStackNavigator()
export interface Props {
name: 'Screen-Local-Root' | 'Screen-Public-Root'
name: 'Local' | 'Public'
content: { title: string; page: App.Pages }[]
}
@ -27,7 +26,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
const [segment, setSegment] = useState(0)
const onPressSearch = useCallback(() => {
navigation.navigate('Screen-Shared-Search')
navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' })
}, [])
const routes = content
@ -69,9 +68,9 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
return (
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
<Stack.Screen
name={name}
name={`Screen-${name}-Root`}
options={{
headerTitle: name === 'Screen-Public-Root' ? publicDomain : '',
headerTitle: name === 'Public' ? publicDomain : '',
...(localRegistered && {
headerCenter: () => (
<View style={styles.segmentsContainer}>

View File

@ -9,6 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
import client from '@root/api/client'
import { useMutation, useQueryClient } from 'react-query'
import { useTheme } from '@root/utils/styles/ThemeManager'
export interface Props {
conversation: Mastodon.Conversation
@ -35,6 +36,8 @@ const TimelineConversation: React.FC<Props> = ({
queryKey,
highlighted = false
}) => {
const { theme } = useTheme()
const queryClient = useQueryClient()
const { mutate } = useMutation(fireMutation, {
onSettled: () => {
@ -54,7 +57,19 @@ const TimelineConversation: React.FC<Props> = ({
}, [])
return (
<Pressable style={styles.conversationView} onPress={onPress}>
<Pressable
style={[
styles.base,
conversation.unread && {
borderLeftWidth: StyleConstants.Spacing.XS,
borderLeftColor: theme.blue,
paddingLeft:
StyleConstants.Spacing.Global.PagePadding -
StyleConstants.Spacing.XS
}
]}
onPress={onPress}
>
<View style={styles.header}>
<TimelineAvatar
queryKey={queryKey}
@ -100,7 +115,7 @@ const TimelineConversation: React.FC<Props> = ({
}
const styles = StyleSheet.create({
conversationView: {
base: {
flex: 1,
flexDirection: 'column',
padding: StyleConstants.Spacing.Global.PagePadding

View File

@ -58,11 +58,11 @@ const TimelineDefault: React.FC<Props> = ({
{...(!isRemotePublic && { queryKey })}
account={actualStatus.account}
/>
{/* <TimelineHeaderDefault
<TimelineHeaderDefault
{...(!isRemotePublic && { queryKey })}
status={actualStatus}
sameAccount={actualStatus.account.id === localAccountId}
/> */}
/>
</View>
<View

View File

@ -32,6 +32,7 @@ const TimelineNotifications: React.FC<Props> = ({
const onPress = useCallback(
() =>
notification.status &&
navigation.push('Screen-Shared-Toot', {
toot: notification.status
}),
@ -49,7 +50,10 @@ const TimelineNotifications: React.FC<Props> = ({
<View
style={{
opacity:
notification.type === 'follow' || notification.type === 'mention'
notification.type === 'follow' ||
notification.type === 'follow_request' ||
notification.type === 'mention' ||
notification.type === 'status'
? 1
: 0.5
}}

View File

@ -9,7 +9,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
export interface Props {
account: Mastodon.Account
action: 'favourite' | 'follow' | 'poll' | 'reblog' | 'pinned' | 'mention'
action: Mastodon.Notification['type'] | ('reblog' | 'pinned')
notification?: boolean
}
@ -77,6 +77,21 @@ const TimelineActioned: React.FC<Props> = ({
</>
)
break
case 'follow_request':
return (
<>
<Icon
name='UserPlus'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.follow_request', { name }))}
</Pressable>
</>
)
break
case 'poll':
return (
<>
@ -109,6 +124,21 @@ const TimelineActioned: React.FC<Props> = ({
</>
)
break
case 'status':
return (
<>
<Icon
name='Activity'
size={StyleConstants.Font.Size.S}
color={iconColor}
style={styles.icon}
/>
<Pressable onPress={onPress}>
{content(t('shared.actioned.status', { name }))}
</Pressable>
</>
)
break
}
}, [])

View File

@ -102,13 +102,20 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
return oldData
},
onError: (_, { type }, oldData) => {
onError: (err: any, { type }, oldData) => {
haptics('Error')
toast({
type: 'error',
message: t('common:toastMessage.success.message', {
message: t('common:toastMessage.error.message', {
function: t(`timeline:shared.actions.${type}.function`)
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
queryClient.setQueryData(queryKey, oldData)
}
@ -118,8 +125,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
() =>
navigation.navigate('Screen-Shared-Compose', {
type: 'reply',
incomingStatus: status,
visibilityLock: status.visibility === 'direct'
incomingStatus: status
}),
[]
)

View File

@ -1,12 +1,12 @@
import React, { useCallback, useState } from 'react'
import { Image, Pressable, StyleSheet, View } from 'react-native'
import { Audio } from 'expo-av'
import Button from '@components/Button'
import { Slider } from '@sharcoux/slider'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { Audio } from 'expo-av'
import { Surface } from 'gl-react-expo'
import { Blurhash } from 'gl-react-blurhash'
import Slider from '@react-native-community/slider'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import React, { useCallback, useState } from 'react'
import { Image, StyleSheet, View } from 'react-native'
export interface Props {
sensitiveShown: boolean
@ -21,10 +21,6 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
const [audioPosition, setAudioPosition] = useState(0)
const playAudio = useCallback(async () => {
if (!audioPlayer) {
await Audio.setAudioModeAsync({
playsInSilentModeIOS: true,
interruptionModeIOS: 1
})
const { sound } = await Audio.Sound.createAsync(
{ uri: audio.url },
{},
@ -44,7 +40,7 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
}, [audioPlayer])
return (
<View style={styles.base}>
<View style={[styles.base, { backgroundColor: theme.disabled }]}>
<View style={styles.overlay}>
{sensitiveShown ? (
audio.blurhash && (
@ -80,32 +76,33 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
</View>
<View
style={{
position: 'absolute',
bottom: 0,
alignSelf: 'flex-end',
width: '100%',
height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2,
backgroundColor: theme.backgroundOverlay,
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
paddingVertical: StyleConstants.Spacing.XS,
borderRadius: 6,
borderRadius: 100,
opacity: sensitiveShown ? 0.35 : undefined
}}
>
<Slider
style={{
width: '100%'
}}
minimumValue={0}
maximumValue={audio.meta.original.duration * 1000}
value={audioPosition}
minimumTrackTintColor={theme.secondary}
maximumTrackTintColor={theme.disabled}
onSlidingStart={() => {
audioPlayer?.pauseAsync()
setAudioPlaying(false)
}}
onSlidingComplete={value => {
setAudioPosition(value)
}}
// onSlidingStart={() => {
// console.log('yes!!!')
// audioPlayer?.pauseAsync()
// setAudioPlaying(false)
// }}
// onSlidingComplete={value => {
// console.log('no!!!')
// setAudioPosition(value)
// }}
enabled={false} // Bug in above sliding actions
thumbSize={StyleConstants.Spacing.M}
thumbTintColor={theme.primaryOverlay}
/>
</View>
</View>
@ -117,7 +114,8 @@ const styles = StyleSheet.create({
flex: 1,
flexBasis: '50%',
aspectRatio: 16 / 9,
padding: StyleConstants.Spacing.XS / 2
padding: StyleConstants.Spacing.XS / 2,
flexDirection: 'row'
},
background: { position: 'absolute', width: '100%', height: '100%' },
overlay: {

View File

@ -13,15 +13,18 @@ export interface Props {
const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
const videoPlayer = useRef<Video>(null)
const [videoLoading, setVideoLoading] = useState(false)
const [videoLoaded, setVideoLoaded] = useState(false)
const [videoPosition, setVideoPosition] = useState<number>(0)
const playOnPress = useCallback(async () => {
setVideoLoading(true)
if (!videoLoaded) {
await videoPlayer.current?.loadAsync({ uri: video.url })
}
await videoPlayer.current?.setPositionAsync(videoPosition)
await videoPlayer.current?.presentFullscreenPlayer()
videoPlayer.current?.playAsync()
setVideoLoading(false)
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
if (props.isLoaded) {
setVideoLoaded(true)
@ -46,7 +49,7 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
resizeMode='cover'
usePoster
posterSource={{ uri: video.preview_url }}
posterStyle={{ flex: 1 }}
posterStyle={{ resizeMode: 'cover' }}
useNativeControls={false}
/>
<Pressable style={styles.overlay}>
@ -63,12 +66,13 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
) : null
) : (
<Button
type='icon'
content='PlayCircle'
size='L'
round
overlay
size='L'
type='icon'
content='PlayCircle'
onPress={playOnPress}
loading={videoLoading}
/>
)}
</Pressable>

View File

@ -5,6 +5,7 @@ import { toast } from '@components/toast'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
import HeaderSharedAccount from './HeaderShared/Account'
@ -16,25 +17,15 @@ export interface Props {
}
const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
const { t } = useTranslation()
const queryClient = useQueryClient()
const fireMutation = useCallback(async () => {
const res = await client({
const fireMutation = useCallback(() => {
return client({
method: 'delete',
instance: 'local',
url: `conversations/${conversation.id}`
})
if (!res.body.error) {
toast({ type: 'success', message: '删除私信成功' })
return Promise.resolve()
} else {
toast({
type: 'error',
message: '删除私信失败,请重试',
autoHide: false
})
return Promise.reject()
}
}, [])
const { mutate } = useMutation(fireMutation, {
onMutate: () => {
@ -53,9 +44,22 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
return oldData
},
onError: (err, _, oldData) => {
onError: (err: any, _, oldData) => {
haptics('Error')
toast({ type: 'error', message: '请重试', autoHide: false })
toast({
type: 'error',
message: t('common:toastMessage.error.message', {
function: t(`timeline:shared.header.conversation.delete.function`)
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
}),
autoHide: false
})
queryClient.setQueryData(queryKey, oldData)
}
})
@ -85,9 +89,6 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
created_at={conversation.last_status?.created_at}
/>
) : null}
{conversation.unread && (
<Icon name='Circle' color={theme.blue} style={styles.unread} />
)}
</View>
</View>
@ -117,9 +118,6 @@ const styles = StyleSheet.create({
created_at: {
...StyleConstants.FontStyle.S
},
unread: {
marginLeft: StyleConstants.Spacing.XS
},
action: {
flex: 1,
flexDirection: 'row',

View File

@ -65,7 +65,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({
/>
)}
{queryKey && (
{queryKey && modalVisible && (
<BottomSheet
visible={modalVisible}
handleDismiss={() => setBottomSheetVisible(false)}

View File

@ -18,6 +18,7 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
setBottomSheetVisible
}) => {
const { t } = useTranslation()
const queryClient = useQueryClient()
const fireMutation = useCallback(
async ({ type }: { type: 'mute' | 'block' | 'reports' }) => {
@ -57,7 +58,7 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
})
})
},
onError: (_, { type }) => {
onError: (err: any, { type }) => {
haptics('Error')
toast({
type: 'error',
@ -66,7 +67,14 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
`timeline:shared.header.default.actions.account.${type}.function`,
{ acct: account.acct }
)
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
},
onSettled: () => {

View File

@ -105,14 +105,21 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
return oldData
},
onError: (_, { type }, oldData) => {
onError: (err: any, { type }, oldData) => {
toast({
type: 'error',
message: t('common:toastMessage.success.message', {
message: t('common:toastMessage.error.message', {
function: t(
`timeline:shared.header.default.actions.status.${type}.function`
)
})
}),
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
})
})
queryClient.setQueryData(queryKey, oldData)
}

View File

@ -1,110 +1,18 @@
import client from '@api/client'
import haptics from '@components/haptics'
import Icon from '@components/Icon'
import { toast } from '@components/toast'
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
import { RelationshipOutgoing } from '@components/Relationship'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Pressable, StyleSheet, View } from 'react-native'
import { Chase } from 'react-native-animated-spinkit'
import { useQuery } from 'react-query'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedVisibility from './HeaderShared/Visibility'
import RelationshipIncoming from '@root/components/Relationship/Incoming'
export interface Props {
notification: Mastodon.Notification
}
const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const { theme } = useTheme()
const { status, data, refetch } = useQuery(
['Relationship', { id: notification.account.id }],
relationshipFetch,
{
enabled: false
}
)
const [updateData, setUpdateData] = useState<
Mastodon.Relationship | undefined
>()
const relationshipOnPress = useCallback(() => {
client({
method: 'post',
instance: 'local',
url: `accounts/${notification.account.id}/${
updateData
? updateData.following || updateData.requested
? 'un'
: ''
: data!.following || data!.requested
? 'un'
: ''
}follow`
}).then(res => {
if (res.body.id === (updateData && updateData.id) || data!.id) {
setUpdateData(res.body)
haptics('Success')
return Promise.resolve()
} else {
haptics('Error')
toast({ type: 'error', message: '请重试', autoHide: false })
return Promise.reject()
}
})
}, [data, updateData])
useEffect(() => {
if (notification.type === 'follow') {
refetch()
}
}, [notification.type])
const relationshipIcon = useMemo(() => {
switch (status) {
case 'idle':
case 'loading':
return (
<Chase size={StyleConstants.Font.Size.L} color={theme.secondary} />
)
case 'success':
return (
<Pressable onPress={relationshipOnPress}>
<Icon
name={
updateData
? updateData.following
? 'UserCheck'
: updateData.requested
? 'Loader'
: 'UserPlus'
: data!.following
? 'UserCheck'
: data!.requested
? 'Loader'
: 'UserPlus'
}
color={
updateData
? updateData.following
? theme.primary
: theme.secondary
: data!.following
? theme.primary
: theme.secondary
}
size={StyleConstants.Font.Size.M + 2}
/>
</Pressable>
)
default:
return null
}
}, [status, data, updateData])
return (
<View style={styles.base}>
<View style={styles.accountAndMeta}>
@ -114,6 +22,8 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
? notification.status.account
: notification.account
}
{...((notification.type === 'follow' ||
notification.type === 'follow_request') && { withoutName: true })}
/>
<View style={styles.meta}>
<HeaderSharedCreated created_at={notification.created_at} />
@ -127,7 +37,14 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
</View>
{notification.type === 'follow' && (
<View style={styles.relationship}>{relationshipIcon}</View>
<View style={styles.relationship}>
<RelationshipOutgoing id={notification.account.id} />
</View>
)}
{notification.type === 'follow_request' && (
<View style={styles.relationship}>
<RelationshipIncoming id={notification.account.id} />
</View>
)}
</View>
)
@ -136,10 +53,13 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
const styles = StyleSheet.create({
base: {
flex: 1,
flexDirection: 'row'
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start'
},
accountAndMeta: {
flex: 4
flex: 1,
flexGrow: 1
},
meta: {
flexDirection: 'row',
@ -148,9 +68,8 @@ const styles = StyleSheet.create({
marginBottom: StyleConstants.Spacing.S
},
relationship: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
flexShrink: 1,
marginLeft: StyleConstants.Spacing.M
}
})

View File

@ -6,20 +6,26 @@ import { StyleSheet, Text, View } from 'react-native'
export interface Props {
account: Mastodon.Account
withoutName?: boolean
}
const HeaderSharedAccount: React.FC<Props> = ({ account }) => {
const HeaderSharedAccount: React.FC<Props> = ({
account,
withoutName = false
}) => {
const { theme } = useTheme()
return (
<View style={styles.base}>
<Text numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</Text>
{withoutName ? null : (
<Text style={styles.name} numberOfLines={1}>
<ParseEmojis
content={account.display_name || account.username}
emojis={account.emojis}
fontBold
/>
</Text>
)}
<Text style={[styles.acct, { color: theme.secondary }]} numberOfLines={1}>
@{account.acct}
</Text>
@ -32,9 +38,11 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center'
},
name: {
marginRight: StyleConstants.Spacing.XS
},
acct: {
flexShrink: 1,
marginLeft: StyleConstants.Spacing.XS
flexShrink: 1
}
})

View File

@ -5,6 +5,7 @@ import Icon from '@components/Icon'
import relativeTime from '@components/relativeTime'
import { TimelineData } from '@components/Timelines/Timeline'
import { ParseEmojis } from '@root/components/Parse'
import { toast } from '@root/components/toast'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
@ -13,35 +14,6 @@ import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query'
const fireMutation = async ({
id,
options
}: {
id: string
options?: boolean[]
}) => {
const formData = new FormData()
options &&
options.forEach((o, i) => {
if (options[i]) {
formData.append('choices[]', i.toString())
}
})
const res = await client({
method: options ? 'post' : 'get',
instance: 'local',
url: options ? `polls/${id}/votes` : `polls/${id}`,
...(options && { body: formData })
})
if (res.body.id === id) {
return Promise.resolve(res.body as Mastodon.Poll)
} else {
return Promise.reject()
}
}
export interface Props {
queryKey: QueryKey.Timeline
poll: NonNullable<Mastodon.Status['poll']>
@ -57,14 +29,33 @@ const TimelinePoll: React.FC<Props> = ({
}) => {
const { mode, theme } = useTheme()
const { t, i18n } = useTranslation('timeline')
const queryClient = useQueryClient()
const [allOptions, setAllOptions] = useState(
new Array(poll.options.length).fill(false)
)
const queryClient = useQueryClient()
const fireMutation = useCallback(
({ type }: { type: 'vote' | 'refresh' }) => {
const formData = new FormData()
type === 'vote' &&
allOptions.forEach((o, i) => {
if (allOptions[i]) {
formData.append('choices[]', i.toString())
}
})
return client({
method: type === 'vote' ? 'post' : 'get',
instance: 'local',
url: type === 'vote' ? `polls/${poll.id}/votes` : `polls/${poll.id}`,
...(type === 'vote' && { body: formData })
})
},
[allOptions]
)
const mutation = useMutation(fireMutation, {
onSuccess: (data, { id }) => {
onSuccess: ({ body }) => {
queryClient.cancelQueries(queryKey)
queryClient.setQueryData<TimelineData>(queryKey, old => {
@ -72,7 +63,7 @@ const TimelinePoll: React.FC<Props> = ({
const pageIndex = findIndex(old?.pages, page => {
const tempIndex = findIndex(page.toots, [
reblog ? 'reblog.poll.id' : 'poll.id',
id
poll.id
])
if (tempIndex >= 0) {
tootIndex = tempIndex
@ -84,9 +75,9 @@ const TimelinePoll: React.FC<Props> = ({
if (pageIndex >= 0 && tootIndex >= 0) {
if (reblog) {
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = data
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = body
} else {
old!.pages[pageIndex].toots[tootIndex].poll = data
old!.pages[pageIndex].toots[tootIndex].poll = body
}
}
return old
@ -94,8 +85,21 @@ const TimelinePoll: React.FC<Props> = ({
haptics('Success')
},
onError: () => {
onError: (err: any) => {
haptics('Error')
toast({
type: 'error',
message: '投票错误',
...(err.status &&
typeof err.status === 'number' &&
err.data &&
err.data.error &&
typeof err.data.error === 'string' && {
description: err.data.error
}),
autoHide: false
})
queryClient.invalidateQueries(queryKey)
}
})
@ -105,9 +109,7 @@ const TimelinePoll: React.FC<Props> = ({
return (
<View style={styles.button}>
<Button
onPress={() =>
mutation.mutate({ id: poll.id, options: allOptions })
}
onPress={() => mutation.mutate({ type: 'vote' })}
type='text'
content={t('shared.poll.meta.button.vote')}
loading={mutation.isLoading}
@ -119,7 +121,7 @@ const TimelinePoll: React.FC<Props> = ({
return (
<View style={styles.button}>
<Button
onPress={() => mutation.mutate({ id: poll.id })}
onPress={() => mutation.mutate({ type: 'refresh' })}
type='text'
content={t('shared.poll.meta.button.refresh')}
loading={mutation.isLoading}

View File

@ -5,6 +5,7 @@ import React from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import Toast from 'react-native-toast-message'
import * as Sentry from 'sentry-expo'
export interface Params {
type: 'success' | 'error' | 'warning'
@ -73,11 +74,17 @@ const ToastBase = ({ config }: { config: Config }) => {
color={theme[colorMapping[config.type]]}
/>
<View style={styles.texts}>
<Text style={[styles.text1, { color: theme.primary }]}>
<Text
style={[styles.text1, { color: theme.primary }]}
numberOfLines={2}
>
{config.text1}
</Text>
{config.text2 && (
<Text style={[styles.text2, { color: theme.secondary }]}>
<Text
style={[styles.text2, { color: theme.secondary }]}
numberOfLines={2}
>
{config.text2}
</Text>
)}
@ -89,8 +96,11 @@ const ToastBase = ({ config }: { config: Config }) => {
const toastConfig = {
success: (config: Config) => <ToastBase config={config} />,
error: (config: Config) => <ToastBase config={config} />,
warning: (config: Config) => <ToastBase config={config} />
warning: (config: Config) => <ToastBase config={config} />,
error: (config: Config) => {
Sentry.Native.captureException([config.text1, config.text2])
return <ToastBase config={config} />
}
}
const styles = StyleSheet.create({

View File

@ -19,5 +19,6 @@ export default {
sharedToot: require('./screens/sharedToot').default,
sharedAnnouncements: require('./screens/sharedAnnouncements').default,
relationship: require('./components/relationship').default,
timeline: require('./components/timeline').default
}

View File

@ -0,0 +1,16 @@
export default {
follow: {
function: '关注'
},
block: {
function: '屏蔽'
},
button: {
error: '读取错误',
blocked_by: '被用户屏蔽',
blocking: '取消屏蔽',
following: '取消关注',
requested: '取消关注请求',
default: '关注'
}
}

View File

@ -12,7 +12,9 @@ export default {
actioned: {
pinned: '置顶',
favourite: '{{name}} 喜欢了你的嘟嘟',
status: '{{name}} 刚刚发嘟',
follow: '{{name}} 开始关注你',
follow_request: '{{name}} 请求关注',
poll: '您参与的投票已结束',
reblog: {
default: '{{name}} 转嘟了',
@ -52,6 +54,11 @@ export default {
shared: {
application: '发自于 {{application}}'
},
conversation: {
delete: {
function: '删除私信'
}
},
default: {
actions: {
account: {

View File

@ -8,7 +8,7 @@ const ScreenLocal: React.FC = () => {
return (
<Timelines
name='Screen-Local-Root'
name='Local'
content={[
{ title: t('local:heading.segments.left'), page: 'Following' },
{ title: t('local:heading.segments.right'), page: 'Local' }

View File

@ -10,16 +10,18 @@ import accountInitialState from '@screens/Shared/Account/utils/initialState'
import AccountContext from '@screens/Shared/Account/utils/createContext'
import { getLocalUrl } from '@utils/slices/instancesSlice'
import React, { useReducer, useRef, useState } from 'react'
import { Animated, ScrollView } from 'react-native'
import { useSelector } from 'react-redux'
import Animated, {
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
const ScreenMeRoot: React.FC = () => {
const localRegistered = useSelector(getLocalUrl)
const scrollRef = useRef<ScrollView>(null)
const scrollRef = useRef<Animated.ScrollView>(null)
useScrollToTop(scrollRef)
const scrollY = useRef(new Animated.Value(0))
const [data, setData] = useState<Mastodon.Account>()
const [accountState, accountDispatch] = useReducer(
@ -27,26 +29,27 @@ const ScreenMeRoot: React.FC = () => {
accountInitialState
)
const scrollY = useSharedValue(0)
const onScroll = useAnimatedScrollHandler(event => {
scrollY.value = event.contentOffset.y
})
return (
<AccountContext.Provider value={{ accountState, accountDispatch }}>
{localRegistered && data ? (
<AccountNav scrollY={scrollY} account={data} />
) : null}
<ScrollView
<Animated.ScrollView
ref={scrollRef}
keyboardShouldPersistTaps='handled'
bounces={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY.current } } }],
{ useNativeDriver: false }
)}
scrollEventThrottle={8}
onScroll={onScroll}
scrollEventThrottle={16}
>
{localRegistered ? <MyInfo setData={setData} /> : <Login />}
{localRegistered && <Collections />}
<Settings />
{localRegistered && <Logout />}
</ScrollView>
</Animated.ScrollView>
</AccountContext.Provider>
)
}

View File

@ -13,9 +13,9 @@ import {
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import prettyBytes from 'pretty-bytes'
import React, { useEffect, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ActionSheetIOS, StyleSheet, Text } from 'react-native'
import { ActionSheetIOS, Button, StyleSheet, Text, View } from 'react-native'
import { CacheManager } from 'react-native-expo-image-cache'
import { useDispatch, useSelector } from 'react-redux'

View File

@ -8,7 +8,7 @@ const ScreenPublic: React.FC = () => {
return (
<Timelines
name='Screen-Public-Root'
name='Public'
content={[
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
{ title: t('public:heading.segments.right'), page: 'RemotePublic' }

View File

@ -4,7 +4,10 @@ import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/H
import { accountFetch } from '@utils/fetches/accountFetch'
import { getLocalAccountId } from '@utils/slices/instancesSlice'
import React, { useEffect, useReducer, useRef, useState } from 'react'
import { Animated, ScrollView } from 'react-native'
import Animated, {
useAnimatedScrollHandler,
useSharedValue
} from 'react-native-reanimated'
import { useQuery } from 'react-query'
import { useSelector } from 'react-redux'
import AccountHeader from './Account/Header'
@ -36,7 +39,7 @@ const ScreenSharedAccount: React.FC<Props> = ({
const localAccountId = useSelector(getLocalAccountId)
const { data } = useQuery(['Account', { id: account.id }], accountFetch)
const scrollY = useRef(new Animated.Value(0))
const scrollY = useSharedValue(0)
const [accountState, accountDispatch] = useReducer(
accountReducer,
accountInitialState
@ -56,6 +59,10 @@ const ScreenSharedAccount: React.FC<Props> = ({
return updateHeaderRight()
}, [])
const onScroll = useAnimatedScrollHandler(event => {
scrollY.value = event.contentOffset.y
})
return (
<AccountContext.Provider value={{ accountState, accountDispatch }}>
<AccountNav scrollY={scrollY} account={data} />
@ -63,18 +70,15 @@ const ScreenSharedAccount: React.FC<Props> = ({
accountState.informationLayout.y ? (
<AccountSegmentedControl scrollY={scrollY} />
) : null}
<ScrollView
<Animated.ScrollView
scrollEventThrottle={16}
showsVerticalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY.current } } }],
{ useNativeDriver: false }
)}
onScroll={onScroll}
>
<AccountHeader account={data} />
<AccountInformation account={data} />
<AccountToots id={account.id} />
</ScrollView>
</Animated.ScrollView>
<BottomSheet
visible={modalVisible}

View File

@ -1,6 +1,11 @@
import { useTheme } from '@root/utils/styles/ThemeManager'
import React, { useContext, useEffect, useState } from 'react'
import { Dimensions, Image, StyleSheet, View } from 'react-native'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useContext, useEffect } from 'react'
import { Dimensions, Image } from 'react-native'
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming
} from 'react-native-reanimated'
import AccountContext from './utils/createContext'
export interface Props {
@ -11,59 +16,40 @@ export interface Props {
const AccountHeader: React.FC<Props> = ({ account, limitHeight = false }) => {
const { accountState, accountDispatch } = useContext(AccountContext)
const { theme } = useTheme()
const [ratio, setRatio] = useState(accountState.headerRatio)
let isMounted = false
useEffect(() => {
isMounted = true
return () => {
isMounted = false
const height = useSharedValue(
Dimensions.get('screen').width * accountState.headerRatio
)
const styleHeight = useAnimatedStyle(() => {
return {
height: withTiming(height.value)
}
})
useEffect(() => {
if (
account?.header &&
!account.header.includes('/headers/original/missing.png')
) {
isMounted &&
Image.getSize(account.header, (width, height) => {
if (!limitHeight) {
accountDispatch &&
accountDispatch({
type: 'headerRatio',
payload: height / width
})
}
isMounted &&
setRatio(limitHeight ? accountState.headerRatio : height / width)
})
} else {
isMounted && setRatio(1 / 3)
Image.getSize(account.header, (width, height) => {
if (!limitHeight) {
accountDispatch({
type: 'headerRatio',
payload: height / width
})
}
})
}
}, [account, isMounted])
const windowWidth = Dimensions.get('window').width
}, [account])
return (
<View
style={{
height: windowWidth * ratio,
backgroundColor: theme.disabled
}}
>
<Image source={{ uri: account?.header }} style={styles.image} />
</View>
<Animated.Image
source={{ uri: account?.header }}
style={[styleHeight, { backgroundColor: theme.disabled }]}
/>
)
}
const styles = StyleSheet.create({
image: {
width: '100%',
height: '100%'
}
})
export default React.memo(
AccountHeader,
(_, next) => next.account === undefined

View File

@ -1,148 +1,38 @@
import Button from '@components/Button'
import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native'
import client from '@root/api/client'
import Button from '@root/components/Button'
import haptics from '@root/components/haptics'
import { toast } from '@root/components/toast'
import { relationshipFetch } from '@root/utils/fetches/relationshipFetch'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import React, { useEffect, useMemo } from 'react'
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
import { StyleConstants } from '@utils/styles/constants'
import React from 'react'
import { StyleSheet, View } from 'react-native'
import { useMutation, useQuery, useQueryClient } from 'react-query'
import { useQuery } from 'react-query'
export interface Props {
account: Mastodon.Account | undefined
}
const fireMutation = async ({
type,
id,
prevState
}: {
type: 'follow' | 'block'
id: string
prevState: boolean
}) => {
let res
switch (type) {
case 'follow':
case 'block':
res = await client({
method: 'post',
instance: 'local',
url: `accounts/${id}/${prevState ? 'un' : ''}${type}`
})
if (res.body.id === id) {
return Promise.resolve(res.body)
} else {
return Promise.reject()
}
}
}
const AccountInformationActions: React.FC<Props> = ({ account }) => {
const { theme } = useTheme()
const navigation = useNavigation()
const relationshipQueryKey = ['Relationship', { id: account?.id }]
const query = useQuery(relationshipQueryKey, relationshipFetch)
useEffect(() => {
if (account?.id) {
query.refetch()
}
}, [account])
const queryClient = useQueryClient()
const mutation = useMutation(fireMutation, {
onSuccess: data => {
haptics('Success')
queryClient.setQueryData(relationshipQueryKey, data)
},
onError: () => {
haptics('Error')
toast({ type: 'error', content: '关注失败,请重试' })
}
})
const mainAction = useMemo(() => {
let content: string
let onPress: () => void
if (query.isError) {
content = '读取错误'
onPress = () => {}
} else {
if (query.data?.blocked_by) {
content = '被用户屏蔽'
onPress = () => null
} else {
if (query.data?.blocking) {
content = '取消屏蔽'
onPress = () =>
mutation.mutate({
type: 'block',
id: account!.id,
prevState: query.data?.blocking
})
} else {
if (query.data?.following) {
content = '取消关注'
onPress = () =>
mutation.mutate({
type: 'follow',
id: account!.id,
prevState: query.data?.following
})
} else {
if (query.data?.requested) {
content = '取消关注请求'
onPress = () =>
mutation.mutate({
type: 'follow',
id: account!.id,
prevState: query.data?.requested
})
} else {
content = '关注'
onPress = () =>
mutation.mutate({
type: 'follow',
id: account!.id,
prevState: false
})
}
}
}
}
}
return (
<Button
type='text'
content={content}
onPress={onPress}
loading={query.isLoading || mutation.isLoading}
disabled={query.isError || query.data?.blocked_by}
/>
)
}, [theme, query, mutation])
return (
<View style={styles.actions}>
{query.data && !query.data.blocked_by ? (
<Button
round
type='icon'
content='Mail'
round
style={styles.actionConversation}
onPress={() =>
navigation.navigate('Screen-Shared-Compose', {
type: 'conversation',
incomingStatus: { account }
})
}
style={styles.actionConversation}
/>
) : null}
{mainAction}
{account && account.id && <RelationshipOutgoing id={account.id} />}
</View>
)
}
@ -152,10 +42,7 @@ const styles = StyleSheet.create({
alignSelf: 'flex-end',
flexDirection: 'row'
},
actionConversation: { marginRight: StyleConstants.Spacing.S },
error: {
...StyleConstants.FontStyle.S
}
actionConversation: { marginRight: StyleConstants.Spacing.S }
})
export default AccountInformationActions

View File

@ -1,13 +1,18 @@
import { ParseEmojis } from '@components/Parse'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { MutableRefObject, useContext } from 'react'
import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native'
import React, { useContext } from 'react'
import { Dimensions, StyleSheet, Text, View } from 'react-native'
import Animated, {
Extrapolate,
interpolate,
useAnimatedStyle
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import AccountContext from './utils/createContext'
export interface Props {
scrollY: MutableRefObject<Animated.Value>
scrollY: Animated.SharedValue<number>
account: Mastodon.Account | undefined
}
@ -23,19 +28,28 @@ const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
StyleConstants.Spacing.M -
headerHeight
const styleOpacity = useAnimatedStyle(() => {
return {
opacity: interpolate(scrollY.value, [0, 200], [0, 1], Extrapolate.CLAMP)
}
})
const styleMarginTop = useAnimatedStyle(() => {
return {
marginTop: interpolate(
scrollY.value,
[nameY, nameY + 20],
[50, 0],
Extrapolate.CLAMP
)
}
})
return (
<Animated.View
style={[
styles.base,
{
backgroundColor: theme.background,
opacity: scrollY.current.interpolate({
inputRange: [0, 200],
outputRange: [0, 1],
extrapolate: 'clamp'
}),
height: headerHeight
}
styleOpacity,
{ backgroundColor: theme.background, height: headerHeight }
]}
>
<View
@ -47,18 +61,7 @@ const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
}
]}
>
<Animated.View
style={[
styles.display_name,
{
marginTop: scrollY.current.interpolate({
inputRange: [nameY, nameY + 20],
outputRange: [50, 0],
extrapolate: 'clamp'
})
}
]}
>
<Animated.View style={[styles.display_name, styleMarginTop]}>
{account ? (
<Text numberOfLines={1}>
<ParseEmojis
@ -89,4 +92,5 @@ const styles = StyleSheet.create({
}
})
export default React.memo(AccountNav, (_, next) => next.account === undefined)
// export default React.memo(AccountNav, (_, next) => next.account === undefined)
export default AccountNav

View File

@ -1,14 +1,19 @@
import SegmentedControl from '@react-native-community/segmented-control'
import { StyleConstants } from '@root/utils/styles/constants'
import { useTheme } from '@root/utils/styles/ThemeManager'
import React, { MutableRefObject, useContext } from 'react'
import React, { useContext } from 'react'
import { useTranslation } from 'react-i18next'
import { Animated, StyleSheet } from 'react-native'
import { StyleSheet } from 'react-native'
import Animated, {
Extrapolate,
interpolate,
useAnimatedStyle
} from 'react-native-reanimated'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import AccountContext from './utils/createContext'
export interface Props {
scrollY: MutableRefObject<Animated.Value>
scrollY: Animated.SharedValue<number>
}
const AccountSegmentedControl: React.FC<Props> = ({ scrollY }) => {
@ -17,32 +22,40 @@ const AccountSegmentedControl: React.FC<Props> = ({ scrollY }) => {
const { mode, theme } = useTheme()
const headerHeight = useSafeAreaInsets().top + 44
const translateY = scrollY.current.interpolate({
inputRange: [
0,
(accountState.informationLayout?.y || 0) +
(accountState.informationLayout?.height || 0) -
headerHeight
],
outputRange: [
0,
-(accountState.informationLayout?.y || 0) -
(accountState.informationLayout?.height || 0) +
headerHeight
],
extrapolate: 'clamp',
easing: undefined
const styleTransform = useAnimatedStyle(() => {
return {
transform: [
{
translateY: interpolate(
scrollY.value,
[
0,
(accountState.informationLayout?.y || 0) +
(accountState.informationLayout?.height || 0) -
headerHeight
],
[
0,
-(accountState.informationLayout?.y || 0) -
(accountState.informationLayout?.height || 0) +
headerHeight
],
Extrapolate.CLAMP
)
}
]
}
})
return (
<Animated.View
style={[
styles.base,
styleTransform,
{
top:
(accountState.informationLayout?.y || 0) +
(accountState.informationLayout?.height || 0),
transform: [{ translateY }],
borderTopColor: theme.border,
backgroundColor: theme.background
}

View File

@ -1,15 +1,16 @@
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { MutableRefObject, useCallback, useContext, useRef } from 'react'
import {
Animated,
Dimensions,
Image,
StyleSheet,
Text,
View
} from 'react-native'
import React, { MutableRefObject, useContext } from 'react'
import { Dimensions, Image, StyleSheet, Text, View } from 'react-native'
import { PanGestureHandler } from 'react-native-gesture-handler'
import Animated, {
Extrapolate,
interpolate,
runOnJS,
useAnimatedGestureHandler,
useAnimatedStyle,
useSharedValue
} from 'react-native-reanimated'
import Svg, { Circle, G, Path } from 'react-native-svg'
import ComposeContext from '../utils/createContext'
@ -35,48 +36,61 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect!
}
const panFocus = useRef(
new Animated.ValueXY(
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.x &&
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.y
? {
x:
((theAttachmentRemote as Mastodon.AttachmentImage).meta!.focus!
.x *
imageDimensionis.width) /
2,
y:
(-(theAttachmentRemote as Mastodon.AttachmentImage).meta!.focus!
.y *
imageDimensionis.height) /
2
}
: { x: 0, y: 0 }
)
).current
const panX = panFocus.x.interpolate({
inputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
outputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
extrapolate: 'clamp'
})
const panY = panFocus.y.interpolate({
inputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
outputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
extrapolate: 'clamp'
})
panFocus.addListener(e => {
focus.current = {
x: e.x / (imageDimensionis.width / 2),
y: -e.y / (imageDimensionis.height / 2)
const panX = useSharedValue(
(((theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.x || 0) *
imageDimensionis.width) /
2
)
const panY = useSharedValue(
(((theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.y || 0) *
imageDimensionis.height) /
2
)
const updateFocus = ({ x, y }: { x: number; y: number }) => {
focus.current = { x, y }
}
type PanContext = {
startX: number
startY: number
}
const onGestureEvent = useAnimatedGestureHandler({
onStart: (_, context: PanContext) => {
context.startX = panX.value
context.startY = panY.value
},
onActive: ({ translationX, translationY }, context: PanContext) => {
panX.value = context.startX + translationX
panY.value = context.startY + translationY
},
onEnd: ({ translationX, translationY }, context: PanContext) => {
runOnJS(updateFocus)({
x: (context.startX + translationX) / (imageDimensionis.width / 2),
y: (context.startY + translationY) / (imageDimensionis.height / 2)
})
}
})
const styleTransform = useAnimatedStyle(() => {
return {
transform: [
{
translateX: interpolate(
panX.value,
[-imageDimensionis.width / 2, imageDimensionis.width / 2],
[-imageDimensionis.width / 2, imageDimensionis.width / 2],
Extrapolate.CLAMP
)
},
{
translateY: interpolate(
panY.value,
[-imageDimensionis.height / 2, imageDimensionis.height / 2],
[-imageDimensionis.height / 2, imageDimensionis.height / 2],
Extrapolate.CLAMP
)
}
]
}
})
const handleGesture = Animated.event(
[{ nativeEvent: { translationX: panFocus.x, translationY: panFocus.y } }],
{ useNativeDriver: true }
)
const onHandlerStateChange = useCallback(() => {
panFocus.extractOffset()
}, [])
return (
<>
@ -90,17 +104,14 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
uri: theAttachmentLocal.uri || theAttachmentRemote.preview_url
}}
/>
<PanGestureHandler
onGestureEvent={handleGesture}
onHandlerStateChange={onHandlerStateChange}
>
<PanGestureHandler onGestureEvent={onGestureEvent}>
<Animated.View
style={[
styleTransform,
{
position: 'absolute',
top: -1000 + imageDimensionis.height / 2,
left: -1000 + imageDimensionis.width / 2,
transform: [{ translateX: panX }, { translateY: panY }]
left: -1000 + imageDimensionis.width / 2
}
]}
>

View File

@ -1,17 +1,14 @@
import { getLocalAccountPreferences } from '@root/utils/slices/instancesSlice'
import { store } from '@root/store'
import { getLocalAccountPreferences } from '@utils/slices/instancesSlice'
import composeInitialState from './initialState'
import { ComposeState } from './types'
const composeParseState = ({
type,
incomingStatus,
visibilityLock
}: {
export interface Props {
type: 'reply' | 'conversation' | 'edit'
incomingStatus: Mastodon.Status
visibilityLock?: boolean
}): ComposeState => {
}
const composeParseState = ({ type, incomingStatus }: Props): ComposeState => {
switch (type) {
case 'edit':
return {
@ -52,10 +49,8 @@ const composeParseState = ({
const actualStatus = incomingStatus.reblog || incomingStatus
return {
...composeInitialState,
...(visibilityLock && {
visibility: 'direct',
visibilityLock: true
}),
visibility: actualStatus.visibility,
visibilityLock: actualStatus.visibility === 'direct',
replyToStatus: actualStatus
}
case 'conversation':

View File

@ -1,6 +1,6 @@
import haptics from '@root/components/haptics'
import { HeaderLeft, HeaderRight } from '@root/components/Header'
import { StyleConstants } from '@root/utils/styles/constants'
import haptics from '@components/haptics'
import { HeaderLeft, HeaderRight } from '@components/Header'
import { StyleConstants } from '@utils/styles/constants'
import { findIndex } from 'lodash'
import React, { useCallback, useState } from 'react'
import { ActionSheetIOS, Image, StyleSheet, Text } from 'react-native'
@ -22,13 +22,13 @@ export interface Props {
imageIndex: number
}
}
navigation: any
}
const TheImage = ({
style,
source,
imageUrls,
imageIndex
imageUrls
}: {
style: any
source: { uri: string }
@ -37,7 +37,6 @@ const TheImage = ({
remote_url: Mastodon.AttachmentImage['remote_url']
imageIndex: number
})[]
imageIndex: number
}) => {
const [imageVisible, setImageVisible] = useState(false)
Image.getSize(source.uri, () => setImageVisible(true))
@ -47,8 +46,7 @@ const TheImage = ({
source={{
uri: imageVisible
? source.uri
: imageUrls[findIndex(imageUrls, ['imageIndex', imageIndex])]
.preview_url
: imageUrls[findIndex(imageUrls, ['url', source.uri])].preview_url
}}
/>
)
@ -68,19 +66,18 @@ const ScreenSharedImagesViewer: React.FC<Props> = ({
const component = useCallback(
() => (
<ImageViewer
style={{ flex: 1, marginBottom: 44 + safeAreaInsets.bottom }}
imageUrls={imageUrls}
index={initialIndex}
onSwipeDown={() => navigation.goBack()}
imageUrls={imageUrls}
pageAnimateTime={250}
enableSwipeDown={true}
swipeDownThreshold={100}
useNativeDriver={true}
saveToLocalByLongPress={false}
swipeDownThreshold={100}
renderIndicator={() => <></>}
saveToLocalByLongPress={false}
onSwipeDown={() => navigation.goBack()}
style={{ flex: 1, marginBottom: 44 + safeAreaInsets.bottom }}
onChange={index => index !== undefined && setCurrentIndex(index)}
renderImage={props => (
<TheImage {...props} imageUrls={imageUrls} imageIndex={imageIndex} />
)}
renderImage={props => <TheImage {...props} imageUrls={imageUrls} />}
/>
),
[]

View File

@ -3,13 +3,13 @@ const Base = 4
export const StyleConstants = {
Font: {
Size: { S: 14, M: 16, L: 18 },
LineHeight: { S: 16, M: 20, L: 24 },
LineHeight: { S: 18, M: 22, L: 30 },
Weight: { Bold: '600' as '600' }
},
FontStyle: {
S: { fontSize: 14, lineHeight: 16 },
M: { fontSize: 16, lineHeight: 20 },
L: { fontSize: 20, lineHeight: 24 }
S: { fontSize: 14, lineHeight: 18 },
M: { fontSize: 16, lineHeight: 22 },
L: { fontSize: 20, lineHeight: 30 }
},
Spacing: {

111
yarn.lock
View File

@ -736,7 +736,7 @@
dependencies:
"@babel/helper-plugin-utils" "^7.10.4"
"@babel/plugin-transform-object-assign@^7.0.0":
"@babel/plugin-transform-object-assign@^7.0.0", "@babel/plugin-transform-object-assign@^7.10.4":
version "7.12.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.12.1.tgz#9102b06625f60a5443cc292d32b565373665e1e4"
integrity sha512-geUHn4XwHznRAFiuROTy0Hr7bKbpijJCmr1Svt/VNGhpxmp0OrdxURNpWbOAf94nUbL+xj6gbxRVPHWIbRpRoA==
@ -1145,6 +1145,25 @@
pouchdb-collections "^1.0.1"
tiny-queue "^0.2.1"
"@gorhom/bottom-sheet@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@gorhom/bottom-sheet/-/bottom-sheet-3.0.1.tgz#e1b1067c8e0666fc08e6c00732048f13116b478a"
integrity sha512-uvXaPJjqyyKMOhFaMtPkClo+48NoMG6yQus+/9c09teR4CdZndxg8lWvCVx2cN/Z5AZHKEHWoBPqmZe4QDYsDQ==
dependencies:
"@gorhom/portal" "^0.1.4"
invariant "^2.2.4"
lodash.isequal "^4.5.0"
nanoid "^3.1.20"
react-native-redash "^16.0.4"
"@gorhom/portal@^0.1.4":
version "0.1.4"
resolved "https://registry.yarnpkg.com/@gorhom/portal/-/portal-0.1.4.tgz#ef9e50d4b6c98ebe606a16d22d6cb58d661421b4"
integrity sha512-iU3D0i9NureT5ULTvD8moF2FWyFvVGcr/ahcRMDzBblUO1AwwixZRZ+Lf3d6uk0w4ujOQZ7+Az+uUnI+AsXXBw==
dependencies:
lodash.isequal "^4.5.0"
nanoid "^3.1.20"
"@hapi/address@2.x.x":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -1712,11 +1731,6 @@
resolved "https://registry.yarnpkg.com/@react-native-community/segmented-control/-/segmented-control-2.2.1.tgz#5ca418d78c5f6051353c9586918458713b88a83c"
integrity sha512-BzxFbI9Iqv+31yVqEvCTzJYmwb8jOMTf/UPuC4Hj176tmEPqBpuDaGH+rkAFg1miOco3/43RQxiAZO+mkY40Fg==
"@react-native-community/slider@3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-3.0.3.tgz#830167fd757ba70ac638747ba3169b2dbae60330"
integrity sha512-8IeHfDwJ9/CTUwFs6x90VlobV3BfuPgNLjTgC6dRZovfCWigaZwVNIFFJnHBakK3pW2xErAPwhdvNR4JeNoYbw==
"@react-navigation/bottom-tabs@^5.11.2":
version "5.11.2"
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.2.tgz#5b541612fcecdea2a5024a4028da35e4a727bde6"
@ -1969,6 +1983,11 @@
xcode "2.0.0"
yargs "^12.0.2"
"@sharcoux/slider@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@sharcoux/slider/-/slider-5.0.1.tgz#e99a9db88ea35eec57ea6b6dab686ab0d4e2d8e3"
integrity sha512-YwqUNeZt8eGbOdc57X6g12fHoIPI/bDE3UWnwRTRFvtw7FIl1je/7TxpuuxxIFc2pqxBi8u41ZjcA/VDKHrqBg==
"@sinonjs/commons@^1.7.0":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
@ -2281,6 +2300,11 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"
abs-svg-path@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/abs-svg-path/-/abs-svg-path-0.1.1.tgz#df601c8e8d2ba10d4a76d625e236a9a39c2723bf"
integrity sha1-32Acjo0roQ1KdtYl4japo5wnI78=
absolute-path@^0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7"
@ -3396,6 +3420,13 @@ cosmiconfig@^5.0.5, cosmiconfig@^5.1.0:
js-yaml "^3.13.1"
parse-json "^4.0.0"
cross-fetch@^3.0.4:
version "3.0.6"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c"
integrity sha512-KBPUbqgFjzWlVcURG+Svp9TlhA5uliYtiNx/0r8nv0pdypeQCRJ9IaSIc3q/x3q8t3F75cHuwxVql1HFGHCNJQ==
dependencies:
node-fetch "2.6.1"
cross-spawn@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@ -4439,6 +4470,19 @@ fbjs@^0.8.4:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
fbjs@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.0.tgz#0907067fb3f57a78f45d95f1eacffcacd623c165"
integrity sha512-dJd4PiDOFuhe7vk4F80Mba83Vr2QuK86FoxtgPmzBqEJahncp+13YCmfoa53KHCo6OnlXLG7eeMWPfB5CrpVKg==
dependencies:
cross-fetch "^3.0.4"
fbjs-css-vars "^1.0.0"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@ -7280,7 +7324,7 @@ nan@^2.12.1:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
nanoid@^3.1.15:
nanoid@^3.1.15, nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
@ -7337,6 +7381,11 @@ nocache@^2.1.0:
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
node-fetch@2.6.1, node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-fetch@^1.0.1:
version "1.7.3"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
@ -7345,11 +7394,6 @@ node-fetch@^1.0.1:
encoding "^0.1.11"
is-stream "^1.0.1"
node-fetch@^2.0.0-alpha.8, node-fetch@^2.2.0, node-fetch@^2.6.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@ -7420,6 +7464,13 @@ normalize-path@^3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
normalize-svg-path@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/normalize-svg-path/-/normalize-svg-path-1.1.0.tgz#0e614eca23c39f0cffe821d6be6cd17e569a766c"
integrity sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==
dependencies:
svg-arc-to-cubic-bezier "^3.0.0"
normalize-url@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
@ -7740,6 +7791,11 @@ parse-node-version@^1.0.0:
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
parse-svg-path@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/parse-svg-path/-/parse-svg-path-0.1.2.tgz#7a7ec0d1eb06fa5325c7d3e009b859a09b5d49eb"
integrity sha1-en7A0esG+lMlx9PgCbhZoJtdSes=
parse5@5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
@ -8159,12 +8215,23 @@ react-native-iphone-x-helper@^1.3.0:
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
react-native-reanimated@~1.13.0:
version "1.13.2"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-1.13.2.tgz#1ae5457b24b4913d173a5a064bb28eae7783d293"
integrity sha512-O+WhgxSjOIzcVdAAvx+h2DY331Ek1knKlaq+jsNLpC1fhRy9XTdOObovgob/aF2ve9uJfPEawCx8381g/tUJZQ==
react-native-reanimated@2.0.0-rc.0:
version "2.0.0-rc.0"
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.0.0-rc.0.tgz#7a1b0bfd48e3de9dfa985a524463b6a216531358"
integrity sha512-v+SMpeSxQ8kO116B5q3/D6VlFSot4eIRASw0nxxU+6zh9wb4W8shMyQi7/ag/gt246FvjBZOPwxsBS2iTcw8Zg==
dependencies:
fbjs "^1.0.0"
"@babel/plugin-transform-object-assign" "^7.10.4"
fbjs "^3.0.0"
string-hash-64 "^1.0.3"
react-native-redash@^16.0.4:
version "16.0.5"
resolved "https://registry.yarnpkg.com/react-native-redash/-/react-native-redash-16.0.5.tgz#4f34b2b25fd2c30cd6852c97eeddeaa1e2911efc"
integrity sha512-anRgwjMCqXBhOm2kwT0IuYTwpEj9xCPlcqtGo8BvoW2uBJnYklvZQXEb2oFeuZWIwn9or5qWKS4wwCss8nbTdA==
dependencies:
abs-svg-path "^0.1.1"
normalize-svg-path "^1.0.1"
parse-svg-path "^0.1.2"
react-native-safe-area-context@3.1.9:
version "3.1.9"
@ -9126,6 +9193,11 @@ strict-uri-encode@^2.0.0:
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
string-hash-64@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/string-hash-64/-/string-hash-64-1.0.3.tgz#0deb56df58678640db5c479ccbbb597aaa0de322"
integrity sha512-D5OKWKvDhyVWWn2x5Y9b+37NUllks34q1dCDhk/vYcso9fmhs+Tl3KR/gE4v5UNj2UA35cnX4KdVVGkG1deKqw==
string-length@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837"
@ -9287,6 +9359,11 @@ supports-hyperlinks@^2.0.0:
has-flag "^4.0.0"
supports-color "^7.0.0"
svg-arc-to-cubic-bezier@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz#390c450035ae1c4a0104d90650304c3bc814abe6"
integrity sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==
symbol-observable@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"