diff --git a/readme.md b/readme.md index ac067798a37..7b98625c454 100644 --- a/readme.md +++ b/readme.md @@ -143,6 +143,7 @@ GitHub Enterprise is also supported. More info in the options. - [SVG files in a PR default to rich-diff view.](https://user-images.githubusercontent.com/5243867/57125552-c08a2b00-6d81-11e9-9b84-cdb535baa98e.png) - [Download count next to release assets.](https://user-images.githubusercontent.com/14323370/58944460-e1aeb480-874f-11e9-8052-2d4dc794ecab.png) - [The most useful comment in issues is highlighted.](https://user-images.githubusercontent.com/1402241/58757449-5b238880-853f-11e9-9526-e86c41a32f00.png) +- [Your repo forks are shown under the repo title.](https://user-images.githubusercontent.com/55841/60543588-f5c9df80-9d16-11e9-8667-52ff16b2cb16.png) ### Declutter diff --git a/source/content.ts b/source/content.ts index e6146cce545..0ca23b0380a 100644 --- a/source/content.ts +++ b/source/content.ts @@ -107,6 +107,7 @@ import './features/highest-rated-comment'; import './features/clean-issue-filters'; import './features/minimize-upload-bar'; import './features/cycle-lists-with-keyboard-shortcuts'; +import './features/forked-to'; import './features/scrollable-code-and-blockquote.css'; import './features/center-reactions-popup.css'; diff --git a/source/features/forked-to.tsx b/source/features/forked-to.tsx new file mode 100644 index 00000000000..5990c5ca4b5 --- /dev/null +++ b/source/features/forked-to.tsx @@ -0,0 +1,100 @@ +import React from 'dom-chef'; +import select from 'select-dom'; +import cache from 'webext-storage-cache'; +import features from '../libs/features'; +import {getRepoURL} from '../libs/utils'; +import {isRepoWithAccess} from '../libs/page-detect'; + +const getCacheKey = (repo: string): string => `forked-to:${repo}`; + +async function showForks(): Promise { + const cached = await getValidatedCache(getSourceRepo()); + const pageHeader = select('.pagehead h1.public')!; + for (const fork of cached.filter(fork => fork !== getRepoURL())) { + pageHeader.append( + forked to  + {fork} + + ); + } +} + +function rememberCurrentFork(): void { + if (!isRepoWithAccess()) { + return; + } + + const forkedRepo = findForkedRepo(); + if (forkedRepo) { + addAndStoreCache(forkedRepo, getRepoURL()); + } +} + +function watchForkDialog(): void { + const forkDialog = select('details-dialog[src*="/fork"]')!; + select('include-fragment', forkDialog)!.addEventListener('load', () => { + const forks = select.all('.octicon-repo-forked', forkDialog).map(forkElement => { + return forkElement.parentNode!.textContent!.trim(); + }); + addAndStoreCache(getSourceRepo(), ...forks); + }); +} + +function findForkedRepo(): string | undefined { + const forkSourceElement = select('.fork-flag:not(.rgh-forked) a'); + if (forkSourceElement) { + return forkSourceElement.pathname.slice(1); + } + + return undefined; +} + +function getSourceRepo(): string { + return findForkedRepo() || getRepoURL(); +} + +async function validateFork(repo: string): Promise { + const response = await fetch(location.origin + '/' + repo, {method: 'HEAD'}); + return response.ok; +} + +async function getValidatedCache(repo: string): Promise { + const cached = await cache.get(getCacheKey(repo)) || []; + const validForks = cached.filter(validateFork).sort(undefined); + + if (cached.length !== validForks.length) { + await cache.set(getCacheKey(repo), validForks, 10); + } + + return validForks; +} + +async function addAndStoreCache(repo: string, ...forks: string[]): Promise { + const cached = await cache.get(getCacheKey(repo)) || []; + for (const fork of forks) { + if (!cached.includes(fork)) { + cached.push(fork); + } + } + + await cache.set(getCacheKey(repo), cached, 10); +} + +async function init(): Promise { + watchForkDialog(); + + rememberCurrentFork(); + + showForks(); +} + +features.add({ + id: __featureName__, + description: 'Your repo forks are shown under the repo title', + screenshot: 'https://user-images.githubusercontent.com/55841/60543588-f5c9df80-9d16-11e9-8667-52ff16b2cb16.png', + include: [ + features.isRepo + ], + load: features.onAjaxedPages, + init +}); diff --git a/source/libs/page-detect.ts b/source/libs/page-detect.ts index 9c667072947..c54173a832b 100644 --- a/source/libs/page-detect.ts +++ b/source/libs/page-detect.ts @@ -93,6 +93,8 @@ export const isRepoSettings = (): boolean => /^settings/.test(getRepoPath()!); export const isRepoTree = (): boolean => isRepoRoot() || /^tree\//.test(getRepoPath()!); +export const isRepoWithAccess = (): boolean => isRepo() && select.exists('.reponav-item[href$="/settings"]'); + export const isSingleCommit = (): boolean => /^commit\/[0-9a-f]{5,40}/.test(getRepoPath()!); export const isSingleFile = (): boolean => /^blob\//.test(getRepoPath()!);