diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index 6252c97a..c0268f92 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -795,20 +795,22 @@ export default class EmoteMenu extends Module { // Finally, emotes added by FrankerFaceZ. - const me = t.site.getUser(), - ffz_room = t.emotes.getRoomSetsWithSources(me.id, me.login, props.channel_id, null), - ffz_global = t.emotes.getGlobalSetsWithSources(me.id, me.login); + const me = t.site.getUser(); + if ( me ) { + const ffz_room = t.emotes.getRoomSetsWithSources(me.id, me.login, props.channel_id, null), + ffz_global = t.emotes.getGlobalSetsWithSources(me.id, me.login); - for(const [emote_set, provider] of ffz_room) { - const section = this.processFFZSet(emote_set, provider); - if ( section ) - channel.push(section); - } + for(const [emote_set, provider] of ffz_room) { + const section = this.processFFZSet(emote_set, provider); + if ( section ) + channel.push(section); + } - for(const [emote_set, provider] of ffz_global) { - const section = this.processFFZSet(emote_set, provider); - if ( section ) - all.push(section); + for(const [emote_set, provider] of ffz_global) { + const section = this.processFFZSet(emote_set, provider); + if ( section ) + all.push(section); + } } diff --git a/src/sites/twitch-twilight/modules/chat/tab_completion.jsx b/src/sites/twitch-twilight/modules/chat/tab_completion.jsx new file mode 100644 index 00000000..86aa639e --- /dev/null +++ b/src/sites/twitch-twilight/modules/chat/tab_completion.jsx @@ -0,0 +1,152 @@ +'use strict'; + +// ============================================================================ +// RichContent Component +// ============================================================================ + +import Module from 'utilities/module'; +import Twilight from 'site'; + +export default class TabCompletion extends Module { + constructor(...args) { + super(...args); + + this.inject('chat'); + this.inject('chat.emotes'); + this.inject('i18n'); + this.inject('settings'); + + this.inject('site.fine'); + this.inject('site.web_munch'); + + + // Settings + + this.settings.add('chat.tab-complete.ffz-emotes', { + default: true, + ui: { + path: 'Chat > Input >> Tab Completion', + title: 'Allow tab-completion of FrankerFaceZ emotes.', + component: 'setting-check-box' + } + }); + + + // Components + + this.ChatInput = this.fine.define( + 'chat-input', + n => n && n.setChatInputRef && n.setAutocompleteInputRef, + Twilight.CHAT_ROUTES + ); + + + this.EmoteSuggestions = this.fine.define( + 'tab-emote-suggestions', + n => n && n.getMatchedEmotes, + Twilight.CHAT_ROUTES + ); + } + + onEnable() { + const React = this.web_munch.getModule('react'), + createElement = React && React.createElement; + + if ( ! createElement ) + return this.log.warn('Unable to get React.'); + + this.ChatInput.ready((cls, instances) => { + for(const inst of instances) + this.updateEmoteCompletion(inst); + }); + + this.EmoteSuggestions.ready((cls, instances) => { + for(const inst of instances) + this.overrideEmoteMatcher(inst); + }); + + this.ChatInput.on('update', this.updateEmoteCompletion, this); + this.EmoteSuggestions.on('mount', this.overrideEmoteMatcher, this); + } + + + updateEmoteCompletion(inst, child) { + if ( ! child ) + child = this.fine.searchTree(inst, 'tab-emote-suggestions'); + if ( ! child ) + return; + + child._ffz_user = inst.props.sessionUser; + child._ffz_channel_id = inst.props.channelID; + child._ffz_channel_login = inst.props.channelLogin; + } + + + overrideEmoteMatcher(inst) { + if ( inst._ffz_override ) + return; + + const t = this, + old_get_matched = inst.getMatchedEmotes; + + inst.doesEmoteMatchTerm = function(emote, term) { + const emote_name = emote.name || emote.token, + emote_lower = emote_name.toLowerCase(), + term_lower = term.toLowerCase(); + + if (emote_lower.startsWith(term_lower)) + return true; + + const idx = emote_name.indexOf(term.charAt(0).toUpperCase()); + if (idx !== -1) + return emote_lower.slice(idx + 1).startsWith(term_lower.slice(1)); + } + + inst.getMatchedEmotes = function(input) { + const results = old_get_matched.call(this, input); + if ( ! t.chat.context.get('chat.tab-complete.ffz-emotes') ) + return results; + + return results.concat(t.getEmoteSuggestions(input, this)); + } + } + + + getEmoteSuggestions(input, inst) { + const user = inst._ffz_user, + channel_id = inst._ffz_channel_id, + channel_login = inst._ffz_channel_login; + + if ( ! channel_login ) { + const parent = this.fine.searchParent(inst, 'chat-input'); + if ( parent ) + this.updateEmoteCompletion(parent, inst); + + if ( ! channel_login ) + return []; + } + + const search = input.slice(1), + results = [], + emotes = this.emotes.getEmotes( + user && user.id, + user && user.login, + channel_id, + channel_login + ); + + for(const emote of Object.values(emotes)) + if ( inst.doesEmoteMatchTerm(emote, search) ) + results.push({ + current: input, + replacement: emote.name, + element: inst.renderEmoteSuggestion({ + token: emote.name, + id: `${emote.token.provider}-${emote.id}`, + srcSet: emote.srcSet + }) + }); + + return results; + } +}