a tool for shared writing and social publishing
at feature/hard_breaks 176 lines 5.8 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 | { type: "quote"; bsky_post_uri: string; document_uri: string }; 16 17export type HydratedNotification = 18 | HydratedCommentNotification 19 | HydratedSubscribeNotification 20 | HydratedQuoteNotification; 21export async function hydrateNotifications( 22 notifications: NotificationRow[], 23): Promise<Array<HydratedNotification>> { 24 // Call all hydrators in parallel 25 const [commentNotifications, subscribeNotifications, quoteNotifications] = await Promise.all([ 26 hydrateCommentNotifications(notifications), 27 hydrateSubscribeNotifications(notifications), 28 hydrateQuoteNotifications(notifications), 29 ]); 30 31 // Combine all hydrated notifications 32 const allHydrated = [...commentNotifications, ...subscribeNotifications, ...quoteNotifications]; 33 34 // Sort by created_at to maintain order 35 allHydrated.sort( 36 (a, b) => 37 new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), 38 ); 39 40 return allHydrated; 41} 42 43// Type guard to extract notification type 44type ExtractNotificationType<T extends NotificationData["type"]> = Extract< 45 NotificationData, 46 { type: T } 47>; 48 49export type HydratedCommentNotification = Awaited< 50 ReturnType<typeof hydrateCommentNotifications> 51>[0]; 52 53async function hydrateCommentNotifications(notifications: NotificationRow[]) { 54 const commentNotifications = notifications.filter( 55 (n): n is NotificationRow & { data: ExtractNotificationType<"comment"> } => 56 (n.data as NotificationData)?.type === "comment", 57 ); 58 59 if (commentNotifications.length === 0) { 60 return []; 61 } 62 63 // Fetch comment data from the database 64 const commentUris = commentNotifications.flatMap((n) => 65 n.data.parent_uri 66 ? [n.data.comment_uri, n.data.parent_uri] 67 : [n.data.comment_uri], 68 ); 69 const { data: comments } = await supabaseServerClient 70 .from("comments_on_documents") 71 .select( 72 "*,bsky_profiles(*), documents(*, documents_in_publications(publications(*)))", 73 ) 74 .in("uri", commentUris); 75 76 return commentNotifications.map((notification) => ({ 77 id: notification.id, 78 recipient: notification.recipient, 79 created_at: notification.created_at, 80 type: "comment" as const, 81 comment_uri: notification.data.comment_uri, 82 parentData: notification.data.parent_uri 83 ? comments?.find((c) => c.uri === notification.data.parent_uri)! 84 : undefined, 85 commentData: comments?.find( 86 (c) => c.uri === notification.data.comment_uri, 87 )!, 88 })); 89} 90 91export type HydratedSubscribeNotification = Awaited< 92 ReturnType<typeof hydrateSubscribeNotifications> 93>[0]; 94 95async function hydrateSubscribeNotifications(notifications: NotificationRow[]) { 96 const subscribeNotifications = notifications.filter( 97 ( 98 n, 99 ): n is NotificationRow & { data: ExtractNotificationType<"subscribe"> } => 100 (n.data as NotificationData)?.type === "subscribe", 101 ); 102 103 if (subscribeNotifications.length === 0) { 104 return []; 105 } 106 107 // Fetch subscription data from the database with related data 108 const subscriptionUris = subscribeNotifications.map( 109 (n) => n.data.subscription_uri, 110 ); 111 const { data: subscriptions } = await supabaseServerClient 112 .from("publication_subscriptions") 113 .select("*, identities(bsky_profiles(*)), publications(*)") 114 .in("uri", subscriptionUris); 115 116 return subscribeNotifications.map((notification) => ({ 117 id: notification.id, 118 recipient: notification.recipient, 119 created_at: notification.created_at, 120 type: "subscribe" as const, 121 subscription_uri: notification.data.subscription_uri, 122 subscriptionData: subscriptions?.find( 123 (s) => s.uri === notification.data.subscription_uri, 124 )!, 125 })); 126} 127 128export type HydratedQuoteNotification = Awaited< 129 ReturnType<typeof hydrateQuoteNotifications> 130>[0]; 131 132async function hydrateQuoteNotifications(notifications: NotificationRow[]) { 133 const quoteNotifications = notifications.filter( 134 (n): n is NotificationRow & { data: ExtractNotificationType<"quote"> } => 135 (n.data as NotificationData)?.type === "quote", 136 ); 137 138 if (quoteNotifications.length === 0) { 139 return []; 140 } 141 142 // Fetch bsky post data and document data 143 const bskyPostUris = quoteNotifications.map((n) => n.data.bsky_post_uri); 144 const documentUris = quoteNotifications.map((n) => n.data.document_uri); 145 146 const { data: bskyPosts } = await supabaseServerClient 147 .from("bsky_posts") 148 .select("*") 149 .in("uri", bskyPostUris); 150 151 const { data: documents } = await supabaseServerClient 152 .from("documents") 153 .select("*, documents_in_publications(publications(*))") 154 .in("uri", documentUris); 155 156 return quoteNotifications.map((notification) => ({ 157 id: notification.id, 158 recipient: notification.recipient, 159 created_at: notification.created_at, 160 type: "quote" as const, 161 bsky_post_uri: notification.data.bsky_post_uri, 162 document_uri: notification.data.document_uri, 163 bskyPost: bskyPosts?.find((p) => p.uri === notification.data.bsky_post_uri)!, 164 document: documents?.find((d) => d.uri === notification.data.document_uri)!, 165 })); 166} 167 168export async function pingIdentityToUpdateNotification(did: string) { 169 let channel = supabaseServerClient.channel(`identity.atp_did:${did}`); 170 await channel.send({ 171 type: "broadcast", 172 event: "notification", 173 payload: { message: "poke" }, 174 }); 175 await supabaseServerClient.removeChannel(channel); 176}