1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00
* Added: "Copy Message" chat action for copying a message to your clipboard.
* Added: Setting to pause the player by clicking on it. This is disabled by default, and the pause happens after half a second to avoid pausing as part of a double-click.
* Added: Setting to clear the emote menu's search when closing it.
* Added: Setting to hide the "Elevate Your Message" button in the chat input field.
* Changed: Remove code related to channel hosting.
* Fixed: Do not attempt to load FFZ on `gql` or `passport` subdomains.
* Fixed: Channel leader-boards not being hidden on channels within a specific experiment.
This commit is contained in:
SirStendec 2022-10-07 15:12:15 -04:00
parent bc0eab4409
commit 8cd6545556
16 changed files with 185 additions and 660 deletions

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.36.2",
"version": "4.37.0",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true,
"license": "Apache-2.0",
@ -12,9 +12,9 @@
"dev": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config webpack.web.dev.js",
"dev:prod": "cross-env NODE_OPTIONS=--openssl-legacy-provider webpack-dev-server --config webpack.web.dev.prod.js",
"build": "pnpm build:prod",
"build:stats": "cross-env NODE_ENV=production webpack --config webpack.web.prod.js --json > stats.json",
"build:prod": "cross-env NODE_ENV=production webpack --config webpack.web.prod.js",
"build:dev": "pnpm clean && webpack --config webpack.web.dev.js",
"build:stats": "cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack --config webpack.web.prod.js --json > stats.json",
"build:prod": "cross-env NODE_OPTIONS=--openssl-legacy-provider NODE_ENV=production webpack --config webpack.web.prod.js",
"build:dev": "pnpm clean && cross-env NODE_OPTIONS=--openssl-legacy-provider webpack --config webpack.web.dev.js",
"font": "pnpm font:edit",
"font:edit": "fontello-cli --cli-config fontello.client.json edit",
"font:save": "fontello-cli --cli-config fontello.client.json save && pnpm font:update",

View file

@ -2,7 +2,7 @@
'use strict';
(() => {
// Don't run on certain sub-domains.
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev)\./.test(location.hostname) )
if ( /^(?:localhost\.rig|blog|im|chatdepot|tmi|api|brand|dev|gql|passport)\./.test(location.hostname) )
return;
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev'),

View file

@ -0,0 +1,29 @@
<template lang="html">
<div class="tw-flex tw-align-items-start">
<label for="edit_format" class="tw-mg-y-05">
{{ t('setting.actions.format', 'Format') }}
</label>
<div class="tw-full-width">
<input
id="edit_format"
v-model="value.format"
:placeholder="defaults.format"
class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-input tw-pd-x-1 tw-pd-y-05 tw-mg-y-05"
@input="$emit('input', value)"
>
<div class="tw-c-text-alt-2 tw-mg-b-1">
{{ t('setting.actions.variables', 'Available Variables: {vars}', {vars}) }}
</div>
</div>
</div>
</template>
<script>
export default {
props: ['value', 'defaults', 'vars'],
}
</script>

View file

@ -83,6 +83,51 @@ export const edit_overrides = {
}
// ============================================================================
// Copy to Clipboard
// ============================================================================
export const copy_message = {
presets: [{
appearance: {
type: 'icon',
icon: 'ffz-i-docs'
}
}],
defaults: {
format: '{{user.displayName}}: {{message.text}}'
},
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-copy.vue'),
required_context: ['user', 'message'],
title: 'Copy Message',
description: 'Allows you to quickly copy a chat message to your clipboard.',
can_self: true,
tooltip(data) {
const msg = this.replaceVariables(data.options.format, data);
return [
(<div class="tw-border-b tw-mg-b-05">{ // eslint-disable-line react/jsx-key
this.i18n.t('chat.actions.copy_message', 'Copy Message')
}</div>),
(<div class="tw-align-left">{ // eslint-disable-line react/jsx-key
msg
}</div>)
];
},
click(event, data) {
const msg = this.replaceVariables(data.options.format, data);
navigator.clipboard.writeText(msg);
}
}
// ============================================================================
// Open URL
// ============================================================================

View file

@ -559,6 +559,15 @@ export default class PlayerBase extends Module {
},
changed: val => this.css_tweaks.toggle('player-hide-mouse', val)
});
this.settings.add('player.single-click-pause', {
default: false,
ui: {
path: 'Player > General >> Playback',
title: "Pause/Unpause the player by clicking.",
component: 'setting-check-box'
}
});
}
async onEnable() {
@ -643,8 +652,6 @@ export default class PlayerBase extends Module {
onShortcut(e) {
this.log.info('Compressor Hotkey', e);
for(const inst of this.Player.instances)
this.compressPlayer(inst, e);
}
@ -825,10 +832,14 @@ export default class PlayerBase extends Module {
if ( ! this._ffz_click_handler )
this._ffz_click_handler = this.ffzClickHandler.bind(this);
if ( ! this._ffz_dblclick_handler )
this._ffz_dblclick_handler = this.ffzDblClickHandler.bind(this);
if ( ! this._ffz_menu_handler )
this._ffz_menu_handler = this.ffzMenuHandler.bind(this);
on(cont, 'wheel', this._ffz_scroll_handler);
on(cont, 'dblclick', this._ffz_dblclick_handler);
on(cont, 'mousedown', this._ffz_click_handler);
on(cont, 'contextmenu', this._ffz_menu_handler);
}
@ -853,23 +864,60 @@ export default class PlayerBase extends Module {
this._ffz_menu_handler = null;
}
if ( this._ffz_dblclick_handler ) {
off(cont, 'dblclick', this._ffz_dblclick_handler);
this._ffz_dblclick_handler = null;
}
this._ffz_listeners = false;
}
cls.prototype.ffzDelayPause = function() {
if ( this._ffz_pause_timer )
clearTimeout(this._ffz_pause_timer);
const player = this.props?.mediaPlayerInstance;
if (! player.isPaused())
this._ffz_pause_timer = setTimeout(() => {
const player = this.props?.mediaPlayerInstance;
if (!player.isPaused())
player.pause();
}, 500);
}
cls.prototype.ffzDblClickHandler = function(event) {
if ( ! event )
return;
if ( this._ffz_pause_timer )
clearTimeout(this._ffz_pause_timer);
}
cls.prototype.ffzClickHandler = function(event) {
if ( ! event )
return;
const vol_scroll = t.settings.get('player.volume-scroll'),
gain_scroll = t.settings.get('player.gain.scroll'),
click_pause = t.settings.get('player.single-click-pause'),
wants_rmb = wantsRMB(vol_scroll) || wantsRMB(gain_scroll);
// Left Click
if (click_pause && event.button === 0) {
if (! event.target || ! event.target.classList.contains('click-handler'))
return;
this.ffzDelayPause();
}
// Right Click
if ( wants_rmb && event.button === 2 ) {
this.ffz_rmb = true;
this.ffz_scrolled = false;
}
// Middle Click
if ( ! t.settings.get('player.mute-click') || event.button !== 1 )
return;

View file

@ -79,7 +79,7 @@ export default class Channel extends Module {
changed: () => this.updateLinks()
});
this.settings.add('channel.hosting.enable', {
/*this.settings.add('channel.hosting.enable', {
default: true,
ui: {
path: 'Channel > Behavior >> Hosting',
@ -87,8 +87,7 @@ export default class Channel extends Module {
component: 'setting-check-box'
},
changed: val => ! val && this.InfoBar.each(el => this.updateBar(el))
});
});*/
this.ChannelPanels = this.fine.define(
'channel-panels',
@ -116,7 +115,7 @@ export default class Channel extends Module {
{childNodes: true, subtree: true}, 1
);
const strip_host = resp => {
/*const strip_host = resp => {
if ( this.settings.get('channel.hosting.enable') )
return;
@ -130,7 +129,7 @@ export default class Channel extends Module {
};
this.apollo.registerModifier('UseHosting', strip_host, false);
this.apollo.registerModifier('PlayerTrackingContextQuery', strip_host, false);
this.apollo.registerModifier('PlayerTrackingContextQuery', strip_host, false);*/
}
onEnable() {
@ -162,7 +161,7 @@ export default class Channel extends Module {
this.InfoBar.on('unmount', this.removeBar, this);
this.InfoBar.each(el => this.updateBar(el));
this.subpump.on(':pubsub-message', this.onPubSub, this);
//this.subpump.on(':pubsub-message', this.onPubSub, this);
this.router.on(':route', this.checkNavigation, this);
this.checkNavigation();
@ -230,7 +229,7 @@ export default class Channel extends Module {
}
}
setHost(channel_id, channel_login, target_id, target_login) {
/*setHost(channel_id, channel_login, target_id, target_login) {
const topic = `stream-chat-room-v1.${channel_id}`;
this.subpump.inject(topic, {
@ -272,7 +271,7 @@ export default class Channel extends Module {
event.message.data.num_viewers = 0;
event.markChanged();
}
}
}*/
updateSubscription(login) {
@ -441,8 +440,8 @@ export default class Channel extends Module {
});
}*/
if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin )
this.setHost(props.channelID, props.channelLogin, null, null);
//if ( ! this.settings.get('channel.hosting.enable') && props.hostLogin )
// this.setHost(props.channelID, props.channelLogin, null, null);
this.updateSubscription(props.channelLogin);
this.updateMetadata(el);
@ -492,10 +491,10 @@ export default class Channel extends Module {
live_since: props.liveSince
},
props,
hosted: {
/*hosted: {
login: props.hostLogin,
display_name: props.hostDisplayName
},
},*/
el,
getViewerCount: () => {
const thing = cont.querySelector('p[data-a-target="animated-channel-viewers-count"]'),

View file

@ -188,6 +188,15 @@ export default class EmoteMenu extends Module {
}
});
this.settings.add('chat.emote-menu.clear-search', {
default: false,
ui: {
path: 'Chat > Emote Menu >> General',
title: 'Reset search when closing the Emote Menu.',
component: 'setting-check-box'
}
});
this.settings.add('chat.emote-menu.enabled', {
default: true,
ui: {
@ -1078,6 +1087,7 @@ export default class EmoteMenu extends Module {
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
clearSearch: t.chat.context.get('chat.emote-menu.clear-search'),
tone: t.settings.provider.get('emoji-tone', null)
}
@ -1226,6 +1236,7 @@ export default class EmoteMenu extends Module {
t.chat.context.on('changed:chat.emote-menu.show-heading', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.show-search', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.clear-search', this.updateSettingState, this);
t.chat.context.on('changed:chat.emote-menu.tall', this.updateSettingState, this);
window.ffz_menu = this;
@ -1241,6 +1252,7 @@ export default class EmoteMenu extends Module {
t.chat.context.off('changed:chat.emote-menu.reduced-padding', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.show-search', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.clear-search', this.updateSettingState, this);
t.chat.context.off('changed:chat.emote-menu.tall', this.updateSettingState, this);
if ( window.ffz_menu === this )
@ -1256,6 +1268,7 @@ export default class EmoteMenu extends Module {
reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'),
combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'),
showSearch: t.chat.context.get('chat.emote-menu.show-search'),
clearSearch: t.chat.context.get('chat.emote-menu.clear-search'),
tall: t.chat.context.get('chat.emote-menu.tall')
});
}
@ -2309,6 +2322,13 @@ export default class EmoteMenu extends Module {
return;
}
if ( ! this.props.visible && old_props.visible ) {
if ( this.state.clearSearch ) {
this.setState(this.filterState('', this.state));
return;
}
}
const cd = this.props.channel_data,
old_cd = old_props.channel_data,
cd_diff = cd?.user !== old_cd?.user || cd?.channel !== old_cd?.channel,

View file

@ -201,7 +201,7 @@ export default class ChatHook extends Module {
this.ChatController = this.fine.define(
'chat-controller',
n => n.hostingHandler && n.onRoomStateUpdated,
n => n.parseOutgoingMessage && n.onRoomStateUpdated && n.renderNotifications,
Twilight.CHAT_ROUTES
);
@ -656,6 +656,15 @@ export default class ChatHook extends Module {
component: 'setting-check-box'
}
});
this.settings.add('chat.input.show-elevate-your-message', {
default: true,
ui: {
path: 'Chat > Input >> Appearance',
title: 'Allow the "Elevate Your Message" button to be displayed.',
component: 'setting-check-box'
}
});
}
get currentChat() {
@ -941,6 +950,9 @@ export default class ChatHook extends Module {
this.updateMentionCSS();
});
this.chat.context.getChanges('chat.input.show-elevate-your-message', val =>
this.css_tweaks.toggleHide('elevate-your-message', ! val));
this.updateChatCSS();
this.updateColors();
this.updateLineBorders();
@ -2441,7 +2453,7 @@ export default class ChatHook extends Module {
return old_points.call(i, e);
}
const old_host = this.onHostingEvent;
/*const old_host = this.onHostingEvent;
this.onHostingEvent = function (e, _t) {
t.emit('tmi:host', e, _t);
return old_host.call(i, e, _t);
@ -2451,7 +2463,7 @@ export default class ChatHook extends Module {
this.onUnhostEvent = function (e, _t) {
t.emit('tmi:unhost', e, _t);
return old_unhost.call(i, e, _t);
}
}*/
const old_add = this.addMessage;
this.addMessage = function(e) {

View file

@ -26,6 +26,7 @@ const CLASSES = {
'modview-hide-info': '.modview-player-widget__hide-stream-info',
'community-highlights': '.community-highlight-stack__card',
'elevate-your-message': '.chat-input__input-icons button[aria-label="ElevatedMessage"]',
'prime-offers': '.top-nav__prime',
'discover-luna': '.top-nav__external-link[data-a-target="try-presto-link"]',
@ -38,7 +39,7 @@ const CLASSES = {
'player-event-bar': '.channel-root .live-event-banner-ui__header',
'player-rerun-bar': '.channel-root__player-container div.tw-c-text-overlay:not([data-a-target="hosting-ui-header"])',
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2,.channel-leaderboard',
'pinned-cheer': '.pinned-cheer,.pinned-cheer-v2,.channel-leaderboard,.channel-leaderboard-marquee',
'whispers': 'body .whispers-open-threads,.tw-core-button[data-a-target="whisper-box-button"],.whispers__pill',
'dir-live-ind': '.live-channel-card[data-ffz-type="live"] .tw-channel-status-text-indicator, article[data-ffz-type="live"] .tw-channel-status-text-indicator',

View file

@ -54,7 +54,7 @@ export default class Dashboard extends Module {
this.settings.updateContext({
channel: get('props.channelLogin', inst),
channelID: get('props.channelID', inst),
hosting: !! inst.props?.hostedChannel?.id
//hosting: !! inst.props?.hostedChannel?.id
});
}
@ -62,7 +62,7 @@ export default class Dashboard extends Module {
this.settings.updateContext({
channel: null,
channelID: null,
hosting: false
//hosting: false
});
}

View file

@ -1,249 +0,0 @@
'use strict';
// ============================================================================
// Following Page
// ============================================================================
import {SiteModule} from 'utilities/module';
import {createElement} from 'utilities/dom';
import {get} from 'utilities/object';
import {createPopper} from '@popperjs/core';
import {makeReference} from 'utilities/tooltip';
export default class Following extends SiteModule {
constructor(...args) {
super(...args);
this.inject('site.fine');
this.inject('site.router');
this.inject('site.apollo');
this.inject('site.css_tweaks');
this.inject('i18n');
this.inject('settings');
this.settings.add('directory.following.group-hosts', {
default: true,
ui: {
path: 'Directory > Following @{"description": "**Note:** These settings do not currently work due to changes made by Twitch to how the directory works."} >> Hosts',
title: 'Group Hosts',
description: 'Only show a given hosted channel once in the directory.',
component: 'setting-check-box'
},
changed: () => {
this.apollo.maybeRefetch('FollowedIndex_CurrentUser');
this.apollo.maybeRefetch('FollowingHosts_CurrentUser');
}
});
this.settings.add('directory.following.host-menus', {
default: 1,
ui: {
path: 'Directory > Following >> Hosts',
title: 'Hosted Channel Menus',
description: 'Display a menu to select which channel to visit when clicking a hosted channel in the directory.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'When Multiple are Hosting'},
{value: 2, title: 'Always'}
]
},
changed: () => this.parent.DirectoryCard.forceUpdate()
});
this.hosts = new WeakMap;
}
modifyLiveUsers(res, path = 'followedLiveUsers') {
const followed_live = get(`data.currentUser.${path}`, res);
if ( ! followed_live )
return;
if ( followed_live.nodes )
followed_live.nodes = this.parent.processNodes(followed_live.nodes);
else if ( followed_live.edges )
followed_live.edges = this.parent.processNodes(followed_live.edges);
return res;
}
modifyLiveHosts(res) {
const blocked_games = this.settings.provider.get('directory.game.blocked-games', []),
do_grouping = this.settings.get('directory.following.group-hosts'),
edges = get('data.currentUser.followedHosts.nodes', res);
if ( ! edges || ! edges.length )
return res;
this.hosts = new WeakMap();
const out = [];
for(const edge of edges) {
if ( ! edge )
continue;
const node = edge.node || edge,
hosted = node.hosting,
stream = hosted && hosted.stream;
if ( ! stream || stream.game && blocked_games.includes(stream.game.name) )
continue;
if ( ! stream.viewersCount ) {
if ( ! do_grouping || ! this.hosts[hosted.login] )
out.push(edge);
continue;
}
const store = stream.viewersCount = new Number(stream.viewersCount || 0);
store.createdAt = stream.createdAt;
store.title = stream.title;
//store.game = stream.game;
if ( do_grouping ) {
const host_nodes = this.hosts[hosted.login];
if ( host_nodes ) {
host_nodes.push(node);
this.hosts.set(store, host_nodes);
} else {
this.hosts.set(store, this.hosts[hosted.login] = [node]);
out.push(edge);
}
} else
out.push(edge);
}
res.data.currentUser.followedHosts.nodes = out;
return res;
}
onEnable() {
document.body.addEventListener('click', this.destroyHostMenu.bind(this));
}
destroyHostMenu(event) {
if (!event || ! this.hostMenu || event && event.target && event.target.closest('.ffz-channel-selector-outer') === null && Date.now() > this.hostMenuBuffer) {
this.hostMenuPopper && this.hostMenuPopper.destroy();
this.hostMenu && this.hostMenu.remove();
this.hostMenuPopper = this.hostMenu = undefined;
}
}
showHostMenu(inst, channels, event) {
if (this.settings.get('directory.following.host-menus') === 0 || this.settings.get('directory.following.host-menus') === 1 && channels.length < 2) return;
event.preventDefault();
event.stopPropagation();
this.hostMenuPopper && this.hostMenuPopper.destroy();
this.hostMenu && this.hostMenu.remove();
const simplebarContentChildren = [];
// Hosted Channel Header
simplebarContentChildren.push(<p class="tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2">
{this.i18n.t('directory.hosted', 'Hosted Channel')}
</p>);
// Hosted Channel Content
simplebarContentChildren.push(<a
class="tw-block tw-border-radius-small tw-full-width ffz-interactable ffz-interactable--default ffz-interactable--hover-enabled tw-interactive"
href={`/${inst.props.channelLogin}`}
onClick={e => this.parent.hijackUserClick(e, inst.props.channelLogin, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
>
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05">
<div class="ffz-channel-avatar">
<img src={inst.props.channelImageProps.src} alt={inst.props.channelDisplayName} />
</div>
<p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">
{inst.props.channelDisplayName}
</p>
</div>
</a>);
// Hosting Channels Header
simplebarContentChildren.push(<p class="tw-pd-t-05 tw-pd-x-1 tw-c-text-alt-2">
{this.i18n.t('directory.hosting', 'Hosting Channels')}
</p>);
// Hosting Channels Content
for (const channel of channels) {
simplebarContentChildren.push(<a
class="tw-block tw-border-radius-small tw-full-width ffz-interactable ffz-interactable--default ffz-interactable--hover-enabled tw-interactive"
href={`/${channel.login}`}
onClick={e => this.parent.hijackUserClick(e, channel.login, this.destroyHostMenu.bind(this))} // eslint-disable-line react/jsx-no-bind
>
<div class="tw-align-items-center tw-flex tw-flex-row tw-flex-nowrap tw-mg-x-1 tw-mg-y-05">
<div class="ffz-channel-avatar">
<img src={channel.profileImageURL} alt={channel.displayName} />
</div>
<p class="tw-ellipsis tw-flex-grow-1 tw-mg-l-1 tw-font-size-5">
{channel.displayName}
</p>
</div>
</a>);
}
this.hostMenu = (<div class="ffz-host-menu ffz-balloon tw-block">
<div class="tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base tw-pd-05">
<div class="scrollable-area" data-simplebar>
{simplebarContentChildren}
</div>
</div>
</div>);
const root = (document.body.querySelector('#root>div') || document.body);
root.appendChild(this.hostMenu);
this.hostMenuPopper = createPopper(
makeReference(event.clientX - 60, event.clientY - 60),
this.hostMenu,
{
placement: 'bottom-start',
modifiers: {
flip: {
enabled: false
}
}
}
);
this.hostMenuBuffer = Date.now() + 50;
}
updateChannelCard(inst) {
const card = this.fine.getChildNode(inst);
if ( ! card )
return;
const login = inst.props.channelLogin,
hosting = inst.props.channelLinkTo && inst.props.channelLinkTo.state.content === 'live_host' && this.hosts && this.hosts[login];
if ( hosting && this.settings.get('directory.following.group-hosts') ) {
const host_data = this.hosts[login];
const title_link = card.querySelector('a[data-test-selector="preview-card-titles__primary-link"]'),
thumbnail_link = card.querySelector('a[data-a-target="preview-card-image-link"]');
if ( title_link )
title_link.addEventListener('click', this.showHostMenu.bind(this, inst, host_data));
if ( thumbnail_link )
thumbnail_link.addEventListener('click', this.showHostMenu.bind(this, inst, host_data));
}
}
}

View file

@ -48,7 +48,6 @@ export default class Directory extends SiteModule {
this.inject('i18n');
this.inject('settings');
//this.inject(Following);
this.inject(Game);
this.DirectoryCard = this.elemental.define(

View file

@ -1,379 +0,0 @@
'use strict';
// ============================================================================
// Host Button
// ============================================================================
import Module from 'utilities/module';
import {get} from 'utilities/object';
import {createElement} from 'utilities/dom';
const HOST_ERRORS = {
COMMAND_EXECUTION: {
key: 'command-execution',
text: 'There was an error executing the host command. Please try again later.',
},
CHAT_CONNECTION: {
key: 'chat-connection',
text: 'There was an issue connecting to chat. Please try again later.',
}
};
export default class HostButton extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.inject('site');
this.inject('site.fine');
this.inject('site.chat');
this.inject('site.twitch_data');
this.inject('i18n');
this.inject('metadata');
this.inject('settings');
this.settings.add('metadata.host-button', {
default: true,
ui: {
path: 'Channel > Metadata >> Player',
title: 'Host Button',
description: 'Show a host button with the current hosted channel in the tooltip.',
component: 'setting-check-box'
},
changed: () => {
const ffz_user = this.site.getUser(),
userLogin = ffz_user && ffz_user.login;
if (userLogin)
this.joinChannel(userLogin);
this.metadata.updateMetadata('host');
}
});
}
isChannelHosted(channelLogin) {
return this._last_hosted_channel === channelLogin;
}
sendHostUnhostCommand(channel) {
if (!this._chat_con) {
this._host_error = HOST_ERRORS.CHAT_CONNECTION;
this._host_updating = false;
return;
}
const ffz_user = this.site.getUser(),
userLogin = ffz_user && ffz_user.login;
const commandData = {channel: userLogin, username: channel};
this._host_updating = true;
this.metadata.updateMetadata('host');
this._host_feedback = setTimeout(() => {
if (this._last_hosted_channel === null) {
this._host_error = HOST_ERRORS.COMMAND_EXECUTION;
this._host_updating = false;
this.metadata.updateMetadata('host');
}
}, 3000);
if (this.isChannelHosted(channel)) {
this._chat_con.commands.unhost.execute(commandData);
} else {
this._chat_con.commands.host.execute(commandData);
}
}
joinChannel(channel) {
if (this._chat_con) {
if (this.settings.get('metadata.host-button') && !this._chat_con.session.channelstate[`#${channel}`]) {
this._chat_con.joinChannel(channel);
}
}
}
hookIntoChatConnection(inst) {
const userLogin = inst.props.currentUserLogin;
if (this._chat_con) {
this.joinChannel(userLogin);
return;
}
this.on('tmi:host', e => {
if (e.channel.substring(1) !== userLogin) return;
clearTimeout(this._host_feedback);
this._host_error = false;
this._last_hosted_channel = e.target;
this._host_updating = false;
this.metadata.updateMetadata('host');
});
this.on('tmi:unhost', e => {
if (e.channel.substring(1) !== userLogin) return;
clearTimeout(this._host_feedback);
this._host_error = false;
this._last_hosted_channel = null;
this._host_updating = false;
this.metadata.updateMetadata('host');
});
const chatServiceClient = inst.client;
this._chat_con = chatServiceClient;
if (this.settings.get('metadata.host-button'))
this.joinChannel(userLogin);
}
onEnable() {
this.on('i18n:update', () => this.metadata.updateMetadata('host'));
this.metadata.definitions.host = {
order: 150,
border: true,
button: true,
fade_in: true,
modview: true,
disabled: () => this._host_updating || this._host_error,
click: data => {
if ( data.channel )
this.sendHostUnhostCommand(data.channel.login);
},
popup: async (data, tip) => {
const vue = this.resolve('vue'),
_host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'),
_autoHosts = this.fetchAutoHosts(),
_autoHostSettings = this.fetchAutoHostSettings();
const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]);
this._auto_host_tip = tip;
tip.element.classList.remove('tw-pd-1');
tip.element.classList.add('ffz-balloon--lg');
vue.component('host-options', host_options_vue.default);
return this.buildAutoHostMenu(vue, autoHosts, autoHostSettings, data.channel);
},
label: data => {
const ffz_user = this.site.getUser();
if ( ! this.settings.get('metadata.host-button') || ! ffz_user || ! data.channel || data.channel.login === ffz_user.login )
return;
if ( data.channel.video && ! this.isChannelHosted(data.channel.login) )
return;
if ( this._host_updating )
return this.i18n.t('metadata.host-button.updating', 'Updating...');
return (this._last_hosted_channel && this.isChannelHosted(data.channel && data.channel.login))
? this.i18n.t('metadata.host-button.unhost', 'Unhost')
: this.i18n.t('metadata.host-button.host', 'Host');
},
tooltip: () => {
if (this._host_error) {
return this.i18n.t(
`metadata.host-button.tooltip.error.${this._host_error.key}`,
this._host_error.text);
} else {
return this.i18n.t('metadata.host-button.tooltip',
'Currently hosting: {channel}',
{
channel: this._last_hosted_channel || this.i18n.t('metadata.host-button.tooltip.none', 'None')
});
}
}
};
this.metadata.updateMetadata('host');
this.chat.ChatService.ready((cls, instances) => {
for(const inst of instances)
this.hookIntoChatConnection(inst);
})
this.chat.ChatService.on('mount', this.hookIntoChatConnection, this);
}
buildAutoHostMenu(vue, hosts, autoHostSettings, data) {
this._current_channel_id = data.id;
this.activeTab = this.activeTab || 'auto-host';
const vueEl = new vue.Vue({
el: createElement('div'),
render: h => this.vueHostMenu = h('host-options', {
hosts,
autoHostSettings,
activeTab: this.activeTab,
addedToHosts: this.currentRoomInHosts(),
addToAutoHosts: () => this.addCurrentRoomToHosts(),
rearrangeHosts: event => this.rearrangeHosts(event.oldIndex, event.newIndex),
removeFromHosts: event => this.removeUserFromHosts(event),
setActiveTab: tab => {
this.vueHostMenu.data.activeTab = this.activeTab = tab;
},
updatePopper: () => {
if (this._auto_host_tip) this._auto_host_tip.update();
},
updateCheckbox: e => {
const t = e.target;
let setting = t.dataset.setting,
state = t.checked;
if ( setting === 'enabled' )
setting = 'isEnabled';
else if ( setting === 'teamHost' )
setting = 'willAutohostTeam';
else if ( setting === 'strategy' )
state = state ? 'RANDOM' : 'ORDERED';
else if ( setting === 'deprioritizeVodcast' ) {
setting = 'willPrioritizeAutohost';
}
this.updateAutoHostSetting(setting, state);
}
})
});
return vueEl.$el;
}
async fetchAutoHosts() {
const user = this.site.getUser();
if ( ! user )
return;
const result = await this.twitch_data.queryApollo(
await import(/* webpackChunkName: 'host-options' */ './autohost_list.gql'),
{
id: user.id
},
{
fetchPolicy: 'network-only'
}
);
return this.autoHosts = get('data.user.autohostChannels.nodes', result);
}
async fetchAutoHostSettings() {
const user = this.site.getUser();
if ( ! user )
return;
const result = await this.twitch_data.queryApollo(
await import(/* webpackChunkName: 'host-options' */ './autohost_settings.gql'),
{
id: user.id
},
{
fetchPolicy: 'network-only'
}
);
return this.autoHostSettings = get('data.user.autohostSettings', result);
}
queueHostUpdate() {
if (this._host_update_timer) clearTimeout(this._host_update_timer);
this._host_update_timer = setTimeout(() => {
this._host_update_timer = undefined;
this.updateAutoHosts(this.autoHosts);
}, 1000);
}
rearrangeHosts(oldIndex, newIndex) {
const host = this.autoHosts.splice(oldIndex, 1)[0];
this.autoHosts.splice(newIndex, 0, host);
this.queueHostUpdate();
}
currentRoomInHosts() {
return this.getAutoHostIDs(this.autoHosts).includes(this._current_channel_id);
}
addCurrentRoomToHosts() {
const newHosts = this.autoHosts.slice(0);
newHosts.push({ id: this._current_channel_id});
this.updateAutoHosts(newHosts);
}
removeUserFromHosts(event) {
const id = event.target.closest('.ffz--host-user').dataset.id;
const newHosts = [];
for (let i = 0; i < this.autoHosts.length; i++) {
if (this.autoHosts[i].id != id) newHosts.push(this.autoHosts[i]);
}
this.updateAutoHosts(newHosts);
}
getAutoHostIDs(hosts) { // eslint-disable-line class-methods-use-this
const ids = [];
if (hosts) {
for (let i = 0; i < hosts.length; i++) {
ids.push(hosts[i].id);
}
}
return ids;
}
async updateAutoHosts(newHosts) {
const user = this.site.getUser();
if ( ! user )
return;
const autoHosts = this.getAutoHostIDs(newHosts);
const result = await this.twitch_data.mutate({
mutation: await import(/* webpackChunkName: 'host-options' */ './autohost_list_mutate.gql'),
variables: {
userID: user.id,
channelIDs: autoHosts
}
});
this.autoHosts = get('data.setAutohostChannels.user.autohostChannels.nodes', result);
if (this.vueHostMenu) {
this.vueHostMenu.data.hosts = this.autoHosts;
this.vueHostMenu.data.addedToHosts = this.currentRoomInHosts();
}
}
async updateAutoHostSetting(setting, newValue) {
const user = this.site.getUser();
if ( ! user )
return;
const result = await this.twitch_data.mutate({
mutation: await import(/* webpackChunkName: 'host-options' */ './autohost_settings_mutate.gql'),
variables: {
userID: user.id,
[setting]: newValue
}
});
this.autoHostSettings = get('data.updateAutohostSettings.user.autohostSettings', result);
if (this.vueHostMenu) {
this.vueHostMenu.data.autoHostSettings = this.autoHostSettings;
}
}
}

View file

@ -172,7 +172,7 @@ export default class Layout extends Module {
});
this.settings.add('layout.portrait-extra-height', {
requires: ['context.new_channel', 'context.squad_bar', 'context.hosting', 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
requires: ['context.new_channel', 'context.squad_bar', /*'context.hosting',*/ 'context.ui.theatreModeEnabled', 'player.theatre.no-whispers', 'whispers.show', 'layout.minimal-navigation'],
process(ctx) {
let height = 0;
if ( ctx.get('context.ui.theatreModeEnabled') ) {
@ -192,8 +192,8 @@ export default class Layout extends Module {
height += ctx.get('context.new_channel') ? 1 : 5;
if ( ctx.get('context.hosting') )
height += 4;
/*if ( ctx.get('context.hosting') )
height += 4;*/
}
return height;

View file

@ -196,7 +196,7 @@ export default class Player extends PlayerBase {
checkCarousel(inst) {
if ( this.settings.get('channel.hosting.enable') )
/*if ( this.settings.get('channel.hosting.enable') )
return;
if ( inst.props?.playerType === 'channel_home_carousel' ) {
@ -211,7 +211,7 @@ export default class Player extends PlayerBase {
events = inst.props.playerEvents;
this.stopPlayer(player, events, inst);
}
}*/
}

View file

@ -9,7 +9,7 @@
@import 'fixes';
@import 'host_options';
//@import 'host_options';
@import 'featured_follow';
@import 'mod_card';
@import 'easteregg';