forked from abner/for-legacy-web
[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: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -16,6 +16,11 @@ import { IconButton } from "@revoltchat/ui";
|
||||
|
||||
import { emojiDictionary } from "../../../../assets/emojis";
|
||||
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 { 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
|
||||
*/
|
||||
@@ -98,16 +135,97 @@ export const Reactions = observer(({ message }: Props) => {
|
||||
const Entry = useCallback(
|
||||
observer(({ id, user_ids }: { id: string; user_ids?: Set<string> }) => {
|
||||
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);
|
||||
|
||||
return (
|
||||
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 (
|
||||
<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
|
||||
active={active}
|
||||
onClick={() =>
|
||||
active ? message.unreact(id) : message.react(id)
|
||||
}>
|
||||
onClick={handleClick}
|
||||
onTouchStart={isTouchscreenDevice ? handleTouchStart : undefined}
|
||||
onTouchEnd={isTouchscreenDevice ? handleTouchEnd : undefined}
|
||||
onTouchCancel={isTouchscreenDevice ? handleTouchEnd : undefined}>
|
||||
<RenderEmoji match={id} /> {user_ids?.size || 0}
|
||||
</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 DeleteMessage from "./components/DeleteMessage";
|
||||
import ReactMessage from "./components/ReactMessage";
|
||||
import ReactionUsers from "./components/ReactionUsers";
|
||||
import Error from "./components/Error";
|
||||
import ImageViewer from "./components/ImageViewer";
|
||||
import KickMember from "./components/KickMember";
|
||||
@@ -277,6 +278,7 @@ export const modalController = new ModalControllerExtended({
|
||||
custom_status: CustomStatus,
|
||||
delete_message: DeleteMessage,
|
||||
react_message: ReactMessage,
|
||||
reaction_users: ReactionUsers,
|
||||
error: Error,
|
||||
image_viewer: ImageViewer,
|
||||
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",
|
||||
target: Message;
|
||||
}
|
||||
| {
|
||||
type: "reaction_users";
|
||||
emoji: string;
|
||||
userIds: string[];
|
||||
}
|
||||
| {
|
||||
type: "kick_member";
|
||||
member: Member;
|
||||
|
||||
Reference in New Issue
Block a user