an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

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

at fb3fbe804846b1c4d99ad65d51c48ebf2db6c13d 245 lines 7.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 { useMemo } from "react"; 13 14import { useQueryIdentity,useQueryPost, useQueryProfile } from "./useQuery"; 15 16type QueryResultData<T extends (...args: any) => any> = ReturnType<T> extends 17 | { data: infer D } 18 | undefined 19 ? D 20 : never; 21 22function asTyped<T extends { $type: string }>(obj: T): $Typed<T> { 23 return obj as $Typed<T>; 24} 25 26export function hydrateEmbedImages( 27 embed: AppBskyEmbedImages.Main, 28 did: 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.bsky.app/img/feed_thumbnail/plain/${did}/${link}@jpeg`, 38 fullsize: `https://cdn.bsky.app/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): $Typed<AppBskyEmbedExternal.View> { 51 return asTyped({ 52 $type: "app.bsky.embed.external#view" as const, 53 external: { 54 uri: embed.external.uri, 55 title: embed.external.title, 56 description: embed.external.description, 57 thumb: embed.external.thumb?.ref?.$link 58 ? `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${embed.external.thumb.ref.$link}@jpeg` 59 : undefined, 60 }, 61 }); 62} 63 64export function hydrateEmbedVideo( 65 embed: AppBskyEmbedVideo.Main, 66 did: string, 67): $Typed<AppBskyEmbedVideo.View> { 68 const videoLink = embed.video.ref.$link; 69 return asTyped({ 70 $type: "app.bsky.embed.video#view" as const, 71 playlist: `https://video.bsky.app/watch/${did}/${videoLink}/playlist.m3u8`, 72 thumbnail: `https://video.bsky.app/watch/${did}/${videoLink}/thumbnail.jpg`, 73 aspectRatio: embed.aspectRatio, 74 cid: videoLink, 75 }); 76} 77 78function hydrateEmbedRecord( 79 embed: AppBskyEmbedRecord.Main, 80 quotedPost: QueryResultData<typeof useQueryPost>, 81 quotedProfile: QueryResultData<typeof useQueryProfile>, 82 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 83): $Typed<AppBskyEmbedRecord.View> | undefined { 84 if (!quotedPost || !quotedProfile || !quotedIdentity) { 85 return undefined; 86 } 87 88 const author: $Typed<AppBskyActorDefs.ProfileViewBasic> = asTyped({ 89 $type: "app.bsky.actor.defs#profileViewBasic" as const, 90 did: quotedIdentity.did, 91 handle: quotedIdentity.handle, 92 displayName: quotedProfile.value.displayName ?? quotedIdentity.handle, 93 avatar: quotedProfile.value.avatar?.ref?.$link 94 ? `https://cdn.bsky.app/img/avatar/plain/${quotedIdentity.did}/${quotedProfile.value.avatar.ref.$link}@jpeg` 95 : undefined, 96 viewer: {}, 97 labels: [], 98 }); 99 100 const viewRecord: $Typed<AppBskyEmbedRecord.ViewRecord> = asTyped({ 101 $type: "app.bsky.embed.record#viewRecord" as const, 102 uri: quotedPost.uri, 103 cid: quotedPost.cid, 104 author, 105 value: quotedPost.value, 106 indexedAt: quotedPost.value.createdAt, 107 embeds: quotedPost.value.embed ? [quotedPost.value.embed] : undefined, 108 }); 109 110 return asTyped({ 111 $type: "app.bsky.embed.record#view" as const, 112 record: viewRecord, 113 }); 114} 115 116function hydrateEmbedRecordWithMedia( 117 embed: AppBskyEmbedRecordWithMedia.Main, 118 mediaHydratedEmbed: 119 | $Typed<AppBskyEmbedImages.View> 120 | $Typed<AppBskyEmbedVideo.View> 121 | $Typed<AppBskyEmbedExternal.View>, 122 quotedPost: QueryResultData<typeof useQueryPost>, 123 quotedProfile: QueryResultData<typeof useQueryProfile>, 124 quotedIdentity: QueryResultData<typeof useQueryIdentity>, 125): $Typed<AppBskyEmbedRecordWithMedia.View> | undefined { 126 const hydratedRecord = hydrateEmbedRecord( 127 embed.record, 128 quotedPost, 129 quotedProfile, 130 quotedIdentity, 131 ); 132 133 if (!hydratedRecord) return undefined; 134 135 return asTyped({ 136 $type: "app.bsky.embed.recordWithMedia#view" as const, 137 record: hydratedRecord, 138 media: mediaHydratedEmbed, 139 }); 140} 141 142type HydratedEmbedView = 143 | $Typed<AppBskyEmbedImages.View> 144 | $Typed<AppBskyEmbedExternal.View> 145 | $Typed<AppBskyEmbedVideo.View> 146 | $Typed<AppBskyEmbedRecord.View> 147 | $Typed<AppBskyEmbedRecordWithMedia.View>; 148 149export function useHydratedEmbed( 150 embed: AppBskyFeedPost.Record["embed"], 151 postAuthorDid: string | undefined, 152) { 153 const recordInfo = useMemo(() => { 154 if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 155 const recordUri = embed.record.record.uri; 156 const quotedAuthorDid = new AtUri(recordUri).hostname; 157 return { recordUri, quotedAuthorDid, isRecordType: true }; 158 } else if (AppBskyEmbedRecord.isMain(embed)) { 159 const recordUri = embed.record.uri; 160 const quotedAuthorDid = new AtUri(recordUri).hostname; 161 return { recordUri, quotedAuthorDid, isRecordType: true }; 162 } 163 return { 164 recordUri: undefined, 165 quotedAuthorDid: undefined, 166 isRecordType: false, 167 }; 168 }, [embed]); 169 170 const { isRecordType, recordUri, quotedAuthorDid } = recordInfo; 171 172 const usequerypostresults = useQueryPost(recordUri); 173 174 const profileUri = quotedAuthorDid 175 ? `at://${quotedAuthorDid}/app.bsky.actor.profile/self` 176 : undefined; 177 178 const { 179 data: quotedProfile, 180 isLoading: isLoadingProfile, 181 error: profileError, 182 } = useQueryProfile(profileUri); 183 184 const queryidentityresult = useQueryIdentity(quotedAuthorDid); 185 186 const hydratedEmbed: HydratedEmbedView | undefined = (() => { 187 if (!embed || !postAuthorDid) return undefined; 188 189 if (isRecordType && (!usequerypostresults?.data || !quotedProfile || !queryidentityresult?.data)) { 190 return undefined; 191 } 192 193 try { 194 if (AppBskyEmbedImages.isMain(embed)) { 195 return hydrateEmbedImages(embed, postAuthorDid); 196 } else if (AppBskyEmbedExternal.isMain(embed)) { 197 return hydrateEmbedExternal(embed, postAuthorDid); 198 } else if (AppBskyEmbedVideo.isMain(embed)) { 199 return hydrateEmbedVideo(embed, postAuthorDid); 200 } else if (AppBskyEmbedRecord.isMain(embed)) { 201 return hydrateEmbedRecord( 202 embed, 203 usequerypostresults?.data, 204 quotedProfile, 205 queryidentityresult?.data, 206 ); 207 } else if (AppBskyEmbedRecordWithMedia.isMain(embed)) { 208 let hydratedMedia: 209 | $Typed<AppBskyEmbedImages.View> 210 | $Typed<AppBskyEmbedVideo.View> 211 | $Typed<AppBskyEmbedExternal.View> 212 | undefined; 213 214 if (AppBskyEmbedImages.isMain(embed.media)) { 215 hydratedMedia = hydrateEmbedImages(embed.media, postAuthorDid); 216 } else if (AppBskyEmbedExternal.isMain(embed.media)) { 217 hydratedMedia = hydrateEmbedExternal(embed.media, postAuthorDid); 218 } else if (AppBskyEmbedVideo.isMain(embed.media)) { 219 hydratedMedia = hydrateEmbedVideo(embed.media, postAuthorDid); 220 } 221 222 if (hydratedMedia) { 223 return hydrateEmbedRecordWithMedia( 224 embed, 225 hydratedMedia, 226 usequerypostresults?.data, 227 quotedProfile, 228 queryidentityresult?.data, 229 ); 230 } 231 } 232 } catch (e) { 233 console.error("Error hydrating embed", e); 234 return undefined; 235 } 236 })(); 237 238 const isLoading = isRecordType 239 ? usequerypostresults?.isLoading || isLoadingProfile || queryidentityresult?.isLoading 240 : false; 241 242 const error = usequerypostresults?.error || profileError || queryidentityresult?.error; 243 244 return { data: hydratedEmbed, isLoading, error }; 245}