├── 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 | '' +
184 | 'Available Keyboard Commands' +
185 | '' +
186 | '' +
187 | 'Standard | Extended | ' +
188 | '
';
189 |
190 | var base = [];
191 | for (var key in BUILTIN_KEYS_HELP) {
192 | base.push("" + key + " | " + BUILTIN_KEYS_HELP[key] + " | ");
193 | }
194 |
195 | var added = [];
196 | for (var key in ADDED_KEYS_HELP) {
197 | added.push("" + key + " | " + ADDED_KEYS_HELP[key] + " | ");
198 | }
199 |
200 | for(var i = 0; i < base.length; i++) {
201 | html += "" + base[i] + added[i] + "
";
202 | }
203 |
204 | html +=
205 | '' +
206 | '' +
207 | '* Hold <Shift> for action in a new window' +
208 | ' | ' +
209 | '
' +
210 | '
';
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 |
--------------------------------------------------------------------------------