summaryrefslogtreecommitdiff
path: root/source/features/open-all-notifications.tsx
blob: 7af28417577c84992afa2e9a60e96ffa99490f09 (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
131
132
133
134
135
136
137
138
139
import './open-all-notifications.css';
import React from 'dom-chef';
import {$$, elementExists} 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 $$('.notification-unread', container);
}

async function openNotifications(notifications: Element[], markAsDone = false): Promise<void> {
	const urls = notifications
		.reverse() // Open oldest first #6755
		.map(notification => 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 = $$('.notifications-list-item :checked')
		.map(checkbox => checkbox.closest('.notifications-list-item')!);
	await openNotifications(selectedNotifications);

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

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

function addSelectedButton(selectedActionsGroup: HTMLElement): void {
	const button = (
		<button className={'btn btn-sm mr-2 ' + 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,
});

/*

Test URLs:

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)

*/