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
|
# Current
|
||||||
|
|
||||||
|
- [ ] Collapse relaycard and relaycardsimple?
|
||||||
|
|
||||||
- [ ] Go over onboarding process, suggest some good relays for newcomers
|
- [ ] Go over onboarding process, suggest some good relays for newcomers
|
||||||
- [ ] Submit blog post with new onboarding process built in
|
- [ ] Submit blog post with new onboarding process built in
|
||||||
- [ ] Fix hover on notes in modal
|
- [ ] Fix hover on notes in modal
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
import ConnectUser from "src/views/login/ConnectUser.svelte"
|
import ConnectUser from "src/views/login/ConnectUser.svelte"
|
||||||
import PrivKeyLogin from "src/views/login/PrivKeyLogin.svelte"
|
import PrivKeyLogin from "src/views/login/PrivKeyLogin.svelte"
|
||||||
import PubKeyLogin from "src/views/login/PubKeyLogin.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 NoteCreate from "src/views/notes/NoteCreate.svelte"
|
||||||
import NoteDetail from "src/views/notes/NoteDetail.svelte"
|
import NoteDetail from "src/views/notes/NoteDetail.svelte"
|
||||||
import PersonList from "src/views/person/PersonList.svelte"
|
import PersonList from "src/views/person/PersonList.svelte"
|
||||||
@ -244,8 +244,8 @@
|
|||||||
<NoteCreate pubkey={$modal.pubkey} />
|
<NoteCreate pubkey={$modal.pubkey} />
|
||||||
{:else if $modal.type === 'relay/add'}
|
{:else if $modal.type === 'relay/add'}
|
||||||
<AddRelay />
|
<AddRelay />
|
||||||
{:else if $modal.type === 'signUp'}
|
{:else if $modal.type === 'onboarding'}
|
||||||
<SignUp />
|
<Onboarding stage={$modal.stage} />
|
||||||
{:else if $modal.type === 'room/edit'}
|
{:else if $modal.type === 'room/edit'}
|
||||||
<ChatEdit {...$modal} />
|
<ChatEdit {...$modal} />
|
||||||
{:else if $modal.type === 'login/privkey'}
|
{:else if $modal.type === 'login/privkey'}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type {DisplayEvent} from 'src/util/types'
|
import type {DisplayEvent} from 'src/util/types'
|
||||||
import {navigate} from 'svelte-routing'
|
|
||||||
import {omit, sortBy} from 'ramda'
|
import {omit, sortBy} from 'ramda'
|
||||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||||
import {renderContent} from 'src/util/html'
|
import {renderContent} from 'src/util/html'
|
||||||
|
import {sleep} from 'src/util/misc'
|
||||||
import {displayPerson, findReplyId} from 'src/util/nostr'
|
import {displayPerson, findReplyId} from 'src/util/nostr'
|
||||||
import {getUserFollows} from 'src/agent/social'
|
import {getUserFollows} from 'src/agent/social'
|
||||||
import {getUserReadRelays} from 'src/agent/relays'
|
import {getUserReadRelays} from 'src/agent/relays'
|
||||||
@ -14,6 +14,9 @@ import {routes, modal, toast} from 'src/app/ui'
|
|||||||
|
|
||||||
export const loadAppData = async pubkey => {
|
export const loadAppData = async pubkey => {
|
||||||
if (getUserReadRelays().length > 0) {
|
if (getUserReadRelays().length > 0) {
|
||||||
|
// Delay since this gets in the way of quickly loading feeds very often
|
||||||
|
await sleep(5000)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
alerts.listen(pubkey),
|
alerts.listen(pubkey),
|
||||||
network.loadPeople(getUserFollows()),
|
network.loadPeople(getUserFollows()),
|
||||||
@ -27,12 +30,6 @@ export const login = (method, key) => {
|
|||||||
modal.set({type: 'login/connect', noEscape: true})
|
modal.set({type: 'login/connect', noEscape: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signup = privkey => {
|
|
||||||
keys.login('privkey', privkey)
|
|
||||||
|
|
||||||
navigate('/notes/follows')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const renderNote = (note, {showEntire = false}) => {
|
export const renderNote = (note, {showEntire = false}) => {
|
||||||
let content
|
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">
|
<script lang="ts">
|
||||||
import {fly} from 'svelte/transition'
|
import {fly} from 'svelte/transition'
|
||||||
|
import {navigate} from 'svelte-routing'
|
||||||
import Anchor from "src/partials/Anchor.svelte"
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Heading from 'src/partials/Heading.svelte'
|
import Heading from 'src/partials/Heading.svelte'
|
||||||
|
import user from 'src/agent/user'
|
||||||
import {modal} from "src/app/ui"
|
import {modal} from "src/app/ui"
|
||||||
import {login} from "src/app"
|
import {login} from "src/app"
|
||||||
|
|
||||||
@ -19,13 +21,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const signUp = () => {
|
const signUp = () => {
|
||||||
modal.set({type: 'signUp'})
|
modal.set({type: 'onboarding', stage: 'intro'})
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkeyLogIn = () => {
|
const pubkeyLogIn = () => {
|
||||||
modal.set({type: 'login/pubkey'})
|
modal.set({type: 'login/pubkey'})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.getPubkey()) {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
|
||||||
document.title = "Log In"
|
document.title = "Log In"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
import Spinner from 'src/partials/Spinner.svelte'
|
import Spinner from 'src/partials/Spinner.svelte'
|
||||||
import Input from 'src/partials/Input.svelte'
|
import Input from 'src/partials/Input.svelte'
|
||||||
import Heading from 'src/partials/Heading.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 Anchor from 'src/partials/Anchor.svelte'
|
||||||
import Modal from 'src/partials/Modal.svelte'
|
import Modal from 'src/partials/Modal.svelte'
|
||||||
import {getUserReadRelays} from 'src/agent/relays'
|
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 ImageInput from "src/partials/ImageInput.svelte"
|
||||||
import Preview from "src/partials/Preview.svelte"
|
import Preview from "src/partials/Preview.svelte"
|
||||||
import Input from "src/partials/Input.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 Content from "src/partials/Content.svelte"
|
||||||
import Modal from "src/partials/Modal.svelte"
|
import Modal from "src/partials/Modal.svelte"
|
||||||
import Heading from 'src/partials/Heading.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">
|
<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 {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 {getPubkeyWriteRelays, sampleRelays} from 'src/agent/relays'
|
||||||
import user from 'src/agent/user'
|
import user from 'src/agent/user'
|
||||||
import {routes} from "src/app/ui"
|
|
||||||
|
|
||||||
export let person
|
export let person
|
||||||
|
|
||||||
const {petnamePubkeys} = user
|
const {petnamePubkeys} = user
|
||||||
|
|
||||||
const addPetname = pubkey => {
|
const addPetname = ({pubkey}) => {
|
||||||
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
|
const [{url}] = sampleRelays(getPubkeyWriteRelays(pubkey))
|
||||||
|
|
||||||
user.addPetname(pubkey, url, displayPerson(person))
|
user.addPetname(pubkey, url, displayPerson(person))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removePetname = ({pubkey}) => {
|
||||||
|
user.removePetname(pubkey)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<PersonInfo
|
||||||
in:fly={{y: 20}}
|
{person}
|
||||||
href={routes.person(person.pubkey)}
|
addPetname={$petnamePubkeys.includes(person.pubkey) ? addPetname : null}
|
||||||
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">
|
removePetname={$petnamePubkeys.includes(person.pubkey) ? null : removePetname} />
|
||||||
<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>
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from 'classnames'
|
import {find, propEq} from 'ramda'
|
||||||
import {last, find, propEq} from 'ramda'
|
|
||||||
import {onMount} from 'svelte'
|
import {onMount} from 'svelte'
|
||||||
import {poll, stringToColor} from "src/util/misc"
|
import {poll} from "src/util/misc"
|
||||||
import {between} from 'hurdak/lib/hurdak'
|
|
||||||
import {fly} from 'svelte/transition'
|
|
||||||
import Toggle from "src/partials/Toggle.svelte"
|
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 pool from 'src/agent/pool'
|
||||||
import user from "src/agent/user"
|
import user from "src/agent/user"
|
||||||
import {loadAppData} from 'src/app'
|
import {loadAppData} from 'src/app'
|
||||||
@ -17,17 +14,16 @@
|
|||||||
|
|
||||||
let quality = null
|
let quality = null
|
||||||
let message = null
|
let message = null
|
||||||
let showStatus = false
|
|
||||||
let joined = false
|
let joined = false
|
||||||
|
|
||||||
const {relays, canPublish} = user
|
const {relays, canPublish} = user
|
||||||
|
|
||||||
$: joined = find(propEq('url', relay.url), $relays)
|
$: joined = find(propEq('url', relay.url), $relays)
|
||||||
|
|
||||||
const removeRelay = () => user.removeRelay(relay.url)
|
const removeRelay = ({url}) => user.removeRelay(url)
|
||||||
|
|
||||||
const addRelay = async () => {
|
const addRelay = async ({url}) => {
|
||||||
await user.addRelay(relay.url)
|
await user.addRelay(url)
|
||||||
|
|
||||||
if (!user.getProfile()?.kind0) {
|
if (!user.getProfile()?.kind0) {
|
||||||
loadAppData(user.getPubkey())
|
loadAppData(user.getPubkey())
|
||||||
@ -48,61 +44,16 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<RelayCard
|
||||||
class={cx(
|
{relay} {theme}
|
||||||
`bg-${theme}`,
|
addRelay={!joined && $canPublish ? addRelay : null}
|
||||||
"rounded border border-l-2 border-solid border-medium shadow flex flex-col justify-between gap-3 py-3 px-6"
|
removeRelay={joined && $relays.length > 1 && canPublish ? removeRelay : null}>
|
||||||
)}
|
<div slot="controls" class="flex justify-between gap-2">
|
||||||
style={`border-left-color: ${stringToColor(relay.url)}`}
|
{#if showControls && $canPublish}
|
||||||
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">
|
|
||||||
<span>Publish to this relay?</span>
|
<span>Publish to this relay?</span>
|
||||||
<Toggle
|
<Toggle
|
||||||
value={relay.write}
|
value={relay.write}
|
||||||
on:change={() => user.setRelayWriteCondition(relay.url, !relay.write)} />
|
on:change={() => user.setRelayWriteCondition(relay.url, !relay.write)} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</RelayCard>
|
||||||
</div>
|
|
||||||
|
Loading…
Reference in New Issue
Block a user