1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-27 13:08:30 +00:00

Move chat-line into its own module. Update the chat buffer code to always remove visible lines in pairs to avoid messing up rendering. Grab chat line types from webpack.

This commit is contained in:
SirStendec 2017-11-14 22:13:30 -05:00
parent 2c28afc6a6
commit 10c6c1cb87
3 changed files with 269 additions and 162 deletions

View file

@ -86,7 +86,8 @@ export default class Twilight extends BaseSite {
Twilight.KNOWN_MODULES = { Twilight.KNOWN_MODULES = {
simplebar: n => n.globalObserver && n.initDOMLoadedElements, simplebar: n => n.globalObserver && n.initDOMLoadedElements,
react: n => n.Component && n.createElement, react: n => n.Component && n.createElement,
'extension-service': n => n.extensionService 'extension-service': n => n.extensionService,
'chat-types': n => n.a && n.a.PostWithMention
} }

View file

@ -11,6 +11,47 @@ import {has} from 'utilities/object';
import Module from 'utilities/module'; import Module from 'utilities/module';
import Scroller from './scroller'; import Scroller from './scroller';
import ChatLine from './line';
const ChatTypes = (e => {
e[e.Post = 0] = "Post",
e[e.Action = 1] = "Action",
e[e.PostWithMention = 2] = "PostWithMention",
e[e.Ban = 3] = "Ban",
e[e.Timeout = 4] = "Timeout",
e[e.AutoModRejectedPrompt = 5] = "AutoModRejectedPrompt",
e[e.AutoModMessageRejected = 6] = "AutoModMessageRejected",
e[e.AutoModMessageAllowed = 7] = "AutoModMessageAllowed",
e[e.AutoModMessageDenied = 8] = "AutoModMessageDenied",
e[e.Connected = 9] = "Connected",
e[e.Disconnected = 10] = "Disconnected",
e[e.Reconnect = 11] = "Reconnect",
e[e.Hosting = 12] = "Hosting",
e[e.Unhost = 13] = "Unhost",
e[e.Subscription = 14] = "Subscription",
e[e.Resubscription = 15] = "Resubscription",
e[e.SubGift = 16] = "SubGift",
e[e.Clear = 17] = "Clear",
e[e.SubscriberOnlyMode = 18] = "SubscriberOnlyMode",
e[e.FollowerOnlyMode = 19] = "FollowerOnlyMode",
e[e.SlowMode = 20] = "SlowMode",
e[e.RoomMods = 21] = "RoomMods",
e[e.RoomState = 22] = "RoomState",
e[e.Raid = 23] = "Raid",
e[e.Unraid = 24] = "Unraid",
e[e.Notice = 25] = "Notice",
e[e.Info = 26] = "Info",
e[e.BadgesUpdated = 27] = "BadgesUpdated",
e[e.Purchase = 28] = "Purchase"
})({});
const NULL_TYPES = [
'Reconnect',
'RoomState',
'BadgesUpdated'
];
const EVENTS = [ const EVENTS = [
@ -57,6 +98,7 @@ export default class ChatHook extends Module {
this.inject('chat'); this.inject('chat');
this.inject(Scroller); this.inject(Scroller);
this.inject(ChatLine);
this.ChatController = this.fine.define( this.ChatController = this.fine.define(
@ -69,11 +111,6 @@ export default class ChatHook extends Module {
n => n.showViewersList && n.onChatInputFocus n => n.showViewersList && n.onChatInputFocus
); );
this.ChatLine = this.fine.define(
'chat-line',
n => n.renderMessageBody
);
this.PinnedCheer = this.fine.define( this.PinnedCheer = this.fine.define(
'pinned-cheer', 'pinned-cheer',
n => n.collapseCheer && n.saveRenderedMessageRef n => n.collapseCheer && n.saveRenderedMessageRef
@ -233,10 +270,12 @@ export default class ChatHook extends Module {
onEnable() { onEnable() {
const ct = this.web_munch.getModule('chat-types');
this.chatTypes = ct ? ct.a : ChatTypes;
this.chat.context.on('changed:chat.width', this.updateChatCSS, this); 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-size', this.updateChatCSS, this);
this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this); this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this);
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-mode', this.updateColors, this);
this.chat.context.on('changed:chat.adjustment-contrast', 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:theme.is-dark', this.updateColors, this);
@ -272,6 +311,10 @@ export default class ChatHook extends Module {
if ( ! service._ffz_was_here ) if ( ! service._ffz_was_here )
this.wrapChatService(service.constructor); this.wrapChatService(service.constructor);
const buffer = inst.chatBuffer;
if ( ! buffer._ffz_was_here )
this.wrapChatBuffer(buffer.constructor);
service.client.events.removeAll(); service.client.events.removeAll();
service.connectHandlers(); service.connectHandlers();
@ -297,107 +340,34 @@ export default class ChatHook extends Module {
for(const inst of instances) for(const inst of instances)
this.fixPinnedCheer(inst); this.fixPinnedCheer(inst);
}); });
}
const React = this.web_munch.getModule('react'); wrapChatBuffer(cls) {
const t = this;
if ( React ) { cls.prototype._ffz_was_here = true;
const t = this,
e = React.createElement;
this.ChatLine.ready((cls, instances) => { cls.prototype.toArray = function() {
cls.prototype.shouldComponentUpdate = function(props, state) { const buf = this.buffer,
const show = state.alwaysShowMessage || ! props.message.deleted, ct = t.chatTypes,
old_show = this._ffz_show; target = buf.length - this.maxSize;
// We can't just compare props.message.deleted to this.props.message.deleted if ( target > 0 ) {
// because the message object is the same object. So, store the old show let removed = 0, last;
// state for later reference. for(let i=0; i < target; i++)
this._ffz_show = show; if ( buf[i] && ! NULL_TYPES.includes(ct[buf[i].type]) ) {
removed++;
last = i;
}
return show !== old_show || this.buffer = buf.slice(removed % 2 === 0 ? target : last);
//state.renderDebug !== this.state.renderDebug || } else
props.message !== this.props.message || // Make a shallow copy of the array because other code expects it to change.
props.isCurrentUserModerator !== this.props.isCurrentUserModerator || this.buffer = Array.from(buf);
props.showModerationIcons !== this.props.showModerationIcons ||
props.showTimestamps !== this.props.showTimestamps;
}
//const old_render = cls.prototype.render; this._isDirty = false;
return this.buffer;
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,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
//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',
style: { color },
onClick: this.usernameClickHandler
}, ` (${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();
});
} }
} }
@ -464,8 +434,7 @@ export default class ChatHook extends Module {
for(const inst of this.PinnedCheer.instances) for(const inst of this.PinnedCheer.instances)
inst.forceUpdate(); inst.forceUpdate();
for(const inst of this.ChatLine.instances) this.chat_line.updateLines();
inst.forceUpdate();
} }
@ -697,63 +666,4 @@ function extractCheerPrefix(parts) {
} }
return null; 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;
} }

View file

@ -0,0 +1,196 @@
'use strict';
// ============================================================================
// Chat Line
// ============================================================================
import {createElement as e} from 'utilities/dom';
import Module from 'utilities/module';
export default class ChatLine extends Module {
constructor(...args) {
super(...args);
this.inject('settings');
this.inject('i18n');
this.inject('chat');
this.inject('site.fine');
this.inject('site.web_munch');
this.ChatLine = this.fine.define(
'chat-line',
n => n.renderMessageBody
);
}
onEnable() {
this.chat.context.on('changed:chat.bits.stack', this.updateLines, this);
const t = this,
React = this.web_munch.getModule('react');
if ( ! React )
return;
const 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;
}
cls.prototype.render = function() {
const msg = this.props.message,
is_action = msg.type === 1,
user = msg.user,
color = t.parent.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);
const out = e('div', {
className: 'chat-line__message',
'data-room-id': this.props.channelID,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
//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',
style: { color },
onClick: this.usernameClickHandler
}, ` (${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))*/
]);
return out;
}
for(const inst of instances)
inst.forceUpdate();
})
}
updateLines() {
for(const inst of this.ChatLine.instances)
inst.forceUpdate();
}
}
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;
}