2017-11-13 01:23:39 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Chat Hooks
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
import {ColorAdjuster} from 'utilities/color';
|
|
|
|
import {setChildren} from 'utilities/dom';
|
|
|
|
import {has} from 'utilities/object';
|
|
|
|
|
|
|
|
import Module from 'utilities/module';
|
|
|
|
|
2017-11-14 04:12:10 -05:00
|
|
|
import Scroller from './scroller';
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
const EVENTS = [
|
|
|
|
'onJoinedEvent',
|
|
|
|
'onDisconnectedEvent',
|
|
|
|
'onReconnectingEvent',
|
|
|
|
'onHostingEvent',
|
|
|
|
'onUnhostEvent',
|
|
|
|
'onChatMessageEvent',
|
|
|
|
'onChatActionEvent',
|
|
|
|
'onChatNoticeEvent',
|
|
|
|
'onTimeoutEvent',
|
|
|
|
'onBanEvent',
|
|
|
|
'onModerationEvent',
|
|
|
|
'onSubscriptionEvent',
|
|
|
|
'onResubscriptionEvent',
|
|
|
|
'onRoomStateEvent',
|
|
|
|
'onSlowModeEvent',
|
|
|
|
'onFollowerOnlyModeEvent',
|
|
|
|
'onSubscriberOnlyModeEvent',
|
|
|
|
'onClearChatEvent',
|
|
|
|
'onRaidEvent',
|
|
|
|
'onUnraidEvent',
|
|
|
|
'onBadgesUpdatedEvent'
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
export default class ChatHook extends Module {
|
|
|
|
constructor(...args) {
|
|
|
|
super(...args);
|
|
|
|
|
|
|
|
this.should_enable = true;
|
|
|
|
|
|
|
|
this.colors = new ColorAdjuster;
|
|
|
|
|
|
|
|
this.inject('settings');
|
|
|
|
|
|
|
|
this.inject('site');
|
|
|
|
this.inject('site.router');
|
|
|
|
this.inject('site.fine');
|
|
|
|
this.inject('site.web_munch');
|
|
|
|
this.inject('site.css_tweaks');
|
|
|
|
|
|
|
|
this.inject('chat');
|
|
|
|
|
2017-11-14 04:12:10 -05:00
|
|
|
this.inject(Scroller);
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
this.ChatController = this.fine.define(
|
|
|
|
'chat-controller',
|
|
|
|
n => n.chatService
|
|
|
|
);
|
|
|
|
|
|
|
|
this.ChatContainer = this.fine.define(
|
|
|
|
'chat-container',
|
|
|
|
n => n.showViewersList && n.onChatInputFocus
|
|
|
|
);
|
|
|
|
|
|
|
|
this.ChatLine = this.fine.define(
|
|
|
|
'chat-line',
|
|
|
|
n => n.renderMessageBody
|
|
|
|
);
|
|
|
|
|
|
|
|
this.PinnedCheer = this.fine.define(
|
|
|
|
'pinned-cheer',
|
|
|
|
n => n.collapseCheer && n.saveRenderedMessageRef
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 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.',
|
|
|
|
component: 'setting-text-box',
|
|
|
|
process(val) {
|
|
|
|
val = parseInt(val, 10);
|
|
|
|
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
|
|
|
return 340;
|
|
|
|
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-11-14 18:48:56 -05:00
|
|
|
this.settings.add('chat.font-size', {
|
|
|
|
default: 12,
|
|
|
|
ui: {
|
|
|
|
path: 'Chat > Appearance >> General',
|
|
|
|
title: 'Font Size',
|
|
|
|
description: 'How large should text in chat be, in pixels.',
|
|
|
|
component: 'setting-text-box',
|
|
|
|
process(val) {
|
|
|
|
val = parseInt(val, 10);
|
|
|
|
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
|
|
|
|
return 12;
|
|
|
|
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.settings.add('chat.font-family', {
|
|
|
|
default: '',
|
|
|
|
ui: {
|
|
|
|
path: 'Chat > Appearance >> General',
|
|
|
|
title: 'Font Family',
|
|
|
|
description: 'Set the font used for displaying chat messages.',
|
|
|
|
component: 'setting-text-box'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
this.settings.add('chat.bits.show-pinned', {
|
|
|
|
default: true,
|
|
|
|
ui: {
|
|
|
|
path: 'Chat > Bits and Cheering >> Pinned Cheers',
|
|
|
|
title: 'Display Pinned Cheer',
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
// TODO: Get the background color from the theme system.
|
|
|
|
c._base = is_dark ? '#0e0c13' : '#faf9fa';
|
|
|
|
c.mode = mode;
|
|
|
|
c.contrast = contrast;
|
|
|
|
|
|
|
|
this.updateChatLines();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-14 18:48:56 -05:00
|
|
|
updateChatCSS() {
|
|
|
|
const width = this.chat.context.get('chat.width'),
|
|
|
|
size = this.chat.context.get('chat.font-size'),
|
|
|
|
font = this.chat.context.get('chat.font-family');
|
|
|
|
|
|
|
|
if ( size === 12 )
|
|
|
|
this.css_tweaks.style.delete('chat-font-size');
|
|
|
|
else {
|
|
|
|
const lh = Math.round((20/12) * size);
|
|
|
|
this.css_tweaks.style.set('chat-font-size',`.chat-list{font-size:${size}px;line-height:${lh}px}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( ! font )
|
|
|
|
this.css_tweaks.style.delete('chat-font-family');
|
|
|
|
else {
|
|
|
|
let val = font;
|
|
|
|
if ( font.indexOf(' ') !== -1 && val.indexOf(',') === -1 && val.indexOf('"') === -1 && val.indexOf("'") === -1 )
|
|
|
|
val = `"${val}"`;
|
|
|
|
this.css_tweaks.style.set('chat-font-family', `.chat-list{font-family:${val}}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( width === 340 )
|
|
|
|
this.css_tweaks.style.delete('chat-width');
|
|
|
|
else
|
|
|
|
this.css_tweaks.style.set('chat-width', `.channel-page__right-column{width:${width}px!important}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onEnable() {
|
2017-11-14 18:48:56 -05:00
|
|
|
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);
|
2017-11-13 01:23:39 -05:00
|
|
|
this.chat.context.on('changed:chat.bits.stack', this.updateChatLines, 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.lines.alternate', val =>
|
|
|
|
this.css_tweaks.toggle('chat-rows', val));
|
|
|
|
|
|
|
|
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.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'));
|
|
|
|
|
2017-11-14 18:48:56 -05:00
|
|
|
this.updateChatCSS();
|
2017-11-13 01:23:39 -05:00
|
|
|
this.updateColors();
|
|
|
|
this.updateLineBorders();
|
|
|
|
|
|
|
|
this.ChatController.on('mount', this.chatMounted, this);
|
|
|
|
this.ChatController.on('unmount', this.removeRoom, this);
|
|
|
|
this.ChatController.on('receive-props', this.chatUpdated, this);
|
|
|
|
|
|
|
|
this.ChatController.ready((cls, instances) => {
|
|
|
|
for(const inst of instances) {
|
|
|
|
const service = inst.chatService;
|
|
|
|
if ( ! service._ffz_was_here )
|
|
|
|
this.wrapChatService(service.constructor);
|
|
|
|
|
|
|
|
service.client.events.removeAll();
|
|
|
|
service.connectHandlers();
|
|
|
|
|
|
|
|
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) => {
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const React = this.web_munch.getModule('react');
|
|
|
|
|
|
|
|
if ( React ) {
|
|
|
|
const t = this,
|
|
|
|
e = React.createElement;
|
|
|
|
|
|
|
|
this.ChatLine.ready((cls, instances) => {
|
|
|
|
cls.prototype.shouldComponentUpdate = function(props, state) {
|
|
|
|
const show = state.alwaysShowMessage || ! props.message.deleted,
|
|
|
|
old_show = this._ffz_show;
|
|
|
|
|
|
|
|
// We can't just compare props.message.deleted to this.props.message.deleted
|
|
|
|
// because the message object is the same object. So, store the old show
|
|
|
|
// state for later reference.
|
|
|
|
this._ffz_show = show;
|
|
|
|
|
|
|
|
return show !== old_show ||
|
|
|
|
//state.renderDebug !== this.state.renderDebug ||
|
|
|
|
props.message !== this.props.message ||
|
|
|
|
props.isCurrentUserModerator !== this.props.isCurrentUserModerator ||
|
|
|
|
props.showModerationIcons !== this.props.showModerationIcons ||
|
|
|
|
props.showTimestamps !== this.props.showTimestamps;
|
|
|
|
}
|
|
|
|
|
|
|
|
//const old_render = cls.prototype.render;
|
|
|
|
|
|
|
|
cls.prototype.render = function() {
|
|
|
|
const msg = this.props.message,
|
|
|
|
is_action = msg.type === 1,
|
|
|
|
user = msg.user,
|
|
|
|
color = t.colors.process(user.color),
|
|
|
|
room = msg.channel ? msg.channel.slice(1) : undefined,
|
|
|
|
|
|
|
|
show = this.state.alwaysShowMessage || ! this.props.message.deleted;
|
|
|
|
|
|
|
|
if ( ! msg.message && msg.messageParts )
|
|
|
|
detokenizeMessage(msg);
|
|
|
|
|
|
|
|
const tokens = t.chat.tokenizeMessage(msg),
|
|
|
|
fragment = t.chat.renderTokens(tokens, e);
|
|
|
|
|
|
|
|
return e('div', {
|
|
|
|
className: 'chat-line__message',
|
|
|
|
'data-room-id': this.props.channelID,
|
|
|
|
'data-room': room,
|
|
|
|
'data-user-id': user.userID,
|
2017-11-14 18:48:56 -05:00
|
|
|
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
//onClick: () => this.setState({renderDebug: ((this.state.renderDebug||0) + 1) % 3})
|
|
|
|
}, [
|
|
|
|
this.props.showTimestamps && e('span', {
|
|
|
|
className: 'chat-line__timestamp'
|
|
|
|
}, t.chat.formatTime(msg.timestamp)),
|
|
|
|
this.renderModerationIcons(),
|
|
|
|
e('span', {
|
|
|
|
className: 'chat-line__message--badges'
|
|
|
|
}, t.chat.renderBadges(msg, e)),
|
|
|
|
e('a', {
|
|
|
|
className: 'chat-author__display-name',
|
|
|
|
style: { color },
|
|
|
|
onClick: this.usernameClickHandler
|
|
|
|
}, user.userDisplayName),
|
|
|
|
user.isIntl && e('span', {
|
|
|
|
className: 'chat-author__intl-login',
|
2017-11-14 04:12:10 -05:00
|
|
|
style: { color },
|
|
|
|
onClick: this.usernameClickHandler
|
2017-11-13 01:23:39 -05:00
|
|
|
}, ` (${user.userLogin})`),
|
|
|
|
e('span', null, is_action ? ' ' : ': '),
|
|
|
|
show ?
|
|
|
|
e('span', {
|
|
|
|
className:'message',
|
|
|
|
style: is_action ? { color } : null
|
|
|
|
}, fragment)
|
|
|
|
:
|
|
|
|
e('span', {
|
|
|
|
className: 'chat-line__message--deleted',
|
|
|
|
}, e('a', {
|
|
|
|
href: '',
|
|
|
|
onClick: this.alwaysShowMessage
|
|
|
|
}, `<message deleted>`)),
|
|
|
|
|
|
|
|
/*this.state.renderDebug === 2 && e('div', {
|
|
|
|
className: 'border mg-t-05'
|
|
|
|
}, old_render.call(this)),
|
|
|
|
|
|
|
|
this.state.renderDebug === 1 && e('div', {
|
|
|
|
className: 'message--debug',
|
|
|
|
style: {
|
|
|
|
fontFamily: 'monospace',
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
lineHeight: '1.1em'
|
|
|
|
}
|
|
|
|
}, JSON.stringify([tokens, msg.emotes], null, 2))*/
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
for(const inst of instances)
|
|
|
|
inst.forceUpdate();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wrapChatService(cls) {
|
|
|
|
const t = this,
|
|
|
|
old_handler = cls.prototype.connectHandlers;
|
|
|
|
|
|
|
|
cls.prototype._ffz_was_here = true;
|
|
|
|
|
|
|
|
cls.prototype.connectHandlers = function(...args) {
|
|
|
|
if ( ! this._ffz_init ) {
|
|
|
|
const i = this,
|
|
|
|
pm = this.postMessage;
|
|
|
|
|
|
|
|
for(const key of EVENTS) { // eslint-disable-line guard-for-in
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.postMessage = function(e) {
|
|
|
|
const original = this._wrapped;
|
|
|
|
if ( original ) {
|
|
|
|
// Check that the message is relevant to this channel.
|
2017-11-14 18:48:56 -05:00
|
|
|
if ( original.channel && this.channelLogin && original.channel.slice(1) !== this.channelLogin.toLowerCase() )
|
2017-11-13 01:23:39 -05:00
|
|
|
return;
|
|
|
|
|
|
|
|
const c = e.channel = original.channel;
|
|
|
|
if ( c )
|
|
|
|
e.roomLogin = c.charAt(0) === '#' ? c.slice(1) : c;
|
|
|
|
|
|
|
|
if ( original.message ) {
|
|
|
|
if ( original.action )
|
|
|
|
e.message = original.action;
|
|
|
|
else
|
|
|
|
e.message = original.message.body;
|
|
|
|
|
|
|
|
if ( original.message.user )
|
|
|
|
e.emotes = original.message.user.emotes;
|
|
|
|
}
|
|
|
|
|
|
|
|
//e.original = original;
|
|
|
|
}
|
|
|
|
|
|
|
|
//t.log.info('postMessage', e);
|
|
|
|
return pm.call(this, e);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._ffz_init = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return old_handler.apply(this, ...args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateChatLines() {
|
|
|
|
for(const inst of this.PinnedCheer.instances)
|
|
|
|
inst.forceUpdate();
|
|
|
|
|
|
|
|
for(const inst of this.ChatLine.instances)
|
|
|
|
inst.forceUpdate();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
// Pinned Cheers
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
fixPinnedCheer(inst) {
|
|
|
|
const el = this.fine.getHostNode(inst),
|
|
|
|
container = el && el.querySelector && el.querySelector('.pinned-cheer__headline'),
|
|
|
|
tc = inst.props.topCheer;
|
|
|
|
|
|
|
|
if ( ! container || ! tc )
|
|
|
|
return;
|
|
|
|
|
|
|
|
container.dataset.roomId = inst.props.channelID;
|
2017-11-14 18:48:56 -05:00
|
|
|
container.dataset.room = inst.props.channelLogin && inst.props.channelLogin.toLowerCase();
|
2017-11-13 01:23:39 -05:00
|
|
|
container.dataset.userId = tc.user.userID;
|
2017-11-14 18:48:56 -05:00
|
|
|
container.dataset.user = tc.user.userLogin && tc.user.userLogin.toLowerCase();
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2017-11-14 18:48:56 -05:00
|
|
|
const room = thing._ffz_room = this.chat.getRoom(props.channelID, props.channelLogin && props.channelLogin.toLowerCase(), false, true);
|
2017-11-13 01:23:39 -05:00
|
|
|
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 ( ! props )
|
|
|
|
props = chat.props;
|
|
|
|
|
|
|
|
if ( ! this.addRoom(chat, props) )
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.updateRoomBitsConfig(chat, props.bitsConfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
chatUpdated(chat, props) {
|
|
|
|
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,
|
2017-11-14 18:48:56 -05:00
|
|
|
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
2017-11-13 01:23:39 -05:00
|
|
|
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 Containers
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
containerMounted(cont, props) {
|
|
|
|
if ( ! props )
|
|
|
|
props = cont.props;
|
|
|
|
|
|
|
|
if ( ! this.addRoom(cont, props) )
|
|
|
|
return;
|
|
|
|
|
|
|
|
if ( props.badgeSets ) {
|
|
|
|
this.chat.updateBadges(props.badgeSets.globalsBySet);
|
|
|
|
this.updateRoomBadges(cont, props.badgeSets.channelsBySet);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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 bs = props.badgeSets,
|
|
|
|
obs = cont.props.badgeSets,
|
|
|
|
|
|
|
|
bsgl = bs.globalsBySet && bs.globalsBySet.size || 0,
|
|
|
|
obsgl = obs.globalsBySet && obs.globalsBySet.size || 0,
|
|
|
|
|
|
|
|
bscl = bs.channelsBySet && bs.channelsBySet.size || 0,
|
|
|
|
obscl = obs.channelsBySet && obs.channelsBySet.size || 0;
|
|
|
|
|
|
|
|
if ( bsgl !== obsgl )
|
|
|
|
this.chat.updateBadges(bs.globalsBySet);
|
|
|
|
|
|
|
|
if ( bscl !== obscl )
|
|
|
|
this.updateRoomBadges(cont, bs.channelsBySet);
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function extractCheerPrefix(parts) {
|
|
|
|
for(const part of parts) {
|
|
|
|
if ( part.type !== 3 || ! part.content.cheerAmount )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
return part.content.alt;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function detokenizeMessage(msg) {
|
|
|
|
const out = [],
|
|
|
|
parts = msg.messageParts,
|
|
|
|
l = parts.length,
|
|
|
|
emotes = {};
|
|
|
|
|
|
|
|
let idx = 0, ret, last_type = null;
|
|
|
|
|
|
|
|
for(let i=0; i < l; i++) {
|
|
|
|
const part = parts[i],
|
|
|
|
type = part.type,
|
|
|
|
content = part.content;
|
|
|
|
|
|
|
|
if ( type === 0 )
|
|
|
|
ret = content;
|
|
|
|
|
|
|
|
else if ( type === 1 )
|
|
|
|
ret = `@${content.recipient}`;
|
|
|
|
|
|
|
|
else if ( type === 2 )
|
|
|
|
ret = content.displayText;
|
|
|
|
|
|
|
|
else if ( type === 3 ) {
|
|
|
|
if ( content.cheerAmount ) {
|
|
|
|
ret = `${content.alt}${content.cheerAmount}`;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
const url = (content.images.themed ? content.images.dark : content.images.sources)['1x'],
|
|
|
|
match = /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url),
|
|
|
|
id = match && match[1];
|
|
|
|
|
|
|
|
ret = content.alt;
|
|
|
|
|
|
|
|
if ( id ) {
|
|
|
|
const em = emotes[id] = emotes[id] || [],
|
|
|
|
offset = last_type > 0 ? 1 : 0;
|
|
|
|
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( last_type > 0 )
|
|
|
|
ret = ` ${ret}`;
|
|
|
|
|
|
|
|
} else if ( type === 4 )
|
|
|
|
ret = `https://clips.twitch.tv/${content.slug}`;
|
|
|
|
|
|
|
|
if ( ret ) {
|
|
|
|
idx += ret.length;
|
|
|
|
last_type = type;
|
|
|
|
out.push(ret);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
msg.message = out.join('');
|
|
|
|
msg.emotes = emotes;
|
|
|
|
return msg;
|
|
|
|
}
|