feat: more details for invite link
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "送信先"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "发送至",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,22 +1,56 @@
|
||||
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">
|
||||
{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
|
||||
@@ -28,6 +62,7 @@ const InviteLink: FC<Props> = () => {
|
||||
<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
|
||||
@@ -37,7 +72,15 @@ const InviteLink: FC<Props> = () => {
|
||||
{linkCopied ? "Copied" : t("action.copy", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-100">{t("invite_link_expire")}</span>
|
||||
<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>
|
||||
@@ -45,6 +88,70 @@ const InviteLink: FC<Props> = () => {
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user