mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-07-02 17:18:31 +00:00
4.14.2
* 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:
parent
b53e0e6427
commit
3b7e99e5a3
10 changed files with 198 additions and 14 deletions
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "frankerfacez",
|
||||
"author": "Dan Salvato LLC",
|
||||
"version": "4.14.1",
|
||||
"version": "4.14.2",
|
||||
"description": "FrankerFaceZ is a Twitch enhancement suite.",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
|
|
|
@ -317,8 +317,8 @@ export class TranslationManager extends Module {
|
|||
}
|
||||
|
||||
|
||||
async loadStrings() {
|
||||
if ( this.strings_loaded )
|
||||
async loadStrings(ignore_loaded = false) {
|
||||
if ( this.strings_loaded && ! ignore_loaded )
|
||||
return;
|
||||
|
||||
if ( this.strings_loading )
|
||||
|
|
|
@ -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', {
|
||||
default: false,
|
||||
ui: {
|
||||
|
|
|
@ -647,6 +647,17 @@ export const AutomoddedTerms = {
|
|||
let idx = 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) {
|
||||
const length = token.length || (token.text && split_chars(token.text).length) || 0,
|
||||
t_start = idx,
|
||||
|
@ -1085,7 +1096,9 @@ export const AddonEmotes = {
|
|||
|
||||
plain_name = true;
|
||||
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
|
||||
return;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
ref="editor"
|
||||
v-model="value"
|
||||
: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"
|
||||
@blur="onBlur"
|
||||
@focus="open = true"
|
||||
|
@ -38,10 +38,15 @@
|
|||
<div
|
||||
v-for="(line, idx) in source"
|
||||
: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"
|
||||
>
|
||||
<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 v-if="context_str && ! open">
|
||||
|
@ -187,7 +192,22 @@ export default {
|
|||
if ( ! Array.isArray(calls) || ! calls.length )
|
||||
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() {
|
||||
|
|
|
@ -34,6 +34,14 @@
|
|||
{{ t('i18n.ui.save', 'Generate Change Blob') }}
|
||||
</div>
|
||||
</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">
|
||||
<span class="tw-button-icon__icon">
|
||||
<figure class="ffz-i-arrows-cw" />
|
||||
|
@ -222,7 +230,7 @@ import displace from 'displacejs';
|
|||
import Parser from '@ffz/icu-msgparser';
|
||||
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 PER_PAGE = 20;
|
||||
|
@ -236,6 +244,9 @@ export default {
|
|||
data.page = 1;
|
||||
data.page_open = false;
|
||||
|
||||
data.can_upload = false;
|
||||
data.uploading = false;
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
|
@ -327,6 +338,7 @@ export default {
|
|||
},
|
||||
|
||||
created() {
|
||||
this.checkUpload();
|
||||
this.requestKeys();
|
||||
this.grabKeys();
|
||||
|
||||
|
@ -356,7 +368,7 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
saveBlob() {
|
||||
getBlob() {
|
||||
const out = [];
|
||||
|
||||
for(const entry of this.phrases) {
|
||||
|
@ -371,6 +383,12 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
},
|
||||
|
||||
saveBlob() {
|
||||
const out = this.getBlob();
|
||||
|
||||
try {
|
||||
const blob = new Blob([JSON.stringify(out, null, '\t')], {type: 'application/json;charset=utf-8'});
|
||||
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() {
|
||||
this.page_open = true;
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -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}>
|
||||
{image}
|
||||
<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
|
||||
class={`tw-mg-x-05 ffz--expiry-info ffz-tooltip ffz-i-${calendar.icon}`}
|
||||
data-tooltip-type="html"
|
||||
|
@ -587,7 +587,7 @@ export default class EmoteMenu extends Module {
|
|||
/>)}
|
||||
</div>
|
||||
<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`} />}
|
||||
</heading>) : null}
|
||||
{collapsed || this.renderBody(show_heading)}
|
||||
|
@ -1108,8 +1108,10 @@ export default class EmoteMenu extends Module {
|
|||
key: `emoji-${emoji.category}`,
|
||||
emoji: true,
|
||||
image: t.emoji.getFullImage(emoji.image),
|
||||
i18n: `emoji.category.${emoji.category.toSnakeCase()}`,
|
||||
title: emoji.category,
|
||||
source: t.i18n.t('emote-menu.emoji', 'Emoji'),
|
||||
source: 'Emoji',
|
||||
source_i18n: 'emote-menu.emoji',
|
||||
emotes: cat
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1790,8 +1790,14 @@ export default class ChatHook extends Module {
|
|||
if ( user )
|
||||
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);
|
||||
if ( clear_mod )
|
||||
this.props.isCurrentUserModerator = true;
|
||||
}
|
||||
|
||||
if ( typeof original.action === 'string' )
|
||||
message.message = original.action;
|
||||
|
|
|
@ -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
|
||||
// ========================================================================
|
||||
|
|
|
@ -36,6 +36,10 @@
|
|||
width: 33%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue