Skip to content

Commit

Permalink
Merge pull request #13 from hyphacoop/timelines
Browse files Browse the repository at this point in the history
feat: implement random sort and dynamic timeline sorting via dropdown
  • Loading branch information
RangerMauve authored Jun 5, 2024
2 parents 3e099c6 + 63a49dd commit 192b27f
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 34 deletions.
48 changes: 35 additions & 13 deletions db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
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">
<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
33 changes: 33 additions & 0 deletions 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,6 +46,10 @@ reader-timeline {
}

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

reader-timeline {
width: 100%;
max-width: 100%;
Expand Down
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.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)
}
Expand Down

0 comments on commit 192b27f

Please sign in to comment.