Thread viewer for Bluesky

extracted posting page

+1
index.html
··· 134 134 <script src="rich_text_lite.js"></script> 135 135 <script src="models.js"></script> 136 136 <script src="thread_page.js"></script> 137 + <script src="posting_stats_page.js"></script> 137 138 <script src="embed_component.js"></script> 138 139 <script src="post_component.js"></script> 139 140 <script src="skythread.js"></script>
+182
posting_stats_page.js
··· 1 + /** 2 + * Manages the Posting Stats page. 3 + */ 4 + 5 + class PostingStatsPage { 6 + 7 + /** @type {number | undefined} */ 8 + scanStartTime; 9 + 10 + constructor() { 11 + this.pageElement = $id('posting_stats_page'); 12 + 13 + this.setupEvents(); 14 + } 15 + 16 + setupEvents() { 17 + $(this.pageElement.querySelector('form')).addEventListener('submit', (e) => { 18 + e.preventDefault(); 19 + 20 + if (!this.scanStartTime) { 21 + this.scanPostingStats(); 22 + } else { 23 + this.stopScan(); 24 + } 25 + }); 26 + 27 + $(this.pageElement.querySelector('input[type="range"]')).addEventListener('input', (e) => { 28 + let range = $(e.target, HTMLInputElement); 29 + let days = parseInt(range.value, 10); 30 + this.configurePostingStats({ days }); 31 + }); 32 + 33 + } 34 + 35 + show() { 36 + this.pageElement.style.display = 'block'; 37 + } 38 + 39 + /** @param {{ days: number }} args */ 40 + 41 + configurePostingStats(args) { 42 + if (args.days) { 43 + let label = $(this.pageElement.querySelector('input[type=range] + label')); 44 + label.innerText = (args.days == 1) ? '1 day' : `${args.days} days`; 45 + } 46 + } 47 + 48 + scanPostingStats() { 49 + let submit = $(this.pageElement.querySelector('input[type=submit]'), HTMLInputElement); 50 + submit.value = 'Cancel'; 51 + 52 + let range = $(this.pageElement.querySelector('input[type=range]'), HTMLInputElement); 53 + let days = parseInt(range.value, 10); 54 + 55 + let progressBar = $(this.pageElement.querySelector('input[type=submit] + progress'), HTMLProgressElement); 56 + progressBar.max = days; 57 + progressBar.value = 0; 58 + progressBar.style.display = 'inline'; 59 + 60 + let table = $(this.pageElement.querySelector('table.scan-result')); 61 + table.style.display = 'none'; 62 + 63 + let tbody = $(table.querySelector('tbody')); 64 + tbody.innerHTML = ''; 65 + 66 + let now = new Date().getTime(); 67 + this.scanStartTime = now; 68 + 69 + let minTime = now; 70 + let daysBack = 0; 71 + 72 + let scanInfo = $(this.pageElement.querySelector('.scan-info')); 73 + scanInfo.style.display = 'none'; 74 + 75 + accountAPI.loadTimeline(days, { 76 + onPageLoad: (data) => { 77 + if (this.scanStartTime != now) { 78 + return { cancel: true }; 79 + } 80 + 81 + for (let item of data) { 82 + let timestamp = item.reason ? item.reason.indexedAt : item.post.record.createdAt; 83 + let date = Date.parse(timestamp); 84 + minTime = Math.min(minTime, date); 85 + } 86 + 87 + daysBack = (now - minTime) / 86400 / 1000; 88 + progressBar.value = daysBack; 89 + } 90 + }).then(items => { 91 + if (this.scanStartTime != now) { 92 + return; 93 + } 94 + 95 + let users = {}; 96 + let total = 0; 97 + let allReposts = 0; 98 + let allNormalPosts = 0; 99 + 100 + for (let item of items) { 101 + if (item.reply) { continue; } 102 + 103 + let user = item.reason ? item.reason.by : item.post.author; 104 + let handle = user.handle; 105 + users[handle] = users[handle] ?? { handle: handle, own: 0, reposts: 0, avatar: user.avatar }; 106 + total += 1; 107 + 108 + if (item.reason) { 109 + users[handle].reposts += 1; 110 + allReposts += 1; 111 + } else { 112 + users[handle].own += 1; 113 + allNormalPosts += 1; 114 + } 115 + } 116 + 117 + let tr = $tag('tr.total'); 118 + 119 + tr.append( 120 + $tag('td.no', { text: '' }), 121 + $tag('td.handle', { text: 'Total:' }), 122 + $tag('td', { text: (total / daysBack).toFixed(1) }), 123 + $tag('td', { text: (allNormalPosts / daysBack).toFixed(1) }), 124 + $tag('td', { text: (allReposts / daysBack).toFixed(1) }), 125 + $tag('td.percent', { text: '' }) 126 + ); 127 + 128 + tbody.append(tr); 129 + 130 + let sorted = Object.values(users).sort((a, b) => { 131 + let asum = a.own + a.reposts; 132 + let bsum = b.own + b.reposts; 133 + 134 + if (asum < bsum) { 135 + return 1; 136 + } else if (asum > bsum) { 137 + return -1; 138 + } else { 139 + return 0; 140 + } 141 + }); 142 + 143 + for (let i = 0; i < sorted.length; i++) { 144 + let user = sorted[i]; 145 + let tr = $tag('tr'); 146 + 147 + tr.append( 148 + $tag('td.no', { text: i + 1 }), 149 + $tag('td.handle', { 150 + html: `<img class="avatar" src="${user.avatar}"> ` + 151 + `<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>` 152 + }), 153 + $tag('td', { text: ((user.own + user.reposts) / daysBack).toFixed(1) }), 154 + $tag('td', { text: user.own > 0 ? (user.own / daysBack).toFixed(1) : '–' }), 155 + $tag('td', { text: user.reposts > 0 ? (user.reposts / daysBack).toFixed(1) : '–' }), 156 + $tag('td.percent', { text: ((user.own + user.reposts) * 100 / total).toFixed(1) + '%' }) 157 + ); 158 + 159 + tbody.append(tr); 160 + } 161 + 162 + if (Math.ceil(daysBack) < days) { 163 + scanInfo.innerText = `🕓 Showing data from ${Math.round(daysBack)} days (your timeline only goes that far):`; 164 + scanInfo.style.display = 'block'; 165 + } 166 + 167 + table.style.display = 'table'; 168 + submit.value = 'Start scan'; 169 + progressBar.style.display = 'none'; 170 + this.scanStartTime = undefined; 171 + }); 172 + } 173 + 174 + stopScan() { 175 + let submit = $(this.pageElement.querySelector('input[type=submit]'), HTMLInputElement); 176 + submit.value = 'Start scan'; 177 + this.scanStartTime = undefined; 178 + 179 + let progressBar = $(this.pageElement.querySelector('input[type=submit] + progress'), HTMLProgressElement); 180 + progressBar.style.display = 'none'; 181 + } 182 + }
+2 -163
skythread.js
··· 7 7 8 8 window.loginDialog = $(document.querySelector('#login')); 9 9 window.accountMenu = $(document.querySelector('#account_menu')); 10 - window.postingStatsPage = $id('posting_stats_page'); 11 10 12 11 window.avatarPreloader = buildAvatarPreloader(); 13 12 14 13 window.threadPage = new ThreadPage(); 14 + window.postingStatsPage = new PostingStatsPage(); 15 15 16 16 html.addEventListener('click', (e) => { 17 17 $id('account_menu').style.visibility = 'hidden'; ··· 126 126 $(accountMenu.querySelector('a[data-action=logout]')).addEventListener('click', (e) => { 127 127 e.preventDefault(); 128 128 logOut(); 129 - }); 130 - 131 - $(postingStatsPage.querySelector('form')).addEventListener('submit', (e) => { 132 - e.preventDefault(); 133 - 134 - if (!window.scanStartTime) { 135 - scanPostingStats(); 136 - } else { 137 - stopScan(); 138 - } 139 - }); 140 - 141 - $(postingStatsPage.querySelector('input[type="range"]')).addEventListener('input', (e) => { 142 - let range = $(e.target, HTMLInputElement); 143 - configurePostingStats({ days: range.value }); 144 129 }); 145 130 146 131 window.appView = new BlueskyAPI('api.bsky.app', false); ··· 445 430 showLoader(); 446 431 showNotificationsPage(); 447 432 } else if (page == 'posting_stats') { 448 - showPostingStatsPage(); 433 + window.postingStatsPage.show(); 449 434 } 450 - } 451 - 452 - function showPostingStatsPage() { 453 - $id('posting_stats_page').style.display = 'block'; 454 - } 455 - 456 - function configurePostingStats(args) { 457 - if (args.days) { 458 - let label = $(postingStatsPage.querySelector('input[type=range] + label')); 459 - label.innerText = (args.days == 1) ? '1 day' : `${args.days} days`; 460 - } 461 - } 462 - 463 - function scanPostingStats() { 464 - let submit = $(postingStatsPage.querySelector('input[type=submit]'), HTMLInputElement); 465 - submit.value = 'Cancel'; 466 - 467 - let range = $(postingStatsPage.querySelector('input[type=range]'), HTMLInputElement); 468 - let days = parseInt(range.value, 10); 469 - 470 - let progressBar = $(postingStatsPage.querySelector('input[type=submit] + progress'), HTMLProgressElement); 471 - progressBar.max = days; 472 - progressBar.value = 0; 473 - progressBar.style.display = 'inline'; 474 - 475 - let table = $(postingStatsPage.querySelector('table.scan-result')); 476 - table.style.display = 'none'; 477 - 478 - let tbody = $(table.querySelector('tbody')); 479 - tbody.innerHTML = ''; 480 - 481 - let now = new Date().getTime(); 482 - window.scanStartTime = now; 483 - 484 - let minTime = now; 485 - let daysBack = 0; 486 - 487 - let scanInfo = $(postingStatsPage.querySelector('.scan-info')); 488 - scanInfo.style.display = 'none'; 489 - 490 - accountAPI.loadTimeline(days, { 491 - onPageLoad: (data) => { 492 - if (window.scanStartTime != now) { 493 - return { cancel: true }; 494 - } 495 - 496 - for (let item of data) { 497 - let timestamp = item.reason ? item.reason.indexedAt : item.post.record.createdAt; 498 - let date = Date.parse(timestamp); 499 - minTime = Math.min(minTime, date); 500 - } 501 - 502 - daysBack = (now - minTime) / 86400 / 1000; 503 - progressBar.value = daysBack; 504 - } 505 - }).then(items => { 506 - if (window.scanStartTime != now) { 507 - return; 508 - } 509 - 510 - let users = {}; 511 - let total = 0; 512 - let allReposts = 0; 513 - let allNormalPosts = 0; 514 - 515 - for (let item of items) { 516 - if (item.reply) { continue; } 517 - 518 - let user = item.reason ? item.reason.by : item.post.author; 519 - let handle = user.handle; 520 - users[handle] = users[handle] ?? { handle: handle, own: 0, reposts: 0, avatar: user.avatar }; 521 - total += 1; 522 - 523 - if (item.reason) { 524 - users[handle].reposts += 1; 525 - allReposts += 1; 526 - } else { 527 - users[handle].own += 1; 528 - allNormalPosts += 1; 529 - } 530 - } 531 - 532 - let tr = $tag('tr.total'); 533 - 534 - tr.append( 535 - $tag('td.no', { text: '' }), 536 - $tag('td.handle', { text: 'Total:' }), 537 - $tag('td', { text: (total / daysBack).toFixed(1) }), 538 - $tag('td', { text: (allNormalPosts / daysBack).toFixed(1) }), 539 - $tag('td', { text: (allReposts / daysBack).toFixed(1) }), 540 - $tag('td.percent', { text: '' }) 541 - ); 542 - 543 - tbody.append(tr); 544 - 545 - let sorted = Object.values(users).sort((a, b) => { 546 - let asum = a.own + a.reposts; 547 - let bsum = b.own + b.reposts; 548 - 549 - if (asum < bsum) { 550 - return 1; 551 - } else if (asum > bsum) { 552 - return -1; 553 - } else { 554 - return 0; 555 - } 556 - }); 557 - 558 - for (let i = 0; i < sorted.length; i++) { 559 - let user = sorted[i]; 560 - let tr = $tag('tr'); 561 - 562 - tr.append( 563 - $tag('td.no', { text: i + 1 }), 564 - $tag('td.handle', { 565 - html: `<img class="avatar" src="${user.avatar}"> ` + 566 - `<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>` 567 - }), 568 - $tag('td', { text: ((user.own + user.reposts) / daysBack).toFixed(1) }), 569 - $tag('td', { text: user.own > 0 ? (user.own / daysBack).toFixed(1) : '–' }), 570 - $tag('td', { text: user.reposts > 0 ? (user.reposts / daysBack).toFixed(1) : '–' }), 571 - $tag('td.percent', { text: ((user.own + user.reposts) * 100 / total).toFixed(1) + '%' }) 572 - ); 573 - 574 - tbody.append(tr); 575 - } 576 - 577 - if (Math.ceil(daysBack) < days) { 578 - scanInfo.innerText = `🕓 Showing data from ${Math.round(daysBack)} days (your timeline only goes that far):`; 579 - scanInfo.style.display = 'block'; 580 - } 581 - 582 - table.style.display = 'table'; 583 - submit.value = 'Start scan'; 584 - progressBar.style.display = 'none'; 585 - window.scanStartTime = undefined; 586 - }); 587 - } 588 - 589 - function stopScan() { 590 - let submit = $(postingStatsPage.querySelector('input[type=submit]'), HTMLInputElement); 591 - submit.value = 'Start scan'; 592 - window.scanStartTime = undefined; 593 - 594 - let progressBar = $(postingStatsPage.querySelector('input[type=submit] + progress'), HTMLProgressElement); 595 - progressBar.style.display = 'none'; 596 435 } 597 436 598 437 function showNotificationsPage() {
+1 -2
types.d.ts
··· 11 11 declare var api: BlueskyAPI; 12 12 declare var isIncognito: boolean; 13 13 declare var biohazardEnabled: boolean; 14 - declare var scanStartTime: number | undefined; 15 14 declare var loginDialog: HTMLElement; 16 15 declare var accountMenu: HTMLElement; 17 - declare var postingStatsPage: HTMLElement; 18 16 declare var avatarPreloader: IntersectionObserver; 19 17 declare var threadPage: ThreadPage; 18 + declare var postingStatsPage: PostingStatsPage; 20 19 21 20 type json = Record<string, any>; 22 21