view who was fronting when a record was made

feat: apply fronters to most notifications

ptr.pet ae26612b 8f4ed1bd

verified
Changed files
+183 -15
src
+125 -7
src/entrypoints/background.ts
···
deleteFronter,
getPkFronters,
FronterView,
+
docResolver,
} from "@/lib/utils";
-
import { AppBskyFeedPost } from "@atcute/bluesky";
+
import {
+
AppBskyFeedLike,
+
AppBskyFeedPost,
+
AppBskyFeedRepost,
+
AppBskyNotificationListNotifications,
+
} from "@atcute/bluesky";
import { feedViewPostSchema } from "@atcute/bluesky/types/app/feed/defs";
+
import { getAtprotoHandle } from "@atcute/identity";
import { is, parseResourceUri, ResourceUri } from "@atcute/lexicons";
-
import { AtprotoDid, parseCanonicalResourceUri } from "@atcute/lexicons/syntax";
+
import {
+
AtprotoDid,
+
Handle,
+
parseCanonicalResourceUri,
+
} from "@atcute/lexicons/syntax";
export default defineBackground({
persistent: true,
···
// hijack timeline fronter message because when a write is made it is either on the timeline
// or its a reply to a depth === 0 post on a threaded view, which is the same as a timeline post
browser.tabs.sendMessage(sender.tab?.id!, {
-
type: "TIMELINE_FRONTER",
+
type: "APPLY_FRONTERS",
results: Object.fromEntries(
results.flatMap((fronter) =>
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
···
),
});
};
+
const handleNotifications = async (
+
items: any,
+
sender: globalThis.Browser.runtime.MessageSender,
+
) => {
+
const fetchReply = async (
+
uri: ResourceUri,
+
): Promise<FronterView | undefined> => {
+
const cachedFronter = await frontersCache.get(uri);
+
const fronter =
+
(cachedFronter ?? null) ||
+
(await getFronter(uri).then((fronter) => {
+
if (!fronter.ok) {
+
frontersCache.set(uri, null);
+
return null;
+
}
+
return fronter.value;
+
}));
+
if (!fronter) return;
+
const parsedUri = await cacheFronter(uri, fronter);
+
return {
+
type: "post",
+
rkey: parsedUri.rkey!,
+
...fronter,
+
};
+
};
+
const handleNotif = async (
+
item: AppBskyNotificationListNotifications.Notification,
+
): Promise<FronterView | undefined> => {
+
let postUrl: ResourceUri | null = null;
+
const fronterUrl: ResourceUri = item.uri;
+
if (
+
item.reason === "subscribed-post" ||
+
item.reason === "quote" ||
+
item.reason === "reply"
+
)
+
postUrl = item.uri;
+
if (item.reason === "repost" || item.reason === "repost-via-repost")
+
postUrl = (item.record as AppBskyFeedRepost.Main).subject.uri;
+
if (item.reason === "like" || item.reason === "like-via-repost")
+
postUrl = (item.record as AppBskyFeedLike.Main).subject.uri;
+
if (!postUrl) return;
+
const cachedFronter = await frontersCache.get(fronterUrl);
+
let fronter =
+
(cachedFronter ?? null) ||
+
(await getFronter(fronterUrl).then((fronter) => {
+
if (!fronter.ok) {
+
frontersCache.set(fronterUrl, null);
+
return null;
+
}
+
return fronter.value;
+
}));
+
if (!fronter) return;
+
if (item.reason === "reply")
+
fronter.replyTo = (
+
item.record as AppBskyFeedPost.Main
+
).reply?.parent.uri;
+
const parsedUri = await cacheFronter(fronterUrl, fronter);
+
const postParsedUri = expect(parseCanonicalResourceUri(postUrl));
+
let handle: Handle | undefined = undefined;
+
try {
+
handle =
+
getAtprotoHandle(
+
await docResolver.resolve(postParsedUri.repo as AtprotoDid),
+
) ?? undefined;
+
} catch (err) {
+
console.error(`failed to get handle for ${postParsedUri.repo}:`, err);
+
}
+
return {
+
type: "notification",
+
reason: item.reason,
+
rkey: parsedUri.rkey!,
+
subject: {
+
did: postParsedUri.repo as AtprotoDid,
+
rkey: postParsedUri.rkey,
+
handle,
+
},
+
...fronter,
+
};
+
};
+
const allPromises = [];
+
for (const item of items.notifications ?? []) {
+
if (!is(AppBskyNotificationListNotifications.notificationSchema, item))
+
continue;
+
console.log("Handling notification:", item);
+
allPromises.push(handleNotif(item));
+
if (item.reason === "reply" && item.record) {
+
const parentUri = (item.record as AppBskyFeedPost.Main).reply?.parent
+
.uri;
+
if (parentUri) allPromises.push(fetchReply(parentUri));
+
}
+
}
+
const results = new Map(
+
(await Promise.allSettled(allPromises))
+
.filter((result) => result.status === "fulfilled")
+
.flatMap((result) => result.value ?? [])
+
.flatMap((fronter) =>
+
fronterGetSocialAppHrefs(fronter).map((href) => [href, fronter]),
+
),
+
);
+
if (results.size === 0) return;
+
browser.tabs.sendMessage(sender.tab?.id!, {
+
type: "APPLY_FRONTERS",
+
results: Object.fromEntries(results),
+
});
+
};
const handleTimeline = async (
feed: any[],
sender: globalThis.Browser.runtime.MessageSender,
···
);
if (results.size === 0) return;
browser.tabs.sendMessage(sender.tab?.id!, {
-
type: "TIMELINE_FRONTER",
+
type: "APPLY_FRONTERS",
results: Object.fromEntries(results),
});
// console.log("sent timeline fronters", results);
···
) => {
// check if this request was made for fetching replies
// if anchor is not the same as current document url, that is the case
-
// which means the depth of the returned posts are invalid to us, in the case of THREAD_FRONTER
-
// if so we will use TIMELINE_FRONTER to send it back to content script
+
// which means the depth of the returned posts are invalid to us
let isReplyThreadFetch = false;
const parsedDocumentUri = parseSocialAppPostUrl(documentUrl);
const anchorUri = new URL(requestUrl).searchParams.get("anchor");
···
);
if (results.size === 0) return;
browser.tabs.sendMessage(sender.tab?.id!, {
-
type: isReplyThreadFetch ? "TIMELINE_FRONTER" : "THREAD_FRONTER",
+
type: "APPLY_FRONTERS",
results: Object.fromEntries(results),
});
// console.log("sent thread fronters", results);
···
break;
case "thread":
await handleThread(message, sender);
+
break;
+
case "notifications":
+
await handleNotifications(JSON.parse(message.data.body), sender);
break;
}
});
+42 -4
src/entrypoints/content.ts
···
type: "posts",
body,
};
+
} else if (
+
response.url.includes("/xrpc/app.bsky.notification.listNotifications")
+
) {
+
detail = {
+
type: "notifications",
+
body,
+
};
}
if (detail) {
sendEvent(detail);
···
);
return;
}
-
} else {
+
} else if (
+
fronter.type === "post" ||
+
fronter.type === "thread_reply" ||
+
fronter.type === "thread_post" ||
+
(fronter.type === "notification" &&
+
(fronter.reason === "reply" || fronter.reason === "quote"))
+
) {
if (fronter.type === "thread_post" && fronter.depth === 0) {
if (match && match.rkey !== fronter.rkey) return;
if (el.ariaLabel !== fronter.displayName) return;
···
}
}
}
+
} else if (fronter.type === "notification") {
+
const multiOne =
+
el.firstElementChild?.nextElementSibling?.nextElementSibling
+
?.firstElementChild?.firstElementChild?.nextElementSibling
+
?.nextElementSibling?.firstElementChild?.firstElementChild
+
?.firstElementChild ?? null;
+
const singleOne =
+
el.firstElementChild?.nextElementSibling?.nextElementSibling
+
?.firstElementChild?.nextElementSibling?.nextElementSibling
+
?.firstElementChild?.firstElementChild?.firstElementChild ?? null;
+
displayNameElement = multiOne ?? singleOne ?? null;
+
if (displayNameElement?.tagName !== "A") {
+
console.log(
+
`invalid display element tag ${displayNameElement?.tagName}, expected a:`,
+
displayNameElement,
+
);
+
return;
+
}
+
const profileHref = displayNameElement?.getAttribute("href");
+
if (profileHref) {
+
const actorIdentifier = profileHref.split("/").slice(2)[0];
+
const isUser =
+
fronter.handle !== actorIdentifier &&
+
fronter.did !== actorIdentifier;
+
if (isUser) displayNameElement = null;
+
} else displayNameElement = null;
}
if (!displayNameElement) return;
return applyFronterName(displayNameElement, fronter.members);
···
applyFronters();
});
window.addEventListener("message", (event) => {
-
if (!["TIMELINE_FRONTER", "THREAD_FRONTER"].includes(event.data.type))
-
return;
-
console.log(`received ${event.data.type} fronters`, event.data.results);
+
if (event.data.type !== "APPLY_FRONTERS") return;
+
console.log(`received new fronters`, event.data.results);
applyFrontersToPage(new Map(Object.entries(event.data.results)), false);
});
},
+2 -2
src/entrypoints/isolated.content.ts
···
data,
});
});
-
const messageTypes = ["TAB_FRONTER", "THREAD_FRONTER", "TIMELINE_FRONTER"];
+
const bgMessageTypes = ["APPLY_FRONTERS"];
browser.runtime.onMessage.addListener((message) => {
-
if (!messageTypes.includes(message.type)) return;
+
if (!bgMessageTypes.includes(message.type)) return;
window.postMessage(message);
});
const updateOnUrlChange = async () => {
+14 -2
src/lib/utils.ts
···
} from "@atcute/identity-resolver";
import { getAtprotoHandle, getPdsEndpoint } from "@atcute/identity";
import { PersistentCache } from "./cache";
+
import { AppBskyNotificationListNotifications } from "@atcute/bluesky";
export type Subject = {
handle?: Handle;
···
}
| {
type: "repost";
+
}
+
| {
+
type: "notification";
+
reason: InferOutput<AppBskyNotificationListNotifications.notificationSchema>["reason"];
}
);
export type FronterType = FronterView["type"];
···
.flatMap((p) => p.value ?? []);
};
-
const handleResolver = new CompositeHandleResolver({
+
export const handleResolver = new CompositeHandleResolver({
strategy: "race",
methods: {
dns: new DohJsonHandleResolver({
···
http: new WellKnownHandleResolver(),
},
});
-
const docResolver = new CompositeDidDocumentResolver({
+
export const docResolver = new CompositeDidDocumentResolver({
methods: {
plc: new PlcDidDocumentResolver(),
web: new WebDidDocumentResolver(),
···
return [
handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}#repost`] : [],
`${fronterGetSocialAppHref(subject.did, subject.rkey)}#repost`,
+
].flat();
+
} else if (view.type === "notification" && view.subject) {
+
const subject = view.subject;
+
const handle = subject?.handle;
+
return [
+
handle ? [`${fronterGetSocialAppHref(handle, subject.rkey)}`] : [],
+
`${fronterGetSocialAppHref(subject.did, subject.rkey)}`,
].flat();
}
const depth = view.type === "thread_post" ? view.depth : undefined;