1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +00:00

A ton of stuff. Initial migration to React 16. Initial changes for Rooms support. Add support for clip champ badge. Start using .gql source files that are compiled during import so we don't need to include GraphQL-tag in builds.

This commit is contained in:
SirStendec 2018-02-22 18:23:44 -05:00
parent f0bcf7f53e
commit 0ae6e5021d
29 changed files with 536 additions and 263 deletions

View file

@ -1,3 +1,9 @@
<div class="list-header">4.0.0-beta1.5<span>@ef163f5d644217193110</span> <time datetime="2018-02-22">(2018-02-22)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Fixed: React 16 support.</li>
<li>Note: Many features are still broken. This is just an initial quick fix.</li>
</ul>
<div class="list-header">4.0.0-beta1.5<span>@a752805865b1313466a7</span> <time datetime="2018-02-08">(2018-02-08)</time></div> <div class="list-header">4.0.0-beta1.5<span>@a752805865b1313466a7</span> <time datetime="2018-02-08">(2018-02-08)</time></div>
<ul class="chat-menu-content menu-side-padding"> <ul class="chat-menu-content menu-side-padding">
<li>Added: Setting to hide the Live indicator from live channels in the directory.</li> <li>Added: Setting to hide the Live indicator from live channels in the directory.</li>

View file

