diff --git a/db.js b/db.js index f09b0c6..eee26ea 100644 --- a/db.js +++ b/db.js @@ -173,6 +173,13 @@ export class ActivityPubDB extends EventTarget { } } + async getTotalNotesCount () { + const tx = this.db.transaction(NOTES_STORE, 'readonly') + const store = tx.objectStore(NOTES_STORE) + const totalNotes = await store.count() + return totalNotes + } + async getActivity (url) { try { return this.db.get(ACTIVITIES_STORE, url) @@ -205,28 +212,43 @@ export class ActivityPubDB extends EventTarget { async * searchNotes ({ attributedTo } = {}, { skip = 0, limit = DEFAULT_LIMIT, sort = -1 } = {}) { const tx = this.db.transaction(NOTES_STORE, 'readonly') let count = 0 - const direction = sort > 0 ? 'next' : 'prev' // 'prev' for descending order + const direction = sort > 0 ? 'next' : (sort === 0 ? 'next' : 'prev') // 'prev' for descending order let cursor = null const indexName = attributedTo ? ATTRIBUTED_TO_FIELD + ', published' : PUBLISHED_FIELD const index = tx.store.index(indexName) - if (attributedTo) { - cursor = await index.openCursor([attributedTo], direction) + if (sort === 0) { // Random sort + // TODO: Consider removing duplicates in the future to improve UX + const totalNotes = await index.count() + for (let i = 0; i < limit; i++) { + const randomSkip = Math.floor(Math.random() * totalNotes) + cursor = await index.openCursor() + if (randomSkip > 0) { + await cursor.advance(randomSkip) + } + if (cursor) { + yield cursor.value + } + } } else { - cursor = await index.openCursor(null, direction) - } + if (attributedTo) { + cursor = await index.openCursor([attributedTo], direction) + } else { + cursor = await index.openCursor(null, direction) + } - // Skip the required entries - if (skip) await cursor.advance(skip) + // Skip the required entries + if (skip) await cursor.advance(skip) - // Collect the required limit of entries - while (cursor) { - if (count >= limit) break - count++ - yield cursor.value - cursor = await cursor.continue() + // Collect the required limit of entries + while (cursor) { + if (count >= limit) break + count++ + yield cursor.value + cursor = await cursor.continue() + } } await tx.done diff --git a/index.html b/index.html index 54d695c..d99a83c 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,17 @@
- +
+
+ + +
+ +
diff --git a/theme-selector.js b/theme-selector.js index d12fe67..029ce92 100644 --- a/theme-selector.js +++ b/theme-selector.js @@ -39,7 +39,7 @@ class ThemeSelector extends HTMLElement { const style = document.createElement('style') style.textContent = ` select { - padding: 4px; + padding: 2px; margin: 6px 0; border: 1px solid var(--rdp-border-color); border-radius: 4px; diff --git a/timeline.css b/timeline.css index c322362..6fb9a9e 100644 --- a/timeline.css +++ b/timeline.css @@ -9,6 +9,35 @@ body { background: var(--bg-color); } +.reader-container { + margin-top: 20px; + display: flex; + flex-direction: column; + align-items: flex-start; +} + +.sort-container { + width: 100%; + display: flex; + justify-content: flex-start; +} + +.sort-container label { + color: var(--rdp-text-color); + font-size: 0.875rem; + margin-right: 4px; + display: flex; + align-items: center; + margin-left: 48px; +} + +.sort-dropdown { + padding: 2px; + border: 1px solid var(--rdp-border-color); + border-radius: 4px; + width: 75px; +} + reader-timeline { flex: 1; max-width: 600px; @@ -17,6 +46,10 @@ reader-timeline { } @media screen and (max-width: 768px) { + .reader-container { + margin-top: 160px; + } + reader-timeline { width: 100%; max-width: 100%; diff --git a/timeline.js b/timeline.js index b42477b..fb7a685 100644 --- a/timeline.js +++ b/timeline.js @@ -6,12 +6,15 @@ class ReaderTimeline extends HTMLElement { skip = 0 limit = 32 hasMoreItems = true + sort = 'latest' + totalNotesCount = 0 + loadedNotesCount = 0 loadMoreBtn = null constructor () { super() this.loadMoreBtn = document.createElement('button') - this.loadMoreBtn.textContent = 'Load More..' + this.loadMoreBtn.textContent = 'Load More...' this.loadMoreBtn.className = 'load-more-btn' this.loadMoreBtnWrapper = document.createElement('div') @@ -21,10 +24,43 @@ class ReaderTimeline extends HTMLElement { this.loadMoreBtn.addEventListener('click', () => this.loadMore()) } - connectedCallback () { + async connectedCallback () { + this.initializeSortOrder() this.initializeDefaultFollowedActors().then(() => this.initTimeline()) } + initializeSortOrder () { + const params = new URLSearchParams(window.location.search) + this.sort = params.get('sort') || 'latest' + + const sortOrderSelect = document.getElementById('sortOrder') + if (sortOrderSelect) { + sortOrderSelect.value = this.sort + sortOrderSelect.addEventListener('change', (event) => { + this.sort = event.target.value + this.updateURL() + this.resetTimeline() + }) + } + } + + updateURL () { + const url = new URL(window.location) + url.searchParams.set('sort', this.sort) + window.history.pushState({}, '', url) + } + + async resetTimeline () { + this.skip = 0 + this.totalNotesCount = await db.getTotalNotesCount() + this.loadedNotesCount = 0 + this.hasMoreItems = true + while (this.firstChild) { + this.removeChild(this.firstChild) + } + this.loadMore() + } + async initializeDefaultFollowedActors () { const defaultActors = [ 'https://social.distributed.press/v1/@announcements@social.distributed.press/', @@ -35,41 +71,68 @@ class ReaderTimeline extends HTMLElement { // "https://staticpub.mauve.moe/about.jsonld", ] - // Check if followed actors have already been initialized const hasFollowedActors = await db.hasFollowedActors() if (!hasFollowedActors) { - await Promise.all( - defaultActors.map(async (actorUrl) => { - await db.followActor(actorUrl) - }) - ) + await Promise.all(defaultActors.map(actorUrl => db.followActor(actorUrl))) } } async initTimeline () { + this.loadMore() // Start loading notes immediately + if (!hasLoaded) { hasLoaded = true const followedActors = await db.getFollowedActors() - await Promise.all(followedActors.map(({ url }) => db.ingestActor(url))) + // Ingest actors in the background without waiting for them + Promise.all(followedActors.map(({ url }) => db.ingestActor(url))) + .then(() => console.log('All followed actors have been ingested')) + .catch(error => console.error('Error ingesting followed actors:', error)) } - this.loadMore() } async loadMore () { - // Remove the button before loading more items this.loadMoreBtnWrapper.remove() - let count = 0 - for await (const note of db.searchNotes({}, { skip: this.skip, limit: this.limit })) { - count++ - this.appendNoteElement(note) + + if (this.sort === 'random') { + for await (const note of db.searchNotes({}, { limit: this.limit, sort: this.sort === 'random' ? 0 : (this.sort === 'oldest' ? 1 : -1) })) { + this.appendNoteElement(note) + count++ + } + } else { + const notesToShow = await this.fetchSortedNotes() + for (const note of notesToShow) { + if (note) { + this.appendNoteElement(note) + count++ + } + } + } + + this.updateHasMore(count) + this.appendLoadMoreIfNeeded() + } + + async fetchSortedNotes () { + const notesGenerator = db.searchNotes({}, { skip: this.skip, limit: this.limit, sort: this.sort === 'oldest' ? 1 : -1 }) + const notes = [] + for await (const note of notesGenerator) { + notes.push(note) } + return notes + } - // Update skip value and determine if there are more items - this.skip += this.limit - this.hasMoreItems = count === this.limit + updateHasMore (count) { + if (this.sort === 'random') { + this.loadedNotesCount += count + this.hasMoreItems = this.loadedNotesCount < this.totalNotesCount + } else { + this.skip += count + this.hasMoreItems = count === this.limit + } + } - // Append the button at the end if there are more items + appendLoadMoreIfNeeded () { if (this.hasMoreItems) { this.appendChild(this.loadMoreBtnWrapper) }