├── .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 | 
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 |
22 | Personal Access Token:
23 |
24 |
25 |
26 | Save
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 |
22 | Personal Access Token:
23 |
24 |
25 |
26 | Save
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 |
--------------------------------------------------------------------------------