@threenine/nuxstr-comments 1.1.1 → 1.2.1

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.1.1",
4
+ "version": "1.2.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -45,6 +45,14 @@ const module = defineNuxtModule({
45
45
  name: "NuxstrComments",
46
46
  filePath: resolver.resolve("./runtime/components/NuxstrComments.vue")
47
47
  });
48
+ addComponent({
49
+ name: "PostComment",
50
+ filePath: resolver.resolve("./runtime/components/PostComment.vue")
51
+ });
52
+ addComponent({
53
+ name: "ScaffoldComment",
54
+ filePath: resolver.resolve("./runtime/components/ScaffoldComment.vue")
55
+ });
48
56
  }
49
57
  });
50
58
 
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { ref, onMounted } from "vue";
2
+ import { onMounted } from "vue";
3
3
  import { useNuxstr } from "../composables/useNuxstr";
4
4
  import { useNuxstrComments } from "../composables/useNuxstrComments";
5
5
  import { marked } from "marked";
@@ -7,16 +7,10 @@ const props = defineProps({
7
7
  contentId: { type: String, required: false }
8
8
  });
9
9
  const { login, isLoggedIn } = useNuxstr();
10
- const { comments, fetchComments, postComment, loading } = useNuxstrComments(props.contentId);
11
- const comment = ref("");
10
+ const { comments, subscribeComments, loading } = useNuxstrComments(props.contentId);
12
11
  onMounted(() => {
13
- fetchComments();
12
+ subscribeComments();
14
13
  });
15
- async function handlePost() {
16
- if (!comment.value.trim()) return;
17
- const ok = await postComment(comment.value);
18
- if (ok) comment.value = "";
19
- }
20
14
  </script>
21
15
 
22
16
  <template>
