Add the reply functionality
This commit is contained in:
parent
994e76313e
commit
7c6bded69e
2 changed files with 96 additions and 12 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
import { SimplePool } from "@nostr/tools";
|
import { SimplePool } from "@nostr/tools";
|
||||||
import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata";
|
import { loadNostrUser, type NostrUser } from "@nostr/gadgets/metadata";
|
||||||
import { RELAY_URL } from "$lib/config";
|
import { RELAY_URL, GROUP_ID } from "$lib/config";
|
||||||
import { threads as mockThreads } from "$lib/mock";
|
import { threads as mockThreads } from "$lib/mock";
|
||||||
|
|
||||||
const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id);
|
const isNostrId = (id: string) => /^[0-9a-f]{64}$/.test(id);
|
||||||
|
|
@ -95,3 +95,51 @@ export async function loadThread(id: string) {
|
||||||
pool.close([RELAY_URL]);
|
pool.close([RELAY_URL]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sendReply(content: string, ownPubkey: string) {
|
||||||
|
if (!detail) throw new Error("No thread loaded");
|
||||||
|
if (!window.nostr) throw new Error("No Nostr extension found");
|
||||||
|
|
||||||
|
// Use last 3 events not authored by us as previous refs (NIP-29)
|
||||||
|
const previousRefs = [...detail.replies, detail.op]
|
||||||
|
.filter((p) => p.pubkey !== ownPubkey)
|
||||||
|
.slice(-3)
|
||||||
|
.map((p) => p.id.slice(0, 8));
|
||||||
|
|
||||||
|
const tags: string[][] = [
|
||||||
|
["h", GROUP_ID],
|
||||||
|
["K", "11"],
|
||||||
|
["E", detail.id, RELAY_URL, detail.op.pubkey],
|
||||||
|
];
|
||||||
|
if (previousRefs.length > 0) tags.push(["previous", ...previousRefs]);
|
||||||
|
|
||||||
|
console.log("[reply] signing event…");
|
||||||
|
const signed = await window.nostr.signEvent({
|
||||||
|
kind: 1111,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
tags,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
console.log("[reply] event signed:", signed.id);
|
||||||
|
|
||||||
|
console.log("[reply] publishing…");
|
||||||
|
const pool = new SimplePool();
|
||||||
|
try {
|
||||||
|
const timeout = new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error("Relay did not respond in time")), 8000)
|
||||||
|
);
|
||||||
|
await Promise.race([Promise.all(pool.publish([RELAY_URL], signed)), timeout]);
|
||||||
|
} finally {
|
||||||
|
pool.destroy();
|
||||||
|
}
|
||||||
|
console.log("[reply] published");
|
||||||
|
|
||||||
|
const newReply: PostData = {
|
||||||
|
id: signed.id,
|
||||||
|
pubkey: signed.pubkey,
|
||||||
|
createdAt: signed.created_at,
|
||||||
|
content: signed.content,
|
||||||
|
};
|
||||||
|
detail = { ...detail, replies: [...detail.replies, newReply] };
|
||||||
|
loadProfile(signed.pubkey);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
import { threadDetailStore, loadThread, type PostData } from "$lib/thread.svelte";
|
import { threadDetailStore, loadThread, sendReply, type PostData } from "$lib/thread.svelte";
|
||||||
|
import { auth, login } from "$lib/auth.svelte";
|
||||||
import Reactions from "$lib/components/Reactions.svelte";
|
import Reactions from "$lib/components/Reactions.svelte";
|
||||||
import ThreadScrubber from "$lib/components/ThreadScrubber.svelte";
|
import ThreadScrubber from "$lib/components/ThreadScrubber.svelte";
|
||||||
import type { NostrUser } from "@nostr/gadgets/metadata";
|
import type { NostrUser } from "@nostr/gadgets/metadata";
|
||||||
|
|
@ -28,6 +29,23 @@
|
||||||
let opEl = $state<HTMLElement | null>(null);
|
let opEl = $state<HTMLElement | null>(null);
|
||||||
let replyEls = $state<(HTMLElement | null)[]>([]);
|
let replyEls = $state<(HTMLElement | null)[]>([]);
|
||||||
let isScrolled = $state(false);
|
let isScrolled = $state(false);
|
||||||
|
let replyContent = $state("");
|
||||||
|
let replying = $state(false);
|
||||||
|
let replyError = $state<string | null>(null);
|
||||||
|
|
||||||
|
async function submitReply() {
|
||||||
|
if (!auth.user || !replyContent.trim()) return;
|
||||||
|
replying = true;
|
||||||
|
replyError = null;
|
||||||
|
try {
|
||||||
|
await sendReply(replyContent.trim(), auth.user!.pubkey);
|
||||||
|
replyContent = "";
|
||||||
|
} catch (e) {
|
||||||
|
replyError = e instanceof Error ? e.message : "Failed to post reply";
|
||||||
|
} finally {
|
||||||
|
replying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
let opTopOffset = $state(0);
|
let opTopOffset = $state(0);
|
||||||
|
|
||||||
const detail = $derived(threadDetailStore.detail);
|
const detail = $derived(threadDetailStore.detail);
|
||||||
|
|
@ -132,16 +150,34 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-8 border-t border-gray-200 pt-6">
|
<div class="mt-8 border-t border-gray-200 pt-6">
|
||||||
<textarea
|
{#if auth.user}
|
||||||
rows="4"
|
{#if replyError}
|
||||||
placeholder="Write a reply..."
|
<div class="mb-4 rounded bg-red-50 px-4 py-3 text-sm text-red-700 border border-red-200">
|
||||||
class="w-full rounded border border-gray-200 px-3 py-2 focus:outline-none focus:ring-1 focus:ring-brand"
|
{replyError}
|
||||||
></textarea>
|
</div>
|
||||||
<div class="mt-2 flex justify-end">
|
{/if}
|
||||||
<button class="rounded bg-brand px-6 py-1.5 font-medium text-white hover:bg-brand-hover">
|
<textarea
|
||||||
Reply
|
rows="4"
|
||||||
</button>
|
placeholder="Write a reply..."
|
||||||
</div>
|
bind:value={replyContent}
|
||||||
|
disabled={replying}
|
||||||
|
class="w-full rounded border border-gray-200 px-3 py-2 focus:outline-none focus:ring-1 focus:ring-brand disabled:opacity-50"
|
||||||
|
></textarea>
|
||||||
|
<div class="mt-2 flex justify-end">
|
||||||
|
<button
|
||||||
|
onclick={submitReply}
|
||||||
|
disabled={replying || !replyContent.trim()}
|
||||||
|
class="rounded bg-brand px-6 py-1.5 font-medium text-white hover:bg-brand-hover disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{replying ? "Posting…" : "Reply"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
To participate and reply,
|
||||||
|
<button onclick={login} class="text-brand hover:underline">login now</button>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue