mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 4.4 kB view raw
1import {useEffect, useMemo, useState} from 'react' 2import { 3 AppBskyEmbedRecord, 4 AppBskyEmbedRecordWithMedia, 5 AppBskyFeedDefs, 6} from '@atproto/api' 7import {QueryClient} from '@tanstack/react-query' 8import EventEmitter from 'eventemitter3' 9 10import {batchedUpdates} from '#/lib/batchedUpdates' 11import {findAllPostsInQueryData as findAllPostsInNotifsQueryData} from '../queries/notifications/feed' 12import {findAllPostsInQueryData as findAllPostsInFeedQueryData} from '../queries/post-feed' 13import {findAllPostsInQueryData as findAllPostsInQuoteQueryData} from '../queries/post-quotes' 14import {findAllPostsInQueryData as findAllPostsInThreadQueryData} from '../queries/post-thread' 15import {findAllPostsInQueryData as findAllPostsInSearchQueryData} from '../queries/search-posts' 16import {castAsShadow, Shadow} from './types' 17export type {Shadow} from './types' 18 19export interface PostShadow { 20 likeUri: string | undefined 21 repostUri: string | undefined 22 isDeleted: boolean 23 embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined 24} 25 26export const POST_TOMBSTONE = Symbol('PostTombstone') 27 28const emitter = new EventEmitter() 29const shadows: WeakMap< 30 AppBskyFeedDefs.PostView, 31 Partial<PostShadow> 32> = new WeakMap() 33 34export function usePostShadow( 35 post: AppBskyFeedDefs.PostView, 36): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 37 const [shadow, setShadow] = useState(() => shadows.get(post)) 38 const [prevPost, setPrevPost] = useState(post) 39 if (post !== prevPost) { 40 setPrevPost(post) 41 setShadow(shadows.get(post)) 42 } 43 44 useEffect(() => { 45 function onUpdate() { 46 setShadow(shadows.get(post)) 47 } 48 emitter.addListener(post.uri, onUpdate) 49 return () => { 50 emitter.removeListener(post.uri, onUpdate) 51 } 52 }, [post, setShadow]) 53 54 return useMemo(() => { 55 if (shadow) { 56 return mergeShadow(post, shadow) 57 } else { 58 return castAsShadow(post) 59 } 60 }, [post, shadow]) 61} 62 63function mergeShadow( 64 post: AppBskyFeedDefs.PostView, 65 shadow: Partial<PostShadow>, 66): Shadow<AppBskyFeedDefs.PostView> | typeof POST_TOMBSTONE { 67 if (shadow.isDeleted) { 68 return POST_TOMBSTONE 69 } 70 71 let likeCount = post.likeCount ?? 0 72 if ('likeUri' in shadow) { 73 const wasLiked = !!post.viewer?.like 74 const isLiked = !!shadow.likeUri 75 if (wasLiked && !isLiked) { 76 likeCount-- 77 } else if (!wasLiked && isLiked) { 78 likeCount++ 79 } 80 likeCount = Math.max(0, likeCount) 81 } 82 83 let repostCount = post.repostCount ?? 0 84 if ('repostUri' in shadow) { 85 const wasReposted = !!post.viewer?.repost 86 const isReposted = !!shadow.repostUri 87 if (wasReposted && !isReposted) { 88 repostCount-- 89 } else if (!wasReposted && isReposted) { 90 repostCount++ 91 } 92 repostCount = Math.max(0, repostCount) 93 } 94 95 let embed: typeof post.embed 96 if ('embed' in shadow) { 97 if ( 98 (AppBskyEmbedRecord.isView(post.embed) && 99 AppBskyEmbedRecord.isView(shadow.embed)) || 100 (AppBskyEmbedRecordWithMedia.isView(post.embed) && 101 AppBskyEmbedRecordWithMedia.isView(shadow.embed)) 102 ) { 103 embed = shadow.embed 104 } 105 } 106 107 return castAsShadow({ 108 ...post, 109 embed: embed || post.embed, 110 likeCount: likeCount, 111 repostCount: repostCount, 112 viewer: { 113 ...(post.viewer || {}), 114 like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like, 115 repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost, 116 }, 117 }) 118} 119 120export function updatePostShadow( 121 queryClient: QueryClient, 122 uri: string, 123 value: Partial<PostShadow>, 124) { 125 const cachedPosts = findPostsInCache(queryClient, uri) 126 for (let post of cachedPosts) { 127 shadows.set(post, {...shadows.get(post), ...value}) 128 } 129 batchedUpdates(() => { 130 emitter.emit(uri) 131 }) 132} 133 134function* findPostsInCache( 135 queryClient: QueryClient, 136 uri: string, 137): Generator<AppBskyFeedDefs.PostView, void> { 138 for (let post of findAllPostsInFeedQueryData(queryClient, uri)) { 139 yield post 140 } 141 for (let post of findAllPostsInNotifsQueryData(queryClient, uri)) { 142 yield post 143 } 144 for (let node of findAllPostsInThreadQueryData(queryClient, uri)) { 145 if (node.type === 'post') { 146 yield node.post 147 } 148 } 149 for (let post of findAllPostsInSearchQueryData(queryClient, uri)) { 150 yield post 151 } 152 for (let post of findAllPostsInQuoteQueryData(queryClient, uri)) { 153 yield post 154 } 155}