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