Open the space menu in a drawer instead of navigating away from the room (#425)
This commit is contained in:
parent
6818aa240f
commit
9810695782
49 changed files with 346 additions and 167 deletions
|
|
@ -192,6 +192,8 @@ return a `Command`, so `.then(publish)` is usually all you need.
|
|||
- Import from `app/modal.ts` or `app/toast.ts`
|
||||
- Pass component objects with parameters
|
||||
- Use `$state.snapshot` if calling component might unmount
|
||||
- Navigate with `navigate` from `app/modal.ts` rather than `goto` — an open modal owns the current
|
||||
history entry, so a navigation that drops it replaces that entry instead of stacking on it
|
||||
|
||||
## Development Workflow
|
||||
|
||||
|
|
|
|||
|
|
@ -1565,14 +1565,14 @@ Acceptance:
|
|||
|
||||
### US-110 — See another space's unread activity from a phone
|
||||
|
||||
As bob on a phone, I want the bottom bar's Spaces button to tell me another
|
||||
As bob on a phone, I want the bottom bar's space-menu button to tell me another
|
||||
space wants attention, so that I don't have to leave the room I'm reading to
|
||||
find out.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- While bob has a room open, a message in a space he isn't in raises an unread
|
||||
dot on the Spaces button, matching the dot that space's row carries in
|
||||
dot on the space-menu button, matching the dot that space's row carries in
|
||||
`/spaces`.
|
||||
- A message in another room of the space he's already in does not; that space's
|
||||
own room list carries it.
|
||||
|
|
|
|||
|
|
@ -41,10 +41,9 @@ const settingRow = (page: Page, label: string) =>
|
|||
const spaceNavItem = (page: Page, name: string) =>
|
||||
page.locator(`.primary-nav [data-tip^="${name}"]`)
|
||||
|
||||
// The desktop rail and the phone's bottom bar each carry a link to the space list, and neither one
|
||||
// has an accessible name — both icons are masked svgs. Below tailwind's md breakpoint the rail is
|
||||
// display:none, so on a phone the visible one is the bar's.
|
||||
const spacesNavItem = (page: Page) => page.locator('a[href="/spaces"]:visible')
|
||||
// The phone's bottom bar opens the space menu in a drawer; the desktop rail has no equivalent,
|
||||
// since the menu is always on screen there.
|
||||
const spaceMenuNavItem = (page: Page) => page.getByRole("button", {name: "Open space menu"})
|
||||
|
||||
// The space menu's header, the one button in the secondary nav carrying the relay's address.
|
||||
const spaceMenu = (page: Page, url: string) =>
|
||||
|
|
@ -53,8 +52,7 @@ const spaceMenu = (page: Page, url: string) =>
|
|||
const roomLink = (page: Page, name: string) =>
|
||||
page.locator(".space-menu__scroll").getByRole("link", {name})
|
||||
|
||||
// The room's page bar carries a back arrow (display:none at this viewport), a search button and the
|
||||
// detail button, in that order.
|
||||
// The room's page bar carries a search button and the detail button, in that order.
|
||||
const openRoomDetail = (page: Page) =>
|
||||
page.locator('[data-component="PageBar"]').getByRole("button").last().click()
|
||||
|
||||
|
|
@ -448,16 +446,16 @@ test("US-110 see another space's unread activity from a phone", async ({seed, as
|
|||
context: {viewport: {width: 390, height: 844}, hasTouch: true},
|
||||
})
|
||||
|
||||
const spacesButton = spacesNavItem(bob)
|
||||
const menuButton = spaceMenuNavItem(bob)
|
||||
|
||||
await expect(spacesButton).toBeVisible()
|
||||
await expect(unreadDot(spacesButton)).toHaveCount(0)
|
||||
await expect(menuButton).toBeVisible()
|
||||
await expect(unreadDot(menuButton)).toHaveCount(0)
|
||||
|
||||
const inOther = await as(users.alice, roomPath(other.url, "general"))
|
||||
|
||||
await send(inOther, "the server is on fire")
|
||||
|
||||
await expect(unreadDot(spacesButton)).toBeVisible()
|
||||
await expect(unreadDot(menuButton)).toBeVisible()
|
||||
|
||||
// Meanwhile the space bob is sitting in gets a message too, in a room he isn't reading
|
||||
const inSpace = await as(users.alice, roomPath(space.url, "random"))
|
||||
|
|
@ -473,7 +471,7 @@ test("US-110 see another space's unread activity from a phone", async ({seed, as
|
|||
await bob.goto(spacePath(space.url))
|
||||
|
||||
await expect(unreadDot(roomLink(bob, "Random"))).toBeVisible()
|
||||
await expect(unreadDot(spacesButton)).toHaveCount(0)
|
||||
await expect(unreadDot(menuButton)).toHaveCount(0)
|
||||
})
|
||||
|
||||
// SpaceMenuNavItems hides a content type until the space has an event of that kind, so this link
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ const openMessageMenu = (page: Page, text: string) => messageActions(page, text)
|
|||
// left corner of the viewport is outside it.
|
||||
const dismissMenu = (page: Page) => page.mouse.move(0, 0)
|
||||
|
||||
// The room's page bar carries a back arrow, a search button and the detail button, in that order.
|
||||
// The back arrow is display:none at this viewport, so a role query sees the other two.
|
||||
// The room's page bar carries a search button and the detail button, in that order.
|
||||
const pageBar = (page: Page) => page.locator('[data-component="PageBar"]')
|
||||
|
||||
const openRoomDetail = (page: Page) => pageBar(page).getByRole("button").last().click()
|
||||
|
|
|
|||
|
|
@ -112,3 +112,37 @@ test("does not stack a history entry for the space you are already in", async ({
|
|||
|
||||
await expect(page).toHaveURL(new RegExp(`${roomPath(space.url, "lounge")}$`))
|
||||
})
|
||||
|
||||
test("takes over the space menu's history entry when you navigate out of it", async ({
|
||||
seed,
|
||||
as,
|
||||
}) => {
|
||||
const scenario = await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
|
||||
space.room("lounge", {name: "Space Lounge"})
|
||||
space.room("garden", {name: "Space Garden"})
|
||||
space.join(user.alice, "lounge")
|
||||
space.join(user.alice, "garden")
|
||||
})
|
||||
|
||||
const space = scenario.space("space")
|
||||
const page = await as(users.alice, roomPath(space.url, "lounge"), {
|
||||
context: {viewport: {width: 390, height: 844}, hasTouch: true},
|
||||
})
|
||||
|
||||
const drawer = page.locator(".drawer")
|
||||
|
||||
await page.getByRole("button", {name: "Open space menu"}).click()
|
||||
await drawer.getByRole("link", {name: "Space Garden"}).click()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`${roomPath(space.url, "garden")}$`))
|
||||
await expect(drawer).toHaveCount(0)
|
||||
|
||||
// The menu held the entry the room it opened now holds, so one step back is the room it opened
|
||||
// over. Stacked instead, this lands on the menu again.
|
||||
await page.goBack()
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`${roomPath(space.url, "lounge")}$`))
|
||||
await expect(drawer).toHaveCount(0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,3 +24,82 @@ test("renders a seeded space, its rooms and its messages", async ({seed, as}) =>
|
|||
await expect(page.getByText("morning all")).toBeVisible()
|
||||
await expect(page.getByText("morning!")).toBeVisible()
|
||||
})
|
||||
|
||||
test("opens the space menu in a drawer on a phone", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
space.join(user.alice, "general")
|
||||
})
|
||||
|
||||
const {url} = scenario.space("space")
|
||||
const page = await as(users.alice, roomPath(url, "general"), {
|
||||
context: {viewport: {width: 390, height: 844}, hasTouch: true},
|
||||
})
|
||||
|
||||
const drawer = page.locator(".drawer")
|
||||
|
||||
await expect(drawer).toHaveCount(0)
|
||||
|
||||
await page.getByRole("button", {name: "Open space menu"}).click()
|
||||
|
||||
await expect(drawer.getByRole("link", {name: "General"})).toBeVisible()
|
||||
|
||||
// The space rail sits beside the menu inside the panel, so the menu gets what the rail leaves.
|
||||
// Sized to the panel instead, it runs off the right of the screen.
|
||||
await expect(drawer.locator(".space-menu")).toBeInViewport({ratio: 1})
|
||||
|
||||
// The panel is full width, so the only way back out is the bottom bar, which the drawer stops
|
||||
// short of rather than covering. Playwright's hit-target check is what proves it: a drawer over
|
||||
// the bar would take the click itself.
|
||||
const closeButton = page.getByRole("button", {name: "Close space menu"})
|
||||
|
||||
await closeButton.click()
|
||||
|
||||
await expect(drawer).toHaveCount(0)
|
||||
|
||||
await page.getByRole("button", {name: "Open space menu"}).click()
|
||||
|
||||
await expect(drawer.getByRole("link", {name: "General"})).toBeVisible()
|
||||
|
||||
// The modal stack lives in the url hash, so the drawer is a history entry and back closes it.
|
||||
await page.goBack()
|
||||
|
||||
await expect(drawer).toHaveCount(0)
|
||||
|
||||
// The button is the bottom bar's rather than the page's, so it still opens the menu from a page
|
||||
// that is in no space at all.
|
||||
await page.locator('a[href="/people"]:visible').click()
|
||||
|
||||
await page.getByRole("button", {name: "Open space menu"}).click()
|
||||
|
||||
await expect(drawer.getByRole("link", {name: "General"})).toBeVisible()
|
||||
})
|
||||
|
||||
test("keeps the drawer's space rail free of a horizontal scrollbar", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
|
||||
space.room("general", {name: "General"})
|
||||
space.join(user.alice, "general")
|
||||
})
|
||||
|
||||
const {url} = scenario.space("space")
|
||||
|
||||
// No touch, so the app is not in mobile mode and hover tooltips are live. The rail scrolls
|
||||
// vertically, which makes it scroll horizontally too, and a tooltip is 140px wider than it is.
|
||||
const page = await as(users.alice, roomPath(url, "general"), {
|
||||
context: {viewport: {width: 390, height: 844}},
|
||||
})
|
||||
|
||||
await page.getByRole("button", {name: "Open space menu"}).click()
|
||||
|
||||
const rail = page.locator(".drawer .primary-nav")
|
||||
|
||||
await expect(rail).toBeVisible()
|
||||
|
||||
const overflow = await rail.evaluate(el => el.scrollWidth - el.clientWidth)
|
||||
|
||||
expect(overflow).toBe(0)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import {fly} from "svelte/transition"
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {page} from "$app/stores"
|
||||
import cx from "classnames"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
const goToRoom = () => {
|
||||
if (!$callTargetRoom) return
|
||||
void goto(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
|
||||
void navigate(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
|
||||
}
|
||||
|
||||
const toggleMute = async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import {
|
||||
ago,
|
||||
int,
|
||||
|
|
@ -45,7 +44,7 @@
|
|||
import {userSettingsValues} from "@app/settings"
|
||||
import {deriveChat, makeChatId} from "@app/chats"
|
||||
import {makeFeedContext} from "@app/feeds"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import {DraftKey, type Draft} from "@app/drafts"
|
||||
import {prependParent} from "@app/rooms"
|
||||
import {pendingShare, type Share} from "@app/share"
|
||||
|
|
@ -73,7 +72,7 @@
|
|||
? pushModal(ProfileDetail, {pubkey: others[0]})
|
||||
: pushModal(ChatMembers, {pubkeys: others})
|
||||
|
||||
const back = () => goto("/chat")
|
||||
const back = () => navigate("/chat")
|
||||
|
||||
const replyTo = (event: TrustedEvent) => {
|
||||
parent = event
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
|
||||
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
|
||||
|
|
@ -9,7 +8,7 @@
|
|||
import Revote from "@assets/icons/revote.svg?dataurl"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import {makeArticleCreatePath} from "@app/routes"
|
||||
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
|
||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||
|
|
@ -35,7 +34,7 @@
|
|||
|
||||
const createPoll = () => pushModal(PollCreate, {url, h, shareToChat: true})
|
||||
|
||||
const createArticle = () => goto(makeArticleCreatePath(url, {h, shareToChat: true}))
|
||||
const createArticle = () => navigate(makeArticleCreatePath(url, {h, shareToChat: true}))
|
||||
|
||||
let ul: Element
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
<script lang="ts">
|
||||
import {marked} from "marked"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {removeUndefined, tryCatch} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {fromNostrURI} from "@welshman/util"
|
||||
|
|
@ -119,7 +119,7 @@
|
|||
|
||||
if (href && isEntityPath(href)) {
|
||||
clickEvent.preventDefault()
|
||||
goto(href)
|
||||
navigate(href)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -21,12 +21,11 @@
|
|||
<Modal>
|
||||
<ModalBody>
|
||||
<div class="flex flex-col gap-8 items-center py-12 max-w-[16rem] m-auto w-full">
|
||||
<Link replaceState href="/settings/profile">
|
||||
<Link href="/settings/profile">
|
||||
<Profile inert pubkey={$user.pubkey} />
|
||||
</Link>
|
||||
<div class="grid grid-cols-3 gap-3 w-full">
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/alerts"
|
||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||
<Icon icon={Bell} size={5} />
|
||||
|
|
@ -34,7 +33,6 @@
|
|||
</Link>
|
||||
{#if Capacitor.getPlatform() !== "ios"}
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/wallet"
|
||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||
<Icon icon={Wallet} size={5} />
|
||||
|
|
@ -42,21 +40,18 @@
|
|||
</Link>
|
||||
{/if}
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/hosting"
|
||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||
<Icon icon={ServerPath} size={5} />
|
||||
Hosting
|
||||
</Link>
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/content"
|
||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||
<Icon icon={GalleryMinimalistic} size={5} />
|
||||
Content
|
||||
</Link>
|
||||
<Link
|
||||
replaceState
|
||||
href="/settings/privacy"
|
||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||
<Icon icon={Shield} size={5} />
|
||||
|
|
@ -64,9 +59,9 @@
|
|||
</Link>
|
||||
</div>
|
||||
<div class="flex gap-3 items-center opacity-75 text-sm">
|
||||
<Link replaceState href="/settings/theme">Theme</Link>
|
||||
<Link href="/settings/theme">Theme</Link>
|
||||
/
|
||||
<Link replaceState href="/settings/about">About</Link>
|
||||
<Link href="/settings/about">About</Link>
|
||||
/
|
||||
<Button onclick={logout}>Log Out</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -61,4 +61,4 @@
|
|||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
|
||||
<div bind:this={element}></div>
|
||||
<div bind:this={element} data-sveltekit-replacestate="true"></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {getAddress} from "@welshman/util"
|
||||
import {publishAsRelay} from "@welshman/app"
|
||||
import {Pinboard} from "@welshman/domain"
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
back()
|
||||
} else {
|
||||
pushToast({message: "Shelf created!"})
|
||||
goto(makeLibraryPath(url, getAddress(thunk.event)))
|
||||
navigate(makeLibraryPath(url, getAddress(thunk.event)))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
import {Profiles} from "@welshman/app"
|
||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||
import Settings from "@assets/icons/settings.svg?dataurl"
|
||||
import Sidebar from "@assets/icons/sidebar-minimalistic.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
|
|
@ -14,11 +14,12 @@
|
|||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
||||
import Search from "@app/components/Search.svelte"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import SpaceMenuDrawer from "@app/components/SpaceMenuDrawer.svelte"
|
||||
import {PLATFORM_RELAYS} from "@app/env"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {modal, popModal, pushDrawer, pushModal} from "@app/modal"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {goToChat, makeSpacePath} from "@app/routes"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {goToChat, lastSpaceUrl, makeSpacePath} from "@app/routes"
|
||||
import {deriveUserItem} from "@app/core"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -35,12 +36,26 @@
|
|||
|
||||
const showSearch = () => pushModal(Search)
|
||||
|
||||
const anySpaceNotifications = $derived(
|
||||
$userSpaceUrls.some(p => $notifications.has(makeSpacePath(p))),
|
||||
// The menu is reachable from every page, so it opens on the space the user is in, or the last one
|
||||
// they were in when they're somewhere else.
|
||||
const spaceUrl = $derived($lastSpaceUrl ?? PLATFORM_RELAYS[0] ?? $userSpaceUrls[0])
|
||||
|
||||
const spaceMenuIsOpen = $derived($modal?.component === SpaceMenuDrawer)
|
||||
|
||||
const spaceMenuLabel = $derived(spaceMenuIsOpen ? "Close space menu" : "Open space menu")
|
||||
|
||||
const toggleSpaceMenu = () =>
|
||||
spaceMenuIsOpen ? popModal() : pushDrawer(SpaceMenuDrawer, {url: spaceUrl})
|
||||
|
||||
const otherSpaceNotifications = $derived(
|
||||
$userSpaceUrls.some(url => url !== spaceUrl && $notifications.has(makeSpacePath(url))),
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class={cx("primary-nav", {"justify-between": PLATFORM_RELAYS.length === 0})}>
|
||||
<div
|
||||
class={cx("primary-nav ml-sai mt-sai mb-sai hidden md:flex", {
|
||||
"justify-between": PLATFORM_RELAYS.length === 0,
|
||||
})}>
|
||||
<PrimaryNavSpaces />
|
||||
{#if PLATFORM_RELAYS.length > 0}
|
||||
<Divider />
|
||||
|
|
@ -71,9 +86,17 @@
|
|||
<div class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-surface md:hidden">
|
||||
</div>
|
||||
<div
|
||||
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-line bg-surface md:hidden">
|
||||
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-(--mobile-nav-height) border border-line bg-surface md:hidden">
|
||||
<div class="flex h-full justify-between px-2">
|
||||
<div class="flex items-center gap-6">
|
||||
{#if spaceUrl}
|
||||
<PrimaryNavItem
|
||||
onclick={toggleSpaceMenu}
|
||||
aria-label={spaceMenuLabel}
|
||||
notification={otherSpaceNotifications}>
|
||||
<ImageIcon alt={spaceMenuLabel} src={Sidebar} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
{#if PLATFORM_RELAYS.length === 1}
|
||||
<PrimaryNavItemSpace url={PLATFORM_RELAYS[0]} />
|
||||
{:else}
|
||||
|
|
@ -84,11 +107,6 @@
|
|||
<PrimaryNavItem href="/chat" onclick={chatHandler} notification={$notifications.has("/chat")}>
|
||||
<ImageIcon alt="Messages" src={Letter} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{#if PLATFORM_RELAYS.length !== 1}
|
||||
<PrimaryNavItem href="/spaces" notification={anySpaceNotifications}>
|
||||
<ImageIcon alt="Spaces" src={Widget} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryNavItem onclick={showSettingsMenu}>
|
||||
{#if $userProfile?.picture()}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import {relays} from "@app/core"
|
||||
import {navigate} from "@app/modal"
|
||||
import {makeSpacePath, makeSpaceEntryPath} from "@app/routes"
|
||||
import {notifications} from "@app/notifications"
|
||||
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
const onClick = () => {
|
||||
const entryPath = makeSpaceEntryPath(url)
|
||||
|
||||
goto(entryPath, {replaceState: entryPath === $page.url.pathname})
|
||||
navigate(entryPath, {replaceState: entryPath === $page.url.pathname})
|
||||
}
|
||||
|
||||
const path = makeSpacePath(url)
|
||||
|
|
|
|||
|
|
@ -12,11 +12,19 @@
|
|||
import {notifications} from "@app/notifications"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
overflow?: boolean
|
||||
}
|
||||
|
||||
const {overflow = true}: Props = $props()
|
||||
|
||||
let windowHeight = $state(0)
|
||||
|
||||
const itemHeight = 56
|
||||
const navPadding = 8 * itemHeight
|
||||
const itemLimit = $derived(Math.max(0, (windowHeight - navPadding) / itemHeight))
|
||||
const itemLimit = $derived(
|
||||
overflow ? Math.max(0, (windowHeight - navPadding) / itemHeight) : $userSpaceUrls.length,
|
||||
)
|
||||
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||
const otherSpaceNotifications = $derived(
|
||||
secondarySpaceUrls.some(url => $notifications.has(makeSpacePath(url))),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import {removeUndefined, spec} from "@welshman/lib"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||
|
|
@ -19,7 +18,7 @@
|
|||
import ProfileMenu from "@app/components/ProfileMenu.svelte"
|
||||
import {messagingRelayLists, profiles, relayManagement} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {makeProfilePath} from "@app/routes"
|
||||
|
||||
|
|
@ -34,7 +33,7 @@
|
|||
|
||||
const back = () => history.back()
|
||||
|
||||
const viewProfile = () => goto(makeProfilePath(pubkey), {replaceState: true})
|
||||
const viewProfile = () => navigate(makeProfilePath(pubkey), {replaceState: true})
|
||||
|
||||
const report = (error: string | undefined, message: string) => {
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {formatTimestamp} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {tagValue, tagSpec} from "@welshman/util"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const h = tagValue(tagSpec("h"), event.tags)
|
||||
|
||||
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
||||
const onClick = () => navigate(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
||||
</script>
|
||||
|
||||
<Button class="cv card card-interactive" onclick={onClick}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import {onDestroy, onMount} from "svelte"
|
||||
import {readable} from "svelte/store"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {debounce} from "throttle-debounce"
|
||||
import cx from "classnames"
|
||||
|
|
@ -341,7 +341,7 @@
|
|||
|
||||
const scrollToBottom = () => {
|
||||
if (!isNaN(at)) {
|
||||
goto($page.url.pathname, {replaceState: true})
|
||||
navigate($page.url.pathname, {replaceState: true})
|
||||
} else {
|
||||
element?.scrollTo({top: 0, behavior: "smooth"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import type {RoomMeta} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
|
||||
const onsubmit = (room: RoomMeta) => navigate(makeSpacePath(url, room.h))
|
||||
</script>
|
||||
|
||||
<RoomForm {url} {onsubmit}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {MembershipStatus, publish} from "@welshman/app"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
|
|
@ -16,7 +15,7 @@
|
|||
import {joinRoom, leaveRoom} from "@app/access"
|
||||
import {deriveUserIsRoomAdmin, deriveUserRoomMembershipStatus} from "@app/rooms"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -63,7 +62,7 @@
|
|||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
await $roomLists.removeRoom(h, url).then(publish)
|
||||
goto(makeSpacePath(url))
|
||||
navigate(makeSpacePath(url))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import type {RoomMeta} from "@welshman/app"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
|
||||
const back = () => history.back()
|
||||
|
||||
const onsubmit = () => goto(makeSpacePath(url, h))
|
||||
const onsubmit = () => navigate(makeSpacePath(url, h))
|
||||
</script>
|
||||
|
||||
<RoomForm {url} {onsubmit} {initialValues}>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = () => shareTo(selection, share, {replaceState: true})
|
||||
const submit = () => shareTo(selection, share)
|
||||
|
||||
let term = $state("")
|
||||
let selection = $state("")
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
const back = () => history.back()
|
||||
|
||||
const onSubmit = () => {
|
||||
shareTo(makeRoomPath(url, selection), {type: "event", value: event}, {replaceState: true})
|
||||
shareTo(makeRoomPath(url, selection), {type: "event", value: event})
|
||||
}
|
||||
|
||||
const toggleRoom = (h: string) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {parse, renderAsHtml} from "@welshman/content"
|
||||
import {publish} from "@welshman/app"
|
||||
|
|
@ -18,7 +17,7 @@
|
|||
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
||||
import {publishLeaveRequest} from "@app/access"
|
||||
import {roomLists} from "@app/core"
|
||||
import {pushModal, clearModals} from "@app/modal"
|
||||
import {clearModals, navigate, pushModal} from "@app/modal"
|
||||
import {removeTrustedRelay} from "@app/settings"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -28,7 +27,7 @@
|
|||
|
||||
const {url, error}: Props = $props()
|
||||
|
||||
const back = () => goto("/home")
|
||||
const back = () => navigate("/home")
|
||||
|
||||
const requestAccess = () => pushModal(SpaceAccessRequest, {url, callback: clearModals})
|
||||
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
loading = false
|
||||
}
|
||||
|
||||
goto("/home")
|
||||
navigate("/home")
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
interface Props {
|
||||
back?: () => unknown
|
||||
|
|
@ -18,16 +16,18 @@
|
|||
[key: string]: any
|
||||
}
|
||||
|
||||
const {back = () => goto(makeSpacePath(url)), leading, title, action, ...props}: Props = $props()
|
||||
const {back, leading, title, action, ...props}: Props = $props()
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
</script>
|
||||
|
||||
<PageBar {...props}>
|
||||
<div class="flex">
|
||||
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
||||
<Icon icon={ArrowLeft} size={7} />
|
||||
</Button>
|
||||
{#if back}
|
||||
<Button onclick={back} aria-label="Go back" class="place-self-start pr-3 md:hidden">
|
||||
<Icon icon={ArrowLeft} size={7} />
|
||||
</Button>
|
||||
{/if}
|
||||
<div class="flex grow items-center justify-between gap-4">
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<div class="flex min-w-0 items-start gap-2">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {publish} from "@welshman/app"
|
||||
import {preventDefault} from "@lib/html"
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
loading = false
|
||||
}
|
||||
|
||||
goto("/home")
|
||||
navigate("/home")
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {maybe} from "@welshman/lib"
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import {slideAndFade} from "@lib/transition"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
const qp = data.code ? `?code=${encodeURIComponent(data.code)}` : ""
|
||||
|
||||
pushToast({message: "Welcome to the room!"})
|
||||
await goto(path + qp)
|
||||
await navigate(path + qp)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,16 +95,13 @@
|
|||
`${$shouldNotify ? "Turn off" : "Turn on"} notifications`,
|
||||
)}
|
||||
{:else}
|
||||
<Link href="/settings/alerts" replaceState class="button button-neutral w-full justify-start">
|
||||
<Link href="/settings/alerts" class="button button-neutral w-full justify-start">
|
||||
<Icon size={4} icon={Bell} />
|
||||
Enable notifications
|
||||
</Link>
|
||||
{/if}
|
||||
{#if HOSTING_ENABLED && $hostedRelay.relay}
|
||||
<Link
|
||||
href={makeSpacePath(url, "admin")}
|
||||
replaceState
|
||||
class="button button-neutral w-full justify-start">
|
||||
<Link href={makeSpacePath(url, "admin")} class="button button-neutral w-full justify-start">
|
||||
<Icon size={4} icon={ServerPath} />
|
||||
Hosting settings
|
||||
</Link>
|
||||
|
|
|
|||
38
src/app/components/SpaceMenuDrawer.svelte
Normal file
38
src/app/components/SpaceMenuDrawer.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import {translate} from "@lib/transition"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
||||
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
const {url}: Props = $props()
|
||||
|
||||
const duration = 200
|
||||
|
||||
const parallax = 10
|
||||
|
||||
const slide = (delay: number) => ({
|
||||
axis: "x" as const,
|
||||
offset: -100,
|
||||
unit: "cqw",
|
||||
duration,
|
||||
delay,
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="@container flex h-full min-h-0 w-full">
|
||||
<div
|
||||
class="primary-nav flex overflow-y-auto pb-2"
|
||||
in:translate={slide(0)}
|
||||
out:translate={slide(parallax)}>
|
||||
<PrimaryNavSpaces overflow={false} />
|
||||
</div>
|
||||
<div class="flex w-0 min-w-0 grow" in:translate={slide(parallax)} out:translate={slide(0)}>
|
||||
<SecondaryNav visible class="secondary-nav--mobile-space mt-0 h-full w-0 min-w-0 grow pb-0">
|
||||
<SpaceMenu {url} mobile />
|
||||
</SecondaryNav>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
const openMenu = () => {
|
||||
if (mobile) {
|
||||
pushModal(SpaceMenuMobile, {url})
|
||||
pushModal(SpaceMenuMobile, {url}, {nested: true})
|
||||
} else {
|
||||
showMenu = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,10 @@
|
|||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
replaceState?: boolean
|
||||
tooltip?: boolean
|
||||
}
|
||||
|
||||
const {url, h, replaceState = false, tooltip = true}: Props = $props()
|
||||
const {url, h, tooltip = true}: Props = $props()
|
||||
|
||||
const room = $rooms.forRoom(url, h)
|
||||
const roomType = $derived(getRoomType($room))
|
||||
|
|
@ -28,9 +27,9 @@
|
|||
</script>
|
||||
|
||||
{#if roomType === RoomType.Voice}
|
||||
<VoiceRoomItem {url} {h} {replaceState} {notification} />
|
||||
<VoiceRoomItem {url} {h} {notification} />
|
||||
{:else}
|
||||
<SecondaryNavItem href={path} title={tooltip ? roomName : ""} {replaceState} {notification}>
|
||||
<SecondaryNavItem href={path} title={tooltip ? roomName : ""} {notification}>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
{#if $isMuted}
|
||||
<Icon icon={BellOff} size={4} class="ml-auto opacity-50" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {remove} from "@welshman/lib"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {publish} from "@welshman/app"
|
||||
|
|
@ -18,7 +17,7 @@
|
|||
import InfoSignatures from "@app/components/InfoSignatures.svelte"
|
||||
import {relaysPendingTrust} from "@app/policies"
|
||||
import {addTrustedRelay, removeTrustedRelay} from "@app/settings"
|
||||
import {popModal, pushModal} from "@app/modal"
|
||||
import {navigate, popModal, pushModal} from "@app/modal"
|
||||
import {roomLists} from "@app/core"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -35,7 +34,7 @@
|
|||
try {
|
||||
await $roomLists.removeRelay(url).then(publish)
|
||||
await removeTrustedRelay(url)
|
||||
goto("/home")
|
||||
navigate("/home")
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {derived} from "svelte/store"
|
||||
import {filter, formatTimestamp, max, spec} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||
const title = tagValue(tagSpec("title"), event.tags)
|
||||
const path = makeThreadPath(url, event.id)
|
||||
const onClick = () => goto(path)
|
||||
const onClick = () => navigate(path)
|
||||
</script>
|
||||
|
||||
{#if mobile}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {goto} from "$app/navigation"
|
||||
import {makeRoomKey} from "@welshman/app"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
|
|
@ -10,7 +9,7 @@
|
|||
import RoomImage from "@app/components/RoomImage.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
||||
import {
|
||||
|
|
@ -30,11 +29,10 @@
|
|||
interface Props {
|
||||
url: string
|
||||
h: string
|
||||
replaceState?: boolean
|
||||
notification?: boolean
|
||||
}
|
||||
|
||||
const {url, h, replaceState = false, notification = false}: Props = $props()
|
||||
const {url, h, notification = false}: Props = $props()
|
||||
|
||||
// Beyond this many participants, a full name+avatar row per person makes the
|
||||
// sidebar item too tall — fall back to the compact ProfileCircles cluster instead.
|
||||
|
|
@ -59,7 +57,7 @@
|
|||
}
|
||||
|
||||
e.preventDefault()
|
||||
await goto(makeRoomPath(url, h), {replaceState})
|
||||
await navigate(makeRoomPath(url, h))
|
||||
pushModal(VoiceRoomJoinDialog, {url, h})
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +74,6 @@
|
|||
|
||||
<SecondaryNavItem
|
||||
href={makeRoomPath(url, h)}
|
||||
{replaceState}
|
||||
{notification}
|
||||
onclick={handleClick}
|
||||
class={cx("items-start!", isActive && "bg-surface! text-content!")}>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {publish} from "@welshman/app"
|
||||
import RelayForm, {type RelayFormValues} from "@app/components/hosting/RelayForm.svelte"
|
||||
import PaymentDialog from "@app/components/hosting/PaymentDialog.svelte"
|
||||
import PaymentSetup from "@app/components/hosting/PaymentSetup.svelte"
|
||||
import {roomLists, user} from "@app/core"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {HOSTING_RELAY_DOMAIN} from "@app/env"
|
||||
import {
|
||||
|
|
@ -56,7 +55,7 @@
|
|||
|
||||
const tenant = await getTenant($user.pubkey)
|
||||
|
||||
await goto(makeSpacePath(url, "admin"), {replaceState: true})
|
||||
await navigate(makeSpacePath(url, "admin"), {replaceState: true})
|
||||
|
||||
if (values.plan_id !== "free" && !autopayConfigured(tenant)) {
|
||||
const invoice = selectPayableInvoice(await listTenantInvoices($user.pubkey))
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type {Component} from "svelte"
|
|||
import {get, writable} from "svelte/store"
|
||||
import {randomId, always, assoc, Emitter} from "@welshman/lib"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import type {DialogSize} from "@lib/components/Dialog.svelte"
|
||||
|
||||
|
|
@ -56,6 +57,16 @@ const setModalHash = (hash: string, replace: boolean) => {
|
|||
modalHash.set(hash)
|
||||
}
|
||||
|
||||
// An open modal owns the current history entry, so a navigation that drops it takes that entry over
|
||||
const closesModal = (path: string) => {
|
||||
const hash = get(modalHash)
|
||||
|
||||
return hash !== "" && !path.endsWith(hash)
|
||||
}
|
||||
|
||||
export const navigate = (path: string, options?: Parameters<typeof goto>[1]) =>
|
||||
goto(path, {...options, replaceState: options?.replaceState || closesModal(path)})
|
||||
|
||||
export const modalStack = deriveDeduplicated([modalHash, modals], ([$hash, $modals]) => {
|
||||
return getIdsFromHash($hash)
|
||||
.map(id => $modals[id])
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
type Token,
|
||||
} from "@capacitor/push-notifications"
|
||||
import type {PluginListenerHandle} from "@capacitor/core"
|
||||
import {goto} from "$app/navigation"
|
||||
import {navigate} from "@app/modal"
|
||||
import {assoc, call, now, on, parseJson, poll, spec, throttle, uniq} from "@welshman/lib"
|
||||
import {LOCAL_RELAY_URL} from "@welshman/net"
|
||||
import type {RepositoryUpdate} from "@welshman/net"
|
||||
|
|
@ -140,9 +140,9 @@ export const onPushNotificationAction = async (action: ActionPerformed) => {
|
|||
const target = event?.kind === WRAP ? await loadNotificationRumor(event) : event
|
||||
const path = target && makeEventPath(target, [relay])
|
||||
|
||||
// Kinds we have no route for get a coracle.social permalink, which goto refuses
|
||||
// Kinds we have no route for get a coracle.social permalink, which navigate refuses
|
||||
if (path && !path.includes("://")) {
|
||||
goto(path)
|
||||
navigate(path)
|
||||
} else if (event?.kind === WRAP) {
|
||||
goToChat()
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import theme from "tailwindcss/defaultTheme"
|
||||
import {get} from "svelte/store"
|
||||
import {get, writable} from "svelte/store"
|
||||
import * as nip19 from "nostr-tools/nip19"
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import {identity} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
|
|
@ -23,10 +22,10 @@ import {
|
|||
import {app, messagingRelayLists, relays, user} from "@app/core"
|
||||
import {makeChatId} from "@app/chats"
|
||||
import {entityLink, PLATFORM_URL, PLATFORM_RELAYS} from "@app/env"
|
||||
import {encodeRelay} from "@app/relays"
|
||||
import {decodeRelay, encodeRelay} from "@app/relays"
|
||||
import {DM_KINDS} from "@app/content"
|
||||
import {ROOM} from "@app/rooms"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||
|
||||
// State
|
||||
|
|
@ -35,10 +34,15 @@ export let lastChatUrl: string | undefined = undefined
|
|||
|
||||
export const lastPageBySpaceUrl = new Map<string, string>()
|
||||
|
||||
// The space the user was in most recently, so the space menu can be opened from a page that isn't
|
||||
// in a space. A store because it's read from markup, unlike lastChatUrl.
|
||||
export const lastSpaceUrl = writable<string | undefined>(undefined)
|
||||
|
||||
export const setupHistory = () =>
|
||||
page.subscribe($page => {
|
||||
if ($page.params.relay) {
|
||||
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
||||
lastSpaceUrl.set(decodeRelay($page.params.relay))
|
||||
}
|
||||
|
||||
if ($page.params.chat) {
|
||||
|
|
@ -62,9 +66,9 @@ export const goToChat = (pubkeys: string[] = [], options: {replaceState?: boolea
|
|||
if (messagingRelayLists.get().urls(user.get().pubkey).get().length === 0) {
|
||||
pushModal(ChatEnable, {next: () => goToChat(pubkeys, options)})
|
||||
} else if (pubkeys.length === 0) {
|
||||
goto(lastChatUrl ?? "/chat", options)
|
||||
navigate(lastChatUrl ?? "/chat", options)
|
||||
} else {
|
||||
goto(makeChatPath(pubkeys), options)
|
||||
navigate(makeChatPath(pubkeys), options)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,17 +108,19 @@ export const makeSpaceEntryPath = (url: string) => {
|
|||
}
|
||||
|
||||
export const goToSpace = (url: string, options: {replaceState?: boolean} = {}) =>
|
||||
goto(makeSpaceEntryPath(url), options)
|
||||
navigate(makeSpaceEntryPath(url), options)
|
||||
|
||||
export const goToMovedSpace = (oldUrl: string, newUrl: string) =>
|
||||
goto(get(page).url.pathname.replace(encodeRelay(oldUrl), encodeRelay(newUrl)))
|
||||
navigate(get(page).url.pathname.replace(encodeRelay(oldUrl), encodeRelay(newUrl)))
|
||||
|
||||
export const goToHome = () => {
|
||||
if (PLATFORM_RELAYS.length > 0) {
|
||||
return goto(makeSpaceEntryPath(PLATFORM_RELAYS[0]) + get(page).url.hash, {replaceState: true})
|
||||
return navigate(makeSpaceEntryPath(PLATFORM_RELAYS[0]) + get(page).url.hash, {
|
||||
replaceState: true,
|
||||
})
|
||||
}
|
||||
|
||||
return goto("/home" + get(page).url.hash)
|
||||
return navigate("/home" + get(page).url.hash)
|
||||
}
|
||||
|
||||
// Content types, events
|
||||
|
|
@ -262,6 +268,6 @@ export const goToEvent = (event: TrustedEvent, options: Record<string, any> = {}
|
|||
} else if (!scrollToEvent(event.id)) {
|
||||
const replaceState = path.replace(/\?.*$/, "") === get(page).url.pathname
|
||||
|
||||
goto(path, {replaceState, ...options})
|
||||
navigate(path, {replaceState, ...options})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import {writable} from "svelte/store"
|
||||
import {goto} from "$app/navigation"
|
||||
import {Capacitor, registerPlugin} from "@capacitor/core"
|
||||
import type {PluginListenerHandle} from "@capacitor/core"
|
||||
import {noop} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {app, relays} from "@app/core"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {navigate, pushModal} from "@app/modal"
|
||||
import {makeSpaceChatPath} from "@app/routes"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {UPLOAD_MIME_TYPES} from "@app/uploads"
|
||||
|
|
@ -20,13 +19,13 @@ export type Share =
|
|||
|
||||
export const pendingShare = writable<Maybe<Share>>(undefined)
|
||||
|
||||
// Set pendingShare after goto so the current view doesn't pop it
|
||||
// Set pendingShare after navigating so the current view doesn't pop it
|
||||
export const shareTo = async (
|
||||
path: string,
|
||||
share: Share,
|
||||
options: {replaceState?: boolean} = {},
|
||||
) => {
|
||||
await goto(path, options)
|
||||
await navigate(path, options)
|
||||
|
||||
pendingShare.set(share)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type {Component} from "svelte"
|
||||
import {fade, translate} from "@lib/transition"
|
||||
import {fade} from "@lib/transition"
|
||||
|
||||
type Props = {
|
||||
onClose?: any
|
||||
|
|
@ -13,7 +13,8 @@
|
|||
const {onClose, children}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="drawer fixed inset-0 z-modal">
|
||||
<div
|
||||
class="drawer fixed inset-x-0 top-0 z-modal bottom-[calc(var(--mobile-nav-height)+var(--saib))] md:bottom-0">
|
||||
<button
|
||||
aria-label="Close drawer"
|
||||
class="absolute inset-0 cursor-pointer bg-black opacity-50"
|
||||
|
|
@ -21,8 +22,7 @@
|
|||
onclick={onClose}>
|
||||
</button>
|
||||
<div
|
||||
class="bg-surface scroll-container py-sai pr-sair absolute bottom-0 right-0 top-0 w-72 overflow-auto lg:w-96"
|
||||
transition:translate={{axis: "x", duration: 300}}>
|
||||
class="pt-sai pl-sai border-line absolute bottom-0 left-0 top-0 flex w-full overflow-hidden border-r border-solid shadow-xl md:w-80 md:pb-sai lg:w-96">
|
||||
<children.component {...children.props} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type {Snippet} from "svelte"
|
||||
import cx from "classnames"
|
||||
import {goto} from "$app/navigation"
|
||||
import {stopPropagation} from "@lib/html"
|
||||
import {navigate} from "@app/modal"
|
||||
|
||||
const {
|
||||
children,
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
if (!external) {
|
||||
e.preventDefault()
|
||||
|
||||
goto(href, {replaceState})
|
||||
navigate(href, {replaceState})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
</script>
|
||||
|
||||
{#if onclick}
|
||||
<Button {onclick} data-tip={title} class={className}>
|
||||
<Button {...restProps} {onclick} data-tip={title} class={className}>
|
||||
{@render children?.()}
|
||||
{#if notification}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
{/if}
|
||||
</Button>
|
||||
{:else}
|
||||
<Link {href} data-tip={title} class={className}>
|
||||
<Link {...restProps} {href} data-tip={title} class={className}>
|
||||
{@render children?.()}
|
||||
{#if notification}
|
||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
||||
|
|
|
|||
|
|
@ -25,14 +25,7 @@
|
|||
import {fade} from "@lib/transition"
|
||||
import {page} from "$app/stores"
|
||||
|
||||
const {
|
||||
children,
|
||||
href = "",
|
||||
title = "",
|
||||
notification = false,
|
||||
replaceState = false,
|
||||
...restProps
|
||||
} = $props()
|
||||
const {children, href = "", title = "", notification = false, ...restProps} = $props()
|
||||
|
||||
const active = $derived($page.url.pathname === href)
|
||||
const wrapperClass = $derived(
|
||||
|
|
@ -48,12 +41,7 @@
|
|||
</script>
|
||||
|
||||
{#if href}
|
||||
<a
|
||||
{href}
|
||||
{...restProps}
|
||||
data-tip={title}
|
||||
data-sveltekit-replacestate={replaceState}
|
||||
class={wrapperClass}>
|
||||
<a {href} {...restProps} data-tip={title} class={wrapperClass}>
|
||||
{@render children?.()}
|
||||
{#if notification}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
|
||||
--mobile-nav-height: 3.5rem;
|
||||
|
||||
text-size-adjust: 100%;
|
||||
font-size: 110%;
|
||||
|
|
@ -203,6 +204,7 @@
|
|||
|
||||
body.keyboard-open {
|
||||
--saib: 0px;
|
||||
--mobile-nav-height: 0px;
|
||||
}
|
||||
body.keyboard-open .hide-on-keyboard {
|
||||
@apply hidden;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
@layer components {
|
||||
.primary-nav {
|
||||
@apply ml-sai mt-sai mb-sai z-popover bg-surface border-line relative isolate hidden w-14 shrink-0 flex-col border-r border-solid pt-2 md:flex;
|
||||
@apply z-popover bg-surface border-line relative isolate w-14 shrink-0 flex-col border-r border-solid pt-2;
|
||||
}
|
||||
|
||||
.drawer .primary-nav .tip[data-tip]::before {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.primary-nav__nav-item {
|
||||
|
|
|
|||
|
|
@ -13,19 +13,27 @@ export type TranslateParams = {
|
|||
duration?: number
|
||||
easing?: (t: number) => number
|
||||
axis?: "x" | "y"
|
||||
reverse?: boolean
|
||||
offset?: number
|
||||
unit?: string
|
||||
}
|
||||
|
||||
export const translate = (
|
||||
node: Element,
|
||||
{delay = 0, duration = 400, easing = cubicOut, axis = "y", reverse = false}: TranslateParams = {},
|
||||
{
|
||||
delay = 0,
|
||||
duration = 400,
|
||||
easing = cubicOut,
|
||||
axis = "y",
|
||||
offset = 100,
|
||||
unit = "%",
|
||||
}: TranslateParams = {},
|
||||
) => {
|
||||
return {
|
||||
delay,
|
||||
duration,
|
||||
easing,
|
||||
css: (t: number) => {
|
||||
const p = reverse ? `${t * 100}%` : `${100 - t * 100}%`
|
||||
const p = `${offset - t * offset}${unit}`
|
||||
|
||||
if (axis === "x") {
|
||||
return `transform: translateX(${p})`
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||
import Home from "@assets/icons/home.svg?dataurl"
|
||||
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||
|
|
@ -16,18 +15,22 @@
|
|||
import SpaceMembersSummary from "@app/components/SpaceMembersSummary.svelte"
|
||||
import SpaceFeaturedContent from "@app/components/SpaceFeaturedContent.svelte"
|
||||
import {relays} from "@app/core"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
const relay = $relays.one(url)
|
||||
const showMenu = () => goto(makeSpacePath(url))
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
{#snippet leading()}
|
||||
<Icon icon={Home} />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Space Details</strong>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
<PageContent class="flex flex-col gap-4 p-4">
|
||||
<Button onclick={showMenu} class="button button-neutral md:hidden place-self-start">
|
||||
<Icon icon={ArrowLeft} size={7} /> Go Back
|
||||
</Button>
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="relative flex gap-4">
|
||||
<div class="relative">
|
||||
|
|
|
|||
|
|
@ -1,31 +1,32 @@
|
|||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from "$app/navigation"
|
||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||
import RelayDetailCard from "@app/components/hosting/RelayDetailCard.svelte"
|
||||
import ActivityFeed from "@app/components/hosting/ActivityFeed.svelte"
|
||||
import {deriveHostedRelay, deriveRelayActivity} from "@app/hosting"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
const url = decodeRelay($page.params.relay!)
|
||||
const hostedRelay = deriveHostedRelay(url)
|
||||
const relayActivity = deriveRelayActivity(derived(hostedRelay, $h => $h.relay?.id))
|
||||
|
||||
const showMenu = () => goto(makeSpacePath(url))
|
||||
</script>
|
||||
|
||||
<SpaceBar>
|
||||
{#snippet leading()}
|
||||
<Icon icon={Server} />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<strong>Hosting settings</strong>
|
||||
{/snippet}
|
||||
</SpaceBar>
|
||||
|
||||
<PageContent class="flex flex-col gap-4 p-4">
|
||||
<Button onclick={showMenu} class="button button-neutral md:hidden place-self-start">
|
||||
<Icon icon={ArrowLeft} size={7} /> Go Back
|
||||
</Button>
|
||||
{#if $hostedRelay.relay}
|
||||
<RelayDetailCard relay={$hostedRelay.relay} />
|
||||
<ActivityFeed activity={$relayActivity.activity} loading={$relayActivity.loading} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue