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
|
import './forked-to.css';
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import select from 'select-dom';
import pFilter from 'p-filter';
import onetime from 'onetime';
import features from '../libs/features';
import {isRepoWithAccess} from '../libs/page-detect';
import {getRepoURL, getUsername} from '../libs/utils';
import * as icons from '../libs/icons';
const getCacheKey = onetime((): string => `forked-to:${getUsername()}@${findForkedRepo() || getRepoURL()}`);
async function save(forks: string[]): Promise<void> {
if (forks.length === 0) {
return cache.delete(getCacheKey());
}
return cache.set(getCacheKey(), forks, 10);
}
function saveAllForks(): void {
const forks = select
.all('details-dialog[src*="/fork"] .octicon-repo-forked')
.map(({nextSibling}) => nextSibling!.textContent!.trim());
save(forks);
}
function findForkedRepo(): string | undefined {
const forkSourceElement = select<HTMLAnchorElement>('.fork-flag a');
if (forkSourceElement) {
return forkSourceElement.pathname.slice(1);
}
return undefined;
}
async function validateFork(repo: string): Promise<boolean> {
const response = await fetch(location.origin + '/' + repo, {method: 'HEAD'});
return response.ok;
}
async function updateForks(forks: string[]): Promise<void> {
// Don't validate current page: it exists; it won't be shown in the list; it will be added later anyway
const validForks = await pFilter(forks.filter(fork => fork !== getRepoURL()), validateFork);
// Add current repo to cache if it's a fork
if (isRepoWithAccess() && findForkedRepo()) {
save([...validForks, getRepoURL()].sort(undefined));
} else {
save(validForks);
}
}
async function init(): Promise<void> {
select('details-dialog[src*="/fork"] include-fragment')!
.addEventListener('load', saveAllForks);
const forks = await cache.get<string[]>(getCacheKey());
if (!forks) {
return;
}
document.body.classList.add('rgh-forked-to');
const forkCounter = select('.social-count[href$="/network/members"]')!;
if (forks.length === 1) {
forkCounter.before(
<a href={`/${forks[0]}`}
className="btn btn-sm float-left rgh-forked-button"
title={`Open your fork to ${forks[0]}`}>
{icons.externalLink()}
</a>
);
} else {
forkCounter.before(
<details className="details-reset details-overlay select-menu float-left">
<summary
className="select-menu-button float-left btn btn-sm btn-with-count rgh-forked-button"
title="Open any of your forks"/>
<details-menu
style={{zIndex: 99}}
className="select-menu-modal position-absolute right-0 mt-5">
<div className="select-menu-header">
<span className="select-menu-title">Your forks</span>
</div>
{...forks.map(fork =>
<a
href={`/${fork}`}
className="select-menu-item"
title={`Open your fork to ${fork}`}>
{icons.fork()}
{fork}
</a>
)}
</details-menu>
</details>
);
}
// Validate cache after showing links once, to make it faster
await updateForks(forks);
}
features.add({
id: __featureName__,
description: 'Adds a shortcut to your forks next to the `Fork` button on the current repo.',
screenshot: 'https://user-images.githubusercontent.com/55841/64077281-17bbf000-cccf-11e9-9123-092063f65357.png',
include: [
features.isRepo
],
load: features.onAjaxedPages,
init
});
|