mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.15.4
* Added: Setting to hide the mass gift sub banner at the top of chat. * Changed: Messages for redeeming Channel Points now have custom rendering with less padding and background colors to properly highlight them. * Fixed: Moderation profiles not always applying when navigating between channels. * Fixed: Settings to disable auto-play not working. * Fixed: Remove debug logging when importing a profile.
This commit is contained in:
parent
0fdb988da7
commit
8ac1b2ce91
18 changed files with 481 additions and 44 deletions
153
bin/static_i18n.js
Normal file
153
bin/static_i18n.js
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
const transformSync = require('@babel/core').transformSync;
|
||||||
|
const type = require('@babel/types');
|
||||||
|
const traverse = require('@babel/traverse').default;
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
function matchesPattern(member, match, allowPartial = false) {
|
||||||
|
if ( ! type.isMemberExpression(member) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const parts = Array.isArray(match) ? match : match.split('.');
|
||||||
|
const nodes = [];
|
||||||
|
|
||||||
|
let node;
|
||||||
|
for(node = member; type.isMemberExpression(node); node = node.object)
|
||||||
|
nodes.push(node.property);
|
||||||
|
|
||||||
|
nodes.push(node);
|
||||||
|
|
||||||
|
if ( nodes.length < parts.length )
|
||||||
|
return false;
|
||||||
|
if ( ! allowPartial && nodes.length > parts.length )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for(let i = 0, j = nodes.length - 1; i < parts.length; i++, j--) {
|
||||||
|
const node = nodes[j];
|
||||||
|
let value;
|
||||||
|
if ( type.isIdentifier(node) )
|
||||||
|
value = node.name;
|
||||||
|
else if ( type.isStringLiteral(node) )
|
||||||
|
value = node.value;
|
||||||
|
else if ( type.isThisExpression(node) )
|
||||||
|
value = 'this';
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if ( parts[i] !== value )
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const babelOptions = {
|
||||||
|
ast: true,
|
||||||
|
parserOpts: JSON.parse(fs.readFileSync('.babelrc', 'utf8'))
|
||||||
|
};
|
||||||
|
|
||||||
|
babelOptions.parserOpts.plugins = [
|
||||||
|
'jsx',
|
||||||
|
'dynamicImport',
|
||||||
|
'optionalChaining',
|
||||||
|
'objectRestSpread'
|
||||||
|
]
|
||||||
|
|
||||||
|
function getString(node) {
|
||||||
|
if ( type.isStringLiteral(node) )
|
||||||
|
return node.value;
|
||||||
|
|
||||||
|
if ( type.isTemplateLiteral(node) && (! node.expressions || ! node.expressions.length) && node.quasis && node.quasis.length === 1 )
|
||||||
|
return node.quasis[0].value.cooked;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFromCode(code) {
|
||||||
|
const { ast } = transformSync(code, babelOptions);
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
traverse(ast, {
|
||||||
|
CallExpression(path) {
|
||||||
|
const callee = path.get('callee');
|
||||||
|
if ( ! callee )
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ( !( matchesPattern(callee.node, 'this.i18n.t') ||
|
||||||
|
matchesPattern(callee.node, 'i18n.t') ||
|
||||||
|
matchesPattern(callee.node, 't.i18n.t') ||
|
||||||
|
matchesPattern(callee.node, 'this.i18n.tList') ||
|
||||||
|
matchesPattern(callee.node, 'i18n.tList') ||
|
||||||
|
matchesPattern(callee.node, 't.i18n.tList') ||
|
||||||
|
matchesPattern(callee.node, 'this.t') ||
|
||||||
|
matchesPattern(callee.node, 'this.tList') ))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const key = getString(path.get('arguments.0').node);
|
||||||
|
if ( ! key )
|
||||||
|
return;
|
||||||
|
|
||||||
|
matches.push({
|
||||||
|
key,
|
||||||
|
loc: path.node.loc.start,
|
||||||
|
phrase: getString(path.get('arguments.1').node)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFromFiles(files) {
|
||||||
|
const results = [];
|
||||||
|
|
||||||
|
if ( ! Array.isArray(files) )
|
||||||
|
files = [files];
|
||||||
|
|
||||||
|
const scannable = new Set;
|
||||||
|
for(const thing of files) {
|
||||||
|
for(const file of glob.sync(thing, {}))
|
||||||
|
scannable.add(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(const file of scannable) {
|
||||||
|
const code = fs.readFileSync(file, 'utf8');
|
||||||
|
const matches = extractFromCode(code);
|
||||||
|
for(const match of matches) {
|
||||||
|
match.source = `${file}:${match.loc.line}:${match.loc.column}`;
|
||||||
|
delete match.loc;
|
||||||
|
results.push(match);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const bits = extractFromFiles([
|
||||||
|
'src/**/*.js',
|
||||||
|
'src/**/*.jsx'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const seen = new Set;
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for(const entry of bits) {
|
||||||
|
if ( seen.has(entry.key) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
seen.add(entry.key);
|
||||||
|
if ( entry.key && entry.phrase )
|
||||||
|
out.push({
|
||||||
|
key: entry.key,
|
||||||
|
phrase: entry.phrase,
|
||||||
|
calls: [
|
||||||
|
`/${entry.source}`
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync('extracted.json', JSON.stringify(out, null, '\t'));
|
||||||
|
|
||||||
|
console.log(`Extracted ${out.length} strings.`);
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.15.3",
|
"version": "4.15.4",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -341,8 +341,6 @@ export default {
|
||||||
if ( ! allow_update )
|
if ( ! allow_update )
|
||||||
delete profile_data.url;
|
delete profile_data.url;
|
||||||
|
|
||||||
console.log('Importing', profile_data, data, this.import_data);
|
|
||||||
|
|
||||||
const prof = this.context.createProfile(profile_data);
|
const prof = this.context.createProfile(profile_data);
|
||||||
|
|
||||||
prof.update({
|
prof.update({
|
||||||
|
|
|
@ -795,7 +795,7 @@ export default class MainMenu extends Module {
|
||||||
},
|
},
|
||||||
|
|
||||||
resize: e => {
|
resize: e => {
|
||||||
if ( this.dialog.exclusive || this.site?.router?.current_name === 'squad' )
|
if ( this.dialog.exclusive || this.site?.router?.current_name === 'squad' || this.site?.router?.current_name === 'command-center' )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( this.settings.get('context.ui.theatreModeEnabled') )
|
if ( this.settings.get('context.ui.theatreModeEnabled') )
|
||||||
|
|
|
@ -204,7 +204,8 @@ Twilight.CHAT_ROUTES = [
|
||||||
'user',
|
'user',
|
||||||
'dash',
|
'dash',
|
||||||
'embed-chat',
|
'embed-chat',
|
||||||
'squad'
|
'squad',
|
||||||
|
'command-center'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
@ -252,6 +253,7 @@ Twilight.ROUTES = {
|
||||||
'turbo': '/turbo',
|
'turbo': '/turbo',
|
||||||
'user': '/:userName',
|
'user': '/:userName',
|
||||||
'squad': '/:userName/squad',
|
'squad': '/:userName/squad',
|
||||||
|
'command-center': '/:userName/commandcenter',
|
||||||
'embed-chat': '/embed/:userName/chat'
|
'embed-chat': '/embed/:userName/chat'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Module from 'utilities/module';
|
||||||
import { get, has } from 'utilities/object';
|
import { get, has } from 'utilities/object';
|
||||||
|
|
||||||
import Twilight from 'site';
|
import Twilight from 'site';
|
||||||
|
import { Color } from 'src/utilities/color';
|
||||||
|
|
||||||
|
|
||||||
export default class Channel extends Module {
|
export default class Channel extends Module {
|
||||||
|
@ -18,6 +19,7 @@ export default class Channel extends Module {
|
||||||
|
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
this.inject('site.fine');
|
this.inject('site.fine');
|
||||||
|
this.inject('site.css_tweaks');
|
||||||
|
|
||||||
this.joined_raids = new Set;
|
this.joined_raids = new Set;
|
||||||
|
|
||||||
|
@ -71,6 +73,20 @@ export default class Channel extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updateChannelColor(color) {
|
||||||
|
const parsed = color && Color.RGBA.fromHex(color);
|
||||||
|
if ( parsed ) {
|
||||||
|
this.css_tweaks.setVariable('channel-color', parsed.toCSS());
|
||||||
|
this.css_tweaks.setVariable('channel-color-20', parsed._a(0.2).toCSS());
|
||||||
|
this.css_tweaks.setVariable('channel-color-30', parsed._a(0.3).toCSS());
|
||||||
|
} else {
|
||||||
|
this.css_tweaks.deleteVariable('channel-color');
|
||||||
|
this.css_tweaks.deleteVariable('channel-color-20');
|
||||||
|
this.css_tweaks.deleteVariable('channel-color-30');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
this.ChannelPage.on('mount', this.wrapChannelPage, this);
|
this.ChannelPage.on('mount', this.wrapChannelPage, this);
|
||||||
this.RaidController.on('mount', this.wrapRaidController, this);
|
this.RaidController.on('mount', this.wrapRaidController, this);
|
||||||
|
@ -84,19 +100,11 @@ export default class Channel extends Module {
|
||||||
this.wrapRaidController(inst);
|
this.wrapRaidController(inst);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ChannelPage.on('mount', inst => {
|
this.ChannelPage.on('mount', this.onChannelMounted, this);
|
||||||
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.broadcastSettings.game', inst);
|
|
||||||
|
|
||||||
this.settings.updateContext({
|
|
||||||
channel: get('state.channel.login', inst),
|
|
||||||
channelID: get('state.channel.id', inst),
|
|
||||||
channelColor: get('state.primaryColorHex', inst),
|
|
||||||
category: category?.name,
|
|
||||||
categoryID: category?.id
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ChannelPage.on('unmount', () => {
|
this.ChannelPage.on('unmount', () => {
|
||||||
|
this.updateChannelColor(null);
|
||||||
|
|
||||||
this.settings.updateContext({
|
this.settings.updateContext({
|
||||||
channel: null,
|
channel: null,
|
||||||
channelID: null,
|
channelID: null,
|
||||||
|
@ -109,10 +117,13 @@ export default class Channel extends Module {
|
||||||
this.ChannelPage.on('update', inst => {
|
this.ChannelPage.on('update', inst => {
|
||||||
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.broadcastSettings.game', inst);
|
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.broadcastSettings.game', inst);
|
||||||
|
|
||||||
|
const color = get('state.primaryColorHex', inst);
|
||||||
|
this.updateChannelColor(color);
|
||||||
|
|
||||||
this.settings.updateContext({
|
this.settings.updateContext({
|
||||||
channel: get('state.channel.login', inst),
|
channel: get('state.channel.login', inst),
|
||||||
channelID: get('state.channel.id', inst),
|
channelID: get('state.channel.id', inst),
|
||||||
channelColor: get('state.primaryColorHex', inst),
|
channelColor: color,
|
||||||
category: category?.name,
|
category: category?.name,
|
||||||
categoryID: category?.id
|
categoryID: category?.id
|
||||||
});
|
});
|
||||||
|
@ -133,7 +144,24 @@ export default class Channel extends Module {
|
||||||
|
|
||||||
this.ChannelPage.ready((cls, instances) => {
|
this.ChannelPage.ready((cls, instances) => {
|
||||||
for(const inst of instances)
|
for(const inst of instances)
|
||||||
this.wrapChannelPage(inst);
|
this.onChannelMounted(inst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChannelMounted(inst) {
|
||||||
|
this.wrapChannelPage(inst);
|
||||||
|
|
||||||
|
const category = get('state.video.game', inst) || get('state.clip.game', inst) || get('state.channel.broadcastSettings.game', inst);
|
||||||
|
|
||||||
|
const color = get('state.primaryColorHex', inst);
|
||||||
|
this.updateChannelColor(color);
|
||||||
|
|
||||||
|
this.settings.updateContext({
|
||||||
|
channel: get('state.channel.login', inst),
|
||||||
|
channelID: get('state.channel.id', inst),
|
||||||
|
channelColor: color,
|
||||||
|
category: category?.name,
|
||||||
|
categoryID: category?.id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import {ColorAdjuster} from 'utilities/color';
|
import {ColorAdjuster} from 'utilities/color';
|
||||||
import {setChildren} from 'utilities/dom';
|
import {setChildren} from 'utilities/dom';
|
||||||
import {get, has, make_enum, split_chars, shallow_object_equals, set_equals} from 'utilities/object';
|
import {get, has, make_enum, split_chars, shallow_object_equals, set_equals} from 'utilities/object';
|
||||||
|
import {WEBKIT_CSS as WEBKIT} from 'utilities/constants';
|
||||||
import {FFZEvent} from 'utilities/events';
|
import {FFZEvent} from 'utilities/events';
|
||||||
|
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
|
@ -19,6 +20,7 @@ import SettingsMenu from './settings_menu';
|
||||||
import EmoteMenu from './emote_menu';
|
import EmoteMenu from './emote_menu';
|
||||||
import Input from './input';
|
import Input from './input';
|
||||||
import ViewerCards from './viewer_card';
|
import ViewerCards from './viewer_card';
|
||||||
|
import { isHighlightedReward } from './points';
|
||||||
|
|
||||||
|
|
||||||
const REGEX_EMOTES = {
|
const REGEX_EMOTES = {
|
||||||
|
@ -237,8 +239,29 @@ export default class ChatHook extends Module {
|
||||||
Twilight.CHAT_ROUTES
|
Twilight.CHAT_ROUTES
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.PointsInfo = this.fine.define(
|
||||||
|
'points-info',
|
||||||
|
n => n.pointIcon !== undefined && n.pointName !== undefined,
|
||||||
|
Twilight.CHAT_ROUTES
|
||||||
|
);
|
||||||
|
|
||||||
|
this.GiftBanner = this.fine.define(
|
||||||
|
'gift-banner',
|
||||||
|
n => n.getBannerText && n.handleCountdownEnd && n.getRemainingTime,
|
||||||
|
Twilight.CHAT_ROUTES
|
||||||
|
);
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
||||||
|
this.settings.add('chat.subs.gift-banner', {
|
||||||
|
default: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Chat > Appearance >> Subscriptions',
|
||||||
|
title: 'Display a banner at the top of chat when a mass gift sub happens.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('chat.community-chest.show', {
|
this.settings.add('chat.community-chest.show', {
|
||||||
default: true,
|
default: true,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -248,6 +271,16 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settings.add('chat.points.custom-rendering', {
|
||||||
|
default: true,
|
||||||
|
ui: {
|
||||||
|
path: 'Chat > Channel Points >> Appearance',
|
||||||
|
title: 'Use custom rendering for channel points reward messages in chat.',
|
||||||
|
description: 'Custom rendering applies a background color to highlighted messages, which some users may not appreciate.',
|
||||||
|
component: 'setting-check-box'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.settings.add('chat.points.show-callouts', {
|
this.settings.add('chat.points.show-callouts', {
|
||||||
default: true,
|
default: true,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -536,6 +569,25 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
updatePointsInfo(inst) {
|
||||||
|
const icon = inst?.pointIcon,
|
||||||
|
name = inst?.pointName;
|
||||||
|
|
||||||
|
if ( icon ) {
|
||||||
|
this.css_tweaks.set('points-icon', `.ffz--points-icon:before { display: none }
|
||||||
|
.ffz--points-icon:after {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 0.5rem -0.6rem;
|
||||||
|
background-image: url("${icon.url}");
|
||||||
|
background-image: ${WEBKIT}image-set(url("${icon.url}") 1x, url("${icon.url2x}") 2x, url("${icon.url4x}") 4x);
|
||||||
|
}`);
|
||||||
|
} else
|
||||||
|
this.css_tweaks.delete('points-icon');
|
||||||
|
|
||||||
|
this.point_name = name || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async grabTypes() {
|
async grabTypes() {
|
||||||
const ct = await this.web_munch.findModule('chat-types'),
|
const ct = await this.web_munch.findModule('chat-types'),
|
||||||
changes = [];
|
changes = [];
|
||||||
|
@ -579,6 +631,7 @@ export default class ChatHook extends Module {
|
||||||
this.PointsClaimButton.forceUpdate();
|
this.PointsClaimButton.forceUpdate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.chat.context.on('changed:chat.subs.gift-banner', () => this.GiftBanner.forceUpdate(), this);
|
||||||
this.chat.context.on('changed:chat.width', this.updateChatCSS, this);
|
this.chat.context.on('changed:chat.width', this.updateChatCSS, this);
|
||||||
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
|
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
|
||||||
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
|
||||||
|
@ -652,6 +705,23 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
const t = this;
|
const t = this;
|
||||||
|
|
||||||
|
this.PointsInfo.on('mount', this.updatePointsInfo, this);
|
||||||
|
this.PointsInfo.on('update', this.updatePointsInfo, this);
|
||||||
|
this.PointsInfo.on('unmount', () => this.updatePointsInfo(null));
|
||||||
|
this.PointsInfo.ready(() => this.updatePointsInfo(this.PointsInfo.first));
|
||||||
|
|
||||||
|
this.GiftBanner.ready(cls => {
|
||||||
|
const old_render = cls.prototype.render;
|
||||||
|
cls.prototype.render = function() {
|
||||||
|
if ( ! t.chat.context.get('chat.subs.gift-banner') )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return old_render.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.GiftBanner.forceUpdate();
|
||||||
|
});
|
||||||
|
|
||||||
this.CommunityChestBanner.ready(cls => {
|
this.CommunityChestBanner.ready(cls => {
|
||||||
const old_render = cls.prototype.render;
|
const old_render = cls.prototype.render;
|
||||||
cls.prototype.render = function() {
|
cls.prototype.render = function() {
|
||||||
|
@ -751,13 +821,17 @@ export default class ChatHook extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.PointsClaimButton.ready(cls => {
|
this.PointsClaimButton.ready(cls => {
|
||||||
|
cls.prototype.ffzHasOffer = function() {
|
||||||
|
return ! this.props.hidden && ! this.state?.error && this.getClaim() != null;
|
||||||
|
};
|
||||||
|
|
||||||
const old_render = cls.prototype.render;
|
const old_render = cls.prototype.render;
|
||||||
cls.prototype.render = function() {
|
cls.prototype.render = function() {
|
||||||
try {
|
try {
|
||||||
if ( ! this._ffz_timer && t.chat.context.get('chat.points.auto-rewards') )
|
if ( this.ffzHasOffer() && ! this._ffz_timer && t.chat.context.get('chat.points.auto-rewards') )
|
||||||
this._ffz_timer = setTimeout(() => {
|
this._ffz_timer = setTimeout(() => {
|
||||||
this._ffz_timer = null;
|
this._ffz_timer = null;
|
||||||
if ( this.onClick )
|
if ( this.onClick && this.ffzHasOffer() )
|
||||||
this.onClick();
|
this.onClick();
|
||||||
}, 1000 + Math.floor(Math.random() * 5000));
|
}, 1000 + Math.floor(Math.random() * 5000));
|
||||||
|
|
||||||
|
@ -777,7 +851,8 @@ export default class ChatHook extends Module {
|
||||||
|
|
||||||
this.ChatController.on('mount', this.chatMounted, this);
|
this.ChatController.on('mount', this.chatMounted, this);
|
||||||
this.ChatController.on('unmount', this.chatUnmounted, this);
|
this.ChatController.on('unmount', this.chatUnmounted, this);
|
||||||
this.ChatController.on('receive-props', this.chatUpdated, this);
|
//this.ChatController.on('receive-props', this.chatUpdated, this);
|
||||||
|
this.ChatController.on('update', this.chatUpdated, this);
|
||||||
|
|
||||||
this.ChatService.ready((cls, instances) => {
|
this.ChatService.ready((cls, instances) => {
|
||||||
this.wrapChatService(cls);
|
this.wrapChatService(cls);
|
||||||
|
@ -839,7 +914,7 @@ export default class ChatHook extends Module {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.ChatBufferConnector.on('mount', this.connectorMounted, this);
|
this.ChatBufferConnector.on('mount', this.connectorMounted, this);
|
||||||
this.ChatBufferConnector.on('receive-props', this.connectorUpdated, this);
|
this.ChatBufferConnector.on('update', this.connectorUpdated, this);
|
||||||
this.ChatBufferConnector.on('unmount', this.connectorUnmounted, this);
|
this.ChatBufferConnector.on('unmount', this.connectorUnmounted, this);
|
||||||
|
|
||||||
this.ChatBufferConnector.ready((cls, instances) => {
|
this.ChatBufferConnector.ready((cls, instances) => {
|
||||||
|
@ -1747,6 +1822,29 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const old_points = this.onChannelPointsRewardEvent;
|
||||||
|
this.onChannelPointsRewardEvent = function(e) {
|
||||||
|
try {
|
||||||
|
if ( t.chat.context.get('chat.points.custom-rendering') ) {
|
||||||
|
const reward = e.rewardID && get(e.rewardID, i.props.rewardMap);
|
||||||
|
if ( reward ) {
|
||||||
|
const out = i.convertMessage(e);
|
||||||
|
|
||||||
|
out.ffz_type = 'points';
|
||||||
|
out.ffz_reward = reward;
|
||||||
|
|
||||||
|
return i.postMessageToCurrentChannel(e, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
t.log.error(err);
|
||||||
|
t.log.capture(err, {extra: e});
|
||||||
|
}
|
||||||
|
|
||||||
|
return old_points.call(i, e);
|
||||||
|
}
|
||||||
|
|
||||||
const old_host = this.onHostingEvent;
|
const old_host = this.onHostingEvent;
|
||||||
this.onHostingEvent = function (e, _t) {
|
this.onHostingEvent = function (e, _t) {
|
||||||
t.emit('tmi:host', e, _t);
|
t.emit('tmi:host', e, _t);
|
||||||
|
@ -1943,6 +2041,11 @@ export default class ChatHook extends Module {
|
||||||
channelID: null
|
channelID: null
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.settings.updateContext({
|
||||||
|
moderator: false,
|
||||||
|
chatHidden: false
|
||||||
|
});
|
||||||
|
|
||||||
this.chat.context.updateContext({
|
this.chat.context.updateContext({
|
||||||
moderator: false,
|
moderator: false,
|
||||||
channel: null,
|
channel: null,
|
||||||
|
@ -1960,30 +2063,34 @@ export default class ChatHook extends Module {
|
||||||
if ( ! chat._ffz_room || props.channelID != chat._ffz_room.id ) {
|
if ( ! chat._ffz_room || props.channelID != chat._ffz_room.id ) {
|
||||||
this.removeRoom(chat);
|
this.removeRoom(chat);
|
||||||
if ( chat._ffz_mounted )
|
if ( chat._ffz_mounted )
|
||||||
this.chatMounted(chat, props);
|
this.chatMounted(chat);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( props.bitsConfig !== chat.props.bitsConfig )
|
if ( props.bitsConfig !== chat.props.bitsConfig )
|
||||||
this.updateRoomBitsConfig(chat, props.bitsConfig);
|
this.updateRoomBitsConfig(chat, chat.props.bitsConfig);
|
||||||
|
|
||||||
// TODO: Check if this is the room for the current channel.
|
// TODO: Check if this is the room for the current channel.
|
||||||
|
|
||||||
if ( props.isEmbedded || props.isPopout )
|
let login = chat.props.channelLogin;
|
||||||
|
if ( login )
|
||||||
|
login = login.toLowerCase();
|
||||||
|
|
||||||
|
if ( chat.props.isEmbedded || chat.props.isPopout )
|
||||||
this.settings.updateContext({
|
this.settings.updateContext({
|
||||||
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
channel: login,
|
||||||
channelID: props.channelID
|
channelID: chat.props.channelID
|
||||||
});
|
});
|
||||||
|
|
||||||
this.settings.updateContext({
|
this.settings.updateContext({
|
||||||
moderator: props.isCurrentUserModerator,
|
moderator: chat.props.isCurrentUserModerator,
|
||||||
chatHidden: props.isHidden
|
chatHidden: chat.props.isHidden
|
||||||
});
|
});
|
||||||
|
|
||||||
this.chat.context.updateContext({
|
this.chat.context.updateContext({
|
||||||
moderator: props.isCurrentUserModerator,
|
moderator: chat.props.isCurrentUserModerator,
|
||||||
channel: props.channelLogin && props.channelLogin.toLowerCase(),
|
channel: login,
|
||||||
channelID: props.channelID,
|
channelID: chat.props.channelID,
|
||||||
/*ui: {
|
/*ui: {
|
||||||
theme: props.theme
|
theme: props.theme
|
||||||
}*/
|
}*/
|
||||||
|
@ -2027,8 +2134,8 @@ export default class ChatHook extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectorUpdated(inst, props) { // eslint-disable-line class-methods-use-this
|
connectorUpdated(inst, props) { // eslint-disable-line class-methods-use-this
|
||||||
const buffer = inst.props.messageBufferAPI,
|
const buffer = props.messageBufferAPI,
|
||||||
new_buffer = props.messageBufferAPI;
|
new_buffer = inst.props.messageBufferAPI;
|
||||||
|
|
||||||
if ( buffer === new_buffer )
|
if ( buffer === new_buffer )
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import RichContent from './rich_content';
|
||||||
import { has } from 'utilities/object';
|
import { has } from 'utilities/object';
|
||||||
import { KEYS } from 'utilities/constants';
|
import { KEYS } from 'utilities/constants';
|
||||||
import { print_duration } from 'src/utilities/time';
|
import { print_duration } from 'src/utilities/time';
|
||||||
|
import { getRewardTitle, getRewardCost, isHighlightedReward } from './points';
|
||||||
|
|
||||||
const SUB_TIERS = {
|
const SUB_TIERS = {
|
||||||
1000: 1,
|
1000: 1,
|
||||||
|
@ -357,19 +358,26 @@ other {# messages were deleted by a moderator.}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined;
|
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined,
|
||||||
|
room_id = msg.roomId ? msg.roomId : this.props.channelID;
|
||||||
|
|
||||||
if ( ! room && this.props.channelID ) {
|
if ( ! room && room_id ) {
|
||||||
const r = t.chat.getRoom(this.props.channelID, null, true);
|
const r = t.chat.getRoom(room_id, null, true);
|
||||||
if ( r && r.login )
|
if ( r && r.login )
|
||||||
room = msg.roomLogin = 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;
|
||||||
|
}
|
||||||
|
|
||||||
//if ( ! msg.message && msg.messageParts )
|
//if ( ! msg.message && msg.messageParts )
|
||||||
// t.chat.detokenizeMessage(msg);
|
// t.chat.detokenizeMessage(msg);
|
||||||
|
|
||||||
const u = t.site.getUser(),
|
const u = t.site.getUser(),
|
||||||
r = {id: this.props.channelID, login: room};
|
r = {id: room_id, login: room};
|
||||||
|
|
||||||
if ( u ) {
|
if ( u ) {
|
||||||
u.moderator = this.props.isCurrentUserModerator;
|
u.moderator = this.props.isCurrentUserModerator;
|
||||||
|
@ -542,7 +550,7 @@ other {# messages were deleted by a moderator.}
|
||||||
sub_list,
|
sub_list,
|
||||||
out && e('div', {
|
out && e('div', {
|
||||||
className: 'chat-line--inline chat-line__message',
|
className: 'chat-line--inline chat-line__message',
|
||||||
'data-room-id': this.props.channelID,
|
'data-room-id': room_id,
|
||||||
'data-room': room,
|
'data-room': room,
|
||||||
'data-user-id': user.userID,
|
'data-user-id': user.userID,
|
||||||
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
||||||
|
@ -596,7 +604,7 @@ other {# messages were deleted by a moderator.}
|
||||||
]),
|
]),
|
||||||
out && e('div', {
|
out && e('div', {
|
||||||
className: 'chat-line--inline chat-line__message',
|
className: 'chat-line--inline chat-line__message',
|
||||||
'data-room-id': this.props.channelID,
|
'data-room-id': room_id,
|
||||||
'data-room': room,
|
'data-room': room,
|
||||||
'data-user-id': user.userID,
|
'data-user-id': user.userID,
|
||||||
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
||||||
|
@ -658,7 +666,7 @@ other {# messages were deleted by a moderator.}
|
||||||
]),
|
]),
|
||||||
out && e('div', {
|
out && e('div', {
|
||||||
className: 'chat-line--inline chat-line__message',
|
className: 'chat-line--inline chat-line__message',
|
||||||
'data-room-id': this.props.channelID,
|
'data-room-id': room_id,
|
||||||
'data-room': room,
|
'data-room': room,
|
||||||
'data-user-id': user.userID,
|
'data-user-id': user.userID,
|
||||||
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
||||||
|
@ -687,13 +695,46 @@ other {# messages were deleted by a moderator.}
|
||||||
system_msg,
|
system_msg,
|
||||||
out && e('div', {
|
out && e('div', {
|
||||||
className: 'chat-line--inline chat-line__message',
|
className: 'chat-line--inline chat-line__message',
|
||||||
'data-room-id': this.props.channelID,
|
'data-room-id': room_id,
|
||||||
'data-room': room,
|
'data-room': room,
|
||||||
'data-user-id': user.userID,
|
'data-user-id': user.userID,
|
||||||
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
||||||
}, out)
|
}, out)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} else if ( msg.ffz_type === 'points' && msg.ffz_reward ) {
|
||||||
|
const reward = e('span', {className: 'ffz--points-reward'}, getRewardTitle(msg.ffz_reward, t.i18n)),
|
||||||
|
cost = e('span', {className: 'ffz--points-cost'}, [
|
||||||
|
e('span', {className: 'ffz--points-icon'}),
|
||||||
|
t.i18n.formatNumber(getRewardCost(msg.ffz_reward))
|
||||||
|
]);
|
||||||
|
|
||||||
|
cls = `ffz--points-line tw-pd-l-1 tw-pd-y-05 tw-pd-r-2${isHighlightedReward(msg.ffz_reward) ? ' ffz--points-highlight' : ''}`;
|
||||||
|
out = [
|
||||||
|
e('div', {className: 'tw-c-text-alt-2'}, [
|
||||||
|
out ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
|
||||||
|
out ?
|
||||||
|
t.i18n.tList('chat.points.redeemed', 'Redeemed {reward} {cost}', {reward, cost}) :
|
||||||
|
t.i18n.tList('chat.points.user-redeemed', '{user} redeemed {reward} {cost}', {
|
||||||
|
reward, cost,
|
||||||
|
user: e('span', {
|
||||||
|
role: 'button',
|
||||||
|
className: 'chatter-name',
|
||||||
|
onClick: this.ffz_user_click_handler
|
||||||
|
}, e('span', {
|
||||||
|
className: 'tw-c-text-base tw-strong'
|
||||||
|
}, user.userDisplayName))
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
out && e('div', {
|
||||||
|
className: 'chat-line--inline chat-line__message',
|
||||||
|
'data-room-id': room_id,
|
||||||
|
'data-room': room,
|
||||||
|
'data-user-id': user.userID,
|
||||||
|
'data-user': user.userLogin && user.userLogin.toLowerCase()
|
||||||
|
}, out)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! out )
|
if ( ! out )
|
||||||
|
@ -702,7 +743,7 @@ other {# messages were deleted by a moderator.}
|
||||||
return e('div', {
|
return e('div', {
|
||||||
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
|
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
|
||||||
style: {backgroundColor: bg_css},
|
style: {backgroundColor: bg_css},
|
||||||
'data-room-id': this.props.channelID,
|
'data-room-id': room_id,
|
||||||
'data-room': room,
|
'data-room': room,
|
||||||
'data-user-id': user.userID,
|
'data-user-id': user.userID,
|
||||||
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
'data-user': user.userLogin && user.userLogin.toLowerCase(),
|
||||||
|
|
45
src/sites/twitch-twilight/modules/chat/points.js
Normal file
45
src/sites/twitch-twilight/modules/chat/points.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
export function isAutomaticReward(reward) {
|
||||||
|
return reward?.__typename === 'CommunityPointsAutomaticReward';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCustomReward(reward) {
|
||||||
|
return reward?.__typename === 'CommunityPointsCustomReward';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isHighlightedReward(reward) {
|
||||||
|
return isAutomaticReward(reward) && reward.type === 'SEND_HIGHLIGHTED_MESSAGE';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRewardCost(reward) {
|
||||||
|
if ( isAutomaticReward(reward) )
|
||||||
|
return reward.cost || reward.defaultCost;
|
||||||
|
|
||||||
|
return reward.cost;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRewardColor(reward) {
|
||||||
|
if ( isAutomaticReward(reward) )
|
||||||
|
return reward.backgroundColor || reward.defaultBackgroundColor;
|
||||||
|
|
||||||
|
return reward.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRewardTitle(reward, i18n) {
|
||||||
|
if ( isCustomReward(reward) )
|
||||||
|
return reward.title;
|
||||||
|
|
||||||
|
switch(reward.type) {
|
||||||
|
case 'SEND_HIGHLIGHTED_MESSAGE':
|
||||||
|
return i18n.t('chat.points.highlighted', 'Highlight My Message');
|
||||||
|
case 'SINGLE_MESSAGE_BYPASS_SUB_MODE':
|
||||||
|
return i18n.t('chat.points.bypass-sub', 'Send a Message in Sub-Only Mode');
|
||||||
|
case 'CHOSEN_SUB_EMOTE_UNLOCK':
|
||||||
|
return i18n.t('chat.points.choose-emote', 'Choose an Emote to Unlock');
|
||||||
|
case 'RANDOM_SUB_EMOTE_UNLOCK':
|
||||||
|
return i18n.t('chat.points.random-emote', 'Unlock a Random Sub Emote');
|
||||||
|
case 'CHOSEN_MODIFIED_SUB_EMOTE_UNLOCK':
|
||||||
|
return i18n.t('chat.points.modify-emote', 'Modify a Single Emote');
|
||||||
|
default:
|
||||||
|
return i18n.t('chat.points.reward', 'Reward');
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
.vod-message,
|
.vod-message,
|
||||||
|
|
||||||
|
.ffz--points-line,
|
||||||
|
|
||||||
.chat-line__message:not(.chat-line--inline),
|
.chat-line__message:not(.chat-line--inline),
|
||||||
.chat-line__moderation,
|
.chat-line__moderation,
|
||||||
.chat-line__status,
|
.chat-line__status,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
.thread-message__timestamp,
|
.thread-message__timestamp,
|
||||||
.thread-message__warning,
|
.thread-message__warning,
|
||||||
|
|
||||||
|
.ffz--points-line,
|
||||||
.chat-line__message:not(.chat-line--inline),
|
.chat-line__message:not(.chat-line--inline),
|
||||||
.chat-line__moderation,
|
.chat-line__moderation,
|
||||||
.chat-line__status,
|
.chat-line__status,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
.thread-message__timestamp,
|
.thread-message__timestamp,
|
||||||
.thread-message__warning,
|
.thread-message__warning,
|
||||||
|
|
||||||
|
.ffz--points-line,
|
||||||
.chat-line__message:not(.chat-line--inline),
|
.chat-line__message:not(.chat-line--inline),
|
||||||
.chat-line__moderation,
|
.chat-line__moderation,
|
||||||
.chat-line__status,
|
.chat-line__status,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
.thread-message__timestamp,
|
.thread-message__timestamp,
|
||||||
.thread-message__warning,
|
.thread-message__warning,
|
||||||
|
|
||||||
|
.ffz--points-line,
|
||||||
.chat-line__message:not(.chat-line--inline),
|
.chat-line__message:not(.chat-line--inline),
|
||||||
.chat-line__moderation,
|
.chat-line__moderation,
|
||||||
.chat-line__status,
|
.chat-line__status,
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
padding: .5rem 1rem !important;
|
padding: .5rem 1rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz--points-line,
|
||||||
.user-notice-line {
|
.user-notice-line {
|
||||||
padding: .5rem 1rem !important;
|
padding: .5rem 1rem !important;
|
||||||
padding-left: .6rem !important;
|
padding-left: .6rem !important;
|
||||||
|
|
|
@ -31,4 +31,8 @@
|
||||||
.tw-root--theme-dark & {
|
.tw-root--theme-dark & {
|
||||||
background-color: rgba(255,255,255,0.05) !important;
|
background-color: rgba(255,255,255,0.05) !important;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--points-highlight:nth-child(2n+0) {
|
||||||
|
background-color: var(--ffz-channel-color-30);
|
||||||
}
|
}
|
|
@ -45,6 +45,12 @@ export default class MenuButton extends SiteModule {
|
||||||
n => n.exitSquadMode && n.props && n.props.squadID,
|
n => n.exitSquadMode && n.props && n.props.squadID,
|
||||||
['squad']
|
['squad']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.MultiController = this.fine.define(
|
||||||
|
'multi-controller',
|
||||||
|
n => n.handleAddStream && n.handleRemoveStream && n.getInitialStreamLayout,
|
||||||
|
['command-center']
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get loading() {
|
get loading() {
|
||||||
|
@ -162,6 +168,9 @@ export default class MenuButton extends SiteModule {
|
||||||
|
|
||||||
for(const inst of this.SquadBar.instances)
|
for(const inst of this.SquadBar.instances)
|
||||||
this.updateButton(inst);
|
this.updateButton(inst);
|
||||||
|
|
||||||
|
for(const inst of this.MultiController.instances)
|
||||||
|
this.updateButton(inst);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,6 +184,10 @@ export default class MenuButton extends SiteModule {
|
||||||
this.SquadBar.on('mount', this.updateButton, this);
|
this.SquadBar.on('mount', this.updateButton, this);
|
||||||
this.SquadBar.on('update', this.updateButton, this);
|
this.SquadBar.on('update', this.updateButton, this);
|
||||||
|
|
||||||
|
this.MultiController.ready(() => this.update());
|
||||||
|
this.MultiController.on('mount', this.updateButton, this);
|
||||||
|
this.MultiController.on('update', this.updateButton, this);
|
||||||
|
|
||||||
this.on(':clicked', () => this.important_update = false);
|
this.on(':clicked', () => this.important_update = false);
|
||||||
|
|
||||||
this.once(':clicked', this.loadMenu);
|
this.once(':clicked', this.loadMenu);
|
||||||
|
@ -200,6 +213,12 @@ export default class MenuButton extends SiteModule {
|
||||||
is_squad = true;
|
is_squad = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( ! container && inst.handleAddStream ) {
|
||||||
|
container = this.fine.searchTree(inst, n => n.classList && n.classList.contains('multiview-stream-page__header'));
|
||||||
|
if ( container )
|
||||||
|
is_squad = true;
|
||||||
|
}
|
||||||
|
|
||||||
if ( ! container )
|
if ( ! container )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -499,7 +499,7 @@ export default class Player extends Module {
|
||||||
const player = this.props.mediaPlayerInstance,
|
const player = this.props.mediaPlayerInstance,
|
||||||
events = this.props.playerEvents;
|
events = this.props.playerEvents;
|
||||||
|
|
||||||
if ( player && player.pause )
|
if ( player && player.pause && player.getPlayerState?.() === 'Playing' )
|
||||||
player.pause();
|
player.pause();
|
||||||
else if ( events ) {
|
else if ( events ) {
|
||||||
const immediatePause = () => {
|
const immediatePause = () => {
|
||||||
|
|
|
@ -155,6 +155,40 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ffz--points-highlight {
|
||||||
|
background-color: var(--ffz-channel-color-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--points-line {
|
||||||
|
border-left: 4px solid var(--ffz-channel-color);
|
||||||
|
|
||||||
|
.ffz--points-reward,
|
||||||
|
.ffz--points-cost {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--points-reward {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ffz--points-icon {
|
||||||
|
&:before {
|
||||||
|
content: '\e83c';
|
||||||
|
font-family: 'ffz-fontello';
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
margin: 0 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ffz--emote-picker {
|
.ffz--emote-picker {
|
||||||
section:not(.filtered) heading {
|
section:not(.filtered) heading {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue