1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-01 16:48:32 +00:00
* 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:
SirStendec 2019-08-06 15:39:45 -04:00
parent 9ab57897ef
commit 3583dd8fe7
6 changed files with 228 additions and 23 deletions

View file

@ -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": {

View file

@ -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);

View file

@ -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 {

View file

@ -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;
}, },

View file

@ -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'

View file

@ -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;
}
} }
} }