var FFZ = window.FrankerFaceZ,
utils = require("../utils"),
constants = require("../constants"),
BAN_SPLIT = /[/\.](?:ban ([^ ]+)|timeout ([^ ]+)(?: (\d+))?)(?: (.*))?$/;
// ---------------------
// Settings
// ---------------------
FFZ.settings_info.alias_italics = {
type: "boolean",
value: true,
category: "Chat Appearance",
no_bttv: true,
name: "Display Aliases in Italics",
help: "Format the names of users that have aliases with italics to make it obvious at a glance that they have been renamed.",
on_update: function(val) {
document.body.classList.toggle('ffz-alias-italics', val);
}
};
FFZ.settings_info.username_display = {
type: "select",
options: {
0: "Username Only",
1: "Capitalization Only",
2: "Display Name Only",
3: "Username in Parenthesis",
4: "Username in Tooltip"
},
category: "Chat Appearance",
no_bttv: true,
name: "Username Display",
help: "How a user's name should be rendered when their display name differs from the username.",
value: 3,
process_value: utils.process_int(3),
on_update: function(val) {
var CL = utils.ember_resolve('component:chat/chat-line'),
views = CL ? utils.ember_views() : [];
for(var vid in views) {
var view = views[vid];
if ( view instanceof CL && view.buildFromHTML ) {
view.$('.from').replaceWith(view.buildFromHTML());
if ( view.get('msgObject.to') )
view.$('.to').replaceWith(view.buildFromHTML(true));
}
}
}
}
FFZ.settings_info.room_status = {
type: "boolean",
value: true,
category: "Chat Appearance",
no_bttv: true,
name: "Room Status Indicators",
help: "Display the current room state (slow mode, sub mode, and r9k mode) next to the Chat button.",
on_update: function() {
if ( this._roomv )
this._roomv.ffzUpdateStatus();
}
};
FFZ.settings_info.replace_bad_emotes = {
type: "boolean",
value: true,
category: "Chat Appearance",
warn_bttv: "Only affects Whispers when BetterTTV is enabled.",
name: "Fix Low Quality Twitch Global Emoticons",
help: "Replace emoticons such as DansGame and RedCoat with cleaned up versions that don't have pixels around the edges or white backgrounds for nicer display on dark chat."
};
FFZ.settings_info.parse_emoticons = {
type: "boolean",
value: true,
category: "Chat Appearance",
no_bttv: true,
name: "Display Emoticons",
help: "Display emoticons in chat messages rather than just text."
};
FFZ.settings_info.parse_emoji = {
type: "select",
options: {
0: "No Images / Font Only",
1: "Twitter Emoji Images",
2: "Google Noto Images",
3: "EmojiOne Images"
},
value: 1,
process_value: utils.process_int(1, 0, 1),
category: "Chat Appearance",
name: "Display Emoji",
help: "Replace emoji in chat messages with nicer looking images from either Twitter or Google."
};
FFZ.settings_info.scrollback_length = {
type: "button",
value: 150,
category: "Chat Appearance",
no_bttv: true,
name: "Scrollback Length",
help: "Set the maximum number of lines to keep in chat.",
method: function() {
var f = this;
utils.prompt(
"Scrollback Length",
"Please enter a new maximum length for the chat scrollback. Please note that setting this too high may cause your computer to begin lagging as chat messages accumulate.
Default: 150",
this.settings.scrollback_length,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
new_val = parseInt(new_val);
if ( Number.isNaN(new_val) || ! Number.isFinite(new_val) )
new_val = 150;
new_val = Math.max(10, new_val);
f.settings.set("scrollback_length", new_val);
// Update our everything.
var Chat = utils.ember_lookup('controller:chat'),
current_id = Chat && Chat.get('currentRoom.id');
for(var room_id in f.rooms) {
var room = f.rooms[room_id];
room.room && room.room.set('messageBufferSize', new_val + ((f._roomv && !f._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0));
}
});
}
};
FFZ.settings_info.hosted_sub_notices = {
type: "boolean",
value: true,
category: "Chat Filtering",
no_bttv: true,
name: "Show Hosted Channel Subscriber Notices",
help: "Display (or more specifically hides when disabled) notices in chat when someone subscribes to the hosted channel."
};
FFZ.settings_info.filter_whispered_links = {
type: "boolean",
value: true,
category: "Chat Filtering",
warn_bttv: "Only affects Whispers when BetterTTV is enabled.",
name: "Auto-Hide Potentially Dangerous Whispered Links",
help: "Removes whispered links and displays a placeholder, with a warning that the link has not been approved by moderation or staff. Links remain accessible with an additional click."
};
FFZ.settings_info.banned_words = {
type: "button",
value: [],
category: "Chat Filtering",
warn_bttv: "Only affects Whispers when BetterTTV is enabled.",
name: "Banned Words",
help: "Set a list of words that will be locally removed from chat messages.",
method: function(e, from_basic) {
var f = this,
old_val = this.settings.banned_words.join("\n"),
input = utils.createElement('textarea');
input.style.marginBottom = "20px";
utils.prompt(
"Banned Words",
"Please enter a list of words or phrases that you would like to have removed from chat messages. One item per line." + (from_basic ? "" : "
Advanced Stuff: If you know regex, you can use regular expressions to match too! Start a line with regex: to trigger that behavior.
(Note: Your expression is wrapped in a capture group and may be joined with other expressions within that group via |. All regular expressions are executed with the flags ig.)
"),
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
var vals = new_val.trim().split(/\s*\n\s*/g),
i = vals.length;
while(i--)
if ( vals[i].length === 0 )
vals.splice(i, 1);
f.settings.set("banned_words", vals);
},
600, input);
}
};
FFZ.settings_info.keywords = {
type: "button",
value: [],
category: "Chat Filtering",
warn_bttv: "Only affects Whispers when BetterTTV is enabled.",
name: "Highlight Keywords",
help: "Set additional keywords that will be highlighted in chat.",
method: function(e, from_basic) {
var f = this,
old_val = this.settings.keywords.join("\n"),
input = utils.createElement('textarea');
input.style.marginBottom = "20px";
utils.prompt(
"Highlight Keywords",
"Please enter a list of words or phrases that you would like to be highlighted in chat. One item per line." + (from_basic ? "" : "Advanced Stuff: If you know regex, you can use regular expressions to match too! Start a line with regex: to trigger that behavior.
(Note: Your expression is wrapped in a capture group and may be joined with other expressions within that group via |. All regular expressions are executed with the flags ig.)
"),
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
// Split them up.
var vals = new_val.trim().split(/\s*\n\s*/g),
i = vals.length;
while(i--)
if ( vals[i].length === 0 )
vals.splice(i,1);
f.settings.set("keywords", vals);
},
600, input);
}
};
FFZ.settings_info.clickable_emoticons = {
type: "boolean",
value: false,
category: "Chat Tooltips",
warn_bttv: "Only affects Whispers when BetterTTV is enabled.",
no_mobile: true,
name: "Emoticon Information Pages",
help: "When enabled, holding shift and clicking on an emoticon will open it on the FrankerFaceZ website or Twitch Emotes."
};
FFZ.settings_info.link_info = {
type: "boolean",
value: true,
category: "Chat Tooltips",
no_bttv: true,
name: "Link Information Beta",
help: "Check links against known bad websites, unshorten URLs, and show YouTube info."
};
FFZ.settings_info.link_image_hover = {
type: "boolean",
value: false,
category: "Chat Tooltips",
no_bttv: true,
no_mobile: true,
name: "Image Preview",
help: "Display image thumbnails for links to Imgur and YouTube."
};
FFZ.settings_info.emote_image_hover = {
type: "boolean",
value: false,
category: "Chat Tooltips",
no_mobile: true,
name: "Emote Preview",
help: "Display scaled up high-DPI emoticon images in tooltips to help see details on low-resolution monitors.",
on_update: function(val) {
this._reset_tooltips();
}
};
FFZ.settings_info.image_hover_all_domains = {
type: "boolean",
value: false,
category: "Chat Tooltips",
no_bttv: true,
no_mobile: true,
name: "Image Preview - All Domains",
help: "Requires Image Preview. Attempt to show an image preview for any URL ending in the appropriate extension. Warning: This may be used to leak your IP address to malicious users."
};
FFZ.settings_info.chat_rows = {
type: "boolean",
value: false,
category: "Chat Appearance",
no_bttv: true,
name: "Chat Line Backgrounds",
help: "Display alternating background colors for lines in chat.",
on_update: function(val) {
this.toggle_style('chat-background', !this.has_bttv && val);
this.toggle_style('chat-setup', !this.has_bttv && (val || this.settings.chat_separators || this.settings.highlight_messages_with_mod_card));
}
};
FFZ.settings_info.chat_separators = {
type: "select",
options: {
0: "Disabled",
1: "Basic Line (1px solid)",
2: "3D Line (2px groove)",
3: "3D Line (2px groove inset)",
4: "Wide Line (2px solid)"
},
value: 0,
process_value: utils.process_int(0, 0, 1),
no_bttv: true,
category: "Chat Appearance",
name: "Chat Line Separators",
help: "Display thin lines between chat messages for further visual separation.",
on_update: function(val) {
this.toggle_style('chat-setup', !this.has_bttv && (val || this.settings.chat_rows || this.settings.highlight_messages_with_mod_card));
this.toggle_style('chat-separator', !this.has_bttv && val);
this.toggle_style('chat-separator-3d', !this.has_bttv && val === 2);
this.toggle_style('chat-separator-3d-inset', !this.has_bttv && val === 3);
this.toggle_style('chat-separator-wide', !this.has_bttv && val === 4);
}
};
FFZ.settings_info.old_sub_notices = {
type: "boolean",
value: false,
category: "Chat Appearance",
no_bttv: true,
name: "Old-Style Subscriber Notices",
help: "Display the old style subscriber notices, with the message on a separate line."
};
FFZ.settings_info.emote_alignment = {
type: "boolean",
value: false,
category: "Chat Appearance",
no_bttv: true,
name: "Baseline Emoticon Alignment",
help: "Align emotes on the text baseline, making messages taller but ensuring emotes don't overlap.",
on_update: function(val) { document.body.classList.toggle('ffz-baseline-emoticons', !this.has_bttv && val) }
};
FFZ.settings_info.chat_padding = {
type: "boolean",
value: false,
category: "Chat Appearance",
no_bttv: true,
name: "Reduced Chat Line Padding",
help: "Reduce the amount of padding around chat messages to fit more on-screen at once.",
on_update: function(val) { this.toggle_style('chat-padding', !this.has_bttv && val); }
};
FFZ.settings_info.high_contrast_chat = {
type: "select",
options: {
'222': "Disabled",
'212': "Bold",
'221': "Text",
'211': "Text + Bold",
'122': "Background",
'121': "Background + Text",
'112': "Background + Bold",
'111': 'All'
},
value: '222',
process_value: function(val) {
if ( val === false )
return '222';
else if ( val === true )
return '111';
return val;
},
category: "Chat Appearance",
name: "High Contrast",
help: "Display chat using white and black for maximum contrast. This is suitable for capturing and chroma keying chat to display on stream.",
on_update: function(val) {
this.toggle_style('chat-hc-text', val[2] === '1');
this.toggle_style('chat-hc-bold', val[1] === '1');
this.toggle_style('chat-hc-background', val[0] === '1');
}
};
FFZ.settings_info.chat_font_family = {
type: "button",
value: null,
category: "Chat Appearance",
no_bttv: true,
name: "Font Family",
help: "Change the font used for rendering chat messages.",
method: function() {
var f = this,
old_val = this.settings.chat_font_family || "";
utils.prompt(
"Chat Font Family",
"Please enter a font family to use rendering chat. Leave this blank to use the default.",
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
// Should we wrap this with quotes?
if ( ! new_val )
new_val = null;
f.settings.set("chat_font_family", new_val);
});
},
on_update: function(val) {
if ( this.has_bttv || ! this._chat_style )
return;
var css;
if ( ! val )
css = "";
else {
// Let's escape this to avoid goofing anything up if there's bad user input.
if ( val.indexOf(' ') !== -1 && val.indexOf(',') === -1 && val.indexOf('"') === -1 && val.indexOf("'") === -1)
val = '"' + val + '"';
var span = document.createElement('span');
span.style.fontFamily = val;
css = ".timestamp-line,.conversation-chat-line,.conversation-system-messages,.chat-history,.ember-chat .chat-messages {" + span.style.cssText + "}";
}
utils.update_css(this._chat_style, "chat_font_family", css);
}
};
FFZ.settings_info.chat_font_size = {
type: "button",
value: 12,
category: "Chat Appearance",
no_bttv: true,
name: "Font Size",
help: "Make the chat font bigger or smaller.",
method: function() {
var f = this,
old_val = this.settings.chat_font_size;
utils.prompt(
"Chat Font Size",
"Please enter a new size for the chat font.
Default: 12",
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
var parsed = parseInt(new_val);
if ( ! parsed || Number.isNaN(parsed) || parsed < 1 )
parsed = 12;
f.settings.set("chat_font_size", parsed);
});
},
on_update: function(val) {
if ( this.has_bttv || ! this._chat_style )
return;
var css;
if ( val === 12 || ! val )
css = "";
else {
var lh = Math.max(20, Math.round((20/12)*val)),
pd = Math.floor((lh - 20) / 2);
css = ".timestamp-line,.conversation-chat-line,.conversation-system-messages,.chat-history .chat-line,.ember-chat .chat-messages .chat-line { font-size: " + val + "px !important; line-height: " + lh + "px !important; }";
if ( pd )
css += ".ember-chat .chat-messages .chat-line .mod-icons, .ember-chat .chat-messages .chat-line .badges { padding-top: " + pd + "px; }";
}
utils.update_css(this._chat_style, "chat_font_size", css);
FFZ.settings_info.chat_ts_size.on_update.call(this, this.settings.chat_ts_size);
}
};
FFZ.settings_info.chat_ts_size = {
type: "button",
value: null,
category: "Chat Appearance",
no_bttv: true,
name: "Timestamp Font Size",
help: "Make the chat timestamp font bigger or smaller.",
method: function() {
var f = this,
old_val = this.settings.chat_ts_size;
if ( ! old_val )
old_val = this.settings.chat_font_size;
utils.prompt(
"Chat Timestamp Font Size",
"Please enter a new size for the chat timestamp font. The default is to match the regular chat font size.",
old_val,
function(new_val) {
if ( new_val === null || new_val === undefined )
return;
var parsed = parseInt(new_val);
if ( parsed < 1 || Number.isNaN(parsed) || ! Number.isFinite(parsed) )
parsed = null;
f.settings.set("chat_ts_size", parsed);
});
},
on_update: function(val) {
if ( this.has_bttv || ! this._chat_style )
return;
var css;
if ( val === null )
css = "";
else {
var lh = Math.max(20, Math.round((20/12)*val), Math.round((20/12)*this.settings.chat_font_size));
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);
}
};
// ---------------------
// Initialization
// ---------------------
FFZ.prototype.setup_line = function() {
// Tipsy Handler
jQuery(document.body).on("mouseleave", ".tipsy", function() {
this.parentElement.removeChild(this);
});
// Aliases
try {
this.aliases = JSON.parse(localStorage.ffz_aliases || '{}');
} catch(err) {
this.log("Error Loading Aliases: " + err);
this.aliases = {};
}
// Chat Style
var s = this._chat_style = document.createElement('style');
s.id = "ffz-style-chat";
s.type = 'text/css';
document.head.appendChild(s);
// Initial calculation.
FFZ.settings_info.chat_font_size.on_update.call(this, this.settings.chat_font_size);
FFZ.settings_info.chat_font_family.on_update.call(this, this.settings.chat_font_family);
// Chat Enhancements
document.body.classList.toggle('ffz-alias-italics', this.settings.alias_italics);
document.body.classList.toggle('ffz-baseline-emoticons', !this.has_bttv && this.settings.emote_alignment);
this.toggle_style('chat-setup', !this.has_bttv && (this.settings.chat_rows || this.settings.chat_separators || this.settings.highlight_messages_with_mod_card));
this.toggle_style('chat-padding', !this.has_bttv && this.settings.chat_padding);
this.toggle_style('chat-background', !this.has_bttv && this.settings.chat_rows);
this.toggle_style('chat-separator', !this.has_bttv && this.settings.chat_separators);
this.toggle_style('chat-separator-3d', !this.has_bttv && this.settings.chat_separators === 2);
this.toggle_style('chat-separator-3d-inset', !this.has_bttv && this.settings.chat_separators === 3);
this.toggle_style('chat-separator-wide', !this.has_bttv && this.settings.chat_separators === 4);
this.toggle_style('chat-hc-text', this.settings.high_contrast_chat[2] === '1');
this.toggle_style('chat-hc-bold', this.settings.high_contrast_chat[1] === '1');
this.toggle_style('chat-hc-background', this.settings.high_contrast_chat[0] === '1');
this._last_row = {};
/*this.log("Hooking the Ember Chat Line component.");
var Line = utils.ember_resolve('component:chat-line');
if ( Line )
this._modify_chat_line(Line);*/
this.log("Hooking the Ember VOD Chat Line component.");
var VOD = utils.ember_resolve('component:vod-chat-line');
if ( VOD )
this._modify_vod_line(VOD);
else
this.log("Couldn't find VOD Chat Line component.");
this.log("Hooking the Ember Message Line component.");
var MLine = utils.ember_resolve('component:chat/message-line');
if ( MLine )
this._modify_chat_subline(MLine);
else
this.error("Couldn't find the Message Line component.");
this.log("Hooking the Ember Whisper Line component.");
var WLine = utils.ember_resolve('component:chat/whisper-line');
if ( WLine )
this._modify_chat_subline(WLine);
else
this.error("Couldn't find the Whisper Line component.");
// Store the capitalization of our own name.
var user = this.get_user();
if ( user && user.name )
FFZ.capitalization[user.login] = [user.name, Date.now()];
}
FFZ.prototype.save_aliases = function() {
this.log("Saving " + Object.keys(this.aliases).length + " aliases to local storage.");
localStorage.ffz_aliases = JSON.stringify(this.aliases);
}
FFZ.prototype._modify_chat_line = function(component, is_vod) {
var f = this,
Layout = utils.ember_lookup('service:layout'),
Settings = utils.ember_settings();
component.reopen({
/*tokenizedMessage: function() {
return [{type: 'text', text: 'hi'}];
}.property('msgObject.message'),*/
ffzTokenizedMessage: function() {
try {
return f.tokenize_chat_line(this.get('msgObject'));
} catch(err) {
f.error("chat-line tokenizedMessage: " + err);
return this._super();
}
}.property("msgObject.message", "isChannelLinksDisabled", "currentUserNick", "msgObject.from", "msgObject.tags.emotes"),
lineChanged: Ember.observer("msgObject.deleted", "isModeratorOrHigher", "msgObject.ffz_old_messages", "ffzTokenizedMessage", function() {
this.$(".mod-icons").replaceWith(this.buildModIconsHTML());
if ( this.get("msgObject.deleted") ) {
this.$(".message").replaceWith(this.buildDeletedMessageHTML());
} else
this.$(".deleted,.message").replaceWith(this.buildMessageHTML());
}),
ffzUpdateBadges: function() {
this.$(".badges").html(f.render_badges(f.get_line_badges(this.get('msgObject'))));
},
ffzUserLevel: function() {
if ( this.get('isStaff') )
return 5;
else if ( this.get('isAdmin') )
return 4;
else if ( this.get('isBroadcaster') )
return 3;
else if ( this.get('isGlobalMod') )
return 2;
else if ( this.get('isModerator') )
return 1;
return 0;
}.property('msgObject.labels.[]'),
buildModIconsHTML: function() {
var user = this.get('msgObject.from'),
room_id = this.get('msgObject.room'),
room = f.rooms && f.rooms[room_id],
deleted = this.get('msgObject.deleted'),
recipient = this.get('msgObject.to'),
is_whisper = recipient && recipient.length,
this_ul = this.get('ffzUserLevel'),
other_ul = room && room.room && room.room.get('ffzUserLevel') || 0,
output;
if ( is_whisper || this_ul >= other_ul || f.settings.mod_buttons.length === 0 )
return '';
output = '';
for(var i=0, l = f.settings.mod_buttons.length; i < l; i++) {
var pair = f.settings.mod_buttons[i],
prefix = pair[0], btn = pair[1], had_label = pair[2], is_emoji = pair[3],
cmd, tip;
if ( is_emoji ) {
var setting = f.settings.parse_emoji,
token = f.emoji_data[is_emoji],
url = null;
if ( token ) {
if ( setting === 1 && token.tw )
url = token.tw_src;
else if ( setting === 2 && token.noto )
url = token.noto_src;
else if ( setting === 3 && token.one )
url = token.one_src;
if ( url )
prefix = '';
}
}
if ( btn === false ) {
if ( deleted )
output += 'Unban';
else
output += '' + (had_label ? prefix : 'Ban') + '';
} else if ( btn === 600 )
output += '' + ( had_label ? prefix : 'Timeout') + '';
else {
if ( typeof btn === "string" ) {
cmd = utils.replace_cmd_variables(btn, {name: user}, room && room.room, this.get('msgObject')).replace(/\s*\s*/g, '\n');
tip = "Custom Command" + (cmd.indexOf("\n") !== -1 ? 's' : '') + ' ' + utils.quote_san(cmd).replace('\n',' ');
} else {
cmd = "/timeout " + user + " " + btn;
tip = "Timeout User (" + utils.duration_string(btn) + ")";
}
output += '' + prefix + '';
}
}
return output + '';
},
buildFromHTML: function(is_recipient) {
var username = this.get(is_recipient ? 'msgObject.to' : 'msgObject.from'),
raw_display = this.get(is_recipient ? 'msgObject.tags.recipient-display-name' : 'msgObject.tags.display-name'),
alias = f.aliases[username],
raw_color = this.get(is_recipient ? 'msgObject.toColor' : 'msgObject.color'),
is_dark = (Layout && Layout.get('isTheatreMode')) || (is_replay ? f.settings.dark_twitch : (Settings && Settings.get('darkMode'))),
is_replay = this.get('ffz_is_replay'),
colors = raw_color && f._handle_color(raw_color),
style = colors ? 'color:' + (is_dark ? colors[1] : colors[0]) : '',
colored = colors ? ' has-color' + (is_replay ? ' replay-color' : '') : '',
results = f.format_display_name(raw_display, username);
return '' + results[0] + '';
},
buildSenderHTML: function() {
var system_msg = this.get('systemMsg'),
output = '';
output = '
';
// System Message
if ( system_msg ) {
output += '