├── .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 |
15 |

News feed

16 |

17 | 21 |

22 |
23 |
24 |

GitHub Enterprise support

25 |

26 | 27 | 28 |

29 |
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 | 37 | 38 | 39 | 40 | 43 | 44 |
35 | Dashboard cleanup 36 |
41 | 42 |
45 | 46 | 47 | 48 | 52 | 55 | 56 | 57 | 58 | 61 | 64 | 65 |
49 | Mark issues and pull requests as unread
50 | (They will reappear in Notifications) 51 |
53 | Preserves the original Markdown when you copy text from comments 54 |
59 | 60 | 62 | 63 |
66 | 67 | 68 | 69 | 72 | 75 | 76 | 77 | 78 | 81 | 84 | 85 |
70 | Reaction avatars 71 | 73 | Moves destructive buttons in commenting forms away from the primary button 74 |
79 | 80 | 82 | 83 |
86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 98 | 101 | 102 |
90 | Linkifies issue/PR references in code, comments and titles 91 |
96 | 97 | 99 | 100 |
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 (ShiftTab 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) *(dw 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) *(gr 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. *(gt 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 | 209 | 210 | 211 | 212 | 220 | 227 | 228 |
ChromeFirefox
213 |
    214 |
  1. Open chrome://extensions 215 |
  2. Check the Developer mode checkbox 216 |
  3. Click on the Load unpacked extension button 217 |
  4. Select the folder refined-github/extension 218 |
219 |
221 |
    222 |
  1. Open about:debugging#addons 223 |
  2. Click on the Load Temporary Add-on button 224 |
  3. Select the file refined-github/extension/manifest.json 225 |
226 |
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 |
26 | 30 | {hidingWhitespace ? icons.check() : ''} 31 | {' '} 32 | No Whitespace 33 | 34 |
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 | 17 |
18 | Everything commented by you 19 |
20 |
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 | 23 | {icons.code()} 24 | 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 |
  • 14 | Trending 15 |
  • 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 | 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 | 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 = ; 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 |
    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 {`@${participant.username}`}; 181 | }); 182 | 183 | const item = ( 184 |
  • 185 | 186 | {icon} 187 | 188 | 189 | {title} 190 | 191 | 192 | 193 |
      194 |
    • 195 | 198 |
    • 199 | 200 |
    • 201 | 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 |

    Are you sure?

    290 | 291 |

    Are you sure you want to mark all unread notifications as read?

    292 | 293 |
    294 | 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 |
    18 | 19 | 38 |
    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 | 36 | Original Poster 37 | 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 |

    Are you sure?

    45 | 46 |

    Are you sure you want to open {unreadCount} tabs?

    47 | 48 |
    49 | 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 | 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 |
      23 |
    • try me out
    • 24 |
    • test across lines
    • 25 |
    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 | ![](https://camo.githubusercontent.com/7a0ef30dc39981585543e0bbd816392a52dddd8a/687474703a2f2f692e696d6775722e636f6d2f4b6361644c36472e706e67) 41 | ` 42 | ); 43 | }); 44 | 45 | test('keep img tags if they have width, height or align', t => { 46 | t.is( 47 | getSmarterMarkdown(stripIndent` 48 | copy 49 | `), 50 | stripIndent` 51 | copy 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 |
    1. big lists
    2. 93 |
    3. deserve big numbers
    4. 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 | --------------------------------------------------------------------------------