tooot/src/utils/queryHooks/timeline.ts

484 lines
13 KiB
TypeScript
Raw Normal View History

2021-01-11 21:36:57 +01:00
import haptics from '@components/haptics'
import {
2022-12-31 00:07:28 +01:00
MutationOptions,
QueryFunctionContext,
useInfiniteQuery,
UseInfiniteQueryOptions,
2023-01-01 17:20:35 +01:00
useMutation
} from '@tanstack/react-query'
2023-02-12 14:50:31 +01:00
import apiGeneral from '@utils/api/general'
import { PagedResponse } from '@utils/api/helpers'
import apiInstance from '@utils/api/instance'
2023-02-12 14:50:31 +01:00
import { appendRemote } from '@utils/helpers/appendRemote'
import { featureCheck } from '@utils/helpers/featureCheck'
2023-01-04 22:39:29 +01:00
import { useNavState } from '@utils/navigation/navigators'
2023-01-03 23:57:23 +01:00
import { queryClient } from '@utils/queryHooks'
2023-01-14 15:21:31 +01:00
import { getAccountStorage, setAccountStorage } from '@utils/storage/actions'
import { AxiosError } from 'axios'
import { uniqBy } from 'lodash'
2023-01-03 23:57:23 +01:00
import { searchLocalStatus } from './search'
2021-01-11 21:36:57 +01:00
import deleteItem from './timeline/deleteItem'
2023-01-06 01:01:10 +01:00
import editItem from './timeline/editItem'
2021-01-11 21:36:57 +01:00
import updateStatusProperty from './timeline/updateStatusProperty'
2023-01-07 18:01:08 +01:00
import { infinitePageParams } from './utils'
2021-01-07 19:13:09 +01:00
export type QueryKeyTimeline = [
'Timeline',
2022-12-14 23:37:41 +01:00
(
| {
2023-02-12 14:50:31 +01:00
page: Exclude<App.Pages, 'Following' | 'Hashtag' | 'List' | 'Toot' | 'Account' | 'Explore'>
2022-12-14 23:37:41 +01:00
}
| {
page: 'Following'
showBoosts: boolean
showReplies: boolean
}
| {
page: 'Hashtag'
2023-01-26 23:07:13 +01:00
tag_name: Mastodon.Tag['name']
2022-12-14 23:37:41 +01:00
}
| {
page: 'List'
list: Mastodon.List['id']
}
| {
page: 'Account'
2023-01-02 02:08:12 +01:00
id?: Mastodon.Account['id']
2022-12-14 23:37:41 +01:00
exclude_reblogs: boolean
only_media: boolean
}
2022-12-31 00:07:28 +01:00
| {
page: 'Toot'
toot: Mastodon.Status['id']
2023-01-01 16:44:55 +01:00
remote: boolean
2022-12-31 00:07:28 +01:00
}
2023-02-12 14:50:31 +01:00
| { page: 'Explore'; domain?: string }
2022-12-14 23:37:41 +01:00
)
2021-01-07 19:13:09 +01:00
]
export const queryFunctionTimeline = async ({
queryKey,
pageParam
}: QueryFunctionContext<QueryKeyTimeline>) => {
2022-12-14 23:37:41 +01:00
const page = queryKey[1]
let marker: string | undefined
if (page.page === 'Following' && !pageParam?.offset && !pageParam?.min_id && !pageParam?.max_id) {
marker = getAccountStorage.string('read_marker_following')
}
const params: { [key: string]: string } = marker
? { limit: 40, max_id: marker }
: { limit: 40, ...pageParam }
2021-01-07 19:13:09 +01:00
2022-12-14 23:37:41 +01:00
switch (page.page) {
2021-01-07 19:13:09 +01:00
case 'Following':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: 'timelines/home',
params
2022-12-14 23:37:41 +01:00
}).then(res => {
2023-01-14 15:21:31 +01:00
if (marker && !res.body.length) {
setAccountStorage([{ key: 'read_marker_following', value: undefined }])
return Promise.reject()
}
2022-12-14 23:37:41 +01:00
if (!page.showBoosts || !page.showReplies) {
return {
...res,
body: res.body
.filter(status => {
if (!page.showBoosts && status.reblog) {
return null
}
if (!page.showReplies && status.in_reply_to_id?.length) {
return null
}
return status
})
.filter(s => s)
}
} else {
return res
}
2021-01-07 19:13:09 +01:00
})
case 'Local':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: 'timelines/public',
2021-01-11 21:36:57 +01:00
params: {
...params,
local: 'true'
}
2021-01-07 19:13:09 +01:00
})
case 'LocalPublic':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: 'timelines/public',
params
})
2023-02-12 14:50:31 +01:00
case 'Explore':
if (page.domain) {
return apiGeneral<Mastodon.Status[]>({
method: 'get',
domain: page.domain,
url: 'api/v1/timelines/public',
params: {
...params,
local: 'true'
}
}).then(res => ({ ...res, body: res.body.map(status => appendRemote.status(status)) }))
} else {
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: 'trends/statuses',
params
})
}
2021-01-07 19:13:09 +01:00
case 'Notifications':
const notificationsFilter = getAccountStorage.object('notifications')
const usePositiveFilter = featureCheck('notification_types_positive_filter')
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Notification[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: 'notifications',
2021-03-17 15:30:28 +01:00
params: {
...params,
...(notificationsFilter &&
(usePositiveFilter
? {
types: Object.keys(notificationsFilter)
// @ts-ignore
.filter(filter => notificationsFilter[filter] === true)
}
: {
exclude_types: Object.keys(notificationsFilter)
// @ts-ignore
.filter(filter => notificationsFilter[filter] === false)
}))
2021-03-17 15:30:28 +01:00
}
2021-01-07 19:13:09 +01:00
})
2022-12-14 23:37:41 +01:00
case 'Account':
2023-01-02 23:18:22 +01:00
if (!page.id) return Promise.reject('Timeline query account id not provided')
2023-01-02 02:08:12 +01:00
2023-01-07 18:01:08 +01:00
if (page.only_media) {
return apiInstance<Mastodon.Status[]>({
method: 'get',
url: `accounts/${page.id}/statuses`,
params: {
only_media: 'true',
...params
}
})
} else if (page.exclude_reblogs) {
2022-12-14 23:37:41 +01:00
if (pageParam && pageParam.hasOwnProperty('max_id')) {
return apiInstance<Mastodon.Status[]>({
method: 'get',
2023-01-02 02:08:12 +01:00
url: `accounts/${page.id}/statuses`,
2022-12-14 23:37:41 +01:00
params: {
exclude_replies: 'true',
...params
}
})
} else {
const res1 = await apiInstance<(Mastodon.Status & { _pinned: boolean })[]>({
method: 'get',
2023-01-02 02:08:12 +01:00
url: `accounts/${page.id}/statuses`,
2022-12-14 23:37:41 +01:00
params: {
pinned: 'true'
}
})
res1.body = res1.body.map(status => {
status._pinned = true
return status
})
const res2 = await apiInstance<Mastodon.Status[]>({
method: 'get',
2023-01-02 02:08:12 +01:00
url: `accounts/${page.id}/statuses`,
2022-12-14 23:37:41 +01:00
params: {
exclude_replies: 'true'
}
})
return {
body: uniqBy([...res1.body, ...res2.body], 'id'),
...(res2.links?.next && { links: { next: res2.links.next } })
2021-01-07 19:13:09 +01:00
}
2022-12-14 23:37:41 +01:00
}
2021-01-07 19:13:09 +01:00
} else {
2022-12-14 23:37:41 +01:00
return apiInstance<Mastodon.Status[]>({
2021-03-29 00:22:30 +02:00
method: 'get',
2023-01-02 02:08:12 +01:00
url: `accounts/${page.id}/statuses`,
2021-03-29 00:22:30 +02:00
params: {
2022-12-14 23:37:41 +01:00
...params,
2023-01-07 18:01:08 +01:00
exclude_replies: false,
only_media: false
2021-02-11 01:33:31 +01:00
}
2021-01-07 19:13:09 +01:00
})
}
case 'Hashtag':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
2023-01-26 23:07:13 +01:00
url: `timelines/tag/${page.tag_name}`,
2021-01-07 19:13:09 +01:00
params
})
case 'Conversations':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Conversation[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: `conversations`,
params
})
case 'Bookmarks':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: `bookmarks`,
params
})
case 'Favourites':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
url: `favourites`,
params
})
case 'List':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status[]>({
2021-01-07 19:13:09 +01:00
method: 'get',
2022-12-14 23:37:41 +01:00
url: `timelines/list/${page.list}`,
2021-01-07 19:13:09 +01:00
params
})
default:
2023-01-02 23:18:22 +01:00
return Promise.reject('Timeline query no page matched')
2021-01-07 19:13:09 +01:00
}
}
type Unpromise<T extends Promise<any>> = T extends Promise<infer U> ? U : never
export type TimelineData = Unpromise<ReturnType<typeof queryFunctionTimeline>>
2021-12-18 19:59:38 +01:00
const useTimelineQuery = ({
2021-01-07 19:13:09 +01:00
options,
...queryKeyParams
}: QueryKeyTimeline[1] & {
2023-01-07 18:01:08 +01:00
options?: Omit<
UseInfiniteQueryOptions<PagedResponse<Mastodon.Status[]>, AxiosError>,
'getPreviousPageParam' | 'getNextPageParam'
>
2021-01-07 19:13:09 +01:00
}) => {
const queryKey: QueryKeyTimeline = ['Timeline', { ...queryKeyParams }]
return useInfiniteQuery(queryKey, queryFunctionTimeline, {
2021-03-14 00:47:55 +01:00
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
2023-01-07 18:01:08 +01:00
...options,
...infinitePageParams
2021-03-14 00:47:55 +01:00
})
2021-01-07 19:13:09 +01:00
}
2021-01-11 21:36:57 +01:00
// --- 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'
2023-01-01 18:37:05 +01:00
status: Mastodon.Status
2021-01-11 21:36:57 +01:00
payload:
| {
2023-01-02 23:18:22 +01:00
type: 'bookmarked' | 'muted' | 'pinned'
to: boolean
}
| {
type: 'favourited'
to: boolean
2021-01-23 02:41:50 +01:00
}
| {
2023-01-01 18:37:05 +01:00
type: 'reblogged'
visibility: 'public' | 'unlisted'
2023-01-02 23:18:22 +01:00
to: boolean
2021-01-11 21:36:57 +01:00
}
2022-10-31 23:43:42 +01:00
| {
2023-01-01 18:37:05 +01:00
type: 'poll'
action: 'vote'
options: boolean[]
2022-10-31 23:43:42 +01:00
}
2021-01-11 21:36:57 +01:00
| {
2023-01-01 18:37:05 +01:00
type: 'poll'
action: 'refresh'
2021-01-11 21:36:57 +01:00
}
}
export type MutationVarsTimelineUpdateAccountProperty = {
// This is status in general, including "status" inside conversation and notification
type: 'updateAccountProperty'
id: Mastodon.Account['id']
payload: {
property: 'mute' | 'block' | 'reports'
currentValue?: boolean
2021-01-11 21:36:57 +01:00
}
}
2023-01-06 01:01:10 +01:00
export type MutationVarsTimelineEditItem = {
type: 'editItem'
status: Mastodon.Status
navigationState: (QueryKeyTimeline | undefined)[]
}
2021-01-11 21:36:57 +01:00
export type MutationVarsTimelineDeleteItem = {
type: 'deleteItem'
source: 'statuses' | 'conversations'
2022-04-30 17:44:39 +02:00
id: Mastodon.Status['id']
2021-01-11 21:36:57 +01:00
}
export type MutationVarsTimelineDomainBlock = {
// This is for deleting status and conversation
type: 'domainBlock'
domain: string
}
export type MutationVarsTimeline =
| MutationVarsTimelineUpdateStatusProperty
| MutationVarsTimelineUpdateAccountProperty
2023-01-06 01:01:10 +01:00
| MutationVarsTimelineEditItem
2021-01-11 21:36:57 +01:00
| MutationVarsTimelineDeleteItem
| MutationVarsTimelineDomainBlock
const mutationFunction = async (params: MutationVarsTimeline) => {
switch (params.type) {
case 'updateStatusProperty':
2023-02-01 14:40:22 +01:00
let tootId = params.status.id
let pollId = params.status.poll?.id
if (params.status._remote) {
const fetched = await searchLocalStatus(params.status.uri)
if (fetched) {
tootId = fetched.id
pollId = fetched.poll?.id
} else {
return Promise.reject('Fetching for remote toot failed')
}
}
2023-01-01 18:37:05 +01:00
switch (params.payload.type) {
2021-01-11 21:36:57 +01:00
case 'poll':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Poll>({
2023-01-01 18:37:05 +01:00
method: params.payload.action === 'vote' ? 'post' : 'get',
2023-02-01 14:40:22 +01:00
url: params.payload.action === 'vote' ? `polls/${pollId}/votes` : `polls/${pollId}`,
2023-01-29 00:37:56 +01:00
...(params.payload.action === 'vote' && {
body: {
choices: params.payload.options
.map((option, index) => (option ? index.toString() : undefined))
.filter(o => o)
}
})
2021-01-11 21:36:57 +01:00
})
default:
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Status>({
2021-01-11 21:36:57 +01:00
method: 'post',
2023-01-02 23:18:22 +01:00
url: `statuses/${tootId}/${params.payload.to ? '' : 'un'}${
2023-01-01 18:37:05 +01:00
MapPropertyToUrl[params.payload.type]
2022-12-03 20:47:11 +01:00
}`,
2023-01-29 00:37:56 +01:00
...(params.payload.type === 'reblogged' && {
body: { visibility: params.payload.visibility }
})
2021-01-11 21:36:57 +01:00
})
}
case 'updateAccountProperty':
switch (params.payload.property) {
case 'block':
case 'mute':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Account>({
2021-01-11 21:36:57 +01:00
method: 'post',
2022-12-03 20:47:11 +01:00
url: `accounts/${params.id}/${params.payload.currentValue ? 'un' : ''}${
params.payload.property
}`
2021-01-11 21:36:57 +01:00
})
case 'reports':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Account>({
2021-01-11 21:36:57 +01:00
method: 'post',
url: `reports`,
params: {
account_id: params.id
}
})
}
2023-01-06 01:01:10 +01:00
case 'editItem':
return { body: params.status }
2021-01-11 21:36:57 +01:00
case 'deleteItem':
2021-02-20 19:12:44 +01:00
return apiInstance<Mastodon.Conversation>({
2021-01-11 21:36:57 +01:00
method: 'delete',
url: `${params.source}/${params.id}`
})
case 'domainBlock':
2021-02-20 19:12:44 +01:00
return apiInstance<any>({
2021-01-11 21:36:57 +01:00
method: 'post',
url: `domain_blocks`,
params: {
domain: params.domain
}
})
}
}
type MutationOptionsTimeline = MutationOptions<
2021-02-11 01:33:31 +01:00
{ body: Mastodon.Conversation | Mastodon.Notification | Mastodon.Status },
2021-01-11 21:36:57 +01:00
AxiosError,
MutationVarsTimeline
>
const useTimelineMutation = ({
onError,
onMutate,
onSettled,
onSuccess
}: {
onError?: MutationOptionsTimeline['onError']
onMutate?: boolean
onSettled?: MutationOptionsTimeline['onSettled']
2021-02-13 01:26:02 +01:00
onSuccess?: MutationOptionsTimeline['onSuccess']
2021-01-11 21:36:57 +01:00
}) => {
2023-01-04 22:39:29 +01:00
const navigationState = useNavState()
2021-01-11 21:36:57 +01:00
return useMutation<
2021-02-11 01:33:31 +01:00
{ body: Mastodon.Conversation | Mastodon.Notification | Mastodon.Status },
2021-01-11 21:36:57 +01:00
AxiosError,
MutationVarsTimeline
>(mutationFunction, {
onError,
onSettled,
2021-02-13 01:26:02 +01:00
onSuccess,
2021-01-11 21:36:57 +01:00
...(onMutate && {
onMutate: params => {
2023-01-04 22:39:29 +01:00
queryClient.cancelQueries(navigationState[0])
const oldData = navigationState[0] && queryClient.getQueryData(navigationState[0])
2021-01-11 21:36:57 +01:00
2021-02-27 16:33:54 +01:00
haptics('Light')
2021-01-11 21:36:57 +01:00
switch (params.type) {
case 'updateStatusProperty':
2023-01-04 22:39:29 +01:00
updateStatusProperty(params, navigationState)
2022-04-30 17:44:39 +02:00
break
2023-01-06 01:01:10 +01:00
case 'editItem':
editItem(params)
break
2021-01-11 21:36:57 +01:00
case 'deleteItem':
2023-01-04 22:39:29 +01:00
deleteItem(params, navigationState)
2021-01-11 21:36:57 +01:00
break
}
return oldData
}
})
})
}
export { useTimelineQuery, useTimelineMutation }