mirror of
https://github.com/coracle-social/coracle.git
synced 2024-10-06 11:43:30 +00:00
Separate people and profile storage
This commit is contained in:
parent
bd2bceaecc
commit
1adc003a98
23
ROADMAP.md
23
ROADMAP.md
@ -1,20 +1,15 @@
|
||||
# Current
|
||||
|
||||
- [ ] Fix re-connects
|
||||
- [ ] Fix memory usage
|
||||
- Re-write database
|
||||
- Use LRU cache and persist that instead. Use purgeStale/dump/load
|
||||
- Split state persistence elsewhere
|
||||
- Keep it focused to abstract interface, split actual tables out elsewhere
|
||||
- Put all other state in same place
|
||||
- Re-write to use arrays with an index of id to index
|
||||
- Fix compatibility, or clear data on first load of new version
|
||||
- Add table of user events, derive profile from this using `watch`.
|
||||
- Refine sync, set up some kind of system where we register tables with events coming in
|
||||
- People.petnames is massive. Split people caches up
|
||||
- Display/picture/about
|
||||
- Minimal zapper info
|
||||
- Drop petnames
|
||||
- [x] Add table of user events, derive profile from this using `watch`.
|
||||
- [ ] Remove petnames from users, retrieve lazily. Then, increase people table size
|
||||
- [ ] Make zapper info more compact
|
||||
- [ ] Move settings storage to an encrypted event https://github.com/nostr-protocol/nips/blob/master/78.md
|
||||
- [ ] Migrate
|
||||
- [ ] Test
|
||||
- [ ] Test that relays/follows made as anon don't stomp user settings on login
|
||||
- [ ] Test anonymous usage, public key only usage
|
||||
- [ ] Fix re-connects
|
||||
- [ ] Show loading/success on zap invoice screen
|
||||
- [ ] Fix iOS/safari/firefox
|
||||
- [ ] Update https://nostr.com/clients/coracle
|
||||
|
@ -9,9 +9,9 @@
|
||||
"preview": "vite preview",
|
||||
"check:es": "eslint src/*/** --quiet",
|
||||
"check:ts": "svelte-check --tsconfig ./tsconfig.json --threshold error",
|
||||
"check:fmt": "prettier --check $(git diff --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)",
|
||||
"check:fmt": "prettier --check $(git diff head --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)",
|
||||
"check": "run-p check:*",
|
||||
"format": "prettier --write $(git diff --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)",
|
||||
"format": "prettier --write $(git diff head --name-only --diff-filter d | grep -E 'js|ts|svelte$' | xargs)",
|
||||
"watch": "find src -type f | entr -r"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -14,12 +14,13 @@
|
||||
import {timedelta, shuffle, now, sleep} from "src/util/misc"
|
||||
import {displayPerson, isLike} from "src/util/nostr"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {ready, onReady, relays} from "src/agent/state"
|
||||
import {ready, onReady, relays} from "src/agent/tables"
|
||||
import keys from "src/agent/keys"
|
||||
import network from "src/agent/network"
|
||||
import pool from "src/agent/pool"
|
||||
import {getUserRelays, initializeRelayList} from "src/agent/relays"
|
||||
import sync from "src/agent/sync"
|
||||
import * as tables from "src/agent/tables"
|
||||
import user from "src/agent/user"
|
||||
import {loadAppData} from "src/app"
|
||||
import alerts from "src/app/alerts"
|
||||
@ -64,7 +65,7 @@
|
||||
import AddRelay from "src/views/relays/AddRelay.svelte"
|
||||
import RelayCard from "src/views/relays/RelayCard.svelte"
|
||||
|
||||
Object.assign(window, {cmd, user, keys, network, pool, sync})
|
||||
Object.assign(window, {cmd, user, keys, network, pool, sync, tables})
|
||||
|
||||
export let url = ""
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import {pick, last, prop, uniqBy} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {roomAttrs, displayPerson, findReplyId, findRootId} from 'src/util/nostr'
|
||||
import {getPubkeyWriteRelays, getRelayForPersonHint, sampleRelays} from 'src/agent/relays'
|
||||
import {getPersonWithFallback} from 'src/agent/state'
|
||||
import pool from 'src/agent/pool'
|
||||
import sync from 'src/agent/sync'
|
||||
import keys from 'src/agent/keys'
|
||||
import {pick, last, prop, uniqBy} from "ramda"
|
||||
import {get} from "svelte/store"
|
||||
import {roomAttrs, displayPerson, findReplyId, findRootId} from "src/util/nostr"
|
||||
import {getPubkeyWriteRelays, getRelayForPersonHint, sampleRelays} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import pool from "src/agent/pool"
|
||||
import sync from "src/agent/sync"
|
||||
import keys from "src/agent/keys"
|
||||
|
||||
const updateUser = updates =>
|
||||
new PublishableEvent(0, {content: JSON.stringify(updates)})
|
||||
const updateUser = updates => new PublishableEvent(0, {content: JSON.stringify(updates)})
|
||||
|
||||
const setRelays = newRelays =>
|
||||
new PublishableEvent(10002, {
|
||||
@ -16,18 +15,16 @@ const setRelays = newRelays =>
|
||||
const t = ["r", r.url]
|
||||
|
||||
if (!r.write) {
|
||||
t.push('read')
|
||||
t.push("read")
|
||||
}
|
||||
|
||||
return t
|
||||
}),
|
||||
})
|
||||
|
||||
const setPetnames = petnames =>
|
||||
new PublishableEvent(3, {tags: petnames})
|
||||
const setPetnames = petnames => new PublishableEvent(3, {tags: petnames})
|
||||
|
||||
const setMutes = mutes =>
|
||||
new PublishableEvent(10000, {tags: mutes})
|
||||
const setMutes = mutes => new PublishableEvent(10000, {tags: mutes})
|
||||
|
||||
const createRoom = room =>
|
||||
new PublishableEvent(40, {content: JSON.stringify(pick(roomAttrs, room))})
|
||||
@ -58,7 +55,11 @@ const getReplyTags = n => {
|
||||
const {url} = getRelayForPersonHint(n.pubkey, n)
|
||||
const rootId = findRootId(n) || findReplyId(n) || n.id
|
||||
|
||||
return [["p", n.pubkey, url], ["e", n.id, url, 'reply'], ["e", rootId, url, 'root']]
|
||||
return [
|
||||
["p", n.pubkey, url],
|
||||
["e", n.id, url, "reply"],
|
||||
["e", rootId, url, "root"],
|
||||
]
|
||||
}
|
||||
|
||||
const tagsFromParent = (n, newTags = []) => {
|
||||
@ -66,18 +67,17 @@ const tagsFromParent = (n, newTags = []) => {
|
||||
|
||||
return uniqBy(
|
||||
// Remove duplicates due to inheritance. Keep earlier ones
|
||||
t => t.slice(0, 2).join(':'),
|
||||
t => t.slice(0, 2).join(":"),
|
||||
// Mentions have to come first for interpolation to work
|
||||
newTags
|
||||
// Add standard reply tags
|
||||
.concat(getReplyTags(n))
|
||||
// Inherit p and e tags, but remove marks and self-mentions
|
||||
.concat(
|
||||
n.tags
|
||||
.filter(t => {
|
||||
n.tags.filter(t => {
|
||||
if (t[1] === pubkey) return false
|
||||
if (!["p", "e"].includes(t[0])) return false
|
||||
if (['reply', 'root'].includes(last(t))) return false
|
||||
if (["reply", "root"].includes(last(t))) return false
|
||||
|
||||
return true
|
||||
})
|
||||
@ -93,7 +93,7 @@ const createReply = (note, content, mentions = [], topics = []) => {
|
||||
const tags = tagsFromParent(
|
||||
note,
|
||||
mentions
|
||||
.map(pk => ["p", pk, prop('url', getRelayForPersonHint(pk, note))])
|
||||
.map(pk => ["p", pk, prop("url", getRelayForPersonHint(pk, note))])
|
||||
.concat(topics.map(t => ["t", t]))
|
||||
)
|
||||
|
||||
@ -115,14 +115,13 @@ const requestZap = (relays, content, pubkey, eventId, amount, lnurl) => {
|
||||
return new PublishableEvent(9734, {content, tags})
|
||||
}
|
||||
|
||||
const deleteEvent = ids =>
|
||||
new PublishableEvent(5, {tags: ids.map(id => ["e", id])})
|
||||
const deleteEvent = ids => new PublishableEvent(5, {tags: ids.map(id => ["e", id])})
|
||||
|
||||
// Utils
|
||||
|
||||
class PublishableEvent {
|
||||
event: Record<string, any>
|
||||
constructor(kind, {content = '', tags = []}) {
|
||||
constructor(kind, {content = "", tags = []}) {
|
||||
const pubkey = get(keys.pubkey)
|
||||
const createdAt = Math.round(new Date().valueOf() / 1000)
|
||||
|
||||
@ -139,7 +138,18 @@ class PublishableEvent {
|
||||
}
|
||||
|
||||
export default {
|
||||
updateUser, setRelays, setPetnames, setMutes, createRoom, updateRoom,
|
||||
createChatMessage, createDirectMessage, createNote, createReaction,
|
||||
createReply, requestZap, deleteEvent, PublishableEvent,
|
||||
updateUser,
|
||||
setRelays,
|
||||
setPetnames,
|
||||
setMutes,
|
||||
createRoom,
|
||||
updateRoom,
|
||||
createChatMessage,
|
||||
createDirectMessage,
|
||||
createNote,
|
||||
createReaction,
|
||||
createReply,
|
||||
requestZap,
|
||||
deleteEvent,
|
||||
PublishableEvent,
|
||||
}
|
||||
|
@ -1,23 +1,26 @@
|
||||
import type {MyEvent} from 'src/util/types'
|
||||
import {sortBy, assoc, uniq, uniqBy, prop, propEq, reject, groupBy, pluck} from 'ramda'
|
||||
import {personKinds, findReplyId} from 'src/util/nostr'
|
||||
import {log} from 'src/util/logger'
|
||||
import {chunk} from 'hurdak/lib/hurdak'
|
||||
import {batch, now, timedelta} from 'src/util/misc'
|
||||
import type {MyEvent} from "src/util/types"
|
||||
import {sortBy, assoc, uniq, uniqBy, prop, propEq, reject, groupBy, pluck} from "ramda"
|
||||
import {personKinds, findReplyId} from "src/util/nostr"
|
||||
import {log} from "src/util/logger"
|
||||
import {chunk} from "hurdak/lib/hurdak"
|
||||
import {batch, now, timedelta} from "src/util/misc"
|
||||
import {
|
||||
getRelaysForEventParent, getAllPubkeyWriteRelays, aggregateScores,
|
||||
getRelaysForEventChildren, sampleRelays,
|
||||
} from 'src/agent/relays'
|
||||
import {people} from 'src/agent/state'
|
||||
import pool from 'src/agent/pool'
|
||||
import sync from 'src/agent/sync'
|
||||
getRelaysForEventParent,
|
||||
getAllPubkeyWriteRelays,
|
||||
aggregateScores,
|
||||
getRelaysForEventChildren,
|
||||
sampleRelays,
|
||||
} from "src/agent/relays"
|
||||
import {people} from "src/agent/tables"
|
||||
import pool from "src/agent/pool"
|
||||
import sync from "src/agent/sync"
|
||||
|
||||
const getStalePubkeys = pubkeys => {
|
||||
// If we're not reloading, only get pubkeys we don't already know about
|
||||
return uniq(pubkeys).filter(pubkey => {
|
||||
const p = people.get(pubkey)
|
||||
|
||||
return !p || p.updated_at < now() - timedelta(1, 'days')
|
||||
return !p || p.updated_at < now() - timedelta(1, "days")
|
||||
})
|
||||
}
|
||||
|
||||
@ -145,8 +148,8 @@ const streamContext = ({notes, onChunk, depth = 0}) =>
|
||||
// Instead of recurring to depth, trampoline so we can batch requests
|
||||
while (events.length > 0 && depth > 0) {
|
||||
const chunk = events.splice(0)
|
||||
const authors = getStalePubkeys(pluck('pubkey', chunk))
|
||||
const filter = [{kinds: [1, 7, 9735], '#e': pluck('id', chunk)}] as Array<object>
|
||||
const authors = getStalePubkeys(pluck("pubkey", chunk))
|
||||
const filter = [{kinds: [1, 7, 9735], "#e": pluck("id", chunk)}] as Array<object>
|
||||
const relays = sampleRelays(aggregateScores(chunk.map(getRelaysForEventChildren)))
|
||||
|
||||
// Load authors and reactions in one subscription
|
||||
@ -169,11 +172,11 @@ const streamContext = ({notes, onChunk, depth = 0}) =>
|
||||
)
|
||||
|
||||
const applyContext = (notes, context) => {
|
||||
context = context.map(assoc('isContext', true))
|
||||
context = context.map(assoc("isContext", true))
|
||||
|
||||
const replies = context.filter(propEq('kind', 1))
|
||||
const reactions = context.filter(propEq('kind', 7))
|
||||
const zaps = context.filter(propEq('kind', 9735))
|
||||
const replies = context.filter(propEq("kind", 1))
|
||||
const reactions = context.filter(propEq("kind", 7))
|
||||
const zaps = context.filter(propEq("kind", 9735))
|
||||
|
||||
const repliesByParentId = groupBy(findReplyId, replies)
|
||||
const reactionsByParentId = groupBy(findReplyId, reactions)
|
||||
@ -186,9 +189,9 @@ const applyContext = (notes, context) => {
|
||||
|
||||
return {
|
||||
...note,
|
||||
replies: sortBy(e => -e.created_at, uniqBy(prop('id'), combinedReplies).map(annotate)),
|
||||
reactions: uniqBy(prop('id'), combinedReactions),
|
||||
zaps: uniqBy(prop('id'), combinedZaps),
|
||||
replies: sortBy(e => -e.created_at, uniqBy(prop("id"), combinedReplies).map(annotate)),
|
||||
reactions: uniqBy(prop("id"), combinedReactions),
|
||||
zaps: uniqBy(prop("id"), combinedZaps),
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,5 +199,10 @@ const applyContext = (notes, context) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
listen, load, loadPeople, personKinds, loadParents, streamContext, applyContext,
|
||||
listen,
|
||||
load,
|
||||
loadPeople,
|
||||
loadParents,
|
||||
streamContext,
|
||||
applyContext,
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import type {Relay} from 'src/util/types'
|
||||
import LRUCache from 'lru-cache'
|
||||
import {warn} from 'src/util/logger'
|
||||
import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} from 'ramda'
|
||||
import {first} from 'hurdak/lib/hurdak'
|
||||
import {Tags, isRelay, findReplyId} from 'src/util/nostr'
|
||||
import {shuffle, fetchJson} from 'src/util/misc'
|
||||
import {relays, routes} from 'src/agent/state'
|
||||
import pool from 'src/agent/pool'
|
||||
import user from 'src/agent/user'
|
||||
import type {Relay} from "src/util/types"
|
||||
import LRUCache from "lru-cache"
|
||||
import {warn} from "src/util/logger"
|
||||
import {filter, pipe, pick, groupBy, objOf, map, assoc, sortBy, uniqBy, prop} from "ramda"
|
||||
import {first} from "hurdak/lib/hurdak"
|
||||
import {Tags, isRelay, findReplyId} from "src/util/nostr"
|
||||
import {shuffle, fetchJson} from "src/util/misc"
|
||||
import {relays, routes} from "src/agent/tables"
|
||||
import pool from "src/agent/pool"
|
||||
import user from "src/agent/user"
|
||||
|
||||
// From Mike Dilger:
|
||||
// 1) Other people's write relays — pull events from people you follow,
|
||||
@ -26,21 +26,21 @@ import user from 'src/agent/user'
|
||||
export const initializeRelayList = async () => {
|
||||
// Throw some hardcoded defaults in there
|
||||
await relays.bulkPatch([
|
||||
{url: 'wss://brb.io'},
|
||||
{url: 'wss://nostr.zebedee.cloud'},
|
||||
{url: 'wss://nostr-pub.wellorder.net'},
|
||||
{url: 'wss://relay.nostr.band'},
|
||||
{url: 'wss://nostr.pleb.network'},
|
||||
{url: 'wss://relay.nostrich.de'},
|
||||
{url: 'wss://relay.damus.io'},
|
||||
{url: "wss://brb.io"},
|
||||
{url: "wss://nostr.zebedee.cloud"},
|
||||
{url: "wss://nostr-pub.wellorder.net"},
|
||||
{url: "wss://relay.nostr.band"},
|
||||
{url: "wss://nostr.pleb.network"},
|
||||
{url: "wss://relay.nostrich.de"},
|
||||
{url: "wss://relay.damus.io"},
|
||||
])
|
||||
|
||||
// Load relays from nostr.watch via dufflepud
|
||||
try {
|
||||
const url = import.meta.env.VITE_DUFFLEPUD_URL + '/relay'
|
||||
const url = import.meta.env.VITE_DUFFLEPUD_URL + "/relay"
|
||||
const json = await fetchJson(url)
|
||||
|
||||
await relays.bulkPatch(map(objOf('url'), json.relays.filter(isRelay)))
|
||||
await relays.bulkPatch(map(objOf("url"), json.relays.filter(isRelay)))
|
||||
} catch (e) {
|
||||
warn("Failed to fetch relays list", e)
|
||||
}
|
||||
@ -52,7 +52,7 @@ const _getPubkeyRelaysCache = new LRUCache({max: 1000})
|
||||
|
||||
export const getPubkeyRelays = (pubkey, mode = null, routesOverride = null) => {
|
||||
const filter = mode ? {pubkey, mode} : {pubkey}
|
||||
const key = [mode, pubkey].join(':')
|
||||
const key = [mode, pubkey].join(":")
|
||||
|
||||
let result = routesOverride || _getPubkeyRelaysCache.get(key)
|
||||
if (!result) {
|
||||
@ -60,41 +60,42 @@ export const getPubkeyRelays = (pubkey, mode = null, routesOverride = null) => {
|
||||
_getPubkeyRelaysCache.set(key, result)
|
||||
}
|
||||
|
||||
return sortByScore(map(pick(['url', 'score']), result))
|
||||
return sortByScore(map(pick(["url", "score"]), result))
|
||||
}
|
||||
|
||||
export const getPubkeyReadRelays = pubkey => getPubkeyRelays(pubkey, 'read')
|
||||
export const getPubkeyReadRelays = pubkey => getPubkeyRelays(pubkey, "read")
|
||||
|
||||
export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, 'write')
|
||||
export const getPubkeyWriteRelays = pubkey => getPubkeyRelays(pubkey, "write")
|
||||
|
||||
// Multiple pubkeys
|
||||
|
||||
export const getAllPubkeyRelays = (pubkeys, mode = null) => {
|
||||
// As an optimization, filter the database once and group by pubkey
|
||||
const filter = mode ? {pubkey: pubkeys, mode} : {pubkey: pubkeys}
|
||||
const routesByPubkey = groupBy(prop('pubkey'), routes.all(filter))
|
||||
const routesByPubkey = groupBy(prop("pubkey"), routes.all(filter))
|
||||
|
||||
return aggregateScores(
|
||||
pubkeys.map(
|
||||
pubkey => getPubkeyRelays(pubkey, mode, routesByPubkey[pubkey] || [])
|
||||
)
|
||||
pubkeys.map(pubkey => getPubkeyRelays(pubkey, mode, routesByPubkey[pubkey] || []))
|
||||
)
|
||||
}
|
||||
|
||||
export const getAllPubkeyReadRelays = pubkeys => getAllPubkeyRelays(pubkeys, 'read')
|
||||
export const getAllPubkeyReadRelays = pubkeys => getAllPubkeyRelays(pubkeys, "read")
|
||||
|
||||
export const getAllPubkeyWriteRelays = pubkeys => getAllPubkeyRelays(pubkeys, 'write')
|
||||
export const getAllPubkeyWriteRelays = pubkeys => getAllPubkeyRelays(pubkeys, "write")
|
||||
|
||||
// Current user
|
||||
|
||||
export const getUserRelays = () =>
|
||||
user.getRelays().map(assoc('score', 1))
|
||||
export const getUserRelays = () => user.getRelays().map(assoc("score", 1))
|
||||
|
||||
export const getUserReadRelays = () =>
|
||||
getUserRelays().filter(prop('read')).map(pick(['url', 'score']))
|
||||
getUserRelays()
|
||||
.filter(prop("read"))
|
||||
.map(pick(["url", "score"]))
|
||||
|
||||
export const getUserWriteRelays = (): Array<Relay> =>
|
||||
getUserRelays().filter(prop('write')).map(pick(['url', 'score']))
|
||||
getUserRelays()
|
||||
.filter(prop("write"))
|
||||
.map(pick(["url", "score"]))
|
||||
|
||||
// Event-related special cases
|
||||
|
||||
@ -113,12 +114,10 @@ export const getRelaysForEventParent = event => {
|
||||
// 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({url: event.seen_on, score: 1}))
|
||||
return uniqByUrl(getPubkeyReadRelays(event.pubkey).concat({url: event.seen_on, score: 1}))
|
||||
}
|
||||
|
||||
export const getRelayForEventHint = event =>
|
||||
({url: event.seen_on, score: 1})
|
||||
export const getRelayForEventHint = event => ({url: event.seen_on, score: 1})
|
||||
|
||||
export const getRelayForPersonHint = (pubkey, event) =>
|
||||
first(getPubkeyWriteRelays(pubkey)) || getRelayForEventHint(event)
|
||||
@ -135,15 +134,14 @@ export const getEventPublishRelays = event => {
|
||||
return uniqByUrl(aggregateScores(relayChunks).concat(getUserWriteRelays()))
|
||||
}
|
||||
|
||||
|
||||
// Utils
|
||||
|
||||
export const uniqByUrl = pipe(uniqBy(prop('url')), filter(prop('url')))
|
||||
export const uniqByUrl = pipe(uniqBy(prop("url")), filter(prop("url")))
|
||||
|
||||
export const sortByScore = sortBy(r => -r.score)
|
||||
|
||||
export const sampleRelays = (relays, scale = 1) => {
|
||||
let limit = user.getSetting('relayLimit')
|
||||
let limit = user.getSetting("relayLimit")
|
||||
|
||||
// Allow the caller to scale down how many relays we're bothering depending on
|
||||
// the use case, but only if we have enough relays to handle it
|
||||
@ -159,21 +157,22 @@ export const sampleRelays = (relays, scale = 1) => {
|
||||
|
||||
// If we're still under the limit, add user relays for good measure
|
||||
if (relays.length < limit) {
|
||||
relays = relays.concat(
|
||||
shuffle(getUserReadRelays()).slice(0, limit - relays.length)
|
||||
)
|
||||
relays = relays.concat(shuffle(getUserReadRelays()).slice(0, limit - relays.length))
|
||||
}
|
||||
|
||||
return uniqByUrl(relays)
|
||||
}
|
||||
|
||||
export const aggregateScores = relayGroups => {
|
||||
const scores = {} as Record<string, {
|
||||
score: number,
|
||||
count: number,
|
||||
weight?: number,
|
||||
const scores = {} as Record<
|
||||
string,
|
||||
{
|
||||
score: number
|
||||
count: number
|
||||
weight?: number
|
||||
weightedScore?: number
|
||||
}>
|
||||
}
|
||||
>
|
||||
|
||||
for (const relays of relayGroups) {
|
||||
for (const relay of relays) {
|
||||
@ -195,7 +194,6 @@ export const aggregateScores = relayGroups => {
|
||||
}
|
||||
|
||||
return sortByScore(
|
||||
Object.entries(scores)
|
||||
.map(([url, {weightedScore}]) => ({url, score: weightedScore}))
|
||||
Object.entries(scores).map(([url, {weightedScore}]) => ({url, score: weightedScore}))
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {uniq, without} from 'ramda'
|
||||
import {Tags} from 'src/util/nostr'
|
||||
import {getPersonWithFallback} from 'src/agent/state'
|
||||
import user from 'src/agent/user'
|
||||
import {uniq, without} from "ramda"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import user from "src/agent/user"
|
||||
|
||||
export const getFollows = pubkey =>
|
||||
Tags.wrap(getPersonWithFallback(pubkey).petnames).type("p").values().all()
|
||||
@ -12,8 +12,7 @@ export const getNetwork = pubkey => {
|
||||
return uniq(without(follows, follows.flatMap(getFollows)))
|
||||
}
|
||||
|
||||
export const getUserFollows = (): Array<string> =>
|
||||
Tags.wrap(user.getPetnames()).values().all()
|
||||
export const getUserFollows = (): Array<string> => Tags.wrap(user.getPetnames()).values().all()
|
||||
|
||||
export const getUserNetwork = () => {
|
||||
const follows = getUserFollows()
|
||||
|
@ -1,20 +1,23 @@
|
||||
import {error} from 'src/util/logger'
|
||||
import type {Writable} from "svelte/store"
|
||||
import LRUCache from "lru-cache"
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {objOf, is, without} from "ramda"
|
||||
import {writable} from "svelte/store"
|
||||
import {isObject, mapValues, ensurePlural} from "hurdak/lib/hurdak"
|
||||
import {log, error} from "src/util/logger"
|
||||
import {where} from "src/util/misc"
|
||||
|
||||
// Types
|
||||
// ----------------------------------------------------------------------------
|
||||
// Localforage interface via web worker
|
||||
|
||||
type Message = {
|
||||
topic: string
|
||||
payload: object
|
||||
}
|
||||
|
||||
// Plumbing
|
||||
const worker = new Worker(new URL("./workers/database.js", import.meta.url), {type: "module"})
|
||||
|
||||
const worker = new Worker(
|
||||
new URL('./workers/database.js', import.meta.url),
|
||||
{type: 'module'}
|
||||
)
|
||||
|
||||
worker.addEventListener('error', error)
|
||||
worker.addEventListener("error", error)
|
||||
|
||||
class Channel {
|
||||
id: string
|
||||
@ -27,10 +30,10 @@ class Channel {
|
||||
}
|
||||
}
|
||||
|
||||
worker.addEventListener('message', this.onMessage)
|
||||
worker.addEventListener("message", this.onMessage)
|
||||
}
|
||||
close() {
|
||||
worker.removeEventListener('message', this.onMessage)
|
||||
worker.removeEventListener("message", this.onMessage)
|
||||
}
|
||||
send(topic, payload) {
|
||||
worker.postMessage({channel: this.id, topic, payload})
|
||||
@ -50,12 +53,209 @@ const call = (topic, payload): Promise<Message> => {
|
||||
})
|
||||
}
|
||||
|
||||
export const lf = async (method, ...args) => {
|
||||
const message = await call('localforage.call', {method, args})
|
||||
const lf = async (method, ...args) => {
|
||||
const message = await call("localforage.call", {method, args})
|
||||
|
||||
if (message.topic !== 'localforage.return') {
|
||||
if (message.topic !== "localforage.return") {
|
||||
throw new Error(`callLocalforage received invalid response: ${message}`)
|
||||
}
|
||||
|
||||
return message.payload
|
||||
}
|
||||
|
||||
export const setItem = (k, v) => lf("setItem", k, v)
|
||||
export const removeItem = k => lf("removeItem", k)
|
||||
export const getItem = k => lf("getItem", k)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Database table abstraction, synced to worker storage
|
||||
|
||||
type CacheEntry = [string, {value: any}]
|
||||
|
||||
type TableOpts = {
|
||||
maxEntries?: number
|
||||
initialize?: (table: Table) => Promise<Array<CacheEntry>>
|
||||
}
|
||||
|
||||
export const registry = {} as Record<string, Table>
|
||||
|
||||
export class Table {
|
||||
name: string
|
||||
pk: string
|
||||
opts: TableOpts
|
||||
cache: LRUCache<string, any>
|
||||
listeners: Array<(Table) => void>
|
||||
ready: Writable<boolean>
|
||||
constructor(name, pk, opts: TableOpts = {}) {
|
||||
this.name = name
|
||||
this.pk = pk
|
||||
this.opts = {maxEntries: 1000, initialize: t => this.dump(), ...opts}
|
||||
this.cache = new LRUCache({max: this.opts.maxEntries})
|
||||
this.listeners = []
|
||||
this.ready = writable(false)
|
||||
|
||||
registry[name] = this
|
||||
|
||||
// Sync from storage initially
|
||||
;(async () => {
|
||||
const t = Date.now()
|
||||
|
||||
this.cache.load((await this.opts.initialize(this)) || [])
|
||||
this._notify()
|
||||
|
||||
log(`Table ${name} ready in ${Date.now() - t}ms (${this.cache.size} records)`)
|
||||
|
||||
this.ready.set(true)
|
||||
})()
|
||||
}
|
||||
_persist = throttle(4_000, () => {
|
||||
setItem(this.name, this.cache.dump())
|
||||
})
|
||||
_notify() {
|
||||
// Notify subscribers
|
||||
for (const cb of this.listeners) {
|
||||
cb(this)
|
||||
}
|
||||
|
||||
// Save to localstorage
|
||||
this._persist()
|
||||
}
|
||||
subscribe(cb) {
|
||||
cb = throttle(100, cb)
|
||||
|
||||
this.listeners.push(cb)
|
||||
|
||||
cb(this)
|
||||
|
||||
return () => {
|
||||
this.listeners = without([cb], this.listeners)
|
||||
}
|
||||
}
|
||||
bulkPut(items) {
|
||||
for (const item of items) {
|
||||
const k = item[this.pk]
|
||||
|
||||
if (!k) {
|
||||
throw new Error(`Missing primary key on ${this.name}`)
|
||||
}
|
||||
|
||||
this.cache.set(k, item)
|
||||
}
|
||||
|
||||
this._persist()
|
||||
}
|
||||
put(item) {
|
||||
this.bulkPut([item])
|
||||
}
|
||||
bulkPatch(items) {
|
||||
for (const item of items) {
|
||||
const k = item[this.pk]
|
||||
|
||||
if (!k) {
|
||||
throw new Error(`Missing primary key on ${this.name}`)
|
||||
}
|
||||
|
||||
this.cache.set(k, {...this.cache.get(k), ...item})
|
||||
}
|
||||
|
||||
this._persist()
|
||||
}
|
||||
patch(item) {
|
||||
this.bulkPatch([item])
|
||||
}
|
||||
bulkRemove(ks) {
|
||||
for (const k of ks) {
|
||||
this.cache.delete(k)
|
||||
}
|
||||
|
||||
this._persist()
|
||||
}
|
||||
remove(k) {
|
||||
this.bulkRemove([k])
|
||||
}
|
||||
async drop() {
|
||||
this.cache.clear()
|
||||
|
||||
return removeItem(this.name)
|
||||
}
|
||||
async dump() {
|
||||
let data = (await getItem(this.name)) || []
|
||||
|
||||
// Backwards compat - we used to store objects rather than cache dump arrays
|
||||
if (isObject(data)) {
|
||||
data = Object.entries(mapValues(objOf("value"), data))
|
||||
}
|
||||
|
||||
return data as Array<CacheEntry>
|
||||
}
|
||||
toArray() {
|
||||
const result = []
|
||||
for (const item of this.cache.values()) {
|
||||
result.push(item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
all(spec = {}) {
|
||||
return this.toArray().filter(where(spec))
|
||||
}
|
||||
find(spec = {}) {
|
||||
return this.cache.find(where(spec))
|
||||
}
|
||||
get(k) {
|
||||
return this.cache.get(k)
|
||||
}
|
||||
}
|
||||
|
||||
const listener = (() => {
|
||||
let listeners = []
|
||||
|
||||
for (const table of Object.values(registry) as Array<Table>) {
|
||||
table.subscribe(() => listeners.forEach(f => f(table.name)))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: f => {
|
||||
listeners.push(f)
|
||||
|
||||
return () => {
|
||||
listeners = without([f], listeners)
|
||||
}
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
export const watch = (names, f) => {
|
||||
names = ensurePlural(names)
|
||||
|
||||
const store = writable(null)
|
||||
const tables = names.map(name => registry[name])
|
||||
|
||||
// Initialize synchronously if possible
|
||||
const initialValue = f(...tables)
|
||||
if (is(Promise, initialValue)) {
|
||||
initialValue.then(v => store.set(v))
|
||||
} else {
|
||||
store.set(initialValue)
|
||||
}
|
||||
|
||||
// Debounce refresh so we don't get UI lag
|
||||
const refresh = throttle(300, async () => store.set(await f(...tables)))
|
||||
|
||||
// Listen for changes
|
||||
listener.subscribe(name => {
|
||||
if (names.includes(name)) {
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const dropAll = async () => {
|
||||
for (const table of Object.values(registry)) {
|
||||
await table.drop()
|
||||
|
||||
log(`Successfully dropped table ${table.name}`)
|
||||
}
|
||||
}
|
||||
|
@ -12,25 +12,28 @@ import {
|
||||
timedelta,
|
||||
hash,
|
||||
} from "src/util/misc"
|
||||
import {
|
||||
Tags,
|
||||
roomAttrs,
|
||||
isRelay,
|
||||
isShareableRelay,
|
||||
normalizeRelayUrl,
|
||||
} from "src/util/nostr"
|
||||
import {getPersonWithFallback, people, relays, rooms, routes} from "src/agent/state"
|
||||
import {Tags, roomAttrs, isRelay, isShareableRelay, normalizeRelayUrl} from "src/util/nostr"
|
||||
import {people, userEvents, relays, rooms, routes} from "src/agent/tables"
|
||||
import {uniqByUrl} from "src/agent/relays"
|
||||
import user from "src/agent/user"
|
||||
|
||||
const handlers = {}
|
||||
|
||||
const addHandler = (kind, f) => (handlers[kind] || []).push(f)
|
||||
const addHandler = (kind, f) => {
|
||||
handlers[kind] = handlers[kind] || []
|
||||
handlers[kind].push(f)
|
||||
}
|
||||
|
||||
const processEvents = async events => {
|
||||
const userPubkey = user.getPubkey()
|
||||
const chunks = chunk(100, ensurePlural(events))
|
||||
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
for (const event of chunks[i]) {
|
||||
if (event.pubkey === userPubkey) {
|
||||
userEvents.put(event)
|
||||
}
|
||||
|
||||
for (const handler of handlers[event.kind] || []) {
|
||||
handler(event)
|
||||
}
|
||||
@ -45,12 +48,19 @@ const processEvents = async events => {
|
||||
|
||||
// People
|
||||
|
||||
const updatePerson = (pubkey, data) => {
|
||||
people.patch({pubkey, updated_at: now(), ...data})
|
||||
|
||||
// If our pubkey matches, copy to our user's profile as well
|
||||
if (pubkey === user.getPubkey()) {
|
||||
user.profile.update($p => ({...$p, ...data}))
|
||||
}
|
||||
}
|
||||
|
||||
const verifyNip05 = (pubkey, as) =>
|
||||
nip05.queryProfile(as).then(result => {
|
||||
if (result?.pubkey === pubkey) {
|
||||
const person = getPersonWithFallback(pubkey)
|
||||
|
||||
people.patch({...person, verified_as: as})
|
||||
updatePerson(pubkey, {verified_as: as})
|
||||
|
||||
if (result.relays?.length > 0) {
|
||||
const urls = result.relays.filter(isRelay)
|
||||
@ -92,7 +102,7 @@ const verifyZapper = async (pubkey, address) => {
|
||||
const lnurl = lnurlEncode("lnurl", url)
|
||||
|
||||
if (zapper?.allowsNostr && zapper?.nostrPubkey) {
|
||||
people.patch({pubkey, zapper, lnurl})
|
||||
updatePerson(pubkey, {zapper, lnurl})
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,48 +126,59 @@ addHandler(0, e => {
|
||||
verifyZapper(e.pubkey, address.toLowerCase())
|
||||
}
|
||||
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
updatePerson(e.pubkey, {
|
||||
kind0: {...person?.kind0, ...kind0},
|
||||
kind0_updated_at: e.created_at,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
addHandler(2, e => {
|
||||
const person = people.get(e.pubkey)
|
||||
|
||||
if (e.created_at < person?.relays_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
relays_updated_at: e.created_at,
|
||||
relays: uniqByUrl((person?.relays || []).concat({url: e.content})),
|
||||
})
|
||||
})
|
||||
|
||||
addHandler(3, e => {
|
||||
const person = people.get(e.pubkey)
|
||||
|
||||
if (e.created_at > (person?.petnames_updated_at || 0)) {
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
if (e.created_at < person?.petnames_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
updatePerson(e.pubkey, {
|
||||
petnames_updated_at: e.created_at,
|
||||
petnames: e.tags.filter(t => t[0] === "p"),
|
||||
})
|
||||
})
|
||||
|
||||
// User profile, except for events also handled for other users
|
||||
|
||||
const profileHandler = (key, getValue) => e => {
|
||||
const profile = user.getProfile()
|
||||
|
||||
if (e.pubkey !== profile.pubkey) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.created_at > (person.relays_updated_at || 0)) {
|
||||
const updated_at_key = `${key}_updated_at`
|
||||
|
||||
if (e.created_at < profile?.[updated_at_key]) {
|
||||
return
|
||||
}
|
||||
|
||||
user.profile.update($p => ({
|
||||
...$p,
|
||||
[key]: getValue(e, $p),
|
||||
[updated_at_key]: e.created_at,
|
||||
}))
|
||||
}
|
||||
|
||||
addHandler(
|
||||
2,
|
||||
profileHandler("relays", (e, p) => uniqByUrl(p.relays.concat({url: e.content})))
|
||||
)
|
||||
|
||||
addHandler(
|
||||
3,
|
||||
profileHandler("relays", (e, p) => {
|
||||
return (
|
||||
tryJson(() => {
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
relays_updated_at: e.created_at,
|
||||
relays: Object.entries(JSON.parse(e.content))
|
||||
return Object.entries(JSON.parse(e.content))
|
||||
.map(([url, conditions]) => {
|
||||
const {write, read} = conditions as Record<string, boolean | string>
|
||||
|
||||
@ -167,62 +188,36 @@ addHandler(3, e => {
|
||||
read: [false, "!"].includes(read) ? false : true,
|
||||
}
|
||||
})
|
||||
.filter(r => isRelay(r.url)),
|
||||
})
|
||||
})
|
||||
}
|
||||
.filter(r => isRelay(r.url))
|
||||
}) || p.relays
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
addHandler(10000, e => {
|
||||
const person = people.get(e.pubkey)
|
||||
|
||||
if (e.created_at < person?.mutes_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
mutes_updated_at: e.created_at,
|
||||
mutes: e.tags,
|
||||
})
|
||||
})
|
||||
addHandler(
|
||||
10000,
|
||||
profileHandler("mutes", (e, p) => e.tags)
|
||||
)
|
||||
|
||||
// DEPRECATED
|
||||
addHandler(12165, e => {
|
||||
const person = people.get(e.pubkey)
|
||||
|
||||
if (e.created_at < person?.mutes_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
mutes_updated_at: e.created_at,
|
||||
mutes: e.tags,
|
||||
})
|
||||
addHandler(
|
||||
10001,
|
||||
profileHandler("relays", (e, p) => {
|
||||
return e.tags.map(([url, read, write]) => ({url, read: read !== "!", write: write !== "!"}))
|
||||
})
|
||||
)
|
||||
|
||||
addHandler(10002, e => {
|
||||
const person = people.get(e.pubkey)
|
||||
|
||||
if (e.created_at < person?.relays_updated_at) {
|
||||
return
|
||||
}
|
||||
|
||||
people.patch({
|
||||
pubkey: e.pubkey,
|
||||
updated_at: now(),
|
||||
relays_updated_at: e.created_at,
|
||||
relays: e.tags.map(([_, url, mode]) => {
|
||||
addHandler(
|
||||
10002,
|
||||
profileHandler("relays", (e, p) => {
|
||||
return e.tags.map(([_, url, mode]) => {
|
||||
const read = (mode || "read") === "read"
|
||||
const write = (mode || "write") === "write"
|
||||
|
||||
return {url, read, write}
|
||||
}),
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
// Rooms
|
||||
|
||||
|
@ -1,208 +0,0 @@
|
||||
import type {Writable} from 'svelte/store'
|
||||
import LRUCache from 'lru-cache'
|
||||
import {throttle} from 'throttle-debounce'
|
||||
import {objOf, is, without} from 'ramda'
|
||||
import {writable} from 'svelte/store'
|
||||
import {isObject, mapValues, ensurePlural} from 'hurdak/lib/hurdak'
|
||||
import {log} from 'src/util/logger'
|
||||
import {where} from 'src/util/misc'
|
||||
import {lf} from 'src/agent/storage'
|
||||
|
||||
// Local copy of data so we can provide a sync observable interface. The worker
|
||||
// is just for storing data and processing expensive queries
|
||||
|
||||
type CacheEntry = [string, {value: any}]
|
||||
|
||||
type TableOpts = {
|
||||
maxEntries?: number
|
||||
initialize?: (table: Table) => Promise<Array<CacheEntry>>
|
||||
}
|
||||
|
||||
export const registry = {} as Record<string, Table>
|
||||
|
||||
export class Table {
|
||||
name: string
|
||||
pk: string
|
||||
opts: TableOpts
|
||||
cache: LRUCache<string, any>
|
||||
listeners: Array<(Table) => void>
|
||||
ready: Writable<boolean>
|
||||
constructor(name, pk, opts: TableOpts = {}) {
|
||||
this.name = name
|
||||
this.pk = pk
|
||||
this.opts = {maxEntries: 1000, initialize: t => this.dump(), ...opts}
|
||||
this.cache = new LRUCache({max: this.opts.maxEntries})
|
||||
this.listeners = []
|
||||
this.ready = writable(false)
|
||||
|
||||
registry[name] = this
|
||||
|
||||
// Sync from storage initially
|
||||
;(async () => {
|
||||
const t = Date.now()
|
||||
|
||||
this.cache.load(await this.opts.initialize(this) || [])
|
||||
this._notify()
|
||||
|
||||
log(`Table ${name} ready in ${Date.now() - t}ms (${this.cache.size} records)`)
|
||||
|
||||
this.ready.set(true)
|
||||
})()
|
||||
}
|
||||
_persist = throttle(4_000, () => {
|
||||
lf('setItem', this.name, this.cache.dump())
|
||||
})
|
||||
_notify() {
|
||||
// Notify subscribers
|
||||
for (const cb of this.listeners) {
|
||||
cb(this)
|
||||
}
|
||||
|
||||
// Save to localstorage
|
||||
this._persist()
|
||||
}
|
||||
subscribe(cb) {
|
||||
cb = throttle(100, cb)
|
||||
|
||||
this.listeners.push(cb)
|
||||
|
||||
cb(this)
|
||||
|
||||
return () => {
|
||||
this.listeners = without([cb], this.listeners)
|
||||
}
|
||||
}
|
||||
bulkPut(items) {
|
||||
for (const item of items) {
|
||||
const k = item[this.pk]
|
||||
|
||||
if (!k) {
|
||||
throw new Error(`Missing primary key on ${this.name}`)
|
||||
}
|
||||
|
||||
this.cache.set(k, item)
|
||||
}
|
||||
|
||||
this._persist()
|
||||
}
|
||||
put(item) {
|
||||
this.bulkPut([item])
|
||||
}
|
||||
bulkPatch(items) {
|
||||
for (const item of items) {
|
||||
const k = item[this.pk]
|
||||
|
||||
if (!k) {
|
||||
throw new Error(`Missing primary key on ${this.name}`)
|
||||
}
|
||||
|
||||
this.cache.set(k, {...this.cache.get(k), ...item})
|
||||
}
|
||||
|
||||
this._persist()
|
||||
}
|
||||
patch(item) {
|
||||
this.bulkPatch([item])
|
||||
}
|
||||
bulkRemove(ks) {
|
||||
for (const k of ks) {
|
||||
this.cache.delete(k)
|
||||
}
|
||||
|
||||
this._persist()
|
||||
}
|
||||
remove(k) {
|
||||
this.bulkRemove([k])
|
||||
}
|
||||
async drop() {
|
||||
this.cache.clear()
|
||||
|
||||
return lf('removeItem', this.name)
|
||||
}
|
||||
async dump() {
|
||||
let data = await lf('getItem', this.name) || []
|
||||
|
||||
// Backwards compat - we used to store objects rather than cache dump arrays
|
||||
if (isObject(data)) {
|
||||
data = Object.entries(mapValues(objOf('value'), data))
|
||||
}
|
||||
|
||||
return data as Array<CacheEntry>
|
||||
}
|
||||
toArray() {
|
||||
const result = []
|
||||
for (const item of this.cache.values()) {
|
||||
result.push(item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
all(spec = {}) {
|
||||
return this.toArray().filter(where(spec))
|
||||
}
|
||||
find(spec = {}) {
|
||||
return this.cache.find(where(spec))
|
||||
}
|
||||
get(k) {
|
||||
return this.cache.get(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to allow us to listen to changes of any given table
|
||||
|
||||
const listener = (() => {
|
||||
let listeners = []
|
||||
|
||||
for (const table of Object.values(registry) as Array<Table>) {
|
||||
table.subscribe(() => listeners.forEach(f => f(table.name)))
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: f => {
|
||||
listeners.push(f)
|
||||
|
||||
return () => {
|
||||
listeners = without([f], listeners)
|
||||
}
|
||||
},
|
||||
}
|
||||
})()
|
||||
|
||||
// Helper to re-run a query every time a given table changes
|
||||
|
||||
export const watch = (names, f) => {
|
||||
names = ensurePlural(names)
|
||||
|
||||
const store = writable(null)
|
||||
const tables = names.map(name => registry[name])
|
||||
|
||||
// Initialize synchronously if possible
|
||||
const initialValue = f(...tables)
|
||||
if (is(Promise, initialValue)) {
|
||||
initialValue.then(v => store.set(v))
|
||||
} else {
|
||||
store.set(initialValue)
|
||||
}
|
||||
|
||||
// Debounce refresh so we don't get UI lag
|
||||
const refresh = throttle(300, async () => store.set(await f(...tables)))
|
||||
|
||||
// Listen for changes
|
||||
listener.subscribe(name => {
|
||||
if (names.includes(name)) {
|
||||
refresh()
|
||||
}
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// Methods that work on all tables
|
||||
|
||||
export const dropAll = async () => {
|
||||
for (const table of Object.values(registry)) {
|
||||
await table.drop()
|
||||
|
||||
log(`Successfully dropped table ${table.name}`)
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import {pluck, all, identity} from "ramda"
|
||||
import {derived} from "svelte/store"
|
||||
import {Table, registry} from "src/agent/table"
|
||||
import {Table, registry} from "src/agent/storage"
|
||||
|
||||
export const userEvents = new Table("userEvents", "id")
|
||||
export const people = new Table("people", "pubkey")
|
||||
export const contacts = new Table("contacts", "pubkey")
|
||||
export const rooms = new Table("rooms", "id")
|
||||
@ -21,5 +22,3 @@ export const onReady = cb => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
(window as any).t = {people, contacts, rooms, alerts, relays, routes}
|
@ -1,91 +1,56 @@
|
||||
import type {Person} from 'src/util/types'
|
||||
import type {Readable} from 'svelte/store'
|
||||
import {slice, identity, prop, find, pipe, assoc, whereEq, when, concat, reject, nth, map} from 'ramda'
|
||||
import {findReplyId, findRootId} from 'src/util/nostr'
|
||||
import {synced} from 'src/util/misc'
|
||||
import {derived} from 'svelte/store'
|
||||
import {people} from 'src/agent/state'
|
||||
import keys from 'src/agent/keys'
|
||||
import cmd from 'src/agent/cmd'
|
||||
import type {Relay} from "src/util/types"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {slice, prop, find, pipe, assoc, whereEq, when, concat, reject, nth, map} from "ramda"
|
||||
import {findReplyId, findRootId} from "src/util/nostr"
|
||||
import {synced} from "src/util/misc"
|
||||
import {derived} from "svelte/store"
|
||||
import keys from "src/agent/keys"
|
||||
import cmd from "src/agent/cmd"
|
||||
|
||||
// Create a special wrapper to manage profile data, follows, and relays in the same
|
||||
// way whether the user is logged in or not. This involves creating a store that we
|
||||
// allow an anonymous user to write to, then once the user logs in we use that until
|
||||
// we have actual event data for them, which we then prefer. For extra fun, we also
|
||||
// sync this stuff to regular private variables so we don't have to constantly call
|
||||
// `get` on our stores.
|
||||
|
||||
let settingsCopy = null
|
||||
let profileCopy = null
|
||||
let petnamesCopy = []
|
||||
let relaysCopy = []
|
||||
let mutesCopy = []
|
||||
|
||||
const anonPetnames = synced('agent/user/anonPetnames', [])
|
||||
const anonRelays = synced('agent/user/anonRelays', [])
|
||||
const anonMutes = synced('agent/user/anonMutes', [])
|
||||
|
||||
const settings = synced("agent/user/settings", {
|
||||
const profile = synced("agent/user/profile", {
|
||||
pubkey: null,
|
||||
kind0: null,
|
||||
lnurl: null,
|
||||
zapper: null,
|
||||
settings: {
|
||||
relayLimit: 20,
|
||||
defaultZap: 21,
|
||||
showMedia: true,
|
||||
reportAnalytics: true,
|
||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||
},
|
||||
petnames: [],
|
||||
relays: [],
|
||||
mutes: [],
|
||||
})
|
||||
|
||||
const profile = derived(
|
||||
[keys.pubkey, people as Readable<any>],
|
||||
([pubkey, t]) => pubkey ? (t.get(pubkey) || {pubkey}) : null
|
||||
) as Readable<Person>
|
||||
|
||||
const profileKeyWithDefault = (key, stores) => derived(
|
||||
[profile, ...stores],
|
||||
([$profile, ...values]) =>
|
||||
$profile?.[key] || find(identity, values)
|
||||
)
|
||||
|
||||
const petnames = profileKeyWithDefault('petnames', [anonPetnames])
|
||||
const relays = profileKeyWithDefault('relays', [anonRelays])
|
||||
|
||||
// Backwards compat, migrate muffle to mute temporarily
|
||||
const mutes = profileKeyWithDefault('mutes', [anonMutes, derived(profile, prop('muffle'))])
|
||||
const settings = derived(profile, prop("settings"))
|
||||
const petnames = derived(profile, prop("petnames"))
|
||||
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
|
||||
const mutes = derived(profile, prop("mutes"))
|
||||
|
||||
const canPublish = derived(
|
||||
[keys.pubkey, relays],
|
||||
([$pubkey, $relays]) =>
|
||||
keys.canSign() && find(prop('write'), $relays)
|
||||
([$pubkey, $relays]) => keys.canSign() && find(prop("write"), $relays)
|
||||
)
|
||||
|
||||
// Keep our copies up to date
|
||||
// Keep a copy so we can avoid calling `get` all the time
|
||||
|
||||
settings.subscribe($settings => {
|
||||
settingsCopy = $settings
|
||||
})
|
||||
let profileCopy = null
|
||||
|
||||
profile.subscribe($profile => {
|
||||
profileCopy = $profile
|
||||
})
|
||||
|
||||
petnames.subscribe($petnames => {
|
||||
petnamesCopy = $petnames
|
||||
// Watch pubkey and add to profile
|
||||
|
||||
keys.pubkey.subscribe($pubkey => {
|
||||
if ($pubkey) {
|
||||
profile.update($p => ({...$p, pubkey: $pubkey}))
|
||||
}
|
||||
})
|
||||
|
||||
mutes.subscribe($mutes => {
|
||||
mutesCopy = $mutes
|
||||
})
|
||||
|
||||
relays.subscribe($relays => {
|
||||
relaysCopy = $relays
|
||||
})
|
||||
|
||||
const user = {
|
||||
// Settings
|
||||
|
||||
settings,
|
||||
getSettings: () => settingsCopy,
|
||||
getSetting: k => settingsCopy[k],
|
||||
dufflepud: path => `${settingsCopy.dufflepudUrl}${path}`,
|
||||
|
||||
export default {
|
||||
// Profile
|
||||
|
||||
profile,
|
||||
@ -93,24 +58,36 @@ const user = {
|
||||
getProfile: () => profileCopy,
|
||||
getPubkey: () => profileCopy?.pubkey,
|
||||
|
||||
// Settings
|
||||
|
||||
settings,
|
||||
getSettings: () => profileCopy.settings,
|
||||
getSetting: k => profileCopy.settings[k],
|
||||
dufflepud: path => `${profileCopy.settings.dufflepudUrl}${path}`,
|
||||
|
||||
// Petnames
|
||||
|
||||
petnames,
|
||||
getPetnames: () => petnamesCopy,
|
||||
getPetnames: () => profileCopy.petnames,
|
||||
petnamePubkeys: derived(petnames, map(nth(1))) as Readable<Array<string>>,
|
||||
updatePetnames(f) {
|
||||
const $petnames = f(petnamesCopy)
|
||||
const $petnames = f(profileCopy.petnames)
|
||||
|
||||
anonPetnames.set($petnames)
|
||||
profile.update(assoc("petnames", $petnames))
|
||||
|
||||
if (profileCopy) {
|
||||
return cmd.setPetnames($petnames).publish(relaysCopy)
|
||||
return cmd.setPetnames($petnames).publish(profileCopy.relays)
|
||||
}
|
||||
},
|
||||
addPetname(pubkey, url, name) {
|
||||
const tag = ["p", pubkey, url, name || ""]
|
||||
|
||||
return this.updatePetnames(pipe(reject(t => t[1] === pubkey), concat([tag])))
|
||||
return this.updatePetnames(
|
||||
pipe(
|
||||
reject(t => t[1] === pubkey),
|
||||
concat([tag])
|
||||
)
|
||||
)
|
||||
},
|
||||
removePetname(pubkey) {
|
||||
return this.updatePetnames(reject(t => t[1] === pubkey))
|
||||
@ -119,11 +96,11 @@ const user = {
|
||||
// Relays
|
||||
|
||||
relays,
|
||||
getRelays: () => relaysCopy,
|
||||
getRelays: () => profileCopy.relays,
|
||||
updateRelays(f) {
|
||||
const $relays = f(relaysCopy)
|
||||
const $relays = f(profileCopy.relays)
|
||||
|
||||
anonRelays.set($relays)
|
||||
profile.update(assoc("relays", $relays))
|
||||
|
||||
if (profileCopy) {
|
||||
return cmd.setRelays($relays).publish($relays)
|
||||
@ -136,28 +113,27 @@ const user = {
|
||||
return this.updateRelays(reject(whereEq({url})))
|
||||
},
|
||||
setRelayWriteCondition(url, write) {
|
||||
return this.updateRelays(map(when(whereEq({url}), assoc('write', write))))
|
||||
return this.updateRelays(map(when(whereEq({url}), assoc("write", write))))
|
||||
},
|
||||
|
||||
// Mutes
|
||||
|
||||
mutes,
|
||||
getMutes: () => mutesCopy,
|
||||
getMutes: () => profileCopy.mutes,
|
||||
applyMutes: events => {
|
||||
const m = new Set(mutesCopy.map(m => m[1]))
|
||||
const m = new Set(profileCopy.mutes.map(m => m[1]))
|
||||
|
||||
return events.filter(e =>
|
||||
!(m.has(e.id) || m.has(e.pubkey) || m.has(findReplyId(e)) || m.has(findRootId(e)))
|
||||
return events.filter(
|
||||
e => !(m.has(e.id) || m.has(e.pubkey) || m.has(findReplyId(e)) || m.has(findRootId(e)))
|
||||
)
|
||||
},
|
||||
updateMutes(f) {
|
||||
const $mutes = f(mutesCopy)
|
||||
console.log(mutesCopy, $mutes)
|
||||
const $mutes = f(profileCopy.mutes)
|
||||
|
||||
anonMutes.set($mutes)
|
||||
profile.update(assoc("mutes", $mutes))
|
||||
|
||||
if (profileCopy) {
|
||||
return cmd.setMutes($mutes.map(slice(0, 2))).publish(relaysCopy)
|
||||
return cmd.setMutes($mutes.map(slice(0, 2))).publish(profileCopy.relays)
|
||||
}
|
||||
},
|
||||
addMute(type, value) {
|
||||
@ -172,5 +148,3 @@ const user = {
|
||||
return this.updateMutes(reject(t => t[1] === pubkey))
|
||||
},
|
||||
}
|
||||
|
||||
export default user
|
||||
|
@ -1,13 +1,13 @@
|
||||
import type {DisplayEvent} from 'src/util/types'
|
||||
import {max, find, pluck, propEq, partition, uniq} from 'ramda'
|
||||
import {derived} from 'svelte/store'
|
||||
import {createMap} from 'hurdak/lib/hurdak'
|
||||
import {synced, tryJson, now, timedelta} from 'src/util/misc'
|
||||
import {Tags, personKinds, isAlert, asDisplayEvent, findReplyId} from 'src/util/nostr'
|
||||
import {getUserReadRelays} from 'src/agent/relays'
|
||||
import {alerts, contacts, rooms} from 'src/agent/state'
|
||||
import {watch} from 'src/agent/table'
|
||||
import network from 'src/agent/network'
|
||||
import type {DisplayEvent} from "src/util/types"
|
||||
import {max, find, pluck, propEq, partition, uniq} from "ramda"
|
||||
import {derived} from "svelte/store"
|
||||
import {createMap} from "hurdak/lib/hurdak"
|
||||
import {synced, tryJson, now, timedelta} from "src/util/misc"
|
||||
import {Tags, userKinds, isAlert, asDisplayEvent, findReplyId} from "src/util/nostr"
|
||||
import {getUserReadRelays} from "src/agent/relays"
|
||||
import {alerts, contacts, rooms} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import network from "src/agent/network"
|
||||
|
||||
let listener
|
||||
|
||||
@ -20,25 +20,23 @@ type AlertEvent = DisplayEvent & {
|
||||
|
||||
// State
|
||||
|
||||
const seenAlertIds = synced('app/alerts/seenAlertIds', [])
|
||||
const seenAlertIds = synced("app/alerts/seenAlertIds", [])
|
||||
|
||||
export const lastChecked = synced('app/alerts/lastChecked', {})
|
||||
export const lastChecked = synced("app/alerts/lastChecked", {})
|
||||
|
||||
export const newAlerts = derived(
|
||||
[watch('alerts', t => pluck('created_at', t.all()).reduce(max, 0)), lastChecked],
|
||||
[watch("alerts", t => pluck("created_at", t.all()).reduce(max, 0)), lastChecked],
|
||||
([$lastAlert, $lastChecked]) => $lastAlert > ($lastChecked.alerts || 0)
|
||||
)
|
||||
|
||||
export const newDirectMessages = derived(
|
||||
[watch('contacts', t => t.all()), lastChecked],
|
||||
([contacts, $lastChecked]) =>
|
||||
Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], contacts))
|
||||
[watch("contacts", t => t.all()), lastChecked],
|
||||
([contacts, $lastChecked]) => Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], contacts))
|
||||
)
|
||||
|
||||
export const newChatMessages = derived(
|
||||
[watch('rooms', t => t.all()), lastChecked],
|
||||
([rooms, $lastChecked]) =>
|
||||
Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], rooms))
|
||||
[watch("rooms", t => t.all()), lastChecked],
|
||||
([rooms, $lastChecked]) => Boolean(find(c => c.lastMessage > $lastChecked[c.pubkey], rooms))
|
||||
)
|
||||
|
||||
// Synchronization from events to state
|
||||
@ -59,10 +57,15 @@ const processAlerts = async (pubkey, events) => {
|
||||
return
|
||||
}
|
||||
|
||||
const parents = createMap('id', await network.loadParents(events))
|
||||
const parents = createMap("id", await network.loadParents(events))
|
||||
|
||||
const asAlert = (e): AlertEvent =>
|
||||
({repliesFrom: [], likedBy: [], zappedBy: [], isMention: false, ...asDisplayEvent(e)})
|
||||
const asAlert = (e): AlertEvent => ({
|
||||
repliesFrom: [],
|
||||
likedBy: [],
|
||||
zappedBy: [],
|
||||
isMention: false,
|
||||
...asDisplayEvent(e),
|
||||
})
|
||||
|
||||
const isPubkeyChild = e => {
|
||||
const parentId = findReplyId(e)
|
||||
@ -70,9 +73,9 @@ const processAlerts = async (pubkey, events) => {
|
||||
return parents[parentId]?.pubkey === pubkey
|
||||
}
|
||||
|
||||
const [replies, mentions] = partition(isPubkeyChild, events.filter(propEq('kind', 1)))
|
||||
const likes = events.filter(propEq('kind', 7))
|
||||
const zaps = events.filter(propEq('kind', 9735))
|
||||
const [replies, mentions] = partition(isPubkeyChild, events.filter(propEq("kind", 1)))
|
||||
const likes = events.filter(propEq("kind", 7))
|
||||
const zaps = events.filter(propEq("kind", 9735))
|
||||
|
||||
zaps.filter(isPubkeyChild).forEach(e => {
|
||||
const parent = parents[findReplyId(e)]
|
||||
@ -107,7 +110,7 @@ const processAlerts = async (pubkey, events) => {
|
||||
}
|
||||
|
||||
const processMessages = async (pubkey, events) => {
|
||||
const messages = events.filter(propEq('kind', 4))
|
||||
const messages = events.filter(propEq("kind", 4))
|
||||
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
@ -133,7 +136,7 @@ const processMessages = async (pubkey, events) => {
|
||||
}
|
||||
|
||||
const processChats = async (pubkey, events) => {
|
||||
const messages = events.filter(propEq('kind', 42))
|
||||
const messages = events.filter(propEq("kind", 42))
|
||||
|
||||
if (messages.length === 0) {
|
||||
return
|
||||
@ -159,8 +162,8 @@ const processChats = async (pubkey, events) => {
|
||||
|
||||
const listen = async pubkey => {
|
||||
// Include an offset so we don't miss alerts on one relay but not another
|
||||
const since = now() - timedelta(7, 'days')
|
||||
const roomIds = pluck('id', rooms.all({joined: true}))
|
||||
const since = now() - timedelta(7, "days")
|
||||
const roomIds = pluck("id", rooms.all({joined: true}))
|
||||
|
||||
if (listener) {
|
||||
listener.unsub()
|
||||
@ -170,13 +173,13 @@ const listen = async pubkey => {
|
||||
delay: 10000,
|
||||
relays: getUserReadRelays(),
|
||||
filter: [
|
||||
{kinds: personKinds, authors: [pubkey], since},
|
||||
{kinds: userKinds, authors: [pubkey], since},
|
||||
{kinds: [4], authors: [pubkey], since},
|
||||
{kinds: [1, 7, 4, 9735], '#p': [pubkey], since},
|
||||
{kinds: [42], '#e': roomIds, since},
|
||||
{kinds: [1, 7, 4, 9735], "#p": [pubkey], since},
|
||||
{kinds: [42], "#e": roomIds, since},
|
||||
],
|
||||
onChunk: async events => {
|
||||
await network.loadPeople(pluck('pubkey', events))
|
||||
await network.loadPeople(pluck("pubkey", events))
|
||||
await processMessages(pubkey, events)
|
||||
await processAlerts(pubkey, events)
|
||||
await processChats(pubkey, events)
|
||||
|
@ -1,15 +1,15 @@
|
||||
import type {DisplayEvent} from 'src/util/types'
|
||||
import {omit, sortBy} from 'ramda'
|
||||
import {createMap, ellipsize} from 'hurdak/lib/hurdak'
|
||||
import {renderContent} from 'src/util/html'
|
||||
import {displayPerson, findReplyId} from 'src/util/nostr'
|
||||
import {getUserFollows} from 'src/agent/social'
|
||||
import {getUserReadRelays} from 'src/agent/relays'
|
||||
import {getPersonWithFallback} from 'src/agent/state'
|
||||
import network from 'src/agent/network'
|
||||
import keys from 'src/agent/keys'
|
||||
import alerts from 'src/app/alerts'
|
||||
import {routes, modal, toast} from 'src/app/ui'
|
||||
import type {DisplayEvent} from "src/util/types"
|
||||
import {omit, sortBy} from "ramda"
|
||||
import {createMap, ellipsize} from "hurdak/lib/hurdak"
|
||||
import {renderContent} from "src/util/html"
|
||||
import {displayPerson, findReplyId} from "src/util/nostr"
|
||||
import {getUserFollows} from "src/agent/social"
|
||||
import {getUserReadRelays} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import network from "src/agent/network"
|
||||
import keys from "src/agent/keys"
|
||||
import alerts from "src/app/alerts"
|
||||
import {routes, modal, toast} from "src/app/ui"
|
||||
|
||||
export const loadAppData = async pubkey => {
|
||||
if (getUserReadRelays().length > 0) {
|
||||
@ -24,23 +24,20 @@ export const loadAppData = async pubkey => {
|
||||
export const login = (method, key) => {
|
||||
keys.login(method, key)
|
||||
|
||||
modal.set({type: 'login/connect', noEscape: true})
|
||||
modal.set({type: "login/connect", noEscape: true})
|
||||
}
|
||||
|
||||
export const renderNote = (note, {showEntire = false}) => {
|
||||
let content
|
||||
|
||||
// Ellipsize
|
||||
content = note.content.length > 500 && !showEntire
|
||||
? ellipsize(note.content, 500)
|
||||
: note.content
|
||||
content = note.content.length > 500 && !showEntire ? ellipsize(note.content, 500) : note.content
|
||||
|
||||
// Escape html, replace urls
|
||||
content = renderContent(content)
|
||||
|
||||
// Mentions
|
||||
content = content
|
||||
.replace(/#\[(\d+)\]/g, (tag, i) => {
|
||||
content = content.replace(/#\[(\d+)\]/g, (tag, i) => {
|
||||
if (!note.tags[parseInt(i)]) {
|
||||
return tag
|
||||
}
|
||||
@ -57,7 +54,7 @@ export const renderNote = (note, {showEntire = false}) => {
|
||||
}
|
||||
|
||||
export const mergeParents = (notes: Array<DisplayEvent>) => {
|
||||
const notesById = createMap('id', notes) as Record<string, DisplayEvent>
|
||||
const notesById = createMap("id", notes) as Record<string, DisplayEvent>
|
||||
const childIds = []
|
||||
|
||||
for (const note of Object.values(notesById)) {
|
||||
@ -94,8 +91,8 @@ export const publishWithToast = (relays, thunk) =>
|
||||
}
|
||||
|
||||
if (extra.length > 0) {
|
||||
message += ` (${extra.join(', ')})`
|
||||
message += ` (${extra.join(", ")})`
|
||||
}
|
||||
|
||||
toast.show('info', message, pending.size ? null : 5)
|
||||
toast.show("info", message, pending.size ? null : 5)
|
||||
})
|
||||
|
@ -6,7 +6,7 @@
|
||||
import {sleep, createScroller, Cursor} from "src/util/misc"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import user from "src/agent/user"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import network from "src/agent/network"
|
||||
|
||||
export let loadMessages
|
||||
|
@ -7,7 +7,7 @@
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import {fromParentOffset} from "src/util/html"
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import {people} from "src/agent/state"
|
||||
import {people} from "src/agent/tables"
|
||||
|
||||
export let onSubmit
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Alert from "src/views/alerts/Alert.svelte"
|
||||
import Mention from "src/views/alerts/Mention.svelte"
|
||||
import {alerts} from "src/agent/state"
|
||||
import {alerts} from "src/agent/tables"
|
||||
import user from "src/agent/user"
|
||||
import {lastChecked} from "src/app/alerts"
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
import user from "src/agent/user"
|
||||
import {getRelaysForEventChildren, sampleRelays} from "src/agent/relays"
|
||||
import network from "src/agent/network"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {modal} from "src/app/ui"
|
||||
import {lastChecked} from "src/app/alerts"
|
||||
|
@ -5,7 +5,7 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import ChatListItem from "src/views/chat/ChatListItem.svelte"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
import network from "src/agent/network"
|
||||
import {getUserReadRelays} from "src/agent/relays"
|
||||
import {modal} from "src/app/ui"
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {fly} from "svelte/transition"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import {dropAll} from "src/agent/table"
|
||||
import {dropAll} from "src/agent/storage"
|
||||
|
||||
let confirmed = false
|
||||
|
||||
|
@ -8,8 +8,8 @@
|
||||
import Channel from "src/partials/Channel.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {getAllPubkeyRelays, sampleRelays} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {watch} from "src/agent/table"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import network from "src/agent/network"
|
||||
import keys from "src/agent/keys"
|
||||
import user from "src/agent/user"
|
||||
|
@ -4,7 +4,7 @@
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import MessagesListItem from "src/views/messages/MessagesListItem.svelte"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
|
||||
let activeTab = "messages"
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
import user from "src/agent/user"
|
||||
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
||||
import network from "src/agent/network"
|
||||
import {getPersonWithFallback, people} from "src/agent/state"
|
||||
import {getPersonWithFallback, people} from "src/agent/tables"
|
||||
import {routes, modal} from "src/app/ui"
|
||||
import PersonCircle from "src/partials/PersonCircle.svelte"
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Feed from "src/views/feed/Feed.svelte"
|
||||
import {relays} from "src/agent/state"
|
||||
import {relays} from "src/agent/tables"
|
||||
import pool from "src/agent/pool"
|
||||
import user from "src/agent/user"
|
||||
|
||||
|
154
src/util/misc.ts
154
src/util/misc.ts
@ -1,10 +1,25 @@
|
||||
import {bech32, utf8} from '@scure/base'
|
||||
import {debounce, throttle} from 'throttle-debounce'
|
||||
import {gt, aperture, path as getPath, allPass, pipe, isNil, complement, equals, is, pluck, sum, identity, sortBy} from "ramda"
|
||||
import {bech32, utf8} from "@scure/base"
|
||||
import {debounce, throttle} from "throttle-debounce"
|
||||
import {
|
||||
gt,
|
||||
mergeDeepRight,
|
||||
aperture,
|
||||
path as getPath,
|
||||
allPass,
|
||||
pipe,
|
||||
isNil,
|
||||
complement,
|
||||
equals,
|
||||
is,
|
||||
pluck,
|
||||
sum,
|
||||
identity,
|
||||
sortBy,
|
||||
} from "ramda"
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
import {writable} from 'svelte/store'
|
||||
import {isObject, round} from 'hurdak/lib/hurdak'
|
||||
import {warn} from 'src/util/logger'
|
||||
import {writable} from "svelte/store"
|
||||
import {isObject, round} from "hurdak/lib/hurdak"
|
||||
import {warn} from "src/util/logger"
|
||||
|
||||
export const fuzzy = (data, opts = {}) => {
|
||||
const fuse = new Fuse(data, opts)
|
||||
@ -16,7 +31,7 @@ export const fuzzy = (data, opts = {}) => {
|
||||
export const hash = s =>
|
||||
Math.abs(s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0))
|
||||
|
||||
export const getLocalJson = (k) => {
|
||||
export const getLocalJson = k => {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(k))
|
||||
} catch (e) {
|
||||
@ -36,20 +51,29 @@ export const setLocalJson = (k, v) => {
|
||||
|
||||
export const now = () => Math.round(new Date().valueOf() / 1000)
|
||||
|
||||
export const timedelta = (n, unit = 'seconds') => {
|
||||
export const timedelta = (n, unit = "seconds") => {
|
||||
switch (unit) {
|
||||
case 'seconds': case 'second': return n
|
||||
case 'minutes': case 'minute': return n * 60
|
||||
case 'hours': case 'hour': return n * 60 * 60
|
||||
case 'days': case 'day': return n * 60 * 60 * 24
|
||||
default: throw new Error(`Invalid unit ${unit}`)
|
||||
case "seconds":
|
||||
case "second":
|
||||
return n
|
||||
case "minutes":
|
||||
case "minute":
|
||||
return n * 60
|
||||
case "hours":
|
||||
case "hour":
|
||||
return n * 60 * 60
|
||||
case "days":
|
||||
case "day":
|
||||
return n * 60 * 60 * 24
|
||||
default:
|
||||
throw new Error(`Invalid unit ${unit}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const formatTimestamp = ts => {
|
||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
const formatter = new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
})
|
||||
|
||||
return formatter.format(new Date(ts * 1000))
|
||||
@ -58,21 +82,21 @@ export const formatTimestamp = ts => {
|
||||
export const formatTimestampRelative = ts => {
|
||||
let unit
|
||||
let delta = now() - ts
|
||||
if (delta < timedelta(1, 'minute')) {
|
||||
unit = 'second'
|
||||
} else if (delta < timedelta(1, 'hour')) {
|
||||
unit = 'minute'
|
||||
delta = Math.round(delta / timedelta(1, 'minute'))
|
||||
} else if (delta < timedelta(2, 'day')) {
|
||||
unit = 'hour'
|
||||
delta = Math.round(delta / timedelta(1, 'hour'))
|
||||
if (delta < timedelta(1, "minute")) {
|
||||
unit = "second"
|
||||
} else if (delta < timedelta(1, "hour")) {
|
||||
unit = "minute"
|
||||
delta = Math.round(delta / timedelta(1, "minute"))
|
||||
} else if (delta < timedelta(2, "day")) {
|
||||
unit = "hour"
|
||||
delta = Math.round(delta / timedelta(1, "hour"))
|
||||
} else {
|
||||
unit = 'day'
|
||||
delta = Math.round(delta / timedelta(1, 'day'))
|
||||
unit = "day"
|
||||
delta = Math.round(delta / timedelta(1, "day"))
|
||||
}
|
||||
|
||||
const formatter = new Intl.RelativeTimeFormat('en-US', {
|
||||
numeric: 'auto',
|
||||
const formatter = new Intl.RelativeTimeFormat("en-US", {
|
||||
numeric: "auto",
|
||||
})
|
||||
|
||||
return formatter.format(-delta, unit as Intl.RelativeTimeFormatUnit)
|
||||
@ -137,15 +161,6 @@ export const createScroller = (loadMore, {reverse = false} = {}) => {
|
||||
|
||||
export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)]
|
||||
|
||||
export const getLastSync = (k, fallback = 0) => {
|
||||
const key = `${k}/lastSync`
|
||||
const lastSync = getLocalJson(key) || fallback
|
||||
|
||||
setLocalJson(key, now())
|
||||
|
||||
return lastSync
|
||||
}
|
||||
|
||||
export class Cursor {
|
||||
until: number
|
||||
limit: number
|
||||
@ -179,17 +194,14 @@ export class Cursor {
|
||||
// feed. Multiply it by the number of events we have but scale down to avoid
|
||||
// blowing past big gaps due to misbehaving relays skewing the results. Trim off
|
||||
// outliers and scale based on results/requests to help with that
|
||||
const timestamps = sortBy(identity, pluck('created_at', events))
|
||||
const timestamps = sortBy(identity, pluck("created_at", events))
|
||||
const gaps = aperture(2, timestamps).map(([a, b]) => b - a)
|
||||
const high = quantile(gaps, 0.5)
|
||||
const gap = avg(gaps.filter(gt(high)))
|
||||
|
||||
// If we're just warming up, scale the window down even further to avoid
|
||||
// blowing past the most relevant time period
|
||||
const scale = (
|
||||
Math.min(1, Math.log10(events.length))
|
||||
* Math.min(1, Math.log10(this.count + 1))
|
||||
)
|
||||
const scale = Math.min(1, Math.log10(events.length)) * Math.min(1, Math.log10(this.count + 1))
|
||||
|
||||
// Only paginate part of the way so we can avoid missing stuff
|
||||
this.until -= Math.round(gap * scale * this.limit)
|
||||
@ -201,8 +213,8 @@ export const synced = (key, defaultValue = null) => {
|
||||
// If it's an object, merge defaults
|
||||
const store = writable(
|
||||
isObject(defaultValue)
|
||||
? {...defaultValue, ...getLocalJson(key)}
|
||||
: (getLocalJson(key) || defaultValue)
|
||||
? mergeDeepRight(defaultValue, getLocalJson(key) || {})
|
||||
: getLocalJson(key) || defaultValue
|
||||
)
|
||||
|
||||
store.subscribe(debounce(1000, $value => setLocalJson(key, $value)))
|
||||
@ -236,30 +248,31 @@ export const avg = xs => sum(xs) / xs.length
|
||||
|
||||
export const where = filters =>
|
||||
allPass(
|
||||
Object.entries(filters)
|
||||
.map(([key, value]) => {
|
||||
Object.entries(filters).map(([key, value]) => {
|
||||
/* eslint prefer-const: 0 */
|
||||
let [field, operator = 'eq'] = key.split(':')
|
||||
let test, modifier = identity, parts = field.split('.')
|
||||
let [field, operator = "eq"] = key.split(":")
|
||||
let test,
|
||||
modifier = identity,
|
||||
parts = field.split(".")
|
||||
|
||||
if (operator.startsWith('!')) {
|
||||
if (operator.startsWith("!")) {
|
||||
operator = operator.slice(1)
|
||||
modifier = complement
|
||||
}
|
||||
|
||||
if (operator === 'eq' && is(Array, value)) {
|
||||
if (operator === "eq" && is(Array, value)) {
|
||||
test = v => (value as Array<any>).includes(v)
|
||||
} else if (operator === 'eq') {
|
||||
} else if (operator === "eq") {
|
||||
test = equals(value)
|
||||
} else if (operator === 'lt') {
|
||||
} else if (operator === "lt") {
|
||||
test = v => (v || 0) < value
|
||||
} else if (operator === 'lte') {
|
||||
} else if (operator === "lte") {
|
||||
test = v => (v || 0) <= value
|
||||
} else if (operator === 'gt') {
|
||||
} else if (operator === "gt") {
|
||||
test = v => (v || 0) > value
|
||||
} else if (operator === 'gte') {
|
||||
} else if (operator === "gte") {
|
||||
test = v => (v || 0) >= value
|
||||
} else if (operator === 'nil') {
|
||||
} else if (operator === "nil") {
|
||||
test = isNil
|
||||
} else {
|
||||
throw new Error(`Invalid operator ${operator}`)
|
||||
@ -271,7 +284,7 @@ export const where = filters =>
|
||||
|
||||
// https://stackoverflow.com/a/21682946
|
||||
export const stringToHue = value => {
|
||||
let hash = 0;
|
||||
let hash = 0
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
hash = value.charCodeAt(i) + ((hash << 5) - hash)
|
||||
hash = hash & hash
|
||||
@ -303,14 +316,12 @@ export const tryFunc = (f, ignore = null) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const tryJson = f => tryFunc(f, 'JSON')
|
||||
export const tryFetch = f => tryFunc(f, 'fetch')
|
||||
export const tryJson = f => tryFunc(f, "JSON")
|
||||
export const tryFetch = f => tryFunc(f, "fetch")
|
||||
|
||||
export const union = (...sets) =>
|
||||
new Set(sets.flatMap(s => Array.from(s)))
|
||||
export const union = (...sets) => new Set(sets.flatMap(s => Array.from(s)))
|
||||
|
||||
export const difference = (a, b) =>
|
||||
new Set(Array.from(a).filter(x => !b.has(x)))
|
||||
export const difference = (a, b) => new Set(Array.from(a).filter(x => !b.has(x)))
|
||||
|
||||
export const quantile = (a, q) => {
|
||||
const sorted = sortBy(identity, a)
|
||||
@ -334,7 +345,7 @@ export const fetchJson = async (url, opts: FetchOpts = {}) => {
|
||||
opts.headers = {}
|
||||
}
|
||||
|
||||
opts.headers['Accept'] = 'application/json'
|
||||
opts.headers["Accept"] = "application/json"
|
||||
|
||||
const res = await fetch(url, opts as RequestInit)
|
||||
const json = await res.json()
|
||||
@ -344,14 +355,14 @@ export const fetchJson = async (url, opts: FetchOpts = {}) => {
|
||||
|
||||
export const postJson = async (url, data, opts: FetchOpts = {}) => {
|
||||
if (!opts.method) {
|
||||
opts.method = 'POST'
|
||||
opts.method = "POST"
|
||||
}
|
||||
|
||||
if (!opts.headers) {
|
||||
opts.headers = {}
|
||||
}
|
||||
|
||||
opts.headers['Content-Type'] = 'application/json'
|
||||
opts.headers["Content-Type"] = "application/json"
|
||||
opts.body = JSON.stringify(data)
|
||||
|
||||
return fetchJson(url, opts)
|
||||
@ -362,20 +373,19 @@ export const uploadFile = (url, fileObj) => {
|
||||
|
||||
body.append("file", fileObj)
|
||||
|
||||
return fetchJson(url, {method: 'POST', body})
|
||||
return fetchJson(url, {method: "POST", body})
|
||||
}
|
||||
|
||||
export const lnurlEncode = (prefix, url) =>
|
||||
bech32.encode(prefix, bech32.toWords(utf8.decode(url)), false)
|
||||
|
||||
export const lnurlDecode = b32 =>
|
||||
utf8.encode(bech32.fromWords(bech32.decode(b32, false).words))
|
||||
export const lnurlDecode = b32 => utf8.encode(bech32.fromWords(bech32.decode(b32, false).words))
|
||||
|
||||
export const formatSats = sats => {
|
||||
const formatter = new Intl.NumberFormat()
|
||||
|
||||
if (sats < 1_000) return formatter.format(sats)
|
||||
if (sats < 1_000_000) return formatter.format(round(1, sats / 1000)) + 'K'
|
||||
if (sats < 100_000_000) return formatter.format(round(1, sats / 1_000_000)) + 'MM'
|
||||
return formatter.format(round(2, sats / 100_000_000)) + 'BTC'
|
||||
if (sats < 1_000_000) return formatter.format(round(1, sats / 1000)) + "K"
|
||||
if (sats < 100_000_000) return formatter.format(round(1, sats / 1_000_000)) + "MM"
|
||||
return formatter.format(round(2, sats / 100_000_000)) + "BTC"
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import type {DisplayEvent} from 'src/util/types'
|
||||
import {is, fromPairs, mergeLeft, last, identity, objOf, prop, flatten, uniq} from 'ramda'
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {ensurePlural, ellipsize, first} from 'hurdak/lib/hurdak'
|
||||
import type {DisplayEvent} from "src/util/types"
|
||||
import {is, fromPairs, mergeLeft, last, identity, objOf, prop, flatten, uniq} from "ramda"
|
||||
import {nip19} from "nostr-tools"
|
||||
import {ensurePlural, ellipsize, first} from "hurdak/lib/hurdak"
|
||||
|
||||
export const personKinds = [0, 2, 3, 10000, 10001, 10002, 12165]
|
||||
export const personKinds = [0, 2, 3, 10001, 10002]
|
||||
export const userKinds = personKinds.concat([10000])
|
||||
|
||||
export class Tags {
|
||||
tags: Array<any>
|
||||
@ -11,7 +12,7 @@ export class Tags {
|
||||
this.tags = tags
|
||||
}
|
||||
static from(events) {
|
||||
return new Tags(ensurePlural(events).flatMap(prop('tags')))
|
||||
return new Tags(ensurePlural(events).flatMap(prop("tags")))
|
||||
}
|
||||
static wrap(tags) {
|
||||
return new Tags((tags || []).filter(identity))
|
||||
@ -26,7 +27,7 @@ export class Tags {
|
||||
return last(this.tags)
|
||||
}
|
||||
relays() {
|
||||
return uniq(flatten(this.tags).filter(isShareableRelay)).map(objOf('url'))
|
||||
return uniq(flatten(this.tags).filter(isShareableRelay)).map(objOf("url"))
|
||||
}
|
||||
pubkeys() {
|
||||
return this.type("p").values().all()
|
||||
@ -55,12 +56,17 @@ export class Tags {
|
||||
export const findReply = e =>
|
||||
Tags.from(e).type("e").mark("reply").first() || Tags.from(e).type("e").last()
|
||||
|
||||
export const findReplyId = e => Tags.wrap([findReply(e)]).values().first()
|
||||
export const findReplyId = e =>
|
||||
Tags.wrap([findReply(e)])
|
||||
.values()
|
||||
.first()
|
||||
|
||||
export const findRoot = e =>
|
||||
Tags.from(e).type("e").mark("root").first()
|
||||
export const findRoot = e => Tags.from(e).type("e").mark("root").first()
|
||||
|
||||
export const findRootId = e => Tags.wrap([findRoot(e)]).values().first()
|
||||
export const findRootId = e =>
|
||||
Tags.wrap([findRoot(e)])
|
||||
.values()
|
||||
.first()
|
||||
|
||||
export const displayPerson = p => {
|
||||
if (p.kind0?.display_name) {
|
||||
@ -76,13 +82,13 @@ export const displayPerson = p => {
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
return ''
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
export const displayRelay = ({url}) => last(url.split('://'))
|
||||
export const displayRelay = ({url}) => last(url.split("://"))
|
||||
|
||||
export const isLike = content => ['', '+', '🤙', '👍', '❤️', '😎', '🏅'].includes(content)
|
||||
export const isLike = content => ["", "+", "🤙", "👍", "❤️", "😎", "🏅"].includes(content)
|
||||
|
||||
export const isAlert = (e, pubkey) => {
|
||||
if (![1, 7, 9735].includes(e.kind)) {
|
||||
@ -102,28 +108,26 @@ export const isAlert = (e, pubkey) => {
|
||||
return true
|
||||
}
|
||||
|
||||
export const isRelay = url => (
|
||||
typeof url === 'string'
|
||||
export const isRelay = url =>
|
||||
typeof url === "string" &&
|
||||
// It should have the protocol included
|
||||
&& url.match(/^wss?:\/\/.+/)
|
||||
)
|
||||
url.match(/^wss?:\/\/.+/)
|
||||
|
||||
export const isShareableRelay = url => (
|
||||
isRelay(url)
|
||||
export const isShareableRelay = url =>
|
||||
isRelay(url) &&
|
||||
// Don't match stuff with a port number
|
||||
&& !url.slice(6).match(/:\d+/)
|
||||
!url.slice(6).match(/:\d+/) &&
|
||||
// Don't match raw ip addresses
|
||||
&& !url.slice(6).match(/\d+\.\d+\.\d+\.\d+/)
|
||||
!url.slice(6).match(/\d+\.\d+\.\d+\.\d+/) &&
|
||||
// Skip nostr.wine's virtual relays
|
||||
&& !url.slice(6).match(/\/npub/)
|
||||
)
|
||||
!url.slice(6).match(/\/npub/)
|
||||
|
||||
export const normalizeRelayUrl = url => url.replace(/\/+$/, '').toLowerCase().trim()
|
||||
export const normalizeRelayUrl = url => url.replace(/\/+$/, "").toLowerCase().trim()
|
||||
|
||||
export const roomAttrs = ['name', 'about', 'picture']
|
||||
export const roomAttrs = ["name", "about", "picture"]
|
||||
|
||||
export const asDisplayEvent = event =>
|
||||
({replies: [], reactions: [], zaps: [], ...event}) as DisplayEvent
|
||||
({replies: [], reactions: [], zaps: [], ...event} as DisplayEvent)
|
||||
|
||||
export const toHex = (data: string): string | null => {
|
||||
try {
|
||||
|
@ -6,7 +6,7 @@
|
||||
import RelaySearch from "src/views/relays/RelaySearch.svelte"
|
||||
import RelayCard from "src/views/relays/RelayCard.svelte"
|
||||
import PersonSearch from "src/views/person/PersonSearch.svelte"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import user from "src/agent/user"
|
||||
|
||||
export let enforceRelays = true
|
||||
|
@ -21,7 +21,7 @@
|
||||
const submit = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
user.settings.set(values)
|
||||
user.profile.update($p => ({...$p, settings: values}))
|
||||
|
||||
toast.show("info", "Your settings have been saved!")
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
import {menuIsOpen, installPrompt, routes} from "src/app/ui"
|
||||
import {newAlerts, newDirectMessages, newChatMessages} from "src/app/alerts"
|
||||
import {slowConnections} from "src/app/connection"
|
||||
import PersonCircle from "src/partials/PersonCircle.svelte";
|
||||
import PersonCircle from "src/partials/PersonCircle.svelte"
|
||||
|
||||
const {profile, canPublish} = user
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
class="fixed top-0 bottom-0 left-0 z-20 mt-16 w-56 overflow-hidden border-r border-medium bg-dark pt-4
|
||||
pb-20 text-white shadow-xl transition-all lg:mt-0 lg:ml-0"
|
||||
class:-ml-56={!$menuIsOpen}>
|
||||
{#if $profile}
|
||||
{#if $profile.pubkey}
|
||||
<li>
|
||||
<a href={routes.person($profile.pubkey)} class="flex items-center gap-2 px-4 py-2 pb-6">
|
||||
<PersonCircle size={6} person={$profile} />
|
||||
|
@ -3,7 +3,7 @@
|
||||
import Badge from "src/partials/Badge.svelte"
|
||||
import Popover from "src/partials/Popover.svelte"
|
||||
import {formatTimestamp} from "src/util/misc"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
export let note
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import Popover from "src/partials/Popover.svelte"
|
||||
import PersonSummary from "src/views/person/PersonSummary.svelte"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {modal} from "src/app/ui"
|
||||
import PersonCircle from "src/partials/PersonCircle.svelte"
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import {getUserWriteRelays} from "src/agent/relays"
|
||||
import {rooms} from "src/agent/state"
|
||||
import {rooms} from "src/agent/tables"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {toast, modal} from "src/app/ui"
|
||||
import {publishWithToast} from "src/app"
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {fly} from "svelte/transition"
|
||||
import {ellipsize} from "hurdak/lib/hurdak"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {rooms} from "src/agent/state"
|
||||
import {rooms} from "src/agent/tables"
|
||||
|
||||
export let room
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
import {navigate} from "svelte-routing"
|
||||
import {sleep, shuffle} from "src/util/misc"
|
||||
import {isRelay} from "src/util/nostr"
|
||||
import {isRelay, userKinds} from "src/util/nostr"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
@ -12,7 +12,7 @@
|
||||
import RelayCardSimple from "src/partials/RelayCardSimple.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Modal from "src/partials/Modal.svelte"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
import network from "src/agent/network"
|
||||
import user from "src/agent/user"
|
||||
import {loadAppData} from "src/app"
|
||||
@ -48,7 +48,9 @@
|
||||
attemptedRelays.add(relay.url)
|
||||
currentRelays[i] = relay
|
||||
|
||||
network.loadPeople([user.getPubkey()], {relays: [relay], force: true}).then(async () => {
|
||||
network
|
||||
.loadPeople([user.getPubkey()], {relays: [relay], force: true, kinds: userKinds})
|
||||
.then(async () => {
|
||||
// Wait a bit before removing the relay to smooth out the ui
|
||||
await sleep(1000)
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
import {fly} from "svelte/transition"
|
||||
import {ellipsize} from "hurdak/lib/hurdak"
|
||||
import {displayPerson} from "src/util/nostr"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {lastChecked} from "src/app/alerts"
|
||||
import PersonCircle from "src/partials/PersonCircle.svelte"
|
||||
|
||||
|
@ -29,8 +29,8 @@
|
||||
import keys from "src/agent/keys"
|
||||
import network from "src/agent/network"
|
||||
import {getEventPublishRelays, getRelaysForEventParent} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {watch} from "src/agent/table"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {routes} from "src/app/ui"
|
||||
import {publishWithToast} from "src/app"
|
||||
|
@ -16,8 +16,8 @@
|
||||
import Modal from "src/partials/Modal.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import {getUserWriteRelays} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {watch} from "src/agent/table"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import cmd from "src/agent/cmd"
|
||||
import {toast, modal} from "src/app/ui"
|
||||
import {publishWithToast} from "src/app"
|
||||
|
@ -13,7 +13,7 @@
|
||||
import OnboardingComplete from "src/views/onboarding/OnboardingComplete.svelte"
|
||||
import {getFollows} from "src/agent/social"
|
||||
import {getPubkeyWriteRelays, sampleRelays} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import network from "src/agent/network"
|
||||
import user from "src/agent/user"
|
||||
import keys from "src/agent/keys"
|
||||
|
@ -6,8 +6,8 @@
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import PersonInfo from "src/partials/PersonInfo.svelte"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {watch} from "src/agent/table"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
export let follows
|
||||
|
@ -6,7 +6,7 @@
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import RelayCard from "src/partials/RelayCard.svelte"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
import {modal} from "src/app/ui"
|
||||
|
||||
export let relays
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script type="ts">
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import PersonInfo from "src/views/person/PersonInfo.svelte"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {watch} from "src/agent/table"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import network from "src/agent/network"
|
||||
|
||||
export let pubkeys
|
||||
|
@ -5,7 +5,7 @@
|
||||
import Spinner from "src/partials/Spinner.svelte"
|
||||
import PersonInfo from "src/views/person/PersonInfo.svelte"
|
||||
import {getUserReadRelays} from "src/agent/relays"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
import network from "src/agent/network"
|
||||
import user from "src/agent/user"
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import user from "src/agent/user"
|
||||
import {sampleRelays, getPubkeyWriteRelays} from "src/agent/relays"
|
||||
import {getPersonWithFallback} from "src/agent/state"
|
||||
import {watch} from "src/agent/table"
|
||||
import {getPersonWithFallback} from "src/agent/tables"
|
||||
import {watch} from "src/agent/storage"
|
||||
import {routes} from "src/app/ui"
|
||||
import PersonCircle from "src/partials/PersonCircle.svelte"
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import RelayCard from "src/views/relays/RelayCard.svelte"
|
||||
import {watch} from "src/agent/table"
|
||||
import {watch} from "src/agent/storage"
|
||||
import user from "src/agent/user"
|
||||
|
||||
let q = ""
|
||||
|
Loading…
Reference in New Issue
Block a user