mirror of
https://github.com/tooot-app/app
synced 2025-02-04 04:17:36 +01: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
|
||||
})
|
||||
|
||||
if (__DEV__) {
|
||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||
// whyDidYouRender(React)
|
||||
}
|
||||
// if (__DEV__) {
|
||||
// const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||
// whyDidYouRender(React, {
|
||||
// trackAllPureComponents: true,
|
||||
// trackHooks: true,
|
||||
// hotReloadBufferMs: 1000
|
||||
// })
|
||||
// }
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
|
@ -53,7 +53,6 @@
|
||||
"@types/react": "~16.9.35",
|
||||
"@types/react-dom": "^16.9.9",
|
||||
"@types/react-native": "~0.63.2",
|
||||
"@types/react-native-htmlview": "^0.12.2",
|
||||
"@types/react-navigation": "^3.4.0",
|
||||
"@types/react-redux": "^7.1.11",
|
||||
"@welldone-software/why-did-you-render": "^6.0.0-rc.1",
|
||||
@ -61,4 +60,4 @@
|
||||
"typescript": "~3.9.2"
|
||||
},
|
||||
"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 React from 'react'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
// @ts-ignore
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
|
||||
import ScreenLocal from 'src/screens/Local'
|
||||
import ScreenPublic from 'src/screens/Public'
|
||||
@ -17,6 +15,7 @@ import ScreenMe from 'src/screens/Me'
|
||||
import { themes } from 'src/utils/styles/themes'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import getCurrentTab from 'src/utils/getCurrentTab'
|
||||
import { toastConfig } from 'src/components/toast'
|
||||
|
||||
enableScreens()
|
||||
const Tab = createBottomTabNavigator<RootStackParamList>()
|
||||
@ -89,7 +88,7 @@ export const Index: React.FC = () => {
|
||||
<Tab.Screen name='Screen-Me' component={ScreenMe} />
|
||||
</Tab.Navigator>
|
||||
|
||||
<Toast ref={(ref: any) => Toast.setRef(ref)} />
|
||||
<Toast ref={(ref: any) => Toast.setRef(ref)} config={toastConfig} />
|
||||
</NavigationContainer>
|
||||
)
|
||||
}
|
||||
|
@ -45,9 +45,8 @@ const client = async ({
|
||||
// } catch (error) {
|
||||
// return Promise.reject('ky error: ' + error.json())
|
||||
// }
|
||||
console.log('upload done')
|
||||
if (response?.ok) {
|
||||
console.log('returning ok')
|
||||
console.log('Query: /' + endpoint)
|
||||
if (response.ok) {
|
||||
return Promise.resolve({
|
||||
headers: response.headers,
|
||||
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 (
|
||||
<View style={styles.core}>
|
||||
<Text style={{ color: destructive ? theme.dangerous : theme.primary }}>
|
||||
<Text style={{ color: destructive ? theme.error : theme.primary }}>
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { Text } from 'react-native'
|
||||
import HTMLView, { HTMLViewNode } from 'react-native-htmlview'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
@ -102,22 +102,30 @@ const ParseContent: React.FC<Props> = ({
|
||||
const navigation = useNavigation()
|
||||
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 (
|
||||
<HTMLView
|
||||
value={content}
|
||||
renderNode={(node, index) =>
|
||||
renderNode({ theme, node, index, navigation, mentions, showFullLink })
|
||||
}
|
||||
TextComponent={({ children }) =>
|
||||
emojis && children ? (
|
||||
<Emojis content={children.toString()} emojis={emojis} size={size} />
|
||||
) : (
|
||||
<Text>{children}</Text>
|
||||
)
|
||||
}
|
||||
RootComponent={({ children }) => {
|
||||
return <Text numberOfLines={linesTruncated}>{children}</Text>
|
||||
}}
|
||||
TextComponent={textComponent}
|
||||
RootComponent={rootComponent}
|
||||
renderNode={renderNodeCallback}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Dimensions, FlatList, Text, View } from 'react-native'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Dimensions, FlatList, StyleSheet, Text, 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'
|
||||
@ -59,6 +59,52 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
|
||||
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 (
|
||||
<Stack.Navigator>
|
||||
<Stack.Screen
|
||||
@ -71,13 +117,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
<SegmentedControl
|
||||
values={[content[0].title, content[1].title]}
|
||||
selectedIndex={segment}
|
||||
onChange={({ nativeEvent }) => {
|
||||
setSegmentManuallyTriggered(true)
|
||||
setSegment(nativeEvent.selectedSegmentIndex)
|
||||
horizontalPaging.current.scrollToIndex({
|
||||
index: nativeEvent.selectedSegmentIndex
|
||||
})
|
||||
}}
|
||||
onChange={onChangeSegment}
|
||||
style={{ width: 150, height: 30 }}
|
||||
/>
|
||||
),
|
||||
@ -86,11 +126,7 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
name='search'
|
||||
size={24}
|
||||
color={theme.secondary}
|
||||
onPress={() => {
|
||||
navigation.navigate(getCurrentTab(navigation), {
|
||||
screen: 'Screen-Shared-Search'
|
||||
})
|
||||
}}
|
||||
onPress={onPressSearch}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -99,42 +135,19 @@ const Timelines: React.FC<Props> = ({ name, content }) => {
|
||||
{() => {
|
||||
return (
|
||||
<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
|
||||
onMomentumScrollEnd={() => setSegmentManuallyTriggered(false)}
|
||||
onScroll={({ nativeEvent }) =>
|
||||
!segmentManuallyTriggered &&
|
||||
setSegment(
|
||||
nativeEvent.contentOffset.x <=
|
||||
Dimensions.get('window').width / 2
|
||||
? 0
|
||||
: 1
|
||||
)
|
||||
}
|
||||
pagingEnabled
|
||||
data={content}
|
||||
bounces={false}
|
||||
onScroll={flOnScroll}
|
||||
ref={horizontalPaging}
|
||||
style={styles.flatList}
|
||||
renderItem={flRenderItem}
|
||||
extraData={localRegistered}
|
||||
keyExtractor={flKeyExtrator}
|
||||
getItemLayout={flGetItemLayout}
|
||||
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 { ActivityIndicator, AppState, FlatList, Text, View } from 'react-native'
|
||||
import React, { useCallback } from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AppState,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View
|
||||
} from 'react-native'
|
||||
import { setFocusHandler, useInfiniteQuery } from 'react-query'
|
||||
|
||||
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 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
|
||||
if (!isSuccess) {
|
||||
content = <ActivityIndicator />
|
||||
@ -60,53 +101,18 @@ const Timeline: React.FC<Props> = ({
|
||||
content = (
|
||||
<>
|
||||
<FlatList
|
||||
style={{ minHeight: '100%' }}
|
||||
scrollEnabled={scrollEnabled} // For timeline in Account view
|
||||
data={flattenData}
|
||||
keyExtractor={({ id }) => id}
|
||||
renderItem={({ item, index, separators }) => {
|
||||
switch (page) {
|
||||
case 'Conversations':
|
||||
return <TimelineConversation key={index} item={item} />
|
||||
case 'Notifications':
|
||||
return (
|
||||
<TimelineNotifications
|
||||
key={index}
|
||||
notification={item}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<TimelineDefault
|
||||
key={index}
|
||||
item={item}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}}
|
||||
ItemSeparatorComponent={() => <TimelineSeparator />}
|
||||
onRefresh={flOnRefresh}
|
||||
renderItem={flRenderItem}
|
||||
onEndReached={flOnEndReach}
|
||||
keyExtractor={flKeyExtrator}
|
||||
style={styles.flatList}
|
||||
scrollEnabled={scrollEnabled} // For timeline in Account view
|
||||
ItemSeparatorComponent={flItemSeparatorComponent}
|
||||
refreshing={!disableRefresh && isLoading}
|
||||
onEndReachedThreshold={!disableRefresh ? 0.5 : null}
|
||||
// require getItemLayout
|
||||
// {...(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 />}
|
||||
</>
|
||||
@ -116,4 +122,10 @@ const Timeline: React.FC<Props> = ({
|
||||
return <View>{content}</View>
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flatList: {
|
||||
minHeight: '100%'
|
||||
}
|
||||
})
|
||||
|
||||
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 { useNavigation } from '@react-navigation/native'
|
||||
|
||||
@ -24,6 +24,43 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
|
||||
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(() => {
|
||||
return (
|
||||
<View style={styles.statusView}>
|
||||
@ -54,40 +91,7 @@ const TimelineDefault: React.FC<Props> = ({ item, queryKey }) => {
|
||||
application={item.application}
|
||||
/>
|
||||
{/* Can pass toot info to next page to speed up performance */}
|
||||
<Pressable
|
||||
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>
|
||||
<Pressable onPress={pressableToot} children={childrenToot} />
|
||||
<ActionsStatus queryKey={queryKey} status={actualStatus} />
|
||||
</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 {
|
||||
ActionSheetIOS,
|
||||
Clipboard,
|
||||
@ -11,12 +11,14 @@ import {
|
||||
import Toast from 'react-native-toast-message'
|
||||
import { useMutation, useQueryCache } from 'react-query'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
import { findIndex } from 'lodash'
|
||||
|
||||
import client from 'src/api/client'
|
||||
import { getLocalAccountId } from 'src/utils/slices/instancesSlice'
|
||||
import { store } from 'src/store'
|
||||
import { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
import constants from 'src/utils/styles/constants'
|
||||
import { toast } from 'src/components/toast'
|
||||
|
||||
const fireMutation = async ({
|
||||
id,
|
||||
@ -25,14 +27,7 @@ const fireMutation = async ({
|
||||
prevState
|
||||
}: {
|
||||
id: string
|
||||
type:
|
||||
| 'favourite'
|
||||
| 'reblog'
|
||||
| 'bookmark'
|
||||
| 'mute'
|
||||
| 'pin'
|
||||
| 'delete'
|
||||
| 'account/mute'
|
||||
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin' | 'delete'
|
||||
stateKey:
|
||||
| 'favourited'
|
||||
| 'reblogged'
|
||||
@ -53,27 +48,13 @@ const fireMutation = async ({
|
||||
method: 'post',
|
||||
instance: 'local',
|
||||
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
|
||||
})
|
||||
}) // bug in response from Mastodon
|
||||
|
||||
if (!res.body[stateKey] === prevState) {
|
||||
if (type === 'bookmark' || 'mute' || 'pin')
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
text1: '功能成功',
|
||||
visibilityTime: 2000,
|
||||
autoHide: true,
|
||||
bottomOffset: 65
|
||||
})
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
position: 'bottom',
|
||||
text1: '请重试',
|
||||
autoHide: false,
|
||||
bottomOffset: 65
|
||||
})
|
||||
toast({ type: 'error', content: '功能错误' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
@ -85,23 +66,10 @@ const fireMutation = async ({
|
||||
})
|
||||
|
||||
if (res.body[stateKey] === id) {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
text1: '删除成功',
|
||||
visibilityTime: 2000,
|
||||
autoHide: true,
|
||||
bottomOffset: 65
|
||||
})
|
||||
toast({ type: 'success', content: '删除成功' })
|
||||
return Promise.resolve(res.body)
|
||||
} else {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
position: 'bottom',
|
||||
text1: '请重试',
|
||||
autoHide: false,
|
||||
bottomOffset: 65
|
||||
})
|
||||
toast({ type: 'error', content: '删除失败' })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
@ -120,136 +88,204 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
state ? theme.primary : theme.secondary
|
||||
|
||||
const localAccountId = getLocalAccountId(store.getState())
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [bottomSheetVisible, setBottomSheetVisible] = useState(false)
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const [mutateAction] = useMutation(fireMutation, {
|
||||
onMutate: () => {
|
||||
onMutate: ({ id, type, stateKey, prevState }) => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const prevData = queryCache.getQueryData(queryKey)
|
||||
return prevData
|
||||
},
|
||||
onSuccess: (newData, params) => {
|
||||
if (params.type === 'reblog') {
|
||||
queryCache.invalidateQueries(['Following', { page: 'Following' }])
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
|
||||
switch (type) {
|
||||
case 'favourite':
|
||||
case 'reblog':
|
||||
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 &&
|
||||
// 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()
|
||||
|
||||
return oldData
|
||||
},
|
||||
onError: (err, variables, prevData) => {
|
||||
queryCache.setQueryData(queryKey, prevData)
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries(queryKey)
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试' })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
}
|
||||
})
|
||||
|
||||
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 (
|
||||
<>
|
||||
<View style={styles.actions}>
|
||||
<Pressable style={styles.action}>
|
||||
<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>
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={onPressReply}
|
||||
children={childrenReply}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() =>
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'reblog',
|
||||
stateKey: 'reblogged',
|
||||
prevState: status.reblogged
|
||||
})
|
||||
onPress={
|
||||
status.visibility === 'public' || status.visibility === 'unlisted'
|
||||
? onPressReblog
|
||||
: null
|
||||
}
|
||||
>
|
||||
<Feather
|
||||
name='repeat'
|
||||
color={iconColorAction(status.reblogged)}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
children={childrenReblog}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() =>
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'favourite',
|
||||
stateKey: 'favourited',
|
||||
prevState: status.favourited
|
||||
})
|
||||
}
|
||||
>
|
||||
<Feather
|
||||
name='heart'
|
||||
color={iconColorAction(status.favourited)}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
onPress={onPressFavourite}
|
||||
children={childrenFavourite}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() =>
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'bookmark',
|
||||
stateKey: 'bookmarked',
|
||||
prevState: status.bookmarked
|
||||
})
|
||||
}
|
||||
>
|
||||
<Feather
|
||||
name='bookmark'
|
||||
color={iconColorAction(status.bookmarked)}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
onPress={onPressBookmark}
|
||||
children={childrenBookmark}
|
||||
/>
|
||||
|
||||
<Pressable style={styles.action} onPress={() => setModalVisible(true)}>
|
||||
<Feather
|
||||
name='share-2'
|
||||
color={iconColor}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={onPressShare}
|
||||
children={childrenShare}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
animationType='fade'
|
||||
presentationStyle='overFullScreen'
|
||||
transparent
|
||||
visible={modalVisible}
|
||||
visible={bottomSheetVisible}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.modalBackground}
|
||||
onPress={() => setModalVisible(false)}
|
||||
onPress={() => setBottomSheetVisible(false)}
|
||||
>
|
||||
<View style={styles.modalSheet}>
|
||||
<Pressable
|
||||
@ -266,15 +302,8 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
},
|
||||
() => {},
|
||||
() => {
|
||||
setModalVisible(false)
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
text1: '分享成功',
|
||||
visibilityTime: 2000,
|
||||
autoHide: true,
|
||||
bottomOffset: 65
|
||||
})
|
||||
setBottomSheetVisible(false)
|
||||
toast({ type: 'success', content: '分享成功' })
|
||||
}
|
||||
)
|
||||
}
|
||||
@ -284,15 +313,8 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
Clipboard.setString(status.uri)
|
||||
setModalVisible(false)
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
text1: '链接复制成功',
|
||||
visibilityTime: 2000,
|
||||
autoHide: true,
|
||||
bottomOffset: 65
|
||||
})
|
||||
setBottomSheetVisible(false)
|
||||
toast({ type: 'success', content: '链接复制成功' })
|
||||
}}
|
||||
>
|
||||
<Text>复制链接</Text>
|
||||
@ -300,7 +322,7 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
{status.account.id === localAccountId && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'delete',
|
||||
@ -314,7 +336,7 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
<Text>(删除并重发)</Text>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
type: 'mute',
|
||||
@ -325,10 +347,11 @@ const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
|
||||
>
|
||||
<Text>{status.muted ? '取消静音' : '静音'}</Text>
|
||||
</Pressable>
|
||||
{/* Also note that reblogs cannot be pinned. */}
|
||||
{status.account.id === localAccountId && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
setBottomSheetVisible(false)
|
||||
mutateAction({
|
||||
id: status.id,
|
||||
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 { useNavigation } from '@react-navigation/native'
|
||||
|
||||
@ -12,15 +12,14 @@ export interface Props {
|
||||
const Avatar: React.FC<Props> = ({ uri, id }) => {
|
||||
const navigation = useNavigation()
|
||||
// Need to fix go back root
|
||||
const onPress = useCallback(() => {
|
||||
navigation.navigate('Screen-Shared-Account', {
|
||||
id: id
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.avatar}
|
||||
onPress={() => {
|
||||
navigation.navigate('Screen-Shared-Account', {
|
||||
id: id
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Pressable style={styles.avatar} onPress={onPress}>
|
||||
<Image source={{ uri: uri }} style={styles.image} />
|
||||
</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 { useNavigation } from '@react-navigation/native'
|
||||
|
||||
@ -8,32 +8,29 @@ export interface Props {
|
||||
|
||||
const Card: React.FC<Props> = ({ card }) => {
|
||||
const navigation = useNavigation()
|
||||
const onPress = useCallback(() => {
|
||||
navigation.navigate('Screen-Shared-Webview', {
|
||||
uri: card.url
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
card && (
|
||||
<Pressable
|
||||
style={styles.card}
|
||||
onPress={() => {
|
||||
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>
|
||||
<Pressable style={styles.card} onPress={onPress}>
|
||||
{card.image && (
|
||||
<View style={styles.left}>
|
||||
<Image source={{ uri: card.image }} style={styles.image} />
|
||||
</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 { Modal, Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { Feather } from '@expo/vector-icons'
|
||||
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 { useTheme } from 'src/utils/styles/ThemeManager'
|
||||
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 ({
|
||||
id,
|
||||
@ -32,24 +35,11 @@ const fireMutation = async ({
|
||||
endpoint: `accounts/${id}/${type}`
|
||||
})
|
||||
|
||||
if (res.body[stateKey] === true) {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
text1: '功能成功',
|
||||
visibilityTime: 2000,
|
||||
autoHide: true,
|
||||
bottomOffset: 65
|
||||
})
|
||||
if (res.body[stateKey!] === true) {
|
||||
toast({ type: 'success', content: '功能成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
position: 'bottom',
|
||||
text1: '请重试',
|
||||
autoHide: false,
|
||||
bottomOffset: 65
|
||||
})
|
||||
toast({ type: 'error', content: '功能错误', autoHide: false })
|
||||
return Promise.reject()
|
||||
}
|
||||
break
|
||||
@ -64,57 +54,40 @@ const fireMutation = async ({
|
||||
})
|
||||
|
||||
if (!res.body.error) {
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
position: 'bottom',
|
||||
text1: '隐藏域名成功',
|
||||
visibilityTime: 2000,
|
||||
autoHide: true,
|
||||
bottomOffset: 65
|
||||
})
|
||||
toast({ type: 'success', content: '隐藏域名成功' })
|
||||
return Promise.resolve()
|
||||
} else {
|
||||
Toast.show({
|
||||
toast({
|
||||
type: 'error',
|
||||
position: 'bottom',
|
||||
text1: '隐藏域名失败,请重试',
|
||||
autoHide: false,
|
||||
bottomOffset: 65
|
||||
content: '隐藏域名失败,请重试',
|
||||
autoHide: false
|
||||
})
|
||||
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()
|
||||
}
|
||||
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, {
|
||||
onMutate: () => {
|
||||
queryCache.cancelQueries(queryKey)
|
||||
const prevData = queryCache.getQueryData(queryKey)
|
||||
return prevData
|
||||
const oldData = queryCache.getQueryData(queryKey)
|
||||
return oldData
|
||||
},
|
||||
onSuccess: (newData, params) => {
|
||||
if (params.type === 'domain_blocks') {
|
||||
console.log('clearing cache')
|
||||
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)
|
||||
onError: (err, _, oldData) => {
|
||||
toast({ type: 'error', content: '请重试', autoHide: false })
|
||||
queryCache.setQueryData(queryKey, oldData)
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries(queryKey)
|
||||
@ -189,6 +143,24 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
}, 1000)
|
||||
}, [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 (
|
||||
<View>
|
||||
<View style={styles.nameAndAction}>
|
||||
@ -212,17 +184,12 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
@{account}
|
||||
</Text>
|
||||
</View>
|
||||
{accountId !== localAccountId && domain !== localDomain && (
|
||||
{(accountId !== localAccountId || domain !== localDomain) && (
|
||||
<Pressable
|
||||
style={styles.action}
|
||||
onPress={() => setModalVisible(true)}
|
||||
>
|
||||
<Feather
|
||||
name='more-horizontal'
|
||||
color={theme.secondary}
|
||||
size={constants.FONT_SIZE_M + 2}
|
||||
/>
|
||||
</Pressable>
|
||||
onPress={onPressAction}
|
||||
children={pressableAction}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.meta}>
|
||||
@ -234,11 +201,7 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
{application && application.name !== 'Web' && (
|
||||
<View>
|
||||
<Text
|
||||
onPress={() => {
|
||||
navigation.navigate('Webview', {
|
||||
uri: application.website
|
||||
})
|
||||
}}
|
||||
onPress={onPressApplication}
|
||||
style={[styles.application, { color: theme.secondary }]}
|
||||
>
|
||||
发自于 - {application.name}
|
||||
@ -246,75 +209,65 @@ const HeaderDefault: React.FC<Props> = ({
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Modal
|
||||
animationType='fade'
|
||||
presentationStyle='overFullScreen'
|
||||
transparent
|
||||
<BottomSheet
|
||||
visible={modalVisible}
|
||||
handleDismiss={() => setModalVisible(false)}
|
||||
>
|
||||
<Pressable
|
||||
style={styles.modalBackground}
|
||||
onPress={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.modalSheet}>
|
||||
{accountId !== localAccountId && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'mute',
|
||||
stateKey: 'muting'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text>静音用户</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
{accountId !== localAccountId && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'block',
|
||||
stateKey: 'blocking'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text>屏蔽用户</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
{domain !== localDomain && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: domain,
|
||||
type: 'domain_blocks'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text>屏蔽域名</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
{accountId !== localAccountId && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'reports'
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Text>举报用户</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
{accountId !== localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPressFunction={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'mute',
|
||||
stateKey: 'muting'
|
||||
})
|
||||
}}
|
||||
icon='eye-off'
|
||||
text={`隐藏 @${account} 的嘟嘟`}
|
||||
/>
|
||||
)}
|
||||
{accountId !== localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPressFunction={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'block',
|
||||
stateKey: 'blocking'
|
||||
})
|
||||
}}
|
||||
icon='x-circle'
|
||||
text={`屏蔽用户 @${account}`}
|
||||
/>
|
||||
)}
|
||||
{domain !== localDomain && (
|
||||
<BottomSheetRow
|
||||
onPressFunction={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: domain,
|
||||
type: 'domain_blocks'
|
||||
})
|
||||
}}
|
||||
icon='cloud-off'
|
||||
text={`屏蔽域名 ${domain}`}
|
||||
/>
|
||||
)}
|
||||
{accountId !== localAccountId && (
|
||||
<BottomSheetRow
|
||||
onPressFunction={() => {
|
||||
setModalVisible(false)
|
||||
mutateAction({
|
||||
id: accountId,
|
||||
type: 'reports'
|
||||
})
|
||||
}}
|
||||
icon='alert-triangle'
|
||||
text={`举报 @${account}`}
|
||||
/>
|
||||
)}
|
||||
</BottomSheet>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@ -349,22 +302,7 @@ const styles = StyleSheet.create({
|
||||
application: {
|
||||
fontSize: constants.FONT_SIZE_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 { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import ScreenMeRoot from 'src/screens/Me/Root'
|
||||
import ScreenMeConversations from './Me/Cconversations'
|
||||
@ -8,7 +9,8 @@ import ScreenMeFavourites from './Me/Favourites'
|
||||
import ScreenMeLists from './Me/Lists'
|
||||
import sharedScreens from 'src/screens/Shared/sharedScreens'
|
||||
import ScreenMeListsList from './Me/Root/Lists/List'
|
||||
import { useSelector } from 'react-redux'
|
||||
import ScreenMeSettings from './Me/Settings'
|
||||
|
||||
import { RootState } from 'src/store'
|
||||
|
||||
const Stack = createNativeStackNavigator()
|
||||
@ -37,7 +39,7 @@ const ScreenMe: React.FC = () => {
|
||||
name='Screen-Me-Conversations'
|
||||
component={ScreenMeConversations}
|
||||
options={{
|
||||
headerTitle: '对话'
|
||||
headerTitle: '私信'
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@ -51,21 +53,28 @@ const ScreenMe: React.FC = () => {
|
||||
name='Screen-Me-Favourites'
|
||||
component={ScreenMeFavourites}
|
||||
options={{
|
||||
headerTitle: '书签'
|
||||
headerTitle: '喜欢'
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Me-Lists'
|
||||
component={ScreenMeLists}
|
||||
options={{
|
||||
headerTitle: '书签'
|
||||
headerTitle: '列表'
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Me-Lists-List'
|
||||
component={ScreenMeListsList}
|
||||
options={({ route }: any) => ({
|
||||
headerTitle: `列表:${route.params.title}`
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name='Screen-Me-Settings'
|
||||
component={ScreenMeSettings}
|
||||
options={{
|
||||
headerTitle: '书签'
|
||||
headerTitle: '设置'
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { ActivityIndicator, Text } from 'react-native'
|
||||
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'
|
||||
|
||||
@ -17,25 +17,22 @@ const ScreenMeLists: React.FC = () => {
|
||||
lists = <Text>载入错误</Text>
|
||||
break
|
||||
case 'success':
|
||||
lists = data?.map(d => (
|
||||
lists = data?.map((d: Mastodon.List, i: number) => (
|
||||
<MenuItem
|
||||
key={i}
|
||||
icon='list'
|
||||
title={d.title}
|
||||
navigateTo='Screen-Me-Lists-List'
|
||||
navigateToParams={{
|
||||
list: d.id
|
||||
list: d.id,
|
||||
title: d.title
|
||||
}}
|
||||
/>
|
||||
))
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuHeader heading='我的列表' />
|
||||
{lists}
|
||||
</MenuContainer>
|
||||
)
|
||||
return <MenuContainer>{lists}</MenuContainer>
|
||||
}
|
||||
|
||||
export default ScreenMeLists
|
||||
|
@ -4,7 +4,7 @@ import { MenuContainer, MenuItem } from 'src/components/Menu'
|
||||
const Settings: React.FC = () => {
|
||||
return (
|
||||
<MenuContainer>
|
||||
<MenuItem icon='settings' title='设置' navigateTo='Local' />
|
||||
<MenuItem icon='settings' title='设置' navigateTo='Screen-Me-Settings' />
|
||||
</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 AccountHeader: React.FC<Props> = ({ uri, limitHeight = false }) => {
|
||||
console.log(uri)
|
||||
useEffect(() => {
|
||||
if (uri) {
|
||||
if (uri.includes('/headers/original/missing.png')) {
|
||||
|
@ -303,6 +303,5 @@ const Compose: React.FC = () => {
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
}
|
||||
// ;(PostMain as any).whyDidYouRender = true
|
||||
|
||||
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({
|
||||
base: {
|
||||
flex: 1
|
||||
|
@ -12,6 +12,7 @@ export default {
|
||||
SPACING_XL: 40,
|
||||
|
||||
GLOBAL_PAGE_PADDING: 16, // SPACING_M
|
||||
GLOBAL_SPACING_BASE: 8, // SPACING_S
|
||||
|
||||
AVATAR_S: 52,
|
||||
AVATAR_L: 104
|
||||
|
@ -3,11 +3,14 @@ import { DefaultTheme, DarkTheme } from '@react-navigation/native'
|
||||
export type ColorDefinitions =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'disabled'
|
||||
| 'background'
|
||||
| 'link'
|
||||
| 'border'
|
||||
| 'separator'
|
||||
| 'dangerous'
|
||||
| 'success'
|
||||
| 'error'
|
||||
| 'warning'
|
||||
|
||||
const themeColors: {
|
||||
[key in ColorDefinitions]: {
|
||||
@ -23,6 +26,10 @@ const themeColors: {
|
||||
light: 'rgb(153, 153, 153)',
|
||||
dark: 'rgb(117, 117, 117)'
|
||||
},
|
||||
disabled: {
|
||||
light: 'rgb(229, 229, 234)',
|
||||
dark: 'rgb(44, 44, 46)'
|
||||
},
|
||||
background: {
|
||||
light: 'rgb(255, 255, 255)',
|
||||
dark: 'rgb(0, 0, 0)'
|
||||
@ -39,9 +46,17 @@ const themeColors: {
|
||||
light: 'rgba(0, 0, 0, 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)',
|
||||
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