feat: context menu in channel member list

This commit is contained in:
zerosoul
2022-05-27 17:50:11 +08:00
parent e10b7f741e
commit b4a29c1571
6 changed files with 295 additions and 167 deletions
-106
View File
@@ -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>
</>
);
}
+69
View File
@@ -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>
);
}
+58
View File
@@ -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;
+26 -61
View File
@@ -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>
)}