summaryrefslogtreecommitdiff
path: root/source/features/open-all-notifications.tsx
blob: b6af66b897a191688b88529285b4d33a9962de55 (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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import './open-all-notifications.css';
import React from 'dom-chef';
import select from 'select-dom';
import * as pageDetect from 'github-url-detection';
import {LinkExternalIcon} from '@primer/octicons-react';
import delegate, {DelegateEvent} from 'delegate-it';

import features from '../feature-manager.js';
import openTabs from '../helpers/open-tabs.js';
import {appendBefore} from '../helpers/dom-utils.js';
import observe from '../helpers/selector-observer.js';

// Selector works on:
// https://github.com/notifications (Grouped by date)
// https://github.com/notifications (Grouped by repo)
// https://github.com/notifications?query=reason%3Acomment (which is an unsaved filter)
const notificationHeaderSelector = '.js-check-all-container .js-bulk-action-toasts ~ div .Box-header';

const openUnread = features.getIdentifiers('open-notifications-button');
const openSelected = features.getIdentifiers('open-selected-button');

function getUnreadNotifications(container: ParentNode = document): HTMLElement[] {
	return select.all('.notification-unread', container);
}

async function openNotifications(notifications: Element[], markAsDone = false): Promise<void> {
	const urls: string[] = [];
	for (const notification of notifications) {
		urls.push(notification.querySelector('a')!.href);
	}

	const openingTabs = openTabs(urls);
	if (!await openingTabs) {
		return;
	}

	for (const notification of notifications) {
		if (markAsDone) {
			notification.querySelector('[title="Done"]')!.click();
		} else {
			// Mark all as read instead
			notification.classList.replace('notification-unread', 'notification-read');
		}
	}
}

async function openUnreadNotifications({delegateTarget, altKey}: DelegateEvent<MouseEvent>): Promise<void> {
	const container = delegateTarget.closest('.js-notifications-group') ?? document;
	await openNotifications(getUnreadNotifications(container), altKey);

	// Remove all now-unnecessary buttons
	removeOpenUnreadButtons(container);
}

async function openSelectedNotifications(): Promise<void> {
	const selectedNotifications = select.all('.notifications-list-item :checked')
		.map(checkbox => checkbox.closest('.notifications-list-item')!);
	await openNotifications(selectedNotifications);

	if (!select.exists('.notification-unread')) {
		removeOpenUnreadButtons();
	}
}

function removeOpenUnreadButtons(container: ParentNode = document): void {
	for (const button of select.all(openUnread.selector, container)) {
		button.remove();
	}
}

function addSelectedButton(selectedActionsGroup: HTMLElement): void {
	const button = (
		<button className={'btn btn-sm ' + openSelected.class} type="button">
			<LinkExternalIcon className="mr-1"/>Open
		</button>
	);
	appendBefore(
		selectedActionsGroup,
		'details',
		button,
	);
}

function addToRepoGroup(markReadButton: HTMLElement): void {
	const repository = markReadButton.closest('.js-notifications-group')!;
	if (getUnreadNotifications(repository).length === 0) {
		return;
	}

	markReadButton.before(
		<button
			type="button"
			className={'btn btn-sm mr-2 tooltipped tooltipped-w ' + openUnread.class}
			aria-label="Open all unread notifications from this repo"
		>
			<LinkExternalIcon width={16}/> Open unread
		</button>,
	);
}

function addToMainHeader(notificationHeader: HTMLElement): void {
	if (getUnreadNotifications().length === 0) {
		return;
	}

	notificationHeader.append(
		<button className={'btn btn-sm ml-auto d-none ' + openUnread.class} type="button">
			<LinkExternalIcon className="mr-1"/>Open all unread
		</button>,
	);
}

function init(signal: AbortSignal): void {
	delegate(openSelected.selector, 'click', openSelectedNotifications, {signal});
	delegate(openUnread.selector, 'click', openUnreadNotifications, {signal});

	observe(notificationHeaderSelector + ' .js-notifications-mark-selected-actions', addSelectedButton, {signal});
	observe(notificationHeaderSelector, addToMainHeader, {signal});
	observe('.js-grouped-notifications-mark-all-read-button', addToRepoGroup, {signal});
}

void features.add(import.meta.url, {
	include: [
		pageDetect.isNotifications,
	],
	exclude: [
		pageDetect.isBlank, // Empty notification list
	],
	init,
});