您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Highlight mentions, make links clickable, add tabbed channels & automatically remove spam
// ==UserScript== // @name Robin Enhancement Script // @namespace https://www.reddit.com/ // @version 3.3.5 // @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().toLowerCase(); var ignored_users = {}; // for spam counter - very important i know :P var blocked_spam_el = null; var blocked_spam = 0; // via RobinEggs var messageHistory = []; var messageHistoryIndex = -1; var _robin_grow_detected = false; var colors = [ 'rgba(255,0,0,0.1)', 'rgba(0,255,0,0.1)', 'rgba(0,0,255,0.1)', 'rgba(0,255,255,0.1)', 'rgba(255,0,255,0.1)', 'rgba(255,255,0,0.1)', 'rgba(211,211,211, .1)', 'rgba(0,100,0, .1)', 'rgba(255,20,147, .1)', 'rgba(184,134,11, .1)', ]; // 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(/\s+\/r\/(\w+)\/?/gi, ' <a target="blank" href="https://reddit.com/r/$1">/r/$1</a>'); text = text.replace(/\s+\/u\/(\w+)\/?/gi, ' <a target="blank" href="https://reddit.com/u/$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()); }; // create persistant option function createOption(name, click_action, default_state){ var checked_markup; var key = "robin-enhance-" + name.replace(/\W/g, ''); var state = (typeof default_state !== "undefined") ? default_state : false; // try and state if setting is defined if(GM_getValue(key)){ state = (GM_getValue(key) === 'true') ? true : false; } // markup for state checked_markup = (state === true) ? "checked='checked'" : ""; // render option var $option = $("<label><input type='checkbox' "+checked_markup+">"+name+"</label>").click(function(){ var checked = $(this).find("input").is(':checked'); // persist state if(checked != state){ GM_setValue(key, checked ? 'true' : 'false'); // true/false stored as strings, to avoid unset matching state = checked; } click_action(checked, $(this)); }); // add to dom $("#robinDesktopNotifier").append($option); // init click_action(state, $option) }; // update spam count var update_spam_count = function(){ blocked_spam++; blocked_spam_el.innerHTML = blocked_spam; }; // when name is clicked, fill it into the chat box 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(); } }; // remove channel key from message var remove_channel_key_from_message = function(message){ if($("#robinChatWindow").attr("data-channel-key")){ var offset = $("#robinChatWindow").attr("data-channel-key").length; if(offset === 0) return message; if(message.indexOf("/me") === 0){ return "/me "+ message.slice(offset+5); }else{ return message.slice(offset+1); } } return message; } /** * 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){ $(".text-counter-input").val(messageHistory[messageHistoryIndex-1]); } 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 || line.indexOf("/r/") !== -1 || line.indexOf("/u/") !== -1)){ auto_link($msg); } // Add mute button to users if(!$ele.hasClass("robin--user-class--system") && $usr.text().toLowerCase() != 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"); createOption("Hide spam completely (<span id='spamcount'>0</span> removed)", function(checked, ele){ if(checked){ $("#robinChat").removeClass("mute-spam").addClass("hide-spam"); }else{ $("#robinChat").removeClass("hide-spam").addClass("mute-spam"); } // correct scroll after spam filter change _scroll_to_bottom(); },true); createOption("Use channel colors", function(checked, ele){ if(checked){ $("#robinChat").addClass("show-colors"); }else{ $("#robinChat").removeClass("show-colors"); } // correct scroll after spam filter change _scroll_to_bottom(); },false); 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(){ var user_last_message = $(".text-counter-input").val(); // if message history is to long, clear it out if(messageHistory.length === 25){ messageHistory = messageHistory.shift(); } messageHistory.push(remove_channel_key_from_message(user_last_message)); messageHistoryIndex = messageHistory.length; }); // up for last message send, down for prev (if moving between em) $('input.text-counter-input').on('keydown', function(e) { if(e.keyCode == 38) { e.preventDefault(); messageHistoryIndex--; if(messageHistoryIndex > -1){ $(this).val(messageHistory[messageHistoryIndex]); } }else if(e.keyCode == 40){ e.preventDefault(); if(messageHistoryIndex <= messageHistory.length){ messageHistoryIndex++; $(this).val(messageHistory[messageHistoryIndex]); }else{ $(this).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); var color; for(var c=0;c<35;c++){ color = colors[(c % (colors.length))]; stylesheet.insertRule("#robinChat.show-colors #robinChatWindow div.robin-message.robin-filter-"+c+" { background: "+color+";}", 0); 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.hide-spam #robinChatWindow div.robin-message.spam-hidden { display:none; }", 0); stylesheet.insertRule("#robinChat.mute-spam #robinChatWindow div.robin-message.spam-hidden { opacity:0.3; font-size:1.2em; }", 0); stylesheet.insertRule("#robinChat.show-colors #robinChatWindow 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); }); }; })();