- import {onMount} from 'svelte'
- import {propEq} from 'ramda'
- import {createMap} from 'hurdak/lib/hurdak'
+ import {onMount, onDestroy} from 'svelte'
+ import {fly} from 'svelte/transition'
+ import {writable} from 'svelte/store'
+ import {sortBy, uniqBy, prop} from 'ramda'
import {now} from 'src/util/misc'
- import {alerts, annotateNotes} from 'src/state/app'
+ import {ellipsize} from 'hurdak/src/core'
+ import {user} from 'src/state/user'
+ import {Cursor, epoch} from 'src/state/nostr'
+ import {alerts, annotateAlerts, notesListener, createScroller, modal} from 'src/state/app'
+ import Spinner from "src/partials/Spinner.svelte"
+ import UserBadge from "src/partials/UserBadge.svelte"
import Note from 'src/partials/Note.svelte'
- let notesById
+ let cursor
+ let listener
+ let scroller
+ let interval
+ let modalUnsub
+ let loading = true
+ let events = writable([])
- onMount(() => {
+ onMount(async () => {
// Clear notification badge
alerts.set({...$alerts, since: now()})
- annotateNotes($alerts.items.filter(propEq('kind', 1))).then($notes => {
- notesById = createMap('id', $notes)
+ cursor = new Cursor({kinds: [1, 7], '#p': [$user.pubkey]})
+ listener = await notesListener(events, [{kinds: [1, 5, 7]}], {repliesOnly: true})
+ scroller = createScroller(cursor, async chunk => {
+ // Add chunk context
+ chunk = await annotateAlerts(chunk)
+
+ // Sort and deduplicate
+ events.set(sortBy(n => -n.created_at, uniqBy(prop('id'), $events.concat(chunk))))
})
+
+ // Track loading based on cursor cutoff date
+ interval = setInterval(() => {
+ loading = cursor.since > epoch
+ }, 1000)
+
+ modalUnsub = modal.subscribe(async $modal => {
+ if ($modal) {
+ cursor.stop()
+ listener.stop()
+ scroller.stop()
+ } else {
+ cursor.start()
+ listener.start()
+ scroller.start()
+ }
+ })
+ })
+
+ onDestroy(() => {
+ cursor?.stop()
+ listener?.stop()
+ scroller?.stop()
+ modalUnsub?.()
+ clearInterval(interval)
})
-{#each $alerts.items as e (e.id)}
-{#if e.kind === 1 && notesById}
-
+
+
+
+
+{#if loading}
+
+{:else if $events.length === 0}
+
No recent activity found.
-{/each}
-
+{/if}
diff --git a/src/routes/UserDetail.svelte b/src/routes/UserDetail.svelte
index 592f259a..8cf62931 100644
--- a/src/routes/UserDetail.svelte
+++ b/src/routes/UserDetail.svelte
@@ -25,7 +25,7 @@
const follow = () => {
const petnames = $currentUser.petnames
- .concat([t("p", user.pubkey, user.name)])
+ .concat([t("p", pubkey, user?.name)])
dispatch('account/petnames', petnames)
@@ -34,7 +34,7 @@
const unfollow = () => {
const petnames = $currentUser.petnames
- .filter(([_, pubkey]) => pubkey !== user.pubkey)
+ .filter(([_, pubkey]) => pubkey !== pubkey)
dispatch('account/petnames', petnames)
@@ -42,7 +42,7 @@
}
const openAdvanced = () => {
- modal.set({form: 'user/advanced', user})
+ modal.set({form: 'user/advanced', user: user || {pubkey}})
}
diff --git a/src/state/app.js b/src/state/app.js
index bac3c74a..ee0f55bc 100644
--- a/src/state/app.js
+++ b/src/state/app.js
@@ -189,7 +189,6 @@ export const threadify = async notes => {
user: $accounts[note.pubkey],
reactions: reactionsByParent[note.id] || [],
children: uniqBy(prop('id'), _notes.filter(n => findReply(n) === note.id)).map(annotate),
- numberOfAncestors: note.tags.filter(([x]) => x === 'e').length,
}
}
@@ -241,6 +240,42 @@ export const annotateNotes = async (notes, {showParent = false} = {}) => {
})
}
+export const annotateAlerts = async events => {
+ if (events.length === 0) {
+ return []
+ }
+
+ const eventIds = pluck('id', events)
+ const parentIds = events.map(findReply).filter(identity)
+ const filters = [
+ {kinds: [1], ids: parentIds},
+ {kinds: [7], '#e': parentIds},
+ {kinds: [1, 7], '#e': eventIds},
+ ]
+
+ const relatedEvents = await channels.getter.all(filters)
+
+ await ensureAccounts(uniq(pluck('pubkey', events.concat(relatedEvents))))
+
+ const $accounts = get(accounts)
+ const reactionsByParent = groupBy(findReply, relatedEvents.filter(e => e.kind === 7 && e.content === '+'))
+ const allNotes = uniqBy(prop('id'), events.concat(relatedEvents).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 uniqBy(e => e.parent?.id || e.id, events.map(event => {
+ const parentId = findReply(event)
+
+ return {...annotate(event), parent: annotate(notesById[parentId])}
+ }))
+}
+
export const annotateNewNote = async (note) => {
await ensureAccounts([note.pubkey])
@@ -249,13 +284,12 @@ export const annotateNewNote = async (note) => {
return {
...note,
user: $accounts[note.pubkey],
- numberOfAncestors: note.tags.filter(([x]) => x === 'e').length,
children: [],
reactions: [],
}
}
-export const notesListener = (notes, filter, {shouldMuffle = false} = {}) => {
+export const notesListener = (notes, filter, {shouldMuffle = false, repliesOnly = false} = {}) => {
const updateNote = (note, id, f) => {
if (note.id === id) {
return f(note)
@@ -296,7 +330,7 @@ export const notesListener = (notes, filter, {shouldMuffle = false} = {}) => {
const note = await annotateNewNote(e)
updateNotes(id, n => ({...n, children: n.children.concat(note)}))
- } else if (!muffle && filterMatches(filter, e)) {
+ } else if (!repliesOnly && !muffle && filterMatches(filter, e)) {
const [note] = await threadify([e])
notes.update($notes => [note].concat($notes))