Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# Changelog


## v1.5.3

[compare changes](https://github.com/threenine/nuxstr-comments/compare/v1.5.2...v1.5.3)

### 🏡 Chore

- **runtime & dependencies:** Fix padding typo, enhance UI, and update dependencies ([2098aca](https://github.com/threenine/nuxstr-comments/commit/2098aca))
- **dependencies:** Update `@nuxt/ui` to `^4.2.1` ([4b90159](https://github.com/threenine/nuxstr-comments/commit/4b90159))

### ❤️ Contributors

- Gary Woodfine <lnb0l9dc@duck.com>

## v1.5.2

[compare changes](https://github.com/threenine/nuxstr-comments/compare/v1.5.1...v1.5.2)
Expand Down
31 changes: 28 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,37 @@ Enable [nostr protocol](https://nostr.com/) based comment system on your Nuxt 4
## Features

- Nostr-powered comments for Nuxt Content blog posts
- NIP-07 login prompt if user is not authenticated
- Comments are written in Markdown and rendered via @nuxt/content's ContentRendererMarkdown
- [NIP-07](https://github.com/nostr-protocol/nips/blob/master/07.md) Browser Extension login prompt if user is not authenticated
- [NIP-22](https://github.com/nostr-protocol/nips/blob/master/22.md) Plain Text Content - (no HTML, Markdown, or other formatting)
- Configurable relay list and tagging strategy
- Comments are published as kind:1111 as Website Url
```json
{
"kind": 1111,
"content": "Nice article!",
"tags": [
// referencing the root url
["I", "https://abc.com/articles/1"],
// the root "kind": for an url
["K", "web"],

// the parent reference (same as root for top-level comments)
["i", "https://abc.com/articles/1"],
// the parent "kind": for an url
["k", "web"]
]
// other fields
}
```

## Quick Setup

> [!WARNING]
> NuxstrComments [NIP-22] MUST NOT be used to reply to kind 1 notes.
> NIP-10 should instead be followed.


## Quick Setup

Install the module to your Nuxt application with one command:

```bash
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@threenine/nuxstr-comments",
"version": "1.5.2",
"version": "1.5.3",
"description": "Nuxt module to enable Nostr Comments on Nuxt 4 based websites",
"repository": "threenine/nuxstr-comments",
"license": "MIT",
Expand Down Expand Up @@ -35,8 +35,7 @@
},
"dependencies": {
"@nostr-dev-kit/ndk": "^2.18.1",
"defu": "^6.1.4",
"marked": "^16.2.1"
"defu": "^6.1.4"
},
"devDependencies": {
"@nuxt/devtools": "^2.6.2",
Expand All @@ -47,7 +46,7 @@
"@nuxt/schema": "^4.2.1",
"@nuxt/scripts": "0.11.10",
"@nuxt/test-utils": "^3.19.2",
"@nuxt/ui": "^4.2.1",
"@nuxt/ui": "^4.3.0",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/vue": "^8.1.0",
"@types/node": "latest",
Expand Down
2 changes: 1 addition & 1 deletion playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ export default defineNuxtConfig({
css: ['~/assets/css/main.css'],
compatibilityDate: '2025-08-19',
nuxstrComments: {
relays: ['wss://frens.nostr1.com', 'wss://purplerelay.com', 'wss://a.nos.lol', 'wss://freelay.sovbit.host', 'wss://nos.lol'],
relays: ['wss://relay.threenine.services']
},
})
35 changes: 18 additions & 17 deletions src/runtime/components/CommentView.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { marked } from 'marked'

const props = defineProps<{ content: string, id: string }>()

async function renderMarkdown(md: string): Promise<string> {
// Marked.parse returns string; keep narrow surface for future changes (e.g., sanitization).
return marked.parse(md)
}

onMounted(async () => {
const targetEl = document.getElementById('comment-content')
if (!targetEl) return
targetEl.innerHTML = await renderMarkdown(props.content)
})
defineProps<{ content: string, id: string }>()
</script>

<template>
<div class="mt-2 mb-2">
<div id="comment-content" />
<ReplyButton :content-id="props.id" />
<UCard
variant="subtle"
class="mt-auto"
:ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
>
<UTextarea
:model-value="content"
color="neutral"
variant="none"
autoresize
readonly
:rows="4"
class="w-full"
:ui="{ base: 'p-0 resize-none' }"
/>
</ucard>
<ReplyButton :content-id="id" />
</div>
</template>

Expand Down
26 changes: 15 additions & 11 deletions src/runtime/components/NuxstrComments.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ onMounted(() => {
v-if="!isLoggedIn"
class="text-sm text-muted-foreground"
>
<UButton
color="primary"
variant="solid"
leading-icon="game-icons:ostrich"
@click="login"
>
Sign in
</UButton>
<u-tooltip text="Sign in with NIP07 browser extension like Alby or nos2fx to comment">
<UButton
color="primary"
variant="solid"
leading-icon="game-icons:ostrich"
@click="login"
>
Sign in
</UButton>
</u-tooltip>
</div>
</div>
<ClientOnly>
Expand All @@ -56,11 +58,13 @@ onMounted(() => {
<div v-if="comments.length === 0">
<scaffold-comment />
</div>
<div
<UCard
v-for="c in comments"
v-else
:key="c.id"
class="rounded border border-gray-900 p-3 mt-2 mb-2"
variant="subtle"
class="mt-auto"
:ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
>
<comment-author
:profile="c.profile"
Expand All @@ -70,7 +74,7 @@ onMounted(() => {
:id="c.id"
:content="c.content"
/>
</div>
</UCard>

<div
v-if="isLoggedIn"
Expand Down
51 changes: 29 additions & 22 deletions src/runtime/components/PostComment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,36 @@ async function handlePost() {
</script>

<template>
<div class="text-sm text-muted-foreground border border-green mt-4 p-6">
<div class="flex gap-2">
<div class="flex-1">
<UTextarea
v-model="comment"
class="w-full mb-4"
placeholder="Write a comment ...."
:rows="4"
/>
</div>
<div class="flex flex-col justify-center items-center p-2">
<UButton
icon="mingcute:send-line"
color="primary"
variant="solid"
:disabled="!comment.trim()"
class=""
size="xl"
@click="handlePost"
/>
<UCard
variant="subtle"
class="mt-auto"
:ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
>
<form @submit.prevent="handlePost">
<UTextarea
v-model="comment"
color="neutral"
variant="none"
required
autoresize
placeholder="Write your comment..."
:rows="4"
class="w-full"
:ui="{ base: 'p-0 resize-none' }"
/>

<div class="flex items-center justify-end">
<div class="flex items-center justify-end gap-2">
<UButton
type="submit"
color="primary"
label="Comment"
icon="i-lucide-send"
/>
</div>
</div>
</div>
</div>
</form>
</UCard>
</template>

<style scoped>
Expand Down
37 changes: 22 additions & 15 deletions src/runtime/components/PostReply.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,34 @@ async function postReply(comment: string) {
</script>

<template>
<div class="text-sm text-muted-foreground mt-4 p-6">
<div class="flex gap-2">
<div class="flex-1">
<UTextarea
v-model="content"
class="w-full mb-4 rounded-xl"
placeholder="Write a reply to this comment ...."
:rows="4"
/>
</div>
<div class="flex flex-col justify-center items-center p-2">
<UCard
variant="subtle"
class="mt-auto"
:ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
>
<UTextarea
v-model="content"
color="neutral"
variant="none"
required
autoresize
placeholder="Write your comment..."
:rows="4"
class="w-full"
:ui="{ base: 'p-0 resize-none' }"
/>
<div class="flex items-center justify-end">
<div class="flex items-center justify-end gap-2">
<UButton
icon="mingcute:send-line"
size="xl"
type="submit"
color="primary"
variant="solid"
label="Reply"
icon="i-lucide-send"
@click="postReply(content)"
/>
</div>
</div>
</div>
</UCard>
</template>

<style scoped>
Expand Down
8 changes: 5 additions & 3 deletions src/runtime/components/ReplyView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ const props = defineProps<{ replies: Comment[] }>()

<template>
<div class="px-10 py-4">
<div
<UCard
v-for="reply in props.replies"
:key="reply.id"
class="rounded-md border p-3 mt-2 mb-2"
variant="subtle"
class="mt-auto mb-3"
:ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
>
<div>
<comment-author
Expand All @@ -18,7 +20,7 @@ const props = defineProps<{ replies: Comment[] }>()
{{ reply.content }}
</p>
</div>
</div>
</UCard>
</div>
</template>

Expand Down
12 changes: 7 additions & 5 deletions src/runtime/components/ScaffoldComment.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
<p class="text-xs">
No comments available
</p>
<div class="rounded border p-3 mt-2 mb-2">
<div class="flex gap-2 mb-3 items-center">
<span><USkeleton class="h-4 w-5 rounded-full" /></span><USkeleton class="h-4" />
</div>
<UCard
variant="subtle"
class="mt-auto"
:ui="{ header: 'flex items-center gap-1.5 text-dimmed' }"
>
<span><USkeleton class="h-4 w-5 rounded-full" /></span><USkeleton class="h-4" />
<div class="mt-3">
<USkeleton class="h-4" />
</div>
</div>
</UCard>
</div>
</template>

Expand Down
10 changes: 7 additions & 3 deletions src/runtime/composables/useNuxstr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,13 @@ function useNuxstr() {
async function fetchProfile(pubkey: string): Promise<Profile | undefined> {
try {
const ndk = initializeNDK()
const user = ndk.getUser({ pubkey: pubkey })
const profile = await user.fetchProfile()
return mapProfile(profile)
const user = await ndk.fetchUser(pubkey)
if (user !== null) {
const profile = await user?.fetchProfile()
if (!profile) return undefined
return mapProfile(profile)
}
return undefined
}
catch (error) {
console.error('Failed to fetch profile for', pubkey, error)
Expand Down