diff --git a/src/main.js b/src/main.js index fb84b059..490d3538 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: '-rc16.3', + major: 4, minor: 0, revision: 0, extra: '-rc17', commit: __git_commit__, build: __webpack_hash__, toString: () => diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx index a3d0c461..c2df36bd 100644 --- a/src/modules/chat/badges.jsx +++ b/src/modules/chat/badges.jsx @@ -155,67 +155,7 @@ export default class Badges extends Module { path: 'Chat > Badges >> tabs ~> Visibility', title: 'Visibility', component: 'badge-visibility', - data: () => { - const twitch = [], - game = [], - ffz = [], - addon = []; - - for(const key in this.twitch_badges) - if ( has(this.twitch_badges, key) ) { - const badge = this.twitch_badges[key], - vs = []; - let v = badge && (badge[1] || badge[0]); - - for(const key in badge) - if ( has(badge, key) ) { - const version = badge[key]; - if ( ! v ) - v = version; - - if ( version && version.image1x ) - vs.push({ - version: key, - name: version.title, - image: version.image1x, - styleImage: `url("${version.image1x}")` - }); - } - - if ( v ) - (badge.__game ? game : twitch).push({ - id: key, - provider: 'twitch', - name: v.title, - color: 'transparent', - image: v.image2x, - versions: vs, - styleImage: `url("${v.image2x}")` - }); - } - - for(const key in this.badges) - if ( has(this.badges, key) ) { - const badge = this.badges[key], - image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image; - - (/^addon/.test(key) ? addon : ffz).push({ - id: key, - provider: 'ffz', - name: badge.title, - color: badge.color || 'transparent', - image, - styleImage: `url("${image}")` - }); - } - - return [ - {title: 'Twitch', badges: twitch}, - {title: 'Twitch: Game', key: 'game', badges: game}, - {title: 'FrankerFaceZ', badges: ffz}, - {title: 'Add-on', badges: addon} - ]; - } + data: () => this.getSettingsBadges(true) } }); @@ -247,6 +187,69 @@ export default class Badges extends Module { }); } + getSettingsBadges(include_addons) { + const twitch = [], + game = [], + ffz = [], + addon = []; + + for(const key in this.twitch_badges) + if ( has(this.twitch_badges, key) ) { + const badge = this.twitch_badges[key], + vs = []; + let v = badge && (badge[1] || badge[0]); + + for(const key in badge) + if ( has(badge, key) ) { + const version = badge[key]; + if ( ! v ) + v = version; + + if ( version && version.image1x ) + vs.push({ + version: key, + name: version.title, + image: version.image1x, + styleImage: `url("${version.image1x}")` + }); + } + + if ( v ) + (badge.__game ? game : twitch).push({ + id: key, + provider: 'twitch', + name: v.title, + color: 'transparent', + image: v.image2x, + versions: vs, + styleImage: `url("${v.image2x}")` + }); + } + + if ( include_addons ) + for(const key in this.badges) + if ( has(this.badges, key) ) { + const badge = this.badges[key], + image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image; + + (/^addon/.test(key) ? addon : ffz).push({ + id: key, + provider: 'ffz', + name: badge.title, + color: badge.color || 'transparent', + image, + styleImage: `url("${image}")` + }); + } + + return [ + {title: 'Twitch', badges: twitch}, + {title: 'Twitch: Game', key: 'game', badges: game}, + {title: 'FrankerFaceZ', badges: ffz}, + {title: 'Add-on', badges: addon} + ]; + } + onEnable() { this.parent.context.on('changed:chat.badges.custom-mod', this.rebuildAllCSS, this); @@ -278,9 +281,19 @@ export default class Badges extends Module { if ( ! bd ) continue; + let title = bd.title || global_badge.title; + if ( d.data ) { + if ( d.badge === 'subscriber' ) { + title = this.i18n.t('badges.subscriber.months', '%{title} (%{count} Month%{count|en_plural})', { + title, + count: d.data + }); + } + } + out.push(
{show_previews && } - {bd.title || global_badge.title} + {title}
); /*out.push(e('div', {className: 'ffz-badge-tip'}, [ @@ -332,6 +345,7 @@ export default class Badges extends Module { out = [], slotted = {}, twitch_badges = msg.badges || {}, + dynamic_data = msg.badgeDynamicData || {}, user = msg.user || {}, user_id = user.id, @@ -357,7 +371,8 @@ export default class Badges extends Module { else slot = last_slot++; - const urls = badge_id === 'moderator' && custom_mod && room && room.data && room.data.mod_urls, + const data = dynamic_data[badge_id], + urls = badge_id === 'moderator' && custom_mod && room && room.data && room.data.mod_urls, badges = []; if ( urls ) { @@ -366,14 +381,16 @@ export default class Badges extends Module { provider: 'ffz', image: urls[4] || urls[2] || urls[1], color: '#34ae0a', - title: bd ? bd.title : 'Moderator' + title: bd ? bd.title : 'Moderator', + data }); } else badges.push({ provider: 'twitch', badge: badge_id, - version + version, + data }); slotted[slot] = { diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 1460b50f..c578d9ac 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -186,6 +186,7 @@ export default class Chat extends Module { ui: { path: 'Chat > Behavior >> Deleted Messages', title: 'Deleted Message Style', + description: 'This style will be applied to deleted messages showed in Detailed rendering mode to differentiate them from normal chat messages.', component: 'setting-select-box', data: [ {value: 0, title: 'Faded'}, @@ -200,8 +201,8 @@ export default class Chat extends Module { default: false, ui: { path: 'Chat > Behavior >> Deleted Messages', - title: 'Deleted Message Rendering', - description: 'This, when set, overrides the mode selected in Twitch Chat settings. We do this to allow non-moderators access to the setting.', + title: 'Rendering Mode', + description: 'This, when set, overrides the mode selected in Twitch chat settings. We do this to allow non-moderators access to the setting.', component: 'setting-select-box', data: [ {value: false, title: 'Do Not Override'}, @@ -216,7 +217,7 @@ export default class Chat extends Module { default: 1, ui: { path: 'Chat > Behavior >> Deleted Messages', - title: 'Display Deletion Reason', + title: 'Display Reason', component: 'setting-select-box', data: [ {value: 0, title: 'Never'}, @@ -285,6 +286,177 @@ export default class Chat extends Module { } }); + this.settings.add('chat.filtering.highlight-basic-users', { + default: [], + type: 'array_merge', + always_inherit: true, + ui: { + path: 'Chat > Filtering >> Highlight Users', + component: 'basic-terms', + colored: true, + words: false + } + }); + + this.settings.add('chat.filtering.highlight-basic-users--color-regex', { + requires: ['chat.filtering.highlight-basic-users'], + process(ctx) { + const val = ctx.get('chat.filtering.highlight-basic-users'); + if ( ! val || ! val.length ) + return null; + + const colors = new Map; + + for(const item of val) { + const c = item.c || null, + t = item.t; + + let v = item.v; + + if ( t === 'glob' ) + v = glob_to_regex(v); + + else if ( t !== 'raw' ) + v = escape_regex(v); + + if ( ! v || ! v.length ) + continue; + + try { + new RegExp(v); + } catch(err) { + continue; + } + + if ( colors.has(c) ) + colors.get(c).push(v); + else { + colors.set(c, [v]); + } + } + + for(const [key, list] of colors) { + colors.set(key, new RegExp(`^${list.join('|')}$`, 'gi')); + } + + return colors; + } + }); + + + this.settings.add('chat.filtering.highlight-basic-users-blocked', { + default: [], + type: 'array_merge', + always_inherit: true, + ui: { + path: 'Chat > Filtering >> Blocked Users', + component: 'basic-terms', + removable: true, + words: false + } + }); + + + this.settings.add('chat.filtering.highlight-basic-users-blocked--regex', { + requires: ['chat.filtering.highlight-basic-blocked'], + process(ctx) { + const val = ctx.get('chat.filtering.highlight-basic-users-blocked'); + if ( ! val || ! val.length ) + return null; + + const out = [[], []]; + + for(const item of val) { + const t = item.t; + let v = item.v; + + if ( t === 'glob' ) + v = glob_to_regex(v); + + else if ( t !== 'raw' ) + v = escape_regex(v); + + if ( ! v || ! v.length ) + continue; + + out[item.remove ? 1 : 0].push(v); + } + + return out.map(data => { + if ( ! data.length ) + return null; + + return new RegExp(`^${data.join('|')}$`, 'gi'); + }); + } + }); + + + this.settings.add('chat.filtering.highlight-basic-badges', { + default: [], + type: 'array_merge', + always_inherit: true, + ui: { + path: 'Chat > Filtering >> Highlight Badges', + component: 'badge-highlighting', + colored: true, + data: () => this.badges.getSettingsBadges() + } + }); + + + this.settings.add('chat.filtering.highlight-basic-badges--colors', { + requires: ['chat.filtering.highlight-basic-badges'], + process(ctx) { + const val = ctx.get('chat.filtering.highlight-basic-badges'); + if ( ! val || ! val.length ) + return null; + + const colors = new Map; + + for(const item of val) { + const c = item.c || null, + v = item.v; + + colors.set(v, c); + } + + return colors; + } + }); + + + this.settings.add('chat.filtering.highlight-basic-badges-blocked', { + default: [], + type: 'array_merge', + always_inherit: true, + ui: { + path: 'Chat > Filtering >> Blocked Badges', + component: 'badge-highlighting', + removable: true, + data: () => this.badges.getSettingsBadges() + } + }); + + this.settings.add('chat.filtering.highlight-basic-badges-blocked--list', { + requires: ['chat.filtering.highlight-basic-badges-blocked'], + process(ctx) { + const val = ctx.get('chat.filtering.highlight-basic-badges-blocked'); + if ( ! val || ! val.length ) + return null; + + const out = [[], []]; + for(const item of val) + if ( item.v ) + out[item.remove ? 1 : 0].push(item.v); + + if ( ! out[0].length && ! out[1].length ) + return null; + + return out; + } + }); + this.settings.add('chat.filtering.highlight-basic-terms', { default: [], @@ -297,7 +469,6 @@ export default class Chat extends Module { } }); - this.settings.add('chat.filtering.highlight-basic-terms--color-regex', { requires: ['chat.filtering.highlight-basic-terms'], process(ctx) { @@ -636,10 +807,10 @@ export default class Chat extends Module { if ( id && typeof id === 'number' ) id = `${id}`; - if ( this.user_ids[id] ) + if ( id && this.user_ids[id] ) user = this.user_ids[id]; - else if ( this.users[login] && ! no_login ) + else if ( login && this.users[login] && ! no_login ) user = this.users[login]; if ( user && user.destroyed ) @@ -696,10 +867,10 @@ export default class Chat extends Module { if ( id && typeof id === 'number' ) id = `${id}`; - if ( this.room_ids[id] ) + if ( id && this.room_ids[id] ) room = this.room_ids[id]; - else if ( this.rooms[login] && ! no_login ) + else if ( login && this.rooms[login] && ! no_login ) room = this.rooms[login]; if ( room && room.destroyed ) @@ -863,7 +1034,9 @@ export default class Chat extends Module { if ( ! user ) user = msg.user = {}; - user.color = user.color || user.chatColor || null; + const ext = msg.extension || {}; + + user.color = user.color || user.chatColor || ext.chatColor || null; user.type = user.type || user.userType || null; user.id = user.id || user.userID || null; user.login = user.login || user.userLogin || null; @@ -888,7 +1061,13 @@ export default class Chat extends Module { // Standardize Badges if ( ! msg.badges && user.displayBadges ) { const b = msg.badges = {}; - for(const item of msg.user.displayBadges) + for(const item of user.displayBadges) + b[item.setID] = item.version; + } + + if ( ! msg.badges && ext.displayBadges ) { + const b = msg.badges = {}; + for(const item of ext.displayBadges) b[item.setID] = item.version; } diff --git a/src/modules/chat/room.js b/src/modules/chat/room.js index 8946ba56..51f939cc 100644 --- a/src/modules/chat/room.js +++ b/src/modules/chat/room.js @@ -185,10 +185,10 @@ export default class Room { if ( id && typeof id === 'number' ) id = `${id}`; - if ( this.user_ids[id] ) + if ( id && this.user_ids[id] ) user = this.user_ids[id]; - else if ( this.users[login] && ! no_login ) + else if ( login && this.users[login] && ! no_login ) user = this.users[login]; if ( user && user.destroyed ) diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 7cb50c39..120b58da 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -268,6 +268,119 @@ export const Mentions = { // Custom Highlight Terms // ============================================================================ +export const UserHighlights = { + type: 'user_highlight', + priority: 90, + + process(tokens, msg, user) { + if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') ) + return tokens; + + const colors = this.context.get('chat.filtering.highlight-basic-users--color-regex'); + if ( ! colors || ! colors.size ) + return tokens; + + const u = msg.user; + for(const [color, regex] of colors) { + if ( regex.test(u.login) || regex.test(u.displayName) ) { + msg.mentioned = true; + if ( color ) { + msg.mention_color = color; + return tokens; + } + } + } + + return tokens; + } +} + +export const BlockedUsers = { + type: 'user_block', + priority: 100, + + process(tokens, msg, user) { + if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') ) + return tokens; + + const u = msg.user, + regexes = this.context.get('chat.filtering.highlight-basic-users-blocked--regex'); + if ( ! regexes ) + return tokens; + + if ( regexes[1] && (regexes[1].test(u.login) || regexes[1].test(u.displayName)) ) { + msg.deleted = true; + msg.ffz_removed = true; + } + + if ( ! msg.deleted && regexes[0] && (regexes[0].test(u.login) || regexes[0].test(u.displayName)) ) + msg.deleted = true; + + return tokens; + } +} + +export const BadgeHighlights = { + type: 'badge_highlight', + priority: 80, + + process(tokens, msg, user) { + if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') ) + return tokens; + + const badges = msg.badges; + if ( ! badges ) + return tokens; + + const colors = this.context.get('chat.filtering.highlight-basic-badges--colors'); + if ( ! colors || ! colors.size ) + return tokens; + + for(const badge of Object.keys(badges)) { + if ( colors.has(badge) ) { + const color = colors.get(badge); + msg.mentioned = true; + if ( color ) { + msg.mention_color = color; + return tokens; + } + } + } + + return tokens; + } +} + +export const BlockedBadges = { + type: 'badge_block', + priority: 100, + process(tokens, msg, user) { + if ( user && user.login && user.login == msg.user.login && ! this.context.get('chat.filtering.process-own') ) + return tokens; + + const badges = msg.badges; + if ( ! badges ) + return tokens; + + const list = this.context.get('chat.filtering.highlight-basic-badges-blocked--list'); + if ( ! list || (! list[0].length && ! list[1].length) ) + return tokens; + + for(const badge of Object.keys(badges)) { + if ( list[1].includes(badge) ) { + msg.deleted = true; + msg.ffz_removed = true; + return tokens; + } + + if ( ! msg.deleted && list[0].includes(badge) ) + msg.deleted = true; + } + + return tokens; + } +} + export const CustomHighlights = { type: 'highlight', priority: 100, @@ -310,7 +423,7 @@ export const CustomHighlights = { out.push({type: 'text', text: text.slice(idx, nix)}); msg.mentioned = true; - msg.mention_color = color; + msg.mention_color = color || msg.mention_color; out.push({ type: 'highlight', diff --git a/src/modules/main_menu/components/badge-highlighting.vue b/src/modules/main_menu/components/badge-highlighting.vue new file mode 100644 index 00000000..557cb070 --- /dev/null +++ b/src/modules/main_menu/components/badge-highlighting.vue @@ -0,0 +1,99 @@ + + + \ No newline at end of file diff --git a/src/modules/main_menu/components/badge-term-editor.vue b/src/modules/main_menu/components/badge-term-editor.vue new file mode 100644 index 00000000..9d479925 --- /dev/null +++ b/src/modules/main_menu/components/badge-term-editor.vue @@ -0,0 +1,213 @@ + + + \ No newline at end of file diff --git a/src/modules/main_menu/components/basic-terms.vue b/src/modules/main_menu/components/basic-terms.vue index efb3878e..a8c5dc0c 100644 --- a/src/modules/main_menu/components/basic-terms.vue +++ b/src/modules/main_menu/components/basic-terms.vue @@ -3,6 +3,7 @@ - + @@ -137,6 +137,10 @@ let id = 0; export default { props: { term: Object, + words: { + type: Boolean, + default: true + }, colored: { type: Boolean, default: false diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index fc024443..9584e95d 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -36,13 +36,19 @@ export default class ChatLine extends Module { this.ChatLine = this.fine.define( 'chat-line', - n => n.renderMessageBody && n.props && !has(n.props, 'hasModPermissions'), + n => n.renderMessageBody && n.props && ! n.onExtensionNameClick && !has(n.props, 'hasModPermissions'), + Twilight.CHAT_ROUTES + ); + + this.ExtensionLine = this.fine.define( + 'extension-line', + n => n.renderMessageBody && n.onExtensionNameClick, Twilight.CHAT_ROUTES ); this.ChatRoomLine = this.fine.define( 'chat-room-line', - n => n.renderMessageBody && n.props && has(n.props, 'hasModPermissions'), + n => n.renderMessageBody && n.props && ! n.onExtensionNameClick && has(n.props, 'hasModPermissions'), Twilight.CHAT_ROUTES ); @@ -78,7 +84,11 @@ export default class ChatLine extends Module { this.chat.context.on('changed:chat.filtering.process-own', this.updateLines, this); this.chat.context.on('changed:chat.timestamp-format', this.updateLines, this); this.chat.context.on('changed:chat.filtering.highlight-basic-terms--color-regex', this.updateLines, this); + this.chat.context.on('changed:chat.filtering.highlight-basic-users--color-regex', this.updateLines, this); + this.chat.context.on('changed:chat.filtering.highlight-basic-badges--colors', this.updateLines, this); this.chat.context.on('changed:chat.filtering.highlight-basic-blocked--regex', this.updateLines, this); + this.chat.context.on('changed:chat.filtering.highlight-basic-users-blocked--regex', this.updateLines, this); + this.chat.context.on('changed:chat.filtering.highlight-basic-badges-blocked--list', this.updateLines, this); const t = this, React = await this.web_munch.findModule('react'); @@ -692,6 +702,8 @@ export default class ChatLine extends Module { }, out); } catch(err) { + t.log.info(err); + t.log.capture(err, { extra: { props: this.props @@ -704,6 +716,85 @@ export default class ChatLine extends Module { // Do this after a short delay to hopefully reduce the chance of React // freaking out on us. setTimeout(() => this.ChatLine.forceUpdate()); + }); + + this.ExtensionLine.ready(cls => { + const old_render = cls.prototype.render; + + cls.prototype.render = function() { try { + if ( ! this.props.installedExtensions ) + return null; + + const msg = t.chat.standardizeMessage(this.props.message), + ext = msg && msg.extension; + if( ! ext ) + return null; + + if ( ! this.props.installedExtensions.some(val => { + const e = val.extension; + return e && e.clientID === ext.clientID && e.version === ext.version; + }) ) + return null; + + const color = t.parent.colors.process(ext.chatColor); + let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined; + if ( ! room && this.props.channelID ) { + const r = t.chat.getRoom(this.props.channelID, null, true); + if ( r && r.login ) + room = msg.roomLogin = r.login; + } + + const u = t.site.getUser(), + r = {id: this.props.channelID, login: room}, + + tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r), + rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg), + bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null; + + if ( ! tokens.length ) + return null; + + return e('div', { + className: `chat-line__message${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`, + style: {backgroundColor: bg_css}, + 'data-room-id': r.id, + 'data-room': r.login, + 'data-extension': ext.clientID + }, [ + this.props.showTimestamps && e('span', { + className: 'chat-line__timestamp' + }, t.chat.formatTime(msg.timestamp)), + e('span', { + className: 'chat-line__message--badges' + }, t.chat.badges.render(msg, e)), + e('button', { + className: 'chat-line__username notranslate', + style: { color }, + onClick: this.onExtensionNameClick + }, e('span', { + className: 'chat-author__display-name' + }, ext.displayName)), + e('span', null, ': '), + e('span', { + className: 'message' + }, t.chat.renderTokens(tokens, e)), + rich_content && e(FFZRichContent, rich_content) + ]); + + } catch(err) { + t.log.info(err); + t.log.capture(err, { + extra: { + props: this.props + } + }); + + return old_render.call(this); + } } + + // Do this after a short delay to hopefully reduce the chance of React + // freaking out on us. + setTimeout(() => this.ExtensionLine.forceUpdate()); }) } @@ -722,6 +813,14 @@ export default class ChatLine extends Module { } } + for(const inst of this.ExtensionLine.instances) { + const msg = inst.props.message; + if ( msg ) { + msg.ffz_tokens = null; + msg.mentioned = msg.mention_color = null; + } + } + for(const inst of this.ChatRoomLine.instances) { const msg = inst.props.message; if ( msg ) { @@ -737,6 +836,7 @@ export default class ChatLine extends Module { } this.ChatLine.forceUpdate(); + this.ExtensionLine.forceUpdate(); this.ChatRoomLine.forceUpdate(); this.WhisperLine.forceUpdate();