a tool for shared writing and social publishing
at feature/notifications 139 lines 4.3 kB view raw
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}