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}