Friendfeed Keyboard Shortcuts

By trentono Last update Sep 11, 2008 — Installed 148 times. Daily Installs: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0

There are 2 previous versions of this script.

// ==UserScript==
// @name           Friendfeed Keyboard Shortcuts
// @namespace      http://userscripts.org/users/44035
// @description    Like Google Reader keyboard shortcuts, but for FF! w00t.
// @include        http://friendfeed.com/*
// @include         http://beta.friendfeed.com/*
// @exclude        http://friendfeed.com/settings*
// ==/UserScript==

unsafeWindow.document.onkeydown = KeyCheck;
unsafeWindow.document.onkeyup = checkModifier;
unsafeWindow.document.onclick = mouseUse;


var currentCluster, currentEntry;
var modifierShift = 0;
var f=0, g=0;

function addGlobalStyle(css) {
    var head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
}

addGlobalStyle('div.selectedEntry { margin-left: -18px; padding-left: 13px; border-left: 5px solid #D7E4F4;}');

function GM_wait() 
{
	if(typeof unsafeWindow.jQuery == 'undefined') 
	{ 
		window.setTimeout(GM_wait,100); 
	}
	else 
	{ 
		$ = unsafeWindow.jQuery; letsJQuery(); 
	}
}

function addHandlers() {	// if "Comment" link is clicked, go into typing mode
	$('.feed').find('.l_comment').each(function () {
		this.onclick = function () {
			unsafeWindow.document.onkeydown = typingComment;
			unsafeWindow.document.onclick = typingMouse;
			}
		});
	$('.feed').find('.l_editcomment').each(function () {
		this.onclick = function () {
			unsafeWindow.document.onkeydown = typingComment;
			unsafeWindow.document.onclick = typingMouse;
			}
		});		
	$('input.query').each(function () { 
		this.onfocus = function () {
			unsafeWindow.document.onkeydown = typingComment;
			unsafeWindow.document.onclick = typingMouse;
			}
		this.onblur = function () {
			unsafeWindow.document.onkeydown = KeyCheck;
			unsafeWindow.document.onclick = mouseUse;
			}
		});
	$('.sharebox textarea.title').each(function () {
		this.onfocus = function () {
			unsafeWindow.document.onkeydown = typingComment;
			unsafeWindow.document.onclick = typingMouse;
			}
		this.onblur = function () {
			this.onblur;
			unsafeWindow.document.onkeydown = KeyCheck;
			unsafeWindow.document.onclick = mouseUse;
			}
		});
}

GM_wait();
addHandlers();


function mouseUse(event) {
	var tgt = event.target;
	if ($(tgt).parents('.entry').length != 0) {
		var oldEntry = currentEntry;
		oldEntry.toggleClass('selectedEntry');
		currentCluster = $(tgt).parents('.cluster');
		currentEntry = $(tgt).parents('.entry').toggleClass('selectedEntry');
	}
}

function checkModifier(event) {

	var key = event.keyCode;
	if (key == 16) modifierShift = 0;
	
}

function KeyCheck(event)
{
	var key = event.keyCode;
	switch(key)
	{
		case 16: //shift
			modifierShift = 1;
			break;
		case 65: // a  -- todo: need to fix odd entry selection behaviour after expanding cluster
			triggerClick(currentCluster.find('.l_expandcluster'));
			break;
		case 67: //c  -- todo: clear out comment form (the 'c' keypress appears in the form)
			triggerClick(currentEntry.find('.l_comment'));
			event.preventDefault();
			unsafeWindow.document.onkeydown = typingComment;
			unsafeWindow.document.onclick = typingMouse;
			break;
		case 68: // d
			if (g) {
				g=0;
				location.href = '/summary?days=1';
			}
			break;
		case 69: //e
			if (g) {
				g=0;
				location.href = '/public';
			} else {
				triggerClick(currentEntry.find('.l_expandcomments'));
			}
			break;
		case 74: //j  -- suggested behaviour when at bottom of page: move to first entry on next page (and vice versa with 'k')
			var oldEntry = currentEntry;
			currentEntry = currentEntry.toggleClass('selectedEntry').next('.entry').toggleClass('selectedEntry');
			if (currentEntry.length != 0) {
				$.scrollTo(currentEntry, 1, {offset: -150} );
			} else {
				var oldCluster = currentCluster;
				currentCluster = currentCluster.next('.cluster');
				if(currentCluster.length != 0) {
					currentEntry = currentCluster.find('.entry:first');
					currentEntry.toggleClass('selectedEntry');
					$.scrollTo( currentEntry, 1, {offset: -150} );
				} else {
					currentCluster = oldCluster;
					currentEntry = oldEntry.toggleClass('selectedEntry');
				}
			}
			break;
		case 75: //k
			currentEntry = currentEntry.toggleClass('selectedEntry').prev('.entry').toggleClass('selectedEntry');
			if (currentEntry.length != 0) {
				$.scrollTo( currentEntry, 1, {offset: -150} );
			} else {
				currentCluster = currentCluster.prev('.cluster');
				if(currentCluster.length != 0) {
					currentEntry = currentCluster.find('.entry:last');
					currentEntry.toggleClass('selectedEntry');
					$.scrollTo( currentEntry, 1, {offset: -150} );
				} else {
					currentCluster = $('.feed .cluster:first');
					currentEntry = currentCluster.find('.entry:first');
					currentEntry.toggleClass('selectedEntry');
				}
			}
			break;
		case 72: //h
			if (g) {
				g=0;
				location.href = '/';
			} else {
				var link = currentEntry.find('.l_hideone');
				if (link.length == 0) link = currentEntry.find('.l_unhideone');
				triggerClick(link);
			}
			break;
		case 76: //l
			var link = currentEntry.find('.l_unlike');
			if (link.length == 0) link = currentEntry.find('.l_like');
			triggerClick(link);
			break;
		case 13: //enter
			if (f) {
				openLink(currentCluster.find('.summary .l_person:first'));
				endHover(currentCluster.find('.summary .l_person:first'));
				f=0;
				break;
			}
		case 86: // v
			openLink(currentEntry.find('.main'));
			break;
		case 70: // f
			if (!f) {
				triggerHover(currentCluster.find('.summary .l_person:first'));
				f=1;
			} else {
				endHover(currentCluster.find('.summary .l_person:first'));
				f=0;
			}
			break;
		case 71: // g
			g=1;
			break;
		case 77: // m
			if (g) {
				g=0;
				var profile = document.getElementById('header').getElementsByTagName('a')[0].href;
				location.href = profile;
			} else {
				triggerClick(currentEntry.find('.l_expandmedia'));
				triggerClick(currentEntry.find('.l_audio'));
				triggerClick(currentEntry.find('a.l_play'));
			}
			break;
		case 80: // p
			event.preventDefault();
			$('.sharebox textarea.title').each(function () { this.focus(); });
			break;
		case 82: // r
			if (g) {
				g=0;
				location.href = '/rooms/';
			} else {
				location.href = location.href;
			}
			break;
		case 83: // s
			if (g) {
				g=0;
				location.href = '/settings/subscriptions';
			} else {
				event.preventDefault();
				triggerClick(currentEntry.find('.l_more'));
				triggerClick($('.l_reshare'));
				unsafeWindow.document.onkeydown = typingComment;
				unsafeWindow.document.onclick = typingMouse;
			}
			break;
		case 84: // t  -- doesn't function yet
			triggerClick($('.tabs .selected').next('.tab').find('a'));
			break;
		case 87: // w
			if (g) {
				g=0;
				location.href = '/summary?days=7';
			}
			break;
		case 191: // /
			if (modifierShift) {
				showHelp();
				break;
			}
			event.preventDefault();
			$('input.query').each(function () { this.focus(); });
			break;
	}
	addHandlers();
}

function endHover(link) {
	link.each(function () { 
		var evt = document.createEvent("HTMLEvents");
		evt.initEvent("mouseout", true, true);
		this.dispatchEvent(evt);
	});
}

function triggerHover(link) {
	link.each(function () { 
		var evt = document.createEvent("HTMLEvents");
		evt.initEvent("mouseover", true, true);
		this.dispatchEvent(evt);
	});
}

// todo: make a pretty help, not just a fugly alert box.
function showHelp() {
	alert("friendfeed keyboard shortcuts:\nj: down (next entry)\nk: up (previous entry)\nl: like/unlike\n" +
		"c: comment\nh: hide/unhide\ne: expand comments\nm: expand media\n" +
		"a: expand all itmes in cluster (buggy)\nv or ENTER: open link in new tab/window\n" + 
		"f: show/hide poster's userpopup\n\t(if userpopup open: ENTER: open user's page)\n" +
		"p: post\n/: search\nr: refresh page\ng then m: go to \"My Feed\"\ng then r: go to \"Rooms\"\n" +
		"g then e: go to \"Everyone\"\ng then h: go to \"Home\" feed\ng then s: go to Friend Settings\n" + 
		"g then d: go to Best of Day\ng then w: go to Best of Week\n?: this help");
}

function openLink(link) {
	link.each(function () {
		unsafeWindow.open(this.href);
	});
}

function triggerClick(link) {
	link.each(function () { 
		var evt = document.createEvent("HTMLEvents");
		evt.initEvent("click", true, true);
		this.dispatchEvent(evt);
	});
}	

function typingMouse(event) {
	if (event.target.value == "Post" || event.target.value == "Search" || event.target.value == "Share on FriendFeed") {
		unsafeWindow.document.onkeydown = KeyCheck;
		unsafeWindow.document.onclick = mouseUse;
		return;
	}
}

function typingComment(event) {
	var key = event.keyCode;
	if (key == 13) {
		unsafeWindow.document.onkeydown = KeyCheck;
		unsafeWindow.document.onclick = mouseUse;
	}
	if (key == 27) {
		triggerClick(currentEntry.find('.l_cancelcomment'));
		unsafeWindow.document.onkeydown = KeyCheck;
		unsafeWindow.document.onclick = mouseUse;
	}
	// todo:  cancelling the comment form via mouseclick needs to return the onkeydown event handlers to KeyCheck, mouseUse
}
	


var o=$.scrollTo=function(a,b,c){o.window().scrollTo(a,b,c)};o.defaults={axis:'y',duration:1};o.window=function(){return $($.browser.safari?'body':'html')};$.fn.scrollTo=function(l,m,n){if(typeof m=='object'){n=m;m=0}n=$.extend({},o.defaults,n);m=m||n.speed||n.duration;n.queue=n.queue&&n.axis.length>1;if(n.queue)m/=2;n.offset=j(n.offset);n.over=j(n.over);return this.each(function(){var a=this,b=$(a),t=l,c,d={},w=b.is('html,body');switch(typeof t){case'number':case'string':if(/^([+-]=)?\d+(px)?$/.test(t)){t=j(t);break}t=$(t,this);case'object':if(t.is||t.style)c=(t=$(t)).offset()}$.each(n.axis.split(''),function(i,f){var P=f=='x'?'Left':'Top',p=P.toLowerCase(),k='scroll'+P,e=a[k],D=f=='x'?'Width':'Height';if(c){d[k]=c[p]+(w?0:e-b.offset()[p]);if(n.margin){d[k]-=parseInt(t.css('margin'+P))||0;d[k]-=parseInt(t.css('border'+P+'Width'))||0}d[k]+=n.offset[p]||0;if(n.over[p])d[k]+=t[D.toLowerCase()]()*n.over[p]}else d[k]=t[p];if(/^\d+$/.test(d[k]))d[k]=d[k]<=0?0:Math.min(d[k],h(D));if(!i&&n.queue){if(e!=d[k])g(n.onAfterFirst);delete d[k]}});g(n.onAfter);function g(a){b.animate(d,m,n.easing,a&&function(){a.call(this,l)})};function h(D){var b=w?$.browser.opera?document.body:document.documentElement:a;return b['scroll'+D]-b['client'+D]}})};function j(a){return typeof a=='object'?a:{top:a,left:a}}

function letsJQuery() 
{
	currentCluster = $('.feed .cluster:first');
	currentEntry = currentCluster.find('.entry:first');
	currentEntry.toggleClass('selectedEntry');

	at = new RegExp(/name="at" value="([\S\s]*?)"/ig).exec(unsafeWindow.gShareMainForm);
	//console.log(at[1].toString());
}