1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +00:00
* Added: Additional settings for controlling the behavior of Twitch's native Chat Filters.
* Added: Upload strings directly from the Translation Tester UI. (This is for me only, but I like it.)
* Changed: Allow resizing text entry boxes in the Translation Tester UI.
* Fixed: Emoji categories were not being localized.
This commit is contained in:
SirStendec 2019-10-11 17:41:07 -04:00
parent b53e0e6427
commit 3b7e99e5a3
10 changed files with 198 additions and 14 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.14.1", "version": "4.14.2",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {

View file

@ -317,8 +317,8 @@ export class TranslationManager extends Module {
} }
async loadStrings() { async loadStrings(ignore_loaded = false) {
if ( this.strings_loaded ) if ( this.strings_loaded && ! ignore_loaded )
return; return;
if ( this.strings_loading ) if ( this.strings_loading )

View file

@ -232,6 +232,34 @@ export default class Chat extends Module {
} }
}); });
this.settings.add('chat.automod.delete-messages', {
default: true,
ui: {
path: 'Chat > Filtering >> AutoMod Filters @{"description": "Extra configuration for Twitch\'s native `Chat Filters`."}',
title: 'Mark messages as deleted if they contain filtered phrases.',
component: 'setting-check-box'
}
});
this.settings.add('chat.automod.remove-messages', {
default: true,
ui: {
path: 'Chat > Filtering >> AutoMod Filters',
title: 'Remove messages entirely if they contain filtered phrases.',
component: 'setting-check-box'
}
});
this.settings.add('chat.automod.run-as-mod', {
default: false,
ui: {
path: 'Chat > Filtering >> AutoMod Filters',
title: 'Use Chat Filters as a moderator.',
description: 'By default, Twitch\'s Chat Filters feature does not function for moderators. This overrides that behavior.',
component: 'setting-check-box'
}
});
this.settings.add('chat.filtering.process-own', { this.settings.add('chat.filtering.process-own', {
default: false, default: false,
ui: { ui: {

View file

@ -647,6 +647,17 @@ export const AutomoddedTerms = {
let idx = 0, let idx = 0,
fix = 0; fix = 0;
const remove = this.context.get('chat.automod.remove-messages');
const del = this.context.get('chat.automod.delete-messages');
if ( del )
msg.deleted = true;
if ( remove ) {
msg.ffz_removed = true;
return tokens;
}
for(const token of tokens) { for(const token of tokens) {
const length = token.length || (token.text && split_chars(token.text).length) || 0, const length = token.length || (token.text && split_chars(token.text).length) || 0,
t_start = idx, t_start = idx,
@ -1085,7 +1096,9 @@ export const AddonEmotes = {
plain_name = true; plain_name = true;
name = `:${emoji.names[0]}:${vcode ? `:${vcode.names[0]}:` : ''}`; name = `:${emoji.names[0]}:${vcode ? `:${vcode.names[0]}:` : ''}`;
source = this.i18n.t('tooltip.emoji', 'Emoji - {category}', emoji);
const category = emoji.category ? this.i18n.t(`emoji.category.${emoji.category.toSnakeCase()}`, emoji.category) : null;
source = this.i18n.t('tooltip.emoji', 'Emoji - {category}', {category});
} else } else
return; return;

View file

@ -22,7 +22,7 @@
ref="editor" ref="editor"
v-model="value" v-model="value"
:class="{'tw-textarea--error': ! valid}" :class="{'tw-textarea--error': ! valid}"
class="tw-block tw-font-size-6 tw-full-width tw-full-height tw-textarea" class="tw-block tw-font-size-6 tw-full-width tw-textarea"
@input="onInput" @input="onInput"
@blur="onBlur" @blur="onBlur"
@focus="open = true" @focus="open = true"
@ -38,10 +38,15 @@
<div <div
v-for="(line, idx) in source" v-for="(line, idx) in source"
:key="idx" :key="idx"
:title="line" :title="Array.isArray(line) ? `${line[0]} (${line[1]})` : line"
class="tw-font-size-7 tw-c-text-alt-2 tw-ellipsis tw-full-width" class="tw-font-size-7 tw-c-text-alt-2 tw-ellipsis tw-full-width"
> >
{{ line }} <span v-if="Array.isArray(line)">
{{ line[0] }} (<a :href="line[2]" rel="noopener noreferrer" target="_blank">{{ line[1] }}</a>)
</span>
<span v-else>
{{ line }}
</span>
</div> </div>
</div> </div>
<div v-if="context_str && ! open"> <div v-if="context_str && ! open">
@ -187,7 +192,22 @@ export default {
if ( ! Array.isArray(calls) || ! calls.length ) if ( ! Array.isArray(calls) || ! calls.length )
return null; return null;
return calls.join('\n').split(/\n/); const lines = calls.join('\n').split(/\n/),
out = [];
for(const line of lines) {
const match = /^(?:(.*?) \()?(\/[^:\)]+):(\d+):(\d+)\)?$/.exec(line);
if ( match )
out.push([
match[1] || '???',
`${match[2]}:${match[3]}:${match[4]}`,
`https://www.github.com/FrankerFaceZ/FrankerFaceZ/blob/master${match[2]}#L${match[3]}`
]);
else
out.push(line);
}
return out;
}, },
preview() { preview() {

View file

@ -34,6 +34,14 @@
{{ t('i18n.ui.save', 'Generate Change Blob') }} {{ t('i18n.ui.save', 'Generate Change Blob') }}
</div> </div>
</button> </button>
<button v-if="can_upload" class="tw-button-icon tw-mg-x-05 tw-relative tw-tooltip-wrapper" @click="uploadBlob">
<span class="tw-button-icon__icon">
<figure class="ffz-i-upload-cloud" />
</span>
<div class="tw-tooltip tw-tooltip--down tw-tooltip--align-right">
{{ t('i18n.ui.upload', 'Upload Changes') }}
</div>
</button>
<button class="tw-button-icon tw-mg-x-05 tw-relative tw-tooltip-wrapper" @click="requestKeys"> <button class="tw-button-icon tw-mg-x-05 tw-relative tw-tooltip-wrapper" @click="requestKeys">
<span class="tw-button-icon__icon"> <span class="tw-button-icon__icon">
<figure class="ffz-i-arrows-cw" /> <figure class="ffz-i-arrows-cw" />
@ -222,7 +230,7 @@ import displace from 'displacejs';
import Parser from '@ffz/icu-msgparser'; import Parser from '@ffz/icu-msgparser';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import { deep_equals, deep_copy } from 'utilities/object'; import { deep_equals, deep_copy, sleep } from 'utilities/object';
const parser = new Parser(); const parser = new Parser();
const PER_PAGE = 20; const PER_PAGE = 20;
@ -236,6 +244,9 @@ export default {
data.page = 1; data.page = 1;
data.page_open = false; data.page_open = false;
data.can_upload = false;
data.uploading = false;
return data; return data;
}, },
@ -327,6 +338,7 @@ export default {
}, },
created() { created() {
this.checkUpload();
this.requestKeys(); this.requestKeys();
this.grabKeys(); this.grabKeys();
@ -356,7 +368,7 @@ export default {
}, },
methods: { methods: {
saveBlob() { getBlob() {
const out = []; const out = [];
for(const entry of this.phrases) { for(const entry of this.phrases) {
@ -371,6 +383,12 @@ export default {
}); });
} }
return out;
},
saveBlob() {
const out = this.getBlob();
try { try {
const blob = new Blob([JSON.stringify(out, null, '\t')], {type: 'application/json;charset=utf-8'}); const blob = new Blob([JSON.stringify(out, null, '\t')], {type: 'application/json;charset=utf-8'});
saveAs(blob, 'ffz-strings.json'); saveAs(blob, 'ffz-strings.json');
@ -379,6 +397,55 @@ export default {
} }
}, },
async uploadBlob() {
if ( this.uploading || ! this.can_upload )
return;
const blob = JSON.stringify(this.getBlob());
const socket = this.getI18n().resolve('socket');
if ( ! socket )
return;
this.uploading = true;
const token = await socket.getAPIToken();
if ( ! token?.token )
return;
const data = await fetch(`https://api-test.frankerfacez.com/v2/i18n/strings`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token.token}`
},
body: blob
}).then(r => r.json());
alert(`Uploaded ${data?.added || 0} new strings and ${data?.changed || 0} changed strings.`); // eslint-disable-line no-alert
this.uploading = false;
this.getI18n().loadStrings(true);
},
async checkUpload() {
this.can_upload = false;
const socket = this.getI18n().resolve('socket');
if ( ! socket )
return;
const token = await socket.getAPIToken();
if ( ! token?.token )
return;
const data = await fetch(`https://api-test.frankerfacez.com/v2/user/${token.id}/role/strings_upload`, {
headers: {
Authorization: `Bearer ${token.token}`
}
}).then(r => r.json());
if ( ! data?.has_role )
return;
this.can_upload = true;
},
openPage() { openPage() {
this.page_open = true; this.page_open = true;
this.$nextTick(() => { this.$nextTick(() => {

View file

@ -579,7 +579,7 @@ export default class EmoteMenu extends Module {
{show_heading ? (<heading class="tw-pd-1 tw-border-b tw-flex tw-flex-nowrap" onClick={this.clickHeading}> {show_heading ? (<heading class="tw-pd-1 tw-border-b tw-flex tw-flex-nowrap" onClick={this.clickHeading}>
{image} {image}
<div class="tw-pd-l-05"> <div class="tw-pd-l-05">
{data.title || t.i18n.t('emote-menu.unknown', 'Unknown Source')} {(data.i18n ? t.i18n.t(data.i18n, data.title) : data.title) || t.i18n.t('emote-menu.unknown', 'Unknown Source')}
{calendar && (<span {calendar && (<span
class={`tw-mg-x-05 ffz--expiry-info ffz-tooltip ffz-i-${calendar.icon}`} class={`tw-mg-x-05 ffz--expiry-info ffz-tooltip ffz-i-${calendar.icon}`}
data-tooltip-type="html" data-tooltip-type="html"
@ -587,7 +587,7 @@ export default class EmoteMenu extends Module {
/>)} />)}
</div> </div>
<div class="tw-flex-grow-1" /> <div class="tw-flex-grow-1" />
{data.source || 'FrankerFaceZ'} {(data.source_i18n ? t.i18n.t(data.source_i18n, data.source) : data.source) || 'FrankerFaceZ'}
{filtered ? '' : <figure class={`tw-pd-l-05 ffz-i-${collapsed ? 'left' : 'down'}-dir`} />} {filtered ? '' : <figure class={`tw-pd-l-05 ffz-i-${collapsed ? 'left' : 'down'}-dir`} />}
</heading>) : null} </heading>) : null}
{collapsed || this.renderBody(show_heading)} {collapsed || this.renderBody(show_heading)}
@ -1108,8 +1108,10 @@ export default class EmoteMenu extends Module {
key: `emoji-${emoji.category}`, key: `emoji-${emoji.category}`,
emoji: true, emoji: true,
image: t.emoji.getFullImage(emoji.image), image: t.emoji.getFullImage(emoji.image),
i18n: `emoji.category.${emoji.category.toSnakeCase()}`,
title: emoji.category, title: emoji.category,
source: t.i18n.t('emote-menu.emoji', 'Emoji'), source: 'Emoji',
source_i18n: 'emote-menu.emoji',
emotes: cat emotes: cat
}); });
} }

View file

@ -1790,8 +1790,14 @@ export default class ChatHook extends Module {
if ( user ) if ( user )
message.emotes = user.emotes; message.emotes = user.emotes;
if ( flags && this.getFilterFlagOptions ) if ( flags && this.getFilterFlagOptions ) {
const clear_mod = this.props.isCurrentUserModerator && t.chat.context.get('chat.automod.run-as-mod');
if ( clear_mod )
this.props.isCurrentUserModerator = false;
message.flags = this.getFilterFlagOptions(flags); message.flags = this.getFilterFlagOptions(flags);
if ( clear_mod )
this.props.isCurrentUserModerator = true;
}
if ( typeof original.action === 'string' ) if ( typeof original.action === 'string' )
message.message = original.action; message.message = original.action;

View file

@ -110,6 +110,50 @@ export default class SocketClient extends Module {
} }
// ========================================================================
// FFZ API Helpers
// ========================================================================
getAPIToken() {
if ( this._cached_token ) {
if ( this._cached_token.expires > (Date.now() + 15000) )
return Promise.resolve(this._cached_token);
}
if ( this._token_waiters )
return new Promise((s, f) => this._token_waiters.push([s, f]));
this._token_waiters = [];
return new Promise((s, f) => {
this._token_waiters.push([s, f]);
this.call('get_api_token').then(token => {
token.expires = (new Date(token.expires)).getTime();
this._cached_token = token;
const waiters = this._token_waiters;
this._token_waiters = null;
for(const pair of waiters)
pair[0](token);
}).catch(err => {
this.log.error('Unable to get API token.', err);
const waiters = this._token_waiters;
this._token_waiters = null;
for(const pair of waiters)
pair[1](err);
});
});
}
async getBareAPIToken() {
return (await this.getAPIToken())?.token;
}
// ======================================================================== // ========================================================================
// Connection Logic // Connection Logic
// ======================================================================== // ========================================================================

View file

@ -36,6 +36,10 @@
width: 33%; width: 33%;
} }
textarea {
min-height: 100%;
}
code { code {
white-space: pre-wrap; white-space: pre-wrap;
} }