Bluesky app fork with some witchin' additions 馃挮
at main 199 lines 5.4 kB view raw
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}