UI tweaks, change how role colors work
This commit is contained in:
parent
182e2ce6fd
commit
c88a981fb6
23 changed files with 149 additions and 89 deletions
3
.env
3
.env
|
|
@ -8,10 +8,9 @@ VITE_PLATFORM_PRIVACY=https://flotilla.social/privacy
|
|||
VITE_PLATFORM_NAME=Flotilla
|
||||
VITE_PLATFORM_LOGO=static/logo.png
|
||||
VITE_PLATFORM_RELAYS=
|
||||
VITE_PLATFORM_RELAYS_STRICT=false
|
||||
VITE_PLATFORM_ACCENT="#7161FF"
|
||||
VITE_THEME="clay"
|
||||
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
|
||||
VITE_PLATFORM_DESCRIPTION="The chat app built for self-hosted communities."
|
||||
VITE_PUSH_SERVER=https://nps.flotilla.social/
|
||||
VITE_PUSH_BRIDGE=wss://npb.coracle.social/
|
||||
VITE_BLOCKED_RELAYS=brb.io,relay.nostr.band,nostr.mutinywallet.com,feeds.nostr.band,nostr.zbd.gg,wot.utxo.one,blastr.f7z.xyz,relay.current.fyi
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import LogIn from "@app/components/LogIn.svelte"
|
||||
import SignUp from "@app/components/SignUp.svelte"
|
||||
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/env"
|
||||
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME, PLATFORM_DESCRIPTION} from "@app/env"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const logIn = () => pushModal(LogIn)
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
<ModalBody>
|
||||
<div class="py-2">
|
||||
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
|
||||
<p class="text-center">The chat app built for self-hosted communities.</p>
|
||||
<p class="text-center">{PLATFORM_DESCRIPTION}</p>
|
||||
</div>
|
||||
<Button onclick={logIn}>
|
||||
<CardButton primary>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
{#if PLATFORM_RELAYS.length > 0}
|
||||
<Divider />
|
||||
{/if}
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-center">
|
||||
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
|
||||
{#if $userProfile?.picture}
|
||||
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<svelte:window bind:innerHeight={windowHeight} />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-center">
|
||||
{#each PLATFORM_RELAYS as url (url)}
|
||||
<PrimaryNavItemSpace {url} />
|
||||
{:else}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Card from "@lib/components/Card.svelte"
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||
import {displayUrl} from "@welshman/lib"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay, deriveRelayStats} from "@welshman/app"
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
{@render children?.()}
|
||||
</div>
|
||||
{#if $relay?.description}
|
||||
<p class="ellipsize">{$relay.description}</p>
|
||||
<ContentMinimal singleLine event={{content: $relay.description, tags: []}} />
|
||||
{/if}
|
||||
<span class="flex items-center gap-1 whitespace-nowrap text-sm">
|
||||
{#if $relay?.contact}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="badge min-w-0"
|
||||
class="badge w-fit min-w-0"
|
||||
style="background-color: {roleColorSoft(role.color)}; border-color: {roleColor(
|
||||
role.color,
|
||||
)}; color: {roleColor(role.color)};">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script module lang="ts">
|
||||
import type {SpaceRole} from "@app/members"
|
||||
import type {SpaceRole, SpaceRoleColor} from "@app/members"
|
||||
|
||||
export type Values = Pick<SpaceRole, "label" | "description" | "color">
|
||||
</script>
|
||||
|
|
@ -21,16 +21,34 @@
|
|||
|
||||
const {initialValues = {}, loading = false, onSubmit}: Props = $props()
|
||||
|
||||
const values: Values = $state({
|
||||
label: "",
|
||||
description: "",
|
||||
color: Math.floor(Math.random() * 256),
|
||||
...initialValues,
|
||||
const values = $state({
|
||||
label: initialValues.label ?? "",
|
||||
description: initialValues.description ?? "",
|
||||
})
|
||||
|
||||
// Preserve any unedited components from the existing color; the form edits hue and lightness.
|
||||
const baseColor: SpaceRoleColor = {
|
||||
hue: "",
|
||||
saturation: "",
|
||||
lightness: "",
|
||||
...initialValues.color,
|
||||
}
|
||||
|
||||
const initialHue = parseInt(baseColor.hue, 10)
|
||||
const initialLightness = parseFloat(baseColor.lightness)
|
||||
|
||||
let hue = $state(isNaN(initialHue) ? Math.floor(Math.random() * 360) : initialHue)
|
||||
let lightness = $state(isNaN(initialLightness) ? 0.5 : initialLightness)
|
||||
|
||||
const color = $derived<SpaceRoleColor>({
|
||||
...baseColor,
|
||||
hue: String(hue),
|
||||
lightness: String(lightness),
|
||||
})
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = () => onSubmit(values)
|
||||
const submit = () => onSubmit({...values, color})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
|
|
@ -58,15 +76,25 @@
|
|||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-8 w-8 shrink-0 rounded-full border-2"
|
||||
style="background-color: {roleColor(values.color)}; border-color: var(--line)">
|
||||
style="background-color: {roleColor(color)}; border-color: var(--line)">
|
||||
</div>
|
||||
<div class="flex grow flex-col gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="360"
|
||||
bind:value={hue}
|
||||
class="range"
|
||||
style="color: {roleColor(color)}; --range-shdw: {roleColor(color)}" />
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
bind:value={lightness}
|
||||
class="range"
|
||||
style="color: {roleColor(color)}; --range-shdw: {roleColor(color)}" />
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="255"
|
||||
bind:value={values.color}
|
||||
class="range grow"
|
||||
style="color: {roleColor(values.color)}; --range-shdw: {roleColor(values.color)}" />
|
||||
</div>
|
||||
{/snippet}
|
||||
</Field>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
const {role}: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
<div class="flex min-w-0 flex-col gap-2 justify-between">
|
||||
<RoleBadge {role} />
|
||||
{#if role.description}
|
||||
<p class="text-sm opacity-70">{role.description}</p>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
<Card sm class="flex flex-col gap-4">
|
||||
<div class="flex justify-between gap-2">
|
||||
<div>
|
||||
<Button class="button button-link inline text-primary" onclick={showProfile}>
|
||||
<Button class="link text-primary" onclick={showProfile}>
|
||||
<ProfileName pubkey={event.pubkey} {url} />
|
||||
</Button>
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
import cx from "classnames"
|
||||
import type {ClientOptions} from "@pomade/core"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {first} from "@welshman/lib"
|
||||
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||
import {loginWithNip01, publishThunk, waitForThunkCompletion} from "@welshman/app"
|
||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||
|
|
@ -48,7 +47,7 @@
|
|||
await attemptRelayAccess(url, claim)
|
||||
|
||||
return url
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
// Add default outbox/inbox/messaging relays, profile, spaces
|
||||
|
|
|
|||
|
|
@ -65,9 +65,8 @@
|
|||
</ModalHeader>
|
||||
<div class="m-auto flex flex-col gap-4">
|
||||
<p>
|
||||
This space has opted not to publish <Button
|
||||
class="link"
|
||||
onclick={showInfoSignatures}>digital signatures</Button
|
||||
This space has opted not to publish <Button class="link" onclick={showInfoSignatures}
|
||||
>digital signatures</Button
|
||||
>, which means that they have the ability to forge messages from other users.
|
||||
</p>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export const POMADE_SIGNERS = fromCsv(import.meta.env.VITE_POMADE_SIGNERS)
|
|||
export const DEFAULT_BLOSSOM_SERVERS = fromCsv(import.meta.env.VITE_DEFAULT_BLOSSOM_SERVERS)
|
||||
|
||||
export const DEFAULT_SPACES = fromCsv(import.meta.env.VITE_DEFAULT_SPACES).map(item => {
|
||||
const [url, claim = ""] = item.split('|')
|
||||
const [url, claim = ""] = item.split("|")
|
||||
|
||||
return [normalizeRelayUrl(url), claim]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -32,20 +32,60 @@ export const deriveSpaceMembers = (url: string) =>
|
|||
|
||||
export const RELAY_ROLE = 33534
|
||||
|
||||
// An hsl color tuple. Any component may be an empty string, in which case the
|
||||
// client supplies its own default. Usually only `hue` is set per the spec.
|
||||
export type SpaceRoleColor = {
|
||||
hue: string // 0 to 360
|
||||
saturation: string // 0 to 1
|
||||
lightness: string // 0 to 1
|
||||
}
|
||||
|
||||
export type SpaceRole = {
|
||||
id: string
|
||||
label: string
|
||||
description: string
|
||||
color: number
|
||||
color: SpaceRoleColor
|
||||
order: number
|
||||
}
|
||||
|
||||
// hue is 0-255; map to 0-360deg. Saturation/lightness chosen to read on both themes.
|
||||
export const roleColorHue = (color: number) => (((color % 256) + 256) % 256) * (360 / 256)
|
||||
// Defaults filled in for empty color components, chosen to read on both themes.
|
||||
const DEFAULT_SATURATION = 0.7
|
||||
const DEFAULT_LIGHTNESS = 0.5
|
||||
|
||||
export const roleColor = (color: number) => `hsl(${roleColorHue(color)}, 70%, 50%)`
|
||||
const roleColorValue = (value: string, fallback: number) => {
|
||||
const parsed = parseFloat(value)
|
||||
|
||||
export const roleColorSoft = (color: number) => `hsl(${roleColorHue(color)}, 70%, 90%)`
|
||||
return isNaN(parsed) ? fallback : parsed
|
||||
}
|
||||
|
||||
// Parse the ["color", hue, saturation, lightness] tag, preserving empty values.
|
||||
export const parseRoleColor = (tags: string[][]): SpaceRoleColor => {
|
||||
const tag = first(getTags("color", tags)) ?? []
|
||||
|
||||
return {
|
||||
hue: tag[1] ?? "",
|
||||
saturation: tag[2] ?? "",
|
||||
lightness: tag[3] ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
// Build an hsl() string from a role color, using the role's values or our defaults.
|
||||
export const roleColor = (color: SpaceRoleColor) => {
|
||||
const h = roleColorValue(color.hue, 0)
|
||||
const s = roleColorValue(color.saturation, DEFAULT_SATURATION)
|
||||
const l = roleColorValue(color.lightness, DEFAULT_LIGHTNESS)
|
||||
|
||||
return `hsl(${h}, ${s * 100}%, ${l * 100}%)`
|
||||
}
|
||||
|
||||
// A translucent tint of the role color for use as a background fill.
|
||||
export const roleColorSoft = (color: SpaceRoleColor) => {
|
||||
const h = roleColorValue(color.hue, 0)
|
||||
const s = roleColorValue(color.saturation, DEFAULT_SATURATION)
|
||||
const l = roleColorValue(color.lightness, DEFAULT_LIGHTNESS)
|
||||
|
||||
return `hsl(${h}, ${s * 100}%, ${l * 100}%, 0.15)`
|
||||
}
|
||||
|
||||
export const deriveSpaceRoles = (url: string) =>
|
||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
||||
|
|
@ -59,7 +99,7 @@ export const deriveSpaceRoles = (url: string) =>
|
|||
id,
|
||||
label: getTagValue("label", event.tags) ?? "",
|
||||
description: getTagValue("description", event.tags) ?? "",
|
||||
color: parseInt(getTagValue("color", event.tags) ?? "0", 10) || 0,
|
||||
color: parseRoleColor(event.tags),
|
||||
order: parseInt(getTagValue("order", event.tags) ?? "0", 10) || 0,
|
||||
})
|
||||
}
|
||||
|
|
@ -87,17 +127,21 @@ export const deriveSpaceMemberRoles = (url: string) =>
|
|||
return memberRoles
|
||||
})
|
||||
|
||||
// Flatten a color tuple into management params, mirroring the color tag's value order.
|
||||
const roleColorParams = (color: SpaceRoleColor) =>
|
||||
[color.hue, color.saturation, color.lightness] as unknown as string
|
||||
|
||||
export const createRole = async (
|
||||
url: string,
|
||||
id: string,
|
||||
label: string,
|
||||
description: string,
|
||||
color: number,
|
||||
color: SpaceRoleColor,
|
||||
order: number,
|
||||
): Promise<string | undefined> => {
|
||||
const {error} = await manageRelay(url, {
|
||||
method: "createrole" as ManagementMethod,
|
||||
params: [id, label, description, color.toString(), order.toString()],
|
||||
params: [id, label, description, roleColorParams(color), order.toString()],
|
||||
})
|
||||
|
||||
return error
|
||||
|
|
@ -108,12 +152,12 @@ export const editRole = async (
|
|||
id: string,
|
||||
label: string,
|
||||
description: string,
|
||||
color: number,
|
||||
color: SpaceRoleColor,
|
||||
order: number,
|
||||
): Promise<string | undefined> => {
|
||||
const {error} = await manageRelay(url, {
|
||||
method: "editrole" as ManagementMethod,
|
||||
params: [id, label, description, color.toString(), order.toString()],
|
||||
params: [id, label, description, roleColorParams(color), order.toString()],
|
||||
})
|
||||
|
||||
return error
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {notificationSettings} from "@app/settings"
|
|||
import {chatsById} from "@app/chats"
|
||||
import {userGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
|
||||
import {hasNip29} from "@app/relays"
|
||||
import {dufflepud, DUFFLEPUD_URL} from "@app/env"
|
||||
import {dufflepud, DUFFLEPUD_URL, PLATFORM_RELAYS} from "@app/env"
|
||||
import {kv} from "@app/storage"
|
||||
import {page} from "$app/stores"
|
||||
|
||||
|
|
@ -224,7 +224,10 @@ export const allNotifications = derived(
|
|||
}
|
||||
}
|
||||
|
||||
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||
const urls =
|
||||
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : getSpaceUrlsFromGroupList($userGroupList)
|
||||
|
||||
for (const url of urls) {
|
||||
const spacePath = makeSpacePath(url)
|
||||
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ import {
|
|||
hasNegentropy,
|
||||
} from "@welshman/app"
|
||||
import {REACTION_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
||||
import {INDEXER_RELAYS} from "@app/env"
|
||||
import {INDEXER_RELAYS, PLATFORM_RELAYS} from "@app/env"
|
||||
import {loadSettings} from "@app/settings"
|
||||
import {
|
||||
loadGroupList,
|
||||
|
|
@ -210,8 +210,10 @@ const syncUserData = () => {
|
|||
const syncGroupList = ($userGroupList: List | undefined) => {
|
||||
if ($userGroupList) {
|
||||
const keys = new Set<string>()
|
||||
const urls =
|
||||
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : getSpaceUrlsFromGroupList($userGroupList)
|
||||
|
||||
for (const url of getSpaceUrlsFromGroupList($userGroupList)) {
|
||||
for (const url of urls) {
|
||||
if (!unsubscribersByKey.has(url)) {
|
||||
unsubscribersByKey.set(url, syncUserSpaceMembership(url))
|
||||
}
|
||||
|
|
@ -310,7 +312,9 @@ const syncSpaces = () => {
|
|||
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||
|
||||
const unsubscribe = store.subscribe(([$userGroupList, $page]) => {
|
||||
const urls = new Set(getSpaceUrlsFromGroupList($userGroupList))
|
||||
const urls = new Set(
|
||||
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : getSpaceUrlsFromGroupList($userGroupList),
|
||||
)
|
||||
const currentUrl = $page.params.relay ? decodeRelay($page.params.relay) : undefined
|
||||
|
||||
if (currentUrl) {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@
|
|||
<div>
|
||||
{#each value as item (item)}
|
||||
<Badge variant="neutral" class="mr-1">
|
||||
<Button class="button button-ghost flex items-center" onclick={() => removeItem(item)}>
|
||||
<Button class="p-1 flex items-center" onclick={() => removeItem(item)}>
|
||||
<Icon icon={CloseCircle} size={4} class="-ml-1 mt-px" />
|
||||
</Button>
|
||||
<span>{item}</span>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
@apply ml-sai mb-sai mt-sai mr-sai relative flex min-w-0 grow flex-col md:mb-0 md:ml-0;
|
||||
|
||||
.page__content {
|
||||
@apply z-feature mb-14 flex max-h-screen min-h-0 w-full min-w-0 flex-col gap-2 overflow-x-hidden overflow-y-auto sm:gap-4 md:mb-0;
|
||||
@apply z-feature mb-14 flex h-full max-h-screen min-h-0 w-full min-w-0 flex-col gap-2 overflow-x-hidden overflow-y-auto sm:gap-4 md:mb-0;
|
||||
}
|
||||
|
||||
.page__content:not(.page__content--no-pad) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
}
|
||||
|
||||
.primary-nav__nav-item {
|
||||
@apply text-content relative mx-auto my-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full transition-all;
|
||||
@apply text-content relative my-1 flex h-12 w-12 cursor-pointer items-center justify-center rounded-full transition-all;
|
||||
}
|
||||
|
||||
.primary-nav__nav-item:hover,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {page} from "$app/stores"
|
||||
import {modal} from "@app/modal"
|
||||
import {goToSpace} from "@app/routes"
|
||||
import {PLATFORM_RELAYS} from "@app/env"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {createScroller, isMobile} from "@lib/html"
|
||||
import {profileSearch} from "@welshman/app"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
|
|
@ -9,23 +8,11 @@
|
|||
import PageContent from "@lib/components/PageContent.svelte"
|
||||
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
||||
import PeopleItem from "@app/components/PeopleItem.svelte"
|
||||
import {bootstrapPubkeys} from "@app/social"
|
||||
|
||||
let term = $state("")
|
||||
let limit = $state(10)
|
||||
let pubkeys = $state($bootstrapPubkeys)
|
||||
let element: Element | undefined = $state()
|
||||
|
||||
const search = debounce(200, (term: string) => {
|
||||
if (term) {
|
||||
pubkeys = $profileSearch.searchValues(term)
|
||||
} else {
|
||||
pubkeys = $bootstrapPubkeys
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => search(term))
|
||||
|
||||
onMount(() => {
|
||||
const scroller = createScroller({
|
||||
element: element!,
|
||||
|
|
@ -55,7 +42,7 @@
|
|||
{/snippet}
|
||||
{#snippet content()}
|
||||
<div class="flex flex-col gap-2 h-full" bind:this={element}>
|
||||
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
|
||||
{#each $profileSearch.searchValues(term).slice(0, limit) as pubkey (pubkey)}
|
||||
<PeopleItem {pubkey} />
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -142,8 +142,8 @@
|
|||
{/snippet}
|
||||
{#snippet info()}
|
||||
<p>
|
||||
Your public key is your user identifier. It also allows other people to authenticate
|
||||
your messages.
|
||||
Your public key is your user identifier. It also allows other people to authenticate your
|
||||
messages.
|
||||
</p>
|
||||
{/snippet}
|
||||
</FieldInline>
|
||||
|
|
@ -172,7 +172,7 @@
|
|||
<SignerStatus />
|
||||
{#if $session?.method === SessionMethod.Pomade}
|
||||
<div class="flex flex-col lg:flex-row gap-4 lg:gap-2 justify-end">
|
||||
<Button onclick={startPasswordReset}>
|
||||
<Button class="button button-neutral" onclick={startPasswordReset}>
|
||||
<Spinner {loading}>Update your password</Spinner>
|
||||
</Button>
|
||||
<Button class="button button-primary" onclick={startRecovery}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
import type {Snippet} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {once} from "@welshman/lib"
|
||||
import {pubkey} from "@welshman/app"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||
|
|
@ -35,16 +34,16 @@
|
|||
|
||||
const showPendingTrust = once(() => pushModal(SpaceTrustRelay, {url}, {noEscape: true}))
|
||||
|
||||
// Watch for relay errors and notify the user
|
||||
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
||||
$effect(() => {
|
||||
// Watch for relay errors and notify the user
|
||||
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
||||
$effect(() => {
|
||||
void $userGroupList
|
||||
|
||||
if ($modal) return
|
||||
if ($authError) return showAuthError()
|
||||
if ($relaysPendingTrust.includes(url)) return showPendingTrust()
|
||||
|
||||
if (!$userSpaceUrls.includes(url)) {
|
||||
if (!$userSpaceUrls.includes(url) && !joinPrompted.has(url)) {
|
||||
joinPrompted.add(url)
|
||||
pushModal(SpaceJoin, {url})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,24 +29,16 @@
|
|||
|
||||
<PageContent class="flex flex-col gap-4 p-4">
|
||||
<div class="card flex flex-col gap-4">
|
||||
<div class="flex justify-between">
|
||||
<div class="relative flex gap-4">
|
||||
<div class="relative">
|
||||
<RelayIcon {url} size={14} class="rounded-full" />
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<h1 class="ellipsize whitespace-nowrap">
|
||||
<RelayName {url} class="text-2xl font-bold" />
|
||||
</h1>
|
||||
<p class="ellipsize text-sm text-primary">{displayRelayUrl(url)}</p>
|
||||
</div>
|
||||
<div class="relative flex gap-4">
|
||||
<div class="relative">
|
||||
<RelayIcon {url} size={14} class="rounded-full" />
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<h1 class="ellipsize whitespace-nowrap">
|
||||
<RelayName {url} class="text-2xl font-bold" />
|
||||
</h1>
|
||||
<p class="ellipsize text-sm text-primary">{displayRelayUrl(url)}</p>
|
||||
</div>
|
||||
{#if $userIsAdmin}
|
||||
<Button class="button button-primary" onclick={startEdit}>
|
||||
<Icon icon={Pen} />
|
||||
<span class="hidden sm:inline">Edit</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<RelayDescription {url} />
|
||||
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
||||
|
|
@ -105,6 +97,12 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $userIsAdmin}
|
||||
<Button class="button button-primary" onclick={startEdit}>
|
||||
<Icon icon={Pen} />
|
||||
<span class="hidden sm:inline">Edit Space</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<div class="lg:col-span-2 flex flex-col gap-4">
|
||||
|
|
|
|||
Loading…
Reference in a new issue