1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-02 07:58:31 +00:00

More work on viewer cards. Enable the rich card provider for videos. Start working on a standard data model for chat lines. Add a dev build notice to the menu button. Work on player no-hosting logic. Work on getting Room lines ready. Attempt to fix webmunch not working with a read-only webpackJsonp. Start adding logviewer support.

This commit is contained in:
SirStendec 2018-05-18 02:10:00 -04:00
parent 40a829355f
commit 6c77e2ca5c
26 changed files with 809 additions and 103 deletions

View file

@ -1,3 +1,11 @@
<div class="list-header">4.0.0-rc1.5<span>@eb1433f63b4667bf9010</span> <time datetime="2018-05-10">(2018-05-10)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Basic support for rich video cards in chat.</li>
<li>Changed: Begin working towards a standard data model for chat lines.</li>
<li>Fixed: More robust handling of <code>webpackJsonp</code> wrapping to deal with read-only variables.</li>
<li>Fixed: The option to disable channel hosting not always working after the client refreshes data.</li>
</ul>
<div class="list-header">4.0.0-rc1.5<span>@eb1433f63b4667bf9010</span> <time datetime="2018-05-10">(2018-05-10)</time></div>
<ul class="chat-menu-content menu-side-padding">
<li>Added: Option to hide the mouse cursor over the player. This does not work consistently in some web browser due to browser policies preventing the cursor from being hidden in certain situations.</li>

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: '-rc1.5',
major: 4, minor: 0, revision: 0, extra: '-rc1.6',
build: __webpack_hash__,
toString: () =>
`${VER.major}.${VER.minor}.${VER.revision}${VER.extra || ''}${DEBUG ? '-dev' : ''}`

View file

