feat: add deep link customization
This commit is contained in:
@@ -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}
|
||||||
@@ -18,22 +18,16 @@
|
|||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {PROTECTED} from "@app/core/state"
|
import {PROTECTED} from "@app/core/state"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/util/drafts"
|
|
||||||
import {canEnforceNip70} from "@app/core/commands"
|
import {canEnforceNip70} from "@app/core/commands"
|
||||||
|
|
||||||
type Values = {
|
|
||||||
content?: string | object
|
|
||||||
title?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h?: 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 shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
@@ -77,29 +71,18 @@
|
|||||||
event: makeEvent(THREAD, {content, tags}),
|
event: makeEvent(THREAD, {content, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
draftKey.clear()
|
|
||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = $state(initialValues?.title ?? "")
|
|
||||||
let content = $state(initialValues?.content ?? "")
|
|
||||||
|
|
||||||
const onChange = (json: object) => {
|
|
||||||
content = json
|
|
||||||
}
|
|
||||||
|
|
||||||
const editor = makeEditor({
|
const editor = makeEditor({
|
||||||
|
content: initialContent,
|
||||||
url,
|
url,
|
||||||
submit,
|
submit,
|
||||||
uploading,
|
uploading,
|
||||||
onChange,
|
|
||||||
placeholder: "What's on your mind?",
|
placeholder: "What's on your mind?",
|
||||||
content,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
let title: string = $state("")
|
||||||
draftKey.update({title, content})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||||
|
|||||||
@@ -259,8 +259,9 @@ export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pu
|
|||||||
export const bootstrapPubkeys = derived(userFollowList, $userFollowList => {
|
export const bootstrapPubkeys = derived(userFollowList, $userFollowList => {
|
||||||
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
const appPubkeys = DEFAULT_PUBKEYS.split(",")
|
||||||
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollowList)))
|
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({
|
export const deriveEvent = makeDeriveEvent({
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import Close from "@assets/icons/close.svg?dataurl"
|
import Close from "@assets/icons/close.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import {clearModals} from "@app/util/modal"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose?: any
|
onClose?: any
|
||||||
@@ -56,7 +55,7 @@
|
|||||||
<div class={wrapperClass}>
|
<div class={wrapperClass}>
|
||||||
<div class={innerClass} transition:fly>
|
<div class={innerClass} transition:fly>
|
||||||
{#if !noEscape}
|
{#if !noEscape}
|
||||||
<Button class={buttonClass} onclick={clearModals}>
|
<Button class={buttonClass} onclick={onClose}>
|
||||||
<Icon icon={Close} size={6} />
|
<Icon icon={Close} size={6} />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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")} />
|
||||||
@@ -13,4 +13,4 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Dialog {children} />
|
<Dialog {children} onClose={() => goto("/home")} />
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
const search = debounce(200, (term: string) => {
|
const search = debounce(200, (term: string) => {
|
||||||
if (term) {
|
if (term) {
|
||||||
pubkeys = $profileSearch.searchValues(term)
|
pubkeys = Array.from(new Set($profileSearch.searchValues(term)))
|
||||||
} else {
|
} else {
|
||||||
pubkeys = $bootstrapPubkeys
|
pubkeys = Array.from(new Set($bootstrapPubkeys))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,6 @@
|
|||||||
</SecondaryNavSection>
|
</SecondaryNavSection>
|
||||||
</SecondaryNav>
|
</SecondaryNav>
|
||||||
|
|
||||||
<Page>
|
<Page class="scroll-container overflow-y-auto overflow-x-hidden">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
Reference in New Issue
Block a user