a tool for shared writing and social publishing
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix id col on notifications table

+153 -26
+3
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/commentAction.ts
··· 9 9 import { supabaseServerClient } from "supabase/serverClient"; 10 10 import { Json } from "supabase/database.types"; 11 11 import { Notification } from "src/notifications"; 12 + import { v7 } from "uuid"; 12 13 13 14 export async function publishComment(args: { 14 15 document: string; ··· 68 69 .select(); 69 70 let notifications: Notification[] = [ 70 71 { 72 + id: v7(), 71 73 recipient: new AtUri(args.document).host, 72 74 data: { type: "comment", comment_uri: uri.toString() }, 73 75 }, 74 76 ]; 75 77 if (args.comment.replyTo) 76 78 notifications.push({ 79 + id: v7(), 77 80 recipient: new AtUri(args.comment.replyTo).host, 78 81 data: { type: "comment", comment_uri: uri.toString() }, 79 82 });
+1 -9
drizzle/relations.ts
··· 1 1 import { relations } from "drizzle-orm/relations"; 2 - import { identities, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, notifications, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; 2 + import { identities, publications, documents, comments_on_documents, bsky_profiles, entity_sets, entities, facts, email_auth_tokens, poll_votes_on_entity, permission_tokens, phone_rsvps_to_entity, custom_domains, custom_domain_routes, email_subscriptions_to_entity, atp_poll_records, atp_poll_votes, bsky_follows, subscribers_to_publications, permission_token_on_homepage, documents_in_publications, document_mentions_in_bsky, bsky_posts, publication_domains, leaflets_in_publications, publication_subscriptions, permission_token_rights } from "./schema"; 3 3 4 4 export const publicationsRelations = relations(publications, ({one, many}) => ({ 5 5 identity: one(identities, { ··· 27 27 custom_domains_identity_id: many(custom_domains, { 28 28 relationName: "custom_domains_identity_id_identities_id" 29 29 }), 30 - notifications: many(notifications), 31 30 bsky_follows_follows: many(bsky_follows, { 32 31 relationName: "bsky_follows_follows_identities_atp_did" 33 32 }), ··· 192 191 193 192 export const atp_poll_recordsRelations = relations(atp_poll_records, ({many}) => ({ 194 193 atp_poll_votes: many(atp_poll_votes), 195 - })); 196 - 197 - export const notificationsRelations = relations(notifications, ({one}) => ({ 198 - identity: one(identities, { 199 - fields: [notifications.recipient], 200 - references: [identities.atp_did] 201 - }), 202 194 })); 203 195 204 196 export const bsky_followsRelations = relations(bsky_follows, ({one}) => ({
-14
drizzle/schema.ts
··· 165 165 } 166 166 }); 167 167 168 - export const notification = pgTable("notification", { 169 - recipient: text("recipient").primaryKey().notNull(), 170 - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 171 - read: boolean("read").default(false).notNull(), 172 - data: jsonb("data").notNull(), 173 - }); 174 - 175 168 export const custom_domain_routes = pgTable("custom_domain_routes", { 176 169 id: uuid("id").defaultRandom().primaryKey().notNull(), 177 170 domain: text("domain").notNull().references(() => custom_domains.domain), ··· 236 229 export const oauth_session_store = pgTable("oauth_session_store", { 237 230 key: text("key").primaryKey().notNull(), 238 231 session: jsonb("session").notNull(), 239 - }); 240 - 241 - export const notifications = pgTable("notifications", { 242 - recipient: text("recipient").primaryKey().notNull().references(() => identities.atp_did, { onDelete: "cascade", onUpdate: "cascade" } ), 243 - created_at: timestamp("created_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(), 244 - read: boolean("read").default(false).notNull(), 245 - data: jsonb("data").notNull(), 246 232 }); 247 233 248 234 export const bsky_follows = pgTable("bsky_follows", {
+142
src/notifications.ts
··· 1 + "use server"; 2 + 3 + import { supabaseServerClient } from "supabase/serverClient"; 4 + import { Tables, TablesInsert } from "supabase/database.types"; 5 + 6 + type NotificationRow = Tables<"notifications">; 7 + 8 + export type Notification = Omit<TablesInsert<"notifications">, "data"> & { 9 + data: NotificationData; 10 + }; 11 + // Notification data types (for writing to the notifications table) 12 + export type NotificationData = 13 + | { type: "comment"; comment_uri: string } 14 + | { type: "subscribe"; subscription_uri: string }; 15 + 16 + // Hydrated notification types 17 + export type HydratedCommentNotification = { 18 + id: string; 19 + recipient: string; 20 + created_at: string; 21 + type: "comment"; 22 + comment_uri: string; 23 + commentData?: Tables<"comments_on_documents">; 24 + }; 25 + 26 + export type HydratedSubscribeNotification = { 27 + id: string; 28 + recipient: string; 29 + created_at: string; 30 + type: "subscribe"; 31 + subscription_uri: string; 32 + subscriptionData?: Tables<"publication_subscriptions">; 33 + }; 34 + 35 + export type HydratedNotification = 36 + | HydratedCommentNotification 37 + | HydratedSubscribeNotification; 38 + 39 + // Type guard to extract notification type 40 + type ExtractNotificationType<T extends NotificationData["type"]> = Extract< 41 + NotificationData, 42 + { type: T } 43 + >; 44 + 45 + // Hydrator function type 46 + type NotificationHydrator<T extends NotificationData["type"]> = ( 47 + notifications: NotificationRow[], 48 + ) => Promise<Array<HydratedNotification & { type: T }>>; 49 + 50 + /** 51 + * Hydrates comment notifications 52 + */ 53 + async function hydrateCommentNotifications( 54 + notifications: NotificationRow[], 55 + ): Promise<HydratedCommentNotification[]> { 56 + const commentNotifications = notifications.filter( 57 + (n): n is NotificationRow & { data: ExtractNotificationType<"comment"> } => 58 + (n.data as NotificationData)?.type === "comment", 59 + ); 60 + 61 + if (commentNotifications.length === 0) { 62 + return []; 63 + } 64 + 65 + // Fetch comment data from the database 66 + const commentUris = commentNotifications.map((n) => n.data.comment_uri); 67 + const { data: comments } = await supabaseServerClient 68 + .from("comments_on_documents") 69 + .select("*") 70 + .in("uri", commentUris); 71 + 72 + return commentNotifications.map((notification) => ({ 73 + id: notification.id, 74 + recipient: notification.recipient, 75 + created_at: notification.created_at, 76 + type: "comment" as const, 77 + comment_uri: notification.data.comment_uri, 78 + commentData: comments?.find((c) => c.uri === notification.data.comment_uri), 79 + })); 80 + } 81 + 82 + /** 83 + * Hydrates subscribe notifications 84 + */ 85 + async function hydrateSubscribeNotifications( 86 + notifications: NotificationRow[], 87 + ): Promise<HydratedSubscribeNotification[]> { 88 + const subscribeNotifications = notifications.filter( 89 + ( 90 + n, 91 + ): n is NotificationRow & { data: ExtractNotificationType<"subscribe"> } => 92 + (n.data as NotificationData)?.type === "subscribe", 93 + ); 94 + 95 + if (subscribeNotifications.length === 0) { 96 + return []; 97 + } 98 + 99 + // Fetch subscription data from the database 100 + const subscriptionUris = subscribeNotifications.map( 101 + (n) => n.data.subscription_uri, 102 + ); 103 + const { data: subscriptions } = await supabaseServerClient 104 + .from("publication_subscriptions") 105 + .select("*") 106 + .in("uri", subscriptionUris); 107 + 108 + return subscribeNotifications.map((notification) => ({ 109 + id: notification.id, 110 + recipient: notification.recipient, 111 + created_at: notification.created_at, 112 + type: "subscribe" as const, 113 + subscription_uri: notification.data.subscription_uri, 114 + subscriptionData: subscriptions?.find( 115 + (s) => s.uri === notification.data.subscription_uri, 116 + ), 117 + })); 118 + } 119 + 120 + /** 121 + * Main hydration function that processes all notifications 122 + */ 123 + export async function hydrateNotifications( 124 + notifications: NotificationRow[], 125 + ): Promise<HydratedNotification[]> { 126 + // Call all hydrators in parallel 127 + const [commentNotifications, subscribeNotifications] = await Promise.all([ 128 + hydrateCommentNotifications(notifications), 129 + hydrateSubscribeNotifications(notifications), 130 + ]); 131 + 132 + // Combine all hydrated notifications 133 + const allHydrated = [...commentNotifications, ...subscribeNotifications]; 134 + 135 + // Sort by created_at to maintain order 136 + allHydrated.sort( 137 + (a, b) => 138 + new Date(b.created_at).getTime() - new Date(a.created_at).getTime(), 139 + ); 140 + 141 + return allHydrated; 142 + }
+4 -1
supabase/database.types.ts
··· 628 628 Row: { 629 629 created_at: string 630 630 data: Json 631 + id: string 631 632 read: boolean 632 633 recipient: string 633 634 } 634 635 Insert: { 635 636 created_at?: string 636 637 data: Json 638 + id: string 637 639 read?: boolean 638 640 recipient: string 639 641 } 640 642 Update: { 641 643 created_at?: string 642 644 data?: Json 645 + id?: string 643 646 read?: boolean 644 647 recipient?: string 645 648 } ··· 647 650 { 648 651 foreignKeyName: "notifications_recipient_fkey" 649 652 columns: ["recipient"] 650 - isOneToOne: true 653 + isOneToOne: false 651 654 referencedRelation: "identities" 652 655 referencedColumns: ["atp_did"] 653 656 },
+3 -2
supabase/migrations/20251030215033_add_notifications_table.sql
··· 2 2 "recipient" text not null, 3 3 "created_at" timestamp with time zone not null default now(), 4 4 "read" boolean not null default false, 5 - "data" jsonb not null 5 + "data" jsonb not null, 6 + "id" uuid not null 6 7 ); 7 8 8 9 9 10 alter table "public"."notifications" enable row level security; 10 11 11 - CREATE UNIQUE INDEX notifications_pkey ON public.notifications USING btree (recipient); 12 + CREATE UNIQUE INDEX notifications_pkey ON public.notifications USING btree (id); 12 13 13 14 alter table "public"."notifications" add constraint "notifications_pkey" PRIMARY KEY using index "notifications_pkey"; 14 15