grain.social is a photo sharing platform built on atproto.
1import { RichText } from "@atproto/api";
2import {
3 ActorTable,
4 BffContext,
5 LabelerPolicies,
6 RouteHandler,
7} from "@bigmoves/bff";
8import { ProfilePage, ProfileTabs } from "../components/ProfilePage.tsx";
9import {
10 getActorGalleries,
11 getActorGalleryFavs,
12 getActorProfileDetailed,
13 getActorProfiles,
14} from "../lib/actor.ts";
15import {
16 isLabeler as isLabelerFn,
17 moderateGallery,
18 ModerationDecsion,
19} from "../lib/moderation.ts";
20import { parseFacetedText } from "../lib/rich_text.ts";
21import { type SocialNetwork } from "../lib/timeline.ts";
22import { getPageMeta } from "../meta.ts";
23import type { State } from "../state.ts";
24import { profileLink } from "../utils.ts";
25
26export const handler: RouteHandler = async (
27 req,
28 params,
29 ctx: BffContext<State>,
30) => {
31 const url = new URL(req.url);
32 const tab = url.searchParams.get("tab") as ProfileTabs;
33 const handleOrDid = params.handleOrDid;
34
35 let actor: ActorTable | undefined;
36 if (handleOrDid.includes("did:")) {
37 actor = ctx.indexService.getActor(handleOrDid);
38 } else {
39 actor = ctx.indexService.getActorByHandle(handleOrDid);
40 }
41
42 if (!actor) return ctx.next();
43
44 const isHxRequest = req.headers.get("hx-request") !== null;
45 const render = isHxRequest ? ctx.html : ctx.render;
46
47 const profile = getActorProfileDetailed(actor.did, ctx);
48 const galleries = getActorGalleries(actor.did, ctx);
49
50 let descriptionFacets: RichText["facets"] = undefined;
51 if (profile?.description) {
52 const resp = parseFacetedText(profile?.description, ctx);
53 descriptionFacets = resp.facets;
54 }
55
56 let labelerDefinitions: LabelerPolicies | undefined = undefined;
57 const isLabeler = await isLabelerFn(actor.did, ctx);
58 if (isLabeler) {
59 const labelerDefs = await ctx.getLabelerDefinitions();
60 labelerDefinitions = labelerDefs[actor.did] ?? [];
61 }
62
63 const galleryModDecisionsMap = new Map<string, ModerationDecsion>();
64 for (const gallery of galleries) {
65 if (!gallery.labels || gallery.labels.length === 0) {
66 continue;
67 }
68 const modDecision = await moderateGallery(
69 gallery.labels ?? [],
70 ctx,
71 );
72 if (!modDecision) {
73 continue;
74 }
75 galleryModDecisionsMap.set(gallery.uri, modDecision);
76 }
77
78 if (!profile) return ctx.next();
79
80 let actorProfiles: SocialNetwork[] = [];
81 let userProfiles: SocialNetwork[] = [];
82
83 if (ctx.currentUser) {
84 actorProfiles = getActorProfiles(ctx.currentUser.did, ctx);
85 }
86
87 userProfiles = getActorProfiles(actor.did, ctx);
88
89 ctx.state.meta = [
90 {
91 title: profile.displayName
92 ? `${profile.displayName} (${profile.handle}) — Grain`
93 : `${profile.handle} — Grain`,
94 },
95 ...getPageMeta(profileLink(actor.did)),
96 ];
97
98 if (tab === "favs") {
99 const galleryFavs = getActorGalleryFavs(actor.did, ctx);
100 return render(
101 <ProfilePage
102 userProfiles={userProfiles}
103 actorProfiles={actorProfiles}
104 loggedInUserDid={ctx.currentUser?.did}
105 profile={profile}
106 selectedTab="favs"
107 galleries={galleries}
108 galleryFavs={galleryFavs}
109 galleryModDecisionsMap={galleryModDecisionsMap}
110 />,
111 );
112 }
113 return render(
114 <ProfilePage
115 userProfiles={userProfiles}
116 actorProfiles={actorProfiles}
117 loggedInUserDid={ctx.currentUser?.did}
118 profile={profile}
119 descriptionFacets={descriptionFacets}
120 selectedTab={isLabeler ? "labels" : "galleries"}
121 galleries={galleries}
122 galleryModDecisionsMap={galleryModDecisionsMap}
123 isLabeler={isLabeler}
124 labelerDefinitions={labelerDefinitions}
125 />,
126 );
127};