mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-22 05:50:54 +00:00
* Fixed: Issue with chat disappearing after a recent Twitch update. * Fixed: Various font size issues.
1652 lines
49 KiB
JavaScript
1652 lines
49 KiB
JavaScript
'use strict';
|
|
|
|
// ============================================================================
|
|
// Chat Line
|
|
// ============================================================================
|
|
|
|
import Twilight from 'site';
|
|
import Module from 'utilities/module';
|
|
|
|
import RichContent from './rich_content';
|
|
import { has } from 'utilities/object';
|
|
import { KEYS, RERENDER_SETTINGS, UPDATE_BADGE_SETTINGS, UPDATE_TOKEN_SETTINGS } from 'utilities/constants';
|
|
import { print_duration } from 'utilities/time';
|
|
|
|
import { getRewardTitle, getRewardCost, isGiantEmoteReward, doesRewardCostBits, isMessageEffect } from './points';
|
|
import awaitMD, {getMD} from 'utilities/markdown';
|
|
|
|
const SUB_TIERS = {
|
|
1000: 1,
|
|
2000: 2,
|
|
3000: 3
|
|
};
|
|
|
|
|
|
export default class ChatLine extends Module {
|
|
constructor(...args) {
|
|
super(...args);
|
|
|
|
this.inject('settings');
|
|
this.inject('i18n');
|
|
this.inject('chat');
|
|
this.inject('site');
|
|
this.inject('site.fine');
|
|
this.inject(RichContent);
|
|
this.inject('experiments');
|
|
|
|
this.inject('chat.actions');
|
|
this.inject('chat.overrides');
|
|
this.inject('chat.emotes');
|
|
|
|
this.line_types = {};
|
|
|
|
this.line_types.unknown = {
|
|
renderNotice: (msg, current_user, room, inst, e) => {
|
|
return `Unknown message type: ${msg.ffz_type}`
|
|
}
|
|
};
|
|
|
|
this.line_types.notice = {
|
|
renderNotice: (msg, current_user, room, inst, e) => {
|
|
const data = msg.ffz_data;
|
|
let content = this.line_types.notice.renderContent(msg, current_user, room, inst, e);
|
|
|
|
if ( ! data.icon )
|
|
return content;
|
|
|
|
if ( typeof content === 'string' )
|
|
content = e('span', {}, content);
|
|
|
|
if ( typeof data.icon === 'function' ) {
|
|
try {
|
|
content.ffz_icon = data.icon(data, inst, e);
|
|
} catch(err) {
|
|
this.log.capture(err);
|
|
this.log.error('Error using custom renderer for notice:', err);
|
|
}
|
|
|
|
} else if ( data.icon instanceof URL )
|
|
content.ffz_icon = e('img', {
|
|
className: 'ffz-notice-icon tw-mg-r-05',
|
|
src: data.icon.toString()
|
|
});
|
|
|
|
else
|
|
content.ffz_icon = e('span', {
|
|
className: `${data.icon} tw-mg-r-05`
|
|
});
|
|
|
|
return content;
|
|
},
|
|
|
|
renderContent: (msg, current_user, room, inst, e) => {
|
|
const data = msg.ffz_data;
|
|
if ( data.renderer )
|
|
try {
|
|
return data.renderer(data, inst, e);
|
|
} catch(err) {
|
|
this.log.capture(err);
|
|
this.log.error('Error using custom renderer for notice:', err);
|
|
return `Error rendering notice.`
|
|
}
|
|
|
|
const text = data.i18n ? this.i18n.t(data.i18n, data.messgae, data) : data.message;
|
|
|
|
if ( data.markdown ) {
|
|
const md = getMD();
|
|
if ( ! md ) {
|
|
awaitMD().then(() => inst.forceUpdate());
|
|
return 'Loading...';
|
|
}
|
|
|
|
return e('span', {
|
|
dangerouslySetInnerHTML: {
|
|
__html: getMD().renderInline(text)
|
|
}
|
|
});
|
|
}
|
|
|
|
if ( data.tokenize ) {
|
|
const tokens = data.ffz_tokens = data.ffz_tokens || this.chat.tokenizeMessage({
|
|
badges: {},
|
|
message: text,
|
|
id: msg.id,
|
|
user: msg.user,
|
|
roomLogin: msg.roomLogin,
|
|
roomID: msg.roomID
|
|
});
|
|
|
|
return this.chat.renderTokens(tokens, e);
|
|
}
|
|
|
|
return text;
|
|
}
|
|
};
|
|
|
|
this.line_types.hype = {
|
|
renderNotice: (msg, current_user, room, inst, e) => {
|
|
const setting = this.chat.context.get('chat.hype.message-style');
|
|
if ( setting === 0 )
|
|
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;
|
|
|
|
let amount = msg.hype_amount;
|
|
const digits = msg.hype_exponent ?? 0;
|
|
if ( digits > 0 )
|
|
amount /= Math.pow(10, digits);
|
|
|
|
try {
|
|
// TODO: Cache formatter?
|
|
const fmt = new Intl.NumberFormat(navigator.languages, {
|
|
style: 'currency',
|
|
currency: msg.hype_currency,
|
|
compactDisplay: 'short',
|
|
minimumFractionDigits: digits,
|
|
maximumFractionDigits: digits
|
|
});
|
|
|
|
amount = fmt.format(amount);
|
|
|
|
} catch(err) {
|
|
amount = `${msg.hype_currency} ${amount}`;
|
|
}
|
|
|
|
if (! has_message)
|
|
return this.i18n.tList('chat.hype-chat.user', '{user} sent a Hype Chat for {amount}!', {
|
|
amount,
|
|
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.hype-chat',
|
|
'Sent a Hype Chat for {amount}!',
|
|
{
|
|
amount
|
|
}
|
|
)
|
|
}
|
|
};
|
|
|
|
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
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
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,
|
|
is_bits = doesRewardCostBits(msg.ffz_reward),
|
|
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: is_bits ? 'ffz-i-bits' : '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');
|
|
|
|
let has_message;
|
|
if (setting === 1 && months > 1) {
|
|
const tokens = msg.ffz_tokens = msg.ffz_tokens || this.chat.tokenizeMessage(msg, current_user);
|
|
has_message = tokens.length > 0;
|
|
}
|
|
|
|
if ( !(setting === 3 || (setting === 1 && has_message && months > 1) || (setting === 2 && months > 1)) )
|
|
return null;
|
|
|
|
const user = msg.user,
|
|
plan = msg.sub_plan || {},
|
|
tier = SUB_TIERS[plan.plan] || 1,
|
|
multi = msg.sub_multi,
|
|
|
|
has_multi = (multi?.count ?? 0) > 1 && multi.tenure === 0;
|
|
|
|
const sub_msg = this.i18n.tList(
|
|
`chat.sub.main${has_multi ? '-multi' : ''}`,
|
|
`{user} subscribed {plan}${has_multi ? ' for {multi, plural, one {# month} other {# months}} in advance' : ''}. `,
|
|
{
|
|
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)),
|
|
plan: plan.prime ?
|
|
this.i18n.t('chat.sub.twitch-prime', 'with Prime Gaming') :
|
|
this.i18n.t('chat.sub.plan', 'at Tier {tier}', {tier}),
|
|
multi: has_multi
|
|
? multi.count
|
|
: 1
|
|
}
|
|
);
|
|
|
|
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
|
|
}
|
|
));
|
|
|
|
} 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))
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
this.line_types.sub_gift = {
|
|
getClass: () => 'ffz--subscribe-line',
|
|
|
|
renderNotice: (msg, current_user, room, inst, e) => {
|
|
const user = msg.user,
|
|
|
|
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: 'chatter-name',
|
|
onClick: inst.ffz_user_click_handler,
|
|
'data-user': JSON.stringify(msg.sub_recipient)
|
|
}, e('span', {
|
|
className: 'tw-c-text-base tw-strong'
|
|
}, 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, source) => {
|
|
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: source?.displayName || source?.login || 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);
|
|
}
|
|
|
|
const target = [
|
|
sub_msg
|
|
];
|
|
|
|
if ( mystery )
|
|
target.push(e('span', {
|
|
className: `tw-pd-l-05 ffz-font-size-4 ffz-i-${expanded ? 'down' : 'right'}-dir`
|
|
}));
|
|
|
|
const out = [
|
|
e('div', {
|
|
className: 'tw-full-width tw-c-text-alt-2',
|
|
onClick: inst.ffz_click_expand
|
|
}, target),
|
|
sub_list
|
|
];
|
|
|
|
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',
|
|
n => n.renderMessageBody && n.props && ! n.onExtensionNameClick && !has(n.props, 'hasModPermissions'),
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.ExtensionLine = this.fine.define(
|
|
'extension-line',
|
|
n => n.renderMessageBody && n.onExtensionNameClick,
|
|
Twilight.CHAT_ROUTES
|
|
);
|
|
|
|
this.WhisperLine = this.fine.define(
|
|
'whisper-line',
|
|
n => n.props && n.props.message && has(n.props, 'reportOutgoingWhisperRendered')
|
|
)
|
|
}
|
|
|
|
async onEnable() {
|
|
this.on('chat.overrides:changed', id => this.updateLinesByUser(id, null, false, false), this);
|
|
this.on('chat:update-lines-by-user', this.updateLinesByUser, this);
|
|
this.on('chat:update-line', this.updateLineById, this);
|
|
this.on('chat:update-lines', this.updateLines, this);
|
|
this.on('chat:rerender-lines', this.rerenderLines, this);
|
|
this.on('chat:update-line-tokens', this.updateLineTokens, this);
|
|
this.on('chat:update-line-badges', this.updateLineBadges, this);
|
|
this.on('i18n:update', this.rerenderLines, this);
|
|
this.on('chat.emotes:update-effects', this.checkEffects, this);
|
|
|
|
this.can_reprocess = true;
|
|
|
|
this.on('chat:reload-data', () => this.can_reprocess = true);
|
|
this.on('chat:room-add', () => this.can_reprocess = true);
|
|
|
|
this.on('load_tracker:complete:chat-data', () => {
|
|
const val = this.chat.context.get('chat.update-when-loaded');
|
|
if ( ! val || ! this.can_reprocess )
|
|
return;
|
|
|
|
this.can_reprocess = false;
|
|
this.log.info('Reprocessing chat lines due to data loads.');
|
|
this.updateLines();
|
|
});
|
|
|
|
for(const setting of RERENDER_SETTINGS)
|
|
this.chat.context.on(`changed:${setting}`, this.rerenderLines, this);
|
|
|
|
for(const setting of UPDATE_TOKEN_SETTINGS)
|
|
this.chat.context.on(`changed:${setting}`, this.updateLineTokens, this);
|
|
|
|
for(const setting of UPDATE_BADGE_SETTINGS)
|
|
this.chat.context.on(`changed:${setting}`, this.updateLineBadges, this);
|
|
|
|
this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this);
|
|
this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this);
|
|
|
|
this.on('chat:get-messages', (include_chat, include_whisper, include_video, messages) => {
|
|
if ( include_chat ) {
|
|
for(const inst of this.ChatLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg )
|
|
messages.push({
|
|
message: msg,
|
|
_instance: inst,
|
|
update: () => inst.forceUpdate()
|
|
});
|
|
}
|
|
|
|
for(const inst of this.ExtensionLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg )
|
|
messages.push({
|
|
message: msg,
|
|
_instance: inst,
|
|
update: () => inst.forceUpdate()
|
|
});
|
|
}
|
|
}
|
|
|
|
if ( include_whisper )
|
|
for(const inst of this.WhisperLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg && msg._ffz_message )
|
|
messages.push({
|
|
message: msg._ffz_message,
|
|
_instance: inst,
|
|
update: () => inst.forceUpdate()
|
|
});
|
|
}
|
|
});
|
|
|
|
this.on('chat:get-tab-commands', e => {
|
|
if ( this.experiments.getTwitchAssignmentByName('chat_replies') === 'control' )
|
|
return;
|
|
|
|
e.commands.push({
|
|
name: 'reply',
|
|
description: 'Reply to a user\'s last message.',
|
|
permissionLevel: 0,
|
|
ffz_group: 'FrankerFaceZ',
|
|
commandArgs: [
|
|
{name: 'username', isRequired: true},
|
|
{name: 'message', isRequired: false}
|
|
]
|
|
})
|
|
});
|
|
|
|
this.on('chat:pre-send-message', e => {
|
|
if ( this.experiments.getTwitchAssignmentByName('chat_replies') === 'control' )
|
|
return;
|
|
|
|
const msg = e.message,
|
|
types = this.parent.chat_types || {};
|
|
|
|
let user, message;
|
|
if ( /^\/reply ?/i.test(msg) )
|
|
user = msg.slice(7).trim();
|
|
else
|
|
return;
|
|
|
|
e.preventDefault();
|
|
|
|
const idx = user.indexOf(' ');
|
|
if ( idx !== -1 ) {
|
|
message = user.slice(idx + 1);
|
|
user = user.slice(0, idx);
|
|
}
|
|
|
|
if ( user.startsWith('@') )
|
|
user = user.slice(1);
|
|
|
|
if ( user && user.length ) {
|
|
user = user.toLowerCase();
|
|
|
|
const lines = Array.from(this.ChatLine.instances);
|
|
let i = lines.length;
|
|
while(i--) {
|
|
const line = lines[i],
|
|
msg = line?.props?.message,
|
|
u = msg?.user;
|
|
|
|
if ( ! u )
|
|
continue;
|
|
|
|
if ( u.login === user || u.displayName?.toLowerCase?.() === user ) {
|
|
if ( message ) {
|
|
e.sendMessage(message, {
|
|
reply: {
|
|
parentDeleted: msg.deleted || false,
|
|
parentDisplayName: u.displayName,
|
|
parentMessageBody: msg.message,
|
|
parentMsgId: msg.id,
|
|
parentUid: u.id,
|
|
parentUserLogin: u.login
|
|
}
|
|
});
|
|
} else
|
|
requestAnimationFrame(() => line.ffzOpenReply());
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
e.addMessage({
|
|
type: types.Notice,
|
|
message: this.i18n.t('chat.reply.bad-user', 'Invalid user or no known message to reply to.')
|
|
});
|
|
});
|
|
|
|
const t = this,
|
|
React = await this.site.findReact();
|
|
if ( ! React )
|
|
return;
|
|
|
|
const e = React.createElement,
|
|
FFZRichContent = this.rich_content && this.rich_content.RichContent;
|
|
|
|
|
|
this.WhisperLine.ready(cls => {
|
|
const old_render = cls.prototype.render;
|
|
|
|
cls.prototype.render = function() {
|
|
this._ffz_no_scan = true;
|
|
|
|
if ( ! this.props.message || ! this.props.message.content || ! this.props.message.from )
|
|
return old_render.call(this);
|
|
|
|
try {
|
|
const msg = t.chat.standardizeWhisper(this.props.message),
|
|
|
|
is_action = msg.is_action,
|
|
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,
|
|
user = msg.user,
|
|
raw_color = t.overrides.getColor(user.id) || user.color,
|
|
color = t.parent.colors.process(raw_color),
|
|
|
|
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, null),
|
|
contents = t.chat.renderTokens(tokens, e),
|
|
|
|
override_name = t.overrides.getName(user.id);
|
|
|
|
return e('div', {className: 'thread-message__message'},
|
|
e('div', {className: 'tw-pd-x-1 tw-pd-y-05'}, [
|
|
e('span', {
|
|
className: `thread-message__message--user-name notranslate${override_name ? ' ffz--name-override' : ''}`,
|
|
style: {
|
|
color
|
|
}
|
|
}, override_name || user.displayName),
|
|
e('span', null, is_action ? ' ' : ': '),
|
|
e('span', {
|
|
className: `message${action_italic ? ' chat-line__message-body--italicized' : ''}`,
|
|
style: {
|
|
color: action_color && color
|
|
}
|
|
}, contents)
|
|
])
|
|
);
|
|
|
|
} catch(err) {
|
|
t.log.error(err);
|
|
t.log.capture(err, {
|
|
extra: {
|
|
props: this.props
|
|
}
|
|
});
|
|
|
|
return old_render.call(this);
|
|
}
|
|
}
|
|
|
|
// Do this after a short delay to hopefully reduce the chance of React
|
|
// freaking out on us.
|
|
setTimeout(() => this.WhisperLine.forceUpdate());
|
|
});
|
|
|
|
this.ChatLine.ready(cls => {
|
|
const old_render = cls.prototype.render;
|
|
|
|
cls.prototype.shouldComponentUpdate = function(props, state) {
|
|
const show = state && 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 && this.state && (state.ffz_expanded !== this.state.ffz_expanded)) ||
|
|
//state.renderDebug !== this.state.renderDebug ||
|
|
props.deletedMessageDisplay !== this.props.deletedMessageDisplay ||
|
|
props.deletedCount !== this.props.deletedCount ||
|
|
props.message !== this.props.message ||
|
|
props.isCurrentUserModerator !== this.props.isCurrentUserModerator ||
|
|
props.showModerationIcons !== this.props.showModerationIcons ||
|
|
props.showTimestamps !== this.props.showTimestamps ||
|
|
(props.pinnedMessage?.message?.id === props.message?.id) !== (this.props.pinnedMessage?.message?.id === this.props.message?.id);
|
|
}
|
|
|
|
cls.prototype.ffzOpenReply = function() {
|
|
if ( this.onMessageClick ) {
|
|
return this.onMessageClick();
|
|
}
|
|
|
|
if ( this.props.reply ) {
|
|
this.setOPCardTray(this.props.reply);
|
|
return;
|
|
}
|
|
|
|
if ( this.props.hasReply ) {
|
|
const msg = this.props.message;
|
|
if (msg?.user) {
|
|
this.setOPCardTray({
|
|
parentMsgId: msg.id,
|
|
parentUid: msg.user.userID,
|
|
parentUserLogin: msg.user.userLogin,
|
|
parentDeleted: msg.deleted,
|
|
parentDisplayName: msg.user.userDisplayName,
|
|
parentMessageBody: msg.messageBody
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
const old_render_author = this.renderMessageAuthor;
|
|
this.renderMessageAuthor = () => this.ffzReplyAuthor();
|
|
|
|
const tokens = this.props.message?.ffz_tokens;
|
|
if ( ! tokens )
|
|
return;
|
|
|
|
this.setMessageTray(this.props.message, t.chat.renderTokens(tokens, e));
|
|
|
|
this.renderMessageAuthor = old_render_author;
|
|
}
|
|
|
|
cls.prototype.ffzReplyAuthor = function() {
|
|
const msg = t.chat.standardizeMessage(this.props.message),
|
|
user = msg.user,
|
|
raw_color = t.overrides.getColor(user.id) || user.color,
|
|
color = t.parent.colors.process(raw_color);
|
|
|
|
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined,
|
|
room_id = msg.roomId ? msg.roomId : this.props.channelID;
|
|
|
|
if ( ! room && room_id ) {
|
|
const r = t.chat.getRoom(room_id, null, true);
|
|
if ( r && r.login )
|
|
room = msg.roomLogin = r.login;
|
|
}
|
|
|
|
if ( ! room_id && room ) {
|
|
const r = t.chat.getRoom(null, room_id, true);
|
|
if ( r && r.id )
|
|
room_id = msg.roomId = r.id;
|
|
}
|
|
|
|
const user_block = t.chat.formatUser(user, e);
|
|
const override_name = t.overrides.getName(user.id);
|
|
|
|
return e('span', {
|
|
'data-room-id': room_id,
|
|
'data-room': room,
|
|
'data-user-id': user.userID,
|
|
'data-user': user.userLogin && user.userLogin.toLowerCase()
|
|
}, [
|
|
//t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
|
|
e('span', {
|
|
className: 'chat-line__message--badges'
|
|
}, t.chat.badges.render(msg, e)),
|
|
e('span', {
|
|
className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''}`,
|
|
role: 'button',
|
|
style: { color },
|
|
onClick: this.ffz_user_click_handler,
|
|
onContextMenu: t.actions.handleUserContext
|
|
}, override_name ? [
|
|
e('span', {
|
|
className: 'chat-author__display-name'
|
|
}, override_name),
|
|
e('div', {
|
|
className: 'ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-center'
|
|
}, user_block)
|
|
] : user_block)
|
|
]);
|
|
}
|
|
|
|
cls.prototype.ffzRenderSharedChatPill = function() {
|
|
let style = t.chat.context.get('chat.shared-chat.style');
|
|
if ( style == null )
|
|
style = this.props.isCurrentUserModerator ? 2 : 0;
|
|
if ( style === 0 )
|
|
return null;
|
|
|
|
const msg = this.props.message,
|
|
source_id = msg.sourceRoomID,
|
|
source = source_id && this.props.sharedChatDataByChannelID?.get(source_id),
|
|
in_source = ! source || source_id === (msg.roomId ?? this.props.channelID);
|
|
|
|
if ( ! source )
|
|
return null;
|
|
|
|
const title = t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName ?? source.login});
|
|
|
|
if ( style === 1 )
|
|
return e('span', {
|
|
className: `ffz-pill ffz-tooltip tw-mg-r-05 ${in_source ? 'ffz-pill--brand' : ''}`,
|
|
'data-title': title
|
|
}, `${source.displayName ?? source.login}`);
|
|
|
|
if ( style === 2 )
|
|
return e('figure', {
|
|
className: `ffz-tooltip ffz-tooltip--no-mouse ffz-shared-chat-badge ffz-avatar tw-border-radius-rounded ${in_source ? 'ffz-shared-chat-badge--active' : ''} tw-mg-r-05`,
|
|
'data-title': t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName}),
|
|
}, e('img', {
|
|
className: 'tw-block tw-border-radius-rounded tw-image',
|
|
src: source.profileImageURL,
|
|
alt: source.displayName
|
|
}));
|
|
|
|
return null;
|
|
}
|
|
|
|
cls.prototype.ffzNewRender = function() { try {
|
|
this._ffz_no_scan = true;
|
|
|
|
const msg = t.chat.standardizeMessage(this.props.message),
|
|
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
|
|
})
|
|
);
|
|
}
|
|
|
|
// First, we need to handle Shared Chat.
|
|
const source_id = msg.sourceRoomID,
|
|
source = source_id && this.props.sharedChatDataByChannelID?.get(source_id);
|
|
|
|
// 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;
|
|
|
|
if ( ! room && room_id ) {
|
|
const r = t.chat.getRoom(room_id, null, true);
|
|
if ( r && r.login )
|
|
room = msg.roomLogin = r.login;
|
|
}
|
|
|
|
if ( ! room_id && room ) {
|
|
const r = t.chat.getRoom(null, room, true);
|
|
if ( r && r.id )
|
|
room_id = msg.roomId = r.id;
|
|
}
|
|
|
|
// Construct the current room and current user objects.
|
|
const current_user = t.site.getUser(),
|
|
current_room = {id: room_id, login: room};
|
|
|
|
const is_in_source = ! source || source_id === room_id,
|
|
source_room = is_in_source
|
|
? current_room
|
|
: {id: source_id, login: source.login};
|
|
|
|
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 && (has_replies || (current_user && current_user.login !== msg.user?.login));
|
|
|
|
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);
|
|
|
|
if ( ! this.ffz_user_click_handler ) {
|
|
if ( this.props.onUsernameClick )
|
|
this.ffz_user_click_handler = event => {
|
|
if ( this.isKeyboardEvent(event) && event.keyCode !== KEYS.Space && event.keyCode !== KEYS.Enter )
|
|
return;
|
|
|
|
const target = event.currentTarget,
|
|
ds = target && target.dataset;
|
|
let target_user = msg.user;
|
|
|
|
if ( ds && ds.user ) {
|
|
try {
|
|
target_user = JSON.parse(ds.user);
|
|
} catch(err) { /* nothing~! */ }
|
|
}
|
|
|
|
const fe = t.makeEvent({
|
|
inst: this,
|
|
event,
|
|
message: msg,
|
|
user: target_user,
|
|
room: current_room,
|
|
source: source_room
|
|
});
|
|
|
|
t.emit('chat:user-click', fe);
|
|
|
|
if ( fe.defaultPrevented )
|
|
return;
|
|
|
|
this.props.onUsernameClick(target_user.login, 'chat_message', msg.id, target.getBoundingClientRect().bottom);
|
|
}
|
|
else
|
|
this.ffz_user_click_handler = this.openViewerCard || this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
|
|
}
|
|
|
|
let notice;
|
|
let klass;
|
|
let bg_css = null;
|
|
|
|
// 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 && msg.ffz_type )
|
|
type = t.line_types.unknown;
|
|
|
|
if ( type ) {
|
|
if ( type.render )
|
|
return type.render(msg, current_user, current_room, this, e, source);
|
|
|
|
if ( type.renderNotice )
|
|
notice = type.renderNotice(msg, current_user, current_room, this, e, source);
|
|
|
|
if ( type.getClass )
|
|
klass = type.getClass(msg, current_user, current_room, this, e, source);
|
|
}
|
|
|
|
// 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})`);
|
|
}
|
|
}
|
|
|
|
// Are we listing highlight reasons?
|
|
let highlight_tags = null;
|
|
const hl_position = t.chat.context.get('chat.filtering.show-reasons');
|
|
|
|
if ( msg.highlights?.size && hl_position ) {
|
|
highlight_tags = [];
|
|
|
|
for(const tag of msg.highlights) {
|
|
const reason = t.chat._hl_reasons[tag];
|
|
let label,
|
|
tooltip;
|
|
|
|
if ( reason ) {
|
|
tooltip = reason.i18n_key
|
|
? t.i18n.t(reason.i18n_key, reason.title)
|
|
: reason.title;
|
|
|
|
label = reason.i18n_label
|
|
? t.i18n.t(reason.i18n_label, reason.label)
|
|
: reason.label;
|
|
|
|
if ( label === tooltip )
|
|
tooltip = null;
|
|
}
|
|
|
|
if ( ! label )
|
|
label = tag;
|
|
|
|
highlight_tags.push(e('span', {
|
|
className: `ffz-pill ffz-highlight-tag${reason ? ' ffz-tooltip' : ''}`,
|
|
'data-title': tooltip
|
|
}, label));
|
|
}
|
|
|
|
if ( highlight_tags.length > 0 )
|
|
highlight_tags = e('span', {
|
|
className: `ffz-highlight-tags ${
|
|
hl_position === 1
|
|
? 'ffz-highlight-tags__above'
|
|
: 'tw-mg-r-05'
|
|
}`
|
|
}, highlight_tags);
|
|
else
|
|
highlight_tags = null;
|
|
}
|
|
|
|
// 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,
|
|
is_giant_emote = this.props.isLastEmoteGigantified || isGiantEmoteReward(msg.ffz_reward),
|
|
|
|
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),
|
|
giant_emote = is_giant_emote && t.chat.pluckLastEmote(tokens, msg);
|
|
|
|
// First, render the user block.
|
|
const username = t.chat.formatUser(user, e),
|
|
override_name = t.overrides.getName(user.id);
|
|
|
|
let user_class = msg.ffz_user_class;
|
|
if ( user_class instanceof Set )
|
|
user_class = [...user_class].join(' ');
|
|
else if ( Array.isArray(user_class) )
|
|
user_class = user_class.join(' ');
|
|
|
|
const want_source_tip = source && t.chat.context.get('chat.shared-chat.username-tooltip');
|
|
|
|
const user_props = {
|
|
className: `chat-line__username notranslate${override_name ? ' ffz--name-override' : ''}${(override_name || want_source_tip) ? ' tw-relative ffz-il-tooltip__container' : ''} ${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-tooltip--down ffz-il-tooltip--align-center'
|
|
}, [
|
|
username,
|
|
want_source_tip
|
|
? e('div', {
|
|
className: 'ffz-font-size-8 tw-mg-t-05'
|
|
}, t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName ?? source.login}))
|
|
: null
|
|
]
|
|
)
|
|
]
|
|
: want_source_tip
|
|
? [
|
|
username,
|
|
e('span', {
|
|
className: 'ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-center'
|
|
}, t.i18n.t('chat.sent-from-source', 'Sent from {source}', {source: source.displayName ?? source.login}))
|
|
]
|
|
: 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'
|
|
? e('div', {className: 'ffz--fix-reply-line'}, this.renderReplyLine())
|
|
: null;
|
|
|
|
// Now assemble the pieces.
|
|
message = [
|
|
twitch_reply,
|
|
|
|
source ? this.ffzRenderSharedChatPill() : null,
|
|
|
|
// The preamble
|
|
timestamp,
|
|
msg.ffz_no_actions ? null : t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this),
|
|
this.renderInlineHighlight ? this.renderInlineHighlight() : null,
|
|
hl_position === 2 ? highlight_tags : 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,
|
|
|
|
// Giant Emote
|
|
giant_emote
|
|
? e('div', {className: 'chat-line__message--ffz-giant-emote'}, t.chat.renderGiantEmote(giant_emote, e))
|
|
: 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 = msg.ffz_no_actions ? null : t.actions.renderInline(msg, this.props.showModerationIcons, current_user, current_room, e, this);
|
|
|
|
if ( is_raw ) {
|
|
notice.ffz_target.unshift(
|
|
source ? this.ffzRenderSharedChatPill() : null,
|
|
notice.ffz_icon ?? null,
|
|
timestamp,
|
|
actions,
|
|
hl_position === 2 ? highlight_tags : null
|
|
);
|
|
|
|
} else
|
|
notice = [
|
|
source ? this.ffzRenderSharedChatPill() : null,
|
|
notice.ffz_icon ?? null,
|
|
timestamp,
|
|
actions,
|
|
hl_position === 2 ? highlight_tags : null,
|
|
notice
|
|
];
|
|
|
|
} else {
|
|
if ( is_raw ) {
|
|
if ( notice.ffz_icon )
|
|
notice.ffz_target.unshift(notice.ffz_icon);
|
|
|
|
} else if ( notice.ffz_icon )
|
|
notice = [
|
|
notice.ffz_icon,
|
|
notice,
|
|
];
|
|
|
|
message = e(
|
|
'div',
|
|
{
|
|
className: 'chat-line--inline chat-line__message',
|
|
'data-room-id': source_room?.id ?? msg.roomId ?? current_room.id,
|
|
'data-room': source_room?.login ?? 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 ( highlight_tags && hl_position === 1 ) {
|
|
out = [highlight_tags, notice, message ?? null];
|
|
} else if ( message )
|
|
out = [notice, message];
|
|
else
|
|
out = notice;
|
|
|
|
} else {
|
|
klass = `${klass} chat-line__message`;
|
|
if ( highlight_tags && hl_position === 1 )
|
|
out = [highlight_tags, message];
|
|
else
|
|
out = message;
|
|
}
|
|
|
|
// Check for hover actions, as those require we wrap the output in a few extra elements.
|
|
const hover_actions = (user && msg.id && ! msg.ffz_no_actions)
|
|
? 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': source_room?.id ?? msg.roomId ?? current_room.id,
|
|
'data-room': source_room?.login ?? 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);
|
|
t.log.capture(err, {
|
|
extra: {
|
|
props: this.props
|
|
}
|
|
});
|
|
|
|
try {
|
|
console.log('trying old 1');
|
|
return old_render.call(this);
|
|
} catch(e2) {
|
|
t.log.error('An error in Twitch shit.', e2);
|
|
t.log.capture(e2, {
|
|
extra: {
|
|
props: this.props
|
|
}
|
|
});
|
|
|
|
return 'An error occurred rendering this chat line.';
|
|
}
|
|
} };
|
|
|
|
cls.prototype.render = cls.prototype.ffzNewRender;
|
|
|
|
// Do this after a short delay to hopefully reduce the chance of React
|
|
// freaking out on us.
|
|
setTimeout(() => this.ChatLine.forceUpdate());
|
|
});
|
|
|
|
this.ExtensionLine.ready(cls => {
|
|
const old_render = cls.prototype.render;
|
|
|
|
cls.prototype.render = function() { try {
|
|
this._ffz_no_scan = true;
|
|
|
|
if ( ! this.props.installedExtensions )
|
|
return null;
|
|
|
|
const msg = t.chat.standardizeMessage(this.props.message),
|
|
ext = msg && msg.extension;
|
|
if( ! ext )
|
|
return null;
|
|
|
|
if ( ! this.props.installedExtensions.some(val => {
|
|
const e = val.extension;
|
|
return e && (e.clientId || e.clientID) === (ext.clientId || ext.clientID) && e.version === ext.version;
|
|
}) )
|
|
return null;
|
|
|
|
const color = t.parent.colors.process(ext.chatColor);
|
|
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined;
|
|
if ( ! room && this.props.channelID ) {
|
|
const r = t.chat.getRoom(this.props.channelID, null, true);
|
|
if ( r && r.login )
|
|
room = msg.roomLogin = r.login;
|
|
}
|
|
|
|
const u = t.site.getUser(),
|
|
r = {id: this.props.channelID, login: room},
|
|
|
|
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u),
|
|
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg),
|
|
bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null;
|
|
|
|
if ( ! tokens.length )
|
|
return null;
|
|
|
|
return e('div', {
|
|
className: `chat-line__message${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
|
|
style: {backgroundColor: bg_css},
|
|
'data-room-id': r.id,
|
|
'data-room': r.login,
|
|
'data-extension': ext.clientID
|
|
}, [
|
|
this.props.showTimestamps && e('span', {
|
|
className: 'chat-line__timestamp'
|
|
}, t.chat.formatTime(msg.timestamp)),
|
|
e('span', {
|
|
className: 'chat-line__message--badges'
|
|
}, t.chat.badges.render(msg, e)),
|
|
e('span', {
|
|
className: 'chat-line__username notranslate',
|
|
role: 'button',
|
|
style: { color },
|
|
onClick: this.onExtensionNameClick
|
|
}, e('span', {
|
|
className: 'chat-author__display-name'
|
|
}, ext.displayName)),
|
|
e('span', null, ': '),
|
|
e('span', {
|
|
className: 'message'
|
|
}, t.chat.renderTokens(tokens, e)),
|
|
rich_content && e(FFZRichContent, rich_content)
|
|
]);
|
|
|
|
} catch(err) {
|
|
t.log.info(err);
|
|
t.log.capture(err, {
|
|
extra: {
|
|
props: this.props
|
|
}
|
|
});
|
|
|
|
return old_render.call(this);
|
|
} }
|
|
|
|
// Do this after a short delay to hopefully reduce the chance of React
|
|
// freaking out on us.
|
|
setTimeout(() => this.ExtensionLine.forceUpdate());
|
|
})
|
|
}
|
|
|
|
|
|
checkEffects() {
|
|
for(const inst of this.ChatLine.instances) {
|
|
const msg = inst.props.message,
|
|
tokens = msg?.ffz_tokens;
|
|
|
|
if ( tokens )
|
|
for(const token of tokens) {
|
|
if ( token.type === 'emote' && token.modifier_flags )
|
|
this.emotes.ensureEffect(token.modifier_flags);
|
|
}
|
|
}
|
|
|
|
for(const inst of this.WhisperLine.instances) {
|
|
const msg = inst.props.message?._ffz_message,
|
|
tokens = msg?.ffz_tokens;
|
|
|
|
if ( tokens )
|
|
for(const token of tokens) {
|
|
if ( token.type === 'emote' && token.modifier_flags )
|
|
this.emotes.ensureEffect(token.modifier_flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
updateLineById(id, clear_tokens = true, clear_badges = null) {
|
|
if ( clear_badges == null )
|
|
clear_badges = clear_tokens;
|
|
|
|
for(const inst of this.ChatLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg?.id === id ) {
|
|
if ( clear_badges )
|
|
msg.ffz_badges = msg.ffz_badge_cache = null;
|
|
|
|
if ( clear_tokens ) {
|
|
msg.ffz_tokens = null;
|
|
msg.ffz_reply = null;
|
|
msg.highlights = msg.mentioned = msg.mention_color = msg.color_priority = null;
|
|
}
|
|
|
|
inst.forceUpdate();
|
|
return;
|
|
}
|
|
}
|
|
|
|
for(const inst of this.WhisperLine.instances) {
|
|
const msg = inst.props.message?._ffz_message;
|
|
if ( msg?.id === id ) {
|
|
// TODO: Better support for clear_tokens and clear_badges
|
|
if ( clear_badges || clear_tokens )
|
|
msg._ffz_message = null;
|
|
|
|
inst.forceUpdate();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateLinesByUser(id, login, clear_tokens = true, clear_badges = true) {
|
|
for(const inst of this.ChatLine.instances) {
|
|
const msg = inst.props.message,
|
|
user = msg?.user;
|
|
if ( user && ((id && id == user.id) || (login && login == user.login)) ) {
|
|
if ( clear_badges )
|
|
msg.ffz_badges = msg.ffz_badge_cache = null;
|
|
|
|
if ( clear_tokens ) {
|
|
msg.ffz_tokens = null;
|
|
msg.ffz_reply = null;
|
|
msg.highlights = msg.mentioned = msg.mention_color = msg.color_priority = null;
|
|
}
|
|
|
|
inst.forceUpdate();
|
|
}
|
|
}
|
|
|
|
for(const inst of this.WhisperLine.instances) {
|
|
const msg = inst.props.message?._ffz_message,
|
|
user = msg?.user;
|
|
if ( user && ((id && id == user.id) || (login && login == user.login)) ) {
|
|
msg._ffz_message = null;
|
|
inst.forceUpdate();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
maybeUpdateLines() {
|
|
if ( this.chat.context.get('chat.rich.all-links') )
|
|
this.updateLines();
|
|
}
|
|
|
|
updateLines() {
|
|
return this._updateLines();
|
|
}
|
|
|
|
rerenderLines() {
|
|
return this._updateLines(false, false);
|
|
}
|
|
|
|
updateLineTokens() {
|
|
return this._updateLines(true, false);
|
|
}
|
|
|
|
updateLineBadges() {
|
|
return this._updateLines(false, true);
|
|
}
|
|
|
|
_updateLines(clear_tokens = true, clear_badges = true) {
|
|
for(const inst of this.ChatLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg ) {
|
|
if ( clear_badges )
|
|
msg.ffz_badge_cache = msg.ffz_badges = null;
|
|
|
|
if ( clear_tokens ) {
|
|
msg.ffz_tokens = null;
|
|
msg.ffz_reply = null;
|
|
msg.highlights = msg.mentioned = msg.mention_color = msg.mention_priority = msg.clear_priority = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(const inst of this.ExtensionLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg ) {
|
|
if ( clear_badges )
|
|
msg.ffz_badge_cache = msg.ffz_badges = null;
|
|
|
|
if ( clear_tokens ) {
|
|
msg.ffz_tokens = null;
|
|
msg.ffz_reply = null;
|
|
msg.highlights = msg.mentioned = msg.mention_color = msg.mention_priority = msg.clear_priority = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(const inst of this.WhisperLine.instances) {
|
|
const msg = inst.props.message;
|
|
if ( msg && msg._ffz_message )
|
|
msg._ffz_message = null;
|
|
}
|
|
|
|
this.ChatLine.forceUpdate();
|
|
this.ExtensionLine.forceUpdate();
|
|
this.WhisperLine.forceUpdate();
|
|
|
|
this.emit('chat:updated-lines');
|
|
}
|
|
}
|