├── .editorconfig
├── .gitattributes
├── .gitignore
├── .npmrc
├── .travis.yml
├── extension
├── content.css
├── icon.png
├── manifest.json
└── options.html
├── license
├── media
├── icon.ai
├── promo.png
├── screenshot-delete-fork.png
├── screenshot-reactions.png
└── screenshot-repo.png
├── package.json
├── readme.md
├── src
├── background.js
├── content.js
├── features
│ ├── add-ci-link.js
│ ├── add-confirmation-to-comment-cancellation.js
│ ├── add-delete-fork-link.js
│ ├── add-diff-view-without-whitespace-option.js
│ ├── add-filter-comments-by-you.js
│ ├── add-keyboard-shortcuts-to-comment-fields.js
│ ├── add-milestone-navigation.js
│ ├── add-patch-diff-links.js
│ ├── add-profile-hotkey.js
│ ├── add-project-new-link.js
│ ├── add-readme-buttons.js
│ ├── add-releases-tab.js
│ ├── add-time-machine-links-to-comments.js
│ ├── add-title-to-emojis.js
│ ├── add-trending-menu-item.js
│ ├── add-yours-menu-item.js
│ ├── auto-load-more-news.js
│ ├── copy-file-path.js
│ ├── copy-file.js
│ ├── copy-markdown.js
│ ├── copy-on-y.js
│ ├── fix-squash-and-merge-title.js
│ ├── focus-confirmation-buttons.js
│ ├── hide-empty-meta.js
│ ├── hide-own-stars.js
│ ├── linkify-branch-refs.js
│ ├── linkify-issues-in-titles.js
│ ├── linkify-urls-in-code.js
│ ├── mark-merge-commits-in-list.js
│ ├── mark-unread.js
│ ├── more-dropdown.js
│ ├── move-account-switcher-to-sidebar.js
│ ├── move-marketplace-link-to-profile-dropdown.js
│ ├── op-labels.js
│ ├── open-all-notifications.js
│ ├── open-ci-details-in-new-tab.js
│ ├── preserve-whitespace-option-in-nav.js
│ ├── reactions-avatars.js
│ ├── remove-diff-signs.js
│ ├── remove-projects-tab.js
│ ├── remove-upload-files-button.js
│ ├── scroll-to-top-on-collapse.js
│ ├── show-names.js
│ ├── show-recently-pushed-branches.js
│ ├── sort-milestones-by-closest-due-date.js
│ └── upload-button.js
├── libs
│ ├── api.js
│ ├── domify.js
│ ├── get-text-nodes.js
│ ├── icons.js
│ ├── page-detect.js
│ ├── synchronous-storage.js
│ └── utils.js
└── options.js
├── test
├── copy-markdown.js
├── fixtures
│ └── window.js
└── page-detect.js
└── webpack.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.yml]
11 | indent_style = space
12 | indent_size = 2
13 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.ai binary
3 | *.js text eol=lf
4 | readme.md merge=union
5 | extension/content.css merge=union
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | yarn.lock
3 | extension/**/*.js
4 | extension/**/*.map
5 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 'node'
4 | env:
5 | - EXTENSION_ID=hlepfoohegkhhmjieoechaddaejaokhf
6 | deploy:
7 | - provider: script
8 | skip_cleanup: true
9 | script: npm run release
10 | on:
11 | tags: true
12 | - provider: script
13 | skip_cleanup: true
14 | script: npm run release
15 | on:
16 | # Set to deploy on: cron with recent commits
17 | branch: master
18 | condition: $(npm run can-release --silent)
19 |
--------------------------------------------------------------------------------
/extension/content.css:
--------------------------------------------------------------------------------
1 | .subscribe-feed {
2 | display: none !important;
3 | }
4 |
5 | /* Allow for absolute positioning relative to the dashboard */
6 | #dashboard,
7 | .orgpage > .container {
8 | position: relative !important;
9 | }
10 |
11 | #dashboard .new-repo {
12 | display: none !important;
13 | }
14 |
15 | /* Remove "Repositories you contribute to" dashboard sidebar box */
16 | .dashboard-sidebar > .boxed-group[role="navigation"]:not(.repos) {
17 | display: none !important;
18 | }
19 |
20 | /* Match the width of the account switcher modal to that of the user repo list */
21 | .dashboard-sidebar .select-menu-modal {
22 | margin: 0;
23 | width: 313px;
24 | }
25 |
26 | /* Remove the toolbar from the right repo box on the dashboard */
27 | .dashboard-sidebar .user-repos > h3,
28 | .dashboard-sidebar .user-repos > .boxed-group-action {
29 | display: none !important;
30 | }
31 | .dashboard-sidebar .user-repos .boxed-group-inner {
32 | border-radius: 3px !important;
33 | }
34 | .dashboard-sidebar .user-repos .filter-repos {
35 | border-top-left-radius: 3px !important;
36 | border-top-right-radius: 3px !important;
37 | }
38 | .dashboard-sidebar .user-repos .repo-icon {
39 | opacity: 0.6 !important;
40 | }
41 |
42 | /* Remove border between dashboard news items */
43 | .news .alert {
44 | border: 0 !important;
45 | }
46 |
47 | /* Remove news feed tips */
48 | .news > div:last-of-type.mt-4 {
49 | display: none !important;
50 | }
51 |
52 | /* Remove tooltips where unnecessary */
53 | .tooltipped:before,
54 | .tooltipped:after {
55 | display: none !important;
56 | }
57 |
58 | .notification-actions .tooltipped:before,
59 | .notification-actions .tooltipped:after,
60 | .reaction-summary-item:before,
61 | .reaction-summary-item:after,
62 | .avatar-group-item:before,
63 | .avatar-group-item:after,
64 | .js-zeroclipboard:before,
65 | .js-zeroclipboard:after,
66 | .rgh-tooltipped:before,
67 | .rgh-tooltipped:after,
68 | .avatar:before,
69 | .avatar:after {
70 | display: inline-block !important;
71 | }
72 |
73 | /* Fade out the file icons */
74 | .file-wrap .files .octicon {
75 | opacity: 0.6;
76 | }
77 |
78 | /* Remove annoying hover in the repo file list */
79 | .file-wrap .files .navigation-focus td {
80 | background: none !important;
81 | }
82 |
83 | #readme > h3 {
84 | display: none !important;
85 | }
86 |
87 | #readme .markdown-body {
88 | border-radius: 3px !important;
89 | }
90 |
91 | .paginate-protip {
92 | display: none !important;
93 | }
94 |
95 | /* Remove useless message on the right side of the PR merge box */
96 | .alt-merge-options {
97 | display: none !important;
98 | }
99 |
100 | /* Remove top buttons on comment box */
101 | .timeline-comment-wrapper .tabnav-extra, .inline-comment-form-container .tabnav-extra {
102 | display: none !important;
103 | }
104 |
105 | /* Remove commentbox toolbar */
106 | .toolbar-group {
107 | display: none;
108 | margin-left: 0 !important;
109 | }
110 | .toolbar-group:last-child {
111 | display: inline-block;
112 | }
113 | .toolbar-group .toolbar-item:not(.js-saved-reply-container):not(.rgh-upload-btn) {
114 | display: none;
115 | }
116 | .toolbar-commenting {
117 | margin-top: 4px;
118 | }
119 |
120 | /* Remove upload message on comment box */
121 | .is-default .drag-and-drop {
122 | display: none;
123 | }
124 | .upload-enabled textarea {
125 | border-bottom: 1px solid #ccc !important;
126 | border-radius: 3px !important;
127 | }
128 | .upload-enabled textarea:focus {
129 | border-bottom: 1px solid #2188ff !important;
130 | }
131 |
132 | /* Remove random protip at the bottom of the page */
133 | .protip {
134 | display: none !important;
135 | }
136 |
137 | /* Remove message under the `Unsubscribe` button */
138 | .sidebar-notifications .reason {
139 | display: none !important;
140 | }
141 |
142 | /* Move the dashboard organization switcher to the right column */
143 | .news.column.two-thirds .account-switcher {
144 | /* Hide switcher from the main column, but if JS fails show it */
145 | animation: temporarily-hide 5s steps(1, end);
146 | }
147 | @keyframes temporarily-hide {
148 | from {
149 | position: absolute;
150 | top: -1000px;
151 | }
152 | }
153 | .account-switcher button {
154 | width: 100%;
155 | margin-bottom: 15px;
156 | }
157 | .account-switcher .select-menu-button-gravatar {
158 | float: none !important;
159 | display: inline-block !important;
160 | vertical-align: top !important;
161 | }
162 | /* Organization dashboard */
163 | .orgpage .underline-nav {
164 | float: left !important;
165 | }
166 | .orgpage .account-switcher {
167 | right: 0 !important;
168 | }
169 | .orgpage .user-repos {
170 | position: static !important;
171 | }
172 | .orgpage + .container .dashboard-sidebar {
173 | padding-top: 0;
174 | }
175 |
176 | /* Remove "New pull request" button on repo page */
177 | .file-navigation .new-pull-request-btn {
178 | display: none !important;
179 | }
180 |
181 | /* Add hover underline for `content.js` linkified branch refs in pull requests */
182 | a .commit-ref:hover,
183 | a .commit-ref:hover span {
184 | text-decoration: underline !important;
185 | }
186 |
187 | /* The extra span on branch names interferes with a possible deletion line-through */
188 | .commit-ref span {
189 | text-decoration: inherit;
190 | }
191 |
192 | /* Remove the "new feature" notification box */
193 | .dashboard-sidebar .octofication {
194 | display: none !important;
195 | }
196 |
197 | /* Remove useless tip in the organization news feed */
198 | #dashboard .alert.git_hub {
199 | display: none !important;
200 | }
201 |
202 | /* Style for edit README button */
203 | #readme.blob #refined-github-readme-buttons {
204 | display: none;
205 | }
206 |
207 | #refined-github-readme-buttons {
208 | position: absolute;
209 | top: 10px;
210 | right: 10px;
211 | }
212 |
213 | #refined-github-readme-buttons a {
214 | opacity: 0.2;
215 | transition: opacity 250ms;
216 | text-decoration: none;
217 | }
218 |
219 | #refined-github-readme-buttons a:hover {
220 | opacity: 1;
221 | }
222 |
223 | #refined-github-readme-buttons a:not(:first-child) {
224 | margin-left: 5px;
225 | }
226 |
227 | /* Style for delete fork link */
228 | .post-merge-message {
229 | min-height: 85px;
230 | }
231 |
232 | #refined-github-delete-fork-link {
233 | position: absolute;
234 | top: 45px;
235 | right: 30px;
236 | padding: 10px 10px 0;
237 | color: #df3e3e;
238 | }
239 |
240 | /* Hide news items when people:
241 | pushed to a branch
242 | deleted a branch
243 | added someone as a collaborator
244 | forked a repo
245 | */
246 | .news .push,
247 | .news .alert.delete.simple,
248 | .news .member_add,
249 | .news .fork {
250 | display: none !important;
251 | }
252 |
253 | /* Ensure the news feed is still shown once the items above have been hidden */
254 | .news {
255 | min-height: 1px;
256 | }
257 |
258 | .news .alert {
259 | padding: 0 0 0 45px !important;
260 | }
261 |
262 | :root .news .alert .body { /* Higher specificity, avoids !important */
263 | padding: 1em 0;
264 | }
265 |
266 | /*
267 | Remove padding from body of simple news alerts in Dashboard since some will be hidden
268 | we will add it back on for the simple news alerts we decide to show
269 | */
270 | .news .alert.create.simple,
271 | .news .alert.create.simple .body {
272 | padding-top: 0 !important;
273 | padding-bottom: 0 !important;
274 | }
275 |
276 | /* Add padding back to the contents of simple news alerts in Dashboard */
277 | .news .alert.create.simple .body .title {
278 | padding-bottom: 1.5em !important;
279 | }
280 | .news .alert.create.simple .body .time {
281 | padding-top: 1.5em !important;
282 | }
283 | .news .alert.create.simple .body .dashboard-event-icon {
284 | top: 22px !important;
285 | }
286 |
287 | /* Don't show contents of simple news alert in Dashboard when you create a branch */
288 | .news .alert.create.simple .octicon-git-branch,
289 | .news .alert.create.simple .octicon-git-branch + .title,
290 | .news .alert.create.simple .octicon-git-branch + .title + .time {
291 | display: none;
292 | }
293 |
294 | /* Align items top to bottom */
295 | .news .alert .body,
296 | .news .alert .body .simple {
297 | display: flex;
298 | flex-direction: column;
299 | }
300 |
301 | /* Move time to the top of news item */
302 | .news .alert .body .time {
303 | order: -1;
304 | }
305 |
306 | /* Highlight all the titles */
307 | .news .alert .body .title {
308 | font-size: 14px !important;
309 | font-weight: 600 !important;
310 | color: inherit !important;
311 | }
312 |
313 | /* Increase size of all event icons */
314 | .news .alert svg.octicon.dashboard-event-icon {
315 | height: 32px !important;
316 | width: 28px !important;
317 | }
318 |
319 |
320 |
321 | /* Decrease font-size on commit details so our custom patch and diff links fit */
322 | .commit .sha-block {
323 | margin-left: 7px !important;
324 | }
325 | .commit .sha-block,
326 | .commit .sha {
327 | font-size: 10px !important;
328 | }
329 | .signed-commit-badge-medium {
330 | padding: 2px 4px !important;
331 | font-size: 10px !important;
332 | margin-left: 5px !important;
333 | }
334 |
335 | /* Remove `Developer Program Member` from profile page */
336 | .page-profile .member-badge {
337 | display: none !important;
338 | }
339 |
340 | /* Fade out merge commits from commit list */
341 | .refined-github-merge-commit .commit-title {
342 | font-size: 12px !important;
343 | margin-top: 4px !important;
344 | }
345 |
346 | .refined-github-merge-commit .commit-author-section {
347 | font-size: 11.4px !important;
348 | }
349 |
350 | .refined-github-merge-commit .octicon-git-pull-request {
351 | color: #4078c0;
352 | margin-left: 9px;
353 | width: 27px;
354 | height: 36px;
355 | }
356 |
357 | .refined-github-merge-commit .avatar-child {
358 | width: 16px !important;
359 | height: 16px !important;
360 | }
361 |
362 | /* Move new button to the right on single commit page */
363 | #toc .refined-github-toggle-whitespace {
364 | float: right;
365 | }
366 |
367 | /* Limit width of commit title and description inputs to 50/80 chars */
368 | #commit-summary-input, #commit-description-textarea {
369 | font-family: monospace !important;
370 | }
371 |
372 | #commit-summary-input {
373 | width: 410px !important;
374 | }
375 |
376 | #commit-description-textarea {
377 | width: 645px !important;
378 | }
379 |
380 | /* Make tab indented code more readable on GitHub and Gist */
381 | * {
382 | -moz-tab-size: 4 !important;
383 | tab-size: 4 !important;
384 | }
385 |
386 | /* Larger comment box */
387 | .comment-form-textarea {
388 | min-height: 200px !important;
389 | }
390 |
391 | /* Styles for avatars Refined GitHub adds to Reactions */
392 | .rgh-reactions a:first-of-type {
393 | margin-left: 4px;
394 | }
395 |
396 | .reaction-summary-item {
397 | padding-left: 10px !important;
398 | padding-right: 10px !important;
399 | }
400 |
401 | .reaction-summary-item.add-reaction-btn {
402 | padding-right: 0 !important;
403 | }
404 |
405 | .reaction-summary-item {
406 | --background: #FFF;
407 | }
408 |
409 | .reaction-summary-item.user-has-reacted {
410 | --background: #f2f8fa;
411 | }
412 |
413 | .reaction-summary-item a {
414 | display: inline-block;
415 | vertical-align: middle;
416 | width: 20px;
417 | height: 20px;
418 |
419 | background: #efefef; /* Placeholder before the images load */
420 | box-shadow: 0 0 0 2px var(--background);
421 | border-radius: 3px;
422 |
423 | margin-left: -2px;
424 | transition: margin-left 0.2s;
425 | }
426 |
427 | /* This image will start at height:0 and will expand once loaded, covering the gray placeholder */
428 | .reaction-summary-item img {
429 | max-width: 100%;
430 | background-color: var(--background);
431 | border-radius: inherit;
432 | }
433 |
434 | /* Overlap reaction avatars when there are 5+ types of reactions */
435 | .rgh-reactions-near-limit .reaction-summary-item:not(:hover) a:not(:first-of-type) {
436 | margin-left: -12px;
437 | }
438 |
439 | /* Hide reaction popover text */
440 | .reaction-popover-form.js-pick-reaction span.js-reaction-description,
441 | .reaction-popover-form.js-pick-reaction .dropdown-divider {
442 | display: none;
443 | }
444 |
445 | /* Remove "Add your Reaction" tooltips */
446 | .reaction-popover-container .tooltipped::before,
447 | .reaction-popover-container .tooltipped::after {
448 | display: none !important;
449 | }
450 |
451 | /*
452 | Create invisible content area below/above reactions popover to increase hover target
453 | Center popup on add reactions button
454 | */
455 | .reaction-popover-container.dropdown .dropdown-menu-content {
456 | margin-top: -15px;
457 | position: absolute;
458 | left: -94px;
459 | width: 220px;
460 | height: 15px;
461 | }
462 |
463 | /*
464 | Remove margin-top from invisible hover target for popover below comment content
465 | Add some space below popup so it doesn't cover button
466 | */
467 | .comment-reactions .reaction-popover-container.dropdown .dropdown-menu-content {
468 | margin-top: 0;
469 | bottom: 20px;
470 | }
471 |
472 | /*
473 | Show reaction list when hovering over:
474 | - Popover
475 | - Add Reactions button
476 | Hidden content area created to increase hover target
477 | */
478 | .dropdown-menu.add-reaction-popover:hover,
479 | .reaction-popover-container.dropdown:hover .dropdown-menu-content,
480 | .dropdown-menu-content:hover {
481 | display: block;
482 | pointer-events: all;
483 | }
484 |
485 | /* Remove top/bottom margin from popovers so there's no gap with invisible hover element */
486 | .reaction-popover-container.dropdown .dropdown-menu.add-reaction-popover {
487 | margin-top: 0;
488 | margin-bottom: 0;
489 | }
490 |
491 | /* Never show loading spinner for reactions */
492 | .reaction-popover-container .reaction-popover-form .loading-spinner {
493 | display: none !important;
494 | }
495 |
496 | /* Move popup arrow point to center of popup */
497 | .reaction-popover-container.dropdown .dropdown-menu-sw.dropdown-menu::before,
498 | .reaction-popover-container.dropdown .dropdown-menu-sw.dropdown-menu::after,
499 | .reaction-popover-container.dropdown .dropdown-menu-ne.dropdown-menu::before,
500 | .reaction-popover-container.dropdown .dropdown-menu-ne.dropdown-menu::after {
501 | left: 102px; /* 220px / 2 - 8px for popover arrow */
502 | right: auto;
503 | }
504 |
505 | /* Add colored button when hovering over reaction popover or hidden hover element */
506 | .reaction-popover-container:hover .timeline-comment-action {
507 | color: #4078c0;
508 | text-decoration: none;
509 | opacity: 1;
510 | }
511 |
512 | /* Collapse multiple discussion items */
513 | .discussion-item + .discussion-item {
514 | padding-top: 0 !important;
515 | border-top: none !important;
516 | }
517 |
518 | /* Mark as unread */
519 | .btn-mark-unread {
520 | margin-top: 8px;
521 | }
522 |
523 | /* Change color of marker if there are only discussion marked as unread */
524 | .notification-indicator[data-ga-click$=":read"] .unread {
525 | background: linear-gradient(#34d058, #28a745)
526 | }
527 |
528 | /* +/- Pseudo elements on diffs */
529 | .refined-github-diff-signs .blob-code-addition:before,
530 | .refined-github-diff-signs .blob-code-deletion:before {
531 | display: inline-block;
532 | position: absolute;
533 | top: 1px;
534 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace !important;
535 | font-size: 11px;
536 | text-indent: -8px;
537 | color: rgba(0, 0, 0, 0.3);
538 | }
539 |
540 | .refined-github-diff-signs .blob-code-addition:before {
541 | content: '+';
542 | }
543 |
544 | .refined-github-diff-signs .blob-code-deletion:before {
545 | content: '-';
546 | }
547 |
548 | /* Prevent copy of ghost whitespace where supported (#317) */
549 | .refined-github-diff-signs .add-line-comment {
550 | -moz-user-select: none;
551 | user-select: none;
552 | }
553 |
554 | /* Restore removed space with unselectable one */
555 | .refined-github-diff-signs .blob-code-inner:before {
556 | content: ' ' !important;
557 | user-select: none;
558 | }
559 |
560 | /* Remove "pro tip!" box on profile page (appears when name isn't set) */
561 | .new-user-avatar-cta {
562 | display: none !important;
563 | }
564 |
565 |
566 | /* Fix PR diffbar links position #474 */
567 | .stale-files-tab,
568 | .subset-files-tab {
569 | font-size: 0 !important;
570 | }
571 | .subset-files-tab .stale-files-tab-link,
572 | .stale-files-tab .stale-files-tab-link {
573 | font-size: 14px;
574 | }
575 | .stale-files-tab-link:before {
576 | content: 'Out of date. ';
577 | font-weight: normal;
578 | }
579 | .stale-files-tab-link:before {
580 | content: 'Subset of changes. ';
581 | font-weight: normal;
582 | }
583 |
584 | /* Multiple comment labels margin */
585 | .timeline-comment .timeline-comment-label + .timeline-comment-label {
586 | margin-right: -5px;
587 | }
588 |
589 | .review-comment .timeline-comment-label + .timeline-comment-label {
590 | margin-left: 5px;
591 | }
592 |
593 | /* Move "close issue" and "cancel" buttons on authoring comments to the left */
594 |
595 | /* ...in issue comment form */
596 | .form-actions .btn.js-comment-and-button {
597 | float: left;
598 | }
599 |
600 | /* ...in comment edit form */
601 | div.previewable-edit .previewable-comment-form .form-actions {
602 | float: none;
603 | margin-left: 10px;
604 | }
605 |
606 | .previewable-edit .previewable-comment-form .form-actions .btn.js-comment-cancel-button {
607 | float: left;
608 | }
609 |
610 | /* ...in inline comment form */
611 | div.inline-comment-form .form-actions,
612 | .inline-comment-form .form-actions .js-hide-inline-comment-form {
613 | float: none;
614 | }
615 |
616 | /* "Add a Project" button */
617 | #refined-github-project-new-link {
618 | margin-top: 5px;
619 | }
620 |
621 | /* Decrease the size of the search box to fit 'Yours' menu item */
622 | .subnav-search-input[aria-label="Search all issues"] {
623 | width: 420px;
624 | }
625 |
626 | .CodeMirror-lines pre.CodeMirror-line {
627 | font-variant-ligatures: normal;
628 | }
629 |
630 | /* Remove Marketplace marketing box on PRs */
631 | .js-marketplace-callout-container {
632 | display: none !important;
633 | }
634 |
635 | /* Remove the "Styling with Markdown is supported" link when a PR comment is in edit mode */
636 | a.tabnav-extra[href$="mastering-markdown/"] {
637 | display: none !important;
638 | }
639 |
640 | /* Hide empty description of repo */
641 | .repository-meta.mb-3 > .repository-meta-content > em {
642 | display: none !important;
643 | }
644 |
645 | /* Hide marketplace navbar button */
646 | .HeaderNavlink[href="/marketplace"] {
647 | display: none !important;
648 | }
649 |
650 | /* Move labels in the Issue/PR list below the title */
651 | .js-issue-row .lh-condensed {
652 | display: flex;
653 | flex-wrap: wrap;
654 | }
655 | .js-issue-row .labels {
656 | order: 1;
657 | }
658 | .js-issue-row .h4, /* Title */
659 | .js-issue-row .d-inline-block.mr-1 { /* Build status */
660 | padding-right: 4px; /* Space before the build status */
661 | }
662 | .js-issue-row .mt-1.text-small.text-gray { /* Issue details line */
663 | flex-basis: 100%;
664 | }
665 | .js-issue-row .label {
666 | margin-top: 4px;
667 | font-size: 11px !important;
668 | padding: 2px 3px 3px 3px !important;
669 | }
670 |
671 | /* Fix for GHE More Dropdown positioning */
672 | .reponav-dropdown.active .dropdown-menu {
673 | left: auto !important;
674 | }
675 |
676 | /* Optically align Dependencies dropdown link*/
677 | .rgh-dependency-graph .octicon-package {
678 | margin-left: -2px;
679 | margin-right: 2px;
680 | }
681 |
682 | /* Hide PR/Issue rename events */
683 | .discussion-item-renamed {
684 | display: none;
685 | }
686 |
687 | /* For `add-ci-link` */
688 | .rgh-ci-link {
689 | position: relative;
690 | top: -0.1em;
691 | animation: fade-in 0.2s;
692 | }
693 |
694 | :root .repohead h1 .commit-build-statuses .octicon {
695 | position: static;
696 | color: inherit;
697 | }
698 |
699 | /* Sticky file headers on pull request diff view */
700 | .pull-request-tab-content .diff-view .file-header {
701 | position: sticky;
702 | top: 60px;
703 | z-index: 10;
704 | }
705 |
706 | /* Sticky file headers in regular file view */
707 | .file .file-header {
708 | position: sticky;
709 | top: 0;
710 | z-index: 10;
711 | }
712 |
713 | /* Remove annoying "helpful" banner on the issue tracker listing */
714 | .issues-listing .mb-4.js-notice {
715 | display: none !important;
716 | }
717 |
718 | /* Remove annoying "helpful" popover on the repo label page */
719 | .labels-list .TutorialPopover {
720 | display: none !important;
721 | }
722 |
723 | /* For the `add-time-machine-links-to-comments` feature */
724 | :root .rgh-timestamp-button {
725 | padding: 0;
726 | }
727 | :root .rgh-timestamp-button .octicon {
728 | vertical-align: middle;
729 | }
730 |
731 | /* Make the modal backdrop visible */
732 | :root body.menu-active .modal-backdrop,
733 | :root .dropdown-details[open] > summary::before {
734 | background: rgba(0, 0, 0, 0.1);
735 | z-index: 20;
736 | animation: fade-in 0.2s;
737 | }
738 | :root .pagehead ul.pagehead-actions {
739 | position: static; /* Demote watch/star/fork */
740 | }
741 |
742 | /* Reduce duplicate backdrop in PR diff view */
743 | :root .pr-toolbar .modal-backdrop {
744 | position: absolute;
745 | height: 100%;
746 | width: 100%;
747 | }
748 |
749 | @keyframes fade-in {
750 | from {
751 | opacity: 0;
752 | }
753 | }
754 |
755 | /* Fix subpixel rendering Chrome bug in tooltips' triangles */
756 | .tooltipped:hover::before,
757 | .tooltipped:hover::after,
758 | .tooltipped:active::before,
759 | .tooltipped:active::after,
760 | .tooltipped:focus::before,
761 | .tooltipped:focus::after {
762 | animation-fill-mode: backwards !important;
763 | opacity: 1;
764 | }
765 |
--------------------------------------------------------------------------------
/extension/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/refined-github/0498e8d4aee842c2e5e1a3dc586c0ed1af8270b3/extension/icon.png
--------------------------------------------------------------------------------
/extension/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Refined GitHub",
3 | "version": "0.0.0",
4 | "description": "Simplifies the GitHub interface and adds useful features",
5 | "homepage_url": "https://github.com/sindresorhus/refined-github",
6 | "manifest_version": 2,
7 | "minimum_chrome_version": "58",
8 | "applications": {
9 | "gecko": {
10 | "id": "{a4c4eda4-fb84-4a84-b4a1-f7c1cbf2a1ad}",
11 | "strict_min_version": "55.0"
12 | }
13 | },
14 | "permissions": [
15 | "storage",
16 | "clipboardWrite"
17 | ],
18 | "optional_permissions": [
19 | "http://*/*",
20 | "https://*/*"
21 | ],
22 | "icons": {
23 | "128": "icon.png"
24 | },
25 | "options_ui": {
26 | "chrome_style": true,
27 | "page": "options.html"
28 | },
29 | "background": {
30 | "scripts": [
31 | "browser-polyfill.min.js",
32 | "background.js"
33 | ],
34 | "persistent": false
35 | },
36 | "content_scripts": [
37 | {
38 | "run_at": "document_start",
39 | "matches": [
40 | "https://github.com/*",
41 | "https://gist.github.com/*"
42 | ],
43 | "css": [
44 | "content.css"
45 | ],
46 | "js": [
47 | "jquery.slim.min.js",
48 | "browser-polyfill.min.js",
49 | "content.js"
50 | ]
51 | }
52 | ]
53 | }
54 |
--------------------------------------------------------------------------------
/extension/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Refined GitHub options
4 |
14 |
23 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Sindre Sorhus (sindresorhus.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/media/icon.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/refined-github/0498e8d4aee842c2e5e1a3dc586c0ed1af8270b3/media/icon.ai
--------------------------------------------------------------------------------
/media/promo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/refined-github/0498e8d4aee842c2e5e1a3dc586c0ed1af8270b3/media/promo.png
--------------------------------------------------------------------------------
/media/screenshot-delete-fork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/refined-github/0498e8d4aee842c2e5e1a3dc586c0ed1af8270b3/media/screenshot-delete-fork.png
--------------------------------------------------------------------------------
/media/screenshot-reactions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/refined-github/0498e8d4aee842c2e5e1a3dc586c0ed1af8270b3/media/screenshot-reactions.png
--------------------------------------------------------------------------------
/media/screenshot-repo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/github/refined-github/0498e8d4aee842c2e5e1a3dc586c0ed1af8270b3/media/screenshot-repo.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "test": "xo && cross-env BABEL_ENV=testing ava && run-s build:minified",
4 | "build": "webpack",
5 | "build:minified": "cross-env NODE_ENV=production webpack",
6 | "watch": "webpack --watch",
7 | "release:amo": "cd extension && webext submit",
8 | "release:cws": "cd extension && webstore upload --auto-publish",
9 | "release": "run-s build:minified update-version release:*",
10 | "update-version": "VERSION=$(date -u +%y.%-m.%-d.%-H%M); echo $VERSION; dot-json extension/manifest.json version $VERSION",
11 | "can-release": "if [ \"$TRAVIS_EVENT_TYPE\" = cron ] && [ $(git rev-list -n 1 --since=\"26 hours ago\" master) ]; then echo :ship-it:; else false; fi"
12 | },
13 | "dependencies": {
14 | "copy-text-to-clipboard": "^1.0.3",
15 | "debounce-fn": "^1.0.0",
16 | "dom-chef": "^3.0.0",
17 | "dom-loaded": "^1.0.0",
18 | "element-ready": "^2.2.0",
19 | "github-injection": "^1.0.1",
20 | "github-reserved-names": "^1.0.6",
21 | "jquery": "^3.2.1",
22 | "linkify-issues": "^1.3.0",
23 | "linkify-urls": "^1.3.0",
24 | "onetime": "^2.0.1",
25 | "select-dom": "^4.1.0",
26 | "shorten-repo-url": "^1.1.0",
27 | "to-markdown": "^3.1.0",
28 | "to-semver": "^1.1.0",
29 | "webext-dynamic-content-scripts": "github:bfred-it/webext-dynamic-content-scripts#debug",
30 | "webext-options-sync": "^0.12.0",
31 | "webextension-polyfill": "^0.2.1"
32 | },
33 | "devDependencies": {
34 | "ava": "*",
35 | "babel-core": "^6.26.0",
36 | "babel-loader": "^7.1.2",
37 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
38 | "babel-plugin-transform-react-jsx": "^6.24.1",
39 | "chrome-webstore-upload-cli": "^1.0.0",
40 | "common-tags": "^1.4.0",
41 | "copy-webpack-plugin": "^4.2.0",
42 | "cross-env": "^5.0.5",
43 | "dot-json": "^1.0.3",
44 | "npm-run-all": "^4.1.1",
45 | "uglifyjs-webpack-plugin": "^1.0.0-beta.1",
46 | "webext": "^1.9.1-with-submit.1",
47 | "webpack": "^3.7.1",
48 | "xo": "*"
49 | },
50 | "xo": {
51 | "envs": [
52 | "browser",
53 | "jquery"
54 | ],
55 | "rules": {
56 | "import/no-unassigned-import": 0,
57 | "no-unused-vars": [
58 | 2,
59 | {
60 | "varsIgnorePattern": "^h$"
61 | }
62 | ]
63 | },
64 | "ignores": [
65 | "extension/**"
66 | ],
67 | "globals": [
68 | "browser"
69 | ]
70 | },
71 | "ava": {
72 | "files": [
73 | "test/*.js"
74 | ],
75 | "source": [
76 | "extension/*.js"
77 | ],
78 | "require": [
79 | "babel-register"
80 | ]
81 | },
82 | "babel": {
83 | "plugins": [
84 | [
85 | "transform-react-jsx",
86 | {
87 | "pragma": "h",
88 | "useBuiltIns": true
89 | }
90 | ]
91 | ],
92 | "env": {
93 | "testing": {
94 | "plugins": [
95 | "transform-es2015-modules-commonjs"
96 | ]
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Refined GitHub [![Chrome version][badge-cws]][link-cws] [![Firefox version][badge-amo]][link-amo] [![Deployment][badge-travis]][link-travis]
2 |
3 | [badge-cws]: https://img.shields.io/chrome-web-store/v/hlepfoohegkhhmjieoechaddaejaokhf.svg?label=chrome
4 | [badge-amo]: https://img.shields.io/amo/v/refined-github-.svg?label=firefox
5 | [badge-travis]: https://img.shields.io/travis/sindresorhus/refined-github/master.svg?label=deployment
6 | [link-cws]: https://chrome.google.com/webstore/detail/refined-github/hlepfoohegkhhmjieoechaddaejaokhf "Version published on Chrome Web Store"
7 | [link-amo]: https://addons.mozilla.org/en-US/firefox/addon/refined-github-/ "Version published on Mozilla Add-ons"
8 | [link-travis]: https://travis-ci.org/sindresorhus/refined-github
9 |
10 | > Browser extension that simplifies the GitHub interface and adds useful features
11 |
12 | **Discuss it on [Product Hunt](https://www.producthunt.com/posts/refined-github)** 🦄
13 |
14 | We use GitHub a lot and notice many dumb annoyances we'd like to fix. So here be dragons.
15 |
16 | Our hope is that GitHub will notice and implement some of these much needed improvements. So if you like any of these improvements, please email [GitHub support](mailto:support@github.com) about doing it.
17 |
18 | GitHub Enterprise is also supported by [authorizing your own domain in the options](https://github.com/sindresorhus/refined-github/pull/450).
19 |
20 | - **[What's new lately](https://blog.sindresorhus.com/whats-new-in-refined-github-836d05582df7)**
21 | - [Original announcement](https://blog.sindresorhus.com/refined-github-21185789685d)
22 |
23 |
24 | ## Install
25 |
26 | - [**Chrome** extension][link-cws]
27 | - [**Firefox** add-on][link-amo]
28 | - Opera - Use [this Opera extension](https://addons.opera.com/en/extensions/details/download-chrome-extension-9/) to install the Chrome version.
29 |
30 | ## Highlights
31 |
32 |
33 |
34 |
35 | Dashboard cleanup
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Mark issues and pull requests as unread
50 | (They will reappear in Notifications)
51 |
52 |
53 | Preserves the original Markdown when you copy text from comments
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Reaction avatars
71 |
72 |
73 | Moves destructive buttons in commenting forms away from the primary button
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Linkifies issue/PR references in code, comments and titles
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | ### New Features
106 |
107 | - Copy canonical link to file when [the y hotkey](https://help.github.com/articles/getting-permanent-links-to-files/) is used
108 | - Supports indenting with the tab key in textareas like the comment box (Shift Tab for original behavior)
109 | - [Uses the pull request title as commit title when merging with 'Squash and merge'](https://github.com/sindresorhus/refined-github/issues/276)
110 |
111 | ### More actions
112 |
113 | - [Linkifies branch references in pull requests](https://github.com/sindresorhus/refined-github/issues/1)
114 | - [Adds a quick edit button and a link to the latest release to the readme](https://user-images.githubusercontent.com/170270/27501200-31a1fa20-586c-11e7-9a3f-ce270014bf0a.png)
115 | - [Adds a shortcut to quickly delete a forked repo](https://cloud.githubusercontent.com/assets/170270/13520281/b2c9335c-e211-11e5-9e36-b0f325166356.png)
116 | - [Adds a button to open all notifications at once](https://user-images.githubusercontent.com/1402241/31700005-1b3be428-b38c-11e7-90a6-8f572968993b.png)
117 | - [Adds option to view diffs without whitespace changes](https://cloud.githubusercontent.com/assets/170270/17603894/7b71a166-6013-11e6-81b8-22950ab8bce3.png) *(d w hotkey)*
118 | - [Adds a 'Copy' button to the file view](https://cloud.githubusercontent.com/assets/170270/14453865/8abeaefe-00c1-11e6-8718-9406cee1dc0d.png)
119 | - [Adds `Copy` button to gist files](https://cloud.githubusercontent.com/assets/170270/21074840/5dc37578-bf03-11e6-9fd9-501d73edef87.png)
120 | - [Adds `Copy` button for file paths to pull request diffs](https://cloud.githubusercontent.com/assets/4201088/26023064/18c9c77c-37d2-11e7-8926-b0a05a2706ae.png)
121 | - [Adds links to patch and diff for each commit](https://cloud.githubusercontent.com/assets/737065/13605562/22faa79e-e516-11e5-80db-2da6aa7965ac.png)
122 | - [Adds links to view the repo at the time of each comment](https://user-images.githubusercontent.com/1402241/32310022-7fef6174-bf5d-11e7-960f-5041a8f073ac.png)
123 |
124 | ### More info at a glance
125 |
126 | - [Makes file headers sticky underneath the pull request header](https://user-images.githubusercontent.com/81981/28682784-78bac340-72fe-11e7-9386-bdbab7703693.gif)
127 | - [Shows user's full name in comments](https://cloud.githubusercontent.com/assets/170270/16172068/0a67b98c-3580-11e6-92f0-6fc930ee17d1.png)
128 | - [Differentiates merge commits from regular commits](https://cloud.githubusercontent.com/assets/170270/14101222/2fe2c24a-f5bd-11e5-8b1f-4e589917d4c4.png)
129 | - [Adds labels to comments by the original poster](https://cloud.githubusercontent.com/assets/4331946/25075520/d62fbbd0-2316-11e7-921f-ab736dc3522e.png)
130 | - [Adds build status and link to CI by the repo's title](https://user-images.githubusercontent.com/1402241/32562120-d65166e4-c4e8-11e7-90fb-cbaf36e2709f.png)
131 |
132 | ### Declutter
133 |
134 | - Hides other users starring/forking your repos from the news feed ([optional](https://user-images.githubusercontent.com/1402241/27267240-9d2e18c8-54d9-11e7-8a64-971af9e066f3.png))
135 | - Moves the dashboard organization switcher to the right column
136 | - Removes annoying hover effect in the repo file browser
137 | - Removes the comment box toolbar
138 | - Removes tooltips
139 | - Removes the "Projects" repo tab when there are no projects (New projects can be created on the "Settings" tab)
140 |
141 | ### UI improvements
142 |
143 | - [Improves readability of tab indented code](https://cloud.githubusercontent.com/assets/170270/14170088/d3be931e-f755-11e5-8edf-c5f864336382.png)
144 | - [Aligns labels to the left in Issues and PRs lists](https://user-images.githubusercontent.com/1402241/28006237-070b8214-6581-11e7-94bc-2b01a007d00b.png)
145 | - Prompts you when pressing `Cancel` on an inline comment in case it was a mistake
146 | - Easier copy-pasting from diffs by making +/- signs unselectable
147 | - Automagically expands the news feed when you scroll down
148 | - Shows the reactions popover on hover instead of click
149 | - Changes the default sort order of milestones to "Closest due date"
150 |
151 | And [lots](extension/content.css) [more...](src/content.js)
152 |
153 | ### More shortcuts
154 |
155 | - [Adds a 'Releases' tab to repos](https://cloud.githubusercontent.com/assets/170270/13136797/16d3f0ea-d64f-11e5-8a45-d771c903038f.png) *(g r hotkey)*
156 | - [Adds a 'Compare' tab to repos](https://user-images.githubusercontent.com/170270/27501134-cf0a2a18-586b-11e7-8430-22f33030e923.png)
157 | - [Adds navigation to milestone pages](https://cloud.githubusercontent.com/assets/170270/25217211/37b67aea-25d0-11e7-8482-bead2b04ee74.png)
158 | - [Adds search filter for 'Everything commented by you'](https://user-images.githubusercontent.com/170270/27501170-f394a304-586b-11e7-92d8-d92d6922356b.png)
159 | - [Adds `Yours` button to Issues/Pull Requests page](https://user-images.githubusercontent.com/170270/27501189-1b914bbe-586c-11e7-8e1e-b3ffd8b767fa.png)
160 | - [Condenses long URLs into references like _user/repo/.file@`d71718d`_](https://user-images.githubusercontent.com/1402241/27252232-8fdf8ed0-538b-11e7-8f19-12d317c9cd32.png)
161 | - Adds a `Trending` link to the global navbar. *(g t hotkey)*
162 | - Adds a keyboard shortcut to leave a single comment in PR diffs instead of starting a review. *(shift +enter )*
163 | - Adds a keyboard shortcut to visit your profile. *(g m hotkey)*
164 |
165 | ### Previously part of Refined GitHub
166 |
167 | - ~~[Adds blame links for parent commits in blame view](https://github.com/sindresorhus/refined-github/issues/2#issuecomment-189141373)~~ [Implemented by GitHub](https://github.com/blog/2304-navigate-file-history-faster-with-improved-blame-view)
168 | - ~~[Adds ability to collapse/expand files in a pull request diff](https://cloud.githubusercontent.com/assets/170270/13954167/40caa604-f072-11e5-89ba-3145217c4e28.png)~~ [Implemented by GitHub](https://cloud.githubusercontent.com/assets/170270/25772137/6a6b678e-3296-11e7-97c7-02e31ef17743.png)
169 | - ~~[Adds issue/PR title preview tooltip in comments](https://user-images.githubusercontent.com/170270/30729486-2816df06-9f8a-11e7-8069-8999302e9ddd.png)~~ [Implemented by GitHub](https://user-images.githubusercontent.com/1402241/31265633-779ad0fe-aa35-11e7-8c42-a3b375f8f32c.png)
170 |
171 |
172 | ### Community tweaks
173 |
174 | *Stuff that didn't get included, but might be useful.*
175 |
176 | - [Quickly edit files in the repo file browser](https://github.com/devkhan/refined-github/commit/51fdf4998fc9392950e932e18018fda870f34666)
177 |
178 | ## Customization
179 |
180 | We're happy to receive suggestions and contributions, but be aware this is a highly opinionated project. There's [a single commonly-requested option](https://user-images.githubusercontent.com/1402241/27267240-9d2e18c8-54d9-11e7-8a64-971af9e066f3.png) but we're not interested in adding more as it's a slippery slope into adding one for everything. Users will always disagree with something. That being said, we're open to discussing things.
181 |
182 | While this project is highly opinionated, this doesn't necessarily limit you from manually disabling functionality that is not useful for your workflow. Options include:
183 |
184 | 1. *(CSS Only)* Use a Chrome extension that allows injecting custom styles into sites, based on a URL pattern. [Stylist](https://chrome.google.com/webstore/detail/stylish/fjnbnpbmkenffdnngjfgmeleoegfcffe?hl=en) is one such tool. [Example](https://github.com/sindresorhus/refined-github/issues/136#issuecomment-204072018)
185 |
186 | 2. Clone the repository, make the adjustments you need, and [load the unpacked extension in Chrome](https://developer.chrome.com/extensions/getstarted#unpacked), rather than installing from the Chrome Store.
187 |
188 |
189 | ## Contribute
190 |
191 | Suggestions and pull requests are highly encouraged!
192 |
193 | In order to make modifications to the extension you'd need to run it locally.
194 | Please follow the steps below:
195 |
196 | ```sh
197 | git clone git@github.com:sindresorhus/refined-github
198 | cd refined-github
199 | npm install # Install dev dependencies
200 | npm run build # Build the extension code so it's ready for the browser
201 | npm run watch # Listen for file changes and automatically rebuild
202 | ```
203 |
204 | Once built, load it in the browser of your choice:
205 |
206 |
207 |
208 | Chrome
209 | Firefox
210 |
211 |
212 |
213 |
214 | Open chrome://extensions
215 | Check the Developer mode checkbox
216 | Click on the Load unpacked extension button
217 | Select the folder refined-github/extension
218 |
219 |
220 |
221 |
222 | Open about:debugging#addons
223 | Click on the Load Temporary Add-on button
224 | Select the file refined-github/extension/manifest.json
225 |
226 |
227 |
228 |
229 |
230 | ## Related
231 |
232 | - [Refined Wikipedia](https://github.com/ismamz/refined-wikipedia) - Like this, but for Wikipedia
233 | - [Notifier for GitHub](https://github.com/sindresorhus/notifier-for-github-chrome) - Shows your notification unread count
234 | - [Hide Files on GitHub](https://github.com/sindresorhus/hide-files-on-github) - Hides dotfiles from the file browser
235 | - [Show All GitHub Issues](https://github.com/sindresorhus/show-all-github-issues) - Shows both Issues and Pull Requests in the Issues tab
236 | - [Contributors on GitHub](https://github.com/hzoo/contributors-on-github) - Shows stats about contributors
237 | - [Twitter for GitHub](https://github.com/bevacqua/twitter-for-github) - Shows a user's Twitter handle on their profile page
238 | - [GifHub](https://github.com/DrewML/GifHub) - Quickly insert GIFs in comments
239 | - [Octo Linker](https://github.com/octo-linker/chrome-extension/) - Navigate across files and packages
240 | - [Awesome browser extensions for GitHub](https://github.com/stefanbuck/awesome-browser-extensions-for-github) - Awesome list
241 | - [OctoEdit](https://github.com/DrewML/OctoEdit) - Markdown syntax highlighting in comments
242 | - [GitHub Clean Feed](https://github.com/bfred-it/github-clean-feed) - Group news feed events by repo
243 | - [Do Not Merge WIP for GitHub](https://github.com/sanemat/do-not-merge-wip-for-github) - Prevents merging of incomplete PRs
244 | - [GitHub Custom Tab Size](https://github.com/lukechilds/github-custom-tab-size) - Set custom tab size for code views *(Refined GitHub hard-codes it to 4)*
245 |
246 | Want more? Here are some ideas you could develop!
247 |
248 | - [Notification Previews for GitHub](https://github.com/sindresorhus/module-requests/issues/100)
249 | - [Format JS code blocks with Prettier](https://github.com/sindresorhus/module-requests/issues/99)
250 | - [Customize the font of code blocks](https://github.com/sindresorhus/module-requests/issues/97)
251 |
252 |
253 | ## Created by
254 |
255 | - [Sindre Sorhus](https://github.com/sindresorhus)
256 | - [Haralan Dobrev](https://github.com/hkdobrev)
257 | - [Paul Molluzzo](https://github.com/paulmolluzzo)
258 | - [Andrew Levine](https://github.com/DrewML)
259 | - [Kees Kluskens](https://github.com/SpaceK33z)
260 | - [Jonas Gierer](https://github.com/jgierer12)
261 | - [Federico Brigante](https://github.com/bfred-it)
262 | - [Scott Busche](https://github.com/busches)
263 | - [Contributors…](https://github.com/sindresorhus/refined-github/graphs/contributors)
264 |
265 |
266 | ## License
267 |
268 | MIT
269 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | import OptionsSync from 'webext-options-sync';
2 | import injectContentScripts from 'webext-dynamic-content-scripts';
3 |
4 | // Define defaults
5 | new OptionsSync().define({
6 | defaults: {
7 | hideStarsOwnRepos: true
8 | },
9 | migrations: [
10 | OptionsSync.migrations.removeUnused
11 | ]
12 | });
13 |
14 | browser.runtime.onMessage.addListener(async message => {
15 | if (!message || message.action !== 'openAllInTabs') {
16 | return;
17 | }
18 | const [currentTab] = await browser.tabs.query({currentWindow: true, active: true});
19 | for (const [i, url] of message.urls.entries()) {
20 | browser.tabs.create({
21 | url,
22 | index: currentTab.index + i + 1,
23 | active: false
24 | });
25 | }
26 | });
27 |
28 | // GitHub Enterprise support
29 | injectContentScripts();
30 |
--------------------------------------------------------------------------------
/src/content.js:
--------------------------------------------------------------------------------
1 | import 'webext-dynamic-content-scripts';
2 | import onAjaxedPages from 'github-injection';
3 | import {applyToLink as shortenLink} from 'shorten-repo-url';
4 | import select from 'select-dom';
5 | import domLoaded from 'dom-loaded';
6 |
7 | import markUnread from './features/mark-unread';
8 | import addOpenAllNotificationsButton from './features/open-all-notifications';
9 | import addUploadBtn from './features/upload-button';
10 | import enableCopyOnY from './features/copy-on-y';
11 | import addReactionParticipants from './features/reactions-avatars';
12 | import showRealNames from './features/show-names';
13 | import addCopyFilePathToPRs from './features/copy-file-path';
14 | import addFileCopyButton from './features/copy-file';
15 | // - import copyMarkdown from './features/copy-markdown';
16 | import linkifyCode from './features/linkify-urls-in-code';
17 | import autoLoadMoreNews from './features/auto-load-more-news';
18 | import addOPLabels from './features/op-labels';
19 | import addMoreDropdown from './features/more-dropdown';
20 | import addReleasesTab from './features/add-releases-tab';
21 | import addTimeMachineLinksToComments from './features/add-time-machine-links-to-comments';
22 | import removeUploadFilesButton from './features/remove-upload-files-button';
23 | import scrollToTopOnCollapse from './features/scroll-to-top-on-collapse';
24 | import removeDiffSigns from './features/remove-diff-signs';
25 | import * as linkifyBranchRefs from './features/linkify-branch-refs';
26 | import hideEmptyMeta from './features/hide-empty-meta';
27 | import hideOwnStars from './features/hide-own-stars';
28 | import moveMarketplaceLinkToProfileDropdown from './features/move-marketplace-link-to-profile-dropdown';
29 | import addTrendingMenuItem from './features/add-trending-menu-item';
30 | import addProfileHotkey from './features/add-profile-hotkey';
31 | import addYoursMenuItem from './features/add-yours-menu-item';
32 | import addReadmeButtons from './features/add-readme-buttons';
33 | import addDeleteForkLink from './features/add-delete-fork-link';
34 | import linkifyIssuesInTitles from './features/linkify-issues-in-titles';
35 | import addPatchDiffLinks from './features/add-patch-diff-links';
36 | import markMergeCommitsInList from './features/mark-merge-commits-in-list';
37 | import showRecentlyPushedBranches from './features/show-recently-pushed-branches';
38 | import addDiffViewWithoutWhitespaceOption from './features/add-diff-view-without-whitespace-option';
39 | import preserveWhitespaceOptionInNav from './features/preserve-whitespace-option-in-nav';
40 | import addMilestoneNavigation from './features/add-milestone-navigation';
41 | import addFilterCommentsByYou from './features/add-filter-comments-by-you';
42 | import addProjectNewLink from './features/add-project-new-link';
43 | import removeProjectsTab from './features/remove-projects-tab';
44 | import fixSquashAndMergeTitle from './features/fix-squash-and-merge-title';
45 | import addTitleToEmojis from './features/add-title-to-emojis';
46 | import sortMilestonesByClosestDueDate from './features/sort-milestones-by-closest-due-date';
47 | import moveAccountSwitcherToSidebar from './features/move-account-switcher-to-sidebar';
48 | import openCIDetailsInNewTab from './features/open-ci-details-in-new-tab';
49 | import focusConfirmationButtons from './features/focus-confirmation-buttons';
50 | import addKeyboardShortcutsToCommentFields from './features/add-keyboard-shortcuts-to-comment-fields';
51 | import addConfirmationToCommentCancellation from './features/add-confirmation-to-comment-cancellation';
52 | import addCILink from './features/add-ci-link';
53 |
54 | import * as pageDetect from './libs/page-detect';
55 | import {observeEl, safeElementReady, safely} from './libs/utils';
56 |
57 | // Add globals for easier debugging
58 | window.$ = $;
59 | window.select = select;
60 |
61 | async function init() {
62 | await safeElementReady('body');
63 | if (document.body.classList.contains('logged-out')) {
64 | return;
65 | }
66 |
67 | if (select.exists('html.refined-github')) {
68 | console.error(`
69 | ❤️💛💚💙💜❤️💛💚💙💜❤️💛💚💙💜❤️💛💚💙💜
70 | Minor bug in Refined GitHub, but we need your help to fix it:
71 | https://github.com/sindresorhus/refined-github/issues/565
72 |
73 | We'll need to know:
74 |
75 | 1. Are you running two extensions at once? Chrome Web Store + development. If so, just disable one of them.
76 | 2. Are you on GitHub Enteprise?
77 | 3. The content of the console of this page.
78 | 4. The content of the console of the background page after enabling the Developer mode in the Extensions page: https://i.imgur.com/zjIngb4.png
79 |
80 | Thank you! 🎉
81 | ❤️💛💚💙💜❤️💛💚💙💜❤️💛💚💙💜❤️💛💚💙💜`);
82 | return;
83 | }
84 |
85 | document.documentElement.classList.add('refined-github');
86 |
87 | if (!pageDetect.isGist()) {
88 | safely(addTrendingMenuItem);
89 | }
90 |
91 | if (pageDetect.isDashboard()) {
92 | safely(moveAccountSwitcherToSidebar);
93 | }
94 |
95 | safely(focusConfirmationButtons);
96 | safely(addKeyboardShortcutsToCommentFields);
97 | safely(addConfirmationToCommentCancellation);
98 |
99 | // TODO: Enable this when we've improved how copying Markdown works
100 | // See #522
101 | // $(document).on('copy', '.markdown-body', copyMarkdown);
102 |
103 | domLoaded.then(onDomReady);
104 | }
105 |
106 | function onDomReady() {
107 | safely(markUnread.setup);
108 | safely(addOpenAllNotificationsButton);
109 | safely(addProfileHotkey);
110 |
111 | if (!pageDetect.isGist()) {
112 | safely(moveMarketplaceLinkToProfileDropdown);
113 | }
114 |
115 | if (pageDetect.isGist()) {
116 | safely(addFileCopyButton);
117 | }
118 |
119 | if (pageDetect.isDashboard()) {
120 | safely(hideOwnStars);
121 | safely(autoLoadMoreNews);
122 | }
123 |
124 | onAjaxedPages(ajaxedPagesHandler);
125 | }
126 |
127 | function ajaxedPagesHandler() {
128 | safely(hideEmptyMeta);
129 | safely(removeUploadFilesButton);
130 | safely(addTitleToEmojis);
131 | safely(enableCopyOnY.destroy);
132 |
133 | safely(() => {
134 | for (const a of select.all('a[href]')) {
135 | shortenLink(a, location.href);
136 | }
137 | });
138 |
139 | safely(linkifyCode); // Must be after link shortening #789
140 |
141 | if (pageDetect.isIssueSearch() || pageDetect.isPRSearch()) {
142 | safely(addYoursMenuItem);
143 | }
144 |
145 | if (pageDetect.isMilestone()) {
146 | safely(addMilestoneNavigation); // Needs to be before sortMilestonesByClosestDueDate
147 | }
148 |
149 | if (pageDetect.isRepo()) {
150 | safely(addMoreDropdown);
151 | safely(addReleasesTab);
152 | safely(removeProjectsTab);
153 | safely(addReadmeButtons);
154 | safely(addDiffViewWithoutWhitespaceOption);
155 | safely(removeDiffSigns);
156 | safely(addCILink);
157 | safely(sortMilestonesByClosestDueDate); // Needs to be after addMilestoneNavigation
158 | }
159 |
160 | if (pageDetect.isPR()) {
161 | safely(scrollToTopOnCollapse);
162 | safely(linkifyBranchRefs.inPR);
163 | safely(addDeleteForkLink);
164 | safely(fixSquashAndMergeTitle);
165 | safely(openCIDetailsInNewTab);
166 | }
167 |
168 | if (pageDetect.isQuickPR()) {
169 | safely(linkifyBranchRefs.inQuickPR);
170 | }
171 |
172 | if (pageDetect.isPR() || pageDetect.isIssue()) {
173 | safely(linkifyIssuesInTitles);
174 | safely(addUploadBtn);
175 |
176 | observeEl('.new-discussion-timeline', () => {
177 | safely(addOPLabels);
178 | safely(addTimeMachineLinksToComments);
179 | });
180 | }
181 |
182 | if (pageDetect.isPRList() || pageDetect.isIssueList()) {
183 | safely(addFilterCommentsByYou);
184 | safely(showRecentlyPushedBranches);
185 | }
186 |
187 | if (pageDetect.isCommit()) {
188 | safely(addPatchDiffLinks);
189 | }
190 |
191 | if (pageDetect.isPR() || pageDetect.isIssue() || pageDetect.isCommit()) {
192 | safely(addReactionParticipants);
193 | safely(showRealNames);
194 | }
195 |
196 | if (pageDetect.isCommitList()) {
197 | safely(markMergeCommitsInList);
198 | }
199 |
200 | if (pageDetect.isPRFiles() || pageDetect.isPRCommit()) {
201 | safely(addCopyFilePathToPRs);
202 | safely(preserveWhitespaceOptionInNav);
203 | }
204 |
205 | if (pageDetect.isSingleFile()) {
206 | safely(addFileCopyButton);
207 | safely(enableCopyOnY.setup);
208 | }
209 |
210 | if (pageDetect.isRepoSettings()) {
211 | safely(addProjectNewLink);
212 | }
213 | }
214 |
215 | init();
216 |
--------------------------------------------------------------------------------
/src/features/add-ci-link.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import domify from '../libs/domify';
3 | import {getRepoURL} from '../libs/page-detect';
4 |
5 | // This var will be:
6 | // - undefined on first load
7 | // - a Promised dom element after a successful fetch
8 | // - false after a failed fetch
9 | let request;
10 |
11 | async function fetchStatus() {
12 | const url = `${location.origin}/${getRepoURL()}/commits/`;
13 | const dom = await fetch(url, {
14 | credentials: 'include'
15 | }).then(r => r.text()).then(domify);
16 |
17 | const icon = select('.commit-build-statuses', dom);
18 |
19 | // This will error if the element isn't found.
20 | // It's caught later.
21 | icon.classList.add('rgh-ci-link');
22 |
23 | return icon;
24 | }
25 |
26 | export default async function () {
27 | // Avoid duplicates and avoid on pages that already failed to load
28 | if (request === false || select.exists('.rgh-ci-link')) {
29 | return;
30 | }
31 |
32 | try {
33 | if (request) {
34 | // Skip icon re-animation because
35 | // it was probably already animated once
36 | (await request).style.animation = 'none';
37 | } else {
38 | request = fetchStatus();
39 | }
40 | select('.pagehead h1').append(await request);
41 | } catch (err) {
42 | // Network failure or no CI status found.
43 | // Don’t try again
44 | request = false;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/features/add-confirmation-to-comment-cancellation.js:
--------------------------------------------------------------------------------
1 | // Prompt user to confirm erasing a comment with the Cancel button
2 | export default function () {
3 | $(document).on('click', '.js-hide-inline-comment-form', event => {
4 | // Do not prompt if textarea is empty
5 | const textarea = event.target.closest('.js-inline-comment-form').querySelector('.js-comment-field');
6 | if (textarea.value === '') {
7 | return;
8 | }
9 |
10 | if (window.confirm('Are you sure you want to discard your unsaved changes?') === false) { // eslint-disable-line no-alert
11 | event.stopPropagation();
12 | event.stopImmediatePropagation();
13 | }
14 | });
15 | }
16 |
--------------------------------------------------------------------------------
/src/features/add-delete-fork-link.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 |
5 | const repoUrl = pageDetect.getRepoURL();
6 |
7 | export default function () {
8 | const postMergeDescription = select('#partial-pull-merging .merge-branch-description');
9 |
10 | if (postMergeDescription) {
11 | const currentBranch = postMergeDescription.querySelector('.commit-ref');
12 | const forkPath = currentBranch ? currentBranch.title.split(':')[0] : null;
13 |
14 | if (forkPath && forkPath !== repoUrl) {
15 | postMergeDescription.append(
16 |
17 | Delete fork
18 |
19 | );
20 | }
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/features/add-diff-view-without-whitespace-option.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as icons from '../libs/icons';
4 |
5 | export default function () {
6 | const container = select([
7 | '.table-of-contents.Details .BtnGroup', // In single commit view
8 | '.pr-review-tools > .diffbar-item' // In review view
9 | ].join(','));
10 |
11 | if (!container || select.exists('.refined-github-toggle-whitespace')) {
12 | return;
13 | }
14 |
15 | const url = new URL(location.href);
16 | const hidingWhitespace = url.searchParams.get('w') === '1';
17 |
18 | if (hidingWhitespace) {
19 | url.searchParams.delete('w');
20 | } else {
21 | url.searchParams.set('w', 1);
22 | }
23 |
24 | container.after(
25 |
35 | );
36 |
37 | // Make space for the new button by removing "Changes from" #655
38 | select('[data-hotkey="c"]').firstChild.remove();
39 | }
40 |
--------------------------------------------------------------------------------
/src/features/add-filter-comments-by-you.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 | import {getUsername} from '../libs/utils';
5 |
6 | const repoUrl = pageDetect.getRepoURL();
7 |
8 | export default function () {
9 | if (select.exists('.refined-github-filter')) {
10 | return;
11 | }
12 | select('.subnav-search-context .js-navigation-item:last-child')
13 | .before(
14 |
21 | );
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/src/features/add-keyboard-shortcuts-to-comment-fields.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | function indentInput(el, size = 4) {
4 | const selection = window.getSelection().toString();
5 | const {selectionStart, selectionEnd, value} = el;
6 | const isMultiLine = /\n/.test(selection);
7 | const firstLineStart = value.lastIndexOf('\n', selectionStart) + 1;
8 |
9 | el.focus();
10 |
11 | if (isMultiLine) {
12 | const selectedLines = value.substring(firstLineStart, selectionEnd);
13 |
14 | // Find the start index of each line
15 | const indexes = selectedLines.split('\n').map(line => line.length);
16 | indexes.unshift(firstLineStart);
17 | indexes.pop();
18 |
19 | // `indexes` contains lengths. Update them to point to each line start index
20 | for (let i = 1; i < indexes.length; i++) {
21 | indexes[i] += indexes[i - 1] + 1;
22 | }
23 |
24 | for (let i = indexes.length - 1; i >= 0; i--) {
25 | el.setSelectionRange(indexes[i], indexes[i]);
26 | document.execCommand('insertText', false, ' '.repeat(size));
27 | }
28 |
29 | // Restore selection position
30 | el.setSelectionRange(
31 | selectionStart + size,
32 | selectionEnd + (size * indexes.length)
33 | );
34 | } else {
35 | const indentSize = (size - ((selectionEnd - firstLineStart) % size)) || size;
36 | document.execCommand('insertText', false, ' '.repeat(indentSize));
37 | }
38 | }
39 |
40 | export default function () {
41 | $(document).on('keydown', '.js-comment-field', event => {
42 | const field = event.target;
43 | if (event.key === 'Tab' && !event.shiftKey) {
44 | // Don't indent if the suggester box is active
45 | if (select.exists('.suggester.active')) {
46 | return;
47 | }
48 |
49 | indentInput(field);
50 | return false;
51 | } else if (event.key === 'Enter' && event.shiftKey) {
52 | const singleCommentButton = select('.review-simple-reply-button', field.form);
53 |
54 | if (singleCommentButton) {
55 | singleCommentButton.click();
56 | return false;
57 | }
58 | } else if (event.key === 'Escape') {
59 | const cancelButton = select('.js-hide-inline-comment-form', field.form);
60 |
61 | if (field.value !== '' && cancelButton) {
62 | cancelButton.click();
63 | return false;
64 | }
65 | }
66 | });
67 | }
68 |
--------------------------------------------------------------------------------
/src/features/add-milestone-navigation.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 |
5 | const repoUrl = pageDetect.getRepoURL();
6 |
7 | export default function () {
8 | select('.repository-content').before(
9 |
15 | );
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/src/features/add-patch-diff-links.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 |
5 | export default function () {
6 | if (select.exists('.sha-block.patch-diff-links')) {
7 | return;
8 | }
9 |
10 | let commitUrl = location.pathname.replace(/\/$/, '');
11 |
12 | if (pageDetect.isPRCommit()) {
13 | commitUrl = commitUrl.replace(/\/pull\/\d+\/commits/, '/commit');
14 | }
15 |
16 | select('.commit-meta span.float-right').append(
17 |
18 | patch
19 | { ' ' /* Workaround for: JSX eats whitespace between elements */ }
20 | diff
21 |
22 | );
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/src/features/add-profile-hotkey.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {getUsername} from '../libs/utils';
3 |
4 | export default function () {
5 | const menuItem = select(`#user-links a.dropdown-item[href="/${getUsername()}"]`);
6 |
7 | if (menuItem) {
8 | menuItem.setAttribute('data-hotkey', 'g m');
9 | }
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/features/add-project-new-link.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 |
5 | const repoUrl = pageDetect.getRepoURL();
6 |
7 | export default function () {
8 | if (select.exists('#projects-feature:checked') && !select.exists('#refined-github-project-new-link')) {
9 | select('#projects-feature ~ p.note').after(
10 | Add a project
11 | );
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/features/add-readme-buttons.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import toSemver from 'to-semver';
4 | import * as icons from '../libs/icons';
5 | import * as pageDetect from '../libs/page-detect';
6 |
7 | const repoUrl = pageDetect.getRepoURL();
8 |
9 | export default function () {
10 | const readmeContainer = select('.repository-content > #readme');
11 | if (!readmeContainer) {
12 | return;
13 | }
14 |
15 | const buttons =
;
16 |
17 | /**
18 | * Generate Release button
19 | */
20 | const tags = select.all('.branch-select-menu [data-tab-filter="tags"] .select-menu-item')
21 | .map(element => [
22 | element.getAttribute('data-name'),
23 | element.getAttribute('href')
24 | ]);
25 | const releases = new Map(tags);
26 | const [latestRelease] = toSemver([...releases.keys()], {clean: false});
27 | if (latestRelease) {
28 | buttons.appendChild(
29 |
33 | {icons.tag()}
34 |
35 | );
36 | }
37 |
38 | /**
39 | * Generate Edit button
40 | */
41 | if (select('.branch-select-menu i').textContent === 'Branch:') {
42 | const readmeName = select('#readme > h3').textContent.trim();
43 | const path = select('.breadcrumb').textContent.trim().split('/').slice(1).join('/');
44 | const currentBranch = select('.branch-select-menu .select-menu-item.selected').textContent.trim();
45 | buttons.appendChild(
46 |
47 | {icons.edit()}
48 |
49 | );
50 | }
51 |
52 | readmeContainer.appendChild(buttons);
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/src/features/add-releases-tab.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 | import * as icons from '../libs/icons';
4 | import * as pageDetect from '../libs/page-detect';
5 |
6 | const repoUrl = pageDetect.getRepoURL();
7 |
8 | function appendReleasesCount(count) {
9 | if (!count) {
10 | return;
11 | }
12 |
13 | select('.reponav-releases').append({count} );
14 | }
15 |
16 | async function cacheReleasesCount() {
17 | const releasesCountCacheKey = `${repoUrl}-releases-count`;
18 |
19 | if (pageDetect.isRepoRoot()) {
20 | const releasesCount = select('.numbers-summary a[href$="/releases"] .num').textContent.trim();
21 | appendReleasesCount(releasesCount);
22 | browser.storage.local.set({[releasesCountCacheKey]: releasesCount});
23 | } else {
24 | const items = await browser.storage.local.get(releasesCountCacheKey);
25 |
26 | appendReleasesCount(items[releasesCountCacheKey]);
27 | }
28 | }
29 |
30 | export default () => {
31 | if (select.exists('.reponav-releases')) {
32 | return;
33 | }
34 |
35 | const releasesTab = (
36 |
37 | {icons.tag()}
38 | Releases
39 |
40 | );
41 |
42 | select('.reponav-dropdown').before(releasesTab);
43 |
44 | cacheReleasesCount();
45 |
46 | if (pageDetect.isReleases()) {
47 | releasesTab.classList.add('js-selected-navigation-item', 'selected');
48 | select('.reponav-item.selected')
49 | .classList.remove('js-selected-navigation-item', 'selected');
50 | }
51 | };
52 |
--------------------------------------------------------------------------------
/src/features/add-time-machine-links-to-comments.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 |
4 | import * as icons from '../libs/icons';
5 | import {getRepoURL} from '../libs/page-detect';
6 |
7 | export default async () => {
8 | const comments = select.all('.timeline-comment-header:not(.rgh-timestamp-tree-link)');
9 |
10 | for (const comment of comments) {
11 | const timestampEl = select('relative-time', comment);
12 | const timestamp = timestampEl.attributes.datetime.value;
13 | const href = `/${getRepoURL()}/tree/HEAD@{${timestamp}}`;
14 |
15 | timestampEl.parentNode.after(
16 |
17 |
18 |
25 |
26 | );
27 |
28 | comment.classList.add('rgh-timestamp-tree-link');
29 | }
30 | };
31 |
--------------------------------------------------------------------------------
/src/features/add-title-to-emojis.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | export default function () {
4 | for (const emoji of select.all('g-emoji')) {
5 | emoji.setAttribute('title', `:${emoji.getAttribute('alias')}:`);
6 | }
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/features/add-trending-menu-item.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import * as pageDetect from '../libs/page-detect';
3 | import {safeElementReady} from '../libs/utils';
4 |
5 | export default async function () {
6 | const selectedClass = pageDetect.isTrending() ? 'selected' : '';
7 | const issuesLink = await safeElementReady('.HeaderNavlink[href="/issues"], .header-nav-link[href="/issues"]');
8 | if (!issuesLink) {
9 | return;
10 | }
11 |
12 | issuesLink.parentNode.after(
13 |
16 | );
17 |
18 | // Explore link highlights /trending urls by default, remove that behavior
19 | if (pageDetect.isTrending()) {
20 | const exploreLink = await safeElementReady('a[href="/explore"]');
21 | if (exploreLink) {
22 | exploreLink.classList.remove('selected');
23 | }
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/src/features/add-yours-menu-item.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 | import {getUsername} from '../libs/utils';
5 |
6 | export default function () {
7 | const pageName = pageDetect.isIssueSearch() ? 'issues' : 'pulls';
8 | const username = getUsername();
9 |
10 | if (select.exists('.refined-github-yours')) {
11 | return;
12 | }
13 |
14 | const yoursMenuItem = Yours ;
15 |
16 | if (!select.exists('.subnav-links .selected') && location.search.includes(`user%3A${username}`)) {
17 | yoursMenuItem.classList.add('selected');
18 | for (const tab of select.all(`.subnav-links a[href*="user%3A${username}"]`)) {
19 | tab.href = tab.href.replace(`user%3A${username}`, '');
20 | }
21 | }
22 |
23 | select('.subnav-links').append(yoursMenuItem);
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/src/features/auto-load-more-news.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import debounce from 'debounce-fn';
3 | import {observeEl} from '../libs/utils';
4 |
5 | let btn;
6 | let newsfeedObserver;
7 |
8 | const loadMore = debounce(() => {
9 | btn.click();
10 |
11 | // If GH hasn't loaded the JS, the click will not load anything.
12 | // We can detect if it worked by looking at the button's state,
13 | // and then trying again (auto-debounced)
14 | if (!btn.disabled) {
15 | loadMore();
16 | }
17 | }, {wait: 200});
18 |
19 | const inView = new IntersectionObserver(([{isIntersecting}]) => {
20 | if (isIntersecting) {
21 | loadMore();
22 | }
23 | }, {
24 | rootMargin: '500px' // https://github.com/sindresorhus/refined-github/pull/505#issuecomment-309273098
25 | });
26 |
27 | const findButton = () => {
28 | // If the old button is still there, leave
29 | if (btn && document.contains(btn)) {
30 | return;
31 | }
32 |
33 | // Forget the old button
34 | inView.disconnect();
35 |
36 | // Watch the new button, or stop everything
37 | btn = select('.ajax-pagination-btn');
38 | if (btn) {
39 | inView.observe(btn);
40 | } else {
41 | newsfeedObserver.disconnect();
42 | }
43 | };
44 |
45 | export default () => {
46 | const form = select('.ajax-pagination-form');
47 | if (form) {
48 | // If GH hasn't loaded the JS,
49 | // the fake click will submit the form without ajax.
50 | form.addEventListener('submit', e => e.preventDefault());
51 | newsfeedObserver = observeEl('#dashboard .news', findButton);
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/src/features/copy-file-path.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 | import {observeEl} from '../libs/utils';
4 |
5 | function addFilePathCopyBtn() {
6 | for (const file of select.all('#files .file-header:not(.rgh-copy-file-path)')) {
7 | file.classList.add(
8 | 'rgh-copy-file-path',
9 | 'js-zeroclipboard-container'
10 | );
11 |
12 | select('.file-info a', file).classList.add('js-zeroclipboard-target');
13 |
14 | const group = (
15 |
16 | Copy path
17 |
18 | );
19 | const viewButton = select('[aria-label^="View"]', file);
20 | viewButton.classList.add('BtnGroup-item');
21 | viewButton.replaceWith(group);
22 | group.append(viewButton);
23 | }
24 | }
25 |
26 | export default () => {
27 | observeEl('#files', addFilePathCopyBtn, {childList: true, subtree: true});
28 | };
29 |
--------------------------------------------------------------------------------
/src/features/copy-file.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 | import {groupButtons} from '../libs/utils';
4 |
5 | export default function () {
6 | // This selector skips binaries + markdowns with code
7 | for (const code of select.all('.file .blob-wrapper > .highlight:not(.rgh-copy-file)')) {
8 | code.classList.add('rgh-copy-file');
9 | const file = code.closest('.file');
10 |
11 | // Enable copy behavior
12 | file.classList.add('js-zeroclipboard-container');
13 | code.classList.add('js-zeroclipboard-target');
14 |
15 | // Prepend to list of buttons
16 | const firstAction = select('.file-actions .btn', file);
17 | firstAction.before(
18 | Copy
19 | );
20 |
21 | // Group buttons if necessary
22 | groupButtons(firstAction.parentNode.children);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/features/copy-markdown.js:
--------------------------------------------------------------------------------
1 | import toMarkdown from 'to-markdown';
2 | import copyToClipboard from 'copy-text-to-clipboard';
3 |
4 | const unwrapContent = content => content;
5 | const unshortenRegex = /^https:[/][/](www[.])?|[/]$/g;
6 |
7 | const converters = [
8 | // Drop unnecessary elements
9 | // is GH's emoji wrapper
10 | // input and .handle appear in "- [ ] lists", let's not copy tasks
11 | {
12 | filter: node => node.matches('g-emoji,.handle,input.task-list-item-checkbox'),
13 | replacement: unwrapContent
14 | },
15 |
16 | // Unwrap commit/issue autolinks
17 | {
18 | filter: node => node.matches('.commit-link,.issue-link') || // GH autolinks
19 | (node.href && node.href.replace(unshortenRegex, '') === node.textContent), // Some of bfred-it/shorten-repo-url
20 | replacement: (content, element) => element.href
21 | },
22 |
23 | // Unwrap images
24 | {
25 | filter: node => node.tagName === 'A' && // It's a link
26 | node.childNodes.length === 1 && // It has one child
27 | node.firstChild.tagName === 'IMG' && // Its child is an image
28 | node.firstChild.src === node.href, // It links to its own image
29 | replacement: unwrapContent
30 | },
31 |
32 | // Keep if it's customized
33 | {
34 | filter: node => node.matches('img[width],img[height],img[align]'),
35 | replacement: (content, element) => element.outerHTML
36 | }
37 | ];
38 |
39 | export const getSmarterMarkdown = html => toMarkdown(html, {
40 | converters,
41 | gfm: true
42 | });
43 |
44 | export default event => {
45 | const selection = window.getSelection();
46 | const range = selection.getRangeAt(0);
47 | const container = range.commonAncestorContainer;
48 | const containerEl = container.closest ? container : container.parentNode;
49 |
50 | // Exclude pure code selections and selections across markdown elements:
51 | // https://github.com/sindresorhus/refined-github/issues/522#issuecomment-311271274
52 | if (containerEl.closest('pre') || containerEl.querySelector('.markdown-body')) {
53 | return;
54 | }
55 |
56 | const holder = document.createElement('div');
57 | holder.append(range.cloneContents());
58 |
59 | // Wrap orphaned s in their original parent
60 | // And keep the their original number
61 | if (holder.firstChild.tagName === 'LI') {
62 | const list = document.createElement(containerEl.tagName);
63 | try {
64 | const originalLi = range.startContainer.parentNode.closest('li');
65 | list.start = containerEl.start + [...containerEl.children].indexOf(originalLi);
66 | } catch (err) {}
67 | list.append(...holder.childNodes);
68 | holder.appendChild(list);
69 | }
70 |
71 | const markdown = getSmarterMarkdown(holder.innerHTML);
72 |
73 | if (copyToClipboard(markdown)) {
74 | event.stopImmediatePropagation();
75 | event.preventDefault();
76 | } else {
77 | console.warn('Refined GitHub: copy-markdown failed');
78 | }
79 | };
80 |
--------------------------------------------------------------------------------
/src/features/copy-on-y.js:
--------------------------------------------------------------------------------
1 | import copyToClipboard from 'copy-text-to-clipboard';
2 | import select from 'select-dom';
3 |
4 | const Y_KEYCODE = 89;
5 |
6 | const handler = ({keyCode, target}) => {
7 | if (keyCode === Y_KEYCODE && target.nodeName !== 'INPUT') {
8 | const commitIsh = select('.commit-tease-sha').textContent.trim();
9 | const uri = location.href.replace(/\/blob\/[\w-]+\//, `/blob/${commitIsh}/`);
10 |
11 | copyToClipboard(uri);
12 | }
13 | };
14 |
15 | const setup = () => {
16 | window.addEventListener('keyup', handler);
17 | };
18 |
19 | const destroy = () => {
20 | window.removeEventListener('keyup', handler);
21 | };
22 |
23 | export default {
24 | setup,
25 | destroy
26 | };
27 |
--------------------------------------------------------------------------------
/src/features/fix-squash-and-merge-title.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | export default function () {
4 | const btn = select('.merge-message .btn-group-squash [type=submit]');
5 | if (!btn) {
6 | return;
7 | }
8 | btn.addEventListener('click', () => {
9 | const title = select('.js-issue-title').textContent;
10 | const number = select('.gh-header-number').textContent;
11 | select('#merge_title_field').value = `${title.trim()} (${number})`;
12 | });
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/features/focus-confirmation-buttons.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | // Ensure that confirm buttons (like Mark all as read) are always in focus
4 | export default function () {
5 | window.addEventListener('facebox:reveal', () => {
6 | select('.facebox-content button').focus();
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/src/features/hide-empty-meta.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import * as pageDetect from '../libs/page-detect';
3 |
4 | export default function () {
5 | if (pageDetect.isRepoRoot()) {
6 | const meta = select('.repository-meta');
7 | if (select.exists('em', meta) && !select.exists('.js-edit-repo-meta-button')) {
8 | meta.style.display = 'none';
9 | }
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/features/hide-own-stars.js:
--------------------------------------------------------------------------------
1 | import OptionsSync from 'webext-options-sync';
2 | import {getUsername, observeEl} from '../libs/utils';
3 |
4 | const options = new OptionsSync();
5 |
6 | // Hide other users starring/forking your repos
7 | export default async function () {
8 | const {hideStarsOwnRepos} = await options.getAll();
9 |
10 | if (hideStarsOwnRepos) {
11 | const username = getUsername();
12 | observeEl('#dashboard .news', () => {
13 | $('#dashboard .news .watch_started, #dashboard .news .fork')
14 | .has(`a[href^="/${username}"]`)
15 | .css('display', 'none');
16 | });
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/src/features/linkify-branch-refs.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import {safeElementReady} from '../libs/utils';
4 | import * as pageDetect from '../libs/page-detect';
5 |
6 | export function inPR() {
7 | let deletedBranch = false;
8 | const lastBranchAction = select.all(`
9 | .discussion-item-head_ref_deleted .head-ref,
10 | .discussion-item-head_ref_restored .head-ref
11 | `).pop();
12 | if (lastBranchAction && lastBranchAction.closest('.discussion-item-head_ref_deleted')) {
13 | deletedBranch = lastBranchAction.title;
14 | }
15 |
16 | for (const el of select.all('.commit-ref[title], .base-ref[title], .head-ref[title]')) {
17 | if (el.textContent === 'unknown repository') {
18 | continue;
19 | }
20 |
21 | if (el.title === deletedBranch) {
22 | el.title = 'Deleted: ' + el.title;
23 | el.style.textDecoration = 'line-through';
24 | continue;
25 | }
26 |
27 | const branchUrl = '/' + el.title.replace(':', '/tree/');
28 | $(el).closest('.commit-ref').wrap( );
29 | }
30 | }
31 |
32 | export function inQuickPR() {
33 | safeElementReady('.branch-name').then(el => {
34 | if (!el) {
35 | return;
36 | }
37 | const {ownerName, repoName} = pageDetect.getOwnerAndRepo();
38 | const branchUrl = `/${ownerName}/${repoName}/tree/${el.textContent}`;
39 | $(el).closest('.branch-name').wrap( );
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/src/features/linkify-issues-in-titles.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import linkifyIssues from 'linkify-issues';
3 | import {observeEl} from '../libs/utils';
4 | import {editTextNodes} from './linkify-urls-in-code';
5 |
6 | export default function () {
7 | observeEl(select('#partial-discussion-header').parentNode, () => {
8 | const title = select('.js-issue-title:not(.refined-linkified-title)');
9 | if (title) {
10 | title.classList.add('refined-linkified-title');
11 | editTextNodes(linkifyIssues, title);
12 | }
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/features/linkify-urls-in-code.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import linkifyUrls from 'linkify-urls';
3 | import linkifyIssues from 'linkify-issues';
4 | import {getOwnerAndRepo} from '../libs/page-detect';
5 | import getTextNodes from '../libs/get-text-nodes';
6 |
7 | const linkifiedURLClass = 'refined-github-linkified-code';
8 | const {
9 | ownerName,
10 | repoName
11 | } = getOwnerAndRepo();
12 |
13 | const options = {
14 | user: ownerName,
15 | repo: repoName,
16 | type: 'dom',
17 | baseUrl: '',
18 | attrs: {
19 | target: '_blank'
20 | }
21 | };
22 |
23 | export const editTextNodes = (fn, el) => {
24 | // Spread required because the elements will change and the TreeWalker will break
25 | for (const textNode of [...getTextNodes(el)]) {
26 | if (fn === linkifyUrls && textNode.textContent.length < 11) { // Shortest url: http://j.mp
27 | continue;
28 | }
29 | const linkified = fn(textNode.textContent, options);
30 | if (linkified.children.length > 0) { // Children are
31 | textNode.replaceWith(linkified);
32 | }
33 | }
34 | };
35 |
36 | export default () => {
37 | const wrappers = select.all(`.highlight:not(.${linkifiedURLClass})`);
38 |
39 | // Don't linkify any already linkified code
40 | if (wrappers.length === 0) {
41 | return;
42 | }
43 |
44 | // Linkify full URLs
45 | // `.blob-code-inner` in diffs
46 | // `pre` in GitHub comments
47 | for (const el of select.all('.blob-code-inner, pre', wrappers)) {
48 | editTextNodes(linkifyUrls, el);
49 | }
50 |
51 | // Linkify issue refs in comments
52 | for (const el of select.all('span.pl-c', wrappers)) {
53 | editTextNodes(linkifyIssues, el);
54 | }
55 |
56 | // Mark code block as touched
57 | for (const el of wrappers) {
58 | el.classList.add(linkifiedURLClass);
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/src/features/mark-merge-commits-in-list.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import * as icons from '../libs/icons';
3 |
4 | export default function () {
5 | for (const commit of select.all('.commits-list-item:not(.refined-github-merge-commit)')) {
6 | if (select.exists('[title^="Merge pull request"]', commit)) {
7 | commit.classList.add('refined-github-merge-commit');
8 | commit.querySelector('.commit-avatar-cell').prepend(icons.mergedPullRequest());
9 | commit.querySelector('.avatar').classList.add('avatar-child');
10 | }
11 | }
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/src/features/mark-unread.js:
--------------------------------------------------------------------------------
1 | import gitHubInjection from 'github-injection';
2 | import select from 'select-dom';
3 | import {h} from 'dom-chef';
4 | import SynchronousStorage from '../libs/synchronous-storage';
5 | import * as icons from '../libs/icons';
6 | import * as pageDetect from '../libs/page-detect';
7 | import {getUsername} from '../libs/utils';
8 |
9 | let storage;
10 |
11 | function stripHash(url) {
12 | return url.replace(/#.+$/, '');
13 | }
14 |
15 | function addMarkUnreadButton() {
16 | const container = select('.js-thread-subscription-status');
17 | if (container) {
18 | const button = Mark as unread ;
19 | button.addEventListener('click', markUnread, {
20 | once: true
21 | });
22 | container.append(button);
23 | }
24 | }
25 |
26 | function markRead(url) {
27 | const unreadNotifications = storage.get();
28 | unreadNotifications.forEach((notification, index) => {
29 | if (notification.url === url) {
30 | unreadNotifications.splice(index, 1);
31 | }
32 | });
33 |
34 | for (const a of select.all(`a.js-notification-target[href="${url}"]`)) {
35 | const li = a.closest('li.js-notification');
36 | li.classList.remove('unread');
37 | li.classList.add('read');
38 | }
39 |
40 | storage.set(unreadNotifications);
41 | }
42 |
43 | function markUnread() {
44 | const participants = select.all('.participant-avatar').map(el => ({
45 | username: el.getAttribute('aria-label'),
46 | avatar: el.querySelector('img').src
47 | }));
48 |
49 | const {ownerName, repoName} = pageDetect.getOwnerAndRepo();
50 | const repository = `${ownerName}/${repoName}`;
51 | const title = select('.js-issue-title').textContent.trim();
52 | const type = pageDetect.isPR() ? 'pull-request' : 'issue';
53 | const url = stripHash(location.href);
54 |
55 | const stateLabel = select('.gh-header-meta .State');
56 | let state;
57 |
58 | if (stateLabel.classList.contains('State--green')) {
59 | state = 'open';
60 | } else if (stateLabel.classList.contains('State--purple')) {
61 | state = 'merged';
62 | } else if (stateLabel.classList.contains('State--red')) {
63 | state = 'closed';
64 | }
65 |
66 | const lastCommentTime = select.all('.timeline-comment-header relative-time').pop();
67 | const dateTitle = lastCommentTime.title;
68 | const date = lastCommentTime.getAttribute('datetime');
69 |
70 | const unreadNotifications = storage.get();
71 |
72 | unreadNotifications.push({
73 | participants,
74 | repository,
75 | title,
76 | state,
77 | type,
78 | dateTitle,
79 | date,
80 | url
81 | });
82 |
83 | storage.set(unreadNotifications);
84 | updateUnreadIndicator();
85 |
86 | this.setAttribute('disabled', 'disabled');
87 | this.textContent = 'Marked as unread';
88 | }
89 |
90 | function renderNotifications() {
91 | const myUserName = getUsername();
92 | const unreadNotifications = storage.get()
93 | .filter(notification => !isNotificationExist(notification.url))
94 | .filter(notification => {
95 | if (!isParticipatingPage()) {
96 | return true;
97 | }
98 |
99 | return isParticipatingNotification(notification, myUserName);
100 | });
101 |
102 | if (unreadNotifications.length === 0) {
103 | return;
104 | }
105 |
106 | if (isEmptyPage()) {
107 | select('.blankslate').remove();
108 | select('.js-navigation-container').append(
);
109 | }
110 |
111 | unreadNotifications.forEach(notification => {
112 | const {
113 | participants,
114 | repository,
115 | title,
116 | state,
117 | type,
118 | dateTitle,
119 | date,
120 | url
121 | } = notification;
122 |
123 | let icon;
124 |
125 | if (type === 'issue') {
126 | if (state === 'open') {
127 | icon = icons.openIssue();
128 | }
129 |
130 | if (state === 'closed') {
131 | icon = icons.closedIssue();
132 | }
133 | }
134 |
135 | if (type === 'pull-request') {
136 | if (state === 'open') {
137 | icon = icons.openPullRequest();
138 | }
139 |
140 | if (state === 'merged') {
141 | icon = icons.mergedPullRequest();
142 | }
143 |
144 | if (state === 'closed') {
145 | icon = icons.closedPullRequest();
146 | }
147 | }
148 |
149 | const hasList = select.exists(`a.notifications-repo-link[title="${repository}"]`);
150 | if (!hasList) {
151 | const list = (
152 |
153 |
158 |
159 |
164 |
165 |
166 |
167 | );
168 |
169 | $('.notifications-list').prepend(list);
170 | }
171 |
172 | const list = $(`a.notifications-repo-link[title="${repository}"]`).parent().siblings('ul.notifications');
173 |
174 | const usernames = participants
175 | .map(participant => participant.username)
176 | .join(', ');
177 |
178 | const avatars = participants
179 | .map(participant => {
180 | return ;
181 | });
182 |
183 | const item = (
184 |
185 |
186 | {icon}
187 |
188 |
189 | {title}
190 |
191 |
192 |
193 |
194 |
195 |
196 | {icons.check()}
197 |
198 |
199 |
200 |
201 |
202 | {icons.mute()}
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | {avatars}
213 |
214 |
215 |
216 |
217 | );
218 |
219 | list.prepend(item);
220 | });
221 |
222 | // Make sure that all the boxes with unread items are at the top
223 | // This is necessary in the "All notifications" view
224 | $('.boxed-group:has(".unread")').prependTo('.notifications-list');
225 | }
226 |
227 | function isNotificationExist(url) {
228 | return select.exists(`a.js-notification-target[href^="${stripHash(url)}"]`);
229 | }
230 |
231 | function isEmptyPage() {
232 | return select.exists('.blankslate');
233 | }
234 |
235 | function isParticipatingPage() {
236 | return /\/notifications\/participating/.test(location.pathname);
237 | }
238 |
239 | function isParticipatingNotification(notification, myUserName) {
240 | const {participants} = notification;
241 |
242 | return participants
243 | .filter(participant => participant.username === myUserName)
244 | .length > 0;
245 | }
246 |
247 | function updateUnreadIndicator() {
248 | const icon = select('.notification-indicator');
249 | if (!icon) {
250 | return;
251 | }
252 | const statusMark = icon.querySelector('.mail-status');
253 | const hasRealNotifications = icon.matches('[data-ga-click$=":unread"]');
254 |
255 | const hasUnread = hasRealNotifications || storage.get().length > 0;
256 | const label = hasUnread ? 'You have unread notifications' : 'You have no unread notifications';
257 |
258 | icon.setAttribute('aria-label', label);
259 | statusMark.classList.toggle('unread', hasUnread);
260 | }
261 |
262 | function markNotificationRead(e) {
263 | const notification = e.target.closest('li.js-notification');
264 | const a = notification.querySelector('a.js-notification-target');
265 | markRead(a.href);
266 | updateUnreadIndicator();
267 | }
268 |
269 | function markAllNotificationsRead(e) {
270 | e.preventDefault();
271 | const repoGroup = e.target.closest('.boxed-group');
272 | for (const a of repoGroup.querySelectorAll('a.js-notification-target')) {
273 | markRead(a.href);
274 | }
275 | updateUnreadIndicator();
276 | }
277 |
278 | function addCustomAllReadBtn() {
279 | const hasMarkAllReadBtnExists = select.exists('#notification-center a[href="#mark_as_read_confirm_box"]');
280 | if (hasMarkAllReadBtnExists || storage.get().length === 0) {
281 | return;
282 | }
283 |
284 | $('#notification-center .tabnav-tabs:first').append(
285 |
286 |
Mark all as read
287 |
288 |
289 |
290 |
291 |
Are you sure you want to mark all unread notifications as read?
292 |
293 |
294 | Mark all notifications as read
295 |
296 |
297 |
298 | );
299 |
300 | $(document).on('click', '#clear-local-notification', () => {
301 | storage.set([]);
302 | location.reload();
303 | });
304 | }
305 |
306 | function updateLocalNotificationsCount() {
307 | const unreadCount = select('#notification-center .filter-list a[href="/notifications"] .count');
308 | const githubNotificationsCount = Number(unreadCount.textContent);
309 | const localNotifications = storage.get();
310 |
311 | if (localNotifications.length > 0) {
312 | unreadCount.textContent = githubNotificationsCount + localNotifications.length;
313 | }
314 | }
315 |
316 | function updateLocalParticipatingCount() {
317 | const unreadCount = select('#notification-center .filter-list a[href="/notifications/participating"] .count');
318 | const githubNotificationsCount = Number(unreadCount.textContent);
319 | const myUserName = getUsername();
320 |
321 | const participatingNotifications = storage.get()
322 | .filter(notification => isParticipatingNotification(notification, myUserName));
323 |
324 | if (participatingNotifications.length > 0) {
325 | unreadCount.textContent = githubNotificationsCount + participatingNotifications.length;
326 | }
327 | }
328 |
329 | async function setup() {
330 | storage = await new SynchronousStorage(
331 | () => {
332 | return browser.storage.local.get({
333 | unreadNotifications: []
334 | }).then(storage => storage.unreadNotifications);
335 | },
336 | unreadNotifications => {
337 | return browser.storage.local.set({unreadNotifications});
338 | }
339 | );
340 | gitHubInjection(() => {
341 | destroy();
342 |
343 | // Remove old data from previous storage
344 | // Drop code in 2018
345 | localStorage.removeItem('_unreadNotifications_migrated');
346 | localStorage.removeItem('unreadNotifications');
347 |
348 | if (pageDetect.isNotifications()) {
349 | renderNotifications();
350 | addCustomAllReadBtn();
351 | updateLocalNotificationsCount();
352 | updateLocalParticipatingCount();
353 | $(document).on('click', '.js-mark-read', markNotificationRead);
354 | $(document).on('click', '.js-mark-all-read', markAllNotificationsRead);
355 | $(document).on('click', '.js-delete-notification button', updateUnreadIndicator);
356 | $(document).on('click', 'form[action="/notifications/mark"] button', () => {
357 | storage.set([]);
358 | });
359 | } else if (pageDetect.isPR() || pageDetect.isIssue()) {
360 | markRead(location.href);
361 | addMarkUnreadButton();
362 | }
363 |
364 | updateUnreadIndicator();
365 | });
366 | }
367 |
368 | function destroy() {
369 | $(document).off('click', '.js-mark-unread', markUnread);
370 | $('.js-mark-unread').remove();
371 | }
372 |
373 | export default {
374 | setup,
375 | destroy
376 | };
377 |
--------------------------------------------------------------------------------
/src/features/more-dropdown.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 | import * as icons from '../libs/icons';
4 | import * as pageDetect from '../libs/page-detect';
5 |
6 | const repoUrl = pageDetect.getRepoURL();
7 |
8 | export default function () {
9 | if (select.exists('.refined-github-more')) {
10 | return;
11 | }
12 |
13 | // Last tab, but before settings
14 | const insertionPoint = select.all('.reponav-item:not([href$="settings"])').pop();
15 |
16 | insertionPoint.after(
17 |
39 | );
40 |
41 | // Remove native Insights tab
42 | const insightsTab = select('[data-selected-links~="pulse"]');
43 | if (insightsTab) {
44 | insightsTab.remove();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/features/move-account-switcher-to-sidebar.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {safeElementReady} from '../libs/utils';
3 |
4 | export default function () {
5 | safeElementReady('.dashboard-sidebar').then(sidebar => {
6 | const switcher = select('.account-switcher');
7 | if (sidebar && switcher) {
8 | sidebar.prepend(switcher);
9 | }
10 | });
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/src/features/move-marketplace-link-to-profile-dropdown.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 |
4 | export default function () {
5 | const lastDivider = select.all('.user-nav .dropdown-divider').pop();
6 | if (!lastDivider) {
7 | return;
8 | }
9 | const marketplaceLink = Marketplace ;
10 | const divider =
;
11 | lastDivider.before(divider);
12 | lastDivider.before(marketplaceLink);
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/features/op-labels.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 | import * as pageDetect from '../libs/page-detect';
4 | import {getUsername} from '../libs/utils';
5 |
6 | export default () => {
7 | let op;
8 | if (pageDetect.isPR()) {
9 | const titleRegex = /^(?:.+) by (\S+) · Pull Request #(\d+)/;
10 | [, op] = titleRegex.exec(document.title) || [];
11 | } else {
12 | op = select('.timeline-comment-header-text .author').textContent;
13 | }
14 |
15 | let newComments = $(`.js-comment:not(.refined-github-op)`).has(`strong .author[href="/${op}"]`).get();
16 |
17 | if (!pageDetect.isPRFiles()) {
18 | newComments = newComments.slice(1);
19 | }
20 |
21 | if (newComments.length === 0) {
22 | return;
23 | }
24 |
25 | const type = pageDetect.isPR() ? 'pull request' : 'issue';
26 | const tooltip = `${op === getUsername() ? 'You' : 'This user'} submitted this ${type}.`;
27 |
28 | const placeholders = select.all(`
29 | .timeline-comment .timeline-comment-header-text,
30 | .review-comment .comment-body
31 | `, newComments);
32 |
33 | for (const placeholder of placeholders) {
34 | placeholder.before(
35 |
38 | );
39 | }
40 |
41 | for (const el of newComments) {
42 | el.classList.add('refined-github-op');
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/features/open-all-notifications.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import {h} from 'dom-chef';
3 | import {isNotifications} from '../libs/page-detect';
4 |
5 | function openNotifications() {
6 | const urls = select.all('.js-notification-target').map(el => el.href);
7 | browser.runtime.sendMessage({
8 | urls,
9 | action: 'openAllInTabs'
10 | });
11 | for (const listItem of select.all('.list-group .list-group-item')) {
12 | listItem.classList.add('read');
13 | }
14 | }
15 |
16 | export default function () {
17 | if (!isNotifications()) {
18 | return;
19 | }
20 | const unreadCount = select.all('.js-notification-target').length;
21 |
22 | if (unreadCount === 0) {
23 | return;
24 | }
25 |
26 | const openButton = Open all in tabs ;
27 |
28 | // Make a button group
29 | const markAsReadButton = select('[href="#mark_as_read_confirm_box"]');
30 | markAsReadButton.parentNode.classList.add('BtnGroup');
31 | markAsReadButton.classList.add('BtnGroup-item');
32 | markAsReadButton.before(openButton);
33 |
34 | // Move out the extra node that messes with .BtnGroup-item:last-child
35 | document.body.append(select('#mark_as_read_confirm_box'));
36 |
37 | if (unreadCount < 10) {
38 | openButton.addEventListener('click', openNotifications);
39 | } else {
40 | // Add confirmation modal
41 | openButton.setAttribute('rel', 'facebox');
42 | document.body.append(
43 |
44 |
45 |
46 |
Are you sure you want to open {unreadCount} tabs?
47 |
48 |
49 | Open all notifications
50 |
51 |
52 | );
53 |
54 | $(document).on('click', '#open-all-notifications', () => {
55 | openNotifications();
56 | select('.js-facebox-close').click(); // Close modal
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/features/open-ci-details-in-new-tab.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | export default function () {
4 | const CIDetailsLinks = select.all('a.status-actions');
5 | for (const link of CIDetailsLinks) {
6 | link.setAttribute('target', '_blank');
7 | link.setAttribute('rel', 'noopener');
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/src/features/preserve-whitespace-option-in-nav.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | // When navigating with next/previous in review mode, preserve whitespace option.
4 | export default function () {
5 | const navLinks = select.all('.commit > .BtnGroup.float-right > a.BtnGroup-item');
6 | if (navLinks.length === 0) {
7 | return;
8 | }
9 |
10 | const url = new URL(location.href);
11 | const hidingWhitespace = url.searchParams.get('w') === '1';
12 |
13 | if (hidingWhitespace) {
14 | for (const a of navLinks) {
15 | const linkUrl = new URL(a.href);
16 | linkUrl.searchParams.set('w', '1');
17 | a.href = linkUrl;
18 | }
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/features/reactions-avatars.js:
--------------------------------------------------------------------------------
1 | import debounce from 'debounce-fn';
2 | import select from 'select-dom';
3 | import {h} from 'dom-chef';
4 | import {getUsername, flatZip} from '../libs/utils';
5 |
6 | const arbitraryAvatarLimit = 36;
7 | const approximateHeaderLength = 3; // Each button header takes about as much as 3 avatars
8 |
9 | function getParticipants(container) {
10 | const currentUser = getUsername();
11 | return container.getAttribute('aria-label')
12 | .replace(/ reacted with.*/, '')
13 | .replace(/,? and /, ', ')
14 | .replace(/, \d+ more/, '')
15 | .split(', ')
16 | .filter(username => username !== currentUser)
17 | .map(username => ({
18 | container,
19 | username
20 | }));
21 | }
22 |
23 | function add() {
24 | for (const list of select.all(`.has-reactions .comment-reactions-options:not(.rgh-reactions)`)) {
25 | const avatarLimit = arbitraryAvatarLimit - (list.children.length * approximateHeaderLength);
26 |
27 | const participantByReaction = [].map.call(list.children, getParticipants);
28 | const flatParticipants = flatZip(participantByReaction, avatarLimit);
29 |
30 | for (const participant of flatParticipants) {
31 | participant.container.append(
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | list.classList.add('rgh-reactions');
39 |
40 | // Overlap reaction avatars when near the avatarLimit
41 | if (flatParticipants.length > avatarLimit * 0.9) {
42 | list.classList.add('rgh-reactions-near-limit');
43 | }
44 | }
45 | }
46 |
47 | // Feature testable on
48 | // https://github.com/babel/babel/pull/3646
49 | export default () => {
50 | add();
51 | document.addEventListener('socket:message', debounce(add, {wait: 100}));
52 | };
53 |
--------------------------------------------------------------------------------
/src/features/remove-diff-signs.js:
--------------------------------------------------------------------------------
1 | /* Lasciate ogne speranza, voi ch'entrate. */
2 | import select from 'select-dom';
3 | import {observeEl} from '../libs/utils';
4 |
5 | function removeDiffSigns() {
6 | for (const line of select.all('.diff-table tr:not(.refined-github-diff-signs)')) {
7 | line.classList.add('refined-github-diff-signs');
8 | for (const code of select.all('.blob-code-inner', line)) {
9 | // Drop -, + or space
10 | code.firstChild.textContent = code.firstChild.textContent.slice(1);
11 |
12 | // If a line is empty, the next line will collapse
13 | if (code.textContent.length === 0) {
14 | code.prepend(' ');
15 | }
16 | }
17 | }
18 | }
19 |
20 | function removeSelectableWhiteSpaceFromDiffs() {
21 | for (const commentBtn of select.all('.add-line-comment')) {
22 | for (const node of commentBtn.childNodes) {
23 | if (node.nodeType === Node.TEXT_NODE) {
24 | node.remove();
25 | }
26 | }
27 | }
28 | }
29 |
30 | function removeDiffSignsAndWatchExpansions() {
31 | removeSelectableWhiteSpaceFromDiffs();
32 | removeDiffSigns();
33 | for (const file of $('.diff-table:not(.rgh-watching-lines)').has('.diff-expander')) {
34 | file.classList.add('rgh-watching-lines');
35 | observeEl(file.tBodies[0], removeDiffSigns);
36 | }
37 | }
38 |
39 | export default function () {
40 | const diffElements = select('.js-discussion, #files');
41 | if (diffElements) {
42 | observeEl(diffElements, removeDiffSignsAndWatchExpansions, {childList: true, subtree: true});
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/features/remove-projects-tab.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | export default function () {
4 | const projectsTab = select('.js-repo-nav .reponav-item[data-selected-links^="repo_projects"]');
5 | if (projectsTab && projectsTab.querySelector('.Counter, .counter').textContent === '0') {
6 | projectsTab.remove();
7 | }
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/src/features/remove-upload-files-button.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 | import * as pageDetect from '../libs/page-detect';
3 |
4 | const repoUrl = pageDetect.getRepoURL();
5 |
6 | export default () => {
7 | if (pageDetect.isRepoRoot()) {
8 | const uploadFilesButton = select(`.file-navigation a[href^="/${repoUrl}/upload"]`);
9 | if (uploadFilesButton) {
10 | uploadFilesButton.remove();
11 | }
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/src/features/scroll-to-top-on-collapse.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | export default () => {
4 | const toolbar = select('.pr-toolbar');
5 |
6 | $('.js-diff-progressive-container').on('details:toggled', '.file', ({target}) => {
7 | const elOffset = target.getBoundingClientRect().top;
8 | const toolbarHeight = toolbar.getBoundingClientRect().top;
9 |
10 | // Bring element in view if it's above the PR toolbar
11 | if (elOffset < toolbarHeight) {
12 | window.scrollBy(0, elOffset - toolbarHeight);
13 | }
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/src/features/show-names.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import domify from '../libs/domify';
4 | import {getUsername, groupBy} from '../libs/utils';
5 |
6 | const storageKey = 'cachedNames';
7 |
8 | const getCachedUsers = async () => {
9 | const keys = await browser.storage.local.get({
10 | [storageKey]: {}
11 | });
12 | return keys[storageKey];
13 | };
14 |
15 | const fetchName = async username => {
16 | // /following/you_know is the lightest page we know
17 | // location.origin is required for Firefox #490
18 | const pageHTML = await fetch(`${location.origin}/${username}/following`)
19 | .then(res => res.text());
20 |
21 | const el = domify(pageHTML).querySelector('h1 strong');
22 |
23 | // The full name might not be set
24 | const fullname = el && el.textContent.slice(1, -1);
25 | if (!fullname || fullname === username) {
26 | // It has to be stored as false or else it will be fetched every time
27 | return false;
28 | }
29 | return fullname;
30 | };
31 |
32 | export default async () => {
33 | const myUsername = getUsername();
34 | const cache = await getCachedUsers();
35 |
36 | // {sindresorhus: [a.author, a.author], otheruser: [a.author]}
37 | const selector = `.js-discussion .author:not(.refined-github-fullname)`;
38 | const usersOnPage = groupBy(select.all(selector), el => el.textContent);
39 |
40 | const fetchAndAdd = async username => {
41 | if (typeof cache[username] === 'undefined' && username !== myUsername) {
42 | cache[username] = await fetchName(username);
43 | }
44 |
45 | for (const usernameEl of usersOnPage[username]) {
46 | const commentedNode = usernameEl.parentNode.nextSibling;
47 | if (commentedNode && commentedNode.textContent.includes('commented')) {
48 | commentedNode.remove();
49 | }
50 |
51 | usernameEl.classList.add('refined-github-fullname');
52 |
53 | if (cache[username] && username !== myUsername) {
54 | // If it's a regular comment author, add it outside
55 | // otherwise it's something like "User added some commits"
56 | const insertionPoint = usernameEl.parentNode.tagName === 'STRONG' ? usernameEl.parentNode : usernameEl;
57 | insertionPoint.after(' (', {cache[username]} , ') ');
58 | }
59 | }
60 | };
61 |
62 | const fetches = Object.keys(usersOnPage).map(fetchAndAdd);
63 |
64 | // Wait for all the fetches to be done
65 | await Promise.all(fetches);
66 |
67 | browser.storage.local.set({[storageKey]: cache});
68 | };
69 |
--------------------------------------------------------------------------------
/src/features/show-recently-pushed-branches.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import * as pageDetect from '../libs/page-detect';
4 |
5 | const repoUrl = pageDetect.getRepoURL();
6 |
7 | export default async function () {
8 | // Don't duplicate on back/forward in history
9 | if (select.exists('[data-url$=recently_touched_branches_list]')) {
10 | return;
11 | }
12 |
13 | const codeTabURL = select('[data-hotkey="g c"]').href;
14 | const fragmentURL = `/${repoUrl}/show_partial?partial=tree%2Frecently_touched_branches_list`;
15 |
16 | const html = await fetch(codeTabURL, {
17 | credentials: 'include'
18 | }).then(res => res.text());
19 |
20 | // https://github.com/sindresorhus/refined-github/issues/216
21 | if (html.includes(fragmentURL)) {
22 | select('.repository-content').prepend( );
23 | }
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/src/features/sort-milestones-by-closest-due-date.js:
--------------------------------------------------------------------------------
1 | import select from 'select-dom';
2 |
3 | export default function () {
4 | for (const a of select.all('a[href$="/milestones"], a[href*="/milestones?"]')) {
5 | const url = new URL(a.href);
6 | // Only if they aren't explicitly sorted differently
7 | if (!url.searchParams.get('direction') && !url.searchParams.get('sort')) {
8 | url.searchParams.set('direction', 'asc');
9 | url.searchParams.set('sort', 'due_date');
10 | a.href = url;
11 | }
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/features/upload-button.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import onetime from 'onetime';
3 | import {metaKey} from '../libs/utils';
4 | import * as icons from '../libs/icons';
5 |
6 | function addButtons() {
7 | $('.js-previewable-comment-form:not(.rgh-has-upload-field)')
8 | .has('.js-manual-file-chooser[type=file]')
9 | .addClass('rgh-has-upload-field')
10 | .find('.js-saved-reply-container')
11 | .after(
12 |
13 | {icons.cloudUpload()}
14 |
15 | );
16 | }
17 |
18 | function triggerUploadUI({target}) {
19 | target
20 | .closest('.js-previewable-comment-form') // Find container form
21 | .querySelector('.js-manual-file-chooser') // Find
22 | .click(); // Open UI
23 | }
24 |
25 | function handleKeydown(event) {
26 | if (event[metaKey] && event.key === 'u') {
27 | triggerUploadUI(event);
28 | event.preventDefault();
29 | }
30 | }
31 |
32 | // Delegated events don't need to be added on ajax loads.
33 | // Unfortunately they aren't natively deduplicated, so onetime is required.
34 | const listenOnce = onetime(() => {
35 | $(document).on('keydown', '.rgh-has-upload-field', handleKeydown);
36 | $(document).on('click', '.rgh-upload-btn', triggerUploadUI);
37 | });
38 |
39 | export default function () {
40 | addButtons();
41 | listenOnce();
42 | }
43 |
--------------------------------------------------------------------------------
/src/libs/api.js:
--------------------------------------------------------------------------------
1 | export default endpoint => {
2 | const api = location.hostname === 'github.com' ? 'https://api.github.com/' : `${location.origin}/api/`;
3 | return fetch(api + endpoint).then(res => res.json());
4 | };
5 |
--------------------------------------------------------------------------------
/src/libs/domify.js:
--------------------------------------------------------------------------------
1 | export default html => {
2 | const template = document.createElement('template');
3 | template.innerHTML = html;
4 | return template.content;
5 | };
6 |
--------------------------------------------------------------------------------
/src/libs/get-text-nodes.js:
--------------------------------------------------------------------------------
1 | export default el => {
2 | const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
3 | const next = () => {
4 | const value = walker.nextNode();
5 | return {
6 | value,
7 | done: !value
8 | };
9 | };
10 | walker[Symbol.iterator] = () => ({next});
11 | return walker;
12 | };
13 |
--------------------------------------------------------------------------------
/src/libs/icons.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 |
3 | export const check = () => ;
4 |
5 | export const mute = () => ;
6 |
7 | export const edit = () => ;
8 |
9 | export const openIssue = () => ;
10 |
11 | export const closedIssue = () => ;
12 |
13 | export const openPullRequest = () => ;
14 |
15 | export const closedPullRequest = () => ;
16 |
17 | export const mergedPullRequest = () => ;
18 |
19 | export const tag = () => ;
20 |
21 | export const cloudUpload = () => ;
22 |
23 | export const darkCompare = () => ;
24 |
25 | export const graph = () => ;
26 |
27 | export const code = () => ;
28 |
29 | export const dependency = () => ;
30 |
31 | export const triangleDown = () => ;
32 |
--------------------------------------------------------------------------------
/src/libs/page-detect.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define, Allows alphabetical order */
2 | /* eslint-disable unicorn/prefer-starts-ends-with, The tested var might not be a string */
3 |
4 | import {check as isReserved} from 'github-reserved-names';
5 |
6 | // Drops leading and trailing slash to avoid /\/?/ everywhere
7 | export const getCleanPathname = () => location.pathname.replace(/^[/]|[/]$/g, '');
8 |
9 | // Parses a repo's subpage, e.g.
10 | // '/user/repo/issues/' -> 'issues'
11 | // '/user/repo/' -> ''
12 | // returns false if the path is not a repo
13 | export const getRepoPath = () => {
14 | if (!isRepo()) {
15 | return false;
16 | }
17 | const match = /^[^/]+[/][^/]+[/]?(.*)$/.exec(getCleanPathname());
18 | return match && match[1];
19 | };
20 |
21 | export const getRepoURL = () => location.pathname.slice(1).split('/', 2).join('/');
22 |
23 | export const getOwnerAndRepo = () => {
24 | const [, ownerName, repoName] = location.pathname.split('/');
25 | return {ownerName, repoName};
26 | };
27 |
28 | export const is404 = () => document.title.startsWith('Page not found');
29 |
30 | export const isBlame = () => /^blame\//.test(getRepoPath());
31 |
32 | export const isCommit = () => isSingleCommit() || isPRCommit();
33 |
34 | export const isCommitList = () => /^commits\//.test(getRepoPath());
35 |
36 | export const isCompare = () => /^compare/.test(getRepoPath());
37 |
38 | export const isDashboard = () => /^((orgs[/][^/]+[/])?dashboard([/]index[/]\d+)?)?$/.test(getCleanPathname());
39 |
40 | export const isEnterprise = () => location.hostname !== 'github.com' && location.hostname !== 'gist.github.com';
41 |
42 | export const isGist = () => location.hostname.startsWith('gist.') || location.pathname.startsWith('gist/');
43 |
44 | export const isIssue = () => /^issues\/\d+/.test(getRepoPath());
45 |
46 | export const isIssueList = () => /^issues\/?$/.test(getRepoPath());
47 |
48 | export const isIssueSearch = () => location.pathname.startsWith('/issues');
49 |
50 | export const isLabel = () => /^labels\/\w+/.test(getRepoPath());
51 |
52 | export const isLabelList = () => /^labels\/?(((?=\?).*)|$)/.test(getRepoPath());
53 |
54 | export const isMilestone = () => /^milestone\/\d+/.test(getRepoPath());
55 |
56 | export const isMilestoneList = () => /^milestones\/?$/.test(getRepoPath());
57 |
58 | export const isNotifications = () => /^([^/]+[/][^/]+\/)?notifications/.test(getCleanPathname());
59 |
60 | export const isPR = () => /^pull\/\d+/.test(getRepoPath());
61 |
62 | export const isPRCommit = () => /^pull\/\d+\/commits\/[0-9a-f]{5,40}/.test(getRepoPath());
63 |
64 | export const isPRFiles = () => /^pull\/\d+\/files/.test(getRepoPath());
65 |
66 | export const isPRList = () => /^pulls\/?$/.test(getRepoPath());
67 |
68 | export const isPRSearch = () => location.pathname.startsWith('/pulls');
69 |
70 | export const isQuickPR = () => isCompare() && /[?&]quick_pull=1(&|$)/.test(location.search);
71 |
72 | export const isReleases = () => /^(releases|tags)/.test(getRepoPath());
73 |
74 | export const isRepo = () => /^[^/]+\/[^/]+/.test(getCleanPathname()) &&
75 | !isReserved(getOwnerAndRepo().ownerName) &&
76 | !isNotifications() &&
77 | !isDashboard() &&
78 | !isGist();
79 |
80 | export const isRepoRoot = () => /^(tree[/][^/]+)?$/.test(getRepoPath());
81 |
82 | export const isRepoSettings = () => /^settings/.test(getRepoPath());
83 |
84 | export const isRepoTree = () => /^tree\//.test(getRepoPath());
85 |
86 | export const isSingleCommit = () => /^commit\/[0-9a-f]{5,40}/.test(getRepoPath());
87 |
88 | export const isSingleFile = () => /^blob\//.test(getRepoPath());
89 |
90 | export const isTrending = () => location.pathname.startsWith('/trending');
91 |
--------------------------------------------------------------------------------
/src/libs/synchronous-storage.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Allows usage of async get/set API synchronously.
3 | * Requirements:
4 | * - SynchronousStorage must be the only way to get/set the storage
5 | * - The source API must be promised
6 | * - The first call must be awaited to make sure the value has been loaded
7 | *
8 | * Usage:
9 |
10 | import SynchronousStorage from './synchronous-storage';
11 |
12 | const storage = await new SynchronousStorage(
13 | () => browser.storage.local.get('name'),
14 | va => browser.storage.local.set({name: va})
15 | );
16 |
17 | console.log(storage.get()); // {}
18 | storage.set('Federico');
19 | console.log(storage.get()); // {name: 'Federico'}
20 |
21 | *
22 | * Caveats:
23 | * - .set() returns a promise that you can use to catch write errors.
24 | * However if an error happens, SynchronousStorage will no longer match the real cache.
25 | */
26 | export default class SynchronousStorage {
27 | constructor(get, set) {
28 | this._get = get;
29 | this._set = set;
30 | return get().then(value => {
31 | this._cache = value;
32 | return this;
33 | });
34 | }
35 | get() {
36 | return this._cache;
37 | }
38 | set(value) {
39 | this._cache = value;
40 | return this._set(value);
41 | }
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/src/libs/utils.js:
--------------------------------------------------------------------------------
1 | import {h} from 'dom-chef';
2 | import select from 'select-dom';
3 | import elementReady from 'element-ready';
4 | import domLoaded from 'dom-loaded';
5 |
6 | /**
7 | * Prevent fn's errors from blocking the remaining tasks.
8 | * https://github.com/sindresorhus/refined-github/issues/678
9 | * The code looks weird but it's synchronous and fn is called without args.
10 | */
11 | export const safely = async fn => fn();
12 |
13 | export const getUsername = () => select('meta[name="user-login"]').getAttribute('content');
14 |
15 | export const groupBy = (array, grouper) => array.reduce((map, item) => {
16 | const key = grouper(item);
17 | map[key] = map[key] || [];
18 | map[key].push(item);
19 | return map;
20 | }, {});
21 |
22 | export const emptyElement = element => {
23 | // https://stackoverflow.com/a/3955238/288906
24 | while (element.firstChild) {
25 | element.firstChild.remove();
26 | }
27 | };
28 |
29 | /**
30 | * Automatically stops checking for an element to appear once the DOM is ready.
31 | */
32 | export const safeElementReady = selector => {
33 | const waiting = elementReady(selector);
34 |
35 | // Don't check ad-infinitum
36 | domLoaded.then(() => requestAnimationFrame(() => waiting.cancel()));
37 |
38 | // If cancelled, return null like a regular select() would
39 | return waiting.catch(() => null);
40 | };
41 |
42 | export const observeEl = (el, listener, options = {childList: true}) => {
43 | if (typeof el === 'string') {
44 | el = select(el);
45 | }
46 |
47 | if (!el) {
48 | return;
49 | }
50 |
51 | // Run first
52 | listener([]);
53 |
54 | // Run on updates
55 | const observer = new MutationObserver(listener);
56 | observer.observe(el, options);
57 | return observer;
58 | };
59 |
60 | // Concats arrays but does so like a zipper instead of appending them
61 | // [[0, 1, 2], [0, 1]] => [0, 0, 1, 1, 2]
62 | // Like lodash.zip
63 | export const flatZip = (table, limit = Infinity) => {
64 | const maxColumns = Math.max(...table.map(row => row.length));
65 | const zipped = [];
66 | for (let col = 0; col < maxColumns; col++) {
67 | for (const row of table) {
68 | if (row[col]) {
69 | zipped.push(row[col]);
70 | if (limit !== Infinity && zipped.length === limit) {
71 | return zipped;
72 | }
73 | }
74 | }
75 | }
76 | return zipped;
77 | };
78 |
79 | export const isMac = /Mac/.test(window.navigator.platform);
80 |
81 | export const metaKey = isMac ? 'metaKey' : 'ctrlKey';
82 |
83 | export const groupButtons = buttons => {
84 | // Ensure every button has this class
85 | $(buttons).addClass('BtnGroup-item');
86 |
87 | // They may already be part of a group
88 | let group = buttons[0].closest('.BtnGroup');
89 |
90 | // If it doesn't exist, wrap them in a new group
91 | if (!group) {
92 | group =
;
93 | $(buttons).wrapAll(group);
94 | }
95 |
96 | return group;
97 | };
98 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | import OptionsSync from 'webext-options-sync';
2 |
3 | new OptionsSync().syncForm('#options-form');
4 |
5 | /**
6 | * GitHub Enterprise support
7 | */
8 | const cdForm = document.querySelector('#custom-domain');
9 | const cdInput = document.querySelector('#custom-domain-origin');
10 |
11 | cdForm.addEventListener('submit', async event => {
12 | event.preventDefault();
13 |
14 | const origin = new URL(cdInput.value).origin;
15 |
16 | if (origin) {
17 | const granted = await browser.permissions.request({
18 | origins: [
19 | `${origin}/*`
20 | ]
21 | });
22 | if (granted) {
23 | cdForm.reset();
24 | }
25 | }
26 | });
27 |
--------------------------------------------------------------------------------
/test/copy-markdown.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import {stripIndent} from 'common-tags';
3 | import {getSmarterMarkdown} from '../src/features/copy-markdown';
4 |
5 | test('base markdown', t => {
6 | t.is(
7 | getSmarterMarkdown('this is markdown '),
8 | '[this](url) is **markdown**'
9 | );
10 | });
11 |
12 | test('drop ', t => {
13 | t.is(
14 | getSmarterMarkdown('🔥 '),
15 | '🔥'
16 | );
17 | });
18 |
19 | test('drop tasks from lists', t => {
20 | t.is(
21 | getSmarterMarkdown(stripIndent`
22 |
26 | `),
27 | stripIndent`
28 | * try me out
29 | * test across lines
30 | `
31 | );
32 | });
33 |
34 | test('drop autolinks around images', t => {
35 | t.is(
36 | getSmarterMarkdown(stripIndent`
37 |
38 | `),
39 | stripIndent`
40 | 
41 | `
42 | );
43 | });
44 |
45 | test('keep img tags if they have width, height or align', t => {
46 | t.is(
47 | getSmarterMarkdown(stripIndent`
48 |
49 | `),
50 | stripIndent`
51 |
52 | `
53 | );
54 | });
55 |
56 | test('drop autolinks from issue links and commit links', t => {
57 | t.is(
58 | getSmarterMarkdown(stripIndent`
59 | #522
60 | `),
61 | 'https://github.com/sindresorhus/refined-github/issues/522'
62 | );
63 | t.is(
64 | getSmarterMarkdown(stripIndent`
65 | 833d598
66 | `),
67 | 'https://github.com/sindresorhus/refined-github/commit/833d5984fffb18a44b83d965b397f82e0ff3085e'
68 | );
69 | });
70 |
71 | test('drop autolinks around some shortened links', t => {
72 | t.is(
73 | getSmarterMarkdown(stripIndent`
74 | npmjs.com
75 | twitter.com/bfred_it
76 | github.com
77 | `),
78 | stripIndent`
79 | https://www.npmjs.com/
80 |
81 | https://twitter.com/bfred_it
82 |
83 | https://github.com/
84 | `
85 | );
86 | });
87 |
88 | test('wrap orphaned li in their original parent', t => {
89 | t.is(
90 | getSmarterMarkdown(stripIndent`
91 |
92 | big lists
93 | deserve big numbers
94 |
95 | `),
96 | stripIndent`
97 | 99. big lists
98 | 100. deserve big numbers
99 | `
100 | );
101 | });
102 |
--------------------------------------------------------------------------------
/test/fixtures/window.js:
--------------------------------------------------------------------------------
1 | const URL = require('url').URL;
2 |
3 | function WindowMock(initialURI = 'https://github.com') {
4 | this.location = new URL(initialURI);
5 | }
6 |
7 | module.exports = WindowMock;
8 |
--------------------------------------------------------------------------------
/test/page-detect.js:
--------------------------------------------------------------------------------
1 | import test from 'ava';
2 | import * as pageDetect from '../src/libs/page-detect';
3 | import Window from './fixtures/window';
4 |
5 | global.window = new Window();
6 | global.location = window.location;
7 | global.document = {};
8 |
9 | function urlMatcherMacro(t, detectFn, shouldMatch = [], shouldNotMatch = []) {
10 | for (const url of shouldMatch) {
11 | location.href = url;
12 | t.true(detectFn(url));
13 | }
14 |
15 | for (const url of shouldNotMatch) {
16 | location.href = url;
17 | t.false(detectFn(url));
18 | }
19 | }
20 |
21 | test('getRepoPath', t => {
22 | const pairs = new Map([
23 | [
24 | 'https://github.com',
25 | false
26 | ],
27 | [
28 | 'https://gist.github.com/',
29 | false
30 | ],
31 | [
32 | 'https://github.com/settings/developers',
33 | false
34 | ],
35 | [
36 | 'https://github.com/sindresorhus/notifications/notifications',
37 | false
38 | ],
39 | [
40 | 'https://github.com/sindresorhus/refined-github',
41 | ''
42 | ],
43 | [
44 | 'https://github.com/sindresorhus/refined-github/',
45 | ''
46 | ],
47 | [
48 | 'https://github.com/sindresorhus/refined-github/blame/master/package.json',
49 | 'blame/master/package.json'
50 | ],
51 | [
52 | 'https://github.com/sindresorhus/refined-github/commit/57bf4',
53 | 'commit/57bf4'
54 | ],
55 | [
56 | 'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=0',
57 | 'compare/test-branch'
58 | ],
59 | [
60 | 'https://github.com/sindresorhus/refined-github/tree/master/extension',
61 | 'tree/master/extension'
62 | ]
63 | ]);
64 |
65 | for (const [url, result] of pairs) {
66 | location.href = url;
67 | t.is(result, pageDetect.getRepoPath(url));
68 | }
69 | });
70 |
71 | test('getOwnerAndRepo', t => {
72 | const ownerAndRepo = {
73 | 'https://github.com/sindresorhus/refined-github/pull/148': {
74 | ownerName: 'sindresorhus',
75 | repoName: 'refined-github'
76 | },
77 | 'https://github.com/DrewML/GifHub/blob/master/.gitignore': {
78 | ownerName: 'DrewML',
79 | repoName: 'GifHub'
80 | }
81 | };
82 |
83 | Object.keys(ownerAndRepo).forEach(url => {
84 | location.href = url;
85 | t.deepEqual(ownerAndRepo[url], pageDetect.getOwnerAndRepo());
86 | });
87 | });
88 |
89 | test('is404', t => {
90 | document.title = 'Page not found • GitHub';
91 | t.true(pageDetect.is404());
92 |
93 | document.title = 'examples/404: Page not found examples';
94 | t.false(pageDetect.is404());
95 |
96 | document.title = 'Dashboard';
97 | t.false(pageDetect.is404());
98 | });
99 |
100 | test('isBlame', urlMatcherMacro, pageDetect.isBlame, [
101 | 'https://github.com/sindresorhus/refined-github/blame/master/package.json'
102 | ], [
103 | 'https://github.com/sindresorhus/refined-github/blob/master/package.json'
104 | ]);
105 |
106 | test('isCommit', urlMatcherMacro, pageDetect.isCommit, [
107 | 'https://github.com/sindresorhus/refined-github/commit/5b614b9035f2035b839f48b4db7bd5c3298d526f',
108 | 'https://github.com/sindresorhus/refined-github/commit/5b614',
109 | 'https://github.com/sindresorhus/refined-github/pull/148/commits/0019603b83bd97c2f7ef240969f49e6126c5ec85',
110 | 'https://github.com/sindresorhus/refined-github/pull/148/commits/00196'
111 | ], [
112 | 'https://github.com/sindresorhus/refined-github/pull/148/commits',
113 | 'https://github.com/sindresorhus/refined-github/branches',
114 | 'https://github.com/sindresorhus/refined-github/pull/148',
115 | 'https://github.com/sindresorhus/refined-github/pull/commits',
116 | 'https://github.com/sindresorhus/refined-github/pulls'
117 | ]);
118 |
119 | test('isCommitList', urlMatcherMacro, pageDetect.isCommitList, [
120 | 'https://github.com/sindresorhus/refined-github/commits/master?page=2',
121 | 'https://github.com/sindresorhus/refined-github/commits/test-branch',
122 | 'https://github.com/sindresorhus/refined-github/commits/0.13.0',
123 | 'https://github.com/sindresorhus/refined-github/commits/230c2',
124 | 'https://github.com/sindresorhus/refined-github/commits/230c2935fc5aea9a681174ddbeba6255ca040d63'
125 | ], [
126 | 'https://github.com/sindresorhus/refined-github/pull/148',
127 | 'https://github.com/sindresorhus/refined-github/pull/commits',
128 | 'https://github.com/sindresorhus/refined-github/branches'
129 | ]);
130 |
131 | test('isCompare', urlMatcherMacro, pageDetect.isCompare, [
132 | 'https://github.com/sindresorhus/refined-github/compare',
133 | 'https://github.com/sindresorhus/refined-github/compare/'
134 | ], [
135 | 'https://github.com/sindresorhus/refined-github',
136 | 'https://github.com/sindresorhus/refined-github/graphs'
137 | ]);
138 |
139 | test('isDashboard', urlMatcherMacro, pageDetect.isDashboard, [
140 | 'https://github.com/',
141 | 'https://github.com',
142 | 'https://github.com/orgs/test/dashboard',
143 | 'https://github.com/dashboard/index/2',
144 | 'https://github.com/dashboard'
145 | ], [
146 | 'https://github.com/sindresorhus/refined-github/tree/master/dashboard/index/2',
147 | 'https://github.com/sindresorhus/dashboard',
148 | 'https://github.com/sindresorhus'
149 | ]);
150 |
151 | test('isEnterprise', urlMatcherMacro, pageDetect.isEnterprise, [
152 | 'https://github.big-corp.com/',
153 | 'https://not-github.com/',
154 | 'https://my-little-hub.com/'
155 | ], [
156 | 'https://github.com/',
157 | 'https://gist.github.com/'
158 | ]);
159 |
160 | test('isGist', urlMatcherMacro, pageDetect.isGist, [
161 | 'https://gist.github.com',
162 | 'http://gist.github.com',
163 | 'https://gist.github.com/sindresorhus/0ea3c2845718a0a0f0beb579ff14f064'
164 | ], [
165 | 'https://github.com',
166 | 'https://help.github.com/'
167 | ]);
168 |
169 | test('isIssue', urlMatcherMacro, pageDetect.isIssue, [
170 | 'https://github.com/sindresorhus/refined-github/issues/146'
171 | ], [
172 | 'http://github.com/sindresorhus/ava',
173 | 'https://github.com',
174 | 'https://github.com/sindresorhus/refined-github/issues'
175 | ]);
176 |
177 | test('isIssueList', urlMatcherMacro, pageDetect.isIssueList, [
178 | 'http://github.com/sindresorhus/ava/issues'
179 | ], [
180 | 'http://github.com/sindresorhus/ava',
181 | 'https://github.com',
182 | 'https://github.com/sindresorhus/refined-github/issues/170'
183 | ]);
184 |
185 | test('isIssueSearch', urlMatcherMacro, pageDetect.isIssueSearch, [
186 | 'https://github.com/issues'
187 | ], [
188 | 'https://github.com/sindresorhus/refined-github/issues',
189 | 'https://github.com/sindresorhus/refined-github/issues/170'
190 | ]);
191 |
192 | test('isMilestone', urlMatcherMacro, pageDetect.isMilestone, [
193 | 'https://github.com/sindresorhus/refined-github/milestone/12'
194 | ], [
195 | 'http://github.com/sindresorhus/ava',
196 | 'https://github.com',
197 | 'https://github.com/sindresorhus/refined-github/milestones'
198 | ]);
199 |
200 | test('isNotifications', urlMatcherMacro, pageDetect.isNotifications, [
201 | 'https://github.com/notifications',
202 | 'https://github.com/notifications/participating',
203 | 'http://github.com/sindresorhus/refined-github/notifications',
204 | 'https://github.com/sindresorhus/notifications/notifications',
205 | 'https://github.com/notifications?all=1'
206 | ], [
207 | 'https://github.com/settings/notifications',
208 | 'https://github.com/watching',
209 | 'https://github.com/sindresorhus/notifications/',
210 | 'https://github.com/jaredhanson/node-notifications/tree/master/lib/notifications'
211 | ]);
212 |
213 | test('isPR', urlMatcherMacro, pageDetect.isPR, [
214 | 'https://github.com/sindresorhus/refined-github/pull/148'
215 | ], [
216 | 'http://github.com/sindresorhus/ava',
217 | 'https://github.com',
218 | 'https://github.com/sindresorhus/refined-github/pulls'
219 | ]);
220 |
221 | test('isPRCommit', urlMatcherMacro, pageDetect.isPRCommit, [
222 | 'https://github.com/sindresorhus/refined-github/pull/148/commits/0019603b83bd97c2f7ef240969f49e6126c5ec85',
223 | 'https://github.com/sindresorhus/refined-github/pull/148/commits/00196'
224 | ], [
225 | 'https://github.com/sindresorhus/refined-github/pull/148',
226 | 'https://github.com/sindresorhus/refined-github/pull/commits',
227 | 'https://github.com/sindresorhus/refined-github/pulls'
228 | ]);
229 |
230 | test('isPRFiles', urlMatcherMacro, pageDetect.isPRFiles, [
231 | 'https://github.com/sindresorhus/refined-github/pull/148/files'
232 | ], [
233 | 'https://github.com/sindresorhus/refined-github/pull/148',
234 | 'https://github.com/sindresorhus/refined-github/pull/commits',
235 | 'https://github.com/sindresorhus/refined-github/pulls'
236 | ]);
237 |
238 | test('isPRList', urlMatcherMacro, pageDetect.isPRList, [
239 | 'https://github.com/sindresorhus/refined-github/pulls'
240 | ], [
241 | 'http://github.com/sindresorhus/ava',
242 | 'https://github.com',
243 | 'https://github.com/sindresorhus/refined-github/pull/148'
244 | ]);
245 |
246 | test('isPRSearch', urlMatcherMacro, pageDetect.isPRSearch, [
247 | 'https://github.com/pulls'
248 | ], [
249 | 'https://github.com/sindresorhus/refined-github/pulls',
250 | 'https://github.com/sindresorhus/refined-github/pull/148'
251 | ]);
252 |
253 | test('isQuickPR', urlMatcherMacro, pageDetect.isQuickPR, [
254 | 'https://github.com/sindresorhus/refined-github/compare/master...branch-name?quick_pull=1',
255 | 'https://github.com/sindresorhus/refined-github/compare/branch-1...branch-2?quick_pull=1',
256 | 'https://github.com/sindresorhus/refined-github/compare/test-branch?quick_pull=1'
257 | ], [
258 | 'https://github.com/sindresorhus/refined-github',
259 | 'https://github.com/sindresorhus/refined-github/compare',
260 | 'https://github.com/sindresorhus/refined-github/compare/',
261 | 'https://github.com/sindresorhus/refined-github/compare/test-branch',
262 | 'https://github.com/sindresorhus/refined-github/compare/branch-1...branch-2',
263 | 'https://github.com/sindresorhus/refined-github/compare/branch-1...branch-2?expand=1'
264 | ]);
265 |
266 | test('isReleases', urlMatcherMacro, pageDetect.isReleases, [
267 | 'https://github.com/sindresorhus/refined-github/releases'
268 | ], [
269 | 'https://github.com/sindresorhus/refined-github',
270 | 'https://github.com/sindresorhus/refined-github/graphs'
271 | ]);
272 |
273 | test('isRepo', urlMatcherMacro, pageDetect.isRepo, [
274 | 'http://github.com/sindresorhus/refined-github',
275 | 'https://github.com/sindresorhus/refined-github/issues/146',
276 | 'https://github.com/sindresorhus/notifications/',
277 | 'https://github.com/sindresorhus/refined-github/pull/145'
278 | ], [
279 | 'https://github.com/sindresorhus',
280 | 'https://github.com',
281 | 'https://github.com/stars',
282 | 'http://github.com/sindresorhus/refined-github/notifications',
283 | 'https://github.com/sindresorhus/notifications/notifications',
284 | 'https://github.com/orgs/test/dashboard',
285 | 'https://github.com/settings/profile',
286 | 'https://github.com/trending/developers'
287 | ]);
288 |
289 | test('isRepoRoot', urlMatcherMacro, pageDetect.isRepoRoot, [
290 | 'https://github.com/sindresorhus/refined-github',
291 | 'https://github.com/sindresorhus/refined-github/',
292 | 'https://github.com/sindresorhus/refined-github/tree/native-copy-buttons',
293 | 'https://github.com/sindresorhus/refined-github/tree/native-copy-buttons/',
294 | 'https://github.com/sindresorhus/refined-github/tree/03fa6b8b4d6e68dea9dc9bee1d197ef5d992fbd6',
295 | 'https://github.com/sindresorhus/refined-github/tree/03fa6b8b4d6e68dea9dc9bee1d197ef5d992fbd6/'
296 | ], [
297 | 'https://github.com/',
298 | 'https://github.com/tree/master/issues',
299 | 'https://github.com/sindresorhus/refined-github/issues',
300 | 'https://github.com/sindresorhus/refined-github/tree/master/extension',
301 | 'https://github.com/sindresorhus/refined-github/tree/master/tree/master'
302 | ]);
303 |
304 | test('isRepoSettings', urlMatcherMacro, pageDetect.isRepoSettings, [
305 | 'https://github.com/sindresorhus/refined-github/settings',
306 | 'https://github.com/sindresorhus/refined-github/settings/branches'
307 | ], [
308 | 'https://github.com/sindresorhus/refined-github/releases'
309 | ]);
310 |
311 | test('isRepoTree', urlMatcherMacro, pageDetect.isRepoTree, [
312 | 'https://github.com/sindresorhus/refined-github/tree/master/extension',
313 | 'https://github.com/sindresorhus/refined-github/tree/0.13.0/extension',
314 | 'https://github.com/sindresorhus/refined-github/tree/57bf435ee12d14b482df0bbd88013a2814c7512e/extension',
315 | 'https://github.com/sindresorhus/refined-github/tree/57bf4'
316 | ], [
317 | 'https://github.com/sindresorhus/refined-github/issues',
318 | 'https://github.com/sindresorhus/refined-github'
319 | ]);
320 |
321 | test('isSingleCommit', urlMatcherMacro, pageDetect.isSingleCommit, [
322 | 'https://github.com/sindresorhus/refined-github/commit/5b614b9035f2035b839f48b4db7bd5c3298d526f',
323 | 'https://github.com/sindresorhus/refined-github/commit/5b614'
324 | ], [
325 | 'https://github.com/sindresorhus/refined-github/pull/148/commits',
326 | 'https://github.com/sindresorhus/refined-github/branches'
327 | ]);
328 |
329 | test('isSingleFile', urlMatcherMacro, pageDetect.isSingleFile, [
330 | 'https://github.com/sindresorhus/refined-github/blob/master/.gitattributes',
331 | 'https://github.com/sindresorhus/refined-github/blob/fix-narrow-diff/extension/content.css'
332 | ], [
333 | 'https://github.com/sindresorhus/refined-github/pull/164/files',
334 | 'https://github.com/sindresorhus/refined-github/commit/57bf4'
335 | ]);
336 |
337 | test('isTrending', urlMatcherMacro, pageDetect.isTrending, [
338 | 'https://github.com/trending',
339 | 'https://github.com/trending/developers',
340 | 'https://github.com/trending/unknown'
341 | ], [
342 | 'https://github.com/settings/trending',
343 | 'https://github.com/watching',
344 | 'https://github.com/jaredhanson/node-trending/tree/master/lib/trending'
345 | ]);
346 |
347 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('path');
3 | const webpack = require('webpack');
4 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
5 | const CopyWebpackPlugin = require('copy-webpack-plugin');
6 |
7 | module.exports = {
8 | entry: {
9 | content: './src/content',
10 | background: './src/background',
11 | options: './src/options'
12 | },
13 | plugins: [
14 | new webpack.DefinePlugin({
15 | process: '0'
16 | }),
17 | new webpack.optimize.ModuleConcatenationPlugin(),
18 | new CopyWebpackPlugin([{
19 | from: 'node_modules/webextension-polyfill/dist/browser-polyfill.min.js'
20 | }, {
21 | from: 'node_modules/jquery/dist/jquery.slim.min.js'
22 | }])
23 | ],
24 | output: {
25 | path: path.join(__dirname, 'extension'),
26 | filename: '[name].js'
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js$/,
32 | exclude: /node_modules/,
33 | use: {
34 | loader: 'babel-loader'
35 | }
36 | }
37 | ]
38 | }
39 | };
40 |
41 | if (process.env.NODE_ENV === 'production') {
42 | module.exports.plugins.push(
43 | new UglifyJSPlugin({
44 | sourceMap: true,
45 | uglifyOptions: {
46 | mangle: false,
47 | output: {
48 | // Keep it somewhat readable for AMO reviewers
49 | beautify: true,
50 |
51 | // Reduce beautification indentation from 4 spaces to 1 to save space
52 | indent_level: 1 // eslint-disable-line camelcase
53 | }
54 | }
55 | })
56 | );
57 | } else {
58 | module.exports.devtool = 'source-map';
59 | }
60 |
--------------------------------------------------------------------------------