1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-27 21:05:53 +00:00

4.0.0-rc7

* Added: Initial support for Chat on Videos.

Please note that moderation features are not currently available when FFZ features for Chat on Videos are enabled. There is a setting to disable custom FFZ rendering which makes moderation tools accessible.

More work has also been done on getting custom viewer cards ready.
This commit is contained in:
SirStendec 2018-07-19 22:03:01 -04:00
parent 99ba52d4ad
commit 5a7a4f3ea9
25 changed files with 679 additions and 62 deletions

Binary file not shown.

View file

@ -92,6 +92,8 @@
<glyph glyph-name="eye-off" unicode="&#xe82a;" d="M310 105l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
<glyph glyph-name="conversations" unicode="&#xe82b;" d="M155 556a77 77 0 0 0 76 63h1077a77 77 0 0 0 76-63l1 1v-630a77 77 0 0 0-77-77h-792l-296-296a39 39 0 0 0-66 27v976l1-1z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
<glyph glyph-name="twitter" unicode="&#xf099;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 115 44-21-64-80-100 52 6 104 28z" horiz-adv-x="928.6" />

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

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: '-rc6.4.1',
major: 4, minor: 0, revision: 0, extra: '-rc7',
commit: __git_commit__,
build: __webpack_hash__,
toString: () =>

View file

