Thread viewer for Bluesky

added list scan mode

+38
api.js
··· 366 }); 367 } 368 369 /** @param {string} postURI, @returns {Promise<json>} */ 370 371 async loadPost(postURI) {
··· 366 }); 367 } 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 + 407 /** @param {string} postURI, @returns {Promise<json>} */ 408 409 async loadPost(postURI) {
+8
index.html
··· 97 <input type="radio" name="scan_type" id="scan_type_timeline" value="home" checked> 98 <label for="scan_type_timeline">Home timeline</label> 99 100 <input type="radio" name="scan_type" id="scan_type_users" value="users" disabled> 101 <label for="scan_type_users">Selected users (coming soon)</label> 102 ··· 106 107 <p> 108 Time range: <input type="range" min="1" max="60" value="7"> <label>7 days</label> 109 </p> 110 111 <p>
··· 97 <input type="radio" name="scan_type" id="scan_type_timeline" value="home" checked> 98 <label for="scan_type_timeline">Home timeline</label> 99 100 + <input type="radio" name="scan_type" id="scan_type_list" value="list"> 101 + <label for="scan_type_list">List feed</label> 102 + 103 <input type="radio" name="scan_type" id="scan_type_users" value="users" disabled> 104 <label for="scan_type_users">Selected users (coming soon)</label> 105 ··· 109 110 <p> 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> 117 </p> 118 119 <p>
+64 -12
posting_stats_page.js
··· 37 38 this.pageElement.querySelectorAll('input[type="radio"]').forEach(r => { 39 r.addEventListener('click', (e) => { 40 this.table.style.display = 'none'; 41 }); 42 }); ··· 44 45 show() { 46 this.pageElement.style.display = 'block'; 47 } 48 49 /** @returns {number} */ ··· 52 return parseInt(this.rangeInput.value, 10); 53 } 54 55 /** @param {{ days: number }} args */ 56 57 configurePostingStats(args) { ··· 105 } 106 107 this.updateResultsTable(items, startTime, requestedDays); 108 } else { 109 let items = await accountAPI.loadUserTimeline(accountAPI.user.did, requestedDays, { 110 filter: 'posts_no_replies', ··· 151 * @param {json[]} items 152 * @param {number} startTime 153 * @param {number} requestedDays 154 - * @param {{ showTotal?: boolean, showPercentages?: boolean }} [options] 155 */ 156 157 updateResultsTable(items, startTime, requestedDays, options = {}) { ··· 185 let thead = $(this.table.querySelector('thead')); 186 let headRow = $tag('tr'); 187 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 - ); 195 196 if (options.showPercentages !== false) { 197 headRow.append($tag('th', { text: '% of all' })); ··· 207 tr.append( 208 $tag('td.no', { text: '' }), 209 $tag('td.handle', { text: 'Total:' }), 210 - $tag('td', { text: (total / daysBack).toFixed(1) }), 211 $tag('td', { text: (allNormalPosts / daysBack).toFixed(1) }), 212 - $tag('td', { text: (allReposts / daysBack).toFixed(1) }) 213 ); 214 215 if (options.showPercentages !== false) { ··· 231 html: `<img class="avatar" src="${user.avatar}"> ` + 232 `<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>` 233 }), 234 - $tag('td', { text: ((user.own + user.reposts) / daysBack).toFixed(1) }), 235 $tag('td', { text: user.own > 0 ? (user.own / daysBack).toFixed(1) : '–' }), 236 - $tag('td', { text: user.reposts > 0 ? (user.reposts / daysBack).toFixed(1) : '–' }) 237 ); 238 239 if (options.showPercentages !== false) {
··· 37 38 this.pageElement.querySelectorAll('input[type="radio"]').forEach(r => { 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 + 44 this.table.style.display = 'none'; 45 }); 46 }); ··· 48 49 show() { 50 this.pageElement.style.display = 'block'; 51 + this.fetchLists(); 52 } 53 54 /** @returns {number} */ ··· 57 return parseInt(this.rangeInput.value, 10); 58 } 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 + 79 /** @param {{ days: number }} args */ 80 81 configurePostingStats(args) { ··· 129 } 130 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 }); 142 } else { 143 let items = await accountAPI.loadUserTimeline(accountAPI.user.did, requestedDays, { 144 filter: 'posts_no_replies', ··· 185 * @param {json[]} items 186 * @param {number} startTime 187 * @param {number} requestedDays 188 + * @param {{ showTotal?: boolean, showPercentages?: boolean, showReposts?: boolean }} [options] 189 */ 190 191 updateResultsTable(items, startTime, requestedDays, options = {}) { ··· 219 let thead = $(this.table.querySelector('thead')); 220 let headRow = $tag('tr'); 221 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 + } 237 238 if (options.showPercentages !== false) { 239 headRow.append($tag('th', { text: '% of all' })); ··· 249 tr.append( 250 $tag('td.no', { text: '' }), 251 $tag('td.handle', { text: 'Total:' }), 252 + 253 + (options.showReposts !== false) ? 254 + $tag('td', { text: (total / daysBack).toFixed(1) }) : '', 255 + 256 $tag('td', { text: (allNormalPosts / daysBack).toFixed(1) }), 257 + 258 + (options.showReposts !== false) ? 259 + $tag('td', { text: (allReposts / daysBack).toFixed(1) }) : '' 260 ); 261 262 if (options.showPercentages !== false) { ··· 278 html: `<img class="avatar" src="${user.avatar}"> ` + 279 `<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>` 280 }), 281 + 282 + (options.showReposts !== false) ? 283 + $tag('td', { text: ((user.own + user.reposts) / daysBack).toFixed(1) }) : '', 284 + 285 $tag('td', { text: user.own > 0 ? (user.own / daysBack).toFixed(1) : '–' }), 286 + 287 + (options.showReposts !== false) ? 288 + $tag('td', { text: user.reposts > 0 ? (user.reposts / daysBack).toFixed(1) : '–' }) : '' 289 ); 290 291 if (options.showPercentages !== false) {
+11 -2
style.css
··· 742 margin-left: 5px; 743 } 744 745 - #posting_stats_page label { 746 user-select: none; 747 -webkit-user-select: none; 748 } 749 750 - #posting_stats_page input:disabled + label { 751 color: #999; 752 } 753 ··· 762 padding: 5px 10px; 763 } 764 765 #posting_stats_page progress { 766 width: 300px; 767 margin-left: 10px; 768 vertical-align: middle; 769 display: none; 770 } 771
··· 742 margin-left: 5px; 743 } 744 745 + #posting_stats_page input[type="radio"] + label { 746 user-select: none; 747 -webkit-user-select: none; 748 } 749 750 + #posting_stats_page input[type="radio"]:disabled + label { 751 color: #999; 752 } 753 ··· 762 padding: 5px 10px; 763 } 764 765 + #posting_stats_page select { 766 + font-size: 12pt; 767 + margin-left: 5px; 768 + } 769 + 770 #posting_stats_page progress { 771 width: 300px; 772 margin-left: 10px; 773 vertical-align: middle; 774 + display: none; 775 + } 776 + 777 + #posting_stats_page .list-choice { 778 display: none; 779 } 780