From d55af32b4ed87fb711ec88f7ab3e44f7b9c9f09e Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 23 Mar 2016 19:28:22 -0400 Subject: [PATCH] I really need to remember to commit more frequently. --- dark.css | 184 ++++- gulpfile.js | 50 +- src/badges.js | 75 +- src/colors.js | 467 +++++++------ src/constants.js | 87 ++- src/ember/channel.js | 40 +- src/ember/chatview.js | 60 +- src/ember/conversations.js | 32 +- src/ember/directory.js | 81 ++- src/ember/following.js | 135 +++- src/ember/layout.js | 27 +- src/ember/line.js | 837 ++++++++++++++-------- src/ember/moderation-card.js | 309 ++++---- src/ember/player.js | 105 ++- src/ember/room.js | 82 ++- src/ember/vod-chat.js | 326 +++++++++ src/emoticons.js | 129 +--- src/ext/api.js | 16 +- src/ext/betterttv.js | 185 +++-- src/ext/rechat.js | 17 +- src/featurefriday.js | 4 +- src/main.js | 97 ++- src/settings.js | 1007 +++++++++++---------------- src/socket.js | 9 +- src/styles/badges-sub-notice-on.css | 1 + src/styles/badges-sub-notice.css | 1 + src/styles/badges-transparent.css | 2 + src/styles/chat-background.css | 54 +- src/styles/chat-separator.css | 9 +- src/tokenize.js | 793 +++++++++------------ src/ui/about_page.js | 463 ++++++++---- src/ui/channel_stats.js | 2 + src/ui/dark.js | 11 +- src/ui/dash_stats.js | 271 +++++++ src/ui/following-count.js | 70 +- src/ui/following.js | 12 +- src/ui/menu.js | 215 +++++- src/ui/menu_button.js | 2 +- src/ui/my_emotes.js | 506 +++++++++----- src/ui/notifications.js | 27 +- src/ui/popups.js | 8 +- src/ui/races.js | 2 +- src/ui/sub_count.js | 102 +-- src/utils.js | 388 +++++------ style.css | 390 +++++++++-- 45 files changed, 4777 insertions(+), 2913 deletions(-) create mode 100644 src/ember/vod-chat.js create mode 100644 src/styles/badges-sub-notice-on.css create mode 100644 src/styles/badges-sub-notice.css create mode 100644 src/ui/channel_stats.js create mode 100644 src/ui/dash_stats.js diff --git a/dark.css b/dark.css index 309ca7cb..2cdd6884 100644 --- a/dark.css +++ b/dark.css @@ -89,6 +89,7 @@ /* main column */ +body.ffz-dark, .ffz-dark div#mantle_skin, .ffz-dark div#main_col { background:rgb(16,16,16); @@ -143,9 +144,14 @@ /* Popups */ +.ffz-dark #commission_modal { + background-color: #101010 !important; + border-color: #32323e !important; +} + .ffz-dark .conversation-settings-menu, .ffz-dark .ember-chat .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, -.ffz-dark .card, +.ffz-dark .card:not(#passport_modal), .ffz-dark #flyout .content, .ffz-dark .whatisthis, .ffz-dark .ui-menu, @@ -164,6 +170,7 @@ background-color: rgb(16,16,16); color: rgb(195,195,195); /*#acacbf;*/ border-color: #32323e; + box-shadow: rgba(255,255,255,0.2) 0 0 0 1px inset; } .ffz-dark .st-autocomplete-sidebar .label, @@ -207,6 +214,7 @@ .ffz-dark .change-banner .banner-preview, .ffz-dark form.js-new_panel_form input, .ffz-dark form.js-new_panel_form textarea, +.ffz-dark .conversation-input-bar textarea, .ffz-dark .card input, .ffz-dark .card textarea, .ffz-dark .dropmenu input, @@ -216,6 +224,7 @@ .ffz-dark textarea, .ffz-dark select, .ffz-dark option, +.ffz-dark #mantle_skin .dropdown, .ffz-dark .directory_header #custom_filter input { background-color: rgba(255,255,255,0.05); border-color: rgba(255,255,255,0.1); @@ -379,6 +388,17 @@ color: #fff; } +.ffz-dark .tabs.tabs--fullwidth li:not(.selected) a:hover { + color: #fff !important; + background: #24242a; + border-color: #8c8c9c; +} + +.ffz-dark .tabs li.selected a { + color: #fff !important; + border-color: #a68cd4 !important; +} + /* Subscriptions Page */ @@ -715,6 +735,25 @@ /* Dashboard */ +.ffz-dark .brick { + background-color: #121212; +} + +.ffz-dark .brick--faint, +.ffz-dark .brick--block { + background-color: rgb(25,25,25); +} + +.ffz-dark .brick--faint, +.ffz-dark .brick--block, +.ffz-dark #action_feed .action, +.ffz-dark .revHeader__item { + border-color: rgba(255,255,255,0.2); +} + +.ffz-dark #mantle_skin .what { background-color: #6441a5; } +.ffz-dark #action_feed .action { padding-bottom: 20px } + .ffz-dark .js-stream-key-button-container .button { margin: 15px auto; } @@ -824,6 +863,13 @@ border-right-color: rgba(255,255,255,0.2); } +.ffz-dark #dash_main .dash-player-contain.collapsed #player_overlay, +.ffz-dark .dash-hostmode-contain { + background: #202020; +} + +.ffz-dark #dash_main #delay_controls, +.ffz-dark #dash_main #commercial_buttons, .ffz-dark .dash-hostmode-list-contain { border-top-color: rgba(255,255,255,0.2); } @@ -909,15 +955,111 @@ } -/* Conversations */ +/* VoDs */ -.ffz-dark .ignore-cta { - background-color: #333; - box-shadow: 0 3px 0 #000; +.ffz-dark .app-main .chatReplay .notice-wrapper .svg-logo_glitch { + fill: #242424 !important; } -.ffz-dark .ignore-cta .conversation-system-message { - color: #ccc; +.ffz-dark .app-main .chatReplay .notice-wrapper { + background-color: #191919; +} + +.ffz-dark .chatReplay .loading-spinner-container { + background: rgba(25,25,25,0.65); +} + + +/* Conversations */ + +.ffz-dark .conversations-list-bottom-bar { + background-color: #19191f; + color: #8c8c9c; + border-color: rgba(255,255,255,0.1); +} + +.ffz-dark .conversation-list-bottom-bar:hover { + color: #fff; +} + +.ffz-dark .conversations-list { + background-color: #19191f; + border: 1px solid #32323e; + color: #fff; +} + +.ffz-dark .conversations-list:before { + right: 9px; + border-color: rgba(50,50,62,0); + border-top-color: #32323e; +} + +.ffz-dark .conversations-list:after { + border-color: rgba(25,25,31,0); + border-top-color: #19191f; + border-width: 10px; + margin-left: -10px; +} + +.ffz-dark .conversations-list .conversations-list-header { + background-color: #121217; + border-bottom: 1px solid #32323e; + color: #fff; +} + +.ffz-dark .conversations-list .conversation-preview-line { + color: #8c8c9c; +} + +.ffz-dark .conversations-list .search-divider, +.ffz-dark .conversations-list .conversations-list-item { + border-bottom: 1px solid #32323e; +} + +.ffz-dark .conversations-list .conversations-list-item:hover { + background-color: #121217; +} + +.ffz-dark .conversation-window { + background-color: #19191f; + box-shadow: none; + color: #8c8c9c; +} + +.ffz-dark .conversations-list .search-divider, +.ffz-dark .conversation-header { + background-color: #121217; + box-shadow: none; +} + +.ffz-dark .conversation-input-actions .button, +.ffz-dark .conversation-input-actions .follow-button:not(.ember-follow) .follow, +.ffz-dark .follow-button:not(.ember-follow) .conversation-input-actions .follow { + background-color: #444; +} + +.ffz-dark .conversation-window.has-focus .conversation-header { + background-color: #121217; +} + +.ffz-dark .conversation-window.has-focus .conversation-header-name { + color: #fff; +} + +.ffz-dark .conversation-window.has-focus .conversation-input-bar textarea:focus { + border-color: rgba(255,255,255,0.2); +} + +.ffz-dark .conversation-window.has-focus .conversation-input-actions .button, +.ffz-dark .conversation-window.has-focus .conversation-input-actions .follow-button:not(.ember-follow) .follow, +.ffz-dark .follow-button:not(.ember-follow) .conversation-window.has-focus .conversation-input-actions .follow { + background-color: #6441a5; +} + +.ffz-dark .conversation-system-message { + background-color: #19191f; + color: #8c8c9c; + border-bottom: 1px solid #32323e; } .ffz-dark .conversation-input-bar .emoticon-selector-toggle svg path { @@ -954,6 +1096,32 @@ border-bottom-color: rgb(16,16,16); } +.ffz-dark .conversation-window { + border: 1px solid rgba(255,255,255,0.2); +} + +.ffz-dark:not(.ffz-top-conversations) .conversation-window { + border-bottom: none; +} + +.ffz-dark.ffz-top-conversations .conversation-window { + border-top: none; +} + +.ffz-dark .conversation-window:not(.collapsed) .conversation-header { + border-bottom: 1px solid rgba(255,255,255,0.2); +} + + +/*.ffz-dark .ignore-cta { + background-color: #333; + box-shadow: 0 3px 0 #000; +} + +.ffz-dark .ignore-cta .conversation-system-message { + color: #ccc; +} + .ffz-dark .conversations-list-icon { background: #19191f; color: #8c8c9c; @@ -1054,4 +1222,4 @@ .ffz-dark .conversation-window .new-message-divider span { background: transparent; } .ffz-dark .conversation-window .timestamp-line:after, -.ffz-dark .conversation-window .new-message-divider:after { display: none; } \ No newline at end of file +.ffz-dark .conversation-window .new-message-divider:after { display: none; }*/ \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 9d300617..513683fa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -24,7 +24,8 @@ var ftp = require('vinyl-ftp'), // Server Dependencies var http = require("http"), - //https = require("https"), + https = require("https"), + net = require('net'), path = require("path"), request = require("request"), url = require("url"); @@ -220,7 +221,21 @@ gulp.task('server', function() { fs.exists(file, function(exists) { if ( ! exists ) { util.log("[" + util.colors.cyan("HTTP") + "] " + util.colors.bold.blue("CDN") + " GET " + util.colors.magenta(uri)); - return request.get("http://cdn.frankerfacez.com/" + uri).pipe(res); + /*https.request({ + hostname: 'cdn.frankerfacez.com', + port: 443, + path: uri, + method: 'GET' + }, function(cli_res) { + res.writeHead(cli_res.statusCode, cli_res.headers); + cli_res.on('data', function(chunk) { res.write(chunk); }); + cli_res.on('end', function() { res.end() }); + }).on('error', function(e) { + res.writeHead(502, {"Access-Control-Allow-Origin": "*"}); + res.write('502 Bad Gateway'); + res.end(); + });*/ + return request.get("http://cdn.frankerfacez.com/" + uri).on('error', function(err) { res.end() }).pipe(res); } var headers = {"Access-Control-Allow-Origin": "*"}; @@ -242,8 +257,33 @@ gulp.task('server', function() { }; - http.createServer(handle_req).listen(8000, "localhost"); - //https.createServer(handle_req).listen(8000, "localhost"); + if ( fs.existsSync("dev_key.pem") ) { + var https_options = { + key: fs.readFileSync("dev_key.pem"), + cert: fs.readFileSync("dev_cert.pem") + }; - util.log("[" + util.colors.cyan("HTTP") + "] Listening on Port: " + util.colors.magenta("8000")); + http.createServer(handle_req).listen(8001, "localhost"); + https.createServer(https_options, handle_req).listen(8002, "localhost"); + + net.createServer(function(conn) { + conn.on('error', function(e) { + util.log("[" + util.colors.cyan("HTTP") + "] Connection Error: " + util.colors.magenta('' + e)); + }); + + conn.once('data', function(buf) { + var address = (buf[0] === 22) ? 8002 : 8001; + var proxy = net.createConnection(address, function() { + proxy.write(buf); + conn.pipe(proxy).pipe(conn); + }); + }); + }).listen(8000); + + util.log("[" + util.colors.cyan("HTTPS") + "] Listening on Port: " + util.colors.magenta("8000")); + + } else { + http.createServer(handle_req).listen(8000, "localhost"); + util.log("[" + util.colors.cyan("HTTP") + "] Listening on Port: " + util.colors.magenta("8000")); + } }); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index f0359d37..611275b5 100644 --- a/src/badges.js +++ b/src/badges.js @@ -2,11 +2,10 @@ var FFZ = window.FrankerFaceZ, constants = require('./constants'), utils = require('./utils'), - MOD_BADGES = [ + SPECIAL_BADGES = [ ['staff', 'staff', 'Staff'], ['admin', 'admin', 'Admin'], - ['global_mod', 'global-moderator', 'Global Moderator'], - ['mod', 'moderator', 'Moderator'] + ['global_mod', 'global-moderator', 'Global Moderator'] ], badge_css = function(badge) { @@ -31,6 +30,21 @@ FFZ.settings_info.show_badges = { }; +FFZ.settings_info.sub_notice_badges = { + type: "boolean", + value: false, + + category: "Chat Appearance", + name: "Subscriber Notice Badges", + help: "Display a subscriber badge on chat messages about new subscribers.", + + on_update: function(val) { + this.toggle_style('badges-sub-notice', ! val); + this.toggle_style('badges-sub-notice-on', val); + } + }; + + FFZ.settings_info.legacy_badges = { type: "select", options: { @@ -120,6 +134,9 @@ FFZ.prototype.setup_badges = function() { this.toggle_style('badges-circular-small', val === 4); this.toggle_style('badges-transparent', val === 5); document.body.classList.toggle('ffz-transparent-badges', val === 5); + + this.toggle_style('badges-sub-notice', ! this.settings.sub_notice_badges); + this.toggle_style('badges-sub-notice-on', this.settings.sub_notice_badges); } this.toggle_style('badges-legacy', this.settings.legacy_badges === 3); @@ -184,7 +201,7 @@ FFZ.prototype.get_badges = function(user, room_id, badges, msg) { if ( full_badge.visible !== undefined ) { var visible = full_badge.visible; if ( typeof visible === "function" ) - visible = visible.bind(this)(room_id, user, msg, badges); + visible = visible.call(this, room_id, user, msg, badges); if ( ! visible ) continue; @@ -215,26 +232,34 @@ FFZ.prototype.get_badges = function(user, room_id, badges, msg) { FFZ.prototype.get_line_badges = function(msg) { - var badges = {}; + var badges = {}, + room = msg.get && msg.get('room') || msg.room, + from = msg.get && msg.get('from') || msg.from, + tags = msg.get && msg.get('tags') || msg.tags || {}, + labels = msg.labels || []; - if ( msg.room && msg.from === msg.room ) + if ( room && from === room ) badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; - else if ( msg.labels ) - for(var i=0, l = MOD_BADGES.length; i < l; i++) { - var mb = MOD_BADGES[i]; - if ( msg.labels.indexOf(mb[0]) !== -1 ) { - badges[0] = {klass: mb[1], title: mb[2]} - break; - } - } + else { + for(var i=0, l = SPECIAL_BADGES.length; i < l; i++) { + var mb = SPECIAL_BADGES[i]; + if ( tags['user-type'] === mb[0] || labels.indexOf(mb[0]) !== -1 ) { + badges[0] = {klass: mb[1], title: mb[2]} + break; + } + } - if ( msg.labels && msg.labels.indexOf('subscriber') !== -1 ) + if ( tags.mod || labels.indexOf('mod') !== -1 ) + badges[1] = {klass: 'moderator', title: 'Moderator'}; + } + + if ( tags.subscriber || labels.indexOf('subscriber') !== -1 ) badges[10] = {klass: 'subscriber', title: 'Subscriber'} - if ( msg.labels && msg.labels.indexOf('turbo') !== -1 ) + if ( tags.turbo || labels.indexOf('turbo') !== -1 ) badges[15] = {klass: 'turbo', title: 'Turbo'}; // FFZ Badges - return this.get_badges(msg.from, msg.room, badges, msg); + return this.get_badges(from, room, badges, msg); } @@ -244,8 +269,8 @@ FFZ.prototype.get_other_badges = function(user_id, room_id, user_type, has_sub, if ( room_id && user_id === room_id ) badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; else - for(var i=0, l = MOD_BADGES.length; i < l; i++) { - var mb = MOD_BADGES[i]; + for(var i=0, l = SPECIAL_BADGES.length; i < l; i++) { + var mb = SPECIAL_BADGES[i]; if ( user_type === mb[0] ) { badges[0] = {klass: mb[1], title: mb[2]}; break; @@ -325,7 +350,7 @@ FFZ.prototype.bttv_badges = function(data) { if ( full_badge.visible !== undefined ) { var visible = full_badge.visible; if ( typeof visible == "function" ) - visible = visible.bind(this)(null, user_id); + visible = visible.call(this, null, user_id); if ( ! visible ) continue; @@ -407,12 +432,12 @@ FFZ.prototype._legacy_add_donors = function() { badges = user.badges = user.badges || {}; if ( ! badges[0] ) - badges[0] = {id:2}; + badges[1] = {id:2}; } // Special Badges - this.users.sirstendec = {badges: {1: {id:0}}, sets: [4330]}; - this.users.zenwan = {badges: {0: {id:2, image: "//cdn.frankerfacez.com/script/momiglee_badge.png", title: "WAN"}}}; + this.users.sirstendec = {badges: {5: {id:0}}, sets: [4330]}; + this.users.zenwan = {badges: {1: {id:2, image: "//cdn.frankerfacez.com/script/momiglee_badge.png", title: "WAN"}}}; this._legacy_load_bots(); this._legacy_load_donors(); @@ -421,7 +446,7 @@ FFZ.prototype._legacy_add_donors = function() { FFZ.prototype._legacy_load_bots = function(callback, tries) { jQuery.ajax(constants.SERVER + "script/bots.txt", {context: this}) .done(function(data) { - this._legacy_parse_badges(callback, data, 0, 2, "Bot (By: {})"); + this._legacy_parse_badges(callback, data, 1, 2, "Bot (By: {})"); }).fail(function(data) { if ( data.status == 404 ) @@ -436,7 +461,7 @@ FFZ.prototype._legacy_load_bots = function(callback, tries) { FFZ.prototype._legacy_load_donors = function(callback, tries) { jQuery.ajax(constants.SERVER + "script/donors.txt", {context: this}) .done(function(data) { - this._legacy_parse_badges(callback, data, 1, 1); + this._legacy_parse_badges(callback, data, 5, 1); }).fail(function(data) { if ( data.status == 404 ) diff --git a/src/colors.js b/src/colors.js index 3290db5c..15953ea5 100644 --- a/src/colors.js +++ b/src/colors.js @@ -1,4 +1,5 @@ var FFZ = window.FrankerFaceZ, + utils = require('./utils'), hue2rgb = function(p, q, t) { if ( t < 0 ) t += 1; @@ -29,6 +30,8 @@ FFZ.settings_info.fix_color = { }, value: '1', + visible: function() { return localStorage.hasOwnProperty("ffz_setting_fix_color") }, + category: "Chat Appearance", no_bttv: true, @@ -64,24 +67,31 @@ FFZ.settings_info.luv_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); + var f = this, + old_val = this.settings.luv_contrast; - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Luv Adjustment Minimum Contrast Ratio", + "Please enter a new value for the minimum contrast ratio required between username colors and the background.

Default: 4.5", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - var parsed = parseFloat(new_val); - if ( parsed === NaN || parsed < 1 ) - parsed = 4.5; + var parsed = parseFloat(new_val); + if ( Number.isNaN(parsed) || ! Number.isFinite(parsed) ) + parsed = 4.5; - this.settings.set("luv_contrast", parsed); + f.settings.set("luv_contrast", parsed); + }); }, on_update: function(val) { this._rebuild_contrast(); + this._rebuild_filter_styles(); if ( ! this.has_bttv && this.settings.fix_color == '1' ) - this._rebuild_colors(); + this._rebuild_colors(); } }; @@ -116,7 +126,7 @@ FFZ.settings_info.color_blind = { FFZ.prototype.setup_colors = function() { this.toggle_style('chat-colors-gray', !this.has_bttv && this.settings.fix_color === '-1'); - this._colors = {}; + this._hex_colors = {}; this._rebuild_contrast(); this._update_colors(); @@ -141,6 +151,9 @@ FFZ.prototype.setup_colors = function() { FFZ.Color = {}; +FFZ.Color._canvas = null; +FFZ.Color._context = null; + FFZ.Color.CVDMatrix = { protanope: [ // reds are greatly reduced (1% men) 0.0, 2.02344, -2.52581, @@ -160,44 +173,66 @@ FFZ.Color.CVDMatrix = { } -var RGBColor = FFZ.Color.RGB = function(r, g, b) { - this.r = r||0; this.g = g||0; this.b = b||0; +var RGBAColor = FFZ.Color.RGBA = function(r, g, b, a) { + this.r = r||0; this.g = g||0; this.b = b||0; this.a = a||0; }; -var HSVColor = FFZ.Color.HSV = function(h, s, v) { - this.h = h||0; this.s = s||0; this.v = v||0; +var HSVAColor = FFZ.Color.HSVA = function(h, s, v, a) { + this.h = h||0; this.s = s||0; this.v = v||0; this.a = a||0; }; -var HSLColor = FFZ.Color.HSL = function(h, s, l) { - this.h = h||0; this.s = s||0; this.l = l||0; +var HSLAColor = FFZ.Color.HSLA = function(h, s, l, a) { + this.h = h||0; this.s = s||0; this.l = l||0; this.a = a||0; }; -var XYZColor = FFZ.Color.XYZ = function(x, y, z) { - this.x = x||0; this.y = y||0; this.z = z||0; +var XYZAColor = FFZ.Color.XYZA = function(x, y, z, a) { + this.x = x||0; this.y = y||0; this.z = z||0; this.a = a||0; }; -var LUVColor = FFZ.Color.LUV = function(l, u, v) { - this.l = l||0; this.u = u||0; this.v = v||0; +var LUVAColor = FFZ.Color.LUVA = function(l, u, v, a) { + this.l = l||0; this.u = u||0; this.v = v||0; this.a = a||0; }; -// RGB Colors +// RGBA Colors -RGBColor.prototype.eq = function(rgb) { - return rgb.r === this.r && rgb.g === this.g && rgb.b === this.b; +RGBAColor.prototype.eq = function(rgb) { + return rgb.r === this.r && rgb.g === this.g && rgb.b === this.b && rgb.a === this.a; } -RGBColor.fromCSS = function(rgb) { +RGBAColor.fromName = function(name) { + var context = FFZ.Color._context; + if ( ! context ) { + var canvas = FFZ.Color._canvas = document.createElement('canvas'); + context = FFZ.Color._context = canvas.getContext("2d"); + } + + context.clearRect(0,0,1,1); + context.fillStyle = name; + context.fillRect(0,0,1,1); + var data = context.getImageData(0,0,1,1); + + if ( ! data || ! data.data || data.data.length !== 4 ) + return null; + + return new RGBAColor(data.data[0], data.data[1], data.data[2], data.data[3] / 255); +} + +RGBAColor.fromCSS = function(rgb) { + if ( ! rgb ) + return null; + rgb = rgb.trim(); if ( rgb.charAt(0) === '#' ) - return RGBColor.fromHex(rgb); + return RGBAColor.fromHex(rgb); - var match = /rgba?\( *(\d+%?) *, *(\d+%?) *, *(\d+%?) *(?:,[^\)]+)?\)/.exec(rgb); + var match = /rgba?\( *(\d+%?) *, *(\d+%?) *, *(\d+%?) *(?:, *([\d\.]+))?\)/i.exec(rgb); if ( match ) { var r = match[1], g = match[2], - b = match[3]; + b = match[3], + a = match[4]; if ( r.charAt(r.length-1) === '%' ) r = 255 * (parseInt(r) / 100); @@ -214,26 +249,36 @@ RGBColor.fromCSS = function(rgb) { else b = parseInt(b); - return new RGBColor( + if ( a ) + if ( a.charAt(a.length-1) === '%' ) + a = parseInt(a) / 100; + else + a = parseFloat(a); + else + a = 1; + + return new RGBAColor( Math.min(Math.max(0, r), 255), Math.min(Math.max(0, g), 255), - Math.min(Math.max(0, b), 255) + Math.min(Math.max(0, b), 255), + Math.min(Math.max(0, a), 1) ); } - return null; + return RGBAColor.fromName(rgb); } -RGBColor.fromHex = function(code) { +RGBAColor.fromHex = function(code) { var raw = parseInt(code.charAt(0) === '#' ? code.substr(1) : code, 16); - return new RGBColor( + return new RGBAColor( (raw >> 16), // Red (raw >> 8 & 0x00FF), // Green - (raw & 0x0000FF) // Blue + (raw & 0x0000FF), // Blue, + 1 // Alpha ) } -RGBColor.fromHSV = function(h, s, v) { +RGBAColor.fromHSVA = function(h, s, v, a) { var r, g, b, i = Math.floor(h * 6), @@ -251,55 +296,58 @@ RGBColor.fromHSV = function(h, s, v) { case 5: r = v, g = p, b = q; } - return new RGBColor( + return new RGBAColor( 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)) + Math.round(Math.min(Math.max(0, b*255), 255)), + a === undefined ? 1 : a ); } -RGBColor.fromXYZ = function(x, y, z) { +RGBAColor.fromXYZA = function(x, y, z, a) { 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))) + return new RGBAColor( + Math.max(0, Math.min(255, 255 * XYZAColor.channelConverter(R))), + Math.max(0, Math.min(255, 255 * XYZAColor.channelConverter(G))), + Math.max(0, Math.min(255, 255 * XYZAColor.channelConverter(B))), + a === undefined ? 1 : a ); } -RGBColor.fromHSL = function(h, s, l) { +RGBAColor.fromHSLA = function(h, s, l, a) { if ( s === 0 ) { var v = Math.round(Math.min(Math.max(0, 255*l), 255)); - return new RGBColor(v, v, v); + return new RGBAColor(v, v, v, a === undefined ? 1 : a); } var q = l < 0.5 ? l * (1 + s) : l + s - l * s, p = 2 * l - q; - return new RGBColor( + return new RGBAColor( 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)) + Math.round(Math.min(Math.max(0, 255 * hue2rgb(p, q, h - 1/3)), 255)), + a === undefined ? 1 : a ); } -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(); } +RGBAColor.prototype.toHSVA = function() { return HSVAColor.fromRGBA(this.r, this.g, this.b, this.a); } +RGBAColor.prototype.toHSLA = function() { return HSLAColor.fromRGBA(this.r, this.g, this.b, this.a); } +RGBAColor.prototype.toCSS = function() { return "rgb" + (this.a !== 1 ? "a" : "") + "(" + Math.round(this.r) + "," + Math.round(this.g) + "," + Math.round(this.b) + (this.a !== 1 ? "," + this.a : "") + ")"; } +RGBAColor.prototype.toXYZA = function() { return XYZAColor.fromRGBA(this.r, this.g, this.b, this.a); } +RGBAColor.prototype.toLUVA = function() { return this.toXYZA().toLUVA(); } -RGBColor.prototype.toHex = function() { +RGBAColor.prototype.toHex = function() { var rgb = this.b | (this.g << 8) | (this.r << 16); return '#' + (0x1000000 + rgb).toString(16).slice(1); } -RGBColor.prototype.luminance = function() { +RGBAColor.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) { @@ -312,19 +360,20 @@ RGBColor.prototype.luminance = function() { } -RGBColor.prototype.brighten = function(amount) { +RGBAColor.prototype.brighten = function(amount) { amount = typeof amount === "number" ? amount : 1; amount = Math.round(255 * (amount / 100)); - return new RGBColor( + return new RGBAColor( 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)) + Math.max(0, Math.min(255, this.b + amount)), + this.a ); } -RGBColor.prototype.daltonize = function(type, amount) { +RGBAColor.prototype.daltonize = function(type, amount) { amount = typeof amount === "number" ? amount : 1.0; var cvd; if ( typeof type === "string" ) { @@ -366,21 +415,22 @@ RGBColor.prototype.daltonize = function(type, amount) { 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); + return new RGBAColor(R, G, B, this.a); } -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); } +RGBAColor.prototype._r = function(r) { return new RGBAColor(r, this.g, this.b, this.a); } +RGBAColor.prototype._g = function(g) { return new RGBAColor(this.r, g, this.b, this.a); } +RGBAColor.prototype._b = function(b) { return new RGBAColor(this.r, this.g, b, this.a); } +RGBAColor.prototype._a = function(a) { return new RGBAColor(this.r, this.g, this.b, a); } // HSL Colors -HSLColor.prototype.eq = function(hsl) { - return hsl.h === this.h && hsl.s === this.s && hsl.l === this.l; +HSLAColor.prototype.eq = function(hsl) { + return hsl.h === this.h && hsl.s === this.s && hsl.l === this.l && hsl.a === this.a; } -HSLColor.fromRGB = function(r, g, b) { +HSLAColor.fromRGBA = function(r, g, b, a) { r /= 255; g /= 255; b /= 255; var max = Math.max(r,g,b), @@ -406,27 +456,27 @@ HSLColor.fromRGB = function(r, g, b) { h /= 6; } - return new HSLColor(h, s, l); + return new HSLAColor(h, s, l, a === undefined ? 1 : a); } -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(); } +HSLAColor.prototype.toRGBA = function() { return RGBAColor.fromHSLA(this.h, this.s, this.l, this.a); } +HSLAColor.prototype.toCSS = function() { return "hsl" + (this.a !== 1 ? "a" : "") + "(" + Math.round(this.h*360) + "," + Math.round(this.s*100) + "%," + Math.round(this.l*100) + "%" + (this.a !== 1 ? "," + this.a : "") + ")"; } +HSLAColor.prototype.toHSVA = function() { return this.toRGBA().toHSVA(); } +HSLAColor.prototype.toXYZA = function() { return this.toRGBA().toXYZA(); } +HSLAColor.prototype.toLUVA = function() { return this.toRGBA().toLUVA(); } -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); } +HSLAColor.prototype._h = function(h) { return new HSLAColor(h, this.s, this.l, this.a); } +HSLAColor.prototype._s = function(s) { return new HSLAColor(this.h, s, this.l, this.a); } +HSLAColor.prototype._l = function(l) { return new HSLAColor(this.h, this.s, l, this.a); } +HSLAColor.prototype._a = function(a) { return new HSLAColor(this.h, this.s, this.l, a); } // HSV Colors -HSVColor.prototype.eq = function(hsv) { return hsv.h === this.h && hsv.s === this.s && hsv.v === this.v; } +HSVAColor.prototype.eq = function(hsv) { return hsv.h === this.h && hsv.s === this.s && hsv.v === this.v && hsv.a === this.a; } -HSVColor.fromRGB = function(r, g, b) { +HSVAColor.fromRGBA = function(r, g, b, a) { r /= 255; g /= 255; b /= 255; var max = Math.max(r, g, b), @@ -453,24 +503,25 @@ HSVColor.fromRGB = function(r, g, b) { h /= 6; } - return new HSVColor(h, s, v); + return new HSVAColor(h, s, v, a === undefined ? 1 : a); } -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(); } +HSVAColor.prototype.toRGBA = function() { return RGBAColor.fromHSVA(this.h, this.s, this.v, this.a); } +HSVAColor.prototype.toHSLA = function() { return this.toRGBA().toHSLA(); } +HSVAColor.prototype.toXYZA = function() { return this.toRGBA().toXYZA(); } +HSVAColor.prototype.toLUVA = function() { return this.toRGBA().toLUVA(); } -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); } +HSVAColor.prototype._h = function(h) { return new HSVAColor(h, this.s, this.v, this.a); } +HSVAColor.prototype._s = function(s) { return new HSVAColor(this.h, s, this.v, this.a); } +HSVAColor.prototype._v = function(v) { return new HSVAColor(this.h, this.s, v, this.a); } +HSVAColor.prototype._a = function(a) { return new HSVAColor(this.h, this.s, this.v, a); } // XYZ Colors -RGBColor.channelConverter = function (channel) { +RGBAColor.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); @@ -479,7 +530,7 @@ RGBColor.channelConverter = function (channel) { return (channel <= 0.04045) ? channel / 12.92 : Math.pow((channel + 0.055) / 1.055, 2.4); }; -XYZColor.channelConverter = function (channel) { +XYZAColor.channelConverter = function (channel) { // Using lazy conversion in the other direction as well return Math.pow(channel, 1/2.2); @@ -488,27 +539,28 @@ XYZColor.channelConverter = function (channel) { }; -XYZColor.prototype.eq = function(xyz) { return xyz.x === this.x && xyz.y === this.y && xyz.z === this.z; } +XYZAColor.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); +XYZAColor.fromRGBA = function(r, g, b, a) { + var R = RGBAColor.channelConverter(r / 255), + G = RGBAColor.channelConverter(g / 255), + B = RGBAColor.channelConverter(b / 255); - return new XYZColor( + return new XYZAColor( 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 + 0.019334 * R + 0.119193 * G + 0.950227 * B, + a === undefined ? 1 : a ); } -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; +XYZAColor.fromLUVA = function(l, u, v, alpha) { + var deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z); + var uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor; + var vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor; - // XYZColor.EPSILON * XYZColor.KAPPA = 8 - var Y = (l > 8) ? Math.pow((l + 16) / 116, 3) : l / XYZColor.KAPPA; + // XYZAColor.EPSILON * XYZAColor.KAPPA = 8 + var Y = (l > 8) ? Math.pow((l + 16) / 116, 3) : l / XYZAColor.KAPPA; var a = 1/3 * (((52 * l) / (u + 13 * l * uDeltaGamma)) - 1); var b = -5 * Y; @@ -518,36 +570,37 @@ XYZColor.fromLUV = function(l, u, v) { var X = (d - b) / (a - c); var Z = X * a + b; - return new XYZColor(X, Y, Z); + return new XYZAColor(X, Y, Z, alpha === undefined ? 1 : alpha); } -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(); } +XYZAColor.prototype.toRGBA = function() { return RGBAColor.fromXYZA(this.x, this.y, this.z, this.a); } +XYZAColor.prototype.toLUVA = function() { return LUVAColor.fromXYZA(this.x, this.y, this.z, this.a); } +XYZAColor.prototype.toHSLA = function() { return this.toRGBA().toHSLA(); } +XYZAColor.prototype.toHSVA = function() { return this.toRGBA().toHSVA(); } -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); } +XYZAColor.prototype._x = function(x) { return new XYZAColor(x, this.y, this.z, this.a); } +XYZAColor.prototype._y = function(y) { return new XYZAColor(this.x, y, this.z, this.a); } +XYZAColor.prototype._z = function(z) { return new XYZAColor(this.x, this.y, z, this.a); } +XYZAColor.prototype._a = function(a) { return new XYZAColor(this.x, this.y, this.z, a); } // LUV Colors -XYZColor.EPSILON = Math.pow(6 / 29, 3); -XYZColor.KAPPA = Math.pow(29 / 3, 3); -XYZColor.WHITE = (new RGBColor(255, 255, 255)).toXYZ(); +XYZAColor.EPSILON = Math.pow(6 / 29, 3); +XYZAColor.KAPPA = Math.pow(29 / 3, 3); +XYZAColor.WHITE = (new RGBAColor(255, 255, 255, 1)).toXYZA(); -LUVColor.prototype.eq = function(luv) { return luv.l === this.l && luv.u === this.u && luv.v === this.v; } +LUVAColor.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; +LUVAColor.fromXYZA = function(X, Y, Z, a) { + var deltaGammaFactor = 1 / (XYZAColor.WHITE.x + 15 * XYZAColor.WHITE.y + 3 * XYZAColor.WHITE.z); + var uDeltaGamma = 4 * XYZAColor.WHITE.x * deltaGammaFactor; + var vDeltagamma = 9 * XYZAColor.WHITE.y * deltaGammaFactor; - var yGamma = Y / XYZColor.WHITE.y; + var yGamma = Y / XYZAColor.WHITE.y; var deltaDivider = (X + 15 * Y + 3 * Z); if (deltaDivider === 0) { @@ -559,23 +612,24 @@ LUVColor.fromXYZ = function(X, Y, Z) { 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 L = (yGamma > XYZAColor.EPSILON) ? 116 * Math.pow(yGamma, 1/3) - 16 : XYZAColor.KAPPA * yGamma; var u = 13 * L * (uDelta - uDeltaGamma); var v = 13 * L * (vDelta - vDeltagamma); - return new LUVColor(L, u, v); + return new LUVAColor(L, u, v, a === undefined ? 1 : a); } -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(); } +LUVAColor.prototype.toXYZA = function() { return XYZAColor.fromLUVA(this.l, this.u, this.v, this.a); } +LUVAColor.prototype.toRGBA = function() { return this.toXYZA().toRGBA(); } +LUVAColor.prototype.toHSLA = function() { return this.toXYZA().toHSLA(); } +LUVAColor.prototype.toHSVA = function() { return this.toXYZA().toHSVA(); } -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); } +LUVAColor.prototype._l = function(l) { return new LUVAColor(l, this.u, this.v, this.a); } +LUVAColor.prototype._u = function(u) { return new LUVAColor(this.l, u, this.v, this.a); } +LUVAColor.prototype._v = function(v) { return new LUVAColor(this.l, this.u, v, this.a); } +LUVAColor.prototype._a = function(a) { return new LUVAColor(this.l, this.u, this.v, a); } // -------------------- @@ -583,8 +637,11 @@ LUVColor.prototype._v = function(v) { return new LUVColor(this.l, this.u, v); } // -------------------- 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; + this._luv_required_bright = new XYZAColor(0, (this.settings.luv_contrast * (new RGBAColor(35,35,35,1).toXYZA().y + 0.05) - 0.05), 0, 1).toLUVA().l; + this._luv_required_dark = new XYZAColor(0, ((new RGBAColor(217,217,217,1).toXYZA().y + 0.05) / this.settings.luv_contrast - 0.05), 0, 1).toLUVA().l; + + this._luv_background_bright = new XYZAColor(0, (this.settings.luv_contrast * (RGBAColor.fromCSS("#3c3a41").toXYZA().y + 0.05) - 0.05), 0, 1).toLUVA().l; + this._luv_background_dark = new XYZAColor(0, ((RGBAColor.fromCSS("#acacbf").toXYZA().y + 0.05) / this.settings.luv_contrast - 0.05), 0, 1).toLUVA().l; } FFZ.prototype._rebuild_colors = function() { @@ -592,7 +649,7 @@ FFZ.prototype._rebuild_colors = function() { return; // With update colors, we'll automatically process the colors we care about. - this._colors = {}; + this._hex_colors = {}; this._update_colors(); } @@ -602,7 +659,8 @@ FFZ.prototype._update_colors = function(darkness_only) { 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('settings.darkMode')); + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')), + cr_dark = this.settings.dark_twitch || (Layout && Layout.get('isTheatreMode')); if ( darkness_only && this._color_old_darkness === is_dark ) return; @@ -618,96 +676,117 @@ FFZ.prototype._update_colors = function(darkness_only) { if ( ! colors ) continue; - bit.style.color = is_dark ? colors[1] : colors[0]; + bit.style.color = (bit.classList.contains('replay-color') ? cr_dark : is_dark) ? colors[1] : colors[0]; } } +FFZ.prototype._handle_filter_color = function(color) { + if (!( color instanceof RGBAColor )) + color = RGBAColor.fromCSS(color); + + var light_color = color, + dark_color = color, + luv = color.toLUVA(); + + if ( luv.l < this._luv_background_bright ) + light_color = luv._l(this._luv_background_bright).toRGBA(); + + if ( luv.l > this._luv_background_dark ) + dark_color = luv._l(this._luv_background_dark).toRGBA(); + + return [light_color, dark_color]; +} + + FFZ.prototype._handle_color = function(color) { - if ( color instanceof RGBColor ) - color = color.toHex(); + if ( color instanceof RGBAColor ) + color = color.toCSS(); - if ( ! color || this._colors.hasOwnProperty(color) ) - return this._colors[color]; + if ( ! color ) + return null; - var rgb = RGBColor.fromHex(color), + if ( this._hex_colors.hasOwnProperty(color) ) + return this._hex_colors[color]; - light_color = color, - dark_color = color; + var rgb = RGBAColor.fromCSS(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(); - } - } + light_color = rgb, + dark_color = rgb; + + // 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; + } + } - // Color Processing - RGB - if ( this.settings.fix_color === '4' ) { - var lum = rgb.luminance(); + // 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; - } + 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(); - } + light_color = nc; + } - if ( lum < 0.15 ) { - var s = 127, nc = rgb; - while(s--) { - nc = nc.brighten(); - if ( nc.luminance() >= 0.15 ) - break; - } + if ( lum < 0.15 ) { + var s = 127, nc = rgb; + while(s--) { + nc = nc.brighten(); + if ( nc.luminance() >= 0.15 ) + break; + } - dark_color = nc.toHex(); - } - } + dark_color = nc; + } + } - // Color Processing - HSL - if ( this.settings.fix_color === '2' ) { - var hsl = rgb.toHSL(); + // Color Processing - HSL + if ( this.settings.fix_color === '2' ) { + var hsl = rgb.toHSLA(); - 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(); - } + light_color = hsl._l(Math.min(Math.max(0, 0.7 * hsl.l), 1)).toRGBA(); + dark_color = hsl._l(Math.min(Math.max(0, 0.3 + (0.7 * hsl.l)), 1)).toRGBA(); + } - // Color Processing - HSV - if ( this.settings.fix_color === '3' ) { - var hsv = rgb.toHSV(); + // Color Processing - HSV + if ( this.settings.fix_color === '3' ) { + var hsv = rgb.toHSVA(); - 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(); + if ( hsv.s === 0 ) { + // Black and White + light_color = hsv._v(Math.min(Math.max(0.5, 0.5 * hsv.v), 1)).toRGBA(); + dark_color = hsv._v(Math.min(Math.max(0.5, 0.5 + (0.5 * hsv.v)), 1)).toRGBA(); - } 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(); - } - } + } else { + light_color = RGBAColor.fromHSVA(hsv.h, Math.min(Math.max(0.7, 0.7 + (0.3 * hsv.s)), 1), Math.min(0.7, hsv.v), hsv.a); + dark_color = RGBAColor.fromHSVA(hsv.h, Math.min(0.7, hsv.s), Math.min(Math.max(0.7, 0.7 + (0.3 * hsv.v)), 1), hsv.a); + } + } - // Color Processing - LUV - if ( this.settings.fix_color === '1' ) { - var luv = rgb.toLUV(); + // Color Processing - LUV + if ( this.settings.fix_color === '1' ) { + var luv = rgb.toLUVA(); - if ( luv.l > this._luv_required_dark ) - light_color = luv._l(this._luv_required_dark).toRGB().toHex(); + if ( luv.l > this._luv_required_dark ) + light_color = luv._l(this._luv_required_dark).toRGBA(); - if ( luv.l < this._luv_required_bright ) - dark_color = luv._l(this._luv_required_bright).toRGB().toHex(); - } + if ( luv.l < this._luv_required_bright ) + dark_color = luv._l(this._luv_required_bright).toRGBA(); + } - var out = this._colors[color] = [light_color, dark_color]; - return out; + var out = this._hex_colors[color] = [light_color.toHex(), dark_color.toHex()]; + return out; } \ No newline at end of file diff --git a/src/constants.js b/src/constants.js index 5ed52212..ec575f43 100644 --- a/src/constants.js +++ b/src/constants.js @@ -1,16 +1,24 @@ -var SVGPATH = '', +var SVGPATH = 'm120.95 1.74c4.08-0.09 8.33-0.84 12.21 0.82 3.61 1.8 7 4.16 11.01 5.05 2.08 3.61 6.12 5.46 8.19 9.07 3.6 5.67 7.09 11.66 8.28 18.36 1.61 9.51 7.07 17.72 12.69 25.35 3.43 7.74 1.97 16.49 3.6 24.62 2.23 5.11 4.09 10.39 6.76 15.31 1.16 2 4.38 0.63 4.77-1.32 1.2-7.1-2.39-13.94-1.97-21.03 0.38-3.64-0.91-7.48 0.25-10.99 2.74-3.74 4.57-8.05 7.47-11.67 3.55-5.47 10.31-8.34 16.73-7.64 2.26 2.89 5.13 5.21 7.58 7.92 2.88 4.3 6.52 8.01 9.83 11.97 1.89 2.61 3.06 5.64 4.48 8.52 2.81 4.9 4 10.5 6.63 15.49 2.16 6.04 5.56 11.92 5.37 18.5 0.65 1.95 0.78 4 0.98 6.03 1.01 3.95 2.84 8.55 0.63 12.42-2.4 5.23-7.03 8.97-11.55 12.33-6.06 4.66-11.62 10.05-18.37 13.75-4.06 2.65-8.24 5.17-12.71 7.08-3.59 1.57-6.06 4.94-9.85 6.09-2.29 1.71-3.98 4.51-6.97 5.02-4.56 1.35-8.98-3.72-13.5-1.25-2.99 1.83-6.19 3.21-9.39 4.6-8.5 5.61-18.13 9.48-28.06 11.62-8.36-0.2-16.69 0.62-25.05 0.47-3.5-1.87-7.67-1.08-11.22-2.83-6.19-1.52-10.93-6.01-16.62-8.61-2.87-1.39-5.53-3.16-8.11-4.99-2.58-1.88-4.17-4.85-6.98-6.44-3.83-0.11-6.54 3.42-10.24 3.92-2.31 0.28-4.64 0.32-6.96 0.31-3.5-3.65-5.69-8.74-10.59-10.77-5.01-3.68-10.57-6.67-14.84-11.25-2.52-2.55-5.22-4.87-8.24-6.8-4.73-4.07-7.93-9.51-11.41-14.62-3.08-4.41-5.22-9.73-4.6-15.19 0.65-8.01 0.62-16.18 2.55-24.02 4.06-10.46 11.15-19.34 18.05-28.06 3.71-5.31 9.91-10.21 16.8-8.39 3.25 1.61 5.74 4.56 7.14 7.89 1.19 2.7 3.49 4.93 3.87 7.96 0.97 5.85 1.6 11.86 0.74 17.77-1.7 6.12-2.98 12.53-2.32 18.9 0.01 2.92 2.9 5.36 5.78 4.57 3.06-0.68 3.99-4.07 5.32-6.48 1.67-4.06 4.18-7.66 6.69-11.23 3.61-5.28 5.09-11.57 7.63-17.37 2.07-4.56 1.7-9.64 2.56-14.46 0.78-7.65-0.62-15.44 0.7-23.04 1.32-3.78 1.79-7.89 3.8-11.4 3.01-3.66 6.78-6.63 9.85-10.26 1.72-2.12 4.21-3.32 6.55-4.6 7.89-2.71 15.56-6.75 24.06-7z', DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'), - SERVER = DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/"; - //DIRECT_SERVER = DEBUG ? "//localhost:8000/" : "//direct-cdn.frankerfacez.com/"; + SERVER = DEBUG ? "//localhost:8000/" : "https://cdn.frankerfacez.com/", -module.exports = { + 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 + "*"), + + svg = function(cls, width, height, path, viewbox) { + return ''; + }; + + +module.exports = FrankerFaceZ.constants = { DEBUG: DEBUG, SERVER: SERVER, - //DIRECT_SERVER: DIRECT_SERVER, + + // Twitch Client ID for API Stuff + CLIENT_ID: "a3bc9znoz6vi8ozsoca0inlcr4fcvkl", API_SERVER: "https://api.frankerfacez.com/", - //API_SERVER_2: "http://direct-api.frankerfacez.com/", WS_SERVER_POOLS: { 1: [ @@ -21,8 +29,13 @@ module.exports = { ["ws://localhost:8001/", 1]] }, + CHAT_COLORS: ["#FF0000", "#0000FF", "#008000", "#B22222", "#FF7F50", "#9ACD32", "#FF4500", "#2E8B57", "#DAA520", "#D2691E", "#5F9EA0", "#1E90FF", "#FF69B4", "#8A2BE2", "#00FF7F"], + TOOLTIP_DISTANCE: 50, + SEPARATORS: SEPARATORS, + SPLITTER: SPLITTER, + KNOWN_CODES: { "#-?[\\\\/]": "#-/", ":-?(?:7|L)": ":-7", @@ -48,7 +61,7 @@ module.exports = { "Gr(a|e)yFace": "GrayFace" }, - TWITCH_BASE: 'http://static-cdn.jtvnw.net/emoticons/v1/', + TWITCH_BASE: '//static-cdn.jtvnw.net/emoticons/v1/', EMOTE_MIRROR_BASE: SERVER + "twitch-emote-mirror/", EMOTE_REPLACEMENT_BASE: SERVER + "script/replacements/", @@ -68,28 +81,46 @@ module.exports = { 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, + EMOJI_REGEX: /(\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc41\u200d\ud83d\udde8|(?:[\u0023\u002a\u0030-\u0039])\ufe0f?\u20e3|(?:(?:[\u261d\u270c])(?:\ufe0f|(?!\ufe0e))|\ud83c[\udf85\udfc2-\udfc4\udfc7\udfca\udfcb]|\ud83d[\udc42\udc43\udc46-\udc50\udc66-\udc69\udc6e\udc70-\udc78\udc7c\udc81-\udc83\udc85-\udc87\udcaa\udd75\udd90\udd95\udd96\ude45-\ude47\ude4b-\ude4f\udea3\udeb4-\udeb6\udec0]|\ud83e\udd18|[\u26f9\u270a\u270b\u270d])(?:\ud83c[\udffb-\udfff]|)|\ud83c\udde6\ud83c[\udde8-\uddec\uddee\uddf1\uddf2\uddf4\uddf6-\uddfa\uddfc\uddfd\uddff]|\ud83c\udde7\ud83c[\udde6\udde7\udde9-\uddef\uddf1-\uddf4\uddf6-\uddf9\uddfb\uddfc\uddfe\uddff]|\ud83c\udde8\ud83c[\udde6\udde8\udde9\uddeb-\uddee\uddf0-\uddf5\uddf7\uddfa-\uddff]|\ud83c\udde9\ud83c[\uddea\uddec\uddef\uddf0\uddf2\uddf4\uddff]|\ud83c\uddea\ud83c[\udde6\udde8\uddea\uddec\udded\uddf7-\uddfa]|\ud83c\uddeb\ud83c[\uddee-\uddf0\uddf2\uddf4\uddf7]|\ud83c\uddec\ud83c[\udde6\udde7\udde9-\uddee\uddf1-\uddf3\uddf5-\uddfa\uddfc\uddfe]|\ud83c\udded\ud83c[\uddf0\uddf2\uddf3\uddf7\uddf9\uddfa]|\ud83c\uddee\ud83c[\udde8-\uddea\uddf1-\uddf4\uddf6-\uddf9]|\ud83c\uddef\ud83c[\uddea\uddf2\uddf4\uddf5]|\ud83c\uddf0\ud83c[\uddea\uddec-\uddee\uddf2\uddf3\uddf5\uddf7\uddfc\uddfe\uddff]|\ud83c\uddf1\ud83c[\udde6-\udde8\uddee\uddf0\uddf7-\uddfb\uddfe]|\ud83c\uddf2\ud83c[\udde6\udde8-\udded\uddf0-\uddff]|\ud83c\uddf3\ud83c[\udde6\udde8\uddea-\uddec\uddee\uddf1\uddf4\uddf5\uddf7\uddfa\uddff]|\ud83c\uddf4\ud83c\uddf2|\ud83c\uddf5\ud83c[\udde6\uddea-\udded\uddf0-\uddf3\uddf7-\uddf9\uddfc\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7\ud83c[\uddea\uddf4\uddf8\uddfa\uddfc]|\ud83c\uddf8\ud83c[\udde6-\uddea\uddec-\uddf4\uddf7-\uddf9\uddfb\uddfd-\uddff]|\ud83c\uddf9\ud83c[\udde6\udde8\udde9\uddeb-\udded\uddef-\uddf4\uddf7\uddf9\uddfb\uddfc\uddff]|\ud83c\uddfa\ud83c[\udde6\uddec\uddf2\uddf8\uddfe\uddff]|\ud83c\uddfb\ud83c[\udde6\udde8\uddea\uddec\uddee\uddf3\uddfa]|\ud83c\uddfc\ud83c[\uddeb\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c\uddfe\ud83c[\uddea\uddf9]|\ud83c\uddff\ud83c[\udde6\uddf2\uddfc]|\ud83c[\udccf\udd8e\udd91-\udd9a\udde6-\uddff\ude01\ude32-\ude36\ude38-\ude3a\ude50\ude51\udf00-\udf21\udf24-\udf84\udf86-\udf93\udf96\udf97\udf99-\udf9b\udf9e-\udfc1\udfc5\udfc6\udfc8\udfc9\udfcc-\udff0\udff3-\udff5\udff7-\udfff]|\ud83d[\udc00-\udc41\udc44\udc45\udc51-\udc65\udc6a-\udc6d\udc6f\udc79-\udc7b\udc7d-\udc80\udc84\udc88-\udca9\udcab-\udcfd\udcff-\udd3d\udd49-\udd4e\udd50-\udd67\udd6f\udd70\udd73\udd74\udd76-\udd79\udd87\udd8a-\udd8d\udda5\udda8\uddb1\uddb2\uddbc\uddc2-\uddc4\uddd1-\uddd3\udddc-\uddde\udde1\udde3\udde8\uddef\uddf3\uddfa-\ude44\ude48-\ude4a\ude80-\udea2\udea4-\udeb3\udeb7-\udebf\udec1-\udec5\udecb-\uded0\udee0-\udee5\udee9\udeeb\udeec\udef0\udef3]|\ud83e[\udd10-\udd17\udd80-\udd84\uddc0]|[\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u2602-\u2604\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638\u2692\u2694\u2696\u2697\u2699\u269b\u269c\u26b0\u26b1\u26c8\u26ce\u26cf\u26d1\u26d3\u26e9\u26f0\u26f1\u26f4\u26f7\u26f8\u2705\u271d\u2721\u2728\u274c\u274e\u2753-\u2755\u2763\u2795-\u2797\u27b0\u27bf\ue50a]|(?:\ud83c[\udc04\udd70\udd71\udd7e\udd7f\ude02\ude1a\ude2f\ude37]|[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600\u2601\u260e\u2611\u2614\u2615\u2639\u263a\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2693\u26a0\u26a1\u26aa\u26ab\u26bd\u26be\u26c4\u26c5\u26d4\u26ea\u26f2\u26f3\u26f5\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714\u2716\u2733\u2734\u2744\u2747\u2757\u2764\u27a1\u2934\u2935\u2b05-\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299])(?:\ufe0f|(?!\ufe0e)))/g, - SVGPATH: SVGPATH, - ZREKNARF: '' + SVGPATH + '', - CHAT_BUTTON: '' + SVGPATH + '', + EMOJI_CATEGORIES: { + people: "People & Smileys", + nature: "Animals & Nature", + food: "Food & Drink", + activity: "Activity", + travel: "Travel & Places", + objects: "Objects", + symbols: "Symbols", + flags: "Flags" + //modifier: "Modifiers" + }, - ROOMS: '', - CAMERA: '', - INVITE: '', + EMOJI_LOGOS: { + activity: '26bd', + food: '1f34e', + flags: '1f1fa-1f1f8', + nature: '1f436', + objects: '1f4a1', + people: '1f632', + symbols: '2049', + travel: '1f697' + //modifier: '262f' + }, - LIVE: '', - - EYE: '', - CLOCK: '', - GEAR: '', - HEART: '', - UNHEART: '', - EMOTE: '', - STAR: '', - CLOSE: '', - - EDIT: '', - - GRAPH: '' + ZREKNARF: svg('glyph_views svg-zreknarf', 16, 12.5, SVGPATH, '0 0 249 195'), + CHAT_BUTTON: svg('emoticons', 24, 18, SVGPATH, '0 0 249 195'), + ROOMS: svg('glyph_views svg-roomlist', 16, 16, 'M1,13v-2h14v2H1z M1,5h13v2H1V5z M1,2h10v2H1V2z M12,10H1V8h11V10z'), + CAMERA: svg('camera', 16, 16, 'M24,20v6H4V10h20v6l8-6v16L24,20z', '0 0 36 36'), + INVITE: svg('plus', 16, 16, 'M15,9h-3v3h-2V9H7V7h3V4h2v3h3V9z M9,6H6v4h2h1v3h4l0,0l0,0v1h-3H4H1v-1l3-3h2L4,8V2h6v1H9V6z'), + LIVE: svg('glyph_live_small', 13, 16,'M11,14H5H2v-1l3-3h2L5,8V2h6v6l-2,2h2l3,3v1H11z'), + EYE: svg('glyph_views svg-eye', 16, 16, 'M11,13H5L1,9V8V7l4-4h6l4,4v1v1L11,13z M8,5C6.344,5,5,6.343,5,8c0,1.656,1.344,3,3,3c1.657,0,3-1.344,3-3C11,6.343,9.657,5,8,5z M8,9C7.447,9,7,8.552,7,8s0.447-1,1-1s1,0.448,1,1S8.553,9,8,9z'), + CLOCK: svg('glyph_views svg-clock', 16, 16, 'M8,15c-3.866,0-7-3.134-7-7s3.134-7,7-7s7,3.134,7,7 S11.866,15,8,15z M8,3C5.238,3,3,5.238,3,8s2.238,5,5,5s5-2.238,5-5S10.762,3,8,3z M7.293,8.707L7,8l1-4l0.902,3.607L11,11 L7.293,8.707z'), + GEAR: svg('gear', 16, 16, 'M15,7v2h-2.115c-0.125,0.615-0.354,1.215-0.713,1.758l1.484,1.484l-1.414,1.414l-1.484-1.484C10.215,12.531,9.615,12.76,9,12.885V15H7v-2.12c-0.614-0.126-1.21-0.356-1.751-0.714l-1.491,1.49l-1.414-1.414l1.491-1.49C3.477,10.211,3.247,9.613,3.12,9H1V7h2.116C3.24,6.384,3.469,5.785,3.829,5.242L2.343,3.757l1.414-1.414l1.485,1.485C5.785,3.469,6.384,3.24,7,3.115V1h2v2.12c0.613,0.126,1.211,0.356,1.752,0.714l1.49-1.491l1.414,1.414l-1.49,1.492C12.523,5.79,12.754,6.387,12.88,7H15z M8,6C6.896,6,6,6.896,6,8s0.896,2,2,2s2-0.896,2-2S9.104,6,8,6z'), + HEART: svg('heart', 16, 16, 'M8,13.5L1.5,7V4l2-2h3L8,3.5L9.5,2h3l2,2v3L8,13.5z'), + UNHEART: svg('unheart', 16, 16, 'M1,9V7h14v2H1z M1,4l2-2h3l2,2l2-2h3l2,2v2H1V4z M8,14l-4.667-4h9.333L8,14z'), + EMOTE: svg('emote', 16, 16, 'M9,18c-4.971,0-9-4.029-9-9s4.029-9,9-9s9,4.029,9,9S13.971,18,9,18z M14,4.111V4h-0.111C12.627,2.766,10.904,2,9,2C7.095,2,5.373,2.766,4.111,4H4v0.111C2.766,5.373,2,7.096,2,9s0.766,3.627,2,4.889V14l0.05-0.051C5.317,15.217,7.067,16,9,16c1.934,0,3.684-0.783,4.949-2.051L14,14v-0.111c1.234-1.262,2-2.984,2-4.889S15.234,5.373,14,4.111zM11,6h2v4h-2V6z M12.535,12.535C11.631,13.44,10.381,14,9,14s-2.631-0.56-3.536-1.465l0.707-0.707C6.896,12.553,7.896,13,9,13s2.104-0.447,2.828-1.172L12.535,12.535z M5,6h2v4H5V6z', '0 0 18 18'), + STAR: svg('star', 16, 16, 'M15,6l-4.041,2.694L13,14l-5-3.333L3,14l2.041-5.306L1,6h5.077L8,1l1.924,5H15z'), + CLOSE: svg('close_small', 16, 16, 'M12.657,4.757L9.414,8l3.243,3.242l-1.415,1.415L8,9.414l-3.243,3.243l-1.414-1.415L6.586,8L3.343,4.757l1.414-1.414L8,6.586l3.242-3.243L12.657,4.757z'), + EDIT: svg('edit', 16, 16, 'M6.414,12.414L3.586,9.586l8-8l2.828,2.828L6.414,12.414z M4.829,14H2l0,0v-2.828l0.586-0.586l2.828,2.828L4.829,14z'), + GRAPH: svg('glyph_views graph', 16, 16, 'M1,16V2h16v14H1z M5,4H3v1h2V4z M5,7H3v1h2V7z M5,10H3v1h2V10zM5,13H3v1h2V13z M9,7H7v7h2V7z M12,10h-2v4h2V10z M15,4h-2v10h2V4z', '0 0 18 18') } \ No newline at end of file diff --git a/src/ember/channel.js b/src/ember/channel.js index 7c00b8ec..c608859e 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -100,30 +100,30 @@ FFZ.prototype.setup_channel = function() { var t = this, id = t.get('content.id'); - id && Twitch.api && Twitch.api.get("streams/" + id, {}, {version:3}) + id && utils.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); + t.set('content.stream.created_at', null); + t.set('content.stream.viewers', 0); return; } - t.set('stream.created_at', data.stream.created_at || null); - t.set('stream.viewers', data.stream.viewers || 0); + t.set('content.stream.created_at', data.stream.created_at || null); + t.set('content.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); + t.set('content.game', game); + t.set('content.rollbackData.game', game); } if ( data.stream.channel ) { if ( data.stream.channel.status ) - t.set('status', data.stream.channel.status); + t.set('content.status', data.stream.channel.status); if ( data.stream.channel.views ) - t.set('views', data.stream.channel.views); + t.set('content.views', data.stream.channel.views); if ( data.stream.channel.followers && t.get('content.followers.isLoaded') ) t.set('content.followers.total', data.stream.channel.followers); @@ -206,7 +206,7 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzInit: function() { - var id = this.get('controller.id'), + var id = this.get('controller.content.id') || this.get('controller.id'), el = this.get('element'); f._cindex = this; @@ -251,8 +251,8 @@ FFZ.prototype._modify_cindex = function(view) { if ( f.has_bttv || ! f.settings.stream_title ) return; - var status = this.get("controller.status"), - channel = this.get("controller.id"); + var status = this.get("controller.content.status") || this.get("controller.status"), + channel = this.get("controller.content.id") || this.get("controller.id"); status = f.render_tokens(f.tokenize_line(channel, channel, status, true)); @@ -267,7 +267,7 @@ FFZ.prototype._modify_cindex = function(view) { ffzUpdateHostButton: function() { - var channel_id = this.get('controller.id'), + var channel_id = this.get('controller.content.id') || this.get('controller.id'), hosted_id = this.get('controller.hostModeTarget.id'), user = f.get_user(), @@ -360,7 +360,7 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzClickHost: function(controller, is_host) { - var target = controller.get(is_host ? 'controller.hostModeTarget.id' : 'controller.id'), + var target = is_host ? controller.get('controller.hostModeTarget.id') : (controller.get('controller.content.id') || controller.get('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; @@ -381,7 +381,7 @@ FFZ.prototype._modify_cindex = function(view) { ffzUpdateChatters: function() { // Get the counts. - var room_id = this.get('controller.id'), + var room_id = this.get('controller.content.id') || this.get('controller.id'), room = f.rooms && f.rooms[room_id]; if ( ! room || ! f.settings.chatter_count ) { @@ -457,7 +457,7 @@ FFZ.prototype._modify_cindex = function(view) { ffzUpdatePlayerStats: function() { - var channel_id = this.get('controller.id'), + var channel_id = this.get('controller.content.id') || this.get('controller.id'), hosted_id = this.get('controller.hostModeTarget.id'), el = this.get('element'); @@ -471,13 +471,13 @@ FFZ.prototype._modify_cindex = function(view) { player = undefined, stats = undefined; try { - player = player_cont && player_cont.ffz_player; + player = player_cont && player_cont.get && player_cont.get('player'); stats = player && player.stats; } catch(err) { f.error("Channel ffzUpdatePlayerStats: player.stats: " + err); } - if ( ! container || ! f.settings.player_stats || ! stats || stats.hlsLatencyBroadcaster === 'NaN' || stats.hlsLatencyBroadcaster === NaN ) { + if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hlsLatencyBroadcaster || stats.hlsLatencyBroadcaster === 'NaN' || Number.isNaN(stats.hlsLatencyBroadcaster) ) { if ( stat_el ) stat_el.parentElement.removeChild(stat_el); } else { @@ -538,7 +538,7 @@ FFZ.prototype._modify_cindex = function(view) { } - if ( ! container || ! f.settings.player_stats || ! stats || stats.hlsLatencyBroadcaster === 'NaN' || stats.hlsLatencyBroadcaster === NaN ) { + if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hlsLatencyBroadcaster || stats.hlsLatencyBroadcaster === 'NaN' || Number.isNaN(stats.hlsLatencyBroadcaster) ) { if ( stat_el ) stat_el.parentElement.removeChild(stat_el); } else { @@ -648,7 +648,7 @@ FFZ.prototype._modify_cindex = function(view) { }, ffzTeardown: function() { - var id = this.get('controller.id'); + var id = this.get('controller.content.id') || this.get('controller.id'); if ( id ) f.ws_send("unsub", "channel." + id); diff --git a/src/ember/chatview.js b/src/ember/chatview.js index cb354b63..c325f1b7 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -13,7 +13,12 @@ FFZ.basic_settings.delayed_chat = { 0: "No Delay", 300: "Minor (Bot Moderation; 0.3s)", 1200: "Normal (Human Moderation; 1.2s)", - 5000: "Large (Spoiler Removal / Really Slow Mods; 5s)" + 5000: "Large (Spoiler Removal / Really Slow Mods; 5s)", + 10000: "Extra Large (10s)", + 15000: "Extremely Large (15s)", + 20000: "Mods Asleep; Delay Chat (20s)", + 30000: "Half a Minute (30s)", + 60000: "Why??? (1m)" }, category: "Chat", @@ -134,7 +139,12 @@ FFZ.settings_info.chat_delay = { 0: "No Delay", 300: "Minor (Bot Moderation; 0.3s)", 1200: "Normal (Human Moderation; 1.2s)", - 5000: "Large (Spoiler Removal / Really Slow Mods; 5s)" + 5000: "Large (Spoiler Removal / Really Slow Mods; 5s)", + 10000: "Extra Large (10s)", + 15000: "Extremely Large (15s)", + 20000: "Mods Asleep; Delay Chat (20s)", + 30000: "Half a Minute (30s)", + 60000: "Why??? (1m)" }, value: 0, @@ -316,6 +326,25 @@ FFZ.settings_info.visible_rooms = { // Initialization // -------------------- +FFZ.prototype.refresh_chat = function() { + var parents, lines = jQuery('ul.chat-lines'); + if ( this.has_bttv || ! lines || ! lines.length ) + return; + + parents = lines.parents('.chatReplay'); + if ( parents && parents.length ) + return; + + // There are chat-lines in the DOM and they aren't chat replay. + var controller = App.__container__.lookup('controller:chat'); + if ( ! controller ) + return; + + var current_room = controller.get("currentRoom"); + controller.blurRoom(); + controller.focusRoom(current_room); +} + FFZ.prototype.setup_chatview = function() { document.body.classList.toggle("ffz-minimal-chat-head", this.settings.minimal_chat === 1 || this.settings.minimal_chat === 3); document.body.classList.toggle("ffz-minimal-chat-input", this.settings.minimal_chat === 2 || this.settings.minimal_chat === 3); @@ -423,7 +452,7 @@ FFZ.prototype.setup_chatview = function() { } catch(err) { } // Modify all existing Chat views. - var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + var views = window.App && App.__container__.lookup('-view-registry:main'); for(var key in views) { if ( ! views.hasOwnProperty(key) ) continue; @@ -434,14 +463,18 @@ FFZ.prototype.setup_chatview = function() { this.log("Manually updating existing Chat view.", view); try { + if ( ! view.ffzInit ) + this._modify_cview(view); view.ffzInit(); } catch(err) { this.error("setup: build_ui_link: " + err); } } + /*this.log("Hooking the Ember chat-right-column Component."); + var Column = App.__container__.resolve('component:') - this.log("Hooking the Ember 'Right Column' controller. Seriously..."); + /*this.log("Hooking the Ember 'Right Column' controller. Seriously..."); var Column = App.__container__.lookup('controller:right-column'); if ( ! Column ) return; @@ -454,7 +487,7 @@ FFZ.prototype.setup_chatview = function() { },0); } }.observes("firstTabSelected") - }); + });*/ } @@ -467,7 +500,7 @@ FFZ.prototype._modify_cview = function(view) { view.reopen({ didInsertElement: function() { - this._super(); + this._super(); try { this.ffzInit(); @@ -487,10 +520,11 @@ FFZ.prototype._modify_cview = function(view) { ffzInit: function() { f._chatv = this; + this.ffz_unread = {}; + this.$('.textarea-contain').append(f.build_ui_link(this)); this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); - - this.ffz_unread = {}; + this.$('.chat-messages').find('.ffz-tooltip').tipsy({live: true, html: true, title: f.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); if ( ! f.has_bttv ) { if ( f.settings.group_tabs ) @@ -556,13 +590,19 @@ FFZ.prototype._modify_cview = function(view) { ffzChangeRoom: Ember.observer('controller.currentRoom', function() { f.update_ui_link(); + if ( ! this.ffz_unread ) + f.log("!! Chat View Not Initialized !!", this); + + // Temporary fix + this.ffz_unread = this.ffz_unread || {}; + // Close mod cards when changing to a new room. if ( f._mod_card ) f._mod_card.send('close'); var room = this.get('controller.currentRoom'), room_id = room && room.get('id'), - was_unread = room_id && this.ffz_unread[room_id], + was_unread = room_id && this.ffz_unread && this.ffz_unread[room_id], update_height = false; if ( room ) { @@ -1209,7 +1249,7 @@ FFZ.prototype.connect_extra_chat = function() { FFZ.prototype.disconnect_extra_chat = function() { - var Chat = App.__container__.lookup('controller:chat'), + var Chat = window.App && App.__container__.lookup('controller:chat'), current_channel_id = Chat && Chat.get('currentChannelRoom.id'), current_id = Chat && Chat.get('currentRoom.id'), user = this.get_user(); diff --git a/src/ember/conversations.js b/src/ember/conversations.js index 52f3c243..7270936d 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -13,7 +13,7 @@ FFZ.settings_info.conv_focus_on_click = { no_mobile: true, visible: false, - category: "Conversations", + category: "Whispers", name: "Focus Input on Click", help: "Focus on a conversation's input box when you click it." }; @@ -23,7 +23,7 @@ FFZ.settings_info.top_conversations = { value: false, no_mobile: true, - category: "Conversations", + category: "Whispers", name: "Position on Top", help: "Display the new conversation-style whisper UI at the top of the window instead of the bottom.", on_update: function(val) { @@ -32,12 +32,27 @@ FFZ.settings_info.top_conversations = { }; +FFZ.settings_info.minimize_conversations = { + type: "boolean", + value: false, + no_mobile: true, + + category: "Whispers", + name: "Minimize Whisper UI", + help: "Slide the Whisper UI mostly out of view when it's not being used and you have no unread messages.", + on_update: function(val) { + document.body.classList.toggle('ffz-minimize-conversations', val); + } + }; + + // --------------- // Initialization // --------------- FFZ.prototype.setup_conversations = function() { document.body.classList.toggle('ffz-top-conversations', this.settings.top_conversations); + document.body.classList.toggle('ffz-minimize-conversations', this.settings.minimize_conversations); this.log("Hooking the Ember Conversation Window component."); var ConvWindow = App.__container__.resolve('component:conversation-window'); @@ -52,6 +67,7 @@ FFZ.prototype.setup_conversations = function() { // TODO: Make this better later. jQuery('.conversations-list').find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); + jQuery('.conversations-list').find('.ffz-tooltip').tipsy({live: true, html: true, title: this.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); } @@ -62,8 +78,7 @@ FFZ.prototype._modify_conversation_window = function(component) { component.reopen({ headerBadges: Ember.computed("thread.participants", "currentUsername", function() { - var e = this.get("thread.participants").rejectBy("username", this.get("currentUsername")).objectAt(0), - + var e = this.get("otherUser"), badges = f.get_other_badges(e.get('username'), null, e.get('userType'), false, e.get('hasTurbo')), out = []; @@ -97,6 +112,7 @@ FFZ.prototype._modify_conversation_window = function(component) { jQuery('.badge', el).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); jQuery(el).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); + jQuery(el).find('.ffz-tooltip').tipsy({live: true, html: true, title: f.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); } }); } @@ -120,7 +136,7 @@ FFZ.prototype._modify_conversation_line = function(component) { click: function(e) { if ( e.target && e.target.classList.contains('deleted-link') ) - return f._deleted_link_click.bind(e.target)(e); + return f._deleted_link_click.call(e.target, e); if ( f._click_emote(e.target, e) ) return; @@ -133,7 +149,9 @@ FFZ.prototype._modify_conversation_line = function(component) { raw_color = this.get('message.from.color'), colors = raw_color && f._handle_color(raw_color), - is_dark = (Layout && Layout.get('isTheatreMode')) || f.settings.dark_twitch; + is_dark = (Layout && Layout.get('isTheatreMode')) || f.settings.dark_twitch, + myself = f.get_user(), + from_me = myself && myself.login === user; e.push('

