mirror of
https://github.com/tooot-app/app
synced 2025-05-10 06:49:03 +02:00
A lot of updates
This commit is contained in:
parent
8200375c92
commit
735cc0b903
12
App.tsx
12
App.tsx
@ -15,10 +15,14 @@ setConsole({
|
|||||||
error: console.warn
|
error: console.warn
|
||||||
})
|
})
|
||||||
|
|
||||||
if (__DEV__) {
|
// if (__DEV__) {
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||||
// whyDidYouRender(React)
|
// whyDidYouRender(React, {
|
||||||
}
|
// trackAllPureComponents: true,
|
||||||
|
// trackHooks: true,
|
||||||
|
// hotReloadBufferMs: 1000
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -53,7 +53,6 @@
|
|||||||
"@types/react": "~16.9.35",
|
"@types/react": "~16.9.35",
|
||||||
"@types/react-dom": "^16.9.9",
|
"@types/react-dom": "^16.9.9",
|
||||||
"@types/react-native": "~0.63.2",
|
"@types/react-native": "~0.63.2",
|
||||||
"@types/react-native-htmlview": "^0.12.2",
|
|
||||||
"@types/react-navigation": "^3.4.0",
|
"@types/react-navigation": "^3.4.0",
|
||||||
"@types/react-redux": "^7.1.11",
|
"@types/react-redux": "^7.1.11",
|
||||||
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
||||||
@ -61,4 +60,4 @@
|
|||||||
"typescript": "~3.9.2"
|
"typescript": "~3.9.2"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
1
src/@types/untyped.d.ts
vendored
Normal file
1
src/@types/untyped.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'react-native-toast-message'
|
@ -4,10 +4,8 @@ import { NavigationContainer } from '@react-navigation/native'
|
|||||||
import { enableScreens } from 'react-native-screens'
|
import { enableScreens } from 'react-native-screens'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Feather } from '@expo/vector-icons'
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
|
||||||
import ScreenLocal from 'src/screens/Local'
|
import ScreenLocal from 'src/screens/Local'
|
||||||
import ScreenPublic from 'src/screens/Public'
|
import ScreenPublic from 'src/screens/Public'
|
||||||
@ -17,6 +15,7 @@ import ScreenMe from 'src/screens/Me'
|
|||||||
import { themes } from 'src/utils/styles/themes'
|
import { themes } from 'src/utils/styles/themes'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import getCurrentTab from 'src/utils/getCurrentTab'
|
import getCurrentTab from 'src/utils/getCurrentTab'
|
||||||
|
import { toastConfig } from 'src/components/toast'
|
||||||
|
|
||||||
enableScreens()
|
enableScreens()
|
||||||
const Tab = createBottomTabNavigator<RootStackParamList>()
|
const Tab = createBottomTabNavigator<RootStackParamList>()
|
||||||
@ -89,7 +88,7 @@ export const Index: React.FC = () => {
|
|||||||
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|
||||||
<Toast ref={(ref: any) => Toast.setRef(ref)} />
|
<Toast ref={(ref: any) => Toast.setRef(ref)} config={toastConfig} />
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,8 @@ const client = async ({
|
|||||||
// } catch (error) {
|
// } catch (error) {
|
||||||
// return Promise.reject('ky error: ' + error.json())
|
// return Promise.reject('ky error: ' + error.json())
|
||||||
// }
|
// }
|
||||||
console.log('upload done')
|
console.log('Query: /' + endpoint)
|
||||||
if (response?.ok) {
|
if (response.ok) {
|
||||||
console.log('returning ok')
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
headers: response.headers,
|
headers: response.headers,
|
||||||
body: await response.json()
|
body: await response.json()
|
||||||
|
128
src/components/BottomSheet.tsx
Normal file
128
src/components/BottomSheet.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
import {
|
||||||
|
Animated,
|
||||||
|
Dimensions,
|
||||||
|
Modal,
|
||||||
|
PanResponder,
|
||||||
|
Pressable,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
|
|
||||||
|
import constants from 'src/utils/styles/constants'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
children: React.ReactNode
|
||||||
|
visible: boolean
|
||||||
|
handleDismiss: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}, [visible])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal animated animationType='fade' visible={visible} transparent>
|
||||||
|
<View
|
||||||
|
style={[styles.overlay, { backgroundColor: theme.border }]}
|
||||||
|
{...panResponder.panHandlers}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.container,
|
||||||
|
{
|
||||||
|
top,
|
||||||
|
backgroundColor: theme.background,
|
||||||
|
paddingBottom: insets.bottom
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[styles.handle, { backgroundColor: theme.background }]}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
<Pressable
|
||||||
|
onPress={() => closeModal.start(() => handleDismiss())}
|
||||||
|
style={[styles.cancel, { borderColor: theme.primary }]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.text, { color: theme.primary }]}>取消</Text>
|
||||||
|
</Pressable>
|
||||||
|
</Animated.View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
overlay: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'flex-end'
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: constants.SPACING_L,
|
||||||
|
paddingTop: constants.SPACING_M
|
||||||
|
},
|
||||||
|
handle: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
width: constants.GLOBAL_SPACING_BASE * 8,
|
||||||
|
height: constants.GLOBAL_SPACING_BASE / 2,
|
||||||
|
borderRadius: 100,
|
||||||
|
top: -constants.SPACING_M * 2
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
padding: constants.SPACING_S,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 100
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: constants.FONT_SIZE_L,
|
||||||
|
textAlign: 'center'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default BottomSheet
|
37
src/components/BottomSheet/Row.tsx
Normal file
37
src/components/BottomSheet/Row.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { Pressable, StyleSheet, Text } from 'react-native'
|
||||||
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
|
||||||
|
import constants from 'src/utils/styles/constants'
|
||||||
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
onPressFunction: () => void
|
||||||
|
icon: string
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const BottomSheetRow: React.FC<Props> = ({ onPressFunction, icon, text }) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable onPress={() => onPressFunction()} style={styles.pressable}>
|
||||||
|
<Feather name={icon} color={theme.primary} size={constants.FONT_SIZE_L} />
|
||||||
|
<Text style={[styles.text, { color: theme.primary }]}>{text}</Text>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
pressable: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginBottom: constants.SPACING_L
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: constants.FONT_SIZE_M,
|
||||||
|
lineHeight: constants.FONT_SIZE_L,
|
||||||
|
marginLeft: constants.SPACING_S
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default BottomSheetRow
|
@ -28,7 +28,7 @@ const Core: React.FC<Props> = ({ text, destructive = false }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.core}>
|
<View style={styles.core}>
|
||||||
<Text style={{ color: destructive ? theme.dangerous : theme.primary }}>
|
<Text style={{ color: destructive ? theme.error : theme.primary }}>
|
||||||
{text}
|
{text}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Text } from 'react-native'
|
import { Text } from 'react-native'
|
||||||
import HTMLView, { HTMLViewNode } from 'react-native-htmlview'
|
import HTMLView, { HTMLViewNode } from 'react-native-htmlview'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
@ -102,22 +102,30 @@ const ParseContent: React.FC<Props> = ({
|
|||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
|
const renderNodeCallback = useCallback(
|
||||||
|
(node, index) =>
|
||||||
|
renderNode({ theme, node, index, navigation, mentions, showFullLink }),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const textComponent = useCallback(
|
||||||
|
({ children }) =>
|
||||||
|
emojis && children ? (
|
||||||
|
<Emojis content={children.toString()} emojis={emojis} size={size} />
|
||||||
|
) : (
|
||||||
|
<Text>{children}</Text>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const rootComponent = useCallback(({ children }) => {
|
||||||
|
return <Text numberOfLines={linesTruncated}>{children}</Text>
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HTMLView
|
<HTMLView
|
||||||
value={content}
|
value={content}
|
||||||
renderNode={(node, index) =>
|
TextComponent={textComponent}
|
||||||
renderNode({ theme, node, index, navigation, mentions, showFullLink })
|
RootComponent={rootComponent}
|
||||||
}
|
renderNode={renderNodeCallback}
|
||||||
TextComponent={({ children }) =>
|
|
||||||
emojis && children ? (
|
|
||||||
<Emojis content={children.toString()} emojis={emojis} size={size} />
|
|
||||||
) : (
|
|
||||||
<Text>{children}</Text>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RootComponent={({ children }) => {
|
|
||||||
return <Text numberOfLines={linesTruncated}>{children}</Text>
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { Dimensions, FlatList, Text, View } from 'react-native'
|
import { Dimensions, FlatList, StyleSheet, Text, View } from 'react-native'
|
||||||
import SegmentedControl from '@react-native-community/segmented-control'
|
import SegmentedControl from '@react-native-community/segmented-control'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
@ -59,6 +59,52 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
|
|
||||||
const horizontalPaging = useRef<FlatList>(null!)
|
const horizontalPaging = useRef<FlatList>(null!)
|
||||||
|
|
||||||
|
const onChangeSegment = useCallback(({ nativeEvent }) => {
|
||||||
|
setSegmentManuallyTriggered(true)
|
||||||
|
setSegment(nativeEvent.selectedSegmentIndex)
|
||||||
|
horizontalPaging.current.scrollToIndex({
|
||||||
|
index: nativeEvent.selectedSegmentIndex
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const onPressSearch = useCallback(() => {
|
||||||
|
navigation.navigate(getCurrentTab(navigation), {
|
||||||
|
screen: 'Screen-Shared-Search'
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const flGetItemLayout = useCallback(
|
||||||
|
(data, index) => ({
|
||||||
|
length: Dimensions.get('window').width,
|
||||||
|
offset: Dimensions.get('window').width * index,
|
||||||
|
index
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const flKeyExtrator = useCallback(({ page }) => page, [])
|
||||||
|
const flRenderItem = useCallback(
|
||||||
|
({ item, index }) => {
|
||||||
|
if (!localRegistered && index === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return <Page item={item} localRegistered={localRegistered} />
|
||||||
|
},
|
||||||
|
[localRegistered]
|
||||||
|
)
|
||||||
|
const flOnMomentumScrollEnd = useCallback(
|
||||||
|
() => setSegmentManuallyTriggered(false),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const flOnScroll = useCallback(
|
||||||
|
({ nativeEvent }) =>
|
||||||
|
!segmentManuallyTriggered &&
|
||||||
|
setSegment(
|
||||||
|
nativeEvent.contentOffset.x <= Dimensions.get('window').width / 2
|
||||||
|
? 0
|
||||||
|
: 1
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack.Navigator>
|
<Stack.Navigator>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -71,13 +117,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
values={[content[0].title, content[1].title]}
|
values={[content[0].title, content[1].title]}
|
||||||
selectedIndex={segment}
|
selectedIndex={segment}
|
||||||
onChange={({ nativeEvent }) => {
|
onChange={onChangeSegment}
|
||||||
setSegmentManuallyTriggered(true)
|
|
||||||
setSegment(nativeEvent.selectedSegmentIndex)
|
|
||||||
horizontalPaging.current.scrollToIndex({
|
|
||||||
index: nativeEvent.selectedSegmentIndex
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
style={{ width: 150, height: 30 }}
|
style={{ width: 150, height: 30 }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -86,11 +126,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
name='search'
|
name='search'
|
||||||
size={24}
|
size={24}
|
||||||
color={theme.secondary}
|
color={theme.secondary}
|
||||||
onPress={() => {
|
onPress={onPressSearch}
|
||||||
navigation.navigate(getCurrentTab(navigation), {
|
|
||||||
screen: 'Screen-Shared-Search'
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -99,42 +135,19 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
{() => {
|
{() => {
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
style={{ width: Dimensions.get('window').width, height: '100%' }}
|
|
||||||
data={content}
|
|
||||||
extraData={localRegistered}
|
|
||||||
keyExtractor={({ page }) => page}
|
|
||||||
renderItem={({ item, index }) => {
|
|
||||||
if (!localRegistered && index === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
key={index}
|
|
||||||
item={item}
|
|
||||||
localRegistered={localRegistered}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
ref={horizontalPaging}
|
|
||||||
bounces={false}
|
|
||||||
getItemLayout={(data, index) => ({
|
|
||||||
length: Dimensions.get('window').width,
|
|
||||||
offset: Dimensions.get('window').width * index,
|
|
||||||
index
|
|
||||||
})}
|
|
||||||
horizontal
|
horizontal
|
||||||
onMomentumScrollEnd={() => setSegmentManuallyTriggered(false)}
|
|
||||||
onScroll={({ nativeEvent }) =>
|
|
||||||
!segmentManuallyTriggered &&
|
|
||||||
setSegment(
|
|
||||||
nativeEvent.contentOffset.x <=
|
|
||||||
Dimensions.get('window').width / 2
|
|
||||||
? 0
|
|
||||||
: 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
pagingEnabled
|
pagingEnabled
|
||||||
|
data={content}
|
||||||
|
bounces={false}
|
||||||
|
onScroll={flOnScroll}
|
||||||
|
ref={horizontalPaging}
|
||||||
|
style={styles.flatList}
|
||||||
|
renderItem={flRenderItem}
|
||||||
|
extraData={localRegistered}
|
||||||
|
keyExtractor={flKeyExtrator}
|
||||||
|
getItemLayout={flGetItemLayout}
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
|
onMomentumScrollEnd={flOnMomentumScrollEnd}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@ -145,4 +158,11 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Timelines
|
const styles = StyleSheet.create({
|
||||||
|
flatList: {
|
||||||
|
width: Dimensions.get('window').width,
|
||||||
|
height: '100%'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default React.memo(Timelines, () => true)
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { ActivityIndicator, AppState, FlatList, Text, View } from 'react-native'
|
import {
|
||||||
|
ActivityIndicator,
|
||||||
|
AppState,
|
||||||
|
FlatList,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
View
|
||||||
|
} from 'react-native'
|
||||||
import { setFocusHandler, useInfiniteQuery } from 'react-query'
|
import { setFocusHandler, useInfiniteQuery } from 'react-query'
|
||||||
|
|
||||||
import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications'
|
import TimelineNotifications from 'src/components/Timelines/Timeline/Notifications'
|
||||||
@ -51,6 +58,40 @@ const Timeline: React.FC<Props> = ({
|
|||||||
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
|
const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
|
||||||
// const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
|
// const flattenPointer = data ? data.flatMap(d => [d?.pointer]) : []
|
||||||
|
|
||||||
|
const flKeyExtrator = useCallback(({ id }) => id, [])
|
||||||
|
const flRenderItem = useCallback(({ item }) => {
|
||||||
|
switch (page) {
|
||||||
|
case 'Conversations':
|
||||||
|
return <TimelineConversation item={item} />
|
||||||
|
case 'Notifications':
|
||||||
|
return <TimelineNotifications notification={item} queryKey={queryKey} />
|
||||||
|
default:
|
||||||
|
return <TimelineDefault item={item} queryKey={queryKey} />
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
const flItemSeparatorComponent = useCallback(() => <TimelineSeparator />, [])
|
||||||
|
const flOnRefresh = useCallback(
|
||||||
|
() =>
|
||||||
|
!disableRefresh &&
|
||||||
|
fetchMore(
|
||||||
|
{
|
||||||
|
direction: 'prev',
|
||||||
|
id: flattenData[0].id
|
||||||
|
},
|
||||||
|
{ previous: true }
|
||||||
|
),
|
||||||
|
[disableRefresh]
|
||||||
|
)
|
||||||
|
const flOnEndReach = useCallback(
|
||||||
|
() =>
|
||||||
|
!disableRefresh &&
|
||||||
|
fetchMore({
|
||||||
|
direction: 'next',
|
||||||
|
id: flattenData[flattenData.length - 1].id
|
||||||
|
}),
|
||||||
|
[disableRefresh]
|
||||||
|
)
|
||||||
|
|
||||||
let content
|
let content
|
||||||
if (!isSuccess) {
|
if (!isSuccess) {
|
||||||
content = <ActivityIndicator />
|
content = <ActivityIndicator />
|
||||||
@ -60,53 +101,18 @@ const Timeline: React.FC<Props> = ({
|
|||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
<FlatList
|
<FlatList
|
||||||
style={{ minHeight: '100%' }}
|
|
||||||
scrollEnabled={scrollEnabled} // For timeline in Account view
|
|
||||||
data={flattenData}
|
data={flattenData}
|
||||||
keyExtractor={({ id }) => id}
|
onRefresh={flOnRefresh}
|
||||||
renderItem={({ item, index, separators }) => {
|
renderItem={flRenderItem}
|
||||||
switch (page) {
|
onEndReached={flOnEndReach}
|
||||||
case 'Conversations':
|
keyExtractor={flKeyExtrator}
|
||||||
return <TimelineConversation key={index} item={item} />
|
style={styles.flatList}
|
||||||
case 'Notifications':
|
scrollEnabled={scrollEnabled} // For timeline in Account view
|
||||||
return (
|
ItemSeparatorComponent={flItemSeparatorComponent}
|
||||||
<TimelineNotifications
|
refreshing={!disableRefresh && isLoading}
|
||||||
key={index}
|
onEndReachedThreshold={!disableRefresh ? 0.5 : null}
|
||||||
notification={item}
|
|
||||||
queryKey={queryKey}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
<TimelineDefault
|
|
||||||
key={index}
|
|
||||||
item={item}
|
|
||||||
queryKey={queryKey}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ItemSeparatorComponent={() => <TimelineSeparator />}
|
|
||||||
// require getItemLayout
|
// require getItemLayout
|
||||||
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
|
// {...(flattenPointer[0] && { initialScrollIndex: flattenPointer[0] })}
|
||||||
{...(!disableRefresh && {
|
|
||||||
onRefresh: () =>
|
|
||||||
fetchMore(
|
|
||||||
{
|
|
||||||
direction: 'prev',
|
|
||||||
id: flattenData[0].id
|
|
||||||
},
|
|
||||||
{ previous: true }
|
|
||||||
),
|
|
||||||
refreshing: isLoading,
|
|
||||||
onEndReached: () => {
|
|
||||||
fetchMore({
|
|
||||||
direction: 'next',
|
|
||||||
id: flattenData[flattenData.length - 1].id
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onEndReachedThreshold: 0.5
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
{isFetchingMore && <ActivityIndicator />}
|
{isFetchingMore && <ActivityIndicator />}
|
||||||
</>
|
</>
|
||||||
@ -116,4 +122,10 @@ const Timeline: React.FC<Props> = ({
|
|||||||
return <View>{content}</View>
|
return <View>{content}</View>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
flatList: {
|
||||||
|
minHeight: '100%'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default Timeline
|
export default Timeline
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
import { Dimensions, Pressable, StyleSheet, View } from 'react-native'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
|
||||||
@ -24,6 +24,43 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
|||||||
|
|
||||||
let actualStatus = item.reblog ? item.reblog : item
|
let actualStatus = item.reblog ? item.reblog : item
|
||||||
|
|
||||||
|
const pressableToot = useCallback(
|
||||||
|
() =>
|
||||||
|
navigation.navigate('Screen-Shared-Toot', {
|
||||||
|
toot: actualStatus.id
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
const childrenToot = useMemo(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
{actualStatus.content ? (
|
||||||
|
<Content
|
||||||
|
content={actualStatus.content}
|
||||||
|
emojis={actualStatus.emojis}
|
||||||
|
mentions={actualStatus.mentions}
|
||||||
|
spoiler_text={actualStatus.spoiler_text}
|
||||||
|
// tags={actualStatus.tags}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
{actualStatus.poll && <Poll poll={actualStatus.poll} />}
|
||||||
|
{actualStatus.media_attachments.length > 0 && (
|
||||||
|
<Attachment
|
||||||
|
media_attachments={actualStatus.media_attachments}
|
||||||
|
sensitive={actualStatus.sensitive}
|
||||||
|
width={
|
||||||
|
Dimensions.get('window').width - constants.SPACING_M * 2 - 50 - 8
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{actualStatus.card && <Card card={actualStatus.card} />}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
const statusView = useMemo(() => {
|
const statusView = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.statusView}>
|
<View style={styles.statusView}>
|
||||||
@ -54,40 +91,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
|||||||
application={item.application}
|
application={item.application}
|
||||||
/>
|
/>
|
||||||
{/* Can pass toot info to next page to speed up performance */}
|
{/* Can pass toot info to next page to speed up performance */}
|
||||||
<Pressable
|
<Pressable onPress={pressableToot} children={childrenToot} />
|
||||||
onPress={() =>
|
|
||||||
navigation.navigate('Screen-Shared-Toot', {
|
|
||||||
toot: actualStatus.id
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{actualStatus.content ? (
|
|
||||||
<Content
|
|
||||||
content={actualStatus.content}
|
|
||||||
emojis={actualStatus.emojis}
|
|
||||||
mentions={actualStatus.mentions}
|
|
||||||
spoiler_text={actualStatus.spoiler_text}
|
|
||||||
// tags={actualStatus.tags}
|
|
||||||
// style={{ flex: 1 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
{actualStatus.poll && <Poll poll={actualStatus.poll} />}
|
|
||||||
{actualStatus.media_attachments.length > 0 && (
|
|
||||||
<Attachment
|
|
||||||
media_attachments={actualStatus.media_attachments}
|
|
||||||
sensitive={actualStatus.sensitive}
|
|
||||||
width={
|
|
||||||
Dimensions.get('window').width -
|
|
||||||
constants.SPACING_M * 2 -
|
|
||||||
50 -
|
|
||||||
8
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{actualStatus.card && <Card card={actualStatus.card} />}
|
|
||||||
</Pressable>
|
|
||||||
<ActionsStatus queryKey={queryKey} status={actualStatus} />
|
<ActionsStatus queryKey={queryKey} status={actualStatus} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -114,4 +118,12 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default TimelineDefault
|
export default React.memo(TimelineDefault, (prev, next) => {
|
||||||
|
let skipUpdate = true
|
||||||
|
skipUpdate = prev.item.id === next.item.id
|
||||||
|
skipUpdate = prev.item.replies_count === next.item.replies_count
|
||||||
|
skipUpdate = prev.item.favourited === next.item.favourited
|
||||||
|
skipUpdate = prev.item.reblogged === next.item.reblogged
|
||||||
|
skipUpdate = prev.item.bookmarked === next.item.bookmarked
|
||||||
|
return skipUpdate
|
||||||
|
})
|
||||||
|
@ -108,4 +108,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Actioned
|
export default React.memo(Actioned)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
ActionSheetIOS,
|
ActionSheetIOS,
|
||||||
Clipboard,
|
Clipboard,
|
||||||
@ -11,12 +11,14 @@ import {
|
|||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
import { useMutation, useQueryCache } from 'react-query'
|
import { useMutation, useQueryCache } from 'react-query'
|
||||||
import { Feather } from '@expo/vector-icons'
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
import { findIndex } from 'lodash'
|
||||||
|
|
||||||
import client from 'src/api/client'
|
import client from 'src/api/client'
|
||||||
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
|
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
|
||||||
import { store } from 'src/store'
|
import { store } from 'src/store'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import constants from 'src/utils/styles/constants'
|
import constants from 'src/utils/styles/constants'
|
||||||
|
import { toast } from 'src/components/toast'
|
||||||
|
|
||||||
const fireMutation = async ({
|
const fireMutation = async ({
|
||||||
id,
|
id,
|
||||||
@ -25,14 +27,7 @@ const fireMutation = async ({
|
|||||||
prevState
|
prevState
|
||||||
}: {
|
}: {
|
||||||
id: string
|
id: string
|
||||||
type:
|
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin' | 'delete'
|
||||||
| 'favourite'
|
|
||||||
| 'reblog'
|
|
||||||
| 'bookmark'
|
|
||||||
| 'mute'
|
|
||||||
| 'pin'
|
|
||||||
| 'delete'
|
|
||||||
| 'account/mute'
|
|
||||||
stateKey:
|
stateKey:
|
||||||
| 'favourited'
|
| 'favourited'
|
||||||
| 'reblogged'
|
| 'reblogged'
|
||||||
@ -53,27 +48,13 @@ const fireMutation = async ({
|
|||||||
method: 'post',
|
method: 'post',
|
||||||
instance: 'local',
|
instance: 'local',
|
||||||
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
|
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
|
||||||
})
|
}) // bug in response from Mastodon
|
||||||
|
|
||||||
if (!res.body[stateKey] === prevState) {
|
if (!res.body[stateKey] === prevState) {
|
||||||
if (type === 'bookmark' || 'mute' || 'pin')
|
toast({ type: 'success', content: '功能成功' })
|
||||||
Toast.show({
|
|
||||||
type: 'success',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '功能成功',
|
|
||||||
visibilityTime: 2000,
|
|
||||||
autoHide: true,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
return Promise.resolve(res.body)
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
toast({ type: 'error', content: '功能错误' })
|
||||||
type: 'error',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '请重试',
|
|
||||||
autoHide: false,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -85,23 +66,10 @@ const fireMutation = async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (res.body[stateKey] === id) {
|
if (res.body[stateKey] === id) {
|
||||||
Toast.show({
|
toast({ type: 'success', content: '删除成功' })
|
||||||
type: 'success',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '删除成功',
|
|
||||||
visibilityTime: 2000,
|
|
||||||
autoHide: true,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.resolve(res.body)
|
return Promise.resolve(res.body)
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
toast({ type: 'error', content: '删除失败' })
|
||||||
type: 'error',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '请重试',
|
|
||||||
autoHide: false,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -120,136 +88,204 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
state ? theme.primary : theme.secondary
|
state ? theme.primary : theme.secondary
|
||||||
|
|
||||||
const localAccountId = getLocalAccountId(store.getState())
|
const localAccountId = getLocalAccountId(store.getState())
|
||||||
const [modalVisible, setModalVisible] = useState(false)
|
const [bottomSheetVisible, setBottomSheetVisible] = useState(false)
|
||||||
|
|
||||||
const queryCache = useQueryCache()
|
const queryCache = useQueryCache()
|
||||||
const [mutateAction] = useMutation(fireMutation, {
|
const [mutateAction] = useMutation(fireMutation, {
|
||||||
onMutate: () => {
|
onMutate: ({ id, type, stateKey, prevState }) => {
|
||||||
queryCache.cancelQueries(queryKey)
|
queryCache.cancelQueries(queryKey)
|
||||||
const prevData = queryCache.getQueryData(queryKey)
|
const oldData = queryCache.getQueryData(queryKey)
|
||||||
return prevData
|
|
||||||
},
|
switch (type) {
|
||||||
onSuccess: (newData, params) => {
|
case 'favourite':
|
||||||
if (params.type === 'reblog') {
|
case 'reblog':
|
||||||
queryCache.invalidateQueries(['Following', { page: 'Following' }])
|
case 'bookmark':
|
||||||
|
case 'mute':
|
||||||
|
case 'pin':
|
||||||
|
queryCache.setQueryData(queryKey, old =>
|
||||||
|
(old as {}[]).map((paging: any) => ({
|
||||||
|
toots: paging.toots.map((toot: any) => {
|
||||||
|
if (toot.id === id) {
|
||||||
|
toot[stateKey] =
|
||||||
|
typeof prevState === 'boolean' ? !prevState : true
|
||||||
|
}
|
||||||
|
return toot
|
||||||
|
}),
|
||||||
|
pointer: paging.pointer
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
queryCache.setQueryData(queryKey, old =>
|
||||||
|
(old as {}[]).map((paging: any) => ({
|
||||||
|
toots: paging.toots.map((toot: any, index: number) => {
|
||||||
|
if (toot.id === id) {
|
||||||
|
paging.toots.splice(index, 1)
|
||||||
|
}
|
||||||
|
return toot
|
||||||
|
}),
|
||||||
|
pointer: paging.pointer
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
// queryCache.setQueryData(queryKey, (oldData: any) => {
|
|
||||||
// oldData &&
|
return oldData
|
||||||
// oldData.map((paging: any) => {
|
|
||||||
// paging.toots.map(
|
|
||||||
// (status: Mastodon.Status | Mastodon.Notification, i: number) => {
|
|
||||||
// if (status.id === newData.id) {
|
|
||||||
// paging.toots[i] = newData
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// return oldData
|
|
||||||
// })
|
|
||||||
return Promise.resolve()
|
|
||||||
},
|
},
|
||||||
onError: (err, variables, prevData) => {
|
onError: (err, _, oldData) => {
|
||||||
queryCache.setQueryData(queryKey, prevData)
|
toast({ type: 'error', content: '请重试' })
|
||||||
},
|
queryCache.setQueryData(queryKey, oldData)
|
||||||
onSettled: () => {
|
|
||||||
queryCache.invalidateQueries(queryKey)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onPressReply = useCallback(() => {}, [])
|
||||||
|
const onPressReblog = useCallback(
|
||||||
|
() =>
|
||||||
|
mutateAction({
|
||||||
|
id: status.id,
|
||||||
|
type: 'reblog',
|
||||||
|
stateKey: 'reblogged',
|
||||||
|
prevState: status.reblogged
|
||||||
|
}),
|
||||||
|
[status.reblogged]
|
||||||
|
)
|
||||||
|
const onPressFavourite = useCallback(
|
||||||
|
() =>
|
||||||
|
mutateAction({
|
||||||
|
id: status.id,
|
||||||
|
type: 'favourite',
|
||||||
|
stateKey: 'favourited',
|
||||||
|
prevState: status.favourited
|
||||||
|
}),
|
||||||
|
[status.favourited]
|
||||||
|
)
|
||||||
|
const onPressBookmark = useCallback(
|
||||||
|
() =>
|
||||||
|
mutateAction({
|
||||||
|
id: status.id,
|
||||||
|
type: 'bookmark',
|
||||||
|
stateKey: 'bookmarked',
|
||||||
|
prevState: status.bookmarked
|
||||||
|
}),
|
||||||
|
[status.bookmarked]
|
||||||
|
)
|
||||||
|
const onPressShare = useCallback(() => setBottomSheetVisible(true), [])
|
||||||
|
|
||||||
|
const childrenReply = useMemo(
|
||||||
|
() => (
|
||||||
|
<>
|
||||||
|
<Feather
|
||||||
|
name='message-circle'
|
||||||
|
color={iconColor}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
{status.replies_count > 0 && (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: theme.secondary,
|
||||||
|
fontSize: constants.FONT_SIZE_M,
|
||||||
|
marginLeft: constants.SPACING_XS
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{status.replies_count}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[status.replies_count]
|
||||||
|
)
|
||||||
|
const childrenReblog = useMemo(
|
||||||
|
() => (
|
||||||
|
<Feather
|
||||||
|
name='repeat'
|
||||||
|
color={
|
||||||
|
status.visibility === 'public' || status.visibility === 'unlisted'
|
||||||
|
? iconColorAction(status.reblogged)
|
||||||
|
: theme.disabled
|
||||||
|
}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status.reblogged]
|
||||||
|
)
|
||||||
|
const childrenFavourite = useMemo(
|
||||||
|
() => (
|
||||||
|
<Feather
|
||||||
|
name='heart'
|
||||||
|
color={iconColorAction(status.favourited)}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status.favourited]
|
||||||
|
)
|
||||||
|
const childrenBookmark = useMemo(
|
||||||
|
() => (
|
||||||
|
<Feather
|
||||||
|
name='bookmark'
|
||||||
|
color={iconColorAction(status.bookmarked)}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[status.bookmarked]
|
||||||
|
)
|
||||||
|
const childrenShare = useMemo(
|
||||||
|
() => (
|
||||||
|
<Feather
|
||||||
|
name='share-2'
|
||||||
|
color={iconColor}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
<Pressable style={styles.action}>
|
<Pressable
|
||||||
<Feather
|
style={styles.action}
|
||||||
name='message-circle'
|
onPress={onPressReply}
|
||||||
color={iconColor}
|
children={childrenReply}
|
||||||
size={constants.FONT_SIZE_M + 2}
|
/>
|
||||||
/>
|
|
||||||
{status.replies_count > 0 && (
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
color: theme.secondary,
|
|
||||||
fontSize: constants.FONT_SIZE_M,
|
|
||||||
marginLeft: constants.SPACING_XS
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{status.replies_count}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={() =>
|
onPress={
|
||||||
mutateAction({
|
status.visibility === 'public' || status.visibility === 'unlisted'
|
||||||
id: status.id,
|
? onPressReblog
|
||||||
type: 'reblog',
|
: null
|
||||||
stateKey: 'reblogged',
|
|
||||||
prevState: status.reblogged
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
>
|
children={childrenReblog}
|
||||||
<Feather
|
/>
|
||||||
name='repeat'
|
|
||||||
color={iconColorAction(status.reblogged)}
|
|
||||||
size={constants.FONT_SIZE_M + 2}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={() =>
|
onPress={onPressFavourite}
|
||||||
mutateAction({
|
children={childrenFavourite}
|
||||||
id: status.id,
|
/>
|
||||||
type: 'favourite',
|
|
||||||
stateKey: 'favourited',
|
|
||||||
prevState: status.favourited
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Feather
|
|
||||||
name='heart'
|
|
||||||
color={iconColorAction(status.favourited)}
|
|
||||||
size={constants.FONT_SIZE_M + 2}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={() =>
|
onPress={onPressBookmark}
|
||||||
mutateAction({
|
children={childrenBookmark}
|
||||||
id: status.id,
|
/>
|
||||||
type: 'bookmark',
|
|
||||||
stateKey: 'bookmarked',
|
|
||||||
prevState: status.bookmarked
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Feather
|
|
||||||
name='bookmark'
|
|
||||||
color={iconColorAction(status.bookmarked)}
|
|
||||||
size={constants.FONT_SIZE_M + 2}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Pressable style={styles.action} onPress={() => setModalVisible(true)}>
|
<Pressable
|
||||||
<Feather
|
style={styles.action}
|
||||||
name='share-2'
|
onPress={onPressShare}
|
||||||
color={iconColor}
|
children={childrenShare}
|
||||||
size={constants.FONT_SIZE_M + 2}
|
/>
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
animationType='fade'
|
animationType='fade'
|
||||||
presentationStyle='overFullScreen'
|
presentationStyle='overFullScreen'
|
||||||
transparent
|
transparent
|
||||||
visible={modalVisible}
|
visible={bottomSheetVisible}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.modalBackground}
|
style={styles.modalBackground}
|
||||||
onPress={() => setModalVisible(false)}
|
onPress={() => setBottomSheetVisible(false)}
|
||||||
>
|
>
|
||||||
<View style={styles.modalSheet}>
|
<View style={styles.modalSheet}>
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -266,15 +302,8 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
},
|
},
|
||||||
() => {},
|
() => {},
|
||||||
() => {
|
() => {
|
||||||
setModalVisible(false)
|
setBottomSheetVisible(false)
|
||||||
Toast.show({
|
toast({ type: 'success', content: '分享成功' })
|
||||||
type: 'success',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '分享成功',
|
|
||||||
visibilityTime: 2000,
|
|
||||||
autoHide: true,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -284,15 +313,8 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
Clipboard.setString(status.uri)
|
Clipboard.setString(status.uri)
|
||||||
setModalVisible(false)
|
setBottomSheetVisible(false)
|
||||||
Toast.show({
|
toast({ type: 'success', content: '链接复制成功' })
|
||||||
type: 'success',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '链接复制成功',
|
|
||||||
visibilityTime: 2000,
|
|
||||||
autoHide: true,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text>复制链接</Text>
|
<Text>复制链接</Text>
|
||||||
@ -300,7 +322,7 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
{status.account.id === localAccountId && (
|
{status.account.id === localAccountId && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModalVisible(false)
|
setBottomSheetVisible(false)
|
||||||
mutateAction({
|
mutateAction({
|
||||||
id: status.id,
|
id: status.id,
|
||||||
type: 'delete',
|
type: 'delete',
|
||||||
@ -314,7 +336,7 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
<Text>(删除并重发)</Text>
|
<Text>(删除并重发)</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModalVisible(false)
|
setBottomSheetVisible(false)
|
||||||
mutateAction({
|
mutateAction({
|
||||||
id: status.id,
|
id: status.id,
|
||||||
type: 'mute',
|
type: 'mute',
|
||||||
@ -325,10 +347,11 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
|||||||
>
|
>
|
||||||
<Text>{status.muted ? '取消静音' : '静音'}</Text>
|
<Text>{status.muted ? '取消静音' : '静音'}</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{/* Also note that reblogs cannot be pinned. */}
|
||||||
{status.account.id === localAccountId && (
|
{status.account.id === localAccountId && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setModalVisible(false)
|
setBottomSheetVisible(false)
|
||||||
mutateAction({
|
mutateAction({
|
||||||
id: status.id,
|
id: status.id,
|
||||||
type: 'pin',
|
type: 'pin',
|
||||||
|
@ -84,4 +84,4 @@ const Attachment: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Attachment
|
export default React.memo(Attachment, () => true)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Image, Pressable, StyleSheet } from 'react-native'
|
import { Image, Pressable, StyleSheet } from 'react-native'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
|
||||||
@ -12,15 +12,14 @@ export interface Props {
|
|||||||
const Avatar: React.FC<Props> = ({ uri, id }) => {
|
const Avatar: React.FC<Props> = ({ uri, id }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
// Need to fix go back root
|
// Need to fix go back root
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
navigation.navigate('Screen-Shared-Account', {
|
||||||
|
id: id
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable style={styles.avatar} onPress={onPress}>
|
||||||
style={styles.avatar}
|
|
||||||
onPress={() => {
|
|
||||||
navigation.navigate('Screen-Shared-Account', {
|
|
||||||
id: id
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image source={{ uri: uri }} style={styles.image} />
|
<Image source={{ uri: uri }} style={styles.image} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@ -39,4 +38,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Avatar
|
export default React.memo(Avatar, () => true)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
|
import { Image, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
|
|
||||||
@ -8,32 +8,29 @@ export interface Props {
|
|||||||
|
|
||||||
const Card: React.FC<Props> = ({ card }) => {
|
const Card: React.FC<Props> = ({ card }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
const onPress = useCallback(() => {
|
||||||
|
navigation.navigate('Screen-Shared-Webview', {
|
||||||
|
uri: card.url
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
card && (
|
<Pressable style={styles.card} onPress={onPress}>
|
||||||
<Pressable
|
{card.image && (
|
||||||
style={styles.card}
|
<View style={styles.left}>
|
||||||
onPress={() => {
|
<Image source={{ uri: card.image }} style={styles.image} />
|
||||||
navigation.navigate('Webview', {
|
|
||||||
uri: card.url
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{card.image && (
|
|
||||||
<View style={styles.left}>
|
|
||||||
<Image source={{ uri: card.image }} style={styles.image} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={styles.right}>
|
|
||||||
<Text numberOfLines={1}>{card.title}</Text>
|
|
||||||
{card.description ? (
|
|
||||||
<Text numberOfLines={2}>{card.description}</Text>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<Text numberOfLines={1}>{card.url}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
)}
|
||||||
)
|
<View style={styles.right}>
|
||||||
|
<Text numberOfLines={1}>{card.title}</Text>
|
||||||
|
{card.description ? (
|
||||||
|
<Text numberOfLines={2}>{card.description}</Text>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<Text numberOfLines={1}>{card.url}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,4 +53,4 @@ const styles = StyleSheet.create({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default Card
|
export default React.memo(Card, () => true)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native'
|
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { Feather } from '@expo/vector-icons'
|
import { Feather } from '@expo/vector-icons'
|
||||||
import Toast from 'react-native-toast-message'
|
import Toast from 'react-native-toast-message'
|
||||||
@ -12,6 +12,9 @@ import { getLocalAccountId, getLocalUrl } from 'src/utils/slices/instancesSlice'
|
|||||||
import { store } from 'src/store'
|
import { store } from 'src/store'
|
||||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
import constants from 'src/utils/styles/constants'
|
import constants from 'src/utils/styles/constants'
|
||||||
|
import BottomSheet from 'src/components/BottomSheet'
|
||||||
|
import BottomSheetRow from 'src/components/BottomSheet/Row'
|
||||||
|
import { toast } from 'src/components/toast'
|
||||||
|
|
||||||
const fireMutation = async ({
|
const fireMutation = async ({
|
||||||
id,
|
id,
|
||||||
@ -32,24 +35,11 @@ const fireMutation = async ({
|
|||||||
endpoint: `accounts/${id}/${type}`
|
endpoint: `accounts/${id}/${type}`
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.body[stateKey] === true) {
|
if (res.body[stateKey!] === true) {
|
||||||
Toast.show({
|
toast({ type: 'success', content: '功能成功' })
|
||||||
type: 'success',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '功能成功',
|
|
||||||
visibilityTime: 2000,
|
|
||||||
autoHide: true,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
toast({ type: 'error', content: '功能错误', autoHide: false })
|
||||||
type: 'error',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '请重试',
|
|
||||||
autoHide: false,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@ -64,57 +54,40 @@ const fireMutation = async ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!res.body.error) {
|
if (!res.body.error) {
|
||||||
Toast.show({
|
toast({ type: 'success', content: '隐藏域名成功' })
|
||||||
type: 'success',
|
|
||||||
position: 'bottom',
|
|
||||||
text1: '隐藏域名成功',
|
|
||||||
visibilityTime: 2000,
|
|
||||||
autoHide: true,
|
|
||||||
bottomOffset: 65
|
|
||||||
})
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
} else {
|
} else {
|
||||||
Toast.show({
|
toast({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
position: 'bottom',
|
content: '隐藏域名失败,请重试',
|
||||||
text1: '隐藏域名失败,请重试',
|
autoHide: false
|
||||||
autoHide: false,
|
})
|
||||||
bottomOffset: 65
|
return Promise.reject()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'reports':
|
||||||
|
console.log('reporting')
|
||||||
|
res = await client({
|
||||||
|
method: 'post',
|
||||||
|
instance: 'local',
|
||||||
|
endpoint: `reports`,
|
||||||
|
query: {
|
||||||
|
account_id: id || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log(res.body)
|
||||||
|
if (!res.body.error) {
|
||||||
|
toast({ type: 'success', content: '举报账户成功' })
|
||||||
|
return Promise.resolve()
|
||||||
|
} else {
|
||||||
|
toast({
|
||||||
|
type: 'error',
|
||||||
|
content: '举报账户失败,请重试',
|
||||||
|
autoHide: false
|
||||||
})
|
})
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
// case 'reports':
|
|
||||||
// res = await client({
|
|
||||||
// method: 'post',
|
|
||||||
// instance: 'local',
|
|
||||||
// endpoint: `reports`,
|
|
||||||
// query: {
|
|
||||||
// domain: id || ''
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if (!res.body.error) {
|
|
||||||
// Toast.show({
|
|
||||||
// type: 'success',
|
|
||||||
// position: 'bottom',
|
|
||||||
// text1: '隐藏域名成功',
|
|
||||||
// visibilityTime: 2000,
|
|
||||||
// autoHide: true,
|
|
||||||
// bottomOffset: 65
|
|
||||||
// })
|
|
||||||
// return Promise.resolve()
|
|
||||||
// } else {
|
|
||||||
// Toast.show({
|
|
||||||
// type: 'error',
|
|
||||||
// position: 'bottom',
|
|
||||||
// text1: '隐藏域名失败,请重试',
|
|
||||||
// autoHide: false,
|
|
||||||
// bottomOffset: 65
|
|
||||||
// })
|
|
||||||
// return Promise.reject()
|
|
||||||
// }
|
|
||||||
// break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,31 +124,12 @@ const HeaderDefault: React.FC<Props> = ({
|
|||||||
const [mutateAction] = useMutation(fireMutation, {
|
const [mutateAction] = useMutation(fireMutation, {
|
||||||
onMutate: () => {
|
onMutate: () => {
|
||||||
queryCache.cancelQueries(queryKey)
|
queryCache.cancelQueries(queryKey)
|
||||||
const prevData = queryCache.getQueryData(queryKey)
|
const oldData = queryCache.getQueryData(queryKey)
|
||||||
return prevData
|
return oldData
|
||||||
},
|
},
|
||||||
onSuccess: (newData, params) => {
|
onError: (err, _, oldData) => {
|
||||||
if (params.type === 'domain_blocks') {
|
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||||
console.log('clearing cache')
|
queryCache.setQueryData(queryKey, oldData)
|
||||||
queryCache.invalidateQueries(['Following', { page: 'Following' }])
|
|
||||||
}
|
|
||||||
// queryCache.setQueryData(queryKey, (oldData: any) => {
|
|
||||||
// oldData &&
|
|
||||||
// oldData.map((paging: any) => {
|
|
||||||
// paging.toots.map(
|
|
||||||
// (status: Mastodon.Status | Mastodon.Notification, i: number) => {
|
|
||||||
// if (status.id === newData.id) {
|
|
||||||
// paging.toots[i] = newData
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// })
|
|
||||||
// return oldData
|
|
||||||
// })
|
|
||||||
return Promise.resolve()
|
|
||||||
},
|
|
||||||
onError: (err, variables, prevData) => {
|
|
||||||
queryCache.setQueryData(queryKey, prevData)
|
|
||||||
},
|
},
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
queryCache.invalidateQueries(queryKey)
|
queryCache.invalidateQueries(queryKey)
|
||||||
@ -189,6 +143,24 @@ const HeaderDefault: React.FC<Props> = ({
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
}, [since])
|
}, [since])
|
||||||
|
|
||||||
|
const onPressAction = useCallback(() => setModalVisible(true), [])
|
||||||
|
const onPressApplication = useCallback(() => {
|
||||||
|
navigation.navigate('Webview', {
|
||||||
|
uri: application!.website
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const pressableAction = useMemo(
|
||||||
|
() => (
|
||||||
|
<Feather
|
||||||
|
name='more-horizontal'
|
||||||
|
color={theme.secondary}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<View style={styles.nameAndAction}>
|
<View style={styles.nameAndAction}>
|
||||||
@ -212,17 +184,12 @@ const HeaderDefault: React.FC<Props> = ({
|
|||||||
@{account}
|
@{account}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{accountId !== localAccountId && domain !== localDomain && (
|
{(accountId !== localAccountId || domain !== localDomain) && (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.action}
|
style={styles.action}
|
||||||
onPress={() => setModalVisible(true)}
|
onPress={onPressAction}
|
||||||
>
|
children={pressableAction}
|
||||||
<Feather
|
/>
|
||||||
name='more-horizontal'
|
|
||||||
color={theme.secondary}
|
|
||||||
size={constants.FONT_SIZE_M + 2}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.meta}>
|
<View style={styles.meta}>
|
||||||
@ -234,11 +201,7 @@ const HeaderDefault: React.FC<Props> = ({
|
|||||||
{application && application.name !== 'Web' && (
|
{application && application.name !== 'Web' && (
|
||||||
<View>
|
<View>
|
||||||
<Text
|
<Text
|
||||||
onPress={() => {
|
onPress={onPressApplication}
|
||||||
navigation.navigate('Webview', {
|
|
||||||
uri: application.website
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
style={[styles.application, { color: theme.secondary }]}
|
style={[styles.application, { color: theme.secondary }]}
|
||||||
>
|
>
|
||||||
发自于 - {application.name}
|
发自于 - {application.name}
|
||||||
@ -246,75 +209,65 @@ const HeaderDefault: React.FC<Props> = ({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
<BottomSheet
|
||||||
<Modal
|
|
||||||
animationType='fade'
|
|
||||||
presentationStyle='overFullScreen'
|
|
||||||
transparent
|
|
||||||
visible={modalVisible}
|
visible={modalVisible}
|
||||||
|
handleDismiss={() => setModalVisible(false)}
|
||||||
>
|
>
|
||||||
<Pressable
|
{accountId !== localAccountId && (
|
||||||
style={styles.modalBackground}
|
<BottomSheetRow
|
||||||
onPress={() => setModalVisible(false)}
|
onPressFunction={() => {
|
||||||
>
|
setModalVisible(false)
|
||||||
<View style={styles.modalSheet}>
|
mutateAction({
|
||||||
{accountId !== localAccountId && (
|
id: accountId,
|
||||||
<Pressable
|
type: 'mute',
|
||||||
onPress={() => {
|
stateKey: 'muting'
|
||||||
setModalVisible(false)
|
})
|
||||||
mutateAction({
|
}}
|
||||||
id: accountId,
|
icon='eye-off'
|
||||||
type: 'mute',
|
text={`隐藏 @${account} 的嘟嘟`}
|
||||||
stateKey: 'muting'
|
/>
|
||||||
})
|
)}
|
||||||
}}
|
{accountId !== localAccountId && (
|
||||||
>
|
<BottomSheetRow
|
||||||
<Text>静音用户</Text>
|
onPressFunction={() => {
|
||||||
</Pressable>
|
setModalVisible(false)
|
||||||
)}
|
mutateAction({
|
||||||
{accountId !== localAccountId && (
|
id: accountId,
|
||||||
<Pressable
|
type: 'block',
|
||||||
onPress={() => {
|
stateKey: 'blocking'
|
||||||
setModalVisible(false)
|
})
|
||||||
mutateAction({
|
}}
|
||||||
id: accountId,
|
icon='x-circle'
|
||||||
type: 'block',
|
text={`屏蔽用户 @${account}`}
|
||||||
stateKey: 'blocking'
|
/>
|
||||||
})
|
)}
|
||||||
}}
|
{domain !== localDomain && (
|
||||||
>
|
<BottomSheetRow
|
||||||
<Text>屏蔽用户</Text>
|
onPressFunction={() => {
|
||||||
</Pressable>
|
setModalVisible(false)
|
||||||
)}
|
mutateAction({
|
||||||
{domain !== localDomain && (
|
id: domain,
|
||||||
<Pressable
|
type: 'domain_blocks'
|
||||||
onPress={() => {
|
})
|
||||||
setModalVisible(false)
|
}}
|
||||||
mutateAction({
|
icon='cloud-off'
|
||||||
id: domain,
|
text={`屏蔽域名 ${domain}`}
|
||||||
type: 'domain_blocks'
|
/>
|
||||||
})
|
)}
|
||||||
}}
|
{accountId !== localAccountId && (
|
||||||
>
|
<BottomSheetRow
|
||||||
<Text>屏蔽域名</Text>
|
onPressFunction={() => {
|
||||||
</Pressable>
|
setModalVisible(false)
|
||||||
)}
|
mutateAction({
|
||||||
{accountId !== localAccountId && (
|
id: accountId,
|
||||||
<Pressable
|
type: 'reports'
|
||||||
onPress={() => {
|
})
|
||||||
setModalVisible(false)
|
}}
|
||||||
mutateAction({
|
icon='alert-triangle'
|
||||||
id: accountId,
|
text={`举报 @${account}`}
|
||||||
type: 'reports'
|
/>
|
||||||
})
|
)}
|
||||||
}}
|
</BottomSheet>
|
||||||
>
|
|
||||||
<Text>举报用户</Text>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
</Modal>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -349,22 +302,7 @@ const styles = StyleSheet.create({
|
|||||||
application: {
|
application: {
|
||||||
fontSize: constants.FONT_SIZE_S,
|
fontSize: constants.FONT_SIZE_S,
|
||||||
marginLeft: constants.SPACING_S
|
marginLeft: constants.SPACING_S
|
||||||
},
|
|
||||||
modalBackground: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'flex-end'
|
|
||||||
},
|
|
||||||
modalSheet: {
|
|
||||||
width: '100%',
|
|
||||||
height: '50%',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
flex: 1
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export default HeaderDefault
|
export default React.memo(HeaderDefault, () => true)
|
||||||
|
102
src/components/toast.tsx
Normal file
102
src/components/toast.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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 { useTheme } from 'src/utils/styles/ThemeManager'
|
||||||
|
import constants from 'src/utils/styles/constants'
|
||||||
|
import { Feather } from '@expo/vector-icons'
|
||||||
|
|
||||||
|
export interface Params {
|
||||||
|
type: 'success' | 'error' | 'warning'
|
||||||
|
position?: 'top' | 'bottom'
|
||||||
|
content: string
|
||||||
|
description?: string
|
||||||
|
autoHide?: boolean
|
||||||
|
onShow?: () => void
|
||||||
|
onHide?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config = {
|
||||||
|
type: Params['type']
|
||||||
|
position: Params['position']
|
||||||
|
text1: Params['content']
|
||||||
|
text2: Params['description']
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = ({
|
||||||
|
type,
|
||||||
|
position = 'top',
|
||||||
|
content,
|
||||||
|
description,
|
||||||
|
autoHide = true,
|
||||||
|
onShow,
|
||||||
|
onHide
|
||||||
|
}: Params) => {
|
||||||
|
Toast.show({
|
||||||
|
type: type,
|
||||||
|
position: position,
|
||||||
|
text1: content,
|
||||||
|
text2: description,
|
||||||
|
visibilityTime: 2000,
|
||||||
|
autoHide: autoHide,
|
||||||
|
topOffset: 0,
|
||||||
|
bottomOffset: 0,
|
||||||
|
onShow: onShow,
|
||||||
|
onHide: onHide
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToastBase = ({ config }: { config: Config }) => {
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const iconSet = {
|
||||||
|
success: 'check-circle',
|
||||||
|
error: 'x-circle',
|
||||||
|
warning: 'alert-circle'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView
|
||||||
|
style={[
|
||||||
|
styles.base,
|
||||||
|
{ backgroundColor: theme.background, shadowColor: theme.primary }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<Feather
|
||||||
|
name={iconSet[config.type]}
|
||||||
|
color={theme[config.type]}
|
||||||
|
size={constants.FONT_SIZE_M + 2}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.text, { color: theme.primary }]}>
|
||||||
|
{config.text1}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastConfig = {
|
||||||
|
success: (config: Config) => <ToastBase config={config} />,
|
||||||
|
error: (config: Config) => <ToastBase config={config} />,
|
||||||
|
warning: (config: Config) => <ToastBase config={config} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
base: {
|
||||||
|
width: '100%',
|
||||||
|
shadowOpacity: 1,
|
||||||
|
shadowRadius: 6
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: constants.SPACING_M
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: constants.FONT_SIZE_M,
|
||||||
|
marginLeft: constants.SPACING_S
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { toast, toastConfig }
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import ScreenMeRoot from 'src/screens/Me/Root'
|
import ScreenMeRoot from 'src/screens/Me/Root'
|
||||||
import ScreenMeConversations from './Me/Cconversations'
|
import ScreenMeConversations from './Me/Cconversations'
|
||||||
@ -8,7 +9,8 @@ import ScreenMeFavourites from './Me/Favourites'
|
|||||||
import ScreenMeLists from './Me/Lists'
|
import ScreenMeLists from './Me/Lists'
|
||||||
import sharedScreens from 'src/screens/Shared/sharedScreens'
|
import sharedScreens from 'src/screens/Shared/sharedScreens'
|
||||||
import ScreenMeListsList from './Me/Root/Lists/List'
|
import ScreenMeListsList from './Me/Root/Lists/List'
|
||||||
import { useSelector } from 'react-redux'
|
import ScreenMeSettings from './Me/Settings'
|
||||||
|
|
||||||
import { RootState } from 'src/store'
|
import { RootState } from 'src/store'
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator()
|
const Stack = createNativeStackNavigator()
|
||||||
@ -37,7 +39,7 @@ const ScreenMe: React.FC = () => {
|
|||||||
name='Screen-Me-Conversations'
|
name='Screen-Me-Conversations'
|
||||||
component={ScreenMeConversations}
|
component={ScreenMeConversations}
|
||||||
options={{
|
options={{
|
||||||
headerTitle: '对话'
|
headerTitle: '私信'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@ -51,21 +53,28 @@ const ScreenMe: React.FC = () => {
|
|||||||
name='Screen-Me-Favourites'
|
name='Screen-Me-Favourites'
|
||||||
component={ScreenMeFavourites}
|
component={ScreenMeFavourites}
|
||||||
options={{
|
options={{
|
||||||
headerTitle: '书签'
|
headerTitle: '喜欢'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Me-Lists'
|
name='Screen-Me-Lists'
|
||||||
component={ScreenMeLists}
|
component={ScreenMeLists}
|
||||||
options={{
|
options={{
|
||||||
headerTitle: '书签'
|
headerTitle: '列表'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name='Screen-Me-Lists-List'
|
name='Screen-Me-Lists-List'
|
||||||
component={ScreenMeListsList}
|
component={ScreenMeListsList}
|
||||||
|
options={({ route }: any) => ({
|
||||||
|
headerTitle: `列表:${route.params.title}`
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name='Screen-Me-Settings'
|
||||||
|
component={ScreenMeSettings}
|
||||||
options={{
|
options={{
|
||||||
headerTitle: '书签'
|
headerTitle: '设置'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { ActivityIndicator, Text } from 'react-native'
|
import { ActivityIndicator, Text } from 'react-native'
|
||||||
import { useQuery } from 'react-query'
|
import { useQuery } from 'react-query'
|
||||||
import { MenuContainer, MenuHeader, MenuItem } from 'src/components/Menu'
|
import { MenuContainer, MenuItem } from 'src/components/Menu'
|
||||||
|
|
||||||
import { listsFetch } from 'src/utils/fetches/listsFetch'
|
import { listsFetch } from 'src/utils/fetches/listsFetch'
|
||||||
|
|
||||||
@ -17,25 +17,22 @@ const ScreenMeLists: React.FC = () => {
|
|||||||
lists = <Text>载入错误</Text>
|
lists = <Text>载入错误</Text>
|
||||||
break
|
break
|
||||||
case 'success':
|
case 'success':
|
||||||
lists = data?.map(d => (
|
lists = data?.map((d: Mastodon.List, i: number) => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
key={i}
|
||||||
icon='list'
|
icon='list'
|
||||||
title={d.title}
|
title={d.title}
|
||||||
navigateTo='Screen-Me-Lists-List'
|
navigateTo='Screen-Me-Lists-List'
|
||||||
navigateToParams={{
|
navigateToParams={{
|
||||||
list: d.id
|
list: d.id,
|
||||||
|
title: d.title
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <MenuContainer>{lists}</MenuContainer>
|
||||||
<MenuContainer>
|
|
||||||
<MenuHeader heading='我的列表' />
|
|
||||||
{lists}
|
|
||||||
</MenuContainer>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScreenMeLists
|
export default ScreenMeLists
|
||||||
|
@ -4,7 +4,7 @@ import { MenuContainer, MenuItem } from 'src/components/Menu'
|
|||||||
const Settings: React.FC = () => {
|
const Settings: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<MenuContainer>
|
<MenuContainer>
|
||||||
<MenuItem icon='settings' title='设置' navigateTo='Local' />
|
<MenuItem icon='settings' title='设置' navigateTo='Screen-Me-Settings' />
|
||||||
</MenuContainer>
|
</MenuContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
9
src/screens/Me/Settings.tsx
Normal file
9
src/screens/Me/Settings.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { MenuContainer } from 'src/components/Menu'
|
||||||
|
|
||||||
|
const ScreenMeSettings: React.FC = () => {
|
||||||
|
return <MenuContainer></MenuContainer>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScreenMeSettings
|
@ -9,7 +9,6 @@ export interface Props {
|
|||||||
const limitRatio = 0.4
|
const limitRatio = 0.4
|
||||||
|
|
||||||
const AccountHeader: React.FC<Props> = ({ uri, limitHeight = false }) => {
|
const AccountHeader: React.FC<Props> = ({ uri, limitHeight = false }) => {
|
||||||
console.log(uri)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uri) {
|
if (uri) {
|
||||||
if (uri.includes('/headers/original/missing.png')) {
|
if (uri.includes('/headers/original/missing.png')) {
|
||||||
|
@ -303,6 +303,5 @@ const Compose: React.FC = () => {
|
|||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// ;(PostMain as any).whyDidYouRender = true
|
|
||||||
|
|
||||||
export default Compose
|
export default Compose
|
||||||
|
@ -304,10 +304,6 @@ const PostMain: React.FC<Props> = ({ postState, postDispatch }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// (PostSuggestions as any).whyDidYouRender = true,
|
|
||||||
// (PostPoll as any).whyDidYouRender = true,
|
|
||||||
// (PostEmojis as any).whyDidYouRender = true
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
base: {
|
base: {
|
||||||
flex: 1
|
flex: 1
|
||||||
|
@ -12,6 +12,7 @@ export default {
|
|||||||
SPACING_XL: 40,
|
SPACING_XL: 40,
|
||||||
|
|
||||||
GLOBAL_PAGE_PADDING: 16, // SPACING_M
|
GLOBAL_PAGE_PADDING: 16, // SPACING_M
|
||||||
|
GLOBAL_SPACING_BASE: 8, // SPACING_S
|
||||||
|
|
||||||
AVATAR_S: 52,
|
AVATAR_S: 52,
|
||||||
AVATAR_L: 104
|
AVATAR_L: 104
|
||||||
|
@ -3,11 +3,14 @@ import { DefaultTheme, DarkTheme } from '@react-navigation/native'
|
|||||||
export type ColorDefinitions =
|
export type ColorDefinitions =
|
||||||
| 'primary'
|
| 'primary'
|
||||||
| 'secondary'
|
| 'secondary'
|
||||||
|
| 'disabled'
|
||||||
| 'background'
|
| 'background'
|
||||||
| 'link'
|
| 'link'
|
||||||
| 'border'
|
| 'border'
|
||||||
| 'separator'
|
| 'separator'
|
||||||
| 'dangerous'
|
| 'success'
|
||||||
|
| 'error'
|
||||||
|
| 'warning'
|
||||||
|
|
||||||
const themeColors: {
|
const themeColors: {
|
||||||
[key in ColorDefinitions]: {
|
[key in ColorDefinitions]: {
|
||||||
@ -23,6 +26,10 @@ const themeColors: {
|
|||||||
light: 'rgb(153, 153, 153)',
|
light: 'rgb(153, 153, 153)',
|
||||||
dark: 'rgb(117, 117, 117)'
|
dark: 'rgb(117, 117, 117)'
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
light: 'rgb(229, 229, 234)',
|
||||||
|
dark: 'rgb(44, 44, 46)'
|
||||||
|
},
|
||||||
background: {
|
background: {
|
||||||
light: 'rgb(255, 255, 255)',
|
light: 'rgb(255, 255, 255)',
|
||||||
dark: 'rgb(0, 0, 0)'
|
dark: 'rgb(0, 0, 0)'
|
||||||
@ -39,9 +46,17 @@ const themeColors: {
|
|||||||
light: 'rgba(0, 0, 0, 0.1)',
|
light: 'rgba(0, 0, 0, 0.1)',
|
||||||
dark: 'rgba(255, 255, 255, 0.1)'
|
dark: 'rgba(255, 255, 255, 0.1)'
|
||||||
},
|
},
|
||||||
dangerous: {
|
success: {
|
||||||
|
light: 'rgb(52, 199, 89)',
|
||||||
|
dark: 'rgb(48, 209, 88)'
|
||||||
|
},
|
||||||
|
error: {
|
||||||
light: 'rgb(255, 59, 48)',
|
light: 'rgb(255, 59, 48)',
|
||||||
dark: 'rgb(255, 69, 58)'
|
dark: 'rgb(255, 69, 58)'
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
light: 'rgb(255, 149, 0)',
|
||||||
|
dark: 'rgb(255, 159, 10)'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user