Thread viewer for Bluesky

added beta version of timeline search

+24
index.html
··· 155 155 </table> 156 156 </div> 157 157 158 + <div id="private_search_page"> 159 + <h2>Archive search *Beta*</h2> 160 + 161 + <form> 162 + <p> 163 + Fetch timeline posts: <input type="range" min="1" max="60" value="7"> <label>7 days</label> 164 + </p> 165 + 166 + <p> 167 + <input type="submit" value="Fetch timeline"> <progress></progress> 168 + </p> 169 + </form> 170 + 171 + <p class="archive-status"></p> 172 + 173 + <hr> 174 + 175 + <p class="search">Search: <input type="text" class="search-query"></p> 176 + 177 + <div class="results"> 178 + </div> 179 + </div> 180 + 158 181 <script src="lib/purify.min.js"></script> 159 182 <script src="minisky.js"></script> 160 183 <script src="api.js"></script> ··· 166 189 <script src="posting_stats_page.js"></script> 167 190 <script src="like_stats_page.js"></script> 168 191 <script src="notifications_page.js"></script> 192 + <script src="private_search_page.js"></script> 169 193 <script src="embed_component.js"></script> 170 194 <script src="post_component.js"></script> 171 195 <script src="skythread.js"></script>
+9
models.js
··· 342 342 return this.record.text; 343 343 } 344 344 345 + /** @returns {string} */ 346 + get lowercaseText() { 347 + if (!this._lowercaseText) { 348 + this._lowercaseText = this.record.text.toLowerCase(); 349 + } 350 + 351 + return this._lowercaseText; 352 + } 353 + 345 354 /** @returns {json} */ 346 355 get facets() { 347 356 return this.record.facets;
+143
private_search_page.js
··· 1 + class 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.loadTimeline(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 lastTimestamp = last.reason ? last.reason.indexedAt : last.post.record.createdAt; 94 + let lastDate = Date.parse(lastTimestamp); 95 + daysBack = Math.round((startTime - lastDate) / 86400 / 1000); 96 + } else { 97 + daysBack = 0; 98 + } 99 + 100 + this.timelinePosts = timeline.map(x => Post.parseFeedPost(x)); 101 + 102 + this.archiveStatus.innerText = "Timeline archive fetched: " + ((daysBack == 1) ? '1 day' : `${daysBack} days`); 103 + this.searchLine.style.display = 'block'; 104 + 105 + this.submitButton.value = 'Fetch timeline'; 106 + this.progressBar.style.display = 'none'; 107 + this.fetchStartTime = undefined; 108 + } 109 + 110 + searchInTimeline(query) { 111 + this.results.innerHTML = ''; 112 + 113 + if (query.length == 0) { 114 + return; 115 + } 116 + 117 + let matching = this.timelinePosts.filter(x => x.lowercaseText.includes(query)); 118 + 119 + for (let post of matching) { 120 + let postView = new PostComponent(post, 'feed').buildElement(); 121 + this.results.appendChild(postView); 122 + } 123 + } 124 + 125 + /** @param {json[]} dataPage, @param {number} startTime */ 126 + 127 + updateProgress(dataPage, startTime) { 128 + if (dataPage.length == 0) { return } 129 + 130 + let last = dataPage[dataPage.length - 1]; 131 + let lastTimestamp = last.reason ? last.reason.indexedAt : last.post.record.createdAt; 132 + let lastDate = Date.parse(lastTimestamp); 133 + 134 + let daysBack = (startTime - lastDate) / 86400 / 1000; 135 + this.progressBar.value = daysBack; 136 + } 137 + 138 + stopFetch() { 139 + this.submitButton.value = 'Fetch timeline'; 140 + this.progressBar.style.display = 'none'; 141 + this.fetchStartTime = undefined; 142 + } 143 + }
+3
skythread.js
··· 12 12 window.postingStatsPage = new PostingStatsPage(); 13 13 window.likeStatsPage = new LikeStatsPage(); 14 14 window.notificationsPage = new NotificationsPage(); 15 + window.privateSearchPage = new PrivateSearchPage(); 15 16 16 17 $(document.querySelector('#search form')).addEventListener('submit', (e) => { 17 18 e.preventDefault(); ··· 316 317 window.postingStatsPage.show(); 317 318 } else if (page == 'like_stats') { 318 319 window.likeStatsPage.show(); 320 + } else if (page == 'search') { 321 + window.privateSearchPage.show(); 319 322 } 320 323 } 321 324
+38
style.css
··· 898 898 padding: 2px; 899 899 } 900 900 901 + #private_search_page { 902 + display: none; 903 + } 904 + 905 + #private_search_page input[type="range"] { 906 + width: 250px; 907 + vertical-align: middle; 908 + } 909 + 910 + #private_search_page input[type="submit"] { 911 + font-size: 12pt; 912 + margin: 5px 0px; 913 + padding: 5px 10px; 914 + } 915 + 916 + #private_search_page progress { 917 + width: 300px; 918 + margin-left: 10px; 919 + vertical-align: middle; 920 + display: none; 921 + } 922 + 923 + #private_search_page .search { 924 + display: none; 925 + } 926 + 927 + #private_search_page .search-query { 928 + font-size: 12pt; 929 + border: 1px solid #ccc; 930 + border-radius: 6px; 931 + padding: 5px 6px; 932 + margin-left: 8px; 933 + } 934 + 901 935 @media (prefers-color-scheme: dark) { 902 936 body { 903 937 background-color: rgb(39, 39, 37); ··· 1088 1122 1089 1123 #like_stats_page .scan-result th { 1090 1124 background-color: hsl(207, 90%, 25%); 1125 + } 1126 + 1127 + #private_search_page .search-query { 1128 + border: 1px solid #666; 1091 1129 } 1092 1130 }
+1
types.d.ts
··· 18 18 declare var postingStatsPage: PostingStatsPage; 19 19 declare var likeStatsPage: LikeStatsPage; 20 20 declare var notificationsPage: NotificationsPage; 21 + declare var privateSearchPage: PrivateSearchPage; 21 22 22 23 type json = Record<string, any>; 23 24