mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Some refactoring of data sync stuff
This commit is contained in:
parent
e4b67d914d
commit
1e0b09594b
@ -40,6 +40,10 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
|
||||
- [ ] Check firefox - in dev it won't work, but it should in production
|
||||
- [ ] Re-implement muffle
|
||||
- [ ] Rename users/accounts to people
|
||||
- [ ] Move relays to db
|
||||
- [ ] Make user a livequery instead of a store
|
||||
- [ ] Figure out if multiple relays congest response times because we wait for all eose
|
||||
- [ ] Set default relay when storage is empty
|
||||
- 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/
|
||||
|
@ -12,9 +12,7 @@
|
||||
import {hasParent} from 'src/util/html'
|
||||
import {timedelta} from 'src/util/misc'
|
||||
import {store as toast} from "src/state/toast"
|
||||
import {channels} from "src/state/nostr"
|
||||
import {modal, logout} from "src/state/app"
|
||||
import {user} from 'src/state/user'
|
||||
import {modal, alerts, user} from "src/state/app"
|
||||
import relay from 'src/relay'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import NoteDetail from "src/views/NoteDetail.svelte"
|
||||
@ -29,12 +27,14 @@
|
||||
import RelayList from "src/routes/RelayList.svelte"
|
||||
import AddRelay from "src/routes/AddRelay.svelte"
|
||||
import UserDetail from "src/routes/UserDetail.svelte"
|
||||
// import UserAdvanced from "src/routes/UserAdvanced.svelte"
|
||||
import UserAdvanced from "src/routes/UserAdvanced.svelte"
|
||||
import NoteCreate from "src/routes/NoteCreate.svelte"
|
||||
// import Chat from "src/routes/Chat.svelte"
|
||||
// import ChatRoom from "src/routes/ChatRoom.svelte"
|
||||
// import ChatEdit from "src/routes/ChatEdit.svelte"
|
||||
|
||||
export let url = ""
|
||||
|
||||
const menuIsOpen = writable(false)
|
||||
const toggleMenu = () => menuIsOpen.update(x => !x)
|
||||
|
||||
@ -44,9 +44,24 @@
|
||||
let menuIcon
|
||||
let scrollY
|
||||
let suspendedSubs = []
|
||||
let mostRecentAlert = 0
|
||||
let mostRecentAlert = relay.lq(async () => {
|
||||
const [e] = await relay
|
||||
.filterAlerts($user, {since: $alerts.since})
|
||||
.limit(1).reverse().sortBy('created_at')
|
||||
|
||||
export let url = ""
|
||||
return e?.created_at
|
||||
})
|
||||
|
||||
const logout = () => {
|
||||
// Give any animations a moment to finish
|
||||
setTimeout(() => {
|
||||
localStorage.clear()
|
||||
relay.db.delete()
|
||||
|
||||
// Do a hard refresh so everything gets totally cleared
|
||||
window.location = '/login'
|
||||
}, 200)
|
||||
}
|
||||
|
||||
// Close menu on click outside
|
||||
document.querySelector("html").addEventListener("click", e => {
|
||||
@ -56,7 +71,7 @@
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
relay.sync()
|
||||
relay.pool.sync($user)
|
||||
|
||||
return modal.subscribe($modal => {
|
||||
// Keep scroll position on body, but don't allow scrolling
|
||||
@ -119,7 +134,7 @@
|
||||
<div
|
||||
class="overflow-hidden w-6 h-6 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||
style="background-image: url({$user.picture})" />
|
||||
<span class="text-lg font-bold">{$user.name}</span>
|
||||
<span class="text-lg font-bold">{$user.name || $user.pubkey.slice(0, 8)}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="cursor-pointer relative">
|
||||
|
@ -3,7 +3,6 @@
|
||||
import {onMount} from 'svelte'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {channels} from 'src/state/nostr'
|
||||
import Input from "src/partials/Input.svelte"
|
||||
|
||||
export let className = ''
|
||||
|
@ -1,5 +1,7 @@
|
||||
import Dexie from 'dexie'
|
||||
import {filterTags} from 'src/util/nostr'
|
||||
import {groupBy, prop, flatten, pick} from 'ramda'
|
||||
import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
|
||||
import {filterTags, findReply, findRoot} from 'src/util/nostr'
|
||||
|
||||
export const db = new Dexie('coracle/relay')
|
||||
|
||||
@ -13,24 +15,58 @@ window.db = db
|
||||
|
||||
// Hooks
|
||||
|
||||
db.events.hook('creating', (id, e, t) => {
|
||||
setTimeout(() => {
|
||||
for (const tag of e.tags) {
|
||||
db.tags.put({
|
||||
id: [id, ...tag.slice(0, 2)].join(':'),
|
||||
event: id,
|
||||
db.events.process = async events => {
|
||||
// Only persist ones we care about, the rest can be
|
||||
// ephemeral and used to update users etc
|
||||
const eventsByKind = groupBy(prop('kind'), ensurePlural(events))
|
||||
const notesAndReactions = flatten(Object.values(pick([1, 7], eventsByKind)))
|
||||
const profileUpdates = flatten(Object.values(pick([0, 3, 12165], eventsByKind)))
|
||||
const deletions = eventsByKind[5] || []
|
||||
|
||||
// Persist notes and reactions
|
||||
if (notesAndReactions.length > 0) {
|
||||
const persistentEvents = notesAndReactions
|
||||
.map(e => ({...e, root: findRoot(e), reply: findReply(e)}))
|
||||
|
||||
db.events.bulkPut(persistentEvents)
|
||||
|
||||
db.tags.bulkPut(
|
||||
persistentEvents
|
||||
.flatMap(e =>
|
||||
e.tags.map(
|
||||
tag => ({
|
||||
id: [e.id, ...tag.slice(0, 2)].join(':'),
|
||||
event: e.id,
|
||||
type: tag[0],
|
||||
value: tag[1],
|
||||
relay: tag[2],
|
||||
mark: tag[3],
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (e.kind === 5) {
|
||||
const eventIds = filterTags({tag: "e"}, e)
|
||||
// Delete stuff that needs to be deleted
|
||||
if (deletions.length > 0) {
|
||||
const eventIds = deletions.flatMap(e => filterTags({tag: "e"}, e))
|
||||
|
||||
db.events.where('id').anyOf(eventIds).delete()
|
||||
db.tags.where('event').anyOf(eventIds).delete()
|
||||
}
|
||||
|
||||
// Update our users
|
||||
for (const event of profileUpdates) {
|
||||
const {pubkey, kind, content, tags} = event
|
||||
const user = await db.users.where('pubkey').equals(pubkey).first()
|
||||
|
||||
await switcherFn(kind, {
|
||||
0: () => db.users.put({...user, ...JSON.parse(content), pubkey}),
|
||||
3: () => db.users.put({...user, petnames: tags, pubkey}),
|
||||
12165: () => db.users.put({...user, muffle: tags, pubkey}),
|
||||
default: () => {
|
||||
console.log(`Received unsupported event type ${event.kind}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -17,22 +17,13 @@ const lq = f => liveQuery(async () => {
|
||||
})
|
||||
|
||||
const ensureContext = async e => {
|
||||
// We can't return a promise, so use setTimeout instead
|
||||
const user = await db.users.where('pubkey').equals(e.pubkey).first() || {
|
||||
muffle: [],
|
||||
petnames: [],
|
||||
updated_at: 0,
|
||||
pubkey: e.pubkey,
|
||||
}
|
||||
const user = await db.users.where('pubkey').equals(e.pubkey).first()
|
||||
|
||||
// Throttle updates for users
|
||||
if (user.updated_at < now() - timedelta(1, 'hours')) {
|
||||
Object.assign(user, await pool.getUserInfo({pubkey: e.pubkey, ...user}))
|
||||
if (!user || user.updated_at < now() - timedelta(1, 'hours')) {
|
||||
await pool.syncUserInfo({pubkey: e.pubkey, ...user})
|
||||
}
|
||||
|
||||
// Even if we didn't find a match, save it so we don't keep trying to refresh
|
||||
db.users.put({...user, updated_at: now()})
|
||||
|
||||
// TODO optimize this like user above so we're not double-fetching
|
||||
await pool.fetchContext(e)
|
||||
}
|
||||
@ -130,7 +121,15 @@ const renderNote = async (note, {showEntire = false}) => {
|
||||
})
|
||||
}
|
||||
|
||||
const filterAlerts = async (user, filter) => {
|
||||
const tags = db.tags.where('value').equals(user.pubkey)
|
||||
const ids = pluck('event', await tags.toArray())
|
||||
const events = await filterEvents({...filter, kinds: [1, 7], ids})
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
export default {
|
||||
db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions,
|
||||
findReaction, filterReplies, findNote, renderNote,
|
||||
findReaction, filterReplies, findNote, renderNote, filterAlerts,
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {relayPool, getPublicKey} from 'nostr-tools'
|
||||
import {noop, switcherFn, uuid} from 'hurdak/lib/hurdak'
|
||||
import {now, randomChoice, timedelta} from "src/util/misc"
|
||||
import {filterTags, findReply, findRoot} from "src/util/nostr"
|
||||
import {noop} from 'hurdak/lib/hurdak'
|
||||
import {now, randomChoice, timedelta, getLocalJson, setLocalJson} from "src/util/misc"
|
||||
import {filterTags, getTagValues} from "src/util/nostr"
|
||||
import {db} from 'src/relay/db'
|
||||
|
||||
// ============================================================================
|
||||
@ -81,8 +81,6 @@ export const channels = [
|
||||
|
||||
const req = filter => randomChoice(channels).all(filter)
|
||||
|
||||
const prepEvent = e => ({...e, root: findRoot(e), reply: findReply(e)})
|
||||
|
||||
const getPubkey = () => {
|
||||
return pool._pubkey || getPublicKey(pool._privkey)
|
||||
}
|
||||
@ -113,25 +111,30 @@ const setPublicKey = pubkey => {
|
||||
|
||||
const publishEvent = event => {
|
||||
pool.publish(event)
|
||||
db.events.put(prepEvent(event))
|
||||
db.events.process(event)
|
||||
}
|
||||
|
||||
const loadEvents = async filter => {
|
||||
const events = await req(filter)
|
||||
|
||||
db.events.bulkPut(events.map(prepEvent))
|
||||
db.events.process(events)
|
||||
}
|
||||
|
||||
const getUserInfo = async user => {
|
||||
for (const e of await req({kinds: [0, 3, 12165], authors: [user.pubkey]})) {
|
||||
switcherFn(e.kind, {
|
||||
0: () => Object.assign(user, JSON.parse(e.content)),
|
||||
3: () => Object.assign(user, {petnames: e.tags}),
|
||||
12165: () => Object.assign(user, {muffle: e.tags}),
|
||||
})
|
||||
}
|
||||
const syncUserInfo = async user => {
|
||||
const [events] = await Promise.all([
|
||||
// Get profile info events
|
||||
req({kinds: [0, 3, 12165], authors: [user.pubkey]}),
|
||||
// Make sure we have something in the database
|
||||
db.users.put({muffle: [], petnames: [], updated_at: 0, ...user}),
|
||||
])
|
||||
|
||||
return user
|
||||
// Process the events to flesh out the user
|
||||
await db.events.process(events)
|
||||
|
||||
// Return our user for convenience
|
||||
const person = await db.users.where('pubkey').equals(user.pubkey).first()
|
||||
|
||||
return person
|
||||
}
|
||||
|
||||
const fetchContext = async event => {
|
||||
@ -140,39 +143,38 @@ const fetchContext = async event => {
|
||||
{kinds: [5], 'ids': filterTags({tag: "e"}, event)},
|
||||
])
|
||||
|
||||
db.events.bulkPut(events.map(prepEvent))
|
||||
db.events.process(events)
|
||||
}
|
||||
|
||||
let syncSub = null
|
||||
|
||||
const sync = async user => {
|
||||
if (!user) throw new Error('No point sycing if we have no user')
|
||||
|
||||
const channel = randomChoice(channels)
|
||||
const since = Math.max(now() - interval(1, 'weeks'), getLocalJson('pool/lastSync') || 0)
|
||||
|
||||
channel.sub(
|
||||
[{since, authors: filterTags(user.petnames).concat(user.pubkey)},
|
||||
{since, '#p': [user.pubkey]}],
|
||||
onEvent: async e => {
|
||||
if ([1, 5, 7].includes(e.kind)) {
|
||||
return db.events.put(e)
|
||||
if (syncSub) {
|
||||
(await syncSub).unsub()
|
||||
}
|
||||
|
||||
const {pubkey, kind, content, tags} = e
|
||||
const user = await db.users.where('pubkey').equals(pubkey).first()
|
||||
if (!user) return
|
||||
|
||||
switcherFn(kind, {
|
||||
0: () => db.users.put({...user, pubkey, ...JSON.parse(content)}),
|
||||
3: () => db.users.put({...user, pubkey, petnames: e.tags}),
|
||||
12165: () => db.users.put({...user, pubkey, muffle: e.tags}),
|
||||
default: e => {
|
||||
console.log(`Received unsupported event type ${e.kind}`)
|
||||
},
|
||||
})
|
||||
}
|
||||
const {petnames, pubkey} = await syncUserInfo(user)
|
||||
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 user has something to look at right away
|
||||
syncSub = randomChoice(channels).sub(
|
||||
[{since, authors: getTagValues(petnames).concat(pubkey)},
|
||||
{since, '#p': [pubkey]}],
|
||||
db.events.process
|
||||
)
|
||||
}
|
||||
|
||||
export default {
|
||||
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
|
||||
publishEvent, loadEvents, getUserInfo, fetchContext, sync,
|
||||
publishEvent, loadEvents, syncUserInfo, fetchContext, sync,
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {registerRelay} from 'src/state/nostr'
|
||||
import toast from 'src/state/toast'
|
||||
import {modal} from 'src/state/app'
|
||||
import {modal, registerRelay} from 'src/state/app'
|
||||
import {dispatch} from 'src/state/dispatch'
|
||||
import Input from 'src/partials/Input.svelte'
|
||||
import Button from 'src/partials/Button.svelte'
|
||||
|
@ -4,17 +4,13 @@
|
||||
import {findReply} from 'src/util/nostr'
|
||||
import {ellipsize} from 'hurdak/src/core'
|
||||
import relay from 'src/relay'
|
||||
import {user} from 'src/state/user'
|
||||
import {alerts, modal} from 'src/state/app'
|
||||
import {alerts, modal, user} from 'src/state/app'
|
||||
import UserBadge from "src/partials/UserBadge.svelte"
|
||||
import Note from 'src/views/Note.svelte'
|
||||
|
||||
const events = relay.lq(async () => {
|
||||
const events = await relay
|
||||
.filterEvents({kinds: [1, 7], '#p': [$user.pubkey]})
|
||||
.limit(10)
|
||||
.reverse()
|
||||
.sortBy('created_at')
|
||||
const alerts = await relay.filterAlerts($user)
|
||||
const events = await alerts.limit(10).reverse().sortBy('created_at')
|
||||
|
||||
return events
|
||||
// Add parent in
|
||||
@ -24,7 +20,7 @@
|
||||
})
|
||||
|
||||
// Clear notification badge
|
||||
alerts.set({...$alerts, since: now()})
|
||||
alerts.set({since: now()})
|
||||
</script>
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {onMount} from "svelte"
|
||||
import {navigate} from "svelte-routing"
|
||||
import RoomList from "src/partials/chat/RoomList.svelte"
|
||||
import {user} from "src/state/user"
|
||||
import {user} from "src/state/app"
|
||||
|
||||
onMount(() => {
|
||||
if (!$user) {
|
||||
|
@ -7,7 +7,6 @@
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {channels} from "src/state/nostr"
|
||||
import toast from "src/state/toast"
|
||||
|
||||
export let room
|
||||
|
@ -6,10 +6,8 @@
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import {createScroller, renderNote} from 'src/util/notes'
|
||||
import UserBadge from 'src/partials/UserBadge.svelte'
|
||||
import {Listener, Cursor, epoch} from 'src/state/nostr'
|
||||
import {accounts, ensureAccounts} from 'src/state/app'
|
||||
import {user} from 'src/state/app'
|
||||
import {dispatch} from 'src/state/dispatch'
|
||||
import {user} from 'src/state/user'
|
||||
import RoomList from "src/partials/chat/RoomList.svelte"
|
||||
|
||||
export let room
|
||||
|
@ -5,7 +5,7 @@
|
||||
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/user"
|
||||
import {user} from "src/state/app"
|
||||
import toast from "src/state/toast"
|
||||
|
||||
const keypairUrl = 'https://www.cloudflare.com/learning/ssl/how-does-public-key-encryption-work/'
|
||||
|
@ -8,7 +8,7 @@
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import toast from "src/state/toast"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {relays} from "src/state/nostr"
|
||||
import {relays, user} from "src/state/app"
|
||||
|
||||
let privkey = ''
|
||||
let hasExtension = false
|
||||
@ -32,11 +32,15 @@
|
||||
}
|
||||
|
||||
const logIn = async ({privkey, pubkey}) => {
|
||||
const {found} = await dispatch("account/init", {privkey, pubkey})
|
||||
console.log(1)
|
||||
const person = await dispatch("account/init", pubkey)
|
||||
console.log(person)
|
||||
|
||||
user.set({...person, pubkey, privkey})
|
||||
|
||||
if ($relays.length === 0) {
|
||||
navigate('/relays')
|
||||
} else if (found) {
|
||||
} else if (user.name) {
|
||||
navigate('/notes/global')
|
||||
} else {
|
||||
navigate('/profile')
|
||||
|
@ -5,7 +5,7 @@
|
||||
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/user"
|
||||
import {user} from "src/state/app"
|
||||
import toast from "src/state/toast"
|
||||
|
||||
let values = {}
|
||||
|
@ -4,8 +4,7 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Notes from "src/views/Notes.svelte"
|
||||
import {relays} from "src/state/nostr"
|
||||
import {user} from "src/state/user"
|
||||
import {user, relays} from "src/state/app"
|
||||
|
||||
export let activeTab
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
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/user"
|
||||
import {user} from "src/state/app"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import toast from "src/state/toast"
|
||||
|
||||
@ -38,16 +38,12 @@
|
||||
const submit = async event => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!values.name.match(/^\w[\w-]+\w$/)) {
|
||||
toast.show("error", "Names must be comprised of letters, numbers, and dashes only.")
|
||||
} else {
|
||||
await dispatch("account/update", values)
|
||||
|
||||
navigate(`/users/${$user.pubkey}/profile`)
|
||||
|
||||
toast.show("info", "Your profile has been updated!")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form on:submit={submit} class="flex justify-center py-8 px-4" in:fly={{y: 20}}>
|
||||
|
@ -4,8 +4,7 @@
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {relays, knownRelays} from "src/state/nostr"
|
||||
import {modal} from "src/state/app"
|
||||
import {modal, relays, knownRelays} from "src/state/app"
|
||||
|
||||
let q = ""
|
||||
let search
|
||||
@ -15,7 +14,6 @@
|
||||
$: search = fuzzy(data, {keys: ["name", "description", "url"]})
|
||||
|
||||
const join = url => dispatch("relay/join", url)
|
||||
|
||||
const leave = url => dispatch("relay/leave", url)
|
||||
</script>
|
||||
|
||||
|
@ -5,8 +5,7 @@
|
||||
import Toggle from "src/partials/Toggle.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import {user} from "src/state/user"
|
||||
import {settings} from "src/state/app"
|
||||
import {settings, user} from "src/state/app"
|
||||
import toast from "src/state/toast"
|
||||
|
||||
let values = {...$settings}
|
||||
|
@ -4,16 +4,15 @@
|
||||
import {fly} from 'svelte/transition'
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import SelectButton from "src/partials/SelectButton.svelte"
|
||||
import {getMuffleValue} from "src/util/notes"
|
||||
import {user} from 'src/state/user'
|
||||
import {getMuffleValue} from "src/util/nostr"
|
||||
import {dispatch, t} from 'src/state/dispatch'
|
||||
import {modal} from "src/state/app"
|
||||
import {modal, user} from "src/state/app"
|
||||
|
||||
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']
|
||||
|
||||
const values = {
|
||||
// Scale up to integers for each choice we have
|
||||
muffle: switcher(Math.round(getMuffleValue($modal.user.pubkey) * 3), muffleOptions),
|
||||
muffle: switcher(Math.round(getMuffleValue($modal.user) * 3), muffleOptions),
|
||||
}
|
||||
|
||||
const save = async e => {
|
||||
|
@ -6,9 +6,8 @@
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import Notes from "src/views/Notes.svelte"
|
||||
import Likes from "src/views/Likes.svelte"
|
||||
import {user as currentUser} from 'src/state/user'
|
||||
import {t, dispatch} from 'src/state/dispatch'
|
||||
import {modal} from "src/state/app"
|
||||
import {modal, user as currentUser} from "src/state/app"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let pubkey
|
||||
|
109
src/state/app.js
109
src/state/app.js
@ -1,9 +1,101 @@
|
||||
import {writable} from 'svelte/store'
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {writable, get} 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 {user} from 'src/state/user'
|
||||
import {relays} from 'src/state/nostr'
|
||||
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.users.where('pubkey').equals($user?.pubkey).first()
|
||||
}
|
||||
})
|
||||
|
||||
userLq.subscribe(person => {
|
||||
user.update($user => $user ? ({...$user, ...person}) : null)
|
||||
})
|
||||
|
||||
// Keep track of known relays
|
||||
|
||||
export const knownRelays = writable((getLocalJson("coracle/knownRelays") || [
|
||||
{url: "wss://nostr-pub.wellorder.net"},
|
||||
{url: "wss://nostr.rocks"},
|
||||
{url: "wss://nostr-pub.semisol.dev"},
|
||||
{url: "wss://nostr.drss.io"},
|
||||
{url: "wss://relay.damus.io"},
|
||||
{url: "wss://nostr.openchain.fr"},
|
||||
{url: "wss://nostr.delo.software"},
|
||||
{url: "wss://relay.nostr.info"},
|
||||
{url: "wss://nostr.ono.re"},
|
||||
{url: "wss://relay.grunch.dev"},
|
||||
{url: "wss://nostr.sandwich.farm"},
|
||||
{url: "wss://relay.nostr.ch"},
|
||||
{url: "wss://nostr-relay.wlvs.space"},
|
||||
]).filter(x => x.url))
|
||||
|
||||
knownRelays.subscribe($knownRelays => {
|
||||
setLocalJson("coracle/knownRelays", $knownRelays)
|
||||
})
|
||||
|
||||
export const registerRelay = async url => {
|
||||
let json
|
||||
try {
|
||||
const res = await fetch(url.replace(/^ws/, 'http'), {
|
||||
headers: {
|
||||
Accept: 'application/nostr_json',
|
||||
},
|
||||
})
|
||||
|
||||
json = await res.json()
|
||||
} catch (e) {
|
||||
json = {}
|
||||
}
|
||||
|
||||
knownRelays.update($xs => uniqBy(prop('url'), $xs.concat({...json, url})))
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
export const modal = {
|
||||
subscribe: cb => {
|
||||
@ -26,6 +118,8 @@ export const modal = {
|
||||
},
|
||||
}
|
||||
|
||||
// Settings, alerts, etc
|
||||
|
||||
export const settings = writable({
|
||||
showLinkPreviews: true,
|
||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||
@ -44,12 +138,3 @@ export const alerts = writable({
|
||||
alerts.subscribe($alerts => {
|
||||
setLocalJson("coracle/alerts", $alerts)
|
||||
})
|
||||
|
||||
export const logout = () => {
|
||||
// Give any animations a moment to finish
|
||||
setTimeout(() => {
|
||||
user.set(null)
|
||||
relays.set([])
|
||||
navigate("/login")
|
||||
}, 200)
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import {identity, isNil, uniqBy, last, without} from 'ramda'
|
||||
import {get} from 'svelte/store'
|
||||
import {first, defmulti} from "hurdak/lib/hurdak"
|
||||
import {user} from "src/state/user"
|
||||
import relay from 'src/relay'
|
||||
import {relays} from 'src/state/nostr'
|
||||
import {relays} from 'src/state/app'
|
||||
|
||||
// Commands are processed in two layers:
|
||||
// - App-oriented commands are created via dispatch
|
||||
@ -12,48 +11,19 @@ import {relays} from 'src/state/nostr'
|
||||
|
||||
export const dispatch = defmulti("dispatch", identity)
|
||||
|
||||
dispatch.addMethod("account/init", async (topic, { privkey, pubkey }) => {
|
||||
// Set what we know about the user to our store
|
||||
user.set({
|
||||
name: pubkey.slice(0, 8),
|
||||
privkey,
|
||||
pubkey,
|
||||
petnames: [],
|
||||
muffle: [],
|
||||
})
|
||||
|
||||
// Make sure we have data for this user
|
||||
const {name} = await relay.pool.updateUser({pubkey})
|
||||
|
||||
// Tell the caller whether this user was found
|
||||
return {found: Boolean(name)}
|
||||
dispatch.addMethod("account/init", (topic, pubkey) => {
|
||||
return relay.pool.syncUserInfo({pubkey})
|
||||
})
|
||||
|
||||
dispatch.addMethod("account/update", async (topic, updates) => {
|
||||
// Update our local copy
|
||||
user.set({...get(user), ...updates})
|
||||
|
||||
// Tell the network
|
||||
await relay.pool.publishEvent(makeEvent(0, JSON.stringify(updates)))
|
||||
})
|
||||
|
||||
dispatch.addMethod("account/petnames", async (topic, petnames) => {
|
||||
const $user = get(user)
|
||||
|
||||
// Update our local copy
|
||||
user.set({...$user, petnames})
|
||||
|
||||
// Tell the network
|
||||
await relay.pool.publishEvent(makeEvent(3, '', petnames))
|
||||
})
|
||||
|
||||
dispatch.addMethod("account/muffle", async (topic, muffle) => {
|
||||
const $user = get(user)
|
||||
|
||||
// Update our local copy
|
||||
user.set({...$user, muffle})
|
||||
|
||||
// Tell the network
|
||||
await relay.pool.publishEvent(makeEvent(12165, '', muffle))
|
||||
})
|
||||
|
||||
|
@ -1,238 +0,0 @@
|
||||
import {writable, get} from 'svelte/store'
|
||||
import {assoc, uniqBy, prop} from 'ramda'
|
||||
import {noop, ensurePlural} from 'hurdak/lib/hurdak'
|
||||
import relay from 'src/relay'
|
||||
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
|
||||
|
||||
export class Channel {
|
||||
constructor(name) {
|
||||
this.name = name
|
||||
this.p = Promise.resolve()
|
||||
}
|
||||
async sub(filter, cb, onEose = noop) {
|
||||
// Make sure callers have to wait for the previous sub to be done
|
||||
// before they can get a new one.
|
||||
await this.p
|
||||
|
||||
// If we don't have any relays, we'll wait forever for an eose, but
|
||||
// we already know we're done. Use a timeout since callers are
|
||||
// expecting this to be async and we run into errors otherwise.
|
||||
if (get(relays).length === 0) {
|
||||
setTimeout(onEose)
|
||||
|
||||
return {unsub: noop}
|
||||
}
|
||||
|
||||
let resolve
|
||||
const eoseRelays = []
|
||||
const sub = relay.pool.sub({filter, cb}, this.name, r => {
|
||||
eoseRelays.push(r)
|
||||
|
||||
if (eoseRelays.length === get(relays).length) {
|
||||
onEose()
|
||||
}
|
||||
})
|
||||
|
||||
this.p = new Promise(r => {
|
||||
resolve = r
|
||||
})
|
||||
|
||||
return {
|
||||
unsub: () => {
|
||||
sub.unsub()
|
||||
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
all(filter) {
|
||||
/* eslint no-async-promise-executor: 0 */
|
||||
return new Promise(async resolve => {
|
||||
const result = []
|
||||
|
||||
const sub = await this.sub(
|
||||
filter,
|
||||
e => result.push(e),
|
||||
r => {
|
||||
sub.unsub()
|
||||
|
||||
resolve(uniqBy(prop('id'), result))
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const channels = {
|
||||
listener: new Channel('listener'),
|
||||
getter: new Channel('getter'),
|
||||
}
|
||||
|
||||
// We want to get old events, then listen for new events, then potentially retrieve
|
||||
// older events again for pagination. Since we have to limit channels to 3 per nip 01,
|
||||
// this requires us to unsubscribe and re-subscribe frequently
|
||||
export class Cursor {
|
||||
constructor(filter, delta) {
|
||||
this.filter = ensurePlural(filter)
|
||||
this.delta = delta || timedelta(15, 'minutes')
|
||||
this.since = now() - this.delta
|
||||
this.until = now()
|
||||
this.sub = null
|
||||
this.q = []
|
||||
this.p = Promise.resolve()
|
||||
this.seen = new Set()
|
||||
}
|
||||
async start() {
|
||||
if (!this.since) {
|
||||
throw new Error("Since must not be null")
|
||||
}
|
||||
|
||||
if (!this.sub) {
|
||||
this.sub = await channels.getter.sub(
|
||||
this.filter.map(f => ({...f, since: this.since, until: this.until})),
|
||||
e => this.onEvent(e),
|
||||
r => this.onEose(r)
|
||||
)
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
if (this.sub) {
|
||||
this.sub.unsub()
|
||||
this.sub = null
|
||||
}
|
||||
}
|
||||
async restart() {
|
||||
this.stop()
|
||||
|
||||
await this.start()
|
||||
}
|
||||
async step() {
|
||||
this.since -= this.delta
|
||||
|
||||
await this.restart()
|
||||
}
|
||||
onEvent(e) {
|
||||
// Save a little memory
|
||||
const shortId = e.id.slice(-10)
|
||||
|
||||
if (!this.seen.has(shortId)) {
|
||||
this.seen.add(shortId)
|
||||
this.q.push(e)
|
||||
}
|
||||
|
||||
this.until = Math.min(this.until, e.created_at - 1)
|
||||
}
|
||||
onEose() {
|
||||
this.stop()
|
||||
}
|
||||
async chunk() {
|
||||
await this.step()
|
||||
|
||||
/* eslint no-constant-condition: 0 */
|
||||
while (true) {
|
||||
await new Promise(requestAnimationFrame)
|
||||
|
||||
if (!this.sub) {
|
||||
return this.q.splice(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Listener {
|
||||
constructor(filter, onEvent) {
|
||||
this.filter = ensurePlural(filter).map(assoc('since', now()))
|
||||
this.onEvent = onEvent
|
||||
this.since = now()
|
||||
this.sub = null
|
||||
this.q = []
|
||||
this.p = Promise.resolve()
|
||||
}
|
||||
async start() {
|
||||
const {filter, since} = this
|
||||
|
||||
if (!this.sub) {
|
||||
this.sub = await channels.listener.sub(
|
||||
filter.map(f => ({since, ...f})),
|
||||
e => {
|
||||
// Not sure why since filter isn't working here, it's just slightly off
|
||||
if (e.created_at >= since) {
|
||||
this.since = e.created_at + 1
|
||||
this.onEvent(e)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
stop() {
|
||||
if (this.sub) {
|
||||
this.sub.unsub()
|
||||
this.sub = null
|
||||
}
|
||||
}
|
||||
restart() {
|
||||
this.stop()
|
||||
this.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of known relays
|
||||
|
||||
export const knownRelays = writable((getLocalJson("coracle/knownRelays") || [
|
||||
{url: "wss://nostr-pub.wellorder.net"},
|
||||
{url: "wss://nostr.rocks"},
|
||||
{url: "wss://nostr-pub.semisol.dev"},
|
||||
{url: "wss://nostr.drss.io"},
|
||||
{url: "wss://relay.damus.io"},
|
||||
{url: "wss://nostr.openchain.fr"},
|
||||
{url: "wss://nostr.delo.software"},
|
||||
{url: "wss://relay.nostr.info"},
|
||||
{url: "wss://nostr.ono.re"},
|
||||
{url: "wss://relay.grunch.dev"},
|
||||
{url: "wss://nostr.sandwich.farm"},
|
||||
{url: "wss://relay.nostr.ch"},
|
||||
{url: "wss://nostr-relay.wlvs.space"},
|
||||
]).filter(x => x.url))
|
||||
|
||||
knownRelays.subscribe($knownRelays => {
|
||||
setLocalJson("coracle/knownRelays", $knownRelays)
|
||||
})
|
||||
|
||||
export const registerRelay = async url => {
|
||||
let json
|
||||
try {
|
||||
const res = await fetch(url.replace(/^ws/, 'http'), {
|
||||
headers: {
|
||||
Accept: 'application/nostr_json',
|
||||
},
|
||||
})
|
||||
|
||||
json = await res.json()
|
||||
} catch (e) {
|
||||
json = {}
|
||||
}
|
||||
|
||||
knownRelays.update($xs => uniqBy(prop('url'), $xs.concat({...json, url})))
|
||||
}
|
||||
|
||||
// Create writable store for relays so we can observe changes in the app
|
||||
|
||||
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)
|
||||
})
|
@ -1,22 +0,0 @@
|
||||
import {writable} from "svelte/store"
|
||||
import {getLocalJson, setLocalJson} from "src/util/misc"
|
||||
import relay from 'src/relay'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Migrate data from old formats
|
||||
if ($user && (!$user.petnames || !$user.muffle)) {
|
||||
user.set({...$user, petnames: [], muffle: []})
|
||||
}
|
||||
})
|
||||
|
@ -1,8 +1,10 @@
|
||||
import {last, intersection} from 'ramda'
|
||||
import {last, find, intersection} from 'ramda'
|
||||
import {ensurePlural, first} from 'hurdak/lib/hurdak'
|
||||
|
||||
export const epoch = 1633046400
|
||||
|
||||
export const getTagValues = tags => tags.map(t => t[1])
|
||||
|
||||
export const filterTags = (where, events) =>
|
||||
ensurePlural(events)
|
||||
.flatMap(
|
||||
@ -45,14 +47,12 @@ export const filterMatches = (filter, e) => {
|
||||
))
|
||||
}
|
||||
|
||||
export const getMuffleValue = pubkey => {
|
||||
const $user = get(user)
|
||||
|
||||
if (!$user) {
|
||||
export const getMuffleValue = user => {
|
||||
if (!user) {
|
||||
return 1
|
||||
}
|
||||
|
||||
const tag = find(t => t[1] === pubkey, $user.muffle)
|
||||
const tag = find(t => t[1] === user.pubkey, user.muffle)
|
||||
|
||||
if (!tag) {
|
||||
return 1
|
||||
|
@ -1,190 +0,0 @@
|
||||
import {identity, uniq, propEq, uniqBy, prop, groupBy, pluck} from 'ramda'
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import {get} from 'svelte/store'
|
||||
import {getMuffleValue, epoch, filterMatches, findReply} from 'src/util/nostr'
|
||||
import {switcherFn, createMap} from 'hurdak/lib/hurdak'
|
||||
import {timedelta, sleep} from "src/util/misc"
|
||||
import {Listener, channels} from 'src/state/nostr'
|
||||
|
||||
export const annotateNotes = async (notes, {showParent = false} = {}) => {
|
||||
if (notes.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const noteIds = pluck('id', notes)
|
||||
const parentIds = notes.map(findReply).filter(identity)
|
||||
const filters = [{kinds: [1, 7], '#e': noteIds}]
|
||||
|
||||
if (showParent && parentIds.length > 0) {
|
||||
filters.push({kinds: [1], ids: parentIds})
|
||||
filters.push({kinds: [7], '#e': parentIds})
|
||||
}
|
||||
|
||||
const events = await channels.getter.all(filters)
|
||||
|
||||
await ensureAccounts(uniq(pluck('pubkey', notes.concat(events))))
|
||||
|
||||
const $accounts = get(accounts)
|
||||
const reactionsByParent = groupBy(findReply, events.filter(propEq('kind', 7)))
|
||||
const allNotes = uniqBy(prop('id'), notes.concat(events.filter(propEq('kind', 1))))
|
||||
const notesById = createMap('id', allNotes)
|
||||
|
||||
const annotate = note => ({
|
||||
...note,
|
||||
user: $accounts[note.pubkey],
|
||||
reactions: reactionsByParent[note.id] || [],
|
||||
children: uniqBy(prop('id'), allNotes.filter(n => findReply(n) === note.id)).map(annotate),
|
||||
})
|
||||
|
||||
return notes.map(note => {
|
||||
const parentId = findReply(note)
|
||||
|
||||
// If we have a parent, return that instead
|
||||
return annotate(
|
||||
showParent && notesById[parentId]
|
||||
? notesById[parentId]
|
||||
: note
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const annotateNewNote = async (note) => {
|
||||
await ensureAccounts([note.pubkey])
|
||||
|
||||
const $accounts = get(accounts)
|
||||
|
||||
return {
|
||||
...note,
|
||||
user: $accounts[note.pubkey],
|
||||
children: [],
|
||||
reactions: [],
|
||||
}
|
||||
}
|
||||
|
||||
export const notesListener = (notes, filter, {shouldMuffle = false, repliesOnly = false} = {}) => {
|
||||
const updateNote = (note, id, f) => {
|
||||
if (note.id === id) {
|
||||
return f(note)
|
||||
}
|
||||
|
||||
return {
|
||||
...note,
|
||||
parent: note.parent ? updateNote(note.parent, id, f) : null,
|
||||
children: note.children.map(n => updateNote(n, id, f)),
|
||||
}
|
||||
}
|
||||
|
||||
const updateNotes = (id, f) =>
|
||||
notes.update($notes => $notes.map(n => updateNote(n, id, f)))
|
||||
|
||||
const deleteNote = (note, ids, deleted_at) => {
|
||||
if (ids.includes(note.id)) {
|
||||
return {...note, deleted_at}
|
||||
}
|
||||
|
||||
return {
|
||||
...note,
|
||||
parent: note.parent ? deleteNote(note.parent, ids, deleted_at) : null,
|
||||
children: note.children.map(n => deleteNote(n, ids, deleted_at)),
|
||||
reactions: note.reactions.filter(e => !ids.includes(e.id)),
|
||||
}
|
||||
}
|
||||
|
||||
const deleteNotes = (ids, t) =>
|
||||
notes.update($notes => $notes.map(n => deleteNote(n, ids, t)))
|
||||
|
||||
return new Listener(filter, e => switcherFn(e.kind, {
|
||||
1: async () => {
|
||||
const id = findReply(e)
|
||||
const muffle = shouldMuffle && Math.random() > getMuffleValue(e.pubkey)
|
||||
|
||||
if (id) {
|
||||
const note = await annotateNewNote(e)
|
||||
|
||||
updateNotes(id, n => ({...n, children: n.children.concat(note)}))
|
||||
} else if (!repliesOnly && !muffle && filterMatches(filter, e)) {
|
||||
const [note] = await threadify([e])
|
||||
|
||||
notes.update($notes => [note].concat($notes))
|
||||
}
|
||||
},
|
||||
5: () => {
|
||||
deleteNotes(e.tags.map(t => t[1]), e.created_at)
|
||||
},
|
||||
7: () => {
|
||||
updateNotes(findReply(e), n => ({...n, reactions: n.reactions.concat(e)}))
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// UI
|
||||
|
||||
export const createScroller = (
|
||||
cursor,
|
||||
onChunk,
|
||||
{since = epoch, reverse = false} = {}
|
||||
) => {
|
||||
const startingDelta = cursor.delta
|
||||
|
||||
let active = false
|
||||
|
||||
const start = debounce(1000, async () => {
|
||||
if (active) {
|
||||
return
|
||||
}
|
||||
|
||||
active = true
|
||||
|
||||
/* eslint no-constant-condition: 0 */
|
||||
while (true) {
|
||||
// While we have empty space, fill it
|
||||
const {scrollY, innerHeight} = window
|
||||
const {scrollHeight} = document.body
|
||||
|
||||
if (
|
||||
(reverse && scrollY > innerHeight * 2)
|
||||
|| (!reverse && scrollY + innerHeight * 2 < scrollHeight)
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
// Stop if we've gone back far enough
|
||||
if (cursor.since <= since) {
|
||||
break
|
||||
}
|
||||
|
||||
// Get our chunk
|
||||
const chunk = await cursor.chunk()
|
||||
|
||||
// Notify the caller
|
||||
if (chunk.length > 0) {
|
||||
await onChunk(chunk)
|
||||
}
|
||||
|
||||
// If we have an empty chunk, increase our step size so we can get back to where
|
||||
// we might have old events. Once we get a chunk, knock it down to the default again
|
||||
if (chunk.length === 0) {
|
||||
cursor.delta = Math.min(timedelta(30, 'days'), cursor.delta * 2)
|
||||
} else {
|
||||
cursor.delta = startingDelta
|
||||
}
|
||||
|
||||
if (!active) {
|
||||
break
|
||||
}
|
||||
|
||||
// Wait a moment before proceeding to the next chunk for the caller
|
||||
// to load results into the dom
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
active = false
|
||||
})
|
||||
|
||||
return {
|
||||
start,
|
||||
stop: () => { active = false },
|
||||
isActive: () => Boolean(cursor.sub),
|
||||
}
|
||||
}
|
@ -9,8 +9,7 @@
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import relay from 'src/relay'
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {settings, modal} from "src/state/app"
|
||||
import {user} from "src/state/user"
|
||||
import {settings, user, modal} from "src/state/app"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import UserBadge from "src/partials/UserBadge.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
export let note
|
||||
|
||||
const observable = relay.lq(() => relay.findNote(note.id, {showEntire: true}))
|
||||
const observable = relay.lq(() => relay.findNote(note, {showEntire: true}))
|
||||
</script>
|
||||
|
||||
{#if $observable}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import {fly} from 'svelte/transition'
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {user} from "src/state/user"
|
||||
import {user} from "src/state/app"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let q
|
||||
|
Loading…
Reference in New Issue
Block a user