From 86b66bb8f568b4e96f1e826438bbe900b9d7a273 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Wed, 13 Jul 2016 02:06:50 -0400 Subject: [PATCH] Commiting code in 2016 LOL (See changelog.html I'm a bad person) --- dark.css | 89 +++- gulpfile.js | 14 - src/badges.js | 107 +++-- src/constants.js | 3 +- src/ember/bits.js | 112 +++++ src/ember/channel.js | 216 ++++----- src/ember/chat-input.js | 60 +-- src/ember/chatview.js | 85 +--- src/ember/conversations.js | 50 +-- src/ember/directory.js | 313 +++++-------- src/ember/feed-card.js | 75 ++-- src/ember/following.js | 2 +- src/ember/layout.js | 28 +- src/ember/line.js | 86 ++-- src/ember/moderation-card.js | 701 +++++++++++++++--------------- src/ember/player.js | 170 ++------ src/ember/room.js | 279 ++++++++---- src/ember/vod-chat.js | 148 +++---- src/ember/wrapper.js | 88 ++++ src/ext/api.js | 84 +++- src/ext/betterttv.js | 86 ++-- src/ext/rechat.js | 305 ------------- src/ext/warpworld.js | 30 ++ src/main.js | 14 +- src/settings.js | 188 +++++++- src/styles/badges-blank.css | 2 +- src/styles/badges-circular.css | 2 +- src/styles/badges-transparent.css | 14 +- src/tokenize.js | 187 ++++++-- src/ui/about_page.js | 31 +- src/ui/following-count.js | 2 +- src/ui/tooltips.js | 9 + src/utils.js | 130 ++++-- style.css | 197 +++++++-- 34 files changed, 2175 insertions(+), 1732 deletions(-) create mode 100644 src/ember/bits.js create mode 100644 src/ember/wrapper.js delete mode 100644 src/ext/rechat.js create mode 100644 src/ext/warpworld.js diff --git a/dark.css b/dark.css index d2df4c16..a32596b6 100644 --- a/dark.css +++ b/dark.css @@ -361,11 +361,11 @@ body.ffz-dark:not([data-page="teams#show"]), color: #a68ed2; } -.ffz-dark .button.button--icon-only svg path { +.ffz-dark .button.button--icon-only svg { fill: #a68ed2; } -.ffz-dark .button.button--icon-only:hover svg path { +.ffz-dark .button.button--icon-only:hover svg { fill: #fff; } @@ -483,11 +483,6 @@ body.ffz-dark:not([data-page="teams#show"]), fill: rgba(255,255,255,0.5); } -.ffz-dark .following-col .col-header, -.ffz-dark .following-col .header { - border-color: #32323e; -} - .ffz-dark .following-col .following-list .load-more span, .ffz-dark .viewall a { background-color: rgb(25,25,31); @@ -1176,10 +1171,73 @@ body.ffz-dark:not([data-page="teams#show"]), } +/* Creative UI */ + +.ffz-dark .ct-spotlight__right { + background-color: #121212; +} + +.ffz-dark .ct-spotlight__controls-container { + background-color: #161616; + box-shadow: -1px 0 0 #303030 inset; +} + +.ffz-dark .ct-bar { + background-color: #121212; + box-shadow: + 0 2px 6px -2px rgba(255,255,255,0.1), + 0 1px 0 rgba(255,255,255,0.05), + 0 -1px 0 rgba(255,255,255,0.05); +} + +.ffz-dark .ct-banner__name, +.ffz-dark .ct-banner__status, +.ffz-dark .ct-type-2, +.ffz-dark .ct-type-4 { + color: #aaa; +} + +.ffz-dark .ct-type-grey-light { + color: #ccc; +} + + +.ffz-dark .ct-crumb:after { + box-shadow: 12px -6px 24px -8px rgba(255,255,255,0.1); + border-color: #303030; +} + +.ffz-dark .ct-spotlight__avatar { + box-shadow: 0 0 0 1px #303030, 0 2px 3px #303030; +} + +.ffz-dark .ct-spotlight__right--col .ct-spotlight__card-container { + box-shadow: 0 -1px 0 #303030 inset; +} + +.ffz-dark hr, +.ffz-dark .ct-spotlight__section, +.ffz-dark .balloon__stroke, +.ffz-dark .ct-spotlight__avatar, +.ffz-dark .ct-bar__item { + border-color: #303030; +} + +.ffz-dark .ct-crumb--1:after, +.ffz-dark .ct-crumb--1 .ct-crumb__label { + background-color: #121212; +} + +.ffz-dark .ct-crumb--2:after, +.ffz-dark .ct-crumb--2 .ct-crumb__label { + background-color: #161616; +} + + /* Creative Tags */ .ffz-dark .ct-tags__tag { - background-color: #191919; + background-color: #121212; color: #999 !important; } @@ -1228,6 +1286,7 @@ body.ffz-dark:not([data-page="teams#show"]), } .ffz-dark .activity-meta-divider:before, +.ffz-dark .list-load-more, .ffz-dark .activity-card { border-color: #474747; } @@ -1244,6 +1303,20 @@ body.ffz-dark:not([data-page="teams#show"]), .ffz-dark .activity-meta__name { color: #ccc } +.ffz-dark .activity-card__comments { + background-color: #121212; + box-shadow:inset 0 1px 0 #474747; +} + +.ffz-dark .activity-add-comment__textarea:before { + background: #1d1d1d; + border-left-color: #474747; + border-bottom-color: #474747; +} + +.inherit-color { color: inherit !important } + + /* Search Panel */ .ffz-dark[data-current-path="user.channel.index.index"] .searchPanel { background-color: rgba(16,16,16,0.9) } diff --git a/gulpfile.js b/gulpfile.js index 513683fa..e084d2e4 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -221,20 +221,6 @@ 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)); - /*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); } diff --git a/src/badges.js b/src/badges.js index e788c832..903c8d84 100644 --- a/src/badges.js +++ b/src/badges.js @@ -3,7 +3,11 @@ var FFZ = window.FrankerFaceZ, utils = require('./utils'), SPECIAL_BADGES = ['staff', 'admin', 'global_mod'], - OTHER_KNOWN = ['turbo', 'warcraft'], + OTHER_KNOWN = ['turbo', 'warcraft', 'bits'], + + NO_INVERT_BADGES = ['subscriber', 'ffz-badge-1'], + INVERT_INVERT_BADGES = ['bits'], + TRANSPARENT_BADGES = ['subscriber'], BTTV_TYPE_REPLACEMENTS = { 'global-moderator': 'global_mod' @@ -25,14 +29,6 @@ var FFZ = window.FrankerFaceZ, BADGE_KLASSES = { 'global_mod': 'global-moderator' - }, - - badge_css = function(badge, klass) { - klass = klass || ('ffz-badge-' + badge.id); - var out = ".badges ." + klass + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.css || "") + '}'; - if ( badge.alpha_image ) - out += ".badges .badge.alpha." + klass + ",.ffz-transparent-badges .badges ." + klass + ' { background-image: url("' + badge.alpha_image + '"); }'; - return out; }; @@ -92,8 +88,13 @@ FFZ.settings_info.hidden_badges = { } for(var badge_id in f.badges) { - if ( f.badges.hasOwnProperty(badge_id) && f.badges[badge_id].name ) - values.push('ffz-' + f.badges[badge_id].name + ''); + if ( ! f.badges.hasOwnProperty(badge_id) ) + continue; + + var badge = f.badges[badge_id], + hide_key = (badge.source_ext ? f._apis[badge.source_ext].name_key : 'ffz') + '-' + (badge.name || badge.id); + + values.push('' + hide_key + ''); } if ( this.has_bttv && window.BetterTTV ) { @@ -129,11 +130,13 @@ FFZ.settings_info.sub_notice_badges = { category: "Chat Appearance", name: "Old-Style Subscriber Notice Badges", + no_bttv: true, + help: "Display a subscriber badge on old-style chat messages about new subscribers.", on_update: function(val) { - this.toggle_style('badges-sub-notice', ! val); - this.toggle_style('badges-sub-notice-on', val); + this.toggle_style('badges-sub-notice', ! this.has_bttv && ! val); + this.toggle_style('badges-sub-notice-on', ! this.has_bttv && val); } }; @@ -269,7 +272,7 @@ FFZ.ws_commands.set_badge = function(data) { badges = user.badges = user.badges || {}; if ( badge === undefined || badge === null ) - delete badges[slot]; + badges[slot] = null; else badges[slot] = badge; } @@ -287,14 +290,16 @@ FFZ.prototype.get_badges = function(user, room_id, badges, msg) { return badges; for(var slot in data.badges) { - if ( ! data.badges.hasOwnProperty(slot) ) + var badge = data.badges[slot]; + if ( ! data.badges.hasOwnProperty(slot) || ! badge ) continue; - var badge = data.badges[slot], - full_badge = this.badges[badge.id] || {}, - old_badge = badges[slot]; + var full_badge = this.badges[badge.id] || {}, + old_badge = badges[slot], - if ( hidden_badges.indexOf('ffz-' + full_badge.name) !== -1 ) + hide_key = (full_badge.source_ext ? this._apis[full_badge.source_ext].name_key : 'ffz') + '-' + (full_badge.name || full_badge.id); + + if ( hidden_badges.indexOf(hide_key) !== -1 ) continue; if ( full_badge.visible !== undefined ) { @@ -319,10 +324,13 @@ FFZ.prototype.get_badges = function(user, room_id, badges, msg) { badges[slot] = { klass: 'ffz-badge-' + badge.id, - title: badge.title || full_badge.title, + title: badge.title || full_badge.title || ('Unknown FFZ Badge\nID: ' + badge.id), image: badge.image, full_image: full_badge.image, color: badge.color, + no_invert: badge.no_invert || full_badge.no_invert, + invert_invert: badge.invert_invert || full_badge.invert_invert, + transparent: badge.transparent || full_badge.transparent || (badge.color || full_badge.color) === "transparent", extra_css: badge.extra_css }; } @@ -390,12 +398,15 @@ FFZ.prototype.get_line_badges = function(msg) { badges[last_id] = { klass: (BADGE_KLASSES[badge] || badge) + (is_known ? '' : ' unknown-badge') + ' version-' + version, title: binfo && binfo.title || BADGE_NAMES[badge] || badge.capitalize(), - click_url: binfo && binfo.click_action === 'visit_url' && binfo.click_url + click_url: binfo && binfo.click_action === 'visit_url' && binfo.click_url, + no_invert: NO_INVERT_BADGES.indexOf(badge) !== -1, + invert_invert: INVERT_INVERT_BADGES.indexOf(badge) !== -1, + transparent: TRANSPARENT_BADGES.indexOf(badge) !== -1 }; if ( ! is_known && binfo ) { badges[last_id].image = binfo.image_url_1x; - badges[last_id].srcSet = 'url("' + binfo.image_url_1x + '") 1x, url("' + binfo.image_url_2x + '") 2x, url("' + binfo.image_url_3x + '") 4x'; + badges[last_id].srcSet = 'url("' + binfo.image_url_1x + '") 1x, url("' + binfo.image_url_2x + '") 2x, url("' + binfo.image_url_4x + '") 4x'; } } @@ -413,13 +424,13 @@ FFZ.prototype.get_other_badges = function(user_id, room_id, user_type, has_sub, for(var i=0, l = SPECIAL_BADGES.length; i < l; i++) { var mb = SPECIAL_BADGES[i]; if ( user_type === mb ) { - badges[0] = {klass: BADGE_KLASSES[mb] || mb, title: BADGE_TITLES[mb] || mb.capitalize()}; + badges[0] = {klass: BADGE_KLASSES[mb] || mb, title: BADGE_NAMES[mb] || mb.capitalize()}; break; } } if ( has_sub ) - badges[10] = {klass: 'subscriber', title: 'Subscriber'} + badges[10] = {klass: 'subscriber', title: 'Subscriber', no_invert: true, transparent: true} if ( has_turbo ) badges[15] = {klass: 'turbo', title: 'Turbo'} @@ -450,7 +461,16 @@ FFZ.prototype.render_badges = function(badges) { if ( badge.click_url ) klass += ' click_url'; - out.push('
'); + if ( badge.no_invert ) + klass += ' no-invert'; + + if ( badge.invert_invert ) + klass += ' invert-invert'; + + if ( badge.transparent ) + klass += ' transparent'; + + out.push('
'); } return out.join(""); @@ -482,17 +502,27 @@ FFZ.prototype.bttv_badges = function(data) { for(var i=0; i < data.badges.length; i++) { var badge = data.badges[i], space_ind = badge.type.indexOf(' '), - hidden_key = BTTV_TYPE_REPLACEMENTS[badge.type] || (space_ind === -1 ? badge.type : badge.type.substr(0, space_ind)); + hidden_key = space_ind !== -1 ? badge.type.substr(0, space_ind) : badge.type; + + if ( hidden_key.indexOf('twitch-') === 0 ) + hidden_key = hidden_key.substr(7); + + if ( BTTV_TYPE_REPLACEMENTS.hasOwnProperty(hidden_key) ) + hidden_key = BTTV_TYPE_REPLACEMENTS[hidden_key]; + else { + var ind = hidden_key.indexOf('-'); + if ( ind !== -1 ) + hidden_key = hidden_key.substr(0, ind); + } if ( hidden_badges.indexOf(hidden_key) !== -1 ) { data.badges.splice(i, 1); + i--; continue; } - if ( badge.type === "subscriber" || badge.type === "turbo" || badge.type.substr(0, 8) === 'warcraft' ) { + if ( insert_at === -1 && (badge.type === "subscriber" || badge.type === "turbo" || badge.type.substr(0, 7) === 'twitch-') ) insert_at = i; - break; - } } // If there's no user, we're done now. @@ -502,15 +532,17 @@ FFZ.prototype.bttv_badges = function(data) { // We have a user. Start replacing badges. for (var slot in user.badges) { - if ( ! user.badges.hasOwnProperty(slot) ) + var badge = user.badges[slot]; + if ( ! user.badges.hasOwnProperty(slot) || ! badge ) continue; - var badge = user.badges[slot], - full_badge = this.badges[badge.id] || {}, + var full_badge = this.badges[badge.id] || {}, desc = badge.title || full_badge.title, - style = ""; + style = "", - if ( hidden_badges.indexOf('ffz-' + full_badge.name) !== -1 ) + hide_key = (full_badge.source_ext ? this._apis[full_badge.source_ext].name_key : 'ffz') + '-' + (full_badge.name || full_badge.id); + + if ( hidden_badges.indexOf(hide_key) !== -1 ) continue; if ( full_badge.visible !== undefined ) { @@ -565,8 +597,6 @@ FFZ.prototype.bttv_badges = function(data) { while(badges_out.length) data.badges.insertAt(insert_at, badges_out.shift()[1]); } - - } @@ -644,10 +674,13 @@ FFZ.prototype._load_badge_json = function(badge_id, data) { data.replaces = true; } + if ( data.name === 'developer' ) + data.no_invert = true; + if ( data.name === 'bot' ) data.visible = function(r,user) { return !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); }; - utils.update_css(this._badge_style, badge_id, badge_css(data)); + utils.update_css(this._badge_style, badge_id, utils.badge_css(data)); } diff --git a/src/constants.js b/src/constants.js index f3d5eabe..68db9d71 100644 --- a/src/constants.js +++ b/src/constants.js @@ -4,7 +4,6 @@ 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 SERVER = DEBUG ? "//localhost:8000/" : "https://cdn.frankerfacez.com/", IS_OSX = navigator.platform ? navigator.platform.indexOf('Mac') !== -1 : /OS X/.test(navigator.userAgent), - IS_WIN = navigator.platform ? navigator.platform.indexOf('Win') !== -1 : /Windows/.test(navigator.userAgent), 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]", @@ -44,6 +43,8 @@ module.exports = FrankerFaceZ.constants = { SEPARATORS: SEPARATORS, SPLITTER: SPLITTER, + UUID_TEST: /(?:^| +)([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}) *$/i, + KNOWN_CODES: { "#-?[\\\\/]": "#-/", ":-?(?:7|L)": ":-7", diff --git a/src/ember/bits.js b/src/ember/bits.js new file mode 100644 index 00000000..697aa496 --- /dev/null +++ b/src/ember/bits.js @@ -0,0 +1,112 @@ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + constants = require('../constants'); + + +// -------------------- +// Settings +// -------------------- + +FFZ.settings_info.bits_animated = { + type: "boolean", + value: true, + + category: "Chat Appearance", + no_bttv: true, + + visible: function() { + var globals = utils.ember_lookup('service:globals'), + user = this.get_user(); + + return (globals && globals.get('isBitsEnabled')) || (user && user.is_staff); + }, + + name: "Bits Animation", + help: "Display bits with animation.", + + on_update: utils.toggle_cls('ffz-animate-bits') +} + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_bits = function() { + utils.toggle_cls('ffz-animate-bits')(this.settings.bits_animated); + + var f = this, + Service = utils.ember_lookup('service:bits-rendering-config'); + if ( ! Service ) + return this.error("Unable to locate the Ember service:bits-rendering-config"); + + Service.reopen({ + ffz_get_tier: function(amount) { + var config = this.get('config'), + tiers = config.tiers || [], + tier = null, + index = null; + + for(var i=0, l = tiers.length; i < l; i++) { + var t = tiers[i]; + if ( amount < t.min_bits ) + break; + + tier = t; + index = i; + } + + return [index, tier]; + }, + + ffz_get_preview: function(tier) { + return this._templateUrlConstructor(tier.image, "dark", (f.settings.bits_animated ? 'animated' : 'static'), 4); + }, + + _ffz_tier_css: function(ind, tier) { + var selector = '.ffz-bit.bit-tier-' + ind, + color = f._handle_color(tier.color), + + template = 'url("' + this.get('config.templateUrl').replace('{background}', 'light').replace('{image}', tier.image) + '")', + template_srcset = template.replace('{scale}', 1) + ' 1x, ' + template.replace('{scale}', 2) + ' 2x, ' + template.replace('{scale}', 4) + ' 4x', + output; + + output = selector + '{' + + 'color: ' + color[0] + ';' + + 'background-image: ' + template.replace('{scale}', 1).replace(/{state}/g, 'static') + ';' + + 'background-image: -webkit-image-set(' + template_srcset.replace(/{state}/g, 'static') + ');' + + '}.ffz-animate-bits ' + selector + '{' + + 'background-image: ' + template.replace('{scale}', 1).replace(/{state}/g, 'animated') + ';' + + 'background-image: -webkit-image-set(' + template_srcset.replace(/{state}/g, 'animated') + ');' + + '}'; + + template = template.replace('/light/', '/dark/'); + template_srcset = template_srcset.replace(/\/light\//g, '/dark/'); + + return output + '.tipsy ' + selector + ',.dark ' + selector + ',.force-dark ' + selector + ',.theatre ' + selector + '{' + + 'color: ' + color[1] + ';' + + 'background-image: ' + template.replace('{scale}', 1).replace(/{state}/g, 'static') + ';' + + 'background-image: -webkit-image-set(' + template_srcset.replace(/{state}/g, 'static') + ');' + + '}.ffz-animate-bits .tipsy ' + selector + ',.ffz-animate-bits .dark ' + selector + ',.ffz-animate-bits .force-dark ' + selector + ',.ffz-animate-bits .theatre ' + selector + '{' + + 'background-image: ' + template.replace('{scale}', 1).replace(/{state}/g, 'animated') + ';' + + 'background-image: -webkit-image-set(' + template_srcset.replace(/{state}/g, 'animated') + ');' + + '}'; + }, + + ffz_update_css: function() { + var tiers = this.get('config.tiers') || [], + output = []; + + for(var i=0, l = tiers.length; i < l; i++) + output.push(this._ffz_tier_css(i, tiers[i])); + + utils.update_css(f._chat_style, 'bit-styles', output.join('')); + + }.observes('config') + }); + + if ( ! Service.get('isLoaded') ) + Service.loadRenderConfig(); + else + Service.ffz_update_css(); +} \ No newline at end of file diff --git a/src/ember/channel.js b/src/ember/channel.js index c27db74b..9f1486e9 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -19,55 +19,24 @@ FFZ.prototype.setup_channel = function() { document.body.classList.toggle('ffz-theater-stats', this.settings.theater_stats); this.log("Hooking the Ember Channel Index view."); - var Channel = utils.ember_resolve('view:channel/index'), - f = this; - - if ( ! Channel ) + if ( ! this.update_views('view:channel/index', this.modify_channel_index) ) return; - this._modify_cindex(Channel); - - // The Stupid View Fix. Is this necessary still? - try { - Channel.create().destroy(); - } catch(err) { } - - // Update Existing - var views = utils.ember_views(); - for(var key in views) { - var view = views[key]; - if ( view instanceof Channel ) { - this.log("Manually updating existing Channel Index view.", view); - try { - if ( ! view.ffzInit ) - this._modify_cindex(view); - view.ffzInit(); - } catch(err) { - this.error("setup: view:channel/index: " + err); - } - } - }; - - this.log("Hooking the Ember Channel model."); - Channel = utils.ember_resolve('model:channel'); + var f = this, + Channel = utils.ember_resolve('model:deprecated-channel'); + if ( ! Channel ) - return; + return this.log("Unable to find the Ember model:deprecated-channel"); - Channel.reopen({ - ffz_host_target: undefined, + this._modify_cmodel(Channel); - setHostMode: function(e) { - if ( f.settings.hosted_channels ) { - this.set('ffz_host_target', e.target); - return this._super(e); - } else { - this.set('ffz_host_target', undefined); - return this._super({target: void 0, delay: 0}); - } - } - }); + var Store = utils.ember_lookup('service:store'), + type_map = Store && Store.typeMapFor(Channel); + if ( type_map && type_map.records ) + for(var i=0; i < type_map.records.length; i++) + this._modify_cmodel(type_map.records[i]); this.log("Hooking the Ember Channel controller."); @@ -89,7 +58,7 @@ FFZ.prototype.setup_channel = function() { if ( ! this.get('content.id') ) return; - this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 60000); + this._ffz_update_timer = setTimeout(this.ffzCheckUpdate.bind(this), 55000 + (Math.random() * 10000)); }.observes("content.id"), ffzCheckUpdate: function() { @@ -130,7 +99,6 @@ FFZ.prototype.setup_channel = function() { }); }, - ffzUpdateTitle: function() { var name = this.get('content.name'), display_name = this.get('content.display_name'); @@ -178,29 +146,28 @@ FFZ.prototype.setup_channel = function() { } -FFZ.prototype._modify_cindex = function(view) { +FFZ.prototype._modify_cmodel = function(model) { var f = this; + model.reopen({ + ffz_host_target: undefined, - view.reopen({ - didInsertElement: function() { - this._super(); - try { - this.ffzInit(); - } catch(err) { - f.error("CIndex didInsertElement: " + err); + setHostMode: function(e) { + if ( f.settings.hosted_channels ) { + this.set('ffz_host_target', e.target); + return this._super(e); + } else { + this.set('ffz_host_target', undefined); + return this._super({target: void 0, delay: 0}); } - }, + } + }); +} - willClearRender: function() { - try { - this.ffzTeardown(); - } catch(err) { - f.error("CIndex willClearRender: " + err); - } - return this._super(); - }, - ffzInit: function() { +FFZ.prototype.modify_channel_index = function(view) { + var f = this; + utils.ember_reopen_view(view, { + ffz_init: function() { var id = this.get('controller.content.id') || this.get('controller.id'), el = this.get('element'); @@ -216,6 +183,10 @@ FFZ.prototype._modify_cindex = function(view) { this.ffzUpdateHostButton(); this.ffzUpdatePlayerStats(); + // Listen to scrolling. + this._ffz_scroller = this.ffzOnScroll.bind(this); + jQuery(el).parents('.tse-scroll-content').on('scroll', this._ffz_scroller); + var views = this.get('element').querySelector('.svg-glyph_views:not(.ffz-svg)') if ( views ) views.parentNode.classList.add('twitch-channel-views'); @@ -242,6 +213,41 @@ FFZ.prototype._modify_cindex = function(view) { }); }, + ffz_destroy: function() { + var id = this.get('controller.content.id') || this.get('controller.id'); + if ( id ) + f.ws_send("unsub", "channel." + id); + + this.get('element').setAttribute('data-channel', ''); + f._cindex = undefined; + if ( this._ffz_update_uptime ) + clearTimeout(this._ffz_update_uptime); + + if ( this._ffz_update_stats ) + clearTimeout(this._ffz_update_stats); + + if ( this._ffz_scroller ) { + jQuery(this.get('element')).parents('.tse-scroll-content').off('scroll', this._ffz_scroller); + this._ffz_scroller = null; + } + + document.body.classList.remove('ffz-small-player'); + utils.update_css(f._channel_style, id, null); + }, + + + ffzOnScroll: function(event) { + // When we scroll past the bottom of the player, do stuff! + var top = event && event.target && event.target.scrollTop, + height = this.get('layout.playerSize.1'); + + if ( ! top ) + top = jQuery(this.get('element')).parents('.tse-scroll-content').scrollTop(); + + document.body.classList.toggle('ffz-small-player', f.settings.small_player && top >= height); + }, + + ffzFixTitle: function() { if ( f.has_bttv || ! f.settings.stream_title ) return; @@ -463,6 +469,15 @@ FFZ.prototype._modify_cindex = function(view) { ffzUpdatePlayerStats: function() { + if ( this._ffz_update_stats ) { + clearTimeout(this._ffz_update_stats); + this._ffz_update_stats = null; + } + + // Schedule an update. + if ( f.settings.player_stats ) + this._ffz_update_stats = setTimeout(this.ffzUpdatePlayerStats.bind(this), 1000); + var channel_id = this.get('controller.content.id') || this.get('controller.id'), hosted_id = this.get('controller.hostModeTarget.id'), @@ -478,12 +493,12 @@ FFZ.prototype._modify_cindex = function(view) { try { player = player_cont && player_cont.get && player_cont.get('player'); - stats = player && player.stats; + stats = player && player.getVideoInfo(); } catch(err) { - f.error("Channel ffzUpdatePlayerStats: player.stats: " + err); + f.error("Channel ffzUpdatePlayerStats: player.getVideoInfo: " + err); } - if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hlsLatencyBroadcaster || stats.hlsLatencyBroadcaster === 'NaN' || Number.isNaN(stats.hlsLatencyBroadcaster) ) { + if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hls_latency_broadcaster || Number.isNaN(stats.hls_latency_broadcaster) ) { if ( stat_el ) stat_el.parentElement.removeChild(stat_el); } else { @@ -505,21 +520,20 @@ FFZ.prototype._modify_cindex = function(view) { jQuery(stat_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } - var delay = parseFloat(stats.hlsLatencyBroadcaster); + var delay = Math.round(stats.hls_latency_broadcaster / 10) / 100, + bitrate = Math.round(stats.current_bitrate * 1000) / 1000; if ( delay > 180 ) { delay = Math.floor(delay); - stat_el.setAttribute('original-title', 'Video Information
Broadcast ' + utils.time_to_string(delay, true) + ' Ago

Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps') + stat_el.setAttribute('original-title', 'Video Information
Broadcast ' + utils.time_to_string(delay, true) + ' Ago

Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p @ ' + stats.current_fps + '
Playback Rate: ' + bitrate + ' Kbps') el.textContent = utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'; } else { - stat_el.setAttribute('original-title', 'Stream Latency
Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps'); - - delay = stats.hlsLatencyBroadcaster; - var pos = delay.lastIndexOf('.'); - - if ( pos === -1 ) + stat_el.setAttribute('original-title', 'Stream Latency
Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p @ ' + stats.current_fps + '
Playback Rate: ' + stats.current_bitrate + ' Kbps'); + delay = delay.toString(); + var ind = delay.indexOf('.'); + if ( ind === -1 ) delay = delay + '.00'; - else if ( delay.length - pos < 3 ) + else if ( ind >= delay.length - 2 ) delay = delay + '0'; el.textContent = delay + 's'; @@ -544,7 +558,7 @@ FFZ.prototype._modify_cindex = function(view) { } - if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hlsLatencyBroadcaster || stats.hlsLatencyBroadcaster === 'NaN' || Number.isNaN(stats.hlsLatencyBroadcaster) ) { + if ( ! container || ! f.settings.player_stats || ! stats || ! stats.hls_latency_broadcaster || Number.isNaN(stats.hls_latency_broadcaster) ) { if ( stat_el ) stat_el.parentElement.removeChild(stat_el); } else { @@ -566,21 +580,20 @@ FFZ.prototype._modify_cindex = function(view) { jQuery(stat_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } - var delay = parseFloat(stats.hlsLatencyBroadcaster); + var delay = Math.round(stats.hls_latency_broadcaster / 10) / 100, + bitrate = Math.round(stats.current_bitrate * 1000) / 1000; if ( delay > 180 ) { delay = Math.floor(delay); - stat_el.setAttribute('original-title', 'Video Information
Broadcast ' + utils.time_to_string(delay, true) + ' Ago

Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps') + stat_el.setAttribute('original-title', 'Video Information
Broadcast ' + utils.time_to_string(delay, true) + ' Ago

Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p @ ' + stats.current_fps + '
Playback Rate: ' + bitrate + ' Kbps') el.textContent = utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'; } else { - stat_el.setAttribute('original-title', 'Stream Latency
Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps'); - - delay = stats.hlsLatencyBroadcaster; - var pos = delay.lastIndexOf('.'); - - if ( pos === -1 ) + stat_el.setAttribute('original-title', 'Stream Latency
Video: ' + stats.vid_width + 'x' + stats.vid_height + 'p @ ' + stats.current_fps + '
Playback Rate: ' + stats.current_bitrate + ' Kbps'); + delay = delay.toString(); + var ind = delay.indexOf('.'); + if ( ind === -1 ) delay = delay + '.00'; - else if ( delay.length - pos < 3 ) + else if ( ind >= delay.length - 2 ) delay = delay + '0'; el.textContent = delay + 's'; @@ -651,19 +664,6 @@ FFZ.prototype._modify_cindex = function(view) { } el.innerHTML = utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1 || f.settings.stream_uptime === 3); - }, - - ffzTeardown: function() { - var id = this.get('controller.content.id') || this.get('controller.id'); - if ( id ) - f.ws_send("unsub", "channel." + id); - - this.get('element').setAttribute('data-channel', ''); - f._cindex = undefined; - if ( this._ffz_update_uptime ) - clearTimeout(this._ffz_update_uptime); - - utils.update_css(f._channel_style, id, null); } }); } @@ -686,6 +686,28 @@ FFZ.settings_info.auto_theater = { }; +FFZ.settings_info.small_player = { + type: "boolean", + value: false, + no_mobile: true, + no_bttv: true, + + category: "Appearance", + name: "Mini-Player on Scroll", + help: "When you scroll down on the page, shrink the player and put it in the upper right corner so you can still watch.", + + on_update: function(val) { + if ( ! val ) + return document.body.classList.remove('ffz-small-player'); + + else if ( this._vodc ) + this._vodc.ffzOnScroll(); + else if ( this._cindex ) + this._cindex.ffzOnScroll(); + } +} + + FFZ.settings_info.chatter_count = { type: "boolean", value: false, diff --git a/src/ember/chat-input.js b/src/ember/chat-input.js index f1a37c7c..350d6542 100644 --- a/src/ember/chat-input.js +++ b/src/ember/chat-input.js @@ -210,35 +210,13 @@ FFZ.settings_info.input_emoji = { FFZ.prototype.setup_chat_input = function() { this.log("Hooking the Ember Chat Input component."); - var Input = utils.ember_resolve('component:chat/twitch-chat-input'), - f = this; - - if ( ! Input ) - return this.log("Unable to get Chat Input component."); - - this._modify_chat_input(Input); - - try { Input.create().destroy() - } catch(err) { } - - var views = utils.ember_views(); - for(var key in views) { - var v = views[key]; - if ( v instanceof Input ) { - this.log("Manually modifying Chat Input component.", v); - if ( ! v.ffzInit ) - this._modify_chat_input(v); - - v.ffzInit(); - } - } + this.update_views("component:chat/twitch-chat-input", this.modify_chat_input); } -FFZ.prototype._modify_chat_input = function(component) { +FFZ.prototype.modify_chat_input = function(component) { var f = this; - - component.reopen({ + utils.ember_reopen_view(component, { ffz_mru_index: -1, ffz_current_suggestion: 0, ffz_partial_word: '', @@ -249,22 +227,7 @@ FFZ.prototype._modify_chat_input = function(component) { ffz_name_suggestions: [], ffz_chatters: [], - didInsertElement: function() { - this._super(); - - try { - this.ffzInit(); - } catch(err) { f.error("ChatInput didInsertElement: " + err); } - }, - - willClearRender: function() { - try { - this.ffzTeardown(); - } catch(err) { f.error("ChatInput willClearRender: " + err); } - return this._super(); - }, - - ffzInit: function() { + ffz_init: function() { f._inputv = this; var s = this._ffz_minimal_style = document.createElement('style'); @@ -290,7 +253,7 @@ FFZ.prototype._modify_chat_input = function(component) { setTimeout(this.ffzResizeInput.bind(this), 500); }, - ffzTeardown: function() { + ffz_destroy: function() { if ( f._inputv === this ) f._inputv = undefined; @@ -322,11 +285,10 @@ FFZ.prototype._modify_chat_input = function(component) { return null; var t = this, - el = document.createElement('div'), - inner = document.createElement('div'), + el = utils.createElement('div', 'suggestion'), + inner = utils.createElement('div'), width = item.width ? (246 - item.width) + 'px' : null; - el.className = 'suggestion'; el.setAttribute('data-id', i); el.classList.toggle('ffz-is-favorite', item.favorite || false); @@ -342,7 +304,7 @@ FFZ.prototype._modify_chat_input = function(component) { el.appendChild(inner); if ( f.settings.input_complete_emotes && item.info ) { - var info = document.createElement('span'); + var info = utils.createElement('span'); info.innerHTML = item.info; el.classList.add('has-info'); if ( width ) @@ -394,8 +356,7 @@ FFZ.prototype._modify_chat_input = function(component) { current = this.get('ffz_current_suggestion') || 0; if ( ! el ) { - el = this.ffz_suggestions_el = document.createElement('div'); - el.className = 'suggestions ffz-suggestions'; + el = this.ffz_suggestions_el = utils.createElement('div', 'suggestions ffz-suggestions'); this.get('element').appendChild(el); } else @@ -430,8 +391,7 @@ FFZ.prototype._modify_chat_input = function(component) { } if ( ! added ) { - var item_el = document.createElement('div'); - item_el.className = 'suggestion disabled'; + var item_el = utils.createElement('div', 'suggestion disabled'); item_el.textContent = 'No matches.'; el.appendChild(item_el); } diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 5ae03593..8bc35a1c 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -107,6 +107,7 @@ FFZ.settings_info.chat_batching = { type: "select", options: { 0: "No Batching", + 125: "Minimal (0.125s)", 250: "Minor (0.25s)", 500: "Normal (0.5s)", 750: "Large (0.75s)", @@ -474,36 +475,7 @@ FFZ.prototype.setup_chatview = function() { this.log("Hooking the Ember Chat view."); - - var Chat = utils.ember_resolve('view:chat'); - this._modify_cview(Chat); - - // For some reason, this doesn't work unless we create an instance of the - // chat view and then destroy it immediately. - try { - Chat.create().destroy(); - } catch(err) { } - - - // Modify all existing Chat views. - var views = utils.ember_views(); - for(var key in views) { - if ( ! views.hasOwnProperty(key) ) - continue; - - var view = views[key]; - if ( !(view instanceof Chat) ) - continue; - - 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.update_views('view:chat', this.modify_chat_view); } @@ -511,35 +483,10 @@ FFZ.prototype.setup_chatview = function() { // Modify Chat View // -------------------- -FFZ.prototype._modify_cview = function(view) { +FFZ.prototype.modify_chat_view = function(view) { var f = this; - - view.reopen({ - didInsertElement: function() { - this._super(); - - try { - this.ffzInit(); - } catch(err) { - f.error("view:chat ffzInit error: " + err); - } - }, - - didUpdate: function() { - this._super(); - f.log("view:chat didUpdate", this) - }, - - willClearRender: function() { - try { - this.ffzTeardown(); - } catch(err) { - f.error("view:chat ffzTeardown error: " + err); - } - this._super(); - }, - - ffzInit: function() { + utils.ember_reopen_view(view, { + ffz_init: function() { f._chatv = this; var room_id = this.get('controller.currentRoom.id'), @@ -567,7 +514,7 @@ FFZ.prototype._modify_cview = function(view) { }, 1000); }, - ffzTeardown: function() { + ffz_destroy: function() { if ( f._chatv === this ) f._chatv = null; @@ -868,10 +815,9 @@ FFZ.prototype._modify_cview = function(view) { chan_table = this._ffz_chan_table || room_list.querySelector('#ffz-channel-table tbody'); if ( ! chan_table ) { - var tbl = document.createElement('table'); + var tbl = utils.createElement('table', 'ffz'); tbl.setAttribute('cellspacing', '0'); tbl.id = 'ffz-channel-table'; - tbl.className = 'ffz'; tbl.innerHTML = 'ChannelsPin'; room_list.insertBefore(tbl, room_list.firstChild); @@ -914,10 +860,9 @@ FFZ.prototype._modify_cview = function(view) { // Group Chat Table var group_table = this._ffz_group_table || room_list.querySelector('#ffz-group-table tbody'); if ( ! group_table ) { - var tbl = document.createElement('table'); + var tbl = utils.createElement('table', 'ffz'); tbl.setAttribute('cellspacing', '0'); tbl.id = 'ffz-group-table'; - tbl.className = 'ffz'; tbl.innerHTML = 'Group Chats'; var before = room_list.querySelector('#ffz-channel-table'); @@ -1007,8 +952,7 @@ FFZ.prototype._modify_cview = function(view) { this.classList.toggle('active', !is_pinned); }); } else { - btn = document.createElement('a'); - btn.className = 'leave-chat html-tooltip'; + btn = utils.createElement('a', 'leave-chat html-tooltip'); btn.innerHTML = constants.CLOSE; btn.title = 'Leave Group'; @@ -1085,12 +1029,10 @@ FFZ.prototype._modify_cview = function(view) { if ( f.has_bttv || ! f.settings.group_tabs ) return; - var link = document.createElement('a'), + var link = utils.createElement('a', 'button button--icon-only'), view = this; - // Chat Room Management Button - link.className = 'button button--icon-only'; link.title = "Chat Room Management"; link.innerHTML = '
' + constants.ROOMS + '
'; @@ -1105,8 +1047,7 @@ FFZ.prototype._modify_cview = function(view) { // Invite Button - link = document.createElement('a'), - link.className = 'button button--icon-only html-tooltip invite'; + link = utils.createElement('a', 'button button--icon-only html-tooltip invite'); link.title = "Invite a User"; link.innerHTML = '
' + constants.INVITE + '
'; @@ -1216,6 +1157,10 @@ FFZ.prototype._modify_cview = function(view) { now = Date.now(); + // Non-Existant Rooms + if ( ! room ) + return false; + if ( is_current || is_channel || room_id === this._ffz_host || f.settings.group_tabs === 3 ) // Important Tabs return true; diff --git a/src/ember/conversations.js b/src/ember/conversations.js index 55e1bb9d..2fe165fa 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -71,42 +71,16 @@ FFZ.prototype.setup_conversations = function() { document.body.classList.toggle('ffz-minimize-conversations', this.settings.minimize_conversations); document.body.classList.toggle('ffz-theatre-conversations', this.settings.hide_conversations_in_theatre); - var ConvWindow = utils.ember_resolve('component:twitch-conversations/conversation-window'); - if ( ConvWindow ) { - this.log("Hooking the Ember Conversation Window component."); - this._modify_conversation_window(ConvWindow); - try { ConvWindow.create().destroy() } - catch(err) { } - } else - this.log("Unable to resolve: component:twitch-conversations/conversation-window"); - - - var ConvSettings = utils.ember_resolve('component:twitch-conversations/conversation-settings-menu'); - if ( ConvSettings ) { - this.log("Hooking the Ember Conversation Settings Menu component."); - this._modify_conversation_menu(ConvSettings); - try { ConvSettings.create().destroy() } - catch(err) { } - } else - this.log("Unable to resolve: component:twitch-conversations/conversation-settings-menu"); - - - var ConvLine = utils.ember_resolve('component:twitch-conversations/conversation-line'); - if ( ConvLine ) { - this.log("Hooking the Ember Conversation Line component."); - this._modify_conversation_line(ConvLine); - try { ConvLine.create().destroy() } - catch(err) { } - } else - this.log("Unable to resolve: component:twitch-conversations/conversation-line"); + this.update_views('component:twitch-conversations/conversation-window', this.modify_conversation_window); + this.update_views('component:twitch-conversations/conversation-settings-menu', this.modify_conversation_menu); + this.update_views('component:twitch-conversations/conversation-line', this.modify_conversation_line); } -FFZ.prototype._modify_conversation_menu = function(component) { +FFZ.prototype.modify_conversation_menu = function(component) { var f = this; - - component.reopen({ - didInsertElement: function() { + utils.ember_reopen_view(component, { + ffz_init: function() { var user = this.get('thread.otherUsername'), el = this.get('element'), sections = el && el.querySelectorAll('.options-section'); @@ -132,11 +106,11 @@ FFZ.prototype._modify_conversation_menu = function(component) { } -FFZ.prototype._modify_conversation_window = function(component) { +FFZ.prototype.modify_conversation_window = function(component) { var f = this, Layout = utils.ember_lookup('service:layout'); - component.reopen({ + utils.ember_reopen_view(component, { headerBadges: Ember.computed("thread.participants", "currentUsername", function() { return []; }), @@ -154,7 +128,7 @@ FFZ.prototype._modify_conversation_window = function(component) { badge_el.innerHTML = f.render_badges(badges); }.observes('ffzHeaderBadges'), - didInsertElement: function() { + ffz_init: function() { var el = this.get('element'), header = el && el.querySelector('.conversation-header'), header_name = header && header.querySelector('.conversation-header-name'), @@ -178,11 +152,11 @@ FFZ.prototype._modify_conversation_window = function(component) { } -FFZ.prototype._modify_conversation_line = function(component) { +FFZ.prototype.modify_conversation_line = function(component) { var f = this, Layout = utils.ember_lookup('service:layout'); - component.reopen({ + utils.ember_reopen_view(component, { tokenizedMessage: function() { try { return f.tokenize_conversation_line(this.get('message')); @@ -204,7 +178,7 @@ FFZ.prototype._modify_conversation_line = function(component) { }, didUpdate: function() { this.ffzRender() }, - didInsertElement: function() { this.ffzRender() }, + ffz_init: function() { this.ffzRender() }, ffzRender: function() { var el = this.get('element'), diff --git a/src/ember/directory.js b/src/ember/directory.js index 47928037..ae10ddab 100644 --- a/src/ember/directory.js +++ b/src/ember/directory.js @@ -77,14 +77,44 @@ FFZ.settings_info.directory_group_hosts = { }; -FFZ.settings_info.banned_games = { - type: "button", - value: [], +FFZ.settings_info.enable_recommended_vods = { + type: "boolean", + value: true, category: "Directory", no_mobile: true, - name: "Banned Games", - help: "A list of games that will not be displayed in the Directory.", + experiment_warn: true, + + name: 'Show Twitch\'s Recommended Videos', + help: 'Show the "Based on your Viewing History" section of the directory rather than Most Recent Videos.', + + on_update: function(val) { + Ember.propertyDidChange(utils.ember_lookup('service:vod-coviews'), 'areVodsViewable'); + } +} + + +FFZ.settings_info.recommended_above_hosts = { + type: "boolean", + value: function() { var s = utils.ember_lookup('service:vod-coviews'); return s && s.get('isFollowingAboveHost') }, + + category: "Directory", + no_mobile: true, + experiment_warn: true, + + name: "Show Twitch's Recommended Videos above Hosts", + help: 'Enable this to place the "Based on your Viewing History" section above Live Hosts.', + + on_update: function(val) { + Ember.propertyDidChange(utils.ember_lookup('service:vod-coviews'), 'isFollowingAboveHost'); + //utils.ember_lookup('service:vod-coviews').set('isFollowingAboveHost', val); + } +} + + +FFZ.settings_info.banned_games = { + visible: false, + value: [], on_update: function() { var banned = this.settings.banned_games, @@ -96,34 +126,14 @@ FFZ.settings_info.banned_games = { el.classList.toggle('ffz-game-banned', banned.indexOf(game && game.toLowerCase()) !== -1); } - }, - - method: function() { - var f = this, - old_val = f.settings.banned_games.join(", "); - - utils.prompt( - "Banned Games", - "Please enter a comma-separated list of games that you would like to be banned from viewing in the Directory.

This is case insensitive, however you must type the full name.

Example: League of Legends, Dota 2, Smite", - old_val, - function(new_val) { - if ( new_val === null || new_val === undefined ) - return; - f.settings.set("banned_games", _.unique(new_val.trim().toLowerCase().split(/\s*,\s*/))); - }, 600); } } FFZ.settings_info.spoiler_games = { - type: "button", + visible: false, value: [], - category: "Directory", - no_mobile: true, - name: "No-Thumbnail Games", - help: "Stream and video thumbnails will be hidden for games that you add to this list.", - on_update: function() { var spoiled = this.settings.spoiler_games, els = document.querySelectorAll('.ffz-directory-preview'); @@ -134,21 +144,6 @@ FFZ.settings_info.spoiler_games = { el.classList.toggle('ffz-game-spoilered', spoiled.indexOf(game && game.toLowerCase()) !== -1); } - }, - - method: function() { - var f = this, - old_val = f.settings.spoiler_games.join(", "); - - utils.prompt( - "No-Thumbnail Games", - "Please enter a comma-separated list of games that you would like to have the thumbnails hidden for in the Directory.

This is case insensitive, however you must type the full name.

Example: Undertale", - old_val, - function(new_val) { - if ( new_val === null || new_val === undefined ) - return; - f.settings.set("spoiler_games", _.unique(new_val.trim().toLowerCase().split(/\s*,\s*/))); - }, 600); } } @@ -198,88 +193,46 @@ 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); + var f = this, + VodCoviews = utils.ember_lookup('service:vod-coviews'); + + if ( VodCoviews ) { + VodCoviews.reopen({ + // checkExperiment likes setting this back. Don't let it. + isFollowingAboveHost: Ember.computed('_ffz', { + get: function(key) { + return f.settings.recommended_above_hosts; + }, + set: function(key, val) { + return f.settings.recommended_above_hosts; + } + }), + + areVodsViewable: function() { + var filtered = this.get('filteredVods'); + return f.settings.enable_recommended_vods && filtered && filtered.length > 0; + }.property('filteredVods') + }); + + Ember.propertyDidChange(VodCoviews, 'isFollowingAboveHost'); + Ember.propertyDidChange(VodCoviews, 'areVodsViewable'); + + } else + this.log("Unable to locate the Ember service:vod-coviews"); + + this.log("Attempting to modify the Following collection."); this._modify_following(); this.log("Hooking the Ember Directory views."); - var ChannelView = utils.ember_resolve('component:stream-preview'); - if ( ChannelView ) { - this._modify_directory_live(ChannelView, false); - try { - ChannelView.create().destroy(); - } catch(err) { } - } else - this.log("Unable to locate the Ember component:stream-preview"); + this.update_views('component:stream-preview', function(x) { this.modify_directory_live(x, false) }, true); + this.update_views('component:creative-preview', function(x) { this.modify_directory_live(x, false) }, true); + this.update_views('component:csgo-channel-preview', function(x) { this.modify_directory_live(x, true) }, true); + this.update_views('component:host-preview', this.modify_directory_host, true); + this.update_views('component:video-preview', this.modify_video_preview, true); - var CreativeChannel = utils.ember_resolve('component:creative-preview'); - if ( CreativeChannel ) { - this._modify_directory_live(CreativeChannel, false); - try { - CreativeChannel.create().destroy(); - } catch(err) { } - } else - this.log("Unable to locate the Ember component:creative-preview"); - - var CSGOChannel = utils.ember_resolve('component:csgo-channel-preview'); - CSGOChannel = this._modify_directory_live(CSGOChannel, true, 'component:csgo-channel-preview'); - try { - CSGOChannel.create().destroy(); - } catch(err) { } - - var HostView = utils.ember_resolve('component:host-preview'); - HostView = this._modify_directory_host(HostView); - try { - HostView.create().destroy(); - } catch(err) { } - - - var VideoPreview = utils.ember_resolve('component:video-preview'); - if ( VideoPreview ) { - this._modify_video_preview(VideoPreview); - try { VideoPreview.create().destroy(); - } catch(err) { } - } else - this.log("Unable to locate the Ember component:video-preview"); - - - var GameFollow = utils.ember_resolve('component:game-follow-button'); - if ( GameFollow ) { - this._modify_game_follow(GameFollow); - try { GameFollow.create().destroy() } - catch(err) { } - } else - this.log("Unable to locate the Ember component:game-follow-button"); - - - // Initialize existing views. - var views = utils.ember_views(); - for(var key in views) { - var view = views[key]; - if ( (ChannelView && view instanceof ChannelView) || (CreativeChannel && view instanceof CreativeChannel) ) { - if ( ! view.ffzInit ) - this._modify_directory_live(view, false); - } else if ( CSGOChannel && view instanceof CSGOChannel ) { - if ( ! view.ffzInit ) - this._modify_directory_live(view, true); - } else if ( view instanceof HostView || view.get('tt_content') === 'live_host' ) { - if ( ! view.ffzInit ) - this._modify_directory_host(view); - } else if ( VideoPreview && view instanceof VideoPreview ) { - if ( ! view.ffzInit ) - this._modify_video_preview(view); - } else if ( GameFollow && view instanceof GameFollow ) { - if ( ! view.ffzInit ) - this._modify_game_follow(view); - } else - continue; - - try { - view.ffzInit(); - } catch(err) { - this.error("Directory Setup: " + err); - } - } + this.update_views('component:game-follow-button', this.modify_game_follow_button); } @@ -407,15 +360,10 @@ FFZ.prototype._modify_following = function() { } -FFZ.prototype._modify_game_follow = function(component) { +FFZ.prototype.modify_game_follow_button = function(component) { var f = this; - component.reopen({ - didInsertElement: function() { - this._super(); - this.ffzInit(); - }, - - ffzInit: function() { + utils.ember_reopen_view(component, { + ffz_init: function() { var el = this.get('element'), game = this.get('game.id').toLowerCase(), @@ -436,37 +384,33 @@ FFZ.prototype._modify_game_follow = function(component) { }; // Block Button - var block = utils.createElement('div', 'follow-button ffz-block-button'), - block_link = utils.createElement('a', 'tooltip follow block'); - + var block = utils.createElement('button', 'button tooltip ffz-block-button'), update_block = function() { var is_blocked = f.settings.banned_games.indexOf(game) !== -1; - block_link.classList.toggle('active', is_blocked); + block.classList.toggle('active', is_blocked); - block_link.innerHTML = '' + (is_blocked ? 'Unblock' : 'Block') + ''; - block_link.title = 'Click to ' + (is_blocked ? 'unblock' : 'block') + " this game.\n\nBlocking a game hides all the streams and videos of the game when you're not viewing it directly."; + block.innerHTML = (is_blocked ? 'Unblock' : 'Block'); + block.title = 'Click to ' + (is_blocked ? 'unblock' : 'block') + " this game.\n\nBlocking a game hides all the streams and videos of the game when you're not viewing it directly."; + jQuery(block).trigger('mouseout').trigger('mouseover'); }; update_block(); - block_link.addEventListener('click', click_button('banned_games', update_block)); - block.appendChild(block_link); + block.addEventListener('click', click_button('banned_games', update_block)); el.appendChild(block); // Spoiler Button - var spoiler = utils.createElement('div', 'follow-button ffz-spoiler-button'), - spoiler_link = utils.createElement('a', 'tooltip follow spoiler'), - + var spoiler = utils.createElement('button', 'button tooltip ffz-spoiler-button'), update_spoiler = function() { var is_spoiled = f.settings.spoiler_games.indexOf(game) !== -1; - spoiler_link.classList.toggle('active', is_spoiled); + spoiler.classList.toggle('active', is_spoiled); - spoiler_link.innerHTML = '' + (is_spoiled ? 'Show Thumbnails' : 'Hide Thumbnails') + ''; - spoiler_link.title = 'Click to ' + (is_spoiled ? 'show' : 'hide') + " thumbnails for this game.\n\nHiding thumbnails for a game will help you avoid spoilers for a game that you haven't played yet."; + spoiler.innerHTML = (is_spoiled ? 'Show Thumbnails' : 'Hide Thumbnails'); + spoiler.title = 'Click to ' + (is_spoiled ? 'show' : 'hide') + " thumbnails for this game.\n\nHiding thumbnails for a game will help you avoid spoilers for a game that you haven't played yet."; + jQuery(spoiler).trigger('mouseout').trigger('mouseover'); } update_spoiler(); - spoiler_link.addEventListener('click', click_button('spoiler_games', update_spoiler)); - spoiler.appendChild(spoiler_link); + spoiler.addEventListener('click', click_button('spoiler_games', update_spoiler)); el.appendChild(spoiler); jQuery('.tooltip', el).tipsy(); @@ -475,17 +419,12 @@ FFZ.prototype._modify_game_follow = function(component) { } -FFZ.prototype._modify_directory_live = function(dir, is_csgo, component_name) { +FFZ.prototype.modify_directory_live = function(component, is_csgo) { var f = this, pref = is_csgo ? 'channel.' : 'stream.'; - var mutator = { - didInsertElement: function() { - this._super(); - this.ffzInit(); - }, - - ffzInit: function() { + utils.ember_reopen_view(component, { + ffz_init: function() { var el = this.get('element'), meta = el && el.querySelector('.meta'), thumb = el && el.querySelector('.thumb'), @@ -532,12 +471,12 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo, component_name) { if ( e.button !== 0 || e.altKey || e.ctrlKey || e.shiftKey || e.metaKey ) return; - var Channel = utils.ember_resolve('model:channel'); + var Channel = utils.ember_resolve('model:deprecated-channel'); if ( ! Channel ) return; - e.preventDefault(); utils.ember_lookup('router:main').transitionTo('channel.index', Channel.find({id: channel_id}).load()); + e.preventDefault(); return false; }); @@ -546,7 +485,7 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo, component_name) { } }, - willClearRender: function() { + ffz_destroy: function() { if ( this._ffz_uptime ) { this._ffz_uptime.parentElement.removeChild(this._ffz_uptime); this._ffz_uptime = null; @@ -557,8 +496,6 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo, component_name) { if ( this._ffz_image_timer ) clearInterval(this._ffz_image_timer); - - this._super(); }, ffzRotateImage: function() { @@ -587,32 +524,14 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo, component_name) { this._ffz_uptime.innerHTML = ''; } } - }; - - if ( dir ) - dir.reopen(mutator); - else { - dir = Ember.Component.extend(mutator); - App.__deprecatedInstance__.registry.register(component_name, dir); - } - - return dir; + }); } -FFZ.prototype._modify_video_preview = function(vp) { +FFZ.prototype.modify_video_preview = function(component) { var f = this; - vp.reopen({ - didInsertElement: function() { - this._super(); - try { - this.ffzInit(); - } catch(err) { - f.error("component:video-preview ffzInit: " + err); - } - }, - - ffzInit: function() { + utils.ember_reopen_view(component, { + ffz_init: function() { var el = this.get('element'), game = this.get('video.game'), @@ -659,30 +578,11 @@ FFZ.prototype._modify_video_preview = function(vp) { } -FFZ.prototype._modify_directory_host = function(dir) { - var f = this, mutator; - - mutator = { - didInsertElement: function() { - this._super(); - try { - this.ffzInit(); - } catch(err) { - f.error("component:host-preview ffzInit: " + err); - } - }, - - willClearRender: function() { - this._super(); - try { - this.ffzCleanup(); - } catch(err) { - f.error("component:host-preview ffzCleanup: " + err); - } - }, - +FFZ.prototype.modify_directory_host = function(component) { + var f = this; + utils.ember_reopen_view(component, { ffzVisitChannel: function(target, e) { - var Channel = utils.ember_resolve('model:channel'); + var Channel = utils.ember_resolve('model:deprecated-channel'); if ( ! Channel ) return; @@ -773,7 +673,7 @@ FFZ.prototype._modify_directory_host = function(dir) { this.$('.thumb .cap img').attr('src', url); }, - ffzCleanup: function() { + ffz_destroy: function() { var target = this.get('stream.target.channel'); if ( f._popup && f._popup.classList.contains('ffz-channel-selector') && f._popup.getAttribute('data-channel') === target ) f.close_popup(); @@ -782,7 +682,7 @@ FFZ.prototype._modify_directory_host = function(dir) { clearInterval(this._ffz_image_timer); }, - ffzInit: function() { + ffz_init: function() { var el = this.get('element'), meta = el && el.querySelector('.meta'), thumb = el && el.querySelector('.thumb'), @@ -839,14 +739,5 @@ FFZ.prototype._modify_directory_host = function(dir) { cap.addEventListener('click', this.ffzShowHostMenu.bind(this)); } } - }; - - if ( dir ) - dir.reopen(mutator); - else { - dir = Ember.Component.extend(mutator); - App.__deprecatedInstance__.registry.register('component:host-preview', dir); - } - - return dir; + }); } \ No newline at end of file diff --git a/src/ember/feed-card.js b/src/ember/feed-card.js index 11d2a157..5600e24c 100644 --- a/src/ember/feed-card.js +++ b/src/ember/feed-card.js @@ -21,15 +21,8 @@ var FFZ = window.FrankerFaceZ, FFZ.prototype.setup_feed_cards = function() { - var FeedCard = utils.ember_resolve('component:channel-feed/card'); - if ( ! FeedCard ) - return this.error("Unable to locate component:channel-feed/card"); - - this.log("Modifying the feed-card component."); - this._modify_feed_card(FeedCard); - - try { FeedCard.create().destroy(); - } catch(err) { } + this.update_views('component:channel-feed/card', this.modify_feed_card); + this.update_views('component:channel-feed/comment', this.modify_feed_comment); this.rerender_feed_cards(); } @@ -37,6 +30,7 @@ FFZ.prototype.setup_feed_cards = function() { FFZ.prototype.rerender_feed_cards = function(for_set) { var FeedCard = utils.ember_resolve('component:channel-feed/card'), + FeedComment = utils.ember_resolve('component:channel-feed/comment'), views = utils.ember_views(); if ( ! FeedCard ) @@ -46,30 +40,31 @@ FFZ.prototype.rerender_feed_cards = function(for_set) { var view = views[view_id]; if ( view instanceof FeedCard ) { try { - if ( ! view.ffzInit ) - this._modify_feed_card(view); - view.ffzInit(for_set); + if ( ! view.ffz_init ) + this.modify_feed_card(view); + view.ffz_init(for_set); } catch(err) { - this.error("setup component:channel-feed/card ffzInit: " + err) + this.error("setup component:channel-feed/card ffzInit", err) + } + } + + if ( FeedComment && view instanceof FeedComment ) { + try { + if ( ! view.ffz_init ) + this.modify_feed_comment(view); + view.ffz_init(for_set); + } catch(err) { + this.error("setup component:channel-feed/comment ffzInit", err); } } } } -FFZ.prototype._modify_feed_card = function(component) { +FFZ.prototype.modify_feed_card = function(component) { var f = this; - component.reopen({ - didInsertElement: function() { - this._super(); - try { - this.ffzInit(); - } catch(err) { - f.error("component:channel-feed/card ffzInit: " + err); - } - }, - - ffzInit: function(for_set) { + utils.ember_reopen_view(component, { + ffz_init: function(for_set) { var el = this.get('element'), message = this.get('post.body'), emotes = parse_emotes(this.get('post.emotes')), @@ -96,4 +91,34 @@ FFZ.prototype._modify_feed_card = function(component) { //jQuery('.html-tooltip', pbody).tipsy({html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); } }); +} + + +FFZ.prototype.modify_feed_comment = function(component) { + var f = this; + utils.ember_reopen_view(component, { + ffz_init: function(for_set) { + var el = this.get('element'), + message = this.get('comment.body'), + emotes = parse_emotes(this.get('comment.emotes')), + user_id = this.get('comment.user.login'), + room_id = this.get('parentView.parentView.channelId') || this.get('parentView.parentView.post.user.login') || null, + pbody = el && el.querySelector('.activity-body'); + + if ( ! message || ! el || ! pbody ) + return; + + // If this is for a specific emote set, only rerender if it matters. + if ( for_set && f.rooms && f.rooms[room_id] ) { + var sets = f.getEmotes(user_id, room_id); + if ( sets.indexOf(for_set) === -1 ) + return; + } + + var tokens = f.tokenize_feed_body(message, emotes, user_id, room_id), + output = f.render_tokens(tokens, true, false); + + pbody.innerHTML = '

' + output + '

'; + } + }) } \ No newline at end of file diff --git a/src/ember/following.js b/src/ember/following.js index c5aeef6c..229c4c62 100644 --- a/src/ember/following.js +++ b/src/ember/following.js @@ -167,7 +167,7 @@ FFZ.prototype.setup_profile_following = function() { // Refresh all existing following data. var count = 0, - Channel = utils.ember_resolve('model:channel'); + Channel = utils.ember_resolve('model:deprecated-channel'); if ( Channel && Channel._cache ) for(var key in Channel._cache) { diff --git a/src/ember/layout.js b/src/ember/layout.js index 05f0c89f..cce6a7e1 100644 --- a/src/ember/layout.js +++ b/src/ember/layout.js @@ -240,16 +240,26 @@ FFZ.prototype.setup_layout = function() { }.observes("isTooSmallForRightColumn"), ffzUpdateCss: function() { - var out = ''; + var window_height = this.get('windowHeight'), + window_width = this.get('windowWidth'), + out = 'body.ffz-small-player #player .dynamic-player {' + + 'position: fixed;' + + 'z-index: 9;' + + 'box-shadow: 0 0 20px 0 black;'; + + if ( .25 * window_width >= .5 * window_height ) + out += 'width: 25vw !important; height: 14.0625vw !important;'; + else + out += 'width: 50vh !important; height: 28.125vh !important;'; + if ( ! f.has_bttv ) { if ( this.get('isRightColumnClosed') ) - out = ''; + out += 'top: 0; right: 0}'; + else { if ( this.get('portraitMode') ) { var size = this.get('playerSize'), video_below = this.get('portraitVideoBelow'), - window_height = this.get('windowHeight'), - window_width = this.get('windowWidth'), video_height = size[1] + 120 + 60, chat_height = window_height - video_height, @@ -263,7 +273,8 @@ FFZ.prototype.setup_layout = function() { theatre_video_top = video_below ? theatre_chat_height : 0, theatre_chat_top = video_below ? 0 : theatre_video_height; - out = 'body[data-current-path^="user."] #left_col .warp { min-height: inherit }' + + out += 'top: ' + video_top + 'px;right: 0}' + + 'body[data-current-path^="user."] #left_col .warp { min-height: inherit }' + 'body[data-current-path^="user."] #left_col { overflow: hidden }' + 'body[data-current-path^="user."] #left_col .warp,' + 'body[data-current-path^="user."] #left_col,' + @@ -290,7 +301,9 @@ FFZ.prototype.setup_layout = function() { } else { var width = this.get('rightColumnWidth'); - out = '#main_col.expandRight #right_close{left: none !important}' + + + out += 'top: 0; right: ' + width + 'px}' + + '#main_col.expandRight #right_close{left: none !important}' + '#right_col{width:' + width + 'px}' + 'body:not(.ffz-sidebar-swap) #main_col:not(.expandRight){' + 'margin-right:' + width + 'px}' + @@ -317,7 +330,8 @@ FFZ.prototype.setup_layout = function() { ffzFixTabs: function() { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) { setTimeout(function() { - f._chatv && f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); + var cr = f._chatv && f._chatv.$('.chat-room'); + cr && cr.css && cr.css('top', f._chatv._ffz_tabs.offsetHeight + "px"); },0); } }.observes("isRightColumnClosed", "rightColumnWidth", "portraitMode", "playerSize") diff --git a/src/ember/line.js b/src/ember/line.js index ec691607..9a8b022b 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -18,9 +18,9 @@ FFZ.settings_info.alias_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); - } - }; + document.body.classList.toggle('ffz-alias-italics', val); + } +}; FFZ.settings_info.room_status = { type: "boolean", @@ -33,10 +33,10 @@ FFZ.settings_info.room_status = { 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(); - } - }; + if ( this._roomv ) + this._roomv.ffzUpdateStatus(); + } +}; FFZ.settings_info.replace_bad_emotes = { @@ -48,7 +48,19 @@ FFZ.settings_info.replace_bad_emotes = { name: "Fix Low Quality Twitch Global Emoticons", help: "Replace emoticons such as DansGame and RedCoat with cleaned up versions that don't have pixels around the edges or white backgrounds for nicer display on dark chat." - }; +}; + + +FFZ.settings_info.parse_emoticons = { + type: "boolean", + value: true, + + category: "Chat Appearance", + no_bttv: true, + + name: "Display Emoticons", + help: "Display emoticons in chat messages rather than just text." +}; FFZ.settings_info.parse_emoji = { @@ -74,9 +86,9 @@ FFZ.settings_info.parse_emoji = { category: "Chat Appearance", - name: "Emoji Display", + name: "Display Emoji", help: "Replace emoji in chat messages with nicer looking images from either Twitter or Google." - }; +}; FFZ.settings_info.scrollback_length = { @@ -354,6 +366,19 @@ FFZ.settings_info.old_sub_notices = { }; +FFZ.settings_info.emote_alignment = { + type: "boolean", + value: false, + + category: "Chat Appearance", + no_bttv: true, + + name: "Baseline Emoticon Alignment", + help: "Align emotes on the text baseline, making messages taller but ensuring emotes don't overlap.", + + on_update: function(val) { document.body.classList.toggle('ffz-baseline-emoticons', !this.has_bttv && val) } +}; + FFZ.settings_info.chat_padding = { type: "boolean", value: false, @@ -588,6 +613,7 @@ FFZ.prototype.setup_line = function() { // Chat Enhancements document.body.classList.toggle('ffz-alias-italics', this.settings.alias_italics); + document.body.classList.toggle('ffz-baseline-emoticons', !this.has_bttv && this.settings.emote_alignment); this.toggle_style('chat-setup', !this.has_bttv && (this.settings.chat_rows || this.settings.chat_separators || this.settings.highlight_messages_with_mod_card)); this.toggle_style('chat-padding', !this.has_bttv && this.settings.chat_padding); @@ -710,7 +736,7 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) { if ( is_whisper || this_ul >= other_ul || f.settings.mod_buttons.length === 0 ) return ''; - output = ''; + output = ''; for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) { var pair = f.settings.mod_buttons[i], @@ -720,22 +746,22 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) { if ( btn === false ) { if ( deleted ) - output += 'Unban'; + output += 'Unban'; else - output += 'Ban'; + output += 'Ban'; } else if ( btn === 600 ) - output += 'Timeout'; + output += 'Timeout'; else { if ( typeof btn === "string" ) { - cmd = btn.replace(/{user}/g, user).replace(/ * */, "\n"); + cmd = btn.replace(/{user}/g, user).replace(/{id}/g, this.get('msgObject.tags.id')).replace(/ * */, "\n"); tip = "Custom Command" + (cmd.indexOf("\n") !== -1 ? 's' : '') + '
' + utils.quote_san(cmd).replace('\n','
'); } else { cmd = "/timeout " + user + " " + btn; tip = "Timeout User (" + utils.duration_string(btn) + ")"; } - output += '' + prefix + ''; + output += '' + prefix + ''; } } @@ -770,18 +796,18 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) { // System Message if ( system_msg ) { output += '
' + utils.sanitize(system_msg) + '
'; - if ( this.get('shouldRenderMessageBody') === false ) + if ( this.get('ffzShouldRenderMessageBody') === false ) return output; } // Timestamp - output += '' + this.get('timestamp') + ' '; + output += '' + this.get('timestamp') + ' '; // Moderator Actions output += this.buildModIconsHTML(); // Badges - output += '' + f.render_badges(f.get_line_badges(this.get('msgObject'))) + ''; + output += '' + f.render_badges(f.get_line_badges(this.get('msgObject'))) + ''; // Alias Support var alias = f.aliases[user], @@ -842,7 +868,7 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) { } else output = ''; - output += f.render_tokens(this.get('ffzTokenizedMessage'), true, is_whisper && f.settings.filter_whispered_links && this.get("ffzUserLevel") < 4); + output += f.render_tokens(this.get('ffzTokenizedMessage'), true, is_whisper && f.settings.filter_whispered_links && this.get("ffzUserLevel") < 4, this.get('isBitsEnabled')); var old_messages = this.get('msgObject.ffz_old_messages'); if ( old_messages && old_messages.length ) @@ -856,7 +882,7 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) { output = this.buildSenderHTML(); // If this is a whisper, or if we should render the message body, render it. - if ( this.get('shouldRenderMessageBody') !== false ) + if ( this.get('ffzShouldRenderMessageBody') !== false ) if ( this.get('msgObject.deleted') ) output += this.buildDeletedMessageHTML() else @@ -865,6 +891,14 @@ FFZ.prototype._modify_chat_line = function(component, is_vod) { el.innerHTML = output; }, + ffzShouldRenderMessageBody: function() { + return ! this.get('hasSystemMsg') || this.get('hasMessageBody'); + }.property('hasSystemMsg', 'hasMessageBody'), + + shouldRenderMessageBody: function() { + return false; + }.property('hasSystemMsg', 'hasMessageBody'), + ffzWasDeleted: function() { return f.settings.prevent_clear && this.get("msgObject.ffz_deleted") }.property("msgObject.ffz_deleted"), @@ -883,8 +917,8 @@ FFZ.prototype._modify_chat_subline = function(component) { this._modify_chat_line(component); component.reopen({ - classNameBindings: [":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"], + classNameBindings: ["msgObject.style", "msgObject.ffz_has_mention:ffz-mentioned", "ffzWasDeleted:ffz-deleted", "ffzHasOldMessages:clearfix", "ffzHasOldMessages:ffz-has-deleted"], + attributeBindings: ["msgObject.tags.id:data-id", "msgObject.room:data-room", "msgObject.from:data-sender", "msgObject.deleted:data-deleted"], didInsertElement: function() { this.set('msgObject._line', this); @@ -1024,10 +1058,10 @@ FFZ.prototype._modify_vod_line = function(component) { if ( ! this.get("isViewerModeratorOrHigher") || this.get("isModeratorOrHigher") ) return ""; - return '' + + return '' + (this.get('msgObject.deleted') ? - '' : - 'Delete') + ''; + '' : + 'Delete') + ''; }, buildDeletedMesageHTML: function() { diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index 6d2aade5..d255ea90 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -353,7 +353,8 @@ FFZ.settings_info.mod_buttons = { "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 " + + "into the command, otherwise it will be appended to the end. Use {id} to insert the unique message ID into the command.

" + + "

Example: !permit \"!reg add {user}\" \"/timeout {user} 1 {id}\"

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.

" + @@ -583,7 +584,6 @@ FFZ.prototype.setup_mod_card = function() { helpers = window.require && window.require("web-client/helpers/chat/chat-line-helpers"); } catch(err) { } - this.log("Listening to the Settings controller to catch mod icon state changes."); var f = this, Settings = utils.ember_lookup('controller:settings'); @@ -594,7 +594,6 @@ FFZ.prototype.setup_mod_card = function() { f.settings.set('chat_mod_icon_visibility', 1); }); - this.log("Modifying Mousetrap stopCallback so we can catch ESC."); var orig_stop = Mousetrap.stopCallback; Mousetrap.stopCallback = function(e, element, combo) { @@ -609,11 +608,13 @@ FFZ.prototype.setup_mod_card = function() { el && el.classList.toggle('ffz-flip'); }); - this.log("Hooking the Ember Moderation Card view."); - var Card = utils.ember_resolve('component:chat/moderation-card'); + this.update_views('component:chat/moderation-card', this.modify_moderation_card); +} - Card.reopen({ +FFZ.prototype.modify_moderation_card = function(component) { + var f = this; + utils.ember_reopen_view(component, { ffzForceRedraw: function() { this.rerender(); if ( f.settings.mod_card_history ) @@ -667,402 +668,383 @@ FFZ.prototype.setup_mod_card = function() { return alias || this.get("cardInfo.user.display_name") || user_id.capitalize(); }), - willDestroy: function() { + ffz_destroy: function() { if ( f._mod_card === this ) f._mod_card = undefined; utils.update_css(f._chat_style, 'mod-card-highlight'); - - this._super(); }, - didInsertElement: function() { - this._super(); - try { - if ( f.has_bttv ) - return; + ffz_init: function() { + if ( f.has_bttv ) + return; - f._mod_card = this; + f._mod_card = this; - var el = this.get('element'), - controller = this.get('controller'), - t = this, - line, + var el = this.get('element'), + controller = this.get('controller'), + t = this, + line, - is_mod = controller.get('cardInfo.isModeratorOrHigher'), - ban_reasons, + is_mod = controller.get('cardInfo.isModeratorOrHigher'), + ban_reasons, - chat = utils.ember_lookup('controller:chat'), - user = f.get_user(), - room_id = chat && chat.get('currentRoom.id'), - is_broadcaster = user && room_id === user.login, + chat = utils.ember_lookup('controller:chat'), + user = f.get_user(), + room_id = chat && chat.get('currentRoom.id'), + is_broadcaster = user && room_id === user.login, - user_id = controller.get('cardInfo.user.id'), - alias = f.aliases[user_id], + user_id = controller.get('cardInfo.user.id'), + alias = f.aliases[user_id], - handle_key, + handle_key, - ban_reason = function() { - return ban_reasons && ban_reasons.value ? ' ' + ban_reasons.value : ""; - }; + ban_reason = function() { + return ban_reasons && ban_reasons.value ? ' ' + ban_reasons.value : ""; + }; - this.ffz_room_id = room_id; + this.ffz_room_id = room_id; - // Highlight this user's chat messages. - if ( f.settings.highlight_messages_with_mod_card ) - utils.update_css(f._chat_style, 'mod-card-highlight', styles['chat-user-bg'].replace(/{user_id}/g, user_id)); + // Highlight this user's chat messages. + if ( f.settings.highlight_messages_with_mod_card ) + utils.update_css(f._chat_style, 'mod-card-highlight', styles['chat-user-bg'].replace(/{user_id}/g, user_id)); - // Action Override - this.set('banAction', function(e) { - var room = utils.ember_lookup('controller:chat').get('currentRoom'); - room.send("/ban " + e.user + ban_reason(), true); - }); + // Action Override + this.set('banAction', function(e) { + var room = utils.ember_lookup('controller:chat').get('currentRoom'); + room.send("/ban " + e.user + ban_reason(), true); + }); - this.set('timeoutAction', function(e) { - var room = utils.ember_lookup('controller:chat').get('currentRoom'); - room.send("/timeout " + e.user + " 600 " + ban_reason(), true); - }); + this.set('timeoutAction', function(e) { + var room = utils.ember_lookup('controller:chat').get('currentRoom'); + room.send("/timeout " + e.user + " 600 " + ban_reason(), true); + }); - // Alias Display - if ( alias ) { - var name = el.querySelector('h3.name'), - link = name && name.querySelector('a'); + // Alias Display + if ( alias ) { + var name = el.querySelector('h3.name'), + link = name && name.querySelector('a'); - if ( link ) - name = link; - if ( name ) { - name.classList.add('ffz-alias'); - name.title = utils.sanitize(controller.get('cardInfo.user.display_name') || user_id.capitalize()); - jQuery(name).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - } + if ( link ) + name = link; + if ( name ) { + name.classList.add('ffz-alias'); + name.title = utils.sanitize(controller.get('cardInfo.user.display_name') || user_id.capitalize()); + jQuery(name).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } + } - // Style it! - el.classList.add('ffz-moderation-card'); + // Style it! + el.classList.add('ffz-moderation-card'); - // Info-tize it! - if ( f.settings.mod_card_info ) { - var info = document.createElement('div'), - after = el.querySelector('h3.name'); - if ( after ) { - el.classList.add('ffz-has-info'); - info.className = 'info channel-stats'; - after.parentElement.insertBefore(info, after.nextSibling); - this.ffzRebuildInfo(); - } + // Info-tize it! + if ( f.settings.mod_card_info ) { + var info = utils.createElement('div', 'info channel-stats'), + after = el.querySelector('h3.name'); + if ( after ) { + el.classList.add('ffz-has-info'); + after.parentElement.insertBefore(info, after.nextSibling); + this.ffzRebuildInfo(); } + } - // Additional Buttons - if ( is_mod && f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) { - line = document.createElement('div'); - line.className = 'extra-interface interface clearfix'; + // Additional Buttons + if ( is_mod && f.settings.mod_card_buttons && f.settings.mod_card_buttons.length ) { + line = utils.createElement('div', 'extra-interface interface clearfix'); - var cmds = {}, - add_btn_click = function(cmd) { - var user_id = controller.get('cardInfo.user.id'), - cont = utils.ember_lookup('controller:chat'), - room = cont && cont.get('currentRoom'), + var cmds = {}, + add_btn_click = function(cmd) { + var user_id = controller.get('cardInfo.user.id'), + cont = utils.ember_lookup('controller:chat'), + room = cont && cont.get('currentRoom'), - cm = cmd.replace(USER_REG, user_id), - reason = ban_reason(); + cm = cmd.replace(USER_REG, user_id), + reason = ban_reason(); - if ( reason ) { - var match = TO_REG.exec(cm); - if ( match ) { - if ( ! match[2] ) - cm += " 600"; - if ( ! match[3] ) - cm += reason; + if ( reason ) { + var match = TO_REG.exec(cm); + if ( match ) { + if ( ! match[2] ) + cm += " 600"; + if ( ! match[3] ) + cm += reason; - } else { - match = BAN_REG.exec(cm); - if ( match && ! match[2] ) { - cm += reason; - } + } else { + match = BAN_REG.exec(cm); + if ( match && ! match[2] ) { + cm += reason; } } + } - room && room.send(cm, true); - }, + room && room.send(cm, true); + }, - add_btn_make = function(cmd) { - var btn = utils.createElement('button', 'button ffz-no-bg'), - segment = cmd.split(' ', 1)[0], - title = cmds[segment] > 1 ? cmd.split(' ', cmds[segment]) : [segment]; + add_btn_make = function(cmd) { + var btn = utils.createElement('button', 'button ffz-no-bg'), + segment = cmd.split(' ', 1)[0], + title = cmds[segment] > 1 ? cmd.split(' ', cmds[segment]) : [segment]; - if ( /^[!~./]/.test(title[0]) ) - title[0] = title[0].substr(1); + if ( /^[!~./]/.test(title[0]) ) + title[0] = title[0].substr(1); - title = _.map(title, function(s){ return s.capitalize() }).join(' '); + title = _.map(title, function(s){ return s.capitalize() }).join(' '); - btn.innerHTML = utils.sanitize(title); - btn.title = utils.sanitize(cmd.replace(/{user}/g, controller.get('cardInfo.user.id') || '{user}')); + btn.innerHTML = utils.sanitize(title); + btn.title = utils.sanitize(cmd.replace(/{user}/g, controller.get('cardInfo.user.id') || '{user}')); - jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - btn.addEventListener('click', add_btn_click.bind(this, cmd)); - return btn; - }; + jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + btn.addEventListener('click', add_btn_click.bind(this, cmd)); + return btn; + }; - var cmds = {}; - for(var i=0; i < f.settings.mod_card_buttons.length; i++) - cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] = (cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] || 0) + 1; + var cmds = {}; + for(var i=0; i < f.settings.mod_card_buttons.length; i++) + cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] = (cmds[f.settings.mod_card_buttons[i].split(' ',1)[0]] || 0) + 1; - for(var i=0; i < f.settings.mod_card_buttons.length; i++) { - var cmd = f.settings.mod_card_buttons[i], - ind = cmd.indexOf('{user}'); + for(var i=0; i < f.settings.mod_card_buttons.length; i++) { + var cmd = f.settings.mod_card_buttons[i], + ind = cmd.indexOf('{user}'); - if ( ind === -1 ) - cmd += ' {user}'; + if ( ind === -1 ) + cmd += ' {user}'; - line.appendChild(add_btn_make(cmd)) + line.appendChild(add_btn_make(cmd)) + } + + el.appendChild(line); + } + + + // Key Handling + el.setAttribute('tabindex', 1); + if ( f.settings.mod_card_hotkeys ) { + el.classList.add('no-mousetrap'); + + handle_key = function(e) { + var key = e.keyCode || e.which, + user_id = controller.get('cardInfo.user.id'), + is_mod = controller.get('cardInfo.isModeratorOrHigher'), + room = utils.ember_lookup('controller:chat').get('currentRoom'); + + if ( is_mod && key == keycodes.P ) + room.send("/timeout " + user_id + " 1" + ban_reason(), true); + + else if ( is_mod && key == keycodes.B ) + room.send("/ban " + user_id + ban_reason(), true); + + else if ( is_mod && key == keycodes.T ) + room.send("/timeout " + user_id + " 600" + ban_reason(), true); + + else if ( is_mod && key == keycodes.U ) + room.send("/unban " + user_id, true); + + else if ( is_mod && ban_reasons && key == keycodes.R ) { + var event = document.createEvent('MouseEvents'); + event.initMouseEvent('mousedown', true, true, window); + ban_reasons.focus(); + ban_reasons.dispatchEvent(event); + return; + } + + else if ( key == keycodes.ESC && e.target === ban_reasons ) { + el.focus(); + return; + } + + else if ( key != keycodes.ESC ) + return; + + t.get('closeAction')(); + }; + + el.addEventListener('keyup', handle_key); + } + + + // Only do the big stuff if we're mod. + if ( is_mod ) { + el.classList.add('ffz-is-mod'); + + var btn_click = function(timeout) { + var user_id = controller.get('cardInfo.user.id'), + room = utils.ember_lookup('controller:chat').get('currentRoom'); + + if ( timeout === -1 ) + room.send("/unban " + user_id, true); + else + room.send("/timeout " + user_id + " " + timeout + ban_reason(), true); + }, + + btn_make = function(timeout) { + var btn = utils.createElement('button', 'button ffz-no-bg'); + btn.innerHTML = utils.duration_string(timeout); + btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); + + if ( f.settings.mod_card_hotkeys && timeout === 600 ) + btn.title = "(T)" + btn.title.substr(1); + else if ( f.settings.mod_card_hotkeys && timeout === 1 ) + btn.title = "(P)urge - " + btn.title; + + jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + + btn.addEventListener('click', btn_click.bind(this, timeout)); + return btn; + }; + + if ( f.settings.mod_card_durations && f.settings.mod_card_durations.length ) { + // Extra Moderation + line = utils.createElement('div', 'extra-interface interface clearfix'); + line.appendChild(btn_make(1)); + + var s = utils.createElement('span', 'right'); + line.appendChild(s); + + for(var i=0; i < f.settings.mod_card_durations.length; i++) + s.appendChild(btn_make(f.settings.mod_card_durations[i])); + + el.appendChild(line); + + // Fix Other Buttons + this.$("button.timeout").remove(); + } + + + if ( f.settings.mod_card_reasons && f.settings.mod_card_reasons.length ) { + // Moderation Reasons + line = utils.createElement('div', 'extra-interface interface clearfix'); + ban_reasons = utils.createElement('select', 'ffz-ban-reasons', ''); + line.appendChild(ban_reasons); + + for(var i=0; i < f.settings.mod_card_reasons.length; i++) { + var opt = utils.createElement('option'), r = f.settings.mod_card_reasons[i]; + opt.value = r; + opt.textContent = (i+1) + ') ' + r; + ban_reasons.appendChild(opt); } el.appendChild(line); } - // Key Handling - el.setAttribute('tabindex', 1); - if ( f.settings.mod_card_hotkeys ) { - el.classList.add('no-mousetrap'); + var ban_btn = el.querySelector('button.ban'); + if ( f.settings.mod_card_hotkeys ) + ban_btn.setAttribute('title', '(B)an User'); - handle_key = function(e) { - var key = e.keyCode || e.which, - user_id = controller.get('cardInfo.user.id'), - is_mod = controller.get('cardInfo.isModeratorOrHigher'), - room = utils.ember_lookup('controller:chat').get('currentRoom'); + // Unban Button + var unban_btn = utils.createElement('button', 'unban button button--icon-only light'); + unban_btn.innerHTML = '

' + CHECK + '
'; + unban_btn.title = (f.settings.mod_card_hotkeys ? "(U)" : "U") + "nban User"; - if ( is_mod && key == keycodes.P ) - room.send("/timeout " + user_id + " 1" + ban_reason(), true); + jQuery(unban_btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + unban_btn.addEventListener("click", btn_click.bind(this, -1)); - else if ( is_mod && key == keycodes.B ) - room.send("/ban " + user_id + ban_reason(), true); - - else if ( is_mod && key == keycodes.T ) - room.send("/timeout " + user_id + " 600" + ban_reason(), true); - - else if ( is_mod && key == keycodes.U ) - room.send("/unban " + user_id, true); - - else if ( is_mod && ban_reasons && key == keycodes.R ) { - var event = document.createEvent('MouseEvents'); - event.initMouseEvent('mousedown', true, true, window); - ban_reasons.focus(); - ban_reasons.dispatchEvent(event); - return; - } - - else if ( key == keycodes.ESC && e.target === ban_reasons ) { - el.focus(); - return; - } - - else if ( key != keycodes.ESC ) - return; - - t.get('closeAction')(); - }; - - el.addEventListener('keyup', handle_key); - } - - - // Only do the big stuff if we're mod. - if ( is_mod ) { - el.classList.add('ffz-is-mod'); - - var btn_click = function(timeout) { - var user_id = controller.get('cardInfo.user.id'), - room = utils.ember_lookup('controller:chat').get('currentRoom'); - - if ( timeout === -1 ) - room.send("/unban " + user_id, true); - else - room.send("/timeout " + user_id + " " + timeout + ban_reason(), true); - }, - - btn_make = function(timeout) { - var btn = document.createElement('button') - btn.className = 'button ffz-no-bg'; - btn.innerHTML = utils.duration_string(timeout); - btn.title = "Timeout User for " + utils.number_commas(timeout) + " Second" + (timeout != 1 ? "s" : ""); - - if ( f.settings.mod_card_hotkeys && timeout === 600 ) - btn.title = "(T)" + btn.title.substr(1); - else if ( f.settings.mod_card_hotkeys && timeout === 1 ) - btn.title = "(P)urge - " + btn.title; - - jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - - btn.addEventListener('click', btn_click.bind(this, timeout)); - return btn; - }; - - if ( f.settings.mod_card_durations && f.settings.mod_card_durations.length ) { - // Extra Moderation - line = document.createElement('div'); - line.className = 'extra-interface interface clearfix'; - - line.appendChild(btn_make(1)); - - var s = document.createElement('span'); - s.className = 'right'; - line.appendChild(s); - - for(var i=0; i < f.settings.mod_card_durations.length; i++) - s.appendChild(btn_make(f.settings.mod_card_durations[i])); - - el.appendChild(line); - - // Fix Other Buttons - this.$("button.timeout").remove(); - } - - - if ( f.settings.mod_card_reasons && f.settings.mod_card_reasons.length ) { - // Moderation Reasons - line = utils.createElement('div', 'extra-interface interface clearfix'); - ban_reasons = utils.createElement('select', 'ffz-ban-reasons', ''); - line.appendChild(ban_reasons); - - for(var i=0; i < f.settings.mod_card_reasons.length; i++) { - var opt = utils.createElement('option'), r = f.settings.mod_card_reasons[i]; - opt.value = r; - opt.textContent = (i+1) + ') ' + r; - ban_reasons.appendChild(opt); - } - - el.appendChild(line); - } - - - var ban_btn = el.querySelector('button.ban'); - if ( f.settings.mod_card_hotkeys ) - ban_btn.setAttribute('title', '(B)an User'); - - // Unban Button - var unban_btn = document.createElement('button'); - unban_btn.className = 'unban button button--icon-only light'; - unban_btn.innerHTML = '
' + CHECK + '
'; - unban_btn.title = (f.settings.mod_card_hotkeys ? "(U)" : "U") + "nban User"; - - jQuery(unban_btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - unban_btn.addEventListener("click", btn_click.bind(this, -1)); - - jQuery(ban_btn).after(unban_btn); - } - - - // Tooltips for ban and ignore. - jQuery("button.ignore, button.ban").tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - - - // More Fixing Other Buttons - var op_btn = el.querySelector('button.mod'); - if ( op_btn ) { - var can_op = is_broadcaster || (user && user.is_admin) || (user && user.is_staff); - - if ( ! can_op ) - op_btn.parentElement.removeChild(op_btn); - } - - - // Follow Button - var follow_button = el.querySelector(".interface > .follow-button"); - if ( follow_button ) - jQuery(follow_button).tipsy({title: function() { return follow_button.classList.contains('is-following') ? "Unfollow" : "Follow"}}); - - - // Whisper and Message Buttons - var msg_btn = el.querySelector(".interface > button.message-button"); - if ( msg_btn ) { - msg_btn.innerHTML = 'W'; - msg_btn.classList.add('button--icon-only'); - msg_btn.classList.add('message'); - - msg_btn.title = "Whisper User"; - jQuery(msg_btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); - - - var real_msg = document.createElement('button'); - real_msg.className = 'message-button button button--icon-only message html-tooltip'; - real_msg.innerHTML = '
' + MESSAGE + '
'; - real_msg.title = "Message User"; - - real_msg.addEventListener('click', function() { - window.open('//www.twitch.tv/message/compose?to=' + controller.get('cardInfo.user.id')); - }) - - msg_btn.parentElement.insertBefore(real_msg, msg_btn.nextSibling); - } - - - // Alias Button - var alias_btn = document.createElement('button'); - alias_btn.className = 'alias button button--icon-only html-tooltip'; - alias_btn.innerHTML = '
' + constants.EDIT + '
'; - alias_btn.title = "Set Alias"; - - alias_btn.addEventListener('click', function() { - var user = controller.get('cardInfo.user.id'), - alias = f.aliases[user]; - - 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; - - f.aliases[user] = new_val; - f.save_aliases(); - - // Update UI - f._update_alias(user); - - 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 ) - msg_btn.parentElement.insertBefore(alias_btn, msg_btn); - else { - var follow_btn = el.querySelector(".interface > .follow-button"); - if ( follow_btn ) - follow_btn.parentElement.insertBefore(alias_btn, follow_btn.nextSibling); - } - - - // Message History - if ( f.settings.mod_card_history ) - this.ffzRenderHistory(); - - // Reposition the menu if it's off-screen. - this.ffzReposition(); - - // Focus the Element - this.$().draggable({ - start: function() { - el.focus(); - }}); - - el.focus(); - - } catch(err) { - try { - f.error("ModerationCardView didInsertElement: " + err); - } catch(err) { } + jQuery(ban_btn).after(unban_btn); } + + + // Tooltips for ban and ignore. + jQuery("button.ignore, button.ban").tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + + + // More Fixing Other Buttons + var op_btn = el.querySelector('button.mod'); + if ( op_btn ) { + var can_op = is_broadcaster || (user && user.is_admin) || (user && user.is_staff); + + if ( ! can_op ) + op_btn.parentElement.removeChild(op_btn); + } + + + // Follow Button + var follow_button = el.querySelector(".follow-button"); + if ( follow_button ) + jQuery(follow_button).tipsy({title: function() { return follow_button.classList.contains('is-following') ? "Unfollow" : "Follow"}}); + + + // Whisper and Message Buttons + var msg_btn = el.querySelector(".interface > button.message-button"); + if ( msg_btn ) { + msg_btn.innerHTML = 'W'; + msg_btn.classList.add('button--icon-only'); + msg_btn.classList.add('message'); + + msg_btn.title = "Whisper User"; + jQuery(msg_btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + + + var real_msg = utils.createElement('button', 'message-button button button--icon-only message html-tooltip'); + real_msg.innerHTML = '
' + MESSAGE + '
'; + real_msg.title = "Message User"; + + real_msg.addEventListener('click', function() { + window.open('//www.twitch.tv/message/compose?to=' + controller.get('cardInfo.user.id')); + }) + + msg_btn.parentElement.insertBefore(real_msg, msg_btn.nextSibling); + } + + + // Alias Button + var alias_btn = utils.createElement('button', 'alias button button--icon-only html-tooltip'); + alias_btn.innerHTML = '
' + constants.EDIT + '
'; + alias_btn.title = "Set Alias"; + + alias_btn.addEventListener('click', function() { + var user = controller.get('cardInfo.user.id'), + alias = f.aliases[user]; + + 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; + + f.aliases[user] = new_val; + f.save_aliases(); + + // Update UI + f._update_alias(user); + + 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 ) + msg_btn.parentElement.insertBefore(alias_btn, msg_btn); + else { + var follow_btn = el.querySelector(".interface > .follow-button"); + if ( follow_btn ) + follow_btn.parentElement.insertBefore(alias_btn, follow_btn.nextSibling); + } + + + // Message History + if ( f.settings.mod_card_history ) + this.ffzRenderHistory(); + + // Reposition the menu if it's off-screen. + this.ffzReposition(); + + // Focus the Element + this.$().draggable({ + start: function() { + el.focus(); + }}); + + el.focus(); }, ffzReposition: function() { @@ -1104,8 +1086,7 @@ FFZ.prototype.setup_mod_card = function() { history = el && el.querySelector('.chat-history'); if ( ! history ) { - history = document.createElement('ul'); - history.className = 'interface clearfix chat-history'; + history = utils.createElement('ul', 'interface clearfix chat-history'); el.appendChild(history); } else { history.classList.remove('loading'); @@ -1118,7 +1099,7 @@ FFZ.prototype.setup_mod_card = function() { if ( ! success ) return; - f.parse_history(data, null, room_id, delete_links, tmiSession); + f.parse_history(data, null, null, room_id, delete_links, tmiSession); var i = data.length, was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight), @@ -1211,10 +1192,9 @@ FFZ.prototype.setup_mod_card = function() { logs.innerHTML = ''; } else { - logs = document.createElement('ul'); - back = document.createElement('button'); + logs = utils.createElement('ul', 'interface clearfix chat-history adjacent-history'); + back = utils.createElement('button', 'button ffz-no-bg back-button'); - back.className = 'button ffz-no-bg back-button'; back.innerHTML = '« Back'; back.addEventListener('click', function() { @@ -1222,12 +1202,10 @@ FFZ.prototype.setup_mod_card = function() { back.parentElement.removeChild(back); history.classList.remove('hidden'); }); - - logs.className = 'interface clearfix chat-history adjacent-history'; } - f.parse_history(data, null, room_id, delete_links, tmiSession, function(msg) { + f.parse_history(data, null, null, room_id, delete_links, tmiSession, function(msg) { msg.from_server = true; var line_time = line.date.getTime() - (line.from_server ? 0 : (f._ws_server_offset || 0)), @@ -1276,7 +1254,7 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { style = '', colored = ''; if ( helpers && helpers.getTime ) - out.push('' + helpers.getTime(msg.date) + ''); + out.push('' + helpers.getTime(msg.date) + ''); var alias = this.aliases[msg.from], @@ -1284,7 +1262,7 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { if ( show_from ) { // Badges - out.push(''); + out.push(''); out.push(this.render_badges(this.get_line_badges(msg, false))); out.push(''); @@ -1349,6 +1327,7 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { l_el.setAttribute('data-room', msg.room); l_el.setAttribute('data-sender', msg.from); + l_el.setAttribute('data-id', msg.tags && msg.tags.id); l_el.setAttribute('data-deleted', msg.deleted || false); l_el.innerHTML = out.join(""); diff --git a/src/ember/player.js b/src/ember/player.js index ee23cd0b..bc67023c 100644 --- a/src/ember/player.js +++ b/src/ember/player.js @@ -18,12 +18,6 @@ FFZ.settings_info.player_stats = { help: "Display your current stream latency (how far behind the broadcast you are) under the player, with a few useful statistics in a tooltip.", on_update: function(val) { - for(var key in this.players) { - var player = this.players[key]; - if ( player && player.player && player.player.ffzSetStatsEnabled ) - player.player.ffzSetStatsEnabled(val || player.player.ffz_stats); - } - if ( ! this._cindex ) return; @@ -79,36 +73,7 @@ FFZ.prototype.setup_player = function() { this.players = {}; - var Player2 = utils.ember_resolve('component:twitch-player2'); - if ( ! Player2 ) - return this.log("Unable to find twitch-player2 component."); - - this.log("Hooking HTML5 Player UI."); - this._modify_player(Player2) - - try { - Player2.create().destroy(); - } catch(err) { } - - // Modify all existing players. - var views = utils.ember_views(); - for(var key in views) { - if ( ! views.hasOwnProperty(key) ) - continue; - - var view = views[key]; - if ( !(view instanceof Player2) ) - continue; - - this.log("Manually updating existing Player instance.", view); - try { - this._modify_player(view); - view.ffzInit(); - - } catch(err) { - this.error("Player2 setup ffzInit: " + err); - } - } + this.update_views('component:twitch-player2', this.modify_twitch_player); } @@ -116,29 +81,22 @@ FFZ.prototype.setup_player = function() { // Component // --------------- -FFZ.prototype._modify_player = function(player) { - var f = this, - update_stats = function() { - f._cindex && f._cindex.ffzUpdatePlayerStats(); - }; +FFZ.prototype.modify_twitch_player = function(player) { + var f = this; + utils.ember_reopen_view(player, { + ffz_init: function() { + var id = this.get('channel.id'); + f.players[id] = this; - player.reopen({ - didInsertElement: function() { - this._super(); - try { - this.ffzInit(); - } catch(err) { - f.error("Player2 didInsertElement: " + err); - } + var player = this.get('player'); + if ( player ) + this.ffzPostPlayer(); }, - willClearRender: function() { - try { - this.ffzTeardown(); - } catch(err) { - f.error("Player2 willClearRender: " + err); - } - this._super(); + ffz_destroy: function() { + var id = this.get('channel.id'); + if ( f.players[id] === this ) + f.players[id] = undefined; }, postPlayerSetup: function() { @@ -150,24 +108,21 @@ FFZ.prototype._modify_player = function(player) { } }, - ffzInit: function() { - var id = this.get('channel.id'); - f.players[id] = this; + ffzRecreatePlayer: function() { + var player = this.get('player'), + theatre = player && player.getTheatre(); - var player = this.get('player'); - if ( player ) - this.ffzPostPlayer(); - }, + // Tell the player to destroy itself. + if ( player ) + player.destroy(); - ffzTeardown: function() { - var id = this.get('channel.id'); - if ( f.players[id] === this ) - f.players[id] = undefined; + // Break down everything left over from that player. + this.$('#video-1').html(''); + Mousetrap.unbind(['alt+x', 'alt+t', 'esc']); + this.set('player', null); - if ( this._ffz_stat_interval ) { - clearInterval(this._ffz_stat_interval); - this._ffz_stat_interval = null; - } + // Now, let Twitch create a new player as usual. + Ember.run.next(this.insertPlayer.bind(this)); }, ffzPostPlayer: function() { @@ -175,74 +130,29 @@ FFZ.prototype._modify_player = function(player) { if ( ! player ) return; - // Make the stats window draggable and fix the button. var stats = this.$('.player .js-playback-stats'); stats.draggable({cancel: 'li', containment: 'parent'}); - - // Only set up the stats hooks if we need stats. - var has_video = false; - - try { - has_video = player.getVideo(); - } catch(err) { - f.error("Player2 ffzPostPlayer: getVideo: " + err); - } - - if ( ! has_video ) - this.ffzInitStats(); - }, - - ffzInitStats: function() { - if ( this.get('ffzStatsInitialized') ) - return; - + // Add an option to the menu to recreate the player. var t = this, - player = this.get('player'); + el = this.$('.player-menu .player-menu__item--stats')[0], + container = el && el.parentElement; - if ( ! player ) - return; + if ( el && ! container.querySelector('.js-player-reset') ) { + var btn_link = utils.createElement('a', 'player-text-link js-player-reset', 'Reset Player'), + btn = utils.createElement('p', 'player-menu__item player-menu__item--reset', btn_link); - this.set('ffzStatsInitialized', true); + btn_link.tabindex = '-1'; + btn_link.href = '#'; - // Make it so stats can no longer be disabled if we want them. - 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; - } + btn_link.addEventListener('click', function(e) { + t.ffzRecreatePlayer(); + e.preventDefault(); + return false; + }); - player.setStatsEnabled = function(e, s) { - if ( s !== false ) - player.ffz_stats = e; - - 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); - } - - return out; - } - - 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.setStatsEnabled || ! player.ffz_stats ) ) { - this._ffz_player_stats_initialized = true; - player.addEventListener('statschange', update_stats); - player.ffzSetStatsEnabled(true); + container.insertBefore(btn, el.nextSibling); } } }); diff --git a/src/ember/room.js b/src/ember/room.js index 1a110e8d..6a0e23d6 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -50,7 +50,8 @@ FFZ.prototype.setup_room = function() { if ( RC ) { var orig_ban = RC._actions.banUser, - orig_to = RC._actions.timeoutUser; + orig_to = RC._actions.timeoutUser, + orig_show = RC._actions.showModOverlay; RC._actions.banUser = function(e) { orig_ban.call(this, e); @@ -62,12 +63,15 @@ FFZ.prototype.setup_room = function() { this.get("model").clearMessages(e.user, null, true); } - RC._actions.showModOverlay = function(e) { - var Channel = utils.ember_resolve('model:channel'); - if ( ! Channel ) - return; - var chan = Channel.find({id: e.sender}); + RC._actions.showModOverlay = function(e) { + var Channel = utils.ember_resolve('model:deprecated-channel'), + chan = Channel && Channel.find && Channel.find({id: e.sender}); + + if ( ! chan ) { + f.log("Error opening mod card. model:deprecated-channel does not exist or does not have find!"); + return orig_show.call(this, e); + } // Don't try loading the channel if it's already loaded. Don't make mod cards // refresh the channel page when you click the broadcaster, basically. @@ -111,33 +115,7 @@ FFZ.prototype.setup_room = function() { } this.log("Hooking the Ember Room view."); - - var RoomView = utils.ember_resolve('view:room'); - this._modify_rview(RoomView); - - // For some reason, this doesn't work unless we create an instance of the - // room view and then destroy it immediately. - try { - RoomView.create().destroy(); - } catch(err) { } - - // Modify all existing Room views. - var views = utils.ember_views(); - for(var key in views) { - if ( ! views.hasOwnProperty(key) ) - continue; - - var view = views[key]; - if ( !(view instanceof RoomView) ) - continue; - - this.log("Manually updating existing Room view.", view); - try { - view.ffzInit(); - } catch(err) { - this.error("RoomView setup ffzInit: " + err); - } - } + this.update_views('view:room', this.modify_room_view); } @@ -145,29 +123,10 @@ FFZ.prototype.setup_room = function() { // View Customization // -------------------- -FFZ.prototype._modify_rview = function(view) { +FFZ.prototype.modify_room_view = function(view) { var f = this; - view.reopen({ - didInsertElement: function() { - this._super(); - - try { - this.ffzInit(); - } catch(err) { - f.error("RoomView didInsertElement: " + err); - } - }, - - willClearRender: function() { - try { - this.ffzTeardown(); - } catch(err) { - f.error("RoomView willClearRender: " + err); - } - this._super(); - }, - - ffzInit: function() { + utils.ember_reopen_view(view, { + ffz_init: function() { f._roomv = this; this.ffz_frozen = false; @@ -196,6 +155,34 @@ FFZ.prototype._modify_rview = function(view) { var controller = this.get('controller'); if ( controller ) { controller.reopen({ + calcRecipientEligibility: function(e) { + // Because this doesn't work properly with multiple channel rooms + // by default, do it ourselves. + var id = controller.get('model.roomProperties._id'), + update = function(data) { + if ( controller.isDestroyed || controller.get('model.roomProperties._id') !== id ) + return; + + controller.set('model._ffz_bits_eligibility', data); + controller.set('isRecipientBitsIneligible', ! data.eligible); + controller.set('isBitsHelperShown', data.eligible); + controller.set('minimumBits', data.minBits); + + if ( ! data.eligible ) + controller.set('isBitsTooltipActive', false); + }; + + if ( id === undefined ) + return; + + var data = controller.get('model._ffz_bits_eligibility'); + if ( data === undefined ) + controller.get('bits').loadRecipientEligibility(id).then(update); + else + update(data); + + }, + submitButtonText: function() { if ( this.get("model.isWhisperMessage") && this.get("model.isWhispersEnabled") ) return i18n("Whisper"); @@ -214,7 +201,7 @@ FFZ.prototype._modify_rview = function(view) { } }, - ffzTeardown: function() { + ffz_destroy: function() { if ( f._roomv === this ) f._roomv = undefined; @@ -280,7 +267,7 @@ FFZ.prototype._modify_rview = function(view) { btn.classList.toggle('ffz-banned', (room && room.get('ffz_banned') || false)); } - var badge, id, info, vis_count = 0; + var badge, id, info, vis_count = 0, label; for(var i=0; i < STATUS_BADGES.length; i++) { info = STATUS_BADGES[i]; id = 'ffz-stat-' + info[0]; @@ -289,13 +276,18 @@ FFZ.prototype._modify_rview = function(view) { if ( typeof visible === "string" ) visible = visible === "1"; + label = typeof info[3] === "function" ? info[3].call(f, room) : undefined; + if ( ! badge ) { - badge = utils.createElement('span', 'ffz room-state stat float-right', info[0].charAt(0).toUpperCase() + '' + info[0].substr(1).toUpperCase() + ''); + badge = utils.createElement('span', 'ffz room-state stat float-right', (label || info[0]).charAt(0).toUpperCase() + '' + (label || info[0]).substr(1).toUpperCase() + ''); badge.id = id; jQuery(badge).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'se')}); cont.appendChild(badge); } + if ( label ) + badge.innerHTML = (label || info[0]).charAt(0).toUpperCase() + '' + (label || info[0]).substr(1).toUpperCase() + ''; + badge.title = typeof info[2] === "function" ? info[2].call(f, room) : info[2]; badge.classList.toggle('hidden', ! visible); if ( visible ) @@ -494,7 +486,6 @@ FFZ.prototype._modify_rview = function(view) { warning.classList.remove('hidden'); }, - ffzUnwarnPaused: function() { var el = this.get('element'), warning = el && el.querySelector('.chat-interface .more-messages-indicator.ffz-freeze-indicator'); @@ -502,7 +493,6 @@ FFZ.prototype._modify_rview = function(view) { if ( warning ) warning.classList.add('hidden'); } - }); } @@ -640,12 +630,13 @@ FFZ.ffz_commands.help.help = "Usage: /ffz help [command]\nList available command FFZ.prototype.update_room_important = function(id, controller) { var Chat = controller || utils.ember_lookup('controller:chat'), + current_room = Chat && Chat.get('currentChannelRoom'), room = this.rooms[id]; if ( ! room ) return; - room.important = (Chat && room.room && Chat.get('currentChannelRoom') === room.room) || (room.room && room.room.get('isGroupRoom')) || (this.settings.pinned_rooms.indexOf(id) !== -1); + room.important = (room.room && current_room === room.room) || (current_room && current_room.ffz_host_target === id) || (room.room && room.room.get('isGroupRoom')) || (this.settings.pinned_rooms.indexOf(id) !== -1); }; @@ -798,7 +789,7 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) { before = first_existing.date && first_existing.date.getTime(); - this.parse_history(data, null, room_id, delete_links, tmiSession, function(msg) { + this.parse_history(data, null, null, room_id, delete_links, tmiSession, function(msg) { if ( from_server ) msg.from_server = true; @@ -815,7 +806,7 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) { return true; // Display the Ban Reason if we're a moderator or that user. - if ( msg.tags['ban-reason'] && is_mine || r.get('isModeratorOrHigher') ) { + if ( msg.tags['ban-reason'] && (is_mine || r.get('isModeratorOrHigher')) ) { msg.message = msg.message.substr(0, msg.message.length - 1) + ' with reason: ' + msg.tags['ban-reason']; msg.cachedTokens = [utils.sanitize(msg.message)]; } @@ -834,6 +825,12 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) { if ( ! first_inserted ) first_inserted = msg; + // Store the message ID for this message, of course. + var msg_id = msg.tags && msg.tags.id, + ids = r.ffz_ids = r.ffz_ids || {}; + if ( msg_id && ! ids[msg_id] ) + ids[msg_id] = msg; + messages.unshiftObject(msg); inserted += 1; @@ -870,6 +867,11 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) { if ( r.shouldShowMessage(msg) ) { messages.insertAt(inserted, msg); while ( messages.length > buffer_size ) { + // Remove this message from the ID tracker. + var m = messages.get(0); + if ( m.tags && m.tags.id && r.ffz_ids && r.ffz_ids[m.tags.id] ) + delete r.ffz_ids[m.tags.id]; + messages.removeAt(0); removed++; } @@ -1042,6 +1044,7 @@ FFZ.prototype._modify_room = function(room) { try { f.add_room(this.id, this); this.set("ffz_chatters", {}); + this.set("ffz_ids", this.get('ffz_ids') || {}); } catch(err) { f.error("add_room: " + err); } @@ -1063,6 +1066,7 @@ FFZ.prototype._modify_room = function(room) { if ( user ) { var duration = Infinity, reason = undefined, + msg_id = undefined, current_user = f.get_user(), is_me = current_user && current_user.login === user; @@ -1077,6 +1081,18 @@ FFZ.prototype._modify_room = function(room) { reason = tags['ban-reason']; + // Is there a UUID on the end of the ban reason? + if ( reason ) { + var match = constants.UUID_TEST.exec(reason); + if ( match ) { + msg_id = match[1]; + reason = reason.substr(0, reason.length - match[0].length); + if ( ! reason.length ) + reason = undefined; + } + } + + // If we were banned, set the state and update the UI. if ( is_me ) { t.set('ffz_banned', true); @@ -1098,39 +1114,81 @@ FFZ.prototype._modify_room = function(room) { t.ffzRecentlyBanned.shift(); - // Delete Visible Messages - var msgs = t.get('messages'), - total = msgs.get('length'), - i = total, - removed = 0; + // Are we deleting a specific message? + if ( msg_id && this.ffz_ids ) { + var msg = this.ffz_ids[msg_id]; + if ( msg && msg.from === user ) { + msg.ffz_deleted = true; + if ( ! f.settings.prevent_clear ) + msg.deleted = true; - while(i--) { - var msg = msgs.get(i); + if ( f.settings.remove_deleted ) + if ( msg.pending ) + msg.removed = true; + else { + var msgs = t.get('messages'), + total = msgs.get('length'), + i = total; - if ( msg.from === user ) { - if ( f.settings.remove_deleted ) { - msgs.removeAt(i); - removed++; - continue; + while(i--) { + var msg = msgs.get(i); + if ( msg.tags && msg.tags.id === msg_id ) { + msgs.removeAt(i); + delete this.ffz_ids[msg_id]; + break; + } + } + } + + if ( msg._line ) { + Ember.propertyDidChange(msg._line, 'msgObject.ffz_deleted'); + Ember.propertyDidChange(msg._line, 'msgObject.deleted'); } - t.set('messages.' + i + '.ffz_deleted', true); - if ( ! f.settings.prevent_clear ) - t.set('messages.' + i + '.deleted', true); - } - } + } else if ( msg.from !== user ) + f.log("Banned Message ID #" + msg_id + " not owned by: " + user); + else + f.log("Banned Message ID #" + msg_id + " not found in chat."); + } else { + // Delete all messages from this user / chat. + // Delete Visible Messages + var msgs = t.get('messages'), + total = msgs.get('length'), + i = total, + removed = 0; - // Delete Panding Messages - if ( t.ffzPending ) { - msgs = t.ffzPending; - i = msgs.length; while(i--) { var msg = msgs.get(i); - if ( msg.from !== user ) continue; - msg.ffz_deleted = true; - msg.deleted = !f.settings.prevent_clear; - msg.removed = f.settings.remove_deleted; + if ( msg.from === user ) { + if ( f.settings.remove_deleted ) { + // Remove this message from the ID tracker. + if ( msg.tags && msg.tags.id && this.ffz_ids && this.ffz_ids[msg.tags.id] ) + delete this.ffz_ids[msg.tags.id]; + + msgs.removeAt(i); + removed++; + continue; + } + + t.set('messages.' + i + '.ffz_deleted', true); + if ( ! f.settings.prevent_clear ) + t.set('messages.' + i + '.deleted', true); + } + } + + + // Delete Panding Messages + if ( t.ffzPending ) { + msgs = t.ffzPending; + i = msgs.length; + while(i--) { + var msg = msgs.get(i); + if ( msg.from !== user ) continue; + msg.ffz_deleted = true; + msg.deleted = !f.settings.prevent_clear; + msg.removed = f.settings.remove_deleted; + } } } @@ -1164,6 +1222,7 @@ FFZ.prototype._modify_room = function(room) { date: now, ffz_ban_target: user, reasons: reason ? [reason] : [], + msg_ids: msg_id ? [msg_id] : [], durations: [duration], end_time: end_time, timeouts: 1, @@ -1176,6 +1235,9 @@ FFZ.prototype._modify_room = function(room) { this.addMessage(msg); } else { + if ( msg_id && last_ban.msg_ids.indexOf(msg_id) === -1 ) + last_ban.msg_ids.push(msg_id); + if ( reason && last_ban.reasons.indexOf(reason) === -1 ) last_ban.reasons.push(reason); @@ -1205,6 +1267,9 @@ FFZ.prototype._modify_room = function(room) { last_ban = null; if ( last_ban ) { + if ( msg_id && last_ban.msg_ids.indexOf(msg_id) === -1 ) + last_ban.msg_ids.push(msg_id); + if ( reason && last_ban.reasons.indexOf(reason) === -1 ) last_ban.reasons.push(reason); @@ -1223,6 +1288,7 @@ FFZ.prototype._modify_room = function(room) { date: now, ffz_ban_target: user, reasons: reason ? [reason] : [], + msg_ids: msg_id ? [msg_id] : [], durations: [duration], end_time: end_time, timeouts: 1, @@ -1256,8 +1322,17 @@ FFZ.prototype._modify_room = function(room) { len = messages.get("length"), limit = this.get("messageBufferSize"); - if ( len > limit ) - messages.removeAt(0, len - limit); + if ( len > limit ) { + var to_remove = len - limit; + for(var i = 0; i < to_remove; i++) { + // Remove this message from the ID tracker. + var msg = messages.get(i); + if ( msg.tags && msg.tags.id && this.ffz_ids && this.ffz_ids[msg.tags.id] ) + delete this.ffz_ids[msg.tags.id]; + } + + messages.removeAt(0, to_remove); + } }, // Artificial chat delay @@ -1267,6 +1342,7 @@ FFZ.prototype._modify_room = function(room) { this.ffzPending = []; var now = msg.time = Date.now(); + msg.pending = true; this.ffzPending.push(msg); this.ffzSchedulePendingFlush(now); @@ -1320,12 +1396,18 @@ FFZ.prototype._modify_room = function(room) { for (var i = 0, l = this.ffzPending.length; i < l; i++) { var msg = this.ffzPending[i]; - if ( msg.removed ) + if ( msg.removed ) { + // Don't keep this message ID around. + var msg_id = msg && msg.tags && msg.tags.id; + if ( msg_id && this.ffz_ids && this.ffz_ids[msg_id] ) + delete this.ffz_ids[msg_id]; continue; + } if ( f.settings.chat_delay !== 0 && (f.settings.chat_delay + msg.time > now) ) break; + msg.pending = false; this.ffzActualPushMessage(msg); } @@ -1367,10 +1449,16 @@ FFZ.prototype._modify_room = function(room) { } }, + onMessage: function(msg) { + // We do our own batching. With blackjack, and hookers. You know what? Forget the batching. + this.addMessage(msg); + }, + addMessage: function(msg) { if ( msg ) { var is_resub = msg.tags && msg.tags['msg-id'] === 'resub', - room_id = this.get('id'); + room_id = this.get('id'), + msg_id = msg.tags && msg.tags.id; // Ignore resubs in other rooms. if ( is_resub && ! f.settings.hosted_sub_notices && (msg.tags['room-id'] != this.get('roomProperties._id') || HOSTED_SUB.test(msg.tags['system-msg'])) ) @@ -1515,6 +1603,13 @@ FFZ.prototype._modify_room = function(room) { if ( f._chat_filters[i](msg) === false ) return; + // We're past the last return, so store the message + // now that we know we're keeping it. + if ( msg_id ) { + var ids = this.ffz_ids = this.ffz_ids || {}; + ids[msg_id] = msg; + } + // 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/"); diff --git a/src/ember/vod-chat.js b/src/ember/vod-chat.js index f264439e..020e11e5 100644 --- a/src/ember/vod-chat.js +++ b/src/ember/vod-chat.js @@ -12,16 +12,10 @@ var FFZ = window.FrankerFaceZ, // --------------------- FFZ.prototype.setup_vod_chat = function() { - var f = this, - VRC = utils.ember_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 = utils.ember_lookup('service:vod-chat-service'); + var f = this, + VODService = utils.ember_lookup('service:vod-chat-service'); + if ( VODService ) VODService.reopen({ messageBufferSize: f.settings.scrollback_length, @@ -50,56 +44,59 @@ FFZ.prototype.setup_vod_chat = function() { else f.error("Unable to locate VOD Chat Service."); - // Get the VOD Chat Display - var VODChat = utils.ember_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 = utils.ember_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); - } - } - } + this.update_views('component:vod-right-column', this.modify_vod_right_column); + this.update_views('view:vod', this.modify_vod_view); + this.update_views('component:vod-chat-display', this.modify_vod_chat_display); } -FFZ.prototype._modify_vod_right_column = function(component) { +FFZ.prototype.modify_vod_view = function(view) { var f = this; + utils.ember_reopen_view(view, { + ffz_init: function() { + f._vodv = this; - component.reopen({ - didInsertElement: function() { - this._super(); - try { - this.ffzInit(); - } catch(err) { - f.error("VODRightColumn didInsertElement: " + err); - } + var channel_id = this.get('context.channel.name'); + + if ( f.settings.auto_theater ) { + var player = f.players && f.players[channel_id] && f.players[channel_id].get('player'); + if ( player ) + player.setTheatre(true); + } + + // Listen to scrolling. + this._ffz_scroller = this.ffzOnScroll.bind(this); + jQuery(this.get('element')).parents('.tse-scroll-content').on('scroll', this._ffz_scroller); }, - ffzInit: function() { + ffz_destroy: function() { + if ( f._vodv === this ) + f._vodv = null; + + if ( this._ffz_scroller ) { + jQuery(this.get('element')).parents('.tse-scroll-content').off('scroll', this._ffz_scroller); + this._ffz_scroller = null; + } + }, + + ffzOnScroll: function(event) { + // When we scroll past the bottom of the player, do stuff! + var top = event && event.target && event.target.scrollTop, + height = this.get('layout.playerSize.1'); + + if ( ! top ) + top = jQuery(this.get('element')).parents('.tse-scroll-content').scrollTop(); + + document.body.classList.toggle('ffz-small-player', f.settings.small_player && top >= height); + } + }); +} + + +FFZ.prototype.modify_vod_right_column = function(component) { + var f = this; + utils.ember_reopen_view(component, { + ffz_init: function() { if ( f.settings.dark_twitch ) { var el = this.get('element'), cont = el && el.querySelector('.chat-container'); @@ -112,29 +109,11 @@ FFZ.prototype._modify_vod_right_column = function(component) { } -FFZ.prototype._modify_vod_chat_display = function(component) { +FFZ.prototype.modify_vod_chat_display = function(component) { var f = this, VODService = utils.ember_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(); - }, - + utils.ember_reopen_view(component, { _prepareToolTips: function() { this.$(".tooltip").tipsy({ live: true, @@ -142,7 +121,7 @@ FFZ.prototype._modify_vod_chat_display = function(component) { }) }, - ffzInit: function() { + ffz_init: function() { f._vodc = this; // Load the room, if necessary @@ -153,22 +132,9 @@ FFZ.prototype._modify_vod_chat_display = function(component) { 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() { + ffz_destroy: function() { if ( f._vodc === this ) f._vodc = undefined; @@ -232,14 +198,6 @@ FFZ.prototype._modify_vod_chat_display = function(component) { } }, - /*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; diff --git a/src/ember/wrapper.js b/src/ember/wrapper.js new file mode 100644 index 00000000..1451563b --- /dev/null +++ b/src/ember/wrapper.js @@ -0,0 +1,88 @@ +var FFZ = window.FrankerFaceZ, + utils = require("../utils"), + constants = require("../constants"); + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_ember_wrapper = function() { + this._views_to_update = []; + this._ember_finalized = false; +} + + +FFZ.prototype.update_views = function(klass, modifier, if_not_exists) { + var original_klass; + if ( typeof klass === 'string' ) { + original_klass = klass; + klass = utils.ember_resolve(klass); + if ( ! klass && if_not_exists ) { + if ( typeof if_not_exists === "function" ) + if_not_exists.call(this, klass, modifier); + else { + klass = Ember.Component.extend({}); + App.__registry__.register(original_klass, klass); + } + } + + if ( ! klass ) { + this.error("Unable to locate the Ember " + original_klass); + return false; + } + } else + original_klass = klass.toString(); + + if ( this._ember_finalized ) + this._update_views([[original_klass, klass, modifier]]); + else + this._views_to_update.push([original_klass, klass, modifier]); + + return true; +} + + +FFZ.prototype.finalize_ember_wrapper = function() { + this._ember_finalized = true; + var views = this._views_to_update; + this._views_to_update = null; + this._update_views(views); +} + + +FFZ.prototype._update_views = function(klasses) { + this.log("Updating Ember classes and instances.", klasses); + // Modify all pending classes and clear them from cache. + for(var i=0; i < klasses.length; i++) { + klasses[i][2].call(this, klasses[i][1]); + + try { + klasses[i][1].create().destroy() + } catch(err) { + this.log("There was an error creating and destroying an instance of the Ember class \"" + klasses[i][0] + "\" to clear its cache.", err); + } + } + + // Iterate over all existing views and update them as necessary. + var views = utils.ember_views(); + for(var view_id in views) { + var view = views[view_id]; + if ( ! view ) + continue; + + for(var i=0; i < klasses.length; i++) + if ( view instanceof klasses[i][1] ) { + try { + if ( ! view.ffz_modified ) + klasses[i][2].call(this, view); + + (view.ffz_update || view.ffz_init).call(view); + + } catch(err) { + this.error("An error occured when updating an existing Ember instance of: " + klasses[i][0], err); + } + + break; + } + } +} \ No newline at end of file diff --git a/src/ext/api.js b/src/ext/api.js index 88420426..2af0ca84 100644 --- a/src/ext/api.js +++ b/src/ext/api.js @@ -1,7 +1,6 @@ var FFZ = window.FrankerFaceZ, utils = require('../utils'), - build_css = function(emote) { if ( ! emote.margins && ! emote.css ) return ""; @@ -50,11 +49,15 @@ var API = FFZ.API = function(instance, name, icon, version) { this.global_sets = []; this.default_sets = []; + this.badges = {}; + this.users = {}; this.chat_filters = []; this.on_room_callbacks = []; this.name = name || ("Extension#" + this.id); + this.name_key = this.name.replace(/[^A-Z0-9_\-]/g, '').toLowerCase(); + this.icon = icon || null; this.version = version || null; @@ -386,13 +389,75 @@ API.prototype.unregister_room_set = function(room_id, id) { } +// ----------------------- +// Badge APIs +// ----------------------- + +API.prototype.add_badge = function(badge_id, badge) { + var exact_id = this.id + '-' + badge_id, + + real_badge = { + id: exact_id, + source_ext: this.id, + source_id: badge_id, + alpha_image: badge.alpha_image, + color: badge.color || "transparent", + no_invert: badge.no_invert, + invert_invert: badge.invert_invert, + css: badge.css, + image: badge.image, + name: badge.name, + title: badge.title, + slot: badge.slot, + visible: badge.visible, + replaces: badge.replaces, + replaces_type: badge.replaces_type + }; + + this.ffz.badges[exact_id] = this.badges[badge_id] = real_badge; + utils.update_css(this.ffz._badge_style, exact_id, utils.badge_css(real_badge)); +} + + +API.prototype.remove_badge = function(badge_id) { + var exact_id = this.id + '-' + badge_id; + this.ffz.badges[exact_id] = this.badges[badge_id] = undefined; + utils.update_css(this.ffz._badge_style, exact_id); +} + + // ----------------------- // User Modifications // ----------------------- -API.prototype.user_add_set = function(user_name, set_id) { - var user = this.users[user_name] = this.users[user_name] || {}, - ffz_user = this.ffz.users[user_name] = this.ffz.users[user_name] || {}, +API.prototype.user_add_badge = function(username, slot, badge_id) { + var user = this.users[username] = this.users[username] || {}, + ffz_user = this.ffz.users[username] = this.ffz.users[username] || {}, + + badges = user.badges = user.badges || {}, + ffz_badges = ffz_user.badges = ffz_user.badges || {}, + + exact_id = this.id + '-' + badge_id, + badge = {id: exact_id}; + + badges[slot] = ffz_badges[slot] = badge; +} + + +API.prototype.user_remove_badge = function(username, slot) { + var user = this.users[username] = this.users[username] || {}, + ffz_user = this.ffz.users[username] = this.ffz.users[username] || {}, + + badges = user.badges = user.badges || {}, + ffz_badges = ffz_user.badges = ffz_user.badges || {}; + + badges[slot] = ffz_badges[slot] = null; +} + + +API.prototype.user_add_set = function(username, set_id) { + var user = this.users[username] = this.users[username] || {}, + ffz_user = this.ffz.users[username] = this.ffz.users[username] || {}, emote_sets = user.sets = user.sets || [], ffz_sets = ffz_user.sets = ffz_user.sets || [], @@ -407,14 +472,14 @@ API.prototype.user_add_set = function(user_name, set_id) { // Update tab completion. var user = this.ffz.get_user(); - if ( this.ffz._inputv && user && user.login === user_name ) + if ( this.ffz._inputv && user && user.login === username ) Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons'); } -API.prototype.user_remove_set = function(user_name, set_id) { - var user = this.users[user_name], - ffz_user = this.ffz.users[user_name], +API.prototype.user_remove_set = function(username, set_id) { + var user = this.users[username], + ffz_user = this.ffz.users[username], emote_sets = user && user.sets, ffz_sets = ffz_user && ffz_user.sets, @@ -431,7 +496,7 @@ API.prototype.user_remove_set = function(user_name, set_id) { // Update tab completion. var user = this.ffz.get_user(); - if ( this.ffz._inputv && user && user.login === user_name ) + if ( this.ffz._inputv && user && user.login === username ) Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons'); } @@ -455,7 +520,6 @@ API.prototype.unregister_chat_filter = function(filter) { this.ffz._chat_filters.splice(ind, 1); } - // ----------------------- // Channel Callbacks // ----------------------- diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index 7f441135..18a23a6f 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -96,6 +96,10 @@ FFZ.prototype.setup_bttv = function(delay) { this.toggle_style('chat-hc-background');*/ this.toggle_style('chat-colors-gray'); + this.toggle_style('badges-rounded'); + this.toggle_style('badges-circular'); + this.toggle_style('badges-blank'); + this.toggle_style('badges-circular-small'); this.toggle_style('badges-transparent'); this.toggle_style('badges-sub-notice'); this.toggle_style('badges-sub-notice-on'); @@ -114,12 +118,15 @@ FFZ.prototype.setup_bttv = function(delay) { } // Send Message Behavior - var original_send = BetterTTV.chat.helpers.sendMessage, f = this; - BetterTTV.chat.helpers.sendMessage = function(message) { + var f = this, + BC = BetterTTV.chat, + original_send = BC.helpers.sendMessage; + + BC.helpers.sendMessage = function(message) { var cmd = message.split(' ', 1)[0].toLowerCase(); - if ( cmd === "/ffz" ) - f.run_ffz_command(message.substr(5), BetterTTV.chat.store.currentRoom); + if ( cmd === '/ffz' ) + f.run_ffz_command(message.substr(5), BC.store.currentRoom); else return original_send(message); } @@ -127,9 +134,10 @@ FFZ.prototype.setup_bttv = function(delay) { // Ugly Hack for Current Room, as this is stripped out before we get to // the actual privmsg renderer. - var original_handler = BetterTTV.chat.handlers.onPrivmsg, + var original_handler = BC.handlers.onPrivmsg, received_room; - BetterTTV.chat.handlers.onPrivmsg = function(room, data) { + + BC.handlers.onPrivmsg = function(room, data) { received_room = room; var output = original_handler(room, data); received_room = null; @@ -138,39 +146,48 @@ FFZ.prototype.setup_bttv = function(delay) { // Message Display Behavior - var original_privmsg = BetterTTV.chat.templates.privmsg; - BetterTTV.chat.templates.privmsg = function(highlight, action, server, isMod, data) { + var original_privmsg = BC.templates.privmsg; + BC.templates.privmsg = function(data, opts) { try { + opts = opts || {}; + // Handle badges. f.bttv_badges(data); // Now, do everything else manually because things are hard-coded. - return '
'+ - BetterTTV.chat.templates.timestamp(data.time)+' '+ - (isMod?BetterTTV.chat.templates.modicons():'')+' '+ - BetterTTV.chat.templates.badges(data.badges)+ - BetterTTV.chat.templates.from(data.nickname, data.color)+ - BetterTTV.chat.templates.message(data.sender, data.message, data.emotes, action?data.color:false)+ + return '
'+ + BC.templates.timestamp(data.time)+' '+ + (opts.isMod ? BC.templates.modicons():'')+' '+ + BC.templates.badges(data.badges)+ + BC.templates.from(data.nickname, data.color)+ + BC.templates.message(data.sender, data.message, { + emotes: data.emotes, + colored: (opts.action && !opts.highlight) ? data.color : false, + bits: data.bits + }) + '
'; } catch(err) { f.log("Error: ", err); - return original_privmsg(highlight, action, server, isMod, data); + return original_privmsg(data, opts); } } // Whispers too! - var original_whisper = BetterTTV.chat.templates.whisper; - BetterTTV.chat.templates.whisper = function(data) { + var original_whisper = BC.templates.whisper; + BC.templates.whisper = function(data) { try { // Handle badges. f.bttv_badges(data); // Now, do everything else manually because things are hard-coded. return '
' + - BetterTTV.chat.templates.timestamp(data.time) + ' ' + - (data.badges && data.badges.length ? BetterTTV.chat.templates.badges(data.badges) : '') + - BetterTTV.chat.templates.whisperName(data.sender, data.receiver, data.from, data.to, data.fromColor, data.toColor) + - BetterTTV.chat.templates.message(data.sender, data.message, data.emotes, false) + + BC.templates.timestamp(data.time) + ' ' + + (data.badges && data.badges.length ? BC.templates.badges(data.badges) : '') + + BC.templates.whisperName(data.sender, data.receiver, data.from, data.to, data.fromColor, data.toColor) + + BC.templates.message(data.sender, data.message, { + emotes: data.emotes, + colored: false + }) + '
'; } catch(err) { f.log("Error: ", err); @@ -180,22 +197,24 @@ FFZ.prototype.setup_bttv = function(delay) { // Message Renderer. I had to completely rewrite this method to get it to // use my replacement emoticonizer. - var original_message = BetterTTV.chat.templates.message, + var original_message = BC.templates.message, received_sender; - BetterTTV.chat.templates.message = function(sender, message, emotes, colored) { + BC.templates.message = function(sender, message, data) { try { - colored = colored || false; - var rawMessage = encodeURIComponent(message); + var colored = data.colored || false, + force = data.force || false, + emotes = data.emotes, + rawMessage = encodeURIComponent(message); if(sender !== 'jtv') { // Hackilly send our state across. received_sender = sender; - var tokenizedMessage = BetterTTV.chat.templates.emoticonize(message, emotes); + var tokenizedMessage = BC.templates.emoticonize(message, emotes); received_sender = null; for(var i=0; i'+message+''; + var spam = false; + if ( BetterTTV.settings.get('hideSpam') && BC.helpers.isSpammer(sender) && !BC.helpers.isModerator(sender) && !force) { + message = '<spam deleted>'; + spam = true; + } + + return '' + message + ''; + } catch(err) { f.log("Error: ", err); return original_message(sender, message, emotes, colored); @@ -247,8 +273,8 @@ FFZ.prototype.setup_bttv = function(delay) { } // Emoticonize - var original_emoticonize = BetterTTV.chat.templates.emoticonize; - BetterTTV.chat.templates.emoticonize = function(message, emotes) { + var original_emoticonize = BC.templates.emoticonize; + BC.templates.emoticonize = function(message, emotes) { var tokens = original_emoticonize(message, emotes), room = (received_room || BetterTTV.getChannel()), diff --git a/src/ext/rechat.js b/src/ext/rechat.js deleted file mode 100644 index 57a3c8e0..00000000 --- a/src/ext/rechat.js +++ /dev/null @@ -1,305 +0,0 @@ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'), - utils = require('../utils'); - - -// -------------------- -// Initialization -// -------------------- - -FFZ.prototype.setup_rechat = function() { - if ( this.has_bttv || navigator.userAgent.indexOf('Android') !== -1 ) - return; - - this._rechat_listening = false; - - this.log("Installing ReChat mutation observer."); - - var f = this; - this._rechat_observer = new MutationObserver(function(mutations) { - for(var i=0; i < mutations.length; i++) { - var mutation = mutations[i]; - if ( mutation.type !== "childList" ) - continue; - - for(var x=0; x < mutation.addedNodes.length; x++) { - var added = mutation.addedNodes[x]; - if ( added.nodeType !== added.ELEMENT_NODE || added.tagName !== "DIV" ) - continue; - - // Is this a ReChat line? - if ( added.classList.contains('rechat-chat-line') && ! added.classList.contains('ffz-processed') ) - f.process_rechat_line(added); - } - } - }); - - this.log("Starting ReChat check loop."); - this._rechat_interval = setInterval(this.find_rechat.bind(this), 1000); - this.find_rechat(); -} - - -// -------------------- -// ReChat Detection -// -------------------- - -FFZ.prototype.find_rechat = function() { - var el = !this.has_bttv ? document.querySelector('.rechat-chat-line') : null; - - if ( ! this._rechat_listening && ! el ) { - // Try darkening a chat container. We don't have chat. - var container = document.querySelector('.chat-container'), - header = container && container.querySelector('.chat-header'); - - if ( header && header.textContent.indexOf('ReChat') !== -1 ) { - // Look-up dark mode. - var dark_chat = this.settings.dark_twitch; - if ( ! dark_chat ) { - var Settings = utils.ember_lookup('controller:settings'), - model = Settings ? Settings.get('model') : undefined; - - dark_chat = model && model.get('darkMode'); - } - - container.classList.toggle('dark', dark_chat); - jQuery(container).find('.chat-lines').addClass('ffz-scrollbar'); - } - - return; - } - - // If there's no change, don't continue. - if ( !!el === this._rechat_listening ) - return; - - // If we're no longer listening, stop the observer and quit. - if ( ! el ) { - this._rechat_observer.disconnect(); - this._rechat_listening = false; - return; - } - - // We're newly listening. Process all existing ReChat chat lines - // and darken the container if required, also enable the observer. - var container = jQuery(el).parents('.chat-container'); - if ( ! container.length ) - return; - - container = container[0]; - - // Look-up dark mode. - var dark_chat = this.settings.dark_twitch; - if ( ! dark_chat ) { - var Settings = utils.ember_lookup('controller:settings'), - model = Settings ? Settings.get('model') : undefined; - - dark_chat = model && model.get('darkMode'); - } - - container.classList.toggle('dark', dark_chat); - jQuery(container).find('.chat-lines').addClass('ffz-scrollbar'); - - // 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'); - if ( room_id && ! this.rooms[room_id] ) - this.load_room(room_id, this._reprocess_rechat.bind(this, container)); - - // Do stuff. - var lines = container.querySelectorAll('.rechat-chat-line'); - for(var i=0; i < lines.length; i++) { - var line = lines[i]; - if ( line.classList.contains('ffz-processed') ) - continue; - - this.process_rechat_line(line); - } - - // Start observing. - this._rechat_observer.observe(container, { - childList: true, - subtree: true - }); - - this._rechat_listening = true; -} - - -// -------------------- -// ReChat Lines -// -------------------- - -FFZ.prototype._reprocess_rechat = function(container) { - var lines = container.querySelectorAll('.rechat-chat-line'); - for(var i=0; i < lines.length; i++) - this.process_rechat_line(lines[i], true); -} - - -FFZ.prototype.process_rechat_line = function(line, reprocess) { - if ( ! reprocess && line.classList.contains('ffz-processed') ) - return; - - line.classList.add('ffz-processed'); - - var f = this, - user_id = line.getAttribute('data-sender'), - room_id = line.getAttribute('data-room'), - - Layout = utils.ember_lookup('service:layout'), - Settings = utils.ember_lookup('controller:settings'), - is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')), - - badges_el = line.querySelector('.badges'), - from_el = line.querySelector('.from'), - message_el = line.querySelector('.message'), - - badges = {}, - had_badges = !!badges_el, - - raw_color = from_el && FFZ.Color.RGB.fromCSS(from_el.style.color), - colors = raw_color && this._handle_color(raw_color), - - alias = this.aliases[user_id]; - - - if ( ! badges_el ) { - badges_el = document.createElement('span'); - badges_el.className = 'badges float-left'; - line.insertBefore(badges_el, from_el || line.firstElementChild); - } - - if ( ! reprocess || ! had_badges ) { - // Read existing known badges. - var existing = badges_el.querySelectorAll('.badge'); - for(var i=0; i < existing.length; i++) { - var badge = existing[i]; - if ( badge.classList.contains('broadcaster') ) - badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; - else if ( badge.classList.contains('staff') ) - badges[0] = {klass: 'staff', title: 'Staff'}; - else if ( badge.classList.contains('admin') ) - badges[0] = {klass: 'admin', title: 'Admin'}; - else if ( badge.classList.contains('global-moderator') ) - badges[0] = {klass: 'global-moderator', title: 'Global Moderator'}; - else if ( badge.classList.contains('moderator') ) - badges[0] = {klass: 'moderator', title: 'Moderator'}; - else if ( badge.classList.contains('subscriber') ) - badges[10] = {klass: 'subscriber', title: 'Subscriber'}; - else if ( badge.classList.contains('turbo') ) - badges[15] = {klass: 'turbo', title: 'Turbo'}; - } - - if ( user_id && user_id === room_id ) - badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; - - if ( user_id ) - badges = this.get_badges(user_id, room_id, badges, null); - - badges_el.innerHTML = this.render_badges(badges); - } - - if ( ! reprocess && from_el ) { - from_el.style.fontWeight = ""; - if ( colors ) { - from_el.classList.add('has_color'); - from_el.style.color = is_dark ? colors[1] : colors[0]; - } - - if ( alias ) { - from_el.classList.add('ffz-alias'); - from_el.title = from_el.textContent; - from_el.textContent = alias; - } - } - - if ( ! message_el ) - return; - - if ( ! reprocess && colors && message_el.style.color ) { - message_el.classList.add('has-color'); - message_el.style.color = is_dark ? colors[1] : colors[0]; - } - - var raw_tokens = line.getAttribute('data-tokens'), - tokens = raw_tokens ? JSON.parse(raw_tokens) : []; - - if ( ! raw_tokens ) { - for(var i=0; i < message_el.childNodes.length; i++) { - var node = message_el.childNodes[i]; - - if ( node.nodeType === node.TEXT_NODE ) - tokens.push(node.textContent); - - else if ( node.nodeType === node.ELEMENT_NODE ) { - if ( node.tagName === 'IMG' ) - tokens.push({ - type: "emoticon", - altText: node.alt, - imgSrc: node.src - }); - - else if ( node.tagName === 'A' ) - tokens.push({ - type: "link", - isDeleted: false, - isLong: false, - length: node.textContent.length, - link: node.href, - text: node.textContent - }); - - else if ( node.tagName === 'SPAN' ) - tokens.push({ - type: "mention", - user: node.textContent, - isOwnMessage: node.classList.contains('mentioning') - }); - - else { - this.log("Unknown Tag Type: " + node.tagName); - tokens.push({ - isRaw: true, - html: node.outerHTML - }); - } - - } else - this.log("Unknown Node Type Tokenizing Message: " + node.nodeType); - } - } - - line.setAttribute('data-tokens', JSON.stringify(tokens)); - - // Further tokenization~! - if ( this.settings.replace_bad_emotes ) - tokens = this.tokenize_replace_emotes(tokens); - - tokens = this._remove_banned(tokens); - tokens = this.tokenize_emotes(user_id, room_id, tokens, false); - - if ( this.settings.parse_emoji ) - tokens = this.tokenize_emoji(tokens); - - tokens = this.tokenize_mentions(tokens); - - // Check for a mention - if ( ! line.classList.contains('ffz-mentioend') ) - for(var i=0; i < tokens.length; i++) - if ( tokens[i].mentionedUser ) { - line.classList.add('ffz-mentioned'); - break; - } - - // Now, put the content back into the element. - message_el.innerHTML = this.render_tokens(tokens); - - // Interactions - jQuery('a.deleted-link', message_el).click(f._deleted_link_click); - jQuery('img.emoticon', message_el).click(function(e) { f._click_emote(e.target, e); }); -} \ No newline at end of file diff --git a/src/ext/warpworld.js b/src/ext/warpworld.js new file mode 100644 index 00000000..5b113df7 --- /dev/null +++ b/src/ext/warpworld.js @@ -0,0 +1,30 @@ +var FFZ = window.FrankerFaceZ; + +// -------------------- +// Initialization +// -------------------- + +FFZ.settings_info.warp_world = { + type: "boolean", + value: true, + + category: "Channel Metadata", + name: "Warp World (Requires Refresh)", + + help: 'Automatically load Warp World when viewing a channel that uses Warp World.' +} + +FFZ.ws_commands.warp_world = function(data) { + if ( ! data || ! this.settings.warp_world ) + return; + + // Make sure that Warp World isn't already loaded or loading. + var ww_script = document.querySelector('script#ww_script'); + if ( ww_script || window.WarpWorld ) + return; + + ww_script = document.createElement('script'); + ww_script.id = 'ww_script'; + ww_script.src = '//cdn.warp.world/twitch_script/main.min.js?_=' + Date.now(); + document.head.appendChild(ww_script); +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 5de67534..f3cfaea6 100644 --- a/src/main.js +++ b/src/main.js @@ -37,7 +37,7 @@ FFZ.msg_commands = {}; // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 216, + major: 3, minor: 5, revision: 247, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -167,8 +167,9 @@ require('./badges'); require('./tokenize'); //require('./filtering'); - +require('./ember/wrapper'); require('./ember/router'); +require('./ember/bits'); require('./ember/channel'); require('./ember/player'); require('./ember/room'); @@ -188,7 +189,6 @@ require('./ember/sidebar'); require('./debug'); -//require('./ext/rechat'); require('./ext/betterttv'); require('./ext/emote_menu'); @@ -214,6 +214,7 @@ require('./ui/about_page'); require('./commands'); require('./ext/api'); +require('./ext/warpworld'); // --------------- @@ -404,6 +405,7 @@ FFZ.prototype.init_ember = function(delay) { // Initialize all the modules. this.load_settings(); + this.setup_ember_wrapper(); // Start this early, for quick loading. this.setup_dark(); @@ -426,6 +428,7 @@ FFZ.prototype.init_ember = function(delay) { this.setup_room(); this.setup_vod_chat(); this.setup_line(); + this.setup_bits(); this.setup_layout(); this.setup_chatview(); this.setup_conversations(); @@ -446,10 +449,13 @@ FFZ.prototype.init_ember = function(delay) { this.setup_following_count(true); this.setup_races(); + + // Do all Ember modification before this point. + this.finalize_ember_wrapper(); + this.fix_tooltips(); this.connect_extra_chat(); - //this.setup_rechat(); this.setup_message_event(); this.find_bttv(10); this.find_emote_menu(10); diff --git a/src/settings.js b/src/settings.js index 4146335d..6a645a59 100644 --- a/src/settings.js +++ b/src/settings.js @@ -9,6 +9,23 @@ var FFZ = window.FrankerFaceZ, return "ffz_setting_" + key; }, + favorite_setting = function(swit, key, info) { + var ind = this.settings.favorite_settings.indexOf(key); + + if ( ind === -1 ) { + this.settings.favorite_settings.push(key); + swit.setAttribute('original-title', 'Unfavorite this Setting'); + swit.classList.add('active'); + } else { + this.settings.favorite_settings.splice(ind,1); + swit.setAttribute('original-title', 'Favorite this Setting'); + swit.classList.remove('active'); + } + + jQuery(swit).trigger('mouseout').trigger('mouseover'); + this.settings.set('favorite_settings', this.settings.favorite_settings); + }, + 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" ) @@ -208,12 +225,71 @@ FFZ.prototype._load_settings_file = function(data, hide_alert) { // -------------------- var is_android = navigator.userAgent.indexOf('Android') !== -1, - settings_renderer = function(settings_data, collapsable, collapsed_key) { + settings_renderer = function(settings_data, collapsable, collapsed_key, show_pin) { return function(view, container) { var f = this, settings = {}, categories = []; + // Searching! + if ( show_pin ) { + var search_cont = utils.createElement('div', 'ffz-filter-container'), + search_input = utils.createElement('input', 'emoticon-selector__filter-input js-filter-input text text--full-width'), + filtered_cont = utils.createElement('div', 'ffz-filter-children ffz-ui-sub-menu-page'); + + search_input.placeholder = 'Search for Settings'; + search_input.type = 'text'; + + filtered_cont.style.maxHeight = (parseInt(container.style.maxHeight) - 51) + 'px'; + + search_cont.appendChild(search_input); + container.appendChild(filtered_cont); + container.appendChild(search_cont); + + container = filtered_cont; + + search_input.addEventListener('input', function(e) { + var filter = search_input.value || '', + groups = filtered_cont.querySelectorAll('.chat-menu-content'); + + filter = filter.toLowerCase(); + + for(var i=0; i < groups.length; i++) { + var el = groups[i], + settings = el.querySelectorAll('.ffz-setting'), + hidden = true; + + for(var j=0; j < settings.length; j++) { + var se = settings[j], + shidden = filter.length && se.getAttribute('data-filter').indexOf(filter) === -1; + + se.classList.toggle('hidden', shidden); + hidden = hidden && shidden; + } + + var incompat = el.querySelector('.bttv-incompatibility'), + settings = incompat && incompat.querySelectorAll('b'), + incompat_hidden = true; + + if ( incompat ) { + for(var j=0; j < settings.length; j++) { + var se = settings[j], + shidden = filter.length && se.getAttribute('data-filter').indexOf(filter) === -1; + + se.classList.toggle('hidden', shidden); + incompat_hidden = incompat_hidden && shidden; + } + + incompat.classList.toggle('hidden', incompat_hidden); + hidden = hidden && incompat_hidden; + } + + el.classList.toggle('collapsable', ! filter.length); + el.classList.toggle('hidden', hidden); + } + }); + } + for(var key in settings_data) { var info = settings_data[key], cat = info.category || "Miscellaneous", @@ -254,7 +330,7 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, return 0; }); - var current_category = (collapsed_key ? this[collapsed_key] : null) || categories[0]; + var current_category = collapsed_key ? this[collapsed_key] || true : categories[0]; for(var ci=0; ci < categories.length; ci++) { var category = categories[ci], @@ -274,15 +350,23 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, if ( collapsable ) { menu.classList.add('collapsable'); menu.classList.toggle('collapsed', current_category !== category); - menu.addEventListener('click', function() { + menu.addEventListener('click', function(e) { var t = this; - if ( ! t.classList.contains('collapsed') ) + if ( ! t.classList.contains('collapsable') ) return; + else if ( ! t.classList.contains('collapsed') ) { + if ( e.target.classList.contains('heading') ) { + t.classList.add('collapsed'); + if ( collapsed_key ) + f[collapsed_key] = true; + } - jQuery(".chat-menu-content:not(.collapsed)", container).addClass("collapsed"); - t.classList.remove('collapsed'); - if ( collapsed_key ) - f[collapsed_key] = t.getAttribute('data-category'); + } else { + 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()}); }); @@ -313,15 +397,26 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, for(var i=0; i < cset.length; i++) { var key = cset[i][0], info = cset[i][1], + el = createElement('p'), + pin_btn = createElement('a'), val = info.get ? (typeof info.get === 'function' ? info.get.call(this) : this.settings.get(info.get)) : this.settings.get(key); - el.className = 'clearfix'; + el.className = 'ffz-setting clearfix'; if ( this.has_bttv && info.no_bttv ) { bttv_skipped.push([info.name, info.help]); continue; } else { + if ( show_pin ) { + var faved = this.settings.favorite_settings.indexOf(key) !== -1; + pin_btn.className = 'pin-switch html-tooltip'; + pin_btn.classList.toggle('active', faved); + pin_btn.addEventListener('click', favorite_setting.bind(this, pin_btn, key, info)); + pin_btn.title = (faved ? 'Unf' : 'F') + 'avorite this Setting'; + pin_btn.innerHTML = constants.STAR; + } + if ( info.type === "boolean" ) { var swit = createElement('a'), label = createElement('span'); @@ -334,6 +429,8 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, label.innerHTML = info.name; el.appendChild(swit); + if ( show_pin ) + el.appendChild(pin_btn); el.appendChild(label); swit.addEventListener('click', toggle_setting.bind(this, swit, key, info)) @@ -356,6 +453,8 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, select.addEventListener('change', option_setting.bind(this, select, key, info)); + if ( show_pin ) + el.appendChild(pin_btn); el.appendChild(label); el.appendChild(select); @@ -364,6 +463,9 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, var link = createElement('a'); link.innerHTML = info.name; link.href = '#'; + + if ( show_pin ) + el.appendChild(pin_btn); el.appendChild(link); link.addEventListener('click', info.method.bind(this)); @@ -371,14 +473,27 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, } else continue; - if ( info.help || (this.has_bttv && info.warn_bttv) ) { + if ( info.help || info.experiment_warn || (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 || ""); + var parts = []; + if ( info.experiment_warn ) + parts.push('Note: This affects an active Twitch experiment. Give feedback at: feedback@twitch.tv'); + + if ( this.has_bttv && info.warn_bttv ) + parts.push('' + info.warn_bttv + ''); + + if ( info.help ) + parts.push(info.help); + + help.innerHTML = parts.join('
'); el.appendChild(help); } } + // Search by any of the present text. + el.setAttribute('data-filter', el.textContent.toLowerCase()); + added++; menu.appendChild(el); } @@ -397,8 +512,9 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, help.className = 'help'; for(var i=0; i < bttv_skipped.length; i++) { - var skipped = bttv_skipped[i]; - help.innerHTML += (i > 0 ? ', ' : '') + '' + skipped[0] + ''; + var skipped = bttv_skipped[i], + filter_text = skipped[0].toLowerCase() + (skipped[1] ? ' ' + skipped[1].toLowerCase() : ''); + help.innerHTML += '' + skipped[0] + ''; } el.appendChild(label); @@ -414,7 +530,13 @@ var is_android = navigator.userAgent.indexOf('Android') !== -1, }, render_basic = settings_renderer(FFZ.basic_settings, false, '_ffz_basic_settings_page'), - render_advanced = settings_renderer(FFZ.settings_info, true, '_ffz_settings_page'); + render_advanced = settings_renderer(FFZ.settings_info, true, '_ffz_settings_page', true); + + +FFZ.settings_info.favorite_settings = { + value: [], + hidden: true +} FFZ.menu_pages.settings = { @@ -423,12 +545,42 @@ FFZ.menu_pages.settings = { sort_order: 99999, wide: true, - default_page: function() { return this.settings.advanced_settings ? 'advanced' : 'basic' }, + default_page: function() { return this.settings.favorite_settings.length ? "favorites" : this.settings.advanced_settings ? 'advanced' : 'basic' }, pages: { + favorites: { + name: "Favorites", + sort_order: 1, + + render: function(view, container) { + var favorites = this.settings.favorite_settings; + if ( ! favorites.length ) { + var el = utils.createElement('div'); + el.className = 'emoticon-grid ffz-no-emotes center'; + el.innerHTML = "You have no favorite settings.
" + + '
' + + 'To make a setting a favorite, find it on the Advanced tab and click the star icon to the right.'; + + container.appendChild(el); + return; + } + + var favorite_settings = {}; + for(var i=0, l = favorites.length; i < l; i++) { + var key = favorites[i], + val = FFZ.settings_info[key]; + + if ( val ) + favorite_settings[key] = val; + } + + return settings_renderer(favorite_settings, false, '_ffz_favorite_settings_page').call(this, view, container); + } + }, + basic: { name: "Basic", - sort_order: 1, + sort_order: 2, render: function(view, container) { this.settings.set("advanced_settings", false); @@ -438,7 +590,7 @@ FFZ.menu_pages.settings = { advanced: { name: "Advanced", - sort_order: 2, + sort_order: 3, render: function(view, container) { this.settings.set("advanced_settings", true); @@ -448,7 +600,7 @@ FFZ.menu_pages.settings = { backup: { name: "Backup & Restore", - sort_order: 3, + sort_order: 4, render: function(view, container) { var backup_head = createElement('div'), diff --git a/src/styles/badges-blank.css b/src/styles/badges-blank.css index 3fec7ca8..170c88ce 100644 --- a/src/styles/badges-blank.css +++ b/src/styles/badges-blank.css @@ -1,3 +1,3 @@ .badges .badge:not(.subscriber):not(.unknown-badge) { - background-size: 0px; + background-size: 0px !important; } \ No newline at end of file diff --git a/src/styles/badges-circular.css b/src/styles/badges-circular.css index 166df5f7..a186a221 100644 --- a/src/styles/badges-circular.css +++ b/src/styles/badges-circular.css @@ -1,4 +1,4 @@ -.badges .badge:not(.subscriber):not(.unknown-badge) { +.badges .badge:not(.unknown-badge):not(.transparent) { border-radius: 9px; background-size: 16px; background-repeat: no-repeat; diff --git a/src/styles/badges-transparent.css b/src/styles/badges-transparent.css index 92294ef6..240aae3f 100644 --- a/src/styles/badges-transparent.css +++ b/src/styles/badges-transparent.css @@ -4,15 +4,19 @@ /* Invert Some Badges */ -.badge:not(.subscriber):not(.ffz-badge-1) { +.ffz-dark .badge.invert-invert, +.theatre .badge.invert-invert, +.dark .badge.invert-invert, +.force-dark .badge.invert-invert, +.badge:not(.no-invert):not(.invert-invert) { filter: invert(75%); -webkit-filter: invert(75%); } -.ffz-dark .badge, -.theatre .badge, -.dark .badge, -.force-dark .badge { +.ffz-dark .badge:not(.invert-invert), +.theatre .badge:not(.invert-invert), +.dark .badge:not(.invert-invert), +.force-dark .badge:not(.invert-invert) { filter: none !important; -webkit-filter: none !important; } \ No newline at end of file diff --git a/src/tokenize.js b/src/tokenize.js index 713ca2c7..35e3552e 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -4,6 +4,8 @@ var FFZ = window.FrankerFaceZ, helpers, conv_helpers, emote_helpers, + bits_helpers, + bits_service, 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.', @@ -13,6 +15,8 @@ var FFZ = window.FrankerFaceZ, LINK = /(?:https?:\/\/)?(?:[-a-zA-Z0-9@:%_\+~#=]+\.)+[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/g, + CLIP_URL = /(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)\/(\w+)/, + LINK_SPLIT = /^(?:(https?):\/\/)?(?:(.*?)@)?([^\/:]+)(?::(\d+))?(.*?)(?:\?(.*?))?(?:\#(.*?))?$/, YOUTUBE_CHECK = /^(?:https?:\/\/)?(?:m\.|www\.)?youtu(?:be\.com|\.be)\/(?:v\/|watch\/|.*?(?:embed|watch).*?v=)?([a-zA-Z0-9\-_]+)$/, IMGUR_PATH = /^\/(?:gallery\/)?[A-Za-z0-9]+(?:\.(?:png|jpg|jpeg|gif|gifv|bmp))?$/, @@ -123,6 +127,18 @@ FFZ.settings_info.timestamp_seconds = { }; +FFZ.settings_info.collect_bits = { + type: "boolean", + value: false, + + category: "Chat Appearance", + no_bttv: true, + + name: "Bits Stacking", + help: "Collect all the bits emoticons in a message into a single one at the start of the message." +}; + + FFZ.settings_info.show_deleted_links = { type: "boolean", value: false, @@ -155,6 +171,14 @@ FFZ.prototype.setup_tokenization = function() { if ( ! helpers ) return this.log("Unable to get chat helper functions."); + try { + bits_helpers = window.require && window.require("web-client/utilities/bits/tokenize"); + } catch(err) { + this.error("Unable to get bits tokenizer.", err); + } + + bits_service = utils.ember_lookup('service:bits-rendering-config'); + try { conv_helpers = window.require && window.require("web-client/helpers/twitch-conversations/conversation-line-helpers"); } catch(err) { @@ -268,7 +292,31 @@ FFZ.prototype.load_twitch_emote_data = function(tries) { FFZ.prototype.render_tooltip = function(el) { var f = this, func = function() { - if ( this.classList.contains('emoticon') ) { + if ( this.classList.contains('ffz-bit') ) { + var amount = parseInt(this.getAttribute('data-amount').replace(/,/g, '')), + individuals = JSON.parse(this.getAttribute('data-individuals') || "null"), + tier = bits_service.ffz_get_tier(amount), + preview_url, + image, + out = utils.number_commas(amount) + ' Bit' + utils.pluralize(amount); + + if ( f.settings.emote_image_hover ) + preview_url = bits_service.ffz_get_preview(tier[1]); + + if ( individuals && individuals.length > 1 ) { + out += '
'; + individuals.sort().reverse(); + for(var i=0; i < individuals.length && i < 12; i++) + out += f.render_token(false, false, true, {type: "bits", amount: individuals[i]}); + + if ( individuals.length >= 12 ) + out += '
(and ' + (individuals.length - 12) + ' more)'; + } + + image = preview_url ? '' : ''; + return image + out; + + } else 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 ) { @@ -356,17 +404,29 @@ FFZ.prototype.render_tooltip = function(el) { } else if ( this.classList.contains('chat-link') ) { // TODO: A lot of shit. Lookup data. var url = this.getAttribute("data-url"), + data = url && f._link_data[url], + + preview_url = null, + preview_iframe = true, + image = '', 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; + // Do we have data? + if ( data && data !== true ) { + text = data.html; + preview_url = data.image; + preview_iframe = data.image_iframe !== undefined ? data.image_iframe : true; + } else + preview_url = is_image(url, f.settings.image_hover_all_domains) ? url : null; - image = preview_url ? image_iframe(url) : ''; + if ( f.settings.link_image_hover && preview_url ) + if ( preview_iframe ) + image = image_iframe(url); + else + image = ''; // If it's not a deleted link, don't waste time showing the URL in the tooltip. if ( this.classList.contains('deleted-link') ) @@ -412,12 +472,14 @@ FFZ.prototype.tokenize_conversation_line = function(message, prevent_notificatio if ( user && user.login && helpers && helpers.mentionizeMessage ) tokens = helpers.mentionizeMessage(tokens, user.login, from_me); - if ( helpers && helpers.emoticonizeMessage && emotes ) + if ( helpers && helpers.emoticonizeMessage && emotes && this.settings.parse_emoticons ) tokens = helpers.emoticonizeMessage(tokens, emotes); // FrankerFaceZ Extras tokens = this._remove_banned(tokens); - tokens = this.tokenize_emotes(from_user, undefined, tokens, from_me); + + if ( this.settings.parse_emoticons && this.settings.parse_emoticons !== 2 ) + tokens = this.tokenize_emotes(from_user, undefined, tokens, from_me); if ( this.settings.parse_emoji ) tokens = this.tokenize_emoji(tokens); @@ -456,12 +518,14 @@ FFZ.prototype.tokenize_vod_line = function(msgObject, delete_links) { if ( user && user.login && helpers && helpers.mentionizeMessage ) tokens = helpers.mentionizeMessage(tokens, user.login, from_me); - if ( helpers && helpers.emoticonizeMessage && emotes ) + if ( helpers && helpers.emoticonizeMessage && emotes && this.settings.parse_emoticons ) tokens = helpers.emoticonizeMessage(tokens, emotes); // FrankerFaceZ Extras tokens = this._remove_banned(tokens); - tokens = this.tokenize_emotes(from_user, room_id, tokens, from_me); + + if ( this.settings.parse_emoticons && this.settings.parse_emoticons !== 2 ) + tokens = this.tokenize_emotes(from_user, room_id, tokens, from_me); if ( this.settings.parse_emoji ) tokens = this.tokenize_emoji(tokens); @@ -486,7 +550,7 @@ FFZ.prototype.tokenize_vod_line = function(msgObject, delete_links) { } -FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, delete_links, use_bits) { +FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, delete_links) { if ( msgObject.cachedTokens ) return msgObject.cachedTokens; @@ -495,12 +559,17 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del from_user = msgObject.from, user = this.get_user(), from_me = user && from_user === user.login, - emotes = msgObject.tags && msgObject.tags.emotes, + tags = msgObject.tags || {}, + emotes = tags.emotes, tokens = [msg]; - // Standard tokenization - if ( use_bits && helpers && helpers.tokenizeBits ) - tokens = helpers.tokenizeBits(tokens); + // Standard Tokenization + if ( tags.bits && bits_helpers && bits_helpers.tokenizeBits ) + tokens = bits_helpers.tokenizeBits(tokens); + + // For Later + //if ( helpers && helpers.tokenizeRichContent ) + // tokens = helpers.tokenizeRichContent(tokens, tags.content, delete_links); if ( helpers && helpers.linkifyMessage ) { var labels = msgObject.labels || [], @@ -518,18 +587,37 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del if ( user && user.login && helpers && helpers.mentionizeMessage ) tokens = helpers.mentionizeMessage(tokens, user.login, from_me); - if ( helpers && helpers.emoticonizeMessage ) + if ( helpers && helpers.emoticonizeMessage && this.settings.parse_emoticons ) tokens = helpers.emoticonizeMessage(tokens, emotes); // FrankerFaceZ Extras tokens = this._remove_banned(tokens); - tokens = this.tokenize_emotes(from_user, room_id, tokens, from_me); + + if ( tags.bits && this.settings.collect_bits ) { + var total = 0, individuals = []; + for(var i=0; i < tokens.length; i++) + if ( tokens[i] && tokens[i].type === "bits" ) { + tokens[i].hidden = true; + total += tokens[i].amount || 0; + individuals.push(tokens[i].amount); + } + + tokens.splice(0, 0, { + type: "bits", + amount: total, + individuals: individuals, + length: 0 + }); + } + + if ( this.settings.parse_emoticons && this.settings.parse_emoticons !== 2 ) + tokens = this.tokenize_emotes(from_user, room_id, tokens, from_me); if ( this.settings.parse_emoji ) tokens = this.tokenize_emoji(tokens); // Capitalization - var display = msgObject.tags && msgObject.tags['display-name']; + var display = tags['display-name']; if ( display && display.length ) FFZ.capitalization[from_user] = [display.trim(), Date.now()]; @@ -634,7 +722,7 @@ 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 && this.settings.parse_emoticons && this.settings.parse_emoticons !== 2 ) message = this.tokenize_emotes(user, room, message); if ( this.settings.parse_emoji && ! no_emoji ) @@ -653,7 +741,7 @@ FFZ.prototype.tokenize_feed_body = function(message, emotes, user_id, room_id) { if ( helpers && helpers.linkifyMessage ) message = helpers.linkifyMessage(message); - if ( helpers && helpers.emoticonizeMessage ) + if ( helpers && helpers.emoticonizeMessage && this.settings.parse_emoticons ) message = helpers.emoticonizeMessage(message, emotes); // Tokenize Lines @@ -680,7 +768,8 @@ FFZ.prototype.tokenize_feed_body = function(message, emotes, user_id, room_id) { } } - tokens = this.tokenize_emotes(user_id, room_id, tokens) + if ( this.settings.parse_emoticons && this.settings.parse_emoticons !== 2 ) + tokens = this.tokenize_emotes(user_id, room_id, tokens) if ( this.settings.parse_emoji ) tokens = this.tokenize_emoji(tokens); @@ -689,7 +778,7 @@ FFZ.prototype.tokenize_feed_body = function(message, emotes, user_id, room_id) { } -FFZ.prototype.render_token = function(render_links, warn_links, token) { +FFZ.prototype.render_token = function(render_links, warn_links, render_bits, token) { if ( ! token ) return ""; @@ -764,7 +853,24 @@ FFZ.prototype.render_token = function(render_links, warn_links, token) { if (!( this._link_data && this._link_data[href] )) { this._link_data = this._link_data || {}; this._link_data[href] = true; - this.ws_send("get_link", href, load_link_data.bind(this, href)); + + var success = load_link_data.bind(this, href), + clip_info = CLIP_URL.exec(href); + + if ( clip_info ) { + var clips = utils.ember_lookup('service:clips'); + clips && clips.getClipInfo(clip_info[1], clip_info[2]).then(function(data) { + success(true, { + image: data.previewImage, + image_iframe: false, + html: '' + utils.sanitize(data.title) + '' + + 'Channel: ' + utils.sanitize(data.broadcasterDisplayName) + + '
Game: ' + utils.sanitize(data.game) + }); + }); + + } else + this.ws_send("get_link", href, success); } } } @@ -784,6 +890,14 @@ FFZ.prototype.render_token = function(render_links, warn_links, token) { return '' + utils.sanitize(text) + ''; } + else if ( token.type === "bits" ) { + var tier = render_bits && bits_service.ffz_get_tier(token.amount) || [null, null]; + if ( ! tier[1] ) + return 'cheer' + token.amount; + + return ''; + } + else if ( token.type === "deleted" ) return '×××'; //return `×××`; @@ -796,15 +910,15 @@ FFZ.prototype.render_token = function(render_links, warn_links, token) { return utils.sanitize(token.text); else if ( typeof token !== "string" ) - return '[invalid token]'; - //return `[invalid token]`; + return '[unknown token]'; + //return `[unknown token]`; return utils.sanitize(token); } -FFZ.prototype.render_tokens = function(tokens, render_links, warn_links) { - return _.map(tokens, this.render_token.bind(this, render_links, warn_links)).join(""); +FFZ.prototype.render_tokens = function(tokens, render_links, warn_links, render_bits) { + return _.map(tokens, this.render_token.bind(this, render_links, warn_links, render_bits)).join(""); } @@ -1100,13 +1214,15 @@ FFZ.prototype._deleted_link_click = function(e) { // History Loading // --------------------- -FFZ.prototype.parse_history = function(history, purged, room_id, delete_links, tmiSession, per_line) { +FFZ.prototype.parse_history = function(history, purged, bad_ids, room_id, delete_links, tmiSession, per_line) { var i = history.length, was_cleared = false; purged = purged || {}; + bad_ids = bad_ids || {}; while(i--) { var msg = history[i], - is_deleted = msg.ffz_deleted = purged[msg.from] || false; + msg_id = msg.tags && msg.tags.id, + is_deleted = msg.ffz_deleted = purged[msg.from] || (msg_id && bad_ids[msg_id]) || false; if ( is_deleted && ! this.settings.prevent_clear ) msg.deleted = true; @@ -1159,8 +1275,17 @@ FFZ.prototype.parse_history = function(history, purged, room_id, delete_links, t if ( msg.tags && msg.tags.target === '@@' ) was_cleared = true; - else if ( msg.tags && msg.tags.target ) - purged[msg.tags.target] = true; + else if ( msg.tags && msg.tags.target ) { + var ban_reason = msg.tags && msg.tags['ban-reason'], + ban_id = ban_reason && constants.UUID_TEST.exec(ban_reason); + + if ( ban_id ) { + bad_ids[ban_id[1]] = true; + ban_reason = ban_reason.substr(0, ban_reason.length - ban_id[0].length); + msg.tags['ban-reason'] = ban_reason ? ban_reason : undefined; + } else + purged[msg.tags.target] = true; + } // Per-line if ( per_line && ! per_line(msg) ) diff --git a/src/ui/about_page.js b/src/ui/about_page.js index be95fdd1..5d146816 100644 --- a/src/ui/about_page.js +++ b/src/ui/about_page.js @@ -131,6 +131,10 @@ var update_player_stats = function(player, container) { if ( ! player_data ) return; + try { + player_data.backend = player.getBackend(); + } catch(err) { player_data.backend = undefined } + var sorted_keys = Object.keys(player_data).sort(); for(var i=0; i < sorted_keys.length; i++) { var key = sorted_keys[i], @@ -309,6 +313,9 @@ FFZ.menu_pages.about = { ['Deploy Flavor', SiteOptions.deploy_flavor] ], + exp_head = createElement('div'), + experiments = createElement('ul'), + has_memory = window.performance && performance.memory, mem_head = createElement('div'), mem_list = createElement('ul'), @@ -346,8 +353,8 @@ FFZ.menu_pages.about = { heading.className = 'chat-menu-content center'; heading.innerHTML = '

FrankerFaceZ

woofs for nerds
'; - info_head.className = mem_head.className = twitch_head.className = player_head.className = ver_head.className = log_head.className = 'list-header'; - info.className = mem_list.className = twitch.className = player_list.className = vers.className = 'chat-menu-content menu-side-padding version-list'; + info_head.className = exp_head.className = mem_head.className = twitch_head.className = player_head.className = ver_head.className = log_head.className = 'list-header'; + info.className = mem_list.className = twitch.className = experiments.className = player_list.className = vers.className = 'chat-menu-content menu-side-padding version-list'; info_head.innerHTML = 'Client Status'; @@ -394,6 +401,21 @@ FFZ.menu_pages.about = { twitch.appendChild(line); } + var exp_service = utils.ember_lookup('service:experiments'); + if ( exp_service ) { + exp_head.innerHTML = 'Twitch Experiments'; + for(var key in exp_service.values) { + if ( ! exp_service.values.hasOwnProperty(key) ) + continue; + + var val = exp_service.values[key], + line = createElement('li'); + + line.innerHTML = key + '' + utils.sanitize(val) + ''; + experiments.appendChild(line); + } + } + ver_head.innerHTML = 'Versions'; if ( this.has_bttv ) @@ -431,6 +453,11 @@ FFZ.menu_pages.about = { container.appendChild(twitch_head); container.appendChild(twitch); + if ( exp_service ) { + container.appendChild(exp_head); + container.appendChild(experiments); + } + if ( has_memory ) { mem_head.innerHTML = 'Memory Statistics'; setTimeout(update_mem_stats.bind(this,mem_list),0); diff --git a/src/ui/following-count.js b/src/ui/following-count.js index 89be25d4..9e39dd17 100644 --- a/src/ui/following-count.js +++ b/src/ui/following-count.js @@ -73,7 +73,7 @@ FFZ.prototype.setup_following_count = function(has_ember) { return this._following_get_me(); this.log("Connecting to Live Streams model."); - var Stream = utils.ember_resolve('model:stream'); + var Stream = utils.ember_resolve('model:deprecated-stream'); if ( ! Stream ) return this.log("Unable to find Stream model."); diff --git a/src/ui/tooltips.js b/src/ui/tooltips.js index 8028e860..0f4e263a 100644 --- a/src/ui/tooltips.js +++ b/src/ui/tooltips.js @@ -30,6 +30,15 @@ FFZ.prototype.fix_tooltips = function() { }) } + // Fix tipsy invalidation + if ( window.jQuery && jQuery.fn && jQuery.fn.tipsy ) + jQuery.fn.tipsy.revalidate = function() { + jQuery(".tipsy").each(function() { + var t = jQuery.data(this, "tipsy-pointee"); + (!t || !t[0] || !document.contains(t[0])) && jQuery(this).remove(); + }) + }; + // Iterate all existing tipsy stuff~! this.log('Fixing already existing tooltips.'); if ( ! window.jQuery || ! jQuery.cache ) diff --git a/src/utils.js b/src/utils.js index b3fd18f0..fcba7641 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,7 +2,20 @@ var FFZ = window.FrankerFaceZ, constants = require('./constants'); -var sanitize_el = document.createElement('span'), +var createElement = function(tag, className, content) { + var out = document.createElement(tag); + if ( className ) + out.className = className; + if ( content ) + if ( content.nodeType ) + out.appendChild(content); + else + out.innerHTML = content; + + return out; + }, + + sanitize_el = createElement('span'), sanitize = function(msg) { sanitize_el.textContent = msg; @@ -206,25 +219,18 @@ var sanitize_el = document.createElement('span'), // 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'), + var container = createElement('div', 'twitch_subwindow_container'), + subwindow = createElement('div', 'twitch_subwindow ffz-subwindow'), + card = createElement('div', 'card'), + close_button = createElement('div', 'modal-close-button', constants.CLOSE), 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 ) @@ -265,6 +271,7 @@ var sanitize_el = document.createElement('span'), module.exports = FFZ.utils = { + // Ember Manipulation ember_views: function() { return ember_lookup('-view-registry:main') || {}; }, @@ -281,6 +288,38 @@ module.exports = FFZ.utils = { return App.__container__.resolve(thing); }, + ember_reopen_view: function(component, data) { + if ( typeof component === 'string' ) + component = ember_resolve(component); + + data.ffz_modified = true; + + if ( data.ffz_init && ! data.didInsertElement ) + data.didInsertElement = function() { + this._super(); + try { + this.ffz_init(); + } catch(err) { + FFZ.get().error("An error occured running ffz_init on " + this.toString(), err); + } + }; + + if ( data.ffz_destroy && ! data.willClearRender ) + data.willClearRender = function() { + try { + this.ffz_destroy(); + } catch(err) { + FFZ.get().error("An error occured running ffz_destroy on " + this.toString(), err); + } + + this._super(); + }; + + return component.reopen(data); + }, + + // Other Stuff + build_srcset: build_srcset, /*build_tooltip: build_tooltip, load_emote_data: load_emote_data,*/ @@ -294,18 +333,52 @@ module.exports = FFZ.utils = { show_modal: show_modal, + confirm: function(title, description, callback) { + var contents = createElement('div', 'text-content'), + heading = title ? createElement('div', 'content-header', '

' + title + '

') : null, + body = createElement('div', 'item'), + buttons = createElement('div', 'buttons', 'Cancel'), + + close_btn = buttons.querySelector('.js-subwindow-close'), + okay_btn = buttons.querySelector('.button.primary'); + + if ( heading ) + contents.appendChild(heading); + + if ( description ) { + if ( description.nodeType ) + body.appendChild(description); + else + body.innerHTML = '

' + description + '

'; + + contents.appendChild(body); + } + + contents.appendChild(buttons); + + var closer, + cb = function(success) { + closer(); + if ( ! callback ) + return; + + callback(success); + }; + + closer = show_modal(contents, cb); + + okay_btn.addEventListener('click', function(e) { e.preventDefault(); cb(true); return false }); + close_btn.addEventListener('click', function(e) { e.preventDefault(); cb(false); return false }); + }, + prompt: function(title, description, old_value, callback, width, input) { - var contents = document.createElement('div'), - heading = document.createElement('div'), - form = document.createElement('form'), + var contents = createElement('div', 'text-content'), + heading = createElement('div', 'content-header', '

' + title + '

'), + form = createElement('form'), close_btn, okay_btn; - contents.className = 'text-content'; - heading.className = 'content-header'; - heading.innerHTML = '

' + title + '

'; - if ( ! input ) { - input = document.createElement('input'); + input = createElement('input'); input.type = 'text'; } @@ -540,18 +613,19 @@ module.exports = FFZ.utils = { escape_regex: escape_regex, - createElement: function(tag, className, content) { - var out = document.createElement(tag); - if ( className ) - out.className = className; - if ( content ) - out.innerHTML = content; - return out; - }, + createElement: createElement, toggle_cls: function(cls) { return function(val) { document.body.classList.toggle(cls, val); } + }, + + badge_css: function(badge, klass) { + klass = klass || ('ffz-badge-' + badge.id); + var out = ".badges ." + klass + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.css || "") + '}'; + if ( badge.alpha_image ) + out += ".badges .badge.alpha." + klass + ",.ffz-transparent-badges .badges ." + klass + ' { background-image: url("' + badge.alpha_image + '"); }'; + return out; } } \ No newline at end of file diff --git a/style.css b/style.css index 1661e30a..0ac8dfbb 100644 --- a/style.css +++ b/style.css @@ -321,10 +321,6 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill: background-color: #25252a; } -.ffz-theater-stats .app-main.theatre .button:not(.primary) { - color: #a68ed2; -} - .ffz-theater-stats .app-main.theatre .button.button--icon-only svg path { fill: #a68ed2; } @@ -501,6 +497,11 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill: border-top-color: rgba(255,255,255, 0.2); } +.ffz-dark .ffz-ui-menu-page .ffz-filter-container, +.theatre .ffz-ui-menu-page .ffz-filter-container, +.dark .ffz-ui-menu-page .ffz-filter-container, +.force-dark .ffz-ui-menu-page .ffz-filter-container, + .ffz-dark .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content .heading, .theatre .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content .heading, .dark .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content .heading, @@ -598,17 +599,18 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill: .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, -.chat-menu-content.collapsed p { display: none; } +.emoticon-grid.collapsable.collapsed span, +.chat-menu-content.collapsable.collapsed p { display: none; } -.chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content.collapsed .heading, -.chat-menu.ffz-ui-popup .ffz-ui-menu-page .emoticon-grid.collapsed .heading { +.chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content.collapsable.collapsed .heading, +.chat-menu.ffz-ui-popup .ffz-ui-menu-page .emoticon-grid.collapsable.collapsed .heading { padding-bottom: 0; } .emoticon-grid.collapsable .heading, -.emoticon-grid.collapsed, -.chat-menu-content.collapsed { +.emoticon-grid.collapsable.collapsed, +.chat-menu-content.collapsable.collapsed, +.chat-menu-content.collapsable .heading { cursor: pointer; } @@ -617,6 +619,25 @@ body.ffz-bttv-dark .ffz-ui-toggle.blue.live:hover svg.svg-emoticons path { fill: position: relative; } +.pin-switch { + display: block; + height: 16px; + padding: 2px; + margin: 0 !important; + cursor: pointer; +} + +.pin-switch:hover svg path, +.pin-switch.active svg path { fill: #14b866 !important } +.pin-switch.active:hover svg path { fill: #fc3636 !important } + +.pin-switch svg path { fill: rgba(0,0,0,0.2); } + +.dark .pin-switch svg path, +.force-dark .pin-switch svg path, +.theatre .dark .pin-switch svg path { fill: rgba(255,255,255,0.2) } + +.pin-switch, .list-header span.right { float: right; } .ember-chat .chat-menu .list-header.sub-header { @@ -1307,7 +1328,7 @@ img.channel_background[src="null"] { display: none; } .ffz-moderation-card .right button:last-of-type, .ffz-moderation-card .mod-controls:last-of-type button:last-of-type { margin-right: 0 } -.ffz-moderation-card .follow-button a { +.ffz-moderation-card .follow-button { font-size: 0 !important; padding-right: 0 !important; } @@ -1413,6 +1434,8 @@ img.channel_background[src="null"] { display: none; } /* Chat Rows */ +.chat-line { overflow: hidden } + .theatre .conversation-window .conversation-chat-line, .dark .chat-line, .force-dark .chat-line, @@ -1456,6 +1479,7 @@ body:not(.ffz-bttv) .chat-container:not(.chatReplay) .more-messages-indicator { padding: 0; } + /* Emoticon Tooltips */ .ffz-wide-tip hr { @@ -2170,6 +2194,14 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter .ffz-no-blue .theatre .conversation-system-message, .ffz-no-blue.ffz-dark .conversation-system-message, +.ffz-no-blue .theatre .bits-card, +.ffz-no-blue .dark .bits-card, +.ffz-no-blue .force-dark .bits-card, + +.ffz-no-blue .theatre .bits-card--standard, +.ffz-no-blue .dark .bits-card--standard, +.ffz-no-blue .force-dark .bits-card--standard, + .ffz-no-blue .warp, .ffz-no-blue #large_nav .content, .ffz-no-blue #small_nav .content, @@ -2370,6 +2402,14 @@ li[data-name="following"] a { /* Image Tooltips */ +.ffz-clip-title { + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-bottom: 5px; +} + .ffz-image-hover { border:none; max-width: 186px; @@ -2802,14 +2842,17 @@ body:not(.ffz-top-conversations) .conversations-list-bottom-bar { /* Creative Directory */ -.ffz-top-conversations .ct-banner { margin-top: -50px } -.ffz-top-conversations .ct-banner__feature { top: 40px; margin-bottom: 0 } - -body:not(.ffz-tags-on-channel) #broadcast-meta .ct-tags, -body:not(.ffz-creative-showcase) .ct-gallery { display: none; } +/* Make sure the fancy banner art goes all the way up */ +.ffz-top-conversations .ct-banner-container { margin-top: -50px } body:not(.ffz-tags-on-channel) .tw-title--tall { height: 60px } +body:not(.ffz-tags-on-channel) #broadcast-meta .ct-tags, +/*body:not(.ffz-creative-showcase) .ct-banner-handler .tower > div:last-child,*/ +body:not(.ffz-creative-showcase) .ct-spotlight-container { display: none; } + + + /* Content-Box~! Down with Twitch randomly changing the box-sizing for everything! */ #ffz-channel-table *, @@ -2874,33 +2917,42 @@ body:not([data-current-path^="directory.csgo"]):not([data-current-path^="directo background-size: cover !important; } -.follow-button.ffz-block-button { - margin-left: 2px; +.directory_header .ffz-block-button, +.directory_header .ffz-spoiler-button { + margin-left: 1rem; + vertical-align: top; +} + +.balloon .balloon__title button { + display: block; +} + +.balloon .balloon__title button:not(:last-of-type) { + margin-bottom: 1rem; } body[data-current-path^="directory.creative"] .follow-button.ffz-block-button { margin: 0 1rem; } -.follow-button.ffz-spoiler-button .spoiler, -.follow-button.ffz-block-button .block { +.button.ffz-spoiler-button, +.button.ffz-block-button { background: #555; - padding: 0 10px; } -.follow-button.ffz-spoiler-button .spoiler.active { +.button.ffz-spoiler-button.active { background: #006700; } -.follow-button.ffz-spoiler-button .spoiler:not(.disabled):hover { +.button.ffz-spoiler-button:not(.disabled):hover { background: #247324; } -.follow-button.ffz-block-button .block.active { +.button.ffz-block-button.active { background: #973333; } -.follow-button.ffz-block-button .block:not(.disabled):hover { +.button.ffz-block-button:not(.disabled):hover { background: #a94444; } @@ -2946,6 +2998,8 @@ body.ffz-bttv #ffz-feed-tabs .tabs { margin-bottom: 0 } /* Badges */ +.badges .badge { background-size: 18px 18px } + /*.badges .badge { height: 18px; min-width: 18px; @@ -3001,18 +3055,41 @@ body.ffz-bttv #ffz-feed-tabs .tabs { margin-bottom: 0 } /* Odd Badges */ .badge.click_url { cursor: pointer } -.badge.warcraft.version-alliance { - background: url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/1.png") #004094; - background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/1.png") 1x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/2.png") 2x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/4.png") 4x); - background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/1.png") 1x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/2.png") 2x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/alliance/4.png") 4x); +.badge.bits.version-1 { + background: url("https://cdn.frankerfacez.com/badges/twitch/bits/1/1.png") #cbc8d0; + background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/1/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1/4.png") 4x); + background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/1/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1/4.png") 4x); } -.badge.warcraft.version-horde { - background: url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/1.png") #ab0016; - background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/1.png") 1x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/2.png") 2x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/4.png") 4x); - background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/1.png") 1x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/2.png") 2x,url("https://cdn.frankerfacez.com/badges/twitch/warcraft/horde/4.png") 4x); +.badge.bits.version-100 { + background: url("https://cdn.frankerfacez.com/badges/twitch/bits/100/1.png") #ca7eff; + background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/100/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100/4.png") 4x); + background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/100/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100/4.png") 4x); } +.badge.bits.version-1000 { + background: url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/1.png") #3ed8b3; + background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/4.png") 4x); + background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/1000/4.png") 4x); +} + +.badge.bits.version-5000 { + background: url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/1.png") #49acff; + background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/4.png") 4x); + background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/5000/4.png") 4x); +} + +.badge.bits.version-10000 { + background: url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/1.png") #ff271e; + background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/4.png") 4x); + background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/10000/4.png") 4x); +} + +.badge.bits.version-100000 { + background: url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/1.png") #ffcb13; + background-image: -webkit-image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/4.png") 4x); + background-image: image-set(url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/1.png") 1x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/2.png") 2x, url("https://cdn.frankerfacez.com/badges/twitch/bits/100000/4.png") 4x); +} /* New Resub Banner */ @@ -3045,4 +3122,58 @@ body.ffz-bttv #ffz-feed-tabs .tabs { margin-bottom: 0 } .dark .chat-room .show-mod-icons .chat-line:not(.admin) .mod-icons, .force-dark .chat-room .show-mod-icons .chat-line:not(.admin) .mod-icons { background-color: rgba(0,0,0,0.8); +} + + +/* Bits */ + +.bits-card { z-index: 10 } + +.ffz-bit { + display: inline-block; + height: 28px; + line-height: 28px; + font-weight: bold; + padding-left: 30px; + margin: -5px 5px; + box-sizing: content-box; + background-position: 0; +} + +.tipsy .ffz-bit { + margin: 0 5px; +} + +.ffz-bit:after { + content: attr(data-amount); +} + +/* New Chat Formatting */ + +.ember-chat .chat-messages .timestamp { margin-right: 0; } +.badges { display: inline-block } +.badges .badge { + float: none; + margin: -1px 3px 0 0; +} + +.ffz-baseline-emoticons .activity-body .emoticon, +.ffz-baseline-emoticons .chat-line .emoticon { + vertical-align: baseline; + padding-top: 5px; +} + + +/* Settings Filtering */ + +.ffz-ui-menu-page .ffz-filter-container { + padding: 10px; + border-top: 1px solid rgba(0,0,0,0.2); +} + +.ffz-ui-menu-page input.js-filter-input { margin: 0 } + +.bttv-incompatibility b + b:before { + content: ', '; + font-weight: normal; } \ No newline at end of file