(function(window) {(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1 ) badges[slot].title = title_template.replace('{}', line_data[1]); count += 1; } } this.log('Added "' + title + '" badge to ' + utils.number_commas(count) + " users."); } },{"./constants":5,"./utils":35}],3:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, hue2rgb = function(p, q, t) { if ( t < 0 ) t += 1; if ( t > 1 ) t -= 1; if ( t < 1/6 ) return p + (q-p) * 6 * t; if ( t < 1/2 ) return q; if ( t < 2/3 ) return p + (q-p) * (2/3 - t) * 6; return p; }; // --------------------- // Settings // --------------------- FFZ.settings_info.fix_color = { type: "select", options: { '-1': "Disabled", 0: "Default Colors", 1: "Luv Adjustment", 2: "HSL Adjustment (Depreciated)", 3: "HSV Adjustment (Depreciated)", 4: "RGB Adjustment (Depreciated)" }, value: '1', category: "Chat Appearance", no_bttv: true, name: "Username Colors - Brightness", help: "Ensure that username colors contrast with the background enough to be readable.", process_value: function(val) { // Load legacy setting. if ( val === false ) return '0'; else if ( val === true ) return '1'; return val; }, on_update: function(val) { document.body.classList.toggle("ffz-chat-colors-gray", !this.has_bttv && (val === '-1')); if ( ! this.has_bttv && val !== '-1' ) this._rebuild_colors(); } }; FFZ.settings_info.luv_contrast = { type: "button", value: 4.5, category: "Chat Appearance", no_bttv: true, name: "Username Colors - Luv Minimum Contrast", help: "Set the minimum contrast ratio used by Luv Adjustment to ensure colors are readable.", method: function() { var old_val = this.settings.luv_contrast, new_val = prompt("Luv Adjustment Minimum Contrast Ratio\n\nPlease enter a new value for the minimum contrast ratio required between username colors and the background. The default is: 4.5", old_val); if ( new_val === null || new_val === undefined ) return; var parsed = parseFloat(new_val); if ( parsed === NaN || parsed < 1 ) parsed = 4.5; this.settings.set("luv_contrast", parsed); }, on_update: function(val) { this._rebuild_contrast(); if ( ! this.has_bttv && this.settings.fix_color == '1' ) this._rebuild_colors(); } }; FFZ.settings_info.color_blind = { type: "select", options: { 0: "Disabled", protanope: "Protanope", deuteranope: "Deuteranope", tritanope: "Tritanope" }, value: '0', category: "Chat Appearance", no_bttv: true, name: "Username Colors - Color Blindness", help: "Adjust username colors in an attempt to make them more distinct for people with color blindness.", on_update: function(val) { if ( ! this.has_bttv && this.settings.fix_color !== '-1' ) this._rebuild_colors(); } }; // -------------------- // Initialization // -------------------- FFZ.prototype.setup_colors = function() { this._colors = {}; this._rebuild_contrast(); this._update_colors(); // Events for rebuilding colors. var Layout = window.App && App.__container__.lookup('controller:layout'), Settings = window.App && App.__container__.lookup('controller:settings'); if ( Layout ) Layout.addObserver("isTheatreMode", this._update_colors.bind(this, true)); if ( Settings ) Settings.addObserver("model.darkMode", this._update_colors.bind(this, true)) this._color_old_darkness = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); } // ----------------------- // Color Handling Classes // ----------------------- FFZ.Color = {}; FFZ.Color.CVDMatrix = { protanope: [ // reds are greatly reduced (1% men) 0.0, 2.02344, -2.52581, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0 ], deuteranope: [ // greens are greatly reduced (1% men) 1.0, 0.0, 0.0, 0.494207, 0.0, 1.24827, 0.0, 0.0, 1.0 ], tritanope: [ // blues are greatly reduced (0.003% population) 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, -0.395913, 0.801109, 0.0 ] } var RGBColor = FFZ.Color.RGB = function(r, g, b) { this.r = r||0; this.g = g||0; this.b = b||0; }; var HSVColor = FFZ.Color.HSV = function(h, s, v) { this.h = h||0; this.s = s||0; this.v = v||0; }; var HSLColor = FFZ.Color.HSL = function(h, s, l) { this.h = h||0; this.s = s||0; this.l = l||0; }; var XYZColor = FFZ.Color.XYZ = function(x, y, z) { this.x = x||0; this.y = y||0; this.z = z||0; }; var LUVColor = FFZ.Color.LUV = function(l, u, v) { this.l = l||0; this.u = u||0; this.v = v||0; }; // RGB Colors RGBColor.prototype.eq = function(rgb) { return rgb.r === this.r && rgb.g === this.g && rgb.b === this.b; } RGBColor.fromHex = function(code) { var raw = parseInt(code.charAt(0) === '#' ? code.substr(1) : code, 16); return new RGBColor( (raw >> 16), // Red (raw >> 8 & 0x00FF), // Green (raw & 0x0000FF) // Blue ) } RGBColor.fromHSV = function(h, s, v) { var r, g, b, i = Math.floor(h * 6), f = h * 6 - i, p = v * (1 - s), q = v * (1 - f * s), t = v * (1 - (1 - f) * s); switch(i % 6) { case 0: r = v, g = t, b = p; break; case 1: r = q, g = v, b = p; break; case 2: r = p, g = v, b = t; break; case 3: r = p, g = q, b = v; break; case 4: r = t, g = p, b = v; break; case 5: r = v, g = p, b = q; } return new RGBColor( Math.round(Math.min(Math.max(0, r*255), 255)), Math.round(Math.min(Math.max(0, g*255), 255)), Math.round(Math.min(Math.max(0, b*255), 255)) ); } RGBColor.fromXYZ = function(x, y, z) { var R = 3.240479 * x - 1.537150 * y - 0.498535 * z, G = -0.969256 * x + 1.875992 * y + 0.041556 * z, B = 0.055648 * x - 0.204043 * y + 1.057311 * z; // Make sure we end up in a real color space return new RGBColor( Math.max(0, Math.min(255, 255 * XYZColor.channelConverter(R))), Math.max(0, Math.min(255, 255 * XYZColor.channelConverter(G))), Math.max(0, Math.min(255, 255 * XYZColor.channelConverter(B))) ); } RGBColor.fromHSL = function(h, s, l) { if ( s === 0 ) { var v = Math.round(Math.min(Math.max(0, 255*l), 255)); return new RGBColor(v, v, v); } var q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q; return new RGBColor( Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h + 1/3)), 255)), Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h)), 255)), Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h - 1/3)), 255)) ); } RGBColor.prototype.toHSV = function() { return HSVColor.fromRGB(this.r, this.g, this.b); } RGBColor.prototype.toHSL = function() { return HSLColor.fromRGB(this.r, this.g, this.b); } RGBColor.prototype.toCSS = function() { return "rgb(" + Math.round(this.r) + "," + Math.round(this.g) + "," + Math.round(this.b) + ")"; } RGBColor.prototype.toXYZ = function() { return XYZColor.fromRGB(this.r, this.g, this.b); } RGBColor.prototype.toLUV = function() { return this.toXYZ().toLUV(); } RGBColor.prototype.toHex = function() { var rgb = this.b | (this.g << 8) | (this.r << 16); return '#' + (0x1000000 + rgb).toString(16).slice(1); } RGBColor.prototype.luminance = function() { var rgb = [this.r / 255, this.g / 255, this.b / 255]; for (var i =0, l = rgb.length; i < l; i++) { if (rgb[i] <= 0.03928) { rgb[i] = rgb[i] / 12.92; } else { rgb[i] = Math.pow( ((rgb[i]+0.055)/1.055), 2.4 ); } } return (0.2126 * rgb[0]) + (0.7152 * rgb[1]) + (0.0722 * rgb[2]); } RGBColor.prototype.brighten = function(amount) { amount = typeof amount === "number" ? amount : 1; amount = Math.round(255 * (amount / 100)); return new RGBColor( Math.max(0, Math.min(255, this.r + amount)), Math.max(0, Math.min(255, this.g + amount)), Math.max(0, Math.min(255, this.b + amount)) ); } RGBColor.prototype.daltonize = function(type, amount) { amount = typeof amount === "number" ? amount : 1.0; var cvd; if ( typeof type === "string" ) { if ( FFZ.Color.CVDMatrix.hasOwnProperty(type) ) cvd = FFZ.Color.CVDMatrix[type]; else throw "Invalid CVD matrix."; } else cvd = type; var cvd_a = cvd[0], cvd_b = cvd[1], cvd_c = cvd[2], cvd_d = cvd[3], cvd_e = cvd[4], cvd_f = cvd[5], cvd_g = cvd[6], cvd_h = cvd[7], cvd_i = cvd[8], L, M, S, l, m, s, R, G, B, RR, GG, BB; // RGB to LMS matrix conversion L = (17.8824 * this.r) + (43.5161 * this.g) + (4.11935 * this.b); M = (3.45565 * this.r) + (27.1554 * this.g) + (3.86714 * this.b); S = (0.0299566 * this.r) + (0.184309 * this.g) + (1.46709 * this.b); // Simulate color blindness l = (cvd_a * L) + (cvd_b * M) + (cvd_c * S); m = (cvd_d * L) + (cvd_e * M) + (cvd_f * S); s = (cvd_g * L) + (cvd_h * M) + (cvd_i * S); // LMS to RGB matrix conversion R = (0.0809444479 * l) + (-0.130504409 * m) + (0.116721066 * s); G = (-0.0102485335 * l) + (0.0540193266 * m) + (-0.113614708 * s); B = (-0.000365296938 * l) + (-0.00412161469 * m) + (0.693511405 * s); // Isolate invisible colors to color vision deficiency (calculate error matrix) R = this.r - R; G = this.g - G; B = this.b - B; // Shift colors towards visible spectrum (apply error modifications) RR = (0.0 * R) + (0.0 * G) + (0.0 * B); GG = (0.7 * R) + (1.0 * G) + (0.0 * B); BB = (0.7 * R) + (0.0 * G) + (1.0 * B); // Add compensation to original values R = Math.min(Math.max(0, RR + this.r), 255); G = Math.min(Math.max(0, GG + this.g), 255); B = Math.min(Math.max(0, BB + this.b), 255); return new RGBColor(R, G, B); } RGBColor.prototype._r = function(r) { return new RGBColor(r, this.g, this.b); } RGBColor.prototype._g = function(g) { return new RGBColor(this.r, g, this.b); } RGBColor.prototype._b = function(b) { return new RGBColor(this.r, this.g, b); } // HSL Colors HSLColor.prototype.eq = function(hsl) { return hsl.h === this.h && hsl.s === this.s && hsl.l === this.l; } HSLColor.fromRGB = function(r, g, b) { r /= 255; g /= 255; b /= 255; var max = Math.max(r,g,b), min = Math.min(r,g,b), h, s, l = Math.min(Math.max(0, (max+min) / 2), 1), d = Math.min(Math.max(0, max - min), 1); if ( d === 0 ) h = s = 0; else { s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch(max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; } h /= 6; } return new HSLColor(h, s, l); } HSLColor.prototype.toRGB = function() { return RGBColor.fromHSL(this.h, this.s, this.l); } HSLColor.prototype.toCSS = function() { return "hsl(" + Math.round(this.h*360) + "," + Math.round(this.s*100) + "%," + Math.round(this.l*100) + "%)"; } HSLColor.prototype.toHex = function() { return RGBColor.fromHSL(this.h, this.s, this.l).toHex(); } HSLColor.prototype.toHSV = function() { return RGBColor.fromHSL(this.h, this.s, this.l).toHSV(); } HSLColor.prototype.toXYZ = function() { return RGBColor.fromHSL(this.h, this.s, this.l).toXYZ(); } HSLColor.prototype.toLUV = function() { return RGBColor.fromHSL(this.h, this.s, this.l).toLUV(); } HSLColor.prototype._h = function(h) { return new HSLColor(h, this.s, this.l); } HSLColor.prototype._s = function(s) { return new HSLColor(this.h, s, this.l); } HSLColor.prototype._l = function(l) { return new HSLColor(this.h, this.s, l); } // HSV Colors HSVColor.prototype.eq = function(hsv) { return hsv.h === this.h && hsv.s === this.s && hsv.v === this.v; } HSVColor.fromRGB = function(r, g, b) { r /= 255; g /= 255; b /= 255; var max = Math.max(r, g, b), min = Math.min(r, g, b), d = Math.min(Math.max(0, max - min), 1), h, s = max === 0 ? 0 : d / max, v = max; if ( d === 0 ) h = 0; else { switch(max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; } h /= 6; } return new HSVColor(h, s, v); } HSVColor.prototype.toRGB = function() { return RGBColor.fromHSV(this.h, this.s, this.v); } HSVColor.prototype.toHSL = function() { return RGBColor.fromHSV(this.h, this.s, this.v).toHSL(); } HSVColor.prototype.toXYZ = function() { return RGBColor.fromHSV(this.h, this.s, this.v).toXYZ(); } HSVColor.prototype.toLUV = function() { return RGBColor.fromHSV(this.h, this.s, this.v).toLUV(); } HSVColor.prototype._h = function(h) { return new HSVColor(h, this.s, this.v); } HSVColor.prototype._s = function(s) { return new HSVColor(this.h, s, this.v); } HSVColor.prototype._v = function(v) { return new HSVColor(this.h, this.s, v); } // XYZ Colors RGBColor.channelConverter = function (channel) { // http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html // This converts rgb 8bit to rgb linear, lazy because the other algorithm is really really dumb return Math.pow(channel, 2.2); // CSS Colors Level 4 says 0.03928, Bruce Lindbloom who cared to write all algos says 0.04045, used bruce because whynawt return (channel <= 0.04045) ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4); }; XYZColor.channelConverter = function (channel) { // Using lazy conversion in the other direction as well return Math.pow(channel, 1/2.2); // I'm honestly not sure about 0.0031308, I've only seen it referenced on Bruce Lindbloom's site return (channel <= 0.0031308) ? channel * 12.92 : Math.pow(1.055 * channel, 1/2.4) - 0.055; }; XYZColor.prototype.eq = function(xyz) { return xyz.x === this.x && xyz.y === this.y && xyz.z === this.z; } XYZColor.fromRGB = function(r, g, b) { var R = RGBColor.channelConverter(r / 255), G = RGBColor.channelConverter(g / 255), B = RGBColor.channelConverter(b / 255); return new XYZColor( 0.412453 * R + 0.357580 * G + 0.180423 * B, 0.212671 * R + 0.715160 * G + 0.072169 * B, 0.019334 * R + 0.119193 * G + 0.950227 * B ); } XYZColor.fromLUV = function(l, u, v) { var deltaGammaFactor = 1 / (XYZColor.WHITE.x + 15 * XYZColor.WHITE.y + 3 * XYZColor.WHITE.z); var uDeltaGamma = 4 * XYZColor.WHITE.x * deltaGammaFactor; var vDeltagamma = 9 * XYZColor.WHITE.y * deltaGammaFactor; // XYZColor.EPSILON * XYZColor.KAPPA = 8 var Y = (l > 8) ? Math.pow((l + 16) / 116, 3) : l / XYZColor.KAPPA; var a = 1/3 * (((52 * l) / (u + 13 * l * uDeltaGamma)) - 1); var b = -5 * Y; var c = -1/3; var d = Y * (((39 * l) / (v + 13 * l * vDeltagamma)) - 5); var X = (d - b) / (a - c); var Z = X * a + b; return new XYZColor(X, Y, Z); } XYZColor.prototype.toRGB = function() { return RGBColor.fromXYZ(this.x, this.y, this.z); } XYZColor.prototype.toLUV = function() { return LUVColor.fromXYZ(this.x, this.y, this.z); } XYZColor.prototype.toHSL = function() { return RGBColor.fromXYZ(this.x, this.y, this.z).toHSL(); } XYZColor.prototype.toHSV = function() { return RGBColor.fromXYZ(this.x, this.y, this.z).toHSV(); } XYZColor.prototype._x = function(x) { return new XYZColor(x, this.y, this.z); } XYZColor.prototype._y = function(y) { return new XYZColor(this.x, y, this.z); } XYZColor.prototype._z = function(z) { return new XYZColor(this.x, this.y, z); } // LUV Colors XYZColor.EPSILON = Math.pow(6 / 29, 3); XYZColor.KAPPA = Math.pow(29 / 3, 3); XYZColor.WHITE = (new RGBColor(255, 255, 255)).toXYZ(); LUVColor.prototype.eq = function(luv) { return luv.l === this.l && luv.u === this.u && luv.v === this.v; } LUVColor.fromXYZ = function(X, Y, Z) { var deltaGammaFactor = 1 / (XYZColor.WHITE.x + 15 * XYZColor.WHITE.y + 3 * XYZColor.WHITE.z); var uDeltaGamma = 4 * XYZColor.WHITE.x * deltaGammaFactor; var vDeltagamma = 9 * XYZColor.WHITE.y * deltaGammaFactor; var yGamma = Y / XYZColor.WHITE.y; var deltaDivider = (X + 15 * Y + 3 * Z); if (deltaDivider === 0) { deltaDivider = 1; } var deltaFactor = 1 / deltaDivider; var uDelta = 4 * X * deltaFactor; var vDelta = 9 * Y * deltaFactor; var L = (yGamma > XYZColor.EPSILON) ? 116 * Math.pow(yGamma, 1/3) - 16 : XYZColor.KAPPA * yGamma; var u = 13 * L * (uDelta - uDeltaGamma); var v = 13 * L * (vDelta - vDeltagamma); return new LUVColor(L, u, v); } LUVColor.prototype.toXYZ = function() { return XYZColor.fromLUV(this.l, this.u, this.v); } LUVColor.prototype.toRGB = function() { return XYZColor.fromLUV(this.l, this.u, this.v).toRGB(); } LUVColor.prototype.toHSL = function() { return XYZColor.fromLUV(this.l, this.u, this.v).toHSL(); } LUVColor.prototype.toHSV = function() { return XYZColor.fromLUV(this.l, this.u, this.v).toHSV(); } LUVColor.prototype._l = function(l) { return new LUVColor(l, this.u, this.v); } LUVColor.prototype._u = function(u) { return new LUVColor(this.l, u, this.v); } LUVColor.prototype._v = function(v) { return new LUVColor(this.l, this.u, v); } // -------------------- // Rebuild Colors // -------------------- FFZ.prototype._rebuild_contrast = function() { this._luv_required_bright = new XYZColor(0, (this.settings.luv_contrast * (new RGBColor(35,35,35).toXYZ().y + 0.05) - 0.05), 0).toLUV().l; this._luv_required_dark = new XYZColor(0, ((new RGBColor(217,217,217).toXYZ().y + 0.05) / this.settings.luv_contrast - 0.05), 0).toLUV().l; } FFZ.prototype._rebuild_colors = function() { if ( this.has_bttv ) return; // With update colors, we'll automatically process the colors we care about. this._colors = {}; this._update_colors(); } FFZ.prototype._update_colors = function(darkness_only) { // Update the lines. ALL of them. var Layout = window.App && App.__container__.lookup('controller:layout'), Settings = window.App && App.__container__.lookup('controller:settings'), is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); if ( darkness_only && this._color_old_darkness === is_dark ) return; this._color_old_darkness = is_dark; var colored_bits = document.querySelectorAll('.chat-line .has-color'); for(var i=0, l=colored_bits.length; i < l; i++) { var bit = colored_bits[i], color = bit.getAttribute('data-color'), colors = color && this._handle_color(color); if ( ! colors ) continue; bit.style.color = is_dark ? colors[1] : colors[0]; } } FFZ.prototype._handle_color = function(color) { if ( ! color || this._colors.hasOwnProperty(color) ) return this._colors[color]; var rgb = RGBColor.fromHex(color), light_color = color, dark_color = color; // Color Blindness Handling if ( this.settings.color_blind !== '0' ) { var new_color = rgb.daltonize(this.settings.color_blind); if ( ! rgb.eq(new_color) ) { rgb = new_color; light_color = dark_color = rgb.toHex(); } } // Color Processing - RGB if ( this.settings.fix_color === '4' ) { var lum = rgb.luminance(); if ( lum > 0.3 ) { var s = 127, nc = rgb; while(s--) { nc = nc.brighten(-1); if ( nc.luminance() <= 0.3 ) break; } light_color = nc.toHex(); } if ( lum < 0.15 ) { var s = 127, nc = rgb; while(s--) { nc = nc.brighten(); if ( nc.luminance() >= 0.15 ) break; } dark_color = nc.toHex(); } } // Color Processing - HSL if ( this.settings.fix_color === '2' ) { var hsl = rgb.toHSL(); light_color = hsl._l(Math.min(Math.max(0, 0.7 * hsl.l), 1)).toHex(); dark_color = hsl._l(Math.min(Math.max(0, 0.3 + (0.7 * hsl.l)), 1)).toHex(); } // Color Processing - HSV if ( this.settings.fix_color === '3' ) { var hsv = rgb.toHSV(); if ( hsv.s === 0 ) { // Black and White light_color = hsv._v(Math.min(Math.max(0.5, 0.5 * hsv.v), 1)).toRGB().toHex(); dark_color = hsv._v(Math.min(Math.max(0.5, 0.5 + (0.5 * hsv.v)), 1)).toRGB().toHex(); } else { light_color = RGBColor.fromHSV(hsv.h, Math.min(Math.max(0.7, 0.7 + (0.3 * hsv.s)), 1), Math.min(0.7, hsv.v)).toHex(); dark_color = RGBColor.fromHSV(hsv.h, Math.min(0.7, hsv.s), Math.min(Math.max(0.7, 0.7 + (0.3 * hsv.v)), 1)).toHex(); } } // Color Processing - LUV if ( this.settings.fix_color === '1' ) { var luv = rgb.toLUV(); if ( luv.l > this._luv_required_dark ) light_color = luv._l(this._luv_required_dark).toRGB().toHex(); if ( luv.l < this._luv_required_bright ) dark_color = luv._l(this._luv_required_bright).toRGB().toHex(); } var out = this._colors[color] = [light_color, dark_color]; return out; } },{}],4:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; // ----------------- // Log Export // ----------------- FFZ.ffz_commands.log = function(room, args) { this._pastebin(this._log_data.join("\n"), function(url) { if ( ! url ) return this.room_message(room, "There was an error uploading the FrankerFaceZ log."); this.room_message(room, "Your FrankerFaceZ log has been pasted to: " + url); }); }; // ----------------- // Mass Moderation // ----------------- FFZ.ffz_commands.massunmod = function(room, args) { args = args.join(" ").trim(); if ( ! args.length ) return "You must provide a list of users to unmod."; args = args.split(/\W*,\W*/); var user = this.get_user(); if ( ! user || ! user.login == room.id ) return "You must be the broadcaster to use massunmod."; if ( args.length > 50 ) 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."; var count = args.length; while(args.length) { var name = args.shift(); room.room.tmiRoom.sendMessage("/unmod " + name); } return "Sent unmod command for " + count + " users."; } FFZ.ffz_commands.massunmod.help = "Usage: /ffz massunmod \nBroadcaster only. Unmod all the users in the provided list."; FFZ.ffz_commands.massmod = function(room, args) { args = args.join(" ").trim(); if ( ! args.length ) return "You must provide a list of users to mod."; args = args.split(/\W*,\W*/); var user = this.get_user(); if ( ! user || ! user.login == room.id ) return "You must be the broadcaster to use massmod."; if ( args.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."; var count = args.length; while(args.length) { var name = args.shift(); room.room.tmiRoom.sendMessage("/mod " + name); } return "Sent mod command for " + count + " users."; } FFZ.ffz_commands.massmod.help = "Usage: /ffz massmod \nBroadcaster only. Mod all the users in the provided list."; /*FFZ.ffz_commands.massunban = function(room, args) { args = args.join(" ").trim(); }*/ },{}],5:[function(require,module,exports){ var SVGPATH = '', DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'), SERVER = DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/"; module.exports = { DEBUG: DEBUG, SERVER: SERVER, API_SERVER: "//api.frankerfacez.com/", API_SERVER_2: "//direct-api.frankerfacez.com/", KNOWN_CODES: { "#-?[\\\\/]": "#-/", ":-?(?: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" }, EMOTE_REPLACEMENT_BASE: SERVER + "script/replacements/", EMOTE_REPLACEMENTS: { 15: "15-JKanStyle.png", 16: "16-OptimizePrime.png", 17: "17-StoneLightning.png", 18: "18-TheRinger.png", 19: "19-PazPazowitz.png", 20: "20-EagleEye.png", 21: "21-CougarHunt.png", 22: "22-RedCoat.png", 26: "26-JonCarnage.png", 27: "27-PicoMause.png", 30: "30-BCWarrior.png", 33: "33-DansGame.png", 36: "36-PJSalt.png" }, 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 + '', ROOMS: '', CAMERA: '', INVITE: '', LIVE: '', EYE: '', CLOCK: '', GEAR: '', HEART: '', EMOTE: '', STAR: '', CLOSE: '', EDIT: '', GRAPH: '' } },{}],6:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; // ----------------------- // Developer Mode // ----------------------- FFZ.settings_info.developer_mode = { type: "boolean", value: false, storage_key: "ffzDebugMode", visible: function() { return this.settings.developer_mode || (Date.now() - parseInt(localStorage.ffzLastDevMode || "0")) < 604800000; }, 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(); } }; FFZ.ffz_commands.developer_mode = function(room, args) { var enabled, args = args && args.length ? args[0].toLowerCase() : null; if ( args == "y" || args == "yes" || args == "true" || args == "on" ) enabled = true; else if ( args == "n" || args == "no" || args == "false" || args == "off" ) enabled = false; if ( enabled === undefined ) return "Developer Mode is currently " + (this.settings.developer_mode ? "enabled." : "disabled."); this.settings.set("developer_mode", enabled); return "Developer Mode is now " + (enabled ? "enabled" : "disabled") + ". Please refresh your browser."; } FFZ.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."; },{}],7:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'), constants = require('../constants'); // -------------------- // Initialization // -------------------- 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); this.log("Creating channel style element."); var s = this._channel_style = document.createElement('style'); s.id = "ffz-channel-css"; document.head.appendChild(s); this.log("Hooking the Ember Channel Index view."); var Channel = App.__container__.resolve('view:channel/index'), f = this; if ( ! Channel ) return; this._modify_cindex(Channel); // The Stupid View Fix. Is this necessary still? try { Channel.create().destroy(); } catch(err) { } // Update Existing for(var key in Ember.View.views) { if ( ! Ember.View.views.hasOwnProperty(key) ) continue; var view = Ember.View.views[key]; if ( !(view instanceof Channel) ) continue; this.log("Manually updating Channel Index view.", view); this._modify_cindex(view); view.ffzInit(); }; this.log("Hooking the Ember Channel model."); Channel = App.__container__.resolve('model:channel'); if ( ! Channel ) return; Channel.reopen({ ffz_host_target: undefined, setHostMode: function(e) { if ( f.settings.hosted_channels ) { this.set('ffz_host_target', e.target); return this._super(e); } else { this.set('ffz_host_target', undefined); return this._super({target: void 0, delay: 0}); } } }); this.log("Hooking the Ember Channel controller."); Channel = App.__container__.lookup('controller:channel'); if ( ! Channel ) return; Channel.reopen({ ffzUpdateUptime: function() { if ( f._cindex ) f._cindex.ffzUpdateUptime(); }.observes("isLive", "content.id"), ffzUpdateInfo: function() { if ( this._ffz_update_timer ) clearTimeout(this._ffz_update_timer); if ( ! this.get('content.id') ) return; this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 60000); }.observes("content.id"), ffzCheckUpdate: function() { var t = this, id = t.get('content.id'); id && Twitch.api && Twitch.api.get("streams/" + id, {}, {version:3}) .done(function(data) { if ( ! data || ! data.stream ) { // If the stream is offline, clear its created_at time and set it to zero viewers. t.set('stream.created_at', null); t.set('stream.viewers', 0); return; } t.set('stream.created_at', data.stream.created_at || null); t.set('stream.viewers', data.stream.viewers || 0); var game = data.stream.game || (data.stream.channel && data.stream.channel.game); if ( game ) { t.set('game', game); t.set('rollbackData.game', game); } if ( data.stream.channel ) { if ( data.stream.channel.status ) t.set('status', data.stream.channel.status); if ( data.stream.channel.views ) t.set('views', data.stream.channel.views); if ( data.stream.channel.followers && t.get('content.followers.isLoaded') ) t.set('content.followers.total', data.stream.channel.followers); } }) .always(function(data) { t.ffzUpdateInfo(); }); }, ffzUpdateTitle: function() { var name = this.get('content.name'), display_name = this.get('content.display_name'); if ( display_name ) FFZ.capitalization[name] = [display_name, Date.now()]; if ( f._cindex ) f._cindex.ffzFixTitle(); }.observes("content.status", "content.id"), 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") }); Channel.ffzUpdateInfo(); } FFZ.prototype._modify_cindex = function(view) { var f = this; view.reopen({ didInsertElement: function() { this._super(); try { this.ffzInit(); } catch(err) { f.error("CIndex didInsertElement: " + err); } }, willClearRender: function() { try { this.ffzTeardown(); } catch(err) { f.error("CIndex willClearRender: " + err); } return this._super(); }, ffzInit: function() { var id = this.get('controller.id'), el = this.get('element'); f._cindex = this; f.ws_send("sub_channel", id); el.setAttribute('data-channel', id); el.classList.add('ffz-channel'); // Try changing the theater mode tooltip. this.$('.theatre-button a').attr('title', 'Theater Mode (Alt+T)'); this.ffzFixTitle(); this.ffzUpdateUptime(); this.ffzUpdateChatters(); this.ffzUpdateHostButton(); this.ffzUpdatePlayerStats(); 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(); if ( f.settings.auto_theater ) { var Layout = App.__container__.lookup('controller:layout'); if ( Layout ) Layout.set('isTheatreMode', true); } }, ffzFixTitle: function() { if ( f.has_bttv || ! f.settings.stream_title ) return; var status = this.get("controller.status"), channel = this.get("controller.id"); status = f.render_tokens(f.tokenize_line(channel, channel, status, true)); this.$(".title span").each(function(i, el) { var scripts = el.querySelectorAll("script"); if ( ! scripts.length ) el.innerHTML = status; else el.innerHTML = scripts[0].outerHTML + status + scripts[1].outerHTML; }); }, ffzUpdateHostButton: function() { var channel_id = this.get('controller.id'), hosted_id = this.get('controller.hostModeTarget.id'), user = f.get_user(), room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room, now_hosting = room && room.ffz_host_target, hosts_left = room && room.ffz_hosts_left, el = this.get('element'); this.set('ffz_host_updating', false); if ( channel_id ) { var container = el && el.querySelector('.stats-and-actions .channel-actions'), btn = container && container.querySelector('#ffz-ui-host-button'); if ( ! container || ! f.settings.stream_host_button || ! user || user.login === channel_id ) { if ( btn ) btn.parentElement.removeChild(btn); } else { if ( ! btn ) { btn = document.createElement('span'); btn.id = 'ffz-ui-host-button'; btn.className = 'button action tooltip'; btn.addEventListener('click', this.ffzClickHost.bind(btn, this, false)); var before; try { before = container.querySelector(':scope > .theatre-button'); } catch(err) { before = undefined; } if ( before ) container.insertBefore(btn, before); else container.appendChild(btn); } btn.classList.remove('disabled'); btn.innerHTML = channel_id === now_hosting ? 'Unhost' : 'Host'; if ( now_hosting ) btn.title = 'You are now hosting ' + utils.sanitize(FFZ.get_capitalization(now_hosting)) + '.'; else btn.title = 'You are not hosting any channel.'; if ( typeof hosts_left === "number" ) btn.title += ' You have ' + hosts_left + ' host command' + utils.pluralize(hosts_left) + ' remaining this half hour.'; } } if ( hosted_id ) { var container = el && el.querySelector('#hostmode .channel-actions'), btn = container && container.querySelector('#ffz-ui-host-button'); if ( ! container || ! f.settings.stream_host_button || ! user || user.login === hosted_id ) { if ( btn ) btn.parentElement.removeChild(btn); } else { if ( ! btn ) { btn = document.createElement('span'); btn.id = 'ffz-ui-host-button'; btn.className = 'button action tooltip'; btn.addEventListener('click', this.ffzClickHost.bind(btn, this, true)); var before; try { before = container.querySelector(':scope > .theatre-button'); } catch(err) { before = undefined; } if ( before ) container.insertBefore(btn, before); else container.appendChild(btn); } btn.classList.remove('disabled'); btn.innerHTML = hosted_id === now_hosting ? 'Unhost' : 'Host'; if ( now_hosting ) btn.title = 'You are currently hosting ' + utils.sanitize(FFZ.get_capitalization(now_hosting)) + '. Click to ' + (hosted_id === now_hosting ? 'unhost' : 'host') + ' this channel.'; else btn.title = 'You are not currently hosting any channel. Click to host this channel.'; if ( typeof hosts_left === "number" ) btn.title += ' You have ' + hosts_left + ' host command' + utils.pluralize(hosts_left) + ' remaining this half hour.'; } } }, ffzClickHost: function(controller, is_host) { var target = controller.get(is_host ? 'controller.hostModeTarget.id' : 'controller.id'), user = f.get_user(), room = user && f.rooms && f.rooms[user.login] && f.rooms[user.login].room, now_hosting = room && room.ffz_host_target; if ( ! room || controller.get('ffz_host_updating') ) return; this.classList.add('disabled'); this.title = 'Updating...'; controller.set('ffz_host_updating', true); if ( now_hosting === target ) room.send("/unhost"); else room.send("/host " + target); }, ffzUpdateChatters: function() { // Get the counts. var room_id = this.get('controller.id'), room = f.rooms && f.rooms[room_id]; if ( ! room || ! f.settings.chatter_count ) { var el = this.get('element').querySelector('#ffz-chatter-display'); el && el.parentElement.removeChild(el); el = this.get('element').querySelector('#ffz-ffzchatter-display'); el && el.parentElement.removeChild(el); return; } var chatter_count = Object.keys(room.room.get('ffz_chatters') || {}).length, ffz_chatters = room.ffz_chatters || 0, ffz_viewers = room.ffz_viewers || 0; var el = this.get('element').querySelector('#ffz-chatter-display span'); if ( ! el ) { var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); if ( ! cont ) return; var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-chatter-display'; stat.title = "Currently in Chat"; stat.innerHTML = constants.ROOMS + " "; el = document.createElement("span"); stat.appendChild(el); var other = cont.querySelector("#ffz-ffzchatter-display"); if ( other ) cont.insertBefore(stat, other); else cont.appendChild(stat); jQuery(stat).tipsy(); } el.innerHTML = utils.number_commas(chatter_count); if ( ! ffz_chatters && ! ffz_viewers ) { el = this.get('element').querySelector('#ffz-ffzchatter-display'); el && el.parentNode.removeChild(el); return; } el = this.get('element').querySelector('#ffz-ffzchatter-display span'); if ( ! el ) { var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); if ( ! cont ) return; var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-ffzchatter-display'; stat.title = "Viewers (In Chat) with FrankerFaceZ"; stat.innerHTML = constants.ZREKNARF + " "; el = document.createElement("span"); stat.appendChild(el); var other = cont.querySelector("#ffz-chatter-display"); if ( other ) cont.insertBefore(stat, other.nextSibling); else cont.appendChild(stat); jQuery(stat).tipsy(); } el.innerHTML = utils.number_commas(ffz_viewers) + " (" + utils.number_commas(ffz_chatters) + ")"; }, ffzUpdatePlayerStats: function() { var channel_id = this.get('controller.id'), hosted_id = this.get('controller.hostModeTarget.id'), el = this.get('element'); if ( channel_id ) { var container = el && el.querySelector('.stats-and-actions .channel-stats'), stat_el = container && container.querySelector('#ffz-ui-player-stats'), el = stat_el && stat_el.querySelector('span'), player_cont = f.players && f.players[channel_id], player = player_cont && player_cont.player, stats = player && player.stats; if ( ! container || ! f.settings.player_stats || ! stats || stats.hlsLatencyBroadcaster === 'NaN' || stats.hlsLatencyBroadcaster === NaN ) { if ( stat_el ) stat_el.parentElement.removeChild(stat_el); } else { if ( ! stat_el ) { stat_el = document.createElement('span'); stat_el.id = 'ffz-ui-player-stats'; stat_el.className = 'ffz stat tooltip'; stat_el.innerHTML = constants.GRAPH + " "; el = document.createElement('span'); stat_el.appendChild(el); var other = container.querySelector('#ffz-uptime-display'); if ( other ) container.insertBefore(stat_el, other.nextSibling); else container.appendChild(stat_el); } stat_el.title = 'Stream Latency\nFPS: ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'; el.textContent = stats.hlsLatencyBroadcaster + 's'; } } if ( hosted_id ) { var container = el && el.querySelector('#hostmode .channel-stats'), stat_el = container && container.querySelector('#ffz-ui-player-stats'), el = stat_el && stat_el.querySelector('span'), player_cont = f.players && f.players[hosted_id], player = player_cont && player_cont.player, stats = player && player.stats; if ( ! container || ! f.settings.player_stats || ! stats || stats.hlsLatencyBroadcaster === 'NaN' || stats.hlsLatencyBroadcaster === NaN ) { if ( stat_el ) stat_el.parentElement.removeChild(stat_el); } else { if ( ! stat_el ) { stat_el = document.createElement('span'); stat_el.id = 'ffz-ui-player-stats'; stat_el.className = 'ffz stat tooltip'; stat_el.innerHTML = constants.GRAPH + " "; el = document.createElement('span'); stat_el.appendChild(el); var other = container.querySelector('#ffz-uptime-display'); if ( other ) container.insertBefore(stat_el, other.nextSibling); else container.appendChild(stat_el); } stat_el.title = 'Stream Latency\nFPS: ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'; el.textContent = stats.hlsLatencyBroadcaster + 's'; } } }, ffzUpdateUptime: function() { if ( this._ffz_update_uptime ) { clearTimeout(this._ffz_update_uptime); delete this._ffz_update_uptime; } if ( ! f.settings.stream_uptime || ! this.get("controller.isLiveAccordingToKraken") ) { var el = this.get('element').querySelector('#ffz-uptime-display'); if ( el ) el.parentElement.removeChild(el); return; } // Schedule an update. this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000); // Determine when the channel last went live. var online = this.get("controller.content.stream.created_at"); online = online && utils.parse_date(online); var uptime = online && Math.floor((Date.now() - online.getTime()) / 1000) || -1; if ( uptime < 0 ) { var el = this.get('element').querySelector('#ffz-uptime-display'); if ( el ) el.parentElement.removeChild(el); return; } var el = this.get('element').querySelector('#ffz-uptime-display span'); if ( ! el ) { var cont = this.get('element').querySelector('.stats-and-actions .channel-stats'); if ( ! cont ) return; var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-uptime-display'; stat.title = "Stream Uptime (since " + online.toLocaleString() + ")"; stat.innerHTML = constants.CLOCK + " "; el = document.createElement("span"); stat.appendChild(el); var viewers = cont.querySelector(".live-count"); if ( viewers ) cont.insertBefore(stat, viewers.nextSibling); else { try { viewers = cont.querySelector("script:nth-child(0n+2)"); cont.insertBefore(stat, viewers.nextSibling); } catch(err) { cont.insertBefore(stat, cont.childNodes[0]); } } jQuery(stat).tipsy({html: true}); } el.innerHTML = utils.time_to_string(uptime); }, 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); } }); } // --------------- // Settings // --------------- FFZ.settings_info.auto_theater = { type: "boolean", value: false, category: "Appearance", no_mobile: true, no_bttv: true, name: "Automatic Theater Mode", help: "Automatically enter theater mode when opening a channel." }; FFZ.settings_info.chatter_count = { type: "boolean", value: false, no_mobile: true, category: "Channel Metadata", name: "Chatter Count", help: "Display the current number of users connected to chat beneath the channel.", on_update: function(val) { if ( this._cindex ) this._cindex.ffzUpdateChatters(); if ( ! val || ! this.rooms ) return; // Refresh the data. for(var room_id in this.rooms) this.rooms.hasOwnProperty(room_id) && this.rooms[room_id].room && this.rooms[room_id].room.ffzInitChatterCount(); } }; FFZ.settings_info.channel_views = { type: "boolean", value: true, no_mobile: true, category: "Channel Metadata", name: "Channel Views", help: 'Display the number of times the channel has been viewed beneath the stream.', on_update: function(val) { document.body.classList.toggle("ffz-hide-view-count", !val); } }; FFZ.settings_info.hosted_channels = { type: "boolean", value: true, no_mobile: true, category: "Channel Metadata", name: "Channel Hosting", help: "Display other channels that have been featured by the current channel.", on_update: function(val) { var cb = document.querySelector('input.ffz-setting-hosted-channels'); if ( cb ) cb.checked = val; if ( ! this._cindex ) return; var chan = this._cindex.get('controller.model'), room = chan && this.rooms && this.rooms[chan.get('id')], target = room && room.room && room.room.get('ffz_host_target'); if ( ! chan || ! room ) return; chan.setHostMode({target: target, delay: 0}); } }; FFZ.settings_info.stream_host_button = { type: "boolean", value: true, no_mobile: true, category: "Channel Metadata", name: "Host This Channel Button", help: "Display a button underneath streams that make it easy to host them with your own channel.", on_update: function(val) { if ( this._cindex ) this._cindex.ffzUpdateHostButton(); } }; FFZ.settings_info.stream_uptime = { type: "boolean", value: false, no_mobile: true, category: "Channel Metadata", name: "Stream Uptime", help: 'Display the stream uptime under a channel by the viewer count.', on_update: function(val) { if ( this._cindex ) this._cindex.ffzUpdateUptime(); } }; FFZ.settings_info.stream_title = { type: "boolean", value: true, no_bttv: true, no_mobile: true, category: "Channel Metadata", name: "Title Links", help: "Make links in stream titles clickable.", on_update: function(val) { if ( this._cindex ) this._cindex.ffzFixTitle(); } }; },{"../constants":5,"../utils":35}],8:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), constants = require("../constants"), is_android = navigator.userAgent.indexOf('Android') !== -1, KEYCODES = { BACKSPACE: 8, TAB: 9, ENTER: 13, ESC: 27, SPACE: 32, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, TWO: 50, COLON: 59, FAKE_COLON: 186 }, selection_start = function(e) { if ( typeof e.selectionStart === "number" ) return e.selectionStart; if ( ! e.createTextRange ) return -1; var n = document.selection.createRange(), r = e.createTextRange(); r.moveToBookmark(n.getBookmark()); r.moveStart("character", -e.value.length); return r.text.length; }, move_selection = function(e, pos) { if ( e.setSelectionRange ) e.setSelectionRange(pos, pos); else if ( e.createTextRange ) { var r = e.createTextRange(); r.move("character", -e.value.length); r.move("character", pos); r.select(); } }; // --------------------- // Settings // --------------------- FFZ.settings_info.input_quick_reply = { type: "boolean", value: true, category: "Chat Input", no_bttv: true, name: "Reply to Whispers with /r", help: "Automatically replace /r at the start of the line with the command to whisper to the person you've whispered with most recently." }; FFZ.settings_info.input_mru = { type: "boolean", value: true, category: "Chat Input", no_bttv: true, name: "Chat Input History", help: "Use the Up and Down arrows in chat to select previously sent chat messages." }; FFZ.settings_info.input_emoji = { type: "boolean", value: false, category: "Chat Input", //visible: false, no_bttv: true, name: "Enter Emoji By Name", help: "Replace emoji that you type by name with the character. :+1: becomes 👍." }; // --------------------- // Initialization // --------------------- FFZ.prototype.setup_chat_input = function() { this.log("Hooking the Ember Chat Input controller."); var Input = App.__container__.resolve('component:twitch-chat-input'), f = this; if ( ! Input ) return; this._modify_chat_input(Input); if ( this._roomv ) { for(var i=0; i < this._roomv._childViews.length; i++) { var v = this._roomv._childViews[i]; if ( v instanceof Input ) { this._modify_chat_input(v); v.ffzInit(); } } } } FFZ.prototype._modify_chat_input = function(component) { var f = this; component.reopen({ ffz_mru_index: -1, didInsertElement: function() { this._super(); try { this.ffzInit(); } catch(err) { f.error("ChatInput didInsertElement: " + err); } }, willClearRender: function() { try { this.ffzTeardown(); } catch(err) { f.error("ChatInput willClearRender: " + err); } return this._super(); }, ffzInit: function() { f._inputv = this; var s = this._ffz_minimal_style = document.createElement('style'); s.id = 'ffz-minimal-chat-textarea-height'; document.head.appendChild(s); // Redo our key bindings. var t = this.$("textarea"); t.off("keydown"); t.on("keydown", this._ffzKeyDown.bind(this)); t.attr('rows', 1); this.ffzResizeInput(); setTimeout(this.ffzResizeInput.bind(this), 500); /*var suggestions = this._parentView.get('context.model.chatSuggestions'); this.set('ffz_chatters', suggestions);*/ }, ffzTeardown: function() { if ( f._inputv === this ) f._inputv = undefined; this.ffzResizeInput(); if ( this._ffz_minimal_style ) { this._ffz_minimal_style.parentElement.removeChild(this._ffz_minimal_style); this._ffz_minimal_style = undefined; } // Reset normal key bindings. var t = this.$("textarea"); t.attr('rows', undefined); t.off("keydown"); t.on("keydown", this._onKeyDown.bind(this)); }, // Input Control ffzOnInput: function() { if ( ! f._chat_style || ! f.settings.minimal_chat || is_android ) return; var now = Date.now(), since = now - (this._ffz_last_resize || 0); if ( since > 500 ) this.ffzResizeInput(); }.observes('textareaValue'), ffzResizeInput: function() { this._ffz_last_resize = Date.now(); var el = this.get('element'), t = el && el.querySelector('textarea'); if ( ! t || ! f._chat_style || ! f.settings.minimal_chat ) return; // Unfortunately, we need to change this with CSS. this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat .ember-chat .chat-interface .textarea-contain textarea { height: auto !important; }'; var height = Math.max(32, Math.min(128, t.scrollHeight)); this._ffz_minimal_style.innerHTML = 'body.ffz-minimal-chat .ember-chat .chat-interface .textarea-contain textarea { height: ' + height + 'px !important; }'; if ( height !== this._ffz_last_height ) { utils.update_css(f._chat_style, "input_height", 'body.ffz-minimal-chat .ember-chat .chat-interface { height: ' + height + 'px !important; }' + 'body.ffz-minimal-chat .ember-chat .chat-messages, body.ffz-minimal-chat .ember-chat .chat-interface .emoticon-selector { bottom: ' + height + 'px !important; }'); f._roomv && f._roomv.get('stuckToBottom') && f._roomv._scrollToBottom(); } this._ffz_last_height = height; }, _ffzKeyDown: function(event) { var e = event || window.event, key = e.charCode || e.keyCode; switch(key) { case KEYCODES.UP: case KEYCODES.DOWN: if ( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) return; else if ( this.get("isShowingSuggestions") ) e.preventDefault(); else if ( f.settings.input_mru ) Ember.run.next(this.ffzCycleMRU.bind(this, key, selection_start(this.get("chatTextArea")))); else return this._onKeyDown(event); break; case KEYCODES.SPACE: if ( f.settings.input_quick_reply && selection_start(this.get("chatTextArea")) === 2 && this.get("textareaValue").substring(0,2) === "/r" ) { var t = this; Ember.run.next(function() { var wt = t.get("uniqueWhisperSuggestions.0"); if ( wt ) { var text = "/w " + wt + t.get("textareaValue").substr(2); t.set("_currentWhisperTarget", 0); t.set("textareaValue", text); Ember.run.next(function() { move_selection(t.get('chatTextArea'), 4 + wt.length); }); } }); } else return this._onKeyDown(event); break; case KEYCODES.COLON: case KEYCODES.FAKE_COLON: if ( f.settings.input_emoji && (e.shiftKey || e.shiftLeft) ) { var t = this, ind = selection_start(this.get("chatTextArea")); ind > 0 && Ember.run.next(function() { var text = t.get("textareaValue"), emoji_start = text.lastIndexOf(":", ind - 1); if ( emoji_start !== -1 && ind !== -1 && text.charAt(ind) === ":" ) { var match = text.substr(emoji_start + 1, ind-emoji_start - 1), emoji_id = f.emoji_names[match], emoji = f.emoji_data[emoji_id]; if ( emoji ) { var prefix = text.substr(0, emoji_start) + emoji.raw; t.set('textareaValue', prefix + text.substr(ind + 1)); Ember.run.next(function() { move_selection(t.get('chatTextArea'), prefix.length); }); } } }); return; } return this._onKeyDown(event); case KEYCODES.ENTER: if ( ! e.shiftKey && ! e.shiftLeft ) this.set('ffz_mru_index', -1); default: return this._onKeyDown(event); } }, ffzCycleMRU: function(key, start_ind) { // We don't want to do this if the keys were just moving the cursor around. var cur_pos = selection_start(this.get("chatTextArea")); if ( start_ind !== cur_pos ) return; var ind = this.get('ffz_mru_index'), mru = this._parentView.get('context.model.mru_list') || []; if ( key === KEYCODES.UP ) ind = (ind + 1) % (mru.length + 1); else ind = (ind + mru.length) % (mru.length + 1); var old_val = this.get('ffz_old_mru'); if ( old_val === undefined || old_val === null ) { old_val = this.get('textareaValue'); this.set('ffz_old_mru', old_val); } var new_val = mru[ind]; if ( new_val === undefined ) { this.set('ffz_old_mru', undefined); new_val = old_val; } this.set('ffz_mru_index', ind); this.set('textareaValue', new_val); }, completeSuggestion: function(e) { var r, n, i = this, o = this.get("textareaValue"), a = this.get("partialNameStartIndex"); r = o.substring(0, a) + (o.charAt(0) === "/" ? e : FFZ.get_capitalization(e)); n = o.substring(a + this.get("partialName").length); if ( ! n ) r += " "; this.set("textareaValue", r + n); this.set("isShowingSuggestions", false); this.set("partialName", ""); this.trackSuggestionsCompleted(); Ember.run.next(function() { move_selection(i.get('chatTextArea'), r.length); }); } /*ffz_emoticons: function() { var output = [], room = this._parentView.get('context.model'), room_id = room && room.get('id'), tmi = room && room.tmiSession, user = f.get_user(), ffz_sets = f.getEmotes(user && user.login, room_id); if ( tmi ) { var es = tmi.getEmotes(); if ( es && es.emoticon_sets ) { for(var set_id in es.emoticon_sets) { var emote_set = es.emoticon_sets[set_id]; for(var emote_id in emote_set) { if ( emote_set[emote_id] ) { var code = emote_set[emote_id].code; output.push({id: constants.KNOWN_CODES[code] || code}); } } } } } for(var i=0; i < ffz_sets.length; i++) { var emote_set = f.emote_sets[ffz_sets[i]]; if ( ! emote_set ) continue; for(var emote_id in emote_set.emoticons) { var emote = emote_set.emoticons[emote_id]; if ( ! emote.hidden ) output.push({id:emote.name}); } } return output; }.property(), ffz_chatters: [], suggestions: function(key, value, previousValue) { if ( arguments.length > 1 ) { this.set('ffz_chatters', value); } var output = []; // Chatters output = output.concat(this.get('ffz_chatters')); // Emoticons if ( this.get('isSuggestionsTriggeredWithTab') ) { output = output.concat(this.get('ffz_emoticons')); } return output; }.property("ffz_emoticons", "ffz_chatters", "isSuggestionsTriggeredWithTab")*/ }); } },{"../constants":5,"../utils":35}],9:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'), constants = require('../constants'); // -------------------- // Settings // -------------------- FFZ.basic_settings.cure_cancer = { type: "boolean", category: "Chat", name: "Cure Cancer", help: "Destroys all cancerous chat messages before they can even be seen.", get: function() { return this.settings.remove_deleted && this.settings.remove_bot_ban_notices && +this.settings.chat_delay; }, set: function(val) { this.settings.set('remove_deleted', val); this.settings.set('remove_bot_ban_notices', val); this.settings.set('chat_delay', val ? ''+(+this.settings.chat_delay || 300) : '0'); } }; FFZ.settings_info.minimal_chat = { type: "boolean", value: false, category: "Chat Appearance", 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"); f._roomv && f._roomv.get('stuckToBottom') && f._roomv._scrollToBottom(); },0); } if ( this._chatv && this._chatv.get('controller.showList') ) this._chatv.set('controller.showList', false); // Remove the style if we have it. if ( ! val && this._chat_style ) { if ( this._inputv ) { if ( this._inputv._ffz_minimal_style ) this._inputv._ffz_minimal_style.innerHTML = ''; this._inputv._ffz_last_height = undefined; } utils.update_css(this._chat_style, "input_height", ''); this._roomv && this._roomv.get('stuckToBottom') && this._roomv._scrollToBottom(); } else if ( this._inputv ) this._inputv.ffzResizeInput(); } }; FFZ.settings_info.chat_delay = { type: "select", options: { 0: "No Delay", 300: "Wait for bot auto-bans (300ms)", 1200: "Wait for human mods (1200ms)", 5000: "ESPORTS (5000ms)" }, value: 0, category: "Chat Appearance", name: "Artificial Chat Delay", help: "Delay messages allowing moderators to ban them before you see them.", on_update: function (val) { var delay_badge = document.querySelector('#ffz-stat-delay'); delay_badge.title = utils.number_commas(+val||300) + "ms of artifical chat delay added."; delay_badge.classList.toggle('hidden', !+val); } }; FFZ.settings_info.remove_deleted = { type: "boolean", value: false, no_bttv: true, category: "Chat Filtering", name: "Remove Deleted Messages", help: "Remove deleted messages from chat entirely rather than leaving behind a clickable <deleted message>.", on_update: function(val) { if ( this.has_bttv || ! this.rooms || ! val ) return; for(var room_id in this.rooms) { var ffz_room = this.rooms[room_id], room = ffz_room && ffz_room.room; if ( ! room ) continue; var msgs = room.get('messages'), total = msgs.get('length'), i = total, alternate; while(i--) { var msg = msgs.get(i); if ( msg.ffz_deleted || msg.deleted ) { if ( alternate === undefined ) alternate = msg.ffz_alternate; msgs.removeAt(i); continue; } if ( alternate === undefined ) alternate = msg.ffz_alternate; else { alternate = ! alternate; room.set('messages.' + i + '.ffz_alternate', alternate); } } } } }; FFZ.settings_info.remove_bot_ban_notices = { type: "boolean", value: false, category: "Chat Filtering", name: "Remove Bot Ban Notices", help: "Remove messages from bots announcing who was banned for what reason and for how long.", }; FFZ.settings_info.prevent_clear = { type: "boolean", value: false, no_bttv: true, category: "Chat Filtering", name: "Show Deleted Messages", help: "Fade deleted messages instead of replacing them, and prevent chat from being cleared.", on_update: function(val) { if ( this.has_bttv || ! this.rooms ) return; for(var room_id in this.rooms) { var ffz_room = this.rooms[room_id], room = ffz_room && ffz_room.room; if ( ! room ) continue; room.get("messages").forEach(function(s, n) { if ( val && ! s.ffz_deleted && s.deleted ) room.set("messages." + n + ".deleted", false); else if ( s.ffz_deleted && ! val && ! s.deleted ) room.set("messages." + n + ".deleted", true); }); } } }; FFZ.settings_info.chat_history = { type: "boolean", value: true, visible: false, category: "Chat Appearance", name: "Chat History Alpha", help: "Load previous chat messages when loading a chat room so you can see what people have been talking about. This currently only works in a handful of channels due to server capacity.", }; FFZ.settings_info.group_tabs = { type: "boolean", value: false, no_bttv: true, category: "Chat Moderation", name: "Chat Room Tabs Beta", help: "Enhanced UI for switching the current chat room and noticing new messages.", on_update: function(val) { var enabled = !this.has_bttv && val; if ( ! this._chatv || enabled === this._group_tabs_state ) return; if ( enabled ) this._chatv.ffzEnableTabs(); else this._chatv.ffzDisableTabs(); } }; FFZ.settings_info.pinned_rooms = { value: [], visible: false, }; FFZ.settings_info.visible_rooms = { value: [], visible: false, }; // -------------------- // Initialization // -------------------- FFZ.prototype.setup_chatview = function() { 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'), f = this; if ( Chat ) { Chat.reopen({ ffzUpdateChannels: function() { if ( ! f._chatv ) return; f._chatv.ffzRebuildMenu(); if ( f.settings.group_tabs ) f._chatv.ffzRebuildTabs(); }.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'), user = f.get_user(); if ( ! f.settings.pinned_rooms || f.settings.pinned_rooms.indexOf(room_id) === -1 ) { if ( room === this.get("currentRoom") ) this.blurRoom(); // Don't destroy it if it's the user's room. if ( room && user && user.login === room_id ) room.destroy(); } this.set("currentChannelRoom", void 0); } }); } this.log("Hooking the Ember Chat view."); var Chat = App.__container__.resolve('view:chat'); this._modify_cview(Chat); // For some reason, this doesn't work unless we create an instance of the // chat view and then destroy it immediately. try { Chat.create().destroy(); } catch(err) { } // Modify all existing Chat views. for(var key in Ember.View.views) { if ( ! Ember.View.views.hasOwnProperty(key) ) continue; var view = Ember.View.views[key]; if ( !(view instanceof Chat) ) continue; this.log("Manually updating existing Chat view.", view); try { view.ffzInit(); } catch(err) { this.error("setup: build_ui_link: " + err); } } this.log("Hooking the Ember 'Right Column' controller. Seriously..."); var Column = App.__container__.lookup('controller:right-column'); if ( ! Column ) return; Column.reopen({ ffzFixTabs: function() { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { setTimeout(function() { f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); },0); } }.observes("firstTabSelected") }); } // -------------------- // Modify Chat View // -------------------- FFZ.prototype._modify_cview = function(view) { var f = this; view.reopen({ didInsertElement: function() { this._super(); try { this.ffzInit(); } catch(err) { f.error("ChatView didInsertElement: " + err); } }, willClearRender: function() { try { this.ffzTeardown(); } catch(err) { f.error("ChatView willClearRender: " + err); } this._super(); }, ffzInit: function() { f._chatv = this; this.$('.textarea-contain').append(f.build_ui_link(this)); this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true, gravity: jQuery.fn.tipsy.autoNS}); if ( !f.has_bttv && f.settings.group_tabs ) this.ffzEnableTabs(); this.ffzRebuildMenu(); setTimeout(function() { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); var controller = f._chatv.get('controller'); controller && controller.set('showList', false); }, 1000); }, ffzTeardown: function() { if ( f._chatv === this ) f._chatv = null; this.$('.textarea-contain .ffz-ui-toggle').remove(); if ( f.settings.group_tabs ) this.ffzDisableTabs(); }, ffzChangeRoom: Ember.observer('controller.currentRoom', function() { f.update_ui_link(); var room = this.get('controller.currentRoom'), rows; room && room.resetUnreadCount(); if ( this._ffz_chan_table ) { rows = jQuery(this._ffz_chan_table); rows.children('.ffz-room-row').removeClass('active'); if ( room ) rows.children('.ffz-room-row[data-room="' + room.get('id') + '"]').addClass('active').children('span').text(''); } if ( this._ffz_group_table ) { rows = jQuery(this._ffz_group_table); rows.children('.ffz-room-row').removeClass('active'); if ( room ) rows.children('.ffz-room-row[data-room="' + room.get('id') + '"]').addClass('active').children('span').text(''); } if ( !f.has_bttv && f.settings.group_tabs && this._ffz_tabs ) { var tabs = jQuery(this._ffz_tabs); tabs.children('.ffz-chat-tab').removeClass('active'); if ( room && room._ffz_tab ) { room._ffz_tab.classList.remove('tab-mentioned'); room._ffz_tab.classList.remove('hidden'); room._ffz_tab.classList.add('active'); var sp = room._ffz_tab.querySelector('span'); if ( sp ) sp.innerHTML = ''; } // Invite Link var can_invite = room && room.get('canInvite'); this._ffz_invite && this._ffz_invite.classList.toggle('hidden', !can_invite); this.set('controller.showInviteUser', can_invite && this.get('controller.showInviteUser')) // Now, adjust the chat-room. this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); } }), // Better Menu ffzRebuildMenu: function() { return; var el = this.get('element'), room_list = el && el.querySelector('.chat-rooms .tse-content'); if ( ! room_list ) return; if ( ! room_list.classList.contains('ffz-room-list') ) { room_list.classList.add('ffz-room-list'); // Find the Pending Invitations var headers = room_list.querySelectorAll('.list-header'), hdr = headers.length ? headers[headers.length-1] : undefined; if ( hdr ) { hdr.classList.add('ffz'); if ( hdr.nextSibling && hdr.nextSibling.classList ) hdr.nextSibling.classList.add('ffz'); } } // Channel Table var t = this, chan_table = this._ffz_chan_table || room_list.querySelector('#ffz-channel-table tbody'); if ( ! chan_table ) { var tbl = document.createElement('table'); tbl.setAttribute('cellspacing', 0); tbl.id = 'ffz-channel-table'; tbl.className = 'ffz'; tbl.innerHTML = 'ChannelsJoinPin'; room_list.insertBefore(tbl, room_list.firstChild); chan_table = this._ffz_chan_table = tbl.querySelector('tbody'); } chan_table.innerHTML = ''; // Current Channel var room = this.get('controller.currentChannelRoom'), row; if ( room ) { row = this.ffzBuildRow(this, room, true); row && chan_table.appendChild(row); } // Host Target if ( this._ffz_host_room ) { row = this.ffzBuildRow(this, this._ffz_host_room, false, true); row && chan_table.appendChild(row); } // Pinned Rooms for(var i=0; i < f.settings.pinned_rooms.length; i++) { var room_id = f.settings.pinned_rooms[i]; if ( room && room.get('id') !== room_id && this._ffz_host !== room_id && f.rooms[room_id] && f.rooms[room_id].room ) { row = this.ffzBuildRow(this, f.rooms[room_id].room); row && chan_table.appendChild(row); } } // Group Chat Table var group_table = this._ffz_group_table || room_list.querySelector('#ffz-group-table tbody'); if ( ! group_table ) { var tbl = document.createElement('table'); tbl.setAttribute('cellspacing', 0); tbl.id = 'ffz-group-table'; tbl.className = 'ffz'; tbl.innerHTML = 'Group ChatsPin'; var before = room_list.querySelector('#ffz-channel-table'); room_list.insertBefore(tbl, before.nextSibling); group_table = this._ffz_group_table = tbl.querySelector('tbody'); } group_table.innerHTML = ''; _.each(this.get('controller.connectedPrivateGroupRooms'), function(room) { var row = t.ffzBuildRow(t, room); row && group_table && group_table.appendChild(row); }); // Change Create Tooltip var create_btn = el.querySelector('.button.create'); if ( create_btn ) create_btn.title = 'Create a Group Room'; }, ffzBuildRow: function(view, room, current_channel, host_channel) { var row = document.createElement('tr'), icon = document.createElement('td'), name_el = document.createElement('td'), btn, toggle_pinned = document.createElement('td'), toggle_visible = document.createElement('td'), group = room.get('isGroupRoom'), current = room === view.get('controller.currentRoom'), //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) { f.log("Name for Row: " + name); //unread = format_unread(current ? 0 : room.get('unreadCount')); name_el.innerHTML = utils.sanitize(name); })); name_el.className = 'ffz-room'; name_el.innerHTML = utils.sanitize(name); if ( current_channel ) { icon.innerHTML = constants.CAMERA; icon.title = name_el.title = "Current Channel"; icon.className = name_el.className = 'tooltip'; } else if ( host_channel ) { icon.innerHTML = constants.EYE; icon.title = name_el.title = "Hosted Channel"; icon.className = name_el.className = 'tooltip'; } toggle_pinned.className = toggle_visible.className = 'ffz-row-switch'; toggle_pinned.innerHTML = ''; toggle_visible.innerHTML = ''; row.setAttribute('data-room', room.get('id')); row.className = 'ffz-room-row'; row.classList.toggle('current-channel', current_channel); row.classList.toggle('host-channel', host_channel); row.classList.toggle('group-chat', group); row.classList.toggle('active', current); row.appendChild(icon); row.appendChild(name_el); if ( ! group ) { row.appendChild(toggle_pinned); btn = toggle_pinned.querySelector('a.switch'); btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation && e.stopPropagation(); var room_id = room.get('id'), is_pinned = f.settings.pinned_rooms.indexOf(room_id) !== -1; if ( is_pinned ) f._leave_room(room_id); else f._join_room(room_id); this.classList.toggle('active', !is_pinned); }); } else { btn = document.createElement('a'); btn.className = 'leave-chat tooltip'; btn.innerHTML = constants.CLOSE; btn.title = 'Leave Group'; name_el.appendChild(btn); btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation && e.stopPropagation(); if ( ! confirm('Are you sure you want to leave the group room "' + name + '"?') ) return; room.get('isGroupRoom') && room.del(); }); } row.appendChild(toggle_visible); btn = toggle_visible.querySelector('a.switch'); btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation && e.stopPropagation(); var room_id = room.get('id'), visible_rooms = f.settings.visible_rooms, is_visible = visible_rooms.indexOf(room_id) !== -1; if ( is_visible ) visible_rooms.removeObject(room_id); else visible_rooms.push(room_id); f.settings.set('visible_rooms', visible_rooms); this.classList.toggle('active', !is_visible); view.ffzRebuildTabs(); }); row.addEventListener('click', function() { var controller = view.get('controller'); controller.focusRoom(room); controller.set('showList', false); }); return row; }, // Group Tabs~! ffzEnableTabs: function() { if ( f.has_bttv || ! f.settings.group_tabs ) return; // Hide the existing chat UI. this.$(".chat-header").addClass("hidden"); // Create our own UI. var tabs = this._ffz_tabs = document.createElement("div"); tabs.id = "ffz-group-tabs"; this.$(".chat-header").after(tabs); // List the Rooms this.ffzRebuildTabs(); }, ffzRebuildTabs: function() { if ( f.has_bttv || ! f.settings.group_tabs ) return; var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'); if ( ! tabs ) return; tabs.innerHTML = ""; var link = document.createElement('a'), view = this; link.className = 'button glyph-only tooltip'; link.title = "Chat Room Management"; link.innerHTML = constants.ROOMS; link.addEventListener('click', function() { var controller = view.get('controller'); controller && controller.set('showList', !controller.get('showList')); }); tabs.appendChild(link); link = document.createElement('a'), link.className = 'button glyph-only tooltip invite'; link.title = "Invite a User"; link.innerHTML = constants.INVITE; link.addEventListener('click', function() { var controller = view.get('controller'); controller && controller.set('showInviteUser', controller.get('currentRoom.canInvite') && !controller.get('showInviteUser')); }); link.classList.toggle('hidden', !this.get("controller.currentRoom.canInvite")); view._ffz_invite = link; tabs.appendChild(link); var room = this.get('controller.currentChannelRoom'), tab; if ( room ) { tab = this.ffzBuildTab(view, room, true); tab && tabs.appendChild(tab); } // Check Host Target var Channel = App.__container__.lookup('controller:channel'), Room = App.__container__.resolve('model:room'); target = Channel && Channel.get('hostModeTarget'); if ( target && Room ) { var target_id = target.get('id'); if ( this._ffz_host !== target_id ) { 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(); } this._ffz_host = target_id; this._ffz_host_room = Room.findOne(target_id); } } else if ( this._ffz_host ) { 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(); } delete this._ffz_host; delete this._ffz_host_room; } if ( this._ffz_host_room ) { tab = view.ffzBuildTab(view, this._ffz_host_room, false, true); tab && tabs.appendChild(tab); } // Pinned Rooms for(var i=0; i < f.settings.pinned_rooms.length; i++) { var room_id = f.settings.pinned_rooms[i]; if ( room && room.get('id') !== room_id && this._ffz_host !== room_id && f.rooms[room_id] && f.rooms[room_id].room ) { var tab = view.ffzBuildTab(view, f.rooms[room_id].room, false, false); tab && tabs.appendChild(tab); } } _.each(this.get('controller.connectedPrivateGroupRooms'), function(room) { var tab = view.ffzBuildTab(view, room); tab && tabs.appendChild(tab); }); // Now, adjust the chat-room. this.$('.chat-room').css('top', tabs.offsetHeight + "px"); }, ffzTabUnread: function(room_id) { // TODO: Update menu. if ( f.has_bttv || ! f.settings.group_tabs ) return; var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'), current_id = this.get('controller.currentRoom.id'); if ( ! tabs ) return; if ( room_id ) { var room = f.rooms && f.rooms[room_id] && f.rooms[room_id].room, tab = room && room._ffz_tab; if ( tab ) { var unread = utils.format_unread(room_id === current_id ? 0 : room.get('unreadCount')); tab.querySelector('span').innerHTML = unread; } } var children = tabs.querySelectorAll('.ffz-chat-tab'); for(var i=0; i < children.length; i++) { var tab = children[i], room_id = tab.getAttribute('data-room'), room = f.rooms && f.rooms[room_id] && f.rooms[room_id]; if ( ! room ) continue; var unread = utils.format_unread(room_id === current_id ? 0 : room.room.get('unreadCount')); tab.querySelector('span').innerHTML = unread; } }, ffzBuildTab: function(view, room, current_channel, host_channel) { var tab = document.createElement('span'), name, unread, icon = '', room_id = room.get('id'), group = room.get('isGroupRoom'), current = room === view.get('controller.currentRoom'), visible = current || f.settings.visible_rooms.indexOf(room_id) !== -1; tab.setAttribute('data-room', room.id); tab.className = 'ffz-chat-tab tooltip'; //tab.classList.toggle('hidden', ! visible); tab.classList.toggle('current-channel', current_channel); tab.classList.toggle('host-channel', host_channel); tab.classList.toggle('group-chat', group); tab.classList.toggle('active', current); unread = utils.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 = utils.format_unread(current ? 0 : room.get('unreadCount')); tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; })); if ( current_channel ) { icon = constants.CAMERA; tab.title = "Current Channel"; } else if ( host_channel ) { icon = constants.EYE; tab.title = "Hosted Channel"; } else if ( group ) tab.title = "Group Chat"; else tab.title = "Pinned Channel"; tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; tab.addEventListener('click', function() { var controller = view.get('controller'); controller.focusRoom(room); controller.set('showList', false); }); room._ffz_tab = tab; return tab; }, ffzDisableTabs: function() { if ( this._ffz_tabs ) { this._ffz_tabs.parentElement.removeChild(this._ffz_tabs); delete this._ffz_tabs; delete this._ffz_invite; } if ( this._ffz_host ) { 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(); } delete this._ffz_host; delete this._ffz_host_room; } // Show the old chat UI. this.$('.chat-room').css('top', ''); this.$(".chat-header").removeClass("hidden"); }, }); } // ---------------------- // Chat Room Connections // ---------------------- FFZ.prototype.connect_extra_chat = function() { var user = this.get_user(); if ( user && user.login ) { // Make sure we're in the user's room. if ( ! this.rooms[user.login] || this.rooms[user.login].room ) { var Room = App.__container__.resolve('model:room'), r = Room && Room.findOne(user.login); } } if ( this.has_bttv ) return; for(var i=0; i < this.settings.pinned_rooms.length; i++) this._join_room(this.settings.pinned_rooms[i], true); if ( ! this._chatv ) return; if ( ! this.has_bttv && this.settings.group_tabs ) this._chatv.ffzRebuildTabs(); this._chatv.ffzRebuildMenu(); } FFZ.prototype._join_room = function(room_id, no_rebuild) { var did_join = false; if ( this.settings.pinned_rooms.indexOf(room_id) === -1 ) { this.settings.pinned_rooms.push(room_id); this.settings.set("pinned_rooms", this.settings.pinned_rooms); did_join = true; } // Make sure we're not already there. if ( this.rooms[room_id] && this.rooms[room_id].room ) { if ( did_join && ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) this._chatv.ffzRebuildTabs(); return did_join; } // Okay, fine. Get it. var Room = App.__container__.resolve('model:room'), r = Room && Room.findOne(room_id); // Finally, rebuild the chat UI. if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) this._chatv.ffzRebuildTabs(); if ( ! no_rebuild && this._chatv ) this._chatv.ffzRebuildMenu(); return did_join; } FFZ.prototype._leave_room = function(room_id, no_rebuild) { var did_leave = false; if ( this.settings.pinned_rooms.indexOf(room_id) !== -1 ) { this.settings.pinned_rooms.removeObject(room_id); this.settings.set("pinned_rooms", this.settings.pinned_rooms); did_leave = true; } if ( ! this.rooms[room_id] || ! this.rooms[room_id].room ) return did_leave; var Chat = App.__container__.lookup('controller:chat'), r = this.rooms[room_id].room, user = this.get_user(); if ( ! Chat || Chat.get('currentChannelRoom.id') === room_id || (this._chatv && this._chatv._ffz_host === room_id) ) return did_leave; if ( Chat.get('currentRoom.id') === room_id ) Chat.blurRoom(); // Don't leave the user's room, but update the UI. if ( ! user || user.login !== room_id ) r.destroy(); if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) this._chatv.ffzRebuildTabs(); if ( ! no_rebuild && this._chatv ) this._chatv.ffzRebuildMenu(); return did_leave; } // ---------------------- // Commands // ---------------------- FFZ.chat_commands.join = function(room, args) { if ( ! args || ! args.length || args.length > 1 ) return "Join Usage: /join "; var room_id = args[0].toLowerCase(); if ( room_id.charAt(0) === "#" ) room_id = room_id.substr(1); if ( this._join_room(room_id) ) return "Joining " + room_id + ". You will always connect to this channel's chat unless you later /part from it."; else return "You have already joined " + room_id + ". Please use \"/part " + room_id + "\" to leave it."; } FFZ.chat_commands.part = function(room, args) { if ( ! args || ! args.length || args.length > 1 ) return "Part Usage: /part "; var room_id = args[0].toLowerCase(); if ( room_id.charAt(0) === "#" ) room_id = room_id.substr(1); if ( this._leave_room(room_id) ) return "Leaving " + room_id + "."; else if ( this.rooms[room_id] ) return "You do not have " + room_id + " pinned and you cannot leave the current channel or hosted channels via /part."; else return "You are not in " + room_id + "."; } },{"../constants":5,"../utils":35}],10:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; // -------------------- // Settings // -------------------- FFZ.settings_info.swap_sidebars = { type: "boolean", value: false, category: "Appearance", no_mobile: true, no_bttv: true, name: "Swap Sidebar Positions", help: "Swap the positions of the left and right sidebars, placing chat on the left.", on_update: function(val) { if ( this.has_bttv ) return; document.body.classList.toggle("ffz-sidebar-swap", val); this._fix_menu_position(); } }; FFZ.settings_info.right_column_width = { type: "button", value: 340, category: "Appearance", no_mobile: true, no_bttv: true, name: "Right Sidebar Width", help: "Set the width of the right sidebar for chat.", method: function() { var old_val = this.settings.right_column_width || 340, new_val = prompt("Right Sidebar Width\n\nPlease enter a new width for the right sidebar, in pixels. Minimum: 250, Default: 340", old_val); if ( new_val === null || new_val === undefined ) return; var width = parseInt(new_val); if ( ! width || width === NaN ) width = 340; this.settings.set('right_column_width', Math.max(250, width)); }, on_update: function(val) { if ( this.has_bttv ) return; var Layout = App.__container__.lookup('controller:layout'); if ( ! Layout ) return; Layout.set('rightColumnWidth', val); Ember.propertyDidChange(Layout, 'contentWidth'); } }; // -------------------- // Initialization // -------------------- FFZ.prototype.setup_layout = function() { if ( this.has_bttv ) return; document.body.classList.toggle("ffz-sidebar-swap", this.settings.swap_sidebars); this.log("Creating layout style element."); var s = this._layout_style = document.createElement('style'); s.id = 'ffz-layout-css'; document.head.appendChild(s); this.log("Hooking the Ember Layout controller."); var Layout = App.__container__.lookup('controller:layout'), f = this; if ( ! Layout ) return; Layout.reopen({ rightColumnWidth: 340, isTooSmallForRightColumn: function() { return this.get("windowWidth") < (1090 - this.get('rightColumnWidth')) }.property("windowWidth", "rightColumnWidth"), contentWidth: function() { var left_width = this.get("isLeftColumnClosed") ? 50 : 240, right_width = this.get("isRightColumnClosed") ? 0 : this.get("rightColumnWidth"); return this.get("windowWidth") - left_width - right_width - 60; }.property("windowWidth", "isRightColumnClosed", "isLeftColumnClosed", "rightColumnWidth"), /*ffzUpdateWidth: _.throttle(function() { var rc = document.querySelector('#right_close'); if ( ! rc ) return; var left_width = this.get("isLeftColumnClosed") ? 50 : 240, right_width; if ( f.settings.swap_sidebars ) right_width = rc.offsetLeft; // + this.get('rightColumnWidth') - 5; else right_width = document.body.offsetWidth - rc.offsetLeft - left_width - 25; if ( right_width < 250 ) { // Close it! } this.set('rightColumnWidth', right_width); Ember.propertyDidChange(Layout, 'contentWidth'); }, 200),*/ ffzUpdateCss: function() { var width = this.get('rightColumnWidth'); f._layout_style.innerHTML = '#main_col.expandRight #right_close { left: none !important; } #right_col { width: ' + width + 'px; } body:not(.ffz-sidebar-swap) #main_col:not(.expandRight) { margin-right: ' + width + 'px; } body.ffz-sidebar-swap #main_col:not(.expandRight) { margin-left: ' + width + 'px; }'; }.observes("rightColumnWidth"), ffzFixTabs: function() { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { setTimeout(function() { f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); },0); } }.observes("isRightColumnClosed", "rightColumnWidth") }); /* // Try modifying the closer. var rc = jQuery("#right_close"); if ( ! rc || ! rc.length ) return; rc.draggable({ axis: "x", drag: Layout.ffzUpdateWidth.bind(Layout), stop: Layout.ffzUpdateWidth.bind(Layout) });*/ // Force the layout to update. Layout.set('rightColumnWidth', this.settings.right_column_width); Ember.propertyDidChange(Layout, 'contentWidth'); } },{}],11:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), constants = require("../constants"), 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 + "*"); // --------------------- // Settings // --------------------- FFZ.settings_info.room_status = { type: "boolean", value: true, category: "Chat Appearance", 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.line_purge_icon = { type: "boolean", value: false, no_bttv: true, category: "Chat Moderation", name: "Purge Icon in Mod Icons", help: "Display a Purge Icon in chat line Mod Icons for quickly purging users.", on_update: function(val) { if ( this.has_bttv ) return; document.body.classList.toggle("ffz-chat-purge-icon", val); } }; FFZ.settings_info.replace_bad_emotes = { type: "boolean", value: true, category: "Chat Appearance", no_bttv: true, name: "Fix Low Quality Twitch Global Emoticons", help: "Replace emoticons such as DansGame and RedCoat with cleaned up versions that don't have pixels around the edges or white backgrounds for nicer display on dark chat." }; FFZ.settings_info.parse_emoji = { type: "boolean", value: true, category: "Chat Appearance", 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 Appearance", 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 Appearance", 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.hosted_sub_notices = { type: "boolean", value: true, category: "Chat Filtering", no_bttv: true, name: "Show Hosted Channel Subscriber Notices", help: "Display notices in chat when someone subscribes to the hosted channel." }; FFZ.settings_info.banned_words = { type: "button", value: [], category: "Chat Filtering", no_bttv: true, //visible: function() { return ! this.has_bttv }, name: "Banned Words", help: "Set a list of words that will be locally removed from chat messages.", method: function() { var old_val = this.settings.banned_words.join(", "), new_val = prompt("Banned Words\n\nPlease enter a comma-separated list of words that you would like to be removed from chat messages.", old_val); if ( new_val === null || new_val === undefined ) return; new_val = new_val.trim().split(SPLITTER); var vals = []; for(var i=0; i < new_val.length; i++) new_val[i] && vals.push(new_val[i]); if ( vals.length == 1 && vals[0] == "disable" ) vals = []; this.settings.set("banned_words", vals); } }; FFZ.settings_info.keywords = { type: "button", value: [], category: "Chat Filtering", no_bttv: true, //visible: function() { return ! this.has_bttv }, name: "Highlight Keywords", help: "Set additional keywords that will be highlighted in chat.", method: function() { var old_val = this.settings.keywords.join(", "), new_val = prompt("Highlight Keywords\n\nPlease enter a comma-separated list of words that you would like to be highlighted in chat.", old_val); if ( new_val === null || new_val === undefined ) return; // Split them up. new_val = new_val.trim().split(SPLITTER); var vals = []; for(var i=0; i < new_val.length; i++) new_val[i] && vals.push(new_val[i]); if ( vals.length == 1 && vals[0] == "disable" ) vals = []; this.settings.set("keywords", vals); } }; FFZ.settings_info.clickable_emoticons = { type: "boolean", value: false, category: "Chat Tooltips", no_bttv: true, no_mobile: true, name: "Emoticon Information Pages", help: "When enabled, holding shift and clicking on an emoticon will open it on the FrankerFaceZ website or Twitch Emotes." }; FFZ.settings_info.link_info = { type: "boolean", value: true, category: "Chat Tooltips", no_bttv: true, name: "Link Information Beta", help: "Check links against known bad websites, unshorten URLs, and show YouTube info." }; FFZ.settings_info.link_image_hover = { type: "boolean", value: false, category: "Chat Tooltips", no_bttv: true, no_mobile: true, name: "Image Preview", help: "Display image thumbnails for links to Imgur and YouTube." }; FFZ.settings_info.image_hover_all_domains = { type: "boolean", value: false, category: "Chat Tooltips", no_bttv: true, no_mobile: true, name: "Image Preview - All Domains", help: "Requires Image Preview. Attempt to show an image preview for any URL ending in the appropriate extension. Warning: This may be used to leak your IP address to malicious users." }; FFZ.settings_info.legacy_badges = { type: "boolean", value: false, category: "Chat Appearance", name: "Legacy Badges", help: "Display the old, pre-vector chat badges from Twitch.", on_update: function(val) { document.body.classList.toggle("ffz-legacy-badges", val); } }; FFZ.settings_info.chat_rows = { type: "boolean", value: false, category: "Chat Appearance", no_bttv: true, name: "Chat Line Backgrounds", help: "Display alternating background colors for lines in chat.", on_update: function(val) { document.body.classList.toggle("ffz-chat-background", !this.has_bttv && val); } }; FFZ.settings_info.chat_separators = { type: "select", options: { 0: "Disabled", 1: "Basic Line (1px solid)", 2: "3D Line (2px groove)" }, value: '0', category: "Chat Appearance", no_bttv: true, process_value: function(val) { if ( val === false ) return '0'; else if ( val === true ) return '1'; return val; }, name: "Chat Line Separators", help: "Display thin lines between chat messages for further visual separation.", on_update: function(val) { document.body.classList.toggle("ffz-chat-separator", !this.has_bttv && val !== '0'); document.body.classList.toggle("ffz-chat-separator-3d", !this.has_bttv && val === '2'); } }; FFZ.settings_info.chat_padding = { type: "boolean", value: false, category: "Chat Appearance", no_bttv: true, name: "Reduced Chat Line Padding", help: "Reduce the amount of padding around chat messages to fit more on-screen at once.", on_update: function(val) { document.body.classList.toggle("ffz-chat-padding", !this.has_bttv && val); } }; FFZ.settings_info.high_contrast_chat = { type: "select", options: { '222': "Disabled", '212': "Bold", '221': "Text", '211': "Text + Bold", '122': "Background", '121': "Background + Text", '112': "Background + Bold", '111': 'All' }, value: '222', category: "Chat Appearance", no_bttv: true, name: "High Contrast", help: "Display chat using white and black for maximum contrast. This is suitable for capturing and chroma keying chat to display on stream.", process_value: function(val) { if ( val === false ) return '222'; else if ( val === true ) return '111'; return val; }, on_update: function(val) { document.body.classList.toggle("ffz-high-contrast-chat-text", !this.has_bttv && val[2] === '1'); document.body.classList.toggle("ffz-high-contrast-chat-bold", !this.has_bttv && val[1] === '1'); document.body.classList.toggle("ffz-high-contrast-chat-bg", !this.has_bttv && val[0] === '1'); } }; FFZ.settings_info.chat_font_size = { type: "button", value: 12, category: "Chat Appearance", no_bttv: true, name: "Font Size", help: "Make the chat font bigger or smaller.", method: function() { var old_val = this.settings.chat_font_size, new_val = prompt("Chat Font Size\n\nPlease enter a new size for the chat font. The default is 12.", old_val); if ( new_val === null || new_val === undefined ) return; var parsed = parseInt(new_val); if ( ! parsed || parsed === NaN || parsed < 1 ) parsed = 12; this.settings.set("chat_font_size", parsed); }, on_update: function(val) { if ( this.has_bttv || ! this._chat_style ) return; var css; if ( val === 12 || ! val ) css = ""; else { var lh = Math.max(20, Math.round((20/12)*val)), pd = Math.floor((lh - 20) / 2); css = ".ember-chat .chat-messages .chat-line { font-size: " + val + "px !important; line-height: " + lh + "px !important; }"; if ( pd ) css += ".ember-chat .chat-messages .chat-line .mod-icons, .ember-chat .chat-messages .chat-line .badges { padding-top: " + pd + "px; }"; } utils.update_css(this._chat_style, "chat_font_size", css); FFZ.settings_info.chat_ts_size.on_update.bind(this)(this.settings.chat_ts_size); } }; FFZ.settings_info.chat_ts_size = { type: "button", value: null, category: "Chat Appearance", no_bttv: true, name: "Timestamp Font Size", help: "Make the chat timestamp font bigger or smaller.", method: function() { var old_val = this.settings.chat_ts_size; if ( ! old_val ) old_val = this.settings.chat_font_size; var new_val = prompt("Chat Timestamp Font Size\n\nPlease enter a new size for the chat timestamp font. The default is to match the regular chat font size.", old_val); if ( new_val === null || new_val === undefined ) return; var parsed = parseInt(new_val); if ( ! parsed || parsed === NaN || parsed < 1 ) parsed = null; this.settings.set("chat_ts_size", parsed); }, on_update: function(val) { if ( this.has_bttv || ! this._chat_style ) return; var css; if ( val === null ) css = ""; else { var lh = Math.max(20, Math.round((20/12)*val), Math.round((20/12)*this.settings.chat_font_size)); css = ".ember-chat .chat-messages .timestamp { font-size: " + val + "px !important; line-height: " + lh + "px !important; }"; } utils.update_css(this._chat_style, "chat_ts_font_size", css); } }; // --------------------- // Initialization // --------------------- FFZ.prototype.setup_line = function() { // Tipsy Handler jQuery(document.body).on("mouseleave", ".tipsy", function() { this.parentElement.removeChild(this); }); // Aliases try { this.aliases = JSON.parse(localStorage.ffz_aliases || '{}'); } catch(err) { this.log("Error Loading Aliases: " + err); this.aliases = {}; } // Chat Style var s = this._chat_style = document.createElement('style'); s.id = "ffz-style-chat"; s.type = 'text/css'; document.head.appendChild(s); // Initial calculation. FFZ.settings_info.chat_font_size.on_update.bind(this)(this.settings.chat_font_size); // Chat Enhancements document.body.classList.toggle("ffz-chat-colors", !this.has_bttv && this.settings.fix_color !== '-1'); document.body.classList.toggle("ffz-chat-colors-gray", !this.has_bttv && this.settings.fix_color === '-1'); document.body.classList.toggle("ffz-legacy-badges", this.settings.legacy_badges); document.body.classList.toggle('ffz-chat-background', !this.has_bttv && this.settings.chat_rows); document.body.classList.toggle("ffz-chat-separator", !this.has_bttv && this.settings.chat_separators !== '0'); document.body.classList.toggle("ffz-chat-separator-3d", !this.has_bttv && this.settings.chat_separators === '2'); document.body.classList.toggle("ffz-chat-padding", !this.has_bttv && this.settings.chat_padding); document.body.classList.toggle("ffz-chat-purge-icon", !this.has_bttv && this.settings.line_purge_icon); document.body.classList.toggle("ffz-high-contrast-chat-text", !this.has_bttv && this.settings.high_contrast_chat[2] === '1'); document.body.classList.toggle("ffz-high-contrast-chat-bold", !this.has_bttv && this.settings.high_contrast_chat[1] === '1'); document.body.classList.toggle("ffz-high-contrast-chat-bg", !this.has_bttv && this.settings.high_contrast_chat[0] === '1'); this._last_row = {}; this.log("Hooking the Ember Whisper Line component."); var Whisper = App.__container__.resolve('component:whisper-line'); if ( Whisper ) this._modify_line(Whisper); this.log("Hooking the Ember Message Line component."); var Line = App.__container__.resolve('component:message-line'); if ( Line ) this._modify_line(Line); // Store the capitalization of our own name. var user = this.get_user(); if ( user && user.name ) FFZ.capitalization[user.login] = [user.name, Date.now()]; } FFZ.prototype.save_aliases = function() { this.log("Saving " + Object.keys(this.aliases).length + " aliases to local storage."); localStorage.ffz_aliases = JSON.stringify(this.aliases); } FFZ.prototype._modify_line = function(component) { var f = this, Layout = App.__container__.lookup('controller:layout'), Settings = App.__container__.lookup('controller:settings'); component.reopen({ tokenizedMessage: function() { // Add our own step to the tokenization procedure. var tokens = this.get("msgObject.cachedTokens"); if ( tokens ) return tokens; tokens = this._super(); var start = performance.now(), user = f.get_user(), from_me = user && this.get("msgObject.from") === user.login; 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 ) FFZ.capitalization[this.get("msgObject.from")] = [display.trim(), Date.now()]; if ( ! from_me ) tokens = f.tokenize_mentions(tokens); for(var i = 0; i < tokens.length; i++) { var token = tokens[i]; if ( ! _.isString(token) && token.mentionedUser && ! token.own ) { this.set('msgObject.ffz_has_mention', true); break; } } var end = performance.now(); if ( end - start > 5 ) f.log("Tokenizing Message Took Too Long - " + (end-start) + "ms", tokens, false, true); this.set("msgObject.cachedTokens", tokens); return tokens; }.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"), ffzUpdated: Ember.observer("msgObject.ffz_deleted", "msgObject.ffz_old_messages", function() { this.rerender(); }), click: function(e) { if ( e.target && e.target.classList.contains('ffz-old-messages') ) return f._show_deleted(this.get('msgObject.room')); if ( e.target && e.target.classList.contains('deleted-link') ) return f._deleted_link_click.bind(e.target)(e); if ( e.target && e.target.classList.contains('mod-icon') ) { jQuery(e.target).trigger('mouseout'); if ( e.target.classList.contains('purge') ) { var i = this.get('msgObject.from'), room_id = this.get('msgObject.room'), room = room_id && f.rooms[room_id] && f.rooms[room_id].room; if ( room ) { room.send("/timeout " + i + " 1"); room.clearMessages(i); } return; } } if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons && e.target && e.target.classList.contains('emoticon') ) { var eid = e.target.getAttribute('data-emote'); if ( eid ) window.open("https://twitchemotes.com/emote/" + eid); else { eid = e.target.getAttribute("data-ffz-emote"); window.open("https://www.frankerfacez.com/emoticons/" + eid); } } return this._super(e); }, ffzUserLevel: function() { if ( this.get('isStaff') ) return 5; else if ( this.get('isAdmin') ) return 4; else if ( this.get('isBroadcaster') ) return 3; else if ( this.get('isGlobalModerator') ) return 2; else if ( this.get('isModerator') ) return 1; return 0; }.property('msgObject.labels.[]'), render: function(e) { var deleted = this.get('msgObject.deleted'), r = this, badges = {}, user = this.get('msgObject.from'), room_id = this.get('msgObject.room'), room = f.rooms && f.rooms[room_id], recipient = this.get('msgObject.to'), is_whisper = recipient && recipient.length, this_ul = this.get('ffzUserLevel'), other_ul = room && room.room && room.room.get('ffzUserLevel') || 0, row_type = this.get('msgObject.ffz_alternate'), raw_color = this.get('msgObject.color'), colors = raw_color && f._handle_color(raw_color), is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); if ( row_type === undefined ) { row_type = f._last_row[room_id] = f._last_row.hasOwnProperty(room_id) ? !f._last_row[room_id] : false; this.set("msgObject.ffz_alternate", row_type); } e.push('
'); e.push('' + this.get("timestamp") + ' '); if ( ! is_whisper && this_ul < other_ul ) { e.push(''); if ( deleted ) e.push('Unban'); else e.push('Ban'); e.push('Timeout'); e.push('Purge'); e.push(''); } // Stock Badges if ( ! is_whisper && this.get('isBroadcaster') ) badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; else if ( this.get('isStaff') ) badges[0] = {klass: 'staff', title: 'Staff'}; else if ( this.get('isAdmin') ) badges[0] = {klass: 'admin', title: 'Admin'}; else if ( this.get('isGlobalMod') ) badges[0] = {klass: 'global-moderator', title: 'Global Moderator'}; else if ( ! is_whisper && this.get('isModerator') ) badges[0] = {klass: 'moderator', title: 'Moderator'}; if ( ! is_whisper && this.get('isSubscriber') ) badges[10] = {klass: 'subscriber', title: 'Subscriber'}; if ( this.get('hasTurbo') ) badges[15] = {klass: 'turbo', title: 'Turbo'}; // FFZ Badges badges = f.render_badges(this, badges); // Rendering! e.push(''); for(var key in badges) { var badge = badges[key], css = badge.image ? 'background-image:url("' + badge.image + '");' : ''; if ( badge.color ) css += 'background-color:' + badge.color + ';'; if ( badge.extra_css ) css += badge.extra_css; e.push('
'); } e.push('
'); var alias = f.aliases[user], name = this.get('msgObject.tags.display-name') || (user && user.capitalize()) || "unknown user", style = colors && 'color:' + (is_dark ? colors[1] : colors[0]), colored = style ? ' has-color' : ''; if ( alias ) e.push('' + utils.sanitize(alias) + ''); else e.push('' + utils.sanitize(name) + ''); if ( is_whisper ) { var to_alias = f.aliases[recipient], to_name = this.get('msgObject.tags.recipient-display-name') || (recipient && recipient.capitalize()) || "unknown user", to_color = this.get('msgObject.toColor'), to_colors = to_color && f._handle_color(to_color), to_style = to_color && 'color:' + (is_dark ? to_colors[1] : to_colors[0]), to_colored = to_style ? ' has-color' : ''; this._renderWhisperArrow(e); if ( to_alias ) e.push('' + utils.sanitize(to_alias) + ''); else e.push('' + utils.sanitize(to_name) + ''); } e.push(': '); if ( this.get('msgObject.style') !== 'action' ) { style = ''; colored = ''; } if ( deleted ) e.push('<message deleted>'); else { e.push(''); e.push(f.render_tokens(this.get('tokenizedMessage'), true)); var old_messages = this.get('msgObject.ffz_old_messages'); if ( old_messages && old_messages.length ) e.push('
Show ' + utils.number_commas(old_messages.length) + ' Old
'); e.push('
'); } }, classNameBindings: [ 'msgObject.ffz_alternate:ffz-alternate', 'msgObject.ffz_has_mention:ffz-mentioned', 'ffzWasDeleted:ffz-deleted', 'ffzHasOldMessages:clearfix', 'ffzHasOldMessages:ffz-has-deleted' ], ffzWasDeleted: function() { return f.settings.prevent_clear && this.get('msgObject.ffz_deleted'); }.property('msgObject.ffz_deleted'), ffzHasOldMessages: function() { var old_messages = this.get('msgObject.ffz_old_messages'); return old_messages && old_messages.length; }.property('msgObject.ffz_old_messages'), didInsertElement: function() { this._super(); var el = this.get('element'); el.setAttribute('data-room', this.get('msgObject.room')); el.setAttribute('data-sender', this.get('msgObject.from')); el.setAttribute('data-deleted', this.get('msgObject.deleted') || false); } }); } // --------------------- // Capitalization // --------------------- FFZ.capitalization = {}; FFZ._cap_fetching = 0; FFZ.get_capitalization = function(name, callback) { if ( ! name ) return name; name = name.toLowerCase(); if ( name == "jtv" || name == "twitchnotify" ) return name; var old_data = FFZ.capitalization[name]; if ( old_data ) { if ( Date.now() - old_data[1] < 3600000 ) return old_data[0]; } if ( FFZ._cap_fetching < 25 ) { FFZ._cap_fetching++; FFZ.get().ws_send("get_display_name", name, function(success, data) { var cap_name = success ? data : name; FFZ.capitalization[name] = [cap_name, Date.now()]; FFZ._cap_fetching--; typeof callback === "function" && callback(cap_name); }); } return old_data ? old_data[0] : name; } // --------------------- // Banned Words // --------------------- FFZ.prototype._remove_banned = function(tokens) { var banned_words = this.settings.banned_words, banned_links = ['j.mp', 'bit.ly'], has_banned_words = banned_words && banned_words.length; if ( !has_banned_words && (! banned_links || ! banned_links.length) ) return tokens; if ( typeof tokens == "string" ) tokens = [tokens]; var regex = FFZ._words_to_regex(banned_words), link_regex = FFZ._words_to_regex(banned_links), new_tokens = []; for(var i=0; i < tokens.length; i++) { var token = tokens[i]; if ( ! _.isString(token ) ) { if ( token.emoticonSrc && has_banned_words && regex.test(token.altText) ) new_tokens.push(token.altText.replace(regex, "$1***")); else if ( token.isLink && has_banned_words && regex.test(token.href) ) new_tokens.push({ isLink: true, href: token.href, isDeleted: true, isLong: false, censoredHref: token.href.replace(regex, "$1***") }); else if ( token.isLink && link_regex.test(token.href) ) new_tokens.push({ isLink: true, href: token.href, isDeleted: true, isLong: false, censoredHref: token.href.replace(link_regex, "$1***") }); else new_tokens.push(token); } else if ( has_banned_words ) new_tokens.push(token.replace(regex, "$1***")); else new_tokens.push(token); } return new_tokens; } // --------------------- // Emoticon Replacement // --------------------- FFZ.prototype._emoticonize = function(component, tokens) { var room_id = component.get("msgObject.room"), user_id = component.get("msgObject.from"); return this.tokenize_emotes(user_id, room_id, tokens); } },{"../constants":5,"../utils":35}],12:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("../utils"), constants = require("../constants"), helpers, keycodes = { ESC: 27, P: 80, B: 66, T: 84, U: 85 }, MESSAGE = '', 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.basic_settings.enhanced_moderation_cards = { type: "boolean", no_bttv: true, category: "Chat", name: "Enhanced Moderation Cards", help: "Improve moderation cards with hotkeys, additional buttons, chat history, and other information to make moderating easier.", get: function() { return this.settings.mod_card_hotkeys && this.settings.mod_card_info && this.settings.mod_card_history; }, set: function(val) { this.settings.set('mod_card_hotkeys', val); this.settings.set('mod_card_info', val); this.settings.set('mod_card_history', val); } }; FFZ.basic_settings.chat_hover_pause = { type: "boolean", no_bttv: true, category: "Chat", 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 misclicks.", get: 'chat_hover_pause', set: 'chat_hover_pause' }; FFZ.settings_info.chat_hover_pause = { type: "boolean", value: false, no_bttv: true, 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 misclicks.", 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_info = { type: "boolean", value: true, no_bttv: true, category: "Chat Moderation", name: "Moderation Card Additional Information", help: "Display a channel's follower count, view count, and account age on moderation cards." }; 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_buttons = { type: "button", value: [], category: "Chat Moderation", no_bttv: true, name: "Moderation Card Additional Buttons", help: "Add additional buttons to moderation cards for running chat commands on those users.", method: function() { var old_val = ""; for(var i=0; i < this.settings.mod_card_buttons.length; i++) { var cmd = this.settings.mod_card_buttons[i]; if ( cmd.indexOf(' ') !== -1 ) old_val += ' "' + cmd + '"'; else old_val += ' ' + cmd; } var new_val = prompt("Moderation Card Additional Buttons\n\nPlease enter a list of additional commands to display buttons for on moderation cards. Commands are separated by spaces. To include spaces in a command, surround the command with double quotes (\"). Use \"{user}\" to insert the user's username into the command, otherwise it will be appended to the end.\n\nExample: !permit \"!reg add {user}\"", old_val); if ( new_val === null || new_val === undefined ) return; var vals = []; new_val = new_val.trim(); while(new_val) { if ( new_val.charAt(0) === '"' ) { var end = new_val.indexOf('"', 1); if ( end === -1 ) end = new_val.length; var segment = new_val.substr(1, end - 1); if ( segment ) vals.push(segment); new_val = new_val.substr(end + 1); } else { var ind = new_val.indexOf(' '); if ( ind === -1 ) { if ( new_val ) vals.push(new_val); new_val = ''; } else { var segment = new_val.substr(0, ind); if ( segment ) vals.push(segment); new_val = new_val.substr(ind + 1); } } } this.settings.set("mod_card_buttons", vals); } }; 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); } }; // ---------------- // Initialization // ---------------- 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"), ffzRebuildInfo: function() { var el = this.get('element'), info = el && el.querySelector('.info'); if ( ! info ) return; var out = '' + constants.EYE + ' ' + utils.number_commas(this.get('cardInfo.user.views') || 0) + '', since = utils.parse_date(this.get('cardInfo.user.created_at') || ''), followers = this.get('cardInfo.user.ffz_followers'); if ( typeof followers === "number" ) { out += '' + constants.HEART + ' ' + utils.number_commas(followers || 0) + ''; } else if ( followers === undefined ) { var t = this; this.set('cardInfo.user.ffz_followers', false); Twitch.api.get("channels/" + this.get('cardInfo.user.id') + '/follows', {limit:1}).done(function(data) { t.set('cardInfo.user.ffz_followers', data._total); t.ffzRebuildInfo(); }).fail(function(data) { t.set('cardInfo.user.ffz_followers', undefined); }); } if ( since ) { var age = Math.floor((Date.now() - since.getTime()) / 1000); if ( age > 0 ) { out += '' + constants.CLOCK + ' ' + utils.human_time(age, 10) + ''; } } info.innerHTML = out; }.observes("cardInfo.user.views"), userName: Ember.computed("cardInfo.user.id", "cardInfo.user.display_name", function() { var user_id = this.get("cardInfo.user.id"), alias = f.aliases[user_id]; return alias || this.get("cardInfo.user.display_name") || user_id.capitalize(); }), didInsertElement: function() { this._super(); window._card = this; try { if ( f.has_bttv ) return; var el = this.get('element'), controller = this.get('controller'), line, user_id = controller.get('cardInfo.user.id'), alias = f.aliases[user_id]; // Alias Display if ( alias ) { var name = el.querySelector('h3.name'), link = name && name.querySelector('a'); if ( link ) name = link; if ( name ) { name.classList.add('ffz-alias'); name.title = utils.sanitize(controller.get('cardInfo.user.display_name') || user_id.capitalize()); jQuery(name).tipsy(); } } // Style it! el.classList.add('ffz-moderation-card'); // Info-tize it! if ( f.settings.mod_card_info ) { var info = document.createElement('div'), after = el.querySelector('h3.name'); if ( after ) { el.classList.add('ffz-has-info'); info.className = 'info channel-stats'; after.parentElement.insertBefore(info, after.nextSibling); this.ffzRebuildInfo(); } } // Additional Buttons if ( f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) { line = document.createElement('div'); line.className = 'extra-interface interface clearfix'; var cmds = {}, add_btn_click = function(cmd) { var user_id = controller.get('cardInfo.user.id'), cont = App.__container__.lookup('controller:chat'), room = cont && cont.get('currentRoom'); room && room.send(cmd.replace(/{user}/g, user_id)); }, add_btn_make = function(cmd) { var btn = document.createElement('button'), segment = cmd.split(' ', 1)[0], title = cmds[segment] > 1 ? cmd.split(' ', cmds[segment]) : [segment]; if ( /^[!~./]/.test(title[0]) ) title[0] = title[0].substr(1); title = _.map(title, function(s){ return s.capitalize() }).join(' '); btn.className = 'button'; btn.innerHTML = utils.sanitize(title); btn.title = utils.sanitize(cmd.replace(/{user}/g, controller.get('cardInfo.user.id') || '{user}')); jQuery(btn).tipsy(); btn.addEventListener('click', add_btn_click.bind(this, cmd)); return btn; }; var cmds = {}; for(var i=0; i < f.settings.mod_card_buttons.length; i++) cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] = (cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] || 0) + 1; for(var i=0; i < f.settings.mod_card_buttons.length; i++) { var cmd = f.settings.mod_card_buttons[i], ind = cmd.indexOf('{user}'); if ( ind === -1 ) cmd += ' {user}'; line.appendChild(add_btn_make(cmd)) } el.appendChild(line); } // Key Handling el.setAttribute('tabindex', 1); if ( f.settings.mod_card_hotkeys ) { el.classList.add('no-mousetrap'); el.addEventListener('keyup', function(e) { var key = e.keyCode || e.which, user_id = controller.get('cardInfo.user.id'), is_mod = controller.get('cardInfo.isModeratorOrHigher'), room = App.__container__.lookup('controller:chat').get('currentRoom'); if ( is_mod && key == keycodes.P ) room.send("/timeout " + user_id + " 1"); else if ( is_mod && key == keycodes.B ) room.send("/ban " + user_id); else if ( is_mod && key == keycodes.T ) room.send("/timeout " + user_id + " 600"); else if ( is_mod && key == keycodes.U ) room.send("/unban " + user_id); else if ( key != keycodes.ESC ) return; controller.send('close'); }); } // Only do the big stuff if we're mod. if ( controller.get('cardInfo.isModeratorOrHigher') ) { el.classList.add('ffz-is-mod'); // Key Handling if ( f.settings.mod_card_hotkeys ) { el.classList.add('no-mousetrap'); 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 ( 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('close'); }); } var btn_click = function(timeout) { var 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); }, 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 line = document.createElement('div'); line.className = 'extra-interface 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 = (f.settings.mod_card_hotkeys ? "(U)" : "U") + "nban User"; jQuery(unban_btn).tipsy(); unban_btn.addEventListener("click", btn_click.bind(this, -1)); jQuery(ban_btn).after(unban_btn); } // More Fixing Other Buttons var op_btn = el.querySelector('button.mod'); if ( op_btn ) { var is_owner = controller.get('cardInfo.isChannelOwner'), user = ffz.get_user(); can_op = is_owner || (user && user.is_admin) || (user && user.is_staff); if ( ! can_op ) op_btn.parentElement.removeChild(op_btn); } var msg_btn = el.querySelector(".interface > button.message-button"); if ( msg_btn ) { msg_btn.innerHTML = 'W'; msg_btn.classList.add('glyph-only'); msg_btn.classList.add('message'); msg_btn.title = "Whisper User"; jQuery(msg_btn).tipsy(); var real_msg = document.createElement('button'); real_msg.className = 'message-button button glyph-only message tooltip'; real_msg.innerHTML = MESSAGE; real_msg.title = "Message User"; real_msg.addEventListener('click', function() { window.open('http://www.twitch.tv/message/compose?to=' + controller.get('cardInfo.user.id')); }) msg_btn.parentElement.insertBefore(real_msg, msg_btn.nextSibling); } // Alias Button var alias_btn = document.createElement('button'); alias_btn.className = 'alias button glyph-only tooltip'; alias_btn.innerHTML = constants.EDIT; alias_btn.title = "Set Alias"; alias_btn.addEventListener('click', function() { var user = controller.get('cardInfo.user.id'), alias = f.aliases[user]; var new_val = prompt("Alias for User: " + user + "\n\nPlease enter an alias for the user. Leave it blank to remove the alias.", alias); if ( new_val === null || new_val === undefined ) return; new_val = new_val.trim(); if ( ! new_val ) new_val = undefined; f.aliases[user] = new_val; f.save_aliases(); // Update UI f._update_alias(user); Ember.propertyDidChange(controller, 'userName'); var name = el.querySelector('h3.name'), link = name && name.querySelector('a'); if ( link ) name = link; if ( name ) name.classList.toggle('ffz-alias', new_val); }); if ( msg_btn ) msg_btn.parentElement.insertBefore(alias_btn, msg_btn); else { var follow_btn = el.querySelector(".interface > .follow-button"); if ( follow_btn ) follow_btn.parentElement.insertBefore(alias_btn, follow_btn.nextSibling); } // 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 clearfix'; 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('.html-tooltip', l_el).tipsy({html:true}); history.appendChild(l_el); } el.appendChild(history); // Lazy scroll-to-bottom history.scrollTop = history.scrollHeight; } } // Reposition the menu if it's off-screen. var el_bound = el.getBoundingClientRect(), body_bound = document.body.getBoundingClientRect(); if ( el_bound.bottom > body_bound.bottom ) { var offset = el_bound.bottom - body_bound.bottom; if ( el_bound.top - offset > body_bound.top ) el.style.top = (el_bound.top - offset) + "px"; } // Focus the Element this.$().draggable({ start: function() { el.focus(); }}); el.focus(); } catch(err) { try { f.error("ModerationCardView didInsertElement: " + err); } catch(err) { } } }}); } // ---------------- // Aliases // ---------------- FFZ.prototype._update_alias = function(user) { var alias = this.aliases && this.aliases[user], cap_name = FFZ.get_capitalization(user), display_name = alias || cap_name, el = this._roomv && this._roomv.get('element'), lines = el && el.querySelectorAll('.chat-line[data-sender="' + user + '"]'); if ( ! lines ) return; for(var i=0, l = lines.length; i < l; i++) { var line = lines[i], el_from = line.querySelector('.from'); el_from.classList.toggle('ffz-alias', alias); el_from.textContent = display_name; el_from.title = alias ? cap_name : ''; } } // ---------------- // Chat Commands // ---------------- FFZ.chat_commands.purge = function(room, args) { if ( ! args || ! args.length ) return "Purge Usage: /p username [more usernames separated by spaces]"; if ( args.length > 10 ) return "Please only purge up to 10 users at once."; for(var i=0; i < args.length; i++) { var name = args[i]; if ( name ) room.room.send("/timeout " + name + " 1"); } } 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) { if ( ! args || ! args.length ) return "Timeout Usage: /t username [duration]"; room.room.send("/timeout " + args.join(" ")); } FFZ.chat_commands.t.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.b = function(room, args) { if ( ! args || ! args.length ) return "Ban Usage: /b username [more usernames separated by spaces]"; if ( args.length > 10 ) return "Please only ban up to 10 users at once."; for(var i=0; i < args.length; i++) { var name = args[i]; if ( name ) room.room.send("/ban " + name); } } FFZ.chat_commands.b.enabled = function() { return this.settings.short_commands; } FFZ.chat_commands.u = function(room, args) { if ( ! args || ! args.length ) return "Unban Usage: /u username [more usernames separated by spaces]"; if ( args.length > 10 ) return "Please only unban up to 10 users at once."; for(var i=0; i < args.length; i++) { var name = args[i]; if ( name ) room.room.send("/unban " + name); } } FFZ.chat_commands.u.enabled = function() { return this.settings.short_commands; } },{"../constants":5,"../utils":35}],13:[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*['"]([^'"]+)['"][^}]+(?:}|$)/, GROUP_CHAT = /^_([^_]+)_\d+$/, HOSTED_SUB = / subscribed to /, constants = require('../constants'), utils = require('../utils'), // StrimBagZ Support is_android = navigator.userAgent.indexOf('Android') !== -1, moderator_css = function(room) { if ( ! room.moderator_badge ) return ""; return '.chat-line[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement) { background-image:url("' + room.moderator_badge + '") !important; }'; } // -------------------- // Initialization // -------------------- FFZ.prototype.setup_room = function() { this.rooms = {}; this.log("Creating room style element."); var s = this._room_style = document.createElement("style"); s.id = "ffz-room-css"; document.head.appendChild(s); this.log("Hooking the Ember Room controller."); // Responsive ban button. var f = this, RC = App.__container__.lookup('controller:room'); if ( RC ) { var orig_ban = RC._actions.banUser, orig_to = RC._actions.timeoutUser; RC._actions.banUser = function(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); // Modify all current instances of Room, as the changes to the base // class won't be inherited automatically. var instances = Room.instances; for(var key in instances) { if ( ! instances.hasOwnProperty(key) ) continue; var inst = instances[key]; this.add_room(inst.id, inst); 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; // Fix scrolling. this._ffz_mouse_down = this.ffzMouseDown.bind(this); if ( is_android ) // We don't unbind scroll because that messes with the scrollbar. ;_; this._$chatMessagesScroller.bind('scroll', this._ffz_mouse_down); this._$chatMessagesScroller.unbind('mousedown'); this._$chatMessagesScroller.bind('mousedown', this._ffz_mouse_down); 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; 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'), delay_badge = cont.querySelector('#ffz-stat-delay'), 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}); } if ( ! delay_badge ) { delay_badge = document.createElement('span'); delay_badge.className = 'ffz room-state stat float-right'; delay_badge.id = 'ffz-stat-delay'; delay_badge.innerHTML = 'DELAY'; delay_badge.title = "300ms of artifical chat delay added."; cont.appendChild(delay_badge); jQuery(delay_badge).tipsy({gravity:"s", offset:15}); } r9k_badge.classList.toggle('hidden', !(room && room.get('r9k'))); sub_badge.classList.toggle('hidden', !(room && room.get('subsOnly'))); 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('slow')||120) + " seconds."; banned_badge.classList.toggle('hidden', !(room && room.get('ffz_banned'))); delay_badge.title = utils.number_commas(+f.settings.chat_delay||300) + "ms of artifical chat delay added."; delay_badge.classList.toggle('hidden', !+f.settings.chat_delay); 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('touchmove', 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(); }, ffzMouseDown: function(event) { var t = this._$chatMessagesScroller; if ( t && t[0] && ((!this.ffz_frozen && "mousedown" === event.type) || "mousewheel" === event.type || (is_android && "scroll" === event.type) ) ) { if ( event.type === "mousedown" ) f.log("Freezing from mouse down!", event); var r = t[0].scrollHeight - t[0].scrollTop - t[0].offsetHeight; this._setStuckToBottom(10 >= r); } }, ffzMouseOut: function(event) { this._ffz_outside = true; var e = this; setTimeout(function() { if ( e._ffz_outside ) e.ffzUnfreeze(); }, 25); }, 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; 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(){ if ( e.ffz_frozen || ! s || ! s.length ) return; s[0].scrollTop = s[0].scrollHeight; e._setStuckToBottom(true); }) }) }, 200), _setStuckToBottom: function(val) { this.set("stuckToBottom", val); this.get("controller.model") && this.set("controller.model.messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150)); if ( ! val ) this.ffzUnfreeze(); }, // 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'); } }); } // -------------------- // Command System // -------------------- FFZ.chat_commands = {}; FFZ.ffz_commands = {}; FFZ.prototype.room_message = function(room, text) { var lines = text.split("\n"); if ( this.has_bttv ) { for(var i=0; i < lines.length; i++) BetterTTV.chat.handlers.onPrivmsg(room.id, {style: 'admin', date: new Date(), from: 'jtv', message: lines[i]}); } else { for(var i=0; i < lines.length; i++) room.room.addMessage({style: 'ffz admin', date: new Date(), from: 'FFZ', message: lines[i]}); } } FFZ.prototype.run_command = function(text, room_id) { var room = this.rooms[room_id]; if ( ! room || ! room.room ) return false; if ( ! text ) return; var args = text.split(" "), cmd = args.shift().substr(1).toLowerCase(), command = FFZ.chat_commands[cmd], output; if ( ! command ) return false; if ( command.hasOwnProperty('enabled') ) { var val = command.enabled; if ( typeof val == "function" ) { try { val = command.enabled.bind(this)(room, args); } catch(err) { this.error('command "' + cmd + '" enabled: ' + err); val = false; } } if ( ! val ) return false; } this.log("Received Command: " + cmd, args, true); try { output = command.bind(this)(room, args); } catch(err) { this.error('command "' + cmd + '" runner: ' + err); output = "There was an error running the command."; } if ( output ) this.room_message(room, output); return true; } FFZ.prototype.run_ffz_command = function(text, room_id) { var room = this.rooms[room_id]; if ( ! room || !room.room ) return; if ( ! text ) { // Try to pop-up the menu. var link = document.querySelector('a.ffz-ui-toggle'); if ( link ) return link.click(); text = "help"; } var args = text.split(" "), cmd = args.shift().toLowerCase(); this.log("Received Command: " + cmd, args, true); var command = FFZ.ffz_commands[cmd], output; if ( command ) { try { output = command.bind(this)(room, args); } catch(err) { this.log("Error Running Command - " + cmd + ": " + err, room); output = "There was an error running the command."; } } else output = 'There is no "' + cmd + '" command.'; if ( output ) this.room_message(room, output); } FFZ.ffz_commands.help = function(room, args) { if ( args && args.length ) { var command = FFZ.ffz_commands[args[0].toLowerCase()]; if ( ! command ) return 'There is no "' + args[0] + '" command.'; else if ( ! command.help ) return 'No help is available for the command "' + args[0] + '".'; else return command.help; } var cmds = []; for(var c in FFZ.ffz_commands) FFZ.ffz_commands.hasOwnProperty(c) && cmds.push(c); return "The available commands are: " + cmds.join(", "); } FFZ.ffz_commands.help.help = "Usage: /ffz help [command]\nList available commands, or show help for a specific command."; // -------------------- // Room Management // -------------------- FFZ.prototype.add_room = function(id, room) { if ( this.rooms[id] ) return this.log("Tried to add existing room: " + id); this.log("Adding Room: " + id); // 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); // See if we need history? if ( ! this.has_bttv && this.settings.chat_history && room && (room.get('messages.length') || 0) < 10 ) { if ( ! this.ws_send("chat_history", [id,25], this._load_history.bind(this, id)) ) data.needs_history = true; } // 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); } FFZ.prototype.remove_room = function(id) { var room = this.rooms[id]; if ( ! room ) return; this.log("Removing Room: " + id); // Remove the CSS if ( room.css || room.moderator_badge ) utils.update_css(this._room_style, id, null); // Let the server know we're gone and delete our data for this room. this.ws_send("unsub", id); delete this.rooms[id]; // Clean up sets we aren't using any longer. if ( id.charAt(0) === "_" ) return; var set = this.emote_sets[room.set]; if ( set ) { set.users.removeObject(id); if ( ! this.global_sets.contains(room.set) && ! set.users.length ) this.unload_set(room.set); } } // -------------------- // Chat History // -------------------- FFZ.prototype._load_history = function(room_id, success, data) { var room = this.rooms[room_id]; if ( ! room || ! room.room ) return; if ( success ) this.log("Received " + data.length + " old messages for: " + room_id); else return this.log("Error retrieving chat history for: " + room_id); if ( ! data.length ) return; return this._insert_history(room_id, data); } FFZ.prototype._show_deleted = function(room_id) { var room = this.rooms[room_id]; if ( ! room || ! room.room ) return; var old_messages = room.room.get('messages.0.ffz_old_messages'); if ( ! old_messages || ! old_messages.length ) return; room.room.set('messages.0.ffz_old_messages', undefined); this._insert_history(room_id, old_messages); } FFZ.prototype._insert_history = function(room_id, data) { var room = this.rooms[room_id]; if ( ! room || ! room.room ) return; var r = room.room, messages = r.get('messages'), tmiSession = r.tmiSession || (TMI._sessions && TMI._sessions[0]), tmiRoom = r.tmiRoom, inserted = 0, last_msg = data[data.length - 1], now = new Date(), last_date = typeof last_msg.date === "string" ? utils.parse_date(last_msg.date) : last_msg.date, age = (now - last_date) / 1000, is_old = age > 300, i = data.length, alternation = r.get('messages.0.ffz_alternate') || false; if ( is_old ) alternation = ! alternation; var i = data.length; while(i--) { var msg = data[i]; if ( typeof msg.date === "string" ) msg.date = utils.parse_date(msg.date); msg.ffz_alternate = alternation = ! alternation; if ( ! msg.room ) msg.room = room_id; if ( ! msg.color ) msg.color = msg.tags && msg.tags.color ? msg.tags.color : tmiSession && msg.from ? tmiSession.getColor(msg.from.toLowerCase()) : "#755000"; if ( ! msg.labels || ! msg.labels.length ) { var labels = msg.labels = []; if ( msg.tags ) { if ( msg.tags.turbo ) labels.push("turbo"); if ( msg.tags.subscriber ) labels.push("subscriber"); if ( msg.from === room_id ) labels.push("owner") else { var ut = msg.tags['user-type']; if ( ut === 'mod' || ut === 'staff' || ut === 'admin' || ut === 'global_mod' ) labels.push(ut); } } } if ( ! msg.style ) { if ( msg.from === "jtv" ) msg.style = "admin"; else if ( msg.from === "twitchnotify" ) msg.style = "notification"; } if ( ! msg.cachedTokens || ! msg.cachedTokens.length ) this.tokenize_chat_line(msg, true, r.get('roomProperties.hide_chat_links')); if ( r.shouldShowMessage(msg) ) { if ( messages.length < r.get("messageBufferSize") ) { // One last thing! Make sure we don't have too many messages. if ( msg.ffz_old_messages ) { var max_msgs = r.get("messageBufferSize") - (messages.length + 1); if ( msg.ffz_old_messages.length > max_msgs ) msg.ffz_old_messages = msg.ffz_old_messages.slice(msg.ffz_old_messages.length - max_msgs); } messages.unshiftObject(msg); inserted += 1; } else break; } } if ( is_old ) { var msg = { ffz_alternate: ! alternation, color: "#755000", date: new Date(), from: "frankerfacez_admin", style: "admin", message: "(Last message is " + utils.human_time(age) + " old.)", room: room_id }; this.tokenize_chat_line(msg, true, r.get('roomProperties.hide_chat_links')); if ( r.shouldShowMessage(msg) ) { messages.insertAt(inserted, msg); while( messages.length > r.get('messageBufferSize') ) messages.removeAt(0); } } } // -------------------- // Receiving Set Info // -------------------- FFZ.prototype.load_room = function(room_id, callback, tries) { var f = this; jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/room/" + room_id) .done(function(data) { if ( data.sets ) { for(var key in data.sets) data.sets.hasOwnProperty(key) && f._load_set_json(key, undefined, data.sets[key]); } f._load_room_json(room_id, callback, data); }).fail(function(data) { if ( data.status == 404 ) return typeof callback == "function" && callback(false); tries = (tries || 0) + 1; if ( tries < 10 ) return f.load_room(room_id, callback, tries); return typeof callback == "function" && callback(false); }); } FFZ.prototype._load_room_json = function(room_id, callback, data) { if ( ! data || ! data.room ) return typeof callback == "function" && callback(false); data = data.room; // Preserve the pointer to the Room instance. 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; if ( data.css || data.moderator_badge ) 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, 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(); if ( callback ) callback(true, data); } // -------------------- // Ember Modifications // -------------------- FFZ.prototype._modify_room = function(room) { var f = this; room.reopen({ slowWaiting: false, slow: 0, mru_list: [], updateWait: function(value, was_banned) { var wait = this.get('slowWait') || 0; this.set('slowWait', value); if ( wait < 1 && value > 0 ) { if ( this._ffz_wait_timer ) clearTimeout(this._ffz_wait_timer); this._ffz_wait_timer = 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() { this._ffz_wait_timer = undefined; var wait = this.get('slowWait') || 0; if ( wait < 1 ) return; this.set('slowWait', --wait); if ( wait > 0 ) this._ffz_wait_timer = 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('r9k', 'subsOnly', 'slow', 'ffz_banned'), // User Level ffzUserLevel: function() { if ( this.get('isStaff') ) return 5; else if ( this.get('isAdmin') ) return 4; else if ( this.get('isBroadcaster') ) return 3; else if ( this.get('isGlobalModerator') ) return 2; else if ( this.get('isModerator') ) return 1; return 0; }.property('id', 'chatLabels.[]'), // Track which rooms the user is currently in. init: function() { this._super(); try { f.add_room(this.id, this); this.set("ffz_chatters", {}); } catch(err) { f.error("add_room: " + err); } }, willDestroy: function() { this._super(); try { f.remove_room(this.id); } catch(err) { f.error("remove_room: " + err); } }, clearMessages: function(user) { var t = this; if ( user ) { if (!this.ffzRecentlyBanned) this.ffzRecentlyBanned = []; this.ffzRecentlyBanned.push(user); while (this.ffzRecentlyBanned.length > 100) this.ffzRecentlyBanned.shift(); var msgs = t.get('messages'), total = msgs.get('length'), i = total, alternate; // Delete visible messages while(i--) { var msg = msgs.get(i); if ( msg.from === user ) { if ( f.settings.remove_deleted ) { if ( alternate === undefined ) alternate = msg.ffz_alternate; msgs.removeAt(i); continue; } t.set('messages.' + i + '.ffz_deleted', true); if ( ! f.settings.prevent_clear ) t.set('messages.' + i + '.deleted', true); } if ( alternate === undefined ) alternate = msg.ffz_alternate; else { alternate = ! alternate; t.set('messages.' + i + '.ffz_alternate', alternate); } } // Delete pending messages if (t.ffzPending) { msgs = t.ffzPending; i = msgs.length; while(i--) { var msg = msgs.get(i); if ( msg.from !== user ) continue; msg.ffz_deleted = true; msg.deleted = !f.settings.prevent_clear; msg.removed = f.settings.remove_deleted; } } 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 ) { last.cachedTokens = ['User has been timed out ' + utils.number_commas(++last.deleted_times) + ' times.']; } else { user_history.push({from: 'jtv', is_delete: true, style: 'admin', cachedTokens: ['User has been timed out.'], deleted_times: 1, 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."); else { var msgs = t.get("messages"); t.set("messages", []); t.addMessage({ style: 'admin', message: i18n("Chat was cleared by a moderator"), ffz_old_messages: msgs }); } } }, trimMessages: function() { var messages = this.get("messages"), len = messages.get("length"), limit = this.get("messageBufferSize"); if ( len > limit ) messages.removeAt(0, len - limit); }, // Artificial chat delay pushMessage: function(msg) { if (+f.settings.chat_delay) { if (!this.ffzPending) this.ffzPending = []; // uses black magic to ensure messages get flushed, but without a setInterval if (!this.ffzPending.length) setTimeout(this.ffzPendingFlush.bind(this), 100); msg.time = Date.now(); this.ffzPending.push(msg); } else { this.ffzActualPushMessage(msg); } }, ffzActualPushMessage: function (msg) { if ( this.shouldShowMessage(msg) && this.ffzShouldShowMessage(msg) ) { this.get("messages").pushObject(msg); this.trimMessages(); "admin" === msg.style || ("whisper" === msg.style && ! this.ffz_whisper_room ) || this.incrementProperty("unreadCount", 1); } }, ffzPendingFlush: function() { var now = Date.now(); for (var i = 0, l = this.ffzPending.length; i < l; i++) { var msg = this.ffzPending[i]; if (msg.removed) continue; if (+f.settings.chat_delay + msg.time > now) break; this.ffzActualPushMessage(msg); } this.ffzPending = this.ffzPending.slice(i); // uses black magic to ensure messages get flushed, but without a setInterval if (this.ffzPending.length) setTimeout(this.ffzPendingFlush.bind(this), 100); }, ffzShouldShowMessage: function (msg) { if (f.settings.remove_bot_ban_notices && this.ffzRecentlyBanned) { var banned = '(' + this.ffzRecentlyBanned.join('|') + ')'; var bots = { 'nightbot': '^' + banned, 'moobot': '\\(' + banned + '\\)', 'xanbot': '^' + banned, }; if (msg.from in bots && (new RegExp(bots[msg.from])).test(msg.message)) { return false; } } return true; }, addMessage: function(msg) { if ( msg ) { if ( ! f.settings.hosted_sub_notices && msg.style === 'notification' && HOSTED_SUB.test(msg.message) ) return; var is_whisper = msg.style === 'whisper'; if ( f.settings.group_tabs && f.settings.whisper_room ) { if ( ( is_whisper && ! this.ffz_whisper_room ) || ( ! is_whisper && this.ffz_whisper_room ) ) return; } if ( ! is_whisper ) msg.room = this.get('id'); // Tokenization f.tokenize_chat_line(msg, false, this.get('roomProperties.hide_chat_links')); // 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 }); if ( 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('isSubscriber') || this.get('isModeratorOrHigher') || ! this.get('slowMode') ) this.updateWait(0, was_banned) else if ( this.get('slowMode') ) this.updateWait(this.get('slow')); } } // Also update chatters. if ( ! is_whisper && this.chatters && ! this.chatters[msg.from] && msg.from !== 'twitchnotify' && msg.from !== 'jtv' ) this.ffzUpdateChatters(msg.from); } var out = this._super(msg); // Color processing. if ( msg.color ) f._handle_color(msg.color); return out; }, setHostMode: function(e) { this.set('ffz_host_target', e && e.hostTarget || null); var user = f.get_user(); if ( user && f._cindex && this.get('id') === user.login ) f._cindex.ffzUpdateHostButton(); var Chat = App.__container__.lookup('controller:chat'); if ( ! Chat || Chat.get('currentChannelRoom') !== this ) return; return this._super(e); }, send: function(text) { if ( f.settings.group_tabs && f.settings.whisper_room && this.ffz_whisper_room ) return; try { if ( text ) { // Command History var mru = this.get('mru_list'), ind = mru.indexOf(text); if ( ind !== -1 ) mru.splice(ind, 1) else if ( mru.length > 20 ) mru.pop(); mru.unshift(text); } var cmd = text.split(' ', 1)[0].toLowerCase(); if ( cmd === "/ffz" ) { this.set("messageToSend", ""); f.run_ffz_command(text.substr(5), this.get('id')); return; } else if ( cmd.charAt(0) === "/" && f.run_command(text, this.get('id')) ) { this.set("messageToSend", ""); return; } } catch(err) { f.error("send: " + err); } return this._super(text); }, ffzUpdateUnread: function() { if ( f.settings.group_tabs ) { var Chat = App.__container__.lookup('controller:chat'); if ( Chat && Chat.get('currentRoom') === this ) this.resetUnreadCount(); else if ( f._chatv ) f._chatv.ffzTabUnread(this.get('id')); } }.observes('unreadCount'), ffzInitChatterCount: function() { if ( ! this.tmiRoom ) return; if ( this._ffz_chatter_timer ) { clearTimeout(this._ffz_chatter_timer); this._ffz_chatter_timer = undefined; } var room = this; this.tmiRoom.list().done(function(data) { var chatters = {}; data = data.data.chatters; for(var i=0; i < data.admins.length; i++) chatters[data.admins[i]] = true; for(var i=0; i < data.global_mods.length; i++) chatters[data.global_mods[i]] = true; for(var i=0; i < data.moderators.length; i++) chatters[data.moderators[i]] = true; for(var i=0; i < data.staff.length; i++) chatters[data.staff[i]] = true; for(var i=0; i < data.viewers.length; i++) chatters[data.viewers[i]] = true; room.set("ffz_chatters", chatters); room.ffzUpdateChatters(); }).always(function() { room._ffz_chatter_timer = setTimeout(room.ffzInitChatterCount.bind(room), 300000); }); }, ffzUpdateChatters: function(add, remove) { var chatters = this.get("ffz_chatters") || {}; if ( add ) chatters[add] = true; if ( remove && chatters[remove] ) delete chatters[remove]; if ( ! f.settings.chatter_count ) return; if ( f._cindex ) f._cindex.ffzUpdateChatters(); 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 */ } }, ffzPatchTMI: function() { if ( this.get('ffz_is_patched') || ! this.get('tmiRoom') ) return; if ( f.settings.chatter_count ) this.ffzInitChatterCount(); var tmi = this.get('tmiRoom'), room = this; // 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; connection._send("CAP REQ :twitch.tv/membership"); connection.on("opened", function() { this._send("CAP REQ :twitch.tv/membership"); }, connection); } // 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(); } if ( msg.msgId === 'hosts_remaining' ) { var match = /(\d+) host command/.exec(msg.message); if ( match ) { room.set('ffz_hosts_left', parseInt(match[1] || 0)); f._cindex && f._cindex.ffzUpdateHostButton(); } } }); // Check this shit. tmi._roomConn._connection.off("message", tmi._roomConn._onIrcMessage, tmi._roomConn); tmi._roomConn._onIrcMessage = function(ircMsg) { if ( ircMsg.target != this.ircChannel ) return; switch ( ircMsg.command ) { case "JOIN": if ( this._session && this._session.nickname === ircMsg.sender ) { this._onIrcJoin(ircMsg); } else f.settings.chatter_count && room.ffzUpdateChatters(ircMsg.sender); break; case "PART": if ( this._session && this._session.nickname === ircMsg.sender ) { this._resetActiveState(); this._connection._exitedRoomConn(); this._trigger("exited"); } else f.settings.chatter_count && room.ffzUpdateChatters(null, ircMsg.sender); break; default: break; } } tmi._roomConn._connection.on("message", tmi._roomConn._onIrcMessage, tmi._roomConn); this.set('ffz_is_patched', true); }.observes('tmiRoom'), // Room State Stuff slowMode: function() { return this.get('slow') > 0; }.property('slow'), onSlowOff: function() { if ( ! this.get('slowMode') ) this.updateWait(0); }.observes('slowMode') }); } },{"../constants":5,"../utils":35}],14:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; // -------------------- // Initialization // -------------------- FFZ.prototype.setup_viewers = function() { this.log("Hooking the Ember Viewers controller."); var Viewers = App.__container__.resolve('controller:viewers'); this._modify_viewers(Viewers); } FFZ.prototype._modify_viewers = function(controller) { var f = this; controller.reopen({ lines: function() { var viewers = this._super(); try { var categories = [], data = {}, last_category = null; // Get the broadcaster name. var Channel = App.__container__.lookup('controller:channel'), room_id = this.get('parentController.model.id'), broadcaster = Channel && Channel.get('id'); // We can get capitalization for the broadcaster from the channel. if ( broadcaster ) { var display_name = Channel.get('display_name'); if ( display_name ) FFZ.capitalization[broadcaster] = [display_name, Date.now()]; } // If the current room isn't the channel's chat, then we shouldn't // display them as the broadcaster. if ( room_id != broadcaster ) broadcaster = null; // Now, break the viewer array down into something we can use. for(var i=0; i < viewers.length; i++) { var entry = viewers[i]; if ( entry.category ) { last_category = entry.category; categories.push(last_category); data[last_category] = []; } else { var viewer = entry.chatter.toLowerCase(); if ( ! viewer ) continue; // If the viewer is the broadcaster, give them their own // group. Don't put them with normal mods! if ( viewer == broadcaster ) { categories.unshift("Broadcaster"); data["Broadcaster"] = [viewer]; } else if ( data.hasOwnProperty(last_category) ) data[last_category].push(viewer); } } // Now, rebuild the viewer list. However, we're going to actually // sort it this time. viewers = []; for(var i=0; i < categories.length; i++) { var category = categories[i], chatters = data[category]; if ( ! chatters || ! chatters.length ) continue; viewers.push({category: category}); viewers.push({chatter: ""}); // Push the chatters, capitalizing them as we go. chatters.sort(); while(chatters.length) { var viewer = chatters.shift(); viewer = FFZ.get_capitalization(viewer); viewers.push({chatter: viewer}); } } } catch(err) { f.error("ViewersController lines: " + err); } return viewers; }.property("content.chatters") }); } },{}],15:[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*['"]([^'"]+)['"][^}]+(?:}|$)/, constants = require('./constants'), utils = require('./utils'), check_margins = function(margins, height) { var mlist = margins.split(/ +/); if ( mlist.length != 2 ) return margins; mlist[0] = parseFloat(mlist[0]); mlist[1] = parseFloat(mlist[1]); if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) return null; return margins; }, build_legacy_css = function(emote) { var margin = emote.margins, srcset = ""; if ( ! margin ) margin = ((emote.height - 18) / -2) + "px 0"; if ( emote.urls[2] || emote.urls[4] ) { srcset = 'url("' + emote.urls[1] + '") 1x'; if ( emote.urls[2] ) srcset += ', url("' + emote.urls[2] + '") 2x'; if ( emote.urls[4] ) srcset += ', url("' + emote.urls[4] + '") 4x'; srcset = '-webkit-image-set(' + srcset + '); image-set(' + srcset + ');'; } return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.urls[1] + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (srcset ? '; ' + srcset : '') + (emote.css ? "; " + emote.css : "") + "}\n"; }, build_new_css = function(emote) { if ( ! emote.margins && ! emote.css ) return build_legacy_css(emote); return build_legacy_css(emote) + 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n"; }, build_css = build_new_css, from_code_point = function(cp) { var code = typeof cp === "string" ? parseInt(cp, 16) : cp; if ( code < 0x10000) return String.fromCharCode(code); code -= 0x10000; return String.fromCharCode( 0xD800 + (code >> 10), 0xDC00 + (code & 0x3FF) ); }; // --------------------- // Initialization // --------------------- FFZ.prototype.setup_emoticons = function() { this.log("Preparing emoticon system."); this.emoji_data = {}; this.emoji_names = {}; this.emote_sets = {}; this.global_sets = []; this.default_sets = []; this._last_emote_id = 0; // Usage Data this.emote_usage = {}; this.log("Creating emoticon style element."); var s = this._emote_style = document.createElement('style'); s.id = "ffz-emoticon-css"; document.head.appendChild(s); 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); } // ------------------------ // Emote Usage // ------------------------ FFZ.prototype.add_usage = function(room_id, emote_id, count) { var rooms = this.emote_usage[emote_id] = this.emote_usage[emote_id] || {}; rooms[room_id] = (rooms[room_id] || 0) + (count || 1); if ( this._emote_report_scheduled ) return; this._emote_report_scheduled = setTimeout(this._report_emotes.bind(this), 30000); } FFZ.prototype._report_emotes = function() { if ( this._emote_report_scheduled ) delete this._emote_report_scheduled; var usage = this.emote_usage; this.emote_usage = {}; this.ws_send("emoticon_uses", [usage], function(){}, true); } // ------------------------ // Twitch Emoticon Checker // ------------------------ FFZ.prototype.check_twitch_emotes = function() { if ( this._twitch_emote_check ) { clearTimeout(this._twitch_emote_check); delete this._twitch_emote_check; } var room; if ( this.rooms ) { for(var key in this.rooms) { if ( this.rooms.hasOwnProperty(key) ) { room = this.rooms[key]; break; } } } if ( ! room || ! room.room || ! room.room.tmiSession ) { this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); return; } var parser = room.room.tmiSession._emotesParser, emotes = Object.keys(parser.emoticonRegexToIds).length; // If we have emotes, we're done! if ( emotes > 0 ) return; // No emotes. Try loading them. var sets = parser.emoticonSetIds; parser.emoticonSetIds = ""; parser.updateEmoticons(sets); // Check again in a bit to see if we've got them. this._twitch_emote_check = setTimeout(this.check_twitch_emotes.bind(this), 10000); } // --------------------- // Set Management // --------------------- 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] || [], room && room.extra_sets || [], this.default_sets); } // --------------------- // Commands // --------------------- FFZ.ws_commands.reload_set = function(set_id) { if ( this.emote_sets.hasOwnProperty(set_id) ) this.load_set(set_id); } FFZ.ws_commands.load_set = function(set_id) { this.load_set(set_id); } // --------------------- // Tooltip Powah! // --------------------- FFZ.prototype._emote_tooltip = function(emote) { if ( ! emote ) return null; if ( emote._tooltip ) return emote._tooltip; var set = this.emote_sets[emote.set_id], owner = emote.owner, title = set && set.title || "Global"; emote._tooltip = "Emoticon: " + (emote.hidden ? "???" : emote.name) + "\nFFZ " + title + (owner ? "\nBy: " + owner.display_name : ""); return emote._tooltip; } // --------------------- // 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 = {}, by_name = {}; for(var eid in data) { var emoji = data[eid]; eid = eid.toLowerCase(); emoji.code = eid; new_data[eid] = emoji; by_name[emoji.short_name] = eid; emoji.raw = _.map(emoji.code.split("-"), from_code_point).join(""); 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, ffzEmoji: eid, altText: emoji.raw }; } f.emoji_data = new_data; f.emoji_names = by_name; 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 // --------------------- FFZ.prototype.load_global_sets = function(callback, tries) { var f = this; jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/set/global") .done(function(data) { f.default_sets = data.default_sets; var gs = f.global_sets = [], sets = data.sets || {}; if ( f.feature_friday && f.feature_friday.set ) { if ( f.global_sets.indexOf(f.feature_friday.set) === -1 ) f.global_sets.push(f.feature_friday.set); if ( f.default_sets.indexOf(f.feature_friday.set) === -1 ) f.default_sets.push(f.feature_friday.set); } for(var key in sets) { if ( ! sets.hasOwnProperty(key) ) continue; var set = sets[key]; gs.push(key); f._load_set_json(key, undefined, set); } }).fail(function(data) { if ( data.status == 404 ) return typeof callback == "function" && callback(false); tries = tries || 0; tries++; if ( tries < 50 ) return f.load_global_sets(callback, tries); return typeof callback == "function" && callback(false); }); } FFZ.prototype.load_set = function(set_id, callback, tries) { var f = this; jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/set/" + set_id) .done(function(data) { f._load_set_json(set_id, callback, data && data.set); }).fail(function(data) { if ( data.status == 404 ) return typeof callback == "function" && callback(false); tries = tries || 0; tries++; if ( tries < 10 ) return f.load_set(set_id, callback, tries); return typeof callback == "function" && callback(false); }); } FFZ.prototype.unload_set = function(set_id) { var set = this.emote_sets[set_id]; if ( ! set ) return; this.log("Unloading emoticons for set: " + set_id); utils.update_css(this._emote_style, set_id, null); delete this.emote_sets[set_id]; } 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 = users; data.count = 0; // Iterate through all the emoticons, building CSS and regex objects as appropriate. var output_css = "", ems = data.emoticons; data.emoticons = {}; for(var i=0; i < ems.length; i++) { var emote = ems[i]; emote.klass = "ffz-emote-" + emote.id; emote.set_id = set_id; emote.srcSet = emote.urls[1] + " 1x"; if ( emote.urls[2] ) emote.srcSet += ", " + emote.urls[2] + " 2x"; if ( emote.urls[4] ) emote.srcSet += ", " + emote.urls[4] + " 4x"; if ( emote.name[emote.name.length-1] === "!" ) emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")(?=\\W|$)", "g"); else emote.regex = new RegExp("(^|\\W|\\b)(" + emote.name + ")\\b", "g"); output_css += build_css(emote); data.count++; data.emoticons[emote.id] = emote; } utils.update_css(this._emote_style, set_id, output_css + (data.css || "")); this.log("Updated emoticons for set #" + set_id + ": " + data.title, data); if ( this._cindex ) this._cindex.ffzFixTitle(); this.update_ui_link(); if ( callback ) callback(true, data); } },{"./constants":5,"./utils":35}],16:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'), SENDER_REGEX = /(\sdata-sender="[^"]*"(?=>))/; // -------------------- // Initialization // -------------------- FFZ.prototype.find_bttv = function(increment, delay) { this.has_bttv = false; if ( window.BTTVLOADED ) return this.setup_bttv(delay||0); if ( delay >= 60000 ) this.log("BetterTTV was not detected after 60 seconds."); else setTimeout(this.find_bttv.bind(this, increment, (delay||0) + increment), increment); } FFZ.prototype.setup_bttv = function(delay) { this.log("BetterTTV was detected after " + delay + "ms. Hooking."); this.has_bttv = true; // this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); // Disable Dark if it's enabled. document.body.classList.remove("ffz-dark"); if ( this._dark_style ) { this._dark_style.parentElement.removeChild(this._dark_style); this._dark_style = undefined; } if ( this._layout_style ) { this._layout_style.parentElement.removeChild(this._layout_style); this._layout_style = undefined; } if ( this._chat_style ) { utils.update_css(this._chat_style, 'chat_font_size', ''); utils.update_css(this._chat_style, 'chat_ts_font_size', ''); } // Disable Chat Tabs if ( this.settings.group_tabs && this._chatv ) { 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-colors-gray"); document.body.classList.remove("ffz-chat-background"); document.body.classList.remove("ffz-chat-padding"); document.body.classList.remove("ffz-chat-separator"); document.body.classList.remove("ffz-chat-separator-3d"); document.body.classList.remove("ffz-sidebar-swap"); document.body.classList.remove("ffz-transparent-badges"); document.body.classList.remove("ffz-high-contrast-chat-text"); document.body.classList.remove("ffz-high-contrast-chat-bg"); document.body.classList.remove("ffz-high-contrast-chat-bold"); // Remove Following Count if ( this.settings.following_count ) { this._schedule_following_count(); this._draw_following_count(); this._draw_following_channels(); } // Remove Sub Count 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; BetterTTV.chat.helpers.sendMessage = function(message) { var cmd = message.split(' ', 1)[0].toLowerCase(); if ( cmd === "/ffz" ) f.run_ffz_command(message.substr(5), BetterTTV.chat.store.currentRoom); else return original_send(message); } // Ugly Hack for Current Room, as this is stripped out before we get to // the actual privmsg renderer. var original_handler = BetterTTV.chat.handlers.onPrivmsg, received_room; BetterTTV.chat.handlers.onPrivmsg = function(room, data) { received_room = room; var output = original_handler(room, data); received_room = null; return output; } // Message Display Behavior var original_privmsg = BetterTTV.chat.templates.privmsg; BetterTTV.chat.templates.privmsg = function(highlight, action, server, isMod, data) { try { // Handle badges. f.bttv_badges(data); // Now, do everything else manually because things are hard-coded. return '
'+ BetterTTV.chat.templates.timestamp(data.time)+' '+ (isMod?BetterTTV.chat.templates.modicons():'')+' '+ BetterTTV.chat.templates.badges(data.badges)+ BetterTTV.chat.templates.from(data.nickname, data.color)+ BetterTTV.chat.templates.message(data.sender, data.message, data.emotes, action?data.color:false)+ '
'; } catch(err) { f.log("Error: ", err); return original_privmsg(highlight, action, server, isMod, data); } } // 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, received_sender; BetterTTV.chat.templates.message = function(sender, message, emotes, colored) { try { colored = colored || false; var rawMessage = encodeURIComponent(message); if(sender !== 'jtv') { // Hackilly send our state across. received_sender = sender; var tokenizedMessage = BetterTTV.chat.templates.emoticonize(message, emotes); received_sender = null; for(var i=0; i'+message+''; } catch(err) { f.log("Error: ", err); return original_message(sender, message, emotes, colored); } }; // Emoticonize var original_emoticonize = BetterTTV.chat.templates.emoticonize; BetterTTV.chat.templates.emoticonize = function(message, emotes) { var tokens = original_emoticonize(message, emotes), room = (received_room || BetterTTV.getChannel()), l_room = room && room.toLowerCase(), l_sender = received_sender && received_sender.toLowerCase(), sets = f.getEmotes(l_sender, l_room), emotes = [], user = f.get_user(), mine = user && user.login === l_sender; // Build a list of emotes that match. _.each(sets, function(set_id) { var set = f.emote_sets[set_id]; if ( ! set ) return; _.each(set.emoticons, function(emote) { _.any(tokens, function(token) { return _.isString(token) && token.match(emote.regex); }) && emotes.push(emote); }); }); // Don't bother proceeding if we have no emotes. if ( emotes.length ) { // Why is emote parsing so bad? ;_; _.each(emotes, function(emote) { var tooltip = f._emote_tooltip(emote), eo = [''], 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 = []; 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(constants.EMOJI_REGEX); while(tbits.length) { var bit = tbits.shift(); bit && tokens.push(bit); if ( tbits.length ) { var match = tbits.shift(), variant = tbits.shift(); if ( variant === '\uFE0E' ) bits.push(match); else { var eid = utils.emoji_to_codepoint(match, variant), data = f.emoji_data[eid]; if ( data ) { tokens.push(['' + alt + '']); } else tokens.push(match + (variant || "")); } } } } }*/ return tokens; } this.update_ui_link(); } },{"../constants":5,"../utils":35}],17:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; // -------------------- // Initialization // -------------------- FFZ.prototype.find_emote_menu = function(increment, delay) { this.has_emote_menu = false; if ( window.emoteMenu && emoteMenu.registerEmoteGetter ) return this.setup_emote_menu(delay||0); if ( delay >= 60000 ) this.log("Emote Menu for Twitch was not detected after 60 seconds."); else setTimeout(this.find_emote_menu.bind(this, increment, (delay||0) + increment), increment); } FFZ.prototype.setup_emote_menu = function(delay) { this.log("Emote Menu for Twitch was detected after " + delay + "ms. Registering emote enumerator."); emoteMenu.registerEmoteGetter("FrankerFaceZ", this._emote_menu_enumerator.bind(this)); } // -------------------- // Emote Enumerator // -------------------- FFZ.prototype._emote_menu_enumerator = function() { var twitch_user = this.get_user(), user_id = twitch_user ? twitch_user.login : null, controller = App.__container__.lookup('controller:chat'), room_id = controller ? controller.get('currentRoom.id') : null, sets = this.getEmotes(user_id, room_id), emotes = []; for(var x = 0; x < sets.length; x++) { var set = this.emote_sets[sets[x]]; if ( ! set || ! set.emoticons ) continue; for(var emote_id in set.emoticons) { if ( ! set.emoticons.hasOwnProperty(emote_id) ) continue; var emote = set.emoticons[emote_id]; if ( emote.hidden ) continue; var title = "FrankerFaceZ " + set.title, badge = set.icon || '//cdn.frankerfacez.com/script/devicon.png'; emotes.push({text: emote.name, url: emote.urls[1], hidden: false, channel: title, badge: badge}); } } return emotes; } },{}],18:[function(require,module,exports){ // Modify Array and others. // require('./shims'); // ---------------- // The Constructor // ---------------- var FFZ = window.FrankerFaceZ = function() { FFZ.instance = this; // Logging this._log_data = []; // Get things started. this.initialize(); } FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { major: 3, minor: 5, revision: 13, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } } // Logging FFZ.prototype.log = function(msg, data, to_json, log_json) { msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); this._log_data.push(msg + ((!to_json && log_json) ? " -- " + JSON.stringify(data) : "")); if ( data !== undefined && console.groupCollapsed && console.dir ) { console.groupCollapsed(msg); if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) console.log(data); else console.dir(data); console.groupEnd(msg); } else console.log(msg); } FFZ.prototype.error = function(msg, data, to_json) { msg = "FFZ Error: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); this._log_data.push(msg); if ( data !== undefined && console.groupCollapsed && console.dir ) { console.groupCollapsed(msg); if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) console.log(data); else console.dir(data); console.groupEnd(msg); } else console.assert(false, msg); } FFZ.prototype.paste_logs = function() { this._pastebin(this._log_data.join("\n"), function(url) { if ( ! url ) return console.log("FFZ Error: Unable to upload log to pastebin."); console.log("FFZ: Your FrankerFaceZ log has been pasted to: " + url); }); } FFZ.prototype._pastebin = function(data, callback) { jQuery.ajax({url: "http://putco.de/", type: "PUT", data: data, context: this}) .success(function(e) { callback.bind(this)(e.trim() + ".log"); }).fail(function(e) { callback.bind(this)(null); }); } // ------------------- // User Data // ------------------- FFZ.prototype.get_user = function() { if ( window.PP && PP.login ) { return PP; } else if ( window.App ) { var nc = App.__container__.lookup("controller:login"); return nc ? nc.get("userData") : undefined; } } // ------------------- // Import Everything! // ------------------- // Import these first to set up data structures require('./ui/menu'); require('./settings'); require('./socket'); require('./colors'); require('./emoticons'); require('./badges'); require('./tokenize'); // Analytics: require('./ember/router'); require('./ember/channel'); //require('./ember/player'); require('./ember/room'); require('./ember/layout'); require('./ember/line'); require('./ember/chatview'); require('./ember/viewers'); require('./ember/moderation-card'); require('./ember/chat-input'); //require('./ember/teams'); require('./debug'); require('./ext/betterttv'); require('./ext/emote_menu'); require('./featurefriday'); require('./ui/styles'); require('./ui/dark'); require('./ui/notifications'); require('./ui/viewer_count'); require('./ui/sub_count'); require('./ui/menu_button'); require('./ui/following'); require('./ui/following-count'); require('./ui/races'); require('./ui/my_emotes'); require('./ui/about_page'); require('./commands'); // --------------- // Initialization // --------------- FFZ.prototype.initialize = function(increment, delay) { // Make sure that FrankerFaceZ doesn't start setting itself up until the // Twitch ember application is ready. // Check for the player if ( location.hostname === 'player.twitch.tv' ) { //this.init_player(delay); return; } // Check for special non-ember pages. if ( /^\/(?:$|search$|user\/|p\/|settings|m\/|messages?\/)/.test(location.pathname) ) { this.init_normal(delay); return; } if ( location.hostname === 'passport' && /^\/(?:authorize)/.test(location.pathname) ) { this.log("Running on passport!"); this.init_normal(delay, true); return; } // Check for the dashboard. if ( /\/[^\/]+\/dashboard/.test(location.pathname) && !/bookmarks$/.test(location.pathname) ) { this.init_dashboard(delay); return; } var loaded = window.App != undefined && App.__container__ != undefined && App.__container__.resolve('model:room') != undefined; if ( !loaded ) { increment = increment || 10; if ( delay >= 60000 ) this.log("Twitch application not detected in \"" + location.toString() + "\". Aborting."); else setTimeout(this.initialize.bind(this, increment, (delay||0) + increment), increment); return; } this.init_ember(delay); } FFZ.prototype.init_player = function(delay) { var start = (window.performance && performance.now) ? performance.now() : Date.now(); this.log("Found Twitch Player 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; } // Literally only make it dark. this.load_settings(); this.setup_dark(); var end = (window.performance && performance.now) ? performance.now() : Date.now(), duration = end - start; this.log("Initialization complete in " + duration + "ms"); } FFZ.prototype.init_normal = function(delay, no_socket) { var start = (window.performance && performance.now) ? performance.now() : Date.now(); 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(); // Start this early, for quick loading. this.setup_dark(); if ( ! no_socket ) this.ws_create(); this.setup_colors(); this.setup_emoticons(); this.setup_badges(); this.setup_notifications(); this.setup_following_count(false); this.setup_css(); this.setup_menu(); this.find_bttv(10); var end = (window.performance && performance.now) ? performance.now() : Date.now(), duration = end - start; this.log("Initialization complete in " + duration + "ms"); } FFZ.prototype.is_dashboard = false; FFZ.prototype.init_dashboard = function(delay) { var start = (window.performance && performance.now) ? performance.now() : Date.now(); this.log("Found Twitch Dashboard after " + (delay||0) + " ms in \"" + location + "\". Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; this.is_dashboard = true; this.embed_in_dash = false; // Initialize all the modules. this.load_settings(); // Start this early, for quick loading. this.setup_dark(); this.ws_create(); this.setup_colors(); this.setup_emoticons(); this.setup_badges(); this.setup_tokenization(); this.setup_notifications(); this.setup_css(); this._update_subscribers(); // Set up the FFZ message passer. this.setup_message_event(); this.find_bttv(10); var end = (window.performance && performance.now) ? performance.now() : Date.now(), duration = end - start; this.log("Initialization complete in " + duration + "ms"); } FFZ.prototype.init_ember = function(delay) { var start = (window.performance && performance.now) ? performance.now() : Date.now(); 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(); // Start this early, for quick loading. this.setup_dark(); this.ws_create(); this.setup_emoticons(); this.setup_badges(); //this.setup_router(); this.setup_colors(); this.setup_tokenization(); //this.setup_player(); this.setup_channel(); this.setup_room(); this.setup_line(); this.setup_layout(); this.setup_chatview(); this.setup_viewers(); this.setup_mod_card(); this.setup_chat_input(); //this.setup_teams(); this.setup_notifications(); this.setup_css(); this.setup_menu(); this.setup_my_emotes(); this.setup_following(); this.setup_following_count(true); this.setup_races(); this.connect_extra_chat(); this.find_bttv(10); this.find_emote_menu(10); this.check_ff(); var end = (window.performance && performance.now) ? performance.now() : Date.now(), duration = end - start; this.log("Initialization complete in " + duration + "ms"); } // ------------------------ // Dashboard Message Event // ------------------------ FFZ.prototype.setup_message_event = function() { this.log("Listening for Window Messages."); window.addEventListener("message", this._on_window_message.bind(this), false); } FFZ.prototype._on_window_message = function(e) { if ( ! e.data || ! e.data.from_ffz ) return; var msg = e.data; } },{"./badges":2,"./colors":3,"./commands":4,"./debug":6,"./ember/channel":7,"./ember/chat-input":8,"./ember/chatview":9,"./ember/layout":10,"./ember/line":11,"./ember/moderation-card":12,"./ember/room":13,"./ember/viewers":14,"./emoticons":15,"./ext/betterttv":16,"./ext/emote_menu":17,"./featurefriday":19,"./settings":20,"./socket":21,"./tokenize":22,"./ui/about_page":23,"./ui/dark":24,"./ui/following":26,"./ui/following-count":25,"./ui/menu":27,"./ui/menu_button":28,"./ui/my_emotes":29,"./ui/notifications":30,"./ui/races":31,"./ui/styles":32,"./ui/sub_count":33,"./ui/viewer_count":34}],19:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); // -------------------- // Initialization // -------------------- FFZ.prototype.feature_friday = null; // -------------------- // Check FF // -------------------- FFZ.prototype.check_ff = function(tries) { if ( ! tries ) this.log("Checking for Feature Friday data..."); jQuery.ajax(constants.SERVER + "script/event.json", {cache: false, dataType: "json", context: this}) .done(function(data) { return this._load_ff(data); }).fail(function(data) { if ( data.status == 404 ) return this._load_ff(null); tries = tries || 0; tries++; if ( tries < 10 ) return setTimeout(this.check_ff.bind(this, tries), 250); return this._load_ff(null); }); } FFZ.ws_commands.reload_ff = function() { this.check_ff(); } // -------------------- // Rendering UI // -------------------- FFZ.prototype._feature_friday_ui = function(room_id, parent, view) { if ( ! this.feature_friday || this.feature_friday.channel == room_id ) return; this._emotes_for_sets(parent, view, [this.feature_friday.set], this.feature_friday.title, this.feature_friday.icon, "FrankerFaceZ"); // Before we add the button, make sure the channel isn't the // current channel. var Channel = App.__container__.lookup('controller:channel'); if ( Channel && Channel.get('id') == this.feature_friday.channel ) return; var ff = this.feature_friday, f = this, btnc = document.createElement('div'), btn = document.createElement('a'); btnc.className = 'chat-menu-content'; btnc.style.textAlign = 'center'; var message = ff.display_name + (ff.live ? " is live now!" : ""); btn.className = 'button primary'; btn.classList.toggle('live', ff.live); btn.classList.toggle('blue', this.has_bttv && BetterTTV.settings.get('showBlueButtons')); btn.href = "http://www.twitch.tv/" + ff.channel; btn.title = message; btn.target = "_new"; btn.innerHTML = "" + message + ""; // Track the number of users to click this button. // btn.addEventListener('click', function() { f.track('trackLink', this.href, 'link'); }); btnc.appendChild(btn); parent.appendChild(btnc); } // -------------------- // Loading Data // -------------------- FFZ.prototype._load_ff = function(data) { // Check for previous Feature Friday data and remove it. if ( this.feature_friday ) { // Remove the global set, delete the data, and reset the UI link. this.global_sets.removeObject(this.feature_friday.set); this.default_sets.removeObject(this.feature_friday.set); this.feature_friday = null; this.update_ui_link(); } // If there's no data, just leave. if ( ! data || ! data.set || ! data.channel ) return; // We have our data! Set it up. this.feature_friday = {set: data.set, channel: data.channel, live: false, title: data.title || "Feature Friday", display_name: FFZ.get_capitalization(data.channel, this._update_ff_name.bind(this))}; // Add the set. this.global_sets.push(data.set); this.default_sets.push(data.set); this.load_set(data.set); // Check to see if the channel is live. this._update_ff_live(); } FFZ.prototype._update_ff_live = function() { if ( ! this.feature_friday ) return; var f = this; Twitch.api.get("streams/" + this.feature_friday.channel) .done(function(data) { f.feature_friday.live = data.stream != null; f.update_ui_link(); }) .always(function() { f.feature_friday.timer = setTimeout(f._update_ff_live.bind(f), 120000); }); } FFZ.prototype._update_ff_name = function(name) { if ( this.feature_friday ) this.feature_friday.display_name = name; } },{"./constants":5}],20:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("./constants"), FileSaver = require("./FileSaver"); make_ls = function(key) { return "ffz_setting_" + key; }, toggle_setting = function(swit, key) { var val = ! this.settings.get(key); this.settings.set(key, val); swit.classList.toggle('active', val); }, option_setting = function(select, key) { this.settings.set(key, JSON.parse(select.options[select.selectedIndex].value)); }, toggle_basic_setting = function(swit, key) { var getter = FFZ.basic_settings[key].get, val = !(typeof getter === 'function' ? getter.bind(this)() : this.settings.get(getter)), setter = FFZ.basic_settings[key].set; if ( typeof setter === 'function' ) setter.bind(this)(val); else this.settings.set(setter, val); swit.classList.toggle('active', val); }, option_basic_setting = function(select, key) { FFZ.basic_settings[key].set.bind(this)(JSON.parse(select.options[select.selectedIndex].value)); }; // -------------------- // Initializer // -------------------- FFZ.settings_info = { advanced_settings: { value: false, visible: false } }; FFZ.basic_settings = {}; FFZ.prototype.load_settings = function() { this.log("Loading settings."); // Build a settings object. this.settings = {}; for(var key in FFZ.settings_info) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) continue; var info = FFZ.settings_info[key], ls_key = info.storage_key || make_ls(key), val = info.hasOwnProperty("value") ? info.value : undefined; if ( localStorage.hasOwnProperty(ls_key) ) { try { val = JSON.parse(localStorage.getItem(ls_key)); } catch(err) { this.log('Error loading value for "' + key + '": ' + err); } } if ( info.process_value ) val = info.process_value.bind(this)(val); this.settings[key] = val; } // Helpers this.settings.get = this._setting_get.bind(this); this.settings.set = this._setting_set.bind(this); this.settings.del = this._setting_del.bind(this); // Listen for Changes window.addEventListener("storage", this._setting_update.bind(this), false); } // -------------------- // Backup and Restore // -------------------- FFZ.prototype.save_settings_file = function() { var data = { version: 1, script_version: FFZ.version_info + '', aliases: this.aliases, settings: {} }; for(var key in FFZ.settings_info) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) continue; var info = FFZ.settings_info[key], ls_key = info.storage_key || make_ls(key); if ( localStorage.hasOwnProperty(ls_key) ) data.settings[key] = this.settings[key]; } var blob = new Blob([JSON.stringify(data, null, 4)], {type: "application/json;charset=utf-8"}); FileSaver.saveAs(blob, "ffz-settings.json"); } FFZ.prototype.load_settings_file = function(file) { if ( typeof file === "string" ) this._load_settings_file(file); else { var reader = new FileReader(), f = this; reader.onload = function(e) { f._load_settings_file(e.target.result); } reader.readAsText(file); } } FFZ.prototype._load_settings_file = function(data) { try { data = JSON.parse(data); } catch(err) { this.error("Error Loading Settings: " + err); return alert("There was an error attempting to read the provided settings data."); } this.log("Loading Settings Data", data); var skipped = [], applied = []; if ( data.settings ) { for(var key in data.settings) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) { skipped.push(key); continue; } var info = FFZ.settings_info[key], val = data.settings[key]; if ( info.process_value ) val = info.process_value.bind(this)(val); if ( val !== this.settings.get(key) ) this.settings.set(key, val); applied.push(key); } } // Do this in a timeout so that any styles have a moment to update. setTimeout(function(){ alert('Successfully loaded ' + applied.length + ' settings and skipped ' + skipped.length + ' settings.'); }); } // -------------------- // Menu Page // -------------------- FFZ.menu_pages.settings = { render: function(view, container) { // Bottom Bar var menu = document.createElement('ul'), page = document.createElement('div'), tab_basic = document.createElement('li'), link_basic = document.createElement('a'), tab_adv = document.createElement('li'), link_adv = document.createElement('a'), tab_save = document.createElement('li'), link_save = document.createElement('a'), height = parseInt(container.style.maxHeight || '0'); // Height Calculation if ( ! height ) height = Math.max(200, view.$().height() - 172); if ( height && height !== NaN ) { height -= 37; page.style.maxHeight = height + 'px'; } // Menu Building page.className = 'ffz-ui-sub-menu-page'; menu.className = 'menu sub-menu clearfix'; tab_basic.className = 'item'; tab_basic.id = 'ffz-settings-page-basic'; link_basic.innerHTML = 'Basic'; tab_basic.appendChild(link_basic); tab_adv.className = 'item'; tab_adv.id = 'ffz-settings-page-advanced'; link_adv.innerHTML = 'Advanced'; tab_adv.appendChild(link_adv); tab_save.className = 'item'; tab_save.id = 'ffz-settings-page-save'; link_save.textContent = 'Backup & Restore'; tab_save.appendChild(link_save); menu.appendChild(tab_basic); menu.appendChild(tab_adv); menu.appendChild(tab_save); var cp = FFZ.menu_pages.settings.change_page; link_basic.addEventListener('click', cp.bind(this, view, container, menu, page, 'basic')); link_adv.addEventListener('click', cp.bind(this, view, container, menu, page, 'advanced')); link_save.addEventListener('click', cp.bind(this, view, container, menu, page, 'save')); if ( this.settings.advanced_settings ) link_adv.click(); else link_basic.click(); container.appendChild(page); container.appendChild(menu); }, change_page: function(view, container, menu, page, key) { page.innerHTML = ''; page.setAttribute('data-page', key); var els = menu.querySelectorAll('li.active'); for(var i=0, l = els.length; i < l; i++) els[i].classList.remove('active'); var el = menu.querySelector('#ffz-settings-page-' + key); if ( el ) el.classList.add('active'); FFZ.menu_pages.settings['render_' + key].bind(this)(view, page); if ( key === 'advanced' ) this.settings.set('advanced_settings', true); else if ( key === 'basic' ) this.settings.set('advanced_settings', false); }, render_save: function(view, container) { var backup_head = document.createElement('div'), restore_head = document.createElement('div'), backup_cont = document.createElement('div'), restore_cont = document.createElement('div'), backup_para = document.createElement('p'), backup_link = document.createElement('a'), backup_help = document.createElement('span'), restore_para = document.createElement('p'), restore_input = document.createElement('input'), restore_link = document.createElement('a'), restore_help = document.createElement('span'), f = this; backup_cont.className = 'chat-menu-content'; backup_head.className = 'heading'; backup_head.innerHTML = 'Backup Settings'; backup_cont.appendChild(backup_head); backup_para.className = 'clearfix option'; backup_link.href = '#'; backup_link.innerHTML = 'Save to File'; backup_link.addEventListener('click', this.save_settings_file.bind(this)); backup_help.className = 'help'; backup_help.innerHTML = 'This generates a JSON file containing all of your settings and prompts you to save it.'; backup_para.appendChild(backup_link); backup_para.appendChild(backup_help); backup_cont.appendChild(backup_para); restore_cont.className = 'chat-menu-content'; restore_head.className = 'heading'; restore_head.innerHTML = 'Restore Settings'; restore_cont.appendChild(restore_head); restore_para.className = 'clearfix option'; restore_input.type = 'file'; restore_input.addEventListener('change', function() { f.load_settings_file(this.files[0]); }) restore_link.href = '#'; restore_link.innerHTML = 'Restore from File'; restore_link.addEventListener('click', function(e) { e.preventDefault(); restore_input.click(); }); restore_help.className = 'help'; restore_help.innerHTML = 'This loads settings from a previously generated JSON file.'; restore_para.appendChild(restore_link); restore_para.appendChild(restore_help); restore_cont.appendChild(restore_para); container.appendChild(backup_cont); container.appendChild(restore_cont); }, render_basic: function(view, container) { var settings = {}, categories = [], is_android = navigator.userAgent.indexOf('Android') !== -1; for(var key in FFZ.basic_settings) { if ( ! FFZ.basic_settings.hasOwnProperty(key) ) continue; var info = FFZ.basic_settings[key], cat = info.category || "Miscellaneous", cs = settings[cat]; if ( info.visible !== undefined && info.visible !== null ) { var visible = info.visible; if ( typeof info.visible == "function" ) visible = info.visible.bind(this)(); if ( ! visible ) continue; } if ( is_android && info.no_mobile ) continue; if ( ! cs ) { categories.push(cat); cs = settings[cat] = []; } cs.push([key, info]); } categories.sort(function(a,b) { var a = a.toLowerCase(), b = b.toLowerCase(); if ( a === "debugging" ) a = "zzz" + a; if ( b === "debugging" ) b = "zzz" + b; if ( a < b ) return -1; else if ( a > b ) return 1; return 0; }); var f = this, current_page = this._ffz_basic_settings_page || categories[0]; for(var ci=0; ci < categories.length; ci++) { var category = categories[ci], cset = settings[category], menu = document.createElement('div'), heading = document.createElement('div'); heading.className = 'heading'; menu.className = 'chat-menu-content'; // collapsable'; menu.setAttribute('data-category', category); //menu.classList.toggle('collapsed', current_page !== category); heading.innerHTML = category; menu.appendChild(heading); /*menu.addEventListener('click', function() { if ( ! this.classList.contains('collapsed') ) return; var t = this, old_selection = container.querySelectorAll('.chat-menu-content:not(.collapsed)'); for(var i=0; i < old_selection.length; i++) old_selection[i].classList.add('collapsed'); f._ffz_basic_settings_page = t.getAttribute('data-category'); t.classList.remove('collapsed'); setTimeout(function(){t.scrollIntoViewIfNeeded()}); });*/ cset.sort(function(a,b) { var a = a[1], b = b[1], at = a.type === "boolean" ? 1 : 2, bt = b.type === "boolean" ? 1 : 2, an = a.name.toLowerCase(), bn = b.name.toLowerCase(); if ( at < bt ) return -1; else if ( at > bt ) return 1; else if ( an < bn ) return -1; else if ( an > bn ) return 1; return 0; }); for(var i=0; i < cset.length; i++) { var key = cset[i][0], info = cset[i][1], el = document.createElement('p'), val = info.type !== "button" && typeof info.get === 'function' ? info.get.bind(this)() : this.settings.get(info.get); el.className = 'clearfix'; if ( this.has_bttv && info.no_bttv ) { var label = document.createElement('span'), help = document.createElement('span'); label.className = 'switch-label'; label.innerHTML = info.name; help = document.createElement('span'); help.className = 'help'; help.innerHTML = 'Disabled due to incompatibility with BetterTTV.'; el.classList.add('disabled'); el.appendChild(label); el.appendChild(help); } else { if ( info.type == "boolean" ) { var swit = document.createElement('a'), label = document.createElement('span'); swit.className = 'switch'; swit.classList.toggle('active', val); swit.innerHTML = ""; label.className = 'switch-label'; label.innerHTML = info.name; el.appendChild(swit); el.appendChild(label); swit.addEventListener("click", toggle_basic_setting.bind(this, swit, key)); } else if ( info.type === "select" ) { var select = document.createElement('select'), label = document.createElement('span'); label.className = 'option-label'; label.innerHTML = info.name; for(var ok in info.options) { var op = document.createElement('option'); op.value = JSON.stringify(ok); if ( val === ok ) op.setAttribute('selected', true); op.innerHTML = info.options[ok]; select.appendChild(op); } select.addEventListener('change', option_basic_setting.bind(this, select, key)); el.appendChild(label); el.appendChild(select); } else { el.classList.add("option"); var link = document.createElement('a'); link.innerHTML = info.name; link.href = "#"; el.appendChild(link); link.addEventListener("click", info.method.bind(this)); } if ( info.help ) { var help = document.createElement('span'); help.className = 'help'; help.innerHTML = info.help; el.appendChild(help); } } menu.appendChild(el); } container.appendChild(menu); } }, render_advanced: function(view, container) { var settings = {}, categories = [], is_android = navigator.userAgent.indexOf('Android') !== -1; for(var key in FFZ.settings_info) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) continue; var info = FFZ.settings_info[key], cat = info.category || "Miscellaneous", cs = settings[cat]; if ( info.visible !== undefined && info.visible !== null ) { var visible = info.visible; if ( typeof info.visible == "function" ) visible = info.visible.bind(this)(); if ( ! visible ) continue; } if ( is_android && info.no_mobile ) continue; if ( ! cs ) { categories.push(cat); cs = settings[cat] = []; } cs.push([key, info]); } categories.sort(function(a,b) { var a = a.toLowerCase(), b = b.toLowerCase(); if ( a === "debugging" ) a = "zzz" + a; if ( b === "debugging" ) b = "zzz" + b; if ( a < b ) return -1; else if ( a > b ) return 1; return 0; }); var f = this, current_page = this._ffz_settings_page || categories[0]; for(var ci=0; ci < categories.length; ci++) { var category = categories[ci], cset = settings[category], menu = document.createElement('div'), heading = document.createElement('div'); heading.className = 'heading'; menu.className = 'chat-menu-content collapsable'; menu.setAttribute('data-category', category); menu.classList.toggle('collapsed', current_page !== category); heading.innerHTML = category; menu.appendChild(heading); menu.addEventListener('click', function() { if ( ! this.classList.contains('collapsed') ) return; var t = this, old_selection = container.querySelectorAll('.chat-menu-content:not(.collapsed)'); for(var i=0; i < old_selection.length; i++) old_selection[i].classList.add('collapsed'); f._ffz_settings_page = t.getAttribute('data-category'); t.classList.remove('collapsed'); setTimeout(function(){t.scrollIntoViewIfNeeded()}); }); cset.sort(function(a,b) { var a = a[1], b = b[1], at = a.type === "boolean" ? 1 : 2, bt = b.type === "boolean" ? 1 : 2, an = a.name.toLowerCase(), bn = b.name.toLowerCase(); if ( at < bt ) return -1; else if ( at > bt ) return 1; else if ( an < bn ) return -1; else if ( an > bn ) return 1; return 0; }); for(var i=0; i < cset.length; i++) { var key = cset[i][0], info = cset[i][1], el = document.createElement('p'), val = this.settings.get(key); el.className = 'clearfix'; if ( this.has_bttv && info.no_bttv ) { var label = document.createElement('span'), help = document.createElement('span'); label.className = 'switch-label'; label.innerHTML = info.name; help = document.createElement('span'); help.className = 'help'; help.innerHTML = 'Disabled due to incompatibility with BetterTTV.'; el.classList.add('disabled'); el.appendChild(label); el.appendChild(help); } else { if ( info.type == "boolean" ) { var swit = document.createElement('a'), label = document.createElement('span'); swit.className = 'switch'; swit.classList.toggle('active', val); swit.innerHTML = ""; label.className = 'switch-label'; label.innerHTML = info.name; el.appendChild(swit); el.appendChild(label); swit.addEventListener("click", toggle_setting.bind(this, swit, key)); } else if ( info.type === "select" ) { var select = document.createElement('select'), label = document.createElement('span'); label.className = 'option-label'; label.innerHTML = info.name; for(var ok in info.options) { var op = document.createElement('option'); op.value = JSON.stringify(ok); if ( val === ok ) op.setAttribute('selected', true); op.innerHTML = info.options[ok]; select.appendChild(op); } select.addEventListener('change', option_setting.bind(this, select, key)); el.appendChild(label); el.appendChild(select); } else { el.classList.add("option"); var link = document.createElement('a'); link.innerHTML = info.name; link.href = "#"; el.appendChild(link); link.addEventListener("click", info.method.bind(this)); } if ( info.help ) { var help = document.createElement('span'); help.className = 'help'; help.innerHTML = info.help; el.appendChild(help); } } menu.appendChild(el); } container.appendChild(menu); } }, name: "Settings", icon: constants.GEAR, sort_order: 99999, wide: true, sub_menu: true }; // -------------------- // Tracking Updates // -------------------- FFZ.prototype._setting_update = function(e) { if ( ! e ) e = window.event; if ( ! e.key || e.key.substr(0, 12) !== "ffz_setting_" ) return; var ls_key = e.key, key = ls_key.substr(12), val = undefined, info = FFZ.settings_info[key]; if ( ! info ) { // Try iterating to find the key. for(key in FFZ.settings_info) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) continue; info = FFZ.settings_info[key]; if ( info.storage_key == ls_key ) break; } // Not us. if ( info.storage_key != ls_key ) return; } this.log("Updated Setting: " + key); try { val = JSON.parse(e.newValue); } catch(err) { this.log('Error loading new value for "' + key + '": ' + err); val = info.value || undefined; } this.settings[key] = val; if ( info.on_update ) try { info.on_update.bind(this)(val, false); } catch(err) { this.log('Error running updater for setting "' + key + '": ' + err); } } // -------------------- // Settings Access // -------------------- FFZ.prototype._setting_get = function(key) { return this.settings[key]; } FFZ.prototype._setting_set = function(key, val) { var info = FFZ.settings_info[key], ls_key = info.storage_key || make_ls(key), jval = JSON.stringify(val); this.settings[key] = val; localStorage.setItem(ls_key, jval); this.log('Changed Setting "' + key + '" to: ' + jval); if ( info.on_update ) try { info.on_update.bind(this)(val, true); } catch(err) { this.log('Error running updater for setting "' + key + '": ' + err); } } FFZ.prototype._setting_del = function(key) { var info = FFZ.settings_info[key], ls_key = info.storage_key || make_ls(key), val = undefined; if ( localStorage.hasOwnProperty(ls_key) ) localStorage.removeItem(ls_key); delete this.settings[key]; if ( info ) val = this.settings[key] = info.hasOwnProperty("value") ? info.value : undefined; if ( info.on_update ) try { info.on_update.bind(this)(val, true); } catch(err) { this.log('Error running updater for setting "' + key + '": ' + err); } } },{"./FileSaver":1,"./constants":5}],21:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; FFZ.prototype._ws_open = false; FFZ.prototype._ws_delay = 0; FFZ.prototype._ws_last_iframe = 0; FFZ.ws_commands = {}; FFZ.ws_on_close = []; // ---------------- // Socket Creation // ---------------- FFZ.prototype.ws_iframe = function() { this._ws_last_iframe = Date.now(); var ifr = document.createElement('iframe'), f = this; ifr.src = 'http://catbag.frankerfacez.com'; ifr.style.visibility = 'hidden'; document.body.appendChild(ifr); setTimeout(function() { document.body.removeChild(ifr); if ( ! f._ws_open ) f.ws_create(); }, 2000); } FFZ.prototype.ws_create = function() { var f = this, ws; this._ws_last_req = 0; this._ws_callbacks = {}; this._ws_pending = this._ws_pending || []; try { ws = this._ws_sock = new WebSocket("ws://catbag.frankerfacez.com/"); } catch(err) { this._ws_exists = false; return this.log("Error Creating WebSocket: " + err); } this._ws_exists = true; ws.onopen = function(e) { f._ws_open = true; f._ws_delay = 0; f._ws_last_iframe = Date.now(); f.log("Socket connected."); // Check for incognito. We don't want to do a hello in incognito mode. var fs = window.RequestFileSystem || window.webkitRequestFileSystem; if (!fs) // Assume not. f.ws_send("hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)); else fs(window.TEMPORARY, 100, f.ws_send.bind(f, "hello", ["ffz_" + FFZ.version_info, localStorage.ffzClientId], f._ws_on_hello.bind(f)), f.log.bind(f, "Operating in Incognito Mode.")); var user = f.get_user(); if ( user ) f.ws_send("setuser", user.login); // Join the right channel if we're in the dashboard. if ( f.is_dashboard ) { var match = location.pathname.match(/\/([^\/]+)/); if ( match ) { f.ws_send("sub", match[1]); f.ws_send("sub_channel", match[1]); } } // Send the current rooms. for(var room_id in f.rooms) { if ( ! f.rooms.hasOwnProperty(room_id) || ! f.rooms[room_id] ) continue; f.ws_send("sub", room_id); if ( f.rooms[room_id].needs_history ) { f.rooms[room_id].needs_history = false; if ( ! f.has_bttv && f.settings.chat_history ) f.ws_send("chat_history", [room_id,25], f._load_history.bind(f, room_id)); } } // Send 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 = []; for(var i=0; i < pending.length; i++) { var d = pending[i]; f.ws_send(d[0], d[1], d[2]); } } ws.onclose = function(e) { f.log("Socket closed. (Code: " + e.code + ", Reason: " + e.reason + ")"); f._ws_open = false; // When the connection closes, run our callbacks. for(var i=0; i < FFZ.ws_on_close.length; i++) { try { FFZ.ws_on_close[i].bind(f)(); } catch(err) { f.log("Error on Socket Close Callback: " + err); } } if ( f._ws_delay > 10000 ) { var ua = navigator.userAgent.toLowerCase(); if ( Date.now() - f._ws_last_iframe > 1800000 && !(ua.indexOf('chrome') === -1 && ua.indexOf('safari') !== -1) ) return f.ws_iframe(); } // We never ever want to not have a socket. if ( f._ws_delay < 60000 ) f._ws_delay += (Math.floor(Math.random()*10) + 5) * 1000; else // Randomize delay. f._ws_delay = (Math.floor(Math.random()*60)+30)*1000; setTimeout(f.ws_create.bind(f), f._ws_delay); } ws.onmessage = function(e) { // Messages are formatted as REQUEST_ID SUCCESS/FUNCTION_NAME[ JSON_DATA] var cmd, data, ind = e.data.indexOf(" "), msg = e.data.substr(ind + 1), request = parseInt(e.data.slice(0, ind)); ind = msg.indexOf(" "); if ( ind === -1 ) ind = msg.length; cmd = msg.slice(0, ind); msg = msg.substr(ind + 1); if ( msg ) data = JSON.parse(msg); if ( request === -1 ) { // It's a command from the server. var command = FFZ.ws_commands[cmd]; if ( command ) command.bind(f)(data); else f.log("Invalid command: " + cmd, data, false, true); } else { var success = cmd === 'True', has_callback = typeof f._ws_callbacks[request] === "function"; if ( ! has_callback ) f.log("Socket Reply to " + request + " - " + (success ? "SUCCESS" : "FAIL"), data, false, true); else { try { f._ws_callbacks[request](success, data); } catch(err) { f.error("Callback for " + request + ": " + err); } f._ws_callbacks[request] = undefined; } } } } FFZ.prototype.ws_send = function(func, data, callback, can_wait) { if ( ! this._ws_open ) { if ( can_wait ) { var pending = this._ws_pending = this._ws_pending || []; pending.push([func, data, callback]); return true; } else return false; } var request = ++this._ws_last_req; data = data !== undefined ? " " + JSON.stringify(data) : ""; if ( callback ) this._ws_callbacks[request] = callback; try { this._ws_sock.send(request + " " + func + data); } catch(err) { this.log("Socket Send Error: " + err); return false; } return request; } // ---------------- // HELLO Response // ---------------- FFZ.prototype._ws_on_hello = function(success, data) { if ( ! success ) return this.log("Error Saying Hello: " + data); localStorage.ffzClientId = data; this.log("Client ID: " + data); var survey = {}, set = survey['settings'] = {}; for(var key in FFZ.settings_info) set[key] = this.settings[key]; set["keywords"] = this.settings.keywords.length; set["banned_words"] = this.settings.banned_words.length; // Detect BTTV. survey['bttv'] = this.has_bttv || !!document.head.querySelector('script[src*="betterttv"]'); // Client Info survey['user-agent'] = navigator.userAgent; survey['screen'] = [screen.width, screen.height]; survey['language'] = navigator.language; survey['platform'] = navigator.platform; this.ws_send("survey", [survey]); } // ---------------- // Authorization // ---------------- FFZ.ws_commands.do_authorize = function(data) { // Try finding a channel we can send on. var conn; for(var room_id in this.rooms) { if ( ! this.rooms.hasOwnProperty(room_id) ) continue; var r = this.rooms[room_id]; if ( r && r.room && !r.room.get('roomProperties.eventchat') && !r.room.get('isGroupRoom') && r.room.tmiRoom ) { var c = r.room.tmiRoom._getConnection(); if ( c.isConnected ) { conn = c; break; } } } if ( conn ) conn._send("PRIVMSG #frankerfacezauthorizer :AUTH " + data); else // Try again shortly. setTimeout(FFZ.ws_commands.do_authorize.bind(this, data), 5000); } },{}],22:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require("./utils"), constants = require("./constants"), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", helpers, SRCSETS = {}; build_srcset = function(id) { 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; }, data_to_tooltip = function(data) { var set = data.set, set_type = data.set_type, owner = data.owner; if ( set_type === undefined ) set_type = "Channel"; if ( ! set ) return data.code; else if ( set === "--global--" ) { set = "Twitch Global"; set_type = null; } else if ( set == "--twitch-turbo--" || set == "turbo" || set == "--turbo-faces--" ) { set = "Twitch Turbo"; set_type = null; } return "Emoticon: " + data.code + "\n" + (set_type ? set_type + ": " : "") + set + (owner ? "\nBy: " + owner.display_name : ""); }, build_tooltip = function(id) { var emote_data = this._twitch_emotes[id], set = emote_data ? emote_data.set : null; if ( ! emote_data ) return "???"; if ( typeof emote_data == "string" ) return emote_data; if ( emote_data.tooltip ) return emote_data.tooltip; return emote_data.tooltip = data_to_tooltip(emote_data); }, load_emote_data = function(id, code, success, data) { if ( ! success ) return code; if ( code ) data.code = code; this._twitch_emotes[id] = data; var tooltip = build_tooltip.bind(this)(id); var images = document.querySelectorAll('img[data-emote="' + id + '"]'); for(var x=0; x < images.length; x++) images[x].title = tooltip; return tooltip; }, reg_escape = function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); }, LINK = /(?:https?:\/\/)?(?:[-a-zA-Z0-9@:%_\+~#=]+\.)+[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#!?&//=]*)/g, 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 + "*"), LINK_SPLIT = /^(?:(https?):\/\/)?(?:(.*?)@)?([^\/:]+)(?::(\d+))?(.*?)(?:\?(.*?))?(?:\#(.*?))?$/, YOUTUBE_CHECK = /^(?:https?:\/\/)?(?:m\.|www\.)?youtu(?:be\.com|\.be)\/(?:v\/|watch\/|.*?(?:embed|watch).*?v=)?([a-zA-Z0-9\-_]+)$/, IMGUR_PATH = /^\/(?:gallery\/)?[A-Za-z0-9]+(?:\.(?:png|jpg|jpeg|gif|gifv|bmp))?$/, IMAGE_EXT = /\.(?:png|jpg|jpeg|gif|bmp)$/i, IMAGE_DOMAINS = [], is_image = function(href, any_domain) { var match = href.match(LINK_SPLIT); if ( ! match ) return; var domain = match[3].toLowerCase(), port = match[4], path = match[5]; // Don't allow non-standard ports. if ( port && port !== '80' && port !== '443' ) return false; // imgur-specific checks. if ( domain === 'i.imgur.com' || domain === 'imgur.com' || domain === 'www.imgur.com' || domain === 'm.imgur.com' ) return IMGUR_PATH.test(path); return any_domain ? IMAGE_EXT.test(path) : IMAGE_DOMAINS.indexOf(domain) !== -1; } image_iframe = function(href, extra_class) { return ''; }, build_link_tooltip = function(href) { var link_data = this._link_data[href], tooltip; if ( ! link_data ) return ""; if ( link_data.tooltip ) return link_data.tooltip; if ( link_data.type == "youtube" ) { tooltip = this.settings.link_image_hover ? image_iframe(link_data.full || href, 'ffz-yt-thumb') : ''; tooltip += "YouTube: " + utils.sanitize(link_data.title) + "
"; tooltip += "Channel: " + utils.sanitize(link_data.channel) + " | " + utils.time_to_string(link_data.duration) + "
"; tooltip += utils.number_commas(link_data.views||0) + " Views | 👍 " + utils.number_commas(link_data.likes||0) + " 👎 " + utils.number_commas(link_data.dislikes||0); } else if ( link_data.type == "strawpoll" ) { tooltip = "Strawpoll: " + utils.sanitize(link_data.title) + "
"; for(var key in link_data.items) { var votes = link_data.items[key], percentage = Math.floor((votes / link_data.total) * 100); tooltip += '"; } tooltip += "
' + utils.sanitize(key) + '' + utils.number_commas(votes) + "

Total: " + utils.number_commas(link_data.total); var fetched = utils.parse_date(link_data.fetched); if ( fetched ) { var age = Math.floor((fetched.getTime() - Date.now()) / 1000); if ( age > 60 ) tooltip += "
Data was cached " + utils.time_to_string(age) + " ago."; } } else if ( link_data.type == "twitch" ) { tooltip = "Twitch: " + utils.sanitize(link_data.display_name) + "
"; var since = utils.parse_date(link_data.since); if ( since ) tooltip += "Member Since: " + utils.date_string(since) + "
"; tooltip += "Views: " + utils.number_commas(link_data.views) + " | Followers: " + utils.number_commas(link_data.followers) + ""; } else if ( link_data.type == "twitch_vod" ) { tooltip = "Twitch " + (link_data.broadcast_type == "highlight" ? "Highlight" : "Broadcast") + ": " + utils.sanitize(link_data.title) + "
"; tooltip += "By: " + utils.sanitize(link_data.display_name) + (link_data.game ? " | Playing: " + utils.sanitize(link_data.game) : " | Not Playing") + "
"; tooltip += "Views: " + utils.number_commas(link_data.views) + " | " + utils.time_to_string(link_data.length); } else if ( link_data.type == "twitter" ) { tooltip = "Tweet By: " + utils.sanitize(link_data.user) + "
"; tooltip += utils.sanitize(link_data.tweet); } else if ( link_data.type == "reputation" ) { tooltip = (this.settings.link_image_hover && is_image(link_data.full || href, this.settings.image_hover_all_domains)) ? image_iframe(link_data.full || href) : ''; tooltip += '' + utils.sanitize(link_data.full.toLowerCase()) + ''; if ( link_data.trust < 50 || link_data.safety < 50 || (link_data.tags && link_data.tags.length > 0) ) { tooltip += "
"; var had_extra = false; if ( link_data.trust < 50 || link_data.safety < 50 ) { link_data.unsafe = true; tooltip += "Potentially Unsafe Link
"; tooltip += "Trust: " + link_data.trust + "% | Child Safety: " + link_data.safety + "%"; had_extra = true; } if ( link_data.tags && link_data.tags.length > 0 ) tooltip += (had_extra ? "
" : "") + "Tags: " + link_data.tags.join(", "); tooltip += "
Data Source: WOT"; } } else if ( link_data.full ) { tooltip = (this.settings.link_image_hover && is_image(link_data.full || href, this.settings.image_hover_all_domains)) ? image_iframe(link_data.full || href) : ''; tooltip += '' + utils.sanitize(link_data.full.toLowerCase()) + ''; } if ( ! tooltip ) tooltip = '' + utils.sanitize(href.toLowerCase()) + ''; link_data.tooltip = tooltip; return tooltip; }, load_link_data = function(href, success, data) { if ( ! success ) return; this._link_data[href] = data; data.unsafe = false; var tooltip = build_link_tooltip.bind(this)(href), links, no_trail = href.charAt(href.length-1) == "/" ? href.substr(0, href.length-1) : null; if ( no_trail ) links = document.querySelectorAll('span.message a[href="' + href + '"], span.message a[href="' + no_trail + '"], span.message a[data-url="' + href + '"], span.message a[data-url="' + no_trail + '"]'); else links = document.querySelectorAll('span.message a[href="' + href + '"], span.message a[data-url="' + href + '"]'); if ( ! this.settings.link_info ) return; for(var x=0; x < links.length; x++) { if ( data.unsafe ) links[x].classList.add('unsafe-link'); if ( ! links[x].classList.contains('deleted-link') ) links[x].title = tooltip; } }; 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; }; // --------------------- // Settings // --------------------- var ts = new Date(0).toLocaleTimeString().toUpperCase(); FFZ.settings_info.twenty_four_timestamps = { type: "boolean", value: ts.lastIndexOf('PM') === -1 && ts.lastIndexOf('AM') === -1, category: "Chat Appearance", no_bttv: true, name: "24hr Timestamps", help: "Display timestamps in chat in the 24 hour format rather than 12 hour." }; FFZ.settings_info.show_deleted_links = { type: "boolean", value: false, category: "Chat Moderation", no_bttv: true, name: "Show Deleted Links", help: "Do not delete links based on room settings or link length." }; // --------------------- // Setup // --------------------- FFZ.prototype.setup_tokenization = function() { // Tooltip Data this._twitch_emotes = {}; this._twitch_emote_to_set = {}; this._twitch_set_to_channel = {}; this._link_data = {}; this.load_twitch_emote_data(); helpers = window.require && window.require("ember-twitch-chat/helpers/chat-line-helpers"); if ( ! helpers ) return this.log("Unable to get chat helper functions."); this.log("Hooking Ember chat line helpers."); var f = this; // Timestamp Display helpers.getTime = function(e) { if ( e === undefined || e === null ) return '?:??'; var hours = e.getHours(), minutes = e.getMinutes(); if ( hours > 12 && ! f.settings.twenty_four_timestamps ) hours -= 12; else if ( hours === 0 && ! f.settings.twenty_four_timestamps ) hours = 12; return hours + ':' + (minutes < 10 ? '0' : '') + minutes; }; // Linkify Messages helpers.linkifyMessage = function(tokens, delete_links) { var show_deleted = f.settings.show_deleted_links; return _.chain(tokens).map(function(token) { if ( ! _.isString(token) ) return token; var matches = token.match(LINK); if ( ! matches || ! matches.length ) return [token]; return _.zip( token.split(LINK), _.map(matches, function(e) { var long = e.length > 255; if ( ! show_deleted && (delete_links || long) ) return {isLink: true, isDeleted: true, isLong: long, href: e}; //return {mentionedUser: '<' + (e.length > 255 ? 'long link' : 'deleted link') + '>', own: true} return {isLink: true, href: e}; }) ); }).flatten().compact().value(); }; } // --------------------- // Twitch Emote Data // --------------------- FFZ.prototype.load_twitch_emote_data = function(tries) { jQuery.ajax(constants.SERVER + "script/twitch_emotes.json", {cache: false, context: this}) .done(function(data) { for(var set_id in data) { var set = data[set_id]; if ( ! set ) continue; this._twitch_set_to_channel[set_id] = set.name; for(var i=0, l = set.emotes.length; i < l; i++) this._twitch_emote_to_set[set.emotes[i]] = set_id; } this._twitch_set_to_channel[0] = "--global--"; this._twitch_set_to_channel[33] = "--turbo-faces--"; this._twitch_set_to_channel[42] = "--turbo-faces--"; }).fail(function(data) { if ( data.status === 404 ) return; tries = (tries || 0) + 1; if ( tries < 10 ) setTimeout(this.load_twitch_emote_data.bind(this, tries), 1000); }); } // --------------------- // Tokenization // --------------------- FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, delete_links) { if ( msgObject.cachedTokens ) return msgObject.cachedTokens; var msg = msgObject.message, user = this.get_user(), room_id = msgObject.room, from_me = user && msgObject.from === user.login, emotes = msgObject.tags && msgObject.tags.emotes, tokens = [msg]; // Standard tokenization if ( helpers && helpers.linkifyMessage ) { var labels = msg.labels || [], mod_or_higher = labels.indexOf("owner") !== -1 || labels.indexOf("staff") !== -1 || labels.indexOf("admin") !== -1 || labels.indexOf("global_mod") !== -1 || labels.indexOf("mod") !== -1 || msg.style === 'admin'; tokens = helpers.linkifyMessage(tokens, delete_links && !mod_or_higher); } if ( user && user.login && helpers && helpers.mentionizeMessage ) tokens = helpers.mentionizeMessage(tokens, user.login, from_me); if ( helpers && helpers.emoticonizeMessage ) tokens = helpers.emoticonizeMessage(tokens, emotes); if ( this.settings.replace_bad_emotes ) tokens = this.tokenize_replace_emotes(tokens); // FrankerFaceZ Extras 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 ) FFZ.capitalization[msgObject.from] = [display.trim(), Date.now()]; // Mentions! if ( ! from_me ) { tokens = this.tokenize_mentions(tokens); for(var i=0; i < tokens.length; i++) { var token = tokens[i]; if ( msgObject.style !== 'whisper' && (_.isString(token) || ! token.mentionedUser || token.own) ) continue; // We have a mention! msgObject.ffz_has_mention = true; // If we have chat tabs, update the status. if ( room_id && ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'); if ( el && ! el.classList.contains('active') ) el.classList.add('tab-mentioned'); } // 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 && ! this.embed_in_dash && ! document.hasFocus() && ! prevent_notification ) { var room = this.rooms[room_id] && this.rooms[room_id].room, room_name; // Make sure we have UI for this channel. if ( (this.settings.group_tabs && (this.settings.pinned_rooms.indexOf(room_id) !== -1 || this._chatv._ffz_host )) || room.get('isGroupRoom') || room === this._chatv.get('controller.currentChannelRoom') ) { if ( room && room.get('isGroupRoom') ) room_name = room.get('tmiRoom.displayName'); else room_name = FFZ.get_capitalization(room_id); display = display || Twitch.display.capitalize(msgObject.from); if ( msgObject.style === 'action' ) msg = '* ' + display + ' ' + msg; else msg = display + ': ' + msg; var f = this; if ( msgObject.style === 'whisper' ) this.show_notification( msg, "Twitch Chat Whisper", "ffz_whisper_notice", (this.settings.notification_timeout*1000), function() { window.focus(); } ); else this.show_notification( msg, "Twitch Chat Mention in " + room_name, room_id, (this.settings.notification_timeout*1000), function() { window.focus(); var cont = App.__container__.lookup('controller:chat'); room && cont && cont.focusRoom(room); } ); } } break; } } msgObject.cachedTokens = tokens; return tokens; } FFZ.prototype.tokenize_line = function(user, room, message, no_emotes, no_emoji) { if ( typeof message === "string" ) message = [message]; if ( helpers && helpers.linkifyMessage ) message = helpers.linkifyMessage(message); if ( helpers && helpers.mentionizeMessage ) { var u = this.get_user(); if ( u && u.login ) message = helpers.mentionizeMessage(message, u.login, user === u.login); } if ( ! no_emotes ) { message = this.tokenize_emotes(user, room, message); if ( this.settings.replace_bad_emotes ) message = this.tokenize_replace_emotes(message); } if ( this.settings.parse_emoji && ! no_emoji ) message = this.tokenize_emoji(message); return message; } FFZ.prototype.render_tokens = function(tokens, render_links) { var f = this; return _.map(tokens, function(token) { if ( token.emoticonSrc ) { var tooltip, srcset, extra; 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; srcset = emote ? emote.srcSet : token.srcSet; extra = ' data-ffz-emote="' + emote.id + '"'; } 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; srcset = emoji ? emoji.srcSet : token.srcSet; extra = ' data-ffz-emoji="' + eid + '"'; } else { var id = token.replacedId || FFZ.src_to_id(token.emoticonSrc), data = id && f._twitch_emotes && f._twitch_emotes[id]; if ( data ) tooltip = data.tooltip ? data.tooltip : token.altText; else { try { var set_id = f._twitch_emote_to_set[id]; if ( set_id ) { tooltip = load_emote_data.bind(f)(id, token.altText, true, { code: token.altText, id: id, set: f._twitch_set_to_channel[set_id], set_id: set_id }); } else { tooltip = f._twitch_emotes[id] = token.altText; f.ws_send("twitch_emote", id, load_emote_data.bind(f, id, token.altText)); } } catch(err) { f.error("Error Generating Emote Tooltip: " + err); } } extra = ' data-emote="' + id + '"'; if ( ! constants.EMOTE_REPLACEMENTS[id] ) srcset = build_srcset(id); } return ''; } if ( token.isLink ) { var text = token.title || (token.isLong && '') || (token.isDeleted && '') || token.href; if ( ! render_links && render_links !== undefined ) return utils.sanitize(text); var href = token.href, tooltip, cls = '', ind_at = href.indexOf("@"), ind_sl = href.indexOf("/"); if ( ind_at !== -1 && (ind_sl === -1 || ind_at < ind_sl) ) { // E-Mail Link cls = 'email-link'; if ( f.settings.link_info ) { cls += ' tooltip'; tooltip = 'E-Mail ' + href; } href = 'mailto:' + href; } else { // Web Link if ( ! href.match(/^https?:\/\//) ) href = 'http://' + href; if ( f.settings.link_info ) { cls = 'html-tooltip'; var data = f._link_data && f._link_data[href]; if ( data ) { tooltip = data.tooltip; if ( data.unsafe ) cls += ' unsafe-link'; } else { f._link_data = f._link_data || {}; f._link_data[href] = true; f.ws_send("get_link", href, load_link_data.bind(f, href)); if ( f.settings.link_image_hover && is_image(href, f.settings.image_hover_all_domains) ) tooltip = image_iframe(href); } } else if ( f.settings.link_image_hover ) { cls = 'html-tooltip'; if ( is_image(href, f.settings.image_hover_all_domains) ) tooltip = image_iframe(href); } } // Deleted Links var actual_href = href; if ( token.isDeleted ) { cls = 'deleted-link ' + cls; tooltip = utils.sanitize(token.censoredHref || token.href); href = '#'; } return '' + utils.sanitize(text) + ''; } if ( token.mentionedUser ) return '' + utils.sanitize(token.mentionedUser) + ""; if ( token.deletedLink ) return utils.sanitize(token.text); return utils.sanitize(token); }).join(""); } // --------------------- // Emoticon Processing // --------------------- FFZ.prototype.tokenize_replace_emotes = function(tokens) { // Replace bad Twitch emoticons with custom emoticons. var f = this; if ( _.isString(tokens) ) tokens = [tokens]; for(var i=0; i < tokens.length; i++) { var token = tokens[i]; if ( ! token || ! token.emoticonSrc || token.ffzEmote ) continue; // Check for a few specific emoticon IDs. var emote_id = FFZ.src_to_id(token.emoticonSrc); if ( constants.EMOTE_REPLACEMENTS.hasOwnProperty(emote_id) ) { token.replacedId = emote_id; token.emoticonSrc = constants.EMOTE_REPLACEMENT_BASE + constants.EMOTE_REPLACEMENTS[emote_id]; } } return tokens; } FFZ.prototype.tokenize_title_emotes = function(tokens) { var f = this, Channel = App.__container__.lookup('controller:channel'), possible = Channel && Channel.get('product.emoticons'), emotes = []; if ( _.isString(tokens) ) tokens = [tokens]; // Build a list of emotes that match. _.each(_.union(f.__twitch_global_emotes||[], possible), function(emote) { if ( ! emote || emote.state === "inactive" ) return; var r = new RegExp("\\b" + emote.regex + "\\b"); _.any(tokens, function(token) { return _.isString(token) && token.match(r); }) && emotes.push(emote); }); // Include Global Emotes~! if ( f.__twitch_global_emotes === undefined || f.__twitch_global_emotes === null ) { f.__twitch_global_emotes = false; Twitch.api.get("chat/emoticon_images", {emotesets:"0,42"}).done(function(data) { if ( ! data || ! data.emoticon_sets || ! data.emoticon_sets[0] ) { f.__twitch_global_emotes = []; return; } var emotes = f.__twitch_global_emotes = []; data = data.emoticon_sets[0]; for(var i=0; i < data.length; i++) { var em = data[i]; emotes.push({regex: em.code, url: TWITCH_BASE + em.id + "/1.0"}); } if ( f._cindex ) f._cindex.ffzFixTitle(); }).fail(function() { setTimeout(function(){f.__twitch_global_emotes = null;},5000); });; } if ( ! emotes.length ) return tokens; if ( typeof tokens === "string" ) tokens = [tokens]; _.each(emotes, function(emote) { var eo = {isEmoticon:true, srcSet: emote.url + ' 1x', emoticonSrc: emote.url, altText: emote.regex}; var r = new RegExp("\\b" + emote.regex + "\\b"); tokens = _.compact(_.flatten(_.map(tokens, function(token) { if ( _.isObject(token) ) return token; var tbits = token.split(r), bits = []; tbits.forEach(function(val, ind) { bits.push(val); if ( ind !== tbits.length - 1 ) bits.push(eo); }); return bits; }))); }); return tokens; } FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { var f = this; // Get our sets. var sets = this.getEmotes(user, room), emotes = []; // Build a list of emotes that match. _.each(sets, function(set_id) { var set = f.emote_sets[set_id]; if ( ! set ) return; _.each(set.emoticons, function(emote) { _.any(tokens, function(token) { return _.isString(token) && token.match(emote.regex); }) && emotes.push(emote); }); }); // Don't bother proceeding if we have no emotes. if ( ! emotes.length ) return tokens; // Now that we have all the matching tokens, do crazy stuff. if ( typeof tokens === "string" ) tokens = [tokens]; // This is weird stuff I basically copied from the old Twitch code. // Here, for each emote, we split apart every text token and we // put it back together with the matching bits of text replaced // with an object telling Twitch's line template how to render the // emoticon. _.each(emotes, function(emote) { var eo = { srcSet: emote.srcSet, emoticonSrc: emote.urls[1], ffzEmote: emote.id, ffzEmoteSet: emote.set_id, altText: (emote.hidden ? "???" : emote.name) }; tokens = _.compact(_.flatten(_.map(tokens, function(token) { if ( _.isObject(token) ) return token; var tbits = token.split(emote.regex), bits = []; while(tbits.length) { var bit = tbits.shift(); if ( tbits.length ) { bit += tbits.shift(); if ( bit ) bits.push(bit); tbits.shift(); bits.push(eo); if ( do_report && room ) f.add_usage(room, emote.id); } else bits.push(bit); } return bits; }))); }); return tokens; } // --------------------- // 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]; if ( data ) bits.push(data.token); else bits.push(match + (variant || "")); } } } return bits; }))); } // --------------------- // Mention Parsing // --------------------- FFZ._regex_cache = {}; FFZ._get_regex = function(word) { return FFZ._regex_cache[word] = FFZ._regex_cache[word] || RegExp("\\b" + reg_escape(word) + "\\b", "ig"); } FFZ._words_to_regex = function(list) { var regex = FFZ._regex_cache[list]; if ( ! regex ) { var reg = ""; for(var i=0; i < list.length; i++) { if ( ! list[i] ) continue; reg += (reg ? "|" : "") + reg_escape(list[i]); } regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig"); } return regex; } FFZ.prototype.tokenize_mentions = function(tokens) { var mention_words = this.settings.keywords; if ( ! mention_words || ! mention_words.length ) return tokens; if ( typeof tokens === "string" ) tokens = [tokens]; var regex = FFZ._words_to_regex(mention_words), new_tokens = []; for(var i=0; i < tokens.length; i++) { var token = tokens[i]; if ( ! _.isString(token) ) { new_tokens.push(token); continue; } if ( ! token.match(regex) ) { new_tokens.push(token); continue; } token = token.replace(regex, function(all, prefix, match) { new_tokens.push(prefix); new_tokens.push({ mentionedUser: match, own: false }); return ""; }); if ( token ) new_tokens.push(token); } 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(); } },{"./constants":5,"./utils":35}],23:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"); // ------------------- // About Page // ------------------- FFZ.menu_pages.about_changelog = { name: "Changelog", visible: false, wide: true, render: function(view, container) { var heading = document.createElement('div'); heading.className = 'chat-menu-content center'; heading.innerHTML = '

