mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.57.0
* Added: The FFZ Subwoofer badge now displays a user's number of subscribed months in its tool-tip. * Added: Setting to set the default sorting mode of the directory. (Have you tried the Deck add-on?) * Fixed: The location of certain player action buttons was incorrect after Twitch made changes to the player. * Changed: Badges added by add-on are now grouped by add-on in badge visibility settings. This allows users to disable all badges from a given add-on at once, and is just generally nicer to look at. * API Added: `iterateMessages()` method on the `chat` module as an easy way to iterate over all live chat messages, in case existing messages need to be modified without the overhead of tokenization. * API Added: Badges can now be stacked together for visibility purposes, similar to Twitch's native badge versions, by setting a `base_id` on each badge. * API Added: Badges can now display dynamic data on their tool-tip by using a `tooltipExtra` method. This was used to display Subwoofer subscription lengths. * API Added: New setting UI type `setting-text` that can be used to insert arbitrary markdown into settings pages. * API Changed: The `ffz_user_class` special property on messages can be an array instead of a string. * API Fixed: Add-on proxy modules are now correctly used for an add-on's sub-modules.
This commit is contained in:
parent
0eaf1a55be
commit
21ae0059a5
22 changed files with 499 additions and 688 deletions
|
@ -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",
|
||||
|
|
|
@ -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 {
|
|||
</div>);
|
||||
|
||||
} else if ( p === 'ffz' ) {
|
||||
out.push(<div class="ffz-badge-tip">
|
||||
{show_previews && d.image && <div
|
||||
class="preview-image ffz-badge"
|
||||
style={{
|
||||
backgroundColor: d.color,
|
||||
backgroundImage: `url("${d.image}")`
|
||||
}}
|
||||
/>}
|
||||
{d.title}
|
||||
</div>);
|
||||
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 => (<div class="ffz-badge-tip">
|
||||
{show_previews && d.image && <div
|
||||
class="preview-image ffz-badge"
|
||||
style={{
|
||||
backgroundColor: d.color,
|
||||
backgroundImage: `url("${d.image}")`
|
||||
}}
|
||||
/>}
|
||||
{d.title}{stuff||''}
|
||||
</div>)));
|
||||
|
||||
} else
|
||||
out.push(<div class="ffz-badge-tip">
|
||||
{show_previews && d.image && <div
|
||||
class="preview-image ffz-badge"
|
||||
style={{
|
||||
backgroundColor: d.color,
|
||||
backgroundImage: `url("${d.image}")`
|
||||
}}
|
||||
/>}
|
||||
{d.title}{extra||''}
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
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'),
|
||||
|
|
|
@ -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;
|
||||
|
|
58
src/modules/main_menu/components/setting-text.vue
Normal file
58
src/modules/main_menu/components/setting-text.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<template lang="html">
|
||||
<div class="ffz--setting-text" :class="classes">
|
||||
<div v-if="loading" class="tw-align-center tw-pd-05">
|
||||
<h3 class="tw-mg-1 ffz-i-zreknarf loading" />
|
||||
</div>
|
||||
<markdown v-else :source="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {maybe_call} from 'utilities/object';
|
||||
|
||||
export default {
|
||||
props: ['item', 'context'],
|
||||
|
||||
data() {
|
||||
let classes = maybe_call(this.item.classes, this, this.item, this.context);
|
||||
if ( classes instanceof Promise ) {
|
||||
classes.then(classes => {
|
||||
this.classes = classes;
|
||||
}).catch(err => {
|
||||
console.error('Error loading async classes:', err);
|
||||
this.classes = '';
|
||||
});
|
||||
|
||||
classes = '';
|
||||
|
||||
} else if ( ! classes )
|
||||
classes = '';
|
||||
|
||||
const source = maybe_call(this.item.content, this, this.item, this.context);
|
||||
if ( !(source instanceof Promise) )
|
||||
return {
|
||||
loading: false,
|
||||
content: source,
|
||||
classes
|
||||
};
|
||||
|
||||
source.then(content => {
|
||||
this.content = content;
|
||||
this.loading = false;
|
||||
}).catch(err => {
|
||||
console.error('Error loading async content:', err);
|
||||
this.loading = false;
|
||||
this.content = this.t('setting.error', 'An error occurred.');
|
||||
});
|
||||
|
||||
return {
|
||||
loading: true,
|
||||
content: null,
|
||||
classes
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -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;
|
||||
|
|
|
@ -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 }
|
||||
};
|
||||
|
|
|
@ -1918,9 +1918,9 @@ export default class PlayerBase extends Module {
|
|||
{tip = (<div class="ffz-il-tooltip ffz-il-tooltip--align-right ffz-il-tooltip--up" role="tooltip" />)}
|
||||
</div>);
|
||||
|
||||
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 = (<div class="ffz-il-tooltip ffz-il-tooltip--align-right ffz-il-tooltip--up" role="tooltip" />)}
|
||||
</div>);
|
||||
|
||||
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
|
||||
|
|
|
@ -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?',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 )
|
||||
|
|
|
@ -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 (<React.Fragment>
|
||||
|
|
|
@ -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', '<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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.`);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue