@threenine/nuxstr-comments 1.2.2 → 1.3.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 +14 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +23 -2
- package/dist/runtime/components/CommentAuthor.d.vue.ts +6 -0
- package/dist/runtime/components/CommentAuthor.vue +27 -0
- package/dist/runtime/components/CommentAuthor.vue.d.ts +6 -0
- package/dist/runtime/components/CommentCommandBar.d.vue.ts +5 -0
- package/dist/runtime/components/CommentCommandBar.vue +55 -0
- package/dist/runtime/components/CommentCommandBar.vue.d.ts +5 -0
- package/dist/runtime/components/CommentView.d.vue.ts +6 -0
- package/dist/runtime/components/CommentView.vue +19 -0
- package/dist/runtime/components/CommentView.vue.d.ts +6 -0
- package/dist/runtime/components/NuxstrComments.d.vue.ts +5 -0
- package/dist/runtime/components/NuxstrComments.vue +20 -35
- package/dist/runtime/components/PostComment.d.vue.ts +5 -0
- package/dist/runtime/components/PostComment.vue +23 -18
- package/dist/runtime/components/PostReply.d.vue.ts +5 -0
- package/dist/runtime/components/PostReply.vue +47 -0
- package/dist/runtime/components/PostReply.vue.d.ts +5 -0
- package/dist/runtime/components/ReplyView.d.vue.ts +5 -0
- package/dist/runtime/components/ReplyView.vue +23 -0
- package/dist/runtime/components/ReplyView.vue.d.ts +5 -0
- package/dist/runtime/components/ScaffoldComment.d.vue.ts +2 -0
- package/dist/runtime/components/ScaffoldComment.vue +11 -6
- package/dist/runtime/composables/{useNuxstrComments.d.ts → useComments.d.ts} +1 -1
- package/dist/runtime/composables/{useNuxstrComments.js → useComments.js} +26 -20
- package/dist/runtime/composables/useNuxstr.d.ts +2 -1
- package/dist/runtime/composables/useNuxstr.js +17 -8
- package/dist/runtime/composables/useReplies.d.ts +21 -0
- package/dist/runtime/composables/useReplies.js +48 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -14,7 +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
|
+
Enable [nostr protocol](https://nostr.com/) based comment system on your Nuxt 4 based applications.
|
|
18
18
|
|
|
19
19
|
- [✨ Release Notes](/CHANGELOG.md)
|
|
20
20
|
<!-- - [🏀 Online playground](https://stackblitz.com/github/your-org/@threenine/nuxstr-comments?file=playground%2Fapp.vue) -->
|
|
@@ -103,6 +103,19 @@ When a user attempts to post, they will be prompted to log in with their Nostr b
|
|
|
103
103
|
|
|
104
104
|
</details>
|
|
105
105
|
|
|
106
|
+
## Support
|
|
107
|
+
⚡️ lightning address:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
threenine@getalby.com
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
<br/>
|
|
114
|
+
<div align="center">
|
|
115
|
+
<a href="https://www.buymeacoffee.com/xbhtjcric" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
|
116
|
+
|
|
117
|
+
</div>
|
|
118
|
+
|
|
106
119
|
|
|
107
120
|
<!-- Badges -->
|
|
108
121
|
[npm-version-src]: https://img.shields.io/npm/v/@threenine/nuxstr-comments/latest.svg?style=flat&colorA=020420&colorB=00DC82
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const module = defineNuxtModule({
|
|
|
8
8
|
},
|
|
9
9
|
// Default configuration options of the Nuxt module
|
|
10
10
|
defaults: {
|
|
11
|
-
relays: ["wss://relay.damus.io", "wss://
|
|
11
|
+
relays: ["wss://relay.damus.io", "wss://purplepag.es/"],
|
|
12
12
|
tagStrategy: "path",
|
|
13
13
|
tagPrefix: "comment:"
|
|
14
14
|
},
|
|
@@ -39,7 +39,8 @@ const module = defineNuxtModule({
|
|
|
39
39
|
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
40
40
|
addImports([
|
|
41
41
|
{ name: "useNuxstr", as: "useNuxstr", from: resolver.resolve("./runtime/composables/useNuxstr") },
|
|
42
|
-
{ name: "
|
|
42
|
+
{ name: "useComments", as: "useComments", from: resolver.resolve("./runtime/composables/useComments") },
|
|
43
|
+
{ name: "useReplies", as: "useReplies", from: resolver.resolve("./runtime/composables/useReplies") }
|
|
43
44
|
]);
|
|
44
45
|
addComponent({
|
|
45
46
|
name: "NuxstrComments",
|
|
@@ -53,6 +54,26 @@ const module = defineNuxtModule({
|
|
|
53
54
|
name: "ScaffoldComment",
|
|
54
55
|
filePath: resolver.resolve("./runtime/components/ScaffoldComment.vue")
|
|
55
56
|
});
|
|
57
|
+
addComponent({
|
|
58
|
+
name: "CommentCommandBar",
|
|
59
|
+
filePath: resolver.resolve("./runtime/components/CommentCommandBar.vue")
|
|
60
|
+
});
|
|
61
|
+
addComponent({
|
|
62
|
+
name: "CommentView",
|
|
63
|
+
filePath: resolver.resolve("./runtime/components/CommentView.vue")
|
|
64
|
+
});
|
|
65
|
+
addComponent({
|
|
66
|
+
name: "CommentAuthor",
|
|
67
|
+
filePath: resolver.resolve("./runtime/components/CommentAuthor.vue")
|
|
68
|
+
});
|
|
69
|
+
addComponent({
|
|
70
|
+
name: "PostReply",
|
|
71
|
+
filePath: resolver.resolve("./runtime/components/PostReply.vue")
|
|
72
|
+
});
|
|
73
|
+
addComponent({
|
|
74
|
+
name: "ReplyView",
|
|
75
|
+
filePath: resolver.resolve("./runtime/components/ReplyView.vue")
|
|
76
|
+
});
|
|
56
77
|
}
|
|
57
78
|
});
|
|
58
79
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
profile: Profile;
|
|
3
|
+
createdAt: number;
|
|
4
|
+
};
|
|
5
|
+
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>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
profile: { type: null, required: true },
|
|
4
|
+
createdAt: { type: Number, required: true }
|
|
5
|
+
});
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<div class="flex items-center gap-3">
|
|
10
|
+
<div
|
|
11
|
+
v-if="props.profile.image"
|
|
12
|
+
class="flex-shrink-0"
|
|
13
|
+
>
|
|
14
|
+
<img
|
|
15
|
+
:src="props.profile.image"
|
|
16
|
+
:alt="props.profile.name || props.profile.display_name || 'User avatar'"
|
|
17
|
+
class="w-6 h-6 rounded-full object-cover"
|
|
18
|
+
>
|
|
19
|
+
</div>
|
|
20
|
+
<div class="flex-1 min-w-0 mb-3">
|
|
21
|
+
<div class="truncate">
|
|
22
|
+
{{ props.profile.display_name || props.profile.name || `${props.profile.pubkey.slice(0, 8)}\u2026` }}
|
|
23
|
+
<span class="text-xs">{{ new Date(props.createdAt * 1e3).toLocaleString() }}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
profile: Profile;
|
|
3
|
+
createdAt: number;
|
|
4
|
+
};
|
|
5
|
+
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>;
|
|
6
|
+
export default _default;
|
|
@@ -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,55 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useNuxstr } from "../composables/useNuxstr";
|
|
3
|
+
import { useReplies } from "../composables/useReplies";
|
|
4
|
+
import { onMounted } from "vue";
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
contentId: { type: String, required: true }
|
|
7
|
+
});
|
|
8
|
+
const { replies, subscribeReplies } = useReplies(props.contentId);
|
|
9
|
+
const { isLoggedIn } = useNuxstr();
|
|
10
|
+
const open = ref(false);
|
|
11
|
+
function toggleReply() {
|
|
12
|
+
open.value = !open.value;
|
|
13
|
+
}
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
subscribeReplies();
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div>
|
|
21
|
+
<div class="flex items-center gap-4 mx-auto mt-4 mb-4">
|
|
22
|
+
<u-chip
|
|
23
|
+
:text="replies.length"
|
|
24
|
+
size="3xl"
|
|
25
|
+
inset
|
|
26
|
+
>
|
|
27
|
+
<u-button
|
|
28
|
+
variant="ghost"
|
|
29
|
+
icon="mdi:message-reply-text-outline"
|
|
30
|
+
title="Reply"
|
|
31
|
+
square
|
|
32
|
+
class="rounded-full hover:bg-gray-900"
|
|
33
|
+
@click="toggleReply"
|
|
34
|
+
/>
|
|
35
|
+
</u-chip>
|
|
36
|
+
</div>
|
|
37
|
+
<UCollapsible
|
|
38
|
+
class="flex flex-col gap-2 w-48 p-16"
|
|
39
|
+
:open
|
|
40
|
+
>
|
|
41
|
+
<template #content>
|
|
42
|
+
<div>
|
|
43
|
+
<reply-view :replies="replies" />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div
|
|
47
|
+
v-if="isLoggedIn"
|
|
48
|
+
class="mt-4"
|
|
49
|
+
>
|
|
50
|
+
<post-reply :root-id="props.contentId" />
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
</UCollapsible>
|
|
54
|
+
</div>
|
|
55
|
+
</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,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
content: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
};
|
|
5
|
+
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>;
|
|
6
|
+
export default _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { marked } from "marked";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
content: { type: String, required: true },
|
|
5
|
+
id: { type: String, required: false }
|
|
6
|
+
});
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<div class="prose prose-sm prose-invert mt-2 mb-2">
|
|
11
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
12
|
+
<div
|
|
13
|
+
class="mb-4"
|
|
14
|
+
v-html="marked.parse(props.content)"
|
|
15
|
+
/>
|
|
16
|
+
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
17
|
+
<comment-command-bar :content-id="props.id" />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
content: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
};
|
|
5
|
+
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>;
|
|
6
|
+
export default _default;
|
|
@@ -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;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { onMounted } from "vue";
|
|
3
3
|
import { useNuxstr } from "../composables/useNuxstr";
|
|
4
|
-
import {
|
|
5
|
-
import { marked } from "marked";
|
|
4
|
+
import { useComments } from "../composables/useComments";
|
|
6
5
|
const props = defineProps({
|
|
7
6
|
contentId: { type: String, required: false }
|
|
8
7
|
});
|
|
9
8
|
const { login, isLoggedIn } = useNuxstr();
|
|
10
|
-
const { comments, subscribeComments, loading } =
|
|
9
|
+
const { comments, subscribeComments, loading } = useComments(props.contentId);
|
|
11
10
|
onMounted(() => {
|
|
12
11
|
subscribeComments();
|
|
13
12
|
});
|
|
@@ -16,9 +15,9 @@ onMounted(() => {
|
|
|
16
15
|
<template>
|
|
17
16
|
<div class="nuxstr-comments space-y-4">
|
|
18
17
|
<div class="flex items-center justify-between">
|
|
19
|
-
<
|
|
18
|
+
<span class="text-lg font-semibold text-primary">
|
|
20
19
|
Comments
|
|
21
|
-
</
|
|
20
|
+
</span>
|
|
22
21
|
|
|
23
22
|
<div
|
|
24
23
|
v-if="!isLoggedIn"
|
|
@@ -30,7 +29,7 @@ onMounted(() => {
|
|
|
30
29
|
leading-icon="game-icons:ostrich"
|
|
31
30
|
@click="login"
|
|
32
31
|
>
|
|
33
|
-
|
|
32
|
+
Sign in
|
|
34
33
|
</UButton>
|
|
35
34
|
</div>
|
|
36
35
|
</div>
|
|
@@ -47,41 +46,30 @@ onMounted(() => {
|
|
|
47
46
|
<div
|
|
48
47
|
v-if="loading"
|
|
49
48
|
>
|
|
50
|
-
<
|
|
49
|
+
<scaffold-comment />
|
|
51
50
|
</div>
|
|
52
51
|
|
|
53
52
|
<div
|
|
54
53
|
v-else
|
|
55
54
|
class="space-y-6"
|
|
56
55
|
>
|
|
56
|
+
<div v-if="comments.length === 0">
|
|
57
|
+
<scaffold-comment />
|
|
58
|
+
</div>
|
|
57
59
|
<div
|
|
58
60
|
v-for="c in comments"
|
|
61
|
+
v-else
|
|
59
62
|
:key="c.id"
|
|
60
|
-
class="rounded border p-3 mt-2 mb-2"
|
|
63
|
+
class="rounded border border-gray-900 p-3 mt-2 mb-2"
|
|
61
64
|
>
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class="w-8 h-8 rounded-full object-cover"
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
<div class="flex-1 min-w-0">
|
|
74
|
-
<div class="truncate">
|
|
75
|
-
{{ c.profile?.display_name || c.profile?.name || `${c.pubkey.slice(0, 8)}\u2026` }}
|
|
76
|
-
<span class="text-xs">{{ new Date(c.created_at * 1e3).toLocaleString() }}</span>
|
|
77
|
-
</div>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
<div class="prose prose-sm prose-invert mt-2 mb-2">
|
|
81
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
82
|
-
<div v-html="marked.parse(c.content)" />
|
|
83
|
-
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
84
|
-
</div>
|
|
65
|
+
<comment-author
|
|
66
|
+
:profile="c.profile"
|
|
67
|
+
:created-at="c.created_at"
|
|
68
|
+
/>
|
|
69
|
+
<comment-view
|
|
70
|
+
:id="c.id"
|
|
71
|
+
:content="c.content"
|
|
72
|
+
/>
|
|
85
73
|
</div>
|
|
86
74
|
|
|
87
75
|
<div
|
|
@@ -91,9 +79,6 @@ onMounted(() => {
|
|
|
91
79
|
</div>
|
|
92
80
|
</div>
|
|
93
81
|
</ClientOnly>
|
|
82
|
+
<client-only />
|
|
94
83
|
</div>
|
|
95
84
|
</template>
|
|
96
|
-
|
|
97
|
-
<style scoped>
|
|
98
|
-
.nuxstr-comments :deep(pre){white-space:pre-wrap}
|
|
99
|
-
</style>
|
|
@@ -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;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from "vue";
|
|
3
|
-
import {
|
|
3
|
+
import { useComments } from "../composables/useComments";
|
|
4
4
|
const props = defineProps({
|
|
5
5
|
contentId: { type: String, required: false }
|
|
6
6
|
});
|
|
7
7
|
const EMPTY_COMMENT = "";
|
|
8
|
-
const { postComment } =
|
|
8
|
+
const { postComment } = useComments(props.contentId);
|
|
9
9
|
const comment = ref(EMPTY_COMMENT);
|
|
10
10
|
function isValidComment(commentText) {
|
|
11
11
|
return commentText.trim().length > 0;
|
|
@@ -23,22 +23,27 @@ async function handlePost() {
|
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
25
|
<template>
|
|
26
|
-
<div class="text-sm text-muted-foreground border border-green mt-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
/>
|
|
46
|
+
</div>
|
|
42
47
|
</div>
|
|
43
48
|
</div>
|
|
44
49
|
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
rootId: 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,47 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { useReplies } from "../composables/useReplies";
|
|
3
|
+
const props = defineProps({
|
|
4
|
+
rootId: { type: String, required: true }
|
|
5
|
+
});
|
|
6
|
+
const { reply } = useReplies(props.rootId);
|
|
7
|
+
const EMPTY_COMMENT = "";
|
|
8
|
+
const content = ref(EMPTY_COMMENT);
|
|
9
|
+
function isValidComment(commentText) {
|
|
10
|
+
return commentText.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
function clearComment() {
|
|
13
|
+
content.value = EMPTY_COMMENT;
|
|
14
|
+
}
|
|
15
|
+
async function postReply(comment) {
|
|
16
|
+
if (!isValidComment(comment)) return;
|
|
17
|
+
const wasPosted = await reply(comment);
|
|
18
|
+
if (wasPosted) {
|
|
19
|
+
clearComment();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<div class="text-sm text-muted-foreground mt-16 p-6">
|
|
26
|
+
<div class="flex gap-2">
|
|
27
|
+
<div class="flex-1">
|
|
28
|
+
<UTextarea
|
|
29
|
+
v-model="content"
|
|
30
|
+
class="w-full mb-4 rounded-xl"
|
|
31
|
+
placeholder="Write a reply to this comment ...."
|
|
32
|
+
:rows="4"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="flex flex-col justify-center items-center p-2">
|
|
36
|
+
<UButton
|
|
37
|
+
icon="mingcute:send-line"
|
|
38
|
+
size="xl"
|
|
39
|
+
color="primary"
|
|
40
|
+
variant="solid"
|
|
41
|
+
class="mb-4 mr-2"
|
|
42
|
+
@click="postReply(content)"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
rootId: 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,5 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
replies: Comment[];
|
|
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,23 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
replies: { type: Array, required: true }
|
|
4
|
+
});
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<div class="px-10 py-4">
|
|
9
|
+
<div
|
|
10
|
+
v-for="reply in props.replies"
|
|
11
|
+
:key="reply.id"
|
|
12
|
+
class="rounded border border-gray-900 p-3 mt-2 mb-2"
|
|
13
|
+
>
|
|
14
|
+
<div>
|
|
15
|
+
<comment-author
|
|
16
|
+
:profile="reply.profile"
|
|
17
|
+
:created-at="reply.created_at"
|
|
18
|
+
/>
|
|
19
|
+
<p>{{ reply.content }}</p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
replies: Comment[];
|
|
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,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;
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<template>
|
|
6
|
-
<div
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<
|
|
6
|
+
<div>
|
|
7
|
+
<p class="text-xs">
|
|
8
|
+
No comments available
|
|
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>
|
|
14
|
+
<div class="mt-3">
|
|
15
|
+
<USkeleton class="h-4" />
|
|
16
|
+
</div>
|
|
12
17
|
</div>
|
|
13
18
|
</div>
|
|
14
19
|
</template>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Comment } from '~/src/runtime/types';
|
|
2
|
-
export declare function
|
|
2
|
+
export declare function useComments(customContentId?: string): {
|
|
3
3
|
loading: import("vue").Ref<boolean, boolean>;
|
|
4
4
|
error: import("vue").Ref<string | null, string | null>;
|
|
5
5
|
comments: import("vue").Ref<{
|
|
@@ -2,8 +2,8 @@ import { computed, ref } from "vue";
|
|
|
2
2
|
import { useRoute, useRuntimeConfig, useRequestURL } from "#imports";
|
|
3
3
|
import { useNuxstr } from "./useNuxstr.js";
|
|
4
4
|
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
|
5
|
-
export function
|
|
6
|
-
const { ndk, connect, isLoggedIn,
|
|
5
|
+
export function useComments(customContentId) {
|
|
6
|
+
const { ndk, connect, isLoggedIn, mapComment, pubkey, fetchProfile } = useNuxstr();
|
|
7
7
|
const route = useRoute();
|
|
8
8
|
const config = useRuntimeConfig();
|
|
9
9
|
const opts = config.public?.nuxstrComments || {};
|
|
@@ -22,19 +22,12 @@ export function useNuxstrComments(customContentId) {
|
|
|
22
22
|
const url = useRequestURL();
|
|
23
23
|
return `${url.protocol}//${url.host}`;
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const user = ndk.getUser({ pubkey });
|
|
28
|
-
const profile = await user.fetchProfile();
|
|
29
|
-
return mapProfile(profile);
|
|
30
|
-
} catch (error2) {
|
|
31
|
-
console.error("Failed to fetch profile for", pubkey, error2);
|
|
32
|
-
return void 0;
|
|
33
|
-
}
|
|
25
|
+
function fullUrl(path) {
|
|
26
|
+
return `${siteUrl()}${path}`;
|
|
34
27
|
}
|
|
35
28
|
async function subscribeComments() {
|
|
36
29
|
await connect();
|
|
37
|
-
const filter = { kinds: [NDKKind.GenericReply], ["#t"]: [tagValue()], limit: 100, ["#k"]: [
|
|
30
|
+
const filter = { kinds: [NDKKind.GenericReply], ["#t"]: [tagValue()], limit: 100, ["#k"]: ["web"], ["#A"]: [fullUrl(contentId.value)] };
|
|
38
31
|
const sub = await ndk.subscribe(filter);
|
|
39
32
|
sub.on("event", async (event) => {
|
|
40
33
|
const comment = mapComment(event);
|
|
@@ -44,17 +37,30 @@ export function useNuxstrComments(customContentId) {
|
|
|
44
37
|
}
|
|
45
38
|
async function postComment(comment) {
|
|
46
39
|
await connect();
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
e.content = comment;
|
|
50
|
-
e.tags = [
|
|
51
|
-
["t", tagValue()],
|
|
52
|
-
["k", siteUrl()]
|
|
53
|
-
];
|
|
54
|
-
return await e.publish().then(() => true).catch((err) => {
|
|
40
|
+
const ndkEvent = await createCommentEvent(comment);
|
|
41
|
+
return await ndkEvent.publish().then(() => true).catch((err) => {
|
|
55
42
|
error.value = err?.message || String(err);
|
|
56
43
|
return false;
|
|
57
44
|
});
|
|
58
45
|
}
|
|
46
|
+
async function createCommentEvent(comment) {
|
|
47
|
+
const event = new NDKEvent(ndk);
|
|
48
|
+
event.kind = NDKKind.GenericReply;
|
|
49
|
+
event.content = comment;
|
|
50
|
+
event.tags = [
|
|
51
|
+
["A", fullUrl(contentId.value)],
|
|
52
|
+
["a", fullUrl(contentId.value)],
|
|
53
|
+
["I", fullUrl(contentId.value)],
|
|
54
|
+
//
|
|
55
|
+
["i", fullUrl(contentId.value)],
|
|
56
|
+
["t", tagValue()],
|
|
57
|
+
["k", "web"],
|
|
58
|
+
// Defined NIP 73
|
|
59
|
+
["K", "web"],
|
|
60
|
+
// Defined NIP 73,
|
|
61
|
+
["p", pubkey.value]
|
|
62
|
+
];
|
|
63
|
+
return event;
|
|
64
|
+
}
|
|
59
65
|
return { loading, error, comments, isLoggedIn, subscribeComments, postComment };
|
|
60
66
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import NDK, { type NDKEvent } from '@nostr-dev-kit/ndk';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Comment, Profile } from '../types/index.js';
|
|
3
3
|
export declare function useNuxstr(): {
|
|
4
4
|
readonly ndk: NDK;
|
|
5
5
|
connect: () => Promise<NDK>;
|
|
@@ -9,4 +9,5 @@ export declare function useNuxstr(): {
|
|
|
9
9
|
pubkey: import("vue").Ref<string | null | undefined, string | null | undefined>;
|
|
10
10
|
mapProfile: (profile: NDKUserProfile) => Profile;
|
|
11
11
|
mapComment: (event: NDKEvent) => Comment;
|
|
12
|
+
fetchProfile: (pubkey: string) => Promise<Profile | undefined>;
|
|
12
13
|
};
|
|
@@ -19,7 +19,9 @@ export function useNuxstr() {
|
|
|
19
19
|
const opts = config.public?.nuxstrComments || {};
|
|
20
20
|
function initializeNDK() {
|
|
21
21
|
if (!state.ndk) {
|
|
22
|
-
state.ndk = new NDK({
|
|
22
|
+
state.ndk = new NDK({
|
|
23
|
+
explicitRelayUrls: opts.relays || []
|
|
24
|
+
});
|
|
23
25
|
}
|
|
24
26
|
return state.ndk;
|
|
25
27
|
}
|
|
@@ -56,12 +58,8 @@ export function useNuxstr() {
|
|
|
56
58
|
state.signer = signer;
|
|
57
59
|
state.pubkey.value = user.pubkey;
|
|
58
60
|
await connect();
|
|
59
|
-
const profile = ndk.getUser({ pubkey: user.pubkey });
|
|
60
|
-
|
|
61
|
-
state.userProile = mapProfile(profile2);
|
|
62
|
-
}).catch((err) => {
|
|
63
|
-
console.error("Failed to fetch profile", err);
|
|
64
|
-
});
|
|
61
|
+
const profile = await ndk.getUser({ pubkey: user.pubkey });
|
|
62
|
+
state.userProfile.value = mapProfile(profile);
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
function mapComment(event) {
|
|
@@ -73,6 +71,16 @@ export function useNuxstr() {
|
|
|
73
71
|
profile: null
|
|
74
72
|
};
|
|
75
73
|
}
|
|
74
|
+
async function fetchProfile(pubkey) {
|
|
75
|
+
try {
|
|
76
|
+
const user = state.ndk.getUser({ pubkey });
|
|
77
|
+
const profile = await user.fetchProfile();
|
|
78
|
+
return mapProfile(profile);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Failed to fetch profile for", pubkey, error);
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
76
84
|
function mapProfile(profile) {
|
|
77
85
|
return {
|
|
78
86
|
display_name: profile.displayName,
|
|
@@ -98,6 +106,7 @@ export function useNuxstr() {
|
|
|
98
106
|
isLoggedIn,
|
|
99
107
|
pubkey: state.pubkey,
|
|
100
108
|
mapProfile,
|
|
101
|
-
mapComment
|
|
109
|
+
mapComment,
|
|
110
|
+
fetchProfile
|
|
102
111
|
};
|
|
103
112
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function useReplies(rootCommentId?: string): {
|
|
2
|
+
subscribeReplies: () => Promise<void>;
|
|
3
|
+
replies: import("vue").ComputedRef<{
|
|
4
|
+
id: string;
|
|
5
|
+
pubkey: string;
|
|
6
|
+
created_at: number;
|
|
7
|
+
content: string;
|
|
8
|
+
profile?: {
|
|
9
|
+
pubkey: string;
|
|
10
|
+
display_name?: string | undefined;
|
|
11
|
+
about?: string | undefined;
|
|
12
|
+
image?: string | undefined;
|
|
13
|
+
nip05?: string | undefined;
|
|
14
|
+
lud06?: string | undefined;
|
|
15
|
+
lud16?: string | undefined;
|
|
16
|
+
website?: string | undefined;
|
|
17
|
+
} | undefined;
|
|
18
|
+
}[]>;
|
|
19
|
+
reply: (comment: string) => Promise<boolean>;
|
|
20
|
+
count: import("vue").ComputedRef<Promise<string>>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
|
+
import { useNuxstr } from "./useNuxstr.js";
|
|
3
|
+
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
|
4
|
+
export function useReplies(rootCommentId) {
|
|
5
|
+
const { ndk, connect, mapComment, pubkey, fetchProfile } = useNuxstr();
|
|
6
|
+
const repliesData = ref([]);
|
|
7
|
+
const error = ref(null);
|
|
8
|
+
const replies = computed(() => {
|
|
9
|
+
return repliesData.value.slice().sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
10
|
+
});
|
|
11
|
+
async function subscribeReplies() {
|
|
12
|
+
await connect();
|
|
13
|
+
const filter = { kinds: [NDKKind.GenericReply], limit: 100, ["#e"]: [rootCommentId] };
|
|
14
|
+
const sub = await ndk.subscribe(filter);
|
|
15
|
+
sub.on("event", async (event) => {
|
|
16
|
+
const reply2 = mapComment(event);
|
|
17
|
+
reply2.profile = await fetchProfile(event.pubkey);
|
|
18
|
+
repliesData.value.push(reply2);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function reply(comment) {
|
|
22
|
+
const ndkEvent = await createReplyEvent(comment);
|
|
23
|
+
return await ndkEvent.publish().then(() => true).catch((err) => {
|
|
24
|
+
error.value = err?.message || String(err);
|
|
25
|
+
return false;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
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.value]
|
|
37
|
+
];
|
|
38
|
+
return event;
|
|
39
|
+
}
|
|
40
|
+
const count = computed(async () => {
|
|
41
|
+
await connect();
|
|
42
|
+
const filter = { kinds: [NDKKind.GenericReply], limit: 100, ["#e"]: [rootCommentId] };
|
|
43
|
+
const events = await ndk.fetchEvents(filter);
|
|
44
|
+
console.log(events);
|
|
45
|
+
return `${Array.from(events).length}`;
|
|
46
|
+
});
|
|
47
|
+
return { subscribeReplies, replies, reply, count };
|
|
48
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@threenine/nuxstr-comments",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "Nuxt module to enable Nostr Comments on Nuxt 4 based websites",
|
|
5
5
|
"repository": "threenine/nuxstr-comments",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"@nuxt/eslint": "1.9.0",
|
|
34
34
|
"@nuxt/eslint-config": "^1.9.0",
|
|
35
35
|
"@nuxt/module-builder": "^1.0.2",
|
|
36
|
-
"@nuxt/schema": "^4.
|
|
36
|
+
"@nuxt/schema": "^4.1.1",
|
|
37
37
|
"@nuxt/scripts": "0.11.10",
|
|
38
38
|
"@nuxt/test-utils": "^3.19.2",
|
|
39
39
|
"@nuxt/ui": "^3.3.3",
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"@testing-library/vue": "^8.1.0",
|
|
42
42
|
"@unhead/vue": "^2.0.14",
|
|
43
43
|
"changelogen": "^0.6.2",
|
|
44
|
-
"eslint": "^9.
|
|
44
|
+
"eslint": "^9.35.0",
|
|
45
45
|
"jsdom": "^26.1.0",
|
|
46
|
-
"nuxt": "^4.
|
|
46
|
+
"nuxt": "^4.1.2",
|
|
47
47
|
"typescript": "~5.9.2",
|
|
48
48
|
"vitest": "^3.2.4",
|
|
49
49
|
"vue-tsc": "^3.0.6",
|