Read the modal stack from page state instead of the deprecated page store

This commit is contained in:
Coracle-Bot 2026-09-11 17:10:41 +00:00
parent be4a4e0805
commit e5808c11ac
8 changed files with 68 additions and 70 deletions

View file

@ -20,7 +20,7 @@ export default [
}, },
}, },
{ {
files: ["**/*.svelte"], files: ["**/*.svelte", "**/*.svelte.ts"],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
parser: ts.parser, parser: ts.parser,

View file

@ -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>

View file

@ -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
} }

View file

@ -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
View 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)

View file

@ -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")
} }

View file

@ -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}

View file

@ -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)