From 24a5cd512f076f25c2570287f59967fa29cbd270 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Tue, 5 Feb 2019 14:24:45 -0500 Subject: [PATCH] 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. --- src/main.js | 2 +- .../twitch-twilight/modules/chat/index.js | 227 +++++++++++++++- .../twitch-twilight/modules/chat/line.js | 256 +++++++++++++++--- src/sites/twitch-twilight/styles/chat.scss | 5 + src/utilities/constants.js | 7 + 5 files changed, 453 insertions(+), 44 deletions(-) diff --git a/src/main.js b/src/main.js index 3023e6ab..e77ed2cf 100644 --- a/src/main.js +++ b/src/main.js @@ -149,7 +149,7 @@ ${typeof x[1] === 'string' ? x[1] : JSON.stringify(x[1], null, 4)}` FrankerFaceZ.Logger = Logger; 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__, build: __webpack_hash__, toString: () => diff --git a/src/sites/twitch-twilight/modules/chat/index.js b/src/sites/twitch-twilight/modules/chat/index.js index e6c6e0ae..f07f994f 100644 --- a/src/sites/twitch-twilight/modules/chat/index.js +++ b/src/sites/twitch-twilight/modules/chat/index.js @@ -106,7 +106,8 @@ const CHAT_TYPES = make_enum( 'RewardGift', 'SubMysteryGift', 'AnonSubMysteryGift', - 'FirstCheerMessage' + 'FirstCheerMessage', + 'BitsBadgeTierMessage' ); @@ -119,11 +120,7 @@ const NULL_TYPES = [ const MISBEHAVING_EVENTS = [ - 'onBitsCharityEvent', - //'onRitualEvent', -- handled by conversion to Message event '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', { default: false, 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; this.onResubscriptionEvent = function(e) { try { + if ( t.chat.context.get('chat.subs.show') < 2 && ! e.body ) + return; + const out = i.convertMessage({message: e}); out.ffz_type = 'resub'; 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; this.onRitualEvent = function(e) { try { diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index 9fc49f24..e1911155 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -8,7 +8,8 @@ import Twilight from 'site'; import Module from 'utilities/module'; import RichContent from './rich_content'; -import { has } from 'src/utilities/object'; +import { has } from 'utilities/object'; +import { KEYS } from 'utilities/constants'; const SUB_TIERS = { 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.custom-mod', 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.hide-tokens', 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; return show !== old_show || + state.ffz_expanded !== this.state.ffz_expanded || //state.renderDebug !== this.state.renderDebug || props.message !== this.props.message || props.isCurrentUserModerator !== this.props.isCurrentUserModerator || @@ -302,8 +306,30 @@ export default class ChatLine extends Module { rich_content = FFZRichContent && t.chat.pluckRichContent(tokens, msg), bg_css = msg.mentioned && msg.mention_color ? t.parent.inverse_colors.process(msg.mention_color) : null; - if ( ! this.ffz_user_click_handler ) - this.ffz_user_click_handler = this.openViewerCard || this.usernameClickHandler; //event => event.ctrlKey ? this.usernameClickHandler(event) : t.viewer_cards.openCard(r, user, event); + if ( ! this.ffz_user_click_handler ) { + 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' : ''}`, out = (tokens.length || ! msg.ffz_type) ? [ @@ -356,47 +382,83 @@ export default class ChatLine extends Module { }, JSON.stringify([tokens, msg.emotes], null, 2))*/ ] : null; - if ( msg.ffz_type === 'resub' ) { - const plan = msg.sub_plan || {}, - months = msg.sub_cumulative || msg.sub_months, - tier = SUB_TIERS[plan.plan] || 1; + if ( msg.ffz_type === 'sub_mystery' ) { + const mystery = msg.mystery; + if ( mystery ) + msg.mystery.line = this; - const sub_msg = t.i18n.tList('chat.sub.main', '%{user} subscribed %{plan}.', { - user: e('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}) + const sub_msg = t.i18n.tList('chat.sub.gift', "%{user} is gifting %{count} Tier %{tier} Sub%{count|en_plural} to %{channel}'s community! ", { + 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)), + count: msg.sub_count, + tier: SUB_TIERS[msg.sub_plan] || 1, + channel: msg.roomLogin }); - if ( msg.sub_share_streak && msg.sub_streak ) { - 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 - } - )); + 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 + })); - } else if ( months ) { - sub_msg.push(t.i18n.t( - 'chat.sub.months', - "They've subscribed for %{count} months!", - { - count: months - } - )); + if ( ! this.ffz_click_expand ) + this.ffz_click_expand = () => { + this.setState({ + ffz_expanded: ! this.state.ffz_expanded + }); + } + + 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 tw-pd-r-2 ffz--subscribe-line'; + cls = 'user-notice-line tw-pd-y-05 ffz--subscribe-line'; 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', { className: 'chat-line--inline chat-line__message', 'data-room-id': this.props.channelID, @@ -406,12 +468,130 @@ export default class ChatLine extends Module { }, 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') ) { let system_msg; if ( msg.ritual === 'new_chatter' ) system_msg = e('div', {className: 'tw-c-text-alt-2'}, [ t.i18n.tList('chat.ritual', '%{user} is new here. Say hello!', { - user: e('button', { + user: e('span', { + role: 'button', className: 'chatter-name', onClick: this.ffz_user_click_handler }, e('span', { diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index fcd51e81..dca3779d 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -12,6 +12,11 @@ pointer-events: none; } +.ffz--sub-expando { + font-size: 150%; + margin-right: -.5rem; +} + .chat-list__lines .simplebar-scrollbar { will-change: opacity; } diff --git a/src/utilities/constants.js b/src/utilities/constants.js index 15574bc0..0931cccf 100644 --- a/src/utilities/constants.js +++ b/src/utilities/constants.js @@ -14,6 +14,13 @@ export const LV_SERVER = 'https://cbenni.com/api'; 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 KNOWN_CODES = {