mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-01 16:48:32 +00:00
4.8.0
* Added: Chat > Actions > User Context. Define custom actions that appear in a context menu upon right-clicking a username in chat. * Fixed: Custom color for chat highlights in dark theme with alternating chat background colors enabled.
This commit is contained in:
parent
9ab57897ef
commit
3583dd8fe7
6 changed files with 228 additions and 23 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.7.2",
|
"version": "4.8.0",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -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', {
|
this.settings.add('chat.actions.room', {
|
||||||
// Filter out actions
|
// Filter out actions
|
||||||
process: (ctx, val) =>
|
process: (ctx, val) =>
|
||||||
|
@ -168,6 +196,7 @@ export default class Actions extends Module {
|
||||||
|
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
this.handleContext = this.handleContext.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) {
|
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 )
|
if ( target._ffz_destroy )
|
||||||
return target._ffz_destroy();
|
return target._ffz_destroy();
|
||||||
|
|
||||||
|
@ -313,18 +359,6 @@ export default class Actions extends Module {
|
||||||
target._ffz_destroy = target._ffz_outside = null;
|
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,
|
const parent = document.body.querySelector('#root>div') || document.body,
|
||||||
tt = target._ffz_popup = new Tooltip(parent, target, {
|
tt = target._ffz_popup = new Tooltip(parent, target, {
|
||||||
logger: this.log,
|
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(<div class="tw-pd-x-1" />);
|
||||||
|
continue;
|
||||||
|
|
||||||
|
} else if ( data.type === 'space' ) {
|
||||||
|
if ( ! line )
|
||||||
|
lines.push(line = []);
|
||||||
|
|
||||||
|
line.push(<div class="tw-flex-grow-1" />);
|
||||||
|
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 = (<button
|
||||||
|
class="ffz-tooltip ffz-tooltip--no-mouse tw-button tw-button--text"
|
||||||
|
data-tooltip-type="action"
|
||||||
|
data-action={data.action}
|
||||||
|
data-options={data.options ? JSON.stringify(data.options) : null}
|
||||||
|
onClick={handle_click}
|
||||||
|
onContextMenu={this.handleContext}
|
||||||
|
>
|
||||||
|
<span class="tw-button__text">
|
||||||
|
{contents}
|
||||||
|
</span>
|
||||||
|
</button>);
|
||||||
|
|
||||||
|
if ( ap.tooltip )
|
||||||
|
btn.dataset.tip = ap.tooltip;
|
||||||
|
|
||||||
|
line.push(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = (<div class="ffz-action-data tw-pd-05" data-source="msg">
|
||||||
|
<div class="tw-pd-b-05 tw-border-b">
|
||||||
|
<strong>{ msg.user.displayName || msg.user.login }</strong>...
|
||||||
|
</div>
|
||||||
|
{lines.map(line => {
|
||||||
|
if ( ! line || ! line.length )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (<div class="tw-flex tw-flex-no-wrap">
|
||||||
|
{line}
|
||||||
|
</div>);
|
||||||
|
})}
|
||||||
|
</div>);
|
||||||
|
|
||||||
|
out.ffz_message = msg;
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
renderInline(msg, mod_icons, current_user, current_room, createElement) {
|
renderInline(msg, mod_icons, current_user, current_room, createElement) {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
|
|
||||||
|
@ -542,7 +701,29 @@ export default class Actions extends Module {
|
||||||
let user, room, message, loaded = false;
|
let user, room, message, loaded = false;
|
||||||
|
|
||||||
if ( pds ) {
|
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'),
|
const fine = this.resolve('site.fine'),
|
||||||
line = fine && fine.searchParent(parent, n => n.props && n.props.message);
|
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 )
|
if ( ! data.definition.context && ! data.definition.uses_reason )
|
||||||
return;
|
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) {
|
pasteMessage(room, message) {
|
||||||
return this.resolve('site.chat.input').pasteMessage(room, message);
|
return this.resolve('site.chat.input').pasteMessage(room, message);
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tw-flex tw-align-items-center">
|
<div v-if="mod_icons" class="tw-flex tw-align-items-center">
|
||||||
<label for="vis_mod_icons">
|
<label for="vis_mod_icons">
|
||||||
{{ t('setting.actions.edit-visible.mod-icons', 'Mod Icons') }}
|
{{ t('setting.actions.edit-visible.mod-icons', 'Mod Icons') }}
|
||||||
</label>
|
</label>
|
||||||
|
@ -302,7 +302,7 @@ import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['action', 'data', 'inline', 'context', 'modifiers'],
|
props: ['action', 'data', 'inline', 'mod_icons', 'context', 'modifiers'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="item.inline" class="tw-pd-x-1 tw-checkbox">
|
<div v-if="has_icons" class="tw-pd-x-1 tw-checkbox">
|
||||||
<input
|
<input
|
||||||
id="with_mod_icons"
|
id="with_mod_icons"
|
||||||
ref="with_mod_icons"
|
ref="with_mod_icons"
|
||||||
|
@ -147,7 +147,7 @@
|
||||||
>
|
>
|
||||||
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
|
<div class="tw-flex tw-align-items-center tw-pd-y-05 tw-pd-x-1">
|
||||||
<div class="tw-flex-grow-1 tw-mg-r-1">
|
<div class="tw-flex-grow-1 tw-mg-r-1">
|
||||||
{{ t(preset.title_i18n, preset.title, preset) }}
|
{{ preset.title_i18n ? t(preset.title_i18n, preset.title, preset) : preset.title }}
|
||||||
</div>
|
</div>
|
||||||
<action-preview v-if="preset.appearance" :act="preset" :renderers="data.renderers" />
|
<action-preview v-if="preset.appearance" :act="preset" :renderers="data.renderers" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -192,6 +192,7 @@
|
||||||
:action="act"
|
:action="act"
|
||||||
:data="data"
|
:data="data"
|
||||||
:inline="item.inline"
|
:inline="item.inline"
|
||||||
|
:mod_icons="has_icons"
|
||||||
:context="item.context"
|
:context="item.context"
|
||||||
:modifiers="item.modifiers"
|
:modifiers="item.modifiers"
|
||||||
@remove="remove(act)"
|
@remove="remove(act)"
|
||||||
|
@ -260,6 +261,10 @@ export default {
|
||||||
} : null
|
} : null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
has_icons() {
|
||||||
|
return this.item.mod_icons || this.item.inline
|
||||||
|
},
|
||||||
|
|
||||||
has_default() {
|
has_default() {
|
||||||
return this.default_value && this.default_value.length
|
return this.default_value && this.default_value.length
|
||||||
},
|
},
|
||||||
|
@ -450,7 +455,7 @@ export default {
|
||||||
this.show_all = this.$refs.show_all.checked;
|
this.show_all = this.$refs.show_all.checked;
|
||||||
this.is_moderator = this.$refs.as_mod.checked;
|
this.is_moderator = this.$refs.as_mod.checked;
|
||||||
this.is_staff = false; //this.$refs.as_staff.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;
|
this.is_deleted = this.has_msg && this.$refs.is_deleted.checked;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -256,7 +256,6 @@ export default class ChatLine extends Module {
|
||||||
setTimeout(() => this.WhisperLine.forceUpdate());
|
setTimeout(() => this.WhisperLine.forceUpdate());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.ChatLine.ready(cls => {
|
this.ChatLine.ready(cls => {
|
||||||
const old_render = cls.prototype.render;
|
const old_render = cls.prototype.render;
|
||||||
|
|
||||||
|
@ -419,7 +418,8 @@ other {# messages were deleted by a moderator.}
|
||||||
className: 'chat-line__username notranslate',
|
className: 'chat-line__username notranslate',
|
||||||
role: 'button',
|
role: 'button',
|
||||||
style: { color },
|
style: { color },
|
||||||
onClick: this.ffz_user_click_handler
|
onClick: this.ffz_user_click_handler,
|
||||||
|
onContextMenu: t.actions.handleUserContext
|
||||||
}, [
|
}, [
|
||||||
e('span', {
|
e('span', {
|
||||||
className: 'chat-author__display-name'
|
className: 'chat-author__display-name'
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
.chat-line__message:not(.chat-line--inline),
|
.chat-line__message:not(.chat-line--inline),
|
||||||
.user-notice-line {
|
.user-notice-line {
|
||||||
&.ffz-mentioned:not(.ffz-custom-color) {
|
&.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue