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 - [ ] Initial user load doesn't have any relays, cache user or wait for people db to be loaded
- nip07.getRelays, nip05, relay.nostr.band - nip07.getRelays, nip05, relay.nostr.band
- [ ] Fix bugs on bugsnag - [ ] 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 # Snacks
@ -71,3 +74,5 @@
- [ ] Ability to leave/mute DM conversation - [ ] Ability to leave/mute DM conversation
- [ ] Add petnames for channels - [ ] Add petnames for channels
- [ ] Add notifications for chat messages - [ ] 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 network from 'src/agent/network'
import pool from 'src/agent/pool' import pool from 'src/agent/pool'
import {getUserRelays} from 'src/agent/relays' import {getUserRelays} from 'src/agent/relays'
import {relays} from 'src/agent/relays'
import sync from 'src/agent/sync' import sync from 'src/agent/sync'
import {user} from 'src/agent/user' import user from 'src/agent/user'
import {loadAppData} from "src/app" import {loadAppData} from "src/app"
import alerts from "src/app/alerts" import alerts from "src/app/alerts"
import messages from "src/app/messages" import messages from "src/app/messages"
@ -111,8 +110,8 @@
}) })
database.onReady(() => { database.onReady(() => {
if ($user) { if (user.getProfile()) {
loadAppData($user.pubkey) loadAppData(user.getPubkey())
} }
const interval = setInterval( const interval = setInterval(

View File

@ -206,8 +206,8 @@ class Table {
remove(k) { remove(k) {
return this.bulkRemove([k]) return this.bulkRemove([k])
} }
clear() { drop() {
return callLocalforage(this.name, 'clear') return callLocalforage(this.name, 'dropInstance')
} }
dump() { dump() {
return callLocalforage(this.name, 'dump') return callLocalforage(this.name, 'dump')
@ -316,7 +316,7 @@ const watch = (names, f) => {
const getPersonWithFallback = pubkey => people.get(pubkey) || {pubkey} 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)) const ready = derived(pluck('ready', Object.values(registry)), all(identity))
@ -330,6 +330,6 @@ const onReady = cb => {
} }
export default { export default {
watch, getPersonWithFallback, clearAll, people, rooms, messages, watch, getPersonWithFallback, dropAll, people, rooms, messages,
alerts, relays, routes, ready, onReady, 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 {error} from 'src/util/logger'
import {synced} from 'src/util/misc' import {synced} from 'src/util/misc'
const pubkey = synced('agent/user/pubkey') const pubkey = synced('agent/keys/pubkey')
const privkey = synced('agent/user/privkey') const privkey = synced('agent/keys/privkey')
const getExtension = () => (window as {nostr?: any}).nostr const getExtension = () => (window as {nostr?: any}).nostr
const canSign = () => Boolean(getExtension() || get(privkey)) const canSign = () => Boolean(getExtension() || get(privkey))

View File

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

View File

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

View File

@ -1,8 +1,7 @@
import {uniq} from 'ramda' import {uniq} from 'ramda'
import {get} from 'svelte/store'
import {Tags} from 'src/util/nostr' import {Tags} from 'src/util/nostr'
import database from 'src/agent/database' import database from 'src/agent/database'
import {follows} from 'src/agent/user' import user from 'src/agent/user'
export const getFollows = pubkey => export const getFollows = pubkey =>
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all() 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))) 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 = () => { export const getUserNetwork = () => {
const follows = getUserFollows() 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 {nip05} from 'nostr-tools'
import {noop, createMap, ensurePlural, switcherFn} from 'hurdak/lib/hurdak' 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 {now, timedelta, shuffle, hash} from 'src/util/misc'
import {Tags, roomAttrs, isRelay, normalizeRelayUrl} from 'src/util/nostr' import {Tags, roomAttrs, isRelay, normalizeRelayUrl} from 'src/util/nostr'
import database from 'src/agent/database' 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}), 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(), updated_at: now(),
} }
@ -129,7 +187,8 @@ const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
const url = normalizeRelayUrl(rawUrl) const url = normalizeRelayUrl(rawUrl)
const id = hash([pubkey, url, mode].join('')).toString() const id = hash([pubkey, url, mode].join('')).toString()
const score = getWeight(type) * (1 - (now() - created_at) / timedelta(30, 'days')) 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 newTotalScore = route.score * route.count + score
const newCount = route.count + 1 const newCount = route.count + 1
@ -139,6 +198,7 @@ const calculateRoute = (pubkey, rawUrl, type, mode, created_at) => {
...route, ...route,
count: newCount, count: newCount,
score: newTotalScore / newCount, score: newTotalScore / newCount,
types: uniq(route.types.concat(type)),
last_seen: Math.max(created_at, route.last_seen || 0), last_seen: Math.max(created_at, route.last_seen || 0),
} }
} }
@ -193,7 +253,7 @@ const processRoutes = async events => {
}, },
10002: () => { 10002: () => {
e.tags e.tags
.forEach(([url, read, mode]) => { .forEach(([_, url, mode]) => {
if (mode) { if (mode) {
calculateRoute(e.pubkey, url, 'kind:10002', mode, e.created_at) calculateRoute(e.pubkey, url, 'kind:10002', mode, e.created_at)
} else { } else {

View File

@ -1,56 +1,108 @@
import type {Person} from 'src/util/types' import type {Person} from 'src/util/types'
import type {Readable} from 'svelte/store' 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 {synced} from 'src/util/misc'
import {derived, get} from 'svelte/store' import {derived, get} from 'svelte/store'
import database from 'src/agent/database' import database from 'src/agent/database'
import {getUserWriteRelays} from 'src/agent/relays'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd' 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>], [keys.pubkey, database.people as Readable<any>],
([pubkey, $people]) => { ([pubkey, $people]) => {
if (!pubkey) { if (!pubkey) {
return null return null
} }
return ($people[pubkey] || {pubkey}) return profileCopy = ($people[pubkey] || {pubkey})
} }
) as Readable<Person> ) 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( const petnames = derived(
[user, anonPetnames], [profile, anonPetnames],
([$user, $anonPetnames]) => ([$profile, $anonPetnames]) => {
$user?.petnames || $anonPetnames return petnamesCopy = $profile?.petnames || $anonPetnames
}
) )
return { 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, petnames,
pubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>, getPetnames: () => petnamesCopy,
update(f) { petnamePubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>,
const $petnames = f(get(petnames)) updatePetnames(f) {
const $petnames = f(petnamesCopy)
anonPetnames.set($petnames) anonPetnames.set($petnames)
if (get(user)) { if (profileCopy) {
cmd.setPetnames(getUserWriteRelays(), $petnames) cmd.setPetnames(relaysCopy, $petnames)
} }
}, },
addFollow(pubkey, url, name) { addPetname(pubkey, url, name) {
const tag = ["p", pubkey, url, name || ""] const tag = ["p", pubkey, url, name || ""]
this.update(pipe(reject(t => t[1] === pubkey), concat([tag]))) this.updatePetnames(pipe(reject(t => t[1] === pubkey), concat([tag])))
}, },
removeFollow(pubkey) { removePetname(pubkey) {
this.update(reject(t => t[1] === 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 type {DisplayEvent} from 'src/util/types'
import {assoc, omit, sortBy, whereEq, identity, when, reject} from 'ramda' import {omit, sortBy, identity} from 'ramda'
import {navigate} from 'svelte-routing' import {navigate} from 'svelte-routing'
import {createMap, ellipsize} from 'hurdak/lib/hurdak' import {createMap, ellipsize} from 'hurdak/lib/hurdak'
import {get} from 'svelte/store'
import {renderContent} from 'src/util/html' import {renderContent} from 'src/util/html'
import {Tags, displayPerson, findReplyId} from 'src/util/nostr' import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
import {user} from 'src/agent/user'
import {getNetwork} from 'src/agent/social' 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 database from 'src/agent/database'
import network from 'src/agent/network' import network from 'src/agent/network'
import keys from 'src/agent/keys' import keys from 'src/agent/keys'
import cmd from 'src/agent/cmd'
import alerts from 'src/app/alerts' import alerts from 'src/app/alerts'
import messages from 'src/app/messages' import messages from 'src/app/messages'
import {routes, modal} from 'src/app/ui' 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}) => { export const renderNote = (note, {showEntire = false}) => {
const shouldEllipsize = note.content.length > 500 && !showEntire const shouldEllipsize = note.content.length > 500 && !showEntire
const peopleByPubkey = createMap( const peopleByPubkey = createMap(

View File

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

View File

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

View File

@ -2,27 +2,36 @@
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Modal from "src/partials/Modal.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 RelaySearch from "src/partials/RelaySearch.svelte"
import SearchPeople from "src/views/SearchPeople.svelte" import SearchPeople from "src/views/SearchPeople.svelte"
import {relays} from 'src/agent/relays' import database from 'src/agent/database'
import {follows} from 'src/agent/user' import user from 'src/agent/user'
export let enforceRelays = true export let enforceRelays = true
export let enforcePeople = 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 = () => { const closeModal = () => {
modalIsOpen = false modal = null
} }
</script> </script>
{#if $relays.length === 0 && enforceRelays} {#if modal === 'relays'}
{#if modalIsOpen}
<Modal onEscape={closeModal}> <Modal onEscape={closeModal}>
<Content> <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="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center"> <div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" /> <i class="fa fa-triangle-exclamation fa-light" />
@ -32,10 +41,11 @@
Search below to find one to join. Search below to find one to join.
</div> </div>
</div> </div>
{/each}
<RelaySearch /> <RelaySearch />
</Content> </Content>
</Modal> </Modal>
{:else} {:else if needsRelays()}
<Content size="lg"> <Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12"> <div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center"> <div class="text-xl flex gap-2 items-center">
@ -47,11 +57,15 @@
</div> </div>
</div> </div>
</Content> </Content>
{/if} {:else if modal === 'people'}
{:else if $petnames.length === 0 && enforcePeople}
{#if modalIsOpen}
<Modal onEscape={closeModal}> <Modal onEscape={closeModal}>
<Content> <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="flex flex-col items-center gap-4 my-8">
<div class="text-xl flex gap-2 items-center"> <div class="text-xl flex gap-2 items-center">
<i class="fa fa-triangle-exclamation fa-light" /> <i class="fa fa-triangle-exclamation fa-light" />
@ -61,10 +75,11 @@
Search below to find some interesting people. Search below to find some interesting people.
</div> </div>
</div> </div>
<SearchPeople /> {/each}
<SearchPeople hideFollowing />
</Content> </Content>
</Modal> </Modal>
{:else} {:else if needsPeople()}
<Content size="lg"> <Content size="lg">
<div class="flex flex-col items-center gap-4 mt-12"> <div class="flex flex-col items-center gap-4 mt-12">
<div class="text-xl flex gap-2 items-center"> <div class="text-xl flex gap-2 items-center">
@ -77,7 +92,6 @@
</div> </div>
</div> </div>
</Content> </Content>
{/if}
{:else} {:else}
<slot /> <slot />
{/if} {/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"> <div class="bg-dark border-t border-solid border-medium h-full w-full overflow-auto pb-10">
<slot /> <slot />
</div> </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>
</div> </div>

View File

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

View File

@ -8,8 +8,9 @@
import Spinner from 'src/partials/Spinner.svelte' import Spinner from 'src/partials/Spinner.svelte'
import Content from 'src/partials/Content.svelte' import Content from 'src/partials/Content.svelte'
import Note from "src/partials/Note.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 network from 'src/agent/network'
import {getUserReadRelays} from 'src/agent/relays'
import {modal} from "src/app/ui" import {modal} from "src/app/ui"
import {mergeParents} from "src/app" import {mergeParents} from "src/app"
@ -23,8 +24,9 @@
const since = now() const since = now()
const maxNotes = 300 const maxNotes = 300
const cursor = new Cursor() const cursor = new Cursor()
const {profile} = user
const muffle = Tags 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() .values().all()
const processNewNotes = async newNotes => { const processNewNotes = async newNotes => {
@ -75,6 +77,9 @@
} }
onMount(() => { 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 sub = network.listen(relays, {...filter, since}, onChunk)
const scroller = createScroller(() => { const scroller = createScroller(() => {

View File

@ -1,22 +1,36 @@
<script lang="ts"> <script lang="ts">
import {last} from 'ramda' import {last} from 'ramda'
import {fly} from 'svelte/transition'
import {ellipsize} from 'hurdak/lib/hurdak' 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 {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" import {routes} from "src/app/ui"
export let person export let person
const {petnamePubkeys} = user
const addPetname = pubkey => {
const [{url}] = getPubkeyWriteRelays(pubkey)
user.addPetname(pubkey, url, person.name)
}
</script> </script>
<a <a
in:fly={{y: 20}}
href={routes.person(person.pubkey)} 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"> 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 <div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white" 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})" /> style="background-image: url({person.picture})" />
<div class="flex-grow"> <div class="flex-grow flex flex-col gap-4 min-w-0">
<div class="flex gap-2 items-center justify-between"> <div class="flex gap-2 items-start justify-between">
<h1 class="text-2xl">{displayPerson(person)}</h1> <div class="flex flex-col gap-2">
<h1 class="text-xl">{displayPerson(person)}</h1>
{#if person.verified_as} {#if person.verified_as}
<div class="flex gap-1 text-sm"> <div class="flex gap-1 text-sm">
<i class="fa fa-user-check text-accent" /> <i class="fa fa-user-check text-accent" />
@ -24,6 +38,18 @@
</div> </div>
{/if} {/if}
</div> </div>
<p>{@html renderContent(ellipsize(person.about || '', 140))}</p> {#if $petnamePubkeys.includes(person.pubkey)}
<Anchor type="button-accent" on:click={noEvent(() => user.removePetname(person.pubkey))}>
Following
</Anchor>
{:else}
<Anchor type="button" on:click={noEvent(() => addPetname(person.pubkey))}>
Follow
</Anchor>
{/if}
</div>
<p class="overflow-hidden text-ellipsis">
{@html renderContent(ellipsize(person.about || '', 140))}
</p>
</div> </div>
</a> </a>

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
import {nip19} from 'nostr-tools' import {nip19} from 'nostr-tools'
import {navigate} from "svelte-routing" import {navigate} from "svelte-routing"
import {fuzzy} from "src/util/misc" 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 network from 'src/agent/network'
import database from 'src/agent/database' import database from 'src/agent/database'
import {getUserReadRelays} from 'src/agent/relays' import {getUserReadRelays} from 'src/agent/relays'
@ -24,7 +24,7 @@
const rooms = database.watch(['rooms', 'messages'], async () => { const rooms = database.watch(['rooms', 'messages'], async () => {
const rooms = await database.rooms.all({joined: true}) const rooms = await database.rooms.all({joined: true})
const messages = await database.messages.all() 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) await network.loadPeople(pubkeys)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,6 +61,14 @@ export const killEvent = e => {
e.stopImmediatePropagation() e.stopImmediatePropagation()
} }
export const noEvent = f => e => {
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
f()
}
export const fromParentOffset = (element, offset): [HTMLElement, number] => { export const fromParentOffset = (element, offset): [HTMLElement, number] => {
for (const child of element.childNodes) { for (const child of element.childNodes) {
if (offset <= child.textContent.length) { 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 {nip19} from 'nostr-tools'
import {ensurePlural, ellipsize, first} from 'hurdak/lib/hurdak' 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 { export class Tags {
constructor(tags) { constructor(tags) {

View File

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

View File

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

View File

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