diff --git a/web/src/lib/api/comment.ts b/web/src/lib/api/comment.ts index 817bb2b00..750af8a7f 100644 --- a/web/src/lib/api/comment.ts +++ b/web/src/lib/api/comment.ts @@ -1,4 +1,4 @@ -import type { Nsid } from "@atcute/lexicons/syntax"; +import type { Nsid, ResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; import { jsonGet } from "$lib/api/_request"; @@ -19,8 +19,13 @@ export const putComment = async ( return { uri, cid, value: record }; }; -export const deleteComment = async (agent: OAuthUserAgent, rkey: string): Promise => { +export const deleteComment = async ( + agent: OAuthUserAgent, + rkey: string +): Promise<{ uri: ResourceUri }> => { + const uri = `at://${agent.sub}/${COMMENT_COLLECTION}/${rkey}` as ResourceUri; await deleteRecord(agent, COMMENT_COLLECTION, rkey); + return { uri }; }; export interface CommentListPage { diff --git a/web/src/lib/api/indexnow.ts b/web/src/lib/api/indexnow.ts index 317a4ee8d..4624cc19e 100644 --- a/web/src/lib/api/indexnow.ts +++ b/web/src/lib/api/indexnow.ts @@ -3,7 +3,6 @@ * Guarantees fire-and-forget: errors are caught silently so user flows are never blocked. */ export const pingIndexNow = (urls: string | string[]): void => { - // TODO: indexnow send needs to happen after awaitRecord const list = Array.isArray(urls) ? urls : [urls]; if (typeof window === "undefined" || list.length === 0) return; diff --git a/web/src/lib/api/issue.ts b/web/src/lib/api/issue.ts index 791150bb1..0625b1d96 100644 --- a/web/src/lib/api/issue.ts +++ b/web/src/lib/api/issue.ts @@ -1,4 +1,4 @@ -import type { Nsid } from "@atcute/lexicons/syntax"; +import type { Nsid, ResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { deleteRecord, putRecord } from "$lib/api/write"; import type { BobbinContext, XrpcRequestInit } from "$lib/api/client"; @@ -21,8 +21,13 @@ export const putIssue = async ( return { uri, cid, value: record }; }; -export const deleteIssue = async (agent: OAuthUserAgent, rkey: string): Promise => { +export const deleteIssue = async ( + agent: OAuthUserAgent, + rkey: string +): Promise<{ uri: ResourceUri }> => { + const uri = `at://${agent.sub}/${ISSUE_COLLECTION}/${rkey}` as ResourceUri; await deleteRecord(agent, ISSUE_COLLECTION, rkey); + return { uri }; }; export const getIssue = (ctx: BobbinContext, issue: string, init?: XrpcRequestInit) => diff --git a/web/src/lib/api/pull.ts b/web/src/lib/api/pull.ts index 071960df1..d1104af51 100644 --- a/web/src/lib/api/pull.ts +++ b/web/src/lib/api/pull.ts @@ -1,6 +1,6 @@ import { ok } from "@atcute/client"; import { mainSchema as getRecordSchema } from "@atcute/atproto/types/repo/getRecord"; -import type { Nsid, RecordKey } from "@atcute/lexicons/syntax"; +import type { Nsid, RecordKey, ResourceUri } from "@atcute/lexicons/syntax"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { now as tidNow } from "@atcute/tid"; import { createClient, mintServiceAuth, serviceDidForHost } from "$lib/auth/agent"; @@ -58,8 +58,13 @@ export const resubmitPull = ( versions: [...(record.versions ?? []), version] })); -export const deletePull = async (agent: OAuthUserAgent, rkey: string): Promise => { +export const deletePull = async ( + agent: OAuthUserAgent, + rkey: string +): Promise<{ uri: ResourceUri }> => { + const uri = `at://${agent.sub}/${PULL_COLLECTION}/${rkey}` as ResourceUri; await deleteRecord(agent, PULL_COLLECTION, rkey); + return { uri }; }; /** @@ -83,6 +88,7 @@ export const putPullStatus = async ( const { uri, cid } = await putRecord(agent, PULL_STATUS_COLLECTION, tidNow(), record); return { uri, cid, value: record }; }; + const KEEP_COMMIT_NSID = "sh.tangled.git.keepCommit"; export interface KeepCommitInput { diff --git a/web/src/lib/api/repoCreate.ts b/web/src/lib/api/repoCreate.ts index b205c102b..2d4d01e7d 100644 --- a/web/src/lib/api/repoCreate.ts +++ b/web/src/lib/api/repoCreate.ts @@ -201,7 +201,6 @@ export const createRepo = async ( const { repoDid } = await completeCreation(agent, bobbin, input, rkey, record, initial); - // TODO: indexnow send needs to happen after awaitRecord pingIndexNow(`https://tangled.org/${input.ownerHandle}/${name}`); return { diff --git a/web/src/lib/components/comment/CommentCard.svelte b/web/src/lib/components/comment/CommentCard.svelte index 78c836df0..0711b77ce 100644 --- a/web/src/lib/components/comment/CommentCard.svelte +++ b/web/src/lib/components/comment/CommentCard.svelte @@ -4,6 +4,8 @@ import Pencil from "$icon/pencil"; import Trash2 from "$icon/trash-2"; import { deleteComment } from "$lib/api/comment"; + import { awaitDeletedRecord } from "$lib/api/awaitRecord"; + import { createBobbinClient } from "$lib/api/client"; import { getAuth } from "$lib/auth.svelte"; import { type MarkupContext } from "$lib/markup"; import ErrorAlert from "$lib/components/ui/Error.svelte"; @@ -87,7 +89,8 @@ const deleteSelected = createAction(async (comment: CommentView) => { if (!auth) return; const agent = await auth.agentFor(comment.authorDid as Did); - await deleteComment(agent, comment.rkey); + const { uri } = await deleteComment(agent, comment.rkey); + await awaitDeletedRecord(createBobbinClient({ serviceUrl: auth.bobbinUrl }), uri); ondeleted?.(comment.uri); }); diff --git a/web/src/lib/components/comment/CommentEditor.svelte b/web/src/lib/components/comment/CommentEditor.svelte index 8fefdee0d..2f516eb7e 100644 --- a/web/src/lib/components/comment/CommentEditor.svelte +++ b/web/src/lib/components/comment/CommentEditor.svelte @@ -6,6 +6,7 @@ import X from "$icon/x"; import { page } from "$app/state"; import { putComment } from "$lib/api/comment"; + import { awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import { createBobbinClient } from "$lib/api/client"; import { ensureEntitySubscription } from "$lib/api/graph"; import { getAuth } from "$lib/auth.svelte"; @@ -99,6 +100,10 @@ record.replyTo = { uri: replyToUri, cid: replyToCid } as CommentRecord["replyTo"]; } const saved = await putComment(agent, targetRkey, record); + if (bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } // new comments subscribe the author to the entity; editing must not, the // commenter may have unsubscribed since if (rkey === undefined && bobbinUrl) { diff --git a/web/src/lib/components/repo/issues/IssueForm.svelte b/web/src/lib/components/repo/issues/IssueForm.svelte index 8f562377e..5205b62fa 100644 --- a/web/src/lib/components/repo/issues/IssueForm.svelte +++ b/web/src/lib/components/repo/issues/IssueForm.svelte @@ -5,7 +5,10 @@ import CirclePlus from "$icon/circle-plus"; import Pencil from "$icon/pencil"; import X from "$icon/x"; + import { page } from "$app/state"; import { putIssue } from "$lib/api/issue"; + import { awaitIndexedOrSaved } from "$lib/api/awaitRecord"; + import { createBobbinClient } from "$lib/api/client"; import { pingIndexNow } from "$lib/api/indexnow"; import { createEntitySubscription } from "$lib/api/graph"; import { getAuth } from "$lib/auth.svelte"; @@ -25,6 +28,7 @@ markup: MarkupContext; mode?: "create" | "edit"; authorDid?: string; + bobbinUrl?: string; rkey?: string; createdAt?: string; title?: string; @@ -41,6 +45,7 @@ markup, mode = "create", authorDid, + bobbinUrl: propBobbinUrl, rkey, createdAt, title: initialTitle = "", @@ -53,6 +58,9 @@ }: Props = $props(); const auth = getAuth(); + const bobbinUrl = $derived( + propBobbinUrl ?? (page.data.publicConfig?.bobbinUrl as string | undefined) + ); const accounts = $derived(auth?.accounts ?? []); let picked = $state(null); @@ -85,6 +93,10 @@ createdAt: createdAt ?? new Date().toISOString() }; const saved = await putIssue(agent, targetRkey, record); + if (bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } // auto-subscribe the creator to notifications on this issue (create only) if (mode === "create") { const entityUri = `at://${actingDid}/sh.tangled.repo.issue/${targetRkey}`; @@ -92,7 +104,6 @@ console.error("Failed to auto-subscribe to issue:", err); }); - // TODO: indexnow send needs to happen after awaitRecord if (typeof window !== "undefined") { const issuePath = window.location.pathname.replace(/\/new\/?$/, `/${targetRkey}`); pingIndexNow(`https://tangled.org${issuePath}`); diff --git a/web/src/lib/components/repo/issues/IssueThreadView.svelte b/web/src/lib/components/repo/issues/IssueThreadView.svelte index 983e6afd8..76edcd8dc 100644 --- a/web/src/lib/components/repo/issues/IssueThreadView.svelte +++ b/web/src/lib/components/repo/issues/IssueThreadView.svelte @@ -29,6 +29,7 @@ getSubscriptionRkey } from "$lib/api/graph"; import { deleteIssue, putIssue, type IssueRecord } from "$lib/api/issue"; + import { awaitDeletedRecord, awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import { getAuth } from "$lib/auth.svelte"; import { getNotifications } from "$lib/notifications.svelte"; import CommentSection from "$lib/components/comment/CommentSection.svelte"; @@ -140,14 +141,20 @@ body, createdAt: issue.createdAt }; - await putIssue(agent, issue.rkey, record); + const written = await putIssue(agent, issue.rkey, record); + if (props.bobbinUrl) { + await awaitIndexedOrSaved(createBobbinClient({ serviceUrl: props.bobbinUrl }), written); + } const bodyHtml = body ? await renderMarkup(body, props.markup).catch(() => null) : null; issue = { ...issue, title, body, bodyHtml }; }; const removeIssue = async () => { const agent = await auth.agentFor(issue.authorDid as Did); - await deleteIssue(agent, issue.rkey); + const { uri } = await deleteIssue(agent, issue.rkey); + if (props.bobbinUrl) { + await awaitDeletedRecord(createBobbinClient({ serviceUrl: props.bobbinUrl }), uri); + } await goto(resolve(issuesBase as "/")); }; diff --git a/web/src/lib/components/repo/pulls/PullCompose.svelte b/web/src/lib/components/repo/pulls/PullCompose.svelte index 8224a2973..e74aa7d77 100644 --- a/web/src/lib/components/repo/pulls/PullCompose.svelte +++ b/web/src/lib/components/repo/pulls/PullCompose.svelte @@ -24,6 +24,7 @@ import GitCommitVertical from "$icon/git-commit-vertical"; import GitPullRequestCreate from "$icon/git-pull-request-create"; import TriangleAlert from "$icon/triangle-alert"; + import { awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import { createBobbinClient } from "$lib/api/client"; import { getBranch, getMergeBase } from "$lib/api/knotmirror"; import { keepCommit, putPull } from "$lib/api/pull"; @@ -259,13 +260,16 @@ versions: [{ base: mergeBase.commit, head: head.hash, createdAt }], createdAt: createdAt as PullRecord["createdAt"] }; - await putPull(agent, rkey, record); + const saved = await putPull(agent, rkey, record); + if (bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } // auto-subscribe the creator to notifications on this PR createEntitySubscription(agent, uri).catch((err) => { console.error("Failed to auto-subscribe to pull:", err); }); - // TODO: indexnow send needs to happen after awaitRecord if (typeof window !== "undefined") { const pullPath = window.location.pathname.replace(/\/new\/?$/, `/${rkey}`); pingIndexNow(`https://tangled.org${pullPath}`); diff --git a/web/src/routes/[handle]/[repo]/issues/new/+page.svelte b/web/src/routes/[handle]/[repo]/issues/new/+page.svelte index bfa5f4e67..f7b961cd1 100644 --- a/web/src/routes/[handle]/[repo]/issues/new/+page.svelte +++ b/web/src/routes/[handle]/[repo]/issues/new/+page.svelte @@ -55,6 +55,7 @@ diff --git a/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte index 39a3f4ff5..74b387c9f 100644 --- a/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte +++ b/web/src/routes/[handle]/[repo]/pulls/[aturi]/[version]/[[range]]/PullViewPage.svelte @@ -13,6 +13,7 @@ import IterationCcw from "$icon/iteration-ccw"; import PanelRightClose from "$icon/panel-right-close"; import MessagesSquare from "$icon/messages-square"; + import { awaitDeletedRecord, awaitIndexedOrSaved } from "$lib/api/awaitRecord"; import { createBobbinClient } from "$lib/api/client"; import { deleteSubscription, @@ -350,6 +351,10 @@ embed, createdAt }); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } // ensure, not create: repeat commenting must not stack subscriptions const subscribeCtx = data.publicConfig?.bobbinUrl ? createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }) @@ -385,19 +390,27 @@ const agent = auth.agent; const user = auth.currentUser; if (!agent || !user) return; - await putComment(agent, edited.rkey, { + const saved = await putComment(agent, edited.rkey, { $type: "sh.tangled.feed.comment", subject: { uri: data.uri, cid: pull.cid } as CommentRecord["subject"], body: { $type: "sh.tangled.markup.markdown", text: edited.body }, embed: edited.embed, createdAt: edited.createdAt }); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } }; const updatePull = async ({ title, body }: { title: string; body: string }) => { const agent = auth.agent; if (!agent) return; - await editPull(agent, pull.rkey, { title, body }); + const saved = await editPull(agent, pull.rkey, { title, body }); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } pull = { ...pull, title, @@ -409,14 +422,22 @@ const removePull = async () => { const agent = auth.agent; if (!agent) return; - await deletePull(agent, pull.rkey); + const { uri } = await deletePull(agent, pull.rkey); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitDeletedRecord(ctx, uri); + } await goto(pullsBase); }; const setStatus = createAction(async (state: PullState) => { const agent = auth.agent; if (!agent) return; - await putPullStatus(agent, data.uri, state); + const saved = await putPullStatus(agent, data.uri, state); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } pull = { ...pull, state }; }); @@ -436,7 +457,11 @@ source: { repo: data.sourceRepoDid, commit: head }, style: "rebase" }); - await putPullStatus(agent, data.uri, "merged"); + const saved = await putPullStatus(agent, data.uri, "merged"); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } pull = { ...pull, state: "merged" }; }); @@ -525,11 +550,15 @@ oid, record: data.uri }); - await resubmitPull(agent, pull.rkey, { + const saved = await resubmitPull(agent, pull.rkey, { base: mergeBase.commit, head: oid, createdAt: new Date().toISOString() }); + if (data.publicConfig?.bobbinUrl) { + const ctx = createBobbinClient({ serviceUrl: data.publicConfig.bobbinUrl }); + await awaitIndexedOrSaved(ctx, { uri: saved.uri, cid: saved.cid }); + } await goto(href("latest"), { invalidateAll: true }); });