{#snippet template()}
diff --git a/web/src/lib/components/notifications/mock.ts b/web/src/lib/components/notifications/mock.ts
index 9cdb48483..859df3142 100644
--- a/web/src/lib/components/notifications/mock.ts
+++ b/web/src/lib/components/notifications/mock.ts
@@ -17,6 +17,7 @@ const dave = { did: "did:plc:kx4mzvhqrjpwqmvlbdxdlbnn", handle: "dave.tngl.sh" }
const core = { ownerHandle: "tangled.org", name: "core" };
const knot = { ownerHandle: "tangled.org", name: "knot" };
+const knotHost = { did: "did:web:knot.tangled.sh", domain: "knot.tangled.sh" };
const subjectUri = (kind: "issue" | "pull", number: string) =>
`at://did:plc:mock/sh.tangled.repo.${kind}/${number}`;
@@ -114,6 +115,24 @@ export const notifications: NotificationSummary[] = [
title: "appview: resolve handles inside the timeline feed"
}
},
+ {
+ uri: `at://mock/notif/${16}`,
+ id: 16,
+ type: "knot_invited",
+ read: false,
+ createdAt: ago(60 * 33),
+ actor: alice,
+ knot: knotHost
+ },
+ {
+ uri: `at://mock/notif/${17}`,
+ id: 17,
+ type: "collaborator_invited",
+ read: false,
+ createdAt: ago(60 * 36),
+ actor: bob,
+ repo: core
+ },
{
uri: `at://mock/notif/${8}`,
id: 8,
diff --git a/web/src/lib/components/notifications/notification.test.ts b/web/src/lib/components/notifications/notification.test.ts
index 0375c9848..f04422ac2 100644
--- a/web/src/lib/components/notifications/notification.test.ts
+++ b/web/src/lib/components/notifications/notification.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { notificationHref } from "./notification";
+import { notificationHref, notificationTarget } from "./notification";
import type { NotificationSummary } from "./types";
const summary = (overrides: Partial = {}): NotificationSummary => ({
@@ -18,6 +18,13 @@ const summary = (overrides: Partial = {}): NotificationSumm
...overrides
});
+const knotOffer = summary({
+ type: "knot_invited",
+ knot: { did: "did:web:knot.test", domain: "knot.test" },
+ repo: undefined,
+ subject: undefined
+});
+
describe("notificationHref", () => {
it("links a named repo issue to its owner/repo path", () => {
expect(notificationHref(summary())).toBe(
@@ -40,4 +47,20 @@ describe("notificationHref", () => {
it("falls back to the actor's page when there is no repo", () => {
expect(notificationHref(summary({ repo: undefined }))).toBe("/alice.test");
});
+
+ it("links knot offer at admin who made it, because knot pages don't exist yet", () => {
+ expect(notificationHref(knotOffer)).toBe("/alice.test");
+ });
+
+ it("links collaboration offer to its repository", () => {
+ expect(
+ notificationHref(summary({ type: "collaborator_invited", subject: undefined }))
+ ).toBe("/alice.test/core");
+ });
+});
+
+describe("notificationTarget", () => {
+ it("titles knot offer with knot's domain", () => {
+ expect(notificationTarget(knotOffer)).toBe("knot.test");
+ });
});
diff --git a/web/src/lib/components/notifications/notification.ts b/web/src/lib/components/notifications/notification.ts
index 95a4d6cc3..f32c14b75 100644
--- a/web/src/lib/components/notifications/notification.ts
+++ b/web/src/lib/components/notifications/notification.ts
@@ -7,9 +7,11 @@ import GitMerge from "$icon/git-merge";
import GitPullRequestClosed from "$icon/git-pull-request-closed";
import GitPullRequestCreate from "$icon/git-pull-request-create";
import MessageSquare from "$icon/message-square";
+import Server from "$icon/server";
import Star from "$icon/star";
import UserPlus from "$icon/user-plus";
import UserRoundMinus from "$icon/user-round-minus";
+import UsersRound from "$icon/users-round";
import UserRoundArrowForward from "$lib/components/icons/UserRoundArrowForward.svelte";
import type {
NotificationCategory,
@@ -38,7 +40,9 @@ const CATEGORIES: Record = {
pull_reopen: "work",
pull_assigned: "work",
pull_unassigned: "work",
- user_mentioned: "work"
+ user_mentioned: "work",
+ knot_invited: "work",
+ collaborator_invited: "work"
};
const ICONS: Record = {
@@ -57,7 +61,9 @@ const ICONS: Record = {
pull_reopen: GitPullRequestCreate,
pull_assigned: UserRoundArrowForward,
pull_unassigned: UserRoundMinus,
- user_mentioned: AtSign
+ user_mentioned: AtSign,
+ knot_invited: Server,
+ collaborator_invited: UsersRound
};
// the design system has no purple, and a merged pull already reads as info on
@@ -78,7 +84,9 @@ const TONES: Record = {
pull_reopen: "text-foreground-success",
pull_assigned: "text-foreground-info",
pull_unassigned: "text-foreground-muted",
- user_mentioned: "text-foreground-info"
+ user_mentioned: "text-foreground-info",
+ knot_invited: "text-foreground-info",
+ collaborator_invited: "text-foreground-info"
};
const PHRASES: Record = {
@@ -97,7 +105,9 @@ const PHRASES: Record = {
pull_reopen: "reopened a PR on",
pull_assigned: "assigned you to a PR on",
pull_unassigned: "unassigned you from a PR on",
- user_mentioned: "mentioned you in"
+ user_mentioned: "mentioned you in",
+ knot_invited: "invited you to",
+ collaborator_invited: "invited you to collaborate on"
};
export const notificationCategory = (type: NotificationType): NotificationCategory =>
@@ -118,7 +128,8 @@ export const notificationPhrase = (notification: NotificationSummary): string =>
// the subject of the phrase, which the appview prints as owner/name after it
export const notificationTarget = (notification: NotificationSummary): string =>
- notification.repo ? `${notification.repo.ownerHandle}/${notification.repo.name}` : "";
+ notification.knot?.domain ??
+ (notification.repo ? `${notification.repo.ownerHandle}/${notification.repo.name}` : "");
export const notificationHref = (notification: NotificationSummary): string => {
if (notification.type === "followed") return `/${notification.actor.handle}`;
diff --git a/web/src/lib/components/notifications/types.ts b/web/src/lib/components/notifications/types.ts
index 39ba6777a..555066dac 100644
--- a/web/src/lib/components/notifications/types.ts
+++ b/web/src/lib/components/notifications/types.ts
@@ -18,7 +18,9 @@ export type NotificationType =
| "pull_reopen"
| "pull_assigned"
| "pull_unassigned"
- | "user_mentioned";
+ | "user_mentioned"
+ | "knot_invited"
+ | "collaborator_invited";
export type NotificationCategory = "work" | "social";
@@ -36,6 +38,11 @@ export interface NotificationRepo {
name: string;
}
+export interface NotificationKnot {
+ did: string;
+ domain: string;
+}
+
export interface NotificationSubject {
kind: "issue" | "pull";
uri: string;
@@ -51,6 +58,7 @@ export interface NotificationSummary {
createdAt: string;
actor: NotificationActor;
repo?: NotificationRepo;
+ knot?: NotificationKnot;
subject?: NotificationSubject;
}
diff --git a/web/src/lib/components/repo/StarButton.svelte b/web/src/lib/components/repo/StarButton.svelte
index 143f17b00..68ce73c6b 100644
--- a/web/src/lib/components/repo/StarButton.svelte
+++ b/web/src/lib/components/repo/StarButton.svelte
@@ -111,6 +111,7 @@
const wasStarred = relation.active;
const existingRkey = relation.rkey;
const delta = wasStarred ? -1 : 1;
+ const undo = wasStarred ? 1 : -1;
relation.begin(!wasStarred);
starCount.resetFailure();
starCount.adjust(delta);
@@ -127,9 +128,9 @@
}
} catch {
relation.fail();
- starCount.adjust(-delta);
+ starCount.adjust(undo);
starCount.fail();
- profileCounts?.adjust(agent.sub, "stars", -delta);
+ profileCounts?.adjust(agent.sub, "stars", undo);
}
};