Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225import { authedGet, authedPost, type DeliberiContext } from "$lib/api/deliberi";import type { BobbinContext, QueryValue, XrpcRequestInit } from "$lib/api/client";import { resolveMiniDoc } from "$lib/api/identity";import { getReposByRepoDids } from "$lib/api/records";import { getIssues } from "$lib/api/issue";import { getPulls } from "$lib/api/records";import { repoNameOf } from "$lib/api/repo";import { didFromUri, rkeyFromUri } from "$lib/api/uri";import { isDid, type Did } from "@atcute/lexicons/syntax";import { parseDidRkey, type NotificationOffer } from "$lib/components/notifications/types";import type { NotificationActor, NotificationRepo, NotificationSubject, NotificationSummary, NotificationType} from "$lib/components/notifications/types";
/** mirrors org.tangled.temp.notification.getPreferences#preferences */export interface NotificationPreferences { repoStarred: boolean; issueCreated: boolean; issueCommented: boolean; issueClosed: boolean; pullCreated: boolean; pullCommented: boolean; pullMerged: boolean; followed: boolean; userMentioned: boolean; emailNotifications: boolean;}
const GET_PREFERENCES = "org.tangled.temp.notification.getPreferences";const UPDATE_PREFERENCES = "org.tangled.temp.notification.updatePreferences";
export const getNotificationPreferences = ( ctx: DeliberiContext, init?: XrpcRequestInit): Promise<NotificationPreferences> => authedGet<NotificationPreferences>(ctx, GET_PREFERENCES, undefined, init);
/** only the provided fields are updated server-side. */export const updateNotificationPreferences = ( ctx: DeliberiContext, patch: Partial<NotificationPreferences>, init?: XrpcRequestInit): Promise<void> => authedPost(ctx, UPDATE_PREFERENCES, patch, init).then(() => undefined);
/** raw notification row returned by deliberi */export interface NotificationItem { uri: string; type: string; category: string; actorDid: string; read: boolean; createdAt: string; repoDid?: string; issueAt?: string; pullAt?: string;}
export interface NotificationListOutput { notifications: NotificationItem[]; socialUnreadCount: number; workUnreadCount: number;}
const LIST_NOTIFICATIONS = "org.tangled.temp.notification.listNotifications";
export const listNotifications = ( ctx: DeliberiContext, params?: { read?: string; category?: string; limit?: number }, init?: XrpcRequestInit): Promise<NotificationListOutput> => authedGet<NotificationListOutput>( ctx, LIST_NOTIFICATIONS, params as Record<string, QueryValue>, init );
const UPDATE_SEEN = "org.tangled.temp.notification.updateSeen";const MARK_ALL_READ = "org.tangled.temp.notification.markAllRead";const MARK_ENTITY_READ = "org.tangled.temp.notification.markEntityRead";const GET_UNREAD = "org.tangled.temp.notification.getUnreadCount";
export const updateNotificationSeen = ( ctx: DeliberiContext, input: { uri: string; read: boolean }): Promise<void> => authedPost(ctx, UPDATE_SEEN, input).then(() => undefined);
export const markAllNotificationsRead = (ctx: DeliberiContext): Promise<void> => authedPost(ctx, MARK_ALL_READ, {}).then(() => undefined);
export const markEntityNotificationsRead = ( ctx: DeliberiContext, entityAt: string): Promise<void> => authedPost(ctx, MARK_ENTITY_READ, { entityAt }).then(() => undefined);
export const getUnreadCount = (ctx: DeliberiContext): Promise<{ count: number }> => authedGet(ctx, GET_UNREAD);
const subjectFromUri = ( uri: string, kind: "issue" | "pull", title?: string): NotificationSubject => ({ kind, uri, number: rkeyFromUri(uri), title: title ?? rkeyFromUri(uri)});
const asKnot = (host: string | null | undefined): URL | null => host ? URL.parse(`https://${host}`) : null;
const OFFERS: Partial< Record< NotificationType, (item: NotificationItem, knotOfRepo: ReadonlyMap<Did, URL>) => NotificationOffer | undefined >> = { collaborator_invited: (item, knotOfRepo) => { const subject = parseDidRkey(item.repoDid); const knot = subject ? knotOfRepo.get(subject) : undefined; return subject && knot ? { kind: "collaboration", knot, subject } : undefined; }};
/** resolve rows best-effort: missing actor, repo, or entity falls back to the raw value */export const hydrateNotifications = async ( ctx: BobbinContext, items: NotificationItem[]): Promise<NotificationSummary[]> => { if (items.length === 0) return [];
const actorDids = [...new Set(items.map((n) => n.actorDid))];
const repoDids = [...new Set(items.map((n) => n.repoDid).filter((d): d is string => !!d))];
const issueUris = [...new Set(items.map((n) => n.issueAt).filter((u): u is string => !!u))]; const pullUris = [...new Set(items.map((n) => n.pullAt).filter((u): u is string => !!u))];
const [actorDocs, repoList, issueList, pullList] = await Promise.all([ Promise.all(actorDids.map((did) => resolveMiniDoc(ctx, did).catch(() => null))), repoDids.length > 0 ? getReposByRepoDids(ctx, repoDids).catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), issueUris.length > 0 ? getIssues(ctx, issueUris).catch(() => ({ items: [] })) : Promise.resolve({ items: [] }), pullUris.length > 0 ? getPulls(ctx, pullUris).catch(() => ({ items: [] })) : Promise.resolve({ items: [] }) ]); const ownerDids = [...new Set(repoList.items.map((r) => didFromUri(r.uri)))]; const ownerDocs = await Promise.all( ownerDids.map((did) => resolveMiniDoc(ctx, did).catch(() => null)) ); const ownerMap = new Map<string, string>(); for (const doc of ownerDocs) { if (doc) ownerMap.set(doc.did, doc.handle); }
const actorMap = new Map<string, NotificationActor>(); for (const doc of actorDocs) { if (doc) actorMap.set(doc.did, { did: doc.did, handle: doc.handle }); }
const repoMap = new Map<string, NotificationRepo>(); const knotOfRepo = new Map<Did, URL>(); for (const record of repoList.items) { const repoDid = record.value.repoDid; if (!repoDid) continue; const ownerDid = didFromUri(record.uri); const ownerHandle = ownerMap.get(ownerDid) ?? ownerDid; repoMap.set(repoDid, { ownerHandle, name: repoNameOf(record) }); if (isDid(repoDid)) { const knot = asKnot(record.value.knot); if (knot) knotOfRepo.set(repoDid, knot); } } for (const repoDid of repoDids) { if (!repoMap.has(repoDid)) { repoMap.set(repoDid, { ownerHandle: repoDid, name: repoDid }); } }
const titleMap = new Map<string, string>(); for (const record of issueList.items) { titleMap.set(record.uri, record.value.title); } for (const record of pullList.items) { titleMap.set(record.uri, record.value.title); }
return items.map((item): NotificationSummary => { const actor = actorMap.get(item.actorDid) ?? { did: item.actorDid, handle: item.actorDid }; const repo = item.repoDid ? repoMap.get(item.repoDid) : undefined; let subject: NotificationSubject | undefined; if (item.issueAt) { subject = subjectFromUri(item.issueAt, "issue", titleMap.get(item.issueAt)); } else if (item.pullAt) { subject = subjectFromUri(item.pullAt, "pull", titleMap.get(item.pullAt)); }
return { uri: item.uri, type: item.type as NotificationType, read: item.read, createdAt: item.createdAt, actor, repo, subject, offer: OFFERS[item.type as NotificationType]?.(item, knotOfRepo) }; });};