1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00

4.0.0-rc8

* Added: Initial support for chat on Clips.
* Fixed: Do not attempt to apply the dark theme to the Twitch Prime landing page.
* Fixed: Remove debug logging from Chat on Videos.
This commit is contained in:
SirStendec 2018-07-20 18:42:17 -04:00
parent ae9aa66799
commit 2297edb051
26 changed files with 962 additions and 34 deletions

41
package-lock.json generated
View file

@ -429,8 +429,7 @@
"asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
},
"asn1": {
"version": "0.2.3",
@ -2802,7 +2801,6 @@
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
"iconv-lite": "0.4.19"
}
@ -3599,7 +3597,6 @@
"version": "0.8.16",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.16.tgz",
"integrity": "sha1-XmdDL1UNxBtXK/VYR7ispk5TN9s=",
"dev": true,
"requires": {
"core-js": "1.2.7",
"isomorphic-fetch": "2.2.1",
@ -3613,8 +3610,7 @@
"core-js": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=",
"dev": true
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
}
}
},
@ -4494,8 +4490,7 @@
"iconv-lite": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==",
"dev": true
"integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
},
"icss-replace-symbols": {
"version": "1.1.0",
@ -5030,8 +5025,7 @@
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
"dev": true
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
},
"is-svg": {
"version": "2.1.0",
@ -5094,7 +5088,6 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"dev": true,
"requires": {
"node-fetch": "1.7.3",
"whatwg-fetch": "2.0.4"
@ -5845,7 +5838,6 @@
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"dev": true,
"requires": {
"encoding": "0.1.12",
"is-stream": "1.1.0"
@ -6046,8 +6038,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-copy": {
"version": "0.1.0",
@ -7168,7 +7159,6 @@
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"dev": true,
"requires": {
"asap": "2.0.6"
}
@ -7183,7 +7173,6 @@
"version": "15.6.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.1.tgz",
"integrity": "sha512-4ec7bY1Y66LymSUOH/zARVYObB23AT2h8cf6e/O6ZALB/N0sqZFEx7rq6EYPX2MkOdKORuooI/H5k9TlR4q7kQ==",
"dev": true,
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
@ -7361,6 +7350,17 @@
"integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=",
"dev": true
},
"react": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/react/-/react-16.4.1.tgz",
"integrity": "sha512-3GEs0giKp6E0Oh/Y9ZC60CmYgUPnp7voH9fbjWsvXtYFb4EWtgQub0ADSq0sJR0BbHc4FThLLtzlcFaFXIorwg==",
"requires": {
"fbjs": "0.8.16",
"loose-envify": "1.3.1",
"object-assign": "4.1.1",
"prop-types": "15.6.1"
}
},
"read-pkg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
@ -8001,8 +8001,7 @@
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
"dev": true
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"setprototypeof": {
"version": "1.1.0",
@ -9019,8 +9018,7 @@
"ua-parser-js": {
"version": "0.7.17",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==",
"dev": true
"integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g=="
},
"uc.micro": {
"version": "1.0.5",
@ -10115,8 +10113,7 @@
"whatwg-fetch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==",
"dev": true
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
},
"whet.extend": {
"version": "0.9.9",

View file

@ -8,8 +8,12 @@
"start": "webpack-dev-server --config webpack.web.dev.js",
"eslint": "eslint \"src/**/*.{js,jsx,vue}\"",
"dev": "webpack-dev-server --config webpack.web.dev.js",
"dev:clips": "webpack-dev-server --config webpack.clips.dev.js",
"dev:babel": "webpack-dev-server --config webpack.web.dev.babel.js",
"build:all": "npm run build && npm run build:babel && npm run build:clips && npm run build:clips:babel",
"build": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'",
"build:clips": "webpack --config webpack.clips.prod.js --define process.env.NODE_ENV='production'",
"build:clips:babel": "webpack --config webpack.clips.babel.js --define process.env.NODE_ENV='production'",
"build:stats": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production' --json > stats.json",
"build:babel": "webpack --config webpack.web.babel.js --define process.env.NODE_ENV='production'",
"build:prod": "webpack --config webpack.web.prod.js --define process.env.NODE_ENV='production'",
@ -59,6 +63,7 @@
"path-to-regexp": "^2.2.1",
"popper.js": "^1.14.3",
"raven-js": "^3.24.2",
"react": "^16.4.1",
"safe-regex": "^1.1.0",
"sortablejs": "^1.7.0",
"vue": "^2.5.16",

View file

@ -8,6 +8,7 @@
const DEBUG = localStorage.ffzDebugMode == 'true' && document.body.classList.contains('ffz-dev') && ! window.Ember,
SERVER = DEBUG ? '//localhost:8000' : '//cdn.frankerfacez.com',
BABEL = /Edge/.test(window.navigator.userAgent) ? 'babel/' : '',
CLIPS = /clips\.twitch\.tv/.test(location.hostname) ? 'clips/' : '',
FLAVOR = window.Ember ? 'umbral' : 'avalon',
script = document.createElement('script');
@ -15,6 +16,6 @@
script.id = 'ffz-script';
script.async = true;
script.crossOrigin = 'anonymous';
script.src = `${SERVER}/script/${BABEL}${FLAVOR}.js?_=${Date.now()}`;
script.src = `${SERVER}/script/${CLIPS}${BABEL}${FLAVOR}.js?_=${Date.now()}`;
document.head.appendChild(script);
})();

View file

@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-rc7.1',
major: 4, minor: 0, revision: 0, extra: '-rc8',
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>

View file

@ -268,13 +268,14 @@ export default class Badges extends Module {
for(const d of data) {
const p = d.provider;
if ( p === 'twitch' ) {
const bd = this.getTwitchBadge(d.badge, d.version, room_id, room_login);
const bd = this.getTwitchBadge(d.badge, d.version, room_id, room_login),
global_badge = this.getTwitchBadge(d.badge, d.version) || {};
if ( ! bd )
continue;
out.push(<div class="ffz-badge-tip">
{show_previews && <img class="preview-image ffz-badge" src={bd.image4x} />}
{bd.title}
{bd.title || global_badge.title}
</div>);
/*out.push(e('div', {className: 'ffz-badge-tip'}, [
@ -457,6 +458,7 @@ export default class Badges extends Module {
props = data.props;
props.className = 'ffz-tooltip ffz-badge';
props.key = `${props['data-provider']}-${props['data-badge']}`;
props['data-tooltip-type'] = 'badge';
props['data-badge-data'] = JSON.stringify(data.badges);

View file

@ -10,9 +10,9 @@ import {has, deep_copy} from 'utilities/object';
import {parse_path} from 'src/settings';
const EXCLUSIVE_SELECTOR = '.twilight-main,.twilight-minimal-root>div,.twilight-root>.tw-full-height',
MAXIMIZED_SELECTOR = '.twilight-main,.twilight-minimal-root,.twilight-root .dashboard-side-nav+.tw-full-height',
SELECTOR = '.twilight-root>.tw-full-height,.twilight-minimal-root>.tw-full-height';
const EXCLUSIVE_SELECTOR = '.twilight-main,.twilight-minimal-root>div,.twilight-root>.tw-full-height,.clips-root',
MAXIMIZED_SELECTOR = '.twilight-main,.twilight-minimal-root,.twilight-root .dashboard-side-nav+.tw-full-height,.clips-root>.tw-full-height .scrollable-area',
SELECTOR = '.twilight-root>.tw-full-height,.twilight-minimal-root>.tw-full-height,.clips-root>.tw-full-height .scrollable-area';
function format_term(term) {
return term.replace(/<[^>]*>/g, '').toLocaleLowerCase();

View file

@ -36,7 +36,7 @@ export default class TooltipProvider extends Module {
}
onEnable() {
const container = document.querySelector('.twilight-root,.twilight-minimal-root') || document.body,
const container = document.querySelector('.twilight-root,.twilight-minimal-root,.clips-root') || document.body,
is_minimal = container && container.classList.contains('twilight-minimal-root');
this.tips = new Tooltip(is_minimal ? '.twilight-minimal-root,body' : 'body #root,body', 'ffz-tooltip', {

View file

@ -0,0 +1,89 @@
'use strict';
// ============================================================================
// Site Support: Twitch Clips
// ============================================================================
import BaseSite from '../base';
import WebMunch from 'utilities/compat/webmunch';
import Fine from 'utilities/compat/fine';
import Apollo from 'utilities/compat/apollo';
import {createElement} from 'utilities/dom';
import MAIN_URL from 'site/styles/main.scss';
import Switchboard from './switchboard';
// ============================================================================
// The Site
// ============================================================================
export default class Clippy extends BaseSite {
constructor(...args) {
super(...args);
this.inject(WebMunch);
this.inject(Fine);
this.inject(Apollo, false);
this.inject(Switchboard);
}
onLoad() {
this.populateModules();
}
onEnable() {
const thing = this.fine.searchTree(null, n => n.props && n.props.store),
store = this.store = thing && thing.props && thing.props.store;
if ( ! store )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
// Share Context
store.subscribe(() => this.updateContext());
this.updateContext();
this.settings.updateContext({
clips: true
});
document.head.appendChild(createElement('link', {
href: MAIN_URL,
rel: 'stylesheet',
type: 'text/css',
crossOrigin: 'anonymouse'
}));
}
updateContext() {
try {
const state = this.store.getState(),
history = this.router && this.router.history;
this.settings.updateContext({
location: history && history.location,
ui: state && state.ui,
session: state && state.session
});
} catch(err) {
this.log.error('Error updating context.', err);
}
}
getSession() {
const state = this.store && this.store.getState();
return state && state.session;
}
getUser() {
if ( this._user )
return this._user;
const session = this.getSession();
return this._user = session && session.user;
}
}

View file

@ -0,0 +1,13 @@
query FFZ_GetBadges {
badges {
id
setID
version
title
clickAction
clickURL
image1x: imageURL(size: NORMAL)
image2x: imageURL(size: DOUBLE)
image4x: imageURL(size: QUADRUPLE)
}
}

View file

@ -0,0 +1,165 @@
'use strict';
// ============================================================================
// Chat Hooks
// ============================================================================
import {get} from 'utilities/object';
import {ColorAdjuster, Color} from 'utilities/color';
import Module from 'utilities/module';
import Line from './line';
import BADGE_QUERY from './get_badges.gql';
export default class Chat extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.colors = new ColorAdjuster;
this.inverse_colors = new ColorAdjuster;
this.inject('settings');
this.inject('i18n');
this.inject('chat');
this.inject('site');
this.inject('site.fine');
this.inject(Line);
this.ChatController = this.fine.define(
'clip-chat-controller',
n => n.filterChatLines
);
}
onEnable() {
this.chat.context.on('changed:chat.adjustment-mode', this.updateColors, this);
this.chat.context.on('changed:chat.adjustment-contrast', this.updateColors, this);
this.chat.context.on('changed:theme.is-dark', this.updateColors, this);
this.ChatController.on('mount', this.chatMounted, this);
this.ChatController.on('unmount', this.chatMounted, this);
this.ChatController.on('receive-props', this.chatUpdated, this);
this.ChatController.ready((cls, instances) => {
for(const inst of instances)
this.chatMounted(inst);
});
this.loadBadges();
this.updateColors();
}
updateColors() {
const is_dark = this.chat.context.get('theme.is-dark'),
mode = this.chat.context.get('chat.adjustment-mode'),
contrast = this.chat.context.get('chat.adjustment-contrast'),
c = this.colors,
ic = this.inverse_colors;
// TODO: Get the background color from the theme system.
// Updated: Use the lightest/darkest colors from alternating rows for better readibility.
c._base = is_dark ? '#191919' : '#e0e0e0'; //#0e0c13' : '#faf9fa';
c.mode = mode;
c.contrast = contrast;
ic._base = is_dark ? '#dad8de' : '#19171c';
ic.mode = mode;
ic.contrast = contrast;
this.line.updateLines();
}
async loadBadges() {
let data;
try {
data = await this.resolve('site.apollo').client.query({
query: BADGE_QUERY
});
} catch(err) {
this.log.warn('Error loading badge data.', err);
return;
}
if ( data && data.data && data.data.badges )
this.chat.badges.updateTwitchBadges(data.data.badges);
}
// ========================================================================
// Room Handling
// ========================================================================
addRoom(thing, props) {
if ( ! props )
props = thing.props;
const channel_id = get('data.clip.broadcaster.id', props);
if ( ! channel_id )
return null;
const room = thing._ffz_room = this.chat.getRoom(channel_id, null, false, true);
room.ref(thing);
return room;
}
removeRoom(thing) { // eslint-disable-line class-methods-use-this
if ( ! thing._ffz_room )
return;
thing._ffz_room.unref(thing);
thing._ffz_room = null;
}
// ========================================================================
// Chat Controller
// ========================================================================
chatMounted(chat, props) {
if ( ! props )
props = chat.props;
if ( ! this.addRoom(chat, props) )
return;
this.updateRoomBadges(chat, get('data.clip.video.owner.broadcastBadges', props));
}
chatUmounted(chat) {
this.removeRoom(chat);
}
chatUpdated(chat, props) {
if ( get('data.clip.broadcaster.id', props) !== get('data.clip.broadcaster.id', chat.props) ) {
this.chatUmounted(chat);
this.chatMounted(chat, props);
return;
}
const new_room_badges = get('data.clip.video.owner.broadcastBadges', props),
old_room_badges = get('data.clip.video.owner.broadcastBadges', chat.props);
if ( new_room_badges !== old_room_badges )
this.updateRoomBadges(chat, new_room_badges);
}
updateRoomBadges(chat, badges) { // eslint-disable-line class-methods-use-this
const room = chat._ffz_room;
if ( ! room )
return;
room.updateBadges(badges);
}
}

View file

@ -0,0 +1,166 @@
'use strict';
// ============================================================================
// Chat Line
// ============================================================================
import Module from 'utilities/module';
import {createElement} from 'react';
import { split_chars } from '../../../../utilities/object';
export default class Line extends Module {
constructor(...args) {
super(...args);
this.inject('settings');
this.inject('i18n');
this.inject('chat');
this.inject('site');
this.inject('site.fine');
this.ChatLine = this.fine.define(
'clip-chat-line',
n => n.renderFragments && n.renderUserBadges
);
}
onEnable() {
this.chat.context.on('changed:chat.emoji.style', this.updateLines, this);
this.chat.context.on('changed:chat.bits.stack', this.updateLines, this);
this.chat.context.on('changed:chat.badges.style', this.updateLines, this);
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this);
this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this);
this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this);
this.ChatLine.ready((cls, instances) => {
const t = this,
old_render = cls.prototype.render;
cls.prototype.render = function() {
try {
const msg = t.standardizeMessage(this.props.node, this.props.video),
is_action = msg.is_action,
user = msg.user,
color = t.parent.colors.process(user.color),
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, user);
return (<div class="tw-mg-b-1 tw-font-size-5 tw-c-text-alt clip-chat__message">
<div class="tw-animation tw-animation--animate tw-animation--duration-short tw-animation--fill-mode-both tw-animation--slide-in-bottom tw-animation--timing-ease" data-room-id={msg.roomID} data-room={msg.roomLogin} data-user-id={user.id} data-user={user.login}>
<span class="chat-line__message--badges">{
t.chat.badges.render(msg, createElement)
}</span>
<a
class="tw-font-size-5 tw-strong clip-chat__message-author notranslate"
href={`https://www.twitch.tv/${user.login}/clips`}
style={{color}}
>
<span class="chat-author__display-name">{ user.displayName }</span>
{user.isIntl && <span class="chat-author__intl-login"> ({user.login})</span>}
</a>
<span>{is_action ? ' ' : ': '}</span>
<span class="message" style={{color: is_action ? color : null}}>{
t.chat.renderTokens(tokens, createElement)
}</span>
</div>
</div>)
} catch(err) {
t.log.error(err);
t.log.capture(err, {extra:{props: this.props}});
}
return old_render.call(this);
}
this.ChatLine.forceUpdate();
});
}
updateLines() {
for(const inst of this.ChatLine.instances) {
const msg = inst.props.node;
if ( msg )
msg._ffz_message = null;
}
this.ChatLine.forceUpdate();
}
standardizeMessage(msg, video) {
if ( ! msg || ! msg.message )
return msg;
if ( msg._ffz_message )
return msg._ffz_message;
const room = this.chat.getRoom(video.owner.id, null, true, true),
author = msg.commenter,
badges = {};
if ( msg.message.userBadges )
for(const badge of msg.message.userBadges)
if ( badge )
badges[badge.setID] = badge.version;
const out = msg._ffz_message = {
user: {
color: author.chatColor,
id: author.id,
login: author.login,
displayName: author.displayName,
isIntl: author.name && author.displayName && author.displayName.trim().toLowerCase() !== author.name,
type: 'user'
},
roomLogin: room && room.login,
roomID: room && room.id,
badges,
messageParts: msg.message.fragments
};
this.detokenizeMessage(out, msg);
return out;
}
detokenizeMessage(msg) { // eslint-disable-line class-methods-use-this
const out = [],
parts = msg.messageParts,
l = parts.length,
emotes = {};
let idx = 0;
for(let i=0; i < l; i++) {
const part = parts[i],
text = part && part.text;
if ( ! text || ! text.length )
continue;
const len = split_chars(text).length;
if ( part.emote ) {
const id = part.emote.emoteID,
em = emotes[id] = emotes[id] || [];
em.push({startIndex: idx, endIndex: idx + len - 1});
}
out.push(text);
idx += len;
}
msg.message = out.join('');
msg.ffz_emotes = emotes;
return msg;
}
}

View file

@ -0,0 +1,102 @@
'use strict';
// ============================================================================
// Menu Module
// ============================================================================
import Module from 'utilities/module';
import {createElement} from 'utilities/dom';
//import THEME_CSS_URL from 'site/styles/theme.scss';
export default class ThemeEngine extends Module {
constructor(...args) {
super(...args);
this.inject('settings');
this.inject('site');
this.should_enable = true;
this.settings.add('theme.dark', {
requires: ['theme.is-dark'],
default: false,
process(ctx, val) {
return ctx.get('theme.is-dark') ? val : false
},
ui: {
path: 'Appearance @{"description": "Personalize the appearance of Twitch. Change the color scheme and fonts and tune the layout to optimize your experience."} > Theme >> General',
title: 'Gray (no Purple)',
description: '<em>Requires Dark Theme to be Enabled.</em><br>I see my website and I want it painted black...<br>This is a very early feature and will change as there is time.',
component: 'setting-check-box'
},
changed: val => this.updateSetting(val)
});
this.settings.add('theme.can-dark', {
requires: ['context.route.name'],
process(ctx) {
return true;
}
});
this.settings.add('theme.is-dark', {
requires: ['theme.can-dark', 'context.ui.theme'],
process(ctx) {
return ctx.get('theme.can-dark') && ctx.get('context.ui.theme') === 1;
},
changed: () => this.updateCSS()
});
this.settings.add('theme.tooltips-dark', {
requires: ['theme.is-dark'],
process(ctx) {
return ! ctx.get('theme.is-dark')
}
});
this._style = null;
}
updateCSS() {
const dark = this.settings.get('theme.is-dark'),
gray = this.settings.get('theme.dark');
document.body.classList.toggle('tw-theme--dark', dark);
document.body.classList.toggle('tw-theme--ffz', gray);
}
toggleStyle(enable) {
if ( ! this._style ) {
if ( ! enable )
return;
this._style = createElement('link', {
rel: 'stylesheet',
type: 'text/css',
//href: THEME_CSS_URL
});
} else if ( ! enable ) {
this._style.remove();
return;
}
document.head.appendChild(this._style);
}
updateSetting(enable) {
this.toggleStyle(enable);
this.updateCSS();
}
onEnable() {
this.updateSetting(this.settings.get('theme.dark'));
}
}

View file

@ -0,0 +1,25 @@
.chat-line__message--emote {
vertical-align: middle;
margin: -.5rem 0;
}
.chat-author__display-name,
.chat-author__intl-login {
cursor: pointer;
}
.ffz-emoji {
width: calc(var(--ffz-chat-font-size) * 1.5);
height: calc(var(--ffz-chat-font-size) * 1.5);
&.preview-image {
width: 7.2rem;
height: 7.2rem;
}
&.emote-autocomplete-provider__image {
width: 1.8rem;
height: 1.8rem;
margin: .5rem;
}
}

View file

@ -0,0 +1,3 @@
@import 'styles/main.scss';
@import 'chat.scss';

View file

@ -0,0 +1,80 @@
'use strict';
// ============================================================================
// Switchboard
// A hack for React Router to make it load a module.
// ============================================================================
import Module from 'utilities/module';
import pathToRegexp from 'path-to-regexp';
export default class Switchboard extends Module {
constructor(...args) {
super(...args);
this.inject('site.web_munch');
this.inject('site.fine');
}
async onEnable() {
await this.parent.awaitElement('.clips-root');
if ( this.web_munch._require || this.web_munch.v4 === false )
return;
const da_switch = this.fine.searchTree(null, n =>
n.context && n.context.router &&
n.props && n.props.children &&
n.componentWillMount && n.componentWillMount.toString().includes('Switch')
);
if ( ! da_switch )
return new Promise(r => setTimeout(r, 50)).then(() => this.onEnable());
// Identify Router
this.log.info(`Found Switch with ${da_switch.props.children.length} routes.`);
const location = da_switch.context.router.route.location.pathname;
for(const route of da_switch.props.children) {
if ( ! route.props || ! route.props.component )
continue;
try {
const reg = pathToRegexp(route.props.path);
if ( ! reg.exec || reg.exec(location) )
continue;
} catch(err) {
continue;
}
this.log.info('Found Non-Matching Route', route.props.path);
let component;
try {
component = new route.props.component;
} catch(err) {
this.log.error('Error instantiating component for forced chunk loading.', err);
component = null;
}
if ( ! component || ! component.props || ! component.props.children || ! component.props.children.props || ! component.props.children.props.loader )
continue;
try {
component.props.children.props.loader().then(() => {
this.log.info('Successfully forced a chunk to load using route', route.props.path)
});
} catch(err) {
this.log.warn('Unexpected result trying to use component loader to force loading of another chunk.');
}
return;
}
this.log.warn('Unable to use any of the available routes.');
}
}

View file

@ -180,5 +180,6 @@ Twilight.ROUTES = {
'user-followers': '/:userName/followers',
'user-following': '/:userName/following',
'product': '/products/:productName',
'prime': '/prime',
'user': '/:userName'
}

View file

@ -9,7 +9,7 @@ import {createElement} from 'utilities/dom';
import THEME_CSS_URL from 'site/styles/theme.scss';
const BAD_ROUTES = ['product'];
const BAD_ROUTES = ['product', 'prime'];
export default class ThemeEngine extends Module {

View file

@ -195,8 +195,6 @@ export default class VideoChatHook extends Module {
if ( this.state.showReplyForm || ! t.chat.context.get('chat.video-chat.enabled') )
return old_render.call(this);
t.log.info('Video Chat', this);
const context = this.props.messageContext,
msg = t.standardizeMessage(context.comment, context.author),
main_message = this.ffzRenderMessage(msg),

View file

@ -1,7 +1,6 @@
@import 'styles/main.scss';
@import 'menu_button';
@import 'main_menu';
@import 'player';
@import 'channel';

View file

@ -0,0 +1,28 @@
<template>
<div v-html="output" />
</template>
<script>
import MD from 'markdown-it';
export default {
props: {
source: String
},
computed: {
md() {
return new MD({
html: false,
linkify: true
});
},
output() {
return this.md.render(this.source);
}
}
}
</script>

View file

@ -1,6 +1,7 @@
@import 'icons';
@import 'tooltips';
@import 'widgets';
@import 'main_menu';
@import 'chat';

82
webpack.clips.babel.js Normal file
View file

@ -0,0 +1,82 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.clips.common.js');
const path = require('path');
const CleanPlugin = require('clean-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const commit_hash = require('child_process').execSync('git rev-parse HEAD').toString().trim();
/* global module __dirname */
const config = module.exports = merge(common, {
devtool: 'source-map',
module: {
rules: [{
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: ['transform-es2015-classes']
}
}
}]
},
plugins: [
new CleanPlugin(['dist/clips/babel']),
new UglifyJSPlugin({
sourceMap: true,
uglifyOptions: {
compress: {
keep_fnames: true,
keep_classnames: true
},
mangle: {
keep_classnames: true,
keep_fnames: true
}
}
}),
new webpack.DefinePlugin({
__git_commit__: JSON.stringify(commit_hash)
}),
new ManifestPlugin({
basePath: 'clips/babel/',
publicPath: 'clips/babel/',
map: data => {
if ( data.name.endsWith('.scss') )
data.name = `${data.name.substr(0,data.name.length - 5)}.css`;
return data;
}
})
],
output: {
publicPath: '//cdn.frankerfacez.com/static/clips/babel/',
path: path.resolve(__dirname, 'dist/clips/babel'),
filename: '[name].[hash].js'
}
});
// This is why we can't have nice things.
// Why can't I just access process.env.NODE_ENV from
// one of these files when I set it with webpack's
// CLI? So stupid.
//
// So here we go.
// This is crap.
// But it works.
for(const rule of config.module.rules) {
if ( Array.isArray(rule.use) )
for(const use of rule.use)
if ( use.options && use.options.name && use.options.name.startsWith('[name].') )
use.options.name = `[name].[hash].${use.options.name.slice(7)}`;
}

13
webpack.clips.common.js Normal file
View file

@ -0,0 +1,13 @@
const path = require('path');
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
/* global module __dirname */
module.exports = merge(common, {
resolve: {
alias: {
site: path.resolve(__dirname, 'src/sites/twitch-clips/')
}
}
});

73
webpack.clips.dev.js Normal file
View file

@ -0,0 +1,73 @@
/* eslint-disable */
const path = require('path');
const merge = require('webpack-merge');
const common = require('./webpack.clips.common.js');
const CopyPlugin = require('copy-webpack-plugin');
const webpack = require('webpack');
/* global module */
module.exports = merge(common, {
devtool: 'inline-source-map',
plugins: [
new CopyPlugin([
{
from: './src/entry.js',
to: 'script.js'
}
]),
new webpack.DefinePlugin({
__git_commit__: null
})
],
devServer: {
https: true,
port: 8000,
compress: true,
inline: false,
allowedHosts: [
'.twitch.tv',
'.frankerfacez.com'
],
contentBase: path.join(__dirname, 'dev_cdn'),
publicPath: '/script/clips/',
proxy: {
'**': {
target: 'http://cdn.frankerfacez.com/',
changeOrigin: true
}
},
before(app) {
// Because the headers config option is broken.
app.get('/*', (req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
next();
});
app.get('/script/script.js', (req, res, next) => {
req.url = req.url.replace(/^\/script/, '/script/clips');
next();
});
app.get('/dev_server', (req, res) => {
res.json({
path: process.cwd(),
version: 2
})
});
}
},
output: {
publicPath: '//localhost:8000/script/clips/',
filename: '[name].js',
jsonpFunction: 'ffzWebpackJsonp'
}
})

85
webpack.clips.prod.js Normal file
View file

@ -0,0 +1,85 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const common = require('./webpack.clips.common.js');
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const CleanPlugin = require('clean-webpack-plugin');
const uglify = require('uglify-es');
// Get Git info
const commit_hash = require('child_process').execSync('git rev-parse HEAD').toString().trim();
/* global module Buffer __dirname */
const config = module.exports = merge(common, {
devtool: 'source-map',
plugins: [
new CleanPlugin(['dist/clips']),
new UglifyJSPlugin({
sourceMap: true,
uglifyOptions: {
compress: {
keep_fnames: true,
keep_classnames: true
},
mangle: {
keep_classnames: true,
keep_fnames: true
}
}
}),
new webpack.DefinePlugin({
__git_commit__: JSON.stringify(commit_hash)
}),
new CopyPlugin([
{
from: './src/entry.js',
to: 'script.min.js',
transform: content => {
const text = content.toString('utf8');
const minified = uglify.minify(text);
return (minified && minified.code) ? Buffer.from(minified.code) : content;
}
}
]),
new ManifestPlugin({
basePath: 'clips/',
publicPath: 'clips/',
map: data => {
if ( data.name.endsWith('.scss') )
data.name = `${data.name.substr(0,data.name.length - 5)}.css`;
return data;
}
})
],
output: {
publicPath: '//cdn.frankerfacez.com/static/clips/',
path: path.resolve(__dirname, 'dist/clips'),
filename: '[name].[hash].js'
}
});
// This is why we can't have nice things.
// Why can't I just access process.env.NODE_ENV from
// one of these files when I set it with webpack's
// CLI? So stupid.
//
// So here we go.
// This is crap.
// But it works.
for(const rule of config.module.rules) {
if ( Array.isArray(rule.use) )
for(const use of rule.use)
if ( use.options && use.options.name && use.options.name.startsWith('[name].') )
use.options.name = `[name].[hash].${use.options.name.slice(7)}`;
}