From 329ad507f6519414bc4283851e8f3e7bdc227b42 Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Thu, 2 Apr 2026 11:18:41 +0530 Subject: [PATCH 1/7] feat: use NIP-50 relay-side search with scope selection --- src/app/components/SpaceSearch.svelte | 137 ++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 20 deletions(-) diff --git a/src/app/components/SpaceSearch.svelte b/src/app/components/SpaceSearch.svelte index c5557bcb..f25313fe 100644 --- a/src/app/components/SpaceSearch.svelte +++ b/src/app/components/SpaceSearch.svelte @@ -1,15 +1,17 @@ + +
+ {props.event.content || "Poll"} + {$results.voters} voter{$results.voters === 1 ? "" : "s"} +
diff --git a/src/app/components/NoteContentPoll.svelte b/src/app/components/NoteContentPoll.svelte new file mode 100644 index 00000000..db6af151 --- /dev/null +++ b/src/app/components/NoteContentPoll.svelte @@ -0,0 +1,53 @@ + + +
+
+
+

{props.event.content || "Poll"}

+

+ {pollType === "multiplechoice" ? "Multiple choice" : "Single choice"} + {#if endsAt} + · Ends {formatTimestampRelative(endsAt)} + {/if} + {#if closed} + · Closed + {/if} +

+
+

{$results.voters} voter{$results.voters === 1 ? "" : "s"}

+
+ +
+ {#each $results.options as option (option.id)} + {@const maxVotes = Math.max(...$results.options.map(item => item.votes), 1)} +
+
+ {option.label} + {option.votes} +
+ +
+ {/each} +
+
diff --git a/src/app/components/PollCreate.svelte b/src/app/components/PollCreate.svelte new file mode 100644 index 00000000..d4c46a30 --- /dev/null +++ b/src/app/components/PollCreate.svelte @@ -0,0 +1,166 @@ + + + + + + Create a Poll + Ask the room a question with one or more answers. + +
+ + {#snippet label()} +

Question*

+ {/snippet} + {#snippet input()} + + {/snippet} +
+ + + {#snippet label()} +

Options*

+ {/snippet} + {#snippet input()} +
+ {#each options as option, index (index)} +
+ + +
+ {/each} + +
+ {/snippet} +
+ +
+ + {#snippet label()} + Poll type + {/snippet} + {#snippet input()} + + {/snippet} + + + {#snippet label()} + Ends at + {/snippet} + {#snippet input()} + + {/snippet} + +
+
+
+ + + + +
diff --git a/src/app/components/PollItem.svelte b/src/app/components/PollItem.svelte new file mode 100644 index 00000000..7a5eea1f --- /dev/null +++ b/src/app/components/PollItem.svelte @@ -0,0 +1,32 @@ + + + + +
+ + Posted by + {#if h} + in + {/if} + + +
+ diff --git a/src/app/components/PollVotes.svelte b/src/app/components/PollVotes.svelte new file mode 100644 index 00000000..ee31e8a6 --- /dev/null +++ b/src/app/components/PollVotes.svelte @@ -0,0 +1,126 @@ + + +
+
+
+ {pollType === "multiplechoice" ? "Multiple choice" : "Single choice"} + {#if endsAt} + · Ends {formatTimestampRelative(endsAt)} + {/if} + {#if closed} + · Closed + {/if} +
+
{results.voters} vote{results.voters === 1 ? "" : "s"}
+
+ +
+ {#each options as option (option.id)} + {@const maxVotes = Math.max(...results.options.map(result => result.votes), 1)} + {@const current = results.options.find(result => result.id === option.id)} +
+
+ + {current?.votes || 0} vote{(current?.votes || 0) === 1 ? "" : "s"} +
+ +
+ {/each} +
+ + {#if !closed} +
+ +
+ {/if} +
diff --git a/src/app/components/SpaceMenu.svelte b/src/app/components/SpaceMenu.svelte index d7b97c83..fd8d8a5d 100644 --- a/src/app/components/SpaceMenu.svelte +++ b/src/app/components/SpaceMenu.svelte @@ -1,6 +1,7 @@ + + + {#snippet title()} + + Polls + {/snippet} + {#snippet action()} + + {/snippet} + + + + {#each items as event (event.id)} +
+ +
+ {/each} +

+ + {#if loading} + Looking for polls... + {:else if items.length === 0} + No polls found. + {:else} + That's all! + {/if} + +

+
diff --git a/src/routes/spaces/[relay]/polls/[id]/+page.svelte b/src/routes/spaces/[relay]/polls/[id]/+page.svelte new file mode 100644 index 00000000..eac6f17d --- /dev/null +++ b/src/routes/spaces/[relay]/polls/[id]/+page.svelte @@ -0,0 +1,105 @@ + + + + {#snippet title()} +

{$event?.content || "Poll"}

+ {/snippet} +
+ + + {#if $event} +
+ +
+ + + +
+
+ {#if !showAll && $comments.length > 4} +
+ +
+ {/if} + {#each $comments.slice(0, showAll ? undefined : 4) as reply (reply.id)} + +
+ + +
+
+ {/each} +
+ {#if showReply} + + {:else} +
+ +
+ {/if} + {:else} + {#await sleep(5000)} + Loading poll... + {:then} +

Failed to load poll.

+ {/await} + {/if} +
diff --git a/src/routes/spaces/[relay]/recent/+page.svelte b/src/routes/spaces/[relay]/recent/+page.svelte index cc888f00..b4a61071 100644 --- a/src/routes/spaces/[relay]/recent/+page.svelte +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -26,8 +26,10 @@ import ClassifiedItem from "@app/components/ClassifiedItem.svelte" import GoalItem from "@app/components/GoalItem.svelte" import CalendarEventItem from "@app/components/CalendarEventItem.svelte" + import PollItem from "@app/components/PollItem.svelte" import RecentConversation from "@app/components/RecentConversation.svelte" import {decodeRelay, deriveEventsForUrl, CONTENT_KINDS} from "@app/core/state" + import {Poll} from "nostr-tools/kinds" const url = decodeRelay($page.params.relay!) const since = ago(3, MONTH) @@ -126,6 +128,8 @@ {:else if event.kind === EVENT_TIME} + {:else if event.kind === Poll} + {:else} {/if} -- 2.52.0 From 7a44bd1677721da7ebfd6de53ef381defdc7b2d6 Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Fri, 3 Apr 2026 01:09:00 +0530 Subject: [PATCH 5/7] feat: add NIP-88 poll support --- src/app/components/NoteContentPoll.svelte | 4 +++- src/app/components/PollCreate.svelte | 11 +++++++++-- src/app/components/PollItem.svelte | 4 +++- src/app/components/PollVotes.svelte | 15 +++++++++------ src/routes/spaces/[relay]/polls/+page.svelte | 2 +- src/routes/spaces/[relay]/polls/[id]/+page.svelte | 6 +++++- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/app/components/NoteContentPoll.svelte b/src/app/components/NoteContentPoll.svelte index db6af151..ffcf9bc3 100644 --- a/src/app/components/NoteContentPoll.svelte +++ b/src/app/components/NoteContentPoll.svelte @@ -35,7 +35,9 @@ {/if}

-

{$results.voters} voter{$results.voters === 1 ? "" : "s"}

+

+ {$results.voters} voter{$results.voters === 1 ? "" : "s"} +

diff --git a/src/app/components/PollCreate.svelte b/src/app/components/PollCreate.svelte index d4c46a30..fb17bf51 100644 --- a/src/app/components/PollCreate.svelte +++ b/src/app/components/PollCreate.svelte @@ -118,7 +118,11 @@ {#each options as option, index (index)}
diff --git a/src/app/components/PollItem.svelte b/src/app/components/PollItem.svelte index 7a5eea1f..1f96e96a 100644 --- a/src/app/components/PollItem.svelte +++ b/src/app/components/PollItem.svelte @@ -18,7 +18,9 @@ const h = getTagValue("h", event.tags) - +
diff --git a/src/app/components/PollVotes.svelte b/src/app/components/PollVotes.svelte index ee31e8a6..c334f9b9 100644 --- a/src/app/components/PollVotes.svelte +++ b/src/app/components/PollVotes.svelte @@ -33,10 +33,11 @@ const endsAt = getPollEndsAt(event) const results = $derived.by(() => getPollResults(event, $responses)) - const ownResponse = $derived.by(() => - $responses - .filter(response => response.pubkey === $pubkey) - .sort((left, right) => right.created_at - left.created_at)[0], + const ownResponse = $derived.by( + () => + $responses + .filter(response => response.pubkey === $pubkey) + .sort((left, right) => right.created_at - left.created_at)[0], ) const submit = async () => { @@ -111,9 +112,11 @@ {/if} {option.label} - {current?.votes || 0} vote{(current?.votes || 0) === 1 ? "" : "s"} + {current?.votes || 0} vote{(current?.votes || 0) === 1 ? "" : "s"}
- +
{/each} diff --git a/src/routes/spaces/[relay]/polls/+page.svelte b/src/routes/spaces/[relay]/polls/+page.svelte index c875ad9f..c5ac4b2d 100644 --- a/src/routes/spaces/[relay]/polls/+page.svelte +++ b/src/routes/spaces/[relay]/polls/+page.svelte @@ -5,7 +5,7 @@ import {page} from "$app/stores" import {sortBy, partition, spec, pushToMapKey, max} from "@welshman/lib" import type {TrustedEvent} from "@welshman/util" - import {COMMENT, getTagValue} from "@welshman/util" + import {getTagValue} from "@welshman/util" import {fly} from "@lib/transition" import PollIcon from "@assets/icons/revote.svg?dataurl" import Add from "@assets/icons/add.svg?dataurl" diff --git a/src/routes/spaces/[relay]/polls/[id]/+page.svelte b/src/routes/spaces/[relay]/polls/[id]/+page.svelte index eac6f17d..7de2c560 100644 --- a/src/routes/spaces/[relay]/polls/[id]/+page.svelte +++ b/src/routes/spaces/[relay]/polls/[id]/+page.svelte @@ -47,7 +47,11 @@ onMount(() => { const controller = new AbortController() - request({relays: [url], filters: [{kinds: [Poll], ids: [id]}, {kinds: [PollResponse], "#e": [id]}, ...filters], signal: controller.signal}) + request({ + relays: [url], + filters: [{kinds: [Poll], ids: [id]}, {kinds: [PollResponse], "#e": [id]}, ...filters], + signal: controller.signal, + }) return () => { controller.abort() -- 2.52.0 From 70ce54c5a503fa25c6c7e4642f057e8c2dac4e55 Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Fri, 3 Apr 2026 04:04:45 +0530 Subject: [PATCH 6/7] fix: refine poll UX and review and fix requested changes --- src/app/components/ComposeMenu.svelte | 2 +- .../components/NoteContentMinimalPoll.svelte | 9 +- src/app/components/NoteContentPoll.svelte | 58 +++------ src/app/components/PollCreate.svelte | 111 ++++++++++++++---- src/app/components/PollVotes.svelte | 42 ++++--- src/app/util/polls.ts | 20 ++-- .../spaces/[relay]/polls/[id]/+page.svelte | 2 - 7 files changed, 142 insertions(+), 102 deletions(-) diff --git a/src/app/components/ComposeMenu.svelte b/src/app/components/ComposeMenu.svelte index 8c89e9f4..b7828d55 100644 --- a/src/app/components/ComposeMenu.svelte +++ b/src/app/components/ComposeMenu.svelte @@ -67,7 +67,7 @@
  • diff --git a/src/app/components/NoteContentMinimalPoll.svelte b/src/app/components/NoteContentMinimalPoll.svelte index 6e479710..1d67365e 100644 --- a/src/app/components/NoteContentMinimalPoll.svelte +++ b/src/app/components/NoteContentMinimalPoll.svelte @@ -2,21 +2,18 @@ import type {ComponentProps} from "svelte" import {derived} from "svelte/store" import {PollResponse} from "nostr-tools/kinds" - import {repository} from "@welshman/app" - import {deriveArray, deriveEventsById} from "@welshman/store" import ContentMinimal from "@app/components/ContentMinimal.svelte" + import {deriveEvents} from "@app/core/state" import {getPollResults} from "@app/util/polls" const props: ComponentProps = $props() - const responses = deriveArray( - deriveEventsById({repository, filters: [{kinds: [PollResponse], "#e": [props.event.id]}]}), - ) + const responses = deriveEvents([{kinds: [PollResponse], "#e": [props.event.id]}]) const results = derived(responses, $responses => getPollResults(props.event, $responses)) -
    +
    {props.event.content || "Poll"} {$results.voters} voter{$results.voters === 1 ? "" : "s"}
    diff --git a/src/app/components/NoteContentPoll.svelte b/src/app/components/NoteContentPoll.svelte index ffcf9bc3..53fcb203 100644 --- a/src/app/components/NoteContentPoll.svelte +++ b/src/app/components/NoteContentPoll.svelte @@ -1,55 +1,29 @@
    -
    -
    -

    {props.event.content || "Poll"}

    -

    - {pollType === "multiplechoice" ? "Multiple choice" : "Single choice"} - {#if endsAt} - · Ends {formatTimestampRelative(endsAt)} - {/if} - {#if closed} - · Closed - {/if} -

    -
    -

    - {$results.voters} voter{$results.voters === 1 ? "" : "s"} -

    -
    +

    {props.event.content || "Poll"}

    -
    - {#each $results.options as option (option.id)} - {@const maxVotes = Math.max(...$results.options.map(item => item.votes), 1)} -
    -
    - {option.label} - {option.votes} -
    - -
    - {/each} -
    + {#if props.url} + + {/if}
    diff --git a/src/app/components/PollCreate.svelte b/src/app/components/PollCreate.svelte index fb17bf51..33d3cc3e 100644 --- a/src/app/components/PollCreate.svelte +++ b/src/app/components/PollCreate.svelte @@ -1,15 +1,17 @@ Create a Poll - Ask the room a question with one or more answers. + Ask a question and collect votes right in the feed.
    @@ -114,22 +171,33 @@

    Options*

    {/snippet} {#snippet input()} -
    - {#each options as option, index (index)} -
    +
    + {#each options as option, index (option.id)} +
    onDragStart(e, option.id)} + ondragover={e => onDragOver(e, option.id)} + ondrop={e => onDrop(e, option.id)} + ondragend={onDragEnd}> +
    + +
    -
    {/each} - @@ -154,10 +222,7 @@ Ends at {/snippet} {#snippet input()} - + {/snippet}
    diff --git a/src/app/components/PollVotes.svelte b/src/app/components/PollVotes.svelte index c334f9b9..d134f384 100644 --- a/src/app/components/PollVotes.svelte +++ b/src/app/components/PollVotes.svelte @@ -1,10 +1,10 @@
    - {props.event.content || "Poll"} + {$results.voters} voter{$results.voters === 1 ? "" : "s"}
    diff --git a/src/app/components/NoteContentPoll.svelte b/src/app/components/NoteContentPoll.svelte index 53fcb203..a60ecda4 100644 --- a/src/app/components/NoteContentPoll.svelte +++ b/src/app/components/NoteContentPoll.svelte @@ -21,7 +21,7 @@
    -

    {props.event.content || "Poll"}

    + {#if props.url} diff --git a/src/app/components/PollVotes.svelte b/src/app/components/PollVotes.svelte index d134f384..e86f4412 100644 --- a/src/app/components/PollVotes.svelte +++ b/src/app/components/PollVotes.svelte @@ -1,12 +1,14 @@ -
    +
    {pollType === "multiplechoice" ? "Multiple choice" : "Single choice"} @@ -111,12 +137,14 @@ type="radio" class="radio radio-primary radio-sm" checked={selectedIds[0] === option.id} + onclick={stopPropagation(noop)} onchange={() => setSingleChoice(option.id)} /> {:else} toggleMultipleChoice(option.id)} /> {/if} {/if} @@ -130,10 +158,4 @@
    {/each}
    - - {#if !closed} -
    - -
    - {/if}
    -- 2.52.0