diff --git a/external/lang b/external/lang index d4bc47b7..58408da6 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit d4bc47b729c7e69ce97216469692b39f4cd1640e +Subproject commit 58408da6c4090dd3a7808a663eaa95b8b1da7603 diff --git a/package.json b/package.json index 77435e5e..7e89c29a 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@hcaptcha/react-hcaptcha": "^0.3.6", "@insertish/vite-plugin-babel-macros": "^1.0.5", "@preact/preset-vite": "^2.0.0", - "@revoltchat/ui": "1.0.70", + "@revoltchat/ui": "1.0.72", "@rollup/plugin-replace": "^2.4.2", "@styled-icons/boxicons-logos": "^10.38.0", "@styled-icons/boxicons-regular": "^10.38.0", @@ -123,7 +123,7 @@ "preact-context-menu": "0.4.1", "preact-i18n": "^2.4.0-preactx", "prettier": "^2.3.1", - "prismjs": "^1.23.0", + "prismjs": "^1.28.0", "qrcode.react": "^3.0.2", "react-beautiful-dnd": "^13.1.0", "react-device-detect": "2.2.2", diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index cdae5341..7b76aef9 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -7,13 +7,7 @@ import { ulid } from "ulid"; import { Text } from "preact-i18n"; import { memo } from "preact/compat"; -import { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "preact/hooks"; +import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; import { IconButton, Picker } from "@revoltchat/ui"; @@ -28,7 +22,7 @@ import { SMOOTH_SCROLL_ON_RECEIVE, } from "../../../lib/renderer/Singleton"; -import { useApplicationState } from "../../../mobx/State"; +import { state, useApplicationState } from "../../../mobx/State"; import { Reply } from "../../../mobx/stores/MessageQueue"; import { emojiDictionary } from "../../../assets/emojis"; @@ -40,8 +34,8 @@ import { uploadFile, } from "../../../controllers/client/jsx/legacy/FileUploads"; import { modalController } from "../../../controllers/modals/ModalController"; +import { RenderEmoji } from "../../markdown/plugins/emoji"; import AutoComplete, { useAutoComplete } from "../AutoComplete"; -import Emoji from "../Emoji"; import { PermissionTooltip } from "../Tooltip"; import FilePreview from "./bars/FilePreview"; import ReplyBar from "./bars/ReplyBar"; @@ -149,6 +143,56 @@ const RE_SED = new RegExp("^s/([^])*/([^])*$"); // Tests for code block delimiters (``` at start of line) const RE_CODE_DELIMITER = new RegExp("^```", "gm"); +const HackAlertThisFileWillBeReplaced = observer(({ channel }: Props) => { + const renderEmoji = useMemo( + () => + memo(({ emoji }: { emoji: string }) => ( + { + const v = state.draft.get(channel._id); + state.draft.set( + channel._id, + `${v ? `${v} ` : ""}:${emoji}:`, + ); + }}> + + + )), + [], + ); + + const emojis: Record = { + 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 ( + + ); +}); + // ! FIXME: add to app config and load from app config export const CAN_UPLOAD_AT_ONCE = 5; @@ -312,6 +356,7 @@ export default observer(({ channel }: Props) => { async function sendFile(content: string) { if (uploadState.type !== "attached") return; const attachments: string[] = []; + setMessage; const cancel = Axios.CancelToken.source(); const files = uploadState.files; @@ -470,26 +515,6 @@ export default observer(({ channel }: Props) => { : undefined, }); - const renderEmoji = useMemo( - () => - memo(({ emoji }: { emoji: string }) => ( - { - const v = state.draft.get(channel._id); - setMessage(`${v ? `${v} ` : ""}:${emoji}:`); - }}> - - - )), - [], - ); - return ( <> @@ -533,10 +558,7 @@ export default observer(({ channel }: Props) => { /> {picker && ( - + )} @@ -659,11 +681,6 @@ export default observer(({ channel }: Props) => { onFocus={onFocus} onBlur={onBlur} /> - {/* - - - - */} {state.experiments.isEnabled("picker") && ( setPicker(!picker)}> diff --git a/src/components/common/user/UserBadges.tsx b/src/components/common/user/UserBadges.tsx index b8c36e44..b14d8c6a 100644 --- a/src/components/common/user/UserBadges.tsx +++ b/src/components/common/user/UserBadges.tsx @@ -16,6 +16,7 @@ enum Badges { Paw = 128, EarlyAdopter = 256, ReservedRelevantJokeBadge1 = 512, + ReservedRelevantJokeBadge2 = 1024, } const BadgesBase = styled.div` @@ -135,6 +136,13 @@ export default function UserBadges({ badges, uid }: Props) { ) : ( <> )} + {badges & Badges.ReservedRelevantJokeBadge2 ? ( + + + + ) : ( + <> + )} {badges & Badges.Paw ? ( diff --git a/src/components/markdown/RemarkRenderer.tsx b/src/components/markdown/RemarkRenderer.tsx index 26f03b37..02ac472a 100644 --- a/src/components/markdown/RemarkRenderer.tsx +++ b/src/components/markdown/RemarkRenderer.tsx @@ -180,7 +180,7 @@ const Container = styled.div<{ largeEmoji: boolean }>` /** * 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 diff --git a/src/components/markdown/plugins/emoji.tsx b/src/components/markdown/plugins/emoji.tsx index f97e85bc..033af2de 100644 --- a/src/components/markdown/plugins/emoji.tsx +++ b/src/components/markdown/plugins/emoji.tsx @@ -15,15 +15,19 @@ const Emoji = styled.img` `; export function RenderEmoji({ match }: CustomComponentProps) { + const url = + match in emojiDictionary + ? parseEmoji(emojiDictionary[match as keyof typeof emojiDictionary]) + : clientController.getAvailableClient().emojis!.get(match)! + .imageURL; + return ( ); } diff --git a/src/components/navigation/right/MemberList.tsx b/src/components/navigation/right/MemberList.tsx index 3913c20b..1163fea5 100644 --- a/src/components/navigation/right/MemberList.tsx +++ b/src/components/navigation/right/MemberList.tsx @@ -99,7 +99,7 @@ export default function MemberList({ )} {entry.type !== "no_offline" && ( <> - {" - "} + {" – "} {entry.users.length} )} diff --git a/src/components/settings/customisation/EmojiUploader.tsx b/src/components/settings/customisation/EmojiUploader.tsx new file mode 100644 index 00000000..4a1b42c5 --- /dev/null +++ b/src/components/settings/customisation/EmojiUploader.tsx @@ -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(); + + return ( + <> +

Upload Emoji

+
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(""); + }} + /> + + ); +} diff --git a/src/controllers/client/jsx/legacy/FileUploads.tsx b/src/controllers/client/jsx/legacy/FileUploads.tsx index f6597af0..fe175ebc 100644 --- a/src/controllers/client/jsx/legacy/FileUploads.tsx +++ b/src/controllers/client/jsx/legacy/FileUploads.tsx @@ -17,7 +17,11 @@ import { takeError } from "../error"; type BehaviourType = | { behaviour: "ask"; onChange: (file: File) => void } - | { behaviour: "upload"; onUpload: (id: string) => Promise } + | { + behaviour: "upload"; + onUpload: (id: string) => Promise; + previewAfterUpload?: boolean; + } | { behaviour: "multi"; onChange: (files: File[]) => void; @@ -48,7 +52,8 @@ type Props = BehaviourType & | "icons" | "avatars" | "attachments" - | "banners"; + | "banners" + | "emojis"; maxFileSize: number; remove: () => Promise; }; @@ -114,6 +119,17 @@ export function FileUploader(props: Props) { const client = useClient(); const [uploading, setUploading] = useState(false); + const [previewFile, setPreviewFile] = useState(null!); + const [generatedPreviewURL, setGeneratedPreviewURL] = useState(""); + useEffect(() => { + if (previewFile) { + const url: string = URL.createObjectURL(previewFile); + setGeneratedPreviewURL(url); + return () => URL.revokeObjectURL(url); + } + + setGeneratedPreviewURL(""); + }, [previewFile]); function onClick() { if (uploading) return; @@ -136,6 +152,10 @@ export function FileUploader(props: Props) { files[0], ), ); + + if (props.previewAfterUpload) { + setPreviewFile(files[0]); + } } } catch (err) { return modalController.push({ @@ -164,7 +184,11 @@ export function FileUploader(props: Props) { } else { onClick(); } - } else if (props.previewURL) { + } else if (props.previewURL || previewFile) { + if (previewFile) { + setPreviewFile(null!); + } + props.remove(); } else { onClick(); @@ -266,7 +290,11 @@ export function FileUploader(props: Props) { style={{ backgroundImage: style === "icon" - ? `url('${previewURL ?? defaultPreview}')` + ? `url('${ + generatedPreviewURL ?? + previewURL ?? + defaultPreview + }')` : previewURL ? `linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5) ), url('${previewURL}')` : "none", @@ -288,7 +316,7 @@ export function FileUploader(props: Props) { {uploading ? ( - ) : props.previewURL ? ( + ) : props.previewURL || previewFile ? ( ) : ( diff --git a/src/mobx/stores/Experiments.ts b/src/mobx/stores/Experiments.ts index a91318a5..c37ad5d6 100644 --- a/src/mobx/stores/Experiments.ts +++ b/src/mobx/stores/Experiments.ts @@ -43,8 +43,9 @@ export const EXPERIMENTS: { "This will enable the experimental plugin API. Only touch this if you know what you're doing.", }, picker: { - title: "Emoji Picker", - description: "This will enable a work-in-progress emoji picker.", + title: "Custom Emoji", + description: + "This will enable a work-in-progress emoji picker and custom emoji settings.", }, }; diff --git a/src/pages/settings/ServerSettings.tsx b/src/pages/settings/ServerSettings.tsx index d008cf0a..6cd688a7 100644 --- a/src/pages/settings/ServerSettings.tsx +++ b/src/pages/settings/ServerSettings.tsx @@ -6,6 +6,7 @@ import { Envelope, UserX, Trash, + HappyBeaming, } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; import { Route, Switch, useHistory, useParams } from "react-router-dom"; @@ -15,6 +16,8 @@ import { Text } from "preact-i18n"; import { LineDivider } from "@revoltchat/ui"; +import { state } from "../../mobx/State"; + import ButtonItem from "../../components/navigation/items/ButtonItem"; import { useClient } from "../../controllers/client/ClientController"; import RequiresOnline from "../../controllers/client/jsx/RequiresOnline"; @@ -22,6 +25,7 @@ import { modalController } from "../../controllers/modals/ModalController"; import { GenericSettings } from "./GenericSettings"; import { Bans } from "./server/Bans"; import { Categories } from "./server/Categories"; +import { Emojis } from "./server/Emojis"; import { Invites } from "./server/Invites"; import { Members } from "./server/Members"; import { Overview } from "./server/Overview"; @@ -68,6 +72,15 @@ export default observer(() => { title: , hideTitle: true, }, + { + category: ( + + ), + id: "emojis", + icon: , + title: , + hidden: !state.experiments.isEnabled("picker"), + }, { category: ( @@ -116,6 +129,11 @@ export default observer(() => { + + + + + diff --git a/src/pages/settings/server/Emojis.tsx b/src/pages/settings/server/Emojis.tsx new file mode 100644 index 00000000..6acaa453 --- /dev/null +++ b/src/pages/settings/server/Emojis.tsx @@ -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 ( + + +

+ + {" – "} + {emoji.length} +

+ + {emoji.map((emoji) => ( + + + + + {`:${emoji.name}:`} + + + + + + + ))} + +
+ ); +}); diff --git a/yarn.lock b/yarn.lock index 428bc875..61a6f15f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2240,9 +2240,9 @@ __metadata: languageName: node linkType: hard -"@revoltchat/ui@npm:1.0.70": - version: 1.0.70 - resolution: "@revoltchat/ui@npm:1.0.70" +"@revoltchat/ui@npm:1.0.72": + version: 1.0.72 + resolution: "@revoltchat/ui@npm:1.0.72" dependencies: "@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0 @@ -2256,7 +2256,7 @@ __metadata: react-virtuoso: ^2.12.0 peerDependencies: revolt.js: "*" - checksum: 70eba00f8b2d4fed3f83cdd64488ab4eb99acc9dd0427333a41f203912d9664f878ec04300bc3eaea2a67fef1620472cce9ff7048ef1280f950825593cf316a7 + checksum: 8ca6d68709591a9505cc62089ab42fe887fcd4ceaac81c7f673b41b7be1d850ba852d0c2dc09bb77c8e1bce22b3255ebfbf95922d57aa2c3d2288aacd7819a25 languageName: node linkType: hard @@ -3626,7 +3626,7 @@ __metadata: "@hcaptcha/react-hcaptcha": ^0.3.6 "@insertish/vite-plugin-babel-macros": ^1.0.5 "@preact/preset-vite": ^2.0.0 - "@revoltchat/ui": 1.0.70 + "@revoltchat/ui": 1.0.72 "@rollup/plugin-replace": ^2.4.2 "@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0 @@ -3678,7 +3678,7 @@ __metadata: preact-context-menu: 0.4.1 preact-i18n: ^2.4.0-preactx prettier: ^2.3.1 - prismjs: ^1.23.0 + prismjs: ^1.28.0 qrcode.react: ^3.0.2 react-beautiful-dnd: ^13.1.0 react-device-detect: 2.2.2 @@ -7309,13 +7309,6 @@ __metadata: languageName: node 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": version: 1.28.0 resolution: "prismjs@npm:1.28.0"