1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-07-28 21:48:31 +00:00
* Added: Chat Action for editing a user's displayed name and color. Only applies to chat.
* Changed: Re-enable the setting for hiding offline channels from the side-bar.
* Changed: Updated dependencies.
* Fixed: Issue with multiple copies of FFZ emotes appearing in tab-completion.
* Fixed: Issue with tab-completion crashing sometimes in mod view. (Closes #839)
* Fixed: Support for swapping sidebars with the latest Twitch changes.
* Fixed: Hiding Recommended Channels from the sidebar. (Closes #840)
* Removed: Setting for hiding Recommended Friends from the sidebar, since that section no longer exists.
This commit is contained in:
SirStendec 2020-07-10 20:08:29 -04:00
parent 50378bb3dc
commit 7638a885f2
16 changed files with 2862 additions and 2420 deletions

4798
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "frankerfacez",
"author": "Dan Salvato LLC",
"version": "4.20.4",
"version": "4.20.5",
"description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0",
"scripts": {
@ -24,38 +24,38 @@
"font:update": "node bin/update_fonts"
},
"devDependencies": {
"@babel/core": "^7.7.7",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.7",
"@babel/plugin-proposal-optional-chaining": "^7.7.5",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-react-jsx": "^7.7.7",
"@babel/core": "^7.10.4",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
"@babel/plugin-proposal-optional-chaining": "^7.10.4",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-transform-react-jsx": "^7.10.4",
"@ffz/fontello-cli": "^1.0.3",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"cross-env": "^6.0.3",
"cross-env": "^7.0.2",
"css-loader": "^3.1.0",
"eslint": "^6.8.0",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-vue": "^6.1.2",
"eslint": "^7.3.1",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-vue": "^6.2.2",
"extract-loader": "^2.0.1",
"file-loader": "^4.1.0",
"json-loader": "^0.5.7",
"node-sass": "^4.12.0",
"node-sass": "^4.14.1",
"raw-loader": "^3.1.0",
"rimraf": "^3.0.0",
"rimraf": "^3.0.2",
"sass-loader": "^7.1.0",
"semver": "^7.1.1",
"terser-webpack-plugin": "^2.3.1",
"semver": "^7.3.2",
"terser-webpack-plugin": "^3.0.6",
"vue": "^2.6.11",
"vue-loader": "^15.8.3",
"vue-observe-visibility": "^0.4.6",
"vue-template-compiler": "^2.6.11",
"webpack": "^4.41.5",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.10.1",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "^2.2.0",
"webpack-merge": "^4.2.2"
},
@ -65,26 +65,26 @@
},
"dependencies": {
"@ffz/icu-msgparser": "^1.0.2",
"crypto-js": "^3.1.9-1",
"dayjs": "^1.8.18",
"displacejs": "^1.4.0",
"crypto-js": "^4.0.0",
"dayjs": "^1.8.29",
"displacejs": "^1.4.1",
"emoji-regex": "^8.0.0",
"file-saver": "^2.0.1",
"graphql": "^14.5.8",
"graphql-tag": "^2.9.1",
"graphql": "^15.2.0",
"graphql-tag": "^2.10.3",
"js-cookie": "^2.2.1",
"markdown-it": "^9.0.1",
"markdown-it-link-attributes": "^2.1.0",
"markdown-it": "^11.0.0",
"markdown-it-link-attributes": "^3.0.0",
"path-to-regexp": "^3.0.0",
"popper.js": "^1.14.3",
"raven-js": "^3.24.2",
"react": "^16.4.1",
"safe-regex": "^2.0.2",
"sortablejs": "^1.10.0-rc3",
"react": "^16.13.1",
"safe-regex": "^2.1.1",
"sortablejs": "^1.10.2",
"sourcemapped-stacktrace": "^1.1.11",
"text-diff": "^1.0.1",
"vue-clickaway": "^2.2.2",
"vue-color": "^2.4.6",
"vuedraggable": "^2.23.0"
"vue-color": "^2.7.1",
"vuedraggable": "^2.23.2"
}
}

View file

@ -328,7 +328,10 @@ export default class Actions extends Module {
fp.destroy();
}
target._ffz_destroy = target._ffz_outside = null;
if ( target._ffz_on_destroy )
target._ffz_on_destroy();
target._ffz_destroy = target._ffz_outside = target._ffz_on_destroy = null;
}
const parent = document.body.querySelector('#root>div') || document.body,
@ -496,8 +499,8 @@ export default class Actions extends Module {
let line = null;
const handle_click = event => {
tip.hide();
this.handleClick(event);
tip.hide();
};
for(const data of actions) {

View file

@ -1,6 +1,32 @@
'use strict';
import {createElement} from 'utilities/dom';
// ============================================================================
// Edit Overrides
// ============================================================================
export const edit_overrides = {
presets: [{
appearance: {
type: 'icon',
icon: 'ffz-i-pencil'
}
}],
required_context: ['user'],
title: 'Change Name & Color',
description: 'Allows you to set local overrides for a user\'s name and color in chat.',
tooltip() {
return this.i18n.t('chat.actions.edit_overrides', 'Change Name & Color')
},
click(event, data) {
//const ref = makeReference(event.clientX, event.clientY);
this.resolve('chat.overrides').renderUserEditor(data.user, event.target);
}
}
// ============================================================================
// Open URL

View file

@ -0,0 +1,129 @@
<template>
<div class="ffz-override-editor tw-c-background-base tw-c-text-base tw-pd-05 tw-pd-l-1">
<div class="tw-flex tw-align-items-center tw-pd-b-05 tw-border-b tw-mg-b-05">
<div class="tw-flex-grow-1">
{{ t('chat.overrides.editing', 'Editing {user.login}...', {user}) }}
</div>
<button
class="tw-mg-l-05 tw-button tw-button--text"
@click="close"
>
<span class="tw-button__text ffz-i-window-close" />
</button>
</div>
<div class="tw-flex tw-align-items-center">
<label for="user-name" class="tw-mg-r-1">
{{ t('chat.overrides.name', 'Name') }}
</label>
<input
id="user-name"
ref="name"
class="tw-border-radius-medium tw-font-size-6 tw-pd-x-1 tw-pd-y-05 tw-input tw-flex-grow-1"
:value="name"
@change="updateName"
>
<button
class="tw-mg-l-05 tw-button tw-button--text tw-tooltip-wrapper"
:class="{'tw-button--disabled': name == null}"
@click="clearName"
>
<span class="tw-button__text ffz-i-cancel" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</div>
</button>
</div>
<div class="tw-flex tw-align-items-center">
<label for="user-color" class="tw-mg-r-1">
{{ t('chat.overrides.color', 'Color') }}
</label>
<color-picker
id="user-color"
ref="color"
:alpha="false"
:nullable="true"
:value="editColor"
@input="updateColor"
/>
<button
class="tw-mg-l-05 tw-button tw-button--text tw-tooltip-wrapper"
:class="{'tw-button--disabled': color == null}"
@click="clearColor"
>
<span class="tw-button__text ffz-i-cancel" />
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('setting.reset', 'Reset to Default') }}
</div>
</button>
</div>
</div>
</template>
<script>
import { debounce } from 'utilities/object';
export default {
data() {
return this.$vnode.data
},
computed: {
editColor() {
if ( ! this.color )
return '';
return this.color;
}
},
created() {
this.updateName = debounce(this.updateName, 250);
this.updateColor = debounce(this.updateColor, 250);
},
updated() {
this.updateTip();
},
methods: {
clearName() {
this.name = null;
this.deleteName();
},
updateName() {
const value = this.$refs.name.value;
if ( value == null || ! value.length ) {
this.clearName();
return;
}
this.name = value;
this.setName(value);
},
clearColor() {
this.color = null;
this.deleteColor();
},
updateColor(value) {
if ( value == null || ! value.length ) {
this.clearColor();
return;
}
this.color = value;
this.setColor(value);
}
}
}
</script>

View file

@ -5,6 +5,8 @@
// ============================================================================
import Module from 'utilities/module';
import { createElement, ClickOutside } from 'utilities/dom';
import Tooltip from 'utilities/tooltip';
export default class Overrides extends Module {
@ -12,14 +14,126 @@ export default class Overrides extends Module {
super(...args);
this.inject('settings');
this.color_cache = null;
this.name_cache = null;
/*this.settings.addUI('chat.overrides', {
path: 'Chat > Overrides @{"profile_warning": false}',
component: 'chat-overrides',
on: (...args) => this.on(...args),
off: (...args) => this.off(...args),
setColor: (...args) => this.setColor(...args),
setName: (...args) => this.setName(...args),
deleteColor: id => this.deleteColor(id),
deleteName: id => this.deleteName(id),
getColors: () => this.colors,
getNames: () => this.names
});*/
}
onEnable() {
this.settings.provider.on('changed', this.onProviderChange, this);
}
renderUserEditor(user, target) {
let outside, popup, ve;
const destroy = () => {
const o = outside, p = popup, v = ve;
outside = popup = ve = null;
if ( o )
o.destroy();
if ( p )
p.destroy();
if ( v )
v.$destroy();
}
const parent = document.body.querySelector('#root>div') || document.body;
popup = new Tooltip(parent, [], {
logger: this.log,
manual: true,
live: false,
html: true,
hover_events: true,
no_update: true,
no_auto_remove: true,
tooltipClass: 'ffz-action-balloon tw-balloon tw-block tw-border tw-elevation-1 tw-border-radius-small tw-c-background-base',
arrowClass: '', //tw-balloon__tail tw-overflow-hidden tw-absolute',
arrowInner: '', //tw-balloon__tail-symbol tw-border-t tw-border-r tw-border-b tw-border-l tw-border-radius-small tw-c-background-base tw-absolute',
innerClass: '',
popper: {
placement: 'bottom',
modifiers: {
preventOverflow: {
boundariesElement: parent
},
flip: {
behavior: ['bottom', 'top', 'left', 'right']
}
}
},
content: async (t, tip) => {
const vue = this.resolve('vue'),
_editor = import(/* webpackChunkName: "overrides" */ './override-editor.vue');
const [, editor] = await Promise.all([vue.enable(), _editor]);
vue.component('override-editor', editor.default);
ve = new vue.Vue({
el: createElement('div'),
render: h => h('override-editor', {
user,
name: this.getName(user.id),
color: this.getColor(user.id),
updateTip: () => tip.update(),
setColor: val => this.setColor(user.id, val),
deleteColor: () => this.deleteColor(user.id),
setName: val => this.setName(user.id, val),
deleteName: () => this.deleteName(user.id),
close: () => tip.hide()
})
});
return ve.$el;
},
onShow: async (t, tip) => {
await tip.waitForDom();
requestAnimationFrame(() => {
outside = new ClickOutside(tip.outer, destroy)
});
},
onMove: (target, tip, event) => {
this.emit('tooltips:mousemove', target, tip, event)
},
onLeave: (target, tip, event) => {
this.emit('tooltips:leave', target, tip, event);
},
onHide: destroy
});
popup._enter(target);
}
onProviderChange(key) {
if ( key === 'overrides.colors' )
this.loadColors();

View file

@ -613,6 +613,8 @@ export default class MainMenu extends Module {
deleteProfile: profile => settings.deleteProfile(profile),
getFFZ: () => t.resolve('core'),
context: {
_users: 0,

View file

@ -37,11 +37,11 @@ export default class Channel extends Module {
}
});
/*this.SideNav = this.elemental.define(
this.SideNav = this.elemental.define(
'side-nav', '.side-bar-contents .side-nav-section:first-child',
null,
{childNodes: true, subtree: true}, 1
);*/
);
this.ChannelRoot = this.elemental.define(
'channel-root', '.channel-root',
@ -59,8 +59,9 @@ export default class Channel extends Module {
onEnable() {
this.updateChannelColor();
//this.SideNav.on('mount', this.updateHidden, this);
//this.SideNav.on('mutate', this.updateHidden, this);
this.SideNav.on('mount', this.updateHidden, this);
this.SideNav.on('mutate', this.updateHidden, this);
this.SideNav.each(el => this.updateHidden(el));
this.ChannelRoot.on('mount', this.updateRoot, this);
this.ChannelRoot.on('mutate', this.updateRoot, this);
@ -87,18 +88,21 @@ export default class Channel extends Module {
}
}
/*updateHidden(el) { // eslint-disable-line class-methods-use-this
updateHidden(el) { // eslint-disable-line class-methods-use-this
if ( ! el._ffz_raf )
el._ffz_raf = requestAnimationFrame(() => {
el._ffz_raf = null;
const nodes = el.querySelectorAll('.side-nav-card__avatar--offline');
const nodes = el.querySelectorAll('.side-nav-card');
for(const node of nodes) {
const par = node.closest('.tw-transition');
if ( par && el.contains(par) )
par.classList.add('tw-hide');
const react = this.fine.getReactInstance(node),
props = react?.return?.return?.return?.memoizedProps;
const offline = props?.offline ?? node.querySelector('.side-nav-card__avatar--offline') != null;
node.classList.toggle('ffz--offline-side-nav', offline);
}
});
}*/
}
updateSubscription(login) {
if ( this._subbed_login === login )

View file

@ -405,16 +405,23 @@ export default class Input extends Module {
}
inst.doesEmoteMatchTerm = function(emote, term) {
const emote_name = emote.name || emote.token,
emote_lower = emote_name.toLowerCase(),
term_lower = term.toLowerCase();
const emote_name = emote.name || emote.token;
if ( ! emote_name )
return false;
let emote_lower = emote.tokenLower;
if ( ! emote_lower )
emote_lower = emote_name.toLowerCase();
const term_lower = term.toLowerCase();
if (emote_lower.startsWith(term_lower))
return true;
const idx = emote_name.indexOf(term.charAt(0).toUpperCase());
if (idx !== -1)
return emote_lower.slice(idx + 1).startsWith(term_lower.slice(1));
return false;
}
inst.getMatchedEmotes = function(input) {
@ -532,8 +539,12 @@ export default class Input extends Module {
continue;
const id = emote.id,
replacement = REPLACEMENTS[id];
token = KNOWN_CODES[emote.token] || emote.token;
if ( ! token )
continue;
const replacement = REPLACEMENTS[id];
let src, srcSet;
if ( replacement && this.chat.context.get('chat.fix-bad-emotes') ) {
@ -548,7 +559,8 @@ export default class Input extends Module {
out.push({
id,
setID: set.id,
token: KNOWN_CODES[emote.token] || emote.token,
token,
tokenLower: token.toLowerCase(),
srcSet,
favorite: favorites.includes(id)
});
@ -653,7 +665,8 @@ export default class Input extends Module {
const out = [],
hidden_sets = this.settings.provider.get('emote-menu.hidden-sets'),
has_hidden = Array.isArray(hidden_sets) && hidden_sets.length > 0;
has_hidden = Array.isArray(hidden_sets) && hidden_sets.length > 0,
added_emotes = new Set;
for(const set of sets) {
if ( ! set || ! set.emotes )
@ -669,12 +682,18 @@ export default class Input extends Module {
favorites = this.emotes.getFavorites(source);
for(const emote of Object.values(set.emotes)) {
if ( ! emote || ! emote.id || hidden_emotes.includes(emote.id) )
if ( ! emote || ! emote.id || emote.hidden || hidden_emotes.includes(emote.id) || added_emotes.has(emote.name) )
continue;
if ( ! emote.name )
continue;
added_emotes.add(emote.name);
out.push({
id: `${source}-${emote.id}`,
token: emote.name,
tokenLower: emote.name.toLowerCase(),
srcSet: emote.srcSet,
favorite: favorites.includes(emote.id)
});
@ -711,11 +730,10 @@ export default class Input extends Module {
return [];
const search = input.startsWith(':') ? input.slice(1) : input,
results = [],
added_emotes = new Set();
results = [];
for(const emote of emotes) {
if ( inst.doesEmoteMatchTerm(emote, search) && ! added_emotes.has(emote.name) ) {
if ( inst.doesEmoteMatchTerm(emote, search) )
results.push({
current: input,
replacement: emote.token,
@ -724,7 +742,6 @@ export default class Input extends Module {
count: 0 // TODO: Count stuff?
});
}
}
return results;

View file

@ -333,7 +333,7 @@ other {# messages were deleted by a moderator.}
onContextMenu: t.actions.handleUserContext
}, override_name ? [
e('span', {
className: 'chat-author__display_name'
className: 'chat-author__display-name'
}, override_name),
e('div', {
className: 'tw-tooltip tw-tooltip--down tw-tooltip--align-center'

View file

@ -13,12 +13,12 @@ const STYLE_VALIDATOR = document.createElement('span');
const CLASSES = {
'top-discover': '.navigation-link[data-a-target="discover-link"]',
'side-nav': '.side-nav',
'side-rec-channels': '.side-nav .recommended-channels',
'side-rec-friends': '.side-nav .recommended-friends',
'side-rec-channels': '.side-nav .recommended-channels,.side-nav .side-nav-section + .side-nav-section',
//'side-rec-friends': '.side-nav .recommended-friends',
'side-friends': '.side-nav .online-friends',
'side-closed-friends': '.side-nav--collapsed .online-friends',
'side-closed-rec-channels': '.side-nav--collapsed .recommended-channels',
//'side-offline-channels': '.side-nav-card__link[href*="/videos/"],.side-nav-card[href*="/videos/"]',
'side-closed-rec-channels': '.side-nav--collapsed .recommended-channels,.side-nav--collapsed .side-nav-section + .side-nav-section',
'side-offline-channels': '.side-nav-card.ffz--offline-side-nav',
'side-rerun-channels': '.side-nav .ffz--side-nav-card-rerun',
'community-highlights': '.community-highlight-stack__card',
@ -37,7 +37,7 @@ const CLASSES = {
'dir-live-ind': '.preview-card[data-ffz-type="live"] .tw-channel-status-text-indicator,.live-channel-card:not([data-a-target*="host"]) .stream-type-indicator.stream-type-indicator--live,.stream-thumbnail__card .stream-type-indicator.stream-type-indicator--live,.preview-card .stream-type-indicator.stream-type-indicator--live,.preview-card .preview-card-stat.preview-card-stat--live',
'profile-hover': '.preview-card .tw-relative:hover .ffz-channel-avatar',
'not-live-bar': 'div[data-test-selector="non-live-video-banner-layout"]',
'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator',
'channel-live-ind': '.channel-header__user .tw-channel-status-text-indicator,.channel-info-content .user-avatar-animated__live',
'celebration': 'body .celebration__overlay',
'mod-view': '.chat-input__buttons-container .tw-core-button[href*="/moderator"]'
};
@ -153,7 +153,7 @@ export default class CSSTweaks extends Module {
}
});
this.settings.add('layout.side-nav.show-rec-friends', {
/*this.settings.add('layout.side-nav.show-rec-friends', {
default: true,
ui: {
path: 'Appearance > Layout >> Side Navigation',
@ -161,9 +161,9 @@ export default class CSSTweaks extends Module {
component: 'setting-check-box'
},
changed: val => this.toggleHide('side-rec-friends', !val)
});
});*/
/*this.settings.add('layout.side-nav.hide-offline', {
this.settings.add('layout.side-nav.hide-offline', {
default: false,
ui: {
path: 'Appearance > Layout >> Side Navigation',
@ -171,7 +171,7 @@ export default class CSSTweaks extends Module {
component: 'setting-check-box'
},
changed: val => this.toggleHide('side-offline-channels', val)
});*/
});
this.settings.add('layout.side-nav.rerun-style', {
default: 1,
@ -351,8 +351,8 @@ export default class CSSTweaks extends Module {
this.toggle('hide-side-nav-avatars', ! this.settings.get('layout.side-nav.show-avatars'));
this.toggle('hide-side-nav', !this.settings.get('layout.side-nav.show'));
this.toggleHide('side-rec-friends', !this.settings.get('layout.side-nav.show-rec-friends'));
//this.toggleHide('side-offline-channels', this.settings.get('layout.side-nav.hide-offline'));
//this.toggleHide('side-rec-friends', !this.settings.get('layout.side-nav.show-rec-friends'));
this.toggleHide('side-offline-channels', this.settings.get('layout.side-nav.hide-offline'));
this.toggleHide('prime-offers', !this.settings.get('layout.prime-offers'));
this.toggleHide('top-discover', !this.settings.get('layout.discover'));

View file

@ -1,21 +1,24 @@
.twilight-main { order: 2 }
#sideNav,
.side-nav { order: 3 }
.right-column {
order: 1;
z-index: 2;
}
.side-nav__toggle-visibility {
right: unset !important;
left: -2.5rem;
z-index: 10 !important;
svg { transform: rotate(180deg) }
#sideNav .collapse-toggle {
svg {
transform: rotate(180deg);
}
}
.right-column__toggle-visibility {
//left: unset !important;
//right: -2.5rem;
//z-index: 10 !important;
.tw-tooltip--left {
left: 100% !important;
right: auto !important;
margin-left: 6px;
}
svg { transform: rotate(180deg) }
.right-column--collapsed & {
@ -24,14 +27,6 @@
}
}
.channel-header {
padding-left: 4rem !important;
.tw-sm-pd-r-4 {
padding-right: 1rem !important;
}
}
.side-nav__theme-wrapper.tw-border-r {
border-right: none !important;

View file

@ -128,6 +128,9 @@ export class Tooltip {
cleanup() {
if ( this.options.manual )
return;
for(const el of this.elements) {
const tip = el[this._accessor];
if ( document.body.contains(el) )

View file

@ -62,6 +62,7 @@ body {
}
}
.ffz-action-balloon,
.ffz-metadata-balloon,
.ffz__tooltip {
.loader {

View file

@ -375,6 +375,16 @@ textarea.tw-input {
}
}
.ffz-override-editor {
label {
min-width: 5rem;
}
.ffz--color-widget {
flex-grow: 1;
}
}
.ffz--report-upload {
z-index: 1;
position: absolute;