1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
|
import './reactions-avatars.css';
import React from 'dom-chef';
import select from 'select-dom';
import debounce from 'debounce-fn';
import {timerIntervalometer} from 'intervalometer';
import features from '../libs/features';
import {getUsername, flatZip} from '../libs/utils';
const arbitraryAvatarLimit = 36;
const approximateHeaderLength = 3; // Each button header takes about as much as 3 avatars
type Participant = {
container: HTMLElement;
username: string;
src: string;
};
function getParticipants(container: HTMLElement): Participant[] {
const currentUser = getUsername();
const users = container.getAttribute('aria-label')!
.replace(/ reacted with.*/, '')
.replace(/,? and /, ', ')
.replace(/, \d+ more/, '')
.split(', ');
const participants = [];
for (const username of users) {
if (username === currentUser) {
continue;
}
const cleanName = username.replace('[bot]', '');
// Find image on page. Saves a request and a redirect + add support for bots
const existingAvatar = select<HTMLImageElement>(`[alt="@${cleanName}"]`);
if (existingAvatar) {
participants.push({container, username, src: existingAvatar.src});
continue;
}
// If it's not a bot, use a shortcut URL #2125
if (cleanName === username) {
const src = `/${username}.png?size=${window.devicePixelRatio * 20}`;
participants.push({container, username, src});
}
}
return participants;
}
function add(): void {
for (const list of select.all('.has-reactions .comment-reactions-options:not(.rgh-reactions)')) {
const avatarLimit = arbitraryAvatarLimit - (list.children.length * approximateHeaderLength);
const participantByReaction = [...list.children as HTMLCollectionOf<HTMLElement>].map(getParticipants);
const flatParticipants = flatZip(participantByReaction, avatarLimit);
for (const {container, username, src} of flatParticipants) {
container.append(
<a>
<img src={src} />
</a>
);
// Without this, Firefox will follow the link instead of submitting the reaction button
if (!navigator.userAgent.includes('Firefox/')) {
(container.lastElementChild as HTMLAnchorElement).href = `/${username}`;
}
}
list.classList.add('rgh-reactions');
// Overlap reaction avatars when near the avatarLimit
if (flatParticipants.length > avatarLimit * 0.9) {
list.classList.add('rgh-reactions-near-limit');
}
}
}
function init(): void {
add();
// GitHub receives update messages via WebSocket, which seem to trigger
// a fetch for the updated content. When the content is actually updated
// in the DOM there are no further events, so we have to look for changes
// every 300ms for the 3 seconds following the last message.
// This should be lighter than using MutationObserver on the whole page.
const updater = timerIntervalometer(add, 300);
const cancelInterval = debounce(updater.stop, {wait: 3000});
window.addEventListener('socket:message', () => {
updater.start();
cancelInterval();
});
}
features.add({
id: __featureName__,
description: 'Adds reaction avatars showing *who* reacted to a comment',
screenshot: 'https://user-images.githubusercontent.com/1402241/34438653-f66535a4-ecda-11e7-9406-2e1258050cfa.png',
include: [
features.hasComments
],
load: features.onNewComments,
init
});
|