diff --git a/package.json b/package.json index 9a8684a6..0a6d24ad 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ } }, "dependencies": { + "@floating-ui/react-dom": "^0.7.2", + "@floating-ui/react-dom-interactions": "^0.7.0", "fs-extra": "^10.0.0", "klaw": "^3.0.0", "sirv-cli": "^1.0.14", diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index b1fae6a6..a30550d1 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -5,7 +5,7 @@ import { useTriggerEvents } from "preact-context-menu"; import { memo } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; -import { Category } from "@revoltchat/ui"; +import { Category, Button } from "@revoltchat/ui"; import { internalEmit } from "../../../lib/eventEmitter"; import { isTouchscreenDevice } from "../../../lib/isTouchscreenDevice"; @@ -88,6 +88,7 @@ const Message = observer( // ! FIXME(?): animate on hover const [mouseHovering, setAnimate] = useState(false); + const [reactionsOpen, setReactionsOpen] = useState(false); useEffect(() => setAnimate(false), [replacement]); return ( @@ -182,10 +183,12 @@ const Message = observer( ))} - {mouseHovering && + {(mouseHovering || reactionsOpen) && !replacement && !isTouchscreenDevice && ( diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index 093d03d2..4e65cd6b 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -26,7 +26,10 @@ import { state, useApplicationState } from "../../../mobx/State"; import { Reply } from "../../../mobx/stores/MessageQueue"; import { emojiDictionary } from "../../../assets/emojis"; -import { useClient } from "../../../controllers/client/ClientController"; +import { + clientController, + useClient, +} from "../../../controllers/client/ClientController"; import { takeError } from "../../../controllers/client/jsx/error"; import { FileUploader, @@ -143,8 +146,14 @@ 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, onClose }: Props & { onClose: () => void }) => { +export const HackAlertThisFileWillBeReplaced = observer( + ({ + onSelect, + onClose, + }: { + onSelect: (emoji: string) => void; + onClose: () => void; + }) => { const renderEmoji = useMemo( () => memo(({ emoji }: { emoji: string }) => ( @@ -162,8 +171,12 @@ const HackAlertThisFileWillBeReplaced = observer( 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) + const list = [...clientController.getReadyClient()!.emojis.values()] + .filter( + (emoji) => + emoji.parent.type !== "Detached" && + emoji.parent.id === server._id, + ) .map(({ _id, name }) => ({ id: _id, name })); if (list.length > 0) { @@ -187,13 +200,7 @@ const HackAlertThisFileWillBeReplaced = observer( emojis={emojis} categories={categories} renderEmoji={renderEmoji} - onSelect={(emoji) => { - const v = state.draft.get(channel._id); - state.draft.set( - channel._id, - `${v ? `${v} ` : ""}:${emoji}:`, - ); - }} + onSelect={onSelect} onClose={onClose} /> ); @@ -568,7 +575,13 @@ export default observer(({ channel }: Props) => { {picker && ( { + const v = state.draft.get(channel._id); + state.draft.set( + channel._id, + `${v ? `${v} ` : ""}:${emoji}:`, + ); + }} onClose={closePicker} /> )} diff --git a/src/components/common/messaging/attachments/Reactions.tsx b/src/components/common/messaging/attachments/Reactions.tsx index d53ca835..10abd2e6 100644 --- a/src/components/common/messaging/attachments/Reactions.tsx +++ b/src/components/common/messaging/attachments/Reactions.tsx @@ -1,11 +1,20 @@ +import { + autoPlacement, + offset, + shift, + useFloating, +} from "@floating-ui/react-dom-interactions"; import { observer } from "mobx-react-lite"; import { Message } from "revolt.js"; import styled, { css } from "styled-components"; -import { useCallback } from "preact/hooks"; +import { createPortal } from "preact/compat"; +import { useCallback, useRef } from "preact/hooks"; +import { emojiDictionary } from "../../../../assets/emojis"; import { useClient } from "../../../../controllers/client/ClientController"; import { RenderEmoji } from "../../../markdown/plugins/emoji"; +import { HackAlertThisFileWillBeReplaced } from "../MessageBox"; interface Props { message: Message; @@ -131,3 +140,78 @@ export const Reactions = observer(({ message }: Props) => { ); }); + +const Base = styled.div` + > div { + position: unset; + } +`; + +/** + * ! FIXME: rewrite + */ +export const ReactionWrapper: React.FC<{ + message: Message; + open: boolean; + setOpen: (v: boolean) => void; +}> = ({ open, setOpen, message, children }) => { + const { x, y, reference, floating, strategy } = useFloating({ + open, + middleware: [ + offset(4), + shift({ mainAxis: true, crossAxis: true, padding: 4 }), + autoPlacement(), + ], + }); + + const skip = useRef(); + const toggle = () => { + if (skip.current) { + skip.current = null; + return; + } + + setOpen(!open); + + if (!open) { + skip.current = true; + } + }; + + return ( + <> +
+ {children} +
+ + {createPortal( +
+ {open && ( + + + message.react( + emojiDictionary[ + emoji as keyof typeof emojiDictionary + ] ?? emoji, + ) + } + onClose={toggle} + /> + + )} +
, + document.body, + )} + + ); +}; diff --git a/src/components/common/messaging/bars/MessageOverlayBar.tsx b/src/components/common/messaging/bars/MessageOverlayBar.tsx index 8f38e655..f17dcbfd 100644 --- a/src/components/common/messaging/bars/MessageOverlayBar.tsx +++ b/src/components/common/messaging/bars/MessageOverlayBar.tsx @@ -5,6 +5,7 @@ import { Share, InfoSquare, Notification, + HappyBeaming, } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; import { Message as MessageObject } from "revolt.js"; @@ -17,12 +18,16 @@ import { internalEmit } from "../../../../lib/eventEmitter"; import { shiftKeyPressed } from "../../../../lib/modifiers"; import { getRenderer } from "../../../../lib/renderer/Singleton"; +import { state } from "../../../../mobx/State"; import { QueuedMessage } from "../../../../mobx/stores/MessageQueue"; import { modalController } from "../../../../controllers/modals/ModalController"; import Tooltip from "../../../common/Tooltip"; +import { ReactionWrapper } from "../attachments/Reactions"; interface Props { + reactionsOpen: boolean; + setReactionsOpen: (v: boolean) => void; message: MessageObject; queued?: QueuedMessage; } @@ -81,125 +86,152 @@ const Divider = styled.div` background: var(--tertiary-background); `; -export const MessageOverlayBar = observer(({ message, queued }: Props) => { - const client = message.client; - const isAuthor = message.author_id === client.user!._id; +export const MessageOverlayBar = observer( + ({ reactionsOpen, setReactionsOpen, message, queued }: Props) => { + const client = message.client; + const isAuthor = message.author_id === client.user!._id; - const [copied, setCopied] = useState<"link" | "id">(null!); - const [extraActions, setExtra] = useState(shiftKeyPressed); + const [copied, setCopied] = useState<"link" | "id">(null!); + const [extraActions, setExtra] = useState(shiftKeyPressed); - useEffect(() => { - const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey); + useEffect(() => { + const handler = (ev: KeyboardEvent) => setExtra(ev.shiftKey); - document.addEventListener("keyup", handler); - document.addEventListener("keydown", handler); + document.addEventListener("keyup", handler); + document.addEventListener("keydown", handler); - return () => { - document.removeEventListener("keyup", handler); - document.removeEventListener("keydown", handler); - }; - }); + return () => { + document.removeEventListener("keyup", handler); + document.removeEventListener("keydown", handler); + }; + }); - return ( - - - internalEmit("ReplyBar", "add", message)}> - - - + return ( + + {message.channel?.havePermission("SendMessage") && ( + + + internalEmit("ReplyBar", "add", message) + }> + + + + )} - {isAuthor && ( - + {message.channel?.havePermission("React") && + state.experiments.isEnabled("picker") && ( + + + + + + + + )} + + {isAuthor && ( + + + internalEmit( + "MessageRenderer", + "edit_message", + message._id, + ) + }> + + + + )} + {isAuthor || + (message.channel && + message.channel.havePermission("ManageMessages")) ? ( + + + e.shiftKey + ? message.delete() + : modalController.push({ + type: "delete_message", + target: message, + }) + }> + + + + ) : undefined} + - internalEmit( - "MessageRenderer", - "edit_message", - message._id, - ) + openContextMenu("Menu", { + message, + contextualChannel: message.channel_id, + queued, + }) }> - + - )} - {isAuthor || - (message.channel && - message.channel.havePermission("ManageMessages")) ? ( - - - e.shiftKey - ? message.delete() - : modalController.push({ - type: "delete_message", - target: message, - }) - }> - - - - ) : undefined} - - - openContextMenu("Menu", { - message, - contextualChannel: message.channel_id, - queued, - }) - }> - - - - {extraActions && ( - <> - - - { - // ! FIXME: deduplicate this code with ctx menu - const messages = getRenderer( - message.channel!, - ).messages; - const index = messages.findIndex( - (x) => x._id === message._id, - ); + {extraActions && ( + <> + + + { + // ! FIXME: deduplicate this code with ctx menu + const messages = getRenderer( + message.channel!, + ).messages; + const index = messages.findIndex( + (x) => x._id === message._id, + ); - let unread_id = message._id; - if (index > 0) { - unread_id = messages[index - 1]._id; - } + let unread_id = message._id; + if (index > 0) { + unread_id = messages[index - 1]._id; + } - internalEmit("NewMessages", "mark", unread_id); - message.channel?.ack(unread_id, true); - }}> - - - - - { - setCopied("link"); - modalController.writeText(message.url); - }}> - - - - - { - setCopied("id"); - modalController.writeText(message._id); - }}> - - - - - )} - - ); -}); + internalEmit( + "NewMessages", + "mark", + unread_id, + ); + message.channel?.ack(unread_id, true); + }}> + + + + + { + setCopied("link"); + modalController.writeText(message.url); + }}> + + + + + { + setCopied("id"); + modalController.writeText(message._id); + }}> + + + + + )} + + ); + }, +); diff --git a/src/components/navigation/items/ButtonItem.tsx b/src/components/navigation/items/ButtonItem.tsx index 8d812039..77d7e68f 100644 --- a/src/components/navigation/items/ButtonItem.tsx +++ b/src/components/navigation/items/ButtonItem.tsx @@ -155,7 +155,9 @@ export const ChannelButton = observer((props: ChannelProps) => { data-alert={alerting} data-muted={muted} aria-label={channel.name} - className={classNames(styles.item, { [styles.compact]: compact })} + className={classNames(styles.item, { + [styles.compact]: compact, + })} {...useTriggerEvents("Menu", { channel: channel._id, unread: !!alert, @@ -175,7 +177,9 @@ export const ChannelButton = observer((props: ChannelProps) => { )} diff --git a/src/mobx/stores/Experiments.ts b/src/mobx/stores/Experiments.ts index c37ad5d6..23dd1751 100644 --- a/src/mobx/stores/Experiments.ts +++ b/src/mobx/stores/Experiments.ts @@ -45,7 +45,7 @@ export const EXPERIMENTS: { picker: { title: "Custom Emoji", description: - "This will enable a work-in-progress emoji picker and custom emoji settings.", + "This will enable a work-in-progress emoji picker, custom emoji settings and reaction picker.", }, }; diff --git a/src/pages/developer/Developer.tsx b/src/pages/developer/Developer.tsx index c9e7f617..aeeff64e 100644 --- a/src/pages/developer/Developer.tsx +++ b/src/pages/developer/Developer.tsx @@ -43,6 +43,7 @@ export default function Developer() { fields={{ provider: GAMING! }} /> +
setCrash(true)}>click to crash app {crash && (window as any).sus.sus()} diff --git a/yarn.lock b/yarn.lock index 45e1b51b..bf7b2581 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1825,6 +1825,49 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^0.7.3": + version: 0.7.3 + resolution: "@floating-ui/core@npm:0.7.3" + checksum: f48f9fb0d19dcbe7a68c38e8de7fabb11f0c0e6e0ef215ae60b5004900bacb1386e7b89cb377d91a90ff7d147ea1f06c2905136ecf34dea162d9696d8f448d5f + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^0.5.3": + version: 0.5.4 + resolution: "@floating-ui/dom@npm:0.5.4" + dependencies: + "@floating-ui/core": ^0.7.3 + checksum: 9f9d8a51a828c6be5f187204aa6d293c6c9ef70d51dcc5891a4d85683745fceebf79ff8826d0f75ae41b45c3b138367d339756f27f41be87a8770742ebc0de42 + languageName: node + linkType: hard + +"@floating-ui/react-dom-interactions@npm:^0.7.0": + version: 0.7.0 + resolution: "@floating-ui/react-dom-interactions@npm:0.7.0" + dependencies: + "@floating-ui/react-dom": ^0.7.2 + aria-hidden: ^1.1.3 + use-isomorphic-layout-effect: ^1.1.1 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 7cf0875a65c55c06a0d0cc91e07d725cb0be8cccf61d6c08ab91f24c80c9dce370d6a47e6b7c4d49c3d9456136d840a57ca548b0bc2316f3ff869ac8e1797424 + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^0.7.2": + version: 0.7.2 + resolution: "@floating-ui/react-dom@npm:0.7.2" + dependencies: + "@floating-ui/dom": ^0.5.3 + use-isomorphic-layout-effect: ^1.1.1 + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: bc3f2b5557f87f6f4bbccfe3e8d097abafad61a41083d3b79f3499f27590e273bcb3dc7136c2444841ee7a8c0d2a70cc1385458c16103fa8b70eade80c24af52 + languageName: node + linkType: hard + "@fontsource/atkinson-hyperlegible@npm:^4.4.5": version: 4.5.1 resolution: "@fontsource/atkinson-hyperlegible@npm:4.5.1" @@ -3156,6 +3199,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.1.3": + version: 1.1.3 + resolution: "aria-hidden@npm:1.1.3" + dependencies: + tslib: ^1.0.0 + checksum: 2d40a328246baac7ae0b243ebe0cbef53c836c5f78c9212e9c1ff93f3aee185bd9aa51773e161e0025722d691c9d5f125070f6175a7074c4a57778ddc30d9e74 + languageName: node + linkType: hard + "array-includes@npm:^3.1.2, array-includes@npm:^3.1.3": version: 3.1.3 resolution: "array-includes@npm:3.1.3" @@ -3603,6 +3655,8 @@ __metadata: resolution: "client@workspace:." dependencies: "@babel/plugin-proposal-decorators": ^7.17.9 + "@floating-ui/react-dom": ^0.7.2 + "@floating-ui/react-dom-interactions": ^0.7.0 "@fontsource/atkinson-hyperlegible": ^4.4.5 "@fontsource/bitter": ^4.5.7 "@fontsource/comic-neue": ^4.4.5 @@ -8792,7 +8846,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1": +"tslib@npm:^1.0.0, tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd @@ -9131,6 +9185,18 @@ __metadata: languageName: node linkType: hard +"use-isomorphic-layout-effect@npm:^1.1.1": + version: 1.1.2 + resolution: "use-isomorphic-layout-effect@npm:1.1.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: a6532f7fc9ae222c3725ff0308aaf1f1ddbd3c00d685ef9eee6714fd0684de5cb9741b432fbf51e61a784e2955424864f7ea9f99734a02f237b17ad3e18ea5cb + languageName: node + linkType: hard + "use-memo-one@npm:^1.1.1": version: 1.1.2 resolution: "use-memo-one@npm:1.1.2"