grain.social is a photo sharing platform built on atproto.
1import { Record as BskyProfile } from "$lexicon/types/app/bsky/actor/profile.ts";
2import { Record as Profile } from "$lexicon/types/social/grain/actor/profile.ts";
3import { GalleryView } from "$lexicon/types/social/grain/gallery/defs.ts";
4import { PhotoView } from "$lexicon/types/social/grain/photo/defs.ts";
5import { AtUri } from "@atproto/syntax";
6import { onSignedInArgs } from "@bigmoves/bff";
7import {
8 differenceInDays,
9 differenceInHours,
10 differenceInMinutes,
11 differenceInWeeks,
12} from "date-fns";
13import { PUBLIC_URL } from "./env.ts";
14
15export function formatRelativeTime(date: Date) {
16 const now = new Date();
17 const weeks = differenceInWeeks(now, date);
18 if (weeks > 0) return `${weeks}w`;
19
20 const days = differenceInDays(now, date);
21 if (days > 0) return `${days}d`;
22
23 const hours = differenceInHours(now, date);
24 if (hours > 0) return `${hours}h`;
25
26 const minutes = differenceInMinutes(now, date);
27 return `${Math.max(1, minutes)}m`;
28}
29
30export function uploadPageLink(selectedGalleryRkey?: string) {
31 return "/upload" +
32 (selectedGalleryRkey ? "?gallery=" + selectedGalleryRkey : "");
33}
34
35export function profileLink(handleOrDid: string) {
36 return `/profile/${handleOrDid}`;
37}
38
39export function followersLink(handle: string) {
40 return `/profile/${handle}/followers`;
41}
42
43export function followingLink(handle: string) {
44 return `/profile/${handle}/follows`;
45}
46
47export function galleryLink(handle: string, galleryRkey: string) {
48 return `/profile/${handle}/gallery/${galleryRkey}`;
49}
50
51export function photoDialogLink(gallery: GalleryView, image: PhotoView) {
52 return `/dialogs/image?galleryUri=${gallery.uri}&imageCid=${image.cid}`;
53}
54
55export function publicGalleryLink(handle: string, galleryUri: string): string {
56 return `${PUBLIC_URL}${galleryLink(handle, new AtUri(galleryUri).rkey)}`;
57}
58
59export function bskyProfileLink(handle: string) {
60 return `https://bsky.app/profile/${handle}`;
61}
62
63export async function onSignedIn({ actor, ctx }: onSignedInArgs) {
64 const profileResults = ctx.indexService.getRecords<Profile>(
65 "social.grain.actor.profile",
66 {
67 where: [{ field: "did", equals: actor.did }],
68 },
69 );
70
71 const profile = profileResults.items[0];
72
73 if (profile) {
74 console.log("Profile already exists");
75 return `/profile/${actor.handle}`;
76 }
77
78 // This should only happen once for new users
79 await ctx.backfillCollections({
80 externalCollections: [
81 "app.bsky.actor.profile",
82 "app.bsky.graph.follow",
83 "sh.tangled.actor.profile",
84 "sh.tangled.graph.follow",
85 ],
86 repos: [actor.did],
87 });
88
89 const bskyProfileResults = ctx.indexService.getRecords<BskyProfile>(
90 "app.bsky.actor.profile",
91 {
92 where: [{ field: "did", equals: actor.did }],
93 },
94 );
95
96 const bskyProfile = bskyProfileResults.items[0];
97
98 if (!bskyProfile) {
99 console.error("Failed to get bsky profile");
100 }
101
102 await ctx.createRecord<Profile>(
103 "social.grain.actor.profile",
104 {
105 displayName: bskyProfile?.displayName ?? undefined,
106 description: bskyProfile?.description ?? undefined,
107 avatar: bskyProfile?.avatar ?? undefined,
108 createdAt: new Date().toISOString(),
109 },
110 true,
111 );
112
113 return "/onboard";
114}