@@ -25,78 +19,72 @@ async function handlePost() {
25
19
  <h3 class="text-lg font-semibold">
26
20
  Comments
27
21
  </h3>
28
- <UButton
22
+
23
+ <div
29
24
  v-if="!isLoggedIn"
30
- color="primary"
31
- variant="solid"
32
- leading-icon="game-icons:ostrich"
33
- @click="login"
25
+ class="text-sm text-muted-foreground"
34
26
  >
35
- Login
36
- </UButton>
27
+ <UButton
28
+ color="primary"
29
+ variant="solid"
30
+ leading-icon="game-icons:ostrich"
31
+ @click="login"
32
+ >
33
+ Login
34
+ </UButton>
35
+ </div>
37
36
  </div>
38
-
39
37
  <div
40
- v-if="loading"
41
- class="text-sm"
38
+ v-if="isLoggedIn"
39
+ class="text-sm text-muted-foreground"
42
40
  >
43
- Loading comments…
41
+ <PostComment :content-id="contentId" />
44
42
  </div>
45
43
 
46
- <div
47
- v-else
48
- class="space-y-6"
49
- >
44
+ <div class="space-y-4">
50
45
  <div
51
- v-for="c in comments"
52
- :key="c.id"
53
- class="rounded border p-3 mt-2 mb-2"
46
+ v-if="loading"
54
47
  >
55
- <div class="flex items-center gap-3 mb-3 mt-2">
56
- <div
57
- v-if="c.profile?.picture"
58
- class="flex-shrink-0"
59
- >
60
- <UAvatar
61
- :src="c.profile.picture"
62
- :alt="c.profile.name || c.profile.display_name || 'User avatar'"
63
- class="w-8 h-8 rounded-full object-cover"
64
- />
65
- </div>
66
- <div class="flex-1 min-w-0">
67
- <div class="truncate">
68
- {{ c.profile?.display_name || c.profile?.name || `${c.pubkey.slice(0, 8)}\u2026` }}
69
- <span class="text-xs">{{ new Date(c.created_at * 1e3).toLocaleString() }}</span>
48
+ <ScaffoldComment />
49
+ </div>
50
+ <div
51
+ v-else
52
+ class="space-y-6"
53
+ >
54
+ <div
55
+ v-for="c in comments"
56
+ :key="c.id"
57
+ class="rounded border p-3 mt-2 mb-2"
58
+ >
59
+ <div class="flex items-center gap-3 mb-3 mt-2">
60
+ <div
61
+ v-if="c.profile?.image"
62
+ class="flex-shrink-0"
63
+ >
64
+ <UAvatar
65
+ :src="c.profile.image"
66
+ :alt="c.profile.name || c.profile.display_name || 'User avatar'"
67
+ class="w-8 h-8 rounded-full object-cover"
68
+ />
69
+ </div>
70
+ <div class="flex-1 min-w-0">
71
+ <div class="truncate">
72
+ {{ c.profile?.display_name || c.profile?.name || `${c.pubkey.slice(0, 8)}\u2026` }}
73
+ <span class="text-xs">{{ new Date(c.created_at * 1e3).toLocaleString() }}</span>
74
+ </div>
70
75
  </div>
71
76
  </div>
77
+ <div class="prose prose-sm prose-invert mt-2 mb-2">
78
+ <!-- eslint-disable-next-line vue/no-v-html -->
79
+ <div v-html="marked.parse(c.content)" />
80
+ <!-- eslint-disable-next-line vue/no-v-html -->
81
+ </div>
72
82
  </div>
73
- <div class="prose prose-sm prose-invert mt-2 mb-2">
74
- <!-- eslint-disable-next-line vue/no-v-html -->
75
- <div v-html="marked.parse(c.content)" />
76
- <!-- eslint-disable-next-line vue/no-v-html -->
77
- </div>
78
- </div>
79
83
 
80
- <div
81
- v-if="isLoggedIn"
82
- class="space-y-2 mt-5"
83
- >
84
- <UTextarea
85
- v-model="comment"
86
- class="w-full"
87
- placeholder="Write a comment ...."
88
- :rows="4"
84
+ <div
85
+ v-if="isLoggedIn"
86
+ class=" mt-5"
89
87
  />
90
- <div class="flex justify-end">
91
- <UButton
92
- color="primary"
93
- variant="solid"
94
- :disabled="!comment.trim()"
95
- @click="handlePost"
96
- >
97
- Post Comment
98
- </UButton>
99
- </div>
100
88
  </div>
101
89
  </div>
102
90
  </div>
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ import { ref } from "vue";
3
+ import { useNuxstrComments } from "../composables/useNuxstrComments";
4
+ const props = defineProps({
5
+ contentId: { type: String, required: false }
6
+ });
7
+ const EMPTY_COMMENT = "";
8
+ const { postComment } = useNuxstrComments(props.contentId);
9
+ const comment = ref(EMPTY_COMMENT);
10
+ function isValidComment(commentText) {
11
+ return commentText.trim().length > 0;
12
+ }
13
+ function clearComment() {
14
+ comment.value = EMPTY_COMMENT;
15
+ }
16
+ async function handlePost() {
17
+ if (!isValidComment(comment.value)) return;
18
+ const wasPosted = await postComment(comment.value);
19
+ if (wasPosted) {
20
+ clearComment();
21
+ }
22
+ }
23
+ </script>
24
+
25
+ <template>
26
+ <div class="text-sm text-muted-foreground border border-green mt-16">
27
+ <UTextarea
28
+ v-model="comment"
29
+ class="w-full"
30
+ placeholder="Write a comment ...."
31
+ :rows="4"
32
+ />
33
+ <div class="flex justify-end">
34
+ <UButton
35
+ color="primary"
36
+ variant="solid"
37
+ :disabled="!comment.trim()"
38
+ @click="handlePost"
39
+ >
40
+ Post Comment
41
+ </UButton>
42
+ </div>
43
+ </div>
44
+ </template>
@@ -0,0 +1,5 @@
1
+ type __VLS_Props = {
2
+ contentId?: string;
3
+ };
4
+ declare const _default: 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>;
5
+ export default _default;
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+
3
+ </script>
4
+
5
+ <template>
6
+ <div class="flex items-center gap-4">
7
+ <USkeleton class="h-12 w-12 rounded-full animate-pulse rounded-md bg-elevated" />
8
+
9
+ <div class="grid gap-2">
10
+ <USkeleton class="h-4 w-[500px]" />
11
+ <USkeleton class="h-4 w-[500px]" />
12
+ </div>
13
+ </div>
14
+ </template>
15
+
16
+ <style scoped>
17
+
18
+ </style>
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ export default _default;
@@ -1,4 +1,5 @@
1
- import NDK from '@nostr-dev-kit/ndk';
1
+ import NDK, { type NDKEvent } from '@nostr-dev-kit/ndk';
2
+ import type { Profile, Comment } from '../types/index.js';
2
3
  export declare function useNuxstr(): {
3
4
  readonly ndk: NDK;
4
5
  connect: () => Promise<NDK>;
@@ -6,4 +7,6 @@ export declare function useNuxstr(): {
6
7
  logout: () => void;
7
8
  isLoggedIn: import("vue").ComputedRef<boolean>;
8
9
  pubkey: import("vue").Ref<string | null | undefined, string | null | undefined>;
10
+ mapProfile: (profile: NDKUserProfile) => Profile;
11
+ mapComment: (event: NDKEvent) => Comment;
9
12
  };
@@ -10,7 +10,8 @@ export function useNuxstr() {
10
10
  signer: null,
11
11
  pubkey: ref(null),
12
12
  isConnecting: ref(false),
13
- isConnected: ref(false)
13
+ isConnected: ref(false),
14
+ userProfile: ref(null)
14
15
  };
15
16
  }
16
17
  const state = w.__nuxstr;
@@ -55,8 +56,34 @@ export function useNuxstr() {
55
56
  state.signer = signer;
56
57
  state.pubkey.value = user.pubkey;
57
58
  await connect();
59
+ const profile = ndk.getUser({ pubkey: user.pubkey });
60
+ profile.fetchProfile().then((profile2) => {
61
+ state.userProile = mapProfile(profile2);
62
+ }).catch((err) => {
63
+ console.error("Failed to fetch profile", err);
64
+ });
58
65
  }
59
66
  }
67
+ function mapComment(event) {
68
+ return {
69
+ id: event.id,
70
+ pubkey: event.pubkey,
71
+ created_at: event.created_at || 0,
72
+ content: event.content,
73
+ profile: null
74
+ };
75
+ }
76
+ function mapProfile(profile) {
77
+ return {
78
+ display_name: profile.displayName,
79
+ about: profile.about,
80
+ image: profile.picture,
81
+ nip05: profile.nip05,
82
+ lud06: profile.lud06,
83
+ lud16: profile.lud16,
84
+ website: profile.website
85
+ };
86
+ }
60
87
  function logout() {
61
88
  state.signer = null;
62
89
  state.pubkey.value = null;
@@ -69,6 +96,8 @@ export function useNuxstr() {
69
96
  login,
70
97
  logout,
71
98
  isLoggedIn,
72
- pubkey: state.pubkey
99
+ pubkey: state.pubkey,
100
+ mapProfile,
101
+ mapComment
73
102
  };
74
103
  }
@@ -1,4 +1,4 @@
1
- import type { NuxstrComment } from '~/src/runtime/types';
1
+ import type { Comment } from '~/src/runtime/types';
2
2
  export declare function useNuxstrComments(customContentId?: string): {
3
3
  loading: import("vue").Ref<boolean, boolean>;
4
4
  error: import("vue").Ref<string | null, string | null>;
@@ -8,26 +8,32 @@ export declare function useNuxstrComments(customContentId?: string): {
8
8
  created_at: number;
9
9
  content: string;
10
10
  profile?: {
11
- name?: string | undefined;
11
+ pubkey: string;
12
12
  display_name?: string | undefined;
13
13
  about?: string | undefined;
14
- picture?: string | undefined;
14
+ image?: string | undefined;
15
15
  nip05?: string | undefined;
16
+ lud06?: string | undefined;
17
+ lud16?: string | undefined;
18
+ website?: string | undefined;
16
19
  } | undefined;
17
- }[], NuxstrComment[] | {
20
+ }[], Comment[] | {
18
21
  id: string;
19
22
  pubkey: string;
20
23
  created_at: number;
21
24
  content: string;
22
25
  profile?: {
23
- name?: string | undefined;
26
+ pubkey: string;
24
27
  display_name?: string | undefined;
25
28
  about?: string | undefined;
26
- picture?: string | undefined;
29
+ image?: string | undefined;
27
30
  nip05?: string | undefined;
31
+ lud06?: string | undefined;
32
+ lud16?: string | undefined;
33
+ website?: string | undefined;
28
34
  } | undefined;
29
35
  }[]>;
30
36
  isLoggedIn: import("vue").ComputedRef<boolean>;
31
- fetchComments: () => Promise<void>;
37
+ subscribeComments: () => Promise<void>;
32
38
  postComment: (comment: string) => Promise<boolean>;
33
39
  };
@@ -3,7 +3,7 @@ import { useRoute, useRuntimeConfig, useRequestURL } from "#imports";
3
3
  import { useNuxstr } from "./useNuxstr.js";
4
4
  import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
5
5
  export function useNuxstrComments(customContentId) {
6
- const { ndk, connect, isLoggedIn } = useNuxstr();
6
+ const { ndk, connect, isLoggedIn, mapProfile, mapComment } = useNuxstr();
7
7
  const route = useRoute();
8
8
  const config = useRuntimeConfig();
9
9
  const opts = config.public?.nuxstrComments || {};
@@ -24,60 +24,23 @@ export function useNuxstrComments(customContentId) {
24
24
  }
25
25
  async function fetchProfile(pubkey) {
26
26
  try {
27
- const filter = { kinds: [0], authors: [pubkey] };
28
- const events = await ndk.fetchEvents(filter);
29
- const latestEvent = Array.from(events).sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0];
30
- if (!latestEvent?.content) return void 0;
31
- const profileData = JSON.parse(latestEvent.content);
32
- return {
33
- name: profileData.name,
34
- display_name: profileData.display_name,
35
- about: profileData.about,
36
- picture: profileData.picture,
37
- nip05: profileData.nip05
38
- };
27
+ const user = ndk.getUser({ pubkey });
28
+ const profile = await user.fetchProfile();
29
+ return mapProfile(profile);
39
30
  } catch (error2) {
40
- console.warn("Failed to fetch profile for", pubkey, error2);
31
+ console.error("Failed to fetch profile for", pubkey, error2);
41
32
  return void 0;
42
33
  }
43
34
  }
