a tool for shared writing and social publishing
1"use server";
2
3import { supabaseServerClient } from "supabase/serverClient";
4import { Tables, TablesInsert } from "supabase/database.types";
5
6type NotificationRow = Tables<"notifications">;
7
8export type Notification = Omit<TablesInsert<"notifications">, "data"> & {
9 data: NotificationData;
10};
11
12export type NotificationData =
13 | { type: "comment"; comment_uri: string; parent_uri?: string }
14 | { type: "subscribe"; subscription_uri: string };
15
16export type HydratedNotification =
17 | HydratedCommentNotification
18 | HydratedSubscribeNotification;
19export async function hydrateNotifications(
20 notifications: NotificationRow[],
21): Promise<Array<HydratedNotification>> {
22 // Call all hydrators in parallel
23 const [commentNotifications, subscribeNotifications] = await Promise.all([
24 hydrateCommentNotifications(notifications),
25 hydrateSubscribeNotifications(notifications),
26 ]);
27
28 // Combine all hydrated notifications
29 const allHydrated = [...commentNotifications, ...subscribeNotifications];
30
31 // Sort by created_at to maintain order
32 allHydrated.sort(
33 (a, b) =>
34 new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
35 );
36
37 return allHydrated;
38}
39
40// Type guard to extract notification type
41type ExtractNotificationType<T extends NotificationData["type"]> = Extract<
42 NotificationData,
43 { type: T }
44>;
45
46export type HydratedCommentNotification = Awaited<
47 ReturnType<typeof hydrateCommentNotifications>
48>[0];
49
50async function hydrateCommentNotifications(notifications: NotificationRow[]) {
51 const commentNotifications = notifications.filter(
52 (n): n is NotificationRow & { data: ExtractNotificationType<"comment"> } =>
53 (n.data as NotificationData)?.type === "comment",
54 );
55
56 if (commentNotifications.length === 0) {
57 return [];
58 }
59
60 // Fetch comment data from the database
61 const commentUris = commentNotifications.flatMap((n) =>
62 n.data.parent_uri
63 ? [n.data.comment_uri, n.data.parent_uri]
64 : [n.data.comment_uri],
65 );
66 const { data: comments } = await supabaseServerClient
67 .from("comments_on_documents")
68 .select(
69 "*,bsky_profiles(*), documents(*, documents_in_publications(publications(*)))",
70 )
71 .in("uri", commentUris);
72
73 return commentNotifications.map((notification) => ({
74 id: notification.id,
75 recipient: notification.recipient,
76 created_at: notification.created_at,
77 type: "comment" as const,
78 comment_uri: notification.data.comment_uri,
79 parentData: notification.data.parent_uri
80 ? comments?.find((c) => c.uri === notification.data.parent_uri)!
81 : undefined,
82 commentData: comments?.find(
83 (c) => c.uri === notification.data.comment_uri,
84 )!,
85 }));
86}
87
88export type HydratedSubscribeNotification = {
89 id: string;
90 recipient: string;
91 created_at: string;
92 type: "subscribe";
93 subscription_uri: string;
94 subscriptionData?: Tables<"publication_subscriptions">;
95};
96async function hydrateSubscribeNotifications(
97 notifications: NotificationRow[],
98): Promise<HydratedSubscribeNotification[]> {
99 const subscribeNotifications = notifications.filter(
100 (
101 n,
102 ): n is NotificationRow & { data: ExtractNotificationType<"subscribe"> } =>
103 (n.data as NotificationData)?.type === "subscribe",
104 );
105
106 if (subscribeNotifications.length === 0) {
107 return [];
108 }
109
110 // Fetch subscription data from the database
111 const subscriptionUris = subscribeNotifications.map(
112 (n) => n.data.subscription_uri,
113 );
114 const { data: subscriptions } = await supabaseServerClient
115 .from("publication_subscriptions")
116 .select("*")
117 .in("uri", subscriptionUris);
118
119 return subscribeNotifications.map((notification) => ({
120 id: notification.id,
121 recipient: notification.recipient,
122 created_at: notification.created_at,
123 type: "subscribe" as const,
124 subscription_uri: notification.data.subscription_uri,
125 subscriptionData: subscriptions?.find(
126 (s) => s.uri === notification.data.subscription_uri,
127 ),
128 }));
129}
130
131export async function pingIdentityToUpdateNotification(did: string) {
132 let channel = supabaseServerClient.channel(`identity.atp_did:${did}`);
133 await channel.send({
134 type: "broadcast",
135 event: "notification",
136 payload: { message: "poke" },
137 });
138 await supabaseServerClient.removeChannel(channel);
139}