PageRenderTime 26ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/public/lib/stream/streamplugins.js

https://github.com/andywenk/streamie
JavaScript | 442 lines | 347 code | 38 blank | 57 comment | 59 complexity | f20161c8c59e06ecbea8b0aa40f918bd MD5 | raw file
  1. /*
  2. * List of built in plugins for tweet processing
  3. *
  4. */
  5. require.def("stream/streamplugins",
  6. ["stream/tweet", "stream/settings", "stream/twitterRestAPI", "stream/helpers", "stream/keyValueStore", "text!../templates/tweet.ejs.html"],
  7. function(tweetModule, settings, rest, helpers, keyValue, templateText) {
  8. settings.registerNamespace("filter", "Filter");
  9. settings.registerKey("filter", "longConversation", "Filter long (more than 3 tweets) conversations", false);
  10. settings.registerNamespace("stream", "Stream");
  11. settings.registerKey("stream", "showRetweets", "Show Retweets", true);
  12. settings.registerKey("stream", "keepScrollState", "Keep scroll level when new tweets come in", true);
  13. settings.registerKey("stream", "translate", "Automatically translate to your preferred language", false );
  14. // convert google.language.Languages list of support languages to a settings values
  15. var translateValues = {};
  16. for(var humanLang in google.language.Languages){
  17. var codeLang = google.language.Languages[humanLang];
  18. if( codeLang.length == 0 ) continue;
  19. translateValues[codeLang] = humanLang.charAt(0).toUpperCase() + humanLang.substring(1).toLowerCase();
  20. }
  21. settings.registerKey("stream", "preferedLanguage", "Preferred language", "en", translateValues );
  22. var template = _.template(templateText);
  23. var Tweets = {};
  24. var Conversations = {};
  25. var ConversationCounter = 0;
  26. settings.subscribe("stream", "translate", function(value){
  27. console.log("translate value is now "+value);
  28. //window.location.reload();
  29. });
  30. settings.subscribe("stream", "preferedLanguage", function(value){
  31. console.log("preferedLanguage value is now "+value);
  32. //window.location.reload();
  33. });
  34. return {
  35. // Twitter changed some of the IDs to have a second variant that is represented
  36. // as a string because JavaScript does not handle numbers above 2**43 well.
  37. // Because we are JavaScript, we ignore the old variants and replace them with the
  38. // string variants.
  39. stringIDs: {
  40. func: function stringIDs (tweet) {
  41. var data = tweet.data;
  42. data.id = data.id_str;
  43. data.in_reply_to_status_id = data.in_reply_to_status_id_str;
  44. if(data.retweeted_status) {
  45. data = data.retweeted_status
  46. data.id = data.id_str;
  47. data.in_reply_to_status_id = data.in_reply_to_status_id_str;
  48. }
  49. this();
  50. }
  51. },
  52. // Turns direct messages into something similar to a tweet
  53. // Because Streamie uses a stream methaphor for everything it does not make sense to
  54. // make a special case for direct messages
  55. handleDirectMessage: {
  56. func: function handleDirectMessage (tweet) {
  57. if(tweet.data.sender) {
  58. tweet.direct_message = true;
  59. tweet.data.user = tweet.data.sender; // the user is the sender
  60. }
  61. this();
  62. }
  63. },
  64. // Turns retweets into something similar to tweets
  65. handleRetweet: {
  66. func: function handleRetweet (tweet) {
  67. if(tweet.data.retweeted_status) {
  68. if(settings.get("stream", "showRetweets")) {
  69. var orig = tweet.data;
  70. tweet.data = tweet.data.retweeted_status;
  71. tweet.retweet = orig;
  72. } else {
  73. console.log(JSON.stringify(tweet, null, " "));
  74. return;
  75. }
  76. }
  77. this();
  78. }
  79. },
  80. // we only show tweets. No direct messages. For now
  81. tweetsOnly: {
  82. func: function tweetsOnly (tweet, stream) {
  83. if(tweet.data.text != null) {
  84. if(stream.count == 0) {
  85. $(document).trigger("tweet:first");
  86. }
  87. stream.count++;
  88. if(tweet.data.user.id == stream.user.user_id) {
  89. tweet.yourself = true;
  90. }
  91. tweet.created_at = new Date(tweet.data.created_at);
  92. this();
  93. }
  94. }
  95. },
  96. // marks a tweet whether we've ever seen it before using localStorage
  97. everSeen: {
  98. func: function everSeen (tweet, stream) {
  99. var key = "tweet"+tweet.data.id;
  100. if(window.localStorage) {
  101. keyValue.Store("screen_names").set("@"+tweet.data.user.screen_name, 1);
  102. if(window.localStorage[key]) {
  103. tweet.seenBefore = true;
  104. } else {
  105. window.localStorage[key] = 1;
  106. }
  107. var data = tweet.retweet ? tweet.retweet : tweet.data;
  108. var newest = stream.newestTweet();
  109. if(data.id > newest) {
  110. stream.newestTweet(data.id);
  111. }
  112. }
  113. this();
  114. }
  115. },
  116. // find all mentions in a tweet. set tweet.mentioned to true if the current user was mentioned
  117. mentions: {
  118. regex: /(^|\W)\@([a-zA-Z0-9_]+)/g,
  119. func: function mentions (tweet, stream, plugin) {
  120. var screen_name = stream.user.screen_name;
  121. tweet.mentions = [];
  122. tweet.data.text.replace(plugin.regex, function (match, pre, name) {
  123. if(name == screen_name) {
  124. tweet.mentioned = true;
  125. }
  126. tweet.mentions.push(name);
  127. return match;
  128. });
  129. this();
  130. }
  131. },
  132. // translate
  133. translate: {
  134. func: function translate (tweet, stream) {
  135. // if stream.translate setting is disable, or translate has been already tried, do nothing and go on
  136. if( settings.get("stream", "translate") == false || tweet.translateProcess ){
  137. this();
  138. return;
  139. }
  140. var dstLang = settings.get("stream", "preferedLanguage");
  141. var gtranslate_proc = new gTranslateProc(tweet.data.text);
  142. tweet.translateProcess= true;
  143. google.language.translate(gtranslate_proc.prepared_text, "", dstLang, function(result){
  144. //console.log("tweet to translate [", result, "] ", tweet);
  145. if(result.error) return;
  146. var srcLang = result.detectedSourceLanguage;
  147. if( srcLang == dstLang ) return;
  148. //console.log("[", srcLang, "] ", tweet.data.text)
  149. //console.log("[", dstLang, "] ", result.translation);
  150. /**
  151. * - UI issue
  152. * - how to show users than this tweet as been translated
  153. * - how to show users that a translation is available
  154. * - how to allow translation back and forth
  155. * - toggle as tweet action is ok
  156. */
  157. tweet.translate = {
  158. srcLang : srcLang,
  159. curLang : dstLang,
  160. texts : {}
  161. }
  162. var srcText = tweet.data.text;
  163. var dst_text = gtranslate_proc.process_result(result.translation);
  164. tweet.translate.texts[srcLang] = srcText;
  165. tweet.translate.texts[dstLang] = dst_text;
  166. // reprocess this tweet
  167. stream.reProcess(tweet);
  168. });
  169. this();
  170. }
  171. },
  172. // set the tweet template
  173. template: {
  174. func: function templatePlugin (tweet) {
  175. tweet.template = template;
  176. this();
  177. }
  178. },
  179. // render the template (the underscore.js way)
  180. renderTemplate: {
  181. func: function renderTemplate (tweet, stream) {
  182. tweet.html = tweet.template({
  183. stream: stream,
  184. tweet: tweet,
  185. helpers: helpers
  186. });
  187. this();
  188. }
  189. },
  190. // if a tweet with the name id is in the stream already, do not continue
  191. avoidDuplicates: {
  192. func: function avoidDuplicates (tweet, stream) {
  193. var id = tweet.data.id;
  194. if(Tweets[id] && tweet.streamDirty) {
  195. this();
  196. } else if(Tweets[id]) {
  197. // duplicate detected -> do not continue;
  198. } else {
  199. Tweets[id] = tweet;
  200. this();
  201. }
  202. }
  203. },
  204. // Group the tweet into conversations.
  205. // This also tries to work for conversations between more than two people
  206. // by tracking the "root" node of the conversation
  207. conversations: {
  208. func: function conversations (tweet, stream, plugin) {
  209. var id = tweet.data.id;
  210. var in_reply_to = tweet.data.in_reply_to_status_id;
  211. if(tweet.data._conversation) {
  212. tweet.conversation = Conversations[id] = tweet.data._conversation
  213. }
  214. else if(Conversations[id]) {
  215. tweet.conversation = Conversations[id];
  216. }
  217. else if(Conversations[in_reply_to]) {
  218. tweet.conversation = Conversations[id] = Conversations[in_reply_to];
  219. } else {
  220. tweet.conversation = Conversations[id] = {
  221. index: ConversationCounter++,
  222. tweets: 0
  223. };
  224. if(in_reply_to) {
  225. Conversations[in_reply_to] = tweet.conversation;
  226. }
  227. }
  228. tweet.conversation.tweets++;
  229. tweet.fetchNotInStream = function (cb) {
  230. var in_reply_to = tweet.data.in_reply_to_status_id;
  231. if(in_reply_to && !Tweets[in_reply_to]) {
  232. rest.get("/1/statuses/show/"+in_reply_to+".json", function (status) {
  233. if(status) {
  234. status._after = tweet;
  235. status._conversation = tweet.conversation;
  236. stream.process(tweetModule.make(status));
  237. if(cb) {
  238. cb(status);
  239. }
  240. }
  241. })
  242. }
  243. };
  244. this();
  245. }
  246. },
  247. // put the tweet into the stream
  248. prepend: {
  249. func: function prepend (tweet, stream) {
  250. var previous_node = tweet.node;
  251. tweet.node = $(tweet.html);
  252. tweet.node.data("tweet", tweet); // give node access to its tweet
  253. if( tweet.streamDirty ){
  254. console.assert(previous_node);
  255. previous_node.replaceWith(tweet.node);
  256. } else if(tweet.data._after) {
  257. var target = tweet.data._after;
  258. target.node.after(tweet.node);
  259. tweet.fetchNotInStream();
  260. } else {
  261. stream.canvas().prepend(tweet.node);
  262. }
  263. this();
  264. }
  265. },
  266. // htmlencode the text to avoid XSS
  267. htmlEncode: {
  268. func: function htmlEncode (tweet, stream, plugin) {
  269. if( ! tweet.translate ){
  270. var text = tweet.data.text;
  271. }else{
  272. var text = tweet.translate.texts[tweet.translate.curLang];
  273. }
  274. text = helpers.htmlDecode(text);
  275. text = helpers.htmlEncode(text);
  276. tweet.textHTML = text;
  277. this();
  278. }
  279. },
  280. // Format text to HTML hotlinking, links, things that looks like links, scree names and hash tags
  281. // Also filters out some more meta data and puts that on the tweet object. Currently: hashTags
  282. formatTweetText: {
  283. //from http://gist.github.com/492947 and http://daringfireball.net/2010/07/improved_regex_for_matching_urls
  284. GRUBERS_URL_RE: /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig,
  285. SCREEN_NAME_RE: /(^|\W)\@([a-zA-Z0-9_]+)/g,
  286. HASH_TAG_RE: /(^|\s)\#(\S+)/g,
  287. func: function formatTweetText (tweet, stream, plugin) {
  288. var text = tweet.textHTML;
  289. text = text.replace(plugin.GRUBERS_URL_RE, function(url){
  290. return '<a href="'+((/^\w+\:\//.test(url)?'':'http://')+helpers.html(url))+'">'+helpers.html(url)+'</a>';
  291. })
  292. // screen names
  293. text = text.replace(plugin.SCREEN_NAME_RE, function (all, pre, name) {
  294. return pre+'<a href="http://twitter.com/'+name+'" class="user-href">@'+name+'</a>';
  295. });
  296. // hash tags
  297. tweet.hashTags = [];
  298. text = text.replace(plugin.HASH_TAG_RE, function (all, pre, tag) {
  299. tweet.hashTags.push(tag);
  300. return pre+'<a href="http://search.twitter.com/search?q='+encodeURIComponent(tag)+'" class="tag">#'+tag+'</a>';
  301. });
  302. tweet.textHTML = text;
  303. this();
  304. }
  305. },
  306. // runs the link plugins defined in app.js on each link
  307. executeLinkPlugins: {
  308. func: function executeLinkPlugins (tweet, stream) {
  309. var node = $("<div>"+tweet.textHTML+"</div>");
  310. var as = node.find("a");
  311. as.each(function () {
  312. var a = $(this);
  313. stream.linkPlugins.forEach(function (plugin) {
  314. plugin.func.call(function () {}, a, tweet, stream, plugin);
  315. })
  316. })
  317. tweet.textHTML = node.html();
  318. this();
  319. }
  320. },
  321. // Trigger a custom event to inform everyone about a new tweet
  322. // Event is not fired for tweet from the prefill
  323. newTweetEvent: {
  324. func: function newTweetEvent (tweet) {
  325. // Do not fire for tweets
  326. if(!tweet.prefill && !tweet.filtered) {
  327. // { custom-event: tweet:new }
  328. tweet.node.trigger("tweet:new", [tweet])
  329. }
  330. this();
  331. }
  332. },
  333. filter: {
  334. func: function filter (tweet) {
  335. if(settings.get("filter", "longConversation")) {
  336. if(tweet.conversation.tweets > 3) {
  337. tweet.filtered = {
  338. reason: "long-conversation"
  339. }
  340. }
  341. }
  342. this();
  343. }
  344. },
  345. // when we insert a new tweet
  346. // adjust the scrollTop to show the same thing as before
  347. keepScrollState: {
  348. WIN: $(window),
  349. func: function keepScrollState (tweet, stream, plugin) {
  350. var next = tweet.node.next();
  351. if(next.length > 0) {
  352. var height = next.offset().top - tweet.node.offset().top;
  353. tweet.height = height;
  354. if(settings.get("stream", "keepScrollState")) {
  355. if(!tweet.prefill || !tweet.seenBefore) {
  356. var win = plugin.WIN;
  357. var cur = win.scrollTop();
  358. var top = cur + height;
  359. win.scrollTop( top );
  360. }
  361. }
  362. }
  363. this();
  364. }
  365. },
  366. // Notify the user via webkit notification
  367. webkitNotify: {
  368. // how many notifications are currently shown?
  369. current: 0,
  370. func: function webkitNotify(tweet, stream, plugin) {
  371. // only show tweets not seen before, while not prefilling,
  372. // if we have the rights and its enabled in the settings
  373. if (!tweet.seenBefore &&
  374. !tweet.prefill &&
  375. !tweet.filtered &&
  376. !tweet.yourself &&
  377. plugin.current < 5 &&
  378. settings.get('notifications', 'enableWebkitNotifications') &&
  379. window.webkitNotifications &&
  380. window.webkitNotifications.checkPermission() == 0) {
  381. try {
  382. var notification =
  383. window.webkitNotifications.createNotification(tweet.data.user.profile_image_url,
  384. tweet.data.user.name,
  385. tweet.data.text);
  386. notification.show();
  387. notification.onclose = function() {
  388. --plugin.current;
  389. } //onclose
  390. ++plugin.current;
  391. //hide after 5 seconds
  392. setTimeout(function() {
  393. notification.cancel();
  394. }, 5000);
  395. } catch(e) {
  396. }
  397. }
  398. this();
  399. }
  400. }
  401. }
  402. }
  403. );