This commit is contained in:
@s.roertgen 2025-04-30 22:39:25 +02:00
parent 34781e8c29
commit 1878d82fad
7 changed files with 218 additions and 252 deletions

29
package-lock.json generated
View file

@ -774,37 +774,16 @@
}
},
"node_modules/@nostr-dev-kit/ndk-svelte": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-svelte/-/ndk-svelte-2.4.10.tgz",
"integrity": "sha512-meBNrgcVXqM/VFPO8LykhiSncMbqvfMWqbtRF4SOsLwDMbK7IigRKtuUUJoHvalEn+3OB2TZYp/FV9EIMHQ00w==",
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-svelte/-/ndk-svelte-2.4.11.tgz",
"integrity": "sha512-HCHFVQJ0lJBaGmMooJzmUhpTCATMtssbZsyYDLs729xl2xO//fOOGkiV3IUePhk0nKS85TfalP0aCXouQG/XMQ==",
"dependencies": {
"@nostr-dev-kit/ndk": "2.14.4"
"@nostr-dev-kit/ndk": "2.14.5"
},
"peerDependencies": {
"svelte": "*"
}
},
"node_modules/@nostr-dev-kit/ndk-svelte/node_modules/@nostr-dev-kit/ndk": {
"version": "2.14.4",
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.14.4.tgz",
"integrity": "sha512-mh7IoKvzDXoh6PJNyvrNVAMm6Zor1xyEKjnVfXQ11WTDXCwOA8Ksff1qxVQOn7u6ZlxlZN4wHBeUwWswuD9FSA==",
"dependencies": {
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0",
"@noble/secp256k1": "^2.1.0",
"@scure/base": "^1.1.9",
"debug": "^4.3.6",
"light-bolt11-decoder": "^3.2.0",
"tseep": "^1.3.1",
"typescript-lru-cache": "^2"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"nostr-tools": "^2"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",

View file

@ -3,17 +3,14 @@
import { onMount } from 'svelte';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { ndk, ndkReady, user } from '$lib/stores';
import { ndk } from '$lib/stores';
import { writable } from 'svelte/store';
import { login } from '$lib';
import { Carta, Markdown, MarkdownEditor } from 'carta-md';
import 'carta-md/default.css'; /* Default theme */
import DOMPurify from 'dompurify';
import { Confetti } from "svelte-confetti"
console.log("show reactions", showReactions, event)
import { Confetti } from 'svelte-confetti';
console.log('show reactions', showReactions, event);
// Create a new instance of Carta (you might also want to add a sanitizer if you're processing user input)
let carta = new Carta({
@ -21,7 +18,7 @@
});
let reactions = writable([]);
let reacted = writable(window.localStorage.getItem(event.id));
let reaction = writable({})
let reaction = writable({});
let clicked = writable(false);
async function sendReaction() {
@ -31,28 +28,28 @@
tags: [['e', event.id]]
});
await reactionEvent.publish();
$reaction = reactionEvent;
$reaction = reactionEvent;
const r = await $ndk.fetchEvents({ kinds: [7], '#e': [event.id] });
console.log('r', r);
$reactions = Array.from(r);
window.localStorage.setItem(event.id, 'true');
$reacted = true;
$clicked = true
$reacted = 'true';
$clicked = true;
}
async function deleteVote() {
const deletionEvent = new NDKEvent($ndk, {
kind: 5,
content: "User deleted vote",
tags: [
["e", event.id],
["k", 7]
]
})
await deletionEvent.publish()
window.localStorage.removeItem(event.id)
$reacted = false
}
async function deleteVote() {
const deletionEvent = new NDKEvent($ndk, {
kind: 5,
content: 'User deleted vote',
tags: [
['e', event.id],
['k', 7]
]
});
await deletionEvent.publish();
window.localStorage.removeItem(event.id);
$reacted = 'false';
}
onMount(async () => {
const r = await $ndk.fetchEvents({ kinds: [7], '#e': [event.id] });
@ -65,12 +62,12 @@
<Markdown {carta} value={event.content} />
{/key}
<div class="flex gap-2 reactions">
<div class="reactions flex gap-2">
{#if $reacted}
<span>👍 {$reactions.length}</span>
<span class="thanks">Danke für deinen Vote!</span>
<!-- <button onclick={() => deleteVote()} class="btn">Vote zurückziehen</button> -->
{:else if showReactions === "true"}
<!-- <button onclick={() => deleteVote()} class="btn">Vote zurückziehen</button> -->
{:else if showReactions === 'true'}
<button onclick={() => sendReaction()} class="like">👍</button>
<span>{$reactions.length}</span>
{/if}
@ -79,19 +76,20 @@
{/if}
</div>
</div>
<style>
.like {
cursor: pointer;
}
.reactions {
padding-top: 10px;
align-items: center;
}
.thanks {
color: #777;
}
.like {
cursor: pointer;
}
.reactions {
padding-top: 10px;
align-items: center;
}
.thanks {
color: #777;
}
.comment {
border-radius: 20px;
border-color: #ccc;
border-radius: 20px;
border-color: #ccc;
}
</style>

View file

@ -1,15 +1,12 @@
import { NDKNip07Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { ndk as ndkStore, user as userStore } from "$lib/stores";
import { ndk } from "$lib/stores";
import { get } from "svelte/store";
export async function login() {
let ndk = get(ndkStore)
let user = get(userStore)
if (window.nostr) {
const signer = new NDKNip07Signer();
ndk.signer = signer;
const signedUser = await signer.user();
userStore.set(signedUser)
get(ndk).signer = signer;
signer.user();
} else {
console.log("no extension")
const storedPrivateKey = window.localStorage.getItem('nostrPrivateKey');
@ -17,24 +14,20 @@ export async function login() {
const privateKey = JSON.parse(storedPrivateKey);
console.log("stored private key", privateKey)
const signer = new NDKPrivateKeySigner(privateKey);
const signedUser = await signer.user();
userStore.set(signedUser)
ndk.signer = signer;
console.log("ndk signer", ndk)
ndkStore.set(ndk)
ndk.update((ndk) => {
ndk.signer = signer;
return ndk;
});
// get(ndk).signer = signer;
signer.user();
} else {
console.log('No private key found, generating a new one...');
const privateKey = NDKPrivateKeySigner.generate();
const signer = new NDKPrivateKeySigner(privateKey.privateKey);
console.log('Generated Private Key:', privateKey);
const signedUser = await signer.user();
userStore.set(signedUser)
signer.user();
window.localStorage.setItem('nostrPrivateKey', JSON.stringify(privateKey.privateKey));
ndk.signer = signer;
console.log("ndk signer", ndk)
ndkStore.set(ndk)
get(ndk).signer = signer;
}
}
}

View file

@ -1,27 +1,21 @@
import NDKSvelte from "@nostr-dev-kit/ndk-svelte";
import { writable, derived } from "svelte/store";
import { NDKNip07Signer } from "@nostr-dev-kit/ndk";
import { NDKUser } from "@nostr-dev-kit/ndk";
export let connected = writable(false);
export const ndk = writable(new NDKSvelte());
const _ndk = new NDKSvelte({
explicitRelayUrls: [
'wss://relay-rpi.edufeed.org'
],
});
export const ndkReady = derived(ndk, $ndk => $ndk !== null);
export const ndk = writable(_ndk)
export async function initializeNDK() {
const signer = new NDKNip07Signer
const ndkInstance = new NDKSvelte({
explicitRelayUrls: [
'wss://relay-rpi.edufeed.org'
],
});
await ndkInstance.connect();
// ndkInstance.signer = signer;
ndk.set(ndkInstance);
return ndkInstance;
}
export const user = writable(null);
export const user = derived(ndk, $ndk => {
console.log("updating user")
if ($ndk.signer !== undefined) {
return new NDKUser($ndk.signer);
}
return undefined;
});

View file

@ -1,22 +1,19 @@
<script>
import '../app.css';
import { onMount } from 'svelte';
import { NDKNip07Signer } from '@nostr-dev-kit/ndk';
import NDKSvelte from '@nostr-dev-kit/ndk-svelte/svelte5';
import { ndk, connected, initializeNDK } from '$lib/stores';
import { ndk, connected } from '$lib/stores';
import { browser } from '$app/environment';
import { login } from '$lib';
let { children } = $props();
onMount(async () => {
const nip07signer = new NDKNip07Signer();
if (browser) {
try {
await initializeNDK();
await login()
await $ndk.connect();
console.log('NDK initialized successfully');
connected.set(true);
await login()
} catch (error) {
console.error('Failed to initialize NDK:', error);
}

View file

@ -1,24 +1,23 @@
<script>
import { goto } from '$app/navigation';
import { ndk, connected, user, ndkReady } from '$lib/stores';
import { NDKEvent, NDKNip07Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk';
import { ndk, connected, user } from '$lib/stores';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { writable } from 'svelte/store';
import QRCode from 'qrcode';
import { login } from '$lib';
import { Carta, MarkdownEditor } from 'carta-md';
import 'carta-md/default.css'; /* Default theme */
import DOMPurify from 'dompurify';
import { Confetti } from "svelte-confetti"
import { Confetti } from 'svelte-confetti';
// Create a new instance of Carta (you might also want to add a sanitizer if you're processing user input)
let carta = new Carta({
sanitizer: DOMPurify.sanitize
});
let question = writable('');
let questionId = writable(''); ;
let questionId = writable('');
let questionShortId = 0;
let qrCodeUrl = writable('');
let timer = 0;
let votingEnabled = false;
let sessionId = '';
let event = writable();
@ -27,13 +26,13 @@
console.log('join ' + sessionId);
const filter = {
kinds: [1342],
'#d': [sessionId + ''], // filter by `d` tag
'#d': [sessionId + ''] // filter by `d` tag
};
console.log(filter)
console.log(filter);
const question = await $ndk.fetchEvent(filter);
console.log('question', question);
goto('/q/' + question.id);
login()
login();
}
async function postQuestion() {
questionShortId = 10000000 + Math.floor(Math.random() * 90000000);
@ -43,39 +42,20 @@
tags: [['d', questionShortId + '']]
});
await $event.publishReplaceable();
console.log("event id", $event.id)
console.log('event id', $event.id);
$questionId = `${$event.kind}:${$event.pubkey}:${$event.dTag}`;
$qrCodeUrl = await QRCode.toDataURL(`${window.location.origin}/q/${$questionId}`, {
width: 800,
width: 800
});
}
function startTimer() {
const interval = setInterval(() => {
if (timer > 0) {
timer--;
} else {
clearInterval(interval);
votingEnabled = true;
}
}, 1000);
}
$effect(() => {
if ($ndkReady) {
if ($ndk.activeUser) {
console.log('User:', $user);
} else {
login();
}
}
});
</script>
<div class="main-layout">
<img src="logo.png" alt="" class="logo">
{#if !$user}
<div class="login"><button class="btn btn-primary" onclick={() => login()}>Login</button></div>
<img src="logo.png" alt="" class="logo" />
{#if $ndk?.signer === undefined}
<div class="login">
<button class="btn btn-primary" onclick={login}>Login</button>
</div>
{/if}
{#if !$questionId}
@ -84,7 +64,9 @@
{#if $connected}
<div class="join">
<input type="number" class="border p-2" placeholder="12345678" bind:value={sessionId} />
<button class="btn btn-primary rounded" onclick={() => joinSession()}> Teilnehmen </button>
<button class="btn btn-primary rounded" onclick={() => joinSession()}>
Teilnehmen
</button>
</div>
{/if}
</div>
@ -93,23 +75,36 @@
<div class="mx-auto p-4">
<h1 class="mb-4 text-2xl font-bold">Eine Ideensammlung starten</h1>
{#if $connected}
{#key $questionId}
{#if $questionId === ''}
<div class="flex flex-col justify-center items-center">
<MarkdownEditor bind:value={$question} {carta} />
<div><button class="btn btn-primary rounded mt-5" onclick={postQuestion}> Starten </button></div>
</div>
{:else}
<div class="qr-share mt-4">
<h2 class="text-xl font-bold">Diese Sammlung teilen</h2>
<h3 class="text-center short-id">{questionShortId}</h3>
<div class="flex justify-center"><Confetti amount="600" size="15" cone x={[-4.5, 4.5]} y={[-1.5, 1.5]} delay={[0, 1000]} /></div>
<img src={$qrCodeUrl} alt="QR Code" class="mt-2" />
<p class="mb-1 text-center">Diesen QR-Code oder Link teilen:</p>
<p class="text-center mb-2 text-xl">
<a href={`/q/${$questionId}`}>{`${window.location.origin}/q/`}<span class="font-bold">{questionShortId}</span></a>
</p>
<!--
{#key $questionId}
{#if $questionId === ''}
<div class="flex flex-col items-center justify-center">
<MarkdownEditor bind:value={$question} {carta} />
<div>
<button class="btn btn-primary mt-5 rounded" onclick={postQuestion}> Starten </button>
</div>
</div>
{:else}
<div class="qr-share mt-4">
<h2 class="text-xl font-bold">Diese Sammlung teilen</h2>
<h3 class="short-id text-center">{questionShortId}</h3>
<div class="flex justify-center">
<Confetti
amount="600"
size="15"
cone
x={[-4.5, 4.5]}
y={[-1.5, 1.5]}
delay={[0, 1000]}
/>
</div>
<img src={$qrCodeUrl} alt="QR Code" class="mt-2" />
<p class="mb-1 text-center">Diesen QR-Code oder Link teilen:</p>
<p class="mb-2 text-center text-xl">
<a href={`/q/${$questionId}`}
>{`${window.location.origin}/q/`}<span class="font-bold">{questionShortId}</span></a
>
</p>
<!--
<div class="mt-4">
<label for="timer" class="mb-2 block">Set Timer (seconds):</label>
<input type="number" id="timer" class="rounded border p-2" bind:value={timer} />
@ -118,48 +113,48 @@
</button>
</div>
-->
</div>
{/if}
</div>
{/if}
{#if votingEnabled}
<div class="mt-4">
<h2 class="text-xl font-bold">Abstimmung ist jetzt aktiv!</h2>
<p>Es können jetzt Stimmen abgegeben werden.</p>
</div>
{/if}
{/key}
{#if votingEnabled}
<div class="mt-4">
<h2 class="text-xl font-bold">Abstimmung ist jetzt aktiv!</h2>
<p>Es können jetzt Stimmen abgegeben werden.</p>
</div>
{/if}
{/key}
{/if}
</div>
</div>
<style>
:global(.carta-input) {
height: 150px !important;
height: 150px !important;
}
:global(.carta-editor) {
width: 100% !important;
}
:global(.carta-editor) {
width: 100% !important;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.short-id {
font-size: 60px;
font-size: 60px;
}
.qr-share img {
width: 100%;
border: 3px solid #eee;
}
.login {
display: flex;
justify-content: center;
display: flex;
justify-content: center;
}
.logo {
display: flex;
margin: 0 auto;
max-width: 250px;
border-radius: 50%;
display: flex;
margin: 0 auto;
max-width: 250px;
border-radius: 50%;
}
.main-layout {
margin: auto;

View file

@ -1,13 +1,13 @@
<script>
import { Confetti } from 'svelte-confetti';
import { onDestroy } from 'svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
import Comment from '$lib/components/Comment.svelte';
import { ndk, ndkReady, user } from '$lib/stores';
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { writable } from 'svelte/store';
import { ndk, user } from '$lib/stores';
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
import { derived, writable } from 'svelte/store';
import { Carta, Markdown, MarkdownEditor } from 'carta-md';
import 'carta-md/default.css'; /* Default theme */
import DOMPurify from 'dompurify';
@ -16,6 +16,7 @@
let carta = new Carta({
sanitizer: DOMPurify.sanitize
});
function submitComment() {
if (!$comment) {
return;
@ -24,61 +25,71 @@
const commentEvent = new NDKEvent($ndk, {
kind: 2222,
content: $comment,
tags: [['E', data.id]]
tags: [['A', data.id]]
});
commentEvent.publish();
comment.set('');
setTimeout(() => submit.set(false), 3000);
}
/**
* Filters an array of objects to keep only unique objects based on their id property
* @param {Array} array - The array of objects to filter
* @returns {Array} - A new array containing only unique objects by id
*/
function getUniqueById(array) {
// Create a Map to track unique objects by their id
const uniqueMap = new Map();
// Loop through the array and add each object to the map with its id as the key
// This automatically overrides any previous entries with the same id
array.forEach((item) => {
if (item && item.id !== undefined) {
uniqueMap.set(item.id, item);
}
});
// Convert the Map values back to an array
return Array.from(uniqueMap.values());
}
let comment = writable('');
let submit = writable(false);
let comments = writable([]);
let question = writable();
const [kind, pubkey, d] = data.id.split(':');
const showReactions = writable(false);
const showReactions = writable('false');
const comments = writable([]);
let commentStore = $ndk.storeSubscribe(
{ kinds: [2222], '#A': [data.id] },
{
onEvent: (event) => {
console.log('Event received', event);
$comments = [event, ...$comments];
},
onEose: () => console.log('Subscription EOSE reached'),
closeOnEose: false,
autoStart: true
}
);
commentStore.ref();
let questionStore = $ndk.storeSubscribe({ kinds: [Number(kind)], authors: [pubkey], '#d': [d] });
questionStore.ref();
let question = derived(
[questionStore],
([$questionStore]) => {
if ($questionStore.length === 0) {
return null;
}
$questionStore.sort((a, b) => {
return b.created_at - a.created_at;
});
const reactionTag = $questionStore[0].tags.find((t) => t[0] === 'reactions');
if (reactionTag && reactionTag[1] === 'true') {
console.log('reactions enabled');
$showReactions = 'true';
}
console.log('question', $questionStore[0]);
return $questionStore[0];
},
null
);
onDestroy(() => {
commentStore.unsubscribe();
questionStore.unsubscribe();
});
$effect(() => {
if ($ndkReady) {
console.log('ndk ready');
const sub = $ndk.subscribe({ kinds: [2222], '#E': [data.id] });
sub.on('event', (event) => {
console.log(event);
const unique = getUniqueById([...$comments, event]);
$comments = [...unique];
console.log(`${event.content}`);
});
if ($ndk.signer) {
console.log('got a signer');
console.log('start subscription');
commentStore.startSubscription();
const questionSub = $ndk.subscribe({ kinds: [Number(kind)], authors: [pubkey], '#d': [d] });
questionSub.on('event', (event) => {
$question = event;
if ($question?.tags.find((t) => t[0] === 'reactions')[1] === 'true') {
console.log('reactions enabled');
$showReactions = 'true';
}
});
questionStore.startSubscription();
console.log('comment store', $commentStore);
}
console.log('user', $user);
});
async function startReactions(event) {
@ -93,35 +104,34 @@
</script>
<div class="main-layout mx-auto flex w-3/4 flex-col items-center justify-center">
{#if $question}
{#if $question.pubkey === $user.pubkey && $showReactions === false}
<button class="btn btn-primary mb-4" onclick={() => startReactions($question)}
>Reaktionen/Voting aktivieren</button
>
{/if}
<div class="question mb-4 w-full rounded border p-4 text-xl">
<h2 class="text-xl font-bold">Frage / Thema:</h2>
<Markdown {carta} value={$question.content} />
</div>
<div class="mb-2 flex w-full flex-col items-center justify-center gap-2">
<h1 class="pt-15 text-xl font-bold">Meine Idee hinzufügen</h1>
<MarkdownEditor bind:value={$comment} {carta} />
<button class="btn btn-primary mt-5 mb-10" onclick={() => submitComment()}>Hinzufügen</button>
{#if $submit === true}
<div class="flex justify-center"><Confetti amount="50"} /></div>
{#key $question?.pubkey}
{#if $question}
{#if $user && $question?.pubkey === $user?.pubkey && $showReactions === "false"}
<button class="btn btn-primary mb-4" onclick={() => startReactions($question)}
>Reaktionen/Voting aktivieren</button
>
{/if}
</div>
{:else}
<p>Loading...</p>
{/if}
<div class="question mb-4 w-full rounded border p-4 text-xl">
<h2 class="text-xl font-bold">Frage / Thema:</h2>
<Markdown {carta} value={$question.content} />
</div>
<div class="mb-2 flex w-full flex-col items-center justify-center gap-2">
<h1 class="pt-15 text-xl font-bold">Meine Idee hinzufügen</h1>
<MarkdownEditor bind:value={$comment} {carta} />
<button class="btn btn-primary mt-5 mb-10" onclick={() => submitComment()}
>Hinzufügen</button
>
</div>
{:else}
<p>Loading...</p>
{/if}
{/key}
<div class="mx-auto flex w-full flex-col items-center justify-center gap-5">
{#key $showReactions}
{#each $comments.sort((a, b) => b.created_at - a.created_at) as event}
<Comment event={event} showReactions={$showReactions} />
{/each}
{/key}
{#each $commentStore as event}
<Comment {event} showReactions={$showReactions} />
{/each}
</div>
</div>