diff --git a/README.md b/README.md index 6a3532f4..c696f5e2 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/App.svelte b/src/App.svelte index e669412c..d9f9b3f9 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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) { diff --git a/src/relay/db.js b/src/relay/db.js index 5cddc796..83b350ef 100644 --- a/src/relay/db.js +++ b/src/relay/db.js @@ -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 + }) } diff --git a/src/relay/index.js b/src/relay/index.js index d11a3436..8cff96f8 100644 --- a/src/relay/index.js +++ b/src/relay/index.js @@ -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, } diff --git a/src/relay/pool.js b/src/relay/pool.js index 7210122f..7650ddb0 100644 --- a/src/relay/pool.js +++ b/src/relay/pool.js @@ -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, } diff --git a/src/routes/AddRelay.svelte b/src/routes/AddRelay.svelte index dadbc003..350b6fec 100644 --- a/src/routes/AddRelay.svelte +++ b/src/routes/AddRelay.svelte @@ -1,10 +1,11 @@ diff --git a/src/routes/Alerts.svelte b/src/routes/Alerts.svelte index 26bc6a0e..4b89e677 100644 --- a/src/routes/Alerts.svelte +++ b/src/routes/Alerts.svelte @@ -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' diff --git a/src/routes/Keys.svelte b/src/routes/Keys.svelte index c19fe536..d1dd6d2f 100644 --- a/src/routes/Keys.svelte +++ b/src/routes/Keys.svelte @@ -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' diff --git a/src/routes/Login.svelte b/src/routes/Login.svelte index c34193f6..d48124cd 100644 --- a/src/routes/Login.svelte +++ b/src/routes/Login.svelte @@ -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 () => { diff --git a/src/routes/NoteCreate.svelte b/src/routes/NoteCreate.svelte index 5e2b3414..a753aabd 100644 --- a/src/routes/NoteCreate.svelte +++ b/src/routes/NoteCreate.svelte @@ -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 = {} diff --git a/src/routes/Notes.svelte b/src/routes/Notes.svelte index 0e8d16dc..5bfd1fee 100644 --- a/src/routes/Notes.svelte +++ b/src/routes/Notes.svelte @@ -1,21 +1,18 @@ -{#if $relays.length === 0} +{#if $connections.length === 0}
You aren't yet connected to any relays. Please click 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)
@@ -31,7 +44,7 @@
{#each ($knownRelays || []) as r} - {#if $relays.includes(r.url)} + {#if $connections.includes(r.url)}
{r.name || r.url} diff --git a/src/routes/Settings.svelte b/src/routes/Settings.svelte index e10305d3..89afd110 100644 --- a/src/routes/Settings.svelte +++ b/src/routes/Settings.svelte @@ -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} diff --git a/src/state/app.js b/src/state/app.js index 5c67ef04..7103b07e 100644 --- a/src/state/app.js +++ b/src/state/app.js @@ -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) -} diff --git a/src/state/dispatch.js b/src/state/dispatch.js index f08329f5..72b6dc09 100644 --- a/src/state/dispatch.js +++ b/src/state/dispatch.js @@ -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))) }) diff --git a/src/views/Note.svelte b/src/views/Note.svelte index eb849a73..98011e55 100644 --- a/src/views/Note.svelte +++ b/src/views/Note.svelte @@ -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 diff --git a/src/views/Notes.svelte b/src/views/Notes.svelte index 1f681960..27ebf7be 100644 --- a/src/views/Notes.svelte +++ b/src/views/Notes.svelte @@ -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() diff --git a/src/views/PersonSettings.svelte b/src/views/PersonSettings.svelte index f5f99475..cedbb0a4 100644 --- a/src/views/PersonSettings.svelte +++ b/src/views/PersonSettings.svelte @@ -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'] diff --git a/src/views/SearchPeople.svelte b/src/views/SearchPeople.svelte index b094822c..1236cde4 100644 --- a/src/views/SearchPeople.svelte +++ b/src/views/SearchPeople.svelte @@ -1,8 +1,7 @@