1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-05 18:48:31 +00:00

4.0.0-rc14

* Added: New settings in Chat > Appearance for controlling Subscription notices in chat.
* Changed: Display an icon next to new subscription notices, like vanilla Twitch is doing now.
* Changed: Display in-line moderation actions on subscription notices without associated messages.

You can now merge mass gift subs, as well as completely hide subscription notices if you so choose. Please note that you will still see the messages people send with their gift regardless of these new settings.
This commit is contained in:
SirStendec 2019-02-05 14:24:45 -05:00
parent a8886167a3
commit 24a5cd512f
5 changed files with 453 additions and 44 deletions

View file

@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}`
FrankerFaceZ.Logger = Logger; FrankerFaceZ.Logger = Logger;
const VER = FrankerFaceZ.version_info = { const VER = FrankerFaceZ.version_info = {
major: 4, minor: 0, revision: 0, extra: '-rc13.23', major: 4, minor: 0, revision: 0, extra: '-rc14',
commit: __git_commit__, commit: __git_commit__,
build: __webpack_hash__, build: __webpack_hash__,
toString: () => toString: () =>

View file

@ -106,7 +106,8 @@ const CHAT_TYPES = make_enum(
'RewardGift', 'RewardGift',
'SubMysteryGift', 'SubMysteryGift',
'AnonSubMysteryGift', 'AnonSubMysteryGift',
'FirstCheerMessage' 'FirstCheerMessage',
'BitsBadgeTierMessage'
); );
@ -119,11 +120,7 @@ const NULL_TYPES = [
const MISBEHAVING_EVENTS = [ const MISBEHAVING_EVENTS = [
'onBitsCharityEvent',
//'onRitualEvent', -- handled by conversion to Message event
'onBadgesUpdatedEvent', 'onBadgesUpdatedEvent',
'onPurchaseEvent',
'onCrateEvent'
]; ];
@ -234,6 +231,49 @@ export default class ChatHook extends Module {
} }
}); });
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.lines.alternate', { this.settings.add('chat.lines.alternate', {
default: false, default: false,
ui: { ui: {
@ -882,9 +922,30 @@ export default class ChatHook extends Module {
} }
const old_sub = this.onSubscriptionEvent;
this.onSubscriptionEvent = function(e) {
try {
if ( t.chat.context.get('chat.subs.show') < 3 )
return;
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'resub';
out.sub_plan = e.methods;
return i.postMessageToCurrentChannel(e, out);
} catch(err) {
t.log.capture(err, {extra: e});
return old_sub.call(i, e);
}
}
const old_resub = this.onResubscriptionEvent; const old_resub = this.onResubscriptionEvent;
this.onResubscriptionEvent = function(e) { this.onResubscriptionEvent = function(e) {
try { try {
if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body )
return;
const out = i.convertMessage({message: e}); const out = i.convertMessage({message: e});
out.ffz_type = 'resub'; out.ffz_type = 'resub';
out.sub_cumulative = e.cumulativeMonths || 0; out.sub_cumulative = e.cumulativeMonths || 0;
@ -903,6 +964,162 @@ export default class ChatHook extends Module {
} }
} }
const mysteries = this.ffz_mysteries = {};
const old_subgift = this.onSubscriptionGiftEvent;
this.onSubscriptionGiftEvent = function(e) {
try {
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 )
mystery.line.forceUpdate();
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.sub_plan = e.methods;
out.sub_count = 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_anonsubgift = this.onAnonSubscriptionGiftEvent;
this.onAnonSubscriptionGiftEvent = function(e) {
try {
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 )
mystery.line.forceUpdate();
return;
}
}
e.body = '';
const out = i.convertMessage({message: e});
out.ffz_type = 'sub_gift';
out.sub_anon = true;
out.sub_recipient = {
id: e.recipientID,
login: e.recipientLogin,
displayName: e.recipientName
};
out.sub_plan = e.methods;
out.sub_count = 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 {
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.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 {
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; const old_ritual = this.onRitualEvent;
this.onRitualEvent = function(e) { this.onRitualEvent = function(e) {
try { try {

View file

@ -8,7 +8,8 @@ import Twilight from 'site';
import Module from 'utilities/module'; import Module from 'utilities/module';
import RichContent from './rich_content'; import RichContent from './rich_content';
import { has } from 'src/utilities/object'; import { has } from 'utilities/object';
import { KEYS } from 'utilities/constants';
const SUB_TIERS = { const SUB_TIERS = {
1000: 1, 1000: 1,
@ -63,6 +64,8 @@ export default class ChatLine extends Module {
this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this); this.chat.context.on('changed:chat.badges.hidden', this.updateLines, this);
this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this); this.chat.context.on('changed:chat.badges.custom-mod', this.updateLines, this);
this.chat.context.on('changed:chat.rituals.show', this.updateLines, this); this.chat.context.on('changed:chat.rituals.show', this.updateLines, this);
this.chat.context.on('changed:chat.subs.show', this.updateLines, this);
this.chat.context.on('changed:chat.subs.compact', this.updateLines, this);
this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this); this.chat.context.on('changed:chat.rich.enabled', this.updateLines, this);
this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this); this.chat.context.on('changed:chat.rich.hide-tokens', this.updateLines, this);
this.chat.context.on('changed:chat.rich.all-links', this.updateLines, this); this.chat.context.on('changed:chat.rich.all-links', this.updateLines, this);
@ -251,6 +254,7 @@ export default class ChatLine extends Module {
this._ffz_show = show; this._ffz_show = show;
return show !== old_show || return show !== old_show ||
state.ffz_expanded !== this.state.ffz_expanded ||
//state.renderDebug !== this.state.renderDebug || //state.renderDebug !== this.state.renderDebug ||
props.message !== this.props.message || props.message !== this.props.message ||
props.isCurrentUserModerator !== this.props.isCurrentUserModerator || props.isCurrentUserModerator !== this.props.isCurrentUserModerator ||
@ -302,8 +306,30 @@ export default class ChatLine extends Module {
rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg), rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg),
bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null; bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null;
if ( ! this.ffz_user_click_handler ) if ( ! this.ffz_user_click_handler ) {
this.ffz_user_click_handler = this.openViewerCard || this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event); if ( this.props.onUsernameClick )
this.ffz_user_click_handler = event => {
if ( this.isKeyboardEvent(event) && event.keyCode !== KEYS.Space && event.keyCode !== KEYS.Enter )
return;
const target = event.currentTarget,
ds = target && target.dataset;
let target_user = user;
if ( ds && ds.user ) {
try {
target_user = JSON.parse(ds.user);
} catch(err) { /* nothing~! */ }
}
/*if ( event.ctrlKey )
t.viewer_cards.openCard(r, target_user, event);
else*/
this.props.onUsernameClick(target_user.login, 'chat_message', msg.id, target.getBoundingClientRect().bottom);
}
else
this.ffz_user_click_handler = this.openViewerCard || this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event);
}
let cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}`, let cls = `chat-line__message${show_class ? ' ffz--deleted-message' : ''}`,
out = (tokens.length || ! msg.ffz_type) ? [ out = (tokens.length || ! msg.ffz_type) ? [
@ -356,47 +382,83 @@ export default class ChatLine extends Module {
}, JSON.stringify([tokens, msg.emotes], null, 2))*/ }, JSON.stringify([tokens, msg.emotes], null, 2))*/
] : null; ] : null;
if ( msg.ffz_type === 'resub' ) { if ( msg.ffz_type === 'sub_mystery' ) {
const plan = msg.sub_plan || {}, const mystery = msg.mystery;
months = msg.sub_cumulative || msg.sub_months, if ( mystery )
tier = SUB_TIERS[plan.plan] || 1; msg.mystery.line = this;
const sub_msg = t.i18n.tList('chat.sub.main', '%{user} subscribed %{plan}.', { const sub_msg = t.i18n.tList('chat.sub.gift', "%{user} is gifting %{count} Tier %{tier} Sub%{count|en_plural} to %{channel}'s community! ", {
user: e('button', { user: (msg.sub_anon || user.username === 'ananonymousgifter') ?
className: 'chatter-name', t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
onClick: this.ffz_user_click_handler e('span', {
}, e('span', { role: 'button',
className: 'tw-c-text-base tw-strong' className: 'chatter-name',
}, user.userDisplayName)), onClick: this.ffz_user_click_handler
plan: plan.prime ? }, e('span', {
t.i18n.t('chat.sub.twitch-prime', 'with Twitch Prime') : className: 'tw-c-text-base tw-strong'
t.i18n.t('chat.sub.plan', 'at Tier %{tier}', {tier}) }, user.userDisplayName)),
count: msg.sub_count,
tier: SUB_TIERS[msg.sub_plan] || 1,
channel: msg.roomLogin
}); });
if ( msg.sub_share_streak && msg.sub_streak ) { if ( msg.sub_total === 1 )
sub_msg.push(t.i18n.t( sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
'chat.sub.cumulative-months', else if ( msg.sub_total > 1 )
"They've subscribed for %{cumulative} months, currently on a %{streak} month streak!", sub_msg.push(t.i18n.t('chat.sub.gift-total', "They've gifted %{count} Subs in the channel!", {
{ count: msg.sub_total
cumulative: msg.sub_cumulative, }));
streak: msg.sub_streak
}
));
} else if ( months ) { if ( ! this.ffz_click_expand )
sub_msg.push(t.i18n.t( this.ffz_click_expand = () => {
'chat.sub.months', this.setState({
"They've subscribed for %{count} months!", ffz_expanded: ! this.state.ffz_expanded
{ });
count: months }
}
)); let sub_list = null;
if( this.state.ffz_expanded && mystery && mystery.recipients && mystery.recipients.length > 0 ) {
const the_list = [];
for(const x of mystery.recipients) {
if ( the_list.length )
the_list.push(', ');
the_list.push(e('span', {
role: 'button',
className: 'chatter-name',
onClick: this.ffz_user_click_handler,
'data-user': JSON.stringify(x)
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, x.displayName)));
}
sub_list = e('div', {
className: 'tw-mg-t-05 tw-border-t tw-pd-t-05 tw-c-text-alt-2'
}, the_list);
} }
cls = 'user-notice-line tw-pd-y-05 ffz--subscribe-line';
cls = 'user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--subscribe-line';
out = [ out = [
e('div', {className: 'tw-c-text-alt-2'}, sub_msg), e('div', {
className: 'tw-flex tw-c-text-alt-2',
onClick: this.ffz_click_expand
}, [
t.chat.context.get('chat.subs.compact') ? null :
e('figure', {
className: `ffz-i-star${msg.sub_anon ? '-empty' : ''} tw-mg-r-05`
}),
e('div', null, [
(out || msg.sub_anon) ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
sub_msg
]),
mystery ? e('div', {
className: 'tw-mg-l-05 tw-border-l tw-pd-l-05 ffz--sub-expando'
}, e('figure', {
className: `ffz-i-${this.state.ffz_expanded ? 'down' : 'right'}-dir tw-pd-y-1`
})) : null
]),
sub_list,
out && e('div', { out && e('div', {
className: 'chat-line--inline chat-line__message', className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID, 'data-room-id': this.props.channelID,
@ -406,12 +468,130 @@ export default class ChatLine extends Module {
}, out) }, out)
]; ];
} else if ( msg.ffz_type === 'sub_gift' ) {
const plan = msg.sub_plan || {},
tier = SUB_TIERS[plan.plan] || 1;
const sub_msg = t.i18n.tList('chat.sub.mystery', '%{user} gifted a %{plan} Sub to %{recipient}!', {
user: (msg.sub_anon || user.username === 'ananonymousgifter') ?
t.i18n.t('chat.sub.anonymous-gifter', 'An anonymous gifter') :
e('span', {
role: 'button',
className: 'chatter-name',
onClick: this.ffz_user_click_handler
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, user.userDisplayName)),
plan: plan.plan === 'custom' ? '' :
t.i18n.t('chat.sub.gift-plan', 'Tier %{tier}', {tier}),
recipient: e('span', {
role: 'button',
className: 'chatter-name',
onClick: this.ffz_user_click_handler,
'data-user': JSON.stringify(msg.sub_recipient)
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, msg.sub_recipient.displayName))
});
if ( msg.sub_total === 1 )
sub_msg.push(t.i18n.t('chat.sub.gift-first', "It's their first time gifting a Sub in the channel!"));
else if ( msg.sub_total > 1 )
sub_msg.push(t.i18n.t('chat.sub.gift-total', "They've gifted %{count} Subs in the channel!", {
count: msg.sub_total
}));
cls = 'user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--subscribe-line';
out = [
e('div', {className: 'tw-flex tw-c-text-alt-2'}, [
t.chat.context.get('chat.subs.compact') ? null :
e('figure', {
className: 'ffz-i-star tw-mg-r-05'
}),
e('div', null, [
(out || msg.sub_anon) ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
sub_msg
])
]),
out && e('div', {
className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
}, out)
];
} else if ( msg.ffz_type === 'resub' ) {
const months = msg.sub_cumulative || msg.sub_months,
setting = t.chat.context.get('chat.subs.show');
if ( setting === 3 || (setting === 1 && out && months > 1) || (setting === 2 && months > 1) ) {
const plan = msg.sub_plan || {},
tier = SUB_TIERS[plan.plan] || 1;
const sub_msg = t.i18n.tList('chat.sub.main', '%{user} subscribed %{plan}. ', {
user: e('span', {
role: 'button',
className: 'chatter-name',
onClick: this.ffz_user_click_handler
}, e('span', {
className: 'tw-c-text-base tw-strong'
}, user.userDisplayName)),
plan: plan.prime ?
t.i18n.t('chat.sub.twitch-prime', 'with Twitch Prime') :
t.i18n.t('chat.sub.plan', 'at Tier %{tier}', {tier})
});
if ( msg.sub_share_streak && msg.sub_streak > 1 ) {
sub_msg.push(t.i18n.t(
'chat.sub.cumulative-months',
"They've subscribed for %{cumulative} months, currently on a %{streak} month streak!",
{
cumulative: msg.sub_cumulative,
streak: msg.sub_streak
}
));
} else if ( months > 1 ) {
sub_msg.push(t.i18n.t(
'chat.sub.months',
"They've subscribed for %{count} months!",
{
count: months
}
));
}
cls = 'user-notice-line tw-pd-y-05 tw-pd-r-2 ffz--subscribe-line';
out = [
e('div', {className: 'tw-flex tw-c-text-alt-2'}, [
t.chat.context.get('chat.subs.compact') ? null :
e('figure', {
className: `ffz-i-${plan.prime ? 'crown' : 'star'} tw-mg-r-05`
}),
e('div', null, [
out ? null : t.actions.renderInline(msg, this.props.showModerationIcons, u, r, e),
sub_msg
])
]),
out && e('div', {
className: 'chat-line--inline chat-line__message',
'data-room-id': this.props.channelID,
'data-room': room,
'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(),
}, out)
];
}
} else if ( msg.ffz_type === 'ritual' && t.chat.context.get('chat.rituals.show') ) { } else if ( msg.ffz_type === 'ritual' && t.chat.context.get('chat.rituals.show') ) {
let system_msg; let system_msg;
if ( msg.ritual === 'new_chatter' ) if ( msg.ritual === 'new_chatter' )
system_msg = e('div', {className: 'tw-c-text-alt-2'}, [ system_msg = e('div', {className: 'tw-c-text-alt-2'}, [
t.i18n.tList('chat.ritual', '%{user} is new here. Say hello!', { t.i18n.tList('chat.ritual', '%{user} is new here. Say hello!', {
user: e('button', { user: e('span', {
role: 'button',
className: 'chatter-name', className: 'chatter-name',
onClick: this.ffz_user_click_handler onClick: this.ffz_user_click_handler
}, e('span', { }, e('span', {

View file

@ -12,6 +12,11 @@
pointer-events: none; pointer-events: none;
} }
.ffz--sub-expando {
font-size: 150%;
margin-right: -.5rem;
}
.chat-list__lines .simplebar-scrollbar { .chat-list__lines .simplebar-scrollbar {
will-change: opacity; will-change: opacity;
} }

View file

@ -14,6 +14,13 @@ export const LV_SERVER = 'https://cbenni.com/api';
export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/'; export const LV_SOCKET_SERVER = 'wss://cbenni.com/socket.io/';
export const KEYS = {
Space: 32,
Enter: 13,
Escape: 27,
};
export const TWITCH_EMOTE_BASE = '//static-cdn.jtvnw.net/emoticons/v1/'; export const TWITCH_EMOTE_BASE = '//static-cdn.jtvnw.net/emoticons/v1/';
export const KNOWN_CODES = { export const KNOWN_CODES = {