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:
parent
40a829355f
commit
6c77e2ca5c
26 changed files with 809 additions and 103 deletions
|
@ -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>
|
||||
|
|
|
@ -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' : ''}`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
})/
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
95
src/modules/logviewer/index.js
Normal file
95
src/modules/logviewer/index.js
Normal 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);
|
339
src/modules/logviewer/socket.js
Normal file
339
src/modules/logviewer/socket.js
Normal 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;
|
33
src/modules/logviewer/tab-logs.vue
Normal file
33
src/modules/logviewer/tab-logs.vue
Normal 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>
|
|
@ -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();
|
||||
},
|
||||
|
|
@ -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;
|
|
@ -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();
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>)}
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue