diff --git a/package.json b/package.json index 98bc0061..6b5dc097 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.7.2", + "version": "4.8.0", "description": "FrankerFaceZ is a Twitch enhancement suite.", "license": "Apache-2.0", "scripts": { diff --git a/src/modules/chat/actions/index.jsx b/src/modules/chat/actions/index.jsx index 72b28d24..cd920a92 100644 --- a/src/modules/chat/actions/index.jsx +++ b/src/modules/chat/actions/index.jsx @@ -91,6 +91,34 @@ export default class Actions extends Module { } }); + this.settings.add('chat.actions.user-context', { + // Filter out actions + process: (ctx, val) => + val.filter(x => x.type || (x.appearance && + this.renderers[x.appearance.type] && + (! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) && + (! x.action || this.actions[x.action]) + )), + + default: [], + type: 'array_merge', + ui: { + path: 'Chat > Actions > User Context @{"description": "Here, you can define custom actions that will appear in a context menu when you right-click a username in chat."}', + component: 'chat-actions', + context: ['user', 'room', 'message'], + mod_icons: true, + + data: () => { + const chat = this.resolve('site.chat'); + return { + color: val => chat && chat.colors ? chat.colors.process(val) : val, + actions: deep_copy(this.actions), + renderers: deep_copy(this.renderers) + } + } + } + }) + this.settings.add('chat.actions.room', { // Filter out actions process: (ctx, val) => @@ -168,6 +196,7 @@ export default class Actions extends Module { this.handleClick = this.handleClick.bind(this); this.handleContext = this.handleContext.bind(this); + this.handleUserContext = this.handleUserContext.bind(this); } @@ -297,6 +326,23 @@ export default class Actions extends Module { renderInlineContext(target, data) { + const definition = data.definition; + let content; + + if ( definition.context ) + content = (t, tip) => definition.context.call(this, data, t, tip); + + else if ( definition.uses_reason ) { + content = (t, tip) => this.renderInlineReasons(data, t, tip); + + } else + return; + + return this.renderPopup(target, content); + } + + + renderPopup(target, content) { if ( target._ffz_destroy ) return target._ffz_destroy(); @@ -313,18 +359,6 @@ export default class Actions extends Module { target._ffz_destroy = target._ffz_outside = null; } - const definition = data.definition; - let content; - - if ( definition.context ) - content = (t, tip) => definition.context.call(this, data, t, tip); - - else if ( definition.uses_reason ) { - content = (t, tip) => this.renderInlineReasons(data, t, tip); - - } else - return; - const parent = document.body.querySelector('#root>div') || document.body, tt = target._ffz_popup = new Tooltip(parent, target, { logger: this.log, @@ -435,6 +469,131 @@ export default class Actions extends Module { } + renderUserContext(target, actions) { + const fine = this.resolve('site.fine'), + site = this.resolve('site'), + chat = this.resolve('site.chat'), + line = fine && fine.searchParent(target, n => n.props && n.props.message); + + const msg = line?.props?.message; + if ( ! msg || ! site || ! chat ) + return; + + let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined; + if ( ! room && line.props.channelID ) { + const r = this.parent.getRoom(line.props.channelID, null, true); + if ( r && r.login ) + room = msg.roomLogin = r.login; + } + + const u = site.getUser(), + r = {id: line.props.channelID, login: room}; + + msg.roomId = r.id; + + if ( u ) { + u.moderator = line.props.isCurrentUserModerator; + u.staff = line.props.isCurrentUserStaff; + } + + const current_level = this.getUserLevel(r, u), + msg_level = this.getUserLevel(r, msg.user); + + let mod_icons = line.props.showModerationIcons; + if ( current_level < 3 ) + mod_icons = false; + + return this.renderPopup(target, (t, tip) => { + const lines = []; + let line = null; + + const handle_click = event => { + tip.hide(); + this.handleClick(event); + }; + + for(const data of actions) { + if ( ! data ) + continue; + + if ( data.type === 'new-line' ) { + line = null; + continue; + + } else if ( data.type === 'space-small' ) { + if ( ! line ) + lines.push(line = []); + + line.push(
); + continue; + + } else if ( data.type === 'space' ) { + if ( ! line ) + lines.push(line = []); + + line.push(
); + continue; + + } else if ( ! data.action || ! data.appearance ) + continue; + + const ap = data.appearance || {}, + disp = data.display || {}, + + def = this.renderers[ap.type]; + + if ( ! def || disp.disabled || + (disp.mod_icons != null && disp.mod_icons !== !!mod_icons) || + (disp.mod != null && disp.mod !== (current_level > msg_level)) || + (disp.staff != null && disp.staff !== (u ? !!u.staff : false)) ) + continue; + + const has_color = def.colored && ap.color, + color = has_color && (chat && chat.colors ? chat.colors.process(ap.color) : ap.color), + contents = def.render.call(this, ap, createElement, color); + + if ( ! line ) + lines.push(line = []); + + const btn = (); + + if ( ap.tooltip ) + btn.dataset.tip = ap.tooltip; + + line.push(btn); + } + + const out = (
+
+ { msg.user.displayName || msg.user.login }... +
+ {lines.map(line => { + if ( ! line || ! line.length ) + return null; + + return (
+ {line} +
); + })} +
); + + out.ffz_message = msg; + return out; + }); + } + + renderInline(msg, mod_icons, current_user, current_room, createElement) { const actions = []; @@ -542,7 +701,29 @@ export default class Actions extends Module { let user, room, message, loaded = false; if ( pds ) { - if ( pds.source === 'line' ) { + if ( pds.source === 'msg' && parent.ffz_message ) { + const msg = parent.ffz_message; + + loaded = true; + user = msg.user ? { + color: msg.user.color, + id: msg.user.id, + login: msg.user.login, + displayName: msg.user.displayName, + type: msg.user.type + } : null; + + room = { + login: msg.roomLogin, + id: msg.roomId + }; + + message = { + id: msg.id, + text: msg.message + }; + + } else if ( pds.source === 'line' ) { const fine = this.resolve('site.fine'), line = fine && fine.searchParent(parent, n => n.props && n.props.message); @@ -632,9 +813,25 @@ export default class Actions extends Module { if ( ! data.definition.context && ! data.definition.uses_reason ) return; - this.renderInlineContext(event.target, data); + this.renderInlineContext(target, data); } + handleUserContext(event) { + if ( event.shiftKey ) + return; + + const actions = this.parent.context.get('chat.actions.user-context'); + if ( ! Array.isArray(actions) || ! actions.length ) + return; + + event.preventDefault(); + + const target = event.target; + if ( target._ffz_tooltip$0 ) + target._ffz_tooltip$0.hide(); + + this.renderUserContext(target, actions); + } pasteMessage(room, message) { return this.resolve('site.chat.input').pasteMessage(room, message); diff --git a/src/modules/main_menu/components/action-editor.vue b/src/modules/main_menu/components/action-editor.vue index 187a27b3..aec90b20 100644 --- a/src/modules/main_menu/components/action-editor.vue +++ b/src/modules/main_menu/components/action-editor.vue @@ -96,7 +96,7 @@
-
+
@@ -302,7 +302,7 @@ import {has, maybe_call, deep_copy} from 'utilities/object'; let id = 0; export default { - props: ['action', 'data', 'inline', 'context', 'modifiers'], + props: ['action', 'data', 'inline', 'mod_icons', 'context', 'modifiers'], data() { return { diff --git a/src/modules/main_menu/components/chat-actions.vue b/src/modules/main_menu/components/chat-actions.vue index 0392ef09..f57b7a92 100644 --- a/src/modules/main_menu/components/chat-actions.vue +++ b/src/modules/main_menu/components/chat-actions.vue @@ -42,7 +42,7 @@
-
+
- {{ t(preset.title_i18n, preset.title, preset) }} + {{ preset.title_i18n ? t(preset.title_i18n, preset.title, preset) : preset.title }}
@@ -192,6 +192,7 @@ :action="act" :data="data" :inline="item.inline" + :mod_icons="has_icons" :context="item.context" :modifiers="item.modifiers" @remove="remove(act)" @@ -260,6 +261,10 @@ export default { } : null }, + has_icons() { + return this.item.mod_icons || this.item.inline + }, + has_default() { return this.default_value && this.default_value.length }, @@ -450,7 +455,7 @@ export default { this.show_all = this.$refs.show_all.checked; this.is_moderator = this.$refs.as_mod.checked; this.is_staff = false; //this.$refs.as_staff.checked; - this.with_mod_icons = this.item.inline && this.$refs.with_mod_icons.checked; + this.with_mod_icons = this.has_icons && this.$refs.with_mod_icons.checked; this.is_deleted = this.has_msg && this.$refs.is_deleted.checked; }, diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 5c45ca84..dc8384e7 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -256,7 +256,6 @@ export default class ChatLine extends Module { setTimeout(() => this.WhisperLine.forceUpdate()); }); - this.ChatLine.ready(cls => { const old_render = cls.prototype.render; @@ -419,7 +418,8 @@ other {# messages were deleted by a moderator.} className: 'chat-line__username notranslate', role: 'button', style: { color }, - onClick: this.ffz_user_click_handler + onClick: this.ffz_user_click_handler, + onContextMenu: t.actions.handleUserContext }, [ e('span', { className: 'chat-author__display-name' diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-custom.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-custom.scss index 73d575f6..07b43cab 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-custom.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-custom.scss @@ -2,6 +2,9 @@ .chat-line__message:not(.chat-line--inline), .user-notice-line { &.ffz-mentioned:not(.ffz-custom-color) { - background-color: var(--ffz-chat-mention-color) !important; + &, + &:nth-child(2n+0) { + background-color: var(--ffz-chat-mention-color) !important; + } } } \ No newline at end of file