Avoid reflow by showing chat thunk status in a toast

This commit is contained in:
Jon Staab
2025-08-19 14:03:04 -07:00
parent 4f6c08f8a2
commit cde03ec0fe
26 changed files with 268 additions and 124 deletions
@@ -22,15 +22,17 @@
showActivity?: boolean showActivity?: boolean
} = $props() } = $props()
const shouldProtect = canEnforceNip70(url)
const path = makeCalendarPath(url, event.id) const path = makeCalendarPath(url, event.id)
const editEvent = () => pushModal(CalendarEventEdit, {url, event}) const editEvent = () => pushModal(CalendarEventEdit, {url, event})
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)}) publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) => const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
+3 -1
View File
@@ -33,6 +33,8 @@
const {url, header, initialValues}: Props = $props() const {url, header, initialValues}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
const back = () => history.back() const back = () => history.back()
@@ -75,7 +77,7 @@
...ed.storage.nostr.getEditorTags(), ...ed.storage.nostr.getEditorTags(),
] ]
if (await canEnforceNip70(url)) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
+5 -4
View File
@@ -8,7 +8,7 @@
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ThunkStatus from "@app/components/ThunkStatus.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte" import ChannelMessageZapButton from "@app/components/ChannelMessageZapButton.svelte"
@@ -30,6 +30,7 @@
const {url, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props() const {url, event, replyTo = undefined, showPubkey = false, inert = false}: Props = $props()
const thunk = $thunks[event.id] const thunk = $thunks[event.id]
const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey, [url]) const profile = deriveProfile(event.pubkey, [url])
const profileDisplay = deriveProfileDisplay(event.pubkey, [url]) const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
@@ -42,10 +43,10 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)}) publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) => const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<TapTarget <TapTarget
@@ -79,7 +80,7 @@
<div class="text-sm"> <div class="text-sm">
<Content minimalQuote {event} {url} /> <Content minimalQuote {event} {url} />
{#if thunk} {#if thunk}
<ThunkStatus {thunk} class="mt-2" /> <ThunkFailure showToastOnRetry {thunk} class="mt-2" />
{/if} {/if}
</div> </div>
</div> </div>
@@ -6,12 +6,14 @@
const {url, event} = $props() const {url, event} = $props()
const shouldProtect = canEnforceNip70(url)
const onEmoji = async (emoji: NativeEmoji) => const onEmoji = async (emoji: NativeEmoji) =>
publishReaction({ publishReaction({
event, event,
relays: [url], relays: [url],
content: emoji.unicode, content: emoji.unicode,
protect: await canEnforceNip70(url), protect: await shouldProtect,
}) })
</script> </script>
@@ -20,13 +20,15 @@
const {url, event, reply}: Props = $props() const {url, event, reply}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => { const onEmoji = (async (event: TrustedEvent, url: string, emoji: NativeEmoji) => {
history.back() history.back()
publishReaction({ publishReaction({
event, event,
relays: [url], relays: [url],
content: emoji.unicode, content: emoji.unicode,
protect: await canEnforceNip70(url), protect: await shouldProtect,
}) })
}).bind(undefined, event, url) }).bind(undefined, event, url)
+16 -1
View File
@@ -26,9 +26,11 @@
pubkey, pubkey,
tagPubkey, tagPubkey,
sendWrapped, sendWrapped,
mergeThunks,
loadInboxRelaySelections, loadInboxRelaySelections,
inboxRelaySelectionsByPubkey, inboxRelaySelectionsByPubkey,
} from "@welshman/app" } from "@welshman/app"
import type {AbstractThunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -44,6 +46,7 @@
import ChatMessage from "@app/components/ChatMessage.svelte" import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChatCompose.svelte" import ChatCompose from "@app/components/ChatCompose.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte" import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import { import {
INDEXER_RELAYS, INDEXER_RELAYS,
userSettingValues, userSettingValues,
@@ -53,6 +56,7 @@
} from "@app/state" } from "@app/state"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {prependParent} from "@app/commands" import {prependParent} from "@app/commands"
import {pushToast} from "@app/toast"
type Props = { type Props = {
id: string id: string
@@ -121,12 +125,23 @@
// Split the message into multiple pieces so that we can use kind 15 to send images per nip 17 // Split the message into multiple pieces so that we can use kind 15 to send images per nip 17
// Sleep 1 second between each one to make sure timestamps are distinct // Sleep 1 second between each one to make sure timestamps are distinct
const thunks: AbstractThunk[] = []
for (let i = 0; i < templates.length; i++) { for (let i = 0; i < templates.length; i++) {
const template = templates[i] const template = templates[i]
await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)}) thunks.push(
await sendWrapped({pubkeys, template, delay: $userSettingValues.send_delay + ms(i)}),
)
} }
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk: mergeThunks(thunks)},
},
})
clearParent() clearParent()
} }
+2 -2
View File
@@ -11,7 +11,7 @@
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatus from "@app/components/ThunkStatus.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte" import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte" import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
@@ -59,7 +59,7 @@
</script> </script>
{#if thunk} {#if thunk}
<ThunkStatus {thunk} class="mt-1" /> <ThunkFailure showToastOnRetry {thunk} class="mt-1" />
{/if} {/if}
<div <div
data-event={event.id} data-event={event.id}
+4 -2
View File
@@ -15,13 +15,15 @@
const {url, event, showActivity = false}: Props = $props() const {url, event, showActivity = false}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const path = makeThreadPath(url, event.id) const path = makeThreadPath(url, event.id)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)}) publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) => const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
+3 -1
View File
@@ -22,6 +22,8 @@
const {url, noun, event, hideZap, customActions}: Props = $props() const {url, noun, event, hideZap, customActions}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const showPopover = () => popover?.show() const showPopover = () => popover?.show()
const hidePopover = () => popover?.hide() const hidePopover = () => popover?.hide()
@@ -31,7 +33,7 @@
event, event,
content: emoji.unicode, content: emoji.unicode,
relays: [url], relays: [url],
protect: await canEnforceNip70(url), protect: await shouldProtect,
}) })
let popover: Instance | undefined = $state() let popover: Instance | undefined = $state()
+3 -1
View File
@@ -11,8 +11,10 @@
const {url, event}: Props = $props() const {url, event}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const confirm = async () => { const confirm = async () => {
await publishDelete({event, relays: [url], protect: await canEnforceNip70(url)}) await publishDelete({event, relays: [url], protect: await shouldProtect})
clearModals() clearModals()
} }
+3 -1
View File
@@ -14,6 +14,8 @@
const {url, event, onClose, onSubmit} = $props() const {url, event, onClose, onSubmit} = $props()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
const selectFiles = () => editor.then(ed => ed.commands.selectFiles()) const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
@@ -25,7 +27,7 @@
const content = ed.getText({blockSeparator: "\n"}).trim() const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags() const tags = ed.storage.nostr.getEditorTags()
if (await canEnforceNip70(url)) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
+3 -1
View File
@@ -10,6 +10,8 @@
const {url, event} = $props() const {url, event} = $props()
const shouldProtect = canEnforceNip70(url)
const reports = deriveEvents(repository, { const reports = deriveEvents(repository, {
filters: [{kinds: [REPORT], "#e": [event.id]}], filters: [{kinds: [REPORT], "#e": [event.id]}],
}) })
@@ -17,7 +19,7 @@
const back = () => history.back() const back = () => history.back()
const deleteReport = async (report: TrustedEvent) => { const deleteReport = async (report: TrustedEvent) => {
publishDelete({event: report, relays: [url], protect: await canEnforceNip70(url)}) publishDelete({event: report, relays: [url], protect: await shouldProtect})
if ($reports.length === 0) { if ($reports.length === 0) {
history.back() history.back()
+4 -2
View File
@@ -15,13 +15,15 @@
const {url, event, showActivity = false}: Props = $props() const {url, event, showActivity = false}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const path = makeGoalPath(url, event.id) const path = makeGoalPath(url, event.id)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)}) publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) => const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
+3 -1
View File
@@ -17,6 +17,8 @@
const {url} = $props() const {url} = $props()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
const back = () => history.back() const back = () => history.back()
@@ -50,7 +52,7 @@
["relays", url], ["relays", url],
] ]
if (await canEnforceNip70(url)) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
+5 -3
View File
@@ -10,18 +10,20 @@
const {url, event} = $props() const {url, event} = $props()
const shouldProtect = canEnforceNip70(url)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)}) publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) => const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
const onEmoji = async (emoji: NativeEmoji) => const onEmoji = async (emoji: NativeEmoji) =>
publishReaction({ publishReaction({
event, event,
content: emoji.unicode, content: emoji.unicode,
relays: [url], relays: [url],
protect: await canEnforceNip70(url), protect: await shouldProtect,
}) })
</script> </script>
+4 -2
View File
@@ -15,13 +15,15 @@
const {url, event, showActivity = false}: Props = $props() const {url, event, showActivity = false}: Props = $props()
const shouldProtect = canEnforceNip70(url)
const path = makeThreadPath(url, event.id) const path = makeThreadPath(url, event.id)
const deleteReaction = async (event: TrustedEvent) => const deleteReaction = async (event: TrustedEvent) =>
publishDelete({relays: [url], event, protect: await canEnforceNip70(url)}) publishDelete({relays: [url], event, protect: await shouldProtect})
const createReaction = async (template: EventContent) => const createReaction = async (template: EventContent) =>
publishReaction({...template, event, relays: [url], protect: await canEnforceNip70(url)}) publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script> </script>
<div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex flex-wrap items-center justify-between gap-2">
+3 -1
View File
@@ -16,6 +16,8 @@
const {url} = $props() const {url} = $props()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
const back = () => history.back() const back = () => history.back()
@@ -44,7 +46,7 @@
const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]] const tags = [...ed.storage.nostr.getEditorTags(), ["title", title]]
if (await canEnforceNip70(url)) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
+66
View File
@@ -0,0 +1,66 @@
<script lang="ts">
import {stopPropagation} from "svelte/legacy"
import {noop} from "@welshman/lib"
import {
MergedThunk,
publishThunk,
isMergedThunk,
thunkIsComplete,
getFailedThunkUrls,
} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Tippy from "@lib/components/Tippy.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
import {pushToast} from "@app/toast"
interface Props {
thunk: Thunk | MergedThunk
showToastOnRetry?: boolean
class?: string
}
let {thunk, showToastOnRetry, ...restProps}: Props = $props()
const retry = () => {
thunk = isMergedThunk(thunk)
? new MergedThunk(thunk.thunks.map(t => publishThunk(t.options)))
: publishThunk(thunk.options)
if (showToastOnRetry) {
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
}
}
const failedUrls = $derived(getFailedThunkUrls($thunk))
const showFailure = $derived(thunkIsComplete($thunk) && failedUrls.length > 0)
</script>
{#if showFailure}
{@const url = failedUrls[0]}
{@const status = $thunk.status[url]}
{@const message = $thunk.details[url]}
<button
class="flex w-full justify-end px-1 text-xs {restProps.class}"
onclick={stopPropagation(noop)}>
<Tippy
class="flex items-center"
component={ThunkStatusDetail}
props={{url, message, status, retry}}
params={{interactive: true}}>
{#snippet children()}
<span class="flex cursor-pointer items-center gap-1 text-error">
<Icon icon="danger" size={3} />
<span>Failed to send!</span>
</span>
{/snippet}
</Tippy>
</button>
{/if}
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
import {stopPropagation} from "svelte/legacy"
import {PublishStatus} from "@welshman/net"
import type {AbstractThunk} from "@welshman/app"
import {abortThunk, thunkHasStatus} from "@welshman/app"
interface Props {
thunk: AbstractThunk
class?: string
}
const {thunk, ...restProps}: Props = $props()
const abort = () => abortThunk(thunk)
const isSending = $derived(thunkHasStatus(PublishStatus.Sending, $thunk))
</script>
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
<span class="flex items-center gap-1">
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px"></span>
<span class="opacity-50">Sending...</span>
<button
type="button"
class="underline transition-all"
class:link={isSending}
class:opacity-25={!isSending}
onclick={stopPropagation(abort)}>
Cancel
</button>
</span>
</div>
+8 -81
View File
@@ -1,95 +1,22 @@
<script lang="ts"> <script lang="ts">
import {stopPropagation} from "svelte/legacy" import {MergedThunk, thunkIsComplete, getFailedThunkUrls} from "@welshman/app"
import {nth, noop} from "@welshman/lib"
import {PublishStatus} from "@welshman/net"
import {
MergedThunk,
publishThunk,
isMergedThunk,
thunkIsComplete,
thunkHasStatus,
} from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
import Tippy from "@lib/components/Tippy.svelte" import ThunkPending from "@app/components/ThunkPending.svelte"
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
import {userSettingValues} from "@app/state"
interface Props { interface Props {
thunk: Thunk | MergedThunk thunk: Thunk | MergedThunk
class?: string class?: string
} }
let {thunk, ...restProps}: Props = $props() const {thunk, ...restProps}: Props = $props()
const abort = () => thunk.controller.abort() const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
const showPending = $derived(!thunkIsComplete($thunk))
const retry = () => {
thunk = isMergedThunk(thunk)
? new MergedThunk(thunk.thunks.map(t => publishThunk(t.options)))
: publishThunk(thunk.options)
}
const statuses = $derived(Object.entries($thunk.status))
const isSending = $derived(thunkHasStatus($thunk, PublishStatus.Sending))
const canCancel = $derived(isSending && $userSettingValues.send_delay > 0)
const failedUrls = $derived(
statuses
.filter(([_, status]) => [PublishStatus.Failure, PublishStatus.Timeout].includes(status))
.map(nth(0)),
)
const showFailure = $derived(thunkIsComplete($thunk) && failedUrls.length > 0)
let isPending = $state(thunkHasStatus($thunk, PublishStatus.Pending))
const showPending = $derived(canCancel || isPending)
// Delay updating isPending so users can see that the message is sent
$effect(() => {
isPending = isPending || thunkHasStatus($thunk, PublishStatus.Pending)
if (!thunkHasStatus($thunk, PublishStatus.Pending)) {
setTimeout(() => {
isPending = false
}, 2000)
}
})
</script> </script>
{#if showFailure} {#if showFailure}
{@const url = failedUrls[0]} <ThunkFailure class={restProps.class} {thunk} />
{@const status = $thunk.status[url]}
{@const message = $thunk.details[url]}
<button
class="flex w-full justify-end px-1 text-xs {restProps.class}"
onclick={stopPropagation(noop)}>
<Tippy
class="flex items-center"
component={ThunkStatusDetail}
props={{url, message, status, retry}}
params={{interactive: true}}>
{#snippet children()}
<span class="flex cursor-pointer items-center gap-1 text-error">
<Icon icon="danger" size={3} />
<span>Failed to send!</span>
</span>
{/snippet}
</Tippy>
</button>
{:else if showPending} {:else if showPending}
<div class="flex w-full justify-end px-1 text-xs {restProps.class}"> <ThunkPending class={restProps.class} {thunk} />
<span class="flex items-center gap-1">
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px"></span>
<span class="opacity-50">Sending...</span>
<button
type="button"
class="underline transition-all"
class:link={canCancel}
class:opacity-25={!canCancel}
onclick={stopPropagation(abort)}>
Cancel
</button>
</span>
</div>
{/if} {/if}
+39
View File
@@ -0,0 +1,39 @@
<script lang="ts">
import type {AbstractThunk} from "@welshman/app"
import {thunkHasStatus, thunkIsComplete} from "@welshman/app"
import {PublishStatus} from "@welshman/net"
import ThunkPending from "@app/components/ThunkPending.svelte"
import type {Toast} from "@app/toast"
import {popToast} from "@app/toast"
type Props = {
toast: Toast
thunk: AbstractThunk
}
const {toast, ...props}: Props = $props()
const id = toast.id
const thunk = props.thunk
const {Aborted, Timeout, Failure} = PublishStatus
const isFailure = $derived(thunkHasStatus([Aborted, Timeout, Failure], $thunk))
const isComplete = $derived(thunkIsComplete($thunk))
$effect(() => {
if (isFailure) {
popToast(id)
}
})
$effect(() => {
if (isComplete) {
setTimeout(() => popToast(id), 2000)
}
})
</script>
{#if !isComplete}
<ThunkPending {thunk} />
{:else if !isFailure}
<p class="text-xs opacity-75">Message sent!</p>
{/if}
+11 -6
View File
@@ -21,12 +21,17 @@
class:bg-base-100={theme === "info"} class:bg-base-100={theme === "info"}
class:text-base-content={theme === "info"} class:text-base-content={theme === "info"}
class:alert-error={theme === "error"}> class:alert-error={theme === "error"}>
<p class="welshman-content-error"> <p class:welshman-content-error={theme === "error"}>
{@html renderAsHtml(parse({content: $toast.message}))} {#if $toast.message}
{#if $toast.action} {@html renderAsHtml(parse({content: $toast.message}))}
<Button class="cursor-pointer underline" onclick={onActionClick}> {#if $toast.action}
{$toast.action.message} <Button class="cursor-pointer underline" onclick={onActionClick}>
</Button> {$toast.action.message}
</Button>
{/if}
{:else if $toast.children}
{@const {component: Component, props} = $toast?.children || {}}
<Component toast={$toast} {...props} />
{/if} {/if}
</p> </p>
<Button class="flex items-center opacity-75" onclick={() => popToast($toast.id)}> <Button class="flex items-center opacity-75" onclick={() => popToast($toast.id)}>
+2 -2
View File
@@ -86,7 +86,7 @@ import {
userFollows, userFollows,
ensurePlaintext, ensurePlaintext,
thunks, thunks,
walkThunks, flattenThunks,
signer, signer,
makeOutboxLoader, makeOutboxLoader,
appContext, appContext,
@@ -260,7 +260,7 @@ export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thun
const getThunksByEventId = memoize(() => { const getThunksByEventId = memoize(() => {
const thunksByEventId = new Map<string, Thunk[]>() const thunksByEventId = new Map<string, Thunk[]>()
for (const thunk of walkThunks(Object.values($thunks))) { for (const thunk of flattenThunks(Object.values($thunks))) {
pushToMapKey(thunksByEventId, thunk.event.id, thunk) pushToMapKey(thunksByEventId, thunk.event.id, thunk)
} }
+6 -1
View File
@@ -1,11 +1,16 @@
import type {Component} from "svelte"
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId} from "@welshman/lib" import {randomId} from "@welshman/lib"
import {copyToClipboard} from "@lib/html" import {copyToClipboard} from "@lib/html"
export type ToastParams = { export type ToastParams = {
message: string message?: string
timeout?: number timeout?: number
theme?: "error" theme?: "error"
children?: {
component: Component<any>
props: Record<string, any>
}
action?: { action?: {
message: string message: string
onclick: () => void onclick: () => void
+12 -2
View File
@@ -23,6 +23,7 @@
import PageBar from "@lib/components/PageBar.svelte" import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte" import PageContent from "@lib/components/PageContent.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ChannelName from "@app/components/ChannelName.svelte" import ChannelName from "@app/components/ChannelName.svelte"
import ChannelMessage from "@app/components/ChannelMessage.svelte" import ChannelMessage from "@app/components/ChannelMessage.svelte"
@@ -57,6 +58,7 @@
const channel = deriveChannel(url, room) const channel = deriveChannel(url, room)
const filter = {kinds: [MESSAGE], "#h": [room]} const filter = {kinds: [MESSAGE], "#h": [room]}
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room)) const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
const shouldProtect = canEnforceNip70(url)
const membershipStatus = deriveUserMembershipStatus(url, room) const membershipStatus = deriveUserMembershipStatus(url, room)
const addFavorite = () => addRoomMembership(url, room) const addFavorite = () => addRoomMembership(url, room)
@@ -109,7 +111,7 @@
const onSubmit = async ({content, tags}: EventContent) => { const onSubmit = async ({content, tags}: EventContent) => {
tags.push(["h", room]) tags.push(["h", room])
if (await canEnforceNip70(url)) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
@@ -123,12 +125,20 @@
template = prependParent(parent, template) template = prependParent(parent, template)
} }
publishThunk({ const thunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(MESSAGE, template), event: makeEvent(MESSAGE, template),
delay: $userSettingValues.send_delay, delay: $userSettingValues.send_delay,
}) })
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
clearParent() clearParent()
clearShare() clearShare()
} }
+21 -5
View File
@@ -14,14 +14,21 @@
import PageBar from "@lib/components/PageBar.svelte" import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte" import PageContent from "@lib/components/PageContent.svelte"
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import ThunkToast from "@app/components/ThunkToast.svelte"
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte" import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
import ChannelMessage from "@app/components/ChannelMessage.svelte" import ChannelMessage from "@app/components/ChannelMessage.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte"
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte" import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
import {userSettingValues, decodeRelay, getEventsForUrl} from "@app/state" import {
import {setChecked, checked} from "@app/notifications" userSettingValues,
decodeRelay,
getEventsForUrl,
PROTECTED,
REACTION_KINDS,
} from "@app/state"
import {prependParent, canEnforceNip70} from "@app/commands" import {prependParent, canEnforceNip70} from "@app/commands"
import {PROTECTED, REACTION_KINDS} from "@app/state" import {setChecked, checked} from "@app/notifications"
import {pushToast} from "@app/toast"
import {makeFeed} from "@app/requests" import {makeFeed} from "@app/requests"
import {popKey} from "@app/implicit" import {popKey} from "@app/implicit"
@@ -29,6 +36,7 @@
const lastChecked = $checked[$page.url.pathname] const lastChecked = $checked[$page.url.pathname]
const url = decodeRelay($page.params.relay) const url = decodeRelay($page.params.relay)
const filter = {kinds: [MESSAGE]} const filter = {kinds: [MESSAGE]}
const shouldProtect = canEnforceNip70(url)
const replyTo = (event: TrustedEvent) => { const replyTo = (event: TrustedEvent) => {
parent = event parent = event
@@ -44,7 +52,7 @@
} }
const onSubmit = async ({content, tags}: EventContent) => { const onSubmit = async ({content, tags}: EventContent) => {
if (await canEnforceNip70(url)) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
} }
@@ -58,12 +66,20 @@
template = prependParent(parent, template) template = prependParent(parent, template)
} }
publishThunk({ const thunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(MESSAGE, template), event: makeEvent(MESSAGE, template),
delay: $userSettingValues.send_delay, delay: $userSettingValues.send_delay,
}) })
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk},
},
})
clearParent() clearParent()
clearShare() clearShare()
} }