diff --git a/package-lock.json b/package-lock.json
index 13fff87e..e0ce621b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 9ef1f16c..64616f93 100755
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/entry.js b/src/entry.js
index 11fb5af1..2e512f90 100644
--- a/src/entry.js
+++ b/src/entry.js
@@ -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);
})();
\ No newline at end of file
diff --git a/src/main.js b/src/main.js
index 3aa793b1..2b5204fe 100644
--- a/src/main.js
+++ b/src/main.js
@@ -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: () =>
diff --git a/src/modules/chat/badges.jsx b/src/modules/chat/badges.jsx
index e060a1a9..d0e81610 100644
--- a/src/modules/chat/badges.jsx
+++ b/src/modules/chat/badges.jsx
@@ -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(
{show_previews &&

}
- {bd.title}
+ {bd.title || global_badge.title}
);
/*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);
diff --git a/src/modules/main_menu/index.js b/src/modules/main_menu/index.js
index 1e9d9a2d..8428fe15 100644
--- a/src/modules/main_menu/index.js
+++ b/src/modules/main_menu/index.js
@@ -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();
diff --git a/src/modules/tooltips.js b/src/modules/tooltips.js
index 7d3827be..639e2f05 100644
--- a/src/modules/tooltips.js
+++ b/src/modules/tooltips.js
@@ -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', {
diff --git a/src/sites/twitch-clips/index.js b/src/sites/twitch-clips/index.js
new file mode 100644
index 00000000..aeef07ad
--- /dev/null
+++ b/src/sites/twitch-clips/index.js
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-clips/modules/chat/get_badges.gql b/src/sites/twitch-clips/modules/chat/get_badges.gql
new file mode 100644
index 00000000..3be5bf68
--- /dev/null
+++ b/src/sites/twitch-clips/modules/chat/get_badges.gql
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-clips/modules/chat/index.js b/src/sites/twitch-clips/modules/chat/index.js
new file mode 100644
index 00000000..ae810ab7
--- /dev/null
+++ b/src/sites/twitch-clips/modules/chat/index.js
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-clips/modules/chat/line.jsx b/src/sites/twitch-clips/modules/chat/line.jsx
new file mode 100644
index 00000000..9d6c8c45
--- /dev/null
+++ b/src/sites/twitch-clips/modules/chat/line.jsx
@@ -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 ()
+
+ } 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;
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-clips/modules/theme/index.js b/src/sites/twitch-clips/modules/theme/index.js
new file mode 100644
index 00000000..4ace5564
--- /dev/null
+++ b/src/sites/twitch-clips/modules/theme/index.js
@@ -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: 'Requires Dark Theme to be Enabled.
I see my website and I want it painted black...
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'));
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-clips/styles/chat.scss b/src/sites/twitch-clips/styles/chat.scss
new file mode 100644
index 00000000..fd6eed5e
--- /dev/null
+++ b/src/sites/twitch-clips/styles/chat.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-clips/styles/main.scss b/src/sites/twitch-clips/styles/main.scss
new file mode 100644
index 00000000..70a740bb
--- /dev/null
+++ b/src/sites/twitch-clips/styles/main.scss
@@ -0,0 +1,3 @@
+@import 'styles/main.scss';
+
+@import 'chat.scss';
\ No newline at end of file
diff --git a/src/sites/twitch-clips/switchboard.js b/src/sites/twitch-clips/switchboard.js
new file mode 100644
index 00000000..1b153614
--- /dev/null
+++ b/src/sites/twitch-clips/switchboard.js
@@ -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.');
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/index.js b/src/sites/twitch-twilight/index.js
index fa1bee96..3768883e 100644
--- a/src/sites/twitch-twilight/index.js
+++ b/src/sites/twitch-twilight/index.js
@@ -180,5 +180,6 @@ Twilight.ROUTES = {
'user-followers': '/:userName/followers',
'user-following': '/:userName/following',
'product': '/products/:productName',
+ 'prime': '/prime',
'user': '/:userName'
}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/modules/theme/index.js b/src/sites/twitch-twilight/modules/theme/index.js
index 2b94ebb9..a9dfd2a3 100644
--- a/src/sites/twitch-twilight/modules/theme/index.js
+++ b/src/sites/twitch-twilight/modules/theme/index.js
@@ -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 {
diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx
index 2505834d..91fe4e77 100644
--- a/src/sites/twitch-twilight/modules/video_chat/index.jsx
+++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx
@@ -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),
diff --git a/src/sites/twitch-twilight/styles/main.scss b/src/sites/twitch-twilight/styles/main.scss
index 5fb7e555..dfd4f149 100644
--- a/src/sites/twitch-twilight/styles/main.scss
+++ b/src/sites/twitch-twilight/styles/main.scss
@@ -1,7 +1,6 @@
@import 'styles/main.scss';
@import 'menu_button';
-@import 'main_menu';
@import 'player';
@import 'channel';
diff --git a/src/std-components/markdown.vue b/src/std-components/markdown.vue
new file mode 100644
index 00000000..7c9479a1
--- /dev/null
+++ b/src/std-components/markdown.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/styles/main.scss b/styles/main.scss
index b74910c1..521e7c71 100644
--- a/styles/main.scss
+++ b/styles/main.scss
@@ -1,6 +1,7 @@
@import 'icons';
@import 'tooltips';
@import 'widgets';
+@import 'main_menu';
@import 'chat';
diff --git a/src/sites/twitch-twilight/styles/main_menu.scss b/styles/main_menu.scss
similarity index 100%
rename from src/sites/twitch-twilight/styles/main_menu.scss
rename to styles/main_menu.scss
diff --git a/webpack.clips.babel.js b/webpack.clips.babel.js
new file mode 100644
index 00000000..5a04a021
--- /dev/null
+++ b/webpack.clips.babel.js
@@ -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)}`;
+}
\ No newline at end of file
diff --git a/webpack.clips.common.js b/webpack.clips.common.js
new file mode 100644
index 00000000..326357a0
--- /dev/null
+++ b/webpack.clips.common.js
@@ -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/')
+ }
+ }
+});
\ No newline at end of file
diff --git a/webpack.clips.dev.js b/webpack.clips.dev.js
new file mode 100644
index 00000000..079dcb7d
--- /dev/null
+++ b/webpack.clips.dev.js
@@ -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'
+ }
+})
\ No newline at end of file
diff --git a/webpack.clips.prod.js b/webpack.clips.prod.js
new file mode 100644
index 00000000..15efdf79
--- /dev/null
+++ b/webpack.clips.prod.js
@@ -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)}`;
+}
\ No newline at end of file