FrankerFaceZ

change log
'; jQuery.ajax(constants.SERVER + "script/changelog.html", {cache: false, context: this}) .done(function(data) { container.appendChild(heading); container.innerHTML += data; }).fail(function(data) { var content = document.createElement('div'); content.className = 'chat-menu-content menu-side-padding'; content.textContent = 'There was an error loading the change log from the server.'; container.appendChild(heading); container.appendChild(content); }); } }; FFZ.menu_pages.about = { name: "About", icon: constants.HEART, sort_order: 100000, render: function(view, container, inner, menu) { var room = this.rooms[view.get("context.currentRoom.id")], has_emotes = false, f = this; // Check for emoticons. if ( room && room.set ) { var set = this.emote_sets[room.set]; if ( set && set.count > 0 ) has_emotes = true; } // Heading var heading = document.createElement('div'), content = ''; content += "

FrankerFaceZ

"; content += '
new ways to woof
'; heading.className = 'chat-menu-content center'; heading.innerHTML = content; container.appendChild(heading); var clicks = 0, head = heading.querySelector("h1"); head && head.addEventListener("click", function() { head.style.cursor = "pointer"; clicks++; if ( clicks >= 3 ) { clicks = 0; var el = document.querySelector(".app-main") || document.querySelector(".ember-chat-container"); el && el.classList.toggle('ffz-flip'); } setTimeout(function(){clicks=0;head.style.cursor=""},2000); }); // Advertising var btn_container = document.createElement('div'), ad_button = document.createElement('a'), message = "To use custom emoticons in " + (has_emotes ? "this channel" : "tons of channels") + ", get FrankerFaceZ from http://www.frankerfacez.com"; ad_button.className = 'button primary'; ad_button.innerHTML = "Advertise in Chat"; ad_button.addEventListener('click', this._add_emote.bind(this, view, message)); btn_container.appendChild(ad_button); // Donate var donate_button = document.createElement('a'); donate_button.className = 'button ffz-donate'; donate_button.href = "https://www.frankerfacez.com/donate"; donate_button.target = "_new"; donate_button.innerHTML = "Donate"; btn_container.appendChild(donate_button); btn_container.className = 'chat-menu-content center'; container.appendChild(btn_container); // Credits var credits = document.createElement('div'); content = ''; content += ''; content += ''; content += ''; content += ''; credits.className = 'chat-menu-content center'; credits.innerHTML = content; // Functional Changelog credits.querySelector('#ffz-changelog').addEventListener('click', function() { f._ui_change_page(view, inner, menu, container, 'about_changelog'); }); // Make the Logs button functional. var getting_logs = false; credits.querySelector('#ffz-debug-logs').addEventListener('click', function() { if ( getting_logs ) return; getting_logs = true; f._pastebin(f._log_data.join("\n"), function(url) { getting_logs = false; if ( ! url ) alert("There was an error uploading the FrankerFaceZ logs."); else prompt("Your FrankerFaceZ logs have been uploaded to the URL:", url); }); }); container.appendChild(credits); } } },{"../constants":5}],24:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"); // --------------------- // Settings // --------------------- FFZ.basic_settings.dark_twitch = { type: "boolean", no_bttv: true, category: "General", name: "Dark Twitch", help: "Apply a dark background to channels and other related pages for easier viewing.", get: function() { return this.settings.dark_twitch; }, set: function(val) { this.settings.set('dark_twitch', val); this.settings.set('dark_no_blue', val); } }; FFZ.basic_settings.separated_chat = { type: "boolean", no_bttv: true, category: "Chat", name: "Separated Lines", help: "Use alternating rows and thin lines to visually separate chat messages for easier reading.", get: function() { return this.settings.chat_rows && this.settings.chat_separators !== '0'; }, set: function(val) { this.settings.set('chat_rows', val); this.settings.set('chat_separators', val ? '2' : '0'); } }; FFZ.basic_settings.minimalistic_chat = { type: "boolean", category: "Chat", name: "Minimalistic UI", help: "Hide all of chat except messages and the input box and reduce chat margins.", get: function() { return this.settings.minimal_chat && this.settings.chat_padding; }, set: function(val) { this.settings.set('minimal_chat', val); this.settings.set('chat_padding', val); } }; FFZ.basic_settings.high_contrast = { type: "boolean", category: "Chat", no_bttv: true, name: "High Contrast", help: "Display chat using white and black for maximum contrast. This is suitable for capturing and chroma keying chat to display on stream.", get: function() { return this.settings.high_contrast_chat !== '222'; }, set: function(val) { this.settings.set('high_contrast_chat', val ? '111': '222'); } }; FFZ.basic_settings.keywords = { type: "button", category: "Chat", no_bttv: true, name: "Highlight Keywords", help: "Set additional keywords that will be highlighted in chat.", method: function() { FFZ.settings_info.keywords.method.bind(this)(); } }; FFZ.basic_settings.banned_words = { type: "button", category: "Chat", no_bttv: true, name: "Banned Keywords", help: "Set a list of words that will be removed from chat messages, locally.", method: function() { FFZ.settings_info.banned_words.method.bind(this)(); } }; FFZ.settings_info.twitch_chat_dark = { type: "boolean", value: false, visible: false }; FFZ.settings_info.dark_twitch = { type: "boolean", value: false, no_bttv: true, //visible: function() { return ! this.has_bttv }, category: "Appearance", name: "Dark Twitch", help: "Apply a dark background to channels and other related pages for easier viewing.", on_update: function(val) { var cb = document.querySelector('input.ffz-setting-dark-twitch'); if ( cb ) cb.checked = val; if ( this.has_bttv ) return; document.body.classList.toggle("ffz-dark", val); var model = window.App ? App.__container__.lookup('controller:settings').get('model') : undefined; if ( val ) { this._load_dark_css(); model && this.settings.set('twitch_chat_dark', model.get('darkMode')); model && model.set('darkMode', true); } else model && model.set('darkMode', this.settings.twitch_chat_dark); } }; FFZ.settings_info.dark_no_blue = { type: "boolean", value: false, //no_bttv: true, category: "Appearance", name: "Gray Chat (no blue)", help: "Make the dark theme for chat and a few other places on Twitch a bit darker and not at all blue.", on_update: function(val) { document.body.classList.toggle("ffz-no-blue", val); } }; FFZ.settings_info.hide_recent_past_broadcast = { type: "boolean", value: false, //no_bttv: true, no_mobile: true, category: "Channel Metadata", name: "Hide \"Watch Last Broadcast\"", help: "Hide the \"Watch Last Broadcast\" banner at the top of offline Twitch channels.", on_update: function(val) { document.body.classList.toggle("ffz-hide-recent-past-broadcast", val); } }; // --------------------- // Initialization // --------------------- FFZ.prototype.setup_dark = function() { document.body.classList.toggle("ffz-hide-recent-past-broadcast", this.settings.hide_recent_past_broadcast); document.body.classList.toggle("ffz-no-blue", this.settings.dark_no_blue); if ( this.has_bttv ) return; document.body.classList.toggle("ffz-dark", this.settings.dark_twitch); if ( ! this.settings.dark_twitch ) return; window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true); this._load_dark_css(); } FFZ.prototype._load_dark_css = function() { if ( this._dark_style ) return; this.log("Injecting FrankerFaceZ Dark Twitch CSS."); var s = this._dark_style = document.createElement('link'); s.id = "ffz-dark-css"; s.setAttribute('rel', 'stylesheet'); s.setAttribute('href', constants.SERVER + "script/dark.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); } },{"../constants":5}],25:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'), constants = require('../constants'), FOLLOW_GRAVITY = function(f, el) { return (f.settings.following_count && el.parentElement.getAttribute('data-name') === 'following' ? 'n' : '') + (f.settings.swap_sidebars ? 'e' : 'w'); }, WIDE_TIP = function(f, el) { return ( ! f.settings.following_count || (el.id !== 'header_following' && el.parentElement.getAttribute('data-name') !== 'following') ) ? '' : 'ffz-wide-tip'; }; FFZ.settings_info.following_count = { type: "boolean", value: true, no_mobile: true, category: "Appearance", name: "Sidebar Following Data", help: "Display the number of live channels you're following on the sidebar, and list the channels in a tooltip.", on_update: function(val) { this._schedule_following_count(); var Stream = window.App && App.__container__.resolve('model:stream'), Live = Stream && Stream.find("live"); if ( Live ) { var total = Live.get('total') || 0; this._draw_following_count(total); this._draw_following_channels(Live.get('content'), total);; } else { this._update_following_count(); this._draw_following_channels(); } } }; // --------------- // Initialization // --------------- FFZ.prototype.setup_following_count = function(has_ember) { // Start it updating. if ( this.settings.following_count ) this._schedule_following_count(); // Tooltips~! this._install_following_tooltips(); // If we don't have Ember, no point in trying this stuff. if ( ! has_ember ) return this._update_following_count(); this.log("Connecting to Live Streams model."); var Stream = window.App && App.__container__.resolve('model:stream'); if ( ! Stream ) return this.log("Unable to find Stream model."); var Live = Stream.find("live"), f = this; if ( ! Live ) return this.log("Unable to find Live Streams collection."); Live.addObserver('total', function() { f._draw_following_count(this.get('total')); }); Live.addObserver('content.length', function() { f._draw_following_channels(this.get('content'), this.get('total')); }) Live.load(); var total = Live.get('total'), streams = Live.get('content'); if ( typeof total === "number" ) { this._draw_following_count(total); if ( streams && streams.length ) this._draw_following_channels(streams, total); } } FFZ.prototype._schedule_following_count = function() { if ( ! this.settings.following_count ) { if ( this._following_count_timer ) { clearTimeout(this._following_count_timer); this._following_count_timer = undefined; } return; } if ( ! this._following_count_timer ) this._following_count_timer = setTimeout(this._update_following_count.bind(this), 55000 + (10000*Math.random())); } FFZ.prototype._update_following_count = function() { if ( ! this.settings.following_count ) { if ( this._following_count_timer ) { clearTimeout(this._following_count_timer); this._following_count_timer = undefined; } return; } this._following_count_timer = setTimeout(this._update_following_count.bind(this), 55000 + (10000*Math.random())); var Stream = window.App && App.__container__.resolve('model:stream'), Live = Stream && Stream.find("live"), f = this; if ( Live ) Live.load(); else Twitch.api && Twitch.api.get("streams/followed", {limit:5, offset:0}, {version:3}) .done(function(data) { f._draw_following_count(data._total); f._draw_following_channels(data.streams, data._total); }).fail(function() { f._draw_following_count(); f._draw_following_channels(); }) } FFZ.prototype._build_following_tooltip = function(el) { if ( el.id !== 'header_following' && el.parentElement.getAttribute('data-name') !== 'following' ) return el.getAttribute('original-title'); if ( ! this.settings.following_count ) return 'Following'; var tooltip = (this.has_bttv ? 'FrankerFaceZ' : '') + 'Following', bb = el.getBoundingClientRect(), height = document.body.clientHeight - (bb.bottom + 54), max_lines = Math.max(Math.floor(height / 36) - 1, 2), streams = this._tooltip_streams, total = this._tooltip_total || (streams && streams.length) || 0; if ( streams && streams.length ) { var c = 0; for(var i=0, l = streams.length; i < l; i++) { var stream = streams[i]; if ( ! stream || ! stream.channel ) continue; c += 1; if ( c > max_lines ) { tooltip += '
And ' + utils.number_commas(total - max_lines) + ' more...'; break; } var up_since = this.settings.stream_uptime && stream.created_at && utils.parse_date(stream.created_at), uptime = up_since && Math.floor((Date.now() - up_since.getTime()) / 1000) || 0, minutes = Math.floor(uptime / 60) % 60, hours = Math.floor(uptime / 3600); tooltip += (i === 0 ? '
' : '') + (uptime > 0 ? '' + constants.CLOCK + ' ' + (hours > 0 ? hours + 'h' : '') + minutes + 'm' : '') + '' + constants.LIVE + ' ' + utils.number_commas(stream.viewers) + '' + '' + utils.sanitize(stream.channel.display_name || stream.channel.name) + '
' + '' + (stream.channel.game ? 'Playing ' + utils.sanitize(stream.channel.game) : 'Not Playing') + ''; } } else tooltip += "
No one you're following is online."; // Reposition the tooltip. setTimeout(function() { var tip = document.querySelector('.tipsy'), bb = tip.getBoundingClientRect(), left = parseInt(tip.style.left || '0'), right = bb.left + tip.scrollWidth; if ( bb.left < 5 ) tip.style.left = (left - bb.left) + 5 + 'px'; else if ( right > document.body.clientWidth - 5 ) tip.style.left = (left - (5 + right - document.body.clientWidth)) + 'px'; }); return tooltip; } FFZ.prototype._install_following_tooltips = function() { var f = this, data = { html: true, className: function() { return WIDE_TIP(f, this); }, title: function() { return f._build_following_tooltip(this); } }; // Small var small_following = jQuery('#small_nav ul.game_filters li[data-name="following"] a'); if ( small_following && small_following.length ) { var td = small_following.data('tipsy'); if ( td && td.options ) { td.options = _.extend(td.options, data); td.options.gravity = function() { return FOLLOW_GRAVITY(f, this); }; } else small_following.tipsy(_.extend({gravity: function() { return FOLLOW_GRAVITY(f, this); }}, data)); } // Large var large_following = jQuery('#large_nav #nav_personal li[data-name="following"] a'); if ( large_following && large_following.length ) { var td = large_following.data('tipsy'); if ( td && td.options ) td.options = _.extend(td.options, data); else large_following.tipsy(data); } // Heading var head_following = jQuery('#header_actions #header_following'); if ( head_following && head_following.length ) { var td = head_following.data('tipsy'); if ( td && td.options ) td.options = _.extend(td.options, data); else head_following.tipsy(data); } } FFZ.prototype._draw_following_channels = function(streams, total) { this._tooltip_streams = streams; this._tooltip_total = total; } FFZ.prototype._draw_following_count = function(count) { // Small var small_following = document.querySelector('#small_nav ul.game_filters li[data-name="following"] a'); if ( small_following ) { var badge = small_following.querySelector('.ffz-follow-count'); if ( this.has_bttv || ! this.settings.following_count ) { if ( badge ) badge.parentElement.removeChild(badge); } else { if ( ! badge ) { badge = document.createElement('span'); badge.className = 'ffz-follow-count'; small_following.appendChild(badge); } badge.innerHTML = count ? utils.format_unread(count) : ''; } } // Large var large_following = document.querySelector('#large_nav #nav_personal li[data-name="following"] a'); if ( large_following ) { var badge = large_following.querySelector('.ffz-follow-count'); if ( this.has_bttv || ! this.settings.following_count ) { if ( badge ) badge.parentElement.removeChild(badge); } else { if ( ! badge ) { badge = document.createElement('span'); badge.className = 'ffz-follow-count'; large_following.appendChild(badge); } badge.innerHTML = count ? utils.format_unread(count) : ''; } } // Heading var head_following = document.querySelector('#header_actions #header_following'); if ( head_following ) { var badge = head_following.querySelector('.ffz-follow-count'); if ( this.has_bttv || ! this.settings.following_count ) { if ( badge ) badge.parentElement.removeChild(badge); } else { if ( ! badge ) { badge = document.createElement('span'); badge.className = 'ffz-follow-count'; head_following.appendChild(badge); } badge.innerHTML = count ? utils.format_unread(count) : ''; } } } },{"../constants":5,"../utils":35}],26:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'), VALID_CHANNEL = /^[A-Za-z0-9_]+$/, TWITCH_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([A-Za-z0-9_]+)/i; // --------------- // 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, no_mobile: 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().toLowerCase().split(/[ ,]+/); var out = []; for(var i=0,l=args.length; i span'); } catch(err) { before = undefined; } if ( before ) container.insertBefore(cont, before); else container.appendChild(cont); } else cont.innerHTML = ''; var processed = [channel_id]; for(var i=0; i < data.length && i < 10; i++) { var cid = data[i]; if ( processed.indexOf(cid) !== -1 ) continue; this._build_following_button(cont, cid); processed.push(cid); } } } 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; try { before = container.querySelector(':scope > span'); } catch(err) { before = undefined; } if ( before ) container.insertBefore(cont, before); else container.appendChild(cont); } else cont.innerHTML = ''; var processed = [hosted_id]; for(var i=0; i < data.length && i < 10; i++) { var cid = data[i]; if ( processed.indexOf(cid) !== -1 ) continue; this._build_following_button(cont, cid); processed.push(cid); } } } } // --------------- // UI Construction // --------------- FFZ.prototype._build_following_button = function(container, channel_id) { if ( ! VALID_CHANNEL.test(channel_id) ) return this.log("Ignoring Invalid Channel: " + utils.sanitize(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 " + utils.sanitize(display_name); btn.innerHTML = (following ? "" : "Follow ") + utils.sanitize(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(e) { 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; }); btn.addEventListener('mousedown', function(e) { if ( e.button !== 1 ) return; e.preventDefault(); window.open(Twitch.uri.profile(channel_id)); }); 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(); setTimeout(check_following, Math.random()*5000); 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":35}],27:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", fix_menu_position = function(container) { var swapped = document.body.classList.contains('ffz-sidebar-swap'); var bounds = container.getBoundingClientRect(), left = parseInt(container.style.left || '0'), right = bounds.left + container.scrollWidth, moved = !!container.style.left; if ( swapped ) { if ( bounds.left < 20 ) { container.style.left = ''; moved = false; } else if ( right > document.body.clientWidth ) container.style.left = (left - (right - document.body.clientWidth)) + 'px'; } else { if ( bounds.left < 0 ) container.style.left = (left - bounds.left) + 'px'; else if ( right > (document.body.clientWidth - 20) ) { container.style.left = ''; moved = false; } } container.classList.toggle('ui-moved', moved); }; // -------------------- // Initializer // -------------------- FFZ.prototype.setup_menu = function() { this.log("Installing mouse-up event to auto-close menus."); var f = this; jQuery(document).mouseup(function(e) { var popup = f._popup, parent; if ( ! popup ) return; if ( popup.id === 'ffz-chat-menu' && popup.style && popup.style.left ) return; popup = jQuery(popup); parent = popup.parent(); if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { popup.remove(); delete f._popup; f._popup_kill && f._popup_kill(); delete f._popup_kill; } }); 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 = window.App && 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); }); // Channel Hosting p = document.createElement('p'); //p.className = 'no-bttv'; cb = document.createElement('input'); cb.type = "checkbox"; cb.className = "ember-checkbox ffz-setting-hosted-channels"; cb.checked = f.settings.hosted_channels; p.appendChild(cb); p.appendChild(document.createTextNode("Channel Hosting")); content.appendChild(p); cb.addEventListener("change", function(e) { f.settings.set("hosted_channels", this.checked); }); // 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); } } } FFZ.menu_pages = {}; // -------------------- // Create Menu // -------------------- FFZ.prototype._fix_menu_position = function() { var container = document.querySelector('#ffz-chat-menu'); if ( container ) fix_menu_position(container); } FFZ.prototype.build_ui_popup = function(view) { var popup = this._popup; if ( popup ) { popup.parentElement.removeChild(popup); delete this._popup; this._popup_kill && this._popup_kill(); delete this._popup_kill; return; } // Start building the DOM. var container = document.createElement('div'), inner = document.createElement('div'), menu = document.createElement('ul'), dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false); container.className = 'emoticon-selector chat-menu ffz-ui-popup'; container.id = 'ffz-chat-menu'; inner.className = 'emoticon-selector-box dropmenu'; container.appendChild(inner); container.classList.toggle('dark', dark); // Menu Container var sub_container = document.createElement('div'); sub_container.className = 'ffz-ui-menu-page'; inner.appendChild(sub_container); // Render Menu Tabs menu.className = 'menu clearfix'; inner.appendChild(menu); var heading = document.createElement('li'); heading.className = 'title'; heading.innerHTML = 'Franker' + (constants.DEBUG ? 'Dev' : 'Face') + 'Z'; // Close Button var close_btn = document.createElement('span'), f = this; close_btn.className = 'ffz-handle ffz-close-button'; heading.insertBefore(close_btn, heading.firstChild); var can_close = false; close_btn.addEventListener('mousedown', function() { var popup = f._popup; can_close = popup && popup.id === "ffz-chat-menu" && popup.style.left; }); close_btn.addEventListener('click', function() { var popup = f._popup; if ( can_close && popup ) { popup.parentElement.removeChild(popup); delete f._popup; f._popup_kill && f._popup_kill(); delete f._popup_kill; } }); menu.appendChild(heading); // Draggable jQuery(container).draggable({ handle: menu, cancel: 'li.item', axis:"x", stop: function(e) { fix_menu_position(this); } }); // Get rid of the position: relative that draggable adds. container.style.position = ''; var menu_pages = []; for(var key in FFZ.menu_pages) { if ( ! FFZ.menu_pages.hasOwnProperty(key) ) continue; var page = FFZ.menu_pages[key]; try { if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)(view)))) ) continue; } catch(err) { this.error("menu_pages " + key + " visible: " + err); continue; } menu_pages.push([page.sort_order || 0, key, page]); } menu_pages.sort(function(a,b) { if ( a[0] < b[0] ) return 1; else if ( a[0] > b[0] ) return -1; var al = a[1].toLowerCase(), bl = b[1].toLowerCase(); if ( al < bl ) return 1; if ( al > bl ) return -1; return 0; }); for(var i=0; i < menu_pages.length; i++) { var key = menu_pages[i][1], page = menu_pages[i][2], el = document.createElement('li'), link = document.createElement('a'); el.className = 'item' + (page.sub_menu ? ' has-sub-menu' : ''); el.id = "ffz-menu-page-" + key; link.title = page.name; link.innerHTML = page.icon; jQuery(link).tipsy(); link.addEventListener("click", this._ui_change_page.bind(this, view, inner, menu, sub_container, key)); el.appendChild(link); menu.appendChild(el); } // Render Current Page var page = (this._last_page || "channel").split("_", 1)[0]; this._ui_change_page(view, inner, menu, sub_container, page); // Add the menu to the DOM. this._popup = container; sub_container.style.maxHeight = Math.max(200, view.$().height() - 172) + "px"; view.$('.chat-interface').append(container); } FFZ.prototype._ui_change_page = function(view, inner, menu, container, page) { this._last_page = page; container.innerHTML = ""; container.setAttribute('data-page', page); // Allow settings to be wide. We need to know if chat is stand-alone. var app = document.querySelector(".app-main") || document.querySelector(".ember-chat-container"); inner.style.maxWidth = (!FFZ.menu_pages[page].wide || (typeof FFZ.menu_pages[page].wide == "function" && !FFZ.menu_pages[page].wide.bind(this)())) ? "" : (app.offsetWidth < 640 ? (app.offsetWidth-40) : 600) + "px"; var els = menu.querySelectorAll('li.active'); for(var i=0; i < els.length; i++) els[i].classList.remove('active'); var el = menu.querySelector('#ffz-menu-page-' + page); if ( el ) el.classList.add('active'); else this.log("No matching page: " + page); FFZ.menu_pages[page].render.bind(this)(view, container, inner, menu); // Re-position if necessary. var f = this; setTimeout(function(){f._fix_menu_position();}); } // -------------------- // Channel Page // -------------------- FFZ.menu_pages.channel = { render: function(view, inner) { // Get the current room. var room_id = view.get('controller.currentRoom.id'), room = this.rooms[room_id], has_product = false, f = this; // Check for a product. if ( this.settings.replace_twitch_menu ) { var product = room.room.get("product"); if ( product && !product.get("error") ) { // We have a product, and no error~! 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"), header = document.createElement("div"), c = 0; // 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 ) { 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 ) header.style.backgroundImage = 'url("' + icon + '")'; header.innerHTML = 'TwitchSubscriber Emoticons'; grid.appendChild(header); for(var emotes=product.get("emoticons") || [], i=0; i < emotes.length; i++) { var emote = emotes[i]; if ( emote.state !== "active" ) continue; var s = document.createElement('span'), can_use = is_subscribed || !emote.subscriber_only, img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; s.className = 'emoticon tooltip' + (!can_use ? " locked" : ""); s.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")'; s.style.backgroundImage = '-webkit-' + img_set; s.style.backgroundImage = '-moz-' + img_set; s.style.backgroundImage = '-ms-' + img_set; s.style.backgroundImage = img_set; s.style.width = emote.width + "px"; s.style.height = emote.height + "px"; s.title = emote.regex; s.addEventListener('click', function(can_use, id, code, e) { if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) window.open("https://twitchemotes.com/emote/" + id); else if ( can_use ) this._add_emote(view, code); else return; e.preventDefault(); }.bind(this, can_use, emote.id, emote.regex)); grid.appendChild(s); c++; } if ( c > 0 ) inner.appendChild(grid); if ( c > 0 && ! is_subscribed ) { var sub_message = document.createElement("div"), nonsub_message = document.createElement("div"), unlock_text = document.createElement("span"), sub_link = document.createElement("a"); sub_message.className = "subscribe-message"; nonsub_message.className = "non-subscriber-message"; sub_message.appendChild(nonsub_message); unlock_text.className = "unlock-text"; unlock_text.innerHTML = "Subscribe to unlock Emoticons"; nonsub_message.appendChild(unlock_text); sub_link.className = "action subscribe-button button primary"; sub_link.href = product.get("product_url"); sub_link.innerHTML = ''; nonsub_message.appendChild(sub_link); inner.appendChild(sub_message); } 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 ) { var ends_at = utils.parse_date(last_content.access_end || ""); sub_message = document.createElement("div"), nonsub_message = document.createElement("div"), unlock_text = document.createElement("span"), end_time = ends_at ? Math.floor((ends_at.getTime() - Date.now()) / 1000) : null; sub_message.className = "subscribe-message"; nonsub_message.className = "non-subscriber-message"; sub_message.appendChild(nonsub_message); unlock_text.className = "unlock-text"; unlock_text.innerHTML = "Subscription expires in " + utils.time_to_string(end_time, true, true); nonsub_message.appendChild(unlock_text); inner.appendChild(sub_message); } } } } // 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 || 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); }, name: "Channel", icon: constants.ZREKNARF }; // -------------------- // Emotes for Sets // -------------------- FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub_text) { var grid = document.createElement('div'), c = 0, f = this; grid.className = 'emoticon-grid'; if ( header != null ) { var el_header = document.createElement('div'); el_header.className = 'heading'; if ( sub_text ) { var s = document.createElement("span"); s.className = "right"; s.appendChild(document.createTextNode(sub_text)); el_header.appendChild(s); } el_header.appendChild(document.createTextNode(header)); if ( image ) el_header.style.backgroundImage = 'url("' + image + '")'; grid.appendChild(el_header); } var emotes = []; for(var i=0; i < sets.length; i++) { var set = this.emote_sets[sets[i]]; if ( ! set || ! set.emoticons ) continue; for(var eid in set.emoticons) { if ( ! set.emoticons.hasOwnProperty(eid) || set.emoticons[eid].hidden ) continue; emotes.push(set.emoticons[eid]); } } // Sort the emotes! emotes.sort(function(a,b) { var an = a.name.toLowerCase(), bn = b.name.toLowerCase(); if ( an < bn ) return -1; else if ( an > bn ) return 1; return 0; }); for(var i=0; i < emotes.length; i++) { var emote = emotes[i], srcset = null; if ( emote.urls[2] || emote.urls[4] ) { srcset = 'url("' + emote.urls[1] + '") 1x'; if ( emote.urls[2] ) srcset += ', url("' + emote.urls[2] + '") 2x'; if ( emote.urls[4] ) srcset += ', url("' + emote.urls[4] + '") 4x'; } c++; var s = document.createElement('span'); s.className = 'emoticon tooltip'; s.style.backgroundImage = 'url("' + emote.urls[1] + '")'; if ( srcset ) { var img_set = 'image-set(' + srcset + ')'; s.style.backgroundImage = '-webkit-' + img_set; s.style.backgroundImage = '-moz-' + img_set; s.style.backgroundImage = '-ms-' + img_set; s.style.backgroundImage = img_set; } s.style.width = emote.width + "px"; s.style.height = emote.height + "px"; s.title = this._emote_tooltip(emote); s.addEventListener('click', function(id, code, e) { e.preventDefault(); if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) window.open("https://www.frankerfacez.com/emoticons/" + id); else this._add_emote(view, code); }.bind(this, emote.id, emote.name)); grid.appendChild(s); } if ( !c ) { grid.innerHTML += "This channel has no emoticons."; grid.className = "emoticon-grid ffz-no-emotes center"; } parent.appendChild(grid); } FFZ.prototype._add_emote = function(view, emote) { var input_el, text, room; if ( this.has_bttv ) { input_el = view.get('element').querySelector('textarea'); text = input_el.value; } else { room = view.get('controller.currentRoom'); text = room.get('messageToSend') || ''; } text += (text && text.substr(-1) !== " " ? " " : "") + (emote.name || emote); if ( input_el ) input_el.value = text; else room.set('messageToSend', text); } },{"../constants":5,"../utils":35}],28:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); // -------------------- // Initialization // -------------------- FFZ.prototype.build_ui_link = function(view) { var link = document.createElement('a'); link.className = 'ffz-ui-toggle'; link.innerHTML = constants.CHAT_BUTTON; link.addEventListener('click', this.build_ui_popup.bind(this, view)); this.update_ui_link(link); return link; } FFZ.prototype.update_ui_link = function(link) { var controller = window.App && App.__container__.lookup('controller:chat'); link = link || document.querySelector('a.ffz-ui-toggle'); if ( !link || !controller ) return; var room_id = controller.get('currentRoom.id'), room = this.rooms[room_id], has_emotes = false, dark = (this.has_bttv ? BetterTTV.settings.get('darkenedMode') : false), blue = (this.has_bttv ? BetterTTV.settings.get('showBlueButtons') : false), live = (this.feature_friday && this.feature_friday.live); // Check for emoticons. if ( room && room.set ) { var set = this.emote_sets[room.set]; if ( set && set.count > 0 ) has_emotes = true; } link.classList.toggle('no-emotes', ! has_emotes); link.classList.toggle('live', live); link.classList.toggle('dark', dark); link.classList.toggle('blue', blue); } },{"../constants":5}],29:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require("../constants"), utils = require("../utils"), TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", BANNED_SETS = {"00000turbo":true}; // ------------------- // Initialization // ------------------- FFZ.basic_settings.replace_twitch_menu = { type: "boolean", category: "Chat", name: "Unified Emoticons Menu", help: "Completely replace the default Twitch emoticon menu and display global emoticons in the My Emoticons menu.", get: function() { return this.settings.replace_twitch_menu && this.settings.global_emotes_in_menu && this.settings.emoji_in_menu; }, set: function(val) { this.settings.set('replace_twitch_menu', val); this.settings.set('global_emotes_in_menu', val); this.settings.set('emoji_in_menu', val); } }; FFZ.settings_info.replace_twitch_menu = { type: "boolean", value: false, category: "Chat Input", name: "Replace Twitch Emoticon Menu", help: "Completely replace the default Twitch emoticon menu.", on_update: function(val) { document.body.classList.toggle("ffz-menu-replace", val); } }; FFZ.settings_info.global_emotes_in_menu = { type: "boolean", value: false, category: "Chat Input", name: "Display Global Emotes in My Emotes", help: "Display the global Twitch emotes in the My Emoticons menu." }; FFZ.settings_info.emoji_in_menu = { type: "boolean", value: false, category: "Chat Input", name: "Display Emoji in My Emotes", help: "Display the supported emoji images in the My Emoticons menu." }; FFZ.settings_info.emote_menu_collapsed = { value: [], visible: false } FFZ.prototype.setup_my_emotes = function() { this._twitch_badges = {}; 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"; } // ------------------- // Menu Page // ------------------- FFZ.menu_pages.myemotes = { name: "My Emoticons", icon: constants.EMOTE, visible: function(view) { var user = this.get_user(), tmi = view.get('controller.currentRoom.tmiSession'), ffz_sets = user && this.users[user.login] && this.users[user.login].sets || [], twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets']; return ffz_sets.length || (twitch_sets && Object.keys(twitch_sets).length); }, render: function(view, container) { var tmi = view.get('controller.currentRoom.tmiSession'), twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets']; // We don't have to do async stuff anymore cause we pre-load data~! return FFZ.menu_pages.myemotes.draw_menu.bind(this)(view, container, twitch_sets); }, toggle_section: function(heading) { var menu = heading.parentElement, set_id = menu.getAttribute('data-set'), collapsed_list = this.settings.emote_menu_collapsed, is_collapsed = collapsed_list.indexOf(set_id) !== -1; if ( is_collapsed ) collapsed_list.removeObject(set_id); else collapsed_list.push(set_id); this.settings.set('emote_menu_collapsed', collapsed_list); menu.classList.toggle('collapsed', !is_collapsed); }, draw_emoji: function(view) { var heading = document.createElement('div'), menu = document.createElement('div'), f = this; heading.className = 'heading'; heading.innerHTML = 'FrankerFaceZEmoji'; heading.style.backgroundImage = 'url("' + constants.SERVER + '/emoji/1f4af-1x.png")'; menu.className = 'emoticon-grid collapsable'; menu.appendChild(heading); menu.setAttribute('data-set', 'emoji'); menu.classList.toggle('collapsed', this.settings.emote_menu_collapsed.indexOf('emoji') !== -1); heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this); }); var set = []; for(var eid in this.emoji_data) set.push(this.emoji_data[eid]); set.sort(function(a,b) { var an = a.short_name.toLowerCase(), bn = b.short_name.toLowerCase(); if ( an < bn ) return -1; else if ( an > bn ) return 1; if ( a.raw < b.raw ) return -1; if ( a.raw > b.raw ) return 1; return 0; }); for(var i=0; i < set.length; i++) { var emoji = set[i], em = document.createElement('span'), img_set = 'image-set(url("' + emoji.src + '") 1x, url("' + constants.SERVER + 'emoji/' + emoji.code + '-2x.png") 2x, url("' + constants.SERVER + 'emoji/' + emoji.code + '-4x.png") 4x)'; em.className = 'emoticon tooltip'; em.title = 'Emoji: ' + emoji.raw + '\nName: :' + emoji.short_name + ':'; em.addEventListener('click', this._add_emote.bind(this, view, emoji.raw)); em.style.backgroundImage = 'url("' + emoji.src + '")'; em.style.backgroundImage = '-webkit-' + img_set; em.style.backgroundImage = '-moz-' + img_set; em.style.backgroundImage = '-ms-' + img_set; em.style.backgroudnImage = img_set; menu.appendChild(em); } return menu; }, draw_twitch_set: function(view, set_id, set) { var heading = document.createElement('div'), menu = document.createElement('div'), f = this, channel_id = this._twitch_set_to_channel[set_id], title; if ( channel_id === "twitch_unknown" ) title = "Unknown Channel"; else if ( channel_id === "--global--" ) title = "Global Emoticons"; else if ( channel_id === "turbo" || channel_id === "--turbo-faces--" ) title = "Twitch Turbo"; else title = FFZ.get_capitalization(channel_id, function(name) { heading.innerHTML = 'Twitch' + utils.sanitize(name); }); heading.className = 'heading'; heading.innerHTML = 'Twitch' + utils.sanitize(title); if ( this._twitch_badges[channel_id] ) heading.style.backgroundImage = 'url("' + this._twitch_badges[channel_id] + '")'; else { var f = this; Twitch.api.get("chat/" + channel_id + "/badges", null, {version: 3}) .done(function(data) { if ( data.subscriber && data.subscriber.image ) { f._twitch_badges[channel_id] = data.subscriber.image; localStorage.ffzTwitchBadges = JSON.stringify(f._twitch_badges); heading.style.backgroundImage = 'url("' + data.subscriber.image + '")'; } }); } menu.className = 'emoticon-grid collapsable'; menu.appendChild(heading); menu.setAttribute('data-set', 'twitch-' + set_id); menu.classList.toggle('collapsed', this.settings.emote_menu_collapsed.indexOf('twitch-' + set_id) !== -1); heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this); }); set.sort(function(a,b) { var an = a.code.toLowerCase(), bn = b.code.toLowerCase(); if ( an < bn ) return -1; else if ( an > bn ) return 1; if ( a.id < b.id ) return -1; if ( a.id > b.id ) return 1; return 0; }); for(var i=0; i < set.length; i++) { var emote = set[i], code = constants.KNOWN_CODES[emote.code] || emote.code, em = document.createElement('span'), img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; em.className = 'emoticon tooltip'; if ( this.settings.replace_bad_emotes && constants.EMOTE_REPLACEMENTS[emote.id] ) { em.style.backgroundImage = 'url("' + constants.EMOTE_REPLACEMENT_BASE + constants.EMOTE_REPLACEMENTS[emote.id] + '")'; } else { em.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")'; em.style.backgroundImage = '-webkit-' + img_set; em.style.backgroundImage = '-moz-' + img_set; em.style.backgroundImage = '-ms-' + img_set; em.style.backgroudnImage = img_set; } em.title = code; em.addEventListener("click", function(id, code, e) { e.preventDefault(); if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) window.open("https://twitchemotes.com/emote/" + id); else this._add_emote(view, code); }.bind(this, emote.id, emote.code)); menu.appendChild(em); } return menu; }, draw_ffz_set: function(view, set) { var heading = document.createElement('div'), menu = document.createElement('div'), f = this, emotes = []; heading.className = 'heading'; heading.innerHTML = 'FrankerFaceZ' + set.title; heading.style.backgroundImage = 'url("' + (set.icon || '//cdn.frankerfacez.com/script/devicon.png') + '")'; menu.className = 'emoticon-grid collapsable'; menu.appendChild(heading); menu.setAttribute('data-set', 'ffz-' + set.id); menu.classList.toggle('collapsed', this.settings.emote_menu_collapsed.indexOf('ffz-' + set.id) !== -1); heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this); }); for(var emote_id in set.emoticons) set.emoticons.hasOwnProperty(emote_id) && ! set.emoticons[emote_id].hidden && emotes.push(set.emoticons[emote_id]); emotes.sort(function(a,b) { var an = a.name.toLowerCase(), bn = b.name.toLowerCase(); if ( an < bn ) return -1; else if ( an > bn ) return 1; if ( a.id < b.id ) return -1; if ( a.id > b.id ) return 1; return 0; }); for(var i=0; i < emotes.length; i++) { var emote = emotes[i], em = document.createElement('span'), img_set = 'image-set(url("' + emote.urls[1] + '") 1x'; if ( emote.urls[2] ) img_set += ', url("' + emote.urls[2] + '") 2x'; if ( emote.urls[4] ) img_set += ', url("' + emote.urls[4] + '") 4x'; img_set += ')'; em.className = 'emoticon tooltip'; em.style.backgroundImage = 'url("' + emote.urls[1] + '")'; em.style.backgroundImage = '-webkit-' + img_set; em.style.backgroundImage = '-moz-' + img_set; em.style.backgroundImage = '-ms-' + img_set; em.style.backgroudnImage = img_set; if ( emote.height ) em.style.height = emote.height + "px"; if ( emote.width ) em.style.width = emote.width + "px"; em.title = this._emote_tooltip(emote); em.addEventListener("click", function(id, code, e) { e.preventDefault(); if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) window.open("https://www.frankerfacez.com/emoticons/" + id); else this._add_emote(view, code); }.bind(this, emote.id, emote.name)); menu.appendChild(em); } return menu; }, draw_menu: function(view, container, twitch_sets) { // Make sure we're still on the My Emoticons page. Since this is // asynchronous, the user could've tabbed away. if ( container.getAttribute('data-page') !== 'myemotes' ) return; container.innerHTML = ""; try { var user = this.get_user(), ffz_sets = this.getEmotes(user && user.login, null), sets = []; // Start with Twitch Sets for(var set_id in twitch_sets) { if ( ! twitch_sets.hasOwnProperty(set_id) || ( ! this.settings.global_emotes_in_menu && set_id === '0' ) ) continue; var set = twitch_sets[set_id]; if ( ! set.length ) continue; sets.push([this._twitch_set_to_channel[set_id], FFZ.menu_pages.myemotes.draw_twitch_set.bind(this)(view, set_id, set)]); } // Emoji~! if ( this.settings.emoji_in_menu ) sets.push(["emoji", FFZ.menu_pages.myemotes.draw_emoji.bind(this)(view)]); // Now, FFZ! for(var i=0; i < ffz_sets.length; i++) { var set_id = ffz_sets[i], set = this.emote_sets[set_id]; if ( ! set || ! set.count || ( ! this.settings.global_emotes_in_menu && this.default_sets.indexOf(set_id) !== -1 ) ) continue; sets.push([set.title.toLowerCase(), FFZ.menu_pages.myemotes.draw_ffz_set.bind(this)(view, set)]); } // Finally, sort and add them all. sets.sort(function(a,b) { var an = a[0], bn = b[0]; if ( an === "turbo" || an === "--turbo-faces--" ) an = "zza|" + an; else if ( an === "global" || an === "global emoticons" || an === "--global--" ) an = "zzy|" + an; else if ( an === "emoji" ) an = "zzz|" + an; if ( bn === "turbo" || bn === "--turbo-faces--" ) bn = "zza|" + bn; else if ( bn === "global" || bn === "global emoticons" || bn === "--global--" ) bn = "zzy|" + bn; else if ( bn === "emoji" ) bn = "zzz|" + bn; if ( an < bn ) return -1; if ( an > bn ) return 1; return 0; }); for(var i=0; i < sets.length; i++) container.appendChild(sets[i][1]); } catch(err) { this.error("myemotes draw_menu: " + err); container.innerHTML = ""; var menu = document.createElement('div'), heading = document.createElement('div'), p = document.createElement('p'); heading.className = 'heading'; heading.innerHTML = 'Error Loading Menu'; menu.appendChild(heading); p.className = 'clearfix'; p.textContent = err; menu.appendChild(p); menu.className = 'chat-menu-content'; container.appendChild(menu); } } }; },{"../constants":5,"../utils":35}],30:[function(require,module,exports){ var FFZ = window.FrankerFaceZ; // --------------------- // Initialization // --------------------- FFZ.prototype.setup_notifications = function() { this.log("Adding event handler for window focus."); window.addEventListener("focus", this.clear_notifications.bind(this)); } // --------------------- // Settings // --------------------- FFZ.settings_info.highlight_notifications = { type: "boolean", value: false, category: "Chat Filtering", no_bttv: true, no_mobile: true, //visible: function() { return ! this.has_bttv }, name: "Highlight Notifications", 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 // enabled, at least. if ( ! val || ! direct ) return; if ( Notification.permission === "denied" ) { this.log("Notifications have been denied by the user."); this.settings.set("highlight_notifications", false); return; } else if ( Notification.permission === "granted" ) return; var f = this; Notification.requestPermission(function(e) { if ( e === "denied" ) { f.log("Notifications have been denied by the user."); f.settings.set("highlight_notifications", false); } }); } }; FFZ.settings_info.notification_timeout = { type: "button", value: 60, category: "Chat Filtering", no_bttv: true, no_mobile: true, name: "Notification Timeout", help: "Specify how long notifications should be displayed before automatically closing.", method: function() { var old_val = this.settings.notification_timeout, new_val = prompt("Notification Timeout\n\nPlease enter the time you'd like notifications to be displayed before automatically closing, in seconds.\n\nDefault is: 60", old_val); if ( new_val === null || new_val === undefined ) return; var parsed = parseInt(new_val); if ( parsed === NaN || parsed < 1 ) parsed = 60; this.settings.set("notification_timeout", parsed); } }; // --------------------- // Socket Commands // --------------------- FFZ.ws_commands.message = function(message) { this.show_message(message); } // --------------------- // Notifications // --------------------- FFZ._notifications = {}; FFZ._last_notification = 0; FFZ.prototype.clear_notifications = function() { for(var k in FFZ._notifications) { var n = FFZ._notifications[k]; if ( n ) try { n.close(); } catch(err) { } } FFZ._notifications = {}; FFZ._last_notification = 0; } FFZ.prototype.show_notification = function(message, title, tag, timeout, on_click, on_close) { var perm = Notification.permission; if ( perm === "denied " ) return false; if ( perm === "granted" ) { title = title || "FrankerFaceZ"; timeout = timeout || (this.settings.notification_timeout*1000); var options = { lang: "en-US", dir: "ltr", body: message, tag: tag || "FrankerFaceZ", icon: "http://cdn.frankerfacez.com/icon32.png" }; var f = this, n = new Notification(title, options), nid = FFZ._last_notification++; FFZ._notifications[nid] = n; n.addEventListener("click", function() { delete FFZ._notifications[nid]; if ( on_click ) on_click.bind(f)(); }); n.addEventListener("close", function() { delete FFZ._notifications[nid]; if ( on_close ) on_close.bind(f)(); }); if ( typeof timeout == "number" ) n.addEventListener("show", function() { setTimeout(function() { delete FFZ._notifications[nid]; n.close(); }, timeout); }); return; } var f = this; Notification.requestPermission(function(e) { f.show_notification(message, title, tag); }); } // --------------------- // Noty Notification // --------------------- FFZ.prototype.show_message = function(message) { window.noty({ text: message, theme: "ffzTheme", layout: "bottomCenter", closeWith: ["button"] }).show(); } },{}],31:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, utils = require('../utils'); // --------------- // Initialization // --------------- FFZ.prototype.setup_races = function() { this.log("Initializing race support."); this.srl_races = {}; } // --------------- // Settings // --------------- FFZ.settings_info.srl_races = { type: "boolean", value: true, no_mobile: true, category: "Channel Metadata", name: "SRL Race Information", help: 'Display information about SpeedRunsLive races under channels.', on_update: function(val) { this.rebuild_race_ui(); } }; // --------------- // 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; if ( ! controller ) return; for(var chan in this.srl_races) { delete this.srl_races[chan]; if ( chan === current_id || chan === current_host ) need_update = true; } if ( need_update ) this.rebuild_race_ui(); }); FFZ.ws_commands.srl_race = 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.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 || channel_id === current_host ) need_update = true; } if ( data[1] ) { var race = data[1], tte = race.twitch_entrants = {}; for(var ent in race.entrants) { if ( ! race.entrants.hasOwnProperty(ent) ) continue; if ( race.entrants[ent].channel ) tte[race.entrants[ent].channel] = ent; race.entrants[ent].name = ent; } } if ( need_update ) this.rebuild_race_ui(); } // --------------- // Race UI // --------------- FFZ.prototype.rebuild_race_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 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 ( hosted_id ) { var race = this.srl_races && this.srl_races[hosted_id], el = this._cindex.get('element'), container = el && el.querySelector('#hostmode .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', hosted_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, hosted_id)); race_container.appendChild(btn); container.appendChild(race_container); } this._update_race(race_container, true); } } } // --------------- // Race Popup // --------------- FFZ.prototype._race_kill = function() { if ( this._race_timer ) { clearTimeout(this._race_timer); delete this._race_timer; } delete this._race_game; delete this._race_goal; } FFZ.prototype._build_race_popup = function(container, channel_id) { var popup = this._popup; if ( popup ) { popup.parentElement.removeChild(popup); delete this._popup; this._popup_kill && this._popup_kill(); delete this._popup_kill; if ( popup.id === "ffz-race-popup" && popup.getAttribute('data-channel') === channel_id ) return; } if ( ! container ) return; var el = container.querySelector('.button'), pos = el.offsetLeft + el.offsetWidth, race = this.srl_races[channel_id]; var popup = document.createElement('div'), out = ''; popup.id = 'ffz-race-popup'; popup.setAttribute('data-channel', channel_id); popup.className = (pos >= 300 ? 'right' : 'left') + ' share dropmenu'; this._popup_kill = this._race_kill.bind(this); this._popup = popup; var link = 'http://kadgar.net/live', has_entrant = false; for(var ent in race.entrants) { var state = race.entrants[ent].state; if ( race.entrants.hasOwnProperty(ent) && race.entrants[ent].channel && (state == "racing" || state == "entered") ) { link += "/" + race.entrants[ent].channel; has_entrant = true; } } var height = document.querySelector('.app-main.theatre') ? document.body.clientHeight - 300 : container.parentElement.offsetTop - 175, controller = App.__container__.lookup('controller:channel'), display_name = controller ? controller.get('display_name') : FFZ.get_capitalization(channel_id), tweet = encodeURIComponent("I'm watching " + display_name + " race " + race.goal + " in " + race.game + " on SpeedRunsLive!"); out = '
'; out += '
Developers
Dan Salvato  
Stendec  
Version ' + FFZ.version_info + 'Logs
'; out += '
#Entrant Time
'; out += '
'; out += ''; out += '

