mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-18 19:23:40 +00:00
Add new onboarding workflow
This commit is contained in:
parent
6a8763f777
commit
6714eac7e0
@ -1,5 +1,7 @@
|
||||
# Current
|
||||
|
||||
- [ ] Collapse relaycard and relaycardsimple?
|
||||
|
||||
- [ ] Go over onboarding process, suggest some good relays for newcomers
|
||||
- [ ] Submit blog post with new onboarding process built in
|
||||
- [ ] Fix hover on notes in modal
|
||||
|
@ -55,7 +55,7 @@
|
||||
import ConnectUser from "src/views/login/ConnectUser.svelte"
|
||||
import PrivKeyLogin from "src/views/login/PrivKeyLogin.svelte"
|
||||
import PubKeyLogin from "src/views/login/PubKeyLogin.svelte"
|
||||
import SignUp from "src/views/login/SignUp.svelte"
|
||||
import Onboarding from "src/views/onboarding/Onboarding.svelte"
|
||||
import NoteCreate from "src/views/notes/NoteCreate.svelte"
|
||||
import NoteDetail from "src/views/notes/NoteDetail.svelte"
|
||||
import PersonList from "src/views/person/PersonList.svelte"
|
||||
@ -244,8 +244,8 @@
|
||||
<NoteCreate pubkey={$modal.pubkey} />
|
||||
{:else if $modal.type === 'relay/add'}
|
||||
<AddRelay />
|
||||
{:else if $modal.type === 'signUp'}
|
||||
<SignUp />
|
||||
{:else if $modal.type === 'onboarding'}
|
||||
<Onboarding stage={$modal.stage} />
|
||||
{:else if $modal.type === 'room/edit'}
|
||||
<ChatEdit {...$modal} />
|
||||
{:else if $modal.type === 'login/privkey'}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import type {DisplayEvent} from 'src/util/types'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {omit, sortBy} from 'ramda'
|
||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent} from 'src/util/html'
|
||||
import {sleep} from 'src/util/misc'
|
||||
import {displayPerson, findReplyId} from 'src/util/nostr'
|
||||
import {getUserFollows} from 'src/agent/social'
|
||||
import {getUserReadRelays} from 'src/agent/relays'
|
||||
@ -14,6 +14,9 @@ import {routes, modal, toast} from 'src/app/ui'
|
||||
|
||||
export const loadAppData = async pubkey => {
|
||||
if (getUserReadRelays().length > 0) {
|
||||
// Delay since this gets in the way of quickly loading feeds very often
|
||||
await sleep(5000)
|
||||
|
||||
await Promise.all([
|
||||
alerts.listen(pubkey),
|
||||
network.loadPeople(getUserFollows()),
|
||||
@ -27,12 +30,6 @@ export const login = (method, key) => {
|
||||
modal.set({type: 'login/connect', noEscape: true})
|
||||
}
|
||||
|
||||
export const signup = privkey => {
|
||||
keys.login('privkey', privkey)
|
||||
|
||||
navigate('/notes/follows')
|
||||
}
|
||||
|
||||
export const renderNote = (note, {showEntire = false}) => {
|
||||
let content
|
||||
|
||||
|
48
src/partials/PersonInfo.svelte
Normal file
48
src/partials/PersonInfo.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import {last} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent, noEvent} from "src/util/html"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {routes} from "src/app/ui"
|
||||
|
||||
export let person
|
||||
export let addPetname = null
|
||||
export let removePetname = null
|
||||
</script>
|
||||
|
||||
<a
|
||||
in:fly={{y: 20}}
|
||||
href={routes.person(person.pubkey)}
|
||||
class="flex gap-4 border-l-2 border-solid border-dark hover:bg-black hover:border-accent transition-all py-3 px-4 overflow-hidden">
|
||||
<div
|
||||
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||
style="background-image: url({person.kind0?.picture})" />
|
||||
<div class="flex-grow flex flex-col gap-4 min-w-0">
|
||||
<div class="flex gap-2 items-start justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-xl">{displayPerson(person)}</h1>
|
||||
{#if person.verified_as}
|
||||
<div class="flex gap-1 text-sm">
|
||||
<i class="fa fa-user-check text-accent" />
|
||||
<span class="text-light">{last(person.verified_as.split('@'))}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if removePetname}
|
||||
<Anchor type="button-accent" on:click={noEvent(() => removePetname(person))}>
|
||||
Following
|
||||
</Anchor>
|
||||
{/if}
|
||||
{#if addPetname}
|
||||
<Anchor type="button" on:click={noEvent(() => addPetname(person))}>
|
||||
Follow
|
||||
</Anchor>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="overflow-hidden text-ellipsis">
|
||||
{@html renderContent(ellipsize(person.kind0?.about || '', 140))}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
82
src/partials/RelayCard.svelte
Normal file
82
src/partials/RelayCard.svelte
Normal file
@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import cx from 'classnames'
|
||||
import {last} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {poll, stringToColor} from "src/util/misc"
|
||||
import {between} from 'hurdak/lib/hurdak'
|
||||
import {fly} from 'svelte/transition'
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import pool from 'src/agent/pool'
|
||||
|
||||
export let relay
|
||||
export let theme = 'dark'
|
||||
export let removeRelay = null
|
||||
export let addRelay = null
|
||||
|
||||
let quality = null
|
||||
let message = null
|
||||
let showStatus = false
|
||||
|
||||
onMount(() => {
|
||||
return poll(10_000, async () => {
|
||||
const conn = await pool.getConnection(relay.url)
|
||||
|
||||
if (conn) {
|
||||
[quality, message] = conn.getQuality()
|
||||
} else {
|
||||
quality = null
|
||||
message = "Not connected"
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cx(
|
||||
`bg-${theme}`,
|
||||
"rounded border border-l-2 border-solid border-medium shadow flex flex-col justify-between gap-3 py-3 px-6"
|
||||
)}
|
||||
style={`border-left-color: ${stringToColor(relay.url)}`}
|
||||
in:fly={{y: 20}}>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div class="flex gap-2 items-center text-xl">
|
||||
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
|
||||
<Anchor type="unstyled" href={`/relays/${btoa(relay.url)}`}>
|
||||
{last(relay.url.split('://'))}
|
||||
</Anchor>
|
||||
<span
|
||||
on:mouseout={() => {showStatus = false}}
|
||||
on:mouseover={() => {showStatus = true}}
|
||||
class="w-2 h-2 rounded-full bg-medium cursor-pointer"
|
||||
class:bg-medium={message === 'Not connected'}
|
||||
class:bg-danger={quality <= 0.3 && message !== 'Not connected'}
|
||||
class:bg-warning={between(0.3, 0.7, quality)}
|
||||
class:bg-success={quality > 0.7}>
|
||||
</span>
|
||||
<p
|
||||
class="text-light text-sm transition-all hidden sm:block"
|
||||
class:opacity-0={!showStatus}
|
||||
class:opacity-1={showStatus}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
{#if removeRelay}
|
||||
<button
|
||||
class="flex gap-3 items-center text-light"
|
||||
on:click={() => removeRelay(relay)}>
|
||||
<i class="fa fa-right-from-bracket" /> Leave
|
||||
</button>
|
||||
{/if}
|
||||
{#if addRelay}
|
||||
<button
|
||||
class="flex gap-3 items-center text-light"
|
||||
on:click={() => addRelay(relay)}>
|
||||
<i class="fa fa-right-to-bracket" /> Join
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if relay.description}
|
||||
<p>{relay.description}</p>
|
||||
{/if}
|
||||
<slot name="controls" />
|
||||
</div>
|
@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import user from 'src/agent/user'
|
||||
import {modal} from "src/app/ui"
|
||||
import {login} from "src/app"
|
||||
|
||||
@ -19,13 +21,17 @@
|
||||
}
|
||||
|
||||
const signUp = () => {
|
||||
modal.set({type: 'signUp'})
|
||||
modal.set({type: 'onboarding', stage: 'intro'})
|
||||
}
|
||||
|
||||
const pubkeyLogIn = () => {
|
||||
modal.set({type: 'login/pubkey'})
|
||||
}
|
||||
|
||||
if (user.getPubkey()) {
|
||||
navigate('/')
|
||||
}
|
||||
|
||||
document.title = "Log In"
|
||||
</script>
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import RelayCardSimple from 'src/views/relays/RelayCardSimple.svelte'
|
||||
import RelayCardSimple from 'src/partials/RelayCardSimple.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Modal from 'src/partials/Modal.svelte'
|
||||
import {getUserReadRelays} from 'src/agent/relays'
|
||||
|
@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {nip19, generatePrivateKey} from 'nostr-tools'
|
||||
import {copyToClipboard} from "src/util/html"
|
||||
import {toHex} from "src/util/nostr"
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import {toast} from "src/app/ui"
|
||||
import {signup} from "src/app"
|
||||
|
||||
const nsec = nip19.nsecEncode(generatePrivateKey())
|
||||
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
|
||||
|
||||
const logIn = () => signup(toHex(nsec))
|
||||
|
||||
const copyKey = () => {
|
||||
copyToClipboard(nsec)
|
||||
toast.show("info", "Your private key has been copied to the clipboard.")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<Content size="lg" class="text-center">
|
||||
<Heading>Create an Account</Heading>
|
||||
<p>
|
||||
Don't have a nostr account? We've created a brand new private key for you below.
|
||||
Make sure to click to copy and store it somewhere safe - this is your account's password!
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-grow">
|
||||
<Input disabled placeholder={"•".repeat(63)}>
|
||||
<i slot="before" class="fa fa-key" />
|
||||
<button slot="after" class="cursor-pointer fa fa-copy" on:click={copyKey} />
|
||||
</Input>
|
||||
</div>
|
||||
<Anchor type="button" on:click={logIn}>Log In</Anchor>
|
||||
</div>
|
||||
<p class="bg-black rounded border-2 border-solid border-warning py-3 px-6">
|
||||
Note that sharing your private key directly is not recommended, instead you should use
|
||||
a <Anchor href={nip07} external>compatible browser extension</Anchor> to securely store your key.
|
||||
</p>
|
||||
</Content>
|
||||
|
@ -11,7 +11,7 @@
|
||||
import ImageInput from "src/partials/ImageInput.svelte"
|
||||
import Preview from "src/partials/Preview.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import RelayCardSimple from "src/views/relays/RelayCardSimple.svelte"
|
||||
import RelayCardSimple from "src/partials/RelayCardSimple.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Modal from "src/partials/Modal.svelte"
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
|
81
src/views/onboarding/Onboarding.svelte
Normal file
81
src/views/onboarding/Onboarding.svelte
Normal file
@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import {uniq} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {generatePrivateKey} from 'nostr-tools'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {shuffle} from 'src/util/misc'
|
||||
import {displayPerson} from 'src/util/nostr'
|
||||
import OnboardingIntro from 'src/views/onboarding/OnboardingIntro.svelte'
|
||||
import OnboardingKey from 'src/views/onboarding/OnboardingKey.svelte'
|
||||
import OnboardingRelays from 'src/views/onboarding/OnboardingRelays.svelte'
|
||||
import OnboardingFollows from 'src/views/onboarding/OnboardingFollows.svelte'
|
||||
import OnboardingComplete from 'src/views/onboarding/OnboardingComplete.svelte'
|
||||
import {getFollows} from 'src/agent/social'
|
||||
import {getPubkeyWriteRelays, sampleRelays} from 'src/agent/relays'
|
||||
import database from 'src/agent/database'
|
||||
import network from 'src/agent/network'
|
||||
import user from 'src/agent/user'
|
||||
import keys from "src/agent/keys"
|
||||
import {loadAppData} from "src/app"
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
export let stage
|
||||
|
||||
let relays = [
|
||||
{url: 'wss://nostr-pub.wellorder.net'},
|
||||
{url: 'wss://nostr.zebedee.cloud'},
|
||||
{url: 'wss://nos.lol'},
|
||||
{url: 'wss://brb.io'},
|
||||
]
|
||||
|
||||
let follows = [
|
||||
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod
|
||||
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
|
||||
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // jack
|
||||
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", // Gigi
|
||||
]
|
||||
|
||||
const privkey = generatePrivateKey()
|
||||
|
||||
const signup = async () => {
|
||||
await keys.login('privkey', privkey)
|
||||
await user.updateRelays(() => relays)
|
||||
await user.updatePetnames(() => follows.map(pubkey => {
|
||||
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
|
||||
const name = displayPerson(database.getPersonWithFallback(pubkey))
|
||||
|
||||
return ["p", pubkey, url, name]
|
||||
}))
|
||||
|
||||
loadAppData(user.getPubkey())
|
||||
|
||||
modal.set(null)
|
||||
navigate('/notes/follows')
|
||||
}
|
||||
|
||||
// Prime our people cache for hardcoded follows and a sample of people they follow
|
||||
onMount(async () => {
|
||||
await network.loadPeople(follows, {relays})
|
||||
|
||||
const others = shuffle(uniq(follows.flatMap(getFollows))).slice(0, 256)
|
||||
|
||||
await network.loadPeople(others, {relays})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#key stage}
|
||||
<div in:fly={{y: 20}}>
|
||||
{#if stage === 'intro'}
|
||||
<OnboardingIntro />
|
||||
{:else if stage === 'key'}
|
||||
<OnboardingKey {privkey} />
|
||||
{:else if stage === 'relays'}
|
||||
<OnboardingRelays bind:relays={relays} />
|
||||
{:else if stage === 'follows'}
|
||||
<OnboardingFollows bind:follows={follows} />
|
||||
{:else}
|
||||
<OnboardingComplete {signup} />
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
27
src/views/onboarding/OnboardingComplete.svelte
Normal file
27
src/views/onboarding/OnboardingComplete.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
|
||||
export let signup
|
||||
|
||||
let loading = false
|
||||
|
||||
const startSignup = () => {
|
||||
loading = true
|
||||
signup()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Content size="lg" class="text-center">
|
||||
<Heading>Welcome to Nostr</Heading>
|
||||
<p>
|
||||
You’re all set! If have any questions, or need any help, just ask. Your fellow
|
||||
nostriches are always happy to lend a hand.
|
||||
</p>
|
||||
<Anchor {loading} type="button-accent" on:click={startSignup}>Get on Nostr</Anchor>
|
||||
{#if loading}
|
||||
<Spinner />
|
||||
{/if}
|
||||
</Content>
|
72
src/views/onboarding/OnboardingFollows.svelte
Normal file
72
src/views/onboarding/OnboardingFollows.svelte
Normal file
@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import {without} from 'ramda'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import PersonInfo from 'src/partials/PersonInfo.svelte'
|
||||
import database from 'src/agent/database'
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
export let follows
|
||||
|
||||
let q = ""
|
||||
let search
|
||||
|
||||
const knownPeople = database.watch('people', t => t.all({'kind0.name:!nil': null}))
|
||||
|
||||
$: search = fuzzy(
|
||||
$knownPeople.filter(p => !follows.includes(p.pubkey)),
|
||||
{keys: ["kind0.name", "kind0.about", "pubkey"]}
|
||||
)
|
||||
|
||||
const removePetname = ({pubkey}) => {
|
||||
follows = without([pubkey], follows)
|
||||
}
|
||||
|
||||
const addPetname = ({pubkey}) => {
|
||||
follows = follows.concat(pubkey)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Content>
|
||||
<Content class="text-center">
|
||||
<Heading>Find Your People</Heading>
|
||||
<p>
|
||||
To get you started, we’ve added some interesting people to your follow list.
|
||||
You can update your follows list at any time.
|
||||
</p>
|
||||
<Anchor
|
||||
type="button-accent"
|
||||
on:click={() => modal.set({type: 'onboarding', stage: 'complete'})}>
|
||||
Continue
|
||||
</Anchor>
|
||||
</Content>
|
||||
<div class="flex gap-2 items-center">
|
||||
<i class="fa fa-user-astronaut fa-lg" />
|
||||
<h2 class="staatliches text-2xl">Your follows</h2>
|
||||
</div>
|
||||
{#if follows.length === 0}
|
||||
<div class="text-center mt-8 flex gap-2 justify-center items-center">
|
||||
<i class="fa fa-triangle-exclamation" />
|
||||
<span>No follows selected</span>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
{#each follows as pubkey}
|
||||
<PersonInfo person={database.getPersonWithFallback(pubkey)} {removePetname} />
|
||||
{/each}
|
||||
|
||||
{/if}
|
||||
<div class="flex gap-2 items-center">
|
||||
<i class="fa fa-earth-asia fa-lg" />
|
||||
<h2 class="staatliches text-2xl">Other people</h2>
|
||||
</div>
|
||||
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
|
||||
<i slot="before" class="fa-solid fa-search" />
|
||||
</Input>
|
||||
{#each search(q).slice(0, 50) as person (person.pubkey)}
|
||||
<PersonInfo {person} {addPetname} />
|
||||
{/each}
|
||||
</Content>
|
24
src/views/onboarding/OnboardingIntro.svelte
Normal file
24
src/views/onboarding/OnboardingIntro.svelte
Normal file
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
const tutorialUrl = "https://nostr.how/"
|
||||
</script>
|
||||
|
||||
<Content size="lg" class="text-center">
|
||||
<Heading>Create an Account</Heading>
|
||||
<p>
|
||||
New to Nostr? Click <Anchor external href={tutorialUrl}>here</Anchor> or watch the video
|
||||
below for a crash course on what it is, and how to use it.
|
||||
</p>
|
||||
<video controls src="" class="object-center object-contain" />
|
||||
<p>
|
||||
When you’re ready to dive in, click below and we’ll guide you through
|
||||
the process of creating an account.
|
||||
</p>
|
||||
<Anchor type="button-accent" on:click={() => modal.set({type: 'onboarding', stage: 'key'})}>
|
||||
Let's go!
|
||||
</Anchor>
|
||||
</Content>
|
41
src/views/onboarding/OnboardingKey.svelte
Normal file
41
src/views/onboarding/OnboardingKey.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {copyToClipboard} from "src/util/html"
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import {modal, toast} from "src/app/ui"
|
||||
|
||||
export let privkey
|
||||
|
||||
const nsec = nip19.nsecEncode(privkey)
|
||||
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
|
||||
|
||||
const copyKey = () => {
|
||||
copyToClipboard(nsec)
|
||||
toast.show("info", "Your private key has been copied to the clipboard. Keep it secret!")
|
||||
}
|
||||
</script>
|
||||
|
||||
<Content size="lg" class="text-center">
|
||||
<Heading>Generate a Key</Heading>
|
||||
<p>
|
||||
Your private key is your password, and gives you total control over your Nostr account.
|
||||
We've generated a fresh one for you below – store it somewhere safe!
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<Input disabled placeholder={"•".repeat(53)} wrapperClass="flex-grow">
|
||||
<i slot="before" class="fa fa-lock" />
|
||||
<button slot="after" class="cursor-pointer fa fa-copy" on:click={copyKey} />
|
||||
</Input>
|
||||
<Anchor type="button-accent" on:click={() => modal.set({type: 'onboarding', stage: 'relays'})}>
|
||||
Log in
|
||||
</Anchor>
|
||||
</div>
|
||||
<p>
|
||||
Avoid pasting your key into too many apps and websites. Instead, use
|
||||
a <Anchor href={nip07} external>compatible browser extension</Anchor> to
|
||||
securely store your key.
|
||||
</p>
|
||||
</Content>
|
81
src/views/onboarding/OnboardingRelays.svelte
Normal file
81
src/views/onboarding/OnboardingRelays.svelte
Normal file
@ -0,0 +1,81 @@
|
||||
<script lang="ts">
|
||||
import {reject, pluck, whereEq} from 'ramda'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import RelayCard from 'src/partials/RelayCard.svelte'
|
||||
import database from 'src/agent/database'
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
export let relays
|
||||
|
||||
let q = ""
|
||||
let search
|
||||
|
||||
const knownRelays = database.watch('relays', t => t.all())
|
||||
|
||||
$: {
|
||||
const joined = new Set(pluck('url', relays))
|
||||
|
||||
search = fuzzy(
|
||||
$knownRelays.filter(r => !joined.has(r.url)),
|
||||
{keys: ["name", "description", "url"]}
|
||||
)
|
||||
}
|
||||
|
||||
const removeRelay = ({url}) => {
|
||||
relays = reject(whereEq({url}), relays)
|
||||
}
|
||||
|
||||
const addRelay = relay => {
|
||||
relays = relays.concat(relay)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Content>
|
||||
<Content class="text-center">
|
||||
<Heading>Get Connected</Heading>
|
||||
<p>
|
||||
Nostr is a protocol, not a platform. This means that <i>you</i> choose where to store your
|
||||
data. Select your preferred storage relays below, or click "continue" to use some
|
||||
reasonable defaults. You can change your selection any time.
|
||||
</p>
|
||||
<Anchor
|
||||
type="button-accent"
|
||||
on:click={() => modal.set({type: 'onboarding', stage: 'follows'})}>
|
||||
Continue
|
||||
</Anchor>
|
||||
</Content>
|
||||
<div class="flex gap-2 items-center">
|
||||
<i class="fa fa-server fa-lg" />
|
||||
<h2 class="staatliches text-2xl">Your relays</h2>
|
||||
</div>
|
||||
{#if relays.length === 0}
|
||||
<div class="text-center mt-8 flex gap-2 justify-center items-center">
|
||||
<i class="fa fa-triangle-exclamation" />
|
||||
<span>No relays connected</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
{#each relays as relay (relay.url)}
|
||||
<RelayCard {relay} {removeRelay} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-2 items-center">
|
||||
<i class="fa fa-earth-asia fa-lg" />
|
||||
<h2 class="staatliches text-2xl">Other relays</h2>
|
||||
</div>
|
||||
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
|
||||
<i slot="before" class="fa-solid fa-search" />
|
||||
</Input>
|
||||
{#each (search(q) || []).slice(0, 50) as relay (relay.url)}
|
||||
<RelayCard {relay} {addRelay} />
|
||||
{/each}
|
||||
<small class="text-center">
|
||||
Showing {Math.min($knownRelays.length - relays.length, 50)}
|
||||
of {$knownRelays.length - relays.length} known relays
|
||||
</small>
|
||||
</Content>
|
@ -1,55 +1,25 @@
|
||||
<script lang="ts">
|
||||
import {last} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent, noEvent} from "src/util/html"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import PersonInfo from 'src/partials/PersonInfo.svelte'
|
||||
import {getPubkeyWriteRelays, sampleRelays} from 'src/agent/relays'
|
||||
import user from 'src/agent/user'
|
||||
import {routes} from "src/app/ui"
|
||||
|
||||
export let person
|
||||
|
||||
const {petnamePubkeys} = user
|
||||
|
||||
const addPetname = pubkey => {
|
||||
const addPetname = ({pubkey}) => {
|
||||
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
|
||||
|
||||
user.addPetname(pubkey, url, displayPerson(person))
|
||||
}
|
||||
|
||||
const removePetname = ({pubkey}) => {
|
||||
user.removePetname(pubkey)
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
in:fly={{y: 20}}
|
||||
href={routes.person(person.pubkey)}
|
||||
class="flex gap-4 border-l-2 border-solid border-dark hover:bg-black hover:border-accent transition-all py-3 px-6 overflow-hidden">
|
||||
<div
|
||||
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||
style="background-image: url({person.kind0?.picture})" />
|
||||
<div class="flex-grow flex flex-col gap-4 min-w-0">
|
||||
<div class="flex gap-2 items-start justify-between">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-xl">{displayPerson(person)}</h1>
|
||||
{#if person.verified_as}
|
||||
<div class="flex gap-1 text-sm">
|
||||
<i class="fa fa-user-check text-accent" />
|
||||
<span class="text-light">{last(person.verified_as.split('@'))}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $petnamePubkeys.includes(person.pubkey)}
|
||||
<Anchor type="button-accent" on:click={noEvent(() => user.removePetname(person.pubkey))}>
|
||||
Following
|
||||
</Anchor>
|
||||
{:else}
|
||||
<Anchor type="button" on:click={noEvent(() => addPetname(person.pubkey))}>
|
||||
Follow
|
||||
</Anchor>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="overflow-hidden text-ellipsis">
|
||||
{@html renderContent(ellipsize(person.kind0?.about || '', 140))}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<PersonInfo
|
||||
{person}
|
||||
addPetname={$petnamePubkeys.includes(person.pubkey) ? addPetname : null}
|
||||
removePetname={$petnamePubkeys.includes(person.pubkey) ? null : removePetname} />
|
||||
|
@ -1,12 +1,9 @@
|
||||
<script lang="ts">
|
||||
import cx from 'classnames'
|
||||
import {last, find, propEq} from 'ramda'
|
||||
import {find, propEq} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {poll, stringToColor} from "src/util/misc"
|
||||
import {between} from 'hurdak/lib/hurdak'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {poll} from "src/util/misc"
|
||||
import Toggle from "src/partials/Toggle.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import RelayCard from "src/partials/RelayCard.svelte"
|
||||
import pool from 'src/agent/pool'
|
||||
import user from "src/agent/user"
|
||||
import {loadAppData} from 'src/app'
|
||||
@ -17,17 +14,16 @@
|
||||
|
||||
let quality = null
|
||||
let message = null
|
||||
let showStatus = false
|
||||
let joined = false
|
||||
|
||||
const {relays, canPublish} = user
|
||||
|
||||
$: joined = find(propEq('url', relay.url), $relays)
|
||||
|
||||
const removeRelay = () => user.removeRelay(relay.url)
|
||||
const removeRelay = ({url}) => user.removeRelay(url)
|
||||
|
||||
const addRelay = async () => {
|
||||
await user.addRelay(relay.url)
|
||||
const addRelay = async ({url}) => {
|
||||
await user.addRelay(url)
|
||||
|
||||
if (!user.getProfile()?.kind0) {
|
||||
loadAppData(user.getPubkey())
|
||||
@ -48,61 +44,16 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={cx(
|
||||
`bg-${theme}`,
|
||||
"rounded border border-l-2 border-solid border-medium shadow flex flex-col justify-between gap-3 py-3 px-6"
|
||||
)}
|
||||
style={`border-left-color: ${stringToColor(relay.url)}`}
|
||||
in:fly={{y: 20}}>
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<div class="flex gap-2 items-center text-xl">
|
||||
<i class={relay.url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
|
||||
<Anchor type="unstyled" href={`/relays/${btoa(relay.url)}`}>
|
||||
{last(relay.url.split('://'))}
|
||||
</Anchor>
|
||||
<span
|
||||
on:mouseout={() => {showStatus = false}}
|
||||
on:mouseover={() => {showStatus = true}}
|
||||
class="w-2 h-2 rounded-full bg-medium cursor-pointer"
|
||||
class:bg-medium={message === 'Not connected'}
|
||||
class:bg-danger={quality <= 0.3 && message !== 'Not connected'}
|
||||
class:bg-warning={between(0.3, 0.7, quality)}
|
||||
class:bg-success={quality > 0.7}>
|
||||
</span>
|
||||
<p
|
||||
class="text-light text-sm transition-all hidden sm:block"
|
||||
class:opacity-0={!showStatus}
|
||||
class:opacity-1={showStatus}>
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
{#if joined}
|
||||
{#if $relays.length > 1}
|
||||
<button
|
||||
class="flex gap-3 items-center text-light"
|
||||
on:click={removeRelay}>
|
||||
<i class="fa fa-right-from-bracket" /> Leave
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button
|
||||
class="flex gap-3 items-center text-light"
|
||||
on:click={addRelay}>
|
||||
<i class="fa fa-right-to-bracket" /> Join
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if relay.description}
|
||||
<p>{relay.description}</p>
|
||||
{/if}
|
||||
{#if joined && showControls && $canPublish}
|
||||
<div class="border-b border-solid border-medium -mx-6" />
|
||||
<div class="flex justify-between gap-2">
|
||||
<RelayCard
|
||||
{relay} {theme}
|
||||
addRelay={!joined && $canPublish ? addRelay : null}
|
||||
removeRelay={joined && $relays.length > 1 && canPublish ? removeRelay : null}>
|
||||
<div slot="controls" class="flex justify-between gap-2">
|
||||
{#if showControls && $canPublish}
|
||||
<span>Publish to this relay?</span>
|
||||
<Toggle
|
||||
value={relay.write}
|
||||
on:change={() => user.setRelayWriteCondition(relay.url, !relay.write)} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</RelayCard>
|
||||
|
Loading…
Reference in New Issue
Block a user