feat: show per-relay publish status on outgoing messages
This commit is contained in:
@@ -280,7 +280,7 @@
|
||||
</div>
|
||||
</PageBar>
|
||||
|
||||
<PageContent class="flex flex-col-reverse gap-2 py-4">
|
||||
<PageContent class="flex flex-col-reverse gap-2 py-4 md:pb-8!">
|
||||
{#if missingRelayLists.length > 0}
|
||||
<div class="py-12">
|
||||
<div class="card2 col-2 m-auto max-w-md items-center text-center">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import Content from "@app/components/Content.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||
@@ -65,9 +65,6 @@
|
||||
let popoverIsVisible = $state(false)
|
||||
</script>
|
||||
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-1" />
|
||||
{/if}
|
||||
<div
|
||||
data-event={event.id}
|
||||
class="group chat flex items-center justify-end gap-1 px-2"
|
||||
@@ -125,6 +122,9 @@
|
||||
<Content showEntire {event} />
|
||||
</div>
|
||||
</TapTarget>
|
||||
{#if isOwn && thunk}
|
||||
<ThunkStatus showToastOnRetry {thunk} class="mb-2 mr-2" />
|
||||
{/if}
|
||||
<div class="row-2 z-feature -mt-4 ml-4">
|
||||
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ReactionSummary from "@app/components/ReactionSummary.svelte"
|
||||
@@ -126,8 +126,8 @@
|
||||
{/if}
|
||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||
<RoomItemContent {url} event={$innerEvent ?? event} />
|
||||
{#if thunk}
|
||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||
{#if event.pubkey === $pubkey && thunk}
|
||||
<ThunkStatus showToastOnRetry {thunk} class="mt-1 flex justify-end" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script lang="ts">
|
||||
import {stopPropagation} from "svelte/legacy"
|
||||
import type {AbstractThunk} from "@welshman/app"
|
||||
import {
|
||||
flattenThunks,
|
||||
getFailedThunkUrls,
|
||||
getThunkUrlsWithStatus,
|
||||
publishThunk,
|
||||
} from "@welshman/app"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||
import Close from "@assets/icons/close.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 {addPeriod} from "@lib/util"
|
||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||
import {pushToast} from "@app/util/toast"
|
||||
|
||||
type DeliveryState = "delivered" | "partial" | "failed"
|
||||
|
||||
interface Props {
|
||||
thunk: AbstractThunk
|
||||
deliveryState: DeliveryState
|
||||
hide: () => void
|
||||
showToastOnRetry?: boolean
|
||||
}
|
||||
|
||||
const {thunk, deliveryState, hide, showToastOnRetry}: Props = $props()
|
||||
|
||||
const successUrls = $derived(getThunkUrlsWithStatus(PublishStatus.Success, $thunk))
|
||||
const failedUrls = $derived([
|
||||
...getFailedThunkUrls($thunk),
|
||||
...getThunkUrlsWithStatus(PublishStatus.Aborted, $thunk),
|
||||
])
|
||||
const total = $derived(successUrls.length + failedUrls.length)
|
||||
|
||||
const title = $derived(
|
||||
deliveryState === "delivered"
|
||||
? "Message delivered"
|
||||
: deliveryState === "partial"
|
||||
? `Partial delivery ${successUrls.length}/${total} relays`
|
||||
: "Failed to deliver!",
|
||||
)
|
||||
|
||||
const titleIconClass = $derived(
|
||||
deliveryState === "delivered"
|
||||
? "text-success"
|
||||
: deliveryState === "partial"
|
||||
? "text-warning"
|
||||
: "text-error",
|
||||
)
|
||||
|
||||
const titleIcon = $derived(deliveryState === "delivered" ? CheckCircle : Danger)
|
||||
|
||||
const relayMessage = (
|
||||
url: string,
|
||||
status: PublishStatus | undefined,
|
||||
detail: string | undefined,
|
||||
) => {
|
||||
if (detail) {
|
||||
return detail
|
||||
}
|
||||
|
||||
if (status === PublishStatus.Timeout) {
|
||||
return "request timed out"
|
||||
}
|
||||
|
||||
return "no details received"
|
||||
}
|
||||
|
||||
const retryRelay = (url: string) => {
|
||||
for (const child of flattenThunks([thunk])) {
|
||||
if (!child.options.relays.includes(url)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const retried = publishThunk({...child.options, relays: [url]})
|
||||
|
||||
if (showToastOnRetry) {
|
||||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk: retried},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt flex min-w-72 max-w-sm flex-col gap-3 px-4 py-3 shadow-lg">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="flex items-center gap-2 text-sm font-medium">
|
||||
<Icon icon={titleIcon} class={titleIconClass} size={4} />
|
||||
{title}
|
||||
</span>
|
||||
<button type="button" class="opacity-50 hover:opacity-100" onclick={hide}>
|
||||
<Icon icon={Close} size={4} />
|
||||
</button>
|
||||
</div>
|
||||
<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(url, status, detail))}</p>
|
||||
</div>
|
||||
<Button class="link shrink-0 px-1" onclick={stopPropagation(() => retryRelay(url))}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {stopPropagation} from "svelte/legacy"
|
||||
import {noop} from "@welshman/lib"
|
||||
import type {AbstractThunk} from "@welshman/app"
|
||||
import {retryThunk, thunkIsComplete, getFailedThunkUrls} from "@welshman/app"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
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/util/toast"
|
||||
import ThunkStatus from "@app/components/ThunkStatus.svelte"
|
||||
|
||||
interface Props {
|
||||
thunk: AbstractThunk
|
||||
@@ -16,43 +8,7 @@
|
||||
class?: string
|
||||
}
|
||||
|
||||
let {thunk, showToastOnRetry, ...restProps}: Props = $props()
|
||||
|
||||
const retry = () => {
|
||||
thunk = retryThunk(thunk)
|
||||
|
||||
if (showToastOnRetry) {
|
||||
pushToast({
|
||||
timeout: 30_000,
|
||||
children: {
|
||||
component: ThunkToast,
|
||||
props: {thunk},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const failedUrls = $derived(getFailedThunkUrls($thunk))
|
||||
const showFailure = $derived(thunkIsComplete($thunk) && failedUrls.length > 0)
|
||||
const props: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if showFailure}
|
||||
{@const url = failedUrls[0]}
|
||||
{@const {status, detail: message} = $thunk.results[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}
|
||||
<ThunkStatus {...props} />
|
||||
|
||||
@@ -1,22 +1,126 @@
|
||||
<script lang="ts">
|
||||
import type {Instance} from "tippy.js"
|
||||
import {stopPropagation} from "svelte/legacy"
|
||||
import {noop} from "@welshman/lib"
|
||||
import type {AbstractThunk} from "@welshman/app"
|
||||
import {thunkIsComplete, getFailedThunkUrls} from "@welshman/app"
|
||||
import ThunkFailure from "@app/components/ThunkFailure.svelte"
|
||||
import ThunkPending from "@app/components/ThunkPending.svelte"
|
||||
import {
|
||||
abortThunk,
|
||||
getFailedThunkUrls,
|
||||
getThunkUrlsWithStatus,
|
||||
thunkHasStatus,
|
||||
thunkIsComplete,
|
||||
} from "@welshman/app"
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import Check from "@assets/icons/check.svg?dataurl"
|
||||
import CheckRead from "@assets/icons/check-read.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import ThunkDeliveryBreakdown from "@app/components/ThunkDeliveryBreakdown.svelte"
|
||||
|
||||
type DeliveryState = "sending" | "delivered" | "partial" | "failed"
|
||||
|
||||
interface Props {
|
||||
thunk: AbstractThunk
|
||||
showToastOnRetry?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
const {thunk, ...restProps}: Props = $props()
|
||||
const {thunk, showToastOnRetry, ...restProps}: Props = $props()
|
||||
|
||||
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
|
||||
const showPending = $derived(!thunkIsComplete($thunk))
|
||||
const successUrls = $derived(getThunkUrlsWithStatus(PublishStatus.Success, $thunk))
|
||||
const failedUrls = $derived([
|
||||
...getFailedThunkUrls($thunk),
|
||||
...getThunkUrlsWithStatus(PublishStatus.Aborted, $thunk),
|
||||
])
|
||||
const isComplete = $derived(thunkIsComplete($thunk))
|
||||
const isSending = $derived(thunkHasStatus(PublishStatus.Sending, $thunk))
|
||||
|
||||
const deliveryState = $derived.by((): DeliveryState => {
|
||||
if (!isComplete) {
|
||||
return "sending"
|
||||
}
|
||||
|
||||
if (successUrls.length === 0 && failedUrls.length > 0) {
|
||||
return "failed"
|
||||
}
|
||||
|
||||
if (failedUrls.length > 0) {
|
||||
return "partial"
|
||||
}
|
||||
|
||||
return "delivered"
|
||||
})
|
||||
|
||||
const statusLabel = $derived(
|
||||
deliveryState === "sending"
|
||||
? "Sending..."
|
||||
: deliveryState === "delivered"
|
||||
? "Message delivered"
|
||||
: deliveryState === "partial"
|
||||
? "Partial delivery"
|
||||
: "Failed to deliver!",
|
||||
)
|
||||
|
||||
const statusIcon = $derived(
|
||||
deliveryState === "delivered" ? CheckRead : deliveryState === "partial" ? Check : Danger,
|
||||
)
|
||||
|
||||
const statusIconClass = $derived(
|
||||
deliveryState === "delivered"
|
||||
? "text-success"
|
||||
: deliveryState === "partial"
|
||||
? "text-warning"
|
||||
: deliveryState === "failed"
|
||||
? "text-error"
|
||||
: "",
|
||||
)
|
||||
|
||||
const showBreakdown = $derived(isComplete)
|
||||
|
||||
const breakdownState = $derived(
|
||||
deliveryState === "delivered" || deliveryState === "partial" || deliveryState === "failed"
|
||||
? deliveryState
|
||||
: "delivered",
|
||||
)
|
||||
|
||||
const abort = () => abortThunk(thunk)
|
||||
|
||||
let popover: Instance | undefined = $state()
|
||||
|
||||
const hideBreakdown = () => popover?.hide()
|
||||
</script>
|
||||
|
||||
{#if showFailure}
|
||||
<ThunkFailure class={restProps.class} {thunk} />
|
||||
{:else if showPending}
|
||||
<ThunkPending class={restProps.class} {thunk} />
|
||||
{/if}
|
||||
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
|
||||
{#if deliveryState === "sending"}
|
||||
<span class="flex items-center gap-1 opacity-50">
|
||||
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px"></span>
|
||||
<span>{statusLabel}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="underline transition-all"
|
||||
class:link={isSending}
|
||||
class:opacity-25={!isSending}
|
||||
class:pointer-events-none={!isSending}
|
||||
onclick={stopPropagation(abort)}>
|
||||
Cancel
|
||||
</button>
|
||||
</span>
|
||||
{:else if showBreakdown}
|
||||
<button type="button" class="flex items-center" onclick={stopPropagation(noop)}>
|
||||
<Tippy
|
||||
bind:popover
|
||||
class="flex items-center"
|
||||
component={ThunkDeliveryBreakdown}
|
||||
props={{thunk, deliveryState: breakdownState, hide: hideBreakdown, showToastOnRetry}}
|
||||
params={{interactive: true, maxWidth: "none"}}>
|
||||
{#snippet children()}
|
||||
<span class="flex cursor-pointer items-center gap-1 opacity-75">
|
||||
<Icon icon={statusIcon} class={statusIconClass} size={3} />
|
||||
<span>{statusLabel}</span>
|
||||
</span>
|
||||
{/snippet}
|
||||
</Tippy>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {PublishStatus} from "@welshman/net"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {addPeriod} from "@lib/util"
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
status: string
|
||||
message: string
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
let {url, status, message = $bindable(), retry}: Props = $props()
|
||||
|
||||
$effect(() => {
|
||||
if (!message && status === PublishStatus.Timeout) {
|
||||
message = "request timed out"
|
||||
}
|
||||
|
||||
if (!message) {
|
||||
message = "no details recieved"
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="card2 bg-alt col-2 shadow-lg">
|
||||
<p>
|
||||
Failed to publish to {displayRelayUrl(url)}: {addPeriod(message)}
|
||||
</p>
|
||||
<Button class="link" onclick={retry}>Retry</Button>
|
||||
</div>
|
||||
@@ -470,7 +470,7 @@
|
||||
bind:element
|
||||
onscroll={onScroll}
|
||||
class={cx(
|
||||
"flex-col-reverse pb-0! pt-4",
|
||||
"flex-col-reverse pb-0! pt-4 md:pb-8!",
|
||||
showMobileVideoPanel ? "hidden md:flex md:flex-col-reverse" : "flex",
|
||||
pageContentHiddenDesktopVideoOnly && "md:hidden",
|
||||
)}>
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4 pb-0!">
|
||||
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4 pb-0! md:pb-8!">
|
||||
{#if loadingForward}
|
||||
<p class="py-20 flex justify-center">
|
||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||
|
||||
Reference in New Issue
Block a user