Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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