@threenine/nuxstr-comments 1.6.0 → 1.7.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
@@ -6,7 +6,7 @@ Find and replace all on all files (CMD+SHIFT+F):
6
6
  - Package name: @threenine/nuxstr-comments
7
7
  - Description: Nuxstr Comments
8
8
  -->
9
-
9
+ ![Nuxt Fathom module](https://res.cloudinary.com/threenine-co-uk/image/upload/v1733252921/nuxstr-comments_rk7pig.png)
10
10
  # Nuxstr Comments
11
11
 
12
12
  [![npm version][npm-version-src]][npm-version-href]
@@ -14,6 +14,7 @@ Find and replace all on all files (CMD+SHIFT+F):
14
14
  [![License][license-src]][license-href]
15
15
  [![Nuxt][nuxt-src]][nuxt-href]
16
16
 
17
+ ![]()
17
18
  Enable [nostr protocol](https://nostr.com/) based comment system on your Nuxt 4 based applications.
18
19
 
19
20
  - [✨  Release Notes](/CHANGELOG.md)
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@threenine/nuxstr-comments",
3
3
  "configKey": "nuxstrComments",
4
- "version": "1.6.0",
4
+ "version": "1.7.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -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-tools", "defu");
34
+ nitroConfig.externals.inline.push("nostr-tools", "defu");
35
35
  });
36
36
  nuxt.options.build.transpile = nuxt.options.build.transpile || [];
