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_NAME=Flotilla
|
||||||
VITE_PLATFORM_LOGO=static/logo.png
|
VITE_PLATFORM_LOGO=static/logo.png
|
||||||
VITE_PLATFORM_RELAYS=
|
VITE_PLATFORM_RELAYS=
|
||||||
VITE_PLATFORM_RELAYS_STRICT=false
|
|
||||||
VITE_PLATFORM_ACCENT="#7161FF"
|
VITE_PLATFORM_ACCENT="#7161FF"
|
||||||
VITE_THEME="clay"
|
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_SERVER=https://nps.flotilla.social/
|
||||||
VITE_PUSH_BRIDGE=wss://npb.coracle.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
|
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 CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import SignUp from "@app/components/SignUp.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"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const logIn = () => pushModal(LogIn)
|
const logIn = () => pushModal(LogIn)
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
|
<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>
|
</div>
|
||||||
<Button onclick={logIn}>
|
<Button onclick={logIn}>
|
||||||
<CardButton primary>
|
<CardButton primary>
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
{#if PLATFORM_RELAYS.length > 0}
|
{#if PLATFORM_RELAYS.length > 0}
|
||||||
<Divider />
|
<Divider />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col items-center">
|
||||||
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
|
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
|
||||||
{#if $userProfile?.picture}
|
{#if $userProfile?.picture}
|
||||||
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
|
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
<svelte:window bind:innerHeight={windowHeight} />
|
<svelte:window bind:innerHeight={windowHeight} />
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col items-center">
|
||||||
{#each PLATFORM_RELAYS as url (url)}
|
{#each PLATFORM_RELAYS as url (url)}
|
||||||
<PrimaryNavItemSpace {url} />
|
<PrimaryNavItemSpace {url} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Card from "@lib/components/Card.svelte"
|
import Card from "@lib/components/Card.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
import {displayUrl} from "@welshman/lib"
|
import {displayUrl} from "@welshman/lib"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {deriveRelay, deriveRelayStats} from "@welshman/app"
|
import {deriveRelay, deriveRelayStats} from "@welshman/app"
|
||||||
|
|
@ -23,7 +24,7 @@
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
{#if $relay?.description}
|
{#if $relay?.description}
|
||||||
<p class="ellipsize">{$relay.description}</p>
|
<ContentMinimal singleLine event={{content: $relay.description, tags: []}} />
|
||||||
{/if}
|
{/if}
|
||||||
<span class="flex items-center gap-1 whitespace-nowrap text-sm">
|
<span class="flex items-center gap-1 whitespace-nowrap text-sm">
|
||||||
{#if $relay?.contact}
|
{#if $relay?.contact}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="badge min-w-0"
|
class="badge w-fit min-w-0"
|
||||||
style="background-color: {roleColorSoft(role.color)}; border-color: {roleColor(
|
style="background-color: {roleColorSoft(role.color)}; border-color: {roleColor(
|
||||||
role.color,
|
role.color,
|
||||||
)}; color: {roleColor(role.color)};">
|
)}; color: {roleColor(role.color)};">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script module lang="ts">
|
<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">
|
export type Values = Pick<SpaceRole, "label" | "description" | "color">
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -21,16 +21,34 @@
|
||||||
|
|
||||||
const {initialValues = {}, loading = false, onSubmit}: Props = $props()
|
const {initialValues = {}, loading = false, onSubmit}: Props = $props()
|
||||||
|
|
||||||
const values: Values = $state({
|
const values = $state({
|
||||||
label: "",
|
label: initialValues.label ?? "",
|
||||||
description: "",
|
description: initialValues.description ?? "",
|
||||||
color: Math.floor(Math.random() * 256),
|
})
|
||||||
...initialValues,
|
|
||||||
|
// 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 back = () => history.back()
|
||||||
|
|
||||||
const submit = () => onSubmit(values)
|
const submit = () => onSubmit({...values, color})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
|
|
@ -58,15 +76,25 @@
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
class="h-8 w-8 shrink-0 rounded-full border-2"
|
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>
|
</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>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
const {role}: Props = $props()
|
const {role}: Props = $props()
|
||||||
</script>
|
</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} />
|
<RoleBadge {role} />
|
||||||
{#if role.description}
|
{#if role.description}
|
||||||
<p class="text-sm opacity-70">{role.description}</p>
|
<p class="text-sm opacity-70">{role.description}</p>
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
<Card sm class="flex flex-col gap-4">
|
<Card sm class="flex flex-col gap-4">
|
||||||
<div class="flex justify-between gap-2">
|
<div class="flex justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<Button class="button button-link inline text-primary" onclick={showProfile}>
|
<Button class="link text-primary" onclick={showProfile}>
|
||||||
<ProfileName pubkey={event.pubkey} {url} />
|
<ProfileName pubkey={event.pubkey} {url} />
|
||||||
</Button>
|
</Button>
|
||||||
<span>
|
<span>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import type {ClientOptions} from "@pomade/core"
|
import type {ClientOptions} from "@pomade/core"
|
||||||
import type {Profile} from "@welshman/util"
|
import type {Profile} from "@welshman/util"
|
||||||
import {first} from "@welshman/lib"
|
|
||||||
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||||
import {loginWithNip01, publishThunk, waitForThunkCompletion} from "@welshman/app"
|
import {loginWithNip01, publishThunk, waitForThunkCompletion} from "@welshman/app"
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
|
|
@ -48,7 +47,7 @@
|
||||||
await attemptRelayAccess(url, claim)
|
await attemptRelayAccess(url, claim)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add default outbox/inbox/messaging relays, profile, spaces
|
// Add default outbox/inbox/messaging relays, profile, spaces
|
||||||
|
|
|
||||||
|
|
@ -65,9 +65,8 @@
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<div class="m-auto flex flex-col gap-4">
|
<div class="m-auto flex flex-col gap-4">
|
||||||
<p>
|
<p>
|
||||||
This space has opted not to publish <Button
|
This space has opted not to publish <Button class="link" onclick={showInfoSignatures}
|
||||||
class="link"
|
>digital signatures</Button
|
||||||
onclick={showInfoSignatures}>digital signatures</Button
|
|
||||||
>, which means that they have the ability to forge messages from other users.
|
>, which means that they have the ability to forge messages from other users.
|
||||||
</p>
|
</p>
|
||||||
<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_BLOSSOM_SERVERS = fromCsv(import.meta.env.VITE_DEFAULT_BLOSSOM_SERVERS)
|
||||||
|
|
||||||
export const DEFAULT_SPACES = fromCsv(import.meta.env.VITE_DEFAULT_SPACES).map(item => {
|
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]
|
return [normalizeRelayUrl(url), claim]
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -32,20 +32,60 @@ export const deriveSpaceMembers = (url: string) =>
|
||||||
|
|
||||||
export const RELAY_ROLE = 33534
|
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 = {
|
export type SpaceRole = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
description: string
|
description: string
|
||||||
color: number
|
color: SpaceRoleColor
|
||||||
order: number
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// hue is 0-255; map to 0-360deg. Saturation/lightness chosen to read on both themes.
|
// Defaults filled in for empty color components, chosen to read on both themes.
|
||||||
export const roleColorHue = (color: number) => (((color % 256) + 256) % 256) * (360 / 256)
|
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) =>
|
export const deriveSpaceRoles = (url: string) =>
|
||||||
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
derived(deriveRelaySignedEvents(url, [{kinds: [RELAY_ROLE]}]), $events => {
|
||||||
|
|
@ -59,7 +99,7 @@ export const deriveSpaceRoles = (url: string) =>
|
||||||
id,
|
id,
|
||||||
label: getTagValue("label", event.tags) ?? "",
|
label: getTagValue("label", event.tags) ?? "",
|
||||||
description: getTagValue("description", 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,
|
order: parseInt(getTagValue("order", event.tags) ?? "0", 10) || 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -87,17 +127,21 @@ export const deriveSpaceMemberRoles = (url: string) =>
|
||||||
return memberRoles
|
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 (
|
export const createRole = async (
|
||||||
url: string,
|
url: string,
|
||||||
id: string,
|
id: string,
|
||||||
label: string,
|
label: string,
|
||||||
description: string,
|
description: string,
|
||||||
color: number,
|
color: SpaceRoleColor,
|
||||||
order: number,
|
order: number,
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const {error} = await manageRelay(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: "createrole" as ManagementMethod,
|
method: "createrole" as ManagementMethod,
|
||||||
params: [id, label, description, color.toString(), order.toString()],
|
params: [id, label, description, roleColorParams(color), order.toString()],
|
||||||
})
|
})
|
||||||
|
|
||||||
return error
|
return error
|
||||||
|
|
@ -108,12 +152,12 @@ export const editRole = async (
|
||||||
id: string,
|
id: string,
|
||||||
label: string,
|
label: string,
|
||||||
description: string,
|
description: string,
|
||||||
color: number,
|
color: SpaceRoleColor,
|
||||||
order: number,
|
order: number,
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
const {error} = await manageRelay(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: "editrole" as ManagementMethod,
|
method: "editrole" as ManagementMethod,
|
||||||
params: [id, label, description, color.toString(), order.toString()],
|
params: [id, label, description, roleColorParams(color), order.toString()],
|
||||||
})
|
})
|
||||||
|
|
||||||
return error
|
return error
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import {notificationSettings} from "@app/settings"
|
||||||
import {chatsById} from "@app/chats"
|
import {chatsById} from "@app/chats"
|
||||||
import {userGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
|
import {userGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
|
||||||
import {hasNip29} from "@app/relays"
|
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 {kv} from "@app/storage"
|
||||||
import {page} from "$app/stores"
|
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 spacePath = makeSpacePath(url)
|
||||||
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
|
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ import {
|
||||||
hasNegentropy,
|
hasNegentropy,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {REACTION_KINDS, CONTENT_KINDS, makeCommentFilter} from "@app/content"
|
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 {loadSettings} from "@app/settings"
|
||||||
import {
|
import {
|
||||||
loadGroupList,
|
loadGroupList,
|
||||||
|
|
@ -210,8 +210,10 @@ const syncUserData = () => {
|
||||||
const syncGroupList = ($userGroupList: List | undefined) => {
|
const syncGroupList = ($userGroupList: List | undefined) => {
|
||||||
if ($userGroupList) {
|
if ($userGroupList) {
|
||||||
const keys = new Set<string>()
|
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)) {
|
if (!unsubscribersByKey.has(url)) {
|
||||||
unsubscribersByKey.set(url, syncUserSpaceMembership(url))
|
unsubscribersByKey.set(url, syncUserSpaceMembership(url))
|
||||||
}
|
}
|
||||||
|
|
@ -310,7 +312,9 @@ const syncSpaces = () => {
|
||||||
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
const unsubscribersByUrl = new Map<string, Unsubscriber>()
|
||||||
|
|
||||||
const unsubscribe = store.subscribe(([$userGroupList, $page]) => {
|
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
|
const currentUrl = $page.params.relay ? decodeRelay($page.params.relay) : undefined
|
||||||
|
|
||||||
if (currentUrl) {
|
if (currentUrl) {
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
<div>
|
<div>
|
||||||
{#each value as item (item)}
|
{#each value as item (item)}
|
||||||
<Badge variant="neutral" class="mr-1">
|
<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" />
|
<Icon icon={CloseCircle} size={4} class="-ml-1 mt-px" />
|
||||||
</Button>
|
</Button>
|
||||||
<span>{item}</span>
|
<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;
|
@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 {
|
.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) {
|
.page__content:not(.page__content--no-pad) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-nav__nav-item {
|
.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,
|
.primary-nav__nav-item:hover,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {modal} from "@app/modal"
|
|
||||||
import {goToSpace} from "@app/routes"
|
import {goToSpace} from "@app/routes"
|
||||||
import {PLATFORM_RELAYS} from "@app/env"
|
import {PLATFORM_RELAYS} from "@app/env"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {debounce} from "throttle-debounce"
|
|
||||||
import {createScroller, isMobile} from "@lib/html"
|
import {createScroller, isMobile} from "@lib/html"
|
||||||
import {profileSearch} from "@welshman/app"
|
import {profileSearch} from "@welshman/app"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
|
@ -9,23 +8,11 @@
|
||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
import ContentSearch from "@lib/components/ContentSearch.svelte"
|
||||||
import PeopleItem from "@app/components/PeopleItem.svelte"
|
import PeopleItem from "@app/components/PeopleItem.svelte"
|
||||||
import {bootstrapPubkeys} from "@app/social"
|
|
||||||
|
|
||||||
let term = $state("")
|
let term = $state("")
|
||||||
let limit = $state(10)
|
let limit = $state(10)
|
||||||
let pubkeys = $state($bootstrapPubkeys)
|
|
||||||
let element: Element | undefined = $state()
|
let element: Element | undefined = $state()
|
||||||
|
|
||||||
const search = debounce(200, (term: string) => {
|
|
||||||
if (term) {
|
|
||||||
pubkeys = $profileSearch.searchValues(term)
|
|
||||||
} else {
|
|
||||||
pubkeys = $bootstrapPubkeys
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$effect(() => search(term))
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const scroller = createScroller({
|
const scroller = createScroller({
|
||||||
element: element!,
|
element: element!,
|
||||||
|
|
@ -55,7 +42,7 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet content()}
|
{#snippet content()}
|
||||||
<div class="flex flex-col gap-2 h-full" bind:this={element}>
|
<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} />
|
<PeopleItem {pubkey} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,8 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet info()}
|
{#snippet info()}
|
||||||
<p>
|
<p>
|
||||||
Your public key is your user identifier. It also allows other people to authenticate
|
Your public key is your user identifier. It also allows other people to authenticate your
|
||||||
your messages.
|
messages.
|
||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
<SignerStatus />
|
<SignerStatus />
|
||||||
{#if $session?.method === SessionMethod.Pomade}
|
{#if $session?.method === SessionMethod.Pomade}
|
||||||
<div class="flex flex-col lg:flex-row gap-4 lg:gap-2 justify-end">
|
<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>
|
<Spinner {loading}>Update your password</Spinner>
|
||||||
</Button>
|
</Button>
|
||||||
<Button class="button button-primary" onclick={startRecovery}
|
<Button class="button button-primary" onclick={startRecovery}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {once} from "@welshman/lib"
|
import {once} from "@welshman/lib"
|
||||||
import {pubkey} from "@welshman/app"
|
|
||||||
import Page from "@lib/components/Page.svelte"
|
import Page from "@lib/components/Page.svelte"
|
||||||
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
|
||||||
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
import SpaceMenu from "@app/components/SpaceMenu.svelte"
|
||||||
|
|
@ -35,16 +34,16 @@
|
||||||
|
|
||||||
const showPendingTrust = once(() => pushModal(SpaceTrustRelay, {url}, {noEscape: true}))
|
const showPendingTrust = once(() => pushModal(SpaceTrustRelay, {url}, {noEscape: true}))
|
||||||
|
|
||||||
// Watch for relay errors and notify the user
|
// Watch for relay errors and notify the user
|
||||||
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
void $userGroupList
|
void $userGroupList
|
||||||
|
|
||||||
if ($modal) return
|
if ($modal) return
|
||||||
if ($authError) return showAuthError()
|
if ($authError) return showAuthError()
|
||||||
if ($relaysPendingTrust.includes(url)) return showPendingTrust()
|
if ($relaysPendingTrust.includes(url)) return showPendingTrust()
|
||||||
|
|
||||||
if (!$userSpaceUrls.includes(url)) {
|
if (!$userSpaceUrls.includes(url) && !joinPrompted.has(url)) {
|
||||||
joinPrompted.add(url)
|
joinPrompted.add(url)
|
||||||
pushModal(SpaceJoin, {url})
|
pushModal(SpaceJoin, {url})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,24 +29,16 @@
|
||||||
|
|
||||||
<PageContent class="flex flex-col gap-4 p-4">
|
<PageContent class="flex flex-col gap-4 p-4">
|
||||||
<div class="card flex flex-col gap-4">
|
<div class="card flex flex-col gap-4">
|
||||||
<div class="flex justify-between">
|
<div class="relative flex gap-4">
|
||||||
<div class="relative flex gap-4">
|
<div class="relative">
|
||||||
<div class="relative">
|
<RelayIcon {url} size={14} class="rounded-full" />
|
||||||
<RelayIcon {url} size={14} class="rounded-full" />
|
</div>
|
||||||
</div>
|
<div class="flex min-w-0 flex-col">
|
||||||
<div class="flex min-w-0 flex-col">
|
<h1 class="ellipsize whitespace-nowrap">
|
||||||
<h1 class="ellipsize whitespace-nowrap">
|
<RelayName {url} class="text-2xl font-bold" />
|
||||||
<RelayName {url} class="text-2xl font-bold" />
|
</h1>
|
||||||
</h1>
|
<p class="ellipsize text-sm text-primary">{displayRelayUrl(url)}</p>
|
||||||
<p class="ellipsize text-sm text-primary">{displayRelayUrl(url)}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{#if $userIsAdmin}
|
|
||||||
<Button class="button button-primary" onclick={startEdit}>
|
|
||||||
<Icon icon={Pen} />
|
|
||||||
<span class="hidden sm:inline">Edit</span>
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<RelayDescription {url} />
|
<RelayDescription {url} />
|
||||||
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
{#if $relay?.terms_of_service || $relay?.privacy_policy}
|
||||||
|
|
@ -105,6 +97,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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>
|
||||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||||
<div class="lg:col-span-2 flex flex-col gap-4">
|
<div class="lg:col-span-2 flex flex-col gap-4">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue