an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
at main 286 lines 8.7 kB view raw
1import { 2 type $Typed, 3 AppBskyActorDefs, 4 AppBskyEmbedExternal, 5 AppBskyEmbedImages, 6 AppBskyEmbedRecord, 7 AppBskyEmbedRecordWithMedia, 8 AppBskyEmbedVideo, 9 AppBskyFeedPost, 10 AtUri, 11} from "@atproto/api"; 12import { useAtom } from "jotai"; 13import { useMemo } from "react"; 14 15import { imgCDNAtom, videoCDNAtom } from "./atoms"; 16import { useQueryIdentity, useQueryPost, useQueryProfile } from "./useQuery"; 17 18type QueryResultData<T extends (...args: any) => any> = 19 ReturnType<T> extends { data: infer D } | undefined ? D : never; 20 21function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 22 return obj as $Typed<T>; 23} 24 25export function hydrateEmbedImages( 26 embed: AppBskyEmbedImages.Main, 27 did: string, 28 cdn: string 29): $Typed<AppBskyEmbedImages.View> { 30 return asTyped({ 31 $type: "app.bsky.embed.images#view" as const, 32 images: embed.images 33 .map((img) => { 34 const link = img.image.ref?.["$link"]; 35 if (!link) return null; 36 return { 37 thumb: `https://${cdn}/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 fullsize: `https://${cdn}/img/feed_fullsize/plain/${did}/${link}@jpeg`, 39 alt: img.alt || "", 40 aspectRatio: img.aspectRatio, 41 }; 42 }) 43 .filter(Boolean) as AppBskyEmbedImages.ViewImage[], 44 }); 45} 46 47export function hydrateEmbedExternal( 48 embed: AppBskyEmbedExternal.Main, 49 did: string, 50 cdn: string 51): $Typed<AppBskyEmbedExternal.View> { 52 return asTyped({ 53 $type: "app.bsky.embed.external#view" as const, 54 external: { 55 uri: embed.external.uri, 56 title: embed.external.title, 57 description: embed.external.description, 58 thumb: embed.external.thumb?.ref?.$link 59 ? `https://${cdn}/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 60 : undefined, 61 }, 62 }); 63} 64 65export function hydrateEmbedVideo( 66 embed: AppBskyEmbedVideo.Main, 67 did: string, 68 videocdn: string 69): $Typed<AppBskyEmbedVideo.View> { 70 const videoLink = embed.video.ref.$link; 71 return asTyped({ 72 $type: "app.bsky.embed.video#view" as const, 73 playlist: `https://${videocdn}/watch/${did}/${videoLink}/playlist.m3u8`, 74 thumbnail: `https://${videocdn}/watch/${did}/${videoLink}/thumbnail.jpg`, 75 aspectRatio: embed.aspectRatio, 76 cid: videoLink, 77 }); 78} 79 80function hydrateEmbedRecord( 81 embed: AppBskyEmbedRecord.Main, 82 quotedPost: QueryResultData<typeof useQueryPost>, 83 quotedProfile: QueryResultData<typeof useQueryProfile>, 84 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 85 cdn: string 86): $Typed<AppBskyEmbedRecord.View> | undefined { 87 // if (!quotedPost || !quotedProfile || !quotedIdentity) { 88 // return undefined; 89 // } 90 if (!quotedPost || !quotedProfile || !quotedIdentity) { 91 const failureViewRecord: $Typed<AppBskyEmbedRecord.ViewNotFound> = asTyped({ 92 $type: "app.bsky.embed.record#viewNotFound" as const, 93 uri: embed.record.uri, 94 notFound: true as const, 95 }) 96 97 return asTyped({ 98 $type: "app.bsky.embed.record#view" as const, 99 record: failureViewRecord, 100 }); 101 } 102 103 const author: $Typed<AppBskyActorDefs.ProfileViewBasic> = asTyped({ 104 $type: "app.bsky.actor.defs#profileViewBasic" as const, 105 did: quotedIdentity.did, 106 handle: quotedIdentity.handle, 107 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 108 avatar: quotedProfile.value.avatar?.ref?.$link 109 ? `https://${cdn}/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 110 : undefined, 111 viewer: {}, 112 labels: [], 113 }); 114 115 const viewRecord: $Typed<AppBskyEmbedRecord.ViewRecord> = asTyped({ 116 $type: "app.bsky.embed.record#viewRecord" as const, 117 uri: quotedPost.uri, 118 cid: quotedPost.cid, 119 author, 120 value: quotedPost.value, 121 indexedAt: quotedPost.value.createdAt, 122 embeds: quotedPost.value.embed ? [quotedPost.value.embed] : undefined, 123 }); 124 125 return asTyped({ 126 $type: "app.bsky.embed.record#view" as const, 127 record: viewRecord, 128 }); 129} 130 131function hydrateEmbedRecordWithMedia( 132 embed: AppBskyEmbedRecordWithMedia.Main, 133 mediaHydratedEmbed: 134 | $Typed<AppBskyEmbedImages.View> 135 | $Typed<AppBskyEmbedVideo.View> 136 | $Typed<AppBskyEmbedExternal.View>, 137 quotedPost: QueryResultData<typeof useQueryPost>, 138 quotedProfile: QueryResultData<typeof useQueryProfile>, 139 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 140 cdn: string 141): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 142 const hydratedRecord = hydrateEmbedRecord( 143 embed.record, 144 quotedPost, 145 quotedProfile, 146 quotedIdentity, 147 cdn 148 ); 149 150 if (!hydratedRecord) return undefined; 151 152 return asTyped({ 153 $type: "app.bsky.embed.recordWithMedia#view" as const, 154 record: hydratedRecord, 155 media: mediaHydratedEmbed, 156 }); 157} 158 159type HydratedEmbedView = 160 | $Typed<AppBskyEmbedImages.View> 161 | $Typed<AppBskyEmbedExternal.View> 162 | $Typed<AppBskyEmbedVideo.View> 163 | $Typed<AppBskyEmbedRecord.View> 164 | $Typed<AppBskyEmbedRecordWithMedia.View>; 165 166export function useHydratedEmbed( 167 embed: AppBskyFeedPost.Record["embed"], 168 postAuthorDid: string | undefined 169) { 170 const recordInfo = useMemo(() => { 171 if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 172 const recordUri = embed.record.record.uri; 173 const quotedAuthorDid = new AtUri(recordUri).hostname; 174 return { recordUri, quotedAuthorDid, isRecordType: true }; 175 } else if (AppBskyEmbedRecord.isMain(embed)) { 176 const recordUri = embed.record.uri; 177 const quotedAuthorDid = new AtUri(recordUri).hostname; 178 return { recordUri, quotedAuthorDid, isRecordType: true }; 179 } 180 return { 181 recordUri: undefined, 182 quotedAuthorDid: undefined, 183 isRecordType: false, 184 }; 185 }, [embed]); 186 187 const { isRecordType, recordUri, quotedAuthorDid } = recordInfo; 188 189 const usequerypostresults = useQueryPost(recordUri); 190 191 const profileUri = quotedAuthorDid 192 ? `at://${quotedAuthorDid}/app.bsky.actor.profile/self` 193 : undefined; 194 195 const { 196 data: quotedProfile, 197 isLoading: isLoadingProfile, 198 error: profileError, 199 } = useQueryProfile(profileUri); 200 201 const [imgcdn] = useAtom(imgCDNAtom); 202 const [videocdn] = useAtom(videoCDNAtom); 203 204 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 205 206 const hydratedEmbed: HydratedEmbedView | undefined = (() => { 207 if (!embed || !postAuthorDid) return undefined; 208 209 // if ( 210 // isRecordType && 211 // (!usequerypostresults?.data || 212 // !quotedProfile || 213 // !queryidentityresult?.data) 214 // ) { 215 // return undefined; 216 // } 217 try { 218 if (AppBskyEmbedImages.isMain(embed)) { 219 return hydrateEmbedImages(embed, postAuthorDid, imgcdn); 220 } else if (AppBskyEmbedExternal.isMain(embed)) { 221 return hydrateEmbedExternal(embed, postAuthorDid, imgcdn); 222 } else if (AppBskyEmbedVideo.isMain(embed)) { 223 return hydrateEmbedVideo(embed, postAuthorDid, videocdn); 224 } else if (AppBskyEmbedRecord.isMain(embed)) { 225 return hydrateEmbedRecord( 226 embed, 227 usequerypostresults?.data, 228 quotedProfile, 229 queryidentityresult?.data, 230 imgcdn 231 ); 232 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 233 let hydratedMedia: 234 | $Typed<AppBskyEmbedImages.View> 235 | $Typed<AppBskyEmbedVideo.View> 236 | $Typed<AppBskyEmbedExternal.View> 237 | undefined; 238 239 if (AppBskyEmbedImages.isMain(embed.media)) { 240 hydratedMedia = hydrateEmbedImages( 241 embed.media, 242 postAuthorDid, 243 imgcdn 244 ); 245 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 246 hydratedMedia = hydrateEmbedExternal( 247 embed.media, 248 postAuthorDid, 249 imgcdn 250 ); 251 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 252 hydratedMedia = hydrateEmbedVideo( 253 embed.media, 254 postAuthorDid, 255 videocdn 256 ); 257 } 258 259 if (hydratedMedia) { 260 return hydrateEmbedRecordWithMedia( 261 embed, 262 hydratedMedia, 263 usequerypostresults?.data, 264 quotedProfile, 265 queryidentityresult?.data, 266 imgcdn 267 ); 268 } 269 } 270 } catch (e) { 271 console.error("Error hydrating embed", e); 272 return undefined; 273 } 274 })(); 275 276 const isLoading = isRecordType 277 ? usequerypostresults?.isLoading || 278 isLoadingProfile || 279 queryidentityresult?.isLoading 280 : false; 281 282 const error = 283 usequerypostresults?.error || profileError || queryidentityresult?.error; 284 285 return { data: hydratedEmbed, isLoading, error }; 286}