1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 15:27:43 +00:00
* 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:
SirStendec 2024-03-29 13:33:14 -04:00
parent a02c14d84c
commit 38e557e809
11 changed files with 170 additions and 52 deletions

View file

@ -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",

View file

@ -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();
} }

View file

@ -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;

View file

@ -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)

View file

@ -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`;

View file

@ -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'));

View file

@ -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"

View file

@ -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;
} }

View file

@ -11,6 +11,7 @@
position: absolute !important; position: absolute !important;
} }
#root.ffz-has-dialog { #root {
min-height: 100%; min-height: 100%;
min-width: 100%;
} }

View file

@ -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.