From b184fc74b22a87625a6171318b4f58ee882eb72c Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 10 Jun 2015 18:46:04 -0400 Subject: [PATCH] An update! --- dark.css | 34 +++ script.js | 467 ++++++++++++++++++++++++++++++++++++------ script.min.js | 8 +- src/badges.js | 16 +- src/ember/channel.js | 6 +- src/ember/chatview.js | 41 ++++ src/ember/line.js | 53 ++++- src/ember/room.js | 194 +++++++++++++++++- src/main.js | 4 +- src/socket.js | 29 ++- src/tokenize.js | 8 +- src/ui/menu.js | 7 +- src/ui/my_emotes.js | 71 ++++--- src/utils.js | 38 +++- style.css | 54 ++++- 15 files changed, 905 insertions(+), 125 deletions(-) diff --git a/dark.css b/dark.css index dd858c3b..baeadaaa 100644 --- a/dark.css +++ b/dark.css @@ -104,6 +104,7 @@ /* main column */ +.ffz-dark div#mantle_skin, .ffz-dark div#main_col { background:rgb(16,16,16); color:rgb(195,195,195)!important; @@ -422,6 +423,7 @@ border-color: #32323e; } +.ffz-dark ul.mininav li.active, .ffz-dark ul.tabs li.selected a, .ffz-dark .directory_header .nav li.selected a, .ffz-dark ul.tabs_fake li.selected a, @@ -776,6 +778,38 @@ } +/* /p/ Pages */ + +.ffz-dark #mantle_skin .wrapper { + background-color: transparent; +} + +.ffz-dark #mantle_skin ul.vtabs li .not_linked, +.ffz-dark #mantle_skin ul.vtabs li.selected a, +.ffz-dark #mantle_skin ul.submenu li a:hover, +.ffz-dark #mantle_skin ul.submenu .active a { + color: #fff !important; +} + +.ffz-dark #mantle_skin ul.vtabs li a:hover { + color: rgb(222,222,222); + background-color: transparent; +} + +.ffz-dark #mantle_skin .press_list .callout { + color: #acacbf; + border-left-color: #575757; +} + +.ffz-dark .legal_page ol.legal li { + color: rgb(222,222,222); +} + +.ffz-dark .legal_page ol.legal li p { + color: #ccc; +} + + /* TEST .ffz-dark div#main_col .tse-scroll-content { background-image: diff --git a/script.js b/script.js index eabb19d5..67b364cc 100644 --- a/script.js +++ b/script.js @@ -170,6 +170,20 @@ FFZ.prototype.render_badge = function(component) { if ( ! data || ! data.badges ) return; + // If we don't have badges, add them. + if ( ! badges.length ) { + var b_cont = document.createElement('span'), + from = component.$('.from'); + + b_cont.className = 'badges float-left'; + + if ( ! from ) + return; + + from.before(b_cont); + badges = $(b_cont); + } + // Figure out where to place our badge(s). var before = badges.find('.badge').filter(function(i) { var t = this.title.toLowerCase(); @@ -223,7 +237,7 @@ FFZ.prototype.render_badge = function(component) { if ( reverse ) { while(badges_out.length) - before.before(badges_out.shift()[1]); + badges.before(badges_out.shift()[1]); } else { while(badges_out.length) badges.append(badges_out.shift()[1]); @@ -536,9 +550,9 @@ FFZ.prototype.setup_channel = function() { if ( f._cindex ) f._cindex.ffzFixTitle(); - }.observes("content.status", "content.id") + }.observes("content.status", "content.id"), - /*ffzHostTarget: function() { + ffzHostTarget: function() { var target = this.get('content.hostModeTarget'), name = target && target.get('name'), display_name = target && target.get('display_name'); @@ -548,7 +562,7 @@ FFZ.prototype.setup_channel = function() { if ( f.settings.group_tabs && f._chatv ) f._chatv.ffzRebuildTabs(); - }.observes("content.hostModeTarget")*/ + }.observes("content.hostModeTarget") }); } @@ -838,6 +852,47 @@ var FFZ = window.FrankerFaceZ, // Settings // -------------------- +FFZ.settings_info.prevent_clear = { + type: "boolean", + value: false, + + no_bttv: true, + + category: "Chat", + name: "Show Deleted Messages", + help: "Fade deleted messages instead of replacing them, and prevent chat from being cleared.", + + on_update: function(val) { + if ( this.has_bttv || ! this.rooms ) + return; + + for(var room_id in this.rooms) { + var ffz_room = this.rooms[room_id], + room = ffz_room && ffz_room.room; + if ( ! room ) + continue; + + room.get("messages").forEach(function(s, n) { + if ( val && ! s.ffz_deleted && s.deleted ) + room.set("messages." + n + ".deleted", false); + + else if ( s.ffz_deleted && ! val && ! s.deleted ) + room.set("messages." + n + ".deleted", true); + }); + } + } + }; + +FFZ.settings_info.chat_history = { + type: "boolean", + value: true, + + visible: false, + category: "Chat", + name: "Chat History Alpha", + help: "Load previous chat messages when loading a chat room so you can see what people have been talking about. This currently only works in a handful of channels due to server capacity.", + }; + FFZ.settings_info.group_tabs = { type: "boolean", value: false, @@ -1677,13 +1732,29 @@ FFZ.prototype.setup_line = function() { this._twitch_emotes = {}; this._link_data = {}; + this.log("Hooking the Ember Whisper controller."); + var Whisper = App.__container__.resolve('component:whisper-line'); + + if ( Whisper ) + this._modify_line(Whisper); this.log("Hooking the Ember Line controller."); - var Line = App.__container__.resolve('component:message-line'), - f = this; + var Line = App.__container__.resolve('component:message-line'); - Line.reopen({ + if ( Line ) + this._modify_line(Line); + + // Store the capitalization of our own name. + var user = this.get_user(); + if ( user && user.name ) + FFZ.capitalization[user.login] = [user.name, Date.now()]; +} + +FFZ.prototype._modify_line = function(component) { + var f = this; + + component.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. var tokens = this.get("msgObject.cachedTokens"); @@ -1731,6 +1802,10 @@ FFZ.prototype.setup_line = function() { }.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"), + ffzUpdated: Ember.observer("msgObject.ffz_deleted", "msgObject.ffz_old_messages", function() { + this.rerender(); + }), + didInsertElement: function() { this._super(); try { @@ -1753,12 +1828,29 @@ FFZ.prototype.setup_line = function() { } el.classList.toggle('ffz-alternate', row_type); + el.classList.toggle('ffz-deleted', f.settings.prevent_clear && this.get('msgObject.ffz_deleted')); // Basic Data el.setAttribute('data-room', room); el.setAttribute('data-sender', user); - el.setAttribute('data-deleted', this.get('deleted')||false); + el.setAttribute('data-deleted', this.get('msgObject.deleted')||false); + + + // Old Messages (for Chat Clear) + var old_messages = this.get("msgObject.ffz_old_messages"); + if ( old_messages && old_messages.length ) { + var btn = document.createElement('div'); + btn.className = 'button primary float-right'; + btn.innerHTML = 'Show ' + utils.number_commas(old_messages.length) + ' Old'; + + btn.addEventListener("click", f._show_deleted.bind(f, room)); + + el.classList.add('clearfix'); + el.classList.add('ffz-has-deleted'); + + this.$('.message').append(btn); + } // Badge @@ -1906,12 +1998,6 @@ FFZ.prototype.setup_line = function() { } } }); - - - // Store the capitalization of our own name. - var user = this.get_user(); - if ( user && user.name ) - FFZ.capitalization[user.login] = [user.name, Date.now()]; } @@ -2057,7 +2143,7 @@ FFZ.prototype._remove_banned = function(tokens) { // --------------------- FFZ.prototype._emoticonize = function(component, tokens) { - var room_id = component.get("msgObject.room") || App.__container__.lookup('controller:chat').get('currentRoom.id'), + var room_id = component.get("msgObject.room"), user_id = component.get("msgObject.from"); return this.tokenize_emotes(user_id, room_id, tokens); @@ -2518,11 +2604,17 @@ FFZ.prototype.add_room = function(id, room) { this.log("Adding Room: " + id); // Create a basic data table for this room. - this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; + var data = this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null, needs_history: false}; // Let the server know where we are. this.ws_send("sub", id); + // See if we need history? + if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) { + if ( ! this.ws_send("chat_history", [id,25], this._load_history.bind(this, id)) ) + data.needs_history = true; + } + // For now, we use the legacy function to grab the .css file. this.load_room(id); } @@ -2556,6 +2648,142 @@ FFZ.prototype.remove_room = function(id) { } +// -------------------- +// Chat History +// -------------------- + +FFZ.prototype._load_history = function(room_id, success, data) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return; + + if ( success ) + this.log("Received " + data.length + " old messages for: " + room_id); + else + return this.log("Error retrieving chat history for: " + room_id); + + if ( ! data.length ) + return; + + return this._insert_history(room_id, data); +} + + +FFZ.prototype._show_deleted = function(room_id) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return; + + var old_messages = room.room.get('messages.0.ffz_old_messages'); + if ( ! old_messages || ! old_messages.length ) + return; + + room.room.set('messages.0.ffz_old_messages', undefined); + this._insert_history(room_id, old_messages); +} + +FFZ.prototype._insert_history = function(room_id, data) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return; + + var r = room.room, + messages = r.get('messages'), + tmiSession = r.tmiSession || (TMI._sessions && TMI._sessions[0]), + tmiRoom = r.tmiRoom, + + inserted = 0, + + last_msg = data[data.length - 1], + now = new Date(), + last_date = typeof last_msg.date === "string" ? utils.parse_date(last_msg.date) : last_msg.date, + age = (now - last_date) / 1000, + is_old = age > 300, + + i = data.length, + alternation = r.get('messages.0.ffz_alternate') || false; + + if ( is_old ) + alternation = ! alternation; + + var i = data.length; + while(i--) { + var msg = data[i]; + + if ( typeof msg.date === "string" ) + msg.date = utils.parse_date(msg.date); + + msg.ffz_alternate = alternation = ! alternation; + if ( ! msg.room ) + msg.room = room_id; + + if ( ! msg.color ) + msg.color = msg.tags && msg.tags.color ? msg.tags.color : tmiSession && msg.from ? tmiSession.getColor(msg.from.toLowerCase()) : "#755000"; + + if ( ! msg.labels || ! msg.labels.length ) { + var labels = msg.labels = []; + if ( msg.tags ) { + if ( msg.tags.turbo ) + labels.push("turbo"); + if ( msg.tags.subscriber ) + labels.push("subscriber"); + if ( msg.from === room_id ) + labels.push("owner") + else { + var ut = msg.tags['user-type']; + if ( ut === 'mod' || ut === 'staff' || ut === 'admin' || ut === 'global_mod' ) + labels.push(ut); + } + } + } + + if ( ! msg.style ) { + if ( msg.from === "jtv" ) + msg.style = "admin"; + else if ( msg.from === "twitchnotify" ) + msg.style = "notification"; + } + + if ( ! msg.cachedTokens || ! msg.cachedTokens.length ) + this.tokenize_chat_line(msg, true); + + if ( r.shouldShowMessage(msg) ) { + if ( messages.length < r.get("messageBufferSize") ) { + // One last thing! Make sure we don't have too many messages. + if ( msg.ffz_old_messages ) { + var max_msgs = r.get("messageBufferSize") - (messages.length + 1); + if ( msg.ffz_old_messages.length > max_msgs ) + msg.ffz_old_messages = msg.ffz_old_messages.slice(msg.ffz_old_messages.length - max_msgs); + } + + messages.unshiftObject(msg); + inserted += 1; + } else + break; + } + } + + if ( is_old ) { + var msg = { + ffz_alternate: ! alternation, + color: "#755000", + date: new Date(), + from: "frankerfacez_admin", + style: "admin", + message: "(Last message is " + utils.human_time(age) + " old.)", + room: room_id + }; + + this.tokenize_chat_line(msg); + if ( r.shouldShowMessage(msg) ) { + messages.insertAt(inserted, msg); + while( messages.length > r.get('messageBufferSize') ) + messages.removeAt(0); + } + } +} + + // -------------------- // Receiving Set Info // -------------------- @@ -2594,6 +2822,8 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( this.rooms[room_id] ) data.room = this.rooms[room_id].room; + data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false; + this.rooms[room_id] = data; if ( data.css || data.moderator_badge ) @@ -2636,10 +2866,53 @@ FFZ.prototype._modify_room = function(room) { } }, + clearMessages: function(user) { + var t = this; + if ( user ) { + this.get("messages").forEach(function(s, n) { + if ( s.from === user ) { + t.set("messages." + n + ".ffz_deleted", true); + if ( ! f.settings.prevent_clear ) + t.set("messages." + n + ".deleted", true); + } + }); + } else { + if ( f.settings.prevent_clear ) + this.addTmiMessage("A moderator's attempt to clear chat was ignored."); + else { + var msgs = t.get("messages"); + t.set("messages", []); + t.addMessage({ + style: 'admin', + message: i18n("Chat was cleared by a moderator"), + ffz_old_messages: msgs + }); + } + } + }, + + pushMessage: function(msg) { + if ( this.shouldShowMessage(msg) ) { + var t, s, n, a = this.get("messageBufferSize"); + for (this.get("messages").pushObject(msg), t = this.get("messages.length"), s = t - a, n = 0; s > n; n++) + this.get("messages").removeAt(0); + + "admin" === msg.style || ("whisper" === msg.style && ! this.ffz_whisper_room ) || this.incrementProperty("unreadCount", 1); + } + }, + addMessage: function(msg) { try { if ( msg ) { - msg.room = this.get('id'); + var is_whisper = msg.style === 'whisper'; + if ( f.settings.group_tabs && f.settings.whisper_room ) { + if ( ( is_whisper && ! this.ffz_whisper_room ) || ( ! is_whisper && this.ffz_whisper_room ) ) + return; + } + + if ( ! is_whisper ) + msg.room = this.get('id'); + f.tokenize_chat_line(msg); } } catch(err) { @@ -2658,6 +2931,9 @@ FFZ.prototype._modify_room = function(room) { }, send: function(text) { + if ( f.settings.group_tabs && f.settings.whisper_room && this.ffz_whisper_room ) + return; + try { var cmd = text.split(' ', 1)[0].toLowerCase(); if ( cmd === "/ffz" ) { @@ -3685,13 +3961,13 @@ FFZ.prototype.initialize = function(increment, delay) { // Twitch ember application is ready. // Check for special non-ember pages. - if ( /^\/(?:settings|m\/|messages?\/)/.test(location.pathname) ) { + if ( /^\/(?:$|user\/|p\/|settings|m\/|messages?\/)/.test(location.pathname) ) { this.setup_normal(delay); return; } // Check for the dashboard. - if ( /\/[A-Za-z_-]+\/dashboard/.test(location.pathname) && !/bookmarks$/.test(location.pathname) ) { + if ( /\/[^\/]+\/dashboard/.test(location.pathname) && !/bookmarks$/.test(location.pathname) ) { this.setup_dashboard(delay); return; } @@ -4362,8 +4638,18 @@ FFZ.prototype.ws_create = function() { } // Send the current rooms. - for(var room_id in f.rooms) - f.rooms.hasOwnProperty(room_id) && f.ws_send("sub", room_id); + for(var room_id in f.rooms) { + if ( ! f.rooms.hasOwnProperty(room_id) || ! f.rooms[room_id] ) + continue; + + f.ws_send("sub", room_id); + + if ( f.rooms[room_id].needs_history ) { + f.rooms[room_id].needs_history = false; + if ( ! f.has_bttv && f.settings.chat_history ) + f.ws_send("chat_history", [room_id,25], f._load_history.bind(f, room_id)); + } + } // Send any pending commands. var pending = f._ws_pending; @@ -4390,7 +4676,7 @@ FFZ.prototype.ws_create = function() { // We never ever want to not have a socket. if ( f._ws_delay < 60000 ) - f._ws_delay += 5000; + f._ws_delay += (Math.floor(Math.random()*10) + 5) * 1000; else // Randomize delay. f._ws_delay = (Math.floor(Math.random()*60)+30)*1000; @@ -4423,14 +4709,17 @@ FFZ.prototype.ws_create = function() { } else { var success = cmd === 'True', - callback = f._ws_callbacks[request]; + has_callback = f._ws_callbacks.hasOwnProperty(request); - if ( ! success || ! callback ) + if ( ! has_callback ) f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data, false, true); - if ( callback ) { - delete f._ws_callbacks[request]; - callback(success, data); + else { + try { + f._ws_callbacks[request](success, data); + } catch(err) { + f.error("Callback for " + request + ": " + err); + } } } } @@ -4543,7 +4832,7 @@ try { // Tokenization // --------------------- -FFZ.prototype.tokenize_chat_line = function(msgObject) { +FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { if ( msgObject.cachedTokens ) return msgObject.cachedTokens; @@ -4577,14 +4866,14 @@ FFZ.prototype.tokenize_chat_line = function(msgObject) { for(var i=0; i < tokens.length; i++) { var token = tokens[i]; - if ( _.isString(token) || ! token.mentionedUser || token.own ) + if ( _.isString(token) || ! token.mentionedUser || token.own || msgObject.style === 'whisper' ) continue; // We have a mention! msgObject.ffz_has_mention = true; // If we have chat tabs, update the status. - if ( ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { + if ( room_id && ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'); if ( el && ! el.classList.contains('active') ) el.classList.add('tab-mentioned'); @@ -4593,7 +4882,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject) { // Display notifications if that setting is enabled. Also make sure // that we have a chat view because showing a notification when we // can't actually go to it is a bad thing. - if ( this._chatv && this.settings.highlight_notifications && ! document.hasFocus() ) { + if ( this._chatv && this.settings.highlight_notifications && ! document.hasFocus() && ! prevent_notification ) { var room = this.rooms[room_id] && this.rooms[room_id].room, room_name; @@ -5155,8 +5444,13 @@ FFZ.prototype.build_ui_popup = function(view) { continue; var page = FFZ.menu_pages[key]; - if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)()))) ) + try { + if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)(view)))) ) + continue; + } catch(err) { + this.error("menu_pages " + key + " visible: " + err); continue; + } menu_pages.push([page.sort_order || 0, key, page]); } @@ -5533,28 +5827,7 @@ var FFZ = window.FrankerFaceZ, "\\:-?(o|O)": ":-O", "\\>\\;\\(": ">(", "Gr(a|e)yFace": "GrayFace" - }, - - get_emotes = function(ffz) { - var Chat = App.__container__.lookup('controller:chat'), - room_id = Chat.get('currentRoom.id'), - room = ffz.rooms[room_id], - tmiSession = room ? room.room.tmiSession : null, - - set_ids = tmiSession && tmiSession._emotesParser && tmiSession._emotesParser.emoticonSetIds || "0", - user = ffz.get_user(), - user_sets = user && ffz.users[user.login] && ffz.users[user.login].sets || []; - - // Remove the 'default' set. - set_ids = set_ids.split(",").removeObject("0"); - - if ( ffz.settings.global_emotes_in_menu ) { - set_ids.push("0"); - user_sets = _.union(user_sets, ffz.default_sets); - } - - return [set_ids, user_sets]; - }; + }; // ------------------- @@ -5598,9 +5871,13 @@ FFZ.menu_pages.my_emotes = { name: "My Emoticons", icon: constants.EMOTE, - visible: function() { - var emotes = get_emotes(this); - return emotes[0].length > 0 || emotes[1].length > 0; + visible: function(view) { + var user = this.get_user(), + tmi = view.get('controller.currentRoom.tmiSession'), + ffz_sets = user && this.users[user.login] && this.users[user.login].sets || [], + twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets']; + + return ffz_sets.length || (twitch_sets && Object.keys(twitch_sets).length); }, render: function(view, container) { @@ -5615,7 +5892,40 @@ FFZ.menu_pages.my_emotes = { if ( ! needed_sets.length ) return FFZ.menu_pages.my_emotes.draw_menu.bind(this)(view, container, twitch_sets); - container.innerHTML = JSON.stringify(needed_sets); + var f = this, + fail = function() { + if ( ! needed_sets.length ) + return; + + needed_sets = []; + var ts = {}; + for(var set_id in twitch_sets) + if ( f._twitch_set_to_channel[set_id] ) + ts[set_id] = twitch_sets[set_id]; + + return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, ts); + }; + + this.ws_send("twitch_sets", needed_sets, function(success, data) { + if ( ! needed_sets.length ) + return; + + needed_sets = []; + if ( success ) { + for(var set_id in data) { + if ( ! data.hasOwnProperty(set_id) ) + continue; + + f._twitch_set_to_channel[set_id] = data[set_id]; + } + + localStorage.ffzTwitchSets = JSON.stringify(f._twitch_set_to_channel); + return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, twitch_sets); + } else + fail(); + }); + + setTimeout(fail, 2000); }, draw_twitch_set: function(view, set_id, set) { @@ -5644,6 +5954,7 @@ FFZ.menu_pages.my_emotes = { .done(function(data) { if ( data.subscriber && data.subscriber.image ) { f._twitch_badges[channel_id] = data.subscriber.image; + localStorage.ffzTwitchBadges = JSON.stringify(f._twitch_badges); heading.style.backgroundImage = 'url("' + data.subscriber.image + '")'; } }); @@ -5726,7 +6037,7 @@ FFZ.menu_pages.my_emotes = { if ( emote.width ) em.style.width = emote.width + "px"; - em.title = emote.tooltip || emote.name; + em.title = this._emote_tooltip(emote); em.addEventListener("click", this._add_emote.bind(this, view, emote.name)); menu.appendChild(em); } @@ -6458,6 +6769,12 @@ var FFZ = window.FrankerFaceZ, var sanitize_cache = {}, sanitize_el = document.createElement('span'), + pluralize = function(value, singular, plural) { + plural = plural || 's'; + singular = singular || ''; + return value === 1 ? singular : plural; + }, + place_string = function(num) { if ( num == 1 ) return '1st'; else if ( num == 2 ) return '2nd'; @@ -6505,7 +6822,9 @@ var sanitize_cache = {}, if ( ! parts ) return null; - var unix = Date.UTC(parts[1], parts[2] - 1, parts[3], parts[4], parts[5], parts[6], parts[7] || 0); + parts[7] = (parts[7] && parts[7].length) ? parts[7].substr(0, 3) : 0; + + var unix = Date.UTC(parts[1], parts[2] - 1, parts[3], parts[4], parts[5], parts[6], parts[7]); // Check Offset if ( parts[9] ) { @@ -6574,6 +6893,34 @@ module.exports = { return date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate(); }, + pluralize: pluralize, + + human_time: function(elapsed) { + elapsed = Math.floor(elapsed); + + var years = Math.floor(elapsed / 31536000); + if ( years ) + return years + ' year' + pluralize(years); + + var days = Math.floor((elapsed %= 31536000) / 86400); + if ( days ) + return days + ' day' + pluralize(days); + + var hours = Math.floor((elapsed %= 86400) / 3600); + if ( hours ) + return hours + ' hour' + pluralize(hours); + + var minutes = Math.floor((elapsed %= 3600) / 60); + if ( minutes ) + return minutes + ' minute' + pluralize(minutes); + + var seconds = elapsed % 60; + if ( seconds ) + return seconds + ' second' + pluralize(seconds); + + return 'less than a second'; + }, + time_to_string: function(elapsed, separate_days, days_only) { var seconds = elapsed % 60, minutes = Math.floor(elapsed / 60), diff --git a/script.min.js b/script.min.js index af2cd4ae..0e72751e 100644 --- a/script.min.js +++ b/script.min.js @@ -1,4 +1,4 @@ -!function(e){!function t(e,n,s){function i(a,r){if(!n[a]){if(!e[a]){var c="function"==typeof require&&require;if(!r&&c)return c(a,!0);if(o)return o(a,!0);throw new Error("Cannot find module '"+a+"'")}var l=n[a]={exports:{}};e[a][0].call(l.exports,function(t){var n=e[a][1][t];return i(n?n:t)},l,l.exports,t,e,n,s)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;ae&&this._legacy_load_bots(e))})},n.prototype._legacy_load_donors=function(e){jQuery.ajax(s.SERVER+"script/donors.txt",{cache:!1,context:this}).done(function(e){this._legacy_parse_badges(e,1,1)}).fail(function(t){return 404!=t.status?(e=(e||0)+1,10>e?this._legacy_load_donors(e):void 0):void 0})},n.prototype._legacy_parse_badges=function(e,t,n){var s=this.badges[n].title,o=0;if(ds=null,null!=e)for(var a=e.trim().split(/\W+/),r=0;r50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var s=t.length;t.length;){var i=t.shift();e.room.tmiRoom.sendMessage("/unmod "+i)}return"Sent unmod command for "+s+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var n=this.get_user();if(!n||!n.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var s=t.length;t.length;){var i=t.shift();e.room.tmiRoom.sendMessage("/mod "+i)}return"Sent mod command for "+s+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var n='',s="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:s,SERVER:s?"//localhost:8000/":"//cdn.frankerfacez.com/",API_SERVER:"//api.frankerfacez.com/",SVGPATH:n,ZREKNARF:''+n+"",CHAT_BUTTON:''+n+"",ROOMS:'',CAMERA:'',INVITE:'',EYE:'',CLOCK:'',GEAR:'',HEART:'',EMOTE:'',STAR:''}},{}],4:[function(){var t=e.FrankerFaceZ;t.settings_info.developer_mode={type:"boolean",value:!1,storage_key:"ffzDebugMode",visible:function(){return this.settings.developer_mode||Date.now()-parseInt(localStorage.ffzLastDevMode||"0")<6048e5},category:"Debugging",name:"Developer Mode",help:"Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",on_update:function(){localStorage.ffzLastDevMode=Date.now()}},t.ffz_commands.developer_mode=function(e,t){var n,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?n=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(n=!1),void 0===n?"Developer Mode is currently "+(this.settings.developer_mode?"enabled.":"disabled."):(this.settings.set("developer_mode",n),"Developer Mode is now "+(n?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i=t("../constants");n.prototype.setup_channel=function(){document.body.classList.toggle("ffz-hide-view-count",!this.settings.channel_views),this.log("Creating channel style element.");var e=this._channel_style=document.createElement("style");e.id="ffz-channel-css",document.head.appendChild(e),this.log("Hooking the Ember Channel Index view.");var t=App.__container__.resolve("view:channel/index"),s=this;if(t){this._modify_cindex(t);try{t.create().destroy()}catch(i){}for(var o in Ember.View.views)if(Ember.View.views.hasOwnProperty(o)){var a=Ember.View.views[o];a instanceof t&&(this.log("Manually updating Channel Index view.",a),this._modify_cindex(a),a.ffzInit())}this.log("Hooking the Ember Channel controller."),t=App.__container__.lookup("controller:channel"),t&&t.reopen({ffzUpdateUptime:function(){s._cindex&&s._cindex.ffzUpdateUptime()}.observes("isLive","content.id"),ffzUpdateTitle:function(){var e=this.get("content.name"),t=this.get("content.display_name");t&&(n.capitalization[e]=[t,Date.now()]),s._cindex&&s._cindex.ffzFixTitle()}.observes("content.status","content.id")})}},n.prototype._modify_cindex=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(e){t.error("CIndex didInsertElement: "+e)}},willClearRender:function(){try{this.ffzTeardown()}catch(e){t.error("CIndex willClearRender: "+e)}return this._super()},ffzInit:function(){t._cindex=this,this.get("element").setAttribute("data-channel",this.get("controller.id")),this.ffzFixTitle(),this.ffzUpdateUptime(),this.ffzUpdateChatters();var e=this.get("element").querySelector(".svg-glyph_views:not(.ffz-svg)");e&&e.parentNode.classList.add("twitch-channel-views")},ffzFixTitle:function(){if(!t.has_bttv&&t.settings.stream_title){var e=this.get("controller.status"),n=this.get("controller.id");e=t.render_tokens(t.tokenize_line(n,n,e,!0)),this.$(".title span").each(function(t,n){var s=n.querySelectorAll("script");n.innerHTML=s[0].outerHTML+e+s[1].outerHTML})}},ffzUpdateChatters:function(){var e=this.get("controller.id"),n=t.rooms&&t.rooms[e];if(!n||!t.settings.chatter_count){var o=this.get("element").querySelector("#ffz-chatter-display");return o&&o.parentElement.removeChild(o),o=this.get("element").querySelector("#ffz-ffzchatter-display"),void(o&&o.parentElement.removeChild(o))}var a=Object.keys(n.room.get("ffz_chatters")||{}).length,r=n.ffz_chatters||0,o=this.get("element").querySelector("#ffz-chatter-display span");if(!o){var c=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!c)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-chatter-display",l.title="Current Chatters",l.innerHTML=i.ROOMS+" ",o=document.createElement("span"),l.appendChild(o);var u=c.querySelector("#ffz-ffzchatter-display");u?c.insertBefore(l,u):c.appendChild(l),jQuery(l).tipsy()}if(o.innerHTML=s.number_commas(a),!r)return o=this.get("element").querySelector("#ffz-ffzchatter-display"),void(o&&o.parentNode.removeChild(o));if(o=this.get("element").querySelector("#ffz-ffzchatter-display span"),!o){var c=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!c)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-ffzchatter-display",l.title="Chatters with FrankerFaceZ",l.innerHTML=i.ZREKNARF+" ",o=document.createElement("span"),l.appendChild(o);var u=c.querySelector("#ffz-chatter-display");u?c.insertBefore(l,u.nextSibling):c.appendChild(l),jQuery(l).tipsy()}o.innerHTML=s.number_commas(r)},ffzUpdateUptime:function(){if(this._ffz_update_uptime&&(clearTimeout(this._ffz_update_uptime),delete this._ffz_update_uptime),!t.settings.stream_uptime||!this.get("controller.isLiveAccordingToKraken")){var e=this.get("element").querySelector("#ffz-uptime-display");return void(e&&e.parentElement.removeChild(e))}this._ffz_update_uptime=setTimeout(this.ffzUpdateUptime.bind(this),1e3);var n=this.get("controller.content.stream.created_at");if(n&&(n=s.parse_date(n))){var o=Math.floor((Date.now()-n.getTime())/1e3);if(!(0>o)){var e=this.get("element").querySelector("#ffz-uptime-display span");if(!e){var a=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!a)return;var r=document.createElement("span");r.className="ffz stat",r.id="ffz-uptime-display",r.title="Stream Uptime (since "+n.toLocaleString()+")",r.innerHTML=i.CLOCK+" ",e=document.createElement("span"),r.appendChild(e);var c=a.querySelector(".live-count");if(c)a.insertBefore(r,c.nextSibling);else try{c=a.querySelector("script:nth-child(0n+2)"),a.insertBefore(r,c.nextSibling)}catch(l){a.insertBefore(r,a.childNodes[0])}jQuery(r).tipsy({html:!0})}e.innerHTML=s.time_to_string(o)}}},ffzTeardown:function(){this.get("element").setAttribute("data-channel",""),t._cindex=void 0,this._ffz_update_uptime&&clearTimeout(this._ffz_update_uptime)}})},n.settings_info.chatter_count={type:"boolean",value:!1,category:"Channel Metadata",name:"Chatter Count",help:"Display the current number of users connected to chat beneath the channel.",on_update:function(e){if(this._cindex&&this._cindex.ffzUpdateChatters(),e&&this.rooms)for(var t in this.rooms)this.rooms.hasOwnProperty(t)&&this.rooms[t].room&&this.rooms[t].room.ffzInitChatterCount()}},n.settings_info.channel_views={type:"boolean",value:!0,category:"Channel Metadata",name:"Channel Views",help:"Display the number of times the channel has been viewed beneath the stream.",on_update:function(e){document.body.classList.toggle("ffz-hide-view-count",!e)}},n.settings_info.stream_uptime={type:"boolean",value:!1,category:"Channel Metadata",name:"Stream Uptime",help:"Display the stream uptime under a channel by the viewer count.",on_update:function(){this._cindex&&this._cindex.ffzUpdateUptime()}},n.settings_info.stream_title={type:"boolean",value:!0,no_bttv:!0,category:"Channel Metadata",name:"Title Links",help:"Make links in stream titles clickable.",on_update:function(){this._cindex&&this._cindex.ffzFixTitle()}}},{"../constants":3,"../utils":29}],6:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i=t("../constants"),o=function(e){return 1>e?"":e>=99?"99+":""+e};n.settings_info.group_tabs={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Chat Room Tabs Beta",help:"Enhanced UI for switching the current chat room and noticing new messages.",on_update:function(e){var t=!this.has_bttv&&e;this._chatv&&t!==this._group_tabs_state&&(t?this._chatv.ffzEnableTabs():this._chatv.ffzDisableTabs())}},n.settings_info.pinned_rooms={type:"button",value:[],category:"Chat",visible:!1,name:"Pinned Chat Rooms",help:"Set a list of channels that should always be available in chat."},n.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat controller.");var e=App.__container__.lookup("controller:chat"),t=this;e&&e.reopen({ffzUpdateChannels:function(){t.settings.group_tabs&&t._chatv&&t._chatv.ffzRebuildTabs()}.observes("currentChannelRoom","connectedPrivateGroupRooms")}),this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e);try{e.create().destroy()}catch(n){}for(var s in Ember.View.views)if(Ember.View.views.hasOwnProperty(s)){var i=Ember.View.views[s];if(i instanceof e){this.log("Manually updating existing Chat view.",i);try{i.ffzInit()}catch(n){this.error("setup: build_ui_link: "+n)}}}this.log("Hooking the Ember Layout controller.");var o=App.__container__.lookup("controller:layout");if(o){o.reopen({ffzFixTabs:function(){t.settings.group_tabs&&t._chatv&&t._chatv._ffz_tabs&&setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}.observes("isRightColumnClosed")}),this.log("Hooking the Ember 'Right Column' controller. Seriously...");var a=App.__container__.lookup("controller:right-column");a&&a.reopen({ffzFixTabs:function(){t.settings.group_tabs&&t._chatv&&t._chatv._ffz_tabs&&setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}.observes("firstTabSelected")})}},n.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(e){t.error("ChatView didInsertElement: "+e)}},willClearRender:function(){try{this.ffzTeardown()}catch(e){t.error("ChatView willClearRender: "+e)}this._super()},ffzInit:function(){t._chatv=this,this.$(".textarea-contain").append(t.build_ui_link(this)),!t.has_bttv&&t.settings.group_tabs&&this.ffzEnableTabs(),setTimeout(function(){t.settings.group_tabs&&t._chatv._ffz_tabs&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px");var e=t._chatv.get("controller");e&&e.set("showList",!1)},1e3)},ffzTeardown:function(){t._chatv===this&&(t._chatv=null),this.$(".textarea-contain .ffz-ui-toggle").remove(),t.settings.group_tabs&&this.ffzDisableTabs()},ffzChangeRoom:Ember.observer("controller.currentRoom",function(){try{if(t.update_ui_link(),!t.has_bttv&&t.settings.group_tabs&&this._ffz_tabs){var e=this.get("controller.currentRoom");e&&e.resetUnreadCount();var n=jQuery(this._ffz_tabs);n.children(".ffz-chat-tab").removeClass("active"),e&&n.children('.ffz-chat-tab[data-room="'+e.get("id")+'"]').removeClass("tab-mentioned").addClass("active").children("span").text("");var s=e&&e.get("canInvite");this._ffz_invite&&this._ffz_invite.classList.toggle("hidden",!s),this.set("controller.showInviteUser",s&&this.get("controller.showInviteUser")),this.$(".chat-room").css("top",this._ffz_tabs.offsetHeight+"px")}}catch(i){t.error("ChatView ffzUpdateLink: "+i)}}),ffzEnableTabs:function(){if(!t.has_bttv&&t.settings.group_tabs){this.$(".chat-header").addClass("hidden");var e=this._ffz_tabs=document.createElement("div");e.id="ffz-group-tabs",this.$(".chat-header").after(e),this.ffzRebuildTabs()}},ffzRebuildTabs:function(){if(!t.has_bttv&&t.settings.group_tabs){var e=this._ffz_tabs||this.get("element").querySelector("#ffz-group-tabs");if(e){e.innerHTML="";var n=document.createElement("a"),s=this;n.className="button glyph-only tooltip",n.title="Chat Room Management",n.innerHTML=i.ROOMS,n.addEventListener("click",function(){var e=s.get("controller");e&&e.set("showList",!e.get("showList"))}),e.appendChild(n),n=document.createElement("a"),n.className="button glyph-only tooltip invite",n.title="Invite a User",n.innerHTML=i.INVITE,n.addEventListener("click",function(){var e=s.get("controller");e&&e.set("showInviteUser",e.get("currentRoom.canInvite")&&!e.get("showInviteUser"))}),n.classList.toggle("hidden",!this.get("controller.currentRoom.canInvite")),s._ffz_invite=n,e.appendChild(n);var o,a=this.get("controller.currentChannelRoom");a&&(o=this.ffzBuildTab(s,a,!0),o&&e.appendChild(o));var r=App.__container__.lookup("controller:channel"),c=App.__container__.resolve("model:room");if(target=r&&r.get("hostModeTarget"),target&&c){var l=target.get("id");this._ffz_host!==l&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),this._ffz_host=l,this._ffz_host_room=c.findOne(l))}else this._ffz_host&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),delete this._ffz_host,delete this._ffz_host_room);this._ffz_host_room&&(o=s.ffzBuildTab(s,this._ffz_host_room,!1,!0),o&&e.appendChild(o));for(var u=0;u"+l+"",u.addEventListener("click",function(){e.get("controller").focusRoom(t)}),u},ffzDisableTabs:function(){this._ffz_tabs&&(this._ffz_tabs.parentElement.removeChild(this._ffz_tabs),delete this._ffz_tabs,delete this._ffz_invite),this._ffz_host&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),delete this._ffz_host,delete this._ffz_host_room),this.$(".chat-room").css("top",""),this.$(".chat-header").removeClass("hidden")}})},n.prototype.connect_extra_chat=function(){if(!this.has_bttv){for(var e=0;e1)return"Join Usage: /join ";var n=t[0].toLowerCase();return"#"===n.charAt(0)&&(n=n.substr(1)),this._join_room(n)?"Joining "+n+". You will always connect to this channel's chat unless you later /part from it.":"You have already joined "+n+'. Please use "/part '+n+'" to leave it.'},n.chat_commands.part=function(e,t){if(!t||!t.length||t.length>1)return"Part Usage: /part ";var n=t[0].toLowerCase();return"#"===n.charAt(0)&&(n=n.substr(1)),this._leave_room(n)?"Leaving "+n+".":this.rooms[n]?"You do not have "+n+" pinned and you cannot leave the current channel or hosted channels via /part.":"You are not in "+n+"."}},{"../constants":3,"../utils":29}],7:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i="[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",o=new RegExp(i+"*,"+i+"*"),a=function(e){return(e+"").replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(//g,">")},r="http://static-cdn.jtvnw.net/emoticons/v1/",c=function(e){return r+e+"/1.0 1x, "+r+e+"/2.0 2x, "+r+e+"/3.0 4x"},l=function(e){var t=e.set,n=e.set_type,s=e.owner;return void 0===n&&(n="Channel"),t?(("00000turbo"==t||"turbo"==t)&&(t="Twitch Turbo",n=null),"Emoticon: "+e.code+"\n"+(n?n+": ":"")+t+(s?"\nBy: "+s.display_name:"")):e.code},u=function(e){{var t=this._twitch_emotes[e];t?t.set:null}return t?"string"==typeof t?t:t.tooltip?t.tooltip:t.tooltip=l(t):"???"},h=function(e,t,n,s){if(n){t&&(s.code=t),this._twitch_emotes[e]=s;for(var i=u.bind(this)(e),o=document.querySelectorAll('img[emote-id="'+e+'"]'),a=0;aYouTube: "+s.sanitize(n.title)+"
",t+="Channel: "+s.sanitize(n.channel)+" | "+s.time_to_string(n.duration)+"
",t+=s.number_commas(n.views||0)+" Views | 👍 "+s.number_commas(n.likes||0)+" 👎 "+s.number_commas(n.dislikes||0);else if("strawpoll"==n.type){t="Strawpoll: "+s.sanitize(n.title)+"
";for(var i in n.items){{var o=n.items[i];Math.floor(o/n.total*100)}t+='"}t+="
'+s.sanitize(i)+''+s.number_commas(o)+"

Total: "+s.number_commas(n.total);var a=s.parse_date(n.fetched);if(a){var r=Math.floor((a.getTime()-Date.now())/1e3);r>60&&(t+="
Data was cached "+s.time_to_string(r)+" ago.")}}else if("twitch"==n.type){t="Twitch: "+s.sanitize(n.display_name)+"
";var c=s.parse_date(n.since);c&&(t+="Member Since: "+s.date_string(c)+"
"),t+="Views: "+s.number_commas(n.views)+" | Followers: "+s.number_commas(n.followers)+""}else if("twitch_vod"==n.type)t="Twitch "+("highlight"==n.broadcast_type?"Highlight":"Broadcast")+": "+s.sanitize(n.title)+"
",t+="By: "+s.sanitize(n.display_name)+(n.game?" | Playing: "+s.sanitize(n.game):" | Not Playing")+"
",t+="Views: "+s.number_commas(n.views)+" | "+s.time_to_string(n.length);else if("twitter"==n.type)t="Tweet By: "+s.sanitize(n.user)+"
",t+=s.sanitize(n.tweet);else if("reputation"==n.type){if(t=''+s.sanitize(n.full.toLowerCase())+"",n.trust<50||n.safety<50||n.tags&&n.tags.length>0){t+="
";var l=!1;(n.trust<50||n.safety<50)&&(n.unsafe=!0,t+="Potentially Unsafe Link
",t+="Trust: "+n.trust+"% | Child Safety: "+n.safety+"%",l=!0),n.tags&&n.tags.length>0&&(t+=(l?"
":"")+"Tags: "+n.tags.join(", ")),t+="
Data Source: WOT"}}else n.full&&(t=''+s.sanitize(n.full.toLowerCase())+"");return t||(t=''+s.sanitize(e.toLowerCase())+""),n.tooltip=t,t},f=function(e,t,n){if(t){this._link_data[e]=n,n.unsafe=!1;var s,i=d.bind(this)(e),o="/"==e.charAt(e.length-1)?e.substr(0,e.length-1):null;if(s=document.querySelectorAll(o?'span.message a[href="'+e+'"], span.message a[href="'+o+'"], span.message a[data-url="'+e+'"], span.message a[data-url="'+o+'"]':'span.message a[href="'+e+'"], span.message a[data-url="'+e+'"]'),this.settings.link_info)for(var a=0;aBeta",help:"Check links against known bad websites, unshorten URLs, and show YouTube info."},n.settings_info.chat_rows={type:"boolean",value:!1,category:"Chat",no_bttv:!0,name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-background",e)}},n.prototype.setup_line=function(){document.body.classList.toggle("ffz-chat-colors",!this.has_bttv&&this.settings.fix_color),document.body.classList.toggle("ffz-chat-background",!this.has_bttv&&this.settings.chat_rows),this._colors={},this._last_row={};var e=this._fix_color_style=document.createElement("style");e.id="ffz-style-username-colors",e.type="text/css",document.head.appendChild(e),this._twitch_emotes={},this._link_data={},this.log("Hooking the Ember Line controller.");var t=App.__container__.resolve("component:message-line"),s=this;t.reopen({tokenizedMessage:function(){var e=this.get("msgObject.cachedTokens");if(e)return e;e=this._super();try{var t=performance.now(),i=s.get_user(),o=i&&this.get("msgObject.from")===i.login;e=s._remove_banned(e),e=s._emoticonize(this,e);var a=this.get("msgObject.tags.display-name");a&&a.length&&(n.capitalization[this.get("msgObject.from")]=[a.trim(),Date.now()]),o||(e=s.tokenize_mentions(e));for(var r=0;r5&&s.log("Tokenizing Message Took Too Long - "+(l-t)+"ms",e,!1,!0)}catch(u){try{s.error("LineController tokenizedMessage: "+u)}catch(u){}}return this.set("msgObject.cachedTokens",e),e}.property("msgObject.message","isChannelLinksDisabled","currentUserNick","msgObject.from","msgObject.tags.emotes"),didInsertElement:function(){this._super();try{var e=performance.now(),t=this.get("element"),n=this.get("msgObject.from"),i=this.get("msgObject.room")||App.__container__.lookup("controller:chat").get("currentRoom.id"),o=this.get("msgObject.color"),a=this.get("msgObject.ffz_alternate");o&&s._handle_color(o),void 0===a&&(a=s._last_row[i]=s._last_row.hasOwnProperty(i)?!s._last_row[i]:!1,this.set("msgObject.ffz_alternate",a)),t.classList.toggle("ffz-alternate",a),t.setAttribute("data-room",i),t.setAttribute("data-sender",n),t.setAttribute("data-deleted",this.get("deleted")||!1),s.render_badge(this),this.get("msgObject.ffz_has_mention")&&t.classList.add("ffz-mentioned");for(var r=t.querySelectorAll("a.deleted-link"),l=0;l-1&&(-1===t.indexOf("/")||t.indexOf("@")5&&s.log("Line Took Too Long - "+E+"ms",t.innerHTML,!1,!0)}catch(T){try{s.error("LineView didInsertElement: "+T)}catch(T){}}}});var i=this.get_user();i&&i.name&&(n.capitalization[i.login]=[i.name,Date.now()])},n.prototype._handle_color=function(e){if(e&&!this._colors[e]){this._colors[e]=!0;var t=parseInt(e.substr(1),16),n=[t>>16,t>>8&255,255&t],i=s.get_luminance(n),o="",a='span[style="color:'+e+'"]',r=!1;if(i>.3){r=!0;for(var c=127,l=n;c--&&(l=s.darken(l),!(s.get_luminance(l)<=.3)););o+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+s.rgb_to_css(l)+" !important; }\n"}else o+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+e+" !important; }\n";if(.15>i){r=!0;for(var c=127,l=n;c--&&(l=s.brighten(l),!(s.get_luminance(l)>=.15)););o+=".ffz-chat-colors .theatre .chat-container .chat-line "+a+", .ffz-chat-colors .chat-container.dark .chat-line "+a+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+a+" { color: "+s.rgb_to_css(l)+" !important; }\n"}else o+=".ffz-chat-colors .theatre .chat-container .chat-line "+a+", .ffz-chat-colors .chat-container.dark .chat-line "+a+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+a+" { color: "+e+" !important; }\n";r&&(this._fix_color_style.innerHTML+=o)}},n.capitalization={},n._cap_fetching=0,n.get_capitalization=function(t,s){if(e.BetterTTV&&BetterTTV.chat&&BetterTTV.chat.helpers.lookupDisplayName)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var i=n.capitalization[t];return i&&Date.now()-i[1]<36e5?i[0]:(n._cap_fetching<25&&(n._cap_fetching++,n.get().ws_send("get_display_name",t,function(e,i){var o=e?i:t;n.capitalization[t]=[o,Date.now()],n._cap_fetching--,"function"==typeof s&&s(o)})),i?i[0]:t)},n.prototype._remove_banned=function(e){var t=this.settings.banned_words;if(!t||!t.length)return e;"string"==typeof e&&(e=[e]);for(var s=n._words_to_regex(t),i=[],o=0;o<banned link>',own:!0}:r)}return i},n.prototype._emoticonize=function(e,t){var n=e.get("msgObject.room")||App.__container__.lookup("controller:chat").get("currentRoom.id"),s=e.get("msgObject.from");return this.tokenize_emotes(s,n,t)}},{"../utils":29}],8:[function(t){var n=e.FrankerFaceZ,s=t("../utils"),i={ESC:27,P:80,B:66,T:84,U:85},o=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],a='',r='';n.settings_info.enhanced_moderation={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."},n.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var t=App.__container__.resolve("component:moderation-card"),n=this;t.reopen({didInsertElement:function(){this._super(),e._card=this;try{if(n.has_bttv||!n.settings.enhanced_moderation)return;var t=this.get("element"),c=this.get("controller");if(t.classList.add("ffz-moderation-card"),c.get("cardInfo.isModeratorOrHigher")){t.classList.add("ffz-is-mod"),t.setAttribute("tabindex",1),t.addEventListener("keyup",function(e){var t=e.keyCode||e.which,n=c.get("cardInfo.user.id"),s=App.__container__.lookup("controller:chat").get("currentRoom");if(t==i.P)s.send("/timeout "+n+" 1");else if(t==i.B)s.send("/ban "+n);else if(t==i.T)s.send("/timeout "+n+" 600");else if(t==i.U)s.send("/unban "+n);else if(t!=i.ESC)return;c.send("hideModOverlay")});var l=document.createElement("div");l.className="interface clearfix";var u=function(e){var t=c.get("cardInfo.user.id"),n=App.__container__.lookup("controller:chat").get("currentRoom");n.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},h=function(e,t){var n=document.createElement("button");return n.className="button",n.innerHTML=e,n.title="Timeout User for "+s.number_commas(t)+" Second"+(1!=t?"s":""),600===t?n.title="(T)"+n.title.substr(1):1===t&&(n.title="(P)urge - "+n.title),jQuery(n).tipsy(),n.addEventListener("click",u.bind(this,t)),n};l.appendChild(h("Purge",1));var d=document.createElement("span");d.className="right",l.appendChild(d);for(var _=0;_ button");b&&b.classList.contains("message-button")&&(b.innerHTML=a,b.classList.add("glyph-only"),b.classList.add("message"),b.title="Message User",jQuery(b).tipsy()),this.$().draggable({start:function(){t.focus()}}),t.focus()}catch(y){try{n.error("ModerationCardView didInsertElement: "+y)}catch(y){}}}})},n.chat_commands.purge=n.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var n=0;n10)return"Please only ban up to 10 users at once.";for(var n=0;n10)return"Please only unban up to 10 users at once.";for(var n=0;nn?i.load_room(e,t,n):"function"==typeof t&&t(!1))})},n.prototype._load_room_json=function(e,t,n){return n&&n.room?(n=n.room,this.rooms[e]&&(n.room=this.rooms[e].room),this.rooms[e]=n,(n.css||n.moderator_badge)&&i.update_css(this._room_style,e,o(n)+(n.css||"")),this.emote_sets.hasOwnProperty(n.set)||this.load_set(n.set),this.update_ui_link(),void(t&&t(!0,n))):"function"==typeof t&&t(!1)},n.prototype._modify_room=function(t){var n=this;t.reopen({init:function(){this._super();try{n.add_room(this.id,this),this.set("ffz_chatters",{})}catch(e){n.error("add_room: "+e)}},willDestroy:function(){this._super();try{n.remove_room(this.id)}catch(e){n.error("remove_room: "+e)}},addMessage:function(e){try{e&&(e.room=this.get("id"),n.tokenize_chat_line(e))}catch(t){n.error("Room addMessage: "+t)}return this._super(e)},setHostMode:function(e){var t=App.__container__.lookup("controller:chat");if(t&&t.get("currentChannelRoom")===this)return this._super(e)},send:function(e){try{var t=e.split(" ",1)[0].toLowerCase();if("/ffz"===t)return this.set("messageToSend",""),void n.run_ffz_command(e.substr(5),this.get("id"));if("/"===t.charAt(0)&&n.run_command(e,this.get("id")))return void this.set("messageToSend","")}catch(s){n.error("send: "+s)}return this._super(e)},ffzUpdateUnread:function(){if(n.settings.group_tabs){var e=App.__container__.lookup("controller:chat");e&&e.get("currentRoom")===this?this.resetUnreadCount():n._chatv&&n._chatv.ffzTabUnread(this.get("id"))}}.observes("unreadCount"),ffzInitChatterCount:function(){if(this.tmiRoom){var e=this;this.tmiRoom.list().done(function(t){var n={};t=t.data.chatters;for(var s=0;s0)){var i=n.emoticonSetIds;n.emoticonSetIds="",n.updateEmoticons(i),this._twitch_emote_check=setTimeout(this.check_twitch_emotes.bind(this),1e4)}},n.prototype.getEmotes=function(e,t){var n=this.users&&this.users[e],s=this.rooms&&this.rooms[t];return _.union(n&&n.sets||[],s&&s.set&&[s.set]||[],this.default_sets)},n.ws_commands.reload_set=function(e){this.emote_sets.hasOwnProperty(e)&&this.load_set(e)},n.ws_commands.load_set=function(e){this.load_set(e)},n.prototype._emote_tooltip=function(e){if(!e)return null;if(e._tooltip)return e._tooltip;var t=this.emote_sets[e.set_id],n=e.owner,s=t&&t.title||"Global";return e._tooltip="Emoticon: "+(e.hidden?"???":e.name)+"\nFFZ "+s+(n?"\nBy: "+n.display_name:""),e._tooltip},n.prototype.load_global_sets=function(e,t){var n=this;jQuery.getJSON(s.API_SERVER+"v1/set/global").done(function(e){n.default_sets=e.default_sets;var t=n.global_sets=[],s=e.sets||{};for(var i in s)if(s.hasOwnProperty(i)){var o=s[i];t.push(i),n._load_set_json(i,void 0,o)}}).fail(function(s){return 404==s.status?"function"==typeof e&&e(!1):(t=t||0,t++,50>t?n.load_global_sets(e,t):"function"==typeof e&&e(!1))})},n.prototype.load_set=function(e,t,n){var i=this;jQuery.getJSON(s.API_SERVER+"v1/set/"+e).done(function(n){i._load_set_json(e,t,n&&n.set)}).fail(function(s){return 404==s.status?"function"==typeof t&&t(!1):(n=n||0,n++,10>n?i.load_set(e,t,n):"function"==typeof t&&t(!1))})},n.prototype.unload_set=function(e){var t=this.emote_sets[e];t&&(this.log("Unloading emoticons for set: "+e),i.update_css(this._emote_style,e,null),delete this.emote_sets[e])},n.prototype._load_set_json=function(e,t,n){if(!n)return"function"==typeof t&&t(!1);this.emote_sets[e]=n,n.users=[],n.count=0;var s="",o=n.emoticons;n.emoticons={};for(var a=0;a=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(n||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),this.settings.group_tabs&&this._chatv&&this._chatv.ffzDisableTabs(),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background"),this.is_dashboard&&this._update_subscribers();var t=BetterTTV.chat.helpers.sendMessage,n=this;BetterTTV.chat.helpers.sendMessage=function(e){var s=e.split(" ",1)[0].toLowerCase();return"/ffz"!==s?t(e):void n.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var s,i=BetterTTV.chat.handlers.onPrivmsg;BetterTTV.chat.handlers.onPrivmsg=function(e,t){s=e;var n=i(e,t);return s=null,n};var o=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,i,a,r){try{return n.bttv_badges(r),'
'+BetterTTV.chat.templates.timestamp(r.time)+" "+(a?BetterTTV.chat.templates.modicons():"")+" "+BetterTTV.chat.templates.badges(r.badges)+BetterTTV.chat.templates.from(r.nickname,r.color)+BetterTTV.chat.templates.message(r.sender,r.message,r.emotes,t?r.color:!1)+"
"}catch(c){return n.log("Error: ",c),o(e,t,i,a,r)}};var a,r=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,s,i){try{i=i||!1;var o=encodeURIComponent(t);if("jtv"!==e){a=e;var c=BetterTTV.chat.templates.emoticonize(t,s);a=null;for(var l=0;l'+t+"
"}catch(u){return n.log("Error: ",u),r(e,t,s,i)}};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var i=c(e,t),o=s||BetterTTV.getChannel(),r=o&&o.toLowerCase(),l=a&&a.toLowerCase(),u=n.getEmotes(l,r),t=[],h=n.get_user(),d=h&&h.login===l;return _.each(u,function(e){var s=n.emote_sets[e];s&&_.each(s.emoticons,function(e){_.any(i,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=n._emote_tooltip(e),s=[''+t+''],o=i;if(i=[],!o||!o.length)return i;for(var a=0;a=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(n||0)+t),t)) -},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),n=e?e.login:null,s=App.__container__.lookup("controller:chat"),i=s?s.get("currentRoom.id"):null,o=this.getEmotes(n,i),a=[],r=0;r=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(n||0)+t),t)))},n.prototype.setup_normal=function(t){var s=e.performance&&performance.now?performance.now():Date.now();this.log("Found non-Ember Twitch after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.find_bttv(10);var i=e.performance&&performance.now?performance.now():Date.now(),o=i-s;this.log("Initialization complete in "+o+"ms")},n.prototype.is_dashboard=!1,n.prototype.setup_dashboard=function(t){var s=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch Dashboard after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.is_dashboard=!0,this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this._update_subscribers(),this.setup_message_event(),this.find_bttv(10);var i=e.performance&&performance.now?performance.now():Date.now(),o=i-s;this.log("Initialization complete in "+o+"ms")},n.prototype.setup_ember=function(t){var s=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+n.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_channel(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_my_emotes(),this.setup_races(),this.connect_extra_chat(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var i=e.performance&&performance.now?performance.now():Date.now(),o=i-s;this.log("Initialization complete in "+o+"ms")},n.prototype.setup_message_event=function(){this.log("Listening for Window Messages."),e.addEventListener("message",this._on_window_message.bind(this),!1)},n.prototype._on_window_message=function(e){if(e.data&&e.data.from_ffz){var t=e.data;this.log("Window Message",t)}}},{"./badges":1,"./commands":2,"./debug":4,"./ember/channel":5,"./ember/chatview":6,"./ember/line":7,"./ember/moderation-card":8,"./ember/room":9,"./ember/viewers":10,"./emoticons":11,"./ext/betterttv":12,"./ext/emote_menu":13,"./featurefriday":15,"./settings":16,"./socket":17,"./tokenize":18,"./ui/about_page":19,"./ui/dark":20,"./ui/menu":21,"./ui/menu_button":22,"./ui/my_emotes":23,"./ui/notifications":24,"./ui/races":25,"./ui/styles":26,"./ui/sub_count":27,"./ui/viewer_count":28}],15:[function(t){var n=e.FrankerFaceZ,s=t("./constants");n.prototype.feature_friday=null,n.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(s.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},n.ws_commands.reload_ff=function(){this.check_ff()},n.prototype._feature_friday_ui=function(e,t,n){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,n,[this.feature_friday.set],this.feature_friday.title,this.feature_friday.icon,"FrankerFaceZ");var s=App.__container__.lookup("controller:channel");if(!s||s.get("id")!=this.feature_friday.channel){var i=this.feature_friday,o=document.createElement("div"),a=document.createElement("a");o.className="chat-menu-content",o.style.textAlign="center";var r=i.display_name+(i.live?" is live now!":"");a.className="button primary",a.classList.toggle("live",i.live),a.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),a.href="http://www.twitch.tv/"+i.channel,a.title=r,a.target="_new",a.innerHTML=""+r+"",o.appendChild(a),t.appendChild(o)}}},n.prototype._load_ff=function(e){this.feature_friday&&(this.global_sets.removeObject(this.feature_friday.set),this.default_sets.removeObject(this.feature_friday.set),this.feature_friday=null,this.update_ui_link()),e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,title:e.title||"Feature Friday",display_name:n.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.default_sets.push(e.set),this.load_set(e.set),this._update_ff_live())},n.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},n.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],16:[function(t){var n=e.FrankerFaceZ,s=t("./constants");make_ls=function(e){return"ffz_setting_"+e},toggle_setting=function(e,t){var n=!this.settings.get(t);this.settings.set(t,n),e.classList.toggle("active",n)},n.settings_info={},n.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var t in n.settings_info)if(n.settings_info.hasOwnProperty(t)){var s=n.settings_info[t],i=s.storage_key||make_ls(t),o=s.hasOwnProperty("value")?s.value:void 0;if(localStorage.hasOwnProperty(i))try{o=JSON.parse(localStorage.getItem(i))}catch(a){this.log('Error loading value for "'+t+'": '+a)}this.settings[t]=o}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this),!1)},n.settings_info.replace_twitch_menu={type:"boolean",value:!1,name:"Replace Twitch Emoticon Menu Beta",help:"Completely replace the default Twitch emoticon menu.",on_update:function(e){document.body.classList.toggle("ffz-menu-replace",e)}},n.menu_pages.settings={render:function(e,t){var s={},i=[];for(var o in n.settings_info)if(n.settings_info.hasOwnProperty(o)){var a=n.settings_info[o],r=a.category||"Miscellaneous",c=s[r];if(void 0!==a.visible&&null!==a.visible){var l=a.visible;if("function"==typeof a.visible&&(l=a.visible.bind(this)()),!l)continue}c||(i.push(r),c=s[r]=[]),c.push([o,a])}i.sort(function(e,t){var e=e.toLowerCase(),t=t.toLowerCase();return"debugging"===e&&(e="zzz"+e),"debugging"===t&&(t="zzz"+t),t>e?-1:e>t?1:0});for(var u=0;un?-1:n>s?1:o>i?-1:i>o?1:0});for(var m=0;m",v.className="switch-label",v.innerHTML=a.name,p.appendChild(y),p.appendChild(v),y.addEventListener("click",toggle_setting.bind(this,y,o))}else{p.classList.add("option");var w=document.createElement("a");w.innerHTML=a.name,w.href="#",p.appendChild(w),w.addEventListener("click",a.method.bind(this))}if(a.help){var b=document.createElement("span");b.className="help",b.innerHTML=a.help,p.appendChild(b)}}_.appendChild(p)}t.appendChild(_)}},name:"Settings",icon:s.GEAR,sort_order:99999,wide:!0},n.prototype._setting_update=function(t){if(t||(t=e.event),t.key&&"ffz_setting_"===t.key.substr(0,12)){var s=t.key,i=s.substr(12),o=void 0,a=n.settings_info[i];if(!a){for(i in n.settings_info)if(n.settings_info.hasOwnProperty(i)&&(a=n.settings_info[i],a.storage_key==s))break;if(a.storage_key!=s)return}this.log("Updated Setting: "+i);try{o=JSON.parse(t.newValue)}catch(r){this.log('Error loading new value for "'+i+'": '+r),o=a.value||void 0}if(this.settings[i]=o,a.on_update)try{a.on_update.bind(this)(o,!1)}catch(r){this.log('Error running updater for setting "'+i+'": '+r)}}},n.prototype._setting_get=function(e){return this.settings[e]},n.prototype._setting_set=function(e,t){var s=n.settings_info[e],i=s.storage_key||make_ls(e),o=JSON.stringify(t);if(this.settings[e]=t,localStorage.setItem(i,o),this.log('Changed Setting "'+e+'" to: '+o),s.on_update)try{s.on_update.bind(this)(t,!0)}catch(a){this.log('Error running updater for setting "'+e+'": '+a)}},n.prototype._setting_del=function(e){var t=n.settings_info[e],s=t.storage_key||make_ls(e),i=void 0;if(localStorage.hasOwnProperty(s)&&localStorage.removeItem(s),delete this.settings[e],t&&(i=this.settings[e]=t.hasOwnProperty("value")?t.value:void 0),t.on_update)try{t.on_update.bind(this)(i,!0)}catch(o){this.log('Error running updater for setting "'+e+'": '+o)}}},{"./constants":3}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var n,s=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{n=this._ws_sock=new WebSocket("ws://catbag.frankerfacez.com/")}catch(i){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+i)}this._ws_exists=!0,n.onopen=function(){s._ws_open=!0,s._ws_delay=0,s.log("Socket connected.");var n=e.RequestFileSystem||e.webkitRequestFileSystem;n?n(e.TEMPORARY,100,s.ws_send.bind(s,"hello",["ffz_"+t.version_info,localStorage.ffzClientId],s._ws_on_hello.bind(s)),s.log.bind(s,"Operating in Incognito Mode.")):s.ws_send("hello",["ffz_"+t.version_info,localStorage.ffzClientId],s._ws_on_hello.bind(s));var i=s.get_user();if(i&&s.ws_send("setuser",i.login),s.is_dashboard){var o=location.pathname.match(/\/([^\/]+)/);o&&s.ws_send("sub",o[1])}for(var a in s.rooms)s.rooms.hasOwnProperty(a)&&s.ws_send("sub",a);var r=s._ws_pending;s._ws_pending=[];for(var c=0;c';if(e.isLink){if(!t&&void 0!==t)return e.href;var n=e.href;if(n.indexOf("@")>-1&&(-1===n.indexOf("/")||n.indexOf("@")'+n+"";var s=(n.match(/^https?:\/\//)?"":"http://")+n;return''+n+""}return e.mentionedUser?''+e.mentionedUser+"":i.sanitize(e.deletedLink?e.text:e)}).join("")},s.prototype.tokenize_title_emotes=function(e){var t=this,n=App.__container__.lookup("controller:channel"),s=n&&n.get("product.emoticons"),i=[];return _.isString(e)&&(e=[e]),_.each(_.union(t.__twitch_global_emotes||[],s),function(t){if(t&&"inactive"!==t.state){var n=new RegExp("\\b"+t.regex+"\\b");_.any(e,function(e){return _.isString(e)&&e.match(n)})&&i.push(t)}}),(void 0===t.__twitch_global_emotes||null===t.__twitch_global_emotes)&&(t.__twitch_global_emotes=!1,Twitch.api.get("chat/emoticon_images",{emotesets:"0,42"}).done(function(e){if(!e||!e.emoticon_sets||!e.emoticon_sets[0])return void(t.__twitch_global_emotes=[]);var n=t.__twitch_global_emotes=[];e=e.emoticon_sets[0];for(var s=0;s0&&(i=!0)}var r=document.createElement("div"),c="";c+="

FrankerFaceZ

",c+='
new ways to woof
',r.className="chat-menu-content center",r.innerHTML=c,t.appendChild(r);var l=0,u=r.querySelector("h1");u&&u.addEventListener("click",function(){if(u.style.cursor="pointer",l++,l>=3){l=0;var e=document.querySelector(".app-main")||document.querySelector(".ember-chat-container");e&&e.classList.toggle("ffz-flip")}setTimeout(function(){l=0,u.style.cursor=""},2e3)});var h=document.createElement("div"),d=document.createElement("a"),_="To use custom emoticons in "+(i?"this channel":"tons of channels")+", get FrankerFaceZ from http://www.frankerfacez.com";d.className="button primary",d.innerHTML="Advertise in Chat",d.addEventListener("click",this._add_emote.bind(this,e,_)),h.appendChild(d);var f=document.createElement("a");f.className="button ffz-donate",f.href="https://www.frankerfacez.com/donate",f.target="_new",f.innerHTML="Donate",h.appendChild(f),h.className="chat-menu-content center",t.appendChild(h);var m=document.createElement("div");c='',c+='',c+='',c+='',c+='',m.className="chat-menu-content center",m.innerHTML=c;var p=!1;m.querySelector("#ffz-debug-logs").addEventListener("click",function(){p||(p=!0,o._pastebin(o._log_data.join("\n"),function(e){p=!1,e?prompt("Your FrankerFaceZ logs have been uploaded to the URL:",e):alert("There was an error uploading the FrankerFaceZ logs.")}))}),t.appendChild(m)}}},{"../constants":3}],20:[function(t){var n=e.FrankerFaceZ,s=t("../constants");n.settings_info.twitch_chat_dark={type:"boolean",value:!1,visible:!1},n.settings_info.dark_twitch={type:"boolean",value:!1,no_bttv:!0,name:"Dark Twitch",help:"Apply a dark background to channels and other related pages for easier viewing.",on_update:function(t){if(!this.has_bttv){document.body.classList.toggle("ffz-dark",t);var n=e.App?App.__container__.lookup("controller:settings").get("model"):void 0;t?(this._load_dark_css(),n&&this.settings.set("twitch_chat_dark",n.get("darkMode")),n&&n.set("darkMode",!0)):n&&n.set("darkMode",this.settings.twitch_chat_dark)}}},n.prototype.setup_dark=function(){this.has_bttv||(document.body.classList.toggle("ffz-dark",this.settings.dark_twitch),this.settings.dark_twitch&&(e.App&&App.__container__.lookup("controller:settings").set("model.darkMode",!0),this._load_dark_css()))},n.prototype._load_dark_css=function(){if(!this._dark_style){this.log("Injecting FrankerFaceZ Dark Twitch CSS.");var e=this._dark_style=document.createElement("link");e.id="ffz-dark-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",s.SERVER+"script/dark.css?_="+Date.now()),document.head.appendChild(e)}}},{"../constants":3}],21:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils"),o="http://static-cdn.jtvnw.net/emoticons/v1/";n.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var n,s=e._popup;s&&(s=jQuery(s),n=s.parent(),n.is(t.target)||0!==n.has(t.target).length||(s.remove(),delete e._popup,e._popup_kill&&e._popup_kill(),delete e._popup_kill))}),document.body.classList.toggle("ffz-menu-replace",this.settings.replace_twitch_menu)},n.menu_pages={},n.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),delete this._popup,this._popup_kill&&this._popup_kill(),void delete this._popup_kill;var i=document.createElement("div"),o=document.createElement("div"),a=document.createElement("ul"),r=this.has_bttv?BetterTTV.settings.get("darkenedMode"):!1;i.className="emoticon-selector chat-menu ffz-ui-popup",o.className="emoticon-selector-box dropmenu",i.appendChild(o),i.classList.toggle("dark",r);var c=document.createElement("div");c.className="ffz-ui-menu-page",o.appendChild(c),a.className="menu clearfix",o.appendChild(a);var l=document.createElement("li");l.className="title",l.innerHTML=""+(s.DEBUG?"[DEV] ":"")+"FrankerFaceZ",a.appendChild(l);var u=[];for(var h in n.menu_pages)if(n.menu_pages.hasOwnProperty(h)){var d=n.menu_pages[h];d&&(!d.hasOwnProperty("visible")||d.visible&&("function"!=typeof d.visible||d.visible.bind(this)()))&&u.push([d.sort_order||0,h,d])}u.sort(function(e,t){if(e[0]t[0])return-1;var n=e[1].toLowerCase(),s=t[1].toLowerCase();return s>n?1:n>s?-1:0});for(var _=0;_0,h.className="emoticon-grid",d.className="heading",u&&(d.style.backgroundImage='url("'+u+'")'),d.innerHTML='TwitchSubscriber Emoticons',h.appendChild(d);for(var f=r.get("emoticons"),m=0;m0&&t.appendChild(h),l){var y=c.get("content");if(y=y.length>0?y[y.length-1]:void 0,y&&y.purchase_profile&&!y.purchase_profile.will_renew){var w=i.parse_date(y.access_end||"");z=document.createElement("div"),k=document.createElement("div"),F=document.createElement("span"),end_time=w?Math.floor((w.getTime()-Date.now())/1e3):null,z.className="subscribe-message",k.className="non-subscriber-message",z.appendChild(k),F.className="unlock-text",F.innerHTML="Subscription expires in "+i.time_to_string(end_time,!0,!0),k.appendChild(F),t.appendChild(z)}}else{var z=document.createElement("div"),k=document.createElement("div"),F=document.createElement("span"),C=document.createElement("a");z.className="subscribe-message",k.className="non-subscriber-message",z.appendChild(k),F.className="unlock-text",F.innerHTML="Subscribe to unlock Emoticons",k.appendChild(F),C.className="action subscribe-button button primary",C.href=r.get("product_url"),C.innerHTML='",k.appendChild(C),t.appendChild(z)}}}this._emotes_for_sets(t,e,s&&s.set&&[s.set]||[],this.feature_friday||a?"Channel Emoticons":null,"http://cdn.frankerfacez.com/script/devicon.png","FrankerFaceZ"),this._feature_friday_ui(n,t,e)},name:"Channel",icon:s.ZREKNARF},n.prototype._emotes_for_sets=function(e,t,n,s,i,o){var a=document.createElement("div"),r=0;if(a.className="emoticon-grid",null!=s){var c=document.createElement("div");if(c.className="heading",o){var l=document.createElement("span");l.className="right",l.appendChild(document.createTextNode(o)),c.appendChild(l)}c.appendChild(document.createTextNode(s)),i&&(c.style.backgroundImage='url("'+i+'")'),a.appendChild(c)}for(var u=[],h=0;hn?-1:n>s?1:0});for(var h=0;h0&&(o=!0)}t.classList.toggle("no-emotes",!o),t.classList.toggle("live",c),t.classList.toggle("dark",a),t.classList.toggle("blue",r)}}},{"../constants":3}],23:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils"),o="http://static-cdn.jtvnw.net/emoticons/v1/",a={"#-?[\\\\/]":"#-/",":-?(?:7|L)":":-7","\\<\\;\\]":"<]","\\:-?(S|s)":":-S","\\:-?\\\\":":-\\","\\:\\>\\;":":>","B-?\\)":"B-)","\\:-?[z|Z|\\|]":":-Z","\\:-?\\)":":-)","\\:-?\\(":":-(","\\:-?(p|P)":":-P","\\;-?(p|P)":";-P","\\<\\;3":"<3","\\:-?[\\\\/]":":-/","\\;-?\\)":";-)","R-?\\)":"R-)","[o|O](_|\\.)[o|O]":"O.o","\\:-?D":":-D","\\:-?(o|O)":":-O","\\>\\;\\(":">(","Gr(a|e)yFace":"GrayFace"},r=function(e){var t=App.__container__.lookup("controller:chat"),n=t.get("currentRoom.id"),s=e.rooms[n],i=s?s.room.tmiSession:null,o=i&&i._emotesParser&&i._emotesParser.emoticonSetIds||"0",a=e.get_user(),r=a&&e.users[a.login]&&e.users[a.login].sets||[];return o=o.split(",").removeObject("0"),e.settings.global_emotes_in_menu&&(o.push("0"),r=_.union(r,e.default_sets)),[o,r]};n.settings_info.global_emotes_in_menu={type:"boolean",value:!1,name:"Display Global Emotes in My Emotes",help:"Display the global Twitch emotes in the My Emoticons menu."},n.prototype.setup_my_emotes=function(){if(this._twitch_set_to_channel={},this._twitch_badges={},localStorage.ffzTwitchSets)try{this._twitch_set_to_channel=JSON.parse(localStorage.ffzTwitchSets),this._twitch_badges=JSON.parse(localStorage.ffzTwitchBadges)}catch(e){}this._twitch_set_to_channel[0]="global",this._twitch_set_to_channel[33]="tfaces",this._twitch_set_to_channel[42]="tfaces",this._twitch_badges.global="//cdn.frankerfacez.com/script/twitch_logo.png",this._twitch_badges.tfaces=this._twitch_badges.turbo="//cdn.frankerfacez.com/script/turbo_badge.png"},n.menu_pages.my_emotes={name:"My Emoticons",icon:s.EMOTE,visible:function(){var e=r(this);return e[0].length>0||e[1].length>0},render:function(e,t){var s=e.get("controller.currentRoom.tmiSession"),i=(s&&s.getEmotes()||{emoticon_sets:{}}).emoticon_sets,o=[];for(var a in i)i.hasOwnProperty(a)&&!this._twitch_set_to_channel.hasOwnProperty(a)&&o.push(a);return o.length?void(t.innerHTML=JSON.stringify(o)):n.menu_pages.my_emotes.draw_menu.bind(this)(e,t,i)},draw_twitch_set:function(e,t,s){var r,c=document.createElement("div"),l=document.createElement("div"),u=this._twitch_set_to_channel[t];if(r="global"===u?"Global Emoticons":"turbo"===u?"Twitch Turbo":n.get_capitalization(u,function(e){c.innerHTML='Twitch'+i.sanitize(e)}),c.className="heading",c.innerHTML='Twitch'+i.sanitize(r),this._twitch_badges[u])c.style.backgroundImage='url("'+this._twitch_badges[u]+'")';else{var h=this;Twitch.api.get("chat/"+u+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(h._twitch_badges[u]=e.subscriber.image,c.style.backgroundImage='url("'+e.subscriber.image+'")')})}l.className="emoticon-grid",l.appendChild(c);for(var d=0;dFrankerFaceZ'+t.title,n.style.backgroundImage='url("'+(t.icon||"//cdn.frankerfacez.com/script/devicon.png")+'")',s.className="emoticon-grid",s.appendChild(n);for(var o in t.emoticons)t.emoticons.hasOwnProperty(o)&&!t.emoticons[o].hidden&&i.push(t.emoticons[o]);i.sort(function(e,t){var n=e.name.toLowerCase(),s=t.name.toLowerCase();return s>n?-1:n>s?1:e.idt.id?1:0});for(var a=0;an?-1:n>s?1:0});for(var l=0;lSpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},n.ws_on_close.push(function(){var t=e.App&&App.__container__.lookup("controller:channel"),n=t&&t.get("id"),s=!1;if(t){for(var i in this.srl_races)delete this.srl_races[i],i==n&&(s=!0);s&&this.rebuild_race_ui()}}),n.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),n=t.get("id"),s=!1,i=0;i=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var c="http://kadgar.net/live",l=!1;for(var u in a.entrants){var h=a.entrants[u].state;a.entrants.hasOwnProperty(u)&&a.entrants[u].channel&&("racing"==h||"entered"==h)&&(c+="/"+a.entrants[u].channel,l=!0)}var d=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,_=App.__container__.lookup("controller:channel"),f=_?_.get("display_name"):n.get_capitalization(o),m=encodeURIComponent("I'm watching "+f+" race "+a.goal+" in "+a.game+" on SpeedRunsLive!");r='
',r+='
Developers
Dan Salvato  
Stendec  
Version '+n.version_info+'Logs
',r+="
#Entrant Time
",r+='
',r+='',r+='

SRL',l&&(r+='   Multitwitch'),r+="

",e.innerHTML=r,t.appendChild(e),this._update_race(!0)}}},n.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var n=t.getAttribute("data-channel"),i=this.srl_races[n];if(!i)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var o=i.twitch_entrants[n],a=i.entrants[o],r=t.querySelector("#ffz-race-popup"),c=Date.now()/1e3,l=Math.floor(c-i.time);if(t.querySelector(".logo").innerHTML=s.placement(a),r){var u=r.querySelector("tbody"),h=r.querySelector(".heading span"),d=r.querySelector(".heading div");u.innerHTML="";var _=[],f=!0;for(var m in i.entrants)i.entrants.hasOwnProperty(m)&&("racing"==i.entrants[m].state&&(f=!1),_.push(i.entrants[m]));_.sort(function(e,t){var n=e.place||9999,s=t.place||9999,i=e.time||l,o=t.time||l;return("forfeit"==e.state||"dq"==e.state)&&(n=1e4),("forfeit"==t.state||"dq"==t.state)&&(s=1e4),s>n?-1:n>s?1:e.namet.name?1:o>i?-1:i>o?1:void 0});for(var p=0;p<_.length;p++){var m=_[p],g=''+m.display_name+"",v=m.channel?'':"",b=m.hitbox?'':"",y=l?s.time_to_string(m.time||l):"",w=s.place_string(m.place),z=m.comment?s.sanitize(m.comment):"";u.innerHTML+="'+w+""+g+""+v+b+''+("forfeit"==m.state?"Forfeit":y)+""}if(this._race_game!=i.game||this._race_goal!=i.goal){this._race_game=i.game,this._race_goal=i.goal;var k=s.sanitize(i.game),F=s.sanitize(i.goal);d.innerHTML='

'+k+"

Goal: "+F}l?f?h.innerHTML="Done":(h.innerHTML=s.time_to_string(l),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):h.innerHTML="Entry Open"}}}},{"../utils":29}],26:[function(t){var n=e.FrankerFaceZ,s=t("../constants");n.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",s.SERVER+"script/style.css?_="+Date.now()),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":3}],27:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils");n.prototype._update_subscribers=function(){this._update_subscribers_timer&&(clearTimeout(this._update_subscribers_timer),delete this._update_subscribers_timer);var e=this.get_user(),t=this,n=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,o=this.is_dashboard&&n&&n[1];if(this.has_bttv||!o||o!==e.login){var a=document.querySelector("#ffz-sub-display");return void(a&&a.parentElement.removeChild(a))}this._update_subscribers_timer=setTimeout(this._update_subscribers.bind(this),6e4),jQuery.ajax({url:"/broadcast/dashboard/partnership"}).done(function(e){try{var n,a=document.createElement("span");a.innerHTML=e,n=a.querySelector("#dash_main");var r=n&&n.textContent.match(/([\d,\.]+) total active subscribers/),c=r&&r[1];if(!c){var l=document.querySelector("#ffz-sub-display");return l&&l.parentElement.removeChild(l),void(t._update_subscribers_timer&&(clearTimeout(t._update_subscribers_timer),delete t._update_subscribers_timer))}var l=document.querySelector("#ffz-sub-display span");if(!l){var u=document.querySelector(t.is_dashboard?"#stats":"#channel .stats-and-actions .channel-stats");if(!u)return;var h=document.createElement("span");h.className="ffz stat",h.id="ffz-sub-display",h.title="Active Channel Subscribers",h.innerHTML=s.STAR+" ",l=document.createElement("span"),h.appendChild(l),Twitch.api.get("chat/"+o+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(h.innerHTML="",h.appendChild(l),h.style.backgroundImage='url("'+e.subscriber.image+'")',h.style.backgroundRepeat="no-repeat",h.style.paddingLeft="23px",h.style.backgroundPosition="0 50%")}),u.appendChild(h),jQuery(h).tipsy(t.is_dashboard?{gravity:"s"}:void 0)}l.innerHTML=i.number_commas(parseInt(c))}catch(d){t.error("_update_subscribers: "+d)}}).fail(function(){var e=document.querySelector("#ffz-sub-display");e&&e.parentElement.removeChild(e)})}},{"../constants":3,"../utils":29}],28:[function(t){var n=e.FrankerFaceZ,s=t("../constants"),i=t("../utils");n.ws_commands.viewers=function(t){var n=t[0],o=t[1],a=e.App&&App.__container__.lookup("controller:channel"),r=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,c=this.is_dashboard?r&&r[1]:a&&a.get&&a.get("id");if(!this.is_dashboard){var l=this.rooms&&this.rooms[n];return void(l&&(l.ffz_chatters=o,this._cindex&&this._cindex.ffzUpdateChatters()))}if(this.settings.chatter_count&&c===n){var u=document.querySelector("#ffz-ffzchatter-display"),h=s.ZREKNARF+" "+i.number_commas(o);if(u)u.innerHTML=h;else{var d=document.querySelector("#stats");if(!d)return;u=document.createElement("span"),u.id="ffz-ffzchatter-display",u.className="ffz stat",u.title="Chatters with FrankerFaceZ",u.innerHTML=h,d.appendChild(u),jQuery(u).tipsy(this.is_dashboard?{gravity:"s"}:void 0)}}}},{"../constants":3,"../utils":29}],29:[function(t,n){var s=(e.FrankerFaceZ,t("./constants"),{}),i=document.createElement("span"),o=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},a=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var n=Math.max(0,Math.min(255,e[0]-t)),s=Math.max(0,Math.min(255,e[1]-t)),i=Math.max(0,Math.min(255,e[2]-t));return[n,s,i]},r=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},c=function(e,t){return t=0===t?0:t||1,a(e,-t)},l=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;ta;(c||n)&&(c&&(s=s.substr(0,a)+s.substr(r+o.length)),n&&(s+=i+n+o),e.innerHTML=s)},get_luminance:l,brighten:a,darken:c,rgb_to_css:r,parse_date:h,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:o,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?o(e.place):""},sanitize:function(e){var t=s[e];return t||(i.textContent=e,t=s[e]=i.innerHTML,i.innerHTML=""),t},date_string:function(e){return e.getFullYear()+"-"+(e.getMonth()+1)+"-"+e.getDate()},time_to_string:function(e,t,n){var s=e%60,i=Math.floor(e/60),o=Math.floor(i/60),a="";if(i%=60,t){if(a=Math.floor(o/24),o%=24,n&&a>0)return a+" days";a=a>0?a+" days, ":""}return a+(10>o?"0":"")+o+":"+(10>i?"0":"")+i+":"+(10>s?"0":"")+s}}},{"./constants":3}]},{},[14]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file +!function(e){!function t(e,s,n){function o(a,r){if(!s[a]){if(!e[a]){var c="function"==typeof require&&require;if(!r&&c)return c(a,!0);if(i)return i(a,!0);throw new Error("Cannot find module '"+a+"'")}var l=s[a]={exports:{}};e[a][0].call(l.exports,function(t){var s=e[a][1][t];return o(s?s:t)},l,l.exports,t,e,s,n)}return s[a].exports}for(var i="function"==typeof require&&require,a=0;ae&&this._legacy_load_bots(e))})},s.prototype._legacy_load_donors=function(e){jQuery.ajax(n.SERVER+"script/donors.txt",{cache:!1,context:this}).done(function(e){this._legacy_parse_badges(e,1,1)}).fail(function(t){return 404!=t.status?(e=(e||0)+1,10>e?this._legacy_load_donors(e):void 0):void 0})},s.prototype._legacy_parse_badges=function(e,t,s){var n=this.badges[s].title,i=0;if(ds=null,null!=e)for(var a=e.trim().split(/\W+/),r=0;r50)return"Each user you unmod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var n=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/unmod "+o)}return"Sent unmod command for "+n+" users."},t.ffz_commands.massunmod.help="Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list.",t.ffz_commands.massmod=function(e,t){if(t=t.join(" ").trim(),!t.length)return"You must provide a list of users to mod.";t=t.split(/\W*,\W*/);var s=this.get_user();if(!s||!s.login==e.id)return"You must be the broadcaster to use massmod.";if(t.length>50)return"Each user you mod counts as a single message. To avoid being globally banned, please limit yourself to 50 at a time and wait between uses.";for(var n=t.length;t.length;){var o=t.shift();e.room.tmiRoom.sendMessage("/mod "+o)}return"Sent mod command for "+n+" users."},t.ffz_commands.massmod.help="Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."},{}],3:[function(e,t){var s='',n="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:n,SERVER:n?"//localhost:8000/":"//cdn.frankerfacez.com/",API_SERVER:"//api.frankerfacez.com/",SVGPATH:s,ZREKNARF:''+s+"",CHAT_BUTTON:''+s+"",ROOMS:'',CAMERA:'',INVITE:'',EYE:'',CLOCK:'',GEAR:'',HEART:'',EMOTE:'',STAR:''}},{}],4:[function(){var t=e.FrankerFaceZ;t.settings_info.developer_mode={type:"boolean",value:!1,storage_key:"ffzDebugMode",visible:function(){return this.settings.developer_mode||Date.now()-parseInt(localStorage.ffzLastDevMode||"0")<6048e5},category:"Debugging",name:"Developer Mode",help:"Load FrankerFaceZ from the local development server instead of the CDN. Please refresh after changing this setting.",on_update:function(){localStorage.ffzLastDevMode=Date.now()}},t.ffz_commands.developer_mode=function(e,t){var s,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?s=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(s=!1),void 0===s?"Developer Mode is currently "+(this.settings.developer_mode?"enabled.":"disabled."):(this.settings.set("developer_mode",s),"Developer Mode is now "+(s?"enabled":"disabled")+". Please refresh your browser.")},t.ffz_commands.developer_mode.help="Usage: /ffz developer_mode \nEnable or disable Developer Mode. When Developer Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],5:[function(t){var s=e.FrankerFaceZ,n=t("../utils"),o=t("../constants");s.prototype.setup_channel=function(){document.body.classList.toggle("ffz-hide-view-count",!this.settings.channel_views),this.log("Creating channel style element.");var e=this._channel_style=document.createElement("style");e.id="ffz-channel-css",document.head.appendChild(e),this.log("Hooking the Ember Channel Index view.");var t=App.__container__.resolve("view:channel/index"),n=this;if(t){this._modify_cindex(t);try{t.create().destroy()}catch(o){}for(var i in Ember.View.views)if(Ember.View.views.hasOwnProperty(i)){var a=Ember.View.views[i];a instanceof t&&(this.log("Manually updating Channel Index view.",a),this._modify_cindex(a),a.ffzInit())}this.log("Hooking the Ember Channel controller."),t=App.__container__.lookup("controller:channel"),t&&t.reopen({ffzUpdateUptime:function(){n._cindex&&n._cindex.ffzUpdateUptime()}.observes("isLive","content.id"),ffzUpdateTitle:function(){var e=this.get("content.name"),t=this.get("content.display_name");t&&(s.capitalization[e]=[t,Date.now()]),n._cindex&&n._cindex.ffzFixTitle()}.observes("content.status","content.id"),ffzHostTarget:function(){var e=this.get("content.hostModeTarget"),t=e&&e.get("name"),o=e&&e.get("display_name");o&&(s.capitalization[t]=[o,Date.now()]),n.settings.group_tabs&&n._chatv&&n._chatv.ffzRebuildTabs()}.observes("content.hostModeTarget")})}},s.prototype._modify_cindex=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(e){t.error("CIndex didInsertElement: "+e)}},willClearRender:function(){try{this.ffzTeardown()}catch(e){t.error("CIndex willClearRender: "+e)}return this._super()},ffzInit:function(){t._cindex=this,this.get("element").setAttribute("data-channel",this.get("controller.id")),this.ffzFixTitle(),this.ffzUpdateUptime(),this.ffzUpdateChatters();var e=this.get("element").querySelector(".svg-glyph_views:not(.ffz-svg)");e&&e.parentNode.classList.add("twitch-channel-views")},ffzFixTitle:function(){if(!t.has_bttv&&t.settings.stream_title){var e=this.get("controller.status"),s=this.get("controller.id");e=t.render_tokens(t.tokenize_line(s,s,e,!0)),this.$(".title span").each(function(t,s){var n=s.querySelectorAll("script");s.innerHTML=n[0].outerHTML+e+n[1].outerHTML})}},ffzUpdateChatters:function(){var e=this.get("controller.id"),s=t.rooms&&t.rooms[e];if(!s||!t.settings.chatter_count){var i=this.get("element").querySelector("#ffz-chatter-display");return i&&i.parentElement.removeChild(i),i=this.get("element").querySelector("#ffz-ffzchatter-display"),void(i&&i.parentElement.removeChild(i))}var a=Object.keys(s.room.get("ffz_chatters")||{}).length,r=s.ffz_chatters||0,i=this.get("element").querySelector("#ffz-chatter-display span");if(!i){var c=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!c)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-chatter-display",l.title="Current Chatters",l.innerHTML=o.ROOMS+" ",i=document.createElement("span"),l.appendChild(i);var h=c.querySelector("#ffz-ffzchatter-display");h?c.insertBefore(l,h):c.appendChild(l),jQuery(l).tipsy()}if(i.innerHTML=n.number_commas(a),!r)return i=this.get("element").querySelector("#ffz-ffzchatter-display"),void(i&&i.parentNode.removeChild(i));if(i=this.get("element").querySelector("#ffz-ffzchatter-display span"),!i){var c=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!c)return;var l=document.createElement("span");l.className="ffz stat",l.id="ffz-ffzchatter-display",l.title="Chatters with FrankerFaceZ",l.innerHTML=o.ZREKNARF+" ",i=document.createElement("span"),l.appendChild(i);var h=c.querySelector("#ffz-chatter-display");h?c.insertBefore(l,h.nextSibling):c.appendChild(l),jQuery(l).tipsy()}i.innerHTML=n.number_commas(r)},ffzUpdateUptime:function(){if(this._ffz_update_uptime&&(clearTimeout(this._ffz_update_uptime),delete this._ffz_update_uptime),!t.settings.stream_uptime||!this.get("controller.isLiveAccordingToKraken")){var e=this.get("element").querySelector("#ffz-uptime-display");return void(e&&e.parentElement.removeChild(e))}this._ffz_update_uptime=setTimeout(this.ffzUpdateUptime.bind(this),1e3);var s=this.get("controller.content.stream.created_at");if(s&&(s=n.parse_date(s))){var i=Math.floor((Date.now()-s.getTime())/1e3);if(!(0>i)){var e=this.get("element").querySelector("#ffz-uptime-display span");if(!e){var a=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!a)return;var r=document.createElement("span");r.className="ffz stat",r.id="ffz-uptime-display",r.title="Stream Uptime (since "+s.toLocaleString()+")",r.innerHTML=o.CLOCK+" ",e=document.createElement("span"),r.appendChild(e);var c=a.querySelector(".live-count");if(c)a.insertBefore(r,c.nextSibling);else try{c=a.querySelector("script:nth-child(0n+2)"),a.insertBefore(r,c.nextSibling)}catch(l){a.insertBefore(r,a.childNodes[0])}jQuery(r).tipsy({html:!0})}e.innerHTML=n.time_to_string(i)}}},ffzTeardown:function(){this.get("element").setAttribute("data-channel",""),t._cindex=void 0,this._ffz_update_uptime&&clearTimeout(this._ffz_update_uptime)}})},s.settings_info.chatter_count={type:"boolean",value:!1,category:"Channel Metadata",name:"Chatter Count",help:"Display the current number of users connected to chat beneath the channel.",on_update:function(e){if(this._cindex&&this._cindex.ffzUpdateChatters(),e&&this.rooms)for(var t in this.rooms)this.rooms.hasOwnProperty(t)&&this.rooms[t].room&&this.rooms[t].room.ffzInitChatterCount()}},s.settings_info.channel_views={type:"boolean",value:!0,category:"Channel Metadata",name:"Channel Views",help:"Display the number of times the channel has been viewed beneath the stream.",on_update:function(e){document.body.classList.toggle("ffz-hide-view-count",!e)}},s.settings_info.stream_uptime={type:"boolean",value:!1,category:"Channel Metadata",name:"Stream Uptime",help:"Display the stream uptime under a channel by the viewer count.",on_update:function(){this._cindex&&this._cindex.ffzUpdateUptime()}},s.settings_info.stream_title={type:"boolean",value:!0,no_bttv:!0,category:"Channel Metadata",name:"Title Links",help:"Make links in stream titles clickable.",on_update:function(){this._cindex&&this._cindex.ffzFixTitle()}}},{"../constants":3,"../utils":29}],6:[function(t){var s=e.FrankerFaceZ,n=t("../utils"),o=t("../constants"),i=function(e){return 1>e?"":e>=99?"99+":""+e};s.settings_info.prevent_clear={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Show Deleted Messages",help:"Fade deleted messages instead of replacing them, and prevent chat from being cleared.",on_update:function(e){if(!this.has_bttv&&this.rooms)for(var t in this.rooms){var s=this.rooms[t],n=s&&s.room;n&&n.get("messages").forEach(function(t,s){e&&!t.ffz_deleted&&t.deleted?n.set("messages."+s+".deleted",!1):!t.ffz_deleted||e||t.deleted||n.set("messages."+s+".deleted",!0)})}}},s.settings_info.chat_history={type:"boolean",value:!0,visible:!1,category:"Chat",name:"Chat History Alpha",help:"Load previous chat messages when loading a chat room so you can see what people have been talking about. This currently only works in a handful of channels due to server capacity."},s.settings_info.group_tabs={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Chat Room Tabs Beta",help:"Enhanced UI for switching the current chat room and noticing new messages.",on_update:function(e){var t=!this.has_bttv&&e;this._chatv&&t!==this._group_tabs_state&&(t?this._chatv.ffzEnableTabs():this._chatv.ffzDisableTabs())}},s.settings_info.pinned_rooms={type:"button",value:[],category:"Chat",visible:!1,name:"Pinned Chat Rooms",help:"Set a list of channels that should always be available in chat."},s.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat controller.");var e=App.__container__.lookup("controller:chat"),t=this;e&&e.reopen({ffzUpdateChannels:function(){t.settings.group_tabs&&t._chatv&&t._chatv.ffzRebuildTabs()}.observes("currentChannelRoom","connectedPrivateGroupRooms")}),this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e);try{e.create().destroy()}catch(s){}for(var n in Ember.View.views)if(Ember.View.views.hasOwnProperty(n)){var o=Ember.View.views[n];if(o instanceof e){this.log("Manually updating existing Chat view.",o);try{o.ffzInit()}catch(s){this.error("setup: build_ui_link: "+s)}}}this.log("Hooking the Ember Layout controller.");var i=App.__container__.lookup("controller:layout");if(i){i.reopen({ffzFixTabs:function(){t.settings.group_tabs&&t._chatv&&t._chatv._ffz_tabs&&setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}.observes("isRightColumnClosed")}),this.log("Hooking the Ember 'Right Column' controller. Seriously...");var a=App.__container__.lookup("controller:right-column");a&&a.reopen({ffzFixTabs:function(){t.settings.group_tabs&&t._chatv&&t._chatv._ffz_tabs&&setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}.observes("firstTabSelected")})}},s.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(e){t.error("ChatView didInsertElement: "+e)}},willClearRender:function(){try{this.ffzTeardown()}catch(e){t.error("ChatView willClearRender: "+e)}this._super()},ffzInit:function(){t._chatv=this,this.$(".textarea-contain").append(t.build_ui_link(this)),!t.has_bttv&&t.settings.group_tabs&&this.ffzEnableTabs(),setTimeout(function(){t.settings.group_tabs&&t._chatv._ffz_tabs&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px");var e=t._chatv.get("controller");e&&e.set("showList",!1)},1e3)},ffzTeardown:function(){t._chatv===this&&(t._chatv=null),this.$(".textarea-contain .ffz-ui-toggle").remove(),t.settings.group_tabs&&this.ffzDisableTabs()},ffzChangeRoom:Ember.observer("controller.currentRoom",function(){try{if(t.update_ui_link(),!t.has_bttv&&t.settings.group_tabs&&this._ffz_tabs){var e=this.get("controller.currentRoom");e&&e.resetUnreadCount();var s=jQuery(this._ffz_tabs);s.children(".ffz-chat-tab").removeClass("active"),e&&s.children('.ffz-chat-tab[data-room="'+e.get("id")+'"]').removeClass("tab-mentioned").addClass("active").children("span").text("");var n=e&&e.get("canInvite");this._ffz_invite&&this._ffz_invite.classList.toggle("hidden",!n),this.set("controller.showInviteUser",n&&this.get("controller.showInviteUser")),this.$(".chat-room").css("top",this._ffz_tabs.offsetHeight+"px")}}catch(o){t.error("ChatView ffzUpdateLink: "+o)}}),ffzEnableTabs:function(){if(!t.has_bttv&&t.settings.group_tabs){this.$(".chat-header").addClass("hidden");var e=this._ffz_tabs=document.createElement("div");e.id="ffz-group-tabs",this.$(".chat-header").after(e),this.ffzRebuildTabs()}},ffzRebuildTabs:function(){if(!t.has_bttv&&t.settings.group_tabs){var e=this._ffz_tabs||this.get("element").querySelector("#ffz-group-tabs");if(e){e.innerHTML="";var s=document.createElement("a"),n=this;s.className="button glyph-only tooltip",s.title="Chat Room Management",s.innerHTML=o.ROOMS,s.addEventListener("click",function(){var e=n.get("controller");e&&e.set("showList",!e.get("showList"))}),e.appendChild(s),s=document.createElement("a"),s.className="button glyph-only tooltip invite",s.title="Invite a User",s.innerHTML=o.INVITE,s.addEventListener("click",function(){var e=n.get("controller");e&&e.set("showInviteUser",e.get("currentRoom.canInvite")&&!e.get("showInviteUser"))}),s.classList.toggle("hidden",!this.get("controller.currentRoom.canInvite")),n._ffz_invite=s,e.appendChild(s);var i,a=this.get("controller.currentChannelRoom");a&&(i=this.ffzBuildTab(n,a,!0),i&&e.appendChild(i));var r=App.__container__.lookup("controller:channel"),c=App.__container__.resolve("model:room");if(target=r&&r.get("hostModeTarget"),target&&c){var l=target.get("id");this._ffz_host!==l&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),this._ffz_host=l,this._ffz_host_room=c.findOne(l))}else this._ffz_host&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),delete this._ffz_host,delete this._ffz_host_room);this._ffz_host_room&&(i=n.ffzBuildTab(n,this._ffz_host_room,!1,!0),i&&e.appendChild(i));for(var h=0;h"+l+"",h.addEventListener("click",function(){e.get("controller").focusRoom(t)}),h},ffzDisableTabs:function(){this._ffz_tabs&&(this._ffz_tabs.parentElement.removeChild(this._ffz_tabs),delete this._ffz_tabs,delete this._ffz_invite),this._ffz_host&&(this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),delete this._ffz_host,delete this._ffz_host_room),this.$(".chat-room").css("top",""),this.$(".chat-header").removeClass("hidden")}})},s.prototype.connect_extra_chat=function(){if(!this.has_bttv){for(var e=0;e1)return"Join Usage: /join ";var s=t[0].toLowerCase();return"#"===s.charAt(0)&&(s=s.substr(1)),this._join_room(s)?"Joining "+s+". You will always connect to this channel's chat unless you later /part from it.":"You have already joined "+s+'. Please use "/part '+s+'" to leave it.'},s.chat_commands.part=function(e,t){if(!t||!t.length||t.length>1)return"Part Usage: /part ";var s=t[0].toLowerCase();return"#"===s.charAt(0)&&(s=s.substr(1)),this._leave_room(s)?"Leaving "+s+".":this.rooms[s]?"You do not have "+s+" pinned and you cannot leave the current channel or hosted channels via /part.":"You are not in "+s+"."}},{"../constants":3,"../utils":29}],7:[function(t){var s=e.FrankerFaceZ,n=t("../utils"),o="[\\s`~<>!-#%-\\x2A,-/:;\\x3F@\\x5B-\\x5D_\\x7B}\\u00A1\\u00A7\\u00AB\\u00B6\\u00B7\\u00BB\\u00BF\\u037E\\u0387\\u055A-\\u055F\\u0589\\u058A\\u05BE\\u05C0\\u05C3\\u05C6\\u05F3\\u05F4\\u0609\\u060A\\u060C\\u060D\\u061B\\u061E\\u061F\\u066A-\\u066D\\u06D4\\u0700-\\u070D\\u07F7-\\u07F9\\u0830-\\u083E\\u085E\\u0964\\u0965\\u0970\\u0AF0\\u0DF4\\u0E4F\\u0E5A\\u0E5B\\u0F04-\\u0F12\\u0F14\\u0F3A-\\u0F3D\\u0F85\\u0FD0-\\u0FD4\\u0FD9\\u0FDA\\u104A-\\u104F\\u10FB\\u1360-\\u1368\\u1400\\u166D\\u166E\\u169B\\u169C\\u16EB-\\u16ED\\u1735\\u1736\\u17D4-\\u17D6\\u17D8-\\u17DA\\u1800-\\u180A\\u1944\\u1945\\u1A1E\\u1A1F\\u1AA0-\\u1AA6\\u1AA8-\\u1AAD\\u1B5A-\\u1B60\\u1BFC-\\u1BFF\\u1C3B-\\u1C3F\\u1C7E\\u1C7F\\u1CC0-\\u1CC7\\u1CD3\\u2010-\\u2027\\u2030-\\u2043\\u2045-\\u2051\\u2053-\\u205E\\u207D\\u207E\\u208D\\u208E\\u2329\\u232A\\u2768-\\u2775\\u27C5\\u27C6\\u27E6-\\u27EF\\u2983-\\u2998\\u29D8-\\u29DB\\u29FC\\u29FD\\u2CF9-\\u2CFC\\u2CFE\\u2CFF\\u2D70\\u2E00-\\u2E2E\\u2E30-\\u2E3B\\u3001-\\u3003\\u3008-\\u3011\\u3014-\\u301F\\u3030\\u303D\\u30A0\\u30FB\\uA4FE\\uA4FF\\uA60D-\\uA60F\\uA673\\uA67E\\uA6F2-\\uA6F7\\uA874-\\uA877\\uA8CE\\uA8CF\\uA8F8-\\uA8FA\\uA92E\\uA92F\\uA95F\\uA9C1-\\uA9CD\\uA9DE\\uA9DF\\uAA5C-\\uAA5F\\uAADE\\uAADF\\uAAF0\\uAAF1\\uABEB\\uFD3E\\uFD3F\\uFE10-\\uFE19\\uFE30-\\uFE52\\uFE54-\\uFE61\\uFE63\\uFE68\\uFE6A\\uFE6B\\uFF01-\\uFF03\\uFF05-\\uFF0A\\uFF0C-\\uFF0F\\uFF1A\\uFF1B\\uFF1F\\uFF20\\uFF3B-\\uFF3D\\uFF3F\\uFF5B\\uFF5D\\uFF5F-\\uFF65]",i=new RegExp(o+"*,"+o+"*"),a=function(e){return(e+"").replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(//g,">") +},r="http://static-cdn.jtvnw.net/emoticons/v1/",c=function(e){return r+e+"/1.0 1x, "+r+e+"/2.0 2x, "+r+e+"/3.0 4x"},l=function(e){var t=e.set,s=e.set_type,n=e.owner;return void 0===s&&(s="Channel"),t?(("00000turbo"==t||"turbo"==t)&&(t="Twitch Turbo",s=null),"Emoticon: "+e.code+"\n"+(s?s+": ":"")+t+(n?"\nBy: "+n.display_name:"")):e.code},h=function(e){{var t=this._twitch_emotes[e];t?t.set:null}return t?"string"==typeof t?t:t.tooltip?t.tooltip:t.tooltip=l(t):"???"},u=function(e,t,s,n){if(s){t&&(n.code=t),this._twitch_emotes[e]=n;for(var o=h.bind(this)(e),i=document.querySelectorAll('img[emote-id="'+e+'"]'),a=0;aYouTube: "+n.sanitize(s.title)+"
",t+="Channel: "+n.sanitize(s.channel)+" | "+n.time_to_string(s.duration)+"
",t+=n.number_commas(s.views||0)+" Views | 👍 "+n.number_commas(s.likes||0)+" 👎 "+n.number_commas(s.dislikes||0);else if("strawpoll"==s.type){t="Strawpoll: "+n.sanitize(s.title)+"
";for(var o in s.items){{var i=s.items[o];Math.floor(i/s.total*100)}t+='"}t+="
'+n.sanitize(o)+''+n.number_commas(i)+"

Total: "+n.number_commas(s.total);var a=n.parse_date(s.fetched);if(a){var r=Math.floor((a.getTime()-Date.now())/1e3);r>60&&(t+="
Data was cached "+n.time_to_string(r)+" ago.")}}else if("twitch"==s.type){t="Twitch: "+n.sanitize(s.display_name)+"
";var c=n.parse_date(s.since);c&&(t+="Member Since: "+n.date_string(c)+"
"),t+="Views: "+n.number_commas(s.views)+" | Followers: "+n.number_commas(s.followers)+""}else if("twitch_vod"==s.type)t="Twitch "+("highlight"==s.broadcast_type?"Highlight":"Broadcast")+": "+n.sanitize(s.title)+"
",t+="By: "+n.sanitize(s.display_name)+(s.game?" | Playing: "+n.sanitize(s.game):" | Not Playing")+"
",t+="Views: "+n.number_commas(s.views)+" | "+n.time_to_string(s.length);else if("twitter"==s.type)t="Tweet By: "+n.sanitize(s.user)+"
",t+=n.sanitize(s.tweet);else if("reputation"==s.type){if(t=''+n.sanitize(s.full.toLowerCase())+"",s.trust<50||s.safety<50||s.tags&&s.tags.length>0){t+="
";var l=!1;(s.trust<50||s.safety<50)&&(s.unsafe=!0,t+="Potentially Unsafe Link
",t+="Trust: "+s.trust+"% | Child Safety: "+s.safety+"%",l=!0),s.tags&&s.tags.length>0&&(t+=(l?"
":"")+"Tags: "+s.tags.join(", ")),t+="
Data Source: WOT"}}else s.full&&(t=''+n.sanitize(s.full.toLowerCase())+"");return t||(t=''+n.sanitize(e.toLowerCase())+""),s.tooltip=t,t},f=function(e,t,s){if(t){this._link_data[e]=s,s.unsafe=!1;var n,o=d.bind(this)(e),i="/"==e.charAt(e.length-1)?e.substr(0,e.length-1):null;if(n=document.querySelectorAll(i?'span.message a[href="'+e+'"], span.message a[href="'+i+'"], span.message a[data-url="'+e+'"], span.message a[data-url="'+i+'"]':'span.message a[href="'+e+'"], span.message a[data-url="'+e+'"]'),this.settings.link_info)for(var a=0;aBeta",help:"Check links against known bad websites, unshorten URLs, and show YouTube info."},s.settings_info.chat_rows={type:"boolean",value:!1,category:"Chat",no_bttv:!0,name:"Chat Line Backgrounds",help:"Display alternating background colors for lines in chat.",on_update:function(e){this.has_bttv||document.body.classList.toggle("ffz-chat-background",e)}},s.prototype.setup_line=function(){document.body.classList.toggle("ffz-chat-colors",!this.has_bttv&&this.settings.fix_color),document.body.classList.toggle("ffz-chat-background",!this.has_bttv&&this.settings.chat_rows),this._colors={},this._last_row={};var e=this._fix_color_style=document.createElement("style");e.id="ffz-style-username-colors",e.type="text/css",document.head.appendChild(e),this._twitch_emotes={},this._link_data={},this.log("Hooking the Ember Whisper controller.");var t=App.__container__.resolve("component:whisper-line");t&&this._modify_line(t),this.log("Hooking the Ember Line controller.");var n=App.__container__.resolve("component:message-line");n&&this._modify_line(n);var o=this.get_user();o&&o.name&&(s.capitalization[o.login]=[o.name,Date.now()])},s.prototype._modify_line=function(e){var t=this;e.reopen({tokenizedMessage:function(){var e=this.get("msgObject.cachedTokens");if(e)return e;e=this._super();try{var n=performance.now(),o=t.get_user(),i=o&&this.get("msgObject.from")===o.login;e=t._remove_banned(e),e=t._emoticonize(this,e);var a=this.get("msgObject.tags.display-name");a&&a.length&&(s.capitalization[this.get("msgObject.from")]=[a.trim(),Date.now()]),i||(e=t.tokenize_mentions(e));for(var r=0;r5&&t.log("Tokenizing Message Took Too Long - "+(l-n)+"ms",e,!1,!0)}catch(h){try{t.error("LineController tokenizedMessage: "+h)}catch(h){}}return this.set("msgObject.cachedTokens",e),e}.property("msgObject.message","isChannelLinksDisabled","currentUserNick","msgObject.from","msgObject.tags.emotes"),ffzUpdated:Ember.observer("msgObject.ffz_deleted","msgObject.ffz_old_messages",function(){this.rerender()}),didInsertElement:function(){this._super();try{var e=performance.now(),s=this.get("element"),o=this.get("msgObject.from"),i=this.get("msgObject.room")||App.__container__.lookup("controller:chat").get("currentRoom.id"),a=this.get("msgObject.color"),r=this.get("msgObject.ffz_alternate");a&&t._handle_color(a),void 0===r&&(r=t._last_row[i]=t._last_row.hasOwnProperty(i)?!t._last_row[i]:!1,this.set("msgObject.ffz_alternate",r)),s.classList.toggle("ffz-alternate",r),s.classList.toggle("ffz-deleted",t.settings.prevent_clear&&this.get("msgObject.ffz_deleted")),s.setAttribute("data-room",i),s.setAttribute("data-sender",o),s.setAttribute("data-deleted",this.get("msgObject.deleted")||!1);var l=this.get("msgObject.ffz_old_messages");if(l&&l.length){var h=document.createElement("div");h.className="button primary float-right",h.innerHTML="Show "+n.number_commas(l.length)+" Old",h.addEventListener("click",t._show_deleted.bind(t,i)),s.classList.add("clearfix"),s.classList.add("ffz-has-deleted"),this.$(".message").append(h)}t.render_badge(this),this.get("msgObject.ffz_has_mention")&&s.classList.add("ffz-mentioned");for(var d=s.querySelectorAll("a.deleted-link"),_=0;_-1&&(-1===s.indexOf("/")||s.indexOf("@")5&&t.log("Line Took Too Long - "+A+"ms",s.innerHTML,!1,!0)}catch(L){try{t.error("LineView didInsertElement: "+L)}catch(L){}}}})},s.prototype._handle_color=function(e){if(e&&!this._colors[e]){this._colors[e]=!0;var t=parseInt(e.substr(1),16),s=[t>>16,t>>8&255,255&t],o=n.get_luminance(s),i="",a='span[style="color:'+e+'"]',r=!1;if(o>.3){r=!0;for(var c=127,l=s;c--&&(l=n.darken(l),!(n.get_luminance(l)<=.3)););i+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+n.rgb_to_css(l)+" !important; }\n"}else i+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+e+" !important; }\n";if(.15>o){r=!0;for(var c=127,l=s;c--&&(l=n.brighten(l),!(n.get_luminance(l)>=.15)););i+=".ffz-chat-colors .theatre .chat-container .chat-line "+a+", .ffz-chat-colors .chat-container.dark .chat-line "+a+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+a+" { color: "+n.rgb_to_css(l)+" !important; }\n"}else i+=".ffz-chat-colors .theatre .chat-container .chat-line "+a+", .ffz-chat-colors .chat-container.dark .chat-line "+a+", .ffz-chat-colors .ember-chat-container.dark .chat-line "+a+" { color: "+e+" !important; }\n";r&&(this._fix_color_style.innerHTML+=i)}},s.capitalization={},s._cap_fetching=0,s.get_capitalization=function(t,n){if(e.BetterTTV&&BetterTTV.chat&&BetterTTV.chat.helpers.lookupDisplayName)return BetterTTV.chat.helpers.lookupDisplayName(t);if(!t)return t;if(t=t.toLowerCase(),"jtv"==t||"twitchnotify"==t)return t;var o=s.capitalization[t];return o&&Date.now()-o[1]<36e5?o[0]:(s._cap_fetching<25&&(s._cap_fetching++,s.get().ws_send("get_display_name",t,function(e,o){var i=e?o:t;s.capitalization[t]=[i,Date.now()],s._cap_fetching--,"function"==typeof n&&n(i)})),o?o[0]:t)},s.prototype._remove_banned=function(e){var t=this.settings.banned_words;if(!t||!t.length)return e;"string"==typeof e&&(e=[e]);for(var n=s._words_to_regex(t),o=[],i=0;i<banned link>',own:!0}:r)}return o},s.prototype._emoticonize=function(e,t){var s=e.get("msgObject.room"),n=e.get("msgObject.from");return this.tokenize_emotes(n,s,t)}},{"../utils":29}],8:[function(t){var s=e.FrankerFaceZ,n=t("../utils"),o={ESC:27,P:80,B:66,T:84,U:85},i=[["5m",300],["10m",600],["1hr",3600],["12hr",43200],["24hr",86400]],a='',r='';s.settings_info.enhanced_moderation={type:"boolean",value:!1,no_bttv:!0,category:"Chat",name:"Enhanced Moderation",help:"Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards."},s.prototype.setup_mod_card=function(){this.log("Hooking the Ember Moderation Card view.");var t=App.__container__.resolve("component:moderation-card"),s=this;t.reopen({didInsertElement:function(){this._super(),e._card=this;try{if(s.has_bttv||!s.settings.enhanced_moderation)return;var t=this.get("element"),c=this.get("controller");if(t.classList.add("ffz-moderation-card"),c.get("cardInfo.isModeratorOrHigher")){t.classList.add("ffz-is-mod"),t.setAttribute("tabindex",1),t.addEventListener("keyup",function(e){var t=e.keyCode||e.which,s=c.get("cardInfo.user.id"),n=App.__container__.lookup("controller:chat").get("currentRoom");if(t==o.P)n.send("/timeout "+s+" 1");else if(t==o.B)n.send("/ban "+s);else if(t==o.T)n.send("/timeout "+s+" 600");else if(t==o.U)n.send("/unban "+s);else if(t!=o.ESC)return;c.send("hideModOverlay")});var l=document.createElement("div");l.className="interface clearfix";var h=function(e){var t=c.get("cardInfo.user.id"),s=App.__container__.lookup("controller:chat").get("currentRoom");s.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},u=function(e,t){var s=document.createElement("button");return s.className="button",s.innerHTML=e,s.title="Timeout User for "+n.number_commas(t)+" Second"+(1!=t?"s":""),600===t?s.title="(T)"+s.title.substr(1):1===t&&(s.title="(P)urge - "+s.title),jQuery(s).tipsy(),s.addEventListener("click",h.bind(this,t)),s};l.appendChild(u("Purge",1));var d=document.createElement("span");d.className="right",l.appendChild(d);for(var f=0;f button");b&&b.classList.contains("message-button")&&(b.innerHTML=a,b.classList.add("glyph-only"),b.classList.add("message"),b.title="Message User",jQuery(b).tipsy()),this.$().draggable({start:function(){t.focus()}}),t.focus()}catch(y){try{s.error("ModerationCardView didInsertElement: "+y)}catch(y){}}}})},s.chat_commands.purge=s.chat_commands.p=function(e,t){if(!t||!t.length)return"Purge Usage: /p username [more usernames separated by spaces]";if(t.length>10)return"Please only purge up to 10 users at once.";for(var s=0;s10)return"Please only ban up to 10 users at once.";for(var s=0;s10)return"Please only unban up to 10 users at once.";for(var s=0;s300,f=t.length,_=n.get("messages.0.ffz_alternate")||!1;d&&(_=!_);for(var f=t.length;f--;){var m=t[f];if("string"==typeof m.date&&(m.date=o.parse_date(m.date)),m.ffz_alternate=_=!_,m.room||(m.room=e),m.color||(m.color=m.tags&&m.tags.color?m.tags.color:a&&m.from?a.getColor(m.from.toLowerCase()):"#755000"),!m.labels||!m.labels.length){var p=m.labels=[];if(m.tags)if(m.tags.turbo&&p.push("turbo"),m.tags.subscriber&&p.push("subscriber"),m.from===e)p.push("owner");else{var g=m.tags["user-type"];("mod"===g||"staff"===g||"admin"===g||"global_mod"===g)&&p.push(g)}}if(m.style||("jtv"===m.from?m.style="admin":"twitchnotify"===m.from&&(m.style="notification")),m.cachedTokens&&m.cachedTokens.length||this.tokenize_chat_line(m,!0),n.shouldShowMessage(m)){if(!(i.lengthv&&(m.ffz_old_messages=m.ffz_old_messages.slice(m.ffz_old_messages.length-v))}i.unshiftObject(m),r+=1}}if(d){var m={ffz_alternate:!_,color:"#755000",date:new Date,from:"frankerfacez_admin",style:"admin",message:"(Last message is "+o.human_time(u)+" old.)",room:e};if(this.tokenize_chat_line(m),n.shouldShowMessage(m))for(i.insertAt(r,m);i.length>n.get("messageBufferSize");)i.removeAt(0)}}},s.prototype.load_room=function(e,t,s){var o=this;jQuery.getJSON(n.API_SERVER+"v1/room/"+e).done(function(s){if(s.sets)for(var n in s.sets)s.sets.hasOwnProperty(n)&&o._load_set_json(n,void 0,s.sets[n]);o._load_room_json(e,t,s)}).fail(function(n){return 404==n.status?"function"==typeof t&&t(!1):(s=(s||0)+1,10>s?o.load_room(e,t,s):"function"==typeof t&&t(!1))})},s.prototype._load_room_json=function(e,t,s){return s&&s.room?(s=s.room,this.rooms[e]&&(s.room=this.rooms[e].room),s.needs_history=this.rooms[e]&&this.rooms[e].needs_history||!1,this.rooms[e]=s,(s.css||s.moderator_badge)&&o.update_css(this._room_style,e,i(s)+(s.css||"")),this.emote_sets.hasOwnProperty(s.set)||this.load_set(s.set),this.update_ui_link(),void(t&&t(!0,s))):"function"==typeof t&&t(!1)},s.prototype._modify_room=function(t){var s=this;t.reopen({init:function(){this._super();try{s.add_room(this.id,this),this.set("ffz_chatters",{})}catch(e){s.error("add_room: "+e)}},willDestroy:function(){this._super();try{s.remove_room(this.id)}catch(e){s.error("remove_room: "+e)}},clearMessages:function(e){var t=this;if(e)this.get("messages").forEach(function(n,o){n.from===e&&(t.set("messages."+o+".ffz_deleted",!0),s.settings.prevent_clear||t.set("messages."+o+".deleted",!0))});else if(s.settings.prevent_clear)this.addTmiMessage("A moderator's attempt to clear chat was ignored.");else{var n=t.get("messages");t.set("messages",[]),t.addMessage({style:"admin",message:i18n("Chat was cleared by a moderator"),ffz_old_messages:n})}},pushMessage:function(e){if(this.shouldShowMessage(e)){var t,s,n,o=this.get("messageBufferSize");for(this.get("messages").pushObject(e),t=this.get("messages.length"),s=t-o,n=0;s>n;n++)this.get("messages").removeAt(0);"admin"===e.style||"whisper"===e.style&&!this.ffz_whisper_room||this.incrementProperty("unreadCount",1)}},addMessage:function(e){try{if(e){var t="whisper"===e.style;if(s.settings.group_tabs&&s.settings.whisper_room&&(t&&!this.ffz_whisper_room||!t&&this.ffz_whisper_room))return;t||(e.room=this.get("id")),s.tokenize_chat_line(e)}}catch(n){s.error("Room addMessage: "+n)}return this._super(e)},setHostMode:function(e){var t=App.__container__.lookup("controller:chat");if(t&&t.get("currentChannelRoom")===this)return this._super(e)},send:function(e){if(!(s.settings.group_tabs&&s.settings.whisper_room&&this.ffz_whisper_room)){try{var t=e.split(" ",1)[0].toLowerCase();if("/ffz"===t)return this.set("messageToSend",""),void s.run_ffz_command(e.substr(5),this.get("id"));if("/"===t.charAt(0)&&s.run_command(e,this.get("id")))return void this.set("messageToSend","")}catch(n){s.error("send: "+n)}return this._super(e)}},ffzUpdateUnread:function(){if(s.settings.group_tabs){var e=App.__container__.lookup("controller:chat");e&&e.get("currentRoom")===this?this.resetUnreadCount():s._chatv&&s._chatv.ffzTabUnread(this.get("id"))}}.observes("unreadCount"),ffzInitChatterCount:function(){if(this.tmiRoom){var e=this;this.tmiRoom.list().done(function(t){var s={};t=t.data.chatters;for(var n=0;n0)){var o=s.emoticonSetIds;s.emoticonSetIds="",s.updateEmoticons(o),this._twitch_emote_check=setTimeout(this.check_twitch_emotes.bind(this),1e4)}},s.prototype.getEmotes=function(e,t){var s=this.users&&this.users[e],n=this.rooms&&this.rooms[t];return _.union(s&&s.sets||[],n&&n.set&&[n.set]||[],this.default_sets)},s.ws_commands.reload_set=function(e){this.emote_sets.hasOwnProperty(e)&&this.load_set(e)},s.ws_commands.load_set=function(e){this.load_set(e)},s.prototype._emote_tooltip=function(e){if(!e)return null;if(e._tooltip)return e._tooltip;var t=this.emote_sets[e.set_id],s=e.owner,n=t&&t.title||"Global";return e._tooltip="Emoticon: "+(e.hidden?"???":e.name)+"\nFFZ "+n+(s?"\nBy: "+s.display_name:""),e._tooltip},s.prototype.load_global_sets=function(e,t){var s=this;jQuery.getJSON(n.API_SERVER+"v1/set/global").done(function(e){s.default_sets=e.default_sets; +var t=s.global_sets=[],n=e.sets||{};for(var o in n)if(n.hasOwnProperty(o)){var i=n[o];t.push(o),s._load_set_json(o,void 0,i)}}).fail(function(n){return 404==n.status?"function"==typeof e&&e(!1):(t=t||0,t++,50>t?s.load_global_sets(e,t):"function"==typeof e&&e(!1))})},s.prototype.load_set=function(e,t,s){var o=this;jQuery.getJSON(n.API_SERVER+"v1/set/"+e).done(function(s){o._load_set_json(e,t,s&&s.set)}).fail(function(n){return 404==n.status?"function"==typeof t&&t(!1):(s=s||0,s++,10>s?o.load_set(e,t,s):"function"==typeof t&&t(!1))})},s.prototype.unload_set=function(e){var t=this.emote_sets[e];t&&(this.log("Unloading emoticons for set: "+e),o.update_css(this._emote_style,e,null),delete this.emote_sets[e])},s.prototype._load_set_json=function(e,t,s){if(!s)return"function"==typeof t&&t(!1);this.emote_sets[e]=s,s.users=[],s.count=0;var n="",i=s.emoticons;s.emoticons={};for(var a=0;a=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(s||0)+t),t))},t.prototype.setup_bttv=function(e){this.log("BetterTTV was detected after "+e+"ms. Hooking."),this.has_bttv=!0,document.body.classList.remove("ffz-dark"),this._dark_style&&(this._dark_style.parentElement.removeChild(this._dark_style),delete this._dark_style),this.settings.group_tabs&&this._chatv&&this._chatv.ffzDisableTabs(),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background"),this.is_dashboard&&this._update_subscribers();var t=BetterTTV.chat.helpers.sendMessage,s=this;BetterTTV.chat.helpers.sendMessage=function(e){var n=e.split(" ",1)[0].toLowerCase();return"/ffz"!==n?t(e):void s.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var n,o=BetterTTV.chat.handlers.onPrivmsg;BetterTTV.chat.handlers.onPrivmsg=function(e,t){n=e;var s=o(e,t);return n=null,s};var i=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,o,a,r){try{return s.bttv_badges(r),'
'+BetterTTV.chat.templates.timestamp(r.time)+" "+(a?BetterTTV.chat.templates.modicons():"")+" "+BetterTTV.chat.templates.badges(r.badges)+BetterTTV.chat.templates.from(r.nickname,r.color)+BetterTTV.chat.templates.message(r.sender,r.message,r.emotes,t?r.color:!1)+"
"}catch(c){return s.log("Error: ",c),i(e,t,o,a,r)}};var a,r=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,n,o){try{o=o||!1;var i=encodeURIComponent(t);if("jtv"!==e){a=e;var c=BetterTTV.chat.templates.emoticonize(t,n);a=null;for(var l=0;l'+t+"
"}catch(h){return s.log("Error: ",h),r(e,t,n,o)}};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var o=c(e,t),i=n||BetterTTV.getChannel(),r=i&&i.toLowerCase(),l=a&&a.toLowerCase(),h=s.getEmotes(l,r),t=[],u=s.get_user(),d=u&&u.login===l;return _.each(h,function(e){var n=s.emote_sets[e];n&&_.each(n.emoticons,function(e){_.any(o,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length?(_.each(t,function(e){var t=s._emote_tooltip(e),n=[''+t+''],i=o;if(o=[],!i||!i.length)return o;for(var a=0;a=6e4?this.log("Emote Menu for Twitch was not detected after 60 seconds."):setTimeout(this.find_emote_menu.bind(this,t,(s||0)+t),t))},t.prototype.setup_emote_menu=function(e){this.log("Emote Menu for Twitch was detected after "+e+"ms. Registering emote enumerator."),emoteMenu.registerEmoteGetter("FrankerFaceZ",this._emote_menu_enumerator.bind(this))},t.prototype._emote_menu_enumerator=function(){for(var e=this.get_user(),s=e?e.login:null,n=App.__container__.lookup("controller:chat"),o=n?n.get("currentRoom.id"):null,i=this.getEmotes(s,o),a=[],r=0;r=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(s||0)+t),t)))},s.prototype.setup_normal=function(t){var n=e.performance&&performance.now?performance.now():Date.now();this.log("Found non-Ember Twitch after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+s.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.find_bttv(10);var o=e.performance&&performance.now?performance.now():Date.now(),i=o-n;this.log("Initialization complete in "+i+"ms")},s.prototype.is_dashboard=!1,s.prototype.setup_dashboard=function(t){var n=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch Dashboard after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+s.version_info),this.users={},this.is_dashboard=!0,this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_notifications(),this.setup_css(),this._update_subscribers(),this.setup_message_event(),this.find_bttv(10);var o=e.performance&&performance.now?performance.now():Date.now(),i=o-n;this.log("Initialization complete in "+i+"ms")},s.prototype.setup_ember=function(t){var n=e.performance&&performance.now?performance.now():Date.now();this.log("Found Twitch application after "+(t||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+s.version_info),this.users={},this.load_settings(),this.setup_dark(),this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_channel(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_viewers(),this.setup_mod_card(),this.setup_notifications(),this.setup_css(),this.setup_menu(),this.setup_my_emotes(),this.setup_races(),this.connect_extra_chat(),this.find_bttv(10),this.find_emote_menu(10),this.check_ff();var o=e.performance&&performance.now?performance.now():Date.now(),i=o-n;this.log("Initialization complete in "+i+"ms")},s.prototype.setup_message_event=function(){this.log("Listening for Window Messages."),e.addEventListener("message",this._on_window_message.bind(this),!1)},s.prototype._on_window_message=function(e){if(e.data&&e.data.from_ffz){var t=e.data;this.log("Window Message",t)}}},{"./badges":1,"./commands":2,"./debug":4,"./ember/channel":5,"./ember/chatview":6,"./ember/line":7,"./ember/moderation-card":8,"./ember/room":9,"./ember/viewers":10,"./emoticons":11,"./ext/betterttv":12,"./ext/emote_menu":13,"./featurefriday":15,"./settings":16,"./socket":17,"./tokenize":18,"./ui/about_page":19,"./ui/dark":20,"./ui/menu":21,"./ui/menu_button":22,"./ui/my_emotes":23,"./ui/notifications":24,"./ui/races":25,"./ui/styles":26,"./ui/sub_count":27,"./ui/viewer_count":28}],15:[function(t){var s=e.FrankerFaceZ,n=t("./constants");s.prototype.feature_friday=null,s.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(n.SERVER+"script/event.json",{cache:!1,dataType:"json",context:this}).done(function(e){return this._load_ff(e)}).fail(function(t){return 404==t.status?this._load_ff(null):(e=e||0,e++,10>e?setTimeout(this.check_ff.bind(this,e),250):this._load_ff(null))})},s.ws_commands.reload_ff=function(){this.check_ff()},s.prototype._feature_friday_ui=function(e,t,s){if(this.feature_friday&&this.feature_friday.channel!=e){this._emotes_for_sets(t,s,[this.feature_friday.set],this.feature_friday.title,this.feature_friday.icon,"FrankerFaceZ");var n=App.__container__.lookup("controller:channel");if(!n||n.get("id")!=this.feature_friday.channel){var o=this.feature_friday,i=document.createElement("div"),a=document.createElement("a");i.className="chat-menu-content",i.style.textAlign="center";var r=o.display_name+(o.live?" is live now!":"");a.className="button primary",a.classList.toggle("live",o.live),a.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),a.href="http://www.twitch.tv/"+o.channel,a.title=r,a.target="_new",a.innerHTML=""+r+"",i.appendChild(a),t.appendChild(i)}}},s.prototype._load_ff=function(e){this.feature_friday&&(this.global_sets.removeObject(this.feature_friday.set),this.default_sets.removeObject(this.feature_friday.set),this.feature_friday=null,this.update_ui_link()),e&&e.set&&e.channel&&(this.feature_friday={set:e.set,channel:e.channel,live:!1,title:e.title||"Feature Friday",display_name:s.get_capitalization(e.channel,this._update_ff_name.bind(this))},this.global_sets.push(e.set),this.default_sets.push(e.set),this.load_set(e.set),this._update_ff_live())},s.prototype._update_ff_live=function(){if(this.feature_friday){var e=this;Twitch.api.get("streams/"+this.feature_friday.channel).done(function(t){e.feature_friday.live=null!=t.stream,e.update_ui_link()}).always(function(){e.feature_friday.timer=setTimeout(e._update_ff_live.bind(e),12e4)})}},s.prototype._update_ff_name=function(e){this.feature_friday&&(this.feature_friday.display_name=e)}},{"./constants":3}],16:[function(t){var s=e.FrankerFaceZ,n=t("./constants");make_ls=function(e){return"ffz_setting_"+e},toggle_setting=function(e,t){var s=!this.settings.get(t);this.settings.set(t,s),e.classList.toggle("active",s)},s.settings_info={},s.prototype.load_settings=function(){this.log("Loading settings."),this.settings={};for(var t in s.settings_info)if(s.settings_info.hasOwnProperty(t)){var n=s.settings_info[t],o=n.storage_key||make_ls(t),i=n.hasOwnProperty("value")?n.value:void 0;if(localStorage.hasOwnProperty(o))try{i=JSON.parse(localStorage.getItem(o))}catch(a){this.log('Error loading value for "'+t+'": '+a)}this.settings[t]=i}this.settings.get=this._setting_get.bind(this),this.settings.set=this._setting_set.bind(this),this.settings.del=this._setting_del.bind(this),e.addEventListener("storage",this._setting_update.bind(this),!1)},s.settings_info.replace_twitch_menu={type:"boolean",value:!1,name:"Replace Twitch Emoticon Menu Beta",help:"Completely replace the default Twitch emoticon menu.",on_update:function(e){document.body.classList.toggle("ffz-menu-replace",e)}},s.menu_pages.settings={render:function(e,t){var n={},o=[];for(var i in s.settings_info)if(s.settings_info.hasOwnProperty(i)){var a=s.settings_info[i],r=a.category||"Miscellaneous",c=n[r];if(void 0!==a.visible&&null!==a.visible){var l=a.visible;if("function"==typeof a.visible&&(l=a.visible.bind(this)()),!l)continue}c||(o.push(r),c=n[r]=[]),c.push([i,a])}o.sort(function(e,t){var e=e.toLowerCase(),t=t.toLowerCase();return"debugging"===e&&(e="zzz"+e),"debugging"===t&&(t="zzz"+t),t>e?-1:e>t?1:0});for(var h=0;hs?-1:s>n?1:i>o?-1:o>i?1:0});for(var m=0;m",v.className="switch-label",v.innerHTML=a.name,p.appendChild(y),p.appendChild(v),y.addEventListener("click",toggle_setting.bind(this,y,i))}else{p.classList.add("option");var w=document.createElement("a");w.innerHTML=a.name,w.href="#",p.appendChild(w),w.addEventListener("click",a.method.bind(this))}if(a.help){var b=document.createElement("span");b.className="help",b.innerHTML=a.help,p.appendChild(b)}}f.appendChild(p)}t.appendChild(f)}},name:"Settings",icon:n.GEAR,sort_order:99999,wide:!0},s.prototype._setting_update=function(t){if(t||(t=e.event),t.key&&"ffz_setting_"===t.key.substr(0,12)){var n=t.key,o=n.substr(12),i=void 0,a=s.settings_info[o];if(!a){for(o in s.settings_info)if(s.settings_info.hasOwnProperty(o)&&(a=s.settings_info[o],a.storage_key==n))break;if(a.storage_key!=n)return}this.log("Updated Setting: "+o);try{i=JSON.parse(t.newValue)}catch(r){this.log('Error loading new value for "'+o+'": '+r),i=a.value||void 0}if(this.settings[o]=i,a.on_update)try{a.on_update.bind(this)(i,!1)}catch(r){this.log('Error running updater for setting "'+o+'": '+r)}}},s.prototype._setting_get=function(e){return this.settings[e]},s.prototype._setting_set=function(e,t){var n=s.settings_info[e],o=n.storage_key||make_ls(e),i=JSON.stringify(t);if(this.settings[e]=t,localStorage.setItem(o,i),this.log('Changed Setting "'+e+'" to: '+i),n.on_update)try{n.on_update.bind(this)(t,!0)}catch(a){this.log('Error running updater for setting "'+e+'": '+a)}},s.prototype._setting_del=function(e){var t=s.settings_info[e],n=t.storage_key||make_ls(e),o=void 0;if(localStorage.hasOwnProperty(n)&&localStorage.removeItem(n),delete this.settings[e],t&&(o=this.settings[e]=t.hasOwnProperty("value")?t.value:void 0),t.on_update)try{t.on_update.bind(this)(o,!0)}catch(i){this.log('Error running updater for setting "'+e+'": '+i)}}},{"./constants":3}],17:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.ws_on_close=[],t.prototype.ws_create=function(){var s,n=this;this._ws_last_req=0,this._ws_callbacks={},this._ws_pending=this._ws_pending||[];try{s=this._ws_sock=new WebSocket("ws://catbag.frankerfacez.com/")}catch(o){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+o)}this._ws_exists=!0,s.onopen=function(){n._ws_open=!0,n._ws_delay=0,n.log("Socket connected.");var s=e.RequestFileSystem||e.webkitRequestFileSystem;s?s(e.TEMPORARY,100,n.ws_send.bind(n,"hello",["ffz_"+t.version_info,localStorage.ffzClientId],n._ws_on_hello.bind(n)),n.log.bind(n,"Operating in Incognito Mode.")):n.ws_send("hello",["ffz_"+t.version_info,localStorage.ffzClientId],n._ws_on_hello.bind(n));var o=n.get_user();if(o&&n.ws_send("setuser",o.login),n.is_dashboard){var i=location.pathname.match(/\/([^\/]+)/);i&&n.ws_send("sub",i[1])}for(var a in n.rooms)n.rooms.hasOwnProperty(a)&&n.rooms[a]&&(n.ws_send("sub",a),n.rooms[a].needs_history&&(n.rooms[a].needs_history=!1,!n.has_bttv&&n.settings.chat_history&&n.ws_send("chat_history",[a,25],n._load_history.bind(n,a))));var r=n._ws_pending;n._ws_pending=[];for(var c=0;c';if(e.isLink){if(!t&&void 0!==t)return e.href;var s=e.href;if(s.indexOf("@")>-1&&(-1===s.indexOf("/")||s.indexOf("@")'+s+"";var n=(s.match(/^https?:\/\//)?"":"http://")+s;return''+s+""}return e.mentionedUser?''+e.mentionedUser+"":o.sanitize(e.deletedLink?e.text:e)}).join("")},n.prototype.tokenize_title_emotes=function(e){var t=this,s=App.__container__.lookup("controller:channel"),n=s&&s.get("product.emoticons"),o=[];return _.isString(e)&&(e=[e]),_.each(_.union(t.__twitch_global_emotes||[],n),function(t){if(t&&"inactive"!==t.state){var s=new RegExp("\\b"+t.regex+"\\b");_.any(e,function(e){return _.isString(e)&&e.match(s)})&&o.push(t)}}),(void 0===t.__twitch_global_emotes||null===t.__twitch_global_emotes)&&(t.__twitch_global_emotes=!1,Twitch.api.get("chat/emoticon_images",{emotesets:"0,42"}).done(function(e){if(!e||!e.emoticon_sets||!e.emoticon_sets[0])return void(t.__twitch_global_emotes=[]);var s=t.__twitch_global_emotes=[];e=e.emoticon_sets[0];for(var n=0;n0&&(o=!0)}var r=document.createElement("div"),c="";c+="

FrankerFaceZ

",c+='
new ways to woof
',r.className="chat-menu-content center",r.innerHTML=c,t.appendChild(r);var l=0,h=r.querySelector("h1");h&&h.addEventListener("click",function(){if(h.style.cursor="pointer",l++,l>=3){l=0;var e=document.querySelector(".app-main")||document.querySelector(".ember-chat-container");e&&e.classList.toggle("ffz-flip")}setTimeout(function(){l=0,h.style.cursor=""},2e3)});var u=document.createElement("div"),d=document.createElement("a"),f="To use custom emoticons in "+(o?"this channel":"tons of channels")+", get FrankerFaceZ from http://www.frankerfacez.com";d.className="button primary",d.innerHTML="Advertise in Chat",d.addEventListener("click",this._add_emote.bind(this,e,f)),u.appendChild(d);var _=document.createElement("a");_.className="button ffz-donate",_.href="https://www.frankerfacez.com/donate",_.target="_new",_.innerHTML="Donate",u.appendChild(_),u.className="chat-menu-content center",t.appendChild(u);var m=document.createElement("div");c='',c+='',c+='',c+='',c+='',m.className="chat-menu-content center",m.innerHTML=c;var p=!1;m.querySelector("#ffz-debug-logs").addEventListener("click",function(){p||(p=!0,i._pastebin(i._log_data.join("\n"),function(e){p=!1,e?prompt("Your FrankerFaceZ logs have been uploaded to the URL:",e):alert("There was an error uploading the FrankerFaceZ logs.")}))}),t.appendChild(m)}}},{"../constants":3}],20:[function(t){var s=e.FrankerFaceZ,n=t("../constants");s.settings_info.twitch_chat_dark={type:"boolean",value:!1,visible:!1},s.settings_info.dark_twitch={type:"boolean",value:!1,no_bttv:!0,name:"Dark Twitch",help:"Apply a dark background to channels and other related pages for easier viewing.",on_update:function(t){if(!this.has_bttv){document.body.classList.toggle("ffz-dark",t);var s=e.App?App.__container__.lookup("controller:settings").get("model"):void 0;t?(this._load_dark_css(),s&&this.settings.set("twitch_chat_dark",s.get("darkMode")),s&&s.set("darkMode",!0)):s&&s.set("darkMode",this.settings.twitch_chat_dark)}}},s.prototype.setup_dark=function(){this.has_bttv||(document.body.classList.toggle("ffz-dark",this.settings.dark_twitch),this.settings.dark_twitch&&(e.App&&App.__container__.lookup("controller:settings").set("model.darkMode",!0),this._load_dark_css()))},s.prototype._load_dark_css=function(){if(!this._dark_style){this.log("Injecting FrankerFaceZ Dark Twitch CSS.");var e=this._dark_style=document.createElement("link");e.id="ffz-dark-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",n.SERVER+"script/dark.css?_="+Date.now()),document.head.appendChild(e)}}},{"../constants":3}],21:[function(t){var s=e.FrankerFaceZ,n=t("../constants"),o=t("../utils"),i="http://static-cdn.jtvnw.net/emoticons/v1/";s.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var s,n=e._popup;n&&(n=jQuery(n),s=n.parent(),s.is(t.target)||0!==s.has(t.target).length||(n.remove(),delete e._popup,e._popup_kill&&e._popup_kill(),delete e._popup_kill))}),document.body.classList.toggle("ffz-menu-replace",this.settings.replace_twitch_menu)},s.menu_pages={},s.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),delete this._popup,this._popup_kill&&this._popup_kill(),void delete this._popup_kill; +var o=document.createElement("div"),i=document.createElement("div"),a=document.createElement("ul"),r=this.has_bttv?BetterTTV.settings.get("darkenedMode"):!1;o.className="emoticon-selector chat-menu ffz-ui-popup",i.className="emoticon-selector-box dropmenu",o.appendChild(i),o.classList.toggle("dark",r);var c=document.createElement("div");c.className="ffz-ui-menu-page",i.appendChild(c),a.className="menu clearfix",i.appendChild(a);var l=document.createElement("li");l.className="title",l.innerHTML=""+(n.DEBUG?"[DEV] ":"")+"FrankerFaceZ",a.appendChild(l);var h=[];for(var u in s.menu_pages)if(s.menu_pages.hasOwnProperty(u)){var d=s.menu_pages[u];try{if(!d||d.hasOwnProperty("visible")&&(!d.visible||"function"==typeof d.visible&&!d.visible.bind(this)(e)))continue}catch(f){this.error("menu_pages "+u+" visible: "+f);continue}h.push([d.sort_order||0,u,d])}h.sort(function(e,t){if(e[0]t[0])return-1;var s=e[1].toLowerCase(),n=t[1].toLowerCase();return n>s?1:s>n?-1:0});for(var _=0;_0,u.className="emoticon-grid",d.className="heading",h&&(d.style.backgroundImage='url("'+h+'")'),d.innerHTML='TwitchSubscriber Emoticons',u.appendChild(d);for(var _=r.get("emoticons"),m=0;m<_.length;m++){var p=_[m];if("active"===p.state){var g=document.createElement("span"),v=l||!p.subscriber_only,b='image-set(url("'+i+p.id+'/1.0") 1x, url("'+i+p.id+'/2.0") 2x, url("'+i+p.id+'/3.0") 4x)';g.className="emoticon tooltip"+(v?"":" locked"),g.style.backgroundImage='url("'+i+p.id+'/1.0")',g.style.backgroundImage="-webkit-"+b,g.style.backgroundImage="-moz-"+b,g.style.backgroundImage="-ms-"+b,g.style.backgroundImage=b,g.style.width=p.width+"px",g.style.height=p.height+"px",g.title=p.regex,v&&g.addEventListener("click",this._add_emote.bind(this,e,p.regex)),u.appendChild(g),f++}}if(f>0&&t.appendChild(u),l){var y=c.get("content");if(y=y.length>0?y[y.length-1]:void 0,y&&y.purchase_profile&&!y.purchase_profile.will_renew){var w=o.parse_date(y.access_end||"");z=document.createElement("div"),k=document.createElement("div"),C=document.createElement("span"),end_time=w?Math.floor((w.getTime()-Date.now())/1e3):null,z.className="subscribe-message",k.className="non-subscriber-message",z.appendChild(k),C.className="unlock-text",C.innerHTML="Subscription expires in "+o.time_to_string(end_time,!0,!0),k.appendChild(C),t.appendChild(z)}}else{var z=document.createElement("div"),k=document.createElement("div"),C=document.createElement("span"),F=document.createElement("a");z.className="subscribe-message",k.className="non-subscriber-message",z.appendChild(k),C.className="unlock-text",C.innerHTML="Subscribe to unlock Emoticons",k.appendChild(C),F.className="action subscribe-button button primary",F.href=r.get("product_url"),F.innerHTML='",k.appendChild(F),t.appendChild(z)}}}this._emotes_for_sets(t,e,n&&n.set&&[n.set]||[],this.feature_friday||a?"Channel Emoticons":null,"http://cdn.frankerfacez.com/script/devicon.png","FrankerFaceZ"),this._feature_friday_ui(s,t,e)},name:"Channel",icon:n.ZREKNARF},s.prototype._emotes_for_sets=function(e,t,s,n,o,i){var a=document.createElement("div"),r=0;if(a.className="emoticon-grid",null!=n){var c=document.createElement("div");if(c.className="heading",i){var l=document.createElement("span");l.className="right",l.appendChild(document.createTextNode(i)),c.appendChild(l)}c.appendChild(document.createTextNode(n)),o&&(c.style.backgroundImage='url("'+o+'")'),a.appendChild(c)}for(var h=[],u=0;us?-1:s>n?1:0});for(var u=0;u0&&(i=!0)}t.classList.toggle("no-emotes",!i),t.classList.toggle("live",c),t.classList.toggle("dark",a),t.classList.toggle("blue",r)}}},{"../constants":3}],23:[function(t){var s=e.FrankerFaceZ,n=t("../constants"),o=t("../utils"),i="http://static-cdn.jtvnw.net/emoticons/v1/",a={"#-?[\\\\/]":"#-/",":-?(?:7|L)":":-7","\\<\\;\\]":"<]","\\:-?(S|s)":":-S","\\:-?\\\\":":-\\","\\:\\>\\;":":>","B-?\\)":"B-)","\\:-?[z|Z|\\|]":":-Z","\\:-?\\)":":-)","\\:-?\\(":":-(","\\:-?(p|P)":":-P","\\;-?(p|P)":";-P","\\<\\;3":"<3","\\:-?[\\\\/]":":-/","\\;-?\\)":";-)","R-?\\)":"R-)","[o|O](_|\\.)[o|O]":"O.o","\\:-?D":":-D","\\:-?(o|O)":":-O","\\>\\;\\(":">(","Gr(a|e)yFace":"GrayFace"};s.settings_info.global_emotes_in_menu={type:"boolean",value:!1,name:"Display Global Emotes in My Emotes",help:"Display the global Twitch emotes in the My Emoticons menu."},s.prototype.setup_my_emotes=function(){if(this._twitch_set_to_channel={},this._twitch_badges={},localStorage.ffzTwitchSets)try{this._twitch_set_to_channel=JSON.parse(localStorage.ffzTwitchSets),this._twitch_badges=JSON.parse(localStorage.ffzTwitchBadges)}catch(e){}this._twitch_set_to_channel[0]="global",this._twitch_set_to_channel[33]="tfaces",this._twitch_set_to_channel[42]="tfaces",this._twitch_badges.global="//cdn.frankerfacez.com/script/twitch_logo.png",this._twitch_badges.tfaces=this._twitch_badges.turbo="//cdn.frankerfacez.com/script/turbo_badge.png"},s.menu_pages.my_emotes={name:"My Emoticons",icon:n.EMOTE,visible:function(e){var t=this.get_user(),s=e.get("controller.currentRoom.tmiSession"),n=t&&this.users[t.login]&&this.users[t.login].sets||[],o=(s&&s.getEmotes()||{emoticon_sets:{}}).emoticon_sets;return n.length||o&&Object.keys(o).length},render:function(e,t){var n=e.get("controller.currentRoom.tmiSession"),o=(n&&n.getEmotes()||{emoticon_sets:{}}).emoticon_sets,i=[];for(var a in o)o.hasOwnProperty(a)&&!this._twitch_set_to_channel.hasOwnProperty(a)&&i.push(a);if(!i.length)return s.menu_pages.my_emotes.draw_menu.bind(this)(e,t,o);var r=this,c=function(){if(i.length){i=[];var n={};for(var a in o)r._twitch_set_to_channel[a]&&(n[a]=o[a]);return s.menu_pages.my_emotes.draw_menu.bind(r)(e,t,n)}};this.ws_send("twitch_sets",i,function(n,a){if(i.length){if(i=[],n){for(var l in a)a.hasOwnProperty(l)&&(r._twitch_set_to_channel[l]=a[l]);return localStorage.ffzTwitchSets=JSON.stringify(r._twitch_set_to_channel),s.menu_pages.my_emotes.draw_menu.bind(r)(e,t,o)}c()}}),setTimeout(c,2e3)},draw_twitch_set:function(e,t,n){var r,c=document.createElement("div"),l=document.createElement("div"),h=this._twitch_set_to_channel[t];if(r="global"===h?"Global Emoticons":"turbo"===h?"Twitch Turbo":s.get_capitalization(h,function(e){c.innerHTML='Twitch'+o.sanitize(e)}),c.className="heading",c.innerHTML='Twitch'+o.sanitize(r),this._twitch_badges[h])c.style.backgroundImage='url("'+this._twitch_badges[h]+'")';else{var u=this;Twitch.api.get("chat/"+h+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(u._twitch_badges[h]=e.subscriber.image,localStorage.ffzTwitchBadges=JSON.stringify(u._twitch_badges),c.style.backgroundImage='url("'+e.subscriber.image+'")')})}l.className="emoticon-grid",l.appendChild(c);for(var d=0;dFrankerFaceZ'+t.title,s.style.backgroundImage='url("'+(t.icon||"//cdn.frankerfacez.com/script/devicon.png")+'")',n.className="emoticon-grid",n.appendChild(s);for(var i in t.emoticons)t.emoticons.hasOwnProperty(i)&&!t.emoticons[i].hidden&&o.push(t.emoticons[i]);o.sort(function(e,t){var s=e.name.toLowerCase(),n=t.name.toLowerCase();return n>s?-1:s>n?1:e.idt.id?1:0});for(var a=0;as?-1:s>n?1:0});for(var l=0;lSpeedRunsLive races under channels.',on_update:function(){this.rebuild_race_ui()}},s.ws_on_close.push(function(){var t=e.App&&App.__container__.lookup("controller:channel"),s=t&&t.get("id"),n=!1;if(t){for(var o in this.srl_races)delete this.srl_races[o],o==s&&(n=!0);n&&this.rebuild_race_ui()}}),s.ws_commands.srl_race=function(e){for(var t=App.__container__.lookup("controller:channel"),s=t.get("id"),n=!1,o=0;o=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=e;var c="http://kadgar.net/live",l=!1;for(var h in a.entrants){var u=a.entrants[h].state;a.entrants.hasOwnProperty(h)&&a.entrants[h].channel&&("racing"==u||"entered"==u)&&(c+="/"+a.entrants[h].channel,l=!0)}var d=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:t.parentElement.offsetTop-175,f=App.__container__.lookup("controller:channel"),_=f?f.get("display_name"):s.get_capitalization(i),m=encodeURIComponent("I'm watching "+_+" race "+a.goal+" in "+a.game+" on SpeedRunsLive!");r='
',r+='
Developers
Dan Salvato  
Stendec  
Version '+s.version_info+'Logs
',r+="
#Entrant Time
",r+='
',r+='',r+='

SRL',l&&(r+='   Multitwitch'),r+="

",e.innerHTML=r,t.appendChild(e),this._update_race(!0)}}},s.prototype._update_race=function(e){this._race_timer&&e&&(clearTimeout(this._race_timer),delete this._race_timer);var t=document.querySelector("#ffz-ui-race");if(t){var s=t.getAttribute("data-channel"),o=this.srl_races[s];if(!o)return t.parentElement.removeChild(t),this._popup_kill&&this._popup_kill(),void(this._popup&&(delete this._popup,delete this._popup_kill));var i=o.twitch_entrants[s],a=o.entrants[i],r=t.querySelector("#ffz-race-popup"),c=Date.now()/1e3,l=Math.floor(c-o.time);if(t.querySelector(".logo").innerHTML=n.placement(a),r){var h=r.querySelector("tbody"),u=r.querySelector(".heading span"),d=r.querySelector(".heading div");h.innerHTML="";var f=[],_=!0;for(var m in o.entrants)o.entrants.hasOwnProperty(m)&&("racing"==o.entrants[m].state&&(_=!1),f.push(o.entrants[m]));f.sort(function(e,t){var s=e.place||9999,n=t.place||9999,o=e.time||l,i=t.time||l;return("forfeit"==e.state||"dq"==e.state)&&(s=1e4),("forfeit"==t.state||"dq"==t.state)&&(n=1e4),n>s?-1:s>n?1:e.namet.name?1:i>o?-1:o>i?1:void 0});for(var p=0;p'+m.display_name+"",v=m.channel?'':"",b=m.hitbox?'':"",y=l?n.time_to_string(m.time||l):"",w=n.place_string(m.place),z=m.comment?n.sanitize(m.comment):"";h.innerHTML+="'+w+""+g+""+v+b+''+("forfeit"==m.state?"Forfeit":y)+""}if(this._race_game!=o.game||this._race_goal!=o.goal){this._race_game=o.game,this._race_goal=o.goal;var k=n.sanitize(o.game),C=n.sanitize(o.goal);d.innerHTML='

'+k+"

Goal: "+C}l?_?u.innerHTML="Done":(u.innerHTML=n.time_to_string(l),this._race_timer=setTimeout(this._update_race.bind(this),1e3)):u.innerHTML="Entry Open"}}}},{"../utils":29}],26:[function(t){var s=e.FrankerFaceZ,n=t("../constants");s.prototype.setup_css=function(){this.log("Injecting main FrankerFaceZ CSS.");var e=this._main_style=document.createElement("link");e.id="ffz-ui-css",e.setAttribute("rel","stylesheet"),e.setAttribute("href",n.SERVER+"script/style.css?_="+Date.now()),document.head.appendChild(e),jQuery.noty.themes.ffzTheme={name:"ffzTheme",style:function(){this.$bar.removeClass().addClass("noty_bar").addClass("ffz-noty").addClass(this.options.type)},callback:{onShow:function(){},onClose:function(){}}}}},{"../constants":3}],27:[function(t){var s=e.FrankerFaceZ,n=t("../constants"),o=t("../utils");s.prototype._update_subscribers=function(){this._update_subscribers_timer&&(clearTimeout(this._update_subscribers_timer),delete this._update_subscribers_timer);var e=this.get_user(),t=this,s=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,i=this.is_dashboard&&s&&s[1];if(this.has_bttv||!i||i!==e.login){var a=document.querySelector("#ffz-sub-display");return void(a&&a.parentElement.removeChild(a))}this._update_subscribers_timer=setTimeout(this._update_subscribers.bind(this),6e4),jQuery.ajax({url:"/broadcast/dashboard/partnership"}).done(function(e){try{var s,a=document.createElement("span");a.innerHTML=e,s=a.querySelector("#dash_main");var r=s&&s.textContent.match(/([\d,\.]+) total active subscribers/),c=r&&r[1];if(!c){var l=document.querySelector("#ffz-sub-display");return l&&l.parentElement.removeChild(l),void(t._update_subscribers_timer&&(clearTimeout(t._update_subscribers_timer),delete t._update_subscribers_timer))}var l=document.querySelector("#ffz-sub-display span");if(!l){var h=document.querySelector(t.is_dashboard?"#stats":"#channel .stats-and-actions .channel-stats");if(!h)return;var u=document.createElement("span");u.className="ffz stat",u.id="ffz-sub-display",u.title="Active Channel Subscribers",u.innerHTML=n.STAR+" ",l=document.createElement("span"),u.appendChild(l),Twitch.api.get("chat/"+i+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(u.innerHTML="",u.appendChild(l),u.style.backgroundImage='url("'+e.subscriber.image+'")',u.style.backgroundRepeat="no-repeat",u.style.paddingLeft="23px",u.style.backgroundPosition="0 50%")}),h.appendChild(u),jQuery(u).tipsy(t.is_dashboard?{gravity:"s"}:void 0)}l.innerHTML=o.number_commas(parseInt(c))}catch(d){t.error("_update_subscribers: "+d)}}).fail(function(){var e=document.querySelector("#ffz-sub-display");e&&e.parentElement.removeChild(e)})}},{"../constants":3,"../utils":29}],28:[function(t){var s=e.FrankerFaceZ,n=t("../constants"),o=t("../utils");s.ws_commands.viewers=function(t){var s=t[0],i=t[1],a=e.App&&App.__container__.lookup("controller:channel"),r=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,c=this.is_dashboard?r&&r[1]:a&&a.get&&a.get("id");if(!this.is_dashboard){var l=this.rooms&&this.rooms[s];return void(l&&(l.ffz_chatters=i,this._cindex&&this._cindex.ffzUpdateChatters()))}if(this.settings.chatter_count&&c===s){var h=document.querySelector("#ffz-ffzchatter-display"),u=n.ZREKNARF+" "+o.number_commas(i);if(h)h.innerHTML=u;else{var d=document.querySelector("#stats");if(!d)return;h=document.createElement("span"),h.id="ffz-ffzchatter-display",h.className="ffz stat",h.title="Chatters with FrankerFaceZ",h.innerHTML=u,d.appendChild(h),jQuery(h).tipsy(this.is_dashboard?{gravity:"s"}:void 0)}}}},{"../constants":3,"../utils":29}],29:[function(t,s){var n=(e.FrankerFaceZ,t("./constants"),{}),o=document.createElement("span"),i=function(e,t,s){return s=s||"s",t=t||"",1===e?t:s},a=function(e){return 1==e?"1st":2==e?"2nd":3==e?"3rd":null==e?"---":e+"th"},r=function(e,t){t=0===t?0:t||1,t=Math.round(255*-(t/100));var s=Math.max(0,Math.min(255,e[0]-t)),n=Math.max(0,Math.min(255,e[1]-t)),o=Math.max(0,Math.min(255,e[2]-t));return[s,n,o]},c=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},l=function(e,t){return t=0===t?0:t||1,r(e,-t)},h=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;ta;(c||s)&&(c&&(n=n.substr(0,a)+n.substr(r+i.length)),s&&(n+=o+s+i),e.innerHTML=n)},get_luminance:h,brighten:r,darken:l,rgb_to_css:c,parse_date:d,number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")},place_string:a,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?a(e.place):""},sanitize:function(e){var t=n[e];return t||(o.textContent=e,t=n[e]=o.innerHTML,o.innerHTML=""),t},date_string:function(e){return e.getFullYear()+"-"+(e.getMonth()+1)+"-"+e.getDate()},pluralize:i,human_time:function(e){e=Math.floor(e);var t=Math.floor(e/31536e3);if(t)return t+" year"+i(t);var s=Math.floor((e%=31536e3)/86400);if(s)return s+" day"+i(s);var n=Math.floor((e%=86400)/3600);if(n)return n+" hour"+i(n);var o=Math.floor((e%=3600)/60);if(o)return o+" minute"+i(o);var a=e%60;return a?a+" second"+i(a):"less than a second"},time_to_string:function(e,t,s){var n=e%60,o=Math.floor(e/60),i=Math.floor(o/60),a="";if(o%=60,t){if(a=Math.floor(i/24),i%=24,s&&a>0)return a+" days";a=a>0?a+" days, ":""}return a+(10>i?"0":"")+i+":"+(10>o?"0":"")+o+":"+(10>n?"0":"")+n}}},{"./constants":3}]},{},[14]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index 2156521d..fd7345e0 100644 --- a/src/badges.js +++ b/src/badges.js @@ -169,6 +169,20 @@ FFZ.prototype.render_badge = function(component) { if ( ! data || ! data.badges ) return; + // If we don't have badges, add them. + if ( ! badges.length ) { + var b_cont = document.createElement('span'), + from = component.$('.from'); + + b_cont.className = 'badges float-left'; + + if ( ! from ) + return; + + from.before(b_cont); + badges = $(b_cont); + } + // Figure out where to place our badge(s). var before = badges.find('.badge').filter(function(i) { var t = this.title.toLowerCase(); @@ -222,7 +236,7 @@ FFZ.prototype.render_badge = function(component) { if ( reverse ) { while(badges_out.length) - before.before(badges_out.shift()[1]); + badges.before(badges_out.shift()[1]); } else { while(badges_out.length) badges.append(badges_out.shift()[1]); diff --git a/src/ember/channel.js b/src/ember/channel.js index 56473a1d..fb782ffc 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -67,9 +67,9 @@ FFZ.prototype.setup_channel = function() { if ( f._cindex ) f._cindex.ffzFixTitle(); - }.observes("content.status", "content.id") + }.observes("content.status", "content.id"), - /*ffzHostTarget: function() { + ffzHostTarget: function() { var target = this.get('content.hostModeTarget'), name = target && target.get('name'), display_name = target && target.get('display_name'); @@ -79,7 +79,7 @@ FFZ.prototype.setup_channel = function() { if ( f.settings.group_tabs && f._chatv ) f._chatv.ffzRebuildTabs(); - }.observes("content.hostModeTarget")*/ + }.observes("content.hostModeTarget") }); } diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 5d57b8c7..1435f54b 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -17,6 +17,47 @@ var FFZ = window.FrankerFaceZ, // Settings // -------------------- +FFZ.settings_info.prevent_clear = { + type: "boolean", + value: false, + + no_bttv: true, + + category: "Chat", + name: "Show Deleted Messages", + help: "Fade deleted messages instead of replacing them, and prevent chat from being cleared.", + + on_update: function(val) { + if ( this.has_bttv || ! this.rooms ) + return; + + for(var room_id in this.rooms) { + var ffz_room = this.rooms[room_id], + room = ffz_room && ffz_room.room; + if ( ! room ) + continue; + + room.get("messages").forEach(function(s, n) { + if ( val && ! s.ffz_deleted && s.deleted ) + room.set("messages." + n + ".deleted", false); + + else if ( s.ffz_deleted && ! val && ! s.deleted ) + room.set("messages." + n + ".deleted", true); + }); + } + } + }; + +FFZ.settings_info.chat_history = { + type: "boolean", + value: true, + + visible: false, + category: "Chat", + name: "Chat History Alpha", + help: "Load previous chat messages when loading a chat room so you can see what people have been talking about. This currently only works in a handful of channels due to server capacity.", + }; + FFZ.settings_info.group_tabs = { type: "boolean", value: false, diff --git a/src/ember/line.js b/src/ember/line.js index 6248baf9..6d689a6c 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -321,13 +321,29 @@ FFZ.prototype.setup_line = function() { this._twitch_emotes = {}; this._link_data = {}; + this.log("Hooking the Ember Whisper controller."); + var Whisper = App.__container__.resolve('component:whisper-line'); + + if ( Whisper ) + this._modify_line(Whisper); this.log("Hooking the Ember Line controller."); - var Line = App.__container__.resolve('component:message-line'), - f = this; + var Line = App.__container__.resolve('component:message-line'); - Line.reopen({ + if ( Line ) + this._modify_line(Line); + + // Store the capitalization of our own name. + var user = this.get_user(); + if ( user && user.name ) + FFZ.capitalization[user.login] = [user.name, Date.now()]; +} + +FFZ.prototype._modify_line = function(component) { + var f = this; + + component.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. var tokens = this.get("msgObject.cachedTokens"); @@ -375,6 +391,10 @@ FFZ.prototype.setup_line = function() { }.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"), + ffzUpdated: Ember.observer("msgObject.ffz_deleted", "msgObject.ffz_old_messages", function() { + this.rerender(); + }), + didInsertElement: function() { this._super(); try { @@ -397,12 +417,29 @@ FFZ.prototype.setup_line = function() { } el.classList.toggle('ffz-alternate', row_type); + el.classList.toggle('ffz-deleted', f.settings.prevent_clear && this.get('msgObject.ffz_deleted')); // Basic Data el.setAttribute('data-room', room); el.setAttribute('data-sender', user); - el.setAttribute('data-deleted', this.get('deleted')||false); + el.setAttribute('data-deleted', this.get('msgObject.deleted')||false); + + + // Old Messages (for Chat Clear) + var old_messages = this.get("msgObject.ffz_old_messages"); + if ( old_messages && old_messages.length ) { + var btn = document.createElement('div'); + btn.className = 'button primary float-right'; + btn.innerHTML = 'Show ' + utils.number_commas(old_messages.length) + ' Old'; + + btn.addEventListener("click", f._show_deleted.bind(f, room)); + + el.classList.add('clearfix'); + el.classList.add('ffz-has-deleted'); + + this.$('.message').append(btn); + } // Badge @@ -550,12 +587,6 @@ FFZ.prototype.setup_line = function() { } } }); - - - // Store the capitalization of our own name. - var user = this.get_user(); - if ( user && user.name ) - FFZ.capitalization[user.login] = [user.name, Date.now()]; } @@ -701,7 +732,7 @@ FFZ.prototype._remove_banned = function(tokens) { // --------------------- FFZ.prototype._emoticonize = function(component, tokens) { - var room_id = component.get("msgObject.room") || App.__container__.lookup('controller:chat').get('currentRoom.id'), + var room_id = component.get("msgObject.room"), user_id = component.get("msgObject.from"); return this.tokenize_emotes(user_id, room_id, tokens); diff --git a/src/ember/room.js b/src/ember/room.js index 15000da9..163868f0 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -194,11 +194,17 @@ FFZ.prototype.add_room = function(id, room) { this.log("Adding Room: " + id); // Create a basic data table for this room. - this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null}; + var data = this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null, needs_history: false}; // Let the server know where we are. this.ws_send("sub", id); + // See if we need history? + if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) { + if ( ! this.ws_send("chat_history", [id,25], this._load_history.bind(this, id)) ) + data.needs_history = true; + } + // For now, we use the legacy function to grab the .css file. this.load_room(id); } @@ -232,6 +238,142 @@ FFZ.prototype.remove_room = function(id) { } +// -------------------- +// Chat History +// -------------------- + +FFZ.prototype._load_history = function(room_id, success, data) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return; + + if ( success ) + this.log("Received " + data.length + " old messages for: " + room_id); + else + return this.log("Error retrieving chat history for: " + room_id); + + if ( ! data.length ) + return; + + return this._insert_history(room_id, data); +} + + +FFZ.prototype._show_deleted = function(room_id) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return; + + var old_messages = room.room.get('messages.0.ffz_old_messages'); + if ( ! old_messages || ! old_messages.length ) + return; + + room.room.set('messages.0.ffz_old_messages', undefined); + this._insert_history(room_id, old_messages); +} + +FFZ.prototype._insert_history = function(room_id, data) { + var room = this.rooms[room_id]; + if ( ! room || ! room.room ) + return; + + var r = room.room, + messages = r.get('messages'), + tmiSession = r.tmiSession || (TMI._sessions && TMI._sessions[0]), + tmiRoom = r.tmiRoom, + + inserted = 0, + + last_msg = data[data.length - 1], + now = new Date(), + last_date = typeof last_msg.date === "string" ? utils.parse_date(last_msg.date) : last_msg.date, + age = (now - last_date) / 1000, + is_old = age > 300, + + i = data.length, + alternation = r.get('messages.0.ffz_alternate') || false; + + if ( is_old ) + alternation = ! alternation; + + var i = data.length; + while(i--) { + var msg = data[i]; + + if ( typeof msg.date === "string" ) + msg.date = utils.parse_date(msg.date); + + msg.ffz_alternate = alternation = ! alternation; + if ( ! msg.room ) + msg.room = room_id; + + if ( ! msg.color ) + msg.color = msg.tags && msg.tags.color ? msg.tags.color : tmiSession && msg.from ? tmiSession.getColor(msg.from.toLowerCase()) : "#755000"; + + if ( ! msg.labels || ! msg.labels.length ) { + var labels = msg.labels = []; + if ( msg.tags ) { + if ( msg.tags.turbo ) + labels.push("turbo"); + if ( msg.tags.subscriber ) + labels.push("subscriber"); + if ( msg.from === room_id ) + labels.push("owner") + else { + var ut = msg.tags['user-type']; + if ( ut === 'mod' || ut === 'staff' || ut === 'admin' || ut === 'global_mod' ) + labels.push(ut); + } + } + } + + if ( ! msg.style ) { + if ( msg.from === "jtv" ) + msg.style = "admin"; + else if ( msg.from === "twitchnotify" ) + msg.style = "notification"; + } + + if ( ! msg.cachedTokens || ! msg.cachedTokens.length ) + this.tokenize_chat_line(msg, true); + + if ( r.shouldShowMessage(msg) ) { + if ( messages.length < r.get("messageBufferSize") ) { + // One last thing! Make sure we don't have too many messages. + if ( msg.ffz_old_messages ) { + var max_msgs = r.get("messageBufferSize") - (messages.length + 1); + if ( msg.ffz_old_messages.length > max_msgs ) + msg.ffz_old_messages = msg.ffz_old_messages.slice(msg.ffz_old_messages.length - max_msgs); + } + + messages.unshiftObject(msg); + inserted += 1; + } else + break; + } + } + + if ( is_old ) { + var msg = { + ffz_alternate: ! alternation, + color: "#755000", + date: new Date(), + from: "frankerfacez_admin", + style: "admin", + message: "(Last message is " + utils.human_time(age) + " old.)", + room: room_id + }; + + this.tokenize_chat_line(msg); + if ( r.shouldShowMessage(msg) ) { + messages.insertAt(inserted, msg); + while( messages.length > r.get('messageBufferSize') ) + messages.removeAt(0); + } + } +} + + // -------------------- // Receiving Set Info // -------------------- @@ -270,6 +412,8 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( this.rooms[room_id] ) data.room = this.rooms[room_id].room; + data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false; + this.rooms[room_id] = data; if ( data.css || data.moderator_badge ) @@ -312,10 +456,53 @@ FFZ.prototype._modify_room = function(room) { } }, + clearMessages: function(user) { + var t = this; + if ( user ) { + this.get("messages").forEach(function(s, n) { + if ( s.from === user ) { + t.set("messages." + n + ".ffz_deleted", true); + if ( ! f.settings.prevent_clear ) + t.set("messages." + n + ".deleted", true); + } + }); + } else { + if ( f.settings.prevent_clear ) + this.addTmiMessage("A moderator's attempt to clear chat was ignored."); + else { + var msgs = t.get("messages"); + t.set("messages", []); + t.addMessage({ + style: 'admin', + message: i18n("Chat was cleared by a moderator"), + ffz_old_messages: msgs + }); + } + } + }, + + pushMessage: function(msg) { + if ( this.shouldShowMessage(msg) ) { + var t, s, n, a = this.get("messageBufferSize"); + for (this.get("messages").pushObject(msg), t = this.get("messages.length"), s = t - a, n = 0; s > n; n++) + this.get("messages").removeAt(0); + + "admin" === msg.style || ("whisper" === msg.style && ! this.ffz_whisper_room ) || this.incrementProperty("unreadCount", 1); + } + }, + addMessage: function(msg) { try { if ( msg ) { - msg.room = this.get('id'); + var is_whisper = msg.style === 'whisper'; + if ( f.settings.group_tabs && f.settings.whisper_room ) { + if ( ( is_whisper && ! this.ffz_whisper_room ) || ( ! is_whisper && this.ffz_whisper_room ) ) + return; + } + + if ( ! is_whisper ) + msg.room = this.get('id'); + f.tokenize_chat_line(msg); } } catch(err) { @@ -334,6 +521,9 @@ FFZ.prototype._modify_room = function(room) { }, send: function(text) { + if ( f.settings.group_tabs && f.settings.whisper_room && this.ffz_whisper_room ) + return; + try { var cmd = text.split(' ', 1)[0].toLowerCase(); if ( cmd === "/ffz" ) { diff --git a/src/main.js b/src/main.js index db59e240..da3065d5 100644 --- a/src/main.js +++ b/src/main.js @@ -156,13 +156,13 @@ FFZ.prototype.initialize = function(increment, delay) { // Twitch ember application is ready. // Check for special non-ember pages. - if ( /^\/(?:settings|m\/|messages?\/)/.test(location.pathname) ) { + if ( /^\/(?:$|user\/|p\/|settings|m\/|messages?\/)/.test(location.pathname) ) { this.setup_normal(delay); return; } // Check for the dashboard. - if ( /\/[A-Za-z_-]+\/dashboard/.test(location.pathname) && !/bookmarks$/.test(location.pathname) ) { + if ( /\/[^\/]+\/dashboard/.test(location.pathname) && !/bookmarks$/.test(location.pathname) ) { this.setup_dashboard(delay); return; } diff --git a/src/socket.js b/src/socket.js index c1646a5e..abefe8b1 100644 --- a/src/socket.js +++ b/src/socket.js @@ -56,8 +56,18 @@ FFZ.prototype.ws_create = function() { } // Send the current rooms. - for(var room_id in f.rooms) - f.rooms.hasOwnProperty(room_id) && f.ws_send("sub", room_id); + for(var room_id in f.rooms) { + if ( ! f.rooms.hasOwnProperty(room_id) || ! f.rooms[room_id] ) + continue; + + f.ws_send("sub", room_id); + + if ( f.rooms[room_id].needs_history ) { + f.rooms[room_id].needs_history = false; + if ( ! f.has_bttv && f.settings.chat_history ) + f.ws_send("chat_history", [room_id,25], f._load_history.bind(f, room_id)); + } + } // Send any pending commands. var pending = f._ws_pending; @@ -84,7 +94,7 @@ FFZ.prototype.ws_create = function() { // We never ever want to not have a socket. if ( f._ws_delay < 60000 ) - f._ws_delay += 5000; + f._ws_delay += (Math.floor(Math.random()*10) + 5) * 1000; else // Randomize delay. f._ws_delay = (Math.floor(Math.random()*60)+30)*1000; @@ -117,14 +127,17 @@ FFZ.prototype.ws_create = function() { } else { var success = cmd === 'True', - callback = f._ws_callbacks[request]; + has_callback = f._ws_callbacks.hasOwnProperty(request); - if ( ! success || ! callback ) + if ( ! has_callback ) f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data, false, true); - if ( callback ) { - delete f._ws_callbacks[request]; - callback(success, data); + else { + try { + f._ws_callbacks[request](success, data); + } catch(err) { + f.error("Callback for " + request + ": " + err); + } } } } diff --git a/src/tokenize.js b/src/tokenize.js index 90fca6ce..15567884 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -19,7 +19,7 @@ try { // Tokenization // --------------------- -FFZ.prototype.tokenize_chat_line = function(msgObject) { +FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { if ( msgObject.cachedTokens ) return msgObject.cachedTokens; @@ -53,14 +53,14 @@ FFZ.prototype.tokenize_chat_line = function(msgObject) { for(var i=0; i < tokens.length; i++) { var token = tokens[i]; - if ( _.isString(token) || ! token.mentionedUser || token.own ) + if ( _.isString(token) || ! token.mentionedUser || token.own || msgObject.style === 'whisper' ) continue; // We have a mention! msgObject.ffz_has_mention = true; // If we have chat tabs, update the status. - if ( ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { + if ( room_id && ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'); if ( el && ! el.classList.contains('active') ) el.classList.add('tab-mentioned'); @@ -69,7 +69,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject) { // Display notifications if that setting is enabled. Also make sure // that we have a chat view because showing a notification when we // can't actually go to it is a bad thing. - if ( this._chatv && this.settings.highlight_notifications && ! document.hasFocus() ) { + if ( this._chatv && this.settings.highlight_notifications && ! document.hasFocus() && ! prevent_notification ) { var room = this.rooms[room_id] && this.rooms[room_id].room, room_name; diff --git a/src/ui/menu.js b/src/ui/menu.js index 965a712d..ea52182e 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -82,8 +82,13 @@ FFZ.prototype.build_ui_popup = function(view) { continue; var page = FFZ.menu_pages[key]; - if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)()))) ) + try { + if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)(view)))) ) + continue; + } catch(err) { + this.error("menu_pages " + key + " visible: " + err); continue; + } menu_pages.push([page.sort_order || 0, key, page]); } diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js index 7ecf3cea..9a0968b0 100644 --- a/src/ui/my_emotes.js +++ b/src/ui/my_emotes.js @@ -27,28 +27,7 @@ var FFZ = window.FrankerFaceZ, "\\:-?(o|O)": ":-O", "\\>\\;\\(": ">(", "Gr(a|e)yFace": "GrayFace" - }, - - get_emotes = function(ffz) { - var Chat = App.__container__.lookup('controller:chat'), - room_id = Chat.get('currentRoom.id'), - room = ffz.rooms[room_id], - tmiSession = room ? room.room.tmiSession : null, - - set_ids = tmiSession && tmiSession._emotesParser && tmiSession._emotesParser.emoticonSetIds || "0", - user = ffz.get_user(), - user_sets = user && ffz.users[user.login] && ffz.users[user.login].sets || []; - - // Remove the 'default' set. - set_ids = set_ids.split(",").removeObject("0"); - - if ( ffz.settings.global_emotes_in_menu ) { - set_ids.push("0"); - user_sets = _.union(user_sets, ffz.default_sets); - } - - return [set_ids, user_sets]; - }; + }; // ------------------- @@ -92,9 +71,13 @@ FFZ.menu_pages.my_emotes = { name: "My Emoticons", icon: constants.EMOTE, - visible: function() { - var emotes = get_emotes(this); - return emotes[0].length > 0 || emotes[1].length > 0; + visible: function(view) { + var user = this.get_user(), + tmi = view.get('controller.currentRoom.tmiSession'), + ffz_sets = user && this.users[user.login] && this.users[user.login].sets || [], + twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets']; + + return ffz_sets.length || (twitch_sets && Object.keys(twitch_sets).length); }, render: function(view, container) { @@ -109,7 +92,40 @@ FFZ.menu_pages.my_emotes = { if ( ! needed_sets.length ) return FFZ.menu_pages.my_emotes.draw_menu.bind(this)(view, container, twitch_sets); - container.innerHTML = JSON.stringify(needed_sets); + var f = this, + fail = function() { + if ( ! needed_sets.length ) + return; + + needed_sets = []; + var ts = {}; + for(var set_id in twitch_sets) + if ( f._twitch_set_to_channel[set_id] ) + ts[set_id] = twitch_sets[set_id]; + + return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, ts); + }; + + this.ws_send("twitch_sets", needed_sets, function(success, data) { + if ( ! needed_sets.length ) + return; + + needed_sets = []; + if ( success ) { + for(var set_id in data) { + if ( ! data.hasOwnProperty(set_id) ) + continue; + + f._twitch_set_to_channel[set_id] = data[set_id]; + } + + localStorage.ffzTwitchSets = JSON.stringify(f._twitch_set_to_channel); + return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, twitch_sets); + } else + fail(); + }); + + setTimeout(fail, 2000); }, draw_twitch_set: function(view, set_id, set) { @@ -138,6 +154,7 @@ FFZ.menu_pages.my_emotes = { .done(function(data) { if ( data.subscriber && data.subscriber.image ) { f._twitch_badges[channel_id] = data.subscriber.image; + localStorage.ffzTwitchBadges = JSON.stringify(f._twitch_badges); heading.style.backgroundImage = 'url("' + data.subscriber.image + '")'; } }); @@ -220,7 +237,7 @@ FFZ.menu_pages.my_emotes = { if ( emote.width ) em.style.width = emote.width + "px"; - em.title = emote.tooltip || emote.name; + em.title = this._emote_tooltip(emote); em.addEventListener("click", this._add_emote.bind(this, view, emote.name)); menu.appendChild(em); } diff --git a/src/utils.js b/src/utils.js index 0f2ee76d..3932df5a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -5,6 +5,12 @@ var FFZ = window.FrankerFaceZ, var sanitize_cache = {}, sanitize_el = document.createElement('span'), + pluralize = function(value, singular, plural) { + plural = plural || 's'; + singular = singular || ''; + return value === 1 ? singular : plural; + }, + place_string = function(num) { if ( num == 1 ) return '1st'; else if ( num == 2 ) return '2nd'; @@ -52,7 +58,9 @@ var sanitize_cache = {}, if ( ! parts ) return null; - var unix = Date.UTC(parts[1], parts[2] - 1, parts[3], parts[4], parts[5], parts[6], parts[7] || 0); + parts[7] = (parts[7] && parts[7].length) ? parts[7].substr(0, 3) : 0; + + var unix = Date.UTC(parts[1], parts[2] - 1, parts[3], parts[4], parts[5], parts[6], parts[7]); // Check Offset if ( parts[9] ) { @@ -121,6 +129,34 @@ module.exports = { return date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate(); }, + pluralize: pluralize, + + human_time: function(elapsed) { + elapsed = Math.floor(elapsed); + + var years = Math.floor(elapsed / 31536000); + if ( years ) + return years + ' year' + pluralize(years); + + var days = Math.floor((elapsed %= 31536000) / 86400); + if ( days ) + return days + ' day' + pluralize(days); + + var hours = Math.floor((elapsed %= 86400) / 3600); + if ( hours ) + return hours + ' hour' + pluralize(hours); + + var minutes = Math.floor((elapsed %= 3600) / 60); + if ( minutes ) + return minutes + ' minute' + pluralize(minutes); + + var seconds = elapsed % 60; + if ( seconds ) + return seconds + ' second' + pluralize(seconds); + + return 'less than a second'; + }, + time_to_string: function(elapsed, separate_days, days_only) { var seconds = elapsed % 60, minutes = Math.floor(elapsed / 60), diff --git a/style.css b/style.css index f80da38e..650684c6 100644 --- a/style.css +++ b/style.css @@ -659,6 +659,26 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle /* Chat Rows */ +.ember-chat .chat-messages .chat-line.ffz-has-deleted { + line-height: 30px; +} + +.chat-line.ffz-deleted > span { + opacity: 0.15; +} + +.chat-line.ffz-deleted > span.message { + text-decoration: line-through; +} + +.chat-line.ffz-deleted:hover > span { + opacity: 0.5; +} + +.chat-line.ffz-deleted:hover > span.message { + text-decoration: none; +} + .ffz-chat-background .more-messages-indicator { /* This looks better when it's full width. */ margin: 0 -20px; @@ -707,6 +727,31 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle background-color: rgba(255,0,0, 0.3); } + +/* The New Whispers */ + + +.ffz-chat-background .app-main.theatre .ember-chat .chat-messages .whisper-line.whisper-incoming.ffz-alternate, +.ffz-chat-background .chat-container.dark .ember-chat .chat-messages .whisper-line.whisper-incoming.ffz-alternate, +.ffz-chat-background .ember-chat-container.dark .ember-chat .chat-messages .whisper-line.whisper-incoming.ffz-alternate { + background-color: #131317; +} + + + +/* Temporary Fix */ + +.chat-container.dark .ember-chat .chat-messages .whisper-line.whisper-incoming, +.app-main.theatre .chat-container .ember-chat .chat-messages .whisper-line.whisper-incoming, +.chat-container.force-dark .ember-chat .chat-messages .whisper-line.whisper-incoming, +.ember-chat-container.dark .ember-chat .chat-messages .whisper-line.whisper-incoming, +.app-main.theatre .ember-chat-container.chat-container .ember-chat .chat-messages .whisper-line.whisper-incoming, +.ember-chat-container.force-dark .ember-chat .chat-messages .whisper-line.whisper-incoming { + background-color: #101014; + border-left: 2px solid #a68ed2 +} + + /* Emoticon Tooltips */ .tipsy .tipsy-inner { @@ -782,6 +827,13 @@ body:not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle /* Dumb Fixes */ +.chat-container.dark, .app-main.theatre .chat-container, +.chat-container.force-dark, .ember-chat-container.dark, +.app-main.theatre .ember-chat-container.chat-container, +.ember-chat-container.force-dark { + box-shadow: none; +} + .notification-controls .notify-menu { bottom: 40px; } @@ -800,7 +852,7 @@ a.unsafe-link { /* Group Tabs */ #ffz-group-tabs { - padding: 10px 0 6px; + padding: 10px 10px 6px; box-shadow: inset 0 -1px 0 0 rgba(0,0,0,0.2); }