+1
index.html
+1
index.html
···
134
<script src="rich_text_lite.js"></script>
135
<script src="models.js"></script>
136
<script src="thread_page.js"></script>
137
+
<script src="posting_stats_page.js"></script>
138
<script src="embed_component.js"></script>
139
<script src="post_component.js"></script>
140
<script src="skythread.js"></script>
+182
posting_stats_page.js
+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
+2
-163
skythread.js
···
7
8
window.loginDialog = $(document.querySelector('#login'));
9
window.accountMenu = $(document.querySelector('#account_menu'));
10
-
window.postingStatsPage = $id('posting_stats_page');
11
12
window.avatarPreloader = buildAvatarPreloader();
13
14
window.threadPage = new ThreadPage();
15
16
html.addEventListener('click', (e) => {
17
$id('account_menu').style.visibility = 'hidden';
···
126
$(accountMenu.querySelector('a[data-action=logout]')).addEventListener('click', (e) => {
127
e.preventDefault();
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
});
145
146
window.appView = new BlueskyAPI('api.bsky.app', false);
···
445
showLoader();
446
showNotificationsPage();
447
} else if (page == 'posting_stats') {
448
-
showPostingStatsPage();
449
}
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
}
597
598
function showNotificationsPage() {
···
7
8
window.loginDialog = $(document.querySelector('#login'));
9
window.accountMenu = $(document.querySelector('#account_menu'));
10
11
window.avatarPreloader = buildAvatarPreloader();
12
13
window.threadPage = new ThreadPage();
14
+
window.postingStatsPage = new PostingStatsPage();
15
16
html.addEventListener('click', (e) => {
17
$id('account_menu').style.visibility = 'hidden';
···
126
$(accountMenu.querySelector('a[data-action=logout]')).addEventListener('click', (e) => {
127
e.preventDefault();
128
logOut();
129
});
130
131
window.appView = new BlueskyAPI('api.bsky.app', false);
···
430
showLoader();
431
showNotificationsPage();
432
} else if (page == 'posting_stats') {
433
+
window.postingStatsPage.show();
434
}
435
}
436
437
function showNotificationsPage() {
+1
-2
types.d.ts
+1
-2
types.d.ts
···
11
declare var api: BlueskyAPI;
12
declare var isIncognito: boolean;
13
declare var biohazardEnabled: boolean;
14
-
declare var scanStartTime: number | undefined;
15
declare var loginDialog: HTMLElement;
16
declare var accountMenu: HTMLElement;
17
-
declare var postingStatsPage: HTMLElement;
18
declare var avatarPreloader: IntersectionObserver;
19
declare var threadPage: ThreadPage;
20
21
type json = Record<string, any>;
22
···
11
declare var api: BlueskyAPI;
12
declare var isIncognito: boolean;
13
declare var biohazardEnabled: boolean;
14
declare var loginDialog: HTMLElement;
15
declare var accountMenu: HTMLElement;
16
declare var avatarPreloader: IntersectionObserver;
17
declare var threadPage: ThreadPage;
18
+
declare var postingStatsPage: PostingStatsPage;
19
20
type json = Record<string, any>;
21