├── chrome ├── _locales │ └── en │ │ └── messages.json ├── images │ ├── icon.png │ ├── icon128.png │ ├── icon16.png │ └── icon48.png ├── src │ ├── bg │ │ ├── background.html │ │ └── background.js │ ├── inject │ │ ├── inject.css │ │ └── inject.js │ └── options │ │ ├── index.html │ │ ├── options.json │ │ ├── options.css │ │ └── options.js └── manifest.json ├── firefox ├── images │ ├── icon.png │ ├── icon16.png │ ├── icon48.png │ └── icon128.png ├── src │ ├── bg │ │ ├── background.html │ │ └── background.js │ └── inject │ │ ├── inject.css │ │ └── inject.js └── manifest.json ├── LICENSE └── README.md /chrome/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /chrome/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/chrome/images/icon.png -------------------------------------------------------------------------------- /chrome/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/chrome/images/icon128.png -------------------------------------------------------------------------------- /chrome/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/chrome/images/icon16.png -------------------------------------------------------------------------------- /chrome/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/chrome/images/icon48.png -------------------------------------------------------------------------------- /firefox/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/firefox/images/icon.png -------------------------------------------------------------------------------- /firefox/images/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/firefox/images/icon16.png -------------------------------------------------------------------------------- /firefox/images/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/firefox/images/icon48.png -------------------------------------------------------------------------------- /firefox/images/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muan/github-gmail/HEAD/firefox/images/icon128.png -------------------------------------------------------------------------------- /chrome/src/bg/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /firefox/src/bg/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /chrome/src/inject/inject.css: -------------------------------------------------------------------------------- 1 | .github-link, 2 | .github-mute { 3 | text-decoration: none; 4 | cursor: pointer; 5 | margin-right: 12px !important; 6 | } 7 | .github-link:hover, 8 | .github-mute:hover { 9 | border-color: rgba(0,0,0,0.2) !important; 10 | } 11 | -------------------------------------------------------------------------------- /firefox/src/inject/inject.css: -------------------------------------------------------------------------------- 1 | .github-link, 2 | .github-mute { 3 | text-decoration: none; 4 | cursor: pointer; 5 | margin-right: 12px !important; 6 | } 7 | .github-link:hover, 8 | .github-mute:hover { 9 | border-color: rgba(0,0,0,0.2) !important; 10 | } 11 | -------------------------------------------------------------------------------- /chrome/src/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GitHub for Gmail Options 5 | 6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mu-An Chiou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Helper for GitHub notifications in Gmail", 3 | "short_name": "helpergithubgmail", 4 | "version": "0.8.4", 5 | "manifest_version": 2, 6 | "description": "Add links to GitHub threads and shortcuts to your Gmail interface.", 7 | "homepage_url": "http://github.com/muan/github-gmail", 8 | "icons": { 9 | "16": "images/icon16.png", 10 | "48": "images/icon48.png", 11 | "128": "images/icon128.png" 12 | }, 13 | "default_locale": "en", 14 | "background": { 15 | "page": "src/bg/background.html", 16 | "persistent": true 17 | }, 18 | "options_page": "src/options/index.html", 19 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 20 | "content_scripts": [ 21 | { 22 | "matches": [ 23 | "https://mail.google.com/*", 24 | "https://inbox.google.com/*" 25 | ], 26 | "css": [ 27 | "src/inject/inject.css" 28 | ] 29 | }, 30 | { 31 | "matches": [ 32 | "https://mail.google.com/*", 33 | "https://inbox.google.com/*" 34 | ], 35 | "js": [ 36 | "src/inject/inject.js" 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /chrome/src/options/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "Domains" : { 3 | "description": "Specify GitHub enterprise domain, or other domains you'd like to grab links for. There's no need to add github.com, protocol is also not required unless you'd like to be explicit.", 4 | "val": "", 5 | "hint": "Comma separated if multiple." 6 | }, 7 | "Shortcut" : { 8 | "description": "Triggers view button on the mail view.", 9 | "val": "shift + 71", 10 | "hint": "Press shortcut in the input field. See Gmail shortcuts to avoid collision." 11 | }, 12 | "BackgroundShortcut" : { 13 | "description": "Triggers iew button on the mail view, opens the link in the background.", 14 | "val": "shift + 66", 15 | "hint": "Press shortcut in the input field. See Gmail shortcuts to avoid collision." 16 | }, 17 | "MuteShortcut" : { 18 | "description": "Triggers mute button on the mail view.", 19 | "val": "shift + 72", 20 | "hint": "Press shortcut in the input field. See Gmail shortcuts to avoid collision." 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /firefox/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Helper for GitHub notifications in Gmail", 3 | "short_name": "helpergithubgmail", 4 | "version": "0.9.2", 5 | "manifest_version": 2, 6 | "description": "Add links to GitHub threads and shortcuts to your Gmail interface.", 7 | "homepage_url": "http://github.com/muan/github-gmail", 8 | "icons": { 9 | "16": "images/icon16.png", 10 | "48": "images/icon48.png", 11 | "128": "images/icon128.png" 12 | }, 13 | "background": { 14 | "page": "src/bg/background.html", 15 | "persistent": true 16 | }, 17 | "commands": { 18 | "open-link": { 19 | "suggested_key": { 20 | "default": "Ctrl+G", 21 | "mac": "MacCtrl+G" 22 | }, 23 | "description": "Open link" 24 | }, 25 | "open-link-in-background": { 26 | "suggested_key": { 27 | "default": "Ctrl+B", 28 | "mac": "MacCtrl+B" 29 | }, 30 | "description": "Open link in background" 31 | }, 32 | "mute-link": { 33 | "suggested_key": { 34 | "default": "Ctrl+H", 35 | "mac": "MacCtrl+H" 36 | }, 37 | "description": "Mute thread" 38 | } 39 | }, 40 | "content_scripts": [ 41 | { 42 | "matches": [ 43 | "https://mail.google.com/*" 44 | ], 45 | "css": [ 46 | "src/inject/inject.css" 47 | ], 48 | "js": [ 49 | "src/inject/inject.js" 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /firefox/src/bg/background.js: -------------------------------------------------------------------------------- 1 | let data = {} 2 | 3 | browser.runtime.onMessage.addListener(function(req, sender, sendMessage) { 4 | if(req.url) { 5 | browser.tabs.query( 6 | {windowId: sender.tab.windowId}, 7 | function(tabs) { 8 | var position = sender.tab.index; 9 | for(var i = position; i < tabs.length; i++) { 10 | if(tabs[i].openerTabId == sender.tab.id) { 11 | position = i 12 | } 13 | } 14 | var mute = req.mute 15 | delete req.mute 16 | 17 | req.openerTabId = sender.tab.id 18 | req.index = position + 1 19 | browser.tabs.create(req, function(tab) { 20 | if (mute) listenAndCloseTab(tab, req.url, sender.tab.id) 21 | }) 22 | } 23 | ) 24 | } else { 25 | sendMessage(data) 26 | } 27 | }) 28 | 29 | 30 | browser.commands.onCommand.addListener(function (command) { 31 | browser.tabs.query({active: true, currentWindow: true}, function(tabs){ 32 | browser.tabs.sendMessage(tabs[0].id, {action: command}) 33 | }) 34 | }) 35 | 36 | function listenAndCloseTab(tab, url, originalTabId) { 37 | var listener = setInterval(function () { 38 | browser.tabs.get(tab.id, function (tab) { 39 | if (tab.status === 'complete') { 40 | browser.tabs.remove(tab.id) 41 | clearInterval(listener) 42 | // Unsubscription finished 43 | browser.tabs.sendMessage(originalTabId, {muteURL: url}) 44 | } 45 | }) 46 | }, 500) 47 | } 48 | -------------------------------------------------------------------------------- /chrome/src/bg/background.js: -------------------------------------------------------------------------------- 1 | request = new XMLHttpRequest 2 | request.open('GET', '../options/options.json', true) 3 | request.send() 4 | 5 | request.onload = function() { 6 | data = JSON.parse(this.response) 7 | for (var key in data) { 8 | data[key] = data[key].val 9 | if(localStorage[key]) { data[key] = localStorage[key] } 10 | } 11 | 12 | chrome.extension.onMessage.addListener(function(req, sender, sendMessage) { 13 | if(req.url) { 14 | chrome.tabs.query( 15 | {windowId: sender.tab.windowId}, 16 | function(tabs) { 17 | var position = sender.tab.index; 18 | for(var i = position; i < tabs.length; i++) { 19 | if(tabs[i].openerTabId == sender.tab.id) { 20 | position = i 21 | } 22 | } 23 | var mute = req.mute 24 | delete req.mute 25 | 26 | req.openerTabId = sender.tab.id 27 | req.index = position + 1 28 | chrome.tabs.create(req, function(tab) { 29 | if (mute) listenAndCloseTab(tab, req.url, sender.tab.id) 30 | }) 31 | } 32 | ) 33 | } else { 34 | sendMessage(data) 35 | } 36 | }) 37 | } 38 | 39 | function listenAndCloseTab (tab, url, originalTabId) { 40 | var listener = setInterval(function () { 41 | chrome.tabs.get(tab.id, function (tab) { 42 | if (tab.status === 'complete') { 43 | chrome.tabs.remove(tab.id) 44 | clearInterval(listener) 45 | // Unsubscription finished 46 | chrome.tabs.sendMessage(originalTabId, {muteURL: url}) 47 | } 48 | }) 49 | }, 500) 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helper for GitHub notifications in Gmail 2 | 3 | A web extension for Chrome and Firefox which adds keyboard shortcuts for opening and muting GitHub notification emails. 4 | 5 | ## Features 6 | 7 | ### Mail View Shortcut 8 | 9 | Open your GitHub notifications in mail view: 10 | 11 | - Chrome: shift + g, shift + b in the background 12 | - Firefox: ctrl + g, ctrl + b in the background 13 | 14 | ![Mail view button](https://user-images.githubusercontent.com/1153134/42123231-69153916-7c1c-11e8-8bf5-1d8fa2510b63.png) 15 | 16 | ### Mute Thread 17 | 18 | Mute thread in mail view. It will open a background window to load the mute thread URL, and close itself when done. This only works if you have an active GitHub session. 19 | 20 | - Chrome: shift + h 21 | - Firefox: ctrl + h 22 | 23 | ![Mute thread button](https://user-images.githubusercontent.com/1153134/42123234-7c6d271c-7c1c-11e8-9b13-3cd0cbea4eab.png) 24 | 25 | ### List View Shortcut 26 | 27 | ctrl + return to trigger one the action button when an email is selected using gmail key navigation (when the blue bar appears): 28 | 29 | ![action button in list view](https://user-images.githubusercontent.com/1153134/42123260-fa87c648-7c1c-11e8-8d64-9ddd8899e594.png) 30 | 31 | ## Installation 32 | 33 | - [Download in Chrome Web Store](https://chrome.google.com/webstore/detail/github-notification-helpe/gmhijkhbpihfmkmhmcfebmlkaekgmaje)
34 | - [Download in Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/github-for-gmail/) 35 | 36 | ## Shortcuts 37 | 38 | - Chrome: `Select extension` -> `Details` -> `Extension options` 39 | - [Setting extension shortcuts in Firefox](https://support.mozilla.org/en-US/kb/manage-extension-shortcuts-firefox) 40 | 41 | ## Development 42 | 43 | Load the extension manually and modify the code accordingly in these places: 44 | 45 | Chrome: `chrome/src/inject/inject.js`.
46 | Firefox: `firefox/src/inject/inject.js`.
47 | -------------------------------------------------------------------------------- /chrome/src/options/options.css: -------------------------------------------------------------------------------- 1 | body, button { 2 | background-color: #fff; 3 | font-family: Geneva, Tahoma, Verdana, sans-serif; 4 | font-size: 13px; 5 | color: #121210; 6 | line-height: 1.6; 7 | } 8 | 9 | body, * { 10 | box-sizing: border-box; 11 | } 12 | 13 | a { 14 | color: #059; 15 | } 16 | 17 | .wrapper { 18 | width: 540px; 19 | margin: auto; 20 | } 21 | 22 | .notice { 23 | background: #f1f5f9; 24 | border-left: 2px solid #c1c5cd; 25 | border-radius: 0 5px 5px 0; 26 | padding: 12px; 27 | margin: 20px 15px 30px; 28 | font-weight: bold; 29 | } 30 | 31 | .option { 32 | margin: 10px 0 0; 33 | padding: 15px; 34 | transition: all 0.6s; 35 | } 36 | 37 | .saved.option { 38 | background: #ffe; 39 | color: #333; 40 | } 41 | 42 | .description { 43 | margin-top: 5px; 44 | color: #666; 45 | } 46 | 47 | .help { 48 | color: #c58989; 49 | font-size: 12px; 50 | } 51 | 52 | input[type]:focus { 53 | background-color: #fff; 54 | border: 1px solid #49d; 55 | outline: none; 56 | } 57 | 58 | input[type] { 59 | background: #f5f5f5; 60 | border: 1px solid transparent; 61 | border-radius: 3px; 62 | padding: 12px; 63 | margin-top: 5px; 64 | font-family: menlo, monospace; 65 | } 66 | 67 | input[type] { 68 | display: block; 69 | line-height: 1; 70 | width: 100%; 71 | } 72 | 73 | label { 74 | font-size: 14px; 75 | } 76 | 77 | button { 78 | background: #4a6; 79 | border: 0; 80 | padding: 10px 20px; 81 | color: #fff; 82 | font-weight: bold; 83 | cursor: pointer; 84 | letter-spacing: 2px; 85 | border-radius: 3px; 86 | text-transform: uppercase; 87 | box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .1); 88 | transition: all .6s; 89 | margin: 20px 15px; 90 | display: block; 91 | font-size: 12px; 92 | } 93 | 94 | button:active { 95 | box-shadow: inset 0 1px 0 rgba(0, 0, 0, .1); 96 | } 97 | 98 | code { 99 | background-color: #f0f0f0; 100 | padding: 0 3px; 101 | border: 1px solid #ddd; 102 | font-family: monospace; 103 | font-size: 12px; 104 | white-space: nowrap; 105 | border-radius: 2px; 106 | } 107 | -------------------------------------------------------------------------------- /chrome/src/options/options.js: -------------------------------------------------------------------------------- 1 | const save = document.getElementById('save') 2 | const form = document.getElementById('form') 3 | 4 | fetch('options.json').then(function(res) { 5 | return res.json() 6 | }).then(function(data) { 7 | defaultOptions = data 8 | initOptions(defaultOptions) 9 | }) 10 | 11 | function initOptions (defaultOptions) { 12 | var options = defaultOptions 13 | 14 | for (var key in defaultOptions) { 15 | if( localStorage[key] ) { options[key].val = localStorage[key] } 16 | } 17 | 18 | var optionsWrapper = document.getElementById('options') 19 | 20 | for (var key in options) { 21 | optionsWrapper.innerHTML += ` 22 |
23 |

${options[key].description}
*${options[key].hint}

24 |
25 | ` 26 | } 27 | 28 | for(const el of document.querySelectorAll('[name="Shortcut"], [name="BackgroundShortcut"], [name="MuteShortcut"]')) { 29 | el.addEventListener('keypress', function(e) { 30 | if (e.keyCode == 13 && !e.shiftKey && !e.metaKey && !e.altKey && !e.ctrlKey ) return false 31 | code = '' 32 | keys = ['shift', 'alt', 'meta', 'ctrl'] 33 | keys.map(function(key) { 34 | if( eval(`e.${key}Key` ) ) { code += `${key} + ` } 35 | }) 36 | code += e.keyCode 37 | el.value = code 38 | e.preventDefault() 39 | }) 40 | } 41 | } 42 | 43 | 44 | form.addEventListener('submit', function(e) { 45 | fields = [] 46 | for(const el of document.querySelectorAll('input[name]')) { 47 | if( localStorage[el.name] !== el.value ) fields.push(el) 48 | localStorage[el.name] = el.value 49 | } 50 | 51 | // Update status to let user know options were saved. 52 | var save = document.getElementById('save') 53 | document.querySelector('.notice').hidden = false 54 | window.scrollTo(0, 10000) 55 | save.innerHTML = 'Updated!' 56 | fields.forEach(function(el) { 57 | var option = el.closest('.option') 58 | option.classList.remove('saved') 59 | option.classList.add('saved') 60 | setTimeout(function() { 61 | option.classList.remove('saved') 62 | save.innerHTML = 'Save' 63 | }, 2050) 64 | }) 65 | e.preventDefault() 66 | }) 67 | -------------------------------------------------------------------------------- /firefox/src/inject/inject.js: -------------------------------------------------------------------------------- 1 | // Retriving user options 2 | browser.runtime.sendMessage({}, function (settings) { 3 | initOnHashChangeAction(settings['Domains']) 4 | initListViewShortcut() 5 | }) 6 | 7 | browser.runtime.onMessage.addListener(function (req) { 8 | if (req['muteURL']) { 9 | const element = document.querySelector(`[href="${req['muteURL']}"]`) 10 | if (element) element.innerText = "Muted!" 11 | } else if (req['action']){ 12 | if (req['action'] === "open-link") triggerGitHubLink(false) 13 | if (req['action'] === "open-link-in-background") triggerGitHubLink(true) 14 | if (req['action'] === "mute-link") getVisible(document.querySelectorAll('.github-mute')).click() 15 | } 16 | }) 17 | 18 | 19 | function initOnHashChangeAction(domains) { 20 | var allDomains = '//github.com,' 21 | if(domains) allDomains += domains 22 | 23 | // Take string -> make array -> make queries -> avoid nil -> join queries to string 24 | var selectors = allDomains.replace(/\s/g, '').split(',').map(function (name) { 25 | if (name.length) return (".AO [href*='" + name + "']") 26 | }).filter(function (name) { return name }).join(", ") 27 | 28 | intervals = [] 29 | 30 | // Find GitHub link and append it to tool bar on hashchange 31 | window.onhashchange = function () { 32 | fetchAndAppendGitHubLink() 33 | } 34 | 35 | function fetchAndAppendGitHubLink () { 36 | // In case previous intervals got interrupted 37 | clearAllIntervals() 38 | 39 | var retryForActiveMailBody = setInterval(function () { 40 | var mail_body = Array.prototype.filter.call(document.querySelectorAll('.nH.hx'), function () { return this.clientHeight != 0 })[0] 41 | 42 | if (mail_body ) { 43 | var github_links = reject_unwanted_paths(mail_body.querySelectorAll(selectors)) 44 | 45 | // Avoid multple buttons 46 | Array.prototype.forEach.call(document.querySelectorAll('.github-link, .github-mute'), function (ele) { 47 | ele.remove() 48 | }) 49 | 50 | if (github_links.length ) { 51 | var url = github_links[github_links.length-1].href 52 | var muteLink 53 | 54 | // skip notification unsubscribe links: 55 | if (url.match('notifications/unsubscribe')) { 56 | var muteURL = url 57 | url = github_links[github_links.length-2].href 58 | muteLink = document.createElement('a') 59 | muteLink.className = 'github-mute T-I J-J5-Ji T-I-Js-Gs mA mw T-I-ax7 L3 YV' 60 | muteLink.innerText = 'Mute thread' 61 | muteLink.href = muteURL 62 | 63 | muteLink.addEventListener('click', function (evt) { 64 | evt.preventDefault() 65 | browser.runtime.sendMessage({url: muteURL, active: false, mute: true}) 66 | muteLink.innerHTML = '⋯' 67 | }) 68 | } 69 | 70 | // Go to thread instead of diffs or file views 71 | if (url.match(/^(.+\/(issue|pull)\/\d+)/)) url = url.match(/^(.+\/(issue|pull)\/\d+)/)[1] 72 | var link = document.createElement('a') 73 | link.href = url 74 | link.className = 'github-link T-I J-J5-Ji T-I-Js-Gs mA mw T-I-ax7 L3 YV' 75 | link.target = '_blank' 76 | link.innerText = 'View on GitHub' 77 | 78 | document.querySelector('.iH > div').appendChild(link) 79 | 80 | if (muteLink) { 81 | document.querySelector('.iH > div').appendChild(muteLink) 82 | } 83 | 84 | document.getElementsByClassName('github-link')[0].addEventListener("DOMNodeRemovedFromDocument", function (ev) { 85 | fetchAndAppendGitHubLink() 86 | }, false) 87 | } 88 | 89 | clearInterval(retryForActiveMailBody) 90 | } else if ( !document.querySelector('.nH.hx') ) { 91 | // Not in a mail view 92 | clearInterval(retryForActiveMailBody) 93 | } 94 | }, 100) 95 | 96 | intervals.push(retryForActiveMailBody) 97 | } 98 | } 99 | 100 | function initListViewShortcut(regexp) { 101 | document.addEventListener('keypress', function (event) { 102 | // Shortcut: bind ctrl + return 103 | var selected = getVisible(document.querySelectorAll('.zA[tabindex="0"]')) 104 | if (event.ctrlKey && event.keyCode == 13 && selected ) { 105 | generateUrlAndGoTo(selected) 106 | } 107 | }) 108 | } 109 | 110 | // Trigger the appended link in mail view 111 | function triggerGitHubLink (backgroundOrNot) { 112 | var link = getVisible(document.getElementsByClassName('github-link')) 113 | browser.runtime.sendMessage({url: link.href, active: !backgroundOrNot}) 114 | } 115 | 116 | // Go to selected email GitHub thread 117 | function generateUrlAndGoTo (selected) { 118 | var gotoaction = selected.querySelectorAll('.aKS [role="button"]')[0] 119 | 120 | if(gotoaction) { 121 | gotoaction.dispatchEvent(fakeEvent('mousedown', true)) 122 | } 123 | } 124 | 125 | // 126 | // Helpers 127 | // 128 | 129 | function processRightCombinationBasedOnShortcut (shortcut, event) { 130 | // Processing shortcut from preference 131 | combination = shortcut.replace(/\s/g, '').split('+') 132 | 133 | keys = ['shift', 'alt', 'meta', 'ctrl'] 134 | trueOrFalse = [] 135 | 136 | // If a key is in the combination, push the value to trueOrFalse array, and delete it from the combination 137 | keys.map(function (key) { 138 | index = combination.indexOf(key) 139 | if(index >= 0) { 140 | if(key == "shift") trueOrFalse.push(event.shiftKey) 141 | if(key == "alt") trueOrFalse.push(event.altKey) 142 | if(key == "meta") trueOrFalse.push(event.metaKey) 143 | if(key == "ctrl") trueOrFalse.push(event.ctrlKey) 144 | 145 | combination.splice(index, 1) 146 | } 147 | }) 148 | 149 | // If there is a keyCode left, add that to the mix. 150 | if(combination.length) trueOrFalse.push(event.keyCode.toString() == combination[0]) 151 | 152 | // Evaluate trueOrFalse by looking for the existence of False 153 | return trueOrFalse = (trueOrFalse.indexOf(false) < 0) 154 | } 155 | 156 | // .click() doesn't usually work as expected 157 | function fakeEvent (event, bubbles) { 158 | var click = new MouseEvent(event, {bubbles: bubbles}) 159 | return click 160 | } 161 | 162 | function linkWithUrl (url) { 163 | var l = document.createElement('a') 164 | l.href = url 165 | l.target = "_blank" 166 | return l 167 | } 168 | 169 | function getVisible (nodeList) { 170 | if(nodeList.length) { 171 | var node 172 | for(var i=0; i < nodeList.length; i++) { 173 | if(typeof node === 'undefined' && (nodeList[i].offsetHeight > 0 || nodeList[i].clientWidth > 0 || nodeList[i].clientHeight > 0)) { 174 | node = nodeList[i] 175 | break 176 | } 177 | } 178 | return node 179 | } 180 | } 181 | 182 | function notAnInput (element) { 183 | return !element.className.match(/editable/) && element.tagName != "TEXTAREA" && element.tagName != "INPUT" 184 | } 185 | 186 | function clearAllIntervals () { 187 | intervals.map(function (num) { 188 | clearInterval(num) 189 | delete intervals[intervals.indexOf(num)] 190 | }) 191 | } 192 | 193 | // Reject unsubscribe, subscription and verification management paths 194 | // Make sure the keywords((un)subscribe) can still be repository names 195 | function reject_unwanted_paths (links) { 196 | var paths = ['\/\/[^\/]*\/mailers\/unsubscribe\?', 197 | '\/\/[^\/]*\/.*\/.*\/unsubscribe_via_email', 198 | '\/\/[^\/]*\/.*\/.*\/subscription$', 199 | '\/\/[^\/]*\/.*\/.*\/emails\/.*\/confirm_verification\/.*'] 200 | var regexp = new RegExp(paths.join('|')) 201 | return Array.prototype.filter.call(links, function (link) { 202 | if(!link.href.match(regexp)) return this 203 | }) 204 | } 205 | -------------------------------------------------------------------------------- /chrome/src/inject/inject.js: -------------------------------------------------------------------------------- 1 | // Retriving user options 2 | chrome.extension.sendMessage({}, function (settings) { 3 | initOnHashChangeAction(settings['Domains']) 4 | initShortcuts(settings['Shortcut'], settings['BackgroundShortcut'], settings['MuteShortcut']) 5 | 6 | initListViewShortcut() 7 | initForInbox() 8 | }) 9 | 10 | chrome.runtime.onMessage.addListener(function (req) { 11 | var element = req['muteURL'] ? document.querySelector('[href="' + req['muteURL'] + '"]') : null 12 | 13 | if (element) { 14 | element.innerText = "Muted!" 15 | } 16 | }) 17 | 18 | function initForInbox() { 19 | window.idled = true 20 | } 21 | 22 | function initOnHashChangeAction(domains) { 23 | var allDomains = '//github.com,' 24 | if(domains) allDomains += domains 25 | 26 | // Take string -> make array -> make queries -> avoid nil -> join queries to string 27 | var selectors = allDomains.replace(/\s/g, '').split(',').map(function (name) { 28 | if (name.length) return (".AO [href*='" + name + "']") 29 | }).filter(function (name) { return name }).join(", ") 30 | 31 | intervals = [] 32 | 33 | // Find GitHub link and append it to tool bar on hashchange 34 | window.onhashchange = function () { 35 | fetchAndAppendGitHubLink() 36 | } 37 | 38 | function fetchAndAppendGitHubLink () { 39 | // In case previous intervals got interrupted 40 | clearAllIntervals() 41 | 42 | var retryForActiveMailBody = setInterval(function () { 43 | var mail_body = Array.prototype.filter.call(document.querySelectorAll('.nH.hx'), function () { return this.clientHeight != 0 })[0] 44 | 45 | if (mail_body ) { 46 | var github_links = reject_unwanted_paths(mail_body.querySelectorAll(selectors)) 47 | 48 | // Avoid multple buttons 49 | Array.prototype.forEach.call(document.querySelectorAll('.github-link, .github-mute'), function (ele) { 50 | ele.remove() 51 | }) 52 | 53 | if (github_links.length ) { 54 | var url = github_links[github_links.length-1].href 55 | var muteLink 56 | 57 | // skip notification unsubscribe links: 58 | if (url.match('notifications/unsubscribe')) { 59 | var muteURL = url 60 | url = github_links[github_links.length-2].href 61 | muteLink = document.createElement('a') 62 | muteLink.className = 'github-mute T-I J-J5-Ji T-I-Js-Gs mA mw T-I-ax7 L3 YV' 63 | muteLink.innerText = 'Mute thread' 64 | muteLink.href = muteURL 65 | 66 | muteLink.addEventListener('click', function (evt) { 67 | evt.preventDefault() 68 | chrome.extension.sendMessage({url: muteURL, active: false, mute: true}) 69 | muteLink.innerHTML = '⋯' 70 | }) 71 | } 72 | 73 | // Go to thread instead of diffs or file views 74 | if (url.match(/^(.+\/(issue|pull)\/\d+)/)) url = url.match(/^(.+\/(issue|pull)\/\d+)/)[1] 75 | var link = document.createElement('a') 76 | link.href = url 77 | link.className = 'github-link T-I J-J5-Ji T-I-Js-Gs mA mw T-I-ax7 L3 YV' 78 | link.target = '_blank' 79 | link.innerText = 'View on GitHub' 80 | 81 | document.querySelector('.iH > div').appendChild(link) 82 | 83 | if (muteLink) { 84 | document.querySelector('.iH > div').appendChild(muteLink) 85 | } 86 | 87 | window.idled = true 88 | 89 | document.getElementsByClassName('github-link')[0].addEventListener("DOMNodeRemovedFromDocument", function (ev) { 90 | fetchAndAppendGitHubLink() 91 | }, false) 92 | } 93 | 94 | clearInterval(retryForActiveMailBody) 95 | } else if ( !document.querySelector('.nH.hx') ) { 96 | // Not in a mail view 97 | clearInterval(retryForActiveMailBody) 98 | } 99 | }, 100) 100 | 101 | intervals.push(retryForActiveMailBody) 102 | } 103 | } 104 | 105 | function initShortcuts(shortcut, backgroundShortcut, muteShortcut) { 106 | document.addEventListener('keydown', function (event) { 107 | // Shortcut: bind user's combination, if a button exist and event not in a textarea 108 | if (document.querySelector('.gE')) { 109 | document.querySelector('.gE').classList.remove('github-link') 110 | } 111 | 112 | Array.prototype.forEach.call(document.querySelectorAll('.scroll-list-item-open .gE, .scroll-list-item-highlighted .gE'), function (ele) { 113 | ele.classList.add('github-link') 114 | }) 115 | 116 | if (processRightCombinationBasedOnShortcut(shortcut, event) && window.idled && getVisible(document.getElementsByClassName('github-link')) && notAnInput(event.target)) { 117 | triggerGitHubLink(false) 118 | } 119 | 120 | // Bacground Shortcut: bind user's combination, if a button exist and event not in a textarea 121 | if (processRightCombinationBasedOnShortcut(backgroundShortcut, event) && window.idled && getVisible(document.getElementsByClassName('github-link')) && notAnInput(event.target)) { 122 | triggerGitHubLink(true) 123 | } 124 | 125 | // Mute Shortcut: bind user's combination, if a button exist and event not in a textarea 126 | if (processRightCombinationBasedOnShortcut(muteShortcut, event) && window.idled && getVisible(document.getElementsByClassName('github-mute')) && notAnInput(event.target)) { 127 | getVisible(document.getElementsByClassName('github-mute')).click() 128 | } 129 | }) 130 | } 131 | 132 | function initListViewShortcut(regexp) { 133 | document.addEventListener('keypress', function (event) { 134 | // Shortcut: bind ctrl + return 135 | var selected = getVisible(document.querySelectorAll('.zA[tabindex="0"]')) 136 | if (event.ctrlKey && event.keyCode == 13 && selected ) { 137 | generateUrlAndGoTo(selected) 138 | } 139 | }) 140 | } 141 | 142 | // Trigger the appended link in mail view 143 | function triggerGitHubLink (backgroundOrNot) { 144 | // avoid link being appended multiple times 145 | window.idled = false 146 | var link = getVisible(document.getElementsByClassName('github-link')) 147 | chrome.extension.sendMessage({url: link.href, active: !backgroundOrNot}) 148 | 149 | setTimeout( function (){ window.idled = true }, 100) 150 | } 151 | 152 | // Go to selected email GitHub thread 153 | function generateUrlAndGoTo (selected) { 154 | var gotoaction = selected.querySelectorAll('.aKS [role="button"]')[0] 155 | 156 | if(gotoaction) { 157 | gotoaction.dispatchEvent(fakeEvent('mousedown', true)) 158 | } 159 | } 160 | 161 | // 162 | // Helpers 163 | // 164 | 165 | function processRightCombinationBasedOnShortcut (shortcut, event) { 166 | // Processing shortcut from preference 167 | combination = shortcut.replace(/\s/g, '').split('+') 168 | 169 | keys = ['shift', 'alt', 'meta', 'ctrl'] 170 | trueOrFalse = [] 171 | 172 | // If a key is in the combination, push the value to trueOrFalse array, and delete it from the combination 173 | keys.map(function (key) { 174 | index = combination.indexOf(key) 175 | if(index >= 0) { 176 | if(key == "shift") trueOrFalse.push(event.shiftKey) 177 | if(key == "alt") trueOrFalse.push(event.altKey) 178 | if(key == "meta") trueOrFalse.push(event.metaKey) 179 | if(key == "ctrl") trueOrFalse.push(event.ctrlKey) 180 | 181 | combination.splice(index, 1) 182 | } 183 | }) 184 | 185 | // If there is a keyCode left, add that to the mix. 186 | if(combination.length) trueOrFalse.push(event.keyCode.toString() == combination[0]) 187 | 188 | // Evaluate trueOrFalse by looking for the existence of False 189 | return trueOrFalse = (trueOrFalse.indexOf(false) < 0) 190 | } 191 | 192 | // .click() doesn't usually work as expected 193 | function fakeEvent (event, bubbles) { 194 | var click = new MouseEvent(event, {bubbles: bubbles}) 195 | return click 196 | } 197 | 198 | function linkWithUrl (url) { 199 | var l = document.createElement('a') 200 | l.href = url 201 | l.target = "_blank" 202 | return l 203 | } 204 | 205 | function getVisible (nodeList) { 206 | if(nodeList.length) { 207 | var node 208 | for(var i=0; i < nodeList.length; i++) { 209 | if(typeof node === 'undefined' && (nodeList[i].offsetHeight > 0 || nodeList[i].clientWidth > 0 || nodeList[i].clientHeight > 0)) { 210 | node = nodeList[i] 211 | break 212 | } 213 | } 214 | return node 215 | } 216 | } 217 | 218 | function notAnInput (element) { 219 | return !element.className.match(/editable/) && element.tagName != "TEXTAREA" && element.tagName != "INPUT" 220 | } 221 | 222 | function clearAllIntervals () { 223 | intervals.map(function (num) { 224 | clearInterval(num) 225 | delete intervals[intervals.indexOf(num)] 226 | }) 227 | } 228 | 229 | // Reject unsubscribe, subscription and verification management paths 230 | // Make sure the keywords((un)subscribe) can still be repository names 231 | function reject_unwanted_paths (links) { 232 | var paths = ['\/\/[^\/]*\/mailers\/unsubscribe\?', 233 | '\/\/[^\/]*\/.*\/.*\/unsubscribe_via_email', 234 | '\/\/[^\/]*\/.*\/.*\/subscription$', 235 | '\/\/[^\/]*\/.*\/.*\/emails\/.*\/confirm_verification\/.*'] 236 | var regexp = new RegExp(paths.join('|')) 237 | return Array.prototype.filter.call(links, function (link) { 238 | if(!link.href.match(regexp)) return this 239 | }) 240 | } 241 | --------------------------------------------------------------------------------