Read the modal stack from page state instead of the deprecated page store
This commit is contained in:
parent
be4a4e0805
commit
e5808c11ac
8 changed files with 68 additions and 70 deletions
|
|
@ -20,7 +20,7 @@ export default [
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["**/*.svelte"],
|
files: ["**/*.svelte", "**/*.svelte.ts"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: ts.parser,
|
parser: ts.parser,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
import SpeechBanner from "@app/components/SpeechBanner.svelte"
|
import SpeechBanner from "@app/components/SpeechBanner.svelte"
|
||||||
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
import PrimaryNav from "@app/components/PrimaryNav.svelte"
|
||||||
import {app} from "@app/core"
|
import {app} from "@app/core"
|
||||||
import {modal} from "@app/modal"
|
import {getModal} from "@app/modal.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: Snippet
|
children: Snippet
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<PrimaryNav>
|
<PrimaryNav>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</PrimaryNav>
|
</PrimaryNav>
|
||||||
{:else if !$modal}
|
{:else if !getModal()}
|
||||||
<Dialog noEscape children={{component: Landing, props: {}}} />
|
<Dialog noEscape children={{component: Landing, props: {}}} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, mount, unmount} from "svelte"
|
import {mount, unmount, untrack} from "svelte"
|
||||||
import Drawer from "@lib/components/Drawer.svelte"
|
import Drawer from "@lib/components/Drawer.svelte"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import {modal, modalStack, popModal} from "@app/modal"
|
import {popModal} from "@app/modal"
|
||||||
|
import {getModal, getModalStack} from "@app/modal.svelte"
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
if ($modal && !$modal.options.noEscape) {
|
const modal = getModal()
|
||||||
|
|
||||||
|
if (modal && !modal.options.noEscape) {
|
||||||
popModal()
|
popModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -25,9 +28,11 @@
|
||||||
let element: HTMLElement
|
let element: HTMLElement
|
||||||
const instances: Record<string, any> = {}
|
const instances: Record<string, any> = {}
|
||||||
|
|
||||||
onMount(() => {
|
$effect(() => {
|
||||||
return modalStack.subscribe($modalStack => {
|
const stack = getModalStack()
|
||||||
const ids = $modalStack.map(({id}) => id)
|
|
||||||
|
untrack(() => {
|
||||||
|
const ids = stack.map(({id}) => id)
|
||||||
|
|
||||||
for (const [id, instance] of Object.entries(instances)) {
|
for (const [id, instance] of Object.entries(instances)) {
|
||||||
if (!ids.includes(id)) {
|
if (!ids.includes(id)) {
|
||||||
|
|
@ -36,7 +41,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const item of $modalStack) {
|
for (const item of stack) {
|
||||||
if (instances[item.id]) {
|
if (instances[item.id]) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@
|
||||||
import Search from "@app/components/Search.svelte"
|
import Search from "@app/components/Search.svelte"
|
||||||
import SpaceMenuDrawer from "@app/components/SpaceMenuDrawer.svelte"
|
import SpaceMenuDrawer from "@app/components/SpaceMenuDrawer.svelte"
|
||||||
import {PLATFORM_RELAYS} from "@app/env"
|
import {PLATFORM_RELAYS} from "@app/env"
|
||||||
import {modal, popModal, pushModal} from "@app/modal"
|
import {popModal, pushModal} from "@app/modal"
|
||||||
|
import {getModal} from "@app/modal.svelte"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {userSpaceUrls} from "@app/rooms"
|
import {userSpaceUrls} from "@app/rooms"
|
||||||
import {goToChat, lastSpaceUrl, makeSpacePath} from "@app/routes"
|
import {goToChat, lastSpaceUrl, makeSpacePath} from "@app/routes"
|
||||||
|
|
@ -39,7 +40,7 @@
|
||||||
// they were in when they're somewhere else.
|
// they were in when they're somewhere else.
|
||||||
const spaceUrl = $derived($lastSpaceUrl ?? PLATFORM_RELAYS[0] ?? $userSpaceUrls[0])
|
const spaceUrl = $derived($lastSpaceUrl ?? PLATFORM_RELAYS[0] ?? $userSpaceUrls[0])
|
||||||
|
|
||||||
const spaceMenuIsOpen = $derived($modal?.component === SpaceMenuDrawer)
|
const spaceMenuIsOpen = $derived(getModal()?.component === SpaceMenuDrawer)
|
||||||
|
|
||||||
const spaceMenuLabel = $derived(spaceMenuIsOpen ? "Close space menu" : "Open space menu")
|
const spaceMenuLabel = $derived(spaceMenuIsOpen ? "Close space menu" : "Open space menu")
|
||||||
|
|
||||||
|
|
|
||||||
31
src/app/modal.svelte.ts
Normal file
31
src/app/modal.svelte.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import type {Component} from "svelte"
|
||||||
|
import {last} from "@welshman/lib"
|
||||||
|
import {page} from "$app/state"
|
||||||
|
import type {DialogSize} from "@lib/components/Dialog.svelte"
|
||||||
|
|
||||||
|
export type ModalOptions = {
|
||||||
|
drawer?: boolean
|
||||||
|
nested?: boolean
|
||||||
|
noEscape?: boolean
|
||||||
|
fullscreen?: boolean
|
||||||
|
size?: DialogSize
|
||||||
|
replaceState?: boolean
|
||||||
|
path?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Modal = {
|
||||||
|
id: string
|
||||||
|
component: Component
|
||||||
|
props: Record<string, any>
|
||||||
|
options: ModalOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modals = $state<Record<string, Modal>>({})
|
||||||
|
|
||||||
|
// Open modal ids live in SvelteKit's page state (shallow routing): each modal owns a history entry
|
||||||
|
// without a navigation, and any `goto` that doesn't pass `state` along closes them.
|
||||||
|
const modalStack = $derived((page.state.modals ?? []).map(id => modals[id]).filter(Boolean))
|
||||||
|
|
||||||
|
export const getModalStack = () => modalStack
|
||||||
|
|
||||||
|
export const getModal = () => last(modalStack)
|
||||||
|
|
@ -1,78 +1,34 @@
|
||||||
import type {Component} from "svelte"
|
import type {Component} from "svelte"
|
||||||
import {get, writable} from "svelte/store"
|
import {randomId, Emitter} from "@welshman/lib"
|
||||||
import {randomId, last, always, assoc, Emitter} from "@welshman/lib"
|
|
||||||
import {deriveDeduplicated} from "@welshman/store"
|
|
||||||
import {goto, pushState, replaceState} from "$app/navigation"
|
import {goto, pushState, replaceState} from "$app/navigation"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/state"
|
||||||
import type {DialogSize} from "@lib/components/Dialog.svelte"
|
import {modals, type ModalOptions} from "@app/modal.svelte"
|
||||||
|
|
||||||
export type ModalOptions = {
|
|
||||||
drawer?: boolean
|
|
||||||
nested?: boolean
|
|
||||||
noEscape?: boolean
|
|
||||||
fullscreen?: boolean
|
|
||||||
size?: DialogSize
|
|
||||||
replaceState?: boolean
|
|
||||||
path?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Modal = {
|
|
||||||
id: string
|
|
||||||
component: Component
|
|
||||||
props: Record<string, any>
|
|
||||||
options: ModalOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
export const emitter = new Emitter()
|
export const emitter = new Emitter()
|
||||||
|
|
||||||
export const modals = writable<Record<string, Modal>>({})
|
|
||||||
|
|
||||||
// Open modal ids live in SvelteKit's page state (shallow routing): each modal owns a history entry
|
|
||||||
// without a navigation, and any `goto` that doesn't pass `state` along closes them.
|
|
||||||
export const modalStack = deriveDeduplicated([page, modals], ([$page, $modals]) =>
|
|
||||||
($page.state?.modals ?? []).map(id => $modals[id]).filter(Boolean),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const modal = deriveDeduplicated(modalStack, last)
|
|
||||||
|
|
||||||
export type NavigateOptions = Parameters<typeof goto>[1] & {keepModal?: boolean}
|
export type NavigateOptions = Parameters<typeof goto>[1] & {keepModal?: boolean}
|
||||||
|
|
||||||
// An open modal owns the current history entry, so a navigation that drops it takes that entry over
|
// An open modal owns the current history entry, so a navigation that drops it takes that entry over
|
||||||
export const navigate = (path: string, {keepModal, ...options}: NavigateOptions = {}) => {
|
export const navigate = (path: string, {keepModal, ...options}: NavigateOptions = {}) => {
|
||||||
const {state} = get(page)
|
const ids = page.state.modals ?? []
|
||||||
const modalIsOpen = Boolean(state.modals?.length)
|
const modalIsOpen = ids.length > 0
|
||||||
|
|
||||||
if (keepModal && modalIsOpen) {
|
if (keepModal && modalIsOpen) {
|
||||||
return goto(path, {...options, state, replaceState: true})
|
return goto(path, {...options, state: {modals: ids}, replaceState: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
return goto(path, {...options, replaceState: options.replaceState || modalIsOpen})
|
return goto(path, {...options, replaceState: options.replaceState || modalIsOpen})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pop = () => history.back()
|
|
||||||
|
|
||||||
export const popPath = async () => {
|
|
||||||
while (get(modal)) {
|
|
||||||
const popped = new Promise(resolve =>
|
|
||||||
window.addEventListener("popstate", resolve, {once: true}),
|
|
||||||
)
|
|
||||||
|
|
||||||
history.back()
|
|
||||||
|
|
||||||
await popped
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pushModal = (
|
export const pushModal = (
|
||||||
component: Component<any>,
|
component: Component<any>,
|
||||||
props: Record<string, any> = {},
|
props: Record<string, any> = {},
|
||||||
options: ModalOptions = {},
|
options: ModalOptions = {},
|
||||||
) => {
|
) => {
|
||||||
const id = randomId()
|
const id = randomId()
|
||||||
const existingIds = get(page).state.modals ?? []
|
const ids = options.nested ? [...(page.state.modals ?? []), id] : [id]
|
||||||
const ids = options.nested ? [...existingIds, id] : [id]
|
|
||||||
|
|
||||||
modals.update(assoc(id, {id, component, props, options}))
|
modals[id] = {id, component, props, options}
|
||||||
|
|
||||||
if (options.replaceState) {
|
if (options.replaceState) {
|
||||||
replaceState("", {modals: ids})
|
replaceState("", {modals: ids})
|
||||||
|
|
@ -84,7 +40,7 @@ export const pushModal = (
|
||||||
}
|
}
|
||||||
|
|
||||||
export const popModal = () => {
|
export const popModal = () => {
|
||||||
const ids = get(page).state.modals ?? []
|
const ids = page.state.modals ?? []
|
||||||
|
|
||||||
if (ids.length > 0) {
|
if (ids.length > 0) {
|
||||||
replaceState("", {modals: ids.slice(0, -1)})
|
replaceState("", {modals: ids.slice(0, -1)})
|
||||||
|
|
@ -93,6 +49,10 @@ export const popModal = () => {
|
||||||
|
|
||||||
export const clearModals = () => {
|
export const clearModals = () => {
|
||||||
replaceState("", {})
|
replaceState("", {})
|
||||||
modals.update(always({}))
|
|
||||||
|
for (const id of Object.keys(modals)) {
|
||||||
|
delete modals[id]
|
||||||
|
}
|
||||||
|
|
||||||
emitter.emit("close")
|
emitter.emit("close")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||||
import {goToHome} from "@app/routes"
|
import {goToHome} from "@app/routes"
|
||||||
import {modal} from "@app/modal"
|
import {getModal} from "@app/modal.svelte"
|
||||||
|
|
||||||
const children = {
|
const children = {
|
||||||
component: SpaceInviteAccept,
|
component: SpaceInviteAccept,
|
||||||
|
|
@ -15,6 +15,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$modal}
|
{#if !getModal()}
|
||||||
<Dialog {children} onClose={goToHome} />
|
<Dialog {children} onClose={goToHome} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@
|
||||||
import {deriveRelayAuthError} from "@app/access"
|
import {deriveRelayAuthError} from "@app/access"
|
||||||
import {relays, roomLists, user} from "@app/core"
|
import {relays, roomLists, user} from "@app/core"
|
||||||
import {userSpaceUrls} from "@app/rooms"
|
import {userSpaceUrls} from "@app/rooms"
|
||||||
import {modal, pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
import {getModal} from "@app/modal.svelte"
|
||||||
import {relaysPendingTrust} from "@app/policies"
|
import {relaysPendingTrust} from "@app/policies"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
|
@ -81,7 +82,7 @@
|
||||||
// Watch for relay errors and notify the user
|
// Watch for relay errors and notify the user
|
||||||
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($modal) return
|
if (getModal()) return
|
||||||
|
|
||||||
if (redirectUrl && redirectUrl !== url && !redirectPrompted.has(url)) {
|
if (redirectUrl && redirectUrl !== url && !redirectPrompted.has(url)) {
|
||||||
redirectPrompted.add(url)
|
redirectPrompted.add(url)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue