work on white labeling with acl'd default spaces
This commit is contained in:
parent
e5c752ce9e
commit
182e2ce6fd
15 changed files with 102 additions and 73 deletions
1
.env
1
.env
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue