Thread viewer for Bluesky

implemented starting & tracking import from the UI

+21
index.html
··· 193 193 </div> 194 194 </form> 195 195 196 + <div class="lycan-import"> 197 + <form> 198 + <p> 199 + In order to search within your likes and bookmarks, the posts you've liked or saved need to be imported into a database. 200 + This is a one-time process, but it can take several minutes or more, depending on the age of your account. 201 + </p> 202 + <p> 203 + To start the import, press the button below. You can then wait until it finishes, or close this tab and come back a bit later. 204 + After the import is complete, the database will be kept up to date automatically going forward. 205 + </p> 206 + <p> 207 + <input type="submit" value="Start import"> 208 + </p> 209 + </form> 210 + 211 + <div class="import-progress"> 212 + <p class="import-status"></p> 213 + <p><progress></progress> <output></output></p> 214 + </div> 215 + </div> 216 + 196 217 <div class="results"> 197 218 </div> 198 219 </div>
+171 -6
private_search_page.js
··· 3 3 /** @type {number | undefined} */ 4 4 fetchStartTime; 5 5 6 + /** @type {number | undefined} */ 7 + importTimer; 8 + 9 + /** @type {string | undefined} */ 10 + lycanImportStatus; 11 + 6 12 constructor() { 7 13 this.pageElement = $id('private_search_page'); 8 14 ··· 17 23 this.results = $(this.pageElement.querySelector('.results')); 18 24 19 25 this.timelineSearch = $(this.pageElement.querySelector('.timeline-search')); 26 + this.timelineSearchForm = $(this.pageElement.querySelector('.timeline-search form'), HTMLFormElement); 20 27 this.searchCollections = $(this.pageElement.querySelector('.search-collections')); 21 28 29 + this.lycanImportSection = $(this.pageElement.querySelector('.lycan-import')); 30 + this.lycanImportForm = $(this.pageElement.querySelector('.lycan-import form'), HTMLFormElement); 31 + this.importProgress = $(this.pageElement.querySelector('.import-progress')); 32 + this.importProgressBar = $(this.pageElement.querySelector('.import-progress progress'), HTMLProgressElement); 33 + this.importStatusLabel = $(this.pageElement.querySelector('.import-status')); 34 + this.importStatusPosition = $(this.pageElement.querySelector('.import-progress output')); 35 + 36 + this.isCheckingStatus = false; 22 37 this.timelinePosts = []; 23 38 24 39 this.setupEvents(); ··· 33 48 } 34 49 35 50 setupEvents() { 36 - $(this.pageElement.querySelector('form')).addEventListener('submit', (e) => { 51 + this.timelineSearchForm.addEventListener('submit', (e) => { 37 52 e.preventDefault(); 38 53 39 54 if (!this.fetchStartTime) { ··· 62 77 } 63 78 } 64 79 }); 80 + 81 + this.lycanImportForm.addEventListener('submit', (e) => { 82 + e.preventDefault(); 83 + this.startLycanImport(); 84 + }); 65 85 } 66 86 67 87 /** @returns {number} */ ··· 77 97 this.timelineSearch.style.display = 'none'; 78 98 this.searchCollections.style.display = 'block'; 79 99 this.searchLine.style.display = 'block'; 100 + this.lycanImportSection.style.display = 'none'; 101 + this.checkLycanImportStatus(); 80 102 } else { 81 103 this.timelineSearch.style.display = 'block'; 82 104 this.searchCollections.style.display = 'none'; 105 + this.lycanImportSection.style.display = 'none'; 106 + } 107 + } 108 + 109 + /** @returns {Promise<void>} */ 110 + 111 + async checkLycanImportStatus() { 112 + if (this.isCheckingStatus) { 113 + return; 114 + } 115 + 116 + this.isCheckingStatus = true; 117 + 118 + try { 119 + let response = await this.getImportStatus(); 120 + this.showImportStatus(response); 121 + } catch (error) { 122 + this.showImportError(`Couldn't check import status: ${error}`); 123 + } finally { 124 + this.isCheckingStatus = false; 125 + } 126 + } 127 + 128 + /** @returns {Promise<json>} */ 129 + 130 + async getImportStatus() { 131 + if (this.localLycan) { 132 + return await this.localLycan.getRequest('blue.feeds.lycan.getImportStatus', { user: accountAPI.user.did }); 133 + } else { 134 + return await accountAPI.getRequest('blue.feeds.lycan.getImportStatus', null, { 135 + headers: { 'atproto-proxy': 'did:web:lycan.feeds.blue#lycan' } 136 + }); 137 + } 138 + } 139 + 140 + /** @param {json} info */ 141 + 142 + showImportStatus(info) { 143 + console.log(info); 144 + 145 + if (!info.status) { 146 + this.showImportError("Error checking import status"); 147 + return; 148 + } 149 + 150 + this.lycanImportStatus = info.status; 151 + 152 + if (info.status == 'not_started') { 153 + this.lycanImportSection.style.display = 'block'; 154 + this.lycanImportForm.style.display = 'block'; 155 + this.importProgress.style.display = 'none'; 156 + 157 + this.stopImportTimer(); 158 + } else if (info.status == 'in_progress' || info.status == 'scheduled' || info.status == 'requested') { 159 + this.lycanImportSection.style.display = 'block'; 160 + this.lycanImportForm.style.display = 'none'; 161 + this.importProgress.style.display = 'block'; 162 + 163 + this.showImportProgress(info); 164 + this.startImportTimer(); 165 + } else if (info.status == 'finished') { 166 + this.lycanImportForm.style.display = 'none'; 167 + this.importProgress.style.display = 'block'; 168 + 169 + this.showImportProgress({ status: 'finished', progress: 1.0 }); 170 + this.stopImportTimer(); 171 + } else { 172 + this.showImportError("Error checking import status"); 173 + this.stopImportTimer(); 174 + } 175 + } 176 + 177 + /** @param {json} info */ 178 + 179 + showImportProgress(info) { 180 + let progress = Math.max(0, Math.min(info.progress || 0)); 181 + this.importProgressBar.value = progress; 182 + this.importProgressBar.style.display = 'inline'; 183 + 184 + let percent = Math.round(progress * 100); 185 + this.importStatusPosition.innerText = `${percent}%`; 186 + 187 + if (info.progress == 1.0) { 188 + this.importStatusLabel.innerText = `Import complete ✓`; 189 + } else if (info.position) { 190 + let date = new Date(info.position).toLocaleString(window.dateLocale, { day: 'numeric', month: 'short', year: 'numeric' }); 191 + this.importStatusLabel.innerText = `Imported data until: ${date}`; 192 + } else if (info.status == 'requested') { 193 + this.importStatusLabel.innerText = 'Requesting import…'; 194 + } else { 195 + this.importStatusLabel.innerText = 'Import started…'; 196 + } 197 + } 198 + 199 + /** @param {string} message */ 200 + 201 + showImportError(message) { 202 + this.lycanImportSection.style.display = 'block'; 203 + this.lycanImportForm.style.display = 'none'; 204 + this.importProgress.style.display = 'block'; 205 + 206 + this.importStatusLabel.innerText = message; 207 + this.stopImportTimer(); 208 + } 209 + 210 + startImportTimer() { 211 + if (this.importTimer) { 212 + return; 213 + } 214 + 215 + this.importTimer = setInterval(() => { 216 + this.checkLycanImportStatus(); 217 + }, 3000); 218 + } 219 + 220 + stopImportTimer() { 221 + if (this.importTimer) { 222 + clearInterval(this.importTimer); 223 + this.importTimer = undefined; 224 + } 225 + } 226 + 227 + /** @returns {Promise<void>} */ 228 + 229 + async startLycanImport() { 230 + this.showImportStatus({ status: 'requested' }); 231 + 232 + try { 233 + if (this.localLycan) { 234 + await this.localLycan.postRequest('blue.feeds.lycan.startImport', { 235 + user: accountAPI.user.did 236 + }); 237 + } else { 238 + await accountAPI.postRequest('blue.feeds.lycan.startImport', null, { 239 + headers: { 'atproto-proxy': 'did:web:lycan.feeds.blue#lycan' } 240 + }); 241 + } 242 + 243 + this.startImportTimer(); 244 + } catch (err) { 245 + console.error('Failed to start Lycan import', err); 246 + this.showImportError(`Import failed: ${err}`); 83 247 } 84 248 } 85 249 ··· 151 315 /** @param {string} query */ 152 316 153 317 searchInLycan(query) { 154 - this.results.innerHTML = ''; 155 - 156 - if (query.length == 0) { 318 + if (query.length == 0 || this.lycanImportStatus != 'finished') { 157 319 return; 158 320 } 159 321 322 + this.results.innerHTML = ''; 323 + this.lycanImportSection.style.display = 'none'; 324 + 160 325 let collection = this.searchForm.elements['collection'].value; 161 326 162 327 let loading = $tag('p', { text: "..." }); ··· 174 339 let response; 175 340 176 341 if (this.localLycan) { 177 - let params = { collection, query, user: window.accountAPI.user.did }; 342 + let params = { collection, query, user: accountAPI.user.did }; 178 343 if (cursor) params.cursor = cursor; 179 344 180 345 response = await this.localLycan.getRequest('blue.feeds.lycan.searchPosts', params); ··· 197 362 return; 198 363 } 199 364 200 - let records = await window.accountAPI.loadPosts(response.posts); 365 + let records = await accountAPI.loadPosts(response.posts); 201 366 let posts = records.map(x => new Post(x)); 202 367 203 368 if (!firstPageLoaded) {
+25
style.css
··· 1074 1074 vertical-align: middle; 1075 1075 } 1076 1076 1077 + #private_search_page .lycan-import { 1078 + display: none; 1079 + 1080 + margin-top: 30px; 1081 + border-top: 1px solid #ccc; 1082 + padding-top: 5px; 1083 + } 1084 + 1085 + #private_search_page .lycan-import form p { 1086 + line-height: 135%; 1087 + } 1088 + 1089 + #private_search_page .lycan-import .import-progress progress { 1090 + margin-left: 0; 1091 + margin-right: 6px; 1092 + } 1093 + 1094 + #private_search_page .lycan-import .import-progress progress + output { 1095 + font-size: 11pt; 1096 + } 1097 + 1077 1098 #private_search_page .results { 1078 1099 margin-top: 30px; 1079 1100 } ··· 1363 1384 1364 1385 #private_search_page .search-query { 1365 1386 border: 1px solid #666; 1387 + } 1388 + 1389 + #private_search_page .lycan-import { 1390 + border-top-color: #888; 1366 1391 } 1367 1392 1368 1393 #private_search_page .results-end {