forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {
3 type AppBskyActorDefs,
4 type AppBskyFeedDefs,
5 type AppBskyFeedSearchPosts,
6 AtUri,
7 moderatePost,
8} from '@atproto/api'
9import {
10 type InfiniteData,
11 type QueryClient,
12 type QueryKey,
13 useInfiniteQuery,
14} from '@tanstack/react-query'
15
16import {useModerationOpts} from '#/state/preferences/moderation-opts'
17import {useAgent} from '#/state/session'
18import {
19 didOrHandleUriMatches,
20 embedViewRecordToPostView,
21 getEmbeddedPost,
22} from './util'
23
24const searchPostsQueryKeyRoot = 'search-posts'
25const searchPostsQueryKey = ({query, sort}: {query: string; sort?: string}) => [
26 searchPostsQueryKeyRoot,
27 query,
28 sort,
29]
30
31export function useSearchPostsQuery({
32 query,
33 sort,
34 enabled,
35}: {
36 query: string
37 sort?: 'top' | 'latest'
38 enabled?: boolean
39}) {
40 const agent = useAgent()
41 const moderationOpts = useModerationOpts()
42 const selectArgs = React.useMemo(
43 () => ({
44 isSearchingSpecificUser: /from:(\w+)/.test(query),
45 moderationOpts,
46 }),
47 [query, moderationOpts],
48 )
49 const lastRun = React.useRef<{
50 data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
51 args: typeof selectArgs
52 result: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
53 } | null>(null)
54
55 return useInfiniteQuery<
56 AppBskyFeedSearchPosts.OutputSchema,
57 Error,
58 InfiniteData<AppBskyFeedSearchPosts.OutputSchema>,
59 QueryKey,
60 string | undefined
61 >({
62 queryKey: searchPostsQueryKey({query, sort}),
63 queryFn: async ({pageParam}) => {
64 const res = await agent.app.bsky.feed.searchPosts({
65 q: query,
66 limit: 25,
67 cursor: pageParam,
68 sort,
69 })
70 return res.data
71 },
72 initialPageParam: undefined,
73 getNextPageParam: lastPage => lastPage.cursor,
74 enabled: enabled ?? !!moderationOpts,
75 select: React.useCallback(
76 (data: InfiniteData<AppBskyFeedSearchPosts.OutputSchema>) => {
77 const {moderationOpts, isSearchingSpecificUser} = selectArgs
78
79 /*
80 * If a user applies the `from:<user>` filter, don't apply any
81 * moderation. Note that if we add any more filtering logic below, we
82 * may need to adjust this.
83 */
84 if (isSearchingSpecificUser) {
85 return data
86 }
87
88 // Keep track of the last run and whether we can reuse
89 // some already selected pages from there.
90 let reusedPages = []
91 if (lastRun.current) {
92 const {
93 data: lastData,
94 args: lastArgs,
95 result: lastResult,
96 } = lastRun.current
97 let canReuse = true
98 for (let key in selectArgs) {
99 if (selectArgs.hasOwnProperty(key)) {
100 if ((selectArgs as any)[key] !== (lastArgs as any)[key]) {
101 // Can't do reuse anything if any input has changed.
102 canReuse = false
103 break
104 }
105 }
106 }
107 if (canReuse) {
108 for (let i = 0; i < data.pages.length; i++) {
109 if (data.pages[i] && lastData.pages[i] === data.pages[i]) {
110 reusedPages.push(lastResult.pages[i])
111 continue
112 }
113 // Stop as soon as pages stop matching up.
114 break
115 }
116 }
117 }
118
119 const result = {
120 ...data,
121 pages: [
122 ...reusedPages,
123 ...data.pages.slice(reusedPages.length).map(page => {
124 return {
125 ...page,
126 posts: page.posts.filter(post => {
127 const mod = moderatePost(post, moderationOpts!)
128 return !mod.ui('contentList').filter
129 }),
130 }
131 }),
132 ],
133 }
134
135 lastRun.current = {data, result, args: selectArgs}
136
137 return result
138 },
139 [selectArgs],
140 ),
141 })
142}
143
144export function* findAllPostsInQueryData(
145 queryClient: QueryClient,
146 uri: string,
147): Generator<AppBskyFeedDefs.PostView, undefined> {
148 const queryDatas = queryClient.getQueriesData<
149 InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
150 >({
151 queryKey: [searchPostsQueryKeyRoot],
152 })
153 const atUri = new AtUri(uri)
154
155 for (const [_queryKey, queryData] of queryDatas) {
156 if (!queryData?.pages) {
157 continue
158 }
159 for (const page of queryData?.pages) {
160 for (const post of page.posts) {
161 if (didOrHandleUriMatches(atUri, post)) {
162 yield post
163 }
164
165 const quotedPost = getEmbeddedPost(post.embed)
166 if (quotedPost && didOrHandleUriMatches(atUri, quotedPost)) {
167 yield embedViewRecordToPostView(quotedPost)
168 }
169 }
170 }
171 }
172}
173
174export function* findAllProfilesInQueryData(
175 queryClient: QueryClient,
176 did: string,
177): Generator<AppBskyActorDefs.ProfileViewBasic, undefined> {
178 const queryDatas = queryClient.getQueriesData<
179 InfiniteData<AppBskyFeedSearchPosts.OutputSchema>
180 >({
181 queryKey: [searchPostsQueryKeyRoot],
182 })
183 for (const [_queryKey, queryData] of queryDatas) {
184 if (!queryData?.pages) {
185 continue
186 }
187 for (const page of queryData?.pages) {
188 for (const post of page.posts) {
189 if (post.author.did === did) {
190 yield post.author
191 }
192 const quotedPost = getEmbeddedPost(post.embed)
193 if (quotedPost?.author.did === did) {
194 yield quotedPost.author
195 }
196 }
197 }
198 }
199}