diff --git a/src/badges.js b/src/badges.js index 611275b5..1a1c9f5a 100644 --- a/src/badges.js +++ b/src/badges.js @@ -9,9 +9,9 @@ var FFZ = window.FrankerFaceZ, ], 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 + '"); }'; + var out = ".badges .ffz-badge-" + badge.id + " { background-color: " + badge.color + '; background-image: url("' + badge.image + '"); ' + (badge.css || "") + '}'; + if ( badge.alpha_image ) + out += ".badges .badge.alpha.ffz-badge-" + badge.id + ",.ffz-transparent-badges .badges .ffz-badge-" + badge.id + ' { background-image: url("' + badge.alpha_image + '"); }'; return out; }; @@ -151,8 +151,10 @@ FFZ.prototype.setup_badges = function() { s.id = "ffz-badge-css"; document.head.appendChild(s); - this.log("Adding legacy donor badges."); - this._legacy_add_donors(); + this.log("Loading badges."); + this.load_badges(); + //this.log("Adding legacy donor badges."); + //this._legacy_add_donors(); } @@ -161,8 +163,7 @@ FFZ.prototype.setup_badges = function() { // -------------------- FFZ.ws_commands.reload_badges = function() { - this._legacy_load_bots(); - this._legacy_load_donors(); + this.load_badges(); } @@ -403,13 +404,89 @@ FFZ.prototype.bttv_badges = function(data) { // -------------------- -// Legacy Support +// Badge Loading // -------------------- FFZ.bttv_known_bots = ["nightbot","moobot","sourbot","xanbot","manabot","mtgbot","ackbot","baconrobot","tardisbot","deejbot","valuebot","stahpbot"]; +FFZ.prototype.load_badges = function(callback, tries) { + var f = this; + jQuery.getJSON(constants.API_SERVER + "v1/badges") + .done(function(data) { + var badge_total = 0, + badge_count = 0; -FFZ.prototype._legacy_add_donors = function() { + for(var i=0; i < data.badges.length; i++) { + var badge = data.badges[i]; + if ( badge && badge.name ) { + f._load_badge_json(badge.id, badge); + badge_total++; + } + } + + if ( data.users ) + for(var badge_id in data.users) + if ( data.users.hasOwnProperty(badge_id) && f.badges[badge_id] ) { + var badge = f.badges[badge_id], + users = data.users[badge_id]; + + for(var i=0; i < users.length; i++) { + var user = users[i], + ud = f.users[user] = f.users[user] || {}, + badges = ud.badges = ud.badges || {}; + + badge_count++; + badges[badge.slot] = {id: badge.id}; + } + + f.log('Added "' + badge.name + '" badge to ' + utils.number_commas(users.length) + ' users.'); + } + + // Special Badges + var zw = f.users.zenwan = f.users.zenwan || {}, + badges = zw.badges = zw.badges || {}; + if ( ! badges[1] ) + badge_count++; + badges[1] = {id: 2, image: "//cdn.frankerfacez.com/script/momiglee_badge.png", title: "WAN"}; + + + f.log("Loaded " + utils.number_commas(badge_count) + " total badges across " + badge_total + " types."); + typeof callback === "function" && callback(true); + + }).fail(function(data) { + if ( data.status === 404 ) + return typeof callback === "function" && callback(false); + + tries = (tries || 0) + 1; + if ( tries < 10 ) + return setTimeout(f.load_badges.bind(f, callback, tries), 500 + 500*tries); + + f.error("Unable to load badge data. [HTTP Status " + data.status + "]", data); + typeof callback === "function" && callback(false); + }); +} + + +FFZ.prototype._load_badge_json = function(badge_id, data) { + this.badges[badge_id] = data; + if ( data.replaces ) { + data.replaces_type = data.replaces; + data.replaces = true; + } + + if ( data.name === 'bot' ) + data.visible = function(r,user) { return !(this.has_bttv && FFZ.bttv_known_bots.indexOf(user)!==-1); }; + + utils.update_css(this._badge_style, badge_id, badge_css(data)); +} + + + +// -------------------- +// Legacy Support +// -------------------- + +/*FFZ.prototype._legacy_add_donors = function() { // Developer Badge this.badges[0] = {id: 0, title: "FFZ Developer", color: "#FAAF19", image: "//cdn.frankerfacez.com/script/devicon.png", transparent_image: "//cdn.frankerfacez.com/script/devtransicon.png"}; utils.update_css(this._badge_style, 0, badge_css(this.badges[0])); @@ -511,4 +588,4 @@ FFZ.prototype._legacy_parse_badges = function(callback, data, slot, badge_id, ti callback(true, count); return count; -} \ No newline at end of file +}*/ \ No newline at end of file diff --git a/src/ember/channel.js b/src/ember/channel.js index a2882702..8ba72a32 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -222,7 +222,7 @@ FFZ.prototype._modify_cindex = function(view) { tb.attr('title', 'Theater Mode (Alt+T)'); if ( opts && opts.options && typeof opts.options.gravity !== "function" ) - opts.options.gravity = utils.tooltip_placement(constants.TOOLTIP_DISTANCE, opts.options.gravity || 'n');*/ + opts.options.gravity = utils.tooltip_placement(constants.TOOLTIP_DISTANCE, opts.options.gravity || 'n');//*/ this.ffzFixTitle(); this.ffzUpdateUptime(); diff --git a/src/ember/conversations.js b/src/ember/conversations.js index 9ff4031c..aa1d7fc0 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -1,6 +1,8 @@ var FFZ = window.FrankerFaceZ, utils = require('../utils'), - constants = require('../constants'); + constants = require('../constants'), + + createElement = utils.createElement; // --------------- @@ -25,13 +27,27 @@ FFZ.settings_info.top_conversations = { category: "Whispers", name: "Position on Top", - help: "Display the new conversation-style whisper UI at the top of the window instead of the bottom.", + help: "Display the whisper UI at the top of the window instead of the bottom.", on_update: function(val) { document.body.classList.toggle('ffz-top-conversations', val); } }; +FFZ.settings_info.hide_conversations_in_theatre = { + type: "boolean", + value: false, + no_mobile: true, + + category: "Whispers", + name: "Hide Whispers in Theater Mode", + help: "Hide the whisper UI when the page is in theater mode.", + on_update: function(val) { + document.body.classList.toggle('ffz-theatre-conversations', val); + } + }; + + FFZ.settings_info.minimize_conversations = { type: "boolean", value: false, @@ -39,7 +55,7 @@ FFZ.settings_info.minimize_conversations = { category: "Whispers", name: "Minimize Whisper UI", - help: "Slide the Whisper UI mostly out of view when it's not being used and you have no unread messages.", + help: "Slide the whisper UI mostly out of view when it's not being used and you have no unread messages.", on_update: function(val) { document.body.classList.toggle('ffz-minimize-conversations', val); } @@ -53,23 +69,36 @@ FFZ.settings_info.minimize_conversations = { FFZ.prototype.setup_conversations = function() { document.body.classList.toggle('ffz-top-conversations', this.settings.top_conversations); document.body.classList.toggle('ffz-minimize-conversations', this.settings.minimize_conversations); + document.body.classList.toggle('ffz-theatre-conversations', this.settings.hide_conversations_in_theatre); - this.log("Hooking the Ember Conversation Window component."); var ConvWindow = utils.ember_resolve('component:conversation-window'); if ( ConvWindow ) { + this.log("Hooking the Ember Conversation Window component."); this._modify_conversation_window(ConvWindow); try { ConvWindow.create().destroy() } catch(err) { } - } + } else + this.log("Unable to resolve: component:conversation-window"); + + + var ConvSettings = utils.ember_resolve('component:conversation-settings-menu'); + if ( ConvSettings ) { + this.log("Hooking the Ember Conversation Settings Menu component."); + this._modify_conversation_menu(ConvSettings); + try { ConvSettings.create().destroy() } + catch(err) { } + } else + this.log("Unable to resolve: component:conversation-settings-menu"); - this.log("Hooking the Ember Conversation Line component."); var ConvLine = utils.ember_resolve('component:conversation-line'); if ( ConvLine ) { + this.log("Hooking the Ember Conversation Line component."); this._modify_conversation_line(ConvLine); try { ConvLine.create().destroy() } catch(err) { } - } + } else + this.log("Unable to resolve: component:conversation-line"); // TODO: Make this better later. jQuery('.conversations-list').find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); @@ -77,6 +106,36 @@ FFZ.prototype.setup_conversations = function() { } +FFZ.prototype._modify_conversation_menu = function(component) { + var f = this; + + component.reopen({ + didInsertElement: function() { + var user = this.get('thread.otherUsername'), + el = this.get('element'), + sections = el && el.querySelectorAll('.options-section'); + + if ( ! user || ! user.length || f.has_bttv ) + return; + + if ( sections && sections.length ) + el.appendChild(createElement('div', 'options-divider')); + + var ffz_options = createElement('div', 'options-section'), + card_link = createElement('a', 'ffz-show-card', "Open Moderation Card"); + + card_link.addEventListener('click', function(e) { + el.parentElement.classList.add('hidden'); + FFZ.chat_commands.card.call(f, null, [user]); + }); + + ffz_options.appendChild(card_link); + el.appendChild(ffz_options); + } + }) +} + + FFZ.prototype._modify_conversation_window = function(component) { var f = this, Layout = utils.ember_lookup('controller:layout'); diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index 943f1467..707ac6d3 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -46,6 +46,7 @@ FFZ.prototype.setup_bttv = function(delay) { if ( this.is_dashboard ) { this._update_subscribers(); this._remove_dash_chart(); + //this._remove_dash_feed(); } document.body.classList.add('ffz-bttv'); @@ -224,7 +225,7 @@ FFZ.prototype.setup_bttv = function(delay) { if ( text && text.length ) output.push(text); var code = utils.quote_attr(data.raw); - output.push(['' + code + '']); + output.push(['' + code + '']); text = null; } else text = (text || '') + match; @@ -285,7 +286,7 @@ FFZ.prototype.setup_bttv = function(delay) { text = []; } - new_tokens.push(['']); + new_tokens.push(['' + utils.quote_attr(emote.name) + '']); if ( mine && l_room ) f.add_usage(l_room, emote); diff --git a/src/main.js b/src/main.js index cf259318..323cc7f0 100644 --- a/src/main.js +++ b/src/main.js @@ -35,7 +35,7 @@ FFZ.msg_commands = {}; // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 156, + major: 3, minor: 5, revision: 159, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -171,6 +171,7 @@ require('./ui/notifications'); require('./ui/viewer_count'); require('./ui/sub_count'); require('./ui/dash_stats'); +require('./ui/dash_feed'); require('./ui/menu_button'); require('./ui/following'); @@ -349,6 +350,7 @@ FFZ.prototype.init_dashboard = function(delay) { this.setup_following_count(false); this.setup_menu(); this.setup_dash_stats(); + this.setup_dash_feed(); this._update_subscribers(); diff --git a/src/ui/about_page.js b/src/ui/about_page.js index 5b170bb6..b3b298ce 100644 --- a/src/ui/about_page.js +++ b/src/ui/about_page.js @@ -1,7 +1,7 @@ var FFZ = window.FrankerFaceZ, constants = require("../constants"), utils = require("../utils"), - createElement = document.createElement.bind(document), + createElement = utils.createElement, NICE_DESCRIPTION = { "cluster": null, @@ -54,8 +54,7 @@ FFZ.ws_commands.update_news = function(version) { var include_html = function(heading_text, filename) { return function(view, container) { - var heading = createElement('div'); - heading.className = 'chat-menu-content center'; + var heading = createElement('div', 'chat-menu-content center'); heading.innerHTML = '

FrankerFaceZ

' + (heading_text ? '
' + heading_text + '
' : ''); jQuery.ajax(filename, {cache: false, context: this}) @@ -69,8 +68,7 @@ var include_html = function(heading_text, filename) { }); }).fail(function(data) { - var content = createElement('div'); - content.className = 'chat-menu-content menu-side-padding'; + var content = createElement('div', 'chat-menu-content menu-side-padding'); content.textContent = 'There was an error loading this page from the server.'; container.appendChild(heading); @@ -107,9 +105,8 @@ var update_player_stats = function(player, container) { if ( ! desc ) continue; - line = createElement('li'); + line = createElement('li', null, desc + ''); line.setAttribute('data-property', key); - line.innerHTML = desc + ''; container.appendChild(line); } @@ -193,7 +190,7 @@ FFZ.menu_pages.about = { content = ''; content += ''; - content += ''; + content += ''; content += ''; content += ''; diff --git a/src/ui/dash_feed.js b/src/ui/dash_feed.js new file mode 100644 index 00000000..27324c99 --- /dev/null +++ b/src/ui/dash_feed.js @@ -0,0 +1,197 @@ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + + createElement = utils.createElement; + + +// ------------------- +// Settings +// ------------------- + +FFZ.settings_info.dashboard_feed = { + type: "boolean", + value: true, + + no_mobile: true, + //no_bttv: true, + + category: "Dashboard", + name: "Channel Feed (Requires Refresh)", + help: "Add a way to post to your channel feed directly to the dashboard!" +} + + +// ------------------- +// Initialization +// ------------------- + +FFZ.prototype.setup_dash_feed = function() { + var f = this, + user = this.get_user(), + match = this.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, + id = this.is_dashboard && match && match[1]; + + if ( /*this.has_bttv ||*/ ! this.settings.dashboard_feed || ! user || ! id || id !== user.login ) + return; + + utils.api.get("feed/" + id + "/posts", {limit: 1}, {version: 3}).done(function(data) { + // If this works, then the feed is enabled. Show the UI. + /*if ( ! f.has_bttv )*/ + f.build_dash_feed(); + }) +} + + +FFZ.prototype._remove_dash_feed = function() { + var tabs = document.querySelector('#ffz-feed-tabs'), + parent = tabs && tabs.parentElement, + tab_status = document.querySelector('div.dash-broadcast-contain'); + + if ( ! tabs || ! parent ) + return; + + if ( tab_status && tabs.contains(tab_status) ) { + tabs.removeChild(tab_status); + parent.insertBefore(tab_status, tabs); + } + + parent.removeChild(tabs); +} + + +FFZ.prototype.build_dash_feed = function() { + var f = this, + user = this.get_user(), + + tabs = createElement('div'), + tab_bar = createElement('ul', 'tabs'), + + nav_status = createElement('li', 'tab', 'Broadcast Status'), + nav_feed = createElement('li', 'tab', 'Channel Feed'), + + tab_status = document.querySelector('div.dash-broadcast-contain'), + tab_feed = createElement('div', 'dash-broadcast-contain dash-ffz-feed-contain hidden'), + + txt_input = createElement('textarea', 'ffz-feed-entry'), + char_count = createElement('span', 'char-count', '0'); + align_btn = createElement('div', 'ffz-feed-button clearfix'), + chk_share = createElement('span', null, ' Share to Twitter'), + checkbox = chk_share.querySelector('input'), + btn_submit = createElement('button', 'button primary', 'Post'), + + column = tab_status && tab_status.parentElement, + placeholder = 'Post an update to your channel...'; + + if ( ! tab_status || ! column || ! user ) + return; + + tab_feed.appendChild(txt_input); + tab_feed.appendChild(align_btn); + + if ( user.twitter_connected ) + align_btn.appendChild(chk_share); + else { + var tnc = createElement('span', null, '(Twitter Not Connected)'); + tnc.title = 'Click to Refresh'; + tnc.style.cursor = 'pointer'; + tnc.addEventListener('click', function() { + user = f.get_user(); + if ( user.twitter_connected ) { + jQuery(tnc).trigger('mouseout'); + align_btn.removeChild(tnc); + align_btn.insertBefore(chk_share, align_btn.firstChild); + } + }); + jQuery(tnc).tipsy(); + align_btn.appendChild(tnc); + } + + char_count.title = 'Roughly the first 115 characters of a post will appear in a Tweet.'; + jQuery(char_count).tipsy(); + + align_btn.appendChild(btn_submit); + align_btn.appendChild(char_count); + + txt_input.id = 'vod_status'; + txt_input.placeholder = placeholder; + + var updater = function() { + var share = user.twitter_connected && checkbox && checkbox.checked || false, + len = txt_input.value.length; + + char_count.innerHTML = share ? utils.number_commas(115 - len) + '+' : utils.number_commas(len); + char_count.classList.toggle('over-limit', share && 115-len < 0 || false); + + if ( len === 0 ) + txt_input.placeholder = placeholder; + } + + checkbox.addEventListener('change', updater); + txt_input.addEventListener('input', updater); + + btn_submit.addEventListener('click', function() { + var match = f.is_dashboard ? location.pathname.match(/\/([^\/]+)/) : undefined, + id = f.is_dashboard && match && match[1]; + + if ( ! id || this.disabled ) + return; + + var share = user.twitter_connected && checkbox && checkbox.checked || false, + body = txt_input.value; + + txt_input.disabled = true; + btn_submit.disabled = true; + + utils.api.post("feed/" + id + "/posts?share=" + JSON.stringify(share), {content: body, share: share}).done(function(data) { + txt_input.disabled = false; + btn_submit.disabled = false; + + txt_input.value = ''; + + var tweeted = data.tweet ? " The update was tweeted out." : (share ? " There was a problem tweeting the update." : ""); + txt_input.placeholder = 'The update was posted successfully.' + tweeted + ' Post another update to your channel...'; + f.log("Channel Feed Posting Succeeded", data); + + }).fail(function(data) { + txt_input.disabled = false; + btn_submit.disabled = false; + + data = data ? data.data || data.responseJSON || undefined : undefined; + f.log("Channel Feed Posting Failed", data); + alert("An error occured posting to your channel feed:\n\n" + (data && data.message || "Unknown Error")); + + }); + }); + + var switch_tab = function(e) { + var to = this.getAttribute('data-nav'); + jQuery('.selected', tab_bar).removeClass('selected'); + this.classList.add('selected'); + + jQuery('.active-tab', tabs).removeClass('active-tab').addClass('hidden'); + jQuery('div[data-tab="' + to + '"]', tabs).addClass('active-tab').removeClass('hidden'); + } + + tabs.id = 'ffz-feed-tabs'; + + nav_status.setAttribute('data-nav', 'status'); + nav_feed.setAttribute('data-nav', 'feed'); + + nav_status.addEventListener('click', switch_tab); + nav_feed.addEventListener('click', switch_tab); + + tab_status.setAttribute('data-tab', 'status'); + tab_feed.setAttribute('data-tab', 'feed'); + + column.removeChild(tab_status); + + tab_bar.appendChild(nav_status); + tab_bar.appendChild(nav_feed); + + tabs.appendChild(tab_bar); + tabs.appendChild(tab_status); + tabs.appendChild(tab_feed); + + column.insertBefore(tabs, column.firstChild); + switch_tab.call(nav_status); +} \ No newline at end of file diff --git a/src/ui/dash_stats.js b/src/ui/dash_stats.js index 27bef0a3..694f4a4b 100644 --- a/src/ui/dash_stats.js +++ b/src/ui/dash_stats.js @@ -1,5 +1,10 @@ var FFZ = window.FrankerFaceZ, - utils = require('../utils'); + utils = require('../utils'), + + update_viewer_count = function(text) { + var vc = jQuery("#channel_viewer_count"); + vc.text() === 'Hidden' || vc.text(text); + }; // ------------------- @@ -196,7 +201,6 @@ FFZ.prototype._remove_dash_chart = function() { } - FFZ.prototype.update_dash_stats = function() { var f = this, id = this.dashboard_channel; @@ -225,10 +229,10 @@ FFZ.prototype.update_dash_stats = function() { status = null; if ( ! data || ! data.stream ) - !f.has_bttv && jQuery("#channel_viewer_count").text("Offline"); + !f.has_bttv && update_viewer_count("Offline"); else { - !f.has_bttv && jQuery("#channel_viewer_count").text(utils.number_commas(data.stream.viewers)); + !f.has_bttv && update_viewer_count(utils.number_commas(data.stream.viewers)); viewers = data.stream.viewers; var chan = data.stream.channel; diff --git a/src/utils.js b/src/utils.js index ed24907f..0f9af5da 100644 --- a/src/utils.js +++ b/src/utils.js @@ -467,5 +467,14 @@ module.exports = FFZ.utils = { return "" + count; }, - escape_regex: escape_regex + escape_regex: escape_regex, + + createElement: function(tag, className, content) { + var out = document.createElement(tag); + if ( className ) + out.className = className; + if ( content ) + out.innerHTML = content; + return out; + } } \ No newline at end of file diff --git a/style.css b/style.css index 9996bda9..49f85790 100644 --- a/style.css +++ b/style.css @@ -286,6 +286,11 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic opacity: 0.95; } +.ffz-theater-stats.ffz-theatre-conversations .app-main.theatre .player-column:focus #hostmode > div.clearfix, +.ffz-theater-stats.ffz-theatre-conversations .app-main.theatre .player-column:hover #hostmode > div.clearfix, +.ffz-theater-stats.ffz-theatre-conversations .app-main.theatre .player-column:focus .stats-and-actions, +.ffz-theater-stats.ffz-theatre-conversations .app-main.theatre .player-column:hover .stats-and-actions, + .ffz-theater-stats.ffz-top-conversations .app-main.theatre .player-column:focus #hostmode > div.clearfix, .ffz-theater-stats.ffz-top-conversations .app-main.theatre .player-column:hover #hostmode > div.clearfix, .ffz-theater-stats.ffz-top-conversations .app-main.theatre .player-column:focus .stats-and-actions, @@ -2441,6 +2446,8 @@ body:not(.ffz-bttv) .force-dark .ember-chat .chat-commands-dropdown li:hover { /* Conversations */ +.ffz-theatre-conversations .app-main.theatre .conversations-content { display: none } + body:not(.ffz-bttv) .conversation-window .new-message-divider + .timestamp-line { margin-top: -3px; } @@ -2500,6 +2507,8 @@ body:not(.ffz-top-conversations) .conversations-list-bottom-bar { bottom: -10px; } +.ffz-theatre-conversations .theatre .player-controls-bottom, +.ffz-theatre-conversations .theatre .player[data-controls=true] .player-controls-bottom, .ffz-top-conversations .theatre .player-controls-bottom, .ffz-top-conversations .theatre .player[data-controls=true] .player-controls-bottom { padding-bottom: 0; @@ -2721,4 +2730,21 @@ body:not(.ffz-creative-showcase) .creative-hero, .ember-chat .chat-interface .suggestions.ffz-suggestions .suggestion.has-image:not(.has-info) { line-height: 40px; -} \ No newline at end of file +} + +/* Dashboard Channel Feed */ + +.ffz-feed-button span { line-height: 30px; } +#ffz-feed-tabs .tabs { margin-bottom: 10px } +#ffz-feed-tabs textarea { resize: vertical } + +.ffz-feed-button .char-count.over-limit { color: #A00 } +.ffz-feed-button .char-count.over-limit span { display: inline } +.ffz-feed-button .char-count span { display: none; opacity: 0.75 } +.ffz-feed-button .char-count, +.ffz-feed-button button { float: right; margin-left: 10px } + +body.ffz-bttv #ffz-feed-tabs .tabs li:not(.selected):not(:hover) a { color: #6441a5; } +body.ffz-bttv-dark #ffz-feed-tabs .tabs li:not(.selected):not(:hover) a { color: #999; } + +body.ffz-bttv #ffz-feed-tabs .tabs { margin-bottom: 0 } \ No newline at end of file
Developers
Dan Salvato  
Dan Salvato  
Stendec  
Version ' + FFZ.version_info + 'Logs