work on white labeling with acl'd default spaces

This commit is contained in:
Jon Staab 2026-06-30 10:49:17 -07:00
parent e5c752ce9e
commit 182e2ce6fd
15 changed files with 102 additions and 73 deletions

1
.env
View file

@ -8,6 +8,7 @@ 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."

View file

@ -22,7 +22,7 @@ You can also optionally create an `.env.local` file and populate it with the fol
**Defaults**
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust
- `VITE_DEFAULT_SPACES` - A comma-separated list of relay urls that new users will be automatically joined to on signup
- `VITE_DEFAULT_SPACES` - A comma-separated list of relay urls that new users will be automatically joined to on signup. Each one may optionally include an invite code, delimited by `|`, e.g. `my.space.com|CODE`.
- `VITE_DEFAULT_RELAYS` - A comma-separated list of relay urls used as default outbox/inbox relays
- `VITE_DEFAULT_MESSAGING_RELAYS` - A comma-separated list of relay urls used for encrypted direct messages
- `VITE_DEFAULT_BLOSSOM_SERVERS` - A comma-separated list of blossom server urls used for file uploads

View file

@ -114,7 +114,7 @@
{/snippet}
</FieldInline>
<p class="text-sm">
Forgot your password? <Button class="button button-link" onclick={loginWithOTP}
Forgot your password? <Button class="link" onclick={loginWithOTP}
>Log in with a one-time access code</Button
>.
</p>

View file

