Thread viewer for Bluesky
1class PrivateSearchPage { 2 3 /** @type {number | undefined} */ 4 fetchStartTime; 5 6 constructor() { 7 this.pageElement = $id('private_search_page'); 8 9 this.rangeInput = $(this.pageElement.querySelector('input[type="range"]'), HTMLInputElement); 10 this.submitButton = $(this.pageElement.querySelector('input[type="submit"]'), HTMLInputElement); 11 this.progressBar = $(this.pageElement.querySelector('input[type="submit"] + progress'), HTMLProgressElement); 12 this.archiveStatus = $(this.pageElement.querySelector('.archive-status')); 13 14 this.searchLine = $(this.pageElement.querySelector('.search')); 15 this.searchField = $(this.pageElement.querySelector('.search-query'), HTMLInputElement); 16 this.results = $(this.pageElement.querySelector('.results')); 17 18 this.timelinePosts = []; 19 20 this.setupEvents(); 21 } 22 23 setupEvents() { 24 $(this.pageElement.querySelector('form')).addEventListener('submit', (e) => { 25 e.preventDefault(); 26 27 if (!this.fetchStartTime) { 28 this.fetchTimeline(); 29 } else { 30 this.stopFetch(); 31 } 32 }); 33 34 this.rangeInput.addEventListener('input', (e) => { 35 let days = parseInt(this.rangeInput.value, 10); 36 let label = $(this.pageElement.querySelector('input[type=range] + label')); 37 label.innerText = (days == 1) ? '1 day' : `${days} days`; 38 }); 39 40 this.searchField.addEventListener('input', (e) => { 41 let query = this.searchField.value.trim().toLowerCase(); 42 43 if (this.searchTimer) { 44 clearTimeout(this.searchTimer); 45 } 46 47 this.searchTimer = setTimeout(() => this.searchInTimeline(query), 100); 48 }); 49 } 50 51 /** @returns {number} */ 52 53 selectedDaysRange() { 54 return parseInt(this.rangeInput.value, 10); 55 } 56 57 show() { 58 this.pageElement.style.display = 'block'; 59 } 60 61 /** @returns {Promise<void>} */ 62 63 async fetchTimeline() { 64 this.submitButton.value = 'Cancel'; 65 66 let requestedDays = this.selectedDaysRange(); 67 68 this.progressBar.max = requestedDays; 69 this.progressBar.value = 0; 70 this.progressBar.style.display = 'inline'; 71 72 let startTime = new Date().getTime(); 73 this.fetchStartTime = startTime; 74 75 let timeline = await accountAPI.loadHomeTimeline(requestedDays, { 76 onPageLoad: (data) => { 77 if (this.fetchStartTime != startTime) { 78 return { cancel: true }; 79 } 80 81 this.updateProgress(data, startTime); 82 } 83 }); 84 85 if (this.fetchStartTime != startTime) { 86 return; 87 } 88 89 let last = timeline.at(-1); 90 let daysBack; 91 92 if (last) { 93 let lastDate = feedPostTime(last); 94 daysBack = Math.round((startTime - lastDate) / 86400 / 1000); 95 } else { 96 daysBack = 0; 97 } 98 99 this.timelinePosts = timeline.map(x => Post.parseFeedPost(x)); 100 101 this.archiveStatus.innerText = "Timeline archive fetched: " + ((daysBack == 1) ? '1 day' : `${daysBack} days`); 102 this.searchLine.style.display = 'block'; 103 104 this.submitButton.value = 'Fetch timeline'; 105 this.progressBar.style.display = 'none'; 106 this.fetchStartTime = undefined; 107 } 108 109 /** @param {string} query */ 110 111 searchInTimeline(query) { 112 this.results.innerHTML = ''; 113 114 if (query.length == 0) { 115 return; 116 } 117 118 let matching = this.timelinePosts.filter(x => x.lowercaseText.includes(query)); 119 120 for (let post of matching) { 121 let postView = new PostComponent(post, 'feed').buildElement(); 122 this.results.appendChild(postView); 123 } 124 } 125 126 /** @param {json[]} dataPage, @param {number} startTime */ 127 128 updateProgress(dataPage, startTime) { 129 let last = dataPage.at(-1); 130 131 if (!last) { return } 132 133 let lastDate = feedPostTime(last); 134 let daysBack = (startTime - lastDate) / 86400 / 1000; 135 136 this.progressBar.value = daysBack; 137 } 138 139 stopFetch() { 140 this.submitButton.value = 'Fetch timeline'; 141 this.progressBar.style.display = 'none'; 142 this.fetchStartTime = undefined; 143 } 144}