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`
|
- Import from `app/modal.ts` or `app/toast.ts`
|
||||||
- Pass component objects with parameters
|
- Pass component objects with parameters
|
||||||
- Use `$state.snapshot` if calling component might unmount
|
- 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
|
## Development Workflow
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1565,14 +1565,14 @@ Acceptance:
|
||||||
|
|
||||||
### US-110 — See another space's unread activity from a phone
|
### 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
|
space wants attention, so that I don't have to leave the room I'm reading to
|
||||||
find out.
|
find out.
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- While bob has a room open, a message in a space he isn't in raises an unread
|
- 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`.
|
`/spaces`.
|
||||||
- A message in another room of the space he's already in does not; that space's
|
- A message in another room of the space he's already in does not; that space's
|
||||||
own room list carries it.
|
own room list carries it.
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,9 @@ const settingRow = (page: Page, label: string) =>
|
||||||
const spaceNavItem = (page: Page, name: string) =>
|
const spaceNavItem = (page: Page, name: string) =>
|
||||||
page.locator(`.primary-nav [data-tip^="${name}"]`)
|
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
|
// The phone's bottom bar opens the space menu in a drawer; the desktop rail has no equivalent,
|
||||||
// has an accessible name — both icons are masked svgs. Below tailwind's md breakpoint the rail is
|
// since the menu is always on screen there.
|
||||||
// display:none, so on a phone the visible one is the bar's.
|
const spaceMenuNavItem = (page: Page) => page.getByRole("button", {name: "Open space menu"})
|
||||||
const spacesNavItem = (page: Page) => page.locator('a[href="/spaces"]:visible')
|
|
||||||
|
|
||||||
// The space menu's header, the one button in the secondary nav carrying the relay's address.
|
// The space menu's header, the one button in the secondary nav carrying the relay's address.
|
||||||
const spaceMenu = (page: Page, url: string) =>
|
const spaceMenu = (page: Page, url: string) =>
|
||||||
|
|
@ -53,8 +52,7 @@ const spaceMenu = (page: Page, url: string) =>
|
||||||
const roomLink = (page: Page, name: string) =>
|
const roomLink = (page: Page, name: string) =>
|
||||||
page.locator(".space-menu__scroll").getByRole("link", {name})
|
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
|
// The room's page bar carries a search button and the detail button, in that order.
|
||||||
// detail button, in that order.
|
|
||||||
const openRoomDetail = (page: Page) =>
|
const openRoomDetail = (page: Page) =>
|
||||||
page.locator('[data-component="PageBar"]').getByRole("button").last().click()
|
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},
|
context: {viewport: {width: 390, height: 844}, hasTouch: true},
|
||||||
})
|
})
|
||||||
|
|
||||||
const spacesButton = spacesNavItem(bob)
|
const menuButton = spaceMenuNavItem(bob)
|
||||||
|
|
||||||
await expect(spacesButton).toBeVisible()
|
await expect(menuButton).toBeVisible()
|
||||||
await expect(unreadDot(spacesButton)).toHaveCount(0)
|
await expect(unreadDot(menuButton)).toHaveCount(0)
|
||||||
|
|
||||||
const inOther = await as(users.alice, roomPath(other.url, "general"))
|
const inOther = await as(users.alice, roomPath(other.url, "general"))
|
||||||
|
|
||||||
await send(inOther, "the server is on fire")
|
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
|
// 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"))
|
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 bob.goto(spacePath(space.url))
|
||||||
|
|
||||||
await expect(unreadDot(roomLink(bob, "Random"))).toBeVisible()
|
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
|
// 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.
|
// left corner of the viewport is outside it.
|
||||||
const dismissMenu = (page: Page) => page.mouse.move(0, 0)
|
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 room's page bar carries 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.
|
|
||||||
const pageBar = (page: Page) => page.locator('[data-component="PageBar"]')
|
const pageBar = (page: Page) => page.locator('[data-component="PageBar"]')
|
||||||
|
|
||||||
const openRoomDetail = (page: Page) => pageBar(page).getByRole("button").last().click()
|
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")}$`))
|
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 all")).toBeVisible()
|
||||||
await expect(page.getByText("morning!")).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">
|
<script lang="ts">
|
||||||
import {fly} from "svelte/transition"
|
import {fly} from "svelte/transition"
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
const goToRoom = () => {
|
const goToRoom = () => {
|
||||||
if (!$callTargetRoom) return
|
if (!$callTargetRoom) return
|
||||||
void goto(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
|
void navigate(makeRoomPath($callTargetRoom.url, $callTargetRoom.h))
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleMute = async () => {
|
const toggleMute = async () => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {onDestroy, onMount} from "svelte"
|
import {onDestroy, onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {
|
import {
|
||||||
ago,
|
ago,
|
||||||
int,
|
int,
|
||||||
|
|
@ -45,7 +44,7 @@
|
||||||
import {userSettingsValues} from "@app/settings"
|
import {userSettingsValues} from "@app/settings"
|
||||||
import {deriveChat, makeChatId} from "@app/chats"
|
import {deriveChat, makeChatId} from "@app/chats"
|
||||||
import {makeFeedContext} from "@app/feeds"
|
import {makeFeedContext} from "@app/feeds"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import {DraftKey, type Draft} from "@app/drafts"
|
import {DraftKey, type Draft} from "@app/drafts"
|
||||||
import {prependParent} from "@app/rooms"
|
import {prependParent} from "@app/rooms"
|
||||||
import {pendingShare, type Share} from "@app/share"
|
import {pendingShare, type Share} from "@app/share"
|
||||||
|
|
@ -73,7 +72,7 @@
|
||||||
? pushModal(ProfileDetail, {pubkey: others[0]})
|
? pushModal(ProfileDetail, {pubkey: others[0]})
|
||||||
: pushModal(ChatMembers, {pubkeys: others})
|
: pushModal(ChatMembers, {pubkeys: others})
|
||||||
|
|
||||||
const back = () => goto("/chat")
|
const back = () => navigate("/chat")
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
parent = event
|
parent = event
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
import CalendarMinimalistic from "@assets/icons/calendar-minimalistic.svg?dataurl"
|
||||||
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
|
import StarFallMinimalistic from "@assets/icons/star-fall-minimalistic.svg?dataurl"
|
||||||
import NotesMinimalistic from "@assets/icons/notes-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 Revote from "@assets/icons/revote.svg?dataurl"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.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 {makeArticleCreatePath} from "@app/routes"
|
||||||
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
|
import CalendarEventCreate from "@app/components/CalendarEventCreate.svelte"
|
||||||
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
import ThreadCreate from "@app/components/ThreadCreate.svelte"
|
||||||
|
|
@ -35,7 +34,7 @@
|
||||||
|
|
||||||
const createPoll = () => pushModal(PollCreate, {url, h, shareToChat: true})
|
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
|
let ul: Element
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {marked} from "marked"
|
import {marked} from "marked"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {removeUndefined, tryCatch} from "@welshman/lib"
|
import {removeUndefined, tryCatch} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {fromNostrURI} from "@welshman/util"
|
import {fromNostrURI} from "@welshman/util"
|
||||||
|
|
@ -119,7 +119,7 @@
|
||||||
|
|
||||||
if (href && isEntityPath(href)) {
|
if (href && isEntityPath(href)) {
|
||||||
clickEvent.preventDefault()
|
clickEvent.preventDefault()
|
||||||
goto(href)
|
navigate(href)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,11 @@
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="flex flex-col gap-8 items-center py-12 max-w-[16rem] m-auto w-full">
|
<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} />
|
<Profile inert pubkey={$user.pubkey} />
|
||||||
</Link>
|
</Link>
|
||||||
<div class="grid grid-cols-3 gap-3 w-full">
|
<div class="grid grid-cols-3 gap-3 w-full">
|
||||||
<Link
|
<Link
|
||||||
replaceState
|
|
||||||
href="/settings/alerts"
|
href="/settings/alerts"
|
||||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||||
<Icon icon={Bell} size={5} />
|
<Icon icon={Bell} size={5} />
|
||||||
|
|
@ -34,7 +33,6 @@
|
||||||
</Link>
|
</Link>
|
||||||
{#if Capacitor.getPlatform() !== "ios"}
|
{#if Capacitor.getPlatform() !== "ios"}
|
||||||
<Link
|
<Link
|
||||||
replaceState
|
|
||||||
href="/settings/wallet"
|
href="/settings/wallet"
|
||||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||||
<Icon icon={Wallet} size={5} />
|
<Icon icon={Wallet} size={5} />
|
||||||
|
|
@ -42,21 +40,18 @@
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
<Link
|
<Link
|
||||||
replaceState
|
|
||||||
href="/settings/hosting"
|
href="/settings/hosting"
|
||||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||||
<Icon icon={ServerPath} size={5} />
|
<Icon icon={ServerPath} size={5} />
|
||||||
Hosting
|
Hosting
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
replaceState
|
|
||||||
href="/settings/content"
|
href="/settings/content"
|
||||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||||
<Icon icon={GalleryMinimalistic} size={5} />
|
<Icon icon={GalleryMinimalistic} size={5} />
|
||||||
Content
|
Content
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
replaceState
|
|
||||||
href="/settings/privacy"
|
href="/settings/privacy"
|
||||||
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
|
||||||
<Icon icon={Shield} size={5} />
|
<Icon icon={Shield} size={5} />
|
||||||
|
|
@ -64,9 +59,9 @@
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 items-center opacity-75 text-sm">
|
<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>
|
<Button onclick={logout}>Log Out</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -61,4 +61,4 @@
|
||||||
|
|
||||||
<svelte:window onkeydown={onKeyDown} />
|
<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">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {getAddress} from "@welshman/util"
|
import {getAddress} from "@welshman/util"
|
||||||
import {publishAsRelay} from "@welshman/app"
|
import {publishAsRelay} from "@welshman/app"
|
||||||
import {Pinboard} from "@welshman/domain"
|
import {Pinboard} from "@welshman/domain"
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
back()
|
back()
|
||||||
} else {
|
} else {
|
||||||
pushToast({message: "Shelf created!"})
|
pushToast({message: "Shelf created!"})
|
||||||
goto(makeLibraryPath(url, getAddress(thunk.event)))
|
navigate(makeLibraryPath(url, getAddress(thunk.event)))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
import {Profiles} from "@welshman/app"
|
import {Profiles} from "@welshman/app"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Magnifier from "@assets/icons/magnifier.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 UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
import Settings from "@assets/icons/settings.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 ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
|
|
@ -14,11 +14,12 @@
|
||||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||||
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
import PrimaryNavSpaces from "@app/components/PrimaryNavSpaces.svelte"
|
||||||
import Search from "@app/components/Search.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 {PLATFORM_RELAYS} from "@app/env"
|
||||||
import {pushModal} from "@app/modal"
|
import {modal, popModal, pushDrawer, pushModal} from "@app/modal"
|
||||||
import {notifications} from "@app/notifications"
|
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"
|
import {deriveUserItem} from "@app/core"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -35,12 +36,26 @@
|
||||||
|
|
||||||
const showSearch = () => pushModal(Search)
|
const showSearch = () => pushModal(Search)
|
||||||
|
|
||||||
const anySpaceNotifications = $derived(
|
// The menu is reachable from every page, so it opens on the space the user is in, or the last one
|
||||||
$userSpaceUrls.some(p => $notifications.has(makeSpacePath(p))),
|
// 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>
|
</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 />
|
<PrimaryNavSpaces />
|
||||||
{#if PLATFORM_RELAYS.length > 0}
|
{#if PLATFORM_RELAYS.length > 0}
|
||||||
<Divider />
|
<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 class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-surface md:hidden">
|
||||||
</div>
|
</div>
|
||||||
<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 h-full justify-between px-2">
|
||||||
<div class="flex items-center gap-6">
|
<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}
|
{#if PLATFORM_RELAYS.length === 1}
|
||||||
<PrimaryNavItemSpace url={PLATFORM_RELAYS[0]} />
|
<PrimaryNavItemSpace url={PLATFORM_RELAYS[0]} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -84,11 +107,6 @@
|
||||||
<PrimaryNavItem href="/chat" onclick={chatHandler} notification={$notifications.has("/chat")}>
|
<PrimaryNavItem href="/chat" onclick={chatHandler} notification={$notifications.has("/chat")}>
|
||||||
<ImageIcon alt="Messages" src={Letter} size={8} />
|
<ImageIcon alt="Messages" src={Letter} size={8} />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
{#if PLATFORM_RELAYS.length !== 1}
|
|
||||||
<PrimaryNavItem href="/spaces" notification={anySpaceNotifications}>
|
|
||||||
<ImageIcon alt="Spaces" src={Widget} size={8} />
|
|
||||||
</PrimaryNavItem>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<PrimaryNavItem onclick={showSettingsMenu}>
|
<PrimaryNavItem onclick={showSettingsMenu}>
|
||||||
{#if $userProfile?.picture()}
|
{#if $userProfile?.picture()}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import {relays} from "@app/core"
|
import {relays} from "@app/core"
|
||||||
|
import {navigate} from "@app/modal"
|
||||||
import {makeSpacePath, makeSpaceEntryPath} from "@app/routes"
|
import {makeSpacePath, makeSpaceEntryPath} from "@app/routes"
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
const entryPath = makeSpaceEntryPath(url)
|
const entryPath = makeSpaceEntryPath(url)
|
||||||
|
|
||||||
goto(entryPath, {replaceState: entryPath === $page.url.pathname})
|
navigate(entryPath, {replaceState: entryPath === $page.url.pathname})
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = makeSpacePath(url)
|
const path = makeSpacePath(url)
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,19 @@
|
||||||
import {notifications} from "@app/notifications"
|
import {notifications} from "@app/notifications"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
overflow?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const {overflow = true}: Props = $props()
|
||||||
|
|
||||||
let windowHeight = $state(0)
|
let windowHeight = $state(0)
|
||||||
|
|
||||||
const itemHeight = 56
|
const itemHeight = 56
|
||||||
const navPadding = 8 * itemHeight
|
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 [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, $userSpaceUrls))
|
||||||
const otherSpaceNotifications = $derived(
|
const otherSpaceNotifications = $derived(
|
||||||
secondarySpaceUrls.some(url => $notifications.has(makeSpacePath(url))),
|
secondarySpaceUrls.some(url => $notifications.has(makeSpacePath(url))),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {removeUndefined, spec} from "@welshman/lib"
|
import {removeUndefined, spec} from "@welshman/lib"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
|
|
@ -19,7 +18,7 @@
|
||||||
import ProfileMenu from "@app/components/ProfileMenu.svelte"
|
import ProfileMenu from "@app/components/ProfileMenu.svelte"
|
||||||
import {messagingRelayLists, profiles, relayManagement} from "@app/core"
|
import {messagingRelayLists, profiles, relayManagement} from "@app/core"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {makeProfilePath} from "@app/routes"
|
import {makeProfilePath} from "@app/routes"
|
||||||
|
|
||||||
|
|
@ -34,7 +33,7 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
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) => {
|
const report = (error: string | undefined, message: string) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {formatTimestamp} from "@welshman/lib"
|
import {formatTimestamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {tagValue, tagSpec} from "@welshman/util"
|
import {tagValue, tagSpec} from "@welshman/util"
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
const h = tagValue(tagSpec("h"), event.tags)
|
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>
|
</script>
|
||||||
|
|
||||||
<Button class="cv card card-interactive" onclick={onClick}>
|
<Button class="cv card card-interactive" onclick={onClick}>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import {onDestroy, onMount} from "svelte"
|
import {onDestroy, onMount} from "svelte"
|
||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
|
|
@ -341,7 +341,7 @@
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (!isNaN(at)) {
|
if (!isNaN(at)) {
|
||||||
goto($page.url.pathname, {replaceState: true})
|
navigate($page.url.pathname, {replaceState: true})
|
||||||
} else {
|
} else {
|
||||||
element?.scrollTo({top: 0, behavior: "smooth"})
|
element?.scrollTo({top: 0, behavior: "smooth"})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import type {RoomMeta} from "@welshman/app"
|
import type {RoomMeta} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onsubmit = (room: RoomMeta) => goto(makeSpacePath(url, room.h))
|
const onsubmit = (room: RoomMeta) => navigate(makeSpacePath(url, room.h))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<RoomForm {url} {onsubmit}>
|
<RoomForm {url} {onsubmit}>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import {MembershipStatus, publish} from "@welshman/app"
|
import {MembershipStatus, publish} from "@welshman/app"
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
|
|
@ -16,7 +15,7 @@
|
||||||
import {joinRoom, leaveRoom} from "@app/access"
|
import {joinRoom, leaveRoom} from "@app/access"
|
||||||
import {deriveUserIsRoomAdmin, deriveUserRoomMembershipStatus} from "@app/rooms"
|
import {deriveUserIsRoomAdmin, deriveUserRoomMembershipStatus} from "@app/rooms"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -63,7 +62,7 @@
|
||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
} else {
|
} else {
|
||||||
await $roomLists.removeRoom(h, url).then(publish)
|
await $roomLists.removeRoom(h, url).then(publish)
|
||||||
goto(makeSpacePath(url))
|
navigate(makeSpacePath(url))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import type {RoomMeta} from "@welshman/app"
|
import type {RoomMeta} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onsubmit = () => goto(makeSpacePath(url, h))
|
const onsubmit = () => navigate(makeSpacePath(url, h))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<RoomForm {url} {onsubmit} {initialValues}>
|
<RoomForm {url} {onsubmit} {initialValues}>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const submit = () => shareTo(selection, share, {replaceState: true})
|
const submit = () => shareTo(selection, share)
|
||||||
|
|
||||||
let term = $state("")
|
let term = $state("")
|
||||||
let selection = $state("")
|
let selection = $state("")
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
shareTo(makeRoomPath(url, selection), {type: "event", value: event}, {replaceState: true})
|
shareTo(makeRoomPath(url, selection), {type: "event", value: event})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleRoom = (h: string) => {
|
const toggleRoom = (h: string) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {parse, renderAsHtml} from "@welshman/content"
|
import {parse, renderAsHtml} from "@welshman/content"
|
||||||
import {publish} from "@welshman/app"
|
import {publish} from "@welshman/app"
|
||||||
|
|
@ -18,7 +17,7 @@
|
||||||
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
import SpaceAccessRequest from "@app/components/SpaceAccessRequest.svelte"
|
||||||
import {publishLeaveRequest} from "@app/access"
|
import {publishLeaveRequest} from "@app/access"
|
||||||
import {roomLists} from "@app/core"
|
import {roomLists} from "@app/core"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {clearModals, navigate, pushModal} from "@app/modal"
|
||||||
import {removeTrustedRelay} from "@app/settings"
|
import {removeTrustedRelay} from "@app/settings"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -28,7 +27,7 @@
|
||||||
|
|
||||||
const {url, error}: Props = $props()
|
const {url, error}: Props = $props()
|
||||||
|
|
||||||
const back = () => goto("/home")
|
const back = () => navigate("/home")
|
||||||
|
|
||||||
const requestAccess = () => pushModal(SpaceAccessRequest, {url, callback: clearModals})
|
const requestAccess = () => pushModal(SpaceAccessRequest, {url, callback: clearModals})
|
||||||
|
|
||||||
|
|
@ -43,7 +42,7 @@
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
goto("/home")
|
navigate("/home")
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
import ArrowLeft from "@assets/icons/arrow-left.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 PageBar from "@lib/components/PageBar.svelte"
|
import PageBar from "@lib/components/PageBar.svelte"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
back?: () => unknown
|
back?: () => unknown
|
||||||
|
|
@ -18,16 +16,18 @@
|
||||||
[key: string]: any
|
[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!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageBar {...props}>
|
<PageBar {...props}>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
{#if back}
|
||||||
<Icon icon={ArrowLeft} size={7} />
|
<Button onclick={back} aria-label="Go back" class="place-self-start pr-3 md:hidden">
|
||||||
</Button>
|
<Icon icon={ArrowLeft} size={7} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
<div class="flex grow items-center justify-between gap-4">
|
<div class="flex grow items-center justify-between gap-4">
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex min-w-0 items-start gap-2">
|
<div class="flex min-w-0 items-start gap-2">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {publish} from "@welshman/app"
|
import {publish} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
goto("/home")
|
navigate("/home")
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {maybe} from "@welshman/lib"
|
import {maybe} from "@welshman/lib"
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {slideAndFade} from "@lib/transition"
|
import {slideAndFade} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
const qp = data.code ? `?code=${encodeURIComponent(data.code)}` : ""
|
const qp = data.code ? `?code=${encodeURIComponent(data.code)}` : ""
|
||||||
|
|
||||||
pushToast({message: "Welcome to the room!"})
|
pushToast({message: "Welcome to the room!"})
|
||||||
await goto(path + qp)
|
await navigate(path + qp)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,16 +95,13 @@
|
||||||
`${$shouldNotify ? "Turn off" : "Turn on"} notifications`,
|
`${$shouldNotify ? "Turn off" : "Turn on"} notifications`,
|
||||||
)}
|
)}
|
||||||
{:else}
|
{: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} />
|
<Icon size={4} icon={Bell} />
|
||||||
Enable notifications
|
Enable notifications
|
||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
{#if HOSTING_ENABLED && $hostedRelay.relay}
|
{#if HOSTING_ENABLED && $hostedRelay.relay}
|
||||||
<Link
|
<Link href={makeSpacePath(url, "admin")} class="button button-neutral w-full justify-start">
|
||||||
href={makeSpacePath(url, "admin")}
|
|
||||||
replaceState
|
|
||||||
class="button button-neutral w-full justify-start">
|
|
||||||
<Icon size={4} icon={ServerPath} />
|
<Icon size={4} icon={ServerPath} />
|
||||||
Hosting settings
|
Hosting settings
|
||||||
</Link>
|
</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 = () => {
|
const openMenu = () => {
|
||||||
if (mobile) {
|
if (mobile) {
|
||||||
pushModal(SpaceMenuMobile, {url})
|
pushModal(SpaceMenuMobile, {url}, {nested: true})
|
||||||
} else {
|
} else {
|
||||||
showMenu = true
|
showMenu = true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,10 @@
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
h: string
|
h: string
|
||||||
replaceState?: boolean
|
|
||||||
tooltip?: 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 room = $rooms.forRoom(url, h)
|
||||||
const roomType = $derived(getRoomType($room))
|
const roomType = $derived(getRoomType($room))
|
||||||
|
|
@ -28,9 +27,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if roomType === RoomType.Voice}
|
{#if roomType === RoomType.Voice}
|
||||||
<VoiceRoomItem {url} {h} {replaceState} {notification} />
|
<VoiceRoomItem {url} {h} {notification} />
|
||||||
{:else}
|
{:else}
|
||||||
<SecondaryNavItem href={path} title={tooltip ? roomName : ""} {replaceState} {notification}>
|
<SecondaryNavItem href={path} title={tooltip ? roomName : ""} {notification}>
|
||||||
<RoomNameWithImage {url} {h} />
|
<RoomNameWithImage {url} {h} />
|
||||||
{#if $isMuted}
|
{#if $isMuted}
|
||||||
<Icon icon={BellOff} size={4} class="ml-auto opacity-50" />
|
<Icon icon={BellOff} size={4} class="ml-auto opacity-50" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {remove} from "@welshman/lib"
|
import {remove} from "@welshman/lib"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {publish} from "@welshman/app"
|
import {publish} from "@welshman/app"
|
||||||
|
|
@ -18,7 +17,7 @@
|
||||||
import InfoSignatures from "@app/components/InfoSignatures.svelte"
|
import InfoSignatures from "@app/components/InfoSignatures.svelte"
|
||||||
import {relaysPendingTrust} from "@app/policies"
|
import {relaysPendingTrust} from "@app/policies"
|
||||||
import {addTrustedRelay, removeTrustedRelay} from "@app/settings"
|
import {addTrustedRelay, removeTrustedRelay} from "@app/settings"
|
||||||
import {popModal, pushModal} from "@app/modal"
|
import {navigate, popModal, pushModal} from "@app/modal"
|
||||||
import {roomLists} from "@app/core"
|
import {roomLists} from "@app/core"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -35,7 +34,7 @@
|
||||||
try {
|
try {
|
||||||
await $roomLists.removeRelay(url).then(publish)
|
await $roomLists.removeRelay(url).then(publish)
|
||||||
await removeTrustedRelay(url)
|
await removeTrustedRelay(url)
|
||||||
goto("/home")
|
navigate("/home")
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {navigate} from "@app/modal"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {filter, formatTimestamp, max, spec} from "@welshman/lib"
|
import {filter, formatTimestamp, max, spec} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
||||||
const title = tagValue(tagSpec("title"), event.tags)
|
const title = tagValue(tagSpec("title"), event.tags)
|
||||||
const path = makeThreadPath(url, event.id)
|
const path = makeThreadPath(url, event.id)
|
||||||
const onClick = () => goto(path)
|
const onClick = () => navigate(path)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if mobile}
|
{#if mobile}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {makeRoomKey} from "@welshman/app"
|
import {makeRoomKey} from "@welshman/app"
|
||||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
|
@ -10,7 +9,7 @@
|
||||||
import RoomImage from "@app/components/RoomImage.svelte"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
||||||
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,11 +29,10 @@
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
h: string
|
h: string
|
||||||
replaceState?: boolean
|
|
||||||
notification?: 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
|
// 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.
|
// sidebar item too tall — fall back to the compact ProfileCircles cluster instead.
|
||||||
|
|
@ -59,7 +57,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
await goto(makeRoomPath(url, h), {replaceState})
|
await navigate(makeRoomPath(url, h))
|
||||||
pushModal(VoiceRoomJoinDialog, {url, h})
|
pushModal(VoiceRoomJoinDialog, {url, h})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,7 +74,6 @@
|
||||||
|
|
||||||
<SecondaryNavItem
|
<SecondaryNavItem
|
||||||
href={makeRoomPath(url, h)}
|
href={makeRoomPath(url, h)}
|
||||||
{replaceState}
|
|
||||||
{notification}
|
{notification}
|
||||||
onclick={handleClick}
|
onclick={handleClick}
|
||||||
class={cx("items-start!", isActive && "bg-surface! text-content!")}>
|
class={cx("items-start!", isActive && "bg-surface! text-content!")}>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {publish} from "@welshman/app"
|
import {publish} from "@welshman/app"
|
||||||
import RelayForm, {type RelayFormValues} from "@app/components/hosting/RelayForm.svelte"
|
import RelayForm, {type RelayFormValues} from "@app/components/hosting/RelayForm.svelte"
|
||||||
import PaymentDialog from "@app/components/hosting/PaymentDialog.svelte"
|
import PaymentDialog from "@app/components/hosting/PaymentDialog.svelte"
|
||||||
import PaymentSetup from "@app/components/hosting/PaymentSetup.svelte"
|
import PaymentSetup from "@app/components/hosting/PaymentSetup.svelte"
|
||||||
import {roomLists, user} from "@app/core"
|
import {roomLists, user} from "@app/core"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import {makeSpacePath} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {HOSTING_RELAY_DOMAIN} from "@app/env"
|
import {HOSTING_RELAY_DOMAIN} from "@app/env"
|
||||||
import {
|
import {
|
||||||
|
|
@ -56,7 +55,7 @@
|
||||||
|
|
||||||
const tenant = await getTenant($user.pubkey)
|
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)) {
|
if (values.plan_id !== "free" && !autopayConfigured(tenant)) {
|
||||||
const invoice = selectPayableInvoice(await listTenantInvoices($user.pubkey))
|
const invoice = selectPayableInvoice(await listTenantInvoices($user.pubkey))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type {Component} from "svelte"
|
||||||
import {get, writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import {randomId, always, assoc, Emitter} from "@welshman/lib"
|
import {randomId, always, assoc, Emitter} from "@welshman/lib"
|
||||||
import {deriveDeduplicated} from "@welshman/store"
|
import {deriveDeduplicated} from "@welshman/store"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import type {DialogSize} from "@lib/components/Dialog.svelte"
|
import type {DialogSize} from "@lib/components/Dialog.svelte"
|
||||||
|
|
||||||
|
|
@ -56,6 +57,16 @@ const setModalHash = (hash: string, replace: boolean) => {
|
||||||
modalHash.set(hash)
|
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]) => {
|
export const modalStack = deriveDeduplicated([modalHash, modals], ([$hash, $modals]) => {
|
||||||
return getIdsFromHash($hash)
|
return getIdsFromHash($hash)
|
||||||
.map(id => $modals[id])
|
.map(id => $modals[id])
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import {
|
||||||
type Token,
|
type Token,
|
||||||
} from "@capacitor/push-notifications"
|
} from "@capacitor/push-notifications"
|
||||||
import type {PluginListenerHandle} from "@capacitor/core"
|
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 {assoc, call, now, on, parseJson, poll, spec, throttle, uniq} from "@welshman/lib"
|
||||||
import {LOCAL_RELAY_URL} from "@welshman/net"
|
import {LOCAL_RELAY_URL} from "@welshman/net"
|
||||||
import type {RepositoryUpdate} 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 target = event?.kind === WRAP ? await loadNotificationRumor(event) : event
|
||||||
const path = target && makeEventPath(target, [relay])
|
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("://")) {
|
if (path && !path.includes("://")) {
|
||||||
goto(path)
|
navigate(path)
|
||||||
} else if (event?.kind === WRAP) {
|
} else if (event?.kind === WRAP) {
|
||||||
goToChat()
|
goToChat()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import theme from "tailwindcss/defaultTheme"
|
import theme from "tailwindcss/defaultTheme"
|
||||||
import {get} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {identity} from "@welshman/lib"
|
import {identity} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
|
@ -23,10 +22,10 @@ import {
|
||||||
import {app, messagingRelayLists, relays, user} from "@app/core"
|
import {app, messagingRelayLists, relays, user} from "@app/core"
|
||||||
import {makeChatId} from "@app/chats"
|
import {makeChatId} from "@app/chats"
|
||||||
import {entityLink, PLATFORM_URL, PLATFORM_RELAYS} from "@app/env"
|
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 {DM_KINDS} from "@app/content"
|
||||||
import {ROOM} from "@app/rooms"
|
import {ROOM} from "@app/rooms"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
@ -35,10 +34,15 @@ export let lastChatUrl: string | undefined = undefined
|
||||||
|
|
||||||
export const lastPageBySpaceUrl = new Map<string, string>()
|
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 = () =>
|
export const setupHistory = () =>
|
||||||
page.subscribe($page => {
|
page.subscribe($page => {
|
||||||
if ($page.params.relay) {
|
if ($page.params.relay) {
|
||||||
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
||||||
|
lastSpaceUrl.set(decodeRelay($page.params.relay))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($page.params.chat) {
|
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) {
|
if (messagingRelayLists.get().urls(user.get().pubkey).get().length === 0) {
|
||||||
pushModal(ChatEnable, {next: () => goToChat(pubkeys, options)})
|
pushModal(ChatEnable, {next: () => goToChat(pubkeys, options)})
|
||||||
} else if (pubkeys.length === 0) {
|
} else if (pubkeys.length === 0) {
|
||||||
goto(lastChatUrl ?? "/chat", options)
|
navigate(lastChatUrl ?? "/chat", options)
|
||||||
} else {
|
} 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} = {}) =>
|
export const goToSpace = (url: string, options: {replaceState?: boolean} = {}) =>
|
||||||
goto(makeSpaceEntryPath(url), options)
|
navigate(makeSpaceEntryPath(url), options)
|
||||||
|
|
||||||
export const goToMovedSpace = (oldUrl: string, newUrl: string) =>
|
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 = () => {
|
export const goToHome = () => {
|
||||||
if (PLATFORM_RELAYS.length > 0) {
|
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
|
// Content types, events
|
||||||
|
|
@ -262,6 +268,6 @@ export const goToEvent = (event: TrustedEvent, options: Record<string, any> = {}
|
||||||
} else if (!scrollToEvent(event.id)) {
|
} else if (!scrollToEvent(event.id)) {
|
||||||
const replaceState = path.replace(/\?.*$/, "") === get(page).url.pathname
|
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 {writable} from "svelte/store"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {Capacitor, registerPlugin} from "@capacitor/core"
|
import {Capacitor, registerPlugin} from "@capacitor/core"
|
||||||
import type {PluginListenerHandle} from "@capacitor/core"
|
import type {PluginListenerHandle} from "@capacitor/core"
|
||||||
import {noop} from "@welshman/lib"
|
import {noop} from "@welshman/lib"
|
||||||
import type {Maybe} from "@welshman/lib"
|
import type {Maybe} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {app, relays} from "@app/core"
|
import {app, relays} from "@app/core"
|
||||||
import {pushModal} from "@app/modal"
|
import {navigate, pushModal} from "@app/modal"
|
||||||
import {makeSpaceChatPath} from "@app/routes"
|
import {makeSpaceChatPath} from "@app/routes"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {UPLOAD_MIME_TYPES} from "@app/uploads"
|
import {UPLOAD_MIME_TYPES} from "@app/uploads"
|
||||||
|
|
@ -20,13 +19,13 @@ export type Share =
|
||||||
|
|
||||||
export const pendingShare = writable<Maybe<Share>>(undefined)
|
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 (
|
export const shareTo = async (
|
||||||
path: string,
|
path: string,
|
||||||
share: Share,
|
share: Share,
|
||||||
options: {replaceState?: boolean} = {},
|
options: {replaceState?: boolean} = {},
|
||||||
) => {
|
) => {
|
||||||
await goto(path, options)
|
await navigate(path, options)
|
||||||
|
|
||||||
pendingShare.set(share)
|
pendingShare.set(share)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Component} from "svelte"
|
import type {Component} from "svelte"
|
||||||
import {fade, translate} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose?: any
|
onClose?: any
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
const {onClose, children}: Props = $props()
|
const {onClose, children}: Props = $props()
|
||||||
</script>
|
</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
|
<button
|
||||||
aria-label="Close drawer"
|
aria-label="Close drawer"
|
||||||
class="absolute inset-0 cursor-pointer bg-black opacity-50"
|
class="absolute inset-0 cursor-pointer bg-black opacity-50"
|
||||||
|
|
@ -21,8 +22,7 @@
|
||||||
onclick={onClose}>
|
onclick={onClose}>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
class="bg-surface scroll-container py-sai pr-sair absolute bottom-0 right-0 top-0 w-72 overflow-auto lg:w-96"
|
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">
|
||||||
transition:translate={{axis: "x", duration: 300}}>
|
|
||||||
<children.component {...children.props} />
|
<children.component {...children.props} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {stopPropagation} from "@lib/html"
|
import {stopPropagation} from "@lib/html"
|
||||||
|
import {navigate} from "@app/modal"
|
||||||
|
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
if (!external) {
|
if (!external) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
goto(href, {replaceState})
|
navigate(href, {replaceState})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if onclick}
|
{#if onclick}
|
||||||
<Button {onclick} data-tip={title} class={className}>
|
<Button {...restProps} {onclick} data-tip={title} class={className}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if notification}
|
{#if notification}
|
||||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Link {href} data-tip={title} class={className}>
|
<Link {...restProps} {href} data-tip={title} class={className}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if notification}
|
{#if notification}
|
||||||
<div class="absolute right-1 top-1 h-2 w-2 rounded-full bg-primary text-primary-content">
|
<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 {fade} from "@lib/transition"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
|
|
||||||
const {
|
const {children, href = "", title = "", notification = false, ...restProps} = $props()
|
||||||
children,
|
|
||||||
href = "",
|
|
||||||
title = "",
|
|
||||||
notification = false,
|
|
||||||
replaceState = false,
|
|
||||||
...restProps
|
|
||||||
} = $props()
|
|
||||||
|
|
||||||
const active = $derived($page.url.pathname === href)
|
const active = $derived($page.url.pathname === href)
|
||||||
const wrapperClass = $derived(
|
const wrapperClass = $derived(
|
||||||
|
|
@ -48,12 +41,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if href}
|
{#if href}
|
||||||
<a
|
<a {href} {...restProps} data-tip={title} class={wrapperClass}>
|
||||||
{href}
|
|
||||||
{...restProps}
|
|
||||||
data-tip={title}
|
|
||||||
data-sveltekit-replacestate={replaceState}
|
|
||||||
class={wrapperClass}>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
{#if notification}
|
{#if notification}
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||||
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||||
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
|
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
|
||||||
|
--mobile-nav-height: 3.5rem;
|
||||||
|
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
|
|
@ -203,6 +204,7 @@
|
||||||
|
|
||||||
body.keyboard-open {
|
body.keyboard-open {
|
||||||
--saib: 0px;
|
--saib: 0px;
|
||||||
|
--mobile-nav-height: 0px;
|
||||||
}
|
}
|
||||||
body.keyboard-open .hide-on-keyboard {
|
body.keyboard-open .hide-on-keyboard {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
@layer components {
|
@layer components {
|
||||||
.primary-nav {
|
.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 {
|
.primary-nav__nav-item {
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,27 @@ export type TranslateParams = {
|
||||||
duration?: number
|
duration?: number
|
||||||
easing?: (t: number) => number
|
easing?: (t: number) => number
|
||||||
axis?: "x" | "y"
|
axis?: "x" | "y"
|
||||||
reverse?: boolean
|
offset?: number
|
||||||
|
unit?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const translate = (
|
export const translate = (
|
||||||
node: Element,
|
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 {
|
return {
|
||||||
delay,
|
delay,
|
||||||
duration,
|
duration,
|
||||||
easing,
|
easing,
|
||||||
css: (t: number) => {
|
css: (t: number) => {
|
||||||
const p = reverse ? `${t * 100}%` : `${100 - t * 100}%`
|
const p = `${offset - t * offset}${unit}`
|
||||||
|
|
||||||
if (axis === "x") {
|
if (axis === "x") {
|
||||||
return `transform: translateX(${p})`
|
return `transform: translateX(${p})`
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
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 ShieldUser from "@assets/icons/shield-user.svg?dataurl"
|
||||||
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
import BillList from "@assets/icons/bill-list.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import RelayDescription from "@app/components/RelayDescription.svelte"
|
import RelayDescription from "@app/components/RelayDescription.svelte"
|
||||||
|
|
@ -16,18 +15,22 @@
|
||||||
import SpaceMembersSummary from "@app/components/SpaceMembersSummary.svelte"
|
import SpaceMembersSummary from "@app/components/SpaceMembersSummary.svelte"
|
||||||
import SpaceFeaturedContent from "@app/components/SpaceFeaturedContent.svelte"
|
import SpaceFeaturedContent from "@app/components/SpaceFeaturedContent.svelte"
|
||||||
import {relays} from "@app/core"
|
import {relays} from "@app/core"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const relay = $relays.one(url)
|
const relay = $relays.one(url)
|
||||||
const showMenu = () => goto(makeSpacePath(url))
|
|
||||||
</script>
|
</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">
|
<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="card flex flex-col gap-4">
|
||||||
<div class="relative flex gap-4">
|
<div class="relative flex gap-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {page} from "$app/stores"
|
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 Server from "@assets/icons/server.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||||
import RelayDetailCard from "@app/components/hosting/RelayDetailCard.svelte"
|
import RelayDetailCard from "@app/components/hosting/RelayDetailCard.svelte"
|
||||||
import ActivityFeed from "@app/components/hosting/ActivityFeed.svelte"
|
import ActivityFeed from "@app/components/hosting/ActivityFeed.svelte"
|
||||||
import {deriveHostedRelay, deriveRelayActivity} from "@app/hosting"
|
import {deriveHostedRelay, deriveRelayActivity} from "@app/hosting"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {makeSpacePath} from "@app/routes"
|
|
||||||
|
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const hostedRelay = deriveHostedRelay(url)
|
const hostedRelay = deriveHostedRelay(url)
|
||||||
const relayActivity = deriveRelayActivity(derived(hostedRelay, $h => $h.relay?.id))
|
const relayActivity = deriveRelayActivity(derived(hostedRelay, $h => $h.relay?.id))
|
||||||
|
|
||||||
const showMenu = () => goto(makeSpacePath(url))
|
|
||||||
</script>
|
</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">
|
<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}
|
{#if $hostedRelay.relay}
|
||||||
<RelayDetailCard relay={$hostedRelay.relay} />
|
<RelayDetailCard relay={$hostedRelay.relay} />
|
||||||
<ActivityFeed activity={$relayActivity.activity} loading={$relayActivity.loading} />
|
<ActivityFeed activity={$relayActivity.activity} loading={$relayActivity.loading} />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue