diff --git a/src/main.js b/src/main.js
index 59a2143e..761609a1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
- major: 4, minor: 0, revision: 0, extra: '-rc9.3',
+ major: 4, minor: 0, revision: 0, extra: '-rc10',
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>
diff --git a/src/modules/metadata.jsx b/src/modules/metadata.jsx
index aa858d41..0dda7b01 100644
--- a/src/modules/metadata.jsx
+++ b/src/modules/metadata.jsx
@@ -4,7 +4,7 @@
// Channel Metadata
// ============================================================================
-import {createElement, ClickOutside} from 'utilities/dom';
+import {createElement, ClickOutside, setChildren} from 'utilities/dom';
import {maybe_call} from 'utilities/object';
import {duration_to_string} from 'utilities/time';
@@ -58,10 +58,10 @@ export default class Metadata extends Module {
this.definitions.uptime = {
refresh() { return this.settings.get('metadata.uptime') > 0 },
- setup() {
+ setup(data) {
const socket = this.resolve('socket'),
apollo = this.resolve('site.apollo'),
- created_at = apollo.getFromQuery('ChannelPage_ChannelHeader', 'data.user.stream.createdAt');
+ created_at = apollo.getFromQuery(data.legacy ? 'ChannelPage_ChannelHeader' : 'ChannelPage_User', 'data.user.stream.createdAt');
if ( ! created_at )
return {};
@@ -86,6 +86,8 @@ export default class Metadata extends Module {
return duration_to_string(data.uptime, false, false, false, setting !== 2);
},
+ subtitle: () => this.i18n.t('metadata.uptime.subtitle', 'Uptime'),
+
tooltip(data) {
if ( ! data.created )
return null;
@@ -135,7 +137,7 @@ export default class Metadata extends Module {
}
}
- if ( ! stats )
+ if ( ! stats || stats.hlsLatencyBroadcaster < -100 )
return {stats};
let drift = 0;
@@ -154,6 +156,8 @@ export default class Metadata extends Module {
order: 3,
icon: 'ffz-i-gauge',
+ subtitle: () => this.i18n.t('metadata.player-stats.subtitle', 'Latency'),
+
label(data) {
if ( ! this.settings.get('metadata.player-stats') || ! data.delay )
return null;
@@ -226,9 +230,6 @@ export default class Metadata extends Module {
if ( bar ) {
for(const inst of bar.ChannelBar.instances)
bar.updateMetadata(inst, keys);
-
- for(const inst of bar.HostBar.instances)
- bar.updateMetadata(inst, keys);
}
}
@@ -239,6 +240,264 @@ export default class Metadata extends Module {
let el = container.querySelector(`.ffz-stat[data-key="${key}"]`);
+ const def = this.definitions[key],
+ destroy = () => {
+ if ( el ) {
+ if ( el.tooltip )
+ el.tooltip.destroy();
+
+ if ( el.popper )
+ el.popper.destroy();
+
+ if ( el._ffz_destroy )
+ el._ffz_destroy();
+
+ el._ffz_destroy = el.tooltip = el.popper = null;
+ el.remove();
+ }
+ };
+
+ if ( ! def )
+ return destroy();
+
+ try {
+ // Process the data if a setup method is defined.
+ if ( def.setup )
+ data = await def.setup.call(this, data);
+
+ // Let's get refresh logic out of the way now.
+ const refresh = maybe_call(def.refresh, this, data);
+ if ( refresh )
+ timers[key] = setTimeout(
+ () => refresh_fn(key),
+ typeof refresh === 'number' ? refresh : 1000
+ );
+
+
+ // Grab the element again in case it changed, somehow.
+ el = container.querySelector(`.ffz-sidebar-stat[data-key="${key}"]`);
+
+ let stat, old_color, sub_el;
+
+ const label = maybe_call(def.label, this, data);
+
+ if ( ! label )
+ return destroy();
+
+ const tooltip = maybe_call(def.tooltip, this, data),
+ subtitle = maybe_call(def.subtitle, this, data),
+ order = maybe_call(def.order, this, data),
+ color = maybe_call(def.color, this, data) || '';
+
+ if ( ! el ) {
+ let icon = maybe_call(def.icon, this, data);
+ let button = false;
+
+ el = (
);
+
+ if ( def.popup || def.click ) {
+ button = true;
+
+ let btn, popup;
+ let cls = maybe_call(def.button, this, data);
+ if ( typeof cls !== 'string' )
+ cls = `tw-button--${cls ? 'hollow' : 'text'}`;
+
+ if ( typeof icon === 'string' )
+ icon = ();
+
+ if ( def.popup && def.click ) {
+ el.appendChild(
+ {btn = ()}
+ {popup = ()}
+
);
+
+ } else {
+ el.appendChild(btn = popup = ());
+ }
+
+ if ( def.click )
+ btn.addEventListener('click', e => {
+ if ( btn.disabled || btn.classList.contains('disabled') || el.disabled || el.classList.contains('disabled') )
+ return false;
+
+ def.click.call(this, el._ffz_data, e, () => refresh_fn(key));
+ });
+
+ if ( def.popup )
+ popup.addEventListener('click', () => {
+ if ( popup.disabled || popup.classList.contains('disabled') || el.disabled || el.classList.contains('disabled') )
+ return false;
+
+ if ( el._ffz_popup )
+ return el._ffz_destroy();
+
+ const destroy = el._ffz_destroy = () => {
+ if ( el._ffz_outside )
+ el._ffz_outside.destroy();
+
+ if ( el._ffz_popup ) {
+ const fp = el._ffz_popup;
+ el._ffz_popup = null;
+ fp.destroy();
+ }
+
+ el._ffz_destroy = el._ffz_outside = null;
+ };
+
+ const parent = document.body.querySelector('body #root,body'),
+ tt = el._ffz_popup = new Tooltip(parent, el, {
+ logger: this.log,
+ manual: true,
+ html: true,
+
+ tooltipClass: 'ffz-metadata-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background',
+ // Hide the arrow for now, until we re-do our CSS to make it render correctly.
+ arrowClass: 'tw-balloon__tail tw-overflow-hidden tw-absolute',
+ arrowInner: 'tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background tw-absolute',
+ innerClass: 'tw-pd-1',
+
+ popper: {
+ placement: 'right-start',
+ modifiers: {
+ preventOverflow: {
+ boundariesElement: parent
+ },
+ flip: {
+ behavior: ['top', 'bottom', 'left', 'right']
+ }
+ }
+ },
+ content: (t, tip) => def.popup.call(this, el._ffz_data, tip, () => refresh_fn(key)),
+ onShow: (t, tip) =>
+ setTimeout(() => {
+ el._ffz_outside = new ClickOutside(tip.outer, destroy);
+ }),
+ onHide: destroy
+ });
+
+ tt._enter(el);
+ });
+
+ } else {
+ el.appendChild(
+ {stat = }
+
);
+ }
+
+ el.appendChild(sub_el = );
+
+ let subcontainer;
+
+ if ( button ) {
+ subcontainer = container.querySelector('.ffz-sidebar-stats--buttons');
+ if ( ! subcontainer )
+ container.appendChild(subcontainer = ());
+
+ } else {
+ subcontainer = container.querySelector('.ffz-sidebar-stats--stats');
+ if ( ! subcontainer ) {
+ subcontainer = ();
+ const btns = container.querySelector('.ffz-sidebar-stats--buttons');
+ if ( btns )
+ container.insertBefore(subcontainer, btns);
+ else
+ container.appendChild(subcontainer);
+ }
+ }
+
+ el._ffz_order = order;
+
+ if ( order != null )
+ el.style.order = order;
+
+ subcontainer.appendChild(el);
+
+ if ( def.tooltip ) {
+ const parent = document.body.querySelector('body #root,body');
+ el.tooltip = new Tooltip(parent, el, {
+ logger: this.log,
+ live: false,
+ html: true,
+ content: () => el.tip_content,
+ onShow: (t, tip) => el.tip = tip,
+ onHide: () => el.tip = null,
+ popper: {
+ placement: 'top',
+ modifiers: {
+ flip: {
+ behavior: ['bottom', 'top']
+ },
+ preventOverflow: {
+ boundariesElement: parent
+ }
+ }
+ }
+ });
+ }
+
+
+ } else {
+ stat = el.querySelector('.ffz-sidebar-stat--label');
+ sub_el = el.querySelector('.ffz-sidebar-stat--subtitle')
+ old_color = el.dataset.color || '';
+
+ if ( el._ffz_order !== order )
+ el.style.order = el._ffz_order = order;
+
+ if ( el.tip_content !== tooltip ) {
+ el.tip_content = tooltip;
+ if ( el.tip )
+ el.tip.element.innerHTML = tooltip;
+ }
+ }
+
+ if ( old_color !== color )
+ el.dataset.color = el.style.color = color;
+
+ el._ffz_data = data;
+
+ setChildren(stat, label, true);
+ setChildren(sub_el, subtitle, true);
+
+ if ( def.disabled !== undefined )
+ el.disabled = maybe_call(def.disabled, this, data);
+
+ } catch(err) {
+ this.log.capture(err, {
+ tags: {
+ metadata: key
+ }
+ });
+ this.log.error(`Error rendering metadata for ${key}`, err);
+ return destroy();
+ }
+ }
+
+
+ async renderLegacy(key, data, container, timers, refresh_fn) {
+ if ( timers[key] )
+ clearTimeout(timers[key]);
+
+ let el = container.querySelector(`.ffz-stat[data-key="${key}"]`);
+
const def = this.definitions[key],
destroy = () => {
if ( el ) {
diff --git a/src/sites/twitch-twilight/modules/channel.js b/src/sites/twitch-twilight/modules/channel.js
index 2d18a4ff..609e311d 100644
--- a/src/sites/twitch-twilight/modules/channel.js
+++ b/src/sites/twitch-twilight/modules/channel.js
@@ -5,7 +5,7 @@
// ============================================================================
import Module from 'utilities/module';
-import { has } from 'utilities/object';
+import { get, has } from 'utilities/object';
import Twilight from 'site';
@@ -44,9 +44,8 @@ export default class Channel extends Module {
this.ChannelPage = this.fine.define(
'channel-page',
- n => n.getHostedChannelLogin && n.handleHostingChange,
- //n => n.hostModeFromGraphQL,
- ['user']
+ n => (n.getHostedChannelLogin && n.handleHostingChange) || n.hostModeFromGraphQL,
+ ['user', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
);
this.RaidController = this.fine.define(
@@ -68,17 +67,12 @@ export default class Channel extends Module {
});
this.ChannelPage.on('update', inst => {
- if ( this.settings.get('channel.hosting.enable') )
+ if ( this.settings.get('channel.hosting.enable') || has(inst.state, 'hostMode') )
return;
// We can't do this immediately because the player state
// occasionally screws up if we do.
setTimeout(() => {
- /*if ( inst.state.hostMode ) {
- inst.ffzExpectedHost = inst.state.hostMode;
- inst.ffzOldSetState({hostMode: null});
- }*/
-
const current_channel = inst.props.data && inst.props.data.variables && inst.props.data.variables.currentChannelLogin;
if ( current_channel && current_channel !== inst.state.videoPlayerSource ) {
inst.ffzExpectedHost = inst.state.videoPlayerSource;
@@ -134,26 +128,40 @@ export default class Channel extends Module {
if ( inst._ffz_hosting_wrapped )
return;
- const t = this;
+ const t = this,
+ new_style = has(inst.state, 'hostMode');
+
+ inst.ffzGetChannel = () => {
+ const params = inst.props.match.params
+ if ( ! params )
+ return get('props.data.variables.currentChannelLogin', inst)
+
+ return params.channelName || params.channelLogin
+ }
inst.ffzOldSetState = inst.setState;
inst.setState = function(state, ...args) {
try {
- if ( ! t.settings.get('channel.hosting.enable') ) {
- if ( has(state, 'isHosting') )
- state.isHosting = false;
+ if ( new_style ) {
+ if ( has(state, 'hostMode') ) {
+ t.log.info('update-host', state.hostMode)
- if ( has(state, 'videoPlayerSource') )
- state.videoPlayerSource = inst.props.match.params.channelName;
- }
-
- /*if ( has(state, 'hostMode') ) {
- inst.ffzExpectedHost = state.hostMode;
- if ( state.hostMode && ! t.settings.get('channel.hosting.enable') ) {
- state.hostMode = null;
- state.videoPlayerSource = inst.props.match.params.channelName;
+ inst.ffzExpectedHost = state.hostMode;
+ if ( state.hostMode && ! t.settings.get('channel.hosting.enable') ) {
+ state.hostMode = null;
+ state.videoPlayerSource = inst.ffzGetChannel();
+ }
}
- }*/
+
+ } else {
+ if ( ! t.settings.get('channel.hosting.enable') ) {
+ if ( has(state, 'isHosting') )
+ state.isHosting = false;
+
+ if ( has(state, 'videoPlayerSource') )
+ state.videoPlayerSource = inst.ffzGetChannel();
+ }
+ }
} catch(err) {
t.log.capture(err, {extra: {props: inst.props, state}});
@@ -164,34 +172,41 @@ export default class Channel extends Module {
inst._ffz_hosting_wrapped = true;
- inst.ffzOldGetHostedLogin = inst.getHostedChannelLogin;
- inst.getHostedChannelLogin = function() {
- return t.settings.get('channel.hosting.enable') ?
- inst.ffzOldGetHostedLogin() : null;
- }
+ if ( new_style ) {
+ const hosted = inst.ffzExpectedHost = inst.state.hostMode;
+ t.log.info('Expected Host', hosted);
- inst.ffzOldHostHandler = inst.handleHostingChange;
- inst.handleHostingChange = function(channel) {
- inst.ffzExpectedHost = channel;
- if ( t.settings.get('channel.hosting.enable') )
- return inst.ffzOldHostHandler(channel);
- }
+ if ( hosted && ! this.settings.get('channel.hosting.enable') )
+ inst.ffzOldSetState({
+ hostMode: null,
+ videoPlayerSource: inst.props.match.params.channelName
+ });
- // Store the current state and disable the current host if needed.
- inst.ffzExpectedHost = inst.state.isHosting ? inst.state.videoPlayerSource : null;
- if ( ! this.settings.get('channel.hosting.enable') )
- inst.ffzOldHostHandler(null);
+ } else {
+ inst.ffzOldGetHostedLogin = inst.getHostedChannelLogin;
+ inst.getHostedChannelLogin = function() {
+ return t.settings.get('channel.hosting.enable') ?
+ inst.ffzOldGetHostedLogin() : null;
+ }
+
+ inst.ffzOldHostHandler = inst.handleHostingChange;
+ inst.handleHostingChange = function(channel) {
+ inst.ffzExpectedHost = channel;
+ if ( t.settings.get('channel.hosting.enable') )
+ return inst.ffzOldHostHandler(channel);
+ }
+
+ // Store the current state and disable the current host if needed.
+ inst.ffzExpectedHost = inst.state.isHosting ? inst.state.videoPlayerSource : null;
+ if ( ! this.settings.get('channel.hosting.enable') )
+ inst.ffzOldHostHandler(null);
+ }
// Finally, we force an update so that any child components
// receive our updated handler.
inst.forceUpdate();
- /*const hosted = inst.ffzExpectedHost = inst.state.hostMode;
- if ( hosted && ! this.settings.get('channel.hosting.enable') )
- inst.ffzOldSetState({
- hostMode: null,
- videoPlayerSource: inst.props.match.params.channelName
- });*/
+
}
@@ -200,15 +215,17 @@ export default class Channel extends Module {
val = this.settings.get('channel.hosting.enable');
for(const inst of this.ChannelPage.instances) {
- inst.ffzOldHostHandler(val ? inst.ffzExpectedHost : null);
+ if ( has(inst.state, 'hostMode') ) {
+ const host = val ? inst.ffzExpectedHost : null,
+ target = host && host.hostedChannel && host.hostedChannel.login || inst.ffzGetChannel();
- /*const host = val ? inst.ffzExpectedHost : null,
- target = host && host.hostedChannel && host.hostedChannel.login || inst.props.match.params.channelName;
+ inst.ffzOldSetState({
+ hostMode: host,
+ videoPlayerSource: target
+ });
- inst.ffzOldSetState({
- hostMode: host,
- videoPlayerSource: target
- });*/
+ } else
+ inst.ffzOldHostHandler(val ? inst.ffzExpectedHost : null);
}
}
}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/modules/channel_bar.js b/src/sites/twitch-twilight/modules/channel_bar.js
index 5a4dce7d..c7d51f39 100644
--- a/src/sites/twitch-twilight/modules/channel_bar.js
+++ b/src/sites/twitch-twilight/modules/channel_bar.js
@@ -9,7 +9,6 @@ import {deep_copy} from 'utilities/object';
import CHANNEL_QUERY from './channel_bar_query.gql';
-
export default class ChannelBar extends Module {
constructor(...args) {
super(...args);
@@ -21,27 +20,19 @@ export default class ChannelBar extends Module {
this.inject('metadata');
this.inject('socket');
- this.apollo.registerModifier('ChannelPage_ChannelHeader', CHANNEL_QUERY);
- this.apollo.registerModifier('ChannelPage_ChannelHeader', data => {
+ this.apollo.registerModifier('ChannelPage_User', CHANNEL_QUERY);
+ /*this.apollo.registerModifier('ChannelPage_User', data => {
const u = data && data.data && data.data.user;
if ( u ) {
const o = u.profileViewCount = new Number(u.profileViewCount || 0);
o.data = deep_copy(u);
}
- }, false);
-
+ }, false);*/
this.ChannelBar = this.fine.define(
'channel-bar',
- n => n.getTitle && n.getGame && n.renderGame,
- ['user']
- );
-
-
- this.HostBar = this.fine.define(
- 'host-container',
- n => n.handleReportHosterClick,
- ['user']
+ n => n.renderChannelMetadata && n.renderTitleInfo,
+ ['user', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
)
}
@@ -54,16 +45,6 @@ export default class ChannelBar extends Module {
for(const inst of instances)
this.updateChannelBar(inst);
});
-
-
- /*this.HostBar.on('unmount', this.unmountHostBar, this);
- this.HostBar.on('mount', this.updateHostBar, this);
- this.HostBar.on('update', this.updateHostBar, this);
-
- this.HostBar.ready((cls, instances) => {
- for(const inst of instances)
- this.updateHostBar(inst);
- });*/
}
@@ -99,11 +80,13 @@ export default class ChannelBar extends Module {
updateMetadata(inst, keys) {
const container = this.fine.getChildNode(inst),
- metabar = container && container.querySelector && container.querySelector('.channel-info-bar__action-container > .tw-flex');
+ wrapper = container && container.querySelector && container.querySelector('.side-nav-channel-info__info-wrapper > .tw-pd-t-05');
- if ( ! inst._ffz_mounted || ! metabar )
+ if ( ! inst._ffz_mounted || ! wrapper )
return;
+ const metabar = wrapper;
+
if ( ! keys )
keys = this.metadata.keys;
else if ( ! Array.isArray(keys) )
@@ -112,7 +95,7 @@ export default class ChannelBar extends Module {
const timers = inst._ffz_meta_timers = inst._ffz_meta_timers || {},
refresh_func = key => this.updateMetadata(inst, key),
data = {
- channel: inst.props.userData && inst.props.userData.user,
+ channel: inst.props.data && inst.props.data.user,
hosting: false,
_inst: inst
}
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss
index 91de4410..0b386b93 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-width.scss
@@ -8,7 +8,9 @@ body .channel-page__video-player--theatre-mode {
}
body .video-watch-page__right-column,
-body .channel-page__right-column {
+body .channel-page__right-column,
+body .right-column,
+body .channel-root__right-column {
width: var(--ffz-chat-width);
}
diff --git a/src/sites/twitch-twilight/modules/legacy_channel_bar.js b/src/sites/twitch-twilight/modules/legacy_channel_bar.js
new file mode 100644
index 00000000..b290f48d
--- /dev/null
+++ b/src/sites/twitch-twilight/modules/legacy_channel_bar.js
@@ -0,0 +1,124 @@
+'use strict';
+
+// ============================================================================
+// Channel Bar
+// ============================================================================
+
+import Module from 'utilities/module';
+import {deep_copy} from 'utilities/object';
+
+import CHANNEL_QUERY from './channel_bar_query.gql';
+
+
+export default class LegacyChannelBar extends Module {
+ constructor(...args) {
+ super(...args);
+
+ this.should_enable = true;
+
+ this.inject('site.fine');
+ this.inject('site.apollo');
+ this.inject('metadata');
+ this.inject('socket');
+
+ this.apollo.registerModifier('ChannelPage_ChannelHeader', CHANNEL_QUERY);
+ this.apollo.registerModifier('ChannelPage_ChannelHeader', data => {
+ const u = data && data.data && data.data.user;
+ if ( u ) {
+ const o = u.profileViewCount = new Number(u.profileViewCount || 0);
+ o.data = deep_copy(u);
+ }
+ }, false);
+
+
+ this.ChannelBar = this.fine.define(
+ 'legacy-channel-bar',
+ n => n.getTitle && n.getGame && n.renderGame,
+ ['user']
+ );
+
+
+ this.HostBar = this.fine.define(
+ 'legacy-host-container',
+ n => n.handleReportHosterClick,
+ ['user']
+ )
+ }
+
+ onEnable() {
+ this.ChannelBar.on('unmount', this.unmountChannelBar, this);
+ this.ChannelBar.on('mount', this.updateChannelBar, this);
+ this.ChannelBar.on('update', this.updateChannelBar, this);
+
+ this.ChannelBar.ready((cls, instances) => {
+ for(const inst of instances)
+ this.updateChannelBar(inst);
+ });
+
+
+ /*this.HostBar.on('unmount', this.unmountHostBar, this);
+ this.HostBar.on('mount', this.updateHostBar, this);
+ this.HostBar.on('update', this.updateHostBar, this);
+
+ this.HostBar.ready((cls, instances) => {
+ for(const inst of instances)
+ this.updateHostBar(inst);
+ });*/
+ }
+
+
+ updateChannelBar(inst) {
+ const login = inst.props.channelLogin;
+ if ( login !== inst._ffz_old_login ) {
+ if ( inst._ffz_old_login )
+ this.socket.unsubscribe(inst, `channel.${inst._ffz_old_login}`);
+
+ if ( login )
+ this.socket.subscribe(inst, `channel.${login}`);
+ inst._ffz_old_login = login;
+ }
+
+ this.updateMetadata(inst);
+ }
+
+ unmountChannelBar(inst) {
+ if ( inst._ffz_old_login ) {
+ this.socket.unsubscribe(inst, `channel.${inst._ffz_old_login}`);
+ inst._ffz_old_login = null;
+ }
+
+ const timers = inst._ffz_meta_timers;
+ if ( timers )
+ for(const key in timers)
+ if ( timers[key] )
+ clearTimeout(timers[key]);
+
+ inst._ffz_meta_timers = null;
+ }
+
+
+ updateMetadata(inst, keys) {
+ const container = this.fine.getChildNode(inst),
+ metabar = container && container.querySelector && container.querySelector('.channel-info-bar__action-container > .tw-flex');
+
+ if ( ! inst._ffz_mounted || ! metabar )
+ return;
+
+ if ( ! keys )
+ keys = this.metadata.keys;
+ else if ( ! Array.isArray(keys) )
+ keys = [keys];
+
+ const timers = inst._ffz_meta_timers = inst._ffz_meta_timers || {},
+ refresh_func = key => this.updateMetadata(inst, key),
+ data = {
+ channel: inst.props.userData && inst.props.userData.user,
+ hosting: false,
+ legacy: true,
+ _inst: inst
+ }
+
+ for(const key of keys)
+ this.metadata.renderLegacy(key, data, metabar, timers, refresh_func);
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/styles/channel.scss b/src/sites/twitch-twilight/styles/channel.scss
index 7f249ea8..bee101d0 100644
--- a/src/sites/twitch-twilight/styles/channel.scss
+++ b/src/sites/twitch-twilight/styles/channel.scss
@@ -36,8 +36,30 @@
}
}
+.ffz-sidebar-stats {
+ margin-top: .5rem;
+ margin-right: -1rem;
+
+ & + .ffz-sidebar-stats {
+ margin-top: 0 !important;
+ }
+
+ .ffz-sidebar-stat {
+ min-width: calc(50% - 1rem);
+ margin: 0 1rem 1rem 0;
+ }
+}
+
+.ffz-has-stat-arrow {
+ border-top-right-radius: 0 !important;
+ border-bottom-right-radius: 0 !important;
+}
+
+
.ffz-stat-arrow {
border-left: none !important;
+ border-top-left-radius: 0 !important;
+ border-bottom-left-radius: 0 !important;
}