1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 08:28:31 +00:00
FrankerFaceZ/src/sites/twitch-twilight/modules/video_chat/index.jsx
SirStendec 1cdff0ec67 4.20.84
* Fixed: Case-sensitive blocked terms not functioning correctly.
* Fixed: Settings in the FFZ Control Center not reverting to a default appearance when reset.
* Fixed: Current Channel and Channel Color not being properly detected in the mod view, channel pages, and dashboard.
* Fixed: The channel points reward queue not functioning correctly.
* Changed: Allow highlighting and blocking by add-on badge, not just Twitch badge.
* Changed: Don't allocate `user.badges` and `user.emote_sets` until they're actually used to save on memory.
* Changed: Don't default the `Chat > Bits and Cheering >> Display animated cheers.` setting to the `Animated Emotes` setting.
* API Added: `badges.setBulk`, `badges.deleteBulk`, and `badges.extendBulk` for setting badges on users in bulk using an optimized data structure.
* API Added: Tokenizers can set `msg.ffz_halt_tokens = true` to prevent further tokenizers running. Useful when just discarding a message.
2021-03-22 18:19:09 -04:00

530 lines
No EOL
17 KiB
JavaScript

'use strict';
// ============================================================================
// Video Chat Hooks
// ============================================================================
import {get, has} from 'utilities/object';
import {print_duration} from 'utilities/time';
//import {ClickOutside} from 'utilities/dom';
import {formatBitsConfig} from '../chat';
import Module from 'utilities/module';
export default class VideoChatHook extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.inject('i18n');
this.inject('settings');
this.inject('site');
this.inject('site.router');
this.inject('site.fine');
this.inject('site.web_munch');
this.inject('chat');
this.injectAs('site_chat', 'site.chat');
this.inject('site.chat.chat_line.rich_content');
this.VideoChatController = this.fine.define(
'video-chat-controller',
n => n.onMessageScrollAreaMount && n.createReply,
['user-video', 'user-clip', 'video']
);
this.VideoChatMenu = this.fine.define(
'video-chat-menu',
n => n.onToggleMenu && n.getContent && n.props && has(n.props, 'isExpandedLayout'),
['user-video', 'user-clip', 'video']
);
this.VideoChatLine = this.fine.define(
'video-chat-line',
n => n.onReplyClickHandler && n.shouldFocusMessage,
['user-video', 'user-clip', 'video']
);
// Settings
this.settings.add('chat.video-chat.timestamps', {
default: true,
ui: {
path: 'Chat > Chat on Videos >> Appearance',
title: 'Display timestamps alongside chat messages.',
component: 'setting-check-box'
}
});
this.settings.add('chat.video-chat.enabled', {
default: true,
ui: {
path: 'Chat > Chat on Videos @{"description": "This feature is currently in beta. As such, you may experience issues when using FFZ features with Chat on Videos."} >> General',
title: 'Enable FrankerFaceZ features for Chat on Videos.',
description: 'Display FFZ badges, emotes, and other features in Chat on Videos. Moderation features may be unavailable when this is enabled.',
component: 'setting-check-box'
}
});
}
async onEnable() {
this.chat.context.on('changed:chat.video-chat.enabled', this.updateLines, this);
this.chat.context.on('changed:chat.video-chat.timestamps', this.updateLines, this);
this.on('chat.overrides:changed', id => this.updateLinesByUser(id), this);
this.on('chat:update-lines', this.updateLines, this);
this.on('chat:update-lines-by-user', this.updateLinesByUser, this);
this.on('i18n:update', this.updateLines, this);
this.VideoChatController.on('mount', this.chatMounted, this);
this.VideoChatController.on('unmount', this.chatUnmounted, this);
this.VideoChatController.on('receive-props', this.chatUpdated, this);
this.VideoChatController.ready((cls, instances) => {
for(const inst of instances) {
this.chatMounted(inst);
}
});
const t = this,
React = await this.web_munch.findModule('react');
if ( ! React )
return;
/*this.MessageMenu = class FFZMessageMenu extends React.Component {
constructor(props) {
super(props);
this.onClick = () => this.setState({open: ! this.state.open});
this.onClickOutside = () => this.state.open && this.setState({open: false});
this.element = null;
this.saveRef = element => this.element = element;
this.state = {
open: false
}
}
componentDidMount() {
if ( this.element )
this._clicker = new ClickOutside(this.element, this.onClickOutside);
}
componentWillUnmount() {
this._clicker.destroy();
this._clicker = null;
}
render() {
const is_open = this.state.open;
return (<div ref={this.saveRef} data-test-selector="menu-options-wrapper" class={`tw-flex-shrink-0 video-chat__message-menu${is_open ? ' video-chat__message-menu--force-visible' : ''}`}>
<div class="tw-relative">
<button class="tw-interactive tw-button-icon tw-button-icon--secondary tw-button-icon--small" data-test-selector="menu-button" onClick={this.onClick}>
<span class="tw-button-icon__icon">
<figure class="ffz-i-ellipsis-vert" />
</span>
</button>
<div class={`tw-absolute ffz-balloon ffz-balloon--down ffz-balloon--right ffz-balloon--sm ${is_open ? 'tw-block' : 'tw-hide'}`}>
<div class="tw-absolute ffz-balloon__tail tw-overflow-hidden">
<div class="tw-absolute ffz-balloon__tail-symbol tw-border-b tw-border-l tw-border-r tw-border-t tw-c-background-base" />
</div>
<div class="tw-border-b tw-border-l tw-border-r tw-border-radius-medium tw-border-t tw-c-background-base tw-elevation-1 tw-pd-y-1">
<button class="ffz-interactable ffz-interactable--inverted tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.copy-link', 'Copy Link')
}</button>
<button class="ffz-interactable ffz-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.delete', 'Delete')
}</button>
<div class="tw-mg-1 tw-border-b" />
<button class="ffz-interactable ffz-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.ban', 'Ban User')
}</button>
</div>
</div>
</div>
</div>)
}
}*/
const createElement = React.createElement,
FFZRichContent = this.rich_content && this.rich_content.RichContent;
this.MenuContainer = class FFZMenuContainer extends React.Component {
constructor(props) {
super(props);
this.onBanUser = () => {
this.props.onBanUserClick({
bannedUser: this.props.context.comment.commenter,
targetChannel: this.props.context.comment.channelId,
comment: this.props.context.comment
});
}
this.onDeleteComment = () => {
this.props.onDeleteCommentClick(this.props.context.comment);
}
this.onOpen = () => {
this.props.onDisableSync();
this.setState({
force: true
});
}
this.onClose = () => {
this.setState({
force: false
})
};
this.state = {
force: false
}
}
render() {
if ( ! t.VideoChatMenu._class )
return null;
return (<div class={`tw-flex-shrink-0 video-chat__message-menu${this.state.force ? ' video-chat__message-menu--force-visible' : ''}`}>
<t.VideoChatMenu._class
context={this.props.context}
isCurrentUserModerator={this.props.isCurrentUserModerator}
isExpandedLayout={this.props.isExpandedLayout}
onBanUserClick={this.onBanUser}
onClose={this.onClose}
onDeleteCommentClick={this.onDeleteComment}
onOpen={this.onOpen}
onReplyClick={this.props.onReplyClick}
/>
</div>);
}
}
this.VideoChatLine.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.ffzRenderMessage = function(msg, reply) {
const is_action = msg.is_action,
user = msg.user,
color = t.site_chat.colors.process(user.color),
u = t.site.getUser();
if ( u ) {
u.moderator = this.props.isCurrentUserModerator;
u.staff = u.roles && u.roles.isStaff;
}
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u),
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
return (<div class="tw-align-items-start tw-flex tw-flex-nowrap tw-c-text-base">
<div class="tw-flex-grow-1" data-room-id={msg.roomID} data-room={msg.roomLogin} data-user-id={user.id} data-user={user.login}>
<span class="chat-line__message--badges">{
t.chat.badges.render(msg, createElement)
}</span>
<a
class="video-chat__message-author notranslate"
data-test-selector="comment-author-selector"
href={`/${user.login}`}
rel="noopener noreferrer"
target="_blank"
style={{color}}
>
<span class="chat-author__display-name" data-a-target="chat-message-username" data-a-user={user.login} data-test-selector="message-username">{ user.displayName }</span>
{user.isIntl && <span class="chat-author__intl-login" data-test-selector="message-username-canonical"> ({ user.login})</span>}
</a>
<div data-test-selector="comment-message-selector" class="tw-inline video-chat__message">
<span>{is_action ? ' ' : ': '}</span>
<span class="message" style={{color: is_action ? color : null}}>{ t.chat.renderTokens(tokens, createElement) }</span>
{rich_content && createElement(FFZRichContent, rich_content)}
</div>
</div>
{ reply ? (<t.MenuContainer
context={reply}
isCurrentUserModerator={this.props.isCurrentUserModerator}
isExpandedLayout={this.props.isExpandedLayout}
onBanUserClick={this.props.onBanUserClick}
onDeleteCommentClick={this.props.onDeleteCommentClick}
onDisableSync={this.props.onDisableSync}
onReplyClick={this.onReplyClickHandler}
/>) : null}
</div>);
}
cls.prototype.ffzRenderExpanded = function(msg) {
if ( ! msg._reply_handler )
msg._reply_handler = () => this.onReplyClickHandler(msg.user.login);
return (<div class="tw-align-items-center tw-flex tw-pd-t-05">
<button class="tw-button tw-button--text" data-test-selector="parent-reply-button" onClick={msg._reply_handler}>
<span class="tw-button__text tw-pd-0">{ t.i18n.t('video-chat.reply', 'Reply') }</span>
</button>
<span class="tw-c-text-alt-2 tw-font-size-7 tw-mg-l-05 tw-relative tw-tooltip__container">
{ t.i18n.t('video-chat.time', '{time,humantime} ago', {
time: msg.timestamp
}) }
<div class="tw-tooltip tw-tooltip--align-center tw-tooltip--up" role="tooltip">
{ t.i18n.formatDateTime(msg.timestamp, 'full') }
</div>
</span>
</div>)
}
cls.prototype.render = function() {
try {
this._ffz_no_scan = true;
if ( this.state.showReplyForm || ! t.chat.context.get('chat.video-chat.enabled') )
return old_render.call(this);
const context = this.props.messageContext,
msg = t.standardizeMessage(context.comment, context.author),
main_message = this.ffzRenderMessage(msg, context),
hide_timestamps = this.props.hideTimestamp || ! t.chat.context.get('chat.video-chat.timestamps'),
bg_css = msg.mentioned && msg.mention_color ? t.site_chat.inverse_colors.process(msg.mention_color) : null;
if ( msg.ffz_removed )
return null;
const highlight = ! bg_css && msg.highlight && t.chat.context.get('chat.points.allow-highlight');
return (<div
data-test-selector="message-layout"
class={`tw-align-items-start tw-flex tw-flex-nowrap tw-full-width tw-pd-l-05 tw-pd-y-05 vod-message${msg.is_sub ? ' ffz-notice-line ffz--subscribe-line' : ''}${msg.highlight ? ' ffz-notice-line ffz--points-line' : ''}${highlight ? ' ffz--points-highlight ffz-custom-color' : ''}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`}
style={{backgroundColor: bg_css}}
>
{hide_timestamps || (<div data-test-selector="message-timestamp" class="tw-align-right tw-flex tw-flex-shrink-0 vod-message__header">
<div class="tw-mg-r-05">
<div class="tw-inline-flex tw-relative tw-tooltip__container">
<button class="tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive" onClick={this.onTimestampClickHandler}>
<div class="tw-pd-x-05">
<p class="tw-font-size-7">{print_duration(context.comment.contentOffset)}</p>
</div>
</button>
<div class="tw-tooltip tw-tooltip--align-left tw-tooltip--up" role="tooltip">
{t.i18n.t('video-chat.jump', 'Jump to Video')}
</div>
</div>
</div>
</div>)}
<div class="tw-full-width">
{ main_message }
{ this.props.isExpandedLayout && this.ffzRenderExpanded(msg) }
{ context.replies.length > 0 && (<div class="qa-vod-chat-reply tw-mg-l-05 tw-mg-y-05 vod-message__reply">
{ context.comment.moreReplies && (<div class="tw-inline-block vod-message__show-more-replies">
<button class="tw-interactive tw-button tw-button--text" onClick={this.onLoadMoreRepliesClickHandler}>
<span class="tw-button__text" data-a-target="tw-button-text">{
t.i18n.t('video-chat.show-more', 'Show more replies...')
}</span>
</button>
</div>)}
<ul>{
context.replies.map(reply => (<li key={reply.comment && reply.comment.id} class="tw-mg-l-05">
{ this.ffzRenderMessage(t.standardizeMessage(reply.comment, reply.author), reply) }
{ this.props.isExpandedLayout && this.ffzRenderExpanded(msg) }
</li>))
}</ul>
</div>)}
</div>
</div>)
} catch(err) {
t.log.error('Problem rendering Chat', err);
return old_render.call(this);
}
}
// Do this after a short delay to hopefully reduce the chance of React
// freaking out on us.
setTimeout(() => this.VideoChatLine.forceUpdate());
})
}
updateLines() {
for(const inst of this.VideoChatLine.instances) {
const context = inst.props.messageContext;
if ( ! context.comment )
continue;
context.comment._ffz_message = null;
if ( Array.isArray(context.replies) )
for(const reply of context.replies)
if ( reply.comment )
reply.comment._ffz_message = null;
}
this.VideoChatLine.forceUpdate();
}
updateLinesByUser(id, login) {
for(const inst of this.VideoChatLine.instances) {
const context = inst.props.messageContext;
if ( ! context.comment )
continue;
const author = context.author;
if ( author && ((id && id == author.id) || (login && login == author.name))) {
context.comment._ffz_message = null;
inst.forceUpdate();
}
}
}
// ========================================================================
// Message Standardization
// ========================================================================
standardizeMessage(comment, author) { // eslint-disable-line class-methods-use-this
if ( comment._ffz_message )
return comment._ffz_message;
const room = this.chat.getRoom(comment.channelId, null, true, true),
params = comment.message.userNoticeParams,
msg_id = params && params['msg-id'];
const out = comment._ffz_message = {
user: {
color: comment.message.userColor,
id: author.id,
login: author.name,
displayName: author.displayName,
isIntl: author.name && author.displayName && author.displayName.trim().toLowerCase() !== author.name,
type: author.type
},
roomLogin: room && room.login,
roomID: room && room.id,
ffz_badges: this.chat.badges.getBadges(author.id, author.login, room?.id, room?.login),
badges: comment.userBadges,
messageParts: comment.message.tokens,
is_action: comment.message.isAction,
more_replies: comment.moreReplies,
timestamp: comment.createdAt,
is_sub: msg_id === 'sub' || msg_id === 'resub',
highlight: msg_id === 'highlighted-message'
};
// TODO: We need to strip the sub message from chat messages
// because Twitch is dumb.
this.chat.detokenizeMessage(out);
return out;
}
// ========================================================================
// Room Handling
// ========================================================================
addRoom(thing, props) {
if ( ! props )
props = thing.props;
const channel = get('data.video.owner', props);
if ( ! channel || ! channel.id )
return null;
const room = thing._ffz_room = this.chat.getRoom(channel.id, channel.login && channel.login.toLowerCase(), false, true);
room.ref(thing);
return room;
}
removeRoom(thing) { // eslint-disable-line class-methods-use-this
if ( ! thing._ffz_room )
return;
thing._ffz_room.unref(thing);
thing._ffz_room = null;
}
// ========================================================================
// Video Chat Controller
// ========================================================================
chatMounted(chat, props) {
if ( ! props )
props = chat.props;
if ( ! this.addRoom(chat, props) )
return;
this.chat.badges.updateTwitchBadges(get('data.badges', props));
this.updateRoomBadges(chat, get('data.video.owner.broadcastBadges', props));
this.updateRoomBitsConfig(chat, props.bitsConfig);
}
chatUpdated(chat, props) {
if ( get('data.video.owner.id', props) !== get('data.video.owner.id', chat.props) ) {
this.removeRoom(chat);
this.chatMounted(chat, props);
return;
}
const new_badges = get('data.badges', props),
old_badges = get('data.badges', chat.props),
new_room_badges = get('data.video.owner.broadcastBadges', props),
old_room_badges = get('data.video.owner.broadcastBadges', chat.props);
if ( new_badges !== old_badges )
this.chat.badges.updateTwitchBadges(new_badges);
if ( new_room_badges !== old_room_badges )
this.updateRoomBadges(chat, new_room_badges);
if ( props.bitsConfig !== chat.props.bitsConfig )
this.updateRoomBitsConfig(chat, props.bitsConfig);
const channel = get('data.video.owner', props);
this.settings.updateContext({
moderator: props.isCurrentUserModerator
});
this.chat.context.updateContext({
moderator: props.isCurrentUserModerator,
channel: channel ? channel.login : null,
channelID: channel ? channel.id : null
});
}
chatUnmounted(chat) {
this.removeRoom(chat);
}
updateRoomBadges(chat, badges) { // eslint-disable-line class-methods-use-this
const room = chat._ffz_room;
if ( ! room )
return;
room.updateBadges(badges);
}
updateRoomBitsConfig(chat, config) { // eslint-disable-line class-methods-use-this
const room = chat._ffz_room;
if ( ! room )
return;
room.updateBitsConfig(formatBitsConfig(config));
}
}