@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 +28 -3
- package/dist/module.json +1 -1
- package/dist/module.mjs +5 -5
- package/dist/runtime/classes/NostrManager.d.ts +12 -0
- package/dist/runtime/classes/NostrManager.js +40 -0
- package/dist/runtime/components/CommentAuthor.d.vue.ts +2 -1
- package/dist/runtime/components/CommentAuthor.vue +7 -4
- package/dist/runtime/components/CommentAuthor.vue.d.ts +2 -1
- package/dist/runtime/components/CommentView.vue +18 -13
- package/dist/runtime/components/NuxstrComments.vue +15 -11
- package/dist/runtime/components/PostComment.vue +29 -22
- package/dist/runtime/components/PostReply.vue +22 -15
- package/dist/runtime/components/ReplyView.d.vue.ts +1 -0
- package/dist/runtime/components/ReplyView.vue +5 -3
- package/dist/runtime/components/ReplyView.vue.d.ts +1 -0
- package/dist/runtime/components/ScaffoldComment.vue +7 -5
- package/dist/runtime/composables/useComments.js +40 -33
- package/dist/runtime/composables/useNostr.d.ts +8 -0
- package/dist/runtime/composables/useNostr.js +24 -0
- package/dist/runtime/composables/useNuxstr.d.ts +2 -6
- package/dist/runtime/composables/useNuxstr.js +30 -63
- package/dist/runtime/composables/useReplies.js +33 -20
- package/dist/runtime/types/index.d.ts +26 -0
- package/package.json +3 -4
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
|
-
-
|
|
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
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.
|
|
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", "
|
|
20
|
+
config.optimizeDeps.include.push("tseep", "nostr-tools", "defu");
|
|
21
21
|
config.ssr = config.ssr || {};
|
|
22
|
-
const packagesToInclude = ["tseep", "
|
|
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", "
|
|
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", "
|
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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 ||
|
|
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"> </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
|
|
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
|
-
|
|
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
|
-
<
|
|
21
|
-
|
|
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
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
<
|
|
61
|
+
<UCard
|
|
60
62
|
v-for="c in comments"
|
|
61
63
|
v-else
|
|
62
64
|
:key="c.id"
|
|
63
|
-
|
|
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
|
-
</
|
|
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
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
</
|
|
48
|
-
</
|
|
54
|
+
</form>
|
|
55
|
+
</UCard>
|
|
49
56
|
</template>
|
|
@@ -23,25 +23,32 @@ async function postReply(comment) {
|
|
|
23
23
|
</script>
|
|
24
24
|
|
|
25
25
|
<template>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
size="xl"
|
|
45
|
+
type="submit"
|
|
40
46
|
color="primary"
|
|
41
|
-
|
|
47
|
+
label="Reply"
|
|
48
|
+
icon="i-lucide-send"
|
|
42
49
|
@click="postReply(content)"
|
|
43
50
|
/>
|
|
44
51
|
</div>
|
|
45
52
|
</div>
|
|
46
|
-
</
|
|
53
|
+
</UCard>
|
|
47
54
|
</template>
|
|
@@ -6,10 +6,12 @@ const props = defineProps({
|
|
|
6
6
|
|
|
7
7
|
<template>
|
|
8
8
|
<div class="px-10 py-4">
|
|
9
|
-
<
|
|
9
|
+
<UCard
|
|
10
10
|
v-for="reply in props.replies"
|
|
11
11
|
:key="reply.id"
|
|
12
|
-
|
|
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
|
-
</
|
|
25
|
+
</UCard>
|
|
24
26
|
</div>
|
|
25
27
|
</template>
|
|
@@ -7,14 +7,16 @@
|
|
|
7
7
|
<p class="text-xs">
|
|
8
8
|
No comments available
|
|
9
9
|
</p>
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
</
|
|
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 {
|
|
4
|
+
import { useNostr } from "./useNostr.js";
|
|
5
5
|
function useComments(customContentId) {
|
|
6
|
-
const {
|
|
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
|
-
|
|
26
|
-
|
|
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: [
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
const comment =
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
[
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
12
|
+
userProfile: ref(null)
|
|
16
13
|
};
|
|
17
14
|
}
|
|
18
15
|
const state = w.__nuxstr;
|
|
19
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
3
|
+
import { useNostr } from "./useNostr.js";
|
|
4
4
|
function useReplies(rootCommentId) {
|
|
5
|
-
const {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
[
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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",
|