1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-02 17:18:31 +00:00

Start using the new badges API to load badge data. Option to hide whisper UI in theater mode. Channel Feed on Dashboard UI.

This commit is contained in:
SirStendec 2016-04-16 17:45:25 -04:00
parent 21d1ce0d8f
commit d6b1b215d9
10 changed files with 407 additions and 35 deletions

View file

@ -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;
}
}*/

View file

@ -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();

View file

@ -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');

View file

@ -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(['<img class="emoticon emoji ffz-tooltip" height="18px" data-ffz-emoji="' + eid + '" src="' + utils.quote_attr(src) + '" data-regex="' + code + '" alt="' + code + '">']);
output.push(['<img class="emoticon emoji ffz-tooltip" height="18px" data-ffz-emoji="' + eid + '" src="' + utils.quote_attr(src) + '" alt="' + code + '">']);
text = null;
} else
text = (text || '') + match;
@ -285,7 +286,7 @@ FFZ.prototype.setup_bttv = function(delay) {
text = [];
}
new_tokens.push(['<img class="emoticon ffz-tooltip" data-ffz-set="' + emote.set_id + '" data-ffz-emote="' + emote.id + '" srcset="' + utils.quote_attr(emote.srcSet || "") + '" src="' + utils.quote_attr(emote.urls[1]) + '" data-regex="' + utils.quote_attr(emote.name) + '">']);
new_tokens.push(['<img class="emoticon ffz-tooltip" data-ffz-set="' + emote.set_id + '" data-ffz-emote="' + emote.id + '" srcset="' + utils.quote_attr(emote.srcSet || "") + '" src="' + utils.quote_attr(emote.urls[1]) + '" alt="' + utils.quote_attr(emote.name) + '">']);
if ( mine && l_room )
f.add_usage(l_room, emote);

View file

@ -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();

View file

@ -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 = '<h1>FrankerFaceZ</h1>' + (heading_text ? '<div class="ffz-about-subheading">' + heading_text + '</div>' : '');
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 + '<span></span>');
line.setAttribute('data-property', key);
line.innerHTML = desc + '<span></span>';
container.appendChild(line);
}
@ -193,7 +190,7 @@ FFZ.menu_pages.about = {
content = '<table class="ffz-about-table">';
content += '<tr><th colspan="4">Developers</th></tr>';
content += '<tr><td>Dan Salvato</td><td><a class="twitch" href="//www.twitch.tv/dansalvato" title="Twitch" target="_new">&nbsp;</a></td><td><a class="twitter" href="https://twitter.com/dansalvato1" title="Twitter" target="_new">&nbsp;</a></td><td><a class="youtube" href="https://www.youtube.com/user/dansalvato1" title="YouTube" target="_new">&nbsp;</a></td></tr>';
content += '<tr><td>Dan Salvato</td><td><a class="twitch" href="//www.twitch.tv/dansalvato" title="Twitch" target="_new">&nbsp;</a></td><td><a class="twitter" href="https://twitter.com/dansalvato" title="Twitter" target="_new">&nbsp;</a></td><td><a class="youtube" href="https://www.youtube.com/user/dansalvato1" title="YouTube" target="_new">&nbsp;</a></td></tr>';
content += '<tr><td>Stendec</td><td><a class="twitch" href="//www.twitch.tv/sirstendec" title="Twitch" target="_new">&nbsp;</a></td><td><a class="twitter" href="https://twitter.com/SirStendec" title="Twitter" target="_new">&nbsp;</a></td><td><a class="youtube" href="https://www.youtube.com/channel/UCnxuvmK1DCPCXSJ-mXIh4KQ" title="YouTube" target="_new">&nbsp;</a></td></tr>';
content += '<tr class="debug"><td><a href="#" id="ffz-changelog">Version ' + FFZ.version_info + '</a></td><td colspan="3"><a href="#" id="ffz-debug-logs">Logs</a></td></tr>';

197
src/ui/dash_feed.js Normal file
View file

@ -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 <small>(Requires Refresh)</small>",
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', '<a id="ffz-nav-status">Broadcast Status</a>'),
nav_feed = createElement('li', 'tab', '<a id="ffz-nav-feed">Channel Feed</a>'),
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, '<input type="checkbox"> Share to Twitter'),
checkbox = chk_share.querySelector('input'),
btn_submit = createElement('button', 'button primary', '<span>Post</span>'),
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, '<i>(Twitter Not Connected)</i>');
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) + '<span>+</span>' : 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);
}

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;
@ -2722,3 +2731,20 @@ body:not(.ffz-creative-showcase) .creative-hero,
.ember-chat .chat-interface .suggestions.ffz-suggestions .suggestion.has-image:not(.has-info) {
line-height: 40px;
}
/* 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 }