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..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 */ @@ -142,6 +150,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, @@ -247,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; } @@ -902,6 +911,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..f0359d37 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 && msg.labels.indexOf('subscriber') !== -1 ) + badges[10] = {klass: 'subscriber', title: 'Subscriber'} + if ( msg.labels && 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,8 +348,10 @@ 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("' + badge.transparent_image + '");'; + else if ( badge.image ) + style += 'background-image: url("' + badge.image + '");'; if ( badge.color && ! alpha ) style += 'background-color: ' + badge.color + '; '; @@ -236,7 +360,7 @@ FFZ.prototype.bttv_badges = function(data) { 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}]); } @@ -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 // -------------------- @@ -470,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] || {}, @@ -490,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 66f91022..5ed52212 100644 --- a/src/constants.js +++ b/src/constants.js @@ -2,19 +2,23 @@ var SVGPATH = ' 1 ) { this.set('ffz_chatters', value); diff --git a/src/ember/chatview.js b/src/ember/chatview.js index 9cfee7e4..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,16 +490,22 @@ 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 ) 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); }, @@ -435,76 +514,274 @@ 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(); - var room = this.get('controller.currentRoom'), rows; - room && room.resetUnreadCount(); + // Close mod cards when changing to a new room. + if ( f._mod_card ) + f._mod_card.send('close'); - if ( room && room._ffz_was_unread ) { - room._ffz_was_unread = false; + var room = this.get('controller.currentRoom'), + room_id = room && room.get('id'), + was_unread = room_id && this.ffz_unread[room_id], + update_height = 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 ) @@ -526,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); } } @@ -569,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'; @@ -578,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); }); @@ -591,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); @@ -663,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(); @@ -676,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); @@ -685,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. @@ -705,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'); @@ -742,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"; @@ -756,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 + ''; })); @@ -921,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; + } }); } @@ -956,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; @@ -970,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 ) { @@ -985,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; } @@ -1032,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; } @@ -1047,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 "; @@ -1062,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 b9c1ef2a..52f3c243 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -58,72 +58,24 @@ 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() { 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 +95,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')}); } }); @@ -152,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 eba12dd4..dc3ed711 100644 --- a/src/ember/directory.js +++ b/src/ember/directory.js @@ -1,30 +1,179 @@ 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); + + 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, + + 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'); + } else + this.error("Unable to load the Ember games-following controller."); + + + 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 ) @@ -43,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(); @@ -55,6 +205,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 +330,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 +365,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 +389,187 @@ 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]); + + 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(); + }, + + 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'), + + 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'); - - 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..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'), @@ -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(); @@ -257,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 52543528..feba7407 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], @@ -673,12 +671,14 @@ 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('
'); 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..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'), @@ -426,7 +429,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 +462,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'), @@ -728,92 +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'), - 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 < 20 ) { - var before = user_history.length > 0 ? user_history[0].date.getTime() : Date.now(); - f.ws_send("user_history", [room_id, user_id, 50 - user_history.length], function(success, data) { - if ( ! success ) - return; - - 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; - - if ( typeof msg.date === "string" || typeof msg.date === "number" ) - msg.date = utils.parse_date(msg.date); - - 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); - } - - 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; - } - - 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])); - - 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"; @@ -832,29 +771,295 @@ FFZ.prototype.setup_mod_card = function() { f.error("ModerationCardView didInsertElement: " + err); } catch(err) { } } - }}); + }, + + 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'), + 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(), + scroll_top = logs && logs.scrollTop || history && history.scrollTop || 0; + + if ( ! history ) + return; + + if ( logs ) { + logs.classList.add('loading'); + logs.scrollTop = 0; + } else { + history.classList.add('loading'); + history.scrollTop = 0; + } + + if ( ! f.ws_send("adjacent_history", [room_id, when, 2], function(success, data) { + var was_loading = history.classList.contains('loading'); + if ( logs ) { + logs.classList.remove('loading'); + logs.scrollTop = scroll_top; + } else { + history.classList.remove('loading'); + history.scrollTop = scroll_top; + } + + if ( ! success || ! data || ! data.length || ! was_loading ) + 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'); + logs.scrollTop = scroll_top; + } else { + history.classList.remove('loading'); + history.scrollTop = scroll_top; + } + } + }); } -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) + ''); + + + 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(''); + 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('settings.darkMode')); + + + // Aliases and Styling + var 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 +1086,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/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 d045f5c1..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; @@ -713,7 +740,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 +757,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++; } @@ -859,7 +861,7 @@ FFZ.prototype._insert_history = function(room_id, data) { 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) @@ -960,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(); @@ -1100,8 +1124,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); @@ -1115,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); + } } }, @@ -1128,15 +1158,13 @@ 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. + 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 +1191,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 +1212,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 +1240,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 +1253,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); } } @@ -1281,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'), @@ -1317,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/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/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 f024a7f8..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; @@ -122,12 +142,13 @@ 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'), 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'), @@ -173,23 +194,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 ) { @@ -209,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]; } @@ -243,8 +250,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 +287,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..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: 77, + major: 3, minor: 5, revision: 100, 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(); @@ -326,18 +333,29 @@ 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(); // 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..26cb2079 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", undefined, 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..b45acebb 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -443,17 +443,27 @@ 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! 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 @@ -544,7 +554,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], @@ -598,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); @@ -607,7 +620,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 +689,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 +1030,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..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.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/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/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 9a38f083..ef2a6dcc 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; @@ -145,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(); @@ -183,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; @@ -215,14 +197,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 +248,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 +326,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 +487,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..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.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/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..441a297c 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 */ @@ -912,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, @@ -924,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, @@ -944,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, @@ -952,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, @@ -961,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, @@ -971,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; } @@ -1187,6 +1241,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: " "; @@ -1333,7 +1388,7 @@ a.unsafe-link { } .ffz-room-list td svg { - margin: 5px; + margin: 5px 10px 5px 0; float: left; } @@ -1347,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; } @@ -1360,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 { @@ -1375,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); @@ -1481,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; @@ -1608,6 +1676,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 +1944,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 +2201,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 +2413,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