Actions are working well!

This commit is contained in:
Zhiyuan Zheng 2020-11-05 00:47:31 +01:00
parent 631636db15
commit c825895b92
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
11 changed files with 4280 additions and 118 deletions

4150
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
"expo-splash-screen": "~0.6.1", "expo-splash-screen": "~0.6.1",
"expo-status-bar": "~1.0.2", "expo-status-bar": "~1.0.2",
"ky": "^0.24.0", "ky": "^0.24.0",
"lodash": "^4.17.20",
"react": "16.13.1", "react": "16.13.1",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz", "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.3.tar.gz",
@ -44,6 +45,7 @@
"devDependencies": { "devDependencies": {
"@babel/core": "~7.9.0", "@babel/core": "~7.9.0",
"@babel/plugin-proposal-optional-chaining": "^7.12.1", "@babel/plugin-proposal-optional-chaining": "^7.12.1",
"@types/lodash": "^4.14.164",
"@types/react": "^16.9.55", "@types/react": "^16.9.55",
"@types/react-dom": "^16.9.9", "@types/react-dom": "^16.9.9",
"@types/react-native": "^0.63.30", "@types/react-native": "^0.63.30",

38
src/@types/store.d.ts vendored
View File

@ -1,13 +1,11 @@
declare namespace store { declare namespace store {
type AsyncStatus = 'idle' | 'loading' | 'succeeded' | 'failed'
type InstanceInfoState = { type InstanceInfoState = {
local: string local: string
localToken: string localToken: string
remote: string remote: string
} }
type TimelinePage = type Pages =
| 'Following' | 'Following'
| 'Local' | 'Local'
| 'LocalPublic' | 'LocalPublic'
@ -20,28 +18,14 @@ declare namespace store {
| 'Account_All' | 'Account_All'
| 'Account_Media' | 'Account_Media'
type TimelineState = { type QueryKey = [
toots: mastodon.Status[] | [] Pages,
pointer?: string {
status: AsyncStatus page: Pages
} hashtag?: string
list?: string
type TimelinesState = { toot?: string
Following: TimelineState account?: string
Local: TimelineState }
LocalPublic: TimelineState ]
RemotePublic: TimelineState
Notifications: TimelineState
Hashtag: TimelineState
List: TimelineState
Toot: TimelineState
Account_Default: TimelineState
Account_All: TimelineState
Account_Media: TimelineState
}
type AccountState = {
account: mastodon.Account | {}
status: AsyncStatus
}
} }

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { import {
ActionSheetIOS, ActionSheetIOS,
Alert,
Clipboard, Clipboard,
Modal, Modal,
Pressable, Pressable,
@ -8,13 +9,51 @@ import {
Text, Text,
View View
} from 'react-native' } from 'react-native'
import { useDispatch } from 'react-redux' import { useMutation, useQueryCache } from 'react-query'
import { Feather } from '@expo/vector-icons' import { Feather } from '@expo/vector-icons'
import action from './action' import client from 'src/api/client'
import Success from './Responses/Success' 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}`
})
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 { export interface Props {
queryKey: store.QueryKey
id: string id: string
url: string url: string
replies_count: number replies_count: number
@ -22,10 +61,11 @@ export interface Props {
reblogged?: boolean reblogged?: boolean
favourites_count: number favourites_count: number
favourited?: boolean favourited?: boolean
bookmarked: boolean bookmarked?: boolean
} }
const Actions: React.FC<Props> = ({ const Actions: React.FC<Props> = ({
queryKey,
id, id,
url, url,
replies_count, replies_count,
@ -35,7 +75,6 @@ const Actions: React.FC<Props> = ({
favourited, favourited,
bookmarked bookmarked
}) => { }) => {
const dispatch = useDispatch()
const [modalVisible, setModalVisible] = useState(false) const [modalVisible, setModalVisible] = useState(false)
const [successMessage, setSuccessMessage] = useState() const [successMessage, setSuccessMessage] = useState()
@ -46,10 +85,42 @@ const Actions: React.FC<Props> = ({
return () => {} return () => {}
}, [successMessage]) }, [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 ( return (
<> <>
<Success message={successMessage} />
<View style={styles.actions}> <View style={styles.actions}>
<Pressable style={styles.action}> <Pressable style={styles.action}>
<Feather name='message-circle' color='gray' /> <Feather name='message-circle' color='gray' />
@ -59,12 +130,11 @@ const Actions: React.FC<Props> = ({
<Pressable <Pressable
style={styles.action} style={styles.action}
onPress={() => onPress={() =>
action({ mutateAction({
dispatch,
id, id,
type: 'reblog', type: 'reblog',
stateKey: 'reblogged', stateKey: 'reblogged',
statePrev: reblogged || false prevState: reblogged || false
}) })
} }
> >
@ -74,12 +144,11 @@ const Actions: React.FC<Props> = ({
<Pressable <Pressable
style={styles.action} style={styles.action}
onPress={() => onPress={() =>
action({ mutateAction({
dispatch,
id, id,
type: 'favourite', type: 'favourite',
stateKey: 'favourited', stateKey: 'favourited',
statePrev: favourited || false prevState: favourited || false
}) })
} }
> >
@ -89,12 +158,11 @@ const Actions: React.FC<Props> = ({
<Pressable <Pressable
style={styles.action} style={styles.action}
onPress={() => onPress={() =>
action({ mutateAction({
dispatch,
id, id,
type: 'bookmark', type: 'bookmark',
stateKey: 'bookmarked', stateKey: 'bookmarked',
statePrev: bookmarked prevState: bookmarked || false
}) })
} }
> >

View File

@ -1,46 +0,0 @@
import { Dispatch } from '@reduxjs/toolkit'
import { Alert } from 'react-native'
import client from 'src/api/client'
// import { updateStatus } from 'src/stacks/common/timelineSlice'
const action = async ({
dispatch,
id,
type,
stateKey,
statePrev
}: {
dispatch: Dispatch
id: string
type: 'favourite' | 'reblog' | 'bookmark' | 'mute' | 'pin'
stateKey: 'favourited' | 'reblogged' | 'bookmarked' | 'muted' | 'pinned'
statePrev: boolean
}): Promise<void> => {
const alert = {
title: 'This is a title',
message: 'This is a message'
}
// ISSUE: https://github.com/tootsuite/mastodon/issues/3166
let res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${statePrev ? 'un' : ''}${type}`
})
res = await client({
method: 'post',
instance: 'local',
endpoint: `statuses/${id}/${statePrev ? 'un' : ''}${type}`
})
if (!res.body[stateKey] === statePrev) {
// dispatch(updateStatus(res.body))
} else {
Alert.alert(alert.title, alert.message, [
{ text: 'OK', onPress: () => console.log('OK Pressed') }
])
}
}
export default action

