@threenine/nuxstr-comments 1.5.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@threenine/nuxstr-comments",
3
3
  "configKey": "nuxstrComments",
4
- "version": "1.5.4",
4
+ "version": "1.6.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -8,7 +8,7 @@ const module$1 = defineNuxtModule({
8
8
  },
9
9
  // Default configuration options of the Nuxt module
10
10
  defaults: {
11
- relays: ["wss://relay.damus.io", "wss://purplepag.es/"],
11
+ relays: ["wss://relay.threenine.services"],
12
12
  tagStrategy: "path",
13
13
  tagPrefix: "comment:"
14
14
  },
@@ -17,9 +17,9 @@ const module$1 = defineNuxtModule({
17
17
  nuxt.hook("vite:extendConfig", (config) => {
18
18
  config.optimizeDeps = config.optimizeDeps || {};
19
19
  config.optimizeDeps.include = config.optimizeDeps.include || [];
20
- config.optimizeDeps.include.push("tseep", "@nostr-dev-kit/ndk", "nostr-tools", "defu");
20
+ config.optimizeDeps.include.push("tseep", "nostr-tools", "defu");
21
21
  config.ssr = config.ssr || {};
22
- const packagesToInclude = ["tseep", "@nostr-dev-kit/ndk", "nostr-tools", "defu"];
22
+ const packagesToInclude = ["tseep", "nostr-tools", "defu"];
23
23
  if (!config.ssr.noExternal) {
24
24
  config.ssr.noExternal = packagesToInclude;
25
25
  } else if (Array.isArray(config.ssr.noExternal)) {
@@ -31,10 +31,10 @@ const module$1 = defineNuxtModule({
31
31
  nuxt.hook("nitro:config", (nitroConfig) => {
32
32
  nitroConfig.externals = nitroConfig.externals || {};
33
33
  nitroConfig.externals.inline = nitroConfig.externals.inline || [];
34
- nitroConfig.externals.inline.push("tseep", "@nostr-dev-kit/ndk", "nostr-tools", "defu");
34
+ nitroConfig.externals.inline.push("tseep", "nostr-tools", "defu");
35
35
  });
36
36
  nuxt.options.build.transpile = nuxt.options.build.transpile || [];
37
- nuxt.options.build.transpile.push("tseep", "@nostr-dev-kit/ndk", "nostr-tools", "defu");
37
+ nuxt.options.build.transpile.push("tseep", "nostr-tools", "defu");
38
38
  nuxt.options.runtimeConfig.public.nuxstrComments = defu(nuxt.options.runtimeConfig.public.nuxstrComments || {}, options);
39
39
  addPlugin(resolver.resolve("./runtime/plugin"));
40
40
  addImports([
@@ -0,0 +1,12 @@
1
+ import type { Event as NToolEvent, Filter } from 'nostr-tools';
2
+ export declare class NostrManager {
3
+ private static instance;
4
+ private pool;
5
+ private relays;
6
+ private constructor();
7
+ static getInstance(relays: string[]): NostrManager;
8
+ subscribe(filter: Filter, onEvent: (event: NToolEvent) => void): import("nostr-tools/abstract-pool").SubCloser;
9
+ publish(event: NToolEvent): Promise<void>;
10
+ getEvent(filter: Filter): Promise<NToolEvent | null>;
11
+ close(): Promise<void>;
12
+ }
@@ -0,0 +1,40 @@
1
+ import { SimplePool, verifyEvent } from "nostr-tools";
2
+ export class NostrManager {
3
+ static instance;
4
+ pool;
5
+ relays;
6
+ constructor(relays) {
7
+ this.pool = new SimplePool();
8
+ this.relays = relays;
9
+ }
10
+ static getInstance(relays) {
11
+ if (!NostrManager.instance) {
12
+ NostrManager.instance = new NostrManager(relays);
13
+ } else {
14
+ relays.forEach((relay) => {
15
+ if (!NostrManager.instance.relays.includes(relay)) {
16
+ NostrManager.instance.relays.push(relay);
17
+ }
18
+ });
19
+ }
20
+ return NostrManager.instance;
21
+ }
22
+ subscribe(filter, onEvent) {
23
+ return this.pool.subscribeMany(this.relays, filter, {
24
+ onevent(event) {
25
+ if (verifyEvent(event)) {
26
+ onEvent(event);
27
+ }
28
+ }
29
+ });
30
+ }
31
+ async publish(event) {
32
+ await this.pool.publish(this.relays, event);
33
+ }
34
+ async getEvent(filter) {
35
+ return await this.pool.get(this.relays, filter);
36
+ }
37
+ async close() {
38
+ this.pool.close(this.relays);
39
+ }
40
+ }
@@ -1,5 +1,6 @@
1
+ import type { Profile } from '../types/index.js';
1
2
  type __VLS_Props = {
2
- profile: Profile;
3
+ profile?: Profile;
3
4
  createdAt: number;
4
5
  };
5
6
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,25 +1,28 @@
1
1
  <script setup>
2
2
  const props = defineProps({
3
- profile: { type: null, required: true },
3
+ profile: { type: Object, required: false },
4
4
  createdAt: { type: Number, required: true }
5
5
  });
6
6
  </script>
7
7
 
8
8
  <template>
9
- <div class="flex items-center gap-3">
9
+ <div
10
+ v-if="props.profile"
11
+ class="flex items-center gap-3"
12
+ >
10
13
  <div
11
14
  v-if="props.profile.image"
12
15
  class="flex-shrink-0"
13
16
  >
14
17
  <img
15
18
  :src="props.profile.image"
16
- :alt="props.profile.name || props.profile.display_name || 'User avatar'"
19
+ :alt="props.profile.display_name || 'User avatar'"
17
20
  class="w-6 h-6 rounded-full object-cover"
18
21
  >
19
22
  </div>
20
23
  <div class="flex-1 min-w-0 mb-3 ">
21
24
  <div class="flex gap-6 items-center">
22
- <span class="text-sm mr-6">{{ props.profile.display_name || props.profile.name || `${props.profile.pubkey.slice(0, 8)}\u2026` }}</span>
25
+ <span class="text-sm mr-6">{{ props.profile.display_name || `${props.profile.pubkey.slice(0, 8)}\u2026` }}</span>
23
26
  <span class="text-xs text-muted-foreground ml-3">&nbsp;&nbsp;</span>
24
27
  <span class="text-xs text-primary ml-3">{{ new Date(props.createdAt * 1e3).toLocaleString() }}</span>
25
28
  </div>
@@ -1,5 +1,6 @@
1
+ import type { Profile } from '../types/index.js';
1
2
  type __VLS_Props = {
2
- profile: Profile;
3
+ profile?: Profile;
3
4
  createdAt: number;
4
5
  };
5
6
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,3 +1,4 @@
1
+ import type { Comment } from '../types/index.js';
1
2
  type __VLS_Props = {
2
3
  replies: Comment[];
3
4
  };
@@ -1,3 +1,4 @@
1
+ import type { Comment } from '../types/index.js';
1
2
  type __VLS_Props = {
2
3
  replies: Comment[];
3
4
  };
@@ -1,9 +1,10 @@
1
1
  import { computed, ref } from "vue";
2
2
  import { useRequestURL, useRoute, useRuntimeConfig } from "#imports";
3
3
  import useNuxstr from "./useNuxstr.js";
4
- import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
4
+ import { useNostr } from "./useNostr.js";
5
5
  function useComments(customContentId) {
6
- const { ndk, connect, isLoggedIn, mapComment, pubkey, fetchProfile } = useNuxstr();
6
+ const { isLoggedIn, pubkey, fetchProfile } = useNuxstr();
7
+ const { subscribe } = useNostr();
7
8
  const route = useRoute();
8
9
  const config = useRuntimeConfig();
9
10
  const opts = config.public?.nuxstrComments || {};
@@ -22,54 +23,60 @@ function useComments(customContentId) {
22
23
  return `${prefix}${contentId.value}`;
23
24
  }
24
25
  function siteUrl() {
25
- const url = useRequestURL();
26
- return `${url.protocol}//${url.host}`;
26
+ if (import.meta.server) {
27
+ const url = useRequestURL();
28
+ return `${url.protocol}//${url.host}`;
29
+ }
30
+ return window.location.origin;
27
31
  }
28
32
  function fullUrl(path) {
29
33
  return `${siteUrl()}${path}`;
30
34
  }
31
35
  async function subscribeComments() {
32
- await connect();
33
36
  const filter = {
34
- kinds: [NDKKind.GenericReply],
37
+ kinds: [1111],
38
+ // NDKKind.GenericReply is 22
35
39
  ["#t"]: [tagValue()],
36
- limit: 100,
37
- ["#k"]: ["web"],
38
- ["#A"]: [fullUrl(contentId.value)]
40
+ limit: 100
39
41
  };
40
- const sub = ndk.subscribe(filter);
41
- sub.on("event", async (event) => {
42
- const comment = mapComment(event);
42
+ subscribe(filter, async (event) => {
43
+ if (commentsData.value.some((c) => c.id === event.id)) return;
44
+ const comment = {
45
+ id: event.id,
46
+ pubkey: event.pubkey,
47
+ created_at: event.created_at,
48
+ content: event.content,
49
+ profile: void 0
50
+ };
43
51
  comment.profile = await fetchProfile(event.pubkey);
44
52
  commentsData.value.push(comment);
45
53
  });
46
54
  }
47
55
  async function postComment(comment) {
48
- await connect();
49
- const ndkEvent = await createCommentEvent(comment);
50
- return await ndkEvent.publish().then(() => true).catch((err) => {
56
+ const { publish } = useNostr();
57
+ try {
58
+ const event = await createCommentEvent(comment);
59
+ const signedEvent = await window.nostr.signEvent(event);
60
+ await publish(signedEvent);
61
+ return true;
62
+ } catch (err) {
51
63
  error.value = err?.message || String(err);
52
64
  return false;
53
- });
65
+ }
54
66
  }
55
67
  async function createCommentEvent(comment) {
56
- const event = new NDKEvent(ndk);
57
- event.kind = NDKKind.GenericReply;
58
- event.content = comment;
59
- event.tags = [
60
- ["A", fullUrl(contentId.value)],
61
- ["a", fullUrl(contentId.value)],
62
- ["I", fullUrl(contentId.value)],
63
- //
64
- ["i", fullUrl(contentId.value)],
65
- ["t", tagValue()],
66
- ["k", "web"],
67
- // Defined NIP 73
68
- ["K", "web"],
69
- // Defined NIP 73,
70
- ["p", pubkey ?? ""]
71
- ];
72
- return event;
68
+ return {
69
+ kind: 1111,
70
+ // GenericReply
71
+ created_at: Math.floor(Date.now() / 1e3),
72
+ content: comment,
73
+ tags: [
74
+ ["A", fullUrl(contentId.value)],
75
+ ["t", tagValue()],
76
+ ["k", "web"],
77
+ ["p", pubkey ?? ""]
78
+ ]
79
+ };
73
80
  }
74
81
  return { loading, error, comments, isLoggedIn, subscribeComments, postComment };
75
82
  }
@@ -0,0 +1,8 @@
1
+ import type { Filter, Event } from 'nostr-tools';
2
+ import { NostrManager } from '../classes/NostrManager.js';
3
+ export declare const useNostr: (relays?: string[]) => {
4
+ nostrManager: NostrManager;
5
+ subscribe: (filter: Filter, onEvent: (event: Event) => void) => import("nostr-tools/abstract-pool").SubCloser;
6
+ publish: (event: Event) => Promise<void>;
7
+ getEvent: (filter: Filter) => Promise<import("nostr-tools").NostrEvent | null>;
8
+ };
@@ -0,0 +1,24 @@
1
+ import { useRuntimeConfig } from "#imports";
2
+ import { NostrManager } from "../classes/NostrManager.js";
3
+ export const useNostr = (relays) => {
4
+ const config = useRuntimeConfig();
5
+ const opts = config.public?.nuxstrComments || {};
6
+ const effectiveRelays = relays || opts.relays || [];
7
+ const nostrManager = NostrManager.getInstance(effectiveRelays);
8
+ const subscribe = (filter, onEvent) => {
9
+ return nostrManager.subscribe(filter, onEvent);
10
+ };
11
+ const publish = (event) => {
12
+ console.log("publishing Comment", event);
13
+ return nostrManager.publish(event);
14
+ };
15
+ const getEvent = (filter) => {
16
+ return nostrManager.getEvent(filter);
17
+ };
18
+ return {
19
+ nostrManager,
20
+ subscribe,
21
+ publish,
22
+ getEvent
23
+ };
24
+ };
@@ -1,14 +1,10 @@
1
- import NDK, { type NDKEvent, type NDKUserProfile } from '@nostr-dev-kit/ndk';
2
- import type { Comment, Profile } from '../types/index.js';
1
+ import type { Profile } from '../types/index.js';
3
2
  declare function useNuxstr(): {
4
- readonly ndk: NDK;
5
- connect: () => Promise<NDK>;
6
3
  login: () => Promise<void>;
7
4
  logout: () => void;
8
5
  isLoggedIn: import("vue").ComputedRef<boolean>;
9
6
  pubkey: string | undefined;
10
- mapProfile: (profile: NDKUserProfile | null) => Profile;
11
- mapComment: (event: NDKEvent) => Comment;
7
+ userProfile: import("vue").ComputedRef<Profile | null | undefined>;
12
8
  fetchProfile: (pubkey: string) => Promise<Profile | undefined>;
13
9
  };
14
10
  export default useNuxstr;
@@ -1,48 +1,23 @@
1
1
  import { computed, ref } from "vue";
2
- import { useRuntimeConfig } from "#imports";
3
- import NDK, { NDKNip07Signer } from "@nostr-dev-kit/ndk";
4
2
  import { useToast } from "#ui/composables/useToast";
3
+ import { useNostr } from "./useNostr.js";
5
4
  function useNuxstr() {
6
5
  const DEFAULT_PUBKEY = "";
7
6
  const w = globalThis;
8
7
  if (!w.__nuxstr) {
9
8
  w.__nuxstr = {
10
- ndk: null,
11
- signer: null,
12
9
  pubkey: ref(DEFAULT_PUBKEY),
13
10
  isConnecting: ref(false),
14
11
  isConnected: ref(false),
15
- userProfile: ref(void 0)
12
+ userProfile: ref(null)
16
13
  };
17
14
  }
18
15
  const state = w.__nuxstr;
19
- const DEFAULT_TIMESTAMP = 0;
20
- const config = useRuntimeConfig();
21
- const opts = config.public?.nuxstrComments || {};
22
- function initializeNDK() {
23
- if (!state.ndk) {
24
- state.ndk = new NDK({
25
- explicitRelayUrls: opts.relays || []
26
- });
27
- }
28
- return state.ndk;
29
- }
16
+ const { getEvent } = useNostr();
30
17
  const isLoggedIn = computed(() => !!state.pubkey.value);
31
- async function connect() {
32
- const ndk = initializeNDK();
33
- if (state.isConnected.value) return ndk;
34
- if (state.isConnecting.value) return ndk;
35
- state.isConnecting.value = true;
36
- try {
37
- await ndk.connect();
38
- state.isConnected.value = true;
39
- return ndk;
40
- } finally {
41
- state.isConnecting.value = false;
42
- }
43
- }
18
+ const userProfile = computed(() => state.userProfile.value);
44
19
  async function checkExtension() {
45
- if ("nostr" in window) return true;
20
+ if (typeof window !== "undefined" && "nostr" in window) return true;
46
21
  const toast = useToast();
47
22
  toast.add({
48
23
  title: "Nostr extension not found",
@@ -53,35 +28,28 @@ function useNuxstr() {
53
28
  }
54
29
  async function login() {
55
30
  if (await checkExtension()) {
56
- const ndk = initializeNDK();
57
- await connect();
58
- const signer = new NDKNip07Signer();
59
- ndk.signer = signer;
60
- const account = await signer.user();
61
- state.signer = signer;
62
- state.pubkey.value = account.pubkey;
63
- const user = ndk.getUser({ pubkey: account.pubkey });
64
- const profile = await user.fetchProfile();
65
- state.userProfile.value = mapProfile(profile);
31
+ try {
32
+ const pubkey = await window.nostr.getPublicKey();
33
+ state.pubkey.value = pubkey;
34
+ const profile = await fetchProfile(pubkey);
35
+ if (profile) {
36
+ state.userProfile.value = profile;
37
+ }
38
+ } catch (e) {
39
+ console.error("Failed to login", e);
40
+ }
66
41
  }
67
42
  }
68
- function mapComment(event) {
69
- return {
70
- id: event.id,
71
- pubkey: event.pubkey,
72
- created_at: event.created_at || DEFAULT_TIMESTAMP,
73
- content: event.content,
74
- profile: void 0
75
- };
76
- }
77
43
  async function fetchProfile(pubkey) {
78
44
  try {
79
- const ndk = initializeNDK();
80
- const user = await ndk.fetchUser(pubkey);
81
- if (user !== null) {
82
- const profile = await user?.fetchProfile();
83
- if (!profile) return void 0;
84
- return mapProfile(profile);
45
+ const filter = {
46
+ kinds: [0],
47
+ authors: [pubkey]
48
+ };
49
+ const event = await getEvent(filter);
50
+ if (event) {
51
+ const content = JSON.parse(event.content);
52
+ return mapProfile(content, pubkey);
85
53
  }
86
54
  return void 0;
87
55
  } catch (error) {
@@ -89,10 +57,10 @@ function useNuxstr() {
89
57
  return void 0;
90
58
  }
91
59
  }
92
- function mapProfile(profile) {
93
- if (profile === null) return {};
60
+ function mapProfile(profile, pubkey) {
94
61
  return {
95
- display_name: profile.displayName,
62
+ pubkey,
63
+ display_name: profile.display_name || profile.name,
96
64
  about: profile.about,
97
65
  image: profile.picture,
98
66
  nip05: profile.nip05,
@@ -102,20 +70,15 @@ function useNuxstr() {
102
70
  };
103
71
  }
104
72
  function logout() {
105
- state.signer = null;
106
73
  state.pubkey.value = DEFAULT_PUBKEY;
74
+ state.userProfile.value = null;
107
75
  }
108
76
  return {
109
- get ndk() {
110
- return initializeNDK();
111
- },
112
- connect,
113
77
  login,
114
78
  logout,
115
79
  isLoggedIn,
116
80
  pubkey: state.pubkey.value,
117
- mapProfile,
118
- mapComment,
81
+ userProfile,
119
82
  fetchProfile
120
83
  };
121
84
  }
@@ -1,41 +1,54 @@
1
1
  import { computed, ref } from "vue";
2
2
  import useNuxstr from "./useNuxstr.js";
3
- import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
3
+ import { useNostr } from "./useNostr.js";
4
4
  function useReplies(rootCommentId) {
5
- const { ndk, connect, mapComment, pubkey, fetchProfile } = useNuxstr();
5
+ const { pubkey, fetchProfile } = useNuxstr();
6
+ const { subscribe } = useNostr();
6
7
  const repliesData = ref([]);
7
8
  const error = ref(null);
8
9
  const replies = computed(() => {
9
10
  return repliesData.value.slice().sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
10
11
  });
11
12
  async function subscribeReplies() {
12
- await connect();
13
- const filter = { kinds: [NDKKind.GenericReply], limit: 100, ["#e"]: [rootCommentId] };
14
- const sub = ndk.subscribe(filter);
15
- sub.on("event", async (event) => {
16
- const reply2 = mapComment(event);
13
+ const filter = { kinds: [1111], limit: 100, ["#e"]: [rootCommentId] };
14
+ subscribe(filter, async (event) => {
15
+ if (repliesData.value.some((r) => r.id === event.id)) return;
16
+ const reply2 = {
17
+ id: event.id,
18
+ pubkey: event.pubkey,
19
+ created_at: event.created_at,
20
+ content: event.content,
21
+ profile: void 0
22
+ };
17
23
  reply2.profile = await fetchProfile(event.pubkey);
18
24
  repliesData.value.push(reply2);
19
25
  });
20
26
  }
21
27
  async function reply(comment) {
22
- const ndkEvent = await createReplyEvent(comment);
23
- return await ndkEvent.publish().then(() => true).catch((err) => {
28
+ const { publish } = useNostr();
29
+ try {
30
+ const event = await createReplyEvent(comment);
31
+ const signedEvent = await window.nostr.signEvent(event);
32
+ await publish(signedEvent);
33
+ return true;
34
+ } catch (err) {
24
35
  error.value = err?.message || String(err);
25
36
  return false;
26
- });
37
+ }
27
38
  }
28
39
  async function createReplyEvent(comment) {
29
- const event = new NDKEvent(ndk);
30
- event.kind = NDKKind.GenericReply;
31
- event.content = comment;
32
- event.tags = [
33
- ["e", `${rootCommentId}`],
34
- ["k", `${NDKKind.GenericReply}`],
35
- // The parent kind
36
- ["p", pubkey ?? ""]
37
- ];
38
- return event;
40
+ return {
41
+ kind: 1111,
42
+ // GenericReply
43
+ created_at: Math.floor(Date.now() / 1e3),
44
+ content: comment,
45
+ tags: [
46
+ ["e", rootCommentId],
47
+ ["k", "1111"],
48
+ // The parent kind
49
+ ["p", pubkey ?? ""]
50
+ ]
51
+ };
39
52
  }
40
53
  return { subscribeReplies, replies, reply };
41
54
  }
@@ -1,3 +1,29 @@
1
+ import type { Event } from 'nostr-tools'
2
+
3
+ export interface Nip07 {
4
+ getPublicKey: () => Promise<string>
5
+ signEvent: (event: Event) => Promise<Event>
6
+ getRelays?: () => Promise<Record<string, { read: boolean, write: boolean }>>
7
+ nip04?: {
8
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>
9
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>
10
+ }
11
+ nip44?: {
12
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>
13
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>
14
+ }
15
+ }
16
+
17
+ declare global {
18
+ interface Window {
19
+ nostr: Nip07
20
+ }
21
+ }
22
+
23
+ export const enum EventKind {
24
+ Metadata = 0,
25
+ GenericReply = 1111,
26
+ }
1
27
  export type NuxstrProfile = {
2
28
  name?: string
3
29
  display_name?: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threenine/nuxstr-comments",
3
- "version": "1.5.4",
3
+ "version": "1.6.0",
4
4
  "description": "Nuxt module to enable Nostr Comments on Nuxt 4 based websites",
5
5
  "repository": "threenine/nuxstr-comments",
6
6
  "license": "MIT",
@@ -23,8 +23,8 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@nostr-dev-kit/ndk": "^2.18.1",
27
- "defu": "^6.1.4"
26
+ "defu": "^6.1.4",
27
+ "nostr-tools": "^2.19.4"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@nuxt/devtools": "^2.6.2",