1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-02 17:18:31 +00:00
FrankerFaceZ/src/sites/twitch-twilight/modules/chat/scroller.js
SirStendec bd11a6f2aa 4.0.0-rc21
* Added: In-Line Chat Actions can now be set to display with specific modifier keys being held. This feature currently requires that Freeze Chat Scrolling is enabled. (Though, why you'd want to use this without that feature is beyond me.)
* Fixed: Bug with custom chat width in theater mode.
* Fixed: The `Get Bits` button appearing when disabled.
* Fixed: Issue with highlighting chat usernames when using Firefox.
* Fixed: Misc minor bugs.
2019-05-16 14:46:26 -04:00

597 lines
No EOL
17 KiB
JavaScript

'use strict';
// ============================================================================
// Chat Scroller
// ============================================================================
import {createElement} from 'utilities/dom';
import Twilight from 'site';
import Module from 'utilities/module';
import {IS_FIREFOX} from 'utilities/constants';
export default class Scroller extends Module {
constructor(...args) {
super(...args);
this.inject('settings');
this.inject('i18n');
this.inject('chat');
this.inject('site.fine');
this.inject('site.web_munch');
this.ChatScroller = this.fine.define(
'chat-scroller',
n => n.saveScrollRef && n.handleScrollEvent,
Twilight.CHAT_ROUTES
);
this.settings.add('chat.scroller.freeze', {
default: 0,
ui: {
path: 'Chat > Behavior >> Scrolling @{"description": "Please note that FrankerFaceZ is dependant on Twitch\'s own scrolling code working correctly. There are bugs with Twitch\'s scrolling code that have existed for more than six months. If you are using Firefox, Edge, or other non-Webkit browsers, expect to have issues."}',
title: 'Freeze Chat Scrolling',
description: 'Automatically stop chat from scrolling when moving the mouse over it or holding a key.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'On Hover'},
{value: 2, title: 'When Ctrl is Held'},
{value: 3, title: 'When Meta is Held'},
{value: 4, title: 'When Alt is Held'},
{value: 5, title: 'When Shift is Held'},
{value: 6, title: 'Ctrl or Hover'},
{value: 7, title: 'Meta or Hover'},
{value: 8, title: 'Alt or Hover'},
{value: 9, title: 'Shift or Hover'}
]
}
});
this.settings.add('chat.scroller.smooth-scroll', {
default: 0,
ui: {
path: 'Chat > Behavior >> Scrolling',
title: 'Smooth Scrolling',
description: 'Smoothly slide new chat messages into view. Speed will increase as necessary to keep up with chat.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Slow'},
{value: 2, title: 'Medium'},
{value: 3, title: 'Fast'},
{value: 4, title: 'Very Fast'}
]
}
});
}
updateUseKeys() {
const old_use = this.use_keys;
this.use_keys = false;
for(const act of this.chat.context.get('chat.actions.inline'))
if ( act && act.display && act.display.keys )
this.use_keys = true;
if ( this.use_keys !== old_use ) {
for(const inst of this.ChatScroller.instances)
inst && inst.ffzUpdateKeys && inst.ffzUpdateKeys();
}
}
onEnable() {
this.on('i18n:update', () => {
for(const inst of this.ChatScroller.instances)
inst.ffzUpdateText();
});
this.freeze = this.chat.context.get('chat.scroller.freeze');
this.chat.context.on('changed:chat.scroller.freeze', val => {
this.freeze = val;
for(const inst of this.ChatScroller.instances) {
inst.ffzDisableFreeze();
if ( val !== 0 )
inst.ffzEnableFreeze();
}
});
this.chat.context.on('changed:chat.actions.inline', this.updateUseKeys, this);
this.updateUseKeys();
this.smoothScroll = this.chat.context.get('chat.scroller.smooth-scroll');
this.chat.context.on('changed:chat.scroller.smooth-scroll', val => {
this.smoothScroll = val;
for(const inst of this.ChatScroller.instances)
inst.ffzSetSmoothScroll(val);
});
this.ChatScroller.ready((cls, instances) => {
const t = this,
old_catch = cls.prototype.componentDidCatch,
old_render = cls.prototype.render;
// Try catching errors. With any luck, maybe we can
// recover from the error when we re-build?
cls.prototype.componentDidCatch = function(err, info) {
// Don't log infinitely if stuff gets super screwed up.
const errs = this.state.ffz_errors || 0;
if ( errs < 100 ) {
this.setState({
ffz_errors: errs + 1,
ffz_total_errors: (this.state.ffz_total_errors||0) + 1
});
t.log.capture(err, {extra: info});
t.log.info('Error within Chat', err, info, errs);
}
if ( old_catch )
return old_catch.call(this, err, info);
}
cls.prototype.ffzZeroErrors = function() {
this.setState({ffz_errors: 0});
}
cls.prototype.render = function() {
if ( this.state.ffz_errors > 0 ) {
let timer;
const auto = this.state.ffz_total_errors < 10,
React = t.web_munch.getModule('react'),
createElement = React && React.createElement,
handler = () => {
clearTimeout(timer);
this.ffzZeroErrors();
}
if ( auto )
timer = setTimeout(handler, 250);
if ( ! createElement )
return null;
return createElement('div', {
className: 'tw-border-l tw-c-background-alt-2 tw-c-text-base tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative'
}, [
createElement('div', {className: 'tw-mg-b-1'}, 'There was an error displaying chat.'),
! auto && createElement('button', {
className: 'tw-button',
onClick: handler
}, createElement('span', {className: 'tw-button__text'}, 'Try Again'))
]);
} else
return old_render.call(this);
}
cls.prototype.ffzShouldBeFrozen = function(since) {
if ( since === undefined )
since = Date.now() - this.ffz_last_move;
const f = t.freeze;
return ! this.ffz_outside && (
(this.ffz_ctrl && (f === 2 || f === 6)) ||
(this.ffz_meta && (f === 3 || f === 7)) ||
(this.ffz_alt && (f === 4 || f === 8)) ||
(this.ffz_shift && (f === 5 || f === 9)) ||
(since < 750 && (f === 1 || f > 5))
);
}
cls.prototype.ffzMaybeUnfreeze = function() {
if ( this.ffz_frozen )
requestAnimationFrame(() => {
if ( this.ffz_frozen && ! this.ffzShouldBeFrozen() )
this.ffzUnfreeze();
});
}
cls.prototype.ffzUpdateText = function() {
if ( ! this._ffz_freeze_indicator )
return;
const f = t.freeze,
reason = f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') :
f === 3 ? t.i18n.t('key.meta', 'Meta Key') :
f === 4 ? t.i18n.t('key.alt', 'Alt Key') :
f === 5 ? t.i18n.t('key.shift', 'Shift Key') :
f === 6 ? t.i18n.t('key.ctrl_mouse', 'Ctrl or Mouse') :
f === 7 ? t.i18n.t('key.meta_mouse', 'Meta or Mouse') :
f === 8 ? t.i18n.t('key.alt_mouse', 'Alt or Mouse') :
f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') :
t.i18n.t('key.mouse', 'Mouse Movement');
this._ffz_freeze_indicator.firstElementChild.textContent = t.i18n.t(
'chat.paused',
'(Chat Paused Due to {reason})',
{reason}
);
}
cls.prototype.ffzShowFrozen = function() {
this._ffz_freeze_visible = true;
let el = this._ffz_freeze_indicator;
if ( ! el ) {
const node = t.fine.getChildNode(this);
if ( ! node )
return;
node.classList.add('tw-full-height');
el = this._ffz_freeze_indicator = createElement('div', {
className: 'ffz--freeze-indicator chat-list__more-messages-placeholder tw-relative tw-mg-x-2'
}, createElement('div', {
className: 'chat-list__more-messages tw-bottom-0 tw-full-width tw-align-items-center tw-flex tw-justify-content-center tw-absolute tw-pd-05'
}));
this.ffzUpdateText();
node.appendChild(el);
} else
el.classList.remove('tw-hide');
}
cls.prototype.ffzHideFrozen = function() {
this._ffz_freeze_visible = false;
if ( this._ffz_freeze_indicator )
this._ffz_freeze_indicator.classList.add('tw-hide');
}
cls.prototype.ffzFreeze = function() {
if ( ! this._ffz_interval )
this._ffz_interval = setInterval(() => {
if ( ! this.ffzShouldBeFrozen() )
this.ffzMaybeUnfreeze();
}, 200);
this.ffz_frozen = true;
this.setState({ffzFrozen: true});
//this.ffzShowFrozen();
}
cls.prototype.ffzUnfreeze = function() {
if ( this._ffz_interval ) {
clearInterval(this._ffz_interval);
this._ffz_interval = null;
}
this.ffz_frozen = false;
this.setState({ffzFrozen: false});
if ( this.state.isAutoScrolling )
this.scrollToBottom();
//this.ffzHideFrozen();
}
cls.prototype.smoothScrollBottom = function() {
if ( this._ffz_smooth_animation )
cancelAnimationFrame(this._ffz_smooth_animation);
this.ffz_is_smooth_scrolling = true;
// Step setting value is # pixels to scroll per 10ms.
// 1 is pretty slow, 2 medium, 3 fast, 4 very fast.
let step = this.ffz_smooth_scroll,
old_time = Date.now();
const scroll_content = this.scroll.scrollContent;
if ( ! scroll_content )
return;
const target_top = scroll_content.scrollHeight - scroll_content.clientHeight,
difference = target_top - scroll_content.scrollTop;
// If we are falling behind speed us up
if ( difference > scroll_content.clientHeight ) {
// we are a full scroll away, just jump there
step = difference;
} else if ( difference > 200 ) {
// we are starting to fall behind, speed it up a bit
step += step * Math.floor(difference / 200);
}
const smoothAnimation = () => {
if ( this.state.ffzFrozen || ! this.state.isAutoScrolling )
return this.ffz_is_smooth_scrolling = false;
// See how much time has passed to get a step based off the delta
const current_time = Date.now(),
delta = current_time - old_time,
current_step = step * (delta / 10);
// we need to move at least one full pixel for scrollTop to do anything in this delta.
if ( current_step >= 1 ) {
const scroll_top = scroll_content.scrollTop,
target_top = scroll_content.scrollHeight - scroll_content.clientHeight;
old_time = current_time;
if ( scroll_top < target_top ) {
scroll_content.scrollTop = scroll_top + current_step;
this._ffz_smooth_animation = requestAnimationFrame(smoothAnimation);
} else {
// We've reached the bottom.
scroll_content.scrollTop = target_top;
this.ffz_is_smooth_scrolling = false;
}
} else {
// The frame happened so quick since last update that we haven't moved a full pixel.
// Just wait.
this._ffz_smooth_animation = requestAnimationFrame(smoothAnimation);
}
}
smoothAnimation();
}
cls.prototype.ffzInstallHandler = function() {
if ( this._ffz_handleScroll )
return;
const t = this;
this._doScroll = function() {
if ( ! t.ffz_freeze_enabled || ! t.state.ffzFrozen ) {
if ( t.ffz_smooth_scroll )
t.smoothScrollBottom();
else
t._old_scroll();
}
}
this._old_scroll = this.scrollToBottom;
this.scrollToBottom = function() {
if ( this._ffz_animation )
cancelAnimationFrame(this._ffz_animation);
this._ffz_animation = requestAnimationFrame(t._doScroll);
}
this._ffz_handleScroll = this.handleScrollEvent;
this.handleScrollEvent = function(e) {
// If we're frozen because of FFZ, do not allow a mouse click to update
// the auto-scrolling state. That just gets annoying.
if ( e.type === 'mousedown' && t.ffz_frozen )
return;
if ( t.scroll && e.type === 'touchmove' ) {
t.scroll.scrollContent.scrollHeight - t.scroll.scrollContent.scrollTop - t.scroll.scrollContent.offsetHeight <= 10 ? t.setState({
isAutoScrolling: !0
}) : t.setState({
isAutoScrolling: !1
})
}
return t._ffz_handleScroll(e);
}
const scroller = this.scroll && this.scroll.scrollContent;
if ( scroller ) {
scroller.removeEventListener('mousedown', this._ffz_handleScroll);
scroller.addEventListener('mousedown', this.handleScrollEvent);
scroller.addEventListener('touchmove', this.handleScrollEvent);
}
}
cls.prototype.ffzEnableFreeze = function() {
const node = t.fine.getChildNode(this);
if ( ! node || this.ffz_freeze_enabled )
return;
this.ffz_freeze_enabled = true;
if ( t.freeze > 1 ) {
document.body.addEventListener('keydown',
this._ffz_key = this.ffzKey.bind(this));
document.body.addEventListener('keyup', this._ffz_key);
}
node.addEventListener('mousemove',
this._ffz_mousemove = this.ffzMouseMove.bind(this));
node.addEventListener('mouseleave',
this._ffz_mouseleave = this.ffzMouseLeave.bind(this));
}
cls.prototype.ffzDisableFreeze = function() {
this.ffz_freeze_enabled = false;
if ( this.ffz_frozen )
this.ffzUnfreeze();
if ( this._ffz_outside ) {
clearTimeout(this._ffz_outside);
this._ffz_outside = null;
}
const node = t.fine.getChildNode(this);
if ( ! node )
return;
this._ffz_freeze_visible = false;
if ( this._ffz_freeze_indicator ) {
this._ffz_freeze_indicator.remove();
this._ffz_freeze_indicator = null;
}
if ( this._ffz_key ) {
document.body.removeEventListener('keyup', this._ffz_key);
document.body.removeEventListener('keydown', this._ffz_key);
this._ffz_key = null;
}
if ( this._ffz_mousemove ) {
node.removeEventListener('mousemove', this._ffz_mousemove);
this._ffz_mousemove = null;
}
if ( this._ffz_mouseleave ) {
node.removeEventListener('mouseleave', this._ffz_mouseleave);
this._ffz_mouseleave = null;
}
}
cls.prototype.ffzKey = function(e) {
if (e.altKey === this.ffz_alt &&
e.shiftKey === this.ffz_shift &&
e.ctrlKey === this.ffz_ctrl &&
e.metaKey === this.ffz_meta)
return;
this.ffz_alt = e.altKey;
this.ffz_shift = e.shiftKey;
this.ffz_ctrl = e.ctrlKey;
this.ffz_meta = e.metaKey;
this.ffzUpdateKeys();
if ( this.ffz_outside || t.freeze < 2 )
return;
const should_freeze = this.ffzShouldBeFrozen(),
changed = should_freeze !== this.ffz_frozen;
if ( changed )
if ( should_freeze )
this.ffzFreeze();
else
this.ffzUnfreeze();
}
cls.prototype.ffzUpdateKeys = function() {
if ( ! this._ffz_key_update )
this._ffz_key_update = requestAnimationFrame(() => this.ffz_updateKeys());
}
cls.prototype.ffz_updateKeys = function() {
cancelAnimationFrame(this._ffz_key_update);
this._ffz_key_update = null;
if ( ! t.use_keys && this.ffz_use_keys === t.use_keys )
return;
if ( ! this.scroll || ! this.scroll.root )
return;
this.ffz_use_keys = t.use_keys;
this.scroll.root.classList.toggle('ffz--keys', t.use_keys);
const ds = this.scroll.root.dataset;
if ( ! t.use_keys ) {
delete ds.alt;
delete ds.ctrl;
delete ds.shift;
delete ds.meta;
} else {
ds.alt = ! this.ffz_outside && this.ffz_alt;
ds.ctrl = ! this.ffz_outside && this.ffz_ctrl;
ds.shift = ! this.ffz_outside && this.ffz_shift;
ds.meta = ! this.ffz_outside && this.ffz_meta;
}
}
cls.prototype.ffzMouseMove = function(e) {
this.ffz_last_move = Date.now();
const was_outside = this.ffz_outside;
this.ffz_outside = false;
if ( this._ffz_outside ) {
clearTimeout(this._ffz_outside);
this._ffz_outside = null;
}
// If nothing of interest has happened, stop.
if (e.altKey === this.ffz_alt &&
e.shiftKey === this.ffz_shift &&
e.ctrlKey === this.ffz_ctrl &&
e.metaKey === this.ffz_meta &&
e.screenY === this.ffz_sy &&
e.screenX === this.ffz_sx) {
if ( was_outside )
this.ffzUpdateKeys();
return;
}
this.ffz_alt = e.altKey;
this.ffz_shift = e.shiftKey;
this.ffz_ctrl = e.ctrlKey;
this.ffz_meta = e.metaKey;
this.ffz_sy = e.screenY;
this.ffz_sx = e.screenX;
this.ffzUpdateKeys();
const should_freeze = this.ffzShouldBeFrozen(),
changed = should_freeze !== this.ffz_frozen;
if ( changed )
if ( should_freeze )
this.ffzFreeze();
else
this.ffzUnfreeze();
}
cls.prototype.ffzMouseLeave = function() {
this.ffz_outside = true;
if ( this._ffz_outside )
clearTimeout(this._ffz_outside);
this._ffz_outside = setTimeout(() => this.ffzMaybeUnfreeze(), 64);
this.ffzUpdateKeys();
}
cls.prototype.ffzSetSmoothScroll = function(value) {
this.ffz_smooth_scroll = value;
}
for(const inst of instances)
this.onMount(inst);
});
this.ChatScroller.on('mount', this.onMount, this);
this.ChatScroller.on('unmount', this.onUnmount, this);
this.ChatScroller.on('update', inst => {
const should_show = inst.ffz_freeze_enabled && inst.state.ffzFrozen && inst.state.isAutoScrolling,
changed = should_show !== inst._ffz_freeze_visible;
if ( changed )
if ( should_show )
inst.ffzShowFrozen();
else
inst.ffzHideFrozen();
});
}
onMount(inst) {
inst.ffzInstallHandler();
if ( this.freeze !== 0 )
inst.ffzEnableFreeze();
inst.ffzSetSmoothScroll(this.smoothScroll);
}
onUnmount(inst) { // eslint-disable-line class-methods-use-this
inst.ffzDisableFreeze();
}
}