diff --git a/src/i18n.js b/src/i18n.js index c9681ae4..f9a6452c 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -7,7 +7,7 @@ import Parser from '@ffz/icu-msgparser'; import {SERVER} from 'utilities/constants'; -import {get, pick_random, has, timeout} from 'utilities/object'; +import {get, pick_random, timeout} from 'utilities/object'; import Module from 'utilities/module'; import NewTransCore from 'utilities/translation-core'; @@ -45,7 +45,7 @@ const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'Uw upper: (key, ast) => transformText(ast, n => n.toUpperCase()), lower: (key, ast) => transformText(ast, n => n.toLowerCase()), append_key: (key, ast) => [...ast, ` (${key})`], - set_key: (key, ast) => [key], + set_key: key => [key], owo: (key, ast) => transformText(ast, owo) }; diff --git a/src/main.js b/src/main.js index 68c1ef4c..8944dba7 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: '-rc21.6', + major: 4, minor: 0, revision: 0, extra: '-rc21.7', commit: __git_commit__, build: __webpack_hash__, toString: () => diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 3686bd8e..910a5fc1 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -45,6 +45,7 @@ export default class Chat extends Module { // Bind for JSX stuff this.clickToReveal = this.clickToReveal.bind(this); + this.handleMentionClick = this.handleMentionClick.bind(this); this.style = new ManagedStyle; @@ -582,6 +583,16 @@ export default class Chat extends Module { }); + this.settings.add('chat.filtering.clickable-mentions', { + default: false, + ui: { + component: 'setting-check-box', + path: 'Chat > Filtering >> Appearance', + title: 'Enable opening viewer cards by clicking mentions in chat.' + } + }); + + this.settings.add('chat.filtering.highlight-mentions', { default: false, ui: { @@ -950,6 +961,32 @@ export default class Chat extends Module { } + handleMentionClick(event) { + if ( ! this.context.get('chat.filtering.clickable-mentions') ) + return; + + const target = event.target, + ds = target && target.dataset; + + if ( ! ds || ! ds.login ) + return; + + const fine = this.resolve('site.fine'); + if ( ! fine ) + return; + + const chat = fine.searchParent(event.target, n => n.props && n.props.onUsernameClick); + if ( ! chat ) + return; + + chat.props.onUsernameClick( + ds.login, + undefined, undefined, + event.currentTarget.getBoundingClientRect().bottom + ); + } + + clickToReveal(event) { const target = event.target; if ( target ) { diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 0dcc510e..9da39fcf 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -12,7 +12,8 @@ import {TWITCH_EMOTE_BASE, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/const const EMOTE_CLASS = 'chat-image chat-line__message--emote', LINK_REGEX = /([^\w@#%\-+=:~])?((?:(https?:\/\/)?(?:[\w@#%\-+=:~]+\.)+[a-z]{2,6}(?:\/[\w./@#%&()\-+=:?~]*)?))([^\w./@#%&()\-+=:?~]|\s|$)/g, - MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex + //MENTION_REGEX = /([^\w@#%\-+=:~])?(@([^\u0000-\u007F]+|\w+)+)([^\w./@#%&()\-+=:?~]|\s|$)/g; // eslint-disable-line no-control-regex + MENTION_REGEX = /^(['"*([{<\\/]*)(@?)((?:[^\u0000-\u007F]|[\w-])+)/; // eslint-disable-line no-control-regex // ============================================================================ @@ -201,16 +202,29 @@ export const Mentions = { component: () => import(/* webpackChunkName: 'vue-chat' */ './components/chat-mention.vue'), - render(token, createElement) { + oldRender(token, createElement) { return ( {token.text} ); }, + render(token, createElement) { + return ( + {token.text} + ) + }, + process(tokens, msg, user) { if ( ! tokens || ! tokens.length ) return tokens; + if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') ) + return tokens; + let regex, login, display; if ( user && user.login ) { login = user.login.toLowerCase(); @@ -218,7 +232,8 @@ export const Mentions = { if ( display === login ) display = null; - regex = new RegExp(`([^\\w@#%\\-+=:~]|\\b)?(@?(${user.login.toLowerCase()}${display ? `|${display}` : ''})|@([^\\u0000-\\u007F]+|\\w+)+)([^\\w.\\/@#%&()\\-+=:?~]|\\s|\\b|$)`, 'gi'); + regex = new RegExp(`^(['"*([{<\\/]*)(?:(@?)(${user.login.toLowerCase()}${display ? `|${display}` : ''})|@((?:[^\u0000-\u007F]|[\\w-])+))`, 'i'); + //regex = new RegExp(`([^\\w@#%\\-+=:~]|\\b)?(@?(${user.login.toLowerCase()}${display ? `|${display}` : ''})|@([^\\u0000-\\u007F]+|\\w+)+)([^\\w.\\/@#%&()\\-+=:?~]|\\s|\\b|$)`, 'gi'); } else regex = MENTION_REGEX; @@ -229,34 +244,52 @@ export const Mentions = { continue; } - regex.lastIndex = 0; - const text = token.text; - let idx = 0, match; + let text = []; - while((match = regex.exec(text))) { - const nix = match.index + (match[1] ? match[1].length : 0), - m = match[3] || match[4], - ml = m.toLowerCase(), - me = ml === login || ml === display; + for(const segment of token.text.split(/ +/)) { + const match = regex.exec(segment); + if ( match ) { + // If we have pending text, join it together. + if ( text.length || match[1]) { + out.push({ + type: 'text', + text: `${text.join(' ')} ${match[1] || ''}` + }); + text = []; + } - if ( idx !== nix ) - out.push({type: 'text', text: text.slice(idx, nix)}); + let recipient, + mentioned = false, + at = match[2]; - if ( me ) - msg.mentioned = true; + if ( match[4] ) { + recipient = match[4]; + at = '@'; - out.push({ - type: 'mention', - text: match[2], - me, - recipient: m - }); + } else { + recipient = match[3]; + mentioned = true; + } - idx = nix + match[2].length; + out.push({ + type: 'mention', + text: `${at}${recipient}`, + me: mentioned, + recipient + }); + + if ( mentioned ) + msg.mentioned = true; + + // Push the remaining text from the token. + text.push(segment.substr(match[0].length)); + + } else + text.push(segment); } - if ( idx < text.length ) - out.push({type: 'text', text: text.slice(idx)}); + if ( text.length > 1 || (text.length === 1 && text[0] !== '') ) + out.push({type: 'text', text: text.join(' ')}) } return out; diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 99c69e8e..92a52173 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -452,6 +452,7 @@ export default class ChatHook extends Module { this.chat.context.on('changed:chat.fix-bad-emotes', this.updateChatLines, this); this.chat.context.on('changed:chat.filtering.display-deleted', this.updateChatLines, this); this.chat.context.on('changed:chat.filtering.display-mod-action', this.updateChatLines, this); + this.chat.context.on('changed:chat.filtering.clickable-mentions', val => this.css_tweaks.toggle('clickable-mentions', val)); this.chat.context.on('changed:chat.lines.alternate', val => { this.css_tweaks.toggle('chat-rows', val); @@ -475,6 +476,8 @@ export default class ChatHook extends Module { this.css_tweaks.toggle('chat-deleted-strike', val === 1 || val === 2); this.css_tweaks.toggle('chat-deleted-fade', val < 2); + this.css_tweaks.toggle('clickable-mentions', this.chat.context.get('chat.filtering.clickable-mentions')); + this.css_tweaks.toggleHide('pinned-cheer', !this.chat.context.get('chat.bits.show-pinned')); this.css_tweaks.toggle('hide-bits', !this.chat.context.get('chat.bits.show')); this.css_tweaks.toggle('chat-rows', this.chat.context.get('chat.lines.alternate')); diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss index ab41a174..619e6dcd 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-token.scss @@ -1,4 +1,5 @@ .ffz--highlight, +.mention-fragment--recipient, .ffz--mention-me { border-radius: .5rem; padding: .3rem;