Robin Enhancement Script

Highlight mentions, make links clickable, add tabbed channels & automatically remove spam

Από την 06/04/2016. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name		Robin Enhancement Script
// @namespace	https://www.reddit.com/
// @version		3.1.3
// @description	Highlight mentions, make links clickable, add tabbed channels & automatically remove spam
// @author		Bag, netnerd01
// @match		https://www.reddit.com/robin*
// @grant		none
// @grant		GM_setValue
// @grant		GM_getValue
// ==/UserScript==
(function() {

	// Grab users username + play nice with RES
	var robin_user = $("#header-bottom-right .user a").first().text();
	var ignored_users = {};

	// for spam counter - very important i know :P
	var blocked_spam_el = null;
	var blocked_spam = 0;
	var user_last_message = '';
	//
	var _robin_grow_detected = false;

	// Play nice with Greasemonkey
	if(typeof GM_getValue === "undefined") GM_getValue = function(){return false;};
	if(typeof GM_setValue === "undefined") GM_setValue = function(){return false;};

	/**
	 * Pull tabber out in to semi-stand alone module
	 * Big thanks to netnerd01 for his pre-work on this
	 *
	 * Basic usage - tabbedChannels.init( dom_node_to_add_tabs_to );
	 * and hook up tabbedChannels.proccessLine(lower_case_text, jquery_of_line_container); to each line detected by the system
	 */
	var tabbedChannels = new function(){
		var _self = this;

		// Default options
		this.channels = ["~","*",".","%","$","#",";","^","<3",":gov","#rpg","@"];
		this.mode = 'single';

		// internals
		this.unread_counts = {};
		this.$el = null;
		this.$opt = null;
		this.defaultRoomClasses = '';
		this.channelMatchingCache = [];

		//channels user is in currently
		this.currentRooms = 0;

		// When channel is clicked, toggle it on or off
		this.toggle_channel = function(e){
			var channel = $(e.target).data("filter");
			if(channel===null)return; // no a channel

			if(!$("#robinChatWindow").hasClass("robin-filter-" + channel)){
				_self.enable_channel(channel);
				$(e.target).addClass("selected");
				// clear unread counter
				$(e.target).find("span").text(0);
				_self.unread_counts[channel] = 0;
			}else{
				_self.disable_channel(channel);
				$(e.target).removeClass("selected");
			}

			// scroll everything correctly
			_scroll_to_bottom();
		};

		// Enable a channel
		this.enable_channel = function(channel_id){

			// if using room type "single", deslect other rooms on change
			if(this.mode == "single"){
				this.disable_all_channels();
			}

			$("#robinChatWindow").addClass("robin-filter robin-filter-" + channel_id);
			$("#robinChatWindow").attr("data-channel-key", this.channels[channel_id]);
			this.currentRooms++;
			// unselect show all 
			_self.$el.find("span.all").removeClass("selected");
		};

		// disable a channel
		this.disable_channel = function(channel_id){	
			$("#robinChatWindow").removeClass("robin-filter-" + channel_id);
			this.currentRooms--;

			// no rooms selcted, run "show all"
			if(this.currentRooms == 0){
				this.disable_all_channels();
			}else{
				// Grab next channel name if u leave a room in multi mode
				$("#robinChatWindow").attr("data-channel-key", $(".robin-filters span.selected").first().data("filter-name"));
			}
		};

		// turn all channels off
		this.disable_all_channels = function(e){
			$("#robinChatWindow").attr("class", _self.defaultRoomClasses).attr("data-channel-key","");
			_self.$el.find(".robin-filters > span").removeClass("selected");
			this.currentRooms = 0;

			_self.$el.find("span.all").addClass("selected");
			_scroll_to_bottom();
		};

		// render tabs
		this.drawTabs = function(){
			html = '';
			for(var i in this.channels){
				if(typeof this.channels[i] === 'undefined') continue;
				html += '<span data-filter="' + i + '" data-filter-name="'+ this.channels[i] +'">' + this.channels[i] + ' (<span>0</span>)</span> '; 
			}
			this.$el.find(".robin-filters").html(html);
		};

		// After creation of a new channel, go find if any content (not matched by a channel already) is relevant
		this.reScanChannels = function(new_channel){
			$("#robinChatWindow").find("div.robin-message").each(function(idx,item){
				var line = $(item).find(".robin-message--message").text().toLowerCase();
				tabbedChannels.proccessLine(line, $(item), true);
			});
		}

		// Add new channel
		this.addChannel = function(new_channel){
			if(this.channels.indexOf(new_channel) === -1){
				this.channels.push(new_channel);
				this.unread_counts[this.channels.length-1] = 0;
				this.updateChannelMatchCache();
				this.saveChannelList();
				this.drawTabs();

				// Populate content for channel
				this.reScanChannels();

				// refresh everything after redraw
				this.disable_all_channels();
			}
		};

		// remove existing channel
		this.removeChannel = function(channel){
			if(confirm("are you sure you wish to remove the " + channel + " channel?")){
				var idx = this.channels.indexOf(channel);
				delete this.channels[idx];
				this.updateChannelMatchCache();
				this.saveChannelList();
				this.drawTabs();

				// sub channels, will fall back to existing channels
				this.reScanChannels();

				// refresh everything after redraw
				this.disable_all_channels();
			}
		};


		// save channel list
		this.saveChannelList = function(){
			// clean array before save
			var channels = this.channels.filter(function (item) { return item != undefined });
			GM_setValue("robin-enhance-channels", channels);
		};

		// Change chat mode
		this.changeChannelMode = function(e){
			_self.mode = $(this).data("type");

			// swicth bolding
			$(this).parent().find("span").css("font-weight","normal");
			$(this).css("font-weight","bold");
			_self.disable_all_channels();

			// Update mode setting
			GM_setValue("robin-enhance-mode", _self.mode);
		};

		this.updateChannelMatchCache = function(){
			var order = this.channels.slice(0);
			order.sort(function(a, b){
			  return b.length - a.length; // ASC -> a - b; DESC -> b - a
			});
			for(var i in order){
				order[i] = this.channels.indexOf(order[i]);
			}
			// sorted array of channel name indexs

			this.channelMatchingCache = order;
		}

		// Procces each chat line to create text
		this.proccessLine = function(text, $element, rescan){
			var i, idx, channel;

			// If rescanning, clear any existing "channel" classes
			if(typeof rescan !== 'undefined' && rescan === true){
				$element.removeClass("in-channel");

				for(i=0; i <= this.channels.length; i++){
					$element.removeClass("robin-filter-" + i);
				}
			}

			// Scann for channel identifiers
			for(i=0; i< this.channelMatchingCache.length; i++){ // sorted so longer get picked out before shorter ones (sub channel matching)
				idx = this.channelMatchingCache[i];
				channel = this.channels[idx];

				if(typeof channel === 'undefined') continue;

				if(text.indexOf(channel) === 0){
					$element.addClass("robin-filter-" + idx +" in-channel");
					this.unread_counts[idx]++;
					return;
				}
			}
		};

		// If in one channel, auto add channel keys
		this.submit_helper = function(){
			if($("#robinChatWindow").hasClass("robin-filter")){
				// auto add channel key
				var channel_key = $("#robinChatWindow").attr("data-channel-key");

				if($(".text-counter-input").val().indexOf("/me") === 0){
					$(".text-counter-input").val("/me " + channel_key + " " + $(".text-counter-input").val().substr(3));
				}else if($(".text-counter-input").val().indexOf("/") !== 0){
					// if its not a "/" command, add channel
					$(".text-counter-input").val(channel_key + " " + $(".text-counter-input").val());
				}
			}
		};

		// Update everuything
		this.tick = function(){
			_self.$el.find(".robin-filters span").each(function(){
				if($(this).hasClass("selected")) return;
				$(this).find("span").text(_self.unread_counts[$(this).data("filter")]);
			});
		};

		// Init tab zone
		this.init = function($el){
			// Load channels
			if(GM_getValue("robin-enhance-channels")){
				this.channels = GM_getValue("robin-enhance-channels");
			}
			if(GM_getValue("robin-enhance-mode")){
				this.mode = GM_getValue("robin-enhance-mode");
			}

			// init counters
			for(var i in this.channels){
				this.unread_counts[i] = 0;
			}

			// update channel cache
			this.updateChannelMatchCache();

			// set up el
			this.$el = $el;

			// Create inital markup
			this.$el.html("<span class='all selected'>Everything</span><span><div class='robin-filters'></div></span><span class='more'>[Options]</span>");
			this.$opt = $("<div class='robin-channel-add' style='display:none'><input name='add-channel'><button>Add channel</button> <span class='channel-mode'>Channel Mode: <span title='View one channel at a time' data-type='single'>Single</span> | <span title='View many channels at once' data-type='multi'>Multi</span></span></div>").insertAfter(this.$el);

			// Attach events
			this.$el.find(".robin-filters").click(this.toggle_channel);
			this.$el.find("span.all").click(this.disable_all_channels);
			this.$el.find("span.more").click(function(){ $(".robin-channel-add").slideToggle(); });
			this.$el.find(".robin-filters").bind("contextmenu", function(e){
				e.preventDefault();
				e.stopPropagation();
				var chan_id = $(e.target).data("filter");
				if(chan_id===null)return; // no a channel
				_self.removeChannel(_self.channels[chan_id]);
			});
			// Form events
			this.$opt.find(".channel-mode span").click(this.changeChannelMode);
			this.$opt.find("button").click(function(){
				var new_chan = _self.$opt.find("input[name='add-channel']").val();
				if(new_chan != '') _self.addChannel(new_chan);
				_self.$opt.find("input[name='add-channel']").val('');
			});
			

			$("#robinSendMessage").submit(this.submit_helper);
			
			// store default room class
			this.defaultRoomClasses = $("#robinChatWindow").attr("class");

			// redraw tabs
			this.drawTabs();

			// start ticker
			setInterval(this.tick, 1000);
		}
	};

	/**
	 * Check if a message is "spam"
	 */
	var is_spam = function(line){
		return (
			// Hide auto vote messages
			(/^voted to (grow|stay|abandon)/.test(line)) ||
			// random unicode?
			(/[\u0080-\uFFFF]/.test(line)) ||
			// hide any auto voter messages
			(/\[.*autovoter.*\]/.test(line)) ||
			// Common bots
			(/^(\[binbot\]|\[robin-grow\])/.test(line)) ||
			// repeating chars in line (more than 5). e.g. aaaaaaa !!!!!!!!
			(/(.)\1{5,}/.test(line)) ||
			// Some common messages
			(/(voting will end in approximately|\[i spam the most used phrase\]|\[message from creator\]|\[.*bot.*\])/.test(line)) ||
			// no spaces = spam if its longer than 25 chars (dont filter links)
			(line.indexOf(" ") === -1 && line.length > 25 && line.indexOf("http") === -1) ||
			// repeating same word
			/(\b\S+\b)\s+\b\1\b/i.test(line)
		);
	};

	/**
	 * Check if a message is from an ignored user
	 *
	 */
	var is_ignored = function($usr, $ele){
		// no user name, go looking for when said it
		if($usr.length === 0){
			while($usr.length === 0){
				$ele = $ele.prev();
				$usr = $ele.find(".robin--username");
			}
		}
		// are they ignored?
		return (ignored_users[$usr.text()]);
	};

	/**
	 * Make links clickable
	 *
	 */
	var auto_link = function($msg){
		var text = $msg.html(); // read as html so stuff stays escaped
		// normal links
		text = text.replace(/\b(?:https?|ftp):\/\/[a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|]/gim, '<a target="blank" href="$&">$&</a>');
		// reddit subreddit links
		text = text.replace(/ \/r\/(\w+)/gim, ' <a target="blank" href="https://reddit.com/r/$1">/r/$1</a>');
		// update text
		$msg.html(text);
	};

	/**
	 * Mute a user
	 */
	var _mute_user = function(usr){
		// Add to ignore list
		ignored_users[usr] = true;
		_render_muted_list();
	};

	/**
	 * un-mute a user
	 */
	var _unmute_user = function(usr){
		// Add to ignore list
		delete ignored_users[usr];
		_render_muted_list();
	};

	// Render list of ignored users
	var _render_muted_list = function(){
		var html = "<strong>Ignored users</strong><br>";
		for(var u in ignored_users){
			html += "<div data-usr='"+ u + "'>" + u + " - [unmute]</div>";
		}
		$("#muted_users").html(html);
	};

	// Scroll chat back to bottom
	var _scroll_to_bottom = function(){
		$("#robinChatWindow").scrollTop($("#robinChatMessageList").height());
	};

	var update_spam_count = function(){
		blocked_spam++;
		blocked_spam_el.innerHTML = blocked_spam;
	};

	var fill_name = function(e){
		e.preventDefault();
		e.stopPropagation();

		// if text area blank, prefill name. if not, stick it on the end
		if($(".text-counter-input").val() === ''){
			$(".text-counter-input").val($(this).text() + ' ').focus();
		}else{
			$(".text-counter-input").val($(".text-counter-input").val() + ' ' + $(this).text()).focus();
		}
	};

	/**
	 * Parse a link and apply changes
	 */
	var parse_line = function($ele){
		var $msg = $ele.find(".robin-message--message");
		var $usr = $ele.find(".robin--username");
		var line = $msg.text().toLowerCase();

		// dont parse system messages
		if($ele.hasClass("robin--user-class--system")){
			if(line.indexOf("ratelimit | you are doing that too much") !== -1){
				// channel key length + a space length
				var offset = $("#robinChatWindow").attr("data-channel-key").length + 1;
				$(".text-counter-input").val(user_last_message.slice(offset));
			}
			return;
		}

		// If user is ignored or message looks like "Spam". hide it
		if (is_ignored($usr, $ele) || is_spam(line)) {
			$ele.addClass("spam-hidden");
			update_spam_count();
		}

		// Highlight mentions
		if(line.indexOf(robin_user) !== -1){
			$ele.addClass("user-mention");
		}

		// Make links clickable
		if(!_robin_grow_detected && line.indexOf("http") !== -1){
			auto_link($msg);
		}

		// Add mute button to users
		if(!$ele.hasClass("robin--user-class--system") && $usr.text() != robin_user){
			$("<span style='font-size:.8em;cursor:pointer'> [mute] </span>").insertBefore($usr).click(function(){
				_mute_user($usr.text());
			});
		}

		// Track channels
		tabbedChannels.proccessLine(line, $ele);

		// bind click to use (override other click events if we can)
		$usr.bindFirst("click", fill_name);
	};


	// Detect changes, are parse the new message
	$("#robinChatWindow").on('DOMNodeInserted', function(e) {
		if ($(e.target).is('div.robin-message')) {
			// Apply changes to line
			parse_line($(e.target));
		}
	});

	// When everything is ready
	$(document).ready(function(){

		// Set default spam filter type
		$("#robinChatWindow").addClass("hide-spam");

		// Add checkbox to toggle "hide" behaviors
		$("#robinDesktopNotifier").append("<label><input type='checkbox' checked='checked'>Hide spam completely (<span id='spamcount'>0</span> removed)</label>").click(function(){
			if($(this).find("input").is(':checked')){
				$("#robinChatWindow").removeClass("mute-spam").addClass("hide-spam");
			}else{
				$("#robinChatWindow").removeClass("hide-spam").addClass("mute-spam");
			}
			// correct scroll after spam filter change
			_scroll_to_bottom();
		});

		blocked_spam_el = $("#spamcount")[0];

		// Add Muted list & hook up unmute logic
		$('<div id="muted_users" class="robin-chat--sidebar-widget robin-chat--notification-widget"><strong>Ignored users</strong></div>').insertAfter($("#robinDesktopNotifier"));
		$('#muted_users').click(function(e){
			var user = $(e.target).data("usr");
			if(user) _unmute_user(user);
		});

		// Init tabbed channels
		tabbedChannels.init($('<div id="filter_tabs"></div>').insertAfter("#robinChatWindow"));

		// store i copy of last message, in case somthing goes wrong (rate limit)
		$("#robinSendMessage").submit(function(){
			user_last_message = $(".text-counter-input").val();
		});

	});

	// fix by netnerd01
	var stylesheet = document.createElement('style');
	document.head.appendChild(stylesheet);
	stylesheet = stylesheet.sheet;

	// filter for channel
	stylesheet.insertRule("#robinChatWindow.robin-filter div.robin-message { display:none; }", 0);
	stylesheet.insertRule("#robinChatWindow.robin-filter div.robin-message.robin--user-class--system  { display:block; }", 0);
	for(var c=0;c<35;c++){
		stylesheet.insertRule("#robinChatWindow.robin-filter.robin-filter-"+c+" div.robin-message.robin-filter-"+c+" { display:block; }", 0);
	}

	// Styles for filter tabs
	stylesheet.insertRule("#filter_tabs {width:100%; display: table; table-layout: fixed; background:#d7d7d2; border-bottom:1px solid #efefed;}",0);
	stylesheet.insertRule("#filter_tabs > span {width:90%; display: table-cell;}",0);
	stylesheet.insertRule("#filter_tabs > span.all, #filter_tabs > span.more {width:60px; text-align:center; vertical-align:middle; cursor:pointer;}",0);
	stylesheet.insertRule("#filter_tabs > span.all.selected, #filter_tabs > span.all.selected:hover {background: #fff;}", 0);
	stylesheet.insertRule("#filter_tabs .robin-filters { display: table; width:100%;table-layout: fixed; '}", 0);
	stylesheet.insertRule("#filter_tabs .robin-filters > span { padding: 5px 2px;text-align: center; display: table-cell; cursor: pointer;width:2%; vertical-align: middle; font-size: 1.1em;}", 0);
	stylesheet.insertRule("#filter_tabs .robin-filters > span.selected, #filter_tabs .robin-filters > span:hover { background: #fff;}", 0);
	stylesheet.insertRule("#filter_tabs .robin-filters > span > span {pointer-events: none;}", 0);

	stylesheet.insertRule(".robin-channel-add  {padding:5px; display:none;}", 0);
	stylesheet.insertRule(".robin-channel-add input {padding: 2.5px; }", 0);
	stylesheet.insertRule(".robin-channel-add .channel-mode {float:right; font-size:1.2em;padding:5px;}", 0);
	stylesheet.insertRule(".robin-channel-add .channel-mode span {cursor:pointer}", 0);
	//mentions should show even in filter view
	stylesheet.insertRule("#robinChat #robinChatWindow div.robin-message.user-mention { display:block; font-weight:bold; }", 0);

	// Add initial styles for "spam" messages
	stylesheet.insertRule("#robinChat #robinChatWindow.hide-spam div.robin-message.spam-hidden { display:none; }", 0);
	stylesheet.insertRule("#robinChat #robinChatWindow.mute-spam div.robin-message.spam-hidden { opacity:0.3; font-size:1.2em; }", 0);

	// muted user box
	stylesheet.insertRule("#muted_users { font-size:1.2em; }", 0);
	stylesheet.insertRule("#muted_users div { padding: 2px 0; }", 0);
	stylesheet.insertRule("#muted_users strong { font-weight:bold; }", 0);

	// FIX RES nightmode (ish) [ by Kei ]
	stylesheet.insertRule(".res-nightmode #robinChatWindow div.robin-message { color: #ccc; }", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--sidebar-widget { background: #222; color: #ccc;}", 0);
	stylesheet.insertRule(".res-nightmode .robin-room-participant { background: #222; color: #999;}", 0);
	stylesheet.insertRule(".res-nightmode #filter_tabs {background: rgb(51, 51, 51);}", 0);
	stylesheet.insertRule(".res-nightmode #filter_tabs  .robin-filters > span.selected,.res-nightmode #filter_tabs .robin-filters > span:hover,.res-nightmode #filter_tabs > span.all.selected,.res-nightmode #filter_tabs > span.all:hover {background: rgb(34, 34, 34)}", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--input { background: #222 }", 0);
	stylesheet.insertRule(".res-nightmode .robin--presence-class--away .robin--username {color: #999;}", 0);
	stylesheet.insertRule(".res-nightmode .robin--presence-class--present .robin--username {color: #ccc;}", 0);
	stylesheet.insertRule(".res-nightmode #robinChat .robin--user-class--self .robin--username { color: #999; }", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--vote { background: #777; color: #ccc;}", 0);
	stylesheet.insertRule(".res-nightmode .robin-chat--buttons button.robin-chat--vote.robin--active { background: #ccc; color:#999; }", 0);

	$(document).ready(function(){
		setTimeout(function(){
			// Play nice with robin grow (makes room for tab bar we insert)
			if($(".usercount.robin-chat--vote").length !== 0){
				_robin_grow_detected = true;
				stylesheet.insertRule("#robinChat.robin-chat .robin-chat--body { height: calc(100vh - 150px); }", 0);
			}
		},500);
	});

	// Allow me to sneek functions in front of other libaries - used when working with robin grow >.< sorry guys
	//http://stackoverflow.com/questions/2360655/jquery-event-handlers-always-execute-in-order-they-were-bound-any-way-around-t
	$.fn.bindFirst = function(name, fn) {
		// bind as you normally would
		// don't want to miss out on any jQuery magic
		this.on(name, fn);

		// Thanks to a comment by @Martin, adding support for
		// namespaced events too.
		this.each(function() {
			var handlers = $._data(this, 'events')[name.split('.')[0]];
			// take out the handler we just inserted from the end
			var handler = handlers.pop();
			// move it at the beginning
			handlers.splice(0, 0, handler);
		});
	};

})();