Re-work login page

This commit is contained in:
Jonathan Staab 2023-01-16 10:34:18 -08:00
parent b848c92669
commit aaaef18cae
26 changed files with 285 additions and 175 deletions

View File

@ -20,6 +20,7 @@ If you like Coracle and want to support its development, you can donate sats via
- [ ] Persist and load relay list
- [ ] Add followers/follows lists on profile page
- [ ] Image uploads
- Use dufflepud. Default will charge via lightning and have a tos, others can self-host and skip that.
- [ ] Server discovery and relay publishing https://github.com/nostr-protocol/nips/pull/32/files
- [ ] Support invoices https://twitter.com/jb55/status/1604131336247476224
- [ ] NIP 05
@ -42,6 +43,9 @@ If you like Coracle and want to support its development, you can donate sats via
- [ ] Add settings storage on nostr, maybe use kind 0?
- [ ] Stack views so scroll position isn't lost on navigation
- [ ] Suggest relays based on network
- [ ] Attachments (a tag w/content type and url)
- [ ] Add Labs tab with cards for non-standard features
- Time travel - see events as of a date/time
# Changelog
@ -56,26 +60,37 @@ If you like Coracle and want to support its development, you can donate sats via
- [x] Support some read/write config
- [x] Get real home relays for defaults.petnames
- [x] Add notification for slow relays
- [ ] Fix publishing
- [ ] Relay list isn't getting refreshed since we're using getRelay everywhere
- [x] Fix publishing
- [x] Relay list isn't getting refreshed since we're using getRelay everywhere
- [x] Warn that everything will be cleared on logout
- [x] Connection management
- [x] Do I need to implement re-connecting now?
- [x] Handle failed connections
- [x] Close connections that haven't been used in a while
- [x] Add strategy that callers can opt into to accept first eose from a relay that has any events
- [ ] Login
- [ ] Prefer extension, make private key entry "advanced"
- [ ] Improve login UX for bootstrap delay. Nostr facts?
- [x] Prefer extension, make private key entry "advanced"
- [x] Buttons should redirect to login modal if using pubkey login
- [ ] We often get the root as the reply, figure out why that is, compared to astral/damus
- [ ] Load feeds from network rather than user relays?
- Still use "my" relays for global, this could make global feed more useful
- [x] Load feeds from network rather than user relays?
- [x] Still use "my" relays for global, this could make global feed more useful
- [x] If we use my relays for global, we don't have to wait for network to load initially
- [ ] Figure out fast vs complete tradeoff. Skipping loadContext speeds things up a ton
- [ ] Figure out migrations from previous version
- [ ] Add relays/mentions to note and reply composition
- [ ] Add layout component with max-w, padding, etc. Test on mobile size
- [ ] Add tips to login spinner
- [ ] Add banner
## 0.2.7
- [x]
- [x] Add support for profile banner images
- [x] Re-designed relays page
- [x] Support connection status/speed indication
- [x] Add toggle to enable writing to a connected relay
- [x] Re-designed login page
- [x] Use private key login only if extension is not enabled
- [x] Add pubkey login
## 0.2.6

View File

