feat: invit link modal from channel
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 5C9 5.52592 9.203 6.00443 9.53491 6.36145C10.1452 6.12793 10.8076 6 11.5 6C11.8966 6 12.2833 6.04197 12.6561 6.12171C12.8732 5.80182 13 5.41572 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5ZM6.06019 13.0008C6.10955 13.0008 6.15849 13.0005 6.20699 12.9998C6.07215 12.5231 6 12.02 6 11.5C6 10.1704 6.47182 8.95094 7.25716 8H3.5C2.67157 8 2 8.67157 2 9.5V10.2746C2 12.0969 3.44552 13.0008 6.06019 13.0008ZM6 2C7.38094 2 8.50041 3.11947 8.50041 4.5004C8.50041 5.88134 7.38094 7.00081 6 7.00081C4.61906 7.00081 3.49959 5.88134 3.49959 4.5004C3.49959 3.11947 4.61906 2 6 2ZM16 11.5C16 13.9853 13.9853 16 11.5 16C9.01472 16 7 13.9853 7 11.5C7 9.01472 9.01472 7 11.5 7C13.9853 7 16 9.01472 16 11.5ZM12 9.5C12 9.22386 11.7761 9 11.5 9C11.2239 9 11 9.22386 11 9.5V11H9.5C9.22386 11 9 11.2239 9 11.5C9 11.7761 9.22386 12 9.5 12H11V13.5C11 13.7761 11.2239 14 11.5 14C11.7761 14 12 13.7761 12 13.5V12H13.5C13.7761 12 14 11.7761 14 11.5C14 11.2239 13.7761 11 13.5 11H12V9.5Z" fill="#344054"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,12 +1,7 @@
|
||||
import { useEffect } from "react";
|
||||
// import { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useSelector } from "react-redux";
|
||||
import {
|
||||
useLazyCreateInviteLinkQuery,
|
||||
useGetSMTPConfigQuery,
|
||||
} from "../../app/services/server";
|
||||
import useInviteLink from "../hook/useInviteLink";
|
||||
import Button from "./styled/Button";
|
||||
import useCopy from "../hook/useCopy";
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -51,51 +46,31 @@ const StyledWrapper = styled.div`
|
||||
`;
|
||||
export default function InviteLink() {
|
||||
const {
|
||||
data: config,
|
||||
isSuccess: configQuerySuccess,
|
||||
} = useGetSMTPConfigQuery();
|
||||
const [copid, copy] = useCopy();
|
||||
const {
|
||||
inviteLink: { link },
|
||||
loginUser,
|
||||
} = useSelector((store) => {
|
||||
return {
|
||||
inviteLink: store.server.inviteLink,
|
||||
loginUser: store.contacts.byId[store.authData.uid],
|
||||
};
|
||||
});
|
||||
const [createLink, { isLoading }] = useLazyCreateInviteLinkQuery();
|
||||
useEffect(() => {
|
||||
if (!link && loginUser && loginUser.is_admin) {
|
||||
createLink();
|
||||
}
|
||||
}, [link, loginUser]);
|
||||
generating,
|
||||
link,
|
||||
linkCopied,
|
||||
copyLink,
|
||||
generateNewLink,
|
||||
} = useInviteLink();
|
||||
const handleNewLink = () => {
|
||||
createLink();
|
||||
generateNewLink();
|
||||
};
|
||||
if (!loginUser || !loginUser.is_admin || !configQuerySuccess) return null;
|
||||
let finalLink = null;
|
||||
if (link) {
|
||||
const tmpURL = new URL(link);
|
||||
tmpURL.searchParams.set("code", config.enabled);
|
||||
finalLink = tmpURL.href;
|
||||
}
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="tip">
|
||||
Share this link to invite people to this server.
|
||||
</span>
|
||||
<div className="link">
|
||||
<span title={finalLink} className="content">
|
||||
{finalLink}
|
||||
<span title={link} className="content">
|
||||
{link}
|
||||
</span>
|
||||
<Button onClick={copy.bind(null, finalLink)} className="main">
|
||||
{copid ? "Copied" : `Copy`}
|
||||
<Button onClick={copyLink} className="main">
|
||||
{linkCopied ? "Copied" : `Copy`}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="sub_tip">Invite link expires in 7 days.</span>
|
||||
<Button className="ghost" disabled={isLoading} onClick={handleNewLink}>
|
||||
{isLoading ? `Generating` : `Generate New Link`}
|
||||
<Button className="ghost" disabled={generating} onClick={handleNewLink}>
|
||||
{generating ? `Generating` : `Generate New Link`}
|
||||
</Button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// import React from 'react'
|
||||
import styled from "styled-components";
|
||||
import useInviteLink from "../hook/useInviteLink";
|
||||
import CloseIcon from "../../assets/icons/close.svg";
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
box-shadow: 0px 25px 50px rgba(31, 41, 55, 0.25);
|
||||
border-radius: var(--br);
|
||||
padding: 0;
|
||||
min-width: 408px;
|
||||
> .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
color: #374151;
|
||||
padding: 16px;
|
||||
.close {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 16px;
|
||||
box-shadow: 0px -1px 2px rgba(0, 0, 0, 0.06);
|
||||
.input {
|
||||
position: relative;
|
||||
button {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
input {
|
||||
padding-right: 80px;
|
||||
}
|
||||
}
|
||||
> .invite {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
/* position: relative; */
|
||||
}
|
||||
> .link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
/* position: relative; */
|
||||
}
|
||||
label {
|
||||
color: #6b7280;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
> .tip {
|
||||
color: #344054;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #22ccee;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
import Modal from "./Modal";
|
||||
import Button from "./styled/Button";
|
||||
import Input from "./styled/Input";
|
||||
export default function InviteModal({ title, closeModal }) {
|
||||
const {
|
||||
enableSMTP,
|
||||
linkCopied,
|
||||
link,
|
||||
copyLink,
|
||||
generateNewLink,
|
||||
generating,
|
||||
} = useInviteLink();
|
||||
return (
|
||||
<Modal>
|
||||
<Styled>
|
||||
<h2 className="title">
|
||||
Invite friends to #{title}{" "}
|
||||
<CloseIcon className="close" onClick={closeModal} />
|
||||
</h2>
|
||||
<div className="body">
|
||||
<div className="invite">
|
||||
<label htmlFor="">Invite by Email</label>
|
||||
<div className="input">
|
||||
<Input
|
||||
disabled={!enableSMTP}
|
||||
type="email"
|
||||
className="higher"
|
||||
placeholder={enableSMTP ? "Enter Email" : "Enable SMTP First"}
|
||||
/>
|
||||
<Button disabled={!enableSMTP} className="ghost small">
|
||||
Invite
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="link">
|
||||
<label htmlFor="">Or Send invite link to your friends</label>
|
||||
<div className="input">
|
||||
<Input className="higher" placeholder="Generating" value={link} />
|
||||
<Button className="small" onClick={copyLink}>
|
||||
{linkCopied ? `Copied` : `Copy`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tip">
|
||||
Invite link expires in 7 days.{" "}
|
||||
<button
|
||||
disabled={generating}
|
||||
className="new"
|
||||
onClick={generateNewLink}
|
||||
>
|
||||
Generate New Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Styled>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import useCopy from "./useCopy";
|
||||
import {
|
||||
useCreateInviteLinkQuery,
|
||||
useGetSMTPConfigQuery,
|
||||
} from "../../app/services/server";
|
||||
export default function useInviteLink() {
|
||||
const [finalLink, setFinalLink] = useState("");
|
||||
const {
|
||||
data: config,
|
||||
|
||||
isSuccess: configQuerySuccess,
|
||||
} = useGetSMTPConfigQuery();
|
||||
const { data: link, isLoading, refetch } = useCreateInviteLinkQuery();
|
||||
const [linkCopied, copy] = useCopy();
|
||||
const copyLink = () => {
|
||||
copy(finalLink);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (link && config) {
|
||||
const tmpURL = new URL(link);
|
||||
tmpURL.searchParams.set("code", config.enabled);
|
||||
setFinalLink(tmpURL.href);
|
||||
}
|
||||
}, [link, config]);
|
||||
|
||||
return {
|
||||
enableSMTP: config?.enabled,
|
||||
generating: isLoading,
|
||||
generateNewLink: refetch,
|
||||
link: finalLink,
|
||||
linkCopied,
|
||||
copyLink,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useDrop } from "react-dnd";
|
||||
import { NativeTypes } from "react-dnd-html5-backend";
|
||||
@@ -5,6 +6,7 @@ import { useSelector } from "react-redux";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import useContextMenu from "../../../common/hook/useContextMenu";
|
||||
import ContextMenu from "../../../common/component/ContextMenu";
|
||||
import InviteModal from "../../../common/component/InviteModal";
|
||||
import Tooltip from "../../../common/component/Tooltip";
|
||||
// import { useDebounce} from "rooks";
|
||||
import { useReadMessageMutation } from "../../../app/services/message";
|
||||
@@ -15,6 +17,7 @@ import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import { getUnreadCount } from "../utils";
|
||||
|
||||
const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [muteChannel] = useUpdateMuteSettingMutation();
|
||||
const [updateReadIndex] = useReadMessageMutation();
|
||||
@@ -75,6 +78,11 @@ const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
updateReadIndex(param);
|
||||
}
|
||||
};
|
||||
const toggleInviteModalVisible = (evt) => {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
setInviteModalVisible((prev) => !prev);
|
||||
};
|
||||
const handleMute = () => {
|
||||
const data = muted
|
||||
? { remove_groups: [id] }
|
||||
@@ -82,77 +90,97 @@ const NavItem = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
muteChannel(data);
|
||||
};
|
||||
const { is_public, name } = channel;
|
||||
const unreads = getUnreadCount({ mids, messageData, readIndex, loginUid });
|
||||
const isDot = muted || unreads > 99;
|
||||
const { unreads, mentions = [] } = getUnreadCount({
|
||||
mids,
|
||||
messageData,
|
||||
readIndex,
|
||||
loginUid,
|
||||
});
|
||||
const isMentions = mentions.length !== 0;
|
||||
return (
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
offset={[offset.y, offset.x]}
|
||||
visible={contextMenuVisible}
|
||||
onClickOutside={hideContextMenu}
|
||||
key={id}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
underline: true,
|
||||
handler: handleReadAll,
|
||||
},
|
||||
{
|
||||
title: muted ? "Unmute" : "Mute",
|
||||
handler: handleMute,
|
||||
},
|
||||
{
|
||||
title: "Notification Settings",
|
||||
underline: true,
|
||||
},
|
||||
is_public
|
||||
? null
|
||||
: {
|
||||
title: "Invite People",
|
||||
},
|
||||
{
|
||||
title: "Delete Channel",
|
||||
danger: true,
|
||||
handler: toggleRemoveConfirm.bind(null, id),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StyledLink
|
||||
data-cid={id}
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
ref={drop}
|
||||
<>
|
||||
<Tippy
|
||||
interactive
|
||||
placement="right-start"
|
||||
offset={[offset.y, offset.x]}
|
||||
visible={contextMenuVisible}
|
||||
onClickOutside={hideContextMenu}
|
||||
key={id}
|
||||
className={`link ${isActive ? "drop_over" : ""} ${
|
||||
muted ? "muted" : ""
|
||||
}`}
|
||||
to={`/chat/channel/${id}`}
|
||||
content={
|
||||
<ContextMenu
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
underline: true,
|
||||
handler: handleReadAll,
|
||||
},
|
||||
{
|
||||
title: muted ? "Unmute" : "Mute",
|
||||
handler: handleMute,
|
||||
},
|
||||
{
|
||||
title: "Notification Settings",
|
||||
underline: true,
|
||||
},
|
||||
is_public
|
||||
? null
|
||||
: {
|
||||
title: "Invite People",
|
||||
},
|
||||
{
|
||||
title: "Delete Channel",
|
||||
danger: true,
|
||||
handler: toggleRemoveConfirm.bind(null, id),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className={`name`} title={name}>
|
||||
<ChannelIcon personal={!is_public} muted={muted} />
|
||||
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
||||
</div>
|
||||
<div className="icons">
|
||||
<Tooltip placement="bottom" tip="Channel Setting">
|
||||
<i
|
||||
className="setting"
|
||||
data-id={id}
|
||||
onClick={handleChannelSetting}
|
||||
></i>
|
||||
</Tooltip>
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${isDot ? "dot" : ""}`}>
|
||||
{isDot ? null : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</StyledLink>
|
||||
</Tippy>
|
||||
<StyledLink
|
||||
data-cid={id}
|
||||
onContextMenu={handleContextMenuEvent}
|
||||
ref={drop}
|
||||
key={id}
|
||||
className={`link ${isActive ? "drop_over" : ""} ${
|
||||
muted ? "muted" : ""
|
||||
}`}
|
||||
to={`/chat/channel/${id}`}
|
||||
>
|
||||
<div className={`name`} title={name}>
|
||||
<ChannelIcon personal={!is_public} muted={muted} />
|
||||
<span className={`txt ${unreads == 0 ? "read" : ""}`}>{name}</span>
|
||||
</div>
|
||||
<div className="icons">
|
||||
<Tooltip placement="bottom" tip="Add Member">
|
||||
<i
|
||||
className="icon invite"
|
||||
data-id={id}
|
||||
onClick={toggleInviteModalVisible}
|
||||
></i>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom" tip="Channel Setting">
|
||||
<i
|
||||
className="icon setting"
|
||||
data-id={id}
|
||||
onClick={handleChannelSetting}
|
||||
></i>
|
||||
</Tooltip>
|
||||
{unreads > 0 && (
|
||||
<i className={`badge ${isMentions ? "mention" : ""}`}>
|
||||
{isMentions ? mentions.length : unreads}
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
</StyledLink>
|
||||
</Tippy>
|
||||
{inviteModalVisible && (
|
||||
<InviteModal
|
||||
title={channel.name}
|
||||
closeModal={toggleInviteModalVisible}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import settingIcon from "../../../assets/icons/setting.svg?url";
|
||||
import inviteIcon from "../../../assets/icons/invite.from.channel.svg?url";
|
||||
const Styled = styled(NavLink)`
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -21,7 +22,7 @@ const Styled = styled(NavLink)`
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
width: 160px;
|
||||
width: 140px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -33,14 +34,20 @@ const Styled = styled(NavLink)`
|
||||
> .icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
> .setting {
|
||||
gap: 8px;
|
||||
> .icon {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-image: url(${settingIcon});
|
||||
background-size: 16px;
|
||||
background-size: contain;
|
||||
|
||||
&.setting {
|
||||
background-image: url(${settingIcon});
|
||||
}
|
||||
&.invite {
|
||||
background-image: url(${inviteIcon});
|
||||
}
|
||||
}
|
||||
> .badge {
|
||||
color: #fff;
|
||||
@@ -54,6 +61,9 @@ const Styled = styled(NavLink)`
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
line-height: 10px;
|
||||
&.mention {
|
||||
background: #f97066;
|
||||
}
|
||||
&.dot {
|
||||
min-width: unset;
|
||||
width: 6px;
|
||||
@@ -70,11 +80,11 @@ const Styled = styled(NavLink)`
|
||||
background: #bfbfbf;
|
||||
}
|
||||
}
|
||||
&:hover > .icons > {
|
||||
.badge {
|
||||
&:hover > .icons {
|
||||
> .badge {
|
||||
display: none;
|
||||
}
|
||||
.setting {
|
||||
> .icon {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user