mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Re-work login page
This commit is contained in:
parent
b848c92669
commit
aaaef18cae
29
README.md
29
README.md
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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'},
|
||||
],
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
)
|
||||
)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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})
|
||||
)
|
||||
)
|
||||
})
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -2,5 +2,5 @@
|
||||
import {onMount} from 'svelte'
|
||||
import {navigate} from 'svelte-routing'
|
||||
|
||||
onMount(() => navigate('/notes/network'))
|
||||
onMount(() => navigate('/notes/latest'))
|
||||
</script>
|
||||
|
@ -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}
|
||||
|
@ -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.
|
||||
|
@ -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}}>
|
||||
|
@ -21,8 +21,6 @@
|
||||
|
||||
settings.set(values)
|
||||
|
||||
navigate('/notes/network')
|
||||
|
||||
toast.show("info", "Your settings have been saved!")
|
||||
}
|
||||
</script>
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
40
src/views/PrivKeyLogin.svelte
Normal file
40
src/views/PrivKeyLogin.svelte
Normal 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>
|
||||
|
36
src/views/PubKeyLogin.svelte
Normal file
36
src/views/PubKeyLogin.svelte
Normal 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
42
src/views/SignUp.svelte
Normal 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>
|
||||
|
@ -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>
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user