Working on loading

This commit is contained in:
Jonathan Staab 2022-12-20 05:48:28 -08:00
parent f2d1b0c951
commit 831ef12ab5
16 changed files with 363 additions and 216 deletions

View File

@ -32,6 +32,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
- [ ] With link/image previews, remove the url from the note body if it's on a separate last line
- [ ] Stack views so scroll position isn't lost on navigation
- [ ] We're sending client=astral tags, event id 125ff9dc495f65d302e8d95ea6f9385106cc31b81c80e8c582b44be92fa50c44
- [ ] Add notification for slow relays
# Curreent update
@ -39,12 +40,14 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
- [ ] Delete old events
- [ ] Sync account updates to user for e.g. muffle settings
- [ ] Test nos2x
- [ ] Make sure login/out, no user usage works
- [ ] Add a re-sync/clear cache button
- 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
- [x] Sync user
- [x] 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 feed by name, since last sync

View File

@ -3,7 +3,7 @@
import "@fortawesome/fontawesome-free/css/solid.css"
import {onMount} from "svelte"
import {writable} from "svelte/store"
import {writable, get} from "svelte/store"
import {fly, fade} from "svelte/transition"
import {cubicInOut} from "svelte/easing"
import {throttle} from 'throttle-debounce'
@ -13,7 +13,7 @@
import {timedelta} from 'src/util/misc'
import {store as toast} from "src/state/toast"
import {modal, alerts} from "src/state/app"
import relay, {user} from 'src/relay'
import relay, {user, connections} from 'src/relay'
import Anchor from 'src/partials/Anchor.svelte'
import NoteDetail from "src/views/NoteDetail.svelte"
import PersonSettings from "src/views/PersonSettings.svelte"
@ -52,22 +52,34 @@
const logout = () => {
// Give any animations a moment to finish
setTimeout(() => {
const $connections = get(connections)
localStorage.clear()
relay.db.delete()
// Keep relays around
relay.db.events.clear()
relay.db.tags.clear()
// Remember the user's relay selection
connections.set($connections)
// Do a hard refresh so everything gets totally cleared
window.location = '/login'
}, 200)
}
// Close menu on click outside
document.querySelector("html").addEventListener("click", e => {
if (e.target !== menuIcon) {
menuIsOpen.set(false)
}
})
if ($user) {
relay.pool.syncNetwork()
}
onMount(() => {
// Close menu on click outside
document.querySelector("html").addEventListener("click", e => {
if (e.target !== menuIcon) {
menuIsOpen.set(false)
}
})
return modal.subscribe($modal => {
// Keep scroll position on body, but don't allow scrolling
if ($modal) {
@ -137,7 +149,7 @@
</a>
</li>
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/global">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes/network">
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>

View File

@ -18,20 +18,19 @@ 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.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))
db.user.subscribe($user => setLocalJson("db/user", $user))
db.people.subscribe($people => setLocalJson("db/people", $people))
db.network.subscribe($network => setLocalJson("db/network", $network))
db.connections.subscribe($connections => setLocalJson("db/connections", $connections))
// Hooks
db.events.process = async events => {
// Only persist ones we care about, the rest can be
// ephemeral and used to update people etc
// Only persist ones we care about, the rest can be ephemeral and used to update people 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)))
@ -40,7 +39,7 @@ db.events.process = async events => {
// Persist notes and reactions
if (notesAndReactions.length > 0) {
const persistentEvents = notesAndReactions
.map(e => ({...e, root: findRoot(e), reply: findReply(e)}))
.map(e => ({...e, root: findRoot(e), reply: findReply(e), added_at: now()}))
db.events.bulkPut(persistentEvents)

View File

@ -1,8 +1,9 @@
import {liveQuery} from 'dexie'
import {pluck, without, uniqBy, prop, groupBy, concat, uniq, objOf, isNil, identity} from 'ramda'
import {get} from 'svelte/store'
import {pluck, uniqBy, groupBy, concat, without, prop, uniq, objOf, isNil, identity} from 'ramda'
import {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak'
import {escapeHtml} from 'src/util/html'
import {filterTags, findRoot, findReply} from 'src/util/nostr'
import {filterTags, findReply, findRoot} from 'src/util/nostr'
import {db} from 'src/relay/db'
import pool from 'src/relay/pool'
@ -15,12 +16,25 @@ const lq = f => liveQuery(async () => {
}
})
// Filter builders
export const buildNoteContextFilter = async (note, extra = {}) => {
const replyId = findReply(note)
const filter = [
{...extra, kinds: [1, 5, 7], '#e': [note.id]},
{kinds: [0], authors: [note.pubkey]}]
if (replyId && !await db.events.get(replyId)) {
filter.push({...extra, kinds: [1], ids: [replyId]})
}
return filter
}
// 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()
await pool.syncPersonInfo({pubkey, ...person})
await pool.syncPersonInfo({...prop(pubkey, get(db.people)), pubkey})
}
const ensureContext = async events => {
@ -80,6 +94,71 @@ const filterEvents = filter => {
})
}
const filterReplies = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
const ids = pluck('event', await tags.toArray())
const replies = await filterEvents({...filter, kinds: [1], ids}).toArray()
return replies
}
const filterReactions = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
const ids = pluck('event', await tags.toArray())
const reactions = await filterEvents({...filter, kinds: [7], ids}).toArray()
return reactions
}
const findReaction = async (id, filter) =>
first(await filterReactions(id, filter))
const countReactions = async (id, filter) =>
(await filterReactions(id, filter)).length
const getOrLoadNote = async (id, {showEntire = false} = {}) => {
const note = await db.events.get(id)
if (!note) {
return first(await pool.loadEvents({kinds: [1], ids: [id]}))
}
return note
}
const findNote = async (id, {showEntire = false} = {}) => {
const note = await db.events.get(id)
if (!note) {
return
}
const reactions = await filterReactions(note.id)
const replies = await filterReplies(note.id)
const person = prop(note.pubkey, get(db.people))
const html = await renderNote(note, {showEntire})
let parent = null
const parentId = findReply(note)
if (parentId) {
parent = await db.events.get(parentId)
if (parent) {
parent = {
...parent,
reactions: await filterReactions(parent.id),
person: prop(parent.pubkey, get(db.people)),
html: await renderNote(parent, {showEntire}),
}
}
}
return {
...note, reactions, person, html, parent,
replies: await Promise.all(replies.map(r => findNote(r.id))),
}
}
const annotateChunk = async chunk => {
const ancestorIds = concat(chunk.map(findRoot), chunk.map(findReply)).filter(identity)
const ancestors = await filterEvents({kinds: [1], ids: ancestorIds}).toArray()
@ -108,66 +187,15 @@ const annotateChunk = async chunk => {
return await Promise.all(Object.keys(notesByRoot).map(findNote))
}
const filterReplies = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
const ids = pluck('event', await tags.toArray())
const replies = await filterEvents({...filter, kinds: [1], ids}).toArray()
return replies
}
const filterReactions = async (id, filter) => {
const tags = db.tags.where('value').equals(id).filter(t => t.mark === 'reply')
const ids = pluck('event', await tags.toArray())
const reactions = await filterEvents({...filter, kinds: [7], ids}).toArray()
return reactions
}
const findReaction = async (id, filter) =>
first(await filterReactions(id, filter))
const countReactions = async (id, filter) =>
(await filterReactions(id, filter)).length
const findNote = async (id, {giveUp = false, showEntire = false} = {}) => {
const [note, children] = await Promise.all([
db.events.get(id),
db.events.where('reply').equals(id),
])
// If we don't have it, try to retrieve it
if (!note) {
console.warn(`Failed to find context for note ${id}`)
if (giveUp) {
return null
}
await ensureContext(await pool.loadEvents({ids: [id]}))
return findNote(id, {giveUp: true})
}
const [replies, reactions, person, html] = await Promise.all([
children.clone().filter(e => e.kind === 1).toArray(),
children.clone().filter(e => e.kind === 7).toArray(),
db.people.get(note.pubkey),
renderNote(note, {showEntire}),
])
return {
...note, reactions, person, html,
replies: await Promise.all(replies.map(r => findNote(r.id))),
}
}
const renderNote = async (note, {showEntire = false}) => {
const $people = get(db.people)
const shouldEllipsize = note.content.length > 500 && !showEntire
const content = shouldEllipsize ? ellipsize(note.content, 500) : note.content
const people = await db.people.where('pubkey').anyOf(filterTags({tag: "p"}, note)).toArray()
const peopleByPubkey = createMap('pubkey', people)
const peopleByPubkey = createMap(
'pubkey',
filterTags({tag: "p"}, note).map(k => $people[k]).filter(identity)
)
return escapeHtml(content)
.replace(/\n/g, '<br />')
@ -199,13 +227,14 @@ const filterAlerts = async (person, filter) => {
const login = ({privkey, pubkey}) => {
db.user.set({relays: [], muffle: [], petnames: [], updated_at: 0, pubkey, privkey})
pool.syncNetwork()
}
const addRelay = url => {
db.connections.update($connections => $connections.concat(url))
pool.syncNetwork()
pool.syncNetworkNotes()
}
const removeRelay = url => {
@ -216,7 +245,6 @@ const follow = async pubkey => {
db.network.update($network => $network.concat(pubkey))
pool.syncNetwork()
pool.syncNetworkNotes()
}
const unfollow = async pubkey => {
@ -255,7 +283,8 @@ 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,
annotateChunk, login, addRelay, removeRelay, follow, unfollow,
db, pool, lq, buildNoteContextFilter, ensurePerson, ensureContext, filterEvents,
filterReactions, getOrLoadNote,
countReactions, findReaction, filterReplies, findNote, annotateChunk, renderNote,
filterAlerts, login, addRelay, removeRelay, follow, unfollow,
}

View File

@ -2,7 +2,7 @@ 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, timedelta, randomChoice} from "src/util/misc"
import {now, timedelta, randomChoice, getLocalJson, setLocalJson} from "src/util/misc"
import {getTagValues, filterTags} from "src/util/nostr"
import {db} from 'src/relay/db'
@ -16,7 +16,7 @@ class Channel {
this.name = name
this.p = Promise.resolve()
}
async sub(filter, onEvent, onEose = noop) {
async sub(filter, onEvent, onEose = noop, timeout = 30000) {
// 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.
@ -37,18 +37,23 @@ class Channel {
// before they can get a new one.
await p
// Start our subscription, wait for only one relays to eose before
// Start our subscription, wait for only one relay to eose before
// calling it done. We were waiting for all before, but that made
// the slowest relay a bottleneck
const sub = pool.sub({filter, cb: onEvent}, this.name, onEose)
return {
unsub: () => {
sub.unsub()
const done = () => {
sub.unsub()
resolve()
}
resolve()
}
// If the relay takes to long, just give up
if (timeout) {
setTimeout(done, 1000)
}
return {unsub: done}
}
all(filter) {
/* eslint no-async-promise-executor: 0 */
@ -119,12 +124,18 @@ const loadEvents = async filter => {
const subs = {}
const listenForEvents = async (key, filter) => {
const listenForEvents = async (key, filter, onEvent) => {
if (subs[key]) {
subs[key].unsub()
}
subs[key] = await sub(filter, db.events.process)
subs[key] = await sub(filter, e => {
db.events.process(e)
if (onEvent) {
onEvent(e)
}
})
}
const loadPeople = pubkeys => {
@ -145,8 +156,6 @@ const syncNetwork = async () => {
// Merge the new info into our user
Object.assign($user, people[$user.pubkey])
console.log($user)
// Update our user store
db.user.update(() => $user)
@ -161,27 +170,29 @@ const syncNetwork = async () => {
]
}
let networkPubkeys = pubkeys
let authors = 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)
authors = authors.concat(pubkeys)
}
db.network.set(networkPubkeys)
}
// Save this for next time
db.network.set(authors)
const syncNetworkNotes = () => {
const authors = get(db.network)
const since = now() - timedelta(30, 'days')
// Grab everything since our most recent sync
const since = getLocalJson('syncNetwork/lastSync') || now() - timedelta(30, 'days')
loadEvents({kinds: [1, 5, 7], authors, since, until: now()})
listenForEvents('networkNotes', {kinds: [1, 5, 7], authors, since: now()})
listenForEvents('pool/networkNotes', {kinds: [1, 5, 7], authors, since: now()})
// Save our position to speed up next page load
setLocalJson('syncNetwork/lastSync', now() - timedelta(5, 'minutes'))
}
export default {
getPubkey, getRelays, addRelay, removeRelay, setPrivateKey, setPublicKey,
publishEvent, loadEvents, syncNetwork, syncNetworkNotes,
publishEvent, loadEvents, listenForEvents, syncNetwork,
}

View File

@ -2,7 +2,6 @@
import {fly} from 'svelte/transition'
import toast from 'src/state/toast'
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'
@ -17,7 +16,7 @@
return toast.show("error", 'That isn\'t a valid websocket url - relay urls should start with "wss://"')
}
relay.db.relays.put(url)
relay.db.relays.put({url})
relay.addRelay(url)
modal.set(null)
}

View File

@ -2,5 +2,5 @@
import {onMount} from 'svelte'
import {navigate} from 'svelte-routing'
onMount(() => navigate('/notes/global'))
onMount(() => navigate('/notes/network'))
</script>

View File

@ -1,15 +1,72 @@
<script>
import {onMount, onDestroy} from 'svelte'
import {navigate} from 'svelte-routing'
import {findReply} from 'src/util/nostr'
import Anchor from "src/partials/Anchor.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Notes from "src/views/Notes.svelte"
import {timedelta} from 'src/util/misc'
import relay, {user, connections} from 'src/relay'
import {now, timedelta} from 'src/util/misc'
import relay, {network, connections} from 'src/relay'
export let activeTab
const authors = $user ? $user.petnames.map(t => t[1]) : []
let sub
let delta = timedelta(1, 'minutes')
let since = now() - delta
onMount(async () => {
sub = await subscribe(now())
})
onDestroy(() => {
if (sub) {
sub.unsub()
}
})
const setActiveTab = tab => navigate(`/notes/${tab}`)
const subscribe = until =>
relay.pool.listenForEvents(
'routes/Notes',
[{kinds: [1, 5, 7], since, until}],
async e => {
if (e.kind === 1) {
const filter = await relay.buildNoteContextFilter(e, {since})
await relay.pool.loadEvents(filter)
}
if (e.kind === 7) {
const replyId = findReply(e)
if (replyId && !await relay.db.events.get(replyId)) {
await relay.pool.loadEvents({kinds: [1], ids: [replyId]})
}
}
}
)
const loadNetworkNotes = async limit => {
const filter = {kinds: [1], authors: $network}
const notes = await relay.filterEvents(filter).reverse().sortBy('created_at')
return relay.annotateChunk(notes.slice(0, limit))
}
const loadGlobalNotes = async limit => {
const filter = {kinds: [1], since}
const notes = await relay.filterEvents(filter).reverse().sortBy('created_at')
if (notes.length < limit) {
since -= delta
sub = await subscribe(since + delta)
}
return relay.annotateChunk(notes.slice(0, limit))
}
</script>
{#if $connections.length === 0}
@ -21,17 +78,11 @@
</div>
</div>
{:else}
<Tabs tabs={['global', 'follows']} {activeTab} {setActiveTab} />
{#if activeTab === 'follows' && authors.length === 0}
<div class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
You haven't yet followed anyone. Visit a person's profile to follow them.
</div>
</div>
{:else if activeTab === 'follows'}
<Notes filter={{kinds: [1], authors}} shouldMuffle />
<Tabs tabs={['network', 'global']} {activeTab} {setActiveTab} />
{#if activeTab === 'network'}
<Notes shouldMuffle loadNotes={loadNetworkNotes} />
{:else}
<Notes delta={timedelta(5, 'minutes')} filter={{kinds: [1]}} shouldMuffle />
<Notes shouldMuffle loadNotes={loadGlobalNotes} />
{/if}
<div class="fixed bottom-0 right-0 p-8">
<a

View File

@ -1,31 +1,81 @@
<script>
import {find} from 'ramda'
import {onMount, onDestroy} from 'svelte'
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {timedelta} from 'src/util/misc'
import {getLastSync} from 'src/util/misc'
import {getTagValues, findReply} from 'src/util/nostr'
import Tabs from "src/partials/Tabs.svelte"
import Button from "src/partials/Button.svelte"
import Notes from "src/views/Notes.svelte"
import Likes from "src/views/Likes.svelte"
import {t, dispatch} from 'src/state/dispatch'
import {modal} from "src/state/app"
import relay from 'src/relay'
import {user} from "src/relay"
import relay, {user, people} from 'src/relay'
export let pubkey
export let activeTab
relay.ensurePerson({pubkey})
const person = relay.lq(() => relay.db.people.get(pubkey))
let sub = null
let following = $user && find(t => t[1] === pubkey, $user.petnames)
let since = getLastSync(['Person', pubkey])
onMount(async () => {
sub = await relay.pool.listenForEvents(
'routes/Person',
[{kind: [0, 1, 5, 7], authors: [pubkey], since}],
async e => {
if (e.kind === 1) {
const filter = await relay.buildNoteContextFilter(e, {since})
await relay.pool.loadEvents(filter)
}
if (e.kind === 7) {
const replyId = findReply(e)
if (replyId && !await relay.db.events.get(replyId)) {
await relay.pool.loadEvents({kinds: [1], ids: [replyId]})
}
}
}
)
})
onDestroy(() => {
if (sub) {
sub.unsub()
}
})
const getPerson = () => $people[pubkey]
const loadNotes = async limit => {
const filter = {kinds: [1], authors: [pubkey]}
const notes = await relay.filterEvents(filter).reverse().sortBy('created_at')
return relay.annotateChunk(notes.slice(0, limit))
}
const loadLikes = async limit => {
const filter = {kinds: [7], authors: [pubkey]}
const notes = await relay.filterEvents(filter).reverse().sortBy('created_at')
return relay.annotateChunk(notes.slice(0, limit))
}
const loadNetwork = async limit => {
const filter = {kinds: [1], authors: getTagValues(getPerson().petnames)}
const notes = await relay.filterEvents(filter).reverse().sortBy('created_at')
return relay.annotateChunk(notes.slice(0, limit))
}
const setActiveTab = tab => navigate(`/people/${pubkey}/${tab}`)
const follow = () => {
const petnames = $user.petnames
.concat([t("p", pubkey, $person?.name)])
.concat([t("p", pubkey, getPerson()?.name)])
dispatch('user/petnames', petnames)
@ -42,7 +92,7 @@
}
const openAdvanced = () => {
modal.set({form: 'person/settings', person: $person || {pubkey}})
modal.set({form: 'person/settings', person: getPerson() || {pubkey}})
}
</script>
@ -51,15 +101,15 @@
<div class="flex gap-4">
<div
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({$person?.picture})" />
style="background-image: url({getPerson()?.picture})" />
<div class="flex-grow">
<div class="flex items-center gap-2">
<h1 class="text-2xl">{$person?.name || pubkey.slice(0, 8)}</h1>
<h1 class="text-2xl">{getPerson()?.name || pubkey.slice(0, 8)}</h1>
{#if $user && $user.pubkey !== pubkey}
<i class="fa-solid fa-sliders cursor-pointer" on:click={openAdvanced} />
{/if}
</div>
<p>{$person?.about || ''}</p>
<p>{getPerson()?.about || ''}</p>
</div>
<div class="whitespace-nowrap">
{#if $user?.pubkey === pubkey}
@ -82,12 +132,12 @@
<Tabs tabs={['notes', 'likes', 'network']} {activeTab} {setActiveTab} />
{#if activeTab === 'notes'}
<Notes showParent delta={timedelta(1, 'days')} filter={{kinds: [1], authors: [pubkey]}} />
<Notes showParent loadNotes={loadNotes} />
{:else if activeTab === 'likes'}
<Likes author={pubkey} />
<Notes loadNotes={loadLikes} />
{:else if activeTab === 'network'}
{#if $person}
<Notes shouldMuffle filter={{kinds: [1], authors: $person.petnames.map(t => t[1])}} />
{#if getPerson()}
<Notes shouldMuffle loadNotes={loadNetwork} />
{:else}
<div class="py-16 max-w-xl m-auto flex justify-center">
Unable to show network for this person.

View File

@ -22,7 +22,7 @@
settings.set(values)
navigate('/notes/global')
navigate('/notes/network')
toast.show("info", "Your settings have been saved!")
}

View File

@ -85,12 +85,15 @@ export const copyTags = (e, newTags = []) => {
// Remove reply type from e tags
return uniqBy(
t => t.join(':'),
e.tags.map(t => last(t) === 'reply' ? t.slice(0, -1) : t).concat(newTags)
e.tags
.filter(t => ["p", "e"].includes(t[0]))
.map(t => last(t) === 'reply' ? t.slice(0, -1) : t)
.concat(newTags)
)
}
export const t = (type, content, marker) => {
const tag = [type, content, first(Object.keys(relay.pool.relays))]
const tag = [type, content, first(Object.keys(relay.pool.getRelays()))]
if (!isNil(marker)) {
tag.push(marker)

View File

@ -64,7 +64,10 @@ export const createScroller = loadMore => {
await loadMore()
}
await sleep(1000)
// This is a gross hack, basically, keep loading if the user doesn't scroll again,
// but wait a long time because otherwise we'll send off multiple concurrent requests
// that will clog up our channels and stall the app.
await sleep(30000)
if (!done) {
requestAnimationFrame(check)
@ -79,3 +82,12 @@ export const createScroller = loadMore => {
}
export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)]
export const getLastSync = (keyParts, fallback) => {
const key = `${keyParts.join('.')}/lastSync`
const lastSync = getLocalJson(key) || fallback
setLocalJson(key, now())
return lastSync
}

View File

@ -1,35 +0,0 @@
<script>
import {onDestroy} from 'svelte'
import {fly} from 'svelte/transition'
import {uniqBy, identity, prop} from 'ramda'
import {timedelta} from 'src/util/misc'
import Note from "src/views/Note.svelte"
import {findReply} from 'src/util/nostr'
import relay from 'src/relay'
export let author
const filter = {kinds: [7], authors: [author]}
const delta = timedelta(1, 'days')
let notes
onDestroy(relay.scroller(filter, delta, async chunk => {
notes = relay.lq(async () => {
const notes = await Promise.all(chunk.map(r => relay.findNote(findReply(r))))
return uniqBy(prop('id'), notes.filter(identity))
})
}))
</script>
{#if $notes}
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
{#each $notes as n (n.id)}
<li><Note note={n} depth={1} /></li>
{:else}
<li class="p-20 text-center" in:fly={{y: 20}}>No notes found.</li>
{/each}
</ul>
{/if}

View File

@ -28,7 +28,7 @@
let likes, flags, like, flag
$: {
likes = note.reactions.filter(whereEq({content: '+'}))
likes = note.reactions.filter(n => ['', '+'].includes(n.content))
flags = note.reactions.filter(whereEq({content: '-'}))
like = find(whereEq({pubkey: $user?.pubkey}), likes)
flag = find(whereEq({pubkey: $user?.pubkey}), flags)

View File

@ -1,14 +1,58 @@
<script>
import {onMount, onDestroy} from 'svelte'
import relay from 'src/relay'
import {getLastSync} from 'src/util/misc'
import Note from 'src/views/Note.svelte'
import Spinner from 'src/partials/Spinner.svelte'
export let note
const observable = relay.lq(() => relay.findNote(note.id, {showEntire: true}))
let observable, sub
let since = getLastSync(['NoteDetail', note.id])
onMount(async () => {
note = await relay.getOrLoadNote(note.id)
if (note) {
sub = await relay.pool.listenForEvents(
'routes/NoteDetail',
await relay.buildNoteContextFilter(note, {since})
)
}
})
onDestroy(() => {
if (sub) {
sub.unsub()
}
})
onMount(() => {
observable = relay.lq(async () => {
const details = await relay.findNote(note.id, {showEntire: true})
// Log this for debugging purposes
console.log('NoteDetail', details)
return details
})
return () => {
if (sub) {
sub.unsub()
}
}
})
</script>
{#if $observable}
{#if !note}
<div class="text-white">
Sorry, we weren't able to find this note.
</div>
{:else if $observable}
<div n:fly={{y: 20}}>
<Note showParent invertColors anchorId={note.id} note={$observable} depth={2} />
</div>
{:else}
<Spinner />
{/if}

View File

@ -1,14 +1,11 @@
<script>
import {onDestroy} from 'svelte'
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
import {createMap} from 'hurdak/lib/hurdak'
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"
import relay from 'src/relay'
export let filter
export let loadNotes
export let showParent = false
let notes
@ -17,35 +14,7 @@
onDestroy(createScroller(async () => {
limit += 20
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()
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)
// Actually dereference the notes in case we weren't able to retrieve them
if (notesById[rootId]) {
return rootId
}
if (notesById[parentId]) {
return parentId
}
return n.id
},
allNotes
)
return await Promise.all(Object.keys(notesByRoot).map(relay.findNote))
})
notes = relay.lq(() => loadNotes(limit))
}))
</script>