Update to new thunk stuff

This commit is contained in:
Jon Staab
2025-04-14 16:45:52 -07:00
parent d5b1fab1e7
commit 394a1e7d30
4 changed files with 102 additions and 68 deletions
+27 -20
View File
@@ -1,6 +1,6 @@
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {get} from "svelte/store" import {get} from "svelte/store"
import {randomId, poll, uniq, equals} from "@welshman/lib" import {randomId, ifLet, poll, uniq, equals} from "@welshman/lib"
import { import {
DELETE, DELETE,
REPORT, REPORT,
@@ -38,7 +38,7 @@ import {
signer, signer,
repository, repository,
publishThunk, publishThunk,
publishThunks, MergedThunk,
profilesByPubkey, profilesByPubkey,
relaySelectionsByPubkey, relaySelectionsByPubkey,
getWriteRelayUrls, getWriteRelayUrls,
@@ -54,6 +54,7 @@ import {
tagEventForComment, tagEventForComment,
tagEventForQuote, tagEventForQuote,
Router, Router,
thunkIsComplete,
} from "@welshman/app" } from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import { import {
@@ -84,14 +85,20 @@ export const getPubkeyPetname = (pubkey: string) => {
return display return display
} }
export const getThunkError = async (thunk: Thunk) => { export const getThunkError = (thunk: Thunk) =>
const result = await thunk.result new Promise<string>(resolve => {
const [{status, message}] = Object.values(result) as any thunk.subscribe($thunk => {
for (const [relay, status] of Object.entries($thunk.status)) {
if (status === PublishStatus.Failure) {
resolve($thunk.details[relay])
}
}
if (status !== PublishStatus.Success) { if (thunkIsComplete($thunk)) {
return message resolve("")
} }
} })
})
export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => { export const prependParent = (parent: TrustedEvent | undefined, {content, tags}: EventContent) => {
if (parent) { if (parent) {
@@ -261,12 +268,10 @@ export const checkRelayAccess = async (url: string, claim = "") => {
relays: [url], relays: [url],
}) })
const result = await thunk.result ifLet(await getThunkError(thunk), error => {
if (result[url].status === PublishStatus.Failure) {
const message = const message =
socket.auth.details?.replace(/^.*: /, "") || socket.auth.details?.replace(/^.*: /, "") ||
result[url].message?.replace(/^.*: /, "") || error?.replace(/^.*: /, "") ||
"join request rejected" "join request rejected"
// If it's a strict NIP 29 relay don't worry about requesting access // If it's a strict NIP 29 relay don't worry about requesting access
@@ -274,7 +279,7 @@ export const checkRelayAccess = async (url: string, claim = "") => {
if (message !== "missing group (`h`) tag") { if (message !== "missing group (`h`) tag") {
return `Failed to join relay (${message})` return `Failed to join relay (${message})`
} }
} })
} }
export const checkRelayProfile = async (url: string) => { export const checkRelayProfile = async (url: string) => {
@@ -342,13 +347,15 @@ export const sendWrapped = async ({
}) => { }) => {
const nip59 = Nip59.fromSigner(signer.get()!) const nip59 = Nip59.fromSigner(signer.get()!)
return publishThunks( return new MergedThunk(
await Promise.all( await Promise.all(
uniq(pubkeys).map(async recipient => ({ uniq(pubkeys).map(async recipient =>
event: await nip59.wrap(recipient, stamp(template)), publishThunk({
relays: Router.get().PubkeyInbox(recipient).getUrls(), event: await nip59.wrap(recipient, stamp(template)),
delay, relays: Router.get().PubkeyInbox(recipient).getUrls(),
})), delay,
}),
),
), ),
) )
} }
+4 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {hash} from "@welshman/lib" import {hash, now} from "@welshman/lib"
import {now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import { import {
thunks, thunks,
@@ -9,6 +8,7 @@
deriveProfileDisplay, deriveProfileDisplay,
formatTimestampAsDate, formatTimestampAsDate,
formatTimestampAsTime, formatTimestampAsTime,
thunkIsComplete,
} from "@welshman/app" } from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import TapTarget from "@lib/components/TapTarget.svelte" import TapTarget from "@lib/components/TapTarget.svelte"
@@ -42,6 +42,7 @@
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const hideMenuButton = $derived($thunk && !thunkIsComplete($thunk))
const reply = () => replyTo(event) const reply = () => replyTo(event)
@@ -99,7 +100,7 @@
<div class="row-2 ml-10 mt-1"> <div class="row-2 ml-10 mt-1">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" /> <ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
</div> </div>
{#if !isMobile} {#if !isMobile && !hideMenuButton}
<button <button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all" class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile}> class:group-hover:opacity-100={!isMobile}>
+64 -43
View File
@@ -1,12 +1,17 @@
<script lang="ts"> <script lang="ts">
import {get} from "svelte/store" import {nth} from "@welshman/lib"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {mergeThunks, publishThunk} from "@welshman/app" import {
import type {Thunk, MergedThunk} from "@welshman/app" MergedThunk,
import {throttled} from "@welshman/store" publishThunk,
isMergedThunk,
thunkIsComplete,
thunkHasStatus,
} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import {fade} from "@lib/transition"
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"
import Button from "@lib/components/Button.svelte"
import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte" import ThunkStatusDetail from "@app/components/ThunkStatusDetail.svelte"
import {userSettingValues} from "@app/state" import {userSettingValues} from "@app/state"
@@ -17,31 +22,34 @@
let {thunk, ...restProps}: Props = $props() let {thunk, ...restProps}: Props = $props()
const {Pending, Failure, Timeout} = PublishStatus
const abort = () => thunk.controller.abort() const abort = () => thunk.controller.abort()
const retry = () => { const retry = () => {
thunk = (thunk as any).thunks thunk = isMergedThunk(thunk)
? mergeThunks((thunk as MergedThunk).thunks.map(t => publishThunk(t.request))) ? new MergedThunk(thunk.thunks.map(t => publishThunk(t.options)))
: publishThunk((thunk as Thunk).request) : publishThunk(thunk.options)
} }
const status = $derived(throttled(300, thunk.status)) const statuses = $derived(Object.entries($thunk.status))
const ps = $derived(Object.values($status)) const isSending = $derived(thunkHasStatus($thunk, PublishStatus.Sending))
const canCancel = $derived(ps.length === 0 && $userSettingValues.send_delay > 0) const canCancel = $derived(isSending && $userSettingValues.send_delay > 0)
const isFailure = $derived(!canCancel && ps.every(s => [Failure, Timeout].includes(s.status))) const failedUrls = $derived(
const failure = $derived( statuses
Object.entries($status).find(([url, s]) => [Failure, Timeout].includes(s.status)), .filter(([_, status]) => [PublishStatus.Failure, PublishStatus.Timeout].includes(status))
.map(nth(1)),
) )
let isPending = $state(Object.values(get(thunk.status)).some(s => s.status == Pending)) 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 // Delay updating isPending so users can see that the message is sent
$effect(() => { $effect(() => {
isPending = isPending || ps.some(s => s.status === Pending) isPending = isPending || thunkHasStatus($thunk, PublishStatus.Pending)
if (!ps.some(s => s.status === Pending)) { if (!thunkHasStatus($thunk, PublishStatus.Pending)) {
setTimeout(() => { setTimeout(() => {
isPending = false isPending = false
}, 2000) }, 2000)
@@ -49,30 +57,43 @@
}) })
</script> </script>
{#if isFailure && failure} {#if showFailure || showPending}
{@const [url, {message, status}] = failure} <div class="relative" out:fade>
<div class="flex justify-end px-1 text-xs {restProps.class}"> <div class="absolute right-0 top-2">
<Tippy {#if showFailure}
class="flex items-center {restProps.class}" {@const url = failedUrls[0]}
component={ThunkStatusDetail} {@const status = $thunk.status[url]}
props={{url, message, status, retry}} {@const message = $thunk.details[url]}
params={{interactive: true}}> <div class="flex justify-end px-1 text-xs {restProps.class}">
{#snippet children()} <Tippy
<span class="flex cursor-pointer items-center gap-1 text-error"> class="flex items-center {restProps.class}"
<Icon icon="danger" size={3} /> component={ThunkStatusDetail}
<span>Failed to send!</span> props={{url, message, status, retry}}
</span> params={{interactive: true}}>
{/snippet} {#snippet children()}
</Tippy> <span class="flex cursor-pointer items-center gap-1 text-error">
</div> <Icon icon="danger" size={3} />
{:else if canCancel || isPending} <span>Failed to send!</span>
<div class="flex justify-end px-1 text-xs {restProps.class}"> </span>
<span class="flex items-center gap-1 {restProps.class}"> {/snippet}
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px"></span> </Tippy>
<span class="opacity-50">Sending...</span> </div>
{#if canCancel} {:else if canCancel || isPending}
<Button class="link" onclick={abort}>Cancel</Button> <div class="flex justify-end px-1 text-xs {restProps.class}">
<span class="flex items-center gap-1 {restProps.class}">
<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={abort}>
Cancel
</button>
</span>
</div>
{/if} {/if}
</span> </div>
</div> </div>
{/if} {/if}
+7 -2
View File
@@ -251,7 +251,7 @@ export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thun
const urls = Array.from($tracker.getRelays(id)) const urls = Array.from($tracker.getRelays(id))
for (const thunk of getThunksByEventId().get(id) || []) { for (const thunk of getThunksByEventId().get(id) || []) {
for (const url of thunk.request.relays) { for (const url of thunk.options.relays) {
urls.push(url) urls.push(url)
} }
} }
@@ -661,7 +661,12 @@ export const deriveOtherRooms = (url: string) =>
// Other utils // Other utils
export const encodeRelay = (url: string) => encodeURIComponent(normalizeRelayUrl(url)) export const encodeRelay = (url: string) =>
encodeURIComponent(
normalizeRelayUrl(url)
.replace(/^wss:\/\//, "")
.replace(/\/$/, ""),
)
export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url)) export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))