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)
}