A lot of updates

This commit is contained in:
Zhiyuan Zheng 2020-11-28 17:07:30 +01:00
parent 8200375c92
commit 735cc0b903
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
29 changed files with 940 additions and 1275 deletions

12
App.tsx
View File

@ -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 (

View File

@ -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
View File

@ -0,0 +1 @@
declare module 'react-native-toast-message'

View File

@ -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>
) )
} }

View File

@ -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()

View 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

View 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

View File

@ -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>

View File

@ -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>
}}
/> />
) )
} }

View File

@ -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)

View File

@ -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

View File

@ -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
})

View File

@ -108,4 +108,4 @@ const styles = StyleSheet.create({
} }
}) })
export default Actioned export default React.memo(Actioned)

View File

@ -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',

View File

@ -84,4 +84,4 @@ const Attachment: React.FC<Props> = ({
) )
} }
export default Attachment export default React.memo(Attachment, () => true)

View File

@ -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)

View File

@ -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)

View File

@ -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
View 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 }

View File

@ -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: '设置'
}} }}
/> />

View File

@ -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

View File

@ -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>
) )
} }

View 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

View File

@ -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')) {

View File

@ -303,6 +303,5 @@ const Compose: React.FC = () => {
</KeyboardAvoidingView> </KeyboardAvoidingView>
) )
} }
// ;(PostMain as any).whyDidYouRender = true
export default Compose export default Compose

View File

@ -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

View File

@ -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

View File

@ -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)'
} }
} }

788
yarn.lock

File diff suppressed because it is too large Load Diff