mirror of
https://github.com/tooot-app/app
synced 2025-02-18 04:40:57 +01:00
Updates
1. Added more notification types 2. Use `react-native-reanimated` v2
This commit is contained in:
parent
dceaf8d25c
commit
e9ea0ed71e
7
App.tsx
7
App.tsx
@ -6,6 +6,7 @@ import { resetLocal } from '@root/utils/slices/instancesSlice'
|
|||||||
import ThemeManager from '@utils/styles/ThemeManager'
|
import ThemeManager from '@utils/styles/ThemeManager'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as Analytics from 'expo-firebase-analytics'
|
import * as Analytics from 'expo-firebase-analytics'
|
||||||
|
import { Audio } from 'expo-av'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { enableScreens } from 'react-native-screens'
|
import { enableScreens } from 'react-native-screens'
|
||||||
@ -51,6 +52,12 @@ Sentry.init({
|
|||||||
startingLog('log', 'initializing react-query')
|
startingLog('log', 'initializing react-query')
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
startingLog('log', 'setting audio playback default options')
|
||||||
|
Audio.setAudioModeAsync({
|
||||||
|
playsInSilentModeIOS: true,
|
||||||
|
interruptionModeIOS: 1
|
||||||
|
})
|
||||||
|
|
||||||
startingLog('log', 'initializing native screen')
|
startingLog('log', 'initializing native screen')
|
||||||
enableScreens()
|
enableScreens()
|
||||||
|
|
||||||
|
@ -57,5 +57,8 @@ export default (): ExpoConfig => ({
|
|||||||
measurementId: 'G-3J0FS8WV5J'
|
measurementId: 'G-3J0FS8WV5J'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
experiments: {
|
||||||
|
turboModules: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -3,7 +3,7 @@ module.exports = function (api) {
|
|||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
plugins: [
|
plugins: [
|
||||||
['@babel/plugin-proposal-optional-chaining'],
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
[
|
[
|
||||||
'module-resolver',
|
'module-resolver',
|
||||||
{
|
{
|
||||||
@ -17,7 +17,8 @@ module.exports = function (api) {
|
|||||||
'@utils': './src/utils'
|
'@utils': './src/utils'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
'react-native-reanimated/plugin'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-native-community/masked-view": "0.1.10",
|
"@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/segmented-control": "2.2.1",
|
||||||
"@react-native-community/slider": "3.0.3",
|
|
||||||
"@react-navigation/bottom-tabs": "^5.11.2",
|
"@react-navigation/bottom-tabs": "^5.11.2",
|
||||||
"@react-navigation/native": "^5.8.10",
|
"@react-navigation/native": "^5.8.10",
|
||||||
"@reduxjs/toolkit": "^1.5.0",
|
"@reduxjs/toolkit": "^1.5.0",
|
||||||
|
"@sharcoux/slider": "^5.0.1",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"expo": "^40.0.0",
|
"expo": "^40.0.0",
|
||||||
@ -53,7 +53,7 @@
|
|||||||
"react-native-gesture-handler": "~1.8.0",
|
"react-native-gesture-handler": "~1.8.0",
|
||||||
"react-native-htmlview": "^0.16.0",
|
"react-native-htmlview": "^0.16.0",
|
||||||
"react-native-image-zoom-viewer": "^3.0.1",
|
"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-safe-area-context": "3.1.9",
|
||||||
"react-native-screens": "~2.15.0",
|
"react-native-screens": "~2.15.0",
|
||||||
"react-native-shimmer-placeholder": "^2.0.6",
|
"react-native-shimmer-placeholder": "^2.0.6",
|
||||||
@ -116,4 +116,4 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
9
src/@types/mastodon.d.ts
vendored
9
src/@types/mastodon.d.ts
vendored
@ -303,7 +303,14 @@ declare namespace Mastodon {
|
|||||||
type Notification = {
|
type Notification = {
|
||||||
// Base
|
// Base
|
||||||
id: string
|
id: string
|
||||||
type: 'follow' | 'mention' | 'reblog' | 'favourite' | 'poll'
|
type:
|
||||||
|
| 'follow'
|
||||||
|
| 'follow_request'
|
||||||
|
| 'mention'
|
||||||
|
| 'reblog'
|
||||||
|
| 'favourite'
|
||||||
|
| 'poll'
|
||||||
|
| 'status'
|
||||||
created_at: string
|
created_at: string
|
||||||
account: Account
|
account: Account
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import client from '@api/client'
|
import client from '@api/client'
|
||||||
|
import haptics from '@components/haptics'
|
||||||
import Icon from '@components/Icon'
|
import Icon from '@components/Icon'
|
||||||
|
import { toast, toastConfig } from '@components/toast'
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
|
||||||
import {
|
import {
|
||||||
NavigationContainer,
|
NavigationContainer,
|
||||||
NavigationContainerRef
|
NavigationContainerRef
|
||||||
} from '@react-navigation/native'
|
} from '@react-navigation/native'
|
||||||
import ScreenLocal from '@screens/Local'
|
import ScreenLocal from '@screens/Local'
|
||||||
import ScreenPublic from '@screens/Public'
|
|
||||||
import ScreenNotifications from '@screens/Notifications'
|
|
||||||
import ScreenMe from '@screens/Me'
|
import ScreenMe from '@screens/Me'
|
||||||
|
import ScreenNotifications from '@screens/Notifications'
|
||||||
|
import ScreenPublic from '@screens/Public'
|
||||||
import { timelineFetch } from '@utils/fetches/timelineFetch'
|
import { timelineFetch } from '@utils/fetches/timelineFetch'
|
||||||
import {
|
import {
|
||||||
getLocalNotification,
|
getLocalNotification,
|
||||||
@ -18,13 +20,12 @@ import {
|
|||||||
} from '@utils/slices/instancesSlice'
|
} from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { themes } from '@utils/styles/themes'
|
import { themes } from '@utils/styles/themes'
|
||||||
import { toast, toastConfig } from '@components/toast'
|
|
||||||
import * as Analytics from 'expo-firebase-analytics'
|
import * as Analytics from 'expo-firebase-analytics'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||||
import { StatusBar } from 'react-native'
|
import { StatusBar } from 'react-native'
|
||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
|
||||||
import { useInfiniteQuery } from 'react-query'
|
import { useInfiniteQuery } from 'react-query'
|
||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator<RootStackParamList>()
|
const Tab = createBottomTabNavigator<RootStackParamList>()
|
||||||
|
|
||||||
@ -214,12 +215,13 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
|
|||||||
}),
|
}),
|
||||||
[localInstance]
|
[localInstance]
|
||||||
)
|
)
|
||||||
const tabScreenComposeListeners = useCallback(
|
const tabScreenComposeListeners = useMemo(
|
||||||
({ navigation }) => ({
|
() => ({
|
||||||
tabPress: (e: any) => {
|
tabPress: (e: any) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (localInstance) {
|
if (localInstance) {
|
||||||
navigation.navigate('Screen-Shared-Compose')
|
haptics('Medium')
|
||||||
|
navigationRef.current?.navigate('Screen-Shared-Compose')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React from 'react'
|
||||||
import {
|
import { Dimensions, Modal, StyleSheet, View } from 'react-native'
|
||||||
Animated,
|
|
||||||
Dimensions,
|
|
||||||
Modal,
|
|
||||||
PanResponder,
|
|
||||||
StyleSheet,
|
|
||||||
View
|
|
||||||
} from 'react-native'
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import Button from '@components/Button'
|
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 {
|
export interface Props {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -22,76 +25,63 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
const panY = useRef(new Animated.Value(Dimensions.get('screen').height))
|
const screenHeight = Dimensions.get('screen').height
|
||||||
.current
|
const panY = useSharedValue(0)
|
||||||
const top = panY.interpolate({
|
const styleTop = useAnimatedStyle(() => {
|
||||||
inputRange: [-1, 0, 1],
|
return {
|
||||||
outputRange: [0, 0, 1]
|
top: interpolate(
|
||||||
})
|
panY.value,
|
||||||
const resetModal = Animated.timing(panY, {
|
[0, screenHeight],
|
||||||
toValue: 0,
|
[0, screenHeight],
|
||||||
duration: 300,
|
Extrapolate.CLAMP
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}, [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 (
|
return (
|
||||||
<Modal animated animationType='fade' visible={visible} transparent>
|
<Modal animated animationType='fade' visible={visible} transparent>
|
||||||
<View
|
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||||
style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]}
|
|
||||||
{...panResponder.panHandlers}
|
|
||||||
>
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]}
|
||||||
styles.container,
|
|
||||||
{
|
|
||||||
top,
|
|
||||||
backgroundColor: theme.background,
|
|
||||||
paddingBottom: insets.bottom || StyleConstants.Spacing.L
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<View
|
<Animated.View
|
||||||
style={[styles.handle, { backgroundColor: theme.background }]}
|
style={[
|
||||||
/>
|
styles.container,
|
||||||
{children}
|
styleTop,
|
||||||
<Button
|
{
|
||||||
type='text'
|
backgroundColor: theme.background,
|
||||||
content='取消'
|
paddingBottom: insets.bottom || StyleConstants.Spacing.L
|
||||||
onPress={() => closeModal.start(() => handleDismiss())}
|
}
|
||||||
style={styles.button}
|
]}
|
||||||
/>
|
>
|
||||||
|
<View
|
||||||
|
style={[styles.handle, { backgroundColor: theme.primaryOverlay }]}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
<Button
|
||||||
|
type='text'
|
||||||
|
content='取消'
|
||||||
|
onPress={() => handleDismiss()}
|
||||||
|
style={styles.button}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</PanGestureHandler>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import Icon from '@components/Icon'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
import layoutAnimation from '@utils/styles/layoutAnimation'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useEffect, useMemo } from 'react'
|
import React, { useEffect, useMemo, useRef } from 'react'
|
||||||
import {
|
import {
|
||||||
Pressable,
|
Pressable,
|
||||||
StyleProp,
|
StyleProp,
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
ViewStyle
|
ViewStyle
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { Chase } from 'react-native-animated-spinkit'
|
import { Chase } from 'react-native-animated-spinkit'
|
||||||
|
import Animated from 'react-native-reanimated'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
@ -48,7 +49,14 @@ const Button: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { theme } = useTheme()
|
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(
|
const loadingSpinkit = useMemo(
|
||||||
() => (
|
() => (
|
||||||
@ -139,24 +147,26 @@ const Button: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Animated.View>
|
||||||
style={[
|
<Pressable
|
||||||
styles.button,
|
style={[
|
||||||
{
|
styles.button,
|
||||||
borderWidth: overlay ? 0 : 1,
|
{
|
||||||
borderColor: colorBorder,
|
borderWidth: overlay ? 0 : 1,
|
||||||
backgroundColor: colorBackground,
|
borderColor: colorBorder,
|
||||||
paddingVertical: StyleConstants.Spacing[spacing],
|
backgroundColor: colorBackground,
|
||||||
paddingHorizontal:
|
paddingVertical: StyleConstants.Spacing[spacing],
|
||||||
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
|
paddingHorizontal:
|
||||||
},
|
StyleConstants.Spacing[round ? spacing : spacingMapping[spacing]]
|
||||||
customStyle
|
},
|
||||||
]}
|
customStyle
|
||||||
testID='base'
|
]}
|
||||||
onPress={onPress}
|
testID='base'
|
||||||
children={children}
|
onPress={onPress}
|
||||||
disabled={disabled || loading}
|
children={children}
|
||||||
/>
|
disabled={disabled || loading}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ export interface Props {
|
|||||||
name: string
|
name: string
|
||||||
size: number
|
size: number
|
||||||
color: string
|
color: string
|
||||||
|
fill?: string
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
inline?: boolean // When used in line of text, need to drag it down
|
inline?: boolean // When used in line of text, need to drag it down
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
@ -15,6 +16,7 @@ const Icon: React.FC<Props> = ({
|
|||||||
name,
|
name,
|
||||||
size,
|
size,
|
||||||
color,
|
color,
|
||||||
|
fill,
|
||||||
strokeWidth = 2,
|
strokeWidth = 2,
|
||||||
inline = false,
|
inline = false,
|
||||||
style
|
style
|
||||||
@ -36,6 +38,7 @@ const Icon: React.FC<Props> = ({
|
|||||||
width: size,
|
width: size,
|
||||||
height: size,
|
height: size,
|
||||||
color,
|
color,
|
||||||
|
fill,
|
||||||
strokeWidth
|
strokeWidth
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
@ -27,7 +27,8 @@ const ParseEmojis: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
width: StyleConstants.Font.Size[size],
|
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 */}
|
{/* When emoji starts a paragraph, lineHeight will break */}
|
||||||
{i === 0 ? <Text> </Text> : null}
|
{i === 0 ? <Text> </Text> : null}
|
||||||
<Image
|
<Image
|
||||||
resizeMode='contain'
|
// resizeMode='contain'
|
||||||
source={{ uri: emojis[emojiIndex].url }}
|
source={{ uri: emojis[emojiIndex].url }}
|
||||||
style={[styles.image]}
|
style={[styles.image]}
|
||||||
/>
|
/>
|
||||||
|
@ -3,12 +3,18 @@ import openLink from '@components/openLink'
|
|||||||
import ParseEmojis from '@components/Parse/Emojis'
|
import ParseEmojis from '@components/Parse/Emojis'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import layoutAnimation from '@utils/styles/layoutAnimation'
|
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { LinearGradient } from 'expo-linear-gradient'
|
import { LinearGradient } from 'expo-linear-gradient'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Pressable, Text, View } from 'react-native'
|
import { Pressable, Text, View } from 'react-native'
|
||||||
import HTMLView from 'react-native-htmlview'
|
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
|
// Prevent going to the same hashtag multiple times
|
||||||
const renderNode = ({
|
const renderNode = ({
|
||||||
@ -170,6 +176,27 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
const [allowExpand, setAllowExpand] = useState(false)
|
const [allowExpand, setAllowExpand] = useState(false)
|
||||||
const [showAllText, setShowAllText] = 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(() => {
|
const calNumberOfLines = useMemo(() => {
|
||||||
if (numberOfLines === 0) {
|
if (numberOfLines === 0) {
|
||||||
// For spoilers without calculation
|
// For spoilers without calculation
|
||||||
@ -179,11 +206,7 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
if (!heightTruncated) {
|
if (!heightTruncated) {
|
||||||
return numberOfLines
|
return numberOfLines
|
||||||
} else {
|
} else {
|
||||||
if (allowExpand && !showAllText) {
|
return undefined
|
||||||
return numberOfLines
|
|
||||||
} else {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined
|
||||||
@ -215,22 +238,21 @@ const ParseHTML: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<Text
|
<Animated.View style={[ViewHeight, { overflow: 'hidden' }]}>
|
||||||
style={{
|
<Text
|
||||||
...StyleConstants.FontStyle[size],
|
style={{
|
||||||
color: theme.primary,
|
...StyleConstants.FontStyle[size],
|
||||||
overflow: 'hidden'
|
color: theme.primary,
|
||||||
}}
|
height: allowExpand ? heightOriginal : undefined
|
||||||
children={children}
|
}}
|
||||||
numberOfLines={calNumberOfLines}
|
children={children}
|
||||||
onLayout={onLayout}
|
numberOfLines={calNumberOfLines}
|
||||||
/>
|
onLayout={onLayout}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
{allowExpand ? (
|
{allowExpand ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => setShowAllText(!showAllText)}
|
||||||
layoutAnimation()
|
|
||||||
setShowAllText(!showAllText)
|
|
||||||
}}
|
|
||||||
style={{ marginTop: showAllText ? 0 : -lineHeight * 1.25 }}
|
style={{ marginTop: showAllText ? 0 : -lineHeight * 1.25 }}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
|
4
src/components/Relationship.tsx
Normal file
4
src/components/Relationship.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import RelationshipIncoming from '@components/Relationship/Incoming'
|
||||||
|
import RelationshipOutgoing from '@components/Relationship/Outgoing'
|
||||||
|
|
||||||
|
export { RelationshipIncoming, RelationshipOutgoing }
|
86
src/components/Relationship/Incoming.tsx
Normal file
86
src/components/Relationship/Incoming.tsx
Normal 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
|
111
src/components/Relationship/Outgoing.tsx
Normal file
111
src/components/Relationship/Outgoing.tsx
Normal 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
|
@ -1,21 +1,20 @@
|
|||||||
import React, { useCallback, useState } from 'react'
|
import { HeaderRight } from '@components/Header'
|
||||||
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 Timeline from '@components/Timelines/Timeline'
|
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 sharedScreens from '@screens/Shared/sharedScreens'
|
||||||
import { getLocalUrl, getRemoteUrl } from '@utils/slices/instancesSlice'
|
import { getLocalUrl, getRemoteUrl } from '@utils/slices/instancesSlice'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { HeaderRight } from './Header'
|
import { Dimensions, StyleSheet, View } from 'react-native'
|
||||||
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import { TabView } from 'react-native-tab-view'
|
import { TabView } from 'react-native-tab-view'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
name: 'Screen-Local-Root' | 'Screen-Public-Root'
|
name: 'Local' | 'Public'
|
||||||
content: { title: string; page: App.Pages }[]
|
content: { title: string; page: App.Pages }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +26,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
const [segment, setSegment] = useState(0)
|
const [segment, setSegment] = useState(0)
|
||||||
|
|
||||||
const onPressSearch = useCallback(() => {
|
const onPressSearch = useCallback(() => {
|
||||||
navigation.navigate('Screen-Shared-Search')
|
navigation.navigate(`Screen-${name}`, { screen: 'Screen-Shared-Search' })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const routes = content
|
const routes = content
|
||||||
@ -69,9 +68,9 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
return (
|
return (
|
||||||
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
<Stack.Navigator screenOptions={{ headerHideShadow: true }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name={name}
|
name={`Screen-${name}-Root`}
|
||||||
options={{
|
options={{
|
||||||
headerTitle: name === 'Screen-Public-Root' ? publicDomain : '',
|
headerTitle: name === 'Public' ? publicDomain : '',
|
||||||
...(localRegistered && {
|
...(localRegistered && {
|
||||||
headerCenter: () => (
|
headerCenter: () => (
|
||||||
<View style={styles.segmentsContainer}>
|
<View style={styles.segmentsContainer}>
|
||||||
|
@ -9,6 +9,7 @@ import { StyleConstants } from '@utils/styles/constants'
|
|||||||
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
|
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
|
||||||
import client from '@root/api/client'
|
import client from '@root/api/client'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
|
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
conversation: Mastodon.Conversation
|
conversation: Mastodon.Conversation
|
||||||
@ -35,6 +36,8 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
queryKey,
|
queryKey,
|
||||||
highlighted = false
|
highlighted = false
|
||||||
}) => {
|
}) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { mutate } = useMutation(fireMutation, {
|
const { mutate } = useMutation(fireMutation, {
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
@ -54,7 +57,19 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
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}>
|
<View style={styles.header}>
|
||||||
<TimelineAvatar
|
<TimelineAvatar
|
||||||
queryKey={queryKey}
|
queryKey={queryKey}
|
||||||
@ -100,7 +115,7 @@ const TimelineConversation: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
conversationView: {
|
base: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
padding: StyleConstants.Spacing.Global.PagePadding
|
padding: StyleConstants.Spacing.Global.PagePadding
|
||||||
|
@ -58,11 +58,11 @@ const TimelineDefault: React.FC<Props> = ({
|
|||||||
{...(!isRemotePublic && { queryKey })}
|
{...(!isRemotePublic && { queryKey })}
|
||||||
account={actualStatus.account}
|
account={actualStatus.account}
|
||||||
/>
|
/>
|
||||||
{/* <TimelineHeaderDefault
|
<TimelineHeaderDefault
|
||||||
{...(!isRemotePublic && { queryKey })}
|
{...(!isRemotePublic && { queryKey })}
|
||||||
status={actualStatus}
|
status={actualStatus}
|
||||||
sameAccount={actualStatus.account.id === localAccountId}
|
sameAccount={actualStatus.account.id === localAccountId}
|
||||||
/> */}
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -32,6 +32,7 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
|
|
||||||
const onPress = useCallback(
|
const onPress = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
notification.status &&
|
||||||
navigation.push('Screen-Shared-Toot', {
|
navigation.push('Screen-Shared-Toot', {
|
||||||
toot: notification.status
|
toot: notification.status
|
||||||
}),
|
}),
|
||||||
@ -49,7 +50,10 @@ const TimelineNotifications: React.FC<Props> = ({
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
opacity:
|
opacity:
|
||||||
notification.type === 'follow' || notification.type === 'mention'
|
notification.type === 'follow' ||
|
||||||
|
notification.type === 'follow_request' ||
|
||||||
|
notification.type === 'mention' ||
|
||||||
|
notification.type === 'status'
|
||||||
? 1
|
? 1
|
||||||
: 0.5
|
: 0.5
|
||||||
}}
|
}}
|
||||||
|
@ -9,7 +9,7 @@ import { Pressable, StyleSheet, View } from 'react-native'
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account
|
account: Mastodon.Account
|
||||||
action: 'favourite' | 'follow' | 'poll' | 'reblog' | 'pinned' | 'mention'
|
action: Mastodon.Notification['type'] | ('reblog' | 'pinned')
|
||||||
notification?: boolean
|
notification?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +77,21 @@ const TimelineActioned: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
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':
|
case 'poll':
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -109,6 +124,21 @@ const TimelineActioned: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
break
|
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
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
@ -102,13 +102,20 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
|
|
||||||
return oldData
|
return oldData
|
||||||
},
|
},
|
||||||
onError: (_, { type }, oldData) => {
|
onError: (err: any, { type }, oldData) => {
|
||||||
haptics('Error')
|
haptics('Error')
|
||||||
toast({
|
toast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:toastMessage.success.message', {
|
message: t('common:toastMessage.error.message', {
|
||||||
function: t(`timeline:shared.actions.${type}.function`)
|
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)
|
queryClient.setQueryData(queryKey, oldData)
|
||||||
}
|
}
|
||||||
@ -118,8 +125,7 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
|||||||
() =>
|
() =>
|
||||||
navigation.navigate('Screen-Shared-Compose', {
|
navigation.navigate('Screen-Shared-Compose', {
|
||||||
type: 'reply',
|
type: 'reply',
|
||||||
incomingStatus: status,
|
incomingStatus: status
|
||||||
visibilityLock: status.visibility === 'direct'
|
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
@ -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 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 { Surface } from 'gl-react-expo'
|
||||||
import { Blurhash } from 'gl-react-blurhash'
|
import { Blurhash } from 'gl-react-blurhash'
|
||||||
import Slider from '@react-native-community/slider'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { StyleConstants } from '@root/utils/styles/constants'
|
import { Image, StyleSheet, View } from 'react-native'
|
||||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
sensitiveShown: boolean
|
sensitiveShown: boolean
|
||||||
@ -21,10 +21,6 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
|||||||
const [audioPosition, setAudioPosition] = useState(0)
|
const [audioPosition, setAudioPosition] = useState(0)
|
||||||
const playAudio = useCallback(async () => {
|
const playAudio = useCallback(async () => {
|
||||||
if (!audioPlayer) {
|
if (!audioPlayer) {
|
||||||
await Audio.setAudioModeAsync({
|
|
||||||
playsInSilentModeIOS: true,
|
|
||||||
interruptionModeIOS: 1
|
|
||||||
})
|
|
||||||
const { sound } = await Audio.Sound.createAsync(
|
const { sound } = await Audio.Sound.createAsync(
|
||||||
{ uri: audio.url },
|
{ uri: audio.url },
|
||||||
{},
|
{},
|
||||||
@ -44,7 +40,7 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
|||||||
}, [audioPlayer])
|
}, [audioPlayer])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={[styles.base, { backgroundColor: theme.disabled }]}>
|
||||||
<View style={styles.overlay}>
|
<View style={styles.overlay}>
|
||||||
{sensitiveShown ? (
|
{sensitiveShown ? (
|
||||||
audio.blurhash && (
|
audio.blurhash && (
|
||||||
@ -80,32 +76,33 @@ const AttachmentAudio: React.FC<Props> = ({ sensitiveShown, audio }) => {
|
|||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
alignSelf: 'flex-end',
|
||||||
bottom: 0,
|
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
height: StyleConstants.Spacing.M + StyleConstants.Spacing.S * 2,
|
||||||
backgroundColor: theme.backgroundOverlay,
|
backgroundColor: theme.backgroundOverlay,
|
||||||
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
paddingHorizontal: StyleConstants.Spacing.Global.PagePadding,
|
||||||
paddingVertical: StyleConstants.Spacing.XS,
|
borderRadius: 100,
|
||||||
borderRadius: 6,
|
|
||||||
opacity: sensitiveShown ? 0.35 : undefined
|
opacity: sensitiveShown ? 0.35 : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Slider
|
<Slider
|
||||||
style={{
|
|
||||||
width: '100%'
|
|
||||||
}}
|
|
||||||
minimumValue={0}
|
minimumValue={0}
|
||||||
maximumValue={audio.meta.original.duration * 1000}
|
maximumValue={audio.meta.original.duration * 1000}
|
||||||
value={audioPosition}
|
value={audioPosition}
|
||||||
minimumTrackTintColor={theme.secondary}
|
minimumTrackTintColor={theme.secondary}
|
||||||
maximumTrackTintColor={theme.disabled}
|
maximumTrackTintColor={theme.disabled}
|
||||||
onSlidingStart={() => {
|
// onSlidingStart={() => {
|
||||||
audioPlayer?.pauseAsync()
|
// console.log('yes!!!')
|
||||||
setAudioPlaying(false)
|
// audioPlayer?.pauseAsync()
|
||||||
}}
|
// setAudioPlaying(false)
|
||||||
onSlidingComplete={value => {
|
// }}
|
||||||
setAudioPosition(value)
|
// onSlidingComplete={value => {
|
||||||
}}
|
// console.log('no!!!')
|
||||||
|
// setAudioPosition(value)
|
||||||
|
// }}
|
||||||
|
enabled={false} // Bug in above sliding actions
|
||||||
|
thumbSize={StyleConstants.Spacing.M}
|
||||||
|
thumbTintColor={theme.primaryOverlay}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -117,7 +114,8 @@ const styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
flexBasis: '50%',
|
flexBasis: '50%',
|
||||||
aspectRatio: 16 / 9,
|
aspectRatio: 16 / 9,
|
||||||
padding: StyleConstants.Spacing.XS / 2
|
padding: StyleConstants.Spacing.XS / 2,
|
||||||
|
flexDirection: 'row'
|
||||||
},
|
},
|
||||||
background: { position: 'absolute', width: '100%', height: '100%' },
|
background: { position: 'absolute', width: '100%', height: '100%' },
|
||||||
overlay: {
|
overlay: {
|
||||||
|
@ -13,15 +13,18 @@ export interface Props {
|
|||||||
|
|
||||||
const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
||||||
const videoPlayer = useRef<Video>(null)
|
const videoPlayer = useRef<Video>(null)
|
||||||
|
const [videoLoading, setVideoLoading] = useState(false)
|
||||||
const [videoLoaded, setVideoLoaded] = useState(false)
|
const [videoLoaded, setVideoLoaded] = useState(false)
|
||||||
const [videoPosition, setVideoPosition] = useState<number>(0)
|
const [videoPosition, setVideoPosition] = useState<number>(0)
|
||||||
const playOnPress = useCallback(async () => {
|
const playOnPress = useCallback(async () => {
|
||||||
|
setVideoLoading(true)
|
||||||
if (!videoLoaded) {
|
if (!videoLoaded) {
|
||||||
await videoPlayer.current?.loadAsync({ uri: video.url })
|
await videoPlayer.current?.loadAsync({ uri: video.url })
|
||||||
}
|
}
|
||||||
await videoPlayer.current?.setPositionAsync(videoPosition)
|
await videoPlayer.current?.setPositionAsync(videoPosition)
|
||||||
await videoPlayer.current?.presentFullscreenPlayer()
|
await videoPlayer.current?.presentFullscreenPlayer()
|
||||||
videoPlayer.current?.playAsync()
|
videoPlayer.current?.playAsync()
|
||||||
|
setVideoLoading(false)
|
||||||
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
|
videoPlayer.current?.setOnPlaybackStatusUpdate(props => {
|
||||||
if (props.isLoaded) {
|
if (props.isLoaded) {
|
||||||
setVideoLoaded(true)
|
setVideoLoaded(true)
|
||||||
@ -46,7 +49,7 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
|||||||
resizeMode='cover'
|
resizeMode='cover'
|
||||||
usePoster
|
usePoster
|
||||||
posterSource={{ uri: video.preview_url }}
|
posterSource={{ uri: video.preview_url }}
|
||||||
posterStyle={{ flex: 1 }}
|
posterStyle={{ resizeMode: 'cover' }}
|
||||||
useNativeControls={false}
|
useNativeControls={false}
|
||||||
/>
|
/>
|
||||||
<Pressable style={styles.overlay}>
|
<Pressable style={styles.overlay}>
|
||||||
@ -63,12 +66,13 @@ const AttachmentVideo: React.FC<Props> = ({ sensitiveShown, video }) => {
|
|||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
type='icon'
|
|
||||||
content='PlayCircle'
|
|
||||||
size='L'
|
|
||||||
round
|
round
|
||||||
overlay
|
overlay
|
||||||
|
size='L'
|
||||||
|
type='icon'
|
||||||
|
content='PlayCircle'
|
||||||
onPress={playOnPress}
|
onPress={playOnPress}
|
||||||
|
loading={videoLoading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
@ -5,6 +5,7 @@ import { toast } from '@components/toast'
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useCallback, useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
import { Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
import { useMutation, useQueryClient } from 'react-query'
|
||||||
import HeaderSharedAccount from './HeaderShared/Account'
|
import HeaderSharedAccount from './HeaderShared/Account'
|
||||||
@ -16,25 +17,15 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(async () => {
|
const fireMutation = useCallback(() => {
|
||||||
const res = await client({
|
return client({
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
url: `conversations/${conversation.id}`
|
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, {
|
const { mutate } = useMutation(fireMutation, {
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
@ -53,9 +44,22 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
|||||||
|
|
||||||
return oldData
|
return oldData
|
||||||
},
|
},
|
||||||
onError: (err, _, oldData) => {
|
onError: (err: any, _, oldData) => {
|
||||||
haptics('Error')
|
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)
|
queryClient.setQueryData(queryKey, oldData)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -85,9 +89,6 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
|
|||||||
created_at={conversation.last_status?.created_at}
|
created_at={conversation.last_status?.created_at}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{conversation.unread && (
|
|
||||||
<Icon name='Circle' color={theme.blue} style={styles.unread} />
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -117,9 +118,6 @@ const styles = StyleSheet.create({
|
|||||||
created_at: {
|
created_at: {
|
||||||
...StyleConstants.FontStyle.S
|
...StyleConstants.FontStyle.S
|
||||||
},
|
},
|
||||||
unread: {
|
|
||||||
marginLeft: StyleConstants.Spacing.XS
|
|
||||||
},
|
|
||||||
action: {
|
action: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
@ -65,7 +65,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{queryKey && (
|
{queryKey && modalVisible && (
|
||||||
<BottomSheet
|
<BottomSheet
|
||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
handleDismiss={() => setBottomSheetVisible(false)}
|
handleDismiss={() => setBottomSheetVisible(false)}
|
||||||
|
@ -18,6 +18,7 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
|||||||
setBottomSheetVisible
|
setBottomSheetVisible
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const fireMutation = useCallback(
|
const fireMutation = useCallback(
|
||||||
async ({ type }: { type: 'mute' | 'block' | 'reports' }) => {
|
async ({ type }: { type: 'mute' | 'block' | 'reports' }) => {
|
||||||
@ -57,7 +58,7 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError: (_, { type }) => {
|
onError: (err: any, { type }) => {
|
||||||
haptics('Error')
|
haptics('Error')
|
||||||
toast({
|
toast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@ -66,7 +67,14 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
|
|||||||
`timeline:shared.header.default.actions.account.${type}.function`,
|
`timeline:shared.header.default.actions.account.${type}.function`,
|
||||||
{ acct: account.acct }
|
{ acct: account.acct }
|
||||||
)
|
)
|
||||||
})
|
}),
|
||||||
|
...(err.status &&
|
||||||
|
typeof err.status === 'number' &&
|
||||||
|
err.data &&
|
||||||
|
err.data.error &&
|
||||||
|
typeof err.data.error === 'string' && {
|
||||||
|
description: err.data.error
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
|
@ -105,14 +105,21 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
|
|||||||
|
|
||||||
return oldData
|
return oldData
|
||||||
},
|
},
|
||||||
onError: (_, { type }, oldData) => {
|
onError: (err: any, { type }, oldData) => {
|
||||||
toast({
|
toast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
message: t('common:toastMessage.success.message', {
|
message: t('common:toastMessage.error.message', {
|
||||||
function: t(
|
function: t(
|
||||||
`timeline:shared.header.default.actions.status.${type}.function`
|
`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)
|
queryClient.setQueryData(queryKey, oldData)
|
||||||
}
|
}
|
||||||
|
@ -1,110 +1,18 @@
|
|||||||
import client from '@api/client'
|
import { RelationshipOutgoing } from '@components/Relationship'
|
||||||
import haptics from '@components/haptics'
|
|
||||||
import Icon from '@components/Icon'
|
|
||||||
import { toast } from '@components/toast'
|
|
||||||
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import React from 'react'
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { Pressable, StyleSheet, View } from 'react-native'
|
|
||||||
import { Chase } from 'react-native-animated-spinkit'
|
|
||||||
import { useQuery } from 'react-query'
|
|
||||||
import HeaderSharedAccount from './HeaderShared/Account'
|
import HeaderSharedAccount from './HeaderShared/Account'
|
||||||
import HeaderSharedApplication from './HeaderShared/Application'
|
import HeaderSharedApplication from './HeaderShared/Application'
|
||||||
import HeaderSharedCreated from './HeaderShared/Created'
|
import HeaderSharedCreated from './HeaderShared/Created'
|
||||||
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
import HeaderSharedVisibility from './HeaderShared/Visibility'
|
||||||
|
import RelationshipIncoming from '@root/components/Relationship/Incoming'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
notification: Mastodon.Notification
|
notification: Mastodon.Notification
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineHeaderNotification: React.FC<Props> = ({ 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 (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<View style={styles.accountAndMeta}>
|
<View style={styles.accountAndMeta}>
|
||||||
@ -114,6 +22,8 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
? notification.status.account
|
? notification.status.account
|
||||||
: notification.account
|
: notification.account
|
||||||
}
|
}
|
||||||
|
{...((notification.type === 'follow' ||
|
||||||
|
notification.type === 'follow_request') && { withoutName: true })}
|
||||||
/>
|
/>
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
<HeaderSharedCreated created_at={notification.created_at} />
|
<HeaderSharedCreated created_at={notification.created_at} />
|
||||||
@ -127,7 +37,14 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{notification.type === 'follow' && (
|
{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>
|
</View>
|
||||||
)
|
)
|
||||||
@ -136,10 +53,13 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row'
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start'
|
||||||
},
|
},
|
||||||
accountAndMeta: {
|
accountAndMeta: {
|
||||||
flex: 4
|
flex: 1,
|
||||||
|
flexGrow: 1
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -148,9 +68,8 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: StyleConstants.Spacing.S
|
marginBottom: StyleConstants.Spacing.S
|
||||||
},
|
},
|
||||||
relationship: {
|
relationship: {
|
||||||
flex: 1,
|
flexShrink: 1,
|
||||||
flexDirection: 'row',
|
marginLeft: StyleConstants.Spacing.M
|
||||||
justifyContent: 'center'
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -6,20 +6,26 @@ import { StyleSheet, Text, View } from 'react-native'
|
|||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account
|
account: Mastodon.Account
|
||||||
|
withoutName?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderSharedAccount: React.FC<Props> = ({ account }) => {
|
const HeaderSharedAccount: React.FC<Props> = ({
|
||||||
|
account,
|
||||||
|
withoutName = false
|
||||||
|
}) => {
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.base}>
|
<View style={styles.base}>
|
||||||
<Text numberOfLines={1}>
|
{withoutName ? null : (
|
||||||
<ParseEmojis
|
<Text style={styles.name} numberOfLines={1}>
|
||||||
content={account.display_name || account.username}
|
<ParseEmojis
|
||||||
emojis={account.emojis}
|
content={account.display_name || account.username}
|
||||||
fontBold
|
emojis={account.emojis}
|
||||||
/>
|
fontBold
|
||||||
</Text>
|
/>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<Text style={[styles.acct, { color: theme.secondary }]} numberOfLines={1}>
|
<Text style={[styles.acct, { color: theme.secondary }]} numberOfLines={1}>
|
||||||
@{account.acct}
|
@{account.acct}
|
||||||
</Text>
|
</Text>
|
||||||
@ -32,9 +38,11 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center'
|
alignItems: 'center'
|
||||||
},
|
},
|
||||||
|
name: {
|
||||||
|
marginRight: StyleConstants.Spacing.XS
|
||||||
|
},
|
||||||
acct: {
|
acct: {
|
||||||
flexShrink: 1,
|
flexShrink: 1
|
||||||
marginLeft: StyleConstants.Spacing.XS
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import Icon from '@components/Icon'
|
|||||||
import relativeTime from '@components/relativeTime'
|
import relativeTime from '@components/relativeTime'
|
||||||
import { TimelineData } from '@components/Timelines/Timeline'
|
import { TimelineData } from '@components/Timelines/Timeline'
|
||||||
import { ParseEmojis } from '@root/components/Parse'
|
import { ParseEmojis } from '@root/components/Parse'
|
||||||
|
import { toast } from '@root/components/toast'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
@ -13,35 +14,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useMutation, useQueryClient } from 'react-query'
|
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 {
|
export interface Props {
|
||||||
queryKey: QueryKey.Timeline
|
queryKey: QueryKey.Timeline
|
||||||
poll: NonNullable<Mastodon.Status['poll']>
|
poll: NonNullable<Mastodon.Status['poll']>
|
||||||
@ -57,14 +29,33 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { mode, theme } = useTheme()
|
const { mode, theme } = useTheme()
|
||||||
const { t, i18n } = useTranslation('timeline')
|
const { t, i18n } = useTranslation('timeline')
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const [allOptions, setAllOptions] = useState(
|
const [allOptions, setAllOptions] = useState(
|
||||||
new Array(poll.options.length).fill(false)
|
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, {
|
const mutation = useMutation(fireMutation, {
|
||||||
onSuccess: (data, { id }) => {
|
onSuccess: ({ body }) => {
|
||||||
queryClient.cancelQueries(queryKey)
|
queryClient.cancelQueries(queryKey)
|
||||||
|
|
||||||
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
queryClient.setQueryData<TimelineData>(queryKey, old => {
|
||||||
@ -72,7 +63,7 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
const pageIndex = findIndex(old?.pages, page => {
|
const pageIndex = findIndex(old?.pages, page => {
|
||||||
const tempIndex = findIndex(page.toots, [
|
const tempIndex = findIndex(page.toots, [
|
||||||
reblog ? 'reblog.poll.id' : 'poll.id',
|
reblog ? 'reblog.poll.id' : 'poll.id',
|
||||||
id
|
poll.id
|
||||||
])
|
])
|
||||||
if (tempIndex >= 0) {
|
if (tempIndex >= 0) {
|
||||||
tootIndex = tempIndex
|
tootIndex = tempIndex
|
||||||
@ -84,9 +75,9 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
|
|
||||||
if (pageIndex >= 0 && tootIndex >= 0) {
|
if (pageIndex >= 0 && tootIndex >= 0) {
|
||||||
if (reblog) {
|
if (reblog) {
|
||||||
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = data
|
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = body
|
||||||
} else {
|
} else {
|
||||||
old!.pages[pageIndex].toots[tootIndex].poll = data
|
old!.pages[pageIndex].toots[tootIndex].poll = body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return old
|
return old
|
||||||
@ -94,8 +85,21 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
|
|
||||||
haptics('Success')
|
haptics('Success')
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (err: any) => {
|
||||||
haptics('Error')
|
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 (
|
return (
|
||||||
<View style={styles.button}>
|
<View style={styles.button}>
|
||||||
<Button
|
<Button
|
||||||
onPress={() =>
|
onPress={() => mutation.mutate({ type: 'vote' })}
|
||||||
mutation.mutate({ id: poll.id, options: allOptions })
|
|
||||||
}
|
|
||||||
type='text'
|
type='text'
|
||||||
content={t('shared.poll.meta.button.vote')}
|
content={t('shared.poll.meta.button.vote')}
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
@ -119,7 +121,7 @@ const TimelinePoll: React.FC<Props> = ({
|
|||||||
return (
|
return (
|
||||||
<View style={styles.button}>
|
<View style={styles.button}>
|
||||||
<Button
|
<Button
|
||||||
onPress={() => mutation.mutate({ id: poll.id })}
|
onPress={() => mutation.mutate({ type: 'refresh' })}
|
||||||
type='text'
|
type='text'
|
||||||
content={t('shared.poll.meta.button.refresh')}
|
content={t('shared.poll.meta.button.refresh')}
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isLoading}
|
||||||
|
@ -5,6 +5,7 @@ import React from 'react'
|
|||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
|
import * as Sentry from 'sentry-expo'
|
||||||
|
|
||||||
export interface Params {
|
export interface Params {
|
||||||
type: 'success' | 'error' | 'warning'
|
type: 'success' | 'error' | 'warning'
|
||||||
@ -73,11 +74,17 @@ const ToastBase = ({ config }: { config: Config }) => {
|
|||||||
color={theme[colorMapping[config.type]]}
|
color={theme[colorMapping[config.type]]}
|
||||||
/>
|
/>
|
||||||
<View style={styles.texts}>
|
<View style={styles.texts}>
|
||||||
<Text style={[styles.text1, { color: theme.primary }]}>
|
<Text
|
||||||
|
style={[styles.text1, { color: theme.primary }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
{config.text1}
|
{config.text1}
|
||||||
</Text>
|
</Text>
|
||||||
{config.text2 && (
|
{config.text2 && (
|
||||||
<Text style={[styles.text2, { color: theme.secondary }]}>
|
<Text
|
||||||
|
style={[styles.text2, { color: theme.secondary }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
{config.text2}
|
{config.text2}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
@ -89,8 +96,11 @@ const ToastBase = ({ config }: { config: Config }) => {
|
|||||||
|
|
||||||
const toastConfig = {
|
const toastConfig = {
|
||||||
success: (config: Config) => <ToastBase config={config} />,
|
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({
|
const styles = StyleSheet.create({
|
||||||
|
@ -19,5 +19,6 @@ export default {
|
|||||||
sharedToot: require('./screens/sharedToot').default,
|
sharedToot: require('./screens/sharedToot').default,
|
||||||
sharedAnnouncements: require('./screens/sharedAnnouncements').default,
|
sharedAnnouncements: require('./screens/sharedAnnouncements').default,
|
||||||
|
|
||||||
|
relationship: require('./components/relationship').default,
|
||||||
timeline: require('./components/timeline').default
|
timeline: require('./components/timeline').default
|
||||||
}
|
}
|
||||||
|
16
src/i18n/zh/components/relationship.ts
Normal file
16
src/i18n/zh/components/relationship.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export default {
|
||||||
|
follow: {
|
||||||
|
function: '关注'
|
||||||
|
},
|
||||||
|
block: {
|
||||||
|
function: '屏蔽'
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
error: '读取错误',
|
||||||
|
blocked_by: '被用户屏蔽',
|
||||||
|
blocking: '取消屏蔽',
|
||||||
|
following: '取消关注',
|
||||||
|
requested: '取消关注请求',
|
||||||
|
default: '关注'
|
||||||
|
}
|
||||||
|
}
|
@ -12,7 +12,9 @@ export default {
|
|||||||
actioned: {
|
actioned: {
|
||||||
pinned: '置顶',
|
pinned: '置顶',
|
||||||
favourite: '{{name}} 喜欢了你的嘟嘟',
|
favourite: '{{name}} 喜欢了你的嘟嘟',
|
||||||
|
status: '{{name}} 刚刚发嘟',
|
||||||
follow: '{{name}} 开始关注你',
|
follow: '{{name}} 开始关注你',
|
||||||
|
follow_request: '{{name}} 请求关注',
|
||||||
poll: '您参与的投票已结束',
|
poll: '您参与的投票已结束',
|
||||||
reblog: {
|
reblog: {
|
||||||
default: '{{name}} 转嘟了',
|
default: '{{name}} 转嘟了',
|
||||||
@ -52,6 +54,11 @@ export default {
|
|||||||
shared: {
|
shared: {
|
||||||
application: '发自于 {{application}}'
|
application: '发自于 {{application}}'
|
||||||
},
|
},
|
||||||
|
conversation: {
|
||||||
|
delete: {
|
||||||
|
function: '删除私信'
|
||||||
|
}
|
||||||
|
},
|
||||||
default: {
|
default: {
|
||||||
actions: {
|
actions: {
|
||||||
account: {
|
account: {
|
||||||
|
@ -8,7 +8,7 @@ const ScreenLocal: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Timelines
|
<Timelines
|
||||||
name='Screen-Local-Root'
|
name='Local'
|
||||||
content={[
|
content={[
|
||||||
{ title: t('local:heading.segments.left'), page: 'Following' },
|
{ title: t('local:heading.segments.left'), page: 'Following' },
|
||||||
{ title: t('local:heading.segments.right'), page: 'Local' }
|
{ title: t('local:heading.segments.right'), page: 'Local' }
|
||||||
|
@ -10,16 +10,18 @@ import accountInitialState from '@screens/Shared/Account/utils/initialState'
|
|||||||
import AccountContext from '@screens/Shared/Account/utils/createContext'
|
import AccountContext from '@screens/Shared/Account/utils/createContext'
|
||||||
import { getLocalUrl } from '@utils/slices/instancesSlice'
|
import { getLocalUrl } from '@utils/slices/instancesSlice'
|
||||||
import React, { useReducer, useRef, useState } from 'react'
|
import React, { useReducer, useRef, useState } from 'react'
|
||||||
import { Animated, ScrollView } from 'react-native'
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
import Animated, {
|
||||||
|
useAnimatedScrollHandler,
|
||||||
|
useSharedValue
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
|
||||||
const ScreenMeRoot: React.FC = () => {
|
const ScreenMeRoot: React.FC = () => {
|
||||||
const localRegistered = useSelector(getLocalUrl)
|
const localRegistered = useSelector(getLocalUrl)
|
||||||
|
|
||||||
const scrollRef = useRef<ScrollView>(null)
|
const scrollRef = useRef<Animated.ScrollView>(null)
|
||||||
useScrollToTop(scrollRef)
|
useScrollToTop(scrollRef)
|
||||||
|
|
||||||
const scrollY = useRef(new Animated.Value(0))
|
|
||||||
const [data, setData] = useState<Mastodon.Account>()
|
const [data, setData] = useState<Mastodon.Account>()
|
||||||
|
|
||||||
const [accountState, accountDispatch] = useReducer(
|
const [accountState, accountDispatch] = useReducer(
|
||||||
@ -27,26 +29,27 @@ const ScreenMeRoot: React.FC = () => {
|
|||||||
accountInitialState
|
accountInitialState
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const scrollY = useSharedValue(0)
|
||||||
|
const onScroll = useAnimatedScrollHandler(event => {
|
||||||
|
scrollY.value = event.contentOffset.y
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||||
{localRegistered && data ? (
|
{localRegistered && data ? (
|
||||||
<AccountNav scrollY={scrollY} account={data} />
|
<AccountNav scrollY={scrollY} account={data} />
|
||||||
) : null}
|
) : null}
|
||||||
<ScrollView
|
<Animated.ScrollView
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
keyboardShouldPersistTaps='handled'
|
keyboardShouldPersistTaps='handled'
|
||||||
bounces={false}
|
onScroll={onScroll}
|
||||||
onScroll={Animated.event(
|
scrollEventThrottle={16}
|
||||||
[{ nativeEvent: { contentOffset: { y: scrollY.current } } }],
|
|
||||||
{ useNativeDriver: false }
|
|
||||||
)}
|
|
||||||
scrollEventThrottle={8}
|
|
||||||
>
|
>
|
||||||
{localRegistered ? <MyInfo setData={setData} /> : <Login />}
|
{localRegistered ? <MyInfo setData={setData} /> : <Login />}
|
||||||
{localRegistered && <Collections />}
|
{localRegistered && <Collections />}
|
||||||
<Settings />
|
<Settings />
|
||||||
{localRegistered && <Logout />}
|
{localRegistered && <Logout />}
|
||||||
</ScrollView>
|
</Animated.ScrollView>
|
||||||
</AccountContext.Provider>
|
</AccountContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ import {
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import prettyBytes from 'pretty-bytes'
|
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 { 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 { CacheManager } from 'react-native-expo-image-cache'
|
||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ const ScreenPublic: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Timelines
|
<Timelines
|
||||||
name='Screen-Public-Root'
|
name='Public'
|
||||||
content={[
|
content={[
|
||||||
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
{ title: t('public:heading.segments.left'), page: 'LocalPublic' },
|
||||||
{ title: t('public:heading.segments.right'), page: 'RemotePublic' }
|
{ title: t('public:heading.segments.right'), page: 'RemotePublic' }
|
||||||
|
@ -4,7 +4,10 @@ import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/H
|
|||||||
import { accountFetch } from '@utils/fetches/accountFetch'
|
import { accountFetch } from '@utils/fetches/accountFetch'
|
||||||
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
import { getLocalAccountId } from '@utils/slices/instancesSlice'
|
||||||
import React, { useEffect, useReducer, useRef, useState } from 'react'
|
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 { useQuery } from 'react-query'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import AccountHeader from './Account/Header'
|
import AccountHeader from './Account/Header'
|
||||||
@ -36,7 +39,7 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||||||
const localAccountId = useSelector(getLocalAccountId)
|
const localAccountId = useSelector(getLocalAccountId)
|
||||||
const { data } = useQuery(['Account', { id: account.id }], accountFetch)
|
const { data } = useQuery(['Account', { id: account.id }], accountFetch)
|
||||||
|
|
||||||
const scrollY = useRef(new Animated.Value(0))
|
const scrollY = useSharedValue(0)
|
||||||
const [accountState, accountDispatch] = useReducer(
|
const [accountState, accountDispatch] = useReducer(
|
||||||
accountReducer,
|
accountReducer,
|
||||||
accountInitialState
|
accountInitialState
|
||||||
@ -56,6 +59,10 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||||||
return updateHeaderRight()
|
return updateHeaderRight()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const onScroll = useAnimatedScrollHandler(event => {
|
||||||
|
scrollY.value = event.contentOffset.y
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
<AccountContext.Provider value={{ accountState, accountDispatch }}>
|
||||||
<AccountNav scrollY={scrollY} account={data} />
|
<AccountNav scrollY={scrollY} account={data} />
|
||||||
@ -63,18 +70,15 @@ const ScreenSharedAccount: React.FC<Props> = ({
|
|||||||
accountState.informationLayout.y ? (
|
accountState.informationLayout.y ? (
|
||||||
<AccountSegmentedControl scrollY={scrollY} />
|
<AccountSegmentedControl scrollY={scrollY} />
|
||||||
) : null}
|
) : null}
|
||||||
<ScrollView
|
<Animated.ScrollView
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
onScroll={Animated.event(
|
onScroll={onScroll}
|
||||||
[{ nativeEvent: { contentOffset: { y: scrollY.current } } }],
|
|
||||||
{ useNativeDriver: false }
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<AccountHeader account={data} />
|
<AccountHeader account={data} />
|
||||||
<AccountInformation account={data} />
|
<AccountInformation account={data} />
|
||||||
<AccountToots id={account.id} />
|
<AccountToots id={account.id} />
|
||||||
</ScrollView>
|
</Animated.ScrollView>
|
||||||
|
|
||||||
<BottomSheet
|
<BottomSheet
|
||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { useContext, useEffect, useState } from 'react'
|
import React, { useContext, useEffect } from 'react'
|
||||||
import { Dimensions, Image, StyleSheet, View } from 'react-native'
|
import { Dimensions, Image } from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import AccountContext from './utils/createContext'
|
import AccountContext from './utils/createContext'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -11,59 +16,40 @@ export interface Props {
|
|||||||
const AccountHeader: React.FC<Props> = ({ account, limitHeight = false }) => {
|
const AccountHeader: React.FC<Props> = ({ account, limitHeight = false }) => {
|
||||||
const { accountState, accountDispatch } = useContext(AccountContext)
|
const { accountState, accountDispatch } = useContext(AccountContext)
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const [ratio, setRatio] = useState(accountState.headerRatio)
|
|
||||||
|
|
||||||
let isMounted = false
|
const height = useSharedValue(
|
||||||
useEffect(() => {
|
Dimensions.get('screen').width * accountState.headerRatio
|
||||||
isMounted = true
|
)
|
||||||
|
const styleHeight = useAnimatedStyle(() => {
|
||||||
return () => {
|
return {
|
||||||
isMounted = false
|
height: withTiming(height.value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
account?.header &&
|
account?.header &&
|
||||||
!account.header.includes('/headers/original/missing.png')
|
!account.header.includes('/headers/original/missing.png')
|
||||||
) {
|
) {
|
||||||
isMounted &&
|
Image.getSize(account.header, (width, height) => {
|
||||||
Image.getSize(account.header, (width, height) => {
|
if (!limitHeight) {
|
||||||
if (!limitHeight) {
|
accountDispatch({
|
||||||
accountDispatch &&
|
type: 'headerRatio',
|
||||||
accountDispatch({
|
payload: height / width
|
||||||
type: 'headerRatio',
|
})
|
||||||
payload: height / width
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
isMounted &&
|
|
||||||
setRatio(limitHeight ? accountState.headerRatio : height / width)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
isMounted && setRatio(1 / 3)
|
|
||||||
}
|
}
|
||||||
}, [account, isMounted])
|
}, [account])
|
||||||
|
|
||||||
const windowWidth = Dimensions.get('window').width
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<Animated.Image
|
||||||
style={{
|
source={{ uri: account?.header }}
|
||||||
height: windowWidth * ratio,
|
style={[styleHeight, { backgroundColor: theme.disabled }]}
|
||||||
backgroundColor: theme.disabled
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image source={{ uri: account?.header }} style={styles.image} />
|
|
||||||
</View>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
image: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default React.memo(
|
export default React.memo(
|
||||||
AccountHeader,
|
AccountHeader,
|
||||||
(_, next) => next.account === undefined
|
(_, next) => next.account === undefined
|
||||||
|
@ -1,148 +1,38 @@
|
|||||||
|
import Button from '@components/Button'
|
||||||
|
import { RelationshipOutgoing } from '@components/Relationship'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import client from '@root/api/client'
|
import { relationshipFetch } from '@utils/fetches/relationshipFetch'
|
||||||
import Button from '@root/components/Button'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import haptics from '@root/components/haptics'
|
import React from 'react'
|
||||||
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 { StyleSheet, View } from 'react-native'
|
import { StyleSheet, View } from 'react-native'
|
||||||
import { useMutation, useQuery, useQueryClient } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
account: Mastodon.Account | undefined
|
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 AccountInformationActions: React.FC<Props> = ({ account }) => {
|
||||||
const { theme } = useTheme()
|
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
const relationshipQueryKey = ['Relationship', { id: account?.id }]
|
const relationshipQueryKey = ['Relationship', { id: account?.id }]
|
||||||
const query = useQuery(relationshipQueryKey, relationshipFetch)
|
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 (
|
return (
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
{query.data && !query.data.blocked_by ? (
|
{query.data && !query.data.blocked_by ? (
|
||||||
<Button
|
<Button
|
||||||
|
round
|
||||||
type='icon'
|
type='icon'
|
||||||
content='Mail'
|
content='Mail'
|
||||||
round
|
style={styles.actionConversation}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigation.navigate('Screen-Shared-Compose', {
|
navigation.navigate('Screen-Shared-Compose', {
|
||||||
type: 'conversation',
|
type: 'conversation',
|
||||||
incomingStatus: { account }
|
incomingStatus: { account }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
style={styles.actionConversation}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{mainAction}
|
{account && account.id && <RelationshipOutgoing id={account.id} />}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -152,10 +42,7 @@ const styles = StyleSheet.create({
|
|||||||
alignSelf: 'flex-end',
|
alignSelf: 'flex-end',
|
||||||
flexDirection: 'row'
|
flexDirection: 'row'
|
||||||
},
|
},
|
||||||
actionConversation: { marginRight: StyleConstants.Spacing.S },
|
actionConversation: { marginRight: StyleConstants.Spacing.S }
|
||||||
error: {
|
|
||||||
...StyleConstants.FontStyle.S
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default AccountInformationActions
|
export default AccountInformationActions
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
import { ParseEmojis } from '@components/Parse'
|
import { ParseEmojis } from '@components/Parse'
|
||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { MutableRefObject, useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { Animated, Dimensions, StyleSheet, Text, View } from 'react-native'
|
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 { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import AccountContext from './utils/createContext'
|
import AccountContext from './utils/createContext'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollY: MutableRefObject<Animated.Value>
|
scrollY: Animated.SharedValue<number>
|
||||||
account: Mastodon.Account | undefined
|
account: Mastodon.Account | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,19 +28,28 @@ const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
|
|||||||
StyleConstants.Spacing.M -
|
StyleConstants.Spacing.M -
|
||||||
headerHeight
|
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 (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.base,
|
styles.base,
|
||||||
{
|
styleOpacity,
|
||||||
backgroundColor: theme.background,
|
{ backgroundColor: theme.background, height: headerHeight }
|
||||||
opacity: scrollY.current.interpolate({
|
|
||||||
inputRange: [0, 200],
|
|
||||||
outputRange: [0, 1],
|
|
||||||
extrapolate: 'clamp'
|
|
||||||
}),
|
|
||||||
height: headerHeight
|
|
||||||
}
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -47,18 +61,7 @@ const AccountNav: React.FC<Props> = ({ scrollY, account }) => {
|
|||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<Animated.View style={[styles.display_name, styleMarginTop]}>
|
||||||
style={[
|
|
||||||
styles.display_name,
|
|
||||||
{
|
|
||||||
marginTop: scrollY.current.interpolate({
|
|
||||||
inputRange: [nameY, nameY + 20],
|
|
||||||
outputRange: [50, 0],
|
|
||||||
extrapolate: 'clamp'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{account ? (
|
{account ? (
|
||||||
<Text numberOfLines={1}>
|
<Text numberOfLines={1}>
|
||||||
<ParseEmojis
|
<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
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import SegmentedControl from '@react-native-community/segmented-control'
|
import SegmentedControl from '@react-native-community/segmented-control'
|
||||||
import { StyleConstants } from '@root/utils/styles/constants'
|
import { StyleConstants } from '@root/utils/styles/constants'
|
||||||
import { useTheme } from '@root/utils/styles/ThemeManager'
|
import { useTheme } from '@root/utils/styles/ThemeManager'
|
||||||
import React, { MutableRefObject, useContext } from 'react'
|
import React, { useContext } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
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 { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import AccountContext from './utils/createContext'
|
import AccountContext from './utils/createContext'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
scrollY: MutableRefObject<Animated.Value>
|
scrollY: Animated.SharedValue<number>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AccountSegmentedControl: React.FC<Props> = ({ scrollY }) => {
|
const AccountSegmentedControl: React.FC<Props> = ({ scrollY }) => {
|
||||||
@ -17,32 +22,40 @@ const AccountSegmentedControl: React.FC<Props> = ({ scrollY }) => {
|
|||||||
const { mode, theme } = useTheme()
|
const { mode, theme } = useTheme()
|
||||||
|
|
||||||
const headerHeight = useSafeAreaInsets().top + 44
|
const headerHeight = useSafeAreaInsets().top + 44
|
||||||
const translateY = scrollY.current.interpolate({
|
const styleTransform = useAnimatedStyle(() => {
|
||||||
inputRange: [
|
return {
|
||||||
0,
|
transform: [
|
||||||
(accountState.informationLayout?.y || 0) +
|
{
|
||||||
(accountState.informationLayout?.height || 0) -
|
translateY: interpolate(
|
||||||
headerHeight
|
scrollY.value,
|
||||||
],
|
[
|
||||||
outputRange: [
|
0,
|
||||||
0,
|
(accountState.informationLayout?.y || 0) +
|
||||||
-(accountState.informationLayout?.y || 0) -
|
(accountState.informationLayout?.height || 0) -
|
||||||
(accountState.informationLayout?.height || 0) +
|
headerHeight
|
||||||
headerHeight
|
],
|
||||||
],
|
[
|
||||||
extrapolate: 'clamp',
|
0,
|
||||||
easing: undefined
|
-(accountState.informationLayout?.y || 0) -
|
||||||
|
(accountState.informationLayout?.height || 0) +
|
||||||
|
headerHeight
|
||||||
|
],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.base,
|
styles.base,
|
||||||
|
styleTransform,
|
||||||
{
|
{
|
||||||
top:
|
top:
|
||||||
(accountState.informationLayout?.y || 0) +
|
(accountState.informationLayout?.y || 0) +
|
||||||
(accountState.informationLayout?.height || 0),
|
(accountState.informationLayout?.height || 0),
|
||||||
transform: [{ translateY }],
|
|
||||||
borderTopColor: theme.border,
|
borderTopColor: theme.border,
|
||||||
backgroundColor: theme.background
|
backgroundColor: theme.background
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { StyleConstants } from '@utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { useTheme } from '@utils/styles/ThemeManager'
|
import { useTheme } from '@utils/styles/ThemeManager'
|
||||||
import React, { MutableRefObject, useCallback, useContext, useRef } from 'react'
|
import React, { MutableRefObject, useContext } from 'react'
|
||||||
import {
|
import { Dimensions, Image, StyleSheet, Text, View } from 'react-native'
|
||||||
Animated,
|
|
||||||
Dimensions,
|
|
||||||
Image,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View
|
|
||||||
} from 'react-native'
|
|
||||||
import { PanGestureHandler } from 'react-native-gesture-handler'
|
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 Svg, { Circle, G, Path } from 'react-native-svg'
|
||||||
import ComposeContext from '../utils/createContext'
|
import ComposeContext from '../utils/createContext'
|
||||||
|
|
||||||
@ -35,48 +36,61 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
|
|||||||
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect!
|
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.original?.aspect!
|
||||||
}
|
}
|
||||||
|
|
||||||
const panFocus = useRef(
|
const panX = useSharedValue(
|
||||||
new Animated.ValueXY(
|
(((theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.x || 0) *
|
||||||
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.x &&
|
imageDimensionis.width) /
|
||||||
(theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.y
|
2
|
||||||
? {
|
)
|
||||||
x:
|
const panY = useSharedValue(
|
||||||
((theAttachmentRemote as Mastodon.AttachmentImage).meta!.focus!
|
(((theAttachmentRemote as Mastodon.AttachmentImage).meta?.focus?.y || 0) *
|
||||||
.x *
|
imageDimensionis.height) /
|
||||||
imageDimensionis.width) /
|
2
|
||||||
2,
|
)
|
||||||
y:
|
const updateFocus = ({ x, y }: { x: number; y: number }) => {
|
||||||
(-(theAttachmentRemote as Mastodon.AttachmentImage).meta!.focus!
|
focus.current = { x, y }
|
||||||
.y *
|
}
|
||||||
imageDimensionis.height) /
|
type PanContext = {
|
||||||
2
|
startX: number
|
||||||
}
|
startY: number
|
||||||
: { x: 0, y: 0 }
|
}
|
||||||
)
|
const onGestureEvent = useAnimatedGestureHandler({
|
||||||
).current
|
onStart: (_, context: PanContext) => {
|
||||||
const panX = panFocus.x.interpolate({
|
context.startX = panX.value
|
||||||
inputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
|
context.startY = panY.value
|
||||||
outputRange: [-imageDimensionis.width / 2, imageDimensionis.width / 2],
|
},
|
||||||
extrapolate: 'clamp'
|
onActive: ({ translationX, translationY }, context: PanContext) => {
|
||||||
})
|
panX.value = context.startX + translationX
|
||||||
const panY = panFocus.y.interpolate({
|
panY.value = context.startY + translationY
|
||||||
inputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
|
},
|
||||||
outputRange: [-imageDimensionis.height / 2, imageDimensionis.height / 2],
|
onEnd: ({ translationX, translationY }, context: PanContext) => {
|
||||||
extrapolate: 'clamp'
|
runOnJS(updateFocus)({
|
||||||
})
|
x: (context.startX + translationX) / (imageDimensionis.width / 2),
|
||||||
panFocus.addListener(e => {
|
y: (context.startY + translationY) / (imageDimensionis.height / 2)
|
||||||
focus.current = {
|
})
|
||||||
x: e.x / (imageDimensionis.width / 2),
|
}
|
||||||
y: -e.y / (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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -90,17 +104,14 @@ const ComposeEditAttachmentImage: React.FC<Props> = ({ index, focus }) => {
|
|||||||
uri: theAttachmentLocal.uri || theAttachmentRemote.preview_url
|
uri: theAttachmentLocal.uri || theAttachmentRemote.preview_url
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PanGestureHandler
|
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||||
onGestureEvent={handleGesture}
|
|
||||||
onHandlerStateChange={onHandlerStateChange}
|
|
||||||
>
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
|
styleTransform,
|
||||||
{
|
{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -1000 + imageDimensionis.height / 2,
|
top: -1000 + imageDimensionis.height / 2,
|
||||||
left: -1000 + imageDimensionis.width / 2,
|
left: -1000 + imageDimensionis.width / 2
|
||||||
transform: [{ translateX: panX }, { translateY: panY }]
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
|
@ -1,17 +1,14 @@
|
|||||||
import { getLocalAccountPreferences } from '@root/utils/slices/instancesSlice'
|
|
||||||
import { store } from '@root/store'
|
import { store } from '@root/store'
|
||||||
|
import { getLocalAccountPreferences } from '@utils/slices/instancesSlice'
|
||||||
import composeInitialState from './initialState'
|
import composeInitialState from './initialState'
|
||||||
import { ComposeState } from './types'
|
import { ComposeState } from './types'
|
||||||
|
|
||||||
const composeParseState = ({
|
export interface Props {
|
||||||
type,
|
|
||||||
incomingStatus,
|
|
||||||
visibilityLock
|
|
||||||
}: {
|
|
||||||
type: 'reply' | 'conversation' | 'edit'
|
type: 'reply' | 'conversation' | 'edit'
|
||||||
incomingStatus: Mastodon.Status
|
incomingStatus: Mastodon.Status
|
||||||
visibilityLock?: boolean
|
}
|
||||||
}): ComposeState => {
|
|
||||||
|
const composeParseState = ({ type, incomingStatus }: Props): ComposeState => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'edit':
|
case 'edit':
|
||||||
return {
|
return {
|
||||||
@ -52,10 +49,8 @@ const composeParseState = ({
|
|||||||
const actualStatus = incomingStatus.reblog || incomingStatus
|
const actualStatus = incomingStatus.reblog || incomingStatus
|
||||||
return {
|
return {
|
||||||
...composeInitialState,
|
...composeInitialState,
|
||||||
...(visibilityLock && {
|
visibility: actualStatus.visibility,
|
||||||
visibility: 'direct',
|
visibilityLock: actualStatus.visibility === 'direct',
|
||||||
visibilityLock: true
|
|
||||||
}),
|
|
||||||
replyToStatus: actualStatus
|
replyToStatus: actualStatus
|
||||||
}
|
}
|
||||||
case 'conversation':
|
case 'conversation':
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import haptics from '@root/components/haptics'
|
import haptics from '@components/haptics'
|
||||||
import { HeaderLeft, HeaderRight } from '@root/components/Header'
|
import { HeaderLeft, HeaderRight } from '@components/Header'
|
||||||
import { StyleConstants } from '@root/utils/styles/constants'
|
import { StyleConstants } from '@utils/styles/constants'
|
||||||
import { findIndex } from 'lodash'
|
import { findIndex } from 'lodash'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import { ActionSheetIOS, Image, StyleSheet, Text } from 'react-native'
|
import { ActionSheetIOS, Image, StyleSheet, Text } from 'react-native'
|
||||||
@ -22,13 +22,13 @@ export interface Props {
|
|||||||
imageIndex: number
|
imageIndex: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
navigation: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const TheImage = ({
|
const TheImage = ({
|
||||||
style,
|
style,
|
||||||
source,
|
source,
|
||||||
imageUrls,
|
imageUrls
|
||||||
imageIndex
|
|
||||||
}: {
|
}: {
|
||||||
style: any
|
style: any
|
||||||
source: { uri: string }
|
source: { uri: string }
|
||||||
@ -37,7 +37,6 @@ const TheImage = ({
|
|||||||
remote_url: Mastodon.AttachmentImage['remote_url']
|
remote_url: Mastodon.AttachmentImage['remote_url']
|
||||||
imageIndex: number
|
imageIndex: number
|
||||||
})[]
|
})[]
|
||||||
imageIndex: number
|
|
||||||
}) => {
|
}) => {
|
||||||
const [imageVisible, setImageVisible] = useState(false)
|
const [imageVisible, setImageVisible] = useState(false)
|
||||||
Image.getSize(source.uri, () => setImageVisible(true))
|
Image.getSize(source.uri, () => setImageVisible(true))
|
||||||
@ -47,8 +46,7 @@ const TheImage = ({
|
|||||||
source={{
|
source={{
|
||||||
uri: imageVisible
|
uri: imageVisible
|
||||||
? source.uri
|
? source.uri
|
||||||
: imageUrls[findIndex(imageUrls, ['imageIndex', imageIndex])]
|
: imageUrls[findIndex(imageUrls, ['url', source.uri])].preview_url
|
||||||
.preview_url
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -68,19 +66,18 @@ const ScreenSharedImagesViewer: React.FC<Props> = ({
|
|||||||
const component = useCallback(
|
const component = useCallback(
|
||||||
() => (
|
() => (
|
||||||
<ImageViewer
|
<ImageViewer
|
||||||
style={{ flex: 1, marginBottom: 44 + safeAreaInsets.bottom }}
|
|
||||||
imageUrls={imageUrls}
|
|
||||||
index={initialIndex}
|
index={initialIndex}
|
||||||
onSwipeDown={() => navigation.goBack()}
|
imageUrls={imageUrls}
|
||||||
|
pageAnimateTime={250}
|
||||||
enableSwipeDown={true}
|
enableSwipeDown={true}
|
||||||
swipeDownThreshold={100}
|
|
||||||
useNativeDriver={true}
|
useNativeDriver={true}
|
||||||
saveToLocalByLongPress={false}
|
swipeDownThreshold={100}
|
||||||
renderIndicator={() => <></>}
|
renderIndicator={() => <></>}
|
||||||
|
saveToLocalByLongPress={false}
|
||||||
|
onSwipeDown={() => navigation.goBack()}
|
||||||
|
style={{ flex: 1, marginBottom: 44 + safeAreaInsets.bottom }}
|
||||||
onChange={index => index !== undefined && setCurrentIndex(index)}
|
onChange={index => index !== undefined && setCurrentIndex(index)}
|
||||||
renderImage={props => (
|
renderImage={props => <TheImage {...props} imageUrls={imageUrls} />}
|
||||||
<TheImage {...props} imageUrls={imageUrls} imageIndex={imageIndex} />
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
|
@ -3,13 +3,13 @@ const Base = 4
|
|||||||
export const StyleConstants = {
|
export const StyleConstants = {
|
||||||
Font: {
|
Font: {
|
||||||
Size: { S: 14, M: 16, L: 18 },
|
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' }
|
Weight: { Bold: '600' as '600' }
|
||||||
},
|
},
|
||||||
FontStyle: {
|
FontStyle: {
|
||||||
S: { fontSize: 14, lineHeight: 16 },
|
S: { fontSize: 14, lineHeight: 18 },
|
||||||
M: { fontSize: 16, lineHeight: 20 },
|
M: { fontSize: 16, lineHeight: 22 },
|
||||||
L: { fontSize: 20, lineHeight: 24 }
|
L: { fontSize: 20, lineHeight: 30 }
|
||||||
},
|
},
|
||||||
|
|
||||||
Spacing: {
|
Spacing: {
|
||||||
|
111
yarn.lock
111
yarn.lock
@ -736,7 +736,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/helper-plugin-utils" "^7.10.4"
|
"@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"
|
version "7.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.12.1.tgz#9102b06625f60a5443cc292d32b565373665e1e4"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.12.1.tgz#9102b06625f60a5443cc292d32b565373665e1e4"
|
||||||
integrity sha512-geUHn4XwHznRAFiuROTy0Hr7bKbpijJCmr1Svt/VNGhpxmp0OrdxURNpWbOAf94nUbL+xj6gbxRVPHWIbRpRoA==
|
integrity sha512-geUHn4XwHznRAFiuROTy0Hr7bKbpijJCmr1Svt/VNGhpxmp0OrdxURNpWbOAf94nUbL+xj6gbxRVPHWIbRpRoA==
|
||||||
@ -1145,6 +1145,25 @@
|
|||||||
pouchdb-collections "^1.0.1"
|
pouchdb-collections "^1.0.1"
|
||||||
tiny-queue "^0.2.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":
|
"@hapi/address@2.x.x":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
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"
|
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==
|
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":
|
"@react-navigation/bottom-tabs@^5.11.2":
|
||||||
version "5.11.2"
|
version "5.11.2"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.2.tgz#5b541612fcecdea2a5024a4028da35e4a727bde6"
|
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-5.11.2.tgz#5b541612fcecdea2a5024a4028da35e4a727bde6"
|
||||||
@ -1969,6 +1983,11 @@
|
|||||||
xcode "2.0.0"
|
xcode "2.0.0"
|
||||||
yargs "^12.0.2"
|
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":
|
"@sinonjs/commons@^1.7.0":
|
||||||
version "1.8.1"
|
version "1.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
|
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217"
|
||||||
@ -2281,6 +2300,11 @@ abort-controller@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
event-target-shim "^5.0.0"
|
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:
|
absolute-path@^0.0.0:
|
||||||
version "0.0.0"
|
version "0.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/absolute-path/-/absolute-path-0.0.0.tgz#a78762fbdadfb5297be99b15d35a785b2f095bf7"
|
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"
|
js-yaml "^3.13.1"
|
||||||
parse-json "^4.0.0"
|
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:
|
cross-spawn@^5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
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"
|
setimmediate "^1.0.5"
|
||||||
ua-parser-js "^0.7.18"
|
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:
|
figures@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
|
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"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||||
|
|
||||||
nanoid@^3.1.15:
|
nanoid@^3.1.15, nanoid@^3.1.20:
|
||||||
version "3.1.20"
|
version "3.1.20"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
|
||||||
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
|
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"
|
resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f"
|
||||||
integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q==
|
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:
|
node-fetch@^1.0.1:
|
||||||
version "1.7.3"
|
version "1.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
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"
|
encoding "^0.1.11"
|
||||||
is-stream "^1.0.1"
|
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:
|
node-int64@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
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"
|
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
|
||||||
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
|
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:
|
normalize-url@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
|
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"
|
resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b"
|
||||||
integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==
|
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:
|
parse5@5.1.0:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
|
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"
|
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
|
||||||
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
|
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
|
||||||
|
|
||||||
react-native-reanimated@~1.13.0:
|
react-native-reanimated@2.0.0-rc.0:
|
||||||
version "1.13.2"
|
version "2.0.0-rc.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-1.13.2.tgz#1ae5457b24b4913d173a5a064bb28eae7783d293"
|
resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-2.0.0-rc.0.tgz#7a1b0bfd48e3de9dfa985a524463b6a216531358"
|
||||||
integrity sha512-O+WhgxSjOIzcVdAAvx+h2DY331Ek1knKlaq+jsNLpC1fhRy9XTdOObovgob/aF2ve9uJfPEawCx8381g/tUJZQ==
|
integrity sha512-v+SMpeSxQ8kO116B5q3/D6VlFSot4eIRASw0nxxU+6zh9wb4W8shMyQi7/ag/gt246FvjBZOPwxsBS2iTcw8Zg==
|
||||||
dependencies:
|
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:
|
react-native-safe-area-context@3.1.9:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
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:
|
string-length@^3.1.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-3.1.0.tgz#107ef8c23456e187a8abd4a61162ff4ac6e25837"
|
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"
|
has-flag "^4.0.0"
|
||||||
supports-color "^7.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:
|
symbol-observable@1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user