From 46dee22d83c80def2d724760710f94d8f704f32d Mon Sep 17 00:00:00 2001 From: SirStendec Date: Sat, 26 Mar 2016 16:09:36 -0400 Subject: [PATCH] 3.5.146. Tab completion for emotes. --- src/ember/chat-input.js | 907 +++++++++++++++++++++++++++++------ src/ember/chatview.js | 5 + src/ember/moderation-card.js | 11 + src/ember/player.js | 24 +- src/emoticons.js | 6 + src/ext/api.js | 16 + style.css | 52 +- 7 files changed, 875 insertions(+), 146 deletions(-) diff --git a/src/ember/chat-input.js b/src/ember/chat-input.js index ec230584..5cc3f236 100644 --- a/src/ember/chat-input.js +++ b/src/ember/chat-input.js @@ -10,6 +10,10 @@ var FFZ = window.FrankerFaceZ, ENTER: 13, ESC: 27, SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, LEFT: 37, UP: 38, RIGHT: 39, @@ -43,7 +47,18 @@ var FFZ = window.FrankerFaceZ, r.move("character", pos); r.select(); } - }; + }, + + + build_sort_key = function(item, now, is_whisper) { + if ( item.type === 'emoticon' ) + return '2|' + (item.favorite ? 1 : 2) + '|' + item.sort + '|' + item.label; + + else if ( item.type === 'emoji' ) + return '3|' + (item.favorite ? 1 : 2) + '|' + item.label; + + return '4|' + item.label; + }; // --------------------- @@ -72,6 +87,52 @@ FFZ.settings_info.input_mru = { help: "Use the Up and Down arrows in chat to select previously sent chat messages." }; +FFZ.settings_info.input_complete_emotes = { + type: "select", + options: { + 0: "Disabled", + 1: "Channel and Sub Only", + 2: "All Emoticons" + }, + + value: 0, + + process_value: function(val) { + if ( typeof val === 'string' ) + return parseInt(val) || 0; + return val; + }, + + category: "Chat Input", + no_bttv: true, + + name: "Tab-Complete Emoticons Beta", + help: "Use tab completion to complete emoticon names in chat.", + + on_update: function(val) { + if ( this._inputv ) + Ember.propertyDidChange(this._inputv, 'ffz_emoticons'); + } +} + + +FFZ.settings_info.input_complete_without_prefix = { + type: "boolean", + value: true, + + category: "Chat Input", + no_bttv: true, + + name: "Tab-Complete Sub Emotes without Prefix", + help: "Allow you to tab complete a sub emote without including its prefix. Example: Battery into chrisBattery", + + on_update: function(val) { + if ( this._inputv ) + Ember.propertyDidChange(this._inputv, 'ffz_emoticons'); + } +} + + FFZ.settings_info.input_emoji = { type: "boolean", value: false, @@ -116,6 +177,13 @@ FFZ.prototype._modify_chat_input = function(component) { component.reopen({ ffz_mru_index: -1, + ffz_current_suggestion: 0, + ffz_partial_word: '', + ffz_partial_word_start: -1, + ffz_suggestions_visible: false, + ffz_freeze_suggestions: -1, + ffz_suggestions_el: null, + ffz_name_suggestions: [], ffz_chatters: [], didInsertElement: function() { @@ -140,10 +208,13 @@ FFZ.prototype._modify_chat_input = function(component) { s.id = 'ffz-minimal-chat-textarea-height'; document.head.appendChild(s); + this.set('ffz_name_suggestions', this.get('suggestions')); + // Redo our key bindings. var t = this.$("textarea"); t.off("keydown"); + t.off("keyup"); t.on("keydown", this._ffzKeyDown.bind(this)); t.attr('rows', 1); @@ -168,10 +239,486 @@ FFZ.prototype._modify_chat_input = function(component) { t.attr('rows', undefined); + t.off("keyup"); t.off("keydown"); + t.on("keyup", this._onKeyUp.bind(this)); t.on("keydown", this._onKeyDown.bind(this)); }, + // Suggestions + + ffzBuildSuggestionItem: function(i, item) { + // Returns a new element for the suggestions list. + if ( ! item ) + return null; + + var t = this, + el = document.createElement('div'), + inner = document.createElement('div'), + width = item.width ? (246 - item.width) + 'px' : null; + + el.className = 'suggestion'; + el.setAttribute('data-id', i); + el.classList.toggle('ffz-is-favorite', item.favorite || false); + + if ( item.image ) { + el.classList.add('has-image'); + el.classList.toggle('is-emoji', item.type === 'emoji'); + el.style.backgroundImage = 'url("' + utils.quote_attr(item.image) + '")'; + } + + inner.innerHTML = item.label; + if ( width ) + inner.style.maxWidth = width; + el.appendChild(inner); + + if ( f.settings.input_complete_emotes && item.info ) { + var info = document.createElement('span'); + info.innerHTML = item.info; + el.classList.add('has-info'); + if ( width ) + info.style.maxWidth = width; + el.appendChild(info); + } + + el.addEventListener('mouseenter', function() { + if ( t.get('ffz_freeze_suggestions') === -1 ) { + var els = el.parentElement.querySelectorAll('.suggestion'), + middle = els[Math.floor(els.length / 2)]; + t.set('ffz_freeze_suggestions', middle ? parseInt(middle.getAttribute('data-id')) : i) + } + + t.set('ffz_current_suggestion', i); + }); + + el.addEventListener('mouseup', function() { + t.ffzCompleteSuggestion(item); + }); + + el.addEventListener('wheel', function(e) { + // We want to scroll the list up or down. Harder than it sounds. In order + // to scroll it well, we should use the center item, not the one under + // the mouse. + var suggestions = t.get('ffz_sorted_suggestions'), + first_el = el.parentElement.querySelector('.suggestion:first-of-type'), + first = first_el && parseInt(first_el.getAttribute('data-id')); + + first += event.deltaY > 0 ? 1 : -1; + + t.set('ffz_freeze_suggestions', -1); + t.set('ffz_current_suggestion', Math.min(first + 2, suggestions.length - 1)); + }); + + return el; + }, + + + ffzUpdateSuggestions: function() { + var visible = this.get('ffz_suggestions_visible'); + if ( visible ) { + if ( this.get('ffz_updating') ) + return; + + this.set('ffz_updating', true); + + var el = this.ffz_suggestions_el, + current = this.get('ffz_current_suggestion') || 0; + + if ( ! el ) { + el = this.ffz_suggestions_el = document.createElement('div'); + el.className = 'suggestions ffz-suggestions'; + this.get('element').appendChild(el); + + } else + el.innerHTML = ''; + + var suggestions = this.get('ffz_sorted_suggestions'), + freeze = this.get('ffz_freeze_suggestions'), + middle = freeze === -1 ? current : freeze, + + first = Math.max(0, middle - 2), + last = Math.min(suggestions.length, first + 5), + added = false; + + first = Math.min(first, Math.max(0, last - 5)); + + if ( current >= suggestions.length ) { + this.set('ffz_current_suggestion', first); + current = first; + } + + for(var i=first; i < last; i++) { + var item = suggestions[i], + item_el = this.ffzBuildSuggestionItem(i, item); + + if ( i === current ) + item_el.classList.add('highlighted'); + + if ( item_el ) { + el.appendChild(item_el); + added = true; + } + } + + if ( ! added ) { + var item_el = document.createElement('div'); + item_el.className = 'suggestion disabled'; + item_el.textContent = 'No matches.'; + el.appendChild(item_el); + } + + this.set('ffz_updating', false); + + } else if ( this.ffz_suggestions_el ) { + this.ffz_suggestions_el.parentElement.removeChild(this.ffz_suggestions_el); + this.ffz_suggestions_el = null; + } + + }.observes('ffz_suggestions_visible', 'ffz_sorted_suggestions', 'ffz_current_suggestion'), + + + ffzHideSuggestions: function() { + this.set('ffz_suggestions_visible', false); + this.set('ffz_freeze_suggestions', -1); + this.set('ffz_current_suggestion', 0); + }, + + + ffzShowSuggestions: function() { + this.set('ffz_current_suggestion', 0); + this.ffzFetchNameSuggestions(); + this.set('ffz_freeze_suggestions', -1); + this.set('ffz_suggestions_visible', true); + this.ffzSetPartialWord(); + }, + + + ffzSetPartialWord: function() { + var area = this.get('chatTextArea'); + if ( area && this.get('ffz_suggestions_visible') ) { + var text = this.get('textareaValue'), + ind = selection_start(area); + + if ( ind === -1 ) + return this.ffzHideSuggestions(); + + var start = text.lastIndexOf(' ', ind - 1) + 1; + this.set('ffz_partial_word_start', start); + + var match = text.substr(start).match(/^[^ ]*/); + if ( match && match[0] ) + this.set('ffz_partial_word', match[0]); + else if ( text.charAt(0) === '/' && text.charAt(1) !== ' ' && start === (text.indexOf(' ') + 1) ) + // Assume the first word after a command is a username. + this.set('ffz_partial_word', '@'); + else + this.ffzHideSuggestions(); + } + }.observes('textareaValue'), + + + ffzFetchNameSuggestions: function() { + if ( ! this.get('ffz_suggestions_visible') ) + this.set('ffz_name_suggestions', this.get('suggestions')); + }.observes('suggestions'), + + + ffzCompleteSuggestion: function(item) { + if ( ! item ) { + var suggestions = this.get('ffz_sorted_suggestions'), + current = this.get('ffz_current_suggestion'); + + item = suggestions && suggestions[current]; + } + + this.ffzHideSuggestions(); + if ( ! item ) + return; + + var t = this, + ind = this.get('ffz_partial_word_start'), + text = this.get('textareaValue'), + + content = (item.command_content && text.charAt(0) === '/' ? + item.command_content : item.content) || item.label, + + trail = text.substr(ind + this.get('ffz_partial_word').length), + prefix = text.substr(0, ind) + content + (trail ? '' : ' '); + + + this.set('textareaValue', prefix + trail); + this.set('ffz_partial_word', ''); + this.set('ffz_partial_word_start', -1); + this.trackSuggestionsCompleted(); + Ember.run.next(function() { + var area = t.get('chatTextArea'); + move_selection(area, prefix.length); + area.focus(); + }); + }, + + + ffz_emoticons: function() { + var emotes = {}, + + room = this.get('parentView.context.model'), + room_id = room && room.get('id'), + tmi = room && room.tmiSession, + + set_name, replacement, url, is_sub_set, fav_list, + emote_set, emote, emote_id, code, + + user = f.get_user(), + ffz_sets = f.getEmotes(user && user.login, room_id), + + setting = f.settings.input_complete_emotes; + + if ( ! setting ) + return {}; + + + if ( tmi ) { + var es = tmi.getEmotes(); + if ( es && es.emoticon_sets ) { + for(var set_id in es.emoticon_sets) { + emote_set = es.emoticon_sets[set_id]; + fav_list = f.settings.favorite_emotes['twitch-' + set_id] || []; + is_sub_set = false; + set_name = f._twitch_set_to_channel[set_id]; + if ( ! emote_set ) + continue; + + if ( set_name ) { + if ( set_name === '--global--' ) + set_name = 'Twitch Global'; + else if ( set_name === '--twitch-turbo--' || set_name === 'turbo' || set_name === '--turbo-faces--' ) + set_name = 'Twitch Turbo'; + else { + set_name = 'Channel: ' + FFZ.get_capitalization(set_name); + is_sub_set = true; + } + + } else + set_name = "Unknown Source"; + + if ( setting === 1 && ! is_sub_set ) + continue; + + for(var i = 0; i < emote_set.length; i++) { + emote = emote_set[i]; + code = emote && emote.code; + code = code && (constants.KNOWN_CODES[code] || code); + replacement = f.settings.replace_bad_emotes && constants.EMOTE_REPLACEMENTS[emote.id]; + url = replacement ? + (constants.EMOTE_REPLACEMENT_BASE + replacement) : + (constants.TWITCH_BASE + emote.id + "/1.0"); + + if ( ! emotes[code] || ! emotes[code][0] ) + emotes[code] = [true, code, true, is_sub_set, set_name, url, null, fav_list.indexOf(emote.id) !== -1]; + + if ( f.settings.input_complete_without_prefix && is_sub_set ) { + // It's a sub emote, so try splitting off the end of the code. + // It's a bit weird, but people might like it. Also, make sure + // we aren't just grabbing an initial capital. + var unprefixed = code.substr(1).match(/[A-Z].+$/); + unprefixed = unprefixed ? unprefixed[0] : null; + if ( unprefixed && ! emotes[unprefixed] ) + emotes[unprefixed] = [false, code, true, is_sub_set, set_name, url, null, fav_list.indexOf(emote.id) !== -1]; + } + } + } + } + } + + for(var i=0; i < ffz_sets.length; i++) { + emote_set = f.emote_sets[ffz_sets[i]]; + if ( ! emote_set ) + continue; + + if ( setting === 1 && f.default_sets.indexOf(emote_set.id) !== -1 ) + continue; + + set_name = (emote_set.source || "FFZ") + " " + (emote_set.title || "Global"); + fav_list = f.settings.favorite_emotes[emote_set.hasOwnProperty('source_ext') ? 'ffz-ext-' + emote_set.source_ext + '-' + emote_set.source_id : 'ffz-' + emote_set.id] || []; + + for(emote_id in emote_set.emoticons) { + emote = emote_set.emoticons[emote_id]; + if ( ! emote.hidden && emote.name && (! emotes[emote.name] || ! emotes[emote.name][0]) ) + emotes[emote.name] = [true, emote.name, false, emote_set.id, set_name, emote.urls[1], emote.width, fav_list.indexOf(emote.id) !== -1]; + } + } + + return emotes; + }.property(), + + _setPartialName: function() { }, + + ffz_suggestions: function() { + var output = [], + emotes = this.get('ffz_emoticons'), + suggestions = this.get('ffz_name_suggestions'); //.mapBy('id').uniq(); + + if ( f.settings.input_complete_emotes ) { + // Include Emoticons + for(var emote_name in emotes) { + var emote = emotes[emote_name], + sort_factor = 9, + label = emote[1] === emote_name ? emote[1] : ('' + emote[1].substr(0, emote[1].length - emote_name.length) + '' + emote_name); + + if ( emote[2] ) { + if ( emote[3] ) + sort_factor = 1; + + } else { + var set_data = f.emote_sets[emote[3]]; + if ( set_data ) + if ( set_data._type === 1 ) + sort_factor = 3; + else + sort_factor = ffz.default_sets.indexOf(set_data.id) === -1 ? 2 : 6; + } + + output.push({ + type: "emoticon", + match: emote_name, + sort: sort_factor, + content: emote[1], + label: label, + info: emote[4], + image: emote[5], + width: emote[6], + favorite: emote[7] || false + }); + } + + + if ( f.settings.parse_emoji ) { + // Include Emoji + var setting = f.settings.parse_emoji, + fav_list = f.settings.favorite_emotes['emoji'] || []; + + for(var short_name in f.emoji_names) { + var eid = f.emoji_names[short_name], + emoji = f.emoji_data[eid]; + + if ( ! emoji || !(setting === 3 ? emoji.one : (setting === 2 ? emoji.noto : emoji.tw)) ) + continue; + + var sn = ':' + short_name + ':', + src = (f.settings.parse_emoji === 3 ? emoji.one_src : (f.settings.parse_emoji === 2 ? emoji.noto_src : emoji.tw_src)); + + output.push({ + type: "emoji", + match: ':' + short_name + ':', + content: emoji.raw, + label: emoji.name, + info: sn, + image: src, + width: 18, + favorite: fav_list.indexOf(emoji.raw) !== -1 + }); + } + } + } + + + // Always include Users + var user_output = {}; + for(var i=0; i < suggestions.length; i++) { + var suggestion = suggestions[i], + name = suggestion.id; + + if ( user_output[name] ) { + var token = user_output[name]; + token.whispered |= suggestion.whispered; + if ( suggestion.timestamp > token.timestamp ) + token.timestamp = suggestion.timestamp; + + } else + output.push(user_output[name] = { + type: "user", + command_content: name, + label: FFZ.get_capitalization(name), + whispered: suggestion.whispered, + timestamp: suggestion.timestamp || new Date(0), + info: 'User' + }); + } + + return output; + + }.property('ffz_emoticons', 'ffz_name_suggestions'), + + + ffz_filtered_suggestions: Ember.computed("ffz_suggestions", "ffz_partial_word", function() { + var suggestions = this.get('ffz_suggestions'), + partial = this.get('ffz_partial_word'), + part2 = partial.substr(1), + char = partial.charAt(0); + + return suggestions.filter(function(item) { + var name = item.match || item.content || item.label, + type = item.type; + + if ( ! name ) + return false; + + if ( type === 'user' ) { + // Names are case insensitive, and we have to ignore the leading @ of our + // partial word when matching. + name = name.toLowerCase(); + return char === '@' ? name.indexOf(part2.toLowerCase()) === 0 : name.indexOf(partial.toLowerCase()) === 0; + + } else if ( type === 'emoji' ) { + name = name.toLowerCase(); + return name.indexOf(partial.toLowerCase()) === 0; + } + + return name.indexOf(partial) === 0; + }); + }), + + + ffz_sorted_suggestions: Ember.computed("ffz_filtered_suggestions.[]", function() { + var text = this.get('textareaValue'), + now = Date.now(), + is_whisper = text.substr(0,3) === '/w '; + + return this.get('ffz_filtered_suggestions').sort(function(a, b) { + // First off, sort users ahead of everything else. + if ( a.type === 'user' ) { + if ( b.type !== 'user' ) + return -1; + + else if ( is_whisper ) { + if ( a.whisper && ! b.whisper ) + return -1; + else if ( ! a.whisper && b.whisper ) + return 1; + } + + if ( a.timestamp > b.timestamp ) return -1; + else if ( a.timestamp < b.timestamp ) return 1; + + var an = a.label.toLowerCase(), + bn = b.label.toLowerCase(); + + if ( an < bn ) return -1; + else if ( an > bn ) return 1; + return 0; + + } else if ( b.type === 'user' ) + return 1; + + var an = build_sort_key(a, now, is_whisper), + bn = build_sort_key(b, now, is_whisper); + + if ( an < bn ) return -1; + if ( an > bn ) return 1; + return 0; + }); + }), + // Input Control ffzOnInput: function() { @@ -209,81 +756,246 @@ FFZ.prototype._modify_chat_input = function(component) { this._ffz_last_height = height; }, + hideSuggestions: Ember.on("document.mouseup", function(event) { + var target = event.target, + cl = target.classList; + + if ( ! this.get('ffz_suggestions_visible') || cl.contains('suggestion') || cl.contains('suggestions') || target === this.get('chatTextArea') ) + return; + + this.ffzHideSuggestions(); + }), + _ffzKeyDown: function(event) { - var e = event || window.event, + var t = this, + e = event || window.event, key = e.charCode || e.keyCode; switch(key) { + case KEYCODES.ESC: + if ( this.get('ffz_suggestions_visible') ) { + this.ffzHideSuggestions(); + e.preventDefault(); + } + + break; + + + case KEYCODES.BACKSPACE: + if ( this.get('ffz_suggestions_visible') && (this.get('ffz_partial_word').length === 1 || selection_start(this.get('chatTextArea')) === 0) ) + this.ffzHideSuggestions(); + + break; + + + case KEYCODES.TAB: + // If we do Ctrl-Tab or Alt-Tab. Just don't + // even think of doing suggestions. + if ( e.ctrlKey || e.altKey ) + break; + + e.preventDefault(); + + var text = this.get('textareaValue'); + if ( text.length === 0 ) + break; + + if ( text.charAt(0) !== '/' ) { + var parts = text.split(' '); + if ( parts[parts.length - 1].length === 0 ) + break; + } + + // If suggestions aren't visible... show them. And set that we + // triggered the suggestions with tab. + if ( ! this.get('ffz_suggestions_visible') ) { + this.ffzFetchNameSuggestions(); + this.set('ffz_suggestions_visible', true); + this.ffzSetPartialWord(); + this.trackSuggestions("Tab"); + + // If suggestions *are* visible, enter a suggestion. + } else + this.ffzCompleteSuggestion(); + + break; + + + case KEYCODES.TWO: + // Should: Pop open the suggestions tab if we add an @. This is + // probably not the correct way to check for this, given how + // different region keyboards work. + if ( ! this.get('ffz_suggestions_visible') && (e.shiftKey || e.shiftLeft) ) { + this.ffzFetchNameSuggestions(); + this.set('ffz_suggestions_visible', true); + this.trackSuggestions("@"); + } + + break; + + + /* Not convinced this isn't annoying. + case KEYCODES.HOME: + case KEYCODES.END: + // Navigate through suggestions if those are open. + if ( this.get('ffz_suggestions_visible') && !( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) ) { + var suggestions = this.get('ffz_sorted_suggestions'), + current = key === KEYCODES.HOME ? 0 : (suggestions.length - 1); + + this.set('ffz_freeze_suggestions', -1); + this.set('ffz_current_suggestion', current); + e.preventDefault(); + } + + break;*/ + + case KEYCODES.PAGE_UP: + case KEYCODES.PAGE_DOWN: + // Navigate through suggestions if those are open. + if ( this.get('ffz_suggestions_visible') && !( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) ) { + var suggestions = this.get('ffz_sorted_suggestions'), + current = this.get('ffz_current_suggestion') + (key === KEYCODES.PAGE_UP ? -5 : 5); + + if ( current < 0 ) + current = 0; + else if ( current >= suggestions.length ) + current = suggestions.length - 1; + + this.set('ffz_freeze_suggestions', -1); + this.set('ffz_current_suggestion', current); + e.preventDefault(); + } + + break; + case KEYCODES.UP: case KEYCODES.DOWN: - if ( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) - return; - else if ( this.get("isShowingSuggestions") ) - e.preventDefault(); + // First, navigate through suggestions if those are open. + if ( this.get('ffz_suggestions_visible') && !( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) ) { + var suggestions = this.get('ffz_sorted_suggestions'), + current = this.get('ffz_current_suggestion') + (key === KEYCODES.UP ? -1 : 1); + + if ( current < 0 ) + current = suggestions.length - 1; + else if ( current >= suggestions.length ) + current = 0; + + this.set('ffz_freeze_suggestions', -1); + this.set('ffz_current_suggestion', current); + e.preventDefault(); + break; + + // Otherwise, if we're holding any special modifiers, don't do + // anything special to avoid breaking functionality. + } else if ( e.shiftKey || e.shiftLeft || e.ctrlKey || e.metaKey ) + break; + + // If MRU is enabled, cycle through it if the cursor's position doesn't + // change as a result of this action. else if ( f.settings.input_mru ) Ember.run.next(this.ffzCycleMRU.bind(this, key, selection_start(this.get("chatTextArea")))); - else - return this._onKeyDown(event); - break; + + // If MRU isn't enabled, cycle through the whisper targets. + else + Ember.run.next(this.cycleWhisperTargets.bind(this, key)); + + break; + + + case KEYCODES.COLON: + case KEYCODES.FAKE_COLON: + // If the appropriate setting is enabled and the user has entered + // an actual emoji, we need to replace the name in chat. + + // This is also not the right place to do this. We need to find the + // character that was pressed, not the key. + if ( f.settings.input_emoji && ( e.shiftKey || e.shiftLeft ) ) { + var ind = selection_start(this.get('chatTextArea')); + + ind > 0 && Ember.run.next(function() { + var text = t.get('textareaValue'), + emoji_start = text.lastIndexOf(':', ind - 1); + + if ( emoji_start !== -1 && ind !== -1 && text.charAt(ind) === ':' ) { + var match = text.substr(emoji_start + 1, ind - emoji_start - 1), + emoji_id = f.emoji_names[match], + emoji = f.emoji_data[emoji_id]; + + if ( emoji ) { + var prefix = text.substr(0, emoji_start) + emoji.raw; + t.ffzHideSuggestions(); + t.set('textareaValue', prefix + text.substr(ind + 1)); + Ember.run.next(function() { + move_selection(t.get('chatTextArea'), prefix.length); + }); + } + } + }); + + break; + } + + + case KEYCODES.ENTER: + if ( e.shiftKey || e.shiftLeft ) + break; + + if ( this.get('ffz_suggestions_visible') ) + this.ffzCompleteSuggestion(); + else { + this.set("_currentWhisperTarget", -1); + setTimeout(this.ffzResizeInput.bind(this), 25); + this.sendAction("sendMessage"); + } + + if ( e.stopPropagation ) + e.stopPropagation(); + + e.preventDefault(); + break; + case KEYCODES.SPACE: - if ( f.settings.input_quick_reply && selection_start(this.get("chatTextArea")) === 2 && this.get("textareaValue").substring(0,2) === "/r" ) { - var t = this; - Ember.run.next(function() { - var wt = t.get("uniqueWhisperSuggestions.0"); - if ( wt ) { - var text = "/w " + wt + t.get("textareaValue").substr(2); - t.set("_currentWhisperTarget", 0); - t.set("textareaValue", text); + // After pressing space, if we're entering a command, do stuff! + // TODO: Better support for commands. + var sel = selection_start(this.get('chatTextArea')); + Ember.run.next(function() { + var text = t.get("textareaValue"), + ind = text.indexOf(' '), + start = ind !== -1 && text.substr(0, ind); - Ember.run.next(function() { - move_selection(t.get('chatTextArea'), 4 + wt.length); - }); - } - }); - } else - return this._onKeyDown(event); - break; + if ( ind !== sel ) + return; - case KEYCODES.COLON: - case KEYCODES.FAKE_COLON: - if ( f.settings.input_emoji && (e.shiftKey || e.shiftLeft) ) { - var t = this, - ind = selection_start(this.get("chatTextArea")); + if ( f.settings.input_quick_reply && start === '/r' ) { + var target = t.get("uniqueWhisperSuggestions.0"); + if ( target ) { + t.set("_currentWhisperTarget", 0); + t.set("textareaValue", "/w " + target + t.get("textareaValue").substr(2)); - ind > 0 && Ember.run.next(function() { - var text = t.get("textareaValue"), - emoji_start = text.lastIndexOf(":", ind - 1); + Ember.run.next(function() { + move_selection(t.get('chatTextArea'), 4 + target.length); + }); + } else { + t.set("textareaValue", "/w " + t.get('textareaValue').substr(2)); + Ember.run.next(function() { + move_selection(t.get('chatTextArea'), 3); + t.ffzFetchNameSuggestions(); + t.set("ffz_suggestions_visible", true); + t.ffzSetPartialWord(); + }); + } - if ( emoji_start !== -1 && ind !== -1 && text.charAt(ind) === ":" ) { - var match = text.substr(emoji_start + 1, ind-emoji_start - 1), - emoji_id = f.emoji_names[match], - emoji = f.emoji_data[emoji_id]; - - if ( emoji ) { - var prefix = text.substr(0, emoji_start) + emoji.raw; - t.set('textareaValue', prefix + text.substr(ind + 1)); - Ember.run.next(function() { - move_selection(t.get('chatTextArea'), prefix.length); - }); - } - } - }); - return; - } - return this._onKeyDown(event); - - case KEYCODES.ENTER: - if ( ! e.shiftKey && ! e.shiftLeft ) - this.set('ffz_mru_index', -1); - - setTimeout(this.ffzResizeInput.bind(this),10); - - default: - return this._onKeyDown(event); + } else if ( start === '/w' || start === '/ignore' || start === '/unignore' || start === '/mod' || start === '/unmod' || start === '/ban' || start === '/unban' || start === '/timeout' || start === '/purge' ) { + t.ffzFetchNameSuggestions(); + t.set("ffz_suggestions_visible", true); + t.ffzSetPartialWord(); + } + }); } }, + ffzCycleMRU: function(key, start_ind) { // We don't want to do this if the keys were just moving the cursor around. var cur_pos = selection_start(this.get("chatTextArea")); @@ -312,83 +1024,6 @@ FFZ.prototype._modify_chat_input = function(component) { this.set('ffz_mru_index', ind); this.set('textareaValue', new_val); - }, - - completeSuggestion: function(e) { - var r, n, i = this, - o = this.get("textareaValue"), - a = this.get("partialNameStartIndex"); - - r = o.substring(0, a) + (o.charAt(0) === "/" ? e : FFZ.get_capitalization(e)); - n = o.substring(a + this.get("partialName").length); - if ( ! n ) - r += " "; - - this.set("textareaValue", r + n); - this.set("isShowingSuggestions", false); - this.set("partialName", ""); - this.trackSuggestionsCompleted(); - Ember.run.next(function() { - move_selection(i.get('chatTextArea'), r.length); - }); } - - /*ffz_emoticons: function() { - var output = [], - - room = this.get('parentView.context.model'), - room_id = room && room.get('id'), - tmi = room && room.tmiSession, - - user = f.get_user(), - ffz_sets = f.getEmotes(user && user.login, room_id); - - if ( tmi ) { - var es = tmi.getEmotes(); - if ( es && es.emoticon_sets ) { - for(var set_id in es.emoticon_sets) { - var emote_set = es.emoticon_sets[set_id]; - for(var emote_id in emote_set) { - if ( emote_set[emote_id] && emote_set[emote_id].code ) { - var code = emote_set[emote_id].code; - output.push({id: constants.KNOWN_CODES[code] || code}); - } - } - } - } - } - - for(var i=0; i < ffz_sets.length; i++) { - var emote_set = f.emote_sets[ffz_sets[i]]; - if ( ! emote_set ) - continue; - - for(var emote_id in emote_set.emoticons) { - var emote = emote_set.emoticons[emote_id]; - if ( ! emote.hidden && emote.name ) - output.push({id:emote.name}); - } - } - - return output; - }.property(), - - suggestions: function(key, value, previousValue) { - if ( arguments.length > 1 ) { - this.set('ffz_chatters', value); - } - - var output = []; - - // Chatters - output = output.concat(this.get('ffz_chatters')); - - // Emoticons - if ( this.get('isSuggestionsTriggeredWithTab') ) { - output = output.concat(this.get('ffz_emoticons')); - } - - return output; - }.property("ffz_emoticons", "ffz_chatters", "isSuggestionsTriggeredWithTab")*/ }); } \ No newline at end of file diff --git a/src/ember/chatview.js b/src/ember/chatview.js index ed4d042b..b51deb3c 100644 --- a/src/ember/chatview.js +++ b/src/ember/chatview.js @@ -373,6 +373,11 @@ FFZ.prototype.setup_chatview = function() { f._chatv.ffzUpdateMenuUnread(); }.observes("invitedPrivateGroupRooms"), + ffzChangedRoom: function() { + if ( f._inputv ) + Ember.propertyDidChange(f._inputv, 'ffz_emoticons'); + }.observes('currentRoom'), + notificationsCount: function() { if ( ! f._chatv || f.has_bttv ) return this._super(); diff --git a/src/ember/moderation-card.js b/src/ember/moderation-card.js index c5006875..e7f80d7c 100644 --- a/src/ember/moderation-card.js +++ b/src/ember/moderation-card.js @@ -279,6 +279,17 @@ FFZ.settings_info.mod_buttons = { } f.settings.set('mod_buttons', final); + + // Update existing chat lines. + var CL = utils.ember_resolve('component:chat-line'), + views = CL ? utils.ember_views() : []; + + for(var vid in views) { + var view = views[vid]; + if ( view instanceof CL && view.buildModIconsHTML ) + view.$('.mod-icons').replaceWith(view.buildModIconsHTML()); + } + }, 600); } }; diff --git a/src/ember/player.js b/src/ember/player.js index 84887749..9351ccea 100644 --- a/src/ember/player.js +++ b/src/ember/player.js @@ -1,5 +1,6 @@ var FFZ = window.FrankerFaceZ, - utils = require('../utils'); + utils = require('../utils'), + constants = require('../constants'); // --------------- @@ -69,10 +70,11 @@ FFZ.prototype.setup_player = function() { this.log("Hooking HTML5 Player UI."); this._modify_player(Player2) - // Modify all existing players. - if ( ! window.Ember ) - return; + try { + Player2.create().destroy(); + } catch(err) { } + // Modify all existing players. var views = utils.ember_views(); for(var key in views) { if ( ! views.hasOwnProperty(key) ) @@ -87,10 +89,6 @@ FFZ.prototype.setup_player = function() { this._modify_player(view); view.ffzInit(); - var tp2 = window.require("web-client/components/twitch-player2"); - if ( tp2 && tp2.getPlayer && tp2.getPlayer() ) - view.ffzPostPlayer(); - } catch(err) { this.error("Player2 setup ffzInit: " + err); } @@ -161,6 +159,16 @@ FFZ.prototype._modify_player = function(player) { if ( ! player ) return; + + // Make the stats window draggable and fix the button. + var stats = this.$('.player .js-playback-stats'); + stats.draggable({cancel: 'li', containment: 'parent'}); + + // Give the player time to do stuff before we change this + // text. It's a bit weird otherwise. + setTimeout(function(){stats.children('.js-stats-toggle').html(constants.CLOSE);},500); + + // Only set up the stats hooks if we need stats. var has_video = false; diff --git a/src/emoticons.js b/src/emoticons.js index 842d4aa2..0e261442 100644 --- a/src/emoticons.js +++ b/src/emoticons.js @@ -346,6 +346,9 @@ FFZ.prototype.unload_set = function(set_id) { if ( api && api.emote_sets && api.emote_sets[set_id] ) api.emote_sets[set_id] = undefined; } + + if ( this._inputv ) + Ember.propertyDidChange(this._inputv, 'ffz_emoticons'); } @@ -407,6 +410,9 @@ FFZ.prototype._load_set_json = function(set_id, callback, data) { this.update_ui_link(); + if ( this._inputv ) + Ember.propertyDidChange(this._inputv, 'ffz_emoticons'); + if ( callback ) callback(true, data); } \ No newline at end of file diff --git a/src/ext/api.js b/src/ext/api.js index 545ac2ff..a0d38f94 100644 --- a/src/ext/api.js +++ b/src/ext/api.js @@ -288,6 +288,10 @@ API.prototype.register_global_set = function(id, emote_set) { if ( this.ffz.default_sets && this.ffz.default_sets.indexOf(exact_id) === -1 ) this.ffz.default_sets.push(exact_id); + + // Update tab completion. + if ( this.ffz._inputv ) + Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons'); }; @@ -314,6 +318,10 @@ API.prototype.unregister_global_set = function(id) { ind = this.ffz.default_sets ? this.ffz.default_sets.indexOf(exact_id) : -1; if ( ind !== -1 ) this.ffz.default_sets.splice(ind,1); + + // Update tab completion. + if ( this.ffz._inputv ) + Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons'); }; @@ -347,6 +355,10 @@ API.prototype.register_room_set = function(room_id, id, emote_set) { // Register it on the room. room.ext_sets && room.ext_sets.push(exact_id); emote_set.users.push(room_id); + + // Update tab completion. + if ( this.ffz._inputv ) + Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons'); } @@ -365,6 +377,10 @@ API.prototype.unregister_room_set = function(room_id, id) { ind = emote_set.users.indexOf(room_id); if ( ind !== -1 ) emote_set.users.splice(ind,1); + + // Update tab completion. + if ( this.ffz._inputv ) + Ember.propertyDidChange(this.ffz._inputv, 'ffz_emoticons'); } diff --git a/style.css b/style.css index 1ad464cd..3a9283bb 100644 --- a/style.css +++ b/style.css @@ -537,11 +537,13 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic padding: 10px 20px; } +.suggestion.ffz-is-favorite, .emoticon.ffz-favorite { position: relative; } .favorites-grid:not(:empty) + .ffz-no-emotes, .favorites-grid .emoticon.ffz-favorite:before { display: none } +.suggestion.ffz-is-favorite:before, .emoticon.ffz-favorite:before { content: ""; display: block; @@ -551,6 +553,8 @@ body:not(.ffz-minimal-chat-input):not(.ffz-menu-replace) .chat-interface .emotic background: url("//cdn.frankerfacez.com/script/emoticon_favorite.png") no-repeat; } +.suggestion.ffz-is-favorite:before { right: 2.5px; bottom: 2.5px } + .chat-menu.ffz-ui-popup .ffz-ui-menu-page .chat-menu-content.menu-side-padding { padding-left: 20px; padding-right: 20px; } .emoticon-grid.collapsed span, @@ -1815,9 +1819,9 @@ body.ffz-minimal-chat-input .ember-chat .chat-interface .textarea-contain textar pointer-events: none; } -.chat-container:not(.chatReplay) .chat-interface .more-messages-indicator.ffz-freeze-indicator { +/*.chat-container:not(.chatReplay) .chat-interface .more-messages-indicator.ffz-freeze-indicator { top: 0; -} +}*/ /* Chat History */ @@ -2676,6 +2680,50 @@ body:not(.ffz-creative-showcase) .creative-hero, } +/* Draggable Stats on Player */ + +.player .ui-draggable.player-playback-stats { cursor: move } +.player .ui-draggable.player-playback-stats li { cursor: default } + + +/* FFZ Suggestions */ + +.ember-chat .chat-interface .suggestions.ffz-suggestions .suggestion { + width: auto; + background-repeat: no-repeat; + background-position: calc(100% - 10px) center; +} + +.ember-chat .chat-interface .suggestions.ffz-suggestions .suggestion.has-info { + height: 40px; + line-height: 20px; + padding-top: 2px; +} + +.ffz-suggestions .suggestion div, +.ffz-suggestions .suggestion span { + overflow: hidden; + text-overflow: ellipsis; +} + +.ffz-suggestions .suggestion.is-emoji { + background-size: 18px; +} + +.ffz-suggestions .suggestion i { opacity: 0.5 } + +.ffz-suggestions .suggestion.has-info span { + opacity: 0.5; + display: block; + margin-top: -4px; + font-size: 10px; +} + +.ember-chat .chat-interface .suggestions.ffz-suggestions .suggestion.has-image:not(.has-info) { + line-height: 40px; +} + + /* Dank * / .streams .ember-view[data-channel="memeathon"] .stream .thumb:after {