mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-10 10:35:27 +00:00
[feat] Message reaction visualization. (#2)
Allow message user reaction list by hovering on desktop and long-tapping on mobile. Co-authored-by: jmug <u.g.a.mariano@gmail.com> Reviewed-on: HMC/handmade-revolt#2
This commit is contained in:
@@ -16,6 +16,11 @@ import { IconButton } from "@revoltchat/ui";
|
|||||||
|
|
||||||
import { emojiDictionary } from "../../../../assets/emojis";
|
import { emojiDictionary } from "../../../../assets/emojis";
|
||||||
import { useClient } from "../../../../controllers/client/ClientController";
|
import { useClient } from "../../../../controllers/client/ClientController";
|
||||||
|
import { modalController } from "../../../../controllers/modals/ModalController";
|
||||||
|
import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice";
|
||||||
|
import Tooltip from "../../Tooltip";
|
||||||
|
import UserIcon from "../../user/UserIcon";
|
||||||
|
import { Username } from "../../user/UserShort";
|
||||||
import { RenderEmoji } from "../../../markdown/plugins/emoji";
|
import { RenderEmoji } from "../../../markdown/plugins/emoji";
|
||||||
import { HackAlertThisFileWillBeReplaced } from "../MessageBox";
|
import { HackAlertThisFileWillBeReplaced } from "../MessageBox";
|
||||||
|
|
||||||
@@ -85,6 +90,38 @@ const Reaction = styled.div<{ active: boolean }>`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tooltip content for reaction users
|
||||||
|
*/
|
||||||
|
const TooltipUserList = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 0;
|
||||||
|
max-width: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TooltipUserRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
padding: 4px 6px;
|
||||||
|
margin: -4px -6px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TooltipMoreText = styled.div`
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--secondary-foreground);
|
||||||
|
padding-top: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render reactions on a message
|
* Render reactions on a message
|
||||||
*/
|
*/
|
||||||
@@ -98,16 +135,97 @@ export const Reactions = observer(({ message }: Props) => {
|
|||||||
const Entry = useCallback(
|
const Entry = useCallback(
|
||||||
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
|
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
|
||||||
const active = user_ids?.has(client.user!._id) || false;
|
const active = user_ids?.has(client.user!._id) || false;
|
||||||
|
const userIds = user_ids ? Array.from(user_ids) : [];
|
||||||
|
const longPressTimer = useRef<number | null>(null);
|
||||||
|
const didLongPress = useRef(false);
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (didLongPress.current) {
|
||||||
|
didLongPress.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
active ? message.unreact(id) : message.react(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
modalController.push({
|
||||||
|
type: "reaction_users",
|
||||||
|
emoji: id,
|
||||||
|
userIds,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
didLongPress.current = false;
|
||||||
|
longPressTimer.current = window.setTimeout(() => {
|
||||||
|
didLongPress.current = true;
|
||||||
|
openModal();
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: TouchEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (longPressTimer.current) {
|
||||||
|
clearTimeout(longPressTimer.current);
|
||||||
|
longPressTimer.current = null;
|
||||||
|
// Short tap - trigger manually since preventDefault blocks click
|
||||||
|
if (!didLongPress.current) {
|
||||||
|
active ? message.unreact(id) : message.react(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
didLongPress.current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openUserProfile = (userId: string) => {
|
||||||
|
modalController.push({ type: "user_profile", user_id: userId });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build tooltip content showing up to 10 users
|
||||||
|
const tooltipContent = (
|
||||||
|
<TooltipUserList>
|
||||||
|
{userIds.slice(0, 10).map((userId) => {
|
||||||
|
const user = client.users.get(userId);
|
||||||
return (
|
return (
|
||||||
|
<TooltipUserRow
|
||||||
|
key={userId}
|
||||||
|
onClick={() => openUserProfile(userId)}>
|
||||||
|
<UserIcon target={user} size={18} />
|
||||||
|
<Username user={user} />
|
||||||
|
</TooltipUserRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{userIds.length > 10 && (
|
||||||
|
<TooltipMoreText>
|
||||||
|
and {userIds.length - 10} more...
|
||||||
|
</TooltipMoreText>
|
||||||
|
)}
|
||||||
|
</TooltipUserList>
|
||||||
|
);
|
||||||
|
|
||||||
|
const reactionElement = (
|
||||||
<Reaction
|
<Reaction
|
||||||
active={active}
|
active={active}
|
||||||
onClick={() =>
|
onClick={handleClick}
|
||||||
active ? message.unreact(id) : message.react(id)
|
onTouchStart={isTouchscreenDevice ? handleTouchStart : undefined}
|
||||||
}>
|
onTouchEnd={isTouchscreenDevice ? handleTouchEnd : undefined}
|
||||||
|
onTouchCancel={isTouchscreenDevice ? handleTouchEnd : undefined}>
|
||||||
<RenderEmoji match={id} /> {user_ids?.size || 0}
|
<RenderEmoji match={id} /> {user_ids?.size || 0}
|
||||||
</Reaction>
|
</Reaction>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Desktop: show tooltip on hover
|
||||||
|
// Mobile: long-press opens modal, handled by touch events
|
||||||
|
if (!isTouchscreenDevice && userIds.length > 0) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={tooltipContent} delay={300} interactive>
|
||||||
|
{reactionElement}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactionElement;
|
||||||
}),
|
}),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import CreateServer from "./components/CreateServer";
|
|||||||
import CustomStatus from "./components/CustomStatus";
|
import CustomStatus from "./components/CustomStatus";
|
||||||
import DeleteMessage from "./components/DeleteMessage";
|
import DeleteMessage from "./components/DeleteMessage";
|
||||||
import ReactMessage from "./components/ReactMessage";
|
import ReactMessage from "./components/ReactMessage";
|
||||||
|
import ReactionUsers from "./components/ReactionUsers";
|
||||||
import Error from "./components/Error";
|
import Error from "./components/Error";
|
||||||
import ImageViewer from "./components/ImageViewer";
|
import ImageViewer from "./components/ImageViewer";
|
||||||
import KickMember from "./components/KickMember";
|
import KickMember from "./components/KickMember";
|
||||||
@@ -277,6 +278,7 @@ export const modalController = new ModalControllerExtended({
|
|||||||
custom_status: CustomStatus,
|
custom_status: CustomStatus,
|
||||||
delete_message: DeleteMessage,
|
delete_message: DeleteMessage,
|
||||||
react_message: ReactMessage,
|
react_message: ReactMessage,
|
||||||
|
reaction_users: ReactionUsers,
|
||||||
error: Error,
|
error: Error,
|
||||||
image_viewer: ImageViewer,
|
image_viewer: ImageViewer,
|
||||||
kick_member: KickMember,
|
kick_member: KickMember,
|
||||||
|
|||||||
92
src/controllers/modals/components/ReactionUsers.tsx
Normal file
92
src/controllers/modals/components/ReactionUsers.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { Modal } from "@revoltchat/ui";
|
||||||
|
|
||||||
|
import UserIcon from "../../../components/common/user/UserIcon";
|
||||||
|
import { Username } from "../../../components/common/user/UserShort";
|
||||||
|
import { RenderEmoji } from "../../../components/markdown/plugins/emoji";
|
||||||
|
import { useClient } from "../../client/ClientController";
|
||||||
|
import { modalController } from "../ModalController";
|
||||||
|
import { ModalProps } from "../types";
|
||||||
|
|
||||||
|
const List = styled.div`
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow-y: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UserRow = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--secondary-background);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Header = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export default function ReactionUsers({
|
||||||
|
emoji,
|
||||||
|
userIds,
|
||||||
|
onClose,
|
||||||
|
...props
|
||||||
|
}: ModalProps<"reaction_users">) {
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const openProfile = (userId: string) => {
|
||||||
|
onClose();
|
||||||
|
modalController.push({ type: "user_profile", user_id: userId });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Modal must be nonDismissable, it conflicts with the message context menu otherwise.
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
{...props}
|
||||||
|
nonDismissable
|
||||||
|
title={
|
||||||
|
<Header>
|
||||||
|
<RenderEmoji match={emoji} />
|
||||||
|
<span>{userIds.length}</span>
|
||||||
|
</Header>
|
||||||
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
onClick: () => {
|
||||||
|
onClose();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
children: "Close",
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<List>
|
||||||
|
{userIds.map((userId) => {
|
||||||
|
const user = client.users.get(userId);
|
||||||
|
return (
|
||||||
|
<UserRow
|
||||||
|
key={userId}
|
||||||
|
onClick={() => openProfile(userId)}>
|
||||||
|
<UserIcon target={user} size={32} />
|
||||||
|
<Username user={user} />
|
||||||
|
</UserRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -157,6 +157,11 @@ export type Modal = {
|
|||||||
type: "react_message",
|
type: "react_message",
|
||||||
target: Message;
|
target: Message;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "reaction_users";
|
||||||
|
emoji: string;
|
||||||
|
userIds: string[];
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: "kick_member";
|
type: "kick_member";
|
||||||
member: Member;
|
member: Member;
|
||||||
|
|||||||
Reference in New Issue
Block a user