feat: add deep link customization #169

Closed
bhavishy2801 wants to merge 2 commits from (deleted):dev into dev
8 changed files with 303 additions and 11 deletions
+273
View File
@@ -0,0 +1,273 @@
<script lang="ts">
import {onMount} from "svelte"
import {get} from "svelte/store"
import {goto} from "$app/navigation"
import {
userFollowList,
userBlossomServerList,
tagPubkey,
publishThunk,
addRelay,
pubkey,
profilesByPubkey,
waitForThunkError,
} from "@welshman/app"
import {
makeEvent,
FOLLOWS,
BLOSSOM_SERVERS,
getListTags,
getPubkeyTagValues,
getTagValues,
tagger,
RelayMode,
makeProfile,
} from "@welshman/util"
import {Router} from "@welshman/router"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import Button from "@lib/components/Button.svelte"
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"
type Props = {
params: URLSearchParams
back: () => void
}
const {params, back}: Props = $props()
const t = params.get("theme")
const relays = params.getAll("add_relay")
const blossoms = params.getAll("add_blossom")
const follows = params.getAll("add_follow")
const joins = params.getAll("join")
const profile_name = params.get("profile_name")
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 ||
!!profile_nip05 ||
!!profile_lud16
const hasSettings =
!!t ||
relays.length > 0 ||
blossoms.length > 0 ||
follows.length > 0 ||
joins.length > 0 ||
hasProfile
const hasShare = !!shareRelay && !!shareH
let processing = $state(false)
let errors = $state<string[]>([])
onMount(() => {
if (!hasSettings) {
if (hasShare) {
void openShare()
} else {
pushToast({message: "No valid intent actions found", theme: "error"})
back()
}
}
})
const openShare = async () => {
await goto(`/spaces/${encodeURIComponent(shareRelay!)}/${shareH}`)
pushModal(ThreadCreate, {url: shareRelay!, h: shareH!, initialContent: shareText || ""})
}
const accept = async () => {
processing = true
errors = []
const nextErrors: string[] = []
try {
if (t) {
theme.set(t)
}
if (relays.length > 0) {
for (const url of relays) {
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)}`)
}
}
}
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(),
}),
)
if (error) {
nextErrors.push(`Blossom servers: ${errorMessage(error)}`)
}
}
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) {
nextErrors.push(`Follows: ${errorMessage(error)}`)
}
}
if (joins.length > 0) {
for (const url of joins) {
const error = await waitForThunkError(await addSpaceMembership(url))
if (error) {
nextErrors.push(`Join ${url}: ${errorMessage(error)}`)
}
}
}
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) {
nextErrors.push(`Profile: ${errorMessage(error)}`)
}
}
if (nextErrors.length > 0) {
errors = nextErrors
return
}
pushToast({message: "Customizations Applied!"})
if (hasShare) {
await openShare()
} else {
back()
}
} finally {
processing = false
}
}
const continueWithErrors = async () => {
pushToast({message: "Applied what we could. Some actions failed."})
if (hasShare) {
await openShare()
} else {
back()
}
}
</script>
{#if hasSettings}
<Modal tag="form" onsubmit={(event: SubmitEvent) => event.preventDefault()}>
<div class="flex flex-col gap-4 text-center">
<ModalHeader>
<div class="flex flex-col items-start gap-1">
<ModalTitle>Apply Customization?</ModalTitle>
<ModalSubtitle>A link is requesting to customize your app.</ModalSubtitle>
</div>
</ModalHeader>
<ModalBody>
{#if errors.length === 0}
<p>This link will apply the following changes:</p>
<ul class="text-left list-disc list-inside px-6 py-4">
{#if t}
<li>Set theme to "{t}"</li>
{/if}
{#if relays.length > 0}
<li>Add {relays.length} relay{relays.length > 1 ? "s" : ""} to your settings</li>
{/if}
{#if blossoms.length > 0}
<li>Add {blossoms.length} blossom server{blossoms.length > 1 ? "s" : ""}</li>
{/if}
{#if follows.length > 0}
<li>Follow {follows.length} person{follows.length > 1 ? "s" : ""}</li>
{/if}
{#if joins.length > 0}
<li>Join {joins.length} communit{joins.length > 1 ? "ies" : "y"}</li>
{/if}
{#if hasProfile}
<li>Update your profile metadata</li>
{/if}
{#if hasShare}
<li>Open a new post dialog in a specific room</li>
{/if}
</ul>
{:else}
<p>Some actions failed. You can abort or continue with successful actions only.</p>
<ul class="text-left list-disc list-inside px-6 py-4 text-error">
{#each errors as error}
<li>{error}</li>
{/each}
</ul>
{/if}
</ModalBody>
<ModalFooter>
{#if errors.length === 0}
<Button class="btn btn-neutral" onclick={back}>Cancel</Button>
<Button class="btn btn-primary" onclick={accept} disabled={processing}>
{#if processing}
<span class="loading loading-spinner"></span>
{/if}
Accept
</Button>
{:else}
<Button class="btn btn-neutral" onclick={back}>Abort</Button>
<Button class="btn btn-primary" onclick={continueWithErrors}>Continue</Button>
{/if}
</ModalFooter>
</div>
</Modal>
{/if}
+8 -5
View File
@@ -29,11 +29,14 @@
type Props = {
url: string
h?: string
initialContent?: string
}
const {url, h}: Props = $props()
const {url, h, initialContent = ""}: Props = $props()
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
const initialValues = draftKey.get()
const initialValues = initialContent ? {content: initialContent} : draftKey.get()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false)
@@ -81,8 +84,8 @@
history.back()
}
let title = $state(initialValues?.title ?? "")
let content = $state(initialValues?.content ?? "")
let title: string = $state(initialValues?.title ?? "")
let content: string | object = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
@@ -98,7 +101,7 @@
})
$effect(() => {
draftKey.update({title, content})
draftKey.set({title, content})
})
</script>
+2 -1
View File
@@ -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({
+1 -2
View File
@@ -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 @@
<div class={wrapperClass}>
<div class={innerClass} transition:fly>
{#if !noEscape}
<Button class={buttonClass} onclick={clearModals}>
<Button class={buttonClass} onclick={onClose}>
<Icon icon={Close} size={6} />
</Button>
{/if}
+1 -1
View File
@@ -9,6 +9,6 @@
<div
data-component="Page"
class="relative flex-grow flex flex-col min-w-0 ml-sai mb-sai mt-sai mr-sai bg-base-200 md:ml-0 md:mb-0 {props.class}">
class="scroll-container relative flex-grow flex min-h-0 flex-col min-w-0 overflow-y-auto overflow-x-hidden ml-sai mb-sai mt-sai mr-sai bg-base-200 md:ml-0 md:mb-0 {props.class}">
{@render props.children?.()}
</div>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
import {page} from "$app/stores"
import {goto} from "$app/navigation"
import Dialog from "@lib/components/Dialog.svelte"
import IntentHandler from "@app/components/IntentHandler.svelte"
const children = {
component: IntentHandler,
props: {
params: $page.url.searchParams,
back: () => goto("/home"),
},
}
</script>
<Dialog {children} onClose={() => goto("/home")} />
+1 -1
View File
@@ -13,4 +13,4 @@
}
</script>
<Dialog {children} />
<Dialog {children} onClose={() => goto("/home")} />
+1 -1
View File
@@ -17,7 +17,7 @@
const search = debounce(200, (term: string) => {
if (term) {
pubkeys = $profileSearch.searchValues(term)
pubkeys = Array.from(new Set($profileSearch.searchValues(term)))
} else {
pubkeys = $bootstrapPubkeys
hodlbod marked this conversation as resolved Outdated
Outdated
Review

These shouldn't be necessary, were you seeing duplicate values?

These shouldn't be necessary, were you seeing duplicate values?
Outdated
Review

Yes, I was seeing duplicate values when the add-follow deep link was triggered multiple times for the same pubkey. I agree this dedupe does not belong in +page.svelte, so I removed the extra Set(....) there and kept deduping in the follow update flow instead.

Yes, I was seeing duplicate values when the add-follow deep link was triggered multiple times for the same pubkey. I agree this dedupe does not belong in `+page.svelte`, so I removed the extra `Set(....)` there and kept deduping in the follow update flow instead.
}