···903 const mentionedDids = new Set<string>();
904 const mentionedPublications = new Map<string, string>(); // Map of DID -> publication URI
905 const mentionedDocuments = new Map<string, string>(); // Map of DID -> document URI
0906907 // Extract pages from either format
908 let pages: PubLeafletContent.Main["pages"] | undefined;
···917918 if (!pages) return;
919920- // Extract mentions from all text blocks in all pages
921- for (const page of pages) {
922- if (page.$type === "pub.leaflet.pages.linearDocument") {
923- const linearPage = page as PubLeafletPagesLinearDocument.Main;
924- for (const blockWrapper of linearPage.blocks) {
925- const block = blockWrapper.block;
926- if (block.$type === "pub.leaflet.blocks.text") {
927- const textBlock = block as PubLeafletBlocksText.Main;
928- if (textBlock.facets) {
929- for (const facet of textBlock.facets) {
930- for (const feature of facet.features) {
931- // Check for DID mentions
932- if (PubLeafletRichtextFacet.isDidMention(feature)) {
933- if (feature.did !== authorDid) {
934- mentionedDids.add(feature.did);
935- }
936- }
937- // Check for AT URI mentions (publications and documents)
938- if (PubLeafletRichtextFacet.isAtMention(feature)) {
939- const uri = new AtUri(feature.atURI);
0940941- if (isPublicationCollection(uri.collection)) {
942- // Get the publication owner's DID
943- const { data: publication } = await supabaseServerClient
944- .from("publications")
945- .select("identity_did")
946- .eq("uri", feature.atURI)
947- .single();
948949- if (publication && publication.identity_did !== authorDid) {
950- mentionedPublications.set(
951- publication.identity_did,
952- feature.atURI,
953- );
954- }
955- } else if (isDocumentCollection(uri.collection)) {
956- // Get the document owner's DID
957- const { data: document } = await supabaseServerClient
958- .from("documents")
959- .select("uri, data")
960- .eq("uri", feature.atURI)
961- .single();
962963- if (document) {
964- const normalizedMentionedDoc = normalizeDocumentRecord(
965- document.data,
966- );
967- // Get the author from the document URI (the DID is the host part)
968- const mentionedUri = new AtUri(feature.atURI);
969- const docAuthor = mentionedUri.host;
970- if (normalizedMentionedDoc && docAuthor !== authorDid) {
971- mentionedDocuments.set(docAuthor, feature.atURI);
972- }
973- }
000000000000000000000000000000000000974 }
975 }
976 }
···1026 };
1027 await supabaseServerClient.from("notifications").insert(notification);
1028 await pingIdentityToUpdateNotification(recipientDid);
00000000000000000000000000001029 }
1030}
···903 const mentionedDids = new Set<string>();
904 const mentionedPublications = new Map<string, string>(); // Map of DID -> publication URI
905 const mentionedDocuments = new Map<string, string>(); // Map of DID -> document URI
906+ const embeddedBskyPosts = new Map<string, string>(); // Map of author DID -> post URI
907908 // Extract pages from either format
909 let pages: PubLeafletContent.Main["pages"] | undefined;
···918919 if (!pages) return;
920921+ // Helper to extract blocks from all pages (both linear and canvas)
922+ function getAllBlocks(pages: PubLeafletContent.Main["pages"]) {
923+ const blocks: (
924+ | PubLeafletPagesLinearDocument.Block["block"]
925+ | PubLeafletPagesCanvas.Block["block"]
926+ )[] = [];
927+ for (const page of pages) {
928+ if (page.$type === "pub.leaflet.pages.linearDocument") {
929+ const linearPage = page as PubLeafletPagesLinearDocument.Main;
930+ for (const blockWrapper of linearPage.blocks) {
931+ blocks.push(blockWrapper.block);
932+ }
933+ } else if (page.$type === "pub.leaflet.pages.canvas") {
934+ const canvasPage = page as PubLeafletPagesCanvas.Main;
935+ for (const blockWrapper of canvasPage.blocks) {
936+ blocks.push(blockWrapper.block);
937+ }
938+ }
939+ }
940+ return blocks;
941+ }
942943+ const allBlocks = getAllBlocks(pages);
000000944945+ // Extract mentions from all text blocks and embedded Bluesky posts
946+ for (const block of allBlocks) {
947+ // Check for embedded Bluesky posts
948+ if (PubLeafletBlocksBskyPost.isMain(block)) {
949+ const bskyPostUri = block.postRef.uri;
950+ // Extract the author DID from the post URI (at://did:xxx/app.bsky.feed.post/xxx)
951+ const postAuthorDid = new AtUri(bskyPostUri).host;
952+ if (postAuthorDid !== authorDid) {
953+ embeddedBskyPosts.set(postAuthorDid, bskyPostUri);
954+ }
955+ }
00956957+ // Check for text blocks with mentions
958+ if (block.$type === "pub.leaflet.blocks.text") {
959+ const textBlock = block as PubLeafletBlocksText.Main;
960+ if (textBlock.facets) {
961+ for (const facet of textBlock.facets) {
962+ for (const feature of facet.features) {
963+ // Check for DID mentions
964+ if (PubLeafletRichtextFacet.isDidMention(feature)) {
965+ if (feature.did !== authorDid) {
966+ mentionedDids.add(feature.did);
967+ }
968+ }
969+ // Check for AT URI mentions (publications and documents)
970+ if (PubLeafletRichtextFacet.isAtMention(feature)) {
971+ const uri = new AtUri(feature.atURI);
972+973+ if (isPublicationCollection(uri.collection)) {
974+ // Get the publication owner's DID
975+ const { data: publication } = await supabaseServerClient
976+ .from("publications")
977+ .select("identity_did")
978+ .eq("uri", feature.atURI)
979+ .single();
980+981+ if (publication && publication.identity_did !== authorDid) {
982+ mentionedPublications.set(
983+ publication.identity_did,
984+ feature.atURI,
985+ );
986+ }
987+ } else if (isDocumentCollection(uri.collection)) {
988+ // Get the document owner's DID
989+ const { data: document } = await supabaseServerClient
990+ .from("documents")
991+ .select("uri, data")
992+ .eq("uri", feature.atURI)
993+ .single();
994+995+ if (document) {
996+ const normalizedMentionedDoc = normalizeDocumentRecord(
997+ document.data,
998+ );
999+ // Get the author from the document URI (the DID is the host part)
1000+ const mentionedUri = new AtUri(feature.atURI);
1001+ const docAuthor = mentionedUri.host;
1002+ if (normalizedMentionedDoc && docAuthor !== authorDid) {
1003+ mentionedDocuments.set(docAuthor, feature.atURI);
1004 }
1005 }
1006 }
···1056 };
1057 await supabaseServerClient.from("notifications").insert(notification);
1058 await pingIdentityToUpdateNotification(recipientDid);
1059+ }
1060+1061+ // Create notifications for embedded Bluesky posts (only if the author has a Leaflet account)
1062+ if (embeddedBskyPosts.size > 0) {
1063+ // Check which of the Bluesky post authors have Leaflet accounts
1064+ const { data: identities } = await supabaseServerClient
1065+ .from("identities")
1066+ .select("atp_did")
1067+ .in("atp_did", Array.from(embeddedBskyPosts.keys()));
1068+1069+ const leafletUserDids = new Set(identities?.map((i) => i.atp_did) ?? []);
1070+1071+ for (const [postAuthorDid, bskyPostUri] of embeddedBskyPosts) {
1072+ // Only notify if the post author has a Leaflet account
1073+ if (leafletUserDids.has(postAuthorDid)) {
1074+ const notification: Notification = {
1075+ id: v7(),
1076+ recipient: postAuthorDid,
1077+ data: {
1078+ type: "bsky_post_embed",
1079+ document_uri: documentUri,
1080+ bsky_post_uri: bskyPostUri,
1081+ },
1082+ };
1083+ await supabaseServerClient.from("notifications").insert(notification);
1084+ await pingIdentityToUpdateNotification(postAuthorDid);
1085+ }
1086+ }
1087 }
1088}