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