@ -1,11 +1,16 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import Content from "@app/components/Content.svelte"
const {...props} = $props()
type Props = {
url: string
}
const relay = deriveRelay(props.url)
const {url}: Props = $props()
const relay = deriveRelay(url)
</script>
{#if $relay?.description}
<p class={props.class}>{$relay.description}</p>
<Content event={{content: $relay.description, tags: []}} />
{/if}

View file

@ -24,7 +24,7 @@
<div class="relative">
<div
class="flex justify-center items-center h-12 w-12 min-w-12 rounded-full border-2 border-solid bg-surface-more border-line">
<RelayIcon {url} size={10} />
<RelayIcon {url} size={10} class="rounded-full" />
</div>
</div>
{#if $rooms.includes(url)}
@ -40,7 +40,9 @@
<p class="text-xs sm:text-sm opacity-75">{url}</p>
</div>
</div>
<RelayDescription {url} class="text-sm sm:text-md" />
<div class="text-sm sm:text-md">
<RelayDescription {url} />
</div>
</div>
{#if !hideFavorites && $favorited.size > 0}
<div class="flex gap-2 card card-sm">

View file

@ -2,8 +2,9 @@
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} from "@welshman/app"
import {loginWithNip01, publishThunk, waitForThunkCompletion} from "@welshman/app"
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit"
@ -17,6 +18,7 @@
import SignUpProfile from "@app/components/SignUpProfile.svelte"
import SignUpComplete from "@app/components/SignUpComplete.svelte"
import {initProfile} from "@app/profiles"
import {attemptRelayAccess} from "@app/relays"
import {
POMADE_SIGNERS,
PLATFORM_NAME,
@ -39,24 +41,32 @@
const login = () => pushModal(LogIn)
const completeSignup = () => {
// Add default outbox/inbox relays
publishThunk({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
})
const completeSignup = async () => {
// Join all default spaces first
const defaultSpaceUrls = await Promise.all(
DEFAULT_SPACES.map(async ([url, claim]) => {
await attemptRelayAccess(url, claim)
// Add default messaging relays
publishThunk({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS,
})
return url
})
)
// Save the user's profile
initProfile(getKey<Profile>("signup.profile")!)
// Add default outbox/inbox/messaging relays, profile, spaces
const thunks = await Promise.all([
publishThunk({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
}),
publishThunk({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS,
}),
initProfile(getKey<Profile>("signup.profile")!),
setSpaces(defaultSpaceUrls),
])
// Auto-join default spaces
setSpaces(DEFAULT_SPACES)
// Wait for all the thunks to complete
await Promise.all(thunks.map(waitForThunkCompletion))
// Don't show any notifications for old content
setChecked("*")
@ -71,12 +81,12 @@
profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}),
complete: () =>
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
finalize: () => {
finalize: async () => {
const email = getKey<string>("signup.email")!
const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
loginWithPomade(clientOptions, email)
completeSignup()
await completeSignup()
},
},
nostr: {
@ -84,11 +94,11 @@
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => {
finalize: async () => {
const secret = getKey<string>("signup.secret")!
loginWithNip01(secret)
completeSignup()
await completeSignup()
},
},
}

View file

@ -4,6 +4,7 @@
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
@ -20,9 +21,21 @@
const {next, step, totalSteps}: Props = $props()
const back = () => history.back()
const submit = async () => {
loading = true
try {
await next()
} finally {
loading = false
}
}
let loading = $state(false)
</script>
<Modal tag="form" onsubmit={preventDefault(next)}>
<Modal tag="form" onsubmit={preventDefault(submit)}>
<ModalBody>
<ModalHeader>
<ModalTitle>You're all set!</ModalTitle>
@ -44,9 +57,13 @@
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="button button-primary" type="submit">
<Icon icon={HomeSmile} />
Go to Dashboard
<Button class="button button-primary" type="submit" disabled={loading}>
{#if loading}
<Spinner {loading}>Go to Dashboard</Spinner>
{:else}
<Icon icon={HomeSmile} />
Go to Dashboard
{/if}
</Button>
</ModalFooter>
</Modal>

View file

@ -134,16 +134,16 @@
</ModalHeader>
<div>
{#if loading}
<p class="flex-justify-center items-center">
<p class="flex justify-center items-center">
<Spinner {loading}>Requesting an invite link...</Spinner>
</p>
{:else if isGenericError}
<p class="flex-justify-center items-center">
Unable to reach the relay. Please check your connection and try again.
<p class="flex justify-center items-center">
Unable to reach the space. Please check your connection and try again.
</p>
{:else if isExplicitAuthError}
<p class="flex-justify-center items-center">
Oops! It looks like you're not a member of this relay.
<p class="flex justify-center items-center">
Oops! It looks like you're not a member of this space.
</p>
{:else}
<div class="flex flex-col items-center gap-6">

View file

@ -66,7 +66,7 @@
<div class="m-auto flex flex-col gap-4">
<p>
This space has opted not to publish <Button
class="button button-link"
class="link"
onclick={showInfoSignatures}>digital signatures</Button
>, which means that they have the ability to forge messages from other users.
</p>

View file

@ -51,7 +51,11 @@ 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(normalizeRelayUrl)
export const DEFAULT_SPACES = fromCsv(import.meta.env.VITE_DEFAULT_SPACES).map(item => {
const [url, claim = ""] = item.split('|')
return [normalizeRelayUrl(url), claim]
})
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS

View file

@ -83,22 +83,22 @@ export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) =>
return path
}
export const goToSpace = (url: string) => {
export const goToSpace = (url: string, hash = "") => {
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
if (prevPath && prevPath !== makeSpacePath(url)) {
return goto(prevPath, {replaceState: true})
return goto(prevPath + hash, {replaceState: true})
}
if (!hasNip29(getRelay(url))) {
return goto(makeSpaceChatPath(url), {replaceState: true})
return goto(makeSpaceChatPath(url) + hash, {replaceState: true})
}
if (window.matchMedia(`(min-width: ${theme.screens.md})`).matches) {
return goto(makeSpacePath(url, "about"), {replaceState: true})
return goto(makeSpacePath(url, "about") + hash, {replaceState: true})
}
return goto(makeSpacePath(url), {replaceState: true})
return goto(makeSpacePath(url) + hash, {replaceState: true})
}
// Content types, events

View file

@ -49,6 +49,10 @@
@apply border border-solid;
}
[data-fl-theme="navy"] .card-button {
@apply border border-solid;
}
[data-fl-theme="clay"] .card-button {
@apply border-2 border-solid shadow-sm;

View file

@ -1,14 +1,15 @@
<script lang="ts">
import {onMount} from "svelte"
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"
onMount(async () => {
$effect(() => {
if (PLATFORM_RELAYS.length > 0) {
goToSpace(PLATFORM_RELAYS[0])
goToSpace(PLATFORM_RELAYS[0], $page.url.hash)
} else {
goto("/home")
goto("/home" + $page.url.hash)
}
})
</script>

View file

@ -142,7 +142,7 @@
{/snippet}
{#snippet info()}
<p>
Your public key is your nostr user identifier. It also allows other people to authenticate
Your public key is your user identifier. It also allows other people to authenticate
your messages.
</p>
{/snippet}

View file

@ -13,7 +13,7 @@
import SpaceAuthError from "@app/components/SpaceAuthError.svelte"
import SpaceTrustRelay from "@app/components/SpaceTrustRelay.svelte"
import SpaceJoin from "@app/components/SpaceJoin.svelte"
import {pushModal} from "@app/modal"
import {modal, pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
import {decodeRelay, deriveRelayAuthError} from "@app/relays"
import {userGroupList, userSpaceUrls} from "@app/groups"
@ -35,34 +35,19 @@
const showPendingTrust = once(() => pushModal(SpaceTrustRelay, {url}, {noEscape: true}))
// Watch for relay errors and notify the user
$effect(() => {
if ($authError) {
showAuthError()
} else if ($relaysPendingTrust.includes(url)) {
showPendingTrust()
}
})
// Direct links skip Discover — prompt to join when relay is not in the user's space list.
const shouldPromptJoin = $derived.by(() => {
// 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
return (
Boolean($pubkey) &&
!$userSpaceUrls.includes(url) &&
!$authError &&
!$relaysPendingTrust.includes(url)
)
})
if ($modal) return
if ($authError) return showAuthError()
if ($relaysPendingTrust.includes(url)) return showPendingTrust()
$effect(() => {
if (!shouldPromptJoin || joinPrompted.has(url)) {
return
if (!$userSpaceUrls.includes(url)) {
joinPrompted.add(url)
pushModal(SpaceJoin, {url})
}
joinPrompted.add(url)
pushModal(SpaceJoin, {url})
})
</script>