1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: Support for combined emoji using a workaround that compensates for Twitch chat eating unicode it shouldn't. (See #1147 for more details)
* Added: Setting to change how emotes are sorted when using tab completion. Applies to the In-Line Tab Completion add-on as well as the default tab completion.
* Added: Setting to disable a browser's "automatic dark theme" features when supported. This primarily applies to Chromium browsers.
* Added: Ability to make emoji larger in addition to emotes using the "Larger Emotes" setting.
* Fixed: Better handling of community introduction messages.
* Fixed: Catch more chat line rendering errors to prevent chat from breaking entirely.
* Fixed: Chat on Videos not appearing with the chat background color.
* API Changed: Rich Tokens now support `ref` tokens in `header` tokens.
* API Changed: Rich Tokens now support `link` tokens with no content, automatically setting their content to their URLs.
This commit is contained in:
SirStendec 2022-02-11 15:17:32 -05:00
parent feae6bcb89
commit 969ed29668
11 changed files with 360 additions and 81 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.31.6",
"version": "4.32.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",

View file

@ -47,7 +47,9 @@ export const SKIN_TONES = {
4: '1f3fe',
5: '1f3ff'
};
export const JOINER_REPLACEMENT = /(?<!\u{E0002})\u{E0002}/gu;
export const ZWD_REPLACEMENT = /\u{200D}/gu;
export const EMOJI_JOINER = '\u{E0002}';
export const IMAGE_PATHS = {
@ -78,6 +80,21 @@ export default class Emoji extends Module {
this.inject('..emotes');
this.inject('settings');
this.settings.add('chat.emoji.replace-joiner', {
default: 2,
ui: {
path: 'Chat > Behavior >> Emoji',
title: 'Emoji Joiner Workaround',
description: 'This feature is intended to allow the use of combined emoji in supported clients. This is required due to a bug in TMI that strips ZWJ characters from chat messages. [Visit the original issue](https://github.com/FrankerFaceZ/FrankerFaceZ/issues/1147) for more details.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Display Only'},
{value: 2, title: 'Display and Send'}
]
}
});
this.settings.add('chat.emoji.style', {
default: 'twitter',
process(ctx, val) {
@ -110,6 +127,13 @@ export default class Emoji extends Module {
}
onEnable() {
this.on('chat:pre-send-message', event => {
if (event.context.get('chat.emoji.replace-joiner') < 2)
return;
event.message = event.message.replace(ZWD_REPLACEMENT, EMOJI_JOINER);
});
this.loadEmojiData();
}

View file

@ -103,12 +103,22 @@ export default class Emotes extends Module {
});
this.settings.add('chat.emotes.2x', {
default: false,
default: 0,
process(ctx, val) {
if ( val === true ) return 1;
else if ( val === false ) return 0;
return val;
},
ui: {
path: 'Chat > Appearance >> Emotes',
title: 'Larger Emotes',
description: 'This setting will make emotes appear twice as large in chat. It\'s good for use with larger fonts or just if you really like emotes.',
component: 'setting-check-box'
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Emotes'},
{value: 2, title: 'Emotes and Emoji'}
]
}
});

View file

@ -1431,7 +1431,7 @@ export const AddonEmotes = {
if ( ! emotes )
return;
const big = this.context.get('chat.emotes.2x'),
const big = this.context.get('chat.emotes.2x') > 0,
anim = this.context.get('chat.emotes.animated'),
out = [];
@ -1531,6 +1531,7 @@ export const Emoji = {
return;
const splitter = this.emoji.splitter,
replace = this.context.get('chat.emoji.replace-joiner') > 0,
style = this.context.get('chat.emoji.style');
if ( style === 0 )
@ -1547,7 +1548,9 @@ export const Emoji = {
continue;
}
const text = token.text.replace(JOINER_REPLACEMENT, "\u200d");
const text = replace ?
token.text.replace(JOINER_REPLACEMENT, "\u200d") :
token.text;
splitter.lastIndex = 0;
let idx = 0, match;
@ -1610,7 +1613,7 @@ export const TwitchEmotes = {
const data = msg.ffz_emotes,
anim = this.context.get('chat.emotes.animated'),
big = this.context.get('chat.emotes.2x'),
big = this.context.get('chat.emotes.2x') > 0,
use_replacements = this.context.get('chat.fix-bad-emotes'),
emotes = [];

View file

@ -139,7 +139,9 @@ const CHAT_TYPES = make_enum(
'ChannelPointsReward',
'CommunityChallengeContribution',
'CelebrationPurchase',
'LiveMessageSeparator'
'LiveMessageSeparator',
'RestrictedLowTrustUserMessage',
'CommunityIntroduction'
);
@ -292,6 +294,7 @@ export default class ChatHook extends Module {
data: () => Object
.keys(this.chat_types)
.filter(key => ! UNBLOCKABLE_TYPES.includes(key) && ! /^\d+$/.test(key))
.sort()
}
});
@ -887,33 +890,21 @@ export default class ChatHook extends Module {
this.CalloutSelector.forceUpdate();
}, this);
this.chat.context.getChanges('chat.emotes.2x', val =>
this.css_tweaks.toggle('big-emoji', val > 1));
this.chat.context.getChanges('chat.input.show-mod-view', val =>
this.css_tweaks.toggleHide('mod-view', ! val));
/*this.chat.context.on('changed:chat.input.show-mod-view', val => this.css_tweaks.toggleHide('mod-view', ! val));
this.css_tweaks.toggleHide('mod-view', ! this.chat.context.get('chat.input.show-mod-view'));*/
this.chat.context.getChanges('chat.lines.padding', val =>
this.css_tweaks.toggle('chat-padding', val));
/*this.chat.context.on('changed:chat.lines.padding', val =>
this.css_tweaks.toggle('chat-padding', val));
this.css_tweaks.toggle('chat-padding', this.chat.context.get('chat.lines.padding'));*/
this.chat.context.getChanges('chat.bits.show', val =>
this.css_tweaks.toggle('hide-bits', !val));
/*this.chat.context.on('changed:chat.bits.show', val =>
this.css_tweaks.toggle('hide-bits', !val));
this.css_tweaks.toggle('hide-bits', !this.chat.context.get('chat.bits.show'));*/
this.chat.context.getChanges('chat.bits.show-pinned', val =>
this.css_tweaks.toggleHide('pinned-cheer', !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.chat.context.getChanges('chat.filtering.deleted-style', val => {
this.css_tweaks.toggle('chat-deleted-strike', val === 1 || val === 2);
this.css_tweaks.toggle('chat-deleted-fade', val < 2);
@ -922,35 +913,17 @@ export default class ChatHook extends Module {
this.chat.context.getChanges('chat.filtering.clickable-mentions', val =>
this.css_tweaks.toggle('clickable-mentions', val));
/*this.chat.context.on('changed:chat.filtering.clickable-mentions', val =>
this.css_tweaks.toggle('clickable-mentions', val));
this.css_tweaks.toggle('clickable-mentions', this.chat.context.get('chat.filtering.clickable-mentions'));*/
this.chat.context.getChanges('chat.filtering.bold-mentions', val =>
this.css_tweaks.toggle('chat-mention-no-bold', ! val));
/*this.chat.context.on('changed:chat.filtering.bold-mentions', val =>
this.css_tweaks.toggle('chat-mention-no-bold', ! val));
this.css_tweaks.toggle('chat-mention-no-bold', ! this.chat.context.get('chat.filtering.bold-mentions'));*/
this.chat.context.getChanges('chat.hide-community-highlights', val =>
this.css_tweaks.toggleHide('community-highlights', val));
/*this.chat.context.on('changed:chat.hide-community-highlights', val =>
this.css_tweaks.toggleHide('community-highlights', val));
this.css_tweaks.toggleHide('community-highlights', this.chat.context.get('chat.hide-community-highlights'));*/
this.chat.context.getChanges('chat.lines.alternate', val => {
this.css_tweaks.toggle('chat-rows', val);
this.updateMentionCSS();
});
/*this.chat.context.on('changed:chat.lines.alternate', val => {
this.css_tweaks.toggle('chat-rows', val);
this.updateMentionCSS();
});
this.css_tweaks.toggle('chat-rows', this.chat.context.get('chat.lines.alternate'));*/
this.updateChatCSS();
this.updateColors();
this.updateLineBorders();
@ -1534,30 +1507,6 @@ export default class ChatHook extends Module {
if ( msg.type === types.RewardGift && ! t.chat.context.get('chat.bits.show-rewards') )
return;
if ( msg.type === types.CommunityIntroduction ) {
// TODO: Make this better.
msg = {
type: types.Message,
badgeDynamicData: {},
badges: {},
id: msg.id,
isFirstMsg: true,
message: msg.message,
messageBody: msg.message,
messageParts: [
{type: 0, content: msg.message}
],
messageType: 0,
channel: msg.channel,
timestamp: new Date(),
user: {
userDisplayName: msg.displayName,
userLogin: msg.login,
userID: msg.userID
}
};
}
if ( msg.type === types.Message ) {
const m = t.chat.standardizeMessage(msg),
cont = inst._ffz_connector ?? inst.ffzGetConnector();
@ -2082,6 +2031,7 @@ export default class ChatHook extends Module {
const event = new FFZEvent({
message: msg,
extra,
context: t.chat.context,
channel: inst.props.channelLogin,
addMessage,
sendMessage
@ -2281,14 +2231,21 @@ export default class ChatHook extends Module {
}
}
/*this.onCommunityIntroductionEvent = function(e) {
const old_communityintro = this.onCommunityIntroductionEvent;
this.onCommunityIntroductionEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('CommunityIntroduction') ) {
const out = i.convertMessage(e);
return i.postMessageToCurrentChannel(e, out);
}
} catch(err) {
t.log.capture(err, {extra: e});
t.log.error(err);
}
}*/
return old_communityintro.call(this, e);
}
const old_anonsubgift = this.onAnonSubscriptionGiftEvent;
this.onAnonSubscriptionGiftEvent = function(e) {
@ -2487,7 +2444,7 @@ export default class ChatHook extends Module {
// For certain message types, the message is contained within
// a message sub-object.
if ( message.type === t.chat_types.ChannelPointsReward )
if ( message.type === t.chat_types.ChannelPointsReward || message.type === t.chat_types.CommunityIntroduction || (message.message?.user & message.message?.badgeDynamicData) )
message = message.message;
if ( original.channel ) {

View file

@ -12,8 +12,6 @@ import { TWITCH_POINTS_SETS, TWITCH_GLOBAL_SETS, TWITCH_PRIME_SETS, KNOWN_CODES,
import Twilight from 'site';
import {EMOJI_JOINER} from 'src/modules/chat/emoji';
// Prefer using these statically-allocated collators to String.localeCompare
const locale = Intl.Collator();
const localeCaseInsensitive = Intl.Collator(undefined, {sensitivity: 'accent'});
@ -509,9 +507,6 @@ export default class Input extends Module {
t.log.error(err);
}
// replace ZERO WIDTH JOINER with custom joiner so combined emoji work
inst.autocompleteInputRef.setValue(inst.chatInputRef.value.replace('\u{200d}', EMOJI_JOINER));
originalOnMessageSend.call(this, event);
}
}

View file

@ -37,6 +37,114 @@ export default class ChatLine extends Module {
this.inject('chat.actions');
this.inject('chat.overrides');
/*this.line_types = {};
this.line_types.sub_mystery = (msg, u, r, inst, e) => {
const mystery = msg.mystery;
if ( mystery )
mystery.line = this;
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
}, 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);
}
const extra_ts = this.chat.context.get('chat.extra-timestamps');
return inst.ffzDrawLine(
msg,
`ffz-notice-line user-notice-line tw-pd-y-05 ffz--subscribe-line`,
[
e('div', {
className: 'tw-flex 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
]),
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
);
}*/
this.ChatLine = this.fine.define(
'chat-line',
n => n.renderMessageBody && n.props && ! n.onExtensionNameClick && !has(n.props, 'hasModPermissions'),
@ -316,6 +424,139 @@ 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 {
this._ffz_no_scan = true;
const msg = t.chat.standardizeMessage(this.props.message),
reply_mode = t.chat.context.get('chat.replies.style');
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;
}
const u = t.site.getUser(),
r = {id: room_id, login: room};
const 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);
if ( u ) {
u.moderator = this.props.isCurrentUserModerator;
u.staff = this.props.isCurrentUserStaff;
u.can_reply = reply_mode === 2 && can_reply;
}
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 = new FFZEvent({
inst: this,
event,
message: msg,
user: target_user,
room: r
});
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);
}
// 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);
return this.ffz_old_render();
} 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 = function() { try {
this._ffz_no_scan = true;
@ -932,7 +1173,19 @@ other {# messages were deleted by a moderator.}
}
});
return old_render.call(this);
try {
return old_render.call(this);
} catch(e2) {
t.log.error('An error in Twitch rendering.', e2);
t.log.capture(e2, {
extra: {
props: this.props
}
});
return 'An error occurred rendering this chat line.';
}
} }
// Do this after a short delay to hopefully reduce the chance of React

View file

@ -19,6 +19,8 @@ const COLORS = [
0.08, 0.151, 0.212, 0.271, 0.31, 0.351 // 10-15
];
const NO_AUTO_DARK = `:root{color-scheme: only light}`;
const ACCENT_COLORS = {
//dark: {'c':{'accent': 9,'background-accent':8,'background-accent-alt':7,'background-accent-alt-2':6,'background-button':7,'background-button-active':7,'background-button-focus':8,'background-button-hover':8,'background-button-primary-active':7,'background-button-primary-default':9,'background-button-primary-hover':8,'background-graph':2,'background-graph-fill':8,'background-input-checkbox-checked':9,'background-input-checked':8,'background-interactable-active':9,'background-interactable-selected':9,'background-interactable-hover':8,'background-progress-countdown-status':9,'background-progress-status':9,'background-range-fill':9,'background-subscriber-stream-tag-active':4,'background-subscriber-stream-tag-default':4,'background-subscriber-stream-tag-hover':3,'background-toggle-checked':9,/*'background-tooltip':1,*/'background-top-nav':6,'border-brand':9,'border-button':7,'border-button-active':8,'border-button-focus':9,'border-button-hover':8,'border-input-checkbox-checked':9,'border-input-checkbox-focus':9,'border-input-focus':9,'border-interactable-selected':10,'border-subscriber-stream-tag':5,'border-tab-active':11,'border-tab-focus':11,'border-tab-hover':11,'border-toggle-focus':7,'border-toggle-hover':7,'border-whisper-incoming':10,'fill-brand':9,'text-button-text':10,'text-button-text-focus':'o1','text-button-text-hover':'o1','text-link':10,'text-link-active':10,'text-link-focus':10,'text-link-hover':10,'text-link-visited':10,'text-overlay-link-active':13,'text-overlay-link-focus':13,'text-overlay-link-hover':13,'text-tab-active':11,'background-chat':1,'background-chat-alt':3,'background-chat-header':2,'background-modal':3,'text-button-text-active':'o2'/*,'text-tooltip':1*/},'s':{'button-active':[8,'0 0 6px 0',''],'button-focus':[8,'0 0 6px 0',''],'input-focus':[8,'0 0 10px -2px',''],'interactable-focus':[8,'0 0 6px 0',''],'tab-focus':[11,'0 4px 6px -4px',''],'input':[5,'inset 0 0 0 1px','']}},
@ -44,6 +46,21 @@ export default class ThemeEngine extends Module {
this.should_enable = true;
// Tweaks
this.settings.add('theme.disable-auto-dark', {
default: false,
ui: {
path: 'Appearance > Theme >> Tweaks @{"sort": 10}',
title: 'Disable Auto Dark Theme',
component: 'setting-check-box',
description: 'Enabling this will attempt to disable a browser\'s built-in automatic dark theme conversion for websites, which is unnecessary on Twitch and can cause display issues. This may not apply depending on your web browser and settings.'
},
changed: () => this.updateCSS()
});
// Font
this.settings.add('theme.font.size', {
@ -321,6 +338,11 @@ export default class ThemeEngine extends Module {
updateCSS() {
//this.updateOldCSS();
if ( this.settings.get('theme.disable-auto-dark') )
this.css_tweaks.set('nodark', NO_AUTO_DARK);
else
this.css_tweaks.delete('nodark');
this.css_tweaks.setVariable('border-color', 'var(--color-border-base)');
if ( ! this.settings.get('theme.can-dark') ) {

View file

@ -17,6 +17,7 @@
.ach-card--expanded .ach-card__inner,
.room-upsell,
.chat-room,
.video-chat,
.qa-vod-chat,
.video-card {
background-color: var(--color-background-base) !important;

View file

@ -59,6 +59,7 @@ export const UPDATE_TOKEN_SETTINGS = [
'chat.emotes.2x',
'chat.emotes.animated',
'chat.emoji.style',
'chat.emoji.replace-joiner',
'chat.bits.stack',
'chat.rich.enabled',
'chat.rich.want-mid',

View file

@ -249,9 +249,17 @@ export default renderTokens;
// Token Type: Reference
// ============================================================================
function resolveToken(token, ctx) {
if ( token?.type === 'ref' ) {
return ctx.fragments?.[token.name] ?? null;
}
return token;
}
TOKEN_TYPES.ref = function(token, createElement, ctx) {
const frag = ctx.fragments?.[token.name];
if (frag )
if ( frag )
return renderTokens(frag, createElement, ctx);
}
@ -561,9 +569,10 @@ function header_vue(token, h, ctx) {
]
}, content);
let imtok = token.sfw_image;
if ( token.image && canShowImage(token.image, ctx) )
imtok = token.image;
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
imtok = nsfw_token;
if ( imtok ) {
const aspect = imtok.aspect;
@ -651,9 +660,10 @@ function header_normal(token, createElement, ctx) {
className: `tw-flex tw-full-width tw-overflow-hidden ${token.compact ? 'ffz--rich-header ffz--compact-header tw-align-items-center' : 'tw-justify-content-center tw-flex-column tw-flex-grow-1'}`
}, content);
let imtok = token.sfw_image;
if ( token.image && canShowImage(token.image, ctx) )
imtok = token.image;
let imtok = resolveToken(token.sfw_image, ctx);
const nsfw_token = resolveToken(token.image, ctx);
if ( nsfw_token && canShowImage(nsfw_token, ctx) )
imtok = nsfw_token;
if ( imtok ) {
const aspect = imtok.aspect;
@ -839,6 +849,9 @@ TOKEN_TYPES.i18n = function(token, createElement, ctx) {
// ============================================================================
TOKEN_TYPES.link = function(token, createElement, ctx) {
if ( token.content === undefined )
token.content = token.url;
const content = renderTokens(token.content, createElement, ctx, token.markdown);
const klass = [];