@ -18,11 +18,12 @@ export const CSS_BADGES = {
moderator: { 1: { color: '#34ae0a', svg: true } }, moderator: { 1: { color: '#34ae0a', svg: true } },
twitchbot: { 1: { color: '#34ae0a' } }, twitchbot: { 1: { color: '#34ae0a' } },
partner: { 1: { color: 'transparent', trans: { image: true, color: '#6441a5' } } }, partner: { 1: { color: 'transparent', trans: { image: true, color: '#6441a5' } } },
'clip-champ': { 1: { color: '#6441a5'} },
turbo: { 1: { color: '#6441a5', svg: true } }, turbo: { 1: { color: '#6441a5', svg: true } },
premium: { 1: { color: '#009cdc' } }, premium: { 1: { color: '#009cdc' } },
subscriber: { 0: { color: '#6441a4' }, 1: { color: '#6441a4' }}, subscriber: { 0: { color: '#6441a5' }, 1: { color: '#6441a5' }},
} }
export const BADGE_POSITIONS = { export const BADGE_POSITIONS = {
@ -143,13 +144,80 @@ export default class Badges extends Module {
this.style = new ManagedStyle('badges'); this.style = new ManagedStyle('badges');
this.badges = {}; this.badges = {};
this.twitch_badges = {};
this.twitch_badges = new Map; this.settings.add('chat.badges.hidden', {
default: [],
_ui: {
path: 'Chat > Badges >> tabs ~> Visibility',
component: 'badge-visibility',
data: () => {
const twitch = [],
game = [],
ffz = [],
addon = [];
for(const key in this.twitch_badges)
if ( has(this.twitch_badges, key) ) {
const badge = this.twitch_badges[key],
vs = [];
let v = badge && (badge[1] || badge[0]);
for(const key in badge)
if ( has(badge, key) ) {
const version = badge[key];
if ( ! v )
v = version;
if ( version && version.image1x )
vs.push({
name: version.title,
image: version.image1x,
styleImage: `url("${version.image1x}")`
});
}
if ( v )
(badge.__game ? game : twitch).push({
id: key,
provider: 'twitch',
name: v.title,
color: 'transparent',
image: v.image4x,
versions: vs,
styleImage: `url("${v.image4x}")`
});
}
for(const key in this.badges)
if ( has(this.badges, key) ) {
const badge = this.badges[key],
image = badge.urls ? (badge.urls[4] || badge.urls[2] || badge.urls[1]) : badge.image;
(/^addon/.test(key) ? addon : ffz).push({
id: key,
provider: 'ffz',
name: badge.title,
color: badge.color || 'transparent',
image,
styleImage: `url("${image}")`
});
}
return [
{title: 'Twitch', badges: twitch},
{title: 'Twitch: Game', badges: game},
{title: 'FrankerFaceZ', badges: ffz},
{title: 'Add-on', badges: addon}
];
}
}
});
this.settings.add('chat.badges.style', { this.settings.add('chat.badges.style', {
default: 0, default: 0,
ui: { ui: {
path: 'Chat > Badges >> Appearance', path: 'Chat > Badges >> tabs ~> Appearance',
title: 'Style', title: 'Style',
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
@ -497,7 +565,7 @@ export default class Badges extends Module {
const b = {}; const b = {};
for(const data of badges) { for(const data of badges) {
const sid = data.setID, const sid = data.setID,
bs = b[sid] = b[sid] || {}; bs = b[sid] = b[sid] || {__game: /_\d+$/.test(sid)};
bs[data.version] = data; bs[data.version] = data;
} }

View file

@ -429,7 +429,15 @@ export default class Chat extends Module {
tokenizeMessage(msg, user) { tokenizeMessage(msg, user) {
if ( msg.content && ! msg.message )
msg.message = msg.content.text;
if ( msg.sender && ! msg.user )
msg.user = msg.sender;
let tokens = [{type: 'text', text: msg.message}]; let tokens = [{type: 'text', text: msg.message}];
if ( ! tokens[0].text )
return tokens;
for(const tokenizer of this.__tokenizers) for(const tokenizer of this.__tokenizers)
tokens = tokenizer.process.call(this, tokens, msg, user); tokens = tokenizer.process.call(this, tokens, msg, user);

View file

@ -0,0 +1,85 @@
<template lang="html">
<div class="ffz--badge-visibility tw-pd-t-05">
<section class="ffz--menu-container tw-border-t" v-for="sec in data">
<header>{{ sec.title }}</header>
<ul class="tw-flex tw-flex-wrap tw-align-content-start">
<li v-for="i in sort(sec.badges)" class="ffz--badge-info tw-pd-y-1 tw-pd-r-1 tw-flex" :class="{default: isDefault}">
<input
type="checkbox"
class="tw-checkbox__input"
checked="checked"
:id="i.id"
>
<label class="tw-checkbox__label tw-flex" :for="i.id">
<div class="preview-image ffz-badge tw-mg-r-1 tw-flex-shrink-0" :style="{backgroundColor: i.color, backgroundImage: i.styleImage }" />
<div>
<h5>{{ i.name }}</h5>
<section class="tw-mg-t-05" v-if="i.versions && i.versions.length > 1">
<span v-for="v in i.versions" data-tooltip-type="html" class="ffz-badge ffz-tooltip" :title="v.name" :style="{backgroundColor: i.color, backgroundImage: v.styleImage}" />
</section>
<!--button class="tw-mg-t-05 tw-button tw-button--hollow tw-tooltip-wrapper">
<span class="tw-button__text">Reset</span>
<span class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</span>
</button-->
</div>
</label>
</li>
</ul>
</section>
</div>
</template>
<script>
import SettingMixin from '../setting-mixin';
export default {
mixins: [SettingMixin],
props: ['item', 'context'],
methods: {
sort(items) {
return items.sort((a, b) => {
const an = a.name.toLowerCase(),
bn = b.name.toLowerCase();
if ( an < bn ) return -1;
if ( an > bn ) return 1;
return 0;
});
},
onChange() {
this.set(this.$refs.control.checked);
}
}
}
</script>
<style lang="scss" scoped>
.ffz--badge-info {
&.default {
label:before, label:after {
opacity: 0.5
}
}
.tw-checkbox__input:checked+.tw-checkbox__label:after,
label:before, label:after {
top: 1.05rem;
}
.ffz-badge.preview-image {
width: 7.2rem;
height: 7.2rem;
background-size: 7.2rem;
background-repeat: no-repeat;
}
width: 30rem;
}
</style>

View file

@ -39,7 +39,7 @@ export default class TooltipProvider extends Module {
onEnable() { onEnable() {
const container = document.body.querySelector('.twilight-root') || document.body; const container = document.body.querySelector('.twilight-root') || document.body;
this.tips = new Tooltip('body [data-reactroot]', 'ffz-tooltip', { this.tips = new Tooltip('body #root', 'ffz-tooltip', {
html: true, html: true,
delayHide: this.checkDelayHide.bind(this), delayHide: this.checkDelayHide.bind(this),
delayShow: this.checkDelayShow.bind(this), delayShow: this.checkDelayShow.bind(this),

View file

@ -38,10 +38,8 @@ export default class Twilight extends BaseSite {
} }
onEnable() { onEnable() {
const root = this.fine.getParent(this.fine.react), const thing = this.fine.searchTree(null, n => n.props && n.props.store),
ctx = this.context = root && root._context, store = this.store = thing && thing.props && thing.props.store;
store = this.store = ctx && ctx.store;
if ( ! store ) if ( ! store )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable()); return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
@ -64,10 +62,10 @@ export default class Twilight extends BaseSite {
updateContext() { updateContext() {
try { try {
const state = this.store.getState(), const state = this.store.getState(),
route = this.context.router && this.context.router.route; history = this.router && this.router.history;
this.settings.updateContext({ this.settings.updateContext({
location: route && route.location, location: history && history.location,
ui: state && state.ui, ui: state && state.ui,
session: state && state.session session: state && state.session
}); });

View file

@ -7,6 +7,9 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {deep_copy} from 'utilities/object'; import {deep_copy} from 'utilities/object';
import CHANNEL_QUERY from './channel_bar_query.gql';
export default class ChannelBar extends Module { export default class ChannelBar extends Module {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -17,17 +20,9 @@ export default class ChannelBar extends Module {
this.inject('site.apollo'); this.inject('site.apollo');
this.inject('metadata'); this.inject('metadata');
this.apollo.registerModifier('ChannelPage_ChannelInfoBar_User', CHANNEL_QUERY);
this.apollo.registerModifier('ChannelPage_ChannelInfoBar_User', this.apollo.gql`query {
user {
stream {
createdAt
type
}
}
}`);
this.apollo.registerModifier('ChannelPage_ChannelInfoBar_User', data => { this.apollo.registerModifier('ChannelPage_ChannelInfoBar_User', data => {
console.log('BOOP BOOP A DOOP', data);
const u = data && data.data && data.data.user; const u = data && data.data && data.data.user;
if ( u ) { if ( u ) {
const o = u.profileViewCount = new Number(u.profileViewCount || 0); const o = u.profileViewCount = new Number(u.profileViewCount || 0);

View file

@ -0,0 +1,8 @@
query {
user {
stream {
createdAt
type
}
}
}

View file

@ -61,25 +61,30 @@ const EVENTS = [
'onJoinedEvent', 'onJoinedEvent',
'onDisconnectedEvent', 'onDisconnectedEvent',
'onReconnectingEvent', 'onReconnectingEvent',
'onChatMessageEvent',
'onChatNoticeEvent',
'onChatActionEvent',
'onBadgesUpdatedEvent',
'onHostingEvent', 'onHostingEvent',
'onUnhostEvent', 'onUnhostEvent',
'onChatMessageEvent', 'onPurchaseEvent',
'onChatActionEvent', 'onCrateEvent',
'onChatNoticeEvent', //'onRitualEvent',
'onTimeoutEvent',
'onBanEvent',
'onModerationEvent',
'onSubscriptionEvent', 'onSubscriptionEvent',
//'onResubscriptionEvent', //'onResubscriptionEvent',
'onSubscriptionGiftEvent', 'onSubscriptionGiftEvent',
'onRoomStateEvent', 'onTimeoutEvent',
'onSlowModeEvent', 'onBanEvent',
'onFollowerOnlyModeEvent',
'onSubscriberOnlyModeEvent',
'onClearChatEvent', 'onClearChatEvent',
'onRaidEvent', 'onRaidEvent',
'onUnraidEvent', 'onUnraidEvent',
'onBadgesUpdatedEvent' 'onRoomModsEvent',
'onRoomStateEvent',
'onFollowerOnlyModeEvent',
'onSlowModeEvent',
'onSubscriberOnlyModeEvent',
'onEmoteOnlyModeEvent',
'onBitsCharityEvent'
]; ];
@ -408,7 +413,7 @@ export default class ChatHook extends Module {
const i = this, const i = this,
pm = this.postMessage; pm = this.postMessage;
for(const key of EVENTS) { // eslint-disable-line guard-for-in for(const key of EVENTS) {
const original = this[key]; const original = this[key];
if ( original ) if ( original )
this[key] = function(e, t) { this[key] = function(e, t) {

View file

@ -25,7 +25,12 @@ export default class ChatLine extends Module {
this.ChatLine = this.fine.define( this.ChatLine = this.fine.define(
'chat-line', 'chat-line',
n => n.renderMessageBody n => n.renderMessageBody && ! n.getMessageParts
);
this.ChatRoomLine = this.fine.define(
'chat-room-line',
n => n.renderMessageBody && n.getMessageParts
); );
} }
@ -64,8 +69,23 @@ export default class ChatLine extends Module {
const types = t.parent.chat_types || {}, const types = t.parent.chat_types || {},
msg = this.props.message, msg = this.props.message,
is_action = msg.type === types.Action, is_action = msg.type === types.Action;
user = msg.user,
if ( msg.content && ! msg.message )
msg.message = msg.content.text;
if ( msg.sender && ! msg.user ) {
msg.user = msg.sender;
msg.user.color = msg.user.color || msg.user.chatColor;
}
if ( ! msg.badges && msg.user.displayBadges ) {
const b = msg.badges = {};
for(const item of msg.user.displayBadges)
b[item.setID] = item.version;
}
const user = msg.user,
color = t.parent.colors.process(user.color), color = t.parent.colors.process(user.color),
/*bg_rgb = Color.RGBA.fromHex(user.color), /*bg_rgb = Color.RGBA.fromHex(user.color),
bg_color = bg_rgb.luminance() < .005 ? bg_rgb : bg_rgb.toHSLA().targetLuminance(0.005).toRGBA(), bg_color = bg_rgb.luminance() < .005 ? bg_rgb : bg_rgb.toHSLA().targetLuminance(0.005).toRGBA(),

View file

@ -0,0 +1,72 @@
'use strict';
// ============================================================================
// Compatibility Layer
// Emote Menu for Twitch (BTTV Emote Menu)
// ============================================================================
import Module from 'utilities/module';
import {has} from 'utilities/object';
export default class CompatEmoteMenu extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.inject('site.chat');
this.inject('chat.emotes');
}
async onEnable() {
const em = await this.findEmoteMenu();
if ( ! em )
return this.log.info('Emote Menu for Twitch was not found after 60 seconds.');
em.registerEmoteGetter('FrankerFaceZ', () => {
// We get literally no information about the current context,
// so we need to look up everything.
const cont = this.chat.ChatContainer.first,
props = cont && cont.props;
if ( ! props )
return;
const sets = this.emotes.getSets(props.userID, props.currentUserLogin, props.channelID, props.channelLogin),
emotes = [];
for(const set of sets) {
if ( ! set || ! set.emotes )
continue;
for(const emote_id in set.emotes)
if ( has(set.emotes, emote_id) ) {
const emote = set.emotes[emote_id];
if ( emote.hidden )
continue;
emotes.push({
text: emote.name,
url: emote.urls[1],
channel: `${set.source || 'FrankerFaceZ'} ${set.title}`,
badge: set.icon || '//cdn.frankerfacez.com/script/devicon.png'
});
}
}
return emotes;
});
}
async findEmoteMenu(delay = 0) {
if ( window.emoteMenu && emoteMenu.registerEmoteGetter )
return emoteMenu;
if ( delay >= 60000 )
return null;
return new Promise(s => {
setTimeout(() => this.findEmoteMenu(delay + 100).then(s), 100)
});
}
}

View file

@ -0,0 +1,12 @@
query {
streams {
edges {
node {
createdAt
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}

View file

@ -7,6 +7,8 @@
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {get} from 'utilities/object'; import {get} from 'utilities/object';
import BROWSE_POPULAR from './browse_popular.gql';
export default class BrowsePopular extends SiteModule { export default class BrowsePopular extends SiteModule {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -15,18 +17,7 @@ export default class BrowsePopular extends SiteModule {
this.inject('site.fine'); this.inject('site.fine');
this.inject('settings'); this.inject('settings');
this.apollo.registerModifier('BrowsePage_Popular', this.apollo.gql`query { this.apollo.registerModifier('BrowsePage_Popular', BROWSE_POPULAR);
streams {
edges {
node {
createdAt
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}`);
this.ChannelCard = this.fine.define( this.ChannelCard = this.fine.define(
'browse-all-channel-card', 'browse-all-channel-card',

View file

@ -1,33 +0,0 @@
'use strict';
// ============================================================================
// Directory (Following, for now)
// ============================================================================
import {SiteModule} from 'utilities/module';
export default class Community extends SiteModule {
constructor(...args) {
super(...args);
this.inject('site.apollo');
this.apollo.registerModifier('GamePage_Game', this.apollo.gql`query {
directory {
... on Community {
streams {
edges {
node {
createdAt
type
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}
}
}`);
}
}

View file

@ -0,0 +1,13 @@
query {
currentUser {
followedLiveUsers {
nodes {
profileImageURL(width: 70)
stream {
type
createdAt
}
}
}
}
}

View file

@ -0,0 +1,15 @@
query {
currentUser {
followedHosts {
nodes {
profileImageURL(width: 70)
hosting {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
}
}
}

View file

@ -0,0 +1,24 @@
query {
currentUser {
followedLiveUsers {
nodes {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
followedHosts {
nodes {
profileImageURL(width: 70)
hosting {
profileImageURL(width: 70)
stream {
createdAt
type
}
}
}
}
}
}

View file

@ -0,0 +1,14 @@
query {
currentUser {
followedLiveUsers {
edges {
node {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
}
}
}

View file

@ -10,6 +10,11 @@ import {get} from 'utilities/object';
import Popper from 'popper.js'; import Popper from 'popper.js';
import FOLLOWED_INDEX from './followed_index.gql';
import FOLLOWED_HOSTS from './followed_hosts.gql';
import FOLLOWED_CHANNELS from './followed_channels.gql';
import FOLLOWED_LIVE from './followed_live.gql';
export default class Following extends SiteModule { export default class Following extends SiteModule {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -58,75 +63,10 @@ export default class Following extends SiteModule {
changed: () => this.ChannelCard.forceUpdate() changed: () => this.ChannelCard.forceUpdate()
}); });
this.apollo.registerModifier('FollowedIndex_CurrentUser', this.apollo.gql`query { this.apollo.registerModifier('FollowedIndex_CurrentUser', FOLLOWED_INDEX);
currentUser { this.apollo.registerModifier('FollowingLive_CurrentUser', FOLLOWED_LIVE);
followedLiveUsers { this.apollo.registerModifier('FollowingHosts_CurrentUser', FOLLOWED_HOSTS);
nodes { this.apollo.registerModifier('FollowedChannels', FOLLOWED_CHANNELS);
profileImageURL(width: 70)
stream {
createdAt
}
}
}
followedHosts {
nodes {
profileImageURL(width: 70)
hosting {
profileImageURL(width: 70)
stream {
createdAt
type
}
}
}
}
}
}`);
this.apollo.registerModifier('FollowingLive_CurrentUser', this.apollo.gql`query {
currentUser {
followedLiveUsers {
edges {
node {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
}
}
}`);
this.apollo.registerModifier('FollowingHosts_CurrentUser', this.apollo.gql`query {
currentUser {
followedHosts {
nodes {
profileImageURL(width: 70)
hosting {
profileImageURL(width: 70)
stream {
createdAt
}
}
}
}
}
}`);
this.apollo.registerModifier('FollowedChannels', this.apollo.gql`query {
currentUser {
followedLiveUsers {
nodes {
profileImageURL(width: 70)
stream {
type
createdAt
}
}
}
}
}`);
this.ChannelCard = this.fine.define( this.ChannelCard = this.fine.define(
'following-channel-card', 'following-channel-card',

View file

@ -0,0 +1,30 @@
query {
directory {
... on Game {
streams {
edges {
node {
createdAt
type
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}
... on Community {
streams {
edges {
node {
createdAt
type
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}
}
}

View file

@ -7,6 +7,8 @@
import {SiteModule} from 'utilities/module'; import {SiteModule} from 'utilities/module';
import {createElement as e} from 'utilities/dom'; import {createElement as e} from 'utilities/dom';
import GAME_QUERY from './game.gql';
export default class Game extends SiteModule { export default class Game extends SiteModule {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -22,23 +24,7 @@ export default class Game extends SiteModule {
n => n.renderFollowButton && n.renderGameDetailsTab n => n.renderFollowButton && n.renderGameDetailsTab
); );
this.apollo.registerModifier('GamePage_Game', this.apollo.gql`query { this.apollo.registerModifier('GamePage_Game', GAME_QUERY);
directory {
... on Game {
streams {
edges {
node {
createdAt
type
broadcaster {
profileImageURL(width: 70)
}
}
}
}
}
}
}`);
} }
onEnable() { onEnable() {

View file

@ -11,7 +11,6 @@ import {get} from 'utilities/object';
import Following from './following'; import Following from './following';
import Game from './game'; import Game from './game';
import Community from './community';
import BrowsePopular from './browse_popular'; import BrowsePopular from './browse_popular';
export default class Directory extends SiteModule { export default class Directory extends SiteModule {
@ -30,7 +29,6 @@ export default class Directory extends SiteModule {
this.inject(Following); this.inject(Following);
this.inject(Game); this.inject(Game);
this.inject(Community);
this.inject(BrowsePopular); this.inject(BrowsePopular);
this.apollo.registerModifier('GamePage_Game', res => this.modifyStreams(res), false); this.apollo.registerModifier('GamePage_Game', res => this.modifyStreams(res), false);

View file

@ -47,7 +47,7 @@ export default class HostButton extends Module {
if (userLogin) if (userLogin)
this.joinChannel(userLogin); this.joinChannel(userLogin);
this.metadata.updateMetadata('host'); this.metadata.updateMetadata('host');
} }
}); });
@ -57,9 +57,9 @@ export default class HostButton extends Module {
button: true, button: true,
disabled: () => { disabled: () => {
return this._host_updating || this._host_error; return this._host_updating || this._host_error;
}, },
click: data => { click: data => {
if (data.channel) this.sendHostUnhostCommand(data.channel.login); if (data.channel) this.sendHostUnhostCommand(data.channel.login);
}, },
@ -69,7 +69,7 @@ export default class HostButton extends Module {
_host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'), _host_options_vue = import(/* webpackChunkName: "host-options" */ './host-options.vue'),
_autoHosts = this.fetchAutoHosts(), _autoHosts = this.fetchAutoHosts(),
_autoHostSettings = this.fetchAutoHostSettings(); _autoHostSettings = this.fetchAutoHostSettings();
const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]); const [, host_options_vue, autoHosts, autoHostSettings] = await Promise.all([vue.enable(), _host_options_vue, _autoHosts, _autoHostSettings]);
this._auto_host_tip = tip; this._auto_host_tip = tip;
@ -86,7 +86,7 @@ export default class HostButton extends Module {
const ffz_user = this.site.getUser(), const ffz_user = this.site.getUser(),
userLogin = ffz_user && ffz_user.login; userLogin = ffz_user && ffz_user.login;
if (data.channel && data.channel.login === userLogin) { if (data.channel && data.channel.login === userLogin) {
return ''; return '';
} }
@ -168,22 +168,22 @@ export default class HostButton extends Module {
this.on('tmi:host', e => { this.on('tmi:host', e => {
if (e.channel.substring(1) !== userLogin) return; if (e.channel.substring(1) !== userLogin) return;
clearTimeout(this._host_feedback); clearTimeout(this._host_feedback);
this._host_error = false; this._host_error = false;
this._last_hosted_channel = e.target; this._last_hosted_channel = e.target;
this._host_updating = false; this._host_updating = false;
this.metadata.updateMetadata('host'); this.metadata.updateMetadata('host');
}); });
this.on('tmi:unhost', e => { this.on('tmi:unhost', e => {
if (e.channel.substring(1) !== userLogin) return; if (e.channel.substring(1) !== userLogin) return;
clearTimeout(this._host_feedback); clearTimeout(this._host_feedback);
this._host_error = false; this._host_error = false;
this._last_hosted_channel = null; this._last_hosted_channel = null;
this._host_updating = false; this._host_updating = false;
this.metadata.updateMetadata('host'); this.metadata.updateMetadata('host');
}); });
@ -232,12 +232,12 @@ export default class HostButton extends Module {
const t = e.target, const t = e.target,
setting = t.dataset.setting; setting = t.dataset.setting;
let state = t.checked; let state = t.checked;
if ( setting === 'strategy' ) if ( setting === 'strategy' )
state = state ? 'random' : 'ordered'; state = state ? 'random' : 'ordered';
else if ( setting === 'deprioritize_vodcast' ) else if ( setting === 'deprioritize_vodcast' )
state = ! state; state = ! state;
this.updateAutoHostSetting(setting, state); this.updateAutoHostSetting(setting, state);
} }
}) })
@ -302,20 +302,20 @@ export default class HostButton extends Module {
queueHostUpdate() { queueHostUpdate() {
if (this._host_update_timer) clearTimeout(this._host_update_timer); if (this._host_update_timer) clearTimeout(this._host_update_timer);
this._host_update_timer = setTimeout(() => { this._host_update_timer = setTimeout(() => {
this._host_update_timer = undefined; this._host_update_timer = undefined;
this.updateAutoHosts(this.autoHosts); this.updateAutoHosts(this.autoHosts);
}, 1000); }, 1000);
} }
rearrangeHosts(oldIndex, newIndex) { rearrangeHosts(oldIndex, newIndex) {
const host = this.autoHosts.splice(oldIndex, 1)[0]; const host = this.autoHosts.splice(oldIndex, 1)[0];
this.autoHosts.splice(newIndex, 0, host); this.autoHosts.splice(newIndex, 0, host);
this.queueHostUpdate(); this.queueHostUpdate();
} }
currentRoomInHosts() { currentRoomInHosts() {
return this.getAutoHostIDs(this.autoHosts).includes(parseInt(this._current_channel_id, 10)); return this.getAutoHostIDs(this.autoHosts).includes(parseInt(this._current_channel_id, 10));
} }
@ -329,7 +329,7 @@ export default class HostButton extends Module {
removeUserFromHosts(event) { removeUserFromHosts(event) {
const id = event.target.closest('.ffz--host-user').dataset.id; const id = event.target.closest('.ffz--host-user').dataset.id;
const newHosts = []; const newHosts = [];
for (let i = 0; i < this.autoHosts.length; i++) { for (let i = 0; i < this.autoHosts.length; i++) {
if (this.autoHosts[i]._id != id) newHosts.push(this.autoHosts[i]); if (this.autoHosts[i]._id != id) newHosts.push(this.autoHosts[i]);

View file

@ -192,12 +192,12 @@ export default class Player extends Module {
disableAutoplay(inst) { disableAutoplay(inst) {
if ( ! inst.player ) { if ( ! inst.player ) {
this.log.warn("disableAutoplay() called but Player was not ready"); this.log.warn('disableAutoplay() called but Player was not ready');
return; return;
} }
if ( ! inst.ffzAutoplay ) { if ( ! inst.ffzAutoplay ) {
var playListener = () => { const playListener = () => {
this.log.info('Auto-paused player'); this.log.info('Auto-paused player');
inst.ffzAutoplay = null; inst.ffzAutoplay = null;
inst.player.pause(); inst.player.pause();
@ -209,6 +209,7 @@ export default class Player extends Module {
inst.player.removeEventListener('contentShowing', playListener); inst.player.removeEventListener('contentShowing', playListener);
}, 1000); }, 1000);
} }
inst.ffzAutoplay = playListener; inst.ffzAutoplay = playListener;
inst.player.addEventListener('play', inst.ffzAutoplay); inst.player.addEventListener('play', inst.ffzAutoplay);
inst.player.addEventListener('playing', inst.ffzAutoplay); inst.player.addEventListener('playing', inst.ffzAutoplay);

View file

@ -7,7 +7,6 @@
import Module from 'utilities/module'; import Module from 'utilities/module';
import {has, get} from 'utilities/object'; import {has, get} from 'utilities/object';
import gql from 'graphql-tag';
export default class Apollo extends Module { export default class Apollo extends Module {
constructor(...args) { constructor(...args) {
@ -16,17 +15,8 @@ export default class Apollo extends Module {
this.modifiers = {}; this.modifiers = {};
this.post_modifiers = {}; this.post_modifiers = {};
this.gql = gql;
this.inject('..web_munch'); this.inject('..web_munch');
this.inject('..fine'); this.inject('..fine');
this.registerModifier('ViewerCard', gql`query {
targetUser: user {
createdAt
profileViewCount
}
}`);
} }
async onEnable() { async onEnable() {
@ -34,10 +24,10 @@ export default class Apollo extends Module {
let client = this.client; let client = this.client;
if ( ! client ) { if ( ! client ) {
const root = this.fine.getParent(this.fine.react), const root = this.fine.react,
ctx = root && root._context; inst = root && root.stateNode;
client = this.client = ctx && ctx.client; client = this.client = inst && inst.props && inst.props.client;
} }
this.printer = this.web_munch.getModule('gql-printer'); this.printer = this.web_munch.getModule('gql-printer');

View file

@ -22,10 +22,8 @@ export default class FineRouter extends Module {
} }
onEnable() { onEnable() {
const root = this.fine.getParent(this.fine.react), const thing = this.fine.searchTree(null, n => n.props && n.props.history),
ctx = this.context = root && root._context, history = this.history = thing && thing.props && thing.props.history;
router = ctx && ctx.router,
history = this.history = router && router.history;
if ( ! history ) if ( ! history )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable()); return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());

View file

@ -24,20 +24,21 @@ export default class Fine extends Module {
async onEnable(tries=0) { async onEnable(tries=0) {
// TODO: Move awaitElement to utilities/dom // TODO: Move awaitElement to utilities/dom
if ( ! this.root_element ) if ( ! this.root_element )
this.root_element = await this.parent.awaitElement(this.selector || 'body [data-reactroot]'); this.root_element = await this.parent.awaitElement(this.selector || 'body #root');
const accessor = this.accessor = Fine.findAccessor(this.root_element); if ( ! this.root_element || ! this.root_element._reactRootContainer ) {
if ( ! accessor ) {
if ( tries > 500 ) if ( tries > 500 )
throw new Error(`unable to find React after 25 seconds`); throw new Error('Unable to find React after 25 seconds');
this.root_element = null;
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable(tries+1)); return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable(tries+1));
} }
this.react = this.getReactInstance(this.root_element); this.react_root = this.root_element._reactRootContainer;
this.react = this.react_root.current.child;
} }
onDisable() { onDisable() {
this.root_element = this.react = this.accessor = null; this.react_root = this.root_element = this.react = this.accessor = null;
} }
@ -53,101 +54,128 @@ export default class Fine extends Module {
// ======================================================================== // ========================================================================
getReactInstance(element) { getReactInstance(element) {
if ( ! this.accessor )
this.accessor = Fine.findAccessor(element);
if ( ! this.accessor )
return;
return element[this.accessor]; return element[this.accessor];
} }
getOwner(instance) { getOwner(instance) {
if ( instance._reactInternalInstance ) if ( instance._reactInternalFiber )
instance = instance._reactInternalInstance; instance = instance._reactInternalFiber;
else if ( instance instanceof Node ) else if ( instance instanceof Node )
instance = this.getReactInstance(instance); instance = this.getReactInstance(instance);
if ( ! instance ) if ( ! instance )
return null; return null;
return instance._owner || (instance._currentElement && instance._currentElement._owner); return instance.return;
} }
getHostNode(instance) { //eslint-disable-line class-methods-use-this getHostNode(instance) {
if ( instance._reactInternalInstance ) if ( instance._reactInternalFiber )
instance = instance._reactInternalInstance; instance = instance._reactInternalFiber;
else if ( instance instanceof Node ) else if ( instance instanceof Node )
instance = this.getReactInstance(instance); instance = this.getReactInstance(instance);
while( instance ) while( instance )
if ( instance._hostNode ) if ( instance.stateNode instanceof Node )
return instance._hostNode; return instance.stateNode
else else
instance = instance._renderedComponent; instance = instance.parent;
} }
getParent(instance) { getParent(instance) {
const owner = this.getOwner(instance); return this.getOwner(instance);
return owner && this.getOwner(owner); }
getFirstChild(node) {
if ( node._reactInternalFiber )
node = node._reactInternalFiber;
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( ! node )
return null;
return node.child;
}
getChildren(node) {
if ( node._reactInternalFiber )
node = node._reactInternalFiber;
else if ( node instanceof Node )
node = this.getReactInstance(node);
if ( ! node )
return null;
const children = [];
let child = node.child;
while(child) {
children.push(child);
child = child.sibling;
}
return children;
} }
searchParent(node, criteria, max_depth=15, depth=0) { searchParent(node, criteria, max_depth=15, depth=0) {
if ( node._reactInternalInstance ) if ( node._reactInternalFiber )
node = node._reactInternalInstance; node = node._reactInternalFiber;
else if ( node instanceof Node ) else if ( node instanceof Node )
node = this.getReactInstance(node); node = this.getReactInstance(node);
if ( ! node || depth > max_depth ) if ( ! node || depth > max_depth )
return null; return null;
const inst = node._instance; const inst = node.stateNode;
if ( inst && criteria(inst) ) if ( inst && criteria(inst) )
return inst; return inst;
if ( node._currentElement && node._currentElement._owner ) { if ( node.return ) {
const result = this.searchParent(node._currentElement._owner, criteria, max_depth, depth+1); const result = this.searchParent(node.return, criteria, max_depth, depth+1);
if ( result ) if ( result )
return result; return result;
} }
if ( node._hostParent )
return this.searchParent(node._hostParent, criteria, max_depth, depth+1);
return null; return null;
} }
searchTree(node, criteria, max_depth=15, depth=0) { searchTree(node, criteria, max_depth=15, depth=0) {
if ( ! node ) if ( ! node )
node = this.react; node = this.react;
else if ( node._reactInternalInstance ) else if ( node._reactInternalFiber )
node = node._reactInternalInstance; node = node._reactInternalFiber;
else if ( node instanceof Node ) else if ( node instanceof Node )
node = this.getReactInstance(node); node = this.getReactInstance(node);
if ( ! node || depth > max_depth ) if ( ! node || depth > max_depth )
return null; return null;
const inst = node._instance; const inst = node.stateNode;
if ( inst && criteria(inst) ) if ( inst && criteria(inst) )
return inst; return inst;
const children = node._renderedChildren, if ( node.child ) {
component = node._renderedComponent; let child = node.child;
while(child) {
if ( children ) const result = this.searchTree(child, criteria, max_depth, depth+1);
for(const key in children) if ( result )
if ( has(children, key) ) { return result;
const child = children[key]; child = child.sibling;
const result = child && this.searchTree(child, criteria, max_depth, depth+1); }
if ( result ) }
return result;
}
if ( component )
return this.searchTree(component, criteria, max_depth, depth+1);
} }
searchAll(node, criterias, max_depth=15, depth=0, data) { searchAll(node, criterias, max_depth=15, depth=0, data) {
if ( ! node ) if ( ! node )
node = this.react; node = this.react;
else if ( node._reactInternalInstance ) else if ( node._reactInternalFiber )
node = node._reactInternalInstance; node = node._reactInternalFiber;
else if ( node instanceof Node ) else if ( node instanceof Node )
node = this.getReactInstance(node); node = this.getReactInstance(node);
@ -167,7 +195,7 @@ export default class Fine extends Module {
if ( depth > data.max_depth ) if ( depth > data.max_depth )
data.max_depth = depth; data.max_depth = depth;
const inst = node._instance; const inst = node.stateNode;
if ( inst ) { if ( inst ) {
const cls = inst.constructor, const cls = inst.constructor,
idx = data.classes.indexOf(cls); idx = data.classes.indexOf(cls);
@ -189,18 +217,13 @@ export default class Fine extends Module {
} }
} }
const children = node._renderedChildren, if ( node.child ) {
component = node._renderedComponent; let child = node.child;
while(child) {
if ( children ) this.searchAll(child, criterias, max_depth, depth+1, data);
for(const key in children) child = child.sibling;
if ( has(children, key) ) { }
const child = children[key]; }
child && this.searchAll(child, criterias, max_depth, depth+1, data);
}
if ( component )
this.searchAll(component, criterias, max_depth, depth+1, data);
return data.out; return data.out;
} }
@ -400,8 +423,9 @@ export class FineWrapper extends EventEmitter {
if ( instances ) if ( instances )
for(const inst of instances) { for(const inst of instances) {
if ( inst._reactInternalInstance && inst._reactInternalInstance._renderedComponent ) // How do we check mounted state for fibers?
inst._ffz_mounted = true; //if ( inst._reactInternalInstance && inst._reactInternalInstance._renderedComponent )
// inst._ffz_mounted = true;
_instances.add(inst); _instances.add(inst);
} }

View file

@ -160,7 +160,7 @@ export class Tooltip {
if ( ! tip ) if ( ! tip )
return; return;
tip.state = false; tip.state = false;
if ( tip._show_timer ) { if ( tip._show_timer ) {
clearTimeout(tip._show_timer); clearTimeout(tip._show_timer);

View file

@ -45,6 +45,11 @@ module.exports = {
} }
}] }]
}, },
{
test: /\.(graphql|gql)$/,
exclude: /node_modules/,
loader: 'graphql-tag/loader'
},
{ {
test: /\.(?:eot|ttf|woff|woff2)$/, test: /\.(?:eot|ttf|woff|woff2)$/,
use: [{ use: [{