merge: pull request #730 from revoltchat/feat/emojis

This commit is contained in:
Paul Makles 2022-07-09 17:56:49 +01:00 committed by GitHub
commit 2b65e98cd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 279 additions and 66 deletions

2
external/lang vendored

@ -1 +1 @@
Subproject commit d4bc47b729c7e69ce97216469692b39f4cd1640e Subproject commit 58408da6c4090dd3a7808a663eaa95b8b1da7603

View file

@ -73,7 +73,7 @@
"@hcaptcha/react-hcaptcha": "^0.3.6", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@insertish/vite-plugin-babel-macros": "^1.0.5", "@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0", "@preact/preset-vite": "^2.0.0",
"@revoltchat/ui": "1.0.70", "@revoltchat/ui": "1.0.72",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.38.0", "@styled-icons/boxicons-logos": "^10.38.0",
"@styled-icons/boxicons-regular": "^10.38.0", "@styled-icons/boxicons-regular": "^10.38.0",
@ -123,7 +123,7 @@
"preact-context-menu": "0.4.1", "preact-context-menu": "0.4.1",
"preact-i18n": "^2.4.0-preactx", "preact-i18n": "^2.4.0-preactx",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"prismjs": "^1.23.0", "prismjs": "^1.28.0",
"qrcode.react": "^3.0.2", "qrcode.react": "^3.0.2",
"react-beautiful-dnd": "^13.1.0", "react-beautiful-dnd": "^13.1.0",
"react-device-detect": "2.2.2", "react-device-detect": "2.2.2",

View file

@ -7,13 +7,7 @@ import { ulid } from "ulid";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
import { import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "preact/hooks";
import { IconButton, Picker } from "@revoltchat/ui"; import { IconButton, Picker } from "@revoltchat/ui";
@ -28,7 +22,7 @@ import {
SMOOTH_SCROLL_ON_RECEIVE, SMOOTH_SCROLL_ON_RECEIVE,
} from "../../../lib/renderer/Singleton"; } from "../../../lib/renderer/Singleton";
import { useApplicationState } from "../../../mobx/State"; import { state, useApplicationState } from "../../../mobx/State";
import { Reply } from "../../../mobx/stores/MessageQueue"; import { Reply } from "../../../mobx/stores/MessageQueue";
import { emojiDictionary } from "../../../assets/emojis"; import { emojiDictionary } from "../../../assets/emojis";
@ -40,8 +34,8 @@ import {
uploadFile, uploadFile,
} from "../../../controllers/client/jsx/legacy/FileUploads"; } from "../../../controllers/client/jsx/legacy/FileUploads";
import { modalController } from "../../../controllers/modals/ModalController"; import { modalController } from "../../../controllers/modals/ModalController";
import { RenderEmoji } from "../../markdown/plugins/emoji";
import AutoComplete, { useAutoComplete } from "../AutoComplete"; import AutoComplete, { useAutoComplete } from "../AutoComplete";
import Emoji from "../Emoji";
import { PermissionTooltip } from "../Tooltip"; import { PermissionTooltip } from "../Tooltip";
import FilePreview from "./bars/FilePreview"; import FilePreview from "./bars/FilePreview";
import ReplyBar from "./bars/ReplyBar"; import ReplyBar from "./bars/ReplyBar";
@ -149,6 +143,56 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$");
// Tests for code block delimiters (``` at start of line) // Tests for code block delimiters (``` at start of line)
const RE_CODE_DELIMITER = new RegExp("^```", "gm"); const RE_CODE_DELIMITER = new RegExp("^```", "gm");
const HackAlertThisFileWillBeReplaced = observer(({ channel }: Props) => {
const renderEmoji = useMemo(
() =>
memo(({ emoji }: { emoji: string }) => (
<a
onClick={() => {
const v = state.draft.get(channel._id);
state.draft.set(
channel._id,
`${v ? `${v} ` : ""}:${emoji}:`,
);
}}>
<RenderEmoji match={emoji} {...({} as any)} />
</a>
)),
[],
);
const emojis: Record<string, any> = {
default: Object.keys(emojiDictionary),
};
// ! FIXME: also expose typing from component
const categories: any[] = [];
for (const server of state.ordering.orderedServers) {
// ! FIXME: add a separate map on each server for emoji
const list = [...channel.client.emojis.values()]
.filter((emoji) => emoji.parent.id === server._id)
.map((x) => x._id);
if (list.length > 0) {
emojis[server._id] = list;
categories.push({
id: server._id,
name: server.name,
iconURL: server.generateIconURL({ max_side: 256 }),
});
}
}
return (
<Picker
emojis={emojis}
categories={categories}
renderEmoji={renderEmoji}
/>
);
});
// ! FIXME: add to app config and load from app config // ! FIXME: add to app config and load from app config
export const CAN_UPLOAD_AT_ONCE = 5; export const CAN_UPLOAD_AT_ONCE = 5;
@ -312,6 +356,7 @@ export default observer(({ channel }: Props) => {
async function sendFile(content: string) { async function sendFile(content: string) {
if (uploadState.type !== "attached") return; if (uploadState.type !== "attached") return;
const attachments: string[] = []; const attachments: string[] = [];
setMessage;
const cancel = Axios.CancelToken.source(); const cancel = Axios.CancelToken.source();
const files = uploadState.files; const files = uploadState.files;
@ -470,26 +515,6 @@ export default observer(({ channel }: Props) => {
: undefined, : undefined,
}); });
const renderEmoji = useMemo(
() =>
memo(({ emoji }: { emoji: string }) => (
<a
onClick={() => {
const v = state.draft.get(channel._id);
setMessage(`${v ? `${v} ` : ""}:${emoji}:`);
}}>
<Emoji
emoji={
emojiDictionary[
emoji as keyof typeof emojiDictionary
]
}
/>
</a>
)),
[],
);
return ( return (
<> <>
<AutoComplete {...autoCompleteProps} /> <AutoComplete {...autoCompleteProps} />
@ -533,10 +558,7 @@ export default observer(({ channel }: Props) => {
/> />
<FloatingLayer> <FloatingLayer>
{picker && ( {picker && (
<Picker <HackAlertThisFileWillBeReplaced channel={channel} />
emojis={Object.keys(emojiDictionary)}
renderEmoji={renderEmoji}
/>
)} )}
</FloatingLayer> </FloatingLayer>
<Base> <Base>
@ -659,11 +681,6 @@ export default observer(({ channel }: Props) => {
onFocus={onFocus} onFocus={onFocus}
onBlur={onBlur} onBlur={onBlur}
/> />
{/*<Action>
<IconButton>
<Box size={24} />
</IconButton>
</Action>*/}
{state.experiments.isEnabled("picker") && ( {state.experiments.isEnabled("picker") && (
<Action> <Action>
<IconButton onClick={() => setPicker(!picker)}> <IconButton onClick={() => setPicker(!picker)}>

View file

@ -16,6 +16,7 @@ enum Badges {
Paw = 128, Paw = 128,
EarlyAdopter = 256, EarlyAdopter = 256,
ReservedRelevantJokeBadge1 = 512, ReservedRelevantJokeBadge1 = 512,
ReservedRelevantJokeBadge2 = 1024,
} }
const BadgesBase = styled.div` const BadgesBase = styled.div`
@ -135,6 +136,13 @@ export default function UserBadges({ badges, uid }: Props) {
) : ( ) : (
<></> <></>
)} )}
{badges & Badges.ReservedRelevantJokeBadge2 ? (
<Tooltip content="It's Morbin Time">
<img src="/assets/badges/amorbus.svg" />
</Tooltip>
) : (
<></>
)}
{badges & Badges.Paw ? ( {badges & Badges.Paw ? (
<Tooltip content="🦊"> <Tooltip content="🦊">
<img src="/assets/badges/paw.svg" /> <img src="/assets/badges/paw.svg" />

View file

@ -180,7 +180,7 @@ const Container = styled.div<{ largeEmoji: boolean }>`
/** /**
* Regex for matching execessive blockquotes * Regex for matching execessive blockquotes
*/ */
const RE_QUOTE = /(^[>\s][>\s])[>\s]+([^]+$)/gm; const RE_QUOTE = /(^(?:>\s){3})[>\s]+(.*$)/gm;
/** /**
* Regex for matching open angled bracket * Regex for matching open angled bracket

View file

@ -15,15 +15,19 @@ const Emoji = styled.img`
`; `;
export function RenderEmoji({ match }: CustomComponentProps) { export function RenderEmoji({ match }: CustomComponentProps) {
const url =
match in emojiDictionary
? parseEmoji(emojiDictionary[match as keyof typeof emojiDictionary])
: clientController.getAvailableClient().emojis!.get(match)!
.imageURL;
return ( return (
<Emoji <Emoji
alt={match} alt={match}
loading="lazy" loading="lazy"
className="emoji" className="emoji"
draggable={false} draggable={false}
src={parseEmoji( src={url}
emojiDictionary[match as keyof typeof emojiDictionary],
)}
/> />
); );
} }

View file

@ -99,7 +99,7 @@ export default function MemberList({
)} )}
{entry.type !== "no_offline" && ( {entry.type !== "no_offline" && (
<> <>
{" - "} {" "}
{entry.users.length} {entry.users.length}
</> </>
)} )}

View file

@ -0,0 +1,61 @@
import { Server } from "revolt.js";
import { useState } from "preact/hooks";
import { Form } from "@revoltchat/ui";
import { FileUploader } from "../../../controllers/client/jsx/legacy/FileUploads";
interface Props {
server: Server;
}
export function EmojiUploader({ server }: Props) {
const [fileId, setFileId] = useState<string>();
return (
<>
<h3>Upload Emoji</h3>
<Form
schema={{
name: "text",
file: "custom",
}}
data={{
name: {
field: "Name",
palette: "secondary",
},
file: {
element: (
<FileUploader
style="icon"
width={100}
height={100}
fileType="emojis"
behaviour="upload"
previewAfterUpload
maxFileSize={500000}
remove={async () => void setFileId("")}
onUpload={async (id) => void setFileId(id)}
/>
),
},
}}
submitBtn={{
children: "Save",
palette: "secondary",
disabled: !fileId,
}}
onSubmit={async ({ name }) => {
await server.client.api.put(`/custom/emoji/${fileId}`, {
name,
parent: { type: "Server", id: server._id },
});
setFileId("");
}}
/>
</>
);
}

View file

@ -17,7 +17,11 @@ import { takeError } from "../error";
type BehaviourType = type BehaviourType =
| { behaviour: "ask"; onChange: (file: File) => void } | { behaviour: "ask"; onChange: (file: File) => void }
| { behaviour: "upload"; onUpload: (id: string) => Promise<void> } | {
behaviour: "upload";
onUpload: (id: string) => Promise<void>;
previewAfterUpload?: boolean;
}
| { | {
behaviour: "multi"; behaviour: "multi";
onChange: (files: File[]) => void; onChange: (files: File[]) => void;
@ -48,7 +52,8 @@ type Props = BehaviourType &
| "icons" | "icons"
| "avatars" | "avatars"
| "attachments" | "attachments"
| "banners"; | "banners"
| "emojis";
maxFileSize: number; maxFileSize: number;
remove: () => Promise<void>; remove: () => Promise<void>;
}; };
@ -114,6 +119,17 @@ export function FileUploader(props: Props) {
const client = useClient(); const client = useClient();
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [previewFile, setPreviewFile] = useState<File>(null!);
const [generatedPreviewURL, setGeneratedPreviewURL] = useState("");
useEffect(() => {
if (previewFile) {
const url: string = URL.createObjectURL(previewFile);
setGeneratedPreviewURL(url);
return () => URL.revokeObjectURL(url);
}
setGeneratedPreviewURL("");
}, [previewFile]);
function onClick() { function onClick() {
if (uploading) return; if (uploading) return;
@ -136,6 +152,10 @@ export function FileUploader(props: Props) {
files[0], files[0],
), ),
); );
if (props.previewAfterUpload) {
setPreviewFile(files[0]);
}
} }
} catch (err) { } catch (err) {
return modalController.push({ return modalController.push({
@ -164,7 +184,11 @@ export function FileUploader(props: Props) {
} else { } else {
onClick(); onClick();
} }
} else if (props.previewURL) { } else if (props.previewURL || previewFile) {
if (previewFile) {
setPreviewFile(null!);
}
props.remove(); props.remove();
} else { } else {
onClick(); onClick();
@ -266,7 +290,11 @@ export function FileUploader(props: Props) {
style={{ style={{
backgroundImage: backgroundImage:
style === "icon" style === "icon"
? `url('${previewURL ?? defaultPreview}')` ? `url('${
generatedPreviewURL ??
previewURL ??
defaultPreview
}')`
: previewURL : previewURL
? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')` ? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')`
: "none", : "none",
@ -288,7 +316,7 @@ export function FileUploader(props: Props) {
<span onClick={removeOrUpload}> <span onClick={removeOrUpload}>
{uploading ? ( {uploading ? (
<Text id="app.main.channel.uploading_file" /> <Text id="app.main.channel.uploading_file" />
) : props.previewURL ? ( ) : props.previewURL || previewFile ? (
<Text id="app.settings.actions.remove" /> <Text id="app.settings.actions.remove" />
) : ( ) : (
<Text id="app.settings.actions.upload" /> <Text id="app.settings.actions.upload" />

View file

@ -43,8 +43,9 @@ export const EXPERIMENTS: {
"This will enable the experimental plugin API. Only touch this if you know what you're doing.", "This will enable the experimental plugin API. Only touch this if you know what you're doing.",
}, },
picker: { picker: {
title: "Emoji Picker", title: "Custom Emoji",
description: "This will enable a work-in-progress emoji picker.", description:
"This will enable a work-in-progress emoji picker and custom emoji settings.",
}, },
}; };

View file

@ -6,6 +6,7 @@ import {
Envelope, Envelope,
UserX, UserX,
Trash, Trash,
HappyBeaming,
} from "@styled-icons/boxicons-solid"; } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Route, Switch, useHistory, useParams } from "react-router-dom"; import { Route, Switch, useHistory, useParams } from "react-router-dom";
@ -15,6 +16,8 @@ import { Text } from "preact-i18n";
import { LineDivider } from "@revoltchat/ui"; import { LineDivider } from "@revoltchat/ui";
import { state } from "../../mobx/State";
import ButtonItem from "../../components/navigation/items/ButtonItem"; import ButtonItem from "../../components/navigation/items/ButtonItem";
import { useClient } from "../../controllers/client/ClientController"; import { useClient } from "../../controllers/client/ClientController";
import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; import RequiresOnline from "../../controllers/client/jsx/RequiresOnline";
@ -22,6 +25,7 @@ import { modalController } from "../../controllers/modals/ModalController";
import { GenericSettings } from "./GenericSettings"; import { GenericSettings } from "./GenericSettings";
import { Bans } from "./server/Bans"; import { Bans } from "./server/Bans";
import { Categories } from "./server/Categories"; import { Categories } from "./server/Categories";
import { Emojis } from "./server/Emojis";
import { Invites } from "./server/Invites"; import { Invites } from "./server/Invites";
import { Members } from "./server/Members"; import { Members } from "./server/Members";
import { Overview } from "./server/Overview"; import { Overview } from "./server/Overview";
@ -68,6 +72,15 @@ export default observer(() => {
title: <Text id="app.settings.server_pages.roles.title" />, title: <Text id="app.settings.server_pages.roles.title" />,
hideTitle: true, hideTitle: true,
}, },
{
category: (
<Text id="app.settings.server_pages.customisation.title" />
),
id: "emojis",
icon: <HappyBeaming size={20} />,
title: <Text id="app.settings.server_pages.emojis.title" />,
hidden: !state.experiments.isEnabled("picker"),
},
{ {
category: ( category: (
<Text id="app.settings.server_pages.management.title" /> <Text id="app.settings.server_pages.management.title" />
@ -116,6 +129,11 @@ export default observer(() => {
<Roles server={server} /> <Roles server={server} />
</RequiresOnline> </RequiresOnline>
</Route> </Route>
<Route path="/server/:server/settings/emojis">
<RequiresOnline>
<Emojis server={server} />
</RequiresOnline>
</Route>
<Route> <Route>
<Overview server={server} /> <Overview server={server} />
</Route> </Route>

View file

@ -0,0 +1,83 @@
import { observer } from "mobx-react-lite";
import { Server } from "revolt.js";
import styled from "styled-components";
import { Text } from "preact-i18n";
import { Button, Column, Row, Stacked } from "@revoltchat/ui";
import UserShort from "../../../components/common/user/UserShort";
import { EmojiUploader } from "../../../components/settings/customisation/EmojiUploader";
import { modalController } from "../../../controllers/modals/ModalController";
interface Props {
server: Server;
}
const List = styled.div`
gap: 8px;
display: flex;
flex-wrap: wrap;
`;
const Emoji = styled(Column)`
padding: 8px;
border-radius: var(--border-radius);
background: var(--secondary-background);
`;
const Preview = styled.img`
width: 72px;
height: 72px;
object-fit: contain;
border-radius: var(--border-radius);
`;
const UserInfo = styled(Row)`
font-size: 12px;
svg {
width: 14px;
height: 14px;
}
`;
export const Emojis = observer(({ server }: Props) => {
const emoji = [...server.client.emojis.values()].filter(
(x) => x.parent.id === server._id,
);
return (
<Column>
<EmojiUploader server={server} />
<h3>
<Text id="app.settings.server_pages.emojis.title" />
{" "}
{emoji.length}
</h3>
<List>
{emoji.map((emoji) => (
<Emoji key={emoji._id} centred>
<Stacked>
<Preview src={emoji.imageURL} />
</Stacked>
<span>{`:${emoji.name}:`}</span>
<UserInfo centred>
<UserShort user={emoji.creator} />
</UserInfo>
<Button
palette="plain"
onClick={() =>
modalController.writeText(emoji._id)
}>
<Text id="app.context_menu.copy_id" />
</Button>
<Button palette="plain" onClick={() => emoji.delete()}>
<Text id="app.special.modals.actions.delete" />
</Button>
</Emoji>
))}
</List>
</Column>
);
});

View file

@ -2240,9 +2240,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@revoltchat/ui@npm:1.0.70": "@revoltchat/ui@npm:1.0.72":
version: 1.0.70 version: 1.0.72
resolution: "@revoltchat/ui@npm:1.0.70" resolution: "@revoltchat/ui@npm:1.0.72"
dependencies: dependencies:
"@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0
@ -2256,7 +2256,7 @@ __metadata:
react-virtuoso: ^2.12.0 react-virtuoso: ^2.12.0
peerDependencies: peerDependencies:
revolt.js: "*" revolt.js: "*"
checksum: 70eba00f8b2d4fed3f83cdd64488ab4eb99acc9dd0427333a41f203912d9664f878ec04300bc3eaea2a67fef1620472cce9ff7048ef1280f950825593cf316a7 checksum: 8ca6d68709591a9505cc62089ab42fe887fcd4ceaac81c7f673b41b7be1d850ba852d0c2dc09bb77c8e1bce22b3255ebfbf95922d57aa2c3d2288aacd7819a25
languageName: node languageName: node
linkType: hard linkType: hard
@ -3626,7 +3626,7 @@ __metadata:
"@hcaptcha/react-hcaptcha": ^0.3.6 "@hcaptcha/react-hcaptcha": ^0.3.6
"@insertish/vite-plugin-babel-macros": ^1.0.5 "@insertish/vite-plugin-babel-macros": ^1.0.5
"@preact/preset-vite": ^2.0.0 "@preact/preset-vite": ^2.0.0
"@revoltchat/ui": 1.0.70 "@revoltchat/ui": 1.0.72
"@rollup/plugin-replace": ^2.4.2 "@rollup/plugin-replace": ^2.4.2
"@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0
@ -3678,7 +3678,7 @@ __metadata:
preact-context-menu: 0.4.1 preact-context-menu: 0.4.1
preact-i18n: ^2.4.0-preactx preact-i18n: ^2.4.0-preactx
prettier: ^2.3.1 prettier: ^2.3.1
prismjs: ^1.23.0 prismjs: ^1.28.0
qrcode.react: ^3.0.2 qrcode.react: ^3.0.2
react-beautiful-dnd: ^13.1.0 react-beautiful-dnd: ^13.1.0
react-device-detect: 2.2.2 react-device-detect: 2.2.2
@ -7309,13 +7309,6 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prismjs@npm:^1.23.0":
version: 1.24.1
resolution: "prismjs@npm:1.24.1"
checksum: e5d14a4ba56773122039295bd760c72106acc964e04cb9831b9ae7e7a58f67ccac6c053e77e21f1018a3684f31d35bb065c0c81fd4ff00b73b1570c3ace4aef0
languageName: node
linkType: hard
"prismjs@npm:^1.24.1, prismjs@npm:^1.28.0": "prismjs@npm:^1.24.1, prismjs@npm:^1.28.0":
version: 1.28.0 version: 1.28.0
resolution: "prismjs@npm:1.28.0" resolution: "prismjs@npm:1.28.0"