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
|
import './reactions-avatars.css';
import React from 'dom-chef';
import select from 'select-dom';
import {flatZip} from 'flat-zip';
import * as pageDetect from 'github-url-detection';
import features from '.';
import onReplacedElement from '../helpers/on-replaced-element';
import {getUsername, isFirefox} from '../github-helpers';
const arbitraryAvatarLimit = 36;
const approximateHeaderLength = 3; // Each button header takes about as much as 3 avatars
type Participant = {
container: HTMLElement;
username: string;
imageUrl: 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, imageUrl: existingAvatar.src});
continue;
}
// If it's not a bot, use a shortcut URL #2125
if (cleanName === username) {
const imageUrl = `/${username}.png?size=${window.devicePixelRatio * 20}`;
participants.push({container, username, imageUrl});
}
}
return participants;
}
async function showAvatarsOn(commentReactions: Element): Promise<void> {
const avatarLimit = arbitraryAvatarLimit - (commentReactions.children.length * approximateHeaderLength);
const participantByReaction = select
.all(':scope > button', commentReactions)
.map(getParticipants);
const flatParticipants = flatZip(participantByReaction, avatarLimit);
for (const {container, username, imageUrl} of flatParticipants) {
container.append(
// Without this, Firefox will follow the link instead of submitting the reaction button
<a href={isFirefox ? undefined : `/${username}`} className="rounded-1 avatar-user">
<img src={imageUrl} className="avatar-user rounded-1"/>
</a>
);
}
const trackableElement = commentReactions.closest<HTMLElement>('[data-body-version]')!;
const trackingSelector = `[data-body-version="${trackableElement.dataset.bodyVersion!}"]`;
await onReplacedElement(trackingSelector, init);
}
const viewportObserver = new IntersectionObserver(changes => {
for (const change of changes) {
if (change.isIntersecting) {
void showAvatarsOn(change.target);
viewportObserver.unobserve(change.target);
}
}
}, {
// Start loading a little before they become visible
rootMargin: '500px'
});
async function init(): Promise<void> {
for (const commentReactions of select.all('.has-reactions .comment-reactions-options:not(.rgh-reactions)')) {
commentReactions.classList.add('rgh-reactions');
viewportObserver.observe(commentReactions);
}
}
void features.add({
id: __filebasename,
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: [
pageDetect.hasComments
],
init
});
|