mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Re-work relays and connections
This commit is contained in:
parent
933e107500
commit
19ee631f19
16
README.md
16
README.md
@ -58,18 +58,24 @@ If you like Coracle and want to support its development, you can donate sats via
|
||||
- [x] Use user relays for feeds
|
||||
- [x] Publish to user relays + target relays:
|
||||
- [x] Add correct recommended relay to tags
|
||||
- [ ] Support some read/write config on relays page
|
||||
- [ ] Get real home relays for default pubkeys
|
||||
- [ ] Relays
|
||||
- [ ] Support some read/write config
|
||||
- [ ] Get real home relays for defaults.petnames
|
||||
- [ ] Add support for astral's relay hack (but don't publish to it)
|
||||
- [ ] Add settings storage on nostr, maybe use kind 0?
|
||||
- [ ] Warn that everything will be cleared on logout
|
||||
- [ ] Clean up login page to prefer extension, make private key entry "advanced"
|
||||
- [ ] Login
|
||||
- [ ] Prefer extension, make private key entry "advanced"
|
||||
- [ ] Improve login UX for bootstrap delay. Nostr facts?
|
||||
- [ ] Connection management
|
||||
- [ ] Do I need to implement re-connecting now?
|
||||
- [ ] Handle failed connections
|
||||
- [ ] Close connections that haven't been used in a while
|
||||
- [ ] Improve login UX for bootstrap delay. Nostr facts?
|
||||
- [ ] We often get the root as the reply, figure out why that is, compared to astral/damus
|
||||
- [ ] Load feeds from network rather than user relays? This could make global feed more useful: global for _my_ relays
|
||||
- [ ] Load feeds from network rather than user relays?
|
||||
- Still use "my" relays for global, this could make global feed more useful
|
||||
- [ ] Figure out migrations from previous version
|
||||
- [ ] Add relays/mentions to note and reply composition
|
||||
|
||||
## 0.2.7
|
||||
|
||||
|
@ -15,6 +15,7 @@
|
||||
import {modal, toast, settings, alerts} from "src/app"
|
||||
import {routes} from "src/app/ui"
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Modal from 'src/partials/Modal.svelte'
|
||||
import NoteDetailModal from "src/views/NoteDetail.svelte"
|
||||
import PersonSettings from "src/views/PersonSettings.svelte"
|
||||
@ -211,6 +212,9 @@
|
||||
<AddRelay />
|
||||
{:else if $modal.form === 'person/settings'}
|
||||
<PersonSettings />
|
||||
{:else if $modal.message}
|
||||
<p class="text-white text-center p-12">{$modal.message}</p>
|
||||
<Spinner />
|
||||
{/if}
|
||||
</Modal>
|
||||
{/if}
|
||||
|
@ -28,23 +28,35 @@ people.subscribe($p => {
|
||||
export const getPerson = (pubkey, fallback = false) =>
|
||||
$people[pubkey] || (fallback ? {pubkey} : null)
|
||||
|
||||
export const updatePeople = async updates => {
|
||||
// Sync to our in memory copy
|
||||
people.update($people => ({...$people, ...updates}))
|
||||
|
||||
// Sync to our database
|
||||
await db.people.bulkPut(Object.values(updates))
|
||||
}
|
||||
|
||||
// Hooks
|
||||
|
||||
export const processEvents = async events => {
|
||||
const profileEvents = ensurePlural(events)
|
||||
.filter(e => personKinds.includes(e.kind))
|
||||
|
||||
const profileUpdates = {}
|
||||
const updates = {}
|
||||
for (const e of profileEvents) {
|
||||
profileUpdates[e.pubkey] = {
|
||||
updates[e.pubkey] = {
|
||||
...getPerson(e.pubkey, true),
|
||||
...profileUpdates[e.pubkey],
|
||||
...updates[e.pubkey],
|
||||
...switcherFn(e.kind, {
|
||||
0: () => JSON.parse(e.content),
|
||||
2: () => ({relays: ($people[e.pubkey]?.relays || []).concat(e.content)}),
|
||||
2: () => ({
|
||||
relays: ($people[e.pubkey]?.relays || []).concat({url: e.content}),
|
||||
}),
|
||||
3: () => ({petnames: e.tags}),
|
||||
12165: () => ({muffle: e.tags}),
|
||||
10001: () => ({relays: e.tags.map(t => t[0])}),
|
||||
10001: () => ({
|
||||
relays: e.tags.map(([url, read, write]) => ({url, read, write})),
|
||||
}),
|
||||
default: () => {
|
||||
console.log(`Received unsupported event type ${event.kind}`)
|
||||
},
|
||||
@ -53,9 +65,5 @@ export const processEvents = async events => {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync to our in memory copy
|
||||
people.update($people => ({...$people, ...profileUpdates}))
|
||||
|
||||
// Sync to our database
|
||||
await db.people.bulkPut(Object.values(profileUpdates))
|
||||
await updatePeople(updates)
|
||||
}
|
||||
|
@ -1,15 +1,17 @@
|
||||
export default {
|
||||
petnames: [
|
||||
["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "fiatjaf", "wss://relay.damus.io"],
|
||||
["p", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", "fiatjaf", "wss://nostr-pub.wellorder.net"],
|
||||
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", "jb55", "wss://relay.damus.io"],
|
||||
["p", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "hodlbod", "wss://relay.damus.io"],
|
||||
["p", "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", "MartyBent", "wss://relay.damus.io"],
|
||||
["p", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "jack", "wss://relay.damus.io"],
|
||||
["p", "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", "hodlbod", "wss://nostr-pub.wellorder.net"],
|
||||
["p", "472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e", "MartyBent", "wss://relay.damus.io"],
|
||||
["p", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "jack", "wss://brb.io"],
|
||||
["p", "85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204", "preston", "wss://relay.damus.io"],
|
||||
],
|
||||
relays: [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nostr.zebedee.cloud',
|
||||
'wss://nostr-pub.wellorder.net',
|
||||
{url: 'wss://brb.io'},
|
||||
{url: 'wss://relay.damus.io'},
|
||||
{url: 'wss://nostr.zebedee.cloud'},
|
||||
{url: 'wss://nostr-relay.wlvs.space'},
|
||||
{url: 'wss://nostr-pub.wellorder.net'},
|
||||
],
|
||||
}
|
||||
|
@ -1,60 +1,94 @@
|
||||
import {relayInit} from 'nostr-tools'
|
||||
import {uniqBy, is, filter, identity, prop} from 'ramda'
|
||||
import {isRelay} from 'src/util/nostr'
|
||||
import {uniqBy, prop, find, whereEq, is, filter, identity} from 'ramda'
|
||||
import {ensurePlural} from 'hurdak/lib/hurdak'
|
||||
import {isRelay} from 'src/util/nostr'
|
||||
import {sleep} from 'src/util/misc'
|
||||
|
||||
const relays = {}
|
||||
const connections = []
|
||||
|
||||
const init = url => {
|
||||
const relay = relayInit(url)
|
||||
class Connection {
|
||||
constructor(url) {
|
||||
this.nostr = this.init(url)
|
||||
this.status = 'new'
|
||||
this.url = url
|
||||
this.stats = {
|
||||
count: 0,
|
||||
timer: 0,
|
||||
timeouts: 0,
|
||||
activeCount: 0,
|
||||
}
|
||||
|
||||
relay.url = url
|
||||
relay.stats = {
|
||||
count: 0,
|
||||
timer: 0,
|
||||
timeouts: 0,
|
||||
activeCount: 0,
|
||||
connections.push(this)
|
||||
}
|
||||
init(url) {
|
||||
const nostr = relayInit(url)
|
||||
|
||||
relay.on('error', () => {
|
||||
console.log(`failed to connect to ${url}`)
|
||||
})
|
||||
nostr.on('error', () => {
|
||||
console.log(`failed to connect to ${url}`)
|
||||
})
|
||||
|
||||
relay.on('disconnect', () => {
|
||||
delete relays[url]
|
||||
})
|
||||
nostr.on('disconnect', () => {
|
||||
delete connections[url]
|
||||
})
|
||||
|
||||
// Do initialization synchonously and wait on retrieval
|
||||
// so we don't open multiple connections simultaneously
|
||||
return relay.connect().then(
|
||||
() => relay,
|
||||
e => console.log(`Failed to connect to ${url}: ${e}`)
|
||||
)
|
||||
return nostr
|
||||
}
|
||||
async connect() {
|
||||
const shouldConnect = (
|
||||
this.status === 'new'
|
||||
|| (
|
||||
this.status === 'error'
|
||||
&& Date.now() - this.lastRequest > 30_000
|
||||
)
|
||||
)
|
||||
|
||||
if (shouldConnect) {
|
||||
this.status = 'pending'
|
||||
|
||||
try {
|
||||
await this.nostr.connect()
|
||||
this.status = 'ready'
|
||||
} catch (e) {
|
||||
console.error(`Failed to connect to ${this.url}: ${e}`)
|
||||
this.status = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
this.lastRequest = Date.now()
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const connect = url => {
|
||||
if (!relays[url]) {
|
||||
relays[url] = init(url)
|
||||
}
|
||||
const findConnection = url => find(whereEq({url}), connections)
|
||||
|
||||
return relays[url]
|
||||
const connect = async url => {
|
||||
const conn = findConnection(url) || new Connection(url)
|
||||
|
||||
await Promise.race([conn.connect(), sleep(5000)])
|
||||
|
||||
if (conn.status === 'ready') {
|
||||
return conn
|
||||
}
|
||||
}
|
||||
|
||||
const publish = async (urls, event) => {
|
||||
const publish = async (relays, event) => {
|
||||
return Promise.all(
|
||||
urls.filter(isRelay).map(async url => {
|
||||
const relay = await connect(url)
|
||||
relays.filter(r => r.read !== '!' & isRelay(r.url)).map(async relay => {
|
||||
const conn = await connect(relay.url)
|
||||
|
||||
if (relay) {
|
||||
return relay.publish(event)
|
||||
if (conn) {
|
||||
return conn.nostr.publish(event)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const describeFilter = filter => {
|
||||
const describeFilter = ({kinds = [], ...filter}) => {
|
||||
let parts = []
|
||||
|
||||
parts.push(kinds.join(','))
|
||||
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (is(Array, value)) {
|
||||
parts.push(`${key}[${value.length}]`)
|
||||
@ -63,10 +97,11 @@ const describeFilter = filter => {
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(',')
|
||||
return '(' + parts.join(',') + ')'
|
||||
}
|
||||
|
||||
const subscribe = async (urls, filters) => {
|
||||
const subscribe = async (relays, filters) => {
|
||||
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
|
||||
filters = ensurePlural(filters)
|
||||
|
||||
// Create a human readable subscription id for debugging
|
||||
@ -75,25 +110,22 @@ const subscribe = async (urls, filters) => {
|
||||
filters.map(describeFilter).join(':'),
|
||||
].join('-')
|
||||
|
||||
console.log(filters, id)
|
||||
|
||||
const subs = filter(identity, await Promise.all(
|
||||
urls.filter(isRelay).map(async url => {
|
||||
const relay = await connect(url)
|
||||
relays.map(async relay => {
|
||||
const conn = await connect(relay.url)
|
||||
|
||||
// If the relay failed to connect, give up
|
||||
if (!relay) {
|
||||
if (!conn) {
|
||||
return null
|
||||
}
|
||||
|
||||
const sub = conn.nostr.sub(filters, {id})
|
||||
|
||||
const sub = relay.sub(filters, {id})
|
||||
sub.conn = conn
|
||||
sub.conn.stats.activeCount += 1
|
||||
|
||||
sub.relay = relay
|
||||
sub.relay.stats.activeCount += 1
|
||||
|
||||
if (sub.relay.stats.activeCount > 10) {
|
||||
console.warn(`Relay ${url} has >10 active subscriptions`)
|
||||
if (sub.conn.stats.activeCount > 10) {
|
||||
console.warn(`Relay ${sub.url} has >10 active subscriptions`)
|
||||
}
|
||||
|
||||
return sub
|
||||
@ -103,17 +135,18 @@ const subscribe = async (urls, filters) => {
|
||||
const seen = new Set()
|
||||
|
||||
return {
|
||||
subs,
|
||||
unsub: () => {
|
||||
subs.forEach(sub => {
|
||||
sub.unsub()
|
||||
sub.relay.stats.activeCount -= 1
|
||||
sub.conn.stats.activeCount -= 1
|
||||
})
|
||||
},
|
||||
onEvent: cb => {
|
||||
subs.forEach(sub => {
|
||||
sub.on('event', e => {
|
||||
if (!seen.has(e.id)) {
|
||||
e.seen_on = sub.relay.url
|
||||
e.seen_on = sub.conn.url
|
||||
seen.add(e.id)
|
||||
cb(e)
|
||||
}
|
||||
@ -122,60 +155,69 @@ const subscribe = async (urls, filters) => {
|
||||
},
|
||||
onEose: cb => {
|
||||
subs.forEach(sub => {
|
||||
sub.on('eose', () => cb(sub.relay.url))
|
||||
sub.on('eose', () => cb(sub.conn.url))
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const request = (urls, filters) => {
|
||||
urls = urls.filter(isRelay)
|
||||
const request = (relays, filters) => {
|
||||
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
|
||||
|
||||
return new Promise(async resolve => {
|
||||
const subscription = await subscribe(urls, filters)
|
||||
const agg = await subscribe(relays, filters)
|
||||
const now = Date.now()
|
||||
const events = []
|
||||
const eose = []
|
||||
|
||||
const done = () => {
|
||||
subscription.unsub()
|
||||
const attemptToComplete = () => {
|
||||
// If we have all relays, most after a short timeout, or all after
|
||||
// a long timeout, go ahead and unsubscribe.
|
||||
const done = (
|
||||
eose.length === agg.subs.length
|
||||
|| Date.now() - now >= 5000
|
||||
|| (
|
||||
Date.now() - now >= 1000
|
||||
&& eose.length > agg.subs.length - Math.round(agg.subs.length / 10)
|
||||
)
|
||||
)
|
||||
|
||||
resolve(uniqBy(prop('id'), events))
|
||||
if (done) {
|
||||
agg.unsub()
|
||||
resolve(events)
|
||||
|
||||
// Keep track of relay timeouts
|
||||
urls.forEach(async url => {
|
||||
if (!eose.includes(url)) {
|
||||
const relay = await connect(url)
|
||||
// Keep track of relay timeouts
|
||||
agg.subs.forEach(async sub => {
|
||||
if (!eose.includes(sub.conn.url)) {
|
||||
const conn = findConnection(sub.conn.url)
|
||||
|
||||
// Relay may be undefined if we failed to connect
|
||||
if (relay) {
|
||||
relay.stats.count += 1
|
||||
relay.stats.timer += Date.now() - now
|
||||
relay.stats.timeouts += 1
|
||||
conn.stats.count += 1
|
||||
conn.stats.timer += Date.now() - now
|
||||
conn.stats.timeouts += 1
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
subscription.onEvent(e => events.push(e))
|
||||
agg.onEvent(e => events.push(e))
|
||||
|
||||
subscription.onEose(async url => {
|
||||
const relay = await relays[url]
|
||||
agg.onEose(async url => {
|
||||
const conn = findConnection(url)
|
||||
|
||||
eose.push(url)
|
||||
if (!eose.includes(url)) {
|
||||
eose.push(url)
|
||||
|
||||
// Keep track of relay timing stats
|
||||
relay.stats.count += 1
|
||||
relay.stats.timer += Date.now() - now
|
||||
|
||||
if (eose.length === urls.length) {
|
||||
done()
|
||||
// Keep track of relay timing stats
|
||||
conn.stats.count += 1
|
||||
conn.stats.timer += Date.now() - now
|
||||
}
|
||||
|
||||
attemptToComplete()
|
||||
})
|
||||
|
||||
// If a relay takes too long, give up
|
||||
setTimeout(done, 5000)
|
||||
setTimeout(attemptToComplete, 5000)
|
||||
})
|
||||
}
|
||||
|
||||
export default {relays, connect, publish, subscribe, request}
|
||||
export default {connect, publish, subscribe, request}
|
||||
|
@ -3,7 +3,7 @@ import {synced, batch, now} from 'src/util/misc'
|
||||
import {isAlert} from 'src/util/nostr'
|
||||
import {load as _load, listen as _listen, getMuffle, db} from 'src/agent'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import {threadify} from 'src/app'
|
||||
|
||||
let listener
|
||||
|
||||
@ -15,7 +15,7 @@ const onChunk = async (relays, pubkey, events) => {
|
||||
|
||||
if (events.length > 0) {
|
||||
const context = await loaders.loadContext(relays, events)
|
||||
const notes = query.threadify(events, context, {muffle: getMuffle()})
|
||||
const notes = threadify(events, context, {muffle: getMuffle()})
|
||||
|
||||
await db.alerts.bulkPut(notes)
|
||||
|
||||
|
@ -7,8 +7,8 @@ import {keys, publish, getRelays} from 'src/agent'
|
||||
const updateUser = (relays, updates) =>
|
||||
publishEvent(relays, 0, {content: JSON.stringify(updates)})
|
||||
|
||||
const setRelays = relays =>
|
||||
publishEvent(relays, 10001, {tags: relays.map(url => [url, "", ""])})
|
||||
const setRelays = (relays, tags) =>
|
||||
publishEvent(relays, 10001, {tags})
|
||||
|
||||
const setPetnames = (relays, petnames) =>
|
||||
publishEvent(relays, 3, {tags: petnames})
|
||||
|
133
src/app/index.js
133
src/app/index.js
@ -1,11 +1,14 @@
|
||||
import {without} from 'ramda'
|
||||
import {whereEq, sortBy, identity, when, assoc, reject} from 'ramda'
|
||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {get} from 'svelte/store'
|
||||
import {getPerson, getRelays, load, keys} from 'src/agent'
|
||||
import {toast, modal, settings} from 'src/app/ui'
|
||||
import {renderContent} from 'src/util/html'
|
||||
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
|
||||
import {user, people, getPerson, getRelays, load, keys} from 'src/agent'
|
||||
import defaults from 'src/agent/defaults'
|
||||
import {toast, routes, modal, settings} from 'src/app/ui'
|
||||
import cmd from 'src/app/cmd'
|
||||
import alerts from 'src/app/alerts'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export {toast, modal, settings, alerts}
|
||||
|
||||
@ -23,26 +26,49 @@ export const login = async ({privkey, pubkey}) => {
|
||||
])
|
||||
}
|
||||
|
||||
export const addRelay = async url => {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const person = getPerson(pubkey)
|
||||
const relays = (person?.relays || []).concat(url)
|
||||
export const addRelay = async relay => {
|
||||
const person = get(user)
|
||||
const modify = relays => relays.concat(relay)
|
||||
|
||||
await cmd.setRelays(relays)
|
||||
// Set to defaults to support anonymous usage
|
||||
defaults.relays = modify(defaults.relays)
|
||||
|
||||
await Promise.all([
|
||||
loaders.loadNetwork(getRelays(), pubkey),
|
||||
alerts.load(getRelays(), pubkey),
|
||||
alerts.listen(getRelays(), pubkey),
|
||||
])
|
||||
if (person) {
|
||||
const relays = modify(person.relays || [])
|
||||
|
||||
// Publish to the new set of relays
|
||||
await cmd.setRelays(relays, relays)
|
||||
|
||||
await Promise.all([
|
||||
loaders.loadNetwork(relays, person.pubkey),
|
||||
alerts.load(relays, person.pubkey),
|
||||
alerts.listen(relays, person.pubkey),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
export const removeRelay = async url => {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const person = getPerson(pubkey)
|
||||
const relays = person?.relays || []
|
||||
const person = get(user)
|
||||
const modify = relays => reject(whereEq({url}), relays)
|
||||
|
||||
await cmd.setRelays(without([url], relays))
|
||||
// Set to defaults to support anonymous usage
|
||||
defaults.relays = modify(defaults.relays)
|
||||
|
||||
if (person) {
|
||||
await cmd.setRelays(getRelays(), modify(person.relays || []))
|
||||
}
|
||||
}
|
||||
|
||||
export const setRelayWriteCondition = async (url, write) => {
|
||||
const person = get(user)
|
||||
const modify = relays => relays.map(when(whereEq({url}), assoc('write', write)))
|
||||
|
||||
// Set to defaults to support anonymous usage
|
||||
defaults.relays = modify(defaults.relays)
|
||||
|
||||
if (person) {
|
||||
await cmd.setRelays(getRelays(), modify(person.relays || []))
|
||||
}
|
||||
}
|
||||
|
||||
export const loadNote = async (relays, id) => {
|
||||
@ -53,10 +79,79 @@ export const loadNote = async (relays, id) => {
|
||||
}
|
||||
|
||||
const context = await loaders.loadContext(relays, found)
|
||||
const note = query.annotate(found, context, {showEntire: true, depth: 3})
|
||||
const note = annotate(found, context)
|
||||
|
||||
// Log this for debugging purposes
|
||||
console.log('loadNote', note)
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
export const render = (note, {showEntire = false}) => {
|
||||
const shouldEllipsize = note.content.length > 500 && !showEntire
|
||||
const $people = get(people)
|
||||
const peopleByPubkey = createMap(
|
||||
'pubkey',
|
||||
Tags.from(note).type("p").values().all().map(k => $people[k]).filter(identity)
|
||||
)
|
||||
|
||||
let content
|
||||
|
||||
// Ellipsize
|
||||
content = shouldEllipsize ? ellipsize(note.content, 500) : note.content
|
||||
|
||||
// Escape html, replace urls
|
||||
content = renderContent(content)
|
||||
|
||||
// Mentions
|
||||
content = content
|
||||
.replace(/#\[(\d+)\]/g, (tag, i) => {
|
||||
if (!note.tags[parseInt(i)]) {
|
||||
return tag
|
||||
}
|
||||
|
||||
const pubkey = note.tags[parseInt(i)][1]
|
||||
const person = peopleByPubkey[pubkey] || {pubkey}
|
||||
const name = displayPerson(person)
|
||||
const path = routes.person(pubkey)
|
||||
|
||||
return `@<a href="${path}" class="underline">${name}</a>`
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export const annotate = (note, context) => {
|
||||
const reactions = context.filter(e => e.kind === 7 && findReplyId(e) === note.id)
|
||||
const replies = context.filter(e => e.kind === 1 && findReplyId(e) === note.id)
|
||||
|
||||
return {
|
||||
...note, reactions,
|
||||
person: getPerson(note.pubkey),
|
||||
replies: sortBy(e => e.created_at, replies).map(r => annotate(r, context)),
|
||||
}
|
||||
}
|
||||
|
||||
export const threadify = (events, context, {muffle = []} = {}) => {
|
||||
const contextById = createMap('id', context)
|
||||
|
||||
// Show parents when possible. For reactions, if there's no parent,
|
||||
// throw it away. Sort by created date descending
|
||||
const notes = sortBy(
|
||||
e => -e.created_at,
|
||||
events
|
||||
.map(e => contextById[findReplyId(e)] || (e.kind === 1 ? e : null))
|
||||
.filter(e => e && !muffle.includes(e.pubkey))
|
||||
)
|
||||
|
||||
// Annotate our feed with parents, reactions, replies
|
||||
return notes.map(note => {
|
||||
let parent = contextById[findReplyId(note)]
|
||||
|
||||
if (parent) {
|
||||
parent = annotate(parent, context)
|
||||
}
|
||||
|
||||
return annotate({...note, parent}, context)
|
||||
})
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {uniq, flatten, pluck, groupBy, identity} from 'ramda'
|
||||
import {uniqBy, prop, uniq, flatten, pluck, groupBy, identity} from 'ramda'
|
||||
import {ensurePlural, createMap, chunk} from 'hurdak/lib/hurdak'
|
||||
import {findReply, personKinds, Tags, getTagValues} from 'src/util/nostr'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
@ -78,7 +78,13 @@ const loadContext = async (relays, notes, {loadParents = true} = {}) => {
|
||||
const parents = getTagValues(parentTags).map(id => eventsById[id]).filter(identity)
|
||||
const parentRelays = Tags.from(parents).relays()
|
||||
|
||||
return events.concat(await loadContext(parentRelays, parents, {loadParents: false}))
|
||||
// We're recurring and so may end up with duplicates here
|
||||
return uniqBy(
|
||||
prop('id'),
|
||||
events.concat(
|
||||
await loadContext(parentRelays, parents, {loadParents: false})
|
||||
)
|
||||
)
|
||||
})
|
||||
))
|
||||
}
|
||||
|
@ -1,84 +0,0 @@
|
||||
import {get} from 'svelte/store'
|
||||
import {sortBy, identity} from 'ramda'
|
||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent} from 'src/util/html'
|
||||
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
|
||||
import {people, getPerson} from 'src/agent'
|
||||
import {routes} from "src/app/ui"
|
||||
|
||||
const renderNote = (note, {showEntire = false}) => {
|
||||
const shouldEllipsize = note.content.length > 500 && !showEntire
|
||||
const $people = get(people)
|
||||
const peopleByPubkey = createMap(
|
||||
'pubkey',
|
||||
Tags.from(note).type("p").values().all().map(k => $people[k]).filter(identity)
|
||||
)
|
||||
|
||||
let content
|
||||
|
||||
// Ellipsize
|
||||
content = shouldEllipsize ? ellipsize(note.content, 500) : note.content
|
||||
|
||||
// Escape html, replace urls
|
||||
content = renderContent(content)
|
||||
|
||||
// Mentions
|
||||
content = content
|
||||
.replace(/#\[(\d+)\]/g, (tag, i) => {
|
||||
if (!note.tags[parseInt(i)]) {
|
||||
return tag
|
||||
}
|
||||
|
||||
const pubkey = note.tags[parseInt(i)][1]
|
||||
const person = peopleByPubkey[pubkey] || {pubkey}
|
||||
const name = displayPerson(person)
|
||||
const path = routes.person(pubkey)
|
||||
|
||||
return `@<a href="${path}" class="underline">${name}</a>`
|
||||
})
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const annotate = (note, context, {showEntire = false, depth = 1} = {}) => {
|
||||
const reactions = context.filter(e => e.kind === 7 && findReplyId(e) === note.id)
|
||||
const replies = context.filter(e => e.kind === 1 && findReplyId(e) === note.id)
|
||||
|
||||
return {
|
||||
...note, reactions,
|
||||
html: renderNote(note, {showEntire}),
|
||||
person: getPerson(note.pubkey),
|
||||
repliesCount: replies.length,
|
||||
replies: depth === 0
|
||||
? []
|
||||
: sortBy(e => e.created_at, replies)
|
||||
.slice(showEntire ? 0 : -3)
|
||||
.map(r => annotate(r, context, {depth: depth - 1}))
|
||||
}
|
||||
}
|
||||
|
||||
const threadify = (events, context, {muffle = []} = {}) => {
|
||||
const contextById = createMap('id', context)
|
||||
|
||||
// Show parents when possible. For reactions, if there's no parent,
|
||||
// throw it away. Sort by created date descending
|
||||
const notes = sortBy(
|
||||
e => -e.created_at,
|
||||
events
|
||||
.map(e => contextById[findReplyId(e)] || (e.kind === 1 ? e : null))
|
||||
.filter(e => e && !muffle.includes(e.pubkey))
|
||||
)
|
||||
|
||||
// Annotate our feed with parents, reactions, replies
|
||||
return notes.map(note => {
|
||||
let parent = contextById[findReplyId(note)]
|
||||
|
||||
if (parent) {
|
||||
parent = annotate(parent, context)
|
||||
}
|
||||
|
||||
return annotate({...note, parent}, context)
|
||||
})
|
||||
}
|
||||
|
||||
export default {renderNote, threadify, annotate}
|
@ -6,7 +6,7 @@
|
||||
|
||||
const className = cx(
|
||||
$$props.class,
|
||||
"rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full placeholder:text-medium",
|
||||
"rounded bg-light shadow-inset py-2 px-4 pr-10 text-black w-full placeholder:text-placeholder",
|
||||
{"pl-10": $$slots.before, "pr-10": $$slots.after},
|
||||
)
|
||||
</script>
|
||||
|
@ -2,7 +2,7 @@
|
||||
import cx from 'classnames'
|
||||
import extractUrls from 'extract-urls'
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {whereEq, reject, propEq, find} from 'ramda'
|
||||
import {whereEq, pluck, reject, propEq, find} from 'ramda'
|
||||
import {slide} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {quantify} from 'hurdak/lib/hurdak'
|
||||
@ -10,7 +10,7 @@
|
||||
import {findReply, findReplyId, isLike} from "src/util/nostr"
|
||||
import Preview from 'src/partials/Preview.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import {settings, modal} from "src/app"
|
||||
import {settings, modal, render} from "src/app"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import Compose from "src/partials/Compose.svelte"
|
||||
@ -27,7 +27,8 @@
|
||||
let reply = null
|
||||
|
||||
const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : null
|
||||
const interactive = !anchorId || anchorId !== note.id
|
||||
const showEntire = anchorId === note.id
|
||||
const interactive = !anchorId || !showEntire
|
||||
const relays = getEventRelays(note)
|
||||
|
||||
let likes, flags, like, flag
|
||||
@ -48,7 +49,7 @@
|
||||
|
||||
const goToParent = async () => {
|
||||
const [id, url] = findReply(note).slice(1)
|
||||
const relays = getEventRelays(note).concat(url)
|
||||
const relays = getEventRelays(note).concat({url})
|
||||
|
||||
modal.set({note: {id}, relays})
|
||||
}
|
||||
@ -117,7 +118,7 @@
|
||||
<div class="flex gap-4 items-center justify-between">
|
||||
<Badge person={getPerson(note.pubkey, true)} />
|
||||
<Anchor
|
||||
href={"/" + nip19.neventEncode({id: note.id, relays})}
|
||||
href={"/" + nip19.neventEncode({id: note.id, relays: pluck('url', relays)})}
|
||||
class="text-sm text-light"
|
||||
type="unstyled">
|
||||
{formatTimestamp(note.created_at)}
|
||||
@ -136,7 +137,7 @@
|
||||
</p>
|
||||
{:else}
|
||||
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
|
||||
<p>{@html note.html}</p>
|
||||
<p>{@html render(note, {showEntire})}</p>
|
||||
{#each links.slice(-2) as link}
|
||||
<div>
|
||||
<div class="inline-block" on:click={e => e.stopPropagation()}>
|
||||
@ -150,7 +151,7 @@
|
||||
<i
|
||||
class="fa-solid fa-reply cursor-pointer"
|
||||
on:click={startReply} />
|
||||
{note.repliesCount}
|
||||
{note.replies.length}
|
||||
</div>
|
||||
<div class={cx({'text-accent': like})}>
|
||||
<i
|
||||
@ -185,13 +186,13 @@
|
||||
|
||||
{#if depth > 0}
|
||||
<div class="ml-5 border-l border-solid border-medium">
|
||||
{#if note.repliesCount > 3 && note.replies.length < note.repliesCount}
|
||||
{#if !showEntire && note.replies.length > 3}
|
||||
<div class="ml-5 py-2 text-light cursor-pointer" on:click={onClick}>
|
||||
<i class="fa-solid fa-up-down text-sm pr-2" />
|
||||
Show {quantify(note.repliesCount - note.replies.length, 'other reply', 'more replies')}
|
||||
Show {quantify(note.replies.length - 3, 'other reply', 'more replies')}
|
||||
</div>
|
||||
{/if}
|
||||
{#each note.replies as r (r.id)}
|
||||
{#each note.replies.slice(showEntire ? 0 : -3) as r (r.id)}
|
||||
<svelte:self showParent={false} note={r} depth={depth - 1} {invertColors} {anchorId} />
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@
|
||||
import {createScroller} from 'src/util/misc'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import {modal} from "src/app"
|
||||
|
||||
export let loadNotes
|
||||
export let listenForNotes
|
||||
@ -30,6 +31,10 @@
|
||||
})
|
||||
|
||||
const scroller = createScroller(async () => {
|
||||
if ($modal) {
|
||||
return
|
||||
}
|
||||
|
||||
// Drop notes at the top if there are a lot
|
||||
notes = uniqBy(prop('id'), notes.concat(await loadNotes()).slice(-maxNotes))
|
||||
})
|
||||
|
@ -1,10 +1,14 @@
|
||||
<script>
|
||||
import Switch from "svelte-switch"
|
||||
import {createEventDispatcher} from 'svelte'
|
||||
|
||||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onChange = e => {
|
||||
value = e.detail.checked
|
||||
dispatch('change', value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script>
|
||||
import {objOf} from 'ramda'
|
||||
import {nip19} from 'nostr-tools'
|
||||
import NoteDetail from 'src/views/NoteDetail.svelte'
|
||||
import Person from 'src/routes/Person.svelte'
|
||||
@ -6,15 +7,16 @@
|
||||
export let entity
|
||||
|
||||
const {type, data} = nip19.decode(entity)
|
||||
const relays = (data.relays || []).map(objOf('url'))
|
||||
</script>
|
||||
|
||||
<div class="py-4 max-w-xl m-auto">
|
||||
{#if type === "nevent"}
|
||||
<NoteDetail note={{id: data.id}} relays={data.relays} />
|
||||
<NoteDetail note={{id: data.id}} {relays} />
|
||||
{:else if type === "note"}
|
||||
<NoteDetail note={{id: data}} />
|
||||
{:else if type === "nprofile"}
|
||||
<Person npub={nip19.npubEncode(data.pubkey)} relays={data.relays} activeTab="notes" />
|
||||
<Person npub={nip19.npubEncode(data.pubkey)} {relays} activeTab="notes" />
|
||||
{:else if type === "npub"}
|
||||
<Person npub={entity} activeTab="notes" />
|
||||
{/if}
|
||||
|
@ -34,9 +34,9 @@
|
||||
const logIn = async ({privkey, pubkey}) => {
|
||||
loading = true
|
||||
|
||||
await login({privkey, pubkey})
|
||||
login({privkey, pubkey})
|
||||
|
||||
navigate('/notes/network')
|
||||
navigate('/relays')
|
||||
}
|
||||
|
||||
const logInWithExtension = async () => {
|
||||
@ -64,7 +64,7 @@
|
||||
<div class="flex flex-col gap-4 max-w-2xl">
|
||||
<div class="flex justify-center items-center flex-col mb-4">
|
||||
<h1 class="staatliches text-6xl">Welcome!</h1>
|
||||
<i>To the Nostr Protocol Network</i>
|
||||
<i>To the Nostr Protocol</i>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>
|
||||
|
@ -59,7 +59,7 @@
|
||||
following = true
|
||||
|
||||
const relay = first(relays || getRelays(pubkey))
|
||||
const tag = ["p", pubkey, relay, person.name || ""]
|
||||
const tag = ["p", pubkey, relay.url, person.name || ""]
|
||||
const petnames = reject(t => t[1] === pubkey, $user.petnames).concat(tag)
|
||||
|
||||
cmd.setPetnames(getRelays(), petnames)
|
||||
|
@ -1,13 +1,14 @@
|
||||
<script>
|
||||
import {liveQuery} from 'dexie'
|
||||
import {without} from 'ramda'
|
||||
import {whereEq, find, last, reject} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Toggle from "src/partials/Toggle.svelte"
|
||||
import {db, user} from "src/agent"
|
||||
import {modal, addRelay, removeRelay, settings} from "src/app"
|
||||
import {modal, addRelay, removeRelay, setRelayWriteCondition, settings} from "src/app"
|
||||
import defaults from "src/agent/defaults"
|
||||
|
||||
let q = ""
|
||||
@ -22,8 +23,8 @@
|
||||
}
|
||||
})
|
||||
|
||||
for (const url of defaults.relays) {
|
||||
db.relays.put({url})
|
||||
for (const relay of defaults.relays) {
|
||||
db.relays.put(relay)
|
||||
}
|
||||
|
||||
const knownRelays = liveQuery(() => db.relays.toArray())
|
||||
@ -31,70 +32,80 @@
|
||||
$: search = fuzzy($knownRelays, {keys: ["name", "description", "url"]})
|
||||
|
||||
const join = url => {
|
||||
relays = relays.concat(url)
|
||||
relays = relays.concat({url, write: "!"})
|
||||
addRelay(url)
|
||||
|
||||
document.querySelector('input').select()
|
||||
}
|
||||
|
||||
const leave = url => {
|
||||
relays = without([url], relays)
|
||||
relays = reject(whereEq({url}), relays)
|
||||
removeRelay(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center py-8 px-4" in:fly={{y: 20}}>
|
||||
<div class="flex flex-col gap-8 max-w-2xl w-full">
|
||||
<div class="flex justify-center items-center flex-col mb-4">
|
||||
<h1 class="staatliches text-6xl">Get Connected</h1>
|
||||
<p>
|
||||
Relays are hubs for your content and connections. At least one is required to
|
||||
interact with the network, but you can join as many as you like.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6">
|
||||
<div class="flex flex-col gap-6 m-auto max-w-2xl py-12">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-2 items-center">
|
||||
<i class="fa fa-server fa-lg" />
|
||||
<h2 class="staatliches text-2xl">Your relays</h2>
|
||||
{#each ($knownRelays || []) as r}
|
||||
{#if relays.includes(r.url)}
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<strong>{r.name || r.url}</strong>
|
||||
<p class="text-light">{r.description || ''}</p>
|
||||
</div>
|
||||
<a class="underline cursor-pointer" on:click={() => leave(r.url)}>
|
||||
Leave
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if ($knownRelays || []).length > 0}
|
||||
<div class="pt-2 mb-2 border-b border-solid border-medium" />
|
||||
<h2 class="staatliches text-2xl">Other relays</h2>
|
||||
<div class="flex gap-4 items-center">
|
||||
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
|
||||
<i slot="before" class="fa-solid fa-search" />
|
||||
</Input>
|
||||
<Anchor type="button" on:click={() => modal.set({form: 'relay', url: q})}>
|
||||
<i class="fa-solid fa-plus" /> Add Relay
|
||||
</Anchor>
|
||||
</div>
|
||||
{/if}
|
||||
{#each (search(q) || []).slice(0, 50) as r}
|
||||
{#if !relays.includes(r.url)}
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<strong>{r.name || r.url}</strong>
|
||||
<p class="text-light">{r.description || ''}</p>
|
||||
</div>
|
||||
<a class="underline cursor-pointer" on:click={() => join(r.url)}>
|
||||
Join
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<small class="text-center">
|
||||
Showing {Math.min(($knownRelays || []).length, 50)} of {($knownRelays || []).length} known relays
|
||||
</small>
|
||||
</div>
|
||||
<Anchor type="button" on:click={() => modal.set({form: 'relay', url: q})}>
|
||||
<i class="fa-solid fa-plus" /> Add Relay
|
||||
</Anchor>
|
||||
</div>
|
||||
<p>
|
||||
Relays are hubs for your content and connections. At least one is required to
|
||||
interact with the network, but you can join as many as you like.
|
||||
</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{#each relays as {url, write}, i}
|
||||
<div class="rounded border border-solid border-medium bg-dark shadow" in:fly={{y: 20, delay: i * 100}}>
|
||||
<div class="flex flex-col gap-2 py-3 px-6">
|
||||
<div class="flex gap-2 items-center justify-between">
|
||||
<strong class="flex gap-2 items-center">
|
||||
<i class={url.startsWith('wss') ? "fa fa-lock" : "fa fa-unlock"} />
|
||||
{last(url.split('://'))}
|
||||
</strong>
|
||||
<i class="fa fa-times cursor-pointer" on:click={() => leave(url)}/>
|
||||
</div>
|
||||
<p class="text-light">placeholder for description</p>
|
||||
</div>
|
||||
<div class="border-b border-solid border-medium" />
|
||||
<div class="flex justify-between gap-2 py-3 px-6">
|
||||
<span>Publish to this relay?</span>
|
||||
<Toggle
|
||||
value={write !== "!"}
|
||||
on:change={() => setRelayWriteCondition(url, write === "!" ? "" : "!")} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if ($knownRelays || []).length > 0}
|
||||
<div class="pt-2 mb-2 border-b border-solid border-medium" />
|
||||
<div class="flex gap-2 items-center">
|
||||
<i class="fa fa-globe fa-lg" />
|
||||
<h2 class="staatliches text-2xl">Other relays</h2>
|
||||
</div>
|
||||
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
|
||||
<i slot="before" class="fa-solid fa-search" />
|
||||
</Input>
|
||||
{/if}
|
||||
{#each (search(q) || []).slice(0, 50) as {url, name, description}}
|
||||
{#if !find(whereEq({url}), relays)}
|
||||
<div class="flex gap-2 justify-between">
|
||||
<div>
|
||||
<strong>{name || url}</strong>
|
||||
<p class="text-light">{description || ''}</p>
|
||||
</div>
|
||||
<a class="underline cursor-pointer" on:click={() => join(url)}>
|
||||
Join
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<small class="text-center">
|
||||
Showing {Math.min(($knownRelays || []).length - relays.length, 50)}
|
||||
of {($knownRelays || []).length - relays.length} known relays
|
||||
</small>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {last, prop, flatten, uniq} from 'ramda'
|
||||
import {last, identity, objOf, prop, flatten, uniq} from 'ramda'
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {ensurePlural, first} from 'hurdak/lib/hurdak'
|
||||
|
||||
@ -12,7 +12,7 @@ export class Tags {
|
||||
return new Tags(ensurePlural(events).flatMap(prop('tags')))
|
||||
}
|
||||
static wrap(tags) {
|
||||
return new Tags(tags)
|
||||
return new Tags(tags.filter(identity))
|
||||
}
|
||||
all() {
|
||||
return this.tags
|
||||
@ -24,7 +24,7 @@ export class Tags {
|
||||
return last(this.tags)
|
||||
}
|
||||
relays() {
|
||||
return uniq(flatten(this.tags).filter(isRelay))
|
||||
return uniq(flatten(this.tags).filter(isRelay)).map(objOf('url'))
|
||||
}
|
||||
values() {
|
||||
this.tags = this.tags.map(t => t[1])
|
||||
@ -49,12 +49,12 @@ export const getTagValues = tags => tags.map(t => t[1])
|
||||
export const findReply = e =>
|
||||
Tags.from(e).type("e").mark("reply").first() || Tags.from(e).type("e").first()
|
||||
|
||||
export const findReplyId = e => first(Tags.wrap(findReply(e)).values())
|
||||
export const findReplyId = e => Tags.wrap([findReply(e)]).values().first()
|
||||
|
||||
export const findRoot = e =>
|
||||
Tags.from(e).type("e").mark("root").first()
|
||||
|
||||
export const findRootId = e => first(Tags.wrap(findRoot(e)).values())
|
||||
export const findRootId = e => Tags.wrap([findRoot(e)]).values().first()
|
||||
|
||||
export const displayPerson = p => {
|
||||
if (p.name) {
|
||||
@ -81,4 +81,3 @@ export const isAlert = (e, pubkey) => {
|
||||
}
|
||||
|
||||
export const isRelay = url => typeof url === 'string' && url.match(/^wss?:\/\/.+/)
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {loadNote} from 'src/app'
|
||||
import {getRelays} from 'src/agent'
|
||||
import Note from 'src/partials/Note.svelte'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
|
||||
export let note
|
||||
export let relays = getRelays()
|
||||
export let relays
|
||||
|
||||
if (!note.pubkey) {
|
||||
loadNote(relays, note.id).then(found => {
|
||||
|
@ -1,37 +1 @@
|
||||
<script>
|
||||
import {take} from 'ramda'
|
||||
import {onMount} from 'svelte'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {getTagValues} from "src/util/nostr"
|
||||
import Note from "src/partials/Note.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import {user} from 'src/agent'
|
||||
import query from 'src/app/query'
|
||||
|
||||
export let q
|
||||
|
||||
let search
|
||||
|
||||
onMount(async () => {
|
||||
const filter = {kinds: [1], muffle: getTagValues($user?.muffle || [])}
|
||||
|
||||
search = fuzzy(take(5000, await query.filterEvents(filter)), {keys: ["content"]})
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if search}
|
||||
<ul class="py-8 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#await Promise.all(search(q).slice(0, 30).map(n => query.findNote(n.id)))}
|
||||
<Spinner />
|
||||
{:then results}
|
||||
{#each results as e (e.id)}
|
||||
<li in:fly={{y: 20}}>
|
||||
<Note note={e} />
|
||||
</li>
|
||||
{/each}
|
||||
{/await}
|
||||
</ul>
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/if}
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {Cursor, now, batch} from 'src/util/misc'
|
||||
import {getRelays, getMuffle, listen, load} from 'src/agent'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import {threadify} from 'src/app'
|
||||
|
||||
const relays = getRelays()
|
||||
const filter = {kinds: [1, 5, 7]}
|
||||
@ -13,7 +13,7 @@
|
||||
listen(relays, {...filter, since: now()}, batch(300, async notes => {
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
onNotes(query.threadify(notes, context, {muffle: getMuffle()}))
|
||||
onNotes(threadify(notes, context, {muffle: getMuffle()}))
|
||||
}))
|
||||
|
||||
const loadNotes = async () => {
|
||||
@ -21,11 +21,7 @@
|
||||
const notes = await load(relays, {...filter, limit, until})
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
console.log('========')
|
||||
console.log({notes, context})
|
||||
console.log(query.threadify(notes, context, {muffle: getMuffle()}))
|
||||
|
||||
return query.threadify(notes, context, {muffle: getMuffle()})
|
||||
return threadify(notes, context, {muffle: getMuffle()})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {now, Cursor, shuffle, batch} from 'src/util/misc'
|
||||
import {user, getRelays, getFollows, getMuffle, listen, load} from 'src/agent'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import {threadify} from 'src/app'
|
||||
|
||||
// Get first- and second-order follows. shuffle and slice network so we're not
|
||||
// sending too many pubkeys. This will also result in some variety.
|
||||
@ -18,7 +18,7 @@
|
||||
listen(relays, {...filter, since: now()}, batch(300, async notes => {
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
onNotes(query.threadify(notes, context, {muffle: getMuffle()}))
|
||||
onNotes(threadify(notes, context, {muffle: getMuffle()}))
|
||||
}))
|
||||
|
||||
const loadNotes = async () => {
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
cursor.onChunk(notes)
|
||||
|
||||
return query.threadify(notes, context, {muffle: getMuffle()})
|
||||
return threadify(notes, context, {muffle: getMuffle()})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {now, batch, Cursor} from 'src/util/misc'
|
||||
import {load, listen, getRelays, getMuffle} from 'src/agent'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import {threadify} from 'src/app'
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
listen(relays, {...filter, since: now()}, batch(300, async notes => {
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
onNotes(query.threadify(notes, context, {muffle: getMuffle()}))
|
||||
onNotes(threadify(notes, context, {muffle: getMuffle()}))
|
||||
}))
|
||||
|
||||
const loadNotes = async () => {
|
||||
@ -23,7 +23,7 @@
|
||||
const notes = await load(relays, {...filter, limit, until})
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
return query.threadify(notes, context, {muffle: getMuffle()})
|
||||
return threadify(notes, context, {muffle: getMuffle()})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {now, shuffle, batch, Cursor} from 'src/util/misc'
|
||||
import {getRelays, getFollows, getMuffle, listen, load} from 'src/agent'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import {threadify} from 'src/app'
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
listen(relays, {...filter, since: now()}, batch(300, async notes => {
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
onNotes(query.threadify(notes, context, {muffle: getMuffle()}))
|
||||
onNotes(threadify(notes, context, {muffle: getMuffle()}))
|
||||
}))
|
||||
|
||||
const loadNotes = async () => {
|
||||
@ -26,7 +26,7 @@
|
||||
const notes = await load(relays, {...filter, limit, until})
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
return query.threadify(notes, context, {muffle: getMuffle()})
|
||||
return threadify(notes, context, {muffle: getMuffle()})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {now, batch, Cursor} from 'src/util/misc'
|
||||
import {load, listen, getRelays, getMuffle} from 'src/agent'
|
||||
import loaders from 'src/app/loaders'
|
||||
import query from 'src/app/query'
|
||||
import {threadify} from 'src/app'
|
||||
|
||||
export let pubkey
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
listen(relays, {...filter, since: now()}, batch(300, async notes => {
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
onNotes(query.threadify(notes, context, {muffle: getMuffle()}))
|
||||
onNotes(threadify(notes, context, {muffle: getMuffle()}))
|
||||
}))
|
||||
|
||||
const loadNotes = async () => {
|
||||
@ -23,7 +23,7 @@
|
||||
const notes = await load(relays, {...filter, limit, until})
|
||||
const context = await loaders.loadContext(relays, notes)
|
||||
|
||||
return query.threadify(notes, context, {muffle: getMuffle()})
|
||||
return threadify(notes, context, {muffle: getMuffle()})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -15,6 +15,7 @@ module.exports = {
|
||||
medium: "#403D39",
|
||||
dark: "#252422",
|
||||
danger: "#ff0000",
|
||||
placeholder: "#a19989",
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
Loading…
Reference in New Issue
Block a user