├── icon.png
├── install.rdf
├── scripts
├── helper.js
└── utils.js
└── bootstrap.js
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mozilla/twitter-address-bar-search/HEAD/icon.png
--------------------------------------------------------------------------------
/install.rdf:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | Twitter
6 | Use Twitter to search for #hashtag and @mention from the address bar.
7 | https://twitter.com/
8 | twitter.address.bar.search@firefox.twitter
9 | Twitter Address Bar Search
10 | 1
11 |
12 | true
13 | 2
14 |
15 |
16 |
17 | {ec8030f7-c20a-464f-9b0e-13a3a9e97384}
18 | 4.0
19 | 7.0a1
20 |
21 |
22 |
23 |
24 |
25 | {a23983c0-fd0e-11dc-95ff-0800200c9a66}
26 | 4.0
27 | 7.0a1
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/scripts/helper.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Twitter Address Bar Search Helper Functions.
15 | *
16 | * The Initial Developer of the Original Code is The Mozilla Foundation.
17 | * Portions created by the Initial Developer are Copyright (C) 2011
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):
21 | * Edward Lee
22 | *
23 | * Alternatively, the contents of this file may be used under the terms of
24 | * either the GNU General Public License Version 2 or later (the "GPL"), or
25 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 | * in which case the provisions of the GPL or the LGPL are applicable instead
27 | * of those above. If you wish to allow use of your version of this file only
28 | * under the terms of either the GPL or the LGPL, and not to allow others to
29 | * use your version of this file under the terms of the MPL, indicate your
30 | * decision by deleting the provisions above and replace them with the notice
31 | * and other provisions required by the GPL or the LGPL. If you do not delete
32 | * the provisions above, a recipient may use your version of this file under
33 | * the terms of any one of the MPL, the GPL or the LGPL.
34 | *
35 | * ***** END LICENSE BLOCK ***** */
36 |
37 | "use strict";
38 |
39 | const LANDING_PAGE = "https://twitter.com/download/firefox/welcome";
40 | const TWITTER_ICON = "data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2Fv7%2BD%2F7%2B%2Fj%2F%2B%2Fv5g%2Fv7%2BYP7%2B%2FmD%2B%2Fv5I%2Fv7%2BKP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2B%2Fv4H%2Fv7%2BUPbv4pHgx47B1K9Y3tWwWN7Ur1je3sKCx%2BrbuKj%2B%2Fv5n%2Fv7%2BGP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2B%2Fv4Y%2BfbweM2ycMe2iB7%2FvI0f%2F8STIf%2FKlyL%2FzJki%2F8yZIv%2FLmCL%2F0ahK5%2FHp1JH%2B%2Fv4Y%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A7OTTaquHN%2BCujkXPs5ZTv6N6G%2F%2B2iB7%2FxpUh%2F8yZIv%2FMmSL%2FzJki%2F8yZIv%2FKmy738OjUi%2F%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAMKtfY7w6%2BEf%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A3sqbp8iWIf%2FMmSL%2FzJki%2F8yZIv%2FMmSL%2Fy5gi%2F8mePO7%2B%2Fv4w%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2B%2Fv4H%2Fv7%2BV9CtWN3KmCL%2FzJki%2F8yZIv%2FMmSL%2FzJki%2F8yZIv%2FJlyH%2F5tSqp%2F7%2B%2FmD%2B%2Fv4%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2BPXvJtGyZdXNnS%2F3y5gi%2F8qYIv%2FLmCL%2FzJki%2F8yZIv%2FMmSL%2Fy5gi%2F82iPO7LqVfe0byMmf%2F%2F%2FwD%2F%2F%2F8A%2Fv7%2BD%2FDo1JHKmy73ypci%2F8KSIP%2B%2FjyD%2FxpQh%2F8uYIv%2FMmSL%2FzJki%2F8qYIv%2B%2FjyD%2FrIEd%2F9nKqH7%2F%2F%2F8A%2F%2F%2F%2FAPPu4TzAlSz3wZEg%2F7mLH%2F%2BsgR3%2FuZdGz7mLH%2F%2FJlyH%2FzJki%2F8yZIv%2FGlSH%2Fto0r9eXbxD%2FVx6dg%2F%2F%2F%2FAP7%2B%2Fh%2Fp38WhtIsq9al%2FHP%2BkfyjuybaKgf%2F%2F%2FwCzjzjlwJAg%2F8qYIv%2FJlyH%2Fu4wf%2F8CkYrn%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwDj2sRMnHUa%2F7meYa7Vx6dg%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A2MmnYK6DHf%2B%2BjiD%2Fvo4g%2F62CHf%2Fk2sQ%2F%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A8OvhH%2Ff07w%2F%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwC%2Fp3Cfpnwc%2F66GKvPg1LZ8%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FANXHp2DJtoqByLWKgf%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F%2F%2FAP%2F%2F%2FwD%2F%2F%2F8A%2F%2F8AAP%2F%2FAADgPwAAwA8AAIAHAAB4BwAA%2BAMAAPAAAADgAQAA4AMAAMEDAADPhwAA%2F48AAP%2FnAAD%2F%2FwAA%2F%2F8AAA%3D%3D";
41 |
42 | // Look through tabs in the browser to see if any match
43 | function findOpenTab(browser, checkTabAndURI) {
44 | let foundTab;
45 | Array.some(browser.tabs, function(tab) {
46 | // Check if there's an existing page
47 | try {
48 | // Use an activate navigation if it's still loading
49 | let {currentURI, webNavigation, __SS_data} = tab.linkedBrowser;
50 | let channel = webNavigation.documentChannel;
51 | if (channel != null)
52 | currentURI = channel.originalURI
53 |
54 | // Use the session restore entry if it's still restoring
55 | if (currentURI.spec == "about:blank" && __SS_data != null)
56 | currentURI = Services.io.newURI(__SS_data.entries[0].url, null, null);
57 |
58 | // Short circuit now that we found it
59 | if (checkTabAndURI(tab, currentURI)) {
60 | foundTab = tab;
61 | return true;
62 | }
63 | }
64 | catch(ex) {}
65 | });
66 | return foundTab;
67 | }
68 |
69 | // Get a twitter url with a partner code
70 | function getTwitterBase(path, from) {
71 | return "https://twitter.com/" + path + "?partner=mozilla&source=" +
72 | platform + "-" + from;
73 | }
74 |
75 | // Take a window and create various helper properties and functions
76 | function makeWindowHelpers(window) {
77 | let {clearTimeout, setTimeout} = window;
78 |
79 | // Call a function after waiting a little bit
80 | function async(callback, delay) {
81 | let timer = setTimeout(function() {
82 | stopTimer();
83 | callback();
84 | }, delay);
85 |
86 | // Provide a way to stop an active timer
87 | function stopTimer() {
88 | if (timer == null)
89 | return;
90 | clearTimeout(timer);
91 | timer = null;
92 | unUnload();
93 | }
94 |
95 | // Make sure to stop the timer when unloading
96 | let unUnload = unload(stopTimer, window);
97 |
98 | // Give the caller a way to cancel the timer
99 | return stopTimer;
100 | }
101 |
102 | // Replace a value with another value or a function of the original value
103 | function change(obj, prop, val) {
104 | let orig = obj[prop];
105 | obj[prop] = typeof val == "function" ? val(orig) : val;
106 | unload(function() obj[prop] = orig, window);
107 | }
108 |
109 | return {
110 | async: async,
111 | change: change,
112 | };
113 | }
114 |
115 | // Convert a query to a url
116 | function toTwitterUrl(query, from) {
117 | // Replace the #tag or @user with a url + referral code
118 | let path = encodeURIComponent(query).
119 | replace(/^%23/, "search/%23").replace(/^%40/, "");
120 | return getTwitterBase(path, from);
121 | }
122 |
123 | // Check if a query is a twitter-like input
124 | function twitterLike(query) {
125 | return query.search(/^[@#][^ ]*$/) == 0;
126 | }
127 |
--------------------------------------------------------------------------------
/scripts/utils.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Home Dash Utility.
15 | *
16 | * The Initial Developer of the Original Code is The Mozilla Foundation.
17 | * Portions created by the Initial Developer are Copyright (C) 2011
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):
21 | * Edward Lee
22 | *
23 | * Alternatively, the contents of this file may be used under the terms of
24 | * either the GNU General Public License Version 2 or later (the "GPL"), or
25 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 | * in which case the provisions of the GPL or the LGPL are applicable instead
27 | * of those above. If you wish to allow use of your version of this file only
28 | * under the terms of either the GPL or the LGPL, and not to allow others to
29 | * use your version of this file under the terms of the MPL, indicate your
30 | * decision by deleting the provisions above and replace them with the notice
31 | * and other provisions required by the GPL or the LGPL. If you do not delete
32 | * the provisions above, a recipient may use your version of this file under
33 | * the terms of any one of the MPL, the GPL or the LGPL.
34 | *
35 | * ***** END LICENSE BLOCK ***** */
36 |
37 | "use strict";
38 |
39 | /**
40 | * Get a localized string with string replacement arguments filled in and
41 | * correct plural form picked if necessary.
42 | *
43 | * @note: Initialize the strings to use with getString.init(addon).
44 | *
45 | * @usage getString(name): Get the localized string for the given name.
46 | * @param [string] name: Corresponding string name in the properties file.
47 | * @return [string]: Localized string for the string name.
48 | *
49 | * @usage getString(name, arg): Replace %S references in the localized string.
50 | * @param [string] name: Corresponding string name in the properties file.
51 | * @param [any] arg: Value to insert for instances of %S.
52 | * @return [string]: Localized string with %S references replaced.
53 | *
54 | * @usage getString(name, args): Replace %1$S references in localized string.
55 | * @param [string] name: Corresponding string name in the properties file.
56 | * @param [array of any] args: Array of values to replace references like %1$S.
57 | * @return [string]: Localized string with %N$S references replaced.
58 | *
59 | * @usage getString(name, args, plural): Pick the correct plural form.
60 | * @param [string] name: Corresponding string name in the properties file.
61 | * @param [array of any] args: Array of values to replace references like %1$S.
62 | * @param [number] plural: Number to decide what plural form to use.
63 | * @return [string]: Localized string of the correct plural form.
64 | */
65 | function getString(name, args, plural) {
66 | // Use the cached bundle to retrieve the string
67 | let str;
68 | try {
69 | str = getString.bundle.GetStringFromName(name);
70 | }
71 | // Use the fallback in-case the string isn't localized
72 | catch(ex) {
73 | str = getString.fallback.GetStringFromName(name);
74 | }
75 |
76 | // Pick out the correct plural form if necessary
77 | if (plural != null)
78 | str = getString.plural(plural, str);
79 |
80 | // Fill in the arguments if necessary
81 | if (args != null) {
82 | // Convert a string or something not array-like to an array
83 | if (typeof args == "string" || args.length == null)
84 | args = [args];
85 |
86 | // Assume %S refers to the first argument
87 | str = str.replace(/%s/gi, args[0]);
88 |
89 | // Replace instances of %N$S where N is a 1-based number
90 | Array.forEach(args, function(replacement, index) {
91 | str = str.replace(RegExp("%" + (index + 1) + "\\$S", "gi"), replacement);
92 | });
93 | }
94 |
95 | return str;
96 | }
97 |
98 | /**
99 | * Initialize getString() for the provided add-on.
100 | *
101 | * @usage getString.init(addon): Load properties file for the add-on.
102 | * @param [object] addon: Add-on object from AddonManager
103 | *
104 | * @usage getString.init(addon, getAlternate): Load properties with alternate.
105 | * @param [object] addon: Add-on object from AddonManager
106 | * @param [function] getAlternate: Convert a locale to an alternate locale
107 | */
108 | getString.init = function(addon, getAlternate) {
109 | // Set a default get alternate function if it doesn't exist
110 | if (typeof getAlternate != "function")
111 | getAlternate = function() "en-US";
112 |
113 | // Get the bundled properties file for the app's locale
114 | function getBundle(locale) {
115 | let propertyPath = "locales/" + locale + ".properties";
116 | let propertyFile = addon.getResourceURI(propertyPath);
117 |
118 | // Get a bundle and test if it's able to do simple things
119 | try {
120 | // Avoid caching issues by always getting a new file
121 | let uniqueFileSpec = propertyFile.spec + "#" + Math.random();
122 | let bundle = Services.strings.createBundle(uniqueFileSpec);
123 | bundle.getSimpleEnumeration();
124 | return bundle;
125 | }
126 | catch(ex) {}
127 |
128 | // The locale must not exist, so give nothing
129 | return null;
130 | }
131 |
132 | // Use the current locale or the alternate as the primary bundle
133 | let locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
134 | getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global");
135 | getString.bundle = getBundle(locale) || getBundle(getAlternate(locale));
136 |
137 | // Create a fallback in-case a string is missing
138 | getString.fallback = getBundle("en-US");
139 |
140 | // Get the appropriate plural form getter
141 | Cu.import("resource://gre/modules/PluralForm.jsm");
142 | let rule = getString("pluralRule");
143 | [getString.plural] = PluralForm.makeGetter(rule);
144 | }
145 |
146 | /**
147 | * Helper that adds event listeners and remembers to remove on unload
148 | */
149 | function listen(window, node, event, func, capture) {
150 | // Default to use capture
151 | if (capture == null)
152 | capture = true;
153 |
154 | node.addEventListener(event, func, capture);
155 | function undoListen() {
156 | node.removeEventListener(event, func, capture);
157 | }
158 |
159 | // Undo the listener on unload and provide a way to undo everything
160 | let undoUnload = unload(undoListen, window);
161 | return function() {
162 | undoListen();
163 | undoUnload();
164 | };
165 | }
166 |
167 | /**
168 | * Save callbacks to run when unloading. Optionally scope the callback to a
169 | * container, e.g., window. Provide a way to run all the callbacks.
170 | *
171 | * @usage unload(): Run all callbacks and release them.
172 | *
173 | * @usage unload(callback): Add a callback to run on unload.
174 | * @param [function] callback: 0-parameter function to call on unload.
175 | * @return [function]: A 0-parameter function that undoes adding the callback.
176 | *
177 | * @usage unload(callback, container) Add a scoped callback to run on unload.
178 | * @param [function] callback: 0-parameter function to call on unload.
179 | * @param [node] container: Remove the callback when this container unloads.
180 | * @return [function]: A 0-parameter function that undoes adding the callback.
181 | */
182 | function unload(callback, container) {
183 | // Initialize the array of unloaders on the first usage
184 | let unloaders = unload.unloaders;
185 | if (unloaders == null)
186 | unloaders = unload.unloaders = [];
187 |
188 | // Calling with no arguments runs all the unloader callbacks
189 | if (callback == null) {
190 | unloaders.slice().forEach(function(unloader) unloader());
191 | unloaders.length = 0;
192 | return;
193 | }
194 |
195 | // The callback is bound to the lifetime of the container if we have one
196 | if (container != null) {
197 | // Remove the unloader when the container unloads
198 | container.addEventListener("unload", removeUnloader, false);
199 |
200 | // Wrap the callback to additionally remove the unload listener
201 | let origCallback = callback;
202 | callback = function() {
203 | container.removeEventListener("unload", removeUnloader, false);
204 | origCallback();
205 | }
206 | }
207 |
208 | // Wrap the callback in a function that ignores failures
209 | function unloader() {
210 | try {
211 | callback();
212 | }
213 | catch(ex) {}
214 | }
215 | unloaders.push(unloader);
216 |
217 | // Provide a way to remove the unloader
218 | function removeUnloader() {
219 | let index = unloaders.indexOf(unloader);
220 | if (index != -1)
221 | unloaders.splice(index, 1);
222 | }
223 | return removeUnloader;
224 | }
225 |
226 | /**
227 | * Apply a callback to each open and new browser windows.
228 | *
229 | * @usage watchWindows(callback): Apply a callback to each browser window.
230 | * @param [function] callback: 1-parameter function that gets a browser window.
231 | */
232 | function watchWindows(callback) {
233 | // Wrap the callback in a function that ignores failures
234 | function watcher(window) {
235 | try {
236 | // Now that the window has loaded, only handle browser windows
237 | let {documentElement} = window.document;
238 | if (documentElement.getAttribute("windowtype") == "navigator:browser")
239 | callback(window);
240 | }
241 | catch(ex) {}
242 | }
243 |
244 | // Wait for the window to finish loading before running the callback
245 | function runOnLoad(window) {
246 | // Listen for one load event before checking the window type
247 | window.addEventListener("load", function runOnce() {
248 | window.removeEventListener("load", runOnce, false);
249 | watcher(window);
250 | }, false);
251 | }
252 |
253 | // Add functionality to existing windows
254 | let windows = Services.wm.getEnumerator(null);
255 | while (windows.hasMoreElements()) {
256 | // Only run the watcher immediately if the window is completely loaded
257 | let window = windows.getNext();
258 | if (window.document.readyState == "complete")
259 | watcher(window);
260 | // Wait for the window to load before continuing
261 | else
262 | runOnLoad(window);
263 | }
264 |
265 | // Watch for new browser windows opening then wait for it to load
266 | function windowWatcher(subject, topic) {
267 | if (topic == "domwindowopened")
268 | runOnLoad(subject);
269 | }
270 | Services.ww.registerNotification(windowWatcher);
271 |
272 | // Make sure to stop watching for windows if we're unloading
273 | unload(function() Services.ww.unregisterNotification(windowWatcher));
274 | }
275 |
--------------------------------------------------------------------------------
/bootstrap.js:
--------------------------------------------------------------------------------
1 | /* ***** BEGIN LICENSE BLOCK *****
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3 | *
4 | * The contents of this file are subject to the Mozilla Public License Version
5 | * 1.1 (the "License"); you may not use this file except in compliance with
6 | * the License. You may obtain a copy of the License at
7 | * http://www.mozilla.org/MPL/
8 | *
9 | * Software distributed under the License is distributed on an "AS IS" basis,
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing rights and limitations under the
12 | * License.
13 | *
14 | * The Original Code is Twitter Address Bar Search.
15 | *
16 | * The Initial Developer of the Original Code is The Mozilla Foundation.
17 | * Portions created by the Initial Developer are Copyright (C) 2011
18 | * the Initial Developer. All Rights Reserved.
19 | *
20 | * Contributor(s):
21 | * Edward Lee
22 | *
23 | * Alternatively, the contents of this file may be used under the terms of
24 | * either the GNU General Public License Version 2 or later (the "GPL"), or
25 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 | * in which case the provisions of the GPL or the LGPL are applicable instead
27 | * of those above. If you wish to allow use of your version of this file only
28 | * under the terms of either the GPL or the LGPL, and not to allow others to
29 | * use your version of this file under the terms of the MPL, indicate your
30 | * decision by deleting the provisions above and replace them with the notice
31 | * and other provisions required by the GPL or the LGPL. If you do not delete
32 | * the provisions above, a recipient may use your version of this file under
33 | * the terms of any one of the MPL, the GPL or the LGPL.
34 | *
35 | * ***** END LICENSE BLOCK ***** */
36 |
37 | "use strict";
38 | const global = this;
39 |
40 | const {classes: Cc, interfaces: Ci, manager: Cm, utils: Cu} = Components;
41 | Cu.import("resource://gre/modules/AddonManager.jsm");
42 | Cu.import("resource://gre/modules/Services.jsm");
43 | Cu.import("resource://gre/modules/XPCOMUtils.jsm");
44 |
45 | // Remember if we were just installed
46 | let justInstalled = false;
47 |
48 | // Remember if we're on Firefox or Fennec
49 | let platform = Services.appinfo.name == "Firefox" ? "desktop" : "mobile";
50 |
51 | // Add functionality to search from the location bar and hook up autocomplete
52 | function addTwitterAddressBarSearch(window) {
53 | let {change} = makeWindowHelpers(window);
54 | let {BrowserUI, gBrowser, gURLBar} = window;
55 |
56 | // Check the input to see if the twitter icon should be shown
57 | let lastIcon = "";
58 | function checkInput() {
59 | if (skipCheck())
60 | return;
61 |
62 | // Only allow single word #tag and @user
63 | let icon = "";
64 | if (twitterLike(urlbar.value))
65 | icon = TWITTER_ICON;
66 |
67 | // Remember that the icon is showing
68 | lastIcon = icon;
69 | setIcon(icon);
70 | }
71 |
72 | // Convert to twitter urls if necessary
73 | function getTwitterUrl(input) {
74 | // Only fix up the input if we're indicating that it's a twitter term
75 | return lastIcon == TWITTER_ICON ? toTwitterUrl(input, "bar") : input;
76 | }
77 |
78 | // Figure out how to implement various functions depending on the platform
79 | let setIcon, skipCheck, urlbar;
80 | if (gBrowser == null) {
81 | setIcon = function(url) BrowserUI._updateIcon(url);
82 | skipCheck = function() false;
83 | urlbar = BrowserUI._edit;
84 |
85 | // Check the input on various events
86 | listen(window, BrowserUI._edit, "input", checkInput);
87 |
88 | // Convert inputs to twitter urls
89 | change(window.Browser, "getShortcutOrURI", function(orig) {
90 | return function(uri, data) {
91 | uri = getTwitterUrl(uri);
92 | return orig.call(this, uri, data);
93 | };
94 | });
95 | }
96 | else {
97 | setIcon = function(url) window.PageProxySetIcon(url);
98 | skipCheck = function() gURLBar.getAttribute("pageproxystate") == "valid" &&
99 | !gURLBar.hasAttribute("focused");
100 | urlbar = gURLBar;
101 |
102 | // Check the input on various events
103 | listen(window, gURLBar, "input", checkInput);
104 | listen(window, gBrowser.tabContainer, "TabSelect", checkInput);
105 |
106 | // Convert inputs to twitter urls
107 | change(gURLBar, "_canonizeURL", function(orig) {
108 | return function(event) {
109 | this.value = getTwitterUrl(this.value);
110 | return orig.call(this, event);
111 | };
112 | });
113 | }
114 |
115 | // Provide a way to set the autocomplete search engines and initialize
116 | function setSearch(engines) {
117 | urlbar.setAttribute("autocompletesearch", engines);
118 | urlbar.mSearchNames = null;
119 | urlbar.initSearchNames();
120 | };
121 |
122 | // Add in the twitter search and remove on cleanup
123 | let origSearch = urlbar.getAttribute("autocompletesearch");
124 | setSearch("twitter " + origSearch);
125 | unload(function() setSearch(origSearch));
126 | }
127 |
128 | // Add an autocomplete search engine to provide location bar suggestions
129 | function addTwitterAutocomplete() {
130 | const contract = "@mozilla.org/autocomplete/search;1?name=twitter";
131 | const desc = "Twitter Autocomplete";
132 | const uuid = Components.ID("42778970-8fae-454d-ad3f-eea88b945af1");
133 |
134 | // Keep a timer to send a delayed no match
135 | let timer;
136 | function clearTimer() {
137 | if (timer != null)
138 | timer.cancel();
139 | timer = null;
140 | }
141 | function setTimer(callback) {
142 | timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
143 | timer.initWithCallback({
144 | notify: function() {
145 | timer = null;
146 | callback();
147 | }
148 | }, 1000, timer.TYPE_ONE_SHOT);
149 | }
150 |
151 | // Implement the autocomplete search that handles twitter queries
152 | let search = {
153 | createInstance: function(outer, iid) search.QueryInterface(iid),
154 |
155 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteSearch]),
156 |
157 | // Handle searches from the location bar
158 | startSearch: function(query, param, previous, listener) {
159 | // Always clear the timer on a new search
160 | clearTimer();
161 |
162 | // Specially handle twitter-like queries
163 | if (twitterLike(query)) {
164 | let label;
165 | if (query[0] == "#")
166 | label = "Search for " + query;
167 | else
168 | label = "View account for " + query;
169 |
170 | // Call the listener immediately with one result
171 | listener.onSearchResult(search, {
172 | getCommentAt: function() "Twitter: " + query,
173 |
174 | getImageAt: function() TWITTER_ICON,
175 |
176 | getLabelAt: function() label,
177 |
178 | getValueAt: function() toTwitterUrl(query, "autocomplete"),
179 |
180 | getStyleAt: function() "favicon",
181 |
182 | get matchCount() 1,
183 |
184 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult]),
185 |
186 | removeValueAt: function() {},
187 |
188 | searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
189 |
190 | get searchString() query,
191 | });
192 | }
193 | // Send a delayed NOMATCH so the autocomplete doesn't close early
194 | else {
195 | setTimer(function() {
196 | listener.onSearchResult(search, {
197 | searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
198 | });
199 | });
200 | }
201 | },
202 |
203 | // Nothing to cancel other than a delayed search as results are synchronous
204 | stopSearch: function() {
205 | clearTimer();
206 | },
207 | };
208 |
209 | // Register this autocomplete search service and clean up when necessary
210 | const registrar = Ci.nsIComponentRegistrar;
211 | Cm.QueryInterface(registrar).registerFactory(uuid, desc, contract, search);
212 | unload(function() {
213 | Cm.QueryInterface(registrar).unregisterFactory(uuid, search);
214 | });
215 | }
216 |
217 | // Add a default search engine and move it to the right place
218 | function addTwitterSearchEngine() {
219 | // Hide any existing "Twitter" searches
220 | let origEngine = Services.search.getEngineByName("Twitter");
221 | if (origEngine != null) {
222 | origEngine.hidden = true;
223 | unload(function() origEngine.hidden = false);
224 | }
225 |
226 | // Add the "Twitter " search engine if necessary
227 | let engineName = "Twitter ";
228 | try {
229 | Services.search.addEngineWithDetails(engineName, TWITTER_ICON, "", "",
230 | "GET", getTwitterBase("search/{searchTerms}", "search"));
231 | }
232 | catch(ex) {}
233 |
234 | // Get the just-added or existing engine
235 | let engine = Services.search.getEngineByName(engineName);
236 | if (engine == null)
237 | return;
238 |
239 | // Move it to position #2 after Google for the partner package
240 | Services.search.moveEngine(engine, 1);
241 |
242 | // Clean up when disabling
243 | unload(function() Services.search.removeEngine(engine));
244 | }
245 |
246 | // Make sure the window has an app tab set to Twitter
247 | function ensureTwitterAppTab(window) {
248 | // Only bother if we were just installed and support app tabs
249 | if (!justInstalled || platform != "desktop")
250 | return;
251 |
252 | // Try again after a short delay if session store is initializing
253 | let {__SSi, __SS_restoreID, gBrowser, setTimeout} = window;
254 | if (__SSi == null || __SS_restoreID != null) {
255 | setTimeout(function() ensureTwitterAppTab(window), 1000);
256 | return;
257 | }
258 |
259 | // Figure out if we already have a pinned twitter
260 | let twitterTab = findOpenTab(gBrowser, function(tab, URI) {
261 | return tab.pinned && URI.host == "twitter.com";
262 | });
263 |
264 | // Always remove the twitter tab when uninstalling
265 | unload(function() gBrowser.removeTab(twitterTab));
266 |
267 | // No need to add!
268 | if (twitterTab != null)
269 | return;
270 |
271 | // Add the tab and pin it as the last app tab
272 | twitterTab = gBrowser.addTab(getTwitterBase("", "apptab"));
273 | gBrowser.pinTab(twitterTab);
274 | }
275 |
276 | // Open a new tab for the landing page and select it
277 | function showLandingPage(window) {
278 | // Only bother if we were just installed and haven't shown yet
279 | if (!justInstalled || showLandingPage.shown)
280 | return;
281 |
282 | // Do the appropriate thing on each platform
283 | if (platform == "desktop") {
284 | // Try again after a short delay if session store is initializing
285 | let {__SSi, __SS_restoreID, gBrowser, setTimeout} = window;
286 | if (__SSi == null || __SS_restoreID != null) {
287 | setTimeout(function() showLandingPage(window), 1000);
288 | return;
289 | }
290 |
291 | // Figure out if we already have a landing page
292 | let landingTab = findOpenTab(gBrowser, function(tab, URI) {
293 | return URI.spec == LANDING_PAGE;
294 | });
295 |
296 | // Always remove the landing page when uninstalling
297 | unload(function() gBrowser.removeTab(landingTab));
298 |
299 | // Add the landing page if not open yet
300 | if (landingTab == null)
301 | landingTab = gBrowser.loadOneTab(LANDING_PAGE);
302 |
303 | // Make sure it's focused
304 | gBrowser.selectedTab = landingTab;
305 | }
306 | else {
307 | let {BrowserUI} = window;
308 | let tab = BrowserUI.newTab(LANDING_PAGE);
309 | unload(function() BrowserUI.closeTab(tab));
310 | }
311 |
312 | // Only show the landing page once
313 | showLandingPage.shown = true;
314 | }
315 |
316 | /**
317 | * Handle the add-on being activated on install/enable
318 | */
319 | function startup({id}, reason) AddonManager.getAddonByID(id, function(addon) {
320 | // Load various javascript includes for helper functions
321 | ["helper", "utils"].forEach(function(fileName) {
322 | let fileURI = addon.getResourceURI("scripts/" + fileName + ".js");
323 | Services.scriptloader.loadSubScript(fileURI.spec, global);
324 | });
325 |
326 | // Add twitter support to the browser
327 | watchWindows(addTwitterAddressBarSearch);
328 | addTwitterAutocomplete();
329 | addTwitterSearchEngine();
330 | watchWindows(ensureTwitterAppTab);
331 | watchWindows(showLandingPage);
332 |
333 | // We're no longer just installed after we get some windows loaded
334 | watchWindows(function(window) {
335 | if (justInstalled)
336 | window.setTimeout(function() justInstalled = false, 5000);
337 | });
338 | })
339 |
340 | /**
341 | * Handle the add-on being deactivated on uninstall/disable
342 | */
343 | function shutdown(data, reason) {
344 | // Clean up with unloaders when we're deactivating
345 | if (reason != APP_SHUTDOWN)
346 | unload();
347 | }
348 |
349 | /**
350 | * Handle the add-on being installed
351 | */
352 | function install(data, reason) {
353 | justInstalled = reason == ADDON_INSTALL;
354 | }
355 |
356 | /**
357 | * Handle the add-on being uninstalled
358 | */
359 | function uninstall(data, reason) {}
360 |
--------------------------------------------------------------------------------