'); @@ -155,7 +173,7 @@ FFZ.prototype._modify_conversation_line = function(component) { } e.push(''); - e.push(f.render_tokens(this.get('tokenizedMessage'), true)); + e.push(f.render_tokens(this.get('tokenizedMessage'), true, f.settings.filter_whispered_links && !from_me)); e.push(''); } }); diff --git a/src/ember/directory.js b/src/ember/directory.js index dc3ed711..a2e7960a 100644 --- a/src/ember/directory.js +++ b/src/ember/directory.js @@ -2,7 +2,7 @@ var FFZ = window.FrankerFaceZ, utils = require('../utils'), constants = require('../constants'), - NO_LOGO = "http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; + NO_LOGO = "//static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; // -------------------- @@ -38,6 +38,23 @@ FFZ.settings_info.sidebar_followed_games = { } } + +FFZ.settings_info.sidebar_hide_recommended_channels = { + type: "boolean", + value: true, + + category: "Appearance", + no_mobile: true, + + name: "Sidebar Recommended Channels", + help: "Display the Recommended Channels section on the sidebar.", + + on_update: function(val) { + document.body.classList.toggle('ffz-hide-recommended-channels', !val); + } + }; + + FFZ.settings_info.directory_creative_all_tags = { type: "boolean", value: false, @@ -145,9 +162,12 @@ FFZ.settings_info.directory_host_menus = { // Initialization // -------------------- +FFZ._image_cache = {}; + FFZ.prototype.setup_directory = function() { document.body.classList.toggle('ffz-creative-tags', this.settings.directory_creative_all_tags); document.body.classList.toggle('ffz-creative-showcase', this.settings.directory_creative_showcase); + document.body.classList.toggle('ffz-hide-recommended-channels', !this.settings.sidebar_hide_recommended_channels); var GamesFollowing = App.__container__.lookup('controller:games-following'), f = this; @@ -235,6 +255,8 @@ FFZ.prototype._modify_following = function() { offset: this.get('content.length') + this.get('ffz_skipped') }; + // Don't use FFZ's Client ID because loading hosts is a normal part + // of the dashboard. We're just manipulating the logic a bit. return Twitch.api.get("/api/users/:login/followed/hosting", t); }, @@ -304,8 +326,10 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { var el = this.get('element'), meta = el && el.querySelector('.meta'), thumb = el && el.querySelector('.thumb'), - cap = thumb && thumb.querySelector('.cap'); + cap = thumb && thumb.querySelector('.cap'), + channel_id = this.get('context.model.channel.name'); + el.setAttribute('data-channel', channel_id); // CSGO doesn't provide the actual uptime information... if ( !is_csgo && f.settings.stream_uptime && f.settings.stream_uptime < 3 && cap ) { @@ -319,13 +343,15 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { this.ffzUpdateUptime(); } + this._ffz_image_timer = setInterval(this.ffzRotateImage.bind(this), 30000); + this.ffzRotateImage(); + if ( f.settings.directory_logos ) { el.classList.add('ffz-directory-logo'); var link = document.createElement('a'), logo = document.createElement('img'), - t = this, - target = this.get('context.model.channel.name'); + t = this; logo.className = 'profile-photo'; logo.classList.toggle('is-csgo', is_csgo); @@ -333,14 +359,17 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { logo.src = this.get('context.model.channel.logo') || NO_LOGO; logo.alt = this.get('context.model.channel.display_name'); - link.href = '/' + target; + link.href = '/' + channel_id; link.addEventListener('click', function(e) { + if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) + return; + var Channel = App.__container__.resolve('model:channel'); if ( ! Channel ) return; e.preventDefault(); - t.get('controller').transitionTo('channel.index', Channel.find({id: target}).load()); + t.get('controller').transitionTo('channel.index', Channel.find({id: channel_id}).load()); return false; }); @@ -358,9 +387,23 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { if ( this._ffz_uptime_timer ) clearInterval(this._ffz_uptime_timer); + if ( this._ffz_image_timer ) + clearInterval(this._ffz_image_timer); + this._super(); }, + ffzRotateImage: function() { + var url = this.get('context.model.preview.medium'), + now = Math.round((new Date).getTime() / 150000); + + if ( FFZ._image_cache[url] && FFZ._image_cache[url] !== now ) + url += '?_=' + now; + else + FFZ._image_cache[url] = now; + + this.$('.thumb .cap img').attr('src', url); + }, ffzUpdateUptime: function() { var raw_created = this.get('context.model.created_at'), @@ -411,7 +454,7 @@ FFZ.prototype._modify_directory_host = function(dir) { return; if ( e ) { - if ( e.button !== 0 ) + if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) return; e.preventDefault(); @@ -424,7 +467,7 @@ FFZ.prototype._modify_directory_host = function(dir) { }, ffzShowHostMenu: function(e) { - if ( e.button !== 0 ) + if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) return; e.preventDefault(); @@ -485,10 +528,25 @@ FFZ.prototype._modify_directory_host = function(dir) { f.show_popup(menu, [x, y], document.querySelector('#main_col > .tse-scroll-content > .tse-content')); }, + ffzRotateImage: function() { + var url = this.get('context.model.target.preview'), + now = Math.round((new Date).getTime() / 150000); + + if ( FFZ._image_cache[url] && FFZ._image_cache[url] !== now ) + url += '?_=' + now; + else + FFZ._image_cache[url] = now; + + this.$('.thumb .cap img').attr('src', url); + }, + ffzCleanup: function() { var target = this.get('context.model.target.channel'); if ( f._popup && f._popup.classList.contains('ffz-channel-selector') && f._popup.getAttribute('data-channel') === target ) f.close_popup(); + + if ( this._ffz_image_timer ) + clearInterval(this._ffz_image_timer); }, ffzInit: function() { @@ -503,6 +561,10 @@ FFZ.prototype._modify_directory_host = function(dir) { boxart = thumb && thumb.querySelector('.boxart'); + el.setAttribute('data-channel', target.name); + + this._ffz_image_timer = setInterval(this.ffzRotateImage.bind(this), 30000); + this.ffzRotateImage(); // Fix the game not showing if ( ! boxart && thumb && this.get('context.model.game') ) { @@ -516,6 +578,9 @@ FFZ.prototype._modify_directory_host = function(dir) { boxart.setAttribute('original-title', game); boxart.addEventListener('click', function(e) { + if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) + return; + e.preventDefault(); jQuery('.tipsy').remove(); diff --git a/src/ember/following.js b/src/ember/following.js index 321f2a76..bd0b34bb 100644 --- a/src/ember/following.js +++ b/src/ember/following.js @@ -11,7 +11,7 @@ FFZ.settings_info.enhance_profile_following = { type: "boolean", value: true, - category: "Appearance", + category: "Directory", name: "Enhanced Following Control", help: "Display additional controls on your own profile's Following tab to make management easier." } @@ -33,10 +33,8 @@ FFZ.prototype.setup_profile_following = function() { // First, we need to hook the model. This is what we'll use to grab the following notification state, // rather than making potentially hundreds of API requests. var Following = App.__container__.resolve('model:kraken-channel-following'); - if ( ! Following ) - return; - - this._hook_following(Following); + if ( Following ) + this._hook_following(Following); // Also try hooking that other model. var Notification = App.__container__.resolve('model:notification'); @@ -71,7 +69,7 @@ FFZ.prototype.setup_profile_following = function() { ffzInit: function() { // Only process our own profile following page. var user = f.get_user(); - if ( ! f.settings.enhance_profile_following || ! user || user.login !== this.get('context.id') ) + if ( ! f.settings.enhance_profile_following || ! user || user.login !== this.get('context.model.id') ) return; var el = this.get('element'), @@ -99,6 +97,9 @@ FFZ.prototype.setup_profile_following = function() { following.load(); } + // Make sure the Following is modified. + f._hook_following(following); + // We use this weird function to prevent trying to load twice mucking things up. setTimeout(refresher); } @@ -118,14 +119,15 @@ FFZ.prototype.setup_profile_following = function() { continue; // Is it an ember-view? Check its kids. - if ( added.classList.contains('ember-view') ) { + if ( added.classList.contains('user') ) + t.ffzProcessUser(added); + + else if ( added.classList.contains('ember-view') ) { var users = added.querySelectorAll('.user.item'); if ( users ) for(var y=0; y < users.length; y++) t.ffzProcessUser(users[y]); - - } else if ( added.classList.contains('user') ) - t.ffzProcessUser(added); + } } } }); @@ -215,8 +217,8 @@ FFZ.prototype.setup_profile_following = function() { follow.textContent = 'Updating'; (was_following ? - Twitch.api.del("users/:login/follows/channels/" + user_id) : - Twitch.api.put("users/:login/follows/channels/" + user_id, {notifications: false})) + utils.api.del("users/:login/follows/channels/" + user_id) : + utils.api.put("users/:login/follows/channels/" + user_id, {notifications: false})) .done(function() { data = f._following_cache[user_id] = was_following ? null : [new Date(Date.now() - (f._ws_server_offset||0)), false]; }) @@ -235,7 +237,7 @@ FFZ.prototype.setup_profile_following = function() { notif.disabled = true; notif.textContent = 'Updating'; - Twitch.api.put("users/:login/follows/channels/" + user_id, {notifications: !was_following}) + utils.api.put("users/:login/follows/channels/" + user_id, {notifications: !was_following}) .done(function() { data[1] = ! was_following; }) @@ -254,37 +256,106 @@ FFZ.prototype.setup_profile_following = function() { } }); + /*// Try adding a nice Manage button to the Following page of the directory + var DirectoryFollowing = App.__container__.resolve('view:directory'); + if ( DirectoryFollowing ) { + DirectoryFollowing.reopen({ + didInsertElement: function() { + this._super(); + try { + this.ffzInit(); + } catch(err) { + f.error("view:following ffzInit: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("view:following ffzTeardown: " + err); + } + this._super(); + }, + + ffzInit: function() { + f.log("view:directory ffzInit called!", this); + + var el = this.get('element'), + nav_bar = el && el.querySelector('.directory_header ul.nav'), + user = f.get_user(); + + if ( ! nav_bar || ! user || ! user.login || ! f.settings.enhance_profile_following ) + return; + + var li = document.createElement('li'), + btn = document.createElement('a'), + t = this; + + li.className = 'ffz-manage-following'; + btn.innerHTML = 'Manage Following'; + btn.href = '/' + user.login + '/profile/following'; + + btn.addEventListener('click', function(e) { + var Channel = App.__container__.resolve('model:channel'); + if ( ! Channel ) + return; + + e.preventDefault(); + t.get('controller').transitionTo('profile.following', Channel.find({id: user.login}).load()); + return false; + }); + + li.appendChild(btn); + nav_bar.appendChild(li); + }, + + ffzTeardown: function() { + this.$('.ffz-manage-following').remove(); + } + }); + + try { + DirectoryFollowing.create().destroy(); + } catch(err) { } + } else + f.log("Could not find Directory Following View.");*/ + // Now, rebuild any views. try { ProfileView.create().destroy(); } catch(err) { } - var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; - for(var key in views) { - var view = views[key]; - if ( ! view || !(view instanceof ProfileView) ) - continue; + var views = window.App && App.__container__.lookup('-view-registry:main'); + if ( views ) + for(var key in views) { + var view = views[key]; + /*if ( DirectoryFollowing && view instanceof DirectoryFollowing ) { + try { + view.ffzInit(); + } catch(err) { + this.error("setup: view:following: ffzInit: " + err); + } - this.log("Manually updating existing Following View.", view); - try { - var following = view.get('context.following'); - this._hook_following(following); - } catch(err) { - this.error("setup: view:channel/following: model hook: " + err); - } - - try { - view.ffzInit(); - } catch(err) { - this.error("setup: view:channel/following: " + err); - } - } + } else*/ if ( view instanceof ProfileView ) { + this.log("Manually updating existing Following View.", view); + try { + view.ffzInit(); + } catch(err) { + this.error("setup: view:channel/following: " + err); + } + } + } } FFZ.prototype._hook_following = function(Following) { var f = this; + if ( Following.ffz_hooked ) + return; + Following.reopen({ + ffz_hooked: true, apiLoad: function(e) { var user = f.get_user(), channel_id = this.get('id'), diff --git a/src/ember/layout.js b/src/ember/layout.js index 5da9d789..325c4967 100644 --- a/src/ember/layout.js +++ b/src/ember/layout.js @@ -1,4 +1,5 @@ -var FFZ = window.FrankerFaceZ; +var FFZ = window.FrankerFaceZ, + utils = require('../utils'); // -------------------- @@ -78,11 +79,11 @@ FFZ.settings_info.flip_dashboard = { type: "boolean", value: false, - category: "Appearance", + category: "Dashboard", no_mobile: true, no_bttv: true, - name: "Swap Dashboard Positions", + name: "Swap Column Positions", help: "Swap the positions of the left and right columns of the dashboard.", on_update: function(val) { @@ -106,17 +107,19 @@ FFZ.settings_info.right_column_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); + var f = this, + old_val = this.settings.right_column_width || 340; - if ( new_val === null || new_val === undefined ) - return; + utils.prompt("Right Sidebar Width", "Please enter a new width for the right sidebar, in pixels.

Minimum: 250
Default: 340", old_val, function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - var width = parseInt(new_val); - if ( ! width || width === NaN ) - width = 340; + var width = parseInt(new_val); + if ( ! width || Number.isNaN(width) || ! Number.isFinite(width) ) + width = 340; - this.settings.set('right_column_width', Math.max(250, width)); + f.settings.set('right_column_width', Math.max(250, width)); + }); }, on_update: function(val) { @@ -220,7 +223,7 @@ FFZ.prototype.setup_layout = function() { height = size[1], host_height = size[2]; - return ""; + return ""; }.property("playerSize"), ffzPortraitWarning: function() { diff --git a/src/ember/line.js b/src/ember/line.js index feba7407..ea81e761 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -1,15 +1,27 @@ 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 + "*"); + constants = require("../constants"); // --------------------- // Settings // --------------------- +FFZ.settings_info.alias_italics = { + type: "boolean", + value: true, + + category: "Chat Appearance", + no_bttv: true, + + name: "Display Aliases in Italics", + help: "Format the names of users that have aliases with italics to make it obvious at a glance that they have been renamed.", + + on_update: function(val) { + document.body.classList.toggle('ffz-alias-italics', val); + } + }; + FFZ.settings_info.room_status = { type: "boolean", value: true, @@ -32,7 +44,7 @@ FFZ.settings_info.replace_bad_emotes = { value: true, category: "Chat Appearance", - no_bttv: true, + warn_bttv: "Only affects Whispers when BetterTTV is enabled.", 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." @@ -44,7 +56,8 @@ FFZ.settings_info.parse_emoji = { options: { 0: "No Images / Font Only", 1: "Twitter Emoji Images", - 2: "Google Noto Images" + 2: "Google Noto Images", + 3: "EmojiOne Images" }, value: 1, @@ -63,24 +76,7 @@ FFZ.settings_info.parse_emoji = { name: "Emoji Display", help: "Replace emoji in chat messages with nicer looking images from either Twitter or Google." - }; - - -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 = { @@ -94,27 +90,32 @@ FFZ.settings_info.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; + var f = this; + utils.prompt( + "Scrollback Length", + "Please enter a new maximum length for the chat scrollback. Please note that setting this too high may cause your computer to begin lagging as chat messages accumulate.

Default: 150", + this.settings.scrollback_length, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - new_val = parseInt(new_val); - if ( new_val === NaN ) - return; + new_val = parseInt(new_val); + if ( Number.isNaN(new_val) || ! Number.isFinite(new_val) ) + new_val = 150; - if ( new_val < 10 ) - new_val = 10; + new_val = Math.max(10, new_val); - this.settings.set("scrollback_length", new_val); + f.settings.set("scrollback_length", new_val); - // Update our everything. - var Chat = App.__container__.lookup('controller:chat'), - current_id = Chat && Chat.get('currentRoom.id'); + // 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 && room.room.set('messageBufferSize', new_val + ((this._roomv && !this._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0)); - } + for(var room_id in f.rooms) { + var room = f.rooms[room_id]; + room.room && room.room.set('messageBufferSize', new_val + ((f._roomv && !f._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0)); + } + }); } }; @@ -127,19 +128,19 @@ FFZ.settings_info.hosted_sub_notices = { no_bttv: true, name: "Show Hosted Channel Subscriber Notices", - help: "Display notices in chat when someone subscribes to the hosted channel." + help: "Display (or more specifically hides when disabled) notices in chat when someone subscribes to the hosted channel." }; -FFZ.settings_info.filter_bad_shorteners = { +FFZ.settings_info.filter_whispered_links = { type: "boolean", value: true, category: "Chat Filtering", - no_bttv: true, + warn_bttv: "Only affects Whispers when BetterTTV is enabled.", - name: "Auto-Hide Potentially Dangerous Shortened Links", - help: "Replace potentially dangerous shortened links. Links are still accessible, but require an extra click to access." + name: "Auto-Hide Potentially Dangerous Whispered Links", + help: "Removes whispered links and displays a placeholder, with a warning that the link has not been approved by moderation or staff. Links remain accessible with an additional click." }; @@ -148,28 +149,35 @@ FFZ.settings_info.banned_words = { value: [], category: "Chat Filtering", - no_bttv: true, + + warn_bttv: "Only affects Whispers when BetterTTV is enabled.", 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); + var f = this, + old_val = this.settings.banned_words.join(", "); - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Banned Words", + "Please enter a comma-separated list of words that you would like to have removed from chat messages.", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - new_val = new_val.trim().split(SPLITTER); - var vals = []; + new_val = new_val.trim().split(constants.SPLITTER); + var vals = []; - for(var i=0; i < new_val.length; i++) - new_val[i] && vals.push(new_val[i]); + 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 = []; + if ( vals.length == 1 && vals[0] == "disable" ) + vals = []; - this.settings.set("banned_words", vals); + f.settings.set("banned_words", vals); + }); } }; @@ -179,29 +187,36 @@ FFZ.settings_info.keywords = { value: [], category: "Chat Filtering", - no_bttv: true, + + warn_bttv: "Only affects Whispers when BetterTTV is enabled.", 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); + var f = this, + old_val = this.settings.keywords.join(", "); - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Highlight Keywords", + "Please enter a comma-separated list of words that you would like to be highlighted in chat.", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - // Split them up. - new_val = new_val.trim().split(SPLITTER); - var vals = []; + // Split them up. + new_val = new_val.trim().split(constants.SPLITTER); + var vals = []; - for(var i=0; i < new_val.length; i++) - new_val[i] && vals.push(new_val[i]); + 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 = []; + if ( vals.length == 1 && vals[0] == "disable" ) + vals = []; - this.settings.set("keywords", vals); + f.settings.set("keywords", vals); + }); } }; @@ -211,7 +226,7 @@ FFZ.settings_info.clickable_emoticons = { value: false, category: "Chat Tooltips", - no_bttv: true, + warn_bttv: "Only affects Whispers when BetterTTV is enabled.", no_mobile: true, name: "Emoticon Information Pages", @@ -388,17 +403,23 @@ FFZ.settings_info.chat_font_family = { help: "Change the font used for rendering chat messages.", method: function() { - var old_val = this.settings.chat_font_family || "", - new_val = prompt("Chat Font Family\n\nPlease enter a font family to use rendering chat. Leave this blank to use the default.", old_val); + var f = this, + old_val = this.settings.chat_font_family || ""; - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Chat Font Family", + "Please enter a font family to use rendering chat. Leave this blank to use the default.", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - // Should we wrap this with quotes? - if ( ! new_val ) - new_val = null; + // Should we wrap this with quotes? + if ( ! new_val ) + new_val = null; - this.settings.set("chat_font_family", new_val); + f.settings.set("chat_font_family", new_val); + }); }, on_update: function(val) { @@ -434,17 +455,23 @@ FFZ.settings_info.chat_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); + var f = this, + old_val = this.settings.chat_font_size; - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Chat Font Size", + "Please enter a new size for the chat font.

Default: 12", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - var parsed = parseInt(new_val); - if ( ! parsed || parsed === NaN || parsed < 1 ) - parsed = 12; + var parsed = parseInt(new_val); + if ( ! parsed || Number.isNaN(parsed) || parsed < 1 ) + parsed = 12; - this.settings.set("chat_font_size", parsed); + f.settings.set("chat_font_size", parsed); + }); }, on_update: function(val) { @@ -463,7 +490,7 @@ FFZ.settings_info.chat_font_size = { } 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.on_update.call(this, this.settings.chat_ts_size); } }; @@ -479,21 +506,26 @@ FFZ.settings_info.chat_ts_size = { help: "Make the chat timestamp font bigger or smaller.", method: function() { - var old_val = this.settings.chat_ts_size; + var f = this, + 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); + utils.prompt( + "Chat Timestamp Font Size", + "Please enter a new size for the chat timestamp font. The default is to match the regular chat font size.", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - if ( new_val === null || new_val === undefined ) - return; + var parsed = parseInt(new_val); + if ( parsed < 1 || Number.isNaN(parsed) || ! Number.isFinite(parsed) ) + parsed = null; - var parsed = parseInt(new_val); - if ( ! parsed || parsed === NaN || parsed < 1 ) - parsed = null; - - this.settings.set("chat_ts_size", parsed); + f.settings.set("chat_ts_size", parsed); + }); }, on_update: function(val) { @@ -539,11 +571,13 @@ FFZ.prototype.setup_line = function() { document.head.appendChild(s); // Initial calculation. - FFZ.settings_info.chat_font_size.on_update.bind(this)(this.settings.chat_font_size); - FFZ.settings_info.chat_font_family.on_update.bind(this)(this.settings.chat_font_family); + FFZ.settings_info.chat_font_size.on_update.call(this, this.settings.chat_font_size); + FFZ.settings_info.chat_font_family.on_update.call(this, this.settings.chat_font_family); // Chat Enhancements + document.body.classList.toggle('ffz-alias-italics', this.settings.alias_italics); + this.toggle_style('chat-setup', !this.has_bttv && (this.settings.chat_rows || this.settings.chat_separators)); this.toggle_style('chat-padding', !this.has_bttv && this.settings.chat_padding); @@ -560,17 +594,30 @@ FFZ.prototype.setup_line = function() { this._last_row = {}; - this.log("Hooking the Ember Whisper Line component."); + this.log("Hooking the Ember Chat Line component."); + var Line = App.__container__.resolve('component:chat-line'); + + if ( Line ) + this._modify_chat_line(Line); + + /*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'); + var Message = App.__container__.resolve('component:message-line'); - if ( Line ) - this._modify_line(Line); + if ( Message ) + this._modify_line(Message);*/ + + this.log("Hooking the Ember VOD Chat Line component."); + var VOD = App.__container__.resolve('component:vod-chat-line'); + if ( VOD ) + this._modify_vod_line(VOD); + else + this.log("Couldn't find VOD Chat Line component."); // Store the capitalization of our own name. var user = this.get_user(); @@ -585,7 +632,342 @@ FFZ.prototype.save_aliases = function() { } -FFZ.prototype._modify_line = function(component) { +FFZ.prototype._modify_chat_line = function(component, is_vod) { + var f = this, + Layout = App.__container__.lookup('controller:layout'), + Settings = App.__container__.lookup('controller:settings'); + + component.reopen({ + tokenizedMessage: function() { + try { + return f.tokenize_chat_line(this.get('msgObject')); + } catch(err) { + f.error("chat-line tokenizedMessage: " + err); + return this._super(); + } + }.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"), + + lineChanged: Ember.observer("msgObject.deleted", "isModeratorOrHigher", "msgObject.ffz_old_messages", function() { + this.$(".mod-icons").replaceWith(this.buildModIconsHTML()); + if ( this.get("msgObject.deleted") ) { + this.$(".message").replaceWith(this.buildDeletedMessageHTML()); + } else + this.$(".deleted,.message").replaceWith(this.buildMessageHTML()); + }), + + 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('isGlobalMod') ) + return 2; + else if ( this.get('isModerator') ) + return 1; + return 0; + }.property('msgObject.labels.[]'), + + buildModIconsHTML: function() { + var user = this.get('msgObject.from'), + room_id = this.get('msgObject.room'), + room = f.rooms && f.rooms[room_id], + + deleted = this.get('msgObject.deleted'), + + 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, + + output; + + if ( is_whisper || this_ul >= other_ul || f.settings.mod_buttons.length === 0 ) + return ''; + + output = ''; + + for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) { + var pair = f.settings.mod_buttons[i], + prefix = pair[0], btn = pair[1], + + cmd, tip; + + if ( btn === false ) { + if ( deleted ) + output += 'Unban'; + else + output += 'Ban'; + + } else if ( btn === 600 ) + output += 'Timeout'; + + else { + if ( typeof btn === "string" ) { + cmd = btn.replace(/{user}/g, user).replace(/ * */, "\n"); + tip = "Custom Command" + (cmd.indexOf("\n") !== -1 ? 's' : '') + '\n' + cmd; + } else { + cmd = "/timeout " + user + " " + btn; + tip = "Timeout User (" + utils.duration_string(btn) + ")"; + } + output += '' + prefix + ''; + } + } + + return output + ''; + }, + + buildSenderHTML: function() { + var user = this.get('msgObject.from'), + room_id = this.get('msgObject.room'), + room = f.rooms && f.rooms[room_id], + + deleted = this.get('msgObject.deleted'), + r = this, + + recipient = this.get('msgObject.to'), + is_whisper = recipient && recipient.length, + is_replay = this.get('ffz_is_replay'), + + this_ul = this.get('ffzUserLevel'), + other_ul = room && room.room && room.room.get('ffzUserLevel') || 0, + + raw_color = this.get('msgObject.color'), + colors = raw_color && f._handle_color(raw_color), + + is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('settings.darkMode'))), + + output = ''; + + + output = '

' + this.get('timestamp') + ' '; + + // Moderator Actions + output += this.buildModIconsHTML(); + + // Badges + output += '' + f.render_badges(f.get_line_badges(this.get('msgObject'), is_whisper)) + ''; + + // Alias Support + 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' + (is_replay ? ' replay-color' : '') : ''; + + output += '' + utils.sanitize(alias); + else + output += '">' + utils.sanitize(name); + + + // Whisper Legacy Sucks + 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' : ''; + + output += ""; + output += '' + utils.sanitize(to_alias); + else + output += '">' + utils.sanitize(to_name); + } + + return output + ': '; + }, + + buildDeletedMessageHTML: function() { + return '<message deleted>'; + }, + + buildMessageHTML: function() { + var output, + recipient = this.get('msgObject.to'), + is_whisper = recipient && recipient.length; + + if ( this.get('msgObject.style') === 'action' ) { + var raw_color = this.get('msgObject.color'), + colors = raw_color && f._handle_color(raw_color), + is_replay = this.get('ffz_is_replay'), + is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('settings.darkMode'))); + + if ( raw_color ) + output = ''; + else + output = ''; + } else + output = ''; + + output += f.render_tokens(this.get('tokenizedMessage'), true, is_whisper && f.settings.filter_whispered_links && this.get("ffzUserLevel") < 4); + + var old_messages = this.get('msgObject.ffz_old_messages'); + if ( old_messages && old_messages.length ) + output += '
Show ' + utils.number_commas(old_messages.length) + ' Old
'; + + return output + '
'; + }, + + tagName: "li", + classNameBindings: is_vod ? ["msgObject.ffz_has_mention:ffz-mentioned"] : [":message-line", ":chat-line", "msgObject.style", "msgObject.ffz_has_mention:ffz-mentioned", "ffzWasDeleted:ffz-deleted", "ffzHasOldMessages:clearfix", "ffzHasOldMessages:ffz-has-deleted"], + attributeBindings: ["msgObject.room:data-room", "msgObject.from:data-sender", "msgObject.deleted:data-deleted"], + + render: function(e) { + e.push(this.buildSenderHTML()); + if ( this.get("msgObject.deleted") ) + e.push(this.buildDeletedMessageHTML()) + else + e.push(this.buildMessageHTML()); + }, + + 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"), + + click: function(e) { + if ( ! e.target ) + return; + + var cl = e.target.classList, + from = this.get("msgObject.from"); + + if ( cl.contains('ffz-old-messages') ) + return f._show_deleted(this.get('msgObject.room')); + + else if ( cl.contains('deleted-word') ) { + jQuery(e.target).trigger('mouseout'); + e.target.outerHTML = e.target.getAttribute('data-text'); + + } else if ( cl.contains('deleted-link') ) + return f._deleted_link_click.call(e.target, e); + + else if ( cl.contains('mod-icon') ) { + jQuery(e.target).trigger('mouseout'); + e.preventDefault(); + + if ( cl.contains('custom') ) { + var room_id = this.get('msgObject.room'), + room = room_id && f.rooms[room_id] && f.rooms[room_id].room, + cmd = e.target.getAttribute('data-cmd'); + + if ( room ) { + var lines = cmd.split("\n"); + for(var i=0; i < lines.length; i++) + room.send(lines[i], true); + + if ( cl.contains('is-timeout') ) + room.clearMessages(from); + } + return; + + } else if ( cl.contains('ban') ) + this.sendAction("banUser", {user:from}); + + else if ( cl.contains('unban') ) + this.sendAction("unbanUser", {user:from}); + + else if ( cl.contains('timeout') ) + this.sendAction("timeoutUser", {user:from}); + + } else if ( cl.contains('badge') ) { + if ( cl.contains('turbo') ) + window.open("/products/turbo?ref=chat_badge", "_blank"); + + else if ( cl.contains('subscriber') ) + this.sendAction("clickSubscriber"); + + } else if ( f._click_emote(e.target, e) ) + return; + + else if ( e.target.classList.contains('from') ) { + var n = this.$(); + this.sendAction("showModOverlay", { + left: n.offset().left, + top: n.offset().top + n.height(), + sender: from + }); + + } else if ( e.target.classList.contains('undelete') ) + this.set("msgObject.deleted", false); + } + }); +} + + +FFZ.prototype._modify_vod_line = function(component) { + var f = this; + // We need to override a few things. + this._modify_chat_line(component, true); + + component.reopen({ + ffz_is_replay: true, + + /*lineChanged: Ember.observer("msgObject.deleted", "isModeratorOrHigher", function() { + this.$(".mod-icons").replaceWith(this.buildModIconsHTML()); + if ( this.get("msgObject.deleted") ) + this.$(".message").replaceWith(this.buildMessageHTML()); + else + this.$(".deleted").replaceWith(this.buildMessageHTML()); + }),*/ + + buildModIconsHTML: function() { + if ( ! this.get("isViewerModeratorOrHigher") || this.get("isModeratorOrHigher") ) + return ""; + + return '' + + (this.get('msgObject.deleted') ? + '' : + 'Delete') + ''; + }, + + buildDeletedMesageHTML: function() { + return '<message deleted>'; + }, + + render: function(e) { + if ( this.get('msgObject.isHorizontalLine') ) + e.push(this.buildHorizontalLineHTML()); + else { + e.push(this.buildSenderHTML()); + if ( this.get("msgObject.deleted") ) + e.push(this.buildDeletedMessageHTML()) + else + e.push(this.buildMessageHTML()); + } + }, + + click: function(e) { + if ( e.target.classList.contains('delete') ) { + e.preventDefault(); + this.sendAction("timeoutUser", this.get("msgObject.id")); + } + }, + + didInsertElement: function() { + this._super(); + if ( this.get("msgObject.ffz_has_mention") ) + this.get("element").classList.add("ffz-mentioned"); + } + }); +} + + +/*FFZ.prototype._modify_line = function(component) { var f = this, Layout = App.__container__.lookup('controller:layout'), @@ -593,20 +975,6 @@ FFZ.prototype._modify_line = function(component) { component.reopen({ - tokenizedMessage: function() { - try { - return f.tokenize_chat_line(this.get('msgObject')); - } catch(err) { - f.error("chat-line tokenizedMessage: " + err); - return this._super(); - } - - }.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')); @@ -640,147 +1008,12 @@ FFZ.prototype._modify_line = function(component) { 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('isGlobalMod') ) - 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, + e.push(this.buildSenderHTML()); + e.push(this.buildMessageHTML()) + }, - 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, - - raw_color = this.get('msgObject.color'), - colors = raw_color && f._handle_color(raw_color), - - is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); - - - e.push('
'); - e.push('' + this.get("timestamp") + ' '); - - - // Moderation actions - if ( ! is_whisper && this_ul < other_ul ) { - e.push(''); - for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) { - var pair = f.settings.mod_buttons[i], - prefix = pair[0], btn = pair[1], - - cmd, tip; - - if ( btn === false ) { - if ( deleted ) - e.push('Unban'); - else - e.push('Ban'); - - } else if ( btn === 600 ) - e.push('Timeout'); - - else { - if ( typeof btn === "string" ) { - cmd = btn.replace(/{user}/g, user).replace(/ * */, "\n"); - tip = 'Custom Command' + (cmd.indexOf('\n') !== -1 ? 's' : '') + '\n' + cmd; - } else { - cmd = "/timeout " + user + " " + btn; - tip = "Timeout User (" + utils.duration_string(btn) + ")"; - } - - e.push('' + prefix + ''); - } - } - - e.push(''); - } - - - // Badges - e.push(''); - e.push(f.render_badges(f.get_line_badges(this.get('msgObject'), is_whisper))); - e.push(''); - - - // Handle aliases - 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 it's a whisper, we need to get that user's color, alias, and draw the whisper arrow thing. - 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) + ''); - } - - - // Finally, onto the message proper. - 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_has_mention:ffz-mentioned', - 'ffzWasDeleted:ffz-deleted', - 'ffzHasOldMessages:clearfix', - 'ffzHasOldMessages:ffz-has-deleted' - ], - - - ffzWasDeleted: function() { + ffzWasDeleted: function() { return f.settings.prevent_clear && this.get('msgObject.ffz_deleted'); }.property('msgObject.ffz_deleted'), @@ -789,6 +1022,12 @@ FFZ.prototype._modify_line = function(component) { return old_messages && old_messages.length; }.property('msgObject.ffz_old_messages'), + classNameBindings: [ + 'msgObject.ffz_has_mention:ffz-mentioned', + 'ffzWasDeleted:ffz-deleted', + 'ffzHasOldMessages:clearfix', + 'ffzHasOldMessages:ffz-has-deleted' + ], didInsertElement: function() { this._super(); @@ -800,7 +1039,7 @@ FFZ.prototype._modify_line = function(component) { el.setAttribute('data-deleted', this.get('msgObject.deleted') || false); } }); -} +}*/ // --------------------- @@ -843,49 +1082,67 @@ FFZ.get_capitalization = function(name, callback) { // --------------------- FFZ.prototype._remove_banned = function(tokens) { - var banned_words = this.settings.banned_words, - banned_links = this.settings.filter_bad_shorteners ? ['apo.af', 'goo.gl', 'j.mp', 'bit.ly'] : null, - - has_banned_words = banned_words && banned_words.length; - - if ( !has_banned_words && (! banned_links || ! banned_links.length) ) + var banned_words = this.settings.banned_words; + if ( ! banned_words || ! banned_words.length ) return tokens; if ( typeof tokens == "string" ) tokens = [tokens]; var regex = FFZ._words_to_regex(banned_words), - link_regex = this.settings.filter_bad_shorteners && FFZ._words_to_regex(banned_links), new_tokens = []; for(var i=0, l = tokens.length; i < l; 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 && this.settings.filter_bad_shorteners && link_regex.test(token.href) ) - new_tokens.push({ - isLink: true, - href: token.href, - isDeleted: true, - isLong: false, - isShortened: true - }); - else - new_tokens.push(token); + if ( typeof token === "string" ) + token = {type: "text", text: token}; - } else if ( has_banned_words ) - new_tokens.push(token.replace(regex, "$1***")); - else - new_tokens.push(token); + if ( token.type === "text" && regex.test(token.text) ) { + token = token.text.replace(regex, function(all, prefix, match) { + if ( prefix.length ) + new_tokens.push({type: "text", text: prefix}); + new_tokens.push({ + type: "deleted", + length: match.length, + text: match + }); + + return ""; + }); + + if ( token ) + new_tokens.push({type: "text", text: token}); + + } else if ( token.type === "emoticon" && regex.test(token.altText) ) { + token = token.altText.replace(regex, function(all, prefix, match) { + if ( prefix.length ) + new_tokens.push({type: "text", text: prefix}); + new_tokens.push({ + type: "deleted", + length: match.length, + text: match + }); + + return ""; + }); + + if ( token ) + new_tokens.push({type: "text", text: token}); + + } else if ( token.type === "link" && regex.test(token.text) ) + new_tokens.push({ + type: "link", + isDeleted: true, + isMailTo: token.isMailTo, + isLong: false, + length: token.text.length, + censoredLink: token.text.replace(regex, "$1***"), + link: token.link, + text: token.text + }); + + else + new_tokens.push(token); } return new_tokens; diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index a5373bcc..49ab5ce0 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -158,7 +158,9 @@ FFZ.settings_info.mod_buttons = { help: "Change out the different in-line moderation icons to use any command quickly.", method: function() { - var old_val = ""; + var f = this, + old_val = ""; + for(var i=0; i < this.settings.mod_buttons.length; i++) { var pair = this.settings.mod_buttons[i], prefix = pair[0], cmd = pair[1], had_prefix = pair[2]; @@ -182,91 +184,102 @@ FFZ.settings_info.mod_buttons = { old_val += ' ' + prefix + cmd; } - var new_val = prompt("Custom In-Line Moderation Icons\n\nPlease enter a list of commands to be made available as mod icons within chat lines. 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}\"\n\nTo send multiple commands, separate them with \"\".\n\nNumeric values will become timeout buttons for that number of seconds. The text \"\" is a special value that will act like the normal Ban button in chat.\n\nTo assign a specific letter for use as the icon, specify it at the start of the command followed by an equals sign.\n\nExample: A=\"!reg add\"\n\nDefault: 600", old_val); - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Custom In-Line Moderation Icons", + "Please enter a list of commands to be made available as mod icons within chat lines. Commands are separated by spaces. " + + "To include spaces in a command, surround the command with double quotes (\"). Use {user} to insert the user's name " + + "into the command, otherwise it will be appended to the end.

Example: !permit \"!reg add {user}\"

To " + + "send multiple commands, separate them with <LINE>.

Numeric values will become timeout buttons for " + + "that number of seconds. The text <BAN> is a special value that will act like the normal Ban button in chat.

" + + "To assign a specific letter for use as the icon, specify it at the start of the command followed by an equals sign.

" + + "Example: A=\"!reg add\"

Default: <BAN> 600", + old_val.substr(1), + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - var vals = [], prefix = ''; - new_val = new_val.trim(); + var vals = [], prefix = ''; + new_val = new_val.trim(); - while(new_val) { - if ( new_val.charAt(1) === '=' ) { - prefix = new_val.charAt(0); - new_val = new_val.substr(2); - continue; - } + while(new_val) { + if ( new_val.charAt(1) === '=' ) { + prefix = new_val.charAt(0); + new_val = new_val.substr(2); + continue; + } - if ( new_val.charAt(0) === '"' ) { - var end = new_val.indexOf('"', 1); - if ( end === -1 ) - end = new_val.length; + 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([prefix, segment]); - prefix = ''; - } + var segment = new_val.substr(1, end - 1); + if ( segment ) { + vals.push([prefix, segment]); + prefix = ''; + } - new_val = new_val.substr(end + 1); + new_val = new_val.substr(end + 1); - } else { - var ind = new_val.indexOf(' '); - if ( ind === -1 ) { - if ( new_val ) { - vals.push([prefix, new_val]); - prefix = ''; - } + } else { + var ind = new_val.indexOf(' '); + if ( ind === -1 ) { + if ( new_val ) { + vals.push([prefix, new_val]); + prefix = ''; + } - new_val = ''; + new_val = ''; - } else { - var segment = new_val.substr(0, ind); - if ( segment ) { - vals.push([prefix, segment]); - prefix = ''; - } + } else { + var segment = new_val.substr(0, ind); + if ( segment ) { + vals.push([prefix, segment]); + prefix = ''; + } - new_val = new_val.substr(ind + 1); - } - } - } + new_val = new_val.substr(ind + 1); + } + } + } - var final = []; - for(var i=0; i < vals.length; i++) { - var had_prefix = false, prefix = vals[i][0], val = vals[i][1]; - if ( val === "" ) - val = false; + var final = []; + for(var i=0; i < vals.length; i++) { + var had_prefix = false, prefix = vals[i][0], val = vals[i][1]; + if ( val === "" ) + val = false; - var num = parseInt(val); - if ( num > 0 && num !== NaN ) - val = num; + var num = parseInt(val); + if ( num > 0 && ! Number.isNaN(num) ) + val = num; - if ( ! prefix ) { - var tmp; - if ( typeof val === "string" ) - tmp = /\w/.exec(val); - else - tmp = utils.duration_string(val); + if ( ! prefix ) { + var tmp; + if ( typeof val === "string" ) + tmp = /\w/.exec(val); + else + tmp = utils.duration_string(val); - prefix = tmp && tmp.length ? tmp[0].toUpperCase() : "C"; - } else - had_prefix = true; + prefix = tmp && tmp.length ? tmp[0].toUpperCase() : "C"; + } else + had_prefix = true; - if ( typeof val === "string" ) { - // Split it up for this step. - var lines = val.split(/ * */); - for(var x=0; x < lines.length; x++) { - if ( lines[x].indexOf('{user}') === -1 ) - lines[x] += ' {user}'; - } - val = lines.join(""); - } + if ( typeof val === "string" ) { + // Split it up for this step. + var lines = val.split(/ * */); + for(var x=0; x < lines.length; x++) { + if ( lines[x].indexOf('{user}') === -1 ) + lines[x] += ' {user}'; + } + val = lines.join(""); + } - final.push([prefix, val, had_prefix]); - } + final.push([prefix, val, had_prefix]); + } - this.settings.set('mod_buttons', final); + f.settings.set('mod_buttons', final); + }, 600); } }; @@ -282,7 +295,8 @@ FFZ.settings_info.mod_card_buttons = { help: "Add additional buttons to moderation cards for running chat commands on those users.", method: function() { - var old_val = ""; + var f = this, + 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 ) @@ -291,45 +305,51 @@ FFZ.settings_info.mod_card_buttons = { 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); + utils.prompt( + "Moderation Card Additional Buttons", + "Please 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 name into the command, otherwise it will be appended to the end.

Example: !permit \"!reg add {user}\"", + old_val.substr(1), + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - if ( new_val === null || new_val === undefined ) - return; + var vals = []; + new_val = new_val.trim(); - 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; - 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); - var segment = new_val.substr(1, end - 1); - if ( segment ) - vals.push(segment); + new_val = new_val.substr(end + 1); - new_val = new_val.substr(end + 1); + } else { + var ind = new_val.indexOf(' '); + if ( ind === -1 ) { + if ( new_val ) + vals.push(new_val); - } else { - var ind = new_val.indexOf(' '); - if ( ind === -1 ) { - if ( new_val ) - vals.push(new_val); + new_val = ''; - new_val = ''; + } else { + var segment = new_val.substr(0, ind); + if ( segment ) + vals.push(segment); - } else { - var segment = new_val.substr(0, ind); - if ( segment ) - vals.push(segment); + new_val = new_val.substr(ind + 1); + } + } + } - new_val = new_val.substr(ind + 1); - } - } - } - - this.settings.set("mod_card_buttons", vals); + f.settings.set("mod_card_buttons", vals); + }, 600); } }; @@ -345,29 +365,36 @@ FFZ.settings_info.mod_card_durations = { 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); + var f = this, + old_val = this.settings.mod_card_durations.join(", "); - if ( new_val === null || new_val === undefined ) - return; + utils.prompt( + "Moderation Card Timeout Buttons", + "Please enter a comma-separated list of durations that you would like to have timeout buttons for. " + + "Durations must be expressed in seconds.

Default: 300, 600, 3600, 43200, 86400, 604800", + old_val, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - if ( new_val === "reset" ) - new_val = FFZ.settings_info.mod_card_durations.value.join(", "); + 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 = []; + // 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; + 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); - } + if ( ! Number.isNaN(val) && val > 0 ) + vals.push(val); + } - this.settings.set("mod_card_durations", vals); + f.settings.set("mod_card_durations", vals); + }, 600); } }; @@ -420,7 +447,7 @@ FFZ.prototype.setup_mod_card = function() { } 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) { + utils.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) { @@ -682,7 +709,7 @@ FFZ.prototype.setup_mod_card = function() { real_msg.title = "Message User"; real_msg.addEventListener('click', function() { - window.open('http://www.twitch.tv/message/compose?to=' + controller.get('cardInfo.user.id')); + window.open('//www.twitch.tv/message/compose?to=' + controller.get('cardInfo.user.id')); }) msg_btn.parentElement.insertBefore(real_msg, msg_btn.nextSibling); @@ -699,28 +726,29 @@ FFZ.prototype.setup_mod_card = 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; + utils.prompt( + "Alias for " + utils.sanitize(controller.get('cardInfo.user.display_name') || user) + "", + "Please enter an alias for the user. Leave it blank to remove the alias.", + alias, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - new_val = new_val.trim(); - if ( ! new_val ) - new_val = undefined; + new_val = new_val.trim(); + if ( ! new_val ) + new_val = undefined; - f.aliases[user] = new_val; - f.save_aliases(); + f.aliases[user] = new_val; + f.save_aliases(); - // Update UI - f._update_alias(user); + // 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); + Ember.propertyDidChange(controller, 'cardInfo.user.display_name'); + var name = el.querySelector('h3.name'); + if ( name ) + name.classList.toggle('ffz-alias', new_val); + }); }); if ( msg_btn ) @@ -1040,9 +1068,11 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { // Interactivity jQuery('a.undelete', l_el).click(function(e) { this.parentElement.outerHTML = this.getAttribute('data-message'); }); + jQuery('.deleted-word', l_el).click(function(e) { jQuery(this).trigger('mouseout'); this.outerHTML = this.getAttribute('data-text'); }); jQuery('a.deleted-link', l_el).click(f._deleted_link_click); jQuery('img.emoticon', l_el).click(function(e) { f._click_emote(this, e) }); jQuery('.html-tooltip', l_el).tipsy({html:true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 's')}); + jQuery('.ffz-tooltip', l_el).tipsy({live: true, html: true, title: f.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 's')}); if ( modcard ) { modcard.get('cardInfo.user.id') !== msg.from && jQuery('span.from', l_el).click(function(e) { @@ -1082,6 +1112,9 @@ FFZ.prototype._update_alias = function(user) { var line = lines[i], el_from = line.querySelector('.from'); + if ( ! el_from ) + continue; + el_from.classList.toggle('ffz-alias', alias); el_from.textContent = display_name; el_from.title = alias ? cap_name : ''; @@ -1110,7 +1143,7 @@ FFZ.chat_commands.purge = function(room, args) { } FFZ.chat_commands.p = function(room, args) { - return FFZ.chat_commands.purge.bind(this)(room, args); + return FFZ.chat_commands.purge.call(this, room, args); } FFZ.chat_commands.p.enabled = function() { return this.settings.short_commands; } diff --git a/src/ember/player.js b/src/ember/player.js index cd3b5823..198d4c26 100644 --- a/src/ember/player.js +++ b/src/ember/player.js @@ -107,7 +107,6 @@ FFZ.prototype._modify_player = function(player) { f._cindex && f._cindex.ffzUpdatePlayerStats(); }; - player.reopen({ didInsertElement: function() { this._super(); @@ -139,6 +138,10 @@ FFZ.prototype._modify_player = function(player) { ffzInit: function() { var id = this.get('channel.id'); f.players[id] = this; + + var player = this.get('player'); + if ( player ) + this.ffzPostPlayer(); }, ffzTeardown: function() { @@ -153,22 +156,12 @@ FFZ.prototype._modify_player = function(player) { }, ffzPostPlayer: function() { - var player = this.get('ffz_player') || this.get('player'); - if ( ! player ) { - var tp2 = window.require("web-client/components/twitch-player2"); - if ( ! tp2 || ! tp2.getPlayer ) - return; - - player = tp2.getPlayer(); - if ( ! player || ! player.getVideo ) - // We can't get a valid player. :-( - return; - } - - this.set('ffz_player', player); + var player = this.get('player'); + if ( ! player ) + return; // Only set up the stats hooks if we need stats. - var has_video; + var has_video = false; try { has_video = player.getVideo(); @@ -184,72 +177,52 @@ FFZ.prototype._modify_player = function(player) { if ( this.get('ffzStatsInitialized') ) return; - var player = this.get('ffz_player'); + var t = this, + player = this.get('player'); + if ( ! player ) return; this.set('ffzStatsInitialized', true); // Make it so stats can no longer be disabled if we want them. - player.ffzSetStatsEnabled = player.setStatsEnabled; - try { - player.ffz_stats = player.getStatsEnabled(); - } catch(err) { - // Assume stats are off. - f.error("Player2 ffzInitStats: getStatsEnabled still doesn't work: " + err); - player.ffz_stats = false; - } + if ( player.setStatsEnabled ) { + player.ffzSetStatsEnabled = player.setStatsEnabled; + try { + player.ffz_stats = player.getStatsEnabled(); + } catch(err) { + // Assume stats are off. + f.log("Player2 ffzInitStats: getStatsEnabled still doesn't work."); + player.ffz_stats = false; + } - var t = this; + player.setStatsEnabled = function(e, s) { + if ( s !== false ) + player.ffz_stats = e; - player.setStatsEnabled = function(e, s) { - if ( s !== false ) - player.ffz_stats = e; + var out = player.ffzSetStatsEnabled(e || f.settings.player_stats); - var out = player.ffzSetStatsEnabled(e || f.settings.player_stats); + if ( ! t._ffz_player_stats_initialized ) { + t._ffz_player_stats_initialized = true; + player.addEventListener('statschange', update_stats); + } - if ( ! t._ffz_player_stats_initialized ) { - t._ffz_player_stats_initialized = true; - player.addEventListener('statschange', update_stats); - } + return out; + } - return out; - } + this._ffz_stat_interval = setInterval(function() { + if ( f.settings.player_stats || player.ffz_stats ) { + player.ffzSetStatsEnabled(false); + player.ffzSetStatsEnabled(true); + } + }, 5000); + } - this._ffz_stat_interval = setInterval(function() { - if ( f.settings.player_stats || player.ffz_stats ) { - player.ffzSetStatsEnabled(false); - player.ffzSetStatsEnabled(true); - } - }, 5000); - - if ( f.settings.player_stats && ! player.ffz_stats ) { + if ( f.settings.player_stats && ( ! player.setStatsEnabled || ! player.ffz_stats ) ) { this._ffz_player_stats_initialized = true; player.addEventListener('statschange', update_stats); player.ffzSetStatsEnabled(true); } - }, - - ffzSetQuality: function(q) { - var player = this.get('ffz_player'); - if ( ! player ) - return; - - this.$(".js-quality-display-contain").attr("data-q", "loading"); - - player.setQuality(q); - - var t = this.$(".js-player-alert"); - t.find(".js-player-alert__message").text(); - t.attr("data-active", !0); - }, - - ffzGetQualities: function() { - var player = this.get('ffz_player'); - if ( ! player ) - return []; - return player.getQualities(); - }, - + } }); } \ No newline at end of file diff --git a/src/ember/room.js b/src/ember/room.js index 0aa6ba41..a0926177 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -11,7 +11,7 @@ var FFZ = window.FrankerFaceZ, 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; }'; + return '.chat-line[data-room="' + room.id + '"] .badges .moderator:not(.ffz-badge-replacement) { background-repeat: no-repeat; background-size: initial; background-position: center; background-image:url("' + room.moderator_badge + '") !important; }'; }; @@ -37,22 +37,18 @@ FFZ.prototype.setup_room = function() { // 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); + orig_ban.call(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"); + orig_to.call(this, e); this.get("model").clearMessages(e.user); } @@ -79,7 +75,7 @@ FFZ.prototype.setup_room = function() { renderBottom: e.bottom, renderRight: e.right, isIgnored: this.get("tmiSession").isIgnored(e.sender), - isChannelOwner: this.get("controllers.login.userData.login") === e.sender, + isChannelOwner: this.get("login.userData.login") === e.sender, profileHref: Twitch.uri.profile(e.sender), isModeratorOrHigher: this.get("model.isModeratorOrHigher") }); @@ -161,15 +157,6 @@ FFZ.prototype._modify_rview = function(view) { this._super(); }, - ffzAlternate: function() { - /*if ( ! this._ffz_chat_display ) { - var el = this.get('element'); - this._ffz_chat_display = el && el.querySelector('ul.chat-lines'); - } - - this._ffz_chat_display && this._ffz_chat_display.classList.toggle('ffz-should-alternate');*/ - }, - ffzInit: function() { f._roomv = this; @@ -232,6 +219,7 @@ FFZ.prototype._modify_rview = function(view) { var r9k_badge = cont.querySelector('#ffz-stat-r9k'), sub_badge = cont.querySelector('#ffz-stat-sub'), + emote_badge = cont.querySelector('#ffz-stat-emote'), slow_badge = cont.querySelector('#ffz-stat-slow'), banned_badge = cont.querySelector('#ffz-stat-banned'), delay_badge = cont.querySelector('#ffz-stat-delay'), @@ -243,6 +231,8 @@ FFZ.prototype._modify_rview = function(view) { r9k_badge.parentElement.removeChild(r9k_badge); if ( sub_badge ) sub_badge.parentElement.removeChild(sub_badge); + if ( emote_badge ) + emote_badge.parentElement.removeChild(emote_badge); if ( slow_badge ) slow_badge.parentElement.removeChild(slow_badge); if ( delay_badge ) @@ -275,6 +265,16 @@ FFZ.prototype._modify_rview = function(view) { jQuery(sub_badge).tipsy({gravity:"s", offset:15}); } + if ( ! emote_badge ) { + emote_badge = document.createElement('span'); + emote_badge.className = 'ffz room-state stat float-right'; + emote_badge.id = 'ffz-stat-emote'; + emote_badge.innerHTML = 'EMOTE'; + emote_badge.title = "This room is in Twitch emote-only mode. Emotes added by extensions are not permitted in this mode."; + cont.appendChild(emote_badge); + jQuery(emote_badge).tipsy({gravity: "s", offset: 15}); + } + if ( ! slow_badge ) { slow_badge = document.createElement('span'); slow_badge.className = 'ffz room-state stat float-right'; @@ -315,6 +315,7 @@ FFZ.prototype._modify_rview = function(view) { var vis_count = 0, r9k_vis = room && room.get('r9k'), sub_vis = room && room.get('subsOnly'), + emote_vis = room && room.get('emoteOnly') && room.get('emoteOnly') !== '0', ban_vis = room && room.get('ffz_banned'), slow_vis = room && room.get('slowMode'), delay_vis = f.settings.chat_delay !== 0, @@ -322,13 +323,15 @@ FFZ.prototype._modify_rview = function(view) { if ( r9k_vis ) vis_count += 1; if ( sub_vis ) vis_count += 1; - if ( ban_vis ) vis_count += 1; + if ( emote_vis ) vis_count += 1; + if ( ban_vis ) vis_count += 1; if ( slow_vis ) vis_count += 1; if ( delay_vis ) vis_count += 1; if ( batch_vis ) vis_count += 1; r9k_badge.classList.toggle('truncated', vis_count > 3); sub_badge.classList.toggle('truncated', vis_count > 3); + emote_badge.classList.toggle('truncated', vis_count > 3); banned_badge.classList.toggle('truncated', vis_count > 3); slow_badge.classList.toggle('truncated', vis_count > 3); delay_badge.classList.toggle('truncated', vis_count > 3); @@ -336,6 +339,7 @@ FFZ.prototype._modify_rview = function(view) { r9k_badge.classList.toggle('hidden', ! r9k_vis); sub_badge.classList.toggle('hidden', ! sub_vis); + emote_badge.classList.toggle('hidden', ! emote_vis); banned_badge.classList.toggle('hidden', ! ban_vis); slow_badge.classList.toggle('hidden', ! slow_vis); @@ -370,7 +374,6 @@ FFZ.prototype._modify_rview = function(view) { 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() { @@ -389,6 +392,7 @@ FFZ.prototype._modify_rview = function(view) { if ( this._ffz_mouse_move ) { messages.removeEventListener('mousemove', this._ffz_mouse_move); + messages.removeEventListener('touchmove', this._ffz_mouse_move); this._ffz_mouse_move = undefined; } @@ -553,7 +557,7 @@ FFZ.prototype.run_command = function(text, room_id) { var val = command.enabled; if ( typeof val == "function" ) { try { - val = command.enabled.bind(this)(room, args); + val = command.enabled.call(this, room, args); } catch(err) { this.error('command "' + cmd + '" enabled: ' + err); val = false; @@ -567,7 +571,7 @@ FFZ.prototype.run_command = function(text, room_id) { this.log("Received Command: " + cmd, args, true); try { - output = command.bind(this)(room, args); + output = command.call(this, room, args); } catch(err) { this.error('command "' + cmd + '" runner: ' + err); output = "There was an error running the command."; @@ -602,7 +606,7 @@ FFZ.prototype.run_ffz_command = function(text, room_id) { var command = FFZ.ffz_commands[cmd], output; if ( command ) { try { - output = command.bind(this)(room, args); + output = command.call(this, room, args); } catch(err) { this.log("Error Running Command - " + cmd + ": " + err, room); output = "There was an error running the command."; @@ -849,9 +853,6 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) { } } } - - if ( (removed % 2) && this._roomv && this._roomv.get('context.model.id') === room_id ) - this._roomv.ffzAlternate(); } @@ -987,7 +988,7 @@ FFZ.prototype._modify_room = function(room) { ffzUpdateStatus: function() { if ( f._roomv ) f._roomv.ffzUpdateStatus(); - }.observes('r9k', 'subsOnly', 'slow', 'ffz_banned'), + }.observes('r9k', 'subsOnly', 'emoteOnly', 'slow', 'ffz_banned'), // User Level ffzUserLevel: function() { @@ -1057,9 +1058,6 @@ FFZ.prototype._modify_room = function(room) { } } - if ( (removed % 2) && f._roomv && f._roomv.get('context.model.id') === this.get('id') ) - f._roomv.ffzAlternate(); - // Delete pending messages if (t.ffzPending) { msgs = t.ffzPending; @@ -1111,11 +1109,8 @@ FFZ.prototype._modify_room = function(room) { len = messages.get("length"), limit = this.get("messageBufferSize"); - if ( len > limit ) { + if ( len > limit ) messages.removeAt(0, len - limit); - if ( ((len - limit) % 2) && f._roomv && f._roomv.get('context.model.id') === this.get('id') ) - f._roomv.ffzAlternate(); - } }, // Artificial chat delay @@ -1232,6 +1227,15 @@ FFZ.prototype._modify_room = function(room) { // Tokenization f.tokenize_chat_line(msg, false, this.get('roomProperties.hide_chat_links')); + // If it's from Twitch notify, and it's directly related to + if ( msg.from === 'twitchnotify' && msg.message.indexOf('subscribed to') === -1 && msg.message.indexOf('subscribed') !== -1 ) { + if ( ! msg.tags ) + msg.tags = {}; + msg.tags.subscriber = true; + if ( msg.labels && msg.labels.indexOf("subscriber") === -1 ) + msg.labels.push("subscriber"); + } + // 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]; @@ -1293,6 +1297,10 @@ FFZ.prototype._modify_room = function(room) { if ( msg.color ) f._handle_color(msg.color); + // Report this message to the dashboard. + if ( window !== window.parent && parent.postMessage && msg.from && msg.from !== "jtv" && msg.from !== "twitchnotify" ) + parent.postMessage({from_ffz: true, command: 'chat_message', data: {from: msg.from, room: msg.room}}, location.protocol + "//www.twitch.tv/"); + // Add the message. return this._super(msg); }, @@ -1403,10 +1411,8 @@ FFZ.prototype._modify_room = function(room) { 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 */ } + if ( window !== window.parent && parent.postMessage ) + parent.postMessage({from_ffz: true, command: 'chatter_count', data: Object.keys(this.get('ffz_chatters') || {}).length}, location.protocol + "//www.twitch.tv/"); }, diff --git a/src/ember/vod-chat.js b/src/ember/vod-chat.js new file mode 100644 index 00000000..a5ea61a7 --- /dev/null +++ b/src/ember/vod-chat.js @@ -0,0 +1,326 @@ +var FFZ = window.FrankerFaceZ, + utils = require("../utils"), + constants = require("../constants"); + +// --------------------- +// Settings +// --------------------- + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.setup_vod_chat = function() { + var f = this, + VRC = App.__container__.resolve('component:vod-right-column'); + + if ( VRC ) + this._modify_vod_right_column(VRC); + else + f.error("Unable to locate VOD Right Column component."); + + // Get the VOD Chat Service + var VODService = App.__container__.lookup('service:vod-chat-service'); + if ( VODService ) + VODService.reopen({ + messageBufferSize: f.settings.scrollback_length, + + pushMessage: function(msg) { + if ( msg.get("color") === null ) { + var colors = this.get("colorSettings"), + from = msg.get("from"); + + if ( ! colors.get(from) ) + colors.set(from, constants.CHAT_COLORS[Math.floor(Math.random() * constants.CHAT_COLORS.length)]); + + msg.set("color", colors.get(from)); + } + + this.get("messages").pushObject(msg); + + var messages = this.get("messages"), + len = this.get("messages.length"), + limit = this.get("messageBufferSize"); + + if ( len > limit ) + messages.removeAt(0, len - limit); + } + }); + else + f.error("Unable to locate VOD Chat Service."); + + // Get the VOD Chat Display + var VODChat = App.__container__.resolve('component:vod-chat-display'); + + if ( VODChat ) + this._modify_vod_chat_display(VODChat); + else + f.error("Unable to locate VOD Chat Display component."); + + // Modify all existing VOD Chat views. + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + var view = views[key]; + + if ( VRC && view instanceof VRC ) { + this.log("Manually updating existing VOD Right Column."); + try { + this._modify_vod_right_column(view); + view.ffzInit(); + //Ember.propertyDidChange(view, 'canSeeDarkLaunch'); + } catch(err) { + this.error("setup: setup_vod_chat: " + err); + } + + } else if ( VODChat && view instanceof VODChat ) { + this.log("Manually updating existing VOD Chat view.", view); + try { + this._modify_vod_chat_display(view); + view.ffzInit(); + } catch(err) { + this.error("setup: setup_vod_chat: " + err); + } + } + } +} + + +FFZ.prototype._modify_vod_right_column = function(component) { + var f = this; + + component.reopen({ + didInsertElement: function() { + this._super(); + try { + this.ffzInit(); + } catch(err) { + f.error("VODRightColumn didInsertElement: " + err); + } + }, + + ffzInit: function() { + if ( f.settings.dark_twitch ) { + var el = this.get('element'), + cont = el && el.querySelector('.chat-container'); + + if ( cont ) + cont.classList.add('dark'); + } + } + }); +} + + +FFZ.prototype._modify_vod_chat_display = function(component) { + var f = this, + VODService = App.__container__.lookup('service:vod-chat-service'); + + component.reopen({ + didInsertElement: function() { + this._super(); + try { + this.ffzInit(); + } catch(err) { + f.error("VODChat didInsertElement: " + err); + } + }, + + willClearRender: function() { + try { + this.ffzTeardown(); + } catch(err) { + f.error("VODChat willClearRender: " + err); + } + this._super(); + }, + + _prepareToolTips: function() { + this.$(".tooltip").tipsy({ + live: true, + gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 's') + }) + }, + + ffzInit: function() { + f._vodc = this; + + // Load the room, if necessary + var room_id = this.get('video.channel.name'); + if ( room_id && ! f.rooms[room_id] ) + f.load_room(room_id); // TODO: Function to reprocess existing messages. + + this.ffz_frozen = false; + if ( f.settings.chat_hover_pause ) + this.ffzEnableFreeze(); + + this.$('.chat-messages').find('.html-tooltip').tipsy({ + live: true, html: true, + gravity: utils.tooltip_placement(2 * constants.TOOLTIP_DISTANCE, function() { + return this.classList.contains('right') ? 'e' : 'n' + })}); + + this.$('.chat-messages').find('.ffz-tooltip').tipsy({ + live: true, html: true, + title: f.render_tooltip(), + gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, function() { + return this.classList.contains('right') ? 'e' : 'n' + })}); + }, + + ffzTeardown: function() { + if ( f._vodc === this ) + f._vodc = undefined; + + // TODO: Function to unload the old room? + + this.ffzDisableFreeze(); + }, + + ffzEnableFreeze: function() { + var scroller = this.get('chatMessagesScroller'); + if ( ! scroller ) + return; + + this._ffz_interval = setInterval(this.ffzPulse.bind(this), 200); + + this._ffz_mouse_move = this.ffzMouseMove.bind(this); + this._ffz_mouse_out = this.ffzMouseOut.bind(this); + + scroller.on('mousemove', this._ffz_mouse_move); + scroller.on('touchmove', this._ffz_mouse_move); + scroller.on('mouseout', this._ffz_mouse_out); + }, + + ffzDisableFreeze: function() { + if ( this._ffz_interval ) { + clearInterval(this._ffz_interval); + this._ffz_interval = undefined; + } + + this.ffzUnfreeze(); + var scroller = this.get('chatMessagesScroller'); + if ( ! scroller ) + return; + + if ( this._ffz_mouse_move ) { + scroller.off('mousemove', this._ffz_mouse_move); + scroller.off('touchmove', this._ffz_mouse_move); + this._ffz_mouse_move = undefined; + } + + if ( this._ffz_mouse_out ) { + scroller.off('mouseout', this._ffz_mouse_out); + this._ffz_mouse_out = undefined; + } + }, + + ffzUnfreeze: function(from_stuck) { + this.ffz_frozen = false; + this._ffz_last_move = 0; + this.ffzUnwarnPaused(); + + if ( ! from_stuck && this.get('stuckToBottom') ) + this._scrollToBottom(); + }, + + ffzPulse: function() { + if ( this.ffz_frozen ) { + var elapsed = Date.now() - this._ffz_last_move; + if ( elapsed > 750 ) + this.ffzUnfreeze(); + } + }, + + /*ffzMouseDown: function(event) { + var scroller = this.get('chatMessagesScroller'); + if ( scroller && scroller[0] && ((!this.ffz_frozen && "mousedown" === event.type) || "mousewheel" === event.type || (is_android && "scroll" === event.type) ) ) { + var r = scroller[0].scrollHeight - scroller[0].scrollTop - scroller[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') ) { + VODService && VODService.set("messageBufferSize", f.settings.scrollback_length + 150); + this.ffzWarnPaused(); + } + }, + + _scrollToBottom: _.throttle(function() { + var e = this, + scroller = e.get('chatMessagesScroller'); + + if ( ! scroller || ! scroller.length ) + return; + + Ember.run.next(function() { + setTimeout(function() { + if ( e.ffz_frozen ) + return; + + scroller[0].scrollTop = scroller[0].scrollHeight; + e._setStuckToBottom(true); + }) + }) + }, 300), + + _setStuckToBottom: function(val) { + this.set("stuckToBottom", val); + VODService && VODService.set("messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150)); + if ( ! val ) + this.ffUnfreeze(true); + }, + + 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'); + } + }); +} \ No newline at end of file diff --git a/src/emoticons.js b/src/emoticons.js index ea19e83e..842d4aa2 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -9,19 +9,6 @@ var FFZ = window.FrankerFaceZ, return ""; return 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n"; - }, - - - 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) - ); }; @@ -218,97 +205,6 @@ FFZ.ws_commands.load_set = function(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", - source = set && set.source || "FFZ", - - preview_url = this.settings.emote_image_hover ? (emote.urls[4] || emote.urls[2]) : null, - image = preview_url ? '' : ''; - - emote._tooltip = image + "Emoticon: " + (emote.hidden ? "???" : emote.name) + "
" + source + " " + title + (owner ? "
By: " + owner.display_name : ""); - return emote._tooltip; -} - -FFZ.prototype._reset_tooltips = function(twitch_only) { - for(var emote_id in this._twitch_emotes) { - var data = this._twitch_emotes[emote_id]; - if ( data && data.tooltip ) - data.tooltip = null; - } - - if ( ! twitch_only ) { - for(var set_id in this.emote_sets) { - var emote_set = this.emote_sets[set_id]; - for(var emote_id in emote_set.emoticons) { - var emote = emote_set.emoticons[emote_id]; - if ( emote._tooltip ) - emote._tooltip = null; - } - } - } - - var emotes = document.querySelectorAll('img.emoticon'); - for(var i=0; i < emotes.length; i++) { - var emote = emotes[i]; - if ( emote.classList.contains('ffz-image-hover') ) - continue; - - var set_id, - emote_id = emote.getAttribute('data-emote'); - - if ( emote_id ) { - // Twitch Emotes - if ( this.has_bttv ) - continue; - - emote.setAttribute('original-title', utils.build_tooltip.bind(this)(emote_id, false, emote.alt)); - continue; - } - - if ( twitch_only ) - continue; - - // FFZ Emoji - emote_id = emote.getAttribute('data-ffz-emoji'); - if ( emote_id ) { - var emoji = this.emoji_data && this.emoji_data[emote_id], - setting = this.settings.parse_emoji, - - src = emoji ? (setting === 2 ? emoji.noto_src : emoji.tw_src) : null, - image = ''; - - if ( src && this.settings.emote_image_hover ) - image = ''; - - emote.setAttribute('original-title', emoji ? (image + 'Emoji: ' + emote.alt + '
Name: ' + emoji.name + (emoji.short_name ? "
Short Name: :" + emoji.short_name + ":" : "")) : emote.alt); - continue; - } - - // FFZ Emotes - emote_id = emote.getAttribute('data-ffz-emote'); - set_id = emote.getAttribute('data-ffz-set'); - - var emote_set = this.emote_sets[set_id]; - if ( ! emote_set || ! emote_set.emoticons || ! emote_set.emoticons[emote_id] ) - continue; - - emote.setAttribute('original-title', this._emote_tooltip(emote_set.emoticons[emote_id])); - } -} - - // --------------------- // Emoji Loading // --------------------- @@ -325,21 +221,29 @@ FFZ.prototype.load_emoji_data = function(callback, tries) { emoji.code = eid; new_data[eid] = emoji; - by_name[emoji.short_name] = eid; + if ( emoji.short_name ) + by_name[emoji.short_name] = eid; + if ( emoji.names && emoji.names.length ) + for(var x=0,y=emoji.names.length; x < y; x++) + by_name[emoji.names[x]] = eid; - emoji.raw = _.map(emoji.code.split("-"), from_code_point).join(""); + emoji.raw = _.map(emoji.code.split("-"), utils.codepoint_to_emoji).join(""); - emoji.tw_src = constants.SERVER + 'emoji/tw-' + eid + '.svg'; + emoji.tw_src = constants.SERVER + 'emoji/tw/' + eid + '.svg'; emoji.noto_src = constants.SERVER + 'emoji/noto-' + eid + '.svg'; + emoji.one_src = constants.SERVER + 'emoji/one/' + eid + '.svg'; emoji.token = { - emoticonSrc: true, + type: "emoticon", + imgSrc: true, tw_src: emoji.tw_src, noto_src: emoji.noto_src, + one_src: emoji.one_src, tw: emoji.tw, noto: emoji.noto, + one: emoji.one, ffzEmoji: eid, altText: emoji.raw @@ -481,6 +385,15 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { else emote.regex = new RegExp("(^|\\W|\\b)(" + utils.escape_regex(emote.name) + ")\\b", "g"); + emote.token = { + type: "emoticon", + srcSet: emote.srcSet, + imgSrc: emote.urls[1], + ffzEmote: emote.id, + ffzEmoteSet: set_id, + altText: emote.hidden ? '???' : emote.name + }; + output_css += build_css(emote); data.count++; data.emoticons[emote.id] = emote; diff --git a/src/ext/api.js b/src/ext/api.js index 28280973..e5454075 100644 --- a/src/ext/api.js +++ b/src/ext/api.js @@ -14,7 +14,7 @@ var FFZ = window.FrankerFaceZ, // API Constructor // --------------------- -var API = FFZ.API = function(instance, name, icon) { +var API = FFZ.API = function(instance, name, icon, version) { this.ffz = instance || FFZ.get(); // Check for a known API! @@ -54,12 +54,13 @@ var API = FFZ.API = function(instance, name, icon) { this.name = name || ("Extension#" + this.id); this.icon = icon || null; + this.version = version || null; this.ffz.log('Registered New Extension #' + this.id + ': ' + this.name); }; -FFZ.prototype.api = function(name, icon) { +FFZ.prototype.api = function(name, icon, version) { // Load the known APIs list. if ( ! this._known_apis ) { this._known_apis = {}; @@ -71,7 +72,7 @@ FFZ.prototype.api = function(name, icon) { } } - return new API(this, name, icon); + return new API(this, name, icon, version); } @@ -163,6 +164,15 @@ API.prototype._load_set = function(real_id, set_id, data) { else new_emote.regex = new RegExp("(^|\\W|\\b)(" + utils.escape_regex(emote.name) + ")(?=\\W|$)", "g"); + new_emote.token = { + type: "emoticon", + srcSet: new_emote.srcSet, + imgSrc: new_emote.urls[1], + ffzEmote: id, + ffzEmoteSet: real_id, + altText: new_emote.hidden ? '???' : new_emote.name + }; + output_css += build_css(new_emote); emote_set.count++; emoticons[id] = new_emote; diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index 1effcf1c..1051464a 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -42,6 +42,14 @@ FFZ.prototype.setup_bttv = function(delay) { utils.update_css(this._chat_style, 'chat_ts_font_size', ''); } + // Remove Sub Count and the Chart + if ( this.is_dashboard ) { + this._update_subscribers(); + this._remove_dash_chart(); + } + + document.body.classList.add('ffz-bttv'); + // Disable Chat Tabs if ( this._chatv ) { if ( this.settings.group_tabs ) @@ -79,6 +87,8 @@ FFZ.prototype.setup_bttv = function(delay) { this.toggle_style('chat-colors-gray'); this.toggle_style('badges-transparent'); + this.toggle_style('badges-sub-notice'); + this.toggle_style('badges-sub-notice-on'); // Disable other features too. document.body.classList.remove('ffz-transparent-badges'); @@ -93,12 +103,6 @@ FFZ.prototype.setup_bttv = function(delay) { 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) { @@ -197,6 +201,41 @@ FFZ.prototype.setup_bttv = function(delay) { } }; + // Emoji! + var parse_emoji = function(token) { + var setting = f.settings.parse_emoji, + output = [], + segments = token.split(constants.EMOJI_REGEX), + text = null; + + if ( setting === 0 ) + return [token]; + + while(segments.length) { + text = (text || '') + segments.shift(); + if ( segments.length ) { + var match = segments.shift(), + eid = utils.emoji_to_codepoint(match), + data = f.emoji_data[eid], + src = data && (setting === 3 ? data.one_src : (setting === 2 ? data.noto_src : data.tw_src)); + + if ( src ) { + if ( text && text.length ) + output.push(text); + var code = utils.quote_attr(data.raw); + output.push(['' + code + '']); + text = null; + } else + text = (text || '') + match; + } + } + + if ( text && text.length ) + output.push(text); + + return output; + } + // Emoticonize var original_emoticonize = BetterTTV.chat.templates.emoticonize; BetterTTV.chat.templates.emoticonize = function(message, emotes) { @@ -206,107 +245,65 @@ FFZ.prototype.setup_bttv = function(delay) { l_room = room && room.toLowerCase(), l_sender = received_sender && received_sender.toLowerCase(), sets = f.getEmotes(l_sender, l_room), - emotes = [], + emotes = {}, emote, user = f.get_user(), + new_tokens = [], 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; + // Build an object with all of our emotes. + for(var i=0; i < sets.length; i++) { + var emote_set = f.emote_sets[sets[i]]; + if ( emote_set && emote_set.emoticons ) + for(var emote_id in emote_set.emoticons) { + emote = emote_set.emoticons[emote_id]; + if ( ! emotes[emote.name] ) + emotes[emote.name] = emote; + } + } - _.each(set.emoticons, function(emote) { - _.any(tokens, function(token) { - return _.isString(token) && token.match(emote.regex); - }) && emotes.push(emote); - }); - }); + for(var i=0, l=tokens.length; i < l; i++) { + var token = tokens[i]; + if ( typeof token !== "string" ) { + new_tokens.push(token); + continue; + } - // 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; + // Split the token! + var segments = token.split(' '), + text = [], segment; - tokens = []; + for(var x=0,y=segments.length; x < y; x++) { + segment = segments[x]; + emote = emotes[segment]; - for(var i=0; i < old_tokens.length; i++) { - var token = old_tokens[i]; - if ( typeof token !== "string" ) { - tokens.push(token); - continue; - } + if ( emote ) { + if ( text.length ) { + var toks = parse_emoji(text.join(' ') + ' '); + for(var q=0; q < toks.length; q++) + new_tokens.push(toks[q]); - var tbits = token.split(emote.regex); - while(tbits.length) { - var bit = tbits.shift(); - if ( tbits.length ) { - bit += tbits.shift(); - if ( bit ) - tokens.push(bit); + text = []; + } - tbits.shift(); - tokens.push(eo); + new_tokens.push(['']); - if ( mine && l_room ) - f.add_usage(l_room, emote); + if ( mine && l_room ) + f.add_usage(l_room, emote); - } else - tokens.push(bit); - } - } - }); - } + text.push(''); + } else + text.push(segment); + } - // Sneak in Emojicon Processing - if ( f.settings.parse_emoji && f.emoji_data ) { - var old_tokens = tokens, - setting = f.settings.parse_emoji; + if ( text.length > 1 || (text.length === 1 && text[0] !== '') ) { + var toks = parse_emoji(text.join(' ') + ' '); + for(var q=0; q < toks.length; q++) + new_tokens.push(toks[q]); + } + } - 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' ) - tokens.push(match); - else { - var eid = utils.emoji_to_codepoint(match, variant), - data = f.emoji_data[eid], - src = data && (setting === 2 ? data.noto_src : data.tw_src); - - if ( data && src ) { - var image = src && f.settings.emote_image_hover ? '' : '', - tooltip = image + "Emoji: " + data.raw + "
Name: " + data.name + (data.short_name ? "
Short Name: :" + data.short_name + ":" : ""), - code = utils.quote_attr(data.raw); - - tokens.push(['' + code + '']); - } else - tokens.push(match + (variant || "")); - } - } - } - } - } - - return tokens; - } + return new_tokens; + } this.update_ui_link(); } \ No newline at end of file diff --git a/src/ext/rechat.js b/src/ext/rechat.js index 2bc50944..08b91c8f 100644 --- a/src/ext/rechat.js +++ b/src/ext/rechat.js @@ -99,6 +99,7 @@ FFZ.prototype.find_rechat = function() { // Tooltips jQuery(container).find('.tooltip').tipsy({live: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); jQuery(container).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); + jQuery(container).find('.ffz-tooltip').tipsy({live: true, html: true, title: this.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); // Load the room data. var room_id = el.getAttribute('data-room'); @@ -234,20 +235,26 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { else if ( node.nodeType === node.ELEMENT_NODE ) { if ( node.tagName === 'IMG' ) tokens.push({ + type: "emoticon", altText: node.alt, - emoticonSrc: node.src + imgSrc: node.src }); else if ( node.tagName === 'A' ) tokens.push({ - isLink: true, - href: node.textContent + type: "link", + isDeleted: false, + isLong: false, + length: node.textContent.length, + link: node.href, + text: node.textContent }); else if ( node.tagName === 'SPAN' ) tokens.push({ - mentionedUser: node.textContent, - own: node.classList.contains('mentioning') + type: "mention", + user: node.textContent, + isOwnMessage: node.classList.contains('mentioning') }); else { diff --git a/src/featurefriday.js b/src/featurefriday.js index 959a48e0..e8c751ec 100644 --- a/src/featurefriday.js +++ b/src/featurefriday.js @@ -69,7 +69,7 @@ FFZ.prototype._feature_friday_ui = function(room_id, parent, view) { 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.href = "//www.twitch.tv/" + ff.channel; btn.title = message; btn.target = "_new"; btn.innerHTML = "" + message + ""; @@ -122,7 +122,7 @@ FFZ.prototype._update_ff_live = function() { return; var f = this; - Twitch.api.get("streams/" + this.feature_friday.channel) + utils.api.get("streams/" + this.feature_friday.channel) .done(function(data) { f.feature_friday.live = data.stream != null; f.update_ui_link(); diff --git a/src/main.js b/src/main.js index 60999bed..b14c3b26 100644 --- a/src/main.js +++ b/src/main.js @@ -12,6 +12,16 @@ var FFZ = window.FrankerFaceZ = function() { this._log_data = []; this._apis = {}; + // Error Logging + var t = this; + window.addEventListener('error', function(event) { + if ( ! event.error ) + return; + + var has_stack = event.error && event.error.stack; + t.log("JavaScript Error: " + event.message + " [" + event.filename + ":" + event.lineno + ":" + event.colno + "]", has_stack ? event.error.stack : undefined, false, has_stack); + }); + // Get things started. this.initialize(); } @@ -19,10 +29,13 @@ var FFZ = window.FrankerFaceZ = function() { FFZ.get = function() { return FFZ.instance; } +// TODO: This should be in a module. +FFZ.msg_commands = {}; + // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 100, + major: 3, minor: 5, revision: 133, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -32,36 +45,38 @@ var VER = FFZ.version_info = { // Logging FFZ.prototype.log = function(msg, data, to_json, log_json) { - msg = "FFZ: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); + if ( to_json ) + msg = msg + ' -- ' + 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); + console.groupCollapsed("FFZ: " + msg); if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) console.log(data); else console.dir(data); - console.groupEnd(msg); + console.groupEnd("FFZ: " + msg); } else - console.log(msg); + console.log("FFZ: " + msg); } -FFZ.prototype.error = function(msg, data, to_json) { - msg = "FFZ Error: " + msg + (to_json ? " -- " + JSON.stringify(data) : ""); - this._log_data.push(msg); +FFZ.prototype.error = function(msg, data, to_json, log_json) { + msg = "Error: " + 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); + console.groupCollapsed("FFZ " + msg); if ( navigator.userAgent.indexOf("Firefox/") !== -1 ) console.log(data); else console.dir(data); - console.groupEnd(msg); + console.groupEnd("FFZ " + msg); } else - console.assert(false, msg); + console.assert(false, "FFZ " + msg); } @@ -78,9 +93,9 @@ FFZ.prototype.paste_logs = function() { 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"); + callback.call(this, e.trim() + ".log"); }).fail(function(e) { - callback.bind(this)(null); + callback.call(this, null); }); } @@ -130,6 +145,7 @@ require('./ember/router'); require('./ember/channel'); require('./ember/player'); require('./ember/room'); +require('./ember/vod-chat'); require('./ember/layout'); require('./ember/line'); require('./ember/chatview'); @@ -143,7 +159,7 @@ require('./ember/following'); require('./debug'); -require('./ext/rechat'); +//require('./ext/rechat'); require('./ext/betterttv'); require('./ext/emote_menu'); @@ -156,6 +172,7 @@ require('./ui/tooltips'); require('./ui/notifications'); require('./ui/viewer_count'); require('./ui/sub_count'); +require('./ui/dash_stats'); require('./ui/menu_button'); require('./ui/following'); @@ -182,6 +199,14 @@ FFZ.prototype.initialize = function(increment, delay) { return; } + + // Check for the transfer page. + if ( location.pathname === "/crossdomain/transfer" ) { + if ( location.hash.indexOf("ffz-settings-transfer") !== -1 ) + this.init_settings_transfer(); + return; + } + // Check for special non-ember pages. if ( /^\/(?:$|search$|user\/|p\/|settings|m\/|messages?\/)/.test(location.pathname) ) { this.init_normal(delay); @@ -218,9 +243,20 @@ FFZ.prototype.initialize = function(increment, delay) { } +FFZ.prototype.init_settings_transfer = function() { + this.log("This is the HTTP Transfer URL. Building a settings backup and posting it to our parent."); + this.load_settings(); + try { this.setup_line(); } catch(err) { } + var msg = {from_ffz: true, command: "http_settings", data: this._get_settings_object()}; + window.opener.postMessage(msg, "https://www.twitch.tv"); + window.close(); +} + + 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.log("Found Twitch Player after " + (delay||0) + " ms at: " + location); + this.log("Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; this.is_dashboard = false; @@ -243,7 +279,8 @@ FFZ.prototype.init_player = function(delay) { 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.log("Found non-Ember Twitch after " + (delay||0) + " ms at: " + location); + this.log("Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; this.is_dashboard = false; @@ -272,6 +309,7 @@ FFZ.prototype.init_normal = function(delay, no_socket) { this.setup_following_count(false); this.setup_menu(); + this.setup_message_event(); this.fix_tooltips(); this.find_bttv(10); @@ -286,7 +324,11 @@ 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.log("Found Twitch Dashboard after " + (delay||0) + " ms at: " + location); + this.log("Initializing FrankerFaceZ version " + FFZ.version_info); + + var match = location.pathname.match(/\/([^\/]+)/); + this.dashboard_channel = match && match[1] || undefined; this.users = {}; this.is_dashboard = true; @@ -311,6 +353,7 @@ FFZ.prototype.init_dashboard = function(delay) { this.setup_notifications(); this.setup_following_count(false); this.setup_menu(); + this.setup_dash_stats(); this._update_subscribers(); @@ -329,7 +372,8 @@ FFZ.prototype.init_dashboard = function(delay) { 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.log("Found Twitch application after " + (delay||0) + " ms at: " + location); + this.log("Initializing FrankerFaceZ version " + FFZ.version_info); this.users = {}; this.is_dashboard = false; @@ -367,6 +411,7 @@ FFZ.prototype.init_ember = function(delay) { this.setup_player(); this.setup_channel(); this.setup_room(); + this.setup_vod_chat(); this.setup_line(); this.setup_layout(); this.setup_chatview(); @@ -389,12 +434,14 @@ FFZ.prototype.init_ember = function(delay) { this.fix_tooltips(); this.connect_extra_chat(); - this.setup_rechat(); + //this.setup_rechat(); + this.setup_message_event(); this.find_bttv(10); this.find_emote_menu(10); //this.check_news(); this.check_ff(); + this.refresh_chat(); var end = (window.performance && performance.now) ? performance.now() : Date.now(), duration = end - start; @@ -414,8 +461,16 @@ FFZ.prototype.setup_message_event = function() { FFZ.prototype._on_window_message = function(e) { - if ( ! e.data || ! e.data.from_ffz ) + var msg = e.data; + if ( typeof msg === "string" ) + msg = JSON.parse(msg); + + if ( ! msg || ! msg.from_ffz ) return; - var msg = e.data; + var handler = FFZ.msg_commands[msg.command]; + if ( handler ) + handler.call(this, msg.data); + else + this.log("Invalid Message: " + msg.command, msg.data, false, true); } \ No newline at end of file diff --git a/src/settings.js b/src/settings.js index cae0c154..682ce70f 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,40 +1,31 @@ var FFZ = window.FrankerFaceZ, constants = require("./constants"), - FileSaver = require("./FileSaver"); + utils = require("./utils"), + FileSaver = require("./FileSaver"), + createElement = document.createElement.bind(document), 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); + toggle_setting = function(swit, key, info) { + var val = !(info.get ? (typeof info.get === 'function' ? info.get.call(this) : this.settings.get(info.get)) : this.settings.get(key)); + if ( typeof info.set === "function" ) + info.set.call(this, val); + else + this.settings.set(info.set || key, 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)); - }; + option_setting = function(select, key, info) { + var val = JSON.parse(select.options[select.selectedIndex].value); + if ( typeof info.set === "function" ) + info.set.call(this, val); + else + this.settings.set(info.set || key, val); + }; // -------------------- @@ -59,11 +50,17 @@ FFZ.prototype.load_settings = function() { this.settings.del = this._setting_del.bind(this); this.settings.load = this._setting_load.bind(this); + var found_settings = false; + for(var key in FFZ.settings_info) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) continue; - this._setting_load(key); + var info = FFZ.settings_info[key], + ls_key = info && info.storage_key || make_ls(key); + + found_settings = found_settings || localStorage.hasOwnProperty(key); + this._setting_load(key) || found_settings; } // Listen for Changes @@ -75,11 +72,19 @@ FFZ.prototype.load_settings = function() { // Backup and Restore // -------------------- +FFZ.prototype._settings_open_http_window = function() { + window.open("http://www.twitch.tv/crossdomain/transfer#ffz-settings-transfer", "_ffz_settings"); +} + +FFZ.msg_commands.http_settings = function(data) { + this._load_settings_file(data); +} + + FFZ.prototype.reset_settings = function() { if ( ! confirm(this.tr('Are you sure you wish to reset FrankerFaceZ?\n\nThis will force the tab to refresh.')) ) return; - // Clear Settings for(var key in FFZ.settings_info) { if ( ! FFZ.settings_info.hasOwnProperty(key) ) @@ -94,14 +99,13 @@ FFZ.prototype.reset_settings = function() { // TODO: Filters - // Refresh window.location.reload(); } -FFZ.prototype.save_settings_file = function() { - var data = { +FFZ.prototype._get_settings_object = function() { + var data = { version: 1, script_version: FFZ.version_info + '', aliases: this.aliases, @@ -120,7 +124,15 @@ FFZ.prototype.save_settings_file = function() { data.settings[key] = this.settings[key]; } - var blob = new Blob([JSON.stringify(data, null, 4)], {type: "application/json;charset=utf-8"}); + return data; +} + + +FFZ.prototype.save_settings_file = function() { + var data = this._get_settings_object(), + blob = new Blob( + [JSON.stringify(data, null, 4)], {type: "application/json;charset=utf-8"}); + FileSaver.saveAs(blob, "ffz-settings.json"); } @@ -137,13 +149,16 @@ FFZ.prototype.load_settings_file = function(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."); - } +FFZ.prototype._load_settings_file = function(data, hide_alert) { + if ( typeof data === "string" ) + try { + data = JSON.parse(data); + } catch(err) { + this.error("Error Loading Settings: " + err); + if ( ! hide_alert ) + alert("There was an error attempting to read the provided settings data."); + return [-1,-1,-1]; + } this.log("Loading Settings Data", data); @@ -161,7 +176,7 @@ FFZ.prototype._load_settings_file = function(data) { val = data.settings[key]; if ( info.process_value ) - val = info.process_value.bind(this)(val); + val = info.process_value.call(this, val); if ( val !== this.settings.get(key) ) this.settings.set(key, val); @@ -188,9 +203,12 @@ FFZ.prototype._load_settings_file = function(data) { } // 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. Added ' + aliases + ' user nicknames.'); - }); + if ( ! hide_alert ) + setTimeout(function(){ + alert('Successfully loaded ' + applied.length + ' settings and skipped ' + skipped.length + ' settings. Added ' + aliases + ' user nicknames.'); + }); + + return [applied.length, skipped.length, aliases]; } @@ -198,553 +216,355 @@ FFZ.prototype._load_settings_file = function(data) { // Menu Page // -------------------- +var is_android = navigator.userAgent.indexOf('Android') !== -1, + settings_renderer = function(settings_data, collapsable, collapsed_key) { + return function(view, container) { + var f = this, + settings = {}, + categories = []; + + for(var key in settings_data) { + var info = settings_data[key], + cat = info.category || "Miscellaneous", + cat_store = settings[cat]; + + if ( info.hasOwnProperty('visible') ) { + var visible = info.visible; + if ( typeof visible === "function" ) + visible = visible.call(this); + + if ( ! visible ) + continue; + } + + if ( is_android && info.no_mobile ) + continue; + + if ( ! cat_store ) { + categories.push(cat); + cat_store = settings[cat] = []; + } + + cat_store.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 current_category = (collapsed_key ? this[collapsed_key] : null) || categories[0]; + + for(var ci=0; ci < categories.length; ci++) { + var category = categories[ci], + cset = settings[category], + + bttv_skipped = [], + added = 0, + + menu = createElement('div'), + heading = createElement('div'); + + + heading.className = 'heading'; + menu.className = 'chat-menu-content'; + menu.setAttribute('data-category', category); + + if ( collapsable ) { + menu.classList.add('collapsable'); + menu.classList.toggle('collapsed', current_category !== category); + menu.addEventListener('click', function() { + var t = this; + if ( ! t.classList.contains('collapsed') ) + return; + + jQuery(".chat-menu-content:not(.collapsed)", container).addClass("collapsed"); + t.classList.remove('collapsed'); + if ( collapsed_key ) + f[collapsed_key] = t.getAttribute('data-category'); + + setTimeout(function(){t.scrollIntoViewIfNeeded()}); + }); + } + + heading.innerHTML = category; + menu.appendChild(heading); + + cset.sort(function(a,b) { + var a = a[1], + b = b[1], + + at = 2, //a.type === "boolean" ? 1 : 2, + bt = 2, //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 = createElement('p'), + val = info.get ? (typeof info.get === 'function' ? info.get.call(this) : this.settings.get(info.get)) : this.settings.get(key); + + el.className = 'clearfix'; + + if ( this.has_bttv && info.no_bttv ) { + bttv_skipped.push([info.name, info.help]); + continue; + } else { + if ( info.type === "boolean" ) { + var swit = createElement('a'), + label = createElement('span'); + + swit.className = 'switch'; + swit.classList.toggle('active', val); + swit.appendChild(createElement('span')) + + label.className = 'switch-label'; + label.innerHTML = info.name; + + el.appendChild(swit); + el.appendChild(label); + + swit.addEventListener('click', toggle_setting.bind(this, swit, key, info)) + + } else if ( info.type === "select" ) { + var select = createElement('select'), + label = createElement('span'); + + label.className = 'option-label'; + label.innerHTML = info.name; + + for(var ok in info.options) { + var op = 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, info)); + + el.appendChild(label); + el.appendChild(select); + + } else if ( typeof info.method === "function" ) { + el.classList.add("option"); + var link = createElement('a'); + link.innerHTML = info.name; + link.href = '#'; + el.appendChild(link); + + link.addEventListener('click', info.method.bind(this)); + + } else + continue; + + if ( info.help || (this.has_bttv && info.warn_bttv) ) { + var help = document.createElement('span'); + help.className = 'help'; + help.innerHTML = (this.has_bttv && info.warn_bttv ? '' + info.warn_bttv + (info.help ? '
' : '') : '') + (info.help || ""); + el.appendChild(help); + } + } + + added++; + menu.appendChild(el); + } + + if ( ! added ) + continue; + + if ( bttv_skipped.length ) { + var el = createElement('p'), + label = createElement('span'), + help = createElement('span'); + + el.className = 'bttv-incompatibility clearfix disabled'; + label.className = 'switch-label'; + label.innerHTML = "Features Incompatible with BetterTTV"; + + help.className = 'help'; + for(var i=0; i < bttv_skipped.length; i++) { + var skipped = bttv_skipped[i]; + help.innerHTML += (i > 0 ? ', ' : '') + '' + skipped[0] + ''; + } + + el.appendChild(label); + el.appendChild(help); + menu.appendChild(el); + jQuery('.html-tooltip', el).tipsy({html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); + jQuery('.ffz-tooltip', el).tipsy({live: true, html: true, title: this.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); + } + + container.appendChild(menu); + } + } + }, + + render_basic = settings_renderer(FFZ.basic_settings, false, '_ffz_basic_settings_page'), + render_advanced = settings_renderer(FFZ.settings_info, true, '_ffz_settings_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'), - reset_head = document.createElement('div'), - - backup_cont = document.createElement('div'), - restore_cont = document.createElement('div'), - reset_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'), - - reset_para = document.createElement('p'), - reset_link = document.createElement('a'), - reset_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); - - reset_cont.className = 'chat-menu-content'; - reset_head.className = 'heading'; - reset_head.innerHTML = this.tr('Reset Settings'); - reset_cont.appendChild(reset_head); - - reset_para.className = 'clearfix option'; - - reset_link.href = '#'; - reset_link.innerHTML = this.tr('Reset FrankerFaceZ'); - reset_link.addEventListener('click', this.reset_settings.bind(this)); - - reset_help.className = 'help'; - reset_help.innerHTML = this.tr('This resets all of your FFZ data. That includes chat filters, nicknames for users, and settings.'); - - reset_para.appendChild(reset_link); - reset_para.appendChild(reset_help); - reset_cont.appendChild(reset_para); - - container.appendChild(backup_cont); - container.appendChild(restore_cont); - container.appendChild(reset_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", + name: "Settings", icon: constants.GEAR, sort_order: 99999, wide: true, - sub_menu: true - }; + + default_page: function() { return this.settings.advanced_settings ? 'advanced' : 'basic' }, + + pages: { + basic: { + name: "Basic", + sort_order: 1, + + render: function(view, container) { + this.settings.set("advanced_settings", false); + return render_basic.call(this, view, container); + } + }, + + advanced: { + name: "Advanced", + sort_order: 2, + + render: function(view, container) { + this.settings.set("advanced_settings", true); + return render_advanced.call(this, view, container); + } + }, + + backup: { + name: "Backup & Restore", + sort_order: 3, + + render: function(view, container) { + var backup_head = createElement('div'), + restore_head = createElement('div'), + reset_head = createElement('div'), + + backup_cont = createElement('div'), + restore_cont = createElement('div'), + reset_cont = createElement('div'), + + backup_para = createElement('p'), + backup_link = createElement('a'), + backup_help = createElement('span'), + + http_para = createElement('p'), + http_link = createElement('a'), + http_help = createElement('span'), + + restore_para = createElement('p'), + restore_input = createElement('input'), + restore_link = createElement('a'), + restore_help = createElement('span'), + + reset_para = createElement('p'), + reset_link = createElement('a'), + reset_help = 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); + + http_para.className = 'clearfix option'; + http_link.href = '#'; + http_link.innerHTML = 'Import from HTTP'; + http_link.addEventListener('click', this._settings_open_http_window.bind(this)); + + http_help.className = 'help'; + http_help.innerHTML = 'Load your settings from HTTP into HTTPS. (This briefly opens a new window.)'; + + http_para.appendChild(http_link); + http_para.appendChild(http_help); + + if ( location.protocol === "https:" ) + restore_cont.appendChild(http_para); + + reset_cont.className = 'chat-menu-content'; + reset_head.className = 'heading'; + reset_head.innerHTML = this.tr('Reset Settings'); + reset_cont.appendChild(reset_head); + + reset_para.className = 'clearfix option'; + + reset_link.href = '#'; + reset_link.innerHTML = this.tr('Reset FrankerFaceZ'); + reset_link.addEventListener('click', this.reset_settings.bind(this)); + + reset_help.className = 'help'; + reset_help.innerHTML = this.tr('This resets all of your FFZ data. That includes chat filters, nicknames for users, and settings.'); + + reset_para.appendChild(reset_link); + reset_para.appendChild(reset_help); + reset_cont.appendChild(reset_para); + + container.appendChild(backup_cont); + container.appendChild(restore_cont); + container.appendChild(reset_cont); + } + } + } +}; // -------------------- @@ -790,7 +610,7 @@ FFZ.prototype._setting_update = function(e) { if ( info.process_value ) try { - val = info.process_value.bind(this)(val); + val = info.process_value.call(this, val); } catch(err) { this.log('Error processing value for setting "' + key + '": ' + err); return; @@ -799,7 +619,7 @@ FFZ.prototype._setting_update = function(e) { this.settings[key] = val; if ( info.on_update ) try { - info.on_update.bind(this)(val, false); + info.on_update.call(this, val, false); } catch(err) { this.log('Error running updater for setting "' + key + '": ' + err); } @@ -825,7 +645,7 @@ FFZ.prototype._setting_load = function(key, default_value) { } if ( info && info.process_value ) - val = info.process_value.bind(this)(val); + val = info.process_value.call(this, val); this.settings[key] = val; return val; @@ -840,13 +660,13 @@ FFZ.prototype._setting_get = function(key) { } -FFZ.prototype._setting_set = function(key, val) { +FFZ.prototype._setting_set = function(key, val, suppress_log) { var info = FFZ.settings_info[key], ls_key = info.storage_key || make_ls(key); if ( info.process_value ) try { - val = info.process_value.bind(this)(val) + val = info.process_value.call(this, val) } catch(err) { this.log('Error processing value for setting "' + key + '": ' + err); return false; @@ -857,11 +677,12 @@ FFZ.prototype._setting_set = function(key, val) { var jval = JSON.stringify(val); localStorage.setItem(ls_key, jval); - this.log('Changed Setting "' + key + '" to: ' + jval); + if ( ! suppress_log ) + this.log('Changed Setting "' + key + '" to: ' + jval); if ( info.on_update ) try { - info.on_update.bind(this)(val, true); + info.on_update.call(this, val, true); } catch(err) { this.log('Error running updater for setting "' + key + '": ' + err); } @@ -883,7 +704,7 @@ FFZ.prototype._setting_del = function(key) { if ( info.on_update ) try { - info.on_update.bind(this)(val, true); + info.on_update.call(this, val, true); } catch(err) { this.log('Error running updater for setting "' + key + '": ' + err); } diff --git a/src/socket.js b/src/socket.js index 26cb2079..7a58c9fa 100644 --- a/src/socket.js +++ b/src/socket.js @@ -22,6 +22,7 @@ FFZ.prototype._ws_open = false; FFZ.prototype._ws_delay = 0; FFZ.prototype._ws_host_idx = -1; FFZ.prototype._ws_current_pool = -1; +FFZ.prototype._ws_last_ping = null; FFZ.prototype._ws_server_offset = null; @@ -166,7 +167,7 @@ FFZ.prototype.ws_create = function() { // Send the channel(s). if ( f._cindex ) { - var channel_id = f._cindex.get('controller.id'), + var channel_id = f._cindex.get('controller.model.id'), hosted_id = f._cindex.get('controller.hostModeTarget.id'); if ( channel_id ) @@ -343,7 +344,7 @@ FFZ.prototype.setup_time = function() { difference = (new_time - last_time) - 5000; last_time = new_time; - if ( Math.abs(difference) > 250 ) { + if ( Math.abs(difference) > 1000 ) { f.log("WARNING! Time drift of " + difference + "ms across 5 seconds. Did the local time change?"); f._ws_server_offset = null; f.ws_ping(); @@ -373,7 +374,7 @@ FFZ.prototype._ws_on_pong = function(success, server_time) { if ( this._ws_ping_time ) { var rtt = now - this._ws_ping_time, - ping = rtt / 2; + ping = this._ws_last_ping = rtt / 2; this._ws_ping_time = null; this._ws_server_offset = (d_now - (server_time + ping)); @@ -404,7 +405,7 @@ FFZ.ws_commands.reconnect = function() { // Socket Close Callbacks for(var i=0; i < FFZ.ws_on_close.length; i++) { try { - FFZ.ws_on_close[i].bind(this)(); + FFZ.ws_on_close[i].call(this); } catch(err) { this.log("Error on Socket Close Callback: " + err); } diff --git a/src/styles/badges-sub-notice-on.css b/src/styles/badges-sub-notice-on.css new file mode 100644 index 00000000..ac342725 --- /dev/null +++ b/src/styles/badges-sub-notice-on.css @@ -0,0 +1 @@ +.chat-line.notification .indicator { display: none !important } \ No newline at end of file diff --git a/src/styles/badges-sub-notice.css b/src/styles/badges-sub-notice.css new file mode 100644 index 00000000..84f639bd --- /dev/null +++ b/src/styles/badges-sub-notice.css @@ -0,0 +1 @@ +.chat-line.notification .badge.subscriber { display: none } \ No newline at end of file diff --git a/src/styles/badges-transparent.css b/src/styles/badges-transparent.css index 73d607b5..e32c96f3 100644 --- a/src/styles/badges-transparent.css +++ b/src/styles/badges-transparent.css @@ -5,6 +5,8 @@ /* Invert Some Badges */ body:not(.ffz-dark) .app-main:not(.theatre) .conversation-window .badges .badge:not(.subscriber):not(.ffz-badge-0), +body:not(.ffz-dark) > .chat-container:not(.dark):not(.force-dark) .badges .badge:not(.subscriber):not(.ffz-badge-0), +body:not(.ffz-dark) > .ember-chat-container:not(.dark):not(.force-dark) .badges .badge:not(.subscriber):not(.ffz-badge-0), .app-main:not(.theatre) .chat-container:not(.dark):not(.force-dark) .badges .badge:not(.subscriber):not(.ffz-badge-0), .app-main:not(.theatre) .ember-chat-container:not(.dark):not(.force-dark) .badges .badge:not(.subscriber):not(.ffz-badge-0) { filter: invert(100%); diff --git a/src/styles/chat-background.css b/src/styles/chat-background.css index 70a7dc1b..19bad738 100644 --- a/src/styles/chat-background.css +++ b/src/styles/chat-background.css @@ -1,10 +1,6 @@ /* Regular Alternating Background */ .conversation-chat-lines > div:nth-child(2n+0):before, -.chat-history .chat-line:nth-child(2n+0):before, -.ember-chat .chat-lines > div:nth-child(2n+0) .chat-line:before, - -/* ReChat */ -.ember-chat.chat-messages > .rechat-chat-line:nth-child(2n+0):before { +.chat-line:nth-child(2n+0):before { background-color: rgba(0,0,0, 0.1); } @@ -14,40 +10,32 @@ .ffz-dark .chat-history .chat-line:nth-child(2n+0):before, .theatre .conversation-chat-lines > div:nth-child(2n+0):before, -.theatre .chat-history .chat-line:nth-child(2n+0):before, -.theatre .ember-chat .chat-lines > div:nth-child(2n+0) .chat-line:before, +.theatre .chat-line:nth-child(2n+0):before, -.dark .chat-history .chat-line:nth-child(2n+0):before, -.force-dark .chat-history .chat-line:nth-child(2n+0):before, -.dark .chat-lines > div:nth-child(2n+0) .chat-line:before, -.force-dark .chat-lines > div:nth-child(2n+0) .chat-line:before, - -/* ReChat */ -.theatre .chat-lines > .rechat-chat-line:nth-child(2n+0):before, -.dark .chat-lines > .rechat-chat-line:nth-child(2n+0):before, -.force-dark .chat-lines > .rechat-chat-line:nth-child(2n+0):before { +.dark .chat-line:nth-child(2n+0):before, +.force-dark .chat-line:nth-child(2n+0):before { background-color: rgba(255,255,255, 0.05); } /* DEPRECIATED: Whisper Backgrounds */ -.ember-chat .chat-line.whisper-line:before { +.chat-line.whisper:before { background-color: rgba(185, 163, 227, 0.2); } -.ember-chat .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before { +.chat-line.whisper:nth-child(2n+0):before { background-color: rgba(185, 163, 227, 0.4); } -.theatre .chat-line.whisper-line:before, -.dark .chat-line.whisper-line:before, -.force-dark .chat-line.whisper-line:before { +.theatre .chat-line.whisper:before, +.dark .chat-line.whisper:before, +.force-dark .chat-line.whisper:before { background-color: rgba(100, 65, 165, 0.2); } -.theatre .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before, -.dark .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before, -.force-dark .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before { +.theatre .chat-line.whisper:nth-child(2n+0):before, +.dark .chat-line.whisper:nth-child(2n+0):before, +.force-dark .chat-line.whisper:nth-child(2n+0):before { background-color: rgba(100, 65, 165, 0.4); } @@ -111,13 +99,11 @@ /* DEPRECIATED: Mention Backgrounds */ -.chat-history .chat-line.ffz-mentioned:before, -.ember-chat .chat-line.ffz-mentioned:before { +.chat-line.ffz-mentioned:before { background-color: rgba(255,127,127,0.2); } -.chat-history .chat-line.ffz-mentioned:nth-child(2n+0):before, -.ember-chat .chat-lines > div:nth-child(2n+0) .chat-line.ffz-mentioned:before { +.chat-line.ffz-mentioned:nth-child(2n+0):before { background-color: rgba(255,127,127, 0.4); } @@ -131,14 +117,10 @@ background-color: rgba(255,0,0, 0.2) !important; } - -.theatre .chat-lines > div:nth-child(2n+0) .chat-line.ffz-mentioned:before, - -.dark .chat-history .chat-line.ffz-mentioned:nth-child(2n+0):before, -.force-dark .chat-history .chat-line.ffz-mentioned:nth-child(2n+0):before, - -.dark .chat-lines > div:nth-child(2n+0) .chat-line.ffz-mentioned:before, -.force-dark .chat-lines > div:nth-child(2n+0) .chat-line.ffz-mentioned:before { +.ffz-dark .chat-history .chat-line.ffz-mentioned:nth-child(2n+0):before, +.theatre .chat-line.ffz-mentioned:nth-child(2n+0):before, +.dark .chat-line.ffz-mentioned:nth-child(2n+0):before, +.force-dark .chat-line.ffz-mentioned:nth-child(2n+0):before { background-color: rgba(255,0,0, 0.3) !important; } diff --git a/src/styles/chat-separator.css b/src/styles/chat-separator.css index 2aad25cd..5cba6d2c 100644 --- a/src/styles/chat-separator.css +++ b/src/styles/chat-separator.css @@ -15,15 +15,14 @@ } - /* Hide First Line */ .conversation-chat-lines > div:first-child:before, -.chat-lines > div:first-child .chat-line:before { - border-top-color: transparent; +.chat-line:first-of-type:before { + border-top-color: transparent !important; } /* Hide Last Line */ .conversation-chat-lines > div:last-child:nth-child(odd):before, -.chat-lines > div:last-child:nth-child(odd) .chat-line:before { - border-bottom-color: transparent; +.chat-line:last-of-type:nth-child(odd):before { + border-bottom-color: transparent !important; } \ No newline at end of file diff --git a/src/tokenize.js b/src/tokenize.js index b45acebb..1ca4e737 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -4,18 +4,13 @@ var FFZ = window.FrankerFaceZ, helpers, conv_helpers, - EXPLANATION_TRAIL = '


FFZ is hiding this link because this url shortener is known to be used by Twitch spam bots posting malicious links. Please use caution when visiting shortened links.', - + EXPLANATION_WARN = '
This link has been sent to you via a whisper rather than standard chat, and has not been checked or approved of by any moderators or staff members. Please treat this link with caution and do not visit it if you do not trust the sender.', 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 = /(?:https?:\/\/)?(?:[-a-zA-Z0-9@:%_\+~#=]+\.)+[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/g, LINK_SPLIT = /^(?:(https?):\/\/)?(?:(.*?)@)?([^\/:]+)(?::(\d+))?(.*?)(?:\?(.*?))?(?:\#(.*?))?$/, YOUTUBE_CHECK = /^(?:https?:\/\/)?(?:m\.|www\.)?youtu(?:be\.com|\.be)\/(?:v\/|watch\/|.*?(?:embed|watch).*?v=)?([a-zA-Z0-9\-_]+)$/, @@ -46,91 +41,6 @@ var FFZ = window.FrankerFaceZ, return ''; }, - - build_link_tooltip = function(href) { - var link_data = this._link_data[href], - - tooltip; - - if ( link_data && link_data.tooltip ) - return link_data.tooltip; - - if ( ! link_data ) - return ""; - - 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; @@ -138,24 +48,12 @@ var FFZ = window.FrankerFaceZ, 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 ( ! this.settings.link_info ) + return; - 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; - } + // If this link is unsafe, add the unsafe-link class to all instances of the link. + if ( data.unsafe ) + jQuery('a.chat-link[data-url="' + href + '"]').addClass('unsafe-link'); }; @@ -167,7 +65,7 @@ FFZ.src_to_id = function(src) { var match = /\/emoticons\/v1\/(\d+)\/1\.0/.exec(src), id = match ? parseInt(match[1]) : null; - if ( id === NaN ) + if ( Number.isNaN(id) ) id = null; FFZ.SRC_IDS[src] = id; @@ -181,9 +79,10 @@ FFZ._emote_mirror_swap = function(img) { return; img.setAttribute('data-alt-attempts', attempts + 1); - var id = img.getAttribute('data-emote'); + var id = img.getAttribute('data-emote'), + src = '//' + img.src.split('//')[1]; - if ( img.src.substr(0, constants.TWITCH_BASE.length) === constants.TWITCH_BASE ) { + if ( src.substr(0, constants.TWITCH_BASE.length) === constants.TWITCH_BASE ) { img.src = constants.EMOTE_MIRROR_BASE + id + ".png"; img.srcset = ""; } else { @@ -283,6 +182,9 @@ FFZ.prototype.setup_tokenization = function() { var show_deleted = f.settings.show_deleted_links; return _.chain(tokens).map(function(token) { + if ( token.type === "text" ) + token = token.text; + if ( ! _.isString(token) ) return token; @@ -293,10 +195,21 @@ FFZ.prototype.setup_tokenization = function() { 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 {isLink: true, href: e}; + var long = e.length > 255, + out = { + type: "link", + length: e.length, + isDeleted: ! show_deleted && (delete_links || long), + isLong: long, + isMailTo: e.indexOf("@") > -1 && (-1 === e.indexOf("/") || e.indexOf("@") < e.indexOf("/")), + text: e, + link: e + }; + + if ( ! out.isMailTo && ! e.match(/^(?:https?:\/\/)/) ) + out.link = "http://" + e; + + return out; }) ); }).flatten().compact().value(); @@ -325,8 +238,6 @@ FFZ.prototype.load_twitch_emote_data = function(tries) { this._twitch_set_to_channel[33] = "--turbo-faces--"; this._twitch_set_to_channel[42] = "--turbo-faces--"; - this._reset_tooltips(true); - }).fail(function(data) { if ( data.status === 404 ) return; @@ -338,6 +249,127 @@ FFZ.prototype.load_twitch_emote_data = function(tries) { } +// --------------------- +// Tooltip Rendering +// --------------------- + +FFZ.prototype.render_tooltip = function(el) { + var f = this, + func = function() { + if ( this.classList.contains('emoticon') ) { + var preview_url, width=0, height=0, image, set_id, emote, emote_set, + emote_id = this.getAttribute('data-ffz-emote'); + if ( emote_id ) { + set_id = this.getAttribute('data-ffz-set'); + emote_set = f.emote_sets[set_id]; + emote = emote_set && emote_set.emoticons[emote_id]; + + if ( emote ) { + var owner = emote.owner, + title = emote_set.title || "Global", + source = emote_set.source || "FFZ"; + + if ( f.settings.emote_image_hover ) { + if ( emote.urls[4] ) { + height = emote.height * 4; + width = emote.width * 4; + preview_url = emote.urls[4]; + + } else if ( emote.urls[2] ) { + height = emote.height * 2; + width = emote.width * 2; + } + + if ( width > 186 ) + height *= 186 / width; + height = Math.min(186, height); + + } else + preview_url = null; + + //image = preview_url ? `` : ''; + image = preview_url ? '' : ''; + return image + 'Emoticon: ' + (emote.hidden ? '???' : emote.name) + '
' + source + ' ' + title + (owner ? '
By: ' + owner.display_name : ''); + + //return `${image}Emoticon: ${emote.hidden ? '???' : emote.name}
${source} ${title}${owner ? '
By: ' + owner.display_name : ""}`; + } + } + + emote_id = this.getAttribute('data-emote'); + if ( emote_id ) { + set_id = f._twitch_emote_to_set[emote_id]; + emote_set = set_id && f._twitch_set_to_channel[set_id]; + var set_type = "Channel"; + + preview_url = f.settings.emote_image_hover && (constants.TWITCH_BASE + emote_id + '/3.0'); + //image = preview_url ? `` : ''; + image = preview_url ? '' : ''; + + if ( emote_set === "--global--" ) { + emote_set = "Twitch Global"; + set_type = null; + } else if ( emote_set === "--twitch-turbo--" || emote_set === "turbo" || emote_set === "--turbo-faces--" ) { + emote_set = "Twitch Turbo"; + set_type = null; + } + + if ( this.classList.contains('ffz-tooltip-no-credit') ) + return image + this.alt; + else + return image + 'Emoticon: ' + this.alt + '
' + (set_type ? set_type + ': ' : '') + emote_set; + //return `${image}Emoticon: ${this.alt}
${set_type ? set_type + ": " : ""}${emote_set}`; + } + + emote_id = this.getAttribute('data-ffz-emoji'); + if ( emote_id ) { + emote = f.emoji_data[emote_id]; + var src = emote && (f.settings.parse_emoji === 3 ? emote.one_src : (f.settings.parse_emoji === 2 ? emote.noto_src : emote.tw_src)); + + preview_url = f.settings.emote_image_hover && src; + //image = preview_url ? `` : ''; + image = preview_url ? '' : ''; + + return image + "Emoji: " + this.alt + '
Name: ' + emote.name + (emote.short_name ? '
Short Name :' + emote.short_name + ':' : '') + (emote.cat ? '
Category: ' + utils.sanitize(constants.EMOJI_CATEGORIES[emote.cat] || emote.cat) : ''); + //return `${image}Emoji: ${this.alt}
Name: ${emote.name}${emote.short_name ? '
Short Name: :' + emote.short_name + ':' : ''}`; + } + + } else if ( this.classList.contains('email-link') ) { + var url = this.getAttribute("data-url"); + return url ? "E-Mail " + url.substr(7) : ''; + + } else if ( this.classList.contains('chat-link') ) { + // TODO: A lot of shit. Lookup data. + var url = this.getAttribute("data-url"), + text = ''; + + if ( ! url ) + return; + + if ( f.settings.link_image_hover && is_image(url, f.settings.image_hover_all_domains) ) + preview_url = url; + else + preview_url = null; + + image = preview_url ? image_iframe(url) : ''; + + // If it's not a deleted link, don't waste time showing the URL in the tooltip. + if ( this.classList.contains('deleted-link') ) + text = url; + + if ( this.classList.contains('warn-link') ) + text += EXPLANATION_WARN; + + return image + text; //`${image}${text}`; + } + + f.log("Unable to Build Tooltip For: " + this.className, this); + return ""; + }; + + return el ? func(el) : func; +}; + + // --------------------- // Tokenization // --------------------- @@ -364,9 +396,6 @@ FFZ.prototype.tokenize_conversation_line = function(message, prevent_notificatio if ( helpers && helpers.emoticonizeMessage && emotes ) 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(from_user, undefined, tokens, from_me); @@ -392,14 +421,13 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del if ( msgObject.cachedTokens ) return msgObject.cachedTokens; - var msg = msgObject.message || msgObject.get('body'), - user = this.get_user(), - room_id = msgObject.room, - from_user = msgObject.from, - from_me = user && from_user === user.login, - emotes = msgObject.tags && msgObject.tags.emotes, - - tokens = [msg]; + var msg = msgObject.get && (msgObject.get('message') || msgObject.get('body')) || msgObject.message, + room_id = msgObject.get && msgObject.get('room') || msgObject.room, + from_user = msgObject.get && msgObject.get('from') || msgObject.from, + user = this.get_user(), + from_me = user && from_user === user.login, + emotes = msgObject.get && msgObject.get('tags.emotes') || msgObject.tags && msgObject.tags.emotes, + tokens = [msg]; // Standard tokenization if ( helpers && helpers.linkifyMessage ) { @@ -421,9 +449,6 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del 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(from_user, room_id, tokens, from_me); @@ -442,9 +467,11 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del tokens = this.tokenize_mentions(tokens); for(var i=0; i < tokens.length; i++) { - var token = tokens[i]; - if ( _.isString(token) || ! token.mentionedUser || token.own ) - continue; + var token = tokens[i], + is_mention = token.type === "mention"; + + if ( ! is_mention || token.isOwnMessage ) + continue; // We have a mention! msgObject.ffz_has_mention = true; @@ -535,11 +562,8 @@ FFZ.prototype.tokenize_line = function(user, room, message, no_emotes, no_emoji) message = helpers.mentionizeMessage(message, u.login, user === u.login); } - if ( ! no_emotes ) { + 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); @@ -548,153 +572,113 @@ FFZ.prototype.tokenize_line = function(user, room, message, no_emotes, no_emoji) } -FFZ.prototype.render_tokens = function(tokens, render_links) { +FFZ.prototype.render_tokens = function(tokens, render_links, warn_links) { var f = this; return _.map(tokens, function(token) { + if ( ! token ) + return ""; + if ( token.hidden ) return ""; - else if ( token.isRaw ) + else if ( token.type === "raw" ) return token.html; - else if ( token.emoticonSrc ) { - var tooltip, src = token.emoticonSrc, srcset, cls, extra; + else if ( token.type === "emoticon" ) { + var src = token.imgSrc, srcset, cls, 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 ? f._emote_tooltip(emote) : token.altText; srcset = emote ? emote.srcSet : token.srcSet; - extra = (emote ? ' data-ffz-emote="' + emote.id + '"' : '') + (emote_set ? ' data-ffz-set="' + emote_set.id + '"' : ''); + //extra = (emote ? ` data-ffz-emote="${emote.id}"` : '') + (emote_set ? ` data-ffz-set="${emote_set.id}"` : ''); + extra = (emote ? ' data-ffz-emote="' + emote.id + '"' : '') + (emote_set ? ' data-ffz-set="' + emote_set.id + '"' : '') } else if ( token.ffzEmoji ) { - var eid = token.ffzEmoji, - emoji = f.emoji_data && f.emoji_data[eid], - setting = f.settings.parse_emoji, - image = ''; - - if ( setting === 0 || (setting === 1 && ! emoji.tw) || (setting === 2 && ! emoji.noto) ) + var setting = f.settings.parse_emoji; + if ( setting === 0 || (setting === 1 && ! token.tw) || (setting === 2 && ! token.noto) || (setting === 3 && ! token.one) ) return token.altText; - src = setting === 2 ? token.noto_src : token.tw_src; - - if ( emoji && f.settings.emote_image_hover ) - image = ''; - - tooltip = emoji ? image + "Emoji: " + token.altText + "
Name: " + emoji.name + (emoji.short_name ? "
Short Name: :" + emoji.short_name + ":" : "") : token.altText; - extra = ' data-ffz-emoji="' + eid + '" height="18px"'; + src = setting === 3 ? token.one_src : (setting === 2 ? token.noto_src : token.tw_src); + //extra = ` data-ffz-emoji="${token.ffzEmoji}" height="18px"`; + extra = ' data-ffz-emoji="' + token.ffzEmoji + '" height="18px"'; cls = ' emoji'; } else { - var id = token.replacedId || FFZ.src_to_id(token.emoticonSrc), - data = id && f._twitch_emotes && f._twitch_emotes[id]; + var id = FFZ.src_to_id(src), + replacement = f.settings.replace_bad_emotes && constants.EMOTE_REPLACEMENTS[id]; - if ( data ) - tooltip = data.tooltip ? data.tooltip : utils.build_tooltip.bind(f)(id, false, token.altText); - else { - try { - var set_id = f._twitch_emote_to_set[id]; - if ( set_id ) { - tooltip = utils.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, utils.load_emote_data.bind(f, id, token.altText)); - } - } catch(err) { - f.error("Error Generating Emote Tooltip: " + err); - } - } + //extra = ` data-emote="${id}" onerror="FrankerFaceZ._emote_mirror_swap(this)"`; + extra = ' data-emote="' + id + '" onerror="FrankerFaceZ._emote_mirror_swap(this)"'; - //var mirror_url = utils.quote_attr(constants.EMOTE_MIRROR_BASE + id + '.png'); - extra = ' data-emote="' + id + '" onerror="FrankerFaceZ._emote_mirror_swap(this)"'; // Disable error checking for now. - - if ( ! constants.EMOTE_REPLACEMENTS[id] ) + if ( replacement ) { + src = constants.EMOTE_REPLACEMENT_BASE + replacement; + srcset = ''; + } else srcset = utils.build_srcset(id); } - return ''; + //return `${utils.quote_attr(token.altText)}`; + return '' + utils.quote_attr(token.altText) + ''; } - else if ( token.isLink ) { - var text = token.title || (token.isLong && '') || (token.isShortened && '') || (token.isDeleted && '') || token.href; + else if ( token.type === "link" ) { + var text = token.title || (token.isLong && '') || (token.isDeleted && '') || (warn_links && '') || token.text; if ( ! render_links && render_links !== undefined ) return utils.sanitize(text); - var href = token.href, - tooltip, cls = '', + var href = token.link || token.text, + cls = ''; - ind_at = href.indexOf("@"), - ind_sl = href.indexOf("/"); - - if ( ind_at !== -1 && (ind_sl === -1 || ind_at < ind_sl) ) { + if ( token.isMailTo ) { // E-Mail Link cls = 'email-link'; - - if ( f.settings.link_info ) { - cls += ' tooltip'; - tooltip = 'E-Mail ' + href; - } - - href = 'mailto:' + href; + href = 'mailto:' + href; } else { // Web Link - if ( ! href.match(/^https?:\/\//) ) - href = 'http://' + href; + cls = 'chat-link'; - 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); - } + if ( f.settings.link_info ) { + if (!( f._link_data && f._link_data[href] )) { + f._link_data = f._link_data || {}; + f._link_data[href] = true; + f.ws_send("get_link", href, load_link_data.bind(f, href)); + } + } } - // Deleted Links var actual_href = href; - if ( token.isShortened ) { - cls = 'shortened-link deleted-link ' + cls; - tooltip = utils.sanitize(token.href) + EXPLANATION_TRAIL; - href = '#'; - - } else if ( token.isDeleted ) { + if ( token.isDeleted ) { cls = 'deleted-link ' + cls; - tooltip = utils.sanitize(token.censoredHref || token.href); href = '#'; - } - return '' + utils.sanitize(text) + ''; + } else if ( warn_links ) { + cls = 'warn-link deleted-link ' + cls; + href = '#'; + } + + //return `${utils.sanitize(text)}`; + return '' + utils.sanitize(text) + ''; } - else if ( token.mentionedUser ) - return '' + utils.sanitize(token.mentionedUser) + ""; + else if ( token.type === "deleted" ) + return '×××'; + //return `×××`; - else if ( token.deletedLink ) + else if ( token.type === "mention" ) + return '' + utils.sanitize(token.user) + ''; + //return `${utils.sanitize(token.user)}`; + + else if ( token.deletedLink || token.text ) return utils.sanitize(token.text); + else if ( typeof token !== "string" ) + return '[invalid token]'; + //return `[invalid token]`; + return utils.sanitize(token); }).join(""); } @@ -704,170 +688,80 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { // 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: utils.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; + "use strict"; - // Get our sets. - var sets = this.getEmotes(user, room), - emotes = []; + var sets = this.getEmotes(user, room), + emotes = {}, + emote, - // Build a list of emotes that match. - _.each(sets, function(set_id) { - var set = f.emote_sets[set_id]; - if ( ! set ) - return; + new_tokens = []; - _.each(set.emoticons, function(emote) { - _.any(tokens, function(token) { - return _.isString(token) && token.match(emote.regex); - }) && emotes.push(emote); - }); - }); + if ( ! tokens || ! tokens.length || ! sets || ! sets.length ) + return tokens; - // Don't bother proceeding if we have no emotes. - if ( ! emotes.length ) - return tokens; + // Build an object with all of our emotes. + for(var i=0; i < sets.length; i++) { + var emote_set = this.emote_sets[sets[i]]; + if ( emote_set && emote_set.emoticons ) + for(var emote_id in emote_set.emoticons) { + emote = emote_set.emoticons[emote_id]; + if ( ! emotes[emote.name] ) + emotes[emote.name] = emote; + } + } - // Now that we have all the matching tokens, do crazy stuff. - if ( typeof tokens === "string" ) - tokens = [tokens]; + 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) - }; + for(var i=0, l=tokens.length; i < l; i++) { + var token = tokens[i]; + if ( ! token ) + continue; - tokens = _.compact(_.flatten(_.map(tokens, function(token) { - if ( _.isObject(token) ) - return token; + if ( typeof token !== "string" ) + if ( token.type === "text" ) + token = token.text; + else { + new_tokens.push(token); + continue; + } - 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); + // Split the token! + var segments = token.split(' '), + text = [], segment; - tbits.shift(); - bits.push(eo); + for(var x=0,y=segments.length; x < y; x++) { + segment = segments[x]; + emote = emotes[segment]; - if ( do_report && room ) - f.add_usage(room, emote); + if ( emote ) { + if ( text.length ) { + // We have pending text. Join it together, with an extra space + // on the end for good measure. + new_tokens.push({type: "text", text: text.join(' ') + ' '}); + text = [] + } - } else - bits.push(bit); - } - return bits; - }))); - }); + // Push this emote to the tokens. + new_tokens.push(emote.token); - return tokens; + if ( do_report && room ) + this.add_usage(room, emote); + + // Finally, push an empty string to text so that this emote gets spaced. + text.push(''); + + } else + text.push(segment); + } + + // Add any left over text from this segment. + if ( text.length > 1 || (text.length === 1 && text[0] !== '') ) + new_tokens.push({type: "text", text: text.join(' ')}); + } + + return new_tokens; } @@ -876,48 +770,54 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { // --------------------- FFZ.prototype.tokenize_emoji = function(tokens) { - if ( typeof tokens === "string" ) - tokens = [tokens]; + "use strict"; + if ( ! tokens || ! tokens.length || ! this.emoji_data ) + return tokens; - if ( ! this.emoji_data ) - return tokens; + if ( typeof tokens === "string" ) + tokens = [tokens]; - var f = this; + var new_tokens = []; - return _.compact(_.flatten(_.map(tokens, function(token) { - if ( _.isObject(token) ) - return token; + for(var i=0, l=tokens.length; i < l; i++) { + var token = tokens[i]; + if ( ! token ) + continue; - 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 ( typeof token !== "string" ) + if ( token.type === "text" ) + token = token.text; + else { + new_tokens.push(token); + continue; + } - if ( tbits.length ) { - // We have an emoji too, so let's handle that. - var match = tbits.shift(), - variant = tbits.shift(); + var segments = token.split(constants.EMOJI_REGEX), + text = null; - if ( variant === '\uFE0E' ) { - // Text Variant - bits.push(match); + while(segments.length) { + text = (text || '') + segments.shift(); - } else { - // Find the right image~! - var eid = utils.emoji_to_codepoint(match, variant), - data = f.emoji_data[eid]; + if ( segments.length ) { + var match = segments.shift(), + eid = utils.emoji_to_codepoint(match), + data = this.emoji_data[eid]; - if ( data ) - bits.push(data.token); - else - bits.push(match + (variant || "")); - } - } - } + if ( data ) { + if ( text && text.length ) + new_tokens.push({type: "text", text: text}); + new_tokens.push(data.token); + text = null; + } else + text = (text || '') + match; + } + } - return bits; - }))); + if ( text && text.length ) + new_tokens.push({type: "text", text: text}); + } + + return new_tokens; } @@ -942,7 +842,7 @@ FFZ._words_to_regex = function(list) { reg += (reg ? "|" : "") + reg_escape(list[i]); } - regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + SEPARATORS + ")(" + reg + ")(?=$|" + SEPARATORS + ")", "ig"); + regex = FFZ._regex_cache[list] = new RegExp("(^|.*?" + constants.SEPARATORS + ")(" + reg + ")(?=$|" + constants.SEPARATORS + ")", "ig"); } return regex; @@ -962,12 +862,10 @@ FFZ.prototype.tokenize_mentions = function(tokens) { for(var i=0; i < tokens.length; i++) { var token = tokens[i]; - if ( ! _.isString(token) ) { - new_tokens.push(token); - continue; - } + if ( token.type === "text" ) + token = token.text; - if ( ! token.match(regex) ) { + if ( ! _.isString(token) || ! token.match(regex) ) { new_tokens.push(token); continue; } @@ -975,8 +873,10 @@ FFZ.prototype.tokenize_mentions = function(tokens) { token = token.replace(regex, function(all, prefix, match) { new_tokens.push(prefix); new_tokens.push({ - mentionedUser: match, - own: false + type: "mention", + length: match.length, + user: match, + isOwnMessage: false, }); return ""; @@ -998,38 +898,25 @@ FFZ.prototype._deleted_link_click = function(e) { if ( ! this.classList.contains("deleted-link") ) return true; + // Stop from Navigating + e.preventDefault(); + // Get the URL - var href = this.getAttribute('data-url'), - link = this.getAttribute('data-original-url') || href, + var link = this.getAttribute('data-url'), + text = this.getAttribute('data-text') || link, 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; + this.classList.remove('warn-link'); // Set up the Link - this.href = href; - this.target = "_new"; - this.textContent = link; + this.href = link; + this.target = "_blank"; + this.textContent = text; - // Now, check for a tooltip. - var link_data = f._link_data[href]; - 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(); + // Refresh tipsy. + jQuery(this).trigger('mouseout').trigger('mouseover'); } diff --git a/src/ui/about_page.js b/src/ui/about_page.js index 2a6c02d4..62b2c25e 100644 --- a/src/ui/about_page.js +++ b/src/ui/about_page.js @@ -1,18 +1,26 @@ var FFZ = window.FrankerFaceZ, - constants = require("../constants"); + constants = require("../constants"), + utils = require("../utils"), + createElement = document.createElement.bind(document), + + NICE_DESCRIPTION = { + "cluster": null, + "manifest_cluster": null, + "user_ip": null + }; // ------------------- // Initialization // ------------------- -FFZ.prototype._has_news = false; +/*FFZ.prototype._has_news = false; FFZ.prototype._news_id = 0; FFZ.prototype.check_news = function(tries) { jQuery.ajax(constants.SERVER + "script/news.json", {cache: false, dataType: "json", context: this}) .done(function(data) { - FFZ.ws_commands.update_news.bind(this)(data.id); + FFZ.ws_commands.update_news.call(this, data.id); }).fail(function(data) { if ( data.status === 404 ) return; @@ -26,7 +34,7 @@ FFZ.prototype.check_news = function(tries) { FFZ.ws_commands.update_news = function(version) { var old_version = parseInt(localStorage.ffzLastNewsId || "0") || 0; - if ( ! old_version || old_version === NaN || old_version < 0 ) + if ( ! old_version || Number.isNaN(old_version) || old_version < 0 ) old_version = 0; if ( version <= old_version ) { @@ -37,192 +45,363 @@ FFZ.ws_commands.update_news = function(version) { this._has_news = true; this._news_id = version; this.update_ui_link(); -} +}*/ // ------------------- // About Page // ------------------- -FFZ.menu_pages.about_changelog = { - name: "Changelog", - visible: false, - wide: true, +var include_html = function(heading_text, filename) { + return function(view, container) { + var heading = createElement('div'); + heading.className = 'chat-menu-content center'; + heading.innerHTML = '

FrankerFaceZ

' + (heading_text ? '
' + heading_text + '
' : ''); - render: function(view, container) { - var heading = document.createElement('div'); + jQuery.ajax(filename, {cache: false, context: this}) + .done(function(data) { + container.appendChild(heading); + container.innerHTML += data; - heading.className = 'chat-menu-content center'; - heading.innerHTML = '

FrankerFaceZ

change log
'; + jQuery('#ffz-old-news-button', container).on('click', function() { + jQuery(this).remove(); + jQuery('#ffz-old-news', container).css('display', 'block'); + }); - 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 = createElement('div'); + content.className = 'chat-menu-content menu-side-padding'; + content.textContent = 'There was an error loading this page from the server.'; - }).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); - }); - } -}; + container.appendChild(heading); + container.appendChild(content); + }); + } + }, + render_news = include_html("news", constants.SERVER + "script/news.html"); -FFZ.menu_pages.about_news = { - name: "News", - visible: false, - wide: true, +var update_player_stats = function(player, container) { + if ( ! document.querySelector('.ffz-ui-sub-menu-page[data-page="debugging"]') || ! player.getVideoInfo ) + return; - render: function(view, container) { - // Handle the news state. - if ( this._has_news ) { - this._has_news = false; - localStorage.ffzLastNewsId = this._news_id; - this.update_ui_link(); - } + setTimeout(update_player_stats.bind(this, player, container), 1000); + var player_data; - var heading = document.createElement('div'); + try { + player_data = player.getVideoInfo(); + } catch(err) { } - heading.className = 'chat-menu-content center'; - heading.innerHTML = '

FrankerFaceZ

announcements and news
'; + if ( ! player_data ) + return; - jQuery.ajax(constants.SERVER + "script/news.html", {cache: false, context: this}) - .done(function(data) { - container.appendChild(heading); - container.innerHTML += data; + var sorted_keys = Object.keys(player_data).sort(); + for(var i=0; i < sorted_keys.length; i++) { + var key = sorted_keys[i], + data = player_data[key], + line = container.querySelector('li[data-property="' + key + '"]'); - }).fail(function(data) { - var content = document.createElement('div'); - content.className = 'chat-menu-content menu-side-padding'; - content.textContent = 'There was an error loading the announcements from the server.'; + if ( ! line ) { + var desc = NICE_DESCRIPTION.hasOwnProperty(key) ? NICE_DESCRIPTION[key] : key; + if ( ! desc ) + continue; - container.appendChild(heading); - container.appendChild(content); - }); - } + line = createElement('li'); + line.setAttribute('data-property', key); + line.innerHTML = desc + ''; + container.appendChild(line); + } + + line.querySelector('span').textContent = data; + } }; FFZ.menu_pages.about = { - name: "About", - icon: constants.HEART, - sort_order: 100000, + 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; + pages: { + about: { + name: "About", + 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; - } + 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 = ''; + // Heading + var heading = createElement('div'), + content = ''; - content += "

FrankerFaceZ

"; - content += '
new ways to woof
'; + content += "

FrankerFaceZ

"; + content += '
new ways to woof
'; - heading.className = 'chat-menu-content center'; - heading.innerHTML = content; - container.appendChild(heading); + 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); - }); + 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); + }); - // Button Stuff - var btn_container = document.createElement('div'), - ad_button = document.createElement('a'), - news_button = document.createElement('a'), - donate_button = document.createElement('a'), - message = "To use custom emoticons in " + (has_emotes ? "this channel" : "tons of channels") + ", get FrankerFaceZ from http://www.frankerfacez.com"; + // Button Stuff + var btn_container = createElement('div'), + ad_button = createElement('a'), + news_button = createElement('a'), + donate_button = createElement('a'), + message = "To use custom emoticons in " + (has_emotes ? "this channel" : "tons of channels") + ", get FrankerFaceZ from https://www.frankerfacez.com"; - // News - /*news_button.className = 'button ffz-news'; - news_button.innerHTML = 'Announcements and News'; - news_button.addEventListener('click', function() { - f._ui_change_page(view, inner, menu, container, 'about_news'); - }); + // Advertising - btn_container.appendChild(news_button); - btn_container.className = 'chat-menu-content center'; - container.appendChild(btn_container); - btn_container = document.createElement('div');*/ + 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 + + 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); - // Advertising + // Credits + var credits = createElement('div'); - ad_button.className = 'button primary'; - ad_button.innerHTML = "Advertise in Chat"; - ad_button.addEventListener('click', this._add_emote.bind(this, view, message)); + content = ''; + content += ''; + content += ''; + content += ''; - btn_container.appendChild(ad_button); + content += ''; - // Donate + credits.className = 'chat-menu-content center'; + credits.innerHTML = content; - donate_button.className = 'button ffz-donate'; - donate_button.href = "https://www.frankerfacez.com/donate"; - donate_button.target = "_new"; - donate_button.innerHTML = "Donate"; + // Make the Version clickable. + credits.querySelector('#ffz-changelog').addEventListener('click', + f._ui_change_subpage.bind(f, view, inner, menu, container, 'changelog')); - btn_container.appendChild(donate_button); - btn_container.className = 'chat-menu-content center'; - container.appendChild(btn_container); + // 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); + } + }, + + changelog: { + name: "Changelog", + wide: true, + render: include_html("change log", constants.SERVER + "script/changelog.html") + }, + + /*news: { + name: "News", + wide: true, + render: function(view, container) { + if ( this._has_news ) { + this._has_news = false; + localStorage.ffzLastNewsId = this._news_id; + this.update_ui_link(); + } + + return render_news.call(this, view, container); + } + },*/ + + credits: { + name: "Credits", + wide: true, + render: include_html("credits", constants.SERVER + "script/credits.html") + }, + + debugging: { + name: "Debug", + wide: true, + render: function(view, container) { + // Heading + var heading = createElement('div'), + + info_head = createElement('div'), + info = createElement('ul'), + info_list = [ + ['Client ID', localStorage.ffzClientId || 'not set'], + ['Socket Server', this._ws_sock && this._ws_sock.url || 'disconnected' ], + ['Server Ping', this._ws_last_ping || 'unknown'], + ['Time Offset', this._ws_sock && this._ws_server_offset && (this._ws_server_offset < 0 ? "-" : "") + utils.time_to_string(Math.abs(this._ws_server_offset) / 1000) || 'unknown'] + ], + + twitch_head = createElement('div'), + twitch = createElement('ul'), + twitch_list = [ + ['Deploy Flavor', SiteOptions.deploy_flavor] + ], + + player_head = createElement('div'), + player_list = createElement('ul'), + + pkeys = Object.keys(this.players), + player = pkeys.length && this.players[pkeys[0]] && this.players[pkeys[0]].ffz_player, + player_data, + + ver_head = createElement('div'), + vers = createElement('ul'), + version_list = [ + ['Ember', Ember.VERSION], + ['GIT Version', EmberENV.GIT_VERSION], + null, + ['FrankerFaceZ', FFZ.version_info.toString()] + ], + + log_head = createElement('div'), + logs = createElement('pre'); + + if ( player ) { + try { + player_data = player.getVideoInfo(); + } catch(err) { } + } + + heading.className = 'chat-menu-content center'; + heading.innerHTML = '

FrankerFaceZ

woofs for nerds
'; + + info_head.className = twitch_head.className = player_head.className = ver_head.className = log_head.className = 'list-header'; + info.className = twitch.className = player_list.className = vers.className = 'chat-menu-content menu-side-padding version-list'; - // Credits - var credits = document.createElement('div'); + info_head.innerHTML = 'Client Status'; - content = '
Developers
Dan Salvato  
Stendec  
Version ' + FFZ.version_info + 'Logs
'; - content += ''; - content += ''; - content += ''; + for(var i=0; i < info_list.length; i++) { + var data = info_list[i], + line = createElement('li'); + line.innerHTML = data === null ? '
' : data[0] + '' + data[1] + ''; + info.appendChild(line); + } - content += ''; - credits.className = 'chat-menu-content center'; - credits.innerHTML = content; + twitch_head.innerHTML = 'Twitch Configuration'; - // Functional Changelog - credits.querySelector('#ffz-changelog').addEventListener('click', function() { - f._ui_change_page(view, inner, menu, container, 'about_changelog'); - }); + // Check for Twitch geo-location + var user = this.get_user(); + if ( user && user.login ) { + twitch_list.push(["Current User", user.login + " [" + user.id + "]"]); + var us = []; - // Make the Logs button functional. - var getting_logs = false; - credits.querySelector('#ffz-debug-logs').addEventListener('click', function() { - if ( getting_logs ) - return; + user.is_staff && us.push("staff"); + user.is_admin && us.push("admin"); + user.is_partner && us.push("partner"); + user.is_broadcaster && us.push("broadcaster"); + user.has_turbo && us.push("turbo"); - 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); - }); - }); + twitch_list.push(["User State", us.join(", ") || "none"]); - container.appendChild(credits); - } + } else + twitch_list.push(["Current User", "not logged in"]); + + if ( window.Twitch && Twitch.geo && Twitch.geo._result ) { + var data = Twitch.geo._result; + if ( data.geo ) + twitch_list.push(["Region", data.geo + (data.eu ? " [EU]" : "")]); + + if ( data.received_language ) + twitch_list.push(["Received Language", data.received_language]) + } + + for(var i=0; i < twitch_list.length; i++) { + var data = twitch_list[i], + line = createElement('li'); + line.innerHTML = data === null ? '
' : data[0] + '' + data[1] + ''; + twitch.appendChild(line); + } + + + if ( player_data ) { + player_head.innerHTML = "Player Statistics"; + update_player_stats(player, player_list); + } + + + ver_head.innerHTML = 'Versions'; + + if ( this.has_bttv ) + version_list.push(["BetterTTV", BetterTTV.info.version + 'r' + BetterTTV.info.release]); + + if ( Object.keys(this._apis).length ) { + version_list.push(null); + for(var key in this._apis) { + var api = this._apis[key]; + version_list.push(['Ext #' + api.id + '. ' + api.name, api.version || 'unknown']); + } + } + + for(var i=0; i < version_list.length; i++) { + var data = version_list[i], + line = createElement('li'); + line.innerHTML = data === null ? '
' : data[0] + '' + data[1] + ''; + vers.appendChild(line); + } + + log_head.className = 'list-header'; + log_head.innerHTML = 'Logs'; + + logs.className = 'chat-menu-content menu-side-padding'; + logs.textContent = this._log_data.join("\n"); + + container.appendChild(heading); + + container.appendChild(ver_head); + container.appendChild(vers); + + container.appendChild(info_head); + container.appendChild(info); + + container.appendChild(twitch_head); + container.appendChild(twitch); + + if ( player_data ) { + container.appendChild(player_head); + container.appendChild(player_list); + } + + container.appendChild(log_head); + container.appendChild(logs); + } + } + } } \ No newline at end of file diff --git a/src/ui/channel_stats.js b/src/ui/channel_stats.js new file mode 100644 index 00000000..6e859bbe --- /dev/null +++ b/src/ui/channel_stats.js @@ -0,0 +1,2 @@ +var FFZ = window.FrankerFaceZ; + diff --git a/src/ui/dark.js b/src/ui/dark.js index 9d4dcf1f..c556702e 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -88,7 +88,7 @@ FFZ.basic_settings.keywords = { help: "Set additional keywords that will be highlighted in chat.", method: function() { - FFZ.settings_info.keywords.method.bind(this)(); + FFZ.settings_info.keywords.method.call(this); } }; @@ -102,7 +102,7 @@ FFZ.basic_settings.banned_words = { 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.banned_words.method.call(this); } }; @@ -137,7 +137,7 @@ FFZ.settings_info.dark_twitch = { document.body.classList.toggle("ffz-dark", val); var Settings = window.App && App.__container__.lookup('controller:settings'), - settings = Settings.get('settings'); + settings = Settings && Settings.get('settings'); if ( val ) { this._load_dark_css(); @@ -146,8 +146,9 @@ FFZ.settings_info.dark_twitch = { } else settings && settings.set('darkMode', this.settings.twitch_chat_dark); - // Try coloring ReChat - jQuery('.rechat-chat-line').parents('.chat-container').toggleClass('dark', val || this.settings.twitch_chat_dark); + // Try coloring chat replay + jQuery('.chatReplay').toggleClass('dark', val || false); + //jQuery('.rechat-chat-line').parents('.chat-container').toggleClass('dark', val || this.settings.twitch_chat_dark); } }; diff --git a/src/ui/dash_stats.js b/src/ui/dash_stats.js new file mode 100644 index 00000000..27bef0a3 --- /dev/null +++ b/src/ui/dash_stats.js @@ -0,0 +1,271 @@ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'); + + +// ------------------- +// Settings +// ------------------- + +FFZ.settings_info.dashboard_graph = { + type: "boolean", + value: true, + + no_mobile: true, + no_bttv: true, + + category: "Dashboard", + name: "Statistics Graph (Requires Refresh)", + help: "Display a graph of your viewers, followers, and chat activity over time." +} + + +// ------------------- +// Collecting Chat +// ------------------- + +FFZ.msg_commands.chat_message = function(data) { + if ( ! this.dashboard_channel || data.room !== this.dashboard_channel ) + return; + + this._stat_chat_lines++; + if ( this._stat_chatters.indexOf(data.from) === -1 ) + this._stat_chatters.push(data.from); +} + + +// ------------------- +// Initialization +// ------------------- + +FFZ.prototype.setup_dash_stats = function() { + this._stat_chat_lines = 0; + this._stat_chatters = []; + this._stat_last_game = null; + this._stat_last_status = null; + + this._update_dash_stats_timer = setTimeout(this.update_dash_stats.bind(this), 0); + + var f = this, + stats = document.querySelector('#stats'); + + if ( this.has_bttv || ! stats || ! window.Highcharts || ! this.settings.dashboard_graph ) + return; + + f.log("Adding dashboard statistics chart."); + + // Build a chart, under stats. + var container = document.createElement('div'); + container.id = "chart_container"; + container.className = 'ffz-stat-chart'; + stats.parentElement.insertBefore(container, stats.nextSibling); + + Highcharts.setOptions({global: {useUTC: false}}); + + // Load chart visibility + var vis = {}; + if ( localStorage.ffz_dash_chart_visibility ) + vis = JSON.parse(localStorage.ffz_dash_chart_visibility); + + var date_format = this.settings.twenty_four_timestamps ? "%H:%M" : "%l:%M", + chart = this._dash_chart = new Highcharts.Chart({ + chart: { + type: 'line', + zoomType: "x", + animation: false, + renderTo: container, + height: 200 + }, + + title: { text: null }, + credits: { enabled: false }, + exporting: { enabled: false }, + legend: { + backgroundColor: "#fff" + }, + + xAxis: { + type: 'datetime', + tickPixelInterval: 150, + dateTimeLabelFormats: { + millisecond: date_format, + second: date_format, + minute: date_format + } + }, + + tooltip: { + formatter: function() { + if ( this.point ) + this.points = [this.point]; + + var s = [], + key = this.points[0].key || this.points[0].x; + + if ( key ) { + if ( typeof key === "number" ) + key = Highcharts.dateFormat((f.settings.twenty_four_timestamps ? "%H:%M" : "%l:%M %P"), key); + + s.push('' + key + ''); + } + + for(var i=0; i < this.points.length; i++) { + var point = this.points[i], + series = point.series, + to = series.tooltipOptions, + y = point.text || point.y; + + if ( ! to || ! to.enabled || y === undefined || y === null ) + continue; + + if ( typeof y === "number" ) + y = utils.number_commas(y); + + s.push('' + series.name + ': ' + y + ''); + } + + return s.join("
"); + }, + crosshairs: true, + shared: true + }, + + yAxis: [ + {title: { text: null }, min: 0}, + {title: { text: null }, min: 0}, + {title: { text: null }, min: 0, opposite: true}, + {title: { text: null }, min: 0, opposite: true} + ], + + series: [ + { + type: 'flags', + name: 'Status', + showInLegend: false, + shape: 'squarepin', + data: [], + zIndex: 5 + }, + {name: "Viewers", data: [], zIndex: 4, visible: vis.hasOwnProperty('viewers')?vis.viewers:true}, + {name: "Followers", data: [], yAxis: 1, zIndex: 3, visible: vis.hasOwnProperty('followers')?vis.followers:true}, + {name: "Subscribers", data: [], zIndex: 3, showInLegend: false, visible: vis.hasOwnProperty('subscribers')?vis.subscribers:true}, + {name: "Chat Lines", type: 'area', data: [], yAxis: 2, visible: vis.hasOwnProperty('chat_lines')?vis.chat_lines:false, zIndex: 1}, + {name: "Chatters", data: [], yAxis: 3, visible: vis.hasOwnProperty('chatters')?vis.chatters:false, zIndex: 2} + ] + }); +} + + +FFZ.prototype._dash_chart_chatters = function(force) { + var now = utils.last_minute(), + series = this._dash_chart.series[4], + len = series.data.length, + last_point = len > 0 && series.data[len-1], + rendered = false; + + if ( ! force && ! this._stat_chat_lines && len > 0 && last_point.y === 0 ) { + series.addPoint({x: now, y: null}, false); + this._dash_chart.series[5].addPoint({x: now, y: null}, false); + rendered = true; + + } else if ( force || this._stat_chat_lines || len > 0 && last_point && last_point.y !== null ) { + if ( this._stat_chat_lines !== 0 && last_point && last_point.y === null ) { + series.addPoint({x:now-60000, y: 0}, false); + this._dash_chart.series[5].addPoint({x:now-60000, y: 0}, false); + } + + series.addPoint({x: now, y: this._stat_chat_lines || 0}, false); + this._dash_chart.series[5].addPoint({x: now, y: this._stat_chatters.length || 0}, false); + rendered = true; + } + + this._stat_chat_lines = 0; + this._stat_chatters = []; + return rendered; +} + + +FFZ.prototype._remove_dash_chart = function() { + if ( ! this._dash_chart ) + return; + + this.log("Removing dashboard statistics chart."); + + this._dash_chart.destroy(); + this._dash_chart = null; + jQuery("#chart_container.ffz-stat-chart").remove(); +} + + + +FFZ.prototype.update_dash_stats = function() { + var f = this, + id = this.dashboard_channel; + + if ( this.has_bttv || ! id ) + return this._remove_dash_chart(); + + this._update_dash_stats_timer = setTimeout(this.update_dash_stats.bind(this), 60000); + + if ( f._dash_chart ) { + var series = f._dash_chart.series; + localStorage.ffz_dash_chart_visibility = JSON.stringify({ + viewers: series[1].visble, + followers: series[2].visible, + subscribers: series[3].visible, + chat_lines: series[4].visible, + chatters: series[5].visible + }); + } + + utils.api.get("streams/" + id , {}, {version: 3}) + .done(function(data) { + var viewers = null, + followers = null, + game = null, + status = null; + + if ( ! data || ! data.stream ) + !f.has_bttv && jQuery("#channel_viewer_count").text("Offline"); + + else { + !f.has_bttv && jQuery("#channel_viewer_count").text(utils.number_commas(data.stream.viewers)); + viewers = data.stream.viewers; + + var chan = data.stream.channel; + if ( chan ) { + followers = chan.hasOwnProperty('followers') ? chan.followers || 0 : null; + game = chan.game || "Not Playing"; + status = chan.status || "Untitled Broadcast"; + + if ( chan.views ) + jQuery("#views_count span").text(utils.number_commas(chan.views)); + if ( followers ) + jQuery("#followers_count span").text(utils.number_commas(followers)); + } + } + + if ( f._dash_chart ) { + var now = utils.last_minute(), + force_draw; + + // If the game or status changed, we need to insert a pin. + if ( f._stat_last_game !== game || f._stat_last_status !== status ) { + f._dash_chart.series[0].addPoint({x: now, title: game, text: status}, false); + f._stat_last_game = game; + f._stat_last_status = status; + } + + force_draw = utils.maybe_chart(f._dash_chart.series[1], {x: now, y: viewers}, false); + force_draw = f._dash_chart_chatters(force_draw) || force_draw; + + utils.maybe_chart(f._dash_chart.series[2], {x: now, y: followers}, false, force_draw); + + f._dash_chart.redraw(); + } + }).fail(function() { + if ( f._dash_chart ) { + f._dash_chart_chatters(); + f._dash_chart.redraw(); + } + }); +} \ No newline at end of file diff --git a/src/ui/following-count.js b/src/ui/following-count.js index e8891f1d..dc975285 100644 --- a/src/ui/following-count.js +++ b/src/ui/following-count.js @@ -70,6 +70,12 @@ FFZ.prototype.setup_following_count = function(has_ember) { Live.load(); + /*var Host = window.App && App.__container__.resolve('model:host'), + HostLive = Host && Host.find("following"); + + if ( HostLive ) + HostLive.load();*/ + var total = Live.get('total'), streams = Live.get('content'); if ( typeof total === "number" ) { @@ -87,7 +93,7 @@ FFZ.prototype._following_get_me = function(tries) { return setTimeout(this._following_get_me.bind(this, tries), Math.floor(2000*Math.random()) + 500); var f = this; - Twitch.api.get("/api/me").done(function(data) { + utils.api.get("/api/me").done(function(data) { f.log("Fetched User Data -- " + (data.name || data.login)); f.__user = data; f._update_following_count(); @@ -128,17 +134,21 @@ FFZ.prototype._update_following_count = function() { var Stream = window.App && App.__container__.resolve('model:stream'), Live = Stream && Stream.find("live"), + + Host = window.App && App.__container__.resolve('model:host'), + HostLive = Host && Host.find("following"), + f = this; + if ( HostLive && document.body.getAttribute('data-current-path').indexOf('directory.following') !== -1 ) + HostLive.load(); + if ( Live ) Live.load(); else { - var a = {}, - u = this.get_user(); + var u = this.get_user(); - a.Authorization = "OAuth " + u.chat_oauth_token; - - Twitch.api && Twitch.api.get("streams/followed", {limit:20, offset:0}, {version:3, headers: a}) + utils.api.get("streams/followed", {limit:20, offset:0}, {version:3}, u && u.chat_oauth_token) .done(function(data) { f._draw_following_count(data._total); f._draw_following_channels(data.streams, data._total); @@ -159,15 +169,17 @@ FFZ.prototype._build_following_tooltip = function(el) { 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), + height = document.body.clientHeight - (bb.bottom + 50), + max_lines = Math.max(Math.floor(height / 40) - 1, 2), + + /*Host = window.App && App.__container__.resolve('model:host'), + HostLive = Host && Host.find("following"),*/ streams = this._tooltip_streams, - total = this._tooltip_total || (streams && streams.length) || 0; - + total = this._tooltip_total || (streams && streams.length) || 0, + c = 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 ) @@ -189,11 +201,43 @@ FFZ.prototype._build_following_tooltip = function(el) { (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') + ''; + '' + (stream.channel.game === 'Creative' ? 'Being Creative' : (stream.channel.game ? 'Playing ' + utils.sanitize(stream.channel.game) : 'Not Playing')) + ''; } - } else + } else { + c++; // is a terrible programming language tooltip += "
No one you're following is online."; + } + // If we have hosts, and room, try displaying some hosts. + /*if ( HostLive && (c + 1) < max_lines && HostLive.get('content.length') > 0 ) { + var t = HostLive.get('content.length'); + c++; + tooltip += '
Live Hosts'; + for(var i=0; i < t; i++) { + var host = HostLive.get('content.' + i), + stream = host && host.target; + if ( ! stream ) + continue; + + c += 1; + if ( c > max_lines ) { + var sc = 1 + (streams && streams.length || 0); + tooltip += '
And ' + utils.number_commas(t - (max_lines - sc)) + ' more...'; + break; + } + + var hosting; + if ( ! host.ffz_hosts || host.ffz_hosts.length === 1 ) + hosting = host.display_name; + else + hosting = utils.number_commas(host.ffz_hosts.length); + + tooltip += (i === 0 ? '
' : '') + + '' + constants.LIVE + ' ' + utils.number_commas(stream.viewers) + '' + + hosting + ' hosting ' + utils.sanitize(stream.channel.display_name || stream.channel.name) + '
' + + '' + (stream.meta_game ? 'Playing ' + utils.sanitize(stream.meta_game) : 'Not Playing') + ''; + } + }*/ // Reposition the tooltip. setTimeout(function() { diff --git a/src/ui/following.js b/src/ui/following.js index 0207981c..d12df391 100644 --- a/src/ui/following.js +++ b/src/ui/following.js @@ -113,7 +113,7 @@ FFZ.ws_on_close.push(function() { FFZ.ws_commands.follow_buttons = function(data) { var controller = window.App && App.__container__.lookup('controller:channel'), - current_id = controller && controller.get('id'), + current_id = controller && controller.get('content.id'), current_host = controller && controller.get('hostModeTarget.id'), need_update = false; @@ -132,7 +132,7 @@ FFZ.ws_commands.follow_buttons = function(data) { FFZ.ws_commands.follow_sets = function(data) { var controller = App.__container__.lookup('controller:channel'), - current_id = controller && controller.get('id'), + current_id = controller && controller.get('content.id'), current_host = controller && controller.get('hostModeTarget.id'), need_update = false, f = this; @@ -189,7 +189,7 @@ FFZ.ws_commands.follow_sets = function(data) { FFZ.prototype.rebuild_following_ui = function() { var controller = App.__container__.lookup('controller:channel'), - channel_id = controller && controller.get('id'), + channel_id = controller && controller.get('content.id'), hosted_id = controller && controller.get('hostModeTarget.id'); if ( ! this._cindex ) @@ -307,7 +307,7 @@ FFZ.prototype._build_following_button = function(container, channel_id) { return update(); } - Twitch.api.get("users/" + user.login + "/follows/channels/" + channel_id) + utils.api.get("users/" + user.login + "/follows/channels/" + channel_id) .done(function(data) { following = true; notifications = data.notifications; @@ -330,7 +330,7 @@ FFZ.prototype._build_following_button = function(container, channel_id) { return null; notifications = notice; - return Twitch.api.put("users/:login/follows/channels/" + channel_id, {notifications: notifications}) + return utils.api.put("users/:login/follows/channels/" + channel_id, {notifications: notifications}) .fail(check_following); }, @@ -367,7 +367,7 @@ FFZ.prototype._build_following_button = function(container, channel_id) { if ( following ) do_follow() else - Twitch.api.del("users/:login/follows/channels/" + channel_id) + utils.api.del("users/:login/follows/channels/" + channel_id) .done(check_following); return false; diff --git a/src/ui/menu.js b/src/ui/menu.js index ef2a6dcc..3f92e94f 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -5,9 +5,9 @@ var FFZ = window.FrankerFaceZ, fix_menu_position = function(container) { var swapped = document.body.classList.contains('ffz-sidebar-swap') && ! document.body.classList.contains('ffz-portrait'); - var bounds = container.getBoundingClientRect(), + var bounds = container.children[0].getBoundingClientRect(), left = parseInt(container.style.left || '0'), - right = bounds.left + container.scrollWidth, + right = bounds.left + bounds.width, moved = !!container.style.left; if ( swapped ) { @@ -217,7 +217,7 @@ FFZ.prototype.build_ui_popup = function(view) { // Stuff jQuery(inner).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 's')}); - + jQuery(inner).find('.ffz-tooltip').tipsy({live: true, html: true, title: this.render_tooltip(), gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); // Menu Container var sub_container = document.createElement('div'); @@ -270,7 +270,7 @@ FFZ.prototype.build_ui_popup = function(view) { var page = FFZ.menu_pages[key]; try { - if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.bind(this)(view)))) ) + if ( !page || (page.hasOwnProperty("visible") && (!page.visible || (typeof page.visible == "function" && !page.visible.call(this, view)))) ) continue; } catch(err) { this.error("menu_pages " + key + " visible: " + err); @@ -298,7 +298,7 @@ FFZ.prototype.build_ui_popup = function(view) { el = document.createElement('li'), link = document.createElement('a'); - el.className = 'item' + (page.sub_menu ? ' has-sub-menu' : ''); + el.className = 'item' + (page.sub_menu || page.pages ? ' has-sub-menu' : ''); el.id = "ffz-menu-page-" + key; link.title = page.name; link.innerHTML = page.icon; @@ -315,12 +315,12 @@ FFZ.prototype.build_ui_popup = function(view) { var page = (this._last_page || "channel").split("_", 1)[0]; // Do we have news? - if ( this._has_news ) { + /*if ( this._has_news ) { // Render news, then set the page back so our default doesn't change. this._ui_change_page(view, inner, menu, sub_container, 'about_news'); this._last_page = page; - } else + } else*/ // Render Current Page this._ui_change_page(view, inner, menu, sub_container, page); @@ -335,26 +335,144 @@ FFZ.prototype.build_ui_popup = function(view) { } +FFZ.prototype._ui_change_subpage = function(view, inner, menu, container, subpage) { + var page = this._last_page, + last_subpages = this._last_subpage = this._last_subpage || {}; + + last_subpages[page] = subpage; + + container.innerHTML = ""; + container.setAttribute('data-page', subpage); + + // Get the data structure for this page. + var page_data = FFZ.menu_pages[page], + data = page_data.pages[subpage]; + + // Render the page first. If there's an error, it won't update the other UI stuff. + data.render.call(this, view, container, inner, menu); + + // Make sure the correct menu tab is selected + jQuery('li.active', menu).removeClass('active'); + jQuery('#ffz-menu-page-' + page + '-subpage-' + subpage, menu).addClass('active'); + + // Apply wideness - TODO: Revamp wide menus entirely for thin containers + var is_wide = false, + app = document.querySelector(".app-main") || document.querySelector(".ember-chat-container"); + + if ( data.hasOwnProperty('wide') ) + is_wide = data.wide || (typeof data.wide === "function" && data.wide.call(this)); + else if ( page_data.hasOwnProperty('wide') ) + is_wide = page_data.wide || (typeof page_data.wide === "function" && page_data.wide.call(this)); + + inner.style.maxWidth = is_wide ? (app.offsetWidth < 640 ? (app.offsetWidth-40) : 600) + "px" : ""; + + // Re-position if necessary. + var f = this; + setTimeout(function(){f._fix_menu_position();}); +} + + 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"; + // Get the data structure for the new page. + var data = FFZ.menu_pages[page]; - var els = menu.querySelectorAll('li.active'); - for(var i=0; i < els.length; i++) - els[i].classList.remove('active'); + // See if we're dealing with a sub-menu situation. + if ( data.pages ) { + // We need to render the sub-menu, and then call the _ui_change_sub_page method + // to render the sub-page. + var submenu = document.createElement('ul'), + subcontainer = document.createElement('div'), - var el = menu.querySelector('#ffz-menu-page-' + page); - if ( el ) - el.classList.add('active'); - else - this.log("No matching page: " + page); + height = parseInt(container.style.maxHeight || '0'); - FFZ.menu_pages[page].render.bind(this)(view, container, inner, menu); + if ( ! height ) + height = Math.max(200, view.$().height() - 172); + + if ( height && ! Number.isNaN(height) ) { + height -= 37; + subcontainer.style.maxHeight = height + 'px'; + } + + submenu.className = 'menu sub-menu clearfix'; + subcontainer.className = 'ffz-ui-sub-menu-page'; + + // Building Tabs + var subpages = []; + for(var key in data.pages) { + var subpage = data.pages[key]; + try { + if ( ! subpage || (subpage.hasOwnProperty("visible") && (!subpage.visible || (typeof subpage.visible === "function" && !subpage.visible.call(this, view)))) ) + continue; + } catch(err) { + this.error("menu_pages " + page + " subpage " + key + " visible: " + err); + continue; + } + + subpages.push([subpage.sort_order || 0, key, subpage]); + } + + subpages.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 < subpages.length; i++) { + var key = subpages[i][1], + subpage = subpages[i][2], + tab = document.createElement('li'), + link = document.createElement('a'); + + tab.className = 'item'; + tab.id = 'ffz-menu-page-' + page + '-subpage-' + key; + link.innerHTML = subpage.name; + link.addEventListener('click', this._ui_change_subpage.bind(this, view, inner, submenu, subcontainer, key)); + + tab.appendChild(link); + submenu.appendChild(tab); + } + + // Activate a Tab + var last_subpages = this._last_subpage = this._last_subpage || {}, + last_subpage = last_subpages[page] = last_subpages[page] || data.default_page || subpages[0][1]; + + if ( typeof last_subpage === "function" ) + last_subpage = last_subpage.call(this); + + this._ui_change_subpage(view, inner, submenu, subcontainer, last_subpage); + + // Make sure the correct menu tab is selected + jQuery('li.active', menu).removeClass('active'); + jQuery('#ffz-menu-page-' + page, menu).addClass('active'); + + // Add this to the container. + container.appendChild(subcontainer); + container.appendChild(submenu); + return; + } + + // Render the page first. If there's an error, it won't update the other UI stuff. + data.render.call(this, view, container, inner, menu); + + // Make sure the correct menu tab is selected + jQuery('li.active', menu).removeClass('active'); + jQuery('#ffz-menu-page-' + page, menu).addClass('active'); + + // Apply wideness - TODO: Revamp wide menus entirely for thin containers + var is_wide = data.wide || (typeof data.wide === "function" && data.wide.call(this)), + app = document.querySelector(".app-main") || document.querySelector(".ember-chat-container"); + + inner.style.maxWidth = is_wide ? (app.offsetWidth < 640 ? (app.offsetWidth-40) : 600) + "px" : ""; // Re-position if necessary. var f = this; @@ -430,7 +548,16 @@ FFZ.menu_pages.channel = { can_use = is_subscribed || !emote.subscriber_only, img_set = 'image-set(url("' + constants.TWITCH_BASE + emote.id + '/1.0") 1x, url("' + constants.TWITCH_BASE + emote.id + '/2.0") 2x), url("' + constants.TWITCH_BASE + emote.id + '/3.0") 4x)'; - s.className = 'emoticon html-tooltip' + (!can_use ? " locked" : ""); + s.className = 'emoticon ffz-tooltip ffz-tooltip-no-credit' + (!can_use ? " locked" : ""); + + if ( emote.emoticon_set ) { + var favs = this.settings.favorite_emotes["twitch-" + emote.emoticon_set]; + s.classList.add('ffz-can-favorite'); + s.classList.toggle('ffz-favorite', favs && favs.indexOf(emote.id) !== -1); + } + + s.setAttribute('data-emote', emote.id); + s.alt = emote.regex; s.style.backgroundImage = 'url("' + constants.TWITCH_BASE + emote.id + '/1.0")'; s.style.backgroundImage = '-webkit-' + img_set; @@ -438,19 +565,18 @@ FFZ.menu_pages.channel = { 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.settings.emote_image_hover ? '' : '') + emote.regex; + s.style.width = (10+emote.width) + "px"; + s.style.height = (10+emote.height) + "px"; - s.addEventListener('click', function(can_use, id, code, e) { + s.addEventListener('click', function(can_use, id, code, emote_set, 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); + this._add_emote(view, code, "twitch-" + emote_set, id, e); else return; e.preventDefault(); - }.bind(this, can_use, emote.id, emote.regex)); + }.bind(this, can_use, emote.id, emote.regex, emote.emoticon_set)); grid.appendChild(s); c++; @@ -507,7 +633,7 @@ FFZ.menu_pages.channel = { var extra_sets = _.union(room && room.extra_sets || [], room && room.ext_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"); + this._emotes_for_sets(inner, view, room && room.set && [room.set] || [], (this.feature_friday || has_product || extra_sets.length ) ? "Channel Emoticons" : null, (room && room.moderator_badge) || "//cdn.frankerfacez.com/script/devicon.png", "FrankerFaceZ"); for(var i=0; i < extra_sets.length; i++) { // Look up the set name. @@ -593,7 +719,11 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub c++; var s = document.createElement('span'); - s.className = 'emoticon html-tooltip'; + s.className = 'emoticon ffz-tooltip'; + + s.setAttribute('data-ffz-emote', emote.id); + s.setAttribute('data-ffz-set', set.id); + s.style.backgroundImage = 'url("' + emote.urls[1] + '")'; if ( srcset ) { @@ -604,9 +734,8 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub s.style.backgroundImage = img_set; } - s.style.width = emote.width + "px"; - s.style.height = emote.height + "px"; - s.title = this._emote_tooltip(emote); + s.style.width = (10+emote.width) + "px"; + s.style.height = (10+emote.height) + "px"; s.addEventListener('click', function(id, code, e) { e.preventDefault(); @@ -636,7 +765,29 @@ FFZ.prototype._emotes_for_sets = function(parent, view, sets, header, image, sub } -FFZ.prototype._add_emote = function(view, emote) { +FFZ.prototype._add_emote = function(view, emote, favorites_set, favorites_key, event) { + if ( event && event.ctrlKey ) { + var el = event.target; + if ( ! el.classList.contains('locked') && el.classList.contains('ffz-can-favorite') && favorites_set && favorites_key ) { + var favs = this.settings.favorite_emotes[favorites_set] = this.settings.favorite_emotes[favorites_set] || [], + is_favorited = favs.indexOf(favorites_key) !== -1; + + if ( is_favorited ) + favs.removeObject(favorites_key); + else + favs.push(favorites_key); + + this.settings.set("favorite_emotes", this.settings.favorite_emotes, true); + + if ( el.classList.contains('ffz-is-favorite') && is_favorited ) { + jQuery(el).trigger('mouseout'); + el.parentElement.removeChild(el); + } else + el.classList.toggle('ffz-favorite', ! is_favorited); + } + return; + } + var input_el, text, room; if ( this.has_bttv ) { diff --git a/src/ui/menu_button.js b/src/ui/menu_button.js index 69514850..ecb744b5 100644 --- a/src/ui/menu_button.js +++ b/src/ui/menu_button.js @@ -43,5 +43,5 @@ FFZ.prototype.update_ui_link = function(link) { link.classList.toggle('live', live); link.classList.toggle('dark', dark); link.classList.toggle('blue', blue); - link.classList.toggle('news', this._has_news); + //link.classList.toggle('news', this._has_news); } \ No newline at end of file diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js index 36954713..079ba369 100644 --- a/src/ui/my_emotes.js +++ b/src/ui/my_emotes.js @@ -56,7 +56,7 @@ FFZ.settings_info.global_emotes_in_menu = { FFZ.settings_info.emoji_in_menu = { type: "boolean", - value: false, + value: true, category: "Chat Input", @@ -66,11 +66,18 @@ FFZ.settings_info.emoji_in_menu = { FFZ.settings_info.emote_menu_collapsed = { + storage_key: "ffz_setting_my_emoticons_collapsed_sections", value: [], visible: false } +FFZ.settings_info.favorite_emotes = { + value: {}, + visible: false +} + + FFZ.prototype.setup_my_emotes = function() { this._twitch_badges = {}; this._twitch_badges["--global--"] = "//cdn.frankerfacez.com/script/twitch_logo.png"; @@ -100,47 +107,208 @@ FFZ.menu_pages.myemotes = { return ffz_sets.length || (sk && sk.length) || this.settings.emoji_in_menu; }, - render: function(view, container) { - var tmi = view.get('controller.currentRoom.tmiSession'), - twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets']; + default_page: function() { + for(var key in this.settings.favorite_emotes) + if ( this.settings.favorite_emotes[key] && this.settings.favorite_emotes[key].length ) + return 'favorites'; - // 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); + return 'all'; + }, + + pages: { + favorites: { + name: "Favorites", + sort_order: 1, + + render: function(view, container) { + FFZ.menu_pages.myemotes.render_lists.call(this, view, container, true); + + var el = document.createElement("div"); + el.className = "emoticon-grid ffz-no-emotes center"; + el.innerHTML = "You have no favorite emoticons.

To make an emote a favorite, find it on the All Emoticons tab and Ctrl-Click it."; + container.appendChild(el); + } + }, + + all: { + name: "All Emoticons", + sort_order: 2, + + render: function(view, container) { + FFZ.menu_pages.myemotes.render_lists.call(this, view, container, false); + } + }, + + emoji: { + name: "Emoji", + sort_order: 3, + visible: function() { return this.settings.emoji_in_menu }, + + render: function(view, container) { + var sets = []; + + for(var cat in constants.EMOJI_CATEGORIES) { + var menu = FFZ.menu_pages.myemotes.draw_emoji.call(this, view, cat, false); + if ( menu ) + sets.push([cat, menu]); + } + + sets.sort(function(a,b) { + var an = a[0], bn = b[0]; + 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]); + } + } + }, + + render_lists: function(view, container, favorites_only) { + var tmi = view.get('controller.currentRoom.tmiSession'), + twitch_sets = (tmi && tmi.getEmotes() || {'emoticon_sets': {}})['emoticon_sets'], + + 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 favorites_list = this.settings.favorite_emotes["twitch-" + set_id]; + if ( favorites_only && (! favorites_list || ! favorites_list.length) ) + continue; + + var set = twitch_sets[set_id]; + if ( ! set.length ) + continue; + + var menu = FFZ.menu_pages.myemotes.draw_twitch_set.call(this, view, set_id, set, favorites_only); + if ( menu ) + sets.push([this._twitch_set_to_channel[set_id], menu]); + } + + // Emoji~! + if ( favorites_only && this.settings.emoji_in_menu ) { + var favorites_list = this.settings.favorite_emotes["emoji"]; + if ( favorites_list && favorites_list.length ) { + var menu = FFZ.menu_pages.myemotes.draw_emoji.call(this, view, null, favorites_only); + if ( menu ) + sets.push(["emoji", menu]); + } + } + + // Now, FFZ! + for(var i=0; i < ffz_sets.length; i++) { + var set_id = ffz_sets[i], + set = this.emote_sets[set_id], + + menu_id = set.hasOwnProperty('source_ext') ? 'ffz-ext-' + set.source_ext + '-' + set.source_id : 'ffz-' + set.id, + favorites_list = this.settings.favorite_emotes[menu_id]; + + if ( favorites_only && (! favorites_list || ! favorites_list.length) ) + continue; + + if ( ! set || ! set.count || ( ! this.settings.global_emotes_in_menu && this.default_sets.indexOf(set_id) !== -1 ) ) + continue; + + var menu = FFZ.menu_pages.myemotes.draw_ffz_set.call(this, view, set, favorites_only); + if ( menu ) + sets.push([set.title.toLowerCase(), menu]); + } + + + if ( ! sets.length ) + return false; + + + // 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 && an.substr(0,16) === "global emoticons") || an === "--global--" ) + an = "zzy|" + an; + else if ( an.substr(0,5) === "emoji" ) + an = "zzz|" + an; + + if ( bn === "turbo" || bn === "--turbo-faces--" ) + bn = "zza|" + bn; + else if ( bn === "global" || (bn && bn.substr(0,16) === "global emoticons") || bn === "--global--" ) + bn = "zzy|" + bn; + else if ( bn.substr(0,5) === "emoji" ) + bn = "zzz|" + bn; + + if ( an < bn ) return -1; + if ( an > bn ) return 1; + return 0; + }); + + if ( favorites_only ) { + var grid = document.createElement('div'); + grid.className = 'emoticon-grid favorites-grid'; + for(var i=0; i < sets.length; i++) + grid.appendChild(sets[i][1]); + + container.appendChild(grid); + + } else + for(var i=0; i < sets.length; i++) + container.appendChild(sets[i][1]); + + return true; }, - toggle_section: function(heading) { + toggle_section: function(heading, container) { 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; + is_collapsed = collapsed_list.indexOf(set_id) === -1; - if ( is_collapsed ) + if ( ! is_collapsed ) collapsed_list.removeObject(set_id); else collapsed_list.push(set_id); - this.settings.set('emote_menu_collapsed', collapsed_list); + this.settings.set('emote_menu_collapsed', collapsed_list, true); menu.classList.toggle('collapsed', !is_collapsed); + + if ( is_collapsed ) + menu.appendChild(container); + else + menu.removeChild(container); }, - draw_emoji: function(view) { + draw_emoji: function(view, cat, favorites_only) { var heading = document.createElement('div'), menu = document.createElement('div'), + menu_id = 'emoji' + (cat ? '-' + cat : ''), + emotes = favorites_only ? document.createDocumentFragment() : document.createElement('div'), + collapsed = ! favorites_only && this.settings.emote_menu_collapsed.indexOf(menu_id) === -1, f = this, - settings = this.settings.parse_emoji || 1; + settings = this.settings.parse_emoji || 1, + favorites = this.settings.favorite_emotes["emoji"] || [], + c = 0; - heading.className = 'heading'; - heading.innerHTML = 'UnicodeEmoji'; - heading.style.backgroundImage = 'url("' + constants.SERVER + 'emoji/' + (settings === 2 ? 'noto-' : 'tw-') + '1f4af.svg")'; - heading.style.backgroundSize = "18px"; + menu.className = 'emoticon-grid'; + menu.setAttribute('data-set', menu_id); - menu.className = 'emoticon-grid collapsable'; - menu.appendChild(heading); + if ( ! favorites_only ) { + heading.className = 'heading'; + heading.innerHTML = 'Unicode' + (cat ? utils.sanitize(constants.EMOJI_CATEGORIES[cat]) : 'Emoji'); + heading.style.backgroundImage = 'url("' + constants.SERVER + 'emoji/' + (settings === 3 ? 'one/' : (settings === 2 ? 'noto-' : 'tw/')) + (constants.EMOJI_LOGOS[cat] || '1f4af') + '.svg")'; + heading.style.backgroundSize = "18px"; - 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); }); + menu.classList.add('collapsable'); + menu.appendChild(heading); + menu.classList.toggle('collapsed', collapsed); + heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this, emotes); }); + } var set = []; @@ -162,66 +330,92 @@ FFZ.menu_pages.myemotes = { var emoji = set[i], em = document.createElement('span'); - if ( (settings === 1 && ! emoji.tw) || (settings === 2 && ! emoji.noto) ) + if ( (cat && cat !== emoji.cat) || (settings === 1 && ! emoji.tw) || (settings === 2 && ! emoji.noto) || (settings === 3 && ! emoji.one) ) continue; - var src = settings === 2 ? emoji.noto_src : emoji.tw_src, + var is_favorite = favorites.indexOf(emoji.raw) !== -1, + src = settings === 3 ? emoji.one_src : (settings === 2 ? emoji.noto_src : emoji.tw_src), image = this.settings.emote_image_hover ? '' : ''; - em.className = 'emoticon emoji html-tooltip'; - em.title = image + 'Emoji: ' + emoji.raw + '
Name: ' + emoji.name + (emoji.short_name ? '
Short Name: :' + emoji.short_name + ':' : ''); - em.addEventListener('click', this._add_emote.bind(this, view, emoji.raw)); + if ( favorites_only && ! is_favorite ) + continue; + + em.className = 'emoticon emoji ffz-tooltip ffz-can-favorite'; + em.classList.toggle('ffz-favorite', is_favorite); + em.classList.toggle('ffz-is-favorite', favorites_only); + + em.setAttribute('data-ffz-emoji', emoji.code); + em.alt = emoji.raw; + + em.addEventListener('click', this._add_emote.bind(this, view, emoji.raw, "emoji", emoji.raw)); em.style.backgroundImage = 'url("' + src + '")'; em.style.backgroundSize = "18px"; - menu.appendChild(em); + c++; + emotes.appendChild(em); } + if ( ! c ) + return; + + if ( favorites_only ) + return emotes; + + if ( ! collapsed ) + menu.appendChild(emotes); + return menu; }, - draw_twitch_set: function(view, set_id, set) { + draw_twitch_set: function(view, set_id, set, favorites_only) { var heading = document.createElement('div'), menu = document.createElement('div'), + emotes = favorites_only ? document.createDocumentFragment() : document.createElement('div'), + collapsed = ! favorites_only && this.settings.emote_menu_collapsed.indexOf('twitch-' + set_id) === -1, 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); + channel_id = this._twitch_set_to_channel[set_id], title, + favorites = this.settings.favorite_emotes["twitch-" + set_id] || [], + c = 0; + menu.className = 'emoticon-grid'; 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); }); + + if ( ! favorites_only ) { + 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; + utils.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.classList.add('collapsable'); + menu.appendChild(heading); + menu.classList.toggle('collapsed', collapsed); + heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this, emotes); }); + } set.sort(function(a,b) { var an = a.code.toLowerCase(), @@ -237,11 +431,21 @@ FFZ.menu_pages.myemotes = { for(var i=0; i < set.length; i++) { var emote = set[i], code = constants.KNOWN_CODES[emote.code] || emote.code, + is_favorite = favorites.indexOf(emote.id) !== -1; - em = document.createElement('span'), - img_set = 'image-set(url("' + constants.TWITCH_BASE + emote.id + '/1.0") 1x, url("' + constants.TWITCH_BASE + emote.id + '/2.0") 2x, url("' + constants.TWITCH_BASE + emote.id + '/3.0") 4x)'; + if ( favorites_only && ! is_favorite ) + continue; - em.className = 'emoticon html-tooltip'; + var em = document.createElement('span'), + img_set = 'image-set(url("' + constants.TWITCH_BASE + emote.id + '/1.0") 1x, url("' + constants.TWITCH_BASE + emote.id + '/2.0") 2x)'; + + em.className = 'emoticon ffz-tooltip ffz-can-favorite'; + em.setAttribute('data-emote', emote.id); + em.alt = code; + + em.classList.toggle('ffz-favorite', is_favorite); + em.classList.toggle('ffz-is-favorite', favorites_only); + em.classList.toggle('ffz-tooltip-no-credit', ! favorites_only); if ( this.settings.replace_bad_emotes && constants.EMOTE_REPLACEMENTS[emote.id] ) { em.style.backgroundImage = 'url("' + constants.EMOTE_REPLACEMENT_BASE + constants.EMOTE_REPLACEMENTS[emote.id] + '")'; @@ -250,45 +454,63 @@ FFZ.menu_pages.myemotes = { em.style.backgroundImage = '-webkit-' + img_set; em.style.backgroundImage = '-moz-' + img_set; em.style.backgroundImage = '-ms-' + img_set; - em.style.backgroudnImage = img_set; + em.style.backgroundImage = img_set; } - em.title = (this.settings.emote_image_hover ? '' : '') + code; em.addEventListener("click", function(id, c, e) { e.preventDefault(); if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) window.open("https://twitchemotes.com/emote/" + id); else - this._add_emote(view, c); + this._add_emote(view, c, "twitch-" + set_id, id, e); }.bind(this, emote.id, code)); - menu.appendChild(em); + + c++; + emotes.appendChild(em); } + if ( ! c ) + return; + + if ( favorites_only ) + return emotes; + + if ( ! collapsed ) + menu.appendChild(emotes); + return menu; }, - draw_ffz_set: function(view, set) { + draw_ffz_set: function(view, set, favorites_only) { var heading = document.createElement('div'), menu = document.createElement('div'), + emote_container = favorites_only ? document.createDocumentFragment() : document.createElement('div'), f = this, emotes = [], menu_id = set.hasOwnProperty('source_ext') ? 'ffz-ext-' + set.source_ext + '-' + set.source_id : 'ffz-' + set.id, - icon = set.icon || (set.hasOwnProperty('source_ext') && '//cdn.frankerfacez.com/emoji/tw-1f4ac.svg') || '//cdn.frankerfacez.com/script/devicon.png'; + favorites = this.settings.favorite_emotes[menu_id] || [], + c = 0, + icon = set.icon || (set.hasOwnProperty('source_ext') && '//cdn.frankerfacez.com/emoji/tw-1f4ac.svg') || '//cdn.frankerfacez.com/script/devicon.png', + collapsed = ! favorites_only && this.settings.emote_menu_collapsed.indexOf(menu_id) === -1; - heading.className = 'heading'; - heading.innerHTML = '' + (utils.sanitize(set.source) || 'FrankerFaceZ') + '' + set.title; + menu.className = 'emoticon-grid'; + menu.setAttribute('data-set', menu_id); - heading.style.backgroundImage = 'url("' + icon + '")'; - if ( icon.indexOf('.svg') !== -1 ) - heading.style.backgroundSize = "18px"; + if ( ! favorites_only ) { + menu.classList.add('collapsable'); - menu.className = 'emoticon-grid collapsable'; - menu.appendChild(heading); + heading.className = 'heading'; + heading.innerHTML = '' + (utils.sanitize(set.source) || 'FrankerFaceZ') + '' + set.title; - menu.setAttribute('data-set', menu_id); - menu.classList.toggle('collapsed', this.settings.emote_menu_collapsed.indexOf(menu_id) !== -1); - heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this); }); + heading.style.backgroundImage = 'url("' + icon + '")'; + if ( icon.indexOf('.svg') !== -1 ) + heading.style.backgroundSize = "18px"; + + menu.appendChild(heading); + menu.classList.toggle('collapsed', collapsed); + heading.addEventListener('click', function() { FFZ.menu_pages.myemotes.toggle_section.bind(f)(this, emote_container); }); + } for(var emote_id in set.emoticons) set.emoticons.hasOwnProperty(emote_id) && ! set.emoticons[emote_id].hidden && emotes.push(set.emoticons[emote_id]); @@ -306,8 +528,12 @@ FFZ.menu_pages.myemotes = { for(var i=0; i < emotes.length; i++) { var emote = emotes[i], + is_favorite = favorites.indexOf(emote.id) !== -1; - em = document.createElement('span'), + if ( favorites_only && ! is_favorite ) + continue; + + var em = document.createElement('span'), img_set = 'image-set(url("' + emote.urls[1] + '") 1x'; if ( emote.urls[2] ) @@ -318,19 +544,24 @@ FFZ.menu_pages.myemotes = { img_set += ')'; - em.className = 'emoticon html-tooltip'; + em.className = 'emoticon ffz-tooltip ffz-can-favorite'; + em.classList.toggle('ffz-favorite', is_favorite); + em.classList.toggle('ffz-is-favorite', favorites_only); + + em.setAttribute('data-ffz-emote', emote.id); + em.setAttribute('data-ffz-set', set.id); + 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; + em.style.backgroundImage = img_set; if ( emote.height ) - em.style.height = emote.height + "px"; + em.style.height = (10+emote.height) + "px"; if ( emote.width ) - em.style.width = emote.width + "px"; + em.style.width = (10+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 ) { @@ -345,97 +576,22 @@ FFZ.menu_pages.myemotes = { if ( url ) window.open(url); } else - this._add_emote(view, code); + this._add_emote(view, code, menu_id, id, e); }.bind(this, emote.id, emote.name)); - menu.appendChild(em); + + c++; + emote_container.appendChild(em); } + if ( ! c ) + return; + + if ( favorites_only ) + return emote_container; + + if ( ! collapsed ) + menu.appendChild(emote_container); + 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 && an.substr(0,16) === "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 && bn.substr(0,16) === "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); - } } }; \ No newline at end of file diff --git a/src/ui/notifications.js b/src/ui/notifications.js index e7834523..4de0df9c 100644 --- a/src/ui/notifications.js +++ b/src/ui/notifications.js @@ -1,4 +1,5 @@ -var FFZ = window.FrankerFaceZ; +var FFZ = window.FrankerFaceZ, + utils = require("../utils"); // --------------------- @@ -74,17 +75,21 @@ FFZ.settings_info.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); + var f = this; + utils.prompt( + "Notification Timeout", + "Please enter the time you'd like notifications to be displayed before automatically closing, in seconds.

Default: 60", + this.settings.notification_timeout, + function(new_val) { + if ( new_val === null || new_val === undefined ) + return; - if ( new_val === null || new_val === undefined ) - return; + new_val = parseInt(new_val); + if ( Number.isNaN(new_val) || ! Number.isFinite(new_val) || new_val < 1 ) + new_val = 60; - var parsed = parseInt(new_val); - if ( parsed === NaN || parsed < 1 ) - parsed = 60; - - this.settings.set("notification_timeout", parsed); + f.settings.set("notification_timeout", new_val); + }); } }; @@ -136,7 +141,7 @@ FFZ.prototype.show_notification = function(message, title, tag, timeout, on_clic dir: "ltr", body: message, tag: tag || "FrankerFaceZ", - icon: "http://cdn.frankerfacez.com/icon32.png" + icon: "//cdn.frankerfacez.com/icon32.png" }; var f = this, diff --git a/src/ui/popups.js b/src/ui/popups.js index 109f979d..f9831bc7 100644 --- a/src/ui/popups.js +++ b/src/ui/popups.js @@ -1,4 +1,5 @@ -var FFZ = window.FrankerFaceZ; +var FFZ = window.FrankerFaceZ, + constants = require('../constants'); // --------------- @@ -13,6 +14,11 @@ FFZ.prototype.setup_popups = function() { if ( e.button && e.button !== 0 ) return; + // Check for modal clicks + var modal = document.getElementById('ffz-modal-container'); + if ( modal && (modal === e.target || modal.contains(e.target)) ) + return; + var popup = f._popup, parent = f._popup_parent; diff --git a/src/ui/races.js b/src/ui/races.js index d59f2df7..6591bba2 100644 --- a/src/ui/races.js +++ b/src/ui/races.js @@ -304,7 +304,7 @@ FFZ.prototype._update_race = function(container, not_timer) { for(var i=0; i < entrants.length; i++) { var ent = entrants[i], name = '' + ent.display_name + '', - twitch_link = ent.channel ? '' : '', + twitch_link = ent.channel ? '' : '', hitbox_link = ent.hitbox ? '' : '', time = elapsed ? utils.time_to_string(ent.time||elapsed) : "", place = utils.place_string(ent.place), diff --git a/src/ui/sub_count.js b/src/ui/sub_count.js index e2be80a5..1bc496bf 100644 --- a/src/ui/sub_count.js +++ b/src/ui/sub_count.js @@ -31,71 +31,75 @@ FFZ.prototype._update_subscribers = function() { // context of the web user. // Get the count! - jQuery.ajax({url: "/broadcast/dashboard/partnership"}).done(function(data) { - try { - var html = document.createElement('span'), dash; + jQuery.getJSON("/" + id + "/dashboard/revenue/summary_data").done(function(data) { + var el, sub_count = data && data.data && data.data.total_subscriptions; + if ( typeof sub_count === "string" ) + sub_count = parseInt(sub_count.replace(/[,\.]/g, "")); - html.innerHTML = data; - dash = html.querySelector("#dash_main"); + if ( typeof sub_count !== "number" || sub_count === 0 || sub_count === NaN || sub_count === Infinity ) { + el = document.querySelector("#ffz-sub-display"); + if ( el ) + el.parentElement.removeChild(el); - var match = dash && dash.textContent.match(/([\d,\.]+) total active subscribers/), - sub_count = match && match[1]; + var failed = f._failed_sub_checks = (f._failed_sub_checks || 0) + 1; + if ( f._update_subscribers_timer && failed >= 5 ) { + f.log("Subscriber count failed 5 times. Giving up."); + clearTimeout(f._update_subscribers_timer); + delete f._update_subscribers_timer; + } - if ( ! sub_count ) { - var el = document.querySelector("#ffz-sub-display"); - if ( el ) - el.parentElement.removeChild(el); + return; + } - if ( f._update_subscribers_timer ) { - clearTimeout(f._update_subscribers_timer); - delete f._update_subscribers_timer; - } + // Graph this glorious data point + if ( f._dash_chart ) { + if ( ! f._dash_chart.series[3].options.showInLegend ) { + f._dash_chart.series[3].options.showInLegend = true; + f._dash_chart.legend.renderLegend(); + } - return; - } + f._dash_chart.series[3].addPoint({x: utils.last_minute(), y: sub_count}); + } - 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; + 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'; + var stat = document.createElement('span'); + stat.className = 'ffz stat'; + stat.id = 'ffz-sub-display'; + stat.title = 'Subscribers'; - stat.innerHTML = constants.STAR + ' '; + stat.innerHTML = constants.STAR + ' '; - el = document.createElement('span'); - stat.appendChild(el); + 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); + utils.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%'; - } - }); + 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({gravity: f.is_dashboard ? "s" : utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - } + cont.appendChild(stat); + jQuery(stat).tipsy({gravity: f.is_dashboard ? "s" : utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + } - el.innerHTML = sub_count; + el.innerHTML = utils.number_commas(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; - });; -} + }); +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 7c099b71..aa270f53 100644 --- a/src/utils.js +++ b/src/utils.js @@ -73,100 +73,6 @@ var sanitize_el = document.createElement('span'), }, - // 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; - }, - uncompressEmotes = function(value) { var output = {}, emotes = value.split("/"), @@ -199,121 +105,211 @@ var sanitize_el = document.createElement('span'), }, + // This code borrowed from the twemoji project, with tweaks. + UFE0Fg = /\uFE0F/g, + U200D = String.fromCharCode(0x200D), + EMOJI_CODEPOINTS = {}, - emoji_to_codepoint = function(icon, variant) { - if ( EMOJI_CODEPOINTS[icon] && EMOJI_CODEPOINTS[icon][variant] ) - return EMOJI_CODEPOINTS[icon][variant]; + emoji_to_codepoint = function(surrogates, sep) { + if ( EMOJI_CODEPOINTS[surrogates] && EMOJI_CODEPOINTS[surrogates][sep] ) + return EMOJI_CODEPOINTS[surrogates][sep]; - 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; + var input = surrogates.indexOf(U200D) === -1 ? surrogates.replace(UFE0Fg, '') : surrogates, + out = [], + 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)); - } - } + while (i < input.length) { + c = input.charCodeAt(i++); + if ( p ) { + out.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16)); + p = 0; + } else if ( 0xD800 <= c && c <= 0xDBFF ) + p = c; + else + out.push(c.toString(16)); + } - var es = EMOJI_CODEPOINTS[icon] = EMOJI_CODEPOINTS[icon] || {}, - out = es[variant] = r.join("-"); - - return out; + var retval = EMOJI_CODEPOINTS[surrogates] = out.join('-'); + return retval; }, - // Twitch Emote Tooltips + codepoint_to_emoji = function(codepoint) { + var code = typeof codepoint === 'string' ? parseInt(codepoint, 16) : codepoint; + if ( code < 0x10000 ) + return String.fromCharCode(code); + code -= 0x10000; + return String.fromCharCode( + 0xD800 + (code >> 10), + 0xDC00 + (code & 0x3FF) + ) + }, + + + // Twitch Emote Helpers SRCSETS = {}, build_srcset = function(id) { if ( SRCSETS[id] ) return SRCSETS[id]; - var out = SRCSETS[id] = constants.TWITCH_BASE + id + "/1.0 1x, " + constants.TWITCH_BASE + id + "/2.0 2x, " + constants.TWITCH_BASE + id + "/3.0 4x"; + var out = SRCSETS[id] = constants.TWITCH_BASE + id + "/1.0 1x, " + constants.TWITCH_BASE + id + "/2.0 2x"; return out; }, - data_to_tooltip = function(data) { - var emote_set = data.set, - set_type = data.set_type, + // Twitch API - f = FFZ.get(), - image = ''; - - if ( data.id && f.settings.emote_image_hover ) - image = ''; - - if ( set_type === undefined ) - set_type = "Channel"; - - if ( ! emote_set ) - return image + data.code; - - else if ( emote_set === "--global--" ) { - emote_set = "Twitch Global"; - set_type = null; - - } else if ( emote_set == "--twitch-turbo--" || emote_set == "turbo" || emote_set == "--turbo-faces--" ) { - emote_set = "Twitch Turbo"; - set_type = null; - } - - return image + "Emoticon: " + data.code + "
" + (set_type ? set_type + ": " : "") + emote_set; - }, - - build_tooltip = function(id, force_update, code) { - var emote_data = this._twitch_emotes[id]; - - if ( ! emote_data && code ) { - var set_id = this._twitch_emote_to_set[id]; - if ( set_id ) { - emote_data = this._twitch_emotes[id] = { - code: code, - id: id, - set: this._twitch_set_to_channel[set_id], - set_id: set_id - } - } - } - - if ( ! emote_data ) - return "???"; - - if ( typeof emote_data == "string" ) - return emote_data; - - if ( ! force_update && 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; - }; + api_call = function(method, url, data, options, token) { + options = options || {}; + var headers = options.headers = options.headers || {}; + headers['Client-ID'] = constants.CLIENT_ID; + if ( token ) + headers.Authorization = 'OAuth ' + token; + return Twitch.api[method].call(this, url, data, options); + }, -module.exports = { + // Dialogs + show_modal = function(contents, on_close, width) { + var container = document.createElement('div'), + subwindow = document.createElement('div'), + card = document.createElement('div'), + close_button = document.createElement('div'), + + closer = function() { container.parentElement.removeChild(container) }; + + container.className = 'twitch_subwindow_container'; + container.id = 'ffz-modal-container'; + + subwindow.className = 'twitch_subwindow ffz-subwindow'; + subwindow.style.width = '100%'; + subwindow.style.maxWidth = (width||420) + 'px'; + + card.className = 'card'; + + close_button.className = 'modal-close-button'; + close_button.innerHTML = constants.CLOSE; + + close_button.addEventListener('click', function() { + closer(); + if ( on_close ) + on_close(false); + }); + + container.appendChild(subwindow); + subwindow.appendChild(card); + subwindow.appendChild(close_button); + + card.appendChild(contents); + + var el = document.querySelector('app-main'); + + if ( el ) + el.parentElement.insertBefore(container, el.nextSibling); + else + document.body.appendChild(container); + + return closer; + }, + + + ember_lookup = function(thing) { + if ( ! window.App ) + return; + + if ( App.__deprecatedInstance__ && App.__deprecatedInstance__.registry && App.__deprecatedInstance_.registry.lookup ) + return App.__deprecatedInstance__.registry.lookup(thing); + if ( App.__container__ && App.__container__.lookup ) + return App.__container__.lookup(thing); + }; + + +module.exports = FFZ.utils = { + ember_views: function() { + return ember_lookup('-view-registry:main') || {}; + }, + + ember_lookup: ember_lookup, + + ember_resolve: function(thing) { + if ( ! window.App ) + return; + + if ( App.__deprecatedInstance__ && App.__deprecatedInstance__.registry && App.__deprecatedInstance_.registry.resolve ) + return App.__deprecatedInstance__.registry.resolve(thing); + if ( App.__container__ && App.__container__.resolve ) + return App.__container__.resolve(thing); + }, + build_srcset: build_srcset, - build_tooltip: build_tooltip, - load_emote_data: load_emote_data, + /*build_tooltip: build_tooltip, + load_emote_data: load_emote_data,*/ + api: { + del: function(u,d,o,t) { return api_call('del', u,d,o,t); }, + get: function(u,d,o,t) { return api_call('get', u,d,o,t); }, + post: function(u,d,o,t) { return api_call('post', u,d,o,t); }, + put: function(u,d,o,t) { return api_call('put', u,d,o,t); } + }, + + + show_modal: show_modal, + prompt: function(title, description, old_value, callback, width) { + var contents = document.createElement('div'), + heading = document.createElement('div'), + form = document.createElement('form'), + input, close_btn, okay_btn; + + contents.className = 'text-content'; + heading.className = 'content-header'; + heading.innerHTML = '

' + title + '

'; + + form.innerHTML = '
' + (description ? '

' + description + '

' : '') + '
'; + + contents.appendChild(heading); + contents.appendChild(form); + + input = form.querySelector('input'); + close_btn = form.querySelector('.js-subwindow-close'); + okay_btn = form.querySelector('.button.primary'); + + if ( old_value !== undefined ) + input.value = old_value; + + var closer, + cb = function(success) { + closer(); + if ( ! callback ) + return; + + callback(success ? input.value : null); + }; + + closer = show_modal(contents, cb, width); + + form.addEventListener('submit', function(e) { e.preventDefault(); cb(true); return false }); + okay_btn.addEventListener('click', function(e) { e.preventDefault(); cb(true); return false }); + close_btn.addEventListener('click', function(e) { e.preventDefault(); cb(false); return false }); + }, + + + last_minute: function() { + var now = new Date(); + if ( now.getSeconds() >= 30 ) + now.setMinutes(now.getMinutes()+1); + + now.setSeconds(0); + now.setMilliseconds(0); + return now.getTime(); + }, + + maybe_chart: function(series, point, render, force) { + var len = series.data.length; + if ( force || point.y !== null || (len > 0 && series.data[len-1].y !== null) ) { + series.addPoint(point, render); + return true; + } + return false; + }, update_css: function(element, id, css) { var all = element.innerHTML, @@ -338,7 +334,11 @@ module.exports = { tooltip_placement: function(margin, prefer) { return function() { - var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, + var pref = prefer; + if ( typeof pref === "function" ) + pref = pref.call(this); + + var dir = {ns: pref[0], ew: (pref.length > 1 ? pref[1] : false)}, $this = $(this), half_width = $this.width() / 2, half_height = $this.height() / 2, @@ -354,12 +354,10 @@ module.exports = { } }, - - splitIRCMessage: splitIRCMessage, - parseIRCTags: parseIRCTags, uncompressEmotes: uncompressEmotes, emoji_to_codepoint: emoji_to_codepoint, + codepoint_to_emoji: codepoint_to_emoji, parse_date: parse_date, @@ -418,7 +416,7 @@ module.exports = { var seconds = elapsed % 60, minutes = Math.floor(elapsed / 60), hours = Math.floor(minutes / 60), - days = ""; + days = null; minutes = minutes % 60; @@ -431,7 +429,7 @@ module.exports = { days = ( days > 0 ) ? days + " days, " : ""; } - return days + ((!no_hours || days || hours) ? ((days && hours < 10 ? "0" : "") + hours + ':') : '') + (minutes < 10 ? "0" : "") + minutes + (no_seconds ? "" : (":" + (seconds < 10 ? "0" : "") + seconds)); + return (days||'') + ((!no_hours || days || hours) ? ((days && hours < 10 ? "0" : "") + hours + ':') : '') + (minutes < 10 ? "0" : "") + minutes + (no_seconds ? "" : (":" + (seconds < 10 ? "0" : "") + seconds)); }, duration_string: function(val) { diff --git a/style.css b/style.css index 441a297c..6d7fe654 100644 --- a/style.css +++ b/style.css @@ -19,6 +19,7 @@ body > div.tipsy .tipsy-arrow { opacity: 0.8; } cursor: pointer; } +.ffz-hide-recommended-channels .js-recommended-channels, .ffz-hide-recent-past-broadcast .recent-past-broadcast, .ffz-hide-view-count .stat.twitch-channel-views, .ffz-minimal-chat-input .chat-interface .emoticon-selector-toggle, @@ -125,7 +126,7 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic } -.ember-chat .chat-menu.ffz-ui-popup { padding: 0; } +.ember-chat .chat-menu.ffz-ui-popup { padding: 0 } .ffz-button { float: right; @@ -216,8 +217,8 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic color: #aaa; position: absolute; - top: -25px; - left: 120px; + top: -20px; + left: 10px; right: 10px; z-index: 7; opacity: 0.95; @@ -525,6 +526,31 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic margin: 0; } +.emoticon-selector .emoticon-grid.ffz-no-emotes img { + padding: 5px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.emoticon-selector .emoticon-grid.ffz-no-emotes { + padding: 10px 20px; +} + +.emoticon.ffz-favorite { position: relative; } + +.favorites-grid:not(:empty) + .ffz-no-emotes, +.favorites-grid .emoticon.ffz-favorite:before { display: none } + +.emoticon.ffz-favorite:before { + content: ""; + display: block; + height: 14px; width: 14px; + position: absolute; + right: 0; bottom: 0; + background: url("//cdn.frankerfacez.com/script/emoticon_favorite.png") no-repeat; +} + .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content.menu-side-padding { padding-left: 20px; padding-right: 20px; } .emoticon-grid.collapsed span, @@ -584,9 +610,31 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic .ffz-ui-sub-menu-page, .ffz-ui-menu-page { overflow-y: auto; } -.ffz-ui-menu-page[data-page="about"], +.ffz-ui-menu-page[data-page="about"] .ffz-ui-sub-menu-page .chat-menu-content:first-child { + padding: 20px 0 10px; +} + +.ffz-ui-sub-menu-page[data-page="about"], .ffz-ui-menu-page .chat-menu-content p { padding: 0 20px; } +.ffz-ui-menu-page .version-list span { float: right } +.ffz-ui-menu-page .version-list li:not(:last-of-type) { + border-bottom: 1px dotted rgba(127,127,127,0.5); +} + +.ffz-ui-menu-page pre { + box-shadow: none !important; + white-space: pre-wrap; +} + +#ffz-old-news-button { + text-align: center; + text-transform: none; + padding-bottom: 15px; +} + +#ffz-old-news { display: none } + .chat-container.dark .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, .app-main.theatre .chat-container .chat-interface .ffz-ui-popup.emoticon-selector .emoticon-selector-box, @@ -618,7 +666,7 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic .chat-menu.ffz-ui-popup .ffz-ui-menu-page .emoticon-grid:first-of-type .heading { border-top: none; padding-top: 0; - background-position-y: 0; + background-position: 20px 0; } .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content { @@ -661,6 +709,7 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic list-style-type: none; border-top: 1px solid rgba(0,0,0,0.2); background-color: #eee; + margin: 0 1px 1px; } .ffz-ui-popup ul.menu:not(.sub-menu) { @@ -695,7 +744,10 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic float: left; } -.ffz-ui-popup ul.sub-menu { background-color: #dfdfdf; } +.ffz-ui-popup ul.sub-menu { + background-color: #dfdfdf; + margin: 0 1px; +} .app-main.theatre .ffz-ui-popup ul.sub-menu, .chat-container.dark .ffz-ui-popup ul.sub-menu, @@ -814,6 +866,10 @@ span.ffz-handle:after { left: 8px } border-right: 1px solid rgba(0,0,0,0.2); } +.ffz-ui-popup.dark ul.menu { border-top-color: rgba(255,255,255,0.1) } +.ffz-ui-popup.dark ul.menu a { border-left-color: rgba(255,255,255,0.1) } +.ffz-ui-popup.dark ul.sub-menu a { border-right-color: rgba(255,255,255,0.1) } + .ffz-ui-popup ul.menu li.active { background-color: #fff; } @@ -898,12 +954,43 @@ span.ffz-handle:after { left: 8px } /* BTTV Menu Fixes */ +.bttv-incompatibility { + padding-top: 8px !important; +} + .ffz-ui-popup.dark .emoticon-grid .heading, .ffz-ui-popup.dark li.title { color: #fff; } -.ffz-ui-popup.dark .ffz-ui-menu-page { background-color: #1e1e1e; } +.ffz-ui-popup.dark .ffz-ui-menu-page { background-color: #101010; } +.ffz-bttv .ffz-ui-popup .chat-menu-content p a { + padding: 0; +} + +.ffz-bttv .ffz-ui-popup.dark ul.menu, +.ffz-bttv .ffz-ui-popup.dark .ffz-ui-menu-page { margin: 0 } + +.ffz-ui-popup.dark .ffz-ui-menu-page .chat-menu-content .heading, +.ffz-ui-popup.dark .ffz-ui-menu-page .emoticon-grid .heading { + border-color: rgba(255,255,255,0.1); +} + +.ffz-ui-popup.dark ul.menu:not(.sub-menu), +.ffz-ui-popup.dark .ffz-ui-menu-page { border: 1px solid rgba(255,255,255,0.1) } +.ffz-ui-popup.dark .ffz-ui-menu-page { border-bottom: none } + +.ffz-bttv .emoticon.emoji { padding-top: 0 } /* Host Menu */ +.ffz-subwindow input { + padding: 5px; +} + +.ffz-subwindow p { + margin-bottom: 0; + padding: 0 0 10px; +} + +.ffz-subwindow .card, .ffz-channel-selector { background-image: url("//cdn.frankerfacez.com/script/zreknarf-bg.png"); background-repeat: no-repeat; @@ -946,6 +1033,29 @@ span.ffz-handle:after { left: 8px } } +/* Oh my god this is awful why does Chat Replay move chat lines? */ + +.app-main.theatre .chatReplay .chat-messages .chat-line .mod-icons, +.ffz-dark .chatReplay .chat-messages .chat-line .mod-icons { + background-color: rgba(0,0,0,0.8); +} + +.chatReplay .chat-messages .chat-line .mod-icons { + position: absolute; + z-index: 10; + left: 0; top: 2px; bottom: 2px; + padding: 4px 5px 4px 4px; + background: rgba(255,255,255,0.8); + transition: margin-left .08s linear; +} + +.chatReplay .chat-messages .chat-line:hover .mod-icons { + margin-left: 0; +} + +.chatReplay .chat-messages .horizontal-line { margin: 4px 0 } + + /* Positioning Fixes */ body:not(.ffz-bttv) .ember-chat .chat-settings { bottom: 40px; } @@ -953,8 +1063,36 @@ body:not(.ffz-bttv) .notification-controls .notify-menu { bottom: 25px; } body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } +/* Dashboard Changes */ + +#dash_main #stream-config-status, #chart_container.ffz-stat-chart { margin: 20px auto 0 } + +/*#dash_main #video_player { height: 303.75px } + +#stream-config-status { display: none } + +#delay_controls { float: left } +#delay_controls + #commercial_buttons { float: right } +#delay_controls + #commercial_buttons + div { clear: both } + +#delay_controls, #delay_controls + #commercial_buttons { + width: 50%; + margin-bottom: 20px; +} + +#dash_main #delay_controls, #dash_main #commercial_buttons { + margin-top: 0; + padding-top: 0; + border-top: none; +} + +#dash_main #delay_controls .section_header, #commercial_buttons .section_header { padding-top: 0 } +#dash_main #delay_controls .ui-slider { margin-top: 18px }*/ + + /* Menu Scrollbar */ +.conversations-list .scroll-container::-webkit-scrollbar, .chatters-container::-webkit-scrollbar, .ffz-scrollbar::-webkit-scrollbar, .ember-chat .chat-settings::-webkit-scrollbar, @@ -965,9 +1103,11 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } .emoticon-selector-box .all-emotes::-webkit-scrollbar, .ffz-ui-sub-menu-page::-webkit-scrollbar, .ffz-ui-menu-page::-webkit-scrollbar { + height: 6px; width: 6px; } +.conversations-list .scroll-container::-webkit-scrollbar-thumb, .chatters-container::-webkit-scrollbar-thumb, .ffz-scrollbar::-webkit-scrollbar-thumb, .ember-chat .chat-settings::-webkit-scrollbar-thumb, @@ -983,12 +1123,14 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } box-shadow: 0 0 1px 1px rgba(255,255,255,0.25); } +.ffz-dark .conversations-list .scroll-container::-webkit-scrollbar-thumb, .ffz-dark .ffz-scrollbar::-webkit-scrollbar-thumb, .ffz-dark .table::-webkit-scrollbar-thumb, .ffz-dark .conversation-window .conversation-content::-webkit-scrollbar-thumb, .ffz-dark .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb, .ffz-dark .conversation-input-bar .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb, +.theatre .conversations-list .scroll-container::-webkit-scrollbar-thumb, .theatre .chatters-container::-webkit-scrollbar-thumb, .theatre .ffz-scrollbar::-webkit-scrollbar-thumb, .theatre .ember-chat .chat-settings::-webkit-scrollbar-thumb, @@ -1027,6 +1169,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } /* Fix Moderation Cards */ +img.channel_background[src=""], img.channel_background[src="null"] { display: none; } .ffz-moderation-card { @@ -1034,7 +1177,11 @@ img.channel_background[src="null"] { display: none; } max-width: 340px; } -.ffz-moderation-card .extra-interface { +.ember-chat .ffz-moderation-card .close-button { + right: 7px; +} + +.ember-chat .ffz-moderation-card .extra-interface { padding-top: 0; } @@ -1043,7 +1190,9 @@ img.channel_background[src="null"] { display: none; } } .ffz-moderation-card.ffz-has-info h3.name { - margin-top: 0; + margin-top: -2px; + margin-bottom: 0; + padding-top: 0; white-space: nowrap; } @@ -1054,6 +1203,11 @@ img.channel_background[src="null"] { display: none; } margin-left: 50px; height: 18px; line-height: 18px; + pointer-events: none; +} + +.ffz-moderation-card .info .stat { + pointer-events: auto; } .ffz-moderation-card .info.channel-stats .stat { @@ -1108,7 +1262,7 @@ img.channel_background[src="null"] { display: none; } text-shadow: black 0 0 5px; } -.ffz-moderation-card .channel_background { +.ember-chat .ffz-moderation-card .channel_background { width: 100%; top: 0; } @@ -1156,13 +1310,14 @@ img.channel_background[src="null"] { display: none; } text-decoration: none; font-size: 18px; font-weight: bold; + margin-top: -1px !important; color: #888 !important; } /* Chat Rows */ -.ffz-alias { font-style: italic; } +.ffz-alias-italics .ffz-alias { font-style: italic; } .ember-chat .chat-messages .chat-line.ffz-has-deleted { line-height: 30px; @@ -1184,7 +1339,8 @@ img.channel_background[src="null"] { display: none; } text-decoration: none; } -body:not(.ffz-bttv) .more-messages-indicator { +body:not(.ffz-bttv) .ember-chat-container:not(.chatReplay) .more-messages-indicator, +body:not(.ffz-bttv) .chat-container:not(.chatReplay) .more-messages-indicator { /* This looks better when it's full width. */ margin: 0 -20px; } @@ -1199,6 +1355,10 @@ body:not(.ffz-bttv) .more-messages-indicator { /* Emoticon Tooltips */ +.ffz-wide-tip hr { + margin: 5px 0; +} + .ffz-wide-tip .tipsy-inner { min-width: 300px; max-width: 600px; @@ -1310,6 +1470,8 @@ body:not(.ffz-bttv) .more-messages-indicator { /* Dumb Fixes */ +.channel-stats .stat { vertical-align: top; } + .ffz-bttv .no-bttv { display: none; } .chat-container.dark, .app-main.theatre .chat-container, @@ -1320,6 +1482,15 @@ body:not(.ffz-bttv) .more-messages-indicator { } +/* Banned Words */ + +.deleted-word { + font-weight: bold; + opacity: 0.35; + cursor: pointer; +} + + /* Unsafe Links */ a.unsafe-link { @@ -1641,7 +1812,11 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar .ember-chat .chat-interface .more-messages-indicator.ffz-freeze-indicator { opacity: 1; cursor: default; - top: 0; + pointer-events: none; +} + +.chat-container:not(.chatReplay) .chat-interface .more-messages-indicator.ffz-freeze-indicator { + top: 0; } /* Chat History */ @@ -1669,6 +1844,9 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar margin-right: 0px; } +.chat-history .chat-line.admin .from, +.chat-history .chat-line.admin .colon { display: none } + .chat-history .chat-line.admin .message { color: #666; } .chat-history .timestamp { @@ -1878,13 +2056,21 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter /* No Blue */ -.ffz-no-blue .theatre .conversations-list-icon, -.ffz-no-blue.ffz-dark .conversations-list-icon, +.ffz-no-blue #carousel .nav { + background-color: rgba(25,25,25,.5); +} + +.ffz-no-blue .theatre .conversations-list-bottom-bar, +.ffz-no-blue.ffz-dark .conversations-list-bottom-bar, .ffz-no-blue .theatre .conversations-list, .ffz-no-blue.ffz-dark .conversations-list, +.ffz-no-blue .theatre .conversations-list-header, .ffz-no-blue .theatre .conversation-window, .ffz-no-blue.ffz-dark .conversation-window, +.ffz-no-blue .theatre .conversation-system-message, +.ffz-no-blue.ffz-dark .conversation-system-message, +.ffz-no-blue .warp, .ffz-no-blue #large_nav .content, .ffz-no-blue #small_nav .content, .ffz-no-blue .chat-container.dark, @@ -1940,10 +2126,18 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter .ffz-no-blue .takeover #carousel, .ffz-no-blue #carousel_and_background, .ffz-no-blue #carousel .items .pic img, +.ffz-no-blue .app-main.theatre .archives-contain, +.ffz-no-blue.ffz-dark .archives-contain, .ffz-no-blue #content .turbo_landing { background-color: #191919; } +.ffz-no-blue .warp__anchor, +.ffz-no-blue .warp__item--anchor, +.ffz-no-blue .warp__drawer, +.ffz-no-blue .leaf, +.ffz-no-blue .app-main.theatre .archives-contain .list-video:hover, +.ffz-no-blue.ffz-dark .archives-contain .list-video:hover, .ffz-no-blue .theatre .moderation-card .back-button, .ffz-no-blue .dark .moderation-card .back-button, .ffz-no-blue .force-dark .moderation-card .back-button @@ -1956,8 +2150,8 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter background-color: #232323; } -.ffz-no-blue .theatre .conversations-list-icon, -.ffz-no-blue.ffz-dark .conversations-list-icon, +.ffz-no-blue .theatre .conversations-list-bottom-bar, +.ffz-no-blue.ffz-dark .conversations-list-bottom-bar, .ffz-no-blue .theatre .conversations-list .conversation-preview-line, .ffz-no-blue.ffz-dark .conversations-list .conversation-preview-line, .ffz-no-blue .theatre .conversation-window, @@ -1991,7 +2185,8 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter .ffz-no-blue .theatre .conversation-header, .ffz-no-blue.ffz-dark .conversation-header, -.ffz-no-blue .theatre .conversations-list .conversations-list-header, +.ffz-no-blue .theatre .conversations-list .search-divider, +.ffz-no-blue.ffz-dark .conversations-list .search-divider, .ffz-no-blue.ffz-dark .conversations-list .conversations-list-header, .ffz-no-blue .theatre .conversation-window.has-focus .conversation-header, .ffz-no-blue.ffz-dark .conversation-window.has-focus .conversation-header, @@ -2036,14 +2231,13 @@ li[data-name="following"] a { padding: 2px 5px; } -#large_nav .game_filter.selected .ffz-follow-count { right: 13px; } - #small_nav .ffz-follow-count { position: absolute; bottom: 2px; - right: 2px; - padding: 0 2px; + right: 5px; + padding: 2px; font-size: 10px; + line-height: 10px; background-color: #191919; color: rgba(255,255,255,0.5); } @@ -2053,8 +2247,6 @@ li[data-name="following"] a { background-color: #101014; } -#small_nav .game_filter.selected .ffz-follow-count { right: 5px; } - /* Image Tooltips */ .ffz-image-hover { @@ -2109,7 +2301,9 @@ li[data-name="following"] a { } .ffz-classic-player:not(.ffz-top-conversations) .app-main.theatre .player .player-controls-bottom, -.ffz-classic-player .app-main.theatre .player[data-fullscreen="true"] .player-controls-bottom { +.ffz-classic-player:not(.ffz-top-conversations) .app-main.theatre .player[data-controls=true] .player-controls-bottom, +.ffz-classic-player .app-main.theatre .player[data-fullscreen="true"] .player-controls-bottom, +.ffz-classic-player .app-main.theatre .player[data-fullscreen="true"][data-controls=true] .player-controls-bottom { padding-bottom: 0; } @@ -2174,6 +2368,13 @@ li[data-name="following"] a { /* Directory Logos */ +.item .meta .title a a, +.item .meta .title a img { pointer-events: none } + +.ffz-directory-logo .meta p.title { + padding-top: 4px; +} + .ffz-directory-logo .meta p { width: auto; } .ffz-directory-logo .profile-photo { @@ -2240,11 +2441,16 @@ body:not(.ffz-bttv) .conversation-window .new-message-divider + .timestamp-line margin-top: -3px; } -/* Fix the ignore-cta covering up messages with no way to dismiss it. */ -body:not(.ffz-bttv) .conversation-window .ignore-cta + .conversation-content { +/* ignore-cta is a bit silly right now */ +body:not(.ffz-bttv) .conversation-window .ignore-cta:not(.hidden) + .conversation-content { padding-top: 76px; } +.conversation-window .conversation-system-message.ignore-cta-container { + padding-right: 26px; +} + + /* Hide that which should be hidden. */ .conversation-window.collapsed .ignore-cta, .conversation-chat-line.action .colon, @@ -2255,25 +2461,11 @@ body:not(.ffz-conv-title-clickable) .conversation-header a.conversation-header-n .conversation-window.has-unread .conversation-header .conversation-header-name { color: #fff !important; } -.ffz-top-conversations:not(.ffz-bttv) .conversation-window.collapsed .conversation-header { - box-shadow: none; -} -.conversations-list:not(.ffz-bttv) .conversations-list-item:last-of-type { - border-bottom: none !important; -} +/* Top Conversations */ -.ffz-top-conversations:not(.ffz-bttv) .conversations-content .conversations-list-icon, -.ffz-top-conversations:not(.ffz-bttv) .conversation-window { - border: 1px solid #dedede; - border-top: 0; -} - -.ffz-top-conversations.ffz-dark:not(.ffz-bttv) .conversations-content .conversations-list-icon, -.ffz-top-conversations.ffz-dark:not(.ffz-bttv) .conversation-window, -.ffz-top-conversations:not(.ffz-bttv) .theatre .conversations-content .conversations-list-icon, -.ffz-top-conversations:not(.ffz-bttv) .theatre .conversation-window { - border-color: rgba(255,255,255,0.2); +body:not(.ffz-top-conversations) .conversations-list-bottom-bar { + border-bottom: none; } .ffz-top-conversations .conversations-content { @@ -2287,47 +2479,25 @@ body:not(.ffz-conv-title-clickable) .conversation-header a.conversation-header-n top: 0px; } -.ffz-top-conversations .conversations-list { - bottom: inherit; - top: 54px; +.ffz-top-conversations .conversation-window.collapsed .conversation-header { + box-shadow: none; } -.ffz-top-conversations.ffz-bttv .conversations-list { top: 46px; } - -.ffz-top-conversations.ffz-bttv .conversations-list:before, -.ffz-top-conversations.ffz-bttv .conversations-list:after { display: none; } - -.ffz-top-conversations:not(.ffz-bttv) .conversations-list:before, -.ffz-top-conversations:not(.ffz-bttv) .conversations-list:after { - bottom: 100%; - top: auto; +.ffz-top-conversations .conversations-list-container.list-displayed { + top: 243px; } -.ffz-top-conversations:not(.ffz-bttv) .conversations-list:before { - border-top-color: transparent; - border-bottom-color: #dedede; +.ffz-top-conversations .conversations-list-bottom-bar { + border-top: none; } -.ffz-top-conversations:not(.ffz-bttv) .conversations-list:after { - border-top-color: transparent; - border-bottom-color: #f2f2f2; +.ffz-top-conversations .conversations-content .total-unread { + top: inherit; + bottom: -10px; } - -.ffz-dark.ffz-top-conversations:not(.ffz-bttv) .conversations-list:before, -.ffz-top-conversations:not(.ffz-bttv) .theatre .conversations-list:before { - border-top-color: transparent; - border-bottom-color: #32323e; -} - -.ffz-dark.ffz-top-conversations:not(.ffz-bttv) .conversations-list:after, -.ffz-top-conversations:not(.ffz-bttv) .theatre .conversations-list:after { - border-top-color: transparent; - border-bottom-color: #121212; -} - - -.ffz-top-conversations .theatre .player-controls-bottom { +.ffz-top-conversations .theatre .player-controls-bottom, +.ffz-top-conversations .theatre .player[data-controls=true] .player-controls-bottom { padding-bottom: 0; } @@ -2337,8 +2507,52 @@ body:not(.ffz-conv-title-clickable) .conversation-header a.conversation-header-n .ffz-top-conversations #directory-list { padding-top: 50px; } + +/* Minimize Conversations */ + +/*.ffz-minimize-conversations .conversation-window,*/ +.ffz-minimize-conversations .conversations-list-container { + transition: bottom ease-in-out 75ms, top ease-in-out 75ms, padding-bottom ease-in-out 75ms, padding-top ease-in-out 75ms; +} + +.ffz-minimize-conversations .conversations-list-container, +.ffz-minimize-conversations .conversations-content:hover { + pointer-events: all; +} + + +.ffz-minimize-conversations:not(.ffz-top-conversations) .conversations-content:hover .conversation-window.collapsed:not(.has-unread), +.ffz-minimize-conversations:not(.ffz-top-conversations) .conversations-content:hover .conversations-list-container:not(.list-displayed) { + bottom: 0; + padding-top: 0; +} + +.ffz-minimize-conversations:not(.ffz-top-conversations) .conversation-window.collapsed:not(.has-unread), +.ffz-minimize-conversations:not(.ffz-top-conversations) .conversations-list-container:not(.list-displayed) { + bottom: -30px; + padding-top: 30px; +} + +.ffz-minimize-conversations.ffz-top-conversations .conversations-content:hover .conversation-window.collapsed:not(.has-unread), +.ffz-minimize-conversations.ffz-top-conversations .conversations-content:hover .conversations-list-container:not(.list-displayed) { + top: 0; + padding-bottom: 0; +} + +.ffz-minimize-conversations.ffz-top-conversations .conversation-window.collapsed:not(.has-unread), +.ffz-minimize-conversations.ffz-top-conversations .conversations-list-container:not(.list-displayed) { + top: -30px; + padding-bottom: 30px; +} + + /* Following Page */ +.directory_header .nav li.ffz-manage-following { + float: right; + margin-right: 0; +} + .user.item { position: relative; } .user.item .overlay_info.length { @@ -2451,4 +2665,26 @@ body:not(.ffz-creative-showcase) .creative-hero, .ffz-creative-tags .creativetag-list a.active { color: #fff !important; background-color: #6441a5 !important; +} + +/* Content-Box~! Down with Twitch randomly changing the box-sizing for everything! */ + +#ffz-channel-table *, +#ffz-group-table *, +.ffz-ui-popup * { + box-sizing: content-box; +} + + +/* Dank * / + +.streams .ember-view[data-channel="memeathon"] .stream .thumb:after { + content: ""; + position: absolute; + top: 0; bottom: 0; + left: 0; right: 0; + background: url("//cdn.frankerfacez.com/script/wow.gif") no-repeat; + background-size: contain; + background-position: bottom right; + pointer-events: none; } \ No newline at end of file
Developers
Dan Salvato  
Stendec  
Version ' + FFZ.version_info + 'Logs