-
-
-
-
-
-
-
-
-
-
-
+
@@ -56,10 +19,119 @@
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js
index 9123b53d..040cb341 100644
--- a/src/sites/twitch-twilight/modules/chat/line.js
+++ b/src/sites/twitch-twilight/modules/chat/line.js
@@ -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');
}
}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss
index ac12ac76..07f45a32 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d-inset.scss
@@ -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 {
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss
index ce400817..0d78fa6a 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-3d.scss
@@ -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 {
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss
index 27ecd26b..ef51346e 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders-wide.scss
@@ -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 {
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss
index c6ac2e54..f439bf7d 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-borders.scss
@@ -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 {
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-font.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-font.scss
index fe3bf8c1..e5612089 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-font.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-font.scss
@@ -1,3 +1,4 @@
+.video-chat__message-list-wrapper,
.whispers-thread__content,
.chat-list {
font-size: var(--ffz-chat-font-size);
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-alt.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-alt.scss
index 741d99f1..528d8745 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-alt.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg-alt.scss
@@ -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;
+ }
}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg.scss
index 7ce2602a..ea113526 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-mention-bg.scss
@@ -1,3 +1,4 @@
+.vod-message,
.chat-line__message:not(.chat-line--inline),
.user-notice-line {
&.ffz-mentioned:not(.ffz-custom-color) {
diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss
index d3ce8966..44d582b4 100644
--- a/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss
+++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/chat-rows.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/modules/video_chat/index.jsx b/src/sites/twitch-twilight/modules/video_chat/index.jsx
new file mode 100644
index 00000000..2505834d
--- /dev/null
+++ b/src/sites/twitch-twilight/modules/video_chat/index.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
)
+ }
+ }*/
+
+ 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 (
);
+ }
+
+ cls.prototype.ffzRenderExpanded = function(msg) {
+ if ( ! msg._reply_handler )
+ msg._reply_handler = () => this.onReplyClickHandler(msg.user.login);
+
+ return (
+
+
+ • { t.i18n.t('video-chat.time', '%{time|humanTime} ago', {
+ time: msg.timestamp
+ }) }
+
+ { msg.timestamp.toLocaleString() }
+
+
+
)
+ }
+
+ 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 (
+ {this.props.hideTimestamp || ()}
+
+ { main_message }
+ { this.props.isExpandedLayout && this.ffzRenderExpanded(msg) }
+ { context.replies.length > 0 && (
+ { context.comment.moreReplies && (
+
+
)}
+
{
+ context.replies.map(reply => (-
+ { this.ffzRenderMessage(t.standardizeMessage(reply.comment, reply.author)) }
+ { this.props.isExpandedLayout && this.ffzRenderExpanded(msg) }
+
))
+ }
+
)}
+
+
)
+
+ } 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) {
+
+}
\ No newline at end of file
diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss
index 467e5a42..c91c661c 100644
--- a/src/sites/twitch-twilight/styles/chat.scss
+++ b/src/sites/twitch-twilight/styles/chat.scss
@@ -1,3 +1,8 @@
+.chat-line__message--emote {
+ vertical-align: middle;
+ margin: -.5rem 0;
+}
+
.chat-author__display-name,
.chat-author__intl-login {
cursor: pointer;
diff --git a/src/utilities/time.js b/src/utilities/time.js
index 1f58767c..f5a6dd1e 100644
--- a/src/utilities/time.js
+++ b/src/utilities/time.js
@@ -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}`;
}
\ No newline at end of file
diff --git a/styles/icons.scss b/styles/icons.scss
index 91fcf5a9..36b89ba9 100644
--- a/styles/icons.scss
+++ b/styles/icons.scss
@@ -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'; } /* '' */