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 = Awaited<
89 ReturnType<typeof hydrateSubscribeNotifications>
90>[0];
91
92async function hydrateSubscribeNotifications(notifications: NotificationRow[]) {
93 const subscribeNotifications = notifications.filter(
94 (
95 n,
96 ): n is NotificationRow & { data: ExtractNotificationType<"subscribe"> } =>
97 (n.data as NotificationData)?.type === "subscribe",
98 );
99
100 if (subscribeNotifications.length === 0) {
101 return [];
102 }
103
104 // Fetch subscription data from the database with related data
105 const subscriptionUris = subscribeNotifications.map(
106 (n) => n.data.subscription_uri,
107 );
108 const { data: subscriptions } = await supabaseServerClient
109 .from("publication_subscriptions")
110 .select("*, identities(bsky_profiles(*)), publications(*)")
111 .in("uri", subscriptionUris);
112
113 return subscribeNotifications.map((notification) => ({
114 id: notification.id,
115 recipient: notification.recipient,
116 created_at: notification.created_at,
117 type: "subscribe" as const,
118 subscription_uri: notification.data.subscription_uri,
119 subscriptionData: subscriptions?.find(
120 (s) => s.uri === notification.data.subscription_uri,
121 )!,
122 }));
123}
124
125export async function pingIdentityToUpdateNotification(did: string) {
126 let channel = supabaseServerClient.channel(`identity.atp_did:${did}`);
127 await channel.send({
128 type: "broadcast",
129 event: "notification",
130 payload: { message: "poke" },
131 });
132 await supabaseServerClient.removeChannel(channel);
133}