mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at samuel/exp-cli 168 lines 5.0 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 findAllPostsInExploreFeedPreviewsQueryData} from '#/state/queries/explore-feed-previews' 12import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '#/state/queries/notifications/feed' 13import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '#/state/queries/post-feed' 14import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '#/state/queries/post-quotes' 15import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '#/state/queries/post-thread' 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} 28 29export const POST_TOMBSTONE = Symbol('PostTombstone') 30 31const emitter = new EventEmitter() 32const shadows: WeakMap< 33 AppBskyFeedDefs.PostView, 34 Partial<PostShadow> 35> = new WeakMap() 36 37export function usePostShadow( 38 post: AppBskyFeedDefs.PostView, 39): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 40 const [shadow, setShadow] = useState(() => shadows.get(post)) 41 const [prevPost, setPrevPost] = useState(post) 42 if (post !== prevPost) { 43 setPrevPost(post) 44 setShadow(shadows.get(post)) 45 } 46 47 useEffect(() => { 48 function onUpdate() { 49 setShadow(shadows.get(post)) 50 } 51 emitter.addListener(post.uri, onUpdate) 52 return () => { 53 emitter.removeListener(post.uri, onUpdate) 54 } 55 }, [post, setShadow]) 56 57 return useMemo(() => { 58 if (shadow) { 59 return mergeShadow(post, shadow) 60 } else { 61 return castAsShadow(post) 62 } 63 }, [post, shadow]) 64} 65 66function mergeShadow( 67 post: AppBskyFeedDefs.PostView, 68 shadow: Partial<PostShadow>, 69): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 70 if (shadow.isDeleted) { 71 return POST_TOMBSTONE 72 } 73 74 let likeCount = post.likeCount ?? 0 75 if ('likeUri' in shadow) { 76 const wasLiked = !!post.viewer?.like 77 const isLiked = !!shadow.likeUri 78 if (wasLiked && !isLiked) { 79 likeCount-- 80 } else if (!wasLiked && isLiked) { 81 likeCount++ 82 } 83 likeCount = Math.max(0, likeCount) 84 } 85 86 let repostCount = post.repostCount ?? 0 87 if ('repostUri' in shadow) { 88 const wasReposted = !!post.viewer?.repost 89 const isReposted = !!shadow.repostUri 90 if (wasReposted && !isReposted) { 91 repostCount-- 92 } else if (!wasReposted && isReposted) { 93 repostCount++ 94 } 95 repostCount = Math.max(0, repostCount) 96 } 97 98 let embed: typeof post.embed 99 if ('embed' in shadow) { 100 if ( 101 (AppBskyEmbedRecord.isView(post.embed) && 102 AppBskyEmbedRecord.isView(shadow.embed)) || 103 (AppBskyEmbedRecordWithMedia.isView(post.embed) && 104 AppBskyEmbedRecordWithMedia.isView(shadow.embed)) 105 ) { 106 embed = shadow.embed 107 } 108 } 109 110 return castAsShadow({ 111 ...post, 112 embed: embed || post.embed, 113 likeCount: likeCount, 114 repostCount: repostCount, 115 viewer: { 116 ...(post.viewer || {}), 117 like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, 118 repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost, 119 pinned: 'pinned' in shadow ? shadow.pinned : post.viewer?.pinned, 120 }, 121 }) 122} 123 124export function updatePostShadow( 125 queryClient: QueryClient, 126 uri: string, 127 value: Partial<PostShadow>, 128) { 129 const cachedPosts = findPostsInCache(queryClient, uri) 130 for (let post of cachedPosts) { 131 shadows.set(post, {...shadows.get(post), ...value}) 132 } 133 batchedUpdates(() => { 134 emitter.emit(uri) 135 }) 136} 137 138function* findPostsInCache( 139 queryClient: QueryClient, 140 uri: string, 141): Generator<AppBskyFeedDefs.PostView, void> { 142 for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { 143 yield post 144 } 145 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 146 yield post 147 } 148 for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { 149 if (node.type === 'post') { 150 yield node.post 151 } 152 } 153 for (let post of findAllPostsInThreadV2QueryData(queryClient, uri)) { 154 yield post 155 } 156 for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { 157 yield post 158 } 159 for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { 160 yield post 161 } 162 for (let post of findAllPostsInExploreFeedPreviewsQueryData( 163 queryClient, 164 uri, 165 )) { 166 yield post 167 } 168}