feat: more details for invite link

This commit is contained in:
Tristan Yang
2023-06-12 18:48:25 +08:00
parent b698138b67
commit e1a45dc710
13 changed files with 274 additions and 101 deletions
+60
View File
@@ -125,5 +125,65 @@ export const KEY_PWA_INSTALLED = "VOCECHAT_PWA_INSTALLED";
export const KEY_LOCAL_MAGIC_TOKEN = "VOCECHAT_LOCAL_MAGIC_TOKEN";
export const KEY_LOCAL_TRY_PATH = "VOCECHAT_TRY_PATH";
export const Emojis = ["👍", "❤️", "😄", "👀", "👎", "🎉", "🙁", "🚀"];
export const getInviteLinkExpireList = () => [
{
label: i18n.t("auth:invite_expire.min30"),
value: 30 * 60
},
{
label: i18n.t("auth:invite_expire.h1"),
value: 60 * 60
},
{
label: i18n.t("auth:invite_expire.h6"),
value: 6 * 60 * 60
},
{
label: i18n.t("auth:invite_expire.h12"),
value: 12 * 60 * 60
},
{
label: i18n.t("auth:invite_expire.d1"),
value: 24 * 60 * 60
},
{
label: i18n.t("auth:invite_expire.d7"),
value: 7 * 24 * 60 * 60
},
{
label: i18n.t("auth:invite_expire.d30"),
value: 30 * 24 * 60 * 60
}
];
export const getInviteLinkTimesList = () => [
{
label: i18n.t("auth:invite_times.no_limit"),
value: -1
},
{
label: i18n.t("auth:invite_times.time1"),
value: 1
},
{
label: i18n.t("auth:invite_times.times5"),
value: 5
},
{
label: i18n.t("auth:invite_times.times10"),
value: 10
},
{
label: i18n.t("auth:invite_times.times25"),
value: 25
},
{
label: i18n.t("auth:invite_times.times50"),
value: 50
},
{
label: i18n.t("auth:invite_times.times100"),
value: 100
}
];
export default BASE_URL;
+9 -8
View File
@@ -82,15 +82,13 @@ export const channelApi = createApi({
}
}
}),
createInviteLink: builder.query<string, number | void>({
query: (gid) => ({
createInviteLink: builder.query<string, { expire: number; times: number }>({
query: (data) => ({
headers: {
"content-type": "text/plain",
accept: "text/plain"
},
url: gid
? `/group/create_reg_magic_link?expired_in=3600&max_times=1&gid=${gid}`
: `/group/create_reg_magic_link?expired_in=3600&max_times=1`,
url: `/group/create_reg_magic_link?expired_in=${data.expire}&max_times=${data.times}`,
responseHandler: "text"
}),
transformResponse: (link: string) => {
@@ -117,14 +115,17 @@ export const channelApi = createApi({
}
}
}),
createPrivateInviteLink: builder.query<string, number | void>({
query: (gid) => ({
createPrivateInviteLink: builder.query<
string,
{ cid: number; expire: number; times: number | null }
>({
query: (data) => ({
headers: {
"content-type": "text/plain",
accept: "text/plain"
},
// 七天过期
url: `/group/create_invite_private_magic_link?expired_in=604800&max_times=1&gid=${gid}`,
url: `/group/create_invite_private_magic_link?expired_in=${data.expire}&max_times=${data.times}&gid=${data.cid}`,
responseHandler: "text"
}),
transformResponse: (link: string) => {
-18
View File
@@ -21,15 +21,12 @@ import {
TestEmailDTO
} from "@/types/server";
import { User } from "@/types/user";
import { transformInviteLink } from "@/utils";
import BASE_URL, { ContentTypes, IS_OFFICIAL_DEMO, PAYMENT_URL_PREFIX } from "../config";
import { updateInfo } from "../slices/server";
import { updateCallInfo, upsertVoiceList } from "../slices/voice";
import { RootState } from "../store";
import baseQuery from "./base.query";
const defaultExpireDuration = 2 * 24 * 60 * 60;
export const serverApi = createApi({
reducerPath: "serverApi",
baseQuery,
@@ -263,19 +260,6 @@ export const serverApi = createApi({
}
}
}),
createInviteLink: builder.query<string, number>({
query: (expired_in = defaultExpireDuration) => ({
headers: {
"content-type": "text/plain",
accept: "text/plain"
},
url: `/admin/system/create_invite_link?expired_in=${expired_in}`,
responseHandler: "text"
}),
transformResponse: (link: string) => {
return transformInviteLink(link);
}
}),
updateServer: builder.mutation<void, Server>({
query: (data) => ({
url: "admin/system/organization",
@@ -434,8 +418,6 @@ export const {
useLazyGetServerQuery,
useUpdateServerMutation,
useUpdateLogoMutation,
useCreateInviteLinkQuery,
useLazyCreateInviteLinkQuery,
useGetThirdPartySecretQuery,
useUpdateThirdPartySecretMutation,
useCreateAdminMutation,
+139 -32
View File
@@ -1,50 +1,157 @@
import { FC } from "react";
import { FC, useState } from "react";
import { useTranslation } from "react-i18next";
import { getInviteLinkExpireList, getInviteLinkTimesList } from "@/app/config";
import useInviteLink from "@/hooks/useInviteLink";
import IconQuestion from "@/assets/icons/question.svg";
import Modal from "./Modal";
import QRCode from "./QRCode";
import Button from "./styled/Button";
import Input from "./styled/Input";
import StyledModal from "./styled/Modal";
import Select from "./styled/Select";
type Props = {};
const InviteLink: FC<Props> = () => {
type Props = {
context?: "members" | "channel";
cid?: number;
};
const InviteLinkExpireList = getInviteLinkExpireList();
const InviteLinkTimesList = getInviteLinkTimesList();
const InviteLink: FC<Props> = ({ context = "members", cid }) => {
const [current, setCurrent] = useState({
expire: InviteLinkExpireList[4],
times: InviteLinkTimesList[0]
});
const [selectExpire, setSelectExpire] = useState(current.expire);
const [selectTimes, setSelectTimes] = useState(current.times);
const [editVisible, setEditVisible] = useState(false);
const { t } = useTranslation("chat");
const { generating, link, linkCopied, copyLink, generateNewLink } = useInviteLink();
const { generating, link, linkCopied, copyLink, generateNewLink } = useInviteLink(cid);
const handleNewLink = () => {
generateNewLink();
const { expire, times } = current;
generateNewLink({ expire: expire.value, times: times.value });
};
const toggleEditVisible = () => {
setEditVisible((prev) => !prev);
};
const handleUpdate = () => {
setCurrent({
expire: selectExpire,
times: selectTimes
});
toggleEditVisible();
handleNewLink();
};
// useEffect(() => {
// handleNewLink();
// }, [current]);
return (
<div className="flex flex-col items-start pb-8">
<p className="font-semibold text-sm mb-2 text-gray-500 dark:text-gray-50 flex flex-col md:flex-row gap-4">
{t("share_invite_link")}
<a
className="text-primary-500 flex gap-1 items-center"
href="http://doc.voce.chat/faq#fe_url"
target="_blank"
rel="noopener noreferrer"
>
<IconQuestion /> {t("invite_link_faq")}
</a>
</p>
<div className="w-full md:w-[512px] mb-3 relative">
<Input readOnly className={"large !pr-16"} placeholder="Generating" value={link} />
<Button
onClick={copyLink}
className="ghost small border_less absolute right-1 top-1/2 -translate-y-1/2"
>
{linkCopied ? "Copied" : t("action.copy", { ns: "common" })}
<>
<div className="flex flex-col items-start pb-8">
{context == "members" && (
<p className="font-semibold text-sm mb-2 text-gray-500 dark:text-gray-50 flex flex-col md:flex-row gap-4">
{t("share_invite_link")}
<a
className="text-primary-500 flex gap-1 items-center"
href="http://doc.voce.chat/faq#fe_url"
target="_blank"
rel="noopener noreferrer"
>
<IconQuestion /> {t("invite_link_faq")}
</a>
</p>
)}
<div className="w-full md:w-[512px] mb-3 relative">
<Input readOnly className={"large !pr-16"} placeholder="Generating" value={link} />
<Button
onClick={copyLink}
className="ghost small border_less absolute right-1 top-1/2 -translate-y-1/2"
>
{linkCopied ? "Copied" : t("action.copy", { ns: "common" })}
</Button>
</div>
<span className="text-xs text-gray-600 dark:text-gray-100 flex gap-2">
{t("invite_link_setting_tip", {
expire: current.expire.label,
times: current.times.label
})}
<button className="text-primary-500 flex gap-1 items-center" onClick={toggleEditVisible}>
{t("invite_link_edit")}
</button>
</span>
<div className="w-44 h-44 my-2">
<QRCode size={1200} link={link} />
</div>
<Button className="ghost" disabled={generating} onClick={handleNewLink}>
{generating ? `Generating` : t("generate_new_link")}
</Button>
</div>
<span className="text-xs text-gray-600 dark:text-gray-100">{t("invite_link_expire")}</span>
<div className="w-44 h-44 my-2">
<QRCode size={1200} link={link} />
</div>
<Button className="ghost" disabled={generating} onClick={handleNewLink}>
{generating ? `Generating` : t("generate_new_link")}
</Button>
</div>
{editVisible && (
<Modal id="modal-modal">
<StyledModal
compact
title={"Update Invite Link Settings"}
buttons={
<>
<Button onClick={toggleEditVisible} className="cancel small">
{t("action.cancel", { ns: "common" })}
</Button>
<Button className="main small" onClick={handleUpdate}>
Update
</Button>
</>
}
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2 items-start">
<span className="text-sm dark:text-gray-100">Expire After:</span>
<Select
options={InviteLinkExpireList.map((item) => {
const { label, value } = item;
return {
title: label,
value: `${value}`,
selected: selectExpire.value === value
};
})}
current={{
title: selectExpire.label
}}
updateSelect={(option) => {
setSelectExpire(
InviteLinkExpireList.find((item) => item.value === Number(option.value))!
);
}}
></Select>
</div>
<div className="flex flex-col gap-2 items-start">
<span className="text-sm dark:text-gray-100">Max Times of Uses:</span>
<Select
options={InviteLinkTimesList.map((item) => {
const { label, value } = item;
return {
title: label,
value: `${value}`,
selected: selectTimes.value === value
};
})}
current={{
title: selectTimes.label
}}
updateSelect={(option) => {
setSelectTimes(
InviteLinkTimesList.find((item) => item.value === Number(option.value))!
);
}}
></Select>
</div>
</div>
</StyledModal>
</Modal>
)}
</>
);
};
+3 -28
View File
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { useSendLoginMagicLinkMutation } from "@/app/services/auth";
import useInviteLink from "@/hooks/useInviteLink";
import QRCode from "../QRCode";
import InviteLink from "../InviteLink";
import Button from "../styled/Button";
import Input from "../styled/Input";
@@ -18,13 +18,7 @@ const InviteByEmail: FC<Props> = ({ cid }) => {
const [email, setEmail] = useState("");
const formRef = useRef<HTMLFormElement | null>(null);
const [sendMagicLinkByEmail, { isSuccess, isLoading }] = useSendLoginMagicLinkMutation();
const { enableSMTP, linkCopied, link, copyLink, generateNewLink, generating } =
useInviteLink(cid);
useEffect(() => {
if (linkCopied) {
toast.success("Invite Link Copied!");
}
}, [linkCopied]);
const { enableSMTP } = useInviteLink(cid);
useEffect(() => {
if (isSuccess) {
toast.success("Email Sent!");
@@ -77,26 +71,7 @@ const InviteByEmail: FC<Props> = ({ cid }) => {
<label className="text-sm text-gray-400 dark:text-gray-100" htmlFor="">
{t("send_invite_link")}
</label>
<div className="relative flex items-center gap-2">
<Input readOnly className="!pr-[50px]" placeholder="Generating" value={link} />
<button
className="absolute right-1 top-1/2 -translate-y-1/2 pr-2 text-sm text-primary-400 hover:text-primary-600"
onClick={copyLink}
>
{ct("action.copy")}
</button>
</div>
</div>
<div className="w-44 h-44 my-2">{!generating && <QRCode size={1200} link={link} />}</div>
<div className="text-xs text-gray-600 dark:text-gray-200">
{t("invite_link_expire")}
<button
disabled={generating}
className="text-primary-400 ml-1"
onClick={() => generateNewLink()}
>
{t("generate_new_link")}
</button>
<InviteLink context="channel" cid={cid} />
</div>
</div>
);
+3 -2
View File
@@ -36,6 +36,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
return (
<Tippy
onClickOutside={setOptionsVisible.bind(null, false)}
visible={optionsVisible}
appendTo={document.body}
placement="bottom"
@@ -59,10 +60,10 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
}
>
<div
className="select-none border border-solid border-slate-200 p-2 flex items-center gap-2"
className="cursor-pointer select-none border border-slate-200 dark:border-slate-800 p-2 flex items-center gap-2"
onClick={toggleVisible}
>
<span className="text-sm text-gray-500 dark:text-gray-200 min-w-[76px]">
<span className="text-sm text-gray-500 dark:text-gray-200 min-w-[120px]">
{(current !== null ? current : curr)?.title || t("action.select")}
</span>
<IconArrow className="!w-5 !h-5" />
+18 -8
View File
@@ -1,19 +1,24 @@
import { useEffect, useState } from "react";
import { getInviteLinkExpireList, getInviteLinkTimesList } from "@/app/config";
import {
useLazyCreateInviteLinkQuery as useCreateChannelInviteLinkQuery,
useLazyCreateInviteLinkQuery,
useLazyCreatePrivateInviteLinkQuery
} from "@/app/services/channel";
import { useGetSMTPStatusQuery } from "@/app/services/server";
import { useAppSelector } from "@/app/store";
import useCopy from "./useCopy";
const defaultExpire = getInviteLinkExpireList()[4].value;
const defaultTimes = getInviteLinkTimesList()[0].value;
type ParamsProps = { expire: number; times: number };
const defaultParams: ParamsProps = { expire: defaultExpire, times: defaultTimes };
export default function useInviteLink(cid?: number) {
const [finalLink, setFinalLink] = useState("");
const channel = useAppSelector((store) => store.channels.byId[cid ?? 0]);
const { data: SMTPEnabled, isSuccess: smtpStatusFetchSuccess } = useGetSMTPStatusQuery();
const [generateChannelInviteLink, { data: channelInviteLink, isLoading: generatingChannelLink }] =
useCreateChannelInviteLinkQuery();
const [generateInviteLink, { data: channelInviteLink, isLoading: generatingChannelLink }] =
useLazyCreateInviteLinkQuery();
const [generatePrivateInviteLink, { data: privateInviteLink, isLoading: generatingPrivateLink }] =
useLazyCreatePrivateInviteLinkQuery();
@@ -23,9 +28,9 @@ export default function useInviteLink(cid?: number) {
};
useEffect(() => {
if (!cid || channel?.is_public) {
generateChannelInviteLink(cid);
generateInviteLink({ expire: defaultExpire, times: defaultTimes });
} else {
generatePrivateInviteLink(cid);
generatePrivateInviteLink({ cid, expire: defaultExpire, times: defaultTimes });
}
}, [cid, channel]);
@@ -35,14 +40,19 @@ export default function useInviteLink(cid?: number) {
setFinalLink(_link);
}
}, [channelInviteLink, privateInviteLink, smtpStatusFetchSuccess]);
const genServerLink = () => {
generateChannelInviteLink();
const generateNewLink = (params = defaultParams) => {
const { expire, times } = params;
if (!cid || channel?.is_public) {
generateInviteLink({ expire, times });
} else {
generatePrivateInviteLink({ cid, expire, times });
}
};
const generating = generatingPrivateLink || generatingChannelLink;
return {
enableSMTP: SMTPEnabled,
generating,
generateNewLink: cid ? generateChannelInviteLink.bind(null, cid) : genServerLink,
generateNewLink,
link: finalLink,
linkCopied: copied,
copyLink