mirror of
https://github.com/tooot-app/app
synced 2025-06-05 22:19:13 +02:00
Updates
This commit is contained in:
@ -1,10 +1,14 @@
|
||||
import React from 'react'
|
||||
import React, { useRef } 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 {
|
||||
PanGestureHandler,
|
||||
State,
|
||||
TapGestureHandler
|
||||
} from 'react-native-gesture-handler'
|
||||
import Animated, {
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
@ -55,33 +59,44 @@ const BottomSheet: React.FC<Props> = ({ children, visible, handleDismiss }) => {
|
||||
|
||||
return (
|
||||
<Modal animated animationType='fade' visible={visible} transparent>
|
||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={({ nativeEvent }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
callDismiss()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
style={[styles.overlay, { backgroundColor: theme.backgroundOverlay }]}
|
||||
>
|
||||
<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>
|
||||
<PanGestureHandler onGestureEvent={onGestureEvent}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
styleTop,
|
||||
{
|
||||
backgroundColor: theme.background,
|
||||
paddingBottom: insets.bottom || StyleConstants.Spacing.L
|
||||
}
|
||||
]}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.handle,
|
||||
{ backgroundColor: theme.primaryOverlay }
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
<Button
|
||||
type='text'
|
||||
content='取消'
|
||||
onPress={() => handleDismiss()}
|
||||
style={styles.button}
|
||||
/>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</Animated.View>
|
||||
</PanGestureHandler>
|
||||
</TapGestureHandler>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
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, { useMemo } from 'react'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
|
||||
@ -24,6 +25,15 @@ const HeaderRight: React.FC<Props> = ({
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const mounted = useRef(false)
|
||||
useEffect(() => {
|
||||
if (mounted.current) {
|
||||
layoutAnimation()
|
||||
} else {
|
||||
mounted.current = true
|
||||
}
|
||||
}, [content, loading, disabled])
|
||||
|
||||
const loadingSpinkit = useMemo(
|
||||
() => (
|
||||
<View style={{ position: 'absolute' }}>
|
||||
|
@ -3,8 +3,9 @@ import { StyleConstants } from '@utils/styles/constants'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
import { ColorDefinitions } from '@utils/styles/themes'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Pressable, StyleSheet, Switch, Text, View } from 'react-native'
|
||||
import { StyleSheet, Switch, Text, View } from 'react-native'
|
||||
import { Chase } from 'react-native-animated-spinkit'
|
||||
import { State, TapGestureHandler } from 'react-native-gesture-handler'
|
||||
|
||||
export interface Props {
|
||||
iconFront?: any
|
||||
@ -54,81 +55,86 @@ const MenuRow: React.FC<Props> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.base}
|
||||
onPress={onPress}
|
||||
disabled={loading}
|
||||
testID='base'
|
||||
>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
{iconFront && (
|
||||
<Icon
|
||||
name={iconFront}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconFrontColor]}
|
||||
style={styles.iconFront}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.main}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.primary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{(content && content.length) ||
|
||||
switchValue !== undefined ||
|
||||
iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content && content.length ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
{loading && !iconBack && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
<View style={styles.base}>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={({ nativeEvent }) => {
|
||||
if (nativeEvent.state === State.ACTIVE) {
|
||||
if (!loading) {
|
||||
onPress && onPress()
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={styles.core}>
|
||||
<View style={styles.front}>
|
||||
{iconFront && (
|
||||
<Icon
|
||||
name={iconFront}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconFrontColor]}
|
||||
style={styles.iconFront}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<>
|
||||
<Icon
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
<View style={styles.main}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.primary }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{description ? (
|
||||
<Text style={[styles.description, { color: theme.secondary }]}>
|
||||
{description}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{(content && content.length) ||
|
||||
switchValue !== undefined ||
|
||||
iconBack ? (
|
||||
<View style={styles.back}>
|
||||
{content && content.length ? (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
styles.content,
|
||||
{
|
||||
color: theme.secondary,
|
||||
opacity: !iconBack && loading ? 0 : 1
|
||||
}
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
{loading && !iconBack && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
{switchValue !== undefined ? (
|
||||
<Switch
|
||||
value={switchValue}
|
||||
onValueChange={switchOnValueChange}
|
||||
disabled={switchDisabled}
|
||||
trackColor={{ true: theme.blue, false: theme.disabled }}
|
||||
/>
|
||||
) : null}
|
||||
{iconBack ? (
|
||||
<>
|
||||
<Icon
|
||||
name={iconBack}
|
||||
size={StyleConstants.Font.Size.L}
|
||||
color={theme[iconBackColor]}
|
||||
style={[styles.iconBack, { opacity: loading ? 0 : 1 }]}
|
||||
/>
|
||||
{loading && loadingSpinkit}
|
||||
</>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</TapGestureHandler>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Dimensions, Platform, StyleSheet, View } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { TabView } from 'react-native-tab-view'
|
||||
import ViewPagerAdapter from 'react-native-tab-view-viewpager-adapter'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
const Stack = createNativeStackNavigator<
|
||||
@ -53,21 +54,6 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
[localActiveIndex]
|
||||
)
|
||||
|
||||
const screenComponent = useCallback(
|
||||
() => (
|
||||
<TabView
|
||||
lazy
|
||||
swipeEnabled
|
||||
renderScene={renderScene}
|
||||
renderTabBar={() => null}
|
||||
onIndexChange={index => setSegment(index)}
|
||||
navigationState={{ index: segment, routes }}
|
||||
initialLayout={{ width: Dimensions.get('window').width }}
|
||||
/>
|
||||
),
|
||||
[segment, localActiveIndex]
|
||||
)
|
||||
|
||||
const screenOptions = useMemo(() => {
|
||||
if (localActiveIndex === null) {
|
||||
if (name === 'Public') {
|
||||
@ -102,6 +88,8 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
}
|
||||
}, [localActiveIndex, mode, segment])
|
||||
|
||||
const renderPager = useCallback(props => <ViewPagerAdapter {...props} />, [])
|
||||
|
||||
return (
|
||||
<Stack.Navigator
|
||||
screenOptions={{ headerHideShadow: true, headerTopInsetEnabled: false }}
|
||||
@ -109,9 +97,21 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
<Stack.Screen
|
||||
// @ts-ignore
|
||||
name={`Screen-${name}-Root`}
|
||||
component={screenComponent}
|
||||
options={screenOptions}
|
||||
/>
|
||||
>
|
||||
{() => (
|
||||
<TabView
|
||||
lazy
|
||||
swipeEnabled
|
||||
renderPager={renderPager}
|
||||
renderScene={renderScene}
|
||||
renderTabBar={() => null}
|
||||
onIndexChange={index => setSegment(index)}
|
||||
navigationState={{ index: segment, routes }}
|
||||
initialLayout={{ width: Dimensions.get('window').width }}
|
||||
/>
|
||||
)}
|
||||
</Stack.Screen>
|
||||
|
||||
{sharedScreens(Stack)}
|
||||
</Stack.Navigator>
|
||||
|
@ -14,9 +14,7 @@ import { FlatList } from 'react-native-gesture-handler'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
|
||||
import { findIndex } from 'lodash'
|
||||
import CustomRefreshControl from '@components/CustomRefreshControl'
|
||||
import { InfiniteData, useQueryClient } from 'react-query'
|
||||
import { useTheme } from '@utils/styles/ThemeManager'
|
||||
|
||||
export interface Props {
|
||||
page: App.Pages
|
||||
@ -37,8 +35,6 @@ const Timeline: React.FC<Props> = ({
|
||||
disableRefresh = false,
|
||||
disableInfinity = false
|
||||
}) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const queryKeyParams = {
|
||||
page,
|
||||
...(hashtag && { hashtag }),
|
||||
@ -212,6 +208,7 @@ const Timeline: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
bounces={!disableRefresh}
|
||||
ref={flRef}
|
||||
windowSize={11}
|
||||
data={flattenData}
|
||||
|
@ -103,7 +103,8 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
|
||||
() =>
|
||||
navigation.navigate('Screen-Shared-Compose', {
|
||||
type: 'reply',
|
||||
incomingStatus: status
|
||||
incomingStatus: status,
|
||||
queryKey
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
@ -10,6 +10,32 @@ export interface Props {
|
||||
card: Mastodon.Card
|
||||
}
|
||||
|
||||
type CancelPromise = ((reason?: Error) => void) | undefined
|
||||
type ImageSize = { width: number; height: number }
|
||||
interface ImageSizeOperation {
|
||||
start: () => Promise<ImageSize>
|
||||
cancel: CancelPromise
|
||||
}
|
||||
const getImageSize = (uri: string): ImageSizeOperation => {
|
||||
let cancel: CancelPromise
|
||||
const start = (): Promise<ImageSize> =>
|
||||
new Promise<{ width: number; height: number }>((resolve, reject) => {
|
||||
cancel = reject
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width, height) => {
|
||||
cancel = undefined
|
||||
resolve({ width, height })
|
||||
},
|
||||
error => {
|
||||
reject(error)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return { start, cancel }
|
||||
}
|
||||
|
||||
const TimelineCard: React.FC<Props> = ({ card }) => {
|
||||
const { theme } = useTheme()
|
||||
|
||||
@ -19,6 +45,26 @@ const TimelineCard: React.FC<Props> = ({ card }) => {
|
||||
Image.getSize(card.image, () => setImageLoaded(true))
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
let cancel: CancelPromise
|
||||
const sideEffect = async (): Promise<void> => {
|
||||
try {
|
||||
const operation = getImageSize(card.image)
|
||||
cancel = operation.cancel
|
||||
await operation.start()
|
||||
} catch (error) {
|
||||
if (__DEV__) console.warn(error)
|
||||
}
|
||||
}
|
||||
if (card.image) {
|
||||
sideEffect()
|
||||
}
|
||||
return () => {
|
||||
if (cancel) {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
const cardVisual = useMemo(() => {
|
||||
if (imageLoaded) {
|
||||
return <Image source={{ uri: card.image }} style={styles.image} />
|
||||
|
@ -96,7 +96,8 @@ const HeaderActionsStatus: React.FC<Props> = ({
|
||||
if (res.id) {
|
||||
navigation.navigate('Screen-Shared-Compose', {
|
||||
type: 'edit',
|
||||
incomingStatus: res
|
||||
incomingStatus: res,
|
||||
queryKey
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user