Feature: emoji editor (#2411)
* Custom emoji editor: implement backend This reuses the logo upload code * Implement emoji edit admin interface Again reuse base64 logic from the logo upload * Allow toggling between uploaded and default emojis * Add route that always serves uploaded emojis This is needed for the admin emoji interface, as otherwise the emojis will 404 if custom emojis are disabled * Fix linter warnings * Remove custom/uploaded emoji logic * Reset timer after emoji deletion * Setup: copy built-in emojis to emoji directory
This commit is contained in:
@@ -143,6 +143,10 @@ export const MainLayout: FC<MainLayoutProps> = ({ children }) => {
|
||||
label: <Link href="/admin/chat/users">Users</Link>,
|
||||
key: 'chat-users',
|
||||
},
|
||||
{
|
||||
label: <Link href="/admin/chat/emojis">Emojis</Link>,
|
||||
key: 'emojis',
|
||||
},
|
||||
];
|
||||
|
||||
const utilitiesMenu = [
|
||||
|
||||
@@ -18,13 +18,7 @@ import {
|
||||
} from '../../utils/input-statuses';
|
||||
import { NEXT_PUBLIC_API_HOST } from '../../utils/apis';
|
||||
|
||||
const ACCEPTED_FILE_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||
|
||||
function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayBuffer) => void) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result));
|
||||
reader.readAsDataURL(img);
|
||||
}
|
||||
import { ACCEPTED_IMAGE_TYPES, getBase64 } from '../../utils/images';
|
||||
|
||||
export const EditLogo: FC = () => {
|
||||
const [logoUrl, setlogoUrl] = useState(null);
|
||||
@@ -53,7 +47,7 @@ export const EditLogo: FC = () => {
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return new Promise<void>((res, rej) => {
|
||||
if (!ACCEPTED_FILE_TYPES.includes(file.type)) {
|
||||
if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
|
||||
const msg = `File type is not supported: ${file.type}`;
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, `There was an error: ${msg}`));
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
@@ -108,7 +102,7 @@ export const EditLogo: FC = () => {
|
||||
listType="picture"
|
||||
className="avatar-uploader"
|
||||
showUploadList={false}
|
||||
accept={ACCEPTED_FILE_TYPES.join(',')}
|
||||
accept={ACCEPTED_IMAGE_TYPES.join(',')}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={handleLogoUpdate}
|
||||
disabled={loading}
|
||||
|
||||
183
web/pages/admin/chat/emojis.tsx
Normal file
183
web/pages/admin/chat/emojis.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Button, Space, Table, Typography, Upload } from 'antd';
|
||||
import { RcFile } from 'antd/lib/upload';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import FormStatusIndicator from '../../../components/config/FormStatusIndicator';
|
||||
|
||||
import { DELETE_EMOJI, fetchData, UPLOAD_EMOJI } from '../../../utils/apis';
|
||||
|
||||
import { ACCEPTED_IMAGE_TYPES, getBase64 } from '../../../utils/images';
|
||||
import {
|
||||
createInputStatus,
|
||||
STATUS_ERROR,
|
||||
STATUS_PROCESSING,
|
||||
STATUS_SUCCESS,
|
||||
} from '../../../utils/input-statuses';
|
||||
import { RESET_TIMEOUT } from '../../../utils/config-constants';
|
||||
import { URL_CUSTOM_EMOJIS } from '../../../utils/constants';
|
||||
|
||||
type CustomEmoji = {
|
||||
name: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const Emoji = () => {
|
||||
const [emojis, setEmojis] = useState<CustomEmoji[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState(null);
|
||||
const [uploadFile, setUploadFile] = useState<RcFile>(null);
|
||||
|
||||
let resetTimer = null;
|
||||
const resetStates = () => {
|
||||
setSubmitStatus(null);
|
||||
clearTimeout(resetTimer);
|
||||
resetTimer = null;
|
||||
};
|
||||
|
||||
async function getEmojis() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetchData(URL_CUSTOM_EMOJIS);
|
||||
setEmojis(response);
|
||||
} catch (error) {
|
||||
console.error('error fetching emojis', error);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
useEffect(() => {
|
||||
getEmojis();
|
||||
}, []);
|
||||
|
||||
async function handleDelete(name: string) {
|
||||
setLoading(true);
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Deleting emoji...'));
|
||||
|
||||
try {
|
||||
const response = await fetchData(DELETE_EMOJI, {
|
||||
method: 'POST',
|
||||
data: { name },
|
||||
});
|
||||
|
||||
if (response instanceof Error) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Emoji deleted'));
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
} catch (error) {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
|
||||
setLoading(false);
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
}
|
||||
|
||||
getEmojis();
|
||||
}
|
||||
|
||||
async function handleEmojiUpload() {
|
||||
setLoading(true);
|
||||
try {
|
||||
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Converting emoji...'));
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
const emojiData = await new Promise<CustomEmoji>((res, rej) => {
|
||||
if (!ACCEPTED_IMAGE_TYPES.includes(uploadFile.type)) {
|
||||
const msg = `File type is not supported: ${uploadFile.type}`;
|
||||
// eslint-disable-next-line no-promise-executor-return
|
||||
return rej(msg);
|
||||
}
|
||||
|
||||
getBase64(uploadFile, (url: string) =>
|
||||
res({
|
||||
name: uploadFile.name,
|
||||
url,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_PROCESSING, 'Uploading emoji...'));
|
||||
|
||||
const response = await fetchData(UPLOAD_EMOJI, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
name: emojiData.name,
|
||||
data: emojiData.url,
|
||||
},
|
||||
});
|
||||
|
||||
if (response instanceof Error) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
setSubmitStatus(createInputStatus(STATUS_SUCCESS, 'Emoji uploaded successfully!'));
|
||||
getEmojis();
|
||||
} catch (error) {
|
||||
setSubmitStatus(createInputStatus(STATUS_ERROR, `${error}`));
|
||||
}
|
||||
|
||||
resetTimer = setTimeout(resetStates, RESET_TIMEOUT);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '',
|
||||
key: 'delete',
|
||||
render: (text, record) => (
|
||||
<Space size="middle">
|
||||
<Button onClick={() => handleDelete(record.name)} icon={<DeleteOutlined />} />
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Emoji',
|
||||
key: 'url',
|
||||
render: (text, record) => (
|
||||
<img src={record.url} alt={record.name} style={{ maxWidth: '2vw' }} />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>Emojis</Title>
|
||||
<Paragraph>
|
||||
Here you can upload new custom emojis for usage in the chat. When uploading a new emoji, the
|
||||
filename will be used as emoji name.
|
||||
</Paragraph>
|
||||
|
||||
<Table
|
||||
rowKey={record => record.url}
|
||||
dataSource={emojis}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
/>
|
||||
<br />
|
||||
<Upload
|
||||
name="emoji"
|
||||
listType="picture"
|
||||
className="emoji-uploader"
|
||||
showUploadList={false}
|
||||
accept={ACCEPTED_IMAGE_TYPES.join(',')}
|
||||
beforeUpload={setUploadFile}
|
||||
customRequest={handleEmojiUpload}
|
||||
disabled={loading}
|
||||
>
|
||||
<Button type="primary" disabled={loading}>
|
||||
Upload new emoji
|
||||
</Button>
|
||||
</Upload>
|
||||
<FormStatusIndicator status={submitStatus} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Emoji;
|
||||
@@ -64,6 +64,12 @@ export const CHAT_HISTORY = `${API_LOCATION}chat/messages`;
|
||||
// Get chat history
|
||||
export const UPDATE_CHAT_MESSGAE_VIZ = `/api/admin/chat/messagevisibility`;
|
||||
|
||||
// Upload a new custom emoji
|
||||
export const UPLOAD_EMOJI = `${API_LOCATION}emoji/upload`;
|
||||
|
||||
// Delete a custom emoji
|
||||
export const DELETE_EMOJI = `${API_LOCATION}emoji/delete`;
|
||||
|
||||
// Get all access tokens
|
||||
export const ACCESS_TOKENS = `${API_LOCATION}accesstokens`;
|
||||
|
||||
|
||||
7
web/utils/images.ts
Normal file
7
web/utils/images.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/gif'];
|
||||
|
||||
export function getBase64(img: File | Blob, callback: (imageUrl: string | ArrayBuffer) => void) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result));
|
||||
reader.readAsDataURL(img);
|
||||
}
|
||||
Reference in New Issue
Block a user