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