Scroll the space rail instead of hiding spaces behind an overflow menu
This commit is contained in:
parent
f30ef6d344
commit
5ca8eeff51
9 changed files with 156 additions and 195 deletions
|
|
@ -141,20 +141,20 @@ Acceptance:
|
|||
|
||||
## Spaces
|
||||
|
||||
### US-009 — Browse, search, and reorder your spaces
|
||||
### US-009 — Browse and search spaces, and reorder your own
|
||||
|
||||
As alice, I want to see the spaces I've joined and find new ones, so that I can
|
||||
get where I'm going and discover communities.
|
||||
As alice, I want to find new spaces and keep the ones I'm in in the order I
|
||||
want, so that I can discover communities and get where I'm going.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- `/spaces` shows a "Your spaces" section listing every space alice has joined
|
||||
and a "Browse Spaces" section of the rest.
|
||||
- Typing a term filters both sections live, matching name, url, or description.
|
||||
- Clicking a joined space opens it; clicking one she hasn't joined opens a join
|
||||
prompt instead.
|
||||
- Dragging a joined space above another, on the page or in the sidebar rail,
|
||||
reorders the list immediately, and the order survives a reload.
|
||||
- `/spaces` lists the spaces alice hasn't joined, under "Browse Spaces". The
|
||||
ones she has are in the sidebar rail, all of them, and the rail scrolls.
|
||||
- Typing a term filters the list live, matching name, url, or description.
|
||||
- Clicking a space she hasn't joined opens a join prompt; a space in the rail
|
||||
opens.
|
||||
- Dragging a space above another in the rail reorders the list immediately, and
|
||||
the order survives a reload.
|
||||
|
||||
### US-010 — Join a space from an invite link
|
||||
|
||||
|
|
@ -1564,10 +1564,9 @@ check first.
|
|||
Acceptance:
|
||||
|
||||
- After alice posts in a room bob hasn't opened, an unread dot appears on that
|
||||
room and on its space in his sidebar, and on the space's row in `/spaces`,
|
||||
without a reload.
|
||||
- Opening the room clears its dot, and the dot stays cleared when he returns to
|
||||
the space list.
|
||||
room and on its space in his sidebar, without a reload.
|
||||
- Opening the room clears its dot, and the dot stays cleared when he leaves the
|
||||
space.
|
||||
|
||||
### US-104 — Mute a room or a whole space
|
||||
|
||||
|
|
|
|||
|
|
@ -135,23 +135,19 @@ test("US-103 see and clear unread indicators", async ({seed, as}) => {
|
|||
const bob = await as(users.bob, "/spaces")
|
||||
const alice = await as(users.alice, roomPath(space.url, "general"))
|
||||
|
||||
const spaceRow = bob.getByRole("listitem").filter({hasText: space.url})
|
||||
const navItem = spaceNavItem(bob, space.name)
|
||||
|
||||
await expect(spaceRow).toBeVisible()
|
||||
await expect(navItem).toBeVisible()
|
||||
await expect(unreadDot(spaceRow)).toHaveCount(0)
|
||||
await expect(unreadDot(navItem)).toHaveCount(0)
|
||||
|
||||
await post(alice, "the server is on fire")
|
||||
|
||||
// Bob is sitting on the space list the whole time, so both dots arrive without a navigation
|
||||
await expect(unreadDot(spaceRow)).toBeVisible()
|
||||
// Bob is sitting on the spaces page the whole time, so the dot arrives without a navigation
|
||||
await expect(unreadDot(navItem)).toBeVisible()
|
||||
|
||||
// Inside the space, the dot points at the room the message landed in. A space's room list only
|
||||
// exists in its own menu, so this is the one indicator the list above can't show.
|
||||
await spaceRow.click()
|
||||
// exists in its own menu, so this is the one indicator the rail can't show.
|
||||
await navItem.click()
|
||||
|
||||
const general = roomLink(bob, "General")
|
||||
const random = roomLink(bob, "Random")
|
||||
|
|
@ -172,11 +168,10 @@ test("US-103 see and clear unread indicators", async ({seed, as}) => {
|
|||
await expect(general).toBeVisible()
|
||||
await expect(unreadDot(general)).toHaveCount(0)
|
||||
|
||||
// ...and it stays cleared back on the space list he started from
|
||||
// ...and it stays cleared back on the page he started from
|
||||
await bob.locator('.primary-nav a[href="/spaces"]').click()
|
||||
|
||||
await expect(spaceRow).toBeVisible()
|
||||
await expect(unreadDot(spaceRow)).toHaveCount(0)
|
||||
await expect(navItem).toBeVisible()
|
||||
await expect(unreadDot(navItem)).toHaveCount(0)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {DAY, HOUR, WEEK, sortBy} from "@welshman/lib"
|
||||
import {ROOMS} from "@welshman/util"
|
||||
import type {Page} from "@playwright/test"
|
||||
import type {SeededSpace} from "../harness"
|
||||
import {expect, readCachedEvents, roomPath, spacePath, test, users} from "../harness"
|
||||
|
||||
// The space this user's room list names first, read from the copy on disk the app restores itself
|
||||
|
|
@ -13,17 +14,27 @@ const cachedFirstSpace = async (page: Page, pubkey: string) => {
|
|||
return newest?.tags.find(tag => tag[0] === "r")?.[1]
|
||||
}
|
||||
|
||||
// The rail shows icons and no text, so a row is read by the tooltip naming its relay. See
|
||||
// spaceNavItem in notifications.spec.ts for why that is a tooltip rather than an accessible name.
|
||||
const railSpaces = (page: Page) => page.locator(".primary-nav [draggable=true]")
|
||||
|
||||
const expectRailFirst = (page: Page, name: string) =>
|
||||
expect(railSpaces(page).first().locator("[data-tip]")).toHaveAttribute(
|
||||
"data-tip",
|
||||
new RegExp(`^${name}`),
|
||||
)
|
||||
|
||||
// A drop reorders the list in place and publishes a new room list behind it, so the order on screen
|
||||
// is ahead of the one the app has settled on. Reading the next drag off that optimistic order is
|
||||
// what made this spec fail under a full suite and pass alone: the relay is slower when the box is
|
||||
// busy, and a room list landing back from it after the next drop replaces the order that drop
|
||||
// applied. Waiting for the copy on disk to name the new first space is waiting for the round trip.
|
||||
const expectReordered = async (page: Page, pubkey: string, url: string) => {
|
||||
await expect(page.getByRole("listitem").first()).toContainText(url)
|
||||
await expect.poll(() => cachedFirstSpace(page, pubkey)).toBe(url)
|
||||
const expectReordered = async (page: Page, pubkey: string, space: SeededSpace) => {
|
||||
await expectRailFirst(page, space.name)
|
||||
await expect.poll(() => cachedFirstSpace(page, pubkey)).toBe(space.url)
|
||||
}
|
||||
|
||||
test("US-009 browse, search, and reorder your spaces", async ({seed, as}) => {
|
||||
test("US-009 browse and search spaces, and reorder your own", async ({seed, as}) => {
|
||||
const scenario = await seed(({relay, user}) => {
|
||||
const space = relay("space")
|
||||
const other = relay("other")
|
||||
|
|
@ -50,76 +61,56 @@ test("US-009 browse, search, and reorder your spaces", async ({seed, as}) => {
|
|||
// bootstraps from, so pointing that at bob is what puts his other space in front of alice.
|
||||
const page = await as(users.alice, "/spaces", {env: {VITE_DEFAULT_PUBKEYS: users.bob.pubkey}})
|
||||
|
||||
await expect(page.getByText("Your spaces")).toBeVisible()
|
||||
await expect(page.getByText(space.url)).toBeVisible()
|
||||
await expect(page.getByText(other.url)).toBeVisible()
|
||||
|
||||
// ...and the rest of them in a section of their own
|
||||
// The page is for spaces she hasn't joined. The ones she has are in the rail, all of them.
|
||||
await expect(page.getByText("Browse Spaces")).toBeVisible()
|
||||
await expect(page.getByText(unsigned.url)).toBeVisible()
|
||||
await expect(page.getByText(space.url)).toHaveCount(0)
|
||||
await expect(page.getByText(other.url)).toHaveCount(0)
|
||||
await expect(railSpaces(page)).toHaveCount(2)
|
||||
|
||||
const term = page.getByPlaceholder("Search for spaces...")
|
||||
|
||||
await term.fill("unsigned")
|
||||
|
||||
await expect(page.getByText(unsigned.url)).toBeVisible()
|
||||
await expect(page.getByText(space.url)).toHaveCount(0)
|
||||
await expect(page.getByText(other.url)).toHaveCount(0)
|
||||
|
||||
await term.fill("nothing by that name")
|
||||
|
||||
await expect(page.getByText(unsigned.url)).toHaveCount(0)
|
||||
|
||||
await term.fill("")
|
||||
|
||||
await expect(page.getByText(space.url)).toBeVisible()
|
||||
|
||||
// A space she's joined opens
|
||||
await page.getByRole("listitem").filter({hasText: space.url}).click()
|
||||
|
||||
await expect(page).toHaveURL(/\/spaces\/space\.test\//)
|
||||
|
||||
// A space she hasn't asks her to join first
|
||||
await page.goto("/spaces")
|
||||
// A space she hasn't joined asks her to join first
|
||||
await page.getByRole("button").filter({hasText: unsigned.url}).click()
|
||||
|
||||
await expect(page.getByRole("button", {name: "Join Space"})).toBeVisible()
|
||||
await expect(page.getByRole("button", {name: "Go back"})).toBeEnabled()
|
||||
|
||||
// A space she has joined opens from the rail
|
||||
await page.goto("/spaces")
|
||||
await railSpaces(page).first().click()
|
||||
|
||||
await expect(page).toHaveURL(/\/spaces\/space\.test\//)
|
||||
|
||||
// Reordering by dragging, which lives in her room list and so outlives the page
|
||||
await page.goto("/spaces")
|
||||
|
||||
const joined = page.getByRole("listitem")
|
||||
|
||||
await expect(joined.first()).toContainText(space.url)
|
||||
await expectRailFirst(page, space.name)
|
||||
|
||||
// Html5 drag and drop, dispatched rather than mimed with the mouse: chromium's synthetic drag
|
||||
// starts the drag and moves it, but never delivers the drop the reorder is committed in, so the
|
||||
// row would snap back to where it came from.
|
||||
const dataTransfer = await page.evaluateHandle(() => new DataTransfer())
|
||||
|
||||
// The sidebar rail is the same list and reorders the same way. It shows icons and no text, so
|
||||
// its rows are named by position and the list on the page is where the result is read.
|
||||
const rail = page.locator(".primary-nav [draggable=true]")
|
||||
const rail = railSpaces(page)
|
||||
|
||||
await rail.nth(0).dispatchEvent("dragstart", {dataTransfer})
|
||||
await rail.nth(1).dispatchEvent("drop", {dataTransfer})
|
||||
|
||||
await expectReordered(page, users.alice.pubkey, other.url)
|
||||
|
||||
await rail.nth(1).dispatchEvent("dragstart", {dataTransfer})
|
||||
await rail.nth(0).dispatchEvent("drop", {dataTransfer})
|
||||
|
||||
await expectReordered(page, users.alice.pubkey, space.url)
|
||||
|
||||
// Dragging in the page's own list moves the same room list.
|
||||
const source = joined.filter({hasText: other.url})
|
||||
const target = joined.filter({hasText: space.url})
|
||||
|
||||
await source.dispatchEvent("dragstart", {dataTransfer})
|
||||
await target.dispatchEvent("drop", {dataTransfer})
|
||||
|
||||
await expectReordered(page, users.alice.pubkey, other.url)
|
||||
await expectReordered(page, users.alice.pubkey, other)
|
||||
|
||||
await page.reload()
|
||||
|
||||
await expect(page.getByRole("listitem").first()).toContainText(other.url)
|
||||
await expectRailFirst(page, other.name)
|
||||
})
|
||||
|
||||
test("US-010 join a space from an invite link", async ({seed, as}) => {
|
||||
|
|
@ -268,7 +259,7 @@ test("US-011 request access when a space turns you away", async ({seed, as}) =>
|
|||
|
||||
await bob.goto("/spaces")
|
||||
|
||||
await expect(bob.getByText("You haven't joined any spaces yet.")).toBeVisible()
|
||||
await expect(railSpaces(bob)).toHaveCount(0)
|
||||
})
|
||||
|
||||
test("US-012 decide whether to trust an unsigned space", async ({seed, as}) => {
|
||||
|
|
@ -296,7 +287,7 @@ test("US-012 decide whether to trust an unsigned space", async ({seed, as}) => {
|
|||
// page that reloads before it reaches disk reads the list he had a moment ago.
|
||||
await bob.getByRole("link", {name: "All Spaces"}).click()
|
||||
|
||||
await expect(bob.getByText("You haven't joined any spaces yet.")).toBeVisible()
|
||||
await expect(railSpaces(bob)).toHaveCount(0)
|
||||
|
||||
const alice = await as(users.alice, roomPath(unsigned.url, "general"))
|
||||
|
||||
|
|
@ -343,10 +334,8 @@ test("US-013 follow a space that has moved", async ({seed, as}) => {
|
|||
// that reloads before it reaches disk reads the old address back.
|
||||
await alice.getByRole("link", {name: "All Spaces"}).click()
|
||||
|
||||
const aliceSpaces = alice.getByRole("listitem")
|
||||
|
||||
await expect(aliceSpaces).toHaveCount(1)
|
||||
await expect(aliceSpaces.first()).toContainText(other.url)
|
||||
await expect(railSpaces(alice)).toHaveCount(1)
|
||||
await expectRailFirst(alice, other.name)
|
||||
|
||||
const bob = await as(users.bob, spacePath(space.url) + "/about", {relayInfo})
|
||||
|
||||
|
|
@ -358,10 +347,8 @@ test("US-013 follow a space that has moved", async ({seed, as}) => {
|
|||
|
||||
await bob.goto("/spaces")
|
||||
|
||||
const bobSpaces = bob.getByRole("listitem")
|
||||
|
||||
await expect(bobSpaces).toHaveCount(1)
|
||||
await expect(bobSpaces.first()).toContainText(space.url)
|
||||
await expect(railSpaces(bob)).toHaveCount(1)
|
||||
await expectRailFirst(bob, space.name)
|
||||
})
|
||||
|
||||
test("US-014 leave a space", async ({seed, as}) => {
|
||||
|
|
@ -389,7 +376,7 @@ test("US-014 leave a space", async ({seed, as}) => {
|
|||
// reloads before it reaches disk reads the list he had a moment ago.
|
||||
await page.getByRole("link", {name: "All Spaces"}).click()
|
||||
|
||||
await expect(page.getByText("You haven't joined any spaces yet.")).toBeVisible()
|
||||
await expect(railSpaces(page)).toHaveCount(0)
|
||||
|
||||
// Nothing stops him coming back
|
||||
await page.getByRole("button", {name: "Add Space"}).click()
|
||||
|
|
|
|||
|
|
@ -1,47 +1,45 @@
|
|||
<script lang="ts">
|
||||
import {splitAt} from "@welshman/lib"
|
||||
import cx from "classnames"
|
||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
import Tippy from "@lib/components/Tippy.svelte"
|
||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||
import DragList from "@lib/components/DragList.svelte"
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
import PrimaryNavSpacesOverflow from "@app/components/PrimaryNavSpacesOverflow.svelte"
|
||||
import {reorderSpaceUrls, userSpaceUrls} from "@app/rooms"
|
||||
import {PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/env"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
type Props = {
|
||||
overflow?: boolean
|
||||
const fadeSize = 24
|
||||
|
||||
let element: HTMLElement | undefined = $state()
|
||||
let hiddenAbove = $state(0)
|
||||
let hiddenBelow = $state(0)
|
||||
|
||||
const measure = () => {
|
||||
if (element) {
|
||||
hiddenAbove = element.scrollTop
|
||||
hiddenBelow = element.scrollHeight - element.clientHeight - element.scrollTop
|
||||
}
|
||||
}
|
||||
|
||||
const {overflow = true}: Props = $props()
|
||||
|
||||
let windowHeight = $state(0)
|
||||
|
||||
const itemHeight = 56
|
||||
const navPadding = 8 * 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))),
|
||||
)
|
||||
|
||||
// Tippy mounts its content component once, so pass a stable reactive object it can read from
|
||||
const overflowProps = $state({urls: [] as string[]})
|
||||
|
||||
// Nothing fires on a change of scroll height, and the list has one whenever a space is joined
|
||||
// or left, or the window resizes under it.
|
||||
$effect(() => {
|
||||
overflowProps.urls = secondarySpaceUrls
|
||||
const observer = new ResizeObserver(measure)
|
||||
|
||||
if (element) {
|
||||
observer.observe(element)
|
||||
|
||||
for (const child of element.children) {
|
||||
observer.observe(child)
|
||||
}
|
||||
}
|
||||
|
||||
return () => observer.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={windowHeight} />
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class={cx("flex min-h-0 flex-col items-center", {"flex-1": PLATFORM_RELAYS.length === 0})}>
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{:else}
|
||||
|
|
@ -49,32 +47,23 @@
|
|||
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" size={10} />
|
||||
</PrimaryNavItem>
|
||||
<Divider />
|
||||
<DragList
|
||||
class="flex flex-col items-center"
|
||||
items={primarySpaceUrls}
|
||||
onReorder={reorderSpaceUrls}>
|
||||
{#snippet item(url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{/snippet}
|
||||
</DragList>
|
||||
{#snippet allSpaces(title: string)}
|
||||
<PrimaryNavItem
|
||||
href="/spaces"
|
||||
{title}
|
||||
prefix="no-highlight"
|
||||
notification={otherSpaceNotifications}>
|
||||
<ImageIcon alt="All Spaces" src={Widget} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/snippet}
|
||||
{#if secondarySpaceUrls.length > 0}
|
||||
<Tippy
|
||||
component={PrimaryNavSpacesOverflow}
|
||||
props={overflowProps}
|
||||
params={{placement: "right", interactive: true}}>
|
||||
{@render allSpaces("")}
|
||||
</Tippy>
|
||||
{:else}
|
||||
{@render allSpaces("All Spaces")}
|
||||
{/if}
|
||||
<div
|
||||
bind:this={element}
|
||||
onscroll={measure}
|
||||
class="primary-nav__spaces"
|
||||
style:--fade-top="{Math.min(hiddenAbove, fadeSize)}px"
|
||||
style:--fade-bottom="{Math.min(hiddenBelow, fadeSize)}px">
|
||||
<DragList
|
||||
class="flex flex-col items-center"
|
||||
items={$userSpaceUrls}
|
||||
onReorder={reorderSpaceUrls}>
|
||||
{#snippet item(url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{/snippet}
|
||||
</DragList>
|
||||
</div>
|
||||
<PrimaryNavItem href="/spaces" title="All Spaces" prefix="no-highlight">
|
||||
<ImageIcon alt="All Spaces" src={Widget} size={8} />
|
||||
</PrimaryNavItem>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
<script lang="ts">
|
||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||
|
||||
type Props = {
|
||||
urls: string[]
|
||||
}
|
||||
|
||||
const {urls}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex max-h-[80vh] flex-col overflow-y-auto rounded-2xl border border-solid bg-surface p-1 shadow-xl border-line">
|
||||
{#each urls as url (url)}
|
||||
<PrimaryNavItemSpace {url} showTooltip={false} />
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -28,10 +28,10 @@
|
|||
|
||||
<div class="@container flex h-full min-h-0 w-full">
|
||||
<div
|
||||
class="primary-nav flex overflow-y-auto pb-2"
|
||||
class="primary-nav flex min-h-0 pb-2"
|
||||
in:translate={slide(0)}
|
||||
out:translate={slide(parallax)}>
|
||||
<PrimaryNavSpaces overflow={false} />
|
||||
<PrimaryNavSpaces />
|
||||
</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">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,41 @@
|
|||
@apply hidden;
|
||||
}
|
||||
|
||||
.primary-nav__spaces {
|
||||
@apply flex min-h-0 w-full flex-1 flex-col items-center self-start overflow-y-auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
/* Fades whichever end of the list has spaces past it */
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0,
|
||||
black var(--fade-top, 0px),
|
||||
black calc(100% - var(--fade-bottom, 0px)),
|
||||
transparent 100%
|
||||
);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover tooltips reach past the rail, and a scroll container clips whatever leaves it. The extra
|
||||
width is padding, so it holds them without moving the items, and passes its pointer events to
|
||||
the page underneath. Tooltips are a desktop affair, so the rail in the mobile drawer keeps the
|
||||
width it draws in. */
|
||||
@media (min-width: 768px) {
|
||||
.primary-nav__spaces {
|
||||
width: calc(100% + 12rem);
|
||||
padding-right: 12rem;
|
||||
pointer-events: none;
|
||||
|
||||
& > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary-nav__nav-item {
|
||||
@apply text-content relative my-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full transition-all;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import Home from "@assets/icons/home.svg?dataurl"
|
||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
|
|
@ -27,9 +29,15 @@
|
|||
|
||||
<Page>
|
||||
<PageBar>
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Home} size={6} />
|
||||
<strong>Home</strong>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon={Home} size={6} />
|
||||
<strong>Home</strong>
|
||||
</div>
|
||||
<Link href="/spaces" class="button button-neutral button-sm">
|
||||
<Icon icon={Widget} size={4} />
|
||||
Browse spaces
|
||||
</Link>
|
||||
</div>
|
||||
</PageBar>
|
||||
<PageContent noPad bind:element class="flex flex-col bg-surface">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {derived as _derived} from "svelte/store"
|
||||
import {addToMapKey, dec, sleep, spec} from "@welshman/lib"
|
||||
import {addToMapKey, dec, sleep} from "@welshman/lib"
|
||||
import {ROOMS} from "@welshman/util"
|
||||
import type {Relay} from "@welshman/domain"
|
||||
import {throttled} from "@welshman/store"
|
||||
import {Sync, createSearch} from "@welshman/app"
|
||||
import {createScroller, isMobile} from "@lib/html"
|
||||
import {fly} from "@lib/transition"
|
||||
import DragHandle from "@assets/icons/drag-handle.svg?dataurl"
|
||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import DragList from "@lib/components/DragList.svelte"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import PageBar from "@lib/components/PageBar.svelte"
|
||||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
|
|
@ -26,13 +24,12 @@
|
|||
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import {app, relays, roomLists, user} from "@app/core"
|
||||
import {reorderSpaceUrls, userSpaceUrls} from "@app/rooms"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {PLATFORM_RELAYS, DEFAULT_RELAYS} from "@app/env"
|
||||
import {bootstrapPubkeys} from "@app/social"
|
||||
import {parseInviteLink} from "@app/access"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {goToSpace, makeSpacePath} from "@app/routes"
|
||||
import {notifications} from "@app/notifications"
|
||||
import {goToSpace} from "@app/routes"
|
||||
|
||||
const addSpace = () => pushModal(SpaceAdd)
|
||||
|
||||
|
|
@ -91,9 +88,6 @@
|
|||
const inviteData = $derived(parseInviteLink(term))
|
||||
const searchResults = $derived($relaySearch.searchOptions(term))
|
||||
const userSpaceSet = $derived(new Set($userSpaceUrls))
|
||||
const filteredUserUrls = $derived(
|
||||
term ? $userSpaceUrls.filter(url => searchResults.some(spec({url}))) : $userSpaceUrls,
|
||||
)
|
||||
const otherSpaces = $derived(
|
||||
searchResults.filter(r => !userSpaceSet.has(r.url) && r.url !== inviteData?.url),
|
||||
)
|
||||
|
|
@ -158,7 +152,7 @@
|
|||
{#await userSpacesLoaded}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Spinner size="sm" class="mr-3" />
|
||||
Loading your spaces...
|
||||
Loading spaces...
|
||||
</div>
|
||||
{:then}
|
||||
{#if inviteData}
|
||||
|
|
@ -171,36 +165,6 @@
|
|||
</Button>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if filteredUserUrls.length > 0}
|
||||
<Divider>Your spaces</Divider>
|
||||
<DragList
|
||||
class="flex flex-col gap-2"
|
||||
role="list"
|
||||
itemRole="listitem"
|
||||
items={filteredUserUrls}
|
||||
onReorder={reorderSpaceUrls}>
|
||||
{#snippet item(url)}
|
||||
<Button
|
||||
class="group card card-interactive w-full relative min-w-0"
|
||||
onclick={() => openSpace(url)}>
|
||||
<div class="flex w-full items-start gap-2">
|
||||
<div
|
||||
class="mt-4 flex cursor-grab p-1 text-content-subtle transition-colors group-hover:text-content-muted">
|
||||
<Icon icon={DragHandle} />
|
||||
</div>
|
||||
<RelaySummary hideFavorites {url} />
|
||||
</div>
|
||||
{#if $notifications.has(makeSpacePath(url))}
|
||||
<div
|
||||
class="absolute right-3 top-3 h-2 w-2 rounded-full bg-primary text-primary-content">
|
||||
</div>
|
||||
{/if}
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DragList>
|
||||
{:else if !term}
|
||||
<p class="py-12 text-center">You haven't joined any spaces yet.</p>
|
||||
{/if}
|
||||
{#if otherSpaces.length > 0}
|
||||
<Divider>Browse Spaces</Divider>
|
||||
{/if}
|
||||
|
|
@ -214,7 +178,7 @@
|
|||
<Spinner loading>Looking for spaces...</Spinner>
|
||||
{:then}
|
||||
{#if otherSpaces.length === 0}
|
||||
<Spinner>No other spaces found.</Spinner>
|
||||
<Spinner>No spaces found.</Spinner>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue