From a38c30b57fb46a0709cc38b022c82d5db859adb9 Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Tue, 7 Apr 2026 14:06:19 +0530 Subject: [PATCH 1/4] feat: add deep link customization --- src/app/components/IntentHandler.svelte | 201 ++++++++++++++++++++++++ src/app/components/ThreadCreate.svelte | 11 +- src/routes/intent/+page.svelte | 16 ++ 3 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 src/app/components/IntentHandler.svelte create mode 100644 src/routes/intent/+page.svelte diff --git a/src/app/components/IntentHandler.svelte b/src/app/components/IntentHandler.svelte new file mode 100644 index 00000000..0b357ef3 --- /dev/null +++ b/src/app/components/IntentHandler.svelte @@ -0,0 +1,201 @@ + + +{#if hasSettings} +
+ +
+ +
+ Apply Customization? + A link is requesting to customize your app. +
+
+
+ + +

This link will apply the following changes:

+
    + {#if t} +
  • Set theme to "{t}"
  • + {/if} + {#if relays.length > 0} +
  • Add {relays.length} relay{relays.length > 1 ? "s" : ""} to your settings
  • + {/if} + {#if blossoms.length > 0} +
  • Add {blossoms.length} blossom server{blossoms.length > 1 ? "s" : ""}
  • + {/if} + {#if follows.length > 0} +
  • Follow {follows.length} person{follows.length > 1 ? "s" : ""}
  • + {/if} + {#if joins.length > 0} +
  • Join {joins.length} communit{joins.length > 1 ? "ies" : "y"}
  • + {/if} + {#if hasProfile} +
  • Update your profile metadata
  • + {/if} + {#if hasShare} +
  • Open a new post dialog in a specific room
  • + {/if} +
+
+ + + + + +
+{/if} diff --git a/src/app/components/ThreadCreate.svelte b/src/app/components/ThreadCreate.svelte index 5f947e0d..5ffc9d4f 100644 --- a/src/app/components/ThreadCreate.svelte +++ b/src/app/components/ThreadCreate.svelte @@ -23,9 +23,10 @@ type Props = { url: string h?: string + initialContent?: string } - const {url, h}: Props = $props() + const {url, h, initialContent = ""}: Props = $props() const shouldProtect = canEnforceNip70(url) @@ -73,7 +74,13 @@ history.back() } - const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"}) + const editor = makeEditor({ + content: initialContent, + url, + submit, + uploading, + placeholder: "What's on your mind?", + }) let title: string = $state("") diff --git a/src/routes/intent/+page.svelte b/src/routes/intent/+page.svelte new file mode 100644 index 00000000..15708a89 --- /dev/null +++ b/src/routes/intent/+page.svelte @@ -0,0 +1,16 @@ + + + -- 2.52.0 From 17714253ee7a5aa8eaf620a74a6e5faacf032acd Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Tue, 7 Apr 2026 16:36:40 +0530 Subject: [PATCH 2/4] fix: restore settings scroll and deep-link dialog close behavior --- src/app/components/IntentHandler.svelte | 236 ++++++++++++++---------- src/lib/components/Dialog.svelte | 3 +- src/routes/intent/+page.svelte | 2 +- src/routes/join/+page.svelte | 2 +- src/routes/settings/+layout.svelte | 2 +- 5 files changed, 141 insertions(+), 104 deletions(-) diff --git a/src/app/components/IntentHandler.svelte b/src/app/components/IntentHandler.svelte index 0b357ef3..8ce3a763 100644 --- a/src/app/components/IntentHandler.svelte +++ b/src/app/components/IntentHandler.svelte @@ -10,6 +10,7 @@ addRelay, pubkey, profilesByPubkey, + waitForThunkError, } from "@welshman/app" import { makeEvent, @@ -29,11 +30,11 @@ import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import Button from "@lib/components/Button.svelte" - import Icon from "@lib/components/Icon.svelte" - import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import {theme} from "@app/util/theme" import {pushModal} from "@app/util/modal" import {pushToast} from "@app/util/toast" + import {errorMessage} from "@lib/util" + import Modal from "@lib/components/Modal.svelte" import ThreadCreate from "@app/components/ThreadCreate.svelte" import {addSpaceMembership, updateProfile} from "@app/core/commands" @@ -54,12 +55,20 @@ const profile_about = params.get("profile_about") const profile_picture = params.get("profile_picture") const profile_banner = params.get("profile_banner") + const profile_nip05 = params.get("profile_nip05") + const profile_lud16 = params.get("profile_lud16") const shareRelay = params.get("share_relay") || params.get("share_url") const shareH = params.get("share_h") const shareText = params.get("share_text") - const hasProfile = !!profile_name || !!profile_about || !!profile_picture || !!profile_banner + const hasProfile = + !!profile_name || + !!profile_about || + !!profile_picture || + !!profile_banner || + !!profile_nip05 || + !!profile_lud16 const hasSettings = !!t || relays.length > 0 || @@ -74,7 +83,7 @@ onMount(() => { if (!hasSettings) { if (hasShare) { - doShare() + void openShare() } else { pushToast({message: "No valid intent actions found", theme: "error"}) back() @@ -82,120 +91,149 @@ } }) - const doShare = () => { - back() - goto(`/spaces/${encodeURIComponent(shareRelay!)}/${shareH}`).then(() => { - pushModal(ThreadCreate, {url: shareRelay!, h: shareH!, initialContent: shareText || ""}) - }) + const openShare = async () => { + await goto(`/spaces/${encodeURIComponent(shareRelay!)}/${shareH}`) + + pushModal(ThreadCreate, {url: shareRelay!, h: shareH!, initialContent: shareText || ""}) } const accept = async () => { processing = true - - if (t === "dark" || t === "light" || t === "system") { - theme.set(t) - } - - if (relays.length > 0) { - for (const url of relays) { - addRelay(url, RelayMode.Read) - addRelay(url, RelayMode.Write) + try { + if (t) { + theme.set(t) } - } - if (blossoms.length > 0) { - const current = getTagValues("server", getListTags(get(userBlossomServerList))) - const updated = Array.from(new Set([...current, ...blossoms])) - publishThunk({ - event: makeEvent(BLOSSOM_SERVERS, {tags: updated.map(tagger("server"))}), - relays: Router.get().FromUser().getUrls(), - }) - } - - if (follows.length > 0) { - const current = getPubkeyTagValues(getListTags(get(userFollowList))) - const updated = Array.from(new Set([...current, ...follows])) - publishThunk({ - event: makeEvent(FOLLOWS, {tags: updated.map(tagPubkey)}), - relays: Router.get().FromUser().getUrls(), - }) - } - - if (joins.length > 0) { - for (const url of joins) { - await addSpaceMembership(url) + if (relays.length > 0) { + for (const url of relays) { + addRelay(url, RelayMode.Read) + addRelay(url, RelayMode.Write) + } } - } - if (hasProfile) { - const p = get(profilesByPubkey).get(get(pubkey)!) || makeProfile() - if (profile_name) p.name = profile_name - if (profile_about) p.about = profile_about - if (profile_picture) p.picture = profile_picture - if (profile_banner) p.banner = profile_banner - // assuming shouldBroadcast makes it public - await updateProfile({profile: p, shouldBroadcast: true}) - } + if (blossoms.length > 0) { + const current = getTagValues("server", getListTags(get(userBlossomServerList))) + const updated = Array.from(new Set([...current, ...blossoms])) + const error = await waitForThunkError( + publishThunk({ + event: makeEvent(BLOSSOM_SERVERS, {tags: updated.map(tagger("server"))}), + relays: Router.get().FromUser().getUrls(), + }), + ) - pushToast({message: "Customizations Applied!"}) + if (error) { + pushToast({ + theme: "error", + message: `Failed to update blossom servers: ${errorMessage(error)}`, + }) + return + } + } - if (hasShare) { - doShare() - } else { - back() + if (follows.length > 0) { + const current = getPubkeyTagValues(getListTags(get(userFollowList))) + const updated = Array.from(new Set([...current, ...follows])) + const error = await waitForThunkError( + publishThunk({ + event: makeEvent(FOLLOWS, {tags: updated.map(tagPubkey)}), + relays: Router.get().FromUser().getUrls(), + }), + ) + + if (error) { + pushToast({theme: "error", message: `Failed to update follows: ${errorMessage(error)}`}) + return + } + } + + if (joins.length > 0) { + for (const url of joins) { + await addSpaceMembership(url) + } + } + + if (hasProfile) { + const profile = {...(get(profilesByPubkey).get(get(pubkey)!) || makeProfile())} + if (profile_name) profile.name = profile_name + if (profile_about) profile.about = profile_about + if (profile_picture) profile.picture = profile_picture + if (profile_banner) profile.banner = profile_banner + if (profile_nip05) profile.nip05 = profile_nip05 + if (profile_lud16) profile.lud16 = profile_lud16 + + const error = await waitForThunkError(updateProfile({profile, shouldBroadcast: true})) + + if (error) { + if (error.includes("rate-limited") || error.startsWith("blocked:")) { + pushToast({ + message: "Profile update was requested, but one relay rate-limited or blocked it.", + }) + } else { + pushToast({theme: "error", message: `Failed to update profile: ${errorMessage(error)}`}) + return + } + } + } + + pushToast({message: "Customizations Applied!"}) + + if (hasShare) { + await openShare() + } else { + back() + } + } finally { + processing = false } } {#if hasSettings} -
- -
- + event.preventDefault()}> +
+
Apply Customization? A link is requesting to customize your app.
-
- + - -

This link will apply the following changes:

-
    - {#if t} -
  • Set theme to "{t}"
  • - {/if} - {#if relays.length > 0} -
  • Add {relays.length} relay{relays.length > 1 ? "s" : ""} to your settings
  • - {/if} - {#if blossoms.length > 0} -
  • Add {blossoms.length} blossom server{blossoms.length > 1 ? "s" : ""}
  • - {/if} - {#if follows.length > 0} -
  • Follow {follows.length} person{follows.length > 1 ? "s" : ""}
  • - {/if} - {#if joins.length > 0} -
  • Join {joins.length} communit{joins.length > 1 ? "ies" : "y"}
  • - {/if} - {#if hasProfile} -
  • Update your profile metadata
  • - {/if} - {#if hasShare} -
  • Open a new post dialog in a specific room
  • - {/if} -
-
+ +

This link will apply the following changes:

+
    + {#if t} +
  • Set theme to "{t}"
  • + {/if} + {#if relays.length > 0} +
  • Add {relays.length} relay{relays.length > 1 ? "s" : ""} to your settings
  • + {/if} + {#if blossoms.length > 0} +
  • Add {blossoms.length} blossom server{blossoms.length > 1 ? "s" : ""}
  • + {/if} + {#if follows.length > 0} +
  • Follow {follows.length} person{follows.length > 1 ? "s" : ""}
  • + {/if} + {#if joins.length > 0} +
  • Join {joins.length} communit{joins.length > 1 ? "ies" : "y"}
  • + {/if} + {#if hasProfile} +
  • Update your profile metadata
  • + {/if} + {#if hasShare} +
  • Open a new post dialog in a specific room
  • + {/if} +
+
- - - - - + + + + +
+ {/if} diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index 261cce23..4a9a58f3 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -6,7 +6,6 @@ import Close from "@assets/icons/close.svg?dataurl" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" - import {clearModals} from "@app/util/modal" type Props = { onClose?: any @@ -56,7 +55,7 @@
{#if !noEscape} - {/if} diff --git a/src/routes/intent/+page.svelte b/src/routes/intent/+page.svelte index 15708a89..ef30915c 100644 --- a/src/routes/intent/+page.svelte +++ b/src/routes/intent/+page.svelte @@ -13,4 +13,4 @@ } - + goto("/home")} /> diff --git a/src/routes/join/+page.svelte b/src/routes/join/+page.svelte index 1083d874..abf101ed 100644 --- a/src/routes/join/+page.svelte +++ b/src/routes/join/+page.svelte @@ -13,4 +13,4 @@ } - + goto("/home")} /> diff --git a/src/routes/settings/+layout.svelte b/src/routes/settings/+layout.svelte index 4b24ac9e..3603ffb4 100644 --- a/src/routes/settings/+layout.svelte +++ b/src/routes/settings/+layout.svelte @@ -67,6 +67,6 @@ - + {@render children?.()} -- 2.52.0 From 436ced8dd74101e8f082c1834ee3d9530bc3c34e Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Tue, 7 Apr 2026 23:19:20 +0530 Subject: [PATCH 3/4] fix: show deep-link action errors with abort-or-continue flow --- src/app/components/IntentHandler.svelte | 132 +++++++++++++++--------- 1 file changed, 83 insertions(+), 49 deletions(-) diff --git a/src/app/components/IntentHandler.svelte b/src/app/components/IntentHandler.svelte index 8ce3a763..c598ae35 100644 --- a/src/app/components/IntentHandler.svelte +++ b/src/app/components/IntentHandler.svelte @@ -79,6 +79,7 @@ const hasShare = !!shareRelay && !!shareH let processing = $state(false) + let errors = $state([]) onMount(() => { if (!hasSettings) { @@ -99,6 +100,10 @@ const accept = async () => { processing = true + errors = [] + + const nextErrors: string[] = [] + try { if (t) { theme.set(t) @@ -106,8 +111,16 @@ if (relays.length > 0) { for (const url of relays) { - addRelay(url, RelayMode.Read) - addRelay(url, RelayMode.Write) + const readError = await waitForThunkError(await addRelay(url, RelayMode.Read)) + const writeError = await waitForThunkError(await addRelay(url, RelayMode.Write)) + + if (readError) { + nextErrors.push(`Relay ${url} (read): ${errorMessage(readError)}`) + } + + if (writeError) { + nextErrors.push(`Relay ${url} (write): ${errorMessage(writeError)}`) + } } } @@ -122,11 +135,7 @@ ) if (error) { - pushToast({ - theme: "error", - message: `Failed to update blossom servers: ${errorMessage(error)}`, - }) - return + nextErrors.push(`Blossom servers: ${errorMessage(error)}`) } } @@ -141,14 +150,17 @@ ) if (error) { - pushToast({theme: "error", message: `Failed to update follows: ${errorMessage(error)}`}) - return + nextErrors.push(`Follows: ${errorMessage(error)}`) } } if (joins.length > 0) { for (const url of joins) { - await addSpaceMembership(url) + const error = await waitForThunkError(await addSpaceMembership(url)) + + if (error) { + nextErrors.push(`Join ${url}: ${errorMessage(error)}`) + } } } @@ -164,17 +176,15 @@ const error = await waitForThunkError(updateProfile({profile, shouldBroadcast: true})) if (error) { - if (error.includes("rate-limited") || error.startsWith("blocked:")) { - pushToast({ - message: "Profile update was requested, but one relay rate-limited or blocked it.", - }) - } else { - pushToast({theme: "error", message: `Failed to update profile: ${errorMessage(error)}`}) - return - } + nextErrors.push(`Profile: ${errorMessage(error)}`) } } + if (nextErrors.length > 0) { + errors = nextErrors + return + } + pushToast({message: "Customizations Applied!"}) if (hasShare) { @@ -186,6 +196,16 @@ processing = false } } + + const continueWithErrors = async () => { + pushToast({message: "Applied what we could. Some actions failed."}) + + if (hasShare) { + await openShare() + } else { + back() + } + } {#if hasSettings} @@ -199,40 +219,54 @@ -

This link will apply the following changes:

-
    - {#if t} -
  • Set theme to "{t}"
  • - {/if} - {#if relays.length > 0} -
  • Add {relays.length} relay{relays.length > 1 ? "s" : ""} to your settings
  • - {/if} - {#if blossoms.length > 0} -
  • Add {blossoms.length} blossom server{blossoms.length > 1 ? "s" : ""}
  • - {/if} - {#if follows.length > 0} -
  • Follow {follows.length} person{follows.length > 1 ? "s" : ""}
  • - {/if} - {#if joins.length > 0} -
  • Join {joins.length} communit{joins.length > 1 ? "ies" : "y"}
  • - {/if} - {#if hasProfile} -
  • Update your profile metadata
  • - {/if} - {#if hasShare} -
  • Open a new post dialog in a specific room
  • - {/if} -
+ {#if errors.length === 0} +

This link will apply the following changes:

+
    + {#if t} +
  • Set theme to "{t}"
  • + {/if} + {#if relays.length > 0} +
  • Add {relays.length} relay{relays.length > 1 ? "s" : ""} to your settings
  • + {/if} + {#if blossoms.length > 0} +
  • Add {blossoms.length} blossom server{blossoms.length > 1 ? "s" : ""}
  • + {/if} + {#if follows.length > 0} +
  • Follow {follows.length} person{follows.length > 1 ? "s" : ""}
  • + {/if} + {#if joins.length > 0} +
  • Join {joins.length} communit{joins.length > 1 ? "ies" : "y"}
  • + {/if} + {#if hasProfile} +
  • Update your profile metadata
  • + {/if} + {#if hasShare} +
  • Open a new post dialog in a specific room
  • + {/if} +
+ {:else} +

Some actions failed. You can abort or continue with successful actions only.

+
    + {#each errors as error} +
  • {error}
  • + {/each} +
+ {/if}
- - + {#if errors.length === 0} + + + {:else} + + + {/if}
-- 2.52.0 From 5c5ab2fedb9a010442d4699bffa890b70bbebf20 Mon Sep 17 00:00:00 2001 From: Bhavishy Date: Wed, 8 Apr 2026 00:10:15 +0530 Subject: [PATCH 4/4] fix: dedupe people pubkey lists to prevent keyed each duplicate crash --- src/app/core/state.ts | 3 ++- src/routes/people/+page.svelte | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/core/state.ts b/src/app/core/state.ts index 195516fb..78aba6fb 100644 --- a/src/app/core/state.ts +++ b/src/app/core/state.ts @@ -259,8 +259,9 @@ export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pu export const bootstrapPubkeys = derived(userFollowList, $userFollowList => { const appPubkeys = DEFAULT_PUBKEYS.split(",") const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollowList))) + const mergedPubkeys = userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys] - return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys] + return uniq(mergedPubkeys) }) export const deriveEvent = makeDeriveEvent({ diff --git a/src/routes/people/+page.svelte b/src/routes/people/+page.svelte index 7d9937b6..a6006f42 100644 --- a/src/routes/people/+page.svelte +++ b/src/routes/people/+page.svelte @@ -17,9 +17,9 @@ const search = debounce(200, (term: string) => { if (term) { - pubkeys = $profileSearch.searchValues(term) + pubkeys = Array.from(new Set($profileSearch.searchValues(term))) } else { - pubkeys = $bootstrapPubkeys + pubkeys = Array.from(new Set($bootstrapPubkeys)) } }) -- 2.52.0