Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 197 lines 5.9 kB view raw
1import {useEffect, useMemo, useState} from 'react' 2import { 3 AppBskyEmbedRecord, 4 AppBskyEmbedRecordWithMedia, 5 type AppBskyFeedDefs, 6} from '@atproto/api' 7import {type QueryClient} from '@tanstack/react-query' 8import {EventEmitter} from 'eventemitter3' 9 10import {batchedUpdates} from '#/lib/batchedUpdates' 11import {findAllPostsInQueryData as findAllPostsInBookmarksQueryData} from '#/state/queries/bookmarks/useBookmarksQuery' 12import {findAllPostsInQueryData as findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews' 13import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' 14import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 15import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 16import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '#/state/queries/search-posts' 17import {findAllPostsInQueryData as findAllPostsInThreadV2QueryData} from '#/state/queries/usePostThread/queryCache' 18import {castAsShadow, type Shadow} from './types' 19export type {Shadow} from './types' 20 21export interface PostShadow { 22 likeUri: string | undefined 23 repostUri: string | undefined 24 isDeleted: boolean 25 embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined 26 pinned: boolean 27 optimisticReplyCount: number | undefined 28 bookmarked: boolean | undefined 29} 30 31export const POST_TOMBSTONE = Symbol('PostTombstone') 32 33const emitter = new EventEmitter() 34const shadows: WeakMap< 35 AppBskyFeedDefs.PostView, 36 Partial<PostShadow> 37> = new WeakMap() 38 39/** 40 * Use with caution! This function returns the raw shadow data for a post. 41 * Prefer using `usePostShadow`. 42 */ 43export function dangerousGetPostShadow(post: AppBskyFeedDefs.PostView) { 44 return shadows.get(post) 45} 46 47export function usePostShadow( 48 post: AppBskyFeedDefs.PostView, 49): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 50 const [shadow, setShadow] = useState(() => shadows.get(post)) 51 const [prevPost, setPrevPost] = useState(post) 52 if (post !== prevPost) { 53 setPrevPost(post) 54 setShadow(shadows.get(post)) 55 } 56 57 useEffect(() => { 58 function onUpdate() { 59 setShadow(shadows.get(post)) 60 } 61 emitter.addListener(post.uri, onUpdate) 62 return () => { 63 emitter.removeListener(post.uri, onUpdate) 64 } 65 }, [post, setShadow]) 66 67 return useMemo(() => { 68 if (shadow) { 69 return mergeShadow(post, shadow) 70 } else { 71 return castAsShadow(post) 72 } 73 }, [post, shadow]) 74} 75 76function mergeShadow( 77 post: AppBskyFeedDefs.PostView, 78 shadow: Partial<PostShadow>, 79): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 80 if (shadow.isDeleted) { 81 return POST_TOMBSTONE 82 } 83 84 let likeCount = post.likeCount ?? 0 85 if ('likeUri' in shadow) { 86 const wasLiked = !!post.viewer?.like 87 const isLiked = !!shadow.likeUri 88 if (wasLiked && !isLiked) { 89 likeCount-- 90 } else if (!wasLiked && isLiked) { 91 likeCount++ 92 } 93 likeCount = Math.max(0, likeCount) 94 } 95 96 let bookmarkCount = post.bookmarkCount ?? 0 97 if ('bookmarked' in shadow) { 98 const wasBookmarked = !!post.viewer?.bookmarked 99 const isBookmarked = !!shadow.bookmarked 100 if (wasBookmarked && !isBookmarked) { 101 bookmarkCount-- 102 } else if (!wasBookmarked && isBookmarked) { 103 bookmarkCount++ 104 } 105 bookmarkCount = Math.max(0, bookmarkCount) 106 } 107 108 let repostCount = post.repostCount ?? 0 109 if ('repostUri' in shadow) { 110 const wasReposted = !!post.viewer?.repost 111 const isReposted = !!shadow.repostUri 112 if (wasReposted && !isReposted) { 113 repostCount-- 114 } else if (!wasReposted && isReposted) { 115 repostCount++ 116 } 117 repostCount = Math.max(0, repostCount) 118 } 119 120 let replyCount = post.replyCount ?? 0 121 if ('optimisticReplyCount' in shadow) { 122 replyCount = shadow.optimisticReplyCount ?? replyCount 123 } 124 125 let embed: typeof post.embed 126 if ('embed' in shadow) { 127 if ( 128 (AppBskyEmbedRecord.isView(post.embed) && 129 AppBskyEmbedRecord.isView(shadow.embed)) || 130 (AppBskyEmbedRecordWithMedia.isView(post.embed) && 131 AppBskyEmbedRecordWithMedia.isView(shadow.embed)) 132 ) { 133 embed = shadow.embed 134 } 135 } 136 137 return castAsShadow({ 138 ...post, 139 embed: embed || post.embed, 140 likeCount: likeCount, 141 repostCount: repostCount, 142 replyCount: replyCount, 143 bookmarkCount: bookmarkCount, 144 viewer: { 145 ...(post.viewer || {}), 146 like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, 147 repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost, 148 pinned: 'pinned' in shadow ? shadow.pinned : post.viewer?.pinned, 149 bookmarked: 150 'bookmarked' in shadow ? shadow.bookmarked : post.viewer?.bookmarked, 151 }, 152 }) 153} 154 155export function updatePostShadow( 156 queryClient: QueryClient, 157 uri: string, 158 value: Partial<PostShadow>, 159) { 160 const cachedPosts = findPostsInCache(queryClient, uri) 161 for (let post of cachedPosts) { 162 shadows.set(post, {...shadows.get(post), ...value}) 163 } 164 batchedUpdates(() => { 165 emitter.emit(uri) 166 }) 167} 168 169function* findPostsInCache( 170 queryClient: QueryClient, 171 uri: string, 172): Generator<AppBskyFeedDefs.PostView, void> { 173 for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { 174 yield post 175 } 176 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 177 yield post 178 } 179 for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { 180 yield post 181 } 182 for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { 183 yield post 184 } 185 for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { 186 yield post 187 } 188 for (let post of findAllPostsInExploreFeedPreviewsQueryData( 189 queryClient, 190 uri, 191 )) { 192 yield post 193 } 194 for (let post of findAllPostsInBookmarksQueryData(queryClient, uri)) { 195 yield post 196 } 197}