Wrap relays in user, improve first run experience

This commit is contained in:
Jonathan Staab 2023-02-17 11:43:55 -06:00
parent 0b35ccc64d
commit 545f13e0b5
37 changed files with 412 additions and 282 deletions

View File

@ -7,6 +7,9 @@
- [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
- nip07.getRelays, nip05, relay.nostr.band
- [ ] Fix bugs on bugsnag
- [ ] Fix profile merging, put kind0 on its own property so we're not messing other people's profile data up.
- [ ] Test publishing events with zero relays
- [ ] Try lumping tables into a single key each to reduce load/save contention and time
# Snacks
@ -71,3 +74,5 @@
- [ ] Ability to leave/mute DM conversation
- [ ] Add petnames for channels
- [ ] Add notifications for chat messages
- [ ] Compress events
- https://github.com/nostr-protocol/nips/issues/265#issuecomment-1434250263

View File

@ -19,9 +19,8 @@
import network from 'src/agent/network'
import pool from 'src/agent/pool'
import {getUserRelays} from 'src/agent/relays'
import {relays} from 'src/agent/relays'
import sync from 'src/agent/sync'
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {loadAppData} from "src/app"
import alerts from "src/app/alerts"
import messages from "src/app/messages"
@ -111,8 +110,8 @@
})
database.onReady(() => {
if ($user) {
loadAppData($user.pubkey)
if (user.getProfile()) {
loadAppData(user.getPubkey())
}
const interval = setInterval(

View File

@ -206,8 +206,8 @@ class Table {
remove(k) {
return this.bulkRemove([k])
}
clear() {
return callLocalforage(this.name, 'clear')
drop() {
return callLocalforage(this.name, 'dropInstance')
}
dump() {
return callLocalforage(this.name, 'dump')
@ -316,7 +316,7 @@ const watch = (names, f) => {
const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey}
const clearAll = () => Promise.all(Object.values(registry).map(t => t.clear()))
const dropAll = () => Promise.all(Object.values(registry).map(t => t.drop()))
const ready = derived(pluck('ready', Object.values(registry)), all(identity))
@ -330,6 +330,6 @@ const onReady = cb => {
}
export default {
watch, getPersonWithFallback, clearAll, people, rooms, messages,
watch, getPersonWithFallback, dropAll, people, rooms, messages,
alerts, relays, routes, ready, onReady,
}

View File

@ -1,14 +1 @@
/**
* The dependency tree gets a little complex here:
*
* cmd
* -> network
* -> user, pool
* -> keys
* -> sync
* -> database
*
* In other words, command/network depend on utility functions and the network to
* do their job. The database sits at the bottom since it's shared between user
* which query the database, and network which both queries and updates it.
*/

View File

@ -4,8 +4,8 @@ import {get} from 'svelte/store'
import {error} from 'src/util/logger'
import {synced} from 'src/util/misc'
const pubkey = synced('agent/user/pubkey')
const privkey = synced('agent/user/privkey')
const pubkey = synced('agent/keys/pubkey')
const privkey = synced('agent/keys/privkey')
const getExtension = () => (window as {nostr?: any}).nostr
const canSign = () => Boolean(getExtension() || get(privkey))

View File

@ -1,7 +1,7 @@
import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
import {personKinds, findReplyId} from 'src/util/nostr'
import {chunk} from 'hurdak/lib/hurdak'
import {batch, shuffle, timedelta, now} from 'src/util/misc'
import {batch, timedelta, now} from 'src/util/misc'
import {
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
getUserReadRelays, getRelaysForEventChildren,
@ -82,7 +82,7 @@ const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any =
}) as Promise<void>
}
const loadPeople = (pubkeys, {kinds = personKinds, force = false, ...opts} = {}) => {
const loadPeople = async (pubkeys, {kinds = personKinds, force = false, ...opts} = {}) => {
pubkeys = uniq(pubkeys)
// If we're not reloading, only get pubkeys we don't already know about
@ -90,11 +90,14 @@ const loadPeople = (pubkeys, {kinds = personKinds, force = false, ...opts} = {})
pubkeys = getStalePubkeys(pubkeys)
}
return load(
shuffle(getUserReadRelays().concat(getAllPubkeyWriteRelays(pubkeys))).slice(0, 3),
{kinds, authors: pubkeys},
opts
)
// Use the best relays we have, but fall back to user relays
const relays = getAllPubkeyWriteRelays(pubkeys)
.concat(getUserReadRelays())
.slice(0, 3)
if (pubkeys.length > 0) {
await load(relays, {kinds, authors: pubkeys}, opts)
}
}
const loadParents = notes => {

View File

@ -1,10 +1,9 @@
import type {Relay} from 'src/util/types'
import {get} from 'svelte/store'
import {pick, map, assoc, sortBy, uniqBy, prop} from 'ramda'
import {first} from 'hurdak/lib/hurdak'
import {Tags, findReplyId} from 'src/util/nostr'
import {synced} from 'src/util/misc'
import database from 'src/agent/database'
import user from 'src/agent/user'
import keys from 'src/agent/keys'
// From Mike Dilger:
@ -19,8 +18,6 @@ import keys from 'src/agent/keys'
// doesn't need to see.
// 5) Advertise relays — write and read back your own relay list
export const relays = synced('agent/relays', [])
// Pubkey relays
export const getPubkeyRelays = (pubkey, mode = null) => {
@ -46,9 +43,14 @@ export const getAllPubkeyWriteRelays = pubkeys =>
// Current user
export const getUserRelays = (): Array<Relay> => get(relays).map(assoc('score', 1))
export const getUserReadRelays = () => getUserRelays().filter(prop('read'))
export const getUserWriteRelays = () => getUserRelays().filter(prop('write'))
export const getUserRelays = () =>
user.getRelays().map(assoc('score', 1))
export const getUserReadRelays = () =>
getUserRelays().filter(prop('read')).map(pick(['url', 'score']))
export const getUserWriteRelays = () =>
getUserRelays().filter(prop('write')).map(pick(['url', 'score']))
// Event-related special cases

View File

@ -1,8 +1,7 @@
import {uniq} from 'ramda'
import {get} from 'svelte/store'
import {Tags} from 'src/util/nostr'
import database from 'src/agent/database'
import {follows} from 'src/agent/user'
import user from 'src/agent/user'
export const getFollows = pubkey =>
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all()
@ -13,7 +12,8 @@ export const getNetwork = pubkey => {
return uniq(follows.concat(follows.flatMap(getFollows)))
}
export const getUserFollows = (): Array<string> => get(follows.pubkeys)
export const getUserFollows = (): Array<string> =>
Tags.wrap(user.getPetnames()).values().all()
export const getUserNetwork = () => {
const follows = getUserFollows()

View File

@ -1,7 +1,7 @@
import {pick, identity, isEmpty} from 'ramda'
import {uniq, pick, identity, isEmpty} from 'ramda'
import {nip05} from 'nostr-tools'
import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {warn} from 'src/util/logger'
import {warn, log} from 'src/util/logger'
import {now, timedelta, shuffle, hash} from 'src/util/misc'
import {Tags, roomAttrs, isRelay, normalizeRelayUrl} from 'src/util/nostr'
import database from 'src/agent/database'
@ -46,8 +46,66 @@ const processProfileEvents = async events => {
}
})
},
3: () => ({petnames: e.tags}),
2: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
const {relays = []} = database.getPersonWithFallback(e.pubkey)
return {
relays: relays.concat({url: e.content}),
relays_updated_at: e.created_at,
}
}
},
3: () => {
const data = {petnames: e.tags}
if (e.created_at > (person.relays_updated_at || 0)) {
tryJson(() => {
Object.assign(data, {
relays_updated_at: e.created_at,
relays: Object.entries(JSON.parse(e.content))
.map(([url, conditions]) => {
const {write, read} = conditions as Record<string, boolean|string>
return {
url,
write: [false, '!'].includes(write) ? '!' : '',
read: [false, '!'].includes(read) ? '!' : '',
}
})
.filter(r => isRelay(r.url)),
})
})
}
return data
},
12165: () => ({muffle: e.tags}),
// DEPRECATED
10001: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
return {
relays_updated_at: e.created_at,
relays: e.tags.map(([url, read, write]) => ({url, read, write})),
}
}
},
10002: () => {
if (e.created_at > (person.relays_updated_at || 0)) {
return {
relays_updated_at: e.created_at,
relays: e.tags.map(([_, url, mode]) => {
const read = (mode || 'read') === 'read'
const write = (mode || 'write') === 'write'
return {url, read, write}
}),
}
}
},
default: () => {
log(`Received unsupported event type ${e.kind}`)
},
}),
updated_at: now(),
}
@ -129,7 +187,8 @@ const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
const url = normalizeRelayUrl(rawUrl)
const id = hash([pubkey, url, mode].join('')).toString()
const score = getWeight(type) * (1 - (now() - created_at) / timedelta(30, 'days'))
const route = database.routes.get(id) || {id, pubkey, url, mode, score: 0, count: 0}
const defaults = {id, pubkey, url, mode, score: 0, count: 0, types: []}
const route = database.routes.get(id) || defaults
const newTotalScore = route.score * route.count + score
const newCount = route.count + 1
@ -139,6 +198,7 @@ const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
...route,
count: newCount,
score: newTotalScore / newCount,
types: uniq(route.types.concat(type)),
last_seen: Math.max(created_at, route.last_seen || 0),
}
}
@ -193,7 +253,7 @@ const processRoutes = async events => {
},
10002: () => {
e.tags
.forEach(([url, read, mode]) => {
.forEach(([_, url, mode]) => {
if (mode) {
calculateRoute(e.pubkey, url, 'kind:10002', mode, e.created_at)
} else {

View File

@ -1,56 +1,108 @@
import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store'
import {pipe, concat, reject, nth, map} from 'ramda'
import {pipe, assoc, whereEq, when, concat, reject, nth, map} from 'ramda'
import {synced} from 'src/util/misc'
import {derived, get} from 'svelte/store'
import database from 'src/agent/database'
import {getUserWriteRelays} from 'src/agent/relays'
import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd'
export const user = derived(
// Create a special wrapper to manage profile data, follows, and relays in the same
// way whether the user is logged in or not. This involves creating a store that we
// allow an anonymous user to write to, then once the user logs in we use that until
// we have actual event data for them, which we then prefer. For extra fun, we also
// sync this stuff to regular private variables so we don't have to constantly call
// `get` on our stores.
let profileCopy = null
let petnamesCopy = []
let relaysCopy = []
const anonPetnames = synced('agent/user/anonPetnames', [])
const anonRelays = synced('agent/user/anonRelays', [])
const profile = derived(
[keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => {
if (!pubkey) {
return null
}
return ($people[pubkey] || {pubkey})
return profileCopy = ($people[pubkey] || {pubkey})
}
) as Readable<Person>
// Create a special wrapper to manage follows the same way whether
// the user is logged in or not
export const follows = (() => {
const anonPetnames = synced('agent/user/anonPetnames', [])
const petnames = derived(
[user, anonPetnames],
([$user, $anonPetnames]) =>
$user?.petnames || $anonPetnames
)
return {
petnames,
pubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>,
update(f) {
const $petnames = f(get(petnames))
anonPetnames.set($petnames)
if (get(user)) {
cmd.setPetnames(getUserWriteRelays(), $petnames)
}
},
addFollow(pubkey, url, name) {
const tag = ["p", pubkey, url, name || ""]
this.update(pipe(reject(t => t[1] === pubkey), concat([tag])))
},
removeFollow(pubkey) {
this.update(reject(t => t[1] === pubkey))
},
const petnames = derived(
[profile, anonPetnames],
([$profile, $anonPetnames]) => {
return petnamesCopy = $profile?.petnames || $anonPetnames
}
})()
)
const relays = derived(
[profile, anonRelays],
([$profile, $anonRelays]) => {
return relaysCopy = $profile?.relays || $anonRelays
}
)
// Prime our copies
get(profile)
get(petnames)
get(relays)
const user = {
// Profile
profile,
getProfile: () => profileCopy,
getPubkey: () => profileCopy?.pubkey,
// Petnames
petnames,
getPetnames: () => petnamesCopy,
petnamePubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>,
updatePetnames(f) {
const $petnames = f(petnamesCopy)
anonPetnames.set($petnames)
if (profileCopy) {
cmd.setPetnames(relaysCopy, $petnames)
}
},
addPetname(pubkey, url, name) {
const tag = ["p", pubkey, url, name || ""]
this.updatePetnames(pipe(reject(t => t[1] === pubkey), concat([tag])))
},
removePetname(pubkey) {
this.updatePetnames(reject(t => t[1] === pubkey))
},
// Relays
relays,
getRelays: () => relaysCopy,
updateRelays(f) {
const $relays = f(relaysCopy)
anonRelays.set($relays)
if (profileCopy) {
cmd.setRelays($relays, $relays)
}
},
async addRelay(url) {
this.updateRelays($relays => $relays.concat({url, write: false, read: true}))
},
async removeRelay(url) {
this.updateRelays(reject(whereEq({url})))
},
async setRelayWriteCondition(url, write) {
this.updateRelays(map(when(whereEq({url}), assoc('write', write))))
},
}
export default user

View File

@ -1,17 +1,14 @@
import type {Person, DisplayEvent} from 'src/util/types'
import {assoc, omit, sortBy, whereEq, identity, when, reject} from 'ramda'
import type {DisplayEvent} from 'src/util/types'
import {omit, sortBy, identity} 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'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user} from 'src/agent/user'
import {getNetwork} from 'src/agent/social'
import {relays, getUserReadRelays} from 'src/agent/relays'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd'
import alerts from 'src/app/alerts'
import messages from 'src/app/messages'
import {routes, modal} from 'src/app/ui'
@ -52,54 +49,6 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
}
}
export const addRelay = async url => {
const $user = get(user) as Person
relays.update($relays => {
$relays.push({url, write: false, read: true})
if ($user) {
(async () => {
// Publish to the new set of relays
await cmd.setRelays($relays, $relays)
// Reload alerts, messages, etc
await loadAppData($user.pubkey)
})()
}
return $relays
})
}
export const removeRelay = async url => {
const $user = get(user) as Person
relays.update($relays => {
$relays = reject(whereEq({url}), $relays)
if ($user && $relays.length > 0) {
cmd.setRelays($relays, $relays)
}
return $relays
})
}
export const setRelayWriteCondition = async (url, write) => {
const $user = get(user) as Person
relays.update($relays => {
$relays = $relays.map(when(whereEq({url}), assoc('write', write)))
if ($user && $relays.length > 0) {
cmd.setRelays($relays, $relays)
}
return $relays
})
}
export const renderNote = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire
const peopleByPubkey = createMap(

View File

@ -1,7 +1,7 @@
import {pluck, find, reject} from 'ramda'
import {get, derived} from 'svelte/store'
import {derived} from 'svelte/store'
import {synced, now, timedelta} from 'src/util/misc'
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
@ -34,8 +34,6 @@ const listen = async pubkey => {
[{kinds: [4], authors: [pubkey], since},
{kinds: [4], '#p': [pubkey], since}],
async events => {
const $user = get(user)
// Reload annotated messages, don't alert about messages to self
const messages = reject(e => e.pubkey === e.recipient, await database.messages.all())
@ -44,7 +42,7 @@ const listen = async pubkey => {
mostRecentByPubkey.update(o => {
for (const {pubkey, created_at} of messages) {
if (pubkey !== $user.pubkey) {
if (pubkey !== user.getPubkey()) {
o[pubkey] = Math.max(created_at, o[pubkey] || 0)
}
}

View File

@ -8,7 +8,7 @@
import Badge from 'src/partials/Badge.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import Spinner from 'src/partials/Spinner.svelte'
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import database from 'src/agent/database'
import {renderNote} from 'src/app'
@ -29,6 +29,8 @@
let showNewMessages = false
let cursor = new Cursor()
const {profile} = user
$: {
// Group messages so we're only showing the person once per chunk
annotatedMessages = reverse(sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
@ -62,7 +64,7 @@
}
onMount(async () => {
if (!$user) {
if (!$profile) {
return navigate('/login')
}
@ -134,14 +136,14 @@
</div>
{/if}
<div class={cx("flex overflow-hidden text-ellipsis", {
'ml-12 justify-end': type === 'dm' && m.person.pubkey === $user.pubkey,
'mr-12': type === 'dm' && m.person.pubkey !== $user.pubkey,
'ml-12 justify-end': type === 'dm' && m.person.pubkey === $profile.pubkey,
'mr-12': type === 'dm' && m.person.pubkey !== $profile.pubkey,
})}>
<div class={cx({
'ml-6': type === 'chat',
'rounded-2xl py-2 px-4 flex max-w-xl': type === 'dm',
'bg-light text-black rounded-br-none': type === 'dm' && m.person.pubkey === $user.pubkey,
'bg-dark rounded-bl-none': type === 'dm' && m.person.pubkey !== $user.pubkey,
'bg-light text-black rounded-br-none': type === 'dm' && m.person.pubkey === $profile.pubkey,
'bg-dark rounded-bl-none': type === 'dm' && m.person.pubkey !== $profile.pubkey,
})}>
{@html renderNote(m, {showEntire: true})}
</div>

View File

@ -2,82 +2,96 @@
import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import PersonInfo from "src/partials/PersonInfo.svelte"
import RelaySearch from "src/partials/RelaySearch.svelte"
import SearchPeople from "src/views/SearchPeople.svelte"
import {relays} from 'src/agent/relays'
import {follows} from 'src/agent/user'
import database from 'src/agent/database'
import user from 'src/agent/user'
export let enforceRelays = true
export let enforcePeople = true
const {petnames} = follows
const {petnamePubkeys, relays} = user
const needsRelays = () => $relays.length === 0 && enforceRelays
const needsPeople = () => $petnamePubkeys.length === 0 && enforcePeople
let modalIsOpen = true
let modal = needsRelays() ? 'relays' : (needsPeople() ? 'people' : null)
const closeModal = () => {
modalIsOpen = false
modal = null
}
</script>
{#if $relays.length === 0 && enforceRelays}
{#if modalIsOpen}
<Modal onEscape={closeModal}>
<Content>
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Search below to find one to join.
</div>
</div>
<RelaySearch />
</Content>
</Modal>
{:else}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
{#if modal === 'relays'}
<Modal onEscape={closeModal}>
<Content>
{#if $relays.length > 0}
<h1 class="text-2xl">Your Relays</h1>
{/if}
{#each $relays as relay (relay.url)}
<RelayCard showControls {relay} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Click <Anchor href="/relays">here</Anchor> to find one to join.
Search below to find one to join.
</div>
</div>
{/each}
<RelaySearch />
</Content>
{/if}
{:else if $petnames.length === 0 && enforcePeople}
{#if modalIsOpen}
<Modal onEscape={closeModal}>
<Content>
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Search below to find some interesting people.
</div>
</div>
<SearchPeople />
</Content>
</Modal>
{:else}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
</Modal>
{:else if needsRelays()}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet connected to any relays.
</div>
<div>
Click <Anchor href="/relays">here</Anchor> to find one to join.
</div>
</div>
</Content>
{:else if modal === 'people'}
<Modal onEscape={closeModal}>
<Content>
{#if $petnamePubkeys.length > 0}
<h1 class="text-2xl">Your Follows</h1>
{/if}
{#each $petnamePubkeys as pubkey (pubkey)}
<PersonInfo person={database.people.get(pubkey)} />
{:else}
<div class="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Click <Anchor href="/search/people">here</Anchor> to find some
interesting people.
Search below to find some interesting people.
</div>
</div>
{/each}
<SearchPeople hideFollowing />
</Content>
{/if}
</Modal>
{:else if needsPeople()}
<Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" />
You aren't yet following anyone.
</div>
<div>
Click <Anchor href="/search/people">here</Anchor> to find some
interesting people.
</div>
</div>
</Content>
{:else}
<slot />
{/if}

View File

@ -29,5 +29,12 @@
<div class="bg-dark border-t border-solid border-medium h-full w-full overflow-auto pb-10">
<slot />
</div>
{#if onEscape}
<div class="absolute top-0 flex w-full justify-end pr-2 -mt-8">
<div class="w-10 h-10 flex justify-center items-center bg-accent rounded-full cursor-pointer border border-solid border-medium border-b-0">
<i class="fa fa-times fa-lg cursor-pointer" on:click={onEscape} />
</div>
</div>
{/if}
</div>
</div>

View File

@ -17,7 +17,7 @@
import {formatTimestamp, stringToColor} from 'src/util/misc'
import Compose from "src/partials/Compose.svelte"
import Card from "src/partials/Card.svelte"
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {getEventPublishRelays, getRelaysForEventParent} from 'src/agent/relays'
import database from 'src/agent/database'
import cmd from 'src/agent/cmd'
@ -30,12 +30,13 @@
export let shouldDisplay = always(true)
const getDefaultReplyMentions = () =>
without([$user?.pubkey], uniq(Tags.from(note).type("p").values().all().concat(note.pubkey)))
without([$profile?.pubkey], uniq(Tags.from(note).type("p").values().all().concat(note.pubkey)))
let reply = null
let replyMentions = getDefaultReplyMentions()
let replyContainer = null
const {profile} = user
const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : []
const showEntire = anchorId === note.id
const interactive = !anchorId || !showEntire
@ -53,8 +54,8 @@
flags = note.reactions.filter(whereEq({content: '-'}))
}
$: like = find(whereEq({pubkey: $user?.pubkey}), likes)
$: flag = find(whereEq({pubkey: $user?.pubkey}), flags)
$: like = find(whereEq({pubkey: $profile?.pubkey}), likes)
$: flag = find(whereEq({pubkey: $profile?.pubkey}), flags)
$: $likesCount = likes.length
$: $flagsCount = flags.length
@ -85,7 +86,7 @@
}
const react = async content => {
if (!$user) {
if (!$profile) {
return navigate('/login')
}
@ -105,16 +106,16 @@
cmd.deleteEvent(getEventPublishRelays(note), [e.id])
if (e.content === '+') {
likes = reject(propEq('pubkey', $user.pubkey), likes)
likes = reject(propEq('pubkey', $profile.pubkey), likes)
}
if (e.content === '-') {
flags = reject(propEq('pubkey', $user.pubkey), flags)
flags = reject(propEq('pubkey', $profile.pubkey), flags)
}
}
const startReply = () => {
if ($user) {
if ($profile) {
reply = reply || true
} else {
navigate('/login')

View File

@ -8,8 +8,9 @@
import Spinner from 'src/partials/Spinner.svelte'
import Content from 'src/partials/Content.svelte'
import Note from "src/partials/Note.svelte"
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import network from 'src/agent/network'
import {getUserReadRelays} from 'src/agent/relays'
import {modal} from "src/app/ui"
import {mergeParents} from "src/app"
@ -23,8 +24,9 @@
const since = now()
const maxNotes = 300
const cursor = new Cursor()
const {profile} = user
const muffle = Tags
.wrap(($user?.muffle || []).filter(t => Math.random() > parseFloat(last(t))))
.wrap(($profile?.muffle || []).filter(t => Math.random() > parseFloat(last(t))))
.values().all()
const processNewNotes = async newNotes => {
@ -75,6 +77,9 @@
}
onMount(() => {
// Add in our user relays in case they weren't specified above
relays = relays.concat(getUserReadRelays()).slice(0, 3)
const sub = network.listen(relays, {...filter, since}, onChunk)
const scroller = createScroller(() => {

View File

@ -1,29 +1,55 @@
<script lang="ts">
import {last} from 'ramda'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak'
import {renderContent} from "src/util/html"
import {renderContent, noEvent} from "src/util/html"
import {displayPerson} from "src/util/nostr"
import Anchor from 'src/partials/Anchor.svelte'
import {getPubkeyWriteRelays} from 'src/agent/relays'
import user from 'src/agent/user'
import {routes} from "src/app/ui"
export let person
const {petnamePubkeys} = user
const addPetname = pubkey => {
const [{url}] = getPubkeyWriteRelays(pubkey)
user.addPetname(pubkey, url, person.name)
}
</script>
<a
in:fly={{y: 20}}
href={routes.person(person.pubkey)}
class="flex gap-4 border-l-2 border-solid border-dark hover:bg-black hover:border-accent transition-all py-3 px-6 overflow-hidden">
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({person.picture})" />
<div class="flex-grow">
<div class="flex gap-2 items-center justify-between">
<h1 class="text-2xl">{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 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>{@html renderContent(ellipsize(person.about || '', 140))}</p>
<p class="overflow-hidden text-ellipsis">
{@html renderContent(ellipsize(person.about || '', 140))}
</p>
</div>
</a>

View File

@ -6,9 +6,8 @@
import {between} from 'hurdak/lib/hurdak'
import {fly} from 'svelte/transition'
import Toggle from "src/partials/Toggle.svelte"
import {relays} from "src/agent/relays"
import pool from 'src/agent/pool'
import {addRelay, removeRelay, setRelayWriteCondition} from "src/app"
import user from "src/agent/user"
export let relay
export let theme = 'dark'
@ -19,6 +18,8 @@
let showStatus = false
let joined = false
const {relays} = user
$: joined = find(propEq('url', relay.url), $relays)
onMount(() => {
@ -63,11 +64,15 @@
</p>
</div>
{#if joined}
<button class="flex gap-3 items-center text-light" on:click={() => removeRelay(relay.url)}>
<button
class="flex gap-3 items-center text-light"
on:click={() => user.removeRelay(relay.url)}>
<i class="fa fa-right-from-bracket" /> Leave
</button>
{:else}
<button class="flex gap-3 items-center text-light" on:click={() => addRelay(relay.url)}>
<button
class="flex gap-3 items-center text-light"
on:click={() => user.addRelay(relay.url)}>
<i class="fa fa-right-to-bracket" /> Join
</button>
{/if}
@ -81,7 +86,7 @@
<span>Publish to this relay?</span>
<Toggle
value={relay.write}
on:change={() => setRelayWriteCondition(relay.url, !relay.write)} />
on:change={() => user.setRelayWriteCondition(relay.url, !relay.write)} />
</div>
{/if}
</div>

View File

@ -5,12 +5,14 @@
import Input from "src/partials/Input.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import database from 'src/agent/database'
import {relays} from "src/agent/relays"
import user from "src/agent/user"
let q = ""
let search
let knownRelays = database.watch('relays', t => t.all())
const {relays} = user
$: {
const joined = new Set(pluck('url', $relays))

View File

@ -1,6 +1,6 @@
<script lang="ts">
import {displayPerson} from 'src/util/nostr'
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {menuIsOpen, routes} from 'src/app/ui'
import alerts from 'src/app/alerts'
import messages from 'src/app/messages'
@ -8,6 +8,7 @@
const {mostRecentAlert, lastCheckedAlerts} = alerts
const {hasNewMessages} = messages
const {profile} = user
</script>
<ul
@ -15,13 +16,13 @@
border-r border-medium text-white overflow-hidden z-10 lg:ml-0"
class:-ml-56={!$menuIsOpen}
>
{#if $user}
{#if $profile}
<li>
<a href={routes.person($user.pubkey)} class="flex gap-2 px-4 py-2 pb-6 items-center">
<a href={routes.person($profile.pubkey)} class="flex gap-2 px-4 py-2 pb-6 items-center">
<div
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({$user.picture})" />
<span class="text-lg font-bold">{displayPerson($user)}</span>
style="background-image: url({$profile.picture})" />
<span class="text-lg font-bold">{displayPerson($profile)}</span>
</a>
</li>
<li class="cursor-pointer relative">
@ -43,7 +44,7 @@
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>
{#if $user}
{#if $profile}
<li class="cursor-pointer relative">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/chat">
<i class="fa-solid fa-message mr-2" /> Chat
@ -62,7 +63,7 @@
{/if}
</a>
</li>
{#if $user}
{#if $profile}
<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

View File

@ -1,11 +1,11 @@
<script>
import {fly} from 'svelte/transition'
import {toast, modal} from "src/app/ui"
import {addRelay} from 'src/app'
import Input from 'src/partials/Input.svelte'
import Content from 'src/partials/Content.svelte'
import Heading from 'src/partials/Heading.svelte'
import Button from 'src/partials/Button.svelte'
import user from 'src/agent/user'
import {toast, modal} from "src/app/ui"
let url = $modal.url
@ -27,7 +27,7 @@
return toast.show("error", "That isn't a valid websocket url")
}
addRelay(url)
user.addRelay(url)
modal.set(null)
}
</script>

View File

@ -4,7 +4,7 @@
import {nip19} from 'nostr-tools'
import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc"
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import network from 'src/agent/network'
import database from 'src/agent/database'
import {getUserReadRelays} from 'src/agent/relays'
@ -24,7 +24,7 @@
const rooms = database.watch(['rooms', 'messages'], async () => {
const rooms = await database.rooms.all({joined: true})
const messages = await database.messages.all()
const pubkeys = without([$user.pubkey], uniq(messages.flatMap(m => [m.pubkey, m.recipient])))
const pubkeys = without([user.getPubkey()], uniq(messages.flatMap(m => [m.pubkey, m.recipient])))
await network.loadPeople(pubkeys)

View File

@ -3,7 +3,7 @@
import {nip19} from 'nostr-tools'
import {now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {getRelaysForEventChildren} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
@ -57,7 +57,7 @@
name={$room?.name}
about={$room?.about}
picture={$room?.picture}
editRoom={$room?.pubkey === $user.pubkey && editRoom}
editRoom={$room?.pubkey === user.getPubkey() && editRoom}
{loadMessages}
{listenForMessages}
{sendMessage}

View File

@ -9,9 +9,9 @@
const confirm = async () => {
confirmed = true
localStorage.clear()
await database.dropAll()
await database.clearAll()
localStorage.clear()
// do a hard refresh so everything gets totally cleared.
// Give them a moment to see the state transition. Dexie

View File

@ -4,7 +4,7 @@
import {personKinds} from 'src/util/nostr'
import {now} from 'src/util/misc'
import Channel from 'src/partials/Channel.svelte'
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {getAllPubkeyRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
@ -21,7 +21,7 @@
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))
const getRelays = () => getAllPubkeyRelays([pubkey, $user.pubkey]).slice(0, 3)
const getRelays = () => getAllPubkeyRelays([pubkey, user.getPubkey()]).slice(0, 3)
const decryptMessages = async events => {
// Gotta do it in serial because of extension limitations
@ -37,8 +37,8 @@
const listenForMessages = cb => network.listen(
getRelays(),
[{kinds: personKinds, authors: [pubkey]},
{kinds: [4], authors: [$user.pubkey], '#p': [pubkey]},
{kinds: [4], authors: [pubkey], '#p': [$user.pubkey]}],
{kinds: [4], authors: [user.getPubkey()], '#p': [pubkey]},
{kinds: [4], authors: [pubkey], '#p': [user.getPubkey()]}],
async events => {
// Reload from db since we annotate messages there
const messageIds = pluck('id', events.filter(e => e.kind === 4))

View File

@ -1,6 +1,7 @@
<script>
import {onMount} from 'svelte'
import {navigate} from 'svelte-routing'
import user from 'src/agent/user'
onMount(() => navigate('/notes/network'))
onMount(() => navigate(user.getProfile() ? '/notes/network' : '/login'))
</script>

View File

@ -6,7 +6,7 @@
import Tabs from "src/partials/Tabs.svelte"
import Network from "src/views/notes/Network.svelte"
import Popular from "src/views/notes/Popular.svelte"
import {user} from 'src/agent/user'
import user from 'src/agent/user'
export let activeTab
@ -14,7 +14,7 @@
</script>
<Content>
{#if !$user}
{#if !user.getProfile()}
<Content size="lg" class="text-center">
<p class="text-xl">Don't have an account?</p>
<p>Click <Anchor href="/login">here</Anchor> to join the nostr network.</p>

View File

@ -1,10 +1,11 @@
<script lang="ts">
import {last} from 'ramda'
import {last, prop} from 'ramda'
import {onMount} from 'svelte'
import {tweened} from 'svelte/motion'
import {nip19} from 'nostr-tools'
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {first} from 'hurdak/lib/hurdak'
import {log} from 'src/util/logger'
import {renderContent} from 'src/util/html'
import {displayPerson, Tags} from 'src/util/nostr'
@ -16,7 +17,7 @@
import Notes from "src/views/person/Notes.svelte"
import Likes from "src/views/person/Likes.svelte"
import Relays from "src/views/person/Relays.svelte"
import {user, follows} from "src/agent/user"
import user from "src/agent/user"
import {getUserReadRelays, getPubkeyWriteRelays} from "src/agent/relays"
import network from "src/agent/network"
import keys from "src/agent/keys"
@ -28,7 +29,7 @@
export let relays = []
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
const {pubkeys: userFollows} = follows
const {petnamePubkeys} = user
let pubkey = nip19.decode(npub).data as string
let following = false
@ -37,13 +38,17 @@
let person = database.getPersonWithFallback(pubkey)
let loading = true
$: following = $userFollows.includes(pubkey)
$: following = $petnamePubkeys.includes(pubkey)
onMount(async () => {
log('Person', npub, person)
// Add all the relays we know the person uses
relays = relays.concat(getPubkeyWriteRelays(pubkey))
// Add all the relays we know the person uses, as well as our own
// in case we don't have much information
relays = relays
.concat(getPubkeyWriteRelays(pubkey))
.concat(getUserReadRelays())
.slice(0, 3)
// Refresh our person if needed
network.loadPeople([pubkey]).then(() => {
@ -59,7 +64,7 @@
}
})
// Round it out
// Round out our followers count
await network.listenUntilEose(
relays,
[{kinds: [3], '#p': [pubkey]}],
@ -87,13 +92,11 @@
}
const follow = async () => {
const [{url}] = relays.concat(getUserReadRelays())
follows.addFollow(pubkey, url, person.name)
user.addPetname(pubkey, prop('url', first(relays)), person.name)
}
const unfollow = async () => {
follows.removeFollow(pubkey)
user.removePetname(pubkey)
}
const openAdvanced = () => {
@ -132,9 +135,9 @@
{/if}
</div>
<div class="whitespace-nowrap flex gap-3 items-center flex-wrap">
{#if $user?.pubkey === pubkey && keys.canSign()}
{#if user.getPubkey() === pubkey && keys.canSign()}
<Anchor href="/profile"><i class="fa-solid fa-edit" /> Edit profile</Anchor>
{:else if $user && keys.canSign()}
{:else if user.getProfile() && keys.canSign()}
<Anchor type="button-circle" on:click={openAdvanced}>
<i class="fa fa-sliders" />
</Anchor>
@ -146,7 +149,7 @@
<Anchor type="button-circle" on:click={unfollow}>
<i class="fa fa-user-minus" />
</Anchor>
{:else if $user?.pubkey !== pubkey}
{:else if user.getPubkey() !== pubkey}
<Anchor type="button-circle" on:click={follow}>
<i class="fa fa-user-plus" />
</Anchor>
@ -158,7 +161,7 @@
</div>
<p>{@html renderContent(person.about || '')}</p>
{#if person?.petnames}
<div class="flex gap-8">
<div class="flex gap-8" in:fly={{y: 20}}>
<button on:click={showFollows}>
<strong>{person.petnames.length}</strong> following
</button>
@ -173,7 +176,7 @@
<Tabs tabs={['notes', 'likes', 'relays']} {activeTab} {setActiveTab} />
{#if activeTab === 'notes'}
<Notes {pubkey} />
<Notes {pubkey} {relays} />
{:else if activeTab === 'likes'}
<Likes {pubkey} />
{:else if activeTab === 'relays'}

View File

@ -11,7 +11,7 @@
import Button from "src/partials/Button.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {user} from "src/agent/user"
import user from "src/agent/user"
import {getUserWriteRelays} from 'src/agent/relays'
import cmd from "src/agent/cmd"
import {routes, toast} from "src/app/ui"
@ -22,11 +22,11 @@
const pseudUrl = "https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
onMount(async () => {
if (!$user) {
if (!user.getProfile()) {
return navigate("/login")
}
values = pick(Object.keys(values), $user)
values = pick(Object.keys(values), user.getProfile())
document.querySelector('[name=picture]').addEventListener('change', async e => {
const target = e.target as HTMLInputElement
@ -48,7 +48,7 @@
cmd.updateUser(getUserWriteRelays(), values)
navigate(routes.person($user.pubkey, 'profile'))
navigate(routes.person(user.getPubkey(), 'profile'))
toast.show("info", "Your profile has been updated!")
}

View File

@ -4,8 +4,10 @@
import RelaySearch from "src/partials/RelaySearch.svelte"
import Content from "src/partials/Content.svelte"
import RelayCard from "src/partials/RelayCard.svelte"
import {relays} from "src/agent/relays"
import user from "src/agent/user"
import {modal} from "src/app/ui"
const {relays} = user
</script>
<div in:fly={{y: 20}}>

View File

@ -7,13 +7,13 @@
import Button from "src/partials/Button.svelte"
import Content from "src/partials/Content.svelte"
import Heading from "src/partials/Heading.svelte"
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {toast, settings} from "src/app/ui"
let values = {...$settings}
onMount(async () => {
if (!$user) {
if (!user.getProfile()) {
return navigate("/login")
}
})

View File

@ -61,6 +61,14 @@ export const killEvent = e => {
e.stopImmediatePropagation()
}
export const noEvent = f => e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
f()
}
export const fromParentOffset = (element, offset): [HTMLElement, number] => {
for (const child of element.childNodes) {
if (offset <= child.textContent.length) {

View File

@ -2,7 +2,7 @@ import {last, identity, objOf, prop, flatten, uniq} from 'ramda'
import {nip19} from 'nostr-tools'
import {ensurePlural, ellipsize, first} from 'hurdak/lib/hurdak'
export const personKinds = [0, 2, 3, 10002, 12165]
export const personKinds = [0, 2, 3, 10001, 10002, 12165]
export class Tags {
constructor(tags) {

View File

@ -4,7 +4,6 @@
import {quantify} from 'hurdak/lib/hurdak'
import {last, reject, pluck, propEq} from 'ramda'
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc"
import {isRelay} from "src/util/nostr"
import Button from "src/partials/Button.svelte"
@ -14,7 +13,6 @@
import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.svelte"
import Heading from 'src/partials/Heading.svelte'
import {user} from "src/agent/user"
import {getUserWriteRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import cmd from "src/agent/cmd"
@ -75,10 +73,6 @@
}
onMount(() => {
if (!$user) {
navigate("/login")
}
const person = database.people.get(pubkey)
if (person?.name) {

View File

@ -5,12 +5,12 @@
import Button from "src/partials/Button.svelte"
import Content from 'src/partials/Content.svelte'
import SelectButton from "src/partials/SelectButton.svelte"
import {user} from 'src/agent/user'
import user from 'src/agent/user'
import {getUserWriteRelays} from 'src/agent/relays'
import cmd from 'src/agent/cmd'
import {modal} from 'src/app/ui'
const muffle = $user.muffle || []
const muffle = user.getProfile().muffle || []
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']
const muffleValue = parseFloat(first(muffle.filter(t => t[1] === $modal.person.pubkey).map(last)) || 1)

View File

@ -1,17 +1,21 @@
<script>
import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc"
import {personKinds} from "src/util/nostr"
import Input from "src/partials/Input.svelte"
import Spinner from "src/partials/Spinner.svelte"
import PersonInfo from 'src/partials/PersonInfo.svelte'
import {user} from 'src/agent/user'
import {getUserReadRelays} from 'src/agent/relays'
import database from 'src/agent/database'
import network from 'src/agent/network'
import user from 'src/agent/user'
export let hideFollowing = false
let q
let search
const {petnamePubkeys} = user
database.watch('people', people => {
search = fuzzy(
people.all({'name:!nil': null}),
@ -28,9 +32,9 @@
</Input>
{#each (search ? search(q) : []).slice(0, 30) as person (person.pubkey)}
{#if person.pubkey !== $user?.pubkey}
<div in:fly={{y: 20}}>
<PersonInfo {person} />
</div>
{#if person.pubkey !== user.getPubkey() && !(hideFollowing && $petnamePubkeys.includes(person.pubkey))}
<PersonInfo {person} />
{/if}
{:else}
<Spinner />
{/each}