1
0
mirror of https://github.com/tooot-app/app synced 2025-03-28 09:20:12 +01:00

Status actions done, awaiting account actions

This commit is contained in:
Zhiyuan Zheng 2020-11-06 00:59:33 +01:00
parent 04b7b0a7b9
commit c08c25e3b5
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
13 changed files with 6743 additions and 391 deletions

View File

@ -36,6 +36,7 @@
"react-native-render-html": "^4.2.4",
"react-native-safe-area-context": "3.1.4",
"react-native-screens": "~2.10.1",
"react-native-toast-message": "^1.3.4",
"react-native-web": "~0.13.7",
"react-native-webview": "10.7.0",
"react-navigation": "^4.4.3",

View File

@ -187,7 +187,7 @@ declare namespace mastodon {
type Status = {
// Base
id: string
url: string
uri: string
created_at: string
account: Account
content: string

View File

@ -7,6 +7,7 @@ import React from 'react'
import { Feather } from '@expo/vector-icons'
import store from 'src/stacks/common/store'
import { Provider } from 'react-redux'
import Toast from 'react-native-toast-message'
import { StatusBar } from 'expo-status-bar'
import Local from 'src/stacks/Local'
@ -22,6 +23,7 @@ export const Index: React.FC = () => {
return (
<Provider store={store}>
<StatusBar style='auto' />
<NavigationContainer>
<Tab.Navigator
screenOptions={({ route }) => ({
@ -62,6 +64,8 @@ export const Index: React.FC = () => {
<Tab.Screen name='Notifications' component={Notifications} />
<Tab.Screen name='Me' component={Me} />
</Tab.Navigator>
<Toast ref={(ref: any) => Toast.setRef(ref)} />
</NavigationContainer>
</Provider>
)

View File

@ -8,7 +8,7 @@ const client = async ({
query,
body
}: {
method: 'get' | 'post'
method: 'get' | 'post' | 'delete'
instance: 'local' | 'remote'
endpoint: string
query?: {
@ -16,7 +16,7 @@ const client = async ({
}
body?: object
}): Promise<any> => {
const state: RootState['instanceInfo'] = store.getState().instanceInfo
const state: RootState["instanceInfo"] = store.getState().instanceInfo
let response
try {
@ -42,8 +42,8 @@ const client = async ({
body: await response.json()
})
} else {
console.error(response.error)
return Promise.reject({ body: response.error_message })
console.error(response.status + ': ' + response.statusText)
return Promise.reject({ body: response.statusText })
}
}

View File

@ -1,255 +0,0 @@
import React, { useEffect, useState } from 'react'
import {
ActionSheetIOS,
Alert,
Clipboard,
Modal,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import { useMutation, useQueryCache } from 'react-query'
import { Feather } from '@expo/vector-icons'
import client from 'src/api/client'
import Success from './Responses/Success'
const fireMutation = async ({
id,
type,
stateKey,
prevState
}: {
id: string
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin'
stateKey: 'favourited' | 'reblogged' | 'bookmarked' | 'muted' | 'pinned'
prevState: boolean
}) => {
let res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
})
if (!res.body[stateKey] === prevState) {
return Promise.resolve(res.body)
} else {
const alert = {
title: 'This is a title',
message: 'This is a message'
}
Alert.alert(alert.title, alert.message, [
{ text: 'OK', onPress: () => console.log('OK Pressed') }
])
return Promise.reject()
}
}
export interface Props {
queryKey: store.QueryKey
id: string
url: string
replies_count: number
reblogs_count: number
reblogged?: boolean
favourites_count: number
favourited?: boolean
bookmarked?: boolean
}
const Actions: React.FC<Props> = ({
queryKey,
id,
url,
replies_count,
reblogs_count,
reblogged,
favourites_count,
favourited,
bookmarked
}) => {
const [modalVisible, setModalVisible] = useState(false)
const [successMessage, setSuccessMessage] = useState()
useEffect(() => {
setTimeout(() => {
setSuccessMessage(undefined)
}, 2000)
return () => {}
}, [successMessage])
const queryCache = useQueryCache()
const [mutateAction] = useMutation(fireMutation, {
onMutate: () => {
queryCache.cancelQueries(queryKey)
const prevData = queryCache.getQueryData(queryKey)
return prevData
},
onSuccess: (newData, params) => {
if (params.type === 'reblog') {
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: () => {
queryCache.invalidateQueries(queryKey)
}
})
return (
<>
<View style={styles.actions}>
<Pressable style={styles.action}>
<Feather name='message-circle' color='gray' />
{replies_count > 0 && <Text>{replies_count}</Text>}
</Pressable>
<Pressable
style={styles.action}
onPress={() =>
mutateAction({
id,
type: 'reblog',
stateKey: 'reblogged',
prevState: reblogged || false
})
}
>
<Feather name='repeat' color={reblogged ? 'black' : 'gray'} />
</Pressable>
<Pressable
style={styles.action}
onPress={() =>
mutateAction({
id,
type: 'favourite',
stateKey: 'favourited',
prevState: favourited || false
})
}
>
<Feather name='heart' color={favourited ? 'black' : 'gray'} />
</Pressable>
<Pressable
style={styles.action}
onPress={() =>
mutateAction({
id,
type: 'bookmark',
stateKey: 'bookmarked',
prevState: bookmarked || false
})
}
>
<Feather name='bookmark' color={bookmarked ? 'black' : 'gray'} />
</Pressable>
<Pressable style={styles.action} onPress={() => setModalVisible(true)}>
<Feather name='more-horizontal' color='gray' />
</Pressable>
</View>
<Modal
animationType='fade'
presentationStyle='overFullScreen'
transparent
visible={modalVisible}
>
<Pressable
style={styles.modalBackground}
onPress={() => setModalVisible(false)}
>
<View style={styles.modalSheet}>
<Pressable
onPress={() =>
ActionSheetIOS.showShareActionSheetWithOptions(
{
url,
excludedActivityTypes: [
'com.apple.UIKit.activity.Mail',
'com.apple.UIKit.activity.Print',
'com.apple.UIKit.activity.SaveToCameraRoll',
'com.apple.UIKit.activity.OpenInIBooks'
]
},
() => {},
() => {
setModalVisible(false)
setSuccessMessage('分享成功!')
}
)
}
>
<Text></Text>
</Pressable>
<Pressable
onPress={() => {
Clipboard.setString(url)
// Success message
setModalVisible(false)
}}
>
<Text></Text>
</Pressable>
<Text></Text>
<Text></Text>
<Text></Text>
</View>
</Pressable>
</Modal>
</>
)
}
const styles = StyleSheet.create({
actions: {
width: '100%',
flex: 1,
flexDirection: 'row',
marginTop: 8
},
action: {
width: '20%',
flexDirection: 'row',
justifyContent: 'center',
paddingTop: 8,
paddingBottom: 8
},
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 Actions

View File

@ -0,0 +1,347 @@
import React, { useEffect, useState } from 'react'
import {
ActionSheetIOS,
Clipboard,
Modal,
Pressable,
StyleSheet,
Text,
View
} from 'react-native'
import Toast from 'react-native-toast-message'
import { useMutation, useQueryCache } from 'react-query'
import { useSelector } from 'react-redux'
import { Feather } from '@expo/vector-icons'
import client from 'src/api/client'
const fireMutation = async ({
id,
type,
stateKey,
prevState
}: {
id: string
type:
| 'favourite'
| 'reblog'
| 'bookmark'
| 'mute'
| 'pin'
| 'delete'
| 'account/mute'
stateKey:
| 'favourited'
| 'reblogged'
| 'bookmarked'
| 'muted'
| 'pinned'
| 'id'
prevState?: boolean
}) => {
let res
switch (type) {
case 'favourite':
case 'reblog':
case 'bookmark':
case 'mute':
case 'pin':
res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${prevState ? 'un' : ''}${type}`
})
if (!res.body[stateKey] === prevState) {
if (type === 'bookmark' || 'mute' || 'pin')
Toast.show({
type: 'success',
position: 'bottom',
text1: '功能成功',
visibilityTime: 2000,
autoHide: true,
bottomOffset: 65
})
return Promise.resolve(res.body)
} else {
Toast.show({
type: 'error',
position: 'bottom',
text1: '请重试',
autoHide: false,
bottomOffset: 65
})
return Promise.reject()
}
break
case 'delete':
res = await client({
method: 'delete',
instance: 'local',
endpoint: `statuses/${id}`
})
if (res.body[stateKey] === id) {
Toast.show({
type: 'success',
position: 'bottom',
text1: '删除成功',
visibilityTime: 2000,
autoHide: true,
bottomOffset: 65
})
return Promise.resolve(res.body)
} else {
Toast.show({
type: 'error',
position: 'bottom',
text1: '请重试',
autoHide: false,
bottomOffset: 65
})
return Promise.reject()
}
break
}
}
export interface Props {
queryKey: store.QueryKey
status: mastodon.Status
}
const ActionsStatus: React.FC<Props> = ({ queryKey, status }) => {
const localAccountId = useSelector(state => state.instanceInfo.localAccountId)
const [modalVisible, setModalVisible] = useState(false)
const queryCache = useQueryCache()
const [mutateAction] = useMutation(fireMutation, {
onMutate: () => {
queryCache.cancelQueries(queryKey)
const prevData = queryCache.getQueryData(queryKey)
return prevData
},
onSuccess: (newData, params) => {
if (params.type === 'reblog') {
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: () => {
queryCache.invalidateQueries(queryKey)
}
})
return (
<>
<View style={styles.actions}>
<Pressable style={styles.action}>
<Feather name='message-circle' color='gray' />
{status.replies_count > 0 && <Text>{status.replies_count}</Text>}
</Pressable>
<Pressable
style={styles.action}
onPress={() =>
mutateAction({
id: status.id,
type: 'reblog',
stateKey: 'reblogged',
prevState: status.reblogged
})
}
>
<Feather name='repeat' color={status.reblogged ? 'black' : 'gray'} />
</Pressable>
<Pressable
style={styles.action}
onPress={() =>
mutateAction({
id: status.id,
type: 'favourite',
stateKey: 'favourited',
prevState: status.favourited
})
}
>
<Feather name='heart' color={status.favourited ? 'black' : 'gray'} />
</Pressable>
<Pressable
style={styles.action}
onPress={() =>
mutateAction({
id: status.id,
type: 'bookmark',
stateKey: 'bookmarked',
prevState: status.bookmarked
})
}
>
<Feather
name='bookmark'
color={status.bookmarked ? 'black' : 'gray'}
/>
</Pressable>
<Pressable style={styles.action} onPress={() => setModalVisible(true)}>
<Feather name='more-horizontal' color='gray' />
</Pressable>
</View>
<Modal
animationType='fade'
presentationStyle='overFullScreen'
transparent
visible={modalVisible}
>
<Pressable
style={styles.modalBackground}
onPress={() => setModalVisible(false)}
>
<View style={styles.modalSheet}>
<Pressable
onPress={() =>
ActionSheetIOS.showShareActionSheetWithOptions(
{
url: status.uri,
excludedActivityTypes: [
'com.apple.UIKit.activity.Mail',
'com.apple.UIKit.activity.Print',
'com.apple.UIKit.activity.SaveToCameraRoll',
'com.apple.UIKit.activity.OpenInIBooks'
]
},
() => {},
() => {
setModalVisible(false)
Toast.show({
type: 'success',
position: 'bottom',
text1: '分享成功',
visibilityTime: 2000,
autoHide: true,
bottomOffset: 65
})
}
)
}
>
<Text></Text>
</Pressable>
<Pressable
onPress={() => {
Clipboard.setString(status.uri)
setModalVisible(false)
Toast.show({
type: 'success',
position: 'bottom',
text1: '链接复制成功',
visibilityTime: 2000,
autoHide: true,
bottomOffset: 65
})
}}
>
<Text></Text>
</Pressable>
{status.account.id === localAccountId && (
<Pressable
onPress={() => {
setModalVisible(false)
mutateAction({
id: status.id,
type: 'delete',
stateKey: 'id'
})
}}
>
<Text></Text>
</Pressable>
)}
<Text></Text>
<Pressable
onPress={() => {
setModalVisible(false)
mutateAction({
id: status.id,
type: 'mute',
stateKey: 'muted',
prevState: status.muted
})
}}
>
<Text>{status.muted ? '取消静音' : '静音'}</Text>
</Pressable>
{status.account.id === localAccountId && (
<Pressable
onPress={() => {
setModalVisible(false)
mutateAction({
id: status.id,
type: 'pin',
stateKey: 'pinned',
prevState: status.pinned
})
}}
>
<Text>{status.pinned ? '取消置顶' : '置顶'}</Text>
</Pressable>
)}
<Text></Text>
</View>
</Pressable>
</Modal>
</>
)
}
const styles = StyleSheet.create({
actions: {
width: '100%',
flex: 1,
flexDirection: 'row',
marginTop: 8
},
action: {
width: '20%',
flexDirection: 'row',
justifyContent: 'center',
paddingTop: 8,
paddingBottom: 8
},
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 ActionsStatus

View File

@ -1,62 +0,0 @@
import React, { useEffect, useRef } from 'react'
import { Animated, StyleSheet, Text, View } from 'react-native'
export interface Props {
message?: string
}
const Success: React.FC<Props> = ({ message }) => {
const fadeAnim = useRef(new Animated.Value(0)).current
useEffect(() => {
// console.log(message)
if (message !== undefined) {
fadeIn()
} else {
fadeOut()
}
}, [message])
const fadeIn = () => {
// Will change fadeAnim value to 1 in 5 seconds
Animated.timing(fadeAnim, {
useNativeDriver: false,
toValue: 300,
duration: 1000
}).start()
}
const fadeOut = () => {
// Will change fadeAnim value to 0 in 5 seconds
Animated.timing(fadeAnim, {
useNativeDriver: false,
toValue: 400,
duration: 1000
}).start()
}
return (
<Animated.View
style={[
styles.success,
{
top: fadeAnim
}
]}
>
<Text>{message}</Text>
</Animated.View>
)
}
const styles = StyleSheet.create({
success: {
width: '80%',
height: 25,
position: 'absolute',
top: 400,
backgroundColor: 'red',
zIndex: 50
}
})
export default Success

View File

@ -9,92 +9,94 @@ import Content from './Status/Content'
import Poll from './Status/Poll'
import Attachment from './Status/Attachment'
import Card from './Status/Card'
import Actions from './Status/Actions'
import ActionsStatus from './Status/ActionsStatus'
export interface Props {
status: mastodon.Notification
notification: mastodon.Notification
queryKey: store.QueryKey
}
const TootNotification: React.FC<Props> = ({ status }) => {
const TootNotification: React.FC<Props> = ({ notification, queryKey }) => {
const navigation = useNavigation()
const actualAccount = status.status ? status.status.account : status.account
const actualAccount = notification.status
? notification.status.account
: notification.account
const statusView = useMemo(() => {
return (
<View style={styles.statusView}>
<View style={styles.notificationView}>
<Actioned
action={status.type}
name={status.account.display_name || status.account.username}
emojis={status.account.emojis}
action={notification.type}
name={
notification.account.display_name || notification.account.username
}
emojis={notification.account.emojis}
notification
/>
<View style={styles.status}>
<View style={styles.notification}>
<Avatar uri={actualAccount.avatar} id={actualAccount.id} />
<View style={styles.details}>
<Header
name={actualAccount.display_name || actualAccount.username}
emojis={actualAccount.emojis}
account={actualAccount.acct}
created_at={status.created_at}
created_at={notification.created_at}
/>
<Pressable
onPress={() => navigation.navigate('Toot', { toot: status.id })}
onPress={() =>
navigation.navigate('Toot', { toot: notification.id })
}
>
{status.status ? (
{notification.status ? (
<>
{status.status.content && (
{notification.status.content && (
<Content
content={status.status.content}
emojis={status.status.emojis}
mentions={status.status.mentions}
spoiler_text={status.status.spoiler_text}
// tags={status.status.tags}
content={notification.status.content}
emojis={notification.status.emojis}
mentions={notification.status.mentions}
spoiler_text={notification.status.spoiler_text}
// tags={notification.notification.tags}
// style={{ flex: 1 }}
/>
)}
{status.status.poll && <Poll poll={status.status.poll} />}
{status.status.media_attachments.length > 0 && (
{notification.status.poll && (
<Poll poll={notification.status.poll} />
)}
{notification.status.media_attachments.length > 0 && (
<Attachment
media_attachments={status.status.media_attachments}
sensitive={status.status.sensitive}
media_attachments={notification.status.media_attachments}
sensitive={notification.status.sensitive}
width={Dimensions.get('window').width - 24 - 50 - 8}
/>
)}
{status.status.card && <Card card={status.status.card} />}
{notification.status.card && (
<Card card={notification.status.card} />
)}
</>
) : (
<></>
)}
</Pressable>
{status.status && (
<Actions
id={status.status.id}
url={status.status.url}
replies_count={status.status.replies_count}
reblogs_count={status.status.reblogs_count}
reblogged={status.status.reblogged}
favourites_count={status.status.favourites_count}
favourited={status.status.favourited}
bookmarked={status.status.bookmarked}
/>
{notification.status && (
<ActionsStatus queryKey={queryKey} status={notification.status} />
)}
</View>
</View>
</View>
)
}, [status])
}, [notification])
return statusView
}
const styles = StyleSheet.create({
statusView: {
notificationView: {
flex: 1,
flexDirection: 'column',
padding: 12
},
status: {
notification: {
flex: 1,
flexDirection: 'row'
},

View File

@ -9,7 +9,7 @@ import Content from './Status/Content'
import Poll from './Status/Poll'
import Attachment from './Status/Attachment'
import Card from './Status/Card'
import Actions from './Status/Actions'
import ActionsStatus from './Status/ActionsStatus'
export interface Props {
status: mastodon.Status
@ -19,7 +19,7 @@ export interface Props {
const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
const navigation = useNavigation()
let actualContent = status.reblog ? status.reblog : status
let actualStatus = status.reblog ? status.reblog : status
const statusView = useMemo(() => {
return (
@ -33,59 +33,49 @@ const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
)}
<View style={styles.status}>
<Avatar
uri={actualContent.account.avatar}
id={actualContent.account.id}
uri={actualStatus.account.avatar}
id={actualStatus.account.id}
/>
<View style={styles.details}>
<Header
name={
actualContent.account.display_name ||
actualContent.account.username
actualStatus.account.display_name ||
actualStatus.account.username
}
emojis={actualContent.account.emojis}
account={actualContent.account.acct}
emojis={actualStatus.account.emojis}
account={actualStatus.account.acct}
created_at={status.created_at}
application={status.application}
/>
{/* Can pass toot info to next page to speed up performance */}
<Pressable
onPress={() =>
navigation.navigate('Toot', { toot: actualContent.id })
navigation.navigate('Toot', { toot: actualStatus.id })
}
>
{actualContent.content ? (
{actualStatus.content ? (
<Content
content={actualContent.content}
emojis={actualContent.emojis}
mentions={actualContent.mentions}
spoiler_text={actualContent.spoiler_text}
// tags={actualContent.tags}
content={actualStatus.content}
emojis={actualStatus.emojis}
mentions={actualStatus.mentions}
spoiler_text={actualStatus.spoiler_text}
// tags={actualStatus.tags}
// style={{ flex: 1 }}
/>
) : (
<></>
)}
{actualContent.poll && <Poll poll={actualContent.poll} />}
{actualContent.media_attachments.length > 0 && (
{actualStatus.poll && <Poll poll={actualStatus.poll} />}
{actualStatus.media_attachments.length > 0 && (
<Attachment
media_attachments={actualContent.media_attachments}
sensitive={actualContent.sensitive}
media_attachments={actualStatus.media_attachments}
sensitive={actualStatus.sensitive}
width={Dimensions.get('window').width - 24 - 50 - 8}
/>
)}
{actualContent.card && <Card card={actualContent.card} />}
{actualStatus.card && <Card card={actualStatus.card} />}
</Pressable>
<Actions
queryKey={queryKey}
id={actualContent.id}
url={actualContent.url}
replies_count={actualContent.replies_count}
reblogs_count={actualContent.reblogs_count}
reblogged={actualContent.reblogged}
favourites_count={actualContent.favourites_count}
favourited={actualContent.favourited}
bookmarked={actualContent.bookmarked}
/>
<ActionsStatus queryKey={queryKey} status={actualStatus} />
</View>
</View>
</View>

View File

@ -70,7 +70,11 @@ const Timeline: React.FC<Props> = ({
keyExtractor={({ id }) => id}
renderItem={({ item, index, separators }) =>
page === 'Notifications' ? (
<StatusInNotifications key={index} status={item} />
<StatusInNotifications
key={index}
notification={item}
queryKey={queryKey}
/>
) : (
<StatusInTimeline key={index} status={item} queryKey={queryKey} />
)

View File

@ -7,6 +7,7 @@ const preloadedState = {
instanceInfo: {
local: 'social.xmflsct.com',
localToken: 'qjzJ0IjvZ1apsn0_wBkGcdjKgX7Dao9KEPhGwggPwAo',
localAccountId: '1',
remote: 'mastodon.social'
}
}

6315
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

View File

@ -4949,6 +4949,11 @@ react-native-screens@~2.10.1:
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-2.10.1.tgz#06d22fae87ef0ce51c616c34a199726db1403b95"
integrity sha512-Z2kKSk4AwWRQNCBmTjViuBQK0/Lx0jc25TZptn/2gKYUCOuVRvCekoA26u0Tsb3BIQ8tWDsZW14OwDlFUXW1aw==
react-native-toast-message@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-native-toast-message/-/react-native-toast-message-1.3.4.tgz#19ea1c5d3ad8e9d12f7550c4719a9e0258dca6bd"
integrity sha512-83rKaLNC8/JPEPlrONVTurVepIlXKuHnFnxm7mJ58CA3XN4fT6ayRBubs/pd8u980mDFHqo8ysJOSHF3dwPg4g==
react-native-web@~0.13.7:
version "0.13.18"
resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.13.18.tgz#964f058a16521a3b9a31b091415edfef5b6ef305"