diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js index b5694d10..6350d682 100644 --- a/src/sites/twitch-twilight/modules/chat/scroller.js +++ b/src/sites/twitch-twilight/modules/chat/scroller.js @@ -45,6 +45,23 @@ export default class Scroller extends Module { ] } }); + + this.settings.add('chat.scroller.smooth-scroll', { + default: 0, + ui: { + path: 'Chat > Behavior >> General', + title: 'Smooth Scrolling', + description: 'Animates new chat messages into view. Will speed up if 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'} + ] + } + }); } onEnable() { @@ -64,6 +81,15 @@ export default class Scroller extends Module { } }); + 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, @@ -223,6 +249,56 @@ export default class Scroller extends Module { //this.ffzHideFrozen(); } + cls.prototype.smoothScrollBottom = function() { + if(this.state.ffzSmoothAnimation){ + cancelAnimationFrame(this.state.ffzSmoothAnimation); + } + this.isScrollingToBottom = 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; + const scrollContent = this.scroll.scrollContent; + const targetTop = scrollContent.scrollHeight - scrollContent.clientHeight; + const difference = targetTop - scrollContent.scrollTop; + + // If we are falling behind speed us up + if (difference > scrollContent.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 * parseInt(difference / 200, 10); + } + let prevTime = Date.now(); + const smoothAnimation = () => { + if(this.state.ffzFrozen) { + this.isScrollingToBottom = false; + return; + } + // See how much time has passed to get a step based off the delta + const currentTime = Date.now(); + const delta = currentTime - prevTime; + const currentStep = step * (delta / 10); + // we need to move at least one full pixel for scrollTop to do anything in this delta. + if (currentStep >= 1) { + prevTime = currentTime; + if (scrollContent.scrollTop < (scrollContent.scrollHeight - scrollContent.clientHeight)) { + scrollContent.scrollTop += currentStep; + this.state.ffzSmoothAnimation = requestAnimationFrame(smoothAnimation); + } else { + scrollContent.scrollTop = scrollContent.scrollHeight - scrollContent.clientHeight; + this.isScrollingToBottom = false; + } + } else { + // the frame happened so quick since last update we didn't move a full pixel yet. + // should only be possible if the FPS of a browser went over 60fps. + this.state.ffzSmoothAnimation = requestAnimationFrame(smoothAnimation); + } + + } + smoothAnimation(); + } + cls.prototype.ffzInstallHandler = function() { if ( this._ffz_handleScroll ) @@ -231,8 +307,13 @@ export default class Scroller extends Module { const t = this; this._old_scroll = this.scrollToBottom; this.scrollToBottom = function() { - if ( ! this.ffz_freeze_enabled || ! this.state.ffzFrozen ) - return this._old_scroll(); + if ( ! this.ffz_freeze_enabled || ! this.state.ffzFrozen ) { + if (this.ffz_smooth_scroll !== 0) { + this.smoothScrollBottom(); + } else { + this._old_scroll(); + } + } } this._ffz_handleScroll = this.handleScrollEvent; @@ -394,6 +475,11 @@ export default class Scroller extends Module { } + cls.prototype.ffzSetSmoothScroll = function(value) { + this.ffz_smooth_scroll = value; + } + + for(const inst of instances) this.onMount(inst); }); @@ -420,6 +506,8 @@ export default class Scroller extends Module { if ( this.freeze !== 0 ) inst.ffzEnableFreeze(); + + inst.ffzSetSmoothScroll(this.smoothScroll); } onUnmount(inst) { // eslint-disable-line class-methods-use-this