Skip to content

Commit bb6f775

Browse files
committed
add bookmark and pin actions
1 parent 3780d5e commit bb6f775

File tree

12 files changed

+198
-3
lines changed

12 files changed

+198
-3
lines changed

.changeset/empty-clouds-watch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"applesauce-actions": minor
3+
---
4+
5+
Add NIP-51 pins actions

.changeset/twenty-rules-retire.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"applesauce-actions": minor
3+
---
4+
5+
Add NIP-51 bookmark actions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { beforeEach, describe, expect, it, vitest } from "vitest";
2+
import { EventStore } from "applesauce-core";
3+
import { EventFactory } from "applesauce-factory";
4+
import { kinds } from "nostr-tools";
5+
6+
import { FakeUser } from "../../__tests__/fake-user.js";
7+
import { ActionHub } from "../../action-hub.js";
8+
import { CreateBookmarkList } from "../bookmarks.js";
9+
10+
const user = new FakeUser();
11+
12+
let events: EventStore;
13+
let factory: EventFactory;
14+
let publish: () => Promise<void>;
15+
let hub: ActionHub;
16+
beforeEach(() => {
17+
events = new EventStore();
18+
factory = new EventFactory({ signer: user });
19+
publish = vitest.fn().mockResolvedValue(undefined);
20+
hub = new ActionHub(events, factory, publish);
21+
});
22+
23+
describe("CreateBookmarkList", () => {
24+
it("should publish a kind 10003 bookmark list", async () => {
25+
await hub.run(CreateBookmarkList);
26+
27+
expect(publish).toBeCalledWith(expect.any(String), expect.objectContaining({ kind: kinds.BookmarkList }));
28+
});
29+
});
+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { kinds, NostrEvent } from "nostr-tools";
2+
import { isReplaceable } from "applesauce-core/helpers";
3+
import { addEventBookmarkTag, removeEventBookmarkTag } from "applesauce-factory/operations/tag";
4+
import { type EventStore } from "applesauce-core";
5+
6+
import { Action } from "../action-hub.js";
7+
import {
8+
modifyHiddenTags,
9+
modifyPublicTags,
10+
setListDescription,
11+
setListImage,
12+
setListTitle,
13+
} from "applesauce-factory/operations/event";
14+
15+
function getBookmarkEvent(events: EventStore, self: string, identifier?: string) {
16+
return events.getReplaceable(identifier ? kinds.Bookmarksets : kinds.BookmarkList, self, identifier);
17+
}
18+
19+
/**
20+
* An action that adds a note or article to the bookmark list or a bookmark set
21+
* @param event the event to bookmark
22+
* @param identifier the "d" tag of the bookmark set
23+
* @param hidden set to true to add to hidden bookmarks
24+
*/
25+
export function BookmarkEvent(event: NostrEvent, identifier?: string, hidden = false): Action {
26+
return async ({ events, factory, self, publish }) => {
27+
const bookmarks = getBookmarkEvent(events, self, identifier);
28+
if (!bookmarks) throw new Error("Cant find bookmarks");
29+
30+
const operation = addEventBookmarkTag(event);
31+
32+
const draft = await factory.modifyTags(bookmarks, hidden ? { hidden: operation } : operation);
33+
await publish(`Bookmark ${isReplaceable(event.kind) ? "note" : "article"}`, await factory.sign(draft));
34+
};
35+
}
36+
37+
/**
38+
* An action that removes a note or article from the bookmark list or bookmark set
39+
* @param event the event to remove from bookmarks
40+
* @param identifier the "d" tag of the bookmark set
41+
* @param hidden set to true to remove from hidden bookmarks
42+
*/
43+
export function UnbookmarkEvent(event: NostrEvent, identifier: string, hidden = false): Action {
44+
return async ({ events, factory, self, publish }) => {
45+
const bookmarks = getBookmarkEvent(events, self, identifier);
46+
if (!bookmarks) throw new Error("Cant find bookmarks");
47+
48+
const operation = removeEventBookmarkTag(event);
49+
50+
const draft = await factory.modifyTags(bookmarks, hidden ? { hidden: operation } : operation);
51+
await publish("Remove bookmark", await factory.sign(draft));
52+
};
53+
}
54+
55+
/** An action that creates a new bookmark list for a user */
56+
export function CreateBookmarkList(bookmarks?: NostrEvent[]): Action {
57+
return async ({ events, factory, self, publish }) => {
58+
const existing = getBookmarkEvent(events, self);
59+
if (existing) throw new Error("Bookmark list already exists");
60+
61+
const draft = await factory.process(
62+
{ kind: kinds.BookmarkList },
63+
bookmarks ? modifyPublicTags(...bookmarks.map(addEventBookmarkTag)) : undefined,
64+
);
65+
await publish("Create bookmark list", await factory.sign(draft));
66+
};
67+
}
68+
69+
/** An action that creates a new bookmark set for a user */
70+
export function CreateBookmarkSet(
71+
title: string,
72+
description: string,
73+
additional: { image?: string; hidden?: NostrEvent[]; public?: NostrEvent[] },
74+
): Action {
75+
return async ({ events, factory, self, publish }) => {
76+
const existing = getBookmarkEvent(events, self);
77+
if (existing) throw new Error("Bookmark list already exists");
78+
79+
const draft = await factory.process(
80+
{ kind: kinds.BookmarkList },
81+
setListTitle(title),
82+
setListDescription(description),
83+
additional.image ? setListImage(additional.image) : undefined,
84+
additional.public ? modifyPublicTags(...additional.public.map(addEventBookmarkTag)) : undefined,
85+
additional.hidden ? modifyHiddenTags(...additional.hidden.map(addEventBookmarkTag)) : undefined,
86+
);
87+
await publish("Create bookmark set", await factory.sign(draft));
88+
};
89+
}

