1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-03 17:48:30 +00:00
This is a fairly hefty update, behind the scenes. We've rewritten all code that deals with chat scrolling in order to integrate with Twitch's last batch of changes that made chat pausing accessible for moderators. This update also enables the Add-Ons System for all users.

* Added: Setting to allow you to pause chat with a hot key, even when the mouse isn't hovering over chat.
* Added: Setting to control how long chat remains paused due to move movement.
* Fixed: Chat row backgrounds changing when messages are removed from chat.
* Changed: Integrate with Twitch's own chat pausing code, where it makes sense.
* Changed: Remove the experiment locking down access to the Add-Ons Loader.
* Changed: When using the current time as a cache buster, truncate it to the nearest 5 second interval.
This commit is contained in:
SirStendec 2019-06-03 19:47:41 -04:00
parent 535192d931
commit cebb1c0140
11 changed files with 699 additions and 432 deletions

View file

@ -8,6 +8,7 @@ import Module from 'utilities/module';
import { SERVER } from 'utilities/constants'; import { SERVER } from 'utilities/constants';
import { createElement } from 'utilities/dom'; import { createElement } from 'utilities/dom';
import { timeout, has } from 'utilities/object'; import { timeout, has } from 'utilities/object';
import { getBuster } from 'utilities/time';
const fetchJSON = (url, options) => { const fetchJSON = (url, options) => {
return fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null); return fetch(url, options).then(r => r.ok ? r.json() : null).catch(() => null);
@ -23,7 +24,6 @@ export default class AddonManager extends Module {
this.should_enable = true; this.should_enable = true;
this.inject('experiments');
this.inject('settings'); this.inject('settings');
this.inject('i18n'); this.inject('i18n');
@ -33,9 +33,6 @@ export default class AddonManager extends Module {
} }
async onEnable() { async onEnable() {
if ( ! this.experiments.getAssignment('addons') )
return;
this.settings.addUI('add-ons', { this.settings.addUI('add-ons', {
path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch."}', path: 'Add-Ons @{"description": "Add-Ons are additional modules, often written by other people, that can be loaded automatically by FrankerFaceZ to add new capabilities and behaviors to the extension and Twitch."}',
component: 'addon-list', component: 'addon-list',
@ -107,9 +104,9 @@ export default class AddonManager extends Module {
async loadAddonData() { async loadAddonData() {
const [cdn_data, local_data] = await Promise.all([ const [cdn_data, local_data] = await Promise.all([
fetchJSON(`${SERVER}/script/addons.json?_=${FrankerFaceZ.version_info}`), fetchJSON(`${SERVER}/script/addons.json?_=${getBuster(30)}`),
this.settings.get('addons.dev.server') ? this.settings.get('addons.dev.server') ?
fetchJSON(`https://localhost:8001/script/addons.json?_=${Date.now()}`) : null fetchJSON(`https://localhost:8001/script/addons.json?_=${getBuster()}`) : null
]); ]);
if ( Array.isArray(cdn_data) ) if ( Array.isArray(cdn_data) )
@ -251,7 +248,7 @@ export default class AddonManager extends Module {
document.head.appendChild(createElement('script', { document.head.appendChild(createElement('script', {
id: `ffz-loaded-addon-${addon.id}`, id: `ffz-loaded-addon-${addon.id}`,
type: 'text/javascript', type: 'text/javascript',
src: addon.src || `${addon.dev ? 'https://localhost:8001' : SERVER}/script/addons/${addon.id}/script.js`, src: addon.src || `${addon.dev ? 'https://localhost:8001' : SERVER}/script/addons/${addon.id}/script.js?_=${getBuster(30)}`,
crossorigin: 'anonymous' crossorigin: 'anonymous'
})); }));

View file

@ -7,6 +7,7 @@
import {DEBUG, SERVER} from 'utilities/constants'; import {DEBUG, SERVER} from 'utilities/constants';
import Module from 'utilities/module'; import Module from 'utilities/module';
import {has, deep_copy} from 'utilities/object'; import {has, deep_copy} from 'utilities/object';
import { getBuster } from 'utilities/time';
import Cookie from 'js-cookie'; import Cookie from 'js-cookie';
import SHA1 from 'crypto-js/sha1'; import SHA1 from 'crypto-js/sha1';
@ -75,7 +76,7 @@ export default class ExperimentManager extends Module {
let data; let data;
try { try {
data = await fetch(DEBUG ? EXPERIMENTS : `${SERVER}/script/experiments.json?_=${Date.now()}`).then(r => data = await fetch(DEBUG ? EXPERIMENTS : `${SERVER}/script/experiments.json?_=${getBuster()}`).then(r =>
r.ok ? r.json() : null); r.ok ? r.json() : null);
} catch(err) { } catch(err) {

View file

@ -1,12 +1,4 @@
{ {
"addons": {
"name": "Add-Ons Loader",
"description": "Enable the new Add-Ons system in development.",
"groups": [
{"value": true, "weight": 0},
{"value": false, "weight": 100}
]
},
"api_load": { "api_load": {
"name": "New API Stress Testing", "name": "New API Stress Testing",
"description": "Send duplicate requests to the new API server for load testing.", "description": "Send duplicate requests to the new API server for load testing.",

View file

@ -151,7 +151,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger; FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = { const VER = FrankerFaceZ.version_info = {
major: 4, minor: 1, revision: 2, major: 4, minor: 2, revision: 0,
commit: __git_commit__, commit: __git_commit__,
build: __webpack_hash__, build: __webpack_hash__,
toString: () => toString: () =>

View file

@ -7,6 +7,7 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {SERVER} from 'utilities/constants'; import {SERVER} from 'utilities/constants';
import {has} from 'utilities/object'; import {has} from 'utilities/object';
import { getBuster } from 'utilities/time';
import splitter from 'emoji-regex/es2015/index'; import splitter from 'emoji-regex/es2015/index';
@ -74,7 +75,7 @@ export default class Emoji extends Module {
async loadEmojiData(tries = 0) { async loadEmojiData(tries = 0) {
let data; let data;
try { try {
data = await fetch(`${SERVER}/script/emoji/v2-.json?_${Date.now()}`).then(r => data = await fetch(`${SERVER}/script/emoji/v2-.json?_${getBuster(60)}`).then(r =>
r.ok ? r.json() : null r.ok ? r.json() : null
); );

View file

@ -6,7 +6,7 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {createElement} from 'utilities/dom'; import {createElement} from 'utilities/dom';
import {has, deep_copy} from 'utilities/object'; import {get, has, deep_copy} from 'utilities/object';
import Dialog from 'utilities/dialog'; import Dialog from 'utilities/dialog';
@ -44,6 +44,7 @@ export default class MainMenu extends Module {
this.dialog = new Dialog(() => this.buildDialog()); this.dialog = new Dialog(() => this.buildDialog());
this.has_update = false; this.has_update = false;
this.opened = false; this.opened = false;
this.showing = false;
this.settings.addUI('profiles', { this.settings.addUI('profiles', {
path: 'Data Management @{"sort": 1000, "profile_warning": false} > Profiles @{"profile_warning": false}', path: 'Data Management @{"sort": 1000, "profile_warning": false} > Profiles @{"profile_warning": false}',
@ -139,11 +140,13 @@ export default class MainMenu extends Module {
this.on('i18n:update', this.scheduleUpdate, this); this.on('i18n:update', this.scheduleUpdate, this);
this.dialog.on('show', () => { this.dialog.on('show', () => {
this.showing = true;
this.opened = true; this.opened = true;
this.updateButtonUnseen(); this.updateButtonUnseen();
this.emit('show') this.emit('show')
}); });
this.dialog.on('hide', () => { this.dialog.on('hide', () => {
this.showing = false;
this.emit('hide'); this.emit('hide');
this.destroyDialog(); this.destroyDialog();
}); });
@ -163,6 +166,15 @@ export default class MainMenu extends Module {
} }
requestPage(page) {
const vue = get('_vue.$children.0', this);
if ( vue && vue.navigate )
vue.navigate(page);
else
this._wanted_page = page;
}
getUnseen() { getUnseen() {
const pages = this.getSettingsTree(); const pages = this.getSettingsTree();
if ( ! Array.isArray(pages) ) if ( ! Array.isArray(pages) )
@ -229,9 +241,11 @@ export default class MainMenu extends Module {
root.nav = tree; root.nav = tree;
root.nav_keys = tree.keys; root.nav_keys = tree.keys;
root.currentItem = tree.keys[key] || (this.has_update ? root.currentItem = tree.keys[key] || (this._wanted_page && tree.keys[this._wanted_page]) || (this.has_update ?
tree.keys['home.changelog'] : tree.keys['home.changelog'] :
tree.keys['home']); tree.keys['home']);
this._wanted_page = null;
} }
@ -706,8 +720,9 @@ export default class MainMenu extends Module {
getData() { getData() {
const settings = this.getSettingsTree(), const settings = this.getSettingsTree(),
context = this.getContext(), context = this.getContext(),
current = this.has_update ? settings.keys['home.changelog'] : settings.keys['home']; current = (this._wanted_page && settings.keys[this._wanted_page]) || (this.has_update ? settings.keys['home.changelog'] : settings.keys['home']);
this._wanted_page = null;
this.markSeen(current); this.markSeen(current);
let has_unseen = false; let has_unseen = false;

View file

@ -161,7 +161,8 @@ Twilight.KNOWN_MODULES = {
cookie: n => n && n.set && n.get && n.getJSON && n.withConverter, cookie: n => n && n.set && n.get && n.getJSON && n.withConverter,
'extension-service': n => n.extensionService, 'extension-service': n => n.extensionService,
'chat-types': n => n.b && has(n.b, 'Message') && has(n.b, 'RoomMods'), 'chat-types': n => n.b && has(n.b, 'Message') && has(n.b, 'RoomMods'),
'gql-printer': n => n !== window && n.print 'gql-printer': n => n !== window && n.print,
mousetrap: n => n.bindGlobal && n.unbind && n.handleKey
} }

View file

@ -529,6 +529,11 @@ export default class ChatHook extends Module {
addUpdateHandler: inst.addUpdateHandler, addUpdateHandler: inst.addUpdateHandler,
removeUpdateHandler: inst.removeUpdateHandler, removeUpdateHandler: inst.removeUpdateHandler,
getMessages: inst.getMessages, getMessages: inst.getMessages,
isPaused: inst.isPaused,
setPaused: inst.setPaused,
hasNewerLeft: inst.hasNewerLeft,
loadNewer: inst.loadNewer,
loadNewest: inst.loadNewest,
_ffz_inst: inst _ffz_inst: inst
}); });
} }
@ -656,6 +661,8 @@ export default class ChatHook extends Module {
return; return;
const t = this, const t = this,
old_clear = cls.prototype.clear,
old_flush = cls.prototype.flushRawMessages,
old_mount = cls.prototype.componentDidMount; old_mount = cls.prototype.componentDidMount;
cls.prototype._ffzInstall = function() { cls.prototype._ffzInstall = function() {
@ -862,28 +869,41 @@ export default class ChatHook extends Module {
last_msg.deletedCount = deleted_count; last_msg.deletedCount = deleted_count;
} }
inst.setPaused = function(paused) {
if ( inst.paused === paused )
return;
inst.paused = paused;
inst.getMessages = function() { if ( ! paused ) {
const buf = inst.buffer, inst.slidingWindowEnd = Math.min(inst.buffer.length, t.chat.context.get('chat.scrollback-length'));
size = t.chat.context.get('chat.scrollback-length'), if ( ! inst.props.isBackground )
ct = t.chat_types || CHAT_TYPES, inst.notifySubscribers();
target = buf.length - size; }
if ( target > 0 ) {
let removed = 0, last;
for(let i=0; i < target; i++)
if ( buf[i] && ! NULL_TYPES.includes(ct[buf[i].type]) ) {
removed++;
last = i;
} }
inst.buffer = buf.slice(removed % 2 === 0 ? target : Math.max(target - 10, last)); inst.loadNewer = function() {
} else if ( ! inst.hasNewerLeft() )
// Make a shallow copy of the array because other code expects it to change. return;
inst.buffer = buf.slice(0);
return inst.buffer; const end = Math.min(inst.buffer.length, inst.slidingWindowEnd + 40),
start = Math.max(0, end - t.chat.context.get('chat.scrollback-length'));
inst.clear(inst.buffer.length - start);
inst.slidingWindowEnd = end - start;
if ( ! inst.props.isBackground )
inst.notifySubscribers();
}
inst.loadNewest = function() {
if ( ! inst.hasNewerLeft() )
return;
const max_size = t.chat.context.get('chat.scrollback-length');
inst.clear(max_size);
inst.slidingWindowEnd = Math.min(max_size, inst.buffer.length);
if ( ! inst.props.isBackground )
inst.notifySubscribers();
} }
} }
@ -897,16 +917,63 @@ export default class ChatHook extends Module {
return old_mount.call(this); return old_mount.call(this);
} }
cls.prototype.clear = function(count) {
try {
if ( count == null )
count = 0;
const max_size = t.chat.context.get('chat.scrollback-length');
if ( ! this.isPaused() && count > max_size )
count = max_size;
if ( count <= 0 ) {
this.buffer = [];
this.delayedMessageBuffer = [];
this.paused = false;
} else {
const buffer = this.buffer,
ct = t.chat_types || CHAT_TYPES,
target = buffer.length - count;
if ( target > 0 ) {
let removed = 0, last;
for(let i=0; i < target; i++)
if ( buffer[i] && ! NULL_TYPES.includes(ct[buffer[i].type]) ) {
removed++;
last = i;
}
this.buffer = buffer.slice(removed % 2 === 0 ? target : Math.max(target - 4, last));
} else
this.buffer = this.buffer.slice(0);
if ( this.paused && this.buffer.length >= 900 )
this.setPaused(false);
}
} catch(err) {
t.log.error('Error running clear', err);
return old_clear.call(this, count);
}
}
cls.prototype.flushRawMessages = function() { cls.prototype.flushRawMessages = function() {
try {
const out = [], const out = [],
now = Date.now(), now = Date.now(),
raw_delay = t.chat.context.get('chat.delay'), raw_delay = t.chat.context.get('chat.delay'),
delay = raw_delay === -1 ? this.delayDuration : raw_delay, delay = raw_delay === -1 ? this.delayDuration : raw_delay,
first = now - delay, first = now - delay,
see_deleted = this.shouldSeeBlockedAndDeletedMessages || this.props && this.props.shouldSeeBlockedAndDeletedMessages, see_deleted = this.shouldSeeBlockedAndDeletedMessages || this.props && this.props.shouldSeeBlockedAndDeletedMessages,
has_newer = this.hasNewerLeft(),
paused = this.isPaused(),
max_size = t.chat.context.get('chat.scrollback-length'),
do_remove = t.chat.context.get('chat.filtering.remove-deleted'); do_remove = t.chat.context.get('chat.filtering.remove-deleted');
let changed = false; let added = 0,
buffered = this.slidingWindowEnd,
changed = false;
for(const msg of this.delayedMessageBuffer) { for(const msg of this.delayedMessageBuffer) {
if ( msg.time <= first || ! msg.shouldDelay ) { if ( msg.time <= first || ! msg.shouldDelay ) {
@ -916,15 +983,32 @@ export default class ChatHook extends Module {
this.buffer.push(msg.event); this.buffer.push(msg.event);
changed = true; changed = true;
if ( ! this.paused ) {
if ( this.buffer.length > max_size )
added++;
else
buffered++;
}
} else } else
out.push(msg); out.push(msg);
} }
this.delayedMessageBuffer = out; this.delayedMessageBuffer = out;
if ( changed && ! this.props.isBackground ) if ( changed ) {
this.clear(Math.min(900, this.buffer.length - added));
if ( !(added === 0 && buffered === this.slidingWindowEnd && has_newer === this.hasNewerLeft() && paused === this.isPaused()) ) {
this.slidingWindowEnd = buffered;
if ( ! this.props.isBackground )
this.notifySubscribers(); this.notifySubscribers();
} }
} }
} catch(err) {
t.log.error('Error running flush.', err);
return old_flush.call(this);
}
}
}
sendMessage(room, message) { sendMessage(room, message) {

View file

@ -4,10 +4,16 @@
// Chat Scroller // Chat Scroller
// ============================================================================ // ============================================================================
import {createElement} from 'utilities/dom';
import Twilight from 'site'; import Twilight from 'site';
import Module from 'utilities/module'; import Module from 'utilities/module';
import {IS_FIREFOX} from 'utilities/constants';
const SCROLL_EVENTS = [
'touchmove',
'scroll',
'wheel',
'mousewheel',
'DOMMouseScroll'
];
export default class Scroller extends Module { export default class Scroller extends Module {
constructor(...args) { constructor(...args) {
@ -29,7 +35,7 @@ export default class Scroller extends Module {
default: 0, default: 0,
ui: { 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."}', 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', title: 'Pause Chat Scrolling',
description: 'Automatically stop chat from scrolling when moving the mouse over it or holding a key.', description: 'Automatically stop chat from scrolling when moving the mouse over it or holding a key.',
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
@ -47,6 +53,33 @@ export default class Scroller extends Module {
} }
}); });
this.settings.add('chat.scroller.freeze-requires-hover', {
default: true,
ui: {
path: 'Chat > Behavior >> Scrolling',
title: 'Require the mouse to be over chat to freeze with a hotkey.',
component: 'setting-check-box'
}
});
this.settings.add('chat.scroller.hover-delay', {
default: 750,
ui: {
path: 'Chat > Behavior >> Scrolling',
title: 'Hover Timeout',
description: 'Chat will only remain frozen due to mouse hovering for this long after the mouse stops moving.',
component: 'setting-combo-box',
data: [
{value: 250, title: '0.25 Seconds'},
{value: 500, title: '0.50 Seconds'},
{value: 750, title: '0.75 Seconds'},
{value: 1000, title: '1 Second'},
{value: 2500, title: '2.5 Seconds'},
{value: 5000, title: '5 Seconds'}
]
}
});
this.settings.add('chat.scroller.smooth-scroll', { this.settings.add('chat.scroller.smooth-scroll', {
default: 0, default: 0,
ui: { ui: {
@ -69,8 +102,10 @@ export default class Scroller extends Module {
const old_use = this.use_keys; const old_use = this.use_keys;
this.use_keys = false; this.use_keys = false;
for(const act of this.chat.context.get('chat.actions.inline')) for(const act of this.chat.context.get('chat.actions.inline'))
if ( act && act.display && act.display.keys ) if ( act && act.display && act.display.keys ) {
this.use_keys = true; this.use_keys = true;
break;
}
if ( this.use_keys !== old_use ) { if ( this.use_keys !== old_use ) {
for(const inst of this.ChatScroller.instances) for(const inst of this.ChatScroller.instances)
@ -78,37 +113,53 @@ export default class Scroller extends Module {
} }
} }
onEnable() { async onEnable() {
this.on('i18n:update', () => { this.on('i18n:update', () => this.ChatScroller.forceUpdate());
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.chat.context.on('changed:chat.actions.inline', this.updateUseKeys, this);
this.updateUseKeys(); this.updateUseKeys();
this.smoothScroll = this.chat.context.get('chat.scroller.smooth-scroll'); this.pause_hover = this.chat.context.get('chat.scroller.freeze-requires-hover');
this.chat.context.on('changed:chat.scroller.freeze-requires-hover', val => {
this.pause_hover = val;
for(const inst of this.ChatScroller.instances)
inst.ffzMaybeUnpause();
})
this.pause_delay = this.chat.context.get('chat.scroller.hover-delay');
this.chat.context.on('changed:chat.scroller.hover-delay', val => {
this.pause_delay = val;
for(const inst of this.ChatScroller.instances)
inst.ffzMaybeUnpause();
})
this.pause = this.chat.context.get('chat.scroller.freeze');
this.chat.context.on('changed:chat.scroller.freeze', val => {
this.pause = val;
for(const inst of this.ChatScroller.instances)
inst.ffzMaybeUnpause();
});
this.smooth_scroll = this.chat.context.get('chat.scroller.smooth-scroll');
this.chat.context.on('changed:chat.scroller.smooth-scroll', val => { this.chat.context.on('changed:chat.scroller.smooth-scroll', val => {
this.smoothScroll = val; this.smooth_scroll = val;
for(const inst of this.ChatScroller.instances) for(const inst of this.ChatScroller.instances)
inst.ffzSetSmoothScroll(val); inst.ffzSetSmoothScroll(val);
}); });
this.ChatScroller.ready((cls, instances) => {
const t = this, const t = this,
old_catch = cls.prototype.componentDidCatch, React = await this.web_munch.findModule('react'),
createElement = React && React.createElement;
if ( ! createElement )
return t.log.warn(`Unable to get React.`);
this.ChatScroller.ready((cls, instances) => {
const old_catch = cls.prototype.componentDidCatch,
old_render = cls.prototype.render; old_render = cls.prototype.render;
// Try catching errors. With any luck, maybe we can // Try catching errors. With any luck, maybe we can
@ -138,8 +189,6 @@ export default class Scroller extends Module {
if ( this.state.ffz_errors > 0 ) { if ( this.state.ffz_errors > 0 ) {
let timer; let timer;
const auto = this.state.ffz_total_errors < 10, const auto = this.state.ffz_total_errors < 10,
React = t.web_munch.getModule('react'),
createElement = React && React.createElement,
handler = () => { handler = () => {
clearTimeout(timer); clearTimeout(timer);
this.ffzZeroErrors(); this.ffzZeroErrors();
@ -165,34 +214,314 @@ export default class Scroller extends Module {
return old_render.call(this); return old_render.call(this);
} }
cls.prototype.ffzShouldBeFrozen = function(since) { cls.prototype.ffzInstallHandler = function() {
if ( since === undefined ) if ( this._ffz_installed )
since = Date.now() - this.ffz_last_move; return;
const f = t.freeze; this._ffz_installed = true;
const inst = this;
return ! this.ffz_outside && ( inst.ffz_oldScrollEvent = inst.handleScrollEvent;
(this.ffz_ctrl && (f === 2 || f === 6)) || inst.ffz_oldScroll = inst.scrollToBottom;
(this.ffz_meta && (f === 3 || f === 7)) ||
(this.ffz_alt && (f === 4 || f === 8)) || // New Scroll to Bottom
(this.ffz_shift && (f === 5 || f === 9)) || inst.ffz_doScroll = function() {
(since < 750 && (f === 1 || f > 5)) inst._ffz_scroll_frame = null;
); if ( inst.state.isAutoScrolling && ! inst.state.isPaused ) {
if ( inst.ffz_smooth_scroll && ! inst._ffz_one_fast_scroll )
inst.smoothScrollBottom();
else {
inst._ffz_one_fast_scroll = false;
inst.ffz_oldScroll();
}
}
} }
cls.prototype.ffzMaybeUnfreeze = function() { inst.scrollToBottom = function() {
if ( this.ffz_frozen ) if ( inst._ffz_scroll_frame || inst.state.isPaused )
requestAnimationFrame(() => { return;
if ( this.ffz_frozen && ! this.ffzShouldBeFrozen() )
this.ffzUnfreeze(); this._ffz_scroll_frame = requestAnimationFrame(inst.ffz_doScroll);
}
// New Scroll Event Handling
inst.handleScrollEvent = function(event) {
if ( ! inst.scroll || ! inst.scroll.scrollContent )
return;
// TODO: Check for mousedown?
if ( !(event.which > 0 || event.type === 'mousewheel' || event.type === 'wheel' || event.type === 'touchmove') )
return;
// How far are we scrolled up?
const scroller = inst.scroll.scrollContent,
offset = scroller.scrollHeight - scroller.scrollTop - scroller.offsetHeight;
// If we're less than 10 pixels from the bottom and we aren't autoscrolling, resume
if ( offset <= 10 && ! inst.state.isAutoScrolling )
inst.resume();
// If we are autoscrolling and we're more than 10 pixels up, then
// stop autoscrolling without setting paused.
else if ( inst.state.isAutoScrolling && offset > 10 ) {
// If we're paused, unpause.
if ( inst.state.isPaused ) {
inst.setState({
isPaused: false
}, () => {
if ( inst.props.setPaused )
inst.props.setPaused(false);
});
inst.setLoadMoreEnabled(true);
}
inst.setState({
isAutoScrolling: false
});
}
}
inst.pause = function() {
// If we already aren't scrolling, we don't want to further
// pause things.
if ( ! inst.state.isAutoScrolling )
return;
inst.setState({
isPaused: true
}, () => {
if ( inst.props.setPaused )
inst.props.setPaused(true);
}); });
} }
cls.prototype.ffzUpdateText = function() { const old_resume = inst.resume;
if ( ! this._ffz_freeze_indicator )
inst.ffzFastResume = function() {
inst._ffz_one_fast_scroll = true;
inst.resume();
}
inst.resume = function() {
clearInterval(inst._ffz_hover_timer);
inst._ffz_hover_timer = null;
old_resume.call(inst);
}
// Event Registration
const Mousetrap = t.web_munch.getModule('mousetrap') || window.Mousetrap;
if ( Mousetrap != null ) {
Mousetrap.unbind('alt', 'keydown');
Mousetrap.unbind('alt', 'keyup');
}
inst.ffzHandleKey = inst.ffzHandleKey.bind(inst);
Mousetrap.bindGlobal('alt', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('alt', inst.ffzHandleKey, 'keyup');
Mousetrap.bindGlobal('shift', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('shift', inst.ffzHandleKey, 'keyup');
Mousetrap.bindGlobal('ctrl', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('ctrl', inst.ffzHandleKey, 'keyup');
Mousetrap.bindGlobal('command', inst.ffzHandleKey, 'keydown');
Mousetrap.bindGlobal('command', inst.ffzHandleKey, 'keyup');
inst.hoverPause = inst.ffzMouseMove.bind(inst);
inst.hoverResume = inst.ffzMouseLeave.bind(inst);
const node = t.fine.getChildNode(inst);
if ( node )
node.addEventListener('mousemove', inst.hoverPause);
const scroller = this.scroll && this.scroll.scrollContent;
if ( scroller ) {
for(const event of SCROLL_EVENTS) {
scroller.removeEventListener(event, inst.ffz_oldScrollEvent);
scroller.addEventListener(event, inst.handleScrollEvent);
}
}
// We need to refresh the element to make sure it's using the correct
// event handlers for mouse enter / leave.
inst.forceUpdate();
}
cls.prototype.ffzSetSmoothScroll = function(value) {
this.ffz_smooth_scroll = value;
this.ffzMaybeUnpause();
}
// Event Handling
cls.prototype.ffzReadKeysFromEvent = function(event) {
if ( event.altKey === this.ffz_alt &&
event.shiftKey === this.ffz_shift &&
event.ctrlKey === this.ffz_ctrl &&
event.metaKey === this.ffz_meta )
return false;
this.ffz_alt = event.altKey;
this.ffz_shift = event.shiftKey;
this.ffz_ctrl = event.ctrlKey;
this.ffz_meta = event.metaKey;
return true;
}
cls.prototype.ffzHandleKey = function(event) {
if ( ! this.ffzReadKeysFromEvent(event) )
return; return;
const f = t.freeze, this.ffzUpdateKeyTags();
if ( (t.pause_hover && this.ffz_outside) || t.pause < 2 )
return;
const should_pause = this.ffzShouldBePaused(),
changed = should_pause !== this.state.isPaused;
if ( changed )
if ( should_pause ) {
this.pause();
this.setLoadMoreEnabled(false);
} else
this.resume();
}
cls.prototype.ffzInstallHoverTimer = function() {
if ( this._ffz_hover_timer )
return;
this._ffz_hover_timer = setInterval(() => {
if ( this.state.isPaused && this.ffzShouldBePaused() )
return;
this.ffzMaybeUnpause();
}, 50);
}
cls.prototype.ffzMouseMove = function(event) {
this.ffz_last_move = Date.now();
const was_outside = this.ffz_outside;
this.ffz_outside = false;
if ( this._ffz_outside_timer ) {
clearTimeout(this._ffz_outside_timer);
this._ffz_outside_timer = null;
}
const keys_updated = this.ffzReadKeysFromEvent(event);
// If nothing changed, stop processing.
if ( ! keys_updated && event.screenX === this.ffz_sx && event.screenY === this.ffz_sy ) {
if ( was_outside )
this.ffzUpdateKeyTags();
return;
}
this.ffz_sx = event.screenX;
this.ffz_sy = event.screenY;
if ( keys_updated || was_outside )
this.ffzUpdateKeyTags();
const should_pause = this.ffzShouldBePaused(),
changed = should_pause !== this.state.isPaused;
if ( changed )
if ( should_pause ) {
this.pause();
this.ffzInstallHoverTimer();
this.setLoadMoreEnabled(false);
} else
this.resume();
}
cls.prototype.ffzMouseLeave = function() {
this.ffz_outside = true;
if ( this._ffz_outside_timer )
clearTimeout(this._ffz_outside_timer);
this._ffz_outside_timer = setTimeout(() => this.ffzMaybeUnpause(), 64);
this.ffzUpdateKeyTags();
}
// Keyboard Stuff
cls.prototype.ffzUpdateKeyTags = function() {
if ( ! this._ffz_key_frame )
this._ffz_key_frame = requestAnimationFrame(() => this.ffz_updateKeyTags());
}
cls.prototype.ffz_updateKeyTags = function() {
this._ffz_key_frame = 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;
}
}
// Pause Stuff
cls.prototype.ffzShouldBePaused = function(since) {
if ( since == null )
since = Date.now() - this.ffz_last_move;
const mode = t.pause,
require_hover = t.pause_hover;
return (! require_hover || ! this.ffz_outside) && this.state.isAutoScrolling && (
(this.ffz_ctrl && (mode === 2 || mode === 6)) ||
(this.ffz_meta && (mode === 3 || mode === 7)) ||
(this.ffz_alt && (mode === 4 || mode === 8)) ||
(this.ffz_shift && (mode === 5 || mode === 9)) ||
(! this.ffz_outside && since < t.pause_delay && (mode === 1 || mode > 5))
);
}
cls.prototype.ffzMaybeUnpause = function() {
if ( this.state.isPaused && ! this._ffz_unpause_frame )
this._ffz_unpause_frame = requestAnimationFrame(() => {
this._ffz_unpause_frame = null;
if ( this.state.isPaused && ! this.ffzShouldBePaused() )
this.resume();
});
}
cls.prototype.listFooter = function() {
let msg;
if ( this.state.isPaused ) {
const f = t.pause,
reason = f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') : reason = f === 2 ? t.i18n.t('key.ctrl', 'Ctrl Key') :
f === 3 ? t.i18n.t('key.meta', 'Meta Key') : f === 3 ? t.i18n.t('key.meta', 'Meta Key') :
f === 4 ? t.i18n.t('key.alt', 'Alt Key') : f === 4 ? t.i18n.t('key.alt', 'Alt Key') :
@ -203,66 +532,17 @@ export default class Scroller extends Module {
f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') : f === 9 ? t.i18n.t('key.shift_mouse', 'Shift or Mouse') :
t.i18n.t('key.mouse', 'Mouse Movement'); t.i18n.t('key.mouse', 'Mouse Movement');
this._ffz_freeze_indicator.firstElementChild.textContent = t.i18n.t( msg = t.i18n.t('chat.paused', '(Chat Paused Due to {reason})', {reason});
'chat.paused',
'(Chat Paused Due to {reason})',
{reason}
);
}
cls.prototype.ffzShowFrozen = function() { } else if ( this.state.isAutoScrolling )
this._ffz_freeze_visible = true; return null;
let el = this._ffz_freeze_indicator; else
if ( ! el ) { msg = t.i18n.t('chat.messages-below', 'More messages below.');
const node = t.fine.getChildNode(this);
if ( ! node )
return;
node.classList.add('tw-full-height'); return createElement('div', {
className: 'chat-list__list-footer tw-absolute tw-align-items-center tw-border-radius-medium tw-bottom-0 tw-flex tw-full-width tw-justify-content-center tw-pd-05',
el = this._ffz_freeze_indicator = createElement('div', { onClick: this.ffzFastResume
className: 'ffz--freeze-indicator chat-list__more-messages-placeholder tw-relative tw-mg-x-2' }, createElement('div', null, msg));
}, 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() { cls.prototype.smoothScrollBottom = function() {
@ -294,7 +574,7 @@ export default class Scroller extends Module {
} }
const smoothAnimation = () => { const smoothAnimation = () => {
if ( this.state.ffzFrozen || ! this.state.isAutoScrolling ) if ( this.state.isPaused || ! this.state.isAutoScrolling )
return this.ffz_is_smooth_scrolling = false; return this.ffz_is_smooth_scrolling = false;
// See how much time has passed to get a step based off the delta // See how much time has passed to get a step based off the delta
@ -328,238 +608,7 @@ export default class Scroller extends Module {
smoothAnimation(); smoothAnimation();
} }
// Do the thing~
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) for(const inst of instances)
this.onMount(inst); this.onMount(inst);
@ -567,31 +616,26 @@ export default class Scroller extends Module {
this.ChatScroller.on('mount', this.onMount, this); this.ChatScroller.on('mount', this.onMount, this);
this.ChatScroller.on('unmount', this.onUnmount, 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) { onMount(inst) {
inst.ffzSetSmoothScroll(this.smooth_scroll);
inst.ffzInstallHandler(); inst.ffzInstallHandler();
if ( this.freeze !== 0 )
inst.ffzEnableFreeze();
inst.ffzSetSmoothScroll(this.smoothScroll);
} }
onUnmount(inst) { // eslint-disable-line class-methods-use-this onUnmount() { // eslint-disable-line class-methods-use-this
inst.ffzDisableFreeze(); const Mousetrap = this.web_munch.getModule('mousetrap') || window.Mousetrap;
if ( Mousetrap != null ) {
Mousetrap.unbind('alt', 'keydown');
Mousetrap.unbind('alt', 'keyup');
Mousetrap.unbind('shift', 'keydown');
Mousetrap.unbind('shift', 'keyup');
Mousetrap.unbind('ctrl', 'keydown');
Mousetrap.unbind('ctrl', 'keyup');
Mousetrap.unbind('command', 'keydown');
Mousetrap.unbind('command', 'keyup');
}
} }
} }

View file

@ -7,6 +7,8 @@
import Twilight from 'site'; import Twilight from 'site';
import Module from 'utilities/module'; import Module from 'utilities/module';
import { has } from 'utilities/object';
export default class SettingsMenu extends Module { export default class SettingsMenu extends Module {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -22,10 +24,17 @@ export default class SettingsMenu extends Module {
n => n.renderUniversalOptions && n.onBadgesChanged, n => n.renderUniversalOptions && n.onBadgesChanged,
Twilight.CHAT_ROUTES Twilight.CHAT_ROUTES
); );
this.ModSettingsMenu = this.fine.define(
'chat-mod-settings',
n => n.renderModerationSettingsLink && n.onChatClear,
Twilight.CHAT_ROUTES
);
} }
async onEnable() { async onEnable() {
this.on('i18n:update', () => this.SettingsMenu.forceUpdate()); this.on('i18n:update', () => this.SettingsMenu.forceUpdate());
this.chat.context.on('changed:chat.scroller.freeze', () => this.SettingsMenu.forceUpdate());
const t = this, const t = this,
React = await this.web_munch.findModule('react'); React = await this.web_munch.findModule('react');
@ -34,12 +43,19 @@ export default class SettingsMenu extends Module {
const createElement = React.createElement; const createElement = React.createElement;
this.SettingsMenu.ready((cls, instances) => { this.SettingsMenu.ready(cls => {
const old_universal = cls.prototype.renderUniversalOptions; const old_render = cls.prototype.render,
old_universal = cls.prototype.renderUniversalOptions;
cls.prototype.renderUniversalOptions = function() { cls.prototype.renderUniversalOptions = function() {
const val = old_universal.call(this); const val = old_universal.call(this);
if ( ! this.ffzSettingsClick )
this.ffzSettingsClick = e => t.click(this, e);
if ( ! this.ffzPauseClick )
this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu});
val.props.children.push(<div class="tw-mg-t-1"> val.props.children.push(<div class="tw-mg-t-1">
<button onClick={this.ffzSettingsClick}> <button onClick={this.ffzSettingsClick}>
{t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')} {t.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
@ -51,18 +67,118 @@ export default class SettingsMenu extends Module {
</div>} </div>}
</div>); </div>);
const f = t.chat.context.get('chat.scroller.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.hover', 'Hover');
val.props.children.push(<div class="tw-mg-t-1">
<button
class="tw-block tw-full-width tw-interactable tw-interactable--base tw-interactive"
onClick={this.ffzPauseClick}
>
<div class="tw-flex tw-justify-content-between tw-pd-y-05">
{t.i18n.t('chat.settings.pause', 'Pause Chat')}
<div>
{reason}
<figure class="tw-svg ffz-i-right-dir" />
</div>
</div>
</button>
</div>)
return val; return val;
} }
for(const inst of instances) cls.prototype.render = function() {
inst.ffzSettingsClick = e => t.click(inst, e); try {
if ( this.state.ffzPauseMenu ) {
if ( ! this.ffzSettingsClick )
this.ffzSettingsClick = e => t.click(this, e);
if ( ! this.ffzPauseClick )
this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu});
return (<div class="tw-absolute tw-balloon tw-balloon--up tw-block">
<div class="tw-border tw-border-radius-medium tw-c-background-base tw-elevation-1">
<div class="chat-settings scrollable-area scrollable-area--suppress-scroll-x" data-simplebar>
<div class="chat-settings__content tw-c-background-base tw-c-text-base tw-pd-2">
<button
class="tw-interactive tw-link tw-link--button tw-link--hover-underline-none"
onClick={this.ffzPauseClick}
>
<span class="tw-c-text-link ffz-i-left-dir">
{t.i18n.t('chat.settings.back', 'Back')}
</span>
</button>
<div class="tw-mg-b-1 tw-mg-t-2">
<p class="tw-c-text-alt-2 tw-upcase">
{t.i18n.t('chat.settings.pause', 'Pause Chat')}
</p>
</div>
<p>
{t.i18n.t('chat.settings.pause-explain', 'FrankerFaceZ overrides the behavior of Pause Chat entirely. Please use FFZ\'s Scrolling settings within the FFZ Control Center under Chat > Behavior.')}
</p>
<div class="tw-mg-t-1">
<button
class="tw-button tw-full-width"
data-page="chat.behavior"
onClick={this.ffzSettingsClick}
>
<span class="tw-button__text">
{t.i18n.t('chat.settings.open-settings', 'Open Control Center')}
</span>
</button>
{t.cant_window && <div class="tw-mg-t-05 tw-c-text-alt-2">
<span class="ffz-i-attention">
{t.i18n.t('popup.error', 'We tried opening a pop-up window and could not. Make sure to allow pop-ups from Twitch.')}
</span>
</div>}
</div>
</div>
</div>
</div>
</div>)
}
} catch(err) {
t.log.error('Error rendering chat settings menu.', err);
}
return old_render.call(this);
}
this.SettingsMenu.forceUpdate(); this.SettingsMenu.forceUpdate();
}); });
this.SettingsMenu.on('mount', inst => { this.ModSettingsMenu.ready(cls => {
inst.ffzSettingsClick = e => t.click(inst, e) const old_render = cls.prototype.render;
});
cls.prototype.render = function() {
const out = old_render.call(this);
if ( out.props && Array.isArray(out.props.children) ) {
let i = out.props.children.length;
while(i--) {
const thing = out.props.children[i];
if ( thing && thing.props && has(thing.props, 'chatPauseSetting') ) {
out.props.children.splice(i, 1);
break;
}
}
}
return out;
}
this.ModSettingsMenu.forceUpdate();
})
this.SettingsMenu.on('unmount', inst => { this.SettingsMenu.on('unmount', inst => {
inst.ffzSettingsClick = null; inst.ffzSettingsClick = null;
@ -89,6 +205,17 @@ export default class SettingsMenu extends Module {
} }
} else { } else {
const target = event.currentTarget,
page = target && target.dataset && target.dataset.page,
menu = this.resolve('main_menu');
if ( menu ) {
if ( page )
menu.requestPage(page);
if ( menu.showing )
return;
}
this.emit('site.menu_button:clicked'); this.emit('site.menu_button:clicked');
} }

View file

@ -1,5 +1,10 @@
'use strict'; 'use strict';
export function getBuster(resolution = 5) {
const now = Math.floor(Date.now() / 1000);
return now - (now % resolution);
}
export function duration_to_string(elapsed, separate_days, days_only, no_hours, no_seconds) { export function duration_to_string(elapsed, separate_days, days_only, no_hours, no_seconds) {
const seconds = elapsed % 60; const seconds = elapsed % 60;
let minutes = Math.floor(elapsed / 60), let minutes = Math.floor(elapsed / 60),