├── .babelrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── jest.config.js ├── jest.setup.js ├── manifest.dev.json ├── manifest.dist.json ├── marketing ├── 1280x800 - bookbub.png ├── 1280x800 - databaseweekly.png └── 1280x800 - googleexpress.png ├── package-lock.json ├── package.json ├── readme.md ├── src ├── AppContainer.js ├── assets │ ├── icon16.png │ └── icon48.png ├── components │ ├── Link.js │ ├── LinkList.js │ └── __tests__ │ │ └── Link.js ├── config.js ├── default.css ├── gmailNodes.js ├── index.html └── index.js ├── webpack.common.js └── webpack.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dev 3 | dev-ff 4 | dev-chrome 5 | dist 6 | dist-ff 7 | dist-chrome 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | manifest.dev.json 4 | manifest.dist.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "jsxBracketSameLine": false, 5 | "printWidth": 80, 6 | "proseWrap": "always", 7 | "semi": false, 8 | "singleQuote": true, 9 | "tabWidth": 2, 10 | "trailingComma": "none", 11 | "useTabs": false 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kevin Wu 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 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | setupTestFrameworkScriptFile: require.resolve('./jest.setup.js') 3 | } 4 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import 'jest-dom/extend-expect' 2 | -------------------------------------------------------------------------------- /manifest.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Gmail Quick Links", 4 | "version": "0.3.21", 5 | "description": "a replacement for Gmail Quick Links", 6 | "short_name": "Gmail Links", 7 | "icons": { 8 | "16": "./assets/icon16.png", 9 | "48": "./assets/icon48.png", 10 | "128": "./assets/icon48.png" 11 | }, 12 | 13 | "author": "Kevin Wu ", 14 | "homepage_url": "https://github.com/kevinwucodes/gmail-quick-links", 15 | 16 | "content_scripts": [ 17 | { 18 | "matches": ["https://mail.google.com/*"], 19 | "css": ["default.css"], 20 | "js": ["react.js", "react-dom.js", "dev.gmailquicklinks.bundle.js"], 21 | "run_at": "document_idle" 22 | } 23 | ], 24 | 25 | "permissions": ["storage"], 26 | 27 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhWbahShe4OKvw3tHs8V4zB7BF6r47NBMlU164jW2rB7/R57/FdFnW6GHj0cmL0mSR68vpNDBMpTZG/lVpuzm+Vgye/R6zpynmgP2trzsqjoOwWGoFekxMM8hSPrtIltUrU+9pHB5ssZZKYSC7uPajO/9zn/Zb035atAAKddT2r/VY3UuILfKXRQyZrsutjsZ1l8NzB2r0paQS5mwVpMTX8i5H6Oky6AeDyEkkpWvs0VAmo5nOsYyGVF2lIGxeFp3aD7dsNX7R+lfBhicrimHSgv6e/UT0fwHDRy/G18VtrrgZnIrFsRt1HAGUFNaGnymnxvCSFkGAvAXCRNr5rI8iQIDAQAB", 28 | 29 | "applications": { 30 | "gecko": { 31 | "id": "GmailQuickLinks-bf01c022-b064-4fc1-b198-f43d8cbe6ed4@example.com" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /manifest.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Gmail Quick Links", 4 | "version": "0.3.21", 5 | "description": "a replacement for Gmail Quick Links", 6 | "short_name": "Gmail Links", 7 | "icons": { 8 | "16": "./assets/icon16.png", 9 | "48": "./assets/icon48.png", 10 | "128": "./assets/icon48.png" 11 | }, 12 | 13 | "author": "Kevin Wu ", 14 | "homepage_url": "https://github.com/kevinwucodes/gmail-quick-links", 15 | 16 | "content_scripts": [ 17 | { 18 | "matches": ["https://mail.google.com/*"], 19 | "css": ["default.css"], 20 | "js": ["react.js", "react-dom.js", "dist.gmailquicklinks.bundle.js"], 21 | "run_at": "document_idle" 22 | } 23 | ], 24 | 25 | "permissions": ["storage"] 26 | } 27 | -------------------------------------------------------------------------------- /marketing/1280x800 - bookbub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinwucodes/gmail-quick-links/0c54f16083a331639c56a1021813094125bcfd43/marketing/1280x800 - bookbub.png -------------------------------------------------------------------------------- /marketing/1280x800 - databaseweekly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinwucodes/gmail-quick-links/0c54f16083a331639c56a1021813094125bcfd43/marketing/1280x800 - databaseweekly.png -------------------------------------------------------------------------------- /marketing/1280x800 - googleexpress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinwucodes/gmail-quick-links/0c54f16083a331639c56a1021813094125bcfd43/marketing/1280x800 - googleexpress.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gmail-quick-links", 3 | "version": "0.3.21", 4 | "description": "a replacement for Gmail Quick Links", 5 | "keywords": [ 6 | "chrome extension", 7 | "gmail", 8 | "quick links" 9 | ], 10 | "main": "index.js", 11 | "scripts": { 12 | "test": "jest", 13 | "test:watch": "jest --watch", 14 | "dev": "npm run webpack -- --env.env development", 15 | "build": "npm run webpack -- --env.env production", 16 | "webpack": "webpack --config webpack.js --watch", 17 | "format": "npm run prettier -- --write", 18 | "prettier": "prettier \"**/*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx)\"" 19 | }, 20 | "author": "Kevin Wu ", 21 | "homepage": "https://github.com/kevinwucodes/gmail-quick-links", 22 | "bugs": { 23 | "url": "https://github.com/kevinwucodes/gmail-quick-links/issues" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/kevinwucodes/gmail-quick-links" 28 | }, 29 | "license": "MIT", 30 | "devDependencies": { 31 | "babel-core": "^6.26.0", 32 | "babel-loader": "^7.1.2", 33 | "babel-minify-webpack-plugin": "^0.2.0", 34 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 35 | "babel-preset-env": "^1.6.0", 36 | "babel-preset-react": "^6.24.1", 37 | "clean-webpack-plugin": "^0.1.16", 38 | "copy-webpack-plugin": "^4.0.1", 39 | "html-webpack-plugin": "^2.30.1", 40 | "jest": "^23.6.0", 41 | "jest-dom": "^2.1.1", 42 | "prettier": "^1.14.3", 43 | "react-testing-library": "^5.2.3", 44 | "webpack": "^3.5.5", 45 | "webpack-merge": "^4.1.0" 46 | }, 47 | "dependencies": { 48 | "react": "^15.6.1", 49 | "react-dom": "^15.6.1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Gmail Quick Links 2 | 3 | A Chrome Extension / Firefox Add-on to replace the existing Gmail Quick Links 4 | currently found in Gmail Experimental Labs that Google is 5 | [retiring](https://gsuiteupdates.googleblog.com/2017/03/updates-in-g-suite-to-streamline-hangouts-and-gmail.html). 6 | 7 | Supports multiple logged-in gmail accounts, with ability to create quick links 8 | that work across all logged-in accounts or a quick link that is specific to one 9 | account. 10 | 11 | Icons come from [Icons8](https://icons8.com/icon/12610/add-link-filled). Please 12 | use them, they're great. 13 | 14 | ### Background 15 | 16 | As of 3/24/2017, Google retired the Gmail Experimental Labs, which included the 17 | original Dan P. Gmail Labs Quick Links extension. This extension was built to 18 | bring back that original Quick Links experience. 19 | 20 | ### Help, the extension worked before and it no longer works! 21 | 22 | Gmail aggresively changes their UI styles without warning. The extension looks 23 | at certain class names to determine where to insert the Quick Links. If none is 24 | found, it cannot insert. If you are noticing that Quick Links no longer appear 25 | in your navigation, please file an issue. 26 | 27 | ### App IDs 28 | 29 | **Chrome Extension ID:** (ecbkcjeoffcjnppapdlncohmehhnfibd) 30 | https://chrome.google.com/webstore/detail/gmail-quick-links/ecbkcjeoffcjnppapdlncohmehhnfibd 31 | 32 | **Firefox Add-on ID:** (bf01c022-b064-4fc1-b198-f43d8cbe6ed4) 33 | https://addons.mozilla.org/en-US/firefox/addon/gmail-quick-links 34 | -------------------------------------------------------------------------------- /src/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LinkList from './components/LinkList' 3 | 4 | import { 5 | storage, 6 | getQuickLinks, 7 | addQuickLink, 8 | removeGlobalLink, 9 | removeAccountLink, 10 | renameLink, 11 | toggleLink, 12 | GMAIL_QUICK_LINKS_NAME 13 | } from './config' 14 | 15 | import { 16 | gmailSideBar, 17 | gmailHoverSideBar, 18 | quickLinksContainer 19 | } from './gmailNodes' 20 | 21 | var observer 22 | 23 | class AppContainer extends React.Component { 24 | constructor(props) { 25 | super(props) 26 | this.buildList = this.buildList.bind(this) 27 | this.onAdd = this.onAdd.bind(this) 28 | this.state = { 29 | name: '', 30 | version: '', 31 | 32 | //global 33 | linkList: {}, 34 | 35 | //accountList contains all accounts, with nested lists in each account 36 | accountList: {} 37 | } 38 | } 39 | 40 | buildList(gql) { 41 | const { 42 | name = GMAIL_QUICK_LINKS_NAME.name, 43 | version = GMAIL_QUICK_LINKS_NAME.version, 44 | linkList = {}, 45 | accountList = {} 46 | } = gql 47 | 48 | this.setState((prevState, props) => { 49 | return { 50 | name, 51 | version, 52 | linkList, 53 | accountList 54 | } 55 | }) 56 | } 57 | 58 | onAdd(event) { 59 | event.preventDefault() 60 | const {accountName} = this.props 61 | //TODO: do we use location.hash? or something else? 62 | const urlHash = location.hash 63 | const name = prompt( 64 | `Enter title for current view [${urlHash.substring(1)}]`, 65 | urlHash.substring(1) 66 | ) 67 | 68 | if (name) { 69 | addQuickLink(accountName, name, urlHash) 70 | } 71 | } 72 | 73 | componentWillMount() { 74 | // when the storage changes, we should update the list 75 | storage.onChanged.addListener((changes, areaName) => { 76 | getQuickLinks(this.buildList) 77 | }) 78 | } 79 | 80 | componentDidMount() { 81 | getQuickLinks(this.buildList) 82 | 83 | //are we in collapsed gmail sidebar? 84 | //also check that we are compatible with simplify (https://github.com/leggett/simplify) 85 | //because simplify changes the width to zero 86 | if (gmailSideBar().offsetWidth == 72 || gmailSideBar().offsetWidth == 0) { 87 | //immediately hide it 88 | quickLinksContainer().style.display = 'none' 89 | } 90 | 91 | observer = new MutationObserver((mutationsList, observer) => { 92 | for (var mutation of mutationsList) { 93 | if (mutation.type == 'attributes') { 94 | if ( 95 | //are we in a small sidebar? 96 | //also check that we are compatible with simplify (https://github.com/leggett/simplify) 97 | //because simplify changes the width to zero 98 | (mutation.target.offsetWidth == 72 || 99 | mutation.target.offsetWidth == 0) && 100 | gmailHoverSideBar() === null 101 | ) { 102 | quickLinksContainer().style.display = 'none' 103 | } else { 104 | quickLinksContainer().style.display = 'block' 105 | } 106 | } 107 | } 108 | }) 109 | 110 | observer.observe(gmailSideBar(), { 111 | attributes: true, 112 | attributeFilter: ['class'], 113 | attributeOldValue: true 114 | }) 115 | } 116 | 117 | componentWillUnmount() { 118 | observer.disconnect() 119 | } 120 | 121 | render() { 122 | const {linkList, accountList} = this.state 123 | const {accountName, location} = this.props 124 | 125 | const moreProps = { 126 | className: location === 'widget' ? 'py' : '' 127 | } 128 | 129 | return ( 130 | 155 | ) 156 | } 157 | } 158 | 159 | export default AppContainer 160 | -------------------------------------------------------------------------------- /src/assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinwucodes/gmail-quick-links/0c54f16083a331639c56a1021813094125bcfd43/src/assets/icon16.png -------------------------------------------------------------------------------- /src/assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevinwucodes/gmail-quick-links/0c54f16083a331639c56a1021813094125bcfd43/src/assets/icon48.png -------------------------------------------------------------------------------- /src/components/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const renderGlobeCircle = type => onClickGlobeCircle => { 4 | return ( 5 | onClickGlobeCircle()} 9 | /> 10 | ) 11 | } 12 | 13 | const Link = ({ 14 | type, 15 | name, 16 | urlHash, 17 | onClickLink, 18 | onDelete, 19 | onRename, 20 | onClickGlobeCircle 21 | }) => { 22 | return ( 23 |
24 | 30 | {name} 31 | 32 | onDelete()} 36 | /> 37 | onRename()} 41 | /> 42 | {renderGlobeCircle(type)(onClickGlobeCircle)} 43 |
44 |
45 | ) 46 | } 47 | 48 | export default Link 49 | -------------------------------------------------------------------------------- /src/components/LinkList.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Link from './Link' 3 | 4 | const style = { 5 | small: { 6 | fontSize: '80%' 7 | }, 8 | quick: { 9 | cursor: 'auto', 10 | position: 'relative', 11 | overflow: 'hidden', 12 | verticalAlign: 'middle', 13 | outline: 'none', 14 | fontSize: '100%' 15 | }, 16 | list: { 17 | paddingLeft: 30 18 | }, 19 | titleContainer: { 20 | display: 'flex', 21 | alignItems: 'baseline', 22 | justifyContent: 'space-between' 23 | } 24 | } 25 | 26 | const renderList = linkList => accountList => onDelete => onRename => onClickGlobeCircle => { 27 | const _onDelete = (type, key) => () => onDelete(type, key) 28 | const _onRename = (type, key) => () => onRename(type, key) 29 | const _onClickGlobeCircle = (type, key) => () => onClickGlobeCircle(type, key) 30 | 31 | if ( 32 | Object.keys(linkList).length === 0 && 33 | Object.keys(accountList).length === 0 34 | ) { 35 | return ( 36 |
40 | nothing to list: To add one, enter a gmail search and click "Add" to 41 | create a quick list 42 |
43 | ) 44 | } else { 45 | const linksArray = Object.keys(linkList).map(key => { 46 | const {urlHash} = linkList[key] 47 | return ( 48 | 57 | ) 58 | }) 59 | 60 | const accountArray = Object.keys(accountList).map(key => { 61 | const {urlHash} = accountList[key] 62 | return ( 63 | 72 | ) 73 | }) 74 | 75 | return linksArray.concat(accountArray) 76 | } 77 | } 78 | 79 | const displayHelp = () => { 80 | const message = ` 81 | To use Quick Links: 82 | 83 | 1) perform a gmail search 84 | 2) click on "Add" to give a name for that search 85 | 86 | By default, all quick links are specific to the account where they were created. If you have multiple gmail accounts logged in simultaneously, creating a quick link would only be visible for that account. 87 | 88 | You may choose to have quick links available for multiple gmail accounts by clicking on the yellow sphere which then toggles to a globe icon. A quick link with a globe icon will now show up in every gmail account you are logged in. Clicking on the globe again will change that quick link back to a specific account quick link. 89 | 90 | If you do not have multiple gmail accounts, toggling between the yellow sphere and the globe does nothing. 91 | ` 92 | alert(message) 93 | } 94 | 95 | const LinkList = ({ 96 | linkList = {}, 97 | accountList = {}, 98 | onAdd, 99 | onDelete, 100 | onRename, 101 | onClickGlobeCircle 102 | }) => { 103 | return ( 104 |
105 |
106 |
107 | 112 |

Quick Links

113 |
114 |
onAdd(event)} 119 | > 120 | Add Quick Link 121 |
122 |
123 | 124 |
125 |
126 | {renderList(linkList)(accountList)(onDelete)(onRename)(onClickGlobeCircle)} 127 |
128 |
129 |
130 | ) 131 | } 132 | 133 | export default LinkList 134 | -------------------------------------------------------------------------------- /src/components/__tests__/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {render, fireEvent, cleanup} from 'react-testing-library' 3 | import Link from '../Link' 4 | import {debug} from 'util' 5 | 6 | //we mock getComputedStyle because getComputedStyle is returned from window once we 7 | //are inside Gmail 8 | Object.defineProperty(window, 'getComputedStyle', { 9 | value: () => ({ 10 | getPropertyValue: prop => { 11 | return '' 12 | } 13 | }) 14 | }) 15 | 16 | afterEach(() => { 17 | cleanup() 18 | }) 19 | 20 | test('renders simple Link', () => { 21 | const component = 22 | const {getByText} = render(component) 23 | 24 | const link = getByText(/hello/) 25 | 26 | expect(link).toHaveClass('n0') 27 | expect(link).toHaveAttribute('href', 'some-url-here') 28 | expect(link).toHaveAttribute('title', 'some-url-here') 29 | }) 30 | 31 | test('renders global Link type with the globe glyph', () => { 32 | const component = 33 | const {getByTitle} = render(component) 34 | 35 | const theSpan = getByTitle('toggle global/single') 36 | 37 | expect(theSpan).toHaveClass('glyph global') 38 | }) 39 | 40 | test('renders account Link type with the circle glyph', () => { 41 | const component = 42 | const {getByTitle} = render(component) 43 | 44 | const theSpan = getByTitle('toggle global/single') 45 | 46 | expect(theSpan).toHaveClass('glyph circle') 47 | }) 48 | 49 | // test('click on globe', () => { 50 | // const component = 51 | // const {getByTitle, debug} = render(component) 52 | 53 | // const theSpan = getByTitle('toggle global/single') 54 | 55 | // debug(theSpan) 56 | 57 | // expect(theSpan).toHaveClass('glyph global') 58 | 59 | // fireEvent.click(theSpan) 60 | 61 | // debug(theSpan) 62 | 63 | // }) 64 | 65 | //TODO: write test for clicking on globe/circle 66 | 67 | //TODO: write test for window.getComputedStyle 68 | // console.log(window.getComputedStyle()) 69 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export const GMAIL_QUICK_LINKS_NAME = { 2 | name: 'Gmail Quick Links', 3 | divId: 'gmailQuickLinks', 4 | version: '0.3.21' 5 | } 6 | 7 | // this should come out of chrome.storage apis 8 | //TODO: how do we sync this in firefox? 9 | export const storage = chrome.storage 10 | 11 | /* 12 | the storage looks something like this: 13 | 14 | { 15 | // this is global for all accounts, universal linkList 16 | linkList: { 17 | 'unread': { 18 | urlHash: "#search/label%3Aunread+label%3Ainbox" 19 | } 20 | }, 21 | 22 | // these are specific to each account, with each account having own linkList 23 | accountList: { 24 | 'home_account@example.com': { 25 | family: { 26 | urlHash: "from:sister OR from:mom" 27 | }, 28 | friends: { 29 | urlHash: "from:bestfriend1 OR from:bestfriend2" 30 | } 31 | }, 32 | 'work_account@example.com': { 33 | tps: { 34 | urlHash: "#search/tpsReports" 35 | }, 36 | boss: { 37 | urlHash: "from:boss@company.com" 38 | } 39 | } 40 | } 41 | } 42 | */ 43 | 44 | export const getQuickLinks = callback => { 45 | storage.sync.get(null, callback) 46 | } 47 | 48 | //TODO: we need to check for chrome.runtime errors, promisify everything? 49 | export const addQuickLink = (accountName, name, urlHash) => { 50 | getQuickLinks(dataset => { 51 | // does the accountName already exist? 52 | if (dataset.accountList && dataset.accountList[accountName]) { 53 | // yes, then just add another property to the existing accountName 54 | storage.sync.set({ 55 | accountList: { 56 | ...dataset.accountList, 57 | [accountName]: { 58 | ...dataset.accountList[accountName], 59 | [name]: { 60 | urlHash 61 | } 62 | } 63 | } 64 | }) 65 | } else { 66 | // no, create the accountName and add the first property 67 | storage.sync.set({ 68 | accountList: { 69 | ...dataset.accountList, 70 | [accountName]: { 71 | [name]: { 72 | urlHash 73 | } 74 | } 75 | } 76 | }) 77 | } 78 | }) 79 | } 80 | 81 | export const toggleLink = (type, name, accountName) => { 82 | if (type === 'global') { 83 | // then make it NOT global 84 | getQuickLinks(dataset => { 85 | const {urlHash} = dataset.linkList[name] 86 | addQuickLink(accountName, name, urlHash) 87 | removeGlobalLink(name) 88 | }) 89 | } else { 90 | // else make it global 91 | getQuickLinks(dataset => { 92 | const {urlHash} = dataset.accountList[accountName][name] 93 | 94 | // remove the local account 95 | removeAccountLink(accountName, name) 96 | 97 | // now add it globally 98 | storage.sync.set({ 99 | linkList: { 100 | ...dataset.linkList, 101 | [name]: { 102 | urlHash 103 | } 104 | } 105 | }) 106 | }) 107 | } 108 | } 109 | 110 | export const removeGlobalLink = name => { 111 | getQuickLinks(item => { 112 | // removes the 'name' ES7 style! 113 | const {[name]: deleted, ...links} = item.linkList 114 | storage.sync.set({ 115 | linkList: links 116 | }) 117 | }) 118 | } 119 | 120 | export const removeAccountLink = (accountName, name) => { 121 | getQuickLinks(dataset => { 122 | // removes the 'name' ES7 style! 123 | const {[name]: deleted, ...links} = dataset.accountList[accountName] 124 | 125 | storage.sync.set({ 126 | accountList: { 127 | ...dataset.accountList, 128 | [accountName]: links 129 | } 130 | }) 131 | }) 132 | } 133 | 134 | export const renameLink = (type, name, newName, accountName) => { 135 | getQuickLinks(dataset => { 136 | if (type === 'global') { 137 | const globalList = dataset.linkList 138 | globalList[newName] = globalList[name] 139 | delete globalList[name] 140 | storage.sync.set({ 141 | linkList: globalList 142 | }) 143 | } else { 144 | const accountNameList = dataset.accountList[accountName] 145 | accountNameList[newName] = accountNameList[name] 146 | delete accountNameList[name] 147 | storage.sync.set({ 148 | accountList: { 149 | ...dataset.accountList, 150 | [accountName]: { 151 | ...accountNameList 152 | } 153 | } 154 | }) 155 | } 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /src/default.css: -------------------------------------------------------------------------------- 1 | span.glyph { 2 | display: block; 3 | width: 16px; 4 | height: 16px; 5 | } 6 | 7 | span.global { 8 | /*https://icons8.com/icon/12193/globe*/ 9 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACUklEQVQ4T6VTUUgTcRz+/rESu920QI314NmSHrSmoJHBIAXXQy+tyF5ybm+6E1JyPbrt+QYatO2trbEnCfTFhwzcQMhqPWwoUal0e3CUQunublKNLv437mgZEfR7uvvf9333+33f70/wn0V+53MzaiPq5ABUXCEEXfS7qiIHggy+mkPiJNn7lVMjwEVkD/BjhhDSSEEdJ/II9fqhfDdj6kUUO+WWPeDIpMibE7qIIUDJbZaN+C1bCsxRGZntQWSKV/HE6US6OIjIuh+9zc+xe9CMD6V2ry6iCdC2WxrfF4RL45ZyhcH0awGMSYZSMaPfuoS5LTea6j8i5nBr3fhW4vuycpqj41QFItLs+Hnhbr/1GV7uXMbuwSlwlk0Es2FjXI7dRKjnvtYd7ejhmv+ByLMTVYGolEsOuOyMSTEIonQG01kB5QqL4yYJMceIRtbr5tOlnMiz3ZrAxfiGStujpVQYBLICROmsAR6yJTFkS9UENraSxCtvO9EE2qKSGu4bRbro1ECLhRs14JhjGE31n2rOplajSA93VwXoCIxJst+2pbD+xa6Z9+bzBY1AZw/3+WrItEv38nxe9LFduolBQhCgqFDPPXScXNPMTLwb1QylCdA09DEWCy48ejsWEnk2aMRI6iQRIA3eczFca503/BhZrj7TejzgQpku1WqkJCvWViPGapSyhxA1rudNz+a27mg7oBffKYD+/dAi6QAuWrpOgATfGW4QJdshMwF1XwU8os+yoHP+fJmOyRMgoGJ2ClSBPFQs4Jt59q+Xqcbqf3z5Cdt38hGZ9VMEAAAAAElFTkSuQmCC'); 10 | background-repeat: no-repeat; 11 | float: right; 12 | cursor: pointer; 13 | margin-left: 5px; 14 | } 15 | 16 | span.rename { 17 | /*https://icons8.com/icon/20388/pencil-drawing*/ 18 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAYAAAA71pVKAAAABmJLR0QA/wD/AP+gvaeTAAAA70lEQVQokbXRwUoCURTG8f8dpiG3ggUDwWVaJO58gUA3vYC9QongzoU9h1BjrcRNm1YtA0XatzcIWkWEunM2Mhw3k8yMd1LBzvIcfvd+nAP/XdLBFZ8bEVS8b2+EDxwT8gKU6KJFqCuFAFhbwAFQilpX3HP3myATSweXkFEMRgOu6dLOjB2LemYYv7OgByQXkBE1DSuqydca3gUm8K4Q4gvTTgtbFQxwbIJJ7Kgap86cA/WT+rFqgiv85JfPZ8GhxkLjOUH0gDHqGv74zjdazxdM5zmwsDix+5sgRHcWESkeTR6Hn97tZePt9S+wt1oCQiVcecKDUfIAAAAASUVORK5CYII='); 19 | background-repeat: no-repeat; 20 | float: right; 21 | cursor: pointer; 22 | margin-left: 5px; 23 | } 24 | 25 | span.delete { 26 | /*https://icons8.com/icon/11997/delete*/ 27 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAlElEQVQ4T62TwRGAIAwEL3/asQILQTvTQrQB2/EfB1An6MFD9JuwOTYoaPyk8Tz+Bex9N0Ghbt1GlozVswSxAeKhOj0hdw06u2UbrgGvKzBI6XCAUAcWAgk94vGYXExwFQwkjMliWz/FLSQAfAxJnFQT2DunRi6WOmDCatvhayTCShACAOyerbDTS1b/9yl/+bGaExzwjmgRWfhWGQAAAABJRU5ErkJggg=='); 28 | background-repeat: no-repeat; 29 | float: right; 30 | cursor: pointer; 31 | margin-left: 2px; 32 | } 33 | 34 | span.circle { 35 | /*https://icons8.com/icon/40192/sphere*/ 36 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRUlEQVQ4T6WS4U3DMBCF72wpQSqNwwR0A9K/kIqwQZmg3YAwAWxARgiTENQgfhImADaI2yLUqvEhl7pKICmV4r/2fe+d30NoebDlPNQCpk/dsSJ2hQCeFiCAjIGKHH92/1uwAqAXcOWneECgD0Yq6g7miR6YTQ4DhSwkwGPRkRfYh9yAKoA8FRlDFTlns7huNZk6GjJyfdn/A9CXABAIfzrc9S/5RCSMqdiIbB1odU5FaGw3QfQ6BfI742ILkKkg4cu9Uim/3Rvw9XzQWxR24vqyVwv4bwU5cW4BwdPpFMgj15friEsOnJAIz92BvCzvr5WXyhoR4bXNF95iZce1n6iH6mLMU/GOQJnFl+FyZY0JcWjUKw7WjfspUgIEb5wXUfd0/miKVCC/AYAj0ZFBY5GM9U2VQwQ42VT5talge8W2q1itAd/7BqERwkvHKwAAAABJRU5ErkJggg=='); 37 | background-repeat: no-repeat; 38 | float: right; 39 | cursor: pointer; 40 | margin-left: 10px; 41 | } 42 | 43 | span.info { 44 | /*https://icons8.com/icon/34698/information*/ 45 | background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABEUlEQVQ4T2NkwAMCKrYrgKQ3dHg+wKWMEZdEQu/xSaKiouEg+devX69cUGyZh00tTgOypp6/rKikqAPSdP/e/SvTsg11STIguf/kCQ0NdXOQphs3bp6cW2huQZIBEW0HbDnY2KpBmn78+tW6osrhMFEGgAKOi5+znuE/03+4BiaGXcsq7FYQZYBPxWYHFRXl/Xz8/Ay8PNxgPc+eP1/Yn6SZQJQBAQ37Bf78+GIgIytToKyk6E+yATBbCuddXyAlKRk/agAZYQBLB9xcPAZqaioGoEC8devOha9fv53/9ulrE3rGwsgLsHTAzQ1JAzDw9etXhjt37jpu6fA9gCyOYQAsHWBLNCwcPBc2NDh+QJYDAC3ziRELKKRTAAAAAElFTkSuQmCC'); 46 | background-repeat: no-repeat; 47 | float: left; 48 | cursor: pointer; 49 | } 50 | 51 | div.clear { 52 | clear: both; 53 | } 54 | -------------------------------------------------------------------------------- /src/gmailNodes.js: -------------------------------------------------------------------------------- 1 | const getGmailLocationToInject = () => { 2 | const node = document.querySelector('div.wT') 3 | const isFound = node ? true : false 4 | console.log(`GQL:getGmailLocationToInject = ${isFound.toString()}`) 5 | // where we want to put the search quick links 6 | // this has be be defined after the page loads and becomes ready 7 | return node 8 | } 9 | 10 | const gmailAccountName = () => { 11 | const node = Array.from(document.querySelectorAll('a[aria-label]')) 12 | .map(n => n.attributes['aria-label'].nodeValue) 13 | .find(t => /\(.+@.+\)/.test(t)) 14 | .match(/\((.+@.+)\)/)[1] 15 | const isFound = node ? true : false 16 | console.log(`GQL:gmailAccountName = ${isFound.toString()} "${node}"`) 17 | return node 18 | } 19 | 20 | //inside gmail controls container - contains labels such as inbox/starred/drafts/etc 21 | const labelControlsContainer = () => { 22 | const node = document.getElementsByClassName('ajl aib aZ6')[0] 23 | const isFound = node ? true : false 24 | console.log(`GQL:labelControlsContainer = ${isFound.toString()}`) 25 | return node 26 | } 27 | 28 | const widgetMainPanel = () => { 29 | const node = document.querySelector('div.akc.aZ6') 30 | const isFound = node ? true : false 31 | console.log(`GQL:widgetMainPanel = ${isFound.toString()}`) 32 | return node 33 | } 34 | 35 | const widgetInsidePanel = () => { 36 | const node = document.querySelector('div.T0.pp.saH2Ef') 37 | const isFound = node ? true : false 38 | console.log(`GQL:widgetInsidePanel = ${isFound.toString()}`) 39 | return node 40 | } 41 | 42 | const gmailSideBar = () => { 43 | const node = document.querySelector('div.nH.oy8Mbf.nn.aeN') 44 | const isFound = node ? true : false 45 | console.log(`GQL:gmailSideBar = ${isFound.toString()}`) 46 | return node 47 | } 48 | 49 | const gmailHoverSideBar = () => { 50 | const node = document.querySelector('div.nH.oy8Mbf.nn.aeN.bhZ.bym') 51 | const isFound = node ? true : false 52 | console.log(`GQL:gmailHoverSideBar = ${isFound.toString()}`) 53 | return node 54 | } 55 | 56 | const quickLinksContainer = () => { 57 | const node = document.querySelector('#gmailQuickLinksContainer') 58 | const isFound = node ? true : false 59 | console.log(`GQL:quickLinksContainer = ${isFound.toString()}`) 60 | return node 61 | } 62 | 63 | export { 64 | gmailSideBar, 65 | gmailHoverSideBar, 66 | getGmailLocationToInject, 67 | gmailAccountName, 68 | widgetInsidePanel, 69 | labelControlsContainer, 70 | quickLinksContainer 71 | } 72 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gmail quick links 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import {render} from 'react-dom' 3 | 4 | import AppContainer from './AppContainer' 5 | 6 | import { 7 | getGmailLocationToInject, 8 | gmailAccountName, 9 | widgetInsidePanel, 10 | labelControlsContainer 11 | } from './gmailNodes' 12 | 13 | const GMAIL_QUICK_LINKS_CONTAINER = 'gmailQuickLinksContainer' 14 | 15 | const beginReact = accountName => location => { 16 | render( 17 | , 18 | document.getElementById(GMAIL_QUICK_LINKS_CONTAINER) 19 | ) 20 | } 21 | 22 | const injectReact = location => { 23 | // create a div for our react container to be injected 24 | const gmailQuickLinksContainer = document.createElement('div') 25 | gmailQuickLinksContainer.id = GMAIL_QUICK_LINKS_CONTAINER 26 | 27 | if (location === 'widget') { 28 | widgetInsidePanel().append(gmailQuickLinksContainer) 29 | } else { 30 | labelControlsContainer().insertAdjacentElement( 31 | 'beforebegin', 32 | gmailQuickLinksContainer 33 | ) 34 | } 35 | 36 | console.log('Loaded Gmail Quick Links') 37 | 38 | //TODO: what is the person isn't signed in? Does this crash extension? 39 | 40 | //load react 41 | beginReact(gmailAccountName())(location) 42 | } 43 | 44 | const checkWidgetPanel = untilStop => { 45 | const startTime = new Date().getTime() 46 | 47 | const intervalId = setInterval(() => { 48 | if (new Date().getTime() - startTime > untilStop * 1000) { 49 | clearInterval(intervalId) 50 | } 51 | 52 | if (widgetInsidePanel()) { 53 | clearInterval(intervalId) 54 | 55 | console.log('unloading Gmail Quick Links due to presense of widget panel') 56 | document.getElementById(GMAIL_QUICK_LINKS_CONTAINER).remove() 57 | 58 | injectReact('widget') 59 | } 60 | }, 500) 61 | } 62 | 63 | // there are cases where the gmail left nav panel isn't loaded yet, 64 | // so we need to check for it until it exists before we can proceed 65 | const checkDomElementExist = setInterval(() => { 66 | if (getGmailLocationToInject()) { 67 | clearInterval(checkDomElementExist) 68 | 69 | //TODO: if hamburger menu is collapsed, then remove quick links, else show 70 | // //add event listener to hamburger menu if it exists 71 | // if (hambugerMenuContainer()) { 72 | // document 73 | // .getElementsByClassName('gb_jc')[0] 74 | // .addEventListener('click', () => { 75 | // if (gmailControlsContainer().offsetWidth < 100) { 76 | // document.getElementById(GMAIL_QUICK_LINKS_CONTAINER) && 77 | // document.getElementById(GMAIL_QUICK_LINKS_CONTAINER).remove() 78 | // } else { 79 | // document.getElementById(GMAIL_QUICK_LINKS_CONTAINER) && 80 | // document.getElementById(GMAIL_QUICK_LINKS_CONTAINER).remove() 81 | // injectReact() 82 | // } 83 | // }) 84 | // } 85 | 86 | //check the presense of a widget panel for 20 seconds, then stop checking 87 | checkWidgetPanel(20) 88 | 89 | injectReact() 90 | } 91 | }, 200) 92 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin') 2 | const CleanWebpackPlugin = require('clean-webpack-plugin') 3 | 4 | // the path(s) that should be cleaned 5 | let pathsToClean = ['dev', 'dist'] 6 | 7 | const config = { 8 | entry: './src/index.js', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.js$/, 13 | exclude: /node_modules/, 14 | loader: 'babel-loader' 15 | } 16 | ] 17 | }, 18 | 19 | externals: { 20 | react: 'React', 21 | 'react-dom': 'ReactDOM' 22 | }, 23 | 24 | plugins: [ 25 | new CleanWebpackPlugin(pathsToClean), 26 | 27 | // new webpack.optimize.UglifyJsPlugin() 28 | 29 | new HtmlWebpackPlugin({ 30 | template: './src/index.html' 31 | }) 32 | ] 33 | } 34 | 35 | module.exports = config 36 | -------------------------------------------------------------------------------- /webpack.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | const CopyWebpackPlugin = require('copy-webpack-plugin') 4 | 5 | const path = require('path') 6 | 7 | module.exports = args => { 8 | const outputDir = 9 | args && args.env && args.env == 'development' ? 'dev' : 'dist' 10 | const manifestFile = 11 | args && args.env && args.env == 'development' 12 | ? './manifest.dev.json' 13 | : './manifest.dist.json' 14 | const bundleType = 15 | args && args.env && args.env == 'development' ? 'dev' : 'dist' 16 | 17 | return merge(common, { 18 | devtool: 'inline-source-map', 19 | output: { 20 | path: path.resolve(__dirname, outputDir), 21 | filename: `${bundleType}.gmailquicklinks.bundle.js` 22 | }, 23 | plugins: [ 24 | new CopyWebpackPlugin([ 25 | { 26 | from: manifestFile, 27 | to: './manifest.json' 28 | }, 29 | { 30 | from: './src/assets/icon16.png', 31 | to: './assets/icon16.png' 32 | }, 33 | { 34 | from: './src/assets/icon48.png', 35 | to: './assets/icon48.png' 36 | }, 37 | { 38 | from: './src/default.css', 39 | to: './default.css' 40 | }, 41 | { 42 | from: './node_modules/react/dist/react.js', 43 | to: './react.js' 44 | }, 45 | { 46 | from: './node_modules/react-dom/dist/react-dom.js', 47 | to: './react-dom.js' 48 | } 49 | ]) 50 | ] 51 | }) 52 | } 53 | --------------------------------------------------------------------------------