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.