diff --git a/script.js b/script.js index 5210f324..dcdc0a34 100644 --- a/script.js +++ b/script.js @@ -35,6 +35,62 @@ var badge_css = function(badge) { // Render Badge // -------------------- +FFZ.prototype.bttv_badges = function(data) { + var user_id = data.sender, + user = this.users[user_id], + badges_out = [], + insert_at = -1; + + if ( ! user || ! user.badges ) + return; + + // Determine where in the list to insert these badges. + for(var i=0; i < data.badges.length; i++) { + var badge = data.badges[i]; + if ( badge.type == "subscriber" || badge.type == "turbo" ) { + insert_at = i; + break; + } + } + + + for (var slot in user.badges) { + if ( ! user.badges.hasOwnProperty(slot) ) + continue; + + var badge = user.badges[slot], + full_badge = this.badges[badge.id] || {}, + desc = badge.title || full_badge.title, + style = "", + alpha = BetterTTV.settings.get('alphaTags'); + + if ( badge.image ) + style += 'background-image: url(\\"' + badge.image + '\\"); '; + + if ( badge.color && ! alpha ) + style += 'background-color: ' + badge.color + '; '; + + if ( badge.extra_css ) + style += badge.extra_css; + + if ( style ) + desc += '" style="' + style; + + badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: "ffz-badge-" + badge.id + (alpha ? " alpha" : ""), name: "", description: desc}]); + } + + badges_out.sort(function(a,b){return a[0] - b[0]}); + + if ( insert_at == -1 ) { + while(badges_out.length) + data.badges.push(badges_out.shift()[1]); + } else { + while(badges_out.length) + data.badges.insertAt(insert_at, badges_out.shift()[1]); + } +} + + FFZ.prototype.render_badge = function(view) { var user = view.get('context.model.from'), room_id = view.get('context.parentController.content.id'), @@ -134,9 +190,142 @@ FFZ.prototype._legacy_parse_donors = function(data) { this.log("Added donor badge to " + utils.number_commas(count) + " users."); } -},{"./constants":2,"./utils":16}],2:[function(require,module,exports){ +},{"./constants":3,"./utils":17}],2:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.find_bttv = function(increment, delay) { + this.has_bttv = false; + if ( window.BTTVLOADED ) + return this.setup_bttv(); + + if ( delay >= 60000 ) + this.log("BetterTTV was not detected after 60 seconds."); + else + setTimeout(this.find_bttv.bind(this, increment, (delay||0) + increment), + increment); +} + + +FFZ.prototype.setup_bttv = function() { + this.log("BetterTTV was detected. Hooking."); + this.has_bttv = true; + + + // Send Message Behavior + var original_send = BetterTTV.chat.helpers.sendMessage, f = this; + BetterTTV.chat.helpers.sendMessage = function(message) { + var cmd = message.split(' ', 1)[0].toLowerCase(); + + if ( cmd === "/ffz" ) + f.run_command(message.substr(5), BetterTTV.chat.store.currentRoom); + else + return original_send(message); + } + + + // Ugly Hack for Current Room + var original_handler = BetterTTV.chat.handlers.privmsg, + received_room; + BetterTTV.chat.handlers.privmsg = function(room, data) { + received_room = room; + var output = original_handler(room, data); + received_room = null; + return output; + } + + + // Message Display Behavior + var original_privmsg = BetterTTV.chat.templates.privmsg; + BetterTTV.chat.templates.privmsg = function(highlight, action, server, isMod, data) { + // Handle badges. + f.bttv_badges(data); + + var output = original_privmsg(highlight, action, server, isMod, data); + return output.replace(SENDER_REGEX, '$1 data-room="' + received_room + '"'); + } + + + // Ugly Hack for Current Sender + var original_template = BetterTTV.chat.templates.message, + received_sender; + BetterTTV.chat.templates.message = function(sender, message, emotes, colored) { + received_sender = sender; + var output = original_template(sender, message, emotes, colored); + received_sender = null; + return output; + } + + + // Emoticonize + var original_emoticonize = BetterTTV.chat.templates.emoticonize; + BetterTTV.chat.templates.emoticonize = function(message, emotes) { + var tokens = original_emoticonize(message, emotes), + user = f.users[received_sender], + room = f.rooms[received_room]; + + // Get our sets. + var sets = _.union(user && user.sets || [], room && room.sets || [], f.global_sets), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emotes, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Why is emote parsing so bad? ;_; + _.each(emotes, function(emote) { + var eo = ['' + emote.title + ''], + old_tokens = tokens; + + tokens = []; + + if ( ! old_tokens || ! old_tokens.length ) + return tokens; + + for(var i=0; i < old_tokens.length; i++) { + var token = old_tokens[i]; + if ( typeof token != "string" ) { + tokens.push(token); + continue; + } + + var tbits = token.split(emote.regex); + tbits.forEach(function(val, ind) { + if ( val && val.length ) + tokens.push(val); + + if ( ind !== tbits.length - 1 ) + tokens.push(eo); + }); + } + }); + + return tokens; + } + + this.update_ui_link(); +} +},{}],3:[function(require,module,exports){ var SVGPATH = '', - DEBUG = localStorage.ffzDebugMode == "true"; + DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'); module.exports = { DEBUG: DEBUG, @@ -146,15 +335,15 @@ module.exports = { ZREKNARF: '' + SVGPATH + '', CHAT_BUTTON: '' + SVGPATH + '' } -},{}],3:[function(require,module,exports){ +},{}],4:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; -// -------------------- -// Debug Command -// -------------------- +// ----------------------- +// Developer Mode Command +// ----------------------- -FFZ.chat_commands.debug = function(room, args) { +FFZ.chat_commands.developer_mode = function(room, args) { var enabled, args = args && args.length ? args[0].toLowerCase() : null; if ( args == "y" || args == "yes" || args == "true" || args == "on" ) enabled = true; @@ -162,14 +351,15 @@ FFZ.chat_commands.debug = function(room, args) { enabled = false; if ( enabled === undefined ) - enabled = !(localStorage.ffzDebugMode == "true"); + return "Developer Mode is currently " + (localStorage.ffzDebugMode == "true" ? "enabled." : "disabled."); localStorage.ffzDebugMode = enabled; - return "Debug Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; + return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; } -FFZ.chat_commands.debug.help = "Usage: /ffz debug [on|off]\nEnable or disable Debug Mode. When Debug Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."; -},{}],4:[function(require,module,exports){ +FFZ.chat_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(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -225,7 +415,7 @@ FFZ.prototype._modify_cview = function(view) { }) }); } -},{}],5:[function(require,module,exports){ +},{}],6:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -242,8 +432,6 @@ FFZ.prototype.setup_line = function() { Line.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. - var tokens = f._emoticonize(this, this._super()); - f.log("Chat Tokens", tokens); return f._emoticonize(this, this._super()); }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") @@ -258,16 +446,45 @@ FFZ.prototype.setup_line = function() { didInsertElement: function() { this._super(); - var el = this.get('element'); + var el = this.get('element'), + user = this.get('context.model.from'); + el.setAttribute('data-room', this.get('context.parentController.content.id')); - el.setAttribute('data-sender', this.get('context.model.from')); + el.setAttribute('data-sender', user); f.render_badge(this); + f.capitalize(this, user); + } }); } +// --------------------- +// Capitalization +// --------------------- + +FFZ.capitalization = {}; + +FFZ.prototype.capitalize = function(view, user) { + if ( FFZ.capitalization[user] ) + return view.$('.from').text(FFZ.capitalization[user]); + + var f = this; + jQuery.getJSON("https://api.twitch.tv/kraken/channels/" + user + "?callback=?") + .always(function(data) { + if ( data.display_name == undefined ) + FFZ.capitalization[user] = user; + else + FFZ.capitalization[user] = data.display_name; + + f.capitalize(view, user); + }); +} + + + + // --------------------- // Emoticon Replacement // --------------------- @@ -311,7 +528,7 @@ FFZ.prototype._emoticonize = function(controller, tokens) { // emoticon. _.each(emotes, function(emote) { //var eo = {isEmoticon:true, cls: emote.klass}; - var eo = {emoticonSrc: emote.url, altText: emote.name}; + var eo = {isEmoticon:true, cls: emote.klass, emoticonSrc: emote.url, altText: emote.name}; tokens = _.compact(_.flatten(_.map(tokens, function(token) { if ( _.isObject(token) ) @@ -329,7 +546,7 @@ FFZ.prototype._emoticonize = function(controller, tokens) { return tokens; } -},{}],6:[function(require,module,exports){ +},{}],7:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/, @@ -386,8 +603,14 @@ FFZ.chat_commands = {}; FFZ.prototype.room_message = function(room, text) { var lines = text.split("\n"); - for(var i=0; i < lines.length; i++) - room.room.addMessage({style: 'ffz admin', from: 'FFZ', message: lines[i]}); + if ( this.has_bttv ) { + for(var i=0; i < lines.length; i++) + BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); + + } else { + for(var i=0; i < lines.length; i++) + room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); + } } @@ -524,22 +747,6 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { } -/*FFZ.ws_commands.sets_for_room = function(data) { - var room = this.rooms[data.room]; - if ( ! room ) - return; - - for(var i=0; i < data.sets.length; i++) { - var set = data.sets[i]; - if ( room.sets.contains(set) ) - continue; - - room.sets.push(set); - this.load_set(set); - } -}*/ - - // -------------------- // Ember Modifications // -------------------- @@ -615,7 +822,7 @@ FFZ.prototype._legacy_load_room_css = function(room_id, callback, data) { output.css = data || null; return this._load_room_json(room_id, callback, output); } -},{"../constants":2,"../utils":16}],7:[function(require,module,exports){ +},{"../constants":3,"../utils":17}],8:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, constants = require('./constants'), @@ -698,13 +905,15 @@ var build_legacy_css = function(emote) { return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; } -var build_css = function(emote) { +var build_new_css = function(emote) { if ( ! emote.margins && ! emote.extra_css ) - return ""; + return build_legacy_css(emote); - return 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; + return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; } +var build_css = build_new_css; + FFZ.prototype._load_set_json = function(set_id, callback, data) { @@ -712,6 +921,7 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { this.emote_sets[set_id] = data; data.users = []; data.global = false; + data.count = 0; // Iterate through all the emoticons, building CSS and regex objects as appropriate. var output_css = ""; @@ -729,10 +939,12 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); output_css += build_css(emote); + data.count++; } utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); this.log("Updated emoticons for set: " + set_id, data); + this.update_ui_link(); if ( callback ) callback(true, data); @@ -774,7 +986,7 @@ FFZ.prototype._legacy_load_css = function(set_id, callback, data) { this._load_set_json(set_id, callback, output); } -},{"./constants":2,"./utils":16}],8:[function(require,module,exports){ +},{"./constants":3,"./utils":17}],9:[function(require,module,exports){ // Modify Array and others. require('./shims'); @@ -844,6 +1056,8 @@ require('./ember/chatview'); require('./debug'); +require('./betterttv'); + require('./ui/styles'); require('./ui/notifications'); require('./ui/viewer_count'); @@ -851,6 +1065,7 @@ require('./ui/viewer_count'); require('./ui/menu_button'); require('./ui/menu'); + // --------------- // Initialization // --------------- @@ -896,6 +1111,9 @@ FFZ.prototype.setup = function(delay) { this.setup_css(); this.setup_menu(); + this.find_bttv(10); + + } catch(err) { this.log("An error occurred while starting FrankerFaceZ: " + err); return; @@ -903,7 +1121,7 @@ FFZ.prototype.setup = function(delay) { this.log("Initialization complete."); } -},{"./badges":1,"./debug":3,"./ember/chatview":4,"./ember/line":5,"./ember/room":6,"./emoticons":7,"./shims":9,"./socket":10,"./ui/menu":11,"./ui/menu_button":12,"./ui/notifications":13,"./ui/styles":14,"./ui/viewer_count":15}],9:[function(require,module,exports){ +},{"./badges":1,"./betterttv":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/room":7,"./emoticons":8,"./shims":10,"./socket":11,"./ui/menu":12,"./ui/menu_button":13,"./ui/notifications":14,"./ui/styles":15,"./ui/viewer_count":16}],10:[function(require,module,exports){ Array.prototype.equals = function (array) { // if the other array is a falsy value, return if (!array) @@ -929,7 +1147,7 @@ Array.prototype.equals = function (array) { } -},{}],10:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; FFZ.prototype._ws_open = false; @@ -1023,7 +1241,7 @@ FFZ.prototype.ws_send = function(func, data, callback) { this._ws_sock.send(request + " " + func + data); return request; } -},{}],11:[function(require,module,exports){ +},{}],12:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -1126,15 +1344,20 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { for(var i=0; i < sets.length; i++) { var set = this.emote_sets[sets[i]]; + if ( ! set || ! set.emotes ) + continue; + for(var eid in set.emotes) { var emote = set.emotes[eid]; if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) continue; c++; - var s = document.createElement('img'); - s.src = emote.url; - //s.className = 'emoticon ' + emote.klass + ' tooltip'; + var s = document.createElement('span'); + s.className = 'emoticon tooltip'; + s.style.backgroundImage = 'url("' + emote.url + '")'; + s.style.width = emote.width + "px"; + s.style.height = emote.height + "px"; s.title = emote.name; s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); grid.appendChild(s); @@ -1159,7 +1382,7 @@ FFZ.prototype._add_emote = function(view, emote) { room.set('messageToSend', current_text + (emote.name || emote)); } -},{}],12:[function(require,module,exports){ +},{}],13:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -1168,8 +1391,6 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.build_ui_link = function(view) { - // TODO: Detect dark mode from BTTV. - var link = document.createElement('a'); link.className = 'ffz-ui-toggle'; link.innerHTML = constants.CHAT_BUTTON; @@ -1184,18 +1405,32 @@ FFZ.prototype.build_ui_link = function(view) { FFZ.prototype.update_ui_link = function(link) { var controller = App.__container__.lookup('controller:chat'); link = link || document.querySelector('a.ffz-ui-toggle'); - if ( !link || !controller ) return; + if ( !link || !controller ) return this.log("No button."); var room_id = controller.get('currentRoom.id'), room = this.rooms[room_id], - has_emotes = room && room.sets.length > 0; + has_emotes = false, - if ( has_emotes ) - link.classList.remove('no-emotes'); - else - link.classList.add('no-emotes'); + dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), + blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false); + + + // Check for emoticons. + if ( room && room.sets.length ) { + for(var i=0; i < room.sets.length; i++) { + var set = this.emote_sets[room.sets[i]]; + if ( set && set.count > 0 ) { + has_emotes = true; + break; + } + } + } + + link.classList.toggle('no-emotes', ! has_emotes); + link.classList.toggle('dark', dark); + link.classList.toggle('blue', blue); } -},{"../constants":2}],13:[function(require,module,exports){ +},{"../constants":3}],14:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; FFZ.prototype.show_notification = function(message) { @@ -1211,7 +1446,7 @@ FFZ.prototype.show_notification = function(message) { FFZ.ws_commands.message = function(message) { this.show_notification(message); } -},{}],14:[function(require,module,exports){ +},{}],15:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -1236,7 +1471,7 @@ FFZ.prototype.setup_css = function() { } }; } -},{"../constants":2}],15:[function(require,module,exports){ +},{"../constants":3}],16:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'); @@ -1273,7 +1508,7 @@ FFZ.ws_commands.viewers = function(data) { jQuery(view_count).tipsy(); } } -},{"../constants":2,"../utils":16}],16:[function(require,module,exports){ +},{"../constants":3,"../utils":17}],17:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); @@ -1304,4 +1539,4 @@ module.exports = { return parts.join("."); } } -},{"./constants":2}]},{},[8]);window.ffz = new FrankerFaceZ()}(window)); \ No newline at end of file +},{"./constants":3}]},{},[9]);window.ffz = new FrankerFaceZ()}(window)); \ No newline at end of file diff --git a/script.min.js b/script.min.js index 308d2138..f73cb984 100644 --- a/script.min.js +++ b/script.min.js @@ -1 +1 @@ -!function(e){!function t(e,o,s){function n(i,a){if(!o[i]){if(!e[i]){var c="function"==typeof require&&require;if(!a&&c)return c(i,!0);if(r)return r(i,!0);throw new Error("Cannot find module '"+i+"'")}var l=o[i]={exports:{}};e[i][0].call(l.exports,function(t){var o=e[i][1][t];return n(o?o:t)},l,l.exports,t,e,o,s)}return o[i].exports}for(var r="function"==typeof require&&require,i=0;ie?this._legacy_add_donors(e):void 0):void 0})},o.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var o=e.trim().split(/\W+/),s=0;s'+o+"",CHAT_BUTTON:''+o+""}},{}],3:[function(){var t=e.FrankerFaceZ;t.chat_commands.debug=function(e,t){var o,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?o=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(o=!1),void 0===o&&(o=!("true"==localStorage.ffzDebugMode)),localStorage.ffzDebugMode=o,"Debug Mode is now "+(o?"enabled":"disabled")+". Please refresh your browser."},t.chat_commands.debug.help="Usage: /ffz debug [on|off]\nEnable or disable Debug Mode. When Debug Mode is enabled, the script will be reloaded from //localhost:8000/script.js instead of from the CDN."},{}],4:[function(){var t=e.FrankerFaceZ;t.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e),e.create().destroy();for(var t in Ember.View.views)if(Ember.View.views.hasOwnProperty(t)){var o=Ember.View.views[t];o instanceof e&&(this.log("Adding UI link manually to Chat view.",o),o.$(".textarea-contain").append(this.build_ui_link(o)))}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super(),this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))},willClearRender:function(){this._super(),this.$(".ffz-ui-toggle").remove()},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){t.update_ui_link()})})}},{}],5:[function(){var t=e.FrankerFaceZ;t.prototype.setup_line=function(){this.log("Hooking the Ember Line controller.");var e=App.__container__.resolve("controller:line"),t=this;e.reopen({tokenizedMessage:function(){var e=t._emoticonize(this,this._super());return t.log("Chat Tokens",e),t._emoticonize(this,this._super())}.property("model.message","isModeratorOrHigher","controllers.emoticons.emoticons.[]")}),this.log("Hooking the Ember Line view.");var e=App.__container__.resolve("view:line");e.reopen({didInsertElement:function(){this._super();var e=this.get("element");e.setAttribute("data-room",this.get("context.parentController.content.id")),e.setAttribute("data-sender",this.get("context.model.from")),t.render_badge(this)}})},t.prototype._emoticonize=function(e,t){var o=e.get("parentController.model.id"),s=e.get("model.from"),n=this.users[s],r=this.rooms[o],i=this,a=_.union(n&&n.sets||[],r&&r.sets||[],i.global_sets),c=[];return _.each(a,function(e){var o=i.emote_sets[e];o&&_.each(o.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&c.push(e)})}),c.length?("string"==typeof t&&(t=[t]),_.each(c,function(e){var o={emoticonSrc:e.url,altText:e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var s=t.split(e.regex),n=[];return s.forEach(function(e,t){n.push(e),t!==s.length-1&&n.push(o)}),n})))}),t):t}},{}],6:[function(t){var o=e.FrankerFaceZ,s=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,n=/[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,r=/^_([^_]+)_\d+$/,i=t("../constants"),a=t("../utils"),c=function(e){return e.moderator_badge?'.chat-line[data-room="'+e.id+'"] .badges .moderator { background-image:url("'+e.moderator_badge+'") !important; }':""};o.prototype.setup_room=function(){this.rooms={},this.log("Creating room style element.");var e=this._room_style=document.createElement("style");e.id="ffz-room-css",document.head.appendChild(e),this.log("Hooking the Ember Room model.");var t=App.__container__.resolve("model:room");this._modify_room(t);var o=t.instances;for(var s in o)if(o.hasOwnProperty(s)){var n=o[s];this.add_room(n.id,n),this._modify_room(n)}},o.chat_commands={},o.prototype.room_message=function(e,t){for(var o=t.split("\n"),s=0;so?this._legacy_add_room(e,t,o):void 0)})},o.prototype._legacy_load_room_css=function(e,t,o){var i=e,a=i.match(r);a&&a[1]&&(i=a[1]);var c={id:e,menu_sets:[i],sets:[i],moderator_badge:null,css:null};return o&&(o=o.replace(s,"").trim()),o&&(o=o.replace(n,function(e,t){return c.moderator_badge||"modicon.png"!==t.substr(-11)?e:(c.moderator_badge=t,"")})),c.css=o||null,this._load_room_json(e,t,c)}},{"../constants":2,"../utils":16}],7:[function(t){var o=e.FrankerFaceZ,s=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,n=t("./constants"),r=t("./utils"),i=function(e,t,o){t&&(o.global=!0,this.global_sets.push(e))},a=function(e,t){var o=e.split(/ +/);return 2!=o.length?e:(o[0]=parseFloat(o[0]),o[1]=parseFloat(o[1]),o[0]==(t-18)/-2&&0==o[1]?null:e)};o.prototype.setup_emoticons=function(){this.log("Preparing emoticon system."),this.emote_sets={},this.global_sets=[],this._last_emote_id=0,this.log("Creating emoticon style element.");var e=this._emote_style=document.createElement("style");e.id="ffz-emoticon-css",document.head.appendChild(e),this.log("Loading global emote set."),this.load_set("global",i.bind(this,"global"))},o.ws_commands.reload_set=function(e){this.load_set(e)},o.prototype.load_set=function(e,t){return this._legacy_load_set(e,t)},o.prototype.unload_set=function(e){var t=this.emote_sets[e];if(t){this.log("Unloading emoticons for set: "+e),r.update_css(this._emote_style,e,null),delete this.emote_sets[e];for(var o=0;oo?this._legacy_load_set(e,t,o):t&&t(!1))})},o.prototype._legacy_load_css=function(e,t,o){var n={},r={id:e,emotes:n,extra_css:null},i=this;o.replace(s,function(e,t,o,s,r,c,l,d){r=parseInt(r),c=parseInt(c),l=a(l,r);var u="."===s.substr(s.lastIndexOf("/")+1,1),m=++i._last_emote_id,h={id:m,hidden:u,name:o,height:r,width:c,url:s,margins:l,extra_css:d};return n[m]=h,""}),this._load_set_json(e,t,r)}},{"./constants":2,"./utils":16}],8:[function(t){t("./shims");var o=e.FrankerFaceZ=function(){o.instance=this,this.initialize()};o.get=function(){return o.instance};var s=o.version_info={major:3,minor:0,revision:0,toString:function(){return[s.major,s.minor,s.revision].join(".")+(s.extra||"")}};o.prototype.log=function(e,t,o){e="FFZ: "+e+(o?" -- "+JSON.stringify(t):""),void 0!==t&&console.groupCollapsed&&console.dir?(console.groupCollapsed(e),console.dir(t),console.groupEnd(e)):console.log(e)},o.prototype.get_user=function(){if(e.PP&&PP.login)return PP;if(e.App){var t=App.__container__.lookup("controller:navigation");return t?t.get("userData"):void 0}},t("./socket"),t("./emoticons"),t("./badges"),t("./ember/room"),t("./ember/line"),t("./ember/chatview"),t("./debug"),t("./ui/styles"),t("./ui/notifications"),t("./ui/viewer_count"),t("./ui/menu_button"),t("./ui/menu"),o.prototype.initialize=function(t,o){var s=void 0!=e.App&&void 0!=App.__container__&&void 0!=App.__container__.resolve("model:room");return s?void this.setup(o):(t=t||10,void(o>=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(o||0)+t),t)))},o.prototype.setup=function(e){this.log("Found Twitch application after "+(e||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+o.version_info),this.users={};try{this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_css(),this.setup_menu()}catch(t){return void this.log("An error occurred while starting FrankerFaceZ: "+t)}this.log("Initialization complete.")}},{"./badges":1,"./debug":3,"./ember/chatview":4,"./ember/line":5,"./ember/room":6,"./emoticons":7,"./shims":9,"./socket":10,"./ui/menu":11,"./ui/menu_button":12,"./ui/notifications":13,"./ui/styles":14,"./ui/viewer_count":15}],9:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,o=this.length;o>t;t++)if(this[t]instanceof Array&&e[t]instanceof Array){if(!this[t].equals(e[t]))return!1}else if(this[t]!=e[t])return!1;return!0}},{}],10:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.prototype.ws_create=function(){var e=this;this._ws_last_req=0,this._ws_callbacks={};var o=this._ws_sock=new WebSocket("ws://ffz.stendec.me/");o.onopen=function(){e._ws_open=!0,e._ws_delay=0,e.log("Socket connected.");var t=e.get_user();t&&e.ws_send("setuser",t.login);for(var o in e.rooms)e.ws_send("sub",o)},o.onclose=function(){e.log("Socket closed."),e._ws_open=!1,e._ws_delay<3e4&&(e._ws_delay+=5e3),setTimeout(e.ws_create.bind(e),e._ws_delay)},o.onmessage=function(o){var s,n,r=o.data.indexOf(" "),i=o.data.substr(r+1),a=parseInt(o.data.slice(0,r));if(r=i.indexOf(" "),-1===r&&(r=i.length),s=i.slice(0,r),i=i.substr(r+1),i&&(n=JSON.parse(i)),-1===a){var c=t.ws_commands[s];c?c.bind(e)(n):e.log("Invalid command: "+s,n)}else{var l="True"===s,d=e._ws_callbacks[a];e.log("Socket Reply to "+a+" - "+(l?"SUCCESS":"FAIL"),n),d&&(delete e._ws_callbacks[a],d(l,n))}}},t.prototype.ws_send=function(e,t,o){if(!this._ws_open)return!1;var s=++this._ws_last_req;return t=void 0!==t?" "+JSON.stringify(t):"",o&&(this._ws_callbacks[s]=o),this._ws_sock.send(s+" "+e+t),s}},{}],11:[function(){var t=e.FrankerFaceZ;t.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var o,s=e._popup;s&&(s=jQuery(s),o=s.parent(),o.is(t.target)||0!==o.has(t.target).length||(s.remove(),delete e._popup))})},t.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),void delete this._popup;var o=document.createElement("div"),s=document.createElement("div");o.className="emoticon-selector chat-menu ffz-ui-popup",s.className="emoticon-selector-box dropmenu",o.appendChild(s);var n=e.get("controller.currentRoom.id"),r=this.rooms[n];this.log("Menu for Room: "+n,r);var i=document.createElement("a");i.className="button glyph-only ffz-button",i.title="Advertise for FrankerFaceZ in chat!",i.href="#",i.innerHTML='';var a=document.createElement("div");a.className="list-header first",a.appendChild(i),a.appendChild(document.createTextNode("FrankerFaceZ")),s.appendChild(a);var c=this._emotes_for_sets(s,e,r&&r.menu_sets||[]);0===c?i.addEventListener("click",this._add_emote.bind(this,e,"To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com")):i.addEventListener("click",this._add_emote.bind(this,e,"To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com")),this._popup=o,s.style.maxHeight=Math.max(300,e.$().height()-171)+"px",e.$(".chat-interface").append(o)},t.prototype._emotes_for_sets=function(e,t,o,s,n){if(null!=s){var r=document.createElement("div");r.className="list-header",r.appendChild(document.createTextNode(s)),n&&r.appendChild(n),e.appendChild(r)}var i=document.createElement("div"),a=0;i.className="emoticon-grid";for(var c=0;c0;n?e.classList.remove("no-emotes"):e.classList.add("no-emotes")}}},{"../constants":2}],13:[function(){var t=e.FrankerFaceZ;t.prototype.show_notification=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()},t.ws_commands.message=function(e){this.show_notification(e)}},{}],14:[function(t){var o=e.FrankerFaceZ,s=t("../constants");o.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"),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":2}],15:[function(t){var o=e.FrankerFaceZ,s=t("../constants"),n=t("../utils");o.ws_commands.viewers=function(e){var t=e[0],o=e[1],r=App.__container__.lookup("controller:channel"),i=r&&r.get&&r.get("id");if(i===t){var a=document.querySelector(".channel-stats .ffz.stat"),c=s.ZREKNARF+" "+n.number_commas(o);if(a)a.innerHTML=c;else{var l=document.querySelector(".channel-stats");if(!l)return;a=document.createElement("span"),a.className="ffz stat",a.title="Viewers with FrankerFaceZ",a.innerHTML=c,l.appendChild(a),jQuery(a).tipsy()}}}},{"../constants":2,"../utils":16}],16:[function(t,o){e.FrankerFaceZ,t("./constants");o.exports={update_css:function(e,t,o){var s=e.innerHTML,n="/*BEGIN "+t+"*/",r="/*END "+t+"*/",i=s.indexOf(n),a=s.indexOf(r),c=-1!==i&&-1!==a&&a>i;(c||o)&&(c&&(s=s.substr(0,i)+s.substr(a+r.length)),o&&(s+=n+o+r),e.innerHTML=s)},number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")}}},{"./constants":2}]},{},[8]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file +!function(e){!function t(e,o,s){function n(i,a){if(!o[i]){if(!e[i]){var c="function"==typeof require&&require;if(!a&&c)return c(i,!0);if(r)return r(i,!0);throw new Error("Cannot find module '"+i+"'")}var l=o[i]={exports:{}};e[i][0].call(l.exports,function(t){var o=e[i][1][t];return n(o?o:t)},l,l.exports,t,e,o,s)}return o[i].exports}for(var r="function"==typeof require&&require,i=0;ie?this._legacy_add_donors(e):void 0):void 0})},o.prototype._legacy_parse_donors=function(e){var t=0;if(null!=e)for(var o=e.trim().split(/\W+/),s=0;s))/;t.prototype.find_bttv=function(t,o){return this.has_bttv=!1,e.BTTVLOADED?this.setup_bttv():void(o>=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(o||0)+t),t))},t.prototype.setup_bttv=function(){this.log("BetterTTV was detected. Hooking."),this.has_bttv=!0;var e=BetterTTV.chat.helpers.sendMessage,t=this;BetterTTV.chat.helpers.sendMessage=function(o){var s=o.split(" ",1)[0].toLowerCase();return"/ffz"!==s?e(o):void t.run_command(o.substr(5),BetterTTV.chat.store.currentRoom)};var s,n=BetterTTV.chat.handlers.privmsg;BetterTTV.chat.handlers.privmsg=function(e,t){s=e;var o=n(e,t);return s=null,o};var r=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,n,i,a,c){t.bttv_badges(c);var l=r(e,n,i,a,c);return l.replace(o,'$1 data-room="'+s+'"')};var i,a=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,o,s){i=e;var n=a(e,t,o,s);return i=null,n};var c=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,o){var n=c(e,o),r=t.users[i],a=t.rooms[s],l=_.union(r&&r.sets||[],a&&a.sets||[],t.global_sets),o=[];return _.each(l,function(e){var s=t.emote_sets[e];s&&_.each(s.emotes,function(e){_.any(n,function(t){return _.isString(t)&&t.match(e.regex)})&&o.push(e)})}),o.length?(_.each(o,function(e){var t=[''+e.title+''],o=n;if(n=[],!o||!o.length)return n;for(var s=0;s'+o+"",CHAT_BUTTON:''+o+""}},{}],4:[function(){var t=e.FrankerFaceZ;t.chat_commands.developer_mode=function(e,t){var o,t=t&&t.length?t[0].toLowerCase():null;return"y"==t||"yes"==t||"true"==t||"on"==t?o=!0:("n"==t||"no"==t||"false"==t||"off"==t)&&(o=!1),void 0===o?"Developer Mode is currently "+("true"==localStorage.ffzDebugMode?"enabled.":"disabled."):(localStorage.ffzDebugMode=o,"Developer Mode is now "+(o?"enabled":"disabled")+". Please refresh your browser.")},t.chat_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(){var t=e.FrankerFaceZ;t.prototype.setup_chatview=function(){this.log("Hooking the Ember Chat view.");var e=App.__container__.resolve("view:chat");this._modify_cview(e),e.create().destroy();for(var t in Ember.View.views)if(Ember.View.views.hasOwnProperty(t)){var o=Ember.View.views[t];o instanceof e&&(this.log("Adding UI link manually to Chat view.",o),o.$(".textarea-contain").append(this.build_ui_link(o)))}},t.prototype._modify_cview=function(e){var t=this;e.reopen({didInsertElement:function(){this._super(),this.$()&&this.$(".textarea-contain").append(t.build_ui_link(this))},willClearRender:function(){this._super(),this.$(".ffz-ui-toggle").remove()},ffzUpdateLink:Ember.observer("controller.currentRoom",function(){t.update_ui_link()})})}},{}],6:[function(){var t=e.FrankerFaceZ;t.prototype.setup_line=function(){this.log("Hooking the Ember Line controller.");var e=App.__container__.resolve("controller:line"),t=this;e.reopen({tokenizedMessage:function(){return t._emoticonize(this,this._super())}.property("model.message","isModeratorOrHigher","controllers.emoticons.emoticons.[]")}),this.log("Hooking the Ember Line view.");var e=App.__container__.resolve("view:line");e.reopen({didInsertElement:function(){this._super();var e=this.get("element"),o=this.get("context.model.from");e.setAttribute("data-room",this.get("context.parentController.content.id")),e.setAttribute("data-sender",o),t.render_badge(this),t.capitalize(this,o)}})},t.capitalization={},t.prototype.capitalize=function(e,o){if(t.capitalization[o])return e.$(".from").text(t.capitalization[o]);var s=this;jQuery.getJSON("https://api.twitch.tv/kraken/channels/"+o+"?callback=?").always(function(n){t.capitalization[o]=void 0==n.display_name?o:n.display_name,s.capitalize(e,o)})},t.prototype._emoticonize=function(e,t){var o=e.get("parentController.model.id"),s=e.get("model.from"),n=this.users[s],r=this.rooms[o],i=this,a=_.union(n&&n.sets||[],r&&r.sets||[],i.global_sets),c=[];return _.each(a,function(e){var o=i.emote_sets[e];o&&_.each(o.emotes,function(e){_.any(t,function(t){return _.isString(t)&&t.match(e.regex)})&&c.push(e)})}),c.length?("string"==typeof t&&(t=[t]),_.each(c,function(e){var o={isEmoticon:!0,cls:e.klass,emoticonSrc:e.url,altText:e.name};t=_.compact(_.flatten(_.map(t,function(t){if(_.isObject(t))return t;var s=t.split(e.regex),n=[];return s.forEach(function(e,t){n.push(e),t!==s.length-1&&n.push(o)}),n})))}),t):t}},{}],7:[function(t){var o=e.FrankerFaceZ,s=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,n=/[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,r=/^_([^_]+)_\d+$/,i=t("../constants"),a=t("../utils"),c=function(e){return e.moderator_badge?'.chat-line[data-room="'+e.id+'"] .badges .moderator { background-image:url("'+e.moderator_badge+'") !important; }':""};o.prototype.setup_room=function(){this.rooms={},this.log("Creating room style element.");var e=this._room_style=document.createElement("style");e.id="ffz-room-css",document.head.appendChild(e),this.log("Hooking the Ember Room model.");var t=App.__container__.resolve("model:room");this._modify_room(t);var o=t.instances;for(var s in o)if(o.hasOwnProperty(s)){var n=o[s];this.add_room(n.id,n),this._modify_room(n)}},o.chat_commands={},o.prototype.room_message=function(e,t){var o=t.split("\n");if(this.has_bttv)for(var s=0;so?this._legacy_add_room(e,t,o):void 0)})},o.prototype._legacy_load_room_css=function(e,t,o){var i=e,a=i.match(r);a&&a[1]&&(i=a[1]);var c={id:e,menu_sets:[i],sets:[i],moderator_badge:null,css:null};return o&&(o=o.replace(s,"").trim()),o&&(o=o.replace(n,function(e,t){return c.moderator_badge||"modicon.png"!==t.substr(-11)?e:(c.moderator_badge=t,"")})),c.css=o||null,this._load_room_json(e,t,c)}},{"../constants":3,"../utils":17}],8:[function(t){var o=e.FrankerFaceZ,s=/\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/gm,n=t("./constants"),r=t("./utils"),i=function(e,t,o){t&&(o.global=!0,this.global_sets.push(e))},a=function(e,t){var o=e.split(/ +/);return 2!=o.length?e:(o[0]=parseFloat(o[0]),o[1]=parseFloat(o[1]),o[0]==(t-18)/-2&&0==o[1]?null:e)};o.prototype.setup_emoticons=function(){this.log("Preparing emoticon system."),this.emote_sets={},this.global_sets=[],this._last_emote_id=0,this.log("Creating emoticon style element.");var e=this._emote_style=document.createElement("style");e.id="ffz-emoticon-css",document.head.appendChild(e),this.log("Loading global emote set."),this.load_set("global",i.bind(this,"global"))},o.ws_commands.reload_set=function(e){this.load_set(e)},o.prototype.load_set=function(e,t){return this._legacy_load_set(e,t)},o.prototype.unload_set=function(e){var t=this.emote_sets[e];if(t){this.log("Unloading emoticons for set: "+e),r.update_css(this._emote_style,e,null),delete this.emote_sets[e];for(var o=0;oo?this._legacy_load_set(e,t,o):t&&t(!1))})},o.prototype._legacy_load_css=function(e,t,o){var n={},r={id:e,emotes:n,extra_css:null},i=this;o.replace(s,function(e,t,o,s,r,c,l,d){r=parseInt(r),c=parseInt(c),l=a(l,r);var u="."===s.substr(s.lastIndexOf("/")+1,1),h=++i._last_emote_id,m={id:h,hidden:u,name:o,height:r,width:c,url:s,margins:l,extra_css:d};return n[h]=m,""}),this._load_set_json(e,t,r)}},{"./constants":3,"./utils":17}],9:[function(t){t("./shims");var o=e.FrankerFaceZ=function(){o.instance=this,this.initialize()};o.get=function(){return o.instance};var s=o.version_info={major:3,minor:0,revision:0,toString:function(){return[s.major,s.minor,s.revision].join(".")+(s.extra||"")}};o.prototype.log=function(e,t,o){e="FFZ: "+e+(o?" -- "+JSON.stringify(t):""),void 0!==t&&console.groupCollapsed&&console.dir?(console.groupCollapsed(e),console.dir(t),console.groupEnd(e)):console.log(e)},o.prototype.get_user=function(){if(e.PP&&PP.login)return PP;if(e.App){var t=App.__container__.lookup("controller:navigation");return t?t.get("userData"):void 0}},t("./socket"),t("./emoticons"),t("./badges"),t("./ember/room"),t("./ember/line"),t("./ember/chatview"),t("./debug"),t("./betterttv"),t("./ui/styles"),t("./ui/notifications"),t("./ui/viewer_count"),t("./ui/menu_button"),t("./ui/menu"),o.prototype.initialize=function(t,o){var s=void 0!=e.App&&void 0!=App.__container__&&void 0!=App.__container__.resolve("model:room");return s?void this.setup(o):(t=t||10,void(o>=6e4?this.log('Twitch application not detected in "'+location.toString()+'". Aborting.'):setTimeout(this.initialize.bind(this,t,(o||0)+t),t)))},o.prototype.setup=function(e){this.log("Found Twitch application after "+(e||0)+' ms in "'+location+'". Initializing FrankerFaceZ version '+o.version_info),this.users={};try{this.ws_create(),this.setup_emoticons(),this.setup_badges(),this.setup_room(),this.setup_line(),this.setup_chatview(),this.setup_css(),this.setup_menu(),this.find_bttv(10)}catch(t){return void this.log("An error occurred while starting FrankerFaceZ: "+t)}this.log("Initialization complete.")}},{"./badges":1,"./betterttv":2,"./debug":4,"./ember/chatview":5,"./ember/line":6,"./ember/room":7,"./emoticons":8,"./shims":10,"./socket":11,"./ui/menu":12,"./ui/menu_button":13,"./ui/notifications":14,"./ui/styles":15,"./ui/viewer_count":16}],10:[function(){Array.prototype.equals=function(e){if(!e)return!1;if(this.length!=e.length)return!1;for(var t=0,o=this.length;o>t;t++)if(this[t]instanceof Array&&e[t]instanceof Array){if(!this[t].equals(e[t]))return!1}else if(this[t]!=e[t])return!1;return!0}},{}],11:[function(){var t=e.FrankerFaceZ;t.prototype._ws_open=!1,t.prototype._ws_delay=0,t.ws_commands={},t.prototype.ws_create=function(){var e=this;this._ws_last_req=0,this._ws_callbacks={};var o=this._ws_sock=new WebSocket("ws://ffz.stendec.me/");o.onopen=function(){e._ws_open=!0,e._ws_delay=0,e.log("Socket connected.");var t=e.get_user();t&&e.ws_send("setuser",t.login);for(var o in e.rooms)e.ws_send("sub",o)},o.onclose=function(){e.log("Socket closed."),e._ws_open=!1,e._ws_delay<3e4&&(e._ws_delay+=5e3),setTimeout(e.ws_create.bind(e),e._ws_delay)},o.onmessage=function(o){var s,n,r=o.data.indexOf(" "),i=o.data.substr(r+1),a=parseInt(o.data.slice(0,r));if(r=i.indexOf(" "),-1===r&&(r=i.length),s=i.slice(0,r),i=i.substr(r+1),i&&(n=JSON.parse(i)),-1===a){var c=t.ws_commands[s];c?c.bind(e)(n):e.log("Invalid command: "+s,n)}else{var l="True"===s,d=e._ws_callbacks[a];e.log("Socket Reply to "+a+" - "+(l?"SUCCESS":"FAIL"),n),d&&(delete e._ws_callbacks[a],d(l,n))}}},t.prototype.ws_send=function(e,t,o){if(!this._ws_open)return!1;var s=++this._ws_last_req;return t=void 0!==t?" "+JSON.stringify(t):"",o&&(this._ws_callbacks[s]=o),this._ws_sock.send(s+" "+e+t),s}},{}],12:[function(){var t=e.FrankerFaceZ;t.prototype.setup_menu=function(){this.log("Installing mouse-up event to auto-close menus.");var e=this;jQuery(document).mouseup(function(t){var o,s=e._popup;s&&(s=jQuery(s),o=s.parent(),o.is(t.target)||0!==o.has(t.target).length||(s.remove(),delete e._popup))})},t.prototype.build_ui_popup=function(e){var t=this._popup;if(t)return t.parentElement.removeChild(t),void delete this._popup;var o=document.createElement("div"),s=document.createElement("div");o.className="emoticon-selector chat-menu ffz-ui-popup",s.className="emoticon-selector-box dropmenu",o.appendChild(s);var n=e.get("controller.currentRoom.id"),r=this.rooms[n];this.log("Menu for Room: "+n,r);var i=document.createElement("a");i.className="button glyph-only ffz-button",i.title="Advertise for FrankerFaceZ in chat!",i.href="#",i.innerHTML='';var a=document.createElement("div");a.className="list-header first",a.appendChild(i),a.appendChild(document.createTextNode("FrankerFaceZ")),s.appendChild(a);var c=this._emotes_for_sets(s,e,r&&r.menu_sets||[]);0===c?i.addEventListener("click",this._add_emote.bind(this,e,"To use custom emoticons in tons of channels, get FrankerFaceZ from http://www.frankerfacez.com")):i.addEventListener("click",this._add_emote.bind(this,e,"To view this channel's emoticons, get FrankerFaceZ from http://www.frankerfacez.com")),this._popup=o,s.style.maxHeight=Math.max(300,e.$().height()-171)+"px",e.$(".chat-interface").append(o)},t.prototype._emotes_for_sets=function(e,t,o,s,n){if(null!=s){var r=document.createElement("div");r.className="list-header",r.appendChild(document.createTextNode(s)),n&&r.appendChild(n),e.appendChild(r)}var i=document.createElement("div"),a=0;i.className="emoticon-grid";for(var c=0;c0){n=!0;break}}e.classList.toggle("no-emotes",!n),e.classList.toggle("dark",r),e.classList.toggle("blue",i)}},{"../constants":3}],14:[function(){var t=e.FrankerFaceZ;t.prototype.show_notification=function(t){e.noty({text:t,theme:"ffzTheme",layout:"bottomCenter",closeWith:["button"]}).show()},t.ws_commands.message=function(e){this.show_notification(e)}},{}],15:[function(t){var o=e.FrankerFaceZ,s=t("../constants");o.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"),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}],16:[function(t){var o=e.FrankerFaceZ,s=t("../constants"),n=t("../utils");o.ws_commands.viewers=function(e){var t=e[0],o=e[1],r=App.__container__.lookup("controller:channel"),i=r&&r.get&&r.get("id");if(i===t){var a=document.querySelector(".channel-stats .ffz.stat"),c=s.ZREKNARF+" "+n.number_commas(o);if(a)a.innerHTML=c;else{var l=document.querySelector(".channel-stats");if(!l)return;a=document.createElement("span"),a.className="ffz stat",a.title="Viewers with FrankerFaceZ",a.innerHTML=c,l.appendChild(a),jQuery(a).tipsy()}}}},{"../constants":3,"../utils":17}],17:[function(t,o){e.FrankerFaceZ,t("./constants");o.exports={update_css:function(e,t,o){var s=e.innerHTML,n="/*BEGIN "+t+"*/",r="/*END "+t+"*/",i=s.indexOf(n),a=s.indexOf(r),c=-1!==i&&-1!==a&&a>i;(c||o)&&(c&&(s=s.substr(0,i)+s.substr(a+r.length)),o&&(s+=n+o+r),e.innerHTML=s)},number_commas:function(e){var t=e.toString().split(".");return t[0]=t[0].replace(/\B(?=(\d{3})+(?!\d))/g,","),t.join(".")}}},{"./constants":3}]},{},[9]),e.ffz=new FrankerFaceZ}(window); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index 114b69e5..814ff962 100644 --- a/src/badges.js +++ b/src/badges.js @@ -34,6 +34,62 @@ var badge_css = function(badge) { // Render Badge // -------------------- +FFZ.prototype.bttv_badges = function(data) { + var user_id = data.sender, + user = this.users[user_id], + badges_out = [], + insert_at = -1; + + if ( ! user || ! user.badges ) + return; + + // Determine where in the list to insert these badges. + for(var i=0; i < data.badges.length; i++) { + var badge = data.badges[i]; + if ( badge.type == "subscriber" || badge.type == "turbo" ) { + insert_at = i; + break; + } + } + + + for (var slot in user.badges) { + if ( ! user.badges.hasOwnProperty(slot) ) + continue; + + var badge = user.badges[slot], + full_badge = this.badges[badge.id] || {}, + desc = badge.title || full_badge.title, + style = "", + alpha = BetterTTV.settings.get('alphaTags'); + + if ( badge.image ) + style += 'background-image: url(\\"' + badge.image + '\\"); '; + + if ( badge.color && ! alpha ) + style += 'background-color: ' + badge.color + '; '; + + if ( badge.extra_css ) + style += badge.extra_css; + + if ( style ) + desc += '" style="' + style; + + badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: "ffz-badge-" + badge.id + (alpha ? " alpha" : ""), name: "", description: desc}]); + } + + badges_out.sort(function(a,b){return a[0] - b[0]}); + + if ( insert_at == -1 ) { + while(badges_out.length) + data.badges.push(badges_out.shift()[1]); + } else { + while(badges_out.length) + data.badges.insertAt(insert_at, badges_out.shift()[1]); + } +} + + FFZ.prototype.render_badge = function(view) { var user = view.get('context.model.from'), room_id = view.get('context.parentController.content.id'), diff --git a/src/betterttv.js b/src/betterttv.js new file mode 100644 index 00000000..bd5b1581 --- /dev/null +++ b/src/betterttv.js @@ -0,0 +1,132 @@ +var FFZ = window.FrankerFaceZ, + SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.find_bttv = function(increment, delay) { + this.has_bttv = false; + if ( window.BTTVLOADED ) + return this.setup_bttv(); + + if ( delay >= 60000 ) + this.log("BetterTTV was not detected after 60 seconds."); + else + setTimeout(this.find_bttv.bind(this, increment, (delay||0) + increment), + increment); +} + + +FFZ.prototype.setup_bttv = function() { + this.log("BetterTTV was detected. Hooking."); + this.has_bttv = true; + + + // Send Message Behavior + var original_send = BetterTTV.chat.helpers.sendMessage, f = this; + BetterTTV.chat.helpers.sendMessage = function(message) { + var cmd = message.split(' ', 1)[0].toLowerCase(); + + if ( cmd === "/ffz" ) + f.run_command(message.substr(5), BetterTTV.chat.store.currentRoom); + else + return original_send(message); + } + + + // Ugly Hack for Current Room + var original_handler = BetterTTV.chat.handlers.privmsg, + received_room; + BetterTTV.chat.handlers.privmsg = function(room, data) { + received_room = room; + var output = original_handler(room, data); + received_room = null; + return output; + } + + + // Message Display Behavior + var original_privmsg = BetterTTV.chat.templates.privmsg; + BetterTTV.chat.templates.privmsg = function(highlight, action, server, isMod, data) { + // Handle badges. + f.bttv_badges(data); + + var output = original_privmsg(highlight, action, server, isMod, data); + return output.replace(SENDER_REGEX, '$1 data-room="' + received_room + '"'); + } + + + // Ugly Hack for Current Sender + var original_template = BetterTTV.chat.templates.message, + received_sender; + BetterTTV.chat.templates.message = function(sender, message, emotes, colored) { + received_sender = sender; + var output = original_template(sender, message, emotes, colored); + received_sender = null; + return output; + } + + + // Emoticonize + var original_emoticonize = BetterTTV.chat.templates.emoticonize; + BetterTTV.chat.templates.emoticonize = function(message, emotes) { + var tokens = original_emoticonize(message, emotes), + user = f.users[received_sender], + room = f.rooms[received_room]; + + // Get our sets. + var sets = _.union(user && user.sets || [], room && room.sets || [], f.global_sets), + emotes = []; + + // Build a list of emotes that match. + _.each(sets, function(set_id) { + var set = f.emote_sets[set_id]; + if ( ! set ) + return; + + _.each(set.emotes, function(emote) { + _.any(tokens, function(token) { + return _.isString(token) && token.match(emote.regex); + }) && emotes.push(emote); + }); + }); + + // Don't bother proceeding if we have no emotes. + if ( ! emotes.length ) + return tokens; + + // Why is emote parsing so bad? ;_; + _.each(emotes, function(emote) { + var eo = ['' + emote.title + ''], + old_tokens = tokens; + + tokens = []; + + if ( ! old_tokens || ! old_tokens.length ) + return tokens; + + for(var i=0; i < old_tokens.length; i++) { + var token = old_tokens[i]; + if ( typeof token != "string" ) { + tokens.push(token); + continue; + } + + var tbits = token.split(emote.regex); + tbits.forEach(function(val, ind) { + if ( val && val.length ) + tokens.push(val); + + if ( ind !== tbits.length - 1 ) + tokens.push(eo); + }); + } + }); + + return tokens; + } + + this.update_ui_link(); +} \ No newline at end of file diff --git a/src/ember/line.js b/src/ember/line.js index 73d66eb4..208896ca 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -14,8 +14,6 @@ FFZ.prototype.setup_line = function() { Line.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. - var tokens = f._emoticonize(this, this._super()); - f.log("Chat Tokens", tokens); return f._emoticonize(this, this._super()); }.property("model.message", "isModeratorOrHigher", "controllers.emoticons.emoticons.[]") @@ -30,16 +28,45 @@ FFZ.prototype.setup_line = function() { didInsertElement: function() { this._super(); - var el = this.get('element'); + var el = this.get('element'), + user = this.get('context.model.from'); + el.setAttribute('data-room', this.get('context.parentController.content.id')); - el.setAttribute('data-sender', this.get('context.model.from')); + el.setAttribute('data-sender', user); f.render_badge(this); + f.capitalize(this, user); + } }); } +// --------------------- +// Capitalization +// --------------------- + +FFZ.capitalization = {}; + +FFZ.prototype.capitalize = function(view, user) { + if ( FFZ.capitalization[user] ) + return view.$('.from').text(FFZ.capitalization[user]); + + var f = this; + jQuery.getJSON("https://api.twitch.tv/kraken/channels/" + user + "?callback=?") + .always(function(data) { + if ( data.display_name == undefined ) + FFZ.capitalization[user] = user; + else + FFZ.capitalization[user] = data.display_name; + + f.capitalize(view, user); + }); +} + + + + // --------------------- // Emoticon Replacement // --------------------- @@ -83,7 +110,7 @@ FFZ.prototype._emoticonize = function(controller, tokens) { // emoticon. _.each(emotes, function(emote) { //var eo = {isEmoticon:true, cls: emote.klass}; - var eo = {emoticonSrc: emote.url, altText: emote.name}; + var eo = {isEmoticon:true, cls: emote.klass, emoticonSrc: emote.url, altText: emote.name}; tokens = _.compact(_.flatten(_.map(tokens, function(token) { if ( _.isObject(token) ) diff --git a/src/ember/room.js b/src/ember/room.js index 4a702d76..a07786e4 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -54,8 +54,14 @@ FFZ.chat_commands = {}; FFZ.prototype.room_message = function(room, text) { var lines = text.split("\n"); - for(var i=0; i < lines.length; i++) - room.room.addMessage({style: 'ffz admin', from: 'FFZ', message: lines[i]}); + if ( this.has_bttv ) { + for(var i=0; i < lines.length; i++) + BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); + + } else { + for(var i=0; i < lines.length; i++) + room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); + } } @@ -192,22 +198,6 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { } -/*FFZ.ws_commands.sets_for_room = function(data) { - var room = this.rooms[data.room]; - if ( ! room ) - return; - - for(var i=0; i < data.sets.length; i++) { - var set = data.sets[i]; - if ( room.sets.contains(set) ) - continue; - - room.sets.push(set); - this.load_set(set); - } -}*/ - - // -------------------- // Ember Modifications // -------------------- diff --git a/src/emoticons.js b/src/emoticons.js index ab9698a2..4646f8c0 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -80,13 +80,15 @@ var build_legacy_css = function(emote) { return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.url + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (emote.extra_css ? "; " + emote.extra_css : "") + "}\n"; } -var build_css = function(emote) { +var build_new_css = function(emote) { if ( ! emote.margins && ! emote.extra_css ) - return ""; + return build_legacy_css(emote); - return 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; + return build_legacy_css(emote) + 'img[src="' + emote.url + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.extra_css || "") + " }\n"; } +var build_css = build_new_css; + FFZ.prototype._load_set_json = function(set_id, callback, data) { @@ -94,6 +96,7 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { this.emote_sets[set_id] = data; data.users = []; data.global = false; + data.count = 0; // Iterate through all the emoticons, building CSS and regex objects as appropriate. var output_css = ""; @@ -111,10 +114,12 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { emote.regex = new RegExp("\\b" + emote.name + "\\b", "g"); output_css += build_css(emote); + data.count++; } utils.update_css(this._emote_style, set_id, output_css + (data.extra_css || "")); this.log("Updated emoticons for set: " + set_id, data); + this.update_ui_link(); if ( callback ) callback(true, data); diff --git a/src/main.js b/src/main.js index 9028c3e7..5edbe102 100644 --- a/src/main.js +++ b/src/main.js @@ -67,6 +67,8 @@ require('./ember/chatview'); require('./debug'); +require('./betterttv'); + require('./ui/styles'); require('./ui/notifications'); require('./ui/viewer_count'); @@ -74,6 +76,7 @@ require('./ui/viewer_count'); require('./ui/menu_button'); require('./ui/menu'); + // --------------- // Initialization // --------------- @@ -119,6 +122,9 @@ FFZ.prototype.setup = function(delay) { this.setup_css(); this.setup_menu(); + this.find_bttv(10); + + } catch(err) { this.log("An error occurred while starting FrankerFaceZ: " + err); return; diff --git a/src/ui/menu.js b/src/ui/menu.js index e88c5534..1fdaf2eb 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -100,15 +100,20 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, btn) { for(var i=0; i < sets.length; i++) { var set = this.emote_sets[sets[i]]; + if ( ! set || ! set.emotes ) + continue; + for(var eid in set.emotes) { var emote = set.emotes[eid]; if ( !set.emotes.hasOwnProperty(eid) || emote.hidden ) continue; c++; - var s = document.createElement('img'); - s.src = emote.url; - //s.className = 'emoticon ' + emote.klass + ' tooltip'; + var s = document.createElement('span'); + s.className = 'emoticon tooltip'; + s.style.backgroundImage = 'url("' + emote.url + '")'; + s.style.width = emote.width + "px"; + s.style.height = emote.height + "px"; s.title = emote.name; s.addEventListener('click', this._add_emote.bind(this, view, emote.name)); grid.appendChild(s); diff --git a/src/ui/menu_button.js b/src/ui/menu_button.js index 573eb94e..b2b847bd 100644 --- a/src/ui/menu_button.js +++ b/src/ui/menu_button.js @@ -6,8 +6,6 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.build_ui_link = function(view) { - // TODO: Detect dark mode from BTTV. - var link = document.createElement('a'); link.className = 'ffz-ui-toggle'; link.innerHTML = constants.CHAT_BUTTON; @@ -22,14 +20,28 @@ FFZ.prototype.build_ui_link = function(view) { FFZ.prototype.update_ui_link = function(link) { var controller = App.__container__.lookup('controller:chat'); link = link || document.querySelector('a.ffz-ui-toggle'); - if ( !link || !controller ) return; + if ( !link || !controller ) return this.log("No button."); var room_id = controller.get('currentRoom.id'), room = this.rooms[room_id], - has_emotes = room && room.sets.length > 0; + has_emotes = false, - if ( has_emotes ) - link.classList.remove('no-emotes'); - else - link.classList.add('no-emotes'); + dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), + blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false); + + + // Check for emoticons. + if ( room && room.sets.length ) { + for(var i=0; i < room.sets.length; i++) { + var set = this.emote_sets[room.sets[i]]; + if ( set && set.count > 0 ) { + has_emotes = true; + break; + } + } + } + + link.classList.toggle('no-emotes', ! has_emotes); + link.classList.toggle('dark', dark); + link.classList.toggle('blue', blue); } \ No newline at end of file diff --git a/test/server.js b/test/server.js index f20fdde6..3bc0d929 100644 --- a/test/server.js +++ b/test/server.js @@ -1,3 +1,5 @@ +var version = "0.1.0"; + var fs = require("fs"), http = require("http"), path = require("path"), @@ -8,6 +10,12 @@ http.createServer(function(req, res) { var uri = url.parse(req.url).pathname, lpath = path.join(uri).split(path.sep); + if ( uri == "/dev_server" ) { + console.log("[200] GET " + uri); + res.writeHead(200, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}); + return res.end(JSON.stringify({path: process.cwd(), version: version})); + } + if ( ! lpath[0] ) lpath.shift(); @@ -26,7 +34,7 @@ http.createServer(function(req, res) { if ( fs.lstatSync(file).isDirectory() ) { console.log("[403] GET " + uri); - res.writeHead(403); + res.writeHead(403, {"Access-Control-Allow-Origin": "*"}); res.write('403 Forbidden'); return res.end(); }