From e1a45dc71028a81c52d3ea77269d1eb6f020fbe1 Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Mon, 12 Jun 2023 18:48:25 +0800 Subject: [PATCH] feat: more details for invite link --- public/locales/en/auth.json | 18 ++ public/locales/en/chat.json | 3 +- public/locales/jp/chat.json | 2 +- public/locales/tr/chat.json | 2 +- public/locales/zh/auth.json | 18 ++ public/locales/zh/chat.json | 4 +- src/app/config.ts | 60 +++++++ src/app/services/channel.ts | 17 +- src/app/services/server.ts | 18 -- src/components/InviteLink.tsx | 171 +++++++++++++++---- src/components/InviteModal/InviteByEmail.tsx | 31 +--- src/components/styled/Select.tsx | 5 +- src/hooks/useInviteLink.ts | 26 ++- 13 files changed, 274 insertions(+), 101 deletions(-) diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json index 2a22f07c..7c4f8965 100644 --- a/public/locales/en/auth.json +++ b/public/locales/en/auth.json @@ -54,5 +54,23 @@ "start_download": "Start by downloading VoceChat mobile app", "open": "Open VoceChat", "have_already": "Have the app already?" + }, + "invite_expire": { + "min30": "30 minutes", + "h1": "1 hour", + "h6": "6 hours", + "h12": "12 hours", + "d1": "1 day", + "d7": "7 days", + "d30": "30 days" + }, + "invite_times": { + "no_limit": "no limit", + "time1": "1 use", + "times5": "5 uses", + "times10": "10 uses", + "times25": "25 uses", + "times50": "50 uses", + "times100": "100 uses" } } diff --git a/public/locales/en/chat.json b/public/locales/en/chat.json index e1e11920..8ef6d5d0 100644 --- a/public/locales/en/chat.json +++ b/public/locales/en/chat.json @@ -26,7 +26,8 @@ "send_invite_link": "Or Send invite link to your friends", "share_invite_link": "Share this link to invite people to this server.", "invite_link_faq": "Incorrect Invite Link?", - "invite_link_expire": "Invite link expires in 48 hours", + "invite_link_edit": "Edit invite link", + "invite_link_setting_tip": "Invite link expires in {{expire}}, max use times: {{times}}", "generate_new_link": "Generate New Link", "send_to": "Send To", diff --git a/public/locales/jp/chat.json b/public/locales/jp/chat.json index 46f6aeba..4152c4e3 100644 --- a/public/locales/jp/chat.json +++ b/public/locales/jp/chat.json @@ -25,7 +25,7 @@ "send_invite_link": "もしくは、招待用URLをコピーする", "share_invite_link": "このリンクを使って、友達を招待する", - "invite_link_expire": "招待用URLの有効期限は48時間です", + "invite_link_setting_tip": "招待用URLの有効期限は48時間です", "generate_new_link": "新しいリンクを生成する", "send_to": "送信先" diff --git a/public/locales/tr/chat.json b/public/locales/tr/chat.json index 0990ce40..1868865b 100644 --- a/public/locales/tr/chat.json +++ b/public/locales/tr/chat.json @@ -26,7 +26,7 @@ "send_invite_link": "Veya arkadaşlarınıza davet bağlantısı gönderin", "share_invite_link": "Diğerlerini bu bağlantıyı paylaşarak bu sunucuya davet edebilirsiniz.", "invite_link_faq": "Hatalı davet bağlantısı mı?", - "invite_link_expire": "Davet bağlantısı 48 saat geçerli olacaktır.", + "invite_link_setting_tip": "Davet bağlantısı 48 saat geçerli olacaktır.", "generate_new_link": "Yeni bağlantı yarat", "send_to": "Şuna gönder", diff --git a/public/locales/zh/auth.json b/public/locales/zh/auth.json index 4d396474..3ab16cc5 100644 --- a/public/locales/zh/auth.json +++ b/public/locales/zh/auth.json @@ -48,5 +48,23 @@ "title": "邀请链接已过期!", "desc": "请重新生成邀请链接", "desc_close": "该网页可关闭" + }, + "invite_expire": { + "min30": "30 minutes", + "h1": "1 hour", + "h6": "6 hours", + "h12": "12 hours", + "d1": "1 day", + "d7": "7 days", + "d30": "30 days" + }, + "invite_times": { + "no_limit": "no limit", + "time1": "1 use", + "times5": "5 uses", + "times10": "10 uses", + "times25": "25 uses", + "times50": "50 uses", + "times100": "100 uses" } } diff --git a/public/locales/zh/chat.json b/public/locales/zh/chat.json index d3794e1f..1da77ac7 100644 --- a/public/locales/zh/chat.json +++ b/public/locales/zh/chat.json @@ -26,8 +26,8 @@ "send_invite_link": "或者,向朋友发送邀请链接", "share_invite_link": "分享此链接,邀请朋友加入", "invite_link_faq": "链接异常?", - - "invite_link_expire": "邀请链接有效期:48小时", + "invite_link_edit": "编辑邀请链接", + "invite_link_setting_tip": "邀请链接有效期:{{expire}},最多使用次数:{{times}}", "generate_new_link": "生成新邀请链接", "send_to": "发送至", diff --git a/src/app/config.ts b/src/app/config.ts index 02937089..1a0be178 100644 --- a/src/app/config.ts +++ b/src/app/config.ts @@ -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; diff --git a/src/app/services/channel.ts b/src/app/services/channel.ts index 49a30386..1432a832 100644 --- a/src/app/services/channel.ts +++ b/src/app/services/channel.ts @@ -82,15 +82,13 @@ export const channelApi = createApi({ } } }), - createInviteLink: builder.query({ - query: (gid) => ({ + createInviteLink: builder.query({ + 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({ - 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) => { diff --git a/src/app/services/server.ts b/src/app/services/server.ts index 5c065884..4bbff41b 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -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({ - 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({ query: (data) => ({ url: "admin/system/organization", @@ -434,8 +418,6 @@ export const { useLazyGetServerQuery, useUpdateServerMutation, useUpdateLogoMutation, - useCreateInviteLinkQuery, - useLazyCreateInviteLinkQuery, useGetThirdPartySecretQuery, useUpdateThirdPartySecretMutation, useCreateAdminMutation, diff --git a/src/components/InviteLink.tsx b/src/components/InviteLink.tsx index b2a8b5e5..606e2415 100644 --- a/src/components/InviteLink.tsx +++ b/src/components/InviteLink.tsx @@ -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 = () => { +type Props = { + context?: "members" | "channel"; + cid?: number; +}; +const InviteLinkExpireList = getInviteLinkExpireList(); +const InviteLinkTimesList = getInviteLinkTimesList(); + +const InviteLink: FC = ({ 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 ( -
-

- {t("share_invite_link")} - - {t("invite_link_faq")} - -

-
- - +
+ + {t("invite_link_setting_tip", { + expire: current.expire.label, + times: current.times.label + })} + + +
+ +
+
- {t("invite_link_expire")} -
- -
- - + {editVisible && ( + + + + + + } + > +
+
+ Expire After: + +
+
+ Max Times of Uses: + +
+
+
+
+ )} + ); }; diff --git a/src/components/InviteModal/InviteByEmail.tsx b/src/components/InviteModal/InviteByEmail.tsx index de93079f..9b0d9bd5 100644 --- a/src/components/InviteModal/InviteByEmail.tsx +++ b/src/components/InviteModal/InviteByEmail.tsx @@ -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 = ({ cid }) => { const [email, setEmail] = useState(""); const formRef = useRef(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 = ({ cid }) => { -
- - -
- -
{!generating && }
-
- {t("invite_link_expire")} - +
); diff --git a/src/components/styled/Select.tsx b/src/components/styled/Select.tsx index 80014fbe..7fffbb9c 100644 --- a/src/components/styled/Select.tsx +++ b/src/components/styled/Select.tsx @@ -36,6 +36,7 @@ const Select: FC = ({ options = [], updateSelect = null, current = null } return ( = ({ options = [], updateSelect = null, current = null } } >
- + {(current !== null ? current : curr)?.title || t("action.select")} diff --git a/src/hooks/useInviteLink.ts b/src/hooks/useInviteLink.ts index 8e5aa003..c112a6bf 100644 --- a/src/hooks/useInviteLink.ts +++ b/src/hooks/useInviteLink.ts @@ -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