mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Working on loading
This commit is contained in:
parent
f2d1b0c951
commit
831ef12ab5
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -2,5 +2,5 @@
|
||||
import {onMount} from 'svelte'
|
||||
import {navigate} from 'svelte-routing'
|
||||
|
||||
onMount(() => navigate('/notes/global'))
|
||||
onMount(() => navigate('/notes/network'))
|
||||
</script>
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
settings.set(values)
|
||||
|
||||
navigate('/notes/global')
|
||||
navigate('/notes/network')
|
||||
|
||||
toast.show("info", "Your settings have been saved!")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user