diff --git a/gulpfile.js b/gulpfile.js index 3b70c843..ec68f01b 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -10,6 +10,12 @@ var fs = require('fs'), rename = require('gulp-rename'), uglify = require('gulp-uglify'); +// Templates +var jsEscape = require('gulp-js-escape'), + wrap = require('gulp-wrap'), + declare = require('gulp-declare'); + + // Server Dependencies var http = require("http"), path = require("path"), @@ -31,7 +37,26 @@ gulp.task('prepare', ['clean'], function() { .pipe(gulp.dest('build/')); }); -gulp.task('scripts', ['prepare'], function() { + +gulp.task('templates', ['prepare'], function() { + gulp.src(['build/templates/**/*.hbs']) + .pipe(jsEscape()) + .pipe(wrap('Handlebars.compile(<%= contents %>)')) + .pipe(declare({ + root: 'exports', + noRedeclare: true, + processName: function(filePath) { + var match = filePath.match(/build[\\\/]templates[\\\/](.*)\.hbs$/); + return declare.processNameByPath((match && match.length > 1) ? match[1] : filePath); + } + })) + .pipe(concat('templates.js')) + .pipe(gulp.dest('build/')) + .on('error', util.log); +}); + + +gulp.task('scripts', ['prepare', 'templates'], function() { gulp.src(['build/main.js']) .pipe(browserify()) .pipe(concat('script.js')) diff --git a/script.js b/script.js index 67b364cc..c7e95d10 100644 --- a/script.js +++ b/script.js @@ -54,7 +54,7 @@ FFZ.ws_commands.set_badge = function(data) { user = this.users[user_id] = this.users[user_id] || {}, badges = user.badges = user.badges || {}; - if ( badge === undefined ) + if ( badge === undefined || badge === null ) delete badges[slot]; else badges[slot] = badge; @@ -87,6 +87,9 @@ FFZ.prototype.bttv_badges = function(data) { if ( ! user || ! user.badges ) return; + if ( ! data.badges ) + data.badges = []; + // Determine where in the list to insert these badges. for(var i=0; i < data.badges.length; i++) { var badge = data.badges[i]; @@ -237,7 +240,7 @@ FFZ.prototype.render_badge = function(component) { if ( reverse ) { while(badges_out.length) - badges.before(badges_out.shift()[1]); + before.before(badges_out.shift()[1]); } else { while(badges_out.length) badges.append(badges_out.shift()[1]); @@ -342,7 +345,7 @@ FFZ.prototype._legacy_parse_badges = function(data, slot, badge_id) { this.log('Added "' + title + '" badge to ' + utils.number_commas(count) + " users."); } -},{"./constants":3,"./utils":29}],2:[function(require,module,exports){ +},{"./constants":3,"./utils":30}],2:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -425,6 +428,8 @@ module.exports = { SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", API_SERVER: "//api.frankerfacez.com/", + EMOJI_REGEX: /((?:\ud83c\udde8\ud83c\uddf3|\ud83c\uddfa\ud83c\uddf8|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddef\ud83c\uddf5|\ud83c\uddee\ud83c\uddf9|\ud83c\uddec\ud83c\udde7|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddea\ud83c\uddf8|\ud83c\udde9\ud83c\uddea|\u0039\ufe0f?\u20e3|\u0038\ufe0f?\u20e3|\u0037\ufe0f?\u20e3|\u0036\ufe0f?\u20e3|\u0035\ufe0f?\u20e3|\u0034\ufe0f?\u20e3|\u0033\ufe0f?\u20e3|\u0032\ufe0f?\u20e3|\u0031\ufe0f?\u20e3|\u0030\ufe0f?\u20e3|\u0023\ufe0f?\u20e3|\ud83d\udeb3|\ud83d\udeb1|\ud83d\udeb0|\ud83d\udeaf|\ud83d\udeae|\ud83d\udea6|\ud83d\udea3|\ud83d\udea1|\ud83d\udea0|\ud83d\ude9f|\ud83d\ude9e|\ud83d\ude9d|\ud83d\ude9c|\ud83d\ude9b|\ud83d\ude98|\ud83d\ude96|\ud83d\ude94|\ud83d\ude90|\ud83d\ude8e|\ud83d\ude8d|\ud83d\ude8b|\ud83d\ude8a|\ud83d\ude88|\ud83d\ude86|\ud83d\ude82|\ud83d\ude81|\ud83d\ude36|\ud83d\ude34|\ud83d\ude2f|\ud83d\ude2e|\ud83d\ude2c|\ud83d\ude27|\ud83d\ude26|\ud83d\ude1f|\ud83d\ude1b|\ud83d\ude19|\ud83d\ude17|\ud83d\ude15|\ud83d\ude11|\ud83d\ude10|\ud83d\ude0e|\ud83d\ude08|\ud83d\ude07|\ud83d\ude00|\ud83d\udd67|\ud83d\udd66|\ud83d\udd65|\ud83d\udd64|\ud83d\udd63|\ud83d\udd62|\ud83d\udd61|\ud83d\udd60|\ud83d\udd5f|\ud83d\udd5e|\ud83d\udd5d|\ud83d\udd5c|\ud83d\udd2d|\ud83d\udd2c|\ud83d\udd15|\ud83d\udd09|\ud83d\udd08|\ud83d\udd07|\ud83d\udd06|\ud83d\udd05|\ud83d\udd04|\ud83d\udd02|\ud83d\udd01|\ud83d\udd00|\ud83d\udcf5|\ud83d\udcef|\ud83d\udced|\ud83d\udcec|\ud83d\udcb7|\ud83d\udcb6|\ud83d\udcad|\ud83d\udc6d|\ud83d\udc6c|\ud83d\udc65|\ud83d\udc2a|\ud83d\udc16|\ud83d\udc15|\ud83d\udc13|\ud83d\udc10|\ud83d\udc0f|\ud83d\udc0b|\ud83d\udc0a|\ud83d\udc09|\ud83d\udc08|\ud83d\udc07|\ud83d\udc06|\ud83d\udc05|\ud83d\udc04|\ud83d\udc03|\ud83d\udc02|\ud83d\udc01|\ud83d\udc00|\ud83c\udfe4|\ud83c\udfc9|\ud83c\udfc7|\ud83c\udf7c|\ud83c\udf50|\ud83c\udf4b|\ud83c\udf33|\ud83c\udf32|\ud83c\udf1e|\ud83c\udf1d|\ud83c\udf1c|\ud83c\udf1a|\ud83c\udf18|\ud83c\udccf|\ud83c\udd70|\ud83c\udd71|\ud83c\udd7e|\ud83c\udd8e|\ud83c\udd91|\ud83c\udd92|\ud83c\udd93|\ud83c\udd94|\ud83c\udd95|\ud83c\udd96|\ud83c\udd97|\ud83c\udd98|\ud83c\udd99|\ud83c\udd9a|\ud83d\udc77|\ud83d\udec5|\ud83d\udec4|\ud83d\udec3|\ud83d\udec2|\ud83d\udec1|\ud83d\udebf|\ud83d\udeb8|\ud83d\udeb7|\ud83d\udeb5|\ud83c\ude01|\ud83c\ude02|\ud83c\ude32|\ud83c\ude33|\ud83c\ude34|\ud83c\ude35|\ud83c\ude36|\ud83c\ude37|\ud83c\ude38|\ud83c\ude39|\ud83c\ude3a|\ud83c\ude50|\ud83c\ude51|\ud83c\udf00|\ud83c\udf01|\ud83c\udf02|\ud83c\udf03|\ud83c\udf04|\ud83c\udf05|\ud83c\udf06|\ud83c\udf07|\ud83c\udf08|\ud83c\udf09|\ud83c\udf0a|\ud83c\udf0b|\ud83c\udf0c|\ud83c\udf0f|\ud83c\udf11|\ud83c\udf13|\ud83c\udf14|\ud83c\udf15|\ud83c\udf19|\ud83c\udf1b|\ud83c\udf1f|\ud83c\udf20|\ud83c\udf30|\ud83c\udf31|\ud83c\udf34|\ud83c\udf35|\ud83c\udf37|\ud83c\udf38|\ud83c\udf39|\ud83c\udf3a|\ud83c\udf3b|\ud83c\udf3c|\ud83c\udf3d|\ud83c\udf3e|\ud83c\udf3f|\ud83c\udf40|\ud83c\udf41|\ud83c\udf42|\ud83c\udf43|\ud83c\udf44|\ud83c\udf45|\ud83c\udf46|\ud83c\udf47|\ud83c\udf48|\ud83c\udf49|\ud83c\udf4a|\ud83c\udf4c|\ud83c\udf4d|\ud83c\udf4e|\ud83c\udf4f|\ud83c\udf51|\ud83c\udf52|\ud83c\udf53|\ud83c\udf54|\ud83c\udf55|\ud83c\udf56|\ud83c\udf57|\ud83c\udf58|\ud83c\udf59|\ud83c\udf5a|\ud83c\udf5b|\ud83c\udf5c|\ud83c\udf5d|\ud83c\udf5e|\ud83c\udf5f|\ud83c\udf60|\ud83c\udf61|\ud83c\udf62|\ud83c\udf63|\ud83c\udf64|\ud83c\udf65|\ud83c\udf66|\ud83c\udf67|\ud83c\udf68|\ud83c\udf69|\ud83c\udf6a|\ud83c\udf6b|\ud83c\udf6c|\ud83c\udf6d|\ud83c\udf6e|\ud83c\udf6f|\ud83c\udf70|\ud83c\udf71|\ud83c\udf72|\ud83c\udf73|\ud83c\udf74|\ud83c\udf75|\ud83c\udf76|\ud83c\udf77|\ud83c\udf78|\ud83c\udf79|\ud83c\udf7a|\ud83c\udf7b|\ud83c\udf80|\ud83c\udf81|\ud83c\udf82|\ud83c\udf83|\ud83c\udf84|\ud83c\udf85|\ud83c\udf86|\ud83c\udf87|\ud83c\udf88|\ud83c\udf89|\ud83c\udf8a|\ud83c\udf8b|\ud83c\udf8c|\ud83c\udf8d|\ud83c\udf8e|\ud83c\udf8f|\ud83c\udf90|\ud83c\udf91|\ud83c\udf92|\ud83c\udf93|\ud83c\udfa0|\ud83c\udfa1|\ud83c\udfa2|\ud83c\udfa3|\ud83c\udfa4|\ud83c\udfa5|\ud83c\udfa6|\ud83c\udfa7|\ud83c\udfa8|\ud83c\udfa9|\ud83c\udfaa|\ud83c\udfab|\ud83c\udfac|\ud83c\udfad|\ud83c\udfae|\ud83c\udfaf|\ud83c\udfb0|\ud83c\udfb1|\ud83c\udfb2|\ud83c\udfb3|\ud83c\udfb4|\ud83c\udfb5|\ud83c\udfb6|\ud83c\udfb7|\ud83c\udfb8|\ud83c\udfb9|\ud83c\udfba|\ud83c\udfbb|\ud83c\udfbc|\ud83c\udfbd|\ud83c\udfbe|\ud83c\udfbf|\ud83c\udfc0|\ud83c\udfc1|\ud83c\udfc2|\ud83c\udfc3|\ud83c\udfc4|\ud83c\udfc6|\ud83c\udfc8|\ud83c\udfca|\ud83c\udfe0|\ud83c\udfe1|\ud83c\udfe2|\ud83c\udfe3|\ud83c\udfe5|\ud83c\udfe6|\ud83c\udfe7|\ud83c\udfe8|\ud83c\udfe9|\ud83c\udfea|\ud83c\udfeb|\ud83c\udfec|\ud83c\udfed|\ud83c\udfee|\ud83c\udfef|\ud83c\udff0|\ud83d\udc0c|\ud83d\udc0d|\ud83d\udc0e|\ud83d\udc11|\ud83d\udc12|\ud83d\udc14|\ud83d\udc17|\ud83d\udc18|\ud83d\udc19|\ud83d\udc1a|\ud83d\udc1b|\ud83d\udc1c|\ud83d\udc1d|\ud83d\udc1e|\ud83d\udc1f|\ud83d\udc20|\ud83d\udc21|\ud83d\udc22|\ud83d\udc23|\ud83d\udc24|\ud83d\udc25|\ud83d\udc26|\ud83d\udc27|\ud83d\udc28|\ud83d\udc29|\ud83d\udc2b|\ud83d\udc2c|\ud83d\udc2d|\ud83d\udc2e|\ud83d\udc2f|\ud83d\udc30|\ud83d\udc31|\ud83d\udc32|\ud83d\udc33|\ud83d\udc34|\ud83d\udc35|\ud83d\udc36|\ud83d\udc37|\ud83d\udc38|\ud83d\udc39|\ud83d\udc3a|\ud83d\udc3b|\ud83d\udc3c|\ud83d\udc3d|\ud83d\udc3e|\ud83d\udc40|\ud83d\udc42|\ud83d\udc43|\ud83d\udc44|\ud83d\udc45|\ud83d\udc46|\ud83d\udc47|\ud83d\udc48|\ud83d\udc49|\ud83d\udc4a|\ud83d\udc4b|\ud83d\udc4c|\ud83d\udc4d|\ud83d\udc4e|\ud83d\udc4f|\ud83d\udc50|\ud83d\udc51|\ud83d\udc52|\ud83d\udc53|\ud83d\udc54|\ud83d\udc55|\ud83d\udc56|\ud83d\udc57|\ud83d\udc58|\ud83d\udc59|\ud83d\udc5a|\ud83d\udc5b|\ud83d\udc5c|\ud83d\udc5d|\ud83d\udc5e|\ud83d\udc5f|\ud83d\udc60|\ud83d\udc61|\ud83d\udc62|\ud83d\udc63|\ud83d\udc64|\ud83d\udc66|\ud83d\udc67|\ud83d\udc68|\ud83d\udc69|\ud83d\udc6a|\ud83d\udc6b|\ud83d\udc6e|\ud83d\udc6f|\ud83d\udc70|\ud83d\udc71|\ud83d\udc72|\ud83d\udc73|\ud83d\udc74|\ud83d\udc75|\ud83d\udc76|\ud83d\udeb4|\ud83d\udc78|\ud83d\udc79|\ud83d\udc7a|\ud83d\udc7b|\ud83d\udc7c|\ud83d\udc7d|\ud83d\udc7e|\ud83d\udc7f|\ud83d\udc80|\ud83d\udc81|\ud83d\udc82|\ud83d\udc83|\ud83d\udc84|\ud83d\udc85|\ud83d\udc86|\ud83d\udc87|\ud83d\udc88|\ud83d\udc89|\ud83d\udc8a|\ud83d\udc8b|\ud83d\udc8c|\ud83d\udc8d|\ud83d\udc8e|\ud83d\udc8f|\ud83d\udc90|\ud83d\udc91|\ud83d\udc92|\ud83d\udc93|\ud83d\udc94|\ud83d\udc95|\ud83d\udc96|\ud83d\udc97|\ud83d\udc98|\ud83d\udc99|\ud83d\udc9a|\ud83d\udc9b|\ud83d\udc9c|\ud83d\udc9d|\ud83d\udc9e|\ud83d\udc9f|\ud83d\udca0|\ud83d\udca1|\ud83d\udca2|\ud83d\udca3|\ud83d\udca4|\ud83d\udca5|\ud83d\udca6|\ud83d\udca7|\ud83d\udca8|\ud83d\udca9|\ud83d\udcaa|\ud83d\udcab|\ud83d\udcac|\ud83d\udcae|\ud83d\udcaf|\ud83d\udcb0|\ud83d\udcb1|\ud83d\udcb2|\ud83d\udcb3|\ud83d\udcb4|\ud83d\udcb5|\ud83d\udcb8|\ud83d\udcb9|\ud83d\udcba|\ud83d\udcbb|\ud83d\udcbc|\ud83d\udcbd|\ud83d\udcbe|\ud83d\udcbf|\ud83d\udcc0|\ud83d\udcc1|\ud83d\udcc2|\ud83d\udcc3|\ud83d\udcc4|\ud83d\udcc5|\ud83d\udcc6|\ud83d\udcc7|\ud83d\udcc8|\ud83d\udcc9|\ud83d\udcca|\ud83d\udccb|\ud83d\udccc|\ud83d\udccd|\ud83d\udcce|\ud83d\udccf|\ud83d\udcd0|\ud83d\udcd1|\ud83d\udcd2|\ud83d\udcd3|\ud83d\udcd4|\ud83d\udcd5|\ud83d\udcd6|\ud83d\udcd7|\ud83d\udcd8|\ud83d\udcd9|\ud83d\udcda|\ud83d\udcdb|\ud83d\udcdc|\ud83d\udcdd|\ud83d\udcde|\ud83d\udcdf|\ud83d\udce0|\ud83d\udce1|\ud83d\udce2|\ud83d\udce3|\ud83d\udce4|\ud83d\udce5|\ud83d\udce6|\ud83d\udce7|\ud83d\udce8|\ud83d\udce9|\ud83d\udcea|\ud83d\udceb|\ud83d\udcee|\ud83d\udcf0|\ud83d\udcf1|\ud83d\udcf2|\ud83d\udcf3|\ud83d\udcf4|\ud83d\udcf6|\ud83d\udcf7|\ud83d\udcf9|\ud83d\udcfa|\ud83d\udcfb|\ud83d\udcfc|\ud83d\udd03|\ud83d\udd0a|\ud83d\udd0b|\ud83d\udd0c|\ud83d\udd0d|\ud83d\udd0e|\ud83d\udd0f|\ud83d\udd10|\ud83d\udd11|\ud83d\udd12|\ud83d\udd13|\ud83d\udd14|\ud83d\udd16|\ud83d\udd17|\ud83d\udd18|\ud83d\udd19|\ud83d\udd1a|\ud83d\udd1b|\ud83d\udd1c|\ud83d\udd1d|\ud83d\udd1e|\ud83d\udd1f|\ud83d\udd20|\ud83d\udd21|\ud83d\udd22|\ud83d\udd23|\ud83d\udd24|\ud83d\udd25|\ud83d\udd26|\ud83d\udd27|\ud83d\udd28|\ud83d\udd29|\ud83d\udd2a|\ud83d\udd2b|\ud83d\udd2e|\ud83d\udd2f|\ud83d\udd30|\ud83d\udd31|\ud83d\udd32|\ud83d\udd33|\ud83d\udd34|\ud83d\udd35|\ud83d\udd36|\ud83d\udd37|\ud83d\udd38|\ud83d\udd39|\ud83d\udd3a|\ud83d\udd3b|\ud83d\udd3c|\ud83d\udd3d|\ud83d\udd50|\ud83d\udd51|\ud83d\udd52|\ud83d\udd53|\ud83d\udd54|\ud83d\udd55|\ud83d\udd56|\ud83d\udd57|\ud83d\udd58|\ud83d\udd59|\ud83d\udd5a|\ud83d\udd5b|\ud83d\uddfb|\ud83d\uddfc|\ud83d\uddfd|\ud83d\uddfe|\ud83d\uddff|\ud83d\ude01|\ud83d\ude02|\ud83d\ude03|\ud83d\ude04|\ud83d\ude05|\ud83d\ude06|\ud83d\ude09|\ud83d\ude0a|\ud83d\ude0b|\ud83d\ude0c|\ud83d\ude0d|\ud83d\ude0f|\ud83d\ude12|\ud83d\ude13|\ud83d\ude14|\ud83d\ude16|\ud83d\ude18|\ud83d\ude1a|\ud83d\ude1c|\ud83d\ude1d|\ud83d\ude1e|\ud83d\ude20|\ud83d\ude21|\ud83d\ude22|\ud83d\ude23|\ud83d\ude24|\ud83d\ude25|\ud83d\ude28|\ud83d\ude29|\ud83d\ude2a|\ud83d\ude2b|\ud83d\ude2d|\ud83d\ude30|\ud83d\ude31|\ud83d\ude32|\ud83d\ude33|\ud83d\ude35|\ud83d\ude37|\ud83d\ude38|\ud83d\ude39|\ud83d\ude3a|\ud83d\ude3b|\ud83d\ude3c|\ud83d\ude3d|\ud83d\ude3e|\ud83d\ude3f|\ud83d\ude40|\ud83d\ude45|\ud83d\ude46|\ud83d\ude47|\ud83d\ude48|\ud83d\ude49|\ud83d\ude4a|\ud83d\ude4b|\ud83d\ude4c|\ud83d\ude4d|\ud83d\ude4e|\ud83d\ude4f|\ud83d\ude80|\ud83d\ude83|\ud83d\ude84|\ud83d\ude85|\ud83d\ude87|\ud83d\ude89|\ud83d\ude8c|\ud83d\ude8f|\ud83d\ude91|\ud83d\ude92|\ud83d\ude93|\ud83d\ude95|\ud83d\ude97|\ud83d\ude99|\ud83d\ude9a|\ud83d\udea2|\ud83d\udea4|\ud83d\udea5|\ud83d\udea7|\ud83d\udea8|\ud83d\udea9|\ud83d\udeaa|\ud83d\udeab|\ud83d\udeac|\ud83d\udead|\ud83d\udeb2|\ud83d\udeb6|\ud83d\udeb9|\ud83d\udeba|\ud83d\udebb|\ud83d\udebc|\ud83d\udebd|\ud83d\udebe|\ud83d\udec0|\ud83c\udde6|\ud83c\udde7|\ud83c\udde8|\ud83c\udde9|\ud83c\uddea|\ud83c\uddeb|\ud83c\uddec|\ud83c\udded|\ud83c\uddee|\ud83c\uddef|\ud83c\uddf0|\ud83c\uddf1|\ud83c\uddf2|\ud83c\uddf3|\ud83c\uddf4|\ud83c\uddf5|\ud83c\uddf6|\ud83c\uddf7|\ud83c\uddf8|\ud83c\uddf9|\ud83c\uddfa|\ud83c\uddfb|\ud83c\uddfc|\ud83c\uddfd|\ud83c\uddfe|\ud83c\uddff|\ud83c\udf0d|\ud83c\udf0e|\ud83c\udf10|\ud83c\udf12|\ud83c\udf16|\ud83c\udf17|\ue50a|\u3030|\u27b0|\u2797|\u2796|\u2795|\u2755|\u2754|\u2753|\u274e|\u274c|\u2728|\u270b|\u270a|\u2705|\u26ce|\u23f3|\u23f0|\u23ec|\u23eb|\u23ea|\u23e9|\u2122|\u27bf|\u00a9|\u00ae)|(?:(?:\ud83c\udc04|\ud83c\udd7f|\ud83c\ude1a|\ud83c\ude2f|\u3299|\u303d|\u2b55|\u2b50|\u2b1c|\u2b1b|\u2b07|\u2b06|\u2b05|\u2935|\u2934|\u27a1|\u2764|\u2757|\u2747|\u2744|\u2734|\u2733|\u2716|\u2714|\u2712|\u270f|\u270c|\u2709|\u2708|\u2702|\u26fd|\u26fa|\u26f5|\u26f3|\u26f2|\u26ea|\u26d4|\u26c5|\u26c4|\u26be|\u26bd|\u26ab|\u26aa|\u26a1|\u26a0|\u2693|\u267f|\u267b|\u3297|\u2666|\u2665|\u2663|\u2660|\u2653|\u2652|\u2651|\u2650|\u264f|\u264e|\u264d|\u264c|\u264b|\u264a|\u2649|\u2648|\u263a|\u261d|\u2615|\u2614|\u2611|\u260e|\u2601|\u2600|\u25fe|\u25fd|\u25fc|\u25fb|\u25c0|\u25b6|\u25ab|\u25aa|\u24c2|\u231b|\u231a|\u21aa|\u21a9|\u2199|\u2198|\u2197|\u2196|\u2195|\u2194|\u2139|\u2049|\u203c|\u2668)([\uFE0E\uFE0F]?)))/g, + SVGPATH: SVGPATH, ZREKNARF: '' + SVGPATH + '', CHAT_BUTTON: '' + SVGPATH + '', @@ -491,6 +496,12 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.setup_channel = function() { + // Style Stuff! + this.log("Creating channel style element."); + var s = this._channel_style = document.createElement("style"); + s.id = "ffz-channel-css"; + document.head.appendChild(s); + // Settings stuff! document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views); @@ -555,13 +566,32 @@ FFZ.prototype.setup_channel = function() { ffzHostTarget: function() { var target = this.get('content.hostModeTarget'), name = target && target.get('name'), + id = target && target.get('id'), display_name = target && target.get('display_name'); + if ( id !== f.__old_host_target ) { + if ( f.__old_host_target ) + f.ws_send("unsub_channel", f.__old_host_target); + + if ( id ) { + f.ws_send("sub_channel", id); + f.__old_host_target = id; + } else + delete f.__old_host_target; + } + if ( display_name ) FFZ.capitalization[name] = [display_name, Date.now()]; if ( f.settings.group_tabs && f._chatv ) f._chatv.ffzRebuildTabs(); + + if ( f.settings.follow_buttons ) + f.rebuild_following_ui(); + + if ( f.settings.srl_races ) + f.rebuild_race_ui(); + }.observes("content.hostModeTarget") }); } @@ -590,15 +620,28 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzInit: function() { + var id = this.get('controller.id'), + el = this.get('element'); + f._cindex = this; - this.get('element').setAttribute('data-channel', this.get('controller.id')); + f.ws_send("sub_channel", id); + + el.setAttribute('data-channel', id); + el.classList.add('ffz-channel'); + this.ffzFixTitle(); this.ffzUpdateUptime(); this.ffzUpdateChatters(); - var el = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') - if ( el ) - el.parentNode.classList.add('twitch-channel-views'); + var views = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') + if ( views ) + views.parentNode.classList.add('twitch-channel-views'); + + if ( f.settings.follow_buttons ) + f.rebuild_following_ui(); + + if ( f.settings.srl_races ) + f.rebuild_race_ui(); }, ffzFixTitle: function() { @@ -612,7 +655,10 @@ FFZ.prototype._modify_cindex = function(view) { this.$(".title span").each(function(i, el) { var scripts = el.querySelectorAll("script"); - el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; + if ( ! scripts.length ) + el.innerHTML = status; + else + el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; }); }, @@ -631,7 +677,8 @@ FFZ.prototype._modify_cindex = function(view) { } var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length, - ffz_chatters = room.ffz_chatters || 0; + ffz_chatters = room.ffz_chatters || 0, + ffz_viewers = room.ffz_viewers || 0; var el = this.get('element').querySelector('#ffz-chatter-display span'); if ( ! el ) { @@ -642,7 +689,7 @@ FFZ.prototype._modify_cindex = function(view) { var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-chatter-display'; - stat.title = "Current Chatters"; + stat.title = "Currently in Chat"; stat.innerHTML = constants.ROOMS + " "; el = document.createElement("span"); @@ -659,7 +706,7 @@ FFZ.prototype._modify_cindex = function(view) { el.innerHTML = utils.number_commas(chatter_count); - if ( ! ffz_chatters ) { + if ( ! ffz_chatters && ! ffz_viewers ) { el = this.get('element').querySelector('#ffz-ffzchatter-display'); el && el.parentNode.removeChild(el); return; @@ -674,7 +721,7 @@ FFZ.prototype._modify_cindex = function(view) { var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-ffzchatter-display'; - stat.title = "Chatters with FrankerFaceZ"; + stat.title = "Viewers (In Chat) with FrankerFaceZ"; stat.innerHTML = constants.ZREKNARF + " "; el = document.createElement("span"); @@ -689,7 +736,7 @@ FFZ.prototype._modify_cindex = function(view) { jQuery(stat).tipsy(); } - el.innerHTML = utils.number_commas(ffz_chatters); + el.innerHTML = utils.number_commas(ffz_viewers) + " (" + utils.number_commas(ffz_chatters) + ")"; }, @@ -756,10 +803,16 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzTeardown: function() { + var id = this.get('controller.id'); + if ( id ) + f.ws_send("unsub_channel", id); + this.get('element').setAttribute('data-channel', ''); f._cindex = undefined; if ( this._ffz_update_uptime ) clearTimeout(this._ffz_update_uptime); + + utils.update_css(f._channel_style, id, null); } }); } @@ -832,7 +885,7 @@ FFZ.settings_info.stream_title = { this._cindex.ffzFixTitle(); } }; -},{"../constants":3,"../utils":29}],6:[function(require,module,exports){ +},{"../constants":3,"../utils":30}],6:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'), constants = require('../constants'), @@ -852,13 +905,36 @@ var FFZ = window.FrankerFaceZ, // Settings // -------------------- + +FFZ.settings_info.minimal_chat = { + type: "boolean", + value: false, + + //no_bttv: true, + + category: "Chat", + name: "Minimalistic Chat", + help: "Hide all of the chat user interface, only showing messages and an input box.", + + on_update: function(val) { + document.body.classList.toggle("ffz-minimal-chat", val); + if ( this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { + var f = this; + setTimeout(function() { + f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + },0); + } + } + }; + + FFZ.settings_info.prevent_clear = { type: "boolean", value: false, no_bttv: true, - category: "Chat", + category: "Chat Moderation", name: "Show Deleted Messages", help: "Fade deleted messages instead of replacing them, and prevent chat from being cleared.", @@ -933,6 +1009,9 @@ FFZ.settings_info.pinned_rooms = { // -------------------- FFZ.prototype.setup_chatview = function() { + //if ( ! this.has_bttv ) + document.body.classList.toggle("ffz-minimal-chat", this.settings.minimal_chat); + this.log("Hooking the Ember Chat controller."); var Chat = App.__container__.lookup('controller:chat'), @@ -943,7 +1022,26 @@ FFZ.prototype.setup_chatview = function() { ffzUpdateChannels: function() { if ( f.settings.group_tabs && f._chatv ) f._chatv.ffzRebuildTabs(); - }.observes("currentChannelRoom", "connectedPrivateGroupRooms") + }.observes("currentChannelRoom", "connectedPrivateGroupRooms"), + + removeCurrentChannelRoom: function() { + if ( ! f.settings.group_tabs || f.has_bttv ) + return this._super(); + + var room = this.get("currentChannelRoom"), + room_id = room && room.get('id'); + + if ( ! f.settings.pinned_rooms || f.settings.pinned_rooms.indexOf(room_id) === -1 ) { + // We can actually destroy it. + if ( room === this.get("currentRoom") ) + this.blurRoom(); + + if ( room ) + room.destroy(); + } + + this.set("currentChannelRoom", void 0); + } }); } @@ -1161,7 +1259,7 @@ FFZ.prototype._modify_cview = function(view) { if ( target && Room ) { var target_id = target.get('id'); if ( this._ffz_host !== target_id ) { - if ( this._ffz_host_room ) { + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { if ( this.get('controller.currentRoom') === this._ffz_host_room ) this.get('controller').blurRoom(); this._ffz_host_room.destroy(); @@ -1171,7 +1269,7 @@ FFZ.prototype._modify_cview = function(view) { this._ffz_host_room = Room.findOne(target_id); } } else if ( this._ffz_host ) { - if ( this._ffz_host_room ) { + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { if ( this.get('controller.currentRoom') === this._ffz_host_room ) this.get('controller').blurRoom(); this._ffz_host_room.destroy(); @@ -1244,7 +1342,7 @@ FFZ.prototype._modify_cview = function(view) { }, ffzBuildTab: function(view, room, current_channel, host_channel) { - var tab = document.createElement('span'), name, unread, + var tab = document.createElement('span'), name, unread, icon = '', group = room.get('isGroupRoom'), current = room === view.get('controller.currentRoom'); @@ -1256,21 +1354,25 @@ FFZ.prototype._modify_cview = function(view) { tab.classList.toggle('group-chat', group); tab.classList.toggle('active', current); - name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'))); unread = format_unread(current ? 0 : room.get('unreadCount')); + name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'), function(name) { + unread = format_unread(current ? 0 : room.get('unreadCount')); + tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; + })); + if ( current_channel ) { - tab.innerHTML = constants.CAMERA; + icon = constants.CAMERA; tab.title = "Current Channel"; } else if ( host_channel ) { - tab.innerHTML = constants.EYE; + icon = constants.EYE; tab.title = "Hosted Channel"; } else if ( group ) tab.title = "Group Chat"; else tab.title = "Pinned Channel"; - tab.innerHTML += utils.sanitize(name) + '' + unread + ''; + tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; tab.addEventListener('click', function() { view.get('controller').focusRoom(room); @@ -1287,7 +1389,7 @@ FFZ.prototype._modify_cview = function(view) { } if ( this._ffz_host ) { - if ( this._ffz_host_room ) { + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { if ( this.get('controller.currentRoom') === this._ffz_host_room ) this.get('controller').blurRoom(); this._ffz_host_room.destroy(); @@ -1408,7 +1510,7 @@ FFZ.chat_commands.part = function(room, args) { else return "You are not in " + room_id + "."; } -},{"../constants":3,"../utils":29}],7:[function(require,module,exports){ +},{"../constants":3,"../utils":30}],7:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), @@ -1426,8 +1528,12 @@ var FFZ = window.FrankerFaceZ, TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", + SRCSETS = {}; build_srcset = function(id) { - return TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x"; + if ( SRCSETS[id] ) + return SRCSETS[id]; + var out = SRCSETS[id] = TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x"; + return out; }, @@ -1442,7 +1548,7 @@ var FFZ = window.FrankerFaceZ, if ( ! set ) return data.code; - else if ( set == "00000turbo" || set == "turbo" ) { + else if ( set == "--twitch-turbo--" || set == "turbo" ) { set = "Twitch Turbo"; set_type = null; } @@ -1592,6 +1698,69 @@ var FFZ = window.FrankerFaceZ, // Settings // --------------------- +FFZ.settings_info.parse_emoji = { + type: "boolean", + value: true, + + category: "Chat", + + name: "Replace Emoji with Images", + help: "Replace emoji in chat messages with nicer looking images from the open-source Twitter Emoji project." + }; + + +FFZ.settings_info.room_status = { + type: "boolean", + value: true, + + category: "Chat", + no_bttv: true, + + name: "Room Status Indicators", + help: "Display the current room state (slow mode, sub mode, and r9k mode) next to the Chat button.", + + on_update: function() { + if ( this._roomv ) + this._roomv.ffzUpdateStatus(); + } + }; + + +FFZ.settings_info.scrollback_length = { + type: "button", + value: 150, + + category: "Chat", + no_bttv: true, + + name: "Scrollback Length", + help: "Set the maximum number of lines to keep in chat.", + + method: function() { + var new_val = prompt("Scrollback Length\n\nPlease enter a new maximum length for the chat scrollback. Default: 150\n\nNote: Making this too large will cause your browser to lag.", this.settings.scrollback_length); + if ( new_val === null || new_val === undefined ) + return; + + new_val = parseInt(new_val); + if ( new_val === NaN ) + return; + + if ( new_val < 10 ) + new_val = 10; + + this.settings.set("scrollback_length", new_val); + + // Update our everything. + var Chat = App.__container__.lookup('controller:chat'), + current_id = Chat && Chat.get('currentRoom.id'); + + for(var room_id in this.rooms) { + var room = this.rooms[room_id]; + room.room.set('messageBufferSize', new_val + ((this._roomv && !this._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0)); + } + } + }; + FFZ.settings_info.banned_words = { type: "button", value: [], @@ -1771,6 +1940,9 @@ FFZ.prototype._modify_line = function(component) { tokens = f._remove_banned(tokens); tokens = f._emoticonize(this, tokens); + if ( f.settings.parse_emoji ) + tokens = f.tokenize_emoji(tokens); + // Store the capitalization. var display = this.get("msgObject.tags.display-name"); if ( display && display.length ) @@ -1806,6 +1978,18 @@ FFZ.prototype._modify_line = function(component) { this.rerender(); }), + willClearRender: function() { + // This is here to prevent tipsy tooltips from hanging around. + try { + this.$('a.mod-icon').tipsy('disable'); + jQuery('body > .tipsy:last').remove(); + + } catch(err) { + f.error("LineView willClearRender: " + err); + } + this._super(); + }, + didInsertElement: function() { this._super(); try { @@ -1827,8 +2011,8 @@ FFZ.prototype._modify_line = function(component) { this.set("msgObject.ffz_alternate", row_type); } - el.classList.toggle('ffz-alternate', row_type); - el.classList.toggle('ffz-deleted', f.settings.prevent_clear && this.get('msgObject.ffz_deleted')); + el.classList.toggle('ffz-alternate', row_type || false); + el.classList.toggle('ffz-deleted', f.settings.prevent_clear && this.get('msgObject.ffz_deleted') || false); // Basic Data @@ -1863,50 +2047,9 @@ FFZ.prototype._modify_line = function(component) { // Banned Links - var bad_links = el.querySelectorAll('a.deleted-link'); - for(var i=0; i < bad_links.length; i++) { - var link = bad_links[i]; - - link.addEventListener("click", function(e) { - if ( ! this.classList.contains("deleted-link") ) - return true; - - // Get the URL - var href = this.getAttribute('data-url'), - link = href; - - // Delete Old Stuff - this.classList.remove('deleted-link'); - this.removeAttribute("data-url"); - this.removeAttribute("title"); - this.removeAttribute("original-title"); - - // Process URL - if ( href.indexOf("@") > -1 && (-1 === href.indexOf("/") || href.indexOf("@") < href.indexOf("/")) ) - href = "mailto:" + href; - else if ( ! href.match(/^https?:\/\//) ) - href = "http://" + href; - - // Set up the Link - this.href = href; - this.target = "_new"; - this.textContent = link; - - // Now, check for a tooltip. - var link_data = f._link_data[link]; - if ( link_data && typeof link_data != "boolean" ) { - this.title = link_data.tooltip; - if ( link_data.unsafe ) - this.classList.add('unsafe-link'); - } - - // Stop from Navigating - e.preventDefault(); - }); - - // Also add a nice tooltip. - jQuery(link).tipsy({html:true}); - } + var bad_links = el.querySelectorAll('span.message a.deleted-link'); + for(var i=0; i < bad_links.length; i++) + bad_links[i].addEventListener("click", f._deleted_link_click); // Link Tooltips @@ -1942,12 +2085,11 @@ FFZ.prototype._modify_line = function(component) { // Enhanced Emotes - var images = el.querySelectorAll('img.emoticon'); + var images = el.querySelectorAll('span.message img.emoticon'); for(var i=0; i < images.length; i++) { var img = images[i], name = img.alt, - match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(img.src), - id = match ? parseInt(match[1]) : null; + id = FFZ.src_to_id(img.src); if ( id !== null ) { // High-DPI Images @@ -1965,6 +2107,15 @@ FFZ.prototype._modify_line = function(component) { f.ws_send("twitch_emote", id, load_emote_data.bind(f, id, img.alt)); } + } else if ( img.getAttribute('data-ffz-emoji') ) { + var eid = img.getAttribute('data-ffz-emoji'), + data = f.emoji_data && f.emoji_data[eid]; + + if ( data ) { + img.setAttribute('srcset', data.srcSet); + img.title = "Emoji: " + img.alt + "\nName: " + data.short_name; + } + } else if ( img.getAttribute('data-ffz-emote') ) { var data = JSON.parse(decodeURIComponent(img.getAttribute('data-ffz-emote'))), id = data && data[0] || null, @@ -2071,10 +2222,6 @@ FFZ.capitalization = {}; FFZ._cap_fetching = 0; FFZ.get_capitalization = function(name, callback) { - // Use the BTTV code if it's present. - if ( window.BetterTTV && BetterTTV.chat && BetterTTV.chat.helpers.lookupDisplayName ) - return BetterTTV.chat.helpers.lookupDisplayName(name); - if ( ! name ) return name; @@ -2148,9 +2295,10 @@ FFZ.prototype._emoticonize = function(component, tokens) { return this.tokenize_emotes(user_id, room_id, tokens); } -},{"../utils":29}],8:[function(require,module,exports){ +},{"../utils":30}],8:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), + helpers, keycodes = { ESC: 27, @@ -2160,31 +2308,150 @@ var FFZ = window.FrankerFaceZ, U: 85 }, - btns = [ - ['5m', 300], - ['10m', 600], - ['1hr', 3600], - ['12hr', 43200], - ['24hr', 86400]], - MESSAGE = '', - CHECK = ''; + CHECK = '', + + DURATIONS = {}, + duration_string = function(val) { + if ( val === 1 ) + return 'Purge'; + + if ( DURATIONS[val] ) + return DURATIONS[val]; + + var weeks, days, hours, minutes, seconds; + + weeks = Math.floor(val / 604800); + seconds = val % 604800; + + days = Math.floor(seconds / 86400); + seconds %= 86400; + + hours = Math.floor(seconds / 3600); + seconds %= 3600; + + minutes = Math.floor(seconds / 60); + seconds %= 60; + + var out = DURATIONS[val] = (weeks ? weeks + 'w' : '') + ((days || (weeks && (hours || minutes || seconds))) ? days + 'd' : '') + ((hours || ((weeks || days) && (minutes || seconds))) ? hours + 'h' : '') + ((minutes || ((weeks || days || hours) && seconds)) ? minutes + 'm' : '') + (seconds ? seconds + 's' : ''); + return out; + }; + + +try { + helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); +} catch(err) { } // ---------------- // Settings // ---------------- -FFZ.settings_info.enhanced_moderation = { +FFZ.settings_info.chat_hover_pause = { type: "boolean", value: false, no_bttv: true, - //visible: function() { return ! this.has_bttv }, - category: "Chat", - name: "Enhanced Moderation", - help: "Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards." + category: "Chat Moderation", + name: "Pause Chat Scrolling on Mouse Hover", + help: "Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link mis-clicks.", + + on_update: function(val) { + if ( ! this._roomv ) + return; + + if ( val ) + this._roomv.ffzEnableFreeze(); + else + this._roomv.ffzDisableFreeze(); + } + }; + + +FFZ.settings_info.short_commands = { + type: "boolean", + value: true, + + no_bttv: true, + category: "Chat Moderation", + + name: "Short Moderation Commands", + help: "Use /t, /b, and /u in chat in place of /timeout, /ban, /unban for quicker moderation, and use /p for 1 second timeouts." + }; + + +FFZ.settings_info.mod_card_hotkeys = { + type: "boolean", + value: false, + + no_bttv: true, + category: "Chat Moderation", + + name: "Moderation Card Hotkeys", + help: "With a moderation card selected, press B to ban the user, T to time them out for 10 minutes, P to time them out for 1 second, or U to unban them. ESC closes the card." + }; + + +FFZ.settings_info.mod_card_history = { + type: "boolean", + value: false, + + no_bttv: true, + category: "Chat Moderation", + + name: "Moderation Card History", + help: "Display a few of the user's previously sent messages on moderation cards.", + + on_update: function(val) { + if ( val || ! this.rooms ) + return; + + // Delete all history~! + for(var room_id in this.rooms) { + var room = this.rooms[room_id]; + if ( room ) + room.user_history = undefined; + } + } + }; + + +FFZ.settings_info.mod_card_durations = { + type: "button", + value: [300, 600, 3600, 43200, 86400, 604800], + + category: "Chat Moderation", + no_bttv: true, + + name: "Moderation Card Timeout Buttons", + help: "Add additional timeout buttons to moderation cards with specific durations.", + + method: function() { + var old_val = this.settings.mod_card_durations.join(", "), + new_val = prompt("Moderation Card Timeout Buttons\n\nPlease enter a comma-separated list of durations that you would like to have timeout buttons for. Durations must be expressed in seconds.\n\nEnter \"reset\" without quotes to return to the default value.", old_val); + + if ( new_val === null || new_val === undefined ) + return; + + if ( new_val === "reset" ) + new_val = FFZ.settings_info.mod_card_durations.value.join(", "); + + // Split them up. + new_val = new_val.trim().split(/[ ,]+/); + var vals = []; + + for(var i=0; i < new_val.length; i++) { + var val = parseInt(new_val[i]); + if ( val === 0 ) + val = 1; + + if ( val !== NaN && val > 0 ) + vals.push(val); + } + + this.settings.set("mod_card_durations", vals); + } }; @@ -2193,23 +2460,43 @@ FFZ.settings_info.enhanced_moderation = { // ---------------- FFZ.prototype.setup_mod_card = function() { + this.log("Modifying Mousetrap stopCallback so we can catch ESC."); + var orig_stop = Mousetrap.stopCallback; + Mousetrap.stopCallback = function(e, element, combo) { + if ( element.classList.contains('no-mousetrap') ) + return true; + + return orig_stop(e, element, combo); + } + + Mousetrap.bind("up up down down left right left right b a enter", function() { + var el = document.querySelector(".app-main") || document.querySelector(".ember-chat-container"); + el && el.classList.toggle('ffz-flip'); + }); + + this.log("Hooking the Ember Moderation Card view."); var Card = App.__container__.resolve('component:moderation-card'), f = this; Card.reopen({ + ffzForceRedraw: function() { + this.rerender(); + }.observes("cardInfo.isModeratorOrHigher", "cardInfo.user"), + didInsertElement: function() { this._super(); window._card = this; try { - if ( f.has_bttv || ! f.settings.enhanced_moderation ) + if ( f.has_bttv ) return; var el = this.get('element'), controller = this.get('controller'); // Style it! - el.classList.add('ffz-moderation-card'); + if ( f.settings.mod_card_hotkeys || (f.settings.mod_card_durations && f.settings.mod_card_durations.length) ) + el.classList.add('ffz-moderation-card'); // Only do the big stuff if we're mod. if ( controller.get('cardInfo.isModeratorOrHigher') ) { @@ -2217,91 +2504,94 @@ FFZ.prototype.setup_mod_card = function() { el.setAttribute('tabindex', 1); // Key Handling - el.addEventListener('keyup', function(e) { - var key = e.keyCode || e.which, - user_id = controller.get('cardInfo.user.id'), - room = App.__container__.lookup('controller:chat').get('currentRoom'); + if ( f.settings.mod_card_hotkeys ) { + el.classList.add('no-mousetrap'); - if ( key == keycodes.P ) - room.send("/timeout " + user_id + " 1"); - - else if ( key == keycodes.B ) - room.send("/ban " + user_id); - - else if ( key == keycodes.T ) - room.send("/timeout " + user_id + " 600"); - - else if ( key == keycodes.U ) - room.send("/unban " + user_id); - - else if ( key != keycodes.ESC ) - return; - - controller.send('hideModOverlay'); - }); - - - // Extra Moderation - var line = document.createElement('div'); - line.className = 'interface clearfix'; - - var btn_click = function(timeout) { - var user_id = controller.get('cardInfo.user.id'), + el.addEventListener('keyup', function(e) { + var key = e.keyCode || e.which, + user_id = controller.get('cardInfo.user.id'), room = App.__container__.lookup('controller:chat').get('currentRoom'); - if ( timeout === -1 ) - room.send("/unban " + user_id); - else - room.send("/timeout " + user_id + " " + timeout); - }, + if ( key == keycodes.P ) + room.send("/timeout " + user_id + " 1"); - btn_make = function(text, timeout) { - var btn = document.createElement('button'); - btn.className = 'button'; - btn.innerHTML = text; - btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + else if ( key == keycodes.B ) + room.send("/ban " + user_id); - if ( timeout === 600 ) - btn.title = "(T)" + btn.title.substr(1); - else if ( timeout === 1 ) - btn.title = "(P)urge - " + btn.title; + else if ( key == keycodes.T ) + room.send("/timeout " + user_id + " 600"); - jQuery(btn).tipsy(); + else if ( key == keycodes.U ) + room.send("/unban " + user_id); - btn.addEventListener('click', btn_click.bind(this, timeout)); - return btn; - }; + else if ( key != keycodes.ESC ) + return; - line.appendChild(btn_make('Purge', 1)); + controller.send('close'); + }); + } - var s = document.createElement('span'); - s.className = 'right'; - line.appendChild(s); + var btn_click = function(timeout) { + var user_id = controller.get('cardInfo.user.id'), + room = App.__container__.lookup('controller:chat').get('currentRoom'); - for(var i=0; i < btns.length; i++) - s.appendChild(btn_make(btns[i][0], btns[i][1])); + if ( timeout === -1 ) + room.send("/unban " + user_id); + else + room.send("/timeout " + user_id + " " + timeout); + }, - el.appendChild(line); + btn_make = function(timeout) { + var btn = document.createElement('button') + btn.className = 'button'; + btn.innerHTML = duration_string(timeout); + btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + if ( f.settings.mod_card_hotkeys && timeout === 600 ) + btn.title = "(T)" + btn.title.substr(1); + else if ( f.settings.mod_card_hotkeys && timeout === 1 ) + btn.title = "(P)urge - " + btn.title; + + jQuery(btn).tipsy(); + + btn.addEventListener('click', btn_click.bind(this, timeout)); + return btn; + }; + + if ( f.settings.mod_card_durations && f.settings.mod_card_durations.length ) { + // Extra Moderation + var line = document.createElement('div'); + line.className = 'interface clearfix'; + + line.appendChild(btn_make(1)); + + var s = document.createElement('span'); + s.className = 'right'; + line.appendChild(s); + + for(var i=0; i < f.settings.mod_card_durations.length; i++) + s.appendChild(btn_make(f.settings.mod_card_durations[i])); + + el.appendChild(line); + + // Fix Other Buttons + this.$("button.timeout").remove(); + } + + var ban_btn = el.querySelector('button.ban'); + if ( f.settings.mod_card_hotkeys ) + ban_btn.setAttribute('title', '(B)an User'); // Unban Button - var unban_btn = document.createElement('button'); unban_btn.className = 'unban button glyph-only light'; unban_btn.innerHTML = CHECK; - unban_btn.title = "(U)nban User"; + unban_btn.title = (f.settings.mod_card_hotkeys ? "(U)" : "U") + "nban User"; jQuery(unban_btn).tipsy(); unban_btn.addEventListener("click", btn_click.bind(this, -1)); - var ban_btn = el.querySelector('button.ban'); - ban_btn.setAttribute('title', '(B)an User'); - jQuery(ban_btn).after(unban_btn); - - - // Fix Other Buttons - this.$("button.timeout").remove(); } @@ -2323,11 +2613,52 @@ FFZ.prototype.setup_mod_card = function() { msg_btn.classList.add('glyph-only'); msg_btn.classList.add('message'); - msg_btn.title = "Message User"; + msg_btn.title = "Whisper User"; jQuery(msg_btn).tipsy(); } + // Message History + if ( f.settings.mod_card_history ) { + var Chat = App.__container__.lookup('controller:chat'), + room = Chat && Chat.get('currentRoom'), + ffz_room = room && f.rooms && f.rooms[room.get('id')], + user_history = ffz_room && ffz_room.user_history && ffz_room.user_history[controller.get('cardInfo.user.id')]; + + if ( user_history && user_history.length ) { + var history = document.createElement('ul'), + alternate = false; + history.className = 'interface clearfix chat-history'; + + for(var i=0; i < user_history.length; i++) { + var line = user_history[i], + l_el = document.createElement('li'); + + l_el.className = 'message-line chat-line'; + l_el.classList.toggle('ffz-alternate', alternate); + alternate = !alternate; + + if ( line.style ) + l_el.classList.add(line.style); + + l_el.innerHTML = (helpers ? '' + helpers.getTime(line.date) + ' ' : '') + '' + (line.style === 'action' ? '*' + line.from + ' ' : '') + f.render_tokens(line.cachedTokens) + ''; + + // Banned Links + var bad_links = l_el.querySelectorAll('a.deleted-link'); + for(var x=0; x < bad_links.length; x++) + bad_links[x].addEventListener("click", f._deleted_link_click); + + jQuery('a', l_el).tipsy({html:true}); + history.appendChild(l_el); + } + + el.appendChild(history); + + // Lazy scroll-to-bottom + history.scrollTop = history.scrollHeight; + } + } + // Focus the Element this.$().draggable({ start: function() { @@ -2349,7 +2680,7 @@ FFZ.prototype.setup_mod_card = function() { // Chat Commands // ---------------- -FFZ.chat_commands.purge = FFZ.chat_commands.p = function(room, args) { +FFZ.chat_commands.purge = function(room, args) { if ( ! args || ! args.length ) return "Purge Usage: /p username [more usernames separated by spaces]"; @@ -2363,7 +2694,11 @@ FFZ.chat_commands.purge = FFZ.chat_commands.p = function(room, args) { } } -FFZ.chat_commands.p.enabled = function() { return this.settings.enhanced_moderation; } +FFZ.chat_commands.p = function(room, args) { + return FFZ.chat_commands.purge.bind(this)(room, args); +} + +FFZ.chat_commands.p.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.t = function(room, args) { @@ -2372,7 +2707,7 @@ FFZ.chat_commands.t = function(room, args) { room.room.send("/timeout " + args.join(" ")); } -FFZ.chat_commands.t.enabled = function() { return this.settings.enhanced_moderation; } +FFZ.chat_commands.t.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.b = function(room, args) { @@ -2389,7 +2724,7 @@ FFZ.chat_commands.b = function(room, args) { } } -FFZ.chat_commands.b.enabled = function() { return this.settings.enhanced_moderation; } +FFZ.chat_commands.b.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.u = function(room, args) { @@ -2406,8 +2741,8 @@ FFZ.chat_commands.u = function(room, args) { } } -FFZ.chat_commands.u.enabled = function() { return this.settings.enhanced_moderation; } -},{"../utils":29}],9:[function(require,module,exports){ +FFZ.chat_commands.u.enabled = function() { return this.settings.short_commands; } +},{"../utils":30}],9:[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*['"]([^'"]+)['"][^}]+(?:}|$)/, @@ -2436,18 +2771,33 @@ FFZ.prototype.setup_room = function() { s.id = "ffz-room-css"; document.head.appendChild(s); - this.log("Hooking the Ember Room model."); + this.log("Hooking the Ember Room controller."); // Responsive ban button. - var RC = App.__container__.lookup('controller:room'); + var f = this, + RC = App.__container__.lookup('controller:room'); if ( RC ) { - var orig_action = RC._actions.banUser; + var orig_ban = RC._actions.banUser, + orig_to = RC._actions.timeoutUser; + RC._actions.banUser = function(e) { - orig_action.bind(this)(e); + orig_ban.bind(this)(e); + this.get("model").clearMessages(e.user); + } + + RC._actions.timeoutUser = function(e) { + orig_to.bind(this)(e); + this.get("model").clearMessages(e.user); + } + + RC._actions.purgeUser = function(e) { + this.get("model.tmiRoom").sendMessage("/timeout " + e.user + " 1"); this.get("model").clearMessages(e.user); } } + this.log("Hooking the Ember Room model."); + var Room = App.__container__.resolve('model:room'); this._modify_room(Room); @@ -2463,6 +2813,323 @@ FFZ.prototype.setup_room = function() { this._modify_room(inst); inst.ffzPatchTMI(); } + + this.log("Hooking the Ember Room view."); + + var RoomView = App.__container__.resolve('view:room'); + this._modify_rview(RoomView); + + // For some reason, this doesn't work unless we create an instance of the + // room view and then destroy it immediately. + try { + RoomView.create().destroy(); + } catch(err) { } + + // Modify all existing Room views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof RoomView) ) + continue; + + this.log("Manually updating existing Room view.", view); + try { + view.ffzInit(); + } catch(err) { + this.error("RoomView setup ffzInit: " + err); + } + } +} + + +// -------------------- +// View Customization +// -------------------- + +FFZ.prototype._modify_rview = function(view) { + var f = this; + view.reopen({ + didInsertElement: function() { + this._super(); + + try { + this.ffzInit(); + } catch(err) { + f.error("RoomView didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("RoomView willClearRender: " + err); + } + this._super(); + }, + + ffzInit: function() { + f._roomv = this; + + this.ffz_frozen = false; + + if ( f.settings.chat_hover_pause ) + this.ffzEnableFreeze(); + + if ( f.settings.room_status ) + this.ffzUpdateStatus(); + + var controller = this.get('controller'); + if ( controller ) { + controller.reopen({ + submitButtonText: function() { + if ( this.get("model.isWhisperMessage") && this.get("model.isWhispersEnabled") ) + return i18n("Whisper"); + + var wait = this.get("model.slowWait"), + msg = this.get("model.messageToSend") || ""; + + if ( (msg.charAt(0) === "/" && msg.substr(0, 4) !== "/me ") || !wait || !f.settings.room_status ) + return i18n("Chat"); + + return utils.time_to_string(wait, false, false, true); + }.property("model.isWhisperMessage", "model.isWhispersEnabled", "model.slowWait") + }); + + Ember.propertyDidChange(controller, 'submitButtonText'); + } + }, + + ffzTeardown: function() { + if ( f._roomv === this ) + f._roomv = undefined; + + this.ffzDisableFreeze(); + }, + + ffzUpdateStatus: function() { + var room = this.get('controller.model'), + + el = this.get('element'), + cont = el && el.querySelector('.chat-buttons-container'); + + if ( ! cont ) + return f.log("no container"); + + var r9k_badge = cont.querySelector('#ffz-stat-r9k'), + sub_badge = cont.querySelector('#ffz-stat-sub'), + slow_badge = cont.querySelector('#ffz-stat-slow'), + banned_badge = cont.querySelector('#ffz-stat-banned'), + btn = cont.querySelector('button'); + + if ( f.has_bttv || ! f.settings.room_status ) { + if ( r9k_badge ) + r9k_badge.parentElement.removeChild(r9k_badge); + if ( sub_badge ) + sub_badge.parentElement.removeChild(sub_badge); + if ( slow_badge ) + slow_badge.parentElement.removeChild(slow_badge); + + if ( btn ) + btn.classList.remove('ffz-waiting'); + return; + } + + if ( ! r9k_badge ) { + r9k_badge = document.createElement('span'); + r9k_badge.className = 'ffz room-state stat float-right'; + r9k_badge.id = 'ffz-stat-r9k'; + r9k_badge.innerHTML = 'R9K'; + r9k_badge.title = "This room is in R9K-mode."; + cont.appendChild(r9k_badge); + jQuery(r9k_badge).tipsy({gravity:"s", offset:15}); + } + + if ( ! sub_badge ) { + sub_badge = document.createElement('span'); + sub_badge.className = 'ffz room-state stat float-right'; + sub_badge.id = 'ffz-stat-sub'; + sub_badge.innerHTML = 'SUB'; + sub_badge.title = "This room is in subscribers-only mode."; + cont.appendChild(sub_badge); + jQuery(sub_badge).tipsy({gravity:"s", offset:15}); + } + + if ( ! slow_badge ) { + slow_badge = document.createElement('span'); + slow_badge.className = 'ffz room-state stat float-right'; + slow_badge.id = 'ffz-stat-slow'; + slow_badge.innerHTML = 'SLOW'; + slow_badge.title = "This room is in slow mode. You may send messages every 120 seconds."; + cont.appendChild(slow_badge); + jQuery(slow_badge).tipsy({gravity:"s", offset:15}); + } + + if ( ! banned_badge ) { + banned_badge = document.createElement('span'); + banned_badge.className = 'ffz room-state stat float-right'; + banned_badge.id = 'ffz-stat-banned'; + banned_badge.innerHTML = 'BAN'; + banned_badge.title = "You have been banned from talking in this room."; + cont.appendChild(banned_badge); + jQuery(banned_badge).tipsy({gravity:"s", offset:15}); + } + + r9k_badge.classList.toggle('hidden', !(room && room.get('r9kMode'))); + sub_badge.classList.toggle('hidden', !(room && room.get('subsOnlyMode'))); + slow_badge.classList.toggle('hidden', !(room && room.get('slowMode'))); + slow_badge.title = "This room is in slow mode. You may send messages every " + utils.number_commas(room && room.get('slowValue')||120) + " seconds."; + banned_badge.classList.toggle('hidden', !(room && room.get('ffz_banned'))); + + if ( btn ) { + btn.classList.toggle('ffz-waiting', (room && room.get('slowWait') || 0)); + btn.classList.toggle('ffz-banned', (room && room.get('ffz_banned'))); + } + + }.observes('controller.model'), + + ffzEnableFreeze: function() { + var el = this.get('element'), + messages = el.querySelector('.chat-messages'); + + if ( ! messages ) + return; + + this._ffz_interval = setInterval(this.ffzPulse.bind(this), 200); + this._ffz_messages = messages; + this._ffz_mouse_move = this.ffzMouseMove.bind(this); + this._ffz_mouse_out = this.ffzMouseOut.bind(this); + + messages.addEventListener('mousemove', this._ffz_mouse_move); + messages.addEventListener('mouseout', this._ffz_mouse_out); + document.addEventListener('mouseout', this._ffz_mouse_out); + }, + + ffzDisableFreeze: function() { + if ( this._ffz_interval ) { + clearInterval(this._ffz_interval); + this._ffz_interval = undefined; + } + + this.ffzUnfreeze(); + + var messages = this._ffz_messages; + if ( ! messages ) + return; + + this._ffz_messages = undefined; + + if ( this._ffz_mouse_move ) { + messages.removeEventListener('mousemove', this._ffz_mouse_move); + this._ffz_mouse_move = undefined; + } + + if ( this._ffz_mouse_out ) { + messages.removeEventListener('mouseout', this._ffz_mouse_out); + this._ffz_mouse_out = undefined; + } + }, + + ffzPulse: function() { + if ( this.ffz_frozen ) { + var elapsed = Date.now() - this._ffz_last_move; + if ( elapsed > 750 ) + this.ffzUnfreeze(); + } + }, + + ffzUnfreeze: function() { + this.ffz_frozen = false; + this._ffz_last_move = 0; + this.ffzUnwarnPaused(); + + if ( this.get('stuckToBottom') ) + this._scrollToBottom(); + }, + + ffzMouseOut: function(event) { + this._ffz_outside = true; + var e = this; + Ember.run.next(function() { + if ( e._ffz_outside ) + e.ffzUnfreeze(); + }); + }, + + ffzMouseMove: function(event) { + this._ffz_last_move = Date.now(); + this._ffz_outside = false; + + if ( event.screenX === this._ffz_last_screenx && event.screenY === this._ffz_last_screeny ) + return; + + this._ffz_last_screenx = event.screenX; + this._ffz_last_screeny = event.screenY; + + if ( this.ffz_frozen ) + return; + + // Don't do it if we're over the bar itself. + if ( event.clientY >= (this._ffz_messages.getBoundingClientRect().bottom - 21) ) + return; + + this.ffz_frozen = true; + if ( this.get('stuckToBottom') ) { + this.set('controller.model.messageBufferSize', f.settings.scrollback_length + 150); + this.ffzWarnPaused(); + } + }, + + _scrollToBottom: _.throttle(function() { + var e = this, + s = this._$chatMessagesScroller; + + Ember.run.next(function() { + setTimeout(function() { + !e.ffz_frozen && s && s.length && (s.scrollTop(s[0].scrollHeight), e._setStuckToBottom(!0)); + }) + }) + }, 200), + + _setStuckToBottom: function(val) { + this.set("stuckToBottom", val); + this.get("controller.model") && this.set("controller.model.messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150)); + }, + + // Warnings~! + ffzWarnPaused: function() { + var el = this.get('element'), + warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator'); + + if ( ! el ) + return; + + if ( ! warning ) { + warning = document.createElement('div'); + warning.className = 'more-messages-indicator ffz-freeze-indicator'; + warning.innerHTML = '(Chat Paused Due to Mouse Movement)'; + + var cont = el.querySelector('.chat-interface'); + if ( ! cont ) + return; + cont.insertBefore(warning, cont.childNodes[0]) + } + + warning.classList.remove('hidden'); + }, + + + ffzUnwarnPaused: function() { + var el = this.get('element'), + warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator'); + + if ( warning ) + warning.classList.add('hidden'); + } + + }); } @@ -2606,6 +3273,27 @@ FFZ.prototype.add_room = function(id, room) { // Create a basic data table for this room. var data = this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null, needs_history: false}; + if ( this.follow_sets && this.follow_sets[id] ) { + data.extra_sets = this.follow_sets[id]; + delete this.follow_sets[id]; + + for(var i=0; i < data.extra_sets.length; i++) { + var sid = data.extra_sets[i], + set = this.emote_sets && this.emote_sets[sid]; + + if ( set ) { + if ( set.users.indexOf(id) === -1 ) + set.users.push(id); + continue; + } + + this.load_set(sid, function(success, data) { + if ( success ) + data.users.push(id); + }); + } + } + // Let the server know where we are. this.ws_send("sub", id); @@ -2615,6 +3303,9 @@ FFZ.prototype.add_room = function(id, room) { data.needs_history = true; } + // Why don't we set the scrollback length, too? + room.set('messageBufferSize', this.settings.scrollback_length + ((this._roomv && !this._roomv.get('stuckToBottom') && this._roomv.get('controller.model.id') === id) ? 150 : 0)); + // For now, we use the legacy function to grab the .css file. this.load_room(id); } @@ -2822,6 +3513,12 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( this.rooms[room_id] ) data.room = this.rooms[room_id].room; + // Preserve everything else. + for(var key in this.rooms[room_id]) { + if ( key !== 'room' && this.rooms[room_id].hasOwnProperty(key) && ! data.hasOwnProperty(key) ) + data[key] = this.rooms[room_id][key]; + } + data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false; this.rooms[room_id] = data; @@ -2830,7 +3527,12 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); if ( ! this.emote_sets.hasOwnProperty(data.set) ) - this.load_set(data.set); + this.load_set(data.set, function(success, set) { + if ( set.users.indexOf(room_id) === -1 ) + set.users.push(room_id); + }); + else if ( this.emote_sets[data.set].users.indexOf(room_id) === -1 ) + this.emote_sets[data.set].users.push(room_id); this.update_ui_link(); @@ -2846,6 +3548,42 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { FFZ.prototype._modify_room = function(room) { var f = this; room.reopen({ + subsOnlyMode: false, + r9kMode: false, + slowWaiting: false, + slowValue: 0, + + updateWait: function(value, was_banned) { + var wait = this.get('slowWait') || 0; + this.set('slowWait', value); + if ( wait < 1 && value > 0 ) { + setTimeout(this.ffzUpdateWait.bind(this), 1000); + f._roomv && f._roomv.ffzUpdateStatus(); + } else if ( (wait > 0 && value < 1) || was_banned ) { + this.set('ffz_banned', false); + f._roomv && f._roomv.ffzUpdateStatus(); + } + }, + + ffzUpdateWait: function() { + var wait = this.get('slowWait') || 0; + if ( wait < 1 ) + return; + + this.set('slowWait', --wait); + if ( wait > 0 ) + setTimeout(this.ffzUpdateWait.bind(this), 1000); + else { + this.set('ffz_banned', false); + f._roomv && f._roomv.ffzUpdateStatus(); + } + }, + + ffzUpdateStatus: function() { + if ( f._roomv ) + f._roomv.ffzUpdateStatus(); + }.observes('r9kMode', 'subsOnlyMode', 'slowMode', 'slowValue', 'ffz_banned'), + // Track which rooms the user is currently in. init: function() { this._super(); @@ -2876,6 +3614,23 @@ FFZ.prototype._modify_room = function(room) { t.set("messages." + n + ".deleted", true); } }); + + if ( f.settings.mod_card_history ) { + var room = f.rooms && f.rooms[t.get('id')], + user_history = room && room.user_history && room.user_history[user] + + if ( user_history !== null && user_history !== undefined ) { + var has_delete = false, + last = user_history.length > 0 ? user_history[user_history.length-1] : null; + + has_delete = last !== null && last.is_delete; + if ( ! has_delete ) { + user_history.push({from: 'jtv', is_delete: true, style: 'admin', cachedTokens: ['User has been timed out.'], date: new Date()}); + while ( user_history.length > 20 ) + user_history.shift(); + } + } + } } else { if ( f.settings.prevent_clear ) this.addTmiMessage("A moderator's attempt to clear chat was ignored."); @@ -2914,6 +3669,39 @@ FFZ.prototype._modify_room = function(room) { msg.room = this.get('id'); f.tokenize_chat_line(msg); + + // Keep the history. + if ( ! is_whisper && msg.from && msg.from !== 'jtv' && msg.from !== 'twitchnotify' && f.settings.mod_card_history ) { + var room = f.rooms && f.rooms[msg.room]; + if ( room ) { + var chat_history = room.user_history = room.user_history || {}, + user_history = room.user_history[msg.from] = room.user_history[msg.from] || []; + + user_history.push({ + from: msg.tags && msg.tags['display-name'] || msg.from, + cachedTokens: msg.cachedTokens, + style: msg.style, + date: msg.date + }); + while ( user_history.length > 20 ) + user_history.shift(); + } + } + + // Check for message from us. + if ( ! is_whisper ) { + var user = f.get_user(); + if ( user && user.login === msg.from ) { + var was_banned = this.get('ffz_banned'); + this.set('ffz_banned', false); + + // Update the wait time. + if ( this.get('isModeratorOrHigher') || ! this.get('slowMode') ) + this.updateWait(0, was_banned) + else if ( this.get('slowMode') ) + this.updateWait(this.get('slowValue')); + } + } } } catch(err) { f.error("Room addMessage: " + err); @@ -2988,6 +3776,7 @@ FFZ.prototype._modify_room = function(room) { }); }, + ffzUpdateChatters: function(add, remove) { var chatters = this.get("ffz_chatters") || {}; if ( add ) @@ -3001,8 +3790,10 @@ FFZ.prototype._modify_room = function(room) { if ( f._cindex ) f._cindex.ffzUpdateChatters(); - if ( window.parent && window.parent.postMessage ) - window.parent.postMessage({from_ffz: true, command: 'chatter_count', message: Object.keys(this.get('ffz_chatters') || {}).length}, "http://www.twitch.tv/"); + try { + if ( window.parent && window.parent.postMessage ) + window.parent.postMessage({from_ffz: true, command: 'chatter_count', message: Object.keys(this.get('ffz_chatters') || {}).length}, "http://www.twitch.tv/"); + } catch(err) { /* Ignore errors because of security */ } }, @@ -3016,24 +3807,8 @@ FFZ.prototype._modify_room = function(room) { var tmi = this.get('tmiRoom'), room = this; - // This method is stupid and bad and it leaks between rooms. - if ( ! tmi.ffz_notice_patched ) { - tmi.ffz_notice_patched = true; - - tmi._roomConn.off("notice", tmi._onNotice, tmi); - tmi._roomConn.on("notice", function(ircMsg) { - var target = ircMsg.target || (ircMsg.params && ircMsg.params[0]) || this.ircChannel; - if( target != this.ircChannel ) - return; - - this._trigger("notice", { - msgId: ircMsg.tags['msg-id'], - message: ircMsg.message - }); - }, tmi); - } - // Let's get chatter information! + // TODO: Remove this cause it's terrible. var connection = tmi._roomConn._connection; if ( ! connection.ffz_cap_patched ) { connection.ffz_cap_patched = true; @@ -3042,18 +3817,83 @@ FFZ.prototype._modify_room = function(room) { connection.on("opened", function() { this._send("CAP REQ :twitch.tv/membership"); }, connection); - - // Since TMI starts sending SPECIALUSER with this, we need to - // ignore that. \ CatBag / - var orig_handle = connection._handleTmiPrivmsg.bind(connection); - connection._handleTmiPrivmsg = function(msg) { - if ( msg.message && msg.message.split(" ",1)[0] === "SPECIALUSER" ) - return; - return orig_handle(msg); - } } + // NOTICE for catching slow-mode updates + tmi.on('notice', function(msg) { + if ( msg.msgId === 'msg_slowmode' ) { + var match = /in (\d+) seconds/.exec(msg.message); + if ( match ) { + room.updateWait(parseInt(match[1])); + } + } + + if ( msg.msgId === 'msg_timedout' ) { + var match = /for (\d+) more seconds/.exec(msg.message); + if ( match ) { + room.set('ffz_banned', true); + room.updateWait(parseInt(match[1])); + } + } + + if ( msg.msgId === 'msg_banned' ) { + room.set('ffz_banned', true); + f._roomv && f._roomv.ffzUpdateStatus(); + } + }); + + + // ROOMSTATE~! + if ( ! connection.ffz_roomstate_patched ) { + connection.ffz_roomstate_patched = true; + connection._socket.off('data', connection._onSocketDataReceived, connection); + connection._socket.on('data', function(data) { + try { + var msg = utils.splitIRCMessage(data.data); + if ( msg.command === 'ROOMSTATE' ) { + // We have ROOMSTATE! Now, let's parse it a bit + // more and send it on. + msg.tags = utils.parseIRCTags(msg.tags); + msg.target = msg.params && msg.params[0]; + + this._trigger('roomstate', msg); + return; + } + } catch(err) { f.error("Connection onData: " + err); } + + return this._onSocketDataReceived(data); + }, connection); + } + + // Glorious ROOMSTATE. + if ( ! tmi.ffz_roomstate_patched ) { + tmi.ffz_roomstate_patched = true; + tmi._roomConn.on("roomstate", function(ircMsg) { + if ( ircMsg.target !== this.ircChannel ) + return; + + this._trigger("roomstate", ircMsg.tags); + }, tmi); + } + + // IT IS GLORIOUS! + tmi.on('roomstate', function(state) { + if ( state.hasOwnProperty('slow') ) { + room.set('slowMode', state.slow > 0); + room.set('slowValue', state.slow); + if ( ! room.get('slowMode') ) + room.updateWait(0); + } + + if ( state.hasOwnProperty('r9k') ) + room.set('r9kMode', state.r9k); + + if ( state.hasOwnProperty('subs-only') ) + room.set('subsOnlyMode', state['subs-only']); + }); + + // Check this shit. tmi._roomConn._connection.off("message", tmi._roomConn._onIrcMessage, tmi._roomConn); @@ -3085,38 +3925,12 @@ FFZ.prototype._modify_room = function(room) { tmi._roomConn._connection.on("message", tmi._roomConn._onIrcMessage, tmi._roomConn); - - // Okay, we need to patch the *session's* updateUserState - if ( ! tmi.session.ffz_patched ) { - tmi.session.ffz_patched = true; - var uus = tmi.session._updateUserState.bind(tmi.session); - - tmi.session._updateUserState = function(user, tags) { - try { - if ( tags.color ) - this._onUserColorChanged(user, tags.color); - - if ( tags['display-name'] ) - this._onUserDisplayNameChanged(user, tags['display-name']); - - if ( tags.turbo ) - this._onUserSpecialAdded(user, 'turbo'); - - if ( tags['user_type'] === 'staff' || tags['user_type'] === 'admin' || tags['user_type'] === 'global_mod' ) - this._onUserSpecialAdded(user, tags['user-type']); - - } catch(err) { - f.error("SessionManager _updateUserState: " + err); - } - } - } - this.set('ffz_is_patched', true); }.observes('tmiRoom') }); } -},{"../constants":3,"../utils":29}],10:[function(require,module,exports){ +},{"../constants":3,"../utils":30}],10:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -3274,6 +4088,7 @@ var FFZ = window.FrankerFaceZ, FFZ.prototype.setup_emoticons = function() { this.log("Preparing emoticon system."); + this.emoji_data = {}; this.emote_sets = {}; this.global_sets = []; this.default_sets = []; @@ -3291,6 +4106,9 @@ FFZ.prototype.setup_emoticons = function() { this.log("Loading global emote sets."); this.load_global_sets(); + this.log("Loading emoji data."); + this.load_emoji_data(); + this.log("Watching Twitch emoticon parser to ensure it loads."); this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); } @@ -3372,7 +4190,7 @@ FFZ.prototype.getEmotes = function(user_id, room_id) { var user = this.users && this.users[user_id], room = this.rooms && this.rooms[room_id]; - return _.union(user && user.sets || [], room && room.set && [room.set] || [], this.default_sets); + return _.union(user && user.sets || [], room && room.set && [room.set] || [], room && room.extra_sets || [], this.default_sets); } @@ -3411,6 +4229,49 @@ FFZ.prototype._emote_tooltip = function(emote) { } +// --------------------- +// Emoji Loading +// --------------------- + +FFZ.prototype.load_emoji_data = function(callback, tries) { + var f = this; + jQuery.getJSON(constants.SERVER + "emoji/emoji.json") + .done(function(data) { + var new_data = {}; + for(var eid in data) { + var emoji = data[eid]; + eid = eid.toLowerCase(); + new_data[eid] = emoji; + + emoji.src = constants.SERVER + 'emoji/' + eid + '-1x.png'; + emoji.srcSet = emoji.src + ' 1x, ' + constants.SERVER + 'emoji/' + eid + '-2x.png 2x, ' + constants.SERVER + 'emoji/' + eid + '-4x.png 4x'; + + emoji.token = { + srcSet: emoji.srcSet, + emoticonSrc: emoji.src + '" data-ffz-emoji="' + eid + '" height="18px', + ffzEmoji: eid, + }; + + } + + f.emoji_data = new_data; + f.log("Loaded data on " + Object.keys(new_data).length + " emoji."); + if ( typeof callback === "function" ) + callback(true, data); + + }).fail(function(data) { + if ( data.status === 404 ) + return typeof callback === "function" && callback(false); + + tries = (tries || 0) + 1; + if ( tries < 50 ) + return f.load_emoji(callback, tries); + + return typeof callback === "function" && callback(false); + }); +} + + // --------------------- // Set Loading // --------------------- @@ -3481,9 +4342,12 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { if ( ! data ) return typeof callback == "function" && callback(false); + // Do we have existing users? + var users = this.emote_sets[set_id] && this.emote_sets[set_id].users || []; + // Store our set. this.emote_sets[set_id] = data; - data.users = []; + data.users = users; data.count = 0; @@ -3526,8 +4390,10 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { if ( callback ) callback(true, data); } -},{"./constants":3,"./utils":29}],12:[function(require,module,exports){ +},{"./constants":3,"./utils":30}],12:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'), SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; @@ -3566,6 +4432,16 @@ FFZ.prototype.setup_bttv = function(delay) { this._chatv.ffzDisableTabs(); } + if ( this._roomv ) { + // Disable Chat Pause + if ( this.settings.chat_hover_pause ) + this._roomv.ffzDisableFreeze(); + + // And hide the status + if ( this.settings.room_status ) + this._roomv.ffzUpdateStatus(); + } + // Disable other features too. document.body.classList.remove("ffz-chat-colors"); document.body.classList.remove("ffz-chat-background"); @@ -3574,6 +4450,7 @@ FFZ.prototype.setup_bttv = function(delay) { if ( this.is_dashboard ) this._update_subscribers(); + document.body.classList.add('ffz-bttv'); // Send Message Behavior var original_send = BetterTTV.chat.helpers.sendMessage, f = this; @@ -3620,6 +4497,26 @@ FFZ.prototype.setup_bttv = function(delay) { } } + // Whispers too! + var original_whisper = BetterTTV.chat.templates.whisper; + BetterTTV.chat.templates.whisper = function(data) { + try { + // Handle badges. + f.bttv_badges(data); + + // Now, do everything else manually because things are hard-coded. + return '
' + + BetterTTV.chat.templates.timestamp(data.time) + ' ' + + (data.badges && data.badges.length ? BetterTTV.chat.templates.badges(data.badges) : '') + + BetterTTV.chat.templates.whisperName(data.sender, data.receiver, data.from, data.to, data.fromColor, data.toColor) + + BetterTTV.chat.templates.message(data.sender, data.message, data.emotes, false) + + '
'; + } catch(err) { + f.log("Error: ", err); + return original_whisper(data); + } + } + // Message Renderer. I had to completely rewrite this method to get it to // use my replacement emoticonizer. var original_message = BetterTTV.chat.templates.message, @@ -3680,53 +4577,87 @@ FFZ.prototype.setup_bttv = function(delay) { }); // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; + if ( emotes.length ) { + // Why is emote parsing so bad? ;_; + _.each(emotes, function(emote) { + var tooltip = f._emote_tooltip(emote), + eo = ['' + tooltip + ''], + old_tokens = tokens; - // Why is emote parsing so bad? ;_; - _.each(emotes, function(emote) { - var tooltip = f._emote_tooltip(emote), - eo = ['' + tooltip + ''], - old_tokens = tokens; + 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); + while(tbits.length) { + var bit = tbits.shift(); + if ( tbits.length ) { + bit += tbits.shift(); + if ( bit ) + tokens.push(bit); + + tbits.shift(); + tokens.push(eo); + + if ( mine && l_room ) + f.add_usage(l_room, emote.id); + + } else + tokens.push(bit); + } + } + }); + } + + // Sneak in Emojicon Processing + if ( f.settings.parse_emoji && f.emoji_data ) { + var 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" ) { + if ( typeof token !== "string" ) { tokens.push(token); continue; } - var tbits = token.split(emote.regex); + var tbits = token.split(constants.EMOJI_REGEX); while(tbits.length) { var bit = tbits.shift(); + bit && tokens.push(bit); + if ( tbits.length ) { - bit += tbits.shift(); - if ( bit ) - tokens.push(bit); + var match = tbits.shift(), + variant = tbits.shift(); - tbits.shift(); - tokens.push(eo); + if ( variant === '\uFE0E' ) + bits.push(match); + else { + var eid = utils.emoji_to_codepoint(match, variant), + data = f.emoji_data[eid], + alt = match + (variant || ""); - if ( mine && l_room ) - f.add_usage(l_room, emote.id); - - } else - tokens.push(bit); + if ( data ) { + tokens.push(['' + alt + '']); + } else + tokens.push(match + (variant || "")); + } + } } } - }); + } return tokens; } this.update_ui_link(); } -},{}],13:[function(require,module,exports){ +},{"../constants":3,"../utils":30}],13:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -3826,7 +4757,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 4, revision: 2, + major: 3, minor: 4, revision: 10, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -3907,8 +4838,6 @@ FFZ.prototype.get_user = function() { // Import Everything! // ------------------- -//require('./templates'); - // Import these first to set up data structures require('./ui/menu'); require('./settings'); @@ -3945,6 +4874,7 @@ require('./ui/viewer_count'); require('./ui/sub_count'); require('./ui/menu_button'); +require('./ui/following'); require('./ui/races'); require('./ui/my_emotes'); require('./ui/about_page'); @@ -3995,6 +4925,10 @@ FFZ.prototype.setup_normal = function(delay) { this.log("Found non-Ember Twitch after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; + this.is_dashboard = false; + try { + this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname); + } catch(err) { this.embed_in_dash = false; } // Initialize all the modules. this.load_settings(); @@ -4027,6 +4961,7 @@ FFZ.prototype.setup_dashboard = function(delay) { this.users = {}; this.is_dashboard = true; + this.embed_in_dash = false; // Initialize all the modules. this.load_settings(); @@ -4060,6 +4995,10 @@ FFZ.prototype.setup_ember = function(delay) { this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; + this.is_dashboard = false; + try { + this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname); + } catch(err) { this.embed_in_dash = false; } // Initialize all the modules. this.load_settings(); @@ -4087,6 +5026,7 @@ FFZ.prototype.setup_ember = function(delay) { this.setup_css(); this.setup_menu(); this.setup_my_emotes(); + this.setup_following(); this.setup_races(); this.connect_extra_chat(); @@ -4118,9 +5058,8 @@ FFZ.prototype._on_window_message = function(e) { return; var msg = e.data; - this.log("Window Message", msg); } -},{"./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(require,module,exports){ +},{"./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/following":21,"./ui/menu":22,"./ui/menu_button":23,"./ui/my_emotes":24,"./ui/notifications":25,"./ui/races":26,"./ui/styles":27,"./ui/sub_count":28,"./ui/viewer_count":29}],15:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); @@ -4633,8 +5572,10 @@ FFZ.prototype.ws_create = function() { // Join the right channel if we're in the dashboard. if ( f.is_dashboard ) { var match = location.pathname.match(/\/([^\/]+)/); - if ( match ) + if ( match ) { f.ws_send("sub", match[1]); + f.ws_send("sub_channel", match[1]); + } } // Send the current rooms. @@ -4651,6 +5592,18 @@ FFZ.prototype.ws_create = function() { } } + // Send the channel(s). + if ( f._cindex ) { + var channel_id = f._cindex.get('controller.id'), + hosted_id = f._cindex.get('controller.hostModeTarget.id'); + + if ( channel_id ) + f.ws_send("sub_channel", channel_id); + + if ( hosted_id ) + f.ws_send("sub_channel", hosted_id); + } + // Send any pending commands. var pending = f._ws_pending; f._ws_pending = []; @@ -4813,6 +5766,7 @@ FFZ.ws_commands.do_authorize = function(data) { },{}],18:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("./utils"), + constants = require("./constants"), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", helpers, @@ -4823,11 +5777,28 @@ var FFZ = window.FrankerFaceZ, SEPARATORS = "[\\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]", SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"); + try { helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); } catch(err) { } +FFZ.SRC_IDS = {}, +FFZ.src_to_id = function(src) { + if ( FFZ.SRC_IDS.hasOwnProperty(src) ) + return FFZ.SRC_IDS[src]; + + var match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(src), + id = match ? parseInt(match[1]) : null; + + if ( id === NaN ) + id = null; + + FFZ.SRC_IDS[src] = id; + return id; +}; + + // --------------------- // Tokenization // --------------------- @@ -4854,6 +5825,9 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { tokens = this._remove_banned(tokens); tokens = this.tokenize_emotes(msgObject.from, room_id, tokens, from_me); + if ( this.settings.parse_emoji ) + tokens = this.tokenize_emoji(tokens); + // Capitalization var display = msgObject.tags && msgObject.tags['display-name']; if ( display && display.length ) @@ -4866,7 +5840,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { for(var i=0; i < tokens.length; i++) { var token = tokens[i]; - if ( _.isString(token) || ! token.mentionedUser || token.own || msgObject.style === 'whisper' ) + if ( msgObject.style !== 'whisper' && (_.isString(token) || ! token.mentionedUser || token.own) ) continue; // We have a mention! @@ -4882,7 +5856,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { // 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() && ! prevent_notification ) { + if ( this._chatv && this.settings.highlight_notifications && ! this.embed_in_dash && ! document.hasFocus() && ! prevent_notification ) { var room = this.rooms[room_id] && this.rooms[room_id].room, room_name; @@ -4899,17 +5873,28 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { msg = display + ': ' + msg; var f = this; - this.show_notification( - msg, - "Twitch Chat Mention in " + room_name, - room_id, - 60000, - function() { - window.focus(); - var cont = App.__container__.lookup('controller:chat'); - room && cont && cont.focusRoom(room); - } - ); + if ( msgObject.style === 'whisper' ) + this.show_notification( + msg, + "Twitch Chat Whisper", + "ffz_whisper_notice", + 60000, + function() { + window.focus(); + } + ); + else + this.show_notification( + msg, + "Twitch Chat Mention in " + room_name, + room_id, + 60000, + function() { + window.focus(); + var cont = App.__container__.lookup('controller:chat'); + room && cont && cont.focusRoom(room); + } + ); } break; @@ -4942,9 +5927,31 @@ FFZ.prototype.tokenize_line = function(user, room, message, no_emotes) { FFZ.prototype.render_tokens = function(tokens, render_links) { + var f = this; return _.map(tokens, function(token) { - if ( token.emoticonSrc ) - return '' + token.altText + ''; + if ( token.emoticonSrc ) { + var tooltip; + if ( token.ffzEmote ) { + var emote_set = f.emote_sets && f.emote_sets[token.ffzEmoteSet], + emote = emote_set && emote_set.emoticons && emote_set.emoticons[token.ffzEmote]; + + tooltip = emote ? utils.sanitize(f._emote_tooltip(emote)) : token.altText; + + } else if ( token.ffzEmoji ) { + var eid = token.ffzEmoji, + emoji = f.emoji_data && f.emoji_data[eid]; + + tooltip = emoji ? "Emoji: " + token.altText + "\nName: " + emoji.short_name : token.altText; + + } else { + var id = FFZ.src_to_id(token.emoticonSrc), + data = id && f._twitch_emotes && f._twitch_emotes[id]; + + tooltip = data && data.tooltip ? data.tooltip : token.altText; + } + + return ''; + } if ( token.isLink ) { if ( ! render_links && render_links !== undefined ) @@ -4955,7 +5962,11 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { return '' + s + ''; var n = (s.match(/^https?:\/\//) ? "" : "http://") + s; - return '' + s + ''; + + // Check for link data. + var data = f._link_data && f._link_data[n] || {}; + + return '' + s + ''; } if ( token.mentionedUser ) @@ -5070,7 +6081,7 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { return tokens; // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens == "string" ) + if ( typeof tokens === "string" ) tokens = [tokens]; // This is weird stuff I basically copied from the old Twitch code. @@ -5082,6 +6093,8 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { var eo = { srcSet: emote.srcSet, emoticonSrc: emote.urls[1] + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), + ffzEmote: emote.id, + ffzEmoteSet: emote.set_id, altText: (emote.hidden ? "???" : emote.name) }; @@ -5114,6 +6127,58 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { } +// --------------------- +// Emoji Processing +// --------------------- + +FFZ.prototype.tokenize_emoji = function(tokens) { + if ( typeof tokens === "string" ) + tokens = [tokens]; + + if ( ! this.emoji_data ) + return tokens; + + var f = this; + + return _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(constants.EMOJI_REGEX), bits = []; + while(tbits.length) { + // Deal with the unmatched string first. + var bit = tbits.shift(); + bit && bits.push(bit); + + if ( tbits.length ) { + // We have an emoji too, so let's handle that. + var match = tbits.shift(), + variant = tbits.shift(); + + if ( variant === '\uFE0E' ) { + // Text Variant + bits.push(match); + + } else { + // Find the right image~! + var eid = utils.emoji_to_codepoint(match, variant), + data = f.emoji_data[eid], + alt = match + (variant || ""); + + if ( data ) { + data.token.altText = alt; + bits.push(data.token); + } else + bits.push(alt); + } + } + } + + return bits; + }))); +} + + // --------------------- // Mention Parsing // --------------------- @@ -5181,7 +6246,50 @@ FFZ.prototype.tokenize_mentions = function(tokens) { return new_tokens; } -},{"./utils":29}],19:[function(require,module,exports){ + + +// --------------------- +// Handling Bad Stuff +// --------------------- + +FFZ.prototype._deleted_link_click = function(e) { + if ( ! this.classList.contains("deleted-link") ) + return true; + + // Get the URL + var href = this.getAttribute('data-url'), + link = href, + f = FrankerFaceZ.get(); + + // Delete Old Stuff + this.classList.remove('deleted-link'); + this.removeAttribute("data-url"); + this.removeAttribute("title"); + this.removeAttribute("original-title"); + + // Process URL + if ( href.indexOf("@") > -1 && (-1 === href.indexOf("/") || href.indexOf("@") < href.indexOf("/")) ) + href = "mailto:" + href; + else if ( ! href.match(/^https?:\/\//) ) + href = "http://" + href; + + // Set up the Link + this.href = href; + this.target = "_new"; + this.textContent = link; + + // Now, check for a tooltip. + var link_data = f._link_data[link]; + if ( link_data && typeof link_data != "boolean" ) { + this.title = link_data.tooltip; + if ( link_data.unsafe ) + this.classList.add('unsafe-link'); + } + + // Stop from Navigating + e.preventDefault(); +} +},{"./constants":3,"./utils":30}],19:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"); @@ -5356,10 +6464,415 @@ FFZ.prototype._load_dark_css = function() { s.id = "ffz-dark-css"; s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.SERVER + "script/dark.css?_=" + Date.now()); + s.setAttribute('href', constants.SERVER + "script/dark.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); } },{"../constants":3}],21:[function(require,module,exports){ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + + EMOTE_CHANNELS = { + sirstendec: true, + europeanspeedsterassembly: true, + esamarathon2: true + }; + + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.setup_following = function() { + this.log("Initializing following support."); + this.follow_data = {}; + this.follow_sets = {}; +} + + +// --------------- +// Settings +// --------------- + +FFZ.settings_info.follow_buttons = { + type: "boolean", + value: true, + + category: "Channel Metadata", + name: "Relevant Follow Buttons", + help: 'Display additional Follow buttons for channels relevant to the stream, such as people participating in co-operative gameplay.', + on_update: function(val) { + this.rebuild_following_ui(); + } + }; + + +// --------------- +// Command +// --------------- + +FFZ.ffz_commands.following = function(room, args) { + args = args.join(" ").trim().split(/\s*,+\s*/); + + if ( args.length && args[0] === '' ) + args.shift(); + + if ( args.length && args[args.length-1] === '' ) + args.pop(); + + var user = this.get_user(), f = this; + if ( ! user || (user.login !== room.id && user.login !== "sirstendec" && user.login !== "dansalvato") ) + return "You must be logged in as the broadcaster to use this command."; + + if ( ! this.ws_send("update_follow_buttons", [room.id, args], function(success, data) { + if ( ! success ) { + f.room_message(room, "There was an error updating the following buttons."); + return; + } + + if ( data ) + f.room_message(room, "The following buttons have been updated."); + else + f.room_message(room, "The following buttons have been disabled."); + }) ) + return "There was an error communicating with the server."; +} + + +// --------------- +// Socket Handler +// --------------- + +FFZ.ws_on_close.push(function() { + var controller = window.App && App.__container__.lookup('controller:channel'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), + need_update = false; + + this.follow_sets = {}; + + if ( ! controller ) + return; + + for(var channel_id in this.follow_data) { + delete this.follow_data[channel_id]; + if ( channel_id === current_id || channel_id === current_host ) + need_update = true; + + if ( this.rooms && this.rooms[channel_id] && this.rooms[channel_id].extra_sets ) { + var sets = this.rooms[channel_id].extra_sets; + delete this.rooms[channel_id].extra_sets; + + for(var i=0; i < sets.length; i++) { + var set = this.emote_sets[sets[i]]; + if ( set ) { + set.users.removeObject(channel_id); + if ( ! this.global_sets.contains(sets[i]) && ! set.users.length ) + this.unload_set(sets[i]); + } + } + } + } + + if ( need_update ) + this.rebuild_following_ui(); +}); + + +FFZ.ws_commands.follow_buttons = function(data) { + var controller = window.App && App.__container__.lookup('controller:channel'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), + need_update = false; + + this.follow_data = this.follow_data || {}; + + for(var channel_id in data) { + this.follow_data[channel_id] = data[channel_id]; + if ( channel_id === current_id || channel_id === current_host ) + need_update = true; + } + + if ( need_update ) + this.rebuild_following_ui(); +} + + +FFZ.ws_commands.follow_sets = function(data) { + var controller = App.__container__.lookup('controller:channel'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), + need_update = false; + + this.follow_sets = this.follow_sets || {}; + + for(var room_id in data) { + if ( ! this.rooms || ! this.rooms.hasOwnProperty(room_id) ) { + this.follow_sets[room_id] = data[room_id]; + continue; + } + + var old_sets = this.rooms[room_id].extra_sets || [], + new_sets = this.rooms[room_id].extra_sets = data[room_id]; + + // Unload sets we aren't using anymore. + for(var i=0; i < old_sets.length; i++) { + var sid = old_sets[i]; + if ( new_sets.indexOf(sid) !== -1 ) + continue; + + var set = this.emote_sets && this.emote_sets[sid]; + if ( set ) { + set.users.removeObject(room_id); + if ( ! this.global_sets.contains(sid) && ! set.users.length ) + this.unload_set(sid); + } + } + + // And load the new sets. + for(var i=0; i < new_sets.length; i++) { + var sid = new_sets[i], + set = this.emote_sets && this.emote_sets[sid]; + + if ( set ) { + if ( set.users.indexOf(room_id) === -1 ) + set.users.push(room_id); + continue; + } + + this.load_set(sid, function(success, data) { + if ( success ) + data.users.push(room_id); + }); + } + } +} + + +// --------------- +// Following UI +// --------------- + +FFZ.prototype.rebuild_following_ui = function() { + var controller = App.__container__.lookup('controller:channel'), + channel_id = controller && controller.get('id'), + hosted_id = controller && controller.get('hostModeTarget.id'); + + if ( ! this._cindex ) + return; + + if ( channel_id ) { + var data = this.follow_data && this.follow_data[channel_id], + + el = this._cindex.get('element'), + container = el && el.querySelector('.stats-and-actions .channel-actions'), + cont = container && container.querySelector('#ffz-ui-following'); + + if ( ! container || ! this.settings.follow_buttons || ! data || ! data.length ) { + if ( cont ) + cont.parentElement.removeChild(cont); + + } else { + if ( ! cont ) { + cont = document.createElement('span'); + cont.id = 'ffz-ui-following'; + + var before = container.querySelector(':scope > span'); + if ( before ) + container.insertBefore(cont, before); + else + container.appendChild(cont); + } else + cont.innerHTML = ''; + + for(var i=0; i < data.length; i++) { + this._build_following_button(cont, data[i]); + } + } + } + + + if ( hosted_id ) { + var data = this.follow_data && this.follow_data[hosted_id], + + el = this._cindex.get('element'), + container = el && el.querySelector('#hostmode .channel-actions'), + cont = container && container.querySelector('#ffz-ui-following'); + + if ( ! container || ! this.settings.follow_buttons || ! data || ! data.length ) { + if ( cont ) + cont.parentElement.removeChild(cont); + + } else { + if ( ! cont ) { + cont = document.createElement('span'); + cont.id = 'ffz-ui-following'; + + var before = container.querySelector(':scope > span'); + if ( before ) + container.insertBefore(cont, before); + else + container.appendChild(cont); + } else + cont.innerHTML = ''; + + for(var i=0; i < data.length; i++) { + this._build_following_button(cont, data[i]); + } + } + } +} + + +// --------------- +// UI Construction +// --------------- + +FFZ.prototype._build_following_button = function(container, channel_id) { + var btn = document.createElement('a'), f = this, + btn_c = document.createElement('div'), + noti = document.createElement('a'), + noti_c = document.createElement('div'), + + display_name, + following = false, + notifications = false, + + update = function() { + btn_c.classList.toggle('is-following', following); + btn.title = (following ? "Unf" : "F") + "ollow " + display_name; + btn.innerHTML = (following ? "" : "Follow ") + display_name; + noti_c.classList.toggle('hidden', !following); + }, + + check_following = function() { + var user = f.get_user(); + if ( ! user || ! user.login ) { + following = false; + notification = false; + btn_c.classList.add('is-initialized'); + return update(); + } + + Twitch.api.get("users/" + user.login + "/follows/channels/" + channel_id) + .done(function(data) { + following = true; + notifications = data.notifications; + btn_c.classList.add('is-initialized'); + update(); + }).fail(function(data) { + following = false; + notifications = false; + btn_c.classList.add('is-initialized'); + update(); + }); + }, + + do_follow = function(notice) { + if ( notice !== false ) + notice = true; + + var user = f.get_user(); + if ( ! user || ! user.login ) + return null; + + notifications = notice; + return Twitch.api.put("users/:login/follows/channels/" + channel_id, {notifications: notifications}) + .fail(check_following); + }, + + on_name = function(cap_name) { + display_name = cap_name || channel_id; + update(); + }; + + btn_c.className = 'ember-follow follow-button'; + btn_c.appendChild(btn); + + // The drop-down button! + noti.className = 'toggle-notification-menu js-toggle-notification-menu'; + noti.href = '#'; + + noti_c.className = 'notification-controls v2 hidden'; + noti_c.appendChild(noti); + + // Event Listeners! + btn.addEventListener('click', function() { + var user = f.get_user(); + if ( ! user || ! user.login ) + // Show the login dialog~! + return Ember.$.login({mpSourceAction: "follow-button", follow: channel_id}); + + // Immediate update for nice UI. + following = ! following; + update(); + + // Report it! + f.ws_send("track_follow", [channel_id, following]); + + // Do it, and make sure it happened. + if ( following ) + do_follow() + else + Twitch.api.del("users/:login/follows/channels/" + channel_id) + .done(check_following); + + return false; + }); + + noti.addEventListener('click', function() { + var sw = f._build_following_popup(noti_c, channel_id, notifications); + if ( sw ) + sw.addEventListener('click', function() { + var notice = ! notifications; + sw.classList.toggle('active', notice); + do_follow(notice); + return false; + }); + return false; + }); + + + display_name = FFZ.get_capitalization(channel_id, on_name); + update(); + check_following(); + + container.appendChild(btn_c); + container.appendChild(noti_c); +} + + +FFZ.prototype._build_following_popup = function(container, channel_id, notifications) { + var popup = this._popup, out = '', + pos = container.offsetLeft + container.offsetWidth; + + + if ( popup ) { + popup.parentElement.removeChild(popup); + delete this._popup; + this._popup_kill && this._popup_kill(); + delete this._popup_kill; + + if ( popup.id == "ffz-following-popup" && popup.getAttribute('data-channel') === channel_id ) + return null; + } + + popup = this._popup = document.createElement('div'); + popup.id = 'ffz-following-popup'; + popup.setAttribute('data-channel', channel_id); + + popup.className = (pos >= 300 ? 'right' : 'left') + ' dropmenu notify-menu js-notify'; + + out = '
You are following ' + FFZ.get_capitalization(channel_id) + '
'; + out += '

'; + out += ''; + out += 'Notify me when the broadcaster goes live'; + out += '

'; + + popup.innerHTML = out; + container.appendChild(popup); + return popup.querySelector('a.switch'); +} +},{"../utils":30}],22:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'), @@ -5390,6 +6903,135 @@ FFZ.prototype.setup_menu = function() { }); document.body.classList.toggle("ffz-menu-replace", this.settings.replace_twitch_menu); + + // Add FFZ to the chat settings menu. + + this.log("Hooking the Ember Chat Settings view."); + + var Settings = App.__container__.resolve('view:settings'); + + if ( ! Settings ) + return; + + Settings.reopen({ + didInsertElement: function() { + this._super(); + + try { + this.ffzInit(); + } catch(err) { + f.error("ChatSettings didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("ChatSettings willClearRender: " + err); + } + this._super(); + }, + + ffzInit: function() { + var view = this, + el = this.get('element'), + menu = el && el.querySelector('.dropmenu'); + + if ( ! menu ) + return; + + var header = document.createElement('div'), + content = document.createElement('div'), + p, cb, a; + + header.className = 'list-header'; + header.innerHTML = 'FrankerFaceZ'; + + content.className = 'chat-menu-content'; + + // Dark Twitch + p = document.createElement('p'); + p.className = 'no-bttv'; + cb = document.createElement('input'); + cb.type = "checkbox"; + cb.className = "ember-checkbox ffz-setting-dark-twitch"; + cb.checked = f.settings.dark_twitch; + p.appendChild(cb); + p.appendChild(document.createTextNode("Dark Twitch")); + content.appendChild(p); + + cb.addEventListener("change", function(e) { + f.settings.set("dark_twitch", this.checked); + }); + + + // Minimal Chat + /* + p = document.createElement('p'); + //p.className = 'no-bttv'; + cb = document.createElement('input'); + cb.type = "checkbox"; + cb.className = "ember-checkbox ffz-setting-minimal-twitch"; + cb.checked = f.settings.minimal_chat; + p.appendChild(cb); + p.appendChild(document.createTextNode("Minimalistic Chat")); + content.appendChild(p); + + cb.addEventListener("change", function(e) { + f.settings.set("minimal_chat", this.checked); + if ( this.checked ) + view.set('controller.model.hidden', true); + });*/ + + + // More Settings + p = document.createElement('p'); + a = document.createElement('a'); + a.href = '#'; + a.innerHTML = 'More Settings'; + p.appendChild(a); + content.appendChild(p); + + a.addEventListener('click', function(e) { + view.set('controller.model.hidden', true); + f._last_page = 'settings'; + f.build_ui_popup(f._chatv); + e.stopPropagation(); + return false; + }); + + menu.appendChild(header); + menu.appendChild(content); + }, + + ffzTeardown: function() { + // Nothing~! + } + }); + + // For some reason, this doesn't work unless we create an instance of the + // chat settings view and then destroy it immediately. + try { + Settings.create().destroy(); + } catch(err) { } + + // Modify all existing Chat Settings views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Settings) ) + continue; + + this.log("Manually updating existing Chat Settings view.", view); + try { + view.ffzInit(); + } catch(err) { + this.error("setup: ChatSettings ffzInit: " + err); + } + } } @@ -5538,6 +7180,7 @@ FFZ.menu_pages.channel = { has_product = true; var tickets = App.__container__.resolve('model:ticket').find('user', {channel: room_id}), is_subscribed = tickets ? tickets.get('content') : false, + is_loaded = tickets ? tickets.get('isLoaded') : false, icon = room.room.get("badgeSet.subscriber.image"), grid = document.createElement("div"), @@ -5547,6 +7190,25 @@ FFZ.menu_pages.channel = { // Weird is_subscribed check. Might be more accurate? is_subscribed = is_subscribed && is_subscribed.length > 0; + // See if we've loaded. If we haven't loaded the ticket yet + // then try loading it, and then re-render the menu. + if ( tickets && ! is_subscribed && ! is_loaded ) { + var f = this; + tickets.addObserver('isLoaded', function() { + setTimeout(function(){ + if ( inner.getAttribute('data-page') !== 'channel' ) + return; + + inner.innerHTML = ''; + FFZ.menu_pages.channel.render.bind(f)(view, inner); + },0); + + }); + + tickets.load(); + } + + grid.className = "emoticon-grid"; header.className = "heading"; if ( icon ) @@ -5555,7 +7217,7 @@ FFZ.menu_pages.channel = { header.innerHTML = 'TwitchSubscriber Emoticons'; grid.appendChild(header); - for(var emotes=product.get("emoticons"), i=0; i < emotes.length; i++) { + for(var emotes=product.get("emoticons") || [], i=0; i < emotes.length; i++) { var emote = emotes[i]; if ( emote.state !== "active" ) continue; @@ -5584,7 +7246,7 @@ FFZ.menu_pages.channel = { if ( c > 0 ) inner.appendChild(grid); - if ( ! is_subscribed ) { + if ( c > 0 && ! is_subscribed ) { var sub_message = document.createElement("div"), nonsub_message = document.createElement("div"), unlock_text = document.createElement("span"), @@ -5604,7 +7266,7 @@ FFZ.menu_pages.channel = { nonsub_message.appendChild(sub_link); inner.appendChild(sub_message); - } else { + } else if ( c > 0 ) { var last_content = tickets.get("content"); last_content = last_content.length > 0 ? last_content[last_content.length-1] : undefined; if ( last_content && last_content.purchase_profile && !last_content.purchase_profile.will_renew ) { @@ -5627,8 +7289,19 @@ FFZ.menu_pages.channel = { } } + // Do we have extra sets? + var extra_sets = room && room.extra_sets || []; + // Basic Emote Sets - this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); + this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product || extra_sets.length ) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); + + for(var i=0; i < extra_sets.length; i++) { + // Look up the set name. + var set = this.emote_sets[extra_sets[i]], + name = set ? "Featured " + set.title : "Featured Channel"; + + this._emotes_for_sets(inner, view, [extra_sets[i]], name, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); + } // Feature Friday! this._feature_friday_ui(room_id, inner, view); @@ -5750,7 +7423,7 @@ FFZ.prototype._add_emote = function(view, emote) { else room.set('messageToSend', text); } -},{"../constants":3,"../utils":29}],22:[function(require,module,exports){ +},{"../constants":3,"../utils":30}],23:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); @@ -5797,7 +7470,7 @@ FFZ.prototype.update_ui_link = function(link) { link.classList.toggle('dark', dark); link.classList.toggle('blue', blue); } -},{"../constants":3}],23:[function(require,module,exports){ +},{"../constants":3}],24:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"), utils = require("../utils"), @@ -5855,11 +7528,11 @@ FFZ.prototype.setup_my_emotes = function() { } this._twitch_set_to_channel[0] = "global"; - this._twitch_set_to_channel[33] = "tfaces"; - this._twitch_set_to_channel[42] = "tfaces"; + this._twitch_set_to_channel[33] = "turbo_faces"; + this._twitch_set_to_channel[42] = "turbo_faces"; 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"; + this._twitch_badges["turbo_faces"] = this._twitch_badges["turbo"] = "//cdn.frankerfacez.com/script/turbo_badge.png"; } @@ -5906,7 +7579,7 @@ FFZ.menu_pages.my_emotes = { return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, ts); }; - this.ws_send("twitch_sets", needed_sets, function(success, data) { + if ( ! this.ws_send("twitch_sets", needed_sets, function(success, data) { if ( ! needed_sets.length ) return; @@ -5923,9 +7596,10 @@ FFZ.menu_pages.my_emotes = { return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, twitch_sets); } else fail(); - }); - - setTimeout(fail, 2000); + }) ) + fail() + else + setTimeout(fail, 2000); }, draw_twitch_set: function(view, set_id, set) { @@ -5936,7 +7610,7 @@ FFZ.menu_pages.my_emotes = { if ( channel_id === "global" ) title = "Global Emoticons"; - else if ( channel_id === "turbo" ) + else if ( channel_id === "turbo" || channel_id === "turbo_faces" ) title = "Twitch Turbo"; else title = FFZ.get_capitalization(channel_id, function(name) { @@ -6085,12 +7759,12 @@ FFZ.menu_pages.my_emotes = { // Finally, sort and add them all. sets.sort(function(a,b) { var an = a[0], bn = b[0]; - if ( an === "turbo" || an === "tfaces" ) + if ( an === "turbo" || an === "turbo_faces" ) an = "zza|" + an; else if ( an === "global" || an === "global emoticons" ) an = "zzz|" + an; - if ( bn === "turbo" || bn === "tfaces" ) + if ( bn === "turbo" || bn === "turbo_faces" ) bn = "zza|" + bn; else if ( bn === "global" || bn === "global emoticons" ) bn = "zzz|" + bn; @@ -6124,7 +7798,7 @@ FFZ.menu_pages.my_emotes = { } } }; -},{"../constants":3,"../utils":29}],24:[function(require,module,exports){ +},{"../constants":3,"../utils":30}],25:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; @@ -6151,7 +7825,7 @@ FFZ.settings_info.highlight_notifications = { //visible: function() { return ! this.has_bttv }, name: "Highlight Notifications", - help: "Display notifications when a highlighted word appears in chat in an unfocused tab.", + help: "Display notifications when a highlighted word appears in chat in an unfocused tab. This is automatically disabled on the dashboard.", on_update: function(val, direct) { // Check to see if we have notification permission. If this is @@ -6274,7 +7948,7 @@ FFZ.prototype.show_message = function(message) { closeWith: ["button"] }).show(); } -},{}],25:[function(require,module,exports){ +},{}],26:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'); @@ -6313,6 +7987,7 @@ FFZ.settings_info.srl_races = { FFZ.ws_on_close.push(function() { var controller = window.App && App.__container__.lookup('controller:channel'), current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), need_update = false; if ( ! controller ) @@ -6320,7 +7995,7 @@ FFZ.ws_on_close.push(function() { for(var chan in this.srl_races) { delete this.srl_races[chan]; - if ( chan == current_id ) + if ( chan === current_id || chan === current_host ) need_update = true; } @@ -6328,18 +8003,22 @@ FFZ.ws_on_close.push(function() { this.rebuild_race_ui(); }); + FFZ.ws_commands.srl_race = function(data) { var controller = App.__container__.lookup('controller:channel'), - current_id = controller.get('id'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), need_update = false; + this.srl_races = this.srl_races || {}; + for(var i=0; i < data[0].length; i++) { var channel_id = data[0][i]; this.srl_races[channel_id] = data[1]; - if ( channel_id == current_id ) + if ( channel_id === current_id || channel_id === current_host ) need_update = true; } - + if ( data[1] ) { var race = data[1], tte = race.twitch_entrants = {}; @@ -6363,41 +8042,75 @@ FFZ.ws_commands.srl_race = function(data) { FFZ.prototype.rebuild_race_ui = function() { var controller = App.__container__.lookup('controller:channel'), - channel_id = controller.get('id'), - race = this.srl_races[channel_id], - enable_ui = this.settings.srl_races, + channel_id = controller && controller.get('id'), + hosted_id = controller && controller.get('hostModeTarget.id'); - actions = document.querySelector('.stats-and-actions .channel-actions'), - race_container = actions.querySelector('#ffz-ui-race'); - - if ( ! race || ! enable_ui ) { - if ( race_container ) - race_container.parentElement.removeChild(race_container); - if ( this._popup && this._popup.id == "ffz-race-popup" ) { - delete this._popup; - this._popup_kill && this._popup_kill(); - delete this._popup_kill; - } + if ( ! this._cindex ) return; + + if ( channel_id ) { + var race = this.srl_races && this.srl_races[channel_id], + + el = this._cindex.get('element'), + container = el && el.querySelector('.stats-and-actions .channel-actions'), + race_container = container && container.querySelector('#ffz-ui-race'); + + if ( ! container || ! this.settings.srl_races || ! race ) { + if ( race_container ) + race_container.parentElement.removeChild(race_container); + + } else { + if ( ! race_container ) { + race_container = document.createElement('span'); + race_container.id = 'ffz-ui-race'; + race_container.setAttribute('data-channel', channel_id); + + var btn = document.createElement('span'); + btn.className = 'button drop action'; + btn.title = "SpeedRunsLive Race"; + btn.innerHTML = ''; + + btn.addEventListener('click', this._build_race_popup.bind(this, race_container, channel_id)); + + race_container.appendChild(btn); + container.appendChild(race_container); + } + + this._update_race(race_container, true); + } } - if ( race_container ) - return this._update_race(true); + if ( hosted_id ) { + var race = this.srl_races && this.srl_races[hosted_id], - race_container = document.createElement('span'); - race_container.setAttribute('data-channel', channel_id); - race_container.id = 'ffz-ui-race'; + el = this._cindex.get('element'), + container = el && el.querySelector('#hostmode .channel-actions'), + race_container = container && container.querySelector('#ffz-ui-race'); - var btn = document.createElement('span'); - btn.className = 'button drop action'; - btn.title = "SpeedRunsLive Race"; - btn.innerHTML = '",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 +!function(e){!function t(e,s,o){function n(a,r){if(!s[a]){if(!e[a]){var d="function"==typeof require&&require;if(!r&&d)return d(a,!0);if(i)return i(a,!0);throw new Error("Cannot find module '"+a+"'")}var u=s[a]={exports:{}};e[a][0].call(u.exports,function(t){var s=e[a][1][t];return n(s?s:t)},u,u.exports,t,e,s,o)}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(o.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 o=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 o=t.length;t.length;){var n=t.shift();e.room.tmiRoom.sendMessage("/unmod "+n)}return"Sent unmod command for "+o+" 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 o=t.length;t.length;){var n=t.shift();e.room.tmiRoom.sendMessage("/mod "+n)}return"Sent mod command for "+o+" 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='',o="true"==localStorage.ffzDebugMode&&document.body.classList.contains("ffz-dev");t.exports={DEBUG:o,SERVER:o?"//localhost:8000/":"//cdn.frankerfacez.com/",API_SERVER:"//api.frankerfacez.com/",EMOJI_REGEX:/((?:\ud83c\udde8\ud83c\uddf3|\ud83c\uddfa\ud83c\uddf8|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddef\ud83c\uddf5|\ud83c\uddee\ud83c\uddf9|\ud83c\uddec\ud83c\udde7|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddea\ud83c\uddf8|\ud83c\udde9\ud83c\uddea|\u0039\ufe0f?\u20e3|\u0038\ufe0f?\u20e3|\u0037\ufe0f?\u20e3|\u0036\ufe0f?\u20e3|\u0035\ufe0f?\u20e3|\u0034\ufe0f?\u20e3|\u0033\ufe0f?\u20e3|\u0032\ufe0f?\u20e3|\u0031\ufe0f?\u20e3|\u0030\ufe0f?\u20e3|\u0023\ufe0f?\u20e3|\ud83d\udeb3|\ud83d\udeb1|\ud83d\udeb0|\ud83d\udeaf|\ud83d\udeae|\ud83d\udea6|\ud83d\udea3|\ud83d\udea1|\ud83d\udea0|\ud83d\ude9f|\ud83d\ude9e|\ud83d\ude9d|\ud83d\ude9c|\ud83d\ude9b|\ud83d\ude98|\ud83d\ude96|\ud83d\ude94|\ud83d\ude90|\ud83d\ude8e|\ud83d\ude8d|\ud83d\ude8b|\ud83d\ude8a|\ud83d\ude88|\ud83d\ude86|\ud83d\ude82|\ud83d\ude81|\ud83d\ude36|\ud83d\ude34|\ud83d\ude2f|\ud83d\ude2e|\ud83d\ude2c|\ud83d\ude27|\ud83d\ude26|\ud83d\ude1f|\ud83d\ude1b|\ud83d\ude19|\ud83d\ude17|\ud83d\ude15|\ud83d\ude11|\ud83d\ude10|\ud83d\ude0e|\ud83d\ude08|\ud83d\ude07|\ud83d\ude00|\ud83d\udd67|\ud83d\udd66|\ud83d\udd65|\ud83d\udd64|\ud83d\udd63|\ud83d\udd62|\ud83d\udd61|\ud83d\udd60|\ud83d\udd5f|\ud83d\udd5e|\ud83d\udd5d|\ud83d\udd5c|\ud83d\udd2d|\ud83d\udd2c|\ud83d\udd15|\ud83d\udd09|\ud83d\udd08|\ud83d\udd07|\ud83d\udd06|\ud83d\udd05|\ud83d\udd04|\ud83d\udd02|\ud83d\udd01|\ud83d\udd00|\ud83d\udcf5|\ud83d\udcef|\ud83d\udced|\ud83d\udcec|\ud83d\udcb7|\ud83d\udcb6|\ud83d\udcad|\ud83d\udc6d|\ud83d\udc6c|\ud83d\udc65|\ud83d\udc2a|\ud83d\udc16|\ud83d\udc15|\ud83d\udc13|\ud83d\udc10|\ud83d\udc0f|\ud83d\udc0b|\ud83d\udc0a|\ud83d\udc09|\ud83d\udc08|\ud83d\udc07|\ud83d\udc06|\ud83d\udc05|\ud83d\udc04|\ud83d\udc03|\ud83d\udc02|\ud83d\udc01|\ud83d\udc00|\ud83c\udfe4|\ud83c\udfc9|\ud83c\udfc7|\ud83c\udf7c|\ud83c\udf50|\ud83c\udf4b|\ud83c\udf33|\ud83c\udf32|\ud83c\udf1e|\ud83c\udf1d|\ud83c\udf1c|\ud83c\udf1a|\ud83c\udf18|\ud83c\udccf|\ud83c\udd70|\ud83c\udd71|\ud83c\udd7e|\ud83c\udd8e|\ud83c\udd91|\ud83c\udd92|\ud83c\udd93|\ud83c\udd94|\ud83c\udd95|\ud83c\udd96|\ud83c\udd97|\ud83c\udd98|\ud83c\udd99|\ud83c\udd9a|\ud83d\udc77|\ud83d\udec5|\ud83d\udec4|\ud83d\udec3|\ud83d\udec2|\ud83d\udec1|\ud83d\udebf|\ud83d\udeb8|\ud83d\udeb7|\ud83d\udeb5|\ud83c\ude01|\ud83c\ude02|\ud83c\ude32|\ud83c\ude33|\ud83c\ude34|\ud83c\ude35|\ud83c\ude36|\ud83c\ude37|\ud83c\ude38|\ud83c\ude39|\ud83c\ude3a|\ud83c\ude50|\ud83c\ude51|\ud83c\udf00|\ud83c\udf01|\ud83c\udf02|\ud83c\udf03|\ud83c\udf04|\ud83c\udf05|\ud83c\udf06|\ud83c\udf07|\ud83c\udf08|\ud83c\udf09|\ud83c\udf0a|\ud83c\udf0b|\ud83c\udf0c|\ud83c\udf0f|\ud83c\udf11|\ud83c\udf13|\ud83c\udf14|\ud83c\udf15|\ud83c\udf19|\ud83c\udf1b|\ud83c\udf1f|\ud83c\udf20|\ud83c\udf30|\ud83c\udf31|\ud83c\udf34|\ud83c\udf35|\ud83c\udf37|\ud83c\udf38|\ud83c\udf39|\ud83c\udf3a|\ud83c\udf3b|\ud83c\udf3c|\ud83c\udf3d|\ud83c\udf3e|\ud83c\udf3f|\ud83c\udf40|\ud83c\udf41|\ud83c\udf42|\ud83c\udf43|\ud83c\udf44|\ud83c\udf45|\ud83c\udf46|\ud83c\udf47|\ud83c\udf48|\ud83c\udf49|\ud83c\udf4a|\ud83c\udf4c|\ud83c\udf4d|\ud83c\udf4e|\ud83c\udf4f|\ud83c\udf51|\ud83c\udf52|\ud83c\udf53|\ud83c\udf54|\ud83c\udf55|\ud83c\udf56|\ud83c\udf57|\ud83c\udf58|\ud83c\udf59|\ud83c\udf5a|\ud83c\udf5b|\ud83c\udf5c|\ud83c\udf5d|\ud83c\udf5e|\ud83c\udf5f|\ud83c\udf60|\ud83c\udf61|\ud83c\udf62|\ud83c\udf63|\ud83c\udf64|\ud83c\udf65|\ud83c\udf66|\ud83c\udf67|\ud83c\udf68|\ud83c\udf69|\ud83c\udf6a|\ud83c\udf6b|\ud83c\udf6c|\ud83c\udf6d|\ud83c\udf6e|\ud83c\udf6f|\ud83c\udf70|\ud83c\udf71|\ud83c\udf72|\ud83c\udf73|\ud83c\udf74|\ud83c\udf75|\ud83c\udf76|\ud83c\udf77|\ud83c\udf78|\ud83c\udf79|\ud83c\udf7a|\ud83c\udf7b|\ud83c\udf80|\ud83c\udf81|\ud83c\udf82|\ud83c\udf83|\ud83c\udf84|\ud83c\udf85|\ud83c\udf86|\ud83c\udf87|\ud83c\udf88|\ud83c\udf89|\ud83c\udf8a|\ud83c\udf8b|\ud83c\udf8c|\ud83c\udf8d|\ud83c\udf8e|\ud83c\udf8f|\ud83c\udf90|\ud83c\udf91|\ud83c\udf92|\ud83c\udf93|\ud83c\udfa0|\ud83c\udfa1|\ud83c\udfa2|\ud83c\udfa3|\ud83c\udfa4|\ud83c\udfa5|\ud83c\udfa6|\ud83c\udfa7|\ud83c\udfa8|\ud83c\udfa9|\ud83c\udfaa|\ud83c\udfab|\ud83c\udfac|\ud83c\udfad|\ud83c\udfae|\ud83c\udfaf|\ud83c\udfb0|\ud83c\udfb1|\ud83c\udfb2|\ud83c\udfb3|\ud83c\udfb4|\ud83c\udfb5|\ud83c\udfb6|\ud83c\udfb7|\ud83c\udfb8|\ud83c\udfb9|\ud83c\udfba|\ud83c\udfbb|\ud83c\udfbc|\ud83c\udfbd|\ud83c\udfbe|\ud83c\udfbf|\ud83c\udfc0|\ud83c\udfc1|\ud83c\udfc2|\ud83c\udfc3|\ud83c\udfc4|\ud83c\udfc6|\ud83c\udfc8|\ud83c\udfca|\ud83c\udfe0|\ud83c\udfe1|\ud83c\udfe2|\ud83c\udfe3|\ud83c\udfe5|\ud83c\udfe6|\ud83c\udfe7|\ud83c\udfe8|\ud83c\udfe9|\ud83c\udfea|\ud83c\udfeb|\ud83c\udfec|\ud83c\udfed|\ud83c\udfee|\ud83c\udfef|\ud83c\udff0|\ud83d\udc0c|\ud83d\udc0d|\ud83d\udc0e|\ud83d\udc11|\ud83d\udc12|\ud83d\udc14|\ud83d\udc17|\ud83d\udc18|\ud83d\udc19|\ud83d\udc1a|\ud83d\udc1b|\ud83d\udc1c|\ud83d\udc1d|\ud83d\udc1e|\ud83d\udc1f|\ud83d\udc20|\ud83d\udc21|\ud83d\udc22|\ud83d\udc23|\ud83d\udc24|\ud83d\udc25|\ud83d\udc26|\ud83d\udc27|\ud83d\udc28|\ud83d\udc29|\ud83d\udc2b|\ud83d\udc2c|\ud83d\udc2d|\ud83d\udc2e|\ud83d\udc2f|\ud83d\udc30|\ud83d\udc31|\ud83d\udc32|\ud83d\udc33|\ud83d\udc34|\ud83d\udc35|\ud83d\udc36|\ud83d\udc37|\ud83d\udc38|\ud83d\udc39|\ud83d\udc3a|\ud83d\udc3b|\ud83d\udc3c|\ud83d\udc3d|\ud83d\udc3e|\ud83d\udc40|\ud83d\udc42|\ud83d\udc43|\ud83d\udc44|\ud83d\udc45|\ud83d\udc46|\ud83d\udc47|\ud83d\udc48|\ud83d\udc49|\ud83d\udc4a|\ud83d\udc4b|\ud83d\udc4c|\ud83d\udc4d|\ud83d\udc4e|\ud83d\udc4f|\ud83d\udc50|\ud83d\udc51|\ud83d\udc52|\ud83d\udc53|\ud83d\udc54|\ud83d\udc55|\ud83d\udc56|\ud83d\udc57|\ud83d\udc58|\ud83d\udc59|\ud83d\udc5a|\ud83d\udc5b|\ud83d\udc5c|\ud83d\udc5d|\ud83d\udc5e|\ud83d\udc5f|\ud83d\udc60|\ud83d\udc61|\ud83d\udc62|\ud83d\udc63|\ud83d\udc64|\ud83d\udc66|\ud83d\udc67|\ud83d\udc68|\ud83d\udc69|\ud83d\udc6a|\ud83d\udc6b|\ud83d\udc6e|\ud83d\udc6f|\ud83d\udc70|\ud83d\udc71|\ud83d\udc72|\ud83d\udc73|\ud83d\udc74|\ud83d\udc75|\ud83d\udc76|\ud83d\udeb4|\ud83d\udc78|\ud83d\udc79|\ud83d\udc7a|\ud83d\udc7b|\ud83d\udc7c|\ud83d\udc7d|\ud83d\udc7e|\ud83d\udc7f|\ud83d\udc80|\ud83d\udc81|\ud83d\udc82|\ud83d\udc83|\ud83d\udc84|\ud83d\udc85|\ud83d\udc86|\ud83d\udc87|\ud83d\udc88|\ud83d\udc89|\ud83d\udc8a|\ud83d\udc8b|\ud83d\udc8c|\ud83d\udc8d|\ud83d\udc8e|\ud83d\udc8f|\ud83d\udc90|\ud83d\udc91|\ud83d\udc92|\ud83d\udc93|\ud83d\udc94|\ud83d\udc95|\ud83d\udc96|\ud83d\udc97|\ud83d\udc98|\ud83d\udc99|\ud83d\udc9a|\ud83d\udc9b|\ud83d\udc9c|\ud83d\udc9d|\ud83d\udc9e|\ud83d\udc9f|\ud83d\udca0|\ud83d\udca1|\ud83d\udca2|\ud83d\udca3|\ud83d\udca4|\ud83d\udca5|\ud83d\udca6|\ud83d\udca7|\ud83d\udca8|\ud83d\udca9|\ud83d\udcaa|\ud83d\udcab|\ud83d\udcac|\ud83d\udcae|\ud83d\udcaf|\ud83d\udcb0|\ud83d\udcb1|\ud83d\udcb2|\ud83d\udcb3|\ud83d\udcb4|\ud83d\udcb5|\ud83d\udcb8|\ud83d\udcb9|\ud83d\udcba|\ud83d\udcbb|\ud83d\udcbc|\ud83d\udcbd|\ud83d\udcbe|\ud83d\udcbf|\ud83d\udcc0|\ud83d\udcc1|\ud83d\udcc2|\ud83d\udcc3|\ud83d\udcc4|\ud83d\udcc5|\ud83d\udcc6|\ud83d\udcc7|\ud83d\udcc8|\ud83d\udcc9|\ud83d\udcca|\ud83d\udccb|\ud83d\udccc|\ud83d\udccd|\ud83d\udcce|\ud83d\udccf|\ud83d\udcd0|\ud83d\udcd1|\ud83d\udcd2|\ud83d\udcd3|\ud83d\udcd4|\ud83d\udcd5|\ud83d\udcd6|\ud83d\udcd7|\ud83d\udcd8|\ud83d\udcd9|\ud83d\udcda|\ud83d\udcdb|\ud83d\udcdc|\ud83d\udcdd|\ud83d\udcde|\ud83d\udcdf|\ud83d\udce0|\ud83d\udce1|\ud83d\udce2|\ud83d\udce3|\ud83d\udce4|\ud83d\udce5|\ud83d\udce6|\ud83d\udce7|\ud83d\udce8|\ud83d\udce9|\ud83d\udcea|\ud83d\udceb|\ud83d\udcee|\ud83d\udcf0|\ud83d\udcf1|\ud83d\udcf2|\ud83d\udcf3|\ud83d\udcf4|\ud83d\udcf6|\ud83d\udcf7|\ud83d\udcf9|\ud83d\udcfa|\ud83d\udcfb|\ud83d\udcfc|\ud83d\udd03|\ud83d\udd0a|\ud83d\udd0b|\ud83d\udd0c|\ud83d\udd0d|\ud83d\udd0e|\ud83d\udd0f|\ud83d\udd10|\ud83d\udd11|\ud83d\udd12|\ud83d\udd13|\ud83d\udd14|\ud83d\udd16|\ud83d\udd17|\ud83d\udd18|\ud83d\udd19|\ud83d\udd1a|\ud83d\udd1b|\ud83d\udd1c|\ud83d\udd1d|\ud83d\udd1e|\ud83d\udd1f|\ud83d\udd20|\ud83d\udd21|\ud83d\udd22|\ud83d\udd23|\ud83d\udd24|\ud83d\udd25|\ud83d\udd26|\ud83d\udd27|\ud83d\udd28|\ud83d\udd29|\ud83d\udd2a|\ud83d\udd2b|\ud83d\udd2e|\ud83d\udd2f|\ud83d\udd30|\ud83d\udd31|\ud83d\udd32|\ud83d\udd33|\ud83d\udd34|\ud83d\udd35|\ud83d\udd36|\ud83d\udd37|\ud83d\udd38|\ud83d\udd39|\ud83d\udd3a|\ud83d\udd3b|\ud83d\udd3c|\ud83d\udd3d|\ud83d\udd50|\ud83d\udd51|\ud83d\udd52|\ud83d\udd53|\ud83d\udd54|\ud83d\udd55|\ud83d\udd56|\ud83d\udd57|\ud83d\udd58|\ud83d\udd59|\ud83d\udd5a|\ud83d\udd5b|\ud83d\uddfb|\ud83d\uddfc|\ud83d\uddfd|\ud83d\uddfe|\ud83d\uddff|\ud83d\ude01|\ud83d\ude02|\ud83d\ude03|\ud83d\ude04|\ud83d\ude05|\ud83d\ude06|\ud83d\ude09|\ud83d\ude0a|\ud83d\ude0b|\ud83d\ude0c|\ud83d\ude0d|\ud83d\ude0f|\ud83d\ude12|\ud83d\ude13|\ud83d\ude14|\ud83d\ude16|\ud83d\ude18|\ud83d\ude1a|\ud83d\ude1c|\ud83d\ude1d|\ud83d\ude1e|\ud83d\ude20|\ud83d\ude21|\ud83d\ude22|\ud83d\ude23|\ud83d\ude24|\ud83d\ude25|\ud83d\ude28|\ud83d\ude29|\ud83d\ude2a|\ud83d\ude2b|\ud83d\ude2d|\ud83d\ude30|\ud83d\ude31|\ud83d\ude32|\ud83d\ude33|\ud83d\ude35|\ud83d\ude37|\ud83d\ude38|\ud83d\ude39|\ud83d\ude3a|\ud83d\ude3b|\ud83d\ude3c|\ud83d\ude3d|\ud83d\ude3e|\ud83d\ude3f|\ud83d\ude40|\ud83d\ude45|\ud83d\ude46|\ud83d\ude47|\ud83d\ude48|\ud83d\ude49|\ud83d\ude4a|\ud83d\ude4b|\ud83d\ude4c|\ud83d\ude4d|\ud83d\ude4e|\ud83d\ude4f|\ud83d\ude80|\ud83d\ude83|\ud83d\ude84|\ud83d\ude85|\ud83d\ude87|\ud83d\ude89|\ud83d\ude8c|\ud83d\ude8f|\ud83d\ude91|\ud83d\ude92|\ud83d\ude93|\ud83d\ude95|\ud83d\ude97|\ud83d\ude99|\ud83d\ude9a|\ud83d\udea2|\ud83d\udea4|\ud83d\udea5|\ud83d\udea7|\ud83d\udea8|\ud83d\udea9|\ud83d\udeaa|\ud83d\udeab|\ud83d\udeac|\ud83d\udead|\ud83d\udeb2|\ud83d\udeb6|\ud83d\udeb9|\ud83d\udeba|\ud83d\udebb|\ud83d\udebc|\ud83d\udebd|\ud83d\udebe|\ud83d\udec0|\ud83c\udde6|\ud83c\udde7|\ud83c\udde8|\ud83c\udde9|\ud83c\uddea|\ud83c\uddeb|\ud83c\uddec|\ud83c\udded|\ud83c\uddee|\ud83c\uddef|\ud83c\uddf0|\ud83c\uddf1|\ud83c\uddf2|\ud83c\uddf3|\ud83c\uddf4|\ud83c\uddf5|\ud83c\uddf6|\ud83c\uddf7|\ud83c\uddf8|\ud83c\uddf9|\ud83c\uddfa|\ud83c\uddfb|\ud83c\uddfc|\ud83c\uddfd|\ud83c\uddfe|\ud83c\uddff|\ud83c\udf0d|\ud83c\udf0e|\ud83c\udf10|\ud83c\udf12|\ud83c\udf16|\ud83c\udf17|\ue50a|\u3030|\u27b0|\u2797|\u2796|\u2795|\u2755|\u2754|\u2753|\u274e|\u274c|\u2728|\u270b|\u270a|\u2705|\u26ce|\u23f3|\u23f0|\u23ec|\u23eb|\u23ea|\u23e9|\u2122|\u27bf|\u00a9|\u00ae)|(?:(?:\ud83c\udc04|\ud83c\udd7f|\ud83c\ude1a|\ud83c\ude2f|\u3299|\u303d|\u2b55|\u2b50|\u2b1c|\u2b1b|\u2b07|\u2b06|\u2b05|\u2935|\u2934|\u27a1|\u2764|\u2757|\u2747|\u2744|\u2734|\u2733|\u2716|\u2714|\u2712|\u270f|\u270c|\u2709|\u2708|\u2702|\u26fd|\u26fa|\u26f5|\u26f3|\u26f2|\u26ea|\u26d4|\u26c5|\u26c4|\u26be|\u26bd|\u26ab|\u26aa|\u26a1|\u26a0|\u2693|\u267f|\u267b|\u3297|\u2666|\u2665|\u2663|\u2660|\u2653|\u2652|\u2651|\u2650|\u264f|\u264e|\u264d|\u264c|\u264b|\u264a|\u2649|\u2648|\u263a|\u261d|\u2615|\u2614|\u2611|\u260e|\u2601|\u2600|\u25fe|\u25fd|\u25fc|\u25fb|\u25c0|\u25b6|\u25ab|\u25aa|\u24c2|\u231b|\u231a|\u21aa|\u21a9|\u2199|\u2198|\u2197|\u2196|\u2195|\u2194|\u2139|\u2049|\u203c|\u2668)([\uFE0E\uFE0F]?)))/g,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,o=t("../utils"),n=t("../constants");s.prototype.setup_channel=function(){this.log("Creating channel style element.");var e=this._channel_style=document.createElement("style");e.id="ffz-channel-css",document.head.appendChild(e),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"),o=this;if(t){this._modify_cindex(t);try{t.create().destroy()}catch(n){}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(){o._cindex&&o._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()]),o._cindex&&o._cindex.ffzFixTitle()}.observes("content.status","content.id"),ffzHostTarget:function(){var e=this.get("content.hostModeTarget"),t=e&&e.get("name"),n=e&&e.get("id"),i=e&&e.get("display_name");n!==o.__old_host_target&&(o.__old_host_target&&o.ws_send("unsub_channel",o.__old_host_target),n?(o.ws_send("sub_channel",n),o.__old_host_target=n):delete o.__old_host_target),i&&(s.capitalization[t]=[i,Date.now()]),o.settings.group_tabs&&o._chatv&&o._chatv.ffzRebuildTabs(),o.settings.follow_buttons&&o.rebuild_following_ui(),o.settings.srl_races&&o.rebuild_race_ui()}.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(){var e=this.get("controller.id"),s=this.get("element");t._cindex=this,t.ws_send("sub_channel",e),s.setAttribute("data-channel",e),s.classList.add("ffz-channel"),this.ffzFixTitle(),this.ffzUpdateUptime(),this.ffzUpdateChatters();var o=this.get("element").querySelector(".svg-glyph_views:not(.ffz-svg)");o&&o.parentNode.classList.add("twitch-channel-views"),t.settings.follow_buttons&&t.rebuild_following_ui(),t.settings.srl_races&&t.rebuild_race_ui()},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 o=s.querySelectorAll("script");s.innerHTML=o.length?o[0].outerHTML+e+o[1].outerHTML:e})}},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,d=s.ffz_viewers||0,i=this.get("element").querySelector("#ffz-chatter-display span");if(!i){var u=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!u)return;var c=document.createElement("span");c.className="ffz stat",c.id="ffz-chatter-display",c.title="Currently in Chat",c.innerHTML=n.ROOMS+" ",i=document.createElement("span"),c.appendChild(i);var l=u.querySelector("#ffz-ffzchatter-display");l?u.insertBefore(c,l):u.appendChild(c),jQuery(c).tipsy()}if(i.innerHTML=o.number_commas(a),!r&&!d)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 u=this.get("element").querySelector(".stats-and-actions .channel-stats");if(!u)return;var c=document.createElement("span");c.className="ffz stat",c.id="ffz-ffzchatter-display",c.title="Viewers (In Chat) with FrankerFaceZ",c.innerHTML=n.ZREKNARF+" ",i=document.createElement("span"),c.appendChild(i);var l=u.querySelector("#ffz-chatter-display");l?u.insertBefore(c,l.nextSibling):u.appendChild(c),jQuery(c).tipsy()}i.innerHTML=o.number_commas(d)+" ("+o.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=o.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=n.CLOCK+" ",e=document.createElement("span"),r.appendChild(e);var d=a.querySelector(".live-count");if(d)a.insertBefore(r,d.nextSibling);else try{d=a.querySelector("script:nth-child(0n+2)"),a.insertBefore(r,d.nextSibling)}catch(u){a.insertBefore(r,a.childNodes[0])}jQuery(r).tipsy({html:!0})}e.innerHTML=o.time_to_string(i)}}},ffzTeardown:function(){var e=this.get("controller.id");e&&t.ws_send("unsub_channel",e),this.get("element").setAttribute("data-channel",""),t._cindex=void 0,this._ffz_update_uptime&&clearTimeout(this._ffz_update_uptime),o.update_css(t._channel_style,e,null)}})},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":30}],6:[function(t){var s=e.FrankerFaceZ,o=t("../utils"),n=t("../constants"),i=function(e){return 1>e?"":e>=99?"99+":""+e};s.settings_info.minimal_chat={type:"boolean",value:!1,category:"Chat",name:"Minimalistic Chat",help:"Hide all of the chat user interface, only showing messages and an input box.",on_update:function(e){if(document.body.classList.toggle("ffz-minimal-chat",e),this.settings.group_tabs&&this._chatv&&this._chatv._ffz_tabs){var t=this;setTimeout(function(){t._chatv&&t._chatv.$(".chat-room").css("top",t._chatv._ffz_tabs.offsetHeight+"px")},0)}}},s.settings_info.prevent_clear={type:"boolean",value:!1,no_bttv:!0,category:"Chat Moderation",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],o=s&&s.room;o&&o.get("messages").forEach(function(t,s){e&&!t.ffz_deleted&&t.deleted?o.set("messages."+s+".deleted",!1):!t.ffz_deleted||e||t.deleted||o.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(){document.body.classList.toggle("ffz-minimal-chat",this.settings.minimal_chat),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"),removeCurrentChannelRoom:function(){if(!t.settings.group_tabs||t.has_bttv)return this._super();var e=this.get("currentChannelRoom"),s=e&&e.get("id");t.settings.pinned_rooms&&-1!==t.settings.pinned_rooms.indexOf(s)||(e===this.get("currentRoom")&&this.blurRoom(),e&&e.destroy()),this.set("currentChannelRoom",void 0)}}),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 o in Ember.View.views)if(Ember.View.views.hasOwnProperty(o)){var n=Ember.View.views[o];if(n instanceof e){this.log("Manually updating existing Chat view.",n);try{n.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 o=e&&e.get("canInvite");this._ffz_invite&&this._ffz_invite.classList.toggle("hidden",!o),this.set("controller.showInviteUser",o&&this.get("controller.showInviteUser")),this.$(".chat-room").css("top",this._ffz_tabs.offsetHeight+"px")}}catch(n){t.error("ChatView ffzUpdateLink: "+n)}}),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"),o=this;s.className="button glyph-only tooltip",s.title="Chat Room Management",s.innerHTML=n.ROOMS,s.addEventListener("click",function(){var e=o.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=n.INVITE,s.addEventListener("click",function(){var e=o.get("controller");e&&e.set("showInviteUser",e.get("currentRoom.canInvite")&&!e.get("showInviteUser"))}),s.classList.toggle("hidden",!this.get("controller.currentRoom.canInvite")),o._ffz_invite=s,e.appendChild(s);var i,a=this.get("controller.currentChannelRoom");a&&(i=this.ffzBuildTab(o,a,!0),i&&e.appendChild(i));var r=App.__container__.lookup("controller:channel"),d=App.__container__.resolve("model:room");if(target=r&&r.get("hostModeTarget"),target&&d){var u=target.get("id");this._ffz_host!==u&&(-1===t.settings.pinned_rooms.indexOf(this._ffz_host)&&this._ffz_host_room&&(this.get("controller.currentRoom")===this._ffz_host_room&&this.get("controller").blurRoom(),this._ffz_host_room.destroy()),this._ffz_host=u,this._ffz_host_room=d.findOne(u))}else this._ffz_host&&(-1===t.settings.pinned_rooms.indexOf(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=o.ffzBuildTab(o,this._ffz_host_room,!1,!0),i&&e.appendChild(i));for(var c=0;c"+u+""})),a?(l=n.CAMERA,c.title="Current Channel"):r?(l=n.EYE,c.title="Hosted Channel"):c.title=h?"Group Chat":"Pinned Channel",c.innerHTML=l+o.sanitize(d)+""+u+"",c.addEventListener("click",function(){e.get("controller").focusRoom(t)}),c},ffzDisableTabs:function(){this._ffz_tabs&&(this._ffz_tabs.parentElement.removeChild(this._ffz_tabs),delete this._ffz_tabs,delete this._ffz_invite),this._ffz_host&&(-1===t.settings.pinned_rooms.indexOf(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":30}],7:[function(t){var s=e.FrankerFaceZ,o=t("../utils"),n="[\\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(n+"*,"+n+"*"),a=function(e){return(e+"").replace(/&/g,"&").replace(/'/g,"'").replace(/"/g,""").replace(//g,">")},r="http://static-cdn.jtvnw.net/emoticons/v1/",d={};build_srcset=function(e){if(d[e])return d[e];var t=d[e]=r+e+"/1.0 1x, "+r+e+"/2.0 2x, "+r+e+"/3.0 4x";return t},data_to_tooltip=function(e){var t=e.set,s=e.set_type,o=e.owner;return void 0===s&&(s="Channel"),t?(("--twitch-turbo--"==t||"turbo"==t)&&(t="Twitch Turbo",s=null),"Emoticon: "+e.code+"\n"+(s?s+": ":"")+t+(o?"\nBy: "+o.display_name:"")):e.code},build_tooltip=function(e){{var t=this._twitch_emotes[e];t?t.set:null}return t?"string"==typeof t?t:t.tooltip?t.tooltip:t.tooltip=data_to_tooltip(t):"???"},load_emote_data=function(e,t,s,o){if(s){t&&(o.code=t),this._twitch_emotes[e]=o;for(var n=build_tooltip.bind(this)(e),i=document.querySelectorAll('img[emote-id="'+e+'"]'),a=0;aYouTube: "+o.sanitize(s.title)+"
",t+="Channel: "+o.sanitize(s.channel)+" | "+o.time_to_string(s.duration)+"
",t+=o.number_commas(s.views||0)+" Views | 👍 "+o.number_commas(s.likes||0)+" 👎 "+o.number_commas(s.dislikes||0);else if("strawpoll"==s.type){t="Strawpoll: "+o.sanitize(s.title)+"
";for(var n in s.items){{var i=s.items[n];Math.floor(i/s.total*100)}t+='"}t+="
'+o.sanitize(n)+''+o.number_commas(i)+"

Total: "+o.number_commas(s.total);var a=o.parse_date(s.fetched);if(a){var r=Math.floor((a.getTime()-Date.now())/1e3);r>60&&(t+="
Data was cached "+o.time_to_string(r)+" ago.")}}else if("twitch"==s.type){t="Twitch: "+o.sanitize(s.display_name)+"
";var d=o.parse_date(s.since);d&&(t+="Member Since: "+o.date_string(d)+"
"),t+="Views: "+o.number_commas(s.views)+" | Followers: "+o.number_commas(s.followers)+""}else if("twitch_vod"==s.type)t="Twitch "+("highlight"==s.broadcast_type?"Highlight":"Broadcast")+": "+o.sanitize(s.title)+"
",t+="By: "+o.sanitize(s.display_name)+(s.game?" | Playing: "+o.sanitize(s.game):" | Not Playing")+"
",t+="Views: "+o.number_commas(s.views)+" | "+o.time_to_string(s.length);else if("twitter"==s.type)t="Tweet By: "+o.sanitize(s.user)+"
",t+=o.sanitize(s.tweet);else if("reputation"==s.type){if(t=''+o.sanitize(s.full.toLowerCase())+"",s.trust<50||s.safety<50||s.tags&&s.tags.length>0){t+="
";var u=!1;(s.trust<50||s.safety<50)&&(s.unsafe=!0,t+="Potentially Unsafe Link
",t+="Trust: "+s.trust+"% | Child Safety: "+s.safety+"%",u=!0),s.tags&&s.tags.length>0&&(t+=(u?"
":"")+"Tags: "+s.tags.join(", ")),t+="
Data Source: WOT"}}else s.full&&(t=''+o.sanitize(s.full.toLowerCase())+"");return t||(t=''+o.sanitize(e.toLowerCase())+""),s.tooltip=t,t},load_link_data=function(e,t,s){if(t){this._link_data[e]=s,s.unsafe=!1;var o,n=build_link_tooltip.bind(this)(e),i="/"==e.charAt(e.length-1)?e.substr(0,e.length-1):null;if(o=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;ae&&(e=10),this.settings.set("scrollback_length",e);var t=App.__container__.lookup("controller:chat"),s=t&&t.get("currentRoom.id");for(var o in this.rooms){var n=this.rooms[o];n.room.set("messageBufferSize",e+(this._roomv&&!this._roomv.get("stuckToBottom")&&s===o?150:0))}}}},s.settings_info.banned_words={type:"button",value:[],category:"Chat",no_bttv:!0,name:"Banned Words",help:"Set a list of words that will be locally removed from chat messages.",method:function(){var e=this.settings.banned_words.join(", "),t=prompt("Banned Words\n\nPlease enter a comma-separated list of words that you would like to be removed from chat messages.",e);if(null!==t&&void 0!==t){t=t.trim().split(i);for(var s=[],o=0;oBeta",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 o=App.__container__.resolve("component:message-line");o&&this._modify_line(o);var n=this.get_user();n&&n.name&&(s.capitalization[n.login]=[n.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 o=performance.now(),n=t.get_user(),i=n&&this.get("msgObject.from")===n.login;e=t._remove_banned(e),e=t._emoticonize(this,e),t.settings.parse_emoji&&(e=t.tokenize_emoji(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 - "+(u-o)+"ms",e,!1,!0)}catch(c){try{t.error("LineController tokenizedMessage: "+c)}catch(c){}}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()}),willClearRender:function(){try{this.$("a.mod-icon").tipsy("disable"),jQuery("body > .tipsy:last").remove()}catch(e){t.error("LineView willClearRender: "+e)}this._super()},didInsertElement:function(){this._super();try{var e=performance.now(),n=this.get("element"),i=this.get("msgObject.from"),a=this.get("msgObject.room")||App.__container__.lookup("controller:chat").get("currentRoom.id"),r=this.get("msgObject.color"),d=this.get("msgObject.ffz_alternate");r&&t._handle_color(r),void 0===d&&(d=t._last_row[a]=t._last_row.hasOwnProperty(a)?!t._last_row[a]:!1,this.set("msgObject.ffz_alternate",d)),n.classList.toggle("ffz-alternate",d||!1),n.classList.toggle("ffz-deleted",t.settings.prevent_clear&&this.get("msgObject.ffz_deleted")||!1),n.setAttribute("data-room",a),n.setAttribute("data-sender",i),n.setAttribute("data-deleted",this.get("msgObject.deleted")||!1);var u=this.get("msgObject.ffz_old_messages");if(u&&u.length){var c=document.createElement("div");c.className="button primary float-right",c.innerHTML="Show "+o.number_commas(u.length)+" Old",c.addEventListener("click",t._show_deleted.bind(t,a)),n.classList.add("clearfix"),n.classList.add("ffz-has-deleted"),this.$(".message").append(c)}t.render_badge(this),this.get("msgObject.ffz_has_mention")&&n.classList.add("ffz-mentioned");for(var l=n.querySelectorAll("span.message a.deleted-link"),h=0;h5&&t.log("Line Took Too Long - "+F+"ms",n.innerHTML,!1,!0)}catch(x){try{t.error("LineView didInsertElement: "+x)}catch(x){}}}})},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],n=o.get_luminance(s),i="",a='span[style="color:'+e+'"]',r=!1;if(n>.3){r=!0;for(var d=127,u=s;d--&&(u=o.darken(u),!(o.get_luminance(u)<=.3)););i+=".ffz-chat-colors .ember-chat-container:not(.dark) .chat-line "+a+", .ffz-chat-colors .chat-container:not(.dark) .chat-line "+a+" { color: "+o.rgb_to_css(u)+" !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>n){r=!0;for(var d=127,u=s;d--&&(u=o.brighten(u),!(o.get_luminance(u)>=.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: "+o.rgb_to_css(u)+" !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(e,t){if(!e)return e;if(e=e.toLowerCase(),"jtv"==e||"twitchnotify"==e)return e;var o=s.capitalization[e];return o&&Date.now()-o[1]<36e5?o[0]:(s._cap_fetching<25&&(s._cap_fetching++,s.get().ws_send("get_display_name",e,function(o,n){var i=o?n:e;s.capitalization[e]=[i,Date.now()],s._cap_fetching--,"function"==typeof t&&t(i)})),o?o[0]:e)},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 o=s._words_to_regex(t),n=[],i=0;i<banned link>',own:!0}:r)}return n},s.prototype._emoticonize=function(e,t){var s=e.get("msgObject.room"),o=e.get("msgObject.from");return this.tokenize_emotes(o,s,t)}},{"../utils":30}],8:[function(t){var s,o=e.FrankerFaceZ,n=t("../utils"),i={ESC:27,P:80,B:66,T:84,U:85},a='',r='',d={},u=function(e){if(1===e)return"Purge";if(d[e])return d[e];var t,s,o,n,i;t=Math.floor(e/604800),i=e%604800,s=Math.floor(i/86400),i%=86400,o=Math.floor(i/3600),i%=3600,n=Math.floor(i/60),i%=60;var a=d[e]=(t?t+"w":"")+(s||t&&(o||n||i)?s+"d":"")+(o||(t||s)&&(n||i)?o+"h":"")+(n||(t||s||o)&&i?n+"m":"")+(i?i+"s":"");return a};try{s=e.require&&e.require("ember-twitch-chat/helpers/chat-line-helpers")}catch(c){}o.settings_info.chat_hover_pause={type:"boolean",value:!1,no_bttv:!0,category:"Chat Moderation",name:"Pause Chat Scrolling on Mouse Hover",help:"Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link mis-clicks.",on_update:function(e){this._roomv&&(e?this._roomv.ffzEnableFreeze():this._roomv.ffzDisableFreeze())}},o.settings_info.short_commands={type:"boolean",value:!0,no_bttv:!0,category:"Chat Moderation",name:"Short Moderation Commands",help:"Use /t, /b, and /u in chat in place of /timeout, /ban, /unban for quicker moderation, and use /p for 1 second timeouts."},o.settings_info.mod_card_hotkeys={type:"boolean",value:!1,no_bttv:!0,category:"Chat Moderation",name:"Moderation Card Hotkeys",help:"With a moderation card selected, press B to ban the user, T to time them out for 10 minutes, P to time them out for 1 second, or U to unban them. ESC closes the card."},o.settings_info.mod_card_history={type:"boolean",value:!1,no_bttv:!0,category:"Chat Moderation",name:"Moderation Card History",help:"Display a few of the user's previously sent messages on moderation cards.",on_update:function(e){if(!e&&this.rooms)for(var t in this.rooms){var s=this.rooms[t];s&&(s.user_history=void 0)}}},o.settings_info.mod_card_durations={type:"button",value:[300,600,3600,43200,86400,604800],category:"Chat Moderation",no_bttv:!0,name:"Moderation Card Timeout Buttons",help:"Add additional timeout buttons to moderation cards with specific durations.",method:function(){var e=this.settings.mod_card_durations.join(", "),t=prompt('Moderation Card Timeout Buttons\n\nPlease enter a comma-separated list of durations that you would like to have timeout buttons for. Durations must be expressed in seconds.\n\nEnter "reset" without quotes to return to the default value.',e);if(null!==t&&void 0!==t){"reset"===t&&(t=o.settings_info.mod_card_durations.value.join(", ")),t=t.trim().split(/[ ,]+/);for(var s=[],n=0;n0&&s.push(i)}this.settings.set("mod_card_durations",s)}}},o.prototype.setup_mod_card=function(){this.log("Modifying Mousetrap stopCallback so we can catch ESC.");var t=Mousetrap.stopCallback;Mousetrap.stopCallback=function(e,s,o){return s.classList.contains("no-mousetrap")?!0:t(e,s,o)},Mousetrap.bind("up up down down left right left right b a enter",function(){var e=document.querySelector(".app-main")||document.querySelector(".ember-chat-container");e&&e.classList.toggle("ffz-flip")}),this.log("Hooking the Ember Moderation Card view.");var o=App.__container__.resolve("component:moderation-card"),d=this;o.reopen({ffzForceRedraw:function(){this.rerender()}.observes("cardInfo.isModeratorOrHigher","cardInfo.user"),didInsertElement:function(){this._super(),e._card=this;try{if(d.has_bttv)return;var t=this.get("element"),o=this.get("controller");if((d.settings.mod_card_hotkeys||d.settings.mod_card_durations&&d.settings.mod_card_durations.length)&&t.classList.add("ffz-moderation-card"),o.get("cardInfo.isModeratorOrHigher")){t.classList.add("ffz-is-mod"),t.setAttribute("tabindex",1),d.settings.mod_card_hotkeys&&(t.classList.add("no-mousetrap"),t.addEventListener("keyup",function(e){var t=e.keyCode||e.which,s=o.get("cardInfo.user.id"),n=App.__container__.lookup("controller:chat").get("currentRoom");if(t==i.P)n.send("/timeout "+s+" 1");else if(t==i.B)n.send("/ban "+s);else if(t==i.T)n.send("/timeout "+s+" 600");else if(t==i.U)n.send("/unban "+s);else if(t!=i.ESC)return;o.send("close")}));var c=function(e){var t=o.get("cardInfo.user.id"),s=App.__container__.lookup("controller:chat").get("currentRoom");s.send(-1===e?"/unban "+t:"/timeout "+t+" "+e)},l=function(e){var t=document.createElement("button");return t.className="button",t.innerHTML=u(e),t.title="Timeout User for "+n.number_commas(e)+" Second"+(1!=e?"s":""),d.settings.mod_card_hotkeys&&600===e?t.title="(T)"+t.title.substr(1):d.settings.mod_card_hotkeys&&1===e&&(t.title="(P)urge - "+t.title),jQuery(t).tipsy(),t.addEventListener("click",c.bind(this,e)),t};if(d.settings.mod_card_durations&&d.settings.mod_card_durations.length){var h=document.createElement("div");h.className="interface clearfix",h.appendChild(l(1)); +var f=document.createElement("span");f.className="right",h.appendChild(f);for(var _=0;_ button");if(y&&y.classList.contains("message-button")&&(y.innerHTML=a,y.classList.add("glyph-only"),y.classList.add("message"),y.title="Whisper User",jQuery(y).tipsy()),d.settings.mod_card_history){var w=App.__container__.lookup("controller:chat"),z=w&&w.get("currentRoom"),k=z&&d.rooms&&d.rooms[z.get("id")],C=k&&k.user_history&&k.user_history[o.get("cardInfo.user.id")];if(C&&C.length){var E=document.createElement("ul"),T=!1;E.className="interface clearfix chat-history";for(var _=0;_'+s.getTime(h.date)+" ":"")+''+("action"===h.style?"*"+h.from+" ":"")+d.render_tokens(h.cachedTokens)+"";for(var x=F.querySelectorAll("a.deleted-link"),S=0;S10)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;s750&&this.ffzUnfreeze()}},ffzUnfreeze:function(){this.ffz_frozen=!1,this._ffz_last_move=0,this.ffzUnwarnPaused(),this.get("stuckToBottom")&&this._scrollToBottom()},ffzMouseOut:function(){this._ffz_outside=!0;var e=this;Ember.run.next(function(){e._ffz_outside&&e.ffzUnfreeze()})},ffzMouseMove:function(e){this._ffz_last_move=Date.now(),this._ffz_outside=!1,(e.screenX!==this._ffz_last_screenx||e.screenY!==this._ffz_last_screeny)&&(this._ffz_last_screenx=e.screenX,this._ffz_last_screeny=e.screenY,this.ffz_frozen||e.clientY>=this._ffz_messages.getBoundingClientRect().bottom-21||(this.ffz_frozen=!0,this.get("stuckToBottom")&&(this.set("controller.model.messageBufferSize",t.settings.scrollback_length+150),this.ffzWarnPaused())))},_scrollToBottom:_.throttle(function(){var e=this,t=this._$chatMessagesScroller;Ember.run.next(function(){setTimeout(function(){!e.ffz_frozen&&t&&t.length&&(t.scrollTop(t[0].scrollHeight),e._setStuckToBottom(!0))})})},200),_setStuckToBottom:function(e){this.set("stuckToBottom",e),this.get("controller.model")&&this.set("controller.model.messageBufferSize",t.settings.scrollback_length+(e?0:150))},ffzWarnPaused:function(){var e=this.get("element"),t=e&&e.querySelector(".chat-interface .more-messages-indicator.ffz-freeze-indicator");if(e){if(!t){t=document.createElement("div"),t.className="more-messages-indicator ffz-freeze-indicator",t.innerHTML="(Chat Paused Due to Mouse Movement)";var s=e.querySelector(".chat-interface");if(!s)return;s.insertBefore(t,s.childNodes[0])}t.classList.remove("hidden")}},ffzUnwarnPaused:function(){var e=this.get("element"),t=e&&e.querySelector(".chat-interface .more-messages-indicator.ffz-freeze-indicator");t&&t.classList.add("hidden")}})},s.chat_commands={},s.ffz_commands={},s.prototype.room_message=function(e,t){var s=t.split("\n");if(this.has_bttv)for(var o=0;o300,f=t.length,_=o.get("messages.0.ffz_alternate")||!1;h&&(_=!_);for(var f=t.length;f--;){var m=t[f];if("string"==typeof m.date&&(m.date=n.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),o.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(h){var m={ffz_alternate:!_,color:"#755000",date:new Date,from:"frankerfacez_admin",style:"admin",message:"(Last message is "+n.human_time(l)+" old.)",room:e};if(this.tokenize_chat_line(m),o.shouldShowMessage(m))for(i.insertAt(r,m);i.length>o.get("messageBufferSize");)i.removeAt(0)}}},s.prototype.load_room=function(e,t,s){var n=this;jQuery.getJSON(o.API_SERVER+"v1/room/"+e).done(function(s){if(s.sets)for(var o in s.sets)s.sets.hasOwnProperty(o)&&n._load_set_json(o,void 0,s.sets[o]);n._load_room_json(e,t,s)}).fail(function(o){return 404==o.status?"function"==typeof t&&t(!1):(s=(s||0)+1,10>s?n.load_room(e,t,s):"function"==typeof t&&t(!1))})},s.prototype._load_room_json=function(e,t,s){if(!s||!s.room)return"function"==typeof t&&t(!1);s=s.room,this.rooms[e]&&(s.room=this.rooms[e].room);for(var o in this.rooms[e])"room"!==o&&this.rooms[e].hasOwnProperty(o)&&!s.hasOwnProperty(o)&&(s[o]=this.rooms[e][o]);s.needs_history=this.rooms[e]&&this.rooms[e].needs_history||!1,this.rooms[e]=s,(s.css||s.moderator_badge)&&n.update_css(this._room_style,e,i(s)+(s.css||"")),this.emote_sets.hasOwnProperty(s.set)?-1===this.emote_sets[s.set].users.indexOf(e)&&this.emote_sets[s.set].users.push(e):this.load_set(s.set,function(t,s){-1===s.users.indexOf(e)&&s.users.push(e)}),this.update_ui_link(),t&&t(!0,s)},s.prototype._modify_room=function(t){var s=this;t.reopen({subsOnlyMode:!1,r9kMode:!1,slowWaiting:!1,slowValue:0,updateWait:function(e,t){var o=this.get("slowWait")||0;this.set("slowWait",e),1>o&&e>0?(setTimeout(this.ffzUpdateWait.bind(this),1e3),s._roomv&&s._roomv.ffzUpdateStatus()):(o>0&&1>e||t)&&(this.set("ffz_banned",!1),s._roomv&&s._roomv.ffzUpdateStatus())},ffzUpdateWait:function(){var e=this.get("slowWait")||0;1>e||(this.set("slowWait",--e),e>0?setTimeout(this.ffzUpdateWait.bind(this),1e3):(this.set("ffz_banned",!1),s._roomv&&s._roomv.ffzUpdateStatus()))},ffzUpdateStatus:function(){s._roomv&&s._roomv.ffzUpdateStatus()}.observes("r9kMode","subsOnlyMode","slowMode","slowValue","ffz_banned"),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){if(this.get("messages").forEach(function(o,n){o.from===e&&(t.set("messages."+n+".ffz_deleted",!0),s.settings.prevent_clear||t.set("messages."+n+".deleted",!0))}),s.settings.mod_card_history){var o=s.rooms&&s.rooms[t.get("id")],n=o&&o.user_history&&o.user_history[e];if(null!==n&&void 0!==n){var i=!1,a=n.length>0?n[n.length-1]:null;if(i=null!==a&&a.is_delete,!i)for(n.push({from:"jtv",is_delete:!0,style:"admin",cachedTokens:["User has been timed out."],date:new Date});n.length>20;)n.shift()}}}else if(s.settings.prevent_clear)this.addTmiMessage("A moderator's attempt to clear chat was ignored.");else{var r=t.get("messages");t.set("messages",[]),t.addMessage({style:"admin",message:i18n("Chat was cleared by a moderator"),ffz_old_messages:r})}},pushMessage:function(e){if(this.shouldShowMessage(e)){var t,s,o,n=this.get("messageBufferSize");for(this.get("messages").pushObject(e),t=this.get("messages.length"),s=t-n,o=0;s>o;o++)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;if(t||(e.room=this.get("id")),s.tokenize_chat_line(e),!t&&e.from&&"jtv"!==e.from&&"twitchnotify"!==e.from&&s.settings.mod_card_history){var o=s.rooms&&s.rooms[e.room];if(o){var n=(o.user_history=o.user_history||{},o.user_history[e.from]=o.user_history[e.from]||[]);for(n.push({from:e.tags&&e.tags["display-name"]||e.from,cachedTokens:e.cachedTokens,style:e.style,date:e.date});n.length>20;)n.shift()}}if(!t){var i=s.get_user();if(i&&i.login===e.from){var a=this.get("ffz_banned");this.set("ffz_banned",!1),this.get("isModeratorOrHigher")||!this.get("slowMode")?this.updateWait(0,a):this.get("slowMode")&&this.updateWait(this.get("slowValue"))}}}}catch(r){s.error("Room addMessage: "+r)}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(o){s.error("send: "+o)}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 o=0;o0),t.set("slowValue",e.slow),t.get("slowMode")||t.updateWait(0)),e.hasOwnProperty("r9k")&&t.set("r9kMode",e.r9k),e.hasOwnProperty("subs-only")&&t.set("subsOnlyMode",e["subs-only"])}),e._roomConn._connection.off("message",e._roomConn._onIrcMessage,e._roomConn),e._roomConn._onIrcMessage=function(e){if(e.target==this.ircChannel)switch(e.command){case"JOIN":this._session&&this._session.nickname===e.sender?this._onIrcJoin(e):s.settings.chatter_count&&t.ffzUpdateChatters(e.sender);break;case"PART":this._session&&this._session.nickname===e.sender?(this._resetActiveState(),this._connection._exitedRoomConn(),this._trigger("exited")):s.settings.chatter_count&&t.ffzUpdateChatters(null,e.sender)}},e._roomConn._connection.on("message",e._roomConn._onIrcMessage,e._roomConn),this.set("ffz_is_patched",!0)}}.observes("tmiRoom")})}},{"../constants":3,"../utils":30}],10:[function(){var t=e.FrankerFaceZ;t.prototype.setup_viewers=function(){this.log("Hooking the Ember Viewers controller.");var e=App.__container__.resolve("controller:viewers");this._modify_viewers(e)},t.prototype._modify_viewers=function(e){var s=this;e.reopen({lines:function(){var e=this._super();try{var o=[],n={},i=null,a=App.__container__.lookup("controller:channel"),r=this.get("parentController.model.id"),d=a&&a.get("id");if(d){var u=a.get("display_name");u&&(t.capitalization[d]=[u,Date.now()])}r!=d&&(d=null);for(var c=0;c0)){var n=s.emoticonSetIds;s.emoticonSetIds="",s.updateEmoticons(n),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],o=this.rooms&&this.rooms[t];return _.union(s&&s.sets||[],o&&o.set&&[o.set]||[],o&&o.extra_sets||[],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,o=t&&t.title||"Global";return e._tooltip="Emoticon: "+(e.hidden?"???":e.name)+"\nFFZ "+o+(s?"\nBy: "+s.display_name:""),e._tooltip},s.prototype.load_emoji_data=function(e,t){var s=this;jQuery.getJSON(o.SERVER+"emoji/emoji.json").done(function(t){var n={};for(var i in t){var a=t[i];i=i.toLowerCase(),n[i]=a,a.src=o.SERVER+"emoji/"+i+"-1x.png",a.srcSet=a.src+" 1x, "+o.SERVER+"emoji/"+i+"-2x.png 2x, "+o.SERVER+"emoji/"+i+"-4x.png 4x",a.token={srcSet:a.srcSet,emoticonSrc:a.src+'" data-ffz-emoji="'+i+'" height="18px',ffzEmoji:i}}s.emoji_data=n,s.log("Loaded data on "+Object.keys(n).length+" emoji."),"function"==typeof e&&e(!0,t)}).fail(function(o){return 404===o.status?"function"==typeof e&&e(!1):(t=(t||0)+1,50>t?s.load_emoji(e,t):"function"==typeof e&&e(!1))})},s.prototype.load_global_sets=function(e,t){var s=this;jQuery.getJSON(o.API_SERVER+"v1/set/global").done(function(e){s.default_sets=e.default_sets;var t=s.global_sets=[],o=e.sets||{};for(var n in o)if(o.hasOwnProperty(n)){var i=o[n];t.push(n),s._load_set_json(n,void 0,i)}}).fail(function(o){return 404==o.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 n=this;jQuery.getJSON(o.API_SERVER+"v1/set/"+e).done(function(s){n._load_set_json(e,t,s&&s.set)}).fail(function(o){return 404==o.status?"function"==typeof t&&t(!1):(s=s||0,s++,10>s?n.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),n.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);var o=this.emote_sets[e]&&this.emote_sets[e].users||[];this.emote_sets[e]=s,s.users=o,s.count=0;var i="",a=s.emoticons;s.emoticons={};for(var d=0;d=6e4?this.log("BetterTTV was not detected after 60 seconds."):setTimeout(this.find_bttv.bind(this,t,(s||0)+t),t))},s.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(),this._roomv&&(this.settings.chat_hover_pause&&this._roomv.ffzDisableFreeze(),this.settings.room_status&&this._roomv.ffzUpdateStatus()),document.body.classList.remove("ffz-chat-colors"),document.body.classList.remove("ffz-chat-background"),this.is_dashboard&&this._update_subscribers(),document.body.classList.add("ffz-bttv");var t=BetterTTV.chat.helpers.sendMessage,s=this;BetterTTV.chat.helpers.sendMessage=function(e){var o=e.split(" ",1)[0].toLowerCase();return"/ffz"!==o?t(e):void s.run_ffz_command(e.substr(5),BetterTTV.chat.store.currentRoom)};var i,a=BetterTTV.chat.handlers.onPrivmsg;BetterTTV.chat.handlers.onPrivmsg=function(e,t){i=e;var s=a(e,t);return i=null,s};var r=BetterTTV.chat.templates.privmsg;BetterTTV.chat.templates.privmsg=function(e,t,o,n,a){try{return s.bttv_badges(a),'
'+BetterTTV.chat.templates.timestamp(a.time)+" "+(n?BetterTTV.chat.templates.modicons():"")+" "+BetterTTV.chat.templates.badges(a.badges)+BetterTTV.chat.templates.from(a.nickname,a.color)+BetterTTV.chat.templates.message(a.sender,a.message,a.emotes,t?a.color:!1)+"
"}catch(d){return s.log("Error: ",d),r(e,t,o,n,a)}};var d=BetterTTV.chat.templates.whisper;BetterTTV.chat.templates.whisper=function(e){try{return s.bttv_badges(e),'
'+BetterTTV.chat.templates.timestamp(e.time)+" "+(e.badges&&e.badges.length?BetterTTV.chat.templates.badges(e.badges):"")+BetterTTV.chat.templates.whisperName(e.sender,e.receiver,e.from,e.to,e.fromColor,e.toColor)+BetterTTV.chat.templates.message(e.sender,e.message,e.emotes,!1)+"
"}catch(t){return s.log("Error: ",t),d(e)}};var u,c=BetterTTV.chat.templates.message;BetterTTV.chat.templates.message=function(e,t,o,n){try{n=n||!1;var i=encodeURIComponent(t);if("jtv"!==e){u=e;var a=BetterTTV.chat.templates.emoticonize(t,o);u=null;for(var r=0;r'+t+""}catch(d){return s.log("Error: ",d),c(e,t,o,n)}};var l=BetterTTV.chat.templates.emoticonize;BetterTTV.chat.templates.emoticonize=function(e,t){var a=l(e,t),r=i||BetterTTV.getChannel(),d=r&&r.toLowerCase(),c=u&&u.toLowerCase(),h=s.getEmotes(c,d),t=[],f=s.get_user(),m=f&&f.login===c;if(_.each(h,function(e){var o=s.emote_sets[e];o&&_.each(o.emoticons,function(e){_.any(a,function(t){return _.isString(t)&&t.match(e.regex)})&&t.push(e)})}),t.length&&_.each(t,function(e){var t=s._emote_tooltip(e),o=[''+t+''],n=a;a=[];for(var i=0;i']:w+(z||""))}}}else a.push(v)}}return a},this.update_ui_link()}},{"../constants":3,"../utils":30}],13:[function(){var t=e.FrankerFaceZ;t.prototype.find_emote_menu=function(t,s){return this.has_emote_menu=!1,e.emoteMenu&&emoteMenu.registerEmoteGetter?this.setup_emote_menu(s||0):void(s>=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,o=App.__container__.lookup("controller:chat"),n=o?o.get("currentRoom.id"):null,i=this.getEmotes(s,n),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 o=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.is_dashboard=!1;try{this.embed_in_dash=e.top!==e&&/\/[^\/]+\/dashboard/.test(e.top.location.pathname)&&!/bookmarks$/.test(e.top.location.pathname)}catch(n){this.embed_in_dash=!1}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(),a=i-o;this.log("Initialization complete in "+a+"ms")},s.prototype.is_dashboard=!1,s.prototype.setup_dashboard=function(t){var o=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.embed_in_dash=!1,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 n=e.performance&&performance.now?performance.now():Date.now(),i=n-o;this.log("Initialization complete in "+i+"ms")},s.prototype.setup_ember=function(t){var o=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.is_dashboard=!1;try{this.embed_in_dash=e.top!==e&&/\/[^\/]+\/dashboard/.test(e.top.location.pathname)&&!/bookmarks$/.test(e.top.location.pathname)}catch(n){this.embed_in_dash=!1}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_following(),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(),a=i-o;this.log("Initialization complete in "+a+"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){e.data}}},{"./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/following":21,"./ui/menu":22,"./ui/menu_button":23,"./ui/my_emotes":24,"./ui/notifications":25,"./ui/races":26,"./ui/styles":27,"./ui/sub_count":28,"./ui/viewer_count":29}],15:[function(t){var s=e.FrankerFaceZ,o=t("./constants");s.prototype.feature_friday=null,s.prototype.check_ff=function(e){e||this.log("Checking for Feature Friday data..."),jQuery.ajax(o.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 o=App.__container__.lookup("controller:channel");if(!o||o.get("id")!=this.feature_friday.channel){var n=this.feature_friday,i=document.createElement("div"),a=document.createElement("a");i.className="chat-menu-content",i.style.textAlign="center";var r=n.display_name+(n.live?" is live now!":"");a.className="button primary",a.classList.toggle("live",n.live),a.classList.toggle("blue",this.has_bttv&&BetterTTV.settings.get("showBlueButtons")),a.href="http://www.twitch.tv/"+n.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,o=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 o=s.settings_info[t],n=o.storage_key||make_ls(t),i=o.hasOwnProperty("value")?o.value:void 0;if(localStorage.hasOwnProperty(n))try{i=JSON.parse(localStorage.getItem(n))}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 o={},n=[];for(var i in s.settings_info)if(s.settings_info.hasOwnProperty(i)){var a=s.settings_info[i],r=a.category||"Miscellaneous",d=o[r];if(void 0!==a.visible&&null!==a.visible){var u=a.visible;if("function"==typeof a.visible&&(u=a.visible.bind(this)()),!u)continue}d||(n.push(r),d=o[r]=[]),d.push([i,a])}n.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 c=0;cs?-1:s>o?1:i>n?-1:n>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:o.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 o=t.key,n=o.substr(12),i=void 0,a=s.settings_info[n];if(!a){for(n in s.settings_info)if(s.settings_info.hasOwnProperty(n)&&(a=s.settings_info[n],a.storage_key==o))break;if(a.storage_key!=o)return}this.log("Updated Setting: "+n);try{i=JSON.parse(t.newValue)}catch(r){this.log('Error loading new value for "'+n+'": '+r),i=a.value||void 0}if(this.settings[n]=i,a.on_update)try{a.on_update.bind(this)(i,!1)}catch(r){this.log('Error running updater for setting "'+n+'": '+r)}}},s.prototype._setting_get=function(e){return this.settings[e]},s.prototype._setting_set=function(e,t){var o=s.settings_info[e],n=o.storage_key||make_ls(e),i=JSON.stringify(t);if(this.settings[e]=t,localStorage.setItem(n,i),this.log('Changed Setting "'+e+'" to: '+i),o.on_update)try{o.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],o=t.storage_key||make_ls(e),n=void 0;if(localStorage.hasOwnProperty(o)&&localStorage.removeItem(o),delete this.settings[e],t&&(n=this.settings[e]=t.hasOwnProperty("value")?t.value:void 0),t.on_update)try{t.on_update.bind(this)(n,!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,o=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(n){return this._ws_exists=!1,this.log("Error Creating WebSocket: "+n)}this._ws_exists=!0,s.onopen=function(){o._ws_open=!0,o._ws_delay=0,o.log("Socket connected.");var s=e.RequestFileSystem||e.webkitRequestFileSystem;s?s(e.TEMPORARY,100,o.ws_send.bind(o,"hello",["ffz_"+t.version_info,localStorage.ffzClientId],o._ws_on_hello.bind(o)),o.log.bind(o,"Operating in Incognito Mode.")):o.ws_send("hello",["ffz_"+t.version_info,localStorage.ffzClientId],o._ws_on_hello.bind(o));var n=o.get_user();if(n&&o.ws_send("setuser",n.login),o.is_dashboard){var i=location.pathname.match(/\/([^\/]+)/);i&&(o.ws_send("sub",i[1]),o.ws_send("sub_channel",i[1]))}for(var a in o.rooms)o.rooms.hasOwnProperty(a)&&o.rooms[a]&&(o.ws_send("sub",a),o.rooms[a].needs_history&&(o.rooms[a].needs_history=!1,!o.has_bttv&&o.settings.chat_history&&o.ws_send("chat_history",[a,25],o._load_history.bind(o,a))));if(o._cindex){var r=o._cindex.get("controller.id"),d=o._cindex.get("controller.hostModeTarget.id");r&&o.ws_send("sub_channel",r),d&&o.ws_send("sub_channel",d)}var u=o._ws_pending;o._ws_pending=[];for(var c=0;c'}if(e.isLink){if(!t&&void 0!==t)return e.href;var h=e.href;if(h.indexOf("@")>-1&&(-1===h.indexOf("/")||h.indexOf("@")'+h+"";var f=(h.match(/^https?:\/\//)?"":"http://")+h,l=s._link_data&&s._link_data[f]||{};return''+h+""}return e.mentionedUser?''+e.mentionedUser+"":n.sanitize(e.deletedLink?e.text:e)}).join("")},o.prototype.tokenize_title_emotes=function(e){var t=this,s=App.__container__.lookup("controller:channel"),o=s&&s.get("product.emoticons"),n=[];return _.isString(e)&&(e=[e]),_.each(_.union(t.__twitch_global_emotes||[],o),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)})&&n.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 o=0;o-1&&(-1===t.indexOf("/")||t.indexOf("@")0&&(n=!0)}var r=document.createElement("div"),d="";d+="

FrankerFaceZ

",d+='
new ways to woof
',r.className="chat-menu-content center",r.innerHTML=d,t.appendChild(r);var u=0,c=r.querySelector("h1");c&&c.addEventListener("click",function(){if(c.style.cursor="pointer",u++,u>=3){u=0;var e=document.querySelector(".app-main")||document.querySelector(".ember-chat-container");e&&e.classList.toggle("ffz-flip")}setTimeout(function(){u=0,c.style.cursor=""},2e3)});var l=document.createElement("div"),h=document.createElement("a"),f="To use custom emoticons in "+(n?"this channel":"tons of channels")+", get FrankerFaceZ from http://www.frankerfacez.com";h.className="button primary",h.innerHTML="Advertise in Chat",h.addEventListener("click",this._add_emote.bind(this,e,f)),l.appendChild(h);var _=document.createElement("a");_.className="button ffz-donate",_.href="https://www.frankerfacez.com/donate",_.target="_new",_.innerHTML="Donate",l.appendChild(_),l.className="chat-menu-content center",t.appendChild(l);var m=document.createElement("div");d='',d+='',d+='',d+='',d+='',m.className="chat-menu-content center",m.innerHTML=d;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,o=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",o.SERVER+"script/dark.css?_="+(o.DEBUG?Date.now():s.version_info)),document.head.appendChild(e)}}},{"../constants":3}],21:[function(t){{var s=e.FrankerFaceZ;t("../utils")}s.prototype.setup_following=function(){this.log("Initializing following support."),this.follow_data={},this.follow_sets={}},s.settings_info.follow_buttons={type:"boolean",value:!0,category:"Channel Metadata",name:"Relevant Follow Buttons",help:"Display additional Follow buttons for channels relevant to the stream, such as people participating in co-operative gameplay.",on_update:function(){this.rebuild_following_ui()}},s.ffz_commands.following=function(e,t){t=t.join(" ").trim().split(/\s*,+\s*/),t.length&&""===t[0]&&t.shift(),t.length&&""===t[t.length-1]&&t.pop();var s=this.get_user(),o=this;return!s||s.login!==e.id&&"sirstendec"!==s.login&&"dansalvato"!==s.login?"You must be logged in as the broadcaster to use this command.":this.ws_send("update_follow_buttons",[e.id,t],function(t,s){return t?void(s?o.room_message(e,"The following buttons have been updated."):o.room_message(e,"The following buttons have been disabled.")):void o.room_message(e,"There was an error updating the following buttons.") +})?void 0:"There was an error communicating with the server."},s.ws_on_close.push(function(){var t=e.App&&App.__container__.lookup("controller:channel"),s=t&&t.get("id"),o=t&&t.get("hostModeTarget.id"),n=!1;if(this.follow_sets={},t){for(var i in this.follow_data)if(delete this.follow_data[i],(i===s||i===o)&&(n=!0),this.rooms&&this.rooms[i]&&this.rooms[i].extra_sets){var a=this.rooms[i].extra_sets;delete this.rooms[i].extra_sets;for(var r=0;r span");r?i.insertBefore(a,r):i.appendChild(a)}for(var d=0;d span");r?i.insertBefore(a,r):i.appendChild(a)}for(var d=0;d=300?"right":"left")+" dropmenu notify-menu js-notify",i='
You are following '+s.get_capitalization(t)+"
",i+='

',i+='',i+='Notify me when the broadcaster goes live',i+="

",n.innerHTML=i,e.appendChild(n),n.querySelector("a.switch"))}},{"../utils":30}],22:[function(t){var s=e.FrankerFaceZ,o=t("../constants"),n=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,o=e._popup;o&&(o=jQuery(o),s=o.parent(),s.is(t.target)||0!==s.has(t.target).length||(o.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),this.log("Hooking the Ember Chat Settings view.");var t=App.__container__.resolve("view:settings");if(t){t.reopen({didInsertElement:function(){this._super();try{this.ffzInit()}catch(t){e.error("ChatSettings didInsertElement: "+t)}},willClearRender:function(){try{this.ffzTeardown()}catch(t){e.error("ChatSettings willClearRender: "+t)}this._super()},ffzInit:function(){var t=this,s=this.get("element"),o=s&&s.querySelector(".dropmenu");if(o){var n,i,a,r=document.createElement("div"),d=document.createElement("div");r.className="list-header",r.innerHTML="FrankerFaceZ",d.className="chat-menu-content",n=document.createElement("p"),n.className="no-bttv",i=document.createElement("input"),i.type="checkbox",i.className="ember-checkbox ffz-setting-dark-twitch",i.checked=e.settings.dark_twitch,n.appendChild(i),n.appendChild(document.createTextNode("Dark Twitch")),d.appendChild(n),i.addEventListener("change",function(){e.settings.set("dark_twitch",this.checked)}),n=document.createElement("p"),a=document.createElement("a"),a.href="#",a.innerHTML="More Settings",n.appendChild(a),d.appendChild(n),a.addEventListener("click",function(s){return t.set("controller.model.hidden",!0),e._last_page="settings",e.build_ui_popup(e._chatv),s.stopPropagation(),!1}),o.appendChild(r),o.appendChild(d)}},ffzTeardown:function(){}});try{t.create().destroy()}catch(s){}for(var o in Ember.View.views)if(Ember.View.views.hasOwnProperty(o)){var n=Ember.View.views[o];if(n instanceof t){this.log("Manually updating existing Chat Settings view.",n);try{n.ffzInit()}catch(s){this.error("setup: ChatSettings ffzInit: "+s)}}}}},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 n=document.createElement("div"),i=document.createElement("div"),a=document.createElement("ul"),r=this.has_bttv?BetterTTV.settings.get("darkenedMode"):!1;n.className="emoticon-selector chat-menu ffz-ui-popup",i.className="emoticon-selector-box dropmenu",n.appendChild(i),n.classList.toggle("dark",r);var d=document.createElement("div");d.className="ffz-ui-menu-page",i.appendChild(d),a.className="menu clearfix",i.appendChild(a);var u=document.createElement("li");u.className="title",u.innerHTML=""+(o.DEBUG?"[DEV] ":"")+"FrankerFaceZ",a.appendChild(u);var c=[];for(var l in s.menu_pages)if(s.menu_pages.hasOwnProperty(l)){var h=s.menu_pages[l];try{if(!h||h.hasOwnProperty("visible")&&(!h.visible||"function"==typeof h.visible&&!h.visible.bind(this)(e)))continue}catch(f){this.error("menu_pages "+l+" visible: "+f);continue}c.push([h.sort_order||0,l,h])}c.sort(function(e,t){if(e[0]t[0])return-1;var s=e[1].toLowerCase(),o=t[1].toLowerCase();return o>s?1:s>o?-1:0});for(var _=0;_0,u&&!c&&!l){var p=this;u.addObserver("isLoaded",function(){setTimeout(function(){"channel"===t.getAttribute("data-page")&&(t.innerHTML="",s.menu_pages.channel.render.bind(p)(e,t))},0)}),u.load()}f.className="emoticon-grid",_.className="heading",h&&(_.style.backgroundImage='url("'+h+'")'),_.innerHTML='TwitchSubscriber Emoticons',f.appendChild(_);for(var g=d.get("emoticons")||[],v=0;v0&&t.appendChild(f),m>0&&!c){var k=document.createElement("div"),C=document.createElement("div"),E=document.createElement("span"),T=document.createElement("a");k.className="subscribe-message",C.className="non-subscriber-message",k.appendChild(C),E.className="unlock-text",E.innerHTML="Subscribe to unlock Emoticons",C.appendChild(E),T.className="action subscribe-button button primary",T.href=d.get("product_url"),T.innerHTML='",C.appendChild(T),t.appendChild(k)}else if(m>0){var F=u.get("content");if(F=F.length>0?F[F.length-1]:void 0,F&&F.purchase_profile&&!F.purchase_profile.will_renew){var x=n.parse_date(F.access_end||"");k=document.createElement("div"),C=document.createElement("div"),E=document.createElement("span"),end_time=x?Math.floor((x.getTime()-Date.now())/1e3):null,k.className="subscribe-message",C.className="non-subscriber-message",k.appendChild(C),E.className="unlock-text",E.innerHTML="Subscription expires in "+n.time_to_string(end_time,!0,!0),C.appendChild(E),t.appendChild(k)}}}}var S=a&&a.extra_sets||[];this._emotes_for_sets(t,e,a&&a.set&&[a.set]||[],this.feature_friday||r||S.length?"Channel Emoticons":null,"http://cdn.frankerfacez.com/script/devicon.png","FrankerFaceZ");for(var v=0;vs?-1:s>o?1:0});for(var l=0;l0&&(i=!0)}t.classList.toggle("no-emotes",!i),t.classList.toggle("live",d),t.classList.toggle("dark",a),t.classList.toggle("blue",r)}}},{"../constants":3}],24:[function(t){var s=e.FrankerFaceZ,o=t("../constants"),n=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]="turbo_faces",this._twitch_set_to_channel[42]="turbo_faces",this._twitch_badges.global="//cdn.frankerfacez.com/script/twitch_logo.png",this._twitch_badges.turbo_faces=this._twitch_badges.turbo="//cdn.frankerfacez.com/script/turbo_badge.png"},s.menu_pages.my_emotes={name:"My Emoticons",icon:o.EMOTE,visible:function(e){var t=this.get_user(),s=e.get("controller.currentRoom.tmiSession"),o=t&&this.users[t.login]&&this.users[t.login].sets||[],n=(s&&s.getEmotes()||{emoticon_sets:{}}).emoticon_sets;return o.length||n&&Object.keys(n).length},render:function(e,t){var o=e.get("controller.currentRoom.tmiSession"),n=(o&&o.getEmotes()||{emoticon_sets:{}}).emoticon_sets,i=[];for(var a in n)n.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,n);var r=this,d=function(){if(i.length){i=[];var o={};for(var a in n)r._twitch_set_to_channel[a]&&(o[a]=n[a]);return s.menu_pages.my_emotes.draw_menu.bind(r)(e,t,o)}};this.ws_send("twitch_sets",i,function(o,a){if(i.length){if(i=[],o){for(var u in a)a.hasOwnProperty(u)&&(r._twitch_set_to_channel[u]=a[u]);return localStorage.ffzTwitchSets=JSON.stringify(r._twitch_set_to_channel),s.menu_pages.my_emotes.draw_menu.bind(r)(e,t,n)}d()}})?setTimeout(d,2e3):d()},draw_twitch_set:function(e,t,o){var r,d=document.createElement("div"),u=document.createElement("div"),c=this._twitch_set_to_channel[t];if(r="global"===c?"Global Emoticons":"turbo"===c||"turbo_faces"===c?"Twitch Turbo":s.get_capitalization(c,function(e){d.innerHTML='Twitch'+n.sanitize(e)}),d.className="heading",d.innerHTML='Twitch'+n.sanitize(r),this._twitch_badges[c])d.style.backgroundImage='url("'+this._twitch_badges[c]+'")';else{var l=this;Twitch.api.get("chat/"+c+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(l._twitch_badges[c]=e.subscriber.image,localStorage.ffzTwitchBadges=JSON.stringify(l._twitch_badges),d.style.backgroundImage='url("'+e.subscriber.image+'")')})}u.className="emoticon-grid",u.appendChild(d);for(var h=0;hFrankerFaceZ'+t.title,s.style.backgroundImage='url("'+(t.icon||"//cdn.frankerfacez.com/script/devicon.png")+'")',o.className="emoticon-grid",o.appendChild(s);for(var i in t.emoticons)t.emoticons.hasOwnProperty(i)&&!t.emoticons[i].hidden&&n.push(t.emoticons[i]);n.sort(function(e,t){var s=e.name.toLowerCase(),o=t.name.toLowerCase();return o>s?-1:s>o?1:e.idt.id?1:0});for(var a=0;as?-1:s>o?1:0});for(var u=0;uSpeedRunsLive 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"),o=t&&t.get("hostModeTarget.id"),n=!1;if(t){for(var i in this.srl_races)delete this.srl_races[i],(i===s||i===o)&&(n=!0);n&&this.rebuild_race_ui()}}),s.ws_commands.srl_race=function(e){var t=App.__container__.lookup("controller:channel"),s=t&&t.get("id"),o=t&&t.get("hostModeTarget.id"),n=!1;this.srl_races=this.srl_races||{};for(var i=0;i=300?"right":"left")+" share dropmenu",this._popup_kill=this._race_kill.bind(this),this._popup=o;var d="http://kadgar.net/live",u=!1;for(var c in a.entrants){var l=a.entrants[c].state;a.entrants.hasOwnProperty(c)&&a.entrants[c].channel&&("racing"==l||"entered"==l)&&(d+="/"+a.entrants[c].channel,u=!0)}var h=document.querySelector(".app-main.theatre")?document.body.clientHeight-300:e.parentElement.offsetTop-175,f=App.__container__.lookup("controller:channel"),_=f?f.get("display_name"):s.get_capitalization(t),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',u&&(r+='   Multitwitch'),r+="

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

'+k+"

Goal: "+C}u?_?l.innerHTML="Done":(l.innerHTML=o.time_to_string(u),this._race_timer=setTimeout(this._update_race.bind(this,e),1e3)):l.innerHTML="Entry Open"}}}},{"../utils":30}],27:[function(t){var s=e.FrankerFaceZ,o=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",o.SERVER+"script/style.css?_="+(o.DEBUG?Date.now():s.version_info)),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}],28:[function(t){{var s=e.FrankerFaceZ,o=t("../constants");t("../utils")}s.prototype._update_subscribers=function(){this._update_subscribers_timer&&(clearTimeout(this._update_subscribers_timer),delete this._update_subscribers_timer),this._update_subscribers_timer=setTimeout(this._update_subscribers.bind(this),6e4);var e=this.get_user(),t=this,s=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0,n=this.is_dashboard&&s&&s[1];if(this.has_bttv||!n||n!==e.login){var i=document.querySelector("#ffz-sub-display");return void(i&&i.parentElement.removeChild(i))}jQuery.ajax({url:"/broadcast/dashboard/partnership"}).done(function(e){try{var s,i=document.createElement("span");i.innerHTML=e,s=i.querySelector("#dash_main");var a=s&&s.textContent.match(/([\d,\.]+) total active subscribers/),r=a&&a[1];if(!r){var d=document.querySelector("#ffz-sub-display");return d&&d.parentElement.removeChild(d),void(t._update_subscribers_timer&&(clearTimeout(t._update_subscribers_timer),delete t._update_subscribers_timer))}var d=document.querySelector("#ffz-sub-display span");if(!d){var u=document.querySelector(t.is_dashboard?"#stats":"#channel .stats-and-actions .channel-stats");if(!u)return;var c=document.createElement("span");c.className="ffz stat",c.id="ffz-sub-display",c.title="Active Channel Subscribers",c.innerHTML=o.STAR+" ",d=document.createElement("span"),c.appendChild(d),Twitch.api.get("chat/"+n+"/badges",null,{version:3}).done(function(e){e.subscriber&&e.subscriber.image&&(c.innerHTML="",c.appendChild(d),c.style.backgroundImage='url("'+e.subscriber.image+'")',c.style.backgroundRepeat="no-repeat",c.style.paddingLeft="23px",c.style.backgroundPosition="0 50%")}),u.appendChild(c),jQuery(c).tipsy(t.is_dashboard?{gravity:"s"}:void 0)}d.innerHTML=r +}catch(l){t.error("_update_subscribers: "+l)}}).fail(function(){var e=document.querySelector("#ffz-sub-display");e&&e.parentElement.removeChild(e)})}},{"../constants":3,"../utils":30}],29:[function(t){var s=e.FrankerFaceZ,o=t("../constants"),n=t("../utils");s.ws_commands.chatters=function(t){{var s=t[0],o=t[1],n=e.App&&App.__container__.lookup("controller:channel"),i=this.is_dashboard?location.pathname.match(/\/([^\/]+)/):void 0;this.is_dashboard?i&&i[1]:n&&n.get&&n.get("id")}if(!this.is_dashboard){var a=this.rooms&&this.rooms[s];return void(a&&(a.ffz_chatters=o,this._cindex&&this._cindex.ffzUpdateChatters()))}this._dash_chatters=o},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,d=this.is_dashboard?r&&r[1]:a&&a.get&&a.get("id");if(!this.is_dashboard){var u=this.rooms&&this.rooms[s];return void(u&&(u.ffz_viewers=i,this._cindex&&this._cindex.ffzUpdateChatters()))}if(this._dash_viewers=i,this.settings.chatter_count&&d===s){var c=document.querySelector("#ffz-ffzchatter-display"),l=o.ZREKNARF+" "+n.number_commas(i)+("number"==typeof this._dash_chatters?" ("+n.number_commas(this._dash_chatters)+")":"");if(c)c.innerHTML=l;else{var h=document.querySelector("#stats");if(!h)return;c=document.createElement("span"),c.id="ffz-ffzchatter-display",c.className="ffz stat",c.title="Viewers (In Chat) with FrankerFaceZ",c.innerHTML=l,h.appendChild(c),jQuery(c).tipsy(this.is_dashboard?{gravity:"s"}:void 0)}}}},{"../constants":3,"../utils":30}],30:[function(t,s){var o=(e.FrankerFaceZ,t("./constants"),{}),n=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)),o=Math.max(0,Math.min(255,e[1]-t)),n=Math.max(0,Math.min(255,e[2]-t));return[s,o,n]},d=function(e){return"rgb("+e[0]+", "+e[1]+", "+e[2]+")"},u=function(e,t){return t=0===t?0:t||1,r(e,-t)},c=function(e){e=[e[0]/255,e[1]/255,e[2]/255];for(var t=0;t=0?t.trailing=e.substr(i+2):i=e.length;var a=e.substr(n+1,i-n-1).split(" ");return t.command=a[0],a.length>1&&(t.params=a.slice(1)),t},_={":":";",s:" ",r:"\r",n:"\n","\\":"\\"},m=function(e){for(var t="",s=0;s=55296&&56319>=n?i=n:o.push(n.toString(16));var r=v[e]=v[e]||{},d=r[t]=o.join("-");return d};s.exports={update_css:function(e,t,s){var o=e.innerHTML,n="/*BEGIN "+t+"*/",i="/*END "+t+"*/",a=o.indexOf(n),r=o.indexOf(i),d=-1!==a&&-1!==r&&r>a;(d||s)&&(d&&(o=o.substr(0,a)+o.substr(r+i.length)),s&&(o+=n+s+i),e.innerHTML=o)},splitIRCMessage:f,parseIRCTags:g,emoji_to_codepoint:b,get_luminance:c,brighten:r,darken:u,rgb_to_css:d,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:a,placement:function(e){return"forfeit"==e.state?"Forfeit":"dq"==e.state?"DQed":e.place?a(e.place):""},sanitize:function(e){var t=o[e];return t||(n.textContent=e,t=o[e]=n.innerHTML,n.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 o=Math.floor((e%=86400)/3600);if(o)return o+" hour"+i(o);var n=Math.floor((e%=3600)/60);if(n)return n+" minute"+i(n);var a=e%60;return a?a+" second"+i(a):"less than a second"},time_to_string:function(e,t,s,o){var n=e%60,i=Math.floor(e/60),a=Math.floor(i/60),r="";if(i%=60,t){if(r=Math.floor(a/24),a%=24,s&&r>0)return r+" days";r=r>0?r+" days, ":""}return r+(!o||r||a?(10>a?"0":"")+a+":":"")+(10>i?"0":"")+i+":"+(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 fd7345e0..7a2ffc59 100644 --- a/src/badges.js +++ b/src/badges.js @@ -53,7 +53,7 @@ FFZ.ws_commands.set_badge = function(data) { user = this.users[user_id] = this.users[user_id] || {}, badges = user.badges = user.badges || {}; - if ( badge === undefined ) + if ( badge === undefined || badge === null ) delete badges[slot]; else badges[slot] = badge; @@ -86,6 +86,9 @@ FFZ.prototype.bttv_badges = function(data) { if ( ! user || ! user.badges ) return; + if ( ! data.badges ) + data.badges = []; + // Determine where in the list to insert these badges. for(var i=0; i < data.badges.length; i++) { var badge = data.badges[i]; @@ -236,7 +239,7 @@ FFZ.prototype.render_badge = function(component) { if ( reverse ) { while(badges_out.length) - badges.before(badges_out.shift()[1]); + before.before(badges_out.shift()[1]); } else { while(badges_out.length) badges.append(badges_out.shift()[1]); diff --git a/src/constants.js b/src/constants.js index 20281eee..fd017ba5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,6 +6,8 @@ module.exports = { SERVER: DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/", API_SERVER: "//api.frankerfacez.com/", + EMOJI_REGEX: /((?:\ud83c\udde8\ud83c\uddf3|\ud83c\uddfa\ud83c\uddf8|\ud83c\uddf7\ud83c\uddfa|\ud83c\uddf0\ud83c\uddf7|\ud83c\uddef\ud83c\uddf5|\ud83c\uddee\ud83c\uddf9|\ud83c\uddec\ud83c\udde7|\ud83c\uddeb\ud83c\uddf7|\ud83c\uddea\ud83c\uddf8|\ud83c\udde9\ud83c\uddea|\u0039\ufe0f?\u20e3|\u0038\ufe0f?\u20e3|\u0037\ufe0f?\u20e3|\u0036\ufe0f?\u20e3|\u0035\ufe0f?\u20e3|\u0034\ufe0f?\u20e3|\u0033\ufe0f?\u20e3|\u0032\ufe0f?\u20e3|\u0031\ufe0f?\u20e3|\u0030\ufe0f?\u20e3|\u0023\ufe0f?\u20e3|\ud83d\udeb3|\ud83d\udeb1|\ud83d\udeb0|\ud83d\udeaf|\ud83d\udeae|\ud83d\udea6|\ud83d\udea3|\ud83d\udea1|\ud83d\udea0|\ud83d\ude9f|\ud83d\ude9e|\ud83d\ude9d|\ud83d\ude9c|\ud83d\ude9b|\ud83d\ude98|\ud83d\ude96|\ud83d\ude94|\ud83d\ude90|\ud83d\ude8e|\ud83d\ude8d|\ud83d\ude8b|\ud83d\ude8a|\ud83d\ude88|\ud83d\ude86|\ud83d\ude82|\ud83d\ude81|\ud83d\ude36|\ud83d\ude34|\ud83d\ude2f|\ud83d\ude2e|\ud83d\ude2c|\ud83d\ude27|\ud83d\ude26|\ud83d\ude1f|\ud83d\ude1b|\ud83d\ude19|\ud83d\ude17|\ud83d\ude15|\ud83d\ude11|\ud83d\ude10|\ud83d\ude0e|\ud83d\ude08|\ud83d\ude07|\ud83d\ude00|\ud83d\udd67|\ud83d\udd66|\ud83d\udd65|\ud83d\udd64|\ud83d\udd63|\ud83d\udd62|\ud83d\udd61|\ud83d\udd60|\ud83d\udd5f|\ud83d\udd5e|\ud83d\udd5d|\ud83d\udd5c|\ud83d\udd2d|\ud83d\udd2c|\ud83d\udd15|\ud83d\udd09|\ud83d\udd08|\ud83d\udd07|\ud83d\udd06|\ud83d\udd05|\ud83d\udd04|\ud83d\udd02|\ud83d\udd01|\ud83d\udd00|\ud83d\udcf5|\ud83d\udcef|\ud83d\udced|\ud83d\udcec|\ud83d\udcb7|\ud83d\udcb6|\ud83d\udcad|\ud83d\udc6d|\ud83d\udc6c|\ud83d\udc65|\ud83d\udc2a|\ud83d\udc16|\ud83d\udc15|\ud83d\udc13|\ud83d\udc10|\ud83d\udc0f|\ud83d\udc0b|\ud83d\udc0a|\ud83d\udc09|\ud83d\udc08|\ud83d\udc07|\ud83d\udc06|\ud83d\udc05|\ud83d\udc04|\ud83d\udc03|\ud83d\udc02|\ud83d\udc01|\ud83d\udc00|\ud83c\udfe4|\ud83c\udfc9|\ud83c\udfc7|\ud83c\udf7c|\ud83c\udf50|\ud83c\udf4b|\ud83c\udf33|\ud83c\udf32|\ud83c\udf1e|\ud83c\udf1d|\ud83c\udf1c|\ud83c\udf1a|\ud83c\udf18|\ud83c\udccf|\ud83c\udd70|\ud83c\udd71|\ud83c\udd7e|\ud83c\udd8e|\ud83c\udd91|\ud83c\udd92|\ud83c\udd93|\ud83c\udd94|\ud83c\udd95|\ud83c\udd96|\ud83c\udd97|\ud83c\udd98|\ud83c\udd99|\ud83c\udd9a|\ud83d\udc77|\ud83d\udec5|\ud83d\udec4|\ud83d\udec3|\ud83d\udec2|\ud83d\udec1|\ud83d\udebf|\ud83d\udeb8|\ud83d\udeb7|\ud83d\udeb5|\ud83c\ude01|\ud83c\ude02|\ud83c\ude32|\ud83c\ude33|\ud83c\ude34|\ud83c\ude35|\ud83c\ude36|\ud83c\ude37|\ud83c\ude38|\ud83c\ude39|\ud83c\ude3a|\ud83c\ude50|\ud83c\ude51|\ud83c\udf00|\ud83c\udf01|\ud83c\udf02|\ud83c\udf03|\ud83c\udf04|\ud83c\udf05|\ud83c\udf06|\ud83c\udf07|\ud83c\udf08|\ud83c\udf09|\ud83c\udf0a|\ud83c\udf0b|\ud83c\udf0c|\ud83c\udf0f|\ud83c\udf11|\ud83c\udf13|\ud83c\udf14|\ud83c\udf15|\ud83c\udf19|\ud83c\udf1b|\ud83c\udf1f|\ud83c\udf20|\ud83c\udf30|\ud83c\udf31|\ud83c\udf34|\ud83c\udf35|\ud83c\udf37|\ud83c\udf38|\ud83c\udf39|\ud83c\udf3a|\ud83c\udf3b|\ud83c\udf3c|\ud83c\udf3d|\ud83c\udf3e|\ud83c\udf3f|\ud83c\udf40|\ud83c\udf41|\ud83c\udf42|\ud83c\udf43|\ud83c\udf44|\ud83c\udf45|\ud83c\udf46|\ud83c\udf47|\ud83c\udf48|\ud83c\udf49|\ud83c\udf4a|\ud83c\udf4c|\ud83c\udf4d|\ud83c\udf4e|\ud83c\udf4f|\ud83c\udf51|\ud83c\udf52|\ud83c\udf53|\ud83c\udf54|\ud83c\udf55|\ud83c\udf56|\ud83c\udf57|\ud83c\udf58|\ud83c\udf59|\ud83c\udf5a|\ud83c\udf5b|\ud83c\udf5c|\ud83c\udf5d|\ud83c\udf5e|\ud83c\udf5f|\ud83c\udf60|\ud83c\udf61|\ud83c\udf62|\ud83c\udf63|\ud83c\udf64|\ud83c\udf65|\ud83c\udf66|\ud83c\udf67|\ud83c\udf68|\ud83c\udf69|\ud83c\udf6a|\ud83c\udf6b|\ud83c\udf6c|\ud83c\udf6d|\ud83c\udf6e|\ud83c\udf6f|\ud83c\udf70|\ud83c\udf71|\ud83c\udf72|\ud83c\udf73|\ud83c\udf74|\ud83c\udf75|\ud83c\udf76|\ud83c\udf77|\ud83c\udf78|\ud83c\udf79|\ud83c\udf7a|\ud83c\udf7b|\ud83c\udf80|\ud83c\udf81|\ud83c\udf82|\ud83c\udf83|\ud83c\udf84|\ud83c\udf85|\ud83c\udf86|\ud83c\udf87|\ud83c\udf88|\ud83c\udf89|\ud83c\udf8a|\ud83c\udf8b|\ud83c\udf8c|\ud83c\udf8d|\ud83c\udf8e|\ud83c\udf8f|\ud83c\udf90|\ud83c\udf91|\ud83c\udf92|\ud83c\udf93|\ud83c\udfa0|\ud83c\udfa1|\ud83c\udfa2|\ud83c\udfa3|\ud83c\udfa4|\ud83c\udfa5|\ud83c\udfa6|\ud83c\udfa7|\ud83c\udfa8|\ud83c\udfa9|\ud83c\udfaa|\ud83c\udfab|\ud83c\udfac|\ud83c\udfad|\ud83c\udfae|\ud83c\udfaf|\ud83c\udfb0|\ud83c\udfb1|\ud83c\udfb2|\ud83c\udfb3|\ud83c\udfb4|\ud83c\udfb5|\ud83c\udfb6|\ud83c\udfb7|\ud83c\udfb8|\ud83c\udfb9|\ud83c\udfba|\ud83c\udfbb|\ud83c\udfbc|\ud83c\udfbd|\ud83c\udfbe|\ud83c\udfbf|\ud83c\udfc0|\ud83c\udfc1|\ud83c\udfc2|\ud83c\udfc3|\ud83c\udfc4|\ud83c\udfc6|\ud83c\udfc8|\ud83c\udfca|\ud83c\udfe0|\ud83c\udfe1|\ud83c\udfe2|\ud83c\udfe3|\ud83c\udfe5|\ud83c\udfe6|\ud83c\udfe7|\ud83c\udfe8|\ud83c\udfe9|\ud83c\udfea|\ud83c\udfeb|\ud83c\udfec|\ud83c\udfed|\ud83c\udfee|\ud83c\udfef|\ud83c\udff0|\ud83d\udc0c|\ud83d\udc0d|\ud83d\udc0e|\ud83d\udc11|\ud83d\udc12|\ud83d\udc14|\ud83d\udc17|\ud83d\udc18|\ud83d\udc19|\ud83d\udc1a|\ud83d\udc1b|\ud83d\udc1c|\ud83d\udc1d|\ud83d\udc1e|\ud83d\udc1f|\ud83d\udc20|\ud83d\udc21|\ud83d\udc22|\ud83d\udc23|\ud83d\udc24|\ud83d\udc25|\ud83d\udc26|\ud83d\udc27|\ud83d\udc28|\ud83d\udc29|\ud83d\udc2b|\ud83d\udc2c|\ud83d\udc2d|\ud83d\udc2e|\ud83d\udc2f|\ud83d\udc30|\ud83d\udc31|\ud83d\udc32|\ud83d\udc33|\ud83d\udc34|\ud83d\udc35|\ud83d\udc36|\ud83d\udc37|\ud83d\udc38|\ud83d\udc39|\ud83d\udc3a|\ud83d\udc3b|\ud83d\udc3c|\ud83d\udc3d|\ud83d\udc3e|\ud83d\udc40|\ud83d\udc42|\ud83d\udc43|\ud83d\udc44|\ud83d\udc45|\ud83d\udc46|\ud83d\udc47|\ud83d\udc48|\ud83d\udc49|\ud83d\udc4a|\ud83d\udc4b|\ud83d\udc4c|\ud83d\udc4d|\ud83d\udc4e|\ud83d\udc4f|\ud83d\udc50|\ud83d\udc51|\ud83d\udc52|\ud83d\udc53|\ud83d\udc54|\ud83d\udc55|\ud83d\udc56|\ud83d\udc57|\ud83d\udc58|\ud83d\udc59|\ud83d\udc5a|\ud83d\udc5b|\ud83d\udc5c|\ud83d\udc5d|\ud83d\udc5e|\ud83d\udc5f|\ud83d\udc60|\ud83d\udc61|\ud83d\udc62|\ud83d\udc63|\ud83d\udc64|\ud83d\udc66|\ud83d\udc67|\ud83d\udc68|\ud83d\udc69|\ud83d\udc6a|\ud83d\udc6b|\ud83d\udc6e|\ud83d\udc6f|\ud83d\udc70|\ud83d\udc71|\ud83d\udc72|\ud83d\udc73|\ud83d\udc74|\ud83d\udc75|\ud83d\udc76|\ud83d\udeb4|\ud83d\udc78|\ud83d\udc79|\ud83d\udc7a|\ud83d\udc7b|\ud83d\udc7c|\ud83d\udc7d|\ud83d\udc7e|\ud83d\udc7f|\ud83d\udc80|\ud83d\udc81|\ud83d\udc82|\ud83d\udc83|\ud83d\udc84|\ud83d\udc85|\ud83d\udc86|\ud83d\udc87|\ud83d\udc88|\ud83d\udc89|\ud83d\udc8a|\ud83d\udc8b|\ud83d\udc8c|\ud83d\udc8d|\ud83d\udc8e|\ud83d\udc8f|\ud83d\udc90|\ud83d\udc91|\ud83d\udc92|\ud83d\udc93|\ud83d\udc94|\ud83d\udc95|\ud83d\udc96|\ud83d\udc97|\ud83d\udc98|\ud83d\udc99|\ud83d\udc9a|\ud83d\udc9b|\ud83d\udc9c|\ud83d\udc9d|\ud83d\udc9e|\ud83d\udc9f|\ud83d\udca0|\ud83d\udca1|\ud83d\udca2|\ud83d\udca3|\ud83d\udca4|\ud83d\udca5|\ud83d\udca6|\ud83d\udca7|\ud83d\udca8|\ud83d\udca9|\ud83d\udcaa|\ud83d\udcab|\ud83d\udcac|\ud83d\udcae|\ud83d\udcaf|\ud83d\udcb0|\ud83d\udcb1|\ud83d\udcb2|\ud83d\udcb3|\ud83d\udcb4|\ud83d\udcb5|\ud83d\udcb8|\ud83d\udcb9|\ud83d\udcba|\ud83d\udcbb|\ud83d\udcbc|\ud83d\udcbd|\ud83d\udcbe|\ud83d\udcbf|\ud83d\udcc0|\ud83d\udcc1|\ud83d\udcc2|\ud83d\udcc3|\ud83d\udcc4|\ud83d\udcc5|\ud83d\udcc6|\ud83d\udcc7|\ud83d\udcc8|\ud83d\udcc9|\ud83d\udcca|\ud83d\udccb|\ud83d\udccc|\ud83d\udccd|\ud83d\udcce|\ud83d\udccf|\ud83d\udcd0|\ud83d\udcd1|\ud83d\udcd2|\ud83d\udcd3|\ud83d\udcd4|\ud83d\udcd5|\ud83d\udcd6|\ud83d\udcd7|\ud83d\udcd8|\ud83d\udcd9|\ud83d\udcda|\ud83d\udcdb|\ud83d\udcdc|\ud83d\udcdd|\ud83d\udcde|\ud83d\udcdf|\ud83d\udce0|\ud83d\udce1|\ud83d\udce2|\ud83d\udce3|\ud83d\udce4|\ud83d\udce5|\ud83d\udce6|\ud83d\udce7|\ud83d\udce8|\ud83d\udce9|\ud83d\udcea|\ud83d\udceb|\ud83d\udcee|\ud83d\udcf0|\ud83d\udcf1|\ud83d\udcf2|\ud83d\udcf3|\ud83d\udcf4|\ud83d\udcf6|\ud83d\udcf7|\ud83d\udcf9|\ud83d\udcfa|\ud83d\udcfb|\ud83d\udcfc|\ud83d\udd03|\ud83d\udd0a|\ud83d\udd0b|\ud83d\udd0c|\ud83d\udd0d|\ud83d\udd0e|\ud83d\udd0f|\ud83d\udd10|\ud83d\udd11|\ud83d\udd12|\ud83d\udd13|\ud83d\udd14|\ud83d\udd16|\ud83d\udd17|\ud83d\udd18|\ud83d\udd19|\ud83d\udd1a|\ud83d\udd1b|\ud83d\udd1c|\ud83d\udd1d|\ud83d\udd1e|\ud83d\udd1f|\ud83d\udd20|\ud83d\udd21|\ud83d\udd22|\ud83d\udd23|\ud83d\udd24|\ud83d\udd25|\ud83d\udd26|\ud83d\udd27|\ud83d\udd28|\ud83d\udd29|\ud83d\udd2a|\ud83d\udd2b|\ud83d\udd2e|\ud83d\udd2f|\ud83d\udd30|\ud83d\udd31|\ud83d\udd32|\ud83d\udd33|\ud83d\udd34|\ud83d\udd35|\ud83d\udd36|\ud83d\udd37|\ud83d\udd38|\ud83d\udd39|\ud83d\udd3a|\ud83d\udd3b|\ud83d\udd3c|\ud83d\udd3d|\ud83d\udd50|\ud83d\udd51|\ud83d\udd52|\ud83d\udd53|\ud83d\udd54|\ud83d\udd55|\ud83d\udd56|\ud83d\udd57|\ud83d\udd58|\ud83d\udd59|\ud83d\udd5a|\ud83d\udd5b|\ud83d\uddfb|\ud83d\uddfc|\ud83d\uddfd|\ud83d\uddfe|\ud83d\uddff|\ud83d\ude01|\ud83d\ude02|\ud83d\ude03|\ud83d\ude04|\ud83d\ude05|\ud83d\ude06|\ud83d\ude09|\ud83d\ude0a|\ud83d\ude0b|\ud83d\ude0c|\ud83d\ude0d|\ud83d\ude0f|\ud83d\ude12|\ud83d\ude13|\ud83d\ude14|\ud83d\ude16|\ud83d\ude18|\ud83d\ude1a|\ud83d\ude1c|\ud83d\ude1d|\ud83d\ude1e|\ud83d\ude20|\ud83d\ude21|\ud83d\ude22|\ud83d\ude23|\ud83d\ude24|\ud83d\ude25|\ud83d\ude28|\ud83d\ude29|\ud83d\ude2a|\ud83d\ude2b|\ud83d\ude2d|\ud83d\ude30|\ud83d\ude31|\ud83d\ude32|\ud83d\ude33|\ud83d\ude35|\ud83d\ude37|\ud83d\ude38|\ud83d\ude39|\ud83d\ude3a|\ud83d\ude3b|\ud83d\ude3c|\ud83d\ude3d|\ud83d\ude3e|\ud83d\ude3f|\ud83d\ude40|\ud83d\ude45|\ud83d\ude46|\ud83d\ude47|\ud83d\ude48|\ud83d\ude49|\ud83d\ude4a|\ud83d\ude4b|\ud83d\ude4c|\ud83d\ude4d|\ud83d\ude4e|\ud83d\ude4f|\ud83d\ude80|\ud83d\ude83|\ud83d\ude84|\ud83d\ude85|\ud83d\ude87|\ud83d\ude89|\ud83d\ude8c|\ud83d\ude8f|\ud83d\ude91|\ud83d\ude92|\ud83d\ude93|\ud83d\ude95|\ud83d\ude97|\ud83d\ude99|\ud83d\ude9a|\ud83d\udea2|\ud83d\udea4|\ud83d\udea5|\ud83d\udea7|\ud83d\udea8|\ud83d\udea9|\ud83d\udeaa|\ud83d\udeab|\ud83d\udeac|\ud83d\udead|\ud83d\udeb2|\ud83d\udeb6|\ud83d\udeb9|\ud83d\udeba|\ud83d\udebb|\ud83d\udebc|\ud83d\udebd|\ud83d\udebe|\ud83d\udec0|\ud83c\udde6|\ud83c\udde7|\ud83c\udde8|\ud83c\udde9|\ud83c\uddea|\ud83c\uddeb|\ud83c\uddec|\ud83c\udded|\ud83c\uddee|\ud83c\uddef|\ud83c\uddf0|\ud83c\uddf1|\ud83c\uddf2|\ud83c\uddf3|\ud83c\uddf4|\ud83c\uddf5|\ud83c\uddf6|\ud83c\uddf7|\ud83c\uddf8|\ud83c\uddf9|\ud83c\uddfa|\ud83c\uddfb|\ud83c\uddfc|\ud83c\uddfd|\ud83c\uddfe|\ud83c\uddff|\ud83c\udf0d|\ud83c\udf0e|\ud83c\udf10|\ud83c\udf12|\ud83c\udf16|\ud83c\udf17|\ue50a|\u3030|\u27b0|\u2797|\u2796|\u2795|\u2755|\u2754|\u2753|\u274e|\u274c|\u2728|\u270b|\u270a|\u2705|\u26ce|\u23f3|\u23f0|\u23ec|\u23eb|\u23ea|\u23e9|\u2122|\u27bf|\u00a9|\u00ae)|(?:(?:\ud83c\udc04|\ud83c\udd7f|\ud83c\ude1a|\ud83c\ude2f|\u3299|\u303d|\u2b55|\u2b50|\u2b1c|\u2b1b|\u2b07|\u2b06|\u2b05|\u2935|\u2934|\u27a1|\u2764|\u2757|\u2747|\u2744|\u2734|\u2733|\u2716|\u2714|\u2712|\u270f|\u270c|\u2709|\u2708|\u2702|\u26fd|\u26fa|\u26f5|\u26f3|\u26f2|\u26ea|\u26d4|\u26c5|\u26c4|\u26be|\u26bd|\u26ab|\u26aa|\u26a1|\u26a0|\u2693|\u267f|\u267b|\u3297|\u2666|\u2665|\u2663|\u2660|\u2653|\u2652|\u2651|\u2650|\u264f|\u264e|\u264d|\u264c|\u264b|\u264a|\u2649|\u2648|\u263a|\u261d|\u2615|\u2614|\u2611|\u260e|\u2601|\u2600|\u25fe|\u25fd|\u25fc|\u25fb|\u25c0|\u25b6|\u25ab|\u25aa|\u24c2|\u231b|\u231a|\u21aa|\u21a9|\u2199|\u2198|\u2197|\u2196|\u2195|\u2194|\u2139|\u2049|\u203c|\u2668)([\uFE0E\uFE0F]?)))/g, + SVGPATH: SVGPATH, ZREKNARF: '' + SVGPATH + '', CHAT_BUTTON: '' + SVGPATH + '', diff --git a/src/ember/channel.js b/src/ember/channel.js index fb782ffc..a8fdd2e6 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -8,6 +8,12 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.setup_channel = function() { + // Style Stuff! + this.log("Creating channel style element."); + var s = this._channel_style = document.createElement("style"); + s.id = "ffz-channel-css"; + document.head.appendChild(s); + // Settings stuff! document.body.classList.toggle("ffz-hide-view-count", !this.settings.channel_views); @@ -72,13 +78,32 @@ FFZ.prototype.setup_channel = function() { ffzHostTarget: function() { var target = this.get('content.hostModeTarget'), name = target && target.get('name'), + id = target && target.get('id'), display_name = target && target.get('display_name'); + if ( id !== f.__old_host_target ) { + if ( f.__old_host_target ) + f.ws_send("unsub_channel", f.__old_host_target); + + if ( id ) { + f.ws_send("sub_channel", id); + f.__old_host_target = id; + } else + delete f.__old_host_target; + } + if ( display_name ) FFZ.capitalization[name] = [display_name, Date.now()]; if ( f.settings.group_tabs && f._chatv ) f._chatv.ffzRebuildTabs(); + + if ( f.settings.follow_buttons ) + f.rebuild_following_ui(); + + if ( f.settings.srl_races ) + f.rebuild_race_ui(); + }.observes("content.hostModeTarget") }); } @@ -107,15 +132,28 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzInit: function() { + var id = this.get('controller.id'), + el = this.get('element'); + f._cindex = this; - this.get('element').setAttribute('data-channel', this.get('controller.id')); + f.ws_send("sub_channel", id); + + el.setAttribute('data-channel', id); + el.classList.add('ffz-channel'); + this.ffzFixTitle(); this.ffzUpdateUptime(); this.ffzUpdateChatters(); - var el = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') - if ( el ) - el.parentNode.classList.add('twitch-channel-views'); + var views = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') + if ( views ) + views.parentNode.classList.add('twitch-channel-views'); + + if ( f.settings.follow_buttons ) + f.rebuild_following_ui(); + + if ( f.settings.srl_races ) + f.rebuild_race_ui(); }, ffzFixTitle: function() { @@ -129,7 +167,10 @@ FFZ.prototype._modify_cindex = function(view) { this.$(".title span").each(function(i, el) { var scripts = el.querySelectorAll("script"); - el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; + if ( ! scripts.length ) + el.innerHTML = status; + else + el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; }); }, @@ -148,7 +189,8 @@ FFZ.prototype._modify_cindex = function(view) { } var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length, - ffz_chatters = room.ffz_chatters || 0; + ffz_chatters = room.ffz_chatters || 0, + ffz_viewers = room.ffz_viewers || 0; var el = this.get('element').querySelector('#ffz-chatter-display span'); if ( ! el ) { @@ -159,7 +201,7 @@ FFZ.prototype._modify_cindex = function(view) { var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-chatter-display'; - stat.title = "Current Chatters"; + stat.title = "Currently in Chat"; stat.innerHTML = constants.ROOMS + " "; el = document.createElement("span"); @@ -176,7 +218,7 @@ FFZ.prototype._modify_cindex = function(view) { el.innerHTML = utils.number_commas(chatter_count); - if ( ! ffz_chatters ) { + if ( ! ffz_chatters && ! ffz_viewers ) { el = this.get('element').querySelector('#ffz-ffzchatter-display'); el && el.parentNode.removeChild(el); return; @@ -191,7 +233,7 @@ FFZ.prototype._modify_cindex = function(view) { var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-ffzchatter-display'; - stat.title = "Chatters with FrankerFaceZ"; + stat.title = "Viewers (In Chat) with FrankerFaceZ"; stat.innerHTML = constants.ZREKNARF + " "; el = document.createElement("span"); @@ -206,7 +248,7 @@ FFZ.prototype._modify_cindex = function(view) { jQuery(stat).tipsy(); } - el.innerHTML = utils.number_commas(ffz_chatters); + el.innerHTML = utils.number_commas(ffz_viewers) + " (" + utils.number_commas(ffz_chatters) + ")"; }, @@ -273,10 +315,16 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzTeardown: function() { + var id = this.get('controller.id'); + if ( id ) + f.ws_send("unsub_channel", id); + this.get('element').setAttribute('data-channel', ''); f._cindex = undefined; if ( this._ffz_update_uptime ) clearTimeout(this._ffz_update_uptime); + + utils.update_css(f._channel_style, id, null); } }); } diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 1435f54b..d8e13383 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -17,13 +17,36 @@ var FFZ = window.FrankerFaceZ, // Settings // -------------------- + +FFZ.settings_info.minimal_chat = { + type: "boolean", + value: false, + + //no_bttv: true, + + category: "Chat", + name: "Minimalistic Chat", + help: "Hide all of the chat user interface, only showing messages and an input box.", + + on_update: function(val) { + document.body.classList.toggle("ffz-minimal-chat", val); + if ( this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { + var f = this; + setTimeout(function() { + f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + },0); + } + } + }; + + FFZ.settings_info.prevent_clear = { type: "boolean", value: false, no_bttv: true, - category: "Chat", + category: "Chat Moderation", name: "Show Deleted Messages", help: "Fade deleted messages instead of replacing them, and prevent chat from being cleared.", @@ -98,6 +121,9 @@ FFZ.settings_info.pinned_rooms = { // -------------------- FFZ.prototype.setup_chatview = function() { + //if ( ! this.has_bttv ) + document.body.classList.toggle("ffz-minimal-chat", this.settings.minimal_chat); + this.log("Hooking the Ember Chat controller."); var Chat = App.__container__.lookup('controller:chat'), @@ -108,7 +134,26 @@ FFZ.prototype.setup_chatview = function() { ffzUpdateChannels: function() { if ( f.settings.group_tabs && f._chatv ) f._chatv.ffzRebuildTabs(); - }.observes("currentChannelRoom", "connectedPrivateGroupRooms") + }.observes("currentChannelRoom", "connectedPrivateGroupRooms"), + + removeCurrentChannelRoom: function() { + if ( ! f.settings.group_tabs || f.has_bttv ) + return this._super(); + + var room = this.get("currentChannelRoom"), + room_id = room && room.get('id'); + + if ( ! f.settings.pinned_rooms || f.settings.pinned_rooms.indexOf(room_id) === -1 ) { + // We can actually destroy it. + if ( room === this.get("currentRoom") ) + this.blurRoom(); + + if ( room ) + room.destroy(); + } + + this.set("currentChannelRoom", void 0); + } }); } @@ -326,7 +371,7 @@ FFZ.prototype._modify_cview = function(view) { if ( target && Room ) { var target_id = target.get('id'); if ( this._ffz_host !== target_id ) { - if ( this._ffz_host_room ) { + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { if ( this.get('controller.currentRoom') === this._ffz_host_room ) this.get('controller').blurRoom(); this._ffz_host_room.destroy(); @@ -336,7 +381,7 @@ FFZ.prototype._modify_cview = function(view) { this._ffz_host_room = Room.findOne(target_id); } } else if ( this._ffz_host ) { - if ( this._ffz_host_room ) { + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { if ( this.get('controller.currentRoom') === this._ffz_host_room ) this.get('controller').blurRoom(); this._ffz_host_room.destroy(); @@ -409,7 +454,7 @@ FFZ.prototype._modify_cview = function(view) { }, ffzBuildTab: function(view, room, current_channel, host_channel) { - var tab = document.createElement('span'), name, unread, + var tab = document.createElement('span'), name, unread, icon = '', group = room.get('isGroupRoom'), current = room === view.get('controller.currentRoom'); @@ -421,21 +466,25 @@ FFZ.prototype._modify_cview = function(view) { tab.classList.toggle('group-chat', group); tab.classList.toggle('active', current); - name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'))); unread = format_unread(current ? 0 : room.get('unreadCount')); + name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'), function(name) { + unread = format_unread(current ? 0 : room.get('unreadCount')); + tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; + })); + if ( current_channel ) { - tab.innerHTML = constants.CAMERA; + icon = constants.CAMERA; tab.title = "Current Channel"; } else if ( host_channel ) { - tab.innerHTML = constants.EYE; + icon = constants.EYE; tab.title = "Hosted Channel"; } else if ( group ) tab.title = "Group Chat"; else tab.title = "Pinned Channel"; - tab.innerHTML += utils.sanitize(name) + '' + unread + ''; + tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; tab.addEventListener('click', function() { view.get('controller').focusRoom(room); @@ -452,7 +501,7 @@ FFZ.prototype._modify_cview = function(view) { } if ( this._ffz_host ) { - if ( this._ffz_host_room ) { + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { if ( this.get('controller.currentRoom') === this._ffz_host_room ) this.get('controller').blurRoom(); this._ffz_host_room.destroy(); diff --git a/src/ember/line.js b/src/ember/line.js index 6d689a6c..33a427c7 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -15,8 +15,12 @@ TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", + SRCSETS = {}; build_srcset = function(id) { - return TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x"; + if ( SRCSETS[id] ) + return SRCSETS[id]; + var out = SRCSETS[id] = TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x"; + return out; }, @@ -31,7 +35,7 @@ if ( ! set ) return data.code; - else if ( set == "00000turbo" || set == "turbo" ) { + else if ( set == "--twitch-turbo--" || set == "turbo" ) { set = "Twitch Turbo"; set_type = null; } @@ -181,6 +185,69 @@ // Settings // --------------------- +FFZ.settings_info.parse_emoji = { + type: "boolean", + value: true, + + category: "Chat", + + name: "Replace Emoji with Images", + help: "Replace emoji in chat messages with nicer looking images from the open-source Twitter Emoji project." + }; + + +FFZ.settings_info.room_status = { + type: "boolean", + value: true, + + category: "Chat", + no_bttv: true, + + name: "Room Status Indicators", + help: "Display the current room state (slow mode, sub mode, and r9k mode) next to the Chat button.", + + on_update: function() { + if ( this._roomv ) + this._roomv.ffzUpdateStatus(); + } + }; + + +FFZ.settings_info.scrollback_length = { + type: "button", + value: 150, + + category: "Chat", + no_bttv: true, + + name: "Scrollback Length", + help: "Set the maximum number of lines to keep in chat.", + + method: function() { + var new_val = prompt("Scrollback Length\n\nPlease enter a new maximum length for the chat scrollback. Default: 150\n\nNote: Making this too large will cause your browser to lag.", this.settings.scrollback_length); + if ( new_val === null || new_val === undefined ) + return; + + new_val = parseInt(new_val); + if ( new_val === NaN ) + return; + + if ( new_val < 10 ) + new_val = 10; + + this.settings.set("scrollback_length", new_val); + + // Update our everything. + var Chat = App.__container__.lookup('controller:chat'), + current_id = Chat && Chat.get('currentRoom.id'); + + for(var room_id in this.rooms) { + var room = this.rooms[room_id]; + room.room.set('messageBufferSize', new_val + ((this._roomv && !this._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0)); + } + } + }; + FFZ.settings_info.banned_words = { type: "button", value: [], @@ -360,6 +427,9 @@ FFZ.prototype._modify_line = function(component) { tokens = f._remove_banned(tokens); tokens = f._emoticonize(this, tokens); + if ( f.settings.parse_emoji ) + tokens = f.tokenize_emoji(tokens); + // Store the capitalization. var display = this.get("msgObject.tags.display-name"); if ( display && display.length ) @@ -395,6 +465,18 @@ FFZ.prototype._modify_line = function(component) { this.rerender(); }), + willClearRender: function() { + // This is here to prevent tipsy tooltips from hanging around. + try { + this.$('a.mod-icon').tipsy('disable'); + jQuery('body > .tipsy:last').remove(); + + } catch(err) { + f.error("LineView willClearRender: " + err); + } + this._super(); + }, + didInsertElement: function() { this._super(); try { @@ -416,8 +498,8 @@ FFZ.prototype._modify_line = function(component) { this.set("msgObject.ffz_alternate", row_type); } - el.classList.toggle('ffz-alternate', row_type); - el.classList.toggle('ffz-deleted', f.settings.prevent_clear && this.get('msgObject.ffz_deleted')); + el.classList.toggle('ffz-alternate', row_type || false); + el.classList.toggle('ffz-deleted', f.settings.prevent_clear && this.get('msgObject.ffz_deleted') || false); // Basic Data @@ -452,50 +534,9 @@ FFZ.prototype._modify_line = function(component) { // Banned Links - var bad_links = el.querySelectorAll('a.deleted-link'); - for(var i=0; i < bad_links.length; i++) { - var link = bad_links[i]; - - link.addEventListener("click", function(e) { - if ( ! this.classList.contains("deleted-link") ) - return true; - - // Get the URL - var href = this.getAttribute('data-url'), - link = href; - - // Delete Old Stuff - this.classList.remove('deleted-link'); - this.removeAttribute("data-url"); - this.removeAttribute("title"); - this.removeAttribute("original-title"); - - // Process URL - if ( href.indexOf("@") > -1 && (-1 === href.indexOf("/") || href.indexOf("@") < href.indexOf("/")) ) - href = "mailto:" + href; - else if ( ! href.match(/^https?:\/\//) ) - href = "http://" + href; - - // Set up the Link - this.href = href; - this.target = "_new"; - this.textContent = link; - - // Now, check for a tooltip. - var link_data = f._link_data[link]; - if ( link_data && typeof link_data != "boolean" ) { - this.title = link_data.tooltip; - if ( link_data.unsafe ) - this.classList.add('unsafe-link'); - } - - // Stop from Navigating - e.preventDefault(); - }); - - // Also add a nice tooltip. - jQuery(link).tipsy({html:true}); - } + var bad_links = el.querySelectorAll('span.message a.deleted-link'); + for(var i=0; i < bad_links.length; i++) + bad_links[i].addEventListener("click", f._deleted_link_click); // Link Tooltips @@ -531,12 +572,11 @@ FFZ.prototype._modify_line = function(component) { // Enhanced Emotes - var images = el.querySelectorAll('img.emoticon'); + var images = el.querySelectorAll('span.message img.emoticon'); for(var i=0; i < images.length; i++) { var img = images[i], name = img.alt, - match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(img.src), - id = match ? parseInt(match[1]) : null; + id = FFZ.src_to_id(img.src); if ( id !== null ) { // High-DPI Images @@ -554,6 +594,15 @@ FFZ.prototype._modify_line = function(component) { f.ws_send("twitch_emote", id, load_emote_data.bind(f, id, img.alt)); } + } else if ( img.getAttribute('data-ffz-emoji') ) { + var eid = img.getAttribute('data-ffz-emoji'), + data = f.emoji_data && f.emoji_data[eid]; + + if ( data ) { + img.setAttribute('srcset', data.srcSet); + img.title = "Emoji: " + img.alt + "\nName: " + data.short_name; + } + } else if ( img.getAttribute('data-ffz-emote') ) { var data = JSON.parse(decodeURIComponent(img.getAttribute('data-ffz-emote'))), id = data && data[0] || null, @@ -660,10 +709,6 @@ FFZ.capitalization = {}; FFZ._cap_fetching = 0; FFZ.get_capitalization = function(name, callback) { - // Use the BTTV code if it's present. - if ( window.BetterTTV && BetterTTV.chat && BetterTTV.chat.helpers.lookupDisplayName ) - return BetterTTV.chat.helpers.lookupDisplayName(name); - if ( ! name ) return name; diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index d7c5faa5..114527ec 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -1,5 +1,6 @@ var FFZ = window.FrankerFaceZ, utils = require("../utils"), + helpers, keycodes = { ESC: 27, @@ -9,31 +10,150 @@ var FFZ = window.FrankerFaceZ, U: 85 }, - btns = [ - ['5m', 300], - ['10m', 600], - ['1hr', 3600], - ['12hr', 43200], - ['24hr', 86400]], - MESSAGE = '', - CHECK = ''; + CHECK = '', + + DURATIONS = {}, + duration_string = function(val) { + if ( val === 1 ) + return 'Purge'; + + if ( DURATIONS[val] ) + return DURATIONS[val]; + + var weeks, days, hours, minutes, seconds; + + weeks = Math.floor(val / 604800); + seconds = val % 604800; + + days = Math.floor(seconds / 86400); + seconds %= 86400; + + hours = Math.floor(seconds / 3600); + seconds %= 3600; + + minutes = Math.floor(seconds / 60); + seconds %= 60; + + var out = DURATIONS[val] = (weeks ? weeks + 'w' : '') + ((days || (weeks && (hours || minutes || seconds))) ? days + 'd' : '') + ((hours || ((weeks || days) && (minutes || seconds))) ? hours + 'h' : '') + ((minutes || ((weeks || days || hours) && seconds)) ? minutes + 'm' : '') + (seconds ? seconds + 's' : ''); + return out; + }; + + +try { + helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); +} catch(err) { } // ---------------- // Settings // ---------------- -FFZ.settings_info.enhanced_moderation = { +FFZ.settings_info.chat_hover_pause = { type: "boolean", value: false, no_bttv: true, - //visible: function() { return ! this.has_bttv }, - category: "Chat", - name: "Enhanced Moderation", - help: "Use /p, /t, /u and /b in chat to moderate chat, or use hotkeys with moderation cards." + category: "Chat Moderation", + name: "Pause Chat Scrolling on Mouse Hover", + help: "Automatically prevent the chat from scrolling when moving the mouse over it to prevent moderation mistakes and link mis-clicks.", + + on_update: function(val) { + if ( ! this._roomv ) + return; + + if ( val ) + this._roomv.ffzEnableFreeze(); + else + this._roomv.ffzDisableFreeze(); + } + }; + + +FFZ.settings_info.short_commands = { + type: "boolean", + value: true, + + no_bttv: true, + category: "Chat Moderation", + + name: "Short Moderation Commands", + help: "Use /t, /b, and /u in chat in place of /timeout, /ban, /unban for quicker moderation, and use /p for 1 second timeouts." + }; + + +FFZ.settings_info.mod_card_hotkeys = { + type: "boolean", + value: false, + + no_bttv: true, + category: "Chat Moderation", + + name: "Moderation Card Hotkeys", + help: "With a moderation card selected, press B to ban the user, T to time them out for 10 minutes, P to time them out for 1 second, or U to unban them. ESC closes the card." + }; + + +FFZ.settings_info.mod_card_history = { + type: "boolean", + value: false, + + no_bttv: true, + category: "Chat Moderation", + + name: "Moderation Card History", + help: "Display a few of the user's previously sent messages on moderation cards.", + + on_update: function(val) { + if ( val || ! this.rooms ) + return; + + // Delete all history~! + for(var room_id in this.rooms) { + var room = this.rooms[room_id]; + if ( room ) + room.user_history = undefined; + } + } + }; + + +FFZ.settings_info.mod_card_durations = { + type: "button", + value: [300, 600, 3600, 43200, 86400, 604800], + + category: "Chat Moderation", + no_bttv: true, + + name: "Moderation Card Timeout Buttons", + help: "Add additional timeout buttons to moderation cards with specific durations.", + + method: function() { + var old_val = this.settings.mod_card_durations.join(", "), + new_val = prompt("Moderation Card Timeout Buttons\n\nPlease enter a comma-separated list of durations that you would like to have timeout buttons for. Durations must be expressed in seconds.\n\nEnter \"reset\" without quotes to return to the default value.", old_val); + + if ( new_val === null || new_val === undefined ) + return; + + if ( new_val === "reset" ) + new_val = FFZ.settings_info.mod_card_durations.value.join(", "); + + // Split them up. + new_val = new_val.trim().split(/[ ,]+/); + var vals = []; + + for(var i=0; i < new_val.length; i++) { + var val = parseInt(new_val[i]); + if ( val === 0 ) + val = 1; + + if ( val !== NaN && val > 0 ) + vals.push(val); + } + + this.settings.set("mod_card_durations", vals); + } }; @@ -42,23 +162,43 @@ FFZ.settings_info.enhanced_moderation = { // ---------------- FFZ.prototype.setup_mod_card = function() { + this.log("Modifying Mousetrap stopCallback so we can catch ESC."); + var orig_stop = Mousetrap.stopCallback; + Mousetrap.stopCallback = function(e, element, combo) { + if ( element.classList.contains('no-mousetrap') ) + return true; + + return orig_stop(e, element, combo); + } + + Mousetrap.bind("up up down down left right left right b a enter", function() { + var el = document.querySelector(".app-main") || document.querySelector(".ember-chat-container"); + el && el.classList.toggle('ffz-flip'); + }); + + this.log("Hooking the Ember Moderation Card view."); var Card = App.__container__.resolve('component:moderation-card'), f = this; Card.reopen({ + ffzForceRedraw: function() { + this.rerender(); + }.observes("cardInfo.isModeratorOrHigher", "cardInfo.user"), + didInsertElement: function() { this._super(); window._card = this; try { - if ( f.has_bttv || ! f.settings.enhanced_moderation ) + if ( f.has_bttv ) return; var el = this.get('element'), controller = this.get('controller'); // Style it! - el.classList.add('ffz-moderation-card'); + if ( f.settings.mod_card_hotkeys || (f.settings.mod_card_durations && f.settings.mod_card_durations.length) ) + el.classList.add('ffz-moderation-card'); // Only do the big stuff if we're mod. if ( controller.get('cardInfo.isModeratorOrHigher') ) { @@ -66,91 +206,94 @@ FFZ.prototype.setup_mod_card = function() { el.setAttribute('tabindex', 1); // Key Handling - el.addEventListener('keyup', function(e) { - var key = e.keyCode || e.which, - user_id = controller.get('cardInfo.user.id'), - room = App.__container__.lookup('controller:chat').get('currentRoom'); + if ( f.settings.mod_card_hotkeys ) { + el.classList.add('no-mousetrap'); - if ( key == keycodes.P ) - room.send("/timeout " + user_id + " 1"); - - else if ( key == keycodes.B ) - room.send("/ban " + user_id); - - else if ( key == keycodes.T ) - room.send("/timeout " + user_id + " 600"); - - else if ( key == keycodes.U ) - room.send("/unban " + user_id); - - else if ( key != keycodes.ESC ) - return; - - controller.send('hideModOverlay'); - }); - - - // Extra Moderation - var line = document.createElement('div'); - line.className = 'interface clearfix'; - - var btn_click = function(timeout) { - var user_id = controller.get('cardInfo.user.id'), + el.addEventListener('keyup', function(e) { + var key = e.keyCode || e.which, + user_id = controller.get('cardInfo.user.id'), room = App.__container__.lookup('controller:chat').get('currentRoom'); - if ( timeout === -1 ) - room.send("/unban " + user_id); - else - room.send("/timeout " + user_id + " " + timeout); - }, + if ( key == keycodes.P ) + room.send("/timeout " + user_id + " 1"); - btn_make = function(text, timeout) { - var btn = document.createElement('button'); - btn.className = 'button'; - btn.innerHTML = text; - btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + else if ( key == keycodes.B ) + room.send("/ban " + user_id); - if ( timeout === 600 ) - btn.title = "(T)" + btn.title.substr(1); - else if ( timeout === 1 ) - btn.title = "(P)urge - " + btn.title; + else if ( key == keycodes.T ) + room.send("/timeout " + user_id + " 600"); - jQuery(btn).tipsy(); + else if ( key == keycodes.U ) + room.send("/unban " + user_id); - btn.addEventListener('click', btn_click.bind(this, timeout)); - return btn; - }; + else if ( key != keycodes.ESC ) + return; - line.appendChild(btn_make('Purge', 1)); + controller.send('close'); + }); + } - var s = document.createElement('span'); - s.className = 'right'; - line.appendChild(s); + var btn_click = function(timeout) { + var user_id = controller.get('cardInfo.user.id'), + room = App.__container__.lookup('controller:chat').get('currentRoom'); - for(var i=0; i < btns.length; i++) - s.appendChild(btn_make(btns[i][0], btns[i][1])); + if ( timeout === -1 ) + room.send("/unban " + user_id); + else + room.send("/timeout " + user_id + " " + timeout); + }, - el.appendChild(line); + btn_make = function(timeout) { + var btn = document.createElement('button') + btn.className = 'button'; + btn.innerHTML = duration_string(timeout); + btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + if ( f.settings.mod_card_hotkeys && timeout === 600 ) + btn.title = "(T)" + btn.title.substr(1); + else if ( f.settings.mod_card_hotkeys && timeout === 1 ) + btn.title = "(P)urge - " + btn.title; + + jQuery(btn).tipsy(); + + btn.addEventListener('click', btn_click.bind(this, timeout)); + return btn; + }; + + if ( f.settings.mod_card_durations && f.settings.mod_card_durations.length ) { + // Extra Moderation + var line = document.createElement('div'); + line.className = 'interface clearfix'; + + line.appendChild(btn_make(1)); + + var s = document.createElement('span'); + s.className = 'right'; + line.appendChild(s); + + for(var i=0; i < f.settings.mod_card_durations.length; i++) + s.appendChild(btn_make(f.settings.mod_card_durations[i])); + + el.appendChild(line); + + // Fix Other Buttons + this.$("button.timeout").remove(); + } + + var ban_btn = el.querySelector('button.ban'); + if ( f.settings.mod_card_hotkeys ) + ban_btn.setAttribute('title', '(B)an User'); // Unban Button - var unban_btn = document.createElement('button'); unban_btn.className = 'unban button glyph-only light'; unban_btn.innerHTML = CHECK; - unban_btn.title = "(U)nban User"; + unban_btn.title = (f.settings.mod_card_hotkeys ? "(U)" : "U") + "nban User"; jQuery(unban_btn).tipsy(); unban_btn.addEventListener("click", btn_click.bind(this, -1)); - var ban_btn = el.querySelector('button.ban'); - ban_btn.setAttribute('title', '(B)an User'); - jQuery(ban_btn).after(unban_btn); - - - // Fix Other Buttons - this.$("button.timeout").remove(); } @@ -172,11 +315,52 @@ FFZ.prototype.setup_mod_card = function() { msg_btn.classList.add('glyph-only'); msg_btn.classList.add('message'); - msg_btn.title = "Message User"; + msg_btn.title = "Whisper User"; jQuery(msg_btn).tipsy(); } + // Message History + if ( f.settings.mod_card_history ) { + var Chat = App.__container__.lookup('controller:chat'), + room = Chat && Chat.get('currentRoom'), + ffz_room = room && f.rooms && f.rooms[room.get('id')], + user_history = ffz_room && ffz_room.user_history && ffz_room.user_history[controller.get('cardInfo.user.id')]; + + if ( user_history && user_history.length ) { + var history = document.createElement('ul'), + alternate = false; + history.className = 'interface clearfix chat-history'; + + for(var i=0; i < user_history.length; i++) { + var line = user_history[i], + l_el = document.createElement('li'); + + l_el.className = 'message-line chat-line'; + l_el.classList.toggle('ffz-alternate', alternate); + alternate = !alternate; + + if ( line.style ) + l_el.classList.add(line.style); + + l_el.innerHTML = (helpers ? '' + helpers.getTime(line.date) + ' ' : '') + '' + (line.style === 'action' ? '*' + line.from + ' ' : '') + f.render_tokens(line.cachedTokens) + ''; + + // Banned Links + var bad_links = l_el.querySelectorAll('a.deleted-link'); + for(var x=0; x < bad_links.length; x++) + bad_links[x].addEventListener("click", f._deleted_link_click); + + jQuery('a', l_el).tipsy({html:true}); + history.appendChild(l_el); + } + + el.appendChild(history); + + // Lazy scroll-to-bottom + history.scrollTop = history.scrollHeight; + } + } + // Focus the Element this.$().draggable({ start: function() { @@ -198,7 +382,7 @@ FFZ.prototype.setup_mod_card = function() { // Chat Commands // ---------------- -FFZ.chat_commands.purge = FFZ.chat_commands.p = function(room, args) { +FFZ.chat_commands.purge = function(room, args) { if ( ! args || ! args.length ) return "Purge Usage: /p username [more usernames separated by spaces]"; @@ -212,7 +396,11 @@ FFZ.chat_commands.purge = FFZ.chat_commands.p = function(room, args) { } } -FFZ.chat_commands.p.enabled = function() { return this.settings.enhanced_moderation; } +FFZ.chat_commands.p = function(room, args) { + return FFZ.chat_commands.purge.bind(this)(room, args); +} + +FFZ.chat_commands.p.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.t = function(room, args) { @@ -221,7 +409,7 @@ FFZ.chat_commands.t = function(room, args) { room.room.send("/timeout " + args.join(" ")); } -FFZ.chat_commands.t.enabled = function() { return this.settings.enhanced_moderation; } +FFZ.chat_commands.t.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.b = function(room, args) { @@ -238,7 +426,7 @@ FFZ.chat_commands.b = function(room, args) { } } -FFZ.chat_commands.b.enabled = function() { return this.settings.enhanced_moderation; } +FFZ.chat_commands.b.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.u = function(room, args) { @@ -255,4 +443,4 @@ FFZ.chat_commands.u = function(room, args) { } } -FFZ.chat_commands.u.enabled = function() { return this.settings.enhanced_moderation; } \ No newline at end of file +FFZ.chat_commands.u.enabled = function() { return this.settings.short_commands; } \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js index 163868f0..0657a1d7 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -26,18 +26,33 @@ FFZ.prototype.setup_room = function() { s.id = "ffz-room-css"; document.head.appendChild(s); - this.log("Hooking the Ember Room model."); + this.log("Hooking the Ember Room controller."); // Responsive ban button. - var RC = App.__container__.lookup('controller:room'); + var f = this, + RC = App.__container__.lookup('controller:room'); if ( RC ) { - var orig_action = RC._actions.banUser; + var orig_ban = RC._actions.banUser, + orig_to = RC._actions.timeoutUser; + RC._actions.banUser = function(e) { - orig_action.bind(this)(e); + orig_ban.bind(this)(e); + this.get("model").clearMessages(e.user); + } + + RC._actions.timeoutUser = function(e) { + orig_to.bind(this)(e); + this.get("model").clearMessages(e.user); + } + + RC._actions.purgeUser = function(e) { + this.get("model.tmiRoom").sendMessage("/timeout " + e.user + " 1"); this.get("model").clearMessages(e.user); } } + this.log("Hooking the Ember Room model."); + var Room = App.__container__.resolve('model:room'); this._modify_room(Room); @@ -53,6 +68,323 @@ FFZ.prototype.setup_room = function() { this._modify_room(inst); inst.ffzPatchTMI(); } + + this.log("Hooking the Ember Room view."); + + var RoomView = App.__container__.resolve('view:room'); + this._modify_rview(RoomView); + + // For some reason, this doesn't work unless we create an instance of the + // room view and then destroy it immediately. + try { + RoomView.create().destroy(); + } catch(err) { } + + // Modify all existing Room views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof RoomView) ) + continue; + + this.log("Manually updating existing Room view.", view); + try { + view.ffzInit(); + } catch(err) { + this.error("RoomView setup ffzInit: " + err); + } + } +} + + +// -------------------- +// View Customization +// -------------------- + +FFZ.prototype._modify_rview = function(view) { + var f = this; + view.reopen({ + didInsertElement: function() { + this._super(); + + try { + this.ffzInit(); + } catch(err) { + f.error("RoomView didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("RoomView willClearRender: " + err); + } + this._super(); + }, + + ffzInit: function() { + f._roomv = this; + + this.ffz_frozen = false; + + if ( f.settings.chat_hover_pause ) + this.ffzEnableFreeze(); + + if ( f.settings.room_status ) + this.ffzUpdateStatus(); + + var controller = this.get('controller'); + if ( controller ) { + controller.reopen({ + submitButtonText: function() { + if ( this.get("model.isWhisperMessage") && this.get("model.isWhispersEnabled") ) + return i18n("Whisper"); + + var wait = this.get("model.slowWait"), + msg = this.get("model.messageToSend") || ""; + + if ( (msg.charAt(0) === "/" && msg.substr(0, 4) !== "/me ") || !wait || !f.settings.room_status ) + return i18n("Chat"); + + return utils.time_to_string(wait, false, false, true); + }.property("model.isWhisperMessage", "model.isWhispersEnabled", "model.slowWait") + }); + + Ember.propertyDidChange(controller, 'submitButtonText'); + } + }, + + ffzTeardown: function() { + if ( f._roomv === this ) + f._roomv = undefined; + + this.ffzDisableFreeze(); + }, + + ffzUpdateStatus: function() { + var room = this.get('controller.model'), + + el = this.get('element'), + cont = el && el.querySelector('.chat-buttons-container'); + + if ( ! cont ) + return f.log("no container"); + + var r9k_badge = cont.querySelector('#ffz-stat-r9k'), + sub_badge = cont.querySelector('#ffz-stat-sub'), + slow_badge = cont.querySelector('#ffz-stat-slow'), + banned_badge = cont.querySelector('#ffz-stat-banned'), + btn = cont.querySelector('button'); + + if ( f.has_bttv || ! f.settings.room_status ) { + if ( r9k_badge ) + r9k_badge.parentElement.removeChild(r9k_badge); + if ( sub_badge ) + sub_badge.parentElement.removeChild(sub_badge); + if ( slow_badge ) + slow_badge.parentElement.removeChild(slow_badge); + + if ( btn ) + btn.classList.remove('ffz-waiting'); + return; + } + + if ( ! r9k_badge ) { + r9k_badge = document.createElement('span'); + r9k_badge.className = 'ffz room-state stat float-right'; + r9k_badge.id = 'ffz-stat-r9k'; + r9k_badge.innerHTML = 'R9K'; + r9k_badge.title = "This room is in R9K-mode."; + cont.appendChild(r9k_badge); + jQuery(r9k_badge).tipsy({gravity:"s", offset:15}); + } + + if ( ! sub_badge ) { + sub_badge = document.createElement('span'); + sub_badge.className = 'ffz room-state stat float-right'; + sub_badge.id = 'ffz-stat-sub'; + sub_badge.innerHTML = 'SUB'; + sub_badge.title = "This room is in subscribers-only mode."; + cont.appendChild(sub_badge); + jQuery(sub_badge).tipsy({gravity:"s", offset:15}); + } + + if ( ! slow_badge ) { + slow_badge = document.createElement('span'); + slow_badge.className = 'ffz room-state stat float-right'; + slow_badge.id = 'ffz-stat-slow'; + slow_badge.innerHTML = 'SLOW'; + slow_badge.title = "This room is in slow mode. You may send messages every 120 seconds."; + cont.appendChild(slow_badge); + jQuery(slow_badge).tipsy({gravity:"s", offset:15}); + } + + if ( ! banned_badge ) { + banned_badge = document.createElement('span'); + banned_badge.className = 'ffz room-state stat float-right'; + banned_badge.id = 'ffz-stat-banned'; + banned_badge.innerHTML = 'BAN'; + banned_badge.title = "You have been banned from talking in this room."; + cont.appendChild(banned_badge); + jQuery(banned_badge).tipsy({gravity:"s", offset:15}); + } + + r9k_badge.classList.toggle('hidden', !(room && room.get('r9kMode'))); + sub_badge.classList.toggle('hidden', !(room && room.get('subsOnlyMode'))); + slow_badge.classList.toggle('hidden', !(room && room.get('slowMode'))); + slow_badge.title = "This room is in slow mode. You may send messages every " + utils.number_commas(room && room.get('slowValue')||120) + " seconds."; + banned_badge.classList.toggle('hidden', !(room && room.get('ffz_banned'))); + + if ( btn ) { + btn.classList.toggle('ffz-waiting', (room && room.get('slowWait') || 0)); + btn.classList.toggle('ffz-banned', (room && room.get('ffz_banned'))); + } + + }.observes('controller.model'), + + ffzEnableFreeze: function() { + var el = this.get('element'), + messages = el.querySelector('.chat-messages'); + + if ( ! messages ) + return; + + this._ffz_interval = setInterval(this.ffzPulse.bind(this), 200); + this._ffz_messages = messages; + this._ffz_mouse_move = this.ffzMouseMove.bind(this); + this._ffz_mouse_out = this.ffzMouseOut.bind(this); + + messages.addEventListener('mousemove', this._ffz_mouse_move); + messages.addEventListener('mouseout', this._ffz_mouse_out); + document.addEventListener('mouseout', this._ffz_mouse_out); + }, + + ffzDisableFreeze: function() { + if ( this._ffz_interval ) { + clearInterval(this._ffz_interval); + this._ffz_interval = undefined; + } + + this.ffzUnfreeze(); + + var messages = this._ffz_messages; + if ( ! messages ) + return; + + this._ffz_messages = undefined; + + if ( this._ffz_mouse_move ) { + messages.removeEventListener('mousemove', this._ffz_mouse_move); + this._ffz_mouse_move = undefined; + } + + if ( this._ffz_mouse_out ) { + messages.removeEventListener('mouseout', this._ffz_mouse_out); + this._ffz_mouse_out = undefined; + } + }, + + ffzPulse: function() { + if ( this.ffz_frozen ) { + var elapsed = Date.now() - this._ffz_last_move; + if ( elapsed > 750 ) + this.ffzUnfreeze(); + } + }, + + ffzUnfreeze: function() { + this.ffz_frozen = false; + this._ffz_last_move = 0; + this.ffzUnwarnPaused(); + + if ( this.get('stuckToBottom') ) + this._scrollToBottom(); + }, + + ffzMouseOut: function(event) { + this._ffz_outside = true; + var e = this; + Ember.run.next(function() { + if ( e._ffz_outside ) + e.ffzUnfreeze(); + }); + }, + + ffzMouseMove: function(event) { + this._ffz_last_move = Date.now(); + this._ffz_outside = false; + + if ( event.screenX === this._ffz_last_screenx && event.screenY === this._ffz_last_screeny ) + return; + + this._ffz_last_screenx = event.screenX; + this._ffz_last_screeny = event.screenY; + + if ( this.ffz_frozen ) + return; + + // Don't do it if we're over the bar itself. + if ( event.clientY >= (this._ffz_messages.getBoundingClientRect().bottom - 21) ) + return; + + this.ffz_frozen = true; + if ( this.get('stuckToBottom') ) { + this.set('controller.model.messageBufferSize', f.settings.scrollback_length + 150); + this.ffzWarnPaused(); + } + }, + + _scrollToBottom: _.throttle(function() { + var e = this, + s = this._$chatMessagesScroller; + + Ember.run.next(function() { + setTimeout(function() { + !e.ffz_frozen && s && s.length && (s.scrollTop(s[0].scrollHeight), e._setStuckToBottom(!0)); + }) + }) + }, 200), + + _setStuckToBottom: function(val) { + this.set("stuckToBottom", val); + this.get("controller.model") && this.set("controller.model.messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150)); + }, + + // Warnings~! + ffzWarnPaused: function() { + var el = this.get('element'), + warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator'); + + if ( ! el ) + return; + + if ( ! warning ) { + warning = document.createElement('div'); + warning.className = 'more-messages-indicator ffz-freeze-indicator'; + warning.innerHTML = '(Chat Paused Due to Mouse Movement)'; + + var cont = el.querySelector('.chat-interface'); + if ( ! cont ) + return; + cont.insertBefore(warning, cont.childNodes[0]) + } + + warning.classList.remove('hidden'); + }, + + + ffzUnwarnPaused: function() { + var el = this.get('element'), + warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator'); + + if ( warning ) + warning.classList.add('hidden'); + } + + }); } @@ -196,6 +528,27 @@ FFZ.prototype.add_room = function(id, room) { // Create a basic data table for this room. var data = this.rooms[id] = {id: id, room: room, menu_sets: [], sets: [], css: null, needs_history: false}; + if ( this.follow_sets && this.follow_sets[id] ) { + data.extra_sets = this.follow_sets[id]; + delete this.follow_sets[id]; + + for(var i=0; i < data.extra_sets.length; i++) { + var sid = data.extra_sets[i], + set = this.emote_sets && this.emote_sets[sid]; + + if ( set ) { + if ( set.users.indexOf(id) === -1 ) + set.users.push(id); + continue; + } + + this.load_set(sid, function(success, data) { + if ( success ) + data.users.push(id); + }); + } + } + // Let the server know where we are. this.ws_send("sub", id); @@ -205,6 +558,9 @@ FFZ.prototype.add_room = function(id, room) { data.needs_history = true; } + // Why don't we set the scrollback length, too? + room.set('messageBufferSize', this.settings.scrollback_length + ((this._roomv && !this._roomv.get('stuckToBottom') && this._roomv.get('controller.model.id') === id) ? 150 : 0)); + // For now, we use the legacy function to grab the .css file. this.load_room(id); } @@ -412,6 +768,12 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( this.rooms[room_id] ) data.room = this.rooms[room_id].room; + // Preserve everything else. + for(var key in this.rooms[room_id]) { + if ( key !== 'room' && this.rooms[room_id].hasOwnProperty(key) && ! data.hasOwnProperty(key) ) + data[key] = this.rooms[room_id][key]; + } + data.needs_history = this.rooms[room_id] && this.rooms[room_id].needs_history || false; this.rooms[room_id] = data; @@ -420,7 +782,12 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { utils.update_css(this._room_style, room_id, moderator_css(data) + (data.css||"")); if ( ! this.emote_sets.hasOwnProperty(data.set) ) - this.load_set(data.set); + this.load_set(data.set, function(success, set) { + if ( set.users.indexOf(room_id) === -1 ) + set.users.push(room_id); + }); + else if ( this.emote_sets[data.set].users.indexOf(room_id) === -1 ) + this.emote_sets[data.set].users.push(room_id); this.update_ui_link(); @@ -436,6 +803,42 @@ FFZ.prototype._load_room_json = function(room_id, callback, data) { FFZ.prototype._modify_room = function(room) { var f = this; room.reopen({ + subsOnlyMode: false, + r9kMode: false, + slowWaiting: false, + slowValue: 0, + + updateWait: function(value, was_banned) { + var wait = this.get('slowWait') || 0; + this.set('slowWait', value); + if ( wait < 1 && value > 0 ) { + setTimeout(this.ffzUpdateWait.bind(this), 1000); + f._roomv && f._roomv.ffzUpdateStatus(); + } else if ( (wait > 0 && value < 1) || was_banned ) { + this.set('ffz_banned', false); + f._roomv && f._roomv.ffzUpdateStatus(); + } + }, + + ffzUpdateWait: function() { + var wait = this.get('slowWait') || 0; + if ( wait < 1 ) + return; + + this.set('slowWait', --wait); + if ( wait > 0 ) + setTimeout(this.ffzUpdateWait.bind(this), 1000); + else { + this.set('ffz_banned', false); + f._roomv && f._roomv.ffzUpdateStatus(); + } + }, + + ffzUpdateStatus: function() { + if ( f._roomv ) + f._roomv.ffzUpdateStatus(); + }.observes('r9kMode', 'subsOnlyMode', 'slowMode', 'slowValue', 'ffz_banned'), + // Track which rooms the user is currently in. init: function() { this._super(); @@ -466,6 +869,23 @@ FFZ.prototype._modify_room = function(room) { t.set("messages." + n + ".deleted", true); } }); + + if ( f.settings.mod_card_history ) { + var room = f.rooms && f.rooms[t.get('id')], + user_history = room && room.user_history && room.user_history[user] + + if ( user_history !== null && user_history !== undefined ) { + var has_delete = false, + last = user_history.length > 0 ? user_history[user_history.length-1] : null; + + has_delete = last !== null && last.is_delete; + if ( ! has_delete ) { + user_history.push({from: 'jtv', is_delete: true, style: 'admin', cachedTokens: ['User has been timed out.'], date: new Date()}); + while ( user_history.length > 20 ) + user_history.shift(); + } + } + } } else { if ( f.settings.prevent_clear ) this.addTmiMessage("A moderator's attempt to clear chat was ignored."); @@ -504,6 +924,39 @@ FFZ.prototype._modify_room = function(room) { msg.room = this.get('id'); f.tokenize_chat_line(msg); + + // Keep the history. + if ( ! is_whisper && msg.from && msg.from !== 'jtv' && msg.from !== 'twitchnotify' && f.settings.mod_card_history ) { + var room = f.rooms && f.rooms[msg.room]; + if ( room ) { + var chat_history = room.user_history = room.user_history || {}, + user_history = room.user_history[msg.from] = room.user_history[msg.from] || []; + + user_history.push({ + from: msg.tags && msg.tags['display-name'] || msg.from, + cachedTokens: msg.cachedTokens, + style: msg.style, + date: msg.date + }); + while ( user_history.length > 20 ) + user_history.shift(); + } + } + + // Check for message from us. + if ( ! is_whisper ) { + var user = f.get_user(); + if ( user && user.login === msg.from ) { + var was_banned = this.get('ffz_banned'); + this.set('ffz_banned', false); + + // Update the wait time. + if ( this.get('isModeratorOrHigher') || ! this.get('slowMode') ) + this.updateWait(0, was_banned) + else if ( this.get('slowMode') ) + this.updateWait(this.get('slowValue')); + } + } } } catch(err) { f.error("Room addMessage: " + err); @@ -578,6 +1031,7 @@ FFZ.prototype._modify_room = function(room) { }); }, + ffzUpdateChatters: function(add, remove) { var chatters = this.get("ffz_chatters") || {}; if ( add ) @@ -591,8 +1045,10 @@ FFZ.prototype._modify_room = function(room) { if ( f._cindex ) f._cindex.ffzUpdateChatters(); - if ( window.parent && window.parent.postMessage ) - window.parent.postMessage({from_ffz: true, command: 'chatter_count', message: Object.keys(this.get('ffz_chatters') || {}).length}, "http://www.twitch.tv/"); + try { + if ( window.parent && window.parent.postMessage ) + window.parent.postMessage({from_ffz: true, command: 'chatter_count', message: Object.keys(this.get('ffz_chatters') || {}).length}, "http://www.twitch.tv/"); + } catch(err) { /* Ignore errors because of security */ } }, @@ -606,24 +1062,8 @@ FFZ.prototype._modify_room = function(room) { var tmi = this.get('tmiRoom'), room = this; - // This method is stupid and bad and it leaks between rooms. - if ( ! tmi.ffz_notice_patched ) { - tmi.ffz_notice_patched = true; - - tmi._roomConn.off("notice", tmi._onNotice, tmi); - tmi._roomConn.on("notice", function(ircMsg) { - var target = ircMsg.target || (ircMsg.params && ircMsg.params[0]) || this.ircChannel; - if( target != this.ircChannel ) - return; - - this._trigger("notice", { - msgId: ircMsg.tags['msg-id'], - message: ircMsg.message - }); - }, tmi); - } - // Let's get chatter information! + // TODO: Remove this cause it's terrible. var connection = tmi._roomConn._connection; if ( ! connection.ffz_cap_patched ) { connection.ffz_cap_patched = true; @@ -632,18 +1072,83 @@ FFZ.prototype._modify_room = function(room) { connection.on("opened", function() { this._send("CAP REQ :twitch.tv/membership"); }, connection); - - // Since TMI starts sending SPECIALUSER with this, we need to - // ignore that. \ CatBag / - var orig_handle = connection._handleTmiPrivmsg.bind(connection); - connection._handleTmiPrivmsg = function(msg) { - if ( msg.message && msg.message.split(" ",1)[0] === "SPECIALUSER" ) - return; - return orig_handle(msg); - } } + // NOTICE for catching slow-mode updates + tmi.on('notice', function(msg) { + if ( msg.msgId === 'msg_slowmode' ) { + var match = /in (\d+) seconds/.exec(msg.message); + if ( match ) { + room.updateWait(parseInt(match[1])); + } + } + + if ( msg.msgId === 'msg_timedout' ) { + var match = /for (\d+) more seconds/.exec(msg.message); + if ( match ) { + room.set('ffz_banned', true); + room.updateWait(parseInt(match[1])); + } + } + + if ( msg.msgId === 'msg_banned' ) { + room.set('ffz_banned', true); + f._roomv && f._roomv.ffzUpdateStatus(); + } + }); + + + // ROOMSTATE~! + if ( ! connection.ffz_roomstate_patched ) { + connection.ffz_roomstate_patched = true; + connection._socket.off('data', connection._onSocketDataReceived, connection); + connection._socket.on('data', function(data) { + try { + var msg = utils.splitIRCMessage(data.data); + if ( msg.command === 'ROOMSTATE' ) { + // We have ROOMSTATE! Now, let's parse it a bit + // more and send it on. + msg.tags = utils.parseIRCTags(msg.tags); + msg.target = msg.params && msg.params[0]; + + this._trigger('roomstate', msg); + return; + } + } catch(err) { f.error("Connection onData: " + err); } + + return this._onSocketDataReceived(data); + }, connection); + } + + // Glorious ROOMSTATE. + if ( ! tmi.ffz_roomstate_patched ) { + tmi.ffz_roomstate_patched = true; + tmi._roomConn.on("roomstate", function(ircMsg) { + if ( ircMsg.target !== this.ircChannel ) + return; + + this._trigger("roomstate", ircMsg.tags); + }, tmi); + } + + // IT IS GLORIOUS! + tmi.on('roomstate', function(state) { + if ( state.hasOwnProperty('slow') ) { + room.set('slowMode', state.slow > 0); + room.set('slowValue', state.slow); + if ( ! room.get('slowMode') ) + room.updateWait(0); + } + + if ( state.hasOwnProperty('r9k') ) + room.set('r9kMode', state.r9k); + + if ( state.hasOwnProperty('subs-only') ) + room.set('subsOnlyMode', state['subs-only']); + }); + + // Check this shit. tmi._roomConn._connection.off("message", tmi._roomConn._onIrcMessage, tmi._roomConn); @@ -675,32 +1180,6 @@ FFZ.prototype._modify_room = function(room) { tmi._roomConn._connection.on("message", tmi._roomConn._onIrcMessage, tmi._roomConn); - - // Okay, we need to patch the *session's* updateUserState - if ( ! tmi.session.ffz_patched ) { - tmi.session.ffz_patched = true; - var uus = tmi.session._updateUserState.bind(tmi.session); - - tmi.session._updateUserState = function(user, tags) { - try { - if ( tags.color ) - this._onUserColorChanged(user, tags.color); - - if ( tags['display-name'] ) - this._onUserDisplayNameChanged(user, tags['display-name']); - - if ( tags.turbo ) - this._onUserSpecialAdded(user, 'turbo'); - - if ( tags['user_type'] === 'staff' || tags['user_type'] === 'admin' || tags['user_type'] === 'global_mod' ) - this._onUserSpecialAdded(user, tags['user-type']); - - } catch(err) { - f.error("SessionManager _updateUserState: " + err); - } - } - } - this.set('ffz_is_patched', true); }.observes('tmiRoom') diff --git a/src/emoticons.js b/src/emoticons.js index 38b4908e..0893bb40 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -57,6 +57,7 @@ var FFZ = window.FrankerFaceZ, FFZ.prototype.setup_emoticons = function() { this.log("Preparing emoticon system."); + this.emoji_data = {}; this.emote_sets = {}; this.global_sets = []; this.default_sets = []; @@ -74,6 +75,9 @@ FFZ.prototype.setup_emoticons = function() { this.log("Loading global emote sets."); this.load_global_sets(); + this.log("Loading emoji data."); + this.load_emoji_data(); + this.log("Watching Twitch emoticon parser to ensure it loads."); this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); } @@ -155,7 +159,7 @@ FFZ.prototype.getEmotes = function(user_id, room_id) { var user = this.users && this.users[user_id], room = this.rooms && this.rooms[room_id]; - return _.union(user && user.sets || [], room && room.set && [room.set] || [], this.default_sets); + return _.union(user && user.sets || [], room && room.set && [room.set] || [], room && room.extra_sets || [], this.default_sets); } @@ -194,6 +198,49 @@ FFZ.prototype._emote_tooltip = function(emote) { } +// --------------------- +// Emoji Loading +// --------------------- + +FFZ.prototype.load_emoji_data = function(callback, tries) { + var f = this; + jQuery.getJSON(constants.SERVER + "emoji/emoji.json") + .done(function(data) { + var new_data = {}; + for(var eid in data) { + var emoji = data[eid]; + eid = eid.toLowerCase(); + new_data[eid] = emoji; + + emoji.src = constants.SERVER + 'emoji/' + eid + '-1x.png'; + emoji.srcSet = emoji.src + ' 1x, ' + constants.SERVER + 'emoji/' + eid + '-2x.png 2x, ' + constants.SERVER + 'emoji/' + eid + '-4x.png 4x'; + + emoji.token = { + srcSet: emoji.srcSet, + emoticonSrc: emoji.src + '" data-ffz-emoji="' + eid + '" height="18px', + ffzEmoji: eid, + }; + + } + + f.emoji_data = new_data; + f.log("Loaded data on " + Object.keys(new_data).length + " emoji."); + if ( typeof callback === "function" ) + callback(true, data); + + }).fail(function(data) { + if ( data.status === 404 ) + return typeof callback === "function" && callback(false); + + tries = (tries || 0) + 1; + if ( tries < 50 ) + return f.load_emoji(callback, tries); + + return typeof callback === "function" && callback(false); + }); +} + + // --------------------- // Set Loading // --------------------- @@ -264,9 +311,12 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { if ( ! data ) return typeof callback == "function" && callback(false); + // Do we have existing users? + var users = this.emote_sets[set_id] && this.emote_sets[set_id].users || []; + // Store our set. this.emote_sets[set_id] = data; - data.users = []; + data.users = users; data.count = 0; diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index d5e25407..5d888069 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -1,4 +1,6 @@ var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'), SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; @@ -37,6 +39,16 @@ FFZ.prototype.setup_bttv = function(delay) { this._chatv.ffzDisableTabs(); } + if ( this._roomv ) { + // Disable Chat Pause + if ( this.settings.chat_hover_pause ) + this._roomv.ffzDisableFreeze(); + + // And hide the status + if ( this.settings.room_status ) + this._roomv.ffzUpdateStatus(); + } + // Disable other features too. document.body.classList.remove("ffz-chat-colors"); document.body.classList.remove("ffz-chat-background"); @@ -45,6 +57,7 @@ FFZ.prototype.setup_bttv = function(delay) { if ( this.is_dashboard ) this._update_subscribers(); + document.body.classList.add('ffz-bttv'); // Send Message Behavior var original_send = BetterTTV.chat.helpers.sendMessage, f = this; @@ -91,6 +104,26 @@ FFZ.prototype.setup_bttv = function(delay) { } } + // Whispers too! + var original_whisper = BetterTTV.chat.templates.whisper; + BetterTTV.chat.templates.whisper = function(data) { + try { + // Handle badges. + f.bttv_badges(data); + + // Now, do everything else manually because things are hard-coded. + return '
' + + BetterTTV.chat.templates.timestamp(data.time) + ' ' + + (data.badges && data.badges.length ? BetterTTV.chat.templates.badges(data.badges) : '') + + BetterTTV.chat.templates.whisperName(data.sender, data.receiver, data.from, data.to, data.fromColor, data.toColor) + + BetterTTV.chat.templates.message(data.sender, data.message, data.emotes, false) + + '
'; + } catch(err) { + f.log("Error: ", err); + return original_whisper(data); + } + } + // Message Renderer. I had to completely rewrite this method to get it to // use my replacement emoticonizer. var original_message = BetterTTV.chat.templates.message, @@ -151,46 +184,80 @@ FFZ.prototype.setup_bttv = function(delay) { }); // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; + if ( emotes.length ) { + // Why is emote parsing so bad? ;_; + _.each(emotes, function(emote) { + var tooltip = f._emote_tooltip(emote), + eo = ['' + tooltip + ''], + old_tokens = tokens; - // Why is emote parsing so bad? ;_; - _.each(emotes, function(emote) { - var tooltip = f._emote_tooltip(emote), - eo = ['' + tooltip + ''], - old_tokens = tokens; + 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); + while(tbits.length) { + var bit = tbits.shift(); + if ( tbits.length ) { + bit += tbits.shift(); + if ( bit ) + tokens.push(bit); + + tbits.shift(); + tokens.push(eo); + + if ( mine && l_room ) + f.add_usage(l_room, emote.id); + + } else + tokens.push(bit); + } + } + }); + } + + // Sneak in Emojicon Processing + if ( f.settings.parse_emoji && f.emoji_data ) { + var 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" ) { + if ( typeof token !== "string" ) { tokens.push(token); continue; } - var tbits = token.split(emote.regex); + var tbits = token.split(constants.EMOJI_REGEX); while(tbits.length) { var bit = tbits.shift(); + bit && tokens.push(bit); + if ( tbits.length ) { - bit += tbits.shift(); - if ( bit ) - tokens.push(bit); + var match = tbits.shift(), + variant = tbits.shift(); - tbits.shift(); - tokens.push(eo); + if ( variant === '\uFE0E' ) + bits.push(match); + else { + var eid = utils.emoji_to_codepoint(match, variant), + data = f.emoji_data[eid], + alt = match + (variant || ""); - if ( mine && l_room ) - f.add_usage(l_room, emote.id); - - } else - tokens.push(bit); + if ( data ) { + tokens.push(['' + alt + '']); + } else + tokens.push(match + (variant || "")); + } + } } } - }); + } return tokens; } diff --git a/src/main.js b/src/main.js index da3065d5..b6117b1e 100644 --- a/src/main.js +++ b/src/main.js @@ -21,7 +21,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 4, revision: 2, + major: 3, minor: 4, revision: 10, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -102,8 +102,6 @@ FFZ.prototype.get_user = function() { // Import Everything! // ------------------- -//require('./templates'); - // Import these first to set up data structures require('./ui/menu'); require('./settings'); @@ -140,6 +138,7 @@ require('./ui/viewer_count'); require('./ui/sub_count'); require('./ui/menu_button'); +require('./ui/following'); require('./ui/races'); require('./ui/my_emotes'); require('./ui/about_page'); @@ -190,6 +189,10 @@ FFZ.prototype.setup_normal = function(delay) { this.log("Found non-Ember Twitch after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; + this.is_dashboard = false; + try { + this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname); + } catch(err) { this.embed_in_dash = false; } // Initialize all the modules. this.load_settings(); @@ -222,6 +225,7 @@ FFZ.prototype.setup_dashboard = function(delay) { this.users = {}; this.is_dashboard = true; + this.embed_in_dash = false; // Initialize all the modules. this.load_settings(); @@ -255,6 +259,10 @@ FFZ.prototype.setup_ember = function(delay) { this.log("Found Twitch application after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; + this.is_dashboard = false; + try { + this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname); + } catch(err) { this.embed_in_dash = false; } // Initialize all the modules. this.load_settings(); @@ -282,6 +290,7 @@ FFZ.prototype.setup_ember = function(delay) { this.setup_css(); this.setup_menu(); this.setup_my_emotes(); + this.setup_following(); this.setup_races(); this.connect_extra_chat(); @@ -313,5 +322,4 @@ FFZ.prototype._on_window_message = function(e) { return; var msg = e.data; - this.log("Window Message", msg); } \ No newline at end of file diff --git a/src/socket.js b/src/socket.js index abefe8b1..9f1c95a8 100644 --- a/src/socket.js +++ b/src/socket.js @@ -51,8 +51,10 @@ FFZ.prototype.ws_create = function() { // Join the right channel if we're in the dashboard. if ( f.is_dashboard ) { var match = location.pathname.match(/\/([^\/]+)/); - if ( match ) + if ( match ) { f.ws_send("sub", match[1]); + f.ws_send("sub_channel", match[1]); + } } // Send the current rooms. @@ -69,6 +71,18 @@ FFZ.prototype.ws_create = function() { } } + // Send the channel(s). + if ( f._cindex ) { + var channel_id = f._cindex.get('controller.id'), + hosted_id = f._cindex.get('controller.hostModeTarget.id'); + + if ( channel_id ) + f.ws_send("sub_channel", channel_id); + + if ( hosted_id ) + f.ws_send("sub_channel", hosted_id); + } + // Send any pending commands. var pending = f._ws_pending; f._ws_pending = []; diff --git a/src/tokenize.js b/src/tokenize.js index 15567884..6cff25d9 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -1,5 +1,6 @@ var FFZ = window.FrankerFaceZ, utils = require("./utils"), + constants = require("./constants"), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", helpers, @@ -10,11 +11,28 @@ var FFZ = window.FrankerFaceZ, SEPARATORS = "[\\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]", SPLITTER = new RegExp(SEPARATORS + "*," + SEPARATORS + "*"); + try { helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); } catch(err) { } +FFZ.SRC_IDS = {}, +FFZ.src_to_id = function(src) { + if ( FFZ.SRC_IDS.hasOwnProperty(src) ) + return FFZ.SRC_IDS[src]; + + var match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(src), + id = match ? parseInt(match[1]) : null; + + if ( id === NaN ) + id = null; + + FFZ.SRC_IDS[src] = id; + return id; +}; + + // --------------------- // Tokenization // --------------------- @@ -41,6 +59,9 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { tokens = this._remove_banned(tokens); tokens = this.tokenize_emotes(msgObject.from, room_id, tokens, from_me); + if ( this.settings.parse_emoji ) + tokens = this.tokenize_emoji(tokens); + // Capitalization var display = msgObject.tags && msgObject.tags['display-name']; if ( display && display.length ) @@ -53,7 +74,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { for(var i=0; i < tokens.length; i++) { var token = tokens[i]; - if ( _.isString(token) || ! token.mentionedUser || token.own || msgObject.style === 'whisper' ) + if ( msgObject.style !== 'whisper' && (_.isString(token) || ! token.mentionedUser || token.own) ) continue; // We have a mention! @@ -69,7 +90,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { // 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() && ! prevent_notification ) { + if ( this._chatv && this.settings.highlight_notifications && ! this.embed_in_dash && ! document.hasFocus() && ! prevent_notification ) { var room = this.rooms[room_id] && this.rooms[room_id].room, room_name; @@ -86,17 +107,28 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification) { msg = display + ': ' + msg; var f = this; - this.show_notification( - msg, - "Twitch Chat Mention in " + room_name, - room_id, - 60000, - function() { - window.focus(); - var cont = App.__container__.lookup('controller:chat'); - room && cont && cont.focusRoom(room); - } - ); + if ( msgObject.style === 'whisper' ) + this.show_notification( + msg, + "Twitch Chat Whisper", + "ffz_whisper_notice", + 60000, + function() { + window.focus(); + } + ); + else + this.show_notification( + msg, + "Twitch Chat Mention in " + room_name, + room_id, + 60000, + function() { + window.focus(); + var cont = App.__container__.lookup('controller:chat'); + room && cont && cont.focusRoom(room); + } + ); } break; @@ -129,9 +161,31 @@ FFZ.prototype.tokenize_line = function(user, room, message, no_emotes) { FFZ.prototype.render_tokens = function(tokens, render_links) { + var f = this; return _.map(tokens, function(token) { - if ( token.emoticonSrc ) - return '' + token.altText + ''; + if ( token.emoticonSrc ) { + var tooltip; + if ( token.ffzEmote ) { + var emote_set = f.emote_sets && f.emote_sets[token.ffzEmoteSet], + emote = emote_set && emote_set.emoticons && emote_set.emoticons[token.ffzEmote]; + + tooltip = emote ? utils.sanitize(f._emote_tooltip(emote)) : token.altText; + + } else if ( token.ffzEmoji ) { + var eid = token.ffzEmoji, + emoji = f.emoji_data && f.emoji_data[eid]; + + tooltip = emoji ? "Emoji: " + token.altText + "\nName: " + emoji.short_name : token.altText; + + } else { + var id = FFZ.src_to_id(token.emoticonSrc), + data = id && f._twitch_emotes && f._twitch_emotes[id]; + + tooltip = data && data.tooltip ? data.tooltip : token.altText; + } + + return ''; + } if ( token.isLink ) { if ( ! render_links && render_links !== undefined ) @@ -142,7 +196,11 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { return '' + s + ''; var n = (s.match(/^https?:\/\//) ? "" : "http://") + s; - return '' + s + ''; + + // Check for link data. + var data = f._link_data && f._link_data[n] || {}; + + return '' + s + ''; } if ( token.mentionedUser ) @@ -257,7 +315,7 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { return tokens; // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens == "string" ) + if ( typeof tokens === "string" ) tokens = [tokens]; // This is weird stuff I basically copied from the old Twitch code. @@ -269,6 +327,8 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { var eo = { srcSet: emote.srcSet, emoticonSrc: emote.urls[1] + '" data-ffz-emote="' + encodeURIComponent(JSON.stringify([emote.id, emote.set_id])), + ffzEmote: emote.id, + ffzEmoteSet: emote.set_id, altText: (emote.hidden ? "???" : emote.name) }; @@ -301,6 +361,58 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { } +// --------------------- +// Emoji Processing +// --------------------- + +FFZ.prototype.tokenize_emoji = function(tokens) { + if ( typeof tokens === "string" ) + tokens = [tokens]; + + if ( ! this.emoji_data ) + return tokens; + + var f = this; + + return _.compact(_.flatten(_.map(tokens, function(token) { + if ( _.isObject(token) ) + return token; + + var tbits = token.split(constants.EMOJI_REGEX), bits = []; + while(tbits.length) { + // Deal with the unmatched string first. + var bit = tbits.shift(); + bit && bits.push(bit); + + if ( tbits.length ) { + // We have an emoji too, so let's handle that. + var match = tbits.shift(), + variant = tbits.shift(); + + if ( variant === '\uFE0E' ) { + // Text Variant + bits.push(match); + + } else { + // Find the right image~! + var eid = utils.emoji_to_codepoint(match, variant), + data = f.emoji_data[eid], + alt = match + (variant || ""); + + if ( data ) { + data.token.altText = alt; + bits.push(data.token); + } else + bits.push(alt); + } + } + } + + return bits; + }))); +} + + // --------------------- // Mention Parsing // --------------------- @@ -367,4 +479,47 @@ FFZ.prototype.tokenize_mentions = function(tokens) { } return new_tokens; +} + + +// --------------------- +// Handling Bad Stuff +// --------------------- + +FFZ.prototype._deleted_link_click = function(e) { + if ( ! this.classList.contains("deleted-link") ) + return true; + + // Get the URL + var href = this.getAttribute('data-url'), + link = href, + f = FrankerFaceZ.get(); + + // Delete Old Stuff + this.classList.remove('deleted-link'); + this.removeAttribute("data-url"); + this.removeAttribute("title"); + this.removeAttribute("original-title"); + + // Process URL + if ( href.indexOf("@") > -1 && (-1 === href.indexOf("/") || href.indexOf("@") < href.indexOf("/")) ) + href = "mailto:" + href; + else if ( ! href.match(/^https?:\/\//) ) + href = "http://" + href; + + // Set up the Link + this.href = href; + this.target = "_new"; + this.textContent = link; + + // Now, check for a tooltip. + var link_data = f._link_data[link]; + if ( link_data && typeof link_data != "boolean" ) { + this.title = link_data.tooltip; + if ( link_data.unsafe ) + this.classList.add('unsafe-link'); + } + + // Stop from Navigating + e.preventDefault(); } \ No newline at end of file diff --git a/src/ui/dark.js b/src/ui/dark.js index 0a967e47..72542ec4 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -68,6 +68,6 @@ FFZ.prototype._load_dark_css = function() { s.id = "ffz-dark-css"; s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.SERVER + "script/dark.css?_=" + Date.now()); + s.setAttribute('href', constants.SERVER + "script/dark.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); } \ No newline at end of file diff --git a/src/ui/following.js b/src/ui/following.js new file mode 100644 index 00000000..48208cb2 --- /dev/null +++ b/src/ui/following.js @@ -0,0 +1,404 @@ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + + EMOTE_CHANNELS = { + sirstendec: true, + europeanspeedsterassembly: true, + esamarathon2: true + }; + + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.setup_following = function() { + this.log("Initializing following support."); + this.follow_data = {}; + this.follow_sets = {}; +} + + +// --------------- +// Settings +// --------------- + +FFZ.settings_info.follow_buttons = { + type: "boolean", + value: true, + + category: "Channel Metadata", + name: "Relevant Follow Buttons", + help: 'Display additional Follow buttons for channels relevant to the stream, such as people participating in co-operative gameplay.', + on_update: function(val) { + this.rebuild_following_ui(); + } + }; + + +// --------------- +// Command +// --------------- + +FFZ.ffz_commands.following = function(room, args) { + args = args.join(" ").trim().split(/\s*,+\s*/); + + if ( args.length && args[0] === '' ) + args.shift(); + + if ( args.length && args[args.length-1] === '' ) + args.pop(); + + var user = this.get_user(), f = this; + if ( ! user || (user.login !== room.id && user.login !== "sirstendec" && user.login !== "dansalvato") ) + return "You must be logged in as the broadcaster to use this command."; + + if ( ! this.ws_send("update_follow_buttons", [room.id, args], function(success, data) { + if ( ! success ) { + f.room_message(room, "There was an error updating the following buttons."); + return; + } + + if ( data ) + f.room_message(room, "The following buttons have been updated."); + else + f.room_message(room, "The following buttons have been disabled."); + }) ) + return "There was an error communicating with the server."; +} + + +// --------------- +// Socket Handler +// --------------- + +FFZ.ws_on_close.push(function() { + var controller = window.App && App.__container__.lookup('controller:channel'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), + need_update = false; + + this.follow_sets = {}; + + if ( ! controller ) + return; + + for(var channel_id in this.follow_data) { + delete this.follow_data[channel_id]; + if ( channel_id === current_id || channel_id === current_host ) + need_update = true; + + if ( this.rooms && this.rooms[channel_id] && this.rooms[channel_id].extra_sets ) { + var sets = this.rooms[channel_id].extra_sets; + delete this.rooms[channel_id].extra_sets; + + for(var i=0; i < sets.length; i++) { + var set = this.emote_sets[sets[i]]; + if ( set ) { + set.users.removeObject(channel_id); + if ( ! this.global_sets.contains(sets[i]) && ! set.users.length ) + this.unload_set(sets[i]); + } + } + } + } + + if ( need_update ) + this.rebuild_following_ui(); +}); + + +FFZ.ws_commands.follow_buttons = function(data) { + var controller = window.App && App.__container__.lookup('controller:channel'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), + need_update = false; + + this.follow_data = this.follow_data || {}; + + for(var channel_id in data) { + this.follow_data[channel_id] = data[channel_id]; + if ( channel_id === current_id || channel_id === current_host ) + need_update = true; + } + + if ( need_update ) + this.rebuild_following_ui(); +} + + +FFZ.ws_commands.follow_sets = function(data) { + var controller = App.__container__.lookup('controller:channel'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), + need_update = false; + + this.follow_sets = this.follow_sets || {}; + + for(var room_id in data) { + if ( ! this.rooms || ! this.rooms.hasOwnProperty(room_id) ) { + this.follow_sets[room_id] = data[room_id]; + continue; + } + + var old_sets = this.rooms[room_id].extra_sets || [], + new_sets = this.rooms[room_id].extra_sets = data[room_id]; + + // Unload sets we aren't using anymore. + for(var i=0; i < old_sets.length; i++) { + var sid = old_sets[i]; + if ( new_sets.indexOf(sid) !== -1 ) + continue; + + var set = this.emote_sets && this.emote_sets[sid]; + if ( set ) { + set.users.removeObject(room_id); + if ( ! this.global_sets.contains(sid) && ! set.users.length ) + this.unload_set(sid); + } + } + + // And load the new sets. + for(var i=0; i < new_sets.length; i++) { + var sid = new_sets[i], + set = this.emote_sets && this.emote_sets[sid]; + + if ( set ) { + if ( set.users.indexOf(room_id) === -1 ) + set.users.push(room_id); + continue; + } + + this.load_set(sid, function(success, data) { + if ( success ) + data.users.push(room_id); + }); + } + } +} + + +// --------------- +// Following UI +// --------------- + +FFZ.prototype.rebuild_following_ui = function() { + var controller = App.__container__.lookup('controller:channel'), + channel_id = controller && controller.get('id'), + hosted_id = controller && controller.get('hostModeTarget.id'); + + if ( ! this._cindex ) + return; + + if ( channel_id ) { + var data = this.follow_data && this.follow_data[channel_id], + + el = this._cindex.get('element'), + container = el && el.querySelector('.stats-and-actions .channel-actions'), + cont = container && container.querySelector('#ffz-ui-following'); + + if ( ! container || ! this.settings.follow_buttons || ! data || ! data.length ) { + if ( cont ) + cont.parentElement.removeChild(cont); + + } else { + if ( ! cont ) { + cont = document.createElement('span'); + cont.id = 'ffz-ui-following'; + + var before = container.querySelector(':scope > span'); + if ( before ) + container.insertBefore(cont, before); + else + container.appendChild(cont); + } else + cont.innerHTML = ''; + + for(var i=0; i < data.length; i++) { + this._build_following_button(cont, data[i]); + } + } + } + + + if ( hosted_id ) { + var data = this.follow_data && this.follow_data[hosted_id], + + el = this._cindex.get('element'), + container = el && el.querySelector('#hostmode .channel-actions'), + cont = container && container.querySelector('#ffz-ui-following'); + + if ( ! container || ! this.settings.follow_buttons || ! data || ! data.length ) { + if ( cont ) + cont.parentElement.removeChild(cont); + + } else { + if ( ! cont ) { + cont = document.createElement('span'); + cont.id = 'ffz-ui-following'; + + var before = container.querySelector(':scope > span'); + if ( before ) + container.insertBefore(cont, before); + else + container.appendChild(cont); + } else + cont.innerHTML = ''; + + for(var i=0; i < data.length; i++) { + this._build_following_button(cont, data[i]); + } + } + } +} + + +// --------------- +// UI Construction +// --------------- + +FFZ.prototype._build_following_button = function(container, channel_id) { + var btn = document.createElement('a'), f = this, + btn_c = document.createElement('div'), + noti = document.createElement('a'), + noti_c = document.createElement('div'), + + display_name, + following = false, + notifications = false, + + update = function() { + btn_c.classList.toggle('is-following', following); + btn.title = (following ? "Unf" : "F") + "ollow " + display_name; + btn.innerHTML = (following ? "" : "Follow ") + display_name; + noti_c.classList.toggle('hidden', !following); + }, + + check_following = function() { + var user = f.get_user(); + if ( ! user || ! user.login ) { + following = false; + notification = false; + btn_c.classList.add('is-initialized'); + return update(); + } + + Twitch.api.get("users/" + user.login + "/follows/channels/" + channel_id) + .done(function(data) { + following = true; + notifications = data.notifications; + btn_c.classList.add('is-initialized'); + update(); + }).fail(function(data) { + following = false; + notifications = false; + btn_c.classList.add('is-initialized'); + update(); + }); + }, + + do_follow = function(notice) { + if ( notice !== false ) + notice = true; + + var user = f.get_user(); + if ( ! user || ! user.login ) + return null; + + notifications = notice; + return Twitch.api.put("users/:login/follows/channels/" + channel_id, {notifications: notifications}) + .fail(check_following); + }, + + on_name = function(cap_name) { + display_name = cap_name || channel_id; + update(); + }; + + btn_c.className = 'ember-follow follow-button'; + btn_c.appendChild(btn); + + // The drop-down button! + noti.className = 'toggle-notification-menu js-toggle-notification-menu'; + noti.href = '#'; + + noti_c.className = 'notification-controls v2 hidden'; + noti_c.appendChild(noti); + + // Event Listeners! + btn.addEventListener('click', function() { + var user = f.get_user(); + if ( ! user || ! user.login ) + // Show the login dialog~! + return Ember.$.login({mpSourceAction: "follow-button", follow: channel_id}); + + // Immediate update for nice UI. + following = ! following; + update(); + + // Report it! + f.ws_send("track_follow", [channel_id, following]); + + // Do it, and make sure it happened. + if ( following ) + do_follow() + else + Twitch.api.del("users/:login/follows/channels/" + channel_id) + .done(check_following); + + return false; + }); + + noti.addEventListener('click', function() { + var sw = f._build_following_popup(noti_c, channel_id, notifications); + if ( sw ) + sw.addEventListener('click', function() { + var notice = ! notifications; + sw.classList.toggle('active', notice); + do_follow(notice); + return false; + }); + return false; + }); + + + display_name = FFZ.get_capitalization(channel_id, on_name); + update(); + check_following(); + + container.appendChild(btn_c); + container.appendChild(noti_c); +} + + +FFZ.prototype._build_following_popup = function(container, channel_id, notifications) { + var popup = this._popup, out = '', + pos = container.offsetLeft + container.offsetWidth; + + + if ( popup ) { + popup.parentElement.removeChild(popup); + delete this._popup; + this._popup_kill && this._popup_kill(); + delete this._popup_kill; + + if ( popup.id == "ffz-following-popup" && popup.getAttribute('data-channel') === channel_id ) + return null; + } + + popup = this._popup = document.createElement('div'); + popup.id = 'ffz-following-popup'; + popup.setAttribute('data-channel', channel_id); + + popup.className = (pos >= 300 ? 'right' : 'left') + ' dropmenu notify-menu js-notify'; + + out = '
You are following ' + FFZ.get_capitalization(channel_id) + '
'; + out += '

'; + out += ''; + out += 'Notify me when the broadcaster goes live'; + out += '

'; + + popup.innerHTML = out; + container.appendChild(popup); + return popup.querySelector('a.switch'); +} \ No newline at end of file diff --git a/src/ui/menu.js b/src/ui/menu.js index ea52182e..598f356c 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -28,6 +28,135 @@ FFZ.prototype.setup_menu = function() { }); document.body.classList.toggle("ffz-menu-replace", this.settings.replace_twitch_menu); + + // Add FFZ to the chat settings menu. + + this.log("Hooking the Ember Chat Settings view."); + + var Settings = App.__container__.resolve('view:settings'); + + if ( ! Settings ) + return; + + Settings.reopen({ + didInsertElement: function() { + this._super(); + + try { + this.ffzInit(); + } catch(err) { + f.error("ChatSettings didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("ChatSettings willClearRender: " + err); + } + this._super(); + }, + + ffzInit: function() { + var view = this, + el = this.get('element'), + menu = el && el.querySelector('.dropmenu'); + + if ( ! menu ) + return; + + var header = document.createElement('div'), + content = document.createElement('div'), + p, cb, a; + + header.className = 'list-header'; + header.innerHTML = 'FrankerFaceZ'; + + content.className = 'chat-menu-content'; + + // Dark Twitch + p = document.createElement('p'); + p.className = 'no-bttv'; + cb = document.createElement('input'); + cb.type = "checkbox"; + cb.className = "ember-checkbox ffz-setting-dark-twitch"; + cb.checked = f.settings.dark_twitch; + p.appendChild(cb); + p.appendChild(document.createTextNode("Dark Twitch")); + content.appendChild(p); + + cb.addEventListener("change", function(e) { + f.settings.set("dark_twitch", this.checked); + }); + + + // Minimal Chat + /* + p = document.createElement('p'); + //p.className = 'no-bttv'; + cb = document.createElement('input'); + cb.type = "checkbox"; + cb.className = "ember-checkbox ffz-setting-minimal-twitch"; + cb.checked = f.settings.minimal_chat; + p.appendChild(cb); + p.appendChild(document.createTextNode("Minimalistic Chat")); + content.appendChild(p); + + cb.addEventListener("change", function(e) { + f.settings.set("minimal_chat", this.checked); + if ( this.checked ) + view.set('controller.model.hidden', true); + });*/ + + + // More Settings + p = document.createElement('p'); + a = document.createElement('a'); + a.href = '#'; + a.innerHTML = 'More Settings'; + p.appendChild(a); + content.appendChild(p); + + a.addEventListener('click', function(e) { + view.set('controller.model.hidden', true); + f._last_page = 'settings'; + f.build_ui_popup(f._chatv); + e.stopPropagation(); + return false; + }); + + menu.appendChild(header); + menu.appendChild(content); + }, + + ffzTeardown: function() { + // Nothing~! + } + }); + + // For some reason, this doesn't work unless we create an instance of the + // chat settings view and then destroy it immediately. + try { + Settings.create().destroy(); + } catch(err) { } + + // Modify all existing Chat Settings views. + for(var key in Ember.View.views) { + if ( ! Ember.View.views.hasOwnProperty(key) ) + continue; + + var view = Ember.View.views[key]; + if ( !(view instanceof Settings) ) + continue; + + this.log("Manually updating existing Chat Settings view.", view); + try { + view.ffzInit(); + } catch(err) { + this.error("setup: ChatSettings ffzInit: " + err); + } + } } @@ -176,6 +305,7 @@ FFZ.menu_pages.channel = { has_product = true; var tickets = App.__container__.resolve('model:ticket').find('user', {channel: room_id}), is_subscribed = tickets ? tickets.get('content') : false, + is_loaded = tickets ? tickets.get('isLoaded') : false, icon = room.room.get("badgeSet.subscriber.image"), grid = document.createElement("div"), @@ -185,6 +315,25 @@ FFZ.menu_pages.channel = { // Weird is_subscribed check. Might be more accurate? is_subscribed = is_subscribed && is_subscribed.length > 0; + // See if we've loaded. If we haven't loaded the ticket yet + // then try loading it, and then re-render the menu. + if ( tickets && ! is_subscribed && ! is_loaded ) { + var f = this; + tickets.addObserver('isLoaded', function() { + setTimeout(function(){ + if ( inner.getAttribute('data-page') !== 'channel' ) + return; + + inner.innerHTML = ''; + FFZ.menu_pages.channel.render.bind(f)(view, inner); + },0); + + }); + + tickets.load(); + } + + grid.className = "emoticon-grid"; header.className = "heading"; if ( icon ) @@ -193,7 +342,7 @@ FFZ.menu_pages.channel = { header.innerHTML = 'TwitchSubscriber Emoticons'; grid.appendChild(header); - for(var emotes=product.get("emoticons"), i=0; i < emotes.length; i++) { + for(var emotes=product.get("emoticons") || [], i=0; i < emotes.length; i++) { var emote = emotes[i]; if ( emote.state !== "active" ) continue; @@ -222,7 +371,7 @@ FFZ.menu_pages.channel = { if ( c > 0 ) inner.appendChild(grid); - if ( ! is_subscribed ) { + if ( c > 0 && ! is_subscribed ) { var sub_message = document.createElement("div"), nonsub_message = document.createElement("div"), unlock_text = document.createElement("span"), @@ -242,7 +391,7 @@ FFZ.menu_pages.channel = { nonsub_message.appendChild(sub_link); inner.appendChild(sub_message); - } else { + } else if ( c > 0 ) { var last_content = tickets.get("content"); last_content = last_content.length > 0 ? last_content[last_content.length-1] : undefined; if ( last_content && last_content.purchase_profile && !last_content.purchase_profile.will_renew ) { @@ -265,8 +414,19 @@ FFZ.menu_pages.channel = { } } + // Do we have extra sets? + var extra_sets = room && room.extra_sets || []; + // Basic Emote Sets - this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); + this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product || extra_sets.length ) ? "Channel Emoticons" : null, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); + + for(var i=0; i < extra_sets.length; i++) { + // Look up the set name. + var set = this.emote_sets[extra_sets[i]], + name = set ? "Featured " + set.title : "Featured Channel"; + + this._emotes_for_sets(inner, view, [extra_sets[i]], name, "http://cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); + } // Feature Friday! this._feature_friday_ui(room_id, inner, view); diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js index 9a0968b0..2e70abc5 100644 --- a/src/ui/my_emotes.js +++ b/src/ui/my_emotes.js @@ -55,11 +55,11 @@ FFZ.prototype.setup_my_emotes = function() { } this._twitch_set_to_channel[0] = "global"; - this._twitch_set_to_channel[33] = "tfaces"; - this._twitch_set_to_channel[42] = "tfaces"; + this._twitch_set_to_channel[33] = "turbo_faces"; + this._twitch_set_to_channel[42] = "turbo_faces"; 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"; + this._twitch_badges["turbo_faces"] = this._twitch_badges["turbo"] = "//cdn.frankerfacez.com/script/turbo_badge.png"; } @@ -106,7 +106,7 @@ FFZ.menu_pages.my_emotes = { return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, ts); }; - this.ws_send("twitch_sets", needed_sets, function(success, data) { + if ( ! this.ws_send("twitch_sets", needed_sets, function(success, data) { if ( ! needed_sets.length ) return; @@ -123,9 +123,10 @@ FFZ.menu_pages.my_emotes = { return FFZ.menu_pages.my_emotes.draw_menu.bind(f)(view, container, twitch_sets); } else fail(); - }); - - setTimeout(fail, 2000); + }) ) + fail() + else + setTimeout(fail, 2000); }, draw_twitch_set: function(view, set_id, set) { @@ -136,7 +137,7 @@ FFZ.menu_pages.my_emotes = { if ( channel_id === "global" ) title = "Global Emoticons"; - else if ( channel_id === "turbo" ) + else if ( channel_id === "turbo" || channel_id === "turbo_faces" ) title = "Twitch Turbo"; else title = FFZ.get_capitalization(channel_id, function(name) { @@ -285,12 +286,12 @@ FFZ.menu_pages.my_emotes = { // Finally, sort and add them all. sets.sort(function(a,b) { var an = a[0], bn = b[0]; - if ( an === "turbo" || an === "tfaces" ) + if ( an === "turbo" || an === "turbo_faces" ) an = "zza|" + an; else if ( an === "global" || an === "global emoticons" ) an = "zzz|" + an; - if ( bn === "turbo" || bn === "tfaces" ) + if ( bn === "turbo" || bn === "turbo_faces" ) bn = "zza|" + bn; else if ( bn === "global" || bn === "global emoticons" ) bn = "zzz|" + bn; diff --git a/src/ui/notifications.js b/src/ui/notifications.js index bd61547a..d5e7774b 100644 --- a/src/ui/notifications.js +++ b/src/ui/notifications.js @@ -24,7 +24,7 @@ FFZ.settings_info.highlight_notifications = { //visible: function() { return ! this.has_bttv }, name: "Highlight Notifications", - help: "Display notifications when a highlighted word appears in chat in an unfocused tab.", + help: "Display notifications when a highlighted word appears in chat in an unfocused tab. This is automatically disabled on the dashboard.", on_update: function(val, direct) { // Check to see if we have notification permission. If this is diff --git a/src/ui/races.js b/src/ui/races.js index 4081e2b5..89800918 100644 --- a/src/ui/races.js +++ b/src/ui/races.js @@ -36,6 +36,7 @@ FFZ.settings_info.srl_races = { FFZ.ws_on_close.push(function() { var controller = window.App && App.__container__.lookup('controller:channel'), current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), need_update = false; if ( ! controller ) @@ -43,7 +44,7 @@ FFZ.ws_on_close.push(function() { for(var chan in this.srl_races) { delete this.srl_races[chan]; - if ( chan == current_id ) + if ( chan === current_id || chan === current_host ) need_update = true; } @@ -51,18 +52,22 @@ FFZ.ws_on_close.push(function() { this.rebuild_race_ui(); }); + FFZ.ws_commands.srl_race = function(data) { var controller = App.__container__.lookup('controller:channel'), - current_id = controller.get('id'), + current_id = controller && controller.get('id'), + current_host = controller && controller.get('hostModeTarget.id'), need_update = false; + this.srl_races = this.srl_races || {}; + for(var i=0; i < data[0].length; i++) { var channel_id = data[0][i]; this.srl_races[channel_id] = data[1]; - if ( channel_id == current_id ) + if ( channel_id === current_id || channel_id === current_host ) need_update = true; } - + if ( data[1] ) { var race = data[1], tte = race.twitch_entrants = {}; @@ -86,41 +91,75 @@ FFZ.ws_commands.srl_race = function(data) { FFZ.prototype.rebuild_race_ui = function() { var controller = App.__container__.lookup('controller:channel'), - channel_id = controller.get('id'), - race = this.srl_races[channel_id], - enable_ui = this.settings.srl_races, + channel_id = controller && controller.get('id'), + hosted_id = controller && controller.get('hostModeTarget.id'); - actions = document.querySelector('.stats-and-actions .channel-actions'), - race_container = actions.querySelector('#ffz-ui-race'); - - if ( ! race || ! enable_ui ) { - if ( race_container ) - race_container.parentElement.removeChild(race_container); - if ( this._popup && this._popup.id == "ffz-race-popup" ) { - delete this._popup; - this._popup_kill && this._popup_kill(); - delete this._popup_kill; - } + if ( ! this._cindex ) return; + + if ( channel_id ) { + var race = this.srl_races && this.srl_races[channel_id], + + el = this._cindex.get('element'), + container = el && el.querySelector('.stats-and-actions .channel-actions'), + race_container = container && container.querySelector('#ffz-ui-race'); + + if ( ! container || ! this.settings.srl_races || ! race ) { + if ( race_container ) + race_container.parentElement.removeChild(race_container); + + } else { + if ( ! race_container ) { + race_container = document.createElement('span'); + race_container.id = 'ffz-ui-race'; + race_container.setAttribute('data-channel', channel_id); + + var btn = document.createElement('span'); + btn.className = 'button drop action'; + btn.title = "SpeedRunsLive Race"; + btn.innerHTML = ''; + + btn.addEventListener('click', this._build_race_popup.bind(this, race_container, channel_id)); + + race_container.appendChild(btn); + container.appendChild(race_container); + } + + this._update_race(race_container, true); + } } - if ( race_container ) - return this._update_race(true); + if ( hosted_id ) { + var race = this.srl_races && this.srl_races[hosted_id], - race_container = document.createElement('span'); - race_container.setAttribute('data-channel', channel_id); - race_container.id = 'ffz-ui-race'; + el = this._cindex.get('element'), + container = el && el.querySelector('#hostmode .channel-actions'), + race_container = container && container.querySelector('#ffz-ui-race'); - var btn = document.createElement('span'); - btn.className = 'button drop action'; - btn.title = "SpeedRunsLive Race"; - btn.innerHTML = '