summaryrefslogtreecommitdiff
path: root/source/features/forked-to.tsx
blob: fdeecc98918f28c29bd65e6bc280c83eacdf3468 (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
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
});