feat: context menu in channel member list
This commit is contained in:
@@ -1,106 +0,0 @@
|
||||
// import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import IconOwner from "../../assets/icons/owner.svg";
|
||||
import Avatar from "./Avatar";
|
||||
import Profile from "./Profile";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
&.interactive {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
width: ${({ size }) => `${size}px`};
|
||||
height: ${({ size }) => `${size}px`};
|
||||
position: relative;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -4px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
&.online {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
&.offline {
|
||||
background-color: #a1a1aa;
|
||||
}
|
||||
&.alert {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
.name {
|
||||
/* user-select: text; */
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #52525b;
|
||||
}
|
||||
`;
|
||||
export default function Contact({
|
||||
cid = null,
|
||||
owner = false,
|
||||
dm = false,
|
||||
interactive = true,
|
||||
uid = "",
|
||||
popover = false,
|
||||
compact = false,
|
||||
avatarSize = 32,
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const curr = useSelector((store) => store.contacts.byId[uid]);
|
||||
const handleDoubleClick = () => {
|
||||
navigate(`/chat/dm/${uid}`);
|
||||
};
|
||||
if (!curr) return null;
|
||||
return (
|
||||
<Tippy
|
||||
inertia={true}
|
||||
interactive
|
||||
disabled={!popover}
|
||||
placement="left"
|
||||
trigger="click"
|
||||
content={<Profile uid={uid} type="card" cid={cid} />}
|
||||
>
|
||||
<StyledWrapper
|
||||
size={avatarSize}
|
||||
onDoubleClick={dm ? handleDoubleClick : null}
|
||||
className={`${interactive ? "interactive" : ""} ${
|
||||
compact ? "compact" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="avatar">
|
||||
<Avatar url={curr.avatar} name={curr.name} alt="avatar" />
|
||||
<div className={`status ${curr.online ? "online" : "offline"}`}></div>
|
||||
</div>
|
||||
{!compact && <span className="name">{curr?.name}</span>}
|
||||
{owner && <IconOwner />}
|
||||
</StyledWrapper>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// import { useState } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
// import { useDispatch } from "react-redux";
|
||||
import useContactOperation from "../../hook/useContactOperation";
|
||||
import ContextMenu from "../ContextMenu";
|
||||
|
||||
export default function ContactContextMenu({
|
||||
enable = false,
|
||||
uid,
|
||||
cid,
|
||||
visible,
|
||||
hide,
|
||||
children,
|
||||
}) {
|
||||
const {
|
||||
canCall,
|
||||
call,
|
||||
copyEmail,
|
||||
canCopyEmail,
|
||||
startChat,
|
||||
canRemoveFromChannel,
|
||||
removeFromChannel,
|
||||
} = useContactOperation({
|
||||
uid,
|
||||
cid,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<Tippy
|
||||
disabled={!enable}
|
||||
visible={visible}
|
||||
followCursor={"initial"}
|
||||
interactive
|
||||
placement="right-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
onClickOutside={hide}
|
||||
key={uid}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hide}
|
||||
items={[
|
||||
{
|
||||
title: "Message",
|
||||
handler: startChat,
|
||||
},
|
||||
canCall && {
|
||||
title: "Call",
|
||||
handler: call,
|
||||
},
|
||||
canCopyEmail && {
|
||||
title: "Copy Email",
|
||||
handler: copyEmail,
|
||||
},
|
||||
canRemoveFromChannel && {
|
||||
danger: true,
|
||||
title: "Remove From Channel",
|
||||
handler: removeFromChannel,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Tippy>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import IconOwner from "../../../assets/icons/owner.svg";
|
||||
import Avatar from "../Avatar";
|
||||
import Profile from "../Profile";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import StyledWrapper from "./styled";
|
||||
import useContextMenu from "../../hook/useContextMenu";
|
||||
export default function Contact({
|
||||
cid = null,
|
||||
owner = false,
|
||||
dm = false,
|
||||
interactive = true,
|
||||
uid = "",
|
||||
popover = false,
|
||||
compact = false,
|
||||
avatarSize = 32,
|
||||
enableContextMenu = false,
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
visible: contextMenuVisible,
|
||||
handleContextMenuEvent,
|
||||
hideContextMenu,
|
||||
} = useContextMenu();
|
||||
const curr = useSelector((store) => store.contacts.byId[uid]);
|
||||
const handleDoubleClick = () => {
|
||||
navigate(`/chat/dm/${uid}`);
|
||||
};
|
||||
if (!curr) return null;
|
||||
return (
|
||||
<ContextMenu
|
||||
cid={cid}
|
||||
uid={uid}
|
||||
enable={enableContextMenu}
|
||||
visible={contextMenuVisible}
|
||||
hide={hideContextMenu}
|
||||
>
|
||||
<Tippy
|
||||
inertia={true}
|
||||
interactive
|
||||
disabled={!popover}
|
||||
placement="left"
|
||||
trigger="click"
|
||||
content={<Profile uid={uid} type="card" cid={cid} />}
|
||||
>
|
||||
<StyledWrapper
|
||||
onContextMenu={enableContextMenu ? handleContextMenuEvent : null}
|
||||
size={avatarSize}
|
||||
onDoubleClick={dm ? handleDoubleClick : null}
|
||||
className={`${interactive ? "interactive" : ""} ${
|
||||
compact ? "compact" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="avatar">
|
||||
<Avatar url={curr.avatar} name={curr.name} alt="avatar" />
|
||||
<div
|
||||
className={`status ${curr.online ? "online" : "offline"}`}
|
||||
></div>
|
||||
</div>
|
||||
{!compact && <span className="name">{curr?.name}</span>}
|
||||
{owner && <IconOwner />}
|
||||
</StyledWrapper>
|
||||
</Tippy>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
&.compact {
|
||||
padding: 0;
|
||||
}
|
||||
&.interactive {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
}
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
width: ${({ size }) => `${size}px`};
|
||||
height: ${({ size }) => `${size}px`};
|
||||
position: relative;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status {
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
right: -4px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
box-sizing: content-box;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
&.online {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
&.offline {
|
||||
background-color: #a1a1aa;
|
||||
}
|
||||
&.alert {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
}
|
||||
}
|
||||
.name {
|
||||
/* user-select: text; */
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #52525b;
|
||||
}
|
||||
`;
|
||||
export default StyledWrapper;
|
||||
@@ -1,46 +1,34 @@
|
||||
import { useEffect } from "react";
|
||||
// import { useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { NavLink, useNavigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
import { NavLink } from "react-router-dom";
|
||||
// import toast from "react-hot-toast";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { hideAll } from "tippy.js";
|
||||
// import { hideAll } from "tippy.js";
|
||||
import IconMessage from "../../../assets/icons/message.svg";
|
||||
import IconCall from "../../../assets/icons/call.svg";
|
||||
import IconMore from "../../../assets/icons/more.svg";
|
||||
import Avatar from "../Avatar";
|
||||
import StyledWrapper from "./styled";
|
||||
import StyledMenu from "../styled/Menu";
|
||||
import useCopy from "../../hook/useCopy";
|
||||
import { useRemoveMembersMutation } from "../../../app/services/channel";
|
||||
import { useLazyDeleteContactQuery } from "../../../app/services/contact";
|
||||
import useContactOperation from "../../hook/useContactOperation";
|
||||
|
||||
export default function Profile({ uid = null, type = "embed", cid = null }) {
|
||||
const navigateTo = useNavigate();
|
||||
const [
|
||||
removeUser,
|
||||
{ isSuccess: removeUserSuccess },
|
||||
] = useLazyDeleteContactQuery();
|
||||
const [
|
||||
const {
|
||||
canCall,
|
||||
call,
|
||||
canCopyEmail,
|
||||
copyEmail,
|
||||
removeFromChannel,
|
||||
{ isSuccess: removeSuccess },
|
||||
] = useRemoveMembersMutation();
|
||||
const { copy } = useCopy();
|
||||
const { data, channel, loginUid, isAdmin } = useSelector((store) => {
|
||||
canRemoveFromChannel,
|
||||
canRemove,
|
||||
removeUser,
|
||||
} = useContactOperation({ uid, cid });
|
||||
|
||||
const { data } = useSelector((store) => {
|
||||
return {
|
||||
data: store.contacts.byId[uid],
|
||||
channel: store.channels.byId[cid],
|
||||
loginUid: store.authData.uid,
|
||||
isAdmin: store.contacts.byId[store.authData.uid]?.is_admin,
|
||||
};
|
||||
});
|
||||
useEffect(() => {
|
||||
if (removeSuccess || removeUserSuccess) {
|
||||
toast.success("Remove Successfully");
|
||||
if (removeUserSuccess) {
|
||||
navigateTo(`/contacts`);
|
||||
}
|
||||
}
|
||||
}, [removeSuccess, removeUserSuccess]);
|
||||
|
||||
if (!data) return null;
|
||||
// console.log("profile", data);
|
||||
@@ -50,27 +38,10 @@ export default function Profile({ uid = null, type = "embed", cid = null }) {
|
||||
avatar,
|
||||
// introduction = "This guy has nothing to introduce",
|
||||
} = data;
|
||||
const handleClick = () => {
|
||||
toast.success("cooming soon...");
|
||||
};
|
||||
const handlCopyEmail = () => {
|
||||
copy(email);
|
||||
hideAll();
|
||||
};
|
||||
const handleRemove = ({ from = "channel", uid }) => {
|
||||
const remove = from == "channel" ? removeFromChannel : removeUser;
|
||||
remove(from == "channel" ? { id: +cid, members: [+uid] } : uid);
|
||||
hideAll();
|
||||
};
|
||||
const canCall = type == "card" && loginUid != uid;
|
||||
const canRemoveFromServer = type == "embed" && isAdmin;
|
||||
const canRemoveFromChannel =
|
||||
cid &&
|
||||
!channel?.is_public &&
|
||||
(isAdmin || channel?.owner == loginUid) &&
|
||||
channel?.owner != uid;
|
||||
const enableCall = type == "card" && canCall;
|
||||
const canRemoveFromServer = type == "embed" && canRemove;
|
||||
const hasMore =
|
||||
canCall || email || canRemoveFromChannel || canRemoveFromServer;
|
||||
enableCall || email || canRemoveFromChannel || canRemoveFromServer;
|
||||
return (
|
||||
<StyledWrapper className={type}>
|
||||
<Avatar className="avatar" url={avatar} name={name} />
|
||||
@@ -86,7 +57,7 @@ export default function Profile({ uid = null, type = "embed", cid = null }) {
|
||||
</NavLink>
|
||||
{/* <NavLink to={`#`}> */}
|
||||
{type == "embed" && (
|
||||
<li className="icon call" onClick={handleClick}>
|
||||
<li className="icon call" onClick={call}>
|
||||
<IconCall />
|
||||
<span className="txt">Call</span>
|
||||
</li>
|
||||
@@ -101,30 +72,24 @@ export default function Profile({ uid = null, type = "embed", cid = null }) {
|
||||
hideOnClick={true}
|
||||
content={
|
||||
<StyledMenu>
|
||||
{canCall && (
|
||||
<li className="item">
|
||||
{enableCall && (
|
||||
<li className="item" onClick={call}>
|
||||
{/* <IconCall className="icon" /> */}
|
||||
Call
|
||||
</li>
|
||||
)}
|
||||
{email && (
|
||||
<li className="item" onClick={handlCopyEmail}>
|
||||
{canCopyEmail && (
|
||||
<li className="item" onClick={copyEmail}>
|
||||
Copy Email
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromChannel && (
|
||||
<li
|
||||
className="item danger"
|
||||
onClick={handleRemove.bind(null, { uid })}
|
||||
>
|
||||
<li className="item danger" onClick={removeFromChannel}>
|
||||
Remove from Channel
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromServer && (
|
||||
<li
|
||||
className="item danger"
|
||||
onClick={handleRemove.bind(null, { from: "server", uid })}
|
||||
>
|
||||
<li className="item danger" onClick={removeUser}>
|
||||
Remove from Server
|
||||
</li>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user