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
|
import './forked-to.css';
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import select from 'select-dom';
import elementReady from 'element-ready';
import * as pageDetect from 'github-url-detection';
import ForkIcon from 'octicon/repo-forked.svg';
import CheckIcon from 'octicon/check.svg';
import LinkExternalIcon from 'octicon/link-external.svg';
import features from '.';
import fetchDom from '../helpers/fetch-dom';
import GitHubURL from '../github-helpers/github-url';
import {getUsername, getForkedRepo, getRepo} from '../github-helpers';
const getForkSourceRepo = (): string => getForkedRepo() ?? getRepo()!.nameWithOwner;
const getCacheKey = (): string => `forked-to:${getForkSourceRepo()}@${getUsername()}`;
const updateCache = cache.function(async (): Promise<string[] | undefined> => {
const document = await fetchDom(`/${getForkSourceRepo()}/fork?fragment=1`);
const forks = select
.all('.octicon-repo-forked', document)
.map(({nextSibling}) => nextSibling!.textContent!.trim());
return forks.length > 0 ? forks : undefined;
}, {
maxAge: {hours: 1},
staleWhileRevalidate: {days: 5},
cacheKey: getCacheKey
});
function createLink(baseRepo: string): string {
if (pageDetect.isSingleFile() || pageDetect.isRepoTree() || pageDetect.isEditingFile()) {
const [user, repository] = baseRepo.split('/', 2);
const url = new GitHubURL(location.href).assign({
user,
repository
});
return url.pathname;
}
return '/' + baseRepo;
}
async function updateUI(forks: string[]): Promise<void> {
// Don't add button if you're visiting the only fork available
if (forks.length === 1 && forks[0] === getRepo()!.nameWithOwner) {
return;
}
document.body.classList.add('rgh-forked-to');
const forkCounter = (await elementReady('.social-count[href$="/network/members"]'))!;
if (forks.length === 1) {
forkCounter.before(
<a
href={createLink(forks[0])}
className="btn btn-sm float-left rgh-forked-button"
title={`Open your fork at ${forks[0]}`}
>
<LinkExternalIcon/>
</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"
aria-haspopup="menu"
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={createLink(fork)}
className={`select-menu-item ${fork === getRepo()!.nameWithOwner ? 'selected' : ''}`}
title={`Open your fork at ${fork}`}
>
<span className="select-menu-item-icon rgh-forked-to-icon">
{fork === getRepo()!.nameWithOwner ? <CheckIcon/> : <ForkIcon/>}
</span>
{fork}
</a>
))}
</details-menu>
</details>
);
}
}
async function init(): Promise<void | false> {
const forks = await cache.get<string[]>(getCacheKey());
if (forks) {
await updateUI(forks);
}
// This feature only applies to users that have multiple organizations, because that makes a fork picker modal appear when clicking on "Fork"
const hasOrganizations = await elementReady('details-dialog[src*="/fork"] include-fragment');
// Only fetch/update forks when we see a fork (on the current page or in the cache).
// This avoids having to `updateCache` for every single repo you visit.
if (forks || (hasOrganizations && pageDetect.isForkedRepo())) {
await updateCache();
} else {
return false;
}
}
void features.add(__filebasename, {
include: [
pageDetect.isRepo
],
awaitDomReady: false,
init
});
|