View File

@ -13,9 +13,10 @@ import Actions from './Status/Actions'
export interface Props { export interface Props {
status: mastodon.Status status: mastodon.Status
queryKey: store.QueryKey
} }
const StatusInTimeline: React.FC<Props> = ({ status }) => { const StatusInTimeline: React.FC<Props> = ({ status, queryKey }) => {
const navigation = useNavigation() const navigation = useNavigation()
let actualContent = status.reblog ? status.reblog : status let actualContent = status.reblog ? status.reblog : status
@ -75,6 +76,7 @@ const StatusInTimeline: React.FC<Props> = ({ status }) => {
{actualContent.card && <Card card={actualContent.card} />} {actualContent.card && <Card card={actualContent.card} />}
</Pressable> </Pressable>
<Actions <Actions
queryKey={queryKey}
id={actualContent.id} id={actualContent.id}
url={actualContent.url} url={actualContent.url}
replies_count={actualContent.replies_count} replies_count={actualContent.replies_count}

View File

@ -28,6 +28,7 @@ const Header = ({
size: { width: number; height: number } size: { width: number; height: number }
}) => { }) => {
if (uri) { if (uri) {
const heightRatio = size ? size.height / size.width : 1 / 2
return ( return (
<Image <Image
source={{ uri: uri }} source={{ uri: uri }}
@ -36,7 +37,7 @@ const Header = ({
{ {
height: height:
Dimensions.get('window').width * Dimensions.get('window').width *
(size ? size.height / size.width : 1 / 2) (heightRatio > 0.5 ? 1 / 2 : heightRatio)
} }
]} ]}
/> />
@ -144,6 +145,7 @@ const Toots = ({ account }: { account: string }) => {
page={item} page={item}
account={account} account={account}
disableRefresh disableRefresh
scrollEnabled={false}
/> />
</View> </View>
) )
@ -156,17 +158,6 @@ const Toots = ({ account }: { account: string }) => {
index index
})} })}
horizontal horizontal
ListHeaderComponent={
<View
style={{
width: Dimensions.get('window').width,
height: 100,
position: 'absolute'
}}
>
<Text>Test</Text>
</View>
}
onMomentumScrollEnd={() => { onMomentumScrollEnd={() => {
setSegmentManuallyTriggered(false) setSegmentManuallyTriggered(false)
}} }}
@ -204,7 +195,6 @@ const Account: React.FC<Props> = ({
) )
// const stateRelationships = useSelector(relationshipsState) // const stateRelationships = useSelector(relationshipsState)
const [loaded, setLoaded] = useState(false)
interface isHeaderImageSize { interface isHeaderImageSize {
width: number width: number
height: number height: number
@ -214,19 +204,18 @@ const Account: React.FC<Props> = ({
>(undefined) >(undefined)
useEffect(() => { useEffect(() => {
if (data.header) { if (isSuccess && data.header) {
Image.getSize(data.header, (width, height) => { Image.getSize(data.header, (width, height) => {
setHeaderImageSize({ width, height }) setHeaderImageSize({ width, height })
setLoaded(true)
}) })
} else { } else {
setLoaded(true) setHeaderImageSize({ width: 3, height: 1 })
} }
}, [data]) }, [data, isSuccess])
// add emoji support // add emoji support
return isSuccess ? ( return isSuccess && headerImageSize ? (
<View> <ScrollView>
{headerImageSize && ( {headerImageSize && (
<Header <Header
uri={data.header} uri={data.header}
@ -238,7 +227,7 @@ const Account: React.FC<Props> = ({
)} )}
<Information account={data} emojis={data.emojis} /> <Information account={data} emojis={data.emojis} />
<Toots account={id} /> <Toots account={id} />
</View> </ScrollView>
) : ( ) : (
<></> <></>
) )

View File

@ -4,17 +4,19 @@ import { setFocusHandler, useInfiniteQuery } from 'react-query'
import StatusInNotifications from 'src/components/StatusInNotifications' import StatusInNotifications from 'src/components/StatusInNotifications'
import StatusInTimeline from 'src/components/StatusInTimeline' import StatusInTimeline from 'src/components/StatusInTimeline'
import store from './store'
import { timelineFetch } from './timelineFetch' import { timelineFetch } from './timelineFetch'
// Opening nesting hashtag pages // Opening nesting hashtag pages
export interface Props { export interface Props {
page: store.TimelinePage page: store.Pages
hashtag?: string hashtag?: string
list?: string list?: string
toot?: string toot?: string
account?: string account?: string
disableRefresh?: boolean disableRefresh?: boolean
scrollEnabled?: boolean
} }
const Timeline: React.FC<Props> = ({ const Timeline: React.FC<Props> = ({
@ -23,7 +25,8 @@ const Timeline: React.FC<Props> = ({
list, list,
toot, toot,
account, account,
disableRefresh = false disableRefresh = false,
scrollEnabled = true
}) => { }) => {
setFocusHandler(handleFocus => { setFocusHandler(handleFocus => {
const handleAppStateChange = (appState: string) => { const handleAppStateChange = (appState: string) => {
@ -35,6 +38,10 @@ const Timeline: React.FC<Props> = ({
return () => AppState.removeEventListener('change', handleAppStateChange) return () => AppState.removeEventListener('change', handleAppStateChange)
}) })
const queryKey: store.QueryKey = [
page,
{ page, hashtag, list, toot, account }
]
const { const {
isLoading, isLoading,
isFetchingMore, isFetchingMore,
@ -42,10 +49,7 @@ const Timeline: React.FC<Props> = ({
isSuccess, isSuccess,
data, data,
fetchMore fetchMore
} = useInfiniteQuery( } = useInfiniteQuery(queryKey, timelineFetch)
[page, { page, hashtag, list, toot, account }],
timelineFetch
)
const flattenData = data ? data.flatMap(d => [...d?.toots]) : [] const flattenData = data ? data.flatMap(d => [...d?.toots]) : []
let content let content
@ -58,13 +62,14 @@ const Timeline: React.FC<Props> = ({
<> <>
<FlatList <FlatList
style={{ minHeight: '100%' }} style={{ minHeight: '100%' }}
scrollEnabled={scrollEnabled} // For timeline in Account view
data={flattenData} data={flattenData}
keyExtractor={({ id }) => id} keyExtractor={({ id }) => id}
renderItem={({ item, index, separators }) => renderItem={({ item, index, separators }) =>
page === 'Notifications' ? ( page === 'Notifications' ? (
<StatusInNotifications key={index} status={item} /> <StatusInNotifications key={index} status={item} />
) : ( ) : (
<StatusInTimeline key={index} status={item} /> <StatusInTimeline key={index} status={item} queryKey={queryKey} />
) )
} }
// {...(state.pointer && { initialScrollIndex: state.pointer })} // {...(state.pointer && { initialScrollIndex: state.pointer })}

View File

@ -1,3 +1,5 @@
import { uniqBy } from 'lodash'
import client from 'src/api/client' import client from 'src/api/client'
export const timelineFetch = async ( export const timelineFetch = async (
@ -93,7 +95,7 @@ export const timelineFetch = async (
pinned: 'true' pinned: 'true'
} }
}) })
const toots = res.body let toots: mastodon.Status[] = res.body
res = await client({ res = await client({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
@ -102,7 +104,7 @@ export const timelineFetch = async (
exclude_replies: 'true' exclude_replies: 'true'
} }
}) })
toots.push(...res.body) toots = uniqBy([...toots, ...res.body], 'id')
return Promise.resolve({ toots: toots }) return Promise.resolve({ toots: toots })
case 'Account_All': case 'Account_All':

View File

@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES6",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"jsx": "react-native", "jsx": "react-native",
"lib": ["dom", "esnext"], "lib": ["dom", "esnext"],

View File

@ -1380,6 +1380,11 @@
"@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-coverage" "*"
"@types/istanbul-lib-report" "*" "@types/istanbul-lib-report" "*"
"@types/lodash@^4.14.164":
version "4.14.164"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.164.tgz#52348bcf909ac7b4c1bcbeda5c23135176e5dfa0"
integrity sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg==
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.3" version "15.7.3"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"