44
- async function getEventsByTag(tag) {
45
- try {
46
- await connect();
47
- const filter = { kinds: [NDKKind.GenericReply], ["#t"]: [tag], limit: 30, ["#k"]: [siteUrl()] };
48
- const events = await ndk.fetchEvents(filter);
49
- console.log("events", events);
50
- return Array.from(events);
51
- } catch (error2) {
52
- console.warn("Failed to fetch events by tag", tag, error2);
53
- return [];
54
- }
55
- }
56
- async function fetchComments() {
57
- loading.value = true;
58
- error.value = null;
59
- try {
60
- const events = await getEventsByTag(tagValue());
61
- const list = Array.from(events).sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
62
- const pubkeys = [...new Set(list.map((e) => e.pubkey))];
63
- const profilePromises = pubkeys.map(async (pubkey) => {
64
- const profile = await fetchProfile(pubkey);
65
- return { pubkey, profile };
66
- });
67
- const profileResults = await Promise.all(profilePromises);
68
- const profileMap = new Map(profileResults.map((r) => [r.pubkey, r.profile]));
69
- comments.value = list.map((e) => ({
70
- id: e.id,
71
- pubkey: e.pubkey,
72
- created_at: e.created_at || 0,
73
- content: e.content,
74
- profile: profileMap.get(e.pubkey)
75
- }));
76
- } catch (e) {
77
- error.value = e?.message || String(e);
78
- } finally {
79
- loading.value = false;
80
- }
35
+ async function subscribeComments() {
36
+ await connect();
37
+ const filter = { kinds: [NDKKind.GenericReply], ["#t"]: [tagValue()], limit: 100, ["#k"]: [siteUrl()] };
38
+ const sub = await ndk.subscribe(filter);
39
+ sub.on("event", async (event) => {
40
+ const comment = mapComment(event);
41
+ comment.profile = await fetchProfile(event.pubkey);
42
+ comments.value.push(comment);
43
+ });
81
44
  }
82
45
  async function postComment(comment) {
83
46
  await connect();
@@ -88,14 +51,10 @@ export function useNuxstrComments(customContentId) {
88
51
  ["t", tagValue()],
89
52
  ["k", siteUrl()]
90
53
  ];
91
- const ok = await e.publish().then(() => true).catch((err) => {
54
+ return await e.publish().then(() => true).catch((err) => {
92
55
  error.value = err?.message || String(err);
93
56
  return false;
94
57
  });
95
- if (ok) {
96
- await fetchComments();
97
- }
98
- return ok;
99
58
  }
100
- return { loading, error, comments, isLoggedIn, fetchComments, postComment };
59
+ return { loading, error, comments, isLoggedIn, subscribeComments, postComment };
101
60
  }
@@ -14,6 +14,25 @@ export type NuxstrComment = {
14
14
  profile?: NuxstrProfile
15
15
  }
16
16
 
17
+ export type Comment = {
18
+ id: string
19
+ pubkey: string
20
+ created_at: number
21
+ content: string
22
+ profile?: Profile
23
+ }
24
+
25
+ export type Profile = {
26
+ pubkey: string
27
+ display_name?: string
28
+ about?: string
29
+ image?: string
30
+ nip05?: string
31
+ lud06?: string
32
+ lud16?: string
33
+ website?: string
34
+ }
35
+
17
36
  export interface ElementNode {
18
37
  type: 'element'
19
38
  tag: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threenine/nuxstr-comments",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "Nuxstr Comments",
5
5
  "repository": "threenine/nuxstr-comments",
6
6
  "license": "MIT",
@@ -36,7 +36,7 @@
36
36
  "@nuxt/schema": "^4.0.3",
37
37
  "@nuxt/scripts": "0.11.10",
38
38
  "@nuxt/test-utils": "^3.19.2",
39
- "@nuxt/ui": "3.3.2",
39
+ "@nuxt/ui": "^3.3.3",
40
40
  "@testing-library/jest-dom": "^6.8.0",
41
41
  "@testing-library/vue": "^8.1.0",
42
42
  "@unhead/vue": "^2.0.14",