Thread viewer for Bluesky

added avatars

+78 -16
like_stats_page.js
··· 10 10 this.submitButton = $(this.pageElement.querySelector('input[type="submit"]'), HTMLInputElement); 11 11 this.progressBar = $(this.pageElement.querySelector('input[type=submit] + progress'), HTMLProgressElement); 12 12 13 - this.receivedTable = $(this.pageElement.querySelector('.received-likes')); 14 - this.givenTable = $(this.pageElement.querySelector('.given-likes')); 13 + this.receivedTable = $(this.pageElement.querySelector('.received-likes'), HTMLTableElement); 14 + this.givenTable = $(this.pageElement.querySelector('.given-likes'), HTMLTableElement); 15 15 16 16 this.appView = new BlueskyAPI('public.api.bsky.app', false); 17 17 ··· 69 69 let fetchGivenLikes = this.fetchGivenLikes(requestedDays); 70 70 71 71 let receivedLikes = await this.fetchReceivedLikes(requestedDays); 72 - let received = countElementsBy(receivedLikes, (x) => x.actor.handle); 72 + let receivedStats = this.sumUpReceivedLikes(receivedLikes); 73 + let topReceived = this.getTopEntries(receivedStats); 73 74 74 - await this.renderResults(received, this.receivedTable); 75 + await this.renderResults(topReceived, this.receivedTable); 75 76 76 77 let givenLikes = await fetchGivenLikes; 77 - let given = countElementsBy(givenLikes, (x) => atURI(x.value.subject.uri).repo); 78 + let givenStats = this.sumUpGivenLikes(givenLikes); 79 + let topGiven = this.getTopEntries(givenStats); 78 80 79 - await this.renderResults(given, this.givenTable); 81 + let profileInfo = await appView.getRequest('app.bsky.actor.getProfiles', { actors: topGiven.map(x => x.did) }); 82 + 83 + for (let profile of profileInfo.profiles) { 84 + let user = /** @type {LikeStat} */ (topGiven.find(x => x.did == profile.did)); 85 + user.handle = profile.handle; 86 + user.avatar = profile.avatar; 87 + } 88 + 89 + await this.renderResults(topGiven, this.givenTable); 80 90 81 91 this.receivedTable.style.display = 'table'; 82 92 this.givenTable.style.display = 'table'; ··· 151 161 return results.flat(); 152 162 } 153 163 154 - async renderResults(counts, table) { 155 - let tableBody = $(table.querySelector('tbody')); 156 - tableBody.innerHTML = ''; 164 + /** 165 + * @typedef {{ handle?: string, did?: string, avatar?: string, count: number }} LikeStat 166 + * @typedef {Record<string, LikeStat>} LikeStatHash 167 + */ 168 + 169 + /** @param {json[]} likes, @returns {LikeStatHash} */ 170 + 171 + sumUpReceivedLikes(likes) { 172 + /** @type {LikeStatHash} */ 173 + let stats = {}; 174 + 175 + for (let like of likes) { 176 + let handle = like.actor.handle; 157 177 158 - let entries = Object.entries(counts).sort(this.sortResults).slice(0, 20); 178 + if (!stats[handle]) { 179 + stats[handle] = { handle: handle, count: 0, avatar: like.actor.avatar }; 180 + } 159 181 160 - for (let [user, count] of entries) { 161 - let handle = user.startsWith('did:') ? await accountAPI.fetchHandleForDid(user) : user; 182 + stats[handle].count += 1; 183 + } 162 184 185 + return stats; 186 + } 187 + 188 + /** @param {json[]} likes, @returns {LikeStatHash} */ 189 + 190 + sumUpGivenLikes(likes) { 191 + /** @type {LikeStatHash} */ 192 + let stats = {}; 193 + 194 + for (let like of likes) { 195 + let did = atURI(like.value.subject.uri).repo; 196 + 197 + if (!stats[did]) { 198 + stats[did] = { did: did, count: 0 }; 199 + } 200 + 201 + stats[did].count += 1; 202 + } 203 + 204 + return stats; 205 + } 206 + 207 + /** @param {LikeStatHash} counts, @returns {LikeStat[]} */ 208 + 209 + getTopEntries(counts) { 210 + return Object.entries(counts).sort(this.sortResults).map(x => x[1]).slice(0, 20); 211 + } 212 + 213 + /** @param {LikeStat[]} topEntries, @param {HTMLTableElement} table, @returns {Promise<void>} */ 214 + 215 + async renderResults(topEntries, table) { 216 + let tableBody = $(table.querySelector('tbody')); 217 + tableBody.innerHTML = ''; 218 + 219 + for (let user of topEntries) { 163 220 let tr = $tag('tr'); 164 221 tr.append( 165 - $tag('td', { html: `<a href="https://bsky.app/profile/${handle}" target="_blank">${handle}</a>` }), 166 - $tag('td', { text: count }) 222 + $tag('td', { 223 + html: `<img class="avatar" src="${user.avatar}"> ` + 224 + `<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>` 225 + }), 226 + $tag('td', { text: user.count }) 167 227 ); 168 228 169 229 tableBody.append(tr); ··· 201 261 this.progressBar.value = totalProgress; 202 262 } 203 263 264 + /** @param {[string, LikeStat]} a, @param {[string, LikeStat]} b, @returns {-1|1|0} */ 265 + 204 266 sortResults(a, b) { 205 - if (a[1] < b[1]) { 267 + if (a[1].count < b[1].count) { 206 268 return 1; 207 - } else if (a[1] > b[1]) { 269 + } else if (a[1].count > b[1].count) { 208 270 return -1; 209 271 } else { 210 272 return 0;
+8
style.css
··· 866 866 padding: 7px 10px; 867 867 } 868 868 869 + #like_stats_page .scan-result .avatar { 870 + width: 24px; 871 + border-radius: 14px; 872 + vertical-align: middle; 873 + margin-right: 2px; 874 + padding: 2px; 875 + } 876 + 869 877 @media (prefers-color-scheme: dark) { 870 878 body { 871 879 background-color: rgb(39, 39, 37);
-20
utils.js
··· 150 150 url.searchParams.set('post', postId); 151 151 return url.toString(); 152 152 } 153 - 154 - /** 155 - * @template T 156 - * @param {T[]} list 157 - * @param {(T) => string} prop 158 - * @returns {Record<string, number>} 159 - */ 160 - 161 - function countElementsBy(list, prop) { 162 - /** @type {Record<string, number>} */ 163 - let counts = {}; 164 - 165 - for (let obj of list) { 166 - let value = prop(obj); 167 - counts[value] = counts[value] || 0; 168 - counts[value] += 1; 169 - } 170 - 171 - return counts; 172 - }