@threenine/nuxstr-comments 1.6.1 → 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.1",
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
  }
@@ -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,16 +36,16 @@ 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
51
 
@@ -46,8 +59,12 @@ onMounted(() => {
46
59
  <div class="space-y-4">
47
60
  <div
48
61
  v-if="loading"
62
+ class="space-y-4"
49
63
  >
50
- <scaffold-comment />
64
+ <scaffold-comment
65
+ v-for="n in 3"
66
+ :key="n"
67
+ />
51
68
  </div>
52
69
 
53
70
  <div
@@ -55,7 +72,13 @@ onMounted(() => {
55
72
  class="space-y-6"
56
73
  >
57
74
  <div v-if="comments.length === 0">
58
- <scaffold-comment />
75
+ <UEmpty
76
+ icon="i-lucide-message-square-off"
77
+ title="No comments yet"
78
+ description="Be the first to share your thoughts!"
79
+ variant="subtle"
80
+ size="sm"
81
+ />
59
82
  </div>
60
83
  <UCard
61
84
  v-for="c in comments"
@@ -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.1",
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"