feat: i18n
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import dayjs from "dayjs";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import isToday from "dayjs/plugin/isToday";
|
||||
import isYesterday from "dayjs/plugin/isYesterday";
|
||||
import i18n from '../i18n';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(isToday);
|
||||
dayjs.extend(isYesterday);
|
||||
|
||||
dayjs.locale(i18n.language == "en" ? "en" : "zh-cn");
|
||||
|
||||
@@ -8,6 +8,7 @@ import ChannelIcon from "./ChannelIcon";
|
||||
import ChannelModal from "./ChannelModal";
|
||||
import UsersModal from "./UsersModal";
|
||||
import InviteModal from "./InviteModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Styled = styled.ul`
|
||||
z-index: 999;
|
||||
@@ -42,6 +43,7 @@ const Styled = styled.ul`
|
||||
}
|
||||
`;
|
||||
export default function AddEntriesMenu() {
|
||||
const { t } = useTranslation();
|
||||
const currentUser = useAppSelector((store) => store.authData.user);
|
||||
const [isPrivate, setIsPrivate] = useState(false);
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
@@ -79,20 +81,20 @@ export default function AddEntriesMenu() {
|
||||
{currentUser?.is_admin && (
|
||||
<li className="item" onClick={handleOpenChannelModal.bind(null, false)}>
|
||||
<ChannelIcon className="icon" />
|
||||
New Channel
|
||||
{t("action.new_channel")}
|
||||
</li>
|
||||
)}
|
||||
<li className="item" onClick={handleOpenChannelModal.bind(null, true)}>
|
||||
<ChannelIcon personal={true} className="icon" />
|
||||
New Private Channel
|
||||
{t("action.new_private_channel")}
|
||||
</li>
|
||||
<li className="item" onClick={toggleUsersModalVisible}>
|
||||
<IconMention className="icon" />
|
||||
New Message
|
||||
{t("action.new_msg")}
|
||||
</li>
|
||||
<li className="item" onClick={toggleInviteModalVisible}>
|
||||
<IconInvite className="icon" />
|
||||
Invite People
|
||||
{t("action.invite_people")}
|
||||
</li>
|
||||
</Styled>
|
||||
{channelModalVisible && <ChannelModal personal={isPrivate} closeModal={handleCloseModal} />}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ChangeEvent, FC, useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import Avatar from "./Avatar";
|
||||
import uploadIcon from "../../assets/icons/upload.image.svg?url";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
width: 96px;
|
||||
@@ -75,6 +76,7 @@ const AvatarUploader: FC<Props> = ({
|
||||
uploadImage,
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const handleUpload = async (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (uploading) return;
|
||||
@@ -91,7 +93,7 @@ const AvatarUploader: FC<Props> = ({
|
||||
<Avatar type={type} url={url} name={name} />
|
||||
{!disabled && (
|
||||
<>
|
||||
<div className="tip">{uploading ? `Uploading` : `Change Avatar`}</div>
|
||||
<div className="tip">{uploading ? t("status.uploading") : t("action.change_avatar")}</div>
|
||||
<input
|
||||
multiple={false}
|
||||
onChange={handleUpload}
|
||||
|
||||
@@ -7,6 +7,7 @@ import IconInvite from "../../assets/icons/placeholder.invite.svg";
|
||||
import IconDownload from "../../assets/icons/placeholder.download.svg";
|
||||
import UsersModal from "./UsersModal";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -18,6 +19,7 @@ const classes = {
|
||||
boxTip: "px-5 text-sm text-[#475467] font-bold text-center"
|
||||
};
|
||||
const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
|
||||
const { t } = useTranslation("welcome");
|
||||
const server = useAppSelector((store) => store.server);
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const [createChannelVisible, setCreateChannelVisible] = useState(false);
|
||||
@@ -32,22 +34,21 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
|
||||
setInviteModalVisible((prev) => !prev);
|
||||
};
|
||||
const chatTip =
|
||||
type == "chat" ? "Create a Channel to Start a Conversation" : "Send a Direct Message";
|
||||
type == "chat" ? t("start_by_channel") : t("start_by_dm");
|
||||
const chatHandler = type == "chat" ? toggleChannelModalVisible : toggleUserListVisible;
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-8 -mt-[50px]">
|
||||
<div className="flex flex-col gap-2 items-center">
|
||||
<h2 className="text-3xl text-[#344054] font-bold">Welcome to {server.name} server</h2>
|
||||
<h2 className="text-3xl text-[#344054] font-bold">{t("title", { name: server.name })}</h2>
|
||||
<p className="text-sm text-[#98a2b3] max-w-[424px] text-center">
|
||||
Here are some steps to help you get started. For more, check out our Getting Started
|
||||
guide
|
||||
{t("desc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-6">
|
||||
<div className={classes.box} onClick={toggleInviteModalVisible}>
|
||||
<IconInvite className={classes.boxIcon} />
|
||||
<div className={classes.boxTip}>Invite your friends or teammates</div>
|
||||
<div className={classes.boxTip}>{t("invite")}</div>
|
||||
</div>
|
||||
<button onClick={chatHandler} className={classes.box} >
|
||||
<IconChat className={classes.boxIcon} />
|
||||
@@ -55,11 +56,11 @@ const BlankPlaceholder: FC<Props> = ({ type = "chat" }) => {
|
||||
</button>
|
||||
<a href={"https://voce.chat#download"} target={"_blank"} rel="noreferrer" className={classes.box} >
|
||||
<IconDownload className={classes.boxIcon} />
|
||||
<div className={classes.boxTip}>Download Mobile apps</div>
|
||||
<div className={classes.boxTip}>{t("download")}</div>
|
||||
</a>
|
||||
<a href={"https://voce.chat"} target={"_blank"} rel="noreferrer" className={classes.box} >
|
||||
<a href={"https://doc.voce.chat"} target={"_blank"} rel="noreferrer" className={classes.box} >
|
||||
<IconAsk className={classes.boxIcon} />
|
||||
<div className={classes.boxTip}>Got questions? Visit our help center</div>
|
||||
<div className={classes.boxTip}>{t("help")}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ import useFilteredUsers from "../../hook/useFilteredUsers";
|
||||
import { useCreateChannelMutation } from "../../../app/services/channel";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { CreateChannelDTO } from "../../../types/channel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
personal?: boolean;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { usersData, loginUid } = useAppSelector((store) => {
|
||||
return { usersData: store.users.byId, loginUid: store.authData.user?.uid };
|
||||
});
|
||||
@@ -98,7 +100,7 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
|
||||
<input
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Type Username to search"
|
||||
placeholder={t("search_user_placeholder") || ""}
|
||||
/>
|
||||
</div>
|
||||
{users && (
|
||||
@@ -129,21 +131,21 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
|
||||
</div>
|
||||
)}
|
||||
<div className={`right ${!is_public ? "private" : ""}`}>
|
||||
<h3 className="title">Create New Channel</h3>
|
||||
<h3 className="title">{t("create_channel")}</h3>
|
||||
<p className="desc normal">
|
||||
{!is_public
|
||||
? "This is a private channel, only select members will be able to join."
|
||||
: "This is a public channel, everyone in this team can see it."}
|
||||
? t("create_private_channel_desc")
|
||||
: t("create_channel_desc")}
|
||||
</p>
|
||||
<div className="name">
|
||||
<span className="label normal">Channel Name</span>
|
||||
<span className="label normal">{t("channel_name")}</span>
|
||||
<div className="input">
|
||||
<input onChange={handleNameInput} value={name} placeholder="new channel" />
|
||||
<ChannelIcon personal={!is_public} className="icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="private">
|
||||
<span className="txt normal">Private Channel</span>
|
||||
<span className="txt normal">{t("private_channel")}</span>
|
||||
<StyledToggle
|
||||
data-checked={!is_public}
|
||||
data-disabled={!loginUser?.is_admin}
|
||||
@@ -152,10 +154,10 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
|
||||
</div>
|
||||
<div className="btns">
|
||||
<Button onClick={closeModal} className="normal cancel">
|
||||
Cancel
|
||||
{t("action.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button disabled={isLoading} onClick={handleCreate} className="normal">
|
||||
Create
|
||||
{t("action.create", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,6 +50,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
}
|
||||
.right {
|
||||
min-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGetServerVersionQuery } from "../../app/services/server";
|
||||
type Props = {};
|
||||
const FAQ: FC<Props> = () => {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "faq" });
|
||||
const { data: serverVersion } = useGetServerVersionQuery();
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="item">Client Version: {process.env.VERSION}</div>
|
||||
<div className="item">Server Version: {serverVersion}</div>
|
||||
<div className="item">Build Timestamp: {process.env.REACT_APP_BUILD_TIME}</div>
|
||||
<div className="item">{t("client_version")}: {process.env.VERSION}</div>
|
||||
<div className="item">{t("server_version")}: {serverVersion}</div>
|
||||
<div className="item">{t("build_time")}: {process.env.REACT_APP_BUILD_TIME}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, useEffect } from "react";
|
||||
import IconGithub from "../../assets/icons/github.svg";
|
||||
import styled from "styled-components";
|
||||
import Button from "./styled/Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledSocialButton = styled(Button)`
|
||||
width: 100%;
|
||||
@@ -25,6 +26,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", client_id }) => {
|
||||
const { t } = useTranslation("auth");
|
||||
useEffect(() => {
|
||||
const handleGithubLoginStatusChange = (evt: StorageEvent) => {
|
||||
const { key, newValue } = evt;
|
||||
@@ -60,7 +62,7 @@ const GithubLoginButton: FC<Props> = ({ type = "login", source = "webapp", clien
|
||||
return (
|
||||
<StyledSocialButton onClick={handleGithubLogin}>
|
||||
<IconGithub className="icon" />
|
||||
{` ${type === "login" ? "Sign in" : "Sign up"} with Github`}
|
||||
{` ${type === "login" ? t("login.github") : t("reg.github")}`}
|
||||
</StyledSocialButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config";
|
||||
import IconGoogle from "../../assets/icons/google.svg";
|
||||
import Button from "./styled/Button";
|
||||
import { useLoginMutation } from "../../app/services/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledSocialButton = styled(Button)`
|
||||
position: relative;
|
||||
@@ -59,6 +60,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
const { t } = useTranslation("auth");
|
||||
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
|
||||
//拿本地存的magic token
|
||||
const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN);
|
||||
@@ -91,7 +93,7 @@ const GoogleLoginInner: FC<Props> = ({ type = "login", loaded, loadError }) => {
|
||||
{loadError
|
||||
? "Script Load Error!"
|
||||
: loaded
|
||||
? `${type === "login" ? "Sign in" : "Sign up"} with Google`
|
||||
? `${type === "login" ? t("login.google") : t("reg.google")}`
|
||||
: `Initializing`}
|
||||
</div>
|
||||
<div className="hide">
|
||||
|
||||
@@ -3,6 +3,7 @@ import useInviteLink from "../hook/useInviteLink";
|
||||
import Input from "./styled/Input";
|
||||
import Button from "./styled/Button";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -40,6 +41,7 @@ const StyledWrapper = styled.div`
|
||||
`;
|
||||
type Props = {};
|
||||
const InviteLink: FC<Props> = () => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { generating, link, linkCopied, copyLink, generateNewLink } = useInviteLink();
|
||||
const handleNewLink = () => {
|
||||
generateNewLink();
|
||||
@@ -47,16 +49,16 @@ const InviteLink: FC<Props> = () => {
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<span className="tip">Share this link to invite people to this server.</span>
|
||||
<span className="tip">{t("share_invite_link")}</span>
|
||||
<div className="link">
|
||||
<Input readOnly className={"large"} placeholder="Generating" value={link} />
|
||||
<Button onClick={copyLink} className="ghost small border_less">
|
||||
{linkCopied ? "Copied" : `Copy`}
|
||||
{linkCopied ? "Copied" : t("action.copy", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="sub_tip">Invite link expires in 7 days.</span>
|
||||
<span className="sub_tip">{t("invite_link_expire")}</span>
|
||||
<Button className="ghost" disabled={generating} onClick={handleNewLink}>
|
||||
{generating ? `Generating` : `Generate New Link`}
|
||||
{generating ? `Generating` : t("generate_new_link")}
|
||||
</Button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState, ChangeEvent, FC } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import useInviteLink from "../../hook/useInviteLink";
|
||||
import Button from "../styled/Button";
|
||||
@@ -59,6 +60,7 @@ const Styled = styled.div`
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
button {
|
||||
margin-left: 4px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #22ccee;
|
||||
@@ -71,6 +73,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const InviteByEmail: FC<Props> = ({ cid }) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const [email, setEmail] = useState("");
|
||||
const { enableSMTP, linkCopied, link, copyLink, generateNewLink, generating } =
|
||||
useInviteLink(cid);
|
||||
@@ -87,33 +90,33 @@ const InviteByEmail: FC<Props> = ({ cid }) => {
|
||||
return (
|
||||
<Styled>
|
||||
<div className="invite">
|
||||
<label htmlFor="">Invite by Email</label>
|
||||
<label htmlFor="">{t("invite_by_email")}</label>
|
||||
<div className="input">
|
||||
<Input
|
||||
value={email}
|
||||
onChange={handleEmail}
|
||||
disabled={!enableSMTP}
|
||||
type="email"
|
||||
placeholder={enableSMTP ? "Enter Email" : "Enable SMTP First"}
|
||||
placeholder={enableSMTP ? "Enter Email" : t("enable_smtp") || ""}
|
||||
/>
|
||||
<Button disabled={!enableSMTP || !email} className="send">
|
||||
Send
|
||||
{t("action.send", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="link">
|
||||
<label htmlFor="">Or Send invite link to your friends</label>
|
||||
<label htmlFor="">{t("send_invite_link")}</label>
|
||||
<div className="input">
|
||||
<Input readOnly className="invite" placeholder="Generating" value={link} />
|
||||
<button className="copy" onClick={copyLink}>
|
||||
Copy
|
||||
{t("action.copy", { ns: "common" })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tip">
|
||||
Invite link expires in 7 days.{" "}
|
||||
{t("invite_link_expire")}
|
||||
<button disabled={generating} className="new" onClick={() => generateNewLink()}>
|
||||
Generate New Link
|
||||
{t("generate_new_link")}
|
||||
</button>
|
||||
</div>
|
||||
</Styled>
|
||||
|
||||
@@ -5,6 +5,7 @@ import CloseIcon from "../../../assets/icons/close.svg";
|
||||
import Modal from "../Modal";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
@@ -37,6 +38,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const InviteModal: FC<Props> = ({ type = "server", cid, title = "", closeModal }) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { channel, server } = useAppSelector((store) => {
|
||||
return {
|
||||
channel: cid ? store.channels.byId[cid] : undefined,
|
||||
@@ -48,7 +50,7 @@ const InviteModal: FC<Props> = ({ type = "server", cid, title = "", closeModal }
|
||||
<Modal>
|
||||
<Styled>
|
||||
<h2 className="title">
|
||||
Add friends to {finalTitle}
|
||||
{t("invite_title", { name: finalTitle })}
|
||||
<CloseIcon className="close" onClick={closeModal} />
|
||||
</h2>
|
||||
{!channel?.is_public && <AddMembers cid={cid} closeModal={closeModal} />}
|
||||
|
||||
@@ -5,6 +5,7 @@ import useLeaveChannel from "../../hook/useLeaveChannel";
|
||||
import Modal from "../Modal";
|
||||
import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
@@ -13,6 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const LeaveConfirmModal: FC<Props> = ({ id, closeModal, handleNextStep }) => {
|
||||
const { t } = useTranslation("setting");
|
||||
const navigateTo = useNavigate();
|
||||
const { isOwner, leaving, leaveChannel, leaveSuccess } = useLeaveChannel(id);
|
||||
|
||||
@@ -28,16 +30,16 @@ const LeaveConfirmModal: FC<Props> = ({ id, closeModal, handleNextStep }) => {
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
className="compact"
|
||||
title="Leave Channel"
|
||||
title={t("channel.leave") || ""}
|
||||
description={
|
||||
isOwner
|
||||
? "You need to transfer your channel ownership to someone else before leaving the channel."
|
||||
: "Are you sure want to leave this channel?"
|
||||
? t("channel.transfer_desc") || ""
|
||||
: t("channel.leave_desc") || ""
|
||||
}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal.bind(null, undefined)} className="cancel">
|
||||
Cancel
|
||||
{t("action.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
{isOwner ? (
|
||||
<Button onClick={handleNextStep} className="main">
|
||||
|
||||
@@ -7,6 +7,7 @@ import useLeaveChannel from "../../hook/useLeaveChannel";
|
||||
import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
import User from "../User";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const UserList = styled.ul`
|
||||
display: flex;
|
||||
@@ -37,6 +38,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const TransferOwnerModal: FC<Props> = ({ id, closeModal, withLeave = true }) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
transferOwner,
|
||||
otherMembers,
|
||||
@@ -80,7 +82,7 @@ const TransferOwnerModal: FC<Props> = ({ id, closeModal, withLeave = true }) =>
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal.bind(null, undefined)} className="cancel">
|
||||
Cancel
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
<Button disabled={!uid} onClick={handleTransferAndLeave} className="danger">
|
||||
{operating ? "Assigning" : `Assign and Leave`}
|
||||
|
||||
@@ -13,11 +13,13 @@ import IconArrowDown from "../../../assets/icons/arrow.down.mini.svg";
|
||||
import IconCheck from "../../../assets/icons/check.sign.svg";
|
||||
import useUserOperation from "../../hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
cid?: number;
|
||||
}
|
||||
const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
const { t } = useTranslation(["member", "common"]);
|
||||
const { users, channels, loginUser } = useAppSelector((store) => {
|
||||
return {
|
||||
users: store.users,
|
||||
@@ -54,9 +56,9 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
<StyledWrapper>
|
||||
{loginUser?.is_admin && <InviteLink />}
|
||||
<div className="intro">
|
||||
<h4 className="title">Manage Members</h4>
|
||||
<h4 className="title">{t("manage_members")}</h4>
|
||||
<p className="desc">
|
||||
Disabling your account means you can recover it at any time after taking this action.
|
||||
{t("manage_tip")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +86,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
</div>
|
||||
<div className="right">
|
||||
<span className="role">
|
||||
{is_admin ? "Admin" : "User"}
|
||||
{is_admin ? t("admin") : t("user")}
|
||||
{switchRoleVisible && (
|
||||
<Tippy
|
||||
interactive
|
||||
@@ -100,7 +102,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
isAdmin: true
|
||||
})}
|
||||
>
|
||||
Admin
|
||||
{t("admin")}
|
||||
{is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
<li
|
||||
@@ -111,7 +113,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
isAdmin: false
|
||||
})}
|
||||
>
|
||||
User
|
||||
{t("user")}
|
||||
{!is_admin && <IconCheck className="icon" />}
|
||||
</li>
|
||||
</StyledMenu>
|
||||
@@ -130,17 +132,17 @@ const ManageMembers: FC<Props> = ({ cid }) => {
|
||||
<StyledMenu className="menu">
|
||||
{email && (
|
||||
<li className="item" onClick={copyEmail.bind(null, email)}>
|
||||
Copy Email
|
||||
{t("action.copy_email", { ns: "common" })}
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromChannel && (
|
||||
<li className="item danger" onClick={removeFromChannel.bind(null, uid)}>
|
||||
Remove From Channel
|
||||
{t("remove_from_channel")}
|
||||
</li>
|
||||
)}
|
||||
{canRemove && (
|
||||
<li className="item danger" onClick={removeUser.bind(null, uid)}>
|
||||
Remove
|
||||
{t("action.remove", { ns: "common" })}
|
||||
</li>
|
||||
)}
|
||||
</StyledMenu>
|
||||
|
||||
@@ -20,6 +20,7 @@ import IconSelect from "../../../assets/icons/select.svg";
|
||||
import IconDelete from "../../../assets/icons/delete.svg";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import useMessageOperation from "./useMessageOperation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledCmds = styled.ul`
|
||||
z-index: 999;
|
||||
@@ -68,6 +69,7 @@ type Props = {
|
||||
toggleEditMessage: () => void;
|
||||
};
|
||||
const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleEditMessage }) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
canDelete,
|
||||
canReply,
|
||||
@@ -134,27 +136,27 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
|
||||
content={<ReactionPicker mid={mid} hidePicker={hideAll} />}
|
||||
>
|
||||
<li className="cmd">
|
||||
<Tooltip placement="top" tip="Add Reaction">
|
||||
<Tooltip placement="top" tip={t("action.add_reaction")}>
|
||||
<img src={reactIcon} className="toggler" alt="icon emoji" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
</Tippy>
|
||||
{canEdit && (
|
||||
<li className="cmd" onClick={toggleEditMessage}>
|
||||
<Tooltip placement="top" tip="Edit">
|
||||
<Tooltip placement="top" tip={t("action.edit")}>
|
||||
<img src={editIcon} alt="icon edit" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
{canReply && (
|
||||
<li className="cmd" onClick={handleReply}>
|
||||
<Tooltip placement="top" tip="Reply">
|
||||
<Tooltip placement="top" tip={t("action.reply")}>
|
||||
<img src={replyIcon} alt="icon reply" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
<li className="cmd fav" onClick={handleAddFav}>
|
||||
<Tooltip placement="top" tip="Add to Favorites">
|
||||
<Tooltip placement="top" tip={t("action.add_to_fav")}>
|
||||
<IconBookmark />
|
||||
</Tooltip>
|
||||
</li>
|
||||
@@ -169,22 +171,22 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
|
||||
items={
|
||||
[
|
||||
canPin && {
|
||||
title: pinned ? `Unpin Message` : `Pin Message`,
|
||||
title: pinned ? t("action.unpin") : t("action.pin"),
|
||||
icon: <IconPin className="icon" />,
|
||||
handler: pinned ? handleUnpin : togglePinModal
|
||||
},
|
||||
{
|
||||
title: "Forward",
|
||||
title: t("action.forward"),
|
||||
icon: <IconForward className="icon" />,
|
||||
handler: toggleForwardModal
|
||||
},
|
||||
{
|
||||
title: "Select",
|
||||
title: t("action.select"),
|
||||
icon: <IconSelect className="icon" />,
|
||||
handler: handleSelect.bind(null, mid)
|
||||
},
|
||||
canDelete && {
|
||||
title: " Delete",
|
||||
title: t("action.remove"),
|
||||
danger: true,
|
||||
icon: <IconDelete className="icon" />,
|
||||
handler: toggleDeleteModal
|
||||
@@ -195,7 +197,7 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
|
||||
}
|
||||
>
|
||||
<li className="cmd">
|
||||
<Tooltip placement="top" tip="More">
|
||||
<Tooltip placement="top" tip={t("more")}>
|
||||
<img src={moreIcon} alt="icon more" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
|
||||
@@ -12,6 +12,7 @@ import IconSelect from "../../../assets/icons/select.svg";
|
||||
import { updateSelectMessages } from "../../../app/slices/ui";
|
||||
import useSendMessage from "../../hook/useSendMessage";
|
||||
import useMessageOperation from "./useMessageOperation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
type Props = {
|
||||
context: "user" | "channel";
|
||||
contextId: number;
|
||||
@@ -30,6 +31,7 @@ const MessageContextMenu: FC<Props> = ({
|
||||
editMessage,
|
||||
children
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
copyContent,
|
||||
canEdit,
|
||||
@@ -58,37 +60,37 @@ const MessageContextMenu: FC<Props> = ({
|
||||
};
|
||||
const items = [
|
||||
canEdit && {
|
||||
title: "Edit Message",
|
||||
title: t("action.edit_msg"),
|
||||
icon: <IconEdit className="icon" />,
|
||||
handler: editMessage
|
||||
},
|
||||
canReply && {
|
||||
title: "Reply",
|
||||
title: t("action.reply"),
|
||||
icon: <IconReply className="icon" />,
|
||||
handler: handleReply
|
||||
},
|
||||
canCopy && {
|
||||
title: "Copy",
|
||||
title: t("action.copy"),
|
||||
icon: <IconCopy className="icon" />,
|
||||
handler: copyContent
|
||||
},
|
||||
canPin && {
|
||||
title: pinned ? "Unpin" : "Pin",
|
||||
title: pinned ? t("action.unpin") : t("action.pin"),
|
||||
icon: <IconPin className="icon" />,
|
||||
handler: pinned ? unPin.bind(null, mid) : togglePinModal
|
||||
},
|
||||
{
|
||||
title: "Forward",
|
||||
title: t("action.forward"),
|
||||
icon: <IconForward className="icon" />,
|
||||
handler: toggleForwardModal
|
||||
},
|
||||
{
|
||||
title: "Select",
|
||||
title: t("action.select"),
|
||||
icon: <IconSelect className="icon" />,
|
||||
handler: handleSelect
|
||||
},
|
||||
canDelete && {
|
||||
title: "Delete",
|
||||
title: t("action.remove"),
|
||||
danger: true,
|
||||
icon: <IconDelete className="icon" />,
|
||||
handler: toggleDeleteModal
|
||||
|
||||
@@ -5,6 +5,7 @@ import renderContent from "./renderContent";
|
||||
import Avatar from "../Avatar";
|
||||
import IconForward from "../../../assets/icons/forward.svg";
|
||||
import useNormalizeMessage from "../../hook/useNormalizeMessage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledForward = styled.div`
|
||||
display: flex;
|
||||
@@ -36,6 +37,7 @@ type Props = {
|
||||
id: string;
|
||||
};
|
||||
const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
const { t } = useTranslation();
|
||||
const { normalizeMessage, messages } = useNormalizeMessage();
|
||||
const [forwards, setForwards] = useState<ReactElement | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -52,7 +54,7 @@ const ForwardedMessage: FC<Props> = ({ context, to, from_uid, id }) => {
|
||||
<StyledForward data-forwarded-mids={forward_mids.join(",")}>
|
||||
<h4 className="tip">
|
||||
<IconForward className="icon" />
|
||||
Forwarded
|
||||
{t("action.forward")}
|
||||
</h4>
|
||||
<div className="list">
|
||||
{messages.map((msg, idx) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import StyledModal from "../styled/Modal";
|
||||
import Button from "../styled/Button";
|
||||
import Modal from "../Modal";
|
||||
import PreviewMessage from "./PreviewMessage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledPinModal = styled(StyledModal)`
|
||||
min-width: 406px;
|
||||
@@ -30,6 +31,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const PinMessageModal: FC<Props> = ({ closeModal, mid = 0, gid = 0 }) => {
|
||||
const { t } = useTranslation();
|
||||
const { channel, pinMessage, isPining, isSuccess } = usePinMessage(gid);
|
||||
const handlePin = () => {
|
||||
pinMessage(mid);
|
||||
@@ -51,14 +53,14 @@ const PinMessageModal: FC<Props> = ({ closeModal, mid = 0, gid = 0 }) => {
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal} className="cancel">
|
||||
Cancel
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
<Button disabled={isPining} onClick={handlePin} className="main">
|
||||
{isPining ? "Pining" : `Pin It`}
|
||||
{isPining ? "Pining" : t("action.pin")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
title="Pin It"
|
||||
title={t("action.pin") || ""}
|
||||
description={`Do you want to pin this message to #${channel?.name}`}
|
||||
>
|
||||
<PreviewMessage mid={mid} />
|
||||
|
||||
@@ -9,6 +9,7 @@ import StyledWrapper from "./styled";
|
||||
import StyledMenu from "../styled/Menu";
|
||||
import useUserOperation from "../../hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
uid: number;
|
||||
@@ -17,6 +18,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
const { t } = useTranslation(["member", "common"]);
|
||||
const {
|
||||
canCall,
|
||||
call,
|
||||
@@ -56,14 +58,14 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
<NavLink to={`/chat/dm/${uid}`}>
|
||||
<li className="icon chat">
|
||||
<IconMessage />
|
||||
<span className="txt">Message</span>
|
||||
<span className="txt">{t("send_msg")}</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
{/* <NavLink to={`#`}> */}
|
||||
{enableCall && (
|
||||
<li className="icon call" onClick={call}>
|
||||
<IconCall />
|
||||
<span className="txt">Call</span>
|
||||
<span className="txt">{t("call")}</span>
|
||||
</li>
|
||||
)}
|
||||
{/* </NavLink> */}
|
||||
@@ -79,22 +81,22 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
{enableCall && (
|
||||
<li className="item" onClick={call}>
|
||||
{/* <IconCall className="icon" /> */}
|
||||
Call
|
||||
{t("call")}
|
||||
</li>
|
||||
)}
|
||||
{canCopyEmail && (
|
||||
<li className="item" onClick={copyEmail.bind(undefined, email)}>
|
||||
Copy Email
|
||||
{t("copy_email")}
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromChannel && (
|
||||
<li className="item danger" onClick={removeFromChannel.bind(null, uid)}>
|
||||
Remove from Channel
|
||||
{t("remove_from_channel")}
|
||||
</li>
|
||||
)}
|
||||
{canRemoveFromServer && (
|
||||
<li className="item danger" onClick={removeUser.bind(null, uid)}>
|
||||
Remove from Server
|
||||
{t("remove")}
|
||||
</li>
|
||||
)}
|
||||
</StyledMenu>
|
||||
@@ -102,7 +104,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
>
|
||||
<li className={`icon more ${hasMore ? "" : "disabled"}`}>
|
||||
<IconMore />
|
||||
<span className="txt">More</span>
|
||||
<span className="txt">{t("more", { ns: "common" })}</span>
|
||||
</li>
|
||||
</Tippy>
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC, MouseEvent } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
@@ -54,15 +55,16 @@ interface Props {
|
||||
}
|
||||
|
||||
const SaveTip: FC<Props> = ({ saveHandler, resetHandler }) => {
|
||||
const { t } = useTranslation("setting");
|
||||
return (
|
||||
<StyledWrapper className="animate__animated animate__flipInX animate__faster">
|
||||
<span className="txt">You have unsaved changes!</span>
|
||||
<span className="txt">{t('save_tip')}</span>
|
||||
<div className="btns">
|
||||
<button className="btn reset" onClick={resetHandler}>
|
||||
Reset
|
||||
{t('reset')}
|
||||
</button>
|
||||
<button className="btn" onClick={saveHandler}>
|
||||
Save Changes
|
||||
{t('save_change')}
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -5,6 +5,7 @@ import addIcon from "../../assets/icons/add.svg?url";
|
||||
import AddEntriesMenu from "./AddEntriesMenu";
|
||||
import Tooltip from "./Tooltip";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
@@ -35,13 +36,14 @@ const StyledWrapper = styled.div`
|
||||
`;
|
||||
type Props = {};
|
||||
const Search: FC<Props> = () => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="search">
|
||||
<img src={searchIcon} alt="search icon" />
|
||||
<input placeholder="Search..." className="input" />
|
||||
<input placeholder={`${t("action.search")}...`} className="input" />
|
||||
</div>
|
||||
<Tooltip tip="More" placement="bottom">
|
||||
<Tooltip tip={t("more")} placement="bottom">
|
||||
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
||||
<img src={addIcon} alt="add icon" className="add" />
|
||||
</Tippy>
|
||||
|
||||
@@ -6,6 +6,7 @@ import MarkdownIcon from "../../../assets/icons/markdown.svg";
|
||||
import FullscreenIcon from "../../../assets/icons/fullscreen.svg";
|
||||
import ExitFullscreenIcon from "../../../assets/icons/fullscreen.exit.svg";
|
||||
import useUploadFile from "../../hook/useUploadFile";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Styled = styled.div`
|
||||
display: flex;
|
||||
@@ -58,6 +59,7 @@ const Toolbar: FC<Props> = ({
|
||||
to,
|
||||
context
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { addStageFile } = useUploadFile({ context, id: to });
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const handleUpload = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -94,7 +96,7 @@ const Toolbar: FC<Props> = ({
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
<Tooltip placement="top" tip="Upload">
|
||||
<Tooltip placement="top" tip={t("action.upload")}>
|
||||
<div className="add">
|
||||
<AddIcon />
|
||||
<label htmlFor="file">
|
||||
|
||||
@@ -15,6 +15,7 @@ import MixedInput, { useMixedEditor } from "../MixedInput";
|
||||
import useDraft from "../../hook/useDraft";
|
||||
import useUploadFile from "../../hook/useUploadFile";
|
||||
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Modes = {
|
||||
text: "text",
|
||||
@@ -29,6 +30,7 @@ const Send: FC<IProps> = ({
|
||||
context = "channel",
|
||||
id
|
||||
}) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { resetStageFiles } = useUploadFile({ context, id });
|
||||
const { getDraft, getUpdateDraft } = useDraft({ context, id });
|
||||
const editor = useMixedEditor(`${context}_${id}`);
|
||||
@@ -130,14 +132,13 @@ const Send: FC<IProps> = ({
|
||||
setMarkdownFullscreen((prev) => !prev);
|
||||
};
|
||||
const name = context == "channel" ? channelsData[id]?.name : usersData[id]?.name;
|
||||
const placeholder = `Send to ${ChatPrefixes[context]}${name} `;
|
||||
const placeholder = `${t("send_to")} ${ChatPrefixes[context]}${name} `;
|
||||
const members =
|
||||
context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : [];
|
||||
return (
|
||||
<StyledSend
|
||||
className={`send ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${
|
||||
replying_mid ? "reply" : ""
|
||||
} ${context}`}
|
||||
className={`send ${mode} ${markdownFullscreen ? "fullscreen" : ""} ${replying_mid ? "reply" : ""
|
||||
} ${context}`}
|
||||
>
|
||||
{replying_mid && <Replying context={context} mid={replying_mid} id={id} />}
|
||||
{mode == Modes.text && <UploadFileList context={context} id={id} />}
|
||||
|
||||
@@ -5,6 +5,7 @@ import addIcon from "../../assets/icons/add.svg?url";
|
||||
import Tooltip from "./Tooltip";
|
||||
import AddEntriesMenu from "./AddEntriesMenu";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
min-height: 56px;
|
||||
@@ -54,6 +55,7 @@ type Props = {
|
||||
readonly?: boolean;
|
||||
};
|
||||
export default function Server({ readonly = false }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { pathname } = useLocation();
|
||||
const { server, userCount } = useAppSelector((store) => {
|
||||
return {
|
||||
@@ -74,7 +76,7 @@ export default function Server({ readonly = false }: Props) {
|
||||
<h3 className="name" title={description}>
|
||||
{name}
|
||||
</h3>
|
||||
<span className="desc">{userCount} members</span>
|
||||
<span className="desc">{userCount} {t("members")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
@@ -91,12 +93,12 @@ export default function Server({ readonly = false }: Props) {
|
||||
<h3 className="name" title={description}>
|
||||
{name}
|
||||
</h3>
|
||||
<span className="desc">{userCount} members</span>
|
||||
<span className="desc">{userCount} {t("members")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NavLink>
|
||||
|
||||
<Tooltip tip="More" placement="bottom">
|
||||
<Tooltip tip={t("more")} placement="bottom">
|
||||
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
||||
<img src={addIcon} alt="add icon" className="add" />
|
||||
</Tippy>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, ReactElement } from "react";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import useUserOperation from "../../hook/useUserOperation";
|
||||
import ContextMenu, { Item } from "../ContextMenu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
enable?: boolean;
|
||||
@@ -13,6 +14,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, children }) => {
|
||||
const { t } = useTranslation("member");
|
||||
const {
|
||||
canCall,
|
||||
call,
|
||||
@@ -43,25 +45,25 @@ const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, c
|
||||
items={
|
||||
[
|
||||
{
|
||||
title: "Message",
|
||||
title: t("send_msg"),
|
||||
handler: startChat
|
||||
},
|
||||
canCall && {
|
||||
title: "Call",
|
||||
title: t("call"),
|
||||
handler: call
|
||||
},
|
||||
canCopyEmail && {
|
||||
title: "Copy Email",
|
||||
title: t("copy_email"),
|
||||
handler: copyEmail
|
||||
},
|
||||
canRemoveFromChannel && {
|
||||
danger: true,
|
||||
title: "Remove From Channel",
|
||||
title: t("remove_from_channel"),
|
||||
handler: removeFromChannel
|
||||
},
|
||||
canRemove && {
|
||||
danger: true,
|
||||
title: "Remove",
|
||||
title: t("remove"),
|
||||
handler: removeUser
|
||||
}
|
||||
].filter(Boolean) as Item[]
|
||||
|
||||
@@ -6,6 +6,7 @@ import useFilteredUsers from "../hook/useFilteredUsers";
|
||||
|
||||
import User from "./User";
|
||||
import Modal from "./Modal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -56,6 +57,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const UsersModal: FC<Props> = ({ closeModal }) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const { users, updateInput, input } = useFilteredUsers();
|
||||
useOutsideClick(wrapperRef, closeModal);
|
||||
@@ -68,7 +70,7 @@ const UsersModal: FC<Props> = ({ closeModal }) => {
|
||||
<Modal>
|
||||
<StyledWrapper ref={wrapperRef}>
|
||||
<div className="search">
|
||||
<input value={input} onChange={handleSearch} placeholder="Type Username to search" />
|
||||
<input value={input} onChange={handleSearch} placeholder={t("search_user_placeholder") || ""} />
|
||||
</div>
|
||||
{users && (
|
||||
<ul className="users">
|
||||
|
||||
@@ -12,6 +12,7 @@ const StyledButton = styled.button`
|
||||
line-height: 20px;
|
||||
color: #fff;
|
||||
background-color: #22ccee;
|
||||
word-break: keep-all;
|
||||
&.flex {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import Tippy from "@tippyjs/react";
|
||||
import IconSelect from "../../../assets/icons/check.sign.svg";
|
||||
import IconArrow from "../../../assets/icons/arrow.down.svg";
|
||||
import Menu from "./Menu";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Styled = styled.div`
|
||||
user-select: none;
|
||||
@@ -41,6 +42,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }) => {
|
||||
const { t } = useTranslation();
|
||||
const [optionsVisible, setOptionsVisible] = useState(false);
|
||||
const [curr, setCurr] = useState<Partial<Option> | null>(null);
|
||||
const toggleVisible = () => {
|
||||
@@ -79,7 +81,7 @@ const Select: FC<Props> = ({ options = [], updateSelect = null, current = null }
|
||||
}
|
||||
>
|
||||
<Styled onClick={toggleVisible}>
|
||||
<span className="txt">{(current !== null ? current : curr)?.title || "Select"}</span>
|
||||
<span className="txt">{(current !== null ? current : curr)?.title || t("action.select")}</span>
|
||||
<IconArrow className="icon" />
|
||||
</Styled>
|
||||
</Tippy>
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import Backend from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
// don't want to use this?
|
||||
// have a look at the Quick start guide
|
||||
// for passing in lng and translations on init
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales (i.e. https://github.com/i18next/react-i18next/tree/master/example/react/public/locales)
|
||||
// learn more: https://github.com/i18next/i18next-http-backend
|
||||
// want your translations to be loaded from a professional CDN? => https://github.com/locize/react-tutorial#step-2---use-the-locize-cdn
|
||||
.use(Backend)
|
||||
// detect user language
|
||||
// learn more: https://github.com/i18next/i18next-browser-languageDetector
|
||||
.use(LanguageDetector)
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
// init i18next
|
||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||
.init({
|
||||
ns: ["common", "chat", "member", "setting", "fav", "file", "welcome", "auth"],
|
||||
defaultNS: "common",
|
||||
fallbackLng: 'en',
|
||||
debug: true,
|
||||
// interpolation: {
|
||||
// escapeValue: false, // not needed for react as it escapes by default
|
||||
// }
|
||||
});
|
||||
|
||||
|
||||
export default i18n;
|
||||
@@ -4,6 +4,7 @@ import { Provider } from "react-redux";
|
||||
import Widget from "./widget/index";
|
||||
import './assets/index.css';
|
||||
import store from "./app/store";
|
||||
import './i18n';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||
const hostId = new URLSearchParams(location.search).get("host");
|
||||
|
||||
+5
-2
@@ -1,5 +1,6 @@
|
||||
// just for debug performance problem
|
||||
// import "./wdyr";
|
||||
import { Suspense } from 'react';
|
||||
import ReactDOM from "react-dom/client";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { Reset } from "styled-reset";
|
||||
@@ -12,18 +13,20 @@ import { register } from "./serviceWorkerRegistration";
|
||||
import MarkdownStyleOverride from "./common/component/MarkdownStyleOverride";
|
||||
import ReduxRoutes from "./routes";
|
||||
import NewVersion from "./common/component/NewVersion";
|
||||
// import i18n (needs to be bundled ;))
|
||||
import './i18n';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
|
||||
|
||||
root.render(
|
||||
<>
|
||||
<Suspense fallback="loading">
|
||||
<Reset />
|
||||
<Toaster />
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<ReduxRoutes />
|
||||
</DndProvider>
|
||||
<MarkdownStyleOverride />
|
||||
</>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
register({
|
||||
|
||||
@@ -4,6 +4,7 @@ import usePinMessage from "../../../common/hook/usePinMessage";
|
||||
import PreviewMessage from "../../../common/component/Message/PreviewMessage";
|
||||
import IconSurprise from "../../../assets/icons/emoji.surprise.svg";
|
||||
import IconClose from "../../../assets/icons/close.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const Styled = styled.div`
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
@@ -86,6 +87,7 @@ type Props = {
|
||||
id: number;
|
||||
};
|
||||
const PinList: FC<Props> = ({ id }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { pins, unpinMessage, canPin } = usePinMessage(id);
|
||||
const handleUnpin = (evt: FormEvent<HTMLButtonElement>) => {
|
||||
const { mid } = evt.currentTarget.dataset;
|
||||
@@ -95,12 +97,12 @@ const PinList: FC<Props> = ({ id }: Props) => {
|
||||
const noPins = pins.length == 0;
|
||||
return (
|
||||
<Styled>
|
||||
<h4 className="head">Pinned Message({pins.length})</h4>
|
||||
<h4 className="head">{t("pinned_msg")}({pins.length})</h4>
|
||||
|
||||
{noPins ? (
|
||||
<div className="none">
|
||||
<IconSurprise />
|
||||
<div className="tip">This channel doesn’t have any pinned message yet.</div>
|
||||
<div className="tip">{t("pin_empty_tip")}</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="list">
|
||||
|
||||
@@ -28,11 +28,13 @@ import InviteModal from "../../../common/component/InviteModal";
|
||||
import LoadMore from "../LoadMore";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { AgoraConfig } from "../../../types/server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
type Props = {
|
||||
cid?: number;
|
||||
dropFiles?: File[];
|
||||
};
|
||||
function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
const { t } = useTranslation("chat");
|
||||
const { values: agoraConfig } = useConfig("agora");
|
||||
const {
|
||||
list: msgIds,
|
||||
@@ -105,7 +107,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
<Tooltip tip="Pin" placement="left">
|
||||
<Tooltip tip={t("pin")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
@@ -119,7 +121,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
</li>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
<Tooltip tip="Favorite" placement="left">
|
||||
<Tooltip tip={t("fav")} placement="left">
|
||||
<Tippy
|
||||
placement="left-start"
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
@@ -137,7 +139,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
className={`tool ${membersVisible ? "active" : ""}`}
|
||||
onClick={toggleMembersVisible}
|
||||
>
|
||||
<Tooltip tip="Channel Members" placement="left">
|
||||
<Tooltip tip={t("channel_members")} placement="left">
|
||||
<IconPeople />
|
||||
</Tooltip>
|
||||
</li>
|
||||
@@ -160,7 +162,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
{addVisible && (
|
||||
<div className="add" onClick={toggleAddVisible}>
|
||||
<img className="icon" src={addIcon} />
|
||||
<div className="txt">Add members</div>
|
||||
<div className="txt">{t("add_channel_members")}</div>
|
||||
</div>
|
||||
)}
|
||||
{memberIds.map((uid: number) => {
|
||||
@@ -186,12 +188,12 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
|
||||
<LoadMore pullUp={pullUp} />
|
||||
) : (
|
||||
<div className="info">
|
||||
<h2 className="title">Welcome to #{name} !</h2>
|
||||
<p className="desc">This is the start of the #{name} channel. </p>
|
||||
<h2 className="title">{t("welcome_channel", { name })}</h2>
|
||||
<p className="desc">{t("welcome_desc", { name })} </p>
|
||||
{loginUser?.is_admin && (
|
||||
<NavLink to={`/setting/channel/${cid}?f=${pathname}`} className="edit">
|
||||
<EditIcon className="icon" />
|
||||
Edit Channel
|
||||
{t("edit_channel")}
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,12 +15,14 @@ import StyledLink from "./styled";
|
||||
import ChannelIcon from "../../../common/component/ChannelIcon";
|
||||
import { getUnreadCount } from "../utils";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
interface IProps {
|
||||
id: number;
|
||||
setFiles: (files: File[]) => void;
|
||||
toggleRemoveConfirm: (id: number) => void;
|
||||
}
|
||||
const NavItem: FC<IProps> = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
const { t } = useTranslation();
|
||||
const { pathname } = useLocation();
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
@@ -118,20 +120,20 @@ const NavItem: FC<IProps> = ({ id, setFiles, toggleRemoveConfirm }) => {
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
title: t("action.mark_read"),
|
||||
underline: true,
|
||||
handler: handleReadAll
|
||||
},
|
||||
{
|
||||
title: muted ? "Unmute" : "Mute",
|
||||
title: muted ? t("action.unmute") : t("action.mute"),
|
||||
handler: handleMute
|
||||
},
|
||||
{
|
||||
title: "Invite People",
|
||||
title: t("action.invite_people"),
|
||||
handler: toggleInviteModalVisible
|
||||
},
|
||||
{
|
||||
title: "Delete Channel",
|
||||
title: t("channel.delete", { ns: "setting" }),
|
||||
danger: true,
|
||||
handler: toggleRemoveConfirm.bind(null, id)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import User from "../../../common/component/User";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { ArchiveMessage } from "../../../types/resource";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface IProps {
|
||||
uid: number;
|
||||
mid?: number;
|
||||
@@ -24,6 +26,7 @@ interface IProps {
|
||||
setFiles: (files: File[]) => void;
|
||||
}
|
||||
const NavItem: FC<IProps> = ({ uid, mid = 0, unreads, setFiles }) => {
|
||||
const { t } = useTranslation();
|
||||
const [previewMsg, setPreviewMsg] = useState<ArchiveMessage>();
|
||||
const { messages: normalizedMessages, normalizeMessage } = useNormalizeMessage();
|
||||
const dispatch = useDispatch();
|
||||
@@ -93,11 +96,11 @@ const NavItem: FC<IProps> = ({ uid, mid = 0, unreads, setFiles }) => {
|
||||
hideMenu={hideContextMenu}
|
||||
items={[
|
||||
{
|
||||
title: "Mark As Read",
|
||||
title: t("action.mark_read"),
|
||||
handler: handleReadAll
|
||||
},
|
||||
{
|
||||
title: "Hide Session",
|
||||
title: t("action.hide_session"),
|
||||
danger: true,
|
||||
handler: handleRemoveSession
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import FavoredMessage from "../../common/component/Message/FavoredMessage";
|
||||
import IconSurprise from "../../assets/icons/emoji.surprise.svg";
|
||||
import IconRemove from "../../assets/icons/close.svg";
|
||||
import useFavMessage from "../../common/hook/useFavMessage";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Styled = styled.div`
|
||||
padding: 16px;
|
||||
@@ -84,6 +85,7 @@ const Styled = styled.div`
|
||||
`;
|
||||
type Props = { cid?: number; uid?: number };
|
||||
const FavList: FC<Props> = ({ cid = null, uid = null }) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { favorites, removeFavorite } = useFavMessage({ cid, uid });
|
||||
const handleRemove = (evt: MouseEvent<HTMLButtonElement>) => {
|
||||
const { id = "" } = evt.currentTarget.dataset;
|
||||
@@ -93,11 +95,11 @@ const FavList: FC<Props> = ({ cid = null, uid = null }) => {
|
||||
const noFavs = favorites.length == 0;
|
||||
return (
|
||||
<Styled>
|
||||
<h4 className="head">Saved Message({favorites.length})</h4>
|
||||
<h4 className="head">{t('fav_msg')}({favorites.length})</h4>
|
||||
{noFavs ? (
|
||||
<div className="none">
|
||||
<IconSurprise />
|
||||
<div className="tip">This channel doesn’t have any saved message yet.</div>
|
||||
<div className="tip">{t("fav_empty_tip")}</div>
|
||||
</div>
|
||||
) : (
|
||||
<ul className="list">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
@@ -32,6 +33,7 @@ const Styled = styled.div`
|
||||
// type Props = {};
|
||||
|
||||
const GuestBlankPlaceholder = () => {
|
||||
const { t } = useTranslation("auth");
|
||||
const dispatch = useDispatch();
|
||||
const { clearLocalData } = useLogout();
|
||||
const navigateTo = useNavigate();
|
||||
@@ -43,10 +45,10 @@ const GuestBlankPlaceholder = () => {
|
||||
};
|
||||
return (
|
||||
<Styled>
|
||||
<h2 className="title">Welcome to {serverName} server</h2>
|
||||
<h2 className="title">{t("welcome", { name: serverName })}</h2>
|
||||
<div className="details">
|
||||
<span className="desc">Please sign in to send a message</span>
|
||||
<Button onClick={handleSignIn} className="small">{`Sign In`}</Button>
|
||||
<span className="desc">{t("guest_login_tip")}</span>
|
||||
<Button onClick={handleSignIn} className="small">{t("sign_in")}</Button>
|
||||
</div>
|
||||
</Styled>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useAppSelector } from "../../../app/store";
|
||||
import LoginTip from "./LoginTip";
|
||||
import useLicense from "../../../common/hook/useLicense";
|
||||
import LicenseUpgradeTip from "./LicenseOutdatedTip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
readonly?: boolean;
|
||||
@@ -33,6 +34,7 @@ const Layout: FC<Props> = ({
|
||||
context = "channel",
|
||||
to
|
||||
}) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { reachLimit } = useLicense();
|
||||
const { addStageFile } = useUploadFile({ context, id: to });
|
||||
const messagesContainer = useRef<HTMLDivElement>(null);
|
||||
@@ -129,7 +131,7 @@ const Layout: FC<Props> = ({
|
||||
>
|
||||
<div className={`box ${isActive ? "animate__animated animate__bounceIn" : ""}`}>
|
||||
<div className="inner">
|
||||
<h4 className="head">{`Send to ${ChatPrefixes[context]}${name}`}</h4>
|
||||
<h4 className="head">{`${t("send_to")} ${ChatPrefixes[context]}${name}`}</h4>
|
||||
<span className="intro">Photos accept jpg, png, max size limit to 10M.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { removeUserSession } from "../../../app/slices/message.user";
|
||||
import ContextMenu, { Item } from "../../../common/component/ContextMenu";
|
||||
import useUserOperation from "../../../common/hook/useUserOperation";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { t } from "i18next";
|
||||
type Props = {
|
||||
context: "user" | "channel";
|
||||
id: number;
|
||||
@@ -28,6 +30,7 @@ const SessionContextMenu: FC<Props> = ({
|
||||
setInviteChannelId,
|
||||
children
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { canCopyEmail, copyEmail, canDeleteChannel } = useUserOperation({
|
||||
uid: context == "user" ? id : undefined,
|
||||
cid: context == "channel" ? id : undefined
|
||||
@@ -71,45 +74,45 @@ const SessionContextMenu: FC<Props> = ({
|
||||
const items =
|
||||
context == "user"
|
||||
? [
|
||||
{
|
||||
title: "Mark As Read",
|
||||
handler: handleReadAll
|
||||
},
|
||||
canCopyEmail && {
|
||||
title: "Copy Email",
|
||||
handler: copyEmail
|
||||
},
|
||||
{
|
||||
title: "Hide Session",
|
||||
danger: true,
|
||||
handler: handleRemoveSession
|
||||
}
|
||||
]
|
||||
{
|
||||
title: t("action.mark_read"),
|
||||
handler: handleReadAll
|
||||
},
|
||||
canCopyEmail && {
|
||||
title: t("action.copy_email"),
|
||||
handler: copyEmail
|
||||
},
|
||||
{
|
||||
title: t("action.hide_session"),
|
||||
danger: true,
|
||||
handler: handleRemoveSession
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
title: "Settings",
|
||||
underline: true,
|
||||
handler: handleChannelSetting
|
||||
},
|
||||
{
|
||||
title: "Mark As Read",
|
||||
// underline: true
|
||||
handler: handleReadAll
|
||||
},
|
||||
{
|
||||
title: channelMuted ? "Unmute" : "Mute",
|
||||
handler: handleChannelMute
|
||||
},
|
||||
{
|
||||
title: "Invite People",
|
||||
handler: setInviteChannelId.bind(null, id)
|
||||
},
|
||||
canDeleteChannel && {
|
||||
title: "Delete Channel",
|
||||
danger: true,
|
||||
handler: deleteChannel.bind(null, id)
|
||||
}
|
||||
];
|
||||
{
|
||||
title: t("setting"),
|
||||
underline: true,
|
||||
handler: handleChannelSetting
|
||||
},
|
||||
{
|
||||
title: t("action.mark_read"),
|
||||
// underline: true
|
||||
handler: handleReadAll
|
||||
},
|
||||
{
|
||||
title: channelMuted ? t("action.unmute") : t("action.mute"),
|
||||
handler: handleChannelMute
|
||||
},
|
||||
{
|
||||
title: t("action.invite_people"),
|
||||
handler: setInviteChannelId.bind(null, id)
|
||||
},
|
||||
canDeleteChannel && {
|
||||
title: t("action.delete_channel"),
|
||||
danger: true,
|
||||
handler: deleteChannel.bind(null, id)
|
||||
}
|
||||
];
|
||||
return (
|
||||
<Tippy
|
||||
interactive
|
||||
|
||||
+25
-22
@@ -10,32 +10,35 @@ import IconChannel from "../../assets/icons/channel.svg";
|
||||
import { ContentTypes } from "../../app/config";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { Favorite } from "../../app/slices/favorites";
|
||||
const Filters = [
|
||||
{
|
||||
icon: <IconUnknown className="icon" />,
|
||||
title: "All Items",
|
||||
filter: ""
|
||||
},
|
||||
{
|
||||
icon: <IconImage className="icon" />,
|
||||
title: "Images",
|
||||
filter: "image"
|
||||
},
|
||||
{
|
||||
icon: <IconVideo className="icon" />,
|
||||
title: "Videos",
|
||||
filter: "video"
|
||||
},
|
||||
{
|
||||
icon: <IconAudio className="icon" />,
|
||||
title: "Audios",
|
||||
filter: "audio"
|
||||
}
|
||||
];
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type filter = "audio" | "video" | "image" | "";
|
||||
function FavsPage() {
|
||||
const { t } = useTranslation("fav");
|
||||
const [filter, setFilter] = useState<filter>("");
|
||||
const [favs, setFavs] = useState<Favorite[]>([]);
|
||||
const Filters = [
|
||||
{
|
||||
icon: <IconUnknown className="icon" />,
|
||||
title: t("all_items"),
|
||||
filter: ""
|
||||
},
|
||||
{
|
||||
icon: <IconImage className="icon" />,
|
||||
title: t("image"),
|
||||
filter: "image"
|
||||
},
|
||||
{
|
||||
icon: <IconVideo className="icon" />,
|
||||
title: t("video"),
|
||||
filter: "video"
|
||||
},
|
||||
{
|
||||
icon: <IconAudio className="icon" />,
|
||||
title: t("audio"),
|
||||
filter: "audio"
|
||||
}
|
||||
];
|
||||
const { favorites, channelData, userData } = useAppSelector((store) => {
|
||||
return {
|
||||
favorites: store.favorites,
|
||||
|
||||
@@ -15,8 +15,10 @@ import UserIcon from "../../assets/icons/user.svg";
|
||||
import FavIcon from "../../assets/icons/bookmark.svg";
|
||||
import FolderIcon from "../../assets/icons/folder.svg";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// const routes = ["/setting", "/setting/channel/:cid"];
|
||||
function HomePage() {
|
||||
const { t } = useTranslation();
|
||||
const isHomePath = useMatch(`/`);
|
||||
const isChatHomePath = useMatch(`/chat`);
|
||||
const { pathname } = useLocation();
|
||||
@@ -66,22 +68,22 @@ function HomePage() {
|
||||
}}
|
||||
to={chatNav}
|
||||
>
|
||||
<Tooltip tip="Chat">
|
||||
<Tooltip tip={t("chat")}>
|
||||
<ChatIcon />
|
||||
</Tooltip>
|
||||
</NavLink>
|
||||
<NavLink className="link" to={userNav}>
|
||||
<Tooltip tip="Members">
|
||||
<Tooltip tip={t("members")}>
|
||||
<UserIcon />
|
||||
</Tooltip>
|
||||
</NavLink>
|
||||
<NavLink className="link" to={"/favs"}>
|
||||
<Tooltip tip="Saved Items">
|
||||
<Tooltip tip={t("favs")}>
|
||||
<FavIcon />
|
||||
</Tooltip>
|
||||
</NavLink>
|
||||
<NavLink className="link" to={"/files"}>
|
||||
<Tooltip tip="Files">
|
||||
<Tooltip tip={t("files")}>
|
||||
<FolderIcon />
|
||||
</Tooltip>
|
||||
</NavLink>
|
||||
|
||||
@@ -21,6 +21,7 @@ const InvitePage = lazy(() => import("./invite"));
|
||||
const SettingChannelPage = lazy(() => import("./settingChannel"));
|
||||
const SettingPage = lazy(() => import("./setting"));
|
||||
const ResourceManagement = lazy(() => import("./resources"));
|
||||
const GuestLogin = lazy(() => import("./guest"));
|
||||
import RequireAuth from "../common/component/RequireAuth";
|
||||
import RequireNoAuth from "../common/component/RequireNoAuth";
|
||||
import Meta from "../common/component/Meta";
|
||||
@@ -28,7 +29,6 @@ import HomePage from "./home";
|
||||
import ChatPage from "./chat";
|
||||
import Loading from "../common/component/Loading";
|
||||
import store, { useAppSelector } from "../app/store";
|
||||
import GuestLogin from "./guest";
|
||||
let toastId: string;
|
||||
const PageRoutes = () => {
|
||||
const {
|
||||
|
||||
@@ -6,6 +6,7 @@ import metamaskSvg from "../../assets/icons/metamask.svg?url";
|
||||
import { StyledSocialButton } from "./styled";
|
||||
import Onboarding from "@metamask/onboarding";
|
||||
import { LoginCredential } from "../../types/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// import toast from "react-hot-toast";
|
||||
|
||||
export default function MetamaskLoginButton({
|
||||
@@ -13,6 +14,7 @@ export default function MetamaskLoginButton({
|
||||
}: {
|
||||
login: (params: LoginCredential) => void;
|
||||
}) {
|
||||
const { t } = useTranslation("auth");
|
||||
const [requesting, setRequesting] = useState(false);
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [getNonce] = useLazyGetMetamaskNonceQuery();
|
||||
@@ -91,7 +93,7 @@ export default function MetamaskLoginButton({
|
||||
return (
|
||||
<StyledSocialButton disabled={requesting} onClick={handleMetamaskLogin}>
|
||||
<img className="icon" src={metamaskSvg} alt="meta mask icon" />
|
||||
Sign in with MetaMask
|
||||
{t("login.metamask")}
|
||||
</StyledSocialButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { StyledSocialButton } from "./styled";
|
||||
import StyledButton from "../../common/component/styled/Button";
|
||||
import OidcLoginEntry from "./OidcLoginEntry";
|
||||
import { OIDCConfig } from "../../types/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledOidcLoginModal = styled(StyledModal)`
|
||||
text-align: center;
|
||||
@@ -31,6 +32,7 @@ interface IProps {
|
||||
issuers?: OIDCConfig[];
|
||||
}
|
||||
const OidcLoginButton: FC<IProps> = ({ issuers }) => {
|
||||
const { t } = useTranslation("auth");
|
||||
const [modal, setModal] = useState(false);
|
||||
if (!issuers) return null;
|
||||
return (
|
||||
@@ -40,7 +42,7 @@ const OidcLoginButton: FC<IProps> = ({ issuers }) => {
|
||||
setModal(true);
|
||||
}}
|
||||
>
|
||||
Sign in with OIDC
|
||||
{t("login.oidc")}
|
||||
</StyledSocialButton>
|
||||
{modal && (
|
||||
<Modal id="modal-modal">
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
|
||||
export default function MagicLinkLogin() {
|
||||
const { t } = useTranslation("auth");
|
||||
const navigate = useNavigate();
|
||||
const handleSignUp = () => {
|
||||
navigate("/register");
|
||||
@@ -9,8 +11,8 @@ export default function MagicLinkLogin() {
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 mt-7 text-sm text-[#667085] justify-center">
|
||||
<span>Don’t have an account?</span>
|
||||
<a className="text-[#22d3ee]" onClick={handleSignUp}>Sign up</a>
|
||||
<span>{t("login.no_account")}</span>
|
||||
<a className="text-[#22d3ee] cursor-pointer" onClick={handleSignUp}>{t("sign_up")}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
|
||||
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
|
||||
import GithubLoginButton from "../../common/component/GithubLoginButton";
|
||||
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation("auth");
|
||||
const { data: enableSMTP } = useGetSMTPStatusQuery();
|
||||
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
|
||||
const { clientId } = useGoogleAuthConfig();
|
||||
@@ -128,8 +130,8 @@ export default function LoginPage() {
|
||||
<div className="form">
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Login to VoceChat</h2>
|
||||
<span className="desc">Please enter your details.</span>
|
||||
<h2 className="title">{t("login.title")}</h2>
|
||||
<span className="desc">{t("login.desc")}</span>
|
||||
</div>
|
||||
<form onSubmit={handleLogin}>
|
||||
<Input
|
||||
@@ -137,7 +139,7 @@ export default function LoginPage() {
|
||||
name="email"
|
||||
value={email}
|
||||
required
|
||||
placeholder="Enter your email"
|
||||
placeholder={t("placeholder_email") || ""}
|
||||
data-type="email"
|
||||
onChange={handleInput}
|
||||
/>
|
||||
@@ -149,10 +151,10 @@ export default function LoginPage() {
|
||||
required
|
||||
data-type="password"
|
||||
onChange={handleInput}
|
||||
placeholder="Enter your password"
|
||||
placeholder={t("placeholder_pwd") || ""}
|
||||
/>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? "Signing" : `Sign in`}
|
||||
{isLoading ? "Signing" : t("sign_in")}
|
||||
</Button>
|
||||
</form>
|
||||
{hasDivider && <hr className="or" />}
|
||||
|
||||
@@ -11,6 +11,7 @@ import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
|
||||
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
|
||||
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
|
||||
import GithubLoginButton from "../../common/component/GithubLoginButton";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface AuthForm {
|
||||
email: string;
|
||||
@@ -19,6 +20,7 @@ interface AuthForm {
|
||||
}
|
||||
|
||||
export default function Reg() {
|
||||
const { t } = useTranslation("auth");
|
||||
const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] =
|
||||
useSendRegMagicLinkMutation();
|
||||
const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery();
|
||||
@@ -107,8 +109,8 @@ export default function Reg() {
|
||||
<>
|
||||
<div className="tips">
|
||||
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
|
||||
<h2 className="title">Sign Up to VoceChat</h2>
|
||||
<span className="desc">Please enter your details.</span>
|
||||
<h2 className="title">{t("reg.title")}</h2>
|
||||
<span className="desc">{t("reg.desc")}</span>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleReg} autoSave={"false"} autoComplete={"true"}>
|
||||
@@ -117,7 +119,7 @@ export default function Reg() {
|
||||
name="email"
|
||||
value={email}
|
||||
required
|
||||
placeholder="Enter email"
|
||||
placeholder={t("placeholder_email") || ""}
|
||||
data-type="email"
|
||||
onChange={handleInput}
|
||||
/>
|
||||
@@ -129,7 +131,7 @@ export default function Reg() {
|
||||
required
|
||||
data-type="password"
|
||||
onChange={handleInput}
|
||||
placeholder="Enter password"
|
||||
placeholder={t("placeholder_pwd") || ""}
|
||||
/>
|
||||
<Input
|
||||
required
|
||||
@@ -139,10 +141,10 @@ export default function Reg() {
|
||||
value={confirmPassword}
|
||||
data-type="confirmPassword"
|
||||
onChange={handleInput}
|
||||
placeholder="Confirm Password"
|
||||
placeholder={t("placeholder_confirm_pwd") || ""}
|
||||
></Input>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
{isLoading ? "Signing Up" : `Sign Up`}
|
||||
{isLoading ? "Signing Up" : t("sign_up")}
|
||||
</Button>
|
||||
</form>
|
||||
<hr className="or" />
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function SignInLink({ token }: { token?: string }) {
|
||||
const { t } = useTranslation("auth");
|
||||
const handleSignIn = () => {
|
||||
location.href = "/#/login";
|
||||
};
|
||||
@@ -14,8 +16,8 @@ export default function SignInLink({ token }: { token?: string }) {
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 mt-7 text-sm text-[#667085] justify-center">
|
||||
<span>Have an account?</span>
|
||||
<a onClick={handleSignIn} className="text-[#22d3ee]">Sign In</a>
|
||||
<span>{t("reg.have_account")}</span>
|
||||
<a onClick={handleSignIn} className="text-[#22d3ee] cursor-pointer">{t("sign_in")}</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import FilterChannel from "./Channel";
|
||||
import FilterType, { FileTypes } from "./Type";
|
||||
import ArrowDown from "../../../assets/icons/arrow.down.svg";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Styled = styled.div`
|
||||
/* padding: 20px 0; */
|
||||
@@ -44,6 +45,7 @@ const Styled = styled.div`
|
||||
`;
|
||||
|
||||
export default function Filter({ filter, updateFilter }) {
|
||||
const { t } = useTranslation("file");
|
||||
const [filtersVisible, setFiltersVisible] = useState({
|
||||
channel: false,
|
||||
date: false,
|
||||
@@ -93,7 +95,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
{from && (
|
||||
<Avatar className="avatar" name={userMap[from].name} url={userMap[from].avatar} />
|
||||
)}
|
||||
<span className="txt">From {from && userMap[from].name}</span>
|
||||
<span className="txt">{t("from")} {from && userMap[from].name}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
</div>
|
||||
</Tippy>
|
||||
@@ -108,7 +110,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
className={`filter ${channel && "selected"}`}
|
||||
onClick={toggleFilterVisible.bind(null, { channel: true })}
|
||||
>
|
||||
<span className="txt">{channel ? `In ${channelMap[channel].name}` : `Channel`}</span>
|
||||
<span className="txt">{channel ? `In ${channelMap[channel].name}` : t("channel")}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
</div>
|
||||
</Tippy>
|
||||
@@ -123,7 +125,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
className={`filter ${type && "selected"}`}
|
||||
onClick={toggleFilterVisible.bind(null, { type: true })}
|
||||
>
|
||||
<span className="txt">{type ? FileTypes[type].title : "Type"}</span>
|
||||
<span className="txt">{type ? FileTypes[type].title : t("type")}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
</div>
|
||||
</Tippy>
|
||||
@@ -138,7 +140,7 @@ export default function Filter({ filter, updateFilter }) {
|
||||
className={`filter ${date && "selected"}`}
|
||||
onClick={toggleFilterVisible.bind(null, { date: true })}
|
||||
>
|
||||
<span className="txt">{date ? Dates[date].title : "Date"}</span>
|
||||
<span className="txt">{date ? Dates[date].title : t("date")}</span>
|
||||
<ArrowDown className="arrow" />
|
||||
</div>
|
||||
</Tippy>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChangeEvent, FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import iconSearch from "../../assets/icons/search.svg?url";
|
||||
|
||||
@@ -32,6 +33,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (updateSearchValue) {
|
||||
updateSearchValue(evt.target.value);
|
||||
@@ -40,7 +42,7 @@ const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false
|
||||
|
||||
return (
|
||||
<Styled className={embed ? "embed" : ""}>
|
||||
<input value={value} onChange={handleChange} className="search" placeholder="Search..." />
|
||||
<input value={value} onChange={handleChange} className="search" placeholder={`${t("action.search")}...`} />
|
||||
</Styled>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "../../app/services/server";
|
||||
import useConfig from "../../common/hook/useConfig";
|
||||
import { LoginConfig } from "../../types/server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledConfirm = styled.div`
|
||||
padding: 12px;
|
||||
@@ -61,6 +62,7 @@ const Styled = styled.div`
|
||||
`;
|
||||
|
||||
export default function APIConfig() {
|
||||
const { t } = useTranslation(["setting", "common"]);
|
||||
const { updateConfig, values } = useConfig("login");
|
||||
const { data } = useGetThirdPartySecretQuery();
|
||||
const [updateSecret, { data: updatedSecret, isSuccess, isLoading }] =
|
||||
@@ -83,7 +85,7 @@ export default function APIConfig() {
|
||||
data-checked={thirdParty}
|
||||
/>
|
||||
<div className="input">
|
||||
<label htmlFor="secret">API Secure Key:</label>
|
||||
<label htmlFor="secret"> {t("third_app.key")}:</label>
|
||||
<Input disabled={!thirdParty} type="password" id="secret" value={updatedSecret || data} />
|
||||
</div>
|
||||
<Tippy
|
||||
@@ -93,24 +95,23 @@ export default function APIConfig() {
|
||||
content={
|
||||
<StyledConfirm>
|
||||
<div className="tip">
|
||||
Are you sure to update API secret? Previous secret will be invalided
|
||||
{t("third_app.update_tip")}
|
||||
</div>
|
||||
<div className="btns">
|
||||
<Button onClick={() => hideAll()} className="cancel small">
|
||||
Cancel
|
||||
{t("action.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button disabled={isLoading} className="small danger" onClick={() => updateSecret()}>
|
||||
Yes
|
||||
{t("action.yes", { ns: "common" })}
|
||||
</Button>
|
||||
</div>
|
||||
</StyledConfirm>
|
||||
}
|
||||
>
|
||||
<Button disabled={!thirdParty}>Update Secret</Button>
|
||||
<Button disabled={!thirdParty}> {t("third_app.update")}</Button>
|
||||
</Tippy>
|
||||
<div className="tip">
|
||||
Tip: The security key agreed between the server and the third-party app is used to encrypt
|
||||
the communication data.
|
||||
{t("third_app.key_tip")}
|
||||
</div>
|
||||
</Styled>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useState, HTMLAttributes } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import useLicense from "../../common/hook/useLicense";
|
||||
import Button from "../../../common/component/styled/Button";
|
||||
import useLicense from "../../../common/hook/useLicense";
|
||||
import LicensePriceListModal from "./LicensePriceListModal";
|
||||
import clsx from "clsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ItemProps extends HTMLAttributes<HTMLSpanElement> { label: string, data?: string | number | string[], foldable?: boolean }
|
||||
const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
|
||||
@@ -22,6 +23,7 @@ const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
|
||||
|
||||
};
|
||||
export default function License() {
|
||||
const { t } = useTranslation("setting");
|
||||
const { license: licenseInfo, reachLimit } = useLicense();
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [base58Fold, setBase58Fold] = useState(true);
|
||||
@@ -38,25 +40,25 @@ export default function License() {
|
||||
<>
|
||||
<div className="max-w-3xl flex flex-col justify-start items-start gap-4">
|
||||
<div className={clsx('relative w-full p-3 rounded border-solid border flex flex-col gap-4 shadow', reachLimit ? "border-red-600 bg-red-200/50" : "border-green-600 bg-green-200/50")}>
|
||||
<Item label="Signed" data={licenseInfo?.sign ? "Yes" : "Not Yet"} />
|
||||
<Item label="Domains" data={licenseInfo?.domains} />
|
||||
<Item label="User Limit" data={licenseInfo?.user_limit == 99999 ? "No Limit" : licenseInfo?.user_limit} />
|
||||
<Item label="Expired At" data={dayjs(licenseInfo?.expired_at).format("YYYY-MM-DD h:mm:ss A")} />
|
||||
<Item label="Created At" data={dayjs(licenseInfo?.created_at).format("YYYY-MM-DD h:mm:ss A")} />
|
||||
<Item label="License Value" data={licenseInfo?.base58} foldable={base58Fold} title={base58Fold ? "Click to see full text" : "Click to fold text"}
|
||||
<Item label={t("license.signed")} data={licenseInfo?.sign ? "Yes" : "Not Yet"} />
|
||||
<Item label={t("license.domain")} data={licenseInfo?.domains} />
|
||||
<Item label={t("license.user_limit")} data={licenseInfo?.user_limit == 99999 ? "No Limit" : licenseInfo?.user_limit} />
|
||||
<Item label={t("license.expire")} data={dayjs(licenseInfo?.expired_at).format("YYYY-MM-DD h:mm:ss A")} />
|
||||
<Item label={t("license.create")} data={dayjs(licenseInfo?.created_at).format("YYYY-MM-DD h:mm:ss A")} />
|
||||
<Item label={t("license.value")} data={licenseInfo?.base58} foldable={base58Fold} title={base58Fold ? "Click to see full text" : "Click to fold text"}
|
||||
onClick={handleLicenseValueToggle} />
|
||||
</div>
|
||||
<Button onClick={handleRenewLicense}>Renew License</Button>
|
||||
<Button onClick={handleRenewLicense}>{t("license.renew")}</Button>
|
||||
<div className="flex flex-col gap-4 bg-primary-500 text-white rounded drop-shadow-xl p-5">
|
||||
<h2 className="text-2xl font-bold">A chance to get a free license upgrade! 🎁</h2>
|
||||
<h2 className="text-2xl font-bold">{t("license.tip.title")} 🎁</h2>
|
||||
<p className="text-base flex flex-col"><span>
|
||||
Getting a free license upgrade by joining our <em className="font-bold">User Test Session</em>
|
||||
{t("license.tip.user_test")}
|
||||
</span>
|
||||
<span>
|
||||
Book a time here: <a className="underline text-lg text-green-200" href="https://calendly.com/hansu/han-meeting" target="_blank" rel="noopener noreferrer">https://calendly.com/hansu/han-meeting</a>
|
||||
{t("license.tip.booking")} <a className="underline text-lg text-green-200" href="https://calendly.com/hansu/han-meeting" target="_blank" rel="noopener noreferrer">https://calendly.com/hansu/han-meeting</a>
|
||||
</span>
|
||||
<span>
|
||||
Or, add WeChat for more information: <em className="text-lg text-green-200">yanggc_2013</em>
|
||||
{t("license.tip.wechat")}<em className="text-lg text-green-200">yanggc_2013</em>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
+12
-10
@@ -1,12 +1,13 @@
|
||||
import { FC, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import Modal from "../../common/component/Modal";
|
||||
import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import StyledRadio from "../../common/component/styled/Radio";
|
||||
import Modal from "../../../common/component/Modal";
|
||||
import StyledModal from "../../../common/component/styled/Modal";
|
||||
import Button from "../../../common/component/styled/Button";
|
||||
import StyledRadio from "../../../common/component/styled/Radio";
|
||||
|
||||
import { useGetLicensePaymentUrlMutation } from "../../app/services/server";
|
||||
import { LicensePriceList } from "../../app/config";
|
||||
import { useGetLicensePaymentUrlMutation } from "../../../app/services/server";
|
||||
import { LicensePriceList } from "../../../app/config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// import { LicenseMetadata, RenewLicense } from "../../types/server";
|
||||
|
||||
interface Props {
|
||||
@@ -15,6 +16,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
|
||||
const { t } = useTranslation(["setting", "common"]);
|
||||
const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation();
|
||||
const [selectPrice, setSelectPrice] = useState(
|
||||
`${LicensePriceList[0].pid}|${LicensePriceList[0].limit}`
|
||||
@@ -46,15 +48,15 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title="Renew License"
|
||||
description="Please select the price"
|
||||
title={t("license.renew") || ""}
|
||||
description={t("license.renew_select") || ""}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal} className="ghost">
|
||||
Cancel
|
||||
{t("action.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button disabled={isLoading || isSuccess} onClick={handleRenew} className="danger">
|
||||
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : "Renew"}
|
||||
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : t("license.renew")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import Checkbox from "../../common/component/styled/Checkbox";
|
||||
import useLogout from "../../common/hook/useLogout";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledConfirm = styled(StyledModal)`
|
||||
.clear {
|
||||
@@ -32,6 +33,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
|
||||
const { t } = useTranslation();
|
||||
const [clearLocal, setClearLocal] = useState(false);
|
||||
const { logout, exited, exiting, clearLocalData } = useLogout();
|
||||
const handleLogout = () => {
|
||||
@@ -58,9 +60,9 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
|
||||
description="Are you sure want to log out this account?"
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={closeModal}>{t("action.cancel")}</Button>
|
||||
<Button onClick={handleLogout} className="danger">
|
||||
{exiting ? "Logging out" : "Log Out"}
|
||||
{exiting ? "Logging out" : t("action.logout")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import ProfileBasicEditModal from "./ProfileBasicEditModal";
|
||||
import RemoveAccountConfirmModal from "./RemoveAccountConfirmModal";
|
||||
import UpdatePasswordModal from "./UpdatePasswordModal";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -99,24 +100,25 @@ const StyledWrapper = styled.div`
|
||||
border-radius: 8px;
|
||||
}
|
||||
`;
|
||||
const EditModalInfo = {
|
||||
name: {
|
||||
label: "Username",
|
||||
title: "Change your username",
|
||||
intro: "Enter a new username."
|
||||
},
|
||||
email: {
|
||||
label: "Email",
|
||||
title: "Change your email",
|
||||
intro: "Enter a new email."
|
||||
}
|
||||
};
|
||||
type EditFields = "name" | "email";
|
||||
export default function MyAccount() {
|
||||
const { t } = useTranslation(["member", "common"]);
|
||||
const [passwordModal, setPasswordModal] = useState(false);
|
||||
const [editModal, setEditModal] = useState<EditFields | null>(null);
|
||||
const [removeConfirmVisible, setRemoveConfirmVisible] = useState(false);
|
||||
const [uploadAvatar, { isSuccess: uploadSuccess }] = useUpdateAvatarMutation();
|
||||
const EditModalInfo = {
|
||||
name: {
|
||||
label: t("username"),
|
||||
title: t("change_name"),
|
||||
intro: t("change_name_desc")
|
||||
},
|
||||
email: {
|
||||
label: t("email"),
|
||||
title: t("change_email"),
|
||||
intro: t("change_email_desc")
|
||||
}
|
||||
};
|
||||
|
||||
const loginUser = useAppSelector((store) => {
|
||||
return store.users.byId[store.authData.user?.uid || 0];
|
||||
@@ -156,43 +158,39 @@ export default function MyAccount() {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="info">
|
||||
<span className="label">username</span>
|
||||
<span className="label">{t("username")}</span>
|
||||
<span className="txt">
|
||||
{name} <span className="gray"> #{uid}</span>
|
||||
</span>
|
||||
</div>
|
||||
<button data-edit="name" onClick={handleBasicEdit} className="btn">
|
||||
Edit
|
||||
{t("action.edit", { ns: "common" })}
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="info">
|
||||
<span className="label">email</span>
|
||||
<span className="label">{t("email")}</span>
|
||||
<span className="txt">{email}</span>
|
||||
</div>
|
||||
<button data-edit="email" onClick={handleBasicEdit} className="btn">
|
||||
Edit
|
||||
{t("action.edit", { ns: "common" })}
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="info">
|
||||
<span className="label">password</span>
|
||||
<span className="label">{t("password")}</span>
|
||||
<span className="txt">*********</span>
|
||||
</div>
|
||||
<button onClick={togglePasswordModal} className="btn">
|
||||
Edit
|
||||
{t("action.edit", { ns: "common" })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/* uid 1 是初始账户,不能删 */}
|
||||
{uid != 1 && (
|
||||
<div className="danger">
|
||||
<h4 className="head">Account Removal</h4>
|
||||
<div className="desc">
|
||||
Disabling your account means you can recover it at any time after taking this action.
|
||||
</div>
|
||||
<button className="btn" onClick={toggleRemoveAccountModalVisible}>
|
||||
Delete Account
|
||||
{t("delete_account")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
import { useState, useEffect, ChangeEvent } from "react";
|
||||
import styled from "styled-components";
|
||||
import toast from "react-hot-toast";
|
||||
import { useUpdateServerMutation, useUpdateLogoMutation } from "../../app/services/server";
|
||||
import LogoUploader from "../../common/component/AvatarUploader";
|
||||
import Input from "../../common/component/styled/Input";
|
||||
import Label from "../../common/component/styled/Label";
|
||||
import Textarea from "../../common/component/styled/Textarea";
|
||||
import SaveTip from "../../common/component/SaveTip";
|
||||
import StyledRadio from "../../common/component/styled/Radio";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { LoginConfig, WhoCanSignUp } from "../../types/server";
|
||||
import useConfig from "../../common/hook/useConfig";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 512px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
.logo {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.preview {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
.tip {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #374151;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 14px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #1fe1f9;
|
||||
background: #ecfeff;
|
||||
border: 1px solid #ecfeff;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
margin-bottom: 64px;
|
||||
.input {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
> .setting {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
> .label {
|
||||
font-weight: 500;
|
||||
}
|
||||
> .tip {
|
||||
font-weight: 400;
|
||||
color: #667085;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
> form {
|
||||
margin-top: 16px;
|
||||
width: 512px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Overview() {
|
||||
const { loginUser, server } = useAppSelector((store) => {
|
||||
return { loginUser: store.authData.user, server: store.server };
|
||||
});
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [serverValues, setServerValues] = useState<typeof server>(server);
|
||||
const { values: loginConfig, updateConfig: updateLoginConfig } = useConfig("login");
|
||||
const [updateServer] = useUpdateServerMutation();
|
||||
const [uploadLogo, { isSuccess: uploadSuccess }] = useUpdateLogoMutation();
|
||||
const handleUpdateServer = () => {
|
||||
const { name, description } = serverValues;
|
||||
updateServer({ name, description });
|
||||
};
|
||||
const handleUpdateWhoCanSignUp = (value: WhoCanSignUp) => {
|
||||
updateLoginConfig({ ...loginConfig, who_can_sign_up: value });
|
||||
};
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const newValue = evt.target.value;
|
||||
const { type = "" } = evt.target.dataset;
|
||||
setServerValues((prev) => {
|
||||
return { ...prev, [type]: newValue };
|
||||
});
|
||||
};
|
||||
const handleReset = () => {
|
||||
setServerValues(server);
|
||||
};
|
||||
const handleGuestToggle = (v: "true" | "false") => {
|
||||
const guest = v === "true";
|
||||
updateLoginConfig({ ...loginConfig, guest });
|
||||
};
|
||||
useEffect(() => {
|
||||
if (uploadSuccess) {
|
||||
toast.success("Update logo successfully!");
|
||||
}
|
||||
}, [uploadSuccess]);
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
setServerValues(server);
|
||||
}
|
||||
}, [server]);
|
||||
useEffect(() => {
|
||||
if (server && serverValues) {
|
||||
const { name, description } = serverValues;
|
||||
const { name: oName, description: oDescription } = server;
|
||||
if (oName !== name || oDescription !== description) {
|
||||
setChanged(true);
|
||||
} else {
|
||||
setChanged(false);
|
||||
}
|
||||
}
|
||||
}, [server, serverValues]);
|
||||
if (!serverValues || !loginConfig) return null;
|
||||
const { name, description, logo } = serverValues;
|
||||
const { who_can_sign_up: whoCanSignUp, guest } = loginConfig as LoginConfig;
|
||||
const isAdmin = loginUser?.is_admin;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="logo">
|
||||
<div className="preview">
|
||||
<LogoUploader
|
||||
disabled={!isAdmin}
|
||||
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
|
||||
name={name}
|
||||
uploadImage={uploadLogo}
|
||||
/>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="upload">
|
||||
<div className="tip">
|
||||
Minimum size is 128x128, We recommend at least 512x512 for the server. Max size
|
||||
limited to 5M.
|
||||
</div>
|
||||
{/* <button className="btn">Upload Image</button> */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="inputs">
|
||||
<div className="input">
|
||||
<Label htmlFor="name">Server Name</Label>
|
||||
<Input
|
||||
disabled={!isAdmin}
|
||||
data-type="name"
|
||||
onChange={handleChange}
|
||||
value={name}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Server Name"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Server Description</Label>
|
||||
<Textarea
|
||||
disabled={!isAdmin}
|
||||
data-type="description"
|
||||
onChange={handleChange}
|
||||
value={description ?? ""}
|
||||
rows={4}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Tell the world a bit about this server"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="setting">
|
||||
<p className="label">Default Sign Up</p>
|
||||
<p className="tip">Who can sign up this server.</p>
|
||||
<StyledRadio
|
||||
options={["Everyone", "Invitation Link Only"]}
|
||||
values={["EveryOne", "InvitationOnly"]}
|
||||
value={whoCanSignUp}
|
||||
onChange={(v: WhoCanSignUp) => {
|
||||
handleUpdateWhoCanSignUp(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="setting">
|
||||
<p className="label">Guest Mode</p>
|
||||
<p className="tip">
|
||||
<span className="txt">
|
||||
If enabled, visitors will see public channels on this server.
|
||||
</span>
|
||||
</p>
|
||||
<StyledRadio
|
||||
options={["Enabled", "Disabled"]}
|
||||
values={["true", "false"]}
|
||||
value={String(guest)}
|
||||
onChange={(v) => {
|
||||
console.log("wtff", v);
|
||||
|
||||
handleGuestToggle(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{changed && <SaveTip saveHandler={handleUpdateServer} resetHandler={handleReset} />}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// import { useState, useEffect, ChangeEvent } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import StyledRadio from "../../../common/component/styled/Radio";
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import { LoginConfig, WhoCanSignUp } from "../../../types/server";
|
||||
import useConfig from "../../../common/hook/useConfig";
|
||||
import Server from './server';
|
||||
import Language from './language';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
width: 512px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
.logo {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
.preview {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
.tip {
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
}
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 24px;
|
||||
margin-bottom: 64px;
|
||||
.input {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
> .setting {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
> .label {
|
||||
font-weight: 500;
|
||||
}
|
||||
> .tip {
|
||||
font-weight: 400;
|
||||
color: #667085;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
> form {
|
||||
margin-top: 16px;
|
||||
width: 512px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Overview() {
|
||||
const { t } = useTranslation("setting");
|
||||
const { loginUser } = useAppSelector((store) => {
|
||||
return { loginUser: store.authData.user };
|
||||
});
|
||||
|
||||
|
||||
const { values: loginConfig, updateConfig: updateLoginConfig } = useConfig("login");
|
||||
|
||||
|
||||
const handleUpdateWhoCanSignUp = (value: WhoCanSignUp) => {
|
||||
updateLoginConfig({ ...loginConfig, who_can_sign_up: value });
|
||||
};
|
||||
|
||||
const handleGuestToggle = (v: "true" | "false") => {
|
||||
const guest = v === "true";
|
||||
updateLoginConfig({ ...loginConfig, guest });
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (!loginConfig) return null;
|
||||
|
||||
const { who_can_sign_up: whoCanSignUp, guest } = loginConfig as LoginConfig;
|
||||
const isAdmin = loginUser?.is_admin;
|
||||
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<Server />
|
||||
{isAdmin && (
|
||||
<>
|
||||
<div className="setting">
|
||||
<p className="label">{t("overview.sign_up.title")}</p>
|
||||
<p className="tip">{t("overview.sign_up.desc")}</p>
|
||||
<StyledRadio
|
||||
options={[t("overview.sign_up.everyone"), t("overview.sign_up.invite")]}
|
||||
values={["EveryOne", "InvitationOnly"]}
|
||||
value={whoCanSignUp}
|
||||
onChange={(v: WhoCanSignUp) => {
|
||||
handleUpdateWhoCanSignUp(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="setting">
|
||||
<p className="label">{t("overview.guest_mode.title")}</p>
|
||||
<p className="tip">
|
||||
<span className="txt">
|
||||
{t("overview.guest_mode.desc")}
|
||||
</span>
|
||||
</p>
|
||||
<StyledRadio
|
||||
options={[t("overview.guest_mode.enable"), t("overview.guest_mode.disable")]}
|
||||
values={["true", "false"]}
|
||||
value={String(guest)}
|
||||
onChange={(v) => {
|
||||
console.log("wtff", v);
|
||||
|
||||
handleGuestToggle(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Language />
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// import React from 'react'
|
||||
import dayjs from 'dayjs';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import StyledRadio from "../../../common/component/styled/Radio";
|
||||
|
||||
// type Props = {}
|
||||
|
||||
const Index = () => {
|
||||
const { t, i18n } = useTranslation("setting");
|
||||
const handleGuestToggle = (v: "zh" | "en") => {
|
||||
i18n.changeLanguage(v);
|
||||
dayjs.locale(v === "zh" ? "zh-cn" : "en");
|
||||
};
|
||||
return (
|
||||
<div className="setting">
|
||||
<p className="label">{t("overview.lang.title")}</p>
|
||||
<p className="tip">
|
||||
<span className="txt">
|
||||
{t("overview.lang.desc")}
|
||||
</span>
|
||||
</p>
|
||||
<StyledRadio
|
||||
options={[t("overview.lang.en"), t("overview.lang.zh")]}
|
||||
values={["en", "zh"]}
|
||||
value={i18n.language.split("-")[0]}
|
||||
onChange={(v) => {
|
||||
console.log("wtff", v);
|
||||
|
||||
handleGuestToggle(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { ChangeEvent, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import toast from "react-hot-toast";
|
||||
import { useUpdateLogoMutation, useUpdateServerMutation } from '../../../app/services/server';
|
||||
import LogoUploader from "../../../common/component/AvatarUploader";
|
||||
import Input from "../../../common/component/styled/Input";
|
||||
import Label from "../../../common/component/styled/Label";
|
||||
import Textarea from "../../../common/component/styled/Textarea";
|
||||
import SaveTip from '../../../common/component/SaveTip';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
|
||||
|
||||
const Index = () => {
|
||||
const { t } = useTranslation("setting");
|
||||
const { loginUser, server } = useAppSelector((store) => {
|
||||
return { loginUser: store.authData.user, server: store.server };
|
||||
});
|
||||
|
||||
const [uploadLogo, { isSuccess: uploadSuccess }] = useUpdateLogoMutation();
|
||||
const [updateServer] = useUpdateServerMutation();
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [serverValues, setServerValues] = useState<typeof server>(server);
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const newValue = evt.target.value;
|
||||
const { type = "" } = evt.target.dataset;
|
||||
setServerValues((prev) => {
|
||||
return { ...prev, [type]: newValue };
|
||||
});
|
||||
};
|
||||
const handleUpdateServer = () => {
|
||||
const { name, description } = serverValues;
|
||||
updateServer({ name, description });
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setServerValues(server);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (server) {
|
||||
setServerValues(server);
|
||||
}
|
||||
}, [server]);
|
||||
useEffect(() => {
|
||||
if (uploadSuccess) {
|
||||
toast.success("Update logo successfully!");
|
||||
}
|
||||
}, [uploadSuccess]);
|
||||
useEffect(() => {
|
||||
if (server && serverValues) {
|
||||
const { name, description } = serverValues;
|
||||
const { name: oName, description: oDescription } = server;
|
||||
if (oName !== name || oDescription !== description) {
|
||||
setChanged(true);
|
||||
} else {
|
||||
setChanged(false);
|
||||
}
|
||||
}
|
||||
}, [server, serverValues]);
|
||||
const { name, description, logo } = serverValues;
|
||||
const isAdmin = loginUser?.is_admin;
|
||||
if (!loginUser || !serverValues) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="logo">
|
||||
<div className="preview">
|
||||
<LogoUploader
|
||||
disabled={!isAdmin}
|
||||
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
|
||||
name={name}
|
||||
uploadImage={uploadLogo}
|
||||
/>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="upload">
|
||||
<div className="tip">
|
||||
{t("overview.upload_desc")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="inputs">
|
||||
<div className="input">
|
||||
<Label htmlFor="name">{t("overview.name")}</Label>
|
||||
<Input
|
||||
disabled={!isAdmin}
|
||||
data-type="name"
|
||||
onChange={handleChange}
|
||||
value={name}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Server Name"
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">{t("overview.desc")}</Label>
|
||||
<Textarea
|
||||
disabled={!isAdmin}
|
||||
data-type="description"
|
||||
onChange={handleChange}
|
||||
value={description ?? ""}
|
||||
rows={4}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Tell the world a bit about this server"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{changed && <SaveTip saveHandler={handleUpdateServer} resetHandler={handleReset} />}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -6,6 +6,7 @@ import { useUpdateInfoMutation } from "../../app/services/user";
|
||||
import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import Modal from "../../common/component/Modal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledEdit = styled(StyledModal)`
|
||||
.input {
|
||||
@@ -40,6 +41,7 @@ const ProfileBasicEditModal: FC<Props> = ({
|
||||
intro = "Enter a new username and your existing password.",
|
||||
closeModal
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState(value);
|
||||
const [update, { isLoading, isSuccess }] = useUpdateInfoMutation();
|
||||
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -63,9 +65,9 @@ const ProfileBasicEditModal: FC<Props> = ({
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
Cancel
|
||||
{t("action.cancel")}
|
||||
</Button>
|
||||
<Button onClick={handleUpdate}>{isLoading ? "Updating" : `Done`}</Button>
|
||||
<Button onClick={handleUpdate}>{isLoading ? "Updating" : t("action.done")}</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -4,12 +4,14 @@ import Modal from "../../common/component/Modal";
|
||||
import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import { useLazyDeleteCurrentAccountQuery } from "../../app/services/auth";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const RemoveConfirmModal: FC<Props> = ({ closeModal }) => {
|
||||
const { t } = useTranslation("member");
|
||||
const [removeCurrentAccount, { isLoading }] = useLazyDeleteCurrentAccountQuery();
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
@@ -22,13 +24,13 @@ const RemoveConfirmModal: FC<Props> = ({ closeModal }) => {
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
title="Remove Account"
|
||||
description="Are you sure remove this account?"
|
||||
title={t("remove_account") || ""}
|
||||
description={t("remove_account_desc") || ""}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal}>Cancel</Button>
|
||||
<Button onClick={closeModal}>{t("action.cancel", { ns: "common" })}</Button>
|
||||
<Button disabled={isLoading} onClick={handleRemove} className="danger">
|
||||
Remove
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useUpdatePasswordMutation, useGetCredentialsQuery } from "../../app/ser
|
||||
import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import Modal from "../../common/component/Modal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledEdit = styled(StyledModal)`
|
||||
.input {
|
||||
@@ -34,6 +35,7 @@ interface BaseForm {
|
||||
}
|
||||
|
||||
const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
|
||||
const { t } = useTranslation("member");
|
||||
const { data } = useGetCredentialsQuery();
|
||||
const [input, setInput] = useState<BaseForm>({
|
||||
current: "",
|
||||
@@ -75,22 +77,22 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
|
||||
return (
|
||||
<Modal id="modal-modal">
|
||||
<StyledEdit
|
||||
title={"Change your password"}
|
||||
description={"Enter current password and new password."}
|
||||
title={t("change_pwd") || ""}
|
||||
description={t("change_pwd_desc") || ""}
|
||||
buttons={
|
||||
<>
|
||||
<Button className="cancel" onClick={closeModal}>
|
||||
Cancel
|
||||
{t("action.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button disabled={disableBtn} onClick={handleUpdate}>
|
||||
{isLoading ? "Updating" : `Update`}
|
||||
{isLoading ? "Updating" : t("action.update", { ns: "common" })}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{data?.password && (
|
||||
<div className="input">
|
||||
<label htmlFor={"current"}>Current Password</label>
|
||||
<label htmlFor={"current"}>{t("current_pwd")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
id="current"
|
||||
@@ -102,7 +104,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
|
||||
</div>
|
||||
)}
|
||||
<div className="input">
|
||||
<label htmlFor={"newPassword"}>New Password</label>
|
||||
<label htmlFor={"newPassword"}>{t("new_pwd")}</label>
|
||||
<Input
|
||||
type="password"
|
||||
name={"newPassword"}
|
||||
@@ -112,7 +114,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
|
||||
></Input>
|
||||
</div>
|
||||
<div className="input">
|
||||
<label htmlFor={"confirmPassword"}>Confirm New Password</label>
|
||||
<label htmlFor={"confirmPassword"}>{t("confirm_new_pwd")}</label>
|
||||
<Input
|
||||
onBlur={handleCompare}
|
||||
type="password"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// import { useState, MouseEvent } from "react";
|
||||
// import dayjs from "dayjs";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
|
||||
@@ -17,26 +18,27 @@ const Row = ({ paramKey, paramDefault, remarks }: { paramKey: string, paramDefau
|
||||
</tr>;
|
||||
};
|
||||
export default function Widget() {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "widget" });
|
||||
// const disableBtn = !reachLimit;
|
||||
return (
|
||||
<div className="flex flex-col justify-start items-start">
|
||||
<div className="text-gray-600 ">
|
||||
Extending VoceChat by embedding the vocechat widget SDK!
|
||||
{t('tip')}
|
||||
</div>
|
||||
<label htmlFor="code" className="text-gray-500 text-sm mt-5">
|
||||
Code Example:
|
||||
{t('code')}:
|
||||
</label>
|
||||
<SyntaxHighlighter id="code" language="html" style={vscDarkPlus} className="rounded">
|
||||
{`<!-- put this code snippet into your html file -->\n<script \n data-host-id="4" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n src="${location.origin}/widget.js" \n async \n/>`}
|
||||
{`<!-- ${t('code_comment')} -->\n<script \n data-host-id="4" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n src="${location.origin}/widget.js" \n async \n/>`}
|
||||
</SyntaxHighlighter>
|
||||
<div className="text-gray-500 text-sm mt-5 mb-2">
|
||||
Configuration Description:
|
||||
{t('config')}:
|
||||
</div>
|
||||
<div className="w-[700px] border border-solid border-gray-300 rounded overflow-hidden">
|
||||
<table className="min-w-full table-auto">
|
||||
<thead className="border-b bg-gray-50">
|
||||
<tr>
|
||||
{["Parameter Key", "Default Value", "Remarks"].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 px-6 py-4 text-left">
|
||||
{[t('param_key'), t('default_value'), t('remark')].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 px-6 py-4 text-left">
|
||||
{title}
|
||||
</th>)}
|
||||
</tr>
|
||||
|
||||
@@ -7,8 +7,10 @@ import Label from "../../../common/component/styled/Label";
|
||||
import SaveTip from "../../../common/component/SaveTip";
|
||||
import useConfig from "../../../common/hook/useConfig";
|
||||
import { FirebaseConfig } from "../../../types/server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ConfigFirebase() {
|
||||
const { t } = useTranslation("setting");
|
||||
const { values, toggleEnable, updateConfig, setValues, reset, changed } = useConfig("firebase");
|
||||
const handleUpdate = () => {
|
||||
// const { token_url, description } = values;
|
||||
@@ -34,33 +36,33 @@ export default function ConfigFirebase() {
|
||||
<StyledContainer>
|
||||
<div className="inputs">
|
||||
<div className="input row">
|
||||
<Label>Enable</Label>
|
||||
<Label>{t("firebase.enable")}</Label>
|
||||
<Toggle onClick={toggleEnable} data-checked={enabled}></Toggle>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="name">Token Url</Label>
|
||||
<Label htmlFor="name">{t("firebase.token_url")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="token_url"
|
||||
onChange={handleChange}
|
||||
value={token_url}
|
||||
name="token_url"
|
||||
placeholder="Token URL"
|
||||
placeholder={t("firebase.token_url") || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Project ID</Label>
|
||||
<Label htmlFor="desc">{t("firebase.project_id")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="project_id"
|
||||
onChange={handleChange}
|
||||
value={project_id}
|
||||
name="project_id"
|
||||
placeholder="Project ID"
|
||||
placeholder={t("firebase.project_id") || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Private Key</Label>
|
||||
<Label htmlFor="desc">{t("firebase.private_key")}</Label>
|
||||
<Textarea
|
||||
rows={10}
|
||||
spellCheck={false}
|
||||
@@ -69,18 +71,18 @@ export default function ConfigFirebase() {
|
||||
onChange={handleChange}
|
||||
value={private_key}
|
||||
name="private_key"
|
||||
placeholder="Private key"
|
||||
placeholder={t("firebase.private_key") || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Client Email</Label>
|
||||
<Label htmlFor="desc">{t("firebase.client_email")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="client_email"
|
||||
onChange={handleChange}
|
||||
value={client_email}
|
||||
name="client_email"
|
||||
placeholder="Client Email address"
|
||||
placeholder={t("firebase.client_email") || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Toggle from "../../../../common/component/styled/Toggle";
|
||||
import options from "./items.json";
|
||||
import Styled from "./styled";
|
||||
import IconMinus from "../../../../assets/icons/minus.circle.svg";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Issuer {
|
||||
domain: string;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const [select, setSelect] = useState<Partial<Option> | null>(null);
|
||||
const [newDomain, setNewDomain] = useState("");
|
||||
|
||||
@@ -111,7 +113,7 @@ const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
|
||||
setNewDomain("");
|
||||
}}
|
||||
>
|
||||
Add
|
||||
{t("action.add")}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -11,8 +11,10 @@ import IssuerList from "./IssuerList";
|
||||
import useGoogleAuthConfig from "../../../common/hook/useGoogleAuthConfig";
|
||||
import useGithubAuthConfig from "../../../common/hook/useGithubAuthConfig";
|
||||
import { LoginConfig } from "../../../types/server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function Logins() {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "login" });
|
||||
const {
|
||||
changed: clientIdChanged,
|
||||
clientId,
|
||||
@@ -74,9 +76,9 @@ export default function Logins() {
|
||||
<div className="row">
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Password</Label>
|
||||
<Label>{t("password")}</Label>
|
||||
</div>
|
||||
<span className="desc">Allows members login with password.</span>
|
||||
<span className="desc">{t("password_desc")}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
onClick={handleToggle.bind(null, { password: !password })}
|
||||
@@ -88,9 +90,9 @@ export default function Logins() {
|
||||
<div className="row">
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Magic Link</Label>
|
||||
<Label>{t("magic_link")}</Label>
|
||||
</div>
|
||||
<span className="desc">Allows members login with Magic Link.</span>
|
||||
<span className="desc">{t("magic_link_desc")}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
onClick={handleToggle.bind(null, { magic_link: !magic_link })}
|
||||
@@ -102,10 +104,10 @@ export default function Logins() {
|
||||
<div className="row">
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Google</Label>
|
||||
<Label>{t("google")}</Label>
|
||||
<Tooltip link="https://doc.voce.chat/setting/third_login/login-google" />
|
||||
</div>
|
||||
<span className="desc">Allows members login with Google.</span>
|
||||
<span className="desc">{t("google_desc")}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
onClick={handleToggle.bind(null, { google: !google })}
|
||||
@@ -125,10 +127,10 @@ export default function Logins() {
|
||||
<div className="row">
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Github</Label>
|
||||
<Label>{t("github")}</Label>
|
||||
<Tooltip link="https://doc.voce.chat/setting/third_login/login-github" />
|
||||
</div>
|
||||
<span className="desc">Allows members login with Github.</span>
|
||||
<span className="desc">{t("github_desc")}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
onClick={handleToggle.bind(null, { github: !github })}
|
||||
@@ -156,10 +158,10 @@ export default function Logins() {
|
||||
<div className="row">
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label>Metamask</Label>
|
||||
<Label>{t("metamask")}</Label>
|
||||
<Tooltip link="https://doc.voce.chat/setting/third_login/login-metamask" />
|
||||
</div>
|
||||
<span className="desc">Allows members login with Metamask.</span>
|
||||
<span className="desc">{t("metamask_desc")}</span>
|
||||
</div>
|
||||
<Toggle
|
||||
onClick={handleToggle.bind(null, { metamask: !metamask })}
|
||||
@@ -171,10 +173,10 @@ export default function Logins() {
|
||||
<div className="row">
|
||||
<div className="title">
|
||||
<div className="txt">
|
||||
<Label htmlFor="desc">OIDC</Label>
|
||||
<Label htmlFor="desc">{t("oidc")}</Label>
|
||||
<Tooltip link="https://doc.voce.chat/setting/third_login/login-webid" />
|
||||
</div>
|
||||
<span className="desc">Save my login details for next time.</span>
|
||||
<span className="desc">{t("oidc_desc")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row">
|
||||
|
||||
@@ -17,8 +17,10 @@ import Label from "../../../common/component/styled/Label";
|
||||
import SaveTip from "../../../common/component/SaveTip";
|
||||
import toast from "react-hot-toast";
|
||||
import { SMTPConfig } from "../../../types/server";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function ConfigSMTP() {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "smtp" });
|
||||
const [testEmail, setTestEmail] = useState("");
|
||||
const [sendTestEmail, { isSuccess, isError }] = useSendTestEmailMutation();
|
||||
const { reset, updateConfig, values, setValues, changed, toggleEnable } = useConfig("smtp");
|
||||
@@ -61,11 +63,11 @@ export default function ConfigSMTP() {
|
||||
<StyledContainer>
|
||||
<div className="inputs">
|
||||
<div className="input row">
|
||||
<Label>Enable</Label>
|
||||
<Label>{t("enable")}</Label>
|
||||
<Toggle onClick={toggleEnable} data-checked={enabled}></Toggle>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="name">Host</Label>
|
||||
<Label htmlFor="name">{t("host")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="host"
|
||||
@@ -76,7 +78,7 @@ export default function ConfigSMTP() {
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Port</Label>
|
||||
<Label htmlFor="desc">{t("port")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
type={"number"}
|
||||
@@ -88,7 +90,7 @@ export default function ConfigSMTP() {
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">From</Label>
|
||||
<Label htmlFor="desc">{t("from")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="from"
|
||||
@@ -99,7 +101,7 @@ export default function ConfigSMTP() {
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Username</Label>
|
||||
<Label htmlFor="desc">{t("username")}</Label>
|
||||
<Input
|
||||
disabled={!enabled}
|
||||
data-type="username"
|
||||
@@ -110,7 +112,7 @@ export default function ConfigSMTP() {
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Password</Label>
|
||||
<Label htmlFor="desc">{t("password")}</Label>
|
||||
<Input
|
||||
type={"password"}
|
||||
disabled={!enabled}
|
||||
@@ -130,7 +132,7 @@ export default function ConfigSMTP() {
|
||||
className="link"
|
||||
rel="noreferrer"
|
||||
>
|
||||
How to set up SMTP?
|
||||
{t("how_to")}
|
||||
</a>
|
||||
</div>
|
||||
<StyledTest>
|
||||
@@ -143,7 +145,7 @@ export default function ConfigSMTP() {
|
||||
placeholder="test@email.com"
|
||||
/>
|
||||
<Button disabled={!enabled || !testEmail} onClick={handleTestClick}>
|
||||
Send Test Email
|
||||
{t("send_test_email")}
|
||||
</Button>
|
||||
</StyledTest>
|
||||
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { roundArrow } from "tippy.js";
|
||||
import "tippy.js/dist/svg-arrow.css";
|
||||
import styled from "styled-components";
|
||||
import IconQuestion from "../../../assets/icons/question.svg";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
import { PropsWithChildren } from "react";
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const StyledContent = styled.div`
|
||||
padding: 8px 12px;
|
||||
background: #101828;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #ffffff;
|
||||
a {
|
||||
color: #55c7ec;
|
||||
}
|
||||
`;
|
||||
const Link = ({ to, children }: PropsWithChildren<{ to: string }>) => {
|
||||
|
||||
return <a href={to} className="text-primary-500" target="_blank" rel="noreferrer" >
|
||||
{children}
|
||||
</a>;
|
||||
};
|
||||
export default function Tooltip({ link = "#" }) {
|
||||
return (
|
||||
<Tippy
|
||||
@@ -25,13 +20,11 @@ export default function Tooltip({ link = "#" }) {
|
||||
arrow={roundArrow}
|
||||
placement="bottom"
|
||||
content={
|
||||
<StyledContent>
|
||||
Need more detail? See our{" "}
|
||||
<a target={"doc"} href={link}>
|
||||
doc
|
||||
</a>
|
||||
.
|
||||
</StyledContent>
|
||||
<div className="py-2 px-3 bg-gray-800 text-xs text-white rounded-lg">
|
||||
<Trans ns="setting" i18nKey={"login.more_details"} >
|
||||
<Link to={link} />
|
||||
</Trans>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<IconQuestion className="icon" />
|
||||
|
||||
@@ -3,10 +3,12 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import StyledSettingContainer from "../../common/component/StyledSettingContainer";
|
||||
import useNavs from "./navs";
|
||||
import LogoutConfirmModal from "./LogoutConfirmModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
let pageFrom: string = "";
|
||||
|
||||
export default function Setting() {
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const navs = useNavs();
|
||||
const flattenNaves = navs.map(({ items }) => items).flat();
|
||||
@@ -30,9 +32,9 @@ export default function Setting() {
|
||||
<StyledSettingContainer
|
||||
nav={currNav}
|
||||
closeModal={close}
|
||||
title="Settings"
|
||||
title={t("setting") || ""}
|
||||
navs={navs}
|
||||
dangers={[{ title: "Log Out", handler: toggleLogoutConfirm }]}
|
||||
dangers={[{ title: t("action.logout"), handler: toggleLogoutConfirm }]}
|
||||
>
|
||||
{currNav.component}
|
||||
</StyledSettingContainer>
|
||||
|
||||
+35
-36
@@ -4,108 +4,107 @@ import Logins from "./config/Logins";
|
||||
import ConfigFirebase from "./config/Firebase";
|
||||
import ConfigSMTP from "./config/SMTP";
|
||||
import APIConfig from "./APIConfig";
|
||||
import License from "./License";
|
||||
import License from "./License/License";
|
||||
import Widget from "./Widget";
|
||||
import ManageMembers from "../../common/component/ManageMembers";
|
||||
import FAQ from "../../common/component/FAQ";
|
||||
import ConfigAgora from "./config/Agora";
|
||||
// import ConfigAgora from "./config/Agora";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const navs = [
|
||||
{
|
||||
title: "General",
|
||||
title: "general",
|
||||
items: [
|
||||
{
|
||||
name: "overview",
|
||||
title: "Overview",
|
||||
component: <Overview />
|
||||
},
|
||||
{
|
||||
name: "my_account",
|
||||
component: <MyAccount />
|
||||
},
|
||||
{
|
||||
name: "members",
|
||||
title: "Members",
|
||||
component: <ManageMembers />,
|
||||
admin: true
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "User",
|
||||
items: [
|
||||
{
|
||||
name: "my_account",
|
||||
title: "My Account",
|
||||
component: <MyAccount />
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Configuration",
|
||||
title: "config",
|
||||
items: [
|
||||
{
|
||||
name: "firebase",
|
||||
title: "Firebase",
|
||||
component: <ConfigFirebase />
|
||||
},
|
||||
{
|
||||
name: "agora",
|
||||
title: "Agora",
|
||||
component: <ConfigAgora />
|
||||
},
|
||||
// {
|
||||
// name: "agora",
|
||||
// component: <ConfigAgora />
|
||||
// },
|
||||
{
|
||||
name: "smtp",
|
||||
title: "SMTP",
|
||||
component: <ConfigSMTP />
|
||||
},
|
||||
{
|
||||
name: "social_login",
|
||||
title: "Login Methods",
|
||||
name: "login_method",
|
||||
component: <Logins />
|
||||
},
|
||||
{
|
||||
name: "api",
|
||||
title: "Third-party APP",
|
||||
name: "third_app",
|
||||
component: <APIConfig />
|
||||
},
|
||||
{
|
||||
name: "widget",
|
||||
title: "Widget",
|
||||
component: <Widget />
|
||||
},
|
||||
{
|
||||
name: "license",
|
||||
title: "License",
|
||||
component: <License />
|
||||
}
|
||||
],
|
||||
admin: true
|
||||
},
|
||||
{
|
||||
title: "About",
|
||||
title: "about",
|
||||
items: [
|
||||
{
|
||||
name: "faq",
|
||||
title: "FAQ",
|
||||
component: <FAQ />
|
||||
},
|
||||
{
|
||||
name: "terms",
|
||||
title: "Terms & Privacy",
|
||||
component: "Terms & Privacy"
|
||||
},
|
||||
{
|
||||
name: "feedback",
|
||||
title: "Feedback",
|
||||
component: "feedback"
|
||||
component: "Email: han@privoce.com\nWechat: yanggc_2013"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const useNavs = () => {
|
||||
const { t } = useTranslation("setting");
|
||||
const loginUser = useAppSelector((store) => {
|
||||
return store.authData.user;
|
||||
});
|
||||
return navs.filter((nav) => {
|
||||
const transformedNavs = navs.map(n => {
|
||||
const { title, items, ...rest } = n;
|
||||
return {
|
||||
title: t(`nav.${title}`),
|
||||
items: items.map(item => {
|
||||
const { name, ...rest } = item;
|
||||
return {
|
||||
name,
|
||||
title: t(`nav.${name}`),
|
||||
...rest
|
||||
};
|
||||
}),
|
||||
...rest
|
||||
};
|
||||
});
|
||||
return transformedNavs.filter((nav) => {
|
||||
if (loginUser?.is_admin) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ import Modal from "../../common/component/Modal";
|
||||
import { useLazyRemoveChannelQuery } from "../../app/services/channel";
|
||||
import StyledModal from "../../common/component/styled/Modal";
|
||||
import Button from "../../common/component/styled/Button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
@@ -12,6 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const DeleteConfirmModal: FC<Props> = ({ id, closeModal }) => {
|
||||
const { t } = useTranslation("setting");
|
||||
const navigateTo = useNavigate();
|
||||
const [deleteChannel, { isLoading, isSuccess }] = useLazyRemoveChannelQuery();
|
||||
const handleDelete = () => {
|
||||
@@ -31,15 +33,15 @@ const DeleteConfirmModal: FC<Props> = ({ id, closeModal }) => {
|
||||
<Modal id="modal-modal">
|
||||
<StyledModal
|
||||
className="compact"
|
||||
title="Delete Channel"
|
||||
description="Are you sure want to delete this channel?"
|
||||
title={t("channel.delete") || ""}
|
||||
description={t("channel.delete_desc") || ""}
|
||||
buttons={
|
||||
<>
|
||||
<Button onClick={closeModal.bind(null, undefined)} className="cancel">
|
||||
Cancel
|
||||
{t("action.cancel", { ns: "common" })}
|
||||
</Button>
|
||||
<Button onClick={handleDelete} className="danger">
|
||||
{isLoading ? "Deleting" : `Delete`}
|
||||
{isLoading ? "Deleting" : t("action.remove", { ns: "common" })}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import SaveTip from "../../common/component/SaveTip";
|
||||
import channelIcon from "../../assets/icons/channel.svg?url";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { Channel } from "../../types/channel";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: relative;
|
||||
@@ -48,6 +49,7 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
export default function Overview({ id = 0 }) {
|
||||
const { t } = useTranslation("setting", { keyPrefix: "channel" });
|
||||
const { loginUser, channel } = useAppSelector((store) => {
|
||||
return {
|
||||
loginUser: store.authData.user,
|
||||
@@ -122,7 +124,7 @@ export default function Overview({ id = 0 }) {
|
||||
<AvatarUploader type="channel" url={channel?.icon} name={name} uploadImage={updateIcon} />
|
||||
<div className="inputs">
|
||||
<div className="input">
|
||||
<Label htmlFor="name">Channel Name</Label>
|
||||
<Label htmlFor="name">{t("name")}</Label>
|
||||
<Input
|
||||
disabled={readOnly}
|
||||
className="name"
|
||||
@@ -131,11 +133,11 @@ export default function Overview({ id = 0 }) {
|
||||
value={name}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Channel Name"
|
||||
placeholder={t("name") || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="input">
|
||||
<Label htmlFor="desc">Channel Topic</Label>
|
||||
<Label htmlFor="desc">{t("topic")}</Label>
|
||||
<Textarea
|
||||
disabled={readOnly}
|
||||
data-type="description"
|
||||
@@ -144,13 +146,12 @@ export default function Overview({ id = 0 }) {
|
||||
rows={4}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Let everyone know how to use this channel."
|
||||
/>
|
||||
placeholder={t("topic_placeholder") || ""} />
|
||||
</div>
|
||||
{!readOnly && <div className="input">
|
||||
<Label htmlFor="desc">Channel Visibility</Label>
|
||||
<Label htmlFor="desc">{t("visibility")}</Label>
|
||||
<Radio
|
||||
options={["Public", "Private"]}
|
||||
options={[t("public"), t("private")]}
|
||||
values={["true", "false"]}
|
||||
value={String(channel.is_public)}
|
||||
onChange={(v: string) => {
|
||||
|
||||
@@ -5,10 +5,12 @@ import StyledSettingContainer from "../../common/component/StyledSettingContaine
|
||||
import DeleteConfirmModal from "./DeleteConfirmModal";
|
||||
import useNavs from "./navs";
|
||||
import { useAppSelector } from "../../app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
let from: string = "";
|
||||
|
||||
export default function ChannelSetting() {
|
||||
const { t } = useTranslation("setting");
|
||||
const { cid = 0 } = useParams();
|
||||
const { loginUser, channel } = useAppSelector((store) => {
|
||||
return {
|
||||
@@ -52,11 +54,11 @@ export default function ChannelSetting() {
|
||||
navs={navs}
|
||||
dangers={[
|
||||
canLeave && {
|
||||
title: "Leave Channel",
|
||||
title: t("channel.leave"),
|
||||
handler: toggleLeaveConfirm
|
||||
},
|
||||
canDelete && {
|
||||
title: "Delete Channel",
|
||||
title: t("channel.delete"),
|
||||
handler: toggleDeleteConfirm
|
||||
}
|
||||
]}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Overview from "./Overview";
|
||||
import ManageMembers from "../../common/component/ManageMembers";
|
||||
import { ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface NavItem {
|
||||
name: string;
|
||||
@@ -15,18 +16,19 @@ export interface Nav {
|
||||
}
|
||||
|
||||
const useNavs = (cid: number): Nav[] => {
|
||||
const { t } = useTranslation("setting");
|
||||
return [
|
||||
{
|
||||
title: "General",
|
||||
title: t("nav.general"),
|
||||
items: [
|
||||
{
|
||||
name: "overview",
|
||||
title: "Overview",
|
||||
title: t("nav.overview"),
|
||||
component: <Overview id={cid} />
|
||||
},
|
||||
{
|
||||
name: "members",
|
||||
title: "Members",
|
||||
title: t("nav.members"),
|
||||
component: <ManageMembers cid={cid} />
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,30 +5,17 @@ import Welcome from './Welcome';
|
||||
import MessageFeed from './MessageFeed';
|
||||
import MessageInput from './MessageInput';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import useSSE from '../useSSE';
|
||||
|
||||
type Props = {
|
||||
hostId: number,
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
const Index = ({ handleClose, hostId }: Props) => {
|
||||
// 建立SSE连接
|
||||
useSSE();
|
||||
const { user: loginUser, token, guest: isGuest } = useAppSelector(store => store.authData);
|
||||
// const { sendMessage } = useSendMessage({
|
||||
// from: loginUser?.uid,
|
||||
// to: hostId,
|
||||
// context: "user"
|
||||
// });
|
||||
// const [input, setInput] = useState('');
|
||||
// const handleInput = (evt: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
// setInput(evt.target.value);
|
||||
// };
|
||||
// const handleSend = () => {
|
||||
// if (!input) return;
|
||||
// sendMessage({
|
||||
// type: "text",
|
||||
// content: input
|
||||
// });
|
||||
// setInput("");
|
||||
// };
|
||||
|
||||
// no token or guest login
|
||||
const notLogin = !token || isGuest;
|
||||
|
||||
@@ -3,13 +3,13 @@ import { useGetServerQuery } from "../app/services/server";
|
||||
|
||||
import Icon from "./Icon";
|
||||
import Popup from "./Popup";
|
||||
import usePreload from "./usePreload";
|
||||
import useCache from "./useCache";
|
||||
type Props = {
|
||||
hostId: number
|
||||
};
|
||||
|
||||
function Widget({ hostId }: Props) {
|
||||
const { rehydrated } = usePreload();
|
||||
const { rehydrated } = useCache();
|
||||
const [visible, setVisible] = useState(!!new URLSearchParams(location.search).get("open"));
|
||||
const { isLoading, isError } = useGetServerQuery();
|
||||
const toggleVisible = () => {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from "react";
|
||||
import initCache, { useRehydrate } from "../app/cache";
|
||||
export default function useCache() {
|
||||
const { rehydrate, rehydrated } = useRehydrate();
|
||||
useEffect(() => {
|
||||
initCache();
|
||||
rehydrate();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
rehydrated
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { useEffect } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import { useAppSelector } from "../app/store";
|
||||
import initCache, { useRehydrate } from "../app/cache";
|
||||
import useStreaming from "../common/hook/useStreaming";
|
||||
// type Props={
|
||||
// guest?:boolean
|
||||
// }
|
||||
export default function usePreload() {
|
||||
const { rehydrate, rehydrated } = useRehydrate();
|
||||
|
||||
export default function useSSE() {
|
||||
const {
|
||||
loginUid,
|
||||
token,
|
||||
@@ -20,23 +16,17 @@ export default function usePreload() {
|
||||
};
|
||||
});
|
||||
const { setStreamingReady } = useStreaming();
|
||||
useEffect(() => {
|
||||
initCache();
|
||||
rehydrate();
|
||||
return () => {
|
||||
setStreamingReady(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tokenAlmostExpire = dayjs().isAfter(new Date(expireTime - 20 * 1000));
|
||||
const canStreaming = !!loginUid && rehydrated && !!token && !tokenAlmostExpire;
|
||||
const canStreaming = !!loginUid && !!token && !tokenAlmostExpire;
|
||||
// console.log("ttt", loginUid, rehydrated, token);
|
||||
|
||||
useEffect(() => {
|
||||
setStreamingReady(canStreaming);
|
||||
return () => {
|
||||
setStreamingReady(false);
|
||||
};
|
||||
}, [canStreaming]);
|
||||
|
||||
return {
|
||||
rehydrated
|
||||
};
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user