mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-19 11:43:35 +00:00
Re-work relays again
This commit is contained in:
parent
25511fdde4
commit
12506c015b
18
ROADMAP.md
18
ROADMAP.md
@ -1,13 +1,11 @@
|
|||||||
# Current
|
# Current
|
||||||
|
|
||||||
- [ ] Review 10002 usage https://github.com/nostr-protocol/nips/blob/master/65.md
|
- [ ] Review 10002 usage https://github.com/nostr-protocol/nips/blob/master/65.md
|
||||||
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP
|
- [ ] Remove relays from people, pull from routes only
|
||||||
- nevent1qqspjcqw2hu5gfcpkrjhs0aqvxuzjgtp50l375mcqjfpmk48cg5hevgpr3mhxue69uhkummnw3ez6un9d3shjtnhd3m8xtnnwpskxegpzamhxue69uhkummnw3ezuendwsh8w6t69e3xj7spramhxue69uhkummnw3ez6un9d3shjtnwdahxxefwv93kzer9d4usz9rhwden5te0wfjkccte9ejxzmt4wvhxjmcpr9mhxue69uhkummnw3ezuer9d3hjuum0ve68wctjv5n8hwfg
|
|
||||||
- [ ] Click through on relays page to view a feed for only that relay.
|
|
||||||
- [ ] Custom views should combine pubkeys, relays, event ids, and topics
|
|
||||||
|
|
||||||
- [ ] Fix anon/new user experience
|
- [ ] Fix anon/new user experience
|
||||||
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
|
- [ ] Clicking stuff that would publish kicks you to the login page, we should open a modal instead.
|
||||||
|
- [ ] Separate user info and relays so we can still select/figure out relays for anons
|
||||||
|
- [ ] Separate petnames out as well so anons can follow people
|
||||||
- [ ] 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
|
||||||
- [ ] Fix bugs on bugsnag
|
- [ ] Fix bugs on bugsnag
|
||||||
|
|
||||||
@ -26,14 +24,21 @@
|
|||||||
- nevent1qqsyyxtrhpsqeqaqgucd6uzpyh8eq2hkfgr0yzr7ku7tgyl5cn9jw5qpz3mhxue69uhhyetvv9ujumn0wd68ytnzvuq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7l564wx
|
- nevent1qqsyyxtrhpsqeqaqgucd6uzpyh8eq2hkfgr0yzr7ku7tgyl5cn9jw5qpz3mhxue69uhhyetvv9ujumn0wd68ytnzvuq3gamnwvaz7tmjv4kxz7fwv3sk6atn9e5k7l564wx
|
||||||
- [ ] Search by nip05 alias
|
- [ ] Search by nip05 alias
|
||||||
- nevent1qqsdt4ux9c0zvd6hzpwnzznjsmd7a337mpxdspu9wd4fq8drvqejdmqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsffemjp
|
- nevent1qqsdt4ux9c0zvd6hzpwnzznjsmd7a337mpxdspu9wd4fq8drvqejdmqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsffemjp
|
||||||
|
- [ ] Show options on note detail for retrieving replies
|
||||||
|
- Replies from user's network
|
||||||
|
- All replies from author's + user's read relays, including spam
|
||||||
|
|
||||||
# Missions
|
# Missions
|
||||||
|
|
||||||
- [ ] Are write relays the only ones that matter? User read relays only matter for global feed, or where there's no relay hints available. But if relays are navigable, this is unnecessary.
|
- [ ] Make feeds page customizable. This could potentially use the "lists" NIP
|
||||||
|
- nevent1qqspjcqw2hu5gfcpkrjhs0aqvxuzjgtp50l375mcqjfpmk48cg5hevgpr3mhxue69uhkummnw3ez6un9d3shjtnhd3m8xtnnwpskxegpzamhxue69uhkummnw3ezuendwsh8w6t69e3xj7spramhxue69uhkummnw3ez6un9d3shjtnwdahxxefwv93kzer9d4usz9rhwden5te0wfjkccte9ejxzmt4wvhxjmcpr9mhxue69uhkummnw3ezuer9d3hjuum0ve68wctjv5n8hwfg
|
||||||
|
- [ ] Click through on relays page to view a feed for only that relay.
|
||||||
|
- [ ] Custom views should combine pubkeys, relays, event ids, and topics
|
||||||
- [ ] Topics/hashtag views
|
- [ ] Topics/hashtag views
|
||||||
- [ ] Support paid relays
|
- [ ] Support paid relays
|
||||||
- atlas.nostr.land
|
- atlas.nostr.land
|
||||||
- eden.nostr.land
|
- eden.nostr.land
|
||||||
|
- [ ] Re-license using https://polyformproject.org/
|
||||||
- [ ] Image uploads
|
- [ ] Image uploads
|
||||||
- Default will charge via lightning and have a tos, others can self-host and skip that.
|
- Default will charge via lightning and have a tos, others can self-host and skip that.
|
||||||
- Add banner field to profile
|
- Add banner field to profile
|
||||||
@ -55,6 +60,7 @@
|
|||||||
|
|
||||||
# Maintenance
|
# Maintenance
|
||||||
|
|
||||||
|
- [ ] Keep track of all relays an event was seen on
|
||||||
- [ ] Don't waste space caching rooms, load those lazily
|
- [ ] Don't waste space caching rooms, load those lazily
|
||||||
- [ ] Normalize relay urls (lowercase, strip trailing slash)
|
- [ ] Normalize relay urls (lowercase, strip trailing slash)
|
||||||
- [ ] Use nip 56 for reporting
|
- [ ] Use nip 56 for reporting
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
import {displayPerson, isLike} from 'src/util/nostr'
|
import {displayPerson, isLike} from 'src/util/nostr'
|
||||||
import {timedelta, shuffle, now, sleep} from 'src/util/misc'
|
import {timedelta, shuffle, now, sleep} from 'src/util/misc'
|
||||||
import cmd from 'src/agent/cmd'
|
import cmd from 'src/agent/cmd'
|
||||||
import {user, getUserRelays} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
import {getUserRelays} from 'src/agent/relays'
|
||||||
import database from 'src/agent/database'
|
import database from 'src/agent/database'
|
||||||
import keys from 'src/agent/keys'
|
import keys from 'src/agent/keys'
|
||||||
import network from 'src/agent/network'
|
import network from 'src/agent/network'
|
||||||
@ -100,7 +101,7 @@
|
|||||||
|
|
||||||
const alertSlowConnections = () => {
|
const alertSlowConnections = () => {
|
||||||
// Only notify about relays the user is actually subscribed to
|
// Only notify about relays the user is actually subscribed to
|
||||||
const relayUrls = pluck('url', getUserRelays('read'))
|
const relayUrls = pluck('url', getUserRelays())
|
||||||
|
|
||||||
// Prune connections we haven't used in a while
|
// Prune connections we haven't used in a while
|
||||||
pool.getConnections()
|
pool.getConnections()
|
||||||
|
@ -4,7 +4,7 @@ import {get} from 'svelte/store'
|
|||||||
import {first} from "hurdak/lib/hurdak"
|
import {first} from "hurdak/lib/hurdak"
|
||||||
import {log} from 'src/util/logger'
|
import {log} from 'src/util/logger'
|
||||||
import {roomAttrs, displayPerson} from 'src/util/nostr'
|
import {roomAttrs, displayPerson} from 'src/util/nostr'
|
||||||
import {getBestRelay} from 'src/agent/helpers'
|
import {getPubkeyWriteRelays, getRelayForPersonHint} 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'
|
||||||
@ -36,7 +36,7 @@ const createDirectMessage = (relays, pubkey, content) =>
|
|||||||
const createNote = (relays, content, mentions = [], topics = []) => {
|
const createNote = (relays, content, mentions = [], topics = []) => {
|
||||||
mentions = mentions.map(pubkey => {
|
mentions = mentions.map(pubkey => {
|
||||||
const name = displayPerson(database.getPersonWithFallback(pubkey))
|
const name = displayPerson(database.getPersonWithFallback(pubkey))
|
||||||
const {url} = getBestRelay(pubkey, 'write')
|
const [{url}] = getPubkeyWriteRelays(pubkey)
|
||||||
|
|
||||||
return ["p", pubkey, url, name]
|
return ["p", pubkey, url, name]
|
||||||
})
|
})
|
||||||
@ -47,7 +47,7 @@ const createNote = (relays, content, mentions = [], topics = []) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createReaction = (relays, note, content) => {
|
const createReaction = (relays, note, content) => {
|
||||||
const {url} = getBestRelay(note.pubkey, 'write')
|
const {url} = getRelayForPersonHint(note.pubkey, note)
|
||||||
const tags = uniqBy(
|
const tags = uniqBy(
|
||||||
join(':'),
|
join(':'),
|
||||||
note.tags
|
note.tags
|
||||||
@ -60,10 +60,14 @@ const createReaction = (relays, note, content) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createReply = (relays, note, content, mentions = [], topics = []) => {
|
const createReply = (relays, note, content, mentions = [], topics = []) => {
|
||||||
mentions = mentions.map(pubkey => ["p", pubkey, prop('url', getBestRelay(pubkey))])
|
|
||||||
topics = topics.map(t => ["t", t])
|
topics = topics.map(t => ["t", t])
|
||||||
|
mentions = mentions.map(pubkey => {
|
||||||
|
const [{url}] = getRelayForPersonHint(pubkey, note)
|
||||||
|
|
||||||
const {url} = getBestRelay(note.pubkey, 'write')
|
return ["p", pubkey, url]
|
||||||
|
})
|
||||||
|
|
||||||
|
const [{url}] = getRelayForPersonHint(note.pubkey, note)
|
||||||
const tags = uniqBy(
|
const tags = uniqBy(
|
||||||
join(':'),
|
join(':'),
|
||||||
note.tags
|
note.tags
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
import type {Person, MyEvent} from 'src/util/types'
|
import type {Person} from 'src/util/types'
|
||||||
import type {Readable} from 'svelte/store'
|
import type {Readable} from 'svelte/store'
|
||||||
import {isEmpty, pick, identity, sortBy, uniq, reject, groupBy, last, propEq, uniqBy, prop} from 'ramda'
|
import {uniq, last} from 'ramda'
|
||||||
import {first, ensurePlural} from 'hurdak/lib/hurdak'
|
|
||||||
import {derived, get} from 'svelte/store'
|
import {derived, get} from 'svelte/store'
|
||||||
import {Tags} from 'src/util/nostr'
|
import {Tags} from 'src/util/nostr'
|
||||||
import {now, timedelta} from 'src/util/misc'
|
import {now, timedelta} from 'src/util/misc'
|
||||||
import defaults from 'src/agent/defaults'
|
|
||||||
import database from 'src/agent/database'
|
import database from 'src/agent/database'
|
||||||
import keys from 'src/agent/keys'
|
import keys from 'src/agent/keys'
|
||||||
|
|
||||||
@ -33,50 +31,10 @@ export const getMuffle = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getFollows = pubkey =>
|
export const getFollows = pubkey =>
|
||||||
database.getPersonWithFallback(pubkey).petnames || defaults.petnames
|
Tags.wrap(database.getPersonWithFallback(pubkey).petnames).type("p").values().all()
|
||||||
|
|
||||||
export const getPersonRelays = (person, mode = 'all') => {
|
export const getNetwork = pubkey =>
|
||||||
const relays = isEmpty(person?.relays || []) ? defaults.relays : person.relays
|
uniq(getFollows(pubkey).flatMap(getFollows))
|
||||||
|
|
||||||
return reject(propEq(mode, '!'), relays)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getUserRelays = (mode = 'all') =>
|
|
||||||
getPersonRelays(get(user), mode)
|
|
||||||
|
|
||||||
export const getPubkeyRelays = (pubkey, mode = 'all') =>
|
|
||||||
getPersonRelays(database.people.get(pubkey), mode)
|
|
||||||
|
|
||||||
export const getTopRelays = (pubkeys, mode = 'all') => {
|
|
||||||
const routes = database.routes.all({mode, pubkey: pubkeys})
|
|
||||||
const routesByPubkey = groupBy(prop('pubkey'), routes)
|
|
||||||
const selectRoute = k => first(sortBy(prop('score'), routesByPubkey[k] || []))
|
|
||||||
|
|
||||||
return uniqBy(prop('url'), pubkeys.map(selectRoute).filter(identity)).map(pick(['url']))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getBestRelay = (pubkey, mode = 'all') =>
|
|
||||||
first(getTopRelays([pubkey], mode).concat(getPubkeyRelays(pubkey, mode)))
|
|
||||||
|
|
||||||
export const getAllEventRelays = events => {
|
|
||||||
return uniqBy(
|
|
||||||
prop('url'),
|
|
||||||
ensurePlural(events)
|
|
||||||
.flatMap(event =>
|
|
||||||
getPubkeyRelays(event.pubkey, 'write')
|
|
||||||
.concat(Tags.from(event).relays())
|
|
||||||
.concat({url: event.seen_on})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTopEventRelays = (events: Array<MyEvent>, mode = 'all') =>
|
|
||||||
uniqBy(
|
|
||||||
prop('url'),
|
|
||||||
ensurePlural(events)
|
|
||||||
.flatMap(e => [getBestRelay(e.pubkey, mode), {url: e.seen_on}])
|
|
||||||
.filter(identity)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const getStalePubkeys = pubkeys => {
|
export const getStalePubkeys = 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
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
|
import {uniq, uniqBy, prop, map, propEq, indexBy, pluck} from 'ramda'
|
||||||
import {findReply, personKinds, findReplyId, Tags} 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} from 'src/util/misc'
|
import {batch} from 'src/util/misc'
|
||||||
import {getStalePubkeys, getTopEventRelays} from 'src/agent/helpers'
|
import {getStalePubkeys} from 'src/agent/helpers'
|
||||||
|
import {
|
||||||
|
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
|
||||||
|
getUserNetworkWriteRelays,
|
||||||
|
} from 'src/agent/relays'
|
||||||
import pool from 'src/agent/pool'
|
import pool from 'src/agent/pool'
|
||||||
import keys from 'src/agent/keys'
|
import keys from 'src/agent/keys'
|
||||||
import sync from 'src/agent/sync'
|
import sync from 'src/agent/sync'
|
||||||
@ -57,7 +61,7 @@ const listenUntilEose = (relays, filter, onEvents, {shouldProcess = true}: any =
|
|||||||
}) as Promise<void>
|
}) as Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadPeople = (relays, pubkeys, {kinds = personKinds, force = false, ...opts} = {}) => {
|
const loadPeople = (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
|
||||||
@ -65,25 +69,27 @@ const loadPeople = (relays, pubkeys, {kinds = personKinds, force = false, ...opt
|
|||||||
pubkeys = getStalePubkeys(pubkeys)
|
pubkeys = getStalePubkeys(pubkeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pubkeys.length > 0
|
|
||||||
? load(relays, {kinds, authors: pubkeys}, opts)
|
|
||||||
: Promise.resolve([])
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadParents = (relays, notes) => {
|
|
||||||
const parentIds = new Set(Tags.wrap(notes.map(findReply)).values().all())
|
|
||||||
|
|
||||||
if (parentIds.size === 0) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return load(
|
return load(
|
||||||
relays.concat(getTopEventRelays(notes, 'read')),
|
getAllPubkeyWriteRelays(pubkeys).slice(0, 10),
|
||||||
{kinds: [1], ids: Array.from(parentIds)}
|
{kinds, authors: pubkeys},
|
||||||
|
opts
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamContext = ({relays, notes, updateNotes, depth = 0}) => {
|
const loadParents = notes => {
|
||||||
|
const notesWithParent = notes.filter(findReplyId)
|
||||||
|
const relays = aggregateScores(notesWithParent.map(getRelaysForEventParent))
|
||||||
|
|
||||||
|
return load(relays, {kinds: [1], ids: notesWithParent.map(findReplyId)})
|
||||||
|
}
|
||||||
|
|
||||||
|
const streamContext = ({notes, updateNotes, depth = 0}) => {
|
||||||
|
// We could also use getRelaysForEventChildren for a more complete view of replies,
|
||||||
|
// but it's also more likely to include spam. Checking our user's social graph
|
||||||
|
// avoids this problem. TODO: review this, maybe add note authors's graphs to this
|
||||||
|
// as well.
|
||||||
|
const relays = getUserNetworkWriteRelays()
|
||||||
|
|
||||||
// Some relays reject very large filters, send multiple
|
// Some relays reject very large filters, send multiple
|
||||||
chunk(256, notes).forEach(chunk => {
|
chunk(256, notes).forEach(chunk => {
|
||||||
const authors = getStalePubkeys(pluck('pubkey', chunk))
|
const authors = getStalePubkeys(pluck('pubkey', chunk))
|
||||||
@ -99,7 +105,7 @@ const streamContext = ({relays, notes, updateNotes, depth = 0}) => {
|
|||||||
|
|
||||||
// Recur if we need to
|
// Recur if we need to
|
||||||
if (depth > 0) {
|
if (depth > 0) {
|
||||||
streamContext({relays, notes: events, updateNotes, depth: depth - 1})
|
streamContext({notes: events, updateNotes, depth: depth - 1})
|
||||||
}
|
}
|
||||||
|
|
||||||
const annotate = ({replies = [], reactions = [], children = [], ...note}) => {
|
const annotate = ({replies = [], reactions = [], children = [], ...note}) => {
|
||||||
|
152
src/agent/relays.ts
Normal file
152
src/agent/relays.ts
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import {get} from 'svelte/store'
|
||||||
|
import {sortBy, uniq, uniqBy, prop, pluck} from 'ramda'
|
||||||
|
import {createMapOf, first} from 'hurdak/lib/hurdak'
|
||||||
|
import {Tags} from 'src/util/nostr'
|
||||||
|
import {getFollows} from 'src/agent/helpers'
|
||||||
|
import database from 'src/agent/database'
|
||||||
|
import keys from 'src/agent/keys'
|
||||||
|
|
||||||
|
// From Mike Dilger:
|
||||||
|
// 1) Other people's write relays — pull events from people you follow,
|
||||||
|
// including their contact lists
|
||||||
|
// 2) Other people's read relays — push events that tag them (replies or just tagging).
|
||||||
|
// However, these may be authenticated, use with caution
|
||||||
|
// 3) Your write relays —- write events you post to your microblog feed for the
|
||||||
|
// world to see. ALSO write your contact list. ALSO read back your own contact list.
|
||||||
|
// 4) Your read relays —- read events that tag you. ALSO both write and read
|
||||||
|
// client-private data like client configuration events or anything that the world
|
||||||
|
// doesn't need to see.
|
||||||
|
// 5) Advertise relays — write and read back your own relay list
|
||||||
|
|
||||||
|
|
||||||
|
// Pubkey relays
|
||||||
|
|
||||||
|
export const getPubkeyRelays = pubkey => {
|
||||||
|
const person = database.getPersonWithFallback(pubkey)
|
||||||
|
|
||||||
|
return scoreRelays(pubkey, person.relays || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPubkeyReadRelays = pubkey =>
|
||||||
|
getPubkeyRelays(pubkey).filter(r => r.read !== '!')
|
||||||
|
|
||||||
|
export const getPubkeyWriteRelays = pubkey =>
|
||||||
|
getPubkeyRelays(pubkey).filter(r => r.write !== '!')
|
||||||
|
|
||||||
|
// Multiple pubkeys
|
||||||
|
|
||||||
|
export const getAllPubkeyRelays = pubkeys =>
|
||||||
|
aggregateScores(pubkeys.map(getPubkeyRelays))
|
||||||
|
|
||||||
|
export const getAllPubkeyReadRelays = pubkeys =>
|
||||||
|
aggregateScores(pubkeys.map(getPubkeyReadRelays))
|
||||||
|
|
||||||
|
export const getAllPubkeyWriteRelays = pubkeys =>
|
||||||
|
aggregateScores(pubkeys.map(getPubkeyWriteRelays))
|
||||||
|
|
||||||
|
// Current user
|
||||||
|
|
||||||
|
export const getUserRelays = () => getPubkeyRelays(get(keys.pubkey))
|
||||||
|
export const getUserReadRelays = () => getPubkeyReadRelays(get(keys.pubkey))
|
||||||
|
export const getUserWriteRelays = () => getPubkeyWriteRelays(get(keys.pubkey))
|
||||||
|
|
||||||
|
// Network relays
|
||||||
|
|
||||||
|
export const getNetworkWriteRelays = pubkey => {
|
||||||
|
const follows = Tags.wrap(getFollows(pubkey)).values().all()
|
||||||
|
const others = Tags.wrap(follows.flatMap(getFollows)).values().all()
|
||||||
|
|
||||||
|
return getAllPubkeyWriteRelays(uniq(follows.concat(others)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// User's network relays
|
||||||
|
|
||||||
|
export const getUserNetworkWriteRelays = () => getNetworkWriteRelays(get(keys.pubkey))
|
||||||
|
|
||||||
|
// Event-related special cases
|
||||||
|
|
||||||
|
// If we're looking for an event's parent, tags are the most reliable hint,
|
||||||
|
// but we can also look at where other people in the thread write to.
|
||||||
|
export const getRelaysForEventParent = event => {
|
||||||
|
const tags = Tags.from(event)
|
||||||
|
const relays = tags.relays()
|
||||||
|
const pubkeys = tags.type("p").values().all()
|
||||||
|
const pubkeyRelays = pubkeys.flatMap(getPubkeyWriteRelays)
|
||||||
|
|
||||||
|
return uniqByUrl(relays.concat(pubkeyRelays).concat(event.seen_on))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're looking for an event's children, the read relays the author has
|
||||||
|
// advertised would be the most reliable option, since well-behaved clients
|
||||||
|
// will write replies there. However, this may include spam, so we may want
|
||||||
|
// to read from the current user's network's read relays instead.
|
||||||
|
export const getRelaysForEventChildren = event => {
|
||||||
|
return uniqByUrl(getPubkeyReadRelays(event.pubkey).concat(event.seen_on))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRelayForEventHint = event => event.seen_on
|
||||||
|
|
||||||
|
export const getRelayForPersonHint = (pubkey, event) =>
|
||||||
|
first(getPubkeyWriteRelays(pubkey)) || getRelayForEventHint(event)
|
||||||
|
|
||||||
|
// If we're replying or reacting to an event, we want the author to know,
|
||||||
|
// as well as anyone else who is tagged in the original event or the reply.
|
||||||
|
// Get everyone's read relays. We also want to advertise our content to
|
||||||
|
// our followers, so write to our write relays as well.
|
||||||
|
export const getEventPublishRelays = event => {
|
||||||
|
const tags = Tags.from(event)
|
||||||
|
const pubkeys = tags.type("p").values().all()
|
||||||
|
|
||||||
|
return uniqByUrl(
|
||||||
|
pubkeys
|
||||||
|
.concat(event.pubkey)
|
||||||
|
.concat(get(keys.pubkey))
|
||||||
|
.flatMap(getPubkeyReadRelays)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
|
||||||
|
const uniqByUrl = uniqBy(prop('url'))
|
||||||
|
const sortByScore = sortBy(r => -r.score)
|
||||||
|
|
||||||
|
const scoreRelays = (pubkey, relays) => {
|
||||||
|
const routes = database.routes.all({pubkey, url: pluck('url', relays)})
|
||||||
|
const scores = createMapOf('url', 'score', routes)
|
||||||
|
|
||||||
|
return uniqByUrl(sortByScore(relays.map(r => ({...r, score: scores[r.url] || 0}))))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const aggregateScores = relayGroups => {
|
||||||
|
const scores = {} as Record<string, {
|
||||||
|
score: number,
|
||||||
|
count: number,
|
||||||
|
weight?: number,
|
||||||
|
weightedScore?: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
for (const relays of relayGroups) {
|
||||||
|
for (const relay of relays) {
|
||||||
|
const {url, score} = relay
|
||||||
|
|
||||||
|
if (!scores[url]) {
|
||||||
|
scores[url] = {score: 0, count: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
scores[url].score += score
|
||||||
|
scores[url].count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the log-sum-exp and a weighted sum
|
||||||
|
for (const score of Object.values(scores)) {
|
||||||
|
score.weight = Math.log(relayGroups.length / score.count)
|
||||||
|
score.weightedScore = score.weight + Math.log1p(Math.exp(score.score - score.count))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortByScore(
|
||||||
|
Object.entries(scores)
|
||||||
|
.map(([url, {weightedScore}]) => ({url, score: weightedScore}))
|
||||||
|
)
|
||||||
|
}
|
@ -5,6 +5,7 @@ import {synced, timedelta, now} from 'src/util/misc'
|
|||||||
import {isAlert, findReplyId} from 'src/util/nostr'
|
import {isAlert, findReplyId} from 'src/util/nostr'
|
||||||
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 {getUserReadRelays} from 'src/agent/relays'
|
||||||
import {asDisplayEvent, mergeParents} from 'src/app'
|
import {asDisplayEvent, mergeParents} from 'src/app'
|
||||||
|
|
||||||
let listener
|
let listener
|
||||||
@ -12,11 +13,11 @@ let listener
|
|||||||
const mostRecentAlert = synced("app/alerts/mostRecentAlert", 0)
|
const mostRecentAlert = synced("app/alerts/mostRecentAlert", 0)
|
||||||
const lastCheckedAlerts = synced("app/alerts/lastCheckedAlerts", 0)
|
const lastCheckedAlerts = synced("app/alerts/lastCheckedAlerts", 0)
|
||||||
|
|
||||||
const onChunk = async (relays, pubkey, events) => {
|
const onChunk = async (pubkey, events) => {
|
||||||
events = events.filter(e => isAlert(e, pubkey))
|
events = events.filter(e => isAlert(e, pubkey))
|
||||||
|
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
const parents = await network.loadParents(relays, events)
|
const parents = await network.loadParents(events)
|
||||||
const [likes, notes] = partition(propEq('kind', 7), events)
|
const [likes, notes] = partition(propEq('kind', 7), events)
|
||||||
const annotatedNotes = mergeParents(notes.concat(parents).map(asDisplayEvent))
|
const annotatedNotes = mergeParents(notes.concat(parents).map(asDisplayEvent))
|
||||||
const likesByParent = groupBy(findReplyId, likes)
|
const likesByParent = groupBy(findReplyId, likes)
|
||||||
@ -30,31 +31,29 @@ const onChunk = async (relays, pubkey, events) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const load = async (relays, pubkey) => {
|
const load = async pubkey => {
|
||||||
// Include an offset so we don't miss alerts on one relay but not another
|
// Include an offset so we don't miss alerts on one relay but not another
|
||||||
const since = get(mostRecentAlert) - timedelta(30, 'days')
|
const since = get(mostRecentAlert) - timedelta(30, 'days')
|
||||||
|
|
||||||
// Crank the threshold up since we can afford for this to be slow
|
// Crank the threshold up since we can afford for this to be slow
|
||||||
const events = await network.load(
|
const events = await network.load(
|
||||||
relays,
|
getUserReadRelays(),
|
||||||
{kinds: [1, 7], '#p': [pubkey], since, limit: 1000},
|
{kinds: [1, 7], '#p': [pubkey], since, limit: 1000},
|
||||||
{threshold: 0.9}
|
{threshold: 0.9}
|
||||||
)
|
)
|
||||||
|
|
||||||
onChunk(relays, pubkey, events)
|
onChunk(pubkey, events)
|
||||||
}
|
}
|
||||||
|
|
||||||
const listen = async (relays, pubkey) => {
|
const listen = async pubkey => {
|
||||||
if (listener) {
|
if (listener) {
|
||||||
listener.unsub()
|
listener.unsub()
|
||||||
}
|
}
|
||||||
|
|
||||||
listener = await network.listen(
|
listener = await network.listen(
|
||||||
relays,
|
getUserReadRelays(),
|
||||||
{kinds: [1, 7], '#p': [pubkey], since: now()},
|
{kinds: [1, 7], '#p': [pubkey], since: now()},
|
||||||
events => {
|
events => onChunk(pubkey, events)
|
||||||
onChunk(relays, pubkey, events)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@ import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
|||||||
import {get} from 'svelte/store'
|
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, getUserRelays, getFollows} from 'src/agent/helpers'
|
import {user, getNetwork} from 'src/agent/helpers'
|
||||||
|
import {getUserWriteRelays} from 'src/agent/relays'
|
||||||
import defaults from 'src/agent/defaults'
|
import defaults from 'src/agent/defaults'
|
||||||
import database from 'src/agent/database'
|
import database from 'src/agent/database'
|
||||||
import network from 'src/agent/network'
|
import network from 'src/agent/network'
|
||||||
@ -17,17 +18,13 @@ import {toast, routes, modal, settings, logUsage} from 'src/app/ui'
|
|||||||
|
|
||||||
export {toast, modal, settings, alerts, messages, logUsage}
|
export {toast, modal, settings, alerts, messages, logUsage}
|
||||||
|
|
||||||
export const loadAppData = pubkey => {
|
export const loadAppData = pubkey =>
|
||||||
const relays = getUserRelays('read')
|
Promise.all([
|
||||||
const follows = Tags.wrap(getFollows(pubkey))
|
alerts.load(pubkey),
|
||||||
|
alerts.listen(pubkey),
|
||||||
return Promise.all([
|
messages.listen(pubkey),
|
||||||
alerts.load(relays, pubkey),
|
network.loadPeople(getNetwork(pubkey)),
|
||||||
alerts.listen(relays, pubkey),
|
|
||||||
messages.listen(relays, pubkey),
|
|
||||||
network.loadPeople(follows.relays(), follows.values().all()),
|
|
||||||
])
|
])
|
||||||
}
|
|
||||||
|
|
||||||
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
|
export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: string}) => {
|
||||||
if (privkey) {
|
if (privkey) {
|
||||||
@ -38,12 +35,12 @@ export const login = async ({privkey, pubkey}: {privkey?: string, pubkey?: strin
|
|||||||
|
|
||||||
modal.set({type: 'message', message: "Loading your profile data...", spinner: true})
|
modal.set({type: 'message', message: "Loading your profile data...", spinner: true})
|
||||||
|
|
||||||
|
// Load our user so we can populate network and show profile info
|
||||||
|
await network.loadPeople([pubkey])
|
||||||
|
|
||||||
// Load network and start listening, but don't wait for it
|
// Load network and start listening, but don't wait for it
|
||||||
loadAppData(pubkey)
|
loadAppData(pubkey)
|
||||||
|
|
||||||
// Load our user so we can populate network and show profile info
|
|
||||||
await network.loadPeople(getUserRelays('read'), [pubkey])
|
|
||||||
|
|
||||||
// Not ideal, but the network tab depends on the user's social network being
|
// Not ideal, but the network tab depends on the user's social network being
|
||||||
// loaded, so put them on global when they first log in so we're not slowing
|
// loaded, so put them on global when they first log in so we're not slowing
|
||||||
// down users' first run experience too much
|
// down users' first run experience too much
|
||||||
@ -76,7 +73,7 @@ export const removeRelay = async url => {
|
|||||||
defaults.relays = modify(defaults.relays)
|
defaults.relays = modify(defaults.relays)
|
||||||
|
|
||||||
if (person) {
|
if (person) {
|
||||||
await cmd.setRelays(getUserRelays('write'), modify(person.relays || []))
|
await cmd.setRelays(getUserWriteRelays(), modify(person.relays || []))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +85,7 @@ export const setRelayWriteCondition = async (url, write) => {
|
|||||||
defaults.relays = modify(defaults.relays)
|
defaults.relays = modify(defaults.relays)
|
||||||
|
|
||||||
if (person) {
|
if (person) {
|
||||||
await cmd.setRelays(getUserRelays('write'), modify(person.relays || []))
|
await cmd.setRelays(getUserWriteRelays(), modify(person.relays || []))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import {pluck, reject} from 'ramda'
|
|||||||
import {get} from 'svelte/store'
|
import {get} from 'svelte/store'
|
||||||
import {synced, now, timedelta} from 'src/util/misc'
|
import {synced, now, timedelta} from 'src/util/misc'
|
||||||
import {user} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
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'
|
||||||
|
|
||||||
@ -11,13 +12,13 @@ const since = now() - timedelta(30, 'days')
|
|||||||
const mostRecentByPubkey = synced('app/messages/mostRecentByPubkey', {})
|
const mostRecentByPubkey = synced('app/messages/mostRecentByPubkey', {})
|
||||||
const lastCheckedByPubkey = synced('app/messages/lastCheckedByPubkey', {})
|
const lastCheckedByPubkey = synced('app/messages/lastCheckedByPubkey', {})
|
||||||
|
|
||||||
const listen = async (relays, pubkey) => {
|
const listen = async pubkey => {
|
||||||
if (listener) {
|
if (listener) {
|
||||||
listener.unsub()
|
listener.unsub()
|
||||||
}
|
}
|
||||||
|
|
||||||
listener = await network.listen(
|
listener = await network.listen(
|
||||||
relays,
|
getUserReadRelays(),
|
||||||
[{kinds: [4], authors: [pubkey], since},
|
[{kinds: [4], authors: [pubkey], since},
|
||||||
{kinds: [4], '#p': [pubkey], since}],
|
{kinds: [4], '#p': [pubkey], since}],
|
||||||
async events => {
|
async events => {
|
||||||
@ -27,7 +28,7 @@ const listen = async (relays, pubkey) => {
|
|||||||
const messages = reject(e => e.pubkey === e.recipient, await database.messages.all())
|
const messages = reject(e => e.pubkey === e.recipient, await database.messages.all())
|
||||||
|
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
await network.loadPeople(relays, pluck('pubkey', messages))
|
await network.loadPeople(pluck('pubkey', messages))
|
||||||
|
|
||||||
mostRecentByPubkey.update(o => {
|
mostRecentByPubkey.update(o => {
|
||||||
for (const {pubkey, created_at} of messages) {
|
for (const {pubkey, created_at} of messages) {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import {slide} from 'svelte/transition'
|
import {slide} from 'svelte/transition'
|
||||||
import {navigate} from 'svelte-routing'
|
import {navigate} from 'svelte-routing'
|
||||||
import {quantify} from 'hurdak/lib/hurdak'
|
import {quantify} from 'hurdak/lib/hurdak'
|
||||||
import {Tags, findReply, findRoot, findReplyId, displayPerson, isLike} from "src/util/nostr"
|
import {Tags, findReply, findRoot, findRootId, findReplyId, displayPerson, isLike} from "src/util/nostr"
|
||||||
import {extractUrls} from "src/util/html"
|
import {extractUrls} from "src/util/html"
|
||||||
import ImageCircle from 'src/partials/ImageCircle.svelte'
|
import ImageCircle from 'src/partials/ImageCircle.svelte'
|
||||||
import Preview from 'src/partials/Preview.svelte'
|
import Preview from 'src/partials/Preview.svelte'
|
||||||
@ -16,7 +16,8 @@
|
|||||||
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, getTopEventRelays, getAllEventRelays} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
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'
|
||||||
import {routes} from 'src/app/ui'
|
import {routes} from 'src/app/ui'
|
||||||
@ -62,22 +63,20 @@
|
|||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement
|
||||||
|
|
||||||
if (interactive && !['I'].includes(target.tagName) && !target.closest('a')) {
|
if (interactive && !['I'].includes(target.tagName) && !target.closest('a')) {
|
||||||
modal.set({type: 'note/detail', note, relays: getTopEventRelays(note)})
|
modal.set({type: 'note/detail', note})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToParent = async () => {
|
const goToParent = async () => {
|
||||||
const [id, url] = findReply(note).slice(1)
|
const relays = getRelaysForEventParent(note)
|
||||||
const relays = getTopEventRelays(note).concat({url})
|
|
||||||
|
|
||||||
modal.set({type: 'note/detail', note: {id}, relays})
|
modal.set({type: 'note/detail', note: {id: findReplyId(note)}, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToRoot = async () => {
|
const goToRoot = async () => {
|
||||||
const [id, url] = findRoot(note).slice(1)
|
const relays = getRelaysForEventParent(note)
|
||||||
const relays = getTopEventRelays(note).concat({url})
|
|
||||||
|
|
||||||
modal.set({type: 'note/detail', note: {id}, relays})
|
modal.set({type: 'note/detail', note: {id: findRootId(note)}, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showActiveRelays = () => {
|
const showActiveRelays = () => {
|
||||||
@ -89,7 +88,7 @@
|
|||||||
return navigate('/login')
|
return navigate('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
const relays = getTopEventRelays(note)
|
const relays = getEventPublishRelays(note)
|
||||||
const [event] = cmd.createReaction(relays, note, content)
|
const [event] = cmd.createReaction(relays, note, content)
|
||||||
|
|
||||||
if (content === '+') {
|
if (content === '+') {
|
||||||
@ -102,7 +101,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteReaction = e => {
|
const deleteReaction = e => {
|
||||||
cmd.deleteEvent(getAllEventRelays(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', $user.pubkey), likes)
|
||||||
@ -136,7 +135,7 @@
|
|||||||
if (content) {
|
if (content) {
|
||||||
mentions = uniq(mentions.concat(replyMentions))
|
mentions = uniq(mentions.concat(replyMentions))
|
||||||
|
|
||||||
const relays = getTopEventRelays(note)
|
const relays = getEventPublishRelays(note)
|
||||||
const [event] = cmd.createReply(relays, note, content, mentions, topics)
|
const [event] = cmd.createReply(relays, note, content, mentions, topics)
|
||||||
|
|
||||||
toast.show("info", {
|
toast.show("info", {
|
||||||
@ -218,10 +217,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
<Anchor
|
<Anchor
|
||||||
href={"/" + nip19.neventEncode({
|
href={"/" + nip19.neventEncode({id: note.id, relays: [note.seen_on.url]})}
|
||||||
id: note.id,
|
|
||||||
relays: pluck('url', getTopEventRelays(note).slice(0, 5)),
|
|
||||||
})}
|
|
||||||
class="text-sm text-light"
|
class="text-sm text-light"
|
||||||
type="unstyled">
|
type="unstyled">
|
||||||
{formatTimestamp(note.created_at)}
|
{formatTimestamp(note.created_at)}
|
||||||
|
@ -32,13 +32,12 @@
|
|||||||
prop('id'),
|
prop('id'),
|
||||||
newNotes
|
newNotes
|
||||||
.filter(propEq('kind', 1))
|
.filter(propEq('kind', 1))
|
||||||
.concat(await network.loadParents(relays, newNotes))
|
.concat(await network.loadParents(newNotes))
|
||||||
.map(mergeRight({replies: [], reactions: [], children: []}))
|
.map(mergeRight({replies: [], reactions: [], children: []}))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Stream in additional data
|
// Stream in additional data
|
||||||
network.streamContext({
|
network.streamContext({
|
||||||
relays,
|
|
||||||
notes: combined,
|
notes: combined,
|
||||||
updateNotes: cb => {
|
updateNotes: cb => {
|
||||||
notes = cb(notes)
|
notes = cb(notes)
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import Content from 'src/partials/Content.svelte'
|
import Content from 'src/partials/Content.svelte'
|
||||||
import NoteDetail from 'src/views/NoteDetail.svelte'
|
import NoteDetail from 'src/views/NoteDetail.svelte'
|
||||||
import Person from 'src/routes/Person.svelte'
|
import Person from 'src/routes/Person.svelte'
|
||||||
import {getUserRelays} from 'src/agent/helpers'
|
import {getUserReadRelays} from 'src/agent/relays'
|
||||||
|
|
||||||
export let entity
|
export let entity
|
||||||
|
|
||||||
@ -14,7 +14,7 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
try {
|
try {
|
||||||
({type, data} = nip19.decode(entity) as {type: string, data: any})
|
({type, data} = nip19.decode(entity) as {type: string, data: any})
|
||||||
relays = (data.relays || []).map(objOf('url')).concat(getUserRelays())
|
relays = (data.relays || []).map(objOf('url')).concat(getUserReadRelays())
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// pass
|
// pass
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,10 @@
|
|||||||
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 {getUserRelays, getTopRelays, user} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
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 {modal, messages} from 'src/app'
|
import {modal, messages} from 'src/app'
|
||||||
import Room from "src/partials/Room.svelte"
|
import Room from "src/partials/Room.svelte"
|
||||||
import Input from "src/partials/Input.svelte"
|
import Input from "src/partials/Input.svelte"
|
||||||
@ -24,7 +25,7 @@
|
|||||||
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.pubkey], uniq(messages.flatMap(m => [m.pubkey, m.recipient])))
|
||||||
|
|
||||||
await network.loadPeople(getTopRelays(pubkeys, 'write'), pubkeys)
|
await network.loadPeople(pubkeys)
|
||||||
|
|
||||||
return sortBy(k => -(mostRecentByPubkey[k] || 0), pubkeys)
|
return sortBy(k => -(mostRecentByPubkey[k] || 0), pubkeys)
|
||||||
.map(k => ({type: 'npub', id: k, ...database.getPersonWithFallback(k)}))
|
.map(k => ({type: 'npub', id: k, ...database.getPersonWithFallback(k)}))
|
||||||
@ -58,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const sub = network.listen(getUserRelays('read'), [{kinds: [40, 41]}])
|
const sub = network.listen(getUserReadRelays(), [{kinds: [40, 41]}])
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
sub.then(s => {
|
sub.then(s => {
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
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 {getTopEventRelays, user} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
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'
|
||||||
import {modal} from 'src/app'
|
import {modal} from 'src/app'
|
||||||
@ -15,7 +16,7 @@
|
|||||||
const room = database.watch('rooms', rooms => rooms.get(roomId))
|
const room = database.watch('rooms', rooms => rooms.get(roomId))
|
||||||
|
|
||||||
const listenForMessages = async cb => {
|
const listenForMessages = async cb => {
|
||||||
const relays = getTopEventRelays($room)
|
const relays = getRelaysForEventChildren($room)
|
||||||
|
|
||||||
return network.listen(
|
return network.listen(
|
||||||
relays,
|
relays,
|
||||||
@ -33,7 +34,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadMessages = async ({until, limit}) => {
|
const loadMessages = async ({until, limit}) => {
|
||||||
const relays = getTopEventRelays($room)
|
const relays = getRelaysForEventChildren($room)
|
||||||
const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit})
|
const events = await network.load(relays, {kinds: [42], '#e': [roomId], until, limit})
|
||||||
|
|
||||||
if (events.length) {
|
if (events.length) {
|
||||||
@ -48,7 +49,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = content =>
|
const sendMessage = content =>
|
||||||
cmd.createChatMessage(getTopEventRelays($room), roomId, content)
|
cmd.createChatMessage(getRelaysForEventChildren($room), roomId, content)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Channel
|
<Channel
|
||||||
|
@ -4,13 +4,14 @@
|
|||||||
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 {getUserRelays, getPubkeyRelays, user} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
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'
|
||||||
import keys from 'src/agent/keys'
|
import keys from 'src/agent/keys'
|
||||||
import {messages} from 'src/app'
|
|
||||||
import {routes} from 'src/app/ui'
|
|
||||||
import cmd from 'src/agent/cmd'
|
import cmd from 'src/agent/cmd'
|
||||||
|
import {routes} from 'src/app/ui'
|
||||||
|
import {messages} from 'src/app'
|
||||||
|
|
||||||
export let entity
|
export let entity
|
||||||
|
|
||||||
@ -20,7 +21,7 @@
|
|||||||
|
|
||||||
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))
|
messages.lastCheckedByPubkey.update($obj => ({...$obj, [pubkey]: now()}))
|
||||||
|
|
||||||
const getRelays = () => getUserRelays('write').concat(getPubkeyRelays(pubkey, 'write'))
|
const getRelays = () => getAllPubkeyRelays([pubkey, $user.pubkey]).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
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
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 {getPubkeyRelays, getUserRelays, user} from "src/agent/helpers"
|
import {user} from "src/agent/helpers"
|
||||||
|
import {getPubkeyWriteRelays, getUserWriteRelays} 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"
|
||||||
import database from "src/agent/database"
|
import database from "src/agent/database"
|
||||||
@ -43,10 +44,10 @@
|
|||||||
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
|
||||||
relays = relays.concat(getPubkeyRelays(pubkey))
|
relays = relays.concat(getPubkeyWriteRelays(pubkey))
|
||||||
|
|
||||||
// Refresh our person if needed
|
// Refresh our person if needed
|
||||||
network.loadPeople(relays, [pubkey]).then(() => {
|
network.loadPeople([pubkey]).then(() => {
|
||||||
person = database.getPersonWithFallback(pubkey)
|
person = database.getPersonWithFallback(pubkey)
|
||||||
loading = false
|
loading = false
|
||||||
})
|
})
|
||||||
@ -90,13 +91,13 @@
|
|||||||
const tag = ["p", pubkey, relays[0].url, person.name || ""]
|
const tag = ["p", pubkey, relays[0].url, person.name || ""]
|
||||||
const petnames = reject(t => t[1] === pubkey, $user.petnames).concat([tag])
|
const petnames = reject(t => t[1] === pubkey, $user.petnames).concat([tag])
|
||||||
|
|
||||||
cmd.setPetnames(getUserRelays('write'), petnames)
|
cmd.setPetnames(getUserWriteRelays(), petnames)
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfollow = async () => {
|
const unfollow = async () => {
|
||||||
const petnames = reject(t => t[1] === pubkey, $user.petnames)
|
const petnames = reject(t => t[1] === pubkey, $user.petnames)
|
||||||
|
|
||||||
cmd.setPetnames(getUserRelays('write'), petnames)
|
cmd.setPetnames(getUserWriteRelays(), petnames)
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAdvanced = () => {
|
const openAdvanced = () => {
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
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, getUserRelays} from "src/agent/helpers"
|
import {user} from "src/agent/helpers"
|
||||||
|
import {getUserWriteRelays} from 'src/agent/relays'
|
||||||
import cmd from "src/agent/cmd"
|
import cmd from "src/agent/cmd"
|
||||||
import {toast} from "src/app"
|
import {toast} from "src/app"
|
||||||
import {routes} from "src/app/ui"
|
import {routes} from "src/app/ui"
|
||||||
@ -46,7 +47,7 @@
|
|||||||
const submit = async event => {
|
const submit = async event => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
cmd.updateUser(getUserRelays('write'), values)
|
cmd.updateUser(getUserWriteRelays(), values)
|
||||||
|
|
||||||
navigate(routes.person($user.pubkey, 'profile'))
|
navigate(routes.person($user.pubkey, 'profile'))
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Textarea from "src/partials/Textarea.svelte"
|
import Textarea from "src/partials/Textarea.svelte"
|
||||||
import Button from "src/partials/Button.svelte"
|
import Button from "src/partials/Button.svelte"
|
||||||
import {getUserRelays} from 'src/agent/helpers'
|
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"
|
||||||
import {toast, modal} from "src/app"
|
import {toast, modal} from "src/app"
|
||||||
@ -37,8 +37,8 @@
|
|||||||
toast.show("error", "Please enter a name for your room.")
|
toast.show("error", "Please enter a name for your room.")
|
||||||
} else {
|
} else {
|
||||||
const [event] = room.id
|
const [event] = room.id
|
||||||
? cmd.updateRoom(getUserRelays('write'), room)
|
? cmd.updateRoom(getUserWriteRelays(), room)
|
||||||
: cmd.createRoom(getUserRelays('write'), room)
|
: cmd.createRoom(getUserWriteRelays(), room)
|
||||||
|
|
||||||
await database.rooms.patch({id: room.id || event.id, joined: true})
|
await database.rooms.patch({id: room.id || event.id, joined: true})
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
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, getUserRelays} from "src/agent/helpers"
|
import {user} from "src/agent/helpers"
|
||||||
|
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"
|
||||||
import {toast, modal} from "src/app"
|
import {toast, modal} from "src/app"
|
||||||
@ -21,7 +22,7 @@
|
|||||||
export let pubkey = null
|
export let pubkey = null
|
||||||
|
|
||||||
let input = null
|
let input = null
|
||||||
let relays = getUserRelays('write')
|
let relays = getUserWriteRelays()
|
||||||
let showSettings = false
|
let showSettings = false
|
||||||
let q = ''
|
let q = ''
|
||||||
let search
|
let search
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
import {fly} from 'svelte/transition'
|
import {fly} from 'svelte/transition'
|
||||||
import {first} from 'hurdak/lib/hurdak'
|
import {first} from 'hurdak/lib/hurdak'
|
||||||
import {log} from 'src/util/logger'
|
import {log} from 'src/util/logger'
|
||||||
import {getAllEventRelays} from 'src/agent/helpers'
|
|
||||||
import network from 'src/agent/network'
|
import network from 'src/agent/network'
|
||||||
import Note from 'src/partials/Note.svelte'
|
import Note from 'src/partials/Note.svelte'
|
||||||
import Content from 'src/partials/Content.svelte'
|
import Content from 'src/partials/Content.svelte'
|
||||||
@ -17,8 +16,6 @@
|
|||||||
let loading = true
|
let loading = true
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
relays = relays.concat(getAllEventRelays(note))
|
|
||||||
|
|
||||||
if (!note.pubkey) {
|
if (!note.pubkey) {
|
||||||
note = first(await network.load(relays, {ids: [note.id]}))
|
note = first(await network.load(relays, {ids: [note.id]}))
|
||||||
}
|
}
|
||||||
@ -27,7 +24,6 @@
|
|||||||
log('NoteDetail', nip19.noteEncode(note.id), note)
|
log('NoteDetail', nip19.noteEncode(note.id), note)
|
||||||
|
|
||||||
network.streamContext({
|
network.streamContext({
|
||||||
relays,
|
|
||||||
depth: 10,
|
depth: 10,
|
||||||
notes: [note],
|
notes: [note],
|
||||||
updateNotes: cb => {
|
updateNotes: cb => {
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
<script type="ts">
|
<script type="ts">
|
||||||
import Content from 'src/partials/Content.svelte'
|
import Content from 'src/partials/Content.svelte'
|
||||||
import PersonInfo from 'src/partials/PersonInfo.svelte'
|
import PersonInfo from 'src/partials/PersonInfo.svelte'
|
||||||
import {getUserRelays, getTopRelays} from 'src/agent/helpers'
|
import {getAllPubkeyWriteRelays} 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'
|
||||||
|
|
||||||
export let pubkeys
|
export let pubkeys
|
||||||
|
|
||||||
const relays = getUserRelays('read').concat(getTopRelays(pubkeys, 'write'))
|
const relays = getAllPubkeyWriteRelays(pubkeys)
|
||||||
const people = database.watch('people', people => people.all({pubkey: pubkeys}))
|
const people = database.watch('people', people => people.all({pubkey: pubkeys}))
|
||||||
|
|
||||||
network.loadPeople(relays, pubkeys)
|
network.loadPeople(relays, pubkeys)
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
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, getUserRelays} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
import {getUserWriteRelays} from 'src/agent/relays'
|
||||||
import cmd from 'src/agent/cmd'
|
import cmd from 'src/agent/cmd'
|
||||||
import {modal} from 'src/app'
|
import {modal} from 'src/app'
|
||||||
|
|
||||||
@ -28,7 +29,7 @@
|
|||||||
.concat([["p", $modal.person.pubkey, muffleValue.toString()]])
|
.concat([["p", $modal.person.pubkey, muffleValue.toString()]])
|
||||||
.filter(t => last(t) !== "1")
|
.filter(t => last(t) !== "1")
|
||||||
|
|
||||||
cmd.muffle(getUserRelays('write'), muffleTags)
|
cmd.muffle(getUserWriteRelays(), muffleTags)
|
||||||
|
|
||||||
history.back()
|
history.back()
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
import {onMount} from 'svelte'
|
import {onMount} from 'svelte'
|
||||||
import Content from 'src/partials/Content.svelte'
|
import Content from 'src/partials/Content.svelte'
|
||||||
import Input from 'src/partials/Input.svelte'
|
import Input from 'src/partials/Input.svelte'
|
||||||
import {getBestRelay} from 'src/agent/helpers'
|
import {getPubkeyWriteRelays} from 'src/agent/relays'
|
||||||
import {modal, toast} from 'src/app'
|
import {modal, toast} from 'src/app'
|
||||||
|
|
||||||
const {pubkey} = $modal.person
|
const {pubkey} = $modal.person
|
||||||
const relays = [prop('url', getBestRelay(pubkey))]
|
const relays = [prop('url', getPubkeyWriteRelays(pubkey))]
|
||||||
const nprofile = nip19.nprofileEncode({pubkey, relays})
|
const nprofile = nip19.nprofileEncode({pubkey, relays})
|
||||||
|
|
||||||
let canvas
|
let canvas
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
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 PersonInfo from 'src/partials/PersonInfo.svelte'
|
import PersonInfo from 'src/partials/PersonInfo.svelte'
|
||||||
import {user, getUserRelays} from 'src/agent/helpers'
|
import {user} from 'src/agent/helpers'
|
||||||
|
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'
|
||||||
|
|
||||||
@ -18,7 +19,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Prime our database, in case we don't have any people stored yet
|
// Prime our database, in case we don't have any people stored yet
|
||||||
network.listenUntilEose(getUserRelays(), {kinds: personKinds, limit: 300})
|
network.listenUntilEose(getUserReadRelays(), {kinds: personKinds, limit: 300})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Input bind:value={q} placeholder="Search for people">
|
<Input bind:value={q} placeholder="Search for people">
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
<script>
|
<script>
|
||||||
import {uniq} from 'ramda'
|
import {uniq} from 'ramda'
|
||||||
import Notes from "src/partials/Notes.svelte"
|
import Notes from "src/partials/Notes.svelte"
|
||||||
import {shuffle} from 'src/util/misc'
|
import {user, getFollows, getNetwork} from 'src/agent/helpers'
|
||||||
import {Tags} from 'src/util/nostr'
|
import {getAllPubkeyWriteRelays} from 'src/agent/relays'
|
||||||
import {user, getTopRelays, getFollows} from 'src/agent/helpers'
|
|
||||||
|
|
||||||
// Get first- and second-order follows. shuffle and slice network so we're not
|
// 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.
|
// sending too many pubkeys. This will also result in some variety.
|
||||||
const follows = shuffle(Tags.wrap(getFollows($user?.pubkey)).values().all())
|
const follows = getFollows($user?.pubkey)
|
||||||
const others = shuffle(Tags.wrap(follows.flatMap(getFollows)).values().all()).slice(0, 50)
|
const network = getNetwork($user?.pubkey)
|
||||||
const authors = uniq(follows.concat(others)).slice(0, 100)
|
const authors = uniq(follows.concat(network)).slice(0, 100)
|
||||||
const relays = getTopRelays(authors, 'write')
|
const relays = getAllPubkeyWriteRelays(authors)
|
||||||
const filter = {kinds: [1, 7], authors}
|
const filter = {kinds: [1, 7], authors}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<script>
|
<script>
|
||||||
import {uniq} from 'ramda'
|
import {uniq} from 'ramda'
|
||||||
import Notes from "src/partials/Notes.svelte"
|
import Notes from "src/partials/Notes.svelte"
|
||||||
import {shuffle} from 'src/util/misc'
|
import {isLike} from 'src/util/nostr'
|
||||||
import {isLike, Tags} from 'src/util/nostr'
|
import {user, getFollows, getNetwork} from 'src/agent/helpers'
|
||||||
import {user, getTopRelays, getFollows} from 'src/agent/helpers'
|
import {getAllPubkeyWriteRelays} from 'src/agent/relays'
|
||||||
|
|
||||||
// Get first- and second-order follows. shuffle and slice network so we're not
|
// 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.
|
// sending too many pubkeys. This will also result in some variety.
|
||||||
const follows = shuffle(Tags.wrap(getFollows($user?.pubkey)).values().all())
|
const follows = getFollows($user?.pubkey)
|
||||||
const others = shuffle(Tags.wrap(follows.flatMap(getFollows)).values().all()).slice(0, 50)
|
const network = getNetwork($user?.pubkey)
|
||||||
const authors = uniq(follows.concat(others)).slice(0, 100)
|
const authors = uniq(follows.concat(network)).slice(0, 100)
|
||||||
const relays = getTopRelays(authors, 'write')
|
const relays = getAllPubkeyWriteRelays(authors)
|
||||||
const filter = {kinds: [1, 7], authors}
|
const filter = {kinds: [1, 7], authors}
|
||||||
|
|
||||||
const shouldDisplay = note => {
|
const shouldDisplay = note => {
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
<script>
|
|
||||||
import {uniq} from 'ramda'
|
|
||||||
import Notes from "src/partials/Notes.svelte"
|
|
||||||
import {shuffle} from 'src/util/misc'
|
|
||||||
import {isLike, Tags} from 'src/util/nostr'
|
|
||||||
import {user, getTopRelays, getFollows} from 'src/agent/helpers'
|
|
||||||
|
|
||||||
// Get first- and second-order follows. shuffle and slice network so we're not
|
|
||||||
// sending too many pubkeys. This will also result in some variety.
|
|
||||||
const follows = shuffle(Tags.wrap(getFollows($user?.pubkey)).values().all())
|
|
||||||
const others = shuffle(Tags.wrap(follows.flatMap(getFollows)).values().all()).slice(0, 50)
|
|
||||||
const authors = uniq(follows.concat(others)).slice(0, 100)
|
|
||||||
const relays = getTopRelays(authors, 'write')
|
|
||||||
const filter = {kinds: [1, 7], authors}
|
|
||||||
|
|
||||||
const shouldDisplay = note => {
|
|
||||||
return (
|
|
||||||
note.reactions.filter(isLike).length > 2
|
|
||||||
|| note.replies.length > 2
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Notes {relays} {filter} {shouldDisplay} />
|
|
@ -1,10 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import Notes from "src/partials/Notes.svelte"
|
import Notes from "src/partials/Notes.svelte"
|
||||||
import {getPubkeyRelays} from 'src/agent/helpers'
|
import {getPubkeyWriteRelays} from 'src/agent/relays'
|
||||||
|
|
||||||
export let pubkey
|
export let pubkey
|
||||||
|
|
||||||
const relays = getPubkeyRelays(pubkey, 'write')
|
const relays = getPubkeyWriteRelays(pubkey)
|
||||||
const filter = {kinds: [7], authors: [pubkey]}
|
const filter = {kinds: [7], authors: [pubkey]}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Notes from "src/partials/Notes.svelte"
|
import Notes from "src/partials/Notes.svelte"
|
||||||
import {getPubkeyRelays} from 'src/agent/helpers'
|
import {getPubkeyWriteRelays} from 'src/agent/relays'
|
||||||
|
|
||||||
export let pubkey
|
export let pubkey
|
||||||
|
|
||||||
const relays = getPubkeyRelays(pubkey, 'write')
|
const relays = getPubkeyWriteRelays(pubkey)
|
||||||
const filter = {kinds: [1], authors: [pubkey]}
|
const filter = {kinds: [1], authors: [pubkey]}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user