// ==UserScript==
// @name           FriendFeed faviconizer
// @namespace      decafbad.com
// @description    Re-introduces favicons into FriendFeed pages
// @include        http://friendfeed.com/*
// ==/UserScript==
(function() {

    // Save a spot to grab jQuery in package-global scope.
    var $ = null;

    // Define a shortcut to $this that survives jquery event handlers.
    var $this = {

        // Flag controlling debug logging and potentially other odd things.
        debug: false,

        /**
         * Perform initialization.
         */
        init: function() {
            if ( (null===$) && (typeof unsafeWindow.jQuery == 'undefined') ) { 
                // Wait for jQuery availability before continuing with init.
                window.setTimeout(arguments.callee, 100);
            } else {
                // Get a hold of jQuery and continue...
                $ = unsafeWindow.jQuery;

                if ($this.services && !$this.debug) {
                    // Just use the services in the cache if they're there and
                    // we're not debugging.
                    $this.servicesLoaded();
                } else {
                    // Otherwise, go get the latest services, cache them, and fire
                    // it up.
                    $.getJSON('http://friendfeed.com/api/services?callback=?', function(data) {
                        $this.services = data.services;
                        $this.servicesLoaded();
                    });
                }
            }
        },

        /**
         * Finish setup once the FF services have been loaded.
         */
        servicesLoaded: function(data) {

            // Start off with some default styles.
            var styles = $this.styles;

            // Run through the known named service icons to build indices and
            // CSS styles.
            $.each($this.services, function() {
                var service = this;

                // Build indices of names, images, and IDs.
                $this.service_icons_by_id[service.id]   = service.iconUrl;
                $this.service_names_by_id[service.id]   = service.name;
                $this.service_ids_by_name[service.name] = service.id;

                // Synthesize a CSS style based on the ID and image URL.
                styles.push(
                    '#feed .service-'+service.id+' .body ' +
                    '{ background-image: url('+service.iconUrl+'); }'
                );

            });

            // Add the assembled new styles
            GM_addStyle(styles.join("\n"));

            // Set up to respond to changes in the on-page feed.
            $('#feed')[0].addEventListener(
                'DOMSubtreeModified', $this.onFeedModified, false
            );

            // Fire off an initial sweep through the feed.
            $this.onFeedModified();
        },

        /**
         * React to changes in the #feed element.  
         *
         * A timer is used to debounce and absorb streams of multiple changes,
         * as well as to ignore change events generated by the enhancement
         * itself.
         */
        onFeedModified: function() {
            if (!$this.feed_check_timer) {
                $this.feed_check_timer = setTimeout(function() {
                    // Enhance new arrivals to the feed.
                    $this.enhanceEntries();
                    // Clear the timer.
                    $this.feed_check_timer = null;
                }, $this.feed_check_delay);
            }
        },

        // Minimum time period between reacting to feed modifications.
        feed_check_delay: 150,
        // Timer used to debounce feed modification events.
        feed_check_timer: null,

        /**
         * Sweep through entries not yet enhanced, determine the service ID
         * based on the name of the service found in the entry.  Add the
         * service ID as a CSS class to apply the proper favicon image.
         */
        enhanceEntries: function() {
            $('.entry:not(.service-enhanced)').each(function() {
                var entry   = $(this),
                    service = entry.find('.service'),
                    // HACK: Special exceptions for FriendFeed native sources, could be improved.
                    sname   = service.text() || 'FriendFeed',
                    sid     = $this.service_ids_by_name[sname] || 'default';

                entry.addClass('service-enhanced service-' + sid);
            });
        },

        /**
         * Base styles to tweak friendfeed, array of lines will be joined by
         * init() and combined with synthetic styles.
         */
        styles: [
            '#feed .entry .body {',
            '    padding-left: 28px;',
            '    background: transparent url(http://friendfeed.com/static/images/icons/feed.png?v=7f94) no-repeat left top;',
            '}',
            '#feed .service-friendfeed .body { background-image: url(http://friendfeed.com/favicon.ico); }',
            '#feed .service-raptr .body { background-image: url(http://raptr.com/favicon.ico); }',
            ''
        ],

        /**
         * Various indexes between names, ids, and image URLs - 
         * populated in init()
         */
        service_icons_by_id: {},
        service_names_by_id: {},
        service_ids_by_name: {
            'Amazon.com wish list': 'amazon',
            'Twitter / Favorites from...': 'twitter',
            'Bookmarklet': 'friendfeed',
            'Netflix instant queue': 'netflix',
            'YouTube favorites': 'youtube',
            'Flickr favorites': 'flickr',
            'Raptr': 'raptr'
        },

        /**
         * Custom logging utility
         */
        log: function(msg, lvl) {
            if (!lvl) lvl='debug';
            if ('debug'==lvl && !$this.debug) return;
            console.log('FFF:' + (new Date()).getTime() + ' ' + msg);
        },

        // cached on 29 May 2009 from http://friendfeed.com/api/services
        services: 
            {"services":[{"url":"http://en.wikipedia.org/wiki/Web_feed","iconUrl":"http://friendfeed.com/static/images/icons/feed.png?v=7f94a63b8d2a5a538c6666540ba687f0","id":"feed","name":"Custom RSS/Atom"},{"url":"http://www.joost.com/","iconUrl":"http://friendfeed.com/static/images/icons/joost.png?v=78eb6ccea930059d5e9e96ced2076afe","id":"joost","name":"Joost"},{"url":"http://www.polyvore.com/","iconUrl":"http://friendfeed.com/static/images/icons/polyvore.png?v=321d087a6907c0d60c741272bdd9ef9f","id":"polyvore","name":"Polyvore"},{"url":"http://www.livejournal.com/","iconUrl":"http://friendfeed.com/static/images/icons/livejournal.png?v=33ef29c1a6016b182650705856c14095","id":"livejournal","name":"LiveJournal"},{"url":"http://twitter.com/","iconUrl":"http://friendfeed.com/static/images/icons/twitter.png?v=df0a0affa8100c494df42159627a38b0","id":"twitter","name":"Twitter"},{"url":"http://friendfeed.com/","iconUrl":"http://friendfeed.com/static/images/icons/festivus.png?v=0af8f2f7c42492c0eb17182ce330fb1a","id":"festivus","name":"FestivusFeed"},{"url":"http://www.dailymotion.com/","iconUrl":"http://friendfeed.com/static/images/icons/dailymotion.png?v=380c717579180ca5d636563d8fea028c","id":"dailymotion","name":"Dailymotion"},{"url":"http://www.linkedin.com/","iconUrl":"http://friendfeed.com/static/images/icons/linkedin.png?v=713dd48c4564fec4fca3a0628bce8a7b","id":"linkedin","name":"LinkedIn"},{"url":"http://www.tumblr.com/","iconUrl":"http://friendfeed.com/static/images/icons/tumblr.png?v=bd2edb3ac8b549d0ca796af3fb05a9d5","id":"tumblr","name":"Tumblr"},{"url":"http://12seconds.tv/","iconUrl":"http://friendfeed.com/static/images/icons/twelveseconds.png?v=739de3bc40ddc951da2e12be4283ecfe","id":"twelveseconds","name":"12seconds"},{"url":"http://www.hatena.ne.jp/","iconUrl":"http://friendfeed.com/static/images/icons/hatena.png?v=7b6183a66d76226242c9cbf179bae31b","id":"hatena","name":"はてな"},{"url":"http://delicious.com/","iconUrl":"http://friendfeed.com/static/images/icons/delicious.png?v=508c8593b0466b5ed38cce51e4d606de","id":"delicious","name":"delicious"},{"url":"http://en.wikipedia.org/wiki/Blog","iconUrl":"http://friendfeed.com/static/images/icons/blog.png?v=d3fe9e9d1dd09839623e597a92ecbb00","id":"blog","name":"Blog"},{"url":"http://www.furl.net/","iconUrl":"http://friendfeed.com/static/images/icons/furl.png?v=cb7c10774eab33db2c9324a311c98a2e","id":"furl","name":"Furl"},{"url":"http://wakoopa.com/","iconUrl":"http://friendfeed.com/static/images/icons/wakoopa.png?v=c7fae8a27dc2aa0ffbb643dc84c7e96c","id":"wakoopa","name":"Wakoopa"},{"url":"http://www.zooomr.com/","iconUrl":"http://friendfeed.com/static/images/icons/zooomr.png?v=0b09b8772b2cf8771bc960f5906ff478","id":"zooomr","name":"Zooomr"},{"url":"http://www.slideshare.net/","iconUrl":"http://friendfeed.com/static/images/icons/slideshare.png?v=72d15cab657167df4a85ea29869e02ff","id":"slideshare","name":"SlideShare"},{"url":"http://www.disqus.com/","iconUrl":"http://friendfeed.com/static/images/icons/disqus.png?v=82e8e708b4dd20b70566dea3c96b1f7e","id":"disqus","name":"Disqus"},{"url":"http://www.photobucket.com/","iconUrl":"http://friendfeed.com/static/images/icons/photobucket.png?v=d32484a3c3fad680df33c754dc94696d","id":"photobucket","name":"Photobucket"},{"url":"http://www.backtype.com/","iconUrl":"http://friendfeed.com/static/images/icons/backtype.png?v=9732c2b846991a5a9c1c95e76f64c5dd","id":"backtype","name":"Backtype"},{"url":"http://ma.gnolia.com/","iconUrl":"http://friendfeed.com/static/images/icons/magnolia.png?v=1e5962c3097569a0e8428b8d89708eac","id":"magnolia","name":"Ma.gnolia"},{"url":"http://reader.google.com/","iconUrl":"http://friendfeed.com/static/images/icons/googlereader.png?v=5996dd645157314e06bcaaf8f75b6ed9","id":"googlereader","name":"Google Reader"},{"url":"http://www.ilike.com/","iconUrl":"http://friendfeed.com/static/images/icons/ilike.png?v=1c4336e3e137ba1b9a9728a237176cdf","id":"ilike","name":"iLike"},{"url":"http://www.seesmic.com/","iconUrl":"http://friendfeed.com/static/images/icons/seesmic.png?v=820411ddbd7240fd6ce420fb9d253b5d","id":"seesmic","name":"Seesmic"},{"url":"http://meneame.net/","iconUrl":"http://friendfeed.com/static/images/icons/meneame.png?v=851e7d23ca1085f283c7a9f829e5f446","id":"meneame","name":"menéame"},{"url":"http://smotri.com/","iconUrl":"http://friendfeed.com/static/images/icons/smotri.png?v=c60d0cc846724e858eaed00179341daa","id":"smotri","name":"Smotri.com"},{"url":"http://friendfeed.com/","iconUrl":"http://friendfeed.com/static/images/icons/internal.png?v=e471e9afdf04ae568dcbddb5584fc6c0","id":"internal","name":"FriendFeed"},{"url":"http://www.last.fm/","iconUrl":"http://friendfeed.com/static/images/icons/lastfm.png?v=997d01ec84ef5b44cbf30814b02e37c7","id":"lastfm","name":"Last.fm"},{"url":"http://pownce.com/","iconUrl":"http://friendfeed.com/static/images/icons/pownce.png?v=29dea0ac91de74a720b4b1a7b5b4cd4d","id":"pownce","name":"Pownce"},{"url":"http://www.flickr.com/","iconUrl":"http://friendfeed.com/static/images/icons/flickr.png?v=77eeaefbcb3644cec0162a0938ec28e2","id":"flickr","name":"Flickr"},{"url":"http://www.netvibes.com/","iconUrl":"http://friendfeed.com/static/images/icons/netvibes.png?v=e0f3a31f0c2a983a78c03ad69c0d9951","id":"netvibes","name":"Netvibes"},{"url":"http://friendfeed.com/share/mail","iconUrl":"http://friendfeed.com/static/images/icons/email.png?v=afeecf2f63ccab29ff13faa8db88ee1a","id":"email","name":"Secret email address"},{"url":"http://www.goodreads.com/","iconUrl":"http://friendfeed.com/static/images/icons/goodreads.png?v=84260c598b3a96198047f7135988a007","id":"goodreads","name":"Goodreads"},{"url":"http://hi.baidu.com/","iconUrl":"http://friendfeed.com/static/images/icons/baidu.png?v=7eaf3a259ea44e0acabc0f5027812077","id":"baidu","name":"百度空间"},{"url":"http://reddit.com/","iconUrl":"http://friendfeed.com/static/images/icons/reddit.png?v=594c815da00db8b1eac942abf0287674","id":"reddit","name":"Reddit"},{"url":"http://identi.ca/","iconUrl":"http://friendfeed.com/static/images/icons/identica.png?v=d3e77f9f747d1548e9b6256c69fb177e","id":"identica","name":"identi.ca"},{"url":"http://upcoming.yahoo.com/","iconUrl":"http://friendfeed.com/static/images/icons/upcoming.png?v=4bcfe0790f4b062e4b64a88f79e3e358","id":"upcoming","name":"Upcoming"},{"url":"http://www.ameba.jp/","iconUrl":"http://friendfeed.com/static/images/icons/ameba.png?v=e4a16440f314790db3e59ec0c087ddd3","id":"ameba","name":"Ameba"},{"url":"http://www.mister-wong.com/","iconUrl":"http://friendfeed.com/static/images/icons/misterwong.png?v=b9ef4bdc94a3722c06b583f27b4fa288","id":"misterwong","name":"Mister Wong"},{"url":"http://www.twine.com/","iconUrl":"http://friendfeed.com/static/images/icons/twine.png?v=39942c8e2acbcfe4e4128f5cce57939f","id":"twine","name":"Twine"},{"url":"http://www.google.com/s2/sharing/stuff","iconUrl":"http://friendfeed.com/static/images/icons/googleshared.png?v=9558dc2725e2e335acede53d0a19b9e4","id":"googleshared","name":"Google Shared Stuff"},{"url":"http://www.amazon.com/","iconUrl":"http://friendfeed.com/static/images/icons/amazon.png?v=8e061e6b10f0db689f1cbe0caa57add5","id":"amazon","name":"Amazon.com"},{"url":"http://www.skyrock.com/","iconUrl":"http://friendfeed.com/static/images/icons/skyrock.png?v=911319e1ed573ce45e6fa5dd3078834d","id":"skyrock","name":"Skyrock"},{"url":"http://www.facebook.com/","iconUrl":"http://friendfeed.com/static/images/icons/facebook.png?v=3188364f61a5caec6cea3db52bd7ee92","id":"facebook","name":"Facebook"},{"url":"http://www.jaiku.com/","iconUrl":"http://friendfeed.com/static/images/icons/jaiku.png?v=b16111ed805954ca0a29901e84c0a606","id":"jaiku","name":"Jaiku"},{"url":"http://www.fotolog.com/","iconUrl":"http://friendfeed.com/static/images/icons/fotolog.png?v=cae77e8a9bcf1f72c628fd536fe7aa0a","id":"fotolog","name":"Fotolog"},{"url":"http://www.digg.com/","iconUrl":"http://friendfeed.com/static/images/icons/digg.png?v=a7190ae91cb7d1e3b541ed8ab07fa02c","id":"digg","name":"Digg"},{"url":"http://www.librarything.com/","iconUrl":"http://friendfeed.com/static/images/icons/librarything.png?v=6969a44b7af111d762fb735d26a2f372","id":"librarything","name":"LibraryThing"},{"url":"http://www.stumbleupon.com/","iconUrl":"http://friendfeed.com/static/images/icons/stumbleupon.png?v=dbf160cc0d5ba3658cad48e4377a966d","id":"stumbleupon","name":"StumbleUpon"},{"url":"http://www.yelp.com/","iconUrl":"http://friendfeed.com/static/images/icons/yelp.png?v=1d405b96391b12d810e2c0b0dc22467b","id":"yelp","name":"Yelp"},{"url":"http://www.netflix.com/","iconUrl":"http://friendfeed.com/static/images/icons/netflix.png?v=4a3c0d92fe12451fb26f1b9c57325e9c","id":"netflix","name":"Netflix"},{"url":"http://www.vimeo.com/","iconUrl":"http://friendfeed.com/static/images/icons/vimeo.png?v=4889864b885f9b47bb2a6c6a425f17e2","id":"vimeo","name":"Vimeo"},{"url":"http://www.mixx.com/","iconUrl":"http://friendfeed.com/static/images/icons/mixx.png?v=e93dca19b3b09b596e9dbaeeaff1e25b","id":"mixx","name":"Mixx"},{"url":"http://www.youtube.com/","iconUrl":"http://friendfeed.com/static/images/icons/youtube.png?v=c6c140ea173a7cfe98e5128620164d31","id":"youtube","name":"YouTube"},{"url":"http://picasaweb.google.com/","iconUrl":"http://friendfeed.com/static/images/icons/picasa.png?v=6252bea302277f1f51e534705d2ff0bf","id":"picasa","name":"Picasa Web Albums"},{"url":"http://tipjoy.com/","iconUrl":"http://friendfeed.com/static/images/icons/tipjoy.png?v=25abe1e8ce0a57cbb26e383dd84cba8d","id":"tipjoy","name":"tipjoy"},{"url":"http://talk.google.com/","iconUrl":"http://friendfeed.com/static/images/icons/googletalk.png?v=3b074086846ac5c0a711395ebf5d464a","id":"googletalk","name":"Gmail/Google Talk"},{"url":"http://www.pandora.com/","iconUrl":"http://friendfeed.com/static/images/icons/pandora.png?v=7b57d83d65f5fccbd51e3cf3e09c6b50","id":"pandora","name":"Pandora"},{"url":"http://www.smugmug.com/","iconUrl":"http://friendfeed.com/static/images/icons/smugmug.png?v=f1a49ac8f53f5823afafe07fb3fd09e2","id":"smugmug","name":"SmugMug"},{"url":"http://brightkite.com/","iconUrl":"http://friendfeed.com/static/images/icons/brightkite.png?v=5e24d43ca46845c0063a9e4bdf9d6377","id":"brightkite","name":"brightkite.com"},{"url":"http://www.intensedebate.com/","iconUrl":"http://friendfeed.com/static/images/icons/intensedebate.png?v=3d94d28c22142932a6927d112e706640","id":"intensedebate","name":"Intense Debate"},{"url":"http://www.plurk.com/","iconUrl":"http://friendfeed.com/static/images/icons/plurk.png?v=7c798cea14c13389a615756bd9d6ffdb","id":"plurk","name":"Plurk"},{"url":"http://www.diigo.com/","iconUrl":"http://friendfeed.com/static/images/icons/diigo.png?v=77441b008df933e10668f53891b0c566","id":"diigo","name":"Diigo"}]}
            .services,

        EOF:null
    };

    return $this;
})().init();

