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.searchForm = $(this.pageElement.querySelector('.search-form'), HTMLFormElement); 17 this.results = $(this.pageElement.querySelector('.results')); 18 19 this.timelinePosts = []; 20 21 this.setupEvents(); 22 23 let params = new URLSearchParams(location.search); 24 this.mode = params.get('mode'); 25 this.lycanMode = params.get('lycan'); 26 27 if (this.lycanMode == 'local') { 28 this.lycan = new BlueskyAPI('http://localhost:3000', false); 29 } 30 } 31 32 setupEvents() { 33 $(this.pageElement.querySelector('form')).addEventListener('submit', (e) => { 34 e.preventDefault(); 35 36 if (!this.fetchStartTime) { 37 this.fetchTimeline(); 38 } else { 39 this.stopFetch(); 40 } 41 }); 42 43 this.rangeInput.addEventListener('input', (e) => { 44 let days = parseInt(this.rangeInput.value, 10); 45 let label = $(this.pageElement.querySelector('input[type=range] + label')); 46 label.innerText = (days == 1) ? '1 day' : `${days} days`; 47 }); 48 49 this.searchField.addEventListener('keydown', (e) => { 50 if (e.key == 'Enter') { 51 e.preventDefault(); 52 53 let query = this.searchField.value.trim().toLowerCase(); 54 55 if (this.mode == 'likes') { 56 this.searchInLycan(query); 57 } else { 58 this.searchInTimeline(query); 59 } 60 } 61 }); 62 } 63 64 /** @returns {number} */ 65 66 selectedDaysRange() { 67 return parseInt(this.rangeInput.value, 10); 68 } 69 70 show() { 71 this.pageElement.style.display = 'block'; 72 73 if (this.mode == 'likes') { 74 this.pageElement.querySelector('.timeline-search').style.display = 'none'; 75 this.pageElement.querySelector('.search-collections').style.display = 'block'; 76 this.searchLine.style.display = 'block'; 77 } else { 78 this.pageElement.querySelector('.timeline-search').style.display = 'block'; 79 this.pageElement.querySelector('.search-collections').style.display = 'none'; 80 } 81 } 82 83 /** @returns {Promise<void>} */ 84 85 async fetchTimeline() { 86 this.submitButton.value = 'Cancel'; 87 88 let requestedDays = this.selectedDaysRange(); 89 90 this.progressBar.max = requestedDays; 91 this.progressBar.value = 0; 92 this.progressBar.style.display = 'inline'; 93 94 let startTime = new Date().getTime(); 95 this.fetchStartTime = startTime; 96 97 let timeline = await accountAPI.loadHomeTimeline(requestedDays, { 98 onPageLoad: (data) => { 99 if (this.fetchStartTime != startTime) { 100 return { cancel: true }; 101 } 102 103 this.updateProgress(data, startTime); 104 } 105 }); 106 107 if (this.fetchStartTime != startTime) { 108 return; 109 } 110 111 let last = timeline.at(-1); 112 let daysBack; 113 114 if (last) { 115 let lastDate = feedPostTime(last); 116 daysBack = Math.round((startTime - lastDate) / 86400 / 1000); 117 } else { 118 daysBack = 0; 119 } 120 121 this.timelinePosts = timeline.map(x => Post.parseFeedPost(x)); 122 123 this.archiveStatus.innerText = "Timeline archive fetched: " + ((daysBack == 1) ? '1 day' : `${daysBack} days`); 124 this.searchLine.style.display = 'block'; 125 126 this.submitButton.value = 'Fetch timeline'; 127 this.progressBar.style.display = 'none'; 128 this.fetchStartTime = undefined; 129 } 130 131 /** @param {string} query */ 132 133 searchInTimeline(query) { 134 this.results.innerHTML = ''; 135 136 if (query.length == 0) { 137 return; 138 } 139 140 let matching = this.timelinePosts.filter(x => x.lowercaseText.includes(query)); 141 142 for (let post of matching) { 143 let postView = new PostComponent(post, 'feed').buildElement(); 144 this.results.appendChild(postView); 145 } 146 } 147 148 /** @param {string} query */ 149 150 searchInLycan(query) { 151 this.results.innerHTML = ''; 152 153 if (query.length == 0) { 154 return; 155 } 156 157 let collection = this.searchForm.elements['collection'].value; 158 159 let loading = $tag('p', { text: "..." }); 160 this.results.append(loading); 161 162 let isLoading = false; 163 let firstPageLoaded = false; 164 let cursor; 165 let finished = false; 166 167 Paginator.loadInPages(async () => { 168 if (isLoading || finished) { return; } 169 isLoading = true; 170 171 let response; 172 173 if (this.lycanMode == 'local') { 174 let params = { collection, query, user: window.accountAPI.user.did }; 175 if (cursor) params.cursor = cursor; 176 177 response = await this.lycan.getRequest('blue.feeds.lycan.searchPosts', params); 178 } else { 179 let params = { collection, query }; 180 if (cursor) params.cursor = cursor; 181 182 response = await accountAPI.getRequest('blue.feeds.lycan.searchPosts', params, { 183 headers: { 'atproto-proxy': 'did:web:lycan.feeds.blue#lycan' } 184 }); 185 } 186 187 if (response.posts.length == 0) { 188 let p = $tag('p.results-end', { text: firstPageLoaded ? "No more results." : "No results." }); 189 loading.remove(); 190 this.results.append(p); 191 192 isLoading = false; 193 finished = true; 194 return; 195 } 196 197 let records = await window.accountAPI.loadPosts(response.posts); 198 let posts = records.map(x => new Post(x)); 199 200 if (!firstPageLoaded) { 201 loading.remove(); 202 firstPageLoaded = true; 203 } 204 205 for (let post of posts) { 206 let postView = new PostComponent(post, 'feed').buildElement(); 207 this.results.appendChild(postView); 208 } 209 210 isLoading = false; 211 cursor = response.cursor; 212 213 if (!cursor) { 214 finished = true; 215 this.results.append("No more results."); 216 } 217 }); 218 } 219 220 /** @param {json[]} dataPage, @param {number} startTime */ 221 222 updateProgress(dataPage, startTime) { 223 let last = dataPage.at(-1); 224 225 if (!last) { return } 226 227 let lastDate = feedPostTime(last); 228 let daysBack = (startTime - lastDate) / 86400 / 1000; 229 230 this.progressBar.value = daysBack; 231 } 232 233 stopFetch() { 234 this.submitButton.value = 'Fetch timeline'; 235 this.progressBar.style.display = 'none'; 236 this.fetchStartTime = undefined; 237 } 238}