mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-19 11:43:35 +00:00
Allow users to set max concurrent relays
This commit is contained in:
parent
bab545f789
commit
5cbd59d99a
@ -16,6 +16,7 @@
|
||||
- [x] Improve anonymous and new user experience by prompting for relays and follows
|
||||
- [x] Fixed kind0 merging to avoid dropping properties coracle doesn't support
|
||||
- [x] Implement NIP-65 properly
|
||||
- [x] Allow users to set max number of concurrent relays
|
||||
|
||||
## 0.2.11
|
||||
|
||||
|
@ -1,15 +1,10 @@
|
||||
# Current
|
||||
|
||||
- [ ] Fix anon/new user experience
|
||||
- [ ] When logging in rather than generating a new keypair, ask for a relay to bootstrap from
|
||||
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
|
||||
- [ ] Test publishing events with zero relays
|
||||
- [ ] Try lumping tables into a single key each to reduce load/save contention and time
|
||||
- [ ] Fix turning off link previews, or make sure it applies to images/videos too
|
||||
- [ ] Allow user to set sample size to manage bandwidth/performance tradeoff
|
||||
|
||||
# Snacks
|
||||
|
||||
- [ ] If a user has no write relays (or is not logged in), open a modal
|
||||
- [ ] open web+nostr links like snort
|
||||
- [ ] DM/chat read status in encrypted note
|
||||
- [ ] Relay recommendations based on follows/followers
|
||||
|
@ -24,7 +24,7 @@
|
||||
import {loadAppData} from "src/app"
|
||||
import alerts from "src/app/alerts"
|
||||
import messages from "src/app/messages"
|
||||
import {modal, toast, settings, routes, menuIsOpen, logUsage} from "src/app/ui"
|
||||
import {modal, toast, routes, menuIsOpen, logUsage} from "src/app/ui"
|
||||
import RelayCard from "src/partials/RelayCard.svelte"
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
@ -119,7 +119,7 @@
|
||||
|
||||
const interval = setInterval(
|
||||
async () => {
|
||||
const {dufflepudUrl} = $settings
|
||||
const {dufflepudUrl} = user.getSettings()
|
||||
|
||||
if (!dufflepudUrl) {
|
||||
return
|
||||
|
@ -4,7 +4,7 @@ import {chunk} from 'hurdak/lib/hurdak'
|
||||
import {batch, timedelta, now} from 'src/util/misc'
|
||||
import {
|
||||
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
|
||||
getUserReadRelays, getRelaysForEventChildren, getUserRelays
|
||||
getRelaysForEventChildren, sampleRelays,
|
||||
} from 'src/agent/relays'
|
||||
import database from 'src/agent/database'
|
||||
import pool from 'src/agent/pool'
|
||||
@ -32,11 +32,7 @@ const publish = async (relays, event) => {
|
||||
}
|
||||
|
||||
const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> => {
|
||||
if (relays.length === 0) {
|
||||
relays = getUserReadRelays()
|
||||
}
|
||||
|
||||
const events = await pool.request(relays, filter, opts)
|
||||
const events = await pool.request(sampleRelays(relays), filter, opts)
|
||||
|
||||
await sync.processEvents(events)
|
||||
|
||||
@ -44,11 +40,7 @@ const load = async (relays, filter, opts?): Promise<Record<string, unknown>[]> =
|
||||
}
|
||||
|
||||
const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
|
||||
if (relays.length === 0) {
|
||||
relays = getUserReadRelays()
|
||||
}
|
||||
|
||||
return pool.subscribe(relays, filter, {
|
||||
return pool.subscribe(sampleRelays(relays), filter, {
|
||||
onEvent: batch(300, events => {
|
||||
if (shouldProcess) {
|
||||
sync.processEvents(events)
|
||||
@ -62,12 +54,8 @@ const listen = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
|
||||
}
|
||||
|
||||
const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any = {}) => {
|
||||
if (relays.length === 0) {
|
||||
relays = getUserReadRelays()
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
pool.subscribeUntilEose(relays, filter, {
|
||||
pool.subscribeUntilEose(sampleRelays(relays), filter, {
|
||||
onClose: () => resolve(),
|
||||
onEvent: batch(300, events => {
|
||||
if (shouldProcess) {
|
||||
@ -92,25 +80,22 @@ const loadPeople = async (pubkeys, {relays = null, kinds = personKinds, force =
|
||||
|
||||
await Promise.all(
|
||||
chunk(256, pubkeys).map(async chunk => {
|
||||
// Use the best relays we have, but fall back to user relays
|
||||
const chunkRelays = relays || (
|
||||
getAllPubkeyWriteRelays(chunk)
|
||||
.concat(getUserReadRelays())
|
||||
.slice(0, 10)
|
||||
await load(
|
||||
sampleRelays(relays || getAllPubkeyWriteRelays(chunk), 0.5),
|
||||
{kinds, authors: chunk},
|
||||
opts
|
||||
)
|
||||
|
||||
await load(chunkRelays, {kinds, authors: chunk}, opts)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const loadParents = notes => {
|
||||
const notesWithParent = notes.filter(findReplyId)
|
||||
const relays = aggregateScores(notesWithParent.map(getRelaysForEventParent))
|
||||
.concat(getUserRelays())
|
||||
.slice(0, 10)
|
||||
|
||||
return load(relays, {kinds: [1], ids: notesWithParent.map(findReplyId)})
|
||||
return load(
|
||||
sampleRelays(aggregateScores(notesWithParent.map(getRelaysForEventParent)), 0.3),
|
||||
{kinds: [1], ids: notesWithParent.map(findReplyId)}
|
||||
)
|
||||
}
|
||||
|
||||
const streamContext = ({notes, updateNotes, depth = 0}) => {
|
||||
@ -118,9 +103,7 @@ const streamContext = ({notes, updateNotes, depth = 0}) => {
|
||||
chunk(256, notes).forEach(chunk => {
|
||||
const authors = getStalePubkeys(pluck('pubkey', chunk))
|
||||
const filter = [{kinds: [1, 7], '#e': pluck('id', chunk)}] as Array<object>
|
||||
const relays = aggregateScores(chunk.map(getRelaysForEventChildren))
|
||||
.concat(getUserRelays())
|
||||
.slice(0, 10)
|
||||
const relays = sampleRelays(aggregateScores(chunk.map(getRelaysForEventChildren)))
|
||||
|
||||
if (authors.length > 0) {
|
||||
filter.push({kinds: personKinds, authors})
|
||||
|
@ -125,7 +125,27 @@ export const uniqByUrl = uniqBy(prop('url'))
|
||||
|
||||
export const sortByScore = sortBy(r => -r.score)
|
||||
|
||||
export const sampleRelays = relays => shuffle(relays).slice(0, 30)
|
||||
export const sampleRelays = (relays, scale = 1) => {
|
||||
let limit = user.getSetting('relayLimit')
|
||||
|
||||
// Allow the caller to scale down how many relays we're bothering depending on
|
||||
// the use case, but only if we have enough relays to handle it
|
||||
if (limit > 10) {
|
||||
limit *= scale
|
||||
}
|
||||
|
||||
// Shuffle and limit target relays
|
||||
relays = shuffle(relays).slice(0, limit)
|
||||
|
||||
// If we're still under the limit, add user relays for good measure
|
||||
if (relays.length < limit) {
|
||||
relays = relays.concat(
|
||||
shuffle(getUserReadRelays()).slice(0, limit - relays.length)
|
||||
)
|
||||
}
|
||||
|
||||
return relays
|
||||
}
|
||||
|
||||
export const aggregateScores = relayGroups => {
|
||||
const scores = {} as Record<string, {
|
||||
|
@ -180,7 +180,6 @@ const getWeight = type => {
|
||||
if (type === 'kind:3') return 0.8
|
||||
if (type === 'kind:2') return 0.5
|
||||
if (type === 'seen') return 0.2
|
||||
if (type === 'tag') return 0.1
|
||||
}
|
||||
|
||||
const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
|
||||
@ -268,19 +267,6 @@ const processRoutes = async events => {
|
||||
},
|
||||
default: noop,
|
||||
})
|
||||
|
||||
// Add tag hints
|
||||
events.forEach(e => {
|
||||
Tags.wrap(e.tags).type("p").all().forEach(([_, pubkey, url]) => {
|
||||
updates.push(
|
||||
calculateRoute(pubkey, url, 'tag', 'write', e.created_at)
|
||||
)
|
||||
|
||||
updates.push(
|
||||
calculateRoute(pubkey, url, 'tag', 'read', e.created_at)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updates = updates.filter(identity)
|
||||
|
@ -14,6 +14,7 @@ import cmd from 'src/agent/cmd'
|
||||
// sync this stuff to regular private variables so we don't have to constantly call
|
||||
// `get` on our stores.
|
||||
|
||||
let settingsCopy = null
|
||||
let profileCopy = null
|
||||
let petnamesCopy = []
|
||||
let relaysCopy = []
|
||||
@ -21,6 +22,13 @@ let relaysCopy = []
|
||||
const anonPetnames = synced('agent/user/anonPetnames', [])
|
||||
const anonRelays = synced('agent/user/anonRelays', [])
|
||||
|
||||
const settings = synced("agent/user/settings", {
|
||||
relayLimit: 20,
|
||||
showMedia: true,
|
||||
reportAnalytics: true,
|
||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||
})
|
||||
|
||||
const profile = derived(
|
||||
[keys.pubkey, database.people as Readable<any>],
|
||||
([pubkey, $people]) => {
|
||||
@ -46,6 +54,10 @@ const relays = derived(
|
||||
|
||||
// Keep our copies up to date
|
||||
|
||||
settings.subscribe($settings => {
|
||||
settingsCopy = $settings
|
||||
})
|
||||
|
||||
profile.subscribe($profile => {
|
||||
profileCopy = $profile
|
||||
})
|
||||
@ -59,6 +71,13 @@ relays.subscribe($relays => {
|
||||
})
|
||||
|
||||
const user = {
|
||||
// Settings
|
||||
|
||||
settings,
|
||||
getSettings: () => settingsCopy,
|
||||
getSetting: k => settingsCopy[k],
|
||||
dufflepud: path => `${settingsCopy.dufflepudUrl}${path}`,
|
||||
|
||||
// Profile
|
||||
|
||||
profile,
|
||||
@ -102,7 +121,7 @@ const user = {
|
||||
}
|
||||
},
|
||||
async addRelay(url) {
|
||||
this.updateRelays($relays => $relays.concat({url, write: false, read: true}))
|
||||
this.updateRelays($relays => $relays.concat({url, write: true, read: true}))
|
||||
},
|
||||
async removeRelay(url) {
|
||||
this.updateRelays(reject(whereEq({url})))
|
||||
|
@ -1,6 +1,5 @@
|
||||
import {pluck} from 'ramda'
|
||||
import {first} from 'hurdak/lib/hurdak'
|
||||
import {log} from 'src/util/logger'
|
||||
import {writable} from 'svelte/store'
|
||||
import pool from 'src/agent/pool'
|
||||
import {getUserRelays} from 'src/agent/relays'
|
||||
@ -9,23 +8,16 @@ export const slowConnections = writable([])
|
||||
|
||||
setInterval(() => {
|
||||
// Only notify about relays the user is actually subscribed to
|
||||
const relayUrls = pluck('url', getUserRelays())
|
||||
const relayUrls = new Set(pluck('url', getUserRelays()))
|
||||
|
||||
// Prune connections we haven't used in a while
|
||||
pool.getConnections()
|
||||
.filter(conn => conn.lastRequest < Date.now() - 60_000)
|
||||
.forEach(conn => conn.disconnect())
|
||||
|
||||
// Log stats for debugging purposes
|
||||
log(
|
||||
'Connection stats',
|
||||
pool.getConnections()
|
||||
.map(c => `${c.nostr.url} ${c.getQuality().join(' ')}`)
|
||||
)
|
||||
|
||||
// Alert the user to any heinously slow connections
|
||||
slowConnections.set(
|
||||
pool.getConnections()
|
||||
.filter(c => relayUrls.includes(c.nostr.url) && first(c.getQuality()) < 0.3)
|
||||
.filter(c => relayUrls.has(c.nostr.url) && first(c.getQuality()) < 0.3)
|
||||
)
|
||||
}, 30_000)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import type {DisplayEvent} from 'src/util/types'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {omit, sortBy, identity} from 'ramda'
|
||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent} from 'src/util/html'
|
||||
@ -23,7 +24,7 @@ export const loadAppData = async pubkey => {
|
||||
}
|
||||
}
|
||||
|
||||
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
|
||||
export const login = ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
|
||||
if (privkey) {
|
||||
keys.setPrivateKey(privkey)
|
||||
} else {
|
||||
@ -33,6 +34,12 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
|
||||
modal.set({type: 'login/connect', noEscape: true})
|
||||
}
|
||||
|
||||
export const signup = privkey => {
|
||||
keys.setPrivateKey(privkey)
|
||||
|
||||
navigate('/notes/network')
|
||||
}
|
||||
|
||||
export const renderNote = (note, {showEntire = false}) => {
|
||||
const shouldEllipsize = note.content.length > 500 && !showEntire
|
||||
const peopleByPubkey = createMap(
|
||||
|
@ -6,8 +6,9 @@ import {navigate} from "svelte-routing"
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {writable, get} from "svelte/store"
|
||||
import {globalHistory} from "svelte-routing/src/history"
|
||||
import {synced, sleep} from "src/util/misc"
|
||||
import {sleep} from "src/util/misc"
|
||||
import {warn} from 'src/util/logger'
|
||||
import user from 'src/agent/user'
|
||||
|
||||
// Routing
|
||||
|
||||
@ -74,15 +75,6 @@ export const modal = {
|
||||
},
|
||||
}
|
||||
|
||||
// Settings, alerts, etc
|
||||
|
||||
export const settings = synced("coracle/settings", {
|
||||
reportAnalytics: true,
|
||||
showLinkPreviews: true,
|
||||
relayLimit: 30,
|
||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||
})
|
||||
|
||||
// Wait for bugsnag to be started in main
|
||||
setTimeout(() => {
|
||||
Bugsnag.addOnError(event => {
|
||||
@ -90,7 +82,7 @@ setTimeout(() => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!get(settings).reportAnalytics) {
|
||||
if (!user.getSetting('reportAnalytics')) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -103,7 +95,7 @@ setTimeout(() => {
|
||||
const session = Math.random().toString().slice(2)
|
||||
|
||||
export const logUsage = async name => {
|
||||
const {dufflepudUrl, reportAnalytics} = get(settings)
|
||||
const {dufflepudUrl, reportAnalytics} = user.getSettings()
|
||||
|
||||
if (reportAnalytics) {
|
||||
try {
|
||||
|
@ -12,7 +12,7 @@
|
||||
import ImageCircle from 'src/partials/ImageCircle.svelte'
|
||||
import Preview from 'src/partials/Preview.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {toast, settings, modal} from "src/app/ui"
|
||||
import {toast, modal} from "src/app/ui"
|
||||
import {renderNote} from "src/app"
|
||||
import {formatTimestamp, stringToColor} from 'src/util/misc'
|
||||
import Compose from "src/partials/Compose.svelte"
|
||||
@ -37,7 +37,7 @@
|
||||
let replyContainer = null
|
||||
|
||||
const {profile} = user
|
||||
const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : []
|
||||
const links = extractUrls(note.content)
|
||||
const showEntire = anchorId === note.id
|
||||
const interactive = !anchorId || !showEntire
|
||||
const person = database.watch('people', () => database.getPersonWithFallback(note.pubkey))
|
||||
@ -247,11 +247,11 @@
|
||||
{:else}
|
||||
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
|
||||
<p>{@html renderNote(note, {showEntire})}</p>
|
||||
{#each links.slice(-2) as link}
|
||||
{#if user.getSetting('showMedia') && links.length > 0}
|
||||
<button class="inline-block" on:click={e => e.stopPropagation()}>
|
||||
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
|
||||
<Preview url={last(links)} />
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-between text-light" on:click={e => e.stopPropagation()}>
|
||||
<div class="flex gap-6">
|
||||
|
@ -10,7 +10,6 @@
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import user from 'src/agent/user'
|
||||
import network from 'src/agent/network'
|
||||
import {getUserReadRelays, uniqByUrl} from 'src/agent/relays'
|
||||
import {modal} from "src/app/ui"
|
||||
import {mergeParents} from "src/app"
|
||||
|
||||
@ -22,18 +21,13 @@
|
||||
let notesBuffer = []
|
||||
|
||||
const since = now()
|
||||
const maxNotes = 300
|
||||
const maxNotes = 100
|
||||
const cursor = new Cursor()
|
||||
const {profile} = user
|
||||
const muffle = Tags
|
||||
.wrap(($profile?.muffle || []).filter(t => Math.random() > parseFloat(last(t))))
|
||||
.values().all()
|
||||
|
||||
// Sample relays in case we have a whole ton of them. Add in user relays in
|
||||
// case we don't have any
|
||||
const sampleRelays = () =>
|
||||
uniqByUrl(relays.concat(getUserReadRelays())).slice(0, 30)
|
||||
|
||||
const processNewNotes = async newNotes => {
|
||||
// Remove people we're not interested in hearing about, sort by created date
|
||||
newNotes = newNotes.filter(e => !muffle.includes(e.pubkey))
|
||||
@ -82,7 +76,7 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const sub = network.listen(sampleRelays(), {...filter, since}, onChunk)
|
||||
const sub = network.listen(relays, {...filter, since}, onChunk)
|
||||
|
||||
const scroller = createScroller(() => {
|
||||
if ($modal) {
|
||||
@ -91,7 +85,7 @@
|
||||
|
||||
const {limit, until} = cursor
|
||||
|
||||
return network.listenUntilEose(sampleRelays(), {...filter, until, limit}, onChunk)
|
||||
return network.listenUntilEose(relays, {...filter, until, limit}, onChunk)
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
@ -2,9 +2,9 @@
|
||||
import {onMount} from 'svelte'
|
||||
import {slide} from 'svelte/transition'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import user from 'src/agent/user'
|
||||
|
||||
export let url
|
||||
export let endpoint
|
||||
|
||||
let preview
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
preview = {video: url}
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
const res = await fetch(user.dufflepud('/link/preview'), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({url}),
|
||||
headers: {
|
||||
|
@ -64,11 +64,13 @@
|
||||
</p>
|
||||
</div>
|
||||
{#if joined}
|
||||
{#if $relays.length > 1}
|
||||
<button
|
||||
class="flex gap-3 items-center text-light"
|
||||
on:click={() => user.removeRelay(relay.url)}>
|
||||
<i class="fa fa-right-from-bracket" /> Leave
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<button
|
||||
class="flex gap-3 items-center text-light"
|
||||
|
@ -12,7 +12,7 @@
|
||||
const {nostr} = window as any
|
||||
|
||||
if (nostr) {
|
||||
await login({pubkey: await nostr.getPublicKey()})
|
||||
login({pubkey: await nostr.getPublicKey()})
|
||||
} else {
|
||||
modal.set({type: 'login/privkey'})
|
||||
}
|
||||
|
@ -8,9 +8,9 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import user from 'src/agent/user'
|
||||
import {toast, settings} from "src/app/ui"
|
||||
import {toast} from "src/app/ui"
|
||||
|
||||
let values = {...$settings}
|
||||
let values = {...user.getSettings()}
|
||||
|
||||
onMount(async () => {
|
||||
if (!user.getProfile()) {
|
||||
@ -21,7 +21,7 @@
|
||||
const submit = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
settings.set(values)
|
||||
user.settings.set(values)
|
||||
|
||||
toast.show("info", "Your settings have been saved!")
|
||||
}
|
||||
@ -38,11 +38,11 @@
|
||||
<div class="flex flex-col gap-8 w-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex gap-2 items-center">
|
||||
<strong>Show link and image previews</strong>
|
||||
<Toggle bind:value={values.showLinkPreviews} />
|
||||
<strong>Show images and link previews</strong>
|
||||
<Toggle bind:value={values.showMedia} />
|
||||
</div>
|
||||
<p class="text-sm text-light">
|
||||
If enabled, coracle will automatically retrieve a link preview for the first link
|
||||
If enabled, coracle will automatically retrieve a link preview for the last link
|
||||
in any note.
|
||||
</p>
|
||||
</div>
|
||||
|
@ -10,13 +10,13 @@
|
||||
let nsec = ''
|
||||
const nip07 = "https://github.com/nostr-protocol/nips/blob/master/07.md"
|
||||
|
||||
const logIn = async () => {
|
||||
const logIn = () => {
|
||||
const privkey = (nsec.startsWith('nsec') ? nip19.decode(nsec).data : nsec) as string
|
||||
|
||||
if (!privkey.match(/[a-z0-9]{64}/)) {
|
||||
toast.show("error", "Sorry, but that's an invalid private key.")
|
||||
} else {
|
||||
await login({privkey})
|
||||
login({privkey})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -9,13 +9,13 @@
|
||||
|
||||
let npub = ''
|
||||
|
||||
const logIn = async () => {
|
||||
const logIn = () => {
|
||||
const pubkey = (npub.startsWith('npub') ? nip19.decode(npub).data : npub) as string
|
||||
|
||||
if (!pubkey.match(/[a-z0-9]{64}/)) {
|
||||
toast.show("error", "Sorry, but that's an invalid public key.")
|
||||
} else {
|
||||
await login({pubkey})
|
||||
login({pubkey})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -6,13 +6,13 @@
|
||||
import Content from 'src/partials/Content.svelte'
|
||||
import Heading from 'src/partials/Heading.svelte'
|
||||
import {toast} from "src/app/ui"
|
||||
import {login} from "src/app"
|
||||
import {signup} 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 as string})
|
||||
const logIn = () => {
|
||||
signup(nip19.decode(nsec).data as string)
|
||||
}
|
||||
|
||||
const copyKey = () => {
|
||||
|
Loading…
Reference in New Issue
Block a user