home *** CD-ROM | disk | FTP | other *** search
/ Freelog 117 / FreelogNo117-OctobreNovembre2013.iso / Theme / 8GadgetPack / 8GadgetPackSetup.msi / Gadgets.7z / Gadgets / tweetz.gadget / app.js next >
Text File  |  2013-04-09  |  70KB  |  1,860 lines

  1. /// <reference path="jquery.js" />
  2. /*jslint browser: true, windows: true, onevar: true, undef: true, nomen: true, eqeqeq: true, bitwise: true, newcap: true */
  3. /*global $: false, jQuery: false, OAuth: false, window: false, BigInteger: false */
  4.  
  5. var APP = {};
  6.  
  7. jQuery.support.cors = true; // force cross-site scripting (as of jQuery 1.5)
  8. //jQuery.support.appendChecked = true; // as of jQuery 1.6 this is required
  9.  
  10. if (typeof Array.unique !== "function") {
  11.   Array.prototype.unique = function () {
  12.     var i, len = this.length, out = [], obj = {};
  13.     for (i = 0; i < len; i++) { obj[this[i]] = 0; }
  14.     for (i in obj) { if (obj.hasOwnProperty(i)) { out.push(i); } }
  15.     return out;
  16.   };
  17. }
  18.  
  19. if (typeof Object.create !== "function") {
  20.   Object.create = function (o) {
  21.     var F = function () { };
  22.     F.prototype = o;
  23.     return new F();
  24.   };
  25. }
  26.  
  27. String.regexUrl = /(^|[^\/!=])((?:https?:\/\/|www\.)(?:[a-z0-9\/\u272a][a-z0-9\-\/\u272a]*)\.(?:[a-z0-9]+)(?:\w|\([!*';:=+$\/%#\[\]\-_,~.@?&"\w]+\)|\/(?=($|\s))|[!*';:=+$\/%#\[\]\-_,~.@?&"](?=\S))+)/gi;
  28. String.regexHashTag = /(^|[^0-9a-z&\/]+)(#|\uff03)([0-9]*[a-z_][a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff]*)/gi;
  29. String.regexScreenName = /([^a-z0-9_]|^)(@|\uff20)([a-z0-9_]{1,20})(\/[a-z][a-z0-9_\-\x80-\xFF]{0,24})?/gi;
  30.  
  31. String.prototype.format = function () {
  32.   var pattern = /\{\d+\}/g,
  33.       placeHolder = /\d+/,
  34.       args = arguments;
  35.   return this.replace(pattern, function (capture) { return args[capture.match(placeHolder)]; });
  36. };
  37.  
  38. String.prototype.contains = function (text) {
  39.   return this.indexOf(text) !== -1;
  40. };
  41.  
  42. String.prototype.htmlEntities = function () {
  43.   return this.replace(/</g, '<').replace(/>/g, '>');
  44. };
  45.  
  46. String.prototype.isBlank = function () {
  47.   return (/^\s*$/).test(this);
  48. };
  49.  
  50. String.prototype.urlsToLinks = function (showLinks) {
  51.   return this.replace(String.regexUrl, function (text, c1, c2) {
  52.     var amp, que, at, c3 = "";
  53.     amp = text.indexOf("&");
  54.     que = text.indexOf("?");
  55.     if (/^.+@["'][a-zA-Z_\- ]+=/.test(text)) { // don't allow XSS after @
  56.       at = text.indexOf("@") - 1;
  57.       c3 = c2.slice(at);
  58.       c2 = c2.slice(0, at);
  59.     }
  60.     else if (amp > 0 && que < 0) { // don't allow & without ?
  61.       c3 = c2.slice(amp - 1);
  62.       c2 = c2.slice(0, amp - 1);
  63.     }
  64.     return '{0}<a href="{1}" class="link">{2}</a>{3}'.format(c1, c2, (showLinks ? c2 : (APP.locale.link || "[link]")), c3);
  65.   })
  66.   .replace("<a href=\"www.", "<a href=\"http://www.")
  67.   .replace("<a href=\"WWW.", "<a href=\"http://WWW.");
  68. };
  69.  
  70. String.prototype.atScreenNames = function () {
  71.   return this.replace(String.regexScreenName, '$1<span class="screenname" sc="$2$3">$2$3</span>');
  72. };
  73.  
  74. String.prototype.hashTags = function () {
  75.   return this.replace(String.regexHashTag, '$1<span class="hashtag">$2$3</span>');
  76. };
  77.  
  78. String.prototype.htmlDecode = function () {
  79.   return $('<div/>').html(this.toString()).text();
  80. };
  81.  
  82. String.prototype.findUrls = (function () {
  83.   var href = /\bhref="([\S]+\b|$)"/g;
  84.   return function () {
  85.     var matches = this.match(href);
  86.     if (matches) {
  87.       $.each(matches, function (idx) {
  88.         matches[idx] = this.replace(href, "$1");
  89.       });
  90.     }
  91.     return matches;
  92.   };
  93. })();
  94.  
  95. $.fn.setCaretPos = function (pos) {
  96.   return this.each(function () {
  97.     var range = this.createTextRange();
  98.     range.move('character', pos);
  99.     range.select();
  100.   });
  101. };
  102.  
  103. $.fn.pulse = function (options) {
  104.   return this.each(function () {
  105.     var settings = $.extend({}, $.fn.pulse.defaults, options),
  106.         n = settings.count,
  107.         $this = $(this);
  108.     function pulse() {
  109.       $this.fadeOut(settings.speed, function () {
  110.         $this.fadeIn(settings.speed, function () {
  111.           if (--n) { pulse(); return; }
  112.           settings.callback();
  113.         });
  114.       });
  115.     }
  116.     pulse();
  117.   });
  118. };
  119.  
  120. $.fn.pulse.defaults = {
  121.   count: 3,
  122.   speed: 400,
  123.   callback: function () { }
  124. };
  125.  
  126. $.fn.classAddOrRemove = function (className, add) {
  127.   return this.each(function () {
  128.     if (add) { $(this).addClass(className); }
  129.     else { $(this).removeClass(className); }
  130.   });
  131. };
  132.  
  133. $.fn.loadStyleSheet = function (styleSheet) {
  134.   $(this).attr("href", styleSheet);
  135. };
  136.  
  137. jQuery.extend(jQuery.expr[':'], {
  138.   focus: function (element) {
  139.     return element === document.activeElement;
  140.   }
  141. });
  142.  
  143. APP.UTILITY = (function () {
  144.   var that = this;
  145.  
  146.   that.setting = function (name, defaultValue) {
  147.     var setting;
  148.     return function (value) {
  149.       if (value !== undefined) {
  150.         System.Gadget.Settings.write(name, value);
  151.       }
  152.       else {
  153.         setting = System.Gadget.Settings.read(name);
  154.         return (setting !== "") ? setting : defaultValue;
  155.       }
  156.     };
  157.   };
  158.  
  159.   that.modifyStyleSheet = function (selector, properties) {
  160.     selector = selector.toLowerCase();
  161.     $.each(document.styleSheets[0].rules, function () {
  162.       if (this.selectorText.toLowerCase() === selector) {
  163.         for (var property in properties) {
  164.           if (properties.hasOwnProperty(property)) {
  165.             this.style[property] = properties[property];
  166.           }
  167.         }
  168.       }
  169.     });
  170.   };
  171.  
  172.   that.reverseLookup = function (links, link) {
  173.     if (link.substr(0, 7) === "http://" && !links[link]) {
  174.       $.getJSON("http://api.longurl.org/v2/expand",
  175.         { url: link, format: "json" },
  176.         function (r) { links[link] = r["long-url"]; });
  177.     }
  178.   };
  179.  
  180.   that.author = function (name, screen_name) {
  181.     return (name) ? '<span class="screenname author" sc="' + screen_name + '">' + name + "</span>" : "";
  182.   };
  183.  
  184.   that.popup = function (id, text, element, yesAction) {
  185.     var d, dlg, o, t, x, y, h, yes, no;
  186.     $("#" + id).remove();
  187.     d = System.Gadget.docked;
  188.     dlg = $("<div>", { id: id, css: { top: 5000, width: d ? "100px" : "170px"} });
  189.     dlg.append($("<div>", { text: text || "undefined" }));
  190.     if (yesAction) {
  191.       yes = $("<button>", { text: "Yes" }).click(function () { yesAction(); dlg.remove(); });
  192.       no = $("<button>", { text: "No" }).click(function () { dlg.remove(); });
  193.       dlg.append(yes, no);
  194.     }
  195.     $("#content").append(dlg);
  196.     o = element ? element.offset() : { top: 10, left: 10 };
  197.     t = dlg.height();
  198.     x = Math.min(d ? 10 : 80, Math.max(10, o.left));
  199.     y = o.top + (element ? element.height() : 0) + 10;
  200.     h = d ? APP.settings.dockedHeight() : APP.settings.undockedHeight();
  201.     if ((y + t + 10) > h) { y = o.top - t - 10; }
  202.     dlg.hide().css({ "left": x, "top": y }).slideDown("fast");
  203.   };
  204.  
  205.   that.shellRun = function (cmd) {
  206.     var shell = new ActiveXObject("WScript.Shell");
  207.     shell.Run(cmd);
  208.   };
  209.  
  210.   that.showStatus = function (status) {
  211.     $("#content").trigger("showStatus", [status]);
  212.   };
  213.  
  214.   that.versionChecker = (function () {
  215.     var self = {}, result, check, timer;
  216.  
  217.     result = function (xml) {
  218.       var versionNumber = $(xml).find("tweetz31Result").text();
  219.       if (System.Gadget.version !== versionNumber) {
  220.         APP.UTILITY.popup("ask",
  221.           APP.locale.new_version_available || "Hey, there's a newer version available! Go get it?", null,
  222.           function () { APP.UTILITY.shellRun("http://blueonionsoftware.com/tweetz.aspx"); });
  223.       }
  224.     };
  225.  
  226.     check = function () {
  227.       var soapMessage = '<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
  228.                         'xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' +
  229.                         '<soap:Body><tweetz31 xmlns="http://blueonionsoftware.com/"></tweetz31></soap:Body></soap:Envelope>';
  230.       $.ajax({
  231.         url: "http://blueonionsoftware.com/version.asmx",
  232.         type: "POST",
  233.         dataType: "xml",
  234.         data: soapMessage,
  235.         success: function (data, status, xhr) { result(xhr.responseXML); },
  236.         contentType: "text/xml; charset=\"utf-8\""
  237.       });
  238.     };
  239.  
  240.     timer = null;
  241.  
  242.     self.run = function () {
  243.  
  244.       if (APP.settings.checkForUpdates()) {
  245.         if (timer === null) {
  246.           timer = setInterval(function () { check(); }, 1000 * 3600 * 24);
  247.           check();
  248.         }
  249.       }
  250.       else if (timer) {
  251.         clearInterval(timer);
  252.         timer = null;
  253.       }
  254.     };
  255.  
  256.     return self;
  257.   })();
  258.  
  259.   that.command = function (cmd, can) {
  260.     var self = {};
  261.  
  262.     self.execute = function () {
  263.       var args = Array.prototype.slice.call(arguments);
  264.       return self.canExecute() ? cmd.apply(self, args) : undefined;
  265.     };
  266.  
  267.     self.canExecute = function () {
  268.       return (can) ? can() : true;
  269.     };
  270.     return self;
  271.   };
  272.  
  273.   that.maxId = (function () {
  274.     var notDigits = /\D/g,
  275.         removeLeadingZeros = /^0+/,
  276.         isBigger = function (a, b) {
  277.           if (notDigits.test(a) || notDigits.test(b)) { throw "not numbers"; }
  278.           a = a.replace(removeLeadingZeros, '');
  279.           b = b.replace(removeLeadingZeros, '');
  280.           if (a.length !== b.length) { return (a.length > b.length); }
  281.           return a.localeCompare(b) > 0;
  282.         };
  283.     return function (id1, id2) {
  284.       return isBigger(id1, id2) ? id1 : id2;
  285.     };
  286.   })();
  287.  
  288.   return that;
  289. })();
  290.  
  291. APP.settings = {
  292.   userId: APP.UTILITY.setting("userId", ""),
  293.   username: APP.UTILITY.setting("username", ""),
  294.   tokenAccess: APP.UTILITY.setting("tokenAccess", ""),
  295.   tokenSecret: APP.UTILITY.setting("tokenSecret", ""),
  296.   dockedHeight: APP.UTILITY.setting("dockedHeight", 400),
  297.   undockedHeight: APP.UTILITY.setting("undockedHeight", 400),
  298.   chirpOnStatus: APP.UTILITY.setting("chirpOnStatus", false),
  299.   chirpOnMention: APP.UTILITY.setting("chirpOnMention", false),
  300.   chirpOnMessage: APP.UTILITY.setting("chirpOnMessage", false),
  301.   intervalHome: APP.UTILITY.setting("intervalHome", 2),
  302.   intervalMentions: APP.UTILITY.setting("intervalMentions", 4),
  303.   intervalMessages: APP.UTILITY.setting("intervalMessages", 6),
  304.   showLinks: APP.UTILITY.setting("showLinks", false),
  305.   fontSize: APP.UTILITY.setting("fontSize", "medium"),
  306.   checkForUpdates: APP.UTILITY.setting("checkForUpdates", false),
  307.   styleSheet: APP.UTILITY.setting("styleSheet", "css/original.css")
  308. };
  309.  
  310. APP.comm = function () {
  311.   var that = {}, accessor;
  312.  
  313.   accessor =
  314.     {
  315.       consumerKey: "n4Ce1XD9SEXKBpJlkrGpA",
  316.       consumerSecret: "OKcBnIRHubrbKDbkXVEAVFLJ7yUC9glJ6Mhj6enXesU",
  317.       tokenAccess: APP.settings.tokenAccess(),
  318.       tokenSecret: APP.settings.tokenSecret(),
  319.       userId: APP.settings.userId(),
  320.       username: APP.settings.username()
  321.     };
  322.  
  323.   function ajax(type, url, data, success, complete, error) {
  324.     var message = { action: url, method: type };
  325.     if (data) { OAuth.setParameters(message, data); }
  326.     if (accessor.tokenAccess.length > 0) { OAuth.setParameter(message, "oauth_token", accessor.tokenAccess); }
  327.     OAuth.completeRequest(message, accessor);
  328.     $("#content").trigger("commAccessing");
  329.     if (data) { 
  330.       var sdata = "";
  331.       var first = true;
  332.       $.each(data, function(key, value) {
  333.         if (!first) sdata += "&";
  334.         first = false;
  335.         sdata += key + "=" + OAuth.percentEncode(value); 
  336.       });
  337.     }
  338.     $.ajax({ type: message.method, url: message.action, data: sdata,
  339.       headers: {Authorization: OAuth.getAuthorizationHeader(message.parameters)},
  340.       success: function (d, s, x) { if (success) { success(d, s, x); } },
  341.       error: function (d, s, x) { 
  342.         $("#content").trigger("commError"); 
  343.         if (error) { error(d, s, x); } 
  344.       },
  345.       statusCode: {
  346.         200: function () { $("#content").trigger("commSuccess"); },
  347.         429: function () { $("#content").trigger("commTooManyRequests"); }
  348.       },
  349.       complete: complete,
  350.       processData: false,
  351.       crossDomain: true // required as of 1.5
  352.     });
  353.   }
  354.  
  355.   that.get = function (url, data, success, complete, error) { ajax("get", url, data, success, complete, error); };
  356.   that.post = function (url, data, success, complete, error) { ajax("post", url, data, success, complete, error); };
  357.  
  358.   that.tokenAccess = function (t) {
  359.     if (t === undefined) { return accessor.tokenAccess; }
  360.     accessor.tokenAccess = t;
  361.   };
  362.  
  363.   that.tokenSecret = function (t) {
  364.     if (t === undefined) { return accessor.tokenSecret; }
  365.     accessor.tokenSecret = t;
  366.   };
  367.  
  368.   that.userId = function (id) {
  369.     if (id === undefined) { return accessor.userId; }
  370.     accessor.userId = id;
  371.   };
  372.  
  373.   that.username = function (name) {
  374.     if (name === undefined) { return accessor.username; }
  375.     accessor.username = name;
  376.   };
  377.  
  378.   return that;
  379. };
  380.  
  381. APP.twitter = {
  382.   getPin: function (comm, successCallback, errorCallback) {
  383.     comm.tokenAccess("");
  384.     comm.tokenSecret("");
  385.     comm.get("https://twitter.com/oauth/request_token", null, function (data) {
  386.       if (successCallback) { successCallback(); }
  387.       var d = data.split("&");
  388.       comm.tokenAccess(d[0].split("=")[1]);
  389.       comm.tokenSecret(d[1].split("=")[1]);
  390.       APP.UTILITY.shellRun("https://twitter.com/oauth/authorize?" + data + "&oauth_callback=oob");
  391.     }, null, errorCallback);
  392.   },
  393.  
  394.   getAccessToken: function (comm, pin, errorCallback) {
  395.     var verifier = { oauth_verifier: $.trim(pin) };
  396.     comm.post("https://twitter.com/oauth/access_token", verifier, function (data) {
  397.       var d = data.split("&");
  398.       comm.tokenAccess(d[0].split("=")[1]);
  399.       comm.tokenSecret(d[1].split("=")[1]);
  400.       comm.userId(d[2].split("=")[1]);
  401.       comm.username(d[3].split("=")[1]);
  402.       APP.settings.tokenAccess(comm.tokenAccess());
  403.       APP.settings.tokenSecret(comm.tokenSecret());
  404.       APP.settings.userId(comm.userId());
  405.       APP.settings.username(comm.username());
  406.       $("#container").trigger("accessTokenReceived");
  407.     }, null, errorCallback);
  408.   },
  409.  
  410.   getHome: function (model, more) {
  411.     var params = { count: 25 }, max_id;
  412.     if (more) { max_id = model.getOldestId("home"); if (max_id) { params.max_id = max_id; } }
  413.     else { params.since_id = model.sinceIdHome || 1; }
  414.     model.comm.get("https://api.twitter.com/1.1/statuses/home_timeline.json", params, function (data) { model.updateHome(data, more); });
  415.   },
  416.  
  417.   getMentions: function (model, more) {
  418.     var params = { count: 15 }, max_id;
  419.     if (more) { max_id = model.getOldestId("mentions"); if (max_id) { params.max_id = max_id; } }
  420.     else { params.since_id = model.sinceIdMentions || 1; }
  421.     model.comm.get("https://api.twitter.com/1.1/statuses/mentions_timeline.json", params, function (data) { model.updateMentions(data, more); });
  422.   },
  423.  
  424.   getMessages: function (model, more) {
  425.     var params = { count: 15 }, max_id;
  426.     if (more) { max_id = model.getOldestId("messages"); if (max_id) { params.max_id = max_id; } }
  427.     else { params.since_id = model.sinceIdMessages || 1; }
  428.     model.comm.get("https://api.twitter.com/1.1/direct_messages.json", params, function (data) { model.updateMessages(data, more); });
  429.   },
  430.  
  431.   getMessagesSent: function (model, more) {
  432.     var params = { count: 15 };
  433.     model.comm.get("https://api.twitter.com/1.1/direct_messages/sent.json", params, function (data) { model.updateMessages(data, more); });
  434.   },
  435.  
  436.   getFavorites: function (model, more) {
  437.     var params = { count: 100 }, max_id;
  438.     if (more) { max_id = model.getOldestId("favorites"); if (max_id) { params.max_id = max_id; } }
  439.     model.comm.get("https://api.twitter.com/1.1/favorites/list.json", params, function (data) { model.updateFavorites(data, more); });
  440.   },
  441.  
  442.   updateStatus: function (model, message, inReplyToId, callback) {
  443.     try {
  444.       var params = { status: message };
  445.       if (inReplyToId) { params.in_reply_to_status_id = inReplyToId; }
  446.       model.comm.post("https://api.twitter.com/1.1/statuses/update.json", params, function () {
  447.         if (callback) { callback(); }
  448.         APP.UTILITY.showStatus(APP.locale.status_updated || "Status Updated");
  449.       });
  450.     }
  451.     catch (e) {
  452.       System.Diagnostics.EventLog.writeEntry(e.toString());
  453.       $("#content").trigger("internalError");
  454.     }
  455.   },
  456.  
  457.   search: function (model, params, completeCallback) {
  458.     model.comm.get("https://api.twitter.com/1.1/search/tweets.json", params, model.updateSearch, completeCallback);
  459.   },
  460.  
  461.   retweetedBy: function (model, tweet) {
  462.     model.comm.get("https://api.twitter.com/1.1/statuses/retweets/{0}.json".format(tweet.retweeted_status_id),
  463.       { count: 100 }, function (data) {
  464.         var count = Math.max(0, data.length - 1);
  465.         model.retweets[tweet.id] = 
  466.           (APP.locale.retweeted_by_format || " {5} {0} {1}{2}{3}{4}").format(
  467.           APP.UTILITY.author(tweet.retweeted_by, ""),
  468.             count ? (APP.locale.retweeted_by_arg_1 || "+") : "",
  469.             count ? count : "",
  470.             count === 1 ? (APP.locale.retweeted_by_arg_3 || "") : "",
  471.             count > 1 ? (APP.locale.retweeted_by_arg_4 || "") : "",
  472.             APP.locale.retweeted_by || "RT by");
  473.       });
  474.   },
  475.  
  476.   createFavorite: function (model, id) {
  477.     model.comm.post("https://api.twitter.com/1.1/favorites/create/json", {"id": id}, function () {
  478.       APP.UTILITY.showStatus(APP.locale.favorites_updated || "Favorites Updated");
  479.       APP.twitter.getFavorites(model);
  480.     });
  481.   },
  482.  
  483.   deleteFavorite: function (model, id) {
  484.     model.comm.post("https://api.twitter.com/1.1/favorites/destroy.json", {"id": id}, function () {
  485.       setTimeout(function () { model.removeFavorite(id); }, 500);
  486.     });
  487.   },
  488.  
  489.   deleteTweet: function (model, id) {
  490.     model.comm.post("https://api.twitter.com/1.1/statuses/destroy/" + id + ".json", null, function () {
  491.       model.removeStatus(id);
  492.     });
  493.   },
  494.  
  495.   sendMessage: function (model, screen_name, message, success, error) {
  496.     try {
  497.       var params = { screen_name: screen_name, text: message };
  498.       model.comm.post("https://api.twitter.com/1/direct_messages/new.json", params, function () {
  499.         if (success) { success(); }
  500.         APP.UTILITY.showStatus(APP.locale.message_sent || "Message Sent");
  501.         setTimeout(function () { APP.twitter.getMessagesSent(model); }, 500);
  502.       }, undefined, error);
  503.     }
  504.     catch (e) {
  505.       System.Diagnostics.EventLog.writeEntry(e.toString());
  506.       $("#content").trigger("internalError");
  507.     }
  508.   },
  509.  
  510.   retweet: function (model, id) {
  511.     model.comm.post("https://api.twitter.com/1.1/statuses/retweet/" + id + ".json", null, function () {
  512.       APP.UTILITY.showStatus(APP.locale.retweet_sent || "Retweet Sent");
  513.     });
  514.   },
  515.  
  516.   getStatus: function (model, id, callback) {
  517.     model.comm.get("https://api.twitter.com/1.1/statuses/show.json", {"id": id}, callback);
  518.   },
  519.  
  520.   getUserInfo: function (model, screenName, successCallback, errorCallback) {
  521.     model.comm.get("https://api.twitter.com/1.1/users/show.json", {"screen_name": screenName}, successCallback, null, errorCallback);
  522.   },
  523.  
  524.   deleteFriendship: function (model, screenName, callback) {
  525.     model.comm.post("https://api.twitter.com/1.1/friendships/destroy.json", { "screen_name": screenName }, callback);
  526.   },
  527.  
  528.   createFriendship: function (model, screenName, callback) {
  529.     model.comm.post("https://api.twitter.com/1.1/friendships/create.json", { "screen_name": screenName }, callback);
  530.   }
  531. };
  532.  
  533. APP.title = function () {
  534.   var t =
  535.     '<div id="title" title="{0}"><span id="comm_indicator">•</span>' +
  536.     '<span id="lock_indicator" style="display:none"/></div>';
  537.   return t.format(APP.locale.compose_tweet || "Compose tweet (ctrl+S)");
  538. };
  539.  
  540. APP.tabs = function () {
  541.   var locale = APP.locale;
  542.   function tab(id, title) {
  543.     return '<div id="' + id + '" title="' + title + '"/>';
  544.   }
  545.   return '<div id="tabs">' +
  546.     tab("tab_all", locale.tab_all_tooltip || "Unified:Home/Mentions/Messages") +
  547.     tab("tab_home", locale.tab_home_tooltip || "Home") +
  548.     tab("tab_mentions", locale.tab_mentions_tooltip || "Mentions") +
  549.     tab("tab_messages", locale.tab_messages_tooltip || "Messages") +
  550.     tab("tab_search", locale.tab_search_tooltip || "Search") +
  551.     tab("tab_favorites", locale.tab_favorites_tooltip || "Favorites") +
  552.     '</div>';
  553. };
  554.  
  555. APP.edit = function (model) {
  556.   var that = {}, action, tweet, inReplyToId, toScreenName, dlg, title, textarea, counter;
  557.  
  558.   function countChars() {
  559.     var count = textarea.val().length;
  560.     counter.text(count);
  561.     counter.removeClass("counter_ok counter_near counter_over");
  562.     counter.addClass((count > 135) ? ((count > 140) ? "counter_over" : "counter_near") : "counter_ok");
  563.   }
  564.  
  565.   function sendNow() {
  566.     var text = textarea.val(),
  567.         length = text.length,
  568.         successCallback,
  569.         errorCallback;
  570.     if (length <= 0 || length > 140) { textarea.focus(); return; }
  571.     successCallback = function () { that.hide(); textarea.empty(); model.locked(false); };
  572.     errorCallback = function (xhr) { title.text($.parseJSON(xhr.responseText).error); };
  573.     switch (action) {
  574.       case "tweet": APP.twitter.updateStatus(model, text, undefined, successCallback, errorCallback); break;
  575.       case "reply_command": APP.twitter.updateStatus(model, text, inReplyToId, successCallback, errorCallback); break;
  576.       case "reply_all_command": APP.twitter.updateStatus(model, text, inReplyToId, successCallback, errorCallback); break;
  577.       case "message_command": APP.twitter.sendMessage(model, toScreenName, text, successCallback, errorCallback); break;
  578.       case "retweet_command": APP.twitter.updateStatus(model, text, undefined, successCallback, errorCallback); break;
  579.     }
  580.     title.text(APP.locale.send_now_sending || "Sending...").pulse();
  581.   }
  582.  
  583.   function shortenUrl(url) {
  584.     if (url.contains('http://is.gd') === false) {
  585.       url = $.trim(url);
  586.       $.get("http://is.gd/api.php", { longurl: url }, function (shortUrl) {
  587.         textarea.val(textarea.val().replace(url, shortUrl));
  588.         countChars();
  589.       });
  590.     }
  591.   }
  592.  
  593.   function shortenUrls() {
  594.     var length,
  595.         text = textarea.val(),
  596.         matches = text.match(String.regexUrl);
  597.     if (matches) {
  598.       length = matches.length;
  599.       while (length--) { shortenUrl(matches[length]); }
  600.     }
  601.   }
  602.  
  603.   function buildDialog() {
  604.     var shorten, cancel, send, actions, button;
  605.     title = $("<div>", { text: APP.locale.edit_whats_happening || "What's happening?" }).bind("mousedown", that.hide);
  606.     textarea = $("<textarea>").bind("keyup", function (e) {
  607.       var sKey = 83, kKey = 75;
  608.       if (!e.shiftKey && !e.altKey && e.ctrlKey && e.keyCode === sKey) { sendNow(); }
  609.       else if (!e.shiftKey && !e.altKey && e.ctrlKey && e.keyCode === kKey) { shortenUrls(); }
  610.       else { countChars(); }
  611.       return false;
  612.     });
  613.     counter = $("<span>", { id: "counter", text: 0 });
  614.     button = function (id, txt, tip, cmd) {
  615.       return $("<span>", { id: id, text: txt, title: tip, "class": "hover" }).bind("mousedown", cmd);
  616.     };
  617.     shorten = button("shorten", APP.locale.edit_shorten || "Shorten", APP.locale.edit_shorten_tooltip || "Shorten urls (ctrl+K)", shortenUrls);
  618.     cancel = button("cancel", APP.locale.edit_cancel || "Cancel", "Esc", that.hide);
  619.     send = button("send", APP.locale.edit_send || "Send", "ctrl+S", sendNow);
  620.     actions = $("<div>", { css: { "text-align": "right"} }).append(counter, shorten, cancel, send);
  621.     return $("<div>", { id: "edit" }).append(title, textarea, actions).bind("keydown", function (e) {
  622.       var escapeKey = 27;
  623.       if (e.keyCode === escapeKey) {
  624.         that.hide();
  625.         return false;
  626.       }
  627.     });
  628.   }
  629.  
  630.   that.dialog = function () {
  631.     return (dlg = dlg || buildDialog());
  632.   };
  633.  
  634.   that.show = function (actionArg, tweetArg) {
  635.     action = actionArg || "tweet";
  636.     tweet = tweetArg || { screen_name: "" };
  637.     switch (action) {
  638.       case "reply_command":
  639.         title.text("@" + tweet.screen_name);
  640.         textarea.val("@" + tweet.screen_name + " ");
  641.         inReplyToId = tweet.id;
  642.         break;
  643.       case "reply_all_command":
  644.         title.text("@" + tweet.screen_name);
  645.         textarea.val("@{0} {1} ".format(tweet.screen_name, function () {
  646.           var names = tweet.text.match(String.regexScreenName);
  647.           return names ? names.unique().join(" ") : "";
  648.         } ()));
  649.         inReplyToId = tweet.id;
  650.         break;
  651.       case "message_command":
  652.         title.text((APP.locale.edit_message_to || "Message to:") + " " + tweet.screen_name);
  653.         toScreenName = tweet.screen_name;
  654.         break;
  655.       case "retweet_command":
  656.         title.text((APP.locale.edit_retweet || "Retweet") + " " + tweet.screen_name);
  657.         textarea.val("RT @{0} {1}".format(tweet.screen_name, tweet.text));
  658.         break;
  659.       default:
  660.         title.text(APP.locale.edit_whats_happening || "What's happening?");
  661.         break;
  662.     }
  663.     countChars();
  664.     dlg.show();
  665.     setTimeout(function () { textarea.focus().setCaretPos(1000); }, 100);
  666.   };
  667.  
  668.   that.hide = function () {
  669.     dlg.hide();
  670.     textarea.empty();
  671.     $("#content").focus();
  672.   };
  673.  
  674.   that.toggle = function (actionArg, tweetArg) {
  675.     if (dlg.is(":hidden")) { that.show(actionArg, tweetArg); }
  676.     else { that.hide(); }
  677.   };
  678.  
  679.   return that;
  680. };
  681.  
  682. APP.FORM = {
  683.   login: function () {
  684.     var that = {};
  685.  
  686.     that.render = function (model) {
  687.       var content =
  688.         '<div id="login_container" style="height:400px;">' +
  689.         '<div id="login_spacer" style="margin-top:2pc; margin-left:13;">' +
  690.         '<div id="login_pin_button"><button id="get_pin" style="width:100;">{0}</button></div><br/>' +
  691.         '<div id="login_pin_text"><input id="pin_text" value="{1}" class="blur" type="text" style="width:90; margin-left:2;"/></div><br/>' +
  692.         '<div id="login_login_button"><button id="login" style="width:100;" disabled="disabled">{2}</button><br/></div><br/>' +
  693.         '<p id="login_help_link"><a href="http://blueonionsoftware.com/tweetz-help.aspx">{3}</a></p>' +
  694.         '<p id="pin_error"></p>' +
  695.         '</div></div>';
  696.       return content.format(
  697.         APP.locale.login_get_pin || "Get PIN",
  698.         APP.locale.login_enter_pin || "Enter PIN here",
  699.         APP.locale.login_login || "Login",
  700.         APP.locale.settings_help || "Help");
  701.     };
  702.  
  703.     that.onSize = function () {
  704.       var docked = System.Gadget.docked,
  705.           height = docked ? APP.settings.dockedHeight() : APP.settings.undockedHeight();
  706.       $(document.body, "#container").css({ "width": docked ? "130px" : "300px", "height": height });
  707.     };
  708.  
  709.     that.selectTab = function () { };
  710.     return that;
  711.   },
  712.  
  713.   formBase: (function () {
  714.     var that = {}, locale, author, tabs, title;
  715.  
  716.     function contentHeight(height) {
  717.       return height -
  718.         $("#header").outerHeight() -
  719.         $("#search").outerHeight() - 2;
  720.     }
  721.  
  722.     function setFontSizes() {
  723.       var fontClass;
  724.       switch (APP.settings.fontSize()) {
  725.         case "small": fontClass = "small_font"; break;
  726.         case "large": fontClass = "large_font"; break;
  727.         default: fontClass = "medium_font"; break;
  728.       }
  729.       $("#content, #status, #tip, #ask, #edit, #more, #panel")
  730.         .removeClass("small_font medium_font large_font")
  731.         .addClass(fontClass);
  732.     }
  733.  
  734.     that.onSize = function () {
  735.       var docked = System.Gadget.docked,
  736.           height = docked ? APP.settings.dockedHeight() : APP.settings.undockedHeight();
  737.       $("#base_style_sheet").loadStyleSheet("css/base.css");
  738.       $("#style_sheet").loadStyleSheet(APP.settings.styleSheet());
  739.       $(document.body).css({ width: (docked ? 130 : 300), "height": height });
  740.       $("#container").css({ "height": height - 2 });
  741.       $("#container, #edit").classAddOrRemove("docked", docked);
  742.       $("#content").css({ "height": contentHeight(height) });
  743.       setFontSizes();
  744.     };
  745.  
  746.     function timeNumUnit(format, num, unit) {
  747.       var f = format.replace('{0}', '<span class="time_num">{0}</span>').replace(
  748.         '{1}', '<span class="time_unit">{1}</span>');
  749.       return f.format(num, unit);
  750.     }
  751.  
  752.     function relativeTime(date) {
  753.       var diff = ((new Date()).getTime() - date.getTime()) / 1000;
  754.       return true && (
  755.         diff < 20 && (locale.time_seconds_ago || "seconds ago") ||
  756.         diff < 120 && timeNumUnit(locale.time_minute_ago || "{0}{1}", 1, locale.time_minute_unit || "m") ||
  757.         diff < 3600 && timeNumUnit(locale.time_minutes_ago || "{0}{1}", Math.floor(diff / 60), locale.time_minutes_unit || "m") ||
  758.         diff < 7200 && timeNumUnit(locale.time_hour_ago || "h", 1, locale.time_hour_unit || "h") ||
  759.         diff < 86400 && timeNumUnit(locale.time_hours_ago || "{0}{1}", Math.floor(diff / 3600), locale.time_hours_unit || "h") ||
  760.         Math.floor(diff / 86400) === 1 && (locale.time_yesterday || "yesterday") || "");
  761.     }
  762.  
  763.     function timeStamp(time) {
  764.       var minutes = time.getMinutes();
  765.       if (minutes < 10) { minutes = "0" + minutes; }
  766.       return relativeTime(time) ||
  767.         "{0} {1}, {2} {3}:{4} {5}".format(
  768.         (locale.months || ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"])[time.getMonth()],
  769.         time.getDate(),
  770.         time.getYear(),
  771.         (time.getHours() % 12) || "12", minutes,
  772.         (time.getHours() < 12) ? (locale.time_am || "am") : (locale.time_pm || "pm"));
  773.     }
  774.  
  775.     that.updateTimeStamps = function (model) {
  776.       $(".tweet_time").each(function () {
  777.         var ts,
  778.             stamp = $(this),
  779.             id = stamp.closest(".tweet").attr("id"),
  780.             tweet = model.getTweet(id);
  781.         if (tweet) {
  782.           ts = timeStamp(tweet.created_at);
  783.           if (stamp.html() !== ts) { stamp.html(ts); }
  784.         }
  785.       });
  786.     };
  787.  
  788.     function inReplyTo(tweet) {
  789.       if (tweet.in_reply_to_screen_name) {
  790.         return '<span class="inreplyto_label">{2}</span> <span name="{0}" class="inreplyto">{1}</span>'.format(
  791.         tweet.in_reply_to_status_id, tweet.in_reply_to_screen_name, locale.foot_in_reply_to || "in reply to");
  792.       }
  793.       return "";
  794.     }
  795.  
  796.     that.assembleForm = function (model, content) {
  797.       return '<div id="header">{0}{1}</div>'.format(title, tabs) + content;
  798.     };
  799.  
  800.     that.selected = (function () {
  801.       var selectedId = "";
  802.       return function (id) {
  803.         if (id !== undefined) { selectedId = id; }
  804.         return selectedId;
  805.       };
  806.     })();
  807.  
  808.     that.renderTweet = function (model, tweet) {
  809.       var classNames, htmlFormat;
  810.       classNames = "tweet" +
  811.         ((tweet.id.toString() === that.selected().toString()) ? " selected_tweet" : "") +
  812.         ((tweet.timelines.contains("mentions")) ? " mention" : "") +
  813.         ((tweet.timelines.contains("messages")) ? " message" : "") +
  814.         ((tweet.retweeted_status_id) ? " retweet" : "");
  815.       htmlFormat =
  816.         '<div id="{0}" class="{1}">' +
  817.           '<div class="tweet_inner">' +
  818.             '<img id="p{0}" src="{2}" class="pic"/>' +
  819.             '<div class="tweet_content">{3}{4}{5}' +
  820.                 '<div class="tweet_text">{6}</div>' +
  821.             '</div>' +
  822.             '<div class="tweet_info">' +
  823.               '<span class="tweet_time">{7}</span>' +
  824.               '<span class="tweet_source">' +
  825.                 '<span class="source_label">{8}</span>' +
  826.                 '<span class="source">{9}</span> ' +
  827.               '</span>' +
  828.               '<span class="{10}">{11}</span>' +
  829.               '<span class="{12}">•</span>' +
  830.             '</div>' +
  831.           '</div>' +
  832.         '</div>';
  833.       if (!tweet.html) {
  834.         tweet.html = tweet.text.htmlEntities().atScreenNames().urlsToLinks(APP.settings.showLinks()).hashTags();
  835.       }
  836.       return htmlFormat.format(
  837.         tweet.id,
  838.         classNames,
  839.         tweet.profile_image_url,
  840.         (tweet.timelines.contains("messages")) ? '<img src="images/invisible.gif" class="msg"/>' : "",
  841.         (tweet.retweeted_status_id) ? '<img src="images/invisible.gif" class="retweet"/>' : "",
  842.         author(tweet.name, tweet.screen_name),
  843.         tweet.html,
  844.         timeStamp(tweet.created_at),
  845.         tweet.source ? (locale.foot_via || "via") : "",
  846.         tweet.source || "",
  847.         (model.retweets[tweet.id] && "tweet_retweet") || (inReplyTo(tweet) && "tweet_inreplyto"),
  848.         model.retweets[tweet.id] || inReplyTo(tweet),
  849.         model.unread[tweet.id] ? " unread" : "read");
  850.     };
  851.  
  852.     that.render = function (model) {
  853.       var tweets = this.getTweets(model), length = tweets.length, content = "", i, t = that;
  854.       if (!locale) { locale = APP.locale; }
  855.       if (!author) { author = APP.UTILITY.author; }
  856.       if (!tabs) { tabs = APP.tabs(); }
  857.       if (!title) { title = APP.title(); }
  858.       model.locked(false);
  859.       for (i = 0; i < length; ++i) { content += t.renderTweet(model, tweets[i]); }
  860.       if (content.length) { content += this.more(); }
  861.       return this.assembleForm(model, '<div id="content">' + content + '</div>');
  862.     };
  863.  
  864.     that.more = function () {
  865.       return '<div id="more">' + (locale.foot_more || "more") + '</div>';
  866.     };
  867.  
  868.     return that;
  869.   } ()),
  870.  
  871.   all: function () {
  872.     var that = Object.create(APP.FORM.formBase);
  873.     that.tabId = "tab_all";
  874.     that.getTweets = function (model) { return model.getAllTweets(); };
  875.     that.more = function () { return ""; };
  876.     return that;
  877.   },
  878.  
  879.   home: function () {
  880.     var that = Object.create(APP.FORM.formBase);
  881.     that.tabId = "tab_home";
  882.     that.getTweets = function (model) { return model.getHomeTweets(); };
  883.     return that;
  884.   },
  885.  
  886.   search: function () {
  887.     var that = Object.create(APP.FORM.formBase);
  888.     that.tabId = "tab_search";
  889.     that.getTweets = function (model) { return model.getSearchTweets(); };
  890.     that.assembleForm = function (model, content) {
  891.       var div = '<div id="search"><input id="search_query" value="{0}"/><img id="search_button" src="/images/tab/accept.png"/></div>',
  892.           textarea = div.format(model.lastSearch);
  893.       return '<div id="header">{0}{1}</div>'.format(APP.title(), APP.tabs()) + textarea + content;
  894.     };
  895.     return that;
  896.   },
  897.  
  898.   favorites: function () {
  899.     var that = Object.create(APP.FORM.formBase);
  900.     that.tabId = "tab_favorites";
  901.     that.getTweets = function (model) { return model.getFavorites(); };
  902.     that.more = function () { return ""; };
  903.     return that;
  904.   },
  905.  
  906.   mentions: function () {
  907.     var that = Object.create(APP.FORM.formBase);
  908.     that.tabId = "tab_mentions";
  909.     that.getTweets = function (model) { return model.getMentions(); };
  910.     return that;
  911.   },
  912.  
  913.   messages: function () {
  914.     var that = Object.create(APP.FORM.formBase);
  915.     that.tabId = "tab_messages";
  916.     that.getTweets = function (model) { return model.getMessages(); };
  917.     return that;
  918.   }
  919. };
  920.  
  921. APP.model = function (comm) {
  922.     var that = {},
  923.       statuses = {},
  924.       searches = {},
  925.       pendingUpdateEvents = {};
  926.  
  927.     that.comm = comm;
  928.     that.links = {};
  929.     that.unread = {};
  930.     that.retweets = {};
  931.     that.sinceIdHome = "1";
  932.     that.sinceIdMentions = "1";
  933.     that.sinceIdMessages = "1";
  934.     that.lastSearch = "";
  935.  
  936.     that.readTweet = function (data, timeline) {
  937.         try {
  938.             var tweet = {},
  939.             user = data.user || (data.sender_id_str === comm.userId() ? data.recipient : data.sender);
  940.             tweet.id = data.id_str;
  941.             tweet.screen_name = (user) ? user.screen_name : data.from_user;
  942.             tweet.name = (user) ? user.name : data.from_user;
  943.             tweet.profile_image_url = (user) ? user.profile_image_url : data.profile_image_url;
  944.             tweet.text = data.text.htmlDecode();
  945.             tweet.created_at = new Date((data.created_at || "").replace("+0000", "GMT"));
  946.             tweet.favorited = data.favorited;
  947.             tweet.source = (data.from_user) ? data.source.htmlDecode() : data.source;
  948.             tweet.in_reply_to_screen_name = data.in_reply_to_screen_name;
  949.             tweet.in_reply_to_status_id = data.in_reply_to_status_id_str;
  950.             tweet.retweeted_status_id = data.retweeted_status && data.retweeted_status.id_str;
  951.             if (tweet.retweeted_status_id) {
  952.                 tweet.text = data.retweeted_status.text;
  953.                 tweet.retweeted_by = tweet.screen_name;
  954.                 tweet.screen_name = data.retweeted_status.user.screen_name;
  955.                 tweet.name = data.retweeted_status.user.name;
  956.                 tweet.profile_image_url = data.retweeted_status.user.profile_image_url;
  957.                 that.retweets[tweet.id] = (
  958.           '<span class="tweet_retweet">' +
  959.           '<span class="retweet_label">RT by</span> ' +
  960.           '<span class="retweet_person">{0}</span>' +
  961.           '</span>').format(APP.UTILITY.author(tweet.retweeted_by, ""));
  962.             }
  963.             tweet.timelines = timeline || "";
  964.             return tweet;
  965.         }
  966.         catch (e) {
  967.             return null;
  968.         }
  969.     };
  970.  
  971.     function asArraySortedByTime(obj) {
  972.         var items = [];
  973.         $.each(obj, function () { items.push(this); });
  974.         items.sort(function (a, b) { return b.created_at.getTime() - a.created_at.getTime(); });
  975.         return items;
  976.     }
  977.  
  978.     function filterBy(timeline) {
  979.         var items = {};
  980.         $.each(statuses, function () {
  981.             if (this.timelines.contains(timeline)) {
  982.                 items[this.id] = this;
  983.             }
  984.         });
  985.         return items;
  986.     }
  987.  
  988.     function purgeOldTweetsFromTimeline(timeline) {
  989.         var i,
  990.         item,
  991.         items = asArraySortedByTime(filterBy(timeline)),
  992.         length = items.length;
  993.         for (i = 26; i < length; i += 1) {
  994.             item = items[i];
  995.             item.timelines = item.timelines.replace(timeline, "");
  996.             if (item.timelines.isBlank()) {
  997.                 delete statuses[item.id];
  998.                 $(item.id).remove();
  999.             }
  1000.         }
  1001.     }
  1002.  
  1003.     function purgeOldTweetsFromAllTimelines() {
  1004.         purgeOldTweetsFromTimeline("home");
  1005.         purgeOldTweetsFromTimeline("mentions");
  1006.         purgeOldTweetsFromTimeline("messages");
  1007.     }
  1008.  
  1009.     that.fireUpdateEvent = function (updateEvent, params) {
  1010.         if (pendingUpdateEvents[updateEvent]) { clearTimeout(pendingUpdateEvents[updateEvent]); }
  1011.         pendingUpdateEvents[updateEvent] = setTimeout(function () {
  1012.             pendingUpdateEvents[updateEvent] = null;
  1013.             $("#container").trigger(updateEvent, params);
  1014.         }, 2000);
  1015.     };
  1016.  
  1017.     that.locked = (function () {
  1018.         var locked = false;
  1019.         return function (lock) {
  1020.             if (lock === undefined) { return locked; }
  1021.             locked = lock;
  1022.             $("#content").trigger("lockIndicator", [locked]);
  1023.         };
  1024.     })();
  1025.  
  1026.     that.isMention = (function () {
  1027.         var nameExp;
  1028.         return function (tweet) {
  1029.             if (!nameExp) { nameExp = new RegExp("([^a-z0-9_]|^)(@|\\uff20)" + comm.username(), "i"); }
  1030.             return nameExp.test(tweet.text.htmlEntities());
  1031.         };
  1032.     } ());
  1033.  
  1034.     function update(store, data, timeline, updateEvent, more) {
  1035.         var updated = false, sinceId = "0", forceChirp = false, maxId = APP.UTILITY.maxId;
  1036.         $.each(data.results || data, function () {
  1037.             var tweet = that.readTweet(this, timeline), matches;
  1038.             if (tweet) {
  1039.                 if (store[tweet.id] === undefined) {
  1040.                     store[tweet.id] = tweet;
  1041.                     tweet.html = tweet.text.htmlEntities().atScreenNames().urlsToLinks(APP.settings.showLinks()).hashTags();
  1042.                     matches = tweet.html.findUrls();
  1043.                     if (matches) { $.each(matches, function (idx, val) { APP.UTILITY.reverseLookup(that.links, val); }); }
  1044.                     if (tweet.retweeted_status_id) { APP.twitter.retweetedBy(that, tweet); }
  1045.                     if (!more) { that.unread[tweet.id] = true; }
  1046.                     sinceId = maxId(tweet.id, sinceId);
  1047.                     updated = true;
  1048.                 }
  1049.                 tweet = store[tweet.id];
  1050.                 if (tweet.timelines.contains(timeline) === false) {
  1051.                     tweet.timelines += timeline + " ";
  1052.                     updated = true;
  1053.                 }
  1054.                 if (tweet.timelines.contains("mentions") === false && that.isMention(tweet)) {
  1055.                     tweet.timelines += "mentions ";
  1056.                     if (APP.settings.chirpOnMention()) { forceChirp = true; }
  1057.                 }
  1058.             }
  1059.         });
  1060.         if (updated) {
  1061.             switch (timeline) {
  1062.                 case "home":
  1063.                     that.sinceIdHome = maxId(sinceId, that.sinceIdHome);
  1064.                     if (APP.settings.chirpOnStatus() || forceChirp) { that.fireUpdateEvent("chirp"); }
  1065.                     break;
  1066.                 case "mentions":
  1067.                     that.sinceIdMentions = maxId(sinceId, that.sinceIdMentions);
  1068.                     if (APP.settings.chirpOnMention() || forceChirp) { that.fireUpdateEvent("chirp"); }
  1069.                     break;
  1070.                 case "messages":
  1071.                     that.sinceIdMessages = maxId(sinceId, that.sinceIdMessages);
  1072.                     if (APP.settings.chirpOnMessage() || forceChirp) { that.fireUpdateEvent("chirp"); }
  1073.                     break;
  1074.                 case "search":
  1075.                     break;
  1076.             }
  1077.             if (!that.locked() || timeline === "search" || more) { setTimeout(function () { that.fireUpdateEvent(updateEvent, [more], 2000); }); }
  1078.             if (!that.locked()) { purgeOldTweetsFromAllTimelines(); }
  1079.         }
  1080.     }
  1081.  
  1082.     that.updateHome = function (data, more) { update(statuses, data, "home", "homeUpdated", more); };
  1083.     that.updateMentions = function (data, more) { update(statuses, data, "mentions", "mentionsUpdated", more); };
  1084.     that.updateMessages = function (data, more) { update(statuses, data, "messages", "messagesUpdated", more); };
  1085.     that.updateFavorites = function (data, more) { update(statuses, data, "favorites", "favoritesUpdated", more); };
  1086.     that.updateSearch = function (data, more) { update(searches, data.statuses, "search", "searchUpdated", more); };
  1087.  
  1088.     that.clearTweetHtml = function () {
  1089.         $.each(statuses, function () { delete this.html; });
  1090.         $.each(searches, function () { delete this.html; });
  1091.     };
  1092.  
  1093.     that.getOldestId = function (timeline) {
  1094.         var t = asArraySortedByTime(filterBy(timeline)),
  1095.         l = t.length;
  1096.         return l ? t[l - 1].id : undefined;
  1097.     };
  1098.  
  1099.     that.getAllTweets = function () { return asArraySortedByTime(statuses); };
  1100.     that.getHomeTweets = function () { return asArraySortedByTime(filterBy("home")); };
  1101.     that.getSearchTweets = function () { return asArraySortedByTime(searches); };
  1102.     that.getFavorites = function () { return asArraySortedByTime(filterBy("favorites")); };
  1103.     that.getMentions = function () { return asArraySortedByTime(filterBy("mentions")); };
  1104.     that.getMessages = function () { return asArraySortedByTime(filterBy("messages")); };
  1105.     that.getTweet = function (id) { return statuses[id] || searches[id]; };
  1106.     that.clearSearches = function () { searches = {}; that.oldestIdSearch = 1; };
  1107.  
  1108.     function whereRetweetStatusId(items, id) {
  1109.         var tweet;
  1110.         $.each(items, function () {
  1111.             if (this.retweeted_status_id === id) { tweet = this; return false; }
  1112.         });
  1113.         return tweet;
  1114.     }
  1115.  
  1116.     that.findRetweetedTweet = function (id) {
  1117.         return whereRetweetStatusId(statuses, id) || that.getTweet(id);
  1118.     };
  1119.  
  1120.     that.removeStatus = function (id) {
  1121.         if (statuses[id]) {
  1122.             delete statuses[id];
  1123.             that.fireUpdateEvent("homeUpdated", [true]);
  1124.         }
  1125.     };
  1126.  
  1127.     that.removeFavorite = function (id) {
  1128.         if (statuses[id]) {
  1129.             statuses[id].favorited = false;
  1130.             statuses[id].timelines = statuses[id].timelines.replace("favorites", "");
  1131.             that.fireUpdateEvent("favoriteDeleted", [id]);
  1132.         }
  1133.     };
  1134.  
  1135.     return that;
  1136. };
  1137.  
  1138. APP.view = function (model) {
  1139.   var that = {},
  1140.       currentFormName = "home",
  1141.       controller = APP.controller(model, that),
  1142.       forms,
  1143.       showFormMap;
  1144.  
  1145.   forms = {
  1146.     login: APP.FORM.login(),
  1147.     all: APP.FORM.all(),
  1148.     home: APP.FORM.home(),
  1149.     search: APP.FORM.search(),
  1150.     favorites: APP.FORM.favorites(),
  1151.     mentions: APP.FORM.mentions(),
  1152.     messages: APP.FORM.messages()
  1153.   };
  1154.  
  1155.   $(document.body).append('<div id="status"/>');
  1156.  
  1157.   function panel() {
  1158.     var item = function (text, shortcut, commandId) {
  1159.       return $("<tr>", { id: commandId })
  1160.           .bind("mousedown", function () { $("#panel").trigger(commandId); })
  1161.           .append($("<td>", { text: text }), $("<td>", { text: shortcut }));
  1162.     },
  1163.     separator = function () {
  1164.       return $("<tr>", { "class": "panelseparator" }).append($("<td>", { "colspan": "2" }));
  1165.     };
  1166.     return $("<table>", { id: "panel" }).append(
  1167.         item(APP.locale.panel_reply || "Reply", APP.locale.panel_reply_shortcut || "R", "reply_command"),
  1168.         item(APP.locale.panel_reply_all || "Reply All", APP.locale.panel_reply_all_shortcut || "Shift+R", "reply_all_command"),
  1169.         item(APP.locale.panel_message || "Message", APP.locale.panel_message_shortcut || "M", "message_command"),
  1170.         item(APP.locale.panel_retweet || "RT (>)...", APP.locale.panel_retweet_shortcut || "T", "retweet_command"),
  1171.         item(APP.locale.panel_retweet_api || "Retweet", APP.locale.panel_retweet_api_shortcut || "Shift+T", "native_retweet_command"),
  1172.         separator(),
  1173.         item(APP.locale.panel_favorite || "Favorite", APP.locale.panel_favorite_shortcut || "F", "favorite_command"),
  1174.         item(APP.locale.panel_delete || "Delete", "", "delete_command"),
  1175.         separator(),
  1176.         item(APP.locale.panel_browser || "Browser", "", "open_in_browser_command"),
  1177.         item(APP.locale.panel_copy || "Copy", APP.locale.panel_copy_shortcut || "Ctrl+C", "copy_command")
  1178.         );
  1179.   }
  1180.  
  1181.   $("#container").after(panel());
  1182.  
  1183.   that.showStatus = function (status) {
  1184.     var sb, offset;
  1185.     sb = $("#status");
  1186.     offset = $("#content").offset();
  1187.     sb.text(status);
  1188.     sb.css({ "top": offset.top + 2, right: System.Gadget.docked ? "5px" : "21px" });
  1189.     sb.fadeIn("normal", function () { $(this).fadeOut(); }).delay(800);
  1190.   };
  1191.  
  1192.   that.selected = function (id) {
  1193.     var form = forms[currentFormName];
  1194.     if (form && form.selected) {
  1195.       if (id === undefined) { return form.selected(); }
  1196.       form.selected(id);
  1197.     }
  1198.   };
  1199.  
  1200.   that.edit = APP.edit(model);
  1201.   $(document.body).append(that.edit.dialog());
  1202.  
  1203.   showFormMap = {
  1204.     "all": function (more) { that.updateForm("all", more); },
  1205.     "home": function (more) { that.updateForm("home", more); },
  1206.     "mentions": function (more) { that.updateForm("mentions", more); },
  1207.     "messages": function (more) { that.updateForm("messages", more); },
  1208.     "favorites": function (more) { that.updateForm("favorites", more); },
  1209.     "search": function (more) { that.showForm("search", more); }
  1210.   };
  1211.  
  1212.   $("#container").live("homeUpdated mentionsUpdated messagesUpdated favoritesUpdated searchUpdated", function (e, more) {
  1213.     showFormMap[currentFormName](more);
  1214.   });
  1215.  
  1216.   $("#container").live("favoriteDeleted", function (e, id) {
  1217.     if (currentFormName === "favorites") {
  1218.       $("#" + id).remove();
  1219.     }
  1220.   });
  1221.  
  1222.   function setActiveTab(tabId) {
  1223.     $("#tabs > img").removeClass("active");
  1224.     $("#" + tabId).addClass("active");
  1225.   }
  1226.  
  1227.   that.previousForm = function () {
  1228.     var formName;
  1229.     switch (currentFormName) {
  1230.       case "home": formName = "all"; break;
  1231.       case "mentions": formName = "home"; break;
  1232.       case "messages": formName = "mentions"; break;
  1233.       case "search": formName = "messages"; break;
  1234.       case "favorites": formName = "search"; break;
  1235.     }
  1236.     if (formName) { that.showForm(formName); }
  1237.   };
  1238.  
  1239.   that.nextForm = function () {
  1240.     var formName;
  1241.     switch (currentFormName) {
  1242.       case "all": formName = "home"; break;
  1243.       case "home": formName = "mentions"; break;
  1244.       case "mentions": formName = "messages"; break;
  1245.       case "messages": formName = "search"; break;
  1246.       case "search": formName = "favorites"; break;
  1247.     }
  1248.     if (formName) { that.showForm(formName); }
  1249.   };
  1250.  
  1251.   that.onSize = function () { };
  1252.  
  1253.   that.showForm = function (formName, keepScrollTop, callback) {
  1254.     var form, formContent, scrollTop;
  1255.     form = forms[formName];
  1256.     formContent = form.render(model);
  1257.     scrollTop = (keepScrollTop) ? $("#content").scrollTop() : 0;
  1258.     $("#container").empty().html(formContent);
  1259.     setActiveTab(form.tabId);
  1260.     if (keepScrollTop) { $("#content").scrollTop(scrollTop); }
  1261.     currentFormName = formName;
  1262.     System.Gadget.onDock = form.onSize;
  1263.     System.Gadget.onUndock = form.onSize;
  1264.     that.onSize = form.onSize;
  1265.     form.onSize();
  1266.     that.updateCommIndicator();
  1267.     if (callback) { callback(); }
  1268.     $("#content").focus();
  1269.   };
  1270.  
  1271.   that.updateForm = function (formName, more) {
  1272.     var form, content, tweet, updates, formContent;
  1273.     if (more) { that.showForm(formName, more); }
  1274.     updates = [];
  1275.     form = forms[formName];
  1276.     formContent = form.render(model);
  1277.     content = $("<div>").html(formContent);
  1278.     content.find(".tweet").each(function (idx) {
  1279.       var t, id = "#" + this.id;
  1280.       if (idx > 4) {
  1281.         updates.length = 0;
  1282.         that.showForm(formName, more);
  1283.         return false;
  1284.       }
  1285.       if ($(id).length) { return false; }
  1286.       t = $(this).hide();
  1287.       updates.push(t.clone());
  1288.     });
  1289.     content.remove();
  1290.     tweet = updates.pop();
  1291.     while (tweet) {
  1292.       $("#content").prepend(tweet);
  1293.       $(tweet).slideDown("slow");
  1294.       tweet = updates.pop();
  1295.     }
  1296.   };
  1297.  
  1298.   that.updateTimeStamps = function () {
  1299.     if (forms[currentFormName].updateTimeStamps) {
  1300.       forms[currentFormName].updateTimeStamps(model);
  1301.     }
  1302.   };
  1303.  
  1304.   that.getCurrentFormName = function () {
  1305.     return currentFormName;
  1306.   };
  1307.  
  1308.   that.cursor = function (cursor) {
  1309.     $("#container").css("cursor", cursor);
  1310.   };
  1311.  
  1312.   function settingsClosed(e) {
  1313.     if (e.closeAction === e.Action.commit) {
  1314.       model.clearTweetHtml();
  1315.       setTimeout(function () {
  1316.         that.showForm(currentFormName);
  1317.         APP.UTILITY.versionChecker.run();
  1318.       }, 500);
  1319.     }
  1320.   }
  1321.  
  1322.   System.Gadget.settingsUI = "settings.html";
  1323.   System.Gadget.onSettingsClosed = settingsClosed;
  1324.  
  1325.   that.updateCommIndicator = (function () {
  1326.     var timer,
  1327.         lastColor = "#f00",
  1328.         setIndicator = function (color) { $("#comm_indicator").css("color", color); };
  1329.     return function (state) {
  1330.       if (timer) { clearTimeout(timer); timer = null; }
  1331.       if (state === undefined) { setIndicator(lastColor); return; }
  1332.       if (state === "accessing") { lastColor = "#ff0"; setIndicator("#ff0"); return; }
  1333.       if (state === "tooManyRequests") { lastColor = "#00f"; setIndicator("#00f"); return; }
  1334.       lastColor = (state === "success") ? "#0d0" : "#f00";
  1335.       timer = setTimeout(function () { setIndicator(lastColor); }, 1500);
  1336.     };
  1337.   })();
  1338.  
  1339.   that.lockIndicator = function (locked) {
  1340.     var l = $("#lock_indicator");
  1341.     if (locked) { l.show(); }
  1342.     else { l.hide(); }
  1343.   };
  1344.  
  1345.   that.showStatusFlyout = function (tweet) {
  1346.     var form = APP.FORM.home(), renderedTweet = form.renderTweet(model, tweet);
  1347.     APP.statusParams = { tweet: renderedTweet };
  1348.     System.Gadget.Flyout.file = "inreplyto.html";
  1349.     System.Gadget.Flyout.show = true;
  1350.   };
  1351.  
  1352.   that.start = function () {
  1353.     if (APP.settings.username()) { controller.startTimelines(); }
  1354.     else { that.showForm("login"); }
  1355.   };
  1356.  
  1357.   return that;
  1358. };
  1359.  
  1360. APP.controller = function (model, view) {
  1361.     var that = {}, homeTimer, mentionsTimer, messagesTimer, timeStampTimer, switchStyleSheet;
  1362.  
  1363.     function stopTimer(timerId) {
  1364.         if (timerId !== undefined) { clearInterval(timerId); }
  1365.     }
  1366.  
  1367.     that.startTimelines = function () {
  1368.         var start = function (getTimeline, interval, id) {
  1369.             stopTimer(id);
  1370.             getTimeline(model, true);
  1371.             return setInterval(function () { getTimeline(model, false); }, interval * 1000 * 60);
  1372.         };
  1373.         $("#base_style_sheet").loadStyleSheet("css/base.css");
  1374.         $("#style_sheet").loadStyleSheet(APP.settings.styleSheet());
  1375.         view.showForm("all");
  1376.         homeTimer = start(APP.twitter.getHome, APP.settings.intervalHome(), homeTimer);
  1377.         mentionsTimer = start(APP.twitter.getMentions, APP.settings.intervalMentions(), mentionsTimer);
  1378.         messagesTimer = start(APP.twitter.getMessages, APP.settings.intervalMessages(), messagesTimer);
  1379.         APP.twitter.getMessagesSent(model);
  1380.         if (timeStampTimer === undefined) { timeStampTimer = setInterval(view.updateTimeStamps, 60000); }
  1381.         setTimeout(function () { APP.UTILITY.versionChecker.run(); }, 20000);
  1382.     };
  1383.  
  1384.     $("#container").live("accessTokenReceived", function () {
  1385.         that.startTimelines();
  1386.     });
  1387.  
  1388.     $("#title").live("mousedown", function () {
  1389.         view.edit.toggle("tweet");
  1390.     });
  1391.  
  1392.     $("#container").live("keydown", function (e) {
  1393.         var sKey = 83, dKey = 68, source, w;
  1394.         if (e.ctrlKey && e.keyCode === sKey) {
  1395.             view.edit.show("tweet");
  1396.             return false;
  1397.         }
  1398.         if (e.ctrlKey && e.keyCode === dKey) {
  1399.             w = window.open('', 'DOM', '');
  1400.             w.document.write('<html><head><title>Document Dump</title></head><body><pre>');
  1401.             w.document.write('</head><body><pre>');
  1402.             source = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\x0d<html>';
  1403.             source += document.getElementsByTagName("html")[0].innerHTML;
  1404.             source += "\x0d</html>";
  1405.             w.document.write(source.htmlEntities());
  1406.             w.document.write('</pre></body></html>');
  1407.             w.document.close();
  1408.             return false;
  1409.         }
  1410.     });
  1411.  
  1412.     that.commands = {
  1413.         reply: APP.UTILITY.command("reply", function () { }, true),
  1414.         replyAll: APP.UTILITY.command("reply_all", function () { }, true)
  1415.     };
  1416.  
  1417.     function canDelete(id) {
  1418.         var tweet = model.getTweet(id);
  1419.         return (tweet && tweet.screen_name === model.comm.username());
  1420.     }
  1421.  
  1422.     function isFavorite(id) {
  1423.         var tweet = model.getTweet(id);
  1424.         return (tweet && tweet.favorited);
  1425.     }
  1426.  
  1427.     (function () {
  1428.         var timerId;
  1429.         $(".pic").live("mouseenter mouseleave", function (e) {
  1430.             var pic;
  1431.             if (e.type === "mouseenter") {
  1432.                 pic = $(this);
  1433.                 timerId = setTimeout(function () {
  1434.                     var offset, id, panel, diff, top;
  1435.                     if ($("#edit").is(":visible")) { return false; }
  1436.                     offset = pic.offset();
  1437.                     id = pic.attr("id").slice(1);
  1438.                     panel = $("#panel");
  1439.                     panel.data("id", id);
  1440.                     $("#delete_command").classAddOrRemove("menu_off", !canDelete(id));
  1441.                     $("#favorite_command > td:first").html((isFavorite(id) ? "<b>!</b> " : "") + APP.locale.panel_favorite || "Favorite");
  1442.                     diff = $("#container").outerHeight() - (offset.top + panel.outerHeight());
  1443.                     top = (diff < 0) ? offset.top + diff - 4 : offset.top;
  1444.                     panel.css({ "top": top, "left": offset.left - 4, display: "block" });
  1445.                     return false;
  1446.                 }, 300);
  1447.             }
  1448.             else if (e.type === "mouseleave") {
  1449.                 clearTimeout(timerId);
  1450.             }
  1451.         });
  1452.     })();
  1453.  
  1454.     $("#panel").live("mousemove", function () {
  1455.         return false;
  1456.     });
  1457.  
  1458.     $("body").live("mousemove mouseleave", function () {
  1459.         $("#panel").css("display", "none");
  1460.     });
  1461.  
  1462.     $("#usr_button").live("mousedown", function () {
  1463.         var tweet = model.getTweet($("#panel").data("id"));
  1464.         APP.showUserParams = { model: model, view: view, showUserName: "@" + tweet.screen_name };
  1465.         System.Gadget.Flyout.file = "showuser.html";
  1466.         System.Gadget.Flyout.show = true;
  1467.     });
  1468.  
  1469.     $("#panel").live("reply_command", function (evt) {
  1470.         var panel = $(this);
  1471.         panel.hide();
  1472.         view.edit.show(evt.type, model.getTweet(panel.data("id")));
  1473.     });
  1474.  
  1475.     $("#panel").live("reply_all_command", function (evt) {
  1476.         var panel = $(this);
  1477.         panel.hide();
  1478.         view.edit.show(evt.type, model.getTweet(panel.data("id")));
  1479.     });
  1480.  
  1481.     $("#panel").live("message_command", function (evt) {
  1482.         var panel = $(this);
  1483.         panel.hide();
  1484.         view.edit.show(evt.type, model.getTweet(panel.data("id")));
  1485.     });
  1486.  
  1487.     $("#panel").live("retweet_command", function (evt) {
  1488.         var panel = $(this);
  1489.         panel.hide();
  1490.         view.edit.show(evt.type, model.getTweet(panel.data("id")));
  1491.     });
  1492.  
  1493.     $("#panel").live("native_retweet_command", function () {
  1494.         var panel = $(this);
  1495.         panel.hide();
  1496.         APP.UTILITY.popup("ask", APP.locale.retweet_confirmation || "Retweet to your followers?", $(this), function () {
  1497.             APP.twitter.retweet(model, panel.data("id"));
  1498.         });
  1499.     });
  1500.  
  1501.     $("#panel").live("delete_command", function () {
  1502.         var panel;
  1503.         panel = $(this);
  1504.         panel.hide();
  1505.         APP.twitter.deleteTweet(model, panel.data("id"));
  1506.     });
  1507.  
  1508.     $("#panel").live("favorite_command", function () {
  1509.         var panel = $(this), id = panel.data("id"), func;
  1510.         panel.hide();
  1511.         func = isFavorite(id) ? APP.twitter.deleteFavorite : APP.twitter.createFavorite;
  1512.         func(model, id);
  1513.     });
  1514.  
  1515.     $("#panel").live("open_in_browser_command", function () {
  1516.         var panel = $(this),
  1517.         tweet = model.getTweet(panel.data("id")),
  1518.         url = "https://twitter.com/{0}/statuses/{1}".format(tweet.screen_name, tweet.id);
  1519.         panel.hide();
  1520.         APP.UTILITY.shellRun(url);
  1521.     });
  1522.  
  1523.     $("#panel").live("copy_command", function () {
  1524.         var panel = $(this),
  1525.         tweet = model.getTweet(panel.data("id"));
  1526.         panel.hide();
  1527.         if (tweet) { window.clipboardData.setData("text", tweet.text); }
  1528.     });
  1529.  
  1530.     $("#container").live("keydown", function (e) {
  1531.         var cKey = 67, fKey = 70, mKey = 77, rKey = 82, tKey = 84, command, tweetId = view.selected(), panel;
  1532.         if (!tweetId || $("input").is(":focus")) { return; }
  1533.         switch (e.keyCode) {
  1534.             case cKey: if (e.ctrlKey) { command = "copy_command"; } else { return; } break;
  1535.             case fKey: command = "favorite_command"; break;
  1536.             case mKey: command = "message_command"; break;
  1537.             case rKey: command = (e.shiftKey ? "reply_all_command" : "reply_command"); break;
  1538.             case tKey: command = (e.shiftKey ? "native_retweet_command" : "retweet_command"); break;
  1539.             default: return true;
  1540.         }
  1541.         panel = $("#panel");
  1542.         if (panel) {
  1543.             panel.data("id", tweetId);
  1544.             panel.trigger(command);
  1545.         }
  1546.         return false;
  1547.     });
  1548.  
  1549.     function scroll(delta) {
  1550.         var i,
  1551.         content = $("#content"),
  1552.         s = content.scrollTop(),
  1553.         o = content.offset().top - s,
  1554.         t = 0, p1 = 0, p2, gap = 7,
  1555.         tweets = content.find(".tweet"),
  1556.         length = tweets.length;
  1557.         for (i = 0; i < length; i += gap) {
  1558.             if (($(tweets[i]).offset().top - o) > s) { i -= (gap + 1); break; }
  1559.         }
  1560.         if (i > tweets.length) { i -= (gap + 1); }
  1561.         for (i = Math.max(0, i); i < length; i += 1) {
  1562.             p2 = p1;
  1563.             p1 = t;
  1564.             t = $(tweets[i]).offset().top - o;
  1565.             if (t > s) {
  1566.                 if (delta > 0) { t = p2; }
  1567.                 break;
  1568.             }
  1569.         }
  1570.         content.scrollTop(t);
  1571.         model.locked(t !== 0);
  1572.     }
  1573.  
  1574.     $("#content").live("mousewheel", function () {
  1575.         scroll(window.event.wheelDelta);
  1576.         return false;
  1577.     });
  1578.  
  1579.     $("#content").live("keydown", function (e) {
  1580.         var up = 38, down = 40, left = 37, right = 39, pgUp = 33, pgDn = 34, home = 36, end = 35, direction = 0;
  1581.         if (e.keyCode === up) { direction = 1; }
  1582.         if (e.keyCode === down) { direction = -1; }
  1583.         if (e.keyCode === pgUp) { direction = 3; }
  1584.         if (e.keyCode === pgDn) { direction = -3; }
  1585.         if (e.keyCode === home) { model.locked(false); }
  1586.         if (e.keyCode === end) { model.locked(true); }
  1587.         if (e.keyCode === left) { view.previousForm(); }
  1588.         if (e.keyCode === right) { view.nextForm(); }
  1589.         if (direction) { scroll(direction); return false; }
  1590.     });
  1591.  
  1592.     $("#tab_all").live("mousedown", function () {
  1593.         var refresh = view.getCurrentFormName() === "all";
  1594.         $("#content").empty();
  1595.         view.showForm("all");
  1596.         if (refresh) {
  1597.             APP.twitter.getHome(model);
  1598.             APP.twitter.getMentions(model);
  1599.             APP.twitter.getMessages(model);
  1600.         }
  1601.     });
  1602.  
  1603.     $("#tab_home").live("mousedown", function () {
  1604.         var refresh = view.getCurrentFormName() === "home";
  1605.         view.showForm("home", false, function () {
  1606.             if (refresh) { APP.twitter.getHome(model); }
  1607.         });
  1608.     });
  1609.  
  1610.     $("#tab_mentions").live("mousedown", function () {
  1611.         var refresh = view.getCurrentFormName() === "mentions";
  1612.         view.showForm("mentions", false, function () {
  1613.             if (refresh) { APP.twitter.getMentions(model); }
  1614.         });
  1615.     });
  1616.  
  1617.     $("#tab_messages").live("mousedown", function () {
  1618.         var refresh;
  1619.         refresh = view.getCurrentFormName() === "messages";
  1620.         view.showForm("messages", false, function () {
  1621.             if (refresh) { APP.twitter.getMessages(model); }
  1622.         });
  1623.     });
  1624.  
  1625.     $("#tab_search").live("mousedown", function () {
  1626.         view.showForm("search");
  1627.     });
  1628.  
  1629.     $("#tab_favorites").live("mousedown", function () {
  1630.         view.showForm("favorites");
  1631.         APP.twitter.getFavorites(model);
  1632.     });
  1633.  
  1634.     function tooltip(tipText, element) {
  1635.         APP.UTILITY.popup("tip", tipText, element);
  1636.     }
  1637.  
  1638.     (function () {
  1639.         var timerId;
  1640.         $(".link").live("mouseenter mouseleave", function (e) {
  1641.             var link = $(this), href;
  1642.             if (e.type === "mouseenter" && $("#tip").length === 0) {
  1643.                 timerId = setTimeout(function () {
  1644.                     timerId = 0;
  1645.                     href = link.attr("href");
  1646.                     tooltip(model.links[href] || href, link);
  1647.                 }, 300);
  1648.             }
  1649.             else if (e.type === "mouseleave") {
  1650.                 if (timerId) { clearTimeout(timerId); }
  1651.                 else { $("#tip").slideUp("fast", function () { $(this).remove(); }); }
  1652.             }
  1653.         });
  1654.     })();
  1655.  
  1656.     function submitSearch(params) {
  1657.         if (params === undefined) { params = {}; }
  1658.         params.q = $("#search_query").val();
  1659.         if (params.q.length === 0) { return; }
  1660.         if (model.lastSearch !== params.q) {
  1661.             model.clearSearches();
  1662.             model.fireUpdateEvent("searchUpdated");
  1663.         }
  1664.         model.lastSearch = params.q;
  1665.         $("#search_button").attr("src", "/images/waiting.gif");
  1666.         APP.twitter.search(model, params, function () {
  1667.             $("#search_button").attr("src", "/images/tab/accept.png");
  1668.         });
  1669.     }
  1670.  
  1671.     $("#search_button").live("mousedown", function () { submitSearch(); });
  1672.  
  1673.     $("#search_query").live("keydown", function (e) {
  1674.         if (e.keyCode === 13) { submitSearch(); }
  1675.     });
  1676.  
  1677.     $(".hashtag").live("mousedown", function () {
  1678.         var query = $(this).text().replace("\uff03", "#");
  1679.         view.showForm("search", false, function () {
  1680.             $("#search_query").val(query);
  1681.             submitSearch();
  1682.         });
  1683.     });
  1684.  
  1685.     $(".screenname").live("mousedown", function () {
  1686.         var screen_name = $(this).attr("sc").replace("\uff20", "@");
  1687.         APP.showUserParams = { model: model, view: view, showUserName: screen_name };
  1688.         System.Gadget.Flyout.file = "showuser.html";
  1689.         System.Gadget.Flyout.show = true;
  1690.     });
  1691.  
  1692.     $(".tweet").live("mousedown", function () {
  1693.         var oldId = view.selected(),
  1694.         newId = $(this).attr("id");
  1695.         $(".tweet").removeClass("selected_tweet");
  1696.         view.selected(0);
  1697.         if (oldId !== newId) {
  1698.             $(this).addClass("selected_tweet");
  1699.             view.selected($(this).attr("id"));
  1700.         }
  1701.     });
  1702.  
  1703.     $("#container").live("chirp", function () {
  1704.         System.Sound.playSound("notify.wav");
  1705.     });
  1706.  
  1707.     $("#content:not(:text, textarea)").live("keydown", function (e) {
  1708.         var cKey = 67, tweet;
  1709.         if (e.ctrlKey && e.keyCode === cKey) {
  1710.             tweet = model.getTweet(view.selected());
  1711.             if (tweet) { window.clipboardData.setData("text", tweet.text); }
  1712.             return false;
  1713.         }
  1714.     });
  1715.  
  1716.     $("#content").live("selectstart", function () {
  1717.         // Kill selection of text in gadget
  1718.         return false;
  1719.     });
  1720.  
  1721.     $("#content").live("showStatus", function (e, status) {
  1722.         view.showStatus(status);
  1723.     });
  1724.  
  1725.     $("#content").live("commAccessing", function () {
  1726.         view.updateCommIndicator("accessing");
  1727.     });
  1728.  
  1729.     $("#content").live("commSuccess", function () {
  1730.         view.updateCommIndicator("success");
  1731.     });
  1732.  
  1733.     $("#content").live("commError", function () {
  1734.         view.updateCommIndicator("error");
  1735.     });
  1736.  
  1737.     $("#content").live("commTooManyRequests", function() {
  1738.         view.updateCommIndicator("tooManyRequests");
  1739.     });
  1740.  
  1741.     $("#content").live("internalError", function () {
  1742.         view.updateCommIndicator("internal error");
  1743.     });
  1744.  
  1745.     $("#content").live("lockIndicator", function (e, locked) {
  1746.         view.lockIndicator(locked);
  1747.     });
  1748.  
  1749.     $(".inreplyto").live("mousedown", function () {
  1750.         var id = $(this).attr("name"),
  1751.         tweet = model.getTweet(id);
  1752.         if (tweet) { view.showStatusFlyout(tweet); }
  1753.         else {
  1754.             APP.twitter.getStatus(model, id, function (data) {
  1755.                 var replyTweet = model.readTweet(data, "");
  1756.                 view.showStatusFlyout(replyTweet);
  1757.             });
  1758.         }
  1759.     });
  1760.  
  1761.     $("#more").live("mousedown", function () {
  1762.         switch (view.getCurrentFormName()) {
  1763.             case "home": APP.twitter.getHome(model, true); break;
  1764.             case "mentions": APP.twitter.getMentions(model, true); break;
  1765.             case "messages": APP.twitter.getMessages(model, true); break;
  1766.             case "search": submitSearch({ max_id: model.oldestIdSearch }); break;
  1767.             default: return false;
  1768.         }
  1769.         $(this).empty().append($("<img>", { src: "images/waiting.gif" }));
  1770.     });
  1771.  
  1772.     switchStyleSheet = (function () {
  1773.         var index, styleSheet,
  1774.         styleSheets = ["css/original.css", "css/aqua.css", "css/white.css"];
  1775.         return function () {
  1776.             index = $.inArray(APP.settings.styleSheet(), styleSheets);
  1777.             styleSheet = styleSheets[(index + 1) % styleSheets.length];
  1778.             APP.settings.styleSheet(styleSheet);
  1779.             $("#style_sheet").loadStyleSheet(styleSheet);
  1780.             view.onSize();
  1781.         };
  1782.     })();
  1783.  
  1784.     $("#container").live("keydown", function (e) {
  1785.         var qKey = 81;
  1786.         if (e.ctrlKey && e.keyCode === qKey) {
  1787.             switchStyleSheet();
  1788.             return false;
  1789.         }
  1790.     });
  1791.  
  1792.     $(".tweet").live("mouseenter", function () {
  1793.         $(this).find(".unread").removeClass("unread").addClass("read");
  1794.         delete model.unread[this.id];
  1795.     });
  1796.  
  1797.     //  $("#container").live("keydown", function (e) {
  1798.     //    var pKey = 80;
  1799.     //    if (e.keyCode === pKey) {
  1800.     //      $("#content div.tweet:first").remove();
  1801.     //      $("#content div.tweet:first").remove();
  1802.     //      $("#content div.tweet:first").remove();
  1803.     //      model.fireUpdateEvent("homeUpdated");
  1804.     //      return false;
  1805.     //    }
  1806.     //  });
  1807.  
  1808.     $("#get_pin").live("click", function () {
  1809.         $("#pin_error").hide();
  1810.         APP.twitter.getPin(model.comm,
  1811.         function () { },
  1812.         function (x, t, e) { $("#pin_error").text(t).show(); });
  1813.         return false;
  1814.     });
  1815.  
  1816.     $("#pin_text").live("focus", function (e) {
  1817.         $(this).val("").removeClass("blur");
  1818.         $("#login").removeAttr("disabled");
  1819.     });
  1820.  
  1821.     $("#login").live("click", function () {
  1822.         APP.twitter.getAccessToken(model.comm, $("#pin_text").val(), function (x, t, e) {
  1823.             $("#pin_error").text(t).show();
  1824.             return false;
  1825.         });
  1826.     });
  1827.  
  1828.     return that;
  1829. };
  1830.  
  1831. APP.run = function () {
  1832.   var view = APP.view(APP.model(APP.comm()));
  1833.   view.start();
  1834. };
  1835.  
  1836. // x-auth
  1837. // consider new tweet notification like chrome bird
  1838. // different action panels for different states (delete for your own tweets)
  1839. // popup send to list
  1840. // spell checker
  1841. // geolocation
  1842. // switch panels using left/right arrows
  1843. // more keyboard shortcuts
  1844. // twittpic
  1845. // search near you
  1846. // tooltips on panel
  1847. // twittlonger
  1848. // choose sounds
  1849. // Translate tweet (ctrl+T)
  1850. // Auto translate?
  1851. // indicate private account
  1852. // hide user icons option
  1853. // accessibility
  1854. // publish future tweets
  1855. // multiple replies
  1856. // tall tweets
  1857. // twitter symbols
  1858. // add unfavorites
  1859. // fix keyboard accelerators
  1860.