@ -12,13 +12,16 @@
import {hasParent} from 'src/util/html'
import {displayPerson, isLike} from 'src/util/nostr'
import {timedelta, now} from 'src/util/misc'
import {user, pool, getRelays} from 'src/agent'
import {keys, user, pool, getRelays} from 'src/agent'
import {modal, toast, settings, alerts} from "src/app"
import {routes} from "src/app/ui"
import Anchor from 'src/partials/Anchor.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import Modal from 'src/partials/Modal.svelte'
import NoteDetailModal from "src/views/NoteDetail.svelte"
import SignUp from "src/views/SignUp.svelte"
import PrivKeyLogin from "src/views/PrivKeyLogin.svelte"
import PubKeyLogin from "src/views/PubKeyLogin.svelte"
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
import NotFound from "src/routes/NotFound.svelte"
import Search from "src/routes/Search.svelte"
@ -48,11 +51,13 @@
menuIsOpen.set(false)
}
const {privkey} = keys
const {lastCheckedAlerts, mostRecentAlert} = alerts
let menuIcon
let scrollY
let suspendedSubs = []
let slowConnections = []
let {lastCheckedAlerts, mostRecentAlert} = alerts
onMount(() => {
if ($user) {
@ -166,10 +171,11 @@
</li>
{/if}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/network">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/latest">
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>
{#if $user}
<li class="h-px mx-3 my-4 bg-medium" />
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/relays">
@ -179,7 +185,6 @@
{/if}
</a>
</li>
{#if $user}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/keys">
<i class="fa-solid fa-key mr-2" /> Keys
@ -220,7 +225,7 @@
{/if}
</div>
{#if $user}
{#if $privkey}
<div class="fixed bottom-0 right-0 m-8">
<a
href="/notes/new"
@ -235,14 +240,20 @@
<Modal onEscape={closeModal}>
{#if $modal.note}
{#key $modal.note.id}
<NoteDetailModal {...$modal} />
<NoteDetail {...$modal} />
{/key}
{:else if $modal.form === 'relay'}
<AddRelay />
{:else if $modal.form === 'signUp'}
<SignUp />
{:else if $modal.form === 'privkeyLogin'}
<PrivKeyLogin />
{:else if $modal.form === 'pubkeyLogin'}
<PubKeyLogin />
{:else if $modal.form === 'person/settings'}
<PersonSettings />
{:else if $modal.message}
<p class="text-white text-center p-12">{$modal.message}</p>
<p class="text-white text-center py-12 pb-8">{$modal.message}</p>
<Spinner />
{/if}
</Modal>

View File

@ -58,7 +58,7 @@ export const processEvents = async events => {
...switcherFn(e.kind, {
0: () => JSON.parse(e.content),
2: () => {
if (e.created_at > person.updated_at) {
if (e.created_at > (person.relays_updated_at || 0)) {
return {
relays: ($people[e.pubkey]?.relays || []).concat({url: e.content}),
relays_updated_at: e.created_at,
@ -68,7 +68,7 @@ export const processEvents = async events => {
3: () => ({petnames: e.tags}),
12165: () => ({muffle: e.tags}),
10001: () => {
if (e.created_at > person.updated_at) {
if (e.created_at > (person.relays_updated_at || 0)) {
return {
relays: e.tags.map(([url, read, write]) => ({url, read, write})),
relays_updated_at: e.created_at,

View File

@ -9,9 +9,7 @@ export default {
],
relays: [
{url: 'wss://brb.io'},
{url: 'wss://relay.damus.io'},
{url: 'wss://nostr.zebedee.cloud'},
{url: 'wss://nostr-relay.wlvs.space'},
{url: 'wss://nostr-pub.wellorder.net'},
],
}

View File

@ -70,8 +70,8 @@ export const publish = async (relays, event) => {
return signedEvent
}
export const load = async (relays, filter) => {
const events = await pool.request(relays, filter)
export const load = async (relays, filter, opts) => {
const events = await pool.request(relays, filter, opts)
await processEvents(events)

View File

@ -41,4 +41,4 @@ const clear = () => {
// Init signing function by re-setting pubkey
setPublicKey(get(pubkey))
export default {pubkey, setPrivateKey, setPublicKey, sign, clear}
export default {pubkey, privkey, setPrivateKey, setPublicKey, sign, clear}

View File

@ -24,10 +24,6 @@ class Connection {
init(url) {
const nostr = relayInit(url)
nostr.on('error', () => {
console.log(`failed to connect to ${url}`)
})
nostr.on('disconnect', () => {
connections = reject(whereEq({url}), connections)
})
@ -50,7 +46,6 @@ class Connection {
await this.nostr.connect()
this.status = 'ready'
} catch (e) {
console.error(`Failed to connect to ${this.url}: ${e}`)
this.status = 'error'
}
}
@ -82,7 +77,7 @@ const connect = async url => {
const publish = async (relays, event) => {
return Promise.all(
relays.filter(r => r.write !== '!' & isRelay(r.url)).map(async relay => {
relays.filter(r => r.write !== '!' && isRelay(r.url)).map(async relay => {
const conn = await connect(relay.url)
if (conn) {
@ -172,7 +167,7 @@ const subscribe = async (relays, filters) => {
}
}
const request = (relays, filters) => {
const request = (relays, filters, {mode = "most"} = {}) => {
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
return new Promise(async resolve => {
@ -188,7 +183,12 @@ const request = (relays, filters) => {
eose.length === agg.subs.length
|| Date.now() - now >= 5000
|| (
Date.now() - now >= 1000
mode === "fast"
&& events.length
)
|| (
mode === "most"
&& Date.now() - now >= 1000
&& eose.length > agg.subs.length - Math.round(agg.subs.length / 10)
)
)

View File

@ -86,6 +86,8 @@ const publishEvent = (relays, kind, {content = '', tags = []} = {}) => {
const createdAt = Math.round(new Date().valueOf() / 1000)
const event = {kind, content, tags, pubkey, created_at: createdAt}
console.log(`publishing ${JSON.stringify(event)} to ${JSON.stringify(relays)}`)
return publish(relays, event)
}

View File

@ -1,4 +1,5 @@
import {whereEq, sortBy, identity, when, assoc, reject} from 'ramda'
import {navigate} from 'svelte-routing'
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {get} from 'svelte/store'
import {renderContent} from 'src/util/html'
@ -19,11 +20,12 @@ export const login = async ({privkey, pubkey}) => {
keys.setPublicKey(pubkey)
}
await Promise.all([
loaders.loadNetwork(getRelays(), pubkey),
alerts.load(getRelays(), pubkey),
alerts.listen(getRelays(), pubkey),
])
// Load network and start listening, but don't wait for it
loaders.loadNetwork(getRelays(), pubkey),
alerts.load(getRelays(), pubkey),
alerts.listen(getRelays(), pubkey),
navigate('/notes/latest')
}
export const addRelay = async relay => {

View File

@ -1,4 +1,4 @@
import {uniqBy, prop, uniq, flatten, pluck, groupBy, identity} from 'ramda'
import {uniqBy, prop, uniq, flatten, pluck, identity} from 'ramda'
import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak'
import {findReply, personKinds, Tags, getTagValues} from 'src/util/nostr'
import {now, timedelta} from 'src/util/misc'
@ -38,15 +38,13 @@ const loadNetwork = async (relays, pubkey) => {
petnames = defaults.petnames
}
// Get the user's follows, with a fallback if we have no pubkey, then use nip-2 recommended
// relays to load our user's second-order follows in order to bootstrap our social graph
await Promise.all(
Object.entries(groupBy(t => t[2], petnames))
.map(([relay, petnames]) => loadPeople([relay], getTagValues(petnames)))
)
const tags = Tags.wrap(petnames)
// Use nip-2 recommended relays to load our user's second-order follows
await loadPeople(tags.relays(), tags.values().all(), {mode: 'fast'})
}
const loadContext = async (relays, notes, {loadParents = true} = {}) => {
const loadContext = async (relays, notes, {loadParents = true, ...opts} = {}) => {
notes = ensurePlural(notes)
if (notes.length === 0) {
@ -68,7 +66,7 @@ const loadContext = async (relays, notes, {loadParents = true} = {}) => {
filter.push({kinds: [1], ids: getTagValues(parentTags)})
}
const events = await load(combinedRelays, filter)
const events = await load(combinedRelays, filter, opts)
if (parentTags.length === 0) {
return events
@ -82,7 +80,7 @@ const loadContext = async (relays, notes, {loadParents = true} = {}) => {
return uniqBy(
prop('id'),
events.concat(
await loadContext(parentRelays, parents, {loadParents: false})
await loadContext(parentRelays, parents, {loadParents: false, ...opts})
)
)
})

View File

@ -16,6 +16,7 @@
switcher(type, {
anchor: "underline",
button: "py-2 px-4 rounded bg-white text-accent",
'button-accent': "py-2 px-4 rounded bg-accent text-white",
}),
)
</script>

View File

@ -10,11 +10,21 @@
e.preventDefault()
url = url.trim()
if (!url.match(/^wss?:\/\/[\w.:-]+$/)) {
return toast.show("error", 'That isn\'t a valid websocket url - relay urls should start with "wss://"')
if (!url.includes('://')) {
url = 'wss://' + url
}
addRelay(url)
try {
new URL(url)
} catch (e) {
return toast.show("error", "That isn't a valid url")
}
if (!url.match('^wss?://')) {
return toast.show("error", "That isn't a valid websocket url")
}
addRelay({url, write: '!'})
modal.set(null)
}
</script>

View File

@ -6,24 +6,25 @@
import {copyToClipboard} from "src/util/html"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {user} from "src/agent"
import {keys} from "src/agent"
import {toast} from "src/app"
const {pubkey, privkey} = keys
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/'
const delegationUrl = 'https://github.com/nostr-protocol/nips/blob/b62aa418dee13aac1899ea7c6946a0f55dd7ee84/26.md'
const copyKey = type => {
copyToClipboard(
type === 'private'
? nip19.nsecEncode($user.privkey)
: nip19.npubEncode($user.pubkey)
? nip19.nsecEncode($privkey)
: nip19.npubEncode($pubkey)
)
toast.show("info", `Your ${type} key has been copied to the clipboard.`)
}
onMount(async () => {
if (!$user) {
if (!$pubkey) {
return navigate("/login")
}
})
@ -42,7 +43,7 @@
<div class="flex flex-col gap-8 w-full">
<div class="flex flex-col gap-1">
<strong>Public Key</strong>
<Input disabled value={$user ? nip19.npubEncode($user.pubkey) : ''}>
<Input disabled value={$pubkey ? nip19.npubEncode($pubkey) : ''}>
<i slot="after" class="fa-solid fa-copy cursor-pointer" on:click={() => copyKey('public')} />
</Input>
<p class="text-sm text-light">
@ -50,17 +51,18 @@
trying to find you on nostr.
</p>
</div>
{#if $user?.privkey}
{#if $privkey}
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input disabled type="password" value={nip19.nsecEncode($user.privkey)}>
<Input disabled type="password" value={nip19.nsecEncode($privkey)}>
<i slot="after" class="fa-solid fa-copy cursor-pointer" on:click={() => copyKey('private')} />
</Input>
<p class="text-sm text-light">
Your private key is used to prove your identity by cryptographically signing
messages. <strong>Do not share this with anyone.</strong> Be careful about
copying this into other apps - instead, consider
using <Anchor external href={delegationUrl}>delegation keys</Anchor> instead.
copying this into other apps - instead, consider using
a <Anchor href={nip07} external>compatible browser extension</Anchor> to securely
store your key.
</p>
</div>
{/if}

View File

@ -1,103 +1,46 @@
<script>
import {onMount} from 'svelte'
import {fade, fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {generatePrivateKey, getPublicKey} from 'nostr-tools'
import {nip19} from 'nostr-tools'
import {copyToClipboard} from "src/util/html"
import {fly} from 'svelte/transition'
import Anchor from "src/partials/Anchor.svelte"
import Input from "src/partials/Input.svelte"
import {toast, login} from "src/app"
let nsec = ''
let hasExtension = false
let loading = false
onMount(() => {
setTimeout(() => {
hasExtension = !!window.nostr
}, 1000)
})
import {modal, login} from "src/app"
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.")
}
const generateKey = () => {
nsec = nip19.nsecEncode(generatePrivateKey())
toast.show("info", "Your private key has been re-generated.")
}
const logIn = async ({privkey, pubkey}) => {
loading = true
await login({privkey, pubkey})
navigate('/relays')
}
const logInWithExtension = async () => {
const pubkey = await window.nostr.getPublicKey()
if (!pubkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid public key.")
const autoLogIn = async () => {
if (window.nostr) {
await login({pubkey: await window.nostr.getPublicKey()})
} else {
await logIn({pubkey})
modal.set({form: 'privkeyLogin'})
}
}
const logInWithPrivateKey = async () => {
const privkey = nsec.startsWith('nsec') ? nip19.decode(nsec).data : nsec
const signUp = () => {
modal.set({form: 'signUp'})
}
if (!privkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid private key.")
} else {
await logIn({privkey, pubkey: getPublicKey(privkey)})
}
const pubkeyLogIn = () => {
modal.set({form: 'pubkeyLogin'})
}
</script>
<div class="flex justify-center p-12" in:fly={{y: 20}}>
<div class="flex flex-col gap-4 max-w-2xl">
<div class="m-auto max-w-2xl p-12" in:fly={{y: 20}}>
<div class="flex flex-col gap-8 max-w-2xl">
<div class="flex justify-center items-center flex-col mb-4">
<h1 class="staatliches text-6xl">Welcome!</h1>
<i>To the Nostr Protocol</i>
</div>
<div class="flex flex-col gap-4">
<p>
To log in to existing account, simply enter your private key below. To create a new account,
just let us generate one for you.
</p>
<p>
You can also use a <Anchor href={nip07} external>compatible browser extension</Anchor> to
sign events without having to paste your private key here (recommended).
</p>
<div class="flex flex-col gap-1">
<strong>Private Key</strong>
<Input type="password" bind:value={nsec} placeholder="Enter your private key">
<i slot="after" class="fa-solid fa-copy" on:click={copyKey} />
</Input>
<div class="flex justify-end gap-2">
{#if hasExtension}
<div in:fade>
<Anchor on:click={logInWithExtension} class="text-sm">Get from extension</Anchor>
</div>
{/if}
<div>
<Anchor on:click={generateKey} class="text-sm">Generate new key</Anchor>
</div>
</div>
<small>
Your private key is a string of random letters and numbers that allow you to prove
you own your account. Write it down and keep it secret!
</small>
<p class="text-center">
To log in to existing account, simply click below. If you have
a <Anchor href={nip07} external>compatible browser extension</Anchor> installed,
we will use that.
</p>
<div class="flex flex-col gap-4 items-center">
<div class="flex gap-4">
<Anchor class="w-32 text-center" type="button-accent" on:click={autoLogIn}>Log In</Anchor>
<Anchor class="w-32 text-center" type="button" on:click={signUp}>Sign Up</Anchor>
</div>
<Anchor type="unstyled" on:click={pubkeyLogIn}>
<i class="fa fa-cogs" /> Advanced Login
</Anchor>
</div>
<Anchor class="text-center" type="button" on:click={logInWithPrivateKey} {loading}>
Log In
</Anchor>
</div>
</div>

View File

@ -19,11 +19,11 @@
}
</script>
<div class="max-w-xl m-auto text-center py-20" in:fly={{y:20}}>
<div class="max-w-xl m-auto text-center py-20">
{#if confirmed}
<div>Clearing your local database...</div>
<div in:fly={{y:20}}>Clearing your local database...</div>
{:else}
<div class="flex flex-col gap-8 items-center">
<div class="flex flex-col gap-8 items-center" in:fly={{y:20}}>
<div>Are you sure you want to log out? All data will be cleared.</div>
<Anchor type="button" on:click={confirm}>Log out</Anchor>
</div>

View File

@ -2,5 +2,5 @@
import {onMount} from 'svelte'
import {navigate} from 'svelte-routing'
onMount(() => navigate('/notes/network'))
onMount(() => navigate('/notes/latest'))
</script>

View File

@ -3,7 +3,7 @@
import Anchor from "src/partials/Anchor.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Network from "src/views/notes/Network.svelte"
import Global from "src/views/notes/Global.svelte"
import Latest from "src/views/notes/Latest.svelte"
import {user} from 'src/agent'
export let activeTab
@ -19,10 +19,10 @@
</div>
{/if}
<Tabs tabs={['network', 'global']} {activeTab} {setActiveTab} />
<Tabs tabs={['latest', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'network'}
<Network />
{:else}
<Global />
<Latest />
{/if}

View File

@ -9,10 +9,11 @@
import {displayPerson} from 'src/util/nostr'
import Tabs from "src/partials/Tabs.svelte"
import Button from "src/partials/Button.svelte"
import Spinner from "src/partials/Spinner.svelte"
import Notes from "src/views/person/Notes.svelte"
import Likes from "src/views/person/Likes.svelte"
import Network from "src/views/person/Network.svelte"
import {getPerson, getRelays, listen, user} from "src/agent"
import {getPerson, getRelays, listen, user, keys} from "src/agent"
import {modal} from "src/app"
import loaders from "src/app/loaders"
import {routes} from "src/app/ui"
@ -22,17 +23,23 @@
export let activeTab
export let relays = null
const {privkey} = keys
let subs = []
let pubkey = nip19.decode(npub).data
let following = find(t => t[1] === pubkey, $user?.petnames || [])
let following = false
let followers = new Set()
let followersCount = 0
let person = getPerson(pubkey, true)
let loading = true
$: following = find(t => t[1] === pubkey, $user?.petnames || [])
onMount(async () => {
// Refresh our person if needed
loaders.loadPeople(relays || getRelays(pubkey), [pubkey]).then(() => {
person = getPerson(pubkey, true)
loading = false
})
// Get our followers count
@ -56,18 +63,14 @@
const setActiveTab = tab => navigate(routes.person(pubkey, tab))
const follow = async () => {
following = true
const relay = first(relays || getRelays(pubkey))
const tag = ["p", pubkey, relay.url, person.name || ""]
const petnames = reject(t => t[1] === pubkey, $user.petnames).concat(tag)
const petnames = reject(t => t[1] === pubkey, $user.petnames).concat([tag])
cmd.setPetnames(getRelays(), petnames)
}
const unfollow = async () => {
following = false
const petnames = reject(t => t[1] === pubkey, $user.petnames)
cmd.setPetnames(getRelays(), petnames)
@ -78,6 +81,13 @@
}
</script>
<div
class="absolute w-full h-64"
style="z-index: -1;
background-size: cover;
background-image:
linear-gradient(to bottom, rgba(0, 0, 0, 0.3), #0f0f0e),
url('{person.banner}')" />
<div class="max-w-xl m-auto flex flex-col gap-4 py-8 px-4">
<div class="flex flex-col gap-4" in:fly={{y: 20}}>
<div class="flex gap-4">
@ -94,11 +104,11 @@
<p>{@html renderContent(person.about || '')}</p>
</div>
<div class="whitespace-nowrap">
{#if $user?.pubkey === pubkey}
{#if $user?.pubkey === pubkey && $privkey}
<a href="/profile" class="cursor-pointer text-sm">
<i class="fa-solid fa-edit" /> Edit
</a>
{:else if $user?.petnames}
{:else if $user?.petnames && $privkey}
<div class="flex flex-col items-end gap-2">
{#if following}
<Button on:click={unfollow}>Unfollow</Button>
@ -126,6 +136,8 @@
{:else if activeTab === 'network'}
{#if person?.petnames}
<Network person={person} />
{:else if loading}
<Spinner />
{:else}
<div class="py-16 max-w-xl m-auto flex justify-center">
Unable to show network for this person.

View File

@ -1,6 +1,6 @@
<script>
import {liveQuery} from 'dexie'
import {whereEq, find, last, reject} from 'ramda'
import {whereEq, find, last} from 'ramda'
import {noop} from 'hurdak/lib/hurdak'
import {onMount} from 'svelte'
import {get} from 'svelte/store'
@ -9,7 +9,7 @@
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Toggle from "src/partials/Toggle.svelte"
import {pool, db, getRelays, ready} from "src/agent"
import {pool, db, user, ready} from "src/agent"
import {modal, addRelay, removeRelay, setRelayWriteCondition, settings} from "src/app"
import defaults from "src/agent/defaults"
@ -49,7 +49,7 @@
return poll(300, () => {
if ($ready) {
relays = getRelays()
relays = $user?.relays || []
}
status = Object.fromEntries(
@ -80,6 +80,9 @@
Relays are hubs for your content and connections. At least one is required to
interact with the network, but you can join as many as you like.
</p>
{#if relays.length === 0}
<div class="text-center">No relays connected</div>
{/if}
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
{#each relays as {url, write}, i (url)}
<div class="rounded border border-solid border-medium bg-dark shadow" in:fly={{y: 20, delay: i * 100}}>

View File

@ -21,8 +21,6 @@
settings.set(values)
navigate('/notes/network')
toast.show("info", "Your settings have been saved!")
}
</script>

View File

@ -27,19 +27,13 @@ export class Tags {
return uniq(flatten(this.tags).filter(isRelay)).map(objOf('url'))
}
values() {
this.tags = this.tags.map(t => t[1])
return this
return new Tags(this.tags.map(t => t[1]))
}
type(type) {
this.tags = this.tags.filter(t => t[0] === type)
return this
return new Tags(this.tags.filter(t => t[0] === type))
}
mark(mark) {
this.tags = this.tags.filter(t => last(t) === mark)
return this
return new Tags(this.tags.filter(t => last(t) === mark))
}
}

View File

@ -0,0 +1,40 @@
<script>
import {nip19} from 'nostr-tools'
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {toast, login} from "src/app"
let nsec = ''
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const logIn = async () => {
const privkey = nsec.startsWith('nsec') ? nip19.decode(nsec).data : nsec
if (!privkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid private key.")
} else {
await login({privkey})
}
}
</script>
<div class="flex flex-col gap-8 text-white p-12">
<h1 class="staatliches text-4xl text-center">Login with your Private Key</h1>
<p>
To give Coracle full access to your nostr identity, enter your private key below.
</p>
<div class="flex gap-2">
<div class="flex-grow">
<Input bind:value={nsec} placeholder="nsec...">
<i slot="before" class="fa fa-key" />
</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>
</div>

View File

@ -0,0 +1,36 @@
<script>
import {nip19} from 'nostr-tools'
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {toast, login} from "src/app"
let npub = ''
const logIn = async () => {
const pubkey = npub.startsWith('npub') ? nip19.decode(npub).data : npub
if (!pubkey.match(/[a-z0-9]{64}/)) {
toast.show("error", "Sorry, but that's an invalid public key.")
} else {
await login({pubkey})
}
}
</script>
<div class="flex flex-col gap-8 text-white p-12">
<h1 class="staatliches text-4xl text-center">Login with your Public Key</h1>
<p>
For read-only access, enter your public key (or someone else's) below. Your
key should start with "npub".
</p>
<div class="flex gap-2">
<div class="flex-grow">
<Input bind:value={npub} placeholder="npub...">
<i slot="before" class="fa fa-key" />
</Input>
</div>
<Anchor type="button" on:click={logIn}>Log In</Anchor>
</div>
</div>

42
src/views/SignUp.svelte Normal file
View File

@ -0,0 +1,42 @@
<script>
import {nip19, generatePrivateKey} from 'nostr-tools'
import {copyToClipboard} from "src/util/html"
import Input from 'src/partials/Input.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {toast, login} from "src/app"
const nsec = nip19.nsecEncode(generatePrivateKey())
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
const logIn = async () => {
await login({privkey: nip19.decode(nsec).data})
}
const copyKey = () => {
copyToClipboard(nsec)
toast.show("info", "Your private key has been copied to the clipboard.")
}
</script>
<div class="flex flex-col gap-8 text-white p-12">
<h1 class="staatliches text-4xl text-center">Create an Account</h1>
<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" />
<i 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>
</div>

View File

@ -18,9 +18,11 @@
const loadNotes = async () => {
const {limit, until} = cursor
const notes = await load(relays, {...filter, limit, until})
const notes = await load(relays, {...filter, limit, until}, {mode: 'fast'})
const context = await loaders.loadContext(relays, notes)
cursor.onChunk(notes)
return threadify(notes, context, {muffle: getMuffle()})
}
</script>

View File

@ -1,4 +1,5 @@
<script>
import {uniqBy, prop} from 'ramda'
import Notes from "src/partials/Notes.svelte"
import {now, Cursor, shuffle, batch} from 'src/util/misc'
import {user, getRelays, getFollows, getMuffle, listen, load} from 'src/agent'
@ -7,12 +8,12 @@
// Get first- and second-order follows. shuffle and slice network so we're not
// sending too many pubkeys. This will also result in some variety.
const relays = getRelays()
const follows = getFollows($user?.pubkey)
const follows = shuffle(getFollows($user?.pubkey))
const network = shuffle(follows.flatMap(getFollows)).slice(0, 50)
const authors = follows.concat(network)
const authors = follows.concat(network).slice(0, 100)
const filter = {kinds: [1, 7], authors}
const cursor = new Cursor()
const relays = uniqBy(prop('url'), follows.flatMap(getRelays))
const listenForNotes = onNotes =>
listen(relays, {...filter, since: now()}, batch(300, async notes => {
@ -23,7 +24,7 @@
const loadNotes = async () => {
const {limit, until} = cursor
const notes = await load(relays, {...filter, limit, until})
const notes = await load(relays, {...filter, limit, until}, {mode: 'fast'})
const context = await loaders.loadContext(relays, notes)
cursor.onChunk(notes)