mirror of
https://hk.gh-proxy.com/https://github.com/NodeBB/nodebb-plugin-emoji.git
synced 2025-10-04 01:20:58 +08:00
refactor: use Svelte for ACP interface
This commit is contained in:
parent
87b122fe57
commit
310ed52a98
39 changed files with 1349 additions and 1221 deletions
31
.eslintrc.js
31
.eslintrc.js
|
@ -1,8 +1,32 @@
|
|||
/* eslint-disable @typescript-eslint/no-var-requires, import/no-extraneous-dependencies */
|
||||
const typescript = require('typescript');
|
||||
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
plugins: [
|
||||
'svelte3',
|
||||
'@typescript-eslint',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
processor: 'svelte3/svelte3',
|
||||
rules: {
|
||||
'@typescript-eslint/indent': 'off',
|
||||
'no-label-var': 'off',
|
||||
'import/first': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
'import/no-mutable-exports': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
'svelte3/typescript': typescript,
|
||||
},
|
||||
extends: ['airbnb-base', 'plugin:@typescript-eslint/recommended'],
|
||||
rules: {
|
||||
camelcase: 'off',
|
||||
'no-undef': 'off',
|
||||
'prefer-destructuring': 'off',
|
||||
'no-param-reassign': 'warn',
|
||||
'comma-dangle': ['error', {
|
||||
|
@ -10,7 +34,7 @@ module.exports = {
|
|||
objects: 'always-multiline',
|
||||
imports: 'always-multiline',
|
||||
exports: 'always-multiline',
|
||||
functions: 'never'
|
||||
functions: 'never',
|
||||
}],
|
||||
'import/no-unresolved': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
|
@ -20,11 +44,12 @@ module.exports = {
|
|||
'object-curly-newline': ['error', {
|
||||
multiline: true,
|
||||
minProperties: 5,
|
||||
consistent: true
|
||||
consistent: true,
|
||||
}],
|
||||
'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
|
||||
'@typescript-eslint/indent': ['error', 2],
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': ['error', { allowArgumentsExplicitlyTypedAsAny: true }],
|
||||
},
|
||||
};
|
||||
|
|
5
acp/.eslintrc.js
Normal file
5
acp/.eslintrc.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
|
||||
},
|
||||
};
|
1
acp/admin.less
Normal file
1
acp/admin.less
Normal file
|
@ -0,0 +1 @@
|
|||
@import (inline) "../build/acp/admin.css";
|
61
acp/rollup.config.js
Normal file
61
acp/rollup.config.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import svelte from 'rollup-plugin-svelte';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import livereload from 'rollup-plugin-livereload';
|
||||
import sveltePreprocess from 'svelte-preprocess';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import virtual from '@rollup/plugin-virtual';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
export default {
|
||||
input: 'src/admin.ts',
|
||||
external: ['translator', 'jquery', 'api', 'emoji'],
|
||||
output: {
|
||||
sourcemap: !production,
|
||||
format: 'amd',
|
||||
file: '../build/acp/admin.js',
|
||||
},
|
||||
plugins: [
|
||||
virtual({
|
||||
ajaxify: 'export default ajaxify',
|
||||
app: 'export default app',
|
||||
config: 'export default config',
|
||||
textcomplete: 'export default Textcomplete',
|
||||
utils: 'export default utils',
|
||||
}),
|
||||
svelte({
|
||||
preprocess: sveltePreprocess({ sourceMap: !production }),
|
||||
compilerOptions: {
|
||||
// enable run-time checks when not in production
|
||||
dev: !production,
|
||||
},
|
||||
}),
|
||||
// we'll extract any component CSS out into
|
||||
// a separate file - better for performance
|
||||
css({ output: 'admin.css' }),
|
||||
|
||||
// If you have external dependencies installed from
|
||||
// npm, you'll most likely need these plugins. In
|
||||
// some cases you'll need additional configuration -
|
||||
// consult the documentation for details:
|
||||
// https://github.com/rollup/plugins/tree/master/packages/commonjs
|
||||
resolve({
|
||||
browser: true,
|
||||
dedupe: ['svelte'],
|
||||
}),
|
||||
commonjs(),
|
||||
typescript({
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
|
||||
// Watch the `src` directory and refresh the
|
||||
// browser on changes when not in production
|
||||
!production && livereload(),
|
||||
],
|
||||
watch: {
|
||||
clearScreen: false,
|
||||
},
|
||||
};
|
284
acp/src/Adjunct.svelte
Normal file
284
acp/src/Adjunct.svelte
Normal file
|
@ -0,0 +1,284 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Textcomplete from 'textcomplete';
|
||||
import { buildEmoji, strategy, table } from 'emoji';
|
||||
|
||||
export let item: CustomAdjunct;
|
||||
export let id: number;
|
||||
|
||||
function deepEquals(a: unknown, b: unknown) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (typeof a !== typeof b) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
||||
return a.every((value, index) => deepEquals(value, b[index]));
|
||||
}
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a === 'object') {
|
||||
const keys = Object.keys(a);
|
||||
if (Object.keys(b).length !== keys.length) {
|
||||
return false;
|
||||
}
|
||||
return keys.every(key => deepEquals(a[key], b[key]));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let name = item.name;
|
||||
let aliases = item.aliases.slice();
|
||||
let ascii = item.ascii.slice();
|
||||
|
||||
export function reset(): void {
|
||||
name = item.name;
|
||||
aliases = item.aliases.slice();
|
||||
ascii = item.ascii.slice();
|
||||
}
|
||||
|
||||
const empty = !item.name && !item.aliases?.length && !item.ascii?.length;
|
||||
|
||||
let newAlias = '';
|
||||
function removeAlias(a: string) {
|
||||
aliases = aliases.filter(x => x !== a);
|
||||
}
|
||||
function addAlias() {
|
||||
if (!newAlias || aliases.includes(newAlias)) {
|
||||
return;
|
||||
}
|
||||
|
||||
aliases = [...aliases, newAlias];
|
||||
newAlias = '';
|
||||
}
|
||||
|
||||
let newAscii = '';
|
||||
function removeAscii(a: string) {
|
||||
ascii = ascii.filter(x => x !== a);
|
||||
}
|
||||
function addAscii() {
|
||||
if (!newAscii || ascii.includes(newAscii)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ascii = [...ascii, newAscii];
|
||||
newAscii = '';
|
||||
}
|
||||
|
||||
$: emoji = name && table[name];
|
||||
$: editing = !deepEquals({
|
||||
name,
|
||||
aliases,
|
||||
ascii,
|
||||
}, item);
|
||||
|
||||
interface Failures {
|
||||
nameRequired: boolean,
|
||||
nameInvalid: boolean,
|
||||
aliasInvalid: boolean,
|
||||
noChange: boolean,
|
||||
any: boolean,
|
||||
}
|
||||
const failures: Failures = {
|
||||
nameRequired: false,
|
||||
nameInvalid: false,
|
||||
aliasInvalid: false,
|
||||
noChange: false,
|
||||
any: false,
|
||||
};
|
||||
const pattern = /[^a-z\-.+0-9_]/i;
|
||||
$: failures.nameRequired = !name;
|
||||
$: failures.nameInvalid = !table[name];
|
||||
$: failures.aliasInvalid = pattern.test(newAlias);
|
||||
$: failures.noChange = !aliases.length && !ascii.length;
|
||||
$: failures.any = (
|
||||
failures.nameRequired ||
|
||||
failures.nameInvalid ||
|
||||
failures.aliasInvalid ||
|
||||
failures.noChange
|
||||
);
|
||||
|
||||
$: canSave = editing && !failures.any;
|
||||
|
||||
let nameInput: HTMLInputElement;
|
||||
onMount(() => {
|
||||
const { Textarea } = Textcomplete.editors;
|
||||
|
||||
const editor = new Textarea(nameInput);
|
||||
const completer = new Textcomplete(editor, {
|
||||
dropdown: {
|
||||
style: { zIndex: 20000 },
|
||||
},
|
||||
});
|
||||
|
||||
completer.register([{
|
||||
...strategy,
|
||||
replace: (data: StoredEmoji) => data.name,
|
||||
match: /^(.+)$/,
|
||||
}]);
|
||||
|
||||
completer.on('selected', () => {
|
||||
name = nameInput.value;
|
||||
});
|
||||
});
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function onSave() {
|
||||
dispatch('save', {
|
||||
id,
|
||||
item: {
|
||||
name,
|
||||
aliases,
|
||||
ascii,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let deleting = false;
|
||||
let deleted = false;
|
||||
|
||||
function onDelete() {
|
||||
deleting = true;
|
||||
}
|
||||
function confirmDelete() {
|
||||
deleting = false;
|
||||
deleted = true;
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch('delete', {
|
||||
id,
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
function cancelDelete() {
|
||||
deleting = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr class:fadeout={deleted}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control emoji-name"
|
||||
bind:value={name}
|
||||
bind:this={nameInput}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{#if emoji}
|
||||
{@html buildEmoji(emoji)}
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
bind:value={newAlias}
|
||||
/>
|
||||
<div class="input-group-addon"><button class="btn btn-default" on:click={addAlias}>+</button></div>
|
||||
</div>
|
||||
<span>
|
||||
{#each aliases as a}
|
||||
<button class="btn btn-info btn-xs" on:click={() => removeAlias(a)}>{a} x</button>
|
||||
{/each}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
bind:value={newAscii}
|
||||
/>
|
||||
<div class="input-group-addon"><button class="btn btn-default" on:click={addAscii}>+</button></div>
|
||||
</div>
|
||||
<span>
|
||||
{#each ascii as a}
|
||||
<button class="btn btn-info btn-xs" on:click={() => removeAscii(a)}>{a} x</button>
|
||||
{/each}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{#if editing || empty}
|
||||
<button
|
||||
class="btn btn-success"
|
||||
type="button"
|
||||
on:click={onSave}
|
||||
disabled={!canSave}
|
||||
>
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
type="button"
|
||||
on:click={onDelete}
|
||||
disabled={deleting || deleted}
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{#if deleting || deleted}
|
||||
<tr class:fadeout={deleted}>
|
||||
<td>
|
||||
<button class="btn btn-default" type="button" disabled={deleted} on:click={cancelDelete}>Cancel</button>
|
||||
</td>
|
||||
<td colSpan={3}>
|
||||
<span class="help-block">Are you sure you want to delete this extension?</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-danger" type="button" disabled={deleted} on:click={confirmDelete}>Yes</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.nameRequired}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Name</strong> is required</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.nameInvalid}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Name</strong> must be an existing emoji</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.aliasInvalid}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Aliases</strong> can only contain letters, numbers, and <code>_-+.</code></span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.noChange}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span>Must provide at least one <strong>Alias</strong> or <strong>ASCII Pattern</strong>.</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
tr {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
tr.fadeout {
|
||||
opacity: 0;
|
||||
}
|
||||
input.emoji-name {
|
||||
max-width: 125px;
|
||||
}
|
||||
</style>
|
10
acp/src/App.svelte
Normal file
10
acp/src/App.svelte
Normal file
|
@ -0,0 +1,10 @@
|
|||
<script lang="ts">
|
||||
import Settings from './Settings.svelte';
|
||||
</script>
|
||||
|
||||
<div class="admin">
|
||||
<nav id="header" class="header"></nav>
|
||||
<div id="content" class="container">
|
||||
<Settings/>
|
||||
</div>
|
||||
</div>
|
49
acp/src/Customize.svelte
Normal file
49
acp/src/Customize.svelte
Normal file
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import jQuery from 'jquery';
|
||||
import config from 'config';
|
||||
import EmojiList from './EmojiList.svelte';
|
||||
import ItemList from './ItemList.svelte';
|
||||
|
||||
export let data: Customizations;
|
||||
|
||||
let modal: HTMLElement;
|
||||
|
||||
export function show(): void {
|
||||
jQuery(modal).modal('show');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="modal fade" bind:this={modal} tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title" id="editModalLabel">Customize Emoji</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Below you can add custom emoji, and also add new aliases
|
||||
and ASCII patterns for existing emoji. While this list is
|
||||
edited live, you must still <strong>Build Emoji Assets </strong>
|
||||
to actually use these customizations.
|
||||
</p>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Custom Emoji</h3>
|
||||
</div>
|
||||
<EmojiList emojis={data.emojis} />
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">Custom Extensions</h3>
|
||||
</div>
|
||||
<ItemList record={data.adjuncts} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="stylesheet" href={`${config.relative_path}/plugins/nodebb-plugin-emoji/emoji/styles.css?${config['cache-buster']}`} />
|
||||
</svelte:head>
|
330
acp/src/Emoji.svelte
Normal file
330
acp/src/Emoji.svelte
Normal file
|
@ -0,0 +1,330 @@
|
|||
<script lang="ts">
|
||||
import app from 'app';
|
||||
import config from 'config';
|
||||
import utils from 'utils';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let emoji: CustomEmoji;
|
||||
export let id: number;
|
||||
|
||||
import { buildEmoji } from 'emoji';
|
||||
|
||||
function deepEquals(a: unknown, b: unknown) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (typeof a !== typeof b) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) {
|
||||
return a.every((value, index) => deepEquals(value, b[index]));
|
||||
}
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof a === 'object') {
|
||||
const keys = Object.keys(a);
|
||||
if (Object.keys(b).length !== keys.length) {
|
||||
return false;
|
||||
}
|
||||
return keys.every(key => deepEquals(a[key], b[key]));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface Failures {
|
||||
nameRequired: boolean,
|
||||
imageRequired: boolean,
|
||||
nameInvalid: boolean,
|
||||
aliasInvalid: boolean,
|
||||
any: boolean,
|
||||
}
|
||||
|
||||
let name = emoji.name;
|
||||
let image = emoji.image;
|
||||
let aliases = emoji.aliases.slice();
|
||||
let ascii = emoji.ascii.slice();
|
||||
|
||||
export function reset(): void {
|
||||
name = emoji.name;
|
||||
image = emoji.image;
|
||||
aliases = emoji.aliases.slice();
|
||||
ascii = emoji.ascii.slice();
|
||||
}
|
||||
|
||||
let imageForm: HTMLFormElement;
|
||||
let imageInput: HTMLInputElement;
|
||||
let fileNameInput: HTMLInputElement;
|
||||
|
||||
let newAlias = '';
|
||||
function removeAlias(a: string) {
|
||||
aliases = aliases.filter(x => x !== a);
|
||||
}
|
||||
function addAlias() {
|
||||
if (!newAlias || aliases.includes(newAlias)) {
|
||||
return;
|
||||
}
|
||||
|
||||
aliases = [...aliases, newAlias];
|
||||
newAlias = '';
|
||||
}
|
||||
|
||||
let newAscii = '';
|
||||
function removeAscii(a: string) {
|
||||
ascii = ascii.filter(x => x !== a);
|
||||
}
|
||||
function addAscii() {
|
||||
if (!newAscii || ascii.includes(newAscii)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ascii = [...ascii, newAscii];
|
||||
newAscii = '';
|
||||
}
|
||||
|
||||
const empty = !emoji.name && !emoji.image && !emoji.aliases?.length && !emoji.ascii?.length;
|
||||
let editing = false;
|
||||
$: editing = !deepEquals({
|
||||
name,
|
||||
image,
|
||||
aliases,
|
||||
ascii,
|
||||
}, emoji);
|
||||
|
||||
const failures: Failures = {
|
||||
nameRequired: false,
|
||||
imageRequired: false,
|
||||
nameInvalid: false,
|
||||
aliasInvalid: false,
|
||||
any: false,
|
||||
};
|
||||
const pattern = /[^a-z\-.+0-9_]/i;
|
||||
$: failures.nameRequired = !name;
|
||||
$: failures.imageRequired = !image;
|
||||
$: failures.nameInvalid = pattern.test(name);
|
||||
$: failures.aliasInvalid = pattern.test(newAlias);
|
||||
$: failures.any = (
|
||||
failures.nameRequired ||
|
||||
failures.imageRequired ||
|
||||
failures.nameInvalid ||
|
||||
failures.aliasInvalid
|
||||
);
|
||||
|
||||
let canSave = false;
|
||||
$: canSave = editing && !failures.any;
|
||||
|
||||
function editImage() {
|
||||
imageInput.click();
|
||||
|
||||
jQuery(imageInput).one('change', () => {
|
||||
if (!imageInput.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = `${utils.generateUUID()}-${imageInput.files[0].name}`;
|
||||
fileNameInput.value = fileName;
|
||||
|
||||
jQuery(imageForm).ajaxSubmit({
|
||||
headers: {
|
||||
'x-csrf-token': config.csrf_token,
|
||||
},
|
||||
success: () => {
|
||||
image = fileName;
|
||||
imageInput.value = '';
|
||||
},
|
||||
error: () => {
|
||||
const err = Error('Failed to upload file');
|
||||
console.error(err);
|
||||
app.alertError(err);
|
||||
imageInput.value = '';
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function onSave() {
|
||||
dispatch('save', {
|
||||
id,
|
||||
emoji: {
|
||||
name,
|
||||
image,
|
||||
aliases,
|
||||
ascii,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let deleting = false;
|
||||
let deleted = false;
|
||||
|
||||
function onDelete() {
|
||||
deleting = true;
|
||||
}
|
||||
function confirmDelete() {
|
||||
deleting = false;
|
||||
deleted = true;
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch('delete', {
|
||||
id,
|
||||
});
|
||||
}, 250);
|
||||
}
|
||||
function cancelDelete() {
|
||||
deleting = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<tr class:fadeout={deleted}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control emoji-name"
|
||||
bind:value={name}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
on:click={editImage}
|
||||
>{@html buildEmoji({
|
||||
character: '',
|
||||
pack: 'customizations',
|
||||
keywords: [],
|
||||
name,
|
||||
aliases,
|
||||
image,
|
||||
})}</button>
|
||||
<form
|
||||
action={`${config.relative_path}/api/admin/plugins/emoji/upload`}
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
style="display: none;"
|
||||
bind:this={imageForm}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="emojiImage"
|
||||
accept="image/*"
|
||||
bind:this={imageInput}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="fileName"
|
||||
bind:this={fileNameInput}
|
||||
/>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
bind:value={newAlias}
|
||||
/>
|
||||
<div class="input-group-addon"><button class="btn btn-default" on:click={addAlias}>+</button></div>
|
||||
</div>
|
||||
<span>
|
||||
{#each aliases as a}
|
||||
<button class="btn btn-info btn-xs" on:click={() => removeAlias(a)}>{a} x</button>
|
||||
{/each}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
bind:value={newAscii}
|
||||
/>
|
||||
<div class="input-group-addon"><button class="btn btn-default" on:click={addAscii}>+</button></div>
|
||||
</div>
|
||||
<span>
|
||||
{#each ascii as a}
|
||||
<button class="btn btn-info btn-xs" on:click={() => removeAscii(a)}>{a} x</button>
|
||||
{/each}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{#if editing || empty}
|
||||
<button
|
||||
class="btn btn-success"
|
||||
type="button"
|
||||
on:click={onSave}
|
||||
disabled={!canSave}
|
||||
>
|
||||
<i class="fa fa-check"></i>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-warning"
|
||||
type="button"
|
||||
on:click={onDelete}
|
||||
disabled={deleting || deleted}
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{#if deleting || deleted}
|
||||
<tr class:fadeout={deleted}>
|
||||
<td>
|
||||
<button class="btn btn-default" type="button" disabled={deleted} on:click={cancelDelete}>Cancel</button>
|
||||
</td>
|
||||
<td colSpan={3}>
|
||||
<span class="help-block">Are you sure you want to delete this emoji?</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-danger" type="button" disabled={deleted} on:click={confirmDelete}>Yes</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.nameRequired}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Name</strong> is required</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.imageRequired}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Image</strong> is required</span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.nameInvalid}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Name</strong> can only contain letters, numbers, and <code>_-+.</code></span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
{#if editing && failures.aliasInvalid}
|
||||
<tr class="text-danger">
|
||||
<td colSpan={5}>
|
||||
<span><strong>Aliases</strong> can only contain letters, numbers, and <code>_-+.</code></span>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
tr {
|
||||
opacity: 1;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
}
|
||||
tr.fadeout {
|
||||
opacity: 0;
|
||||
}
|
||||
input.emoji-name {
|
||||
max-width: 125px;
|
||||
}
|
||||
</style>
|
68
acp/src/EmojiList.svelte
Normal file
68
acp/src/EmojiList.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script lang="ts">
|
||||
import api from 'api';
|
||||
import app from 'app';
|
||||
import Emoji from './Emoji.svelte';
|
||||
|
||||
export let emojis: {
|
||||
[id: number]: CustomEmoji,
|
||||
};
|
||||
|
||||
let emojiList: { id: number, emoji: CustomEmoji }[];
|
||||
$: {
|
||||
emojiList = Object.keys(emojis).map(key => ({ id: parseInt(key, 10), emoji: emojis[key] }));
|
||||
}
|
||||
|
||||
function onEdit(event: CustomEvent<{ id: number, emoji: CustomEmoji }>) {
|
||||
const { id, emoji } = event.detail;
|
||||
api.put(`/admin/plugins/emoji/customizations/emoji/${id}`, { item: emoji }).then(() => {
|
||||
emojis = {
|
||||
...emojis,
|
||||
[id]: emoji,
|
||||
};
|
||||
}, () => app.alertError());
|
||||
}
|
||||
function onDelete(event: CustomEvent<{ id: number }>) {
|
||||
const { id } = event.detail;
|
||||
api.del(`/admin/plugins/emoji/customizations/emoji/${id}`, {}).then(() => {
|
||||
delete emojis[id];
|
||||
emojis = { ...emojis };
|
||||
}, () => app.alertError());
|
||||
}
|
||||
|
||||
const blank = {
|
||||
name: '',
|
||||
image: '',
|
||||
aliases: [],
|
||||
ascii: [],
|
||||
};
|
||||
|
||||
let resetNew: () => void;
|
||||
let newEmoji = { ...blank };
|
||||
function onAdd(event: CustomEvent<{ id: -1, emoji: CustomEmoji }>) {
|
||||
const { emoji } = event.detail;
|
||||
|
||||
api.post('/admin/plugins/emoji/customizations/emoji', { item: emoji }).then(({ id }) => {
|
||||
emojis = {
|
||||
...emojis,
|
||||
[id]: emoji,
|
||||
};
|
||||
|
||||
newEmoji = { ...blank };
|
||||
resetNew();
|
||||
}, () => app.alertError());
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Image</th><th>Aliases</th><th>ASCII patterns</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each emojiList as item (item.id)}
|
||||
<Emoji {...item} on:save={onEdit} on:delete={onDelete} />
|
||||
{/each}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<Emoji bind:reset={resetNew} id={-1} emoji={newEmoji} on:save={onAdd} />
|
||||
</tfoot>
|
||||
</table>
|
69
acp/src/ItemList.svelte
Normal file
69
acp/src/ItemList.svelte
Normal file
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import api from 'api';
|
||||
import app from 'app';
|
||||
import Adjunct from './Adjunct.svelte';
|
||||
|
||||
type ItemType = CustomAdjunct;
|
||||
export let type = 'adjunct';
|
||||
export let record: {
|
||||
[id: number]: ItemType,
|
||||
};
|
||||
|
||||
let list: { id: number, item: ItemType }[];
|
||||
$: {
|
||||
list = Object.keys(record).map(key => ({ id: parseInt(key, 10), item: record[key] }));
|
||||
}
|
||||
|
||||
function onEdit(event: CustomEvent<{ id: number, item: CustomAdjunct }>) {
|
||||
const { id, item } = event.detail;
|
||||
api.put(`/admin/plugins/emoji/customizations/${type}/${id}`, { item }).then(() => {
|
||||
record = {
|
||||
...record,
|
||||
[id]: item,
|
||||
};
|
||||
}, () => app.alertError());
|
||||
}
|
||||
function onDelete(event: CustomEvent<{ id: number }>) {
|
||||
const { id } = event.detail;
|
||||
api.del(`/admin/plugins/emoji/customizations/${type}/${id}`, {}).then(() => {
|
||||
delete record[id];
|
||||
record = { ...record };
|
||||
}, () => app.alertError());
|
||||
}
|
||||
|
||||
const blank = {
|
||||
name: '',
|
||||
aliases: [],
|
||||
ascii: [],
|
||||
};
|
||||
|
||||
let resetNew: () => void;
|
||||
let newItem = { ...blank };
|
||||
function onAdd(event: CustomEvent<{ id: -1, item: CustomAdjunct }>) {
|
||||
const { item } = event.detail;
|
||||
|
||||
api.post(`/admin/plugins/emoji/customizations/${type}`, { item }).then(({ id }) => {
|
||||
record = {
|
||||
...record,
|
||||
[id]: item,
|
||||
};
|
||||
|
||||
newItem = { ...blank };
|
||||
resetNew();
|
||||
}, () => app.alertError());
|
||||
}
|
||||
</script>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Image</th><th>Aliases</th><th>ASCII patterns</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each list as item (item.id)}
|
||||
<Adjunct {...item} on:save={onEdit} on:delete={onDelete} />
|
||||
{/each}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<Adjunct bind:reset={resetNew} id={-1} item={newItem} on:save={onAdd} />
|
||||
</tfoot>
|
||||
</table>
|
105
acp/src/Settings.svelte
Normal file
105
acp/src/Settings.svelte
Normal file
|
@ -0,0 +1,105 @@
|
|||
<script lang="ts">
|
||||
import api from 'api';
|
||||
import app from 'app';
|
||||
import { init as initEmoji } from 'emoji';
|
||||
|
||||
import Customize from './Customize.svelte';
|
||||
import Translate from './Translate.svelte';
|
||||
|
||||
export let settings: Settings;
|
||||
|
||||
function updateSettings() {
|
||||
api.put('/admin/plugins/emoji/settings', settings).then(
|
||||
() => app.alertSuccess(),
|
||||
err => app.alertError(err)
|
||||
);
|
||||
}
|
||||
|
||||
function buildAssets() {
|
||||
api.put('/admin/plugins/emoji/build', {}).then(
|
||||
() => app.alertSuccess(),
|
||||
err => app.alertError(err)
|
||||
);
|
||||
}
|
||||
|
||||
let openCustomize: () => void;
|
||||
|
||||
interface CustomizationsData {
|
||||
emojis: {
|
||||
[id: number]: CustomEmoji
|
||||
};
|
||||
adjuncts: {
|
||||
[id: number]: CustomAdjunct
|
||||
}
|
||||
}
|
||||
|
||||
let customizationsData: Promise<CustomizationsData>;
|
||||
function getCustomizations(): Promise<CustomizationsData> {
|
||||
return api.get('/admin/plugins/emoji/customizations', {});
|
||||
}
|
||||
function showCustomize() {
|
||||
customizationsData = customizationsData || Promise.all([
|
||||
getCustomizations(),
|
||||
initEmoji(),
|
||||
]).then(([data]) => data);
|
||||
customizationsData.then(() => setTimeout(() => openCustomize(), 0));
|
||||
}
|
||||
</script>
|
||||
|
||||
<form id="emoji-settings">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label for="emoji-parseAscii">
|
||||
<input id="emoji-parseAscii" type="checkbox" bind:checked={settings.parseAscii} />
|
||||
<Translate src="[[admin/plugins/emoji:settings.parseAscii]]"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emoji-parseNative">
|
||||
<input id="emoji-parseNative" type="checkbox" bind:checked={settings.parseNative} />
|
||||
<Translate src="[[admin/plugins/emoji:settings.parseNative]]"/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emoji-customFirst">
|
||||
<input id="emoji-customFirst" type="checkbox" bind:checked={settings.customFirst} />
|
||||
<Translate src="[[admin/plugins/emoji:settings.customFirst]]"/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
<div class="form-group">
|
||||
<button type="button" on:click={buildAssets} class="btn btn-primary" aria-describedby="emoji-build_description"><Translate src="[[admin/plugins/emoji:build]]"/></button>
|
||||
<p id="emoji-build_description" class="help-block">
|
||||
<Translate src="[[admin/plugins/emoji:build_description]]"/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{#if customizationsData}
|
||||
{#await customizationsData then customizations}
|
||||
<Customize bind:show={openCustomize} data={customizations} />
|
||||
{/await}
|
||||
{/if}
|
||||
|
||||
<button on:click={updateSettings} class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
|
||||
<button on:click={showCustomize} class="edit floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button.floating-button.edit {
|
||||
left: 30px;
|
||||
margin-left: 0;
|
||||
background: #ff4081 !important;
|
||||
}
|
||||
</style>
|
11
acp/src/Translate.svelte
Normal file
11
acp/src/Translate.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { Translator } from 'translator';
|
||||
|
||||
const translator = Translator.create();
|
||||
|
||||
export let src: string;
|
||||
</script>
|
||||
|
||||
{#await translator.translate(src) then translated}
|
||||
{@html translated}
|
||||
{/await}
|
16
acp/src/admin.ts
Normal file
16
acp/src/admin.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import jQuery from 'jquery';
|
||||
import ajaxify from 'ajaxify';
|
||||
|
||||
import Settings from './Settings.svelte';
|
||||
|
||||
jQuery(window).on('action:ajaxify.end', () => {
|
||||
if (ajaxify.data.template['admin/plugins/emoji']) {
|
||||
// eslint-disable-next-line no-new
|
||||
new Settings({
|
||||
target: document.getElementById('content'),
|
||||
props: {
|
||||
settings: ajaxify.data.settings,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
64
acp/src/modules.d.ts
vendored
Normal file
64
acp/src/modules.d.ts
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
interface JQuery {
|
||||
ajaxSubmit: any;
|
||||
draggable: any;
|
||||
modal: any;
|
||||
}
|
||||
|
||||
module 'ajaxify' {
|
||||
const ajaxify: {
|
||||
data: any;
|
||||
};
|
||||
export default ajaxify;
|
||||
}
|
||||
|
||||
module 'textcomplete' {
|
||||
const Textcomplete: any;
|
||||
export default Textcomplete;
|
||||
}
|
||||
|
||||
module 'config' {
|
||||
const config: {
|
||||
relative_path: string;
|
||||
'cache-buster': string;
|
||||
emojiCustomFirst: boolean;
|
||||
csrf_token: string;
|
||||
};
|
||||
export default config;
|
||||
}
|
||||
|
||||
declare module 'app' {
|
||||
const app: {
|
||||
alertSuccess(message?: string): void;
|
||||
alertError(message?: string): void;
|
||||
alertError(error?: Error): void;
|
||||
};
|
||||
export default app;
|
||||
}
|
||||
|
||||
declare module 'api' {
|
||||
const api: {
|
||||
get(route: string, payload: NonNullable<unknown>): Promise<any>;
|
||||
head(route: string, payload: NonNullable<unknown>): Promise<any>;
|
||||
post(route: string, payload: NonNullable<unknown>): Promise<any>;
|
||||
put(route: string, payload: NonNullable<unknown>): Promise<any>;
|
||||
del(route: string, payload: NonNullable<unknown>): Promise<any>;
|
||||
};
|
||||
export default api;
|
||||
}
|
||||
|
||||
declare module 'translator' {
|
||||
export class Translator {
|
||||
public static create(lang?: string): Translator;
|
||||
|
||||
public translate(input: string): Promise<string>;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'utils' {
|
||||
const utils: {
|
||||
generateUUID(): string;
|
||||
};
|
||||
export default utils;
|
||||
}
|
12
acp/tsconfig.json
Normal file
12
acp/tsconfig.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
|
||||
"include": ["src/**/*", "../lib/types.d.ts"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
|
||||
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"emoji": ["../public/lib/emoji"]
|
||||
}
|
||||
},
|
||||
}
|
|
@ -10,7 +10,7 @@ import { uniq } from 'lodash';
|
|||
|
||||
import * as cssBuilders from './css-builders';
|
||||
import { clearCache } from './parse';
|
||||
import { getCustomizations } from './customizations';
|
||||
import { getAll as getCustomizations } from './customizations';
|
||||
|
||||
const nconf = require.main.require('nconf');
|
||||
const winston = require.main.require('winston');
|
||||
|
@ -30,7 +30,7 @@ export const charactersFile = join(assetsDir, 'characters.json');
|
|||
export const categoriesFile = join(assetsDir, 'categories.json');
|
||||
export const packsFile = join(assetsDir, 'packs.json');
|
||||
|
||||
export default async function build() {
|
||||
export default async function build(): Promise<void> {
|
||||
winston.verbose('[emoji] Building emoji assets');
|
||||
|
||||
// fetch the emoji definitions
|
||||
|
@ -116,7 +116,7 @@ export default async function build() {
|
|||
categoriesInfo[category] = uniq(categoriesInfo[category]);
|
||||
});
|
||||
|
||||
customizations.emojis.forEach((emoji) => {
|
||||
Object.values(customizations.emojis).forEach((emoji) => {
|
||||
const name = emoji.name.toLowerCase();
|
||||
|
||||
table[name] = {
|
||||
|
@ -144,7 +144,7 @@ export default async function build() {
|
|||
categoriesInfo.custom = categoriesInfo.custom || [];
|
||||
categoriesInfo.custom.push(name);
|
||||
});
|
||||
customizations.adjuncts.forEach((adjunct) => {
|
||||
Object.values(customizations.adjuncts).forEach((adjunct) => {
|
||||
const name = adjunct.name;
|
||||
if (!table[name]) { return; }
|
||||
|
||||
|
|
|
@ -5,16 +5,19 @@ import multer from 'multer';
|
|||
|
||||
import * as settings from './settings';
|
||||
import { build } from './pubsub';
|
||||
import * as customizations from './customizations';
|
||||
|
||||
const nconf = require.main.require('nconf');
|
||||
const { setupApiRoute } = require.main.require('./src/routes/helpers');
|
||||
const { formatApiResponse } = require.main.require('./src/controllers/helpers');
|
||||
|
||||
// eslint-disable-next-line import/no-dynamic-require
|
||||
// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
|
||||
const version: string = require(join(__dirname, '../../package.json')).version;
|
||||
|
||||
export default function controllers({ router, middleware }: {
|
||||
router: Router;
|
||||
middleware: { admin: { [key: string]: RequestHandler } };
|
||||
}) {
|
||||
middleware: { authenticate: RequestHandler; admin: { [key: string]: RequestHandler } };
|
||||
}): void {
|
||||
const renderAdmin: RequestHandler = (req, res, next) => {
|
||||
settings.get().then(sets => setImmediate(() => {
|
||||
res.render('admin/plugins/emoji', {
|
||||
|
@ -44,6 +47,66 @@ export default function controllers({ router, middleware }: {
|
|||
};
|
||||
router.get('/api/admin/plugins/emoji/build', adminBuild);
|
||||
|
||||
const updateSettings: RequestHandler = async (req, res) => {
|
||||
const data = req.body;
|
||||
await settings.set({
|
||||
parseAscii: !!data.parseAscii,
|
||||
parseNative: !!data.parseNative,
|
||||
customFirst: !!data.customFirst,
|
||||
});
|
||||
formatApiResponse(200, res);
|
||||
};
|
||||
setupApiRoute(router, 'put', '/api/v3/admin/plugins/emoji/settings', [middleware.authenticate, middleware.admin.checkPrivileges], updateSettings);
|
||||
|
||||
const buildAssets: RequestHandler = async (req, res) => {
|
||||
await build();
|
||||
formatApiResponse(200, res);
|
||||
};
|
||||
setupApiRoute(router, 'put', '/api/v3/admin/plugins/emoji/build', [middleware.authenticate, middleware.admin.checkPrivileges], buildAssets);
|
||||
|
||||
const provideCustomizations: RequestHandler = async (req, res) => {
|
||||
const data = await customizations.getAll();
|
||||
formatApiResponse(200, res, data);
|
||||
};
|
||||
setupApiRoute(router, 'get', '/api/v3/admin/plugins/emoji/customizations', [middleware.authenticate, middleware.admin.checkPrivileges], provideCustomizations);
|
||||
|
||||
const addCustomization: RequestHandler = async (req, res) => {
|
||||
const type = req.params.type;
|
||||
const item = req.body.item;
|
||||
if (!['emoji', 'adjunct'].includes(type)) {
|
||||
formatApiResponse(400, res);
|
||||
return;
|
||||
}
|
||||
const id = await customizations.add({ type, item });
|
||||
formatApiResponse(200, res, { id });
|
||||
};
|
||||
setupApiRoute(router, 'post', '/api/v3/admin/plugins/emoji/customizations/:type', [middleware.authenticate, middleware.admin.checkPrivileges], addCustomization);
|
||||
|
||||
const editCustomization: RequestHandler = async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const type = req.params.type;
|
||||
const item = req.body.item;
|
||||
if (!['emoji', 'adjunct'].includes(type)) {
|
||||
formatApiResponse(400, res);
|
||||
return;
|
||||
}
|
||||
await customizations.edit({ type, id, item });
|
||||
formatApiResponse(200, res);
|
||||
};
|
||||
setupApiRoute(router, 'put', '/api/v3/admin/plugins/emoji/customizations/:type/:id', [middleware.authenticate, middleware.admin.checkPrivileges], editCustomization);
|
||||
|
||||
const deleteCustomization: RequestHandler = async (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
const type = req.params.type;
|
||||
if (!['emoji', 'adjunct'].includes(type)) {
|
||||
formatApiResponse(400, res);
|
||||
return;
|
||||
}
|
||||
await customizations.remove({ type, id });
|
||||
formatApiResponse(200, res);
|
||||
};
|
||||
setupApiRoute(router, 'delete', '/api/v3/admin/plugins/emoji/customizations/:type/:id', [middleware.authenticate, middleware.admin.checkPrivileges], deleteCustomization);
|
||||
|
||||
const uploadEmoji: RequestHandler = (req, res, next) => {
|
||||
if (!req.file) {
|
||||
res.sendStatus(400);
|
||||
|
|
|
@ -4,7 +4,7 @@ const buster = require.main.require('./src/meta').config['cache-buster'];
|
|||
const nconf = require.main.require('nconf');
|
||||
const url = nconf.get('url');
|
||||
|
||||
export function images(pack: EmojiDefinition) {
|
||||
export function images(pack: EmojiDefinition): string {
|
||||
return `.emoji-${pack.id} {` +
|
||||
'display: inline-block;' +
|
||||
'height: 23px;' +
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import hash from 'string-hash';
|
||||
|
||||
const adminSockets = require.main.require('./src/socket.io/admin');
|
||||
const db = require.main.require('./src/database');
|
||||
|
||||
const emojisKey = 'emoji:customizations:emojis';
|
||||
|
@ -10,52 +7,38 @@ interface SortedResult {
|
|||
value: string;
|
||||
score: number;
|
||||
}
|
||||
export const getCustomizations = async (): Promise<Customizations> => {
|
||||
export async function getAll(): Promise<Customizations> {
|
||||
const [emojis, adjuncts]: [SortedResult[], SortedResult[]] = await Promise.all([
|
||||
db.getSortedSetRangeWithScores(emojisKey, 0, -1),
|
||||
db.getSortedSetRangeWithScores(adjunctsKey, 0, -1),
|
||||
]);
|
||||
|
||||
const emojisParsed: CustomEmoji[] = emojis.map(emoji => JSON.parse(emoji.value));
|
||||
const adjunctsParsed: CustomAdjunct[] = adjuncts.map(adjunct => JSON.parse(adjunct.value));
|
||||
|
||||
return {
|
||||
emojis: emojisParsed,
|
||||
adjuncts: adjunctsParsed,
|
||||
emojis: Object.fromEntries(emojis.map(({ value, score }) => [score, JSON.parse(value)])),
|
||||
adjuncts: Object.fromEntries(adjuncts.map(({ value, score }) => [score, JSON.parse(value)])),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const editThing = async (key: string, name: string, thing: CustomEmoji | CustomAdjunct) => {
|
||||
const num = hash(name);
|
||||
await db.sortedSetsRemoveRangeByScore([key], num, num);
|
||||
export async function add({ type, item }: { type: string, item: unknown }): Promise<string> {
|
||||
const key = type === 'emoji' ? emojisKey : adjunctsKey;
|
||||
// get maximum score from set
|
||||
const [result] = await db.getSortedSetRevRangeWithScores(key, 0, 1);
|
||||
const lastId = (result && result.score) || 1;
|
||||
const id = lastId + 1;
|
||||
await db.sortedSetAdd(key, id, JSON.stringify(item));
|
||||
return id;
|
||||
}
|
||||
|
||||
const thingNum = hash(thing.name);
|
||||
await db.sortedSetAdd(key, thingNum, JSON.stringify(thing));
|
||||
};
|
||||
export async function edit({ type, id, item }: {
|
||||
type: string,
|
||||
id: number,
|
||||
item: unknown,
|
||||
}): Promise<void> {
|
||||
const key = type === 'emoji' ? emojisKey : adjunctsKey;
|
||||
await db.sortedSetsRemoveRangeByScore([key], id, id);
|
||||
await db.sortedSetAdd(key, id, JSON.stringify(item));
|
||||
}
|
||||
|
||||
const deleteThing = async (key: string, name: string) => {
|
||||
const num = hash(name);
|
||||
await db.sortedSetsRemoveRangeByScore([key], num, num);
|
||||
};
|
||||
|
||||
const emojiSockets = {
|
||||
getCustomizations,
|
||||
editEmoji: async (
|
||||
socket: SocketIO.Socket,
|
||||
[name, emoji]: [string, CustomEmoji]
|
||||
) => editThing(emojisKey, name, emoji),
|
||||
deleteEmoji: async (
|
||||
socket: SocketIO.Socket,
|
||||
name: string
|
||||
) => deleteThing(emojisKey, name),
|
||||
editAdjunct: async (
|
||||
socket: SocketIO.Socket,
|
||||
[name, adjunct]: [string, CustomAdjunct]
|
||||
) => editThing(adjunctsKey, name, adjunct),
|
||||
deleteAdjunct: async (
|
||||
socket: SocketIO.Socket,
|
||||
name: string
|
||||
) => deleteThing(adjunctsKey, name),
|
||||
};
|
||||
|
||||
adminSockets.plugins.emoji = emojiSockets;
|
||||
export async function remove({ type, id }: { type: string, id: number }): Promise<void> {
|
||||
const key = type === 'emoji' ? emojisKey : adjunctsKey;
|
||||
await db.sortedSetsRemoveRangeByScore([key], id, id);
|
||||
}
|
||||
|
|
40
lib/index.ts
40
lib/index.ts
|
@ -1,17 +1,15 @@
|
|||
import { access } from 'fs-extra';
|
||||
|
||||
import * as settings from './settings';
|
||||
import * as plugins from './plugins';
|
||||
import * as parse from './parse';
|
||||
import { tableFile } from './build';
|
||||
import { build } from './pubsub';
|
||||
import controllers from './controllers';
|
||||
import './customizations';
|
||||
|
||||
const nconf = require.main.require('nconf');
|
||||
const buster = require.main.require('./src/meta').config['cache-buster'];
|
||||
|
||||
const init = async (params: any): Promise<void> => {
|
||||
export async function init(params: any): Promise<void> {
|
||||
controllers(params);
|
||||
|
||||
const sets = await settings.get();
|
||||
|
@ -39,35 +37,35 @@ const init = async (params: any): Promise<void> => {
|
|||
if (shouldBuild) {
|
||||
await build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const adminMenu = (header: {
|
||||
export async function adminMenu<Payload extends {
|
||||
plugins: { route: string; icon: string; name: string }[];
|
||||
}, callback: NodeBack) => {
|
||||
}>(header: Payload): Promise<Payload> {
|
||||
header.plugins.push({
|
||||
route: '/plugins/emoji',
|
||||
icon: 'fa-smile-o',
|
||||
name: 'Emoji',
|
||||
});
|
||||
callback(null, header);
|
||||
};
|
||||
return header;
|
||||
}
|
||||
|
||||
const composerFormatting = (data: {
|
||||
export async function composerFormatting<Payload extends {
|
||||
options: { name: string; className: string; title: string }[];
|
||||
}, callback: NodeBack) => {
|
||||
}>(data: Payload): Promise<Payload> {
|
||||
data.options.push({
|
||||
name: 'emoji-add-emoji',
|
||||
className: 'fa fa-smile-o emoji-add-emoji',
|
||||
title: '[[emoji:composer.title]]',
|
||||
});
|
||||
callback(null, data);
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
const addStylesheet = (data: {
|
||||
export async function addStylesheet<Payload extends {
|
||||
links: {
|
||||
rel: string; type?: string; href: string;
|
||||
}[];
|
||||
}, callback: NodeBack) => {
|
||||
}>(data: Payload): Promise<Payload> {
|
||||
const rel = nconf.get('relative_path');
|
||||
|
||||
data.links.push({
|
||||
|
@ -75,22 +73,16 @@ const addStylesheet = (data: {
|
|||
href: `${rel}/plugins/nodebb-plugin-emoji/emoji/styles.css?${buster}`,
|
||||
});
|
||||
|
||||
callback(null, data);
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
const configGet = async (config: any): Promise<any> => {
|
||||
export async function configGet(config: any): Promise<any> {
|
||||
const customFirst = await settings.getOne('customFirst');
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.emojiCustomFirst = customFirst;
|
||||
return config;
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
init,
|
||||
adminMenu,
|
||||
composerFormatting,
|
||||
plugins,
|
||||
parse,
|
||||
addStylesheet,
|
||||
configGet,
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ let metaCache: {
|
|||
characters: MetaData.Characters;
|
||||
charPattern: RegExp;
|
||||
} = null;
|
||||
export function clearCache() {
|
||||
export function clearCache(): void {
|
||||
metaCache = null;
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,7 @@ const outsideCode = /(^|<\/code>)([^<]*|<(?!code[^>]*>))*(<code[^>]*>|$)/g;
|
|||
const outsideElements = /(<[^>]*>)?([^<>]*)/g;
|
||||
const emojiPattern = /:([a-z\-.+0-9_]+):/g;
|
||||
|
||||
export const buildEmoji = (emoji: StoredEmoji, whole: string) => {
|
||||
export const buildEmoji = (emoji: StoredEmoji, whole: string): string => {
|
||||
if (emoji.image) {
|
||||
const route = `${url}/plugins/nodebb-plugin-emoji/emoji/${emoji.pack}`;
|
||||
return `<img
|
||||
|
@ -124,7 +124,7 @@ const options: ParseOptions = {
|
|||
native: false,
|
||||
};
|
||||
|
||||
export function setOptions(newOptions: ParseOptions) {
|
||||
export function setOptions(newOptions: ParseOptions): void {
|
||||
Object.assign(options, newOptions);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
import { readFile } from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
|
||||
import { build } from './pubsub';
|
||||
|
||||
const nconf = require.main.require('nconf');
|
||||
const baseDir = nconf.get('base_dir');
|
||||
|
||||
// build when a plugin is (de)activated if that plugin is an emoji pack
|
||||
const toggle = async ({ id }: { id: string }) => {
|
||||
let file;
|
||||
try {
|
||||
file = await readFile(join(baseDir, 'node_modules', id, 'plugin.json'), 'utf8');
|
||||
} catch (err) {
|
||||
if (err && err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const plugin = JSON.parse(file);
|
||||
|
||||
const hasHook = plugin.hooks && plugin.hooks
|
||||
.some((hook: { hook: string }) => hook.hook === 'filter:emoji.packs');
|
||||
|
||||
if (hasHook) {
|
||||
await build();
|
||||
}
|
||||
};
|
||||
|
||||
export {
|
||||
toggle as activation,
|
||||
toggle as deactivation,
|
||||
};
|
|
@ -8,7 +8,7 @@ const pubsub = require.main.require('./src/pubsub');
|
|||
|
||||
const primary = nconf.get('isPrimary') === 'true' || nconf.get('isPrimary') === true;
|
||||
|
||||
export async function build() {
|
||||
export async function build(): Promise<void> {
|
||||
if (pubsub.pubClient) {
|
||||
pubsub.publish('emoji:build', {
|
||||
hostname: hostname(),
|
||||
|
|
104
lib/settings.ts
104
lib/settings.ts
|
@ -1,74 +1,54 @@
|
|||
const settings: {
|
||||
get(key: string): Promise<{ [key: string]: any }>;
|
||||
set(key: string, value: any): Promise<void>;
|
||||
getOne(key: string, field: string): Promise<any>;
|
||||
setOne(key: string, field: string, value: any): Promise<void>;
|
||||
get(key: string): Promise<{ [key: string]: unknown } | null>;
|
||||
set(key: string, value: unknown): Promise<void>;
|
||||
getOne(key: string, field: string): Promise<unknown>;
|
||||
setOne(key: string, field: string, value: unknown): Promise<void>;
|
||||
} = require.main.require('./src/meta').settings;
|
||||
|
||||
interface Settings {
|
||||
parseNative: boolean;
|
||||
parseAscii: boolean;
|
||||
customFirst: boolean;
|
||||
}
|
||||
|
||||
const defaults: Settings = {
|
||||
parseNative: true,
|
||||
parseAscii: true,
|
||||
customFirst: false,
|
||||
};
|
||||
|
||||
const get = async (): Promise<{ [key: string]: any }> => {
|
||||
const data = await settings.get('emoji');
|
||||
const sets: Partial<Settings> = {};
|
||||
|
||||
Object.keys(defaults).forEach((key: keyof Settings) => {
|
||||
const defaultVal = defaults[key];
|
||||
const str = data[key];
|
||||
|
||||
if (typeof str !== 'string') {
|
||||
sets[key] = defaultVal;
|
||||
return;
|
||||
}
|
||||
|
||||
const val = JSON.parse(str);
|
||||
if (typeof val !== typeof defaultVal) {
|
||||
sets[key] = defaultVal;
|
||||
return;
|
||||
}
|
||||
|
||||
sets[key] = val;
|
||||
});
|
||||
|
||||
return sets;
|
||||
};
|
||||
const set = async (data: {
|
||||
[key: string]: any;
|
||||
}) => {
|
||||
const sets: Partial<Record<keyof Settings, string>> = {};
|
||||
Object.keys(data).forEach((key: keyof Settings) => {
|
||||
sets[key] = JSON.stringify(data[key]);
|
||||
});
|
||||
|
||||
await settings.set('emoji', sets);
|
||||
};
|
||||
const getOne = async (field: keyof Settings): Promise<any> => {
|
||||
const str = await settings.getOne('emoji', field);
|
||||
|
||||
const defaultVal = defaults[field];
|
||||
let val = JSON.parse(str);
|
||||
if (typeof val !== typeof defaultVal) {
|
||||
val = defaultVal;
|
||||
function fromStore<
|
||||
K extends keyof Settings
|
||||
>(key: K, x: unknown): Settings[K] {
|
||||
if (typeof x === typeof defaults[key]) {
|
||||
return x as Settings[K];
|
||||
}
|
||||
if (typeof x === 'string') {
|
||||
try {
|
||||
return JSON.parse(x) ?? defaults[key];
|
||||
} catch {
|
||||
return defaults[key];
|
||||
}
|
||||
}
|
||||
return defaults[key];
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
const setOne = async (field: string, value: any) => {
|
||||
export async function get(): Promise<Settings> {
|
||||
const data = await settings.get('emoji');
|
||||
|
||||
return {
|
||||
parseNative: fromStore('parseNative', data?.parseNative),
|
||||
parseAscii: fromStore('parseAscii', data?.parseAscii),
|
||||
customFirst: fromStore('customFirst', data?.customFirst),
|
||||
};
|
||||
}
|
||||
export async function set(data: Settings): Promise<void> {
|
||||
await settings.set('emoji', {
|
||||
parseNative: JSON.stringify(data.parseNative),
|
||||
parseAscii: JSON.stringify(data.parseAscii),
|
||||
customFirst: JSON.stringify(data.customFirst),
|
||||
});
|
||||
}
|
||||
export async function getOne<K extends keyof Settings>(field: K): Promise<Settings[K]> {
|
||||
const val = await settings.getOne('emoji', field);
|
||||
return fromStore(field, val);
|
||||
}
|
||||
export async function setOne<
|
||||
K extends keyof Settings
|
||||
>(field: K, value: Settings[K]): Promise<void> {
|
||||
await settings.setOne('emoji', field, JSON.stringify(value));
|
||||
};
|
||||
|
||||
export {
|
||||
get,
|
||||
set,
|
||||
getOne,
|
||||
setOne,
|
||||
};
|
||||
}
|
||||
|
|
15
lib/types.d.ts
vendored
15
lib/types.d.ts
vendored
|
@ -191,11 +191,16 @@ interface CustomAdjunct {
|
|||
}
|
||||
|
||||
interface Customizations {
|
||||
emojis: CustomEmoji[];
|
||||
adjuncts: CustomAdjunct[];
|
||||
emojis: {
|
||||
[id: number]: CustomEmoji
|
||||
};
|
||||
adjuncts: {
|
||||
[id: number]: CustomAdjunct
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'string-hash' {
|
||||
const hash: (str: string) => number;
|
||||
export = hash;
|
||||
interface Settings {
|
||||
parseNative: boolean;
|
||||
parseAscii: boolean;
|
||||
customFirst: boolean;
|
||||
}
|
||||
|
|
43
package.json
43
package.json
|
@ -11,7 +11,7 @@
|
|||
"url": "https://github.com/NodeBB/nodebb-plugin-emoji.git"
|
||||
},
|
||||
"nbbpm": {
|
||||
"compatibility": "^1.17.0"
|
||||
"compatibility": "~1.16.0 || ~1.17.0"
|
||||
},
|
||||
"keywords": [
|
||||
"nodebb",
|
||||
|
@ -21,33 +21,46 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"fs-extra": "^9.1.0",
|
||||
"lodash": "^4.17.20",
|
||||
"multer": "^1.4.2",
|
||||
"preact": "^10.5.12",
|
||||
"string-hash": "^1.1.3"
|
||||
"lodash": "^4.17.21",
|
||||
"multer": "^1.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.1.0",
|
||||
"@rollup/plugin-node-resolve": "^11.2.0",
|
||||
"@rollup/plugin-typescript": "^8.2.0",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"@tsconfig/svelte": "^1.0.10",
|
||||
"@types/bootstrap": "^3.4.0",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/fs-extra": "^9.0.7",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/multer": "^1.4.5",
|
||||
"@types/nconf": "^0.10.0",
|
||||
"@types/node": "^14.14.24",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@types/socket.io": "^2.1.13",
|
||||
"@typescript-eslint/eslint-plugin": "^4.14.2",
|
||||
"@typescript-eslint/parser": "^4.14.2",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.1",
|
||||
"@typescript-eslint/parser": "^4.15.1",
|
||||
"eslint": "^7.20.0",
|
||||
"eslint-config-airbnb-base": "14.2.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-svelte3": "^3.1.1",
|
||||
"rollup": "^2.39.0",
|
||||
"rollup-plugin-css-only": "^3.1.0",
|
||||
"rollup-plugin-livereload": "^2.0.0",
|
||||
"rollup-plugin-svelte": "^7.1.0",
|
||||
"semver": "^7.3.4",
|
||||
"typescript": "^4.1.3"
|
||||
"svelte": "^3.23.3",
|
||||
"svelte-check": "^1.1.35",
|
||||
"svelte-preprocess": "^4.6.9",
|
||||
"tslib": "^2.1.0",
|
||||
"typescript": "^4.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint lib/* public/**/*.ts public/**/*.js public/**/*.tsx",
|
||||
"lint": "eslint . && svelte-check",
|
||||
"compile": "tsc -p . && tsc -p public",
|
||||
"pretest": "npm run lint && npm run compile",
|
||||
"dev": "npm run compile && cd acp && rollup -c -w",
|
||||
"build": "npm run compile && cd acp && rollup -c",
|
||||
"pretest": "npm run lint && npm run build",
|
||||
"test": "node build/lib/tests.js",
|
||||
"prepare": "rm -r build; npm run test && mkdir -p build/emoji && touch build/emoji/avoid_npm_ignore"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
rules: {
|
||||
'@typescript-eslint/no-var-requires': ['off']
|
||||
}
|
||||
};
|
||||
'@typescript-eslint/no-var-requires': ['off'],
|
||||
},
|
||||
};
|
||||
|
|
10
plugin.json
10
plugin.json
|
@ -3,12 +3,13 @@
|
|||
"less": [
|
||||
"public/style.less"
|
||||
],
|
||||
"acpLess": [
|
||||
"acp/admin.less"
|
||||
],
|
||||
"modules": {
|
||||
"emoji.js": "build/public/lib/emoji.js",
|
||||
"emoji-dialog.js": "build/public/lib/emoji-dialog.js",
|
||||
"preact.js": "node_modules/preact/dist/preact.umd.js",
|
||||
"preact/devtools.js": "node_modules/preact/devtools/dist/devtools.umd.js",
|
||||
"custom-emoji.js": "build/public/lib/admin/custom-emoji.js"
|
||||
"../admin/plugins/emoji.js": "build/acp/admin.js"
|
||||
},
|
||||
"staticDirs": {
|
||||
"emoji": "build/emoji"
|
||||
|
@ -17,7 +18,6 @@
|
|||
"public/emoji-setup.js"
|
||||
],
|
||||
"acpScripts": [
|
||||
"public/admin.js",
|
||||
"public/emoji-setup.js"
|
||||
],
|
||||
"languages": "public/language",
|
||||
|
@ -29,8 +29,6 @@
|
|||
{ "hook": "filter:composer.formatting", "method": "composerFormatting", "priority": 19 },
|
||||
{ "hook": "filter:parse.raw", "method": "parse.raw", "priority": 9 },
|
||||
{ "hook": "filter:parse.post", "method": "parse.post", "priority": 9 },
|
||||
{ "hook": "action:plugin.activate", "method": "plugins.activation" },
|
||||
{ "hook": "action:plugin.deactivate", "method": "plugins.deactivation" },
|
||||
{ "hook": "filter:meta.getLinkTags", "method": "addStylesheet" },
|
||||
{ "hook": "filter:config.get", "method": "configGet" }
|
||||
]
|
||||
|
|
|
@ -10,8 +10,6 @@ module.exports = {
|
|||
app: true,
|
||||
config: true,
|
||||
Textcomplete: true,
|
||||
},
|
||||
rules: {
|
||||
'max-classes-per-file': 'off',
|
||||
ajaxify: true,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/* eslint-disable prefer-arrow-callback, func-names, strict, no-var */
|
||||
/* eslint-disable vars-on-top, no-plusplus, no-bitwise, no-multi-assign */
|
||||
/* eslint-disable no-nested-ternary, no-labels, no-restricted-syntax */
|
||||
/* eslint-disable no-continue, import/no-amd, import/no-dynamic-require */
|
||||
/* eslint-disable prefer-template, global-require */
|
||||
|
||||
define('admin/plugins/emoji', [], function () {
|
||||
$('#save').on('click', function () {
|
||||
var settings = {
|
||||
parseAscii: !!$('#emoji-parseAscii').prop('checked'),
|
||||
parseNative: !!$('#emoji-parseNative').prop('checked'),
|
||||
customFirst: !!$('#emoji-customFirst').prop('checked'),
|
||||
};
|
||||
$.get(window.config.relative_path + '/api/admin/plugins/emoji/save', { settings: JSON.stringify(settings) }, function () {
|
||||
window.app.alertSuccess();
|
||||
});
|
||||
});
|
||||
|
||||
$('#build').on('click', function () {
|
||||
$.get(window.config.relative_path + '/api/admin/plugins/emoji/build', function () {
|
||||
window.app.alertSuccess();
|
||||
});
|
||||
});
|
||||
|
||||
require.config({
|
||||
shim: {
|
||||
preact: {
|
||||
exports: 'preact',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var addedStyle = false;
|
||||
$('#edit').click(function () {
|
||||
require(['custom-emoji'], function (customEmoji) {
|
||||
customEmoji.init(document.getElementById('editModalBody'), function () {
|
||||
$('#editModal').modal({
|
||||
backdrop: false,
|
||||
show: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
if (!addedStyle) {
|
||||
addedStyle = true;
|
||||
$('head').append(
|
||||
'<style>@import "' + window.config.relative_path + '/plugins/nodebb-plugin-emoji/emoji/styles.css";</style>'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -4,5 +4,5 @@
|
|||
"settings.parseNative": "Replace native unicode emoji characters with emoji provided by emoji packs",
|
||||
"settings.customFirst": "Place your custom emoji first in the dialog",
|
||||
"build": "Build Emoji Assets",
|
||||
"build_description": "Compile the necessary metadata and static assets from all emoji packs. <br> This is usually unnecessary to do yourself, but if emojis aren't showing up, try this first."
|
||||
"build_description": "Compile the necessary metadata and static assets from all emoji packs. <br> After installing and activating an emoji pack, restart NodeBB, Build Emoji Assets, then Restart and Rebuild NodeBB."
|
||||
}
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
"settings.parseNative": "Замените нативные символы смайликов в юникоде на смайлики, предоставляемые наборами смайликов",
|
||||
"settings.customFirst": "Поместите свой собственный смайлик первым в диалоге",
|
||||
"build": "Сборка эмодзи",
|
||||
"build_description": "Компиляция необходимых метаданных и статических ресурсов из всех пакетов Emoji. <br> Обычно это не нужно делать самостоятельно, но если эмодзи не появляются, можно попробовать."
|
||||
"build_description": "Компиляция необходимых метаданных и статических ресурсов из всех пакетов Emoji."
|
||||
}
|
||||
|
|
|
@ -1,857 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import {
|
||||
h,
|
||||
Component,
|
||||
FunctionalComponent,
|
||||
render,
|
||||
JSX,
|
||||
} from 'preact';
|
||||
import { strategy, table, buildEmoji, init as initEmoji } from 'emoji';
|
||||
|
||||
// import 'preact/devtools';
|
||||
|
||||
const setsEqual = (arr1: string[], arr2: string[]) => {
|
||||
if (arr1.length !== arr2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const h1: { [val: string]: boolean } = {};
|
||||
arr1.forEach((val) => { h1[val] = true; });
|
||||
|
||||
return arr2.every(val => h1[val]);
|
||||
};
|
||||
|
||||
interface EmojiProps {
|
||||
editing?: boolean;
|
||||
canSave?: boolean;
|
||||
emoji: CustomEmoji;
|
||||
onSave: Callback;
|
||||
onDelete: Callback;
|
||||
onEditName: Callback<string>;
|
||||
onEditImage: Callback<string>;
|
||||
onEditAliases: Callback<string[]>;
|
||||
onEditAscii: Callback<string[]>;
|
||||
}
|
||||
const Emoji: FunctionalComponent<EmojiProps> = ({
|
||||
editing,
|
||||
canSave,
|
||||
emoji,
|
||||
onSave,
|
||||
onDelete,
|
||||
onEditName,
|
||||
onEditImage,
|
||||
onEditAliases,
|
||||
onEditAscii,
|
||||
}) => {
|
||||
let imageForm: HTMLFormElement;
|
||||
let imageInput: HTMLInputElement;
|
||||
let fileNameInput: HTMLInputElement;
|
||||
|
||||
const editImage = () => {
|
||||
imageInput.click();
|
||||
|
||||
$(imageInput).one('change', () => {
|
||||
if (!imageInput.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileName = `${utils.generateUUID()}-${imageInput.files[0].name}`;
|
||||
fileNameInput.value = fileName;
|
||||
|
||||
$(imageForm).ajaxSubmit({
|
||||
headers: {
|
||||
'x-csrf-token': config.csrf_token,
|
||||
},
|
||||
success: () => {
|
||||
onEditImage(fileName);
|
||||
imageInput.value = '';
|
||||
},
|
||||
error: () => {
|
||||
const err = Error('Failed to upload file');
|
||||
console.error(err);
|
||||
app.alertError(err);
|
||||
imageInput.value = '';
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={emoji.name}
|
||||
onInput={(e: Event) => onEditName((e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-default"
|
||||
onClick={editImage}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: buildEmoji({
|
||||
character: '',
|
||||
pack: 'customizations',
|
||||
keywords: [],
|
||||
name: emoji.name,
|
||||
aliases: emoji.aliases,
|
||||
image: emoji.image,
|
||||
}),
|
||||
}}
|
||||
></button>
|
||||
<form
|
||||
action={`${config.relative_path}/api/admin/plugins/emoji/upload`}
|
||||
method="post"
|
||||
encType="multipart/form-data"
|
||||
style={{ display: 'none' }}
|
||||
ref={(form) => { imageForm = form as HTMLFormElement; }}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="emojiImage"
|
||||
accept="image/*"
|
||||
ref={(input) => { imageInput = input as HTMLInputElement; }}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="fileName"
|
||||
ref={(input) => { fileNameInput = input as HTMLInputElement; }}
|
||||
/>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={emoji.aliases.join(',')}
|
||||
onInput={(e: Event) => onEditAliases(
|
||||
(e.target as HTMLInputElement).value.split(',')
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={emoji.ascii.join(',')}
|
||||
onInput={(e: Event) => onEditAscii(
|
||||
(e.target as HTMLInputElement).value.split(',')
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{
|
||||
editing ? (
|
||||
<button
|
||||
className="btn btn-success"
|
||||
type="button"
|
||||
onClick={() => onSave(null)}
|
||||
disabled={!canSave}
|
||||
>
|
||||
<i className="fa fa-check"></i>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
type="button"
|
||||
onClick={() => onDelete(null)}
|
||||
>
|
||||
<i className="fa fa-trash"></i>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
interface EmojiListProps {
|
||||
onEdit: Callback<[string, CustomEmoji]>;
|
||||
onDelete: Callback<string>;
|
||||
emojis: CustomEmoji[];
|
||||
}
|
||||
interface EmojiListState {
|
||||
/** The previous state before a save or deletion */
|
||||
previous: CustomEmoji[];
|
||||
emojis: CustomEmoji[];
|
||||
messages: JSX.Element[];
|
||||
newEmoji: CustomEmoji;
|
||||
newEmojiMessage: JSX.Element;
|
||||
}
|
||||
|
||||
const blankEmoji: CustomEmoji = {
|
||||
name: '',
|
||||
image: '',
|
||||
aliases: [],
|
||||
ascii: [],
|
||||
};
|
||||
class EmojiList extends Component<EmojiListProps, EmojiListState> {
|
||||
private static equal(a: CustomEmoji, b: CustomEmoji) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (a.name === b.name) &&
|
||||
(a.image === b.image) &&
|
||||
setsEqual(a.aliases, b.aliases) &&
|
||||
setsEqual(a.ascii, b.ascii);
|
||||
}
|
||||
|
||||
private static validate(all: CustomEmoji[], emoji: CustomEmoji) {
|
||||
const pattern = /^[a-z\-.+0-9_]*$/i;
|
||||
|
||||
const validations: {
|
||||
fn: () => boolean;
|
||||
message: JSX.Element;
|
||||
}[] = [
|
||||
{
|
||||
fn: () => !!emoji.name,
|
||||
message: (
|
||||
<span><strong>Name</strong> is required</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
fn: () => !!emoji.image,
|
||||
message: (
|
||||
<span><strong>Image</strong> is required</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
fn: () => pattern.test(emoji.name),
|
||||
message: (
|
||||
<span><strong>Name</strong> can only contain letters,
|
||||
numbers, and <code>_-+.</code></span>
|
||||
),
|
||||
},
|
||||
{
|
||||
fn: () => emoji.aliases.every(alias => pattern.test(alias)),
|
||||
message: (
|
||||
<span><strong>Aliases</strong> can only contain letters,
|
||||
numbers, and <code>_-+.</code> (comma-separated)</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
fn: () => all.every(({ name }) => emoji.name !== name),
|
||||
message: (
|
||||
<span>Multiple custom emojis cannot have the same <strong>Name</strong></span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return validations.filter(validation => !validation.fn());
|
||||
}
|
||||
|
||||
public constructor({ emojis }: EmojiListProps) {
|
||||
super();
|
||||
|
||||
this.setState({
|
||||
previous: emojis.slice(),
|
||||
emojis: emojis.slice(),
|
||||
messages: [],
|
||||
newEmoji: blankEmoji,
|
||||
newEmojiMessage: null,
|
||||
});
|
||||
}
|
||||
|
||||
private onAdd() {
|
||||
const emojis = this.state.emojis.slice();
|
||||
const previous = this.state.previous.slice();
|
||||
const emoji = this.state.newEmoji;
|
||||
|
||||
emoji.aliases = emoji.aliases.filter(Boolean);
|
||||
emoji.ascii = emoji.ascii.filter(Boolean);
|
||||
|
||||
emojis.push(emoji);
|
||||
previous.push(emoji);
|
||||
|
||||
this.setState({
|
||||
previous,
|
||||
emojis,
|
||||
newEmoji: blankEmoji,
|
||||
});
|
||||
|
||||
this.props.onEdit([emoji.name, emoji]);
|
||||
}
|
||||
|
||||
private onSave(i: number) {
|
||||
const emojis = this.state.emojis.slice();
|
||||
const previous = this.state.previous.slice();
|
||||
const emoji = this.state.emojis[i];
|
||||
|
||||
emoji.aliases = emoji.aliases.filter(Boolean);
|
||||
emoji.ascii = emoji.ascii.filter(Boolean);
|
||||
|
||||
const [old] = previous.splice(i, 1, emoji);
|
||||
emojis.splice(i, 1, emoji);
|
||||
|
||||
this.setState({
|
||||
previous,
|
||||
emojis,
|
||||
});
|
||||
|
||||
this.props.onEdit([old.name, emoji]);
|
||||
}
|
||||
|
||||
private onDelete(i: number) {
|
||||
const confirm = () => this.onConfirmDelete(i);
|
||||
const nope = () => {
|
||||
const messages = this.state.messages.slice();
|
||||
messages[i] = null;
|
||||
|
||||
this.setState({
|
||||
messages,
|
||||
});
|
||||
};
|
||||
|
||||
const messages = this.state.messages.slice();
|
||||
|
||||
messages[i] = (
|
||||
<tr>
|
||||
<td>
|
||||
<button className="btn btn-default" type="button" onClick={nope}>Cancel</button>
|
||||
</td>
|
||||
<td colSpan={3}>
|
||||
<span class="help-block">Are you sure you want to delete this emoji?</span>
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn btn-danger" type="button" onClick={confirm}>Yes</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
this.setState({
|
||||
messages,
|
||||
});
|
||||
}
|
||||
|
||||
private onConfirmDelete(i: number) {
|
||||
const emojis = this.state.emojis.slice();
|
||||
const previous = this.state.previous.slice();
|
||||
const [old] = previous.splice(i, 1);
|
||||
emojis.splice(i, 1);
|
||||
|
||||
this.setState({
|
||||
emojis,
|
||||
previous,
|
||||
});
|
||||
|
||||
this.props.onDelete(old.name);
|
||||
}
|
||||
|
||||
private onEdit(i: number, emoji: CustomEmoji) {
|
||||
const emojis = this.state.emojis.slice();
|
||||
emojis.splice(i, 1, emoji);
|
||||
this.setState({
|
||||
emojis,
|
||||
});
|
||||
}
|
||||
|
||||
public render(_: EmojiListProps, {
|
||||
previous,
|
||||
emojis,
|
||||
messages,
|
||||
newEmoji,
|
||||
newEmojiMessage,
|
||||
}: EmojiListState) {
|
||||
const rows: JSX.Element[] = [];
|
||||
emojis.forEach((emoji, i) => {
|
||||
const all = previous.slice();
|
||||
all.splice(i, 1);
|
||||
|
||||
const failures = EmojiList.validate(all, emoji);
|
||||
|
||||
const props: EmojiProps = {
|
||||
emoji,
|
||||
onSave: () => this.onSave(i),
|
||||
onDelete: () => this.onDelete(i),
|
||||
onEditName: name => this.onEdit(i, { ...emoji, name }),
|
||||
onEditImage: image => this.onEdit(i, { ...emoji, image }),
|
||||
onEditAliases: aliases => this.onEdit(i, { ...emoji, aliases }),
|
||||
onEditAscii: ascii => this.onEdit(i, { ...emoji, ascii }),
|
||||
editing: !EmojiList.equal(emoji, previous[i]),
|
||||
canSave: !failures.length,
|
||||
};
|
||||
rows.push(<Emoji {...props} key={i} />);
|
||||
rows.push(...failures.map(({ message }) => (
|
||||
<tr className="text-danger">
|
||||
<td colSpan={5}>{message}</td>
|
||||
</tr>
|
||||
)));
|
||||
if (messages[i]) {
|
||||
rows.push(messages[i]);
|
||||
}
|
||||
});
|
||||
|
||||
const newEmojiFailures = EmojiList.validate(previous, newEmoji);
|
||||
|
||||
return (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Image</th><th>Aliases</th><th>ASCII patterns</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<Emoji
|
||||
emoji={newEmoji}
|
||||
onSave={() => this.onAdd()}
|
||||
onDelete={() => {}}
|
||||
onEditName={name => this.setState({ newEmoji: { ...newEmoji, name } })}
|
||||
onEditImage={image => this.setState({ newEmoji: { ...newEmoji, image } })}
|
||||
onEditAliases={aliases => this.setState({ newEmoji: { ...newEmoji, aliases } })}
|
||||
onEditAscii={ascii => this.setState({ newEmoji: { ...newEmoji, ascii } })}
|
||||
editing
|
||||
canSave={!newEmojiFailures.length}
|
||||
/>
|
||||
{EmojiList.equal(newEmoji, blankEmoji) ? null : newEmojiFailures.map(({ message }) => (
|
||||
<tr className="text-danger">
|
||||
<td colSpan={5}>{message}</td>
|
||||
</tr>
|
||||
))}
|
||||
{newEmojiMessage}
|
||||
</tfoot>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface AdjunctProps {
|
||||
editing?: boolean;
|
||||
canSave?: boolean;
|
||||
adjunct: CustomAdjunct;
|
||||
onSave: Callback;
|
||||
onDelete: Callback;
|
||||
onEditName: Callback<string>;
|
||||
onEditAliases: Callback<string[]>;
|
||||
onEditAscii: Callback<string[]>;
|
||||
}
|
||||
class Adjunct extends Component<AdjunctProps, {}> {
|
||||
private nameInput: Element;
|
||||
|
||||
public render({
|
||||
editing,
|
||||
canSave,
|
||||
adjunct,
|
||||
onSave,
|
||||
onDelete,
|
||||
onEditName,
|
||||
onEditAliases,
|
||||
onEditAscii,
|
||||
}: AdjunctProps) {
|
||||
const emoji = adjunct.name && table[adjunct.name];
|
||||
return (
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={adjunct.name}
|
||||
onInput={(e: Event) => onEditName((e.target as HTMLInputElement).value)}
|
||||
ref={(input) => { this.nameInput = input; }}
|
||||
/>
|
||||
</td>
|
||||
<td dangerouslySetInnerHTML={{ __html: emoji ? buildEmoji(emoji) : '' }}></td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={adjunct.aliases.join(',')}
|
||||
onInput={(e: Event) => onEditAliases(
|
||||
(e.target as HTMLInputElement).value.split(',')
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={adjunct.ascii.join(',')}
|
||||
onInput={(e: Event) => onEditAscii(
|
||||
(e.target as HTMLInputElement).value.split(',')
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{
|
||||
editing ? (
|
||||
<button
|
||||
className="btn btn-success"
|
||||
type="button"
|
||||
onClick={() => onSave(null)}
|
||||
disabled={!canSave}
|
||||
>
|
||||
<i className="fa fa-check"></i>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
type="button"
|
||||
onClick={() => onDelete(null)}
|
||||
>
|
||||
<i className="fa fa-trash"></i>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const { Textarea } = Textcomplete.editors;
|
||||
|
||||
const editor = new Textarea(this.nameInput);
|
||||
const completer = new Textcomplete(editor, {
|
||||
dropdown: {
|
||||
style: { zIndex: 20000 },
|
||||
},
|
||||
});
|
||||
|
||||
completer.register([{
|
||||
...strategy,
|
||||
replace: (emoji: StoredEmoji) => emoji.name,
|
||||
match: /^(.+)$/,
|
||||
}]);
|
||||
|
||||
completer.on('selected', () => {
|
||||
this.props.onEditName((this.nameInput as HTMLInputElement).value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface AdjunctListProps {
|
||||
onEdit: Callback<[string, CustomAdjunct]>;
|
||||
onDelete: Callback<string>;
|
||||
adjuncts: CustomAdjunct[];
|
||||
}
|
||||
interface AdjunctListState {
|
||||
/** the previous state before a save or deletion */
|
||||
previous: CustomAdjunct[];
|
||||
adjuncts: CustomAdjunct[];
|
||||
messages: JSX.Element[];
|
||||
newAdjunct: CustomAdjunct;
|
||||
newAdjunctMessage: JSX.Element;
|
||||
}
|
||||
|
||||
const blankAdjunct: CustomAdjunct = {
|
||||
name: '',
|
||||
aliases: [],
|
||||
ascii: [],
|
||||
};
|
||||
class AdjunctList extends Component<AdjunctListProps, AdjunctListState> {
|
||||
private static equal(a: CustomAdjunct, b: CustomAdjunct) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (a.name === b.name) &&
|
||||
setsEqual(a.aliases, b.aliases) &&
|
||||
setsEqual(a.ascii, b.ascii);
|
||||
}
|
||||
|
||||
private static validate(all: CustomAdjunct[], emoji: CustomAdjunct) {
|
||||
const pattern = /^[a-z\-.+0-9_]*$/i;
|
||||
|
||||
const validations: {
|
||||
fn: () => boolean;
|
||||
message: string;
|
||||
}[] = [
|
||||
{
|
||||
fn: () => !!emoji.name,
|
||||
message: '<strong>Name</strong> is required',
|
||||
},
|
||||
{
|
||||
fn: () => !!table[emoji.name],
|
||||
message: '<strong>Name</strong> must be an existing emoji',
|
||||
},
|
||||
{
|
||||
fn: () => emoji.aliases.every(alias => pattern.test(alias)),
|
||||
message: '<strong>Aliases</strong> can only contain ' +
|
||||
'letters, numbers, and <code>_-+.</code> (comma-separated)',
|
||||
},
|
||||
{
|
||||
fn: () => all.every(({ name }) => emoji.name !== name),
|
||||
message: 'Multiple custom extensions cannot have the same <strong>Name</strong>',
|
||||
},
|
||||
];
|
||||
|
||||
return validations.filter(validation => !validation.fn());
|
||||
}
|
||||
|
||||
public constructor({ adjuncts }: AdjunctListProps) {
|
||||
super();
|
||||
|
||||
this.setState({
|
||||
previous: adjuncts.slice(),
|
||||
adjuncts: adjuncts.slice(),
|
||||
messages: [],
|
||||
newAdjunct: blankAdjunct,
|
||||
newAdjunctMessage: null,
|
||||
});
|
||||
}
|
||||
|
||||
private onAdd() {
|
||||
const adjuncts = this.state.adjuncts.slice();
|
||||
const previous = this.state.previous.slice();
|
||||
const adjunct = this.state.newAdjunct;
|
||||
|
||||
adjunct.aliases = adjunct.aliases.filter(Boolean);
|
||||
adjunct.ascii = adjunct.ascii.filter(Boolean);
|
||||
|
||||
adjuncts.push(adjunct);
|
||||
previous.push(adjunct);
|
||||
|
||||
this.setState({
|
||||
previous,
|
||||
adjuncts,
|
||||
newAdjunct: blankAdjunct,
|
||||
});
|
||||
|
||||
this.props.onEdit([adjunct.name, adjunct]);
|
||||
}
|
||||
|
||||
private onSave(i: number) {
|
||||
const adjuncts = this.state.adjuncts.slice();
|
||||
const previous = this.state.previous.slice();
|
||||
const adjunct = this.state.adjuncts[i];
|
||||
|
||||
adjunct.aliases = adjunct.aliases.filter(Boolean);
|
||||
adjunct.ascii = adjunct.ascii.filter(Boolean);
|
||||
|
||||
const [old] = previous.splice(i, 1, adjunct);
|
||||
adjuncts.splice(i, 1, adjunct);
|
||||
|
||||
this.setState({
|
||||
previous,
|
||||
adjuncts,
|
||||
});
|
||||
|
||||
this.props.onEdit([old.name, adjunct]);
|
||||
}
|
||||
|
||||
private onDelete(i: number) {
|
||||
const confirm = () => this.onConfirmDelete(i);
|
||||
const nope = () => {
|
||||
const messages = this.state.messages.slice();
|
||||
messages[i] = null;
|
||||
|
||||
this.setState({
|
||||
messages,
|
||||
});
|
||||
};
|
||||
|
||||
const messages = this.state.messages.slice();
|
||||
|
||||
messages[i] = (
|
||||
<tr>
|
||||
<td>
|
||||
<button className="btn btn-default" type="button" onClick={nope}>Cancel</button>
|
||||
</td>
|
||||
<td colSpan={3}>
|
||||
<span class="help-block">Are you sure you want to delete this extension?</span>
|
||||
</td>
|
||||
<td>
|
||||
<button className="btn btn-danger" type="button" onClick={confirm}>Yes</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
this.setState({
|
||||
messages,
|
||||
});
|
||||
}
|
||||
|
||||
private onConfirmDelete(i: number) {
|
||||
const adjuncts = this.state.adjuncts.slice();
|
||||
const previous = this.state.previous.slice();
|
||||
const [old] = previous.splice(i, 1);
|
||||
adjuncts.splice(i, 1);
|
||||
|
||||
this.setState({
|
||||
adjuncts,
|
||||
previous,
|
||||
});
|
||||
|
||||
this.props.onDelete(old.name);
|
||||
}
|
||||
|
||||
private onEdit(i: number, emoji: CustomAdjunct) {
|
||||
const adjuncts = this.state.adjuncts.slice();
|
||||
adjuncts.splice(i, 1, emoji);
|
||||
this.setState({
|
||||
adjuncts,
|
||||
});
|
||||
}
|
||||
|
||||
public render(_: AdjunctListProps, {
|
||||
previous,
|
||||
adjuncts,
|
||||
messages,
|
||||
newAdjunct,
|
||||
newAdjunctMessage,
|
||||
}: AdjunctListState) {
|
||||
const rows: JSX.Element[] = [];
|
||||
adjuncts.forEach((adjunct, i) => {
|
||||
const all = previous.slice();
|
||||
all.splice(i, 1);
|
||||
|
||||
const failures = AdjunctList.validate(all, adjunct);
|
||||
|
||||
const props: AdjunctProps = {
|
||||
adjunct,
|
||||
onSave: () => this.onSave(i),
|
||||
onDelete: () => this.onDelete(i),
|
||||
onEditName: name => this.onEdit(i, { ...adjunct, name }),
|
||||
onEditAliases: aliases => this.onEdit(i, { ...adjunct, aliases }),
|
||||
onEditAscii: ascii => this.onEdit(i, { ...adjunct, ascii }),
|
||||
editing: !AdjunctList.equal(adjunct, previous[i]),
|
||||
canSave: !failures.length,
|
||||
};
|
||||
rows.push(<Adjunct {...props} key={i} />);
|
||||
rows.push(...failures.map(({ message }) => (
|
||||
<tr className="text-danger">
|
||||
<td colSpan={5} dangerouslySetInnerHTML={{ __html: message }}></td>
|
||||
</tr>
|
||||
)));
|
||||
if (messages[i]) {
|
||||
rows.push(messages[i]);
|
||||
}
|
||||
});
|
||||
|
||||
const newAdjunctFailures = AdjunctList.validate(previous, newAdjunct);
|
||||
|
||||
return (
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Emoji</th><th>Aliases</th><th>ASCII patterns</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<Adjunct
|
||||
adjunct={newAdjunct}
|
||||
onSave={() => this.onAdd()}
|
||||
onDelete={() => {}}
|
||||
onEditName={name => this.setState({ newAdjunct: { ...newAdjunct, name } })}
|
||||
onEditAliases={aliases => this.setState({ newAdjunct: { ...newAdjunct, aliases } })}
|
||||
onEditAscii={ascii => this.setState({ newAdjunct: { ...newAdjunct, ascii } })}
|
||||
editing
|
||||
canSave={!newAdjunctFailures.length}
|
||||
/>
|
||||
{AdjunctList.equal(newAdjunct, blankAdjunct) ? null :
|
||||
newAdjunctFailures.map(({ message }) => (
|
||||
<tr className="text-danger">
|
||||
<td colSpan={5} dangerouslySetInnerHTML={{ __html: message }}></td>
|
||||
</tr>
|
||||
))}
|
||||
{newAdjunctMessage}
|
||||
</tfoot>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface AppProps {
|
||||
/** initial state */
|
||||
state: AppState;
|
||||
|
||||
onEditEmoji: Callback<[string, CustomEmoji]>;
|
||||
onDeleteEmoji: Callback<string>;
|
||||
onEditAdjunct: Callback<[string, CustomAdjunct]>;
|
||||
onDeleteAdjunct: Callback<string>;
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
emojis: CustomEmoji[];
|
||||
adjuncts: CustomAdjunct[];
|
||||
}
|
||||
|
||||
class App extends Component<AppProps, AppState> {
|
||||
public constructor({ state }: AppProps) {
|
||||
super();
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
public render({
|
||||
onEditEmoji,
|
||||
onDeleteEmoji,
|
||||
onEditAdjunct,
|
||||
onDeleteAdjunct,
|
||||
}: AppProps, {
|
||||
emojis,
|
||||
adjuncts,
|
||||
}: AppState) {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Below you can add custom emoji, and also add new aliases
|
||||
and ASCII patterns for existing emoji. While this list is
|
||||
edited live, you must still <strong>Build Emoji Assets </strong>
|
||||
to actually use these customizations.
|
||||
</p>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
<h3 className="panel-title">Custom Emoji</h3>
|
||||
</div>
|
||||
<EmojiList
|
||||
emojis={emojis}
|
||||
onEdit={onEditEmoji}
|
||||
onDelete={onDeleteEmoji}
|
||||
/>
|
||||
</div>
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">
|
||||
<h3 className="panel-title">Custom Extensions</h3>
|
||||
</div>
|
||||
<AdjunctList
|
||||
adjuncts={adjuncts}
|
||||
onEdit={onEditAdjunct}
|
||||
onDelete={onDeleteAdjunct}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
export function init(
|
||||
elem: Element,
|
||||
cb: Callback
|
||||
) {
|
||||
if (initialized) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
socket.emit('admin.plugins.emoji.getCustomizations', (err: Error, customizations: AppState) => {
|
||||
const props: AppProps = {
|
||||
state: customizations,
|
||||
onEditEmoji: (args) => {
|
||||
socket.emit('admin.plugins.emoji.editEmoji', args);
|
||||
},
|
||||
onDeleteEmoji: (name) => {
|
||||
socket.emit('admin.plugins.emoji.deleteEmoji', name);
|
||||
},
|
||||
onEditAdjunct: (args) => {
|
||||
socket.emit('admin.plugins.emoji.editAdjunct', args);
|
||||
},
|
||||
onDeleteAdjunct: (name) => {
|
||||
socket.emit('admin.plugins.emoji.deleteAdjunct', name);
|
||||
},
|
||||
};
|
||||
initEmoji(() => {
|
||||
render((
|
||||
<App {...props} />
|
||||
), elem);
|
||||
|
||||
cb(null);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -17,14 +17,14 @@ import {
|
|||
const $html = $('html');
|
||||
|
||||
export const dialogActions = {
|
||||
open(dialog: JQuery) {
|
||||
open(dialog: JQuery): JQuery {
|
||||
$html.addClass('emoji-insert');
|
||||
dialog.addClass('open');
|
||||
dialog.find('.emoji-dialog-search').focus();
|
||||
|
||||
return dialog;
|
||||
},
|
||||
close(dialog: JQuery) {
|
||||
close(dialog: JQuery): JQuery {
|
||||
$html.removeClass('emoji-insert');
|
||||
return dialog.removeClass('open');
|
||||
},
|
||||
|
@ -59,7 +59,7 @@ function stringCompare(a: string, b: string) {
|
|||
}
|
||||
|
||||
// create modal
|
||||
export function init(callback: Callback<JQuery>) {
|
||||
export function init(callback: Callback<JQuery>): void {
|
||||
Promise.all([
|
||||
$.getJSON(`${base}/emoji/categories.json?${buster}`),
|
||||
$.getJSON(`${base}/emoji/packs.json?${buster}`),
|
||||
|
@ -184,7 +184,7 @@ export function init(callback: Callback<JQuery>) {
|
|||
export function toggle(
|
||||
opener: HTMLElement | null,
|
||||
onClick: (e: JQuery.Event, name: string, dialog: JQuery) => void
|
||||
) {
|
||||
): void {
|
||||
function after(dialog: JQuery) {
|
||||
if (dialog.hasClass('open')) {
|
||||
dialogActions.close(dialog);
|
||||
|
@ -238,7 +238,7 @@ export function toggleForInsert(
|
|||
selectStart: number,
|
||||
selectEnd: number,
|
||||
event: JQuery.ClickEvent
|
||||
) {
|
||||
): void {
|
||||
// handle new and old API case
|
||||
let button;
|
||||
if (event && event.target) {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
// eslint-disable-next-line spaced-comment
|
||||
/// <amd-module name="emoji"/>
|
||||
|
||||
const base = `${config.relative_path}/plugins/nodebb-plugin-emoji`;
|
||||
const buster = config['cache-buster'];
|
||||
|
||||
export { base, buster };
|
||||
export function buildEmoji(emoji: StoredEmoji, defer?: boolean) {
|
||||
export function buildEmoji(emoji: StoredEmoji, defer?: boolean): string {
|
||||
const whole = `:${emoji.name}:`;
|
||||
const deferClass = defer ? ' defer' : '';
|
||||
|
||||
|
@ -30,25 +29,19 @@ export let search: (term: string) => StoredEmoji[];
|
|||
|
||||
export const strategy = {
|
||||
match: /\B:([^\s\n:]+)$/,
|
||||
search: (term: string, callback: Callback<StoredEmoji[]>) => {
|
||||
search: (term: string, callback: Callback<StoredEmoji[]>): void => {
|
||||
callback(search(term.toLowerCase().replace(/[_-]/g, '')).slice(0, 10));
|
||||
},
|
||||
index: 1,
|
||||
replace: (emoji: StoredEmoji) => `:${emoji.name}: `,
|
||||
template: (emoji: StoredEmoji) => `${buildEmoji(emoji)} ${emoji.name}`,
|
||||
replace: (emoji: StoredEmoji): string => `:${emoji.name}: `,
|
||||
template: (emoji: StoredEmoji): string => `${buildEmoji(emoji)} ${emoji.name}`,
|
||||
cache: true,
|
||||
};
|
||||
|
||||
let initialized = false;
|
||||
let initialized: Promise<void>;
|
||||
|
||||
export function init(callback?: Callback<undefined>) {
|
||||
if (initialized) {
|
||||
if (callback) { setTimeout(callback, 0); }
|
||||
return;
|
||||
}
|
||||
initialized = true;
|
||||
|
||||
Promise.all([
|
||||
export function init(callback?: Callback<void>): Promise<void> {
|
||||
initialized = initialized || Promise.all([
|
||||
import('fuzzysearch'),
|
||||
import('leven'),
|
||||
import('composer/formatting'),
|
||||
|
@ -126,12 +119,16 @@ export function init(callback?: Callback<undefined>) {
|
|||
.then(({ toggleForInsert }) => toggleForInsert(textarea, start, end, event));
|
||||
}
|
||||
);
|
||||
|
||||
if (callback) { setTimeout(callback, 0); }
|
||||
}).catch((err) => {
|
||||
const e = Error('[[emoji:meta-load-failed]]');
|
||||
console.error(e);
|
||||
app.alertError(e);
|
||||
throw err;
|
||||
});
|
||||
|
||||
if (callback) {
|
||||
initialized.then(() => setTimeout(callback, 0));
|
||||
}
|
||||
|
||||
return initialized;
|
||||
}
|
||||
|
|
9
public/lib/types.d.ts
vendored
9
public/lib/types.d.ts
vendored
|
@ -1,3 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare type Callback<T = void> = (result: T) => void;
|
||||
|
||||
declare const config: {
|
||||
|
@ -9,7 +11,10 @@ declare const config: {
|
|||
declare const app: {
|
||||
alertSuccess(message?: string): void;
|
||||
alertError(message?: string): void;
|
||||
alertError(error: Error): void;
|
||||
alertError(error?: Error): void;
|
||||
};
|
||||
declare const ajaxify: {
|
||||
data: any;
|
||||
};
|
||||
declare const utils: {
|
||||
generateUUID(): string;
|
||||
|
@ -20,8 +25,6 @@ interface String {
|
|||
startsWith(str: string): boolean;
|
||||
}
|
||||
|
||||
declare const socket: SocketIO.Server;
|
||||
|
||||
interface JQuery {
|
||||
ajaxSubmit: any;
|
||||
draggable: any;
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
<form id="emoji-settings">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<label for="emoji-parseAscii">
|
||||
<input id="emoji-parseAscii" type="checkbox" {{{ if settings.parseAscii }}} checked {{{ end }}} />
|
||||
[[admin/plugins/emoji:settings.parseAscii]]
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emoji-parseNative">
|
||||
<input id="emoji-parseNative" type="checkbox" {{{ if settings.parseNative }}} checked {{{ end }}} />
|
||||
[[admin/plugins/emoji:settings.parseNative]]
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="emoji-customFirst">
|
||||
<input id="emoji-customFirst" type="checkbox" {{{ if settings.customFirst }}} checked {{{ end }}} />
|
||||
[[admin/plugins/emoji:settings.customFirst]]
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-footer">
|
||||
<div class="form-group">
|
||||
<button type="button" id="build" class="btn btn-primary" aria-describedby="emoji-build_description">[[admin/plugins/emoji:build]]</button>
|
||||
<p id="emoji-build_description" class="help-block">
|
||||
[[admin/plugins/emoji:build_description]]
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
|
||||
<button id="edit" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored" style="
|
||||
left: 30px;
|
||||
margin-left: 0;
|
||||
background: rgb(255,64,129) !important;
|
||||
">
|
||||
<i class="material-icons">edit</i>
|
||||
</button>
|
||||
|
||||
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title" id="editModalLabel">Customize Emoji</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="editModalBody">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,12 +1,8 @@
|
|||
{
|
||||
"include": [
|
||||
"lib/**/*.ts",
|
||||
"lib/**/*.tsx",
|
||||
"../lib/types.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"lib/admin/extension.tsx"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "../build/public/lib",
|
||||
"lib": ["es5", "dom", "es2015.promise", "es2015.iterable"],
|
||||
|
@ -16,14 +12,9 @@
|
|||
"sourceMap": true,
|
||||
"types": [
|
||||
"jquery",
|
||||
"bootstrap",
|
||||
"preact",
|
||||
"socket.io"
|
||||
"bootstrap"
|
||||
],
|
||||
|
||||
"jsx": "react",
|
||||
"jsxFactory": "h",
|
||||
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
@ -34,4 +25,4 @@
|
|||
"typeAcquisition": {
|
||||
"enable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,14 @@
|
|||
],
|
||||
"compilerOptions": {
|
||||
"outDir": "build/lib",
|
||||
"lib": ["es2017"],
|
||||
"target": "es2017",
|
||||
"lib": ["es2019"],
|
||||
"target": "es2019",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"alwaysStrict": true,
|
||||
"sourceMap": true,
|
||||
"types": [
|
||||
"socket.io"
|
||||
]
|
||||
"sourceMap": true
|
||||
},
|
||||
"typeAcquisition": {
|
||||
"enable": false
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue