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

3.5.84 to 3.5.100. Most importantly, started using new Ember stuff. -view-registry instead of Ember.View.views, and such. Finally added UI for managing pinned channels. Use HTTPS for the API and socket servers. Don't immediately unload chat rooms. Smarter chat tab behavior. Added /card command for opening mod cards. Other stuff.

This commit is contained in:
SirStendec 2015-12-12 13:28:35 -05:00
parent c167a8b626
commit 800553c602
28 changed files with 1016 additions and 525 deletions

View file

@ -3,7 +3,7 @@
background-color:rgb(16,16,16)!important;
}
.ffz-dark div#channel > .target-frame.active{
.ffz-dark div#channel > .target-frame {
background-color:rgb(16,16,16)!important;
}
@ -22,6 +22,14 @@
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.ffz-dark .offlineChannelStatus {
background-color: rgba(255,255,255, 0.05);
}
.ffz-dark .close-hostmode a:before,
.ffz-dark .close-hostmode a:after {
border-bottom-color: rgba(255,255,255, 0.05);
}
/* hidden chat */
@ -248,7 +256,7 @@
.ffz-dark .manager .videos-grid .video:hover .meta .actions li a,
.ffz-dark .ember-chat .chat-room-list .room:not(:hover) p.room-title,
.ffz-dark .dropmenu_action:not(:hover) span,
.ffz-dark a:not(.filter-item):not(.ui-state-focus):not(.button):not(.switch):not(.follow):not(.fb_button) {
.ffz-dark a:not(.filter-item):not(.ui-state-focus):not(.button):not(.switch):not(.follow):not(.fb_button):not(.what) {
color: #a68ed2;
}

View file

@ -228,9 +228,9 @@ FFZ.prototype.get_line_badges = function(msg) {
}
}
if ( msg.labels.indexOf('subscriber') !== -1 )
if ( msg.labels && msg.labels.indexOf('subscriber') !== -1 )
badges[10] = {klass: 'subscriber', title: 'Subscriber'}
if ( msg.labels.indexOf('turbo') !== -1 )
if ( msg.labels && msg.labels.indexOf('turbo') !== -1 )
badges[15] = {klass: 'turbo', title: 'Turbo'};
// FFZ Badges
@ -338,7 +338,7 @@ FFZ.prototype.bttv_badges = function(data) {
if ( b.type === full_badge.replaces_type ) {
b.type = "ffz-badge-replacement " + b.type;
b.description += ", " + (badge.title || full_badge.title) +
'" style="background-image: url("' + utils.quote_attr(badge.image || full_badge.image) + '")';
'" style="background-image: url(' + utils.quote_attr('"' + (badge.image || full_badge.image) + '"') + ')';
replaced = true;
break;
}
@ -349,18 +349,18 @@ FFZ.prototype.bttv_badges = function(data) {
}
if ( alpha && badge.transparent_image )
style += 'background-image: url("' + utils.quote_attr(badge.transparent_image) + '");';
style += 'background-image: url("' + badge.transparent_image + '");';
else if ( badge.image )
style += 'background-image: url("' + utils.quote_attr(badge.image) + '");';
style += 'background-image: url("' + badge.image + '");';
if ( badge.color && ! alpha )
style += 'background-color: ' + utils.quote_attr(badge.color) + '; ';
style += 'background-color: ' + badge.color + '; ';
if ( badge.extra_css )
style += utils.quote_attr(badge.extra_css);
style += badge.extra_css;
if ( style )
desc += '" style="' + style;
desc += '" style="' + utils.quote_attr(style);
badges_out.push([(insert_at == -1 ? 1 : -1) * slot, {type: "ffz-badge-" + badge.id + (alpha ? " alpha" : ""), name: "", description: desc}]);
}
@ -451,14 +451,17 @@ FFZ.prototype._legacy_load_donors = function(callback, tries) {
FFZ.prototype._legacy_parse_badges = function(callback, data, slot, badge_id, title_template) {
var title = this.badges[badge_id].title,
count = 0;
count = 0,
ds = null;
title_template = title_template || '{}';
if ( data != null ) {
var lines = data.trim().split(/\W+/);
var lines = data.trim().split(/[ \t\n\r]+/);
for(var i=0; i < lines.length; i++) {
if ( ! /^\w/.test(lines[i]) )
continue;
var line_data = lines[i].split(";"),
user_id = line_data[0],
user = this.users[user_id] = this.users[user_id] || {},
@ -471,7 +474,7 @@ FFZ.prototype._legacy_parse_badges = function(callback, data, slot, badge_id, ti
if ( badges[slot] )
continue;
badges[slot] = {id:badge_id};
badges[slot] = {id: badge_id};
if ( line_data.length > 1 )
badges[slot].title = title_template.replace('{}', line_data[1]);
count += 1;

View file

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

View file

@ -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 <username>";
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
// -----------------

View file

@ -2,21 +2,21 @@ var SVGPATH = '<path d="m120.95 1.74c4.08-0.09 8.33-0.84 12.21 0.82 3.61 1.8 7 4
DEBUG = localStorage.ffzDebugMode == "true" && document.body.classList.contains('ffz-dev'),
SERVER = DEBUG ? "//localhost:8000/" : "//cdn.frankerfacez.com/";
DIRECT_SERVER = DEBUG ? "//localhost:8000/" : "//direct-cdn.frankerfacez.com/";
//DIRECT_SERVER = DEBUG ? "//localhost:8000/" : "//direct-cdn.frankerfacez.com/";
module.exports = {
DEBUG: DEBUG,
SERVER: SERVER,
DIRECT_SERVER: DIRECT_SERVER,
//DIRECT_SERVER: DIRECT_SERVER,
API_SERVER: "//api.frankerfacez.com/",
API_SERVER_2: "//direct-api.frankerfacez.com/",
API_SERVER: "https://api.frankerfacez.com/",
//API_SERVER_2: "http://direct-api.frankerfacez.com/",
WS_SERVER_POOLS: {
1: [
["ws://catbag.frankerfacez.com/", 0.5],
["ws://andknuckles.frankerfacez.com/", 1],
["ws://tuturu.frankerfacez.com/", 1]],
["wss://catbag.frankerfacez.com/", 0.5],
["wss://andknuckles.frankerfacez.com/", 1],
["wss://tuturu.frankerfacez.com/", 1]],
2: [
["ws://localhost:8001/", 1]]
},

View file

@ -38,11 +38,12 @@ FFZ.prototype.setup_channel = function() {
} catch(err) { }
// Update Existing
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 Channel) )
continue;
@ -166,8 +167,8 @@ FFZ.prototype.setup_channel = function() {
if ( display_name )
FFZ.capitalization[name] = [display_name, Date.now()];
if ( f.settings.group_tabs && f._chatv )
f._chatv.ffzRebuildTabs();
if ( f._chatv )
f._chatv.ffzUpdateHost(target);
if ( f.settings.follow_buttons )
f.rebuild_following_ui();

View file

@ -90,24 +90,24 @@ FFZ.settings_info.input_emoji = {
// ---------------------
FFZ.prototype.setup_chat_input = function() {
this.log("Hooking the Ember Chat Input controller.");
this.log("Hooking the Ember Chat Input component.");
var Input = App.__container__.resolve('component:twitch-chat-input'),
f = this;
if ( ! Input )
return;
return this.log("Unable to get Chat Input component.");
this._modify_chat_input(Input);
if ( this._roomv ) {
for(var i=0; i < this._roomv._childViews.length; i++) {
var v = this._roomv._childViews[i];
var views = this._roomv && this._roomv._viewRegistry || window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views;
for(var key in views) {
var v = views[key];
if ( v instanceof Input ) {
this.log("Manually modifying Chat Input component.", v);
this._modify_chat_input(v);
v.ffzInit();
}
}
}
}
@ -116,6 +116,7 @@ FFZ.prototype._modify_chat_input = function(component) {
component.reopen({
ffz_mru_index: -1,
ffz_chatters: [],
didInsertElement: function() {
this._super();
@ -149,9 +150,6 @@ FFZ.prototype._modify_chat_input = function(component) {
this.ffzResizeInput();
setTimeout(this.ffzResizeInput.bind(this), 500);
/*var suggestions = this._parentView.get('context.model.chatSuggestions');
this.set('ffz_chatters', suggestions);*/
},
ffzTeardown: function() {
@ -291,7 +289,7 @@ FFZ.prototype._modify_chat_input = function(component) {
return;
var ind = this.get('ffz_mru_index'),
mru = this._parentView.get('context.model.mru_list') || [];
mru = this.get('parentView.context.model.mru_list') || [];
if ( key === KEYCODES.UP )
ind = (ind + 1) % (mru.length + 1);
@ -336,7 +334,7 @@ FFZ.prototype._modify_chat_input = function(component) {
/*ffz_emoticons: function() {
var output = [],
room = this._parentView.get('context.model'),
room = this.get('parentView.context.model'),
room_id = room && room.get('id'),
tmi = room && room.tmiSession,
@ -349,7 +347,7 @@ FFZ.prototype._modify_chat_input = function(component) {
for(var set_id in es.emoticon_sets) {
var emote_set = es.emoticon_sets[set_id];
for(var emote_id in emote_set) {
if ( emote_set[emote_id] ) {
if ( emote_set[emote_id] && emote_set[emote_id].code ) {
var code = emote_set[emote_id].code;
output.push({id: constants.KNOWN_CODES[code] || code});
}
@ -365,7 +363,7 @@ FFZ.prototype._modify_chat_input = function(component) {
for(var emote_id in emote_set.emoticons) {
var emote = emote_set.emoticons[emote_id];
if ( ! emote.hidden )
if ( ! emote.hidden && emote.name )
output.push({id:emote.name});
}
}
@ -373,8 +371,6 @@ FFZ.prototype._modify_chat_input = function(component) {
return output;
}.property(),
ffz_chatters: [],
suggestions: function(key, value, previousValue) {
if ( arguments.length > 1 ) {
this.set('ffz_chatters', value);

File diff suppressed because it is too large Load diff

View file

@ -58,8 +58,7 @@ FFZ.prototype.setup_conversations = function() {
FFZ.prototype._modify_conversation_window = function(component) {
var f = this,
Layout = App.__container__.lookup('controller:layout'),
Settings = App.__container__.lookup('controller:settings');
Layout = App.__container__.lookup('controller:layout');
component.reopen({
headerBadges: Ember.computed("thread.participants", "currentUsername", function() {
@ -106,8 +105,7 @@ FFZ.prototype._modify_conversation_window = function(component) {
FFZ.prototype._modify_conversation_line = function(component) {
var f = this,
Layout = App.__container__.lookup('controller:layout'),
Settings = App.__container__.lookup('controller:settings');
Layout = App.__container__.lookup('controller:layout');
component.reopen({
tokenizedMessage: function() {

View file

@ -149,9 +149,11 @@ FFZ.prototype.setup_directory = function() {
document.body.classList.toggle('ffz-creative-tags', this.settings.directory_creative_all_tags);
document.body.classList.toggle('ffz-creative-showcase', this.settings.directory_creative_showcase);
this.log("Hooking the Ember games-following controller.");
var GamesFollowing = App.__container__.lookup('controller:games-following');
var GamesFollowing = App.__container__.lookup('controller:games-following'),
f = this;
if ( GamesFollowing ) {
this.log("Hooking the Ember games-following controller.");
GamesFollowing.reopen({
ffz_sidebar_games: this.settings.sidebar_followed_games,
@ -164,7 +166,8 @@ FFZ.prototype.setup_directory = function() {
});
Ember.propertyDidChange(GamesFollowing, 'sidePanelFollowing');
}
} else
this.error("Unable to load the Ember games-following controller.");
this.log("Attempting to modify the Following collection.");
@ -189,8 +192,9 @@ FFZ.prototype.setup_directory = function() {
this._modify_directory_host(HostView);
// Initialize existing views.
for(var key in Ember.View.views) {
var view = Ember.View.views[key];
var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views;
for(var key in views) {
var view = views[key];
try {
if ( (ChannelView && view instanceof ChannelView) || (CreativeChannel && view instanceof CreativeChannel) || (CSGOChannel && view instanceof CSGOChannel) || (HostView && view instanceof HostView) )
view.ffzInit();
@ -469,10 +473,20 @@ FFZ.prototype._modify_directory_host = function(dir) {
for(var i=0; i < hosts.length; i++)
make_link(hosts[i]);
f.show_popup(menu, [e.clientX - 60, e.clientY - 60], document.querySelector('#main_col > .tse-scroll-content > .tse-content'));
var cont = document.querySelector('#main_col > .tse-scroll-content > .tse-content'),
bounds = cont && cont.getBoundingClientRect(),
x = e.clientX - 60,
y = e.clientY - 60;
if ( bounds )
x = Math.max(bounds.left, Math.min(x, (bounds.left + bounds.width) - 302));
f.show_popup(menu, [x, y], document.querySelector('#main_col > .tse-scroll-content > .tse-content'));
},
ffzCleanup: function() {
var target = this.get('context.model.target.channel');
if ( f._popup && f._popup.classList.contains('ffz-channel-selector') && f._popup.getAttribute('data-channel') === target )
f.close_popup();
},
@ -485,7 +499,41 @@ FFZ.prototype._modify_directory_host = function(dir) {
title = meta && meta.querySelector('.title a'),
target = this.get('context.model.target.channel'),
hosts = this.get('context.model.ffz_hosts');
hosts = this.get('context.model.ffz_hosts'),
boxart = thumb && thumb.querySelector('.boxart');
// Fix the game not showing
if ( ! boxart && thumb && this.get('context.model.game') ) {
var img = document.createElement('img'),
game = this.get("context.model.game"),
c = this.get('controller');
boxart = document.createElement('a');
boxart.className = 'boxart';
boxart.href = this.get("context.model.gameUrl");
boxart.setAttribute('original-title', game);
boxart.addEventListener('click', function(e) {
e.preventDefault();
jQuery('.tipsy').remove();
if ( game === "Counter-Strike: Global Offensive" )
c.transitionTo('csgo.channels.index')
else if ( game === "Creative" )
c.transitionTo('creative.channels.index');
else
c.transitionTo('gameDirectory.index', encodeURIComponent(game));
return false;
});
img.src = this.get("context.model.gameBoxart");
boxart.appendChild(img);
thumb.appendChild(boxart);
}
if ( f.settings.directory_logos ) {
el.classList.add('ffz-directory-logo');

View file

@ -71,7 +71,7 @@ FFZ.prototype.setup_profile_following = function() {
ffzInit: function() {
// Only process our own profile following page.
var user = f.get_user();
if ( ! f.settings.enhance_profile_following || ! user || ! user.login === this.get('context.id') )
if ( ! f.settings.enhance_profile_following || ! user || user.login !== this.get('context.id') )
return;
var el = this.get('element'),
@ -259,8 +259,9 @@ FFZ.prototype.setup_profile_following = function() {
ProfileView.create().destroy();
} catch(err) { }
for(var key in Ember.View.views) {
var view = Ember.View.views[key];
var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views;
for(var key in views) {
var view = views[key];
if ( ! view || !(view instanceof ProfileView) )
continue;

View file

@ -671,7 +671,7 @@ FFZ.prototype._modify_line = function(component) {
raw_color = this.get('msgObject.color'),
colors = raw_color && f._handle_color(raw_color),
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode'));
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode'));
e.push('<div class="indicator"></div>');

View file

@ -399,7 +399,10 @@ FFZ.prototype.setup_mod_card = function() {
Card.reopen({
ffzForceRedraw: function() {
this.rerender();
}.observes("cardInfo.isModeratorOrHigher", "cardInfo.user"),
if ( f.settings.mod_card_history )
this.ffzRenderHistory();
}.observes("cardInfo.isModeratorOrHigher", "cardInfo.user.id"),
ffzRebuildInfo: function() {
var el = this.get('element'),
@ -730,19 +733,68 @@ FFZ.prototype.setup_mod_card = function() {
// Message History
if ( f.settings.mod_card_history ) {
var Chat = App.__container__.lookup('controller:chat'),
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(),
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";
}
// Focus the Element
this.$().draggable({
start: function() {
el.focus();
}});
el.focus();
} catch(err) {
try {
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 = controller.get('cardInfo.user.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);
@ -787,40 +839,12 @@ FFZ.prototype.setup_mod_card = function() {
for(var i=0; i < user_history.length; i++)
history.appendChild(f._build_mod_card_history(user_history[i], t));
el.appendChild(history);
// Lazy scroll-to-bottom
history.scrollTop = history.scrollHeight;
}
// Reposition the menu if it's off-screen.
var el_bound = el.getBoundingClientRect(),
body_bound = document.body.getBoundingClientRect();
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";
}
// Focus the Element
this.$().draggable({
start: function() {
el.focus();
}});
el.focus();
} catch(err) {
try {
f.error("ModerationCardView didInsertElement: " + err);
} catch(err) { }
}
},
ffzAdjacentHistory: function(line) {
var Chat = App.__container__.lookup('controller:chat'),
controller = this.get('controller'),
t = this,
user_id = this.get('cardInfo.user.id'),
@ -835,23 +859,31 @@ FFZ.prototype.setup_mod_card = function() {
history = el && el.querySelector('.chat-history'),
logs = el && el.querySelector('.chat-history.adjacent-history'),
when = line.date.getTime();
when = line.date.getTime(),
scroll_top = logs && logs.scrollTop || history && history.scrollTop || 0;
if ( ! history )
return;
if ( logs )
if ( logs ) {
logs.classList.add('loading');
else
logs.scrollTop = 0;
} else {
history.classList.add('loading');
history.scrollTop = 0;
}
if ( ! f.ws_send("adjacent_history", [room_id, when, 2], function(success, data) {
if ( logs )
var was_loading = history.classList.contains('loading');
if ( logs ) {
logs.classList.remove('loading');
else
logs.scrollTop = scroll_top;
} else {
history.classList.remove('loading');
history.scrollTop = scroll_top;
}
if ( ! success || ! data || ! data.length )
if ( ! success || ! data || ! data.length || ! was_loading )
return;
var had_logs = false,
@ -908,10 +940,13 @@ FFZ.prototype.setup_mod_card = function() {
});
}) )
if ( logs )
if ( logs ) {
logs.classList.remove('loading');
else
logs.scrollTop = scroll_top;
} else {
history.classList.remove('loading');
history.scrollTop = scroll_top;
}
}
});
}
@ -928,6 +963,9 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
out.push('<span class="timestamp float-left">' + helpers.getTime(msg.date) + '</span>');
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('<span class="badges float-left">');
@ -942,13 +980,11 @@ FFZ.prototype._build_mod_card_history = function(msg, modcard, show_from) {
Layout = App.__container__.lookup('controller:layout'),
Settings = App.__container__.lookup('controller:settings'),
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode'));
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode'));
// Aliases and Styling
var alias = this.aliases[msg.from],
name = (msg.tags && msg.tags['display-name']) || (msg.from && msg.from.capitalize()) || "unknown user",
style = colors && 'color:' + (is_dark ? colors[1] : colors[0]),
var style = colors && 'color:' + (is_dark ? colors[1] : colors[0]),
colored = style ? ' has-color' : '';

View file

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

View file

@ -1,7 +1,4 @@
var FFZ = window.FrankerFaceZ,
CSS = /\.([\w\-_]+)\s*?\{content:\s*?"([^"]+)";\s*?background-image:\s*?url\("([^"]+)"\);\s*?height:\s*?(\d+)px;\s*?width:\s*?(\d+)px;\s*?margin:([^;}]+);?([^}]*)\}/mg,
MOD_CSS = /[^\n}]*\.badges\s+\.moderator\s*{\s*background-image:\s*url\(\s*['"]([^'"]+)['"][^}]+(?:}|$)/,
GROUP_CHAT = /^_([^_]+)_\d+$/,
HOSTED_SUB = / subscribed to /,
constants = require('../constants'),
utils = require('../utils'),
@ -58,6 +55,35 @@ FFZ.prototype.setup_room = function() {
this.get("model.tmiRoom").sendMessage("/timeout " + e.user + " 1");
this.get("model").clearMessages(e.user);
}
RC._actions.showModOverlay = function(e) {
var Channel = App.__container__.resolve('model:channel');
if ( ! Channel )
return;
var chan = Channel.find({id: e.sender});
// Don't try loading the channel if it's already loaded. Don't make mod cards
// refresh the channel page when you click the broadcaster, basically.
if ( ! chan.get('isLoaded') )
chan.load();
this.set("showModerationCard", true);
// We pass in renderBottom and renderRight, which we use to reposition the window
// after we know how big it actually is.
this.set("moderationCardInfo", {
user: chan,
renderTop: e.top,
renderLeft: e.left,
renderBottom: e.bottom,
renderRight: e.right,
isIgnored: this.get("tmiSession").isIgnored(e.sender),
isChannelOwner: this.get("controllers.login.userData.login") === e.sender,
profileHref: Twitch.uri.profile(e.sender),
isModeratorOrHigher: this.get("model.isModeratorOrHigher")
});
}
}
this.log("Hooking the Ember Room model.");
@ -90,11 +116,12 @@ FFZ.prototype.setup_room = function() {
} catch(err) { }
// Modify all existing Room views.
for(var key in Ember.View.views) {
if ( ! Ember.View.views.hasOwnProperty(key) )
var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views;
for(var key in views) {
if ( ! views.hasOwnProperty(key) )
continue;
var view = Ember.View.views[key];
var view = views[key];
if ( !(view instanceof RoomView) )
continue;
@ -834,7 +861,7 @@ FFZ.prototype._insert_history = function(room_id, data, from_server) {
FFZ.prototype.load_room = function(room_id, callback, tries) {
var f = this;
jQuery.getJSON(((tries||0)%2 === 0 ? constants.API_SERVER : constants.API_SERVER_2) + "v1/room/" + room_id)
jQuery.getJSON(constants.API_SERVER + "v1/room/" + room_id)
.done(function(data) {
if ( data.sets ) {
for(var key in data.sets)
@ -935,6 +962,28 @@ FFZ.prototype._modify_room = function(room) {
}
},
ffzScheduleDestroy: function() {
if ( this._ffz_destroy_timer )
return;
var t = this;
this._ffz_destroy_timer = setTimeout(function() {
t._ffz_destroy_timer = null;
t.ffzCheckDestroy();
}, 5000);
},
ffzCheckDestroy: function() {
var Chat = App.__container__.lookup('controller:chat'),
user = f.get_user(),
room_id = this.get('id');
if ( (Chat && Chat.get('currentChannelRoom') === this) || (user && user.login === room_id) || (f._chatv && f._chatv._ffz_host === room_id) || (f.settings.pinned_rooms && f.settings.pinned_rooms.indexOf(room_id) !== -1) )
return;
this.destroy();
},
ffzUpdateStatus: function() {
if ( f._roomv )
f._roomv.ffzUpdateStatus();
@ -1089,7 +1138,14 @@ FFZ.prototype._modify_room = function(room) {
this.get("messages").pushObject(msg);
this.trimMessages();
"admin" === msg.style || ("whisper" === msg.style && ! this.ffz_whisper_room ) || this.incrementProperty("unreadCount", 1);
if ( msg.style !== "admin" && msg.style !== "whisper" ) {
if ( msg.ffz_has_mention ) {
this.ffz_last_mention = Date.now();
}
this.ffz_last_activity = Date.now();
this.incrementProperty("unreadCount", 1);
}
}
},
@ -1102,9 +1158,6 @@ FFZ.prototype._modify_room = function(room) {
if ( this._ffz_pending_flush )
return;
/*if ( this._ffz_pending_flush )
clearTimeout(this._ffz_pending_flush);*/
if ( this.ffzPending && this.ffzPending.length ) {
// We need either the amount of chat delay past the first message, if chat_delay is on, or the
// amount of time from the last batch.
@ -1258,10 +1311,9 @@ FFZ.prototype._modify_room = function(room) {
},
send: function(text, ignore_history) {
if ( f.settings.group_tabs && f.settings.whisper_room && this.ffz_whisper_room )
return;
try {
this.ffz_last_input = Date.now();
if ( text && ! ignore_history ) {
// Command History
var mru = this.get('mru_list'),
@ -1294,16 +1346,13 @@ FFZ.prototype._modify_room = function(room) {
},
ffzUpdateUnread: function() {
if ( f.settings.group_tabs ) {
var Chat = App.__container__.lookup('controller:chat');
if ( Chat && Chat.get('currentRoom') === this )
this.resetUnreadCount();
else if ( f._chatv )
f._chatv.ffzTabUnread(this.get('id'));
}
f._chatv.ffzUpdateUnread(this.get('id'));
}.observes('unreadCount'),
ffzInitChatterCount: function() {
if ( ! this.tmiRoom )
return;

View file

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

View file

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

View file

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

View file

@ -43,10 +43,16 @@ FFZ.prototype.setup_bttv = function(delay) {
}
// Disable Chat Tabs
if ( this.settings.group_tabs && this._chatv ) {
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 )

View file

@ -47,6 +47,26 @@ FFZ.prototype.setup_rechat = function() {
FFZ.prototype.find_rechat = function() {
var el = !this.has_bttv ? document.querySelector('.rechat-chat-line') : null;
if ( ! this._rechat_listening && ! el ) {
// Try darkening a chat container. We don't have chat.
var container = document.querySelector('.chat-container'),
header = container && container.querySelector('.chat-header');
if ( header && header.textContent.indexOf('ReChat') !== -1 ) {
// Look-up dark mode.
var dark_chat = this.settings.dark_twitch;
if ( ! dark_chat ) {
var model = window.App ? App.__container__.lookup('controller:settings').get('model') : undefined;
dark_chat = model && model.get('darkMode');
}
container.classList.toggle('dark', dark_chat);
jQuery(container).find('.chat-lines').addClass('ffz-scrollbar');
}
return;
}
// If there's no change, don't continue.
if ( !!el === this._rechat_listening )
return;
@ -128,7 +148,7 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) {
Layout = App.__container__.lookup('controller:layout'),
Settings = App.__container__.lookup('controller:settings'),
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')),
is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('settings.darkMode')),
badges_el = line.querySelector('.badges'),
from_el = line.querySelector('.from'),
@ -196,7 +216,7 @@ FFZ.prototype.process_rechat_line = function(line, reprocess) {
if ( ! message_el )
return;
if ( ! reprocess && message_el.style.color ) {
if ( ! reprocess && colors && message_el.style.color ) {
message_el.classList.add('has-color');
message_el.style.color = is_dark ? colors[1] : colors[0];
}

View file

@ -22,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; }
// Version
var VER = FFZ.version_info = {
major: 3, minor: 5, revision: 83,
major: 3, minor: 5, revision: 100,
toString: function() {
return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || "");
}
@ -333,10 +333,18 @@ FFZ.prototype.init_ember = function(delay) {
this.users = {};
this.is_dashboard = false;
try {
this.embed_in_dash = window.top !== window && /\/[^\/]+\/dashboard/.test(window.top.location.pathname) && !/bookmarks$/.test(window.top.location.pathname);
} catch(err) { this.embed_in_dash = false; }
// Make an alias so they STOP RENAMING THIS ON ME
var Settings = App.__container__.lookup('controller:settings');
if ( Settings && Settings.get('settings') === undefined )
Settings.reopen({settings: Ember.computed.alias('model')});
// Initialize all the modules.
this.load_settings();

View file

@ -357,7 +357,7 @@ FFZ.prototype.ws_ping = function() {
return;
this._ws_ping_time = window.performance ? performance.now() : Date.now();
if ( ! this.ws_send("ping", null, this._ws_on_pong.bind(this)) )
if ( ! this.ws_send("ping", undefined, this._ws_on_pong.bind(this)) )
this._ws_ping_time = null;
}

View file

@ -449,11 +449,21 @@ FFZ.prototype.tokenize_chat_line = function(msgObject, prevent_notification, del
// We have a mention!
msgObject.ffz_has_mention = true;
// If we have chat tabs, update the status.
if ( room_id && ! this.has_bttv && this.settings.group_tabs && this._chatv && this._chatv._ffz_tabs ) {
var el = this._chatv._ffz_tabs.querySelector('.ffz-chat-tab[data-room="' + room_id + '"]');
if ( el && ! el.classList.contains('active') )
el.classList.add('tab-mentioned');
// If we have chat tabs/rows, update the status.
if ( room_id && ! this.has_bttv && this._chatv ) {
var room = this.rooms[room_id] && this.rooms[room_id].room;
if ( room._ffz_tab && ! room._ffz_tab.classList.contains('active') ) {
room._ffz_tab.classList.add('tab-mentioned');
var was_hidden = room._ffz_tab.classList.contains('hidden');
if ( was_hidden ) {
room._ffz_tab.classList.remove('hidden');
this._chatv.$('.chat-room').css('top', this._chatv._ffz_tabs.offsetHeight + "px");
}
}
if ( room._ffz_row && ! room._ffz_row.classList.contains('active') )
room._ffz_row.classList.add('row-mentioned');
}
// Display notifications if that setting is enabled. Also make sure
@ -601,7 +611,7 @@ FFZ.prototype.render_tokens = function(tokens, render_links) {
}
//var mirror_url = utils.quote_attr(constants.EMOTE_MIRROR_BASE + id + '.png');
extra = ' data-emote="' + id + '"'; // onerror="FrankerFaceZ._emote_mirror_swap(this)"'; // Disable error checking for now.
extra = ' data-emote="' + id + '" onerror="FrankerFaceZ._emote_mirror_swap(this)"'; // Disable error checking for now.
if ( ! constants.EMOTE_REPLACEMENTS[id] )
srcset = utils.build_srcset(id);

View file

@ -136,14 +136,15 @@ FFZ.settings_info.dark_twitch = {
document.body.classList.toggle("ffz-dark", val);
var model = window.App ? App.__container__.lookup('controller:settings').get('model') : undefined;
var Settings = window.App && App.__container__.lookup('controller:settings'),
settings = Settings.get('settings');
if ( val ) {
this._load_dark_css();
model && this.settings.set('twitch_chat_dark', model.get('darkMode'));
model && model.set('darkMode', true);
settings && this.settings.set('twitch_chat_dark', settings.get('darkMode'));
settings && settings.set('darkMode', true);
} else
model && model.set('darkMode', this.settings.twitch_chat_dark);
settings && settings.set('darkMode', this.settings.twitch_chat_dark);
// Try coloring ReChat
jQuery('.rechat-chat-line').parents('.chat-container').toggleClass('dark', val || this.settings.twitch_chat_dark);
@ -199,7 +200,16 @@ FFZ.prototype.setup_dark = function() {
if ( ! this.settings.dark_twitch )
return;
window.App && App.__container__.lookup('controller:settings').set('model.darkMode', true);
var Settings = window.App && App.__container__.lookup('controller:settings');
if ( Settings ) {
try {
Settings.set('settings.darkMode', true);
} catch(err) {
this.error("Unable to set the darkMode setting because it isn't named what we expect. WTF?");
}
} else
this.error("Unable to load the Ember settings controller.");
this._load_dark_css();
}
@ -214,6 +224,6 @@ FFZ.prototype._load_dark_css = function() {
s.id = "ffz-dark-css";
s.setAttribute('rel', 'stylesheet');
s.setAttribute('href', constants.DIRECT_SERVER + "script/dark" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info));
s.setAttribute('href', constants.SERVER + "script/dark" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info));
document.head.appendChild(s);
}

View file

@ -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 <span>Beta</span>",
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();
}
// --------------------
//
// --------------------

View file

@ -126,7 +126,7 @@ FFZ.prototype.setup_menu = function() {
content.appendChild(p);
a.addEventListener('click', function(e) {
view.set('controller.model.hidden', true);
view.set('controller.settings.hidden', true);
f._last_page = 'settings';
f.build_ui_popup(f._chatv);
e.stopPropagation();
@ -164,11 +164,12 @@ FFZ.prototype.setup_menu = function() {
} catch(err) { }
// Modify all existing Chat Settings views.
for(var key in Ember.View.views) {
if ( ! Ember.View.views.hasOwnProperty(key) )
var views = window.App && App.__container__.lookup('-view-registry:main') || Ember.View.views;
for(var key in views) {
if ( ! views.hasOwnProperty(key) )
continue;
var view = Ember.View.views[key];
var view = views[key];
if ( !(view instanceof Settings) )
continue;

View file

@ -11,7 +11,7 @@ FFZ.prototype.setup_css = function() {
var s = this._main_style = document.createElement('link');
s.id = "ffz-main-css";
s.setAttribute('rel', 'stylesheet');
s.setAttribute('href', constants.DIRECT_SERVER + "script/style" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info));
s.setAttribute('href', constants.SERVER + "script/style" + (constants.DEBUG ? "" : ".min") + ".css?_=" + (constants.DEBUG ? Date.now() : FFZ.version_info));
document.head.appendChild(s);
this.log("Readying toggleable styles.");

View file

@ -955,6 +955,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
/* Menu Scrollbar */
.chatters-container::-webkit-scrollbar,
.ffz-scrollbar::-webkit-scrollbar,
.ember-chat .chat-settings::-webkit-scrollbar,
.conversations-list .conversations-list-inner::-webkit-scrollbar,
@ -967,6 +968,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
width: 6px;
}
.chatters-container::-webkit-scrollbar-thumb,
.ffz-scrollbar::-webkit-scrollbar-thumb,
.ember-chat .chat-settings::-webkit-scrollbar-thumb,
.conversations-list .conversations-list-inner::-webkit-scrollbar-thumb,
@ -987,6 +989,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
.ffz-dark .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb,
.ffz-dark .conversation-input-bar .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb,
.theatre .chatters-container::-webkit-scrollbar-thumb,
.theatre .ffz-scrollbar::-webkit-scrollbar-thumb,
.theatre .ember-chat .chat-settings::-webkit-scrollbar-thumb,
.theatre .conversation-window .conversation-content::-webkit-scrollbar-thumb,
@ -995,8 +998,9 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
.theatre .chat-history::-webkit-scrollbar-thumb,
.theatre .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb,
.theatre .ffz-ui-menu-page::-webkit-scrollbar-thumb,
.theatre .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb
.theatre .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb,
.dark .chatters-container::-webkit-scrollbar-thumb,
.dark .ffz-scrollbar::-webkit-scrollbar-thumb,
.dark .ember-chat .chat-settings::-webkit-scrollbar-thumb,
.dark .chat-history::-webkit-scrollbar-thumb,
@ -1004,6 +1008,7 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
.dark .ffz-ui-menu-page::-webkit-scrollbar-thumb,
.dark .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb,
.force-dark .chatters-container::-webkit-scrollbar-thumb,
.force-dark .ffz-scrollbar::-webkit-scrollbar-thumb,
.force-dark .ember-chat .chat-settings::-webkit-scrollbar-thumb,
.force-dark .chat-history::-webkit-scrollbar-thumb,
@ -1014,6 +1019,12 @@ body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; }
box-shadow: 0 0 1px 1px rgba(0,0,0,0.25);
}
.chatters-container {
overflow-x: hidden !important;
overflow-y: auto !important;
}
/* Fix Moderation Cards */
img.channel_background[src="null"] { display: none; }
@ -1377,7 +1388,7 @@ a.unsafe-link {
}
.ffz-room-list td svg {
margin: 5px;
margin: 5px 10px 5px 0;
float: left;
}
@ -1391,9 +1402,9 @@ a.unsafe-link {
.ffz-room-row:focus svg path,
.ffz-room-row.active svg path { fill: #fff; }
.ffz-room-row:hover td,
.ffz-room-row:focus td,
.ffz-room-row.active td {
.ffz-room-row:hover,
.ffz-room-row:focus,
.ffz-room-row.active {
background-color: #6441A5;
color: #fff !important;
}
@ -1404,7 +1415,7 @@ th.ffz-row-switch {
.ffz-room-row a.leave-chat {
float: right;
margin-right: 12px;
padding: 0 7px;
}
.ffz-row-switch .switch {
@ -1419,6 +1430,8 @@ th.ffz-row-switch {
/* Chat Tabs */
.ember-chat .chat-room { z-index: 5; }
#ffz-group-tabs {
padding: 10px 10px 6px;
box-shadow: inset 0 -1px 0 0 rgba(0,0,0,0.2);
@ -1525,6 +1538,17 @@ th.ffz-row-switch {
margin: 5px 0;
}
.ffz-room-row.row-mentioned {
background-color: rgba(128,50,50,0.1);
color: red !important;
}
.ffz-room-row.row-mentioned:not(.active):hover,
.ffz-room-row.row-mentioned:not(.active):focus {
background-color: #a54141;
color: #fff !important;
}
#ffz-group-tabs .button .notifications {
background-color: #d44949;
top: 0;