diff --git a/src/components/AsyncLoadButton.vue b/src/components/AsyncLoadButton.vue index add7477..c3dc02b 100644 --- a/src/components/AsyncLoadButton.vue +++ b/src/components/AsyncLoadButton.vue @@ -44,7 +44,6 @@ export default defineComponent({ methods: { async load() { if (this.loading) return - console.log('loading') this.loading = true this.$emit('loading') diff --git a/src/nostr/model/Note.js b/src/nostr/model/Note.js index 038d567..c21eec6 100644 --- a/src/nostr/model/Note.js +++ b/src/nostr/model/Note.js @@ -41,21 +41,28 @@ export default class Note { return this.eventRefs().ancestor() } + pubkeyTags() { + return this.tags.filter(tag => tag.type === TagType.PUBKEY) + } + + eventTags() { + return this.tags.filter(tag => tag.type === TagType.EVENT) + } + pubkeyRefs() { - return this.tags - .filter(tag => tag.type === TagType.PUBKEY) - .map(tag => tag.ref) + return this.pubkeyTags().map(tag => tag.ref) } eventRefs() { - const refs = this.tags - .filter(tag => tag.type === TagType.EVENT) - .map(tag => tag.ref) - return new EventRefs(refs) + return new EventRefs(this.eventTags().map(tag => tag.ref)) + } + + relatedPubkeys() { + return [this.author].concat(this.pubkeyRefs()) } contentTagRefs() { - const regex = /#\[([0-9]+)]/ig + const regex = /#\[([0-9]+)]/g let refs = [] let match while ((match = regex.exec(this.content))) { @@ -64,9 +71,16 @@ export default class Note { return refs } + isRepostOrTag() { + return Note.isRepostOrTag(this) + } + + static isRepostOrTag(event) { + return /#\[([0-9]+)]/.test(event.content) + } + isReaction() { - return this.kind === EventKind.REACTION - || (this.hasAncestor() && Note.isReactionContent(this.content)) + return Note.isReaction(this) } static isReaction(event) { diff --git a/src/nostr/store/StatStore.js b/src/nostr/store/StatStore.js index 89f20d4..1467713 100644 --- a/src/nostr/store/StatStore.js +++ b/src/nostr/store/StatStore.js @@ -22,6 +22,9 @@ export const useStatStore = defineStore('stat', { if (Note.isReaction(event)) { const stats = this.getOrInit(event.eventRefs().ancestor()) stats.reactions++ + } else if (Note.isRepostOrTag(event)) { + const stats = this.getOrInit(event.eventRefs().ancestor()) + stats.shares++ } else { for (const eventId of event.eventRefs()) { const stats = this.getOrInit(eventId) diff --git a/src/pages/Feed.vue b/src/pages/Feed.vue index d18e77c..09982b3 100644 --- a/src/pages/Feed.vue +++ b/src/pages/Feed.vue @@ -33,13 +33,13 @@
@@ -66,6 +66,7 @@ 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' +import Bots from 'src/utils/bots' const Feeds = { global: { @@ -79,6 +80,8 @@ const Feeds = { const feedOrder = (a, b) => b[0].createdAt - a[0].createdAt +const MAX_ITEMS_VISIBLE = 50 + export default defineComponent({ name: 'Feed', components: { @@ -110,14 +113,11 @@ export default defineComponent({ return this.feeds[this.selectedFeed] }, feedItems() { - return this.activeFeed?.items - }, - feedUnreads() { - return this.activeFeed?.unreads + return this.activeFeed?.visible }, numUnreads() { if (this.recentlyLoaded) return 0 - return this.activeFeed?.unreads.length + return this.activeFeed?.newer.length }, }, methods: { @@ -129,22 +129,27 @@ export default defineComponent({ filters, {subId: `feed:${feedId}`} ) - stream.on('init', events => { - const items = events.map(event => [event]) // TODO Single element thread + stream.on('init', notes => { + const items = notes + .filter(this.filterNote.bind(this)) + .map(note => [note]) // TODO Single element thread items.sort(feedOrder) - this.feeds[feedId].items = items.slice(0, filters.limit) + this.feeds[feedId].visible = 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 + stream.on('update', note => { + if (this.filterNote(note)) { + this.feeds[feedId].newer.push([note]) // TODO Single element thread + } }) this.feeds[feedId] = { - items: [], - unreads: [], + visible: [], + newer: [], + older: [], stream, } }, @@ -152,12 +157,18 @@ export default defineComponent({ this.initFeed(feedId) this.selectedFeed = feedId }, - loadUnreads() { + loadNewer() { // TODO Deduplicate feed items - const items = this.feedUnreads.concat(this.feedItems) + this.activeFeed.newer.sort(feedOrder) + const items = this.activeFeed.newer.concat(this.feedItems) + if (items.length > MAX_ITEMS_VISIBLE) { + const older = items.splice(MAX_ITEMS_VISIBLE) + this.activeFeed.older = older.concat(this.activeFeed.older) + } //items.sort(feedOrder) - this.activeFeed.items = items - this.activeFeed.unreads = [] + + this.activeFeed.visible = items + this.activeFeed.newer = [] // Wait a bit before showing unreads again this.recentlyLoaded = true @@ -167,22 +178,35 @@ export default defineComponent({ }, 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}) + if (this.activeFeed.older.length >= filters.limit) { + const chunk = this.activeFeed.older.splice(0, filters.limit) + this.activeFeed.visible = this.feedItems.concat(chunk) + return chunk + } + + // Remove any residual older items + this.activeFeed.older = [] + const older = await this.nostr.fetch(filters, {subId: `feed:${this.selectedFeed}-older`}) - const items = older.map(event => [event]).sort(feedOrder) + const items = older + .filter(note => note.createdAt <= until) + .filter(this.filterNote.bind(this)) + .map(note => [note]) // TODO Single element thread + .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) + this.activeFeed.visible = this.feedItems.concat(items) return older }, + filterNote(note) { + if (note.isReaction()) return false + if (note.isRepostOrTag()) return false + if (note.relatedPubkeys().some(Bots.isBot)) return false + return true + } }, mounted() { this.initFeed(this.selectedFeed) diff --git a/src/utils/bots.js b/src/utils/bots.js new file mode 100644 index 0000000..5e65637 --- /dev/null +++ b/src/utils/bots.js @@ -0,0 +1,10 @@ +import {bech32ToHex} from 'src/utils/utils' + +const Bots = new Set() +export default Bots +Bots.isBot = Bots.has.bind(Bots) +Bots.add = pubkey => Set.prototype.add.call(Bots, bech32ToHex(pubkey)) + +Bots.add('npub1tsgw6pncspg4d5u778hk63s3pls70evs4czfsmx0fzap9xwt203qtkhtk4') // gpt3 +Bots.add('npub17stpezz4suqdywh33k9x8pht04l76a5sfrsjj7q3mnp5ap5937eqdt58d7') // bitcoin_bot +Bots.add('npub1xe59lfgsdvduqwh8h65zahkc2hv02mzpmdxghhhcpx0puret9taqheapxc') // moe_bot