about summary refs log tree commit diff
path: root/src/state/queries/notifications/feed.ts
blob: a74670b5b4c80f308277bf36f40865cb0eb162cf (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
 * NOTE
 * The ./unread.ts API:
 *
 * - Provides a `checkUnread()` function to sync with the server,
 * - Periodically calls `checkUnread()`, and
 * - Caches the first page of notifications.
 *
 * IMPORTANT: This query uses ./unread.ts's cache as its first page,
 * IMPORTANT: which means the cache-freshness of this query is driven by the unread API.
 *
 * Follow these rules:
 *
 * 1. Call `checkUnread()` if you want to fetch latest in the background.
 * 2. Call `checkUnread({invalidate: true})` if you want latest to sync into this query's results immediately.
 * 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead.
 */

import {useEffect} from 'react'
import {AppBskyFeedDefs} from '@atproto/api'
import {
  useInfiniteQuery,
  InfiniteData,
  QueryKey,
  useQueryClient,
  QueryClient,
} from '@tanstack/react-query'
import {useModerationOpts} from '../preferences'
import {useUnreadNotificationsApi} from './unread'
import {fetchPage} from './util'
import {FeedPage} from './types'
import {useMutedThreads} from '#/state/muted-threads'
import {STALE} from '..'
import {embedViewRecordToPostView, getEmbeddedPost} from '../util'

export type {NotificationType, FeedNotification, FeedPage} from './types'

const PAGE_SIZE = 30

type RQPageParam = string | undefined

export function RQKEY() {
  return ['notification-feed']
}

export function useNotificationFeedQuery(opts?: {enabled?: boolean}) {
  const queryClient = useQueryClient()
  const moderationOpts = useModerationOpts()
  const threadMutes = useMutedThreads()
  const unreads = useUnreadNotificationsApi()
  const enabled = opts?.enabled !== false

  const query = useInfiniteQuery<
    FeedPage,
    Error,
    InfiniteData<FeedPage>,
    QueryKey,
    RQPageParam
  >({
    staleTime: STALE.INFINITY,
    queryKey: RQKEY(),
    async queryFn({pageParam}: {pageParam: RQPageParam}) {
      let page = await fetchPage({
        limit: PAGE_SIZE,
        cursor: pageParam,
        queryClient,
        moderationOpts,
        threadMutes,
      })

      // if the first page has an unread, mark all read
      if (!pageParam && page.items[0] && !page.items[0].notification.isRead) {
        unreads.markAllRead()
      }

      return page
    },
    initialPageParam: undefined,
    getNextPageParam: lastPage => lastPage.cursor,
    enabled,
  })

  useEffect(() => {
    const {isFetching, hasNextPage, data} = query

    let count = 0
    let numEmpties = 0
    for (const page of data?.pages || []) {
      if (!page.items.length) {
        numEmpties++
      }
      count += page.items.length
    }

    if (!isFetching && hasNextPage && count < PAGE_SIZE && numEmpties < 3) {
      query.fetchNextPage()
    }
  }, [query])

  return query
}

/**
 * This helper is used by the post-thread placeholder function to
 * find a post in the query-data cache
 */
export function findPostInQueryData(
  queryClient: QueryClient,
  uri: string,
): AppBskyFeedDefs.PostView | undefined {
  const generator = findAllPostsInQueryData(queryClient, uri)
  const result = generator.next()
  if (result.done) {
    return undefined
  } else {
    return result.value
  }
}

export function* findAllPostsInQueryData(
  queryClient: QueryClient,
  uri: string,
): Generator<AppBskyFeedDefs.PostView, void> {
  const queryDatas = queryClient.getQueriesData<InfiniteData<FeedPage>>({
    queryKey: ['notification-feed'],
  })
  for (const [_queryKey, queryData] of queryDatas) {
    if (!queryData?.pages) {
      continue
    }
    for (const page of queryData?.pages) {
      for (const item of page.items) {
        if (item.subject?.uri === uri) {
          yield item.subject
        }
        const quotedPost = getEmbeddedPost(item.subject?.embed)
        if (quotedPost?.uri === uri) {
          yield embedViewRecordToPostView(quotedPost)
        }
      }
    }
  }
}