From fe20fbfd28b0cbc8211bc539dd9b1b7240e4ed09 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Fri, 3 Apr 2026 10:54:50 -0700 Subject: [PATCH] Add polls --- src/app/components/ComposeMenu.svelte | 10 + src/app/components/NoteContent.svelte | 4 + src/app/components/NoteContentMinimal.svelte | 4 + .../components/NoteContentMinimalPoll.svelte | 19 ++ src/app/components/NoteContentPoll.svelte | 29 +++ src/app/components/PollCreate.svelte | 238 ++++++++++++++++++ src/app/components/PollItem.svelte | 34 +++ src/app/components/PollOption.svelte | 70 ++++++ src/app/components/PollVotes.svelte | 127 ++++++++++ src/app/components/SpaceMenu.svelte | 8 + src/app/core/commands.ts | 17 ++ src/app/core/state.ts | 3 +- src/app/core/sync.ts | 5 +- src/app/util/polls.ts | 76 ++++++ src/app/util/routes.ts | 9 + src/app/util/title.ts | 2 + src/routes/spaces/[relay]/polls/+page.svelte | 95 +++++++ .../spaces/[relay]/polls/[id]/+page.svelte | 107 ++++++++ src/routes/spaces/[relay]/recent/+page.svelte | 4 + 19 files changed, 859 insertions(+), 2 deletions(-) create mode 100644 src/app/components/NoteContentMinimalPoll.svelte create mode 100644 src/app/components/NoteContentPoll.svelte create mode 100644 src/app/components/PollCreate.svelte create mode 100644 src/app/components/PollItem.svelte create mode 100644 src/app/components/PollOption.svelte create mode 100644 src/app/components/PollVotes.svelte create mode 100644 src/app/util/polls.ts create mode 100644 src/routes/spaces/[relay]/polls/+page.svelte create mode 100644 src/routes/spaces/[relay]/polls/[id]/+page.svelte diff --git a/src/app/components/ComposeMenu.svelte b/src/app/components/ComposeMenu.svelte index 990728de..b7828d55 100644 --- a/src/app/components/ComposeMenu.svelte +++ b/src/app/components/ComposeMenu.svelte @@ -4,6 +4,7 @@ import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl" import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl" import CaseMinimalistic from "@assets/icons/case-minimalistic.svg?dataurl" + import Revote from "@assets/icons/revote.svg?dataurl" import Button from "@lib/components/Button.svelte" import Icon from "@lib/components/Icon.svelte" import {pushModal} from "@app/util/modal" @@ -11,6 +12,7 @@ import ThreadCreate from "@app/components/ThreadCreate.svelte" import ClassifiedCreate from "@app/components/ClassifiedCreate.svelte" import GoalCreate from "@app/components/GoalCreate.svelte" + import PollCreate from "@app/components/PollCreate.svelte" type Props = { url: string @@ -28,6 +30,8 @@ const createClassified = () => pushModal(ClassifiedCreate, {url, h}) + const createPoll = () => pushModal(PollCreate, {url, h}) + let ul: Element onMount(() => { @@ -60,4 +64,10 @@ Create Thread +
  • + +
  • diff --git a/src/app/components/NoteContent.svelte b/src/app/components/NoteContent.svelte index d14c6c47..8eca0dd9 100644 --- a/src/app/components/NoteContent.svelte +++ b/src/app/components/NoteContent.svelte @@ -1,10 +1,12 @@ + +
    + + {$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..a60ecda4 --- /dev/null +++ b/src/app/components/NoteContentPoll.svelte @@ -0,0 +1,29 @@ + + +
    + + + {#if props.url} + + {/if} +
    diff --git a/src/app/components/PollCreate.svelte b/src/app/components/PollCreate.svelte new file mode 100644 index 00000000..33d3cc3e --- /dev/null +++ b/src/app/components/PollCreate.svelte @@ -0,0 +1,238 @@ + + + + + + Create a Poll + Ask a question and collect votes right in the feed. + +
    + + {#snippet label()} +

    Question*

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

    Options*

    + {/snippet} + {#snippet input()} +
    + {#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} + +
    + {/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..1f96e96a --- /dev/null +++ b/src/app/components/PollItem.svelte @@ -0,0 +1,34 @@ + + + + +
    + + Posted by + {#if h} + in + {/if} + + +
    + diff --git a/src/app/components/PollOption.svelte b/src/app/components/PollOption.svelte new file mode 100644 index 00000000..784c1fde --- /dev/null +++ b/src/app/components/PollOption.svelte @@ -0,0 +1,70 @@ + + +
    +
    + + {votes} vote{votes === 1 ? "" : "s"} +
    + +
    diff --git a/src/app/components/PollVotes.svelte b/src/app/components/PollVotes.svelte new file mode 100644 index 00000000..83448174 --- /dev/null +++ b/src/app/components/PollVotes.svelte @@ -0,0 +1,127 @@ + + +
    + {#each options as option (option.id)} + + {/each} +
    +
    + {pollType === "multiplechoice" ? "Multiple choice" : "Single choice"} + {#if endsAt} + {#if closed} + • Ended {formatTimestampRelative(endsAt)} + {:else} + • Ends {formatTimestampRelative(endsAt)} + {/if} + {/if} +
    +
    {results.voters} vote{results.voters === 1 ? "" : "s"}
    +
    +
    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..976f9c36 --- /dev/null +++ b/src/routes/spaces/[relay]/polls/[id]/+page.svelte @@ -0,0 +1,107 @@ + + + + {#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 309a1296..d75ddbc0 100644 --- a/src/routes/spaces/[relay]/recent/+page.svelte +++ b/src/routes/spaces/[relay]/recent/+page.svelte @@ -46,9 +46,11 @@ 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 {goToEvent} from "@app/util/routes" + import {Poll} from "nostr-tools/kinds" const url = decodeRelay($page.params.relay!) const since = ago(3, MONTH) @@ -306,6 +308,8 @@ {:else if event.kind === EVENT_TIME} + {:else if event.kind === Poll} + {:else} {/if}