From 8ac1b2ce91d2337c29944cbcbcb595bdcad5601d Mon Sep 17 00:00:00 2001 From: SirStendec Date: Mon, 11 Nov 2019 14:38:49 -0500 Subject: [PATCH] 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. --- bin/static_i18n.js | 153 ++++++++++++++++++ package.json | 2 +- .../main_menu/components/profile-manager.vue | 2 - src/modules/main_menu/index.js | 2 +- src/sites/twitch-twilight/index.js | 4 +- src/sites/twitch-twilight/modules/channel.js | 54 +++++-- .../twitch-twilight/modules/chat/index.js | 139 ++++++++++++++-- .../twitch-twilight/modules/chat/line.js | 59 +++++-- .../twitch-twilight/modules/chat/points.js | 45 ++++++ .../styles/chat-borders-3d-inset.scss | 2 + .../css_tweaks/styles/chat-borders-3d.scss | 1 + .../css_tweaks/styles/chat-borders-wide.scss | 1 + .../css_tweaks/styles/chat-borders.scss | 1 + .../css_tweaks/styles/chat-padding.scss | 1 + .../modules/css_tweaks/styles/chat-rows.scss | 4 + .../twitch-twilight/modules/menu_button.jsx | 19 +++ src/sites/twitch-twilight/modules/player.jsx | 2 +- src/sites/twitch-twilight/styles/chat.scss | 34 ++++ 18 files changed, 481 insertions(+), 44 deletions(-) create mode 100644 bin/static_i18n.js create mode 100644 src/sites/twitch-twilight/modules/chat/points.js diff --git a/bin/static_i18n.js b/bin/static_i18n.js new file mode 100644 index 00000000..764fc5fc --- /dev/null +++ b/bin/static_i18n.js @@ -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.`); diff --git a/package.json b/package.json index 0f8db3c4..9383e30a 100755 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/modules/main_menu/components/profile-manager.vue b/src/modules/main_menu/components/profile-manager.vue index fd8bd7d9..e9f118f7 100644 --- a/src/modules/main_menu/components/profile-manager.vue +++ b/src/modules/main_menu/components/profile-manager.vue @@ -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({ diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js index 5dcf121a..a6885571 100644 --- a/src/modules/main_menu/index.js +++ b/src/modules/main_menu/index.js @@ -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') ) diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js index 5597b3a0..44289907 100644 --- a/src/sites/twitch-twilight/index.js +++ b/src/sites/twitch-twilight/index.js @@ -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' }; diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.js index edffd389..8ad420b4 100644 --- a/src/sites/twitch-twilight/modules/channel.js +++ b/src/sites/twitch-twilight/modules/channel.js @@ -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 }); } diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index 1aac871b..57bf6b7a 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -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; diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 065c5dbd..56c2f1b8 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -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(), diff --git a/src/sites/twitch-twilight/modules/chat/points.js b/src/sites/twitch-twilight/modules/chat/points.js new file mode 100644 index 00000000..d821ff20 --- /dev/null +++ b/src/sites/twitch-twilight/modules/chat/points.js @@ -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'); + } +} \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss index 2d858b40..5bf8b064 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss @@ -4,6 +4,8 @@ .vod-message, +.ffz--points-line, + .chat-line__message:not(.chat-line--inline), .chat-line__moderation, .chat-line__status, diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss index 04450603..bbf116a8 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss @@ -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, diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss index ef51346e..4f0fe0b0 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss @@ -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, diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss index f439bf7d..b15f4b67 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss @@ -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, diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-padding.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-padding.scss index 5af9ca6d..5a09c824 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-padding.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-padding.scss @@ -5,6 +5,7 @@ padding: .5rem 1rem !important; } +.ffz--points-line, .user-notice-line { padding: .5rem 1rem !important; padding-left: .6rem !important; diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss index e0e8db23..a4f39507 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss @@ -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); } \ No newline at end of file diff --git a/src/sites/twitch-twilight/modules/menu_button.jsx b/src/sites/twitch-twilight/modules/menu_button.jsx index 54b4d78b..3fe8797e 100644 --- a/src/sites/twitch-twilight/modules/menu_button.jsx +++ b/src/sites/twitch-twilight/modules/menu_button.jsx @@ -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; diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index 4ff32a4e..93d75cae 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -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 = () => { diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index 66307b6e..935df0fb 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -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 {