Thread viewer for Bluesky

added list scan mode

+38
api.js
··· 366 366 }); 367 367 } 368 368 369 + /** @returns {Promise<json[]>} */ 370 + 371 + async loadUserLists() { 372 + let lists = await this.fetchAll('app.bsky.graph.getLists', { 373 + params: { 374 + actor: this.user.did, 375 + limit: 100 376 + }, 377 + field: 'lists' 378 + }); 379 + 380 + return lists.filter(x => x.purpose == "app.bsky.graph.defs#curatelist"); 381 + } 382 + 383 + /** 384 + * @param {string} list 385 + * @param {number} days 386 + * @param {{ onPageLoad?: FetchAllOnPageLoad }} [options] 387 + * @returns {Promise<json[]>} 388 + */ 389 + 390 + async loadListTimeline(list, days, options = {}) { 391 + let now = new Date(); 392 + let timeLimit = now.getTime() - days * 86400 * 1000; 393 + 394 + return await this.fetchAll('app.bsky.feed.getListFeed', { 395 + params: { 396 + list: list, 397 + limit: 100 398 + }, 399 + field: 'feed', 400 + breakWhen: (x) => { 401 + return Date.parse(x.post.record.createdAt) < timeLimit; 402 + }, 403 + onPageLoad: options.onPageLoad 404 + }); 405 + } 406 + 369 407 /** @param {string} postURI, @returns {Promise<json>} */ 370 408 371 409 async loadPost(postURI) {
+8
index.html
··· 97 97 <input type="radio" name="scan_type" id="scan_type_timeline" value="home" checked> 98 98 <label for="scan_type_timeline">Home timeline</label> 99 99 100 + <input type="radio" name="scan_type" id="scan_type_list" value="list"> 101 + <label for="scan_type_list">List feed</label> 102 + 100 103 <input type="radio" name="scan_type" id="scan_type_users" value="users" disabled> 101 104 <label for="scan_type_users">Selected users (coming soon)</label> 102 105 ··· 106 109 107 110 <p> 108 111 Time range: <input type="range" min="1" max="60" value="7"> <label>7 days</label> 112 + </p> 113 + 114 + <p class="list-choice"> 115 + <label>Select list:</label> 116 + <select name="scan_list"></select> 109 117 </p> 110 118 111 119 <p>
+64 -12
posting_stats_page.js
··· 37 37 38 38 this.pageElement.querySelectorAll('input[type="radio"]').forEach(r => { 39 39 r.addEventListener('click', (e) => { 40 + let value = $(r, HTMLInputElement).value; 41 + 42 + $(this.pageElement.querySelector('.list-choice')).style.display = (value == 'list') ? 'block' : 'none'; 43 + 40 44 this.table.style.display = 'none'; 41 45 }); 42 46 }); ··· 44 48 45 49 show() { 46 50 this.pageElement.style.display = 'block'; 51 + this.fetchLists(); 47 52 } 48 53 49 54 /** @returns {number} */ ··· 52 57 return parseInt(this.rangeInput.value, 10); 53 58 } 54 59 60 + /** @returns {Promise<void>} */ 61 + 62 + async fetchLists() { 63 + let select = $(this.pageElement.querySelector('.list-choice select')); 64 + let lists = await accountAPI.loadUserLists(); 65 + 66 + let sorted = lists.sort((a, b) => { 67 + let aName = a.name.toLocaleLowerCase(); 68 + let bName = b.name.toLocaleLowerCase(); 69 + 70 + return aName.localeCompare(bName); 71 + }); 72 + 73 + for (let list of lists) { 74 + let opt = $tag('option', { value: list.uri, text: list.name + ' ' }); 75 + select.append(opt); 76 + } 77 + } 78 + 55 79 /** @param {{ days: number }} args */ 56 80 57 81 configurePostingStats(args) { ··· 105 129 } 106 130 107 131 this.updateResultsTable(items, startTime, requestedDays); 132 + } else if (scanType == 'list') { 133 + let select = $(this.pageElement.querySelector('.list-choice select'), HTMLSelectElement); 134 + let list = select.value; 135 + let items = await accountAPI.loadListTimeline(list, requestedDays, { onPageLoad }); 136 + 137 + if (this.scanStartTime != startTime) { 138 + return; 139 + } 140 + 141 + this.updateResultsTable(items, startTime, requestedDays, { showReposts: false }); 108 142 } else { 109 143 let items = await accountAPI.loadUserTimeline(accountAPI.user.did, requestedDays, { 110 144 filter: 'posts_no_replies', ··· 151 185 * @param {json[]} items 152 186 * @param {number} startTime 153 187 * @param {number} requestedDays 154 - * @param {{ showTotal?: boolean, showPercentages?: boolean }} [options] 188 + * @param {{ showTotal?: boolean, showPercentages?: boolean, showReposts?: boolean }} [options] 155 189 */ 156 190 157 191 updateResultsTable(items, startTime, requestedDays, options = {}) { ··· 185 219 let thead = $(this.table.querySelector('thead')); 186 220 let headRow = $tag('tr'); 187 221 188 - headRow.append( 189 - $tag('th', { text: '#' }), 190 - $tag('th', { text: 'Handle' }), 191 - $tag('th', { text: 'All posts /d' }), 192 - $tag('th', { text: 'Own posts /d' }), 193 - $tag('th', { text: 'Reposts /d' }) 194 - ); 222 + if (options.showReposts !== false) { 223 + headRow.append( 224 + $tag('th', { text: '#' }), 225 + $tag('th', { text: 'Handle' }), 226 + $tag('th', { text: 'All posts /d' }), 227 + $tag('th', { text: 'Own posts /d' }), 228 + $tag('th', { text: 'Reposts /d' }) 229 + ); 230 + } else { 231 + headRow.append( 232 + $tag('th', { text: '#' }), 233 + $tag('th', { text: 'Handle' }), 234 + $tag('th', { text: 'Posts /d' }), 235 + ); 236 + } 195 237 196 238 if (options.showPercentages !== false) { 197 239 headRow.append($tag('th', { text: '% of all' })); ··· 207 249 tr.append( 208 250 $tag('td.no', { text: '' }), 209 251 $tag('td.handle', { text: 'Total:' }), 210 - $tag('td', { text: (total / daysBack).toFixed(1) }), 252 + 253 + (options.showReposts !== false) ? 254 + $tag('td', { text: (total / daysBack).toFixed(1) }) : '', 255 + 211 256 $tag('td', { text: (allNormalPosts / daysBack).toFixed(1) }), 212 - $tag('td', { text: (allReposts / daysBack).toFixed(1) }) 257 + 258 + (options.showReposts !== false) ? 259 + $tag('td', { text: (allReposts / daysBack).toFixed(1) }) : '' 213 260 ); 214 261 215 262 if (options.showPercentages !== false) { ··· 231 278 html: `<img class="avatar" src="${user.avatar}"> ` + 232 279 `<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>` 233 280 }), 234 - $tag('td', { text: ((user.own + user.reposts) / daysBack).toFixed(1) }), 281 + 282 + (options.showReposts !== false) ? 283 + $tag('td', { text: ((user.own + user.reposts) / daysBack).toFixed(1) }) : '', 284 + 235 285 $tag('td', { text: user.own > 0 ? (user.own / daysBack).toFixed(1) : '–' }), 236 - $tag('td', { text: user.reposts > 0 ? (user.reposts / daysBack).toFixed(1) : '–' }) 286 + 287 + (options.showReposts !== false) ? 288 + $tag('td', { text: user.reposts > 0 ? (user.reposts / daysBack).toFixed(1) : '–' }) : '' 237 289 ); 238 290 239 291 if (options.showPercentages !== false) {
+11 -2
style.css
··· 742 742 margin-left: 5px; 743 743 } 744 744 745 - #posting_stats_page label { 745 + #posting_stats_page input[type="radio"] + label { 746 746 user-select: none; 747 747 -webkit-user-select: none; 748 748 } 749 749 750 - #posting_stats_page input:disabled + label { 750 + #posting_stats_page input[type="radio"]:disabled + label { 751 751 color: #999; 752 752 } 753 753 ··· 762 762 padding: 5px 10px; 763 763 } 764 764 765 + #posting_stats_page select { 766 + font-size: 12pt; 767 + margin-left: 5px; 768 + } 769 + 765 770 #posting_stats_page progress { 766 771 width: 300px; 767 772 margin-left: 10px; 768 773 vertical-align: middle; 774 + display: none; 775 + } 776 + 777 + #posting_stats_page .list-choice { 769 778 display: none; 770 779 } 771 780