37
- nuxt.options.build.transpile.push("tseep", "nostr-tools", "defu");
37
+ nuxt.options.build.transpile.push("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([
@@ -5,7 +5,7 @@ export declare class NostrManager {
5
5
  private relays;
6
6
  private constructor();
7
7
  static getInstance(relays: string[]): NostrManager;
8
- subscribe(filter: Filter, onEvent: (event: NToolEvent) => void): import("nostr-tools/abstract-pool").SubCloser;
8
+ subscribe(filter: Filter, onEvent: (event: NToolEvent) => void, onEose?: () => void): import("nostr-tools/abstract-pool").SubCloser;
9
9
  publish(event: NToolEvent): Promise<void>;
10
10
  getEvent(filter: Filter): Promise<NToolEvent | null>;
11
11
  close(): Promise<void>;
@@ -19,12 +19,15 @@ export class NostrManager {
19
19
  }
20
20
  return NostrManager.instance;
21
21
  }
22
- subscribe(filter, onEvent) {
22
+ subscribe(filter, onEvent, onEose) {
23
23
  return this.pool.subscribeMany(this.relays, filter, {
24
24
  onevent(event) {
25
25
  if (verifyEvent(event)) {
26
26
  onEvent(event);
27
27
  }
28
+ },
29
+ oneose() {
30
+ if (onEose) onEose();
28
31
  }
29
32
  });
30
33
  }
@@ -22,7 +22,7 @@ defineProps({
22
22
  class="w-full"
23
23
  :ui="{ base: 'p-0 resize-none' }"
24
24
  />
25
- </ucard>
25
+ </UCard>
26
26
  <ReplyButton :content-id="id" />
27
27
  </div>
28
28
  </template>
@@ -1,6 +1,14 @@
1
1
  type __VLS_Props = {
2
2
  contentId?: string;
3
3
  };
4
- 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>;
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ querying: (...args: any[]) => void;
6
+ completed: (...args: any[]) => void;
7
+ "no-comments": (...args: any[]) => void;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onQuerying?: ((...args: any[]) => any) | undefined;
10
+ onCompleted?: ((...args: any[]) => any) | undefined;
11
+ "onNo-comments"?: ((...args: any[]) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
13
  declare const _default: typeof __VLS_export;
6
14
  export default _default;
@@ -1,12 +1,25 @@
1
1
  <script setup>
2
- import { onMounted } from "vue";
2
+ import { onMounted, ref, watch } from "vue";
3
3
  import useNuxstr from "../composables/useNuxstr";
4
4
  import useComments from "../composables/useComments";
5
+ import SignInModal from "./SignInModal.vue";
5
6
  const props = defineProps({
6
7
  contentId: { type: String, required: false }
7
8
  });
8
- const { login, isLoggedIn } = useNuxstr();
9
+ const emit = defineEmits(["querying", "completed", "no-comments"]);
10
+ const { isLoggedIn } = useNuxstr();
9
11
  const { comments, subscribeComments, loading } = useComments(props.contentId);
12
+ const isSignInModalOpen = ref(false);
13
+ watch(loading, (isLoading) => {
14
+ if (isLoading) {
15
+ emit("querying");
16
+ } else {
17
+ emit("completed");
18
+ if (comments.value.length === 0) {
19
+ emit("no-comments");
20
+ }
21
+ }
22
+ });
10
23
  onMounted(() => {
11
24
  subscribeComments();
12
25
  });
@@ -23,66 +36,73 @@ onMounted(() => {
23
36
  v-if="!isLoggedIn"
24
37
  class="text-sm text-muted-foreground"
25
38
  >
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>
39
+ <UButton
40
+ color="primary"
41
+ variant="solid"
42
+ leading-icon="game-icons:ostrich"
43
+ @click="isSignInModalOpen = true"
44
+ >
45
+ Sign in
46
+ </UButton>
47
+
48
+ <SignInModal v-model:open="isSignInModalOpen" />
36
49
  </div>
37
50
  </div>
38
- <ClientOnly>
51
+
52
+ <div
53
+ v-if="isLoggedIn"
54
+ class="text-sm text-muted-foreground"
55
+ >
56
+ <PostComment :content-id="contentId" />
57
+ </div>
58
+
59
+ <div class="space-y-4">
39
60
  <div
40
- v-if="isLoggedIn"
41
- class="text-sm text-muted-foreground"
61
+ v-if="loading"
62
+ class="space-y-4"
42
63
  >
43
- <PostComment :content-id="contentId" />
64
+ <scaffold-comment
65
+ v-for="n in 3"
66
+ :key="n"
67
+ />
44
68
  </div>
45
- </ClientOnly>
46
- <ClientOnly>
47
- <div class="space-y-4">
48
- <div
49
- v-if="loading"
50
- >
51
- <scaffold-comment />
52
- </div>
53
69
 
54
- <div
55
- v-else
56
- class="space-y-6"
57
- >
58
- <div v-if="comments.length === 0">
59
- <scaffold-comment />
60
- </div>
61
- <UCard
62
- v-for="c in comments"
63
- v-else
64
- :key="c.id"
70
+ <div
71
+ v-else
72
+ class="space-y-6"
73
+ >
74
+ <div v-if="comments.length === 0">
75
+ <UEmpty
76
+ icon="i-lucide-message-square-off"
77
+ title="No comments yet"
78
+ description="Be the first to share your thoughts!"
65
79
  variant="subtle"
66
- class="mt-auto"
67
- :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
68
- >
69
- <comment-author
70
- :profile="c.profile"
71
- :created-at="c.created_at"
72
- />
73
- <comment-view
74
- :id="c.id"
75
- :content="c.content"
76
- />
77
- </UCard>
78
-
79
- <div
80
- v-if="isLoggedIn"
81
- class=" mt-5"
80
+ size="sm"
82
81
  />
83
82
  </div>
83
+ <UCard
84
+ v-for="c in comments"
85
+ v-else
86
+ :key="c.id"
87
+ variant="subtle"
88
+ class="mt-auto"
89
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
90
+ >
91
+ <comment-author
92
+ :profile="c.profile"
93
+ :created-at="c.created_at"
94
+ />
95
+ <comment-view
96
+ :id="c.id"
97
+ :content="c.content"
98
+ />
99
+ </UCard>
100
+
101
+ <div
102
+ v-if="isLoggedIn"
103
+ class=" mt-5"
104
+ />
84
105
  </div>
85
- </ClientOnly>
86
- <client-only />
106
+ </div>
87
107
  </div>
88
108
  </template>
@@ -1,6 +1,14 @@
1
1
  type __VLS_Props = {
2
2
  contentId?: string;
3
3
  };
4
- 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>;
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ querying: (...args: any[]) => void;
6
+ completed: (...args: any[]) => void;
7
+ "no-comments": (...args: any[]) => void;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
9
+ onQuerying?: ((...args: any[]) => any) | undefined;
10
+ onCompleted?: ((...args: any[]) => any) | undefined;
11
+ "onNo-comments"?: ((...args: any[]) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
5
13
  declare const _default: typeof __VLS_export;
6
14
  export default _default;
@@ -3,21 +3,19 @@
3
3
  </script>
4
4
 
5
5
  <template>
6
- <div>
7
- <p class="text-xs">
8
- No comments available
9
- </p>
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" />
16
- <div class="mt-3">
17
- <USkeleton class="h-4" />
18
- </div>
19
- </UCard>
20
- </div>
6
+ <UCard
7
+ variant="subtle"
8
+ class="mt-auto"
9
+ :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
10
+ >
11
+ <div class="flex items-center gap-1.5">
12
+ <USkeleton class="h-4 w-5 rounded-full" /><USkeleton class="h-4" />
13
+ </div>
14
+
15
+ <div class="mt-3 space-y-2">
16
+ <USkeleton class="h-4" />
17
+ </div>
18
+ </UCard>
21
19
  </template>
22
20
 
23
21
  <style scoped>
@@ -0,0 +1,10 @@
1
+ type __VLS_ModelProps = {
2
+ 'open'?: boolean;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:open": (value: boolean) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
7
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,53 @@
1
+ <script setup>
2
+ import Extension from "./signin/Extension.vue";
3
+ const open = defineModel("open", { type: Boolean, ...{ default: false } });
4
+ function handleSuccess() {
5
+ open.value = false;
6
+ }
7
+ </script>
8
+
9
+ <template>
10
+ <UModal
11
+ v-model:open="open"
12
+ title="Sign in to Comment"
13
+ description="Choose your preferred way to sign in with Nostr."
14
+ >
15
+ <template #body>
16
+ <div class="space-y-6">
17
+ <!-- Nostr Education -->
18
+ <div class="p-4 rounded-lg bg-muted/50">
19
+ <div class="flex items-center gap-2 mb-2 text-sm font-semibold">
20
+ <UIcon
21
+ name="i-lucide-help-circle"
22
+ class="w-4 h-4"
23
+ />
24
+ What is Nostr?
25
+ </div>
26
+ <p class="text-xs leading-relaxed text-muted-foreground">
27
+ Nostr is a decentralized protocol for social media. Instead of a username and password, you use a cryptographic key pair.
28
+ Your "public key" is your identity, and your "private key" is used to sign messages. <a
29
+ href="https://threenine.blog/posts/what-is-nostr"
30
+ target="_blank"
31
+ class="underline hover:text-primary text-primary"
32
+ >Learn more...</a>
33
+ </p>
34
+ </div>
35
+
36
+ <!-- Sign-in Options -->
37
+ <div class="space-y-4">
38
+ <Extension @login-success="handleSuccess" />
39
+ </div>
40
+
41
+ <!-- Footer Info -->
42
+ <div class="pt-4 border-t border-border">
43
+ <p class="text-[10px] text-center text-muted-foreground uppercase tracking-wider font-medium">
44
+ Privacy First
45
+ </p>
46
+ <p class="mt-1 text-xs text-center text-muted-foreground">
47
+ We never see your private key. Signing happens directly in your browser or extension.
48
+ </p>
49
+ </div>
50
+ </div>
51
+ </template>
52
+ </UModal>
53
+ </template>
@@ -0,0 +1,10 @@
1
+ type __VLS_ModelProps = {
2
+ 'open'?: boolean;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:open": (value: boolean) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
7
+ "onUpdate:open"?: ((value: boolean) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,7 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
2
+ "login-success": (...args: any[]) => void;
3
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
4
+ "onLogin-success"?: ((...args: any[]) => any) | undefined;
5
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,56 @@
1
+ <script setup>
2
+ import useNuxstr from "../../composables/useNuxstr";
3
+ const { login } = useNuxstr();
4
+ const emit = defineEmits(["login-success"]);
5
+ async function handleLogin() {
6
+ await login();
7
+ emit("login-success");
8
+ }
9
+ </script>
10
+
11
+ <template>
12
+ <div class="flex flex-col gap-4">
13
+ <div class="flex items-start gap-3 p-4 border rounded-lg border-primary/20 bg-primary/5">
14
+ <UIcon
15
+ name="i-lucide-info"
16
+ class="w-5 h-5 mt-0.5 text-primary"
17
+ />
18
+ <div class="text-sm">
19
+ <p class="font-medium text-primary">
20
+ Browser Extension (NIP-07)
21
+ </p>
22
+ <p class="mt-1 text-muted-foreground">
23
+ Use a browser extension like Diogel, Alby or nos2x. This is the most secure way to sign in without sharing your private key.
24
+ </p>
25
+ </div>
26
+ </div>
27
+
28
+ <UButton
29
+ block
30
+ color="primary"
31
+ size="lg"
32
+ icon="game-icons:ostrich"
33
+ label="Sign in with Extension"
34
+ @click="handleLogin"
35
+ />
36
+
37
+ <p class="text-xs text-center text-muted-foreground">
38
+ Don't have an extension?
39
+ <a
40
+ href="https://threenine.io/products/diogel"
41
+ target="_blank"
42
+ class="underline hover:text-primary"
43
+ >Diogel</a> or
44
+ <a
45
+ href="https://getalby.com/"
46
+ target="_blank"
47
+ class="underline hover:text-primary"
48
+ >Get Alby</a> or
49
+ <a
50
+ href="https://github.com/fiatjaf/nos2x"
51
+ target="_blank"
52
+ class="underline hover:text-primary"
53
+ >nos2x</a>
54
+ </p>
55
+ </div>
56
+ </template>
@@ -0,0 +1,7 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
2
+ "login-success": (...args: any[]) => void;
3
+ }, string, import("vue").PublicProps, Readonly<{}> & Readonly<{
4
+ "onLogin-success"?: ((...args: any[]) => any) | undefined;
5
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -33,24 +33,31 @@ function useComments(customContentId) {
33
33
  return `${siteUrl()}${path}`;
34
34
  }
35
35
  async function subscribeComments() {
36
+ loading.value = true;
36
37
  const filter = {
37
38
  kinds: [1111],
38
39
  // NDKKind.GenericReply is 22
39
40
  ["#t"]: [tagValue()],
40
41
  limit: 100
41
42
  };
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
- };
51
- comment.profile = await fetchProfile(event.pubkey);
52
- commentsData.value.push(comment);
53
- });
43
+ subscribe(
44
+ filter,
45
+ async (event) => {
46
+ if (commentsData.value.some((c) => c.id === event.id)) return;
47
+ const comment = {
48
+ id: event.id,
49
+ pubkey: event.pubkey,
50
+ created_at: event.created_at,
51
+ content: event.content,
52
+ profile: void 0
53
+ };
54
+ comment.profile = await fetchProfile(event.pubkey);
55
+ commentsData.value.push(comment);
56
+ },
57
+ () => {
58
+ loading.value = false;
59
+ }
60
+ );
54
61
  }
55
62
  async function postComment(comment) {
56
63
  const { publish } = useNostr();
@@ -2,7 +2,7 @@ import type { Filter, Event } from 'nostr-tools';
2
2
  import { NostrManager } from '../classes/NostrManager.js';
3
3
  export declare const useNostr: (relays?: string[]) => {
4
4
  nostrManager: NostrManager;
5
- subscribe: (filter: Filter, onEvent: (event: Event) => void) => import("nostr-tools/abstract-pool").SubCloser;
5
+ subscribe: (filter: Filter, onEvent: (event: Event) => void, onEose?: () => void) => import("nostr-tools/abstract-pool").SubCloser;
6
6
  publish: (event: Event) => Promise<void>;
7
7
  getEvent: (filter: Filter) => Promise<import("nostr-tools").NostrEvent | null>;
8
8
  };
@@ -5,8 +5,8 @@ export const useNostr = (relays) => {
5
5
  const opts = config.public?.nuxstrComments || {};
6
6
  const effectiveRelays = relays || opts.relays || [];
7
7
  const nostrManager = NostrManager.getInstance(effectiveRelays);
8
- const subscribe = (filter, onEvent) => {
9
- return nostrManager.subscribe(filter, onEvent);
8
+ const subscribe = (filter, onEvent, onEose) => {
9
+ return nostrManager.subscribe(filter, onEvent, onEose);
10
10
  };
11
11
  const publish = (event) => {
12
12
  console.log("publishing Comment", event);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threenine/nuxstr-comments",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",
@@ -30,12 +30,12 @@
30
30
  "@nuxt/devtools": "^2.6.2",
31
31
  "@nuxt/eslint": "1.9.0",
32
32
  "@nuxt/eslint-config": "^1.9.0",
33
- "@nuxt/kit": "^4.2.1",
33
+ "@nuxt/kit": "^4.3.1",
34
34
  "@nuxt/module-builder": "^1.0.2",
35
35
  "@nuxt/schema": "^4.2.1",
36
36
  "@nuxt/scripts": "0.11.10",
37
37
  "@nuxt/test-utils": "^3.19.2",
38
- "@nuxt/ui": "^4.3.0",
38
+ "@nuxt/ui": "^4.5.1",
39
39
  "@testing-library/jest-dom": "^6.8.0",
40
40
  "@testing-library/vue": "^8.1.0",
41
41
  "@types/node": "latest",
@@ -43,7 +43,7 @@
43
43
  "changelogen": "^0.6.2",
44
44
  "eslint": "^9.35.0",
45
45
  "jsdom": "^26.1.0",
46
- "nuxt": "^4.2.1",
46
+ "nuxt": "^4.3.1",
47
47
  "typescript": "~5.9.2",
48
48
  "vitest": "^3.2.4",
49
49
  "vue-tsc": "^3.0.6"