mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-25 12:08:30 +00:00
* Added: More settings under Chat > Behavior >> Deleted Messages to control the display of moderated messages. * Fixed: Deleted messages displaying for non-moderated users using the settings that would be applied were that user a moderator. * Changed: In-line Chat Actions that only display when you are a moderator now only display when you have moderation powers over that user. Moderators will no longer see those actions next to other moderators.
1625 lines
43 KiB
JavaScript
1625 lines
43 KiB
JavaScript
'use strict';
|
|
|
|
// ============================================================================
|
|
// Chat Hooks
|
|
// ============================================================================
|
|
|
|
import {ColorAdjuster} from 'utilities/color';
|
|
import {setChildren} from 'utilities/dom';
|
|
import {has, make_enum, split_chars, shallow_object_equals} from 'utilities/object';
|
|
import {FFZEvent} from 'utilities/events';
|
|
|
|
import Module from 'utilities/module';
|
|
|
|
import Twilight from 'site';
|
|
|
|
import Scroller from './scroller';
|
|
import ChatLine from './line';
|
|
import SettingsMenu from './settings_menu';
|
|
import EmoteMenu from './emote_menu';
|
|
import TabCompletion from './tab_completion';
|
|
|
|
|
|
const REGEX_EMOTES = {
|
|
'B-?\\)': ['B)', 'B-)'],
|
|
'R-?\\)': ['R)', 'R-)'],
|
|
'[oO](_|\\.)[oO]': ['o_o', 'O_o', 'o_O', 'O_O', 'o.o', 'O.o', 'o.O', 'O.O'],
|
|
'\\>\\;\\(': ['>('],
|
|
'\\<\\;3': ['<3'],
|
|
'\\:-?(o|O)': [':o', ':O', ':-o', ':-O'],
|
|
'\\:-?(p|P)': [':p', ':P', ':-p', ':-P'],
|
|
'\\:-?D': [':D', ':-D'],
|
|
'\\:-?[\\\\/]': [':/', ':-/', ':\\', ':-\\'],
|
|
'\\:-?[z|Z|\\|]': [':z', ':Z', ':|', ':-z', ':-Z', ':-|'],
|
|
'\\:-?\\(': [':(', ':-('],
|
|
'\\:-?\\)': [':)', ':-)'],
|
|
'\\;-?(p|P)': [';p', ';P', ';-p', ';-P'],
|
|
'\\;-?\\)': [';)', ';-)'],
|
|
'#-?[\\\\/]': ['#/', '#-/', '#//', '#-//'],
|
|
':-?(?:7|L)': [':7', ':L', ':-7', ':-L'],
|
|
'\\<\\;\\]': ['<]'],
|
|
'\\:-?(S|s)': [':s', ':S', ':-s', ':-S'],
|
|
'\\:\\>\\;': [':>']
|
|
};
|
|
|
|
|
|
const MESSAGE_TYPES = make_enum(
|
|
'Post',
|
|
'Action',
|
|
'PostWithMention'
|
|
);
|
|
|
|
const MOD_TYPES = make_enum(
|
|
'Ban',
|
|
'Timeout',
|
|
'Delete'
|
|
);
|
|
|
|
const AUTOMOD_TYPES = make_enum(
|
|
'MessageRejectedPrompt',
|
|
'CheerMessageRejectedPrompt',
|
|
'MessageRejected',
|
|
'MessageAllowed',
|
|
'MessageDenied',
|
|
'CheerMessageDenied',
|
|
'CheerMessageTimeout',
|
|
'MessageModApproved',
|
|
'MessageModDenied'
|
|
);
|
|
|
|
const CHAT_TYPES = make_enum(
|
|
'Message',
|
|
'ExtensionMessage',
|
|
'Moderation',
|
|
'ModerationAction',
|
|
'TargetedModerationAction',
|
|
'AutoMod',
|
|
'SubscriberOnlyMode',
|
|
'FollowerOnlyMode',
|
|
'SlowMode',
|
|
'EmoteOnlyMode',
|
|
'R9KMode',
|
|
'Connected',
|
|
'Disconnected',
|
|
'Reconnect',
|
|
'Hosting',
|
|
'Unhost',
|
|
'Hosted',
|
|
'Subscription',
|
|
'Resubscription',
|
|
'GiftPaidUpgrade',
|
|
'AnonGiftPaidUpgrade',
|
|
'PrimePaidUpgrade',
|
|
'SubGift',
|
|
'AnonSubGift',
|
|
'Clear',
|
|
'RoomMods',
|
|
'RoomState',
|
|
'Raid',
|
|
'Unraid',
|
|
'Ritual',
|
|
'Notice',
|
|
'Info',
|
|
'BadgesUpdated',
|
|
'Purchase',
|
|
'BitsCharity',
|
|
'CrateGift',
|
|
'RewardGift',
|
|
'SubMysteryGift',
|
|
'AnonSubMysteryGift',
|
|
'FirstCheerMessage',
|
|
'BitsBadgeTierMessage',
|
|
'InlinePrivateCallout'
|
|
);
|
|
|
|
|
|
const NULL_TYPES = [
|
|
'Reconnect',
|
|
'RoomState',
|
|
'BadgesUpdated',
|
|
'Clear'
|
|
];
|
|
|
|
|
|
const MISBEHAVING_EVENTS = [
|
|
'onBadgesUpdatedEvent',
|
|
];
|
|
|
|
|
|
export default class ChatHook extends Module {
|
|
constructor(...args) {
|
|
super(...args);
|
|
|
|
this.should_enable = true;
|
|
|
|
this.colors = new ColorAdjuster;
|
|
this.inverse_colors = new ColorAdjuster;
|
|
|
|
this.inject('settings');
|
|
this.inject('i18n');
|
|
|
|
this.inject('site');
|
|
this.inject('site.router');
|
|
this.inject('site.fine');
|
|
this.inject('site.web_munch');
|
|
this.inject('site.css_tweaks');
|
|
|
|
this.inject('chat');
|
|
|
|
this.inject(Scroller);
|
|
this.inject(ChatLine);
|
|
this.inject(SettingsMenu);
|
|
this.inject(EmoteMenu);
|
|
this.inject(TabCompletion);
|
|
|
|
this.ChatService = this.fine.define(
|
|
'chat-service',
|
|
n => n.join && n.connectHandlers,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.ChatBuffer = this.fine.define(
|
|
'chat-buffer',
|
|
n => n.updateHandlers && n.delayedMessageBuffer && n.handleMessage,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.ChatController = this.fine.define(
|
|
'chat-controller',
|
|
n => n.hostingHandler && n.onRoomStateUpdated,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.ChatContainer = this.fine.define(
|
|
'chat-container',
|
|
n => n.showViewersList && n.onChatInputFocus,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.ChatBufferConnector = this.fine.define(
|
|
'chat-buffer-connector',
|
|
n => n.clearBufferHandle && n.syncBufferedMessages,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.PinnedCheer = this.fine.define(
|
|
'pinned-cheer',
|
|
n => n.collapseCheer && n.saveRenderedMessageRef,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.RoomPicker = this.fine.define(
|
|
'chat-picker',
|
|
n => n.closeRoomPicker && n.handleRoomSelect,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
|
|
// Settings
|
|
|
|
this.settings.add('chat.width', {
|
|
default: 340,
|
|
ui: {
|
|
path: 'Chat > Appearance >> General @{"sort": -1}',
|
|
title: 'Width',
|
|
description: "How wide chat should be, in pixels. This may be affected by your browser's zoom and font size settings.",
|
|
component: 'setting-text-box',
|
|
process(val) {
|
|
val = parseInt(val, 10);
|
|
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
|
return 340;
|
|
|
|
return val;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.bits.show-pinned', {
|
|
default: true,
|
|
ui: {
|
|
path: 'Chat > Bits and Cheering >> Appearance',
|
|
title: 'Display Top Cheerers',
|
|
|
|
component: 'setting-check-box'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.rituals.show', {
|
|
default: true,
|
|
ui: {
|
|
path: 'Chat > Filtering >> Rituals',
|
|
title: 'Display ritual messages such as "User is new here! Say Hello!".',
|
|
component: 'setting-check-box'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.subs.show', {
|
|
default: 3,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Subscriptions',
|
|
title: 'Display Subs in Chat',
|
|
component: 'setting-select-box',
|
|
description: '**Note**: Messages sent with re-subs will always be displayed. This only controls the special "X subscribed!" message.',
|
|
data: [
|
|
{value: 0, title: 'Do Not Display'},
|
|
{value: 1, title: 'Re-Subs with Messages Only'},
|
|
{value: 2, title: 'Re-Subs Only'},
|
|
{value: 3, title: 'Display All'}
|
|
]
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.subs.compact', {
|
|
default: false,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Subscriptions',
|
|
title: 'Display subscription notices in a more compact (classic style) form.',
|
|
component: 'setting-check-box'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.subs.merge-gifts', {
|
|
default: 1000,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Subscriptions',
|
|
title: 'Merge Mass Sub Gifts',
|
|
component: 'setting-select-box',
|
|
data: [
|
|
{value: 1000, title: 'Disabled'},
|
|
{value: 50, title: 'More than 50'},
|
|
{value: 20, title: 'More than 20'},
|
|
{value: 10, title: 'More than 10'},
|
|
{value: 5, title: 'More than 5'},
|
|
{value: 0, title: 'Always'}
|
|
],
|
|
description: 'Merge mass gift subscriptions into a single message, depending on the quantity.\n**Note:** Only affects newly gifted subs.'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.subs.merge-gifts-visibility', {
|
|
default: false,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Subscriptions',
|
|
title: 'Expand merged mass sub gift messages by default.',
|
|
component: 'setting-check-box'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.lines.alternate', {
|
|
default: false,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Chat Lines',
|
|
title: 'Display lines with alternating background colors.',
|
|
component: 'setting-check-box'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.lines.padding', {
|
|
default: false,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Chat Lines',
|
|
title: 'Reduce padding around lines.',
|
|
component: 'setting-check-box'
|
|
}
|
|
});
|
|
|
|
this.settings.add('chat.lines.borders', {
|
|
default: 0,
|
|
ui: {
|
|
path: 'Chat > Appearance >> Chat Lines',
|
|
title: 'Separators',
|
|
component: 'setting-select-box',
|
|
data: [
|
|
{value: 0, title: 'Disabled'},
|
|
{value: 1, title: 'Basic Line (1px Solid)'},
|
|
{value: 2, title: '3D Line (2px Groove)'},
|
|
{value: 3, title: '3D Line (2px Groove Inset)'},
|
|
{value: 4, title: 'Wide Line (2px Solid)'}
|
|
]
|
|
}
|
|
});
|
|
}
|
|
|
|
get currentChat() {
|
|
for(const inst of this.ChatController.instances)
|
|
if ( inst && inst.chatService )
|
|
return inst;
|
|
}
|
|
|
|
|
|
updateColors() {
|
|
const is_dark = this.chat.context.get('theme.is-dark'),
|
|
mode = this.chat.context.get('chat.adjustment-mode'),
|
|
contrast = this.chat.context.get('chat.adjustment-contrast'),
|
|
c = this.colors,
|
|
ic = this.inverse_colors;
|
|
|
|
// TODO: Get the background color from the theme system.
|
|
// Updated: Use the lightest/darkest colors from alternating rows for better readibility.
|
|
c._base = is_dark ? '#191919' : '#e0e0e0'; //#0e0c13' : '#faf9fa';
|
|
c.mode = mode;
|
|
c.contrast = contrast;
|
|
|
|
ic._base = is_dark ? '#dad8de' : '#19171c';
|
|
ic.mode = mode;
|
|
ic.contrast = contrast;
|
|
|
|
this.updateChatLines();
|
|
}
|
|
|
|
|
|
updateChatCSS() {
|
|
const width = this.chat.context.get('chat.width'),
|
|
size = this.chat.context.get('chat.font-size'),
|
|
emote_alignment = this.chat.context.get('chat.lines.emote-alignment'),
|
|
lh = Math.round((20/12) * size);
|
|
|
|
let font = this.chat.context.get('chat.font-family') || 'inherit';
|
|
if ( font.indexOf(' ') !== -1 && font.indexOf(',') === -1 && font.indexOf('"') === -1 && font.indexOf("'") === -1 )
|
|
font = `"${font}"`;
|
|
|
|
this.css_tweaks.setVariable('chat-font-size', `${size/10}rem`);
|
|
this.css_tweaks.setVariable('chat-line-height', `${lh/10}rem`);
|
|
this.css_tweaks.setVariable('chat-font-family', font);
|
|
this.css_tweaks.setVariable('chat-width', `${width/10}rem`);
|
|
|
|
this.css_tweaks.toggle('chat-font', size !== 12 || font);
|
|
this.css_tweaks.toggle('chat-width', width !== 340);
|
|
|
|
this.css_tweaks.toggle('emote-alignment-padded', emote_alignment === 1);
|
|
this.css_tweaks.toggle('emote-alignment-baseline', emote_alignment === 2);
|
|
}
|
|
|
|
updateLineBorders() {
|
|
const mode = this.chat.context.get('chat.lines.borders');
|
|
|
|
this.css_tweaks.toggle('chat-borders', mode > 0);
|
|
this.css_tweaks.toggle('chat-borders-3d', mode === 2);
|
|
this.css_tweaks.toggle('chat-borders-3d-inset', mode === 3);
|
|
this.css_tweaks.toggle('chat-borders-wide', mode === 4);
|
|
}
|
|
|
|
updateMentionCSS() {
|
|
const enabled = this.chat.context.get('chat.filtering.highlight-mentions');
|
|
this.css_tweaks.toggle('chat-mention-token', this.chat.context.get('chat.filtering.highlight-tokens'));
|
|
this.css_tweaks.toggle('chat-mention-bg', enabled);
|
|
this.css_tweaks.toggle('chat-mention-bg-alt', enabled && this.chat.context.get('chat.lines.alternate'));
|
|
}
|
|
|
|
|
|
async grabTypes() {
|
|
const ct = await this.web_munch.findModule('chat-types'),
|
|
changes = [];
|
|
|
|
this.automod_types = ct && ct.a || AUTOMOD_TYPES;
|
|
this.chat_types = ct && ct.b || CHAT_TYPES;
|
|
this.message_types = ct && ct.c || MESSAGE_TYPES;
|
|
this.mod_types = ct && ct.e || MOD_TYPES;
|
|
|
|
if ( ! ct )
|
|
return;
|
|
|
|
if ( ct.a && ! shallow_object_equals(ct.a, AUTOMOD_TYPES) )
|
|
changes.push('AUTOMOD_TYPES');
|
|
|
|
if ( ct.b && ! shallow_object_equals(ct.b, CHAT_TYPES) )
|
|
changes.push('CHAT_TYPES');
|
|
|
|
if ( ct.c && ! shallow_object_equals(ct.c, MESSAGE_TYPES) )
|
|
changes.push('MESSAGE_TYPES');
|
|
|
|
if ( ct.e && ! shallow_object_equals(ct.e, MOD_TYPES) )
|
|
changes.push('MOD_TYPES');
|
|
|
|
if ( changes.length )
|
|
this.log.info('Chat Types have changed from static mappings for categories:', changes.join(' '));
|
|
}
|
|
|
|
|
|
onEnable() {
|
|
this.on('site.web_munch:loaded', this.grabTypes);
|
|
this.grabTypes();
|
|
|
|
this.chat.context.on('changed:chat.width', this.updateChatCSS, this);
|
|
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
|
this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this);
|
|
this.chat.context.on('changed:chat.lines.emote-alignment', this.updateChatCSS, this);
|
|
this.chat.context.on('changed:chat.adjustment-mode', this.updateColors, this);
|
|
this.chat.context.on('changed:chat.adjustment-contrast', this.updateColors, this);
|
|
this.chat.context.on('changed:theme.is-dark', this.updateColors, this);
|
|
this.chat.context.on('changed:chat.lines.borders', this.updateLineBorders, this);
|
|
this.chat.context.on('changed:chat.filtering.highlight-mentions', this.updateMentionCSS, this);
|
|
this.chat.context.on('changed:chat.filtering.highlight-tokens', this.updateMentionCSS, this);
|
|
this.chat.context.on('changed:chat.fix-bad-emotes', this.updateChatLines, this);
|
|
this.chat.context.on('changed:chat.filtering.display-deleted', this.updateChatLines, this);
|
|
this.chat.context.on('changed:chat.filtering.display-mod-action', this.updateChatLines, this);
|
|
|
|
this.chat.context.on('changed:chat.lines.alternate', val => {
|
|
this.css_tweaks.toggle('chat-rows', val);
|
|
this.updateMentionCSS();
|
|
});
|
|
|
|
this.chat.context.on('changed:chat.lines.padding', val =>
|
|
this.css_tweaks.toggle('chat-padding', val));
|
|
|
|
this.chat.context.on('changed:chat.bits.show', val =>
|
|
this.css_tweaks.toggle('hide-bits', !val));
|
|
this.chat.context.on('changed:chat.bits.show-pinned', val =>
|
|
this.css_tweaks.toggleHide('pinned-cheer', !val));
|
|
|
|
this.chat.context.on('changed:chat.filtering.deleted-style', val => {
|
|
this.css_tweaks.toggle('chat-deleted-strike', val === 1 || val === 2);
|
|
this.css_tweaks.toggle('chat-deleted-fade', val < 2);
|
|
});
|
|
|
|
const val = this.chat.context.get('chat.filtering.deleted-style');
|
|
this.css_tweaks.toggle('chat-deleted-strike', val === 1 || val === 2);
|
|
this.css_tweaks.toggle('chat-deleted-fade', val < 2);
|
|
|
|
this.css_tweaks.toggleHide('pinned-cheer', !this.chat.context.get('chat.bits.show-pinned'));
|
|
this.css_tweaks.toggle('hide-bits', !this.chat.context.get('chat.bits.show'));
|
|
this.css_tweaks.toggle('chat-rows', this.chat.context.get('chat.lines.alternate'));
|
|
this.css_tweaks.toggle('chat-padding', this.chat.context.get('chat.lines.padding'));
|
|
|
|
this.updateChatCSS();
|
|
this.updateColors();
|
|
this.updateLineBorders();
|
|
this.updateMentionCSS();
|
|
|
|
this.ChatController.on('mount', this.chatMounted, this);
|
|
this.ChatController.on('unmount', this.chatUmounted, this);
|
|
this.ChatController.on('receive-props', this.chatUpdated, this);
|
|
|
|
this.ChatService.ready((cls, instances) => {
|
|
this.wrapChatService(cls);
|
|
|
|
for(const inst of instances) {
|
|
inst.client.events.removeAll();
|
|
|
|
inst._ffzInstall();
|
|
|
|
inst.connectHandlers();
|
|
|
|
inst.props.setChatConnectionAPI({
|
|
sendMessage: inst.sendMessage,
|
|
_ffz_inst: inst
|
|
});
|
|
}
|
|
});
|
|
|
|
this.ChatBuffer.ready((cls, instances) => {
|
|
this.wrapChatBuffer(cls);
|
|
|
|
for(const inst of instances) {
|
|
const handler = inst.props.messageHandlerAPI;
|
|
if ( handler )
|
|
handler.removeMessageHandler(inst.handleMessage);
|
|
|
|
inst._ffzInstall();
|
|
|
|
if ( handler )
|
|
handler.addMessageHandler(inst.handleMessage);
|
|
|
|
inst.props.setMessageBufferAPI({
|
|
addUpdateHandler: inst.addUpdateHandler,
|
|
removeUpdateHandler: inst.removeUpdateHandler,
|
|
getMessages: inst.getMessages,
|
|
_ffz_inst: inst
|
|
});
|
|
}
|
|
});
|
|
|
|
this.ChatBufferConnector.on('mount', this.connectorMounted, this);
|
|
this.ChatBufferConnector.on('receive-props', this.connectorUpdated, this);
|
|
this.ChatBufferConnector.on('unmount', this.connectorUnmounted, this);
|
|
|
|
this.ChatBufferConnector.ready((cls, instances) => {
|
|
for(const inst of instances)
|
|
this.connectorMounted(inst);
|
|
})
|
|
|
|
this.ChatController.ready((cls, instances) => {
|
|
const t = this,
|
|
old_catch = cls.prototype.componentDidCatch,
|
|
old_render = cls.prototype.render;
|
|
|
|
// Try catching errors. With any luck, maybe we can
|
|
// recover from the error when we re-build?
|
|
cls.prototype.componentDidCatch = function(err, info) {
|
|
// Don't log infinitely if stuff gets super screwed up.
|
|
const errs = this.state.ffz_errors || 0;
|
|
if ( errs < 100 ) {
|
|
this.setState({ffz_errors: errs + 1});
|
|
t.log.info('Error within Chat', err, info, errs);
|
|
}
|
|
|
|
if ( old_catch )
|
|
return old_catch.call(this, err, info);
|
|
}
|
|
|
|
cls.prototype.render = function() {
|
|
if ( this.state.ffz_errors > 0 ) {
|
|
const React = t.web_munch.getModule('react'),
|
|
createElement = React && React.createElement;
|
|
|
|
if ( ! createElement )
|
|
return null;
|
|
|
|
return createElement('div', {
|
|
className: 'tw-border-l tw-c-background-alt-2 tw-c-text-base tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative'
|
|
}, 'There was an error displaying chat.');
|
|
|
|
} else
|
|
return old_render.call(this);
|
|
}
|
|
|
|
for(const inst of instances)
|
|
this.chatMounted(inst);
|
|
});
|
|
|
|
|
|
this.ChatContainer.on('mount', this.containerMounted, this);
|
|
this.ChatContainer.on('unmount', this.removeRoom, this);
|
|
this.ChatContainer.on('receive-props', this.containerUpdated, this);
|
|
|
|
this.ChatContainer.ready((cls, instances) => {
|
|
const t = this,
|
|
old_catch = cls.prototype.componentDidCatch;
|
|
|
|
// Try catching errors. With any luck, maybe we can
|
|
// recover from the error when we re-build?
|
|
cls.prototype.componentDidCatch = function(err, info) {
|
|
// Don't log infinitely if stuff gets super screwed up.
|
|
const errs = this.state.ffz_errors || 0;
|
|
if ( errs < 100 ) {
|
|
this.setState({ffz_errors: errs + 1});
|
|
t.log.info('Error within Chat Container', err, info, errs);
|
|
}
|
|
|
|
if ( old_catch )
|
|
return old_catch.call(this, err, info);
|
|
}
|
|
|
|
for(const inst of instances)
|
|
this.containerMounted(inst);
|
|
});
|
|
|
|
|
|
this.PinnedCheer.on('mount', this.fixPinnedCheer, this);
|
|
this.PinnedCheer.on('update', this.fixPinnedCheer, this);
|
|
|
|
this.PinnedCheer.ready((cls, instances) => {
|
|
for(const inst of instances)
|
|
this.fixPinnedCheer(inst);
|
|
});
|
|
|
|
|
|
this.RoomPicker.ready((cls, instances) => {
|
|
for(const inst of instances)
|
|
this.closeRoomPicker(inst);
|
|
});
|
|
|
|
this.RoomPicker.on('mount', this.closeRoomPicker, this);
|
|
}
|
|
|
|
|
|
closeRoomPicker(inst) { // eslint-disable-line class-methods-use-this
|
|
inst.closeRoomPicker();
|
|
}
|
|
|
|
|
|
wrapChatBuffer(cls) {
|
|
if ( cls.prototype._ffz_was_here )
|
|
return;
|
|
|
|
const t = this,
|
|
old_mount = cls.prototype.componentDidMount;
|
|
|
|
cls.prototype._ffzInstall = function() {
|
|
if ( this._ffz_installed )
|
|
return;
|
|
|
|
this._ffz_installed = true;
|
|
|
|
const inst = this,
|
|
old_handle = inst.handleMessage,
|
|
old_set = inst.props.setMessageBufferAPI;
|
|
|
|
inst.props.setMessageBufferAPI = function(api) {
|
|
if ( api )
|
|
api._ffz_inst = inst;
|
|
|
|
return old_set(api);
|
|
}
|
|
|
|
inst.handleMessage = function(msg) {
|
|
if ( msg ) {
|
|
try {
|
|
const types = t.chat_types || {},
|
|
mod_types = t.mod_types || {};
|
|
|
|
if ( msg.type === types.Message ) {
|
|
const m = t.chat.standardizeMessage(msg),
|
|
cont = inst._ffz_connector,
|
|
room_id = cont && cont.props.channelID;
|
|
|
|
let room = m.roomLogin = m.roomLogin ? m.roomLogin : m.channel ? m.channel.slice(1) : cont && cont.props.channelLogin;
|
|
|
|
if ( ! room && room_id ) {
|
|
const r = t.chat.getRoom(room_id, null, true);
|
|
if ( r && r.login )
|
|
room = m.roomLogin = r.login;
|
|
}
|
|
|
|
const u = t.site.getUser(),
|
|
r = {id: room_id, login: room};
|
|
|
|
if ( u && cont ) {
|
|
u.moderator = cont.props.isCurrentUserModerator;
|
|
u.staff = cont.props.isStaff;
|
|
}
|
|
|
|
m.ffz_tokens = m.ffz_tokens || t.chat.tokenizeMessage(m, u, r);
|
|
|
|
const event = new FFZEvent({
|
|
message: m,
|
|
channel: room,
|
|
channelID: room_id
|
|
});
|
|
|
|
t.emit('chat:receive-message', event);
|
|
if ( event.defaultPrevented || m.ffz_removed )
|
|
return;
|
|
|
|
} else if ( msg.type === types.ModerationAction ) {
|
|
//t.log.info('Moderation Action', msg);
|
|
if ( ! inst.props.isCurrentUserModerator )
|
|
return;
|
|
|
|
const mod_action = msg.moderationActionType;
|
|
if ( mod_action === 'ban' || mod_action === 'timeout' || mod_action === 'delete' ) {
|
|
const user = msg.targetUserLogin;
|
|
if ( inst.moderatedUsers.has(user) )
|
|
return;
|
|
|
|
const do_remove = t.chat.context.get('chat.filtering.remove-deleted') === 3;
|
|
if ( do_remove ) {
|
|
const len = inst.buffer.length,
|
|
target_id = msg.messageID;
|
|
inst.buffer = inst.buffer.filter(m =>
|
|
m.type !== types.Message || ! m.user || m.user.userLogin !== user ||
|
|
(target_id && m.id !== target_id)
|
|
);
|
|
if ( len !== inst.buffer.length && ! inst.props.isBackground )
|
|
inst.notifySubscribers();
|
|
|
|
inst.ffzModerateBuffer([inst.delayedMessageBuffer], msg);
|
|
|
|
} else
|
|
inst.ffzModerateBuffer([inst.buffer, inst.delayedMessageBuffer], msg);
|
|
|
|
inst.moderatedUsers.add(user);
|
|
setTimeout(inst.unsetModeratedUser(user), 1e3);
|
|
|
|
inst.delayedMessageBuffer.push({
|
|
event: msg,
|
|
time: Date.now(),
|
|
shouldDelay: false
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
} else if ( msg.type === types.Moderation ) {
|
|
//t.log.info('Moderation', msg);
|
|
if ( inst.props.isCurrentUserModerator )
|
|
return;
|
|
|
|
const user = msg.userLogin;
|
|
if ( inst.moderatedUsers.has(user) )
|
|
return;
|
|
|
|
const mod_action = msg.moderationType;
|
|
let new_action;
|
|
if ( mod_action === mod_types.Ban )
|
|
new_action = 'ban';
|
|
else if ( mod_action === mod_types.Delete )
|
|
new_action = 'delete';
|
|
else if ( mod_action === mod_types.Unban )
|
|
new_action = 'unban';
|
|
else if ( mod_action === mod_types.Timeout )
|
|
new_action = 'timeout';
|
|
|
|
if ( new_action )
|
|
msg.moderationActionType = new_action;
|
|
|
|
const do_remove = t.chat.context.get('chat.filtering.remove-deleted') === 3;
|
|
if ( do_remove ) {
|
|
const len = inst.buffer.length,
|
|
target_id = msg.targetMessageID;
|
|
inst.buffer = inst.buffer.filter(m =>
|
|
m.type !== types.Message || ! m.user || m.user.userLogin !== user ||
|
|
(target_id && m.id !== target_id)
|
|
);
|
|
if ( len !== inst.buffer.length && ! inst.props.isBackground )
|
|
inst.notifySubscribers();
|
|
|
|
inst.ffzModerateBuffer([inst.delayedMessageBuffer], msg);
|
|
|
|
} else
|
|
inst.ffzModerateBuffer([inst.buffer, inst.delayedMessageBuffer], msg);
|
|
|
|
inst.moderatedUsers.add(user);
|
|
setTimeout(inst.unsetModeratedUser(user), 1e3);
|
|
|
|
inst.delayedMessageBuffer.push({
|
|
event: msg,
|
|
time: Date.now(),
|
|
shouldDelay: false
|
|
});
|
|
|
|
return;
|
|
|
|
} else if ( msg.type === types.Clear ) {
|
|
if ( t.chat.context.get('chat.filtering.ignore-clear') )
|
|
msg = {
|
|
type: types.Info,
|
|
message: t.i18n.t('chat.ignore-clear', 'An attempt by a moderator to clear chat was ignored.')
|
|
}
|
|
}
|
|
|
|
} catch(err) {
|
|
t.log.error('Error processing chat event.', err);
|
|
t.log.capture(err, {extra: {msg}});
|
|
}
|
|
}
|
|
|
|
return old_handle.call(inst, msg);
|
|
}
|
|
|
|
inst.ffzModerateBuffer = function(buffers, event) {
|
|
const mod_types = t.mod_types || {},
|
|
mod_type = event.moderationActionType,
|
|
user_login = event.targetUserLogin || event.userLogin,
|
|
mod_login = event.createdByLogin,
|
|
target_id = event.targetMessageID || event.messageID;
|
|
|
|
let deleted_count = 0, last_msg;
|
|
|
|
const is_delete = mod_type === mod_types.Delete,
|
|
updater = m => {
|
|
if ( m.event )
|
|
m = m.event;
|
|
|
|
if ( target_id && m.id !== target_id )
|
|
return;
|
|
|
|
const msg = inst.markUserEventDeleted(m, user_login);
|
|
if ( ! msg )
|
|
return;
|
|
|
|
last_msg = msg;
|
|
deleted_count++;
|
|
|
|
msg.modLogin = mod_login;
|
|
msg.modActionType = mod_type;
|
|
msg.duration = event.duration;
|
|
|
|
if ( is_delete )
|
|
return true;
|
|
};
|
|
|
|
for(const buffer of buffers)
|
|
if ( buffer.some(updater) )
|
|
break;
|
|
|
|
//t.log.info('Moderate Buffer', mod_type, user_login, mod_login, target_id, deleted_count, last_msg);
|
|
|
|
if ( last_msg )
|
|
last_msg.deletedCount = deleted_count;
|
|
}
|
|
|
|
|
|
|
|
inst.getMessages = function() {
|
|
const buf = inst.buffer,
|
|
size = t.chat.context.get('chat.scrollback-length'),
|
|
ct = t.chat_types || CHAT_TYPES,
|
|
target = buf.length - size;
|
|
|
|
if ( target > 0 ) {
|
|
let removed = 0, last;
|
|
for(let i=0; i < target; i++)
|
|
if ( buf[i] && ! NULL_TYPES.includes(ct[buf[i].type]) ) {
|
|
removed++;
|
|
last = i;
|
|
}
|
|
|
|
inst.buffer = buf.slice(removed % 2 === 0 ? target : Math.max(target - 10, last));
|
|
} else
|
|
// Make a shallow copy of the array because other code expects it to change.
|
|
inst.buffer = buf.slice(0);
|
|
|
|
return inst.buffer;
|
|
}
|
|
}
|
|
|
|
cls.prototype.componentDidMount = function() {
|
|
try {
|
|
this._ffzInstall();
|
|
} catch(err) {
|
|
t.log.error('Error installing FFZ features onto chat buffer.', err);
|
|
}
|
|
|
|
return old_mount.call(this);
|
|
}
|
|
|
|
cls.prototype.flushRawMessages = function() {
|
|
const out = [],
|
|
now = Date.now(),
|
|
raw_delay = t.chat.context.get('chat.delay'),
|
|
delay = raw_delay === -1 ? this.delayDuration : raw_delay,
|
|
first = now - delay,
|
|
see_deleted = this.shouldSeeBlockedAndDeletedMessages || this.props && this.props.shouldSeeBlockedAndDeletedMessages,
|
|
do_remove = t.chat.context.get('chat.filtering.remove-deleted');
|
|
|
|
let changed = false;
|
|
|
|
for(const msg of this.delayedMessageBuffer) {
|
|
if ( msg.time <= first || ! msg.shouldDelay ) {
|
|
if ( do_remove !== 0 && (do_remove > 1 || ! see_deleted) && this.isDeletable(msg.event) && msg.event.deleted )
|
|
continue;
|
|
|
|
this.buffer.push(msg.event);
|
|
changed = true;
|
|
|
|
} else
|
|
out.push(msg);
|
|
}
|
|
|
|
this.delayedMessageBuffer = out;
|
|
if ( changed && ! this.props.isBackground )
|
|
this.notifySubscribers();
|
|
}
|
|
}
|
|
|
|
|
|
sendMessage(room, message) {
|
|
const service = this.ChatService.first;
|
|
|
|
if ( ! service || ! room )
|
|
return null;
|
|
|
|
if ( room.startsWith('#') )
|
|
room = room.slice(1);
|
|
|
|
if ( room.toLowerCase() !== service.props.channelLogin.toLowerCase() )
|
|
return service.client.sendCommand(room, message);
|
|
|
|
service.sendMessage(message);
|
|
}
|
|
|
|
|
|
wrapChatService(cls) {
|
|
const t = this,
|
|
old_mount = cls.prototype.componentDidMount,
|
|
old_handler = cls.prototype.connectHandlers;
|
|
|
|
cls.prototype._ffz_was_here = true;
|
|
|
|
cls.prototype.ffzGetEmotes = function() {
|
|
const emote_map = this.client && this.client.session && this.client.session.emoteMap;
|
|
if ( this._ffz_cached_map === emote_map )
|
|
return this._ffz_cached_emotes;
|
|
|
|
this._ffz_cached_map = emote_map;
|
|
const emotes = this._ffz_cached_emotes = {};
|
|
|
|
if ( emote_map )
|
|
for(const emote of Object.values(emote_map))
|
|
if ( emote ) {
|
|
const token = emote.token;
|
|
if ( Array.isArray(REGEX_EMOTES[token]) ) {
|
|
for(const tok of REGEX_EMOTES[token] )
|
|
emotes[tok] = emote.id;
|
|
|
|
} else
|
|
emotes[token] = emote.id;
|
|
}
|
|
|
|
return emotes;
|
|
}
|
|
|
|
|
|
cls.prototype._ffzInstall = function() {
|
|
if ( this._ffz_installed )
|
|
return;
|
|
|
|
this._ffz_installed = true;
|
|
|
|
const inst = this,
|
|
old_send = this.sendMessage;
|
|
|
|
inst.sendMessage = function(raw_msg) {
|
|
const msg = raw_msg.replace(/\n/g, '');
|
|
|
|
if ( msg.startsWith('/ffz') ) {
|
|
inst.addMessage({
|
|
type: t.chat_types.Notice,
|
|
message: 'The /ffz command is not yet re-implemented.'
|
|
})
|
|
|
|
return false;
|
|
}
|
|
|
|
const event = new FFZEvent({
|
|
message: msg,
|
|
channel: inst.props.channelLogin
|
|
});
|
|
|
|
t.emit('chat:pre-send-message', event);
|
|
|
|
if ( event.defaultPrevented )
|
|
return;
|
|
|
|
return old_send.call(this, msg);
|
|
}
|
|
}
|
|
|
|
|
|
cls.prototype.componentDidMount = function() {
|
|
try {
|
|
this._ffzInstall();
|
|
} catch(err) {
|
|
t.log.error('Error installing FFZ features onto chat service.', err);
|
|
}
|
|
|
|
return old_mount.call(this);
|
|
}
|
|
|
|
|
|
cls.prototype.connectHandlers = function(...args) {
|
|
if ( ! this._ffz_init ) {
|
|
const i = this;
|
|
|
|
for(const key of MISBEHAVING_EVENTS) {
|
|
const original = this[key];
|
|
if ( original )
|
|
this[key] = function(e, t) {
|
|
i._wrapped = e;
|
|
const ret = original.call(i, e, t);
|
|
i._wrapped = null;
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
const old_chat = this.onChatMessageEvent;
|
|
this.onChatMessageEvent = function(e) {
|
|
if ( e && e.sentByCurrentUser ) {
|
|
try {
|
|
e.message.user.emotes = findEmotes(
|
|
e.message.body,
|
|
i.ffzGetEmotes()
|
|
);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
}
|
|
}
|
|
|
|
return old_chat.call(i, e);
|
|
}
|
|
|
|
|
|
const old_action = this.onChatActionEvent;
|
|
this.onChatActionEvent = function(e) {
|
|
if ( e && e.sentByCurrentUser ) {
|
|
try {
|
|
e.message.user.emotes = findEmotes(
|
|
e.message.body,
|
|
i.ffzGetEmotes()
|
|
);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
}
|
|
}
|
|
|
|
return old_action.call(i, e);
|
|
}
|
|
|
|
|
|
const old_sub = this.onSubscriptionEvent;
|
|
this.onSubscriptionEvent = function(e) {
|
|
try {
|
|
if ( t.chat.context.get('chat.subs.show') < 3 )
|
|
return;
|
|
|
|
e.body = '';
|
|
const out = i.convertMessage({message: e});
|
|
out.ffz_type = 'resub';
|
|
out.sub_plan = e.methods;
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_sub.call(i, e);
|
|
}
|
|
}
|
|
|
|
const old_resub = this.onResubscriptionEvent;
|
|
this.onResubscriptionEvent = function(e) {
|
|
try {
|
|
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
|
|
return;
|
|
|
|
const out = i.convertMessage({message: e});
|
|
out.ffz_type = 'resub';
|
|
out.sub_cumulative = e.cumulativeMonths || 0;
|
|
out.sub_streak = e.streakMonths || 0;
|
|
out.sub_share_streak = e.shouldShareStreakTenure;
|
|
out.sub_months = e.months;
|
|
out.sub_plan = e.methods;
|
|
|
|
//t.log.info('Resub Event', e, out);
|
|
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_resub.call(i, e);
|
|
}
|
|
}
|
|
|
|
const mysteries = this.ffz_mysteries = {};
|
|
|
|
const old_subgift = this.onSubscriptionGiftEvent;
|
|
this.onSubscriptionGiftEvent = function(e) {
|
|
try {
|
|
const key = `${e.channel}:${e.user.userID}`,
|
|
mystery = mysteries[key];
|
|
|
|
if ( mystery ) {
|
|
if ( mystery.expires < Date.now() ) {
|
|
mysteries[key] = null;
|
|
} else {
|
|
mystery.recipients.push({
|
|
id: e.recipientID,
|
|
login: e.recipientLogin,
|
|
displayName: e.recipientName
|
|
});
|
|
|
|
if( mystery.recipients.length >= mystery.size )
|
|
mysteries[key] = null;
|
|
|
|
if ( mystery.line )
|
|
mystery.line.forceUpdate();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
e.body = '';
|
|
const out = i.convertMessage({message: e});
|
|
out.ffz_type = 'sub_gift';
|
|
out.sub_recipient = {
|
|
id: e.recipientID,
|
|
login: e.recipientLogin,
|
|
displayName: e.recipientName
|
|
};
|
|
out.sub_plan = e.methods;
|
|
out.sub_total = e.senderCount;
|
|
|
|
//t.log.info('Sub Gift', e, out);
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_subgift.call(i, e);
|
|
}
|
|
}
|
|
|
|
const old_anonsubgift = this.onAnonSubscriptionGiftEvent;
|
|
this.onAnonSubscriptionGiftEvent = function(e) {
|
|
try {
|
|
const key = `${e.channel}:ANON`,
|
|
mystery = mysteries[key];
|
|
|
|
if ( mystery ) {
|
|
if ( mystery.expires < Date.now() )
|
|
mysteries[key] = null;
|
|
else {
|
|
mystery.recipients.push({
|
|
id: e.recipientID,
|
|
login: e.recipientLogin,
|
|
displayName: e.recipientName
|
|
});
|
|
|
|
if( mystery.recipients.length >= mystery.size )
|
|
mysteries[key] = null;
|
|
|
|
if ( mystery.line )
|
|
mystery.line.forceUpdate();
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
e.body = '';
|
|
const out = i.convertMessage({message: e});
|
|
out.ffz_type = 'sub_gift';
|
|
out.sub_anon = true;
|
|
out.sub_recipient = {
|
|
id: e.recipientID,
|
|
login: e.recipientLogin,
|
|
displayName: e.recipientName
|
|
};
|
|
out.sub_plan = e.methods;
|
|
out.sub_total = e.senderCount;
|
|
|
|
//t.log.info('Anon Sub Gift', e, out);
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_anonsubgift.call(i, e);
|
|
}
|
|
}
|
|
|
|
const old_submystery = this.onSubscriptionMysteryGiftEvent;
|
|
this.onSubscriptionMysteryGiftEvent = function(e) {
|
|
try {
|
|
let mystery = null;
|
|
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
|
|
const key = `${e.channel}:${e.user.userID}`;
|
|
mystery = mysteries[key] = {
|
|
recipients: [],
|
|
size: e.massGiftCount,
|
|
expires: Date.now() + 30000
|
|
};
|
|
}
|
|
|
|
e.body = '';
|
|
const out = i.convertMessage({message: e});
|
|
out.ffz_type = 'sub_mystery';
|
|
out.mystery = mystery;
|
|
out.sub_plan = e.plan;
|
|
out.sub_count = e.massGiftCount;
|
|
out.sub_total = e.senderCount;
|
|
|
|
//t.log.info('Sub Mystery', e, out);
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_submystery.call(i, e);
|
|
}
|
|
}
|
|
|
|
const old_anonsubmystery = this.onAnonSubscriptionMysteryGiftEvent;
|
|
this.onAnonSubscriptionMysteryGiftEvent = function(e) {
|
|
try {
|
|
let mystery = null;
|
|
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
|
|
const key = `${e.channel}:ANON`;
|
|
mystery = mysteries[key] = {
|
|
recipients: [],
|
|
size: e.massGiftCount,
|
|
expires: Date.now() + 30000
|
|
};
|
|
}
|
|
|
|
e.body = '';
|
|
const out = i.convertMessage({message: e});
|
|
out.ffz_type = 'sub_mystery';
|
|
out.sub_anon = true;
|
|
out.mystery = mystery;
|
|
out.sub_plan = e.plan;
|
|
out.sub_count = e.massGiftCount;
|
|
out.sub_total = e.senderCount;
|
|
|
|
//t.log.info('Anon Sub Mystery', e, out);
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_anonsubmystery.call(i, e);
|
|
}
|
|
}
|
|
|
|
const old_ritual = this.onRitualEvent;
|
|
this.onRitualEvent = function(e) {
|
|
try {
|
|
const out = i.convertMessage(e);
|
|
out.ffz_type = 'ritual';
|
|
out.ritual = e.type;
|
|
|
|
return i.postMessageToCurrentChannel(e, out);
|
|
|
|
} catch(err) {
|
|
t.log.capture(err, {extra: e});
|
|
return old_ritual.call(i, e);
|
|
}
|
|
}
|
|
|
|
const old_host = this.onHostingEvent;
|
|
this.onHostingEvent = function (e, _t) {
|
|
t.emit('tmi:host', e, _t);
|
|
return old_host.call(i, e, _t);
|
|
}
|
|
|
|
const old_unhost = this.onUnhostEvent;
|
|
this.onUnhostEvent = function (e, _t) {
|
|
t.emit('tmi:unhost', e, _t);
|
|
return old_unhost.call(i, e, _t);
|
|
}
|
|
|
|
const old_add = this.addMessage;
|
|
this.addMessage = function(e) {
|
|
const original = i._wrapped;
|
|
if ( original && ! e._ffz_checked )
|
|
return i.postMessageToCurrentChannel(original, e);
|
|
|
|
return old_add.call(i, e);
|
|
}
|
|
|
|
this._ffz_init = true;
|
|
}
|
|
|
|
return old_handler.apply(this, ...args);
|
|
}
|
|
|
|
cls.prototype.postMessageToCurrentChannel = function(original, message) {
|
|
message._ffz_checked = true;
|
|
|
|
if ( original.channel ) {
|
|
let chan = message.channel = original.channel.toLowerCase();
|
|
if ( chan.startsWith('#') )
|
|
chan = chan.slice(1);
|
|
|
|
if ( chan !== this.props.channelLogin.toLowerCase() )
|
|
return;
|
|
|
|
message.roomLogin = chan;
|
|
}
|
|
|
|
if ( original.message ) {
|
|
const user = original.message.user,
|
|
flags = original.message.flags;
|
|
if ( user )
|
|
message.emotes = user.emotes;
|
|
|
|
if ( flags && this.getFilterFlagOptions )
|
|
message.flags = this.getFilterFlagOptions(flags);
|
|
|
|
if ( typeof original.action === 'string' )
|
|
message.message = original.action;
|
|
else
|
|
message.message = original.message.body;
|
|
}
|
|
|
|
this.addMessage(message);
|
|
}
|
|
}
|
|
|
|
|
|
updateChatLines() {
|
|
this.PinnedCheer.forceUpdate();
|
|
this.chat_line.updateLines();
|
|
}
|
|
|
|
|
|
// ========================================================================
|
|
// Pinned Cheers
|
|
// ========================================================================
|
|
|
|
fixPinnedCheer(inst) {
|
|
const el = this.fine.getChildNode(inst),
|
|
container = el && el.querySelector && el.querySelector('.pinned-cheer__headline'),
|
|
tc = inst.props.topCheer;
|
|
|
|
if ( ! container || ! tc )
|
|
return;
|
|
|
|
container.dataset.roomId = inst.props.channelID;
|
|
container.dataset.room = inst.props.channelLogin && inst.props.channelLogin.toLowerCase();
|
|
container.dataset.userId = tc.user.userID;
|
|
container.dataset.user = tc.user.userLogin && tc.user.userLogin.toLowerCase();
|
|
|
|
if ( tc.user.color ) {
|
|
const user_el = container.querySelector('.chat-author__display-name');
|
|
if ( user_el )
|
|
user_el.style.color = this.colors.process(tc.user.color);
|
|
|
|
const login_el = container.querySelector('.chat-author__intl-login');
|
|
if ( login_el )
|
|
login_el.style.color = this.colors.process(tc.user.color);
|
|
}
|
|
|
|
const bit_el = container.querySelector('.chat-line__message--emote'),
|
|
cont = bit_el ? bit_el.parentElement.parentElement : container.querySelector('.ffz--pinned-top-emote'),
|
|
prefix = extractCheerPrefix(tc.messageParts);
|
|
|
|
if ( cont && prefix ) {
|
|
const tokens = this.chat.tokenizeString(`${prefix}${tc.bits}`, tc);
|
|
|
|
cont.classList.add('ffz--pinned-top-emote');
|
|
cont.innerHTML = '';
|
|
setChildren(cont, this.chat.renderTokens(tokens));
|
|
}
|
|
}
|
|
|
|
|
|
// ========================================================================
|
|
// Room Handling
|
|
// ========================================================================
|
|
|
|
addRoom(thing, props) {
|
|
if ( ! props )
|
|
props = thing.props;
|
|
|
|
if ( ! props.channelID )
|
|
return null;
|
|
|
|
const room = thing._ffz_room = this.chat.getRoom(props.channelID, props.channelLogin && props.channelLogin.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;
|
|
}
|
|
|
|
|
|
// ========================================================================
|
|
// Chat Controller
|
|
// ========================================================================
|
|
|
|
chatMounted(chat, props) {
|
|
if ( chat.chatBuffer )
|
|
chat.chatBuffer.ffzController = chat;
|
|
|
|
if ( ! props )
|
|
props = chat.props;
|
|
|
|
if ( ! this.addRoom(chat, props) )
|
|
return;
|
|
|
|
this.updateRoomBitsConfig(chat, props.bitsConfig);
|
|
|
|
// TODO: Check if this is the room for the current channel.
|
|
|
|
this.settings.updateContext({
|
|
moderator: props.isCurrentUserModerator,
|
|
chatHidden: props.isHidden
|
|
});
|
|
|
|
this.chat.context.updateContext({
|
|
moderator: props.isCurrentUserModerator,
|
|
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
|
channelID: props.channelID,
|
|
ui: {
|
|
theme: props.theme
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
chatUmounted(chat) {
|
|
if ( chat.chatBuffer && chat.chatBuffer.ffzController === this )
|
|
chat.chatBuffer.ffzController = null;
|
|
|
|
this.removeRoom(chat);
|
|
}
|
|
|
|
|
|
chatUpdated(chat, props) {
|
|
if ( chat.chatBuffer )
|
|
chat.chatBuffer.ffzController = chat;
|
|
|
|
if ( props.channelID !== chat.props.channelID ) {
|
|
this.removeRoom(chat);
|
|
this.chatMounted(chat, props);
|
|
return;
|
|
}
|
|
|
|
if ( props.bitsConfig !== chat.props.bitsConfig )
|
|
this.updateRoomBitsConfig(chat, props.bitsConfig);
|
|
|
|
// TODO: Check if this is the room for the current channel.
|
|
|
|
this.settings.updateContext({
|
|
moderator: props.isCurrentUserModerator,
|
|
chatHidden: props.isHidden
|
|
});
|
|
|
|
this.chat.context.updateContext({
|
|
moderator: props.isCurrentUserModerator,
|
|
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
|
channelID: props.channelID,
|
|
ui: {
|
|
theme: props.theme
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
updateRoomBitsConfig(chat, config) { // eslint-disable-line class-methods-use-this
|
|
const room = chat._ffz_room;
|
|
if ( ! room )
|
|
return;
|
|
|
|
room.updateBitsConfig(formatBitsConfig(config));
|
|
this.updateChatLines();
|
|
}
|
|
|
|
|
|
// ========================================================================
|
|
// Chat Buffer Connector
|
|
// ========================================================================
|
|
|
|
connectorMounted(inst) { // eslint-disable-line class-methods-use-this
|
|
const buffer = inst.props.messageBufferAPI;
|
|
if ( buffer && buffer._ffz_inst && buffer._ffz_inst._ffz_connector !== inst )
|
|
buffer._ffz_inst._ffz_connector = inst;
|
|
}
|
|
|
|
connectorUpdated(inst, props) { // eslint-disable-line class-methods-use-this
|
|
const buffer = inst.props.messageBufferAPI,
|
|
new_buffer = props.messageBufferAPI;
|
|
|
|
if ( buffer === new_buffer )
|
|
return;
|
|
|
|
if ( buffer && buffer._ffz_inst && buffer._ffz_inst._ffz_connector === inst )
|
|
buffer._ffz_inst._ffz_connector = null;
|
|
|
|
if ( new_buffer && new_buffer._ffz_inst && new_buffer._ffz_inst._ffz_connector !== inst )
|
|
buffer._ffz_inst._ffz_connector = inst;
|
|
}
|
|
|
|
connectorUnmounted(inst) { // eslint-disable-line class-methods-use-this
|
|
const buffer = inst.props.messageBufferAPI;
|
|
if ( buffer && buffer._ffz_inst && buffer._ffz_inst._ffz_connector === inst )
|
|
buffer._ffz_inst._ffz_connector = null;
|
|
}
|
|
|
|
|
|
// ========================================================================
|
|
// Chat Containers
|
|
// ========================================================================
|
|
|
|
containerMounted(cont, props) {
|
|
if ( ! props )
|
|
props = cont.props;
|
|
|
|
if ( ! this.addRoom(cont, props) )
|
|
return;
|
|
|
|
if ( props.data ) {
|
|
this.chat.badges.updateTwitchBadges(props.data.badges);
|
|
this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges);
|
|
}
|
|
}
|
|
|
|
|
|
containerUpdated(cont, props) {
|
|
if ( props.channelID !== cont.props.channelID ) {
|
|
this.removeRoom(cont);
|
|
this.containerMounted(cont, props);
|
|
return;
|
|
}
|
|
|
|
// Twitch, React, and Apollo are the trifecta of terror so we
|
|
// can't compare the badgeSets property in any reasonable way.
|
|
// Instead, just check the lengths to see if they've changed
|
|
// and hope that badge versions will never change separately.
|
|
const data = props.data || {},
|
|
odata = cont.props.data || {},
|
|
|
|
bs = data.badges || [],
|
|
obs = odata.badges || [],
|
|
|
|
cs = data.user && data.user.broadcastBadges || [],
|
|
ocs = odata.user && odata.user.broadcastBadges || [];
|
|
|
|
if ( bs.length !== obs.length )
|
|
this.chat.badges.updateTwitchBadges(bs);
|
|
|
|
if ( cs.length !== ocs.length )
|
|
this.updateRoomBadges(cont, cs);
|
|
}
|
|
|
|
updateRoomBadges(cont, badges) { // eslint-disable-line class-methods-use-this
|
|
const room = cont._ffz_room;
|
|
if ( ! room )
|
|
return;
|
|
|
|
room.updateBadges(badges);
|
|
this.updateChatLines();
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================================
|
|
// Processing Functions
|
|
// ============================================================================
|
|
|
|
export function formatBitsConfig(config) {
|
|
if ( ! config )
|
|
return;
|
|
|
|
const out = {},
|
|
actions = config.indexedActions;
|
|
|
|
for(const key in actions)
|
|
if ( has(actions, key) ) {
|
|
const action = actions[key],
|
|
new_act = out[key] = {
|
|
id: action.id,
|
|
prefix: action.prefix,
|
|
tiers: []
|
|
};
|
|
|
|
for(const tier of action.orderedTiers) {
|
|
const images = {};
|
|
for(const im of tier.images) {
|
|
const themed = images[im.theme] = images[im.theme] || [],
|
|
ak = im.isAnimated ? 'animated' : 'static',
|
|
anim = themed[ak] = themed[ak] || {};
|
|
|
|
anim[im.dpiScale] = im.url;
|
|
}
|
|
|
|
new_act.tiers.push({
|
|
amount: tier.bits,
|
|
color: tier.color,
|
|
id: tier.id,
|
|
images
|
|
})
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
|
|
export function findEmotes(msg, emotes) {
|
|
const out = {};
|
|
let idx = 0;
|
|
|
|
for(const part of msg.split(' ')) {
|
|
const len = split_chars(part).length;
|
|
|
|
if ( has(emotes, part) ) {
|
|
const em = emotes[part],
|
|
matches = out[em] = out[em] || [];
|
|
|
|
matches.push({
|
|
startIndex: idx,
|
|
endIndex: idx + len - 1
|
|
});
|
|
}
|
|
|
|
idx += len + 1;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
|
|
function extractCheerPrefix(parts) {
|
|
for(const part of parts) {
|
|
if ( part.type !== 3 || ! part.content.cheerAmount )
|
|
continue;
|
|
|
|
return part.content.alt;
|
|
}
|
|
|
|
return null;
|
|
}
|