2026-04-03 17:54:50 +00:00
|
|
|
<script lang="ts">
|
|
|
|
|
import {onDestroy} from "svelte"
|
|
|
|
|
import {formatTimestampRelative} from "@welshman/lib"
|
2026-07-28 16:16:26 +00:00
|
|
|
import type {TrustedEvent} from "@welshman/util"
|
2026-09-09 02:24:05 +00:00
|
|
|
import {POLL_RESPONSE, tagSpec, tagValues, relay} from "@welshman/util"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {Poll, PollResponse} from "@welshman/domain"
|
|
|
|
|
import type {Thunk} from "@welshman/app"
|
2026-04-03 17:54:50 +00:00
|
|
|
import PollOption from "@app/components/PollOption.svelte"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {command, reader, thunks, user, writer} from "@app/core"
|
|
|
|
|
import {deriveEvents} from "@app/repository"
|
2026-04-03 17:54:50 +00:00
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
url: string
|
|
|
|
|
event: TrustedEvent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {url, event}: Props = $props()
|
|
|
|
|
|
2026-04-16 18:30:59 +00:00
|
|
|
const responses = deriveEvents([{kinds: [POLL_RESPONSE], "#e": [event.id]}])
|
2026-04-03 17:54:50 +00:00
|
|
|
|
|
|
|
|
const getOwnResponse = (responses: TrustedEvent[]) => {
|
|
|
|
|
let latest: TrustedEvent | undefined
|
|
|
|
|
|
|
|
|
|
for (const response of responses) {
|
2026-07-28 16:16:26 +00:00
|
|
|
if (
|
|
|
|
|
response.pubkey === $user.pubkey &&
|
|
|
|
|
(!latest || response.created_at > latest.created_at)
|
|
|
|
|
) {
|
2026-04-03 17:54:50 +00:00
|
|
|
latest = response
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return latest
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const publishSelection = async (selection: string[]) => {
|
|
|
|
|
activeThunk?.abort()
|
|
|
|
|
activeThunk = undefined
|
2026-04-03 17:54:50 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
if (selection.length > 0) {
|
2026-09-09 02:24:05 +00:00
|
|
|
const eventWriter = writer(PollResponse).forceRoutes(relay(url)).setPollId(event.id)
|
2026-04-03 17:54:50 +00:00
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
for (const id of selection) {
|
|
|
|
|
eventWriter.addSelection(id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const responseCommand = await command(eventWriter)
|
|
|
|
|
|
|
|
|
|
// Give the user time to check more boxes before the vote actually goes out.
|
|
|
|
|
activeThunk = $thunks.publish({
|
|
|
|
|
event: responseCommand.event,
|
|
|
|
|
relays: responseCommand.relays,
|
|
|
|
|
delay: pollType === "multiplechoice" ? 1000 : undefined,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-03 17:54:50 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-02 21:39:21 +00:00
|
|
|
const publishCurrentSelection = () =>
|
|
|
|
|
publishSelection(pollType === "singlechoice" ? selectedIds.slice(0, 1) : selectedIds)
|
2026-04-03 17:54:50 +00:00
|
|
|
|
|
|
|
|
const setSingleChoice = (id: string) => {
|
|
|
|
|
selectedIds = [id]
|
|
|
|
|
publishCurrentSelection()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toggleMultipleChoice = (id: string) => {
|
|
|
|
|
selectedIds = selectedIds.includes(id)
|
|
|
|
|
? selectedIds.filter(selectedId => selectedId !== id)
|
|
|
|
|
: [...selectedIds, id]
|
|
|
|
|
|
|
|
|
|
publishCurrentSelection()
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const poll = reader(Poll)(event)
|
2026-04-03 17:54:50 +00:00
|
|
|
let selectedIds = $state<string[]>([])
|
2026-07-28 16:16:26 +00:00
|
|
|
let activeThunk: Thunk | undefined
|
|
|
|
|
|
|
|
|
|
const pollType = poll.pollType()
|
|
|
|
|
const results = $derived(poll.results($responses))
|
|
|
|
|
const ownResponse = $derived(getOwnResponse($responses))
|
2026-04-03 17:54:50 +00:00
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (ownResponse) {
|
2026-07-28 16:16:26 +00:00
|
|
|
const selections = tagValues(tagSpec("response"), ownResponse.tags)
|
|
|
|
|
|
|
|
|
|
selectedIds = pollType === "singlechoice" ? selections.slice(0, 1) : selections
|
2026-04-03 17:54:50 +00:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onDestroy(() => {
|
2026-07-28 16:16:26 +00:00
|
|
|
activeThunk?.abort()
|
2026-04-03 17:54:50 +00:00
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
{#if poll && results}
|
|
|
|
|
{@const endsAt = poll.endsAt()}
|
|
|
|
|
<div class="flex flex-col gap-2">
|
|
|
|
|
{#each poll.options() as option (option.id)}
|
|
|
|
|
<PollOption
|
|
|
|
|
{event}
|
|
|
|
|
{option}
|
|
|
|
|
{results}
|
|
|
|
|
{selectedIds}
|
|
|
|
|
{setSingleChoice}
|
|
|
|
|
{toggleMultipleChoice} />
|
|
|
|
|
{/each}
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
|
|
|
<div class="text-sm opacity-75">
|
|
|
|
|
{pollType === "multiplechoice" ? "Multiple choice" : "Single choice"}
|
|
|
|
|
{#if endsAt}
|
|
|
|
|
{#if poll.isClosed()}
|
|
|
|
|
• Ended {formatTimestampRelative(endsAt)}
|
|
|
|
|
{:else}
|
|
|
|
|
• Ends {formatTimestampRelative(endsAt)}
|
|
|
|
|
{/if}
|
2026-04-03 17:54:50 +00:00
|
|
|
{/if}
|
2026-07-28 16:16:26 +00:00
|
|
|
</div>
|
|
|
|
|
<div class="text-sm opacity-75">{results.voters} vote{results.voters === 1 ? "" : "s"}</div>
|
2026-04-03 17:54:50 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-07-28 16:16:26 +00:00
|
|
|
{/if}
|