├── LICENSE.txt ├── README.md ├── screenshots ├── gmailviewwatcher-compose.jpg ├── gmailviewwatcher-contacts.jpg └── gmailviewwatcher-inbox.jpg └── scripts ├── gmail-conversation-preview.user.js ├── gmail-font-toggle.user.js ├── gmail-label-colors.user.js ├── gmail-macros.user.js ├── gmail-new-macros.user.js ├── gmail-reader.user.js ├── gmail-saved-searches.user.js └── gmail-view-watcher.user.js /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Various Greasemonkey scripts that enhance Gmail. 2 | 3 | N.B. It is highly unlikely that any of these scripts still work; "new version of Gmail" refers to the [late 2007 new version](http://gmailblog.blogspot.com/2007/10/code-changes-to-prepare-gmail-for.html). There was a [late 2011 revamp](http://gmailblog.blogspot.com/2011/11/gmails-new-look.html) that changed Gmail's UI significantly. On the plus side, nearly all of these features are now available as either built-in features or [Gmail Labs](http://support.google.com/mail/bin/answer.py?hl=en&answer=29418). 4 | 5 | # For the new version of Gmail 6 | 7 | * [Macros](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-new-macros.user.js): adds more keyboard shortcuts (navigating to and applying labels, archiving, etc.) ([more info](http://blog.persistent.info/2007/11/macros-for-new-version-of-gmail.html)) 8 | 9 | # For the old version of Gmail 10 | 11 | * [Conversation Preview](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-conversation-preview.user.js): shows a preview bubble with a conversation contents while viewing the list of messages (i.e. without requiring navigation to another page) ([more info](http://blog.persistent.info/2005/08/gmail-conversation-preview-bubbles.html)) 12 | * [Fixed Font Toggle](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-font-toggle.user.js): adds a font toggle button so that messages can be viewed in a fixed-width font ([more info](http://blog.persistent.info/2005/03/adding-persistent-searches-to-gmail.html)) 13 | * [Label Colors](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-label-colors.user.js): allows labels to be colored so they stand out more ([more info](http://blog.persistent.info/2005/12/greasemonkey-christmas.html)) 14 | * [Macros](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-macros.user.js): adds more keyboard shortcuts (for marking messages as read, moving them to the trash, applying labels, etc.) ([more info](http://blog.persistent.info/2005/12/greasemonkey-christmas.html), [even more info](http://blog.persistent.info/2006/11/greasemonkey-script-updates.html)) 15 | * [Google Reader Integration](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-reader.user.js): adds a 'Feeds' links in the navigation area that opens up an embedded instance of Google Reader ([more info](http://blog.persistent.info/2006/10/google-reader-redux.html)) 16 | * [Saved Searches](https://github.com/mihaip/gmail-greasemonkey/blob/master/scripts/gmail-saved-searches.user.js): lets you save search queries so you can re-run them easily ([more info](http://blog.persistent.info/2005/03/adding-persistent-searches-to-gmail.html)) -------------------------------------------------------------------------------- /screenshots/gmailviewwatcher-compose.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/gmail-greasemonkey/1aee9723da4e8b6ec7db12a04ad56c801671a613/screenshots/gmailviewwatcher-compose.jpg -------------------------------------------------------------------------------- /screenshots/gmailviewwatcher-contacts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/gmail-greasemonkey/1aee9723da4e8b6ec7db12a04ad56c801671a613/screenshots/gmailviewwatcher-contacts.jpg -------------------------------------------------------------------------------- /screenshots/gmailviewwatcher-inbox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaip/gmail-greasemonkey/1aee9723da4e8b6ec7db12a04ad56c801671a613/screenshots/gmailviewwatcher-inbox.jpg -------------------------------------------------------------------------------- /scripts/gmail-conversation-preview.user.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Mihai Parparita. All Rights Reserved. 2 | 3 | // ==UserScript== 4 | // @name Gmail Conversation Preview 5 | // @namespace http://persistent.info/greasemonkey 6 | // @description Right-click on any conversation to get a preview bubble. 7 | // @include http://mail.google.com/* 8 | // @include https://mail.google.com/* 9 | 10 | // ==/UserScript== 11 | 12 | // TODO(mihaip): fix up list after archive 13 | // TODO(mihaip): make arrow keys scroll the bubble 14 | 15 | // Shorthand 16 | function newNode(type) {return unsafeWindow.document.createElement(type);} 17 | function newText(text) {return unsafeWindow.document.createTextNode(text);} 18 | function getNode(id) {return unsafeWindow.document.getElementById(id);} 19 | 20 | // Contants 21 | const POINT_IMAGE = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAtCA" + 22 | "YAAABWHLCfAAAABGdBTUEAANbY1E9YMgAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFk" + 23 | "eXHJZTwAAAKaSURBVHjaxJgxqFJRGMevpiWkYfiQhEgIHBycQhcnoUFoDpdcXQPByTEnCbcGlz" + 24 | "eLmxHYYhA%2BCgfFMi1DNA1RrEiJDJ%2Fe9%2FV9l3vgJfW693o8%2FuHPOSqc3znn417%2FfC" + 25 | "YAkA4hk8lkMR0CjmAajg8Fv4fDM%2BFwBB%2Fh8IXmZsFg4lVpHo1GJYlOLsKqHqPB7XavZ7MZ" + 26 | "iISHCWw2m%2BVKpQIgEHwNvSJ4Op0GJhFgeq5eEzgcDsubzUYoPE1gp9O5Hg6HcF77Bt8hMLlU" + 27 | "KsG29gm%2Biv5B4GQyCX%2FTPuv8ksDBYFBerVZC4Q8J7HA41v1%2BH%2F6lfYD9rM6FQgEuEm" + 28 | "%2FwFfQ3AicSCfifeL8%2BnxLY7%2FfLy%2BVSKPwBgW02m9xut0GLeIFvsTrn83nQKh5gC3pE" + 29 | "4FgsBnrEo87HBPZ6vev5fC4UTnEIrFarXKvVQK92AR%2BxOmezWTAio%2BBL6PcExjgERmW0zk" + 30 | "8I7PF4lDgkEn6XxaFqtQq7SC%2F4OotDmUwGdpUeMMXeNwSORCJ%2FxCER8EcEdrlc68lkAjyk" + 31 | "FRxij1W5XAZe0hWHUqkU8BSXOLQvuKY4xB2uJw5xheuNQ9zgRuIQT7juOMQFjrptJA4ZleVc1%" + 32 | "2BAyDq9oHo%2FHJaw1167EYrGQer2e1Ol0pPF4LA0GA0npyajdoQI65vP5No1Gw2K323UDaFFy" + 33 | "s9lUFqc5wQhK8G2xk98nMMahs2KxeCG42%2B1Ko9FIWbzVaikA%2Blyv17Xs77P60jpBf7LgqW" + 34 | "%2FgpEi%2F5HI5cyAQUBaiBcls5wQhsAbRLt6iX6B76Bl6iv4FW60vuu%2BbtKNQKKTAptOpFs" + 35 | "Bz9e%2F1nQr6iqboutTVnWLwre9P1dugxT%2BiP6i7%2F4mAU26tMRXO9F29njMRfbnfAgwAHZ" + 36 | "MoiqxU6iwAAAAASUVORK5CYII%3D"; 37 | 38 | const SCROLLER_PADDING = 2 * 5; 39 | const BUTTON_BAR_PADDING = 2 * 6; 40 | 41 | const SHOW_PREVIEW_KEY = 86; // V 42 | 43 | // Equivalents to values in the "More Actions..." menu 44 | const ARCHIVE_COMMAND = "rc_^i"; 45 | const MARK_UNREAD_COMMAND = "ru"; 46 | const TRASH_COMMAND = "tr"; 47 | 48 | const CONVERSATION_DATA_MAP = [ 49 | "id", 50 | "isUnread", 51 | "isStarred", 52 | "time", 53 | "people", 54 | "personalLevelIndicator", 55 | "subject", 56 | "snippet", 57 | "labels", 58 | "attachments", 59 | "id2", 60 | "isLongSnippet", 61 | "date" 62 | ]; 63 | 64 | const MESSAGE_INFO_DATA_MAP = [ 65 | "ignored", 66 | "unknown", 67 | "unknown", 68 | "id", 69 | "unknown", 70 | "unknown", 71 | "senderFullName", 72 | "senderShortName", 73 | "senderEmail", 74 | "recipients", 75 | "date", 76 | "to", 77 | "cc", 78 | "unknown", 79 | "replyTo", 80 | "date", 81 | "subject", 82 | "unknown", 83 | "unknown", 84 | "unknown", 85 | "unknown", 86 | "unknown", 87 | "date", 88 | "snippet", 89 | "snippet" 90 | ]; 91 | 92 | const SENDER_COLOR_MAP = [ 93 | "#00681c", "#cc0060", "#008391", "#009486", "#5b1094", "#846600", "#670099", 94 | "#790619" 95 | ]; 96 | 97 | const RULES = [ 98 | ".PV_bubble {position: absolute; width: 600px; border: solid 2px #000; " + 99 | "background: #fff; font-size: 12px; margin: 0; padding: 0;}", 100 | ".PV_bubble.PV_loading {width: auto; height: auto;}", 101 | ".PV_bubble.PV_loading .PV_scroller {text-align: center; color: #999; " + 102 | "font-style: italic; padding: 2em;}", 103 | ".PV_bubble .PV_scroller {overflow: auto; padding: 5px; margin: 0;}", 104 | // Hide quoted portions, signatures and other non-essential bits 105 | ".PV_bubble .q, .PV_bubble .ea, .PV_bubble .sg, .PV_bubble .gmail_quote, " + 106 | ".PV_bubble .ad {display: none}", 107 | ".PV_bubble h1 {font-size: 12px; font-weight: normal; margin: 0;}", 108 | ".PV_bubble h1 .sender {font-weight: bold}", 109 | ".PV_bubble .PV_message {border-bottom: solid 2px #ccc; margin: 0;}", 110 | ".PV_bubble .PV_message:last-child {border-bottom: 0}", 111 | ".PV_bubble .PV_message .PV_message-body {margin: 0; padding: 0}", 112 | ".PV_bubble .PV_point {position: absolute; top: 10px; " + 113 | "left: 0; margin-left: -31px; width: 31px; height: 45px;}", 114 | ".PV_bubble .PV_buttons {padding: 6px; border-bottom: solid 1px #616c7f; " + 115 | "border-left: solid 1px #616c7f; white-space: nowrap; margin: 0 0 0 7px; " + 116 | "background: #c3d9ff; -moz-border-radius: 0 0 0 7px;}", 117 | ".PV_bubble .PV_button {padding: 3px 5px 3px 5px; margin-right: 4px; " + 118 | "border-right: solid 1px #616c7f}", 119 | ".PV_bubble span.PV_button:last-child {border-right: 0;}" 120 | ]; 121 | 122 | gCurrentConversationList = []; 123 | unsafeWindow.gCurrentWindow = null; 124 | unsafeWindow.gCurrentContextMenuHandler = null; 125 | 126 | // All data received from the server goes through the function top.js.P. By 127 | // overriding it (but passing through data we get), we can be informed when 128 | // new sets conversations arrive and update the display accordingly. 129 | try { 130 | if (unsafeWindow.P && typeof(unsafeWindow.P) == "function") { 131 | var oldP = unsafeWindow.P; 132 | var thisWindow = window; 133 | 134 | unsafeWindow.P = function(window, data) { 135 | // Only override if it's a P(window, data) signature that we know about 136 | if (arguments.length == 2) { 137 | hookData(data); 138 | } 139 | oldP.apply(thisWindow, arguments); 140 | } 141 | } 142 | } catch (error) { 143 | // ignore; 144 | } 145 | 146 | function hookData(data) { 147 | var mode = data[0]; 148 | 149 | switch (mode) { 150 | // start of conversation list 151 | case "ts": 152 | gCurrentConversationList = []; 153 | break; 154 | // conversation data 155 | case "t": 156 | for (var i = 1; i < data.length; i++) { 157 | var conversationData = data[i]; 158 | var conversation = {}; 159 | 160 | for (var index in CONVERSATION_DATA_MAP) { 161 | var field = CONVERSATION_DATA_MAP[index]; 162 | 163 | conversation[field] = conversationData[index]; 164 | } 165 | 166 | gCurrentConversationList.push(conversation); 167 | } 168 | break; 169 | // end of conversation list 170 | case "te": 171 | window.setTimeout(function() { 172 | triggerHook(gCurrentConversationList); 173 | }, 0); 174 | break; 175 | } 176 | } 177 | 178 | function triggerHook(conversationList) { 179 | if (unsafeWindow.top.gCurrentWindow) { 180 | try { 181 | unsafeWindow.top.gCurrentWindow.PreviewBubble.hook(conversationList); 182 | } catch (error) { 183 | alert("exception: " + error); 184 | } 185 | } else { 186 | window.setTimeout(function() { 187 | triggerHook(conversationList); 188 | }, 10); 189 | } 190 | } 191 | 192 | if (getNode("tbd")) { 193 | initializeStyles(); 194 | 195 | unsafeWindow.top.gCurrentWindow = unsafeWindow; 196 | unsafeWindow.top.gCurrentWindow.PreviewBubble = PreviewBubble; 197 | unsafeWindow.top.gCurrentBubble = null; 198 | unsafeWindow.top.gCurrentContextMenuHandler = null; 199 | } 200 | 201 | function PreviewBubble(conversationRow) { 202 | this.conversationRow = conversationRow; 203 | this.conversationCheckbox = conversationRow.getElementsByTagName("input")[0]; 204 | this.initialConversationSelectionState = this.conversationCheckbox.checked; 205 | 206 | // bubble 207 | this.bubbleNode = newNode("div"); 208 | this.bubbleNode.className = "PV_bubble PV_loading"; 209 | unsafeWindow.document.body.appendChild(this.bubbleNode); 210 | 211 | // buttons 212 | this.buttonsNode = newNode("div"); 213 | this.buttonsNode.className = "PV_buttons"; 214 | this.bubbleNode.appendChild(this.buttonsNode); 215 | 216 | this.buttonBarWidth = BUTTON_BAR_PADDING; 217 | 218 | var self = this; 219 | this.addButton("Close", function() {self.close();}); 220 | this.addButton("Archive", bind(this, this.archive)); 221 | this.addButton("Leave Unread", bind(this, this.markUnread)); 222 | this.addButton("Trash", bind(this, this.trash)); 223 | 224 | // point 225 | this.pointNode = newNode("img"); 226 | this.pointNode.src = POINT_IMAGE; 227 | this.pointNode.className = "PV_point"; 228 | this.bubbleNode.appendChild(this.pointNode); 229 | 230 | // scroller 231 | this.scrollerNode = newNode("div"); 232 | this.scrollerNode.className = "PV_scroller"; 233 | this.scrollerNode.innerHTML = "Loading..."; 234 | this.bubbleNode.appendChild(this.scrollerNode); 235 | 236 | var conversationPosition = getAbsolutePosition(conversationRow); 237 | this.bubbleNode.style.top = (conversationPosition.top - 238 | conversationRow.offsetHeight/2 - 30) + "px"; 239 | var peopleNode = conversationRow.getElementsByTagName("span")[0]; 240 | var peopleNodePosition = getAbsolutePosition(peopleNode); 241 | this.bubbleNode.style.left = (peopleNodePosition.left + 242 | peopleNode.offsetWidth * 0.1 + this.pointNode.offsetWidth) + "px"; 243 | 244 | this.bubbleNode.style.display = "none"; 245 | this.bubbleNode.style.display = "block"; 246 | } 247 | 248 | PreviewBubble.hook = function PreviewBubble_hook(conversationList) { 249 | // The bubble can be shown in response to a right click 250 | if (unsafeWindow.top.gCurrentContextMenuHandler) { 251 | window.removeEventListener("contextmenu", 252 | unsafeWindow.top.gCurrentContextMenuHandler, 253 | false); 254 | } 255 | 256 | // Since contextMenuHandler is an inner function, there are several 257 | // instances of it. We must keep track of the one that we install so that 258 | // we can remove it later (when the conversation list gets refreshed) 259 | unsafeWindow.top.gCurrentContextMenuHandler = function(event) { 260 | return PreviewBubble.contextMenuHandler(event, conversationList); 261 | }; 262 | window.addEventListener("contextmenu", 263 | unsafeWindow.top.gCurrentContextMenuHandler, 264 | false); 265 | 266 | // Or by pressing V. 267 | if (unsafeWindow.top.gCurrentKeyHandler) { 268 | window.removeEventListener("keydown", 269 | unsafeWindow.top.gCurrentKeyHandler, 270 | false); 271 | } 272 | unsafeWindow.top.gCurrentKeyHandler = function(event) { 273 | return PreviewBubble.keyHandler(event, conversationList); 274 | } 275 | window.addEventListener('keydown', 276 | unsafeWindow.top.gCurrentKeyHandler, 277 | false); 278 | } 279 | 280 | PreviewBubble.contextMenuHandler = 281 | function PreviewBubble_contextMenuHandler(event, conversationList) { 282 | var target = event.target; 283 | 284 | while (target && target.id.indexOf("w_") != 0) { 285 | target = target.parentNode; 286 | } 287 | 288 | if (target) { 289 | event.preventDefault(); 290 | event.stopPropagation(); 291 | 292 | var index = parseInt(target.id.substring(2)); 293 | 294 | PreviewBubble.showBubble(target, conversationList[index]); 295 | } 296 | } 297 | 298 | PreviewBubble.keyHandler = function PreviewBubble_keyHandler(event, conversationList) { 299 | // Apparently we still see Firefox shortcuts like control-T for a new tab 300 | // and checking for modifiers lets us ignore those 301 | if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { 302 | return false; 303 | } 304 | 305 | // We also don't want to interfere with regular user typing 306 | if (event.target && event.target.nodeName) { 307 | var targetNodeName = event.target.nodeName.toLowerCase(); 308 | if (targetNodeName == "textarea" || 309 | (targetNodeName == "input" && 310 | (!event.target.getAttribute("type") || 311 | event.target.getAttribute("type").toLowerCase() == "text"))) { 312 | return false; 313 | } 314 | } 315 | 316 | if (event.keyCode != SHOW_PREVIEW_KEY) { 317 | if (unsafeWindow.top.gCurrentBubble) { 318 | // We don't close the bubble straight away since we want the 319 | // conversation to still be selected so that built-in keyboard 320 | // shortcuts still work 321 | window.setTimeout(function() { 322 | unsafeWindow.top.gCurrentBubble.close(); 323 | }, 100); 324 | } 325 | 326 | return false; 327 | } 328 | 329 | var currentConversation = PreviewBubble.getCurrentConversation(); 330 | 331 | if (currentConversation == -1) { 332 | return false; 333 | } 334 | 335 | PreviewBubble.showBubble(getNode("w_" + currentConversation), 336 | conversationList[currentConversation]); 337 | 338 | return true; 339 | } 340 | 341 | 342 | PreviewBubble.getCurrentConversation = function PreviewBubble_getCurrentConversation() { 343 | var chevron = getNode("ar"); 344 | var conversationTable = getNode("tb"); 345 | var row = getNode("w_0"); 346 | 347 | if (!row || !chevron || !conversationTable) { 348 | return -1; 349 | } 350 | 351 | return (chevron.offsetTop - conversationTable.offsetTop - 5)/ 352 | row.offsetHeight; 353 | } 354 | 355 | PreviewBubble.showBubble = 356 | function PreviewBubble_showBubble(conversationRow, conversation) { 357 | if (unsafeWindow.top.gCurrentBubble) { 358 | var sameRow = unsafeWindow.top.gCurrentBubble.conversationRow == 359 | conversationRow; 360 | unsafeWindow.top.gCurrentBubble.close(); 361 | if (sameRow) { 362 | return; 363 | } 364 | } 365 | 366 | hideTooltips(); 367 | var bubble = 368 | unsafeWindow.top.gCurrentBubble = 369 | new PreviewBubble(conversationRow); 370 | 371 | bubble.selectConversation(); 372 | bubble.installGlobalHideHandler(); 373 | bubble.fill(conversation); 374 | } 375 | 376 | 377 | PreviewBubble.prototype.selectConversation = 378 | function PreviewBubble_selectConversation() { 379 | if (!this.conversationCheckbox.checked) { 380 | fakeMouseEvent(this.conversationCheckbox, "click"); 381 | // We have to reset the classname for the conversation to be displayed as 382 | // read, since clicking on the checkbox causes it to be redrawn, and 383 | // according to Gmail's internal state it's still unread 384 | this.conversationRow.className = "rr sr"; 385 | } 386 | } 387 | 388 | PreviewBubble.prototype.deselectConversation = 389 | function PreviewBubble_deselectConversation(leaveUnread) { 390 | if (!this.initialConversationSelectionState) { 391 | fakeMouseEvent(this.conversationCheckbox, "click"); 392 | } 393 | 394 | if (!leaveUnread) { 395 | this.conversationRow.className = "rr"; 396 | } 397 | } 398 | 399 | PreviewBubble.prototype.addButton = 400 | function PreviewBubble_addButton(buttonTitle, action) { 401 | var buttonNode = newNode("span"); 402 | buttonNode.innerHTML = buttonTitle; 403 | buttonNode.className = "PV_button lk"; 404 | buttonNode.addEventListener("click", action, true); 405 | this.buttonsNode.appendChild(buttonNode); 406 | 407 | this.buttonBarWidth += buttonNode.offsetWidth; 408 | } 409 | 410 | PreviewBubble.prototype.fill = function PreviewBubble_fill(conversation) { 411 | this.conversation = conversation; 412 | 413 | var self = this; 414 | GM_xmlhttpRequest({ 415 | 'method': 'GET', 416 | 'url' : getParentUrl() + "?&view=cv&search=all&th=" + 417 | conversation.id + "&lvp=-1&cvp=2&qt=", 418 | 'onload': function(details) { 419 | var messages = parseMessages(details.responseText); 420 | self.setContents(messages); 421 | self.shrinkToFit(); 422 | } 423 | }); 424 | } 425 | 426 | PreviewBubble.prototype.setContents = 427 | function PreviewBubble_setContents(messages) { 428 | var senderColors = {}; 429 | var senderColorCount = 0; 430 | 431 | this.scrollerNode.innerHTML = ""; 432 | 433 | for (var i=0; i < messages.length; i++) { 434 | var m = messages[i]; 435 | 436 | if (!m.body) { 437 | continue; 438 | } 439 | 440 | var sender = m.senderFullName; 441 | if (!senderColors[sender]) { 442 | senderColors[sender] = 443 | SENDER_COLOR_MAP[senderColorCount % SENDER_COLOR_MAP.length]; 444 | senderColorCount++; 445 | } 446 | 447 | this.scrollerNode.innerHTML += 448 | '
' + 449 | "

" + 450 | '' + sender + "" + 452 | " to " + m.to + 453 | "

" + 454 | '
' + m.body + "
" + 455 | '
'; 456 | } 457 | 458 | // Remove PV_loading CSS class 459 | this.bubbleNode.className = "PV_bubble"; 460 | } 461 | 462 | PreviewBubble.prototype.shrinkToFit = function PreviewBubble_shrinkToFit() { 463 | var bubblePosition = getAbsolutePosition(this.bubbleNode); 464 | var rowPosition = getAbsolutePosition(this.conversationRow); 465 | 466 | // We first try to find the ideal width. We do a binary between the maximum 467 | // (all the way to the right edge of the conversation list) and the minimum 468 | // (the button bar's width). 469 | this.bubbleNode.style.width = 470 | (rowPosition.left + this.conversationRow.offsetWidth - bubblePosition.left - 471 | 4) + "px"; 472 | 473 | var maxWidth = this.scrollerNode.offsetWidth - SCROLLER_PADDING; 474 | var minWidth = this.buttonBarWidth; 475 | // We use the height of the scroller node as the conditional, since if the 476 | // bubble gets too narrow the height will increase. We use the clientHeight 477 | // attribute as opposed to the offsetHeight one because we want to detect 478 | // the case where horizontal scrollbars show up (for HTML messages that 479 | // don't wrap) 480 | var startHeight = this.scrollerNode.clientHeight; 481 | 482 | while (maxWidth - minWidth > 1) { 483 | var currentWidth = Math.round((maxWidth + minWidth)/2); 484 | this.scrollerNode.style.width = currentWidth + "px"; 485 | 486 | var currentHeight = this.scrollerNode.clientHeight; 487 | 488 | if (currentHeight == startHeight) { 489 | maxWidth = currentWidth; 490 | } else { 491 | minWidth = currentWidth; 492 | } 493 | } 494 | 495 | this.scrollerNode.style.width = "auto"; 496 | this.bubbleNode.style.width = maxWidth + SCROLLER_PADDING; 497 | 498 | if (this.scrollerNode.innerHTML == "") { 499 | this.scrollerNode.style.display = "none"; 500 | } 501 | 502 | // We want the bubble to be no taller than the window height (minus some 503 | // padding). We also don't want to shift up the bubble more than necessary, 504 | // so that the action links stay as close to the user's cursor as possible. 505 | var newBubbleTop = -1; 506 | var maxHeight = window.innerHeight - 20; 507 | var minTop = window.scrollY + 10; 508 | 509 | if (this.bubbleNode.offsetHeight > maxHeight) { 510 | this.scrollerNode.style.height = 511 | (maxHeight - SCROLLER_PADDING - this.buttonsNode.offsetHeight - 4) + "px"; 512 | newBubbleTop = minTop; 513 | } else { 514 | var bubblePosition = getAbsolutePosition(this.bubbleNode); 515 | var bubbleBottom = bubblePosition.top + this.bubbleNode.offsetHeight; 516 | 517 | if (bubbleBottom > window.scrollY + 10 + maxHeight) { 518 | newBubbleTop = 519 | window.scrollY + 10 + maxHeight - this.bubbleNode.offsetHeight; 520 | } 521 | } 522 | 523 | if (newBubbleTop != -1) { 524 | var oldTop = this.bubbleNode.offsetTop; 525 | this.bubbleNode.style.top = newBubbleTop + "px"; 526 | var delta = this.bubbleNode.offsetTop - oldTop; 527 | this.pointNode.style.marginTop = (-delta) + "px"; 528 | } 529 | } 530 | 531 | PreviewBubble.prototype.installGlobalHideHandler = 532 | function PreviewBubble_installGlobalHideHandler() { 533 | if (this.bodyClickClosure) { 534 | this.removeGlobalHideHandler(); 535 | } 536 | 537 | this.bodyClickClosure = bind(this, 538 | function(event) { 539 | var insideBubble = false; 540 | var node = event.target; 541 | while (node) { 542 | if (node == this.bubbleNode) { 543 | insideBubble = true; 544 | break; 545 | } 546 | node = node.parentNode; 547 | } 548 | 549 | if (!insideBubble) { 550 | this.close(); 551 | } 552 | }); 553 | 554 | document.body.addEventListener("click", this.bodyClickClosure, true); 555 | } 556 | 557 | PreviewBubble.prototype.removeGlobalHideHandler = 558 | function PreviewBubble_removeGlobalHideHandler() { 559 | if (this.bodyClickClosure) { 560 | document.body.removeEventListener("click", this.bodyClickClosure, true); 561 | 562 | this.bodyClickClosure = null; 563 | } 564 | } 565 | 566 | PreviewBubble.prototype.close = function PreviewBubble_close(leaveUnread) { 567 | this.bubbleNode.parentNode.removeChild(this.bubbleNode); 568 | 569 | this.removeGlobalHideHandler(); 570 | this.deselectConversation(leaveUnread); 571 | 572 | showTooltips(); 573 | 574 | unsafeWindow.top.gCurrentBubble = null; 575 | } 576 | 577 | PreviewBubble.prototype.archive = function PreviewBubble_archive() { 578 | doCommand(ARCHIVE_COMMAND); 579 | this.close(); 580 | } 581 | 582 | PreviewBubble.prototype.markUnread = function PreviewBubble_markUnread() { 583 | if (this.conversation) { 584 | var postData = "act=ur&at=" + getCookie("GMAIL_AT") + 585 | "&vp=&msq=&ba=false&t=" + this.conversation.id; 586 | GM_xmlhttpRequest({ 587 | 'method': 'POST', 588 | 'url': getParentUrl() + "?&search=inbox&view=tl&start=0" + 589 | this.conversation.id + "&lvp=-1&cvp=2&qt=", 590 | 'headers': { 591 | 'Content-Length': postData.length, 592 | 'Content-Type': 'application/x-www-form-urlencoded' 593 | }, 594 | 'data': postData, 595 | // TODO(mihaip): check for success? 596 | 'onload': function() {} 597 | }); 598 | } 599 | 600 | this.close(true); 601 | } 602 | 603 | PreviewBubble.prototype.trash = function PreviewBubble_trash() { 604 | doCommand(TRASH_COMMAND); 605 | this.close(); 606 | } 607 | 608 | 609 | // Utility functions 610 | 611 | function initializeStyles() { 612 | var styleNode = newNode("style"); 613 | 614 | document.body.appendChild(styleNode); 615 | 616 | var styleSheet = document.styleSheets[document.styleSheets.length - 1]; 617 | 618 | for (var i=0; i < RULES.length; i++) { 619 | styleSheet.insertRule(RULES[i], 0); 620 | } 621 | } 622 | 623 | function hideTooltips() { 624 | var styleNode = newNode("style"); 625 | styleNode.id = "tooltipHider"; 626 | 627 | document.body.appendChild(styleNode); 628 | 629 | var styleSheet = document.styleSheets[document.styleSheets.length - 1]; 630 | 631 | styleSheet.insertRule("#pop {display: none !important}", 0); 632 | styleSheet.insertRule("#tip {display: none !important}", 0); 633 | } 634 | 635 | function showTooltips() { 636 | var styleNode = getNode("tooltipHider"); 637 | 638 | styleNode.parentNode.removeChild(styleNode); 639 | } 640 | 641 | function doCommand(command) { 642 | // Command execution is accomplished by creating a fake action menu and 643 | // faking a selection from it (we can't use the real action menu since the 644 | // command may not be in it, if it's a button) 645 | var actionMenu = newNode("select"); 646 | var commandOption = newNode("option"); 647 | commandOption.value = command; 648 | commandOption.innerHTML = command; 649 | actionMenu.appendChild(commandOption); 650 | actionMenu.selectedIndex = 0; 651 | 652 | var actionMenuNode = getActionMenu(); 653 | 654 | if (actionMenuNode) { 655 | var onchangeHandler = actionMenuNode.onchange; 656 | 657 | onchangeHandler.apply(actionMenu, null); 658 | } else { 659 | GM_log("Not able to find a 'More Actions...' menu"); 660 | } 661 | } 662 | 663 | function fakeMouseEvent(node, eventType) { 664 | var event = node.ownerDocument.createEvent("MouseEvents"); 665 | 666 | event.initMouseEvent(eventType, 667 | true, // can bubble 668 | true, // cancellable 669 | node.ownerDocument.defaultView, 670 | 1, // clicks 671 | 50, 50, // screen coordinates 672 | 50, 50, // client coordinates 673 | false, false, false, false, // control/alt/shift/meta 674 | 0, // button, 675 | node); 676 | 677 | node.dispatchEvent(event); 678 | } 679 | 680 | function bind(object, func) { 681 | return function() { 682 | return func.apply(object, arguments); 683 | } 684 | } 685 | 686 | function getAbsolutePosition(node) { 687 | var top = node.offsetTop; 688 | var left = node.offsetLeft; 689 | 690 | for (var parent = node.offsetParent; parent; parent = parent.offsetParent) { 691 | top += parent.offsetTop; 692 | left += parent.offsetLeft; 693 | } 694 | 695 | return {top: top, left: left}; 696 | } 697 | 698 | const DATA_BLOCK_RE = new RegExp('(D\\(\\["[\\s\\S]*?\n\\);\n)', 'gm'); 699 | 700 | function parseMessages(conversationText) { 701 | // Unfortunately we can't parse the text to a DOM since it's HTML and 702 | // DOMParser can only deal with XML. RegExps it is. 703 | 704 | var parsedText = ""; 705 | 706 | var matches = conversationText.match(DATA_BLOCK_RE); 707 | 708 | var messages = []; 709 | var currentMessage = null; 710 | 711 | function D(data) { 712 | mode = data[0]; 713 | switch (mode) { 714 | case "mi": 715 | currentMessage = {}; 716 | for (var i=1; i < data.length; i++) { 717 | currentMessage[MESSAGE_INFO_DATA_MAP[i]] = data[i]; 718 | } 719 | currentMessage.body = ""; 720 | messages.push(currentMessage); 721 | break; 722 | case "mb": 723 | currentMessage.body += data[1]; 724 | break; 725 | } 726 | } 727 | 728 | eval(matches.join("")); 729 | 730 | return messages; 731 | } 732 | 733 | function getCookie(name) { 734 | var re = new RegExp(name + "=([^;]+)"); 735 | var value = re.exec(document.cookie); 736 | return (value != null) ? unescape(value[1]) : null; 737 | } 738 | 739 | function getParentUrl() { 740 | return window.location.href.replace(/\?.*/, ''); 741 | } 742 | 743 | function getActionMenu() { 744 | const ACTION_MENU_IDS = ["tam", "ctam", "tamu", "ctamu"]; 745 | 746 | for (var i = 0, id; id = ACTION_MENU_IDS[i]; i++) { 747 | if (getNode(id) != null) { 748 | return getNode(id); 749 | } 750 | } 751 | 752 | return null; 753 | } -------------------------------------------------------------------------------- /scripts/gmail-font-toggle.user.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Mihai Parparita. All Rights Reserved. 2 | 3 | // ==UserScript== 4 | // @name Gmail Fixed Font Toggle 5 | // @namespace http://persistent.info/greasemonkey 6 | // @description Adds a fixed font size toggle button. 7 | // @include http://mail.google.com/* 8 | // @include https://mail.google.com/* 9 | 10 | // ==/UserScript== 11 | 12 | // Utility functions 13 | function getObjectMethodClosure(object, method) { 14 | return function() { 15 | return object[method].apply(object, arguments); 16 | } 17 | } 18 | 19 | // Shorthand 20 | var newNode = getObjectMethodClosure(document, "createElement"); 21 | var getNode = getObjectMethodClosure(document, "getElementById"); 22 | 23 | // Contants 24 | const MONOSPACE_RULE = ".mb, textarea.tb {font-family: monospace !important;}"; 25 | const NORMAL_RULE = ".mb, textarea.tb {}"; 26 | 27 | const TOGGLE_FONT_IMAGE = "data:image/gif;base64,R0lGODlhEAAQAIABAAAAzP%2F%2" + 28 | "F%2FyH5BAEAAAEALAAAAAAQABAAAAImjI%2BJwO28wIGG1rjUlFrZvoHJVz0SGXBqymXphU5" + 29 | "Y17Kg%2BnixKBYAOw%3D%3D"; 30 | 31 | // Globals 32 | var styleSheet = null; 33 | var currentRule = NORMAL_RULE; 34 | 35 | var toggleFontLink = null; 36 | 37 | function initializeToggleFont() { 38 | var linksContainer = getNode("ap"); 39 | 40 | if (!linksContainer) { 41 | return; 42 | } 43 | 44 | toggleFontLink = newNode("div"); 45 | toggleFontLink.className = "ar"; 46 | toggleFontLink.addEventListener("click", toggleMessageBodyFont, false); 47 | toggleFontLink.innerHTML = 48 | '' + 49 | '' + 50 | 'Toggle font' + 51 | ''; 52 | 53 | linksContainer.appendChild(toggleFontLink); 54 | 55 | checkToggleFontParent(); 56 | } 57 | 58 | function checkToggleFontParent() { 59 | if (toggleFontLink.parentNode != getNode("ap")) { 60 | getNode("ap").appendChild(toggleFontLink); 61 | } 62 | 63 | window.setTimeout(checkToggleFontParent, 200); 64 | } 65 | 66 | function toggleMessageBodyFont() { 67 | styleSheet.deleteRule(0); 68 | if (currentRule == NORMAL_RULE) { 69 | currentRule = MONOSPACE_RULE; 70 | } else { 71 | currentRule = NORMAL_RULE; 72 | } 73 | styleSheet.insertRule(currentRule, 0); 74 | } 75 | 76 | function initializeStyles() { 77 | var styleNode = newNode("style"); 78 | 79 | document.body.appendChild(styleNode); 80 | 81 | styleSheet = document.styleSheets[document.styleSheets.length - 1]; 82 | 83 | styleSheet.insertRule(NORMAL_RULE, 0); 84 | } 85 | 86 | initializeStyles(); 87 | initializeToggleFont(); -------------------------------------------------------------------------------- /scripts/gmail-label-colors.user.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Mihai Parparita. All Rights Reserved. 2 | 3 | // ==UserScript== 4 | // @name Gmail Label Colors 5 | // @namespace http://persistent.info/greasemonkey 6 | // @description Optionally colors label names (when they have a #color suffix) 7 | // @include http://mail.google.com/* 8 | // @include https://mail.google.com/* 9 | // ==/UserScript== 10 | 11 | // Constants 12 | const LABELS_INDEX = 8; 13 | const RULES = [ 14 | ".ct .colored {-moz-border-radius: 8px; padding: 2px 4px 2px 4px; color: #fff;}" 15 | ]; 16 | 17 | // These are actually dynamic, and Firefox seems to crash if we initialize them 18 | // inline, so they are set in overrideEscape() 19 | var NO_ESCAPE_START; 20 | var NO_ESCAPE_END; 21 | 22 | // All data received from the server goes through the function top.js.P. By 23 | // overriding it (but passing through data we get), we can be informed when 24 | // new sets conversations arrive and update the display accordingly. 25 | try { 26 | if (unsafeWindow.P && typeof(unsafeWindow.P) == "function") { 27 | var oldP = unsafeWindow.P; 28 | 29 | overrideEscape(); 30 | 31 | unsafeWindow.P = function(iframe, data) { 32 | // Only override if it's a P(iframe, data) signature that we know about 33 | if (arguments.length == 2) { 34 | hookData(iframe, data); 35 | } 36 | oldP.apply(iframe, arguments); 37 | } 38 | } 39 | } catch (error) { 40 | // ignore; 41 | } 42 | 43 | function hookData(iframe, data) { 44 | var mode = data[0]; 45 | 46 | switch (mode) { 47 | // conversation data 48 | case "t": 49 | for (var i = 1; i < data.length; i++) { 50 | var conversationData = data[i]; 51 | var labels = conversationData[LABELS_INDEX]; 52 | 53 | for (var j = 0; j < labels.length; j++) { 54 | var info = getLabelColorInfo(labels[j]); 55 | 56 | if (!info.color) continue; 57 | 58 | labels[j] = 59 | NO_ESCAPE_START + 60 | '' + 61 | info.name + 62 | '' + 63 | NO_ESCAPE_END; 64 | } 65 | } 66 | break; 67 | } 68 | } 69 | 70 | // Label names pass through an escaping function, which means that we can't 71 | // use HTML to color them. We override this function and allow the use of 72 | // markers within it turn off escaping as necessary. 73 | function overrideEscape() { 74 | // We want the no escape start/end markers to be non-deterministic, so that 75 | // an attacker can't embed arbitrary HTML into an evil message. The action token 76 | // seems like a good secret string. 77 | NO_ESCAPE_START = ''; 78 | NO_ESCAPE_END = ''; 79 | 80 | // First find the function (most escaping functions invoke the replace method 81 | // of the string object and have the string "<" in them 82 | for (var propName in unsafeWindow) { 83 | var propValue = unsafeWindow[propName]; 84 | 85 | if (typeof(propValue) != "function") { 86 | continue; 87 | } 88 | 89 | var functionBody = propValue.toString(); 90 | 91 | if (functionBody.indexOf("replace") != -1 && 92 | functionBody.indexOf("<") != -1) { 93 | 94 | unsafeWindow[propName] = getEscapeClosure(propValue); 95 | break; 96 | } 97 | } 98 | } 99 | 100 | function getEscapeClosure(oldEscapeFunction) { 101 | return function(str) { 102 | var escaped = ""; 103 | var start, end; 104 | while ((start = str.indexOf(NO_ESCAPE_START)) != -1) { 105 | escaped += oldEscapeFunction(str.substring(0, start)); 106 | 107 | start += NO_ESCAPE_START.length; 108 | end = str.indexOf(NO_ESCAPE_END); 109 | escaped += str.substring(start, end); 110 | str = str.substring(end + NO_ESCAPE_END.length); 111 | } 112 | escaped += oldEscapeFunction(str); 113 | 114 | return escaped; 115 | } 116 | } 117 | 118 | 119 | function getLabelColorInfo(rawLabelName) { 120 | if (rawLabelName.indexOf(" #") != -1) { 121 | var split = rawLabelName.split(" #"); 122 | return {name: split[0], color: split[1]}; 123 | } else { 124 | return {name: rawLabelName}; 125 | } 126 | } 127 | 128 | if (document.getElementById("tbd")) { 129 | insertStyles(); 130 | } 131 | 132 | function insertStyles() { 133 | var styleNode = document.createElement("style"); 134 | 135 | document.body.appendChild(styleNode); 136 | 137 | var styleSheet = document.styleSheets[document.styleSheets.length - 1]; 138 | 139 | for (var i=0; i < RULES.length; i++) { 140 | styleSheet.insertRule(RULES[i], 0); 141 | } 142 | } 143 | 144 | function getCookie(name) { 145 | var re = new RegExp(name + "=([^;]+)"); 146 | var value = re.exec(document.cookie); 147 | return (value != null) ? unescape(value[1]) : null; 148 | } -------------------------------------------------------------------------------- /scripts/gmail-macros.user.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Mihai Parparita. All Rights Reserved. 2 | 3 | // ==UserScript== 4 | // @name Gmail Macros 5 | // @namespace http://persistent.info/greasemonkey 6 | // @description Extra (customizable) keyboard shortcuts and macros. 7 | // @include http://mail.google.com/* 8 | // @include https://mail.google.com/* 9 | // ==/UserScript== 10 | 11 | // Constants 12 | 13 | const LABEL_PREFIX = "sc_"; 14 | const SELECT_PREFIX = "sl_"; 15 | const SAVED_SEARCH_PREFIX = "savedsearch_"; 16 | 17 | // Maps human readable names to DOM node IDs 18 | const SPECIAL_LABELS = { 19 | "Inbox": "ds_inbox", 20 | "Starred": "ds_starred", 21 | "Chats": "ds_chats", 22 | "Sent Mail": "ds_sent", 23 | "Drafts": "ds_drafts", 24 | "All Mail": "ds_all", 25 | "Spam": "ds_spam", 26 | "Trash": "ds_trash", 27 | "Contacts": "cont" 28 | }; 29 | 30 | // Command Names 31 | const MARK_AS_READ = "rd"; 32 | const MARK_AS_UNREAD = "ur"; 33 | 34 | const ARCHIVE = "rc_^i"; 35 | const MOVE_TO_INBOX = "ib"; 36 | const ADD_STAR = "st"; 37 | const REMOVE_STAR = "xst"; 38 | 39 | const APPLY_LABEL = "ac_"; // Followed by label name 40 | const REMOVE_LABEL = "rc_"; // Followed by label name 41 | 42 | const MOVE_TO_TRASH = "tr"; 43 | const DELETE_FOREVER = "dl"; // Only works when in trash and spam views 44 | 45 | const REPORT_SPAM = "sp"; 46 | const NOT_SPAM = "us"; 47 | 48 | const HANDLERS_TABLE = { 49 | 68: [MARK_AS_READ, ARCHIVE], // D: Discard 50 | 69: [ARCHIVE], // E: always archivE (Y's context-dependent behavior is annoying) 51 | 82: [MARK_AS_READ], // R: mark as Read 52 | 84: [MOVE_TO_TRASH],// T: move to Trash 53 | 90: [MARK_AS_UNREAD] // Z: mark as Unread (undo read similar to Ctrl+Z) 54 | }; 55 | 56 | const LABEL_ACTIONS = { 57 | // g: go to label 58 | 71: function(labelName) { 59 | var labelDiv = getLabelNode(labelName); 60 | 61 | var eventType = labelName == "Contacts" ? "click" : "mousedown"; 62 | 63 | simulateClick(labelDiv, eventType); 64 | }, 65 | // l: apply label 66 | 76: function (labelName) { 67 | // we don't do special labels (there's other commands, like "archive" for 68 | // that) 69 | if (labelName in SPECIAL_LABELS) { 70 | return; 71 | } 72 | 73 | runCommands([APPLY_LABEL + labelName]); 74 | }, 75 | // b: remove label 76 | 66: function (labelName) { 77 | // we don't do special labels (there's other commands, like "archive" for 78 | // that) 79 | if (labelName in SPECIAL_LABELS) { 80 | return; 81 | } 82 | 83 | runCommands([REMOVE_LABEL + labelName]); 84 | } 85 | }; 86 | 87 | const SELECT_KEY_VALUES = { 88 | 65: ['a','All'], 89 | 78: ['n','None'], 90 | 82: ['r','Read'], 91 | 83: ['s','Starred'], 92 | 84: ['t','Unstarred'], 93 | 85: ['u','Unread'] 94 | }; 95 | 96 | const SELECT_ACTIONS = { 97 | // shift-x: select 98 | 88: function(selectionName) { 99 | var selectDiv = getNode(SELECT_PREFIX + selectionName); 100 | simulateClick(selectDiv, "mousedown"); 101 | }, 102 | // h: show help 103 | 72: function() { 104 | banner.show(true); 105 | banner.update(getHelpHtml()); 106 | } 107 | }; 108 | 109 | const SIMPLE_ACTIONS = { 110 | // o: expand/collapse all 111 | 79: function(selectionName) { 112 | if(getNode("ec")){ 113 | simulateClick(getNode("ec"), "mousedown"); 114 | } 115 | if(getNode("ind")){ 116 | simulateClick(getNode("ind"), "mousedown"); 117 | } 118 | } 119 | }; 120 | 121 | const BUILTIN_KEYS_HELP = { 122 | "C*" : "Compose", 123 | "/" : "Search", 124 | "Q" : "Quick contacts", 125 | "J/K" : "Move to an older/newer conversation", 126 | "N/P" : "Next/Previous message", 127 | "<Enter>" : "Open*, expand/collapse, press button", 128 | "U" : "Return to the conversation list", 129 | "Y" : "Archive/remove from current view", 130 | "X" : "Select a conversation", 131 | "S" : "Star a message or conversation", 132 | "!" : "Report Spam!", 133 | "R*" : "Reply", 134 | "A*" : "Reply All", 135 | "F*" : "Forward" 136 | }; 137 | 138 | const ADDED_KEYS_HELP = { 139 | "H" : "What are the keyboard commands?", 140 | "T" : "Trash conversation(s)", 141 | "E" : "ArchivE conversations(s) (always)", 142 | "R" : "Mark conversation(s) as Read", 143 | "Z" : "Mark conversation(s) as unread (vs. Ctrl+Z undo)", 144 | "D" : "Discard (read & archive) conversation(s)", 145 | "O" : "Expand/collapse all messages in a conversation", 146 | 147 | " " : " ", 148 | "V" : "PreView a conversation
(requires Gmail Conversation Preview)", 149 | " " : " ", 150 | 151 | "G+label" : "Go to label (including inbox/starred/trash/etc.)", 152 | "L+label" : "Label conversation(s) as label", 153 | "B+label" : "Remove label from conversation(s)", 154 | 155 | "<Shift>-X+key" : "Select " + 156 | "A - All, " + 157 | "N - None, " + 158 | "R - Read,
" + 159 | "U - Unread, " + 160 | "S - Starred, " + 161 | "T - UnsTarred" 162 | }; 163 | 164 | // Utility functions 165 | function simulateClick(node, eventType) { 166 | var event = node.ownerDocument.createEvent("MouseEvents"); 167 | event.initMouseEvent("mousedown", 168 | true, // can bubble 169 | true, // cancellable 170 | window, 171 | 1, // clicks 172 | 50, 50, // screen coordinates 173 | 50, 50, // client coordinates 174 | false, false, false, false, // control/alt/shift/meta 175 | 0, // button, 176 | node); 177 | node.dispatchEvent(event); 178 | } 179 | 180 | function getHelpHtml() { 181 | var html = 182 | '' + 183 | '' + 186 | '' + 187 | '' + 188 | ''; 189 | 190 | var base = []; 191 | for (var key in BUILTIN_KEYS_HELP) { 192 | base.push(""); 193 | } 194 | 195 | var added = []; 196 | for (var key in ADDED_KEYS_HELP) { 197 | added.push(""); 198 | } 199 | 200 | for(var i = 0; i < base.length; i++) { 201 | html += "" + base[i] + added[i] + ""; 202 | } 203 | 204 | html += 205 | '' + 206 | '' + 209 | '' + 210 | '
' + 184 | 'Available Keyboard Commands' + 185 | '
StandardExtended
" + key + "" + BUILTIN_KEYS_HELP[key] + "" + key + "" + ADDED_KEYS_HELP[key] + "
' + 207 | '* Hold <Shift> for action in a new window' + 208 | '
'; 211 | 212 | return html; 213 | } 214 | 215 | // Shorthand 216 | function bind(func, thisObject) { 217 | return function() { 218 | return func.apply(thisObject, arguments); 219 | } 220 | } 221 | 222 | var newNode = bind(unsafeWindow.document.createElement, unsafeWindow.document); 223 | var getNode = bind(unsafeWindow.document.getElementById, unsafeWindow.document); 224 | 225 | // Globals 226 | 227 | var banner; 228 | 229 | var dispatchedActionTimeout = null; 230 | var activeLabelAction = null; 231 | var activeSelectAction = null; 232 | var labels = new Array(); 233 | var selectedLabels = new Array(); 234 | var labelInput = null; 235 | var labelsBoxWasClosed = false; 236 | 237 | if (isLoaded()) { 238 | banner = new Banner(); 239 | window.addEventListener('keydown', keyHandler, false); 240 | 241 | GM_addStyle(".banner b {font-weight: normal; color: yellow;}"); 242 | } 243 | 244 | function isLoaded() { 245 | // Action or contacts menus is present 246 | return (getActionMenu() != null) || (getNode("co") != null); 247 | } 248 | 249 | function getActionMenu() { 250 | const ACTION_MENU_IDS = ["tam", "ctam", "tamu", "ctamu"]; 251 | 252 | for (var i = 0, id; id = ACTION_MENU_IDS[i]; i++) { 253 | if (getNode(id) != null) { 254 | return getNode(id); 255 | } 256 | } 257 | 258 | return null; 259 | } 260 | 261 | function keyHandler(event) { 262 | // Apparently we still see Firefox shortcuts like control-T for a new tab - 263 | // checking for modifiers lets us ignore those 264 | if (event.altKey || event.ctrlKey || event.metaKey) { 265 | return false; 266 | } 267 | 268 | // We also don't want to interfere with regular user typing 269 | if (event.target && event.target.nodeName) { 270 | var targetNodeName = event.target.nodeName.toLowerCase(); 271 | if (targetNodeName == "textarea" || 272 | (targetNodeName == "input" && event.target.type && 273 | event.target.type.toLowerCase() == "text")) { 274 | return false; 275 | } 276 | } 277 | 278 | var k = event.keyCode; 279 | 280 | if (k in SIMPLE_ACTIONS) { 281 | SIMPLE_ACTIONS[k](); 282 | return true; 283 | } 284 | 285 | if (k in LABEL_ACTIONS) { 286 | if (activeLabelAction) { 287 | endLabelAction(); 288 | return false 289 | } else { 290 | activeLabelAction = LABEL_ACTIONS[k]; 291 | beginLabelAction(); 292 | return true; 293 | } 294 | } 295 | 296 | if ((k in SELECT_ACTIONS) && (k != 88 || event.shiftKey)) { 297 | if (activeSelectAction) { 298 | endSelectAction(); 299 | return false; 300 | } else { 301 | activeSelectAction = SELECT_ACTIONS[k]; 302 | beginSelectAction(); 303 | return true; 304 | } 305 | } 306 | 307 | if (k in HANDLERS_TABLE) { 308 | runCommands(HANDLERS_TABLE[k]); 309 | return true; 310 | } 311 | 312 | return false; 313 | } 314 | 315 | function beginLabelAction() { 316 | // Make sure the labels box is open 317 | var labelsHeaderNode = getNode("nt_0"); 318 | if (labelsHeaderNode.nextSibling == null) { 319 | labelsBoxWasClosed = true; 320 | simulateClick(labelsHeaderNode, "click"); 321 | } 322 | 323 | var divs = getNode("nb_0").getElementsByTagName("div"); 324 | labels = new Array(); 325 | 326 | for (var i=0; i < divs.length; i++) { 327 | if (divs[i].className.indexOf("cs") != -1 && 328 | divs[i].id.indexOf(LABEL_PREFIX) == 0) { 329 | labels.push(divs[i].id.substring(LABEL_PREFIX.length)); 330 | } 331 | } 332 | 333 | var searchesDiv = getNode("nb_9"); 334 | if (searchesDiv != null) { 335 | var divs = searchesDiv.getElementsByTagName("div"); 336 | for (var i=0; i < divs.length; i++) { 337 | if (divs[i].className.indexOf("cs") != -1 && 338 | divs[i].id.indexOf(SAVED_SEARCH_PREFIX) == 0) { 339 | labels.push(divs[i].id.substring(SAVED_SEARCH_PREFIX.length)); 340 | } 341 | } 342 | } 343 | 344 | for (var specialLabel in SPECIAL_LABELS) { 345 | labels.push(specialLabel); 346 | } 347 | 348 | banner.show(); 349 | 350 | dispatchedActionTimeout = null; 351 | 352 | labelInput = makeLabelInput(); 353 | labelInput.addEventListener("keyup", updateLabelAction, false); 354 | // we want escape, clicks, etc. to cancel, which seems to be equivalent to the 355 | // field losing focus 356 | labelInput.addEventListener("blur", endLabelAction, false); 357 | } 358 | 359 | function beginSelectAction(){ 360 | labelInput = makeLabelInput(); 361 | labelInput.addEventListener("keyup", updateSelectAction, false); 362 | // we want escape, clicks, etc. to cancel, which seems to be equivalent to the 363 | // field losing focus 364 | labelInput.addEventListener("blur", endSelectAction, false); 365 | } 366 | 367 | function makeLabelInput(){ 368 | labelInput = newNode("input"); 369 | labelInput.type = "text"; 370 | labelInput.setAttribute("autocomplete", "off"); 371 | with (labelInput.style) { 372 | position = "fixed"; // We need to use fixed positioning since we have to ensure 373 | // that the input is not scrolled out of view (since 374 | // Gecko will scroll for us if it is). 375 | top = "0"; 376 | left = "-300px"; 377 | width = "200px"; 378 | height = "20px"; 379 | zIndex = "1000"; 380 | } 381 | 382 | unsafeWindow.document.body.appendChild(labelInput); 383 | labelInput.focus(); 384 | labelInput.value = ""; 385 | return labelInput; 386 | } 387 | 388 | function endAction() { 389 | banner.hide(); 390 | 391 | if (labelInput) { 392 | labelInput.parentNode.removeChild(labelInput); 393 | labelInput = null; 394 | } 395 | } 396 | 397 | function endLabelAction(){ 398 | if (labelsBoxWasClosed) { 399 | labelsBoxWasClosed = false; 400 | simulateClick(getNode("nt_0"), "click"); 401 | } 402 | 403 | endAction(); 404 | activeLabelAction = null; 405 | } 406 | 407 | function endSelectAction(){ 408 | endAction(); 409 | activeSelectAction = null; 410 | } 411 | 412 | function updateLabelAction(event) { 413 | // We've already dispatched the action, the user is just typing away 414 | if (dispatchedActionTimeout) { 415 | return; 416 | } 417 | 418 | selectedLabels = new Array(); 419 | 420 | // We need to skip the label shortcut that got us here 421 | var labelPrefix = labelInput.value.substring(1).toLowerCase(); 422 | 423 | banner.update(labelPrefix); 424 | 425 | if (labelPrefix.length == 0) { 426 | return; 427 | } 428 | 429 | for (var i=0; i < labels.length; i++) { 430 | if (labels[i].toLowerCase().indexOf(labelPrefix) == 0) { 431 | selectedLabels.push(labels[i]); 432 | } 433 | } 434 | 435 | if (event.keyCode == 13 || selectedLabels.length == 1) { 436 | // Tell the user what we picked 437 | banner.update(selectedLabels[0]); 438 | 439 | // Invoke the action straight away, but keep the banner up so the user can 440 | // see what was picked, and so that extra typing is caught. 441 | activeLabelAction(selectedLabels[0]); 442 | dispatchedActionTimeout = window.setTimeout( 443 | function () { 444 | endLabelAction(); 445 | }, 400); 446 | } 447 | } 448 | 449 | function updateSelectAction(event) { 450 | if (event.keyCode == 88 || event.keyCode == 16) return true; 451 | 452 | if (event.keyCode in SELECT_KEY_VALUES) { 453 | activeSelectAction(SELECT_KEY_VALUES[event.keyCode][0]); 454 | } else if (event.keyCode == 72) { 455 | activeSelectAction(); 456 | return true; 457 | } 458 | 459 | endSelectAction(); 460 | } 461 | 462 | function getLabelNode(labelName) { 463 | if (labelName in SPECIAL_LABELS) { 464 | return getNode(SPECIAL_LABELS[labelName]); 465 | } else { 466 | return getNode(LABEL_PREFIX + labelName) || 467 | getNode(SAVED_SEARCH_PREFIX + labelName); 468 | } 469 | } 470 | 471 | function runCommands(commands) { 472 | for (var i=0; i < commands.length; i++) { 473 | var command = commands[i]; 474 | 475 | // A one second pause between commands seems to be enough for LAN/broadband 476 | // connections 477 | setTimeout(getCommandClosure(commands[i]), 100 + 1000 * i); 478 | } 479 | } 480 | 481 | function getCommandClosure(command) { 482 | return function() { 483 | // We create a fake action menu, add our command to it, and then pretend to 484 | // select something from it. This is easier than dealing with the real 485 | // action menu, since some commands may be disabled and others may be 486 | // present as buttons instead 487 | var actionMenu = newNode("select"); 488 | var commandOption = newNode("option"); 489 | commandOption.value = command; 490 | commandOption.innerHTML = command; 491 | actionMenu.appendChild(commandOption); 492 | actionMenu.selectedIndex = 0; 493 | 494 | var actionMenuNode = getActionMenu(); 495 | 496 | if (actionMenuNode) { 497 | var onchangeHandler = actionMenuNode.onchange; 498 | 499 | onchangeHandler.apply(actionMenu, null); 500 | } else { 501 | GM_log("Not able to find a 'More Actions...' menu"); 502 | return; 503 | } 504 | } 505 | } 506 | 507 | function Banner() { 508 | this.backgroundNode = getNodeSet(); 509 | this.backgroundNode.style.background = "#000"; 510 | this.backgroundNode.style.MozOpacity = "0.70"; 511 | this.backgroundNode.style.zIndex = 100; 512 | for (var child = this.backgroundNode.firstChild; 513 | child; 514 | child = child.nextSibling) { 515 | child.style.visibility = "hidden"; 516 | } 517 | 518 | this.foregroundNode = getNodeSet(); 519 | this.foregroundNode.style.zIndex = 101; 520 | } 521 | 522 | function getNodeSet() { 523 | var boxNode = newNode("div"); 524 | boxNode.className = "banner"; 525 | with (boxNode.style) { 526 | display = "none"; 527 | position = "fixed"; 528 | left = "10%"; 529 | margin = "0 10% 0 10%"; 530 | width = "60%"; 531 | textAlign = "center"; 532 | MozBorderRadius = "10px"; 533 | padding = "10px"; 534 | color = "#fff"; 535 | } 536 | 537 | var messageNode = newNode("div"); 538 | with (messageNode.style) { 539 | fontSize = "24px"; 540 | fontWeight = "bold"; 541 | fontFamily = "Lucida Grande, Trebuchet MS, sans-serif"; 542 | margin = "0 0 10px 0"; 543 | } 544 | boxNode.appendChild(messageNode); 545 | 546 | var taglineNode = newNode("div"); 547 | with (taglineNode.style) { 548 | fontSize = "13px"; 549 | margin = "0"; 550 | } 551 | taglineNode.innerHTML = 'LabelSelector9000'; 552 | boxNode.appendChild(taglineNode); 553 | 554 | return boxNode; 555 | } 556 | 557 | Banner.prototype.hide = function() { 558 | this.backgroundNode.style.display = 559 | this.foregroundNode.style.display = "none"; 560 | } 561 | 562 | Banner.prototype.show = function(opt_isBottomAnchored) { 563 | this.update(""); 564 | document.body.appendChild(this.backgroundNode); 565 | document.body.appendChild(this.foregroundNode); 566 | 567 | this.backgroundNode.style.bottom = this.foregroundNode.style.bottom = 568 | opt_isBottomAnchored ? "10%" : ""; 569 | this.backgroundNode.style.top = this.foregroundNode.style.top = 570 | opt_isBottomAnchored ? "" : "50%"; 571 | 572 | this.backgroundNode.style.display = 573 | this.foregroundNode.style.display = "block"; 574 | } 575 | 576 | Banner.prototype.update = function(message) { 577 | if (message.length) { 578 | this.backgroundNode.firstChild.style.display = 579 | this.foregroundNode.firstChild.style.display = "inline"; 580 | } else { 581 | this.backgroundNode.firstChild.style.display = 582 | this.foregroundNode.firstChild.style.display = "none"; 583 | } 584 | this.backgroundNode.firstChild.innerHTML = 585 | this.foregroundNode.firstChild.innerHTML = message; 586 | } -------------------------------------------------------------------------------- /scripts/gmail-new-macros.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Gmail Macros (New) 3 | // @namespace http://persistent.info 4 | // @include http://mail.google.com/* 5 | // @include https://mail.google.com/* 6 | // ==/UserScript== 7 | 8 | window.addEventListener('load', function() { 9 | if (unsafeWindow.gmonkey) { 10 | unsafeWindow.gmonkey.load('1.0', init) 11 | } 12 | }, true); 13 | 14 | var UNREAD_COUNT_RE = /\s+\(\d+\)?$/; 15 | 16 | var MORE_ACTIONS_MENU_HEADER_CLASS = "QOD9Ec"; 17 | var MORE_ACTIONS_MENU_BODY_CLASS = "Sn99bd"; 18 | var MORE_ACTIONS_MENU_ITEM_CLASS = "SenFne"; 19 | 20 | var LABEL_ITEM_CLASS_NAME = "yyT6sf"; 21 | 22 | var MARK_AS_READ_ACTION = "1"; 23 | var ARCHIVE_ACTION = "7"; 24 | var ADD_LABEL_ACTION = "12"; 25 | var REMOVE_LABEL_ACTION = "13"; 26 | 27 | // Map from nav pane names to location names 28 | var SPECIAL_LABELS = { 29 | "Inbox": "inbox", 30 | "Starred": "starred", 31 | "Chats": "chats", 32 | "Sent Mail": "sent", 33 | "Drafts": "drafts", 34 | "All Mail": "all", 35 | "Spam": "spam", 36 | "Trash": "trash" 37 | } 38 | 39 | const LABEL_ACTIONS = { 40 | // g: go to label 41 | 71: { 42 | label: "Go to label", 43 | func: function(labelName) { 44 | if (labelName in SPECIAL_LABELS) { 45 | top.location.hash = "#" + SPECIAL_LABELS[labelName]; 46 | } else { 47 | top.location.hash = "#label/" + encodeURIComponent(labelName); 48 | } 49 | } 50 | }, 51 | // l: apply label 52 | 76: { 53 | label: "Apply label", 54 | func: function (labelName) { 55 | clickMoreActionsMenuItem(labelName, ADD_LABEL_ACTION); 56 | }, 57 | }, 58 | // b: remove label 59 | 66: { 60 | label: "Remove label", 61 | func: function (labelName) { 62 | clickMoreActionsMenuItem(labelName, REMOVE_LABEL_ACTION); 63 | } 64 | } 65 | }; 66 | 67 | const ACTIONS = { 68 | // d: archive and mark as read, i.e. discard 69 | 68: function() { 70 | clickMoreActionsMenuItem("Mark as read", MARK_AS_READ_ACTION); 71 | 72 | // Wait for the mark as read action to complete 73 | window.setTimeout(function() { 74 | var archiveButton = getFirstVisibleNode( 75 | evalXPath(".//button[@act='" + ARCHIVE_ACTION + "']", getDoc().body)); 76 | 77 | if (archiveButton) { 78 | simulateClick(archiveButton, "click"); 79 | } else { 80 | clickMoreActionsMenuItem("Archive", ARCHIVE_ACTION); 81 | } 82 | }, 500); 83 | }, 84 | // f: focus (only show unread and inbox messages) 85 | 70: function() { 86 | // Can only focus when in threadlist views 87 | if (gmail.getActiveViewType() != 'tl') return; 88 | 89 | var loc = top.location.hash; 90 | if (loc.length <= 1) return; 91 | loc = loc.substring(1); 92 | 93 | var search = getSearchForLocation(loc); 94 | 95 | if (search === null) { 96 | return; 97 | } 98 | 99 | search += " {in:inbox is:starred is:unread} -is:muted"; 100 | 101 | top.location.hash = "#search/" + search; 102 | } 103 | }; 104 | 105 | var LOC_TO_SEARCH = { 106 | "inbox": "in:inbox", 107 | "starred": "is:starred", 108 | "chats": "is:chat", 109 | "sent": "from:me", 110 | "drafts": "is:draft", 111 | "all": "", 112 | "spam": "in:spam", 113 | "trash": "in:trash" 114 | }; 115 | 116 | var LABEL_PREFIX = "label/"; 117 | 118 | function getSearchForLocation(loc) { 119 | if (loc in LOC_TO_SEARCH) { 120 | return LOC_TO_SEARCH[loc]; 121 | } 122 | 123 | if (loc.indexOf(LABEL_PREFIX) == 0) { 124 | var labelName = loc.substring(LABEL_PREFIX.length); 125 | 126 | // Normalize spaces to dashes, since that's what Gmail wants for searches 127 | labelName = labelName.replace(/\+/g, "-"); 128 | 129 | return "label:" + labelName; 130 | } 131 | 132 | return null; 133 | } 134 | 135 | // TODO(mihaip): too many global variables, use objects 136 | var banner = null; 137 | var gmail = null; 138 | 139 | var labelInput = null; 140 | var activeLabelAction = null; 141 | var lastPrefix = null; 142 | var selLabelIndex = null; 143 | 144 | function getDoc() { 145 | return gmail.getNavPaneElement().ownerDocument; 146 | } 147 | 148 | function newNode(tagName) { 149 | return getDoc().createElement(tagName); 150 | } 151 | 152 | function getNode(id) { 153 | return getDoc().getElementById(id); 154 | } 155 | 156 | function getFirstVisibleNode(nodes) { 157 | for (var i = 0, node; node = nodes[i]; i++) { 158 | if (node.offsetHeight) return node; 159 | } 160 | 161 | return null; 162 | } 163 | 164 | function simulateClick(node, eventType) { 165 | var event = node.ownerDocument.createEvent("MouseEvents"); 166 | event.initMouseEvent(eventType, 167 | true, // can bubble 168 | true, // cancellable 169 | node.ownerDocument.defaultView, 170 | 1, // clicks 171 | 50, 50, // screen coordinates 172 | 50, 50, // client coordinates 173 | false, false, false, false, // control/alt/shift/meta 174 | 0, // button, 175 | node); 176 | 177 | node.dispatchEvent(event); 178 | } 179 | 180 | function clickMoreActionsMenuItem(menuItemText, menuItemAction) { 181 | var moreActionsMenu = getFirstVisibleNode(getNodesByTagNameAndClass( 182 | getDoc().body, "div", MORE_ACTIONS_MENU_HEADER_CLASS)); 183 | 184 | if (!moreActionsMenu) { 185 | alert("Couldn't find the menu header node"); 186 | return; 187 | } 188 | 189 | simulateClick(moreActionsMenu, "mousedown"); 190 | 191 | var menuBodyNodes = getNodesByTagNameAndClass( 192 | getDoc().body, "div", MORE_ACTIONS_MENU_BODY_CLASS); 193 | var menuBodyNode = getFirstVisibleNode(menuBodyNodes); 194 | 195 | if (!menuBodyNode) { 196 | alert("Couldn't find the menu body node"); 197 | return; 198 | } 199 | 200 | var menuItemNodes = getNodesByTagNameAndClass( 201 | menuBodyNode, "div", MORE_ACTIONS_MENU_ITEM_CLASS); 202 | 203 | for (var i = 0; menuItemNode = menuItemNodes[i]; i++) { 204 | if (menuItemNode.textContent == menuItemText && 205 | menuItemNode.getAttribute("act") == menuItemAction) { 206 | simulateClick(menuItemNode, "mouseup"); 207 | return; 208 | } 209 | } 210 | 211 | alert("Couldn't find the menu item node '" + menuItemText + "'"); 212 | } 213 | 214 | function init(g) { 215 | gmail = g; 216 | banner = new Banner(); 217 | 218 | getDoc().defaultView.addEventListener('keydown', keyHandler, false); 219 | } 220 | 221 | function keyHandler(event) { 222 | // Apparently we still see Firefox shortcuts like control-T for a new tab - 223 | // checking for modifiers lets us ignore those 224 | if (event.altKey || event.ctrlKey || event.metaKey) return; 225 | 226 | // We also don't want to interfere with regular user typing 227 | if (event.target && event.target.nodeName) { 228 | var targetNodeName = event.target.nodeName.toLowerCase(); 229 | if (targetNodeName == "textarea" || 230 | (targetNodeName == "input" && event.target.type && 231 | (event.target.type.toLowerCase() == "text" || 232 | event.target.type.toLowerCase() == "file"))) { 233 | return; 234 | } 235 | } 236 | 237 | var k = event.keyCode; 238 | 239 | if (k in LABEL_ACTIONS) { 240 | if (activeLabelAction) { 241 | endLabelAction(); 242 | return 243 | } else { 244 | activeLabelAction = LABEL_ACTIONS[k]; 245 | beginLabelAction(); 246 | return; 247 | } 248 | } 249 | 250 | if (k in ACTIONS) { 251 | ACTIONS[k](); 252 | return; 253 | } 254 | 255 | return; 256 | } 257 | 258 | function beginLabelAction() { 259 | // TODO(mihaip): make sure the labels nav pane is open 260 | 261 | banner.show(); 262 | banner.setFooter(activeLabelAction.label); 263 | 264 | lastPrefix = null; 265 | selLabelIndex = 0; 266 | dispatchedActionTimeout = null; 267 | 268 | labelInput = makeLabelInput(); 269 | labelInput.addEventListener("keyup", updateLabelAction, false); 270 | // we want escape, clicks, etc. to cancel, which seems to be equivalent to the 271 | // field losing focus 272 | labelInput.addEventListener("blur", endLabelAction, false); 273 | } 274 | 275 | function makeLabelInput() { 276 | labelInput = newNode("input"); 277 | labelInput.type = "text"; 278 | labelInput.setAttribute("autocomplete", "off"); 279 | with (labelInput.style) { 280 | position = "fixed"; // We need to use fixed positioning since we have to ensure 281 | // that the input is not scrolled out of view (since 282 | // Gecko will scroll for us if it is). 283 | top = "0"; 284 | left = "-300px"; 285 | width = "200px"; 286 | height = "20px"; 287 | zIndex = "1000"; 288 | } 289 | 290 | getDoc().body.appendChild(labelInput); 291 | labelInput.focus(); 292 | labelInput.value = ""; 293 | 294 | return labelInput; 295 | } 296 | 297 | function endLabelAction() { 298 | if (dispatchedActionTimeout) return; 299 | 300 | // TODO(mihaip): re-close label box if necessary 301 | 302 | banner.hide(); 303 | 304 | if (labelInput) { 305 | labelInput.parentNode.removeChild(labelInput); 306 | labelInput = null; 307 | } 308 | 309 | activeLabelAction = null; 310 | } 311 | 312 | function updateLabelAction(event) { 313 | // We've already dispatched the action, the user is just typing away 314 | if (dispatchedActionTimeout) return; 315 | 316 | var labels = getLabels(); 317 | var selectedLabels = []; 318 | 319 | // We need to skip the label shortcut that got us here 320 | var labelPrefix = labelInput.value.substring(1).toLowerCase(); 321 | 322 | // We always want to reset the cursor position to the end of the text 323 | // field, since some of the keys that we support (arrows) would 324 | // otherwise change it 325 | labelInput.selectionStart = labelInput.selectionEnd = labelPrefix.length + 1; 326 | 327 | if (labelPrefix.length == 0) { 328 | banner.update(""); 329 | return; 330 | } 331 | 332 | for (var i = 0; i < labels.length; i++) { 333 | label = labels[i]; 334 | 335 | if (label.toLowerCase().indexOf(labelPrefix) == 0) { 336 | selectedLabels.push(label); 337 | } 338 | } 339 | 340 | if (labelPrefix != lastPrefix) { 341 | lastPrefix = labelPrefix; 342 | selLabelIndex = 0; 343 | } 344 | 345 | if (selectedLabels.length == 0) { 346 | banner.update(labelPrefix); 347 | return; 348 | } 349 | 350 | if (event.keyCode == 13 || selectedLabels.length == 1) { 351 | var selectedLabelName = selectedLabels[selLabelIndex]; 352 | 353 | // Tell the user what we picked 354 | banner.update(selectedLabelName); 355 | 356 | // Invoke the action straight away, but keep the banner up so the user can 357 | // see what was picked, and so that extra typing is caught. 358 | activeLabelAction.func(selectedLabelName); 359 | dispatchedActionTimeout = window.setTimeout(function() { 360 | dispatchedActionTimeout = null; 361 | endLabelAction() 362 | }, 500); 363 | return; 364 | } else if (event.keyCode == 40) { // down 365 | selLabelIndex = (selLabelIndex + 1) % selectedLabels.length; 366 | } else if (event.keyCode == 38) { // up 367 | selLabelIndex = (selLabelIndex + selectedLabels.length - 1) % 368 | selectedLabels.length; 369 | } 370 | 371 | var selectedLabelName = selectedLabels[selLabelIndex]; 372 | 373 | var highlightedSelectedLabelName = selectedLabelName.replace( 374 | new RegExp("(" + labelPrefix + ")", "i"), "$1"); 375 | var labelPosition = " (" + 376 | (selLabelIndex + 1) + "/" + selectedLabels.length + ")"; 377 | 378 | banner.update(highlightedSelectedLabelName + labelPosition); 379 | } 380 | 381 | function getLabels() { 382 | var navPaneNode = gmail.getNavPaneElement(); 383 | 384 | var labelNodes = getNodesByTagNameAndClass( 385 | navPaneNode, "div", LABEL_ITEM_CLASS_NAME); 386 | 387 | var labels = []; 388 | 389 | for (var i = 0, labelNode; labelNode = labelNodes[i]; i++) { 390 | var labelName = labelNode.textContent.replace(UNREAD_COUNT_RE, ""); 391 | 392 | labels.push(labelName); 393 | } 394 | 395 | return labels; 396 | } 397 | 398 | function evalXPath(expression, rootNode) { 399 | try { 400 | var xpathIterator = rootNode.ownerDocument.evaluate( 401 | expression, 402 | rootNode, 403 | null, // no namespace resolver 404 | XPathResult.ORDERED_NODE_ITERATOR_TYPE, 405 | null); // no existing results 406 | } catch (err) { 407 | GM_log("Error when evaluating XPath expression '" + expression + "'" + 408 | ": " + err); 409 | return null; 410 | } 411 | var results = []; 412 | 413 | // Convert result to JS array 414 | for (var xpathNode = xpathIterator.iterateNext(); 415 | xpathNode; 416 | xpathNode = xpathIterator.iterateNext()) { 417 | results.push(xpathNode); 418 | } 419 | 420 | return results; 421 | } 422 | 423 | function getNodesByTagNameAndClass(rootNode, tagName, className) { 424 | var expression = 425 | ".//" + tagName + 426 | "[contains(concat(' ', @class, ' '), ' " + className + " ')]"; 427 | 428 | return evalXPath(expression, rootNode); 429 | } 430 | 431 | function Banner() { 432 | function getNodeSet() { 433 | var boxNode = newNode("div"); 434 | boxNode.className = "banner"; 435 | with (boxNode.style) { 436 | display = "none"; 437 | position = "fixed"; 438 | left = "10%"; 439 | margin = "0 10% 0 10%"; 440 | width = "60%"; 441 | textAlign = "center"; 442 | MozBorderRadius = "10px"; 443 | padding = "10px"; 444 | color = "#fff"; 445 | } 446 | 447 | var messageNode = newNode("div"); 448 | with (messageNode.style) { 449 | fontSize = "24px"; 450 | fontWeight = "bold"; 451 | fontFamily = "Lucida Grande, Trebuchet MS, sans-serif"; 452 | margin = "0 0 10px 0"; 453 | } 454 | boxNode.appendChild(messageNode); 455 | 456 | var taglineNode = newNode("div"); 457 | with (taglineNode.style) { 458 | fontSize = "13px"; 459 | margin = "0"; 460 | position = "absolute"; 461 | right = "0.2em"; 462 | bottom = "0"; 463 | MozOpacity = "0.5"; 464 | } 465 | taglineNode.innerHTML = 'LabelSelector9001'; 466 | boxNode.appendChild(taglineNode); 467 | 468 | var footerNode = newNode("div"); 469 | with (footerNode.style) { 470 | fontSize = "13px"; 471 | } 472 | boxNode.appendChild(footerNode); 473 | 474 | return boxNode; 475 | } 476 | 477 | this.backgroundNode = getNodeSet(); 478 | this.backgroundNode.style.background = "#000"; 479 | this.backgroundNode.style.MozOpacity = "0.70"; 480 | this.backgroundNode.style.zIndex = 100; 481 | for (var child = this.backgroundNode.firstChild; 482 | child; 483 | child = child.nextSibling) { 484 | child.style.visibility = "hidden"; 485 | } 486 | 487 | this.foregroundNode = getNodeSet(); 488 | this.foregroundNode.style.zIndex = 101; 489 | } 490 | 491 | Banner.prototype.hide = function() { 492 | this.backgroundNode.style.display = 493 | this.foregroundNode.style.display = "none"; 494 | } 495 | 496 | Banner.prototype.show = function(opt_isBottomAnchored) { 497 | this.update(""); 498 | getDoc().body.appendChild(this.backgroundNode); 499 | getDoc().body.appendChild(this.foregroundNode); 500 | 501 | this.backgroundNode.style.bottom = this.foregroundNode.style.bottom = 502 | opt_isBottomAnchored ? "10%" : ""; 503 | this.backgroundNode.style.top = this.foregroundNode.style.top = 504 | opt_isBottomAnchored ? "" : "50%"; 505 | 506 | this.backgroundNode.style.display = 507 | this.foregroundNode.style.display = "block"; 508 | } 509 | 510 | Banner.prototype.update = function(message) { 511 | if (message.length) { 512 | this.backgroundNode.firstChild.style.display = 513 | this.foregroundNode.firstChild.style.display = "inline"; 514 | } else { 515 | this.backgroundNode.firstChild.style.display = 516 | this.foregroundNode.firstChild.style.display = "none"; 517 | } 518 | this.backgroundNode.firstChild.innerHTML = 519 | this.foregroundNode.firstChild.innerHTML = message; 520 | } 521 | 522 | Banner.prototype.setFooter = function(text) { 523 | this.backgroundNode.lastChild.innerHTML = 524 | this.foregroundNode.lastChild.innerHTML = text; 525 | } 526 | -------------------------------------------------------------------------------- /scripts/gmail-reader.user.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Mihai Parparita. All Rights Reserved. 2 | 3 | // ==UserScript== 4 | // @name Gmail + Google Reader 5 | // @namespace http://persistent.info/ 6 | // @description Embeds Google Reader into Gmail by adding a "Feeds" link 7 | // @include http://mail.google.com/* 8 | // @include https://mail.google.com/* 9 | // @include http://www.google.com/reader/* 10 | // ==/UserScript== 11 | 12 | var readerFrameNode = null; 13 | var feedsContainerNode = null; 14 | var hiddenNodes = []; 15 | 16 | const GMAIL_STYLES = [ 17 | "#reader-frame {", 18 | " width: 100%;", 19 | " border: 0;", 20 | "}", 21 | 22 | ".reader-embedded #ft {", 23 | "display: none", 24 | "}", 25 | 26 | // Make currently selected item appear normal 27 | ".reader-embedded table.cv * {", 28 | " background: #fff;", 29 | " font-weight: normal", 30 | "}", 31 | 32 | // Make the feeds link appear selected 33 | ".reader-embedded #feeds-container {", 34 | " background: #c3d9ff;", 35 | " -moz-border-radius: 5px 0 0 5px;", 36 | " font-weight: bold;", 37 | " color: #00c;", 38 | "}", 39 | ].join("\n"); 40 | 41 | const READER_STYLES = [ 42 | "body {", 43 | " background: #fff", 44 | "}", 45 | 46 | "#nav,", 47 | "#logo-container,", 48 | "#global-info,", 49 | "#viewer-header {", 50 | " display: none !important;", 51 | "}", 52 | "", 53 | "#main {", 54 | " margin-top: 0;", 55 | "}", 56 | "", 57 | "#chrome {", 58 | " margin-left: 0;", 59 | "}" 60 | ].join("\n"); 61 | 62 | const READER_UNREAD_COUNT_URL = 63 | "http://www.google.com/reader/api/0/unread-count?" + 64 | "all=true&output=json&client=gm"; 65 | 66 | const READER_LIST_VIEW_URL = 67 | "http://www.google.com/reader/view/user/-/state/com.google/reading-list?" + 68 | "gmail-embed=true&view=list"; 69 | 70 | if (document.location.hostname == "mail.google.com") { 71 | injectGmail(); 72 | } else if (document.location.hostname == "www.google.com" && 73 | document.location.search.indexOf("gmail-embed") != -1) { 74 | injectReader(); 75 | } 76 | 77 | function injectGmail() { 78 | if (!getNode("nds") || !getNode("ds_inbox")) return; 79 | 80 | GM_addStyle(GMAIL_STYLES); 81 | 82 | var feedsNode = newNode("span"); 83 | feedsNode.className = "lk"; 84 | feedsNode.innerHTML = 85 | 'Feeds ' + 86 | ''; 87 | feedsNode.onclick = showReaderFrame; 88 | 89 | feedsContainerNode = newNode("div"); 90 | feedsContainerNode.className = "nl"; 91 | feedsContainerNode.id = "feeds-container"; 92 | feedsContainerNode.appendChild(feedsNode); 93 | 94 | var navNode = getNode("nds"); 95 | 96 | navNode.insertBefore(feedsContainerNode, navNode.childNodes[2]); 97 | 98 | window.addEventListener("resize", resizeReaderFrame, false); 99 | 100 | updateUnreadCount(); 101 | 102 | window.setInterval(updateUnreadCount, 5 * 60 * 1000); 103 | 104 | window.setInterval(checkFeedsParent, 1000); 105 | } 106 | 107 | function checkFeedsParent() { 108 | var navNode = getNode("nds"); 109 | 110 | if (feedsContainerNode.parentNode != navNode) { 111 | navNode.insertBefore(feedsContainerNode, navNode.childNodes[2]); 112 | } 113 | } 114 | 115 | function updateUnreadCount() { 116 | var unreadCountNode = getNode("reader-unread-count"); 117 | 118 | if (!unreadCountNode) return; 119 | 120 | GM_xmlhttpRequest({ 121 | method: "GET", 122 | url: READER_UNREAD_COUNT_URL, 123 | onload: function(details) { 124 | var data = eval("(" + details.responseText + ")"); 125 | var isUnread = false; 126 | 127 | for (var i = 0, unreadCountPair; 128 | unreadCountPair = data.unreadcounts[i]; 129 | i++) { 130 | if (unreadCountPair.id.indexOf("reading-list") != -1) { 131 | var count = unreadCountPair.count; 132 | if (count == 0) break; 133 | 134 | unreadCountNode.innerHTML = 135 | " (" + count + (count == data.max ? "+" : "") + ") "; 136 | isUnread = true; 137 | break; 138 | } 139 | } 140 | 141 | if (!isUnread) { 142 | unreadCountNode.innerHTML = ""; 143 | } 144 | } 145 | }); 146 | } 147 | 148 | function resizeReaderFrame() { 149 | if (!readerFrameNode) return; 150 | 151 | readerFrameNode.style.height = 152 | (window.innerHeight - readerFrameNode.offsetTop) + "px"; 153 | } 154 | 155 | function showReaderFrame(event) { 156 | var container = getNode("co"); 157 | 158 | addClass(document.body, "reader-embedded"); 159 | 160 | hiddenNodes = []; 161 | 162 | for (var i = container.firstChild; i; i = i.nextSibling) { 163 | hiddenNodes.push(i); 164 | i.style.display = "none"; 165 | } 166 | 167 | readerFrameNode = newNode("iframe"); 168 | readerFrameNode.src = READER_LIST_VIEW_URL; 169 | readerFrameNode.id = "reader-frame"; 170 | 171 | container.appendChild(readerFrameNode); 172 | 173 | container.parentNode.style.paddingRight = "0"; 174 | container.parentNode.style.paddingBottom = "0"; 175 | 176 | resizeReaderFrame(); 177 | 178 | // Make clicks outside the content area hide it 179 | getNode("nav").addEventListener("click", hideReaderFrame, false); 180 | 181 | // Since we're in a child of the "nav" element, the above handler will get 182 | // triggered immediately unless we stop this event from propagating 183 | event.stopPropagation(); 184 | 185 | return false; 186 | } 187 | 188 | function hideReaderFrame() { 189 | var container = getNode("co"); 190 | 191 | container.removeChild(readerFrameNode); 192 | readerFrameNode = null; 193 | 194 | for (var i=0; i < hiddenNodes.length; i++) { 195 | hiddenNodes[i].style.display = ""; 196 | } 197 | getNode("nav").removeEventListener("click", hideReaderFrame, false); 198 | 199 | removeClass(document.body, "reader-embedded"); 200 | 201 | container.parentNode.style.paddingRight = "1ex"; 202 | container.parentNode.style.paddingBottom = "1ex"; 203 | 204 | return true; 205 | } 206 | 207 | function injectReader() { 208 | GM_addStyle(READER_STYLES); 209 | } 210 | 211 | // Shorthand 212 | function newNode(type) {return unsafeWindow.document.createElement(type);} 213 | function newText(text) {return unsafeWindow.document.createTextNode(text);} 214 | function getNode(id) {return unsafeWindow.document.getElementById(id);} 215 | 216 | function hasClass(node, className) { 217 | return className in getClassMap(node); 218 | } 219 | 220 | function addClass(node, className) { 221 | if (hasClass(node, className)) return; 222 | 223 | node.className += " " + className; 224 | } 225 | 226 | function removeClass(node, className) { 227 | var classMap = getClassMap(node); 228 | 229 | if (!(className in classMap)) return; 230 | 231 | delete classMap[className]; 232 | var newClassList = []; 233 | 234 | for (var className in classMap) { 235 | newClassList.push(className); 236 | } 237 | 238 | node.className = newClassList.join(" "); 239 | } 240 | 241 | function getClassMap(node) { 242 | var classMap = {}; 243 | var classNames = node.className.split(/\s+/); 244 | 245 | for (var i = 0; i < classNames.length; i++) { 246 | classMap[classNames[i]] = true; 247 | } 248 | 249 | return classMap; 250 | } -------------------------------------------------------------------------------- /scripts/gmail-saved-searches.user.js: -------------------------------------------------------------------------------- 1 | // Copyright 2006 Mihai Parparita. All Rights Reserved. 2 | 3 | // ==UserScript== 4 | // @name Gmail Saved Searches 5 | // @namespace http://www.google.com/~mihaip 6 | // @description Adds saved and recent seaches. 7 | // @include http://mail.google.com/* 8 | // @include https://mail.google.com/* 9 | 10 | // ==/UserScript== 11 | 12 | // Utility functions 13 | function getObjectMethodClosure(object, method) { 14 | return function() { 15 | return object[method].apply(object, arguments); 16 | } 17 | } 18 | 19 | function getDateString(date) { 20 | return date.getFullYear() + "/" + 21 | (date.getMonth() + 1) + "/" + 22 | date.getDate(); 23 | } 24 | 25 | function getCookie(name) { 26 | name = getNamespacedName(name); 27 | 28 | if (GM_getValue(name)) { 29 | return GM_getValue(name); 30 | } 31 | 32 | var re = new RegExp(name + "=([^;]+)"); 33 | var value = re.exec(document.cookie); 34 | return (value != null) ? unescape(value[1]) : null; 35 | } 36 | 37 | function setCookie(name, value) { 38 | name = getNamespacedName(name); 39 | 40 | GM_setValue(name, value); 41 | } 42 | 43 | var email = null; 44 | 45 | function getNamespacedName(name) { 46 | if (email == null) { 47 | var settingsNode = getNode("prf_g"); 48 | var emailNode = settingsNode; 49 | 50 | do { 51 | emailNode = emailNode.previousSibling; 52 | } while (!emailNode.innerHTML || emailNode.innerHTML.indexOf("@") == -1); 53 | 54 | email = encodeURIComponent(emailNode.innerHTML); 55 | } 56 | 57 | return email + "-" + name; 58 | } 59 | 60 | // Shorthand 61 | var newNode = getObjectMethodClosure(unsafeWindow.document, "createElement"); 62 | var newText = getObjectMethodClosure(unsafeWindow.document, "createTextNode"); 63 | var getNode = getObjectMethodClosure(unsafeWindow.document, "getElementById"); 64 | 65 | // Contants 66 | 67 | const RULES = new Array( 68 | // Block in sidebar 69 | ".searchesBlock {-moz-border-radius: 5px; background: #fad163; margin: 10px 7px 0 0; padding: 3px;}", 70 | ".refreshButton {display: block; cursor: pointer; float: right; margin-top: -2px;}", 71 | ".searchesBlockList {padding-left: 5px; background: white; overflow: hidden; display: none;}", 72 | ".searchesBlockList h5 {margin: 0 0 1px -4px; color: #999; font-size: 12px; font-weight: bold;}", 73 | ".listItem {color: #ca9c22;}", 74 | ".editLink {background: white; text-align: right; color: #ca9c22; padding: 2px 0px 5px 0;}", 75 | 76 | // Edit page 77 | ".searchesContainer {-moz-border-radius: 10px; background: #fad163; padding: 10px;}", 78 | ".innerContainer {background: #fff7d7; text-align: left; padding: 10px;}", 79 | ".buttonContainer {text-align: center;}", 80 | ".searchesList {width: 100%;}", 81 | ".searchesList th {text-align: left; font-size: 90%;}", 82 | ".searchesList td {padding: 10px 0 10px 0; vertical-align: bottom;}", 83 | ".searchesList td.divider {background: #fad163; height: 3px; padding: 0;}", 84 | ".editItem {font-size: 80%;}", 85 | ".labelCell {width: 210px;}", 86 | ".labelCell input {width: 200px;}", 87 | ".cancelButton {margin-right: 5px;}", 88 | ".editCell {}", 89 | ".editCell input {width: 100%}", 90 | ".saveButton {margin-left: 5px; font-weight: bold;}" 91 | ); 92 | 93 | const UP_TRIANGLE_IMAGE = "data:image/gif;base64,R0lGODlhCwALAKEAAP///wAAAA4" + 94 | "ODv///yH5BAEAAAMALAAAAAALAAsAAAITnI+pGmsBF5xp2mPzmCJHB4ZJAQA7"; 95 | 96 | const DEFAULT_SEARCHES = { 97 | "to:me {in:inbox is:unread}": "TODO", 98 | "has:attachment": "Attachments", 99 | "after:oneweekago": "Last Week", 100 | "label:^g is:unread": "Muted but unread", 101 | "++ {is:unread in:inbox is:starred}": "Add Focus" 102 | }; 103 | 104 | const SEARCHES_COOKIE = "PersistentSearches"; 105 | const SEARCHES_COLLAPSED_COOKIE = "PersistentSearchesCollapsedCookie"; 106 | 107 | const ONE_DAY = 24 * 60 * 60 * 1000; 108 | 109 | const SAVED_SEARCH_PREFIX = "savedsearch_"; 110 | const KEY_PREFIX = "gmailss"; 111 | const POSITION = KEY_PREFIX + "pos"; 112 | const RECENT_NUM = KEY_PREFIX + "numrecent"; 113 | const RECENT_POS = KEY_PREFIX + "recentpos"; 114 | const NEW_VALUE = "NEW"; 115 | 116 | // Globals 117 | var styleSheet = null; 118 | 119 | var searches = new Array(); 120 | var recentSearches = new Array(); 121 | var searchesBlock = null; 122 | var searchesBlockHeader = null; 123 | var searchesBlockList = null; 124 | var recentSearchesBlockList = null; 125 | var editLink = null; 126 | 127 | var hiddenNodes = null; 128 | var searchesContainer = null; 129 | var searchesList = null; 130 | 131 | function initializePersistentSearches() { 132 | var labelsBlock = getNode("nb_0"); 133 | 134 | if (!labelsBlock) { 135 | return; 136 | } 137 | 138 | searchesBlock = newNode("div"); 139 | searchesBlock.id = "nb_9"; 140 | searchesBlock.className = "searchesBlock"; 141 | 142 | // header 143 | searchesBlockHeader = newNode("div"); 144 | searchesBlockHeader.className = "s h"; 145 | searchesBlock.appendChild(searchesBlockHeader); 146 | 147 | searchesBlockHeader.triangleImage = newNode("img"); 148 | searchesBlockHeader.triangleImage.src = "/mail/images/opentriangle.gif"; 149 | searchesBlockHeader.triangleImage.width = 11; 150 | searchesBlockHeader.triangleImage.height = 11; 151 | searchesBlockHeader.triangleImage.addEventListener("click", 152 | togglePersistentSearches, 153 | false); 154 | searchesBlockHeader.appendChild(searchesBlockHeader.triangleImage); 155 | 156 | var searchesText = newNode("span"); 157 | searchesText.appendChild(newText(" Searches")); 158 | searchesText.addEventListener("click", 159 | togglePersistentSearches, 160 | false); 161 | searchesBlockHeader.appendChild(searchesText); 162 | 163 | // recent searches list 164 | recentSearchesBlockList = newNode("div"); 165 | recentSearchesBlockList.className = "searchesBlockList"; 166 | recentSearchesBlockList.appendChild(newNode("h5")).appendChild(newText("Recent")); 167 | 168 | // saved searches list 169 | searchesBlockList = newNode("div"); 170 | searchesBlockList.className = "searchesBlockList"; 171 | searchesBlockList.appendChild(newNode("h5")).appendChild(newText("Saved")); 172 | 173 | var numrecent = GM_getValue(RECENT_NUM); 174 | if (!numrecent) { 175 | GM_setValue(RECENT_NUM, 5); 176 | } 177 | 178 | var recentpos = GM_getValue(RECENT_POS); 179 | if (!recentpos) { 180 | recentpos = "bottom"; 181 | GM_setValue(RECENT_POS, recentpos); 182 | } 183 | 184 | if (recentpos == "top") { 185 | searchesBlock.appendChild(recentSearchesBlockList); 186 | searchesBlock.appendChild(searchesBlockList); 187 | } else { 188 | searchesBlock.appendChild(searchesBlockList); 189 | searchesBlock.appendChild(recentSearchesBlockList); 190 | } 191 | 192 | editLink = newNode("div"); 193 | editLink.appendChild(newText("Edit searches")); 194 | editLink.className = "lk cs editLink"; 195 | editLink.addEventListener("click", editPersistentSearches, false); 196 | searchesBlock.appendChild(editLink); 197 | 198 | if (getCookie(SEARCHES_COOKIE) != null) { 199 | restorePersistentSearches(); 200 | } else { 201 | for (var query in DEFAULT_SEARCHES) { 202 | addPersistentSearch(new PersistentSearch(query, DEFAULT_SEARCHES[query])); 203 | } 204 | } 205 | 206 | checkCurrentQuery(); 207 | 208 | insertSearchesBlock(); 209 | 210 | if (getCookie(SEARCHES_COLLAPSED_COOKIE) == "1") { 211 | togglePersistentSearches(); 212 | } 213 | 214 | checkSearchesBlockParent(); 215 | } 216 | 217 | function rearrangeSeachesBlock() { 218 | if (GM_getValue(RECENT_POS) == "top") { 219 | recentSearchesBlockList.parentNode.removeChild(recentSearchesBlockList); 220 | searchesBlock.insertBefore(recentSearchesBlockList, searchesBlockList); 221 | } else { 222 | searchesBlockList.parentNode.removeChild(searchesBlockList); 223 | searchesBlock.insertBefore(searchesBlockList, recentSearchesBlockList); 224 | } 225 | } 226 | 227 | function checkCurrentQuery() { 228 | var currentQuery = getCurrentQuery(); 229 | if (currentQuery) { 230 | var found = false; 231 | 232 | var recentSearch = new PersistentSearch(currentQuery, 233 | currentQuery, 234 | PersistentSearch.RECENT_TYPE); 235 | 236 | for (var i=0; i < searches.length; i++) { 237 | if (searches[i].equals(recentSearch)) { 238 | found = true; 239 | break; 240 | } 241 | } 242 | 243 | if (!found) { 244 | for (var i=0; i < recentSearches.length; i++) { 245 | if (recentSearches[i].equals(recentSearch)) { 246 | found = true; 247 | break; 248 | } 249 | } 250 | 251 | if (!found) { 252 | addRecentSearch(recentSearch); 253 | } 254 | } 255 | } 256 | } 257 | 258 | function getCurrentQuery() { 259 | var queryString = window.location.search; 260 | var split = queryString.split("&"); 261 | 262 | var params = {}; 263 | 264 | for (var i=0; i < split.length; i++) { 265 | var pair = split[i].split("="); 266 | 267 | params[pair[0]] = pair[1]; 268 | } 269 | 270 | if (params["search"] && params["search"] == "query") { 271 | return decodeURIComponent(params["q"]); 272 | } else { 273 | return null; 274 | } 275 | } 276 | 277 | function insertSearchesBlock() { 278 | var labelsBlock = getNode(GM_getValue(POSITION, "nb_2")); 279 | 280 | if (!labelsBlock) { 281 | labelsBlock = getNode("nb_0"); 282 | if (!labelsBlock) { 283 | return; 284 | } 285 | } 286 | 287 | getNode("nav").insertBefore(searchesBlock, labelsBlock); 288 | } 289 | 290 | // For some reason, when naving back to the Inbox after viewing a message, we seem 291 | // to get removed from the nav section, so we have to add ourselves back. This only 292 | // happens if we're a child of the "nav" div, and nowhere else (but that's the place 293 | // where we're supposed to go, so we have no choice) 294 | function checkSearchesBlockParent() { 295 | if (searchesBlock.parentNode != getNode("nav")) { 296 | insertSearchesBlock(); 297 | } 298 | 299 | window.setTimeout(checkSearchesBlockParent, 200); 300 | } 301 | 302 | function restorePersistentSearches() { 303 | var serializedSearches = getCookie(SEARCHES_COOKIE).split("|"); 304 | 305 | for (var i=0; i < serializedSearches.length; i++) { 306 | var search = PersistentSearch.prototype.fromString(serializedSearches[i]); 307 | 308 | if (search.type == PersistentSearch.RECENT_TYPE) { 309 | addRecentSearch(search); 310 | } else { 311 | addPersistentSearch(search); 312 | } 313 | } 314 | } 315 | 316 | function saveSearches() { 317 | var serializedSearches = new Array(); 318 | 319 | for (var i=0; i < searches.length; i++) { 320 | serializedSearches.push(searches[i].toString()); 321 | } 322 | for (var i=0; i < recentSearches.length; i++) { 323 | serializedSearches.push(recentSearches[i].toString()); 324 | } 325 | 326 | setCookie(SEARCHES_COOKIE, serializedSearches.join("|")); 327 | } 328 | 329 | function clearPersistentSearches() { 330 | for (var i=0; i < searches.length; i++) { 331 | var item = searches[i].getListItem(); 332 | if (item.parentNode) { 333 | item.parentNode.removeChild(item); 334 | } 335 | } 336 | searches = new Array(); 337 | } 338 | 339 | function addPersistentSearch(search) { 340 | searches.push(search); 341 | 342 | searchesBlockList.appendChild(search.getListItem()); 343 | searchesBlockList.style.display = "block"; 344 | 345 | saveSearches(); 346 | } 347 | 348 | function removeRecentSearch(search) { 349 | var removedSearchItem = search.getListItem(); 350 | if (removedSearchItem.parentNode) { 351 | removedSearchItem.parentNode.removeChild(removedSearchItem); 352 | } 353 | } 354 | 355 | function addRecentSearch(search) { 356 | while (recentSearches.length >= GM_getValue(RECENT_NUM)) { 357 | removeRecentSearch(recentSearches.shift()); 358 | } 359 | recentSearches.push(search); 360 | 361 | recentSearchesBlockList.appendChild(search.getListItem()); 362 | recentSearchesBlockList.style.display = "block"; 363 | saveSearches(); 364 | } 365 | 366 | function limitRecentSearch() { 367 | while (recentSearches.length > GM_getValue(RECENT_NUM)) { 368 | removeRecentSearch(recentSearches.shift()); 369 | } 370 | } 371 | 372 | function editPersistentSearches(event) { 373 | var container = getNode("co"); 374 | 375 | hiddenNodes = new Array(); 376 | 377 | for (var i = container.firstChild; i; i = i.nextSibling) { 378 | hiddenNodes.push(i); 379 | i.style.display = "none"; 380 | } 381 | 382 | searchesContainer = newNode("div"); 383 | searchesContainer.className = "searchesContainer"; 384 | searchesContainer.innerHTML += "Persistent Searches"; 385 | 386 | container.appendChild(searchesContainer); 387 | 388 | var innerContainer = newNode("div"); 389 | innerContainer.className = "innerContainer"; 390 | innerContainer.innerHTML += 391 | '

Use operators ' + 392 | 'to specify queries. today, yesterday and oneweekago ' + 393 | 'are also supported as values for the before: and after: ' + 394 | 'operators. ' + 395 | 'Prefix your search query with ++ ' + 396 | 'to make a search that will ADD your query to the current query. ' + 397 | 'Delete an item\'s query to remove it.

'; 398 | searchesContainer.appendChild(innerContainer); 399 | 400 | searchesList = newNode("table"); 401 | searchesList.className = "searchesList"; 402 | innerContainer.appendChild(searchesList); 403 | 404 | var headerRow = newNode("tr"); 405 | searchesList.appendChild(headerRow); 406 | headerRow.appendChild(newNode("th")).appendChild(newText("Label")); 407 | headerRow.appendChild(newNode("th")).appendChild(newText("Query")); 408 | 409 | for (var i=0; i < searches.length; i++) { 410 | if (searches[i].type != PersistentSearch.SAVED_TYPE) { 411 | continue; 412 | } 413 | 414 | searchesList.appendChild(searches[i].getEditItem(i)); 415 | 416 | var dividerRow = newNode("tr"); 417 | var dividerCell = dividerRow.appendChild(newNode("td")); 418 | dividerCell.className = "divider"; 419 | dividerCell.colSpan = 3; 420 | searchesList.appendChild(dividerRow); 421 | } 422 | 423 | var newSearch = new PersistentSearch("", ""); 424 | var newRow = newNode("tr"); 425 | var newCell = newRow.appendChild(newNode("td")); 426 | newCell.colSpan = 3; 427 | newCell.appendChild(newText("Create a new persistent search:")); 428 | 429 | searchesList.appendChild(newRow); 430 | searchesList.appendChild(newSearch.getEditItem(-1)); 431 | 432 | var dividerRow = newNode("tr"); 433 | var dividerCell = dividerRow.appendChild(newNode("td")); 434 | dividerCell.className = "divider"; 435 | dividerCell.colSpan = 3; 436 | searchesList.appendChild(dividerRow); 437 | 438 | // and give them a choice of positioning 439 | var newRow = newNode("tr"); 440 | var newCell = newRow.appendChild(newNode("td")); 441 | newCell.colSpan = 3; 442 | newCell.appendChild(newText("Seaches Position:")); 443 | newCell.appendChild(newRadioButton("pos", "nb_2", "Top")); 444 | newCell.appendChild(newRadioButton("pos", "nb_0", "Under Contacts")); 445 | newCell.appendChild(newRadioButton("pos", "nb_1", "Under Labels")); 446 | 447 | searchesList.appendChild(newRow); 448 | 449 | var newRow = newNode("tr"); 450 | var newCell = newRow.appendChild(newNode("td")); 451 | newCell.colSpan = 3; 452 | newCell.appendChild(newText("Recent Searches: Position")); 453 | newCell.appendChild(newRadioButton("recentpos", "top", "Above")); 454 | newCell.appendChild(newRadioButton("recentpos", "bottom", "Below")); 455 | newCell.appendChild(newText(" - Number ")); 456 | var num = newCell.appendChild(newNode("input")); 457 | num.type = "text"; 458 | num.name = "numrecent"; 459 | num.value = GM_getValue(RECENT_NUM); 460 | num.size = 3; 461 | num.addEventListener("change", function() { 462 | GM_setValue(RECENT_NUM + NEW_VALUE, num.value); 463 | }, false); 464 | 465 | searchesList.appendChild(newRow); 466 | 467 | var buttonContainer = newNode("div"); 468 | buttonContainer.className = "buttonContainer"; 469 | var cancelButton = newNode("button"); 470 | cancelButton.appendChild(newText("Cancel")); 471 | cancelButton.className = "cancelButton"; 472 | cancelButton.addEventListener("click", cancelEditPersistentSearches, false); 473 | buttonContainer.appendChild(cancelButton); 474 | 475 | var saveButton = newNode("button"); 476 | saveButton.appendChild(newText("Save Changes")); 477 | saveButton.className = "saveButton"; 478 | saveButton.addEventListener("click", saveEditPersistentSeaches, false); 479 | 480 | buttonContainer.appendChild(saveButton); 481 | innerContainer.appendChild(buttonContainer); 482 | 483 | // Make clicks outside the edit area hide it 484 | getNode("nav").addEventListener("click", cancelEditPersistentSearches, false); 485 | 486 | // Since we're in a child of the "nav" element, the above handler will get 487 | // triggered immediately unless we stop this event from propagating 488 | event.stopPropagation(); 489 | 490 | return false; 491 | } 492 | 493 | function newRadioButton(name, value, label) { 494 | var span = newNode("span"); 495 | var lab = span.appendChild(newNode("label")); 496 | var rb1 = lab.appendChild(newNode("input")); 497 | rb1.type = "radio"; 498 | rb1.name = name; 499 | rb1.value = value; 500 | rb1.addEventListener("click", function() { 501 | GM_setValue(KEY_PREFIX + name + NEW_VALUE, value); 502 | }, false); 503 | if (GM_getValue(KEY_PREFIX + name, null) == value) { 504 | rb1.checked = true; 505 | } 506 | lab.appendChild(newText(label)); 507 | return span; 508 | } 509 | 510 | function cancelEditPersistentSearches() { 511 | searchesContainer.parentNode.removeChild(searchesContainer); 512 | searchesContainer = null; 513 | 514 | for (var i=0; i < hiddenNodes.length; i++) { 515 | hiddenNodes[i].style.display = ""; 516 | } 517 | getNode("nav").removeEventListener("click", 518 | cancelEditPersistentSearches, 519 | false); 520 | 521 | return true; 522 | } 523 | 524 | function saveEditPersistentSeaches() { 525 | clearPersistentSearches(); 526 | 527 | for (var row = searchesList.firstChild; row; row = row.nextSibling) { 528 | var cells = row.getElementsByTagName("td"); 529 | if (cells.length != 2 && cells.length != 3) { 530 | continue; 531 | } 532 | var label = cells[0].getElementsByTagName("input")[0].value; 533 | var query = cells[1].getElementsByTagName("input")[0].value; 534 | 535 | if (label && query) { 536 | var search = new PersistentSearch(query, label); 537 | 538 | addPersistentSearch(search); 539 | } 540 | } 541 | saveSearches(); 542 | 543 | // cancelling just hides everything, which is what we want to do 544 | cancelEditPersistentSearches(); 545 | 546 | // now move the searches box if we need to... 547 | var newpos = GM_getValue(POSITION + NEW_VALUE); 548 | if (newpos && GM_getValue(POSITION) != newpos) { 549 | GM_setValue(POSITION, newpos); 550 | searchesBlock.parentNode.removeChild(searchesBlock); 551 | insertSearchesBlock(); 552 | } 553 | 554 | var newpos = GM_getValue(RECENT_NUM + NEW_VALUE); 555 | if (newpos && GM_getValue(RECENT_NUM) != newpos) { 556 | GM_setValue(RECENT_NUM, newpos); 557 | limitRecentSearch(); 558 | } 559 | 560 | var newpos = GM_getValue(RECENT_POS + NEW_VALUE); 561 | if (newpos && GM_getValue(RECENT_POS) != newpos) { 562 | GM_setValue(RECENT_POS, newpos); 563 | rearrangeSeachesBlock(); 564 | } 565 | } 566 | 567 | function moveEditPersistentSearch(oldindex, newindex) { 568 | var oldrow; 569 | var newrow; 570 | var idx = 0; 571 | for (var row = searchesList.firstChild; row; row = row.nextSibling) { 572 | var cells = row.getElementsByTagName("td"); 573 | if (cells.length != 3) { 574 | continue; 575 | } 576 | if (idx == oldindex) { 577 | oldrow = cells; 578 | } 579 | if (idx == newindex) { 580 | newrow = cells; 581 | } 582 | idx++; 583 | } 584 | if (oldrow && newrow) { 585 | swapValues(oldrow[0].getElementsByTagName("input")[0], 586 | newrow[0].getElementsByTagName("input")[0]); 587 | swapValues(oldrow[1].getElementsByTagName("input")[0], 588 | newrow[1].getElementsByTagName("input")[0]); 589 | } 590 | } 591 | 592 | function swapValues(oldtag, newtag) { 593 | var tmp = oldtag.value; 594 | oldtag.value = newtag.value; 595 | newtag.value = tmp; 596 | } 597 | 598 | function togglePersistentSearches() { 599 | if (searchesBlockList.style.display == "none") { 600 | searchesBlockList.style.display = "block"; 601 | recentSearchesBlockList.style.display = "block"; 602 | editLink.style.display = ""; 603 | searchesBlockHeader.triangleImage.src = "/mail/images/opentriangle.gif"; 604 | setCookie(SEARCHES_COLLAPSED_COOKIE, "0"); 605 | } else { 606 | searchesBlockList.style.display = "none"; 607 | recentSearchesBlockList.style.display = "none"; 608 | editLink.style.display = "none"; 609 | searchesBlockHeader.triangleImage.src = "/mail/images/triangle.gif"; 610 | setCookie(SEARCHES_COLLAPSED_COOKIE, "1"); 611 | } 612 | 613 | return false; 614 | } 615 | 616 | function PersistentSearch(query, label, type) { 617 | this.query = query; 618 | this.label = label; 619 | this.type = type || PersistentSearch.SAVED_TYPE; 620 | 621 | this.totalResults = -1; 622 | this.unreadResults = -1; 623 | 624 | this.listItem = null; 625 | this.editItem = null; 626 | } 627 | 628 | PersistentSearch.SAVED_TYPE = 0; 629 | PersistentSearch.RECENT_TYPE = 1; 630 | 631 | PersistentSearch.prototype.toString = function() { 632 | var serialized = new Array(); 633 | 634 | for (var property in this) { 635 | if (typeof(this[property]) != "function" && 636 | typeof(this[property]) != "object") { 637 | serialized.push(property + "=" + this[property]); 638 | } 639 | } 640 | 641 | return encodeURIComponent(serialized.join("&")); 642 | } 643 | 644 | PersistentSearch.prototype.fromString = function(serialized) { 645 | var properties = decodeURIComponent(serialized).split("&"); 646 | 647 | var search = new PersistentSearch("", ""); 648 | 649 | for (var i=0; i < properties.length; i++) { 650 | var keyValue = properties[i].split("="); 651 | 652 | search[keyValue[0]] = keyValue[1]; 653 | } 654 | 655 | return search; 656 | } 657 | 658 | PersistentSearch.prototype.equals = function(search) { 659 | return this.getRunnableQuery() == search.getRunnableQuery(); 660 | } 661 | 662 | PersistentSearch.prototype.getListItem = function() { 663 | if (!this.listItem) { 664 | this.listItem = newNode("div"); 665 | if (this.type == PersistentSearch.SAVED_TYPE) { 666 | this.listItem.id = SAVED_SEARCH_PREFIX + this.label; 667 | } 668 | this.listItem.className = "lk cs listItem"; 669 | this.listItem.appendChild(newText(this.label)); 670 | this.listItem.addEventListener("mousedown", 671 | getObjectMethodClosure(this, "execute"), 672 | false); 673 | } 674 | 675 | return this.listItem; 676 | } 677 | 678 | PersistentSearch.prototype.getEditItem = function(pos) { 679 | if (!this.editItem) { 680 | this.editItem = newNode("tr"); 681 | this.editItem.className = "editItem"; 682 | 683 | var labelCell = newNode("td"); 684 | labelCell.className = "labelCell"; 685 | var labelInput = newNode("input"); 686 | labelInput.value = this.label; 687 | labelCell.appendChild(labelInput); 688 | this.editItem.appendChild(labelCell); 689 | 690 | var editCell = newNode("td"); 691 | editCell.className = "editCell"; 692 | var queryInput = newNode("input"); 693 | queryInput.value = this.getEditableQuery(); 694 | editCell.appendChild(queryInput); 695 | this.editItem.appendChild(editCell); 696 | 697 | if (pos != -1) { 698 | var mvCell = newNode("td"); 699 | mvCell.className = "mvCell"; 700 | if (pos != 0) { 701 | var upButton = newNode("img"); 702 | upButton.src = UP_TRIANGLE_IMAGE 703 | upButton.height = 11; 704 | upButton.height = 11; 705 | upButton.addEventListener("click", 706 | function() { 707 | moveEditPersistentSearch(pos, pos - 1); 708 | }, 709 | false); 710 | mvCell.appendChild(upButton); 711 | } 712 | if (pos != searches.length -1) { 713 | var downButton = newNode("img"); 714 | downButton.src = "/mail/images/opentriangle.gif"; 715 | downButton.height = 11; 716 | downButton.height = 11; 717 | downButton.addEventListener("click", 718 | function() { 719 | moveEditPersistentSearch(pos, pos + 1); 720 | }, 721 | false); 722 | mvCell.appendChild(downButton); 723 | } 724 | this.editItem.appendChild(mvCell); 725 | } 726 | } 727 | 728 | return this.editItem; 729 | } 730 | 731 | // Does this search represent a modifier and not a real search 732 | // modifiers just add to the current search string 733 | PersistentSearch.prototype.isSearchModifier = function() { 734 | return this.query.substring(0,2) == "++"; 735 | } 736 | 737 | PersistentSearch.prototype.execute = function() { 738 | var searchForm = getNode("s"); 739 | var queryInput = null; 740 | var inputs = searchForm.getElementsByTagName("input"); 741 | for (var i=0; i < inputs.length; i++) { 742 | if (inputs[i].name == "q") { 743 | queryInput = inputs[i]; 744 | } 745 | } 746 | if (this.isSearchModifier()) { 747 | queryInput.value = queryInput.value + " " + this.getRunnableQuery(); 748 | } else { 749 | queryInput.value = this.getRunnableQuery(); 750 | } 751 | searchForm.onsubmit(); 752 | } 753 | 754 | PersistentSearch.prototype.getRunnableQuery = function() { 755 | var query = this.query; 756 | 757 | if (this.isSearchModifier()) { 758 | query = query.substring(2); 759 | } 760 | 761 | var today = new Date(); 762 | var yesterday = new Date(today.getTime() - ONE_DAY); 763 | var oneWeekAgo = new Date(today.getTime() - 7 * ONE_DAY); 764 | 765 | query = query.replace(/:today/g, ":" + getDateString(today)); 766 | query = query.replace(/:yesterday/g, ":" + getDateString(yesterday)); 767 | query = query.replace(/:oneweekago/g, ":" + getDateString(oneWeekAgo)); 768 | 769 | return query; 770 | } 771 | 772 | PersistentSearch.prototype.getEditableQuery = function() { 773 | return this.query; 774 | } 775 | 776 | function initializeStyles() { 777 | var styleNode = newNode("style"); 778 | 779 | document.body.appendChild(styleNode); 780 | 781 | styleSheet = document.styleSheets[document.styleSheets.length - 1]; 782 | 783 | for (var i=0; i < RULES.length; i++) { 784 | styleSheet.insertRule(RULES[i], 0); 785 | } 786 | } 787 | 788 | initializeStyles(); 789 | initializePersistentSearches(); 790 | -------------------------------------------------------------------------------- /scripts/gmail-view-watcher.user.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2007, Google Inc. 2 | // Released under the BSD license: 3 | // http://www.opensource.org/licenses/bsd-license.php 4 | // 5 | // ==UserScript== 6 | // @name Gmail View Watcher 7 | // @namespace http://mail.google.com/ 8 | // @description Adds a nav box to Gmail which monitors the current view. 9 | // @include http://mail.google.com/* 10 | // @include https://mail.google.com/* 11 | // ==/UserScript== 12 | 13 | window.addEventListener('load', function() { 14 | if (unsafeWindow.gmonkey) { 15 | unsafeWindow.gmonkey.load('1.0', function(gmail) { 16 | function setViewType() { 17 | var str = ''; 18 | switch (gmail.getActiveViewType()) { 19 | case 'tl': str = 'Threadlist'; break; 20 | case 'cv': str = 'Conversation'; break; 21 | case 'co': str = 'Compose'; break; 22 | case 'ct': str = 'Contacts'; break; 23 | case 's': str = 'Settings'; break; 24 | default: str = 'Unknown'; 25 | } 26 | module.setContent(str); 27 | } 28 | var module = gmail.addNavModule('View Monitor'); 29 | gmail.registerViewChangeCallback(setViewType); 30 | setViewType(); 31 | }); 32 | } 33 | }, true); 34 | --------------------------------------------------------------------------------