mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-31 06:58:30 +00:00
4.20.19
* Changed: Update the styling for rich chat embeds to match modern vanilla Twitch. * Fixed: Do not display FFZ's Alternative Viewer Count if the stream is not live. * Fixed: Automatically opening the chat when accessing a channel. (Now using route state instead of page elements.) * Fixed: Dual channel points redemption messages when FFZ is rendering them. * Fixed: Stopping the host video player when accessing a user's home page. * Removed: The old, unfinished logviewer module. Logviewer is a dead project. * API Changed: Add support for rich formatting in rich chat embeds. * API Changed: `fine-router` can now check state when determining the current route.
This commit is contained in:
parent
fa3d73e05a
commit
a4fa1d1491
21 changed files with 451 additions and 629 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.20.18",
|
||||
"version": "4.20.19",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -647,6 +647,10 @@ export class TranslationManager extends Module {
|
|||
return this._.formatNumber(...args);
|
||||
}
|
||||
|
||||
formatDuration(...args) {
|
||||
return this._.formatDuration(...args);
|
||||
}
|
||||
|
||||
formatDate(...args) {
|
||||
return this._.formatDate(...args)
|
||||
}
|
||||
|
|
|
@ -1,77 +1,8 @@
|
|||
<template>
|
||||
<a :href="url" class="chat-card__link" target="_blank" rel="noreferrer noopener">
|
||||
<div class="ffz--chat-card tw-elevation-1 tw-mg-t">
|
||||
<div class="tw-c-background-base tw-flex tw-flex-nowrap tw-pd-05">
|
||||
<div class="chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center">
|
||||
<img
|
||||
v-if="error"
|
||||
class="chat-card__error-img"
|
||||
src=""
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="tw-card-img tw-flex-shrink-0 tw-overflow-hidden"
|
||||
>
|
||||
<aspect :ratio="16/9">
|
||||
<img
|
||||
v-if="loaded && image"
|
||||
:src="image"
|
||||
:alt="title"
|
||||
class="tw-image"
|
||||
>
|
||||
</aspect>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="{'ffz--two-line': desc_2}"
|
||||
class="ffz--card-text tw-overflow-hidden tw-align-items-center tw-flex"
|
||||
>
|
||||
<div class="tw-full-width tw-pd-l-1">
|
||||
<div class="chat-card__title tw-ellipsis">
|
||||
<span
|
||||
v-if="! loaded"
|
||||
class="tw-font-size-5"
|
||||
data-test-selector="chat-card-title"
|
||||
>
|
||||
{{ t('card.loading', 'Loading...') }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:title="title"
|
||||
class="tw-font-size-5"
|
||||
data-test-selector="chat-card-title"
|
||||
>
|
||||
{{ title }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="loaded" class="tw-ellipsis">
|
||||
<span
|
||||
:title="desc_1"
|
||||
class="tw-c-text-alt-2 tw-font-size-6"
|
||||
data-test-selector="chat-card-description"
|
||||
>
|
||||
{{ desc_1 }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="loaded && desc_2" class="tw-ellipsis">
|
||||
<span
|
||||
:title="desc_2"
|
||||
class="tw-c-text-alt-2 tw-font-size-6"
|
||||
data-test-selector="chat-card-description"
|
||||
>
|
||||
{{ desc_2 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {timeout} from 'utilities/object';
|
||||
import {has, timeout} from 'utilities/object';
|
||||
|
||||
const ERROR_IMAGE = 'https://static-cdn.jtvnw.net/emoticons/v1/58765/2.0';
|
||||
|
||||
export default {
|
||||
props: ['data', 'url'],
|
||||
|
@ -80,10 +11,17 @@ export default {
|
|||
return {
|
||||
loaded: false,
|
||||
error: false,
|
||||
title: null,
|
||||
html: null,
|
||||
title: this.t('card.loading', 'Loading...'),
|
||||
title_tokens: null,
|
||||
desc_1: null,
|
||||
desc_1_tokens: null,
|
||||
desc_2: null,
|
||||
image: null
|
||||
desc_2_tokens: null,
|
||||
image: null,
|
||||
image_title: null,
|
||||
image_square: false,
|
||||
accent: null
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -92,7 +30,7 @@ export default {
|
|||
try {
|
||||
data = this.data.getData();
|
||||
if ( data instanceof Promise ) {
|
||||
const to_wait = this.data.timeout || 1000;
|
||||
const to_wait = has(this.data, 'timeout') ? this.data.timeout : 1000;
|
||||
if ( to_wait )
|
||||
data = await timeout(data, to_wait);
|
||||
else
|
||||
|
@ -105,7 +43,6 @@ export default {
|
|||
title: this.t('card.error', 'An error occured.'),
|
||||
desc_1: this.t('card.empty', 'No data was returned.')
|
||||
}
|
||||
|
||||
} catch(err) {
|
||||
data = {
|
||||
error: true,
|
||||
|
@ -116,11 +53,157 @@ export default {
|
|||
|
||||
this.loaded = true;
|
||||
this.error = data.error;
|
||||
this.image = data.image;
|
||||
this.html = data.html;
|
||||
this.title = data.title;
|
||||
this.title_tokens = data.title_tokens;
|
||||
this.desc_1 = data.desc_1;
|
||||
this.desc_1_tokens = data.desc_1_tokens;
|
||||
this.desc_2 = data.desc_2;
|
||||
this.desc_2_tokens = data.desc_2_tokens;
|
||||
this.image = data.image;
|
||||
this.image_square = data.image_square;
|
||||
this.image_title = data.image_title;
|
||||
},
|
||||
|
||||
methods: {
|
||||
renderCard(h) {
|
||||
if ( this.data.renderBody )
|
||||
return [this.data.renderBody(h)];
|
||||
|
||||
if ( this.html )
|
||||
return [h('div', {
|
||||
domProps: {
|
||||
innerHTML: this.html
|
||||
}
|
||||
})];
|
||||
|
||||
return [
|
||||
this.renderImage(h),
|
||||
this.renderDescription(h)
|
||||
];
|
||||
},
|
||||
|
||||
renderTokens(tokens, h) {
|
||||
let out = [];
|
||||
if ( ! Array.isArray(tokens) )
|
||||
tokens = [tokens];
|
||||
|
||||
for(const token of tokens) {
|
||||
if ( Array.isArray(token) )
|
||||
out = out.concat(this.renderTokens(token, h));
|
||||
|
||||
else if ( typeof token !== 'object' )
|
||||
out.push(token);
|
||||
|
||||
else {
|
||||
const el = h(token.tag || 'span', {
|
||||
class: token.class,
|
||||
}, this.renderTokens(token.content, h));
|
||||
|
||||
out.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
renderDescription(h) {
|
||||
let title = this.title,
|
||||
title_tokens = this.title_tokens,
|
||||
desc_1 = this.desc_1,
|
||||
desc_1_tokens = this.desc_1_tokens,
|
||||
desc_2 = this.desc_2,
|
||||
desc_2_tokens = this.desc_2_tokens;
|
||||
|
||||
if ( ! this.loaded ) {
|
||||
desc_1 = this.t('card.loading', 'Loading...');
|
||||
desc_1_tokens = desc_2 = desc_2_tokens = title = title_tokens = null;
|
||||
}
|
||||
|
||||
return h('div', {
|
||||
class: [
|
||||
'ffz--card-text tw-overflow-hidden tw-align-items-center tw-flex',
|
||||
desc_2 && 'ffz--two-line'
|
||||
]
|
||||
}, [h('div', {class: 'tw-full-width tw-pd-l-1'}, [
|
||||
h('div', {class: 'chat-card__title tw-ellipsis'},
|
||||
[h('span', {class: 'tw-strong', attrs: {title}}, title_tokens ? this.renderTokens(title_tokens, h) : title)]),
|
||||
h('div', {class: 'tw-ellipsis'},
|
||||
[h('span', {class: 'tw-c-text-alt-2', attrs: {title: desc_1}}, desc_1_tokens ? this.renderTokens(desc_1_tokens, h) : desc_1)]),
|
||||
desc_2 && h('div', {class: 'tw-ellipsis'},
|
||||
[h('span', {class: 'tw-c-text-alt-2', attrs: {title: desc_2}}, desc_2_tokens ? this.renderTokens(desc_2_tokens, h) : desc_2)])
|
||||
])]);
|
||||
},
|
||||
|
||||
renderImage(h) {
|
||||
let content;
|
||||
if ( this.error )
|
||||
content = h('img', {
|
||||
class: 'chat-card__error-img',
|
||||
attrs: {
|
||||
src: ERROR_IMAGE
|
||||
}
|
||||
});
|
||||
else {
|
||||
content = h('div', {
|
||||
class: 'tw-card-img tw-flex-shrink-0 tw-overflow-hidden'
|
||||
}, [h('aspect', {
|
||||
props: {
|
||||
ratio: 16/9
|
||||
}
|
||||
}, [this.loaded && this.image && h('img', {
|
||||
class: 'tw-image',
|
||||
attrs: {
|
||||
src: this.image,
|
||||
alt: this.image_title ?? this.title
|
||||
}
|
||||
})])]);
|
||||
}
|
||||
|
||||
return h('div', {
|
||||
class: [
|
||||
'chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center',
|
||||
this.image_square && 'square'
|
||||
]
|
||||
}, [content])
|
||||
}
|
||||
},
|
||||
|
||||
render(h) {
|
||||
let content = h('div', {
|
||||
class: 'tw-flex tw-flex-nowrap tw-pd-05'
|
||||
}, this.renderCard(h));
|
||||
|
||||
if ( this.url ) {
|
||||
const tooltip = this.data.card_tooltip;
|
||||
content = h('a', {
|
||||
class: [
|
||||
tooltip && 'ffz-tooltip',
|
||||
this.accent && 'ffz-accent-card',
|
||||
!this.error && 'tw-interactable--hover-enabled',
|
||||
'tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--alpha tw-interactive'
|
||||
],
|
||||
attrs: {
|
||||
'data-tooltip-type': 'link',
|
||||
'data-url': this.url,
|
||||
'data-is-mail': false,
|
||||
target: '_blank',
|
||||
rel: 'noreferrer noopener',
|
||||
href: this.url
|
||||
}
|
||||
}, [content]);
|
||||
}
|
||||
|
||||
return h('div', {
|
||||
class: 'tw-border-radius-medium tw-elevation-1 ffz--chat-card',
|
||||
style: {
|
||||
'--ffz-color-accent': this.accent
|
||||
}
|
||||
}, [h('div', {
|
||||
class: 'tw-border-radius-medium tw-c-background-base tw-flex tw-full-width'
|
||||
}, [content])]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
|
@ -7,6 +7,12 @@
|
|||
const CLIP_URL = /^(?:https?:\/\/)?clips\.twitch\.tv\/(\w+)(?:\/)?(\w+)?(?:\/edit)?/;
|
||||
const NEW_CLIP_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/\w+\/clip\/(\w+)/;
|
||||
const VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/(?:\w+\/v|videos)\/(\w+)/;
|
||||
const USER_URL = /^(?:https?:\/\/)?(?:www\.)?twitch\.tv\/([^/]+)$/;
|
||||
|
||||
const BAD_USERS = [
|
||||
'directory', '_deck', 'p', 'downloads', 'jobs', 'turbo', 'settings', 'friends',
|
||||
'subscriptions', 'inventory', 'wallet'
|
||||
];
|
||||
|
||||
import GET_CLIP from './clip_info.gql';
|
||||
import GET_VIDEO from './video_info.gql';
|
||||
|
@ -55,7 +61,9 @@ export const Links = {
|
|||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: data.accent,
|
||||
image: this.context.get('tooltip.link-images') ? (data.image_safe || this.context.get('tooltip.link-nsfw-images') ) ? data.preview || data.image : null : null,
|
||||
image_square: data.image_square,
|
||||
title: data.title,
|
||||
desc_1: data.desc_1,
|
||||
desc_2: data.desc_2
|
||||
|
@ -66,13 +74,100 @@ export const Links = {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Users
|
||||
// ============================================================================
|
||||
|
||||
export const Users = {
|
||||
type: 'user',
|
||||
hide_token: false,
|
||||
|
||||
test(token) {
|
||||
if ( token.type !== 'link' || (! this.context.get('chat.rich.all-links') && ! token.force_rich) )
|
||||
return false;
|
||||
|
||||
return USER_URL.test(token.url);
|
||||
},
|
||||
|
||||
process(token) {
|
||||
const match = USER_URL.exec(token.url),
|
||||
twitch_data = this.resolve('site.twitch_data');
|
||||
|
||||
if ( ! twitch_data || ! match || BAD_USERS.includes(match[1]) )
|
||||
return;
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
|
||||
getData: async () => {
|
||||
const user = await twitch_data.getUser(null, match[1]);
|
||||
if ( ! user || ! user.id )
|
||||
return null;
|
||||
|
||||
const game = user.broadcastSettings?.game?.displayName;
|
||||
|
||||
let desc_1 = null, desc_2 = null, desc_1_tokens = null, desc_2_tokens = null;
|
||||
if ( user.stream?.id && game ) {
|
||||
desc_1_tokens = this.i18n.tList('cards.user.streaming', 'streaming {game}', {
|
||||
game: {class: 'tw-semibold', content: [game]}
|
||||
});
|
||||
desc_1 = this.i18n.t('cards.user.streaming', 'streaming {game}', {
|
||||
game
|
||||
});
|
||||
}
|
||||
|
||||
const bits_tokens = this.i18n.tList('cards.user.stats', 'Views: {views,number} • Followers: {followers,number}', {
|
||||
views: {class: 'tw-semibold', content: [this.i18n.formatNumber(user.profileViewCount || 0)]},
|
||||
followers: {class: 'tw-semibold', content: [this.i18n.formatNumber(user.followers?.totalCount || 0)]}
|
||||
}),
|
||||
bits = this.i18n.t('cards.user.stats', 'Views: {views,number} • Followers: {followers,number}', {
|
||||
views: user.profileViewCount || 0,
|
||||
followers: user.followers?.totalCount || 0
|
||||
});
|
||||
|
||||
if ( desc_1 ) {
|
||||
desc_2 = bits;
|
||||
desc_2_tokens = bits_tokens;
|
||||
} else {
|
||||
desc_1 = bits;
|
||||
desc_1_tokens = bits_tokens;
|
||||
}
|
||||
|
||||
const has_i18n = user.displayName.trim().toLowerCase() !== user.login;
|
||||
let title = user.displayName, title_tokens = null;
|
||||
if ( has_i18n ) {
|
||||
title = `${user.displayName} (${user.login})`;
|
||||
title_tokens = [
|
||||
user.displayName,
|
||||
{class: 'chat-author__intl-login', content: ` (${user.login})`}
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
accent: user.primaryColorHex ? `#${user.primaryColorHex}` : null,
|
||||
image: user.profileImageURL,
|
||||
image_square: true,
|
||||
title,
|
||||
title_tokens,
|
||||
desc_1,
|
||||
desc_1_tokens,
|
||||
desc_2,
|
||||
desc_2_tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// Clips
|
||||
// ============================================================================
|
||||
|
||||
export const Clips = {
|
||||
type: 'clip',
|
||||
hide_token: true,
|
||||
hide_token: false,
|
||||
|
||||
test(token) {
|
||||
if ( token.type !== 'link' )
|
||||
|
@ -110,29 +205,47 @@ export const Clips = {
|
|||
game_name = game && game.name,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
let desc_1;
|
||||
if ( game_name === 'creative' )
|
||||
let desc_1, desc_1_tokens;
|
||||
if ( game_name === 'creative' ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.creative', '{user} being Creative', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', {
|
||||
user
|
||||
});
|
||||
|
||||
else if ( game )
|
||||
} else if ( game ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', {
|
||||
user: {class: 'tw-semibold', content: user},
|
||||
game: {class: 'tw-semibold', game_display}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
|
||||
user,
|
||||
game: game_display
|
||||
});
|
||||
|
||||
else
|
||||
} else {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1', 'Clip of {user}', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1', 'Clip of {user}', {user});
|
||||
}
|
||||
|
||||
const curator = clip.curator ? clip.curator.displayName : this.i18n.t('clip.unknown', 'Unknown');
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
image: clip.thumbnailURL,
|
||||
title: clip.title,
|
||||
desc_1,
|
||||
desc_1_tokens,
|
||||
desc_2: this.i18n.t('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', {
|
||||
curator: clip.curator ? clip.curator.displayName : this.i18n.t('clip.unknown', 'Unknown'),
|
||||
curator,
|
||||
views: clip.viewCount
|
||||
}),
|
||||
desc_2_tokens: this.i18n.tList('clip.desc.2', 'Clipped by {curator} — {views,number} View{views,en_plural}', {
|
||||
curator: clip.curator ? {class: 'tw-semibold', content: curator} : curator,
|
||||
views: {class: 'tw-semibold', content: this.i18n.formatNumber(clip.viewCount)}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +256,7 @@ export const Clips = {
|
|||
|
||||
export const Videos = {
|
||||
type: 'video',
|
||||
hide_token: true,
|
||||
hide_token: false,
|
||||
|
||||
test(token) {
|
||||
return token.type === 'link' && VIDEO_URL.test(token.url)
|
||||
|
@ -174,26 +287,38 @@ export const Videos = {
|
|||
game_name = game && game.name,
|
||||
game_display = game && game.displayName;
|
||||
|
||||
let desc_1;
|
||||
if ( game_name === 'creative' )
|
||||
let desc_1, desc_1_tokens;
|
||||
if ( game_name === 'creative' ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.creative', '{user} being Creative', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.creative', '{user} being Creative', {
|
||||
user
|
||||
});
|
||||
|
||||
else if ( game )
|
||||
} else if ( game ) {
|
||||
desc_1_tokens = this.i18n.tList('clip.desc.1.playing', '{user} playing {game}', {
|
||||
user: {class: 'tw-semibold', content: user},
|
||||
game: {class: 'tw-semibold', content: game_display}
|
||||
});
|
||||
desc_1 = this.i18n.t('clip.desc.1.playing', '{user} playing {game}', {
|
||||
user,
|
||||
game: game_display
|
||||
});
|
||||
|
||||
else
|
||||
} else {
|
||||
desc_1_tokens = this.i18n.tList('video.desc.1', 'Video of {user}', {
|
||||
user: {class: 'tw-semibold', content: user}
|
||||
});
|
||||
desc_1 = this.i18n.t('video.desc.1', 'Video of {user}', {user});
|
||||
}
|
||||
|
||||
return {
|
||||
url: token.url,
|
||||
image: video.previewThumbnailURL,
|
||||
title: video.title,
|
||||
desc_1,
|
||||
desc_1_tokens,
|
||||
desc_2: this.i18n.t('video.desc.2', '{length,duration} — {views,number} Views - {date,datetime}', {
|
||||
length: video.lengthSeconds,
|
||||
views: video.viewCount,
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
'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;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
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.user.login}`;
|
||||
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);
|
|
@ -1,339 +0,0 @@
|
|||
'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;
|
|
@ -1,33 +0,0 @@
|
|||
<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>
|
|
@ -13,7 +13,9 @@
|
|||
<script>
|
||||
|
||||
const VIDEOS = [
|
||||
'https://www.youtube.com/watch?v=BFSWlDpA6C4'
|
||||
'https://www.twitch.tv/dansalvato',
|
||||
'https://www.twitch.tv/sirstendec',
|
||||
//'https://www.youtube.com/watch?v=BFSWlDpA6C4'
|
||||
];
|
||||
|
||||
export default {
|
||||
|
|
|
@ -109,17 +109,20 @@ export default class Metadata extends Module {
|
|||
refresh() { return this.settings.get('metadata.viewers') },
|
||||
|
||||
setup(data) {
|
||||
return data.getViewerCount();
|
||||
return {
|
||||
live: data.channel?.live_since != null,
|
||||
count: data.getViewerCount()
|
||||
}
|
||||
},
|
||||
|
||||
order: 1,
|
||||
icon: 'ffz-i-viewers',
|
||||
|
||||
label(data) {
|
||||
if ( ! this.settings.get('metadata.viewers') )
|
||||
if ( ! this.settings.get('metadata.viewers') || ! data.live )
|
||||
return null;
|
||||
|
||||
return this.i18n.formatNumber(data)
|
||||
return this.i18n.formatNumber(data.count)
|
||||
},
|
||||
|
||||
tooltip() {
|
||||
|
|
|
@ -50,6 +50,9 @@ export default class Twilight extends BaseSite {
|
|||
this.router.route(Twilight.ROUTES);
|
||||
this.router.routeName(Twilight.ROUTE_NAMES);
|
||||
|
||||
this.router.route('user', '/:userName', null, state => state?.channelView !== 'Home');
|
||||
this.router.route('user-home', '/:userName', null, state => state?.channelView === 'Home');
|
||||
|
||||
this.router.route(Twilight.DASH_ROUTES, 'dashboard.twitch.tv');
|
||||
}
|
||||
|
||||
|
@ -213,6 +216,7 @@ Twilight.CHAT_ROUTES = [
|
|||
'dash-chat',
|
||||
'video',
|
||||
'user-video',
|
||||
'user-home',
|
||||
'user-clip',
|
||||
'user-videos',
|
||||
'user-clips',
|
||||
|
@ -312,7 +316,7 @@ Twilight.ROUTES = {
|
|||
'product': '/products/:productName',
|
||||
'prime': '/prime',
|
||||
'turbo': '/turbo',
|
||||
'user': '/:userName',
|
||||
//'user': '/:userName',
|
||||
'squad': '/:userName/squad',
|
||||
'command-center': '/:userName/commandcenter',
|
||||
'embed-chat': '/embed/:userName/chat',
|
||||
|
|
|
@ -10,7 +10,7 @@ import {debounce} from 'utilities/object';
|
|||
import { createElement, setChildren } from 'utilities/dom';
|
||||
|
||||
|
||||
const USER_PAGES = ['user', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'];
|
||||
const USER_PAGES = ['user', 'user-home', 'video', 'user-video', 'user-clip', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following'];
|
||||
|
||||
export default class Channel extends Module {
|
||||
|
||||
|
@ -103,12 +103,18 @@ export default class Channel extends Module {
|
|||
|
||||
this.subpump.on(':pubsub-message', this.onPubSub, this);
|
||||
|
||||
this.router.on(':route', route => {
|
||||
if ( route?.name === 'user' )
|
||||
setTimeout(this.maybeClickChat.bind(this), 1000);
|
||||
}, this);
|
||||
this.router.on(':route', this.checkNavigation, this);
|
||||
this.checkNavigation();
|
||||
}
|
||||
|
||||
this.maybeClickChat();
|
||||
checkNavigation() {
|
||||
if ( ! this.settings.get('channel.auto-click-chat') || this.router.current_name !== 'user-home' )
|
||||
return;
|
||||
|
||||
if ( this.router.old_location === this.router.location )
|
||||
return;
|
||||
|
||||
this.router.history.replace(this.router.location, {channelView: 'Watch'});
|
||||
}
|
||||
|
||||
updateLinks() {
|
||||
|
@ -118,14 +124,6 @@ export default class Channel extends Module {
|
|||
}
|
||||
}
|
||||
|
||||
maybeClickChat() {
|
||||
if ( this.settings.get('channel.auto-click-chat') && this.router.current_name === 'user' ) {
|
||||
const el = document.querySelector('a[data-a-target="channel-home-tab-Chat"]');
|
||||
if ( el )
|
||||
el.click();
|
||||
}
|
||||
}
|
||||
|
||||
setHost(channel_id, channel_login, target_id, target_login) {
|
||||
const topic = `stream-chat-room-v1.${channel_id}`;
|
||||
|
||||
|
|
|
@ -115,7 +115,8 @@ const CHAT_TYPES = make_enum(
|
|||
'FirstCheerMessage',
|
||||
'BitsBadgeTierMessage',
|
||||
'InlinePrivateCallout',
|
||||
'ChannelPointsReward'
|
||||
'ChannelPointsReward',
|
||||
'CommunityChallengeContribution'
|
||||
);
|
||||
|
||||
|
||||
|
@ -1089,6 +1090,9 @@ export default class ChatHook extends Module {
|
|||
if ( ! message || ! service || message.type !== 'reward-redeemed' || service.props.channelID != data?.channel_id )
|
||||
return;
|
||||
|
||||
if ( data.user_input )
|
||||
return;
|
||||
|
||||
const reward = data.reward?.id && get(data.reward.id, service.props.rewardMap);
|
||||
if ( ! reward )
|
||||
return;
|
||||
|
|
|
@ -75,7 +75,7 @@ export default class RichContent extends Module {
|
|||
}
|
||||
|
||||
renderCardImage() {
|
||||
return (<div class="chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center">
|
||||
return (<div class={`chat-card__preview-img tw-align-items-center tw-c-background-alt-2 tw-flex tw-flex-shrink-0 tw-justify-content-center${this.state.image_square ? ' square' : ''}`}>
|
||||
{this.state.error ?
|
||||
(<img
|
||||
class="chat-card__error-img"
|
||||
|
@ -85,58 +85,84 @@ export default class RichContent extends Module {
|
|||
<div class="tw-aspect tw-aspect--align-top">
|
||||
<div class="tw-aspect__spacer" style={{paddingTop: '56.25%'}} />
|
||||
{this.state.loaded && this.state.image ?
|
||||
(<img class="tw-image" src={this.state.image} alt={this.state.title} />)
|
||||
(<img class="tw-image" src={this.state.image} alt={this.state.image_title ?? this.state.title} />)
|
||||
: null}
|
||||
</div>
|
||||
</div>)}
|
||||
</div>)
|
||||
}
|
||||
|
||||
renderTokens(tokens) {
|
||||
let out = [];
|
||||
if ( ! Array.isArray(tokens) )
|
||||
tokens = [tokens];
|
||||
|
||||
for(const token of tokens) {
|
||||
if ( Array.isArray(token) )
|
||||
out = out.concat(this.renderTokens(token));
|
||||
|
||||
else if ( typeof token !== 'object' )
|
||||
out.push(token);
|
||||
|
||||
else {
|
||||
const el = createElement(token.tag || 'span', {
|
||||
className: token.class
|
||||
}, this.renderTokens(token.content));
|
||||
|
||||
out.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
renderCardDescription() {
|
||||
let title = this.state.title,
|
||||
title_tokens = this.state.title_tokens,
|
||||
desc_1 = this.state.desc_1,
|
||||
desc_2 = this.state.desc_2;
|
||||
desc_1_tokens = this.state.desc_1_tokens,
|
||||
desc_2 = this.state.desc_2,
|
||||
desc_2_tokens = this.state.desc_2_tokens;
|
||||
|
||||
if ( ! this.state.loaded ) {
|
||||
desc_1 = t.i18n.t('card.loading', 'Loading...');
|
||||
desc_2 = '';
|
||||
title = '';
|
||||
desc_1_tokens = desc_2 = desc_2_tokens = title = title_tokens = null;
|
||||
}
|
||||
|
||||
return (<div class={`ffz--card-text tw-overflow-hidden tw-align-items-center tw-flex${desc_2 ? ' ffz--two-line' : ''}`}>
|
||||
<div class="tw-full-width tw-pd-l-1">
|
||||
<div class="chat-card__title tw-ellipsis">
|
||||
<span
|
||||
class="tw-font-size-5"
|
||||
class="tw-strong"
|
||||
data-test-selector="chat-card-title"
|
||||
title={title}
|
||||
>
|
||||
{title}
|
||||
{title_tokens ? this.renderTokens(title_tokens) : title}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tw-ellipsis">
|
||||
<span
|
||||
class="tw-c-text-alt-2 tw-font-size-6"
|
||||
class="tw-c-text-alt-2"
|
||||
data-test-selector="chat-card-description"
|
||||
title={desc_1}
|
||||
>
|
||||
{desc_1}
|
||||
{desc_1_tokens ? this.renderTokens(desc_1_tokens) : desc_1}
|
||||
</span>
|
||||
</div>
|
||||
{desc_2 && (<div class="tw-ellipsis">
|
||||
{(desc_2_tokens || desc_2) && (<div class="tw-ellipsis">
|
||||
<span
|
||||
class="tw-c-text-alt-2 tw-font-size-6"
|
||||
class="tw-c-text-alt-2"
|
||||
data-test-selector="chat-card-description"
|
||||
title={desc_2}
|
||||
>
|
||||
{desc_2}
|
||||
{desc_2_tokens ? this.renderTokens(desc_2_tokens) : desc_2}
|
||||
</span>
|
||||
</div>)}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
renderCardBody() {
|
||||
renderCard() {
|
||||
if ( this.props.renderBody )
|
||||
return this.props.renderBody(this.state, this, createElement);
|
||||
|
||||
|
@ -149,31 +175,31 @@ export default class RichContent extends Module {
|
|||
];
|
||||
}
|
||||
|
||||
renderCard() {
|
||||
return (<div class="ffz--chat-card tw-elevation-1 tw-mg-t">
|
||||
<div class="tw-c-background-base tw-flex tw-flex-nowrap tw-pd-05">
|
||||
{this.renderCardBody()}
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
|
||||
render() {
|
||||
if ( ! this.state.url )
|
||||
return this.renderCard();
|
||||
let content = <div class="tw-flex tw-flex-nowrap tw-pd-05">{this.renderCard()}</div>;
|
||||
if ( this.state.url ) {
|
||||
const tooltip = this.props.card_tooltip;
|
||||
content = (<a
|
||||
class={`${tooltip ? 'ffz-tooltip ' : ''}${this.state.accent ? 'ffz-accent-card ' : ''} tw-block tw-border-radius-medium tw-full-width tw-interactable tw-interactable--alpha tw-interactable--hover-enabled tw-interactive`}
|
||||
data-tooltip-type="link"
|
||||
data-url={this.state.url}
|
||||
data-is-mail={false}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={this.state.url}
|
||||
>
|
||||
{content}
|
||||
</a>);
|
||||
}
|
||||
|
||||
const tooltip = this.props.card_tooltip;
|
||||
|
||||
return (<a
|
||||
class={`${tooltip ? 'ffz-tooltip ' : ''} chat-card__link`}
|
||||
data-tooltip-type="link"
|
||||
data-url={this.state.url}
|
||||
data-is-mail={false}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={this.state.url}
|
||||
return (<div
|
||||
class="tw-border-radius-medium tw-elevation-1 ffz--chat-card"
|
||||
style={{'--ffz-color-accent': this.state.accent || null}}
|
||||
>
|
||||
{this.renderCard()}
|
||||
</a>);
|
||||
<div class="tw-border-radius-medium tw-c-background-base tw-flex tw-full-width">
|
||||
{content}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -349,7 +349,7 @@ export default class CSSTweaks extends Module {
|
|||
this.settings.add('channel.hide-live-indicator', {
|
||||
requires: ['context.route.name'],
|
||||
process(ctx, val) {
|
||||
return ctx.get('context.route.name') === 'user' ? val : false
|
||||
return (ctx.get('context.route.name') === 'user' || ctx.get('context.route.name') === 'user-home') ? val : false
|
||||
},
|
||||
default: false,
|
||||
ui: {
|
||||
|
|
|
@ -12,7 +12,7 @@ export const PLAYER_ROUTES = [
|
|||
'front-page', 'user', 'video', 'user-video', 'user-clip', 'user-videos',
|
||||
'user-clips', 'user-collections', 'user-events', 'user-followers',
|
||||
'user-following', 'dash', 'squad', 'command-center', 'dash-stream-manager',
|
||||
'mod-view'
|
||||
'mod-view', 'user-home'
|
||||
];
|
||||
|
||||
const HAS_COMPRESSOR = window.AudioContext && window.DynamicsCompressorNode != null;
|
||||
|
@ -74,13 +74,13 @@ export default class Player extends Module {
|
|||
this.TheatreHost = this.fine.define(
|
||||
'theatre-host',
|
||||
n => n.toggleTheatreMode && n.props && n.props.onTheatreModeEnabled,
|
||||
['user', 'video', 'user-video', 'user-clip']
|
||||
['user', 'user-home', 'video', 'user-video', 'user-clip']
|
||||
);
|
||||
|
||||
this.PlayerSource = this.fine.define(
|
||||
'player-source',
|
||||
n => n.setSrc && n.setInitialPlaybackSettings,
|
||||
false
|
||||
PLAYER_ROUTES
|
||||
);
|
||||
|
||||
|
||||
|
@ -648,7 +648,7 @@ export default class Player extends Module {
|
|||
}
|
||||
|
||||
cls.prototype.ffzStopAutoplay = function() {
|
||||
if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current.name === 'front-page') )
|
||||
if ( t.settings.get('player.no-autoplay') || (! t.settings.get('player.home.autoplay') && t.router.current?.name === 'front-page') )
|
||||
t.stopPlayer(this.props.mediaPlayerInstance, this.props.playerEvents, this);
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class SubButton extends Module {
|
|||
this.SubButton = this.fine.define(
|
||||
'sub-button',
|
||||
n => n.handleSubMenuAction && n.isUserDataReady,
|
||||
['user', 'user-video', 'user-clip', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
|
||||
['user', 'user-home', 'user-video', 'user-clip', 'video', 'user-videos', 'user-clips', 'user-collections', 'user-events', 'user-followers', 'user-following']
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ffz-accent-card {
|
||||
border-right: .5rem solid var(--ffz-color-accent);
|
||||
}
|
||||
|
||||
.chat-card__preview-img.square {
|
||||
width: 4.5rem;
|
||||
|
||||
.tw-aspect__spacer {
|
||||
padding-top: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-card__title {
|
||||
max-width: unset;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
class="tw-aspect"
|
||||
>
|
||||
<div
|
||||
:style="{paddingBottom: (props.padding ? props.padding : (100 * (1 / (props.ratio || 1)))) + '%'}"
|
||||
:style="{paddingTop: (props.padding ? props.padding : (100 * (1 / (props.ratio || 1)))) + '%'}"
|
||||
class="tw-aspect__spacer"
|
||||
/>
|
||||
<slot />
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import {parse, tokensToRegExp, tokensToFunction} from 'path-to-regexp';
|
||||
import Module from 'utilities/module';
|
||||
import {has} from 'utilities/object';
|
||||
import {has, deep_equals} from 'utilities/object';
|
||||
|
||||
|
||||
export default class FineRouter extends Module {
|
||||
|
@ -39,20 +39,27 @@ export default class FineRouter extends Module {
|
|||
this._navigateTo(history.location);
|
||||
}
|
||||
|
||||
navigate(route, data, opts) {
|
||||
this.history.push(this.getURL(route, data, opts));
|
||||
navigate(route, data, opts, state) {
|
||||
this.history.push(this.getURL(route, data, opts), state);
|
||||
}
|
||||
|
||||
_navigateTo(location) {
|
||||
this.log.debug('New Location', location);
|
||||
const host = window.location.host,
|
||||
path = location.pathname;
|
||||
path = location.pathname,
|
||||
state = location.state;
|
||||
|
||||
if ( path === this.location && host === this.domain )
|
||||
if ( path === this.location && host === this.domain && deep_equals(state, this.current_state) )
|
||||
return;
|
||||
|
||||
this.old_location = this.location;
|
||||
this.old_domain = this.domain;
|
||||
this.old_state = this.current_state;
|
||||
|
||||
this.location = path;
|
||||
this.domain = host;
|
||||
this.current_state = state;
|
||||
|
||||
this._pickRoute();
|
||||
}
|
||||
|
||||
|
@ -60,12 +67,16 @@ export default class FineRouter extends Module {
|
|||
const path = this.location,
|
||||
host = this.domain;
|
||||
|
||||
this.old_route = this.current;
|
||||
this.old_name = this.current_name;
|
||||
this.old_match = this.match;
|
||||
|
||||
for(const route of this.__routes) {
|
||||
if ( route.domain && route.domain !== host )
|
||||
continue;
|
||||
|
||||
const match = route.regex.exec(path);
|
||||
if ( match ) {
|
||||
if ( match && (! route.state_fn || route.state_fn(this.current_state)) ) {
|
||||
this.log.debug('Matching Route', route, match);
|
||||
this.current = route;
|
||||
this.current_name = route.name;
|
||||
|
@ -146,12 +157,12 @@ export default class FineRouter extends Module {
|
|||
this.emit(':updated-route-names');
|
||||
}
|
||||
|
||||
route(name, path, domain = null, process = true) {
|
||||
route(name, path, domain = null, state_fn = null, process = true) {
|
||||
if ( typeof name === 'object' ) {
|
||||
domain = path;
|
||||
for(const key in name)
|
||||
if ( has(name, key) )
|
||||
this.route(key, name[key], domain, false);
|
||||
this.route(key, name[key], domain, state_fn, false);
|
||||
|
||||
if ( process ) {
|
||||
this.__routes.sort((a,b) => b.score - a.score);
|
||||
|
@ -173,6 +184,7 @@ export default class FineRouter extends Module {
|
|||
parts,
|
||||
score,
|
||||
domain,
|
||||
state_fn,
|
||||
regex: tokensToRegExp(parts),
|
||||
url: tokensToFunction(parts)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,21 @@ query FFZ_FetchUser($id: ID, $login: String) {
|
|||
login
|
||||
displayName
|
||||
profileImageURL(width: 50)
|
||||
profileViewCount
|
||||
primaryColorHex
|
||||
broadcastSettings {
|
||||
id
|
||||
game {
|
||||
id
|
||||
displayName
|
||||
}
|
||||
}
|
||||
stream {
|
||||
id
|
||||
}
|
||||
followers {
|
||||
totalCount
|
||||
}
|
||||
roles {
|
||||
isAffiliate
|
||||
isPartner
|
||||
|
|
|
@ -69,7 +69,7 @@ export const DEFAULT_TYPES = {
|
|||
},
|
||||
|
||||
duration(val) {
|
||||
return duration_to_string(val);
|
||||
return this.formatDuration(val);
|
||||
},
|
||||
|
||||
localestring(val) {
|
||||
|
@ -257,6 +257,10 @@ export default class TranslationCore {
|
|||
return formatter.format(value);
|
||||
}
|
||||
|
||||
formatDuration(value) { // eslint-disable-line class-methods-use-this
|
||||
return duration_to_string(value);
|
||||
}
|
||||
|
||||
formatDate(value, format) {
|
||||
if ( typeof format === 'string' && format.startsWith('::') ) {
|
||||
const f = format.substr(2),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue