1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-09-16 10:06:54 +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

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

@ -1,67 +0,0 @@
.ffz-main-menu {
&:not(.maximized) {
position: absolute;
top: 25%;
left: 25%;
z-index: 99999999;
height: 50vh;
width: 50vw;
min-width: 640px;
min-height: 300px;
> header {
cursor: move;
}
&.faded {
opacity: 0.5;
&:hover {
opacity: 0.75;
}
}
}
&.maximized {
position: absolute;
top: 0 !important;
left: 0 !important;
width: 100%;
height: 100%;
&, .tw-theme--dark & {
border: none !important;
}
> header {
cursor: default;
padding: .9rem 3rem;
}
}
&.exclusive {
z-index: 1000;
}
.want-ps {
overflow-y: auto;
}
> header {
user-select: none;
padding: .9rem 1rem .9rem 2rem;
.tw-search-input {
max-width: 40rem
}
}
}
.ffz-has-menu > :not(.ffz-main-menu) {
visibility: hidden;
}

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>