summaryrefslogtreecommitdiff
path: root/source/features/reactions-avatars.tsx
blob: dd1d378239279e994af5419e352e816c37b5732e (plain) (blame)
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
});