SRL'; if ( has_entrant ) out += '   Multitwitch'; out += '

'; popup.innerHTML = out; container.appendChild(popup); this._update_race(container, true); } FFZ.prototype._update_race = function(container, not_timer) { if ( this._race_timer && not_timer ) { clearTimeout(this._race_timer); delete this._race_timer; } if ( ! container ) return; var channel_id = container.getAttribute('data-channel'), race = this.srl_races[channel_id]; if ( ! race ) { // No race. Abort. container.parentElement.removeChild(container); if ( this._popup && this._popup.id === 'ffz-race-popup' && this._popup.getAttribute('data-channel') === channel_id ) { this._popup_kill && this._popup_kill(); if ( this._popup ) { delete this._popup; delete this._popup_kill; } } return; } var entrant_id = race.twitch_entrants[channel_id], entrant = race.entrants[entrant_id], popup = container.querySelector('#ffz-race-popup'), now = Date.now() / 1000, elapsed = Math.floor(now - race.time); container.querySelector('.logo').innerHTML = utils.placement(entrant); if ( popup ) { var tbody = popup.querySelector('tbody'), timer = popup.querySelector('.heading span'), info = popup.querySelector('.heading div'); tbody.innerHTML = ''; var entrants = [], done = true; for(var ent in race.entrants) { if ( ! race.entrants.hasOwnProperty(ent) ) continue; if ( race.entrants[ent].state == "racing" ) done = false; entrants.push(race.entrants[ent]); } entrants.sort(function(a,b) { var a_place = a.place || 9999, b_place = b.place || 9999, a_time = a.time || elapsed, b_time = b.time || elapsed; if ( a.state == "forfeit" || a.state == "dq" ) a_place = 10000; if ( b.state == "forfeit" || b.state == "dq" ) b_place = 10000; if ( a_place < b_place ) return -1; else if ( a_place > b_place ) return 1; else if ( a.name < b.name ) return -1; else if ( a.name > b.name ) return 1; else if ( a_time < b_time ) return -1; else if ( a_time > b_time ) return 1; }); for(var i=0; i < entrants.length; i++) { var ent = entrants[i], name = '' + ent.display_name + '', twitch_link = ent.channel ? '' : '', hitbox_link = ent.hitbox ? '' : '', time = elapsed ? utils.time_to_string(ent.time||elapsed) : "", place = utils.place_string(ent.place), comment = ent.comment ? utils.sanitize(ent.comment) : ""; tbody.innerHTML += '' + place + '' + name + '' + twitch_link + hitbox_link + '' + (ent.state == "forfeit" ? "Forfeit" : time) + ''; } if ( this._race_game != race.game || this._race_goal != race.goal ) { this._race_game = race.game; this._race_goal = race.goal; var game = utils.sanitize(race.game), goal = utils.sanitize(race.goal); info.innerHTML = '

' + game + "

Goal: " + goal; } if ( ! elapsed ) timer.innerHTML = "Entry Open"; else if ( done ) timer.innerHTML = "Done"; else { timer.innerHTML = utils.time_to_string(elapsed); this._race_timer = setTimeout(this._update_race.bind(this, container), 1000); } } } },{"../utils":35}],32:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'); FFZ.prototype.setup_css = function() { this.log("Injecting main FrankerFaceZ CSS."); var s = this._main_style = document.createElement('link'); s.id = "ffz-ui-css"; s.setAttribute('rel', 'stylesheet'); s.setAttribute('href', constants.SERVER + "script/style.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); 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":5}],33:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'); // ------------------- // Subscriber Display // ------------------- FFZ.prototype._update_subscribers = function() { if ( this._update_subscribers_timer ) { clearTimeout(this._update_subscribers_timer); delete this._update_subscribers_timer; } // Schedule an update. this._update_subscribers_timer = setTimeout(this._update_subscribers.bind(this), 60000); var user = this.get_user(), f = this, match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, id = this.is_dashboard && match && match[1]; if ( this.has_bttv || ! id || id !== user.login ) { var el = document.querySelector("#ffz-sub-display"); if ( el ) el.parentElement.removeChild(el); return; } // Spend a moment wishing we could just hit the subscribers API from the // context of the web user. // Get the count! jQuery.ajax({url: "/broadcast/dashboard/partnership"}).done(function(data) { try { var html = document.createElement('span'), dash; html.innerHTML = data; dash = html.querySelector("#dash_main"); var match = dash && dash.textContent.match(/([\d,\.]+) total active subscribers/), sub_count = match && match[1]; if ( ! sub_count ) { var el = document.querySelector("#ffz-sub-display"); if ( el ) el.parentElement.removeChild(el); if ( f._update_subscribers_timer ) { clearTimeout(f._update_subscribers_timer); delete f._update_subscribers_timer; } return; } var el = document.querySelector('#ffz-sub-display span'); if ( ! el ) { var cont = f.is_dashboard ? document.querySelector("#stats") : document.querySelector("#channel .stats-and-actions .channel-stats"); if ( ! cont ) return; var stat = document.createElement('span'); stat.className = 'ffz stat'; stat.id = 'ffz-sub-display'; stat.title = 'Active Channel Subscribers'; stat.innerHTML = constants.STAR + ' '; el = document.createElement('span'); stat.appendChild(el); Twitch.api.get("chat/" + id + "/badges", null, {version: 3}) .done(function(data) { if ( data.subscriber && data.subscriber.image ) { stat.innerHTML = ''; stat.appendChild(el); stat.style.backgroundImage = 'url("' + data.subscriber.image + '")'; stat.style.backgroundRepeat = 'no-repeat'; stat.style.paddingLeft = '23px'; stat.style.backgroundPosition = '0 50%'; } }); cont.appendChild(stat); jQuery(stat).tipsy(f.is_dashboard ? {"gravity":"s"} : undefined); } el.innerHTML = sub_count; } catch(err) { f.error("_update_subscribers: " + err); } }).fail(function(){ var el = document.querySelector("#ffz-sub-display"); if ( el ) el.parentElement.removeChild(el); return; });; } },{"../constants":5,"../utils":35}],34:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'); // ------------ // FFZ Viewers // ------------ FFZ.ws_commands.chatters = function(data) { var channel = data[0], count = data[1]; var controller = window.App && App.__container__.lookup('controller:channel'), match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, id = this.is_dashboard ? match && match[1] : controller && controller.get && controller.get('id'); if ( ! this.is_dashboard ) { var room = this.rooms && this.rooms[channel]; if ( room ) { room.ffz_chatters = count; if ( this._cindex ) this._cindex.ffzUpdateChatters(); } return; } this._dash_chatters = count; } FFZ.ws_commands.viewers = function(data) { var channel = data[0], count = data[1]; var controller = window.App && App.__container__.lookup('controller:channel'), match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, id = this.is_dashboard ? match && match[1] : controller && controller.get && controller.get('id'); if ( ! this.is_dashboard ) { var room = this.rooms && this.rooms[channel]; if ( room ) { room.ffz_viewers = count; if ( this._cindex ) this._cindex.ffzUpdateChatters(); } return; } this._dash_viewers = count; if ( ! this.settings.chatter_count || id !== channel ) return; var view_count = document.querySelector('#ffz-ffzchatter-display'), content = constants.ZREKNARF + ' ' + utils.number_commas(count) + (typeof this._dash_chatters === "number" ? ' (' + utils.number_commas(this._dash_chatters) + ')' : ""); if ( view_count ) view_count.innerHTML = content; else { var parent = document.querySelector("#stats"); if ( ! parent ) return; view_count = document.createElement('span'); view_count.id = "ffz-ffzchatter-display"; view_count.className = 'ffz stat'; view_count.title = 'Viewers (In Chat) with FrankerFaceZ'; view_count.innerHTML = content; parent.appendChild(view_count); jQuery(view_count).tipsy(this.is_dashboard ? {"gravity":"s"} : undefined); } } },{"../constants":5,"../utils":35}],35:[function(require,module,exports){ var FFZ = window.FrankerFaceZ, constants = require('./constants'); var sanitize_el = document.createElement('span'), sanitize = function(msg) { sanitize_el.textContent = msg; return sanitize_el.innerHTML; }, R_QUOTE = /"/g, R_SQUOTE = /'/g, R_AMP = /&/g, R_LT = //g, quote_attr = function(msg) { return msg.replace(R_AMP, "&").replace(R_QUOTE, """).replace(R_SQUOTE, "'").replace(R_LT, "<").replace(R_GT, ">"); }, pluralize = function(value, singular, plural) { plural = plural || 's'; singular = singular || ''; return value === 1 ? singular : plural; }, place_string = function(num) { if ( num == 1 ) return '1st'; else if ( num == 2 ) return '2nd'; else if ( num == 3 ) return '3rd'; else if ( num == null ) return '---'; return num + "th"; }, date_regex = /^(\d{4}|\+\d{6})(?:-?(\d{2})(?:-?(\d{2})(?:T(\d{2})(?::?(\d{2})(?::?(\d{2})(?:(?:\.|,)(\d{1,}))?)?)?(Z|([\-+])(\d{2})(?::?(\d{2}))?)?)?)?)?$/, parse_date = function(str) { var parts = str.match(date_regex); if ( ! parts ) return null; parts[7] = (parts[7] && parts[7].length) ? parts[7].substr(0, 3) : 0; var unix = Date.UTC(parts[1], parts[2] - 1, parts[3], parts[4], parts[5], parts[6], parts[7]); // Check Offset if ( parts[9] ) { var offset = (parts[9] == "-" ? 1 : -1) * 60000 * (60*parts[10] + 1*parts[11]); unix += offset; } return new Date(unix); }, // IRC Messages splitIRCMessage = function(msgString) { msgString = $.trim(msgString); var split = {raw: msgString}; var tagsEnd = -1; if ( msgString.charAt(0) === '@' ) { tagsEnd = msgString.indexOf(' '); split.tags = msgString.substr(1, tagsEnd - 1); } var prefixStart = tagsEnd + 1, prefixEnd = -1; if ( msgString.charAt(prefixStart) === ':' ) { prefixEnd = msgString.indexOf(' ', prefixStart); split.prefix = msgString.substr(prefixStart + 1, prefixEnd - (prefixStart + 1)); } var trailingStart = msgString.indexOf(' :', prefixStart); if ( trailingStart >= 0 ) { split.trailing = msgString.substr(trailingStart + 2); } else { trailingStart = msgString.length; } var commandAndParams = msgString.substr(prefixEnd + 1, trailingStart - prefixEnd - 1).split(' '); split.command = commandAndParams[0]; if ( commandAndParams.length > 1 ) split.params = commandAndParams.slice(1); return split; }, ESCAPE_CHARS = { ':': ';', s: ' ', r: '\r', n: '\n', '\\': '\\' }, unescapeTag = function(tag) { var result = ''; for(var i=0; i < tag.length; i++) { var c = tag.charAt(i); if ( c === '\\' ) { if ( i === tag.length - 1 ) throw 'Improperly escaped tag'; c = ESCAPE_CHARS[tag.charAt(i+1)]; if ( c === undefined ) throw 'Improperly escaped tag'; i++; } result += c; } return result; }, parseTag = function(tag, value) { switch(tag) { case 'slow': try { return parseInt(value); } catch(err) { return 0; } case 'subs-only': case 'r9k': case 'subscriber': case 'turbo': return value === '1'; default: try { return unescapeTag(value); } catch(err) { return ''; } } }, parseIRCTags = function(tagsString) { var tags = {}, keyValues = tagsString.split(';'); for(var i=0; i < keyValues.length; ++i) { var kv = keyValues[i].split('='); if ( kv.length === 2 ) tags[kv[0]] = parseTag(kv[0], kv[1]); } return tags; }, EMOJI_CODEPOINTS = {}, emoji_to_codepoint = function(icon, variant) { if ( EMOJI_CODEPOINTS[icon] && EMOJI_CODEPOINTS[icon][variant] ) return EMOJI_CODEPOINTS[icon][variant]; var ico = variant === '\uFE0F' ? icon.slice(0, -1) : (icon.length === 3 && icon.charAt(1) === '\uFE0F' ? icon.charAt(0) + icon.charAt(2) : icon), r = [], c = 0, p = 0, i = 0; while ( i < ico.length ) { c = ico.charCodeAt(i++); if ( p ) { r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16)); p = 0; } else if ( 0xD800 <= c && c <= 0xDBFF) { p = c; } else { r.push(c.toString(16)); } } var es = EMOJI_CODEPOINTS[icon] = EMOJI_CODEPOINTS[icon] || {}, out = es[variant] = r.join("-"); return out; }; module.exports = { update_css: function(element, id, css) { var all = element.innerHTML, start = "/*BEGIN " + id + "*/", end = "/*END " + id + "*/", s_ind = all.indexOf(start), e_ind = all.indexOf(end), found = s_ind !== -1 && e_ind !== -1 && e_ind > s_ind; if ( !found && !css ) return; if ( found ) all = all.substr(0, s_ind) + all.substr(e_ind + end.length); if ( css ) all += start + css + end; element.innerHTML = all; }, splitIRCMessage: splitIRCMessage, parseIRCTags: parseIRCTags, emoji_to_codepoint: emoji_to_codepoint, parse_date: parse_date, number_commas: function(x) { var parts = x.toString().split("."); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); return parts.join("."); }, place_string: place_string, placement: function(entrant) { if ( entrant.state == "forfeit" ) return "Forfeit"; else if ( entrant.state == "dq" ) return "DQed"; else if ( entrant.place ) return place_string(entrant.place); return ""; }, sanitize: sanitize, quote_attr: quote_attr, date_string: function(date) { return date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate(); }, pluralize: pluralize, human_time: function(elapsed, factor) { factor = factor || 1; elapsed = Math.floor(elapsed); var years = Math.floor((elapsed*factor) / 31536000) / factor; if ( years >= 1 ) return years + ' year' + pluralize(years); var days = Math.floor((elapsed %= 31536000) / 86400); if ( days >= 1 ) return days + ' day' + pluralize(days); var hours = Math.floor((elapsed %= 86400) / 3600); if ( hours >= 1 ) return hours + ' hour' + pluralize(hours); var minutes = Math.floor((elapsed %= 3600) / 60); if ( minutes >= 1 ) return minutes + ' minute' + pluralize(minutes); var seconds = elapsed % 60; if ( seconds >= 1 ) return seconds + ' second' + pluralize(seconds); return 'less than a second'; }, time_to_string: function(elapsed, separate_days, days_only, no_hours) { var seconds = elapsed % 60, minutes = Math.floor(elapsed / 60), hours = Math.floor(minutes / 60), days = ""; minutes = minutes % 60; if ( separate_days ) { days = Math.floor(hours / 24); hours = hours % 24; if ( days_only && days > 0 ) return days + " days"; days = ( days > 0 ) ? days + " days, " : ""; } return days + ((!no_hours || days || hours) ? ((hours < 10 ? "0" : "") + hours + ':') : '') + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds; }, format_unread: function(count) { if ( count < 1 ) return ""; else if ( count >= 99 ) return "99+"; return "" + count; } } },{"./constants":5}]},{},[18]);window.ffz = new FrankerFaceZ()}(window));