Reworking network stuff again

This commit is contained in:
Jonathan Staab 2022-12-19 06:46:22 -08:00
parent 4ec8178660
commit f2d1b0c951
21 changed files with 254 additions and 258 deletions

View File

@ -33,21 +33,21 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
- [ ] Stack views so scroll position isn't lost on navigation
- [ ] We're sending client=astral tags, event id 125ff9dc495f65d302e8d95ea6f9385106cc31b81c80e8c582b44be92fa50c44
# Workers
# Curreent update
- [ ] Check firefox - in dev it won't work, but it should in production
- [ ] Re-implement muffle
- [ ] Delete old events
- [ ] Sync accounts to store to avoid loading jank
- [ ] Sync account updates to user for e.g. muffle settings
- [ ] Test nos2x
- https://vitejs.dev/guide/features.html#web-workers
- https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
- https://web.dev/module-workers/
- [ ] Sync user
- [ ] Based on petnames, sync network to 2 or 3 degrees of separation
- When a user is added/removed, sync them and add to or remove from network
- [ ] Main fetch requests:
- Fetch network, including feed
- Fetch feed by name, since last sync
- Fetch person, including feed
- Fetch note, including context
- This is based on detail pages. Each request should check local db and fall back to network, all within an await.

View File

@ -12,8 +12,8 @@
import {hasParent} from 'src/util/html'
import {timedelta} from 'src/util/misc'
import {store as toast} from "src/state/toast"
import {modal, alerts, user} from "src/state/app"
import relay from 'src/relay'
import {modal, alerts} from "src/state/app"
import relay, {user} from 'src/relay'
import Anchor from 'src/partials/Anchor.svelte'
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
@ -68,8 +68,6 @@
})
onMount(() => {
relay.pool.sync($user)
return modal.subscribe($modal => {
// Keep scroll position on body, but don't allow scrolling
if ($modal) {

View File

@ -1,7 +1,8 @@
import Dexie from 'dexie'
import {writable} from 'svelte/store'
import {groupBy, prop, flatten, pick} from 'ramda'
import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
import {now} from 'src/util/misc'
import {now, getLocalJson, setLocalJson} from 'src/util/misc'
import {filterTags, findReply, findRoot} from 'src/util/nostr'
export const db = new Dexie('coracle/relay')
@ -9,12 +10,23 @@ export const db = new Dexie('coracle/relay')
db.version(4).stores({
relays: '++url, name',
events: '++id, pubkey, created_at, kind, content, reply, root',
people: '++pubkey, name, about',
tags: '++key, event, value',
})
window.db = db
// Some things work better as observables than database tables
db.user = writable(getLocalJson("db/user"))
db.people = writable(getLocalJson('db.people') || {})
db.network = writable(getLocalJson('db/network') || [])
db.connections = writable(getLocalJson("db/connections") || [])
db.user.subscribe($user => setLocalJson("coracle/user", $user))
db.people.subscribe($people => setLocalJson("coracle/people", $people))
db.network.subscribe($network => setLocalJson("coracle/network", $network))
db.connections.subscribe($connections => setLocalJson("coracle/connections", $connections))
// Hooks
db.events.process = async events => {
@ -58,18 +70,28 @@ db.events.process = async events => {
}
// Update our people
for (const event of profileUpdates) {
const {pubkey, kind, content, tags} = event
const person = await db.people.where('pubkey').equals(pubkey).first()
const putPerson = data => db.people.put({...person, ...data, pubkey, updated_at: now()})
db.people.update($people => {
for (const event of profileUpdates) {
const {pubkey, kind, content, tags} = event
const putPerson = data => {
$people[pubkey] = {
...$people[pubkey],
...data,
pubkey,
updated_at: now(),
}
}
await switcherFn(kind, {
0: () => putPerson(JSON.parse(content)),
3: () => putPerson({petnames: tags}),
12165: () => putPerson({muffle: tags}),
default: () => {
console.log(`Received unsupported event type ${event.kind}`)
},
})
}
switcherFn(kind, {
0: () => putPerson(JSON.parse(content)),
3: () => putPerson({petnames: tags}),
12165: () => putPerson({muffle: tags}),
default: () => {
console.log(`Received unsupported event type ${event.kind}`)
},
})
}
return $people
})
}

View File

@ -1,9 +1,8 @@
import {liveQuery} from 'dexie'
import {pluck, uniq, objOf, isNil} from 'ramda'
import {pluck, without, uniqBy, prop, groupBy, concat, uniq, objOf, isNil, identity} from 'ramda'
import {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak'
import {now, timedelta, createScroller} from 'src/util/misc'
import {escapeHtml} from 'src/util/html'
import {filterTags} from 'src/util/nostr'
import {filterTags, findRoot, findReply} from 'src/util/nostr'
import {db} from 'src/relay/db'
import pool from 'src/relay/pool'
@ -16,13 +15,12 @@ const lq = f => liveQuery(async () => {
}
})
// Context getters attempt to retrieve from the db and fall back to the network
const ensurePerson = async ({pubkey}) => {
const person = await db.people.where('pubkey').equals(pubkey).first()
// Throttle updates for people
if (!person || person.updated_at < now() - timedelta(1, 'hours')) {
await pool.syncPersonInfo({pubkey, ...person})
}
await pool.syncPersonInfo({pubkey, ...person})
}
const ensureContext = async events => {
@ -48,6 +46,8 @@ const ensureContext = async events => {
await Promise.all(promises)
}
// Utils for qurying dexie
const prefilterEvents = filter => {
if (filter.ids) {
return db.events.where('id').anyOf(ensurePlural(filter.ids))
@ -64,6 +64,8 @@ const prefilterEvents = filter => {
return db.events
}
// Utils for filtering db
const filterEvents = filter => {
return prefilterEvents(filter)
.filter(e => {
@ -78,44 +80,34 @@ const filterEvents = filter => {
})
}
const getOrLoadChunk = async (filter, since, until) => {
const getChunk = () => {
return filterEvents({...filter, since}).reverse().sortBy('created_at')
}
const annotateChunk = async chunk => {
const ancestorIds = concat(chunk.map(findRoot), chunk.map(findReply)).filter(identity)
const ancestors = await filterEvents({kinds: [1], ids: ancestorIds}).toArray()
const chunk = getChunk()
const allNotes = uniqBy(prop('id'), chunk.concat(ancestors))
const notesById = createMap('id', allNotes)
const notesByRoot = groupBy(
n => {
const rootId = findRoot(n)
const parentId = findReply(n)
// If we have a chunk, go ahead and use it. This will result in not showing all
// data, but it's the best UX I could come up with
if (chunk.length > 0) {
return chunk
}
// Actually dereference the notes in case we weren't able to retrieve them
if (notesById[rootId]) {
return rootId
}
// If we didn't have anything, try loading it
await ensureContext(await pool.loadEvents({...filter, since, until}))
if (notesById[parentId]) {
return parentId
}
// Now return what's in our database
return getChunk()
return n.id
},
allNotes
)
return await Promise.all(Object.keys(notesByRoot).map(findNote))
}
const scroller = (filter, delta, onChunk) => {
let since = now() - delta
let until = now()
const unsub = createScroller(async () => {
since -= delta
until -= delta
await onChunk(await getOrLoadChunk(filter, since, until))
// Set a hard cutoff at 3 weeks back
if (since < now() - timedelta(21, 'days')) {
unsub()
}
})
return unsub
}
const filterReplies = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
@ -203,8 +195,67 @@ const filterAlerts = async (person, filter) => {
return events
}
// Synchronization
const login = ({privkey, pubkey}) => {
db.user.set({relays: [], muffle: [], petnames: [], updated_at: 0, pubkey, privkey})
}
const addRelay = url => {
db.connections.update($connections => $connections.concat(url))
pool.syncNetwork()
pool.syncNetworkNotes()
}
const removeRelay = url => {
db.connections.update($connections => without([url], $connections))
}
const follow = async pubkey => {
db.network.update($network => $network.concat(pubkey))
pool.syncNetwork()
pool.syncNetworkNotes()
}
const unfollow = async pubkey => {
db.network.update($network => $network.concat(pubkey))
}
// Initialization
db.user.subscribe($user => {
if ($user?.privkey) {
pool.setPrivateKey($user.privkey)
} else if ($user?.pubkey) {
pool.setPublicKey($user.pubkey)
}
})
db.connections.subscribe($connections => {
const poolRelays = pool.getRelays()
for (const url of $connections) {
if (!poolRelays.includes(url)) {
pool.addRelay(url)
}
}
for (const url of poolRelays) {
if (!$connections.includes(url)) {
pool.removeRelay(url)
}
}
})
export const user = db.user
export const people = db.people
export const network = db.network
export const connections = db.connections
export default {
db, pool, lq, ensurePerson, ensureContext, filterEvents, filterReactions,
countReactions, findReaction, filterReplies, findNote, renderNote, filterAlerts,
scroller,
annotateChunk, login, addRelay, removeRelay, follow, unfollow,
}

View File

@ -1,9 +1,9 @@
import {uniqBy, without, prop} from 'ramda'
import {writable} from 'svelte/store'
import {uniqBy, prop, uniq} from 'ramda'
import {get} from 'svelte/store'
import {relayPool, getPublicKey} from 'nostr-tools'
import {noop, range} from 'hurdak/lib/hurdak'
import {now, randomChoice, timedelta, getLocalJson, setLocalJson} from "src/util/misc"
import {getTagValues} from "src/util/nostr"
import {now, timedelta, randomChoice} from "src/util/misc"
import {getTagValues, filterTags} from "src/util/nostr"
import {db} from 'src/relay/db'
// ============================================================================
@ -11,16 +11,6 @@ import {db} from 'src/relay/db'
const pool = relayPool()
const relays = writable([])
const setup = () => {
for (const url of getLocalJson('pool/relays') || []) {
addRelay(url)
}
relays.subscribe($relays => setLocalJson('pool/relays', $relays))
}
class Channel {
constructor(name) {
this.name = name
@ -80,20 +70,23 @@ class Channel {
export const channels = range(0, 10).map(i => new Channel(i.toString()))
const req = filter => randomChoice(channels).all(filter)
const req = (...args) => randomChoice(channels).all(...args)
const sub = (...args) => randomChoice(channels).sub(...args)
const getPubkey = () => {
return pool._pubkey || getPublicKey(pool._privkey)
}
const getRelays = () => {
return Object.keys(pool.relays)
}
const addRelay = url => {
pool.addRelay(url)
relays.update($r => $r.concat(url))
}
const removeRelay = url => {
pool.removeRelay(url)
relays.update($r => without([url], $r))
}
const setPrivateKey = privkey => {
@ -102,7 +95,6 @@ const setPrivateKey = privkey => {
}
const setPublicKey = pubkey => {
// TODO fix this, it ain't gonna work
pool.registerSigningFunction(async event => {
const {sig} = await window.nostr.signEvent(event)
@ -125,56 +117,71 @@ const loadEvents = async filter => {
return events
}
const syncPersonInfo = async person => {
const [events] = await Promise.all([
// Get profile info events
req({kinds: [0, 3, 12165], authors: [person.pubkey]}),
// Make sure we have something in the database
db.people.put({muffle: [], petnames: [], updated_at: 0, ...person}),
])
const subs = {}
// Process the events to flesh out the person
await db.events.process(events)
// Return our person for convenience
return await db.people.where('pubkey').equals(person.pubkey).first()
}
let syncSub = null
let syncChan = new Channel('sync')
const sync = async person => {
if (syncSub) {
(await syncSub).unsub()
const listenForEvents = async (key, filter) => {
if (subs[key]) {
subs[key].unsub()
}
if (!person) return
// Get person info right away
const {petnames, pubkey} = await syncPersonInfo(person)
// Don't grab nothing, but don't grab everything either
const since = Math.max(
now() - timedelta(3, 'days'),
Math.min(
now() - timedelta(3, 'hours'),
getLocalJson('pool/lastSync') || 0
)
)
setLocalJson('pool/lastSync', now())
// Populate recent activity in network so the person has something to look at right away
syncSub = syncChan.sub(
[{since, authors: getTagValues(petnames).concat(pubkey)},
{since, '#p': [pubkey]}],
db.events.process
)
subs[key] = await sub(filter, db.events.process)
}
setup()
const loadPeople = pubkeys => {
return pubkeys.length ? loadEvents({kinds: [0, 3, 12165], authors: pubkeys}) : []
}
const syncNetwork = async () => {
const $user = get(db.user)
let pubkeys = []
if ($user) {
// Get this user's profile to start with
await loadPeople([$user.pubkey])
// Get our refreshed person
const people = get(db.people)
// Merge the new info into our user
Object.assign($user, people[$user.pubkey])
console.log($user)
// Update our user store
db.user.update(() => $user)
// Get n degreees of separation using petnames
pubkeys = uniq(getTagValues($user.petnames))
}
// Fall back to some pubkeys we like so we can support new users
if (pubkeys.length === 0) {
pubkeys = [
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322", // hodlbod
]
}
let networkPubkeys = pubkeys
for (let depth = 0; depth < 1; depth++) {
const events = await loadPeople(pubkeys)
pubkeys = uniq(filterTags({type: "p"}, events.filter(e => e.kind === 3)))
networkPubkeys = networkPubkeys.concat(pubkeys)
}
db.network.set(networkPubkeys)
}
const syncNetworkNotes = () => {
const authors = get(db.network)
const since = now() - timedelta(30, 'days')
loadEvents({kinds: [1, 5, 7], authors, since, until: now()})
listenForEvents('networkNotes', {kinds: [1, 5, 7], authors, since: now()})
}
export default {
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
publishEvent, loadEvents, syncPersonInfo, sync, relays,
getPubkey, getRelays, addRelay, removeRelay, setPrivateKey, setPublicKey,
publishEvent, loadEvents, syncNetwork, syncNetworkNotes,
}

View File

@ -1,10 +1,11 @@
<script>
import {fly} from 'svelte/transition'
import toast from 'src/state/toast'
import {modal, registerRelay} from 'src/state/app'
import {modal} from 'src/state/app'
import {dispatch} from 'src/state/dispatch'
import Input from 'src/partials/Input.svelte'
import Button from 'src/partials/Button.svelte'
import relay from 'src/relay'
let url = ''
@ -16,8 +17,8 @@
return toast.show("error", 'That isn\'t a valid websocket url - relay urls should start with "wss://"')
}
registerRelay(url)
dispatch("relay/join", url)
relay.db.relays.put(url)
relay.addRelay(url)
modal.set(null)
}
</script>

View File

@ -3,8 +3,8 @@
import {now} from 'src/util/misc'
import {findReply} from 'src/util/nostr'
import {ellipsize} from 'hurdak/src/core'
import relay from 'src/relay'
import {alerts, modal, user} from 'src/state/app'
import relay, {user} from 'src/relay'
import {alerts, modal} from 'src/state/app'
import Badge from "src/partials/Badge.svelte"
import Note from 'src/views/Note.svelte'

View File

@ -5,8 +5,8 @@
import {copyToClipboard} from "src/util/html"
import Input from "src/partials/Input.svelte"
import Anchor from "src/partials/Anchor.svelte"
import {user} from "src/state/app"
import toast from "src/state/toast"
import {user} from "src/relay"
const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/'
const delegationUrl = 'https://github.com/nostr-protocol/nips/blob/b62aa418dee13aac1899ea7c6946a0f55dd7ee84/26.md'

View File

@ -7,8 +7,7 @@
import Anchor from "src/partials/Anchor.svelte"
import Input from "src/partials/Input.svelte"
import toast from "src/state/toast"
import {dispatch} from "src/state/dispatch"
import {user} from "src/state/app"
import relay from 'src/relay'
let privkey = ''
let hasExtension = false
@ -31,12 +30,10 @@
toast.show("info", "Your private key has been re-generated.")
}
const logIn = async ({privkey, pubkey}) => {
const person = await dispatch("user/init", pubkey)
const logIn = ({privkey, pubkey}) => {
relay.login({privkey, pubkey})
user.set({...person, pubkey, privkey})
navigate('/notes/global')
navigate('/relays')
}
const logInWithExtension = async () => {

View File

@ -5,8 +5,8 @@
import Textarea from "src/partials/Textarea.svelte"
import Button from "src/partials/Button.svelte"
import {dispatch} from "src/state/dispatch"
import {user} from "src/state/app"
import toast from "src/state/toast"
import {user} from "src/relay"
let values = {}

View File

@ -1,21 +1,18 @@
<script>
import {navigate} from 'svelte-routing'
import {timedelta} from 'src/util/misc'
import Anchor from "src/partials/Anchor.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Notes from "src/views/Notes.svelte"
import {user} from "src/state/app"
import {timedelta} from 'src/util/misc'
import relay from 'src/relay'
import relay, {user, connections} from 'src/relay'
export let activeTab
const relays = relay.pool.relays
const authors = $user ? $user.petnames.map(t => t[1]) : []
const setActiveTab = tab => navigate(`/notes/${tab}`)
</script>
{#if $relays.length === 0}
{#if $connections.length === 0}
<div class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
You aren't yet connected to any relays. Please click <Anchor href="/relays"

View File

@ -8,8 +8,9 @@
import Notes from "src/views/Notes.svelte"
import Likes from "src/views/Likes.svelte"
import {t, dispatch} from 'src/state/dispatch'
import {modal, user} from "src/state/app"
import {modal} from "src/state/app"
import relay from 'src/relay'
import {user} from "src/relay"
export let pubkey
export let activeTab

View File

@ -8,9 +8,9 @@
import Textarea from "src/partials/Textarea.svelte"
import Anchor from "src/partials/Anchor.svelte"
import Button from "src/partials/Button.svelte"
import {user} from "src/state/app"
import {dispatch} from "src/state/dispatch"
import toast from "src/state/toast"
import {user} from "src/relay"
let values = {picture: null, about: null, name: null}

View File

@ -3,18 +3,31 @@
import {fuzzy} from "src/util/misc"
import Input from "src/partials/Input.svelte"
import {modal} from "src/state/app"
import relay from 'src/relay'
import relay, {connections} from 'src/relay'
let q = ""
let search
const relays = relay.pool.relays
const defaultRelays = [
"wss://nostr.zebedee.cloud",
"wss://nostr-pub.wellorder.net",
"wss://relay.damus.io",
"wss://relay.grunch.dev",
"wss://nostr.sandwich.farm",
"wss://relay.nostr.ch",
"wss://nostr-relay.wlvs.space",
]
for (const url of defaultRelays) {
relay.db.relays.put({url})
}
const knownRelays = relay.lq(() => relay.db.relays.toArray())
$: search = fuzzy($knownRelays, {keys: ["name", "description", "url"]})
const join = url => relay.pool.addRelay(url)
const leave = url => relay.pool.removeRelay(url)
const join = url => relay.addRelay(url)
const leave = url => relay.removeRelay(url)
</script>
<div class="flex justify-center py-8 px-4" in:fly={{y: 20}}>
@ -31,7 +44,7 @@
</Input>
<div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6">
{#each ($knownRelays || []) as r}
{#if $relays.includes(r.url)}
{#if $connections.includes(r.url)}
<div class="flex gap-2 justify-between">
<div>
<strong>{r.name || r.url}</strong>

View File

@ -5,8 +5,9 @@
import Toggle from "src/partials/Toggle.svelte"
import Input from "src/partials/Input.svelte"
import Button from "src/partials/Button.svelte"
import {settings, user} from "src/state/app"
import {settings} from "src/state/app"
import toast from "src/state/toast"
import {user} from "src/relay"
let values = {...$settings}

View File

@ -1,59 +1,7 @@
import {writable, get} from 'svelte/store'
import {writable} from 'svelte/store'
import {navigate} from "svelte-routing"
import {globalHistory} from "svelte-routing/src/history"
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
import relay from 'src/relay'
// Keep track of our user
export const user = writable(getLocalJson("coracle/user"))
user.subscribe($user => {
setLocalJson("coracle/user", $user)
// Keep nostr in sync
if ($user?.privkey) {
relay.pool.setPrivateKey($user.privkey)
} else if ($user?.pubkey) {
relay.pool.setPublicKey($user.pubkey)
}
})
const userLq = relay.lq(() => {
const $user = get(user)
if ($user) {
return relay.db.people.where('pubkey').equals($user?.pubkey).first()
}
})
userLq.subscribe(person => {
user.update($user => $user ? ({...$user, ...person}) : null)
})
// Keep track of which relays we're subscribed to
export const relays = writable(getLocalJson("coracle/relays") || [])
let prevRelays = []
relays.subscribe($relays => {
prevRelays.forEach(url => {
if (!$relays.includes(url)) {
relay.pool.removeRelay(url)
}
})
$relays.forEach(url => {
if (!prevRelays.includes(url)) {
relay.pool.addRelay(url)
}
})
setLocalJson("coracle/relays", $relays)
relay.pool.sync(get(user))
})
// Modals
@ -98,40 +46,3 @@ export const alerts = writable({
alerts.subscribe($alerts => {
setLocalJson("coracle/alerts", $alerts)
})
// Relays
const defaultRelays = [
"wss://nostr.zebedee.cloud",
"wss://nostr-pub.wellorder.net",
"wss://relay.damus.io",
"wss://relay.grunch.dev",
"wss://nostr.sandwich.farm",
"wss://relay.nostr.ch",
"wss://nostr-relay.wlvs.space",
]
export const registerRelay = async url => {
const {dufflepudUrl} = get(settings)
let json
try {
const res = await fetch(dufflepudUrl + '/relay/info', {
method: 'POST',
body: JSON.stringify({url}),
headers: {
'Content-Type': 'application/json',
},
})
json = await res.json()
} catch (e) {
json = {}
}
relay.db.relays.put({...json, url})
}
for (const url of defaultRelays) {
registerRelay(url)
}

View File

@ -9,13 +9,6 @@ import relay from 'src/relay'
export const dispatch = defmulti("dispatch", identity)
dispatch.addMethod("user/init", (topic, pubkey) => {
// Hardcode one to get them started
relay.pool.addRelay("wss://nostr.zebedee.cloud")
return relay.pool.syncPersonInfo({pubkey})
})
dispatch.addMethod("user/update", async (topic, updates) => {
await relay.pool.publishEvent(makeEvent(0, JSON.stringify(updates)))
})

View File

@ -8,10 +8,11 @@
import Preview from 'src/partials/Preview.svelte'
import Anchor from 'src/partials/Anchor.svelte'
import {dispatch} from "src/state/dispatch"
import {settings, user, modal} from "src/state/app"
import {settings, modal} from "src/state/app"
import {formatTimestamp} from 'src/util/misc'
import Badge from "src/partials/Badge.svelte"
import Card from "src/partials/Card.svelte"
import {user} from 'src/relay'
export let note
export let depth = 0

View File

@ -2,7 +2,7 @@
import {onDestroy} from 'svelte'
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
import {createMap} from 'hurdak/lib/hurdak'
import {timedelta} from 'src/util/misc'
import {createScroller} from 'src/util/misc'
import {findReply, findRoot} from 'src/util/nostr'
import Spinner from 'src/partials/Spinner.svelte'
import Note from "src/views/Note.svelte"
@ -10,13 +10,16 @@
export let filter
export let showParent = false
export let shouldMuffle = false
export let delta = timedelta(10, 'minutes')
let notes
let limit = 0
onDestroy(createScroller(async () => {
limit += 20
onDestroy(relay.scroller(filter, delta, async chunk => {
notes = relay.lq(async () => {
const notes = await relay.filterEvents(filter).reverse().sortBy('created_at')
const chunk = notes.slice(0, limit)
const ancestorIds = concat(chunk.map(findRoot), chunk.map(findReply)).filter(identity)
const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray()

View File

@ -6,7 +6,8 @@
import SelectButton from "src/partials/SelectButton.svelte"
import {getMuffleValue} from "src/util/nostr"
import {dispatch, t} from 'src/state/dispatch'
import {modal, user} from "src/state/app"
import {modal} from "src/state/app"
import {user} from 'src/relay'
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']

View File

@ -1,8 +1,7 @@
<script>
import {fly} from 'svelte/transition'
import {fuzzy} from "src/util/misc"
import {user} from "src/state/app"
import relay from 'src/relay'
import relay, {user} from 'src/relay'
export let q