Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement random sort and dynamic timeline sorting via dropdown #13

Merged
merged 10 commits into from
Jun 5, 2024
Merged
35 changes: 35 additions & 0 deletions db.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,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)
Expand Down Expand Up @@ -250,6 +257,34 @@ export class ActivityPubDB extends EventTarget {
await tx.done
}

async * searchNotesRandom ({ limit = DEFAULT_LIMIT } = {}) {
const tx = this.db.transaction(NOTES_STORE, 'readonly')
const store = tx.objectStore(NOTES_STORE)
const totalNotes = await store.count()

RangerMauve marked this conversation as resolved.
Show resolved Hide resolved
if (totalNotes === 0) return // Early exit if no notes are present

let cursor = await store.openCursor()
const uniqueIndexes = new Set()

while (uniqueIndexes.size < limit && uniqueIndexes.size < totalNotes) {
const randomSkip = Math.floor(Math.random() * totalNotes)
if (!uniqueIndexes.has(randomSkip)) {
uniqueIndexes.add(randomSkip)
if (randomSkip > 0) {
// Move the cursor to the randomSkip position if not already there
await cursor.advance(randomSkip)
}
if (cursor) {
RangerMauve marked this conversation as resolved.
Show resolved Hide resolved
yield cursor.value
// After yielding, reset the cursor to the start for the next random pick
cursor = await store.openCursor()
}
}
}
await tx.done
}

async ingestActor (url, isInitial = false) {
console.log(`Starting ingestion for actor from URL: ${url}`)
const actor = await this.getActor(url)
Expand Down
12 changes: 11 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@
</style>
<div class="container">
<sidebar-nav></sidebar-nav>
<reader-timeline></reader-timeline>
<div class="reader-container">
<div class="sort-container">
RangerMauve marked this conversation as resolved.
Show resolved Hide resolved
<label for="sortOrder">Sort Timeline:</label>
<select id="sortOrder" class="sort-dropdown">
<option value="latest">Newest</option>
<option value="oldest">Oldest</option>
<option value="random">Random</option>
</select>
</div>
<reader-timeline></reader-timeline>
</div>
<div class="right-column">
<!-- This is an empty column to balance the layout -->
</div>
Expand Down
2 changes: 1 addition & 1 deletion theme-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
34 changes: 33 additions & 1 deletion timeline.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,9 +46,12 @@ reader-timeline {
}

@media screen and (max-width: 768px) {
.reader-container {
margin-top: 160px;
}

reader-timeline {
width: 100%;
max-width: 100%;
margin-top: 150px;
}
}
101 changes: 82 additions & 19 deletions timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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/@[email protected]/',
Expand All @@ -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.searchNotesRandom({ limit: this.limit })) {
this.appendNoteElement(note)
count++
}
} else {
const notesToShow = await this.fetchSortedNotes()
for (const note of notesToShow) {
if (note) {
this.appendNoteElement(note)
count++
}
}
}

this.updateIndexes(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
updateIndexes (count) {
RangerMauve marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Expand Down
Loading