From a050063c812fcbf92a99b4f8e053134fae1a26e2 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Sat, 14 Nov 2015 23:52:49 -0500 Subject: [PATCH] 3.5.66 to 3.5.77. Fixed a performance issue with chat scrolling. Fixed CSS issues introduced in the refactor. Added ReChat support. Fixed all tooltip positioning. Fixed emote usage reporting. Fix support for conversations beta. Fix Do Not Show Again link on portrait mode warning. Fix following data not loading on non-ember pages. Added emoji rendering when BTTV is detected. Made standard chat settings menu scroll. Added support for multiple commands with one button to in-line moderation icons. --- dark.css | 23 +++ src/badges.js | 4 + src/colors.js | 40 +++++ src/constants.js | 3 + src/ember/channel.js | 38 ++-- src/ember/chatview.js | 4 +- src/ember/conversations.js | 66 ++----- src/ember/directory.js | 2 +- src/ember/following.js | 2 +- src/ember/layout.js | 2 +- src/ember/line.js | 35 ++-- src/ember/moderation-card.js | 25 ++- src/ember/player.js | 2 +- src/ember/room.js | 38 +++- src/emoticons.js | 124 ++++++++----- src/ext/betterttv.js | 28 +-- src/ext/rechat.js | 277 ++++++++++++++++++++++++++++++ src/main.js | 28 ++- src/styles/chat-background.css | 12 +- src/styles/chat-hc-background.css | 4 +- src/styles/chat-hc-text.css | 10 +- src/tokenize.js | 116 ++++--------- src/ui/dark.js | 3 + src/ui/following-count.js | 33 +++- src/ui/menu.js | 32 +++- src/ui/my_emotes.js | 16 +- src/ui/sub_count.js | 2 +- src/ui/tooltips.js | 38 ++++ src/ui/viewer_count.js | 2 +- src/utils.js | 107 ++++++++++++ style.css | 44 ++++- 31 files changed, 885 insertions(+), 275 deletions(-) create mode 100644 src/ext/rechat.js create mode 100644 src/ui/tooltips.js diff --git a/dark.css b/dark.css index 696bc969..40e00dd1 100644 --- a/dark.css +++ b/dark.css @@ -902,6 +902,26 @@ /* Conversations */ +.ffz-dark .conversation-input-bar .emoticon-selector-toggle svg path { + fill: rgba(255,255,255,0.2); +} + +.ffz-dark .conversation-input-bar .emoticon-selector-toggle:hover svg path { + fill: rgba(255,255,255,0.5); +} + +.ffz-dark .conversation-input-bar .emoticon-selector-box .emote-set { + border-color: #323232; +} + +.ffz-dark .conversation-input-bar .emoticon-selector-box .emoticon-grid { + background-color: #191919; +} + +.ffz-dark .ember-chat .chat-settings .experimental-options { + border-top-color: rgba(255,255,255, 0.2); +} + .ffz-dark .conversation-settings-menu .options-divider { border-bottom-color: rgba(255,255,255,0.2); } @@ -1012,5 +1032,8 @@ background-color: #6441a5 } +.ffz-dark .conversation-window .timestamp-line span, .ffz-dark .conversation-window .new-message-divider span { background: transparent; } + +.ffz-dark .conversation-window .timestamp-line:after, .ffz-dark .conversation-window .new-message-divider:after { display: none; } \ No newline at end of file diff --git a/src/badges.js b/src/badges.js index 453e0301..1cf59849 100644 --- a/src/badges.js +++ b/src/badges.js @@ -260,6 +260,10 @@ FFZ.prototype.render_badges = function(component, badges) { var user = component.get('msgObject.from') || component.get('message.from.username'), room_id = component.get('msgObject.room') || App.__container__.lookup('controller:chat').get('currentRoom.id'); + return this._render_badges(user, room_id, badges, component); +} + +FFZ.prototype._render_badges = function(user, room_id, badges, component) { var data = this.users[user]; if ( ! data || ! data.badges ) return badges; diff --git a/src/colors.js b/src/colors.js index e05ece22..8b844339 100644 --- a/src/colors.js +++ b/src/colors.js @@ -187,6 +187,43 @@ RGBColor.prototype.eq = function(rgb) { return rgb.r === this.r && rgb.g === this.g && rgb.b === this.b; } +RGBColor.fromCSS = function(rgb) { + rgb = rgb.trim(); + + if ( rgb.charAt(0) === '#' ) + return RGBColor.fromHex(rgb); + + var match = /rgba?\( *(\d+%?) *, *(\d+%?) *, *(\d+%?) *(?:,[^\)]+)?\)/.exec(rgb); + if ( match ) { + var r = match[1], + g = match[2], + b = match[3]; + + if ( r.charAt(r.length-1) === '%' ) + r = 255 * (parseInt(r) / 100); + else + r = parseInt(r); + + if ( g.charAt(g.length-1) === '%' ) + g = 255 * (parseInt(g) / 100); + else + g = parseInt(g); + + if ( b.charAt(b.length-1) === '%' ) + b = 255 * (parseInt(b) / 100); + else + b = parseInt(b); + + return new RGBColor( + Math.min(Math.max(0, r), 255), + Math.min(Math.max(0, g), 255), + Math.min(Math.max(0, b), 255) + ); + } + + return null; +} + RGBColor.fromHex = function(code) { var raw = parseInt(code.charAt(0) === '#' ? code.substr(1) : code, 16); return new RGBColor( @@ -587,6 +624,9 @@ FFZ.prototype._update_colors = function(darkness_only) { FFZ.prototype._handle_color = function(color) { + if ( color instanceof RGBColor ) + color = color.toHex(); + if ( ! color || this._colors.hasOwnProperty(color) ) return this._colors[color]; diff --git a/src/constants.js b/src/constants.js index d8422d59..66f91022 100644 --- a/src/constants.js +++ b/src/constants.js @@ -17,6 +17,8 @@ module.exports = { 2: ["ws://localhost:8001/"] }, + TOOLTIP_DISTANCE: 50, + KNOWN_CODES: { "#-?[\\\\/]": "#-/", ":-?(?:7|L)": ":-7", @@ -42,6 +44,7 @@ module.exports = { "Gr(a|e)yFace": "GrayFace" }, + TWITCH_BASE: 'http://static-cdn.jtvnw.net/emoticons/v1/', EMOTE_MIRROR_BASE: SERVER + "twitch-emote-mirror/", EMOTE_REPLACEMENT_BASE: SERVER + "script/replacements/", diff --git a/src/ember/channel.js b/src/ember/channel.js index 20f02e1e..f7c6f07b 100644 --- a/src/ember/channel.js +++ b/src/ember/channel.js @@ -215,7 +215,13 @@ FFZ.prototype._modify_cindex = function(view) { el.classList.add('ffz-channel'); // Try changing the theater mode tooltip. - this.$('.theatre-button a').attr('title', 'Theater Mode (Alt+T)'); + var tb = this.$('.theatre-button > a'), + opts = tb.data('tipsy'); + + 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'); + this.ffzFixTitle(); this.ffzUpdateUptime(); @@ -283,7 +289,7 @@ FFZ.prototype._modify_cindex = function(view) { if ( ! btn ) { btn = document.createElement('span'); btn.id = 'ffz-ui-host-button'; - btn.className = 'button action tooltip'; + btn.className = 'button action'; btn.addEventListener('click', this.ffzClickHost.bind(btn, this, false)); @@ -295,6 +301,8 @@ FFZ.prototype._modify_cindex = function(view) { container.insertBefore(btn, before); else container.appendChild(btn); + + jQuery(btn).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } btn.classList.remove('disabled'); @@ -321,7 +329,7 @@ FFZ.prototype._modify_cindex = function(view) { if ( ! btn ) { btn = document.createElement('span'); btn.id = 'ffz-ui-host-button'; - btn.className = 'button action tooltip'; + btn.className = 'button action'; btn.addEventListener('click', this.ffzClickHost.bind(btn, this, true)); @@ -333,6 +341,8 @@ FFZ.prototype._modify_cindex = function(view) { container.insertBefore(btn, before); else container.appendChild(btn); + + jQuery(btn).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } btn.classList.remove('disabled'); @@ -406,7 +416,7 @@ FFZ.prototype._modify_cindex = function(view) { else cont.appendChild(stat); - jQuery(stat).tipsy(); + jQuery(stat).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } el.innerHTML = utils.number_commas(chatter_count); @@ -438,7 +448,7 @@ FFZ.prototype._modify_cindex = function(view) { else cont.appendChild(stat); - jQuery(stat).tipsy(); + jQuery(stat).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } el.innerHTML = utils.number_commas(ffz_viewers) + " (" + utils.number_commas(ffz_chatters) + ")"; @@ -473,7 +483,7 @@ FFZ.prototype._modify_cindex = function(view) { if ( ! stat_el ) { stat_el = document.createElement('span'); stat_el.id = 'ffz-ui-player-stats'; - stat_el.className = 'ffz stat tooltip'; + stat_el.className = 'ffz stat'; stat_el.innerHTML = constants.GRAPH + " "; el = document.createElement('span'); @@ -484,16 +494,18 @@ FFZ.prototype._modify_cindex = function(view) { container.insertBefore(stat_el, other.nextSibling); else container.appendChild(stat_el); + + jQuery(stat_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } var delay = parseFloat(stats.hlsLatencyBroadcaster); if ( delay > 180 ) { delay = Math.floor(delay); - stat_el.setAttribute('original-title', 'Video Information\nBroadcast ' + utils.time_to_string(delay, true) + ' Ago\n\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps') + stat_el.setAttribute('original-title', 'Video Information
Broadcast ' + utils.time_to_string(delay, true) + ' Ago

Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps') el.textContent = utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'; } else { - stat_el.setAttribute('original-title', 'Stream Latency\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'); + stat_el.setAttribute('original-title', 'Stream Latency
Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps'); delay = stats.hlsLatencyBroadcaster; var pos = delay.lastIndexOf('.'); @@ -532,7 +544,7 @@ FFZ.prototype._modify_cindex = function(view) { if ( ! stat_el ) { stat_el = document.createElement('span'); stat_el.id = 'ffz-ui-player-stats'; - stat_el.className = 'ffz stat tooltip'; + stat_el.className = 'ffz stat'; stat_el.innerHTML = constants.GRAPH + " "; el = document.createElement('span'); @@ -543,16 +555,18 @@ FFZ.prototype._modify_cindex = function(view) { container.insertBefore(stat_el, other.nextSibling); else container.appendChild(stat_el); + + jQuery(stat_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } var delay = parseFloat(stats.hlsLatencyBroadcaster); if ( delay > 180 ) { delay = Math.floor(delay); - stat_el.setAttribute('original-title', 'Video Information\nBroadcast ' + utils.time_to_string(delay, true) + ' Ago\n\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps') + stat_el.setAttribute('original-title', 'Video Information
Broadcast ' + utils.time_to_string(delay, true) + ' Ago

Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps') el.textContent = utils.time_to_string(Math.floor(delay), true, delay > 172800) + ' old'; } else { - stat_el.setAttribute('original-title', 'Stream Latency\nVideo: ' + stats.videoResolution + 'p @ ' + stats.fps + '\nPlayback Rate: ' + stats.playbackRate + ' Kbps'); + stat_el.setAttribute('original-title', 'Stream Latency
Video: ' + stats.videoResolution + 'p @ ' + stats.fps + '
Playback Rate: ' + stats.playbackRate + ' Kbps'); delay = stats.hlsLatencyBroadcaster; var pos = delay.lastIndexOf('.'); @@ -624,7 +638,7 @@ FFZ.prototype._modify_cindex = function(view) { } } - jQuery(stat).tipsy({html: true}); + jQuery(stat).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } el.innerHTML = utils.time_to_string(uptime, false, false, false, f.settings.stream_uptime === 1 || f.settings.stream_uptime === 3); diff --git a/src/ember/chatview.js b/src/ember/chatview.js index e49f0c1d..9cfee7e4 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -415,7 +415,7 @@ FFZ.prototype._modify_cview = function(view) { ffzInit: function() { f._chatv = this; this.$('.textarea-contain').append(f.build_ui_link(this)); - this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true, gravity: jQuery.fn.tipsy.autoNS}); + this.$('.chat-messages').find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); if ( !f.has_bttv && f.settings.group_tabs ) this.ffzEnableTabs(); @@ -447,7 +447,7 @@ FFZ.prototype._modify_cview = function(view) { var room = this.get('controller.currentRoom'), rows; room && room.resetUnreadCount(); - if ( room._ffz_was_unread ) { + if ( room && room._ffz_was_unread ) { room._ffz_was_unread = false; var el = this.get('element'), diff --git a/src/ember/conversations.js b/src/ember/conversations.js index f73996b2..b9c1ef2a 100644 --- a/src/ember/conversations.js +++ b/src/ember/conversations.js @@ -7,23 +7,11 @@ var FFZ = window.FrankerFaceZ, // Settings // --------------- -FFZ.settings_info.conv_title_clickable = { - type: "boolean", - value: false, - no_mobile: true, - - category: "Conversations", - name: "Clickable Header Name", - help: "Make the conversation header a link that takes you to that person's page.", - on_update: function(val) { - document.body.classList.toggle('ffz-conv-title-clickable', val); - } - }; - FFZ.settings_info.conv_focus_on_click = { type: "boolean", value: false, no_mobile: true, + visible: false, category: "Conversations", name: "Focus Input on Click", @@ -43,19 +31,6 @@ FFZ.settings_info.top_conversations = { } }; -FFZ.settings_info.conv_beta_enable = { - type: "boolean", - value: false, - no_mobile: true, - - category: "Conversations", - name: "Enable Conversations", - help: "Twitch hasn't enabled them yet, but they're in the code for testing. Try them out!", - on_update: function(val) { - App.__container__.lookup('route:application').controller.set('isConversationsEnabled', val); - } - }; - // --------------- // Initialization @@ -63,10 +38,6 @@ FFZ.settings_info.conv_beta_enable = { FFZ.prototype.setup_conversations = function() { document.body.classList.toggle('ffz-top-conversations', this.settings.top_conversations); - document.body.classList.toggle('ffz-conv-title-clickable', this.settings.conv_title_clickable);; - - if ( this.settings.conv_beta_enable ) - App.__container__.lookup('route:application').controller.set('isConversationsEnabled', true); this.log("Hooking the Ember Conversation Window component."); var ConvWindow = App.__container__.resolve('component:conversation-window'); @@ -78,6 +49,9 @@ FFZ.prototype.setup_conversations = function() { var ConvLine = App.__container__.resolve('component:conversation-line'); if ( ConvLine ) this._modify_conversation_line(ConvLine); + + // 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')}); } @@ -88,14 +62,8 @@ FFZ.prototype._modify_conversation_window = function(component) { Settings = App.__container__.lookup('controller:settings'); component.reopen({ - onConversationClick: Ember.on('click', function() { - this.markConversationRead(); - if ( f.settings.conv_focus_on_click ) - this.$(".conversation-input-bar textarea").focus(); - }), - - headerBadges: Ember.computed("conversation.participants", "currentUsername", function() { - var e = this.get("conversation.participants").rejectBy("username", this.get("currentUsername")).objectAt(0), + headerBadges: Ember.computed("thread.participants", "currentUsername", function() { + var e = this.get("thread.participants").rejectBy("username", this.get("currentUsername")).objectAt(0), badges = {}, ut = e.get("userType"); @@ -164,28 +132,18 @@ FFZ.prototype._modify_conversation_window = function(component) { header = el && el.querySelector('.conversation-header'), header_name = header && header.querySelector('.conversation-header-name'), - new_header_name = document.createElement('span'), - raw_color = this.get('otherUser.color'), colors = raw_color && f._handle_color(raw_color), is_dark = (Layout && Layout.get('isTheatreMode')) || f.settings.dark_twitch; - if ( header_name ) { - new_header_name.className = 'conversation-header-name'; - new_header_name.textContent = header_name.textContent; - header.insertBefore(new_header_name, header_name); - - if ( raw_color ) { - header_name.style.color = (is_dark ? colors[1] : colors[0]); - header_name.classList.add('has-color'); - header_name.setAttribute('data-color', raw_color); - - new_header_name.style.color = (is_dark ? colors[1] : colors[0]); - new_header_name.classList.add('has-color'); - new_header_name.setAttribute('data-color', raw_color); - } + if ( header_name && raw_color ) { + header_name.style.color = (is_dark ? colors[1] : colors[0]); + header_name.classList.add('has-color'); + header_name.setAttribute('data-color', raw_color); } + + jQuery(el).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); } }); } diff --git a/src/ember/directory.js b/src/ember/directory.js index f3704d36..eba12dd4 100644 --- a/src/ember/directory.js +++ b/src/ember/directory.js @@ -75,7 +75,7 @@ FFZ.prototype._modify_directory_live = function(dir, is_csgo) { var t_el = this._ffz_uptime = document.createElement('div'); t_el.className = 'overlay_info length live'; - jQuery(t_el).tipsy({html: true}); + jQuery(t_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 's')}); cap.appendChild(t_el); this._ffz_uptime_timer = setInterval(this.ffzUpdateUptime.bind(this), 1000); diff --git a/src/ember/following.js b/src/ember/following.js index a743749e..21d20985 100644 --- a/src/ember/following.js +++ b/src/ember/following.js @@ -159,7 +159,7 @@ FFZ.prototype.setup_profile_following = function() { return false; t_el.className = 'overlay_info length'; - jQuery(t_el).tipsy({html: true}); + jQuery(t_el).tipsy({html: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 's')}); var age = data[0] ? Math.floor((Date.now() - data[0].getTime()) / 1000) : 0; if ( age ) { diff --git a/src/ember/layout.js b/src/ember/layout.js index c77207a7..5da9d789 100644 --- a/src/ember/layout.js +++ b/src/ember/layout.js @@ -228,7 +228,7 @@ FFZ.prototype.setup_layout = function() { return; f._portrait_warning = true; - f.show_message('Twitch\'s Chat Sidebar has been hidden as a result of FrankerFaceZ\'s Portrait Mode because the window is too wide.

Please disable Portrait Mode or make your window narrower.

Do not show this message again'); + f.show_message('Twitch\'s Chat Sidebar has been hidden as a result of FrankerFaceZ\'s Portrait Mode because the window is too wide.

Please disable Portrait Mode or make your window narrower.

Do not show this message again'); }.observes("isTooSmallForRightColumn"), diff --git a/src/ember/line.js b/src/ember/line.js index b2e7b53a..52543528 100644 --- a/src/ember/line.js +++ b/src/ember/line.js @@ -113,7 +113,7 @@ FFZ.settings_info.scrollback_length = { for(var room_id in this.rooms) { var room = this.rooms[room_id]; - room.room.set('messageBufferSize', new_val + ((this._roomv && !this._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0)); + room.room && room.room.set('messageBufferSize', new_val + ((this._roomv && !this._roomv.get('stuckToBottom') && current_id === room_id) ? 150 : 0)); } } }; @@ -149,7 +149,6 @@ FFZ.settings_info.banned_words = { category: "Chat Filtering", no_bttv: true, - //visible: function() { return ! this.has_bttv }, name: "Banned Words", help: "Set a list of words that will be locally removed from chat messages.", @@ -181,7 +180,6 @@ FFZ.settings_info.keywords = { category: "Chat Filtering", no_bttv: true, - //visible: function() { return ! this.has_bttv }, name: "Highlight Keywords", help: "Set additional keywords that will be highlighted in chat.", @@ -246,6 +244,21 @@ FFZ.settings_info.link_image_hover = { }; +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, @@ -402,7 +415,7 @@ FFZ.settings_info.chat_font_family = { var span = document.createElement('span'); span.style.fontFamily = val; - css = ".ember-chat .chat-messages {" + span.style.cssText + "}"; + 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); @@ -444,7 +457,7 @@ FFZ.settings_info.chat_font_size = { else { var lh = Math.max(20, Math.round((20/12)*val)), pd = Math.floor((lh - 20) / 2); - css = ".ember-chat .chat-messages .chat-line { font-size: " + val + "px !important; line-height: " + lh + "px !important; }"; + 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; }"; } @@ -492,7 +505,7 @@ FFZ.settings_info.chat_ts_size = { css = ""; else { var lh = Math.max(20, Math.round((20/12)*val), Math.round((20/12)*this.settings.chat_font_size)); - css = ".ember-chat .chat-messages .timestamp { font-size: " + val + "px !important; line-height: " + lh + "px !important; }"; + css = ".chat-history .timestamp,.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); @@ -607,11 +620,13 @@ FFZ.prototype._modify_line = function(component) { if ( e.target.classList.contains('custom') ) { var room_id = this.get('msgObject.room'), room = room_id && f.rooms[room_id] && f.rooms[room_id].room, - cmd = e.target.getAttribute('data-cmd'); if ( room ) { - room.send(cmd, true); + var lines = cmd.split("\n"); + for(var i=0; i < lines.length; i++) + room.send(lines[i], true); + if ( e.target.classList.contains('is-timeout') ) room.clearMessages(this.get('msgObject.from')); } @@ -683,8 +698,8 @@ FFZ.prototype._modify_line = function(component) { else { if ( typeof btn === "string" ) { - cmd = btn.replace(/{user}/g, user); - tip = 'Custom Command\n' + cmd; + cmd = btn.replace(/{user}/g, user).replace(/ * */, "\n"); + tip = 'Custom Command' + (cmd.indexOf('\n') !== -1 ? 's' : '') + '\n' + cmd; } else { cmd = "/timeout " + user + " " + btn; tip = "Timeout User (" + utils.duration_string(btn) + ")"; diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index 536dafc8..dda690af 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -182,7 +182,7 @@ FFZ.settings_info.mod_buttons = { old_val += ' ' + prefix + cmd; } - var new_val = prompt("Custom In-Line Moderation Icons\n\nPlease enter a list of commands to be made available as mod icons within chat lines. Commands are separated by spaces. To include spaces in a command, surround the command with double quotes (\"). Use \"{user}\" to insert the user's username into the command, otherwise it will be appended to the end.\n\nExample: !permit \"!reg add {user}\"\n\nNumeric values will become timeout buttons for that number of seconds. The text \"\" is a special value that will act like the normal Ban button in chat.\n\nTo assign a specific letter for use as the icon, specify it at the start of the command followed by an equals sign.\n\nExample: A=\"!reg add\"\n\nDefault: 600", old_val); + var new_val = prompt("Custom In-Line Moderation Icons\n\nPlease enter a list of commands to be made available as mod icons within chat lines. Commands are separated by spaces. To include spaces in a command, surround the command with double quotes (\"). Use \"{user}\" to insert the user's username into the command, otherwise it will be appended to the end.\n\nExample: !permit \"!reg add {user}\"\n\nTo send multiple commands, separate them with \"\".\n\nNumeric values will become timeout buttons for that number of seconds. The text \"\" is a special value that will act like the normal Ban button in chat.\n\nTo assign a specific letter for use as the icon, specify it at the start of the command followed by an equals sign.\n\nExample: A=\"!reg add\"\n\nDefault: 600", old_val); if ( new_val === null || new_val === undefined ) return; @@ -253,8 +253,15 @@ FFZ.settings_info.mod_buttons = { } else had_prefix = true; - if ( typeof val === "string" && val.indexOf('{user}') === -1 ) - val += ' {user}'; + if ( typeof val === "string" ) { + // Split it up for this step. + var lines = val.split(/ * */); + for(var x=0; x < lines.length; x++) { + if ( lines[x].indexOf('{user}') === -1 ) + lines[x] += ' {user}'; + } + val = lines.join(""); + } final.push([prefix, val, had_prefix]); } @@ -475,7 +482,7 @@ FFZ.prototype.setup_mod_card = function() { if ( name ) { name.classList.add('ffz-alias'); name.title = utils.sanitize(controller.get('cardInfo.user.display_name') || user_id.capitalize()); - jQuery(name).tipsy(); + jQuery(name).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } } @@ -522,7 +529,7 @@ FFZ.prototype.setup_mod_card = function() { btn.innerHTML = utils.sanitize(title); btn.title = utils.sanitize(cmd.replace(/{user}/g, controller.get('cardInfo.user.id') || '{user}')); - jQuery(btn).tipsy(); + jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); btn.addEventListener('click', add_btn_click.bind(this, cmd)); return btn; }; @@ -601,7 +608,7 @@ FFZ.prototype.setup_mod_card = function() { else if ( f.settings.mod_card_hotkeys && timeout === 1 ) btn.title = "(P)urge - " + btn.title; - jQuery(btn).tipsy(); + jQuery(btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); btn.addEventListener('click', btn_click.bind(this, timeout)); return btn; @@ -637,7 +644,7 @@ FFZ.prototype.setup_mod_card = function() { unban_btn.innerHTML = CHECK; unban_btn.title = (f.settings.mod_card_hotkeys ? "(U)" : "U") + "nban User"; - jQuery(unban_btn).tipsy(); + jQuery(unban_btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); unban_btn.addEventListener("click", btn_click.bind(this, -1)); jQuery(ban_btn).after(unban_btn); @@ -661,7 +668,7 @@ FFZ.prototype.setup_mod_card = function() { msg_btn.classList.add('message'); msg_btn.title = "Whisper User"; - jQuery(msg_btn).tipsy(); + jQuery(msg_btn).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); var real_msg = document.createElement('button'); @@ -846,7 +853,7 @@ FFZ.prototype._build_mod_card_history = function(line) { // Interactivity jQuery('a.deleted-link', l_el).click(f._deleted_link_click); jQuery('img.emoticon', l_el).click(function(e) { f._click_emote(this, e) }); - jQuery('.html-tooltip', l_el).tipsy({html:true}); + jQuery('.html-tooltip', l_el).tipsy({html:true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 's')}); return l_el; } diff --git a/src/ember/player.js b/src/ember/player.js index 67c8e888..245dadaf 100644 --- a/src/ember/player.js +++ b/src/ember/player.js @@ -61,7 +61,7 @@ FFZ.prototype.setup_player = function() { this.players = {}; - var Player2 = App && App.__container__.resolve('component:twitch-player2'); + var Player2 = window.App && App.__container__.resolve('component:twitch-player2'); if ( ! Player2 ) return this.log("Unable to find twitch-player2 component."); diff --git a/src/ember/room.js b/src/ember/room.js index 885366d7..d045f5c1 100644 --- a/src/ember/room.js +++ b/src/ember/room.js @@ -134,6 +134,15 @@ FFZ.prototype._modify_rview = function(view) { this._super(); }, + ffzAlternate: function() { + /*if ( ! this._ffz_chat_display ) { + var el = this.get('element'); + this._ffz_chat_display = el && el.querySelector('ul.chat-lines'); + } + + this._ffz_chat_display && this._ffz_chat_display.classList.toggle('ffz-should-alternate');*/ + }, + ffzInit: function() { f._roomv = this; @@ -179,6 +188,9 @@ FFZ.prototype._modify_rview = function(view) { if ( f._roomv === this ) f._roomv = undefined; + if ( this._ffz_chat_display ) + this._ffz_chat_display = undefined; + this.ffzDisableFreeze(); }, @@ -367,12 +379,12 @@ FFZ.prototype._modify_rview = function(view) { } }, - ffzUnfreeze: function() { + ffzUnfreeze: function(from_stuck) { this.ffz_frozen = false; this._ffz_last_move = 0; this.ffzUnwarnPaused(); - if ( this.get('stuckToBottom') ) + if ( ! from_stuck && this.get('stuckToBottom') ) this._scrollToBottom(); }, @@ -434,7 +446,7 @@ FFZ.prototype._modify_rview = function(view) { this.set("stuckToBottom", val); this.get("controller.model") && this.set("controller.model.messageBufferSize", f.settings.scrollback_length + (val ? 0 : 150)); if ( ! val ) - this.ffzUnfreeze(); + this.ffzUnfreeze(true); }, // Warnings~! @@ -728,6 +740,7 @@ FFZ.prototype._insert_history = function(room_id, data) { tmiSession = r.tmiSession || (TMI._sessions && TMI._sessions[0]), tmiRoom = r.tmiRoom, + removed = 0, inserted = 0, purged = {}, @@ -828,10 +841,15 @@ FFZ.prototype._insert_history = function(room_id, data) { this.tokenize_chat_line(msg, true, r.get('roomProperties.hide_chat_links')); if ( r.shouldShowMessage(msg) ) { messages.insertAt(inserted, msg); - while( messages.length > r.get('messageBufferSize') ) + while( messages.length > r.get('messageBufferSize') ) { messages.removeAt(0); + removed++; + } } } + + if ( (removed % 2) && this._roomv && this._roomv.get('context.model.id') === room_id ) + this._roomv.ffzAlternate(); } @@ -995,7 +1013,8 @@ FFZ.prototype._modify_room = function(room) { var msgs = t.get('messages'), total = msgs.get('length'), - i = total; + i = total, + removed = 0; // Delete visible messages while(i--) { @@ -1004,6 +1023,7 @@ FFZ.prototype._modify_room = function(room) { if ( msg.from === user ) { if ( f.settings.remove_deleted ) { msgs.removeAt(i); + removed++; continue; } @@ -1013,6 +1033,9 @@ FFZ.prototype._modify_room = function(room) { } } + if ( (removed % 2) && f._roomv && f._roomv.get('context.model.id') === this.get('id') ) + f._roomv.ffzAlternate(); + // Delete pending messages if (t.ffzPending) { msgs = t.ffzPending; @@ -1064,8 +1087,11 @@ FFZ.prototype._modify_room = function(room) { len = messages.get("length"), limit = this.get("messageBufferSize"); - if ( len > limit ) + if ( len > limit ) { messages.removeAt(0, len - limit); + if ( ((len - limit) % 2) && f._roomv && f._roomv.get('context.model.id') === this.get('id') ) + f._roomv.ffzAlternate(); + } }, // Artificial chat delay diff --git a/src/emoticons.js b/src/emoticons.js index e7b7c9be..34bcea8a 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -4,46 +4,11 @@ var FFZ = window.FrankerFaceZ, constants = require('./constants'), utils = require('./utils'), - - /*check_margins = function(margins, height) { - var mlist = margins.split(/ +/); - if ( mlist.length != 2 ) - return margins; - - mlist[0] = parseFloat(mlist[0]); - mlist[1] = parseFloat(mlist[1]); - - if ( mlist[0] == (height - 18) / -2 && mlist[1] == 0 ) - return null; - - return margins; - }, - - - build_legacy_css = function(emote) { - var margin = emote.margins, srcset = ""; - if ( ! margin ) - margin = ((emote.height - 18) / -2) + "px 0"; - - if ( emote.urls[2] || emote.urls[4] ) { - srcset = 'url("' + emote.urls[1] + '") 1x'; - if ( emote.urls[2] ) - srcset += ', url("' + emote.urls[2] + '") 2x'; - if ( emote.urls[4] ) - srcset += ', url("' + emote.urls[4] + '") 4x'; - - srcset = '-webkit-image-set(' + srcset + '); image-set(' + srcset + ');'; - } - - return ".ffz-emote-" + emote.id + ' { background-image: url("' + emote.urls[1] + '"); height: ' + emote.height + "px; width: " + emote.width + "px; margin: " + margin + (srcset ? '; ' + srcset : '') + (emote.css ? "; " + emote.css : "") + "}\n"; - },*/ - - build_css = function(emote) { if ( ! emote.margins && ! emote.css ) - return ""; //build_legacy_css(emote); + return ""; - return /*build_legacy_css(emote) +*/ 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n"; + return 'img[src="' + emote.urls[1] + '"] { ' + (emote.margins ? "margin: " + emote.margins + ";" : "") + (emote.css || "") + " }\n"; }, @@ -120,8 +85,15 @@ FFZ.prototype.setup_emoticons = function() { // Emote Usage // ------------------------ -FFZ.prototype.add_usage = function(room_id, emote_id, count) { - var rooms = this.emote_usage[emote_id] = this.emote_usage[emote_id] || {}; +FFZ.prototype.add_usage = function(room_id, emote, count) { + // Only report usage from FFZ emotes. Not extensions to FFZ. + var emote_set = this.emote_sets[emote.set_id]; + if ( ! emote_set || emote_set.source_ext ) + return; + + var emote_id = emote.id, + rooms = this.emote_usage[emote_id] = this.emote_usage[emote_id] || {}; + rooms[room_id] = (rooms[room_id] || 0) + (count || 1); if ( this._emote_report_scheduled ) @@ -260,12 +232,82 @@ FFZ.prototype._emote_tooltip = function(emote) { var set = this.emote_sets[emote.set_id], owner = emote.owner, title = set && set.title || "Global", - source = set && set.source || "FFZ"; + source = set && set.source || "FFZ", - emote._tooltip = "Emoticon: " + (emote.hidden ? "???" : emote.name) + "
" + source + " " + title + (owner ? "
By: " + owner.display_name : ""); + preview_url = this.settings.emote_image_hover ? (emote.urls[4] || emote.urls[2]) : null, + image = preview_url ? '' : ''; + + emote._tooltip = image + "Emoticon: " + (emote.hidden ? "???" : emote.name) + "
" + source + " " + title + (owner ? "
By: " + owner.display_name : ""); return emote._tooltip; } +FFZ.prototype._reset_tooltips = function(twitch_only) { + for(var emote_id in this._twitch_emotes) { + var data = this._twitch_emotes[emote_id]; + if ( data && data.tooltip ) + data.tooltip = null; + } + + if ( ! twitch_only ) { + for(var set_id in this.emote_sets) { + var emote_set = this.emote_sets[set_id]; + for(var emote_id in emote_set.emoticons) { + var emote = emote_set.emoticons[emote_id]; + if ( emote._tooltip ) + emote._tooltip = null; + } + } + } + + var emotes = document.querySelectorAll('img.emoticon'); + for(var i=0; i < emotes.length; i++) { + var emote = emotes[i]; + if ( emote.classList.contains('ffz-image-hover') ) + continue; + + var set_id, + emote_id = emote.getAttribute('data-emote'); + + if ( emote_id ) { + // Twitch Emotes + if ( this.has_bttv ) + continue; + + emote.setAttribute('original-title', utils.build_tooltip.bind(this)(emote_id, false, emote.alt)); + continue; + } + + if ( twitch_only ) + continue; + + // FFZ Emoji + emote_id = emote.getAttribute('data-ffz-emoji'); + if ( emote_id ) { + var emoji = this.emoji_data && this.emoji_data[emote_id], + setting = this.settings.parse_emoji, + + src = emoji ? (setting === 2 ? emoji.noto_src : emoji.tw_src) : null, + image = ''; + + if ( src && this.settings.emote_image_hover ) + image = ''; + + emote.setAttribute('original-title', emoji ? (image + 'Emoji: ' + emote.alt + '
Name: ' + emoji.name + (emoji.short_name ? "
Short Name: :" + emoji.short_name + ":" : "")) : emote.alt); + continue; + } + + // FFZ Emotes + emote_id = emote.getAttribute('data-ffz-emote'); + set_id = emote.getAttribute('data-ffz-set'); + + var emote_set = this.emote_sets[set_id]; + if ( ! emote_set || ! emote_set.emoticons || ! emote_set.emoticons[emote_id] ) + continue; + + emote.setAttribute('original-title', this._emote_tooltip(emote_set.emoticons[emote_id])); + } +} + // --------------------- // Emoji Loading diff --git a/src/ext/betterttv.js b/src/ext/betterttv.js index 06d2637c..8a421127 100644 --- a/src/ext/betterttv.js +++ b/src/ext/betterttv.js @@ -25,8 +25,6 @@ FFZ.prototype.setup_bttv = function(delay) { this.log("BetterTTV was detected after " + delay + "ms. Hooking."); this.has_bttv = true; - // this.track('setCustomVariable', '3', 'BetterTTV', BetterTTV.info.versionString()); - // Disable Dark if it's enabled. document.body.classList.remove("ffz-dark"); if ( this._dark_style ) { @@ -224,14 +222,14 @@ FFZ.prototype.setup_bttv = function(delay) { // Why is emote parsing so bad? ;_; _.each(emotes, function(emote) { var tooltip = f._emote_tooltip(emote), - eo = [''], + eo = [''], old_tokens = tokens; tokens = []; for(var i=0; i < old_tokens.length; i++) { var token = old_tokens[i]; - if ( typeof token != "string" ) { + if ( typeof token !== "string" ) { tokens.push(token); continue; } @@ -248,7 +246,7 @@ FFZ.prototype.setup_bttv = function(delay) { tokens.push(eo); if ( mine && l_room ) - f.add_usage(l_room, emote.id); + f.add_usage(l_room, emote); } else tokens.push(bit); @@ -258,9 +256,10 @@ FFZ.prototype.setup_bttv = function(delay) { } // Sneak in Emojicon Processing - /* if ( f.settings.parse_emoji && f.emoji_data ) { - var old_tokens = tokens; + var old_tokens = tokens, + setting = f.settings.parse_emoji; + tokens = []; for(var i=0; i < old_tokens.length; i++) { @@ -280,20 +279,25 @@ FFZ.prototype.setup_bttv = function(delay) { variant = tbits.shift(); if ( variant === '\uFE0E' ) - bits.push(match); + tokens.push(match); else { var eid = utils.emoji_to_codepoint(match, variant), - data = f.emoji_data[eid]; + data = f.emoji_data[eid], + src = data && (setting === 2 ? data.noto_src : data.tw_src); - if ( data ) { - tokens.push(['' + alt + '']); + if ( data && src ) { + var image = src && f.settings.emote_image_hover ? '' : '', + tooltip = image + "Emoji: " + data.raw + "
Name: " + data.name + (data.short_name ? "
Short Name: :" + data.short_name + ":" : ""), + code = utils.quote_attr(data.raw); + + tokens.push(['' + code + '']); } else tokens.push(match + (variant || "")); } } } } - }*/ + } return tokens; } diff --git a/src/ext/rechat.js b/src/ext/rechat.js new file mode 100644 index 00000000..f024a7f8 --- /dev/null +++ b/src/ext/rechat.js @@ -0,0 +1,277 @@ +var FFZ = window.FrankerFaceZ, + constants = require('../constants'), + utils = require('../utils'); + + +// -------------------- +// Initialization +// -------------------- + +FFZ.prototype.setup_rechat = function() { + if ( this.has_bttv || navigator.userAgent.indexOf('Android') !== -1 ) + return; + + this._rechat_listening = false; + + this.log("Installing ReChat mutation observer."); + + var f = this; + this._rechat_observer = new MutationObserver(function(mutations) { + for(var i=0; i < mutations.length; i++) { + var mutation = mutations[i]; + if ( mutation.type !== "childList" ) + continue; + + for(var x=0; x < mutation.addedNodes.length; x++) { + var added = mutation.addedNodes[x]; + if ( added.nodeType !== added.ELEMENT_NODE || added.tagName !== "DIV" ) + continue; + + // Is this a ReChat line? + if ( added.classList.contains('rechat-chat-line') && ! added.classList.contains('ffz-processed') ) + f.process_rechat_line(added); + } + } + }); + + this.log("Starting ReChat check loop."); + this._rechat_interval = setInterval(this.find_rechat.bind(this), 1000); + this.find_rechat(); +} + + +// -------------------- +// ReChat Detection +// -------------------- + +FFZ.prototype.find_rechat = function() { + var el = !this.has_bttv ? document.querySelector('.rechat-chat-line') : null; + + // If there's no change, don't continue. + if ( !!el === this._rechat_listening ) + return; + + // If we're no longer listening, stop the observer and quit. + if ( ! el ) { + this._rechat_observer.disconnect(); + this._rechat_listening = false; + return; + } + + // We're newly listening. Process all existing ReChat chat lines + // and darken the container if required, also enable the observer. + var container = jQuery(el).parents('.chat-container'); + if ( ! container.length ) + return; + + container = container[0]; + + // 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'); + + // Tooltips + jQuery(container).find('.tooltip').tipsy({live: true, gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); + jQuery(container).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 'n')}); + + // Load the room data. + var room_id = el.getAttribute('data-room'); + if ( room_id && ! this.rooms[room_id] ) + this.load_room(room_id, this._reprocess_rechat.bind(this, container)); + + // Do stuff. + var lines = container.querySelectorAll('.rechat-chat-line'); + for(var i=0; i < lines.length; i++) { + var line = lines[i]; + if ( line.classList.contains('ffz-processed') ) + continue; + + this.process_rechat_line(line); + } + + // Start observing. + this._rechat_observer.observe(container, { + childList: true, + subtree: true + }); + + this._rechat_listening = true; +} + + +// -------------------- +// ReChat Lines +// -------------------- + +FFZ.prototype._reprocess_rechat = function(container) { + var lines = container.querySelectorAll('.rechat-chat-line'); + for(var i=0; i < lines.length; i++) + this.process_rechat_line(lines[i], true); +} + + +FFZ.prototype.process_rechat_line = function(line, reprocess) { + if ( ! reprocess && line.classList.contains('ffz-processed') ) + return; + + line.classList.add('ffz-processed'); + + var user_id = line.getAttribute('data-sender'), + room_id = line.getAttribute('data-room'), + + Layout = App.__container__.lookup('controller:layout'), + Settings = App.__container__.lookup('controller:settings'), + is_dark = (Layout && Layout.get('isTheatreMode')) || (Settings && Settings.get('model.darkMode')), + + badges_el = line.querySelector('.badges'), + from_el = line.querySelector('.from'), + message_el = line.querySelector('.message'), + + badges = {}, + had_badges = !!badges_el, + + raw_color = from_el && FFZ.Color.RGB.fromCSS(from_el.style.color), + colors = raw_color && this._handle_color(raw_color), + + alias = this.aliases[user_id]; + + + if ( ! badges_el ) { + badges_el = document.createElement('span'); + badges_el.className = 'badges float-left'; + line.insertBefore(badges_el, from_el || line.firstElementChild); + } + + if ( ! reprocess || ! had_badges ) { + // Read existing known badges. + var existing = badges_el.querySelectorAll('.badge'); + for(var i=0; i < existing.length; i++) { + var badge = existing[i]; + if ( badge.classList.contains('broadcaster') ) + badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; + else if ( badge.classList.contains('staff') ) + badges[0] = {klass: 'staff', title: 'Staff'}; + else if ( badge.classList.contains('admin') ) + badges[0] = {klass: 'admin', title: 'Admin'}; + else if ( badge.classList.contains('global-moderator') ) + badges[0] = {klass: 'global-moderator', title: 'Global Moderator'}; + else if ( badge.classList.contains('moderator') ) + badges[0] = {klass: 'moderator', title: 'Moderator'}; + else if ( badge.classList.contains('subscriber') ) + badges[10] = {klass: 'subscriber', title: 'Subscriber'}; + else if ( badge.classList.contains('turbo') ) + badges[15] = {klass: 'turbo', title: 'Turbo'}; + } + + if ( user_id && user_id === room_id ) + badges[0] = {klass: 'broadcaster', title: 'Broadcaster'}; + + if ( user_id ) + badges = this._render_badges(user_id, room_id, badges); + + var output = ''; + for(var key in badges) { + var badge = badges[key], + css = badge.iamge ? 'background-image:url("' + badge.image + '");' : ''; + + if ( badge.color ) + css += 'background-color:' + badge.color + ';'; + + if ( badge.extra_css ) + css += badge.extra_css; + + output += '
'; + } + + badges_el.innerHTML = output; + } + + if ( ! reprocess && from_el ) { + from_el.style.fontWeight = ""; + if ( colors ) { + from_el.classList.add('has_color'); + from_el.style.color = is_dark ? colors[1] : colors[0]; + } + + if ( alias ) { + from_el.classList.add('ffz-alias'); + from_el.title = from_el.textContent; + from_el.textContent = alias; + } + } + + if ( ! message_el ) + return; + + if ( ! reprocess && message_el.style.color ) { + message_el.classList.add('has-color'); + message_el.style.color = is_dark ? colors[1] : colors[0]; + } + + var raw_tokens = line.getAttribute('data-tokens'), + tokens = raw_tokens ? JSON.parse(raw_tokens) : []; + + if ( ! raw_tokens ) { + for(var i=0; i < message_el.childNodes.length; i++) { + var node = message_el.childNodes[i]; + + if ( node.nodeType === node.TEXT_NODE ) + tokens.push(node.textContent); + + else if ( node.nodeType === node.ELEMENT_NODE ) { + if ( node.tagName === 'IMG' ) + tokens.push({ + altText: node.alt, + emoticonSrc: node.src + }); + + else if ( node.tagName === 'A' ) + tokens.push({ + isLink: true, + href: node.textContent + }); + + else if ( node.tagName === 'SPAN' ) + tokens.push({ + mentionedUser: node.textContent, + own: node.classList.contains('mentioning') + }); + + else + this.log("Unknown Tag Type: " + node.tagName); + } else + this.log("Unknown Node Type Tokenizing Message: " + node.nodeType); + } + } + + line.setAttribute('data-tokens', JSON.stringify(tokens)); + + // Further tokenization~! + if ( this.settings.replace_bad_emotes ) + tokens = this.tokenize_replace_emotes(tokens); + + tokens = this._remove_banned(tokens); + tokens = this.tokenize_emotes(user_id, room_id, tokens, false); + + if ( this.settings.parse_emoji ) + tokens = this.tokenize_emoji(tokens); + + tokens = this.tokenize_mentions(tokens); + + // Check for a mention + if ( ! line.classList.contains('ffz-mentioend') ) + for(var i=0; i < tokens.length; i++) + if ( tokens[i].mentionedUser ) { + line.classList.add('ffz-mentioned'); + break; + } + + // Now, put the content back into the element. + message_el.innerHTML = this.render_tokens(tokens); +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index ed9f7ae0..666649d3 100644 --- a/src/main.js +++ b/src/main.js @@ -22,7 +22,7 @@ FFZ.get = function() { return FFZ.instance; } // Version var VER = FFZ.version_info = { - major: 3, minor: 5, revision: 65, + major: 3, minor: 5, revision: 77, toString: function() { return [VER.major, VER.minor, VER.revision].join(".") + (VER.extra || ""); } @@ -90,12 +90,22 @@ FFZ.prototype._pastebin = function(data, callback) { // ------------------- FFZ.prototype.get_user = function() { - if ( window.PP && PP.login ) { - return PP; - } else if ( window.App ) { - var nc = App.__container__.lookup("controller:login"); - return nc ? nc.get("userData") : undefined; + if ( this.__user ) + return this.__user; + + var user; + if ( window.App ) { + var nc = App.__container__.lookup('controller:login'); + user = nc ? nc.get('userData') : undefined; } + + if ( ! user && window.PP && PP.login ) + user = PP; + + if ( user ) + this.__user = user; + + return user; } @@ -133,6 +143,7 @@ require('./ember/following'); require('./debug'); +require('./ext/rechat'); require('./ext/betterttv'); require('./ext/emote_menu'); @@ -140,6 +151,7 @@ require('./featurefriday'); require('./ui/styles'); require('./ui/dark'); +require('./ui/tooltips'); require('./ui/notifications'); require('./ui/viewer_count'); require('./ui/sub_count'); @@ -256,6 +268,7 @@ FFZ.prototype.init_normal = function(delay, no_socket) { this.setup_following_count(false); this.setup_menu(); + this.fix_tooltips(); this.find_bttv(10); var end = (window.performance && performance.now) ? performance.now() : Date.now(), @@ -297,6 +310,7 @@ FFZ.prototype.init_dashboard = function(delay) { // Set up the FFZ message passer. this.setup_message_event(); + this.fix_tooltips(); this.find_bttv(10); var end = (window.performance && performance.now) ? performance.now() : Date.now(), @@ -354,8 +368,10 @@ FFZ.prototype.init_ember = function(delay) { this.setup_following_count(true); this.setup_races(); + this.fix_tooltips(); this.connect_extra_chat(); + this.setup_rechat(); this.find_bttv(10); this.find_emote_menu(10); diff --git a/src/styles/chat-background.css b/src/styles/chat-background.css index 1b6246bc..8f162569 100644 --- a/src/styles/chat-background.css +++ b/src/styles/chat-background.css @@ -1,7 +1,10 @@ /* Regular Alternating Background */ .conversation-chat-lines > div:nth-child(2n+0):before, .chat-history .chat-line:nth-child(2n+0):before, -.ember-chat .chat-lines > div:nth-child(2n+0) .chat-line:before { +.ember-chat .chat-lines > div:nth-child(2n+0) .chat-line:before, + +/* ReChat */ +.ember-chat.chat-messages > .rechat-chat-line:nth-child(2n+0):before { background-color: rgba(0,0,0, 0.1); } @@ -17,7 +20,12 @@ .dark .chat-history .chat-line:nth-child(2n+0):before, .force-dark .chat-history .chat-line:nth-child(2n+0):before, .dark .chat-lines > div:nth-child(2n+0) .chat-line:before, -.force-dark .chat-lines > div:nth-child(2n+0) .chat-line:before { +.force-dark .chat-lines > div:nth-child(2n+0) .chat-line:before, + +/* ReChat */ +.theatre .chat-lines > .rechat-chat-line:nth-child(2n+0):before, +.dark .chat-lines > .rechat-chat-line:nth-child(2n+0):before, +.force-dark .chat-lines > .rechat-chat-line:nth-child(2n+0):before { background-color: rgba(255,255,255, 0.05); } diff --git a/src/styles/chat-hc-background.css b/src/styles/chat-hc-background.css index 71ff193a..87096e93 100644 --- a/src/styles/chat-hc-background.css +++ b/src/styles/chat-hc-background.css @@ -1,7 +1,7 @@ /* High-Contrast Background */ .chat-container, .ember-chat-container { - background-color: #fff; + background-color: #fff !important; } @@ -12,5 +12,5 @@ .chat-container.force-dark, .ember-chat-container.dark, .ember-chat-container.force-dark { - background-color: #000; + background-color: #000 !important; } \ No newline at end of file diff --git a/src/styles/chat-hc-text.css b/src/styles/chat-hc-text.css index 687bd48c..867b0e6e 100644 --- a/src/styles/chat-hc-text.css +++ b/src/styles/chat-hc-text.css @@ -1,7 +1,7 @@ /* High-Contrast Text */ .chat-container, .ember-chat-container { - color: #000; + color: #000 !important; } /* Dark: High-Contrast Text */ @@ -10,6 +10,10 @@ .chat-container.dark, .chat-container.force-dark, .ember-chat-container.dark, -.ember-chat-container.force-dark { - color: #fff; +.ember-chat-container.force-dark, + +.ffz-dark .ember-chat-container.dark .chat-line, +.ffz-dark .chat-container.dark .chat-line + { + color: #fff !important; } \ No newline at end of file diff --git a/src/tokenize.js b/src/tokenize.js index 1f3d1d51..bd877874 100644 --- a/src/tokenize.js +++ b/src/tokenize.js @@ -1,76 +1,11 @@ var FFZ = window.FrankerFaceZ, utils = require("./utils"), constants = require("./constants"), - TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", helpers, + conv_helpers, EXPLANATION_TRAIL = '
FFZ is hiding this link because this url shortener is known to be used by Twitch spam bots posting malicious links. Please use caution when visiting shortened links.', - SRCSETS = {}; - build_srcset = function(id) { - if ( SRCSETS[id] ) - return SRCSETS[id]; - var out = SRCSETS[id] = TWITCH_BASE + id + "/1.0 1x, " + TWITCH_BASE + id + "/2.0 2x, " + TWITCH_BASE + id + "/3.0 4x"; - return out; - }, - - - data_to_tooltip = function(data) { - var set = data.set, - set_type = data.set_type, - owner = data.owner; - - if ( set_type === undefined ) - set_type = "Channel"; - - if ( ! set ) - return data.code; - - else if ( set === "--global--" ) { - set = "Twitch Global"; - set_type = null; - - } else if ( set == "--twitch-turbo--" || set == "turbo" || set == "--turbo-faces--" ) { - set = "Twitch Turbo"; - set_type = null; - } - - return "Emoticon: " + data.code + "
" + (set_type ? set_type + ": " : "") + set + (owner ? "
By: " + owner.display_name : ""); - }, - - build_tooltip = function(id) { - var emote_data = this._twitch_emotes[id], - set = emote_data ? emote_data.set : null; - - if ( ! emote_data ) - return "???"; - - if ( typeof emote_data == "string" ) - return emote_data; - - if ( emote_data.tooltip ) - return emote_data.tooltip; - - return emote_data.tooltip = data_to_tooltip(emote_data); - }, - - load_emote_data = function(id, code, success, data) { - if ( ! success ) - return code; - - if ( code ) - data.code = code; - - this._twitch_emotes[id] = data; - var tooltip = build_tooltip.bind(this)(id); - - var images = document.querySelectorAll('img[data-emote="' + id + '"]'); - for(var x=0; x < images.length; x++) - images[x].title = tooltip; - - return tooltip; - }, - reg_escape = function(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); @@ -105,7 +40,7 @@ var FFZ = window.FrankerFaceZ, return IMGUR_PATH.test(path); return any_domain ? IMAGE_EXT.test(path) : IMAGE_DOMAINS.indexOf(domain) !== -1; - } + }, image_iframe = function(href, extra_class) { return ''; @@ -248,12 +183,12 @@ FFZ._emote_mirror_swap = function(img) { img.setAttribute('data-alt-attempts', attempts + 1); var id = img.getAttribute('data-emote'); - if ( img.src.substr(0, TWITCH_BASE.length) === TWITCH_BASE ) { + if ( img.src.substr(0, constants.TWITCH_BASE.length) === constants.TWITCH_BASE ) { img.src = constants.EMOTE_MIRROR_BASE + id + ".png"; img.srcset = ""; } else { - img.src = TWITCH_BASE + id + "/1.0"; - img.srcset = build_srcset(id); + img.src = constants.TWITCH_BASE + id + "/1.0"; + img.srcset = utils.build_srcset(id); } } @@ -317,6 +252,10 @@ FFZ.prototype.setup_tokenization = function() { if ( ! helpers ) return this.log("Unable to get chat helper functions."); + conv_helpers = window.require && window.require("ember-twitch-conversations/helpers/conversation-line-helpers"); + if ( ! conv_helpers ) + this.log("Unable to get conversation helper functions."); + this.log("Hooking Ember chat line helpers."); var f = this; @@ -386,6 +325,8 @@ FFZ.prototype.load_twitch_emote_data = function(tries) { this._twitch_set_to_channel[33] = "--turbo-faces--"; this._twitch_set_to_channel[42] = "--turbo-faces--"; + this._reset_tooltips(true); + }).fail(function(data) { if ( data.status === 404 ) return; @@ -410,6 +351,9 @@ FFZ.prototype.tokenize_conversation_line = function(message, prevent_notificatio emotes = message.get('tags.emotes'), tokens = [msg]; + if ( conv_helpers && conv_helpers.checkActionMessage ) + tokens = conv_helpers.checkActionMessage(tokens); + // Standard Tokenization if ( helpers && helpers.linkifyMessage ) tokens = helpers.linkifyMessage(tokens); @@ -597,8 +541,11 @@ FFZ.prototype.tokenize_line = function(user, room, message, no_emotes, no_emoji) FFZ.prototype.render_tokens = function(tokens, render_links) { var f = this; return _.map(tokens, function(token) { + if ( token.hidden ) + return ""; + if ( token.emoticonSrc ) { - var tooltip, src = token.emoticonSrc, srcset, extra; + var tooltip, src = token.emoticonSrc, srcset, cls, extra; if ( token.ffzEmote ) { var emote_set = f.emote_sets && f.emote_sets[token.ffzEmoteSet], emote = emote_set && emote_set.emoticons && emote_set.emoticons[token.ffzEmote]; @@ -610,26 +557,32 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { } else if ( token.ffzEmoji ) { var eid = token.ffzEmoji, emoji = f.emoji_data && f.emoji_data[eid], - setting = f.settings.parse_emoji; + setting = f.settings.parse_emoji, + image = ''; if ( setting === 0 || (setting === 1 && ! emoji.tw) || (setting === 2 && ! emoji.noto) ) return token.altText; - tooltip = emoji ? "Emoji: " + token.altText + "\nName: " + emoji.name + (emoji.short_name ? "\nShort Name: :" + emoji.short_name + ":" : "") : token.altText; - extra = ' data-ffz-emoji="' + eid + '" height="18px"'; src = setting === 2 ? token.noto_src : token.tw_src; + if ( emoji && f.settings.emote_image_hover ) + image = ''; + + tooltip = emoji ? image + "Emoji: " + token.altText + "
Name: " + emoji.name + (emoji.short_name ? "
Short Name: :" + emoji.short_name + ":" : "") : token.altText; + extra = ' data-ffz-emoji="' + eid + '" height="18px"'; + cls = ' emoji'; + } else { var id = token.replacedId || FFZ.src_to_id(token.emoticonSrc), data = id && f._twitch_emotes && f._twitch_emotes[id]; if ( data ) - tooltip = data.tooltip ? data.tooltip : token.altText; + tooltip = data.tooltip ? data.tooltip : utils.build_tooltip.bind(f)(id, false, token.altText); else { try { var set_id = f._twitch_emote_to_set[id]; if ( set_id ) { - tooltip = load_emote_data.bind(f)(id, token.altText, true, { + tooltip = utils.load_emote_data.bind(f)(id, token.altText, true, { code: token.altText, id: id, set: f._twitch_set_to_channel[set_id], @@ -637,19 +590,18 @@ FFZ.prototype.render_tokens = function(tokens, render_links) { }); } else { tooltip = f._twitch_emotes[id] = token.altText; - f.ws_send("twitch_emote", id, load_emote_data.bind(f, id, token.altText)); + f.ws_send("twitch_emote", id, utils.load_emote_data.bind(f, id, token.altText)); } } catch(err) { f.error("Error Generating Emote Tooltip: " + err); } } - var mirror_url = utils.quote_attr(constants.EMOTE_MIRROR_BASE + id + '.png'); - + //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. if ( ! constants.EMOTE_REPLACEMENTS[id] ) - srcset = build_srcset(id); + srcset = utils.build_srcset(id); } return ''; @@ -797,7 +749,7 @@ FFZ.prototype.tokenize_title_emotes = function(tokens) { data = data.emoticon_sets[0]; for(var i=0; i < data.length; i++) { var em = data[i]; - emotes.push({regex: em.code, url: TWITCH_BASE + em.id + "/1.0"}); + emotes.push({regex: em.code, url: utils.TWITCH_BASE + em.id + "/1.0"}); } if ( f._cindex ) @@ -893,7 +845,7 @@ FFZ.prototype.tokenize_emotes = function(user, room, tokens, do_report) { bits.push(eo); if ( do_report && room ) - f.add_usage(room, emote.id); + f.add_usage(room, emote); } else bits.push(bit); diff --git a/src/ui/dark.js b/src/ui/dark.js index 3c7f5260..3a515933 100644 --- a/src/ui/dark.js +++ b/src/ui/dark.js @@ -144,6 +144,9 @@ FFZ.settings_info.dark_twitch = { model && model.set('darkMode', true); } else model && model.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); } }; diff --git a/src/ui/following-count.js b/src/ui/following-count.js index c4ec99be..43da8b50 100644 --- a/src/ui/following-count.js +++ b/src/ui/following-count.js @@ -52,7 +52,7 @@ FFZ.prototype.setup_following_count = function(has_ember) { // If we don't have Ember, no point in trying this stuff. if ( ! has_ember ) - return this._update_following_count(); + return this._following_get_me(); this.log("Connecting to Live Streams model."); var Stream = window.App && App.__container__.resolve('model:stream'); @@ -80,6 +80,27 @@ FFZ.prototype.setup_following_count = function(has_ember) { } +FFZ.prototype._following_get_me = function(tries) { + // get_user doesn't properly return an oauth token any longer, so we need to get me manually. + if ( ! window.Twitch ) + // Wait around till the API shows up. + return setTimeout(this._following_get_me.bind(this, tries), Math.floor(2000*Math.random()) + 500); + + var f = this; + Twitch.api.get("/api/me").done(function(data) { + f.log("Fetched User Data -- " + (data.name || data.login)); + f.__user = data; + f._update_following_count(); + + }).fail(function() { + tries = (tries||0) + 1; + if ( tries < 5 ) + return setTimeout(f._following_get_me.bind(f, tries), Math.floor(2000*Math.random()) + 500); + f.log("Failed to get proper user object."); + }); +} + + FFZ.prototype._schedule_following_count = function() { if ( ! this.settings.following_count ) { if ( this._following_count_timer ) { @@ -111,8 +132,13 @@ FFZ.prototype._update_following_count = function() { if ( Live ) Live.load(); - else - Twitch.api && Twitch.api.get("streams/followed", {limit:5, offset:0}, {version:3}) + else { + var a = {}, + u = this.get_user(); + + a.Authorization = "OAuth " + u.chat_oauth_token; + + Twitch.api && Twitch.api.get("streams/followed", {limit:20, offset:0}, {version:3, headers: a}) .done(function(data) { f._draw_following_count(data._total); f._draw_following_channels(data.streams, data._total); @@ -120,6 +146,7 @@ FFZ.prototype._update_following_count = function() { f._draw_following_count(); f._draw_following_channels(); }) + } } diff --git a/src/ui/menu.js b/src/ui/menu.js index ff29cf0d..9a38f083 100644 --- a/src/ui/menu.js +++ b/src/ui/menu.js @@ -2,8 +2,6 @@ var FFZ = window.FrankerFaceZ, constants = require('../constants'), utils = require('../utils'), - TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", - fix_menu_position = function(container) { var swapped = document.body.classList.contains('ffz-sidebar-swap') && ! document.body.classList.contains('ffz-portrait'); @@ -63,7 +61,8 @@ FFZ.prototype.setup_menu = function() { this.log("Hooking the Ember Chat Settings view."); - var Settings = window.App && App.__container__.resolve('view:settings'); + var Settings = window.App && App.__container__.resolve('view:settings'), + Layout = App.__container__.lookup('controller:layout'); if ( ! Settings ) return; @@ -155,6 +154,12 @@ FFZ.prototype.setup_menu = function() { menu.appendChild(header); menu.appendChild(content); + + // Maximum Height + var e = el.querySelector('.chat-settings'); + if ( Layout && e ) + e.style.maxHeight = (Layout.get('windowHeight') - 90) + 'px'; + }, ffzTeardown: function() { @@ -162,6 +167,15 @@ FFZ.prototype.setup_menu = function() { } }); + // Maximum height~! + if ( Layout ) + Layout.addObserver('windowHeight', function() { + var el = document.querySelector('.ember-chat .chat-settings'); + if ( el ) + el.style.maxHeight = (Layout.get('windowHeight') - 90) + 'px'; + }); + + // For some reason, this doesn't work unless we create an instance of the // chat settings view and then destroy it immediately. try { @@ -225,7 +239,7 @@ FFZ.prototype.build_ui_popup = function(view) { container.classList.toggle('dark', dark); // Stuff - jQuery(inner).find('.html-tooltip').tipsy({live: true, html: true, gravity: jQuery.fn.tipsy.autoNS}); + jQuery(inner).find('.html-tooltip').tipsy({live: true, html: true, gravity: utils.tooltip_placement(2*constants.TOOLTIP_DISTANCE, 's')}); // Menu Container @@ -316,7 +330,7 @@ FFZ.prototype.build_ui_popup = function(view) { link.title = page.name; link.innerHTML = page.icon; - jQuery(link).tipsy(); + jQuery(link).tipsy({gravity: utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); link.addEventListener("click", this._ui_change_page.bind(this, view, inner, menu, sub_container, key)); @@ -438,11 +452,11 @@ FFZ.menu_pages.channel = { var s = document.createElement('span'), can_use = is_subscribed || !emote.subscriber_only, - img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; + img_set = 'image-set(url("' + constants.TWITCH_BASE + emote.id + '/1.0") 1x, url("' + constants.TWITCH_BASE + emote.id + '/2.0") 2x), url("' + constants.TWITCH_BASE + emote.id + '/3.0") 4x)'; - s.className = 'emoticon tooltip' + (!can_use ? " locked" : ""); + s.className = 'emoticon html-tooltip' + (!can_use ? " locked" : ""); - s.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")'; + s.style.backgroundImage = 'url("' + constants.TWITCH_BASE + emote.id + '/1.0")'; s.style.backgroundImage = '-webkit-' + img_set; s.style.backgroundImage = '-moz-' + img_set; s.style.backgroundImage = '-ms-' + img_set; @@ -450,7 +464,7 @@ FFZ.menu_pages.channel = { s.style.width = emote.width + "px"; s.style.height = emote.height + "px"; - s.title = emote.regex; + s.title = (this.settings.emote_image_hover ? '' : '') + emote.regex; s.addEventListener('click', function(can_use, id, code, e) { if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) diff --git a/src/ui/my_emotes.js b/src/ui/my_emotes.js index 9d12f761..36954713 100644 --- a/src/ui/my_emotes.js +++ b/src/ui/my_emotes.js @@ -2,7 +2,6 @@ var FFZ = window.FrankerFaceZ, constants = require("../constants"), utils = require("../utils"), - TWITCH_BASE = "http://static-cdn.jtvnw.net/emoticons/v1/", BANNED_SETS = {"00000turbo":true}; @@ -166,11 +165,14 @@ FFZ.menu_pages.myemotes = { if ( (settings === 1 && ! emoji.tw) || (settings === 2 && ! emoji.noto) ) continue; - em.className = 'emoticon html-tooltip'; - em.title = 'Emoji: ' + emoji.raw + '\nName: ' + emoji.name + (emoji.short_name ? '\nShort Name: :' + emoji.short_name + ':' : ''); + var src = settings === 2 ? emoji.noto_src : emoji.tw_src, + image = this.settings.emote_image_hover ? '' : ''; + + em.className = 'emoticon emoji html-tooltip'; + em.title = image + 'Emoji: ' + emoji.raw + '
Name: ' + emoji.name + (emoji.short_name ? '
Short Name: :' + emoji.short_name + ':' : ''); em.addEventListener('click', this._add_emote.bind(this, view, emoji.raw)); - em.style.backgroundImage = 'url("' + (settings === 2 ? emoji.noto_src : emoji.tw_src) + '")'; + em.style.backgroundImage = 'url("' + src + '")'; em.style.backgroundSize = "18px"; menu.appendChild(em); @@ -237,21 +239,21 @@ FFZ.menu_pages.myemotes = { code = constants.KNOWN_CODES[emote.code] || emote.code, em = document.createElement('span'), - img_set = 'image-set(url("' + TWITCH_BASE + emote.id + '/1.0") 1x, url("' + TWITCH_BASE + emote.id + '/2.0") 2x, url("' + TWITCH_BASE + emote.id + '/3.0") 4x)'; + img_set = 'image-set(url("' + constants.TWITCH_BASE + emote.id + '/1.0") 1x, url("' + constants.TWITCH_BASE + emote.id + '/2.0") 2x, url("' + constants.TWITCH_BASE + emote.id + '/3.0") 4x)'; em.className = 'emoticon html-tooltip'; if ( this.settings.replace_bad_emotes && constants.EMOTE_REPLACEMENTS[emote.id] ) { em.style.backgroundImage = 'url("' + constants.EMOTE_REPLACEMENT_BASE + constants.EMOTE_REPLACEMENTS[emote.id] + '")'; } else { - em.style.backgroundImage = 'url("' + TWITCH_BASE + emote.id + '/1.0")'; + em.style.backgroundImage = 'url("' + constants.TWITCH_BASE + emote.id + '/1.0")'; em.style.backgroundImage = '-webkit-' + img_set; em.style.backgroundImage = '-moz-' + img_set; em.style.backgroundImage = '-ms-' + img_set; em.style.backgroudnImage = img_set; } - em.title = code; + em.title = (this.settings.emote_image_hover ? '' : '') + code; em.addEventListener("click", function(id, c, e) { e.preventDefault(); if ( (e.shiftKey || e.shiftLeft) && f.settings.clickable_emoticons ) diff --git a/src/ui/sub_count.js b/src/ui/sub_count.js index e8c85976..e2be80a5 100644 --- a/src/ui/sub_count.js +++ b/src/ui/sub_count.js @@ -84,7 +84,7 @@ FFZ.prototype._update_subscribers = function() { }); cont.appendChild(stat); - jQuery(stat).tipsy(f.is_dashboard ? {"gravity":"s"} : undefined); + jQuery(stat).tipsy({gravity: f.is_dashboard ? "s" : utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } el.innerHTML = sub_count; diff --git a/src/ui/tooltips.js b/src/ui/tooltips.js new file mode 100644 index 00000000..5e2c27b0 --- /dev/null +++ b/src/ui/tooltips.js @@ -0,0 +1,38 @@ +var FFZ = window.FrankerFaceZ, + utils = require('../utils'), + constants = require('../constants'); + + +// --------------------- +// Initialization +// --------------------- + +FFZ.prototype.fix_tooltips = function() { + // First, override the tooltip mixin. + var TipsyTooltip = window.App && App.__container__.resolve('component:tipsy-tooltip'); + if ( TipsyTooltip ) { + this.log("Modifying Tipsy-Tooltip component to use gravity."); + TipsyTooltip.reopen({ + didInsertElement: function() { + var gravity = this.get("gravity"); + if ( ! gravity || typeof gravity === "string" ) + gravity = utils.tooltip_placement(constants.TOOLTIP_DISTANCE, gravity || 's'); + + this.$().tipsy({ + gravity: gravity + }); + } + }) + } + + // Iterate all existing tipsy stuff~! + this.log('Fixing already existing tooltips.'); + if ( ! window.jQuery || ! jQuery.cache ) + return; + + for(var obj_id in jQuery.cache) { + var obj = jQuery.cache[obj_id]; + if ( obj && obj.data && obj.data.tipsy && obj.data.tipsy.options && typeof obj.data.tipsy.options.gravity !== "function" ) + obj.data.tipsy.options.gravity = utils.tooltip_placement(constants.TOOLTIP_DISTANCE, obj.data.tipsy.options.gravity || 's'); + } +} \ No newline at end of file diff --git a/src/ui/viewer_count.js b/src/ui/viewer_count.js index 98d75edb..4227b078 100644 --- a/src/ui/viewer_count.js +++ b/src/ui/viewer_count.js @@ -66,6 +66,6 @@ FFZ.ws_commands.viewers = function(data) { view_count.innerHTML = content; parent.appendChild(view_count); - jQuery(view_count).tipsy(this.is_dashboard ? {"gravity":"s"} : undefined); + jQuery(view_count).tipsy({gravity: this.is_dashboard ? "s" : utils.tooltip_placement(constants.TOOLTIP_DISTANCE, 'n')}); } } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index b1cce141..364e8d05 100644 --- a/src/utils.js +++ b/src/utils.js @@ -182,10 +182,98 @@ var sanitize_el = document.createElement('span'), out = es[variant] = r.join("-"); return out; + }, + + // Twitch Emote Tooltips + + SRCSETS = {}, + build_srcset = function(id) { + if ( SRCSETS[id] ) + return SRCSETS[id]; + var out = SRCSETS[id] = constants.TWITCH_BASE + id + "/1.0 1x, " + constants.TWITCH_BASE + id + "/2.0 2x, " + constants.TWITCH_BASE + id + "/3.0 4x"; + return out; + }, + + + data_to_tooltip = function(data) { + var emote_set = data.set, + set_type = data.set_type, + + f = FFZ.get(), + image = ''; + + if ( data.id && f.settings.emote_image_hover ) + image = ''; + + if ( set_type === undefined ) + set_type = "Channel"; + + if ( ! emote_set ) + return image + data.code; + + else if ( emote_set === "--global--" ) { + emote_set = "Twitch Global"; + set_type = null; + + } else if ( emote_set == "--twitch-turbo--" || emote_set == "turbo" || emote_set == "--turbo-faces--" ) { + emote_set = "Twitch Turbo"; + set_type = null; + } + + return image + "Emoticon: " + data.code + "
" + (set_type ? set_type + ": " : "") + emote_set; + }, + + build_tooltip = function(id, force_update, code) { + var emote_data = this._twitch_emotes[id]; + + if ( ! emote_data && code ) { + var set_id = this._twitch_emote_to_set[id]; + if ( set_id ) { + emote_data = this._twitch_emotes[id] = { + code: code, + id: id, + set: this._twitch_set_to_channel[set_id], + set_id: set_id + } + } + } + + if ( ! emote_data ) + return "???"; + + if ( typeof emote_data == "string" ) + return emote_data; + + if ( ! force_update && emote_data.tooltip ) + return emote_data.tooltip; + + return emote_data.tooltip = data_to_tooltip(emote_data); + }, + + load_emote_data = function(id, code, success, data) { + if ( ! success ) + return code; + + if ( code ) + data.code = code; + + this._twitch_emotes[id] = data; + var tooltip = build_tooltip.bind(this)(id); + + var images = document.querySelectorAll('img[data-emote="' + id + '"]'); + for(var x=0; x < images.length; x++) + images[x].title = tooltip; + + return tooltip; }; module.exports = { + build_srcset: build_srcset, + build_tooltip: build_tooltip, + load_emote_data: load_emote_data, + + update_css: function(element, id, css) { var all = element.innerHTML, start = "/*BEGIN " + id + "*/", @@ -207,6 +295,25 @@ module.exports = { }, + tooltip_placement: function(margin, prefer) { + return function() { + var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, + $this = $(this), + half_width = $this.width() / 2, + half_height = $this.height() / 2, + boundTop = $(document).scrollTop() + half_height + (margin*2), + boundLeft = $(document).scrollLeft() + half_width + margin; + + if ($this.offset().top < boundTop) dir.ns = 'n'; + if ($this.offset().left < boundLeft) dir.ew = 'w'; + if ($(window).width() + $(document).scrollLeft() - ($this.offset().left + half_width) < margin) dir.ew = 'e'; + if ($(window).height() + $(document).scrollTop() - ($this.offset().top + half_height) < (2*margin)) dir.ns = 's'; + + return dir.ns + (dir.ew ? dir.ew : ''); + } + }, + + splitIRCMessage: splitIRCMessage, parseIRCTags: parseIRCTags, diff --git a/style.css b/style.css index 0a3af431..651bf347 100644 --- a/style.css +++ b/style.css @@ -21,20 +21,20 @@ body > div.tipsy .tipsy-arrow { opacity: 0.8; } .ffz-hide-recent-past-broadcast .recent-past-broadcast, .ffz-hide-view-count .stat.twitch-channel-views, -.ffz-minimal-chat-input .emoticon-selector-toggle, -.ffz-menu-replace .emoticon-selector-toggle { +.ffz-minimal-chat-input .chat-interface .emoticon-selector-toggle, +.ffz-menu-replace .chat-interface .emoticon-selector-toggle { display: none !important; } -body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + .ffz-ui-toggle svg, -body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle svg +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emoticon-selector-toggle + .ffz-ui-toggle svg, +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emoticon-selector-toggle + script + .ffz-ui-toggle svg { height: 14px; width: 18px; } -body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + .ffz-ui-toggle, -body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-toggle + script + .ffz-ui-toggle { +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emoticon-selector-toggle + .ffz-ui-toggle, +body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emoticon-selector-toggle + script + .ffz-ui-toggle { height: 14px; width: 18px; top: 28px; @@ -263,6 +263,11 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .emoticon-selector-togg margin-bottom: 30px; } +.ffz-theater-stats .app-main.theatre #channel .player-column:focus .archive_info + .stats-and-actions, +.ffz-theater-stats .app-main.theatre #channel .player-column:hover .archive_info + .stats-and-actions { + display: none; +} + .ffz-theater-stats .app-main.theatre .player-column:focus #hostmode > div.clearfix, .ffz-theater-stats .app-main.theatre .player-column:hover #hostmode > div.clearfix, .ffz-theater-stats .app-main.theatre .player-column:focus .stats-and-actions, @@ -898,8 +903,17 @@ span.ffz-handle:after { left: 8px } .ffz-ui-popup.dark .ffz-ui-menu-page { background-color: #1e1e1e; } +/* Positioning Fixes */ + +body:not(.ffz-bttv) .ember-chat .chat-settings { bottom: 40px; } +body:not(.ffz-bttv) .notification-controls .notify-menu { bottom: 25px; } +body:not(.ffz-bttv) .dropmenu.share { margin-bottom: 0; } + + /* Menu Scrollbar */ +.ffz-scrollbar::-webkit-scrollbar, +.ember-chat .chat-settings::-webkit-scrollbar, .conversations-list .conversations-list-inner::-webkit-scrollbar, .conversation-window .conversation-content::-webkit-scrollbar, .chat-history::-webkit-scrollbar, @@ -910,6 +924,8 @@ span.ffz-handle:after { left: 8px } width: 6px; } +.ffz-scrollbar::-webkit-scrollbar-thumb, +.ember-chat .chat-settings::-webkit-scrollbar-thumb, .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb, .conversation-window .conversation-content::-webkit-scrollbar-thumb, .chat-history::-webkit-scrollbar-thumb, @@ -922,10 +938,14 @@ span.ffz-handle:after { left: 8px } box-shadow: 0 0 1px 1px rgba(255,255,255,0.25); } +.ffz-dark .ffz-scrollbar::-webkit-scrollbar-thumb, .ffz-dark .table::-webkit-scrollbar-thumb, .ffz-dark .conversation-window .conversation-content::-webkit-scrollbar-thumb, .ffz-dark .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb, +.ffz-dark .conversation-input-bar .emoticon-selector-box .all-emotes::-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, .theatre .conversations-list .conversations-list-inner::-webkit-scrollbar-thumb, @@ -934,11 +954,15 @@ span.ffz-handle:after { left: 8px } .theatre .ffz-ui-menu-page::-webkit-scrollbar-thumb, .theatre .ffz-ui-sub-menu-page::-webkit-scrollbar-thumb +.dark .ffz-scrollbar::-webkit-scrollbar-thumb, +.dark .ember-chat .chat-settings::-webkit-scrollbar-thumb, .dark .chat-history::-webkit-scrollbar-thumb, .dark .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb, .dark .ffz-ui-menu-page::-webkit-scrollbar-thumb, .dark .ffz-ui-sub-menu-page::-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, .force-dark .emoticon-selector-box .all-emotes::-webkit-scrollbar-thumb, .force-dark .ffz-ui-menu-page::-webkit-scrollbar-thumb, @@ -954,7 +978,6 @@ img.channel_background[src="null"] { display: none; } .ffz-moderation-card { border: 2px solid #cbcbcb; max-width: 340px; - /*box-shadow: #808080 0 0 5px;*/ } .ffz-moderation-card .extra-interface { @@ -967,6 +990,7 @@ img.channel_background[src="null"] { display: none; } .ffz-moderation-card.ffz-has-info h3.name { margin-top: 0; + white-space: nowrap; } .ffz-moderation-card .info { @@ -1073,7 +1097,7 @@ img.channel_background[src="null"] { display: none; } } .mod-icons .custom { - text-indent: 0; + text-indent: 0 !important; text-align: center; text-decoration: none; font-size: 18px; @@ -1106,7 +1130,7 @@ img.channel_background[src="null"] { display: none; } text-decoration: none; } -.more-messages-indicator { +body:not(.ffz-bttv) .more-messages-indicator { /* This looks better when it's full width. */ margin: 0 -20px; } @@ -2047,6 +2071,8 @@ li[data-name="following"] a { /* Conversations */ +.conversation-chat-line.action .colon, +.conversation-input-bar .emoticon-selector .tabs, .conversation-preview-line .badges, body.ffz-conv-title-clickable .conversation-header span.conversation-header-name, body:not(.ffz-conv-title-clickable) .conversation-header a.conversation-header-name { display:none }