diff --git a/src/components/Feed/Feed.vue b/src/components/Feed/Feed.vue new file mode 100644 index 0000000..92e6ce0 --- /dev/null +++ b/src/components/Feed/Feed.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/src/components/PageHeader.vue b/src/components/PageHeader.vue index 1df7afb..29a6d74 100644 --- a/src/components/PageHeader.vue +++ b/src/components/PageHeader.vue @@ -108,6 +108,7 @@ export default defineComponent({ } .addon { flex-grow: 1; + text-align: right; } } diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 36fa601..d6d7ac9 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -66,7 +66,7 @@ export default defineComponent({ }, data() { return { - cachedPages: ['Feed', 'Notifications', 'Messages', 'Inbox', 'Settings'], + cachedPages: ['FeedPage', 'Notifications', 'Messages', 'Inbox', 'Settings'], mobileMenuOpen: false, } }, diff --git a/src/nostr/NostrStore.js b/src/nostr/NostrStore.js index 8138a7a..8b9657b 100644 --- a/src/nostr/NostrStore.js +++ b/src/nostr/NostrStore.js @@ -6,7 +6,7 @@ import FetchQueue from 'src/nostr/FetchQueue' import {NoteOrder, useNoteStore} from 'src/nostr/store/NoteStore' import {useProfileStore} from 'src/nostr/store/ProfileStore' import {useContactStore} from 'src/nostr/store/ContactStore' -import {useSettingsStore} from 'stores/Settings' +import {useSettingsStore, RELAYS} from 'stores/Settings' import {useStatStore} from 'src/nostr/store/StatStore' import {Observable} from 'src/nostr/utils' import {CloseAfter} from 'src/nostr/Relay' @@ -61,7 +61,8 @@ export const useNostrStore = defineStore('nostr', { actions: { init() { const settings = useSettingsStore() - this.client = markRaw(new NostrClient(settings.relays)) + // FIXME Use relays from settings + this.client = markRaw(new NostrClient(RELAYS)) this.client.connect() this.profileQueue = profileQueue(this.client) @@ -172,8 +173,8 @@ export const useNostrStore = defineStore('nostr', { { kinds: [EventKind.NOTE], authors: [pubkey], - }, - limit + limit, + } ) }, @@ -191,14 +192,13 @@ export const useNostrStore = defineStore('nostr', { return followers }, - fetchFollowers(pubkey, opts = {}) { - const limit = opts.limit || 500 + fetchFollowers(pubkey, limit = 500) { return this.fetch( { kinds: [EventKind.CONTACT], '#p': [pubkey], + limit, }, - limit ) }, @@ -231,8 +231,8 @@ export const useNostrStore = defineStore('nostr', { { kinds: [EventKind.REACTION], authors: [pubkey], - }, - limit + limit, + } ) }, diff --git a/src/pages/Feed.vue b/src/pages/Feed.vue index 09982b3..6167070 100644 --- a/src/pages/Feed.vue +++ b/src/pages/Feed.vue @@ -2,26 +2,18 @@ @@ -29,27 +21,11 @@ -
-
- -
- - - - - - -
+ + + + +
@@ -57,16 +33,12 @@ import {defineComponent} from 'vue' 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 AsyncLoadButton from 'components/AsyncLoadButton.vue' -import ListPlaceholder from 'components/ListPlaceholder.vue' +import Feed from 'components/Feed/Feed.vue' import {useAppStore} from 'stores/App' 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 ZERO_PUBKEY = '0000000000000000000000000000000000000000000000000000000000000000' const Feeds = { global: { @@ -75,24 +47,33 @@ const Feeds = { kinds: [EventKind.NOTE], // TODO Deletions limit: 20, }, + hideBots: true, + }, + following: { + name: 'following', + filters: () => { + const app = useAppStore() + const nostr = useNostrStore() + const contacts = nostr.getContacts(app.myPubkey) + let authors = contacts?.map(contact => contact.pubkey) + if (!authors || !authors.length) authors = [ZERO_PUBKEY] + return { + authors, + kinds: [EventKind.NOTE], + limit: 50, + } + }, + hideBots: false, }, } -const feedOrder = (a, b) => b[0].createdAt - a[0].createdAt - -const MAX_ITEMS_VISIBLE = 50 - export default defineComponent({ - name: 'Feed', + name: 'FeedPage', components: { PageHeader, PostEditor, - Thread, - BaseIcon, - AsyncLoadButton, - ListPlaceholder, + Feed, }, - mixins: [Defer(2000)], setup() { return { app: useAppStore(), @@ -102,120 +83,31 @@ export default defineComponent({ data() { return { feeds: {}, - availableFeeds: ['global'], - selectedFeed: 'global', - loading: true, - recentlyLoaded: true, + activeFeed: 'global', } }, computed: { - activeFeed() { - return this.feeds[this.selectedFeed] + availableFeeds() { + const feeds = ['global'] + if (this.app.isSignedIn) feeds.push('following') + return feeds }, - feedItems() { - return this.activeFeed?.visible - }, - numUnreads() { - if (this.recentlyLoaded) return 0 - return this.activeFeed?.newer.length + contacts() { + if (!this.app.isSignedIn) return + return this.nostr.getContacts(this.app.myPubkey) }, }, methods: { - initFeed(feedId) { - if (this.feeds[feedId]) return - - const filters = Feeds[feedId].filters - const stream = this.nostr.stream( - filters, - {subId: `feed:${feedId}`} - ) - 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].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', note => { - if (this.filterNote(note)) { - this.feeds[feedId].newer.push([note]) // TODO Single element thread - } - }) - - this.feeds[feedId] = { - visible: [], - newer: [], - older: [], - stream, - } + feedDef(feed) { + return Feeds[feed] }, - switchFeed(feedId) { - this.initFeed(feedId) - this.selectedFeed = feedId - }, - loadNewer() { - // TODO Deduplicate feed items - 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.visible = items - this.activeFeed.newer = [] - - // 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() - 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 - .filter(note => note.createdAt <= until) - .filter(this.filterNote.bind(this)) - .map(note => [note]) // TODO Single element thread - .sort(feedOrder) - - // TODO Deduplicate feed items - 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) + watch: { + contacts() { + console.log('following', this.$refs.following) + this.$refs.following?.[0]?.reload() + }, }, - unmounted() { - for (const feed of Object.values(this.feeds)) { - feed.stream.close() - } - } }) @@ -223,23 +115,9 @@ export default defineComponent({ @import "assets/theme/colors.scss"; @import "assets/variables.scss"; -.feed { - .load-more-container { - border-top: $border-dark; - border-bottom: $border-dark; - min-height: 6px; - } - > .async-load-button:last-child { - border-bottom: 0; - } -} - -.addon-menu { - display: flex; - flex-direction: row-reverse; - &-icon { - cursor: pointer; - } +.feed-selector { + background-color: rgba($color: $color-dark-gray, $alpha: 0.2); + //box-shadow: none; } @media screen and (max-width: $phone) { @@ -249,50 +127,27 @@ export default defineComponent({ .post-editor { display: none; } - .feed { - .load-more-container { - border: 0; - min-height: 0; - &.more-available { - border-bottom: $border-dark; - } - } - } }