packages/actions/src/actions/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from "./contacts.js";
2+
export * from "./bookmarks.js";
3+
export * from "./pins.js";

packages/actions/src/actions/pins.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { kinds, NostrEvent } from "nostr-tools";
2+
import { addEventTag, removeEventTag } from "applesauce-factory/operations/tag";
3+
import { modifyPublicTags } from "applesauce-factory/operations/event";
4+
5+
import { Action } from "../action-hub.js";
6+
7+
/** An action that pins a note to the users pin list */
8+
export function PinNote(note: NostrEvent): Action {
9+
return async ({ events, factory, self, publish }) => {
10+
const pins = events.getReplaceable(kinds.Pinlist, self);
11+
if (!pins) throw new Error("Missing pin list");
12+
13+
const draft = await factory.modifyTags(pins, addEventTag(note.id));
14+
await publish("Pin note", await factory.sign(draft));
15+
};
16+
}
17+
18+
/** An action that removes an event from the users pin list */
19+
export function UnpinNote(note: NostrEvent): Action {
20+
return async ({ events, factory, self, publish }) => {
21+
const pins = events.getReplaceable(kinds.Pinlist, self);
22+
if (!pins) throw new Error("Missing pin list");
23+
24+
const draft = await factory.modifyTags(pins, removeEventTag(note.id));
25+
await publish("Pin note", await factory.sign(draft));
26+
};
27+
}
28+
29+
/** An action that creates a new pin list for a user */
30+
export function CreatePinList(pins: NostrEvent[] = []): Action {
31+
return async ({ events, factory, self, publish }) => {
32+
const existing = events.getReplaceable(kinds.Pinlist, self);
33+
if (existing) throw new Error("Pin list already exists");
34+
35+
const draft = await factory.process(
36+
{ kind: kinds.Pinlist },
37+
modifyPublicTags(...pins.map((event) => addEventTag(event.id))),
38+
);
39+
await publish("Create pin list", await factory.sign(draft));
40+
};
41+
}

packages/factory/src/operations/event/tags.ts

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function includeAltTag(description: string): EventOperation {
3030
/** Creates an operation that modifies the existing array of tags on an event */
3131
export function modifyPublicTags(...operations: TagOperation[]): EventOperation {
3232
return async (draft, ctx) => {
33+
if (operations.length === 0) return draft;
34+
3335
let tags = Array.from(draft.tags);
3436

3537
// modify the pubic tags
@@ -44,6 +46,8 @@ export function modifyPublicTags(...operations: TagOperation[]): EventOperation
4446
/** Creates an operation that modifies the existing array of tags on an event */
4547
export function modifyHiddenTags(...operations: TagOperation[]): EventOperation {
4648
return async (draft, ctx) => {
49+
if (operations.length === 0) return draft;
50+
4751
let tags = Array.from(draft.tags);
4852

4953
if (!ctx.signer) throw new Error("Missing signer for hidden tags");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { kinds, NostrEvent } from "nostr-tools";
2+
import { TagOperation } from "../../event-factory.js";
3+
import { getAddressPointerForEvent, isReplaceable } from "applesauce-core/helpers";
4+
import { addCoordinateTag, addEventTag, removeCoordinateTag, removeEventTag } from "./common.js";
5+
6+
/** Adds an "e" or "a" tag to a bookmark list or set */
7+
export function addEventBookmarkTag(event: NostrEvent): TagOperation {
8+
if (event.kind !== kinds.ShortTextNote && event.kind !== kinds.LongFormArticle)
9+
throw new Error(`Event kind (${event.kind}) cant not be added to bookmarks`);
10+
11+
return isReplaceable(event.kind) ? addCoordinateTag(getAddressPointerForEvent(event)) : addEventTag(event.id);
12+
}
13+
14+
/** Removes an "e" or "a" tag from a bookmark list or set */
15+
export function removeEventBookmarkTag(event: NostrEvent): TagOperation {
16+
if (event.kind !== kinds.ShortTextNote && event.kind !== kinds.LongFormArticle)
17+
throw new Error(`Event kind (${event.kind}) cant not be added to bookmarks`);
18+
19+
return isReplaceable(event.kind) ? removeCoordinateTag(getAddressPointerForEvent(event)) : removeEventTag(event.id);
20+
}

packages/factory/src/operations/tag/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./common.js";
33
export * from "./groups.js";
44
export * from "./mailboxes.js";
55
export * from "./relay.js";
6+
export * from "./bookmarks.js";

packages/wallet/src/actions/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./wallet.js";

packages/wallet/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * as Queries from "./queries/index.js";
22
export * as Helpers from "./helpers/index.js";
33
export * as Blueprints from "./blueprints/index.js";
4+
export * as Actions from "./actions/index.js";

pnpm-lock.yaml

-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)