mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 01:15:28 +00:00
feat: add reaction button to overlay
This commit is contained in:
@@ -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(
|
||||
<Embed key={index} embed={embed} />
|
||||
))}
|
||||
<Reactions message={message} />
|
||||
{mouseHovering &&
|
||||
{(mouseHovering || reactionsOpen) &&
|
||||
!replacement &&
|
||||
!isTouchscreenDevice && (
|
||||
<MessageOverlayBar
|
||||
reactionsOpen={reactionsOpen}
|
||||
setReactionsOpen={setReactionsOpen}
|
||||
message={message}
|
||||
queued={queued}
|
||||
/>
|
||||
|
||||
@@ -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) => {
|
||||
<FloatingLayer>
|
||||
{picker && (
|
||||
<HackAlertThisFileWillBeReplaced
|
||||
channel={channel}
|
||||
onSelect={(emoji) => {
|
||||
const v = state.draft.get(channel._id);
|
||||
state.draft.set(
|
||||
channel._id,
|
||||
`${v ? `${v} ` : ""}:${emoji}:`,
|
||||
);
|
||||
}}
|
||||
onClose={closePicker}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
</List>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
ref={reference}
|
||||
onClick={toggle}
|
||||
style={{ width: "fit-content" }}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<div id="reaction">
|
||||
{open && (
|
||||
<Base
|
||||
ref={floating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
}}>
|
||||
<HackAlertThisFileWillBeReplaced
|
||||
onSelect={(emoji) =>
|
||||
message.react(
|
||||
emojiDictionary[
|
||||
emoji as keyof typeof emojiDictionary
|
||||
] ?? emoji,
|
||||
)
|
||||
}
|
||||
onClose={toggle}
|
||||
/>
|
||||
</Base>
|
||||
)}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<OverlayBar>
|
||||
<Tooltip content="Reply">
|
||||
<Entry onClick={() => internalEmit("ReplyBar", "add", message)}>
|
||||
<Share size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
return (
|
||||
<OverlayBar>
|
||||
{message.channel?.havePermission("SendMessage") && (
|
||||
<Tooltip content="Reply">
|
||||
<Entry
|
||||
onClick={() =>
|
||||
internalEmit("ReplyBar", "add", message)
|
||||
}>
|
||||
<Share size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<Tooltip content="Edit">
|
||||
{message.channel?.havePermission("React") &&
|
||||
state.experiments.isEnabled("picker") && (
|
||||
<ReactionWrapper
|
||||
open={reactionsOpen}
|
||||
setOpen={setReactionsOpen}
|
||||
message={message}>
|
||||
<Tooltip content="React">
|
||||
<Entry>
|
||||
<HappyBeaming size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
</ReactionWrapper>
|
||||
)}
|
||||
|
||||
{isAuthor && (
|
||||
<Tooltip content="Edit">
|
||||
<Entry
|
||||
onClick={() =>
|
||||
internalEmit(
|
||||
"MessageRenderer",
|
||||
"edit_message",
|
||||
message._id,
|
||||
)
|
||||
}>
|
||||
<Pencil size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAuthor ||
|
||||
(message.channel &&
|
||||
message.channel.havePermission("ManageMessages")) ? (
|
||||
<Tooltip content="Delete">
|
||||
<Entry
|
||||
onClick={(e) =>
|
||||
e.shiftKey
|
||||
? message.delete()
|
||||
: modalController.push({
|
||||
type: "delete_message",
|
||||
target: message,
|
||||
})
|
||||
}>
|
||||
<Trash size={18} color={"var(--error)"} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
<Tooltip content="More">
|
||||
<Entry
|
||||
onClick={() =>
|
||||
internalEmit(
|
||||
"MessageRenderer",
|
||||
"edit_message",
|
||||
message._id,
|
||||
)
|
||||
openContextMenu("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel_id,
|
||||
queued,
|
||||
})
|
||||
}>
|
||||
<Pencil size={18} />
|
||||
<DotsVerticalRounded size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAuthor ||
|
||||
(message.channel &&
|
||||
message.channel.havePermission("ManageMessages")) ? (
|
||||
<Tooltip content="Delete">
|
||||
<Entry
|
||||
onClick={(e) =>
|
||||
e.shiftKey
|
||||
? message.delete()
|
||||
: modalController.push({
|
||||
type: "delete_message",
|
||||
target: message,
|
||||
})
|
||||
}>
|
||||
<Trash size={18} color={"var(--error)"} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
) : undefined}
|
||||
<Tooltip content="More">
|
||||
<Entry
|
||||
onClick={() =>
|
||||
openContextMenu("Menu", {
|
||||
message,
|
||||
contextualChannel: message.channel_id,
|
||||
queued,
|
||||
})
|
||||
}>
|
||||
<DotsVerticalRounded size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
{extraActions && (
|
||||
<>
|
||||
<Divider />
|
||||
<Tooltip content="Mark as Unread">
|
||||
<Entry
|
||||
onClick={() => {
|
||||
// ! FIXME: deduplicate this code with ctx menu
|
||||
const messages = getRenderer(
|
||||
message.channel!,
|
||||
).messages;
|
||||
const index = messages.findIndex(
|
||||
(x) => x._id === message._id,
|
||||
);
|
||||
{extraActions && (
|
||||
<>
|
||||
<Divider />
|
||||
<Tooltip content="Mark as Unread">
|
||||
<Entry
|
||||
onClick={() => {
|
||||
// ! 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);
|
||||
}}>
|
||||
<Notification size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={copied === "link" ? "Copied!" : "Copy Link"}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("link");
|
||||
modalController.writeText(message.url);
|
||||
}}>
|
||||
<LinkAlt size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={copied === "id" ? "Copied!" : "Copy ID"}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("id");
|
||||
modalController.writeText(message._id);
|
||||
}}>
|
||||
<InfoSquare size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</OverlayBar>
|
||||
);
|
||||
});
|
||||
internalEmit(
|
||||
"NewMessages",
|
||||
"mark",
|
||||
unread_id,
|
||||
);
|
||||
message.channel?.ack(unread_id, true);
|
||||
}}>
|
||||
<Notification size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={
|
||||
copied === "link" ? "Copied!" : "Copy Link"
|
||||
}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("link");
|
||||
modalController.writeText(message.url);
|
||||
}}>
|
||||
<LinkAlt size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={copied === "id" ? "Copied!" : "Copy ID"}
|
||||
hideOnClick={false}>
|
||||
<Entry
|
||||
onClick={() => {
|
||||
setCopied("id");
|
||||
modalController.writeText(message._id);
|
||||
}}>
|
||||
<InfoSquare size={18} />
|
||||
</Entry>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</OverlayBar>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
<Text
|
||||
id="quantities.members"
|
||||
plural={channel.recipients!.length}
|
||||
fields={{ count: channel.recipients!.length }}
|
||||
fields={{
|
||||
count: channel.recipients!.length,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ export default function Developer() {
|
||||
fields={{ provider: <b>GAMING!</b> }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "16px" }}>
|
||||
<a onClick={() => setCrash(true)}>click to crash app</a>
|
||||
{crash && (window as any).sus.sus()}
|
||||
|
||||
Reference in New Issue
Block a user