Fix multiple choice poll selections not saving, bring back base64 profile image fallback but compressed

This commit is contained in:
Jon Staab 2026-07-02 14:39:21 -07:00
parent e575be921d
commit 27252f543a
4 changed files with 77 additions and 22 deletions

View file

@ -7,7 +7,6 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import {uploadFile} from "@app/uploads" import {uploadFile} from "@app/uploads"
import {pushToast} from "@app/toast"
interface Props { interface Props {
file?: File | undefined file?: File | undefined
@ -18,6 +17,10 @@
const id = randomId() const id = randomId()
// Profile pictures are shown small (avatar-sized), so compress hard: this keeps
// the base64 fallback below small enough to embed directly in the profile event.
const compressOptions = {maxWidth: 128, maxHeight: 128, quality: 0.6, mimeType: "image/jpeg"}
const onDragEnter = () => { const onDragEnter = () => {
active = true active = true
} }
@ -33,11 +36,11 @@
const onDrop = async (e: any) => { const onDrop = async (e: any) => {
active = false active = false
file = await compressFile(e.dataTransfer.files[0]) file = await compressFile(e.dataTransfer.files[0], compressOptions)
} }
const onChange = async (e: any) => { const onChange = async (e: any) => {
file = await compressFile(e.target.files[0]) file = await compressFile(e.target.files[0], compressOptions)
} }
const onClear = () => { const onClear = () => {
@ -55,15 +58,24 @@
if (file) { if (file) {
loading = true loading = true
const {error, result} = await uploadFile(file) const {result} = await uploadFile(file)
loading = false loading = false
if (result?.url) { if (result?.url) {
url = result.url url = result.url
} else { } else {
file = undefined const reader = new FileReader()
pushToast({theme: "error", message: error || "Failed to upload image."})
reader.addEventListener(
"load",
() => {
url = reader.result as string
},
false,
)
reader.readAsDataURL(file)
} }
} else { } else {
url = initialUrl url = initialUrl
@ -73,7 +85,12 @@
</script> </script>
<form> <form>
<input {id} type="file" accept="image/*" onchange={onChange} class="hidden" /> <input
{id}
type="file"
accept="image/jpeg,image/png,image/webp"
onchange={onChange}
class="hidden" />
<label <label
for={id} for={id}
aria-label="Drag and drop files here." aria-label="Drag and drop files here."
@ -85,7 +102,7 @@
ondragleave={stopPropagation(preventDefault(onDragLeave))} ondragleave={stopPropagation(preventDefault(onDragLeave))}
ondrop={stopPropagation(preventDefault(onDrop))}> ondrop={stopPropagation(preventDefault(onDrop))}>
<button <button
onclick={url && stopPropagation(onClear)} onclick={url ? stopPropagation(onClear) : undefined}
class="absolute right-0 top-0 h-5 w-5 overflow-hidden rounded-full flex items-center justify-center" class="absolute right-0 top-0 h-5 w-5 overflow-hidden rounded-full flex items-center justify-center"
style="background: {url ? 'var(--error)' : 'var(--primary)'};"> style="background: {url ? 'var(--error)' : 'var(--primary)'};">
{#if url} {#if url}

View file

@ -0,0 +1,47 @@
<script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import RoomName from "@app/components/RoomName.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
import EventActions from "@app/components/EventActions.svelte"
import {publishDelete} from "@app/deletes"
import {publishReaction} from "@app/reactions"
import {canEnforceNip70} from "@app/relays"
import {makePollPath, makeSpacePath} from "@app/routes"
interface Props {
url: string
event: TrustedEvent
showRoom?: boolean
showActivity?: boolean
}
const {url, event, showRoom, showActivity}: Props = $props()
const h = getTagValue("h", event.tags)
const path = makePollPath(url, event.id)
const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script>
<div class="flex grow flex-wrap justify-end gap-2">
{#if h && showRoom}
<Link href={makeSpacePath(url, h)} class="button button-neutral button-xs rounded-full">
Posted in #<RoomName {h} {url} />
</Link>
{/if}
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
<ThunkStatusOrDeleted {event} />
{#if showActivity}
<EventActivity {url} {path} {event} />
{/if}
<EventActions {url} {event} noun="Poll" />
</div>

View file

@ -3,7 +3,7 @@
import {getTagValue} from "@welshman/util" import {getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import NoteContent from "@app/components/NoteContent.svelte" import NoteContent from "@app/components/NoteContent.svelte"
import CommentActions from "@app/components/CommentActions.svelte" import PollActions from "@app/components/PollActions.svelte"
import RoomLink from "@app/components/RoomLink.svelte" import RoomLink from "@app/components/RoomLink.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
import {makePollPath} from "@app/routes" import {makePollPath} from "@app/routes"
@ -29,6 +29,6 @@
in <RoomLink {url} {h} /> in <RoomLink {url} {h} />
{/if} {/if}
</span> </span>
<CommentActions segment="polls" showActivity {url} {event} /> <PollActions showActivity {url} {event} />
</div> </div>
</Link> </Link>

View file

@ -5,7 +5,6 @@
import {pubkey, publishThunk, abortThunk} from "@welshman/app" import {pubkey, publishThunk, abortThunk} from "@welshman/app"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {deriveEvents} from "@app/repository" import {deriveEvents} from "@app/repository"
import {pushToast} from "@app/toast"
import {makePollResponse} from "@app/polls" import {makePollResponse} from "@app/polls"
import PollOption from "@app/components/PollOption.svelte" import PollOption from "@app/components/PollOption.svelte"
import { import {
@ -30,7 +29,6 @@
const options = getPollOptions(event) const options = getPollOptions(event)
const closed = isPollClosed(event) const closed = isPollClosed(event)
const endsAt = getPollEndsAt(event) const endsAt = getPollEndsAt(event)
const publishDelay = pollType === "multiplechoice" ? 10_000 : undefined
const getOwnResponse = (responses: TrustedEvent[]) => { const getOwnResponse = (responses: TrustedEvent[]) => {
let latest: TrustedEvent | undefined let latest: TrustedEvent | undefined
@ -61,19 +59,12 @@
activeThunk = publishThunk({ activeThunk = publishThunk({
relays: [url], relays: [url],
event: makePollResponse({event, selectedIds: selection}), event: makePollResponse({event, selectedIds: selection}),
delay: publishDelay, delay: pollType === "multiplechoice" ? 1000 : undefined,
}) })
} }
const publishCurrentSelection = () => { const publishCurrentSelection = () =>
const selection = pollType === "singlechoice" ? selectedIds.slice(0, 1) : selectedIds publishSelection(pollType === "singlechoice" ? selectedIds.slice(0, 1) : selectedIds)
if (selection.length === 0) {
return pushToast({theme: "error", message: "Please select at least one option."})
}
publishSelection(selection)
}
const results = $derived(getPollResults(event, $responses)) const results = $derived(getPollResults(event, $responses))
const ownResponse = $derived(getOwnResponse($responses)) const ownResponse = $derived(getOwnResponse($responses))