grain.social is a photo sharing platform built on atproto.

feat: enhance gallery and actor lexicons with cameras and createdAt fields; update related components to utilize new properties

Changed files
+135 -91
__generated__
types
social
grain
actor
gallery
lexicons
social
grain
actor
gallery
src
+30 -2
__generated__/lexicons.ts
··· 2684 2684 description: { 2685 2685 type: 'string', 2686 2686 }, 2687 + cameras: { 2688 + type: 'array', 2689 + description: 2690 + 'List of camera make and models used in this gallery derived from EXIF data.', 2691 + items: { 2692 + type: 'string', 2693 + }, 2694 + }, 2687 2695 facets: { 2688 2696 type: 'array', 2689 2697 description: ··· 2719 2727 type: 'ref', 2720 2728 ref: 'lex:com.atproto.label.defs#label', 2721 2729 }, 2730 + }, 2731 + createdAt: { 2732 + type: 'string', 2733 + format: 'datetime', 2722 2734 }, 2723 2735 indexedAt: { 2724 2736 type: 'string', ··· 3214 3226 defs: { 3215 3227 profileView: { 3216 3228 type: 'object', 3217 - required: ['did', 'handle'], 3229 + required: ['cid', 'did', 'handle'], 3218 3230 properties: { 3231 + cid: { 3232 + type: 'string', 3233 + format: 'cid', 3234 + }, 3219 3235 did: { 3220 3236 type: 'string', 3221 3237 format: 'did', ··· 3253 3269 }, 3254 3270 profileViewDetailed: { 3255 3271 type: 'object', 3256 - required: ['did', 'handle'], 3272 + required: ['cid', 'did', 'handle'], 3257 3273 properties: { 3274 + cid: { 3275 + type: 'string', 3276 + format: 'cid', 3277 + }, 3258 3278 did: { 3259 3279 type: 'string', 3260 3280 format: 'did', ··· 3276 3296 avatar: { 3277 3297 type: 'string', 3278 3298 format: 'uri', 3299 + }, 3300 + cameras: { 3301 + type: 'array', 3302 + items: { 3303 + type: 'string', 3304 + }, 3305 + description: 3306 + 'List of camera make and models used by this actor derived from EXIF data of photos linked to galleries.', 3279 3307 }, 3280 3308 followersCount: { 3281 3309 type: 'integer',
+4
__generated__/types/social/grain/actor/defs.ts
··· 17 17 18 18 export interface ProfileView { 19 19 $type?: 'social.grain.actor.defs#profileView' 20 + cid: string 20 21 did: string 21 22 handle: string 22 23 displayName?: string ··· 38 39 39 40 export interface ProfileViewDetailed { 40 41 $type?: 'social.grain.actor.defs#profileViewDetailed' 42 + cid: string 41 43 did: string 42 44 handle: string 43 45 displayName?: string 44 46 description?: string 45 47 avatar?: string 48 + /** List of camera make and models used by this actor derived from EXIF data of photos linked to galleries. */ 49 + cameras?: string[] 46 50 followersCount?: number 47 51 followsCount?: number 48 52 galleryCount?: number
+3
__generated__/types/social/grain/gallery/defs.ts
··· 24 24 cid: string 25 25 title?: string 26 26 description?: string 27 + /** List of camera make and models used in this gallery derived from EXIF data. */ 28 + cameras?: string[] 27 29 /** Annotations of description text (mentions, URLs, hashtags, etc) */ 28 30 facets?: AppBskyRichtextFacet.Main[] 29 31 creator: SocialGrainActorDefs.ProfileView ··· 32 34 favCount?: number 33 35 commentCount?: number 34 36 labels?: ComAtprotoLabelDefs.Label[] 37 + createdAt?: string 35 38 indexedAt: string 36 39 viewer?: ViewerState 37 40 }
+9 -2
lexicons/social/grain/actor/defs.json
··· 4 4 "defs": { 5 5 "profileView": { 6 6 "type": "object", 7 - "required": ["did", "handle"], 7 + "required": ["cid", "did", "handle"], 8 8 "properties": { 9 + "cid": { "type": "string", "format": "cid" }, 9 10 "did": { "type": "string", "format": "did" }, 10 11 "handle": { "type": "string", "format": "handle" }, 11 12 "displayName": { ··· 31 32 }, 32 33 "profileViewDetailed": { 33 34 "type": "object", 34 - "required": ["did", "handle"], 35 + "required": ["cid", "did", "handle"], 35 36 "properties": { 37 + "cid": { "type": "string", "format": "cid" }, 36 38 "did": { "type": "string", "format": "did" }, 37 39 "handle": { "type": "string", "format": "handle" }, 38 40 "displayName": { ··· 46 48 "maxLength": 2560 47 49 }, 48 50 "avatar": { "type": "string", "format": "uri" }, 51 + "cameras": { 52 + "type": "array", 53 + "items": { "type": "string" }, 54 + "description": "List of camera make and models used by this actor derived from EXIF data of photos linked to galleries." 55 + }, 49 56 "followersCount": { "type": "integer" }, 50 57 "followsCount": { "type": "integer" }, 51 58 "galleryCount": { "type": "integer" },
+6
lexicons/social/grain/gallery/defs.json
··· 10 10 "cid": { "type": "string", "format": "cid" }, 11 11 "title": { "type": "string" }, 12 12 "description": { "type": "string" }, 13 + "cameras": { 14 + "type": "array", 15 + "description": "List of camera make and models used in this gallery derived from EXIF data.", 16 + "items": { "type": "string" } 17 + }, 13 18 "facets": { 14 19 "type": "array", 15 20 "description": "Annotations of description text (mentions, URLs, hashtags, etc)", ··· 35 40 "type": "array", 36 41 "items": { "type": "ref", "ref": "com.atproto.label.defs#label" } 37 42 }, 43 + "createdAt": { "type": "string", "format": "datetime" }, 38 44 "indexedAt": { "type": "string", "format": "datetime" }, 39 45 "viewer": { "type": "ref", "ref": "#viewerState" } 40 46 }
+4 -7
src/components/GalleryInfo.tsx
··· 1 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 1 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 3 - import { getGalleryCameras } from "../lib/gallery.ts"; 4 2 import { ActorInfo } from "./ActorInfo.tsx"; 5 3 import { CameraBadges } from "./CameraBadges.tsx"; 6 4 import { RenderFacetedText } from "./RenderFacetedText.tsx"; ··· 8 6 export function GalleryInfo( 9 7 { gallery }: Readonly<{ gallery: GalleryView }>, 10 8 ) { 11 - const description = (gallery.record as Gallery).description; 12 - const facets = (gallery.record as Gallery).facets; 13 - const cameras = getGalleryCameras(gallery); 9 + const description = gallery.description; 10 + const facets = gallery.facets; 14 11 return ( 15 12 <div 16 13 class="flex flex-col space-y-2 mb-4 max-w-[500px]" 17 14 id="gallery-info" 18 15 > 19 16 <h1 class="font-bold text-2xl"> 20 - {(gallery.record as Gallery).title} 17 + {gallery.title} 21 18 </h1> 22 19 <ActorInfo profile={gallery.creator} /> 23 20 {description ··· 27 24 </p> 28 25 ) 29 26 : null} 30 - <CameraBadges class="my-1" cameras={cameras} /> 27 + <CameraBadges class="my-1" cameras={gallery.cameras ?? []} /> 31 28 </div> 32 29 ); 33 30 }
+4 -5
src/components/GallerySelectDialog.tsx
··· 1 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 1 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 3 2 import { AtUri } from "@atproto/syntax"; 4 3 import { Button } from "./Button.tsx"; ··· 72 71 hx-swap="none" 73 72 class="block text-left w-full px-2 py-4" 74 73 > 75 - {(gallery.record as Gallery).title} 74 + {gallery.title} 76 75 <div class="text-sm text-zinc-600 dark:text-zinc-500"> 77 - {(gallery.record as Gallery).description} 76 + {gallery.description} 78 77 </div> 79 78 </button> 80 79 ) ··· 83 82 href={uploadPageLink(gallery.uri)} 84 83 class="block w-full px-2 py-4" 85 84 > 86 - {(gallery.record as Gallery).title} 85 + {gallery.title} 87 86 <div class="text-sm text-zinc-600 dark:text-zinc-500"> 88 - {(gallery.record as Gallery).description} 87 + {gallery.description} 89 88 </div> 90 89 </a> 91 90 )}
+1 -1
src/components/NotificationsPage.tsx
··· 300 300 return ( 301 301 <div class="text-sm border-l-2 border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-500 pl-2"> 302 302 <RenderFacetedText 303 - text={(gallery.record as Gallery).description ?? ""} 303 + text={gallery.description ?? ""} 304 304 facets={galleryRecord.facets} 305 305 /> 306 306 <div class="mt-2 max-w-[200px]">
+8 -20
src/components/ProfilePage.tsx
··· 1 1 import { LabelValueDefinition } from "$lexicon/types/com/atproto/label/defs.ts"; 2 - import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts"; 3 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 + import { ProfileViewDetailed } from "$lexicon/types/social/grain/actor/defs.ts"; 4 3 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 5 4 import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 6 5 import { Un$Typed } from "$lexicon/util.ts"; 7 6 import { Facet } from "@atproto/api"; 8 7 import { AtUri } from "@atproto/syntax"; 9 8 import { LabelerPolicies } from "@bigmoves/bff"; 10 - import { getGalleryCameras } from "../lib/gallery.ts"; 11 9 import { 12 10 atprotoLabelValueDefinitions, 13 11 ModerationDecsion, ··· 32 30 export type ProfileTabs = "favs" | "galleries" | "labels"; 33 31 34 32 export function ProfilePage({ 35 - followUri, 36 - followersCount, 37 - followingCount, 38 33 userProfiles, 39 34 loggedInUserDid, 40 35 profile, ··· 46 41 isLabeler, 47 42 labelerDefinitions, 48 43 }: Readonly<{ 49 - followUri?: string; 50 - followersCount?: number; 51 - followingCount?: number; 52 44 userProfiles: SocialNetwork[]; 53 45 actorProfiles: SocialNetwork[]; 54 46 loggedInUserDid?: string; 55 - profile: Un$Typed<ProfileView>; 47 + profile: Un$Typed<ProfileViewDetailed>; 56 48 descriptionFacets?: Facet[]; 57 49 selectedTab?: ProfileTabs; 58 50 galleries?: GalleryView[]; ··· 63 55 }>) { 64 56 const isCreator = loggedInUserDid === profile.did; 65 57 const displayName = profile.displayName || profile.handle; 66 - const cameras = Array.from( 67 - new Set(galleries?.flatMap(getGalleryCameras) ?? []), 68 - ); 69 58 return ( 70 59 <div class="px-4 mb-4" id="profile-page"> 71 60 <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between my-4"> ··· 80 69 <p class="space-x-1"> 81 70 <a href={followersLink(profile.handle)}> 82 71 <span class="font-semibold" id="followers-count"> 83 - {followersCount ?? 0} 72 + {profile.followersCount ?? 0} 84 73 </span>{" "} 85 74 <span class="text-zinc-600 dark:text-zinc-500"> 86 75 followers ··· 88 77 </a>{" "} 89 78 <a href={followingLink(profile.handle)}> 90 79 <span class="font-semibold" id="following-count"> 91 - {followingCount ?? 0} 80 + {profile.followsCount ?? 0} 92 81 </span>{" "} 93 82 <span class="text-zinc-600 dark:text-zinc-500"> 94 83 following ··· 97 86 <span class="font-semibold">{galleries?.length ?? 0}</span> 98 87 <span class="text-zinc-600 dark:text-zinc-500">galleries</span> 99 88 </p> 100 - <CameraBadges cameras={cameras} class="mt-2" /> 89 + <CameraBadges cameras={profile.cameras ?? []} class="mt-2" /> 101 90 </> 102 91 )} 103 92 {profile.description ··· 135 124 <div class="flex self-start gap-2 w-full sm:w-fit flex-col sm:flex-row"> 136 125 <FollowButton 137 126 followeeDid={profile.did} 138 - followUri={followUri} 127 + followUri={profile.viewer?.following} 139 128 /> 140 129 </div> 141 130 ) ··· 356 345 ) 357 346 : <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />} 358 347 <div class="absolute sm:flex hidden bottom-0 left-0 bg-black/80 text-white p-2 items-center gap-2"> 359 - {(gallery.record as Gallery).title} 348 + {gallery.title} 360 349 </div> 361 350 </a> 362 351 ); ··· 387 376 ) 388 377 : <div class="w-full h-full bg-zinc-200 dark:bg-zinc-900" />} 389 378 <div class="absolute bottom-0 left-0 bg-black/80 text-white p-2 hidden sm:flex items-center gap-2"> 390 - <ActorAvatar profile={gallery.creator} size={20} />{" "} 391 - {(gallery.record as Gallery).title} 379 + <ActorAvatar profile={gallery.creator} size={20} /> {gallery.title} 392 380 </div> 393 381 </a> 394 382 );
+3 -4
src/components/TimelineItem.tsx
··· 1 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 1 import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 3 2 import { type TimelineItem } from "../lib/timeline.ts"; 4 3 import { CommentsButton } from "../modules/comments.tsx"; ··· 10 9 import { RenderFacetedText } from "./RenderFacetedText.tsx"; 11 10 12 11 export function TimelineItem({ item }: Readonly<{ item: TimelineItem }>) { 13 - const title = (item.gallery.record as Gallery).title; 14 - const description = (item.gallery.record as Gallery).description; 15 - const facets = (item.gallery.record as Gallery).facets; 12 + const title = item.gallery.title; 13 + const description = item.gallery.description; 14 + const facets = item.gallery.facets; 16 15 return ( 17 16 <li> 18 17 <div class="flex flex-col pb-4 max-w-md">
+38 -16
src/lib/actor.ts
··· 10 10 import { Record as Favorite } from "$lexicon/types/social/grain/favorite.ts"; 11 11 import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 12 12 import { Record as Photo } from "$lexicon/types/social/grain/photo.ts"; 13 + import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 13 14 import { Record as PhotoExif } from "$lexicon/types/social/grain/photo/exif.ts"; 14 15 import { Un$Typed } from "$lexicon/util.ts"; 15 16 import { BffContext, WithBffMeta } from "@bigmoves/bff"; 16 17 import { getFollow, getFollowersCount, getFollowsCount } from "./follow.ts"; 17 18 import { 18 19 galleryToView, 19 - getGalleryCount, 20 + getGalleryCameras, 20 21 getGalleryItemsAndPhotos, 21 22 } from "./gallery.ts"; 22 23 import { photoToView, photoUrl } from "./photo.ts"; ··· 39 40 ); 40 41 const followersCount = getFollowersCount(did, ctx); 41 42 const followsCount = getFollowsCount(did, ctx); 42 - const galleryCount = getGalleryCount(did, ctx); 43 + const galleries = getActorGalleries(did, ctx); 44 + const cameras = Array.from( 45 + new Set( 46 + galleries.flatMap((g) => 47 + getGalleryCameras(g.items?.filter(isPhotoView) ?? []) 48 + ), 49 + ), 50 + ).sort((a, b) => a.localeCompare(b)); 43 51 44 52 let followedBy: string | undefined = ""; 45 53 let following: string | undefined = ""; ··· 49 57 } 50 58 51 59 return profileRecord 52 - ? profileDetailedToView( 53 - profileRecord, 54 - actor.handle, 60 + ? profileDetailedToView({ 61 + record: profileRecord, 62 + handle: actor.handle, 63 + cameras, 55 64 followersCount, 56 65 followsCount, 57 - galleryCount, 58 - { 66 + galleryCount: galleries.length, 67 + viewer: { 59 68 followedBy, 60 69 following, 61 70 }, 62 - ) 71 + }) 63 72 : null; 64 73 } 65 74 ··· 68 77 handle: string, 69 78 ): Un$Typed<ProfileView> { 70 79 return { 80 + cid: record.cid, 71 81 did: record.did, 72 82 handle, 73 83 displayName: record.displayName, ··· 78 88 }; 79 89 } 80 90 81 - export function profileDetailedToView( 82 - record: WithBffMeta<GrainProfile>, 83 - handle: string, 84 - followersCount: number, 85 - followsCount: number, 86 - galleryCount: number, 87 - viewer: ViewerState, 88 - ): Un$Typed<ProfileViewDetailed> { 91 + export function profileDetailedToView(params: { 92 + record: WithBffMeta<GrainProfile>; 93 + handle: string; 94 + followersCount: number; 95 + followsCount: number; 96 + galleryCount: number; 97 + viewer: ViewerState; 98 + cameras?: string[]; 99 + }): Un$Typed<ProfileViewDetailed> { 100 + const { 101 + record, 102 + handle, 103 + followersCount, 104 + followsCount, 105 + galleryCount, 106 + viewer, 107 + cameras, 108 + } = params; 89 109 return { 110 + cid: record.cid, 90 111 did: record.did, 91 112 handle, 92 113 displayName: record.displayName, ··· 98 119 followsCount, 99 120 galleryCount, 100 121 viewer, 122 + cameras, 101 123 }; 102 124 } 103 125
+15 -9
src/lib/gallery.ts
··· 212 212 favCount?: number; 213 213 commentCount?: number; 214 214 viewerState?: ViewerState; 215 + cameras?: string[]; 215 216 }): $Typed<GalleryView> { 217 + const viewItems = items 218 + ?.map((item) => itemToView(record.did, item)) 219 + .filter(isPhotoView); 220 + const cameras = getGalleryCameras(viewItems); 216 221 return { 217 222 $type: "social.grain.gallery.defs#galleryView", 218 223 uri: record.uri, 219 224 cid: record.cid, 220 225 title: record.title, 221 226 description: record.description, 227 + cameras, 222 228 facets: record.facets, 223 229 creator, 224 230 record, 225 - items: items 226 - ?.map((item) => itemToView(record.did, item)) 227 - .filter(isPhotoView), 231 + items: viewItems, 228 232 labels, 229 - indexedAt: record.indexedAt, 230 233 favCount, 231 234 commentCount, 232 235 viewer: viewerState, 236 + createdAt: record.createdAt, 237 + indexedAt: record.indexedAt, 233 238 }; 234 239 } 235 240 ··· 248 253 } 249 254 250 255 export function getGalleryCameras( 251 - gallery: GalleryView, 256 + items: Array<PhotoWithExif | PhotoView>, 252 257 ): string[] { 253 - const photos = gallery.items?.filter(isPhotoView) ?? []; 254 258 const cameras = new Set<string>(); 255 - for (const photo of photos) { 256 - if (photo.exif?.make) { 257 - cameras.add(`${photo.exif.make} ${photo.exif.model}`.trim()); 259 + if (!Array.isArray(items)) return []; 260 + for (const item of items) { 261 + const exif = "exif" in item ? item.exif : (item as PhotoView).exif; 262 + if (exif?.make && exif?.model) { 263 + cameras.add(`${exif.make} ${exif.model}`.trim()); 258 264 } 259 265 } 260 266 return Array.from(cameras);
+2 -3
src/meta.ts
··· 1 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 1 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 3 2 import { isPhotoView } from "$lexicon/types/social/grain/photo/defs.ts"; 4 3 import { AtUri } from "@atproto/syntax"; ··· 29 28 ) 30 29 }`, 31 30 }, 32 - { property: "og:title", content: (gallery.record as Gallery).title }, 31 + { property: "og:title", content: gallery.title }, 33 32 { 34 33 property: "og:description", 35 - content: (gallery.record as Gallery).description, 34 + content: gallery.description, 36 35 }, 37 36 { 38 37 property: "og:image",
+2 -3
src/modules/comments.tsx
··· 1 1 import { ProfileView } from "$lexicon/types/social/grain/actor/defs.ts"; 2 2 import { Record as Comment } from "$lexicon/types/social/grain/comment.ts"; 3 3 import { CommentView } from "$lexicon/types/social/grain/comment/defs.ts"; 4 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 5 4 import { 6 5 GalleryView, 7 6 isGalleryView, ··· 64 63 /> 65 64 )} 66 65 {!comment && !photo && gallery && 67 - (gallery.record as Gallery).title} 66 + gallery.title} 68 67 {!comment && !photo && gallery 69 68 ? ( 70 69 <div class="w-[200px] pointer-events-none"> ··· 145 144 {gallery.creator 146 145 ? <div class="font-semibold">{gallery.creator.displayName}</div> 147 146 : null} 148 - {(gallery.record as Gallery).title} 147 + {gallery.title} 149 148 <div class="w-[200px] pointer-events-none"> 150 149 <GalleryPreviewLink 151 150 gallery={gallery}
+1 -2
src/routes/gallery.tsx
··· 1 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 1 import { BffContext, RouteHandler } from "@bigmoves/bff"; 3 2 import { GalleryPage } from "../components/GalleryPage.tsx"; 4 3 import { getGallery } from "../lib/gallery.ts"; ··· 20 19 if (!gallery) return ctx.next(); 21 20 22 21 ctx.state.meta = [ 23 - { title: `${(gallery.record as Gallery).title} — Grain` }, 22 + { title: `${gallery.title} — Grain` }, 24 23 ...getPageMeta(galleryLink(handle, rkey)), 25 24 ...getGalleryMeta(gallery), 26 25 ];
+3 -4
src/routes/hashtag.tsx
··· 1 - import { Record as Gallery } from "$lexicon/types/social/grain/gallery.ts"; 2 1 import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts"; 3 2 import { BffContext, RouteHandler } from "@bigmoves/bff"; 4 3 import { ActorInfo } from "../components/ActorInfo.tsx"; ··· 48 47 gallery: GalleryView; 49 48 }>, 50 49 ) { 51 - const title = (gallery.record as Gallery).title; 52 - const description = (gallery.record as Gallery).description; 53 - const facets = (gallery.record as Gallery).facets || []; 50 + const title = gallery.title; 51 + const description = gallery.description; 52 + const facets = gallery.facets || []; 54 53 return ( 55 54 <div class="flex flex-col gap-2" key={gallery.uri}> 56 55 <ActorInfo profile={gallery.creator} />
+2 -13
src/routes/profile.tsx
··· 9 9 import { 10 10 getActorGalleries, 11 11 getActorGalleryFavs, 12 - getActorProfile, 12 + getActorProfileDetailed, 13 13 getActorProfiles, 14 14 } from "../lib/actor.ts"; 15 - import { getFollow, getFollowers, getFollowing } from "../lib/follow.ts"; 16 15 import { 17 16 isLabeler as isLabelerFn, 18 17 moderateGallery, ··· 45 44 const isHxRequest = req.headers.get("hx-request") !== null; 46 45 const render = isHxRequest ? ctx.html : ctx.render; 47 46 48 - const profile = getActorProfile(actor.did, ctx); 47 + const profile = getActorProfileDetailed(actor.did, ctx); 49 48 const galleries = getActorGalleries(actor.did, ctx); 50 - const followers = getFollowers(actor.did, ctx); 51 - const following = getFollowing(actor.did, ctx); 52 49 53 50 let descriptionFacets: RichText["facets"] = undefined; 54 51 if (profile?.description) { ··· 80 77 81 78 if (!profile) return ctx.next(); 82 79 83 - let followUri: string | undefined; 84 80 let actorProfiles: SocialNetwork[] = []; 85 81 let userProfiles: SocialNetwork[] = []; 86 82 87 83 if (ctx.currentUser) { 88 - followUri = getFollow(profile.did, ctx.currentUser.did, ctx)?.uri; 89 84 actorProfiles = getActorProfiles(ctx.currentUser.did, ctx); 90 85 } 91 86 ··· 104 99 const galleryFavs = getActorGalleryFavs(actor.did, ctx); 105 100 return render( 106 101 <ProfilePage 107 - followersCount={followers.length} 108 - followingCount={following.length} 109 102 userProfiles={userProfiles} 110 103 actorProfiles={actorProfiles} 111 - followUri={followUri} 112 104 loggedInUserDid={ctx.currentUser?.did} 113 105 profile={profile} 114 106 selectedTab="favs" ··· 120 112 } 121 113 return render( 122 114 <ProfilePage 123 - followersCount={followers.length} 124 - followingCount={following.length} 125 115 userProfiles={userProfiles} 126 116 actorProfiles={actorProfiles} 127 - followUri={followUri} 128 117 loggedInUserDid={ctx.currentUser?.did} 129 118 profile={profile} 130 119 descriptionFacets={descriptionFacets}