feat: show per-relay failure, pending breakdown on outgoing messages

This commit is contained in:
2026-06-01 15:36:38 +05:30
parent 1dd0270f4f
commit caf3f620e4
5 changed files with 86 additions and 43 deletions
+3 -3
View File
@@ -65,9 +65,6 @@
let popoverIsVisible = $state(false) let popoverIsVisible = $state(false)
</script> </script>
{#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-1" />
{/if}
<div <div
data-event={event.id} data-event={event.id}
class="group chat flex items-center justify-end gap-1 px-2" class="group chat flex items-center justify-end gap-1 px-2"
@@ -125,6 +122,9 @@
<Content showEntire {event} /> <Content showEntire {event} />
</div> </div>
</TapTarget> </TapTarget>
{#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mb-2 mr-2" />
{/if}
<div class="row-2 z-feature -mt-4 ml-4"> <div class="row-2 z-feature -mt-4 ml-4">
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip /> <ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
</div> </div>
+1 -1
View File
@@ -127,7 +127,7 @@
<div class:mt-2={showPubkey && event.kind !== MESSAGE}> <div class:mt-2={showPubkey && event.kind !== MESSAGE}>
<RoomItemContent {url} event={$innerEvent ?? event} /> <RoomItemContent {url} event={$innerEvent ?? event} />
{#if thunk} {#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" /> <ThunkFailure showToastOnRetry {thunk} class="mt-1 flex justify-end" />
{/if} {/if}
</div> </div>
</div> </div>
+26 -21
View File
@@ -2,7 +2,7 @@
import {stopPropagation} from "svelte/legacy" import {stopPropagation} from "svelte/legacy"
import {noop} from "@welshman/lib" import {noop} from "@welshman/lib"
import type {AbstractThunk} from "@welshman/app" import type {AbstractThunk} from "@welshman/app"
import {retryThunk, thunkIsComplete, getFailedThunkUrls} from "@welshman/app" import {flattenThunks, getFailedThunkUrls, publishThunk, thunkIsComplete} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
@@ -16,40 +16,45 @@
class?: string class?: string
} }
let {thunk, showToastOnRetry, ...restProps}: Props = $props() const {thunk, showToastOnRetry, ...restProps}: Props = $props()
const retry = () => { const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
thunk = retryThunk(thunk)
if (showToastOnRetry) { const retry = (url: string) => {
pushToast({ for (const child of flattenThunks([thunk])) {
timeout: 30_000, if (!child.options.relays.includes(url)) {
children: { continue
component: ThunkToast, }
props: {thunk},
}, const retried = publishThunk({...child.options, relays: [url]})
})
if (showToastOnRetry) {
pushToast({
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk: retried},
},
})
}
return
} }
} }
const failedUrls = $derived(getFailedThunkUrls($thunk))
const showFailure = $derived(thunkIsComplete($thunk) && failedUrls.length > 0)
</script> </script>
{#if showFailure} {#if showFailure}
{@const url = failedUrls[0]}
{@const {status, detail: message} = $thunk.results[url]}
<button <button
class="flex w-full justify-end px-1 text-xs {restProps.class}" class="flex w-full justify-end px-1 text-xs {restProps.class}"
onclick={stopPropagation(noop)}> onclick={stopPropagation(noop)}>
<Tippy <Tippy
class="flex items-center" class="flex items-center"
component={ThunkStatusDetail} component={ThunkStatusDetail}
props={{url, message, status, retry}} props={{thunk, retry}}
params={{interactive: true}}> params={{interactive: true, maxWidth: "none"}}>
{#snippet children()} {#snippet children()}
<span class="flex cursor-pointer items-center gap-1 text-error"> <span class="flex cursor-pointer items-center gap-1 opacity-75">
<Icon icon={Danger} size={3} /> <Icon icon={Danger} class="text-error" size={3} />
<span>Failed to send!</span> <span>Failed to send!</span>
</span> </span>
{/snippet} {/snippet}
+3 -2
View File
@@ -6,17 +6,18 @@
interface Props { interface Props {
thunk: AbstractThunk thunk: AbstractThunk
showToastOnRetry?: boolean
class?: string class?: string
} }
const {thunk, ...restProps}: Props = $props() const {thunk, showToastOnRetry, ...restProps}: Props = $props()
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0) const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
const showPending = $derived(!thunkIsComplete($thunk)) const showPending = $derived(!thunkIsComplete($thunk))
</script> </script>
{#if showFailure} {#if showFailure}
<ThunkFailure class={restProps.class} {thunk} /> <ThunkFailure class={restProps.class} {thunk} {showToastOnRetry} />
{:else if showPending} {:else if showPending}
<ThunkPending class={restProps.class} {thunk} /> <ThunkPending class={restProps.class} {thunk} />
{/if} {/if}
+53 -16
View File
@@ -1,32 +1,69 @@
<script lang="ts"> <script lang="ts">
import {stopPropagation} from "svelte/legacy"
import type {AbstractThunk} from "@welshman/app"
import {getFailedThunkUrls, getThunkUrlsWithStatus} from "@welshman/app"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import {addPeriod} from "@lib/util" import {addPeriod} from "@lib/util"
interface Props { interface Props {
url: string thunk: AbstractThunk
status: string retry: (url: string) => void
message: string
retry: () => void
} }
let {url, status, message = $bindable(), retry}: Props = $props() const {thunk, retry}: Props = $props()
$effect(() => { const successUrls = $derived(getThunkUrlsWithStatus(PublishStatus.Success, $thunk))
if (!message && status === PublishStatus.Timeout) { const failedUrls = $derived(getFailedThunkUrls($thunk))
message = "request timed out" const total = $derived(successUrls.length + failedUrls.length)
const isPartial = $derived(successUrls.length > 0 && failedUrls.length > 0)
const title = $derived(
isPartial ? `Partial delivery ${successUrls.length}/${total} relays` : "Failed to send!",
)
const relayMessage = (status: PublishStatus | undefined, detail: string | undefined) => {
if (detail) {
return detail
} }
if (!message) { if (status === PublishStatus.Timeout) {
message = "no details recieved" return "request timed out"
} }
})
return "no details received"
}
</script> </script>
<div class="card2 bg-alt col-2 shadow-lg"> <div class="card2 bg-alt flex min-w-72 max-w-sm flex-col gap-3 px-4 py-3 shadow-lg">
<p> <span class="flex items-center gap-2 text-sm font-medium">
Failed to publish to {displayRelayUrl(url)}: {addPeriod(message)} <Icon icon={Danger} class="text-error" size={4} />
</p> {title}
<Button class="link" onclick={retry}>Retry</Button> </span>
<div class="divider my-0"></div>
<div class="flex flex-col gap-3">
{#each successUrls as url (url)}
<div class="flex items-start gap-2 text-sm">
<Icon icon={CheckCircle} class="mt-0.5 shrink-0 text-success" size={4} />
<span>{displayRelayUrl(url)}</span>
</div>
{/each}
{#each failedUrls as url (url)}
{@const {detail, status} = $thunk.results[url] || {}}
<div class="grid grid-cols-[1rem_1fr_auto] items-start gap-x-3 gap-y-1 text-sm">
<Icon icon={Danger} class="mt-0.5 text-error" size={4} />
<div class="min-w-0">
<p class="break-all">{displayRelayUrl(url)}</p>
<p class="text-xs opacity-60">{addPeriod(relayMessage(status, detail))}</p>
</div>
<Button class="link shrink-0 px-1" onclick={stopPropagation(() => retry(url))}>
Retry
</Button>
</div>
{/each}
</div>
</div> </div>