a tool for shared writing and social publishing

add follow notification

+56 -21
+23 -7
app/(home-pages)/notifications/FollowNotification.tsx
··· 1 1 import { Avatar } from "components/Avatar"; 2 2 import { Notification } from "./Notification"; 3 + import { HydratedSubscribeNotification } from "src/notifications"; 4 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 5 + import { AppBskyActorProfile, PubLeafletPublication } from "lexicons/api"; 3 6 4 - export const DummyFollowNotification = (props: {}) => { 5 - const identity = "celine"; 6 - const pubName = "Pub Name Here"; 7 + export const FollowNotification = (props: HydratedSubscribeNotification) => { 8 + const profileRecord = props.subscriptionData?.identities?.bsky_profiles 9 + ?.record as AppBskyActorProfile.Record; 10 + const displayName = 11 + profileRecord?.displayName || 12 + props.subscriptionData?.identities?.bsky_profiles?.handle || 13 + "Someone"; 14 + const pubRecord = props.subscriptionData?.publications 15 + ?.record as PubLeafletPublication.Record; 16 + const avatarSrc = 17 + profileRecord?.avatar?.ref && 18 + blobRefToSrc( 19 + profileRecord.avatar.ref, 20 + props.subscriptionData?.identity || "", 21 + ); 22 + 7 23 return ( 8 24 <Notification 9 - timestamp={""} 10 - href="/" 11 - icon={<Avatar src={undefined} displayName={identity} tiny />} 25 + timestamp={props.created_at} 26 + href={`https://${pubRecord?.base_path}`} 27 + icon={<Avatar src={avatarSrc} displayName={displayName} tiny />} 12 28 actionText={ 13 29 <> 14 - {identity} followed {pubName}! 30 + {displayName} subscribed to {pubRecord?.name}! 15 31 </> 16 32 } 17 33 />
+4
app/(home-pages)/notifications/NotificationList.tsx
··· 6 6 import { markAsRead } from "./getNotifications"; 7 7 import { ReplyNotification } from "./ReplyNotification"; 8 8 import { useIdentityData } from "components/IdentityProvider"; 9 + import { FollowNotification } from "./FollowNotification"; 9 10 10 11 export function NotificationList({ 11 12 notifications, ··· 35 36 if (n.type === "comment") { 36 37 if (n.parentData) return <ReplyNotification key={n.id} {...n} />; 37 38 return <CommentNotification key={n.id} {...n} />; 39 + } 40 + if (n.type === "subscribe") { 41 + return <FollowNotification key={n.id} {...n} />; 38 42 } 39 43 })} 40 44 </div>
+21
app/lish/subscribeToPublication.ts
··· 12 12 import { encodeActionToSearchParam } from "app/api/oauth/[route]/afterSignInActions"; 13 13 import { Json } from "supabase/database.types"; 14 14 import { IdResolver } from "@atproto/identity"; 15 + import { 16 + Notification, 17 + pingIdentityToUpdateNotification, 18 + } from "src/notifications"; 19 + import { v7 } from "uuid"; 15 20 16 21 let leafletFeedURI = 17 22 "at://did:plc:btxrwcaeyodrap5mnjw2fvmz/app.bsky.feed.generator/subscribedPublications"; ··· 46 51 publication, 47 52 identity: credentialSession.did!, 48 53 }); 54 + 55 + // Create notification for the publication owner 56 + let publicationOwner = new AtUri(publication).host; 57 + if (publicationOwner !== credentialSession.did) { 58 + let notification: Notification = { 59 + id: v7(), 60 + recipient: publicationOwner, 61 + data: { 62 + type: "subscribe", 63 + subscription_uri: record.uri, 64 + }, 65 + }; 66 + await supabaseServerClient.from("notifications").insert(notification); 67 + await pingIdentityToUpdateNotification(publicationOwner); 68 + } 69 + 49 70 let bsky = new BskyAgent(credentialSession); 50 71 let [prefs, profile, resolveDid] = await Promise.all([ 51 72 bsky.app.bsky.actor.getPreferences(),
+8 -14
src/notifications.ts
··· 85 85 })); 86 86 } 87 87 88 - export 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 - }; 96 - async function hydrateSubscribeNotifications( 97 - notifications: NotificationRow[], 98 - ): Promise<HydratedSubscribeNotification[]> { 88 + export type HydratedSubscribeNotification = Awaited< 89 + ReturnType<typeof hydrateSubscribeNotifications> 90 + >[0]; 91 + 92 + async function hydrateSubscribeNotifications(notifications: NotificationRow[]) { 99 93 const subscribeNotifications = notifications.filter( 100 94 ( 101 95 n, ··· 107 101 return []; 108 102 } 109 103 110 - // Fetch subscription data from the database 104 + // Fetch subscription data from the database with related data 111 105 const subscriptionUris = subscribeNotifications.map( 112 106 (n) => n.data.subscription_uri, 113 107 ); 114 108 const { data: subscriptions } = await supabaseServerClient 115 109 .from("publication_subscriptions") 116 - .select("*") 110 + .select("*, identities(bsky_profiles(*)), publications(*)") 117 111 .in("uri", subscriptionUris); 118 112 119 113 return subscribeNotifications.map((notification) => ({ ··· 124 118 subscription_uri: notification.data.subscription_uri, 125 119 subscriptionData: subscriptions?.find( 126 120 (s) => s.uri === notification.data.subscription_uri, 127 - ), 121 + )!, 128 122 })); 129 123 } 130 124