From c167a8b626828c1e267ef0d67f689e58144a0abe Mon Sep 17 00:00:00 2001 From: SirStendec Date: Thu, 19 Nov 2015 02:45:56 -0500 Subject: [PATCH 1/2] 3.5.78 to 3.5.83. It's been so long since I've commited. I'm a bad person. Added grouping for hosted channels, and a menu to select which channel you visit. Added weights for selecting which socket server to use. Added server time offset calculations. Refactored a LOT of how chat badges and chat line rendering and history processing works behind the scenes. Removed a ton of duplicate code. Added basic adjacent chat message lookups. Need better server support. --- .gitignore | 1 + dark.css | 10 + gulpfile.js | 44 +++- src/badges.js | 291 ++++++++++++------------- src/constants.js | 8 +- src/ember/channel.js | 6 +- src/ember/chatview.js | 6 +- src/ember/conversations.js | 66 +----- src/ember/directory.js | 388 ++++++++++++++++++++++++++++++--- src/ember/following.js | 8 +- src/ember/line.js | 48 +--- src/ember/moderation-card.js | 237 +++++++++++++++++--- src/ember/room.js | 153 ++++++------- src/ember/router.js | 13 ++ src/ext/rechat.js | 33 ++- src/main.js | 14 +- src/socket.js | 110 +++++++--- src/styles/chat-background.css | 92 ++++++++ src/tokenize.js | 85 +++++++- src/ui/dark.js | 2 +- src/ui/following-count.js | 3 +- src/ui/following.js | 20 +- src/ui/menu.js | 48 +--- src/ui/popups.js | 84 +++++++ src/ui/races.js | 25 +-- src/ui/styles.js | 2 +- src/utils.js | 52 ++++- style.css | 190 ++++++++++++++++ 28 files changed, 1501 insertions(+), 538 deletions(-) create mode 100644 src/ui/popups.js diff --git a/.gitignore b/.gitignore index 60225f6d..48d9fe44 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ Extension Building *.iml script.js script.min.js +*.min.css credentials.json /socketserver/cmd/socketserver/socketserver diff --git a/dark.css b/dark.css index 40e00dd1..e189644b 100644 --- a/dark.css +++ b/dark.css @@ -142,6 +142,7 @@ .ffz-dark .whatisthis, .ffz-dark .ui-menu, .ffz-dark .dropmenu, +.ffz-dark .sort-contain .sort-options, .ffz-dark .top-dropdown, .ffz-dark form.js-new_panel_form, .ffz-dark .js-new_panel_btn, @@ -902,6 +903,15 @@ /* Conversations */ +.ffz-dark .ignore-cta { + background-color: #333; + box-shadow: 0 3px 0 #000; +} + +.ffz-dark .ignore-cta .conversation-system-message { + color: #ccc; +} + .ffz-dark .conversation-input-bar .emoticon-selector-toggle svg path { fill: rgba(255,255,255,0.2); } diff --git a/gulpfile.js b/gulpfile.js index 071873ab..9d300617 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -24,6 +24,7 @@ var ftp = require('vinyl-ftp'), // Server Dependencies var http = require("http"), + //https = require("https"), path = require("path"), request = require("request"), url = require("url"); @@ -87,12 +88,10 @@ gulp.task('scripts', ['styles'], function() { .pipe(header('(function(window) {')) .pipe(footer(';window.ffz = new FrankerFaceZ()}(window));')) .pipe(gulp.dest(__dirname)) - .pipe(uglify()) - .pipe(rename('script.min.js')) - .pipe(gulp.dest(__dirname)) .on('error', util.log); }); + gulp.task('watch', ['default', 'server'], function() { return gulp.watch('src/**/*', ['default']); }); @@ -102,7 +101,28 @@ gulp.task('default', ['scripts']); // Deploy -gulp.task('upload', ['default'], function() { +gulp.task('minify_script', ['scripts'], function() { + return gulp.src(['script.js']) + .pipe(uglify()) + .pipe(rename('script.min.js')) + .pipe(gulp.dest(__dirname)) + .on('error', util.log); +}); + +gulp.task('minify_style', function() { + return gulp.src(['style.css', 'dark.css']) + .pipe(minifyCss()) + .pipe(rename(function(path) { + path.basename += '.min'; + })) + .pipe(gulp.dest(__dirname)) + .on('error', util.log); +}); + +gulp.task('minify', ['minify_script', 'minify_style']); + + +gulp.task('upload', ['minify'], function() { // Load credentials from an external file. var contents = fs.readFileSync('credentials.json', 'utf8'), cred = JSON.parse(contents); @@ -117,8 +137,8 @@ gulp.task('upload', ['default'], function() { globs = [ "script.min.js", - "style.css", - "dark.css", + "style.min.css", + "dark.min.css", "changelog.html" ]; @@ -140,8 +160,8 @@ gulp.task('clear_cache', ['upload'], function(cb) { files = [], globs = [ "script.min.js", - "style.css", - "dark.css", + "style.min.css", + "dark.min.css", "changelog.html" ]; @@ -177,7 +197,7 @@ gulp.task('deploy', ['upload', 'clear_cache']); // Server gulp.task('server', function() { - http.createServer(function(req, res) { + var handle_req = function(req, res) { var uri = url.parse(req.url).pathname, lpath = path.join(uri).split(path.sep); @@ -220,6 +240,10 @@ gulp.task('server', function() { fs.createReadStream(file).pipe(res); }); - }).listen(8000, "localhost"); + }; + + http.createServer(handle_req).listen(8000, "localhost"); + //https.createServer(handle_req).listen(8000, "localhost"); + util.log("[" + util.colors.cyan("HTTP") + "] Listening on Port: " + util.colors.magenta("8000")); }); \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index 1cf59849..0632e4bc 100644 --- a/src/badges.js +++ b/src/badges.js @@ -1,6 +1,20 @@ var FFZ = window.FrankerFaceZ, constants = require('./constants'), - utils = require('./utils'); + utils = require('./utils'), + + MOD_BADGES = [ + ['staff', 'staff', 'Staff'], + ['admin', 'admin', 'Admin'], + ['global_mod', 'global-moderator', 'Global Moderator'], + ['mod', 'moderator', 'Moderator'] + ], + + badge_css = function(badge) { + var out = ".badges .ffz-badge-" + badge.id + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.extra_css || "") + '}'; + if ( badge.transparent_image ) + out += ".badges .badge.alpha.ffz-badge-" + badge.id + ",.ffz-transparent-badges .badges .ffz-badge-" + badge.id + ' { background-image: url("' + badge.transparent_image + '"); }'; + return out; + }; // -------------------- @@ -151,14 +165,99 @@ FFZ.ws_commands.set_badge = function(data) { // -------------------- -// Badge CSS +// Badge Selection // -------------------- -var badge_css = function(badge) { - var out = ".badges .ffz-badge-" + badge.id + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.extra_css || "") + '}'; - if ( badge.transparent_image ) - out += ".ffz-transparent-badges .badges .ffz-badge-" + badge.id + ' { background-image: url("' + badge.transparent_image + '"); }'; - return out; +FFZ.prototype.get_badges = function(user, room_id, badges, msg) { + var data = this.users[user]; + if ( ! data || ! data.badges || ! this.settings.show_badges ) + return badges; + + for(var slot in data.badges) { + if ( ! data.badges.hasOwnProperty(slot) ) + continue; + + var badge = data.badges[slot], + full_badge = this.badges[badge.id] || {}, + old_badge = badges[slot]; + + if ( full_badge.visible !== undefined ) { + var visible = full_badge.visible; + if ( typeof visible === "function" ) + visible = visible.bind(this)(room_id, user, msg, badges); + + if ( ! visible ) + continue; + } + + if ( old_badge ) { + var replaces = badge.hasOwnProperty('replaces') ? badge.replaces : full_badge.replaces; + if ( ! replaces ) + continue; + + old_badge.image = badge.image || full_badge.image; + old_badge.klass += ' ffz-badge-replacement'; + old_badge.title += ', ' + (badge.title || full_badge.title); + continue; + } + + badges[slot] = { + klass: 'ffz-badge-' + badge.id, + title: badge.title || full_badge.title, + image: badge.image, + color: badge.color, + extra_css: badge.extra_css + }; + } + + return badges; +} + + +FFZ.prototype.get_line_badges = function(msg) { + var badges = {}; + + if ( msg.room && msg.from === msg.room ) + badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; + else if ( msg.labels ) + for(var i=0, l = MOD_BADGES.length; i < l; i++) { + var mb = MOD_BADGES[i]; + if ( msg.labels.indexOf(mb[0]) !== -1 ) { + badges[0] = {klass: mb[1], title: mb[2]} + break; + } + } + + if ( msg.labels.indexOf('subscriber') !== -1 ) + badges[10] = {klass: 'subscriber', title: 'Subscriber'} + if ( msg.labels.indexOf('turbo') !== -1 ) + badges[15] = {klass: 'turbo', title: 'Turbo'}; + + // FFZ Badges + return this.get_badges(msg.from, msg.room, badges, msg); +} + + +FFZ.prototype.get_other_badges = function(user_id, room_id, user_type, has_sub, has_turbo) { + var badges = {}; + + if ( room_id && user_id === room_id ) + badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; + else + for(var i=0, l = MOD_BADGES.length; i < l; i++) { + var mb = MOD_BADGES[i]; + if ( user_type === mb[0] ) { + badges[0] = {klass: mb[1], title: mb[2]}; + break; + } + } + + if ( has_sub ) + badges[10] = {klass: 'subscriber', title: 'Subscriber'} + if ( has_turbo ) + badges[15] = {klass: 'turbo', title: 'Turbo'} + + return this.get_badges(user_id, room_id, badges, null); } @@ -166,6 +265,29 @@ var badge_css = function(badge) { // Render Badge // -------------------- +FFZ.prototype.render_badges = function(badges) { + var out = []; + for(var key in badges) { + var badge = badges[key], + css = badge.image ? 'background-image:url("' + utils.quote_attr(badge.image) + '");' : ''; + + if ( badge.color ) + css += 'background-color:' + badge.color + ';' + + if ( badge.extra_css ) + css += badge.extra_css; + + out.push('
'); + } + + return out.join(""); +} + + +// -------------------- +// Extension Support +// -------------------- + FFZ.prototype.bttv_badges = function(data) { if ( ! this.settings.show_badges ) return; @@ -216,7 +338,7 @@ FFZ.prototype.bttv_badges = function(data) { if ( b.type === full_badge.replaces_type ) { b.type = "ffz-badge-replacement " + b.type; b.description += ", " + (badge.title || full_badge.title) + - '" style="background-image: url("' + (badge.image || full_badge.image) + "")"; + '" style="background-image: url("' + utils.quote_attr(badge.image || full_badge.image) + '")'; replaced = true; break; } @@ -226,14 +348,16 @@ FFZ.prototype.bttv_badges = function(data) { continue; } - if ( badge.image ) - style += 'background-image: url("' + badge.image + '"); '; + if ( alpha && badge.transparent_image ) + style += 'background-image: url("' + utils.quote_attr(badge.transparent_image) + '");'; + else if ( badge.image ) + style += 'background-image: url("' + utils.quote_attr(badge.image) + '");'; if ( badge.color && ! alpha ) - style += 'background-color: ' + badge.color + '; '; + style += 'background-color: ' + utils.quote_attr(badge.color) + '; '; if ( badge.extra_css ) - style += badge.extra_css; + style += utils.quote_attr(badge.extra_css); if ( style ) desc += '" style="' + style; @@ -253,149 +377,6 @@ FFZ.prototype.bttv_badges = function(data) { } -FFZ.prototype.render_badges = function(component, badges) { - if ( ! this.settings.show_badges ) - return badges; - - var user = component.get('msgObject.from') || component.get('message.from.username'), - room_id = component.get('msgObject.room') || App.__container__.lookup('controller:chat').get('currentRoom.id'); - - return this._render_badges(user, room_id, badges, component); -} - -FFZ.prototype._render_badges = function(user, room_id, badges, component) { - var data = this.users[user]; - if ( ! data || ! data.badges ) - return badges; - - for(var slot in data.badges) { - if ( ! data.badges.hasOwnProperty(slot) ) - continue; - - var badge = data.badges[slot], - full_badge = this.badges[badge.id] || {}, - old_badge = badges[slot]; - - if ( full_badge.visible !== undefined ) { - var visible = full_badge.visible; - if ( typeof visible === "function" ) - visible = visible.bind(this)(room_id, user, component, badges); - - if ( ! visible ) - continue; - } - - if ( old_badge ) { - var replaces = badge.hasOwnProperty('replaces') ? badge.replaces : full_badge.replaces; - if ( ! replaces ) - continue; - - old_badge.image = badge.image || full_badge.image; - old_badge.klass += ' ffz-badge-replacement'; - old_badge.title += ', ' + (badge.title || full_badge.title); - continue; - } - - badges[slot] = { - klass: 'ffz-badge-' + badge.id, - title: badge.title || full_badge.title, - image: badge.image, - color: badge.color, - extra_css: badge.extra_css - }; - } - - return badges; -} - - -FFZ.prototype.render_badge = function(component) { - if ( ! this.settings.show_badges ) - return; - - var user = component.get('msgObject.from'), - room_id = App.__container__.lookup('controller:chat').get('currentRoom.id'), - badges = component.$('.badges'); - - var data = this.users[user]; - if ( ! data || ! data.badges ) - return; - - // If we don't have badges, add them. - if ( ! badges.length ) { - var b_cont = document.createElement('span'), - from = component.$('.from'); - - b_cont.className = 'badges float-left'; - - if ( ! from ) - return; - - from.before(b_cont); - badges = $(b_cont); - } - - // Figure out where to place our badge(s). - var before = badges.find('.badge').filter(function(i) { - var t = this.title.toLowerCase(); - return t == "subscriber" || t == "turbo"; - }).first(); - - var badges_out = [], reverse = !(!before.length); - for ( var slot in data.badges ) { - if ( ! data.badges.hasOwnProperty(slot) ) - continue; - - var badge = data.badges[slot], - full_badge = this.badges[badge.id] || {}; - - if ( full_badge.visible !== undefined ) { - var visible = full_badge.visible; - if ( typeof visible == "function" ) - visible = visible.bind(this)(room_id, user); - - if ( ! visible ) - continue; - } - - if ( full_badge.replaces ) { - var el = badges[0].querySelector('.badge.' + full_badge.replaces); - if ( el ) { - el.style.backgroundImage = 'url("' + (badge.image || full_badge.image) + '")'; - el.classList.add("ffz-badge-replacement"); - el.title += ", " + (badge.title || full_badge.title); - continue; - } - } - - var el = document.createElement('div'); - el.className = 'badge float-left tooltip ffz-badge-' + badge.id; - el.setAttribute('title', badge.title || full_badge.title); - - if ( badge.image ) - el.style.backgroundImage = 'url("' + badge.image + '")'; - - if ( badge.color ) - el.style.backgroundColor = badge.color; - - if ( badge.extra_css ) - el.style.cssText += badge.extra_css; - - badges_out.push([((reverse ? 1 : -1) * slot), el]); - } - - badges_out.sort(function(a,b){return a[0] - b[0]}); - - if ( reverse ) { - while(badges_out.length) - before.before(badges_out.shift()[1]); - } else { - while(badges_out.length) - badges.append(badges_out.shift()[1]); - } -} - - // -------------------- // Legacy Support // -------------------- diff --git a/src/constants.js b/src/constants.js index 66f91022..15220f15 100644 --- a/src/constants.js +++ b/src/constants.js @@ -13,8 +13,12 @@ module.exports = { API_SERVER_2: "//direct-api.frankerfacez.com/", WS_SERVER_POOLS: { - 1: ["ws://catbag.frankerfacez.com/", "ws://andknuckles.frankerfacez.com/"], - 2: ["ws://localhost:8001/"] + 1: [ + ["ws://catbag.frankerfacez.com/", 0.5], + ["ws://andknuckles.frankerfacez.com/", 1], + ["ws://tuturu.frankerfacez.com/", 1]], + 2: [ + ["ws://localhost:8001/", 1]] }, TOOLTIP_DISTANCE: 50, diff --git a/src/ember/channel.js b/src/ember/channel.js index f7c6f07b..777e00a8 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -600,10 +600,12 @@ FFZ.prototype._modify_cindex = function(view) { this._ffz_update_uptime = setTimeout(this.ffzUpdateUptime.bind(this), 1000); // Determine when the channel last went live. - var online = this.get("controller.content.stream.created_at"); + var online = this.get("controller.content.stream.created_at"), + now = Date.now() - (f._ws_server_offset || 0); + online = online && utils.parse_date(online); - var uptime = online && Math.floor((Date.now() - online.getTime()) / 1000) || -1; + var uptime = online && Math.floor((now - online.getTime()) / 1000) || -1; if ( uptime < 0 ) { var el = this.get('element').querySelector('#ffz-uptime-display'); if ( el ) diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 9cfee7e4..8359ffc4 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -426,7 +426,7 @@ FFZ.prototype._modify_cview = function(view) { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) f._chatv.$('.chat-room').css('top', f._chatv._ffz_tabs.offsetHeight + "px"); - var controller = f._chatv.get('controller'); + var controller = f._chatv && f._chatv.get('controller'); controller && controller.set('showList', false); }, 1000); }, @@ -444,6 +444,10 @@ FFZ.prototype._modify_cview = function(view) { ffzChangeRoom: Ember.observer('controller.currentRoom', function() { f.update_ui_link(); + // Close mod cards when changing to a new room. + if ( f._mod_card ) + f._mod_card.send('close'); + var room = this.get('controller.currentRoom'), rows; room && room.resetUnreadCount(); diff --git a/src/ember/conversations.js b/src/ember/conversations.js index b9c1ef2a..5345189d 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -64,66 +64,19 @@ FFZ.prototype._modify_conversation_window = function(component) { component.reopen({ headerBadges: Ember.computed("thread.participants", "currentUsername", function() { var e = this.get("thread.participants").rejectBy("username", this.get("currentUsername")).objectAt(0), - badges = {}, - ut = e.get("userType"); + badges = f.get_other_badges(e.get('username'), null, e.get('userType'), false, e.get('hasTurbo')), + out = []; - if ( ut === "staff" ) - badges[0] = {classes: 'badge staff', title: 'Staff'}; - else if ( ut === 'admin' ) - badges[0] = {classes: 'badge admin', title: 'Admin'}; - else if ( ut === 'global_mod' ) - badges[0] = {classes: 'badge global-moderator', title: 'Global Moderator'}; - - if ( e.get('hasTurbo') ) - badges[15] = {classes: 'badge turbo', title: 'Turbo'} - - // FFZ Badges - var data = f.users[e.get('username')]; - if ( data && data.badges ) { - for(var slot in data.badges) { - if ( ! data.badges.hasOwnProperty(slot) ) - continue; - - var badge = data.badges[slot], - full_badge = f.badges[badge.id] || {}, - old_badge = badges[slot]; - - if ( full_badge.visible !== undefined ) { - var visible = full_badge.visible; - if ( typeof visible === "function" ) - try { - visible = visible.bind(f)(null, e.get('username'), null, badges); - } catch(err) { - f.error("badge " + badge.id + " visible: " + err); - continue; - } - - if ( ! visible ) - continue; - } - - if ( old_badge ) { - var replaces = badge.hasOwnProperty('replaces') ? badge.replaces : full_badge.replaces; - if ( ! replaces ) - continue; - - old_badge.klass = 'badge ffz-badge-' + badge.id; - old_badge.title += ', ' + (badge.title || full_badge.title); - continue; - } - - badges[slot] = { - classes: 'badge ffz-badge-' + badge.id, - title: badge.title || full_badge.title - } - } + // It wants slightly different output from us. + for(var slot in badges) { + var badge = badges[slot]; + out.push({ + classes: 'badge ' + badge.klass, + title: badge.title + }); } - var out = []; - for(var slot in badges) - out.push(badges[slot]); - return out; }), @@ -143,6 +96,7 @@ FFZ.prototype._modify_conversation_window = function(component) { header_name.setAttribute('data-color', raw_color); } + jQuery('.badge', el).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); jQuery(el).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); } }); diff --git a/src/ember/directory.js b/src/ember/directory.js index eba12dd4..f107ed32 100644 --- a/src/ember/directory.js +++ b/src/ember/directory.js @@ -1,30 +1,176 @@ var FFZ = window.FrankerFaceZ, utils = require('../utils'), - constants = require('../constants'); + constants = require('../constants'), + + NO_LOGO = "http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; // -------------------- // Settings // -------------------- -FFZ.settings_info.directory_logos = { - type: "boolean", - value: false, +FFZ.settings_info.sidebar_followed_games = { + type: "select", + options: { + 0: "Disabled", + 5: "Normal (5)", + 10: "Large (10)", + 999: "No Limit" + }, + + value: 5, + process_value: function(val) { + if ( typeof val === "string" ) + return parseInt(val) || 0; + return val; + }, category: "Appearance", no_mobile: true, - name: "Directory Logos", + name: "Sidebar Followed Games", + help: "Display this number of followed games on the sidebar.", + + on_update: function(val) { + var controller = App.__container__.lookup('controller:games-following'); + if ( controller ) + controller.set('ffz_sidebar_games', val); + } + } + +FFZ.settings_info.directory_creative_all_tags = { + type: "boolean", + value: false, + + category: "Directory", + no_mobile: true, + + name: "Display All Creative Tags", + help: "Alter the creative tags display to list them all in a cloud rather than having to scroll.", + + on_update: function(val) { + document.body.classList.toggle('ffz-creative-tags', val); + } + }; + + +FFZ.settings_info.directory_creative_showcase = { + type: "boolean", + value: true, + + category: "Directory", + no_mobile: true, + + name: "Creative Showcase", + help: "Display the showcase on the Creative directory page.", + + on_update: function(val) { + document.body.classList.toggle('ffz-creative-showcase', val); + } + }; + + +FFZ.settings_info.directory_logos = { + type: "boolean", + value: false, + + category: "Directory", + no_mobile: true, + + name: "Channel Logos", help: "Display channel logos in the Twitch directory." }; +FFZ.settings_info.directory_group_hosts = { + type: "boolean", + value: true, + + category:"Directory", + no_mobile: true, + + name: "Group Hosts", + help: "Only show a given hosted channel once in the directory.", + + on_update: function() { + var f = this, + HostModel = App.__container__.resolve('model:host'), + Following = HostModel && HostModel.collections[HostModel.collectionId("following")]; + + if ( ! Following ) + return; + + Following.clear(); + Following.load(); + } + }; + + +FFZ.settings_info.directory_host_menus = { + type: "select", + options: { + 0: "Disabled", + 1: "When Multiple are Hosting", + 2: "Always" + }, + + value: 1, + process_value: function(val) { + if ( typeof val === "string" ) + return parseInt(val) || 0; + return val; + }, + + category: "Directory", + no_mobile: true, + + name: "Hosted Channel Menus", + help: "Display a menu to select which channel to visit when clicking a hosted channel in the directory.", + + on_update: function() { + var f = this, + HostModel = App.__container__.resolve('model:host'), + Following = HostModel && HostModel.collections[HostModel.collectionId("following")]; + + if ( ! Following ) + return; + + Following.clear(); + Following.load(); + } + }; + + // -------------------- // Initialization // -------------------- FFZ.prototype.setup_directory = function() { - this.log("Hooking the Ember Directory View."); + 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); + + this.log("Hooking the Ember games-following controller."); + var GamesFollowing = App.__container__.lookup('controller:games-following'); + if ( GamesFollowing ) { + GamesFollowing.reopen({ + ffz_sidebar_games: this.settings.sidebar_followed_games, + + sidePanelFollowing: function() { + var content = this.get('liveFollowing.sortedContent'), + limit = this.get('ffz_sidebar_games'); + + return limit === 999 ? content : _.first(content, limit); + }.property("liveFollowing.@each", "ffz_sidebar_games") + }); + + Ember.propertyDidChange(GamesFollowing, 'sidePanelFollowing'); + } + + + this.log("Attempting to modify the Following collection."); + this._modify_following(); + + this.log("Hooking the Ember Directory views."); var ChannelView = App.__container__.resolve('view:channel'); if ( ChannelView ) @@ -55,6 +201,93 @@ FFZ.prototype.setup_directory = function() { } +FFZ.prototype._modify_following = function() { + var HostModel = App.__container__.resolve('model:host'), + f = this; + + if ( HostModel ) { + var Following = HostModel.collections[HostModel.collectionId("following")]; + if ( Following ) { + this.log("Found Following model."); + Following.reopen({ + ffz_streams: {}, + ffz_skipped: 0, + + empty: function() { + this._super(); + this.set("ffz_streams", {}); + this.set("ffz_skipped", 0); + }, + + request: function(e) { + // We have to override request with nearly the same logic + // to prevent infinitely trying to load more streams. + if (!Twitch.user.isLoggedIn() || window.App.get("disableFollowingDirectory")) return RSVP.resolve({ + hosts: [], _total: 0 + }); + + var t = { + limit: this.limit, + offset: this.get('content.length') + this.get('ffz_skipped') + }; + + return Twitch.api.get("/api/users/:login/followed/hosting", t); + }, + + afterSuccess: function(e) { + var valid_hosts = [], + streams = this.get('ffz_streams'), + skipped = this.get('ffz_skipped'), + t = this; + + for(var i=0; i < e.hosts.length; i++) { + var host = e.hosts[i], + target = host && host.target && host.target.id; + + if ( f.settings.directory_group_hosts && streams[target] ) { + skipped++; + streams[target].ffz_hosts && streams[target].ffz_hosts.push({logo: host.logo, name: host.name, display_name: host.display_name}); + continue; + } + + streams[target] = host; + host.ffz_hosts = [{logo: host.logo, name: host.name, display_name: host.display_name}]; + + valid_hosts.push(host); + } + + this.set('ffz_skipped', skipped); + this.setContent(valid_hosts); + + // We could get non-empty results even with no new hosts. + this.set('gotNonEmptyResults', e.hosts && e.hosts.length); + this.set('total', e._total - skipped); + } + }); + + // Filter the streams immediately. + if ( true && ! Following.get('isLoading') ) { + var content = Following.get('content'), + total = Following.get('total'), + host_copy = []; + + // TODO: Something less stupid. + for(var i=0; i < content.length; i++) + host_copy.push(content[i]); + + Following.clear(); + Following.afterSuccess({hosts: host_copy, _total: total}); + } + + return; + } + } + + // Couldn't find it. Reschedule. + setTimeout(this._modify_following.bind(this), 250); +} + + FFZ.prototype._modify_directory_live = function(dir, is_csgo) { var f = this; dir.reopen({ @@ -93,7 +326,7 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { logo.className = 'profile-photo'; logo.classList.toggle('is-csgo', is_csgo); - logo.src = this.get('context.model.channel.logo') || "http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; + logo.src = this.get('context.model.channel.logo') || NO_LOGO; logo.alt = this.get('context.model.channel.display_name'); link.href = '/' + target; @@ -128,7 +361,8 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { ffzUpdateUptime: function() { var raw_created = this.get('context.model.created_at'), up_since = raw_created && utils.parse_date(raw_created), - uptime = up_since && Math.floor((Date.now() - up_since.getTime()) / 1000) || 0; + now = Date.now() - (f._ws_server_offset || 0), + uptime = up_since && Math.floor((now - up_since.getTime()) / 1000) || 0; if ( uptime > 0 ) { this._ffz_uptime.innerHTML = constants.CLOCK + utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1); @@ -151,39 +385,143 @@ FFZ.prototype._modify_directory_host = function(dir) { dir.reopen({ didInsertElement: function() { this._super(); + try { + this.ffzInit(); + } catch(err) { + f.error("directory/host ffzInit: " + err); + } + }, + willClearRender: function() { + this._super(); + try { + this.ffzCleanup(); + } catch(err) { + f.error("directory/host ffzCleanup: " + err); + } + }, + + ffzVisitChannel: function(target, e) { + var Channel = App.__container__.resolve('model:channel'); + if ( ! Channel ) + return; + + if ( e ) { + if ( e.button !== 0 ) + return; + + e.preventDefault(); + e.stopPropagation(); + } + + f.close_popup(); + this.get('controller').transitionTo('channel.index', Channel.find({id: target}).load()); + return false; + }, + + ffzShowHostMenu: function(e) { + if ( e.button !== 0 ) + return; + + e.preventDefault(); + e.stopPropagation(); + + var hosts = this.get('context.model.ffz_hosts'), + target = this.get('context.model.target.channel.name'); + + if ( f.settings.directory_host_menus === 0 || ! hosts || (f.settings.directory_host_menus === 1 && hosts.length < 2) ) + return this.ffzVisitChannel((hosts && hosts.length < 2) ? hosts[0].name : target); + + var popup = f._popup ? f.close_popup() : f._last_popup, + t = this; + + // Don't re-show the popup if we were clicking to show it. + if ( popup && popup.classList.contains('ffz-channel-selector') && popup.getAttribute('data-channel') === target ) + return; + + var menu = document.createElement('div'), hdr, + make_link = function(target) { + var link = document.createElement('a'); + link.className = 'dropmenu_action'; + link.setAttribute('data-channel', target.name); + link.href = '/' + target.name; + link.innerHTML = '' + utils.sanitize(target.display_name) + ''; + link.addEventListener('click', t.ffzVisitChannel.bind(t, target.name)); + menu.appendChild(link); + return link; + }; + + menu.className = 'ffz-channel-selector dropmenu menu-like'; + menu.setAttribute('data-channel', target); + + hdr = document.createElement('div'); + hdr.className = 'header'; + hdr.textContent = 'Hosted Channel'; + menu.appendChild(hdr); + + make_link(this.get('context.model.target.channel')); + + hdr = document.createElement('div'); + hdr.className = 'header'; + hdr.textContent = 'Hosting Channels'; + menu.appendChild(hdr); + + for(var i=0; i < hosts.length; i++) + make_link(hosts[i]); + + f.show_popup(menu, [e.clientX - 60, e.clientY - 60], document.querySelector('#main_col > .tse-scroll-content > .tse-content')); + }, + + ffzCleanup: function() { + if ( f._popup && f._popup.classList.contains('ffz-channel-selector') && f._popup.getAttribute('data-channel') === target ) + f.close_popup(); + }, + + ffzInit: function() { var el = this.get('element'), meta = el && el.querySelector('.meta'), thumb = el && el.querySelector('.thumb'), - cap = thumb && thumb.querySelector('.cap'); + cap = thumb && thumb.querySelector('.cap'), + title = meta && meta.querySelector('.title a'), + target = this.get('context.model.target.channel'), + hosts = this.get('context.model.ffz_hosts'); if ( f.settings.directory_logos ) { el.classList.add('ffz-directory-logo'); - - var link = document.createElement('a'), - logo = document.createElement('img'), - t = this, - target = this.get('context.model.target.channel.name'); + var logo = document.createElement('img'), + link = document.createElement('a'); logo.className = 'profile-photo'; - logo.src = this.get('context.model.target.channel.logo') || "http://static-cdn.jtvnw.net/jtv_user_pictures/xarth/404_user_150x150.png"; + logo.src = this.get('context.model.target.channel.logo') || NO_LOGO; logo.alt = this.get('context.model.target.channel.display_name'); - link.href = '/' + target; - link.addEventListener('click', function(e) { - var Channel = App.__container__.resolve('model:channel'); - if ( ! Channel ) - return; - - e.preventDefault(); - t.get('controller').transitionTo('channel.index', Channel.find({id: target}).load()); - return false; - }); + link.href = '/' + target.name; + link.addEventListener('click', this.ffzVisitChannel.bind(this, target.name)); link.appendChild(logo); meta.insertBefore(link, meta.firstChild); } + + var update_links = f.settings.directory_host_menus === 2 || (hosts && hosts.length > 1); + + if ( title ) { + if ( update_links ) { + title.href = '/' + target.name; + title.addEventListener('click', this.ffzShowHostMenu.bind(this)); + } + + if ( hosts && hosts.length > 1 ) { + title.textContent = utils.number_commas(hosts.length) + ' hosting ' + utils.sanitize(target.display_name); + title.title = _.sortBy(hosts, "name").mapProperty("display_name").join(", "); + jQuery(title).tipsy({gravity: 's'}); + } + } + + if ( cap && update_links ) { + cap.href = '/' + target.name; + cap.addEventListener('click', this.ffzShowHostMenu.bind(this)); + } } }); diff --git a/src/ember/following.js b/src/ember/following.js index 21d20985..a57b292a 100644 --- a/src/ember/following.js +++ b/src/ember/following.js @@ -161,7 +161,8 @@ FFZ.prototype.setup_profile_following = function() { t_el.className = 'overlay_info length'; jQuery(t_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 's')}); - var age = data[0] ? Math.floor((Date.now() - data[0].getTime()) / 1000) : 0; + var now = Date.now() - (f._ws_server_offset || 0), + age = data[0] ? Math.floor((now - data[0].getTime()) / 1000) : 0; if ( age ) { t_el.innerHTML = constants.CLOCK + ' ' + utils.human_time(age, 10); t_el.setAttribute('original-title', 'Following Since: ' + data[0].toLocaleString() + ''); @@ -180,7 +181,8 @@ FFZ.prototype.setup_profile_following = function() { follow.innerHTML = constants.HEART + constants.UNHEART + ' Follow'; if ( t_el ) { - var age = data && data[0] ? Math.floor((Date.now() - data[0].getTime()) / 1000) : undefined; + var now = Date.now() - (f._ws_server_offset || 0), + age = data && data[0] ? Math.floor((now - data[0].getTime()) / 1000) : undefined; if ( age !== undefined ) { t_el.innerHTML = constants.CLOCK + ' ' + (age < 60 ? 'now' : utils.human_time(age, 10)); t_el.setAttribute('original-title', 'Following Since: ' + data[0].toLocaleString() + ''); @@ -216,7 +218,7 @@ FFZ.prototype.setup_profile_following = function() { Twitch.api.del("users/:login/follows/channels/" + user_id) : Twitch.api.put("users/:login/follows/channels/" + user_id, {notifications: false})) .done(function() { - data = f._following_cache[user_id] = was_following ? null : [new Date(), false]; + data = f._following_cache[user_id] = was_following ? null : [new Date(Date.now() - (f._ws_server_offset||0)), false]; }) .always(function() { update_follow(); diff --git a/src/ember/line.js b/src/ember/line.js index 52543528..0babc0aa 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -505,7 +505,7 @@ FFZ.settings_info.chat_ts_size = { css = ""; else { var lh = Math.max(20, Math.round((20/12)*val), Math.round((20/12)*this.settings.chat_font_size)); - css = ".chat-history .timestamp,.ember-chat .chat-messages .timestamp { font-size: " + val + "px !important; line-height: " + lh + "px !important; }"; + css = ".ember-chat .chat-messages .timestamp { font-size: " + val + "px !important; line-height: " + lh + "px !important; }"; } utils.update_css(this._chat_style, "chat_ts_font_size", css); @@ -658,8 +658,6 @@ FFZ.prototype._modify_line = function(component) { var deleted = this.get('msgObject.deleted'), r = this, - badges = {}, - user = this.get('msgObject.from'), room_id = this.get('msgObject.room'), room = f.rooms && f.rooms[room_id], @@ -679,6 +677,8 @@ FFZ.prototype._modify_line = function(component) { e.push('
'); e.push('' + this.get("timestamp") + ' '); + + // Moderation actions if ( ! is_whisper && this_ul < other_ul ) { e.push(''); for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) { @@ -712,44 +712,14 @@ FFZ.prototype._modify_line = function(component) { e.push(''); } - // Stock Badges - if ( ! is_whisper && this.get('isBroadcaster') ) - badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; - else if ( this.get('isStaff') ) - badges[0] = {klass: 'staff', title: 'Staff'}; - else if ( this.get('isAdmin') ) - badges[0] = {klass: 'admin', title: 'Admin'}; - else if ( this.get('isGlobalMod') ) - badges[0] = {klass: 'global-moderator', title: 'Global Moderator'}; - else if ( ! is_whisper && this.get('isModerator') ) - badges[0] = {klass: 'moderator', title: 'Moderator'}; - if ( ! is_whisper && this.get('isSubscriber') ) - badges[10] = {klass: 'subscriber', title: 'Subscriber'}; - if ( this.get('hasTurbo') ) - badges[15] = {klass: 'turbo', title: 'Turbo'}; - - // FFZ Badges - badges = f.render_badges(this, badges); - - // Rendering! + // Badges e.push(''); - - for(var key in badges) { - var badge = badges[key], - css = badge.image ? 'background-image:url("' + badge.image + '");' : ''; - - if ( badge.color ) - css += 'background-color:' + badge.color + ';'; - - if ( badge.extra_css ) - css += badge.extra_css; - - e.push('
'); - } - + e.push(f.render_badges(f.get_line_badges(this.get('msgObject'), is_whisper))); e.push('
'); + + // Handle aliases var alias = f.aliases[user], name = this.get('msgObject.tags.display-name') || (user && user.capitalize()) || "unknown user", style = colors && 'color:' + (is_dark ? colors[1] : colors[0]), @@ -760,6 +730,8 @@ FFZ.prototype._modify_line = function(component) { else e.push('' + utils.sanitize(name) + ''); + + // If it's a whisper, we need to get that user's color, alias, and draw the whisper arrow thing. if ( is_whisper ) { var to_alias = f.aliases[recipient], to_name = this.get('msgObject.tags.recipient-display-name') || (recipient && recipient.capitalize()) || "unknown user", @@ -777,6 +749,8 @@ FFZ.prototype._modify_line = function(component) { e.push('' + utils.sanitize(to_name) + ''); } + + // Finally, onto the message proper. e.push(': '); if ( this.get('msgObject.style') !== 'action' ) { diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index dda690af..a66234b7 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -426,7 +426,8 @@ FFZ.prototype.setup_mod_card = function() { } if ( since ) { - var age = Math.floor((Date.now() - since.getTime()) / 1000); + var now = Date.now() - (f._ws_server_offset || 0), + age = Math.floor((now - since.getTime()) / 1000); if ( age > 0 ) { out += '' + constants.CLOCK + ' ' + utils.human_time(age, 10) + ''; } @@ -458,6 +459,7 @@ FFZ.prototype.setup_mod_card = function() { var el = this.get('element'), controller = this.get('controller'), + t = this, line, is_mod = controller.get('cardInfo.isModeratorOrHigher'), @@ -731,6 +733,7 @@ FFZ.prototype.setup_mod_card = function() { if ( f.settings.mod_card_history ) { var Chat = App.__container__.lookup('controller:chat'), room = Chat && Chat.get('currentRoom'), + delete_links = room && room.get('roomProperties.hide_chat_links'), tmiSession = room.tmiSession || (window.TMI && TMI._sessions && TMI._sessions[0]), room_id = room.get('id'), user_id = controller.get('cardInfo.user.id'), @@ -741,12 +744,14 @@ FFZ.prototype.setup_mod_card = function() { history.className = 'interface clearfix chat-history'; - if ( user_history.length < 20 ) { - var before = user_history.length > 0 ? user_history[0].date.getTime() : Date.now(); + if ( user_history.length < 50 ) { + var before = (user_history.length > 0 ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0); f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) { if ( ! success ) return; + f.parse_history(data, null, room_id, delete_links, tmiSession); + var i = data.length, was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight), first = true; @@ -756,8 +761,7 @@ FFZ.prototype.setup_mod_card = function() { if ( ! msg ) continue; - if ( typeof msg.date === "string" || typeof msg.date === "number" ) - msg.date = utils.parse_date(msg.date); + msg.from_server = true; if ( ! msg.date || msg.date.getTime() >= before ) continue; @@ -772,27 +776,7 @@ FFZ.prototype.setup_mod_card = function() { }), history.firstElementChild); } - if ( ! msg.style ) { - if ( msg.from === "jtv" ) - msg.style = "admin"; - else if ( msg.from === "twitchnotify" ) - msg.style = "notification"; - } - - if ( msg.tags && typeof msg.tags.emotes === "string" ) - try { - msg.tags.emotes = JSON.parse(msg.tags.emotes); - } catch(err) { - f.log("Error Parsing JSON Emotes: " + err); - msg.tags.emotes = {}; - } - - if ( ! msg.cachedTokens || ! msg.cachedTokens.length ) - f.tokenize_chat_line(msg, true, room.get('roomProperties.hide_chat_links')); - - history.insertBefore(f._build_mod_card_history(msg), history.firstElementChild); - if ( history.childElementCount >= 50 ) - break; + history.insertBefore(f._build_mod_card_history(msg, t), history.firstElementChild); } if ( was_at_top ) @@ -801,7 +785,7 @@ FFZ.prototype.setup_mod_card = function() { } for(var i=0; i < user_history.length; i++) - history.appendChild(f._build_mod_card_history(user_history[i])); + history.appendChild(f._build_mod_card_history(user_history[i], t)); el.appendChild(history); @@ -832,29 +816,214 @@ FFZ.prototype.setup_mod_card = function() { f.error("ModerationCardView didInsertElement: " + err); } catch(err) { } } - }}); + }, + + ffzAdjacentHistory: function(line) { + var Chat = App.__container__.lookup('controller:chat'), + controller = this.get('controller'), + t = this, + + user_id = this.get('cardInfo.user.id'), + + room = Chat && Chat.get('currentRoom'), + room_id = room.get('id'), + delete_links = room && room.get('roomProperties.hide_chat_links'), + + tmiSession = room.tmiSession || (window.TMI && TMI._sessions && TMI._sessions[0]), + + el = this.get('element'), + history = el && el.querySelector('.chat-history'), + logs = el && el.querySelector('.chat-history.adjacent-history'), + + when = line.date.getTime(); + + if ( ! history ) + return; + + if ( logs ) + logs.classList.add('loading'); + else + history.classList.add('loading'); + + if ( ! f.ws_send("adjacent_history", [room_id, when, 2], function(success, data) { + if ( logs ) + logs.classList.remove('loading'); + else + history.classList.remove('loading'); + + if ( ! success || ! data || ! data.length ) + return; + + var had_logs = false, + found_original = false, + back; + + if ( logs ) { + had_logs = true; + logs.innerHTML = ''; + + } else { + logs = document.createElement('ul'); + back = document.createElement('button'); + + back.className = 'button back-button'; + back.innerHTML = '« Back'; + + back.addEventListener('click', function() { + logs.parentElement.removeChild(logs); + 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) { + msg.from_server = true; + + var line_time = line.date.getTime() - (line.from_server ? 0 : (f._ws_server_offset || 0)), + is_original = ! found_original && Math.abs(line_time - msg.date.getTime()) < (line.from_server ? 50 : 1000) && line.from === msg.from && line.message === msg.message; + + msg.original_sender = user_id === msg.from; + msg.is_original = is_original; + found_original = found_original || is_original; + + logs.insertBefore(f._build_mod_card_history(msg, t, true), logs.firstElementChild); + return true; + }); + + + if ( ! had_logs ) { + history.classList.add('hidden'); + history.parentElement.insertBefore(logs, history); + history.parentElement.insertBefore(back, logs); + } + + if ( found_original ) + setTimeout(function(){ + el = logs.querySelector('.original-msg'); + if ( el ) + logs.scrollTop = (el.offsetTop - logs.offsetTop) - (logs.clientHeight - el.clientHeight) / 2; + }); + + }) ) + if ( logs ) + logs.classList.remove('loading'); + else + history.classList.remove('loading'); + } + }); } -FFZ.prototype._build_mod_card_history = function(line) { +FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { var l_el = document.createElement('li'), + out = [], f = this; + style = '', colored = ''; + + if ( helpers && helpers.getTime ) + out.push('' + helpers.getTime(msg.date) + ''); + + + if ( show_from ) { + // Badges + out.push(''); + out.push(this.render_badges(this.get_line_badges(msg, false))); + out.push(''); + + + // Colors + var raw_color = msg.color, + colors = raw_color && this._handle_color(raw_color), + + Layout = App.__container__.lookup('controller:layout'), + Settings = App.__container__.lookup('controller:settings'), + + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); + + + // Aliases and Styling + var alias = this.aliases[msg.from], + name = (msg.tags && msg.tags['display-name']) || (msg.from && msg.from.capitalize()) || "unknown user", + style = colors && 'color:' + (is_dark ? colors[1] : colors[0]), + colored = style ? ' has-color' : ''; + + + if ( alias ) + out.push('' + utils.sanitize(alias) + ''); + else + out.push('' + utils.sanitize(name ) + ''); + + out.push(': '); + } + + + // The message itself. + if ( msg.style !== 'action' ) { + style = ''; + colored = ''; + } + + + var message = '' + + (msg.style === 'action' && ! show_from ? '*' + name + ' ' : '') + this.render_tokens(msg.cachedTokens) + ''; + + if ( msg.deleted ) + out.push('<message deleted>'); + else + out.push(message); + + + // Line attributes and classes. l_el.className = 'message-line chat-line clearfix'; - if ( line.ffz_has_mention ) + if ( msg.style ) + l_el.classList.add(msg.style); + + if ( msg.original_sender ) + l_el.classList.add('original-sender'); + + if ( msg.is_original ) + l_el.classList.add('original-msg'); + + if ( msg.ffz_has_mention ) l_el.classList.add('ffz-mentioned'); - if ( line.style ) - l_el.classList.add(line.style); + if ( this.settings.prevent_clear && msg.ffz_deleted ) + l_el.classList.add('ffz-deleted'); + + l_el.setAttribute('data-room', msg.room); + l_el.setAttribute('data-sender', msg.from); + l_el.setAttribute('data-deleted', msg.deleted || false); + + l_el.innerHTML = out.join(""); - l_el.innerHTML = (helpers ? '' + helpers.getTime(line.date) + ' ' : '') + '' + (line.style === 'action' ? '*' + line.from + ' ' : '') + f.render_tokens(line.cachedTokens) + ''; // Interactivity + jQuery('a.undelete', l_el).click(function(e) { this.parentElement.outerHTML = this.getAttribute('data-message'); }); jQuery('a.deleted-link', l_el).click(f._deleted_link_click); jQuery('img.emoticon', l_el).click(function(e) { f._click_emote(this, e) }); jQuery('.html-tooltip', l_el).tipsy({html:true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 's')}); + if ( modcard ) { + modcard.get('cardInfo.user.id') !== msg.from && jQuery('span.from', l_el).click(function(e) { + var el = modcard.get('element'); + el && f._roomv && f._roomv.get('context.model.id') === msg.room && f._roomv.get('controller').send('showModOverlay', { + sender: msg.from, + top: parseInt(el.style.top), + left: parseInt(el.style.left) + }); + }); + + l_el.querySelector('.timestamp').addEventListener('click', function(e) { + if ( e.button === 0 ) + modcard.ffzAdjacentHistory(msg); + }); + } + return l_el; } @@ -881,6 +1050,8 @@ FFZ.prototype._update_alias = function(user) { el_from.textContent = display_name; el_from.title = alias ? cap_name : ''; } + + // TODO: Update conversations~ } diff --git a/src/ember/room.js b/src/ember/room.js index d045f5c1..92986ffc 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -713,7 +713,7 @@ FFZ.prototype._load_history = function(room_id, success, data) { if ( ! data.length ) return; - return this._insert_history(room_id, data); + return this._insert_history(room_id, data, true); } @@ -730,118 +730,93 @@ FFZ.prototype._show_deleted = function(room_id) { this._insert_history(room_id, old_messages); } -FFZ.prototype._insert_history = function(room_id, data) { - var room = this.rooms[room_id]; +FFZ.prototype._insert_history = function(room_id, data, from_server) { + var room = this.rooms[room_id], f = this; if ( ! room || ! room.room ) return; var r = room.room, messages = r.get('messages'), + buffer_size = r.get('messageBufferSize'), + tmiSession = r.tmiSession || (TMI._sessions && TMI._sessions[0]), - tmiRoom = r.tmiRoom, + delete_links = r.get('roomProperties.hide_chat_links'), removed = 0, inserted = 0, - purged = {}, - last_msg = data[data.length - 1], - now = new Date(), - last_date = (typeof last_msg.date === "string" || typeof last_msg.date === "number") ? (last_msg.date = utils.parse_date(last_msg.date)) : last_msg.date, - age = (now - last_date) / 1000, - is_old = age > 300, + first_inserted, + first_existing, + before; - i = data.length; + first_existing = messages.length ? messages[0] : null; + if ( first_existing && first_existing.from === 'jtv' && first_existing.message === 'Welcome to the chat room!' ) + first_existing = messages.length > 1 ? messages[1] : null; - var i = data.length; - while(i--) { - var msg = data[i], - is_deleted = msg.ffz_deleted = purged[msg.from] || false; + if ( first_existing ) + before = first_existing.date && first_existing.date.getTime(); - if ( is_deleted && ! this.settings.prevent_clear ) - msg.deleted = true; - if ( typeof msg.date === "string" || typeof msg.date === "number" ) - msg.date = utils.parse_date(msg.date); + this.parse_history(data, null, room_id, delete_links, tmiSession, function(msg) { + if ( from_server ) + msg.from_server = true; - if ( ! msg.room ) - msg.room = room_id; + // Skip messages that are from the future. + if ( ! msg.date || (before && (before - (msg.from_server && ! first_existing.from_server ? f._ws_server_offset || 0 : 0)) < msg.date.getTime()) ) + return true; - if ( ! msg.color ) - msg.color = msg.tags && msg.tags.color ? msg.tags.color : tmiSession && msg.from ? tmiSession.getColor(msg.from.toLowerCase()) : "#755000"; + if ( f.settings.remove_deleted && msg.deleted ) + return true; - if ( ! msg.labels || ! msg.labels.length ) { - var labels = msg.labels = []; - if ( msg.tags ) { - if ( msg.tags.turbo ) - labels.push("turbo"); - if ( msg.tags.subscriber ) - labels.push("subscriber"); - if ( msg.from === room_id ) - labels.push("owner") - else { - var ut = msg.tags['user-type']; - if ( ut === 'mod' || ut === 'staff' || ut === 'admin' || ut === 'global_mod' ) - labels.push(ut); - } - } - } - - if ( ! msg.style ) { - if ( msg.from === "jtv" ) - msg.style = "admin"; - else if ( msg.from === "twitchnotify" ) - msg.style = "notification"; - } - - if ( msg.tags && typeof msg.tags.emotes === "string" ) - try { - msg.tags.emotes = JSON.parse(msg.tags.emotes); - } catch(err) { - f.log("Error Parsing JSON Emotes: " + err); - msg.tags.emotes = {}; - } - - if ( ! msg.cachedTokens || ! msg.cachedTokens.length ) - this.tokenize_chat_line(msg, true, r.get('roomProperties.hide_chat_links')); - - if ( r.shouldShowMessage(msg) ) { - if ( messages.length < r.get("messageBufferSize") ) { - // One last thing! Make sure we don't have too many messages. + if ( r.shouldShowMessage(msg) && r.ffzShouldShowMessage(msg) ) { + if ( messages.length < buffer_size ) { if ( msg.ffz_old_messages ) { - var max_msgs = r.get("messageBufferSize") - (messages.length + 1); - if ( msg.ffz_old_messages.length > max_msgs ) - msg.ffz_old_messages = msg.ffz_old_messages.slice(msg.ffz_old_messages.length - max_msgs); + var max_messages = buffer_size - (messages.length + 1); + if ( max_messages <= 0 ) + msg.ffz_old_messages = null; + else if ( msg.ffz_old_messages.length > max_messages ) + msg.ffz_old_messages = msg.ffz_old_messages.slice(msg.ffz_old_messages.length - max_messages); } + if ( ! first_inserted ) + first_inserted = msg; + messages.unshiftObject(msg); inserted += 1; + } else - break; + return false; } - // If there was a CLEARCHAT, stop processing. + // If there's a CLEARCHAT, stop processing. if ( msg.tags && msg.tags.target === '@@' ) - break; + return false; - // If there was a purge, just track the name. - else if ( msg.tags && msg.tags.target ) - purged[msg.tags.target] = true; - } + return true; + }); - if ( is_old ) { + + if ( ! first_inserted ) + return; + + var now = Date.now() - (first_inserted.from_server ? this._ws_server_offset || 0 : 0), + age = now - first_inserted.date.getTime(); + + if ( age > 300000 ) { var msg = { color: "#755000", - date: new Date(), + date: first_inserted.date, from: "frankerfacez_admin", style: "admin", - message: "(Last message is " + utils.human_time(age) + " old.)", - room: room_id + message: "(Last message is " + utils.human_time(age/1000) + " old.)", + room: room_id, + from_server: from_server }; - this.tokenize_chat_line(msg, true, r.get('roomProperties.hide_chat_links')); + this.tokenize_chat_line(msg, false, delete_links); if ( r.shouldShowMessage(msg) ) { messages.insertAt(inserted, msg); - while( messages.length > r.get('messageBufferSize') ) { + while ( messages.length > buffer_size ) { messages.removeAt(0); removed++; } @@ -1100,8 +1075,7 @@ FFZ.prototype._modify_room = function(room) { if ( ! this.ffzPending ) this.ffzPending = []; - var now = Date.now(); - msg.time = now; + var now = msg.time = Date.now(); this.ffzPending.push(msg); this.ffzSchedulePendingFlush(now); @@ -1134,9 +1108,10 @@ FFZ.prototype._modify_room = function(room) { if ( this.ffzPending && this.ffzPending.length ) { // We need either the amount of chat delay past the first message, if chat_delay is on, or the // amount of time from the last batch. + now = now || Date.now(); var delay = Math.max( - (f.settings.chat_delay !== 0 ? 50 + Math.max(0, (f.settings.chat_delay + (this.ffzPending[0].time||0)) - (now || Date.now())) : 0), - (f.settings.chat_batching !== 0 ? Math.max(0, f.settings.chat_batching - ((now || Date.now()) - (this._ffz_last_batch||0))) : 0)); + (f.settings.chat_delay !== 0 ? 50 + Math.max(0, (f.settings.chat_delay + (this.ffzPending[0].time||0)) - now) : 0), + (f.settings.chat_batching !== 0 ? Math.max(0, f.settings.chat_batching - (now - (this._ffz_last_batch||0))) : 0)); this._ffz_pending_flush = setTimeout(this.ffzPendingFlush.bind(this), delay); } @@ -1163,6 +1138,9 @@ FFZ.prototype._modify_room = function(room) { }, ffzShouldShowMessage: function (msg) { + if ( ! f.settings.hosted_sub_notices && msg.style === 'notification' && HOSTED_SUB.test(msg.message) ) + return false; + if (f.settings.remove_bot_ban_notices && this.ffzRecentlyBanned) { var banned = '(' + this.ffzRecentlyBanned.join('|') + ')'; var bots = { @@ -1181,9 +1159,6 @@ FFZ.prototype._modify_room = function(room) { addMessage: function(msg) { if ( msg ) { - if ( ! f.settings.hosted_sub_notices && msg.style === 'notification' && HOSTED_SUB.test(msg.message) ) - return; - var is_whisper = msg.style === 'whisper'; // Ignore whispers if conversations are enabled. @@ -1212,7 +1187,9 @@ FFZ.prototype._modify_room = function(room) { user_history = room.user_history[msg.from] = room.user_history[msg.from] || []; user_history.push({ - from: msg.tags && msg.tags['display-name'] || msg.from, + from: msg.from, + tags: {'display-name': msg.tags && msg.tags['display-name']}, + message: msg.message, cachedTokens: msg.cachedTokens, style: msg.style, date: msg.date @@ -1223,16 +1200,16 @@ FFZ.prototype._modify_room = function(room) { if ( f._mod_card && f._mod_card.ffz_room_id === msg.room && f._mod_card.get('cardInfo.user.id') === msg.from ) { var el = f._mod_card.get('element'), - history = el && el.querySelector('.chat-history'), + history = el && el.querySelector('.chat-history:not(.adjacent-history)'), was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight); if ( history ) { - history.appendChild(f._build_mod_card_history(msg)); + history.appendChild(f._build_mod_card_history(msg, f._mod_card)); if ( was_at_top ) setTimeout(function() { history.scrollTop = history.scrollHeight; }) // Don't do infinite scrollback. - if ( history.childElementCount > 50 ) + if ( history.childElementCount > 100 ) history.removeChild(history.firstElementChild); } } diff --git a/src/ember/router.js b/src/ember/router.js index 26d96818..7d6a3df0 100644 --- a/src/ember/router.js +++ b/src/ember/router.js @@ -16,6 +16,12 @@ FFZ.prototype.setup_router = function() { if ( Router ) Router.reopen({ ffzTransition: function() { + // TODO: Do this before the transition happens. + if ( f._force_refresh ) { + location.href = this.get('url'); + return; + } + try { document.body.setAttribute('data-current-path', App.get('currentPath')); } catch(err) { @@ -25,4 +31,11 @@ FFZ.prototype.setup_router = function() { }); document.body.setAttribute('data-current-path', App.get('currentPath')); +} + + + +FFZ.ws_commands.please_refresh = function() { + this.log("Refreshing the page upon the next transition."); + this._force_refresh = true; } \ No newline at end of file diff --git a/src/ext/rechat.js b/src/ext/rechat.js index f024a7f8..e06d006c 100644 --- a/src/ext/rechat.js +++ b/src/ext/rechat.js @@ -122,7 +122,8 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { line.classList.add('ffz-processed'); - var user_id = line.getAttribute('data-sender'), + var f = this, + user_id = line.getAttribute('data-sender'), room_id = line.getAttribute('data-room'), Layout = App.__container__.lookup('controller:layout'), @@ -173,23 +174,9 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; if ( user_id ) - badges = this._render_badges(user_id, room_id, badges); + badges = this.get_badges(user_id, room_id, badges, null); - var output = ''; - for(var key in badges) { - var badge = badges[key], - css = badge.iamge ? 'background-image:url("' + badge.image + '");' : ''; - - if ( badge.color ) - css += 'background-color:' + badge.color + ';'; - - if ( badge.extra_css ) - css += badge.extra_css; - - output += '
'; - } - - badges_el.innerHTML = output; + badges_el.innerHTML = this.render_badges(badges); } if ( ! reprocess && from_el ) { @@ -243,8 +230,14 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { own: node.classList.contains('mentioning') }); - else + 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); } @@ -274,4 +267,8 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { // 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/main.js b/src/main.js index 666649d3..8377a227 100644 --- a/src/main.js +++ b/src/main.js @@ -22,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 77, + major: 3, minor: 5, revision: 83, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -149,6 +149,7 @@ require('./ext/emote_menu'); require('./featurefriday'); +require('./ui/popups'); require('./ui/styles'); require('./ui/dark'); require('./ui/tooltips'); @@ -256,9 +257,12 @@ FFZ.prototype.init_normal = function(delay, no_socket) { // Start this early, for quick loading. this.setup_dark(); this.setup_css(); + this.setup_popups(); - if ( ! no_socket ) + if ( ! no_socket ) { + this.setup_time(); this.ws_create(); + } this.setup_colors(); this.setup_emoticons(); @@ -294,8 +298,11 @@ FFZ.prototype.init_dashboard = function(delay) { // Start this early, for quick loading. this.setup_dark(); this.setup_css(); + this.setup_popups(); + this.setup_time(); this.ws_create(); + this.setup_colors(); this.setup_emoticons(); this.setup_badges(); @@ -336,8 +343,11 @@ FFZ.prototype.init_ember = function(delay) { // Start this early, for quick loading. this.setup_dark(); this.setup_css(); + this.setup_popups(); + this.setup_time(); this.ws_create(); + this.setup_emoticons(); this.setup_badges(); diff --git a/src/socket.js b/src/socket.js index ecb30044..e3266130 100644 --- a/src/socket.js +++ b/src/socket.js @@ -1,11 +1,30 @@ var FFZ = window.FrankerFaceZ, - constants = require('./constants'); + constants = require('./constants'), + utils = require('./utils'), + + pick_server = function(pool) { + var total = 0, i = pool.length, val; + while(i--) + total += pool[i][1]; + + val = Math.random() * total; + for(i = 0; i < pool.length; i++) { + val -= pool[i][1]; + if ( val <= 0 ) + return i; + } + + return pool.length - 1; + }; + FFZ.prototype._ws_open = false; FFZ.prototype._ws_delay = 0; FFZ.prototype._ws_host_idx = -1; FFZ.prototype._ws_current_pool = -1; +FFZ.prototype._ws_server_offset = null; + FFZ.ws_commands = {}; FFZ.ws_on_close = []; @@ -15,7 +34,7 @@ FFZ.ws_on_close = []; // Settings // ---------------- -var ffz_socket_seed; +/*var ffz_socket_seed; try { ffz_socket_seed = JSON.parse(localStorage.ffz_socket_seed); @@ -24,7 +43,7 @@ try { if ( ! ffz_socket_seed ) { ffz_socket_seed = Math.random(); localStorage.ffz_socket_seed = JSON.stringify(ffz_socket_seed); -} +}*/ FFZ.settings_info.socket_server_pool = { @@ -35,7 +54,7 @@ FFZ.settings_info.socket_server_pool = { 2: "Development" }, - value: ffz_socket_seed > 0.4 ? 1 : 0, + value: 1, process_value: function(val) { if ( typeof val === "string" ) @@ -95,14 +114,14 @@ FFZ.prototype.ws_create = function() { return; if ( this._ws_host_idx < 0 ) - this._ws_host_idx = Math.floor(Math.random() * pool.length); + this._ws_host_idx = pick_server(pool); - var server = pool[this._ws_host_idx]; + var server = pool[this._ws_host_idx][0]; this.log("Using Socket Server: " + server + " [" + pool_id + ":" + this._ws_host_idx + "]"); try { - ws = this._ws_sock = new WebSocket(pool[this._ws_host_idx]); + ws = this._ws_sock = new WebSocket(server); } catch(err) { this._ws_exists = false; return this.log("Error Creating WebSocket: " + err); @@ -116,6 +135,7 @@ FFZ.prototype.ws_create = function() { f.log("Socket Connected."); // Hard-code the first command. + f._ws_ping_time = window.performance ? performance.now() : Date.now(); ws.send("1 hello " + JSON.stringify(["ffz_" + FFZ.version_info, localStorage.ffzClientId])); var user = f.get_user(); @@ -303,30 +323,70 @@ FFZ.prototype._ws_on_hello = function(success, data) { if ( ! success ) return this.log("Error Saying Hello: " + data); - localStorage.ffzClientId = data; - this.log("Client ID: " + data); + this._ws_on_pong(success, data[1]); - /*var survey = {}, - set = survey['settings'] = {}; - - for(var key in FFZ.settings_info) - set[key] = this.settings[key]; - - set["keywords"] = this.settings.keywords.length; - set["banned_words"] = this.settings.banned_words.length; + localStorage.ffzClientId = data[0]; + this.log("Client ID: " + localStorage.ffzClientId); +} - // Detect BTTV. - survey['bttv'] = this.has_bttv || !!document.head.querySelector('script[src*="betterttv"]'); +// ----------------- +// Time Calculation +// ----------------- +FFZ.prototype.setup_time = function() { + var last_time = Date.now(), + f = this; - // Client Info - survey['user-agent'] = navigator.userAgent; - survey['screen'] = [screen.width, screen.height]; - survey['language'] = navigator.language; - survey['platform'] = navigator.platform; + setInterval(function() { + var new_time = Date.now(), + difference = (new_time - last_time) - 5000; - this.ws_send("survey", [survey]);*/ + last_time = new_time; + if ( Math.abs(difference) > 250 ) { + f.log("WARNING! Time drift of " + difference + "ms across 5 seconds. Did the local time change?"); + f._ws_server_offset = null; + f.ws_ping(); + } + }, 5000); +} + +FFZ.prototype.ws_ping = function() { + // Only 1 ping at a time. + if ( this._ws_ping_time ) + return; + + this._ws_ping_time = window.performance ? performance.now() : Date.now(); + if ( ! this.ws_send("ping", null, this._ws_on_pong.bind(this)) ) + this._ws_ping_time = null; +} + +FFZ.prototype._ws_on_pong = function(success, server_time) { + var d_now = Date.now(), + now = window.performance ? performance.now() : d_now; + + if ( ! success ) { + this._ws_ping_time = null; + this.log("Error Pinging Server: " + server_time); + return; + } + + if ( this._ws_ping_time ) { + var rtt = now - this._ws_ping_time, + ping = rtt / 2; + + this._ws_ping_time = null; + this._ws_server_offset = (d_now - (server_time + ping)); + + this.log("Server Time: " + new Date(server_time).toISOString()); + this.log("Local Time: " + new Date(d_now).toISOString()); + this.log("Estimated Ping: " + ping + "ms"); + this.log("Time Offset: " + (this._ws_server_offset < 0 ? "-" : "") + utils.time_to_string(Math.abs(this._ws_server_offset) / 1000)); + + if ( Math.abs(this._ws_server_offset) > 300000 ) { + this.log("WARNING! The time offset with the server is greater than 5 minutes."); + } + } } diff --git a/src/styles/chat-background.css b/src/styles/chat-background.css index 8f162569..70a7dc1b 100644 --- a/src/styles/chat-background.css +++ b/src/styles/chat-background.css @@ -30,6 +30,85 @@ } +/* DEPRECIATED: Whisper Backgrounds */ +.ember-chat .chat-line.whisper-line:before { + background-color: rgba(185, 163, 227, 0.2); +} + +.ember-chat .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before { + background-color: rgba(185, 163, 227, 0.4); +} + +.theatre .chat-line.whisper-line:before, +.dark .chat-line.whisper-line:before, +.force-dark .chat-line.whisper-line:before { + background-color: rgba(100, 65, 165, 0.2); +} + +.theatre .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before, +.dark .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before, +.force-dark .chat-lines > div:nth-child(2n+0) .chat-line.whisper-line:before { + background-color: rgba(100, 65, 165, 0.4); +} + + +/* Chat History: Original Sender */ +.chat-history .chat-line.original-sender:before { + background-color: rgba(0,127,255, 0.2); +} + +.chat-history .chat-line.original-sender:nth-child(2n+0):before { + background-color: rgba(0,127,255, 0.4); +} + +.theatre .chat-history .chat-line.original-sender:before, +.dark .chat-history .chat-line.original-sender:before, +.force-dark .chat-history .chat-line.original-sender:before { + background-color: rgba(0,63,127, 0.2); +} + +.theatre .chat-history .chat-line.original-sender:nth-child(2n+0):before, +.dark .chat-history .chat-line.original-sender:nth-child(2n+0):before, +.force-dark .chat-history .chat-line.original-sender:nth-child(2n+0):before { + background-color: rgba(0,63,127, 0.4); +} + + +/* Chat History: Original Message */ +.chat-history .chat-line.original-msg:before { + background-color: rgba(0,255,127, 0.4); +} + +.chat-history .chat-line.original-msg:nth-child(2n+0):before { + background-color: rgba(0,255,127, 0.8); +} + +.theatre .chat-history .chat-line.original-msg:before, +.dark .chat-history .chat-line.original-msg:before, +.force-dark .chat-history .chat-line.original-msg:before { + background-color: rgba(0,127,63, 0.4); +} + +.theatre .chat-history .chat-line.original-msg:nth-child(2n+0):before, +.dark .chat-history .chat-line.original-msg:nth-child(2n+0):before, +.force-dark .chat-history .chat-line.original-msg:nth-child(2n+0):before { + background-color: rgba(0,127,63, 0.8); +} + + +/* Reading contrast */ +.chat-history .chat-line.original-msg span.has-color { + color: #000 !important; +} + +.theatre .chat-history .chat-line.original-msg span.has-color, +.dark .chat-history .chat-line.original-msg span.has-color, +.force-dark .chat-history .chat-line.original-msg span.has-color { + color: #fff !important; +} + + + /* DEPRECIATED: Mention Backgrounds */ .chat-history .chat-line.ffz-mentioned:before, @@ -98,4 +177,17 @@ .ember-chat-container.force-dark .chat-line .mentioning { color: #8c8c8c; background-color: rgba(16,16,16, 0.75); +} + + +/* Fix Conversations */ + +.conversation-window .timestamp-line span, +.conversation-window .new-message-divider span { + background-color: transparent; +} + +.conversation-window .new-message-divider:after, +.conversation-window .timestamp-line:after { + display: none; } \ No newline at end of file diff --git a/src/tokenize.js b/src/tokenize.js index bd877874..bd7866ea 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -443,7 +443,7 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del for(var i=0; i < tokens.length; i++) { var token = tokens[i]; - if ( msgObject.style !== 'whisper' && (_.isString(token) || ! token.mentionedUser || token.own) ) + if ( _.isString(token) || ! token.mentionedUser || token.own ) continue; // We have a mention! @@ -544,7 +544,10 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { if ( token.hidden ) return ""; - if ( token.emoticonSrc ) { + else if ( token.isRaw ) + return token.html; + + else if ( token.emoticonSrc ) { var tooltip, src = token.emoticonSrc, srcset, cls, extra; if ( token.ffzEmote ) { var emote_set = f.emote_sets && f.emote_sets[token.ffzEmoteSet], @@ -607,7 +610,7 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { return ''; } - if ( token.isLink ) { + else if ( token.isLink ) { var text = token.title || (token.isLong && '') || (token.isShortened && '') || (token.isDeleted && '') || token.href; if ( ! render_links && render_links !== undefined ) @@ -676,10 +679,10 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { return '' + utils.sanitize(text) + ''; } - if ( token.mentionedUser ) + else if ( token.mentionedUser ) return '' + utils.sanitize(token.mentionedUser) + ""; - if ( token.deletedLink ) + else if ( token.deletedLink ) return utils.sanitize(token.text); return utils.sanitize(token); @@ -1017,4 +1020,76 @@ FFZ.prototype._deleted_link_click = function(e) { // Stop from Navigating e.preventDefault(); +} + + +// --------------------- +// History Loading +// --------------------- + +FFZ.prototype.parse_history = function(history, purged, room_id, delete_links, tmiSession, per_line) { + var i = history.length, was_cleared = false; + purged = purged || {}; + + while(i--) { + var msg = history[i], + is_deleted = msg.ffz_deleted = purged[msg.from] || false; + + if ( is_deleted && ! this.settings.prevent_clear ) + msg.deleted = true; + + if ( ! msg.room && room_id ) + msg.room = room_id; + + if ( typeof msg.date === "string" || typeof msg.date === "number" ) + msg.date = utils.parse_date(msg.date); + + if ( ! msg.color ) + msg.color = msg.tags && msg.tags.color ? msg.tags.color : tmiSession && msg.from ? tmiSession.getColor(msg.from) : "#755000"; + + if ( ! msg.labels || ! msg.labels.length ) { + var labels = msg.labels = []; + + if ( msg.room && msg.room === msg.from ) + labels.push("owner"); + else if ( msg.tags ) { + var ut = msg.tags['user-type']; + if ( ut === 'mod' || ut === 'staff' || ut === 'admin' || ut === 'global_mod' ) + labels.push(ut); + } + + if ( msg.tags ) { + if ( msg.tags.turbo ) + labels.push("turbo"); + if ( msg.tags.subscriber ) + labels.push("subscriber"); + } + } + + if ( ! msg.style ) { + if ( msg.from === "jtv" ) + msg.style = "admin"; + else if ( msg.from === "twitchnotify" ) + msg.style = "notification"; + } + + if ( msg.tags && typeof msg.tags.emotes === "string" ) + msg.tags.emotes = utils.uncompressEmotes(msg.tags.emotes); + + if ( ! msg.cachedTokens || ! msg.cachedTokens.length ) + this.tokenize_chat_line(msg, true, delete_links); + + // CLEARCHAT + if ( msg.tags && msg.tags.target === '@@' ) + was_cleared = true; + + else if ( msg.tags && msg.tags.target ) + purged[msg.tags.target] = true; + + // Per-line + if ( per_line && ! per_line(msg) ) + break; + } + + return [history, purged, was_cleared]; } \ No newline at end of file diff --git a/src/ui/dark.js b/src/ui/dark.js index 3a515933..8992999a 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -214,6 +214,6 @@ FFZ.prototype._load_dark_css = function() { s.id = "ffz-dark-css"; s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.DIRECT_SERVER + "script/dark.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); + s.setAttribute('href', constants.DIRECT_SERVER + "script/dark" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); } \ No newline at end of file diff --git a/src/ui/following-count.js b/src/ui/following-count.js index 43da8b50..e8891f1d 100644 --- a/src/ui/following-count.js +++ b/src/ui/following-count.js @@ -180,7 +180,8 @@ FFZ.prototype._build_following_tooltip = function(el) { } var up_since = this.settings.stream_uptime && stream.created_at && utils.parse_date(stream.created_at), - uptime = up_since && Math.floor((Date.now() - up_since.getTime()) / 1000) || 0, + now = Date.now() - (this._ws_server_offset || 0), + uptime = up_since && Math.floor((now - up_since.getTime()) / 1000) || 0, minutes = Math.floor(uptime / 60) % 60, hours = Math.floor(uptime / 3600); diff --git a/src/ui/following.js b/src/ui/following.js index 45c61d2c..0207981c 100644 --- a/src/ui/following.js +++ b/src/ui/following.js @@ -281,7 +281,7 @@ FFZ.prototype.rebuild_following_ui = function() { FFZ.prototype._build_following_button = function(container, channel_id) { if ( ! VALID_CHANNEL.test(channel_id) ) return this.log("Ignoring Invalid Channel: " + utils.sanitize(channel_id)); - + var btn = document.createElement('a'), f = this, btn_c = document.createElement('div'), noti = document.createElement('a'), @@ -376,7 +376,7 @@ FFZ.prototype._build_following_button = function(container, channel_id) { btn.addEventListener('mousedown', function(e) { if ( e.button !== 1 ) return; - + e.preventDefault(); window.open(Twitch.uri.profile(channel_id)); }); @@ -396,7 +396,7 @@ FFZ.prototype._build_following_button = function(container, channel_id) { display_name = FFZ.get_capitalization(channel_id, on_name); update(); - + setTimeout(check_following, Math.random()*5000); container.appendChild(btn_c); @@ -405,19 +405,11 @@ FFZ.prototype._build_following_button = function(container, channel_id) { FFZ.prototype._build_following_popup = function(container, channel_id, notifications) { - var popup = this._popup, out = '', + var popup = this.close_popup(), out = '', pos = container.offsetLeft + container.offsetWidth; - - if ( popup ) { - popup.parentElement.removeChild(popup); - delete this._popup; - this._popup_kill && this._popup_kill(); - delete this._popup_kill; - - if ( popup.id == "ffz-following-popup" && popup.getAttribute('data-channel') === channel_id ) - return null; - } + if ( popup && popup.id == "ffz-following-popup" && popup.getAttribute('data-channel') === channel_id ) + return null; popup = this._popup = document.createElement('div'); popup.id = 'ffz-following-popup'; diff --git a/src/ui/menu.js b/src/ui/menu.js index 9a38f083..ce29afdd 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -35,26 +35,6 @@ var FFZ = window.FrankerFaceZ, // -------------------- FFZ.prototype.setup_menu = function() { - this.log("Installing mouse-up event to auto-close menus."); - var f = this; - - jQuery(document).mouseup(function(e) { - var popup = f._popup, parent; - if ( ! popup ) return; - if ( popup.id === 'ffz-chat-menu' && popup.style && popup.style.left ) - return; - - popup = jQuery(popup); - parent = popup.parent(); - - if ( ! parent.is(e.target) && parent.has(e.target).length === 0 ) { - popup.remove(); - delete f._popup; - f._popup_kill && f._popup_kill(); - delete f._popup_kill; - } - }); - document.body.classList.toggle("ffz-menu-replace", this.settings.replace_twitch_menu); // Add FFZ to the chat settings menu. @@ -62,7 +42,8 @@ FFZ.prototype.setup_menu = function() { this.log("Hooking the Ember Chat Settings view."); var Settings = window.App && App.__container__.resolve('view:settings'), - Layout = App.__container__.lookup('controller:layout'); + Layout = window.App && App.__container__.lookup('controller:layout'), + f = this; if ( ! Settings ) return; @@ -215,14 +196,9 @@ FFZ.prototype._fix_menu_position = function() { } FFZ.prototype.build_ui_popup = function(view) { - var popup = this._popup; - if ( popup ) { - popup.parentElement.removeChild(popup); - delete this._popup; - this._popup_kill && this._popup_kill(); - delete this._popup_kill; + var popup = this._popup ? this.close_popup() : this._last_popup; + if ( popup && popup.id === 'ffz-chat-menu' ) return; - } // Start building the DOM. var container = document.createElement('div'), @@ -271,12 +247,8 @@ FFZ.prototype.build_ui_popup = function(view) { close_btn.addEventListener('click', function() { var popup = f._popup; - if ( can_close && popup ) { - popup.parentElement.removeChild(popup); - delete f._popup; - f._popup_kill && f._popup_kill(); - delete f._popup_kill; - } + if ( can_close && popup ) + f.close_popup(); }); menu.appendChild(heading); @@ -353,9 +325,12 @@ FFZ.prototype.build_ui_popup = function(view) { // Add the menu to the DOM. - this._popup = container; sub_container.style.maxHeight = Math.max(200, view.$().height() - 172) + "px"; view.$('.chat-interface').append(container); + + // Keep track of the pop-up. + this._popup = container; + this._popup_allow_parent = true; } @@ -511,7 +486,8 @@ FFZ.menu_pages.channel = { sub_message = document.createElement("div"), nonsub_message = document.createElement("div"), unlock_text = document.createElement("span"), - end_time = ends_at ? Math.floor((ends_at.getTime() - Date.now()) / 1000) : null; + now = Date.now() - (this._ws_server_offset || 0), + end_time = ends_at ? Math.floor((ends_at.getTime() - now) / 1000) : null; sub_message.className = "subscribe-message"; nonsub_message.className = "non-subscriber-message"; diff --git a/src/ui/popups.js b/src/ui/popups.js new file mode 100644 index 00000000..109f979d --- /dev/null +++ b/src/ui/popups.js @@ -0,0 +1,84 @@ +var FFZ = window.FrankerFaceZ; + + +// --------------- +// Initialization +// --------------- + +FFZ.prototype.setup_popups = function() { + this.log("Installing mouse-up event to auto-close pop-ups."); + var f = this; + + jQuery(document).mouseup(function(e) { + if ( e.button && e.button !== 0 ) + return; + + var popup = f._popup, + parent = f._popup_parent; + + if ( ! popup ) + f._last_popup = undefined; + + if ( ! popup || popup === e.target || popup.contains(e.target) ) + return; + + if ( popup.id === 'ffz-chat-menu' && popup.style && popup.style.left ) + return; + + if ( f._popup_allow_parent ) { + var parent = f._popup_parent || popup.parentElement; + if ( parent && ( parent === e.target || parent.contains(e.target) ) ) + return; + } + + f.close_popup(); + }); +} + + +// --------------- +// Management +// --------------- + +FFZ.prototype.close_popup = function() { + var popup = this._popup; + this._last_popup = popup; + if ( ! popup ) + return; + + popup.parentElement.removeChild(popup); + + if ( this._popup_kill ) + try { + this._popup_kill(); + } catch(err) { + this.error("_popup_kill: " + err); + } + + this._popup = undefined; + this._popup_parent = undefined; + this._popup_kill = undefined; + this._popup_allow_parent = undefined; + return popup; +} + + +FFZ.prototype.show_popup = function(el, position, container, cleanup, allow_parent, dont_insert_handler) { + if ( this._popup ) + this.close_popup(); + + this._popup = el; + this._popup_allow_parent = allow_parent || false; + this._popup_kill = cleanup; + + container = container || document.querySelector('.app-main') || document.body; + + var bounds = container.getBoundingClientRect(); + + el.style.display = 'block'; + el.style.position = 'absolute'; + el.style.left = (position[0] - bounds.left) + 'px'; + el.style.top = (position[1] - bounds.top) + 'px'; + + container.appendChild(el); +} \ No newline at end of file diff --git a/src/ui/races.js b/src/ui/races.js index 8bd4f4db..d59f2df7 100644 --- a/src/ui/races.js +++ b/src/ui/races.js @@ -180,16 +180,9 @@ FFZ.prototype._race_kill = function() { FFZ.prototype._build_race_popup = function(container, channel_id) { - var popup = this._popup; - if ( popup ) { - popup.parentElement.removeChild(popup); - delete this._popup; - this._popup_kill && this._popup_kill(); - delete this._popup_kill; - - if ( popup.id === "ffz-race-popup" && popup.getAttribute('data-channel') === channel_id ) - return; - } + var popup = this.close_popup(); + if ( popup && popup.id === "ffz-race-popup" && popup.getAttribute('data-channel') === channel_id ) + return; if ( ! container ) return; @@ -204,6 +197,7 @@ FFZ.prototype._build_race_popup = function(container, channel_id) { popup.className = (pos >= 300 ? 'right' : 'left') + ' share dropmenu'; this._popup_kill = this._race_kill.bind(this); + this._popup_allow_parent = true; this._popup = popup; var link = 'http://kadgar.net/live', @@ -256,13 +250,8 @@ FFZ.prototype._update_race = function(container, not_timer) { if ( ! race ) { // No race. Abort. container.parentElement.removeChild(container); - if ( this._popup && this._popup.id === 'ffz-race-popup' && this._popup.getAttribute('data-channel') === channel_id ) { - this._popup_kill && this._popup_kill(); - if ( this._popup ) { - delete this._popup; - delete this._popup_kill; - } - } + if ( this._popup && this._popup.id === 'ffz-race-popup' && this._popup.getAttribute('data-channel') === channel_id ) + this.close_popup(); return; } @@ -270,7 +259,7 @@ FFZ.prototype._update_race = function(container, not_timer) { entrant = race.entrants[entrant_id], popup = container.querySelector('#ffz-race-popup'), - now = Date.now() / 1000, + now = (Date.now() - (this._ws_server_offset || 0)) / 1000, elapsed = Math.floor(now - race.time); container.querySelector('.logo').innerHTML = utils.placement(entrant); diff --git a/src/ui/styles.js b/src/ui/styles.js index b8b8a4b0..84bf28c5 100644 --- a/src/ui/styles.js +++ b/src/ui/styles.js @@ -11,7 +11,7 @@ FFZ.prototype.setup_css = function() { var s = this._main_style = document.createElement('link'); s.id = "ffz-main-css"; s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.DIRECT_SERVER + "script/style.css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); + s.setAttribute('href', constants.DIRECT_SERVER + "script/style" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); this.log("Readying toggleable styles."); diff --git a/src/utils.js b/src/utils.js index 364e8d05..7c099b71 100644 --- a/src/utils.js +++ b/src/utils.js @@ -25,6 +25,16 @@ var sanitize_el = document.createElement('span'), return msg.replace(R_AMP, "&").replace(R_QUOTE, """).replace(R_SQUOTE, "'").replace(R_LT, "<").replace(R_GT, ">"); }, + HUMAN_NUMBERS = [ + "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" + ], + + number_commas = function(x) { + var parts = x.toString().split("."); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return parts.join("."); + }, + pluralize = function(value, singular, plural) { plural = plural || 's'; singular = singular || ''; @@ -157,6 +167,37 @@ var sanitize_el = document.createElement('span'), return tags; }, + uncompressEmotes = function(value) { + var output = {}, + emotes = value.split("/"), + i = emotes.length; + + while(i--) { + var parts = emotes[i].split(":"); + if ( parts.length !== 3 ) + return {}; + + var emote_id = parts[0], + length = parseInt(parts[1]), + positions = parts[2].split(","), + indices = output[emote_id] = output[emote_id] || []; + + for(var j=0, jl = positions.length; j < jl; j++) { + var start = parseInt(positions[j]), + end = start + length; + + for(var x=0, xl = indices.length; x < xl; x++) { + if ( start < indices[x][0] ) + break; + } + + indices.splice(x, 0, [start, end]); + } + } + + return output; + }, + EMOJI_CODEPOINTS = {}, emoji_to_codepoint = function(icon, variant) { @@ -316,16 +357,13 @@ module.exports = { splitIRCMessage: splitIRCMessage, parseIRCTags: parseIRCTags, + uncompressEmotes: uncompressEmotes, emoji_to_codepoint: emoji_to_codepoint, parse_date: parse_date, - number_commas: function(x) { - var parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return parts.join("."); - }, + number_commas: number_commas, place_string: place_string, @@ -345,6 +383,10 @@ module.exports = { pluralize: pluralize, + human_number: function(value) { + return HUMAN_NUMBERS[value] || number_commas(value); + }, + human_time: function(elapsed, factor) { factor = factor || 1; elapsed = Math.floor(elapsed); diff --git a/style.css b/style.css index 651bf347..0c9514b4 100644 --- a/style.css +++ b/style.css @@ -902,6 +902,49 @@ span.ffz-handle:after { left: 8px } .ffz-ui-popup.dark li.title { color: #fff; } .ffz-ui-popup.dark .ffz-ui-menu-page { background-color: #1e1e1e; } +/* Host Menu */ + +.ffz-channel-selector { + background-image: url("//cdn.frankerfacez.com/script/zreknarf-bg.png"); + background-repeat: no-repeat; + background-size: 50%; + background-position: 110% 115%; +} + +.ffz-channel-selector .dropmenu_action { + display: block; + position: relative; + line-height: 34px; + overflow: hidden; + padding: 0 !important; +} + +.ffz-channel-selector .image { + display: inline-block; + width: 20px; + height: 20px; + float: left; + margin: 6px 15px 8px 20px; +} + +.ffz-channel-selector .title { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; + padding-right: 20px; +} + +.ffz-channel-selector .header { + padding: 5px 20px; + margin-bottom: 0; + border-bottom: none; + line-height: 22px; + text-transform: uppercase; + font-size: 11px; + color: #666; +} + /* Positioning Fixes */ @@ -1187,6 +1230,7 @@ body:not(.ffz-bttv) .more-messages-indicator { overflow: hidden; } +.chat-history.loading:after, .ffz-ui-sub-menu-page:empty::after, .ffz-ui-menu-page:empty::after { content: " "; @@ -1608,6 +1652,67 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar margin-right: 5px; } +.chat-history .chat-line:not(.original-sender) span.from:hover, +.chat-history .timestamp:hover { + cursor: pointer; + text-decoration: underline; +} + +.chat-history.loading { + position: relative; + overflow-y: hidden !important; +} +.chat-history.loading li { pointer-events: none; } + +.chat-history .from { font-weight: 700; } + +.chat-history.loading:before { + content: " "; + display: block; + position: absolute; + z-index: 999; + top: 0; bottom: 0; + left: 0; right: 0; + background-color: rgba(128,128,128,0.8); +} + +.theatre .chat-history.loading:before, +.dark .chat-history.loading:before, +.force-dark .chat-history.loading:before { + background-color: rgba(0,0,0,0.8); +} + +.chat-history.loading:after { + visibility: visible; + clear: none; + position: absolute; + top: 0; + left: 50%; + margin-left: -40px; + z-index: 1000; +} + +.ember-chat .moderation-card .back-button { + border: 1px solid rgba(0,0,0,0.2); + border-top: none; + float: none; + display: block; + width: 100%; + background-color: #fff; +} + +.ember-chat .moderation-card .back-button:hover { + background-color: #6441A5 !important; + color: #fff !important; +} + +.theatre .moderation-card .back-button, +.dark .moderation-card .back-button, +.force-dark .moderation-card .back-button { + background-color: #232329; +} + + /* Room State */ .ffz.room-state.stat { @@ -1815,6 +1920,9 @@ body:not([data-current-path^="user."]) .ffz-sidebar-swap .ember-chat .chat-inter background-color: #191919; } +.ffz-no-blue .theatre .moderation-card .back-button, +.ffz-no-blue .dark .moderation-card .back-button, +.ffz-no-blue .force-dark .moderation-card .back-button .ffz-no-blue .chat-container.dark .chat-interface .emoticon-selector .tabs, .ffz-no-blue .app-main.theatre .chat-container .chat-interface .emoticon-selector .tabs, .ffz-no-blue .chat-container.force-dark .chat-interface .emoticon-selector .tabs, @@ -2069,8 +2177,52 @@ li[data-name="following"] a { margin-right: 20px; } +/* Chat Interactions Fixes */ + +body:not(.ffz-bttv) .ember-chat .chat-commands-dropdown { + z-index: 998; /* 1 less than the header */ + padding: 0; + margin-top: -1px; + background-color: #9265d5 !important; +} + +body:not(.ffz-bttv) .ember-chat .chat-commands-dropdown li { + border: none; +} + +body:not(.ffz-bttv) .ember-chat .chat-commands-dropdown li + li { + border-top: 1px solid rgba(0,0,0,0.25); +} + +body:not(.ffz-bttv) .app-main.theatre .chat-container .chat-commands-dropdown li, +body:not(.ffz-bttv) .dark .ember-chat .chat-commands-dropdown li, +body:not(.ffz-bttv) .force-dark .ember-chat .chat-commands-dropdown li, +body:not(.ffz-bttv) .ember-chat .chat-commands-dropdown li:hover { + background-color: rgba(0,0,0,0.25) !important; + color: #fff; +} + +body:not(.ffz-bttv) .app-main.theatre .chat-container .chat-commands-dropdown li:hover, +body:not(.ffz-bttv) .dark .ember-chat .chat-commands-dropdown li:hover, +body:not(.ffz-bttv) .force-dark .ember-chat .chat-commands-dropdown li:hover { + background-color: rgba(0,0,0,0.75) !important; + color: #fff !important; +} + + /* Conversations */ +body:not(.ffz-bttv) .conversation-window .new-message-divider + .timestamp-line { + margin-top: -3px; +} + +/* Fix the ignore-cta covering up messages with no way to dismiss it. */ +body:not(.ffz-bttv) .conversation-window .ignore-cta + .conversation-content { + padding-top: 76px; +} + +/* Hide that which should be hidden. */ +.conversation-window.collapsed .ignore-cta, .conversation-chat-line.action .colon, .conversation-input-bar .emoticon-selector .tabs, .conversation-preview-line .badges, @@ -2237,4 +2389,42 @@ body:not(.ffz-conv-title-clickable) .conversation-header a.conversation-header-n .user.item .actions .follow svg { margin: 4.5px 0 -4.5px -1px; +} + +/* Creative Directory */ + +.ffz-creative-tags .creativetag-list.filter-list, +.ffz-creative-tags .creativetag-list ul, +.ffz-creative-tags .creativetag-list-contain { + width: inherit; + height: inherit; + overflow: inherit; +} + +body:not(.ffz-creative-showcase) .creative-hero, +.ffz-creative-tags .creativetag-list-contain .filter-nav { display: none; } + +.ffz-creative-tags .creativetag-list ul { + display: flex; + flex-wrap: wrap; + position: inherit; + margin-bottom: -5px; +} + +.ffz-creative-tags .creativetag-list li { + flex-grow: 1; + height: inherit; + float: none; + margin: 0 0 5px; +} + +.ffz-creative-tags .creativetag-list a { + display: block; + text-align: center; + padding: 5px 10px; +} + +.ffz-creative-tags .creativetag-list a.active { + color: #fff !important; + background-color: #6441a5 !important; } \ No newline at end of file From 800553c6026c40b7721ee76313979f304d5bf415 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Sat, 12 Dec 2015 13:28:35 -0500 Subject: [PATCH 2/2] 3.5.84 to 3.5.100. Most importantly, started using new Ember stuff. -view-registry instead of Ember.View.views, and such. Finally added UI for managing pinned channels. Use HTTPS for the API and socket servers. Don't immediately unload chat rooms. Smarter chat tab behavior. Added /card command for opening mod cards. Other stuff. --- dark.css | 12 +- src/badges.js | 25 +- src/colors.js | 6 +- src/commands.js | 41 ++ src/constants.js | 14 +- src/ember/channel.js | 11 +- src/ember/chat-input.js | 32 +- src/ember/chatview.js | 799 +++++++++++++++++++++++------------ src/ember/conversations.js | 6 +- src/ember/directory.js | 62 ++- src/ember/following.js | 7 +- src/ember/line.js | 2 +- src/ember/moderation-card.js | 192 +++++---- src/ember/player.js | 7 +- src/ember/room.js | 93 +++- src/ember/viewers.js | 22 +- src/emoticons.js | 4 +- src/ext/api.js | 22 +- src/ext/betterttv.js | 10 +- src/ext/rechat.js | 24 +- src/main.js | 10 +- src/socket.js | 2 +- src/tokenize.js | 22 +- src/ui/dark.js | 22 +- src/ui/group_chat.js | 47 --- src/ui/menu.js | 9 +- src/ui/styles.js | 2 +- style.css | 36 +- 28 files changed, 1016 insertions(+), 525 deletions(-) delete mode 100644 src/ui/group_chat.js diff --git a/dark.css b/dark.css index e189644b..309ca7cb 100644 --- a/dark.css +++ b/dark.css @@ -3,7 +3,7 @@ background-color:rgb(16,16,16)!important; } -.ffz-dark div#channel > .target-frame.active{ +.ffz-dark div#channel > .target-frame { background-color:rgb(16,16,16)!important; } @@ -22,6 +22,14 @@ border-top: 1px solid rgba(255, 255, 255, 0.05); } +.ffz-dark .offlineChannelStatus { + background-color: rgba(255,255,255, 0.05); +} + +.ffz-dark .close-hostmode a:before, +.ffz-dark .close-hostmode a:after { + border-bottom-color: rgba(255,255,255, 0.05); +} /* hidden chat */ @@ -248,7 +256,7 @@ .ffz-dark .manager .videos-grid .video:hover .meta .actions li a, .ffz-dark .ember-chat .chat-room-list .room:not(:hover) p.room-title, .ffz-dark .dropmenu_action:not(:hover) span, -.ffz-dark a:not(.filter-item):not(.ui-state-focus):not(.button):not(.switch):not(.follow):not(.fb_button) { +.ffz-dark a:not(.filter-item):not(.ui-state-focus):not(.button):not(.switch):not(.follow):not(.fb_button):not(.what) { color: #a68ed2; } diff --git a/src/badges.js b/src/badges.js index 0632e4bc..f0359d37 100644 --- a/src/badges.js +++ b/src/badges.js @@ -228,9 +228,9 @@ FFZ.prototype.get_line_badges = function(msg) { } } - if ( msg.labels.indexOf('subscriber') !== -1 ) + if ( msg.labels && msg.labels.indexOf('subscriber') !== -1 ) badges[10] = {klass: 'subscriber', title: 'Subscriber'} - if ( msg.labels.indexOf('turbo') !== -1 ) + if ( msg.labels && msg.labels.indexOf('turbo') !== -1 ) badges[15] = {klass: 'turbo', title: 'Turbo'}; // FFZ Badges @@ -338,7 +338,7 @@ FFZ.prototype.bttv_badges = function(data) { if ( b.type === full_badge.replaces_type ) { b.type = "ffz-badge-replacement " + b.type; b.description += ", " + (badge.title || full_badge.title) + - '" style="background-image: url("' + utils.quote_attr(badge.image || full_badge.image) + '")'; + '" style="background-image: url(' + utils.quote_attr('"' + (badge.image || full_badge.image) + '"') + ')'; replaced = true; break; } @@ -349,18 +349,18 @@ FFZ.prototype.bttv_badges = function(data) { } if ( alpha && badge.transparent_image ) - style += 'background-image: url("' + utils.quote_attr(badge.transparent_image) + '");'; + style += 'background-image: url("' + badge.transparent_image + '");'; else if ( badge.image ) - style += 'background-image: url("' + utils.quote_attr(badge.image) + '");'; + style += 'background-image: url("' + badge.image + '");'; if ( badge.color && ! alpha ) - style += 'background-color: ' + utils.quote_attr(badge.color) + '; '; + style += 'background-color: ' + badge.color + '; '; if ( badge.extra_css ) - style += utils.quote_attr(badge.extra_css); + style += badge.extra_css; if ( style ) - desc += '" style="' + style; + desc += '" style="' + utils.quote_attr(style); badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: "ffz-badge-" + badge.id + (alpha ? " alpha" : ""), name: "", description: desc}]); } @@ -451,14 +451,17 @@ FFZ.prototype._legacy_load_donors = function(callback, tries) { FFZ.prototype._legacy_parse_badges = function(callback, data, slot, badge_id, title_template) { var title = this.badges[badge_id].title, - count = 0; + count = 0, ds = null; title_template = title_template || '{}'; if ( data != null ) { - var lines = data.trim().split(/\W+/); + var lines = data.trim().split(/[ \t\n\r]+/); for(var i=0; i < lines.length; i++) { + if ( ! /^\w/.test(lines[i]) ) + continue; + var line_data = lines[i].split(";"), user_id = line_data[0], user = this.users[user_id] = this.users[user_id] || {}, @@ -471,7 +474,7 @@ FFZ.prototype._legacy_parse_badges = function(callback, data, slot, badge_id, ti if ( badges[slot] ) continue; - badges[slot] = {id:badge_id}; + badges[slot] = {id: badge_id}; if ( line_data.length > 1 ) badges[slot].title = title_template.replace('{}', line_data[1]); count += 1; diff --git a/src/colors.js b/src/colors.js index 8b844339..3290db5c 100644 --- a/src/colors.js +++ b/src/colors.js @@ -129,9 +129,9 @@ FFZ.prototype.setup_colors = function() { Layout.addObserver("isTheatreMode", this._update_colors.bind(this, true)); if ( Settings ) - Settings.addObserver("model.darkMode", this._update_colors.bind(this, true)) + Settings.addObserver("settings.darkMode", this._update_colors.bind(this, true)) - this._color_old_darkness = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); + this._color_old_darkness = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); } @@ -602,7 +602,7 @@ FFZ.prototype._update_colors = function(darkness_only) { var Layout = window.App && App.__container__.lookup('controller:layout'), Settings = window.App && App.__container__.lookup('controller:settings'), - is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); if ( darkness_only && this._color_old_darkness === is_dark ) return; diff --git a/src/commands.js b/src/commands.js index ff279b11..811272ca 100644 --- a/src/commands.js +++ b/src/commands.js @@ -69,6 +69,47 @@ FFZ.ffz_commands.reload = function(room, args) { } +// ----------------- +// Moderation Cards +// ----------------- + +FFZ.chat_commands.card = function(room, args) { + if ( ! args || ! args.length || args.length > 1 ) + return "Usage: /card "; + + if ( ! this._roomv ) + return "An error occured. (We don't have the Room View.)"; + + // Get the position of the input box. + var el = this._roomv.get('element'), + ta = el && el.querySelector('textarea'), + bounds = ta && ta.getBoundingClientRect(), + + x = 0, y = 0, bottom, right; + + if ( ! bounds ) + bounds = el && el.getBoundingClientRect() || document.body.getBoundingClientRect(); + + if ( bounds ) { + if ( bounds.left > 400 ) { + right = bounds.left - 40; + bottom = bounds.top + bounds.height; + } else { + x = bounds.left - 20; + bottom = bounds.top - 20; + } + } + + this._roomv.get('controller').send('showModOverlay', { + top: y, + left: x, + bottom: bottom, + right: right, + sender: args[0] + }); +} + + // ----------------- // Mass Moderation // ----------------- diff --git a/src/constants.js b/src/constants.js index 15220f15..5ed52212 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,21 +2,21 @@ var SVGPATH = ' 1 ) { this.set('ffz_chatters', value); diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 8359ffc4..cb354b63 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -258,24 +258,45 @@ FFZ.settings_info.chat_history = { }; FFZ.settings_info.group_tabs = { - type: "boolean", - value: false, + type: "select", + options: { + 0: "Disabled", + 1: "Rooms with Recent Activity", + 2: "Rooms with Recent Mentions", + 3: "All Rooms" + }, + + value: 0, + + process_value: function(val) { + if ( val === false ) + return 0; + else if ( val === true ) + return 3; + else if ( typeof val === "string" ) + return parseInt(val) || 0; + return val; + }, no_bttv: true, - category: "Chat Moderation", - name: "Chat Room Tabs Beta", - help: "Enhanced UI for switching the current chat room and noticing new messages.", + category: "Chat Appearance", + name: "Chat Room Tabs", + help: "Display tabs for chat rooms with recent activity at the top of the chat window for more convenient chatting.", on_update: function(val) { - var enabled = !this.has_bttv && val; - if ( ! this._chatv || enabled === this._group_tabs_state ) + if ( this.has_bttv || ! this._chatv ) return; - if ( enabled ) - this._chatv.ffzEnableTabs(); + if ( val ) + if ( this._chatv._ffz_tabs ) + this._chatv.ffzRebuildTabs(); + else + this._chatv.ffzEnableTabs(); else this._chatv.ffzDisableTabs(); + + this._chatv.ffzUpdateMenuUnread(); } }; @@ -307,7 +328,7 @@ FFZ.prototype.setup_chatview = function() { if ( Chat ) { Chat.reopen({ ffzUpdateChannels: function() { - if ( ! f._chatv ) + if ( ! f._chatv || f.has_bttv ) return; f._chatv.ffzRebuildMenu(); @@ -316,21 +337,72 @@ FFZ.prototype.setup_chatview = function() { }.observes("currentChannelRoom", "connectedPrivateGroupRooms"), + ffzUpdateInvites: function() { + if ( ! f._chatv || f.has_bttv ) + return; + + f._chatv.ffzUpdateMenuUnread(); + }.observes("invitedPrivateGroupRooms"), + + notificationsCount: function() { + if ( ! f._chatv || f.has_bttv ) + return this._super(); + + var total = this.get('invitedPrivateGroupRooms.length') || 0; + + if ( ! f._chatv._ffz_tabs ) + for(var room_id in f._chatv.ffz_unread) + if ( f._chatv.ffz_unread[room_id] ) + total++; + + return total; + }.property("currentRoom", "currentChannelRoom", "currentChannelRoom.unreadCount", "invitedPrivateGroupRooms.length", "connectedPrivateGroupRooms.@each.unreadCount"), + + _kickUserFromRoomNoLongerInList: function() { + // Remove an unread notice for any missing channels. + if ( f._chatv ) { + var updated = false; + for(var room_id in f._chatv.ffz_unread) + if ( f._chatv.ffz_unread[room_id] && (!f.rooms[room_id] || !f.rooms[room_id].room) ) { + f._chatv.ffz_unread[room_id] = false; + updated = true; + } + + if ( updated ) + f._chatv.ffzUpdateMenuUnread(); + } + + var room = this.get("currentRoom"), + room_id = room && room.get('id'), + channel_room = this.get("currentChannelRoom"), + is_group = room && _.contains(this.get("privateGroupRooms.content") || [], room); + + if ( room === channel_room || is_group || (f._chatv && f._chatv._ffz_host === room_id) || (f.settings.pinned_rooms && f.settings.pinned_rooms.indexOf(room_id) !== -1) ) + return; + + this.blurRoom(); + + if ( ! this.get("showList") ) + this.send("toggleMode"); + + }.observes("privateGroupRooms.@each"), + removeCurrentChannelRoom: function() { - if ( ! f.settings.group_tabs || f.has_bttv ) + if ( f.has_bttv ) return this._super(); var room = this.get("currentChannelRoom"), room_id = room && room.get('id'), user = f.get_user(); - if ( ! f.settings.pinned_rooms || f.settings.pinned_rooms.indexOf(room_id) === -1 ) { + // Don't clean up pinned rooms or the current host target. + if ( !((f._chatv && f._chatv._ffz_host === room_id) || (f.settings.pinned_rooms && f.settings.pinned_rooms.indexOf(room_id) !== -1)) ) { if ( room === this.get("currentRoom") ) this.blurRoom(); // Don't destroy it if it's the user's room. if ( room && user && user.login !== room_id ) - room.destroy(); + room.ffzScheduleDestroy(); } this.set("currentChannelRoom", void 0); @@ -351,11 +423,12 @@ FFZ.prototype.setup_chatview = function() { } catch(err) { } // Modify all existing Chat views. - for(var key in Ember.View.views) { - if ( ! Ember.View.views.hasOwnProperty(key) ) + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + if ( ! views.hasOwnProperty(key) ) continue; - var view = Ember.View.views[key]; + var view = views[key]; if ( !(view instanceof Chat) ) continue; @@ -417,10 +490,16 @@ FFZ.prototype._modify_cview = function(view) { this.$('.textarea-contain').append(f.build_ui_link(this)); this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); - if ( !f.has_bttv && f.settings.group_tabs ) - this.ffzEnableTabs(); + this.ffz_unread = {}; - this.ffzRebuildMenu(); + if ( ! f.has_bttv ) { + if ( f.settings.group_tabs ) + this.ffzEnableTabs(); + + this.ffzRebuildMenu(); + } + + this.ffz_pruner = setInterval(this.ffzPruneTabs.bind(this), 10000); setTimeout(function() { if ( f.settings.group_tabs && f._chatv && f._chatv._ffz_tabs ) @@ -435,12 +514,45 @@ FFZ.prototype._modify_cview = function(view) { if ( f._chatv === this ) f._chatv = null; + if ( this.ffz_pruner ) { + clearInterval(this.ffz_pruner); + this.ffz_pruner = null; + } + this.$('.textarea-contain .ffz-ui-toggle').remove(); if ( f.settings.group_tabs ) this.ffzDisableTabs(); + + this.ffzTeardownMenu(); + this.ffzUnloadHost(); }, + + ffzPruneTabs: function() { + if ( ! this._ffz_tabs ) + return; + + var elements = this._ffz_tabs.querySelectorAll('.ffz-chat-tab:not(.hidden):not(.active)'), + update_height = false; + + for(var i=0; i < elements.length; i++) { + var el = elements[i], + room_id = el.getAttribute('data-room'), + was_hidden = el.classList.contains('hidden'), + is_hidden = ! this.ffzTabVisible(room_id); + + if ( was_hidden !== is_hidden ) { + el.classList.toggle('hidden', is_hidden); + update_height = true; + } + } + + if ( update_height ) + this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); + }, + + ffzChangeRoom: Ember.observer('controller.currentRoom', function() { f.update_ui_link(); @@ -448,67 +560,228 @@ FFZ.prototype._modify_cview = function(view) { if ( f._mod_card ) f._mod_card.send('close'); - var room = this.get('controller.currentRoom'), rows; - room && room.resetUnreadCount(); + var room = this.get('controller.currentRoom'), + room_id = room && room.get('id'), + was_unread = room_id && this.ffz_unread[room_id], + update_height = false; - if ( room && room._ffz_was_unread ) { - room._ffz_was_unread = false; - - var el = this.get('element'), - unread_display = el && el.querySelector('#ffz-group-tabs .button .notifications'), - unread_count = unread_display ? parseInt(unread_display.textContent) : 0; - - unread_count--; - if ( unread_display ) - unread_display.textContent = unread_count || ''; + if ( room ) { + room.resetUnreadCount(); + room.ffz_last_view = Date.now(); } - if ( this._ffz_chan_table ) { - rows = jQuery(this._ffz_chan_table); - rows.children('.ffz-room-row').removeClass('active'); - } - if ( this._ffz_group_table ) { - rows = jQuery(this._ffz_group_table); - rows.children('.ffz-room-row').removeClass('active'); - } + if ( room && room._ffz_tab ) { + var was_hidden = room._ffz_tab.classList.contains('hidden'), + is_hidden = ! this.ffzTabVisible(room_id); - if ( !f.has_bttv && f.settings.group_tabs && this._ffz_tabs ) { - var tabs = jQuery(this._ffz_tabs); - tabs.children('.ffz-chat-tab').removeClass('active'); - - if ( room && room._ffz_tab ) { - room._ffz_tab.classList.remove('tab-mentioned'); - room._ffz_tab.classList.remove('hidden'); - room._ffz_tab.classList.add('active'); - var sp = room._ffz_tab.querySelector('span'); - if ( sp ) - sp.innerHTML = ''; + if ( was_hidden !== is_hidden ) { + room._ffz_tab.classList.toggle('hidden', is_hidden); + update_height = true; } + } - if ( room && room._ffz_row ) { - room._ffz_row.classList.remove('row-mentioned'); - room._ffz_row.classList.remove('hidden'); - room._ffz_row.classList.add('active'); - var sp = room._ffz_row.querySelector('span'); - if ( sp ) - sp.innerHTML = ''; - } + if ( was_unread && room_id ) { + this.ffz_unread[room_id] = false; + this.ffzUpdateMenuUnread(); + } + + if ( this._ffz_chan_table ) + jQuery('.ffz-room-row.active', this._ffz_chan_table).removeClass('active'); + + if ( this._ffz_group_table ) + jQuery('.ffz-room-row.active', this._ffz_group_table).removeClass('active'); + + if ( this._ffz_tabs ) { + jQuery('.ffz-chat-tab.active', this._ffz_tabs).removeClass('active'); // Invite Link var can_invite = room && room.get('canInvite'); - this._ffz_invite && this._ffz_invite.classList.toggle('hidden', !can_invite); - this.set('controller.showInviteUser', can_invite && this.get('controller.showInviteUser')) + if ( this._ffz_invite ) + this._ffz_invite.classList.toggle('hidden', ! can_invite); - // Now, adjust the chat-room. - this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); + this.set('controller.showInviteUser', can_invite && this.get('controller.showInviteUser')); + update_height = true; } + + if ( room && room._ffz_tab ) { + room._ffz_tab.classList.remove('tab-mentioned'); + room._ffz_tab.classList.add('active'); + var sp = room._ffz_tab.querySelector('span'); + if ( sp ) + sp.innerHTML = ''; + } + + if ( room && room._ffz_row ) { + room._ffz_row.classList.remove('row-mentioned'); + room._ffz_row.classList.add('active'); + var sp = room._ffz_row.querySelector('span'); + if ( sp ) + sp.innerHTML = ''; + } + + if ( update_height ) + this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); }), - // Better Menu + + // Hosted Channel Chat + ffzUnloadHost: function() { + if ( ! this._ffz_host ) + return; + + if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 ) { + if ( this.get('controller.currentRoom') === this._ffz_host_room ) + this.get('controller').blurRoom(); + + // Schedule the room to be destroyed. This is after a short + // delay to make sure we aren't just loading the room in a + // new way. + this._ffz_host_room.ffzScheduleDestroy(); + } + + this._ffz_host = null; + this._ffz_host_room = null; + }, + + ffzUpdateHost: function() { + var Channel = App.__container__.lookup('controller:channel'), + Room = App.__container__.resolve('model:room'), + target = Room && Channel && Channel.get('hostModeTarget'), + + updated = false; + + if ( f.has_bttv ) + return; + + if ( target ) { + var target_id = target.get('id'); + if ( this._ffz_host !== target_id ) { + this.ffzUnloadHost(); + + this._ffz_host = target_id; + this._ffz_host_room = Room.findOne(target_id); + updated = true; + } + + } else if ( this._ffz_host ) { + this.ffzUnloadHost(); + updated = true; + } + + if ( updated ) { + this.ffzRebuildMenu(); + this.ffzRebuildTabs(); + } + }, + + + // Unread Handling + + ffzUpdateMenuUnread: function() { + var el = this.get('element'), + controller = this.get('controller'), + unread_display = el && el.querySelector('#ffz-group-tabs .button .notifications'); + + Ember.propertyDidChange(controller, 'notificationsCount'); + + if ( unread_display ) + unread_display.innerHTML = utils.format_unread(controller.get('notificationsCount')); + }, + + + ffzUpdateUnread: function(target_id) { + var current_id = this.get('controller.currentRoom.id'); + + if ( target_id === current_id ) + // We don't care about updates to the current room. + return; + + var to_update, + update_unread = false, + update_height = false; + + // If we DO have a room ID, only update that room. + if ( target_id ) + to_update = [target_id]; + else + to_update = Object.keys(f.rooms); + + for(var i=0; i < to_update.length; i++) { + var room_id = to_update[i], + room = f.rooms[room_id] && f.rooms[room_id].room, + row = room && room._ffz_row, + tab = room && room._ffz_tab, + + unread_count = room_id === current_id ? 0 : room.get('unreadCount'), + is_unread = unread_count > 0, + unread = utils.format_unread(unread_count); + + + if ( this.ffz_unread[room_id] !== is_unread ) { + this.ffz_unread[room_id] = is_unread; + update_unread = true; + } + + if ( row ) { + var sp = row.querySelector('span'); + if ( sp ) + sp.innerHTML = unread; + } + + if ( tab ) { + var was_hidden = tab.classList.contains('hidden'), + is_hidden = ! this.ffzTabVisible(room_id), + sp = tab.querySelector('span'); + + if ( was_hidden !== is_hidden ) { + tab.classList.toggle('hidden', is_hidden); + update_height = true; + } + + if ( sp ) + sp.innerHTML = unread; + } + } + + if ( update_height ) + this.$('.chat-room').css('top', this._ffz_tabs.offsetHeight + "px"); + + if ( update_unread ) + this.ffzUpdateMenuUnread(); + }, + + + // Menu Rendering + + ffzTeardownMenu: function() { + var el = this.get('element'), + room_list = el && el.querySelector('.chat-rooms .tse-content'), + + chan_table = room_list && room_list.querySelector('#ffz-channel-table'), + group_table = room_list && room_list.querySelector('#ffz-group-table'); + + if ( chan_table ) + chan_table.parentElement.removeChild(chan_table); + + if ( group_table ) + group_table.parentElement.removeChild(group_table); + + this._ffz_chan_table = null; + this._ffz_group_table = null; + + if ( room_list && room_list.classList.contains('ffz-room-list') ) { + room_list.classList.remove('ffz-room-list'); + jQuery('.ffz', room_list).removeClass('ffz'); + } + + for(var room_id in f.rooms) + if ( f.rooms[room_id] && f.rooms[room_id].room && f.rooms[room_id].room._ffz_row ) + f.rooms[room_id].room._ffz_row = null; + }, ffzRebuildMenu: function() { - /*var el = this.get('element'), + var el = this.get('element'), room_list = el && el.querySelector('.chat-rooms .tse-content'); if ( ! room_list ) @@ -530,40 +803,48 @@ FFZ.prototype._modify_cview = function(view) { // Channel Table - var t = this, + var view = this, chan_table = this._ffz_chan_table || room_list.querySelector('#ffz-channel-table tbody'); if ( ! chan_table ) { var tbl = document.createElement('table'); - tbl.setAttribute('cellspacing', 0); + tbl.setAttribute('cellspacing', '0'); tbl.id = 'ffz-channel-table'; tbl.className = 'ffz'; - tbl.innerHTML = 'ChannelsPin'; + tbl.innerHTML = 'ChannelsPin'; room_list.insertBefore(tbl, room_list.firstChild); - chan_table = this._ffz_chan_table = tbl.querySelector('tbody'); - } + jQuery('.ffz-row-switch', tbl).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'se')}); + + chan_table = this._ffz_chan_table = tbl.querySelector('tbody'); + + } else + chan_table.innerHTML = ''; - chan_table.innerHTML = ''; // Current Channel - var room = this.get('controller.currentChannelRoom'), row; + var room = this.get('controller.currentChannelRoom'), + room_id = room && room.get('id'), + row; + if ( room ) { - row = this.ffzBuildRow(this, room, true); + row = this.ffzBuildRow(room, true); row && chan_table.appendChild(row); } + // Host Target if ( this._ffz_host_room ) { - row = this.ffzBuildRow(this, this._ffz_host_room, false, true); + row = this.ffzBuildRow(this._ffz_host_room, false, true); row && chan_table.appendChild(row); } + // Pinned Rooms for(var i=0; i < f.settings.pinned_rooms.length; i++) { - var room_id = f.settings.pinned_rooms[i]; - if ( room && room.get('id') !== room_id && this._ffz_host !== room_id && f.rooms[room_id] && f.rooms[room_id].room ) { - row = this.ffzBuildRow(this, f.rooms[room_id].room); + var pinned_id = f.settings.pinned_rooms[i]; + if ( room_id !== pinned_id && this._ffz_host !== pinned_id && f.rooms[pinned_id] && f.rooms[pinned_id].room ) { + row = this.ffzBuildRow(f.rooms[pinned_id].room); row && chan_table.appendChild(row); } } @@ -573,7 +854,7 @@ FFZ.prototype._modify_cview = function(view) { var group_table = this._ffz_group_table || room_list.querySelector('#ffz-group-table tbody'); if ( ! group_table ) { var tbl = document.createElement('table'); - tbl.setAttribute('cellspacing', 0); + tbl.setAttribute('cellspacing', '0'); tbl.id = 'ffz-group-table'; tbl.className = 'ffz'; tbl.innerHTML = 'Group Chats'; @@ -582,12 +863,12 @@ FFZ.prototype._modify_cview = function(view) { room_list.insertBefore(tbl, before.nextSibling); group_table = this._ffz_group_table = tbl.querySelector('tbody'); - } - group_table.innerHTML = ''; + } else + group_table.innerHTML = ''; _.each(this.get('controller.connectedPrivateGroupRooms'), function(room) { - var row = t.ffzBuildRow(t, room); + var row = view.ffzBuildRow(room); row && group_table && group_table.appendChild(row); }); @@ -595,64 +876,67 @@ FFZ.prototype._modify_cview = function(view) { // Change Create Tooltip var create_btn = el.querySelector('.button.create'); if ( create_btn ) - create_btn.title = 'Create a Group Room';*/ + create_btn.title = 'Create a Group Room'; }, - /*ffzBuildRow: function(view, room, current_channel, host_channel) { - var row = document.createElement('tr'), + + ffzBuildRow: function(room, current_channel, host_channel) { + var view = this, + + row = document.createElement('tr'), icon = document.createElement('td'), name_el = document.createElement('td'), btn, toggle_pinned = document.createElement('td'), + room_id = room.get('id'), group = room.get('isGroupRoom'), - current = room === view.get('controller.currentRoom'), - unread = utils.format_unread(current ? 0 : room.get('unreadCount')), + active_channel = room === this.get('controller.currentRoom'), + unread = utils.format_unread(active_channel ? 0 : room.get('unreadCount')), - name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'), function(name) { - f.log("Name for Row: " + name); - unread = utils.format_unread(current ? 0 : room.get('unreadCount')); + name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room_id, function(name) { + var active_channel = room === view.get('controller.currentRoom'); + unread = utils.format_unread(active_channel ? 0 : room.get('unreadCount')); name_el.innerHTML = utils.sanitize(name) + ' ' + unread + ''; })); - name_el.className = 'ffz-room'; - name_el.innerHTML = utils.sanitize(name) + ' ' + unread + ''; - if ( current_channel ) { - icon.innerHTML = constants.CAMERA; - icon.title = name_el.title = "Current Channel"; - icon.className = name_el.className = 'tooltip'; - } else if ( host_channel ) { - icon.innerHTML = constants.EYE; - icon.title = name_el.title = "Hosted Channel"; - icon.className = name_el.className = 'tooltip'; - } - - toggle_pinned.className = 'ffz-row-switch'; - - toggle_pinned.innerHTML = ''; - - row.setAttribute('data-room', room.get('id')); + row.setAttribute('data-room', room_id); row.className = 'ffz-room-row'; row.classList.toggle('current-channel', current_channel); row.classList.toggle('host-channel', host_channel); row.classList.toggle('group-chat', group); - row.classList.toggle('active', current); + row.classList.toggle('active', active_channel); + + if ( current_channel ) { + icon.innerHTML = constants.CAMERA; + row.title = "Current Channel"; + row.classList.add('tooltip'); + + } else if ( host_channel ) { + icon.innerHTML = constants.EYE; + row.title = "Hosted Channel"; + row.classList.add('tooltip'); + } + + name_el.className = 'ffz-room'; + name_el.innerHTML = utils.sanitize(name) + ' ' + unread + ''; row.appendChild(icon); row.appendChild(name_el); + toggle_pinned.className = 'ffz-row-switch'; + if ( ! group ) { - row.appendChild(toggle_pinned); + toggle_pinned.innerHTML = ''; btn = toggle_pinned.querySelector('a.switch'); btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation && e.stopPropagation(); - var room_id = room.get('id'), - is_pinned = f.settings.pinned_rooms.indexOf(room_id) !== -1; + var is_pinned = f.settings.pinned_rooms.indexOf(room_id) !== -1; if ( is_pinned ) f._leave_room(room_id); @@ -667,7 +951,8 @@ FFZ.prototype._modify_cview = function(view) { btn.innerHTML = constants.CLOSE; btn.title = 'Leave Group'; - name_el.appendChild(btn); + toggle_pinned.innerHTML = ''; + toggle_pinned.appendChild(btn); btn.addEventListener('click', function(e) { e.preventDefault(); @@ -680,6 +965,8 @@ FFZ.prototype._modify_cview = function(view) { }); } + row.appendChild(toggle_pinned); + row.addEventListener('click', function() { var controller = view.get('controller'); controller.focusRoom(room); @@ -689,12 +976,13 @@ FFZ.prototype._modify_cview = function(view) { room._ffz_row = row; return row; - },*/ + }, - // Group Tabs~! + + // Group Tabs ffzEnableTabs: function() { - if ( f.has_bttv || ! f.settings.group_tabs ) + if ( f.has_bttv || ! f.settings.group_tabs || this._ffz_tabs ) return; // Hide the existing chat UI. @@ -709,34 +997,41 @@ FFZ.prototype._modify_cview = function(view) { this.ffzRebuildTabs(); }, - ffzRebuildTabs: function() { - if ( f.has_bttv || ! f.settings.group_tabs ) - return; + ffzDisableTabs: function() { + if ( this._ffz_tabs ) { + this._ffz_tabs.parentElement.removeChild(this._ffz_tabs); + this._ffz_tabs = null; + this._ffz_invite = null; + for(var room_id in f.rooms) + if ( f.rooms[room_id] && f.rooms[room_id].room && f.rooms[room_id].room._ffz_tab ) + f.rooms[room_id].room._ffz_tab = null; + } + + // Show the old chat UI. + this.$('.chat-room').css('top', ''); + this.$(".chat-header").removeClass("hidden"); + }, + + + ffzRebuildTabs: function() { var tabs = this._ffz_tabs || this.get('element').querySelector('#ffz-group-tabs'); if ( ! tabs ) return; tabs.innerHTML = ""; + if ( f.has_bttv || ! f.settings.group_tabs ) + return; + var link = document.createElement('a'), view = this; - //total_unread = 0; - /*for(var room_id in f.rooms) { - var room = f.rooms[room_id] && f.rooms[room_id].room, - is_unread = room && room.get('unreadCount') > 0; - - if ( is_unread ) { - room._ffz_was_unread = true; - total_unread++; - } else if ( room ) - room._ffz_was_unread = false; - }*/ + // Chat Room Management Button link.className = 'button glyph-only tooltip'; link.title = "Chat Room Management"; - link.innerHTML = constants.ROOMS; // + '' + (total_unread || '') + ''; + link.innerHTML = constants.ROOMS + ''; link.addEventListener('click', function() { var controller = view.get('controller'); @@ -746,6 +1041,7 @@ FFZ.prototype._modify_cview = function(view) { tabs.appendChild(link); + // Invite Button link = document.createElement('a'), link.className = 'button glyph-only tooltip invite'; link.title = "Invite a User"; @@ -760,145 +1056,70 @@ FFZ.prototype._modify_cview = function(view) { view._ffz_invite = link; tabs.appendChild(link); - var room = this.get('controller.currentChannelRoom'), tab; + + // Current Room + var room = this.get('controller.currentChannelRoom'), + room_id = room && room.get('id'), + tab; + if ( room ) { - tab = this.ffzBuildTab(view, room, true); + tab = this.ffzBuildTab(room, true); tab && tabs.appendChild(tab); } - // Check Host Target - var Channel = App.__container__.lookup('controller:channel'), - Room = App.__container__.resolve('model:room'); - target = Channel && Channel.get('hostModeTarget'); - - if ( target && Room ) { - var target_id = target.get('id'); - if ( this._ffz_host !== target_id ) { - if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { - if ( this.get('controller.currentRoom') === this._ffz_host_room ) - this.get('controller').blurRoom(); - this._ffz_host_room.destroy(); - } - - this._ffz_host = target_id; - this._ffz_host_room = Room.findOne(target_id); - } - } else if ( this._ffz_host ) { - if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { - if ( this.get('controller.currentRoom') === this._ffz_host_room ) - this.get('controller').blurRoom(); - this._ffz_host_room.destroy(); - } - - delete this._ffz_host; - delete this._ffz_host_room; - } + // Host Target if ( this._ffz_host_room ) { - tab = view.ffzBuildTab(view, this._ffz_host_room, false, true); + tab = view.ffzBuildTab(this._ffz_host_room, false, true); tab && tabs.appendChild(tab); } + // Pinned Rooms for(var i=0; i < f.settings.pinned_rooms.length; i++) { - var room_id = f.settings.pinned_rooms[i]; - if ( room && room.get('id') !== room_id && this._ffz_host !== room_id && f.rooms[room_id] && f.rooms[room_id].room ) { - var tab = view.ffzBuildTab(view, f.rooms[room_id].room, false, false); + var pinned_id = f.settings.pinned_rooms[i]; + if ( room_id !== pinned_id && this._ffz_host !== pinned_id && f.rooms[pinned_id] && f.rooms[pinned_id].room ) { + tab = view.ffzBuildTab(f.rooms[pinned_id].room, false, false); tab && tabs.appendChild(tab); } } + + // Group Chat _.each(this.get('controller.connectedPrivateGroupRooms'), function(room) { - var tab = view.ffzBuildTab(view, room); + var tab = view.ffzBuildTab(room); tab && tabs.appendChild(tab); }); - // Now, adjust the chat-room. + + // Adjust the height of the chat room to account for the height of the numerous tabs. this.$('.chat-room').css('top', tabs.offsetHeight + "px"); + this.ffzUpdateMenuUnread(); }, - ffzTabUnread: function(room_id) { - var current_id = this.get('controller.currentRoom.id'); + ffzBuildTab: function(room, current_channel, host_channel) { + var view = this, - if ( room_id ) { - var room = f.rooms && f.rooms[room_id] && f.rooms[room_id].room, - row = room && room._ffz_row, - tab = room && room._ffz_tab, - - unread_count = room_id === current_id ? 0 : room.get('unreadCount'), - is_unread = unread_count > 0, - unread = utils.format_unread(unread_count); - - if ( ! room._ffz_was_unread && is_unread ) { - room._ffz_was_unread = true; - - var el = this.get('element'), - unread_display = el && el.querySelector('#ffz-group-tabs .button .notifications'), - unread_count = unread_display ? parseInt(unread_display.textContent) : 0; - - unread_count++; - if ( unread_display ) - unread_display.textContent = unread_count || ''; - } - - if ( row ) - row.querySelector('span').innerHTML = unread; - - if ( tab ) - tab.querySelector('span').innerHTML = unread; - - return; - } - - for(var room_id in f.rooms) { - var room = f.rooms[room_id] && f.rooms[room_id].room, - row = room && room._ffz_row, - tab = room && room._ffz_tab, - - unread_count = room_id === current_id ? 0 : room.get('unreadCount'), - is_unread = unread_count > 0, - unread = utils.format_unread(unread_count); - - if ( ! room._ffz_was_unread && is_unread ) { - room._ffz_was_unread = true; - - var el = this.get('element'), - unread_display = el && el.querySelector('#ffz-group-tabs .button .notifications'), - unread_count = unread_display ? parseInt(unread_display.textContent) : 0; - - unread_count++; - if ( unread_display ) - unread_display.textContent = unread_count || ''; - } - - if ( row ) - row.querySelector('span').innerHTML = unread; - - if ( tab ) - tab.querySelector('span').innerHTML = unread; - } - }, - - ffzBuildTab: function(view, room, current_channel, host_channel) { - var tab = document.createElement('span'), name, unread, icon = '', + tab = document.createElement('span'), name, unread, icon = '', room_id = room.get('id'), group = room.get('isGroupRoom'), - current = room === view.get('controller.currentRoom'), - visible = current || f.settings.visible_rooms.indexOf(room_id) !== -1; + active_channel = room === this.get('controller.currentRoom'); - tab.setAttribute('data-room', room.id); + tab.setAttribute('data-room', room_id); tab.className = 'ffz-chat-tab tooltip'; - //tab.classList.toggle('hidden', ! visible); tab.classList.toggle('current-channel', current_channel); tab.classList.toggle('host-channel', host_channel); tab.classList.toggle('group-chat', group); - tab.classList.toggle('active', current); + tab.classList.toggle('active', active_channel); - unread = utils.format_unread(current ? 0 : room.get('unreadCount')); + tab.classList.toggle('hidden', ! this.ffzTabVisible(room_id)); - name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room.get('id'), function(name) { - unread = utils.format_unread(current ? 0 : room.get('unreadCount')); + unread = utils.format_unread(active_channel ? 0 : room.get('unreadCount')); + + name = room.get('tmiRoom.displayName') || (group ? room.get('tmiRoom.name') : FFZ.get_capitalization(room_id, function(name) { + var active_channel = room === view.get('controller.currentRoom'); + unread = utils.format_unread(active_channel ? 0 : room.get('unreadCount')); tab.innerHTML = icon + utils.sanitize(name) + '' + unread + ''; })); @@ -925,28 +1146,31 @@ FFZ.prototype._modify_cview = function(view) { return tab; }, - ffzDisableTabs: function() { - if ( this._ffz_tabs ) { - this._ffz_tabs.parentElement.removeChild(this._ffz_tabs); - delete this._ffz_tabs; - delete this._ffz_invite; - } + ffzTabVisible: function(room_id) { + var room = f.rooms[room_id] && f.rooms[room_id].room, + is_current = room === this.get('controller.currentRoom'), + is_channel = room === this.get('controller.currentChannelRoom'), - if ( this._ffz_host ) { - if ( f.settings.pinned_rooms.indexOf(this._ffz_host) === -1 && this._ffz_host_room ) { - if ( this.get('controller.currentRoom') === this._ffz_host_room ) - this.get('controller').blurRoom(); - this._ffz_host_room.destroy(); - } + now = Date.now(); - delete this._ffz_host; - delete this._ffz_host_room; - } + if ( is_current || is_channel || room_id === this._ffz_host || f.settings.group_tabs === 3 ) + // Important Tabs + return true; - // Show the old chat UI. - this.$('.chat-room').css('top', ''); - this.$(".chat-header").removeClass("hidden"); - }, + else if ( now - room.ffz_last_view < 60000 || now - room.ffz_last_input < 2700000 ) + // Recent Self Input or View + return true; + + else if ( f.settings.group_tabs === 1 && now - (room.ffz_last_activity || 0) < 2700000 ) + // Any Recent Activity + return true; + + else if ( f.settings.group_tabs === 2 && now - (room.ffz_last_mention || 0) < 2700000 ) + // Recent Mentions + return true; + + return false; + } }); } @@ -960,11 +1184,12 @@ FFZ.prototype.connect_extra_chat = function() { if ( user && user.login ) { // Make sure we're in the user's room. if ( ! this.rooms[user.login] || this.rooms[user.login].room ) { - var Room = App.__container__.resolve('model:room'), - r = Room && Room.findOne(user.login); + var Room = App.__container__.resolve('model:room'); + Room && Room.findOne(user.login); } } + // We don't join extra rooms with BTTV. if ( this.has_bttv ) return; @@ -974,13 +1199,39 @@ FFZ.prototype.connect_extra_chat = function() { if ( ! this._chatv ) return; - if ( ! this.has_bttv && this.settings.group_tabs ) + + // Rebuild the chat UI. + if ( this.settings.group_tabs ) this._chatv.ffzRebuildTabs(); this._chatv.ffzRebuildMenu(); } +FFZ.prototype.disconnect_extra_chat = function() { + var Chat = App.__container__.lookup('controller:chat'), + current_channel_id = Chat && Chat.get('currentChannelRoom.id'), + current_id = Chat && Chat.get('currentRoom.id'), + user = this.get_user(); + + if ( ! Chat ) + return; + + for(var i=0; i < this.settings.pinned_rooms.length; i++) { + var room_id = this.settings.pinned_rooms[i]; + if ( room_id === current_channel_id || (user && room_id === user.login) ) + continue; + + if ( this.rooms[room_id] && this.rooms[room_id].room ) { + if ( current_id === room_id ) + Chat.blurRoom(); + + this.rooms[room_id].room.destroy(); + } + } +} + + FFZ.prototype._join_room = function(room_id, no_rebuild) { var did_join = false; if ( this.settings.pinned_rooms.indexOf(room_id) === -1 ) { @@ -989,23 +1240,22 @@ FFZ.prototype._join_room = function(room_id, no_rebuild) { did_join = true; } + // Make sure we're not already there. - if ( this.rooms[room_id] && this.rooms[room_id].room ) { - if ( did_join && ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) - this._chatv.ffzRebuildTabs(); - return did_join; + if ( ! this.rooms[room_id] || ! this.rooms[room_id].room ) { + // Okay, fine. Get it. + var Room = App.__container__.resolve('model:room'); + Room && Room.findOne(room_id); } - // Okay, fine. Get it. - var Room = App.__container__.resolve('model:room'), - r = Room && Room.findOne(room_id); - // Finally, rebuild the chat UI. - if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) - this._chatv.ffzRebuildTabs(); + // Rebuild the chat UI. + if ( ! no_rebuild && ! this.has_bttv && this._chatv ) { + if ( this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); - if ( ! no_rebuild && this._chatv ) this._chatv.ffzRebuildMenu(); + } return did_join; } @@ -1036,11 +1286,14 @@ FFZ.prototype._leave_room = function(room_id, no_rebuild) { if ( ! user || user.login !== room_id ) r.destroy(); - if ( ! no_rebuild && ! this.has_bttv && this._chatv && this.settings.group_tabs ) - this._chatv.ffzRebuildTabs(); - if ( ! no_rebuild && this._chatv ) + // Rebuild the chat UI. + if ( ! no_rebuild && ! this.has_bttv && this._chatv ) { + if ( this.settings.group_tabs ) + this._chatv.ffzRebuildTabs(); + this._chatv.ffzRebuildMenu(); + } return did_leave; } @@ -1051,6 +1304,9 @@ FFZ.prototype._leave_room = function(room_id, no_rebuild) { // ---------------------- FFZ.chat_commands.join = function(room, args) { + if ( this.has_bttv ) + return "Pinned Rooms are not available with BetterTTV installed."; + if ( ! args || ! args.length || args.length > 1 ) return "Join Usage: /join "; @@ -1066,6 +1322,9 @@ FFZ.chat_commands.join = function(room, args) { FFZ.chat_commands.part = function(room, args) { + if ( this.has_bttv ) + return "Pinned Rooms are not available with BetterTTV installed."; + if ( ! args || ! args.length || args.length > 1 ) return "Part Usage: /part "; diff --git a/src/ember/conversations.js b/src/ember/conversations.js index 5345189d..52f3c243 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -58,8 +58,7 @@ FFZ.prototype.setup_conversations = function() { FFZ.prototype._modify_conversation_window = function(component) { var f = this, - Layout = App.__container__.lookup('controller:layout'), - Settings = App.__container__.lookup('controller:settings'); + Layout = App.__container__.lookup('controller:layout'); component.reopen({ headerBadges: Ember.computed("thread.participants", "currentUsername", function() { @@ -106,8 +105,7 @@ FFZ.prototype._modify_conversation_window = function(component) { FFZ.prototype._modify_conversation_line = function(component) { var f = this, - Layout = App.__container__.lookup('controller:layout'), - Settings = App.__container__.lookup('controller:settings'); + Layout = App.__container__.lookup('controller:layout'); component.reopen({ tokenizedMessage: function() { diff --git a/src/ember/directory.js b/src/ember/directory.js index f107ed32..dc3ed711 100644 --- a/src/ember/directory.js +++ b/src/ember/directory.js @@ -149,9 +149,11 @@ 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); - this.log("Hooking the Ember games-following controller."); - var GamesFollowing = App.__container__.lookup('controller:games-following'); + var GamesFollowing = App.__container__.lookup('controller:games-following'), + f = this; + if ( GamesFollowing ) { + this.log("Hooking the Ember games-following controller."); GamesFollowing.reopen({ ffz_sidebar_games: this.settings.sidebar_followed_games, @@ -164,7 +166,8 @@ FFZ.prototype.setup_directory = function() { }); Ember.propertyDidChange(GamesFollowing, 'sidePanelFollowing'); - } + } else + this.error("Unable to load the Ember games-following controller."); this.log("Attempting to modify the Following collection."); @@ -189,8 +192,9 @@ FFZ.prototype.setup_directory = function() { this._modify_directory_host(HostView); // Initialize existing views. - for(var key in Ember.View.views) { - var view = Ember.View.views[key]; + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + var view = views[key]; try { if ( (ChannelView && view instanceof ChannelView) || (CreativeChannel && view instanceof CreativeChannel) || (CSGOChannel && view instanceof CSGOChannel) || (HostView && view instanceof HostView) ) view.ffzInit(); @@ -469,10 +473,20 @@ FFZ.prototype._modify_directory_host = function(dir) { for(var i=0; i < hosts.length; i++) make_link(hosts[i]); - f.show_popup(menu, [e.clientX - 60, e.clientY - 60], document.querySelector('#main_col > .tse-scroll-content > .tse-content')); + var cont = document.querySelector('#main_col > .tse-scroll-content > .tse-content'), + bounds = cont && cont.getBoundingClientRect(), + + x = e.clientX - 60, + y = e.clientY - 60; + + if ( bounds ) + x = Math.max(bounds.left, Math.min(x, (bounds.left + bounds.width) - 302)); + + f.show_popup(menu, [x, y], document.querySelector('#main_col > .tse-scroll-content > .tse-content')); }, ffzCleanup: function() { + var target = this.get('context.model.target.channel'); if ( f._popup && f._popup.classList.contains('ffz-channel-selector') && f._popup.getAttribute('data-channel') === target ) f.close_popup(); }, @@ -485,7 +499,41 @@ FFZ.prototype._modify_directory_host = function(dir) { title = meta && meta.querySelector('.title a'), target = this.get('context.model.target.channel'), - hosts = this.get('context.model.ffz_hosts'); + hosts = this.get('context.model.ffz_hosts'), + + boxart = thumb && thumb.querySelector('.boxart'); + + + // Fix the game not showing + if ( ! boxart && thumb && this.get('context.model.game') ) { + var img = document.createElement('img'), + game = this.get("context.model.game"), + c = this.get('controller'); + + boxart = document.createElement('a'); + boxart.className = 'boxart'; + boxart.href = this.get("context.model.gameUrl"); + boxart.setAttribute('original-title', game); + + boxart.addEventListener('click', function(e) { + e.preventDefault(); + jQuery('.tipsy').remove(); + + if ( game === "Counter-Strike: Global Offensive" ) + c.transitionTo('csgo.channels.index') + else if ( game === "Creative" ) + c.transitionTo('creative.channels.index'); + else + c.transitionTo('gameDirectory.index', encodeURIComponent(game)); + + return false; + }); + + img.src = this.get("context.model.gameBoxart"); + boxart.appendChild(img); + thumb.appendChild(boxart); + } + if ( f.settings.directory_logos ) { el.classList.add('ffz-directory-logo'); diff --git a/src/ember/following.js b/src/ember/following.js index a57b292a..321f2a76 100644 --- a/src/ember/following.js +++ b/src/ember/following.js @@ -71,7 +71,7 @@ FFZ.prototype.setup_profile_following = function() { ffzInit: function() { // Only process our own profile following page. var user = f.get_user(); - if ( ! f.settings.enhance_profile_following || ! user || ! user.login === this.get('context.id') ) + if ( ! f.settings.enhance_profile_following || ! user || user.login !== this.get('context.id') ) return; var el = this.get('element'), @@ -259,8 +259,9 @@ FFZ.prototype.setup_profile_following = function() { ProfileView.create().destroy(); } catch(err) { } - for(var key in Ember.View.views) { - var view = Ember.View.views[key]; + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + var view = views[key]; if ( ! view || !(view instanceof ProfileView) ) continue; diff --git a/src/ember/line.js b/src/ember/line.js index 0babc0aa..feba7407 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -671,7 +671,7 @@ FFZ.prototype._modify_line = function(component) { raw_color = this.get('msgObject.color'), colors = raw_color && f._handle_color(raw_color), - is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); e.push('
'); diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index a66234b7..a5373bcc 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -399,7 +399,10 @@ FFZ.prototype.setup_mod_card = function() { Card.reopen({ ffzForceRedraw: function() { this.rerender(); - }.observes("cardInfo.isModeratorOrHigher", "cardInfo.user"), + if ( f.settings.mod_card_history ) + this.ffzRenderHistory(); + + }.observes("cardInfo.isModeratorOrHigher", "cardInfo.user.id"), ffzRebuildInfo: function() { var el = this.get('element'), @@ -730,74 +733,26 @@ FFZ.prototype.setup_mod_card = function() { // Message History - if ( f.settings.mod_card_history ) { - var Chat = App.__container__.lookup('controller:chat'), - room = Chat && Chat.get('currentRoom'), - delete_links = room && room.get('roomProperties.hide_chat_links'), - tmiSession = room.tmiSession || (window.TMI && TMI._sessions && TMI._sessions[0]), - room_id = room.get('id'), - user_id = controller.get('cardInfo.user.id'), - ffz_room = room && f.rooms && f.rooms[room_id], - user_history = ffz_room && ffz_room.user_history && ffz_room.user_history[user_id] || [], - - history = document.createElement('ul'); - - history.className = 'interface clearfix chat-history'; - - if ( user_history.length < 50 ) { - var before = (user_history.length > 0 ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0); - f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) { - if ( ! success ) - return; - - f.parse_history(data, null, room_id, delete_links, tmiSession); - - var i = data.length, - was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight), - first = true; - - while(i--) { - var msg = data[i]; - if ( ! msg ) - continue; - - msg.from_server = true; - - if ( ! msg.date || msg.date.getTime() >= before ) - continue; - - if ( first ) { - first = false; - history.insertBefore(f._build_mod_card_history({ - date: msg.date, - from: "jtv", - style: "admin", - cachedTokens: ["(Server History Above)"] - }), history.firstElementChild); - } - - history.insertBefore(f._build_mod_card_history(msg, t), history.firstElementChild); - } - - if ( was_at_top ) - setTimeout(function() { history.scrollTop = history.scrollHeight; }); - }); - } - - for(var i=0; i < user_history.length; i++) - history.appendChild(f._build_mod_card_history(user_history[i], t)); - - el.appendChild(history); - - // Lazy scroll-to-bottom - history.scrollTop = history.scrollHeight; - } + if ( f.settings.mod_card_history ) + this.ffzRenderHistory(); // Reposition the menu if it's off-screen. var el_bound = el.getBoundingClientRect(), - body_bound = document.body.getBoundingClientRect(); + body_bound = document.body.getBoundingClientRect(), - if ( el_bound.bottom > body_bound.bottom ) { + renderBottom = this.get('cardInfo.renderBottom'), + renderRight = this.get('cardInfo.renderRight'); + + if ( renderRight ) { + var offset = (el_bound.left + el_bound.width) - renderRight; + el.style.left = (el_bound.left - offset) + "px"; + } + + if ( renderBottom ) { + var offset = el_bound.bottom - renderBottom; + el.style.top = (el_bound.top - offset) + "px"; + + } else if ( el_bound.bottom > body_bound.bottom ) { var offset = el_bound.bottom - body_bound.bottom; if ( el_bound.top - offset > body_bound.top ) el.style.top = (el_bound.top - offset) + "px"; @@ -818,9 +773,78 @@ FFZ.prototype.setup_mod_card = function() { } }, + ffzRenderHistory: function() { + var t = this, + Chat = App.__container__.lookup('controller:chat'), + room = Chat && Chat.get('currentRoom'), + delete_links = room && room.get('roomProperties.hide_chat_links'), + tmiSession = room.tmiSession || (window.TMI && TMI._sessions && TMI._sessions[0]), + room_id = room.get('id'), + user_id = this.get('cardInfo.user.id'), + ffz_room = room && f.rooms && f.rooms[room_id], + user_history = ffz_room && ffz_room.user_history && ffz_room.user_history[user_id] || [], + el = this.get('element'), + + history = el && el.querySelector('.chat-history'); + + if ( ! history ) { + history = document.createElement('ul'); + history.className = 'interface clearfix chat-history'; + el.appendChild(history); + } else { + history.classList.remove('loading'); + history.innerHTML = ''; + } + + if ( user_history.length < 50 ) { + var before = (user_history.length > 0 ? user_history[0].date.getTime() : Date.now()) - (f._ws_server_offset || 0); + f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) { + if ( ! success ) + return; + + f.parse_history(data, null, room_id, delete_links, tmiSession); + + var i = data.length, + was_at_top = history && history.scrollTop >= (history.scrollHeight - history.clientHeight), + first = true; + + while(i--) { + var msg = data[i]; + if ( ! msg ) + continue; + + msg.from_server = true; + + if ( ! msg.date || msg.date.getTime() >= before ) + continue; + + if ( first ) { + first = false; + history.insertBefore(f._build_mod_card_history({ + date: msg.date, + from: "jtv", + style: "admin", + cachedTokens: ["(Server History Above)"] + }), history.firstElementChild); + } + + history.insertBefore(f._build_mod_card_history(msg, t), history.firstElementChild); + } + + if ( was_at_top ) + setTimeout(function() { history.scrollTop = history.scrollHeight; }); + }); + } + + for(var i=0; i < user_history.length; i++) + history.appendChild(f._build_mod_card_history(user_history[i], t)); + + // Lazy scroll-to-bottom + history.scrollTop = history.scrollHeight; + }, + ffzAdjacentHistory: function(line) { var Chat = App.__container__.lookup('controller:chat'), - controller = this.get('controller'), t = this, user_id = this.get('cardInfo.user.id'), @@ -835,23 +859,31 @@ FFZ.prototype.setup_mod_card = function() { history = el && el.querySelector('.chat-history'), logs = el && el.querySelector('.chat-history.adjacent-history'), - when = line.date.getTime(); + when = line.date.getTime(), + scroll_top = logs && logs.scrollTop || history && history.scrollTop || 0; if ( ! history ) return; - if ( logs ) + if ( logs ) { logs.classList.add('loading'); - else + logs.scrollTop = 0; + } else { history.classList.add('loading'); + history.scrollTop = 0; + } if ( ! f.ws_send("adjacent_history", [room_id, when, 2], function(success, data) { - if ( logs ) + var was_loading = history.classList.contains('loading'); + if ( logs ) { logs.classList.remove('loading'); - else + logs.scrollTop = scroll_top; + } else { history.classList.remove('loading'); + history.scrollTop = scroll_top; + } - if ( ! success || ! data || ! data.length ) + if ( ! success || ! data || ! data.length || ! was_loading ) return; var had_logs = false, @@ -908,10 +940,13 @@ FFZ.prototype.setup_mod_card = function() { }); }) ) - if ( logs ) + if ( logs ) { logs.classList.remove('loading'); - else + logs.scrollTop = scroll_top; + } else { history.classList.remove('loading'); + history.scrollTop = scroll_top; + } } }); } @@ -928,6 +963,9 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { out.push('' + helpers.getTime(msg.date) + ''); + var alias = this.aliases[msg.from], + name = (msg.tags && msg.tags['display-name']) || (msg.from && msg.from.capitalize()) || "unknown user"; + if ( show_from ) { // Badges out.push(''); @@ -942,13 +980,11 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) { Layout = App.__container__.lookup('controller:layout'), Settings = App.__container__.lookup('controller:settings'), - is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')); + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')); // Aliases and Styling - var alias = this.aliases[msg.from], - name = (msg.tags && msg.tags['display-name']) || (msg.from && msg.from.capitalize()) || "unknown user", - style = colors && 'color:' + (is_dark ? colors[1] : colors[0]), + var style = colors && 'color:' + (is_dark ? colors[1] : colors[0]), colored = style ? ' has-color' : ''; diff --git a/src/ember/player.js b/src/ember/player.js index 245dadaf..cd3b5823 100644 --- a/src/ember/player.js +++ b/src/ember/player.js @@ -72,11 +72,12 @@ FFZ.prototype.setup_player = function() { if ( ! window.Ember ) return; - for(var key in Ember.View.views) { - if ( ! Ember.View.views.hasOwnProperty(key) ) + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + if ( ! views.hasOwnProperty(key) ) continue; - var view = Ember.View.views[key]; + var view = views[key]; if ( !(view instanceof Player2) ) continue; diff --git a/src/ember/room.js b/src/ember/room.js index 92986ffc..0aa6ba41 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -1,7 +1,4 @@ var FFZ = window.FrankerFaceZ, - CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg, - MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/, - GROUP_CHAT = /^_([^_]+)_\d+$/, HOSTED_SUB = / subscribed to /, constants = require('../constants'), utils = require('../utils'), @@ -58,6 +55,35 @@ FFZ.prototype.setup_room = function() { this.get("model.tmiRoom").sendMessage("/timeout " + e.user + " 1"); this.get("model").clearMessages(e.user); } + + RC._actions.showModOverlay = function(e) { + var Channel = App.__container__.resolve('model:channel'); + if ( ! Channel ) + return; + + var chan = Channel.find({id: e.sender}); + + // 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. + if ( ! chan.get('isLoaded') ) + chan.load(); + + this.set("showModerationCard", true); + + // We pass in renderBottom and renderRight, which we use to reposition the window + // after we know how big it actually is. + this.set("moderationCardInfo", { + user: chan, + renderTop: e.top, + renderLeft: e.left, + renderBottom: e.bottom, + renderRight: e.right, + isIgnored: this.get("tmiSession").isIgnored(e.sender), + isChannelOwner: this.get("controllers.login.userData.login") === e.sender, + profileHref: Twitch.uri.profile(e.sender), + isModeratorOrHigher: this.get("model.isModeratorOrHigher") + }); + } } this.log("Hooking the Ember Room model."); @@ -90,11 +116,12 @@ FFZ.prototype.setup_room = function() { } catch(err) { } // Modify all existing Room views. - for(var key in Ember.View.views) { - if ( ! Ember.View.views.hasOwnProperty(key) ) + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + if ( ! views.hasOwnProperty(key) ) continue; - var view = Ember.View.views[key]; + var view = views[key]; if ( !(view instanceof RoomView) ) continue; @@ -834,7 +861,7 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) { FFZ.prototype.load_room = function(room_id, callback, tries) { var f = this; - jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/room/" + room_id) + jQuery.getJSON(constants.API_SERVER + "v1/room/" + room_id) .done(function(data) { if ( data.sets ) { for(var key in data.sets) @@ -935,6 +962,28 @@ FFZ.prototype._modify_room = function(room) { } }, + ffzScheduleDestroy: function() { + if ( this._ffz_destroy_timer ) + return; + + var t = this; + this._ffz_destroy_timer = setTimeout(function() { + t._ffz_destroy_timer = null; + t.ffzCheckDestroy(); + }, 5000); + }, + + ffzCheckDestroy: function() { + var Chat = App.__container__.lookup('controller:chat'), + user = f.get_user(), + room_id = this.get('id'); + + if ( (Chat && Chat.get('currentChannelRoom') === this) || (user && user.login === room_id) || (f._chatv && f._chatv._ffz_host === room_id) || (f.settings.pinned_rooms && f.settings.pinned_rooms.indexOf(room_id) !== -1) ) + return; + + this.destroy(); + }, + ffzUpdateStatus: function() { if ( f._roomv ) f._roomv.ffzUpdateStatus(); @@ -1089,7 +1138,14 @@ FFZ.prototype._modify_room = function(room) { this.get("messages").pushObject(msg); this.trimMessages(); - "admin" === msg.style || ("whisper" === msg.style && ! this.ffz_whisper_room ) || this.incrementProperty("unreadCount", 1); + if ( msg.style !== "admin" && msg.style !== "whisper" ) { + if ( msg.ffz_has_mention ) { + this.ffz_last_mention = Date.now(); + } + + this.ffz_last_activity = Date.now(); + this.incrementProperty("unreadCount", 1); + } } }, @@ -1102,9 +1158,6 @@ FFZ.prototype._modify_room = function(room) { if ( this._ffz_pending_flush ) return; - /*if ( this._ffz_pending_flush ) - clearTimeout(this._ffz_pending_flush);*/ - if ( this.ffzPending && this.ffzPending.length ) { // We need either the amount of chat delay past the first message, if chat_delay is on, or the // amount of time from the last batch. @@ -1258,10 +1311,9 @@ FFZ.prototype._modify_room = function(room) { }, send: function(text, ignore_history) { - if ( f.settings.group_tabs && f.settings.whisper_room && this.ffz_whisper_room ) - return; - try { + this.ffz_last_input = Date.now(); + if ( text && ! ignore_history ) { // Command History var mru = this.get('mru_list'), @@ -1294,16 +1346,13 @@ FFZ.prototype._modify_room = function(room) { }, ffzUpdateUnread: function() { - if ( f.settings.group_tabs ) { - var Chat = App.__container__.lookup('controller:chat'); - if ( Chat && Chat.get('currentRoom') === this ) - this.resetUnreadCount(); - else if ( f._chatv ) - f._chatv.ffzTabUnread(this.get('id')); - } + var Chat = App.__container__.lookup('controller:chat'); + if ( Chat && Chat.get('currentRoom') === this ) + this.resetUnreadCount(); + else if ( f._chatv ) + f._chatv.ffzUpdateUnread(this.get('id')); }.observes('unreadCount'), - ffzInitChatterCount: function() { if ( ! this.tmiRoom ) return; diff --git a/src/ember/viewers.js b/src/ember/viewers.js index 2aab88c0..b34283dc 100644 --- a/src/ember/viewers.js +++ b/src/ember/viewers.js @@ -21,12 +21,28 @@ FFZ.settings_info.sort_viewers = { FFZ.prototype.setup_viewers = function() { this.log("Hooking the Ember Viewers controller."); - var Viewers = App.__container__.resolve('controller:viewers'); - this._modify_viewers(Viewers); + if ( Viewers ) + this._modify_viewers(Viewers); + + /* Disable for now because Twitch reverted this change + this.log("Hooking the Ember Viewers view."); + var ViewerView = App.__container__.resolve('view:viewers'); + if ( ViewerView ) + this._modify_viewer_view(ViewerView);*/ } +/*FFZ.prototype._modify_viewer_view = function(view) { + view.reopen({ + setListDimensions: function(e) { + // Don't set the stupid scroll thing. Don't use the stupid height thing. + this.$(".js-chatters-container").width(e.width).height(e.height); + } + }); +}*/ + + FFZ.prototype._modify_viewers = function(controller) { var f = this; @@ -55,7 +71,7 @@ FFZ.prototype._modify_viewers = function(controller) { // If the current room isn't the channel's chat, then we shouldn't // display them as the broadcaster. - if ( room_id != broadcaster ) + if ( room_id !== broadcaster ) broadcaster = null; // Now, break the viewer array down into something we can use. diff --git a/src/emoticons.js b/src/emoticons.js index 34bcea8a..ea19e83e 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -372,7 +372,7 @@ FFZ.prototype.load_emoji_data = function(callback, tries) { FFZ.prototype.load_global_sets = function(callback, tries) { var f = this; - jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/set/global") + jQuery.getJSON(constants.API_SERVER + "v1/set/global") .done(function(data) { f.default_sets = data.default_sets; var gs = f.global_sets = [], @@ -409,7 +409,7 @@ FFZ.prototype.load_global_sets = function(callback, tries) { FFZ.prototype.load_set = function(set_id, callback, tries) { var f = this; - jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/set/" + set_id) + jQuery.getJSON(constants.API_SERVER + "v1/set/" + set_id) .done(function(data) { f._load_set_json(set_id, callback, data && data.set); diff --git a/src/ext/api.js b/src/ext/api.js index 83cfb267..28280973 100644 --- a/src/ext/api.js +++ b/src/ext/api.js @@ -86,14 +86,14 @@ API.prototype.log = function(msg, data, to_json, log_json) { API.prototype._load_set = function(real_id, set_id, data) { if ( ! data ) - return false; + return null; // Check for an existing set to copy the users. var users = []; if ( this.emote_sets[real_id] && this.emote_sets[real_id].users ) users = this.emote_sets[real_id].users; - var set = { + var emote_set = { source: this.name, source_ext: this.id, source_id: set_id, @@ -108,14 +108,14 @@ API.prototype._load_set = function(real_id, set_id, data) { title: data.title || "Global Emoticons", }; - this.emote_sets[real_id] = set; + this.emote_sets[real_id] = emote_set; if ( this.ffz.emote_sets ) - this.ffz.emote_sets[real_id] = set; + this.ffz.emote_sets[real_id] = emote_set; var output_css = "", ems = data.emoticons, - emoticons = set.emoticons; + emoticons = emote_set.emoticons; for(var i=0; i < ems.length; i++) { var emote = ems[i], @@ -158,15 +158,17 @@ API.prototype._load_set = function(real_id, set_id, data) { new_emote.regex = emote.regex; else if ( typeof emote.name !== "string" ) new_emote.regex = emote.name; + else if ( emote_set.require_spaces || emote.require_spaces ) + new_emote.regex = new RegExp("(^| )(" + utils.escape_regex(emote.name) + ")(?= |$)", "g"); else new_emote.regex = new RegExp("(^|\\W|\\b)(" + utils.escape_regex(emote.name) + ")(?=\\W|$)", "g"); output_css += build_css(new_emote); - set.count++; + emote_set.count++; emoticons[id] = new_emote; } - utils.update_css(this.ffz._emote_style, real_id, output_css + (set.css || "")); + utils.update_css(this.ffz._emote_style, real_id, output_css + (emote_set.css || "")); if ( this.ffz._cindex ) this.ffz._cindex.ffzFixTitle(); @@ -175,7 +177,7 @@ API.prototype._load_set = function(real_id, set_id, data) { this.ffz.update_ui_link(); } catch(err) { } - return set; + return emote_set; } @@ -223,7 +225,7 @@ API.prototype.unload_set = function(id) { if ( ! room ) continue; - ind = room.ext_sets.indexOf(exact_id); + var ind = room.ext_sets.indexOf(exact_id); if ( ind !== -1 ) room.ext_sets.splice(ind,1); } @@ -232,7 +234,7 @@ API.prototype.unload_set = function(id) { } - return set; + return emote_set; } diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index 8a421127..1effcf1c 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -43,10 +43,16 @@ FFZ.prototype.setup_bttv = function(delay) { } // Disable Chat Tabs - if ( this.settings.group_tabs && this._chatv ) { - this._chatv.ffzDisableTabs(); + if ( this._chatv ) { + if ( this.settings.group_tabs ) + this._chatv.ffzDisableTabs(); + + this._chatv.ffzTeardownMenu(); + this._chatv.ffzUnloadHost(); } + this.disconnect_extra_chat(); + if ( this._roomv ) { // Disable Chat Pause if ( this.settings.chat_hover_pause ) diff --git a/src/ext/rechat.js b/src/ext/rechat.js index e06d006c..2bc50944 100644 --- a/src/ext/rechat.js +++ b/src/ext/rechat.js @@ -47,6 +47,26 @@ FFZ.prototype.setup_rechat = function() { 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 model = window.App ? App.__container__.lookup('controller: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; @@ -128,7 +148,7 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { Layout = App.__container__.lookup('controller:layout'), Settings = App.__container__.lookup('controller:settings'), - is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')), + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')), badges_el = line.querySelector('.badges'), from_el = line.querySelector('.from'), @@ -196,7 +216,7 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) { if ( ! message_el ) return; - if ( ! reprocess && message_el.style.color ) { + if ( ! reprocess && colors && message_el.style.color ) { message_el.classList.add('has-color'); message_el.style.color = is_dark ? colors[1] : colors[0]; } diff --git a/src/main.js b/src/main.js index 8377a227..60999bed 100644 --- a/src/main.js +++ b/src/main.js @@ -22,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 83, + major: 3, minor: 5, revision: 100, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -333,10 +333,18 @@ FFZ.prototype.init_ember = function(delay) { this.users = {}; this.is_dashboard = false; + try { this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname); } catch(err) { this.embed_in_dash = false; } + + // Make an alias so they STOP RENAMING THIS ON ME + var Settings = App.__container__.lookup('controller:settings'); + if ( Settings && Settings.get('settings') === undefined ) + Settings.reopen({settings: Ember.computed.alias('model')}); + + // Initialize all the modules. this.load_settings(); diff --git a/src/socket.js b/src/socket.js index e3266130..26cb2079 100644 --- a/src/socket.js +++ b/src/socket.js @@ -357,7 +357,7 @@ FFZ.prototype.ws_ping = function() { return; this._ws_ping_time = window.performance ? performance.now() : Date.now(); - if ( ! this.ws_send("ping", null, this._ws_on_pong.bind(this)) ) + if ( ! this.ws_send("ping", undefined, this._ws_on_pong.bind(this)) ) this._ws_ping_time = null; } diff --git a/src/tokenize.js b/src/tokenize.js index bd7866ea..b45acebb 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -449,11 +449,21 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del // We have a mention! msgObject.ffz_has_mention = true; - // If we have chat tabs, update the status. - if ( room_id && ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) { - var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]'); - if ( el && ! el.classList.contains('active') ) - el.classList.add('tab-mentioned'); + // If we have chat tabs/rows, update the status. + if ( room_id && ! this.has_bttv && this._chatv ) { + var room = this.rooms[room_id] && this.rooms[room_id].room; + if ( room._ffz_tab && ! room._ffz_tab.classList.contains('active') ) { + room._ffz_tab.classList.add('tab-mentioned'); + var was_hidden = room._ffz_tab.classList.contains('hidden'); + + if ( was_hidden ) { + room._ffz_tab.classList.remove('hidden'); + this._chatv.$('.chat-room').css('top', this._chatv._ffz_tabs.offsetHeight + "px"); + } + } + + if ( room._ffz_row && ! room._ffz_row.classList.contains('active') ) + room._ffz_row.classList.add('row-mentioned'); } // Display notifications if that setting is enabled. Also make sure @@ -601,7 +611,7 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { } //var mirror_url = utils.quote_attr(constants.EMOTE_MIRROR_BASE + id + '.png'); - extra = ' data-emote="' + id + '"'; // onerror="FrankerFaceZ._emote_mirror_swap(this)"'; // Disable error checking for now. + extra = ' data-emote="' + id + '" onerror="FrankerFaceZ._emote_mirror_swap(this)"'; // Disable error checking for now. if ( ! constants.EMOTE_REPLACEMENTS[id] ) srcset = utils.build_srcset(id); diff --git a/src/ui/dark.js b/src/ui/dark.js index 8992999a..9d4dcf1f 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -136,14 +136,15 @@ FFZ.settings_info.dark_twitch = { document.body.classList.toggle("ffz-dark", val); - var model = window.App ? App.__container__.lookup('controller:settings').get('model') : undefined; + var Settings = window.App && App.__container__.lookup('controller:settings'), + settings = Settings.get('settings'); if ( val ) { this._load_dark_css(); - model && this.settings.set('twitch_chat_dark', model.get('darkMode')); - model && model.set('darkMode', true); + settings && this.settings.set('twitch_chat_dark', settings.get('darkMode')); + settings && settings.set('darkMode', true); } else - model && model.set('darkMode', this.settings.twitch_chat_dark); + settings && settings.set('darkMode', this.settings.twitch_chat_dark); // Try coloring ReChat jQuery('.rechat-chat-line').parents('.chat-container').toggleClass('dark', val || this.settings.twitch_chat_dark); @@ -199,7 +200,16 @@ FFZ.prototype.setup_dark = function() { if ( ! this.settings.dark_twitch ) return; - window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true); + var Settings = window.App && App.__container__.lookup('controller:settings'); + if ( Settings ) { + try { + Settings.set('settings.darkMode', true); + } catch(err) { + this.error("Unable to set the darkMode setting because it isn't named what we expect. WTF?"); + } + } else + this.error("Unable to load the Ember settings controller."); + this._load_dark_css(); } @@ -214,6 +224,6 @@ FFZ.prototype._load_dark_css = function() { s.id = "ffz-dark-css"; s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.DIRECT_SERVER + "script/dark" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); + s.setAttribute('href', constants.SERVER + "script/dark" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); } \ No newline at end of file diff --git a/src/ui/group_chat.js b/src/ui/group_chat.js deleted file mode 100644 index 269fb491..00000000 --- a/src/ui/group_chat.js +++ /dev/null @@ -1,47 +0,0 @@ -var FFZ = window.FrankerFaceZ, - constants = require('../constants'); - - -// -------------------- -// Configuration -// -------------------- - -FFZ.settings_info.group_tabs = { - type: "boolean", - value: false, - - no_bttv: true, - - category: "Chat", - name: "Chat Room Tabs Beta", - help: "Enhanced UI for switching the current chat room and noticing new messages.", - - on_update: function(val) { - var enabled = !this.has_bttv && val; - if ( ! this._chatv || enabled === this._group_tabs_state ) - return; - - if ( enabled ) - this._chatv.ffzEnableTabs(); - else - this._chatv.ffzDisableTabs(); - } -} - - -// -------------------- -// Initializer -// -------------------- - -FFZ.prototype.setup_group_chat = function() { - if ( this.has_bttv || ! this.settings.group_tabs ) - return; - - this.log("Initializing secondary group chat UI."); - //this.group_tabs_enable(); -} - - -// -------------------- -// -// -------------------- \ No newline at end of file diff --git a/src/ui/menu.js b/src/ui/menu.js index ce29afdd..ef2a6dcc 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -126,7 +126,7 @@ FFZ.prototype.setup_menu = function() { content.appendChild(p); a.addEventListener('click', function(e) { - view.set('controller.model.hidden', true); + view.set('controller.settings.hidden', true); f._last_page = 'settings'; f.build_ui_popup(f._chatv); e.stopPropagation(); @@ -164,11 +164,12 @@ FFZ.prototype.setup_menu = function() { } catch(err) { } // Modify all existing Chat Settings views. - for(var key in Ember.View.views) { - if ( ! Ember.View.views.hasOwnProperty(key) ) + var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views; + for(var key in views) { + if ( ! views.hasOwnProperty(key) ) continue; - var view = Ember.View.views[key]; + var view = views[key]; if ( !(view instanceof Settings) ) continue; diff --git a/src/ui/styles.js b/src/ui/styles.js index 84bf28c5..095e48aa 100644 --- a/src/ui/styles.js +++ b/src/ui/styles.js @@ -11,7 +11,7 @@ FFZ.prototype.setup_css = function() { var s = this._main_style = document.createElement('link'); s.id = "ffz-main-css"; s.setAttribute('rel', 'stylesheet'); - s.setAttribute('href', constants.DIRECT_SERVER + "script/style" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); + s.setAttribute('href', constants.SERVER + "script/style" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info)); document.head.appendChild(s); this.log("Readying toggleable styles."); diff --git a/style.css b/style.css index 0c9514b4..441a297c 100644 --- a/style.css +++ b/style.css @@ -955,6 +955,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } /* Menu Scrollbar */ +.chatters-container::-webkit-scrollbar, .ffz-scrollbar::-webkit-scrollbar, .ember-chat .chat-settings::-webkit-scrollbar, .conversations-list .conversations-list-inner::-webkit-scrollbar, @@ -967,6 +968,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } width: 6px; } +.chatters-container::-webkit-scrollbar-thumb, .ffz-scrollbar::-webkit-scrollbar-thumb, .ember-chat .chat-settings::-webkit-scrollbar-thumb, .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb, @@ -987,6 +989,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } .ffz-dark .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb, .ffz-dark .conversation-input-bar .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb, +.theatre .chatters-container::-webkit-scrollbar-thumb, .theatre .ffz-scrollbar::-webkit-scrollbar-thumb, .theatre .ember-chat .chat-settings::-webkit-scrollbar-thumb, .theatre .conversation-window .conversation-content::-webkit-scrollbar-thumb, @@ -995,8 +998,9 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } .theatre .chat-history::-webkit-scrollbar-thumb, .theatre .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb, .theatre .ffz-ui-menu-page::-webkit-scrollbar-thumb, -.theatre .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb +.theatre .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb, +.dark .chatters-container::-webkit-scrollbar-thumb, .dark .ffz-scrollbar::-webkit-scrollbar-thumb, .dark .ember-chat .chat-settings::-webkit-scrollbar-thumb, .dark .chat-history::-webkit-scrollbar-thumb, @@ -1004,6 +1008,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } .dark .ffz-ui-menu-page::-webkit-scrollbar-thumb, .dark .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb, +.force-dark .chatters-container::-webkit-scrollbar-thumb, .force-dark .ffz-scrollbar::-webkit-scrollbar-thumb, .force-dark .ember-chat .chat-settings::-webkit-scrollbar-thumb, .force-dark .chat-history::-webkit-scrollbar-thumb, @@ -1014,6 +1019,12 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } box-shadow: 0 0 1px 1px rgba(0,0,0,0.25); } + +.chatters-container { + overflow-x: hidden !important; + overflow-y: auto !important; +} + /* Fix Moderation Cards */ img.channel_background[src="null"] { display: none; } @@ -1377,7 +1388,7 @@ a.unsafe-link { } .ffz-room-list td svg { - margin: 5px; + margin: 5px 10px 5px 0; float: left; } @@ -1391,9 +1402,9 @@ a.unsafe-link { .ffz-room-row:focus svg path, .ffz-room-row.active svg path { fill: #fff; } -.ffz-room-row:hover td, -.ffz-room-row:focus td, -.ffz-room-row.active td { +.ffz-room-row:hover, +.ffz-room-row:focus, +.ffz-room-row.active { background-color: #6441A5; color: #fff !important; } @@ -1404,7 +1415,7 @@ th.ffz-row-switch { .ffz-room-row a.leave-chat { float: right; - margin-right: 12px; + padding: 0 7px; } .ffz-row-switch .switch { @@ -1419,6 +1430,8 @@ th.ffz-row-switch { /* Chat Tabs */ +.ember-chat .chat-room { z-index: 5; } + #ffz-group-tabs { padding: 10px 10px 6px; box-shadow: inset 0 -1px 0 0 rgba(0,0,0,0.2); @@ -1525,6 +1538,17 @@ th.ffz-row-switch { margin: 5px 0; } +.ffz-room-row.row-mentioned { + background-color: rgba(128,50,50,0.1); + color: red !important; +} + +.ffz-room-row.row-mentioned:not(.active):hover, +.ffz-room-row.row-mentioned:not(.active):focus { + background-color: #a54141; + color: #fff !important; +} + #ffz-group-tabs .button .notifications { background-color: #d44949; top: 0;