1
0
mirror of https://github.com/tooot-app/app synced 2025-04-04 05:31:05 +02:00

Switch to shared hooks

This commit is contained in:
Zhiyuan Zheng 2021-01-11 21:36:57 +01:00
parent fdce172c57
commit 284d6e46e0
No known key found for this signature in database
GPG Key ID: 078A93AB607D85E0
47 changed files with 1053 additions and 727 deletions

View File

@ -7,7 +7,6 @@ import { enableScreens } from 'react-native-screens'
import { QueryClient, QueryClientProvider } from 'react-query' import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import checkSecureStorageVersion from '@root/startup/checkSecureStorageVersion'
import dev from '@root/startup/dev' import dev from '@root/startup/dev'
import sentry from '@root/startup/sentry' import sentry from '@root/startup/sentry'
import log from '@root/startup/log' import log from '@root/startup/log'

View File

@ -14,7 +14,7 @@ import ScreenLocal from '@screens/Local'
import ScreenMe from '@screens/Me' import ScreenMe from '@screens/Me'
import ScreenNotifications from '@screens/Notifications' import ScreenNotifications from '@screens/Notifications'
import ScreenPublic from '@screens/Public' import ScreenPublic from '@screens/Public'
import hookTimeline from '@utils/queryHooks/timeline' import { useTimelineQuery } from '@utils/queryHooks/timeline'
import { import {
getLocalActiveIndex, getLocalActiveIndex,
getLocalNotification, getLocalNotification,
@ -100,7 +100,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
}, []) }, [])
// On launch check if there is any unread noficiations // On launch check if there is any unread noficiations
const queryNotification = hookTimeline({ const queryNotification = useTimelineQuery({
page: 'Notifications', page: 'Notifications',
options: { options: {
enabled: localActiveIndex !== null ? true : false, enabled: localActiveIndex !== null ? true : false,
@ -112,9 +112,7 @@ const Index: React.FC<Props> = ({ localCorrupt }) => {
const prevNotification = useSelector(getLocalNotification) const prevNotification = useSelector(getLocalNotification)
useEffect(() => { useEffect(() => {
if (queryNotification.data?.pages) { if (queryNotification.data?.pages) {
const flattenData = queryNotification.data.pages.flatMap(d => [ const flattenData = queryNotification.data.pages.flatMap(d => [...d])
...d?.toots
])
const latestNotificationTime = flattenData.length const latestNotificationTime = flattenData.length
? (flattenData[0] as Mastodon.Notification).created_at ? (flattenData[0] as Mastodon.Notification).created_at
: undefined : undefined

View File

@ -12,7 +12,6 @@ import {
ViewStyle ViewStyle
} from 'react-native' } from 'react-native'
import { Chase } from 'react-native-animated-spinkit' import { Chase } from 'react-native-animated-spinkit'
import Animated from 'react-native-reanimated'
export interface Props { export interface Props {
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
@ -156,7 +155,7 @@ const Button: React.FC<Props> = ({
} }
return ( return (
<Animated.View> <View>
<Pressable <Pressable
style={[ style={[
styles.button, styles.button,
@ -175,7 +174,7 @@ const Button: React.FC<Props> = ({
children={children} children={children}
disabled={disabled || active || loading} disabled={disabled || active || loading}
/> />
</Animated.View> </View>
) )
} }

View File

@ -2,8 +2,8 @@ import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import hookApps from '@utils/queryHooks/apps' import { useAppsQuery } from '@utils/queryHooks/apps'
import hookInstance from '@utils/queryHooks/instance' import { useInstanceQuery } from '@utils/queryHooks/instance'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { import {
getLocalInstances, getLocalInstances,
@ -42,11 +42,11 @@ const ComponentInstance: React.FC<Props> = ({
const [appData, setApplicationData] = useState<InstanceLocal['appData']>() const [appData, setApplicationData] = useState<InstanceLocal['appData']>()
const localInstances = useSelector(getLocalInstances) const localInstances = useSelector(getLocalInstances)
const instanceQuery = hookInstance({ const instanceQuery = useInstanceQuery({
instanceDomain, instanceDomain,
options: { enabled: false, retry: false } options: { enabled: false, retry: false }
}) })
const applicationQuery = hookApps({ const applicationQuery = useAppsQuery({
instanceDomain, instanceDomain,
options: { enabled: false, retry: false } options: { enabled: false, retry: false }
}) })

View File

@ -1,13 +1,16 @@
import client from '@api/client'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { QueryKeyRelationship } from '@utils/queryHooks/relationship' import {
QueryKeyRelationship,
useRelationshipMutation
} from '@utils/queryHooks/relationship'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React, { useCallback } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { StyleSheet, View } from 'react-native' import { StyleSheet, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']
@ -17,23 +20,18 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
const { t } = useTranslation() const { t } = useTranslation()
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const queryKeyNotification: QueryKeyTimeline = [
'Timeline',
{ page: 'Notifications' }
]
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback( const mutation = useRelationshipMutation({
({ type }: { type: 'authorize' | 'reject' }) => {
return client<Mastodon.Relationship>({
method: 'post',
instance: 'local',
url: `follow_requests/${id}/${type}`
})
},
[]
)
const mutation = useMutation(fireMutation, {
onSuccess: res => { onSuccess: res => {
haptics('Success') haptics('Success')
queryClient.setQueryData(queryKeyRelationship, res) queryClient.setQueryData<Mastodon.Relationship[]>(queryKeyRelationship, [
queryClient.refetchQueries(['Notifications']) res
])
queryClient.refetchQueries(queryKeyNotification)
}, },
onError: (err: any, { type }) => { onError: (err: any, { type }) => {
haptics('Error') haptics('Error')
@ -60,14 +58,26 @@ const RelationshipIncoming: React.FC<Props> = ({ id }) => {
type='icon' type='icon'
content='X' content='X'
loading={mutation.isLoading} loading={mutation.isLoading}
onPress={() => mutation.mutate({ type: 'reject' })} onPress={() =>
mutation.mutate({
id,
type: 'incoming',
payload: { action: 'reject' }
})
}
/> />
<Button <Button
round round
type='icon' type='icon'
content='Check' content='Check'
loading={mutation.isLoading} loading={mutation.isLoading}
onPress={() => mutation.mutate({ type: 'authorize' })} onPress={() =>
mutation.mutate({
id,
type: 'incoming',
payload: { action: 'authorize' }
})
}
style={styles.approve} style={styles.approve}
/> />
</View> </View>

View File

@ -1,113 +1,127 @@
import client from '@api/client'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import hookRelationship, { import {
QueryKeyRelationship QueryKeyRelationship,
useRelationshipMutation,
useRelationshipQuery
} from '@utils/queryHooks/relationship' } from '@utils/queryHooks/relationship'
import React, { useCallback } from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']
} }
const RelationshipOutgoing: React.FC<Props> = ({ id }) => { const RelationshipOutgoing = React.memo(
const { t } = useTranslation() ({ id }: Props) => {
const { t } = useTranslation()
const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }] const query = useRelationshipQuery({ id })
const query = hookRelationship({ id })
const queryClient = useQueryClient() const queryKeyRelationship: QueryKeyRelationship = ['Relationship', { id }]
const fireMutation = useCallback( const queryClient = useQueryClient()
({ type, state }: { type: 'follow' | 'block'; state: boolean }) => { const mutation = useRelationshipMutation({
return client<Mastodon.Relationship>({ onSuccess: res => {
method: 'post', haptics('Success')
instance: 'local', queryClient.setQueryData<Mastodon.Relationship[]>(
url: `accounts/${id}/${state ? 'un' : ''}${type}` queryKeyRelationship,
}) [res]
}, )
[] },
) onError: (err: any, { type }) => {
const mutation = useMutation(fireMutation, { haptics('Error')
onSuccess: res => { toast({
haptics('Success') type: 'error',
queryClient.setQueryData(queryKeyRelationship, res) message: t('common:toastMessage.error.message', {
}, function: t(`relationship:${type}.function`)
onError: (err: any, { type }) => { }),
haptics('Error') ...(err.status &&
toast({ typeof err.status === 'number' &&
type: 'error', err.data &&
message: t('common:toastMessage.error.message', { err.data.error &&
function: t(`relationship:${type}.function`) typeof err.data.error === 'string' && {
}), description: err.data.error
...(err.status && })
typeof err.status === 'number' && })
err.data && }
err.data.error && })
typeof err.data.error === 'string' && {
description: err.data.error
})
})
}
})
let content: string let content: string
let onPress: () => void let onPress: () => void
if (query.isError) { if (query.isError) {
content = t('relationship:button.error') content = t('relationship:button.error')
onPress = () => {} onPress = () => {}
} else {
if (query.data?.blocked_by) {
content = t('relationship:button.blocked_by')
onPress = () => null
} else { } else {
if (query.data?.blocking) { if (query.data?.blocked_by) {
content = t('relationship:button.blocking') content = t('relationship:button.blocked_by')
onPress = () => onPress = () => null
mutation.mutate({
type: 'block',
state: query.data?.blocking
})
} else { } else {
if (query.data?.following) { if (query.data?.blocking) {
content = t('relationship:button.following') content = t('relationship:button.blocking')
onPress = () => onPress = () =>
mutation.mutate({ mutation.mutate({
type: 'follow', id,
state: query.data?.following type: 'outgoing',
payload: {
action: 'block',
state: query.data?.blocking
}
}) })
} else { } else {
if (query.data?.requested) { if (query.data?.following) {
content = t('relationship:button.requested') content = t('relationship:button.following')
onPress = () => onPress = () =>
mutation.mutate({ mutation.mutate({
type: 'follow', id,
state: query.data?.requested type: 'outgoing',
payload: {
action: 'follow',
state: query.data?.following
}
}) })
} else { } else {
content = t('relationship:button.default') if (query.data?.requested) {
onPress = () => content = t('relationship:button.requested')
mutation.mutate({ onPress = () =>
type: 'follow', mutation.mutate({
state: false id,
}) type: 'outgoing',
payload: {
action: 'follow',
state: query.data?.requested
}
})
} else {
content = t('relationship:button.default')
onPress = () =>
mutation.mutate({
id,
type: 'outgoing',
payload: {
action: 'follow',
state: false
}
})
}
} }
} }
} }
} }
}
return ( return (
<Button <Button
type='text' type='text'
content={content} content={content}
onPress={onPress} onPress={onPress}
loading={query.isLoading || mutation.isLoading} loading={query.isLoading || mutation.isLoading}
disabled={query.isError || query.data?.blocked_by} disabled={query.isError || query.data?.blocked_by}
/> />
) )
} },
() => true
)
export default RelationshipOutgoing export default RelationshipOutgoing

View File

@ -13,13 +13,12 @@ import { RefreshControl, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import { InfiniteData } from 'react-query' import { InfiniteData } from 'react-query'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import hookTimeline, { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline, useTimelineQuery } from '@utils/queryHooks/timeline'
import { findIndex } from 'lodash'
export type TimelineData = type TimelineData =
| InfiniteData<{ | InfiniteData<{
toots: Mastodon.Status[] toots: Mastodon.Status[]
pointer?: number | undefined
pinnedLength?: number | undefined
}> }>
| undefined | undefined
@ -61,35 +60,33 @@ const Timeline: React.FC<Props> = ({
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
isFetchingNextPage isFetchingNextPage
} = hookTimeline({ } = useTimelineQuery({
...queryKeyParams, ...queryKeyParams,
options: { options: {
getPreviousPageParam: firstPage => { getPreviousPageParam: firstPage => {
return firstPage.toots.length return firstPage.length
? { ? {
direction: 'prev', direction: 'prev',
id: firstPage.toots[0].id id: firstPage[0].last_status
? firstPage[0].last_status.id
: firstPage[0].id
} }
: undefined : undefined
}, },
getNextPageParam: lastPage => { getNextPageParam: lastPage => {
return lastPage.toots.length return lastPage.length
? { ? {
direction: 'next', direction: 'next',
id: lastPage.toots[lastPage.toots.length - 1].id id: lastPage[lastPage.length - 1].last_status
? lastPage[lastPage.length - 1].last_status.id
: lastPage[lastPage.length - 1].id
} }
: undefined : undefined
} }
} }
}) })
const flattenData = data?.pages ? data.pages.flatMap(d => [...d?.toots]) : [] const flattenData = data?.pages ? data.pages.flatMap(d => [...d]) : []
const flattenPointer = data?.pages
? data.pages.flatMap(d => [d?.pointer])
: []
const flattenPinnedLength = data?.pages
? data.pages.flatMap(d => [d?.pinnedLength])
: []
// Clear unread notification badge // Clear unread notification badge
const dispatch = useDispatch() const dispatch = useDispatch()
@ -107,48 +104,41 @@ const Timeline: React.FC<Props> = ({
const flRef = useRef<FlatList<any>>(null) const flRef = useRef<FlatList<any>>(null)
useEffect(() => { useEffect(() => {
if (toot && isSuccess) { if (toot && isSuccess) {
const pointer = findIndex(flattenData, ['id', toot])
setTimeout(() => { setTimeout(() => {
flRef.current?.scrollToIndex({ flRef.current?.scrollToIndex({
index: flattenPointer[0]!, index: pointer,
viewOffset: 100 viewOffset: 100
}) })
}, 500) }, 500)
} }
}, [isSuccess]) }, [isSuccess, flattenData])
const keyExtractor = useCallback(({ id }) => id, []) const keyExtractor = useCallback(({ id }) => id, [])
const renderItem = useCallback( const renderItem = useCallback(({ item }) => {
({ item, index }) => { switch (page) {
switch (page) { case 'Conversations':
case 'Conversations': return <TimelineConversation conversation={item} queryKey={queryKey} />
return ( case 'Notifications':
<TimelineConversation conversation={item} queryKey={queryKey} /> return <TimelineNotifications notification={item} queryKey={queryKey} />
) default:
case 'Notifications': // if (item.poll) {
return ( // console.log('Timeline')
<TimelineNotifications notification={item} queryKey={queryKey} /> // console.log(item.poll)
) // }
default: return (
return ( <TimelineDefault
<TimelineDefault item={item}
item={item} queryKey={queryKey}
queryKey={queryKey} {...(queryKey[1].page === 'RemotePublic' && {
index={index} disableDetails: true,
{...(queryKey[1].page === 'RemotePublic' && { disableOnPress: true
disableDetails: true, })}
disableOnPress: true {...(toot === item.id && { highlighted: true })}
})} />
{...(flattenPinnedLength && )
flattenPinnedLength[0] && { }
pinnedLength: flattenPinnedLength[0] }, [])
})}
{...(toot === item.id && { highlighted: true })}
/>
)
}
},
[flattenPinnedLength[0]]
)
const ItemSeparatorComponent = useCallback( const ItemSeparatorComponent = useCallback(
({ leadingItem }) => ( ({ leadingItem }) => (
<ComponentSeparator <ComponentSeparator

View File

@ -1,16 +1,18 @@
import client from '@api/client'
import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback } from 'react' import React, { useCallback } from 'react'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import TimelineAvatar from '@components/Timelines/Timeline/Shared/Avatar'
import TimelineHeaderConversation from '@components/Timelines/Timeline/Shared/HeaderConversation'
import TimelineContent from '@components/Timelines/Timeline/Shared/Content'
import { StyleConstants } from '@utils/styles/constants'
import TimelineActions from '@components/Timelines/Timeline/Shared/Actions'
import client from '@root/api/client'
import { useMutation, useQueryClient } from 'react-query' import { useMutation, useQueryClient } from 'react-query'
import { useTheme } from '@root/utils/styles/ThemeManager' import { useSelector } from 'react-redux'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import TimelineActions from './Shared/Actions'
import TimelineAvatar from './Shared/Avatar'
import TimelineContent from './Shared/Content'
import TimelineHeaderConversation from './Shared/HeaderConversation'
import TimelinePoll from './Shared/Poll'
export interface Props { export interface Props {
conversation: Mastodon.Conversation conversation: Mastodon.Conversation
@ -23,6 +25,7 @@ const TimelineConversation: React.FC<Props> = ({
queryKey, queryKey,
highlighted = false highlighted = false
}) => { }) => {
const localAccount = useSelector(getLocalAccount)
const { theme } = useTheme() const { theme } = useTheme()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@ -88,6 +91,15 @@ const TimelineConversation: React.FC<Props> = ({
status={conversation.last_status} status={conversation.last_status}
highlighted={highlighted} highlighted={highlighted}
/> />
{conversation.last_status.poll && (
<TimelinePoll
queryKey={queryKey}
statusId={conversation.last_status.id}
poll={conversation.last_status.poll}
reblog={false}
sameAccount={conversation.last_status.id === localAccount?.id}
/>
)}
</View> </View>
) : null} ) : null}

View File

@ -17,8 +17,6 @@ import { useSelector } from 'react-redux'
export interface Props { export interface Props {
item: Mastodon.Status item: Mastodon.Status
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
index: number
pinnedLength?: number
highlighted?: boolean highlighted?: boolean
disableDetails?: boolean disableDetails?: boolean
disableOnPress?: boolean disableOnPress?: boolean
@ -28,8 +26,6 @@ export interface Props {
const TimelineDefault: React.FC<Props> = ({ const TimelineDefault: React.FC<Props> = ({
item, item,
queryKey, queryKey,
index,
pinnedLength,
highlighted = false, highlighted = false,
disableDetails = false, disableDetails = false,
disableOnPress = false disableOnPress = false
@ -53,7 +49,7 @@ const TimelineDefault: React.FC<Props> = ({
<Pressable style={styles.statusView} onPress={onPress}> <Pressable style={styles.statusView} onPress={onPress}>
{item.reblog ? ( {item.reblog ? (
<TimelineActioned action='reblog' account={item.account} /> <TimelineActioned action='reblog' account={item.account} />
) : pinnedLength && index < pinnedLength ? ( ) : item.isPinned ? (
<TimelineActioned action='pinned' account={item.account} /> <TimelineActioned action='pinned' account={item.account} />
) : null} ) : null}
@ -84,14 +80,15 @@ const TimelineDefault: React.FC<Props> = ({
disableDetails={disableDetails} disableDetails={disableDetails}
/> />
)} )}
{queryKey && actualStatus.poll && ( {queryKey && actualStatus.poll ? (
<TimelinePoll <TimelinePoll
queryKey={queryKey} queryKey={queryKey}
statusId={actualStatus.id}
poll={actualStatus.poll} poll={actualStatus.poll}
reblog={item.reblog ? true : false} reblog={item.reblog ? true : false}
sameAccount={actualStatus.account.id === localAccount?.id} sameAccount={actualStatus.account.id === localAccount?.id}
/> />
)} ) : null}
{!disableDetails && actualStatus.media_attachments.length > 0 && ( {!disableDetails && actualStatus.media_attachments.length > 0 && (
<TimelineAttachment status={actualStatus} /> <TimelineAttachment status={actualStatus} />
)} )}

View File

@ -82,6 +82,7 @@ const TimelineNotifications: React.FC<Props> = ({
{notification.status.poll && ( {notification.status.poll && (
<TimelinePoll <TimelinePoll
queryKey={queryKey} queryKey={queryKey}
statusId={notification.status.id}
poll={notification.status.poll} poll={notification.status.poll}
reblog={false} reblog={false}
sameAccount={notification.account.id === localAccount?.id} sameAccount={notification.account.id === localAccount?.id}

View File

@ -1,17 +1,18 @@
import client from '@api/client'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import {
MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native' import { ActionSheetIOS, Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -28,87 +29,18 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
state ? theme.primary : theme.secondary state ? theme.primary : theme.secondary
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback( const mutation = useTimelineMutation({
async ({ queryClient,
type, onMutate: true,
state onError: (err: any, params, oldData) => {
}: { const correctParam = params as MutationVarsTimelineUpdateStatusProperty
type: 'favourite' | 'reblog' | 'bookmark'
stateKey: 'favourited' | 'reblogged' | 'bookmarked'
state?: boolean
}) => {
return client<Mastodon.Status>({
method: 'post',
instance: 'local',
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
}) // bug in response from Mastodon
},
[]
)
const { mutate } = useMutation(fireMutation, {
onMutate: ({ type, stateKey, state }) => {
queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey)
haptics('Success')
switch (type) {
case 'favourite':
case 'reblog':
case 'bookmark':
queryClient.setQueryData<TimelineData>(queryKey, old => {
let tootIndex = -1
const pageIndex = findIndex(old?.pages, page => {
const tempIndex = findIndex(page.toots, [
queryKey[1].page === 'Notifications'
? 'status.id'
: reblog
? 'reblog.id'
: 'id',
status.id
])
if (tempIndex >= 0) {
tootIndex = tempIndex
return true
} else {
return false
}
})
if (pageIndex >= 0 && tootIndex >= 0) {
if (
(type === 'favourite' && queryKey[1].page === 'Favourites') ||
(type === 'bookmark' && queryKey[1].page === 'Bookmarks')
) {
old!.pages[pageIndex].toots.splice(tootIndex, 1)
} else {
if (queryKey[1].page === 'Notifications') {
old!.pages[pageIndex].toots[tootIndex].status[stateKey] =
typeof state === 'boolean' ? !state : true
} else {
if (reblog) {
old!.pages[pageIndex].toots[tootIndex].reblog![stateKey] =
typeof state === 'boolean' ? !state : true
} else {
old!.pages[pageIndex].toots[tootIndex][stateKey] =
typeof state === 'boolean' ? !state : true
}
}
}
}
return old
})
break
}
return oldData
},
onError: (err: any, { type }, oldData) => {
haptics('Error') haptics('Error')
toast({ toast({
type: 'error', type: 'error',
message: t('common:toastMessage.error.message', { message: t('common:toastMessage.error.message', {
function: t(`timeline:shared.actions.${type}.function`) function: t(
`timeline:shared.actions.${correctParam.payload.property}.function`
)
}), }),
...(err.status && ...(err.status &&
typeof err.status === 'number' && typeof err.status === 'number' &&
@ -132,28 +64,43 @@ const TimelineActions: React.FC<Props> = ({ queryKey, status, reblog }) => {
) )
const onPressReblog = useCallback( const onPressReblog = useCallback(
() => () =>
mutate({ mutation.mutate({
type: 'reblog', type: 'updateStatusProperty',
stateKey: 'reblogged', queryKey,
state: status.reblogged id: status.id,
reblog,
payload: {
property: 'reblogged',
currentValue: status.reblogged
}
}), }),
[status.reblogged] [status.reblogged]
) )
const onPressFavourite = useCallback( const onPressFavourite = useCallback(
() => () =>
mutate({ mutation.mutate({
type: 'favourite', type: 'updateStatusProperty',
stateKey: 'favourited', queryKey,
state: status.favourited id: status.id,
reblog,
payload: {
property: 'favourited',
currentValue: status.favourited
}
}), }),
[status.favourited] [status.favourited]
) )
const onPressBookmark = useCallback( const onPressBookmark = useCallback(
() => () =>
mutate({ mutation.mutate({
type: 'bookmark', type: 'updateStatusProperty',
stateKey: 'bookmarked', queryKey,
state: status.bookmarked id: status.id,
reblog,
payload: {
property: 'bookmarked',
currentValue: status.bookmarked
}
}), }),
[status.bookmarked] [status.bookmarked]
) )

View File

@ -1,18 +1,19 @@
import client from '@api/client'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import {
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
import React, { useCallback, useMemo } from 'react' import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, View } from 'react-native' import { Pressable, StyleSheet, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import HeaderSharedAccount from './HeaderShared/Account' import HeaderSharedAccount from './HeaderShared/Account'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedMuted from './HeaderShared/Muted'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -23,40 +24,9 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback(() => { const mutation = useTimelineMutation({
return client<Mastodon.Conversation>({ queryClient,
method: 'delete', onMutate: true,
instance: 'local',
url: `conversations/${conversation.id}`
})
}, [])
const { mutate } = useMutation(fireMutation, {
onMutate: () => {
queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey)
haptics('Success')
queryClient.setQueryData<TimelineData>(queryKey, old => {
let tootIndex = -1
const pageIndex = findIndex(old?.pages, page => {
const tempIndex = findIndex(page.toots, ['id', conversation.id])
if (tempIndex >= 0) {
tootIndex = tempIndex
return true
} else {
return false
}
})
if (pageIndex >= 0 && tootIndex >= 0) {
old!.pages[pageIndex].toots.splice(tootIndex, 1)
}
return old
})
return oldData
},
onError: (err: any, _, oldData) => { onError: (err: any, _, oldData) => {
haptics('Error') haptics('Error')
toast({ toast({
@ -79,7 +49,16 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
const { theme } = useTheme() const { theme } = useTheme()
const actionOnPress = useCallback(() => mutate(), []) const actionOnPress = useCallback(
() =>
mutation.mutate({
type: 'deleteItem',
source: 'conversations',
queryKey,
id: conversation.id
}),
[]
)
const actionChildren = useMemo( const actionChildren = useMemo(
() => ( () => (
@ -102,6 +81,7 @@ const HeaderConversation: React.FC<Props> = ({ queryKey, conversation }) => {
created_at={conversation.last_status?.created_at} created_at={conversation.last_status?.created_at}
/> />
) : null} ) : null}
<HeaderSharedMuted muted={conversation.last_status?.muted} />
</View> </View>
</View> </View>

View File

@ -14,6 +14,7 @@ import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedVisibility from './HeaderShared/Visibility' import HeaderSharedVisibility from './HeaderShared/Visibility'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import HeaderSharedMuted from './HeaderShared/Muted'
export interface Props { export interface Props {
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
@ -54,6 +55,7 @@ const TimelineHeaderDefault: React.FC<Props> = ({
<View style={styles.meta}> <View style={styles.meta}>
<HeaderSharedCreated created_at={status.created_at} /> <HeaderSharedCreated created_at={status.created_at} />
<HeaderSharedVisibility visibility={status.visibility} /> <HeaderSharedVisibility visibility={status.visibility} />
<HeaderSharedMuted muted={status.muted} />
<HeaderSharedApplication application={status.application} /> <HeaderSharedApplication application={status.application} />
</View> </View>
</View> </View>
@ -125,4 +127,7 @@ const styles = StyleSheet.create({
} }
}) })
export default React.memo(TimelineHeaderDefault, () => true) export default React.memo(
TimelineHeaderDefault,
(prev, next) => prev.status.muted !== next.status.muted
)

View File

@ -1,11 +1,13 @@
import client from '@api/client'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu' import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import {
import React, { useCallback } from 'react' QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
queryKey?: QueryKeyTimeline queryKey?: QueryKeyTimeline
@ -19,34 +21,10 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
setBottomSheetVisible setBottomSheetVisible
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback( const mutateion = useTimelineMutation({
async ({ type }: { type: 'mute' | 'block' | 'reports' }) => { queryClient,
switch (type) {
case 'mute':
case 'block':
return client<Mastodon.Account>({
method: 'post',
instance: 'local',
url: `accounts/${account.id}/${type}`
})
break
case 'reports':
return client<Mastodon.Account>({
method: 'post',
instance: 'local',
url: `reports`,
params: {
account_id: account.id!
}
})
break
}
},
[]
)
const { mutate } = useMutation(fireMutation, {
onSuccess: (_, { type }) => { onSuccess: (_, { type }) => {
haptics('Success') haptics('Success')
toast({ toast({
@ -91,7 +69,12 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutate({ type: 'mute' }) mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'mute' }
})
}} }}
iconFront='EyeOff' iconFront='EyeOff'
title={t('timeline:shared.header.default.actions.account.mute.button', { title={t('timeline:shared.header.default.actions.account.mute.button', {
@ -101,7 +84,12 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutate({ type: 'block' }) mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'block' }
})
}} }}
iconFront='XCircle' iconFront='XCircle'
title={t( title={t(
@ -114,7 +102,12 @@ const HeaderDefaultActionsAccount: React.FC<Props> = ({
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutate({ type: 'reports' }) mutateion.mutate({
type: 'updateAccountProperty',
queryKey,
id: account.id,
payload: { property: 'reports' }
})
}} }}
iconFront='Flag' iconFront='Flag'
title={t( title={t(

View File

@ -1,12 +1,15 @@
import client from '@api/client'
import MenuContainer from '@components/Menu/Container' import MenuContainer from '@components/Menu/Container'
import MenuHeader from '@components/Menu/Header' import MenuHeader from '@components/Menu/Header'
import MenuRow from '@components/Menu/Row' import MenuRow from '@components/Menu/Row'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import {
import React, { useCallback } from 'react' QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useMutation, useQueryClient } from 'react-query' import { Alert } from 'react-native'
import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -21,17 +24,8 @@ const HeaderDefaultActionsDomain: React.FC<Props> = ({
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback(() => { const mutation = useTimelineMutation({
return client<{}>({ queryClient,
method: 'post',
instance: 'local',
url: `domain_blocks`,
params: {
domain: domain!
}
})
}, [])
const { mutate } = useMutation(fireMutation, {
onSettled: () => { onSettled: () => {
toast({ toast({
type: 'success', type: 'success',
@ -52,8 +46,27 @@ const HeaderDefaultActionsDomain: React.FC<Props> = ({
/> />
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) Alert.alert(
mutate() t('timeline:shared.header.default.actions.domain.alert.title'),
t('timeline:shared.header.default.actions.domain.alert.message'),
[
{ text: t('common:buttons.cancel'), style: 'cancel' },
{
text: t(
'timeline:shared.header.default.actions.domain.alert.confirm'
),
style: 'destructive',
onPress: () => {
setBottomSheetVisible(false)
mutation.mutate({
type: 'domainBlock',
queryKey,
domain: domain
})
}
}
]
)
}} }}
iconFront='CloudOff' iconFront='CloudOff'
title={t(`timeline:shared.header.default.actions.domain.block.button`, { title={t(`timeline:shared.header.default.actions.domain.block.button`, {

View File

@ -1,15 +1,15 @@
import { findIndex } from 'lodash' import React from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Alert } from 'react-native' import { Alert } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
import client from '@api/client'
import haptics from '@components/haptics'
import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu' import { MenuContainer, MenuHeader, MenuRow } from '@components/Menu'
import { TimelineData } from '@components/Timelines/Timeline'
import { toast } from '@components/toast' import { toast } from '@components/toast'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import {
MutationVarsTimelineUpdateStatusProperty,
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
@ -26,92 +26,19 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
const { t } = useTranslation() const { t } = useTranslation()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback( const mutation = useTimelineMutation({
({ type, state }: { type: 'mute' | 'pin' | 'delete'; state?: boolean }) => { queryClient,
switch (type) { onMutate: true,
case 'mute': onError: (err: any, params, oldData) => {
case 'pin': const theFunction = (params as MutationVarsTimelineUpdateStatusProperty)
return client<Mastodon.Status>({ .payload
method: 'post', ? (params as MutationVarsTimelineUpdateStatusProperty).payload.property
instance: 'local', : 'delete'
url: `statuses/${status.id}/${state ? 'un' : ''}${type}`
}) // bug in response from Mastodon, but onMutate ignore the error in response
break
case 'delete':
return client<Mastodon.Status>({
method: 'delete',
instance: 'local',
url: `statuses/${status.id}`
})
break
}
},
[]
)
enum mapTypeToProp {
mute = 'muted',
pin = 'pinned'
}
const { mutate } = useMutation(fireMutation, {
onMutate: ({ type, state }) => {
queryClient.cancelQueries(queryKey)
const oldData = queryClient.getQueryData(queryKey)
switch (type) {
case 'mute':
case 'pin':
haptics('Success')
toast({
type: 'success',
message: t('common:toastMessage.success.message', {
function: t(
`timeline:shared.header.default.actions.status.${type}.function`
)
})
})
queryClient.setQueryData<TimelineData>(queryKey, old => {
let tootIndex = -1
const pageIndex = findIndex(old?.pages, page => {
const tempIndex = findIndex(page.toots, ['id', status.id])
if (tempIndex >= 0) {
tootIndex = tempIndex
return true
} else {
return false
}
})
if (pageIndex >= 0 && tootIndex >= 0) {
old!.pages[pageIndex].toots[tootIndex][mapTypeToProp[type]] =
typeof state === 'boolean' ? !state : true // State could be null from response
}
return old
})
break
case 'delete':
queryClient.setQueryData<TimelineData>(
queryKey,
old =>
old && {
...old,
pages: old?.pages.map(paging => ({
...paging,
toots: paging.toots.filter(toot => toot.id !== status.id)
}))
}
)
break
}
return oldData
},
onError: (err: any, { type }, oldData) => {
toast({ toast({
type: 'error', type: 'error',
message: t('common:toastMessage.error.message', { message: t('common:toastMessage.error.message', {
function: t( function: t(
`timeline:shared.header.default.actions.status.${type}.function` `timeline:shared.header.default.actions.status.${theFunction}.function`
) )
}), }),
...(err.status && ...(err.status &&
@ -134,7 +61,12 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutate({ type: 'delete' }) mutation.mutate({
type: 'deleteItem',
source: 'statuses',
queryKey,
id: status.id
})
}} }}
iconFront='Trash' iconFront='Trash'
title={t('timeline:shared.header.default.actions.status.delete.button')} title={t('timeline:shared.header.default.actions.status.delete.button')}
@ -154,29 +86,19 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
), ),
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
await client<Mastodon.Status>({ setBottomSheetVisible(false)
method: 'delete', const res = await mutation.mutateAsync({
instance: 'local', type: 'deleteItem',
url: `statuses/${status.id}` source: 'statuses',
queryKey,
id: status.id
}) })
.then(res => { if (res.id) {
queryClient.invalidateQueries(queryKey) navigation.navigate('Screen-Shared-Compose', {
setBottomSheetVisible(false) type: 'edit',
navigation.navigate('Screen-Shared-Compose', { incomingStatus: res
type: 'edit',
incomingStatus: res
})
})
.catch(() => {
toast({
type: 'error',
message: t('common:toastMessage.success.message', {
function: t(
`timeline:shared.header.default.actions.status.edit.function`
)
})
})
}) })
}
} }
} }
] ]
@ -188,7 +110,12 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutate({ type: 'mute', state: status.muted }) mutation.mutate({
type: 'updateStatusProperty',
queryKey,
id: status.id,
payload: { property: 'muted', currentValue: status.muted }
})
}} }}
iconFront='VolumeX' iconFront='VolumeX'
title={ title={
@ -206,7 +133,12 @@ const HeaderDefaultActionsStatus: React.FC<Props> = ({
<MenuRow <MenuRow
onPress={() => { onPress={() => {
setBottomSheetVisible(false) setBottomSheetVisible(false)
mutate({ type: 'pin', state: status.pinned }) mutation.mutate({
type: 'updateStatusProperty',
queryKey,
id: status.id,
payload: { property: 'pinned', currentValue: status.pinned }
})
}} }}
iconFront='Anchor' iconFront='Anchor'
title={ title={

View File

@ -7,6 +7,7 @@ import HeaderSharedApplication from './HeaderShared/Application'
import HeaderSharedCreated from './HeaderShared/Created' import HeaderSharedCreated from './HeaderShared/Created'
import HeaderSharedVisibility from './HeaderShared/Visibility' import HeaderSharedVisibility from './HeaderShared/Visibility'
import RelationshipIncoming from '@root/components/Relationship/Incoming' import RelationshipIncoming from '@root/components/Relationship/Incoming'
import HeaderSharedMuted from './HeaderShared/Muted'
export interface Props { export interface Props {
notification: Mastodon.Notification notification: Mastodon.Notification
@ -30,6 +31,7 @@ const TimelineHeaderNotification: React.FC<Props> = ({ notification }) => {
<HeaderSharedVisibility <HeaderSharedVisibility
visibility={notification.status?.visibility} visibility={notification.status?.visibility}
/> />
<HeaderSharedMuted muted={notification.status?.muted} />
<HeaderSharedApplication <HeaderSharedApplication
application={notification.status?.application} application={notification.status?.application}
/> />

View File

@ -0,0 +1,30 @@
import Icon from '@components/Icon'
import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager'
import React from 'react'
import { StyleSheet } from 'react-native'
export interface Props {
muted?: Mastodon.Status['muted']
}
const HeaderSharedMuted: React.FC<Props> = ({ muted }) => {
const { theme } = useTheme()
return muted ? (
<Icon
name='VolumeX'
size={StyleConstants.Font.Size.S}
color={theme.secondary}
style={styles.visibility}
/>
) : null
}
const styles = StyleSheet.create({
visibility: {
marginLeft: StyleConstants.Spacing.S
}
})
export default HeaderSharedMuted

View File

@ -1,22 +1,23 @@
import client from '@api/client'
import Button from '@components/Button' import Button from '@components/Button'
import haptics from '@components/haptics' import haptics from '@components/haptics'
import Icon from '@components/Icon' import Icon from '@components/Icon'
import relativeTime from '@components/relativeTime' import relativeTime from '@components/relativeTime'
import { TimelineData } from '@components/Timelines/Timeline'
import { ParseEmojis } from '@root/components/Parse' import { ParseEmojis } from '@root/components/Parse'
import { toast } from '@root/components/toast' import { toast } from '@root/components/toast'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline' import {
QueryKeyTimeline,
useTimelineMutation
} from '@utils/queryHooks/timeline'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { findIndex } from 'lodash'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Pressable, StyleSheet, Text, View } from 'react-native' import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useMutation, useQueryClient } from 'react-query' import { useQueryClient } from 'react-query'
export interface Props { export interface Props {
queryKey: QueryKeyTimeline queryKey: QueryKeyTimeline
statusId: Mastodon.Status['id']
poll: NonNullable<Mastodon.Status['poll']> poll: NonNullable<Mastodon.Status['poll']>
reblog: boolean reblog: boolean
sameAccount: boolean sameAccount: boolean
@ -24,6 +25,7 @@ export interface Props {
const TimelinePoll: React.FC<Props> = ({ const TimelinePoll: React.FC<Props> = ({
queryKey, queryKey,
statusId,
poll, poll,
reblog, reblog,
sameAccount sameAccount
@ -36,56 +38,9 @@ const TimelinePoll: React.FC<Props> = ({
) )
const queryClient = useQueryClient() const queryClient = useQueryClient()
const fireMutation = useCallback( const mutation = useTimelineMutation({
({ type }: { type: 'vote' | 'refresh' }) => { queryClient,
const formData = new FormData() onSuccess: true,
type === 'vote' &&
allOptions.forEach((o, i) => {
if (allOptions[i]) {
formData.append('choices[]', i.toString())
}
})
return client<Mastodon.Poll>({
method: type === 'vote' ? 'post' : 'get',
instance: 'local',
url: type === 'vote' ? `polls/${poll.id}/votes` : `polls/${poll.id}`,
...(type === 'vote' && { body: formData })
})
},
[allOptions]
)
const mutation = useMutation(fireMutation, {
onSuccess: (res) => {
queryClient.cancelQueries(queryKey)
queryClient.setQueryData<TimelineData>(queryKey, old => {
let tootIndex = -1
const pageIndex = findIndex(old?.pages, page => {
const tempIndex = findIndex(page.toots, [
reblog ? 'reblog.poll.id' : 'poll.id',
poll.id
])
if (tempIndex >= 0) {
tootIndex = tempIndex
return true
} else {
return false
}
})
if (pageIndex >= 0 && tootIndex >= 0) {
if (reblog) {
old!.pages[pageIndex].toots[tootIndex].reblog!.poll = res
} else {
old!.pages[pageIndex].toots[tootIndex].poll = res
}
}
return old
})
haptics('Success')
},
onError: (err: any) => { onError: (err: any) => {
haptics('Error') haptics('Error')
toast({ toast({
@ -110,7 +65,20 @@ const TimelinePoll: React.FC<Props> = ({
return ( return (
<View style={styles.button}> <View style={styles.button}>
<Button <Button
onPress={() => mutation.mutate({ type: 'vote' })} onPress={() =>
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
id: statusId,
reblog,
payload: {
property: 'poll',
id: poll.id,
type: 'vote',
options: allOptions
}
})
}
type='text' type='text'
content={t('shared.poll.meta.button.vote')} content={t('shared.poll.meta.button.vote')}
loading={mutation.isLoading} loading={mutation.isLoading}
@ -122,7 +90,19 @@ const TimelinePoll: React.FC<Props> = ({
return ( return (
<View style={styles.button}> <View style={styles.button}>
<Button <Button
onPress={() => mutation.mutate({ type: 'refresh' })} onPress={() =>
mutation.mutate({
type: 'updateStatusProperty',
queryKey,
id: statusId,
reblog,
payload: {
property: 'poll',
id: poll.id,
type: 'refresh'
}
})
}
type='text' type='text'
content={t('shared.poll.meta.button.refresh')} content={t('shared.poll.meta.button.refresh')}
loading={mutation.isLoading} loading={mutation.isLoading}

View File

@ -1,13 +1,13 @@
import { MenuRow } from '@components/Menu' import { MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import TimelineEmpty from '@root/components/Timelines/Timeline/Empty' import TimelineEmpty from '@root/components/Timelines/Timeline/Empty'
import hookLists from '@utils/queryHooks/lists' import { useListsQuery } from '@utils/queryHooks/lists'
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
const ScreenMeLists: React.FC = () => { const ScreenMeLists: React.FC = () => {
const navigation = useNavigation() const navigation = useNavigation()
const { status, data, refetch } = hookLists({}) const { status, data, refetch } = useListsQuery({})
const children = useMemo(() => { const children = useMemo(() => {
if (status === 'success') { if (status === 'success') {

View File

@ -3,13 +3,13 @@ import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuRow } from '@components/Menu' import { MenuContainer, MenuRow } from '@components/Menu'
import hookAnnouncement from '@utils/queryHooks/announcement' import { useAnnouncementQuery } from '@utils/queryHooks/announcement'
const Collections: React.FC = () => { const Collections: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
const navigation = useNavigation() const navigation = useNavigation()
const { data, isFetching } = hookAnnouncement({ showAll: true }) const { data, isFetching } = useAnnouncementQuery({ showAll: true })
const announcementContent = useMemo(() => { const announcementContent = useMemo(() => {
if (data) { if (data) {

View File

@ -1,6 +1,6 @@
import AccountHeader from '@screens/Shared/Account/Header' import AccountHeader from '@screens/Shared/Account/Header'
import AccountInformation from '@screens/Shared/Account/Information' import AccountInformation from '@screens/Shared/Account/Information'
import hookAccount from '@utils/queryHooks/account' import { useAccountQuery } from '@utils/queryHooks/account'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } from '@utils/slices/instancesSlice'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useSelector } from 'react-redux' import { useSelector } from 'react-redux'
@ -11,7 +11,7 @@ export interface Props {
const MyInfo: React.FC<Props> = ({ setData }) => { const MyInfo: React.FC<Props> = ({ setData }) => {
const localAccount = useSelector(getLocalAccount) const localAccount = useSelector(getLocalAccount)
const { data } = hookAccount({ id: localAccount!.id }) const { data } = useAccountQuery({ id: localAccount!.id })
useEffect(() => { useEffect(() => {
if (data) { if (data) {

View File

@ -1,13 +1,20 @@
import { MenuContainer, MenuRow } from '@components/Menu'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { MenuContainer, MenuRow } from '@components/Menu'
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { t } = useTranslation('meRoot') const { t } = useTranslation('meRoot')
const navigation = useNavigation() const navigation = useNavigation()
const [loadingState, setLoadingState] = React.useState(false)
React.useEffect(() => {
const timer = setTimeout(() => {
setLoadingState(!loadingState)
}, 5000)
return () => clearTimeout(timer)
}, [loadingState])
return ( return (
<MenuContainer> <MenuContainer>
<MenuRow <MenuRow

View File

@ -1,7 +1,7 @@
import Button from '@components/Button' import Button from '@components/Button'
import ComponentInstance from '@components/Instance' import ComponentInstance from '@components/Instance'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import hookAccountCheck from '@utils/queryHooks/accountCheck' import { useAccountCheckQuery } from '@utils/queryHooks/accountCheck'
import { import {
getLocalActiveIndex, getLocalActiveIndex,
getLocalInstances, getLocalInstances,
@ -31,7 +31,7 @@ const AccountButton: React.FC<Props> = ({
const queryClient = useQueryClient() const queryClient = useQueryClient()
const navigation = useNavigation() const navigation = useNavigation()
const dispatch = useDispatch() const dispatch = useDispatch()
const { isLoading, data } = hookAccountCheck({ const { isLoading, data } = useAccountCheckQuery({
id: instance.account.id, id: instance.account.id,
index, index,
options: { retry: false } options: { retry: false }

View File

@ -1,7 +1,7 @@
import BottomSheet from '@components/BottomSheet' import BottomSheet from '@components/BottomSheet'
import { HeaderRight } from '@components/Header' import { HeaderRight } from '@components/Header'
import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount' import HeaderDefaultActionsAccount from '@components/Timelines/Timeline/Shared/HeaderDefault/ActionsAccount'
import hookAccount from '@utils/queryHooks/account' import { useAccountQuery } from '@utils/queryHooks/account'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } from '@utils/slices/instancesSlice'
import React, { useEffect, useReducer, useState } from 'react' import React, { useEffect, useReducer, useState } from 'react'
import Animated, { import Animated, {
@ -28,7 +28,7 @@ const ScreenSharedAccount: React.FC<SharedAccountProp> = ({
navigation navigation
}) => { }) => {
const localAccount = useSelector(getLocalAccount) const localAccount = useSelector(getLocalAccount)
const { data } = hookAccount({ id: account.id }) const { data } = useAccountQuery({ id: account.id })
const scrollY = useSharedValue(0) const scrollY = useSharedValue(0)
const [accountState, accountDispatch] = useReducer( const [accountState, accountDispatch] = useReducer(

View File

@ -1,7 +1,7 @@
import Button from '@components/Button' import Button from '@components/Button'
import { RelationshipOutgoing } from '@components/Relationship' import { RelationshipOutgoing } from '@components/Relationship'
import { useNavigation } from '@react-navigation/native' import { useNavigation } from '@react-navigation/native'
import hookRelationship from '@utils/queryHooks/relationship' import { useRelationshipQuery } from '@utils/queryHooks/relationship'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import React from 'react' import React from 'react'
import { StyleSheet } from 'react-native' import { StyleSheet } from 'react-native'
@ -12,7 +12,7 @@ export interface Props {
const Conversation = ({ account }: { account: Mastodon.Account }) => { const Conversation = ({ account }: { account: Mastodon.Account }) => {
const navigation = useNavigation() const navigation = useNavigation()
const query = hookRelationship({ id: account.id }) const query = useRelationshipQuery({ id: account.id })
return query.data && !query.data.blocked_by ? ( return query.data && !query.data.blocked_by ? (
<Button <Button

View File

@ -4,7 +4,10 @@ import haptics from '@components/haptics'
import { ParseHTML } from '@components/Parse' import { ParseHTML } from '@components/Parse'
import relativeTime from '@components/relativeTime' import relativeTime from '@components/relativeTime'
import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs' import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'
import hookAnnouncement from '@utils/queryHooks/announcement' import {
useAnnouncementMutation,
useAnnouncementQuery
} from '@utils/queryHooks/announcement'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useState } from 'react' import React, { useCallback, useEffect, useState } from 'react'
@ -20,7 +23,6 @@ import {
import { Chase } from 'react-native-animated-spinkit' import { Chase } from 'react-native-animated-spinkit'
import { FlatList, ScrollView } from 'react-native-gesture-handler' import { FlatList, ScrollView } from 'react-native-gesture-handler'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import { useMutation } from 'react-query'
import { SharedAnnouncementsProp } from './sharedScreens' import { SharedAnnouncementsProp } from './sharedScreens'
const fireMutation = async ({ const fireMutation = async ({
@ -61,7 +63,7 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
const [index, setIndex] = useState(0) const [index, setIndex] = useState(0)
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const { data, refetch } = hookAnnouncement({ const query = useAnnouncementQuery({
showAll, showAll,
options: { options: {
select: announcements => select: announcements =>
@ -70,18 +72,18 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
) )
} }
}) })
const queryMutation = useMutation(fireMutation, { const mutation = useAnnouncementMutation({
onSettled: () => { onSettled: () => {
haptics('Success') haptics('Success')
refetch() query.refetch()
} }
}) })
useEffect(() => { useEffect(() => {
if (!showAll && data?.length === 0) { if (!showAll && query.data?.length === 0) {
navigation.goBack() navigation.goBack()
} }
}, [data]) }, [query.data])
const renderItem = useCallback( const renderItem = useCallback(
({ item, index }: { item: Mastodon.Announcement; index: number }) => ( ({ item, index }: { item: Mastodon.Announcement; index: number }) => (
@ -132,8 +134,8 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
} }
]} ]}
onPress={() => onPress={() =>
queryMutation.mutate({ mutation.mutate({
announcementId: item.id, id: item.id,
type: 'reaction', type: 'reaction',
name: reaction.name, name: reaction.name,
me: reaction.me me: reaction.me
@ -172,13 +174,13 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
<Button <Button
type='text' type='text'
content={item.read ? '已读' : '标记阅读'} content={item.read ? '已读' : '标记阅读'}
loading={queryMutation.isLoading} loading={mutation.isLoading}
disabled={item.read} disabled={item.read}
onPress={() => onPress={() =>
!item.read && !item.read &&
queryMutation.mutate({ mutation.mutate({
type: 'dismiss', id: item.id,
announcementId: item.id type: 'dismiss'
}) })
} }
/> />
@ -221,7 +223,7 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
</View> </View>
<FlatList <FlatList
horizontal horizontal
data={data} data={query.data}
pagingEnabled pagingEnabled
renderItem={renderItem} renderItem={renderItem}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
@ -229,9 +231,9 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
ListEmptyComponent={ListEmptyComponent} ListEmptyComponent={ListEmptyComponent}
/> />
<View style={[styles.indicators, { height: bottomTabBarHeight }]}> <View style={[styles.indicators, { height: bottomTabBarHeight }]}>
{data && data.length > 1 ? ( {query.data && query.data.length > 1 ? (
<> <>
{data.map((d, i) => ( {query.data.map((d, i) => (
<View <View
key={i} key={i}
style={[ style={[
@ -239,7 +241,8 @@ const ScreenSharedAnnouncements: React.FC<SharedAnnouncementsProp> = ({
{ {
borderColor: theme.primary, borderColor: theme.primary,
backgroundColor: i === index ? theme.primary : undefined, backgroundColor: i === index ? theme.primary : undefined,
marginLeft: i === data.length ? 0 : StyleConstants.Spacing.S marginLeft:
i === query.data.length ? 0 : StyleConstants.Spacing.S
} }
]} ]}
/> />

View File

@ -4,6 +4,7 @@ import { store } from '@root/store'
import layoutAnimation from '@root/utils/styles/layoutAnimation' import layoutAnimation from '@root/utils/styles/layoutAnimation'
import formatText from '@screens/Shared/Compose/formatText' import formatText from '@screens/Shared/Compose/formatText'
import ComposeRoot from '@screens/Shared/Compose/Root' import ComposeRoot from '@screens/Shared/Compose/Root'
import { QueryKeyTimeline } from '@utils/queryHooks/timeline'
import { getLocalAccount } from '@utils/slices/instancesSlice' import { getLocalAccount } from '@utils/slices/instancesSlice'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
@ -166,7 +167,11 @@ const Compose: React.FC<SharedComposeProp> = ({
composePost(params, composeState) composePost(params, composeState)
.then(() => { .then(() => {
haptics('Success') haptics('Success')
queryClient.invalidateQueries(['Following', {}]) const queryKey: QueryKeyTimeline = [
'Timeline',
{ page: 'Following' }
]
queryClient.invalidateQueries(queryKey)
if ( if (
params?.type && params?.type &&
(params.type === 'reply' || params.type === 'conversation') (params.type === 'reply' || params.type === 'conversation')

View File

@ -1,5 +1,5 @@
import hookEmojis from '@utils/queryHooks/emojis' import { useEmojisQuery } from '@utils/queryHooks/emojis'
import hookSearch from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import { forEach, groupBy, sortBy } from 'lodash' import { forEach, groupBy, sortBy } from 'lodash'
@ -17,7 +17,7 @@ const ComposeRoot: React.FC = () => {
const { composeState, composeDispatch } = useContext(ComposeContext) const { composeState, composeDispatch } = useContext(ComposeContext)
const { isFetching, data, refetch } = hookSearch({ const { isFetching, data, refetch } = useSearchQuery({
type: type:
composeState.tag?.type === 'accounts' || composeState.tag?.type === 'accounts' ||
composeState.tag?.type === 'hashtags' composeState.tag?.type === 'hashtags'
@ -37,7 +37,7 @@ const ComposeRoot: React.FC = () => {
} }
}, [composeState.tag]) }, [composeState.tag])
const { data: emojisData } = hookEmojis({}) const { data: emojisData } = useEmojisQuery({})
useEffect(() => { useEffect(() => {
if (emojisData && emojisData.length) { if (emojisData && emojisData.length) {
let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = [] let sortedEmojis: { title: string; data: Mastodon.Emoji[] }[] = []

View File

@ -1,12 +1,12 @@
import ComponentAccount from '@components/Account' import ComponentAccount from '@components/Account'
import ComponentSeparator from '@components/Separator' import ComponentSeparator from '@components/Separator'
import TimelineEmpty from '@components/Timelines/Timeline/Empty' import TimelineEmpty from '@components/Timelines/Timeline/Empty'
import TimelineEnd from '@root/components/Timelines/Timeline/End' import TimelineEnd from '@components/Timelines/Timeline/End'
import { useScrollToTop } from '@react-navigation/native' import { useScrollToTop } from '@react-navigation/native'
import { useRelationshipsQuery } from '@utils/queryHooks/relationships'
import React, { useCallback, useMemo, useRef } from 'react' import React, { useCallback, useMemo, useRef } from 'react'
import { RefreshControl, StyleSheet } from 'react-native' import { RefreshControl, StyleSheet } from 'react-native'
import { FlatList } from 'react-native-gesture-handler' import { FlatList } from 'react-native-gesture-handler'
import hookRelationships from '@utils/queryHooks/relationships'
export interface Props { export interface Props {
id: Mastodon.Account['id'] id: Mastodon.Account['id']
@ -22,7 +22,7 @@ const RelationshipsList: React.FC<Props> = ({ id, type }) => {
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
isFetchingNextPage isFetchingNextPage
} = hookRelationships({ } = useRelationshipsQuery({
type, type,
id, id,
options: { options: {

View File

@ -2,7 +2,7 @@ import { useNavigation } from '@react-navigation/native'
import ComponentAccount from '@root/components/Account' import ComponentAccount from '@root/components/Account'
import ComponentSeparator from '@root/components/Separator' import ComponentSeparator from '@root/components/Separator'
import TimelineDefault from '@root/components/Timelines/Timeline/Default' import TimelineDefault from '@root/components/Timelines/Timeline/Default'
import hookSearch from '@utils/queryHooks/search' import { useSearchQuery } from '@utils/queryHooks/search'
import { StyleConstants } from '@utils/styles/constants' import { StyleConstants } from '@utils/styles/constants'
import { useTheme } from '@utils/styles/ThemeManager' import { useTheme } from '@utils/styles/ThemeManager'
import React, { useCallback, useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
@ -23,7 +23,7 @@ export interface Props {
const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => { const ScreenSharedSearch: React.FC<Props> = ({ searchTerm }) => {
const navigation = useNavigation() const navigation = useNavigation()
const { theme } = useTheme() const { theme } = useTheme()
const { status, data, refetch } = hookSearch({ const { status, data, refetch } = useSearchQuery({
term: searchTerm, term: searchTerm,
options: { enabled: false } options: { enabled: false }
}) })

View File

@ -14,7 +14,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
}) })
} }
const hookAccount = <TData = Mastodon.Account>({ const useAccountQuery = <TData = Mastodon.Account>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKey[1] & {
@ -24,4 +24,4 @@ const hookAccount = <TData = Mastodon.Account>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookAccount export { useAccountQuery }

View File

@ -22,7 +22,7 @@ const queryFunction = async ({ queryKey }: { queryKey: QueryKey }) => {
}) })
} }
const hookAccountCheck = <TData = Mastodon.Account>({ const useAccountCheckQuery = <TData = Mastodon.Account>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKey[1] & {
@ -32,4 +32,4 @@ const hookAccountCheck = <TData = Mastodon.Account>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookAccountCheck export { useAccountCheckQuery }

View File

@ -1,10 +1,15 @@
import client from '@api/client' import client from '@api/client'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import {
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from 'react-query'
type QueryKey = ['Announcements', { showAll?: boolean }] type QueryKeyAnnouncement = ['Announcements', { showAll?: boolean }]
const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => { const queryFunction = ({ queryKey }: { queryKey: QueryKeyAnnouncement }) => {
const { showAll } = queryKey[1] const { showAll } = queryKey[1]
return client<Mastodon.Announcement[]>({ return client<Mastodon.Announcement[]>({
@ -19,14 +24,52 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
}) })
} }
const hookAnnouncement = <TData = Mastodon.Announcement[]>({ const useAnnouncementQuery = <TData = Mastodon.Announcement[]>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKeyAnnouncement[1] & {
options?: UseQueryOptions<Mastodon.Announcement[], AxiosError, TData> options?: UseQueryOptions<Mastodon.Announcement[], AxiosError, TData>
}) => { }) => {
const queryKey: QueryKey = ['Announcements', { ...queryKeyParams }] const queryKey: QueryKeyAnnouncement = [
'Announcements',
{ ...queryKeyParams }
]
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookAnnouncement type MutationVarsAnnouncement = {
id: Mastodon.Announcement['id']
type: 'reaction' | 'dismiss'
name?: Mastodon.AnnouncementReaction['name']
me?: boolean
}
const mutationFunction = async ({
id,
type,
name,
me
}: MutationVarsAnnouncement) => {
switch (type) {
case 'reaction':
return client<{}>({
method: me ? 'delete' : 'put',
instance: 'local',
url: `announcements/${id}/reactions/${name}`
})
case 'dismiss':
return client<{}>({
method: 'post',
instance: 'local',
url: `announcements/${id}/dismiss`
})
}
}
const useAnnouncementMutation = (
options: UseMutationOptions<{}, AxiosError, MutationVarsAnnouncement>
) => {
return useMutation(mutationFunction, options)
}
export { useAnnouncementQuery, useAnnouncementMutation }

View File

@ -21,7 +21,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
}) })
} }
const hookApps = <TData = Mastodon.Apps>({ const useAppsQuery = <TData = Mastodon.Apps>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKey[1] & {
@ -31,4 +31,4 @@ const hookApps = <TData = Mastodon.Apps>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookApps export { useAppsQuery }

View File

@ -12,7 +12,7 @@ const queryFunction = () => {
}) })
} }
const hookEmojis = <TData = Mastodon.Emoji[]>({ const useEmojisQuery = <TData = Mastodon.Emoji[]>({
options options
}: { }: {
options?: UseQueryOptions<Mastodon.Emoji[], AxiosError, TData> options?: UseQueryOptions<Mastodon.Emoji[], AxiosError, TData>
@ -21,4 +21,4 @@ const hookEmojis = <TData = Mastodon.Emoji[]>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookEmojis export { useEmojisQuery }

View File

@ -15,7 +15,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
}) })
} }
const hookInstance = <TData = Mastodon.Instance>({ const useInstanceQuery = <TData = Mastodon.Instance>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKey[1] & {
@ -25,4 +25,4 @@ const hookInstance = <TData = Mastodon.Instance>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookInstance export { useInstanceQuery }

View File

@ -12,7 +12,7 @@ const queryFunction = () => {
}) })
} }
const hookLists = <TData = Mastodon.List[]>({ const useListsQuery = <TData = Mastodon.List[]>({
options options
}: { }: {
options?: UseQueryOptions<Mastodon.List[], AxiosError, TData> options?: UseQueryOptions<Mastodon.List[], AxiosError, TData>
@ -21,4 +21,4 @@ const hookLists = <TData = Mastodon.List[]>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookLists export { useListsQuery }

View File

@ -1,6 +1,11 @@
import client from '@api/client' import client from '@api/client'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { useQuery, UseQueryOptions } from 'react-query' import {
useMutation,
UseMutationOptions,
useQuery,
UseQueryOptions
} from 'react-query'
export type QueryKeyRelationship = [ export type QueryKeyRelationship = [
'Relationship', 'Relationship',
@ -20,7 +25,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKeyRelationship }) => {
}) })
} }
const hookRelationship = ({ const useRelationshipQuery = ({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKeyRelationship[1] & { }: QueryKeyRelationship[1] & {
@ -37,4 +42,45 @@ const hookRelationship = ({
}) })
} }
export default hookRelationship type MutationVarsRelationship =
| {
id: Mastodon.Account['id']
type: 'incoming'
payload: { action: 'authorize' | 'reject' }
}
| {
id: Mastodon.Account['id']
type: 'outgoing'
payload: { action: 'follow' | 'block'; state: boolean }
}
const mutationFunction = async (params: MutationVarsRelationship) => {
switch (params.type) {
case 'incoming':
return client<Mastodon.Relationship>({
method: 'post',
instance: 'local',
url: `follow_requests/${params.id}/${params.payload.action}`
})
case 'outgoing':
return client<Mastodon.Relationship>({
method: 'post',
instance: 'local',
url: `accounts/${params.id}/${params.payload.state ? 'un' : ''}${
params.payload.action
}`
})
}
}
const useRelationshipMutation = (
options: UseMutationOptions<
Mastodon.Relationship,
AxiosError,
MutationVarsRelationship
>
) => {
return useMutation(mutationFunction, options)
}
export { useRelationshipQuery, useRelationshipMutation }

View File

@ -33,7 +33,7 @@ const queryFunction = ({
}) })
} }
const hookRelationships = <TData = Mastodon.Account[]>({ const useRelationshipsQuery = <TData = Mastodon.Account[]>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKey[1] & {
@ -43,4 +43,4 @@ const hookRelationships = <TData = Mastodon.Account[]>({
return useInfiniteQuery(queryKey, queryFunction, options) return useInfiniteQuery(queryKey, queryFunction, options)
} }
export default hookRelationships export { useRelationshipsQuery }

View File

@ -28,7 +28,7 @@ const queryFunction = ({ queryKey }: { queryKey: QueryKey }) => {
}) })
} }
const hookSearch = <TData = SearchResult>({ const useSearchQuery = <TData = SearchResult>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKey[1] & { }: QueryKey[1] & {
@ -38,4 +38,4 @@ const hookSearch = <TData = SearchResult>({
return useQuery(queryKey, queryFunction, options) return useQuery(queryKey, queryFunction, options)
} }
export default hookSearch export { useSearchQuery }

View File

@ -1,7 +1,16 @@
import client from '@api/client' import client from '@api/client'
import haptics from '@components/haptics'
import { AxiosError } from 'axios' import { AxiosError } from 'axios'
import { uniqBy } from 'lodash' import { uniqBy } from 'lodash'
import { useInfiniteQuery, UseInfiniteQueryOptions } from 'react-query' import {
MutationOptions,
QueryClient,
useInfiniteQuery,
UseInfiniteQueryOptions,
useMutation
} from 'react-query'
import deleteItem from './timeline/deleteItem'
import updateStatusProperty from './timeline/updateStatusProperty'
export type QueryKeyTimeline = [ export type QueryKeyTimeline = [
'Timeline', 'Timeline',
@ -14,7 +23,7 @@ export type QueryKeyTimeline = [
} }
] ]
const queryFunction = async ({ const queryFunction = ({
queryKey, queryKey,
pageParam pageParam
}: { }: {
@ -22,7 +31,6 @@ const queryFunction = async ({
pageParam?: { direction: 'prev' | 'next'; id: Mastodon.Status['id'] } pageParam?: { direction: 'prev' | 'next'; id: Mastodon.Status['id'] }
}) => { }) => {
const { page, account, hashtag, list, toot } = queryKey[1] const { page, account, hashtag, list, toot } = queryKey[1]
let res
let params: { [key: string]: string } = {} let params: { [key: string]: string } = {}
if (pageParam) { if (pageParam) {
@ -46,74 +54,51 @@ const queryFunction = async ({
switch (page) { switch (page) {
case 'Following': case 'Following':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: 'timelines/home', url: 'timelines/home',
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Local': case 'Local':
params.local = 'true' return client<Mastodon.Status[]>({
res = await client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: 'timelines/public', url: 'timelines/public',
params params: {
}) ...params,
return Promise.resolve({ local: 'true'
toots: res, }
pointer: undefined,
pinnedLength: undefined
}) })
case 'LocalPublic': case 'LocalPublic':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: 'timelines/public', url: 'timelines/public',
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'RemotePublic': case 'RemotePublic':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'remote', instance: 'remote',
url: 'timelines/public', url: 'timelines/public',
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Notifications': case 'Notifications':
res = await client<Mastodon.Notification[]>({ return client<Mastodon.Notification[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: 'notifications', url: 'notifications',
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Account_Default': case 'Account_Default':
if (pageParam && pageParam.direction === 'next') { if (pageParam && pageParam.direction === 'next') {
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
@ -122,53 +107,41 @@ const queryFunction = async ({
...params ...params
} }
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
} else { } else {
res = await client<Mastodon.Status[]>({ return client<(Mastodon.Status & { isPinned: boolean })[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params: { params: {
pinned: 'true' pinned: 'true'
} }
}) }).then(async res1 => {
const pinnedLength = res.length let toots = res1.map(status => {
let toots = res status.isPinned = true
res = await client<Mastodon.Status[]>({ return status
method: 'get', })
instance: 'local', const res2 = await client<Mastodon.Status[]>({
url: `accounts/${account}/statuses`, method: 'get',
params: { instance: 'local',
exclude_replies: 'true' url: `accounts/${account}/statuses`,
} params: {
}) exclude_replies: 'true'
toots = uniqBy([...toots, ...res], 'id') }
return Promise.resolve({ })
toots: toots, return uniqBy([...toots, ...res2], 'id')
pointer: undefined,
pinnedLength
}) })
} }
case 'Account_All': case 'Account_All':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Account_Media': case 'Account_Media':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `accounts/${account}/statuses`, url: `accounts/${account}/statuses`,
@ -177,100 +150,62 @@ const queryFunction = async ({
...params ...params
} }
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Hashtag': case 'Hashtag':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `timelines/tag/${hashtag}`, url: `timelines/tag/${hashtag}`,
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Conversations': case 'Conversations':
res = await client<Mastodon.Conversation[]>({ return client<Mastodon.Conversation[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `conversations`, url: `conversations`,
params params
}) })
if (pageParam) {
// Bug in pull to refresh in conversations
res = res.filter(b => b.id !== pageParam.id)
}
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Bookmarks': case 'Bookmarks':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `bookmarks`, url: `bookmarks`,
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Favourites': case 'Favourites':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `favourites`, url: `favourites`,
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'List': case 'List':
res = await client<Mastodon.Status[]>({ return client<Mastodon.Status[]>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `timelines/list/${list}`, url: `timelines/list/${list}`,
params params
}) })
return Promise.resolve({
toots: res,
pointer: undefined,
pinnedLength: undefined
})
case 'Toot': case 'Toot':
res = await client<Mastodon.Status>({ return client<Mastodon.Status>({
method: 'get', method: 'get',
instance: 'local', instance: 'local',
url: `statuses/${toot}` url: `statuses/${toot}`
}) }).then(async res1 => {
const theToot = res const res2 = await client<{
res = await client<{ ancestors: Mastodon.Status[]
ancestors: Mastodon.Status[] descendants: Mastodon.Status[]
descendants: Mastodon.Status[] }>({
}>({ method: 'get',
method: 'get', instance: 'local',
instance: 'local', url: `statuses/${toot}/context`
url: `statuses/${toot}/context` })
}) return [...res2.ancestors, res1, ...res2.descendants]
return Promise.resolve({
toots: [...res.ancestors, theToot, ...res.descendants],
pointer: res.ancestors.length,
pinnedLength: undefined
}) })
default: default:
return Promise.reject() return Promise.reject()
@ -278,7 +213,8 @@ const queryFunction = async ({
} }
type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never
const hookTimeline = <TData = Unpromise<ReturnType<typeof queryFunction>>>({ export type TimelineData = Unpromise<ReturnType<typeof queryFunction>>
const useTimelineQuery = <TData = TimelineData>({
options, options,
...queryKeyParams ...queryKeyParams
}: QueryKeyTimeline[1] & { }: QueryKeyTimeline[1] & {
@ -288,4 +224,199 @@ const hookTimeline = <TData = Unpromise<ReturnType<typeof queryFunction>>>({
return useInfiniteQuery(queryKey, queryFunction, options) return useInfiniteQuery(queryKey, queryFunction, options)
} }
export default hookTimeline // --- Separator ---
enum MapPropertyToUrl {
bookmarked = 'bookmark',
favourited = 'favourite',
muted = 'mute',
pinned = 'pin',
reblogged = 'reblog'
}
export type MutationVarsTimelineUpdateStatusProperty = {
// This is status in general, including "status" inside conversation and notification
type: 'updateStatusProperty'
queryKey: QueryKeyTimeline
id: Mastodon.Status['id'] | Mastodon.Poll['id']
reblog?: boolean
payload:
| {
property: 'bookmarked' | 'favourited' | 'muted' | 'pinned' | 'reblogged'
currentValue: boolean
}
| {
property: 'poll'
id: Mastodon.Poll['id']
type: 'vote' | 'refresh'
options?: boolean[]
data?: Mastodon.Poll
}
}
export type MutationVarsTimelineUpdateAccountProperty = {
// This is status in general, including "status" inside conversation and notification
type: 'updateAccountProperty'
queryKey?: QueryKeyTimeline
id: Mastodon.Account['id']
payload: {
property: 'mute' | 'block' | 'reports'
}
}
export type MutationVarsTimelineDeleteItem = {
// This is for deleting status and conversation
type: 'deleteItem'
source: 'statuses' | 'conversations'
queryKey: QueryKeyTimeline
id: Mastodon.Conversation['id']
}
export type MutationVarsTimelineDomainBlock = {
// This is for deleting status and conversation
type: 'domainBlock'
queryKey: QueryKeyTimeline
domain: string
}
export type MutationVarsTimeline =
| MutationVarsTimelineUpdateStatusProperty
| MutationVarsTimelineUpdateAccountProperty
| MutationVarsTimelineDeleteItem
| MutationVarsTimelineDomainBlock
const mutationFunction = async (params: MutationVarsTimeline) => {
switch (params.type) {
case 'updateStatusProperty':
switch (params.payload.property) {
case 'poll':
const formData = new FormData()
params.payload.type === 'vote' &&
params.payload.options!.forEach((option, index) => {
if (option) {
formData.append('choices[]', index.toString())
}
})
return client<Mastodon.Poll>({
method: params.payload.type === 'vote' ? 'post' : 'get',
instance: 'local',
url:
params.payload.type === 'vote'
? `polls/${params.payload.id}/votes`
: `polls/${params.payload.id}`,
...(params.payload.type === 'vote' && { body: formData })
})
default:
return client<Mastodon.Status>({
method: 'post',
instance: 'local',
url: `statuses/${params.id}/${
params.payload.currentValue ? 'un' : ''
}${MapPropertyToUrl[params.payload.property]}`
})
}
case 'updateAccountProperty':
switch (params.payload.property) {
case 'block':
case 'mute':
return client<Mastodon.Account>({
method: 'post',
instance: 'local',
url: `accounts/${params.id}/${params.payload.property}`
})
case 'reports':
return client<Mastodon.Account>({
method: 'post',
instance: 'local',
url: `reports`,
params: {
account_id: params.id
}
})
}
case 'deleteItem':
return client<Mastodon.Conversation>({
method: 'delete',
instance: 'local',
url: `${params.source}/${params.id}`
})
case 'domainBlock':
return client<any>({
method: 'post',
instance: 'local',
url: `domain_blocks`,
params: {
domain: params.domain
}
})
}
}
type MutationOptionsTimeline = MutationOptions<
Mastodon.Conversation | Mastodon.Notification | Mastodon.Status,
AxiosError,
MutationVarsTimeline
>
const useTimelineMutation = ({
queryClient,
onError,
onMutate,
onSettled,
onSuccess
}: {
queryClient: QueryClient
onError?: MutationOptionsTimeline['onError']
onMutate?: boolean
onSettled?: MutationOptionsTimeline['onSettled']
onSuccess?: MutationOptionsTimeline['onSuccess'] | boolean
}) => {
return useMutation<
Mastodon.Conversation | Mastodon.Notification | Mastodon.Status,
AxiosError,
MutationVarsTimeline
>(mutationFunction, {
onError,
onSettled,
...(typeof onSuccess === 'function'
? { onSuccess }
: {
onSuccess: (data, params) => {
queryClient.cancelQueries(params.queryKey)
haptics('Success')
switch (params.type) {
case 'updateStatusProperty':
switch (params.payload.property) {
case 'poll':
params.payload.data = (data as unknown) as Mastodon.Poll
updateStatusProperty({ queryClient, ...params })
break
}
break
}
}
}),
...(onMutate && {
onMutate: params => {
queryClient.cancelQueries(params.queryKey)
let oldData
params.queryKey && (oldData = queryClient.getQueryData(params.queryKey))
haptics('Success')
switch (params.type) {
case 'updateStatusProperty':
updateStatusProperty({ queryClient, ...params })
break
case 'deleteItem':
deleteItem({ queryClient, ...params })
break
}
return oldData
}
})
})
}
export { useTimelineQuery, useTimelineMutation }

View File

@ -0,0 +1,25 @@
import { InfiniteData, QueryClient } from 'react-query'
import { QueryKeyTimeline } from '../timeline'
const deleteItem = ({
queryClient,
queryKey,
id
}: {
queryClient: QueryClient
queryKey: QueryKeyTimeline
id: Mastodon.Status['id']
}) => {
queryClient.setQueryData<InfiniteData<Mastodon.Conversation[]> | undefined>(
queryKey,
old => {
if (old) {
old.pages = old.pages.map(page => page.filter(item => item.id !== id))
}
return old
}
)
}
export default deleteItem

View File

@ -0,0 +1,27 @@
import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/timeline'
const updateConversation = ({
item,
payload
}: {
item: Mastodon.Conversation
payload: MutationVarsTimelineUpdateStatusProperty['payload']
}) => {
switch (payload.property) {
case 'poll':
if (item.last_status) {
item.last_status[payload.property] = payload.data
}
return item
default:
if (item.last_status) {
item.last_status[payload.property] =
typeof payload.currentValue === 'boolean'
? !payload.currentValue
: true
}
return item
}
}
export default updateConversation

View File

@ -0,0 +1,24 @@
import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/timeline'
const updateNotification = ({
item,
payload
}: {
item: Mastodon.Notification
payload: MutationVarsTimelineUpdateStatusProperty['payload']
}) => {
switch (payload.property) {
case 'poll':
return item
default:
if (item.status) {
item.status[payload.property] =
typeof payload.currentValue === 'boolean'
? !payload.currentValue
: true
}
return item
}
}
export default updateNotification

View File

@ -0,0 +1,37 @@
import { MutationVarsTimelineUpdateStatusProperty } from '@utils/queryHooks/timeline'
const updateStatus = ({
item,
reblog,
payload
}: {
item: Mastodon.Status
reblog?: boolean
payload: MutationVarsTimelineUpdateStatusProperty['payload']
}) => {
switch (payload.property) {
case 'poll':
console.log(payload.data)
if (reblog) {
item.reblog!.poll = payload.data
} else {
item.poll = payload.data
}
break
default:
if (reblog) {
item.reblog![payload.property] =
typeof payload.currentValue === 'boolean'
? !payload.currentValue
: true
} else {
item[payload.property] =
typeof payload.currentValue === 'boolean'
? !payload.currentValue
: true
}
return item
}
}
export default updateStatus

View File

@ -0,0 +1,76 @@
import { findIndex } from 'lodash'
import { InfiniteData, QueryClient } from 'react-query'
import {
MutationVarsTimelineUpdateStatusProperty,
TimelineData
} from '../timeline'
import updateConversation from './update/conversation'
import updateNotification from './update/notification'
import updateStatus from './update/status'
const updateStatusProperty = ({
queryClient,
queryKey,
id,
reblog,
payload
}: {
queryClient: QueryClient
queryKey: MutationVarsTimelineUpdateStatusProperty['queryKey']
id: MutationVarsTimelineUpdateStatusProperty['id']
reblog?: MutationVarsTimelineUpdateStatusProperty['reblog']
payload: MutationVarsTimelineUpdateStatusProperty['payload']
}) => {
queryClient.setQueryData<InfiniteData<TimelineData> | undefined>(
queryKey,
old => {
if (old) {
let foundToot = false
old.pages = old.pages.map(page => {
// Skip rest of the pages if any toot is found
if (foundToot) {
return page
} else {
if (
typeof (page as Mastodon.Conversation[])[0].unread === 'boolean'
) {
const items = page as Mastodon.Conversation[]
const tootIndex = findIndex(items, ['last_status.id', id])
if (tootIndex >= 0) {
foundToot = true
updateConversation({ item: items[tootIndex], payload })
}
return page
} else if (
typeof (page as Mastodon.Notification[])[0].type === 'string'
) {
const items = page as Mastodon.Notification[]
const tootIndex = findIndex(items, ['status.id', id])
if (tootIndex >= 0) {
foundToot = true
updateNotification({ item: items[tootIndex], payload })
}
} else {
const items = page as Mastodon.Status[]
const tootIndex = findIndex(items, [
reblog ? 'reblog.id' : 'id',
id
])
// if favouriets page and notifications page, remove the item instead
if (tootIndex >= 0) {
foundToot = true
updateStatus({ item: items[tootIndex], reblog, payload })
}
}
return page
}
})
}
return old
}
)
}
export default updateStatusProperty