a tool for shared writing and social publishing
1"use server";
2
3import { AtpAgent } from "@atproto/api";
4import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
5import { getIdentityData } from "actions/getIdentityData";
6import { Json } from "supabase/database.types";
7import { supabaseServerClient } from "supabase/serverClient";
8import { idResolver } from "./idResolver";
9import { Cursor } from "./getReaderFeed";
10import {
11 normalizePublicationRecord,
12 type NormalizedPublication,
13} from "src/utils/normalizeRecords";
14
15export async function getSubscriptions(
16 did?: string | null,
17 cursor?: Cursor | null,
18): Promise<{
19 nextCursor: null | Cursor;
20 subscriptions: PublicationSubscription[];
21}> {
22 // If no DID provided, use logged-in user's DID
23 let identity = did;
24 if (!identity) {
25 const auth_res = await getIdentityData();
26 if (!auth_res?.atp_did) return { subscriptions: [], nextCursor: null };
27 identity = auth_res.atp_did;
28 }
29
30 let query = supabaseServerClient
31 .from("publication_subscriptions")
32 .select(`*, publications(*, documents_in_publications(*, documents(*)))`)
33 .order(`created_at`, { ascending: false })
34 .order(`uri`, { ascending: false })
35 .order("documents(sort_date)", {
36 ascending: false,
37 referencedTable: "publications.documents_in_publications",
38 })
39 .limit(1, { referencedTable: "publications.documents_in_publications" })
40 .limit(25)
41 .eq("identity", identity);
42
43 if (cursor) {
44 query = query.or(
45 `created_at.lt.${cursor.timestamp},and(created_at.eq.${cursor.timestamp},uri.lt.${cursor.uri})`,
46 );
47 }
48 let { data: pubs, error } = await query;
49
50 const hydratedSubscriptions = (
51 await Promise.all(
52 pubs?.map(async (pub) => {
53 const normalizedRecord = normalizePublicationRecord(
54 pub.publications?.record
55 );
56 if (!normalizedRecord) return null;
57 let id = await idResolver.did.resolve(pub.publications?.identity_did!);
58 return {
59 ...pub.publications!,
60 record: normalizedRecord,
61 authorProfile: id?.alsoKnownAs?.[0]
62 ? { handle: `@${id.alsoKnownAs[0].slice(5)}` }
63 : undefined,
64 } as PublicationSubscription;
65 }) || []
66 )
67 ).filter((sub): sub is PublicationSubscription => sub !== null);
68
69 const nextCursor =
70 pubs && pubs.length > 0
71 ? {
72 timestamp: pubs[pubs.length - 1].created_at,
73 uri: pubs[pubs.length - 1].uri,
74 }
75 : null;
76
77 return {
78 subscriptions: hydratedSubscriptions,
79 nextCursor,
80 };
81}
82
83export type PublicationSubscription = {
84 authorProfile?: { handle: string };
85 record: NormalizedPublication;
86 uri: string;
87 documents_in_publications: {
88 documents: { data?: Json; sort_date: string } | null;
89 }[];
90};