1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-23 14:30:54 +00:00
* Added: Option to change the size of Message Hover actions.
* Added: New chat action appearance type "Emote" that makes it easy to use an emote image for an action.
* Changed: Do not show the "Pin" action on messages with no message body.
* Changed: Use Twitch's API for embeds/tooltips of Twitch URLs. This now makes use of clip embed data being sent via PubSub, notably.
* Fixed: Multiple emotes with the same name being listed in tab-completion.

* Experiment: There's a new chat line render method available. This is not currently enabled for any users, but it will be enabled after more internal testing. The new method is not necessarily faster, though it should not be slower. The main purpose of the rewrite is code de-duplication and making the renderer easier to maintain.
* API Added: `chat.addLinkProvider(provider);` to register a handler for link data.
* API Fixed: Do not allow duplicate registration of tokenizers or rich embed handlers for chat.
This commit is contained in:
SirStendec 2022-12-18 17:30:34 -05:00
parent 4001d15b18
commit 14400e16bc
24 changed files with 2404 additions and 451 deletions

View file

@ -8,7 +8,7 @@ import Twilight from 'site';
import Module from 'utilities/module';
import RichContent from './rich_content';
import { has } from 'utilities/object';
import { has, maybe_call } from 'utilities/object';
import { KEYS, RERENDER_SETTINGS, UPDATE_BADGE_SETTINGS, UPDATE_TOKEN_SETTINGS } from 'utilities/constants';
import { print_duration } from 'utilities/time';
import { FFZEvent } from 'utilities/events';
@ -37,113 +37,285 @@ export default class ChatLine extends Module {
this.inject('chat.actions');
this.inject('chat.overrides');
/*this.line_types = {};
this.line_types = {};
this.line_types.sub_mystery = (msg, u, r, inst, e) => {
const mystery = msg.mystery;
if ( mystery )
mystery.line = this;
this.line_types.cheer = {
renderNotice: (msg, current_user, room, inst, e) => {
return this.i18n.tList(
'chat.bits-message',
'Cheered {count, plural, one {# Bit} other {# Bits}}',
{
count: msg.bits || 0
}
);
}
};
const sub_msg = this.i18n.tList('chat.sub.gift', "{user} is gifting {count, plural, one {# Tier {tier} Sub} other {# Tier {tier} Subs}} to {channel}'s community! ", {
user: (msg.sub_anon || msg.user.username === 'ananonymousgifter') ?
this.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
e('span', {
this.line_types.points = {
getClass: (msg) => {
const highlight = msg.ffz_reward_highlight && this.chat.context.get('chat.points.allow-highlight') === 2;
return `ffz--points-line tw-pd-l-1 tw-pd-r-2 ${highlight ? 'ffz-custom-color ffz--points-highlight' : ''}`;
},
renderNotice: (msg, current_user, room, inst, e) => {
if ( ! msg.ffz_reward )
return null;
// We need to get the message's tokens to see if it has a message or not.
const user = msg.user,
tokens = msg.ffz_tokens = msg.ffz_tokens || this.chat.tokenizeMessage(msg, current_user),
has_message = tokens.length > 0;
// Elements for the reward and cost with nice formatting.
const reward = e('span', {className: 'ffz--points-reward'}, getRewardTitle(msg.ffz_reward, this.i18n)),
cost = e('span', {className: 'ffz--points-cost'}, [
e('span', {className: 'ffz--points-icon'}),
this.i18n.formatNumber(getRewardCost(msg.ffz_reward))
]);
if (! has_message)
return this.i18n.tList('chat.points.user-redeemed', '{user} redeemed {reward} {cost}', {
reward, cost,
user: e('span', {
role: 'button',
className: 'chatter-name',
onClick: inst.ffz_user_click_handler,
onContextMenu: this.actions.handleUserContext
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, user.displayName))
});
return this.i18n.tList('chat.points.redeemed', 'Redeemed {reward} {cost}', {reward, cost});
}
};
this.line_types.resub = {
getClass: () => `ffz--subscribe-line tw-pd-r-2`,
renderNotice: (msg, current_user, room, inst, e) => {
const months = msg.sub_cumulative || msg.sub_months,
setting = this.chat.context.get('chat.subs.show');
if ( !(setting === 3 || (setting === 1 && out && months > 1) || (setting === 2 && months > 1)) )
return null;
const user = msg.user,
plan = msg.sub_plan || {},
tier = SUB_TIERS[plan.plan] || 1;
const sub_msg = this.i18n.tList('chat.sub.main', '{user} subscribed {plan}. ', {
user: e('span', {
role: 'button',
className: 'chatter-name',
onClick: inst.ffz_user_click_handler
onClick: inst.ffz_user_click_handler,
onContextMenu: this.actions.handleUserContext
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, msg.user.displayName)),
count: msg.sub_count,
tier: SUB_TIERS[msg.sub_plan] || 1,
channel: msg.roomLogin
});
}, user.displayName)),
plan: plan.prime ?
this.i18n.t('chat.sub.twitch-prime', 'with Prime Gaming') :
this.i18n.t('chat.sub.plan', 'at Tier {tier}', {tier})
});
if ( msg.sub_total === 1 )
sub_msg.push(this.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
else if ( msg.sub_total > 1 )
sub_msg.push(this.i18n.t('chat.sub.gift-total', "They've gifted {count} Subs in the channel!", {
count: msg.sub_total
}));
if ( msg.sub_share_streak && msg.sub_streak > 1 ) {
sub_msg.push(this.i18n.t(
'chat.sub.cumulative-months',
"They've subscribed for {cumulative,number} months, currently on a {streak,number} month streak!",
{
cumulative: msg.sub_cumulative,
streak: msg.sub_streak
}
));
if ( ! inst.ffz_click_expand )
inst.ffz_click_expand = () => {
inst.setState({
ffz_expanded: ! inst.state.ffz_expanded
} else if ( months > 1 ) {
sub_msg.push(this.i18n.t(
'chat.sub.months',
"They've subscribed for {count,number} months!",
{
count: months
}
));
}
if ( ! this.chat.context.get('chat.subs.compact') )
sub_msg.ffz_icon = e('span', {
className: `ffz-i-${plan.prime ? 'crown' : 'star'} tw-mg-r-05`
});
return sub_msg;
}
};
this.line_types.ritual = {
getClass: () => `ffz--ritual-line tw-pd-r-2`,
renderNotice: (msg, current_user, room, inst, e) => {
const user = msg.user;
if ( msg.ritual === 'new_chatter' ) {
return this.i18n.tList('chat.ritual', '{user} is new here. Say hello!', {
user: e('span', {
role: 'button',
className: 'chatter-name',
onClick: inst.ffz_user_click_handler,
onContextMenu: this.actions.handleUserContext
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, user.displayName))
});
}
}
};
const expanded = this.chat.context.get('chat.subs.merge-gifts-visibility') ?
! inst.state.ffz_expanded : inst.state.ffz_expanded;
this.line_types.sub_gift = {
getClass: () => 'ffz--subscribe-line',
let sub_list = null;
if( expanded && mystery && mystery.recipients && mystery.recipients.length > 0 ) {
const the_list = [];
for(const x of mystery.recipients) {
if ( the_list.length )
the_list.push(', ');
renderNotice: (msg, current_user, room, inst, e) => {
const user = msg.user,
the_list.push(e('span', {
plan = msg.sub_plan || {},
months = msg.sub_months || 1,
tier = SUB_TIERS[plan.plan] || 1;
let sub_msg;
const bits = {
months,
user: (msg.sub_anon || user.username === 'ananonymousgifter') ?
this.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
e('span', {
role: 'button',
className: 'chatter-name',
onClick: inst.ffz_user_click_handler,
onContextMenu: this.actions.handleUserContext
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, user.displayName)),
plan: plan.plan === 'custom' ? '' :
this.i18n.t('chat.sub.gift-plan', 'Tier {tier}', {tier}),
recipient: e('span', {
role: 'button',
className: 'ffz--giftee-name',
className: 'chatter-name',
onClick: inst.ffz_user_click_handler,
'data-user': JSON.stringify(x)
'data-user': JSON.stringify(msg.sub_recipient)
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, x.displayName)));
}, msg.sub_recipient.displayName))
};
if ( months <= 1 )
sub_msg = this.i18n.tList('chat.sub.mystery', '{user} gifted a {plan} Sub to {recipient}! ', bits);
else
sub_msg = this.i18n.tList('chat.sub.gift-months', '{user} gifted {months, plural, one {# month} other {# months}} of {plan} Sub to {recipient}!', bits);
if ( msg.sub_total === 1 )
sub_msg.push(this.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
else if ( msg.sub_total > 1 )
sub_msg.push(this.i18n.t('chat.sub.gift-total', "They've gifted {count,number} Subs in the channel!", {
count: msg.sub_total
}));
if ( ! this.chat.context.get('chat.subs.compact') )
sub_msg.ffz_icon = e('span', {
className: `ffz-i-${plan.prime ? 'crown' : 'star'} tw-mg-r-05`
});
return sub_msg;
}
}
this.line_types.sub_mystery = {
getClass: () => 'ffz--subscribe-line',
renderNotice: (msg, user, room, inst, e) => {
const mystery = msg.mystery;
if ( mystery )
mystery.line = inst;
const sub_msg = this.i18n.tList('chat.sub.gift', "{user} is gifting {count, plural, one {# Tier {tier} Sub} other {# Tier {tier} Subs}} to {channel}'s community! ", {
user: (msg.sub_anon || msg.user.username === 'ananonymousgifter') ?
this.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
e('span', {
role: 'button',
className: 'chatter-name',
onClick: inst.ffz_user_click_handler,
onContextMenu: this.actions.handleUserContext
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, msg.user.displayName)),
count: msg.sub_count,
tier: SUB_TIERS[msg.sub_plan] || 1,
channel: msg.roomLogin
});
if ( msg.sub_total === 1 )
sub_msg.push(this.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
else if ( msg.sub_total > 1 )
sub_msg.push(this.i18n.t('chat.sub.gift-total', "They've gifted {count} Subs in the channel!", {
count: msg.sub_total
}));
if ( ! inst.ffz_click_expand )
inst.ffz_click_expand = () => {
inst.setState({
ffz_expanded: ! inst.state.ffz_expanded
});
}
const expanded = this.chat.context.get('chat.subs.merge-gifts-visibility') ?
! inst.state.ffz_expanded : inst.state.ffz_expanded;
let sub_list = null;
if( expanded && mystery && mystery.recipients && mystery.recipients.length > 0 ) {
const the_list = [];
for(const x of mystery.recipients) {
if ( the_list.length )
the_list.push(', ');
the_list.push(e('span', {
role: 'button',
className: 'ffz--giftee-name',
onClick: inst.ffz_user_click_handler,
'data-user': JSON.stringify(x)
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, x.displayName)));
}
sub_list = e('div', {
className: 'tw-mg-t-05 tw-border-t tw-pd-t-05 tw-c-text-alt-2'
}, the_list);
}
sub_list = e('div', {
className: 'tw-mg-t-05 tw-border-t tw-pd-t-05 tw-c-text-alt-2'
}, the_list);
}
const target = [
sub_msg
];
const extra_ts = this.chat.context.get('chat.extra-timestamps');
if ( mystery )
target.push(e('span', {
className: `tw-pd-l-05 tw-font-size-4 ffz-i-${expanded ? 'down' : 'right'}-dir`
}));
return inst.ffzDrawLine(
msg,
`ffz-notice-line user-notice-line tw-pd-y-05 ffz--subscribe-line`,
[
const out = [
e('div', {
className: 'tw-flex tw-c-text-alt-2',
className: 'tw-full-width tw-c-text-alt-2',
onClick: inst.ffz_click_expand
}, [
this.chat.context.get('chat.subs.compact') ? null :
e('figure', {
className: `ffz-i-star${msg.sub_anon ? '-empty' : ''} tw-mg-r-05`
}),
e('div', null, [
extra_ts && (inst.props.showTimestamps || inst.props.isHistorical) && e('span', {
className: 'chat-line__timestamp'
}, this.chat.formatTime(msg.timestamp)),
msg.sub_anon ? null : this.actions.renderInline(msg, inst.props.showModerationIcons, u, r, e),
sub_msg
]),
mystery ? e('div', {
className: 'tw-pd-l-05 tw-font-size-4'
}, e('figure', {
className: `ffz-i-${expanded ? 'down' : 'right'}-dir tw-pd-y-1`
})) : null
]),
}, target),
sub_list
],
[
e('button', {
className: 'tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon ffz-core-button tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ffz-tooltip ffz-tooltip--no-mouse',
'data-title': 'Test'
}, e('span', {
className: 'tw-button-icon__icon'
}, e('figure', {className: 'ffz-i-threads'}))),
e('button', {
className: 'tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon ffz-core-button tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative ffz-tooltip ffz-tooltip--no-mouse',
'data-title': 'Thing'
}, e('span', {
className: 'tw-button-icon__icon'
}, e('figure', {className: 'ffz-i-cog'})))
],
null
);
}*/
];
if ( ! this.chat.context.get('chat.subs.compact') )
out.ffz_icon = e('span', {
className: `ffz-i-star${msg.sub_anon ? '-empty' : ''} tw-mg-r-05`
});
out.ffz_target = target;
return out;
}
};
this.ChatLine = this.fine.define(
'chat-line',
@ -172,6 +344,21 @@ export default class ChatLine extends Module {
this.on('chat:update-line-badges', this.updateLineBadges, this);
this.on('i18n:update', this.rerenderLines, this);
this.on('experiments:changed:line_renderer', () => {
const value = this.experiments.get('line_renderer'),
cls = this.ChatLine._class;
this.log.debug('Changing line renderer:', value ? 'new' : 'old');
if (cls) {
cls.prototype.render = this.experiments.get('line_renderer')
? cls.prototype.ffzNewRender
: cls.prototype.ffzOldRender;
this.rerenderLines();
}
});
for(const setting of RERENDER_SETTINGS)
this.chat.context.on(`changed:${setting}`, this.rerenderLines, this);
@ -441,43 +628,40 @@ export default class ChatLine extends Module {
]);
}
cls.prototype.ffzDrawLine = function(msg, cls, out, hover_actions, bg_css) {
const anim_hover = t.chat.context.get('chat.emotes.animated') === 2;
if (hover_actions) {
cls = `${cls} tw-relative`;
out = [
e('div', {
className: 'chat-line__message-highlight tw-absolute tw-border-radius-medium tw-top-0 tw-bottom-0 tw-right-0 tw-left-0',
'data-test-selector': 'chat-message-highlight'
}),
e('div', {
className: 'chat-line__message-container tw-relative'
}, out),
e('div', {
className: 'chat-line__reply-icon tw-absolute tw-border-radius-medium tw-c-background-base tw-elevation-1'
}, hover_actions)
];
}
return e('div', {
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
style: {backgroundColor: bg_css},
'data-room-id': msg.roomId,
'data-room': msg.roomLogin,
'data-user-id': msg.user.userID,
'data-user': msg.user.userLogin && msg.user.userLogin.toLowerCase(),
onMouseOver: anim_hover ? t.chat.emotes.animHover : null,
onMouseOut: anim_hover ? t.chat.emotes.animLeave : null
}, out);
}
/*cls.prototype.new_render = function() { try {
cls.prototype.ffzNewRender = function() { try {
this._ffz_no_scan = true;
const msg = t.chat.standardizeMessage(this.props.message),
reply_mode = t.chat.context.get('chat.replies.style');
override_mode = t.chat.context.get('chat.filtering.display-deleted');
// Before anything else, check to see if the deleted message view is set
// to BRIEF and the message is deleted. In that case we can exit very
// early.
let mod_mode = this.props.deletedMessageDisplay;
if ( override_mode )
mod_mode = override_mode;
else if ( ! this.props.isCurrentUserModerator && mod_mode === 'DETAILED' )
mod_mode = 'LEGACY';
if ( mod_mode === 'BRIEF' && msg.deleted ) {
const deleted_count = this.props.deletedCount;
if ( deleted_count == null )
return null;
return e(
'div', {
className: 'chat-line__status'
},
t.i18n.t('chat.deleted-messages', `{count,plural,
one {One message was deleted by a moderator.}
other {# messages were deleted by a moderator.}
}`, {
count: deleted_count
})
);
}
// Get the current room id and login. We might need to look these up.
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined,
room_id = msg.roomId ? msg.roomId : this.props.channelID;
@ -493,21 +677,23 @@ export default class ChatLine extends Module {
room_id = msg.roomId = r.id;
}
const u = t.site.getUser(),
r = {id: room_id, login: room};
// Construct the current room and current user objects.
const current_user = t.site.getUser(),
current_room = {id: room_id, login: room};
const has_replies = this.props && !!(this.props.hasReply || this.props.reply || ! this.props.replyRestrictedReason),
const reply_mode = t.chat.context.get('chat.replies.style'),
has_replies = this.props && !!(this.props.hasReply || this.props.reply || ! this.props.replyRestrictedReason),
can_replies = has_replies && msg.message && ! msg.deleted && ! this.props.disableReplyClick,
can_reply = can_replies && u && u.login !== msg.user?.login && ! msg.reply,
twitch_clickable = reply_mode === 1 && can_replies && (!!msg.reply || can_reply);
can_reply = can_replies && (has_replies || (current_user && current_user.login !== msg.user?.login));
if ( u ) {
u.moderator = this.props.isCurrentUserModerator;
u.staff = this.props.isCurrentUserStaff;
u.reply_mode = reply_mode;
u.can_reply = can_reply;
if ( current_user ) {
current_user.moderator = this.props.isCurrentUserModerator;
current_user.staff = this.props.isCurrentUserStaff;
current_user.reply_mode = reply_mode;
current_user.can_reply = can_reply;
}
// Set up our click handlers as necessary.
if ( ! this.ffz_open_reply )
this.ffz_open_reply = this.ffzOpenReply.bind(this);
@ -524,7 +710,7 @@ export default class ChatLine extends Module {
if ( ds && ds.user ) {
try {
target_user = JSON.parse(ds.user);
} catch(err) { /* nothing~! * / }
} catch(err) { /* nothing~! */ }
}
const fe = new FFZEvent({
@ -532,7 +718,7 @@ export default class ChatLine extends Module {
event,
message: msg,
user: target_user,
room: r
room: current_room
});
t.emit('chat:user-click', fe);
@ -546,11 +732,316 @@ export default class ChatLine extends Module {
this.ffz_user_click_handler = this.openViewerCard || this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
}
// Do we have a special renderer?
if ( msg.ffz_type && t.line_types[msg.ffz_type] )
return t.line_types[msg.ffz_type](msg, u, r, this, e);
let notice;
let klass;
let bg_css = null;
return this.ffz_old_render();
// Do we have a special renderer?
let type = msg.ffz_type && t.line_types[msg.ffz_type];
if ( ! type && msg.bits > 0 && t.chat.context.get('chat.bits.cheer-notice') )
type = t.line_types.cheer;
if ( type ) {
if ( type.render )
return type.render(msg, current_user, current_room, this, e);
if ( type.renderNotice )
notice = type.renderNotice(msg, current_user, current_room, this, e);
if ( type.getClass )
klass = type.getClass(msg, current_user, current_room, this, e);
}
// Render the line.
const user = msg.user,
anim_hover = t.chat.context.get('chat.emotes.animated') === 2;
// Cache the lower login
if ( user && ! user.lowerLogin && user.userLogin )
user.lowerLogin = user.userLogin.toLowerCase();
// Ensure we have a string for klass.
klass = klass || '';
// RENDERING: Start~
// First, check how we should handle a deleted message.
let show;
let deleted;
let mod_action = null;
if ( mod_mode === 'BRIEF' ) {
// We already handle msg.deleted for BRIEF earlier than this.
show = true;
deleted = false;
} else if ( mod_mode === 'DETAILED' ) {
show = true;
deleted = msg.deleted;
} else {
show = this.state?.alwaysShowMessage || ! msg.deleted;
deleted = false;
}
if ( msg.deleted ) {
const show_mode = t.chat.context.get('chat.filtering.display-mod-action');
if ( show_mode === 2 || (show_mode === 1 && mod_mode === 'DETAILED') ) {
const action = msg.modActionType;
if ( action === 'timeout' )
mod_action = t.i18n.t('chat.mod-action.timeout',
'{duration} Timeout'
, {
duration: print_duration(msg.duration || 1)
});
else if ( action === 'ban' )
mod_action = t.i18n.t('chat.mod-action.ban', 'Banned');
else if ( action === 'delete' || ! action )
mod_action = t.i18n.t('chat.mod-action.delete', 'Deleted');
if ( mod_action && msg.modLogin )
mod_action = t.i18n.t('chat.mod-action.by', '{action} by {login}', {
login: msg.modLogin,
action: mod_action
});
if ( mod_action )
mod_action = e('span', {
className: 'tw-pd-l-05',
'data-test-selector': 'chat-deleted-message-attribution'
}, `(${mod_action})`);
}
}
// Check to see if we have message content to render.
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, current_user),
has_message = tokens.length > 0 || ! notice;
let message;
if ( has_message ) {
// Let's calculate some remaining values that we need.
const reply_tokens = (reply_mode === 2 || (reply_mode === 1 && this.props.repliesAppearancePreference && this.props.repliesAppearancePreference !== 'expanded'))
? (msg.ffz_reply = msg.ffz_reply || t.chat.tokenizeReply(this.props.reply))
: null;
const is_action = t.parent.message_types && t.parent.message_types.Action === msg.messageType,
action_style = is_action ? t.chat.context.get('chat.me-style') : 0,
action_italic = action_style >= 2,
action_color = action_style === 1 || action_style === 3;
const raw_color = t.overrides.getColor(user.id) || user.color,
color = t.parent.colors.process(raw_color);
const rich_content = show && FFZRichContent && t.chat.pluckRichContent(tokens, msg);
// First, render the user block.
const username = t.chat.formatUser(user, e),
override_name = t.overrides.getName(user.id);
const user_props = {
className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${msg.ffz_user_class ?? ''}`,
role: 'button',
style: { color },
onClick: this.ffz_user_click_handler,
onContextMenu: t.actions.handleUserContext
};
if ( msg.ffz_user_props )
Object.assign(user_props, msg.ffz_user_props);
if ( msg.ffz_user_style )
Object.assign(user_props.style, msg.ffz_user_style);
const user_block = e(
'span',
user_props,
override_name
? [
e('span', {
className: 'chat-author__display-name'
}, override_name),
e('div', {
className: 'ffz-il-tooltip ffz-il-tooldip--down ffz-il-tooltip--align-center'
}, username)
]
: username
);
// The timestamp.
const timestamp = (this.props.showTimestamps || this.props.isHistorical)
? e('span', { className: 'chat-line__timestamp' }, t.chat.formatTime(msg.timestamp))
: null;
// The reply token for FFZ style.
const reply_token = show && has_replies && reply_tokens
? t.chat.renderTokens(reply_tokens, e)
: null;
// Check for a Twitch-style points highlight.
const twitch_highlight = msg.ffz_reward_highlight && t.chat.context.get('chat.points.allow-highlight') === 1;
// The reply element for Twitch style.
const twitch_reply = reply_mode === 1 && this.props.reply && this.props.repliesAppearancePreference && this.props.repliesAppearancePreference === 'expanded'
? this.renderReplyLine()
: null;
// Now assemble the pieces.
message = [
twitch_reply,
// The preamble
timestamp,
t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this),
this.renderInlineHighlight ? this.renderInlineHighlight() : null,
// Badges
e('span', {
className: 'chat-line__message--badges'
}, t.chat.badges.render(msg, e)),
// User
user_block,
// The separator
e('span', {'aria-hidden': true}, is_action ? ' ' : ': '),
// Reply Token
reply_token,
// Message
show
? e(
'span',
{
className: `message ${action_italic ? 'chat-line__message-body--italicized' : ''} ${twitch_highlight ? 'chat-line__message-body--highlighted' : ''}`,
style: action_color ? { color} : null
},
t.chat.renderTokens(
tokens, e, (reply_mode !== 0 && has_replies) ? this.props.reply : null
)
)
: e(
'span',
{
className: 'chat-line__message--deleted'
},
e('a', {
href: '',
onClick: this.alwaysShowMessage
}, t.i18n.t('chat.message-deleted', '<message deleted>'))
),
// Moderation Action
mod_action,
// Rich Content
rich_content
? e(FFZRichContent, rich_content)
: null
];
}
// Is there a notice?
let out;
if ( notice ) {
const is_raw = Array.isArray(notice.ffz_target);
if ( ! message ) {
const want_ts = t.chat.context.get('chat.extra-timestamps'),
timestamp = want_ts && (this.props.showTimestamps || this.props.isHistorical)
? e('span', { className: 'chat-line__timestamp' }, t.chat.formatTime(msg.timestamp))
: null;
const actions = t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this);
if ( is_raw )
notice.ffz_target.unshift(notice.ffz_icon ?? null, timestamp, actions);
else
notice = [
notice.ffz_icon ?? null,
timestamp,
actions,
notice
];
} else {
if ( notice.ffz_icon )
notice = [
notice.ffz_icon,
notice
];
message = e(
'div',
{
className: 'chat-line--inline chat-line__message',
'data-room-id': msg.roomId ?? current_room.id,
'data-room': msg.roomLogin,
'data-user-id': user?.userID,
'data-user': user?.lowerLogin,
},
message
);
}
klass = `${klass} ffz-notice-line user-notice-line tw-pd-y-05`;
if ( ! is_raw )
notice = e('div', {
className: 'tw-c-text-alt-2'
}, notice);
if ( message )
out = [notice, message];
else
out = notice;
} else {
klass = `${klass} chat-line__message`;
out = message;
}
// Check for hover actions, as those require we wrap the output in a few extra elements.
const hover_actions = (user && msg.id)
? t.actions.renderHover(msg, this.props.showModerationIcons, current_user, current_room, e, this)
: null;
if ( hover_actions ) {
klass = `${klass} tw-relative`;
out = [
e('div', {
className: 'chat-line__message-highlight tw-absolute tw-border-radius-medium tw-top-0 tw-bottom-0 tw-right-0 tw-left-0',
'data-test-selector': 'chat-message-highlight'
}),
e('div', {
className: 'chat-line__message-container tw-relative'
}, out),
hover_actions
];
}
// If we don't have an override background color, try to assign
// a value based on the mention.
if (bg_css == null)
bg_css = msg.mentioned && msg.mention_color
? t.parent.inverse_colors.process(msg.mention_color)
: null;
// Now, return the final chat line color.
return e('div', {
className: `${klass}${deleted ? ' ffz--deleted-message' : ''}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
style: {backgroundColor: bg_css},
'data-room-id': msg.roomId ?? current_room.id,
'data-room': msg.roomLogin,
'data-user-id': user?.userID,
'data-user': user?.lowerLogin,
onMouseOver: anim_hover ? t.chat.emotes.animHover : null,
onMouseOut: anim_hover ? t.chat.emotes.animLeave : null
}, out);
} catch(err) {
t.log.error(err);
@ -573,9 +1064,9 @@ export default class ChatLine extends Module {
return 'An error occurred rendering this chat line.';
}
} };*/
} };
cls.prototype.render = function() { try {
cls.prototype.ffzOldRender = function() { try {
this._ffz_no_scan = true;
const types = t.parent.message_types || {},
@ -1123,15 +1614,6 @@ other {# messages were deleted by a moderator.}
return null;
if ( twitch_clickable ) {
let icon, title;
if ( can_reply ) {
icon = e('figure', {className: 'ffz-i-reply'});
title = t.i18n.t('chat.actions.reply', 'Reply to Message');
} else {
icon = e('figure', {className: 'ffz-i-threads'});
title = t.i18n.t('chat.actions.reply.thread', 'Open Thread');
}
out = [
e('div', {
className: 'chat-line__message-highlight tw-absolute tw-border-radius-medium tw-top-0 tw-bottom-0 tw-right-0 tw-left-0',
@ -1182,6 +1664,10 @@ other {# messages were deleted by a moderator.}
}
} }
cls.prototype.render = this.experiments.get('line_renderer')
? cls.prototype.ffzNewRender
: cls.prototype.ffzOldRender;
// Do this after a short delay to hopefully reduce the chance of React
// freaking out on us.
setTimeout(() => this.ChatLine.forceUpdate());