1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-08-03 00:18:31 +00:00
FrankerFaceZ/src/sites/twitch-twilight/modules/chat/index.js

3398 lines
93 KiB
JavaScript
Raw Normal View History

2017-11-13 01:23:39 -05:00
'use strict';
// ============================================================================
// Chat Hooks
// ============================================================================
import {Color, ColorAdjuster} from 'utilities/color';
import {get, has, make_enum, shallow_object_equals, set_equals, deep_equals, glob_to_regex, escape_regex} from 'utilities/object';
import {WEBKIT_CSS as WEBKIT} from 'utilities/constants';
import {useFont} from 'utilities/fonts';
2017-11-13 01:23:39 -05:00
import Module from 'utilities/module';
import Twilight from 'site';
2017-11-14 04:12:10 -05:00
import Scroller from './scroller';
import ChatLine from './line';
import SettingsMenu from './settings_menu';
import EmoteMenu from './emote_menu';
import Input from './input';
import ViewerCards from './viewer_card';
import { isHighlightedReward } from './points';
/*const REGEX_EMOTES = {
'B-?\\)': ['B)', 'B-)'],
'R-?\\)': ['R)', 'R-)'],
'[oO](_|\\.)[oO]': ['o_o', 'O_o', 'o_O', 'O_O', 'o.o', 'O.o', 'o.O', 'O.O'],
'\\&gt\\;\\(': ['>('],
'\\&lt\\;3': ['<3'],
'\\:-?(o|O)': [':o', ':O', ':-o', ':-O'],
'\\:-?(p|P)': [':p', ':P', ':-p', ':-P'],
'\\:-?D': [':D', ':-D'],
'\\:-?[\\\\/]': [':/', ':-/', ':\\', ':-\\'],
'\\:-?[z|Z|\\|]': [':z', ':Z', ':|', ':-z', ':-Z', ':-|'],
'\\:-?\\(': [':(', ':-('],
'\\:-?\\)': [':)', ':-)'],
'\\;-?(p|P)': [';p', ';P', ';-p', ';-P'],
'\\;-?\\)': [';)', ';-)'],
'#-?[\\\\/]': ['#/', '#-/', '#//', '#-//'],
':-?(?:7|L)': [':7', ':L', ':-7', ':-L'],
'\\&lt\\;\\]': ['<]'],
'\\:-?(S|s)': [':s', ':S', ':-s', ':-S'],
'\\:\\&gt\\;': [':>']
};*/
const MESSAGE_TYPES = make_enum(
'Post',
'Action'
);
const MOD_TYPES = make_enum(
'Ban',
'Timeout',
'Delete'
);
const AUTOMOD_TYPES = make_enum(
'MessageRejectedPrompt',
'CheerMessageRejectedPrompt',
'MessageRejected',
'MessageAllowed',
'MessageDenied',
'CheerMessageDenied',
'CheerMessageTimeout',
'MessageModApproved',
'MessageModDenied'
);
const UNBLOCKABLE_TYPES = [
'Message',
'Notice',
'Moderation',
'ModerationAction',
'TargetedModerationAction',
'AutoMod',
'SubscriberOnlyMode',
'FollowerOnlyMode',
'SlowMode',
'EmoteOnlyMode',
'R9KMode',
'Connected',
'Disconnected',
'Reconnect',
'RoomMods',
'RoomState',
'BadgesUpdated'
]
const CHAT_TYPES = make_enum(
'Message',
'ExtensionMessage',
'Moderation',
'ModerationAction',
'TargetedModerationAction',
'AutoMod',
'SubscriberOnlyMode',
'FollowerOnlyMode',
'SlowMode',
'EmoteOnlyMode',
'R9KMode',
'Connected',
'Disconnected',
'Reconnect',
'Subscription',
'Resubscription',
'GiftPaidUpgrade',
'AnonGiftPaidUpgrade',
'PrimePaidUpgrade',
'PrimeCommunityGiftReceivedEvent',
'ExtendSubscription',
'SubGift',
'AnonSubGift',
'Clear',
'RoomMods',
'RoomState',
'Raid',
'Unraid',
'Notice',
'Info',
'BadgesUpdated',
'Purchase',
'BitsCharity',
'CrateGift',
'RewardGift',
'SubMysteryGift',
'AnonSubMysteryGift',
'StandardPayForward',
'CommunityPayForward',
'FirstCheerMessage',
'FirstMessageHighlight',
'BitsBadgeTierMessage',
'InlinePrivateCallout',
'ChannelPointsReward',
'CommunityChallengeContribution',
'LiveMessageSeparator',
'RestrictedLowTrustUserMessage',
'CommunityIntroduction',
'Shoutout',
'AnnouncementMessage',
'MidnightSquid',
'CharityDonation',
'MessageIdUpdate',
'PinnedChat',
'ViewerMilestone'
);
const NULL_TYPES = [
'Reconnect',
'RoomState',
'BadgesUpdated',
'Clear'
];
2017-11-14 04:12:10 -05:00
const MISBEHAVING_EVENTS = [
'onBadgesUpdatedEvent',
2017-11-13 01:23:39 -05:00
];
export default class ChatHook extends Module {
constructor(...args) {
super(...args);
this.should_enable = true;
this.colors = new ColorAdjuster;
this.inverse_colors = new ColorAdjuster;
2017-11-13 01:23:39 -05:00
this.inject('settings');
this.inject('i18n');
this.inject('experiments');
2017-11-13 01:23:39 -05:00
this.inject('site');
this.inject('site.router');
this.inject('site.fine');
this.inject('site.web_munch');
this.inject('site.css_tweaks');
this.inject('site.subpump');
2017-11-13 01:23:39 -05:00
this.inject('chat');
2017-11-14 04:12:10 -05:00
this.inject(Scroller);
this.inject(ChatLine);
this.inject(SettingsMenu);
this.inject(EmoteMenu);
this.inject(Input);
this.inject(ViewerCards);
this.ChatLeaderboard = this.fine.define(
'chat-leaderboard',
n => n.props?.topItems && has(n.props, 'forceMiniView') && has(n.props, 'leaderboardType'),
Twilight.CHAT_ROUTES
);
this.ChatService = this.fine.define(
'chat-service',
n => n.join && n.client && n.props.setChatConnectionAPI,
Twilight.CHAT_ROUTES
);
this.ChatBuffer = this.fine.define(
'chat-buffer',
n => n.updateHandlers && n.delayedMessageBuffer && n.handleMessage,
Twilight.CHAT_ROUTES
);
2017-11-13 01:23:39 -05:00
this.ChatController = this.fine.define(
'chat-controller',
n => n.parseOutgoingMessage && n.onRoomStateUpdated && n.renderNotifications,
Twilight.CHAT_ROUTES
2017-11-13 01:23:39 -05:00
);
this.ChatContainer = this.fine.define(
'chat-container',
n => n.closeViewersList && n.onChatInputFocus,
Twilight.CHAT_ROUTES
2017-11-13 01:23:39 -05:00
);
this.ChatBufferConnector = this.fine.define(
'chat-buffer-connector',
n => n.clearBufferHandle && n.syncBufferedMessages,
Twilight.CHAT_ROUTES
);
this.joined_raids = new Set;
this.RaidController = this.fine.define(
'raid-controller',
n => n.handleLeaveRaid && n.handleJoinRaid,
Twilight.CHAT_ROUTES
);
this.InlineCallout = this.fine.define(
'inline-callout',
n => n.showCTA && n.toggleContextMenu && n.actionClick,
Twilight.CHAT_ROUTES
);
this.PinnedCallout = this.fine.define(
'pinned-callout',
n => n.getCalloutTitle && n.buildCalloutProps && n.pin,
Twilight.CHAT_ROUTES
);
2017-11-13 01:23:39 -05:00
this.CalloutSelector = this.fine.define(
'callout-selector',
n => n.selectCalloutComponent && n.props && n.props.callouts,
Twilight.CHAT_ROUTES
);
this.PointsButton = this.fine.define(
'points-button',
n => n.renderIcon && n.renderFlame && n.handleIconAnimationComplete,
Twilight.CHAT_ROUTES
);
this.PointsClaimButton = this.fine.define(
'points-claim-button',
n => n.getClaim && n.onClick && n.props && n.props.claimCommunityPoints,
Twilight.CHAT_ROUTES
);
this.CommunityChestBanner = this.fine.define(
'community-chest-banner',
n => n.getLastGifterText && n.getBannerText && has(n, 'finalCount'),
Twilight.CHAT_ROUTES
);
this.PointsInfo = this.fine.define(
'points-info',
n => n.pointIcon !== undefined && n.pointName !== undefined,
Twilight.CHAT_ROUTES
);
this.GiftBanner = this.fine.define(
'gift-banner',
n => n.getBannerText && n.onGiftMoreClick,
Twilight.CHAT_ROUTES
);
2017-11-13 01:23:39 -05:00
// Settings
4.64.0 This update adds a check that forces users to agree to YouTube's Terms of Service before they are able to view rich embeds for YouTube links. I personally do not agree with this, but we were required to implement this in order to maintain access to YouTube's API. Actually, they said "API Clients must state in their own terms of use that, by using those API Clients, users are agreeing to be bound by the YouTube Terms of Service." but that's obviously ridiculous for this use case. This is my compromise. Sorry for the inconvenience, everyone. This also comes with aesthetic tweaks to make YouTube's compliance team happy. Woo... * Added: Setting to display labels on highlighted chat messages giving the reason why the message was highlighted. * Added: System to force users to agree to a service's Terms of Service before displaying rich content from specific providers. So far this is only used by YouTube. * Changed: Made the background of highlighted words in chat messages slightly smaller. * Fixed: A few page elements in mod view not being themed correctly. * Fixed: Timestamps displaying with an hour when they obviously do not need to. * API Added: `main_menu:open` event for a general way to open the main menu. * API Added: Settings UI elements using components using the `provider-mixin` can now override the provider key they use by setting an `override_setting` value on their definition. * API Changed: The `chat.addHighlightReason(key, data, label)` method now takes an optional `label` parameter to set the text that appears on chat messages when the setting to display labels is enabled.
2023-12-19 16:24:33 -05:00
this.settings.add('chat.filtering.show-reasons', {
default: false,
ui: {
path: 'Chat > Filtering > General >> Appearance',
title: 'Display Reasons',
description: 'If this is enabled, the reasons a given message was highlighted will be displayed alongside the message. This is a simple display. Enable the debugging option below in Behavior for more details, but be aware that the debugging option has a slight performance impact compared to this.',
component: 'setting-select-box',
data: [
{value: false, title: 'Disabled'},
{value: 1, title: 'Above Message'},
{value: 2, title: 'Inline'}
]
}
});
this.settings.add('chat.disable-handling', {
default: null,
requires: ['context.disable-chat-processing'],
process(ctx, val) {
if ( val != null )
return ! val;
if ( ctx.get('context.disable-chat-processing') )
return true;
return false;
},
ui: {
path: 'Debugging > Chat >> Processing',
title: 'Enable processing of chat messages.',
component: 'setting-check-box',
force_seen: true
}
});
this.settings.addUI('debug.chat-test', {
path: 'Debugging > Chat >> Chat',
component: 'chat-tester',
getChat: () => this,
force_seen: true
});
this.settings.add('chat.filtering.blocked-types', {
default: [],
type: 'array_merge',
always_inherit: true,
process(ctx, val) {
const out = new Set;
for(const v of val)
if ( v?.v || ! UNBLOCKABLE_TYPES.includes(v.v) )
out.add(v.v);
return out;
},
ui: {
path: 'Chat > Filtering > Block >> Message Types @{"description":"This filter allows you to remove all messages of a certain type from Twitch chat. It can be used to filter system messages, such as Hosts or Raids. Some types, such as moderation actions, cannot be blocked to prevent chat functionality from breaking."}',
component: 'blocked-types',
data: () => Object
.keys(this.chat_types)
.filter(key => ! UNBLOCKABLE_TYPES.includes(key) && ! /^\d+$/.test(key))
.sort()
}
});
this.settings.add('chat.replies.style', {
default: 1,
ui: {
path: 'Chat > Appearance >> Replies',
title: 'Style',
description: `Twitch's default style adds a floating button to the right and displays a notice above messages that are replies. FrankerFaceZ uses an In-Line Chat Action (that can be removed in [Chat > Actions > In-Line](~chat.actions.in_line)) and uses an in-line mention to denote replies.`,
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Twitch (Default)'},
{value: 2, title: 'FrankerFaceZ'}
]
}
});
this.settings.add('channel.raids.no-autojoin', {
default: false,
ui: {
path: 'Channel > Behavior >> Raids',
title: 'Do not automatically join raids.',
component: 'setting-check-box'
}
});
this.settings.add('channel.raids.blocked-channels', {
default: [],
type: 'array_merge',
always_inherit: true,
ui: {
path: 'Channel > Behavior >> Raids: Blocked Channels @{"description": "You will not automatically join raids to channels listed here."}',
component: 'basic-terms',
words: false
}
});
this.settings.add('__filter:channel.raids.blocked-channels', {
requires: ['channel.raids.blocked-channels'],
equals: 'requirements',
process(ctx) {
const val = ctx.get('channel.raids.blocked-channels');
if ( ! val || ! val.length )
return null;
const out = [];
for(const item of val) {
const t = item.t;
let v = item.v;
if ( t === 'glob' )
v = glob_to_regex(v);
else if ( t !== 'raw' )
v = escape_regex(v);
if ( ! v || ! v.length )
continue;
out.push(v);
}
if ( out.length )
return new RegExp(`^(?:${out.join('|')})$`, 'gi');
return null;
}
})
this.settings.add('chat.hide-community-highlights', {
default: false,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Hide all Community Highlights from the top of chat.',
component: 'setting-check-box',
description: 'Community Highlights are polls, community gift subs, etc. that float over the top of chat temporarily with no way to close them.'
}
});
this.settings.add('chat.subs.gift-banner', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Display a banner at the top of chat when a mass gift sub happens.',
component: 'setting-check-box'
}
});
this.settings.add('chat.banners.last-events', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Allow the Support Activity Feed to be displayed in chat.',
component: 'setting-check-box'
}
});
this.settings.add('chat.banners.charity', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Allow the charity fundraiser progress to be displayed in chat.',
component: 'setting-check-box'
}
});
this.settings.add('chat.banners.hype-train', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Allow the Hype Train to be displayed in chat.',
component: 'setting-check-box',
}
});
this.settings.add('chat.banners.kappa-train', {
default: false,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Attempt to always display the Golden Kappa Train, even if other Hype Trains are hidden.',
description: '**Note**: This setting is currently theoretical and may not work, or may cause non-Kappa hype trains to appear. Due to the infrequent nature of hype trains, and especially the golden kappa hype train, it is very hard to test.',
component: 'setting-check-box'
}
});
this.settings.add('chat.banners.drops', {
default: true,
ui: {
path: 'Chat > Drops >> Appearance',
title: 'Allow messages about Drops to be displayed in chat.',
component: 'setting-check-box'
}
});
this.settings.add('chat.banners.polls', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Allow Polls to be displayed in chat.',
component: 'setting-check-box'
}
});
this.settings.add('chat.banners.prediction', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Allow Predictions to be displayed in chat.',
component: 'setting-check-box'
}
});
this.settings.add('chat.community-chest.show', {
default: true,
ui: {
path: 'Chat > Appearance >> Community',
title: 'Display the Community Gift Chest banner.',
component: 'setting-check-box'
}
});
this.settings.add('chat.points.allow-highlight', {
default: 2,
ui: {
path: 'Chat > Channel Points >> Appearance',
title: 'Highlight the message in chat when someone redeems Highlight My Message.',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Twitch Style'},
{value: 2, title: 'FFZ Style'}
]
}
});
this.settings.add('chat.points.show-callouts', {
default: true,
ui: {
path: 'Chat > Channel Points >> General',
title: 'Display messages in chat about Channel Points rewards.',
component: 'setting-check-box'
}
});
this.settings.add('chat.points.show-button', {
default: true,
ui: {
path: 'Chat > Channel Points >> General',
title: 'Display Channel Points button beneath chat.',
component: 'setting-check-box'
}
});
this.settings.add('chat.points.show-rewards', {
default: true,
requires: ['layout.portrait-min-chat'],
process(ctx, val) {
if ( ctx.get('layout.portrait-min-chat') )
return false;
return val;
},
ui: {
path: 'Chat > Channel Points >> Behavior',
title: 'Allow available rewards to appear next to the Channel Points button.',
component: 'setting-check-box'
}
});
this.settings.add('chat.points.auto-rewards', {
default: false,
ui: {
path: 'Chat > Channel Points >> Behavior',
title: 'Automatically claim bonus rewards.',
component: 'setting-check-box',
force_seen: true
}
});
this.settings.add('chat.drops.auto-rewards', {
default: false,
ui: {
path: 'Chat > Drops >> Behavior',
title: 'Automatically claim drops.',
component: 'setting-check-box',
}
});
this.settings.add('chat.pin-resubs', {
default: false,
ui: {
path: 'Chat > Behavior >> General',
title: 'Automatically pin re-subscription messages in chat.',
component: 'setting-check-box'
}
});
2017-11-13 01:23:39 -05:00
this.settings.add('chat.width', {
default: null,
2017-11-13 01:23:39 -05:00
ui: {
path: 'Chat > Appearance >> General @{"sort": -1}',
title: 'Width',
description: "How wide chat should be, in pixels. This may be affected by your browser's zoom and font size settings.",
2017-11-13 01:23:39 -05:00
component: 'setting-text-box',
process(val) {
val = parseInt(val, 10);
if ( isNaN(val) || ! isFinite(val) || val <= 0 )
return null;
2017-11-13 01:23:39 -05:00
return val;
}
}
});
this.settings.add('chat.effective-width', {
requires: ['chat.width', 'context.ui.rightColumnWidth'],
process(ctx) {
const val = ctx.get('chat.width');
return val == null ? (ctx.get('context.ui.rightColumnWidth') || 340) : val;
}
});
this.settings.add('chat.use-width', {
requires: ['chat.width', 'context.ui.rightColumnExpanded', 'context.isWatchParty'],
process(ctx) {
if ( ! ctx.get('context.ui.rightColumnExpanded') || ctx.get('context.isWatchParty') )
return false;
return ctx.get('chat.width') != null;
}
});
2017-11-13 01:23:39 -05:00
this.settings.add('chat.bits.show-pinned', {
requires: ['chat.bits.show'],
default: null,
process(ctx, val) {
if ( val != null )
return val;
return ctx.get('chat.bits.show')
},
2017-11-13 01:23:39 -05:00
ui: {
path: 'Chat > Appearance >> Community',
title: 'Display Leaderboard',
description: 'The leaderboard shows the top cheerers and sub gifters in a channel.\n\nBy default due to a previous implementation, this inherits its value from [Chat > Bits and Cheering > Display Bits](~chat.bits_and_cheering).',
component: 'setting-check-box'
}
});
this.settings.add('chat.bits.show-rewards', {
requires: ['chat.bits.show'],
default: null,
process(ctx, val) {
if ( val != null )
return val;
2017-11-13 01:23:39 -05:00
return ctx.get('chat.bits.show')
},
ui: {
path: 'Chat > Bits and Cheering >> Behavior',
title: 'Display messages when a cheer shares rewards to people in chat.',
description: 'By default, this inherits its value from Display Bits. This setting only affects newly arrived messages.',
2017-11-13 01:23:39 -05:00
component: 'setting-check-box'
}
});
this.settings.add('chat.bits.cheer-notice', {
default: true,
ui: {
path: 'Chat > Bits and Cheering >> Appearance',
title: 'Display a notice on chat messages that include cheers.',
description: 'This feature is intended to prevent possible confusion from chatters using emotes to fake cheers in messages. When enabled, messages that contain real cheers will display a message above them, similar to how resubscription messages or point redemptions with messages function.',
component: 'setting-check-box'
}
});
this.settings.add('chat.rituals.show', {
default: true,
ui: {
path: 'Chat > Filtering > General >> Rituals',
title: 'Display ritual messages such as "User is new here! Say Hello!".',
component: 'setting-check-box'
}
});
this.settings.add('chat.extra-timestamps', {
default: true,
ui: {
path: 'Chat > Appearance >> Chat Lines',
title: 'Display timestamps on notices.',
description: 'When enabled, timestamps will be displayed on point redemptions, subscriptions, etc.',
component: 'setting-check-box'
}
});
this.settings.add('chat.hype.message-style', {
default: 1,
ui: {
path: 'Chat > Hype Chat >> Appearance',
title: 'Hype Chat Style',
component: 'setting-select-box',
description: '**Note**: Hype Chats that include messages will always have their messages displayed, regardless of setting. Changes made to this setting may not affect existing chat messages.',
data: [
{value: 0, title: 'Do Not Display'},
{value: 1, title: 'Standard Twitch (Large, Colored, Limited FFZ Support)'},
{value: 2, title: 'Minimal (Marked with System Message, No Colors)' }
]
}
});
this.settings.add('chat.subs.show', {
default: 3,
ui: {
path: 'Chat > Appearance >> Subscriptions',
title: 'Display Subs in Chat',
component: 'setting-select-box',
description: '**Note**: Messages sent with re-subs will always be displayed. This only controls the special "X subscribed!" message.',
data: [
{value: 0, title: 'Do Not Display'},
{value: 1, title: 'Re-Subs with Messages Only'},
{value: 2, title: 'Re-Subs Only'},
{value: 3, title: 'Display All'}
]
}
});
this.settings.add('chat.subs.compact', {
default: false,
ui: {
path: 'Chat > Appearance >> Subscriptions',
title: 'Display subscription notices in a more compact (classic style) form.',
component: 'setting-check-box'
}
});
this.settings.add('chat.subs.merge-gifts', {
default: 1000,
ui: {
path: 'Chat > Appearance >> Subscriptions',
title: 'Merge Mass Sub Gifts',
component: 'setting-select-box',
data: [
{value: 1000, title: 'Disabled'},
{value: 50, title: 'More than 50'},
{value: 20, title: 'More than 20'},
{value: 10, title: 'More than 10'},
{value: 5, title: 'More than 5'},
{value: 0, title: 'Always'}
],
description: 'Merge mass gift subscriptions into a single message, depending on the quantity.\n**Note:** Only affects newly gifted subs.'
}
});
this.settings.add('chat.subs.merge-gifts-visibility', {
default: false,
ui: {
path: 'Chat > Appearance >> Subscriptions',
title: 'Expand merged mass sub gift messages by default.',
component: 'setting-check-box'
}
});
2017-11-13 01:23:39 -05:00
this.settings.add('chat.lines.alternate', {
default: false,
ui: {
path: 'Chat > Appearance >> Chat Lines',
title: 'Display lines with alternating background colors.',
component: 'setting-check-box'
}
});
this.settings.add('chat.lines.padding', {
default: false,
ui: {
path: 'Chat > Appearance >> Chat Lines',
title: 'Reduce padding around lines.',
component: 'setting-check-box'
}
});
this.settings.add('chat.lines.borders', {
default: 0,
ui: {
path: 'Chat > Appearance >> Chat Lines',
title: 'Separators',
component: 'setting-select-box',
data: [
{value: 0, title: 'Disabled'},
{value: 1, title: 'Basic Line (1px Solid)'},
{value: 2, title: '3D Line (2px Groove)'},
{value: 3, title: '3D Line (2px Groove Inset)'},
{value: 4, title: 'Wide Line (2px Solid)'}
]
}
});
this.settings.add('chat.input.show-mod-view', {
default: true,
ui: {
path: 'Chat > Input >> Appearance',
title: 'Allow the "Mod View" button to appear in relevant channels.',
component: 'setting-check-box'
}
});
this.settings.add('chat.input.show-highlight', {
default: true,
ui: {
path: 'Chat > Input >> Appearance',
title: 'Allow the "Chat Highlight Settings" button to appear in relevant channels.',
component: 'setting-check-box'
}
});
this.settings.add('chat.input.show-shield', {
default: true,
ui: {
path: 'Chat > Input >> Appearance',
title: 'Allow the "Shield Mode" button to appear in relevant channels.',
component: 'setting-check-box'
}
});
this.settings.add('chat.input.show-elevate-your-message', {
default: true,
ui: {
path: 'Chat > Input >> Appearance',
title: 'Allow the "Elevate Your Message" button to be displayed.',
component: 'setting-check-box'
}
});
2017-11-13 01:23:39 -05:00
}
get currentChat() {
for(const inst of this.ChatController.instances)
if ( inst && inst.chatService )
return inst;
return null;
2017-11-13 01:23:39 -05:00
}
updateColors() {
const is_dark = this.chat.context.get('theme.is-dark'),
mode = this.chat.context.get('chat.adjustment-mode'),
contrast = this.chat.context.get('chat.adjustment-contrast'),
c = this.colors,
ic = this.inverse_colors;
2017-11-13 01:23:39 -05:00
let chat_dark = is_dark,
chat_color = Color.RGBA.fromCSS(
this.chat.context.get('theme.color.chat-background') ||
this.chat.context.get('theme.color.background')
);
if ( chat_color )
chat_dark = chat_color.toHSLA().l < 0.5;
chat_color = chat_dark ? '#191919' : '#E0E0E0';
let text_dark = ! chat_dark,
chat_text = Color.RGBA.fromCSS(
this.chat.context.get('theme.color.chat-text') ||
this.chat.context.get('theme.color.text')
);
if ( chat_text )
text_dark = chat_text.toHSLA().l < 0.5;
chat_text = text_dark ? '#19171c' : '#dad8de';
2017-11-13 01:23:39 -05:00
// TODO: Get the background color from the theme system.
// Updated: Use the lightest/darkest colors from alternating rows for better readibility.
c._base = chat_color; // is_dark ? '#191919' : '#e0e0e0'; //#0e0c13' : '#faf9fa';
2017-11-13 01:23:39 -05:00
c.mode = mode;
c.contrast = contrast;
ic._base = chat_text; // is_dark ? '#dad8de' : '#19171c';
ic.mode = mode;
ic.contrast = contrast;
this.chat_line.rerenderLines();
//this.updateChatLines();
this.updateMentionCSS();
this.emit(':update-colors');
2017-11-13 01:23:39 -05:00
}
updateChatCSS() {
if ( ! this._update_css_waiter )
this._update_css_waiter = requestAnimationFrame(() => this._updateChatCSS());
}
_updateChatCSS() {
cancelAnimationFrame(this._update_css_waiter);
this._update_css_waiter = null;
const width = this.chat.context.get('chat.effective-width'),
action_size = this.chat.context.get('chat.actions.size'),
hover_action_size = this.chat.context.get('chat.actions.hover-size'),
ts_size = this.chat.context.get('chat.timestamp-size'),
size = this.chat.context.get('chat.font-size'),
emote_alignment = this.chat.context.get('chat.lines.emote-alignment'),
lh = Math.round((20/12) * size);
const hover_action_icon = Math.round(hover_action_size * (2/3)),
hover_action_padding = hover_action_size - hover_action_icon;
let font = this.chat.context.get('chat.font-family') || 'inherit';
const [processed, unloader] = useFont(font);
font = processed;
if ( this._font_unloader )
this._font_unloader();
this._font_unloader = unloader;
if ( font.indexOf(' ') !== -1 && font.indexOf(',') === -1 && font.indexOf('"') === -1 && font.indexOf("'") === -1 )
font = `"${font}"`;
if ( ts_size )
this.css_tweaks.set('ts-size', `.chat-line__timestamp{font-size:${ts_size/10}rem}`);
else
this.css_tweaks.delete('ts-size');
this.css_tweaks.setVariable('chat-actions-size', `${action_size/10}rem`);
this.css_tweaks.setVariable('chat-actions-hover-size', `${hover_action_icon/10}rem`);
this.css_tweaks.setVariable('chat-actions-hover-padding', `${hover_action_padding/20}rem`);
this.css_tweaks.setVariable('chat-font-size', `${size/10}rem`);
this.css_tweaks.setVariable('chat-line-height', `${lh/10}rem`);
this.css_tweaks.setVariable('chat-font-family', font);
this.css_tweaks.setVariable('chat-width', `${width/10}rem`);
this.css_tweaks.setVariable('negative-chat-width', `${-width/10}rem`);
this.css_tweaks.toggle('chat-font', size !== 13 || font !== 'inherit');
this.css_tweaks.toggle('chat-width', this.settings.get('chat.use-width'));
this.css_tweaks.toggle('chat-fix--watch-party', this.settings.get('context.isWatchParty'));
this.css_tweaks.toggle('emote-alignment-padded', emote_alignment === 1);
this.css_tweaks.toggle('emote-alignment-baseline', emote_alignment === 2);
this.emit(':update-chat-css');
this.emit('site.player:fix-player');
this.emit('site.layout:resize');
2017-11-13 01:23:39 -05:00
}
updateLineBorders() {
const mode = this.chat.context.get('chat.lines.borders');
this.css_tweaks.toggle('chat-borders', mode > 0);
this.css_tweaks.toggle('chat-borders-3d', mode === 2);
this.css_tweaks.toggle('chat-borders-3d-inset', mode === 3);
this.css_tweaks.toggle('chat-borders-wide', mode === 4);
}
updateMentionCSS() {
const enabled = this.chat.context.get('chat.filtering.highlight-mentions');
this.css_tweaks.toggle('chat-mention-token', this.chat.context.get('chat.filtering.highlight-tokens'));
const raw_color = this.chat.context.get('chat.filtering.mention-color');
if ( raw_color ) {
this.css_tweaks.toggle('chat-mention-bg', false);
this.css_tweaks.toggle('chat-mention-bg-alt', false);
this.css_tweaks.toggle('chat-mention-bg-custom', true);
this.css_tweaks.setVariable('chat-mention-color', this.inverse_colors.process(raw_color));
} else {
this.css_tweaks.toggle('chat-mention-bg-custom', false);
this.css_tweaks.toggle('chat-mention-bg', enabled);
this.css_tweaks.toggle('chat-mention-bg-alt', enabled && this.chat.context.get('chat.lines.alternate'));
}
}
2017-11-13 01:23:39 -05:00
updatePointsInfo(inst) {
const icon = inst?.pointIcon,
name = inst?.pointName;
if ( icon ) {
this.css_tweaks.set('points-icon', `.ffz--points-icon:before { display: none }
.ffz--points-icon:after {
display: inline-block;
margin: 0 0.5rem -0.6rem;
background-image: url("${icon.url}");
background-image: ${WEBKIT}image-set(url("${icon.url}") 1x, url("${icon.url2x}") 2x, url("${icon.url4x}") 4x);
}`);
} else
this.css_tweaks.delete('points-icon');
this.point_name = name || null;
}
async grabTypes() {
2021-02-24 14:38:25 -05:00
if ( this.types_loaded )
return;
const ct = await this.web_munch.findModule('chat-types');
2021-02-24 14:38:25 -05:00
this.automod_types = ct?.automod || AUTOMOD_TYPES;
this.chat_types = ct?.chat || CHAT_TYPES;
this.message_types = ct?.message || MESSAGE_TYPES;
this.mod_types = ct?.mod || MOD_TYPES;
if ( ! ct )
return;
2021-02-24 14:38:25 -05:00
this.types_loaded = true;
const changes = [];
if ( ! shallow_object_equals(this.automod_types, AUTOMOD_TYPES) )
changes.push('AUTOMOD_TYPES');
2021-02-24 14:38:25 -05:00
if ( ! shallow_object_equals(this.chat_types, CHAT_TYPES) )
changes.push('CHAT_TYPES');
2021-02-24 14:38:25 -05:00
if ( ! shallow_object_equals(this.message_types, MESSAGE_TYPES) )
changes.push('MESSAGE_TYPES');
2021-02-24 14:38:25 -05:00
if ( ! shallow_object_equals(this.mod_types, MOD_TYPES) )
changes.push('MOD_TYPES');
if ( changes.length )
this.log.info('Chat Types have changed from static mappings for categories:', changes.join(' '));
}
updateDisableHandling() {
this.disable_handling = this.chat.context.get('chat.disable-handling');
}
onEnable() {
this.on('site.web_munch:loaded', this.grabTypes);
this.on('site.web_munch:loaded', this.defineClasses);
this.grabTypes();
this.defineClasses();
this.chat.context.on('changed:chat.points.show-callouts', () => {
this.InlineCallout.forceUpdate();
this.CalloutSelector.forceUpdate();
});
this.chat.context.on('changed:chat.points.show-button', () => this.PointsButton.forceUpdate());
this.chat.context.on('changed:chat.points.show-rewards', () => {
this.PointsButton.forceUpdate();
this.PointsClaimButton.forceUpdate();
});
this.chat.context.on('changed:chat.banners.hype-train', this.cleanHighlights, this);
this.chat.context.on('changed:chat.subs.gift-banner', this.cleanHighlights, this);
this.chat.context.on('changed:chat.banners.polls', this.cleanHighlights, this);
this.chat.context.on('changed:chat.banners.prediction', this.cleanHighlights, this);
this.chat.context.on('changed:chat.banners.drops', this.cleanHighlights, this);
this.chat.context.on('changed:chat.disable-handling', this.updateDisableHandling, this);
this.chat.context.on('changed:chat.banners.charity', () => {
this.ChatContainer.forceUpdate();
});
this.chat.context.on('changed:chat.subs.gift-banner', () => this.GiftBanner.forceUpdate(), this);
this.chat.context.on('changed:chat.effective-width', this.updateChatCSS, this);
this.settings.main_context.on('changed:chat.use-width', this.updateChatCSS, this);
this.chat.context.on('changed:chat.actions.size', this.updateChatCSS, this);
this.chat.context.on('changed:chat.actions.hover-size', this.updateChatCSS, this);
this.chat.context.on('changed:chat.font-size', this.updateChatCSS, this);
this.chat.context.on('changed:chat.timestamp-size', this.updateChatCSS, this);
this.chat.context.on('changed:chat.font-family', this.updateChatCSS, this);
this.chat.context.on('changed:chat.lines.emote-alignment', this.updateChatCSS, this);
2017-11-13 01:23:39 -05:00
this.chat.context.on('changed:chat.adjustment-mode', this.updateColors, this);
this.chat.context.on('changed:chat.adjustment-contrast', this.updateColors, this);
this.chat.context.on('changed:theme.is-dark', this.updateColors, this);
this.chat.context.on('changed:theme.color.background', this.updateColors, this);
this.chat.context.on('changed:theme.color.chat-background', this.updateColors, this);
this.chat.context.on('changed:theme.color.text', this.updateColors, this);
this.chat.context.on('changed:theme.color.chat-text', this.updateColors, this);
2017-11-13 01:23:39 -05:00
this.chat.context.on('changed:chat.lines.borders', this.updateLineBorders, this);
this.chat.context.on('changed:chat.filtering.highlight-mentions', this.updateMentionCSS, this);
this.chat.context.on('changed:chat.filtering.highlight-tokens', this.updateMentionCSS, this);
this.chat.context.on('changed:chat.filtering.mention-color', this.updateMentionCSS, this);
this.chat.context.on('changed:chat.pin-resubs', val => {
if ( val ) {
this.updateInlineCallouts();
this.updatePinnedCallouts();
}
}, this);
2017-11-13 01:23:39 -05:00
this.chat.context.on('changed:chat.community-chest.show', () => {
this.CommunityChestBanner.forceUpdate();
this.CalloutSelector.forceUpdate();
}, this);
this.chat.context.getChanges('chat.emotes.2x', val => {
this.css_tweaks.toggle('big-emoji', val > 1);
this.toggleEmoteJail();
});
this.chat.context.getChanges('chat.emotes.limit-size', () =>
this.toggleEmoteJail());
this.chat.context.getChanges('chat.banners.last-events', val =>
this.css_tweaks.toggleHide('last-x-events', ! val));
this.chat.context.getChanges('chat.input.show-mod-view', val =>
this.css_tweaks.toggleHide('ci-mod-view', ! val));
this.chat.context.getChanges('chat.input.show-highlight', val =>
this.css_tweaks.toggleHide('ci-highlight-settings', !val));
this.chat.context.getChanges('chat.input.show-shield', val =>
this.css_tweaks.toggleHide('ci-shield-mode', ! val));
this.chat.context.getChanges('chat.lines.padding', val =>
2017-11-13 01:23:39 -05:00
this.css_tweaks.toggle('chat-padding', val));
this.chat.context.getChanges('chat.bits.show', val =>
2017-11-13 01:23:39 -05:00
this.css_tweaks.toggle('hide-bits', !val));
this.chat.context.on('changed:chat.bits.show-pinned', () =>
this.ChatLeaderboard.forceUpdate());
2017-11-13 01:23:39 -05:00
this.chat.context.getChanges('chat.filtering.deleted-style', val => {
this.css_tweaks.toggle('chat-deleted-strike', val === 1 || val === 2);
this.css_tweaks.toggle('chat-deleted-fade', val < 2);
});
this.chat.context.getChanges('chat.filtering.clickable-mentions', val =>
this.css_tweaks.toggle('clickable-mentions', val));
this.chat.context.getChanges('chat.filtering.bold-mentions', val =>
this.css_tweaks.toggle('chat-mention-no-bold', ! val));
this.chat.context.getChanges('chat.hide-community-highlights', val =>
this.css_tweaks.toggleHide('community-highlights', val));
this.chat.context.getChanges('chat.lines.alternate', val => {
this.css_tweaks.toggle('chat-rows', val);
this.updateMentionCSS();
});
this.chat.context.getChanges('chat.input.show-elevate-your-message', val =>
this.css_tweaks.toggleHide('elevate-your-message', ! val));
this.updateDisableHandling();
this.updateChatCSS();
2017-11-13 01:23:39 -05:00
this.updateColors();
this.updateLineBorders();
//this.updateMentionCSS();
2017-11-13 01:23:39 -05:00
this.on('chat:get-messages-late', (include_chat, include_whisper, include_video, messages) => {
if ( ! include_chat )
return;
const msg_ids = new Set(messages.map(x => x.message?.id));
for(const inst of this.ChatBuffer.instances) {
for(const msg of inst.buffer) {
const msg_id = msg?.id;
if ( ! msg_id || msg_ids.has(msg_id) || ! msg.ffz_standardized )
continue;
msg_ids.add(msg_id);
messages.push({
message: msg,
_instance: null,
update: () => null
});
}
for(const raw of inst.delayedMessageBuffer) {
const msg = raw?.event,
msg_id = msg?.id;
if ( ! msg_id || msg_ids.has(msg_id) || ! msg.ffz_standardized )
continue;
msg_ids.add(msg_id);
messages.push({
message: msg,
_instance: null,
update: () => null
});
}
}
});
this.on('chat:get-tab-commands', e => {
e.commands.push({
name: 'reconnect',
description: 'Force chat to reconnect.',
permissionLevel: 0,
ffz_group: 'FrankerFaceZ'
})
});
this.on('chat:pre-send-message', e => {
const msg = e.message,
inst = e._inst;
if ( ! /^\/reconnect ?/i.test(msg) )
return;
e.preventDefault();
if ( ! inst.client?.reconnect )
inst.addMessage({
type: t.chat_types.Notice,
message: t.i18n.t('chat.reconnect.unable', 'FFZ is unable to force chat to reconnect.')
});
else {
inst.addMessage({
type: t.chat_types.Notice,
message: t.i18n.t('chat.reconnect', 'FFZ is forcing chat to reconnect...')
});
inst.client.reconnect();
}
});
this.RaidController.on('mount', this.wrapRaidController, this);
this.RaidController.on('update', this.noAutoRaids, this);
this.RaidController.ready((cls, instances) => {
for(const inst of instances)
this.wrapRaidController(inst);
});
this.InlineCallout.on('mount', this.onInlineCallout, this);
this.InlineCallout.on('update', this.onInlineCallout, this);
this.InlineCallout.ready(() => this.updateInlineCallouts());
this.PinnedCallout.on('mount', this.onPinnedCallout, this);
this.PinnedCallout.on('update', this.onPinnedCallout, this);
this.PinnedCallout.ready(() => this.updatePinnedCallouts());
const t = this;
this.PointsInfo.on('mount', this.updatePointsInfo, this);
this.PointsInfo.on('update', this.updatePointsInfo, this);
this.PointsInfo.on('unmount', () => this.updatePointsInfo(null));
this.PointsInfo.ready(() => this.updatePointsInfo(this.PointsInfo.first));
this.ChatLeaderboard.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
if ( ! t.chat.context.get('chat.bits.show-pinned') )
return null;
return old_render.call(this);
}
this.ChatLeaderboard.forceUpdate();
});
this.GiftBanner.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
if ( ! t.chat.context.get('chat.subs.gift-banner') )
return null;
return old_render.call(this);
}
this.GiftBanner.forceUpdate();
});
this.CommunityChestBanner.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
try {
if ( ! t.chat.context.get('chat.community-chest.show') )
return null;
} catch(err) {
t.log.capture(err);
t.log.error(err);
}
return old_render.call(this);
};
this.CommunityChestBanner.forceUpdate();
});
this.InlineCallout.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
try {
const callout = this.props?.event?.callout,
ctype = callout?.trackingType;
if ( ctype === 'community_points_reward' && ! t.chat.context.get('chat.points.show-callouts') )
return null;
if ( ctype === 'prime_gift_bomb' && ! t.chat.context.get('chat.community-chest.show') )
return null;
if ( ctype === 'megacheer_emote_recipient' && ! t.chat.context.get('chat.bits.show-rewards') )
return null;
} catch(err) {
t.log.capture(err);
t.log.error(err);
}
return old_render.call(this);
}
this.InlineCallout.forceUpdate();
});
this.CalloutSelector.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
try {
const callout = this.props.callouts[0] || this.props.pinnedCallout,
ctype = callout?.event?.type;
if ( ctype === 'prime-gift-bomb-gifter' && ! t.chat.context.get('chat.community-chest.show') )
return null;
if ( ctype === 'community-points-rewards' && ! t.chat.context.get('chat.points.show-callouts') )
return null;
if ( (ctype === 'mega-recipient-rewards' || ctype === 'mega-benefactor-rewards') && ! t.chat.context.get('chat.bits.show-rewards') )
return null;
if ( ctype === 'drop' && ! this._ffz_auto_drop && t.chat.context.get('chat.drops.auto-rewards') )
this._ffz_auto_drop = setTimeout(() => {
this._ffz_auto_drop = null;
t.autoClickDrop(this);
}, 0);
} catch(err) {
t.log.capture(err);
t.log.error(err);
}
return old_render.call(this);
}
this.CalloutSelector.forceUpdate();
});
this.PointsButton.ready(cls => {
const old_render = cls.prototype.render;
cls.prototype.render = function() {
try {
if ( ! t.chat.context.get('chat.points.show-button') )
return null;
if ( ! t.chat.context.get('chat.points.show-rewards') ) {
const aq = this.state.animationQueue;
this.state.animationQueue = [];
const out = old_render.call(this);
this.state.animationQueue = aq;
return out;
}
} catch(err) {
t.log.capture(err);
t.log.error(err);
}
return old_render.call(this);
}
this.PointsButton.forceUpdate();
});
this.PointsClaimButton.ready(cls => {
cls.prototype.ffzHasOffer = function() {
return ! this.props.hidden && ! this.state?.error && this.getClaim() != null;
};
const old_render = cls.prototype.render;
cls.prototype.render = function() {
try {
if ( this.ffzHasOffer() && ! this._ffz_timer && t.chat.context.get('chat.points.auto-rewards') )
this._ffz_timer = setTimeout(() => {
this._ffz_timer = null;
if ( this.onClick && this.ffzHasOffer() )
this.onClick();
}, 1000 + Math.floor(Math.random() * 5000));
if ( ! t.chat.context.get('chat.points.show-rewards') )
return null;
} catch(err) {
t.log.capture(err);
t.log.error(err);
}
return old_render.call(this);
}
this.PointsClaimButton.forceUpdate();
});
2017-11-13 01:23:39 -05:00
this.ChatController.on('mount', this.chatMounted, this);
this.ChatController.on('unmount', this.chatUnmounted, this);
//this.ChatController.on('receive-props', this.chatUpdated, this);
this.ChatController.on('update', this.chatUpdated, this);
2017-11-13 01:23:39 -05:00
this.ChatService.ready((cls, instances) => {
this.wrapChatService(cls);
for(const inst of instances) {
inst.client.events.removeAll();
inst._ffzInstall();
const channel = inst.joinedChannel,
state = inst.client?.session?.channelstate?.[`#${channel}`]?.roomState;
if ( state )
this.updateChatState(state);
inst.addEventListeners();
inst.props.setChatConnectionAPI({
sendMessage: inst.sendMessage,
_ffz_inst: inst
});
}
});
this.ChatBuffer.ready((cls, instances) => {
this.wrapChatBuffer(cls);
for(const inst of instances) {
const handler = inst.props.messageHandlerAPI;
if ( handler )
handler.removeMessageHandler(inst.handleMessage);
inst._ffzInstall();
if ( handler )
handler.addMessageHandler(inst.handleMessage);
// We grab this from the chat client now.
/*if ( Array.isArray(inst.buffer) ) {
let i = inst.buffer.length;
const ct = this.chat_types || CHAT_TYPES;
while(i--) {
const msg = inst.buffer[i];
if ( msg && msg.type === ct.RoomState && msg.state ) {
this.updateChatState(msg.state);
break;
}
}
}*/
inst.props.setMessageBufferAPI({
addUpdateHandler: inst.addUpdateHandler,
removeUpdateHandler: inst.removeUpdateHandler,
getUnreadCount: inst.getUnreadCount,
getMessages: inst.getMessages,
getMessagesForHighlight: inst.getMessagesForHighlight,
getMessagesForAllHighlights: inst.getMessagesForAllHighlights,
isPaused: inst.isPaused,
setPaused: inst.setPaused,
hasNewerLeft: inst.hasNewerLeft,
loadNewer: inst.loadNewer,
loadNewest: inst.loadNewest,
_ffz_inst: inst
});
}
});
this.ChatBufferConnector.on('mount', this.connectorMounted, this);
this.ChatBufferConnector.on('update', this.connectorUpdated, this);
this.ChatBufferConnector.on('unmount', this.connectorUnmounted, this);
this.ChatBufferConnector.ready((cls, instances) => {
for(const inst of instances)
this.connectorMounted(inst);
})
2017-11-13 01:23:39 -05:00
this.ChatController.ready((cls, instances) => {
const t = this,
old_catch = cls.prototype.componentDidCatch,
old_render = cls.prototype.render;
// Try catching errors. With any luck, maybe we can
// recover from the error when we re-build?
cls.prototype.componentDidCatch = function(err, info) {
// Don't log infinitely if stuff gets super screwed up.
const errs = this.state.ffz_errors || 0;
if ( errs < 100 ) {
this.setState({ffz_errors: errs + 1});
t.log.info('Error within Chat', err, info, errs);
}
if ( old_catch )
return old_catch.call(this, err, info);
}
cls.prototype.render = function() {
if ( this.state.ffz_errors > 0 ) {
const React = t.site.getReact(),
createElement = React && React.createElement;
if ( ! createElement )
return null;
return createElement('div', {
className: 'tw-border-l tw-c-background-alt-2 tw-c-text-base tw-full-width tw-full-height tw-align-items-center tw-flex tw-flex-column tw-justify-content-center tw-relative'
}, 'There was an error displaying chat.');
} else
return old_render.call(this);
}
for(const inst of instances)
2017-11-13 01:23:39 -05:00
this.chatMounted(inst);
});
this.ChatContainer.on('mount', this.containerMounted, this);
this.ChatContainer.on('unmount', this.containerUnmounted, this); //removeRoom, this);
this.ChatContainer.on('update', this.containerUpdated, this);
2017-11-13 01:23:39 -05:00
this.ChatContainer.ready((cls, instances) => {
const t = this,
old_render = cls.prototype.render,
old_catch = cls.prototype.componentDidCatch;
cls.prototype.ffzRender = function() {
if ( t.chat.context.get('chat.banners.charity') )
return old_render.call(this);
const cd = this.props.campaignData;
this.props.campaignData = null;
let result;
try {
result = old_render.call(this);
} catch(err) {
this.props.campaignData = cd;
throw err;
}
this.props.campaignData = cd;
return result;
}
cls.prototype.render = function() {
try {
if ( t.CommunityStackHandler ) {
const React = t.site.getReact(),
out = this.ffzRender(),
thing = out?.props?.children?.props?.children;
if ( React && Array.isArray(thing) )
thing.push(React.createElement(t.CommunityStackHandler));
return out;
}
} catch(err) {
// No op
}
return this.ffzRender();
}
// Try catching errors. With any luck, maybe we can
// recover from the error when we re-build?
cls.prototype.componentDidCatch = function(err, info) {
// Don't log infinitely if stuff gets super screwed up.
const errs = this.state.ffz_errors || 0;
if ( errs < 100 ) {
this.setState({ffz_errors: errs + 1});
t.log.info('Error within Chat Container', err, info, errs);
}
if ( old_catch )
return old_catch.call(this, err, info);
}
2017-11-13 01:23:39 -05:00
for(const inst of instances)
this.containerMounted(inst);
});
this.subpump.on(':pubsub-message', event => {
if ( event.prefix !== 'community-points-channel-v1' || this.disable_handling )
return;
if ( event.prefix === 'pinned-chat-updates-v1' ) {
this.log.info('Pinned Chat', event);
return;
}
const service = this.ChatService.first,
message = event.message,
data = message?.data?.redemption;
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;
if ( ! this.chat.context.get('chat.filtering.blocked-types').has('ChannelPointsReward') ) {
const msg = {
id: data.id,
type: this.chat_types.Message,
ffz_type: 'points',
ffz_reward: reward,
ffz_reward_highlight: isHighlightedReward(reward),
messageParts: [],
user: {
id: data.user.id,
login: data.user.login,
displayName: data.user.display_name
},
timestamp: new Date(message.data.timestamp || data.redeemed_at).getTime()
};
service.postMessageToCurrentChannel({}, msg);
}
//event.preventDefault();
});
}
2017-11-13 01:23:39 -05:00
autoClickDrop(inst) {
if ( inst._ffz_clicking )
return;
// Check to ensure the active callout is a drop to claim.
const callout = inst.props?.callouts?.[0] || inst.props?.pinnedCallout,
ctype = callout?.event?.type;
if ( ctype !== 'drop' || ! this.chat.context.get('chat.drops.auto-rewards') )
return;
inst._ffz_clicking = true;
// Wait for the button to be added to the DOM.
const waiter = this.resolve('site').awaitElement(
'button[data-a-target="chat-private-callout__primary-button"]',
this.fine.getHostNode(inst),
10000
);
waiter.then(btn => {
inst._ffz_clicking = false;
// Check AGAIN because time has passed.
const callout = inst.props?.callouts?.[0] || inst.props?.pinnedCallout,
ctype = callout?.event?.type;
if ( ctype !== 'drop' || ! this.chat.context.get('chat.drops.auto-rewards') )
return;
btn.click();
}).catch(() => {
inst._ffz_clicking = false;
});
}
wrapRaidController(inst) {
if ( inst._ffz_wrapped )
return this.noAutoRaids(inst);
2017-11-13 01:23:39 -05:00
inst._ffz_wrapped = true;
const t = this,
old_handle_join = inst.handleJoinRaid;
inst.handleJoinRaid = function(event, ...args) {
const raid_id = inst.props && inst.props.raid && inst.props.raid.id;
if ( event && event.type && raid_id )
t.joined_raids.add(raid_id);
return old_handle_join.call(this, event, ...args);
}
this.noAutoRaids(inst);
}
noAutoRaids(inst) {
if ( inst._ffz_no_raid )
return;
inst._ffz_no_raid = setTimeout(() => {
inst._ffz_no_raid = null;
if ( inst.props && inst.props.raid && ! inst.isRaidCreator && inst.hasJoinedCurrentRaid ) {
const id = inst.props.raid.id;
if ( this.joined_raids.has(id) )
return;
let leave = this.settings.get('channel.raids.no-autojoin');
if ( ! leave ) {
const blocked = this.settings.get('__filter:channel.raids.blocked-channels');
if ( blocked && ((inst.props.raid.targetLogin && blocked.test(inst.props.raid.targetLogin)) || (inst.props.raid.targetDisplayName && blocked.test(inst.props.raid.targetDisplayName))) )
leave = true;
if ( ! leave )
return;
}
this.log.info('Automatically leaving raid:', id);
inst.handleLeaveRaid();
}
});
}
toggleEmoteJail() {
const bigger = this.chat.context.get('chat.emotes.2x'),
enabled = this.chat.context.get('chat.emotes.limit-size');
this.css_tweaks.toggle('big-emote-jail', enabled && ! bigger);
this.css_tweaks.toggle('bigger-emote-jail', enabled && bigger);
}
cleanHighlights() {
const types = {
'community_sub_gift': this.chat.context.get('chat.subs.gift-banner'),
'megacheer': this.chat.context.get('chat.bits.show'),
'hype_train': this.chat.context.get('chat.banners.hype-train'),
'prediction': this.chat.context.get('chat.banners.prediction'),
'poll': this.chat.context.get('chat.banners.polls'),
'mw-drop-available': this.chat.context.get('chat.banners.drops')
};
const highlights = this.community_stack?.highlights;
if ( ! Array.isArray(highlights) )
return;
for(const entry of highlights) {
if ( ! entry || ! entry.event || ! entry.id )
continue;
const type = entry.event.type;
if ( type && has(types, type) && ! types[type] ) {
// Attempt to allow Golden Kappa hype trains?
if ( type === 'hype_train' && entry.event.typeDetails === '0' && this.chat.context.get('chat.banners.kappa-train') )
continue;
this.log.info('Removing community highlight: ', type, '#', entry.id);
this.community_dispatch({
type: 'remove-highlight',
id: entry.id
});
}
}
}
defineClasses() {
if ( this.CommunityStackHandler )
return true;
const t = this,
React = this.site.getReact(),
createElement = React?.createElement,
2021-02-24 14:38:25 -05:00
StackMod = this.web_munch.getModule('highlightstack');
2021-02-24 14:38:25 -05:00
if ( ! createElement || ! StackMod )
return false;
this.CommunityStackHandler = function() {
2021-02-24 14:38:25 -05:00
const stack = React.useContext(StackMod.stack),
dispatch = React.useContext(StackMod.dispatch);
t.community_stack = stack;
t.community_dispatch = dispatch;
t.cleanHighlights();
return null;
}
this.ChatContainer.forceUpdate();
}
updateChatState(state) {
const old_state = this.chat.context.get('context.chat_state') || {};
if ( deep_equals(state, old_state) )
return;
this.chat.context.updateContext({
chat_state: state
});
this.input.updateInput();
}
updatePinnedCallouts() {
for(const inst of this.PinnedCallout.instances)
this.onPinnedCallout(inst);
}
onPinnedCallout(inst) {
if ( ! this.chat.context.get('chat.pin-resubs') || inst._ffz_pinned )
return;
const props = inst.props,
event = props && props.event;
if ( props.pinned || ! event || event.type !== 'share-resub' )
return;
this.log.info('Automatically pinning re-sub notice.');
inst._ffz_pinned = true;
inst.pin();
}
updateInlineCallouts() {
for(const inst of this.InlineCallout.instances)
this.onInlineCallout(inst);
}
onInlineCallout(inst) {
if ( ! this.chat.context.get('chat.pin-resubs') || inst._ffz_pinned )
return;
const event = get('props.event.callout', inst);
if ( ! event || event.cta !== 'Share' )
return;
const onPin = get('contextMenuProps.onPin', event);
if ( ! onPin )
return;
this.log.info('Automatically pinning re-sub notice.');
inst._ffz_pinned = true;
if ( inst.hideOnContextMenuAction )
inst.hideOnContextMenuAction(onPin)();
else
onPin();
}
tryUpdateBadges() {
if ( !this._badge_timer )
this._badge_timer = setTimeout(() => this._tryUpdateBadges(), 0);
}
_tryUpdateBadges() {
if ( this._badge_timer )
clearTimeout(this._badge_timer);
this._badge_timer = null;
this.log.info('Trying to update badge data from the chat container.');
const inst = this.ChatContainer.first;
if ( inst )
this.containerUpdated(inst, inst.props);
}
wrapChatBuffer(cls) {
if ( cls.prototype._ffz_was_here )
return;
const t = this,
old_clear = cls.prototype.clear,
old_flush = cls.prototype.flushRawMessages,
old_mount = cls.prototype.componentDidMount;
2017-11-13 01:23:39 -05:00
cls.prototype._ffzInstall = function() {
if ( this._ffz_installed )
return;
this._ffz_installed = true;
const inst = this,
old_handle = inst.handleMessage,
old_set = inst.props.setMessageBufferAPI;
inst.props.setMessageBufferAPI = function(api) {
if ( api )
api._ffz_inst = inst;
return old_set(api);
}
inst.handleMessage = function(msg) {
if ( msg ) {
try {
const types = t.chat_types || {},
mod_types = t.mod_types || {},
blocked_types = t.chat.context.get('chat.filtering.blocked-types');
if ( blocked_types.has(types[msg.type]) )
return;
if ( msg.type === types.ChannelPointsReward )
return;
if ( msg.type === types.RewardGift && ! t.chat.context.get('chat.bits.show-rewards') )
return;
if ( msg.type === types.Message ) {
const m = t.chat.standardizeMessage(msg),
cont = inst._ffz_connector ?? inst.ffzGetConnector();
let room_id = m.roomID = m.roomID ? m.roomID : cont?.props?.channelID;
let room = m.roomLogin = m.roomLogin ? m.roomLogin : m.channel ? m.channel.slice(1) : cont?.props?.channelLogin;
if ( ! room && room_id ) {
const r = t.chat.getRoom(room_id, null, true);
if ( r && r.login )
room = m.roomLogin = r.login;
}
if ( ! room_id && room ) {
const r = t.chat.getRoom(null, room, true);
if ( r && r.id )
room_id = m.roomID = r.id;
}
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
const u = t.site.getUser();
if ( u && cont ) {
u.moderator = cont.props.isCurrentUserModerator;
u.staff = cont.props.isStaff;
}
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
m.ffz_tokens = m.ffz_tokens || t.chat.tokenizeMessage(m, u, true);
if ( m.ffz_removed )
return;
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
if ( t.hasListeners('chat:receive-message') ) {
const event = t.makeEvent({
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
message: m,
inst,
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
channel: room,
channelID: room_id
});
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
t.emit('chat:receive-message', event);
if ( event.defaultPrevented || m.ffz_removed )
return;
}
} /*else if ( msg.type === types.ModerationAction ) {
t.emit('chat:mod-user', msg.moderationActionType, )
} */ else if ( msg.type === types.Moderation ) {
t.emit('chat:mod-user', msg.moderationType, msg.userLogin, msg.targetMessageID, msg);
// Special handling
if ( ! inst.props.isCurrentUserModerator ) {
const type = msg.moderationType,
target = msg.userLogin;
// Whee~
let mat;
if ( type === mod_types.Ban )
mat = 'ban';
else if ( type === mod_types.Timeout )
mat = 'timeout';
else if ( type === mod_types.Delete )
mat = 'delete';
if ( mat )
msg.moderationActionType = mat;
// Handle moderation events ourself if it's not
// a delete, so that we can pass the action info.
if ( ! inst.moderatedUsers.has(target) && type !== mod_types.Delete ) {
inst.moderateBuffers(
[
inst.buffer,
inst.delayedMessageBuffer.map(e => e.event)
],
target,
msg
);
inst.delayedMessageBuffer.push({
event: msg,
time: Date.now(),
shouldDelay: false
});
return;
}
}
} /*else if ( msg.type === types.ModerationAction && false && inst.markUserEventDeleted && inst.unsetModeratedUser ) {
if ( !((! msg.level || ! msg.level.length) && msg.targetUserLogin && msg.targetUserLogin === inst.props.currentUserLogin) ) {
//t.log.info('Moderation Action', msg);
if ( ! inst.props.isCurrentUserModerator )
return;
const mod_action = msg.moderationActionType;
if ( mod_action === 'ban' || mod_action === 'timeout' || mod_action === 'delete' ) {
const user = msg.targetUserLogin;
if ( inst.moderatedUsers.has(user) )
return;
const do_remove = t.chat.context.get('chat.filtering.remove-deleted') === 3;
if ( do_remove ) {
const len = inst.buffer.length,
target_id = msg.messageID;
inst.buffer = inst.buffer.filter(m =>
m.type !== types.Message || ! m.user || m.user.userLogin !== user ||
(target_id && m.id !== target_id)
);
if ( len !== inst.buffer.length && ! inst.props.isBackground )
inst.notifySubscribers();
inst.moderateBuffers([
inst.delayedMessageBuffer.map(e => e.event)
], user, msg);
} else
inst.moderateBuffers([
inst.buffer,
inst.delayedMessageBuffer.map(e => e.event)
], user, msg);
inst.delayedMessageBuffer.push({
event: msg,
time: Date.now(),
shouldDelay: false
});
return;
}
}
} else if ( msg.type === types.Moderation && false && inst.unsetModeratedUser ) {
//t.log.info('Moderation', msg);
if ( inst.props.isCurrentUserModerator )
return;
const user = msg.userLogin;
if ( inst.moderatedUsers.has(user) )
return;
const mod_action = msg.moderationType;
let new_action;
if ( mod_action === mod_types.Ban )
new_action = 'ban';
else if ( mod_action === mod_types.Delete )
new_action = 'delete';
else if ( mod_action === mod_types.Unban )
new_action = 'unban';
else if ( mod_action === mod_types.Timeout )
new_action = 'timeout';
if ( new_action )
msg.moderationActionType = new_action;
const do_remove = t.chat.context.get('chat.filtering.remove-deleted') === 3;
if ( do_remove ) {
const len = inst.buffer.length,
target_id = msg.targetMessageID;
inst.buffer = inst.buffer.filter(m =>
m.type !== types.Message || ! m.user || m.user.userLogin !== user ||
(target_id && m.id !== target_id)
);
if ( len !== inst.buffer.length && ! inst.props.isBackground )
inst.notifySubscribers();
inst.moderateBuffers([
inst.delayedMessageBuffer.map(e => e.event)
], user, msg);
} else
inst.moderateBuffers([
inst.buffer,
inst.delayedMessageBuffer.map(e => e.event)
], user, msg);
inst.delayedMessageBuffer.push({
event: msg,
time: Date.now(),
shouldDelay: false
});
return;
} */ else if ( msg.type === types.Clear ) {
if ( t.chat.context.get('chat.filtering.ignore-clear') )
msg = {
type: types.Info,
message: t.i18n.t('chat.ignore-clear', 'An attempt by a moderator to clear chat was ignored.')
}
else
t.emit('chat:clear-chat', msg);
}
} catch(err) {
t.log.error('Error processing chat event.', err);
t.log.capture(err, {extra: {msg}});
}
}
return old_handle.call(inst, msg);
}
/*inst.ffzModerateBuffer = function(buffers, event) {
const mod_types = t.mod_types || {},
ctypes = t.chat_types || {},
mod_type = event.moderationActionType,
user_login = event.targetUserLogin || event.userLogin,
mod_login = event.createdByLogin,
target_id = event.targetMessageID || event.messageID;
let deleted_count = 0, last_msg;
const is_delete = mod_type === mod_types.Delete,
updater = m => {
if ( m.event )
m = m.event;
/*if ( m.message && m.type in [ctypes.ChannelPointsReward, ctypes.Resubscription, ctypes.Ritual] )
m = m.message;/
if ( m.reply ) {
if ( target_id ? target_id === m.reply.parentMsgId : (user_login && user_login === m.reply.parentUserLogin) )
m.reply = {
...m.reply,
parentDeleted: true
}
}
if ( ! user_login || ! m.user || user_login !== m.user.userLogin || ! m.messageParts )
return;
if ( ! m || m.deleted )
return;
if ( target_id && m.id !== target_id )
return;
m.deleted = true;
m.banned = mod_type === mod_types.Ban;
last_msg = m;
deleted_count++;
m.modLogin = mod_login;
m.modActionType = mod_type;
m.duration = event.duration;
};
for(const buffer of buffers)
if ( buffer.some(updater) )
break;
//t.log.info('Moderate Buffer', mod_type, user_login, mod_login, target_id, deleted_count, last_msg);
if ( last_msg )
last_msg.deletedCount = deleted_count;
}*/
inst.setPaused = function(paused) {
if ( inst.paused === paused )
return;
inst.paused = paused;
if ( ! paused ) {
inst.slidingWindowEnd = Math.min(inst.buffer.length, t.chat.context.get('chat.scrollback-length'));
if ( ! inst.props.isBackground )
inst.notifySubscribers();
}
}
inst.loadNewer = function() {
if ( ! inst.hasNewerLeft() )
return;
const end = Math.min(inst.buffer.length, inst.slidingWindowEnd + 40),
start = Math.max(0, end - t.chat.context.get('chat.scrollback-length'));
inst.clear(inst.buffer.length - start);
inst.slidingWindowEnd = end - start;
if ( ! inst.props.isBackground )
inst.notifySubscribers();
}
inst.loadNewest = function() {
if ( ! inst.hasNewerLeft() )
return;
const max_size = t.chat.context.get('chat.scrollback-length');
inst.clear(max_size);
inst.slidingWindowEnd = Math.min(max_size, inst.buffer.length);
if ( ! inst.props.isBackground )
inst.notifySubscribers();
}
inst.getMessages = function() {
return inst.buffer.slice(0, inst.slidingWindowEnd + (inst.ffz_extra || 0));
}
}
cls.prototype.componentDidMount = function() {
try {
this._ffzInstall();
} catch(err) {
t.log.error('Error installing FFZ features onto chat buffer.', err);
}
return old_mount.call(this);
}
cls.prototype.clear = function(count) {
try {
if ( count == null )
count = 0;
const max_size = t.chat.context.get('chat.scrollback-length');
if ( ! this.isPaused() && count > max_size )
count = max_size;
if ( count <= 0 ) {
this.ffz_extra = 0;
this.buffer = [];
this.delayedMessageBuffer = [];
this.paused = false;
} else {
const buffer = this.buffer,
ct = t.chat_types || CHAT_TYPES,
target = buffer.length - count;
if ( target > 0 ) {
let removed = 0, last;
for(let i=0; i < target; i++)
if ( buffer[i] && ! NULL_TYPES.includes(ct[buffer[i].type]) ) {
removed++;
last = i;
}
// When we remove less then expected, we want to keep track
// of that so we can return the extra messages from getMessages.
this.buffer = buffer.slice(removed % 2 !== 0 ? Math.max(target - 4, last) : target);
this.ffz_extra = buffer.length - count;
} else {
this.ffz_extra = 0;
this.buffer = this.buffer.slice(0);
}
if ( this.paused && this.buffer.length >= 900 )
this.setPaused(false);
}
} catch(err) {
t.log.error('Error running clear', err);
return old_clear.call(this, count);
}
}
cls.prototype.ffzGetConnector = function() {
if ( this._ffz_connector )
return this._ffz_connector;
const now = Date.now();
if ( now - (this._ffz_connect_tried || 0) > 5000 ) {
this._ffz_connect_tried = now;
const thing = t.ChatBufferConnector.first;
if ( thing?.props?.messageBufferAPI?._ffz_inst === this )
return this._ffz_connector = thing;
}
}
cls.prototype.flushRawMessages = function() {
try {
const out = [],
ct = t.chat_types || CHAT_TYPES,
now = Date.now(),
raw_delay = t.chat.context.get('chat.delay'),
delay = raw_delay === -1 ? this.delayDuration : raw_delay,
first = now - delay,
see_deleted = this.shouldSeeBlockedAndDeletedMessages || this.props && this.props.shouldSeeBlockedAndDeletedMessages,
has_newer = this.hasNewerLeft(),
paused = this.isPaused(),
max_size = t.chat.context.get('chat.scrollback-length'),
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
do_remove = t.chat.context.get('chat.filtering.remove-deleted'),
want_event = t.hasListeners('chat:buffer-message');
let added = 0,
buffered = this.slidingWindowEnd,
changed = false,
event;
for(const msg of this.delayedMessageBuffer) {
if ( msg.time <= first || ! msg.shouldDelay ) {
if ( do_remove !== 0 && (do_remove > 1 || ! see_deleted) && this.isDeletable(msg.event) && msg.event.deleted )
continue;
if ( want_event ) {
if ( ! event ) {
event = t.makeEvent({
inst: this,
channel: undefined,
channelID: undefined,
message: undefined
});
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
const cont = this._ffz_connector ?? this.ffzGetConnector(),
room_id = cont && cont.props.channelID;
event.channelID = room_id;
if ( room_id ) {
const r = t.chat.getRoom(room_id, null, true);
if ( r && r.login )
event.channel = r.login;
}
} else
event._reset();
event.message = msg.event;
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
t.emit('chat:buffer-message', event);
if ( event.defaultPrevented || msg.event.ffz_removed )
continue;
}
const last = this.buffer[this.buffer.length - 1],
type = last?.type;
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
if ( !(
! this.props.isLoadingHistoricalMessages &&
! this.props.historicalMessages ||
type !== ct.Connected ||
msg.event.type === ct.Connected ||
this.buffer.find(e => e.type === ct.LiveMessageSeparator)
) )
this.buffer.push({
type: ct.LiveMessageSeparator,
id: 'live-message-separator'
});
/*if ( type === ct.Connected ) {
const non_null = this.buffer.filter(x => x && ct[x.type] && ! NULL_TYPES.includes(ct[x.type]));
if ( non_null.length > 1 )
this.buffer.push({
type: ct.LiveMessageSeparator,
id: 'live-message-separator'
});
4.22.0 * Added: Highlight rules now have priorities. Priorities allow for finer control over which color is used to highlight a message, as well as giving priority to specific rules when highlighting individual words. * Added: Setting to set a priority for the built-in highlighting when someone mentions you by name. * Changed: The `chat:receive-message` event no longer fires if there are no registered event listeners, to improve performance. * Changed: Attempt to more accurately display re-sub notices in Chat on Videos by parsing out the re-sub message from the start of the message. * Changed: Apply the user's selected `Action Style` to Chat on Videos. * Fixed: Messages being re-tokenized incorrectly sometimes due to an incorrect flag being passed by the chat line component. * Fixed: Update the logic for inserting a "Live Message Separator" to duplicate the logic used by Twitch. * API Added: `chat:buffer-message` event that is fired when a chat message is copied into the buffer of visible messages. If you wish to run an event when a chat message becomes visible to the user, this is what you want to use. * API Added: `FFZEvent` instances now have a `_reset()` method for resetting their defaultPrevented / propagationStopped state. For use by the constructing method only. This is intended for allowing the re-use of FFZEvent instances when firing many events in succession. * API Changed: The various settings used for applying filters during message tokenization have been renamed to start with `__filter:`
2021-04-30 17:38:49 -04:00
}*/
this.buffer.push(msg.event);
changed = true;
if ( ! this.paused ) {
if ( this.buffer.length > max_size )
added++;
else
buffered++;
}
} else
out.push(msg);
}
this.delayedMessageBuffer = out;
if ( changed ) {
this.clear(Math.min(900, this.buffer.length - added));
if ( !(added === 0 && buffered === this.slidingWindowEnd && has_newer === this.hasNewerLeft() && paused === this.isPaused()) ) {
this.slidingWindowEnd = buffered;
if ( ! this.props.isBackground )
this.notifySubscribers();
}
}
if ( this.flushHighlightsBuffer )
this.flushHighlightsBuffer();
} catch(err) {
t.log.error('Error running flush.', err);
return old_flush.call(this);
}
2017-11-13 01:23:39 -05:00
}
}
addNotice(room, message) {
if ( ! room )
return false;
if ( room.startsWith('#') )
room = room.slice(1);
room = room.toLowerCase();
for(const inst of this.ChatService.instances) {
if ( room === '*' || inst.props.channelLogin.toLowerCase() === room ) {
inst.addMessage({
type: this.chat_types.Notice,
message
});
return true;
}
}
return false;
}
sendMessage(room, message) {
const service = this.ChatService.first;
if ( ! service || ! room )
return null;
if ( room.startsWith('#') )
room = room.slice(1);
if ( room.toLowerCase() !== service.props.channelLogin.toLowerCase() )
return service.client.sendCommand(room, message);
service.sendMessage(message);
}
scheduleMystery(mystery) { // eslint-disable-line class-methods-use-this
if ( ! mystery.line )
return;
if ( mystery._timer )
return;
mystery._timer = setTimeout(() => requestAnimationFrame(() => {
mystery._timer = null;
if ( mystery.line )
mystery.line.forceUpdate();
}), 250);
}
2017-11-13 01:23:39 -05:00
wrapChatService(cls) {
const t = this,
old_mount = cls.prototype.componentDidMount,
old_handler = cls.prototype.addEventListeners;
2017-11-13 01:23:39 -05:00
cls.prototype._ffz_was_here = true;
cls.prototype._ffzInstall = function() {
if ( this._ffz_installed )
return;
this._ffz_installed = true;
const inst = this,
old_send = this.sendMessage,
addMessage = (...args) => inst.addMessage(...args),
sendMessage = (msg, extra) => inst.sendMessage(msg, extra);
inst.sendMessage = function(msg, extra) {
msg = msg.replace(/\s+/g, ' ');
if ( msg.startsWith('/ffz:') ) {
msg = msg.slice(5).trim();
const idx = msg.indexOf(' ');
let subcmd;
if ( idx === -1 ) {
subcmd = msg;
msg = '';
} else {
subcmd = msg.slice(0, idx);
msg = msg.slice(idx + 1).trimStart();
}
const event = t.makeEvent({
command: subcmd,
message: msg,
extra,
context: t.chat.context,
channel: inst.props.channelLogin,
_inst: inst,
addMessage,
sendMessage
});
const topic = `chat:ffz-command:${subcmd}`,
listeners = t.listeners(topic);
if ( listeners?.length > 0 )
t.emit(topic, event);
else
inst.addMessage({
type: t.chat_types.Notice,
message: t.i18n.t('chat.ffz-command.invalid', 'No such command: /ffz:{subcmd}', {subcmd})
});
return false;
}
const event = t.makeEvent({
message: msg,
extra,
context: t.chat.context,
channel: inst.props.channelLogin,
_inst: inst,
addMessage,
sendMessage
});
t.emit('chat:pre-send-message', event);
if ( event.defaultPrevented )
return;
return old_send.call(this, event.message, event.extra);
}
}
cls.prototype.componentDidMount = function() {
try {
this._ffzInstall();
} catch(err) {
t.log.error('Error installing FFZ features onto chat service.', err);
}
return old_mount.call(this);
}
cls.prototype.addEventListeners = function(...args) {
2017-11-13 01:23:39 -05:00
if ( ! this._ffz_init ) {
const i = this;
2017-11-13 01:23:39 -05:00
for(const key of MISBEHAVING_EVENTS) {
2017-11-13 01:23:39 -05:00
const original = this[key];
if ( original )
this[key] = function(e, t) {
i._wrapped = e;
const ret = original.call(i, e, t);
i._wrapped = null;
return ret;
}
}
const old_announce = this.onAnnouncementEvent;
this.onAnnouncementEvent = function(e) {
//console.log('announcement', e);
return old_announce.call(this, e);
}
const old_sub = this.onSubscriptionEvent;
this.onSubscriptionEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Subscription') )
return;
if ( t.disable_handling )
return old_sub.call(i, e);
if ( t.chat.context.get('chat.subs.show') < 3 )
return;
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'resub';
out.gift_theme = e.giftTheme;
out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null;
out.sub_plan = e.methods;
out.sub_multi = e.multiMonthData?.multiMonthDuration ? {
count: e.multiMonthData.multiMonthDuration,
tenure: e.multiMonthData.multiMonthTenure
} : null;
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_sub.call(i, e);
}
}
const old_state = this.onRoomStateEvent;
this.onRoomStateEvent = function(e) {
try {
const channel = e.channel,
current = t.chat.context.get('context.channel');
if ( channel && (channel === current || channel === `#${current}`) )
t.updateChatState(e.state);
} catch(err) {
t.log.capture(err, {extra: e});
}
return old_state.call(i, e);
}
const old_pinned = this.onPinnedChatEvent;
this.onPinnedChatEvent = function(e) {
try {
const setting = t.chat.context.get('chat.hype.message-style');
if ( setting !== 1 ) {
// Drop messages with no message if we're not displaying them.
if ( e.isSystemMessage && setting === 0 )
return;
const out = i.convertMessage(e);
out.ffz_type = 'hype';
out.hype_amount = e.amount;
out.hype_canonical_amount = e.canonical_amount;
out.hype_currency = e.currency;
out.hype_exponent = e.exponent;
out.hype_level = e.level;
if ( e.isSystemMessage ) {
// Delete the message it comes with.
out.message = '';
out.messageBody = '';
out.messageParts = [];
e.message.body = '';
}
//t.log.info('Pinned Event', e, out);
return i.postMessageToCurrentChannel(e, out);
}
} catch(err) {
t.log.capture(err, {extra: e});
}
return old_pinned.call(i, e);
}
const old_resub = this.onResubscriptionEvent;
this.onResubscriptionEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Resubscription') )
return;
if ( t.disable_handling )
return old_resub.call(i, e);
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
return;
const out = i.convertMessage({message: e});
out.ffz_type = 'resub';
out.gift_theme = e.giftTheme;
out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null;
out.sub_cumulative = e.cumulativeMonths || 0;
out.sub_streak = e.streakMonths || 0;
out.sub_share_streak = e.shouldShareStreakTenure;
out.sub_months = e.months;
out.sub_plan = e.methods;
out.sub_multi = e.multiMonthData?.multiMonthDuration ? {
count: e.multiMonthData.multiMonthDuration,
tenure: e.multiMonthData.multiMonthTenure
} : null;
//t.log.info('Resub Event', e, out);
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_resub.call(i, e);
}
}
const mysteries = this.ffz_mysteries = {};
const old_subgift = this.onSubscriptionGiftEvent;
this.onSubscriptionGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubGift') )
return;
if ( t.disable_handling )
return old_subgift.call(i, e);
const key = `${e.channel}:${e.user.userID}`,
mystery = mysteries[key];
if ( mystery ) {
if ( mystery.expires < Date.now() ) {
mysteries[key] = null;
} else {
mystery.recipients.push({
id: e.recipientID,
login: e.recipientLogin,
displayName: e.recipientName
});
if( mystery.recipients.length >= mystery.size )
mysteries[key] = null;
if ( mystery.line )
t.scheduleMystery(mystery);
return;
}
}
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'sub_gift';
out.sub_recipient = {
id: e.recipientID,
login: e.recipientLogin,
displayName: e.recipientName
};
out.gift_theme = e.giftTheme;
out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null;
out.sub_months = e.giftMonths;
out.sub_plan = e.methods;
out.sub_total = e.senderCount;
//t.log.info('Sub Gift', e, out);
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_subgift.call(i, e);
}
}
const old_communityintro = this.onCommunityIntroductionEvent;
this.onCommunityIntroductionEvent = function(e) {
try {
if ( t.disable_handling )
return old_communityintro.call(this, e);
if ( t.chat.context.get('chat.filtering.blocked-types').has('CommunityIntroduction') ) {
const out = i.convertMessage(e);
return i.postMessageToCurrentChannel(e, out);
}
} catch(err) {
t.log.capture(err, {extra: e});
t.log.error(err);
}
return old_communityintro.call(this, e);
}
const old_anonsubgift = this.onAnonSubscriptionGiftEvent;
this.onAnonSubscriptionGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubGift') )
return;
if ( t.disable_handling )
return old_anonsubgift.call(i, e);
const key = `${e.channel}:ANON`,
mystery = mysteries[key];
if ( mystery ) {
if ( mystery.expires < Date.now() )
mysteries[key] = null;
else {
mystery.recipients.push({
id: e.recipientID,
login: e.recipientLogin,
displayName: e.recipientName
});
if( mystery.recipients.length >= mystery.size )
mysteries[key] = null;
if ( mystery.line )
t.scheduleMystery(mystery);
return;
}
}
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'sub_gift';
out.gift_theme = e.giftTheme;
out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null;
out.sub_anon = true;
out.sub_recipient = {
id: e.recipientID,
login: e.recipientLogin,
displayName: e.recipientName
};
out.sub_months = e.giftMonths;
out.sub_plan = e.methods;
out.sub_total = e.senderCount;
//t.log.info('Anon Sub Gift', e, out);
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_anonsubgift.call(i, e);
}
}
const old_submystery = this.onSubscriptionMysteryGiftEvent;
this.onSubscriptionMysteryGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('SubMysteryGift') )
return;
if ( t.disable_handling )
return old_submystery.call(i, e);
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:${e.user.userID}`;
mystery = mysteries[key] = {
recipients: [],
size: e.massGiftCount,
expires: Date.now() + 30000
};
}
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'sub_mystery';
out.gift_theme = e.giftTheme;
out.sub_goal = i.getGoalData ? i.getGoalData(e.goalData) : null;
out.mystery = mystery;
out.sub_plan = e.plan;
out.sub_count = e.massGiftCount;
out.sub_total = e.senderCount;
//t.log.info('Sub Mystery', e, out);
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_submystery.call(i, e);
}
}
const old_anonsubmystery = this.onAnonSubscriptionMysteryGiftEvent;
this.onAnonSubscriptionMysteryGiftEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('AnonSubMysteryGift') )
return;
if ( t.disable_handling )
return old_anonsubmystery.call(i, e);
let mystery = null;
if ( e.massGiftCount > t.chat.context.get('chat.subs.merge-gifts') ) {
const key = `${e.channel}:ANON`;
mystery = mysteries[key] = {
recipients: [],
size: e.massGiftCount,
expires: Date.now() + 30000
};
}
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'sub_mystery';
out.sub_anon = true;
out.mystery = mystery;
out.sub_plan = e.plan;
out.sub_count = e.massGiftCount;
out.sub_total = e.senderCount;
//t.log.info('Anon Sub Mystery', e, out);
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_anonsubmystery.call(i, e);
}
}
const old_ritual = this.onRitualEvent;
this.onRitualEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('Ritual') )
return;
if ( t.disable_handling )
return old_ritual.call(i, e);
const out = i.convertMessage(e);
out.ffz_type = 'ritual';
out.ritual = e.type;
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_ritual.call(i, e);
}
}
const old_points = this.onChannelPointsRewardEvent;
this.onChannelPointsRewardEvent = function(e) {
try {
if ( t.chat.context.get('chat.filtering.blocked-types').has('ChannelPointsReward') )
return;
if ( t.disable_handling )
return old_points.call(i, e);
const reward = e.rewardID && get(e.rewardID, i.props.rewardMap);
if ( reward ) {
const out = i.convertMessage(e);
out.ffz_type = 'points';
out.ffz_reward = reward;
out.ffz_reward_highlight = isHighlightedReward(reward);
return i.postMessageToCurrentChannel(e, out);
}
} catch(err) {
t.log.error(err);
t.log.capture(err, {extra: e});
}
return old_points.call(i, e);
}
/*const old_host = this.onHostingEvent;
this.onHostingEvent = function (e, _t) {
t.emit('tmi:host', e, _t);
return old_host.call(i, e, _t);
}
const old_unhost = this.onUnhostEvent;
this.onUnhostEvent = function (e, _t) {
t.emit('tmi:unhost', e, _t);
return old_unhost.call(i, e, _t);
}*/
const old_add = this.addMessage;
this.addMessage = function(e) {
const original = i._wrapped;
if ( original && ! e._ffz_checked )
return i.postMessageToCurrentChannel(original, e);
2017-11-13 01:23:39 -05:00
return old_add.call(i, e);
2017-11-13 01:23:39 -05:00
}
this._ffz_init = true;
}
return old_handler.apply(this, ...args);
}
cls.prototype.postMessageToCurrentChannel = function(original, message) {
const original_msg = message;
message._ffz_checked = true;
// For certain message types, the message is contained within
// a message sub-object.
if ( message.type === t.chat_types.ChannelPointsReward || message.type === t.chat_types.CommunityIntroduction || (message.message?.user & message.message?.badgeDynamicData) ) {
message = message.message;
}
if ( original.channel ) {
let chan = message.channel = original.channel.toLowerCase();
if ( chan.startsWith('#') )
chan = chan.slice(1);
if ( chan !== this.props.channelLogin.toLowerCase() )
return;
message.roomLogin = chan;
}
message.ffz_first_msg = message.isFirstMsg || original.message?.isFirstMsg || false;
message.ffz_returning = original.message?.isReturningChatter || false;
if ( original.message ) {
const user = original.message.user,
flags = original.message.flags;
if ( user )
message.emotes = user.emotes;
if ( flags && this.getFilterFlagOptions ) {
const clear_mod = this.props.isCurrentUserModerator && t.chat.context.get('chat.automod.run-as-mod');
if ( clear_mod )
this.props.isCurrentUserModerator = false;
message.flags = this.getFilterFlagOptions(flags);
if ( clear_mod )
this.props.isCurrentUserModerator = true;
}
if (! message.message || typeof message.message === 'string') {
if ( typeof original.action === 'string' )
message.message = original.action;
else
message.message = original.message.body;
}
}
this.addMessage(original_msg);
}
2017-11-13 01:23:39 -05:00
}
/*updateChatLines() {
this.chat_line.updateLines();
}*/
2017-11-13 01:23:39 -05:00
// ========================================================================
// Room Handling
// ========================================================================
addRoom(thing, props) {
if ( ! props )
props = thing.props;
if ( ! props.channelID )
return null;
const room = thing._ffz_room = this.chat.getRoom(props.channelID, props.channelLogin && props.channelLogin.toLowerCase(), false, true);
2017-11-13 01:23:39 -05:00
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;
}
// ========================================================================
// Chat Controller
// ========================================================================
chatMounted(chat, props) {
if ( chat.chatBuffer )
chat.chatBuffer.ffzController = chat;
2017-11-13 01:23:39 -05:00
if ( ! props )
props = chat.props;
if ( ! this.addRoom(chat, props) )
return;
this.updateRoomBitsConfig(chat, props.bitsConfig);
// TODO: Check if this is the room for the current channel.
this.settings.updateContext({
moderator: props.isCurrentUserModerator,
chatHidden: props.isHidden
});
if ( props.isEmbedded || props.isPopout )
this.settings.updateContext({
channel: props.channelLogin && props.channelLogin.toLowerCase(),
channelID: props.channelID
});
this.chat.context.updateContext({
moderator: props.isCurrentUserModerator,
channel: props.channelLogin && props.channelLogin.toLowerCase(),
channelID: props.channelID,
/*ui: {
theme: props.theme
}*/
});
2017-11-13 01:23:39 -05:00
}
chatUnmounted(chat) {
if ( chat.chatBuffer && chat.chatBuffer.ffzController === this )
chat.chatBuffer.ffzController = null;
if ( chat.props.isEmbedded || chat.props.isPopout )
this.settings.updateContext({
channel: null,
channelID: null
});
this.settings.updateContext({
moderator: false,
chatHidden: false
});
this.chat.context.updateContext({
moderator: false,
channel: null,
channelID: null
});
this.removeRoom(chat);
}
2017-11-13 01:23:39 -05:00
chatUpdated(chat, props) {
if ( chat.chatBuffer )
chat.chatBuffer.ffzController = chat;
if ( ! chat._ffz_room || props.channelID != chat._ffz_room.id ) {
2017-11-13 01:23:39 -05:00
this.removeRoom(chat);
if ( chat._ffz_mounted )
this.chatMounted(chat);
2017-11-13 01:23:39 -05:00
return;
}
if ( props.bitsConfig !== chat.props.bitsConfig )
this.updateRoomBitsConfig(chat, chat.props.bitsConfig);
2017-11-13 01:23:39 -05:00
// TODO: Check if this is the room for the current channel.
let login = chat.props.channelLogin;
if ( login )
login = login.toLowerCase();
if ( chat.props.isEmbedded || chat.props.isPopout )
this.settings.updateContext({
channel: login,
channelID: chat.props.channelID
});
2017-11-13 01:23:39 -05:00
this.settings.updateContext({
moderator: chat.props.isCurrentUserModerator,
chatHidden: chat.props.isHidden
2017-11-13 01:23:39 -05:00
});
this.chat.context.updateContext({
moderator: chat.props.isCurrentUserModerator,
channel: login,
channelID: chat.props.channelID,
/*ui: {
2017-11-13 01:23:39 -05:00
theme: props.theme
}*/
2017-11-13 01:23:39 -05:00
});
}
updateRoomBitsConfig(chat, config) { // eslint-disable-line class-methods-use-this
const room = chat._ffz_room;
if ( ! room )
return;
// We have to check that the available cheers haven't changed
// to avoid doing too many recalculations.
let new_bits = null;
if ( config && Array.isArray(config.orderedActions) ) {
new_bits = new Set;
for(const action of config.orderedActions)
if ( action && action.prefix )
new_bits.add(action.prefix);
}
if ( (! this._ffz_old_bits && ! new_bits) || set_equals(this._ffz_old_bits, new_bits) )
return;
this._ffz_old_bits = new_bits;
2017-11-13 01:23:39 -05:00
room.updateBitsConfig(formatBitsConfig(config));
this.chat_line.updateLineTokens();
//this.updateChatLines();
2017-11-13 01:23:39 -05:00
}
// ========================================================================
// Chat Buffer Connector
// ========================================================================
connectorMounted(inst) { // eslint-disable-line class-methods-use-this
const buffer = inst.props.messageBufferAPI;
if ( buffer && buffer._ffz_inst && buffer._ffz_inst._ffz_connector !== inst )
buffer._ffz_inst._ffz_connector = inst;
}
connectorUpdated(inst, props) { // eslint-disable-line class-methods-use-this
const buffer = props.messageBufferAPI,
new_buffer = inst.props.messageBufferAPI;
if ( buffer === new_buffer )
return;
if ( buffer && buffer._ffz_inst && buffer._ffz_inst._ffz_connector === inst )
buffer._ffz_inst._ffz_connector = null;
if ( new_buffer && new_buffer._ffz_inst && new_buffer._ffz_inst._ffz_connector !== inst )
buffer._ffz_inst._ffz_connector = inst;
}
connectorUnmounted(inst) { // eslint-disable-line class-methods-use-this
const buffer = inst.props.messageBufferAPI;
if ( buffer && buffer._ffz_inst && buffer._ffz_inst._ffz_connector === inst )
buffer._ffz_inst._ffz_connector = null;
}
2017-11-13 01:23:39 -05:00
// ========================================================================
// Chat Containers
// ========================================================================
get shouldUpdateChannel() {
const route = this.router.current_name;
return Twilight.POPOUT_ROUTES.includes(route) || Twilight.SUNLIGHT_ROUTES.includes(route);
}
2017-11-13 01:23:39 -05:00
containerMounted(cont, props) {
if ( ! props )
props = cont.props;
if ( ! this.addRoom(cont, props) )
return;
this.updateRoomBitsConfig(cont, props.bitsConfig);
if ( props.data ) {
if ( this.shouldUpdateChannel ){
const color = props.data.user?.primaryColorHex;
this.resolve('site.channel').updateChannelColor(color);
this.settings.updateContext({
channel: props.channelLogin,
channelID: props.channelID,
channelColor: color
});
}
this.chat.badges.updateTwitchBadges(props.data.badges);
this.updateRoomBadges(cont, props.data.user && props.data.user.broadcastBadges);
this.updateRoomRules(cont, props.chatRules);
2017-11-13 01:23:39 -05:00
}
}
containerUnmounted(cont) {
if ( this.shouldUpdateChannel ) {
this.resolve('site.channel').updateChannelColor();
this.settings.updateContext({
channel: null,
channelID: null,
channelColor: null
});
}
this.removeRoom(cont);
}
2017-11-13 01:23:39 -05:00
containerUpdated(cont, props) {
// If we don't have a room, or if the room ID doesn't match our ID
// then we need to just create a new Room because the chat room changed.
if ( ! cont._ffz_room || props.channelID != cont._ffz_room.id ) {
2017-11-13 01:23:39 -05:00
this.removeRoom(cont);
if ( cont._ffz_mounted )
this.containerMounted(cont, props);
2017-11-13 01:23:39 -05:00
return;
}
if ( props.bitsConfig !== cont.props.bitsConfig )
this.updateRoomBitsConfig(cont, props.bitsConfig);
if ( props.data && Twilight.POPOUT_ROUTES.includes(this.router.current_name) ) {
const color = props.data.user?.primaryColorHex;
this.resolve('site.channel').updateChannelColor(color);
this.settings.updateContext({
channel: props.channelLogin,
channelID: props.channelID,
channelColor: color
});
}
2017-11-13 01:23:39 -05:00
// Twitch, React, and Apollo are the trifecta of terror so we
// can't compare the badgeSets property in any reasonable way.
// Instead, just check the lengths to see if they've changed
// and hope that badge versions will never change separately.
const data = props.data || {},
odata = cont.props.data || {},
2017-11-13 01:23:39 -05:00
bs = data.badges || [],
obs = odata.badges || [],
2017-11-13 01:23:39 -05:00
cs = data.user && data.user.broadcastBadges || [],
ocs = odata.user && odata.user.broadcastBadges || [];
2017-11-13 01:23:39 -05:00
if ( this.chat.badges.getTwitchBadgeCount() !== bs.length || bs.length !== obs.length )
this.chat.badges.updateTwitchBadges(bs);
2017-11-13 01:23:39 -05:00
if ( cont._ffz_room.badgeCount() !== cs.length || cs.length !== ocs.length )
this.updateRoomBadges(cont, cs);
this.updateRoomRules(cont, props.chatRules);
2017-11-13 01:23:39 -05:00
}
hasRoomBadges(cont) { // eslint-disable-line class-methods-use-this
const room = cont._ffz_room;
if ( ! room )
return false;
return room.hasBadges();
}
2017-11-13 01:23:39 -05:00
updateRoomBadges(cont, badges) { // eslint-disable-line class-methods-use-this
const room = cont._ffz_room;
if ( ! room )
return;
room.updateBadges(badges);
this.chat_line.updateLineBadges();
//this.updateChatLines();
2017-11-13 01:23:39 -05:00
}
updateRoomRules(cont, rules) { // eslint-disable-line class-methods-use-this
const room = cont._ffz_room;
if ( ! room )
return;
room.rules = rules;
}
2017-11-13 01:23:39 -05:00
}
// ============================================================================
// Processing Functions
// ============================================================================
export function formatBitsConfig(config) {
if ( ! config )
return;
const out = {},
actions = config.indexedActions,
tier_colors = {};
if ( Array.isArray(config.tiers) )
for(const tier of config.tiers)
tier_colors[tier.bits] = tier.color;
2017-11-13 01:23:39 -05:00
for(const key in actions)
if ( has(actions, key) ) {
const action = actions[key],
new_act = out[key] = {
id: action.id,
prefix: action.prefix,
tiers: []
};
if ( config?.getImage ) {
for(const tier of action.orderedTiers) {
const images = {};
for(const theme of ['light', 'dark']) {
const themed = images[theme] = images[theme] || {},
stat = themed.static = themed.static || {},
animated = themed.animated = themed.animated || {};
for(const scale of [1, 2, 4]) {
// Static Images
stat[scale] = config.getImage(action.prefix, theme, 'static', tier.bits, scale, 'png');
// Animated Images
animated[scale] = config.getImage(action.prefix, theme, 'animated', tier.bits, scale, 'gif');
}
}
2017-11-13 01:23:39 -05:00
new_act.tiers.push({
amount: tier.bits,
color: tier.color || tier_colors[tier.bits] || 'inherit',
id: tier.id,
images
});
2017-11-13 01:23:39 -05:00
}
} else if ( action.orderedTiers[0]?.images ) {
for(const tier of action.orderedTiers) {
const images = {};
for(const im of tier.images) {
const themed = images[im.theme] = images[im.theme] || [],
ak = im.isAnimated ? 'animated' : 'static',
anim = themed[ak] = themed[ak] || {};
anim[im.dpiScale] = im.url;
}
new_act.tiers.push({
amount: tier.bits,
color: tier.color || tier_colors[tier.bits] || 'inherit',
id: tier.id,
images
})
}
2017-11-13 01:23:39 -05:00
}
}
return out;
}