mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-06-27 21:05:53 +00:00
4.72.0
* Fixed: The button to expand chat not appearing when using theater mode along with Swap Sidebars. * Fixed: Emote / link cards not appearing correctly when using moderator view. * API Added: `searchParentNode(input: InputNode, criteria: (node: ReactNode) => boolean)` method to `site.fine` for finding React internal nodes, useful for locating stateless components for extracting props. * API Added: `getRenderers()`, `getActions()` methods for `chat.actions` to get bulk data. * API Added: `getBadge(id: string | number)` method for `chat.badges` to get a badge. * API Added: `getUser(...).getBadges()` method for `chat` to get a user's badges. * API Added: The `action-editor` component for `chat.actions` has an additional `extra_appearance_editor` field for adding additional inputs. * API Changed: The `removeAction(...keys: string[])` and `removeRenderer(...keys: string[])` methods for `chat.actions` now support multiple keys to allow for more efficient removal.
This commit is contained in:
parent
a02c14d84c
commit
38e557e809
11 changed files with 170 additions and 52 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "frankerfacez",
|
"name": "frankerfacez",
|
||||||
"author": "Dan Salvato LLC",
|
"author": "Dan Salvato LLC",
|
||||||
"version": "4.71.0",
|
"version": "4.72.0",
|
||||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
|
|
@ -27,6 +27,11 @@ export default class Actions extends Module {
|
||||||
this.actions = {};
|
this.actions = {};
|
||||||
this.renderers = {};
|
this.renderers = {};
|
||||||
|
|
||||||
|
this.filterAction = (x) => x.appearance &&
|
||||||
|
this.renderers[x.appearance.type] &&
|
||||||
|
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
|
||||||
|
(! x.action || this.actions[x.action]);
|
||||||
|
|
||||||
this.settings.add('chat.actions.size', {
|
this.settings.add('chat.actions.size', {
|
||||||
default: 16,
|
default: 16,
|
||||||
ui: {
|
ui: {
|
||||||
|
@ -82,11 +87,7 @@ export default class Actions extends Module {
|
||||||
|
|
||||||
this.settings.add('chat.actions.hover', {
|
this.settings.add('chat.actions.hover', {
|
||||||
process: (ctx, val) =>
|
process: (ctx, val) =>
|
||||||
val.filter(x => x.appearance &&
|
val.filter(this.filterAction),
|
||||||
this.renderers[x.appearance.type] &&
|
|
||||||
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
|
|
||||||
(! x.action || this.actions[x.action])
|
|
||||||
),
|
|
||||||
|
|
||||||
default: [
|
default: [
|
||||||
{v: {action: 'pin', appearance: {type: 'icon', icon: 'ffz-i-pin'}, options: {}, display: {mod_icons: true}}},
|
{v: {action: 'pin', appearance: {type: 'icon', icon: 'ffz-i-pin'}, options: {}, display: {mod_icons: true}}},
|
||||||
|
@ -119,11 +120,7 @@ export default class Actions extends Module {
|
||||||
this.settings.add('chat.actions.inline', {
|
this.settings.add('chat.actions.inline', {
|
||||||
// Filter out actions
|
// Filter out actions
|
||||||
process: (ctx, val) =>
|
process: (ctx, val) =>
|
||||||
val.filter(x => x.appearance &&
|
val.filter(this.filterAction),
|
||||||
this.renderers[x.appearance.type] &&
|
|
||||||
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
|
|
||||||
(! x.action || this.actions[x.action])
|
|
||||||
),
|
|
||||||
|
|
||||||
default: [
|
default: [
|
||||||
{v: {action: 'ban', appearance: {type: 'icon', icon: 'ffz-i-block'}, options: {}, display: {mod: true, mod_icons: true, deleted: false}}},
|
{v: {action: 'ban', appearance: {type: 'icon', icon: 'ffz-i-block'}, options: {}, display: {mod: true, mod_icons: true, deleted: false}}},
|
||||||
|
@ -157,11 +154,7 @@ export default class Actions extends Module {
|
||||||
this.settings.add('chat.actions.user-context', {
|
this.settings.add('chat.actions.user-context', {
|
||||||
// Filter out actions
|
// Filter out actions
|
||||||
process: (ctx, val) =>
|
process: (ctx, val) =>
|
||||||
val.filter(x => x.type || (x.appearance &&
|
val.filter(x => x.type || this.filterAction(x)),
|
||||||
this.renderers[x.appearance.type] &&
|
|
||||||
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
|
|
||||||
(! x.action || this.actions[x.action])
|
|
||||||
)),
|
|
||||||
|
|
||||||
default: [],
|
default: [],
|
||||||
type: 'array_merge',
|
type: 'array_merge',
|
||||||
|
@ -187,11 +180,7 @@ export default class Actions extends Module {
|
||||||
this.settings.add('chat.actions.room', {
|
this.settings.add('chat.actions.room', {
|
||||||
// Filter out actions
|
// Filter out actions
|
||||||
process: (ctx, val) =>
|
process: (ctx, val) =>
|
||||||
val.filter(x => x.type || (x.appearance &&
|
val.filter(x => x.type || this.filterAction(x)),
|
||||||
this.renderers[x.appearance.type] &&
|
|
||||||
(! this.renderers[x.appearance.type].load || this.renderers[x.appearance.type].load(x.appearance)) &&
|
|
||||||
(! x.action || this.actions[x.action])
|
|
||||||
)),
|
|
||||||
|
|
||||||
default: [],
|
default: [],
|
||||||
type: 'array_merge',
|
type: 'array_merge',
|
||||||
|
@ -316,20 +305,24 @@ export default class Actions extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( is_dev ) {
|
if ( is_dev ) {
|
||||||
overrides.removeAction = key => {
|
overrides.removeAction = (...key) => {
|
||||||
const existing = this.actions[key];
|
for(const entry of key) {
|
||||||
if ( existing && existing.__source !== addon_id )
|
const existing = this.actions[entry];
|
||||||
module.log.warn('[DEV-CHECK] Removed un-owned action with actions.removeAction:', key, ' owner:', existing.__source ?? 'ffz');
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned action with actions.removeAction:', entry, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
}
|
||||||
|
|
||||||
return this.removeAction(key);
|
return this.removeAction(...key);
|
||||||
};
|
};
|
||||||
|
|
||||||
overrides.removeRenderer = key => {
|
overrides.removeRenderer = (...key) => {
|
||||||
const existing = this.renderers[key];
|
for(const entry of key) {
|
||||||
if ( existing && existing.__source !== addon_id )
|
const existing = this.renderers[entry];
|
||||||
module.log.warn('[DEV-CHECK] Removed un-owned renderer with actions.removeRenderer:', key, ' owner:', existing.__source ?? 'ffz');
|
if ( existing && existing.__source !== addon_id )
|
||||||
|
module.log.warn('[DEV-CHECK] Removed un-owned renderer with actions.removeRenderer:', entry, ' owner:', existing.__source ?? 'ffz');
|
||||||
|
}
|
||||||
|
|
||||||
return this.removeRenderer(key);
|
return this.removeRenderer(...key);
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings.actions = 'Please use addAction() or removeAction()';
|
warnings.actions = 'Please use addAction() or removeAction()';
|
||||||
|
@ -367,22 +360,49 @@ export default class Actions extends Module {
|
||||||
this._updateContexts();
|
this._updateContexts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getActions() {
|
||||||
|
return {...this.actions}
|
||||||
|
}
|
||||||
|
|
||||||
removeAction(key) {
|
getAction(key) {
|
||||||
if ( ! has(this.actions, key) )
|
return this.actions[key] ?? null;
|
||||||
return;
|
}
|
||||||
|
|
||||||
delete this.actions[key];
|
getRenderer(key) {
|
||||||
this._updateContexts();
|
return this.renderers[key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRenderers() {
|
||||||
|
return {...this.renderers}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAction(...keys) {
|
||||||
|
let changed = false;
|
||||||
|
for(const entry of keys) {
|
||||||
|
if ( ! has(this.actions, entry) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
delete this.actions[entry];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( changed )
|
||||||
|
this._updateContexts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
removeRenderer(key) {
|
removeRenderer(...keys) {
|
||||||
if ( ! has(this.renderers, key) )
|
let changed = false;
|
||||||
return;
|
for(const entry of keys) {
|
||||||
|
if ( ! has(this.renderers, entry) )
|
||||||
|
return;
|
||||||
|
|
||||||
delete this.renderers[key];
|
delete this.renderers[entry];
|
||||||
this._updateContexts();
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( changed )
|
||||||
|
this._updateContexts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1223,6 +1223,11 @@ export default class Badges extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getBadge(badge_id) {
|
||||||
|
return this.badges[badge_id] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
removeBadge(badge_id, generate_css = true) {
|
removeBadge(badge_id, generate_css = true) {
|
||||||
if ( ! this.badges[badge_id] )
|
if ( ! this.badges[badge_id] )
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -154,6 +154,13 @@ export default class User {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getBadges() {
|
||||||
|
if ( this.badges )
|
||||||
|
return [...this.badges._cache];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
getBadge(badge_id: string) {
|
getBadge(badge_id: string) {
|
||||||
if ( this.badges )
|
if ( this.badges )
|
||||||
for(const badge of this.badges._cache)
|
for(const badge of this.badges._cache)
|
||||||
|
|
|
@ -370,8 +370,8 @@ export default {
|
||||||
if ( ! parent )
|
if ( ! parent )
|
||||||
parent = document.body;
|
parent = document.body;
|
||||||
|
|
||||||
const box = el.getBoundingClientRect(),
|
const box = el.getBoundingClientRect();
|
||||||
pbox = parent.getBoundingClientRect();
|
let pbox = parent.getBoundingClientRect();
|
||||||
|
|
||||||
if ( box.top < pbox.top ) {
|
if ( box.top < pbox.top ) {
|
||||||
el.style.top = `${el.offsetTop + (pbox.top - box.top)}px`;
|
el.style.top = `${el.offsetTop + (pbox.top - box.top)}px`;
|
||||||
|
@ -447,4 +447,4 @@ export default {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -79,6 +79,12 @@
|
||||||
v-if="renderer"
|
v-if="renderer"
|
||||||
v-model="edit_data.appearance"
|
v-model="edit_data.appearance"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<component
|
||||||
|
:is="extra_appearance"
|
||||||
|
v-if="extra_appearance"
|
||||||
|
v-model="edit_data.appearance"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
<section class="tw-mg-t-1 tw-border-t tw-pd-t-1">
|
||||||
|
@ -128,6 +134,28 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="has_following" class="tw-flex tw-align-items-center">
|
||||||
|
<label for="vis_following">
|
||||||
|
{{ t('setting.actions.edit-visible.following', 'Following User') }}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<select
|
||||||
|
id="vis_following"
|
||||||
|
v-model="edit_data.display.following"
|
||||||
|
class="tw-border-radius-medium tw-font-size-6 tw-full-width ffz-select tw-pd-l-1 tw-pd-r-3 tw-pd-y-05 tw-mg-y-05"
|
||||||
|
>
|
||||||
|
<option :value="undefined" selected>
|
||||||
|
{{ t('setting.unset', 'Unset') }}
|
||||||
|
</option>
|
||||||
|
<option :value="true">
|
||||||
|
{{ t('setting.true', 'True') }}
|
||||||
|
</option>
|
||||||
|
<option :value="false">
|
||||||
|
{{ t('setting.false', 'False') }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="has_message" class="tw-flex tw-align-items-center">
|
<div v-if="has_message" class="tw-flex tw-align-items-center">
|
||||||
<label for="vis_deleted">
|
<label for="vis_deleted">
|
||||||
{{ t('setting.actions.edit-visible.deleted', 'Message Deleted') }}
|
{{ t('setting.actions.edit-visible.deleted', 'Message Deleted') }}
|
||||||
|
@ -459,7 +487,7 @@ import {has, maybe_call, deep_copy} from 'utilities/object';
|
||||||
let id = 0;
|
let id = 0;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['vuectx', 'action', 'data', 'inline', 'mod_icons', 'context', 'modifiers', 'hover_modifier'],
|
props: ['vuectx', 'action', 'data', 'inline', 'mod_icons', 'extra_appearance', 'context', 'modifiers', 'hover_modifier'],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -487,6 +515,10 @@ export default {
|
||||||
return this.context && this.context.includes('message')
|
return this.context && this.context.includes('message')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
has_following() {
|
||||||
|
return this.context && this.context.includes('following')
|
||||||
|
},
|
||||||
|
|
||||||
has_mode() {
|
has_mode() {
|
||||||
return this.context && this.context.includes('room-mode')
|
return this.context && this.context.includes('room-mode')
|
||||||
},
|
},
|
||||||
|
@ -642,6 +674,12 @@ export default {
|
||||||
if ( disp.disable )
|
if ( disp.disable )
|
||||||
return this.t('setting.actions.visible.never', 'never');
|
return this.t('setting.actions.visible.never', 'never');
|
||||||
|
|
||||||
|
if ( disp.following === true )
|
||||||
|
out.push(this.t('setting.actions.visible.following', 'when following user'));
|
||||||
|
|
||||||
|
else if ( disp.following === false )
|
||||||
|
out.push(this.t('setting.actions.visible.unfollowing', 'when not following user'));
|
||||||
|
|
||||||
if ( disp.mod === true )
|
if ( disp.mod === true )
|
||||||
out.push(this.t('setting.actions.visible.mod', 'when moderator'));
|
out.push(this.t('setting.actions.visible.mod', 'when moderator'));
|
||||||
|
|
||||||
|
@ -824,4 +862,4 @@ export default {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -376,6 +376,7 @@
|
||||||
:inline="item.inline"
|
:inline="item.inline"
|
||||||
:mod_icons="has_icons"
|
:mod_icons="has_icons"
|
||||||
:context="item.context"
|
:context="item.context"
|
||||||
|
:extra_appearance="item.extra_appearance_editor"
|
||||||
:vuectx="context"
|
:vuectx="context"
|
||||||
:modifiers="item.modifiers"
|
:modifiers="item.modifiers"
|
||||||
:hover_modifier="item.hover_modifier"
|
:hover_modifier="item.hover_modifier"
|
||||||
|
@ -817,4 +818,4 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -10,6 +10,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-column--theatre {
|
||||||
|
/* 1 higher than default Twitch, to make the expand button appear properly */
|
||||||
|
z-index: 3001;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-root {
|
.channel-root {
|
||||||
width: unset !important;
|
width: unset !important;
|
||||||
}
|
}
|
||||||
|
@ -98,4 +103,4 @@ body .whispers--theatre-mode.whispers--right-column-expanded-beside {
|
||||||
left: calc(var(--ffz-chat-width) + 5rem) !important;
|
left: calc(var(--ffz-chat-width) + 5rem) !important;
|
||||||
right: 40rem !important;
|
right: 40rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#root.ffz-has-dialog {
|
#root {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
min-width: 100%;
|
||||||
|
}
|
||||||
|
|
|
@ -12,4 +12,4 @@
|
||||||
//@import 'host_options';
|
//@import 'host_options';
|
||||||
@import 'featured_follow';
|
@import 'featured_follow';
|
||||||
@import 'mod_card';
|
@import 'mod_card';
|
||||||
@import 'easteregg';
|
@import 'easteregg';
|
||||||
|
|
|
@ -270,6 +270,47 @@ export default class Fine extends Module<'site.fine', FineEvents> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchParentNode<TNode extends ReactNode = ReactNode>(
|
||||||
|
input: InputNode,
|
||||||
|
criteria: NodeCriteria,
|
||||||
|
max_depth = 15,
|
||||||
|
depth = 0,
|
||||||
|
traverse_roots = true
|
||||||
|
): TNode | null {
|
||||||
|
if ( depth > max_depth )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const node = this.resolveNode(input);
|
||||||
|
|
||||||
|
// If we don't have a node, then stop.
|
||||||
|
if ( ! node )
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if ( criteria(node) )
|
||||||
|
return node as TNode;
|
||||||
|
|
||||||
|
if ( node.return ) {
|
||||||
|
const result = this.searchParentNode<TNode>(node.return, criteria, max_depth, depth+1, traverse_roots);
|
||||||
|
if ( result )
|
||||||
|
return result as TNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stupid code for traversing up into another React root.
|
||||||
|
const inst = node.stateNode;
|
||||||
|
if ( traverse_roots && (inst as any)?.containerInfo ) {
|
||||||
|
const parent = (inst as any).containerInfo?.parentElement as Node | undefined,
|
||||||
|
parent_node = parent && this.getReactInstance(parent);
|
||||||
|
|
||||||
|
if ( parent_node ) {
|
||||||
|
const result = this.searchParentNode<TNode>(parent_node, criteria, max_depth, depth+1, traverse_roots);
|
||||||
|
if ( result )
|
||||||
|
return result as TNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
searchNode<TNode extends ReactNode = ReactNode>(
|
searchNode<TNode extends ReactNode = ReactNode>(
|
||||||
input: InputNode,
|
input: InputNode,
|
||||||
criteria: NodeCriteria,
|
criteria: NodeCriteria,
|
||||||
|
@ -286,7 +327,7 @@ export default class Fine extends Module<'site.fine', FineEvents> {
|
||||||
if ( ! node )
|
if ( ! node )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if ( node && criteria(node) )
|
if ( criteria(node) )
|
||||||
return node as TNode;
|
return node as TNode;
|
||||||
|
|
||||||
// If the node has disabled scanning, don't scan into its children.
|
// If the node has disabled scanning, don't scan into its children.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue