@threenine/nuxstr-comments 1.5.3 → 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/README.md CHANGED
@@ -23,12 +23,37 @@ Enable [nostr protocol](https://nostr.com/) based comment system on your Nuxt 4
23
23
  ## Features
24
24
 
25
25
  - Nostr-powered comments for Nuxt Content blog posts
26
- - NIP-07 login prompt if user is not authenticated
27
- - Comments are written in Markdown and rendered via @nuxt/content's ContentRendererMarkdown
26
+ - [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) Browser Extension login prompt if user is not authenticated
27
+ - [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md) Plain Text Content - (no HTML, Markdown, or other formatting)
28
28
  - Configurable relay list and tagging strategy
29
+ - Comments are published as kind:1111 as Website Url
30
+ ```json
31
+ {
32
+ "kind": 1111,
33
+ "content": "Nice article!",
34
+ "tags": [
35
+ // referencing the root url
36
+ ["I", "https://abc.com/articles/1"],
37
+ // the root "kind": for an url
38
+ ["K", "web"],
39
+
40
+ // the parent reference (same as root for top-level comments)
41
+ ["i", "https://abc.com/articles/1"],
42
+ // the parent "kind": for an url
43
+ ["k", "web"]
44
+ ]
45
+ // other fields
46
+ }
47
+ ```
29
48
 
30
- ## Quick Setup
31
49
 
50
+ > [!WARNING]
51
+ > NuxstrComments [NIP-22] MUST NOT be used to reply to kind 1 notes.
52
+ > NIP-10 should instead be followed.
53
+
54
+
55
+ ## Quick Setup
56
+
32
57
  Install the module to your Nuxt application with one command:
33
58
 
34
59
  ```bash
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.3",
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,23 +1,28 @@
1
1
  <script setup>
2
- import { onMounted } from "vue";
3
- import { marked } from "marked";
4
- const props = defineProps({
2
+ defineProps({
5
3
  content: { type: String, required: true },
6
4
  id: { type: String, required: true }
7
5
  });
8
- async function renderMarkdown(md) {
9
- return marked.parse(md);
10
- }
11
- onMounted(async () => {
12
- const targetEl = document.getElementById("comment-content");
13
- if (!targetEl) return;
14
- targetEl.innerHTML = await renderMarkdown(props.content);
15
- });
16
6
  </script>
17
7
 
18
8
  <template>
19
9
  <div class="mt-2 mb-2">
20
- <div id="comment-content" />
21
- <ReplyButton :content-id="props.id" />
10
+ <UCard
11
+ variant="subtle"
12
+ class="mt-auto"
13
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
14
+ >
15
+ <UTextarea
16
+ :model-value="content"
17
+ color="neutral"
18
+ variant="none"
19
+ autoresize
20
+ readonly
21
+ :rows="4"
22
+ class="w-full"
23
+ :ui="{ base: 'p-0 resize-none' }"
24
+ />
25
+ </ucard>
26
+ <ReplyButton :content-id="id" />
22
27
  </div>
23
28
  </template>
@@ -23,14 +23,16 @@ onMounted(() => {
23
23
  v-if="!isLoggedIn"
24
24
  class="text-sm text-muted-foreground"
25
25
  >
26
- <UButton
27
- color="primary"
28
- variant="solid"
29
- leading-icon="game-icons:ostrich"
30
- @click="login"
31
- >
32
- Sign in
33
- </UButton>
26
+ <u-tooltip text="Sign in with NIP07 browser extension like Alby or nos2fx to comment">
27
+ <UButton
28
+ color="primary"
29
+ variant="solid"
30
+ leading-icon="game-icons:ostrich"
31
+ @click="login"
32
+ >
33
+ Sign in
34
+ </UButton>
35
+ </u-tooltip>
34
36
  </div>
35
37
  </div>
36
38
  <ClientOnly>
@@ -56,11 +58,13 @@ onMounted(() => {
56
58
  <div v-if="comments.length === 0">
57
59
  <scaffold-comment />
58
60
  </div>
59
- <div
61
+ <UCard
60
62
  v-for="c in comments"
61
63
  v-else
62
64
  :key="c.id"
63
- class="rounded border border-gray-900 p-3 mt-2 mb-2"
65
+ variant="subtle"
66
+ class="mt-auto"
67
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
64
68
  >
65
69
  <comment-author
66
70
  :profile="c.profile"
@@ -70,7 +74,7 @@ onMounted(() => {
70
74
  :id="c.id"
71
75
  :content="c.content"
72
76
  />
73
- </div>
77
+ </UCard>
74
78
 
75
79
  <div
76
80
  v-if="isLoggedIn"
@@ -23,27 +23,34 @@ async function handlePost() {
23
23
  </script>
24
24
 
25
25
  <template>
26
- <div class="text-sm text-muted-foreground border border-green mt-4 p-6">
27
- <div class="flex gap-2">
28
- <div class="flex-1">
29
- <UTextarea
30
- v-model="comment"
31
- class="w-full mb-4"
32
- placeholder="Write a comment ...."
33
- :rows="4"
34
- />
35
- </div>
36
- <div class="flex flex-col justify-center items-center p-2">
37
- <UButton
38
- icon="mingcute:send-line"
39
- color="primary"
40
- variant="solid"
41
- :disabled="!comment.trim()"
42
- class=""
43
- size="xl"
44
- @click="handlePost"
45
- />
26
+ <UCard
27
+ variant="subtle"
28
+ class="mt-auto"
29
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
30
+ >
31
+ <form @submit.prevent="handlePost">
32
+ <UTextarea
33
+ v-model="comment"
34
+ color="neutral"
35
+ variant="none"
36
+ required
37
+ autoresize
38
+ placeholder="Write your comment..."
39
+ :rows="4"
40
+ class="w-full"
41
+ :ui="{ base: 'p-0 resize-none' }"
42
+ />
43
+
44
+ <div class="flex items-center justify-end">
45
+ <div class="flex items-center justify-end gap-2">
46
+ <UButton
47
+ type="submit"
48
+ color="primary"
49
+ label="Comment"
50
+ icon="i-lucide-send"
51
+ />
52
+ </div>
46
53
  </div>
47
- </div>
48
- </div>
54
+ </form>
55
+ </UCard>
49
56
  </template>
@@ -23,25 +23,32 @@ async function postReply(comment) {
23
23
  </script>
24
24
 
25
25
  <template>
26
- <div class="text-sm text-muted-foreground mt-4 p-6">
27
- <div class="flex gap-2">
28
- <div class="flex-1">
29
- <UTextarea
30
- v-model="content"
31
- class="w-full mb-4 rounded-xl"
32
- placeholder="Write a reply to this comment ...."
33
- :rows="4"
34
- />
35
- </div>
36
- <div class="flex flex-col justify-center items-center p-2">
26
+ <UCard
27
+ variant="subtle"
28
+ class="mt-auto"
29
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
30
+ >
31
+ <UTextarea
32
+ v-model="content"
33
+ color="neutral"
34
+ variant="none"
35
+ required
36
+ autoresize
37
+ placeholder="Write your comment..."
38
+ :rows="4"
39
+ class="w-full"
40
+ :ui="{ base: 'p-0 resize-none' }"
41
+ />
42
+ <div class="flex items-center justify-end">
43
+ <div class="flex items-center justify-end gap-2">
37
44
  <UButton
38
- icon="mingcute:send-line"
39
- size="xl"
45
+ type="submit"
40
46
  color="primary"
41
- variant="solid"
47
+ label="Reply"
48
+ icon="i-lucide-send"
42
49
  @click="postReply(content)"
43
50
  />
44
51
  </div>
45
52
  </div>
46
- </div>
53
+ </UCard>
47
54
  </template>
@@ -1,3 +1,4 @@
1
+ import type { Comment } from '../types/index.js';
1
2
  type __VLS_Props = {
2
3
  replies: Comment[];
3
4
  };
@@ -6,10 +6,12 @@ const props = defineProps({
6
6
 
7
7
  <template>
8
8
  <div class="px-10 py-4">
9
- <div
9
+ <UCard
10
10
  v-for="reply in props.replies"
11
11
  :key="reply.id"
12
- class="rounded-md border p-3 mt-2 mb-2"
12
+ variant="subtle"
13
+ class="mt-auto mb-3"
14
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
13
15
  >
14
16
  <div>
15
17
  <comment-author
@@ -20,6 +22,6 @@ const props = defineProps({
20
22
  {{ reply.content }}
21
23
  </p>
22
24
  </div>
23
- </div>
25
+ </UCard>
24
26
  </div>
25
27
  </template>
@@ -1,3 +1,4 @@
1
+ import type { Comment } from '../types/index.js';
1
2
  type __VLS_Props = {
2
3
  replies: Comment[];
3
4
  };
@@ -7,14 +7,16 @@
7
7
  <p class="text-xs">
8
8
  No comments available
9
9
  </p>
10
- <div class="rounded border p-3 mt-2 mb-2">
11
- <div class="flex gap-2 mb-3 items-center">
12
- <span><USkeleton class="h-4 w-5 rounded-full" /></span><USkeleton class="h-4" />
13
- </div>
10
+ <UCard
11
+ variant="subtle"
12
+ class="mt-auto"
13
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
14
+ >
15
+ <span><USkeleton class="h-4 w-5 rounded-full" /></span><USkeleton class="h-4" />
14
16
  <div class="mt-3">
15
17
  <USkeleton class="h-4" />
16
18
  </div>
17
- </div>
19
+ </UCard>
18
20
  </div>
19
21
  </template>
20
22
 
@@ -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,42 +28,39 @@ 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 = ndk.getUser({ pubkey });
81
- const profile = await user.fetchProfile();
82
- 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);
53
+ }
54
+ return void 0;
83
55
  } catch (error) {
84
56
  console.error("Failed to fetch profile for", pubkey, error);
85
57
  return void 0;
86
58
  }
87
59
  }
88
- function mapProfile(profile) {
89
- if (profile === null) return {};
60
+ function mapProfile(profile, pubkey) {
90
61
  return {
91
- display_name: profile.displayName,
62
+ pubkey,
63
+ display_name: profile.display_name || profile.name,
92
64
  about: profile.about,
93
65
  image: profile.picture,
94
66
  nip05: profile.nip05,
@@ -98,20 +70,15 @@ function useNuxstr() {
98
70
  };
99
71
  }
100
72
  function logout() {
101
- state.signer = null;
102
73
  state.pubkey.value = DEFAULT_PUBKEY;
74
+ state.userProfile.value = null;
103
75
  }
104
76
  return {
105
- get ndk() {
106
- return initializeNDK();
107
- },
108
- connect,
109
77
  login,
110
78
  logout,
111
79
  isLoggedIn,
112
80
  pubkey: state.pubkey.value,
113
- mapProfile,
114
- mapComment,
81
+ userProfile,
115
82
  fetchProfile
116
83
  };
117
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.3",
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,9 +23,8 @@
23
23
  "dist"
24
24
  ],
25
25
  "dependencies": {
26
- "@nostr-dev-kit/ndk": "^2.18.1",
27
26
  "defu": "^6.1.4",
28
- "marked": "^16.2.1"
27
+ "nostr-tools": "^2.19.4"
29
28
  },
30
29
  "devDependencies": {
31
30
  "@nuxt/devtools": "^2.6.2",
@@ -36,7 +35,7 @@
36
35
  "@nuxt/schema": "^4.2.1",
37
36
  "@nuxt/scripts": "0.11.10",
38
37
  "@nuxt/test-utils": "^3.19.2",
39
- "@nuxt/ui": "^4.2.1",
38
+ "@nuxt/ui": "^4.3.0",
40
39
  "@testing-library/jest-dom": "^6.8.0",
41
40
  "@testing-library/vue": "^8.1.0",
42
41
  "@types/node": "latest",