├── README.markdown └── githubbub.user.js /README.markdown: -------------------------------------------------------------------------------- 1 | GitHubbub 2 | ========= 3 | 4 | GitHubbub gives the [GitHub Fluid app](http://github.com/blog/47-new-fluid-icon) 5 | some hubbub: 6 | 7 | * Auto-updating News Feed 8 | * Growl notifications for News Feed alerts 9 | * Dock badge with number of new messages 10 | 11 | 12 | INSTALL 13 | ------- 14 | If you're viewing this page from your GitHub Fluid App, just 15 | [click here](http://github.com/stephencelis/githubbub/raw/master/githubbub.user.js). 16 | 17 | If you've downloaded this userscript, drag "githubbub.user.js" to your GitHub 18 | Fluid app. 19 | 20 | 21 | LICENSE 22 | ------- 23 | 24 | (The MIT License) 25 | 26 | Copyright (c) 2008-* Stephen Celis 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy of 29 | this software and associated documentation files (the "Software"), to deal in 30 | the Software without restriction, including without limitation the rights to 31 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 32 | of the Software, and to permit persons to whom the Software is furnished to do 33 | so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. 45 | -------------------------------------------------------------------------------- /githubbub.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name GitHubbub 3 | // @description Hubbub for Fluid Github: Growl/Dock notifications, and more. 4 | // @namespace http://stephencelis.com/ 5 | // @homepage http://github.com/stephencelis/githubbub/ 6 | // @author Stephen Celis 7 | // @include http*://github.com/* 8 | // ==/UserScript== 9 | 10 | (function () { 11 | var parseResponse = function (data) { 12 | var xmlDoc = new DOMParser().parseFromString(data, "application/xhtml+xml"); 13 | $(".relatize", xmlDoc).relatizeDate(); 14 | setCount(xmlDoc); 15 | 16 | if (dashboard) { 17 | var remoteAlerts = $(".alert", xmlDoc); 18 | var localAlert = $(".alert")[0], switched = false; 19 | 20 | for (var i = remoteAlerts.length; i > 0; --i) { 21 | var remoteAlert = document.importNode(remoteAlerts[i - 1], true); 22 | if (moreRecent(remoteAlert, localAlert)) 23 | importAlert(remoteAlert); 24 | } 25 | } 26 | } 27 | 28 | var importAlert = function (imported) { 29 | $(".feed_filter").after(imported); 30 | 31 | var timestamp = $(".title abbr", imported)[0].innerText; 32 | var title = $(".title", imported)[0].innerText.replace(timestamp, ""); 33 | var descriptions = $(".details .message", imported) 34 | var description = descriptions[0].innerText.replace(/^\s*|\s*$/g, ""); 35 | if (descriptions.length > 1) { 36 | description = description + "\n\n(And " + 37 | pluralize("more commit", descriptions.length - 1) + ")"; 38 | $('.reveal_commits, .hide_commits', imported).click(function () { 39 | var div = $(this).parents('.details'); 40 | div.find('.reveal, .hide_commits, .commits').toggle(); 41 | return false; 42 | }); 43 | 44 | } 45 | var icon = $(".gravatar img", imported).attr("src") 46 | if (icon) // Make default Gravatar bigger; FIXME: render GitHub icon instead? 47 | icon = icon.replace(/\?s=\d0&/, "?s=128"); 48 | var identifier = $(".details .message", imported)[0].innerText; 49 | 50 | window.fluid.showGrowlNotification({ 51 | title: title, 52 | description: description, 53 | icon: icon, 54 | identifier: identifier, 55 | onclick: function() { activate(imported) } 56 | }); 57 | } 58 | 59 | var importAll = function (remoteAlerts) { 60 | for (var i = remoteAlerts.length; i > 0; --i) 61 | importAlert(document.importNode(remoteAlerts[i - 1], true)); 62 | } 63 | 64 | var activate = function (node) { 65 | window.fluid.activate(); 66 | $(node).find('.reveal, .hide_commits, .commits').toggle(); 67 | return false; 68 | } 69 | 70 | var setCount = function (doc) { 71 | if (typeof doc == "undefined") 72 | doc = document; 73 | var unreadMessagesCount = $(".usernav .unread_count", doc).text(); 74 | var unreadNotificationCount = $(".notifications_count", doc).text(); 75 | var unreadCount = parseInt(unreadMessagesCount) + parseInt(unreadNotificationCount); 76 | if (doc != document) { 77 | $(".usernav .unread_count").text(unreadMessagesCount); 78 | $(".notifications_count", doc).text(unreadNotificationCount); 79 | } 80 | window.fluid.dockBadge = unreadCount > 0 ? unreadCount : null; 81 | } 82 | 83 | var moreRecent = function (alertOne, alertTwo) { 84 | var oneTime = toTime($(".title abbr", alertOne).attr("title")); 85 | var twoTime = toTime($(".title abbr", alertTwo).attr("title")); 86 | return oneTime > twoTime; 87 | } 88 | 89 | var reRelatizeDates = function () { 90 | $(".relatize").each(function () { 91 | var timestamp = toTime($(this).attr("title")); 92 | $(this).text($.relatizeDate.timeAgoInWords(timestamp)); 93 | }); 94 | } 95 | 96 | var toTime = function (string) { 97 | var time = new Date(reformatTime(string)); 98 | return time; 99 | } 100 | 101 | var reformatTime = function (string) { 102 | var parts = string.split(/\s|\-/); 103 | var monthName = $.relatizeDate.shortMonths[parts[1] - 1]; 104 | return monthName + " " + parts[2] + " " + parts[3] + " -800 " + parts[0]; 105 | } 106 | 107 | var pluralize = function (string, number) { 108 | if (number != 1) 109 | string = string + "s" 110 | return number + " " + string; 111 | } 112 | 113 | // Run... 114 | setCount(); 115 | setInterval(function () { 116 | $.get(window.location.href, function (data) { parseResponse(data); }); 117 | reRelatizeDates(); 118 | }, 120 * 1000); 119 | })(); 120 | --------------------------------------------------------------------------------