mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Working on scrolling stuff
This commit is contained in:
parent
eaf2e45e46
commit
447c112d21
@ -22,6 +22,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
|
||||
- [ ] Optimistically load events the user publishes (e.g. to reduce reflow for reactions/replies).
|
||||
- Essentially, we can pretend to be our own in-memory relay.
|
||||
- This allows us to keep a copy of all user data, and possibly user likes/reply parents
|
||||
- [ ] Support invoices https://twitter.com/jb55/status/1604131336247476224
|
||||
|
||||
# Bugs
|
||||
|
||||
@ -44,6 +45,7 @@ Coracle is currently in _alpha_ - expect bugs, slow loading times, and rough edg
|
||||
- [ ] 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
|
||||
- [ ] Are connections closed when a relay is removed?
|
||||
- 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/
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Dexie from 'dexie'
|
||||
import {groupBy, prop, flatten, pick} from 'ramda'
|
||||
import {ensurePlural, switcherFn} from 'hurdak/lib/hurdak'
|
||||
import {now} from 'src/util/misc'
|
||||
import {filterTags, findReply, findRoot} from 'src/util/nostr'
|
||||
|
||||
export const db = new Dexie('coracle/relay')
|
||||
@ -59,11 +60,12 @@ db.events.process = async events => {
|
||||
for (const event of profileUpdates) {
|
||||
const {pubkey, kind, content, tags} = event
|
||||
const user = await db.users.where('pubkey').equals(pubkey).first()
|
||||
const putUser = data => db.users.put({...user, ...data, pubkey, updated_at: now()})
|
||||
|
||||
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}),
|
||||
0: () => putUser(JSON.parse(content)),
|
||||
3: () => putUser({petnames: tags}),
|
||||
12165: () => putUser({muffle: tags}),
|
||||
default: () => {
|
||||
console.log(`Received unsupported event type ${event.kind}`)
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {liveQuery} from 'dexie'
|
||||
import {pluck, isNil} from 'ramda'
|
||||
import {pluck, uniq, objOf, isNil} from 'ramda'
|
||||
import {ensurePlural, createMap, ellipsize, first} from 'hurdak/lib/hurdak'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
import {escapeHtml} from 'src/util/html'
|
||||
@ -16,16 +16,26 @@ const lq = f => liveQuery(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
const ensureContext = async e => {
|
||||
const user = await db.users.where('pubkey').equals(e.pubkey).first()
|
||||
const ensurePerson = async ({pubkey}) => {
|
||||
const user = await db.users.where('pubkey').equals(pubkey).first()
|
||||
|
||||
// Throttle updates for users
|
||||
if (!user || user.updated_at < now() - timedelta(1, 'hours')) {
|
||||
await pool.syncUserInfo({pubkey: e.pubkey, ...user})
|
||||
await pool.syncUserInfo({pubkey, ...user})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO optimize this like user above so we're not double-fetching
|
||||
await pool.fetchContext(e)
|
||||
const ensureContext = async events => {
|
||||
const ids = events.flatMap(e => filterTags({tag: "e"}, e).concat(e.id))
|
||||
const people = uniq(pluck('pubkey', events)).map(objOf('pubkey'))
|
||||
|
||||
await Promise.all([
|
||||
people.map(ensurePerson),
|
||||
pool.fetchEvents([
|
||||
{kinds: [1, 5, 7], '#e': ids},
|
||||
{kinds: [1, 5], ids},
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
const prefilterEvents = filter => {
|
||||
@ -50,6 +60,8 @@ const filterEvents = filter => {
|
||||
if (filter.ids && !filter.ids.includes(e.id)) return false
|
||||
if (filter.authors && !filter.authors.includes(e.pubkey)) return false
|
||||
if (filter.kinds && !filter.kinds.includes(e.kind)) return false
|
||||
if (filter.since && filter.since > e.created_at) return false
|
||||
if (filter.until && filter.until < e.created_at) return false
|
||||
if (!isNil(filter.content) && filter.content !== e.content) return false
|
||||
|
||||
return true
|
||||
@ -78,12 +90,27 @@ const findReaction = async (id, filter) =>
|
||||
const countReactions = async (id, filter) =>
|
||||
(await filterReactions(id, filter)).length
|
||||
|
||||
const findNote = async id => {
|
||||
const findNote = async (id, giveUp = 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.warning(`Failed to find context for note ${id}`)
|
||||
|
||||
if (giveUp) {
|
||||
return null
|
||||
}
|
||||
|
||||
await ensureContext([
|
||||
await pool.fetchEvents({ids: [id]}),
|
||||
])
|
||||
|
||||
return findNote(id, true)
|
||||
}
|
||||
|
||||
const [replies, reactions, user, html] = await Promise.all([
|
||||
children.clone().filter(e => e.kind === 1).toArray(),
|
||||
children.clone().filter(e => e.kind === 7).toArray(),
|
||||
@ -130,6 +157,6 @@ const filterAlerts = async (user, filter) => {
|
||||
}
|
||||
|
||||
export default {
|
||||
db, pool, lq, ensureContext, filterEvents, filterReactions, countReactions,
|
||||
findReaction, filterReplies, findNote, renderNote, filterAlerts,
|
||||
db, pool, lq, ensurePerson, ensureContext, filterEvents, filterReactions,
|
||||
countReactions, findReaction, filterReplies, findNote, renderNote, filterAlerts,
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import {relayPool, getPublicKey} from 'nostr-tools'
|
||||
import {noop} from 'hurdak/lib/hurdak'
|
||||
import {noop, range} from 'hurdak/lib/hurdak'
|
||||
import {now, randomChoice, timedelta, getLocalJson, setLocalJson} from "src/util/misc"
|
||||
import {filterTags, getTagValues} from "src/util/nostr"
|
||||
import {getTagValues} from "src/util/nostr"
|
||||
import {db} from 'src/relay/db'
|
||||
|
||||
// ============================================================================
|
||||
@ -73,11 +73,7 @@ class Channel {
|
||||
}
|
||||
}
|
||||
|
||||
export const channels = [
|
||||
new Channel('a'),
|
||||
new Channel('b'),
|
||||
new Channel('c'),
|
||||
]
|
||||
export const channels = range(0, 10).map(i => new Channel(i.toString()))
|
||||
|
||||
const req = filter => randomChoice(channels).all(filter)
|
||||
|
||||
@ -117,7 +113,9 @@ const publishEvent = event => {
|
||||
const loadEvents = async filter => {
|
||||
const events = await req(filter)
|
||||
|
||||
db.events.process(events)
|
||||
await db.events.process(events)
|
||||
|
||||
return events
|
||||
}
|
||||
|
||||
const syncUserInfo = async user => {
|
||||
@ -137,16 +135,12 @@ const syncUserInfo = async user => {
|
||||
return person
|
||||
}
|
||||
|
||||
const fetchContext = async event => {
|
||||
const events = await req([
|
||||
{kinds: [5, 7], '#e': [event.id]},
|
||||
{kinds: [5], 'ids': filterTags({tag: "e"}, event)},
|
||||
])
|
||||
|
||||
db.events.process(events)
|
||||
const fetchEvents = async filter => {
|
||||
db.events.process(await req(filter))
|
||||
}
|
||||
|
||||
let syncSub = null
|
||||
let syncChan = new Channel('sync')
|
||||
|
||||
const sync = async user => {
|
||||
if (syncSub) {
|
||||
@ -155,7 +149,10 @@ const sync = async user => {
|
||||
|
||||
if (!user) return
|
||||
|
||||
// Get user info right away
|
||||
const {petnames, pubkey} = await syncUserInfo(user)
|
||||
|
||||
// Don't grab nothing, but don't grab everything either
|
||||
const since = Math.max(
|
||||
now() - timedelta(3, 'days'),
|
||||
Math.min(
|
||||
@ -167,7 +164,7 @@ const sync = async user => {
|
||||
setLocalJson('pool/lastSync', now())
|
||||
|
||||
// Populate recent activity in network so the user has something to look at right away
|
||||
syncSub = randomChoice(channels).sub(
|
||||
syncSub = syncChan.sub(
|
||||
[{since, authors: getTagValues(petnames).concat(pubkey)},
|
||||
{since, '#p': [pubkey]}],
|
||||
db.events.process
|
||||
@ -176,5 +173,5 @@ const sync = async user => {
|
||||
|
||||
export default {
|
||||
getPubkey, addRelay, removeRelay, setPrivateKey, setPublicKey,
|
||||
publishEvent, loadEvents, syncUserInfo, fetchContext, sync,
|
||||
publishEvent, loadEvents, syncUserInfo, fetchEvents, sync,
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
import {find} from 'ramda'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {timedelta} from 'src/util/misc'
|
||||
import Tabs from "src/partials/Tabs.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import Notes from "src/views/Notes.svelte"
|
||||
@ -13,6 +14,8 @@
|
||||
export let pubkey
|
||||
export let activeTab
|
||||
|
||||
relay.ensurePerson({pubkey})
|
||||
|
||||
const user = relay.lq(() => relay.db.users.get(pubkey))
|
||||
|
||||
let following = $currentUser && find(t => t[1] === pubkey, $currentUser.petnames)
|
||||
@ -78,7 +81,7 @@
|
||||
|
||||
<Tabs tabs={['notes', 'likes', 'network']} {activeTab} {setActiveTab} />
|
||||
{#if activeTab === 'notes'}
|
||||
<Notes filter={{kinds: [1], authors: [pubkey]}} />
|
||||
<Notes showParent delta={timedelta(1, 'days')} filter={{kinds: [1], authors: [pubkey]}} />
|
||||
{:else if activeTab === 'likes'}
|
||||
<Likes author={pubkey} />
|
||||
{:else if activeTab === 'network'}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {pluck} from "ramda"
|
||||
import {debounce} from 'throttle-debounce'
|
||||
import Fuse from "fuse.js/dist/fuse.min.js"
|
||||
|
||||
export const fuzzy = (data, opts = {}) => {
|
||||
@ -52,27 +51,31 @@ export const formatTimestamp = ts => {
|
||||
export const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
export const createScroller = loadMore => {
|
||||
const onScroll = debounce(1000, async () => {
|
||||
/* eslint no-constant-condition: 0 */
|
||||
/* eslint no-constant-condition: 0 */
|
||||
|
||||
while (true) {
|
||||
// While we have empty space, fill it
|
||||
const {scrollY, innerHeight} = window
|
||||
const {scrollHeight} = document.body
|
||||
let done = false
|
||||
|
||||
if (scrollY + innerHeight + 600 < scrollHeight) {
|
||||
break
|
||||
}
|
||||
const check = async () => {
|
||||
// While we have empty space, fill it
|
||||
const {scrollY, innerHeight} = window
|
||||
const {scrollHeight} = document.body
|
||||
|
||||
loadMore()
|
||||
|
||||
await sleep(1000)
|
||||
if (scrollY + innerHeight + 600 > scrollHeight) {
|
||||
await loadMore()
|
||||
}
|
||||
})
|
||||
|
||||
onScroll()
|
||||
await sleep(300)
|
||||
|
||||
return onScroll
|
||||
if (!done) {
|
||||
requestAnimationFrame(check)
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(check)
|
||||
|
||||
return () => {
|
||||
done = true
|
||||
}
|
||||
}
|
||||
|
||||
export const randomChoice = xs => xs[Math.floor(Math.random() * xs.length)]
|
||||
|
@ -7,7 +7,6 @@
|
||||
import {findReply} from "src/util/nostr"
|
||||
import Preview from 'src/partials/Preview.svelte'
|
||||
import Anchor from 'src/partials/Anchor.svelte'
|
||||
import relay from 'src/relay'
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {settings, user, modal} from "src/state/app"
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
@ -34,8 +33,6 @@
|
||||
flag = find(whereEq({pubkey: $user?.pubkey}), flags)
|
||||
}
|
||||
|
||||
relay.ensureContext(note)
|
||||
|
||||
const onClick = e => {
|
||||
if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) {
|
||||
modal.set({note})
|
||||
@ -112,14 +109,16 @@
|
||||
<Anchor on:click={() => deleteReaction(flag)}>Unflag</Anchor>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-ellipsis overflow-hidden">
|
||||
{@html note.html}
|
||||
<div class="text-ellipsis overflow-hidden flex flex-col gap-2">
|
||||
<p>{@html note.html}</p>
|
||||
{#if link}
|
||||
<div class="mt-2" on:click={e => e.stopPropagation()}>
|
||||
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
|
||||
<div>
|
||||
<div class="inline-block" on:click={e => e.stopPropagation()}>
|
||||
<Preview endpoint={`${$settings.dufflepudUrl}/link/preview`} url={link} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-6 text-light">
|
||||
<div>
|
||||
<i
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
export let note
|
||||
|
||||
const observable = relay.lq(() => relay.findNote(note, {showEntire: true}))
|
||||
const observable = relay.lq(() => relay.findNote(note.id, {showEntire: true}))
|
||||
</script>
|
||||
|
||||
{#if $observable}
|
||||
|
@ -1,35 +1,35 @@
|
||||
<script>
|
||||
import {onDestroy} from 'svelte'
|
||||
import {prop, identity, concat, uniqBy, groupBy} from 'ramda'
|
||||
import {createMap} from 'hurdak/lib/hurdak'
|
||||
import {now, timedelta} from 'src/util/misc'
|
||||
import {findReply, findRoot} from 'src/util/nostr'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {createScroller} from 'src/util/misc'
|
||||
import Spinner from 'src/partials/Spinner.svelte'
|
||||
import Note from "src/views/Note.svelte"
|
||||
import relay from 'src/relay'
|
||||
|
||||
export let filter
|
||||
export let showParent = false
|
||||
export let shouldMuffle = false
|
||||
export let delta = timedelta(10, 'minutes')
|
||||
|
||||
let limit = 10, init = now(), offset = 0, notes
|
||||
let since = now() - delta, until = now(), notes
|
||||
|
||||
const onScroll = createScroller(async () => {
|
||||
limit += 10
|
||||
offset += 1
|
||||
const done = createScroller(async () => {
|
||||
since -= delta
|
||||
until -= delta
|
||||
|
||||
const delta = timedelta(1, 'minutes')
|
||||
const since = init - delta * offset
|
||||
const until = init - delta * (offset - 1)
|
||||
|
||||
await relay.pool.loadEvents({...filter, since, until})
|
||||
await relay.ensureContext(
|
||||
await relay.pool.loadEvents({...filter, since, until})
|
||||
)
|
||||
|
||||
createNotesObservable()
|
||||
})
|
||||
|
||||
const createNotesObservable = () => {
|
||||
notes = relay.lq(async () => {
|
||||
const notes = await relay.filterEvents(filter).limit(limit).reverse().sortBy('created_at')
|
||||
const notes = await relay.filterEvents({...filter, since}).reverse().sortBy('created_at')
|
||||
const ancestorIds = concat(notes.map(findRoot), notes.map(findReply)).filter(identity)
|
||||
const ancestors = await relay.filterEvents({kinds: [1], ids: ancestorIds}).toArray()
|
||||
|
||||
@ -58,23 +58,13 @@
|
||||
})
|
||||
}
|
||||
|
||||
createNotesObservable()
|
||||
onDestroy(done)
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={onScroll} />
|
||||
|
||||
<ul class="py-4 flex flex-col gap-2 max-w-xl m-auto">
|
||||
{#each ($notes || []) as n (n.id)}
|
||||
<li><Note interactive note={n} depth={2} /></li>
|
||||
<li><Note interactive note={n} depth={2} {showParent} /></li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
{#if $notes?.length === 0}
|
||||
<div in:fly={{y: 20}} class="flex w-full justify-center items-center py-16">
|
||||
<div class="text-center max-w-md">
|
||||
No notes found.
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Spinner />
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user