@ -166,7 +166,7 @@ export default class Actions extends Module {
renderInline(msg, mod_icons, current_user, current_room, createElement) {
const actions = [];
if ( msg.user && current_user && current_user.login === msg.user.userLogin )
if ( msg.user && current_user && current_user.login === msg.user.login )
return;
const chat = this.resolve('site.chat');
@ -209,10 +209,10 @@ export default class Actions extends Module {
const room = current_room && JSON.stringify(current_room),
user = msg.user && JSON.stringify({
login: msg.user.userLogin,
displayName: msg.user.userDisplayName,
id: msg.user.userID,
type: msg.user.userType
login: msg.user.login,
displayName: msg.user.displayName,
id: msg.user.id,
type: msg.user.type
});
return (<div

View file

@ -438,6 +438,154 @@ export default class Chat extends Module {
}
standardizeMessage(msg) { // eslint-disable-line class-methods-use-this
if ( ! msg )
return msg;
// Standardize User
if ( msg.sender && ! msg.user )
msg.user = msg.sender;
let user = msg.user;
if ( ! user )
user = msg.user = {};
user.color = user.color || user.chatColor || null;
user.type = user.type || user.userType || null;
user.id = user.id || user.userID || null;
user.login = user.login || user.userLogin || null;
user.displayName = user.displayName || user.userDisplayName || user.login;
user.isIntl = user.login && user.displayName && user.displayName.trim().toLowerCase() !== user.login;
// Standardize Message Content
if ( ! msg.message && msg.messageParts )
this.detokenizeMessage(msg);
if ( msg.content && ! msg.message ) {
if ( msg.content.fragments )
this.detokenizeContent(msg);
else
msg.message = msg.content.text;
}
// Standardize Badges
if ( ! msg.badges && user.displayBadges ) {
const b = msg.badges = {};
for(const item of msg.user.displayBadges)
b[item.setID] = item.version;
}
// Standardize Timestamp
if ( ! msg.timestamp && msg.sentAt )
msg.timestamp = new Date(msg.sentAt).getTime();
// Standardize Deletion
if ( msg.deletedAt !== undefined )
msg.deleted = !!msg.deletedAt;
return msg;
}
detokenizeContent(msg) { // eslint-disable-line class-methods-use-this
const out = [],
parts = msg.content.fragments,
l = parts.length,
emotes = {};
let idx = 0, ret, first = true;
for(let i=0; i < l; i++) {
const part = parts[i],
content = part.content,
ct = content && content.__typename;
ret = part.text;
if ( ct === 'Emote' ) {
const id = content.emoteID,
em = emotes[id] = emotes[id] || [];
em.push({startIndex: idx, endIndex: idx + ret.length - 1});
}
if ( ret && ret.length ) {
if ( first && ret.startsWith('/me ') ) {
msg.is_action = true;
ret = ret.slice(4);
}
idx += ret.length;
out.push(ret);
}
}
msg.message = out.join('');
msg.emotes = emotes;
return msg;
}
detokenizeMessage(msg) { // eslint-disable-line class-methods-use-this
const out = [],
parts = msg.messageParts,
l = parts.length,
emotes = {};
let idx = 0, ret, last_type = null;
for(let i=0; i < l; i++) {
const part = parts[i],
content = part.content;
if ( ! content )
continue;
if ( typeof content === 'string' )
ret = content;
else if ( content.recipient )
ret = `@${content.recipient}`;
else if ( content.url )
ret = content.url;
else if ( content.cheerAmount )
ret = `${content.alt}${content.cheerAmount}`;
else if ( content.images ) {
const url = (content.images.themed ? content.images.dark : content.images.sources),
match = url && /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url['1x']),
id = match && match[1];
ret = content.alt;
if ( id ) {
const em = emotes[id] = emotes[id] || [],
offset = last_type > 0 ? 1 : 0;
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
}
if ( last_type > 0 )
ret = ` ${ret}`;
} else
continue;
if ( ret ) {
idx += ret.length;
last_type = part.type;
out.push(ret)
}
}
msg.message = out.join('');
msg.emotes = emotes;
return msg;
}
formatTime(time) { // eslint-disable-line class-methods-use-this
if (!( time instanceof Date ))
time = new Date(time);

View file

@ -5,10 +5,10 @@
// ============================================================================
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
//const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
import GET_CLIP from './clip_info.gql';
//import GET_VIDEO from './video_info.gql';
import GET_VIDEO from './video_info.gql';
// ============================================================================
@ -41,7 +41,7 @@ export const Clips = {
}
});
if ( ! result || ! result.data || ! result.data.clip )
if ( ! result || ! result.data || ! result.data.clip || ! result.data.clip.broadcaster )
return null;
const clip = result.data.clip,
@ -71,7 +71,7 @@ export const Clips = {
title: clip.title,
desc_1,
desc_2: this.i18n.t('clip.desc.2', 'Clipped by %{curator} — %{views|number} View%{views|en_plural}', {
curator: clip.curator.displayName,
curator: clip.curator ? clip.curator.displayName : this.i18n.t('clip.unknown', 'Unknown'),
views: clip.viewCount
})
}
@ -81,7 +81,7 @@ export const Clips = {
}
/*export const Videos = {
export const Videos = {
type: 'video',
hide_token: true,
@ -105,6 +105,9 @@ export const Clips = {
}
});
if ( ! result || ! result.data || ! result.data.video || ! result.data.video.owner )
return null;
const video = result.data.video,
user = video.owner.displayName,
game = video.game,
@ -131,13 +134,13 @@ export const Clips = {
image: video.previewThumbnailURL,
title: video.title,
desc_1,
/*desc_2: this.i18n.t('video.desc.2', '%{length} %{views} Views - %{date}', {
desc_2: this.i18n.t('video.desc.2', '%{length} — %{views} Views - %{date}', {
length: video.lengthSeconds,
views: video.viewCount,
date: video.publishedAt
})/
})
}
}
}
}
}*/
}

View file

@ -0,0 +1,95 @@
'use strict';
// ============================================================================
// Logviewer Integration
// ============================================================================
import {once} from 'utilities/object';
import Module from 'utilities/module';
import LVSocketClient from './socket';
export default class Logviewer extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.inject('site');
this.inject('socket');
this.inject('viewer_cards');
this.inject('lv_socket', LVSocketClient);
}
get token() {
const token = this._token;
if ( token && token.token && token.expires > ((Date.now() / 1000) + 300) )
return token.token;
}
onEnable() {
this.viewer_cards.addTab('logs', {
visible: true,
label: 'Chat History',
component: () => import(/* webpackChunkName: 'viewer-cards' */ './tab-logs.vue')
});
this.on('viewer_cards:open', this.onCardOpen);
this.on('viewer_cards:close', this.onCardClose);
this.on('viewer_cards:load', this.onCardLoad);
}
onCardOpen() {
// We're going to need a token soon, so make sure we have one.
this.getToken();
}
onCardLoad(card) {
this.log.info('card:load', card);
if ( ! card.channel || ! card.user )
return;
card.lv_topic = `logs-${card.channel.login}-${card.channel.user}`;
this.lv_socket.subscribe(card, card.lv_topic);
}
onCardClose(card) {
this.log.info('card:close', card);
if ( card.lv_topic ) {
this.lv_socket.unsubscribe(card, card.lv_topic);
card.lv_topic = null;
}
}
async getToken() {
const user = this.site.getUser(),
token = this._token,
now = Date.now() / 1000;
if ( ! user || ! user.login )
return null;
if ( token && token.token && token.expires > (now + 300) )
return token.token;
const new_token = this._token = await this.socket.call('get_logviewer_token');
if ( new_token ) {
this.lv_socket.maybeSendToken();
return new_token.token;
}
}
}
Logviewer.getToken = once(Logviewer.getToken);

View file

@ -0,0 +1,339 @@
'use strict';
// ============================================================================
// Socket Client
// This connects to Logviewer's socket.io server for PubSub.
// ============================================================================
import Module from 'utilities/module';
import {LV_SOCKET_SERVER} from 'utilities/constants';
export const State = {
DISCONNECTED: 0,
CONNECTING: 1,
CONNECTED: 2
}
export default class LVSocketClient extends Module {
constructor(...args) {
super(...args);
this._topics = new Map;
this._socket = null;
this._state = 0;
this._delay = 0;
this.ping_interval = 25000;
}
// ========================================================================
// Properties
// ========================================================================
get connected() {
return this._state === State.CONNECTED;
}
get connecting() {
return this._state === State.CONNECTING;
}
get disconnected() {
return this._state === State.DISCONNECTED;
}
// ========================================================================
// Connection Logic
// ========================================================================
scheduleDisconnect() {
if ( this._disconnect_timer ) {
clearTimeout(this._disconnect_timer);
this._disconnect_timer = null;
}
if ( this.disconnected || this._topics.size )
return;
this._disconnect_timer = setTimeout(() => this.disconnect(), 5000);
}
scheduleReconnect() {
if ( this._reconnect_timer )
return;
if ( this._delay < 60000 )
this._delay += (Math.floor(Math.random() * 10) + 5) * 1000;
else
this._delay = (Math.floor(Math.random() * 60) + 30) * 1000;
this._reconnect_timer = setTimeout(() => this.connect(), this._delay);
}
reconnect() {
this.disconnect();
this.scheduleReconnect();
}
connect() {
this.want_connection = true;
this.clearTimers();
if ( ! this.disconnected )
return;
this._state = State.CONNECTING;
this._delay = 0;
const host = `${LV_SOCKET_SERVER}?EIO=3&transport=websocket`;
this.log.info(`Using Server: ${host}`);
let ws;
try {
ws = this._socket = new WebSocket(host);
} catch(err) {
this._state = State.DISCONNECTED;
this.scheduleReconnect();
this.log.error('Unable to create WebSocket.', err);
return;
}
ws.onopen = () => {
if ( this._socket !== ws ) {
this.log.warn('A socket connected that is not our primary socket.');
return ws.close();
}
this._state = State.CONNECTED;
this._sent_token = false;
ws.send('2probe');
ws.send('5');
this.maybeSendToken();
for(const topic of this._topics.keys())
this.send('subscribe', topic);
this.log.info('Connected.');
this.emit(':connected');
}
ws.onclose = event => {
if ( ws !== this._socket )
return;
this._state = State.DISCONNECTED;
this.log.info(`Disconnected. (${event.code}:${event.reason})`);
this.emit(':closed', event.code, event.reason);
if ( ! this.want_connection )
return;
this.clearTimers();
this.scheduleReconnect();
}
ws.onmessage = event => {
if ( ws !== this._socket || ! event.data )
return;
const raw = event.data,
type = raw.charAt(0);
if ( type === '0' ) {
// OPEN. Try reading ping interval.
try {
const data = JSON.parse(raw.slice(1));
this.ping_interval = data.ping_interval || 25000;
} catch(err) { /* don't care */ }
} else if ( type === '1' ) {
// CLOSE. We should never get this, but whatever.
ws.close();
} else if ( type === '2' ) {
// PING. Respone with a PONG. Shouldn't get this.
ws.send(`3${raw.slice(1)}`);
} else if ( type === '3' ) {
// PONG. Wait for the next ping.
this.schedulePing();
} else if ( type === '4' ) {
const dt = raw.charAt(1);
if ( dt === '0' ) {
// This is sent at connection. Who knows what it is.
} else if ( dt === '2' ) {
let data;
try {
data = JSON.parse(raw.slice(2));
} catch(err) {
this.log.warn('Error decoding packet.', err);
return;
}
this.emit(':message', ...data);
} else
this.log.debug('Unexpected Data Type', raw);
} else if ( type === '6' ) {
// NOOP.
} else
this.log.debug('Unexpected Packet Type', raw);
}
}
clearTimers() {
if ( this._ping_timer ) {
clearTimeout(this._ping_timer);
this._ping_timer = null;
}
if ( this._reconnect_timer ) {
clearTimeout(this._reconnect_timer);
this._reconnect_timer = null;
}
if ( this._disconnect_timer ) {
clearTimeout(this._disconnect_timer);
this._disconnect_timer = null;
}
}
disconnect() {
this.want_connection = false;
this.clearTimers();
if ( this.disconnected )
return;
try {
this._socket.close();
} catch(err) { /* if this caused an exception, we don't care -- it's still closed */ }
this._socket = null;
this._state = State.DISCONNECTED;
this.log.info(`Disconnected. (1000:)`);
this.emit(':closed', 1000, null);
}
// ========================================================================
// Communication
// ========================================================================
maybeSendToken() {
if ( ! this.connected )
return;
const token = this.parent.token;
if ( token ) {
this.send('token', token);
this._sent_token = true;
}
}
send(...args) {
if ( ! this.connected )
return;
this._socket.send(`42${JSON.stringify(args)}`);
}
schedulePing() {
if ( this._ping_timer )
clearTimeout(this._ping_timer);
this._ping_timer = setTimeout(() => this.ping(), this.ping_interval);
}
ping() {
if ( ! this.connected )
return;
if ( this._ping_timer ) {
clearTimeout(this._ping_timer);
this._ping_timer = null;
}
this._socket.send('2');
}
// ========================================================================
// Topics
// ========================================================================
subscribe(referrer, ...topics) {
const t = this._topics;
for(const topic of topics) {
if ( ! t.has(topic) ) {
if ( this.connected )
this.send('subscribe', topic);
else if ( this.disconnected )
this.connect();
t.set(topic, new Set);
}
const tp = t.get(topic);
tp.add(referrer);
}
this.scheduleDisconnect();
}
unsubscribe(referrer, ...topics) {
const t = this._topics;
for(const topic of topics) {
if ( ! t.has(topic) )
continue;
const tp = t.get(topic);
tp.delete(referrer);
if ( ! tp.size ) {
t.delete(topic);
if ( this.connected )
this.send('unsubscribe', topic);
}
}
this.scheduleDisconnect();
}
get topics() {
return Array.from(this._topics.keys());
}
}
LVSocketClient.State = State;

View file

@ -0,0 +1,33 @@
<template>
<div>
<error-tab v-if="errored" />
<loading-tab v-else-if="loading" />
<template v-else>
{{ data }}
</template>
</div>
</template>
<script>
import LoadingTab from 'src/modules/viewer_cards/components/loading-tab.vue';
import ErrorTab from 'src/modules/viewer_cards/components/error-tab.vue';
export default {
components: {
'loading-tab': LoadingTab,
'error-tab': ErrorTab
},
props: ['tab', 'channel', 'user', 'self', 'getFFZ'],
data() {
return {
loading: true,
errored: false,
data: null
}
}
}
</script>

View file

@ -160,11 +160,15 @@ export default {
},
beforeMount() {
this.$emit('emit', ':open', this);
this.data.then(data => {
this.loaded = true;
this.user = data.data.targetUser;
this.channel = data.data.channelUser;
this.self = data.data.currentUser;
this.loaded = true;
this.$emit('emit', ':load', this);
}).catch(err => {
console.error(err); // eslint-disable-line no-console
@ -179,6 +183,7 @@ export default {
},
beforeDestroy() {
this.$emit('emit', ':close', this);
this.destroyDrag();
if ( this._on_resize ) {
@ -222,6 +227,7 @@ export default {
pin() {
this.pinned = true;
this.$emit('pin');
this.$emit('emit', ':pin', this);
},
close() {
@ -251,8 +257,6 @@ export default {
},
onFocus() {
console.log('got focus!');
this.z = this.getZ();
},

View file

@ -3,7 +3,12 @@
<error-tab v-if="errored" />
<loading-tab v-else-if="loading" />
<template v-else>
{{ JSON.stringify(data) }}
<ul>
<li v-for="entry in data" :key="entry[1]">
<span>{{ entry[0] }}</span>
{{ entry[1] }}
</li>
</ul>
</template>
</div>
</template>
@ -40,6 +45,7 @@ export default {
socket.call('get_name_history', this.user.login).then(data => {
this.loading = false;
this.data = data;
}).catch(err => {
console.error(err);
this.errored = true;

View file

@ -5,13 +5,11 @@
// ============================================================================
import Module from 'utilities/module';
import {has} from 'utilities/object';
import {createElement} from 'utilities/dom';
import GET_USER_INFO from './get_user_info.gql';
export default class ModCards extends Module {
export default class ViewerCards extends Module {
constructor(...args) {
super(...args);
@ -45,6 +43,7 @@ export default class ModCards extends Module {
});
}
addTab(key, data) {
if ( this.tabs[key] )
return this.log.warn(`Attempted to re-define known tab "${key}"`);
@ -138,6 +137,8 @@ export default class ModCards extends Module {
},
on: {
emit: (event, ...data) => this.emit(event, ...data),
close: () => {
const el = component.$el;
el.remove();

View file

@ -138,7 +138,8 @@ Twilight.CHAT_ROUTES = [
'user-events',
'user-followers',
'user-following',
'user'
'user',
'dash'
]
@ -156,7 +157,7 @@ Twilight.ROUTES = {
'dir-game-index': '/directory/game/:gameName',
'dir-all': '/directory/all/:filter?',
'dir-category': '/directory/:category?',
'dash': '/:userName/dashboard',
'dash': '/:userName/dashboard/:live?',
'dash-automod': '/:userName/dashboard/settings/automod',
'event': '/event/:eventName',
'popout': '/popout/:userName/chat',

View file

@ -64,14 +64,17 @@ export default class Channel extends Module {
});
this.ChannelPage.on('update', inst => {
if ( this.settings.get('channel.hosting.enable') || ! inst.state.isHosting )
if ( this.settings.get('channel.hosting.enable') )
return;
// We can't do this immediately because the player state
// occasionally screws up if we do.
setTimeout(() => {
inst.ffzExpectedHost = inst.state.videoPlayerSource;
inst.ffzOldHostHandler(null);
const current_channel = inst.props.data && inst.props.data.variables && inst.props.data.variables.currentChannelLogin;
if ( current_channel && current_channel !== inst.state.videoPlayerSource ) {
inst.ffzExpectedHost = inst.state.videoPlayerSource;
inst.ffzOldHostHandler(null);
}
});
});

View file

@ -17,7 +17,6 @@ import ChatLine from './line';
import SettingsMenu from './settings_menu';
import EmoteMenu from './emote_menu';
import TabCompletion from './tab_completion';
import ModCards from './mod_cards';
const MESSAGE_TYPES = ((e = {}) => {
@ -121,8 +120,6 @@ export default class ChatHook extends Module {
this.inject(EmoteMenu);
this.inject(TabCompletion);
this.inject(ModCards);
this.ChatController = this.fine.define(
'chat-controller',

View file

@ -28,19 +28,19 @@ export default class ChatLine extends Module {
this.inject('site.apollo');
this.inject(RichContent);
this.inject('site.chat.mod_cards');
this.inject('viewer_cards');
this.inject('chat.actions');
this.ChatLine = this.fine.define(
'chat-line',
n => n.renderMessageBody && ! n.getMessageParts,
n => n.renderMessageBody && n.props && ! n.props.roomID,
Twilight.CHAT_ROUTES
);
this.ChatRoomLine = this.fine.define(
'chat-room-line',
n => n.renderMessageBody && n.getMessageParts,
n => n.renderMessageBody && n.props && n.props.roomID,
Twilight.CHAT_ROUTES
);
}
@ -64,6 +64,97 @@ export default class ChatLine extends Module {
const e = React.createElement,
FFZRichContent = this.rich_content && this.rich_content.RichContent;
/*this.ChatRoomLine.ready(cls => {
cls.prototype.render = function() {
const msg = t.chat.standardizeMessage(this.props.message),
is_action = msg.is_action,
user = msg.user,
color = t.parent.colors.process(user.color),
bg_css = null,
show = this._ffz_show = this.state.shouldShowDeletedBody || ! msg.deletedAt;
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : null;
if ( ! room && this.props.channelID ) {
const r = t.chat.getRoom(this.props.channelID, null, true);
if ( r && r.login )
room = msg.roomLogin = r.login;
}
const u = t.site.getUser(),
r = {id: this.props.channelID, login: room};
if ( u ) {
u.moderator = this.props.isCurrentUserModerator;
u.staff = this.props.isCurrentUserStaff;
}
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u),
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
if ( ! this.ffz_user_click_handler )
this.ffz_user_click_handler = event =>
event.ctrlKey ?
this.props.onUsernameClick(user.login, null, msg.id, event.currentTarget.getBoundingClientRect().bottom) :
t.viewer_cards.openCard(r, user, event);
let cls = 'chat-line__message',
out = (tokens.length || ! msg.ffz_type) ? [
this.props.showTimestamps && e('span', {
className: 'chat-line__timestamp'
}, t.chat.formatTime(msg.timestamp)),
t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
e('span', {
className: 'chat-line__message--badges'
}, t.chat.badges.render(msg, e)),
e('a', {
className: 'chat-author__display-name notranslate',
style: { color },
onClick: this.ffz_user_click_handler
}, [
user.displayName,
user.isIntl && e('span', {
className: 'chat-author__intl-login'
}, ` (${user.login})`)
]),
e('span', null, is_action ? ' ' : ': '),
show ?
e('span', {
className: 'message',
style: is_action ? { color } : null
}, t.chat.renderTokens(tokens, e))
:
e('span', {
className: 'chat-line__message--deleted'
}, e('a', {
href: '',
onClick: this.showDeleted
}, t.i18n.t('chat.message-deleted', '<message deleted>'))),
show && rich_content && e(FFZRichContent, rich_content)
] : null;
if ( ! out )
return null;
return e('div', {
className: `${cls}${msg.mentioned ? ' ffz-mentioned' : ''}`,
style: {backgroundColor: bg_css},
id: msg.id,
'data-room-id': this.props.channelID,
'data-room': room,
'data-user-id': user.id,
'data-user': user.login && user.login.toLowerCase()
}, out);
}
// Do this after a short delay to hopefully reduce the chance of React
// freaking out on us.
setTimeout(() => this.ChatRoomLine.forceUpdate());
});*/
this.ChatLine.ready(cls => {
cls.prototype.shouldComponentUpdate = function(props, state) {
const show = state.alwaysShowMessage || ! props.message.deleted,
@ -85,10 +176,10 @@ export default class ChatLine extends Module {
cls.prototype.render = function() {
const types = t.parent.message_types || {},
msg = this.props.message,
msg = t.chat.standardizeMessage(this.props.message),
is_action = msg.messageType === types.Action;
if ( msg.content && ! msg.message )
/*if ( msg.content && ! msg.message )
msg.message = msg.content.text;
if ( msg.sender && ! msg.user ) {
@ -100,12 +191,12 @@ export default class ChatLine extends Module {
const b = msg.badges = {};
for(const item of msg.user.displayBadges)
b[item.setID] = item.version;
}
}*/
const user = msg.user,
color = t.parent.colors.process(user.color),
bg_css = null, //Math.random() > .7 ? t.parent.inverse_colors.process(user.color) : null,
show = this._ffz_show = this.state.alwaysShowMessage || ! this.props.message.deleted;
show = this._ffz_show = this.state.alwaysShowMessage || ! msg.deleted;
let room = msg.roomLogin ? msg.roomLogin : msg.channel ? msg.channel.slice(1) : undefined;
@ -115,8 +206,8 @@ export default class ChatLine extends Module {
room = msg.roomLogin = r.login;
}
if ( ! msg.message && msg.messageParts )
detokenizeMessage(msg);
//if ( ! msg.message && msg.messageParts )
// t.chat.detokenizeMessage(msg);
const u = t.site.getUser(),
r = {id: this.props.channelID, login: room};
@ -130,7 +221,7 @@ export default class ChatLine extends Module {
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
if ( ! this.ffz_user_click_handler )
this.ffz_user_click_handler = event => event.ctrlKey ? this.usernameClickHandler(event) : t.mod_cards.openCard(r, user, event);
this.ffz_user_click_handler = event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
let cls = 'chat-line__message',
out = (tokens.length || ! msg.ffz_type) ? [
@ -264,66 +355,13 @@ export default class ChatLine extends Module {
msg.ffz_tokens = null;
}
this.ChatLine.forceUpdate();
}
}
export function detokenizeMessage(msg) {
const out = [],
parts = msg.messageParts,
l = parts.length,
emotes = {};
let idx = 0, ret, last_type = null;
for(let i=0; i < l; i++) {
const part = parts[i],
content = part.content;
if ( ! content )
continue;
if ( typeof content === 'string' )
ret = content;
else if ( content.recipient )
ret = `@${content.recipient}`;
else if ( content.url )
ret = content.url;
else if ( content.cheerAmount )
ret = `${content.alt}${content.cheerAmount}`;
else if ( content.images ) {
const url = (content.images.themed ? content.images.dark : content.images.sources),
match = url && /\/emoticons\/v1\/(\d+)\/[\d.]+$/.exec(url['1x']),
id = match && match[1];
ret = content.alt;
if ( id ) {
const em = emotes[id] = emotes[id] || [],
offset = last_type > 0 ? 1 : 0;
em.push({startIndex: idx + offset, endIndex: idx + ret.length - 1});
}
if ( last_type > 0 )
ret = ` ${ret}`;
} else
continue;
if ( ret ) {
idx += ret.length;
last_type = part.type;
out.push(ret)
for(const inst of this.ChatRoomLine.instances) {
const msg = inst.props.message;
if ( msg )
msg.ffz_tokens = null;
}
}
msg.message = out.join('');
msg.emotes = emotes;
return msg;
this.ChatLine.forceUpdate();
this.ChatRoomLine.forceUpdate();
}
}

View file

@ -4,6 +4,7 @@
// Menu Button Module
// ============================================================================
import {DEBUG} from 'utilities/constants';
import {SiteModule} from 'utilities/module';
import {createElement} from 'utilities/dom';
@ -80,6 +81,11 @@ export default class MenuButton extends SiteModule {
<span class="tw-button-icon__icon">
<figure class="ffz-i-zreknarf" />
</span>
{DEBUG && (<div class="ffz-menu__dev-pill tw-absolute">
<div class="tw-pill">
{this.i18n.t('site.menu_button.dev', 'dev')}
</div>
</div>)}
{pill && (<div class="ffz-menu__pill tw-absolute">
<div class="tw-animation tw-animation--animate tw-animation--duration-medium tw-animation--timing-ease-in tw-animation--bounce-in">
<div class="tw-pill tw-pill--notification">
@ -89,6 +95,9 @@ export default class MenuButton extends SiteModule {
</div>)}
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-center">
{this.i18n.t('site.menu_button', 'FrankerFaceZ Control Center')}
{DEBUG && (<div class="tw-mg-t-1">
{this.i18n.t('site.menu_button.dev-desc', 'You are running a developer build of FrankerFaceZ.')}
</div>)}
</div>
</div>
</button>)}

View file

@ -24,13 +24,13 @@ export default class Player extends Module {
this.Player = this.fine.define(
'twitch-player',
n => n.player && n.onPlayerReady,
['front-page', 'user', 'video']
['front-page', 'user', 'video', 'dash']
);
this.PersistentPlayer = this.fine.define(
'twitch-player-persistent',
n => n.renderMiniControl && n.renderMiniTitle && n.handleWindowResize,
['front-page', 'user', 'video']
['front-page', 'user', 'video', 'dash']
);
this.settings.add('player.volume-scroll', {

View file

@ -5,11 +5,17 @@
font-size: 1.2rem;
}
.ffz-menu__dev-pill {
bottom: -1rem;
right: -.3rem;
font-size: 1.2rem;
}
.tw-pd-r-1 + & {
margin-left: -.5rem;
}
.loading figure {
.loading .ffz-i-zreknarf {
animation: ffz-rotateplane 1.2s infinite linear;
}
}

View file

@ -45,7 +45,17 @@ export default class WebMunch extends Module {
}
this.log.info(`Found and wrapped webpack's loader after ${(attempts||0)*250}ms.`);
window.webpackJsonp = this.webpackJsonp.bind(this);
try {
window.webpackJsonp = this.webpackJsonp.bind(this);
} catch(err) {
this.log.info('Unable to wrap webpackJsonp normally due to write-protection. Escalating.');
try {
Object.defineProperty(window, 'webpackJsonp', {value: this.webpackJsonp.bind(this)});
} catch(e2) {
this.log.info('Unable to wrap webpackJsonp at this time. Some functionality may be broken as a result.');
}
}
}
webpackJsonp(chunk_ids, modules) {

View file

@ -9,6 +9,11 @@ export const NEW_API = '//api-test.frankerfacez.com';
export const SENTRY_ID = 'https://18f42c65339d4164b3fdebfc8c8bc99b@sentry.io/1186960';
export const LV_SERVER = 'https://cbenni.com/api';
export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/';
export const TWITCH_EMOTE_BASE = '//static-cdn.jtvnw.net/emoticons/v1/';
export const KNOWN_CODES = {