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