@ -43,6 +43,7 @@ export default class Actions extends Module {
ui: {
path: 'Chat > In-Line Actions @{"description": "Here, you can define custom actions that will appear along messages in chat. If you aren\'t seeing an action you\'ve defined here in chat, please make sure that you have enabled Mod Icons in the chat settings menu."}',
component: 'chat-actions',
context: ['user', 'room'],
inline: true,
data: () => {
@ -60,11 +61,11 @@ export default class Actions extends Module {
this.settings.add('chat.actions.viewer-card', {
// Filter out actions
process: (ctx, val) =>
val.filter(x => x.appearance &&
val.filter(x => x.type || (x.appearance &&
this.renderers[x.appearance.type] &&
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
(! x.action || this.actions[x.action])
),
)),
default: [
{v: {action: 'friend'}},
@ -80,6 +81,7 @@ export default class Actions extends Module {
_ui: {
path: 'Chat > Viewer Cards >> tabs ~> Actions @{"description": "Here, you define what actions are available on viewer cards."}',
component: 'chat-actions',
context: ['user', 'room', 'product'],
data: () => {
const chat = this.resolve('site.chat');

View file

@ -64,6 +64,8 @@ export const chat = {
}
}],
required_context: ['room'],
defaults: {
command: '@{{user.login}} HeyGuys'
},
@ -121,6 +123,8 @@ export const ban = {
}
}],
required_context: ['room', 'user'],
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-ban.vue'),
title: 'Ban User',
@ -152,6 +156,8 @@ export const timeout = {
duration: 600
},
required_context: ['room', 'user'],
editor: () => import(/* webpackChunkName: 'main-menu' */ './components/edit-timeout.vue'),
title: 'Timeout User',
@ -187,6 +193,8 @@ export const unban = {
}
}],
required_context: ['room', 'user'],
title: 'Unban User',
tooltip(data) {
@ -212,6 +220,8 @@ export const untimeout = {
}
}],
required_context: ['room', 'user'],
title: 'Untimeout User',
tooltip(data) {
@ -236,6 +246,8 @@ export const whisper = {
}
}],
required_context: ['user'],
title: 'Whisper User',
tooltip(data) {
@ -269,7 +281,7 @@ export const whisper = {
// Gift Subscription
// ============================================================================
/*export const gift_sub = {
export const gift_sub = {
presets: [{
appearance: {
type: 'icon',
@ -277,6 +289,8 @@ export const whisper = {
}
}],
required_context: ['room', 'user', 'product'],
title: 'Gift Subscription',
tooltip(data) {
@ -288,4 +302,4 @@ export const whisper = {
Woop woop.
</div>);
}
}*/
}

View file

@ -789,8 +789,8 @@ export const AddonEmotes = {
return tokens;
const emotes = this.emotes.getEmotes(
msg.user.userID,
msg.user.userLogin,
msg.user.id,
msg.user.login,
msg.roomID,
msg.roomLogin
),

View file

@ -51,7 +51,7 @@
import {get} from 'utilities/object';
const TITLE_MATCH = /^(\d+\.\d+\.\d+(?:\-[^\n]+)?)\n+/;
const TITLE_MATCH = /^(\d+\.\d+\.\d+(?:-[^\n]+)?)\n+/;
export default {

View file

@ -245,7 +245,8 @@ export default {
},
presets() {
const out = [];
const out = [],
contexts = this.item.context || [];
out.push({
disabled: this.hasInheritance,
@ -282,7 +283,19 @@ export default {
for(const key in this.data.actions) { // eslint-disable-line guard-for-in
const act = this.data.actions[key];
if ( act && act.presets )
if ( act && act.presets ) {
if ( act.required_context ) {
let okay = true;
for(const ctx of act.required_context)
if ( ! contexts.includes(ctx) ) {
okay = false;
break;
}
if ( ! okay )
continue;
}
for(const preset of act.presets) {
if ( typeof act.title !== 'string' && ! preset.title )
continue;
@ -298,6 +311,7 @@ export default {
}
}, preset));
}
}
}
return out;

View file

@ -1,54 +1,17 @@
<template>
<section class="tw-background-c tw-relative">
<div class="tw-background-c tw-full-width tw-flex tw-flex-row tw-pd-r-05 tw-pd-l-1 tw-pd-y-1">
<div class="tw-mg-r-05">
<div class="tw-inline-block">
<button class="tw-button">
<span class="tw-button__text" data-a-target="tw-button-text">Add Friend</span>
</button>
</div>
</div>
<div class="tw-mg-r-05">
<div class="tw-inline-block">
<button class="tw-button" data-a-target="usercard-whisper-button" data-test-selector="whisper-button">
<span class="tw-button__text" data-a-target="tw-button-text">Whisper</span>
</button>
</div>
</div>
<div class="tw-flex-grow-1 tw-align-right">
<div class="tw-inline-block">
<button data-title="More Options" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-ellipsis-vert" />
</span>
</button>
</div>
</div>
</div>
<div class="tw-c-background-alt-2 tw-pd-x-1 tw-pd-y-05">
<div>
<div class="tw-inline-block tw-pd-r-1">
<button data-title="Ban User" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-block" />
</span>
</button>
</div>
<div class="tw-inline-block tw-pd-r-1">
<button data-title="Timeout User" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-clock" />
</span>
</button>
</div>
<div class="tw-inline-block tw-pd-r-1">
<button data-title="Mod User" data-tooltip-type="html" class="tw-button-icon ffz-tooltip" @click="close">
<span class="tw-button-icon__icon">
<figure class="ffz-i-star" />
</span>
</button>
</div>
</div>
<section class="viewer-card__actions tw-bottom-0">
<div
v-for="(actions, idx) in display"
:key="idx"
:class="[idx === 0 ? 'tw-c-background-alt tw-pd-y-1' : 'tw-c-background-alt-2 tw-pd-y-05']"
class="tw-full-width tw-flex tw-flex-row tw-pd-x-05"
>
<action
v-for="(act, idx) in actions"
:key="idx"
:act="act"
:renderers="renderers"
/>
</div>
</section>
</template>
@ -56,10 +19,119 @@
<script>
import TabMixin from '../tab-mixin';
import {deep_copy} from 'utilities/object';
export default {
components: {
action: {
functional: true,
render(createElement, {props}) {
const {act, renderers} = props;
if ( act.type === 'space' )
return createElement('div', {
class: 'tw-flex-grow-1'
});
else if ( act.type === 'space-small' )
return createElement('div', {
class: 'tw-mg-x-1'
});
const disp = act.appearance,
renderer = disp && renderers[disp.type];
if ( ! renderer || ! renderer.component )
return null;
const content = createElement(renderer.component, {
attrs: {
color: '',
data: disp
}
});
if ( disp.button )
return createElement('button', {
class: 'tw-interactive tw-button'
}, [
createElement('span', {
class: 'tw-button__text'
}, [
content
])
]);
return createElement('button', {
class: 'tw-interactive tw-button-icon'
}, [
createElement('span', {
class: 'tw-button-icon__icon'
}, [
content
])
]);
}
}
},
mixins: [TabMixin],
props: ['tab', 'user', 'room', 'currentUser']
props: ['tab', 'channel', 'user', 'self', 'getFFZ'],
data() {
this._chat = this.getFFZ().resolve('chat');
this._actions = this.getFFZ().resolve('chat.actions');
return {
renderers: deep_copy(this._actions.renderers),
actions: deep_copy(this._chat.context.get('chat.actions.viewer-card'))
}
},
computed: {
display() {
const out = [];
let current = [];
for(const val of this.actions) {
if ( ! val )
continue;
const type = val.type;
if ( type === 'new-line' ) {
out.push(current);
current = [];
} else if ( this.displayAction(val) )
current.push(val);
}
if ( current.length )
out.push(current);
return out;
}
},
mounted() {
window.tab = this;
this._chat.context.on('changed:chat.actions.viewer-card', this.updateSetting, this);
},
destroyed() {
this._chat.context.off('changed:chat.actions.viewer-card', this.updateSetting, this);
},
methods: {
displayAction(action) {
return true;
},
updateSetting() {
this.actions = deep_copy(this._chat.context.get('chat.actions.viewer-card'));
}
},
}
</script>

View file

@ -282,7 +282,7 @@ export default class ChatLine extends Module {
bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null;
if ( ! this.ffz_user_click_handler )
this.ffz_user_click_handler = this.usernameClickHandler; // event => ! event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
this.ffz_user_click_handler = this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
let cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}`,
out = (tokens.length || ! msg.ffz_type) ? [
@ -457,5 +457,7 @@ export default class ChatLine extends Module {
this.ChatLine.forceUpdate();
this.ChatRoomLine.forceUpdate();
this.WhisperLine.forceUpdate();
this.emit('chat:updated-lines');
}
}

View file

@ -2,6 +2,8 @@
.thread-message__timestamp,
.thread-message__warning,
.vod-message,
.chat-line__message:not(.chat-line--inline),
.chat-line__moderation,
.chat-line__status,
@ -22,6 +24,22 @@
}
}
.vod-message {
padding-top: calc(.5rem - 1px) !important;
border-top: 1px solid #aaa;
border-bottom-color: var rgba(255,255,255,0.5);
.tw-theme--dark & {
border-top-color: #000;
border-bottom-color: rgba(255,255,255,0.1);
}
}
.video-chat__message-list-wrapper li:first-child .vod-message {
border-top-color: transparent !important;
}
.thread-message__message,
.thread-message__timestamp,
.thread-message__warning {

View file

@ -22,6 +22,22 @@
}
}
.vod-message {
padding-top: calc(.5rem - 1px) !important;
border-top: 1px solid rgba(255,255,255,0.5);
border-bottom-color: #aaa;
.tw-theme--dark & {
border-top-color: rgba(255,255,255,0.1);
border-bottom-color: #000;
}
}
.video-chat__message-list-wrapper li:first-child .vod-message {
border-top-color: transparent !important;
}
.thread-message__message,
.thread-message__timestamp,
.thread-message__warning {

View file

@ -15,6 +15,15 @@
}
}
.vod-message {
padding-top: calc(.5rem - 1px) !important;
border-top: 1px solid var(--ffz-border-color);
}
.video-chat__message-list-wrapper li:first-child .vod-message {
border-top-color: transparent !important;
}
.thread-message__message,
.thread-message__timestamp,
.thread-message__warning {

View file

@ -15,6 +15,15 @@
}
}
.vod-message {
padding-bottom: calc(.5rem - 1px) !important;
border-bottom: 1px solid var(--ffz-border-color);
}
.video-chat__message-list-wrapper li:last-child .vod-message {
border-bottom-color: transparent !important;
}
.thread-message__message,
.thread-message__timestamp,
.thread-message__warning {

View file

@ -1,3 +1,4 @@
.video-chat__message-list-wrapper,
.whispers-thread__content,
.chat-list {
font-size: var(--ffz-chat-font-size);

View file

@ -7,4 +7,12 @@
background-color: rgba(255,0,0,.3) !important;
}
}
}
.video-chat__message-list-wrapper li:nth-child(2n+0) .vod-message.ffz-mentioned:not(.ffz-custom-color) {
background-color: rgba(255,127,127,.4) !important;
.tw-theme--dark & {
background-color: rgba(255,0,0,.3) !important;
}
}

View file

@ -1,3 +1,4 @@
.vod-message,
.chat-line__message:not(.chat-line--inline),
.user-notice-line {
&.ffz-mentioned:not(.ffz-custom-color) {

View file

@ -21,3 +21,14 @@
}
}
.video-chat__message-list-wrapper li .vod-message:not(.ffz-custom-color) {
background-color: transparent !important;
}
.video-chat__message-list-wrapper li:nth-child(2n+0) .vod-message:not(.ffz-custom-color) {
background-color: rgba(0,0,0,0.1) !important;
.tw-theme--dark & {
background-color: rgba(255,255,255,0.05) !important;
}
}

View file

@ -0,0 +1,421 @@
'use strict';
// ============================================================================
// Video Chat Hooks
// ============================================================================
import {has, get} from 'utilities/object';
import {print_duration} from 'utilities/time';
import {ClickOutside} from 'utilities/dom';
import {formatBitsConfig} from '../chat';
import Module from 'utilities/module';
export default class VideoChatHook extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.inject('i18n');
this.inject('settings');
this.inject('site');
this.inject('site.router');
this.inject('site.fine');
this.inject('site.web_munch');
this.inject('chat');
this.injectAs('site_chat', 'site.chat');
this.inject('site.chat.chat_line.rich_content');
this.VideoChatController = this.fine.define(
'video-chat-controller',
n => n.onMessageScrollAreaMount && n.createReply,
['video']
);
this.VideoChatLine = this.fine.define(
'video-chat-line',
n => n.onReplyClickHandler && n.shouldFocusMessage,
['video']
);
// Settings
this.settings.add('chat.video-chat.enabled', {
default: true,
ui: {
path: 'Chat > Chat on Videos @{"description": "This feature is currently in beta. As such, you may experience issues when using FFZ features with Chat on Videos."} >> General',
title: 'Enable FrankerFaceZ features for Chat on Videos.',
description: 'Display FFZ badges, emotes, and other features in Chat on Videos. Moderation features may be unavailable when this is enabled.',
component: 'setting-check-box'
}
});
}
async onEnable() {
this.chat.context.on('changed:chat.video-chat.enabled', this.updateLines, this);
this.on('chat:updated-lines', this.updateLines, this);
this.VideoChatController.on('mount', this.chatMounted, this);
this.VideoChatController.on('unmount', this.chatUnmounted, this);
this.VideoChatController.on('receive-props', this.chatUpdated, this);
this.VideoChatController.ready((cls, instances) => {
for(const inst of instances) {
this.chatMounted(inst);
}
});
const t = this,
React = await this.web_munch.findModule('react');
if ( ! React )
return;
/*this.MessageMenu = class FFZMessageMenu extends React.Component {
constructor(props) {
super(props);
this.onClick = () => this.setState({open: ! this.state.open});
this.onClickOutside = () => this.state.open && this.setState({open: false});
this.element = null;
this.saveRef = element => this.element = element;
this.state = {
open: false
}
}
componentDidMount() {
if ( this.element )
this._clicker = new ClickOutside(this.element, this.onClickOutside);
}
componentWillUnmount() {
this._clicker.destroy();
this._clicker = null;
}
render() {
const is_open = this.state.open;
return (<div ref={this.saveRef} data-test-selector="menu-options-wrapper" class={`tw-flex-shrink-0 video-chat__message-menu${is_open ? ' video-chat__message-menu--force-visible' : ''}`}>
<div class="tw-relative">
<button class="tw-interactive tw-button-icon tw-button-icon--secondary tw-button-icon--small" data-test-selector="menu-button" onClick={this.onClick}>
<span class="tw-button-icon__icon">
<figure class="ffz-i-ellipsis-vert" />
</span>
</button>
<div class={`tw-absolute tw-balloon tw-balloon--down tw-balloon--right tw-balloon--sm ${is_open ? 'tw-block' : 'tw-hide'}`}>
<div class="tw-absolute tw-balloon__tail tw-overflow-hidden">
<div class="tw-absolute tw-balloon__tail-symbol tw-border-b tw-border-l tw-border-r tw-border-t tw-c-background" />
</div>
<div class="tw-border-b tw-border-l tw-border-r tw-border-radius-medium tw-border-t tw-c-background tw-elevation-1 tw-pd-y-1">
<button class="tw-interactable tw-interactable--inverted tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.copy-link', 'Copy Link')
}</button>
<button class="tw-interactable tw-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.delete', 'Delete')
}</button>
<div class="tw-mg-1 tw-border-b" />
<button class="tw-interactable tw-interactable--alert tw-full-width tw-pd-y-05 tw-pd-x-1">{
t.i18n.t('video-chat.ban', 'Ban User')
}</button>
</div>
</div>
</div>
</div>)
}
}*/
const createElement = React.createElement,
FFZRichContent = this.rich_content && this.rich_content.RichContent;
this.VideoChatLine.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.ffzRenderMessage = function(msg) {
const is_action = msg.is_action,
user = msg.user,
color = t.site_chat.colors.process(user.color),
tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, user),
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg);
return (<div class="tw-align-items-start tw-flex tw-flex-nowrap tw-c-text">
<div class="tw-flex-grow-1" data-room-id={msg.roomID} data-room={msg.roomLogin} data-user-id={user.id} data-user={user.login}>
<span class="chat-line__message--badges">{
t.chat.badges.render(msg, createElement)
}</span>
<a
class="video-chat__message-author notranslate"
data-test-selector="comment-author-selector"
href={`/${user.login}`}
rel="noopener noreferrer"
target="_blank"
style={{color}}
>
<span class="chat-author__display-name" data-a-target="chat-message-username" data-a-user={user.login} data-test-selector="message-username">{ user.displayName }</span>
{user.isIntl && <span class="chat-author__intl-login" data-test-selector="message-username-canonical"> ({ user.login})</span>}
</a>
<div data-test-selector="comment-message-selector" class="tw-inline video-chat__message">
<span>{is_action ? ' ' : ': '}</span>
<span class="message" style={{color: is_action ? color : null}}>{ t.chat.renderTokens(tokens, createElement) }</span>
{rich_content && createElement(FFZRichContent, rich_content)}
</div>
</div>
</div>);
}
cls.prototype.ffzRenderExpanded = function(msg) {
if ( ! msg._reply_handler )
msg._reply_handler = () => this.onReplyClickHandler(msg.user.login);
return (<div class="tw-align-items-center tw-flex tw-pd-t-05">
<button class="tw-button tw-button--text" data-test-selector="parent-reply-button" onClick={msg._reply_handler}>
<span class="tw-button__text tw-pd-0">{ t.i18n.t('video-chat.reply', 'Reply') }</span>
</button>
<span class="tw-c-text-alt-2 tw-font-size-7 tw-mg-l-05 tw-tooltip-wrapper">
{ t.i18n.t('video-chat.time', '%{time|humanTime} ago', {
time: msg.timestamp
}) }
<div class="tw-tooltip tw-tooltip--align-center tw-tooltip--up" role="tooltip">
{ msg.timestamp.toLocaleString() }
</div>
</span>
</div>)
}
cls.prototype.render = function() {
try {
if ( this.state.showReplyForm || ! t.chat.context.get('chat.video-chat.enabled') )
return old_render.call(this);
t.log.info('Video Chat', this);
const context = this.props.messageContext,
msg = t.standardizeMessage(context.comment, context.author),
main_message = this.ffzRenderMessage(msg),
bg_css = msg.mentioned && msg.mention_color ? t.site_chat.inverse_colors.process(msg.mention_color) : null;
if ( msg.ffz_removed )
return null;
return (<div
data-test-selector="message-layout"
class={`tw-align-items-start tw-flex tw-flex-nowrap tw-full-width tw-pd-l-05 tw-pd-y-05 vod-message${msg.mentioned ? ' ffz-mentioned' : ''}${bg_css ? ' ffz-custom-color' : ''}`}
style={{backgroundColor: bg_css}}
>
{this.props.hideTimestamp || (<div data-test-selector="message-timestamp" class="tw-align-right tw-flex tw-flex-shrink-0 vod-message__header">
<div class="tw-mg-r-05">
<div class="tw-inline-flex tw-tooltip-wrapper">
<button class="tw-block tw-full-width tw-interactable tw-interactable--inverted" onClick={this.onTimestampClickHandler}>
<div class="tw-pd-x-05">
<p class="tw-font-size-7">{print_duration(context.comment.contentOffset)}</p>
</div>
</button>
<div class="tw-tooltip tw-tooltip--align-left tw-tooltip--up" role="tooltip">
{t.i18n.t('video-chat.jump', 'Jump to Video')}
</div>
</div>
</div>
</div>)}
<div class="tw-full-width">
{ main_message }
{ this.props.isExpandedLayout && this.ffzRenderExpanded(msg) }
{ context.replies.length > 0 && (<div class="qa-vod-chat-reply tw-mg-l-05 tw-mg-y-05 vod-message__reply">
{ context.comment.moreReplies && (<div class="tw-inline-block vod-message__show-more-replies">
<button class="tw-interactive tw-button tw-button--text" onClick={this.onLoadMoreRepliesClickHandler}>
<span class="tw-button__text" data-a-target="tw-button-text">{
t.i18n.t('video-chat.show-more', 'Show more replies...')
}</span>
</button>
</div>)}
<ul>{
context.replies.map(reply => (<li class="tw-mg-l-05">
{ this.ffzRenderMessage(t.standardizeMessage(reply.comment, reply.author)) }
{ this.props.isExpandedLayout && this.ffzRenderExpanded(msg) }
</li>))
}</ul>
</div>)}
</div>
</div>)
} catch(err) {
t.log.error('Problem rendering Chat', err);
return old_render.call(this);
}
}
// Do this after a short delay to hopefully reduce the chance of React
// freaking out on us.
setTimeout(() => this.VideoChatLine.forceUpdate());
})
}
updateLines() {
for(const inst of this.VideoChatLine.instances) {
const context = inst.props.messageContext;
if ( ! context.comment )
continue;
context.comment._ffz_message = null;
if ( Array.isArray(context.replies) )
for(const reply of context.replies)
if ( reply.comment )
reply.comment._ffz_message = null;
}
this.VideoChatLine.forceUpdate();
}
// ========================================================================
// Message Standardization
// ========================================================================
standardizeMessage(comment, author) { // eslint-disable-line class-methods-use-this
if ( comment._ffz_message )
return comment._ffz_message;
const room = this.chat.getRoom(comment.channelId, null, true, true);
const out = comment._ffz_message = {
user: {
color: comment.message.userColor,
id: author.id,
login: author.name,
displayName: author.displayName,
isIntl: author.name && author.displayName && author.displayName.trim().toLowerCase() !== author.name,
type: author.type
},
roomLogin: room && room.login,
roomID: room && room.id,
badges: comment.userBadges,
messageParts: comment.message.tokens,
is_action: comment.message.isAction,
more_replies: comment.moreReplies,
timestamp: comment.createdAt
};
this.chat.detokenizeMessage(out);
return out;
}
// ========================================================================
// Room Handling
// ========================================================================
addRoom(thing, props) {
if ( ! props )
props = thing.props;
const channel = get('data.video.owner', props);
if ( ! channel || ! channel.id )
return null;
const room = thing._ffz_room = this.chat.getRoom(channel.id, channel.login && channel.login.toLowerCase(), false, true);
room.ref(thing);
return room;
}
removeRoom(thing) { // eslint-disable-line class-methods-use-this
if ( ! thing._ffz_room )
return;
thing._ffz_room.unref(thing);
thing._ffz_room = null;
}
// ========================================================================
// Video Chat Controller
// ========================================================================
chatMounted(chat, props) {
if ( ! props )
props = chat.props;
if ( ! this.addRoom(chat, props) )
return;
this.chat.badges.updateTwitchBadges(get('data.badges', props));
this.updateRoomBadges(chat, get('data.video.owner.broadcastBadges', props));
this.updateRoomBitsConfig(chat, props.bitsConfig);
}
chatUpdated(chat, props) {
if ( get('data.video.owner.id', props) !== get('data.video.owner.id', chat.props) ) {
this.removeRoom(chat);
this.chatMounted(chat, props);
return;
}
const new_badges = get('data.badges', props),
old_badges = get('data.badges', chat.props),
new_room_badges = get('data.video.owner.broadcastBadges', props),
old_room_badges = get('data.video.owner.broadcastBadges', chat.props);
if ( new_badges !== old_badges )
this.chat.badges.updateTwitchBadges(new_badges);
if ( new_room_badges !== old_room_badges )
this.updateRoomBadges(chat, new_room_badges);
if ( props.bitsConfig !== chat.props.bitsConfig )
this.updateRoomBitsConfig(chat, props.bitsConfig);
const channel = get('data.video.owner', props);
this.settings.updateContext({
moderator: props.isCurrentUserModerator
});
this.chat.context.updateContext({
moderator: props.isCurrentUserModerator,
channel: channel ? channel.login : null,
channelID: channel ? channel.id : null
});
}
chatUnmounted(chat) {
this.removeRoom(chat);
}
updateRoomBadges(chat, badges) { // eslint-disable-line class-methods-use-this
const room = chat._ffz_room;
if ( ! room )
return;
room.updateBadges(badges);
}
updateRoomBitsConfig(chat, config) { // eslint-disable-line class-methods-use-this
const room = chat._ffz_room;
if ( ! room )
return;
room.updateBitsConfig(formatBitsConfig(config));
}
}
function formatDuration(seconds) {
}

View file

@ -1,3 +1,8 @@
.chat-line__message--emote {
vertical-align: middle;
margin: -.5rem 0;
}
.chat-author__display-name,
.chat-author__intl-login {
cursor: pointer;

View file

@ -21,4 +21,15 @@ export function duration_to_string(elapsed, separate_days, days_only, no_hours,
(!no_hours || days || hours) ? `${days && hours < 10 ? '0' : ''}${hours}:` : ''
}${minutes < 10 ? '0' : ''}${minutes}${
no_seconds ? '' : `:${seconds < 10 ? '0' : ''}${seconds}`}`;
}
export function print_duration(seconds) {
let minutes = Math.floor(seconds / 60),
hours = Math.floor(minutes / 60);
minutes %= 60;
seconds %= 60;
return `${hours > 0 ? `${hours}:${minutes < 10 ? '0' : ''}` : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
}

View file

@ -105,6 +105,7 @@
.ffz-i-views:before { content: '\e828'; } /* '' */
.ffz-i-eye:before { content: '\e829'; } /* '' */
.ffz-i-eye-off:before { content: '\e82a'; } /* '' */
.ffz-i-conversations:before { content: '\e82b'; } /* '' */
.ffz-i-link-ext:before { content: '\f08e'; } /* '' */
.ffz-i-twitter:before { content: '\f099'; } /* '' */
.ffz-i-gauge:before { content: '\f0e4'; } /* '' */