1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-09-15 17:46:55 +00:00
* Added: Notice when FFZ fails to save settings because `localStorage` is full.
* Added: Setting to move "Chat Identity" from the chat input box to the chat settings menu. (Closes #1025)
* Fixed: Actually hide links when "Hide matching links for rich content." is enabled. (Closes #1019)
* Fixed: Open the control center when clicking a markdown settings link without the control center open.
* API Added: Toasts displayed by the `site.menu_button` module now support markdown.
This commit is contained in:
SirStendec 2021-04-19 15:08:12 -04:00
parent a80728a10d
commit 66702103ff
24 changed files with 303 additions and 190 deletions

View file

@ -558,7 +558,7 @@ export default class Badges extends Module {
}
render(msg, createElement, skip_hide = false) { // eslint-disable-line class-methods-use-this
render(msg, createElement, skip_hide = false, skip_click = false) { // eslint-disable-line class-methods-use-this
if ( ! msg.badges && ! msg.ffz_badges )
return null;
@ -769,7 +769,8 @@ export default class Badges extends Module {
props['data-tooltip-type'] = 'badge';
props['data-badge-data'] = JSON.stringify(data.badges);
props.onClick = this.handleClick;
if ( ! skip_click )
props.onClick = this.handleClick;
if ( data.replaced )
props['data-replaced'] = data.replaced;

View file

@ -206,7 +206,7 @@ export default class Chat extends Module {
});
this.settings.add('chat.rich.hide-tokens', {
default: true,
default: false,
ui: {
path: 'Chat > Appearance >> Rich Content',
title: 'Hide matching links for rich content.',
@ -262,6 +262,13 @@ export default class Chat extends Module {
}
});
this.settings.addUI('chat.filtering.pad-bottom', {
path: 'Chat > Filtering > Highlight',
sort: 1000,
component: 'setting-spacer',
top: '30rem'
});
this.settings.add('chat.filtering.click-to-reveal', {
default: false,
ui: {
@ -1694,7 +1701,7 @@ export default class Chat extends Module {
for(const token of tokens) {
for(const provider of providers)
if ( provider.test.call(this, token, msg) ) {
token.hidden = this.context.get('chat.rich.hide-tokens') && provider.hide_token;
token.hidden = provider.can_hide_token && (this.context.get('chat.rich.hide-tokens') || provider.hide_token);
return provider.process.call(this, token);
}
}

View file

@ -28,7 +28,7 @@ import {truncate} from 'utilities/object';
export const Links = {
type: 'link',
hide_token: false,
can_hide_token: true,
priority: -10,
test(token) {
@ -77,7 +77,7 @@ export const Links = {
export const Users = {
type: 'user',
hide_token: false,
can_hide_token: true,
test(token) {
if ( token.type !== 'link' || (! this.context.get('chat.rich.all-links') && ! token.force_rich) )
@ -190,7 +190,7 @@ export const Users = {
export const Clips = {
type: 'clip',
hide_token: false,
can_hide_token: true,
test(token) {
if ( token.type !== 'link' )
@ -278,7 +278,7 @@ export const Clips = {
export const Videos = {
type: 'video',
hide_token: false,
can_hide_token: true,
test(token) {
return token.type === 'link' && VIDEO_URL.test(token.url)

View file

@ -298,6 +298,9 @@ export default class MainMenu extends Module {
return;
this.requestPage(path);
if ( ! this.showing )
this.emit('site.menu_button:clicked');
}

View file

@ -99,6 +99,9 @@ export default class SettingsManager extends Module {
this.provider = provider;
this.log.info(`Using Provider: ${provider.constructor.name}`);
provider.on('changed', this._onProviderChange, this);
provider.on('quota-exceeded', err => {
this.emit(':quota-exceeded', err);
});
provider.on('change-provider', () => {
this.emit(':change-provider');
});

View file

@ -262,7 +262,18 @@ export class LocalStorageProvider extends SettingsProvider {
}
this._cached.set(key, value);
localStorage.setItem(this.prefix + key, JSON.stringify(value));
try {
localStorage.setItem(this.prefix + key, JSON.stringify(value));
} catch(err) {
if ( this.manager )
this.manager.log.error(`An error occurred while trying to save a value to localStorage for key "${this.prefix + key}"`);
if ( /quota/i.test(err.toString()) )
this.emit('quota-exceeded', err);
throw err;
}
this.broadcast({type: 'set', key});
this.emit('set', key, value, false);
}

View file

@ -18,6 +18,16 @@ export default class SettingsMenu extends Module {
this.inject('chat.badges');
this.inject('site.fine');
this.inject('site.web_munch');
this.inject('site.css_tweaks');
this.settings.add('chat.input.hide-identity', {
default: false,
ui: {
path: 'Chat > Input >> Appearance',
title: 'Display "Chat Identity" in the chat settings menu rather than the input box.',
component: 'setting-check-box'
}
});
this.SettingsMenu = this.fine.define(
'chat-settings',
@ -35,6 +45,12 @@ export default class SettingsMenu extends Module {
async onEnable() {
this.on('i18n:update', () => this.SettingsMenu.forceUpdate());
this.chat.context.on('changed:chat.scroller.freeze', () => this.SettingsMenu.forceUpdate());
this.chat.context.on('changed:chat.input.hide-identity', val => {
this.css_tweaks.toggle('hide-chat-identity', val);
this.SettingsMenu.forceUpdate();
});
this.css_tweaks.toggle('hide-chat-identity', this.chat.context.get('chat.input.hide-identity'));
const t = this,
React = await this.web_munch.findModule('react');
@ -101,71 +117,87 @@ export default class SettingsMenu extends Module {
return val;
}
cls.prototype.render = function() {
try {
if ( this.state.ffzPauseMenu ) {
if ( ! this.ffzSettingsClick )
this.ffzSettingsClick = e => t.click(this, e);
cls.prototype.ffzRenderIdentity = function() {
if ( ! this.state || ! this.props || this.state.moderatorMode || this.state.chatAppearance || this.state.chatPause || this.state.followerMode || this.state.recentRaids || this.state.repliesAppearance || this.state.slowMode || this.props.isShowingChatFilterSettings || this.props.isShowingDeletedMessageDisplaySettings || ! this.props.isLoggedIn || ! this.props.onClickEditAppearance )
return null;
if ( ! this.ffzPauseClick )
this.ffzPauseClick = () => this.setState({ffzPauseMenu: ! this.state.ffzPauseMenu});
if ( ! t.chat.context.get('chat.input.hide-identity') )
return null;
return (<div class="tw-absolute ffz-balloon ffz-balloon--auto ffz-balloon--right ffz-balloon--up tw-block" data-a-target="chat-settings-balloon" style={{marginRight: '-5.3rem'}}>
<div class="tw-border-radius-large tw-c-background-base tw-c-text-inherit tw-elevation-2">
<div class="chat-settings__popover">
<div class="chat-settings__header tw-align-items-center tw-c-background-base tw-flex tw-pd-x-1 tw-relative">
<div class="chat-settings__back-icon-container tw-left-0 tw-mg-r-05">
<button
class="tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon ffz-core-button ffz-core-button--border tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative"
data-test-selector="chat-settings-back-button"
aria-label={t.i18n.t('chat.settings.back', 'Back')}
onClick={this.ffzPauseClick}
>
<div class="tw-align-items-center tw-flex tw-flex-grow-0">
<span class="tw-button-icon__icon">
<figure class="ffz-i-left-open" />
</span>
</div>
</button>
</div>
<div class="tw-align-center tw-align-items-center tw-flex tw-flex-grow-1 tw-justify-content-center">
<p class="tw-c-text-alt tw-font-size-5 tw-semibold">
{ t.i18n.t('chat.settings.pause', 'Pause Chat') }
</p>
</div>
</div>
<div class="chat-settings scrollable-area scrollable-area--suppress-scroll-x" data-simplebar>
<div class="chat-settings__content tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-c-background-base tw-c-text-base tw-pd-1">
<div class="tw-pd-x-05">
<div class="tw-border-b tw-mg-b-1 tw-pd-b-1">
<p class="tw-c-text-alt-2">
{ t.i18n.t('chat.settings.pause-explain', 'FrankerFaceZ overrides the behavior of Pause Chat entirely. Please use FFZ\'s Scrolling settings within the FFZ Control Center under Chat > Behavior.') }
</p>
</div>
<button
class="tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
data-page="chat.behavior"
onClick={this.ffzSettingsClick}
>
<div class="tw-align-items-center tw-flex tw-pd-05 tw-relative">
<div class="tw-flex-grow-1">
{t.i18n.t('chat.settings.open-settings', 'Open Control Center')}
</div>
</div>
</button>
</div>
</div>
</div>
const user = this.props.data?.currentUser,
raw_badges = this.props.data?.user?.self?.displayBadges;
if ( ! user || ! user.login || ! Array.isArray(raw_badges) )
return null;
const is_intl = user.login && user.displayName && user.displayName.trim().toLowerCase() !== user.login,
color = t.parent.colors.process(user.chatColor),
badges = {};
for(const badge of raw_badges) {
if ( badge?.setID && badge.version )
badges[badge.setID] = badge.version;
}
return (<div class="ffz-identity">
<div class="tw-mg-y-05 tw-pd-x-05">
<p class="tw-c-text-alt-2 tw-font-size-6 tw-strong tw-upcase">
{ t.i18n.t('chat.identity-menu', 'Chat Identity') }
</p>
</div>
<div class="tw-full-width tw-relative">
<button
class="tw-block tw-border-radius-medium tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive"
onClick={this.props.onClickEditAppearance}
>
<div class="tw-align-items-center tw-flex tw-pd-05 tw-relative">
<div class="tw-flex-grow-1">
<span class="ffz--editor-name">
<span
class="ffz--editor-badges"
data-room-id={this.props.channelID}
data-room-login={this.props.channelLogin}
>
{t.badges.render({
user,
badges,
ffz_badges: t.badges.getBadges(user.id, user.login, this.props.channelID, this.props.channelLogin),
roomID: this.props.channelID,
roomLogin: this.props.channelLogin
}, createElement, true, true)}
</span>
<span class="tw-strong notranslate" style={{color}}>
<span class="name-display__name">{ user.displayName || user.login}</span>
{is_intl && <span class="intl-name"> ({user.login}) </span>}
</span>
</span>
</div>
<div class="tw-align-items-center tw-flex tw-flex-shrink-0 tw-mg-l-05">
<figure class="ffz-i-right-open" />
</div>
</div>
</div>)
</button>
</div>
</div>);
}
cls.prototype.render = function() {
const out = old_render.call(this);
try {
const children = out?.props?.children?.props?.children?.[1]?.props?.children?.props?.children;
if ( Array.isArray(children) ) {
const extra = this.ffzRenderIdentity();
if ( extra )
children.unshift(extra);
}
} catch(err) {
t.log.error('Error rendering chat settings menu.', err);
}
return old_render.call(this);
return out;
}
this.SettingsMenu.forceUpdate();
@ -228,9 +260,10 @@ export default class SettingsMenu extends Module {
}, this.badges.render({
user,
badges,
ffz_badges: this.badges.getBadges(user.id, user.login, inst.props.channelID, inst.props.channelLogin),
roomID: inst.props.channelID,
roomLogin: inst.props.channelLogin
}, createElement, true)),
}, createElement, true, true)),
<span class="tw-strong notranslate" style={{color}}>
<span class="name-display__name">{user.displayName || user.login}</span>
@ -292,17 +325,25 @@ export default class SettingsMenu extends Module {
} else if ( ! badge )
return;
cont.appendChild(<div class="ffz--badge-selector">
<p class="tw-pd-x-05">
{this.i18n.tList('chat.ffz-badge.about', '{title}: This badge appears globally for users with FrankerFaceZ.', {
title: <span class="tw-strong">{this.i18n.t('chat.ffz-badge.title', 'FrankerFaceZ Badge')}</span>
})}{' '}
{this.i18n.tList('chat.ffz-badge.site', 'Please visit the {website} to change this badge.', {
website: (<a href="https://www.frankerfacez.com/donate" class="ffz-link" rel="noopener noreferrer" target="_blank">
{this.i18n.t('chat.ffz-badge.site-link', 'FrankerFaceZ website')}
</a>)
})}
</p>
const out = (<div class="ffz--badge-selector tw-border-b tw-mg-b-1">
<div class="tw-mg-y-05 tw-pd-x-05">
<p class="tw-c-text-alt-2 tw-font-size-6 tw-strong tw-upcase">
{ this.i18n.t('chat.ffz-badge.title', 'FrankerFaceZ Badge') }
</p>
</div>
<div>
<p class="tw-mg-b-05 tw-pd-x-05">
{ this.i18n.tList(
'chat.ffz-badge.about',
'This badge appears globally for users with FrankerFaceZ. Please visit the {website} to change this badge.',
{
website: (<a href="https://www.frankerfacez.com/donate" class="ffz-link" rel="noopener noreferrer" target="_blank">
{this.i18n.t('chat.ffz-badge.site-link', 'FrankerFaceZ website')}
</a>)
}
) }
</p>
</div>
<div role="radiogroup" class="tw-align-items-center tw-flex tw-flex-wrap tw-mg-b-05 tw-mg-t-05 tw-pd-x-05">
<div class="tw-mg-r-1 tw-mg-y-05">
<div class="tw-inline-flex">
@ -321,6 +362,12 @@ export default class SettingsMenu extends Module {
</div>
</div>
</div>);
const after = cont.querySelector('[data-test-selector="global-badges-test-selector"]')?.nextElementSibling;
if ( after )
cont.insertBefore(out, after);
else
cont.appendChild(out);
}

View file

@ -0,0 +1,7 @@
.chat-input__badge-carousel {
display: none !important;
}
.chat-input__textarea .tw-textarea {
padding-left: 1rem !important;
}

View file

@ -9,6 +9,7 @@ import {SiteModule} from 'utilities/module';
import {createElement, ClickOutside, setChildren} from 'utilities/dom';
import Twilight from 'site';
import getMD from 'src/utilities/markdown';
export default class MenuButton extends SiteModule {
@ -257,6 +258,17 @@ export default class MenuButton extends SiteModule {
this.on('i18n:changed-strings', this.update);
this.on('i18n:update', this.update);
this.on('addons:data-loaded', this.update);
this.once('settings:quota-exceeded', () => {
this.addError(
'site.menu_button.quota-exceeded',
'Your local storage space for this website is full, and settings cannot be saved. Please backup your settings and switch to a higher capacity provider in [Data Management > Storage >> Provider](~data_management.storage.tabs.provider).',
'ffz-i-attention',
true
);
this.update();
});
this.on('settings:change-provider', () => {
this.addError('site.menu_button.changed',
'The FrankerFaceZ settings provider has changed. Please refresh this tab to avoid strange behavior.'
@ -266,11 +278,12 @@ export default class MenuButton extends SiteModule {
}
addError(i18n, text, icon = 'ffz-i-attention') {
addError(i18n, text, icon = 'ffz-i-attention', use_markdown = false) {
this.addToast({
icon,
text_i18n: i18n,
text
text,
markdown: use_markdown
});
}
@ -425,7 +438,8 @@ export default class MenuButton extends SiteModule {
{ data.title_i18n ? this.i18n.tList(data.title_i18n, data.title, data) : data.title}
</header>) : null }
{ data.text ? (<span class={`${data.lines ? 'ffz--line-clamp' : ''}`} style={{'--ffz-lines': data.lines}}>
{ data.text_i18n ? this.i18n.tList(data.text_i18n, data.text, data) : data.text}
{ data.markdown ? <span dangerouslySetInnerHTML={{__html: getMD().render(data.text_i18n ? this.i18n.t(data.text_i18n, data.text, data) : data.text) }} /> : null}
{ data.markdown ? null : (data.text_i18n ? this.i18n.tList(data.text_i18n, data.text, data) : data.text)}
</span>) : null }
</div>) : null}
{ ! data.unclosable && (<button

View file

@ -5,101 +5,7 @@
<script>
import MD from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
import {parse as parse_path} from 'utilities/path-parser';
let _md;
function getMD() {
if ( ! _md ) {
const md = _md = new MD({
html: false,
linkify: true
});
md.use(SettingsLinks);
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
}
return _md;
}
function SettingsLinks(md) {
const default_render = md.renderer.rules.link_open || this.defaultRender;
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
const token = tokens[idx];
if ( token && token.type === 'link_open' && Array.isArray(token.attrs) ) {
let href;
for(const attr of token.attrs) {
if ( attr[0] === 'href' ) {
href = attr[1];
break;
}
}
if ( href.startsWith('~') ) {
let path;
if ( href === '~' ) {
// We don't have a path, make one from the bits.
let i = idx + 1;
let bits = [];
while(i < tokens.length) {
const tok = tokens[i],
type = tok?.type;
if ( type === 'text' )
bits.push(tok);
else if ( type === 'link_close' )
break;
i++;
}
bits = bits.map(x => x.content).join('');
const toks = parse_path(bits);
path = toks.map(x => x.key).join('.');
} else
path = href.slice(1);
if ( path && path.length ) {
for(const attr of token.attrs) {
if ( attr[0] === 'class' ) {
attr[1] = attr[1].replace(/ffz-tooltip/g, '');
break;
}
}
token.attrs.push([
'data-settings-link',
path
]);
token.attrs.push([
'onclick',
'FrankerFaceZ.get().resolve("main_menu").mdNavigate(this);return false'
]);
}
}
}
return default_render(tokens, idx, options, env, self);
}
}
SettingsLinks.defaultRender = function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
}
import getMD from 'utilities/markdown';
export default {
props: {

View file

@ -102,5 +102,6 @@ export default [
"link",
"volume-off",
"reply",
"threads"
"threads",
"right-open"
];

96
src/utilities/markdown.js Normal file
View file

@ -0,0 +1,96 @@
'use strict';
import MD from 'markdown-it';
import MILA from 'markdown-it-link-attributes';
import {parse as parse_path} from 'utilities/path-parser';
let _md;
function SettingsLinks(md) {
const default_render = md.renderer.rules.link_open || this.defaultRender;
md.renderer.rules.link_open = function(tokens, idx, options, env, self) {
const token = tokens[idx];
if ( token && token.type === 'link_open' && Array.isArray(token.attrs) ) {
let href;
for(const attr of token.attrs) {
if ( attr[0] === 'href' ) {
href = attr[1];
break;
}
}
if ( href.startsWith('~') ) {
let path;
if ( href === '~' ) {
// We don't have a path, make one from the bits.
let i = idx + 1;
let bits = [];
while(i < tokens.length) {
const tok = tokens[i],
type = tok?.type;
if ( type === 'text' )
bits.push(tok);
else if ( type === 'link_close' )
break;
i++;
}
bits = bits.map(x => x.content).join('');
const toks = parse_path(bits);
path = toks.map(x => x.key).join('.');
} else
path = href.slice(1);
if ( path && path.length ) {
for(const attr of token.attrs) {
if ( attr[0] === 'class' ) {
attr[1] = attr[1].replace(/ffz-tooltip/g, '');
break;
}
}
token.attrs.push([
'data-settings-link',
path
]);
token.attrs.push([
'onclick',
'FrankerFaceZ.get().resolve("main_menu").mdNavigate(this);return false'
]);
}
}
}
return default_render(tokens, idx, options, env, self);
}
}
SettingsLinks.defaultRender = function(tokens, idx, options, env, self) {
return self.renderToken(tokens, idx, options);
}
export default function getMD() {
if ( ! _md ) {
const md = _md = new MD({
html: false,
linkify: true
});
md.use(SettingsLinks);
md.use(MILA, {
attrs: {
class: 'ffz-tooltip',
target: '_blank',
rel: 'noopener',
'data-tooltip-type': 'link'
}
});
}
return _md;
}