diff --git a/fontello.config.json b/fontello.config.json index b2c3e9d1..2a121afa 100644 --- a/fontello.config.json +++ b/fontello.config.json @@ -753,6 +753,12 @@ "css": "volume-off", "code": 59461, "src": "elusive" + }, + { + "uid": "c6be5a58ee4e63a5ec399c2b0d15cf2c", + "css": "reply", + "code": 61714, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/package.json b/package.json index fae76293..153672e6 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.27", + "version": "4.20.28", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/res/font/ffz-fontello.eot b/res/font/ffz-fontello.eot index cd269901..5f278bb0 100644 Binary files a/res/font/ffz-fontello.eot and b/res/font/ffz-fontello.eot differ diff --git a/res/font/ffz-fontello.svg b/res/font/ffz-fontello.svg index 63608d1f..1ffc6136 100644 --- a/res/font/ffz-fontello.svg +++ b/res/font/ffz-fontello.svg @@ -166,6 +166,8 @@ + + diff --git a/res/font/ffz-fontello.ttf b/res/font/ffz-fontello.ttf index fa3537ba..8694f59b 100644 Binary files a/res/font/ffz-fontello.ttf and b/res/font/ffz-fontello.ttf differ diff --git a/res/font/ffz-fontello.woff b/res/font/ffz-fontello.woff index 604d617f..fe3639b3 100644 Binary files a/res/font/ffz-fontello.woff and b/res/font/ffz-fontello.woff differ diff --git a/res/font/ffz-fontello.woff2 b/res/font/ffz-fontello.woff2 index 2c36f996..3a7ec52a 100644 Binary files a/res/font/ffz-fontello.woff2 and b/res/font/ffz-fontello.woff2 differ diff --git a/src/main.js b/src/main.js index 76d8faed..75be9dcb 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,7 @@ import AddonManager from './addons'; import ExperimentManager from './experiments'; import {TranslationManager} from './i18n'; import SocketClient from './socket'; +//import PubSubClient from './pubsub'; import Site from 'site'; import Vue from 'utilities/vue'; //import Timing from 'utilities/timing'; @@ -56,6 +57,7 @@ class FrankerFaceZ extends Module { this.inject('experiments', ExperimentManager); this.inject('i18n', TranslationManager); this.inject('socket', SocketClient); + //this.inject('pubsub', PubSubClient); this.inject('site', Site); this.inject('addons', AddonManager); diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/index.jsx index b42dd576..935b7393 100644 --- a/src/modules/chat/actions/index.jsx +++ b/src/modules/chat/actions/index.jsx @@ -65,6 +65,7 @@ export default class Actions extends Module { ), default: [ + {v: {action: 'reply', appearance: {type: 'icon', icon: 'ffz-i-reply'}, options: {}, display: {}}}, {v: {action: 'ban', appearance: {type: 'icon', icon: 'ffz-i-block'}, options: {}, display: {mod: true, mod_icons: true, deleted: false}}}, {v: {action: 'unban', appearance: {type: 'icon', icon: 'ffz-i-ok'}, options: {}, display: {mod: true, mod_icons: true, deleted: true}}}, {v: {action: 'timeout', appearance: {type: 'icon', icon: 'ffz-i-clock'}, display: {mod: true, mod_icons: true}}}, @@ -418,6 +419,9 @@ export default class Actions extends Module { (disp.followersOnly != null && disp.followersOnly !== current_room.followersOnly) ) continue; + if ( maybe_call(act.hidden, this, data, null, current_room, current_user, mod_icons) ) + continue; + if ( act.override_appearance ) { const out = act.override_appearance.call(this, Object.assign({}, ap), data, null, current_room, current_user, mod_icons); if ( out ) @@ -539,6 +543,9 @@ export default class Actions extends Module { (disp.deleted != null && disp.deleted !== !!msg.deleted) ) continue; + if ( maybe_call(act.hidden, this, data, msg, r, u, mod_icons) ) + continue; + if ( act.override_appearance ) { const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, r, u, mod_icons); if ( out ) @@ -600,11 +607,9 @@ export default class Actions extends Module { renderInline(msg, mod_icons, current_user, current_room, createElement) { const actions = []; - if ( msg.user && current_user && current_user.login === msg.user.login ) - return; - const current_level = this.getUserLevel(current_room, current_user), - msg_level = this.getUserLevel(current_room, msg.user); + msg_level = this.getUserLevel(current_room, msg.user), + is_self = msg.user && current_user && current_user.login === msg.user.login; if ( current_level < 3 ) mod_icons = false; @@ -630,6 +635,12 @@ export default class Actions extends Module { (disp.deleted != null && disp.deleted !== !!msg.deleted) ) continue; + if ( is_self && ! act.can_self ) + continue; + + if ( maybe_call(act.hidden, this, data, msg, current_room, current_user, mod_icons) ) + continue; + if ( act.override_appearance ) { const out = act.override_appearance.call(this, Object.assign({}, ap), data, msg, current_room, current_user, mod_icons); if ( out ) diff --git a/src/modules/chat/actions/types.jsx b/src/modules/chat/actions/types.jsx index d39694b7..6e94145a 100644 --- a/src/modules/chat/actions/types.jsx +++ b/src/modules/chat/actions/types.jsx @@ -1,5 +1,54 @@ 'use strict'; + +// ============================================================================ +// Send Reply +// ============================================================================ + +export const reply = { + presets: [{ + appearance: { + type: 'icon', + icon: 'ffz-i-reply' + } + }], + + required_context: ['message'], + + title: 'Reply to Message', + description: 'Allows you to directly reply to another user\'s message.', + + can_self: true, + + tooltip() { + return this.i18n.t('chat.actions.reply', 'Reply to Message') + }, + + hidden(data, message, current_room, current_user) { + const id = message?.id; + if ( typeof id !== 'string' || ! /^[0-9a-f]+-[0-9a-f]+/.test(id) ) + return true; + + if ( ! message.message || message.deleted || (current_user && current_user.login === message.user?.login) || ! current_user?.can_reply ) + return true; + + if ( message?.reply ) + return true; + }, + + click(event) { + const fine = this.resolve('site.fine'), + line = fine ? fine.searchParent(event.target, n => n.setMessageTray && n.props && n.props.message) : null; + + if ( ! line ) + return; + + line.ffzOpenReply(); + //line.setMessageTray(line.props.message, line.props.message.message); + } +} + + // ============================================================================ // Edit Overrides // ============================================================================ diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index 1a5248b3..81e965b2 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -50,6 +50,7 @@ export default class Chat extends Module { // Bind for JSX stuff this.clickToReveal = this.clickToReveal.bind(this); this.handleMentionClick = this.handleMentionClick.bind(this); + this.handleReplyClick = this.handleReplyClick.bind(this); this.style = new ManagedStyle; @@ -1073,6 +1074,19 @@ export default class Chat extends Module { } + handleReplyClick(event) { + const target = event.target, + fine = this.resolve('site.fine'); + + if ( ! target || ! fine ) + return; + + const chat = fine.searchParent(target, n => n.props && n.props.reply && n.setOPCardTray); + if ( chat ) + chat.setOPCardTray(chat.props.reply); + } + + handleMentionClick(event) { if ( ! this.context.get('chat.filtering.clickable-mentions') ) return; @@ -1087,7 +1101,7 @@ export default class Chat extends Module { if ( ! fine ) return; - const chat = fine.searchParent(event.target, n => n.props && n.props.onUsernameClick); + const chat = fine.searchParent(target, n => n.props && n.props.onUsernameClick); if ( ! chat ) return; @@ -1174,6 +1188,25 @@ export default class Chat extends Module { } + tokenizeReply(reply) { + if ( ! reply ) + return null; + + return [ + { + type: 'reply', + text: reply.parentDisplayName, + color: this.color_cache ? this.color_cache.get(reply.parentUserLogin) : null, + recipient: reply.parentUserLogin + }, + { + type: 'text', + text: ' ' + } + ]; + } + + standardizeMessage(msg) { // eslint-disable-line class-methods-use-this if ( ! msg ) return msg; @@ -1487,7 +1520,7 @@ export default class Chat extends Module { } - renderTokens(tokens, e) { + renderTokens(tokens, e, reply) { if ( ! e ) e = createElement; @@ -1505,6 +1538,10 @@ export default class Chat extends Module { let res; + // If we have a reply, skip the initial mention. + if ( reply && i === 0 && type === 'mention' && token.recipient && token.recipient === reply.parentUserLogin ) + continue; + if ( type === 'text' ) res = e('span', { className: 'text-fragment', @@ -1512,7 +1549,7 @@ export default class Chat extends Module { }, token.text); else if ( tk ) - res = tk.render.call(this, token, e); + res = tk.render.call(this, token, e, reply); else res = e('em', { diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index e5506f13..0bce585f 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -232,6 +232,61 @@ Links.tooltip.delayHide = function(target) { }*/ +// ============================================================================ +// Replies (Styled Like Mentions) +// ============================================================================ + +export const Replies = { + type: 'reply', + priority: 0, + + component: () => null, + + render(token, createElement) { + let color = token.color; + if ( color ) { + const chat = this.resolve('site.chat'); + color = chat ? chat.colors.process(color) : color; + } + + return ( + {token.text} + ) + }, + + tooltip(target) { + const fine = this.resolve('site.fine'); + if ( ! target || ! fine ) + return null; + + const chat = fine.searchParent(target, n => n.props && n.props.reply && n.setOPCardTray), + reply = chat?.props?.reply; + if ( ! reply ) + return null; + + return [ + createElement('strong', {}, this.i18n.t('chat.reply-to', 'Replying To:')), + '\n\n', + createElement('div', {className: 'tw-align-left'}, [ + createElement('strong', {}, reply.parentDisplayName), + ': ', + reply.parentMessageBody + ]) + ]; + }, + + process(tokens) { + return tokens; + } +} + + // ============================================================================ // Mentions // ============================================================================ diff --git a/src/sites/twitch-twilight/modules/channel.jsx b/src/sites/twitch-twilight/modules/channel.jsx index 167eb559..e9edecd9 100644 --- a/src/sites/twitch-twilight/modules/channel.jsx +++ b/src/sites/twitch-twilight/modules/channel.jsx @@ -46,6 +46,20 @@ export default class Channel extends Module { } }); + this.settings.add('channel.auto-skip-trailer', { + default: false, + ui: { + path: 'Channel > Behavior >> General', + title: 'Automatically skip channel trailers.', + component: 'setting-check-box' + }, + + changed: val => { + if ( val ) + this.ChannelTrailer.each(el => this.maybeSkipTrailer(el)); + } + }) + this.settings.add('channel.auto-click-chat', { default: false, ui: { @@ -83,6 +97,13 @@ export default class Channel extends Module { ); + this.ChannelTrailer = this.elemental.define( + 'channel-trailer', '.channel-trailer-player__wrapper', + USER_PAGES, + {attributes: true}, 1 + ); + + this.ChannelRoot = this.elemental.define( 'channel-root', '.channel-root', USER_PAGES, @@ -115,6 +136,10 @@ export default class Channel extends Module { this.on('i18n:update', this.updateLinks, this); + this.ChannelTrailer.on('mount', this.maybeSkipTrailer, this); + this.ChannelTrailer.on('update', this.maybeSkipTrailer, this); + this.ChannelTrailer.each(el => this.maybeSkipTrailer(el)); + this.ChannelPanels.on('mount', this.updatePanelTips, this); this.ChannelPanels.on('update', this.updatePanelTips, this); this.ChannelPanels.on('unmount', this.removePanelTips, this); @@ -139,6 +164,17 @@ export default class Channel extends Module { this.checkNavigation(); } + maybeSkipTrailer(el) { + if ( ! this.settings.get('channel.auto-skip-trailer') ) + return; + + const inst = this.fine.searchParent(el, n => n.props && n.props.onDismiss); + if ( inst ) { + this.log.info('Automatically skipping channel trailer.'); + inst.props.onDismiss(); + } + } + updatePanelTips(inst) { if ( ! inst ) { for(const inst of this.ChannelPanels.instances) { diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 3fa02369..58294e4e 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -1682,9 +1682,11 @@ export default class ChatHook extends Module { this._ffz_installed = true; const inst = this, - old_send = this.sendMessage; + old_send = this.sendMessage, + addMessage = (...args) => inst.addMessage(...args), + sendMessage = (msg, extra) => inst.sendMessage(msg, extra); - inst.sendMessage = function(msg) { + inst.sendMessage = function(msg, extra) { msg = msg.replace(/\s+/g, ' '); if ( msg.startsWith('/ffz') ) { @@ -1698,7 +1700,10 @@ export default class ChatHook extends Module { const event = new FFZEvent({ message: msg, - channel: inst.props.channelLogin + extra, + channel: inst.props.channelLogin, + addMessage, + sendMessage }); t.emit('chat:pre-send-message', event); @@ -1706,7 +1711,7 @@ export default class ChatHook extends Module { if ( event.defaultPrevented ) return; - return old_send.call(this, event.message); + return old_send.call(this, event.message, event.extra); } } diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx index 77edb9a2..bb477461 100644 --- a/src/sites/twitch-twilight/modules/chat/input.jsx +++ b/src/sites/twitch-twilight/modules/chat/input.jsx @@ -6,9 +6,10 @@ import Module from 'utilities/module'; import { findReactFragment } from 'utilities/dom'; -import { TWITCH_POINTS_SETS, TWITCH_GLOBAL_SETS, TWITCH_PRIME_SETS, KNOWN_CODES, REPLACEMENTS, REPLACEMENT_BASE, TWITCH_EMOTE_BASE } from 'utilities/constants'; +import { TWITCH_POINTS_SETS, TWITCH_GLOBAL_SETS, TWITCH_PRIME_SETS, KNOWN_CODES, REPLACEMENTS, REPLACEMENT_BASE, TWITCH_EMOTE_BASE, KEYS } from 'utilities/constants'; import Twilight from 'site'; +import { FFZEvent } from 'src/utilities/events'; export default class Input extends Module { constructor(...args) { @@ -105,6 +106,12 @@ export default class Input extends Module { Twilight.CHAT_ROUTES ); + this.CommandSuggestions = this.fine.define( + 'tab-cmd-suggestions', + n => n && n.getMatches && n.doesCommandMatchTerm, + Twilight.CHAT_ROUTES + ); + // Implement Twitch's unfinished emote usage object for prioritizing sorting this.EmoteUsageCount = { TriHard: 196568036, @@ -220,10 +227,16 @@ export default class Input extends Module { this.overrideMentionMatcher(inst); }); + this.CommandSuggestions.ready((cls, instances) => { + for(const inst of instances) + this.overrideCommandMatcher(inst); + }); + this.ChatInput.on('update', this.updateEmoteCompletion, this); this.ChatInput.on('mount', this.overrideChatInput, this); this.EmoteSuggestions.on('mount', this.overrideEmoteMatcher, this); this.MentionSuggestions.on('mount', this.overrideMentionMatcher, this); + this.CommandSuggestions.on('mount', this.overrideCommandMatcher, this); this.on('chat.emotes:change-hidden', this.uncacheTabCompletion, this); this.on('chat.emotes:change-set-hidden', this.uncacheTabCompletion, this); @@ -311,6 +324,8 @@ export default class Input extends Module { inst.onKeyDown = function(event) { try { + const code = event.charCode || event.keyCode; + if ( inst.onEmotePickerToggle && t.chat.context.get('chat.emote-menu.shortcut') && event.key === 'e' && event.ctrlKey && ! event.altKey && ! event.shiftKey ) { inst.onEmotePickerToggle(); event.preventDefault(); @@ -318,8 +333,6 @@ export default class Input extends Module { } if ( inst.autocompleteInputRef && t.chat.context.get('chat.mru.enabled') && ! event.shiftKey && ! event.ctrlKey && ! event.altKey ) { - const code = event.charCode || event.keyCode; - // Arrow Up if ( code === 38 && inst.chatInputRef.selectionStart === 0 ) { if ( ! inst.messageHistory.length ) @@ -353,6 +366,14 @@ export default class Input extends Module { } } + // Let users close stuff with Escape. + if ( code === KEYS.Escape && ! event.shiftKey && ! event.ctrlKey && ! event.altKey ) { + if ( inst.props.isShowingEmotePicker ) + inst.props.closeEmotePicker(); + else if ( inst.props.tray && (! inst.state.value || ! inst.state.value.length) ) + inst.closeTray(); + } + } catch(err) { t.log.capture(err); t.log.error(err); @@ -383,10 +404,64 @@ export default class Input extends Module { overrideMentionMatcher(inst) { + inst.canBeTriggeredByTab = !this.chat.context.get('chat.tab-complete.emotes-without-colon'); + } + + + overrideCommandMatcher(inst) { if ( inst._ffz_override ) return; - inst.canBeTriggeredByTab = !this.chat.context.get('chat.tab-complete.emotes-without-colon'); + inst._ffz_override = true; + inst.oldCommands = inst.getCommands; + + const t = this; + + inst.getCommands = function(input) { try { + const commands = inst.props.getCommands(inst.props.permissionLevel, { + isEditor: inst.props.isCurrentUserEditor + }); + + const event = new FFZEvent({ + input, + permissionLevel: inst.props.permissionLevel, + isEditor: inst.props.isCurrentUserEditor, + commands + }); + + t.emit('chat:get-tab-commands', event); + + if ( ! commands || ! commands.length ) + return null; + + // Trim off the starting / + const i = input.slice(1); + + const sorted = commands.filter(cmd => inst.doesCommandMatchTerm(cmd, i)).sort(inst.sortCommands); + const out = []; + for(const cmd of sorted) { + const arg = cmd.commandArgs?.[0]; + let selection; + if ( arg?.isRequired ) + selection = `[${arg.name}]`; + + out.push({ + current: input, + replacement: inst.determineReplacement(cmd), + element: inst.renderCommandSuggestion(cmd, i), + group: cmd.ffz_group ? + (Array.isArray(cmd.ffz_group) ? t.i18n.t(...cmd.ffz_group) : cmd.ffz_group) + : inst.determineGroup(cmd), + selection + }); + } + + return out; + + } catch(err) { + console.error(err); + return inst.oldCommands(input); + }} } diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index c97ae887..883c6ce1 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -31,6 +31,7 @@ export default class ChatLine extends Module { this.inject('site.fine'); this.inject('site.web_munch'); this.inject(RichContent); + this.inject('experiments'); this.inject('chat.actions'); this.inject('chat.overrides'); @@ -83,6 +84,85 @@ export default class ChatLine extends Module { 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); + this.on('chat:get-tab-commands', e => { + if ( this.experiments.getTwitchAssignmentByName('chat_replies') === 'control' ) + return; + + e.commands.push({ + name: 'reply', + description: 'Reply to a user\'s last message.', + permissionLevel: 0, + ffz_group: 'FrankerFaceZ', + commandArgs: [ + {name: 'username', isRequired: true}, + {name: 'message', isRequired: false} + ] + }) + }); + + this.on('chat:pre-send-message', e => { + if ( this.experiments.getTwitchAssignmentByName('chat_replies') === 'control' ) + return; + + const msg = e.message, + types = this.parent.chat_types || {}; + + let user, message; + if ( /^\/reply ?/i.test(msg) ) + user = msg.slice(7).trim(); + else + return; + + e.preventDefault(); + + const idx = user.indexOf(' '); + if ( idx !== -1 ) { + message = user.slice(idx + 1); + user = user.slice(0, idx); + } + + if ( user.startsWith('@') ) + user = user.slice(1); + + if ( user && user.length ) { + user = user.toLowerCase(); + + const lines = Array.from(this.ChatLine.instances); + let i = lines.length; + while(i--) { + const line = lines[i], + msg = line?.props?.message, + u = msg?.user; + + if ( ! u ) + continue; + + if ( u.login === user || u.displayName?.toLowerCase?.() === user ) { + if ( message ) { + e.sendMessage(message, { + reply: { + parentDeleted: msg.deleted || false, + parentDisplayName: u.displayName, + parentMessageBody: msg.message, + parentMsgId: msg.id, + parentUid: u.id, + parentUserLogin: u.login + } + }); + } else + requestAnimationFrame(() => line.ffzOpenReply()); + + return; + } + } + } + + e.addMessage({ + type: types.Notice, + message: this.i18n.t('chat.reply.bad-user', 'Invalid user or no known message to reply to.') + }); + }); + const t = this, React = await this.web_munch.findModule('react'); if ( ! React ) @@ -160,6 +240,78 @@ export default class ChatLine extends Module { props.showTimestamps !== this.props.showTimestamps; } + cls.prototype.ffzOpenReply = function() { + const old_render_author = this.renderMessageAuthor; + this.renderMessageAuthor = () => this.ffzReplyAuthor(); + + const tokens = this.props.message?.ffz_tokens; + if ( ! tokens ) + return; + + this.setMessageTray(this.props.message, t.chat.renderTokens(tokens, e)); + + this.renderMessageAuthor = old_render_author; + } + + cls.prototype.ffzReplyAuthor = function() { + const msg = t.chat.standardizeMessage(this.props.message), + user = msg.user, + raw_color = t.overrides.getColor(user.id) || user.color, + color = t.parent.colors.process(raw_color); + + let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined, + room_id = msg.roomId ? msg.roomId : this.props.channelID; + + if ( ! room && room_id ) { + const r = t.chat.getRoom(room_id, null, true); + if ( r && r.login ) + room = msg.roomLogin = r.login; + } + + if ( ! room_id && room ) { + const r = t.chat.getRoom(null, room_id, true); + if ( r && r.id ) + room_id = msg.roomId = r.id; + } + + const user_block = [ + e('span', { + className: 'chat-author__display-name' + }, user.displayName), + user.isIntl && e('span', { + className: 'chat-author__intl-login' + }, ` (${user.login})`) + ]; + + const override_name = t.overrides.getName(user.id); + + return e('span', { + 'data-room-id': room_id, + 'data-room': room, + 'data-user-id': user.userID, + 'data-user': user.userLogin && user.userLogin.toLowerCase() + }, [ + //t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e), + e('span', { + className: 'chat-line__message--badges' + }, t.chat.badges.render(msg, e)), + e('span', { + className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative tw-tooltip-wrapper' : ''}`, + role: 'button', + style: { color }, + onClick: this.ffz_user_click_handler, + onContextMenu: t.actions.handleUserContext + }, override_name ? [ + e('span', { + className: 'chat-author__display-name' + }, override_name), + e('div', { + className: 'tw-tooltip tw-tooltip--down tw-tooltip--align-center' + }, user_block) + ] : user_block) + ]); + } + cls.prototype.render = function() { try { this._ffz_no_scan = true; @@ -168,6 +320,7 @@ export default class ChatLine extends Module { override_mode = t.chat.context.get('chat.filtering.display-deleted'), msg = t.chat.standardizeMessage(this.props.message), + reply_tokens = msg.ffz_reply = msg.ffz_reply || t.chat.tokenizeReply(this.props.reply), is_action = msg.messageType === types.Action, user = msg.user, @@ -258,12 +411,15 @@ other {# messages were deleted by a moderator.} //if ( ! msg.message && msg.messageParts ) // t.chat.detokenizeMessage(msg); + const has_replies = this.chatRepliesTreatment ? this.chatRepliesTreatment !== 'control' : false; + const u = t.site.getUser(), r = {id: room_id, login: room}; if ( u ) { u.moderator = this.props.isCurrentUserModerator; u.staff = this.props.isCurrentUserStaff; + u.can_reply = has_replies && u.login !== msg.user?.login && ! msg.deleted && ! this.props.disableReplyClick } const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u, r), @@ -317,7 +473,6 @@ other {# messages were deleted by a moderator.} const override_name = t.overrides.getName(user.id); - let cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}`, out = (tokens.length || ! msg.ffz_type) ? [ this.props.showTimestamps && e('span', { @@ -342,11 +497,14 @@ other {# messages were deleted by a moderator.} }, user_block) ] : user_block), e('span', null, is_action ? ' ' : ': '), + show && has_replies && reply_tokens ? + t.chat.renderTokens(reply_tokens, e) + : null, show ? e('span', { className:'message', style: is_action ? { color } : null - }, t.chat.renderTokens(tokens, e)) + }, t.chat.renderTokens(tokens, e, has_replies ? this.props.reply : null)) : e('span', { className: 'chat-line__message--deleted', diff --git a/src/sites/twitch-twilight/modules/theme/index.js b/src/sites/twitch-twilight/modules/theme/index.js index d41b02d7..52f97e4d 100644 --- a/src/sites/twitch-twilight/modules/theme/index.js +++ b/src/sites/twitch-twilight/modules/theme/index.js @@ -21,7 +21,7 @@ const COLORS = [ const ACCENT_COLORS = { - dark: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-hover':8,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'/*,'text-tooltip':1*/},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}}, + dark: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-hover':8,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'/*,'text-tooltip':1*/},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}}, light: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':15,'background-graph-fill':9,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-hover':8,'background-progress-countdown-status':8,'background-progress-status':8,'background-range-fill':9,'background-subscriber-stream-tag-active':13,'background-subscriber-stream-tag-default':13,'background-subscriber-stream-tag-hover':14,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':7,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':9,'border-subscriber-stream-tag':10,'border-tab-active':8,'border-tab-focus':8,'border-tab-hover':8,'border-toggle-focus':8,'border-toggle-hover':8,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':8,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':8,'text-link-active':9,'text-link-focus':9,'text-link-hover':9,'text-link-visited':9,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':8},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[10,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 1px',''],'tab-focus':[8,'0 4px 6px -4px','']}}, accent_dark: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}}, accent_light: {'c':{'accent-hover':10,'accent':9,'accent-primary-1':1,'accent-primary-2':5,'accent-primary-3':6,'accent-primary-4':7,'accent-primary-5':8},'s':{}} diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index 277dd867..3e9094dd 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -55,6 +55,27 @@ pointer-events: none; } +.ffz--reply-mention { + padding: 0.25rem 0.5rem; + border-radius: 1rem; + + &:before { + font-size: 0.8em; + } + + background-color: rgba(0,0,0,0.15); + + .tw-root--theme-dark & { + background-color: rgba(255,255,255,0.15); + } + + &:hover, &:focus { + text-decoration: none; + background-color: var(--color-background-button-hover); + color: var(--color-text-button-hover) !important; + } +} + .ffz--chat-card { .vod-message & { .ffz--card-text { diff --git a/src/socket.js b/src/socket.js index ca80882a..41ccc776 100644 --- a/src/socket.js +++ b/src/socket.js @@ -72,7 +72,6 @@ export default class SocketClient extends Module { this._host_idx = -1; this._host_pool = -1; - this.settings.on(':changed:socket.use-cluster', () => { this._host = null; if ( this.disconnected ) diff --git a/src/std-components/aspect.vue b/src/std-components/aspect.vue index 03fa9227..9a3e0c17 100644 --- a/src/std-components/aspect.vue +++ b/src/std-components/aspect.vue @@ -1,11 +1,11 @@