diff --git a/package.json b/package.json
index 90030300..bcca9326 100755
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
- "version": "4.56.3",
+ "version": "4.57.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx
index ba910f34..810b7115 100644
--- a/src/modules/chat/badges.jsx
+++ b/src/modules/chat/badges.jsx
@@ -10,6 +10,7 @@ import {createElement, ManagedStyle} from 'utilities/dom';
import {has, maybe_call, SourcedSet} from 'utilities/object';
import Module from 'utilities/module';
import { ColorAdjuster } from 'src/utilities/color';
+import { NoContent } from 'src/utilities/tooltip';
const CSS_BADGES = {
1: {
@@ -194,11 +195,7 @@ export default class Badges extends Module {
// objects when we don't need to do so.
this.bulk = new Map;
- // Special data structure for supporters to greatly reduce
- // memory usage and speed things up for people who only have
- // a supporter badge.
- //this.supporter_id = null;
- //this.supporters = new Set;
+ this._woofer_months = {};
this.badges = {};
this.twitch_badges = {};
@@ -316,6 +313,7 @@ export default class Badges extends Module {
tcon = [],
game = [],
ffz = [],
+ specific_addons = {},
addon = [];
const twitch_keys = Object.keys(this.twitch_badges);
@@ -374,40 +372,104 @@ export default class Badges extends Module {
}
}
- if ( include_addons )
- for(const key in this.badges)
- if ( has(this.badges, key) ) {
- const badge = this.badges[key];
- if ( badge.no_visibility )
- continue;
+ if ( include_addons ) {
+ const addon_badges_by_id = {};
- let image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image,
- color = badge.color || 'transparent';
+ for(const [key, badge] of Object.entries(this.badges)) {
+ if ( badge.no_visibility )
+ continue;
- if ( ! badge.addon ) {
- image = `//cdn.frankerfacez.com/badge/${badge.id}/2/rounded`;
- color = 'transparent';
+ let image = badge.urls ? (badge.urls[2] || badge.urls[1]) : badge.image,
+ image1x = badge.urls?.[1] || badge.image,
+ color = badge.color || 'transparent';
+
+ if ( ! badge.addon ) {
+ image = `//cdn.frankerfacez.com/badge/${badge.id}/2/rounded`;
+ image1x = `//cdn.frankerfacez.com/badge/${badge.id}/1/rounded`;
+ color = 'transparent';
+ }
+
+ let store;
+ if ( typeof badge.addon === 'string' )
+ store = specific_addons[badge.addon] = specific_addons[badge.addon] || [];
+ else
+ store = badge.addon ? addon : ffz;
+
+ const id = badge.base_id ?? key,
+ is_this = id === key;
+ let existing = addon_badges_by_id[id];
+
+ if ( existing ) {
+ if ( ! existing.versions )
+ existing.versions = [{
+ version: existing.key,
+ name: existing.name,
+ color: existing.color,
+ image: existing.image1x,
+ styleImage: `url("${existing.image1x}")`
+ }];
+
+ existing.versions.push({
+ version: key,
+ name: badge.title,
+ color,
+ image: image1x,
+ styleImage: `url("${image1x}")`
+ });
+
+ if ( is_this ) {
+ existing.name = badge.title;
+ existing.color = color;
+ existing.image = image;
+ existing.styleImage = `url("${image}")`;
}
- (badge.addon ? addon : ffz).push({
- id: key,
+ } else {
+ existing = {
+ id,
+ key,
provider: 'ffz',
name: badge.title,
color,
image,
+ image1x,
styleImage: `url("${image}")`
- });
- }
+ };
- return [
+ addon_badges_by_id[id] = existing;
+ store.push(existing);
+ }
+ }
+ }
+
+ const out = [
{title: 'Twitch', id: 'm-twitch', badges: twitch},
{title: 'Twitch: TwitchCon', id: 'm-tcon', badges: tcon},
{title: 'Twitch: Other', id: 'm-other', badges: other},
{title: 'Twitch: Overwatch League', id: 'm-owl', badges: owl},
- {title: 'Twitch: Game', id: 'm-game', key: 'game', badges: game},
- {title: 'FrankerFaceZ', id: 'm-ffz', badges: ffz},
- {title: 'Add-on', id: 'm-addon', badges: addon}
+ {title: 'Twitch: Game', id: 'm-game', key: 'game', badges: game}
];
+
+ if ( ffz.length )
+ out.push({title: 'FrankerFaceZ', id: 'm-ffz', badges: ffz});
+
+ const addons = this.resolve('addons'),
+ addon_chunks = [];
+
+ for(const [key, val] of Object.entries(specific_addons)) {
+ const addon = addons?.getAddon?.(key),
+ title = addon?.short_name ?? addon?.name ?? key;
+
+ addon_chunks.push({title: `Add-On: ${title}`, id: `m-addon-${key}`, badges: val});
+ }
+
+ addon_chunks.sort((a,b) => a.title.localeCompare(b.title));
+ out.push(...addon_chunks);
+
+ if ( addon.length )
+ out.push({title: 'Add-on', id: 'm-addon', badges: addon});
+
+ return out;
}
@@ -436,10 +498,11 @@ export default class Badges extends Module {
const show_previews = this.parent.context.get('tooltip.badge-images');
const ds = this.getBadgeData(target);
- const out = [];
-
if ( ds.data == null )
- return out;
+ return NoContent;
+
+ const out = [];
+ let promises = false;
for(const d of ds.data) {
const p = d.provider;
@@ -479,23 +542,72 @@ export default class Badges extends Module {
);
} else if ( p === 'ffz' ) {
- out.push(
- {show_previews && d.image &&
}
- {d.title}
-
);
+ const badge = this.badges[d.id],
+ extra = maybe_call(badge?.tooltipExtra, this, ds, d, target, tip);
+
+ if ( extra instanceof Promise ) {
+ promises = true;
+ out.push(extra.then(stuff => (
+ {show_previews && d.image &&
}
+ {d.title}{stuff||''}
+
)));
+
+ } else
+ out.push(
+ {show_previews && d.image &&
}
+ {d.title}{extra||''}
+
);
}
}
+ if ( promises )
+ return Promise.all(out);
return out;
}
}
+ // ========================================================================
+ // Add-On Proxy
+ // ========================================================================
+
+ getAddonProxy(module) {
+ const path = module.__path;
+ if ( ! path.startsWith('addon.') )
+ return this;
+
+ const addon_id = path.slice(6);
+
+ const loadBadgeData = (badge_id, data, ...args) => {
+ if ( data && data.addon === undefined )
+ data.addon = addon_id;
+
+ return this.loadBadgeData(badge_id, data, ...args);
+ };
+
+ const handler = {
+ get(obj, prop) {
+ if ( prop === 'loadBadgeData' )
+ return loadBadgeData;
+ return Reflect.get(...arguments);
+ }
+ };
+
+ return new Proxy(this, handler);
+ }
+
+
getBadgeData(target) {
let container = target.parentElement?.parentElement;
@@ -638,9 +750,6 @@ export default class Badges extends Module {
is_colored = badge_style !== 5,
has_image = badge_style !== 3 && badge_style !== 4,
- ffz_hidden = hidden_badges['m-ffz'],
- addon_hidden = hidden_badges['m-addon'],
-
tb = this.twitch_badges,
slotted = new Map,
@@ -729,9 +838,15 @@ export default class Badges extends Module {
handled_ids.add(badge.id);
const full_badge = this.badges[badge.id] || {},
- is_hidden = hidden_badges[badge.id];
+ cat = typeof full_badge.addon === 'string'
+ ? `m-addon-${full_badge.addon}`
+ : full_badge.addon
+ ? 'm-addon'
+ : 'm-ffz',
+ hide_key = badge.base_id ?? badge.id,
+ is_hidden = hidden_badges[hide_key];
- if ( is_hidden || (is_hidden == null && (full_badge.addon ? addon_hidden : ffz_hidden)) )
+ if ( is_hidden || (is_hidden == null && hidden_badges[cat]) )
continue;
const slot = has(badge, 'slot') ? badge.slot : full_badge.slot,
@@ -744,6 +859,7 @@ export default class Badges extends Module {
bu = (urls || full_badge.urls || {1: full_badge.image}),
bd = {
provider: 'ffz',
+ id: badge.id,
image: bu[4] || bu[2] || bu[1],
color: badge.color || full_badge.color,
title: badge.title || full_badge.title,
@@ -1047,6 +1163,20 @@ export default class Badges extends Module {
if ( ! data.addon && (data.name === 'developer' || data.name === 'subwoofer' || data.name === 'supporter') )
data.click_url = 'https://www.frankerfacez.com/subscribe';
+
+ if ( ! data.addon && (data.name === 'subwoofer') )
+ data.tooltipExtra = async data => {
+ const d = await this.getSubwooferMonths(data.user_id);
+ if ( ! d?.months )
+ return;
+
+ if ( d.lifetime )
+ return '\n' + this.i18n.t('badges.subwoofer.lifetime', 'Lifetime Subwoofer');
+
+ return '\n' + this.i18n.t('badges.subwoofer.months', '({count, plural, one {# Month} other {# Months}})', {
+ count: d.months
+ });
+ };
}
if ( generate_css )
@@ -1054,6 +1184,48 @@ export default class Badges extends Module {
}
+ getSubwooferMonths(user_id) {
+ let info = this._woofer_months[user_id];
+ if ( info instanceof Promise )
+ return info;
+
+ const expires = info?.expires;
+ if ( expires && Date.now() >= expires )
+ info = this._woofer_months[user_id] = null;
+
+ if ( info?.value )
+ return Promise.resolve(info.value);
+
+ return this._woofer_months[user_id] = fetch(`https://api.frankerfacez.com/v1/_user/id/${user_id}`)
+ .then(resp => resp.ok ? resp.json() : null)
+ .then(data => {
+ let out = null;
+ if ( data?.user?.sub_months )
+ out = {
+ months: data.user.sub_months,
+ lifetime: data.user.sub_lifetime
+ };
+
+ this._woofer_months[user_id] = {
+ expires: Date.now() + (5 * 60 * 1000),
+ value: out
+ };
+
+ return out;
+ })
+ .catch(err => {
+ console.error('Error getting subwoofer data for user', user_id, err);
+
+ this._woofer_months[user_id] = {
+ expires: Date.now() + (60 * 1000),
+ value: null
+ };
+
+ return null;
+ });
+ }
+
+
buildBadgeCSS() {
const style = this.parent.context.get('chat.badges.style'),
is_dark = this.parent.context.get('theme.is-dark'),
diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js
index 75703954..1734b2ec 100644
--- a/src/modules/chat/index.js
+++ b/src/modules/chat/index.js
@@ -1521,6 +1521,13 @@ export default class Chat extends Module {
}
+ iterateMessages(include_chat = true, include_whisper = true, include_video = true) {
+ const messages = [];
+ this.emit('chat:get-messages', include_chat, include_whisper, include_video, messages);
+ return messages;
+ }
+
+
handleLinkClick(event) {
if ( event.ctrlKey || event.shiftKey )
return;
diff --git a/src/modules/main_menu/components/setting-text.vue b/src/modules/main_menu/components/setting-text.vue
new file mode 100644
index 00000000..26a600b4
--- /dev/null
+++ b/src/modules/main_menu/components/setting-text.vue
@@ -0,0 +1,58 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/sites/base.js b/src/sites/base.js
index 9edafd13..b29d66cb 100644
--- a/src/sites/base.js
+++ b/src/sites/base.js
@@ -20,6 +20,31 @@ export default class BaseSite extends Module {
// DOM Manipulation
// ========================================================================
+ getReact() {
+ if ( this._react )
+ return this._react;
+
+ let react;
+ try {
+ react = this.getCore?.()?.intl?.react;
+ } catch(err) { /* no-op */ }
+
+ if ( react?.Component && react.createElement )
+ return this._react = react;
+
+ react = this.resolve('web_munch')?.getModule?.('react');
+ if ( react?.Component && react.createElement )
+ return this._react = react;
+ }
+
+ findReact() {
+ const react = this.getReact();
+ if ( react )
+ return Promise.resolve(react);
+
+ return this.resolve('web_munch').findModule('react');
+ }
+
awaitElement(selector, parent, timeout = 60000) {
if ( ! parent )
parent = document.documentElement;
diff --git a/src/sites/clips/line.jsx b/src/sites/clips/line.jsx
index 94775417..6de51c82 100644
--- a/src/sites/clips/line.jsx
+++ b/src/sites/clips/line.jsx
@@ -56,6 +56,19 @@ export default class Line extends Module {
this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this);
this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this);
+ this.on('chat:get-messages', (include_chat, include_whisper, include_video, messages) => {
+ if ( include_chat )
+ for(const inst of this.ChatLine.instances) {
+ const msg = this.standardizeMessage(inst.props.node, inst.props.video);
+ if ( msg )
+ messages.push({
+ message: msg,
+ _instance: inst,
+ update: () => inst.forceUpdate()
+ });
+ }
+ });
+
this.site = this.resolve('site');
this.ChatLine.ready(cls => {
@@ -86,8 +99,12 @@ export default class Line extends Module {
const user_block = t.chat.formatUser(user, createElement);
const override_name = t.overrides.getName(user.id);
+ let user_class = msg.ffz_user_class;
+ if ( Array.isArray(user_class) )
+ user_class = user_class.join(' ');
+
const user_props = {
- className: `clip-chat__message-author tw-font-size-5 ffz-link notranslate tw-strong${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${msg.ffz_user_class ?? ''}`,
+ className: `clip-chat__message-author tw-font-size-5 ffz-link notranslate tw-strong${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${user_class ?? ''}`,
href: `https://www.twitch.tv/${user.login}/clips`,
style: { color }
};
diff --git a/src/sites/shared/player.jsx b/src/sites/shared/player.jsx
index e634ece2..da129518 100644
--- a/src/sites/shared/player.jsx
+++ b/src/sites/shared/player.jsx
@@ -1918,9 +1918,9 @@ export default class PlayerBase extends Module {
{tip = ()}
);
- let thing = container.querySelector('button[data-a-target="player-theatre-mode-button"]');
- if ( ! thing )
- thing = container.querySelector('button[data-a-target="player-fullscreen-button"]');
+ const thing = container.querySelector('button[data-a-target="player-theatre-mode-button"]') ||
+ container.querySelector('div:not(:has(.tw-tooltip)) button:not([data-a-target])') ||
+ container.querySelector('button[data-a-target="player-fullscreen-button"]');
if ( thing ) {
container.insertBefore(cont, thing.parentElement);
@@ -2022,7 +2022,11 @@ export default class PlayerBase extends Module {
{tip = ()}
);
- const thing = container.querySelector('.ffz--player-pip button') || container.querySelector('button[data-a-target="player-theatre-mode-button"]') || container.querySelector('button[data-a-target="player-fullscreen-button"]');
+ const thing = container.querySelector('.ffz--player-pip button') ||
+ container.querySelector('button[data-a-target="player-theatre-mode-button"]') ||
+ container.querySelector('div:not(:has(.tw-tooltip)) button:not([data-a-target])') ||
+ container.querySelector('button[data-a-target="player-fullscreen-button"]');
+
if ( thing ) {
container.insertBefore(cont, thing.parentElement);
} else
diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js
index 22ca39f8..00be20fb 100644
--- a/src/sites/twitch-twilight/index.js
+++ b/src/sites/twitch-twilight/index.js
@@ -404,9 +404,9 @@ Twilight.ROUTES = {
//'dir-community-index': '/directory/communities',
//'dir-creative': '/directory/creative',
'dir-following': '/directory/following/:category?',
- 'dir-game-index': '/directory/game/:gameName',
- 'dir-game-clips': '/directory/game/:gameName/clips',
- 'dir-game-videos': '/directory/game/:gameName/videos/:filter',
+ 'dir-game-index': '/directory/category/:gameName',
+ 'dir-game-clips': '/directory/category/:gameName/clips',
+ 'dir-game-videos': '/directory/category/:gameName/videos/:filter',
//'dir-game-details': '/directory/game/:gameName/details',
'dir-all': '/directory/all/:filter?',
//'dir-category': '/directory/:category?',
diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
index 20ee86fa..221f2626 100644
--- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
+++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx
@@ -180,7 +180,6 @@ export default class EmoteMenu extends Module {
this.inject('site');
this.inject('site.fine');
this.inject('site.apollo');
- this.inject('site.web_munch');
this.inject('site.css_tweaks');
this.SUB_STATUS = SUB_STATUS;
@@ -435,7 +434,7 @@ export default class EmoteMenu extends Module {
this.css_tweaks.setVariable('emoji-menu--size', 36);
const t = this,
- React = await this.web_munch.findModule('react'),
+ React = await this.site.findReact(),
createElement = React && React.createElement;
if ( ! createElement )
@@ -509,7 +508,7 @@ export default class EmoteMenu extends Module {
defineClasses() {
const t = this,
storage = this.settings.provider,
- React = this.web_munch.getModule('react'),
+ React = this.site.getReact(),
createElement = React && React.createElement;
this.EmojiTonePicker = class FFZEmojiTonePicker extends React.Component {
diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js
index 6baaabf4..95bcf9d6 100644
--- a/src/sites/twitch-twilight/modules/chat/index.js
+++ b/src/sites/twitch-twilight/modules/chat/index.js
@@ -1436,7 +1436,7 @@ export default class ChatHook extends Module {
cls.prototype.render = function() {
if ( this.state.ffz_errors > 0 ) {
- const React = t.web_munch.getModule('react'),
+ const React = t.site.getReact(),
createElement = React && React.createElement;
if ( ! createElement )
@@ -1486,7 +1486,7 @@ export default class ChatHook extends Module {
cls.prototype.render = function() {
try {
if ( t.CommunityStackHandler ) {
- const React = t.web_munch.getModule('react'),
+ const React = t.site.getReact(),
out = this.ffzRender(),
thing = out?.props?.children?.props?.children;
@@ -1702,8 +1702,8 @@ export default class ChatHook extends Module {
return true;
const t = this,
- React = this.web_munch.getModule('react'),
- createElement = React && React.createElement,
+ React = this.site.getReact(),
+ createElement = React?.createElement,
StackMod = this.web_munch.getModule('highlightstack');
if ( ! createElement || ! StackMod )
diff --git a/src/sites/twitch-twilight/modules/chat/input.jsx b/src/sites/twitch-twilight/modules/chat/input.jsx
index 6b1cade8..0beee5fc 100644
--- a/src/sites/twitch-twilight/modules/chat/input.jsx
+++ b/src/sites/twitch-twilight/modules/chat/input.jsx
@@ -68,7 +68,7 @@ export default class Input extends Module {
this.inject('settings');
this.inject('site.fine');
- this.inject('site.web_munch');
+ this.inject('site');
// Settings
@@ -260,7 +260,7 @@ export default class Input extends Module {
}
});
- const React = await this.web_munch.findModule('react'),
+ const React = await this.site.findReact(),
createElement = React && React.createElement;
if ( ! createElement )
@@ -937,8 +937,8 @@ export default class Input extends Module {
return limitResults && results.length > 25 ? results.slice(0, 25) : results;
}
- const React = this.web_munch.getModule('react'),
- createElement = React && React.createElement;
+ const React = this.site.getReact(),
+ createElement = React?.createElement;
inst.renderFFZEmojiSuggestion = function(data) {
return (
diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js
index 3e91f5b1..7769fb73 100644
--- a/src/sites/twitch-twilight/modules/chat/line.js
+++ b/src/sites/twitch-twilight/modules/chat/line.js
@@ -30,7 +30,6 @@ export default class ChatLine extends Module {
this.inject('chat');
this.inject('site');
this.inject('site.fine');
- this.inject('site.web_munch');
this.inject(RichContent);
this.inject('experiments');
@@ -459,6 +458,41 @@ export default class ChatLine extends Module {
this.chat.context.on('changed:tooltip.link-images', this.maybeUpdateLines, this);
this.chat.context.on('changed:tooltip.link-nsfw-images', this.maybeUpdateLines, this);
+ this.on('chat:get-messages', (include_chat, include_whisper, include_video, messages) => {
+ if ( include_chat ) {
+ for(const inst of this.ChatLine.instances) {
+ const msg = inst.props.message;
+ if ( msg )
+ messages.push({
+ message: msg,
+ _instance: inst,
+ update: () => inst.forceUpdate()
+ });
+ }
+
+ for(const inst of this.ExtensionLine.instances) {
+ const msg = inst.props.message;
+ if ( msg )
+ messages.push({
+ message: msg,
+ _instance: inst,
+ update: () => inst.forceUpdate()
+ });
+ }
+ }
+
+ if ( include_whisper )
+ for(const inst of this.WhisperLine.instances) {
+ const msg = inst.props.message;
+ if ( msg && msg._ffz_message )
+ messages.push({
+ message: msg._ffz_message,
+ _instance: inst,
+ update: () => inst.forceUpdate()
+ });
+ }
+ });
+
this.on('chat:get-tab-commands', e => {
if ( this.experiments.getTwitchAssignmentByName('chat_replies') === 'control' )
return;
@@ -539,7 +573,7 @@ export default class ChatLine extends Module {
});
const t = this,
- React = await this.web_munch.findModule('react');
+ React = await this.site.findReact();
if ( ! React )
return;
@@ -934,8 +968,12 @@ other {# messages were deleted by a moderator.}
const username = t.chat.formatUser(user, e),
override_name = t.overrides.getName(user.id);
+ let user_class = msg.ffz_user_class;
+ if ( Array.isArray(user_class) )
+ user_class = user_class.join(' ');
+
const user_props = {
- className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${msg.ffz_user_class ?? ''}`,
+ className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${user_class ?? ''}`,
role: 'button',
style: { color },
onClick: this.ffz_user_click_handler,
@@ -1161,608 +1199,6 @@ other {# messages were deleted by a moderator.}
}
} };
- /*cls.prototype.ffzOldRender = function() { try {
- this._ffz_no_scan = true;
-
- const types = t.parent.message_types || {},
- deleted_count = this.props.deletedCount,
- reply_mode = t.chat.context.get('chat.replies.style'),
- anim_hover = t.chat.context.get('chat.emotes.animated') === 2,
- override_mode = t.chat.context.get('chat.filtering.display-deleted'),
-
- msg = t.chat.standardizeMessage(this.props.message),
- reply_tokens = (reply_mode === 2 || (reply_mode === 1 && this.props.repliesAppearancePreference && this.props.repliesAppearancePreference !== 'expanded')) ? ( msg.ffz_reply = msg.ffz_reply || t.chat.tokenizeReply(this.props.reply) ) : null,
- is_action = msg.messageType === types.Action,
- action_style = is_action ? t.chat.context.get('chat.me-style') : 0,
- action_italic = action_style >= 2,
- action_color = action_style === 1 || action_style === 3,
-
- user = msg.user,
- raw_color = t.overrides.getColor(user.id) || user.color,
-
- color = t.parent.colors.process(raw_color);
-
- let mod_mode = this.props.deletedMessageDisplay;
- let show, show_class, mod_action = null;
-
- const highlight_mode = t.chat.context.get('chat.points.allow-highlight'),
- highlight = highlight_mode > 0 && msg.ffz_type === 'points' && msg.ffz_reward && isHighlightedReward(msg.ffz_reward),
- twitch_highlight = highlight && highlight_mode == 1,
- ffz_highlight = highlight && highlight_mode == 2;
-
- if ( ! this.props.isCurrentUserModerator && mod_mode == 'DETAILED' )
- mod_mode = 'LEGACY';
-
- if ( override_mode )
- mod_mode = override_mode;
-
- if ( mod_mode === 'BRIEF' ) {
- if ( msg.deleted ) {
- if ( deleted_count == null )
- return null;
-
- return e('div', {
- className: 'chat-line__status'
- }, t.i18n.t('chat.deleted-messages', `{count,plural,
-one {One message was deleted by a moderator.}
-other {# messages were deleted by a moderator.}
-}`, {
- count: deleted_count
- }));
- }
-
- show = true;
- show_class = false;
-
- } else if ( mod_mode === 'DETAILED' ) {
- show = true;
- show_class = msg.deleted;
-
- } else {
- show = this.state && this.state.alwaysShowMessage || ! msg.deleted;
- show_class = false;
- }
-
- if ( msg.deleted ) {
- const show_mode = t.chat.context.get('chat.filtering.display-mod-action');
- if ( show_mode === 2 || (show_mode === 1 && mod_mode === 'DETAILED') ) {
- const action = msg.modActionType;
- if ( action === 'timeout' )
- mod_action = t.i18n.t('chat.mod-action.timeout',
- '{duration} Timeout'
- , {
- duration: print_duration(msg.duration || 1)
- });
- else if ( action === 'ban' )
- mod_action = t.i18n.t('chat.mod-action.ban', 'Banned');
- else if ( action === 'delete' || ! action )
- mod_action = t.i18n.t('chat.mod-action.delete', 'Deleted');
-
- if ( mod_action && msg.modLogin )
- mod_action = t.i18n.t('chat.mod-action.by', '{action} by {login}', {
- login: msg.modLogin,
- action: mod_action
- });
-
- if ( mod_action )
- mod_action = e('span', {
- className: 'tw-pd-l-05',
- 'data-test-selector': 'chat-deleted-message-attribution'
- }, `(${mod_action})`);
- }
- }
-
- let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined,
- room_id = msg.roomId ? msg.roomId : this.props.channelID;
-
- 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, true);
- if ( r && r.id )
- room_id = msg.roomId = r.id;
- }
-
- const u = t.site.getUser(),
- r = {id: room_id, login: room};
-
- const has_replies = this.props && !!(this.props.hasReply || this.props.reply || ! this.props.replyRestrictedReason),
- can_replies = has_replies && msg.message && ! msg.deleted && ! this.props.disableReplyClick,
- can_reply = can_replies && (has_replies || (u && u.login !== msg.user?.login));
-
- if ( u ) {
- u.moderator = this.props.isCurrentUserModerator;
- u.staff = this.props.isCurrentUserStaff;
- u.reply_mode = reply_mode;
- u.can_reply = can_reply;
- }
-
- const hover_actions = t.actions.renderHover(msg, this.props.showModerationIcons, u, r, e, this),
- twitch_clickable = hover_actions != null;
-
- const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u),
- rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg),
- bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null;
-
- if ( ! this.ffz_open_reply )
- this.ffz_open_reply = this.ffzOpenReply.bind(this);
-
- if ( ! this.ffz_user_click_handler ) {
- if ( this.props.onUsernameClick )
- this.ffz_user_click_handler = event => {
- if ( this.isKeyboardEvent(event) && event.keyCode !== KEYS.Space && event.keyCode !== KEYS.Enter )
- return;
-
- const target = event.currentTarget,
- ds = target && target.dataset;
- let target_user = user;
-
- if ( ds && ds.user ) {
- try {
- target_user = JSON.parse(ds.user);
- } catch(err) { /* nothing~! * / }
- }
-
- const fe = new FFZEvent({
- inst: this,
- event,
- message: msg,
- user: target_user,
- room: r
- });
-
- t.emit('chat:user-click', fe);
-
- if ( fe.defaultPrevented )
- return;
-
- this.props.onUsernameClick(target_user.login, 'chat_message', msg.id, target.getBoundingClientRect().bottom);
- }
- else
- this.ffz_user_click_handler = this.openViewerCard || this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
- }
-
-
- const user_block = t.chat.formatUser(user, e);
- const override_name = t.overrides.getName(user.id);
-
- const user_props = {
- className: `chat-line__username notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${msg.ffz_user_class ?? ''}`,
- role: 'button',
- style: { color },
- onClick: this.ffz_user_click_handler,
- onContextMenu: t.actions.handleUserContext
- };
-
- if ( msg.ffz_user_props )
- Object.assign(user_props, msg.ffz_user_props);
-
- if ( msg.ffz_user_style )
- Object.assign(user_props.style, msg.ffz_user_style);
-
- const user_bits = [
- t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e, this),
- this.renderInlineHighlight ? this.renderInlineHighlight() : null,
- e('span', {
- className: 'chat-line__message--badges'
- }, t.chat.badges.render(msg, e)),
- e('span', user_props, override_name ? [
- e('span', {
- className: 'chat-author__display-name'
- }, override_name),
- e('div', {
- className: 'ffz-il-tooltip ffz-il-tooltip--down ffz-il-tooltip--align-center'
- }, user_block)
- ] : user_block)
- ];
-
- let extra_ts,
- cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`,
- out = (tokens.length || ! msg.ffz_type) ? [
- (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- user_bits,
- e('span', {'aria-hidden': true}, is_action ? ' ' : ': '),
- show && has_replies && reply_tokens ?
- t.chat.renderTokens(reply_tokens, e)
- : null,
- show ?
- e('span', {
- className:`message ${action_italic ? 'chat-line__message-body--italicized' : ''} ${twitch_highlight ? 'chat-line__message-body--highlighted' : ''}`,
- style: action_color ? { color } : null
- }, t.chat.renderTokens(tokens, e, (reply_mode !== 0 && has_replies) ? this.props.reply : null))
- :
- e('span', {
- className: 'chat-line__message--deleted',
- }, e('a', {
- href: '',
- onClick: this.alwaysShowMessage
- }, t.i18n.t('chat.message-deleted', ''))),
-
- show && rich_content && e(FFZRichContent, rich_content),
-
- mod_action,
- ] : null;
-
- if ( out == null )
- extra_ts = t.chat.context.get('chat.extra-timestamps');
-
- if ( msg.ffz_type === 'sub_mystery' ) {
- const mystery = msg.mystery;
- if ( mystery )
- msg.mystery.line = this;
-
- const sub_msg = t.i18n.tList('chat.sub.gift', "{user} is gifting {count, plural, one {# Tier {tier} Sub} other {# Tier {tier} Subs}} to {channel}'s community! ", {
- user: (msg.sub_anon || user.username === 'ananonymousgifter') ?
- t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
- e('span', {
- role: 'button',
- className: 'chatter-name',
- onClick: this.ffz_user_click_handler
- }, e('span', {
- className: 'tw-c-text-base tw-strong'
- }, user.displayName)),
- count: msg.sub_count,
- tier: SUB_TIERS[msg.sub_plan] || 1,
- channel: msg.roomLogin
- });
-
- if ( msg.sub_total === 1 )
- sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
- else if ( msg.sub_total > 1 )
- sub_msg.push(t.i18n.t('chat.sub.gift-total', "They've gifted {count} Subs in the channel!", {
- count: msg.sub_total
- }));
-
- if ( ! this.ffz_click_expand )
- this.ffz_click_expand = () => {
- this.setState({
- ffz_expanded: ! this.state.ffz_expanded
- });
- }
-
- const expanded = t.chat.context.get('chat.subs.merge-gifts-visibility') ?
- ! this.state.ffz_expanded : this.state.ffz_expanded;
-
- let sub_list = null;
- if( expanded && mystery && mystery.recipients && mystery.recipients.length > 0 ) {
- const the_list = [];
- for(const x of mystery.recipients) {
- if ( the_list.length )
- the_list.push(', ');
-
- the_list.push(e('span', {
- role: 'button',
- className: 'ffz--giftee-name',
- onClick: this.ffz_user_click_handler,
- 'data-user': JSON.stringify(x)
- }, e('span', {
- className: 'tw-c-text-base tw-strong'
- }, x.displayName)));
- }
-
- sub_list = e('div', {
- className: 'tw-mg-t-05 tw-border-t tw-pd-t-05 tw-c-text-alt-2'
- }, the_list);
- }
-
- cls = `ffz-notice-line user-notice-line tw-pd-y-05 ffz--subscribe-line${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`;
- out = [
- e('div', {
- className: 'tw-flex tw-c-text-alt-2',
- onClick: this.ffz_click_expand
- }, [
- t.chat.context.get('chat.subs.compact') ? null :
- e('figure', {
- className: `ffz-i-star${msg.sub_anon ? '-empty' : ''} tw-mg-r-05`
- }),
- e('div', null, [
- out ? null : extra_ts && (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- (out || msg.sub_anon) ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e, this),
- sub_msg
- ]),
- mystery ? e('div', {
- className: 'tw-pd-l-05 tw-font-size-4'
- }, e('figure', {
- className: `ffz-i-${expanded ? 'down' : 'right'}-dir tw-pd-y-1`
- })) : null
- ]),
- sub_list,
- 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)
- ];
-
- } else if ( msg.ffz_type === 'sub_gift' ) {
- const plan = msg.sub_plan || {},
- months = msg.sub_months || 1,
- tier = SUB_TIERS[plan.plan] || 1;
-
- let sub_msg;
-
- const bits = {
- months,
- user: (msg.sub_anon || user.username === 'ananonymousgifter') ?
- t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
- e('span', {
- role: 'button',
- className: 'chatter-name',
- onClick: this.ffz_user_click_handler
- }, e('span', {
- className: 'tw-c-text-base tw-strong'
- }, user.displayName)),
- plan: plan.plan === 'custom' ? '' :
- t.i18n.t('chat.sub.gift-plan', 'Tier {tier}', {tier}),
- recipient: e('span', {
- role: 'button',
- className: 'chatter-name',
- onClick: this.ffz_user_click_handler,
- 'data-user': JSON.stringify(msg.sub_recipient)
- }, e('span', {
- className: 'tw-c-text-base tw-strong'
- }, msg.sub_recipient.displayName))
- };
-
-
- if ( months <= 1 )
- sub_msg = t.i18n.tList('chat.sub.mystery', '{user} gifted a {plan} Sub to {recipient}! ', bits);
- else
- sub_msg = t.i18n.tList('chat.sub.gift-months', '{user} gifted {months, plural, one {# month} other {# months}} of {plan} Sub to {recipient}!', bits);
-
- if ( msg.sub_total === 1 )
- sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
- else if ( msg.sub_total > 1 )
- sub_msg.push(t.i18n.t('chat.sub.gift-total', "They've gifted {count,number} Subs in the channel!", {
- count: msg.sub_total
- }));
-
- cls = `ffz-notice-line user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--subscribe-line${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`;
- out = [
- e('div', {className: 'tw-flex tw-c-text-alt-2'}, [
- t.chat.context.get('chat.subs.compact') ? null :
- e('figure', {
- className: 'ffz-i-star tw-mg-r-05'
- }),
- e('div', null, [
- out ? null : extra_ts && (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- (out || msg.sub_anon) ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e, this),
- sub_msg
- ])
- ]),
- 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)
- ];
-
- } else if ( msg.ffz_type === 'resub' ) {
- const months = msg.sub_cumulative || msg.sub_months,
- setting = t.chat.context.get('chat.subs.show');
-
- if ( setting === 3 || (setting === 1 && out && months > 1) || (setting === 2 && months > 1) ) {
- const plan = msg.sub_plan || {},
- tier = SUB_TIERS[plan.plan] || 1;
-
- const sub_msg = t.i18n.tList('chat.sub.main', '{user} subscribed {plan}. ', {
- user: e('span', {
- role: 'button',
- className: 'chatter-name',
- onClick: this.ffz_user_click_handler
- }, e('span', {
- className: 'tw-c-text-base tw-strong'
- }, user.displayName)),
- plan: plan.prime ?
- t.i18n.t('chat.sub.twitch-prime', 'with Prime Gaming') :
- t.i18n.t('chat.sub.plan', 'at Tier {tier}', {tier})
- });
-
- if ( msg.sub_share_streak && msg.sub_streak > 1 ) {
- sub_msg.push(t.i18n.t(
- 'chat.sub.cumulative-months',
- "They've subscribed for {cumulative,number} months, currently on a {streak,number} month streak!",
- {
- cumulative: msg.sub_cumulative,
- streak: msg.sub_streak
- }
- ));
-
- } else if ( months > 1 ) {
- sub_msg.push(t.i18n.t(
- 'chat.sub.months',
- "They've subscribed for {count,number} months!",
- {
- count: months
- }
- ));
- }
-
- cls = `ffz-notice-line user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--subscribe-line${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`;
- out = [
- e('div', {className: 'tw-flex tw-c-text-alt-2'}, [
- t.chat.context.get('chat.subs.compact') ? null :
- e('figure', {
- className: `ffz-i-${plan.prime ? 'crown' : 'star'} tw-mg-r-05`
- }),
- e('div', null, [
- out ? null : extra_ts && (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- out ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e, this),
- sub_msg
- ])
- ]),
- 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)
- ];
- }
-
- } else if ( msg.ffz_type === 'ritual' && t.chat.context.get('chat.rituals.show') ) {
- let system_msg;
- if ( msg.ritual === 'new_chatter' )
- system_msg = e('div', {className: 'tw-c-text-alt-2'}, [
- t.i18n.tList('chat.ritual', '{user} is new here. Say hello!', {
- user: e('span', {
- role: 'button',
- className: 'chatter-name',
- onClick: this.ffz_user_click_handler
- }, e('span', {
- className: 'tw-c-text-base tw-strong'
- }, user.displayName))
- })
- ]);
-
- if ( system_msg ) {
- cls = `ffz-notice-line user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--ritual-line${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`;
- out = [
- out ? null : extra_ts && (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- system_msg,
- 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)
- ];
- }
-
- } 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-notice-line ffz--points-line tw-pd-l-1 tw-pd-y-05 tw-pd-r-2${ffz_highlight ? ' ffz-custom-color ffz--points-highlight' : ''}${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`;
- out = [
- e('div', {className: 'tw-c-text-alt-2'}, [
- out ? null : extra_ts && (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- out ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e, this),
- 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.displayName))
- })
- ]),
- 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)
- ]
- } else if ( msg.bits > 0 && t.chat.context.get('chat.bits.cheer-notice') ) {
- cls = `ffz-notice-line user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--ritual-line${show_class ? ' ffz--deleted-message' : ''}${twitch_clickable ? ' tw-relative' : ''}`;
- out = [
- e('div', {className: 'tw-c-text-alt-2'}, [
- out ? null : extra_ts && (this.props.showTimestamps || this.props.isHistorical) && e('span', {
- className: 'chat-line__timestamp'
- }, t.chat.formatTime(msg.timestamp)),
- out ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e, this),
- t.i18n.tList('chat.bits-message', 'Cheered {count, plural, one {# Bit} other {# Bits}}', {count: msg.bits || 0})
- ]),
- 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 )
- return null;
-
- if ( twitch_clickable ) {
- out = [
- e('div', {
- className: 'chat-line__message-highlight tw-absolute tw-border-radius-medium tw-top-0 tw-bottom-0 tw-right-0 tw-left-0',
- 'data-test-selector': 'chat-message-highlight'
- }),
- e('div', {
- className: 'chat-line__message-container tw-relative'
- }, reply_mode == 1 ? [
- this.props.repliesAppearancePreference && this.props.repliesAppearancePreference === 'expanded' ? this.renderReplyLine() : null,
- out
- ] : out),
- hover_actions
- ];
- }
-
- return e('div', {
- className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`,
- style: {backgroundColor: bg_css},
- 'data-room-id': room_id,
- 'data-room': room,
- 'data-user-id': user.userID,
- 'data-user': user.userLogin && user.userLogin.toLowerCase(),
- onMouseOver: anim_hover ? t.chat.emotes.animHover : null,
- onMouseOut: anim_hover ? t.chat.emotes.animLeave : null
- }, out);
-
- } catch(err) {
- t.log.info(err);
-
- t.log.capture(err, {
- extra: {
- props: this.props
- }
- });
-
-
- try {
- return old_render.call(this);
- } catch(e2) {
- t.log.error('An error in Twitch rendering.', e2);
- t.log.capture(e2, {
- extra: {
- props: this.props
- }
- });
-
- return 'An error occurred rendering this chat line.';
- }
- } } */
-
- /*cls.prototype.render = this.experiments.get('line_renderer')
- ? cls.prototype.ffzNewRender
- : cls.prototype.ffzOldRender;*/
-
cls.prototype.render = cls.prototype.ffzNewRender;
// Do this after a short delay to hopefully reduce the chance of React
diff --git a/src/sites/twitch-twilight/modules/chat/rich_content.jsx b/src/sites/twitch-twilight/modules/chat/rich_content.jsx
index 3747132b..bf4071d6 100644
--- a/src/sites/twitch-twilight/modules/chat/rich_content.jsx
+++ b/src/sites/twitch-twilight/modules/chat/rich_content.jsx
@@ -15,7 +15,7 @@ export default class RichContent extends Module {
this.inject('chat');
this.inject('i18n');
- this.inject('site.web_munch');
+ this.inject('site');
this.RichContent = null;
this.has_tokenizer = false;
@@ -32,7 +32,7 @@ export default class RichContent extends Module {
async onEnable() {
const t = this,
- React = await this.web_munch.findModule('react');
+ React = await this.site.findReact();
if ( ! React )
return;
diff --git a/src/sites/twitch-twilight/modules/chat/scroller.js b/src/sites/twitch-twilight/modules/chat/scroller.js
index 934716b7..a218a169 100644
--- a/src/sites/twitch-twilight/modules/chat/scroller.js
+++ b/src/sites/twitch-twilight/modules/chat/scroller.js
@@ -24,6 +24,7 @@ export default class Scroller extends Module {
this.inject('settings');
this.inject('i18n');
this.inject('chat');
+ this.inject('site');
this.inject('site.fine');
this.inject('site.web_munch');
@@ -159,8 +160,8 @@ export default class Scroller extends Module {
});
const t = this,
- React = await this.web_munch.findModule('react'),
- createElement = React && React.createElement;
+ React = await this.site.findReact(),
+ createElement = React?.createElement;
if ( ! createElement )
return t.log.warn(`Unable to get React.`);
diff --git a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx
index 6958b5b9..93cd7843 100644
--- a/src/sites/twitch-twilight/modules/chat/settings_menu.jsx
+++ b/src/sites/twitch-twilight/modules/chat/settings_menu.jsx
@@ -17,7 +17,7 @@ export default class SettingsMenu extends Module {
this.inject('chat');
this.inject('chat.badges');
this.inject('site.fine');
- this.inject('site.web_munch');
+ this.inject('site');
this.inject('site.css_tweaks');
this.settings.add('chat.input.hide-identity', {
@@ -53,7 +53,7 @@ export default class SettingsMenu extends Module {
this.css_tweaks.toggle('hide-chat-identity', this.chat.context.get('chat.input.hide-identity'));
const t = this,
- React = await this.web_munch.findModule('react');
+ React = await this.site.findReact();
if ( ! React )
return;
diff --git a/src/sites/twitch-twilight/modules/directory/index.jsx b/src/sites/twitch-twilight/modules/directory/index.jsx
index 92854b11..0f6e5b0e 100644
--- a/src/sites/twitch-twilight/modules/directory/index.jsx
+++ b/src/sites/twitch-twilight/modules/directory/index.jsx
@@ -66,6 +66,12 @@ export default class Directory extends SiteModule {
DIR_ROUTES
);
+ this.DirectorySorter = this.fine.define(
+ 'directory-sorter',
+ n => n.getSortOptionLink && n.getSortOptionText && n.getSortOptionOnClick && n.getFilterIDs,
+ DIR_ROUTES
+ );
+
this.settings.add('directory.hidden.style', {
default: 2,
@@ -268,6 +274,38 @@ export default class Directory extends SiteModule {
changed: () => this.DirectoryLatestVideos.forceUpdate()
});*/
+ this.settings.add('directory.default-sort', {
+ default: false,
+ ui: {
+ path: 'Directory > General >> General',
+ title: 'Force Default Sorting',
+ component: 'setting-select-box',
+ data: [
+ {
+ value: false,
+ title: 'Disabled'
+ },
+ {
+ value: 'RELEVANCE',
+ title: 'Recommended For You'
+ },
+ {
+ value: 'VIEWER_COUNT',
+ title: 'Viewers (High to Low)'
+ },
+ {
+ value: 'VIEWER_COUNT_ASC',
+ title: 'Viewers (Low to High)'
+ },
+ {
+ value: 'RECENT',
+ title: 'Recently Started'
+ }
+ ]
+ },
+ changed: () => this.updateSorting()
+ });
+
this.routeClick = this.routeClick.bind(this);
}
@@ -294,6 +332,9 @@ export default class Directory extends SiteModule {
//this.DirectoryGameCard.on('unmount', this.clearGameCard, this);
this.DirectoryGameCard.each(el => this.updateGameCard(el));
+ this.DirectorySorter.on('mount', this.updateSorting, this);
+ this.DirectorySorter.ready(() => this.updateSorting());
+
const t = this;
this.DirectoryShelf.ready(cls => {
@@ -337,6 +378,29 @@ export default class Directory extends SiteModule {
});
}
+ updateSorting(inst) {
+ if ( ! inst ) {
+ for(const inst of this.DirectorySorter.instances)
+ this.updateSorting(inst);
+ return;
+ }
+
+ const mode = this.settings.get('directory.default-sort');
+ if ( ! mode || mode === inst.state?.activeOption )
+ return;
+
+ const link = inst.getSortOptionLink(mode, false, inst.props);
+ if ( ! link?.props?.linkTo )
+ return;
+
+ // Handle the onClick logic. This sets localStorage values
+ // to restore this sort in the future.
+ if ( link.props.onClick )
+ link.props.onClick();
+
+ // And follow the generated link.
+ this.router.history.push(link.props.linkTo);
+ }
updateGameCard(el) {
const react = this.fine.getReactInstance(el);
diff --git a/src/sites/twitch-twilight/modules/loadable.jsx b/src/sites/twitch-twilight/modules/loadable.jsx
index b919ad1c..aeb3688a 100644
--- a/src/sites/twitch-twilight/modules/loadable.jsx
+++ b/src/sites/twitch-twilight/modules/loadable.jsx
@@ -15,7 +15,7 @@ export default class Loadable extends Module {
this.inject('settings');
this.inject('site.fine');
- this.inject('site.web_munch');
+ this.inject('site');
this.LoadableComponent = this.fine.define(
'loadable-component',
@@ -74,7 +74,7 @@ export default class Loadable extends Module {
if ( t.overrides.has(type) ) {
let cmp = this.state.Component;
if ( typeof cmp === 'function' && ! cmp.ffzWrapped ) {
- const React = t.web_munch.getModule('react'),
+ const React = t.site.getReact(),
createElement = React && React.createElement;
if ( createElement ) {
diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx
index f59b60bc..8a37862e 100644
--- a/src/sites/twitch-twilight/modules/video_chat/index.jsx
+++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx
@@ -44,7 +44,6 @@ export default class VideoChatHook extends Module {
this.inject('site');
this.inject('site.router');
this.inject('site.fine');
- this.inject('site.web_munch');
this.inject('chat');
this.inject('chat.emotes');
@@ -116,6 +115,21 @@ export default class VideoChatHook extends Module {
for(const setting of UPDATE_BADGE_SETTINGS)
this.chat.context.on(`changed:${setting}`, this.updateLineBadges, this);
+ this.on('chat:get-messages', (include_chat, include_whisper, include_video, messages) => {
+ if ( include_video )
+ for(const inst of this.VideoChatLine.instances) {
+ const context = inst.props.messageContext;
+ if ( ! context.comment?._ffz_message )
+ continue;
+
+ messages.push({
+ message: context.comment._ffz_message,
+ _instance: inst,
+ update: () => inst.forceUpdate()
+ });
+ }
+ });
+
this.VideoChatController.on('mount', this.chatMounted, this);
this.VideoChatController.on('unmount', this.chatUnmounted, this);
this.VideoChatController.on('receive-props', this.chatUpdated, this);
@@ -127,7 +141,7 @@ export default class VideoChatHook extends Module {
});
const t = this,
- React = await this.web_munch.findModule('react');
+ React = await this.site.findReact();
if ( ! React )
return;
@@ -265,8 +279,12 @@ export default class VideoChatHook extends Module {
const user_block = t.chat.formatUser(user, createElement);
const override_name = t.overrides.getName(user.id);
+ let user_class = msg.ffz_user_class;
+ if ( Array.isArray(user_class) )
+ user_class = user_class.join(' ');
+
const user_props = {
- className: `video-chat__message-author notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${msg.ffz_user_class ?? ''}`,
+ className: `video-chat__message-author notranslate${override_name ? ' ffz--name-override tw-relative ffz-il-tooltip__container' : ''} ${user_class ?? ''}`,
'data-test-selector': 'comment-author-selector',
href: `/${user.login}`,
rel: 'noopener noreferrer',
diff --git a/src/utilities/addon.js b/src/utilities/addon.js
index 8b6bb34b..37f17a3c 100644
--- a/src/utilities/addon.js
+++ b/src/utilities/addon.js
@@ -4,17 +4,12 @@ export class Addon extends Module {
constructor(...args) {
super(...args);
+ this.addon_root = this;
+
this.inject('i18n');
this.inject('settings');
}
- __processModule(module, name) {
- if ( module.getAddonProxy )
- return module.getAddonProxy(this);
-
- return module;
- }
-
static register(id, info) {
if ( typeof id === 'object' ) {
info = id;
diff --git a/src/utilities/compat/fine-router.js b/src/utilities/compat/fine-router.js
index 3224f793..3ba4bcbb 100644
--- a/src/utilities/compat/fine-router.js
+++ b/src/utilities/compat/fine-router.js
@@ -47,16 +47,19 @@ export default class FineRouter extends Module {
this.log.debug('New Location', location);
const host = window.location.host,
path = location.pathname,
+ search = location.search,
state = location.state;
- if ( path === this.location && host === this.domain && deep_equals(state, this.current_state) )
+ if ( path === this.location && host === this.domain && search === this.search && deep_equals(state, this.current_state) )
return;
this.old_location = this.location;
+ this.old_search = this.search;
this.old_domain = this.domain;
this.old_state = this.current_state;
this.location = path;
+ this.search = search;
this.domain = host;
this.current_state = state;
diff --git a/src/utilities/module.js b/src/utilities/module.js
index 5ae8e4be..dc350d12 100644
--- a/src/utilities/module.js
+++ b/src/utilities/module.js
@@ -37,6 +37,8 @@ export class Module extends EventEmitter {
this.__modules = parent ? parent.__modules : {};
this.children = {};
+ this.addon_root = parent ? parent.addon_root : null;
+
if ( parent && ! parent.children[this.name] )
parent.children[this.name] = this;
@@ -544,6 +546,14 @@ export class Module extends EventEmitter {
}
+ __processModule(module, name) {
+ if ( this.addon_root && module.getAddonProxy )
+ return module.getAddonProxy(this.addon_root, this);
+
+ return module;
+ }
+
+
inject(name, module, require = true) {
if ( name instanceof Module || name.prototype instanceof Module ) {
require = module != null ? module : true;
diff --git a/src/utilities/pubsub.js b/src/utilities/pubsub.js
index af1de8aa..af1c5881 100644
--- a/src/utilities/pubsub.js
+++ b/src/utilities/pubsub.js
@@ -451,7 +451,8 @@ export default class PubSubClient extends EventEmitter {
if ( ! this._client )
return Promise.resolve();
- const topics = [];
+ const topics = [],
+ batch = [];
for(const topic of this._active_topics) {
if ( this._live_topics.has(topic) )
@@ -469,6 +470,7 @@ export default class PubSubClient extends EventEmitter {
// Make a note, we're subscribing to this topic.
this._live_topics.add(topic);
+ batch.push(topic);
}
}
@@ -476,7 +478,7 @@ export default class PubSubClient extends EventEmitter {
return this._client.subscribe({topicFilter: topics })
.catch(() => {
// If there was an error, we did NOT subscribe.
- for(const topic of topics)
+ for(const topic of batch)
this._live_topics.delete(topic);
// Call sendSubscribes again after a bit.