1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 12:55:55 +00:00
* 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:
SirStendec 2019-11-11 14:38:49 -05:00
parent 0fdb988da7
commit 8ac1b2ce91
18 changed files with 481 additions and 44 deletions

153
bin/static_i18n.js Normal file
View 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.`);

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.15.3",
"version": "4.15.4",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {

View file

@ -341,8 +341,6 @@ export default {
if ( ! allow_update )
delete profile_data.url;
console.log('Importing', profile_data, data, this.import_data);
const prof = this.context.createProfile(profile_data);
prof.update({

View file

@ -795,7 +795,7 @@ export default class MainMenu extends Module {
},
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;
if ( this.settings.get('context.ui.theatreModeEnabled') )

View file

@ -204,7 +204,8 @@ Twilight.CHAT_ROUTES = [
'user',
'dash',
'embed-chat',
'squad'
'squad',
'command-center'
];
@ -252,6 +253,7 @@ Twilight.ROUTES = {
'turbo': '/turbo',
'user': '/:userName',
'squad': '/:userName/squad',
'command-center': '/:userName/commandcenter',
'embed-chat': '/embed/:userName/chat'
};

View file

@ -8,6 +8,7 @@ import Module from 'utilities/module';
import { get, has } from 'utilities/object';
import Twilight from 'site';
import { Color } from 'src/utilities/color';
export default class Channel extends Module {
@ -18,6 +19,7 @@ export default class Channel extends Module {
this.inject('settings');
this.inject('site.fine');
this.inject('site.css_tweaks');
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() {
this.ChannelPage.on('mount', this.wrapChannelPage, this);
this.RaidController.on('mount', this.wrapRaidController, this);
@ -84,19 +100,11 @@ export default class Channel extends Module {
this.wrapRaidController(inst);
});
this.ChannelPage.on('mount', inst => {
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('mount', this.onChannelMounted, this);
this.ChannelPage.on('unmount', () => {
this.updateChannelColor(null);
this.settings.updateContext({
channel: null,
channelID: null,
@ -109,10 +117,13 @@ export default class Channel extends Module {
this.ChannelPage.on('update', 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: get('state.primaryColorHex', inst),
channelColor: color,
category: category?.name,
categoryID: category?.id
});
@ -133,7 +144,24 @@ export default class Channel extends Module {
this.ChannelPage.ready((cls, 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
});
}

View file

@ -7,6 +7,7 @@
import {ColorAdjuster} from 'utilities/color';
import {setChildren} from 'utilities/dom';
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 Module from 'utilities/module';
@ -19,6 +20,7 @@ import SettingsMenu from './settings_menu';
import EmoteMenu from './emote_menu';
import Input from './input';
import ViewerCards from './viewer_card';
import { isHighlightedReward } from './points';
const REGEX_EMOTES = {
@ -237,8 +239,29 @@ export default class ChatHook extends Module {
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
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', {
default: true,
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', {
default: true,
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() {
const ct = await this.web_munch.findModule('chat-types'),
changes = [];
@ -579,6 +631,7 @@ export default class ChatHook extends Module {
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.settings.main_context.on('changed:chat.use-width', 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;
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 => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
@ -751,13 +821,17 @@ export default class ChatHook extends Module {
});
this.PointsClaimButton.ready(cls => {
cls.prototype.ffzHasOffer = function() {
return ! this.props.hidden && ! this.state?.error && this.getClaim() != null;
};
const old_render = cls.prototype.render;
cls.prototype.render = function() {
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 = null;
if ( this.onClick )
if ( this.onClick && this.ffzHasOffer() )
this.onClick();
}, 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('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.wrapChatService(cls);
@ -839,7 +914,7 @@ export default class ChatHook extends Module {
});
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.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;
this.onHostingEvent = function (e, _t) {
t.emit('tmi:host', e, _t);
@ -1943,6 +2041,11 @@ export default class ChatHook extends Module {
channelID: null
});
this.settings.updateContext({
moderator: false,
chatHidden: false
});
this.chat.context.updateContext({
moderator: false,
channel: null,
@ -1960,30 +2063,34 @@ export default class ChatHook extends Module {
if ( ! chat._ffz_room || props.channelID != chat._ffz_room.id ) {
this.removeRoom(chat);
if ( chat._ffz_mounted )
this.chatMounted(chat, props);
this.chatMounted(chat);
return;
}
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.
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({
channel: props.channelLogin && props.channelLogin.toLowerCase(),
channelID: props.channelID
channel: login,
channelID: chat.props.channelID
});
this.settings.updateContext({
moderator: props.isCurrentUserModerator,
chatHidden: props.isHidden
moderator: chat.props.isCurrentUserModerator,
chatHidden: chat.props.isHidden
});
this.chat.context.updateContext({
moderator: props.isCurrentUserModerator,
channel: props.channelLogin && props.channelLogin.toLowerCase(),
channelID: props.channelID,
moderator: chat.props.isCurrentUserModerator,
channel: login,
channelID: chat.props.channelID,
/*ui: {
theme: props.theme
}*/
@ -2027,8 +2134,8 @@ export default class ChatHook extends Module {
}
connectorUpdated(inst, props) { // eslint-disable-line class-methods-use-this
const buffer = inst.props.messageBufferAPI,
new_buffer = props.messageBufferAPI;
const buffer = props.messageBufferAPI,
new_buffer = inst.props.messageBufferAPI;
if ( buffer === new_buffer )
return;

View file

@ -11,6 +11,7 @@ import RichContent from './rich_content';
import { has } from 'utilities/object';
import { KEYS } from 'utilities/constants';
import { print_duration } from 'src/utilities/time';
import { getRewardTitle, getRewardCost, isHighlightedReward } from './points';
const SUB_TIERS = {
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 ) {
const r = t.chat.getRoom(this.props.channelID, null, true);
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;
}
//if ( ! msg.message && msg.messageParts )
// t.chat.detokenizeMessage(msg);
const u = t.site.getUser(),
r = {id: this.props.channelID, login: room};
r = {id: room_id, login: room};
if ( u ) {
u.moderator = this.props.isCurrentUserModerator;
@ -542,7 +550,7 @@ other {# messages were deleted by a moderator.}
sub_list,
out && e('div', {
className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID,
'data-room-id': room_id,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
@ -596,7 +604,7 @@ other {# messages were deleted by a moderator.}
]),
out && e('div', {
className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID,
'data-room-id': room_id,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
@ -658,7 +666,7 @@ other {# messages were deleted by a moderator.}
]),
out && e('div', {
className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID,
'data-room-id': room_id,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
@ -687,13 +695,46 @@ other {# messages were deleted by a moderator.}
system_msg,
out && e('div', {
className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID,
'data-room-id': room_id,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
}, 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 )
@ -702,7 +743,7 @@ other {# messages were deleted by a moderator.}
return e('div', {
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
style: {backgroundColor: bg_css},
'data-room-id': this.props.channelID,
'data-room-id': room_id,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),

View 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');
}
}

View file

@ -4,6 +4,8 @@
.vod-message,
.ffz--points-line,
.chat-line__message:not(.chat-line--inline),
.chat-line__moderation,
.chat-line__status,

View file

@ -2,6 +2,7 @@
.thread-message__timestamp,
.thread-message__warning,
.ffz--points-line,
.chat-line__message:not(.chat-line--inline),
.chat-line__moderation,
.chat-line__status,

View file

@ -2,6 +2,7 @@
.thread-message__timestamp,
.thread-message__warning,
.ffz--points-line,
.chat-line__message:not(.chat-line--inline),
.chat-line__moderation,
.chat-line__status,

View file

@ -2,6 +2,7 @@
.thread-message__timestamp,
.thread-message__warning,
.ffz--points-line,
.chat-line__message:not(.chat-line--inline),
.chat-line__moderation,
.chat-line__status,

View file

@ -5,6 +5,7 @@
padding: .5rem 1rem !important;
}
.ffz--points-line,
.user-notice-line {
padding: .5rem 1rem !important;
padding-left: .6rem !important;

View file

@ -31,4 +31,8 @@
.tw-root--theme-dark & {
background-color: rgba(255,255,255,0.05) !important;
}
}
.ffz--points-highlight:nth-child(2n+0) {
background-color: var(--ffz-channel-color-30);
}

View file

@ -45,6 +45,12 @@ export default class MenuButton extends SiteModule {
n => n.exitSquadMode && n.props && n.props.squadID,
['squad']
);
this.MultiController = this.fine.define(
'multi-controller',
n => n.handleAddStream && n.handleRemoveStream && n.getInitialStreamLayout,
['command-center']
);
}
get loading() {
@ -162,6 +168,9 @@ export default class MenuButton extends SiteModule {
for(const inst of this.SquadBar.instances)
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('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.once(':clicked', this.loadMenu);
@ -200,6 +213,12 @@ export default class MenuButton extends SiteModule {
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 )
return;

View file

@ -499,7 +499,7 @@ export default class Player extends Module {
const player = this.props.mediaPlayerInstance,
events = this.props.playerEvents;
if ( player && player.pause )
if ( player && player.pause && player.getPlayerState?.() === 'Playing' )
player.pause();
else if ( events ) {
const immediatePause = () => {

View file

@ -155,6 +155,40 @@
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 {
section:not(.filtered) heading {