├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .node-version ├── LICENSE.md ├── README.md ├── dist ├── options.html ├── options.js └── script.js ├── img ├── icon-128.png ├── icon-16.png └── icon-48.png ├── manifest.json ├── package-lock.json ├── package.json ├── script ├── build └── watch ├── src ├── options.html ├── options.ts ├── script.ts └── style.scss ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/typescript"], 3 | "plugins": [ 4 | "@babel/proposal-class-properties", 5 | "@babel/proposal-object-rest-spread" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | "env": { 12 | "browser": true 13 | }, 14 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/jquery/src 2 | .sass-cache 3 | node_modules 4 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 14.4.0 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ben Balter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Mention Highlighter 2 | 3 | *Highlight user and team mentions on GitHub for easier skimming* 4 | 5 | --- 6 | 7 | # I'm very happy to say, this feature is now [native to GitHub.com](https://twitter.com/github/status/1446164732315066371) - no extension needed! 🎉 8 | 9 | ## Usage 10 | 11 | This Chrome extension automatically highlights any time you are mentioned on a GitHub issue or pull request thread by highlighting your username, any team you're a member of, and the border of any containing comment. 12 | 13 | ![Screenshot](https://cloud.githubusercontent.com/assets/282759/3424209/bbddde02-ffc3-11e3-8cf8-089867a503e7.png) 14 | 15 | Navigate to any GitHub issue you've been mentioned in and your mentions should now be more easily visible. There's no configuration necessary. 16 | 17 | ## Installation 18 | 19 | Simply [visit the Chrome Web Store listing](https://chrome.google.com/webstore/detail/github-mention-highlighte/ojclbekffnkgbacniibdebdihhgenlkp) and click "Add". The extension will update automatically. 20 | 21 | ## Developing locally 22 | 23 | 1. Clone down the repository 24 | 2. Open Tools -> Extensions 25 | 3. Check the "Developer Mode" option (if not already) 26 | 4. Select "Load unpacked extension" 27 | 5. Navigate to the recently cloned folder and click select 28 | -------------------------------------------------------------------------------- /dist/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub Mention Highlighter Options 5 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | You'll need to create a 16 | person access token
with the read:org scope. 17 |

18 | 19 |
20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /dist/options.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = ""; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./src/options.ts"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./src/options.ts": 90 | /*!************************!*\ 91 | !*** ./src/options.ts ***! 92 | \************************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports) { 95 | 96 | eval("var defaultOptions = {\n token: \"\",\n lastChecked: 0\n};\nvar saveBtn = document.getElementById(\"save\");\nvar token = document.getElementById(\"token\");\nvar statusEl = document.getElementById(\"status\");\n\nvar saveOptions = function saveOptions() {\n chrome.storage.sync.set({\n token: token.value\n }, function () {\n if (statusEl) {\n statusEl.innerText = \"Options saved.\";\n setTimeout(function () {\n return statusEl.innerText = \"\";\n }, 750);\n }\n });\n};\n\nvar restoreOptions = function restoreOptions() {\n return chrome.storage.sync.get(defaultOptions, function (items) {\n if (token) {\n token.value = items.token;\n }\n });\n};\n\ndocument.addEventListener(\"DOMContentLoaded\", restoreOptions);\n\nif (saveBtn) {\n saveBtn.addEventListener(\"click\", saveOptions);\n}\n\n//# sourceURL=webpack:///./src/options.ts?"); 97 | 98 | /***/ }) 99 | 100 | /******/ }); -------------------------------------------------------------------------------- /img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benbalter/github-mention-highlighter/54bf7e279cf7248d534698d14d32c940c741bb68/img/icon-128.png -------------------------------------------------------------------------------- /img/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benbalter/github-mention-highlighter/54bf7e279cf7248d534698d14d32c940c741bb68/img/icon-16.png -------------------------------------------------------------------------------- /img/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benbalter/github-mention-highlighter/54bf7e279cf7248d534698d14d32c940c741bb68/img/icon-48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitHub Mention Highlighter", 3 | "version": "0.3.0", 4 | "description": "Highlight user and team mentions on GitHub", 5 | "permissions": [ 6 | "https://*.github.com/*", 7 | "https://*.githubapp.com/*", 8 | "https://mail.google.com/*", 9 | "storage" 10 | ], 11 | "content_scripts": [{ 12 | "matches": [ 13 | "https://github.com/*", 14 | "https://*.githubapp.com/*", 15 | "https://mail.google.com/*" 16 | ], 17 | "js": ["dist/script.js"] 18 | }], 19 | "icons": { 20 | "16": "img/icon-16.png", 21 | "48": "img/icon-48.png", 22 | "128": "img/icon-128.png" 23 | }, 24 | "manifest_version": 2, 25 | "options_ui": { 26 | "page": "dist/options.html", 27 | "browser_style": true 28 | }, 29 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 30 | "browser_specific_settings": { 31 | "gecko": { 32 | "id": "github-mention-highlighter@balter.com", 33 | "strict_min_version": "42.0" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-mention-highlighter", 3 | "version": "0.2.1", 4 | "description": "Highlight user and team mentions on GitHub", 5 | "main": "script.js", 6 | "scripts": { 7 | "crx": "crx pack", 8 | "build": "webpack", 9 | "watch": "webpack --watch --mode=development", 10 | "test": "eslint ./src --ext .js,.jsx,.ts,.tsx", 11 | "fix": "eslint ./src --fix --ext .js,.jsx,.ts,.tsx" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/benbalter/github-mention-highlighter" 16 | }, 17 | "author": "Ben Balter", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/benbalter/github-mention-highlighter/issues" 21 | }, 22 | "dependencies": { 23 | "@types/copy-webpack-plugin": "^6.4.0", 24 | "jquery": "^3.5.0", 25 | "scss": "~0.2.4" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.12.10", 29 | "@babel/core": "^7.12.10", 30 | "@babel/plugin-proposal-class-properties": "^7.12.1", 31 | "@babel/preset-env": "^7.12.11", 32 | "@babel/preset-typescript": "^7.12.7", 33 | "@types/chrome": "0.0.122", 34 | "@types/jquery": "^3.5.5", 35 | "@typescript-eslint/eslint-plugin": "^3.10.1", 36 | "@typescript-eslint/parser": "^3.10.1", 37 | "babel-loader": "^8.2.2", 38 | "babel-preset-env": "^1.7.0", 39 | "babel-preset-minify": "^0.5.1", 40 | "copy-webpack-plugin": "^6.4.1", 41 | "crx": "^5.0.1", 42 | "crx-webpack-plugin": "^0.1.6", 43 | "css-loader": "^4.3.0", 44 | "eslint": "^7.17.0", 45 | "sass": "^1.32.0", 46 | "sass-loader": "^9.0.3", 47 | "style-loader": "^1.3.0", 48 | "ts-loader": "^8.0.14", 49 | "typescript": "^3.9.7", 50 | "webpack": "^4.44.2", 51 | "webpack-cli": "^3.3.11" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | grunt build 4 | -------------------------------------------------------------------------------- /script/watch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | grunt watch 4 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub Mention Highlighter Options 5 | 9 | 10 | 11 | 12 | 13 | 14 |

15 | You'll need to create a 16 | person access token
with the read:org scope. 17 |

18 | 19 |
20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/options.ts: -------------------------------------------------------------------------------- 1 | const defaultOptions = { 2 | token: "", 3 | lastChecked: 0, 4 | }; 5 | 6 | const saveBtn = document.getElementById("save"); 7 | const token = document.getElementById("token"); 8 | const statusEl = document.getElementById("status"); 9 | 10 | const saveOptions = () => { 11 | chrome.storage.sync.set({token: token.value}, () => { 12 | if (statusEl) { 13 | statusEl.innerText = "Options saved."; 14 | setTimeout(() => (statusEl.innerText = ""), 750); 15 | } 16 | }); 17 | } 18 | const restoreOptions = () => 19 | chrome.storage.sync.get(defaultOptions, (items) => { 20 | if (token) { 21 | token.value = items.token; 22 | } 23 | }); 24 | 25 | document.addEventListener("DOMContentLoaded", restoreOptions); 26 | 27 | if (saveBtn) { 28 | saveBtn.addEventListener("click", saveOptions); 29 | } 30 | -------------------------------------------------------------------------------- /src/script.ts: -------------------------------------------------------------------------------- 1 | import * as $ from "jquery"; 2 | import "./style.scss"; 3 | 4 | interface Options { 5 | token: string; 6 | login: string; 7 | teams: string[]; 8 | lastChecked: number; 9 | } 10 | 11 | interface User { 12 | login: string; 13 | } 14 | 15 | interface Team { 16 | slug: string; 17 | organization: Organization; 18 | } 19 | 20 | interface Organization { 21 | login: string; 22 | } 23 | 24 | class GitHubMentionHighlighter implements Options { 25 | token = ""; 26 | login = ""; 27 | teams: string[] = []; 28 | lastChecked = 0; 29 | checkInterval = 86400000; //1000 * 60 * 60 * 24 30 | 31 | handles(): string[] { 32 | return this.teams.concat([this.login]); 33 | } 34 | 35 | mentions(): HTMLElement[] { 36 | const classes = ".user-mention, .member-mention, .team-mention"; 37 | const handles = this.handles(); 38 | const mentions = $(classes).toArray(); 39 | 40 | return mentions.filter((mention) => { 41 | const text = mention.innerText.toLowerCase(); 42 | return text[0] === "@" && handles.includes(text); 43 | }); 44 | } 45 | 46 | highlight() { 47 | const classes = ".timeline-comment, .timeline-entry"; 48 | for (const mention of this.mentions()) { 49 | const $mention = $(mention); 50 | $mention.addClass("highlight"); 51 | $mention.parents(classes).addClass("highlight"); 52 | } 53 | } 54 | 55 | private getUser(successCallback: (user: User) => void) { 56 | return $.ajax({ 57 | dataType: "json", 58 | url: "https://api.github.com/user", 59 | headers: { 60 | Authorization: `token ${this.token}`, 61 | }, 62 | success: (data: User) => { 63 | successCallback(data); 64 | } 65 | }); 66 | } 67 | 68 | private getTeams(successCallback: (teams: string[]) => void) { 69 | return $.ajax({ 70 | dataType: "json", 71 | url: "https://api.github.com/user/teams?per_page=100", 72 | headers: { 73 | Authorization: `token ${this.token}`, 74 | }, 75 | success: (data) => { 76 | const teams: string[] = data.map((team: Team) => { 77 | const org = team["organization"]["login"].toLowerCase(); 78 | const slug = team.slug.toLowerCase(); 79 | return `@${org}/${slug}`; 80 | }); 81 | successCallback(teams); 82 | }, 83 | }); 84 | } 85 | 86 | private getOptions(callback: (options: Options) => void) { 87 | return chrome.storage.sync.get(this.options(), (options) => { 88 | this.token = options.token; 89 | this.login = options.login; 90 | this.teams = options.teams; 91 | this.lastChecked = options.lastChecked; 92 | 93 | callback(options); 94 | }); 95 | } 96 | 97 | private options(): Options { 98 | return { 99 | token: this.token, 100 | login: this.login, 101 | teams: this.teams, 102 | lastChecked: this.lastChecked, 103 | }; 104 | } 105 | 106 | private setOptions() { 107 | chrome.storage.sync.set(this.options()); 108 | } 109 | 110 | update() { 111 | this.getUser((user) => { 112 | this.login = `@${user["login"].toLowerCase()}`; 113 | 114 | this.getTeams((teams: string[]) => { 115 | this.teams = teams; 116 | this.lastChecked = Date.now(); 117 | this.setOptions(); 118 | this.highlight(); 119 | }); 120 | }); 121 | } 122 | 123 | shouldUpdate(): boolean { 124 | return Date.now() > this.lastChecked + this.checkInterval; 125 | } 126 | 127 | constructor() { 128 | this.getOptions(() => { 129 | if (this.token === "") { 130 | return console.warn( 131 | "GitHub Mention Highlighter: Please specify a personal access token via the options page." 132 | ); 133 | } 134 | 135 | if (this.shouldUpdate()) { 136 | this.update(); 137 | } else { 138 | this.highlight(); 139 | } 140 | }); 141 | } 142 | } 143 | 144 | new GitHubMentionHighlighter(); 145 | -------------------------------------------------------------------------------- /src/style.scss: -------------------------------------------------------------------------------- 1 | /* Gmail */ 2 | a.highlight { 3 | font-weight: bold; 4 | background: yellow; 5 | } 6 | 7 | .timeline-comment:not(.current-user) .user-mention.highlight, 8 | .timeline-comment:not(.current-user) .team-mention.highlight, 9 | .timeline-entry .member-mention.highlight, 10 | .timeline-entry .team-mention.highlight, 11 | .markdown-body .user-mention.highlight, 12 | .markdown-body .user-mention.highlight { 13 | background: yellow !important; 14 | } 15 | 16 | .timeline-comment.highlight:not(.current-user), 17 | .timeline-entry.highlight { 18 | border-color: yellow !important; 19 | } 20 | 21 | .timeline-comment.highlight:not(.current-user):before, 22 | .timeline-entry.highlight .comment-header:before { 23 | border-right-color: yellow !important; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | //"rootDir": "src", 4 | "outDir": "dist", 5 | "typeRoots": ["node_modules/@types"], 6 | "lib": ["dom", "es2017"], 7 | "strict": true, 8 | }, 9 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyPlugin = require("copy-webpack-plugin"); 2 | const path = require("path"); 3 | const srcDir = "./src/"; 4 | 5 | module.exports = { 6 | mode: "production", 7 | entry: { 8 | script: path.join(__dirname, srcDir + "script.ts"), 9 | options: path.join(__dirname, srcDir + "options.ts"), 10 | }, 11 | output: { 12 | path: path.join(__dirname, "./dist/"), 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: "babel-loader", 19 | exclude: /node_modules/, 20 | }, 21 | { 22 | test: /\.s[ac]ss$/i, 23 | use: ["style-loader", "css-loader", "sass-loader"], 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: [".ts", ".tsx", ".js"], 29 | }, 30 | plugins: [ 31 | new CopyPlugin({ 32 | patterns: [ 33 | { 34 | from: "src/options.html", 35 | }, 36 | ], 37 | }), 38 | ], 39 | }; 40 | --------------------------------------------------------------------------------