+38
api.js
+38
api.js
···
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
+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
+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
+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