mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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}