diff --git a/src/main.js b/src/main.js index 6f5fec4d..0fcc7485 100644 --- a/src/main.js +++ b/src/main.js @@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}` FrankerFaceZ.Logger = Logger; const VER = FrankerFaceZ.version_info = { - major: 4, minor: 0, revision: 0, extra: '-rc13.21', + major: 4, minor: 0, revision: 0, extra: '-rc13.22', commit: __git_commit__, build: __webpack_hash__, toString: () => diff --git a/src/modules/chat/components/chat-automod-blocked.vue b/src/modules/chat/components/chat-automod-blocked.vue new file mode 100644 index 00000000..2b625d98 --- /dev/null +++ b/src/modules/chat/components/chat-automod-blocked.vue @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index b0e45b16..cfc6e554 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -93,6 +93,33 @@ export default class Emotes extends Module { } }); + this.settings.add('chat.click-emotes', { + default: true, + + ui: { + path: 'Chat > Behavior >> General', + title: 'Open emote information pages by Shift-Clicking them.', + component: 'setting-check-box' + } + }); + + this.settings.add('chat.sub-emotes', { + default: true, + ui: { + path: 'Chat > Behavior >> General', + title: 'Open Twitch subscription pages by Shift-Clicking emotes when relevant.', + component: 'setting-check-box' + } + }); + + this.settings.add('chat.emote-dialogs', { + default: true, + ui: { + path: 'Chat > Behavior >> General', + title: 'Open emote information cards for Twitch emotes by clicking them.', + component: 'setting-check-box' + } + }); // Because this may be used elsewhere. this.handleClick = this.handleClick.bind(this); @@ -303,6 +330,29 @@ export default class Emotes extends Module { return true; } + + if ( provider === 'twitch' && this.parent.context.get('chat.emote-dialogs') ) { + const fine = this.resolve('site.fine'); + if ( ! fine ) + return; + + const chat = fine.searchParent(target, n => n.props && n.props.onEmoteClick); + if ( ! chat || ! chat.props || ! chat.props.message ) + return; + + const props = chat.props; + props.onEmoteClick({ + channelID: props.channelID || '', + channelLogin: props.channelLogin || '', + emoteID: ds.id, + emoteCode: target.alt, + sourceID: 'chat', + referrerID: '', + initialTopOffset: target.getBoundingClientRect().bottom + }); + + return true; + } } diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 15a6cfbb..f41dd127 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -43,6 +43,9 @@ export default class Chat extends Module { this._link_info = {}; + // Bind for JSX stuff + this.clickToReveal = this.clickToReveal.bind(this); + this.style = new ManagedStyle; this.context = this.settings.context({}); @@ -169,6 +172,15 @@ export default class Chat extends Module { } }); + this.settings.add('chat.filtering.click-to-reveal', { + default: false, + ui: { + path: 'Chat > Filtering >> Behavior', + title: 'Click to reveal deleted terms.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.filtering.show-deleted', { default: false, ui: { @@ -540,25 +552,6 @@ export default class Chat extends Module { } }); - this.settings.add('chat.click-emotes', { - default: true, - - ui: { - path: 'Chat > Behavior >> General', - title: 'Open emote information pages by Shift-Clicking them.', - component: 'setting-check-box' - } - }); - - this.settings.add('chat.sub-emotes', { - default: true, - ui: { - path: 'Chat > Behavior >> General', - title: 'Open Twitch subscription pages by Shift-Clicking emotes when relevant.', - component: 'setting-check-box' - } - }); - const ts = new Date(0).toLocaleTimeString().toUpperCase(), default_24 = ts.lastIndexOf('PM') === -1 && ts.lastIndexOf('AM') === -1; @@ -758,6 +751,21 @@ export default class Chat extends Module { } + clickToReveal(event) { + const target = event.target; + if ( target ) { + if ( target._ffz_visible ) + target.textContent = '×××'; + else if ( ! this.context.get('chat.filtering.click-to-reveal') ) + return; + else if ( target.dataset ) + target.textContent = target.dataset.text; + + target._ffz_visible = ! target._ffz_visible; + } + } + + standardizeWhisper(msg) { // eslint-disable-line class-methods-use-this if ( ! msg ) return msg; diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 2d86e5d1..7cb50c39 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -382,6 +382,7 @@ export const BlockedTerms = { data-text={token.text} data-tooltip-type="blocked" class="ffz-tooltip ffz--blocked" + onClick={this.clickToReveal} > ××× ); @@ -419,6 +420,172 @@ export const BlockedTerms = { } +// ============================================================================ +// AutoMod Filtering +// ============================================================================ + +const AM_DESCRIPTIONS = { + A: 'Hostility', + I: 'Discrimination', + P: 'Profanity', + S: 'Sexually Explicit Language' +}; + +export const AutomoddedTerms = { + type: 'amterm', + priority: 99, + + component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-automod-blocked.vue'), + + render(token, createElement) { + return ( + ××× + ); + }, + + tooltip(target) { + const ds = target.dataset, + flags = []; + + let cats; + try { + cats = JSON.parse(ds.categories); + for(const key in cats) { + if ( cats[key] && AM_DESCRIPTIONS[key] ) + flags.push(this.i18n.t(`chat.filtering.automod.${key}`, AM_DESCRIPTIONS[key])) + } + + } catch(err) { + flags.push('Parse Error'); + } + + + return [ + (
{ // eslint-disable-line react/jsx-key + this.i18n.t('chat.filtering.automod-term', 'AutoMod Blocked Term') + }
), + this.i18n.t('chat.filtering.automod-why', 'This was flagged as: '), + flags.join(', ') + ]; + }, + + process(tokens, msg) { + if ( ! tokens || ! tokens.length || ! msg.flags || ! Array.isArray(msg.flags.list) ) + return tokens; + + const cats = msg.flags.preferences, + flagged = msg.flags.list.filter(x => { + if ( ! x || x.startIndex == null || x.endIndex == null ) + return false; + + const y = x.categories; + if ( ! y ) + return false; + + for(const key in y) { + if ( y[key] && cats[key] ) + return true; + } + }), + f_length = flagged.length; + + if ( ! f_length ) + return tokens; + + const out = []; + let idx = 0, + fix = 0; + + for(const token of tokens) { + const length = token.length || (token.text && split_chars(token.text).length) || 0, + t_start = idx, + t_end = idx + length; + + if ( token.type !== 'text' ) { + out.push(token); + idx = t_end; + continue; + } + + const text = split_chars(token.text); + + while ( fix < f_length ) { + const flag = flagged[fix], + f_start = flag.startIndex, + f_end = flag.endIndex + 1; + + // Did this flagged term already end? Skip it! + if ( f_end < t_start ) { + fix++; + continue; + } + + // Does this flagged term start after this token? + if ( f_start > t_end ) { + // Just dump this token and move on. + out.push(token); + idx = t_end; + break; + } + + // If there's text at the beginning of the token that isn't part of + // this flagged term, output it. + if ( f_start > idx ) + out.push({ + type: 'text', + text: text.slice(idx - t_start, f_start - t_start).join('') + }); + + // Clamp the start of the filtered term to the start of this token. + let fs = f_start - t_start; + if ( fs < 0 ) + fs = 0; + + // Add the token. + out.push({ + type: 'amterm', + categories: flag.categories, + text: text.slice(fs, f_end - t_start).join('') + }); + + // Does this flagged term extend past the end of this token? + if ( f_end > t_end ) { + // Don't go to the next term, just continue processing on the + // next token. + idx = t_end; + break; + } + + idx = f_end; + fix++; + } + + // We've finished processing terms. If there's any remaining + // text in the token, push it out. + if ( idx < t_end ) { + if ( t_start === idx ) + out.push(token); + else + out.push({ + type: 'text', + text: text.slice(idx - t_start).join('') + }); + + idx = t_end; + } + } + + return out; + } +} + + // ============================================================================ // Cheers diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index 2b90da72..a3972fa3 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -143,6 +143,10 @@ export default class Twilight extends BaseSite { core = this.web_munch.getModule('core-2'); if ( core ) return this._core = core.p; + + core = this.web_munch.getModule('core-3'); + if ( core ) + return this._core = core.q; } } @@ -152,6 +156,7 @@ Twilight.KNOWN_MODULES = { react: n => n.Component && n.createElement, 'core-1': n => n.o && n.o.experiments, 'core-2': n => n.p && n.p.experiments, + 'core-3': n => n.q && n.q.experiments, cookie: n => n && n.set && n.get && n.getJSON && n.withConverter, 'extension-service': n => n.extensionService, 'chat-types': n => n.b && has(n.b, 'Message') && has(n.b, 'RoomMods'), diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index bd9cecc9..399a01cf 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -954,10 +954,14 @@ export default class ChatHook extends Module { } if ( original.message ) { - const user = original.message.user; + const user = original.message.user, + flags = original.message.flags; if ( user ) message.emotes = user.emotes; + if ( flags && this.getFilterFlagOptions ) + message.flags = this.getFilterFlagOptions(flags); + if ( typeof original.action === 'string' ) message.message = original.action; else