From 2297edb0514753526a1bdf33a824beadb6fe1f43 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Fri, 20 Jul 2018 18:42:17 -0400 Subject: [PATCH] 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. --- package-lock.json | 41 ++--- package.json | 5 + src/entry.js | 3 +- src/main.js | 2 +- src/modules/chat/badges.jsx | 6 +- src/modules/main_menu/index.js | 6 +- src/modules/tooltips.js | 2 +- src/sites/twitch-clips/index.js | 89 ++++++++++ .../twitch-clips/modules/chat/get_badges.gql | 13 ++ src/sites/twitch-clips/modules/chat/index.js | 165 +++++++++++++++++ src/sites/twitch-clips/modules/chat/line.jsx | 166 ++++++++++++++++++ src/sites/twitch-clips/modules/theme/index.js | 102 +++++++++++ src/sites/twitch-clips/styles/chat.scss | 25 +++ src/sites/twitch-clips/styles/main.scss | 3 + src/sites/twitch-clips/switchboard.js | 80 +++++++++ src/sites/twitch-twilight/index.js | 1 + .../twitch-twilight/modules/theme/index.js | 2 +- .../modules/video_chat/index.jsx | 2 - src/sites/twitch-twilight/styles/main.scss | 1 - src/std-components/markdown.vue | 28 +++ styles/main.scss | 1 + .../styles => styles}/main_menu.scss | 0 webpack.clips.babel.js | 82 +++++++++ webpack.clips.common.js | 13 ++ webpack.clips.dev.js | 73 ++++++++ webpack.clips.prod.js | 85 +++++++++ 26 files changed, 962 insertions(+), 34 deletions(-) create mode 100644 src/sites/twitch-clips/index.js create mode 100644 src/sites/twitch-clips/modules/chat/get_badges.gql create mode 100644 src/sites/twitch-clips/modules/chat/index.js create mode 100644 src/sites/twitch-clips/modules/chat/line.jsx create mode 100644 src/sites/twitch-clips/modules/theme/index.js create mode 100644 src/sites/twitch-clips/styles/chat.scss create mode 100644 src/sites/twitch-clips/styles/main.scss create mode 100644 src/sites/twitch-clips/switchboard.js create mode 100644 src/std-components/markdown.vue rename {src/sites/twitch-twilight/styles => styles}/main_menu.scss (100%) create mode 100644 webpack.clips.babel.js create mode 100644 webpack.clips.common.js create mode 100644 webpack.clips.dev.js create mode 100644 webpack.clips.prod.js 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 (
+
+ { + t.chat.badges.render(msg, createElement) + } + + { user.displayName } + {user.isIntl && } + + {is_action ? ' ' : ': '} + { + t.chat.renderTokens(tokens, createElement) + } +
+
) + + } 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