mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 12:55:55 +00:00
4.76.3
* Added: Setting to block call-outs based on type. This should stay up to date automatically whenever Twitch adds new call-out types. * Added: Setting to specifically block clip suggestion call-outs. Twitch has reversed the roll-out for now but I suspect they'll be back. * Fixed: Bug related to badges and emote sets not being tracked correctly in some situations. * Fixed: Bug where the new replacement Clip button not functioning correctly after the player state changes.
This commit is contained in:
parent
18bff3371d
commit
93181dc8e8
9 changed files with 337 additions and 411 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.76.2",
|
||||
"version": "4.76.3",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
.player-controls__right-control-group button[aria-label*="alt+x"] {
|
||||
.player-controls__right-control-group button[aria-label*="alt+x"]:not(.ffz-core-button),
|
||||
.player-controls__right-control-group button[aria-disabled]:has(path[d="M8 9H6v2h2V9zm1 0h2v2H9V9zm5 0h-2v2h2V9z"])
|
||||
{
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,11 @@ const SCROLL_I18N = 'setting.entry.player.volume-scroll.values',
|
|||
{value: 8, title: 'Enabled with Ctrl + Right-Click', i18n_key: `${SCROLL_I18N}.8`}
|
||||
];
|
||||
|
||||
function getNativeClipButton(container) {
|
||||
return container.querySelector('button[aria-label*="alt+x"]') ??
|
||||
container.querySelector('button[aria-label]:has(path[d="M8 9H6v2h2V9zm1 0h2v2H9V9zm5 0h-2v2h2V9z"]');
|
||||
}
|
||||
|
||||
function wantsRMB(setting) {
|
||||
return setting === 2 || setting === 4 || setting === 6 || setting === 8;
|
||||
}
|
||||
|
@ -2023,7 +2028,6 @@ export default class PlayerBase extends Module {
|
|||
video.requestPictureInPicture();
|
||||
}
|
||||
|
||||
|
||||
addClipButton(inst, tries = 0) {
|
||||
const outer = inst.props.containerRef || this.fine.getChildNode(inst),
|
||||
container = outer && outer.querySelector(RIGHT_CONTROLS),
|
||||
|
@ -2046,13 +2050,16 @@ export default class PlayerBase extends Module {
|
|||
return;
|
||||
}
|
||||
|
||||
if (! container.ffz_native_clip || ! container.contains(container.ffz_native_clip) )
|
||||
container.ffz_native_clip = getNativeClipButton(container);
|
||||
|
||||
if ( ! cont ) {
|
||||
// We need the native clip button, so we can dispatch a click.
|
||||
const native_clip = container.querySelector('button[aria-label*="alt+x"]');
|
||||
if ( ! native_clip )
|
||||
return;
|
||||
|
||||
const on_click = e => native_clip.click();
|
||||
const on_click = e => {
|
||||
const native = getNativeClipButton(container);
|
||||
if (native)
|
||||
native.click();
|
||||
}
|
||||
|
||||
cont = (<div class="ffz--player-clip tw-inline-flex tw-relative ffz-il-tooltip__container">
|
||||
{btn = (<button
|
||||
|
@ -2087,11 +2094,22 @@ export default class PlayerBase extends Module {
|
|||
tip = cont.querySelector('.ffz-il-tooltip');
|
||||
}
|
||||
|
||||
const native = container.ffz_native_clip,
|
||||
disabled = native
|
||||
? (native.disabled || native.ariaDisabled === 'true')
|
||||
: false;
|
||||
|
||||
btn.disabled = disabled;
|
||||
btn.setAttribute('aria-label',
|
||||
tip.textContent = this.i18n.t(
|
||||
'player.clip-button',
|
||||
'Clip (Alt+X)'
|
||||
));
|
||||
tip.textContent = disabled
|
||||
? (native.ariaLabel || this.i18n.t(
|
||||
'player.clip-button.disabled',
|
||||
'Clips are Disabled'
|
||||
))
|
||||
: this.i18n.t(
|
||||
'player.clip-button',
|
||||
'Clip (Alt+X)'
|
||||
));
|
||||
}
|
||||
|
||||
clickClip(inst, e) {
|
||||
|
|
|
@ -220,7 +220,19 @@ Twilight.KNOWN_MODULES = {
|
|||
},
|
||||
cookie: n => n && n.set && n.get && n.getJSON && n.withConverter,
|
||||
'extension-service': n => n.extensionService,
|
||||
'callout-types': n => {
|
||||
if ( n.o?.ClipLiveNudge && n.o?.Drop && n.o?.ShareResub )
|
||||
return n.o;
|
||||
},
|
||||
'chat-types': n => {
|
||||
if ( has(n.ZW, 'Message') && has(n.ZW, 'RoomMods') )
|
||||
return {
|
||||
automod: n.sq,
|
||||
chat: n.ZW,
|
||||
message: n.IF,
|
||||
mod: n.RB
|
||||
};
|
||||
|
||||
if ( has(n.b, 'Message') && has(n.b, 'RoomMods') )
|
||||
return {
|
||||
automod: n.a,
|
||||
|
@ -256,6 +268,12 @@ Twilight.KNOWN_MODULES = {
|
|||
if ( n.w9?.prototype?.queryTopResults && n.w9.prototype.queryForType )
|
||||
return n.w9;
|
||||
},
|
||||
calloutstack: n => {
|
||||
if ( has(n.at?._currentValue, 'callouts') && typeof n.at._currentValue.pinCallout === 'function' )
|
||||
return {
|
||||
stack: n.at
|
||||
};
|
||||
},
|
||||
highlightstack: n => {
|
||||
if ( has(n['g$']?._currentValue, 'highlights') && typeof n.SP?._currentValue === 'function' )
|
||||
return {
|
||||
|
@ -301,8 +319,10 @@ Twilight.KNOWN_MODULES.mousetrap.chunks = VEND_CORE;
|
|||
|
||||
const CHAT_CHUNK = n => ! n || n.includes('chat');
|
||||
|
||||
Twilight.KNOWN_MODULES['callout-types'].use_result = true;
|
||||
Twilight.KNOWN_MODULES['chat-types'].use_result = true;
|
||||
Twilight.KNOWN_MODULES['chat-types'].chunks = CHAT_CHUNK;
|
||||
Twilight.KNOWN_MODULES['calloutstack'].use_result = true;
|
||||
Twilight.KNOWN_MODULES['highlightstack'].use_result = true;
|
||||
Twilight.KNOWN_MODULES['highlightstack'].chunks = CHAT_CHUNK;
|
||||
|
||||
|
|
|
@ -155,6 +155,39 @@ const NULL_TYPES = [
|
|||
];
|
||||
|
||||
|
||||
const INLINE_CALLOUT_TYPES = {
|
||||
'pinned_re_sub': 'share-resub'
|
||||
};
|
||||
|
||||
const CALLOUT_TYPES = {
|
||||
"AppointedModerator": "appointed-moderator",
|
||||
"BitsBadgeTier": "bits-badge-tier",
|
||||
"CommunityMoment": "community-moment",
|
||||
"ClipLiveNudge": "clip-live-nudge",
|
||||
"ShareResub": "share-resub",
|
||||
"ThankSubGifter": "thank-sub-gifter",
|
||||
"CommunityPointsRewards": "community-points-rewards",
|
||||
"HypeTrainRewards": "hype-train-rewards",
|
||||
"ReplyByKeyboard": "reply-by-keyboard",
|
||||
"Drop": "drop",
|
||||
"EarnedSubBadge": "earned-sub-badge",
|
||||
"TurnOffAnimatedEmotes": "turn-off-animated-emotes",
|
||||
"CreatorAnniversaries": "creator-anniversaries",
|
||||
"RequestToJoinAccepted": "request-to-join-accepted",
|
||||
"FavoritedGuestCollab": "favorited-guest-collab",
|
||||
"STPromo": "st-promo",
|
||||
"LapsedBitsUser": "lapsed-bits-user",
|
||||
"BitsPowerUps": "bits-power-ups",
|
||||
"CosmicAbyss": "cosmic-abyss",
|
||||
"PartnerPlusUpSellNudge": "partner-plus-up-sell-nudge",
|
||||
"SubtemberPromoBits": "subtember-promo-bits",
|
||||
"GiftBundleUpSell": "gift-bundle-up-sell",
|
||||
"WalletDrop": "wallet-drop"
|
||||
};
|
||||
|
||||
const UNBLOCKABLE_CALLOUTS = [];
|
||||
|
||||
|
||||
const MISBEHAVING_EVENTS = [
|
||||
'onBadgesUpdatedEvent',
|
||||
];
|
||||
|
@ -255,11 +288,11 @@ export default class ChatHook extends Module {
|
|||
Twilight.CHAT_ROUTES
|
||||
);
|
||||
|
||||
this.CalloutSelector = this.fine.define(
|
||||
/*this.CalloutSelector = this.fine.define(
|
||||
'callout-selector',
|
||||
n => n.selectCalloutComponent && n.props && n.props.callouts,
|
||||
n => n.selectCalloutComponent && n.props && has(n.props, 'callouts'),
|
||||
Twilight.CHAT_ROUTES
|
||||
);
|
||||
);*/
|
||||
|
||||
this.PointsButton = this.fine.define(
|
||||
'points-button',
|
||||
|
@ -342,6 +375,30 @@ export default class ChatHook extends Module {
|
|||
force_seen: true
|
||||
});
|
||||
|
||||
this.settings.add('chat.filtering.blocked-callouts', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
always_inherit: true,
|
||||
process: (ctx, val) => {
|
||||
const out = new Set,
|
||||
type_map = this.callout_types ?? CALLOUT_TYPES;
|
||||
for(const v of val)
|
||||
if ( v?.v && type_map[v.v] && ! UNBLOCKABLE_CALLOUTS.includes(v.v) )
|
||||
out.add(type_map[v.v]);
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
ui: {
|
||||
path: 'Chat > Filtering > Block >> Callout Types @{"description":"This filter allows you to remove callouts of specific types from Twitch chat. Callouts are special messages that can be pinned to the bottom of chat and often have associated actions, like claiming a drop or sharing your resubscription."}',
|
||||
component: 'blocked-types',
|
||||
data: () => Object
|
||||
.keys(this.callout_types)
|
||||
.filter(key => ! UNBLOCKABLE_CALLOUTS.includes(key))
|
||||
.sort()
|
||||
}
|
||||
})
|
||||
|
||||
this.settings.add('chat.filtering.blocked-types', {
|
||||
default: [],
|
||||
type: 'array_merge',
|
||||
|
@ -349,7 +406,7 @@ export default class ChatHook extends Module {
|
|||
process(ctx, val) {
|
||||
const out = new Set;
|
||||
for(const v of val)
|
||||
if ( v?.v || ! UNBLOCKABLE_TYPES.includes(v.v) )
|
||||
if ( v?.v && ! UNBLOCKABLE_TYPES.includes(v.v) )
|
||||
out.add(v.v);
|
||||
|
||||
return out;
|
||||
|
@ -554,6 +611,15 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.callouts.clip', {
|
||||
default: true,
|
||||
ui: {
|
||||
path: 'Chat > Appearance >> Community',
|
||||
title: 'Allow the \"Chat seems active.\" clip suggestion to be displayed in chat.',
|
||||
component: 'setting-check-box'
|
||||
}
|
||||
});
|
||||
|
||||
this.settings.add('chat.community-chest.show', {
|
||||
default: true,
|
||||
ui: {
|
||||
|
@ -1067,8 +1133,10 @@ export default class ChatHook extends Module {
|
|||
if ( this.types_loaded )
|
||||
return;
|
||||
|
||||
const ct = await this.web_munch.findModule('chat-types');
|
||||
const ct = await this.web_munch.findModule('chat-types'),
|
||||
callouts = await this.web_munch.findModule('callout-types');
|
||||
|
||||
this.callout_types = callouts || CALLOUT_TYPES;
|
||||
this.automod_types = ct?.automod || AUTOMOD_TYPES;
|
||||
this.chat_types = ct?.chat || CHAT_TYPES;
|
||||
this.message_types = ct?.message || MESSAGE_TYPES;
|
||||
|
@ -1077,9 +1145,15 @@ export default class ChatHook extends Module {
|
|||
if ( ! ct )
|
||||
return;
|
||||
|
||||
if ( callouts )
|
||||
this.chat.context.update('chat.filtering.blocked-callouts');
|
||||
|
||||
this.types_loaded = true;
|
||||
const changes = [];
|
||||
|
||||
if ( ! shallow_object_equals(this.callout_types, CALLOUT_TYPES) )
|
||||
changes.push('CALLOUT_TYPES');
|
||||
|
||||
if ( ! shallow_object_equals(this.automod_types, AUTOMOD_TYPES) )
|
||||
changes.push('AUTOMOD_TYPES');
|
||||
|
||||
|
@ -1123,10 +1197,9 @@ export default class ChatHook extends Module {
|
|||
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.callouts.clip', this.updateCallouts, this);
|
||||
this.chat.context.on('changed:chat.filtering.blocked-callouts', this.updateCallouts, this);
|
||||
this.chat.context.on('changed:chat.points.show-callouts', this.updateCallouts, this);
|
||||
this.chat.context.on('changed:chat.points.show-button', () => this.PointsButton.forceUpdate());
|
||||
this.chat.context.on('changed:chat.points.show-rewards', () => {
|
||||
this.PointsButton.forceUpdate();
|
||||
|
@ -1180,7 +1253,7 @@ export default class ChatHook extends Module {
|
|||
|
||||
this.chat.context.on('changed:chat.community-chest.show', () => {
|
||||
this.CommunityChestBanner.forceUpdate();
|
||||
this.CalloutSelector.forceUpdate();
|
||||
this.updateCallouts();
|
||||
}, this);
|
||||
|
||||
this.chat.context.getChanges('chat.emotes.2x', val => {
|
||||
|
@ -1399,66 +1472,6 @@ export default class ChatHook extends Module {
|
|||
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;
|
||||
|
||||
|
@ -1656,6 +1669,33 @@ export default class ChatHook extends Module {
|
|||
this.ChatContainer.on('unmount', this.containerUnmounted, this); //removeRoom, this);
|
||||
this.ChatContainer.on('update', this.containerUpdated, this);
|
||||
|
||||
/*this.CalloutSelector.ready((cls, instances) => {
|
||||
const t = this,
|
||||
old_render = cls.prototype.render;
|
||||
|
||||
cls.prototype.render = function() {
|
||||
try {
|
||||
if ( t.CalloutStackHandler ) {
|
||||
const React = t.site.getReact(),
|
||||
out = old_render.call(this),
|
||||
thing = out?.props?.children;
|
||||
|
||||
if ( Array.isArray(thing) )
|
||||
thing.push(React.createElement(t.CalloutStackHandler));
|
||||
|
||||
return out;
|
||||
}
|
||||
} catch(err) {
|
||||
/* no-op * /
|
||||
}
|
||||
|
||||
return old_render.call(this);
|
||||
}
|
||||
|
||||
for(const inst of instances)
|
||||
inst.forceUpdate();
|
||||
});*/
|
||||
|
||||
this.ChatContainer.ready((cls, instances) => {
|
||||
const t = this,
|
||||
old_render = cls.prototype.render,
|
||||
|
@ -1765,17 +1805,129 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
|
||||
|
||||
shouldHideCallout(type) {
|
||||
if ( ! type )
|
||||
return;
|
||||
|
||||
type = INLINE_CALLOUT_TYPES[type] ?? type;
|
||||
|
||||
const ctm = this.callout_types ?? CALLOUT_TYPES,
|
||||
blocked = this.chat.context.get('chat.filtering.blocked-callouts');
|
||||
|
||||
if ( blocked && blocked.has(type) )
|
||||
return true;
|
||||
|
||||
if ( type === ctm.CommunityPointsRewards &&
|
||||
! this.chat.context.get('chat.points.show-callouts')
|
||||
)
|
||||
return true;
|
||||
|
||||
if ( type === ctm.ClipLiveNudge &&
|
||||
! this.chat.context.get('chat.callouts.clip')
|
||||
)
|
||||
return true;
|
||||
|
||||
if ( type === 'prime_gift_bomb' &&
|
||||
! this.chat.context.get('chat.community-chest.show')
|
||||
)
|
||||
return true;
|
||||
|
||||
if ( type === 'megacheer_emote_recipient' &&
|
||||
! t.chat.context.get('chat.bits.show-rewards')
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
updateCallouts() {
|
||||
this.updatePinnedCallouts();
|
||||
this.updateInlineCallouts();
|
||||
}
|
||||
|
||||
|
||||
updatePinnedCallouts() {
|
||||
for(const inst of this.PinnedCallout.instances)
|
||||
this.onPinnedCallout(inst);
|
||||
}
|
||||
|
||||
onPinnedCallout(inst) {
|
||||
const props = inst.props,
|
||||
event = props?.event,
|
||||
type = event?.type;
|
||||
|
||||
//console.warn('pinned-callout', type, event, inst);
|
||||
|
||||
// Hidden callouts
|
||||
if ( this.shouldHideCallout(type) ) {
|
||||
if ( inst.props.pinned )
|
||||
inst.unpin();
|
||||
else
|
||||
inst.hide();
|
||||
}
|
||||
|
||||
// Auto-pin resubs
|
||||
if ( type === 'share-resub' && ! props.pinned && this.chat.context.get('chat.pin-resubs') && ! inst._ffz_pinned ) {
|
||||
this.log.info('Automatically pinning re-sub notice.');
|
||||
inst._ffz_pinned = true;
|
||||
inst.pin();
|
||||
}
|
||||
|
||||
// Auto-claim drops
|
||||
if ( type === 'drop' )
|
||||
this.autoClickDrop(inst);
|
||||
}
|
||||
|
||||
updateInlineCallouts() {
|
||||
for(const inst of this.InlineCallout.instances)
|
||||
this.onInlineCallout(inst);
|
||||
}
|
||||
|
||||
onInlineCallout(inst) {
|
||||
// Skip hidden inline callouts.
|
||||
if ( inst.state.isHidden )
|
||||
return;
|
||||
|
||||
const contextMenuProps = inst.props?.event?.callout?.contextMenuProps,
|
||||
event = contextMenuProps?.event ?? inst.props?.event,
|
||||
type = event?.type;
|
||||
|
||||
//console.warn('inline-callout', type, event, inst);
|
||||
|
||||
// Hidden callouts
|
||||
if ( this.shouldHideCallout(type) || this.shouldHideCallout(inst.props?.event?.trackingType) ) {
|
||||
inst.setState({isHidden: true});
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-pin resubs
|
||||
if ( type === 'share-resub' && this.chat.context.get('chat.pin-resubs') && ! inst._ffz_pinned ) {
|
||||
const onPin = contextMenuProps?.onPin;
|
||||
if ( onPin ) {
|
||||
this.log.info('Automatically pinning re-sub notice.');
|
||||
inst._ffz_pinned = true;
|
||||
if ( inst.hideOnContextMenuAction )
|
||||
inst.hideOnContextMenuAction(onPin)();
|
||||
else
|
||||
onPin();
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-claim drops
|
||||
if ( type === 'drop' )
|
||||
this.autoClickDrop(inst);
|
||||
}
|
||||
|
||||
|
||||
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') )
|
||||
const event = inst.props?.event?.callout?.contextMenuProps?.event ?? inst.props?.event,
|
||||
type = event?.type;
|
||||
|
||||
if ( type !== 'drop' || inst._ffz_clicking || ! this.chat.context.get("chat.drops.auto-rewards") )
|
||||
return;
|
||||
|
||||
//console.warn('autoClickDrop', event, inst);
|
||||
inst._ffz_clicking = true;
|
||||
|
||||
// Wait for the button to be added to the DOM.
|
||||
|
@ -1789,10 +1941,10 @@ export default class ChatHook extends Module {
|
|||
inst._ffz_clicking = false;
|
||||
|
||||
// Check AGAIN because time has passed.
|
||||
const callout = inst.props?.callouts?.[0] || inst.props?.pinnedCallout,
|
||||
ctype = callout?.event?.type;
|
||||
const event = inst.props?.event?.callout?.contextMenuProps?.event ?? inst.props?.event,
|
||||
type = event?.type;
|
||||
|
||||
if ( ctype !== 'drop' || ! this.chat.context.get('chat.drops.auto-rewards') )
|
||||
if ( type !== 'drop' || ! this.chat.context.get("chat.drops.auto-rewards") )
|
||||
return;
|
||||
|
||||
btn.click();
|
||||
|
@ -1909,30 +2061,47 @@ export default class ChatHook extends Module {
|
|||
|
||||
|
||||
defineClasses() {
|
||||
if ( this.CommunityStackHandler )
|
||||
if ( this.CommunityStackHandler ) // && this.CalloutStackHandler )
|
||||
return true;
|
||||
|
||||
const t = this,
|
||||
React = this.site.getReact(),
|
||||
createElement = React?.createElement,
|
||||
StackMod = this.web_munch.getModule('highlightstack');
|
||||
//CalloutMod = this.web_munch.getModule('calloutstack');
|
||||
|
||||
if ( ! createElement || ! StackMod )
|
||||
if ( ! createElement )
|
||||
return false;
|
||||
|
||||
this.CommunityStackHandler = function() {
|
||||
const stack = React.useContext(StackMod.stack),
|
||||
dispatch = React.useContext(StackMod.dispatch);
|
||||
/*if ( ! this.CalloutStackHandler && CalloutMod ) {
|
||||
this.CalloutStackHandler = function() {
|
||||
const stack = React.useContext(CalloutMod.stack);
|
||||
|
||||
t.community_stack = stack;
|
||||
t.community_dispatch = dispatch;
|
||||
t.callout_stack = stack;
|
||||
t.cleanCallouts();
|
||||
return null;
|
||||
}
|
||||
|
||||
t.cleanHighlights();
|
||||
this.CalloutSelector.forceUpdate();
|
||||
}*/
|
||||
|
||||
return null;
|
||||
if ( ! this.CommunityStackHandler && StackMod ) {
|
||||
this.CommunityStackHandler = function() {
|
||||
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();
|
||||
}
|
||||
|
||||
this.ChatContainer.forceUpdate();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1949,52 +2118,6 @@ export default class ChatHook extends Module {
|
|||
}
|
||||
|
||||
|
||||
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);
|
||||
|
|
|
@ -238,7 +238,13 @@ export default class ChatLine extends Module {
|
|||
const months = msg.sub_cumulative || msg.sub_months,
|
||||
setting = this.chat.context.get('chat.subs.show');
|
||||
|
||||
if ( !(setting === 3 || (setting === 1 && out && months > 1) || (setting === 2 && months > 1)) )
|
||||
let has_message;
|
||||
if (setting === 1 && months > 1) {
|
||||
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, current_user);
|
||||
has_message = tokens.length > 0;
|
||||
}
|
||||
|
||||
if ( !(setting === 3 || (setting === 1 && has_message && months > 1) || (setting === 2 && months > 1)) )
|
||||
return null;
|
||||
|
||||
const user = msg.user,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.player-controls__right-control-group button[aria-label*="alt+x"] {
|
||||
.player-controls__right-control-group button[aria-label*="alt+x"]:not(.ffz-core-button),
|
||||
.player-controls__right-control-group button[aria-disabled]:has(path[d="M8 9H6v2h2V9zm1 0h2v2H9V9zm5 0h-2v2h2V9z"]) {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -1,245 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// Featured Follow
|
||||
// ============================================================================
|
||||
|
||||
import Module from 'utilities/module';
|
||||
import {createElement} from 'utilities/dom';
|
||||
import {has} from 'utilities/object';
|
||||
|
||||
import FEATURED_QUERY from './featured_follow_query.gql';
|
||||
|
||||
import FEATURED_FOLLOW from './featured_follow_follow.gql';
|
||||
import FEATURED_UNFOLLOW from './featured_follow_unfollow.gql';
|
||||
|
||||
|
||||
export default class FeaturedFollow extends Module {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.should_enable = true;
|
||||
|
||||
this.inject('site');
|
||||
this.inject('site.fine');
|
||||
this.inject('site.apollo');
|
||||
this.inject('i18n');
|
||||
this.inject('metadata');
|
||||
this.inject('settings');
|
||||
this.inject('socket');
|
||||
this.inject('site.router');
|
||||
|
||||
this.inject('chat');
|
||||
|
||||
this.settings.add('metadata.featured-follow', {
|
||||
default: true,
|
||||
|
||||
ui: {
|
||||
path: 'Channel > Metadata >> Player',
|
||||
title: 'Featured Follow',
|
||||
description: 'Show a featured follow button with the currently featured users.',
|
||||
component: 'setting-check-box'
|
||||
},
|
||||
|
||||
changed: () => {
|
||||
this.metadata.updateMetadata('following');
|
||||
}
|
||||
});
|
||||
|
||||
this.follow_data = {};
|
||||
|
||||
this.socket.on(':command:follow_buttons', data => {
|
||||
for(const channel_login in data)
|
||||
if ( has(data, channel_login) )
|
||||
this.follow_data[channel_login] = data[channel_login];
|
||||
|
||||
if ( this.vueFeaturedFollow )
|
||||
this.vueFeaturedFollow.data.hasUpdate = true;
|
||||
|
||||
this.metadata.updateMetadata('following');
|
||||
});
|
||||
}
|
||||
|
||||
onEnable() {
|
||||
this.metadata.definitions.following = {
|
||||
order: 150,
|
||||
button: true,
|
||||
modview: true,
|
||||
|
||||
popup: async (data, tip, refresh_fn, add_callback) => {
|
||||
const vue = this.resolve('vue'),
|
||||
_featured_follow_vue = import(/* webpackChunkName: "featured-follow" */ './featured-follow.vue'),
|
||||
_follows = this.getFollowsForLogin(data.channel.login);
|
||||
|
||||
const [, featured_follows_vue, follows] = await Promise.all([vue.enable(), _featured_follow_vue, _follows]);
|
||||
|
||||
this._featured_follow_tip = tip;
|
||||
tip.element.classList.remove('tw-pd-1');
|
||||
tip.element.classList.add('ffz-balloon--lg');
|
||||
vue.component('featured-follow', featured_follows_vue.default);
|
||||
return this.buildFeaturedFollowMenu(vue, data.channel.login, follows, add_callback);
|
||||
},
|
||||
|
||||
label: data => {
|
||||
if (!this.settings.get('metadata.featured-follow') || !data || !data.channel || !data.channel.login)
|
||||
return null;
|
||||
|
||||
const follows = this.follow_data[data.channel.login];
|
||||
if (!follows || !Object.keys(follows).length) {
|
||||
if (!this.vueFeaturedFollow || !this.vueFeaturedFollow.data.hasUpdate) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.i18n.t('metadata.featured-follow.button.featured', 'Featured');
|
||||
},
|
||||
|
||||
icon: 'ffz-i-heart'
|
||||
};
|
||||
|
||||
this.metadata.updateMetadata('following');
|
||||
}
|
||||
|
||||
async getFollowsForLogin(login) {
|
||||
const follow_data = this.follow_data && this.follow_data[login];
|
||||
if ( ! follow_data || ! follow_data.length )
|
||||
return [];
|
||||
|
||||
const ap_data = await this.apollo.client.query({
|
||||
query: FEATURED_QUERY,
|
||||
variables: {
|
||||
logins: follow_data
|
||||
}
|
||||
}),
|
||||
follows = {};
|
||||
|
||||
for (const user of ap_data.data.users) {
|
||||
if ( ! user || ! user.id )
|
||||
continue;
|
||||
|
||||
follows[user.id] = {
|
||||
loading: false,
|
||||
error: false,
|
||||
id: user.id,
|
||||
login: user.login,
|
||||
displayName: user.displayName,
|
||||
avatar: user.profileImageURL,
|
||||
following: user.self.follower?.followedAt != null,
|
||||
disableNotifications: user.self.follower?.disableNotifications
|
||||
};
|
||||
}
|
||||
|
||||
return follows;
|
||||
}
|
||||
|
||||
buildFeaturedFollowMenu(vue, login, follows, add_close_callback) {
|
||||
const vueEl = new vue.Vue({
|
||||
el: createElement('div'),
|
||||
render: h => this.vueFeaturedFollow = h('featured-follow', {
|
||||
login,
|
||||
follows,
|
||||
hasUpdate: false,
|
||||
|
||||
followUser: id => this.followUser(follows, id),
|
||||
unfollowUser: id => this.unfollowUser(follows, id),
|
||||
updateNotificationStatus: (id, oldStatus) => this.updateNotificationStatus(follows, id, oldStatus),
|
||||
refresh: async () => {
|
||||
if ( ! this.vueFeaturedFollow || ! this.vueFeaturedFollow.data.hasUpdate )
|
||||
return;
|
||||
|
||||
this.vueFeaturedFollow.data.follows = await this.getFollowsForLogin(login);
|
||||
this.vueFeaturedFollow.data.hasUpdate = false;
|
||||
this._featured_follow_tip.update();
|
||||
|
||||
if (this.vueFeaturedFollow.data.follows.length === 0) this.metadata.updateMetadata('following');
|
||||
},
|
||||
route: channel => this.route(channel)
|
||||
}),
|
||||
});
|
||||
|
||||
add_close_callback(() => {
|
||||
this.vueFeaturedFollow = null;
|
||||
})
|
||||
|
||||
return vueEl.$el;
|
||||
}
|
||||
|
||||
async followUser(follows, id) {
|
||||
const f = follows[id];
|
||||
f.loading = true;
|
||||
|
||||
try {
|
||||
const ap_data = await this.apollo.client.mutate({
|
||||
mutation: FEATURED_FOLLOW,
|
||||
variables: {
|
||||
targetID: id,
|
||||
disableNotifications: false
|
||||
}
|
||||
});
|
||||
|
||||
const update = ap_data.data.followUser.follow;
|
||||
|
||||
f.loading = false;
|
||||
f.following = update.followedAt != null;
|
||||
f.disableNotifications = update.disableNotifications;
|
||||
|
||||
} catch(err) {
|
||||
this.log.warn('There was a problem following.', err);
|
||||
f.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
async updateNotificationStatus(follows, id, oldStatus) {
|
||||
const f = follows[id];
|
||||
f.loading = true;
|
||||
|
||||
// Immediate Feedback
|
||||
f.disableNotifications = ! oldStatus;
|
||||
|
||||
try {
|
||||
const ap_data = await this.apollo.client.mutate({
|
||||
mutation: FEATURED_FOLLOW,
|
||||
variables: {
|
||||
targetID: id,
|
||||
disableNotifications: !oldStatus
|
||||
}
|
||||
});
|
||||
|
||||
const update = ap_data.data.followUser.follow;
|
||||
|
||||
f.loading = false;
|
||||
f.following = update.followedAt != null;
|
||||
f.disableNotifications = update.disableNotifications;
|
||||
|
||||
} catch(err) {
|
||||
this.log.warn('There was a problem updating notification status.', err);
|
||||
f.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
async unfollowUser(follows, id) {
|
||||
const f = follows[id];
|
||||
f.loading = true;
|
||||
|
||||
try {
|
||||
await this.apollo.client.mutate({
|
||||
mutation: FEATURED_UNFOLLOW,
|
||||
variables: {
|
||||
targetID: id
|
||||
}
|
||||
});
|
||||
|
||||
f.loading = false;
|
||||
f.following = false;
|
||||
f.disableNotifications = false;
|
||||
|
||||
} catch(err) {
|
||||
this.log.warn('There was a problem unfollowing.', err);
|
||||
f.error = true;
|
||||
}
|
||||
}
|
||||
|
||||
route(channel) {
|
||||
this.router.navigate('user', { userName: channel });
|
||||
}
|
||||
}
|
|
@ -1166,7 +1166,7 @@ export class SourcedSet<T> {
|
|||
*/
|
||||
constructor(use_set = false, source_sorter?: StringSortFn) {
|
||||
this._use_set = use_set;
|
||||
this._cache = use_set ? new Set : [];
|
||||
this._cache = use_set ? new Set<T>() : [];
|
||||
this._sourceSortFn = source_sorter;
|
||||
}
|
||||
|
||||
|
@ -1221,7 +1221,7 @@ export class SourcedSet<T> {
|
|||
return;
|
||||
|
||||
const use_set = this._use_set,
|
||||
cache = this._cache = use_set ? new Set : [];
|
||||
cache = this._cache = use_set ? new Set<T>() : ([] as T[]);
|
||||
|
||||
if (!this._sorted_sources)
|
||||
this._sortSources();
|
||||
|
@ -1273,20 +1273,25 @@ export class SourcedSet<T> {
|
|||
this._sources = new Map;
|
||||
|
||||
const existing = this._sources.get(source);
|
||||
if ( existing )
|
||||
items = [...existing, ...items];
|
||||
if ( existing ) {
|
||||
// If there are existing items, add our new items
|
||||
// to the existing array.
|
||||
for(const item of items)
|
||||
if (! existing.includes(item))
|
||||
existing.push(item);
|
||||
|
||||
this._sources.set(source, items);
|
||||
} else {
|
||||
// If there aren't existing items, just insert our
|
||||
// array into the sources list. Also, clear the
|
||||
// sorted sources cache.
|
||||
this._sources.set(source, items);
|
||||
this._sorted_sources = null;
|
||||
}
|
||||
|
||||
// If we have a sort function and more than one source, then
|
||||
// we need to do a rebuild when making modifications.
|
||||
const need_sorting = this._sourceSortFn != null && this._sources.size > 1;
|
||||
|
||||
// If this is a new source, we need to clear the cache for
|
||||
// future rebuilds to include this.
|
||||
if ( ! existing )
|
||||
this._sorted_sources = null;
|
||||
|
||||
// If we need sorting, do a rebuild, otherwise go ahead
|
||||
// and add the items normally.
|
||||
if ( need_sorting )
|
||||
|
@ -1321,16 +1326,12 @@ export class SourcedSet<T> {
|
|||
|
||||
const existing = this._sources.has(source);
|
||||
this._sources.set(source, items);
|
||||
this._sorted_sources = null;
|
||||
|
||||
// If we have a sort function and more than one source, then
|
||||
// we need to do a rebuild when making modifications.
|
||||
const need_sorting = this._sourceSortFn != null && this._sources.size > 1;
|
||||
|
||||
// If this is a new source, we need to clear the cache for
|
||||
// future rebuilds to include this.
|
||||
if ( ! existing )
|
||||
this._sorted_sources = null;
|
||||
|
||||
// If we need sorting, or if we replaced an existing source,
|
||||
// then we need a rebuild. Otherwise, go ahead and add the
|
||||
// items normally.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue