mirror of
https://github.com/styppo/hamstr.git
synced 2024-10-18 05:23:28 +00:00
* Add infinite scroll in feed
* Refactor Nostr fetch logic * Fix FetchQueue getting stuck
This commit is contained in:
parent
fbdceb6f04
commit
ecbb2f98e3
80
src/components/AsyncLoadButton.vue
Normal file
80
src/components/AsyncLoadButton.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div ref="button" class="async-load-button">
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
:label="noMore ? labelNoMore : label"
|
||||
@click="load"
|
||||
size="md"
|
||||
flat
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AsyncLoadButton',
|
||||
emits: ['loading', 'loaded'],
|
||||
props: {
|
||||
loadFn: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Load more'
|
||||
},
|
||||
labelNoMore: {
|
||||
type: String,
|
||||
default: 'No more items. Try again?'
|
||||
},
|
||||
autoload: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
noMore: false,
|
||||
observer: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
if (this.loading) return
|
||||
console.log('loading')
|
||||
|
||||
this.loading = true
|
||||
this.$emit('loading')
|
||||
|
||||
const result = await this.loadFn()
|
||||
this.noMore = !result || !result.length
|
||||
|
||||
this.loading = false
|
||||
this.$emit('loaded', result)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.autoload) {
|
||||
this.observer = new IntersectionObserver(this.load.bind(this), {
|
||||
root: null,
|
||||
threshold: 0.5,
|
||||
})
|
||||
this.observer.observe(this.$refs.button)
|
||||
}
|
||||
},
|
||||
unmounted() {
|
||||
if (this.observer) this.observer.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.async-load-button button {
|
||||
width: 100%;
|
||||
padding: 1rem 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<div class="load-more">
|
||||
<q-btn
|
||||
:loading="loading"
|
||||
:label="reachedEnd ? 'reached end' : label"
|
||||
:disable="reachedEnd"
|
||||
@click="$emit('click')"
|
||||
size="md"
|
||||
flat
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseButtonLoadMore',
|
||||
emits: ['click'],
|
||||
props: {
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
reachedEnd: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: 'load more'
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.load-more button {
|
||||
width: 100%;
|
||||
padding: 1rem 0;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
</style>
|
@ -128,7 +128,7 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const updateInterval = Date.now() / 1000 - this.note.createdAt >= 3600 // 1h
|
||||
const updateInterval = DateUtils.now() - this.note.createdAt >= 3600 // 1h
|
||||
? 3600 // 1h
|
||||
: 60 // 1m
|
||||
this.refreshTimer = setInterval(() => this.refreshCounter++, updateInterval * 1000)
|
||||
|
@ -5,21 +5,19 @@ export default class FetchQueue extends Observable {
|
||||
super()
|
||||
this.client = client
|
||||
this.subId = subId
|
||||
this.sub = null
|
||||
this.fnGetId = fnGetId
|
||||
this.fnCreateFilter = fnCreateFilter
|
||||
this.throttle = opts.throttle || 250
|
||||
this.batchSize = opts.batchSize || 50
|
||||
this.retryDelay = opts.retryDelay || 5000
|
||||
this.maxRetries = opts.maxRetries || 3
|
||||
this.batchSize = opts.batchSize || 250
|
||||
this.timeout = opts.timeout || 3000
|
||||
this.maxAttempts = opts.maxAttempts || 2
|
||||
|
||||
this.queue = {}
|
||||
this.failed = {}
|
||||
this.fetching = false
|
||||
this.fetchQueued = false
|
||||
this.retryInterval = null
|
||||
|
||||
// XXX
|
||||
setInterval(() => this.failed = {}, 10000)
|
||||
this.fetchTimeout = null
|
||||
}
|
||||
|
||||
add(id) {
|
||||
@ -37,16 +35,19 @@ export default class FetchQueue extends Observable {
|
||||
|
||||
fetch() {
|
||||
this.fetchQueued = false
|
||||
if (this.retryInterval) clearInterval(this.retryInterval)
|
||||
if (this.fetchTimeout) clearTimeout(this.fetchTimeout)
|
||||
|
||||
const ids = Object.keys(this.queue).slice(0, this.batchSize)
|
||||
if (!ids.length) return
|
||||
if (!ids.length) {
|
||||
this.fetching = false
|
||||
return
|
||||
}
|
||||
|
||||
// Remove ids that we have tried too many times.
|
||||
const filteredIds = []
|
||||
for (const id of ids) {
|
||||
this.queue[id]++
|
||||
if (this.queue[id] > this.maxRetries) {
|
||||
if (this.queue[id] > this.maxAttempts) {
|
||||
console.warn(`Failed to fetch ${this.subId} ${id}`)
|
||||
this.failed[id] = true
|
||||
delete this.queue[id]
|
||||
@ -55,12 +56,15 @@ export default class FetchQueue extends Observable {
|
||||
}
|
||||
}
|
||||
|
||||
if (!filteredIds.length) return
|
||||
if (!filteredIds.length) {
|
||||
this.fetching = false
|
||||
return
|
||||
}
|
||||
|
||||
// console.log(`Fetching ${filteredIds.length}/${Object.keys(this.queue).length} ${this.subId}s`, ids)
|
||||
console.log(`Fetching ${this.subId}s ${filteredIds.length}/${Object.keys(this.queue).length} `)
|
||||
|
||||
this.fetching = true
|
||||
this.retryInterval = setInterval(this.fetch.bind(this), this.retryDelay)
|
||||
this.fetchTimeout = setTimeout(this.fetch.bind(this), this.timeout)
|
||||
|
||||
// XXX Needed for some relays?
|
||||
//this.client.unsubscribe(this.subId)
|
||||
@ -78,19 +82,19 @@ export default class FetchQueue extends Observable {
|
||||
this.emit('event', event, relay)
|
||||
|
||||
if (Object.keys(this.queue).length === 0) {
|
||||
if (this.retryInterval) clearInterval(this.retryInterval)
|
||||
if (this.fetchTimeout) clearTimeout(this.fetchTimeout)
|
||||
this.fetching = false
|
||||
sub.close()
|
||||
} else if (filteredIds.length === 0) {
|
||||
this.fetch()
|
||||
}
|
||||
})
|
||||
sub.on('complete', () => {
|
||||
sub.on('end', () => {
|
||||
if (this.fetching && Object.keys(this.queue).length > 0) {
|
||||
this.fetch()
|
||||
} else {
|
||||
console.log('[COMPLETE]', this)
|
||||
if (this.retryInterval) clearInterval(this.retryInterval)
|
||||
if (this.fetchTimeout) clearTimeout(this.fetchTimeout)
|
||||
this.fetching = false
|
||||
sub.close()
|
||||
}
|
||||
|
@ -25,8 +25,8 @@ export default class NostrClient {
|
||||
return this.connectedRelays().some(relay => relay.url === url)
|
||||
}
|
||||
|
||||
subscribe(filters, subId = null) {
|
||||
return this.pool.subscribe(filters, subId)
|
||||
subscribe(filters, subId = null, closeAfter = CloseAfter.NEVER) {
|
||||
return this.pool.subscribe(filters, subId, closeAfter)
|
||||
}
|
||||
|
||||
unsubscribe(subId) {
|
||||
@ -37,47 +37,45 @@ export default class NostrClient {
|
||||
return this.pool.publish(event)
|
||||
}
|
||||
|
||||
// fetchSingle(filters) {
|
||||
// const filtersWithLimit = Object.assign({}, filters, {limit: 1})
|
||||
// return new Promise(resolve => {
|
||||
// const sub = this.pool.subscribe(filters)
|
||||
// sub.on('event')
|
||||
// this.client.subscribe(
|
||||
// filtersWithLimit,
|
||||
// (event, relay) => {
|
||||
// resolve(this.addEvent(event, relay))
|
||||
// },
|
||||
// {
|
||||
// closeAfter: 'single'
|
||||
// }
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
|
||||
fetchMultiple(filters, limit = 100, timeout = 5000) {
|
||||
fetch(filters, opts = {}) {
|
||||
return new Promise(resolve => {
|
||||
const objects = {}
|
||||
const filtersWithLimit = Object.assign({}, filters, {limit})
|
||||
const sub = this.pool.subscribe(filtersWithLimit, null, CloseAfter.EOSE)
|
||||
const events = {}
|
||||
const sub = this.pool.subscribe(filters, opts.subId, CloseAfter.EOSE)
|
||||
const timer = setTimeout(() => {
|
||||
sub.close()
|
||||
resolve(Object.values(objects))
|
||||
}, timeout)
|
||||
resolve(Object.values(events))
|
||||
}, opts.timeout || 4000)
|
||||
sub.on('event', event => {
|
||||
objects[event.id] = event
|
||||
events[event.id] = event
|
||||
})
|
||||
sub.on('complete', () => {
|
||||
sub.on('end', () => {
|
||||
sub.close()
|
||||
clearTimeout(timer)
|
||||
resolve(Object.values(objects))
|
||||
resolve(Object.values(events))
|
||||
})
|
||||
sub.on('close', () => {
|
||||
clearTimeout(timer)
|
||||
resolve(Object.values(objects))
|
||||
resolve(Object.values(events))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
stream(filters, eventCallback, endCallback = () => {}, opts = {}) {
|
||||
const events = {}
|
||||
const sub = this.pool.subscribe(filters, opts.subId)
|
||||
const timer = setTimeout(() => {
|
||||
endCallback(Object.values(events))
|
||||
}, opts.timeout || 5000)
|
||||
sub.on('event', event => {
|
||||
events[event.id] = event
|
||||
})
|
||||
sub.on('end', () => {
|
||||
clearTimeout(timer)
|
||||
endCallback(Object.values(events))
|
||||
})
|
||||
return sub
|
||||
}
|
||||
|
||||
onNotice(relay, message) {
|
||||
console.warn(`[NOTICE] from ${relay}: ${message}`)
|
||||
}
|
||||
|
@ -8,15 +8,20 @@ import {useProfileStore} from 'src/nostr/store/ProfileStore'
|
||||
import {useContactStore} from 'src/nostr/store/ContactStore'
|
||||
import {useSettingsStore} from 'stores/Settings'
|
||||
import {useStatStore} from 'src/nostr/store/StatStore'
|
||||
import {Observable} from 'src/nostr/utils'
|
||||
import {CloseAfter} from 'src/nostr/Relay'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
|
||||
export const Feeds = {
|
||||
GLOBAL: {
|
||||
name: 'global',
|
||||
filters: {
|
||||
kinds: [EventKind.NOTE], // TODO Deletions
|
||||
},
|
||||
initialFetchSize: 100,
|
||||
},
|
||||
class Stream extends Observable {
|
||||
constructor(sub) {
|
||||
super()
|
||||
this.sub = sub
|
||||
sub.on('close', this.emit.bind(this, 'close'))
|
||||
}
|
||||
|
||||
close(relay = null) {
|
||||
this.sub.close(relay)
|
||||
}
|
||||
}
|
||||
|
||||
const eventQueue = (client, subId) => new FetchQueue(
|
||||
@ -76,10 +81,10 @@ export const useNostrStore = defineStore('nostr', {
|
||||
|
||||
if (relay?.url) {
|
||||
if (this.seenBy[event.id]) {
|
||||
this.seenBy[event.id][relay.url] = Date.now()
|
||||
this.seenBy[event.id][relay.url] = DateUtils.now()
|
||||
} else {
|
||||
this.seenBy[event.id] = {
|
||||
[relay.url]: Date.now()
|
||||
[relay.url]: DateUtils.now()
|
||||
}
|
||||
|
||||
const stats = useStatStore()
|
||||
@ -107,7 +112,8 @@ export const useNostrStore = defineStore('nostr', {
|
||||
case EventKind.DELETE:
|
||||
break
|
||||
case EventKind.SHARE:
|
||||
break
|
||||
// TODO
|
||||
return event
|
||||
case EventKind.REACTION: {
|
||||
const notes = useNoteStore()
|
||||
return notes.addEvent(event)
|
||||
@ -162,7 +168,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
},
|
||||
|
||||
fetchNotesByAuthor(pubkey, limit = 100) {
|
||||
return this.fetchMultiple(
|
||||
return this.fetch(
|
||||
{
|
||||
kinds: [EventKind.NOTE],
|
||||
authors: [pubkey],
|
||||
@ -187,7 +193,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
|
||||
fetchFollowers(pubkey, opts = {}) {
|
||||
const limit = opts.limit || 500
|
||||
return this.fetchMultiple(
|
||||
return this.fetch(
|
||||
{
|
||||
kinds: [EventKind.CONTACT],
|
||||
'#p': [pubkey],
|
||||
@ -204,7 +210,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
},
|
||||
|
||||
fetchReactionsTo(id, limit = 500) {
|
||||
return this.fetchMultiple(
|
||||
return this.fetch(
|
||||
{
|
||||
kinds: [EventKind.REACTION],
|
||||
'#e': [id],
|
||||
@ -221,7 +227,7 @@ export const useNostrStore = defineStore('nostr', {
|
||||
},
|
||||
|
||||
fetchReactionsByAuthor(pubkey, limit = 500) {
|
||||
return this.fetchMultiple(
|
||||
return this.fetch(
|
||||
{
|
||||
kinds: [EventKind.REACTION],
|
||||
authors: [pubkey],
|
||||
@ -230,47 +236,98 @@ export const useNostrStore = defineStore('nostr', {
|
||||
)
|
||||
},
|
||||
|
||||
async fetchMultiple(filters, limit = 100, timeout = 5000) {
|
||||
const events = await this.client.fetchMultiple(filters, limit, timeout)
|
||||
return events.map(event => this.addEvent(event))
|
||||
async fetch(filters, opts = {}) {
|
||||
return new Promise(resolve => {
|
||||
const events = {}
|
||||
const sub = this.client.subscribe(filters, opts.subId, CloseAfter.EOSE)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const values = Object.values(events)
|
||||
console.log(`[TIMEOUT] fetch ${sub.subId} (${values.length})`, filters)
|
||||
sub.close()
|
||||
resolve(values)
|
||||
}, opts.timeout || 4000)
|
||||
|
||||
sub.on('end', () => {
|
||||
const values = Object.values(events)
|
||||
console.log(`[COMPLETE] fetch ${sub.subId} (${values.length})`, filters)
|
||||
sub.close()
|
||||
clearTimeout(timer)
|
||||
resolve(values)
|
||||
})
|
||||
sub.on('close', () => {
|
||||
clearTimeout(timer)
|
||||
resolve(Object.values(events))
|
||||
})
|
||||
sub.on('event', (event, relay) => {
|
||||
const object = this.addEvent(event, relay)
|
||||
if (!object) {
|
||||
console.warn('Discarding event', event)
|
||||
return
|
||||
}
|
||||
events[event.id] = object
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
streamThread(rootId, eventCallback, initialFetchCompleteCallback) {
|
||||
return this.streamEvents(
|
||||
{
|
||||
kinds: [EventKind.NOTE],
|
||||
'#e': [rootId],
|
||||
stream(filters, opts = {}) {
|
||||
let objects = {}
|
||||
const sub = this.client.subscribe(filters, opts.subId)
|
||||
const stream = new Stream(sub)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const values = Object.values(objects)
|
||||
console.log(`[TIMEOUT] stream ${sub.subId} (${values.length})`, filters)
|
||||
stream.emit('init', values)
|
||||
objects = null
|
||||
}, opts.timeout || 5000)
|
||||
|
||||
sub.on('end', () => {
|
||||
clearTimeout(timer)
|
||||
const values = Object.values(objects)
|
||||
console.log(`[COMPLETE] stream ${sub.subId} (${values.length})`, filters)
|
||||
stream.emit('init', values)
|
||||
objects = null
|
||||
})
|
||||
sub.on('event', (event, relay) => {
|
||||
const known = this.hasEvent(event.id)
|
||||
const object = this.addEvent(event, relay)
|
||||
if (!object) {
|
||||
console.warn('Discarding event', event)
|
||||
return
|
||||
}
|
||||
if (known) return
|
||||
|
||||
if (!objects) {
|
||||
stream.emit('update', object, relay, stream)
|
||||
} else {
|
||||
objects[event.id] = object
|
||||
}
|
||||
})
|
||||
|
||||
return stream
|
||||
},
|
||||
|
||||
streamThread(rootId) {
|
||||
return this.stream(
|
||||
{
|
||||
kinds: [EventKind.NOTE, EventKind.REACTION, EventKind.SHARE],
|
||||
'#e': [rootId],
|
||||
limit: 500,
|
||||
},
|
||||
500,
|
||||
eventCallback,
|
||||
initialFetchCompleteCallback,
|
||||
{
|
||||
subId: `thread:${rootId}`,
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
streamFeed(feed, eventCallback, initialFetchCompleteCallback) {
|
||||
return this.streamEvents(
|
||||
feed.filters,
|
||||
feed.initialFetchSize,
|
||||
eventCallback,
|
||||
initialFetchCompleteCallback,
|
||||
{
|
||||
subId: feed.name,
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
streamNotifications(pubkey, eventCallback, initialFetchCompleteCallback) {
|
||||
return this.streamEvents(
|
||||
streamNotifications(pubkey) {
|
||||
return this.stream(
|
||||
{
|
||||
kinds: [EventKind.NOTE, EventKind.REACTION], // TODO SHARE, CONTACT
|
||||
'#p': [pubkey],
|
||||
limit: 50,
|
||||
},
|
||||
50,
|
||||
eventCallback,
|
||||
initialFetchCompleteCallback,
|
||||
{
|
||||
subId: `notifications:${pubkey}`,
|
||||
}
|
||||
@ -315,39 +372,5 @@ export const useNostrStore = defineStore('nostr', {
|
||||
this.client.unsubscribe(subId)
|
||||
}
|
||||
},
|
||||
|
||||
streamEvents(filters, initialFetchSize, eventCallback, initialFetchCompleteCallback, opts) {
|
||||
const filtersWithLimit = Object.assign({}, filters, {limit: initialFetchSize})
|
||||
|
||||
let initialFetchComplete = false
|
||||
|
||||
const sub = this.client.subscribe(filtersWithLimit, opts.subId || null)
|
||||
const timer = setTimeout(() => {
|
||||
console.log(`[TIMEOUT] ${sub.subId}, intialFetchComplete=${initialFetchComplete}`)
|
||||
if (!initialFetchComplete) {
|
||||
initialFetchComplete = true
|
||||
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
||||
}
|
||||
}, opts.timeout || 3000)
|
||||
sub.on('event', (event, relay) => {
|
||||
const known = this.hasEvent(event.id)
|
||||
const obj = this.addEvent(event, relay)
|
||||
if (!obj || known) return
|
||||
if (eventCallback) eventCallback(obj, relay)
|
||||
})
|
||||
sub.on('eose', (relay, subId) => {
|
||||
console.log(`[EOSE] ${subId} ${relay} ${this.client.connectedRelays().length}`)
|
||||
})
|
||||
sub.on('complete', () => {
|
||||
console.log(`[COMPLETE] ${sub.subId}, intialFetchComplete=${initialFetchComplete}`)
|
||||
if (!initialFetchComplete) {
|
||||
initialFetchComplete = true
|
||||
clearTimeout(timer)
|
||||
if (initialFetchCompleteCallback) initialFetchCompleteCallback()
|
||||
}
|
||||
})
|
||||
|
||||
return sub
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -45,7 +45,7 @@ class MultiSubscription extends Observable {
|
||||
if (!sub.eoseSeen) {
|
||||
sub.eoseSeen = true
|
||||
if (Object.values(this.subs).every(sub => sub.eoseSeen)) {
|
||||
this.emit('complete', this.subId)
|
||||
this.emit('end', this.subId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -103,6 +103,7 @@ export default class ReplayPool extends Observable {
|
||||
|
||||
subscribe(filters, subId = null, closeAfter = CloseAfter.NEVER) {
|
||||
if (!subId) subId = `sub${this.nextSubId++}`
|
||||
console.log(`[SUBSCRIBE] ${subId}`, filters)
|
||||
|
||||
const sub = new MultiSubscription(subId, [])
|
||||
sub.on('close', this.unsubscribe.bind(this, subId))
|
||||
@ -123,6 +124,7 @@ export default class ReplayPool extends Observable {
|
||||
unsubscribe(subId) {
|
||||
const sub = this.subs[subId]
|
||||
if (!sub) return
|
||||
console.log(`[UNSUBSCRIBE] ${subId}`)
|
||||
sub.sub.close()
|
||||
delete this.subs[subId]
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import {getEventHash} from 'nostr-tools'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
|
||||
export const EventKind = {
|
||||
METADATA: 0,
|
||||
@ -71,7 +72,7 @@ export default class Event {
|
||||
}
|
||||
|
||||
static fresh(opts) {
|
||||
opts.createdAt = opts.createdAt || Math.floor(Date.now() / 1000)
|
||||
opts.createdAt = opts.createdAt || DateUtils.now()
|
||||
return new Event(opts)
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ export const useContactStore = defineStore('contact', {
|
||||
|
||||
const existingContacts = this.contacts[event.pubkey]
|
||||
if (existingContacts && existingContacts.lastUpdatedAt >= event.createdAt) {
|
||||
return
|
||||
return existingContacts
|
||||
}
|
||||
|
||||
const newContacts = []
|
||||
|
@ -31,11 +31,10 @@
|
||||
|
||||
<div class="feed">
|
||||
<div class="load-more-container" :class="{'more-available': numUnreads}">
|
||||
<ButtonLoadMore
|
||||
<AsyncLoadButton
|
||||
v-if="numUnreads"
|
||||
:label="`Load ${numUnreads} unread`"
|
||||
:loading="loading"
|
||||
@click="loadUnreads"
|
||||
:load-fn="loadUnreads"
|
||||
:label="`Load ${numUnreads} unreads`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -45,11 +44,11 @@
|
||||
|
||||
<ListPlaceholder :count="feedItems?.length" :loading="loading" />
|
||||
|
||||
<!-- <ButtonLoadMore-->
|
||||
<!-- :loading="loadingMore"-->
|
||||
<!-- :label="items.length === feed[tab].length ? 'load another day' : 'load 100 more'"-->
|
||||
<!-- @click="loadMore"-->
|
||||
<!-- />-->
|
||||
<AsyncLoadButton
|
||||
v-if="feedItems?.length"
|
||||
:load-fn="loadOlder"
|
||||
autoload
|
||||
/>
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
@ -60,11 +59,23 @@ import PageHeader from 'components/PageHeader.vue'
|
||||
import PostEditor from 'components/CreatePost/PostEditor.vue'
|
||||
import Thread from 'components/Post/Thread.vue'
|
||||
import BaseIcon from 'components/BaseIcon/index.vue'
|
||||
import ButtonLoadMore from 'components/ButtonLoadMore.vue'
|
||||
import AsyncLoadButton from 'components/AsyncLoadButton.vue'
|
||||
import ListPlaceholder from 'components/ListPlaceholder.vue'
|
||||
import {useAppStore} from 'stores/App'
|
||||
import {useNostrStore, Feeds} from 'src/nostr/NostrStore'
|
||||
import {useNostrStore} from 'src/nostr/NostrStore'
|
||||
import Defer from 'src/utils/Defer'
|
||||
import {EventKind} from 'src/nostr/model/Event'
|
||||
import DateUtils from 'src/utils/DateUtils'
|
||||
|
||||
const Feeds = {
|
||||
global: {
|
||||
name: 'global',
|
||||
filters: {
|
||||
kinds: [EventKind.NOTE], // TODO Deletions
|
||||
limit: 20,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const feedOrder = (a, b) => b[0].createdAt - a[0].createdAt
|
||||
|
||||
@ -75,10 +86,10 @@ export default defineComponent({
|
||||
PostEditor,
|
||||
Thread,
|
||||
BaseIcon,
|
||||
ButtonLoadMore,
|
||||
AsyncLoadButton,
|
||||
ListPlaceholder,
|
||||
},
|
||||
mixins: [Defer()],
|
||||
mixins: [Defer(2000)],
|
||||
setup() {
|
||||
return {
|
||||
app: useAppStore(),
|
||||
@ -92,7 +103,6 @@ export default defineComponent({
|
||||
selectedFeed: 'global',
|
||||
loading: true,
|
||||
recentlyLoaded: true,
|
||||
sub: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -114,57 +124,73 @@ export default defineComponent({
|
||||
initFeed(feedId) {
|
||||
if (this.feeds[feedId]) return
|
||||
|
||||
this.feeds[feedId] = {
|
||||
items: [],
|
||||
unreads: [],
|
||||
}
|
||||
|
||||
let initialFetchComplete = false
|
||||
let initialItems = []
|
||||
|
||||
console.log(`subscribing to feed ${feedId}`, this.feeds[feedId])
|
||||
|
||||
this.sub = this.nostr.streamFeed(
|
||||
Feeds[feedId.toUpperCase()],
|
||||
event => {
|
||||
const target = initialFetchComplete
|
||||
? this.feeds[feedId].unreads
|
||||
: initialItems
|
||||
target.push([event]) // FIXME Single element thread
|
||||
},
|
||||
() => {
|
||||
initialItems.sort(feedOrder)
|
||||
this.feeds[feedId].items = initialItems.slice(0, 50)
|
||||
initialFetchComplete = true
|
||||
const filters = Feeds[feedId].filters
|
||||
const stream = this.nostr.stream(
|
||||
filters,
|
||||
{subId: `feed:${feedId}`}
|
||||
)
|
||||
stream.on('init', events => {
|
||||
const items = events.map(event => [event]) // TODO Single element thread
|
||||
items.sort(feedOrder)
|
||||
this.feeds[feedId].items = items.slice(0, filters.limit)
|
||||
this.loading = false
|
||||
|
||||
// Wait a bit before showing the first unreads
|
||||
setTimeout(() => this.recentlyLoaded = false, 5000)
|
||||
})
|
||||
stream.on('update', event => {
|
||||
this.feeds[feedId].unreads.push([event]) // TODO Single element thread
|
||||
})
|
||||
|
||||
this.feeds[feedId] = {
|
||||
items: [],
|
||||
unreads: [],
|
||||
stream,
|
||||
}
|
||||
)
|
||||
},
|
||||
switchFeed(feedId) {
|
||||
this.initFeed(feedId)
|
||||
this.selectedFeed = feedId
|
||||
},
|
||||
loadUnreads() {
|
||||
this.loading = true
|
||||
// TODO Deduplicate feed items
|
||||
const items = this.feedUnreads.concat(this.feedItems)
|
||||
items.sort(feedOrder)
|
||||
//items.sort(feedOrder)
|
||||
this.activeFeed.items = items
|
||||
this.activeFeed.unreads = []
|
||||
this.loading = false
|
||||
|
||||
// Wait a bit before showing unreads again
|
||||
this.recentlyLoaded = true
|
||||
setTimeout(() => this.recentlyLoaded = false, 5000)
|
||||
|
||||
return true
|
||||
},
|
||||
async loadOlder() {
|
||||
const until = this.feedItems[this.feedItems.length - 1]?.[0]?.createdAt || DateUtils.now()
|
||||
console.log('until', new Date(until * 1000))
|
||||
const filters = Object.assign({}, Feeds[this.selectedFeed].filters, {until})
|
||||
|
||||
const older = await this.nostr.fetch(filters, {subId: `feed:${this.selectedFeed}-older`})
|
||||
const items = older.map(event => [event]).sort(feedOrder)
|
||||
|
||||
console.log('got items', older)
|
||||
|
||||
console.log('length before', this.activeFeed.items.length)
|
||||
// TODO Deduplicate feed items
|
||||
this.activeFeed.items = this.feedItems.concat(items)
|
||||
|
||||
console.log('length after', this.activeFeed.items.length)
|
||||
|
||||
return older
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initFeed(this.selectedFeed)
|
||||
},
|
||||
unmounted() {
|
||||
if (this.sub) this.nostr.cancelStream(this.sub)
|
||||
for (const feed of Object.values(this.feeds)) {
|
||||
feed.stream.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -179,7 +205,7 @@ export default defineComponent({
|
||||
border-bottom: $border-dark;
|
||||
min-height: 6px;
|
||||
}
|
||||
> .load-more:last-child {
|
||||
> .async-load-button:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
@ -32,30 +32,24 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
stream: null,
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
},
|
||||
mounted() {
|
||||
const read = []
|
||||
const unread = []
|
||||
this.nostr.streamNotifications(
|
||||
this.app.myPubkey,
|
||||
event => {
|
||||
if (event.createdAt >= this.settings.notificationsLastRead) {
|
||||
unread.push(event)
|
||||
} else {
|
||||
read.push(event)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
unread.sort(NoteOrder.CREATION_DATE_DESC)
|
||||
this.notifications = unread
|
||||
this.stream = this.nostr.streamNotifications(this.app.myPubkey)
|
||||
this.stream.on('init', events => {
|
||||
events.sort(NoteOrder.CREATION_DATE_DESC)
|
||||
this.notifications = events
|
||||
this.loading = false
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
this.stream.on('update', event => this.notifications.unshift(event))
|
||||
},
|
||||
unmounted() {
|
||||
if (this.stream) this.stream.close()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
<div ref="ancestors">
|
||||
<Thread
|
||||
v-if="ancestors?.length"
|
||||
:thread="ancestors"
|
||||
force-bottom-connector
|
||||
class="ancestors"
|
||||
@ -96,17 +97,14 @@ export default defineComponent({
|
||||
methods: {
|
||||
startStream() {
|
||||
if (!this.rootId) return
|
||||
this.subId = this.nostr.streamThread(
|
||||
this.rootId,
|
||||
() => {}, // TODO
|
||||
this.buildThread.bind(this)
|
||||
)
|
||||
this.stream = this.nostr.streamThread(this.rootId)
|
||||
this.stream.on('init', this.buildThread.bind(this))
|
||||
},
|
||||
|
||||
cancelStream() {
|
||||
if (!this.subId) return
|
||||
this.nostr.cancelStream(this.subId)
|
||||
this.subId = null
|
||||
closeStream() {
|
||||
if (!this.stream) return
|
||||
this.stream.close()
|
||||
this.stream = null
|
||||
},
|
||||
|
||||
buildThread() {
|
||||
@ -235,7 +233,7 @@ export default defineComponent({
|
||||
},
|
||||
unmounted() {
|
||||
console.log('unmounted', this.subId)
|
||||
this.cancelStream()
|
||||
this.closeStream()
|
||||
this.resizeObserver.disconnect()
|
||||
}
|
||||
})
|
||||
|
@ -17,6 +17,10 @@ const MONTHS = [
|
||||
]
|
||||
|
||||
export default class DateUtils {
|
||||
static now() {
|
||||
return Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
static formatDate(timestamp) {
|
||||
const date = new Date(timestamp * 1000)
|
||||
const month = MONTHS[date.getMonth()] // TODO i18n
|
||||
@ -49,8 +53,7 @@ export default class DateUtils {
|
||||
}
|
||||
|
||||
static formatFromNowShort(timestamp) {
|
||||
const now = Date.now()
|
||||
const diff = Math.round(Math.max(now - (timestamp * 1000), 0) / 1000)
|
||||
const diff = Math.max(DateUtils.now() - timestamp, 0)
|
||||
const formatDiff = (unit, factor, offset) => Math.max(Math.floor((diff + (unit * offset)) / (unit * factor)), 1)
|
||||
|
||||
if (diff < 45) return `${formatDiff(1, 1, 0)}s`
|
||||
|
@ -9,9 +9,10 @@ export default class Nip05 {
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
const json = await res.json()
|
||||
if (!json || !json.names) return
|
||||
return json.names[user]
|
||||
} catch (e) {
|
||||
console.warn(`Failed to fetch NIP05 data for ${nip05Id}`, e)
|
||||
//console.warn(`Failed to fetch NIP05 data for ${nip05Id}`, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user