From 5bd018365101649833074bc7e5fd583335ea508d Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Fri, 19 May 2023 16:31:28 +0800 Subject: [PATCH] build: format the code with prettier --- .prettierrc | 4 +- .vscode/settings.json | 3 + src/components/AddEntriesMenu.tsx | 8 +- src/components/AutoDeleteMessages.tsx | 180 +++---- src/components/Avatar.tsx | 5 +- src/components/AvatarUploader.tsx | 28 +- src/components/BlankPlaceholder.tsx | 99 ++-- src/components/Channel.tsx | 18 +- src/components/ChannelIcon.tsx | 9 +- src/components/ChannelModal/index.tsx | 50 +- src/components/DeleteMessageConfirm.tsx | 7 +- src/components/Divider.tsx | 18 +- src/components/ErrorCatcher.tsx | 36 +- src/components/FileBox/index.tsx | 30 +- src/components/FileBox/preview/Code.tsx | 8 +- src/components/FileBox/preview/Doc.tsx | 6 +- src/components/FileBox/preview/Image.tsx | 25 +- src/components/FileBox/preview/Video.tsx | 1 + src/components/FileBox/preview/index.tsx | 6 +- src/components/FileMessage/AudioMessage.tsx | 72 +-- src/components/FileMessage/ImageMessage.tsx | 41 +- src/components/FileMessage/Progress.tsx | 6 +- src/components/FileMessage/VideoMessage.tsx | 130 ++--- src/components/FileMessage/index.tsx | 67 +-- src/components/ForwardModal/index.tsx | 28 +- src/components/GithubLoginButton.tsx | 10 +- src/components/GoBackNav.tsx | 64 +-- src/components/GoogleLoginButton.tsx | 17 +- src/components/GuestOnly.tsx | 1 + src/components/ImagePreview.tsx | 103 ++-- src/components/ImagePreviewModal.tsx | 8 +- src/components/InactiveScreen.tsx | 11 +- src/components/InviteLink.tsx | 19 +- src/components/InviteModal/AddMembers.tsx | 30 +- src/components/InviteModal/InviteByEmail.tsx | 34 +- src/components/InviteModal/index.tsx | 10 +- .../LeaveChannel/LeaveConfirmModal.tsx | 13 +- .../LeaveChannel/TransferOwnerModal.tsx | 21 +- src/components/LeaveChannel/index.tsx | 5 +- src/components/LinkifyText.tsx | 116 +++-- src/components/Loading.tsx | 20 +- src/components/ManageMembers/MemberList.tsx | 303 ++++++------ src/components/ManageMembers/index.tsx | 9 +- src/components/Manifest/Prompt.tsx | 7 +- src/components/Manifest/index.tsx | 10 +- src/components/MarkdownEditor/index.tsx | 8 +- src/components/MarkdownRender.tsx | 15 +- src/components/Message/Commands.tsx | 46 +- src/components/Message/ContextMenu.tsx | 18 +- src/components/Message/EditMessage.tsx | 16 +- src/components/Message/ExpireTimer.tsx | 150 +++--- src/components/Message/FavoredMessage.tsx | 27 +- src/components/Message/ForwardedMessage.tsx | 21 +- src/components/Message/Mention.tsx | 3 +- src/components/Message/PinMessageModal.tsx | 13 +- src/components/Message/PreviewMessage.tsx | 30 +- src/components/Message/Reaction.tsx | 28 +- src/components/Message/ReactionPicker.tsx | 16 +- src/components/Message/Reply.tsx | 47 +- src/components/Message/URLPreview.tsx | 24 +- src/components/Message/index.tsx | 106 ++-- src/components/Message/renderContent.tsx | 20 +- .../Message/useMessageOperation.tsx | 18 +- src/components/Meta.tsx | 2 + src/components/MixedInput/index.tsx | 79 +-- src/components/MobileAppTip.tsx | 35 +- src/components/Modal.tsx | 2 +- src/components/NewVersion.tsx | 1 + src/components/Notification/index.tsx | 6 +- src/components/PinnedMessage.tsx | 16 +- src/components/Profile/index.tsx | 16 +- src/components/QRCode.tsx | 65 +-- src/components/ReLoginModal.tsx | 77 +-- src/components/ReactionItem.tsx | 13 +- src/components/RequireAuth.tsx | 8 +- src/components/RequireNoAuth.tsx | 1 + src/components/RequireSingleTab.tsx | 1 + src/components/SaveTip.tsx | 19 +- src/components/SearchUser.tsx | 99 ++-- src/components/Send/EmojiPicker.tsx | 33 +- src/components/Send/Replying.tsx | 12 +- src/components/Send/Toolbar.tsx | 82 ++-- .../Send/UploadFileList/EditFileDetails.tsx | 8 +- src/components/Send/UploadFileList/index.tsx | 41 +- src/components/Send/index.tsx | 71 +-- src/components/Server.tsx | 29 +- src/components/ServerVersionChecker.tsx | 60 ++- src/components/Signal.tsx | 39 +- src/components/StyledSettingContainer.tsx | 82 +++- src/components/TextInput/index.tsx | 24 +- src/components/Tooltip.tsx | 18 +- src/components/UnreadTabTip.tsx | 122 ++--- src/components/User/ContextMenu.tsx | 4 +- src/components/User/index.tsx | 49 +- src/components/UsersModal.tsx | 25 +- src/components/Version.tsx | 28 +- src/components/Voice/DMCalling.tsx | 212 ++++---- src/components/Voice/Operations.tsx | 457 +++++++++++------- src/components/Voice/index.tsx | 410 ++++++++-------- src/components/VoiceMessage.tsx | 251 +++++----- src/components/styled/Button.tsx | 52 +- src/components/styled/Checkbox.tsx | 14 +- src/components/styled/Input.tsx | 6 +- src/components/styled/Modal.tsx | 29 +- src/components/styled/Radio.tsx | 6 +- src/components/styled/Select.tsx | 15 +- src/components/styled/Textarea.tsx | 11 +- src/components/styled/Toggle.tsx | 30 +- src/hooks/useAddLocalFileMessage.ts | 1 - src/hooks/useContextMenu.tsx | 5 +- src/hooks/useDeleteMessage.ts | 1 + src/hooks/useFavMessage.ts | 17 +- src/hooks/useFilteredUsers.ts | 5 +- src/hooks/useGithubAuthConfig.ts | 3 +- src/hooks/useInviteLink.ts | 15 +- src/hooks/useStreaming/chat.handler.ts | 1 + src/hooks/useStreaming/index.ts | 108 +++-- src/index-widget.tsx | 32 +- src/index.tsx | 31 +- src/routes/404/index.tsx | 1 - src/routes/callback/GithubCallback.tsx | 128 ++--- src/routes/callback/PaymentSuccess.tsx | 15 +- src/routes/callback/index.tsx | 12 +- src/routes/chat/ChannelChat/Members.tsx | 110 +++-- src/routes/chat/ChannelChat/PinList.tsx | 26 +- src/routes/chat/ChannelChat/index.tsx | 176 +++---- src/routes/chat/DMChat/index.tsx | 14 +- src/routes/chat/FavList.tsx | 20 +- src/routes/chat/GuestBlankPlaceholder.tsx | 20 +- src/routes/chat/GuestChannelChat/index.tsx | 6 +- src/routes/chat/GuestSessionList/Session.tsx | 39 +- src/routes/chat/GuestSessionList/index.tsx | 7 +- src/routes/chat/Layout/AddContactTip.tsx | 91 ++-- src/routes/chat/Layout/DMVoicing.tsx | 332 ++++++++----- src/routes/chat/Layout/DnDTip.tsx | 16 +- src/routes/chat/Layout/LicenseOutdatedTip.tsx | 2 + src/routes/chat/Layout/LoginTip.tsx | 21 +- .../chat/Layout/NewMessageBottomTip.tsx | 80 +-- src/routes/chat/Layout/NewMessageTopTip.tsx | 77 +-- src/routes/chat/Layout/Operations.tsx | 30 +- .../VirtualMessageFeed/CustomHeader.tsx | 92 ++-- .../Layout/VirtualMessageFeed/CustomList.tsx | 9 +- .../chat/Layout/VirtualMessageFeed/index.tsx | 275 ++++++----- src/routes/chat/Layout/index.tsx | 65 +-- src/routes/chat/LoadMore.tsx | 5 +- src/routes/chat/RTCWidget.tsx | 224 +++++---- src/routes/chat/SessionList/ContextMenu.tsx | 118 ++--- src/routes/chat/SessionList/Session.tsx | 109 +++-- src/routes/chat/SessionList/index.tsx | 133 ++--- src/routes/chat/VoiceChat/Dashboard.tsx | 28 +- src/routes/chat/VoiceChat/VoiceManagement.tsx | 228 +++++---- src/routes/chat/VoiceChat/index.tsx | 151 +++--- src/routes/chat/VoiceFullscreen.tsx | 302 +++++++----- src/routes/chat/index.tsx | 76 ++- src/routes/chat/utils.tsx | 28 +- src/routes/favs/index.tsx | 56 ++- src/routes/guest.tsx | 1 + src/routes/home/Menu.tsx | 2 + src/routes/home/MobileNavs.tsx | 152 +++--- src/routes/home/User.tsx | 11 +- src/routes/home/index.tsx | 104 ++-- src/routes/index.tsx | 128 ++++- src/routes/invitePrivate/index.tsx | 123 ++--- src/routes/lazy.tsx | 11 +- src/routes/login/MagicLinkLogin.tsx | 7 +- src/routes/login/MetamaskLoginButton.tsx | 20 +- src/routes/login/OidcLoginButton.tsx | 14 +- src/routes/login/OidcLoginEntry.tsx | 11 +- src/routes/login/SignUpLink.tsx | 4 +- src/routes/login/SocialLoginButtons.tsx | 87 ++-- src/routes/login/index.tsx | 35 +- src/routes/oauth/index.tsx | 7 +- src/routes/onboarding/index.tsx | 24 +- .../onboarding/steps/UpdateFrontendURL.tsx | 106 ++-- src/routes/onboarding/steps/admin-account.tsx | 18 +- src/routes/onboarding/steps/done-page.tsx | 9 +- src/routes/onboarding/steps/invite-link.tsx | 20 +- src/routes/onboarding/steps/server-name.tsx | 9 +- src/routes/onboarding/steps/welcome-page.tsx | 13 +- .../onboarding/steps/who-can-sign-up.tsx | 25 +- src/routes/reg/EmailNextStepTip.tsx | 5 +- src/routes/reg/RegWithUsername.tsx | 22 +- src/routes/reg/Register.tsx | 86 ++-- src/routes/reg/SignInLink.tsx | 17 +- src/routes/reg/index.tsx | 3 +- src/routes/resources/Filter/Channel.tsx | 20 +- src/routes/resources/Filter/Date.tsx | 17 +- src/routes/resources/Filter/From.tsx | 14 +- src/routes/resources/Filter/Type.tsx | 32 +- src/routes/resources/Filter/index.tsx | 32 +- src/routes/resources/Search.tsx | 18 +- src/routes/resources/View.tsx | 21 +- src/routes/resources/index.tsx | 28 +- src/routes/sendMagicLink/SentTip.tsx | 11 +- src/routes/sendMagicLink/index.tsx | 37 +- src/routes/setting/APIConfig.tsx | 32 +- src/routes/setting/APIDocument.tsx | 141 +++--- src/routes/setting/BotConfig/BotAPIKeys.tsx | 179 ++++--- .../setting/BotConfig/CreateAPIKeyModal.tsx | 172 +++---- src/routes/setting/BotConfig/CreateModal.tsx | 180 +++---- .../setting/BotConfig/DeleteAPIKeyModal.tsx | 86 ++-- src/routes/setting/BotConfig/DeleteModal.tsx | 86 ++-- .../setting/BotConfig/TestAPIKeyModal.tsx | 188 ++++--- src/routes/setting/BotConfig/WebhookEdit.tsx | 209 ++++---- src/routes/setting/BotConfig/WebhookModal.tsx | 131 ++--- src/routes/setting/BotConfig/index.tsx | 9 +- .../setting/License/LicensePriceListModal.tsx | 121 +++-- .../setting/License/UpdateLicenseModal.tsx | 18 +- src/routes/setting/License/index.tsx | 108 +++-- src/routes/setting/LogoutConfirmModal.tsx | 21 +- src/routes/setting/MyAccount.tsx | 11 +- src/routes/setting/Overview/ChatLayout.tsx | 71 +-- .../setting/Overview/ContactVerification.tsx | 69 +-- src/routes/setting/Overview/DarkMode.tsx | 67 +-- src/routes/setting/Overview/FrontendURL.tsx | 81 ++-- src/routes/setting/Overview/Language.tsx | 46 +- src/routes/setting/Overview/OnlineStatus.tsx | 69 +-- src/routes/setting/Overview/Server.tsx | 7 +- src/routes/setting/Overview/SettingBlock.tsx | 32 +- src/routes/setting/Overview/index.tsx | 19 +- src/routes/setting/Overview/language.tsx | 46 +- src/routes/setting/Overview/server.tsx | 7 +- src/routes/setting/ProfileBasicEditModal.tsx | 17 +- .../setting/RemoveAccountConfirmModal.tsx | 9 +- src/routes/setting/UpdatePasswordModal.tsx | 24 +- src/routes/setting/Widget.tsx | 3 +- src/routes/setting/config/Agora.tsx | 4 +- src/routes/setting/config/Firebase.tsx | 14 +- src/routes/setting/config/HowToTip.tsx | 34 +- .../setting/config/IssuerList/index.tsx | 5 +- src/routes/setting/config/Logins.tsx | 21 +- src/routes/setting/config/SMTP.tsx | 14 +- src/routes/setting/config/Tooltip.tsx | 18 +- src/routes/setting/index.tsx | 4 +- src/routes/setting/navs.tsx | 49 +- .../settingChannel/DeleteConfirmModal.tsx | 13 +- src/routes/settingChannel/Overview.tsx | 46 +- src/routes/settingChannel/index.tsx | 7 +- src/routes/settingChannel/navs.tsx | 5 +- src/routes/settingDM/DeleteConfirmModal.tsx | 13 +- src/routes/settingDM/Overview.tsx | 2 +- src/routes/settingDM/index.tsx | 19 +- src/routes/settingDM/navs.tsx | 5 +- src/routes/users/Search.tsx | 44 +- src/routes/users/index.tsx | 54 ++- src/utils.tsx | 115 +++-- src/widget/Icon.tsx | 12 +- src/widget/Popup/Footer.tsx | 16 +- src/widget/Popup/Header.tsx | 27 +- src/widget/Popup/InviteOnlyTip.tsx | 14 +- src/widget/Popup/Login/index.tsx | 174 +++---- src/widget/Popup/Message/Text.tsx | 56 ++- src/widget/Popup/Message/index.tsx | 101 ++-- src/widget/Popup/MessageFeed.tsx | 130 +++-- src/widget/Popup/MessageInput.tsx | 149 +++--- src/widget/Popup/Welcome.tsx | 30 +- src/widget/Popup/index.tsx | 95 ++-- src/widget/WidgetContext.tsx | 62 ++- src/widget/index.tsx | 16 +- 259 files changed, 7627 insertions(+), 5607 deletions(-) diff --git a/.prettierrc b/.prettierrc index 459cc455..6f09f2f9 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,8 +19,8 @@ "^@/assets/(.*)$", "^[./]" ], + "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"], "importOrderCaseInsensitive": true, "importOrderMergeDuplicateImports": true, - "importOrderSeparation": true, - "importOrderGroupNamespaceSpecifiers": true + "importOrderSeparation": true } diff --git a/.vscode/settings.json b/.vscode/settings.json index 73744e8b..ef24fdf5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,5 +62,8 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/src/components/AddEntriesMenu.tsx b/src/components/AddEntriesMenu.tsx index 7a8899b6..5d03891d 100644 --- a/src/components/AddEntriesMenu.tsx +++ b/src/components/AddEntriesMenu.tsx @@ -1,16 +1,17 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { hideAll } from "tippy.js"; + import { useAppSelector } from "@/app/store"; import IconInvite from "@/assets/icons/add.person.svg"; import IconMention from "@/assets/icons/mention.svg"; import IconSearch from "@/assets/icons/search.svg"; import ChannelIcon from "./ChannelIcon"; import ChannelModal from "./ChannelModal"; -import UsersModal from "./UsersModal"; import InviteModal from "./InviteModal"; -import { useTranslation } from "react-i18next"; import SearchUser from "./SearchUser"; import ServerVersionChecker from "./ServerVersionChecker"; +import UsersModal from "./UsersModal"; export default function AddEntriesMenu() { const { t } = useTranslation(); @@ -53,7 +54,8 @@ export default function AddEntriesMenu() { setChannelModalVisible(false); }; - const itemClass = "rounded flex items-center gap-2 text-sm font-semibold cursor-pointer px-2 py-2.5 md:hover:bg-gray-800/20 md:dark:hover:bg-gray-200/20"; + const itemClass = + "rounded flex items-center gap-2 text-sm font-semibold cursor-pointer px-2 py-2.5 md:hover:bg-gray-800/20 md:dark:hover:bg-gray-200/20"; const iconClass = "w-5 h-5 dark:fill-gray-300"; return ( <> diff --git a/src/components/AutoDeleteMessages.tsx b/src/components/AutoDeleteMessages.tsx index c9c90324..54c45e11 100644 --- a/src/components/AutoDeleteMessages.tsx +++ b/src/components/AutoDeleteMessages.tsx @@ -1,97 +1,105 @@ -import { useState, useEffect } from 'react'; -import { toast } from 'react-hot-toast'; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; import { useUpdateAutoDeleteMsgMutation } from "@/app/services/user"; -import { useAppSelector } from '../app/store'; +import { useLazyClearChannelMessageQuery } from "../app/services/channel"; +import { useAppSelector } from "../app/store"; +import { ChatContext } from "../types/common"; import SaveTip from "./SaveTip"; -import StyledRadio from "./styled/Radio"; -import { ChatContext } from '../types/common'; import StyledButton from "./styled/Button"; -import { useLazyClearChannelMessageQuery } from '../app/services/channel'; - +import StyledRadio from "./styled/Radio"; type Props = { - id: number, - type?: ChatContext, - // expires_in?: number -} + id: number; + type?: ChatContext; + // expires_in?: number +}; const AutoDeleteMessages = ({ id, type = "channel" }: Props) => { - const { setting, channel, loginUser } = useAppSelector(store => { - return { - setting: type == "channel" ? store.footprint.autoDeleteMsgChannels.find(item => item.gid == id) : store.footprint.autoDeleteMsgUsers.find(item => item.uid == id), - loginUser: store.authData.user, - channel: type == "channel" ? store.channels.byId[id] : null - }; - }); - const [updateSetting, { isSuccess }] = useUpdateAutoDeleteMsgMutation(); - const [clearMessage, { isSuccess: clearSuccess }] = useLazyClearChannelMessageQuery(); + const { setting, channel, loginUser } = useAppSelector((store) => { + return { + setting: + type == "channel" + ? store.footprint.autoDeleteMsgChannels.find((item) => item.gid == id) + : store.footprint.autoDeleteMsgUsers.find((item) => item.uid == id), + loginUser: store.authData.user, + channel: type == "channel" ? store.channels.byId[id] : null + }; + }); + const [updateSetting, { isSuccess }] = useUpdateAutoDeleteMsgMutation(); + const [clearMessage, { isSuccess: clearSuccess }] = useLazyClearChannelMessageQuery(); - const [value, setValue] = useState(setting?.expires_in ?? 0); - const { t } = useTranslation("setting", { keyPrefix: "auto_delete_msg" }); - const { t: ct } = useTranslation(); - const options = [ - { title: t("off"), value: 0 }, - { title: t("5_min"), value: 5 * 60 }, - { title: t("10_min"), value: 10 * 60 }, - { title: t("1_hour"), value: 60 * 60 }, - { title: t("1_day"), value: 24 * 60 * 60 }, - { title: t("1_week"), value: 7 * 24 * 60 * 60 }, - ]; - const handleSave = () => { - const dto = type == "dm" ? { users: [{ uid: id, expires_in: value }] } : { groups: [{ gid: id, expires_in: value }] }; - updateSetting(dto); - }; - const handleReset = () => { - setValue(setting?.expires_in ?? 0); - }; - const handleChange = (val: number) => { - setValue(val); - }; - useEffect(() => { - if (isSuccess) { - toast.success(ct("tip.update")); - } - }, [isSuccess]); - useEffect(() => { - if (clearSuccess) { - toast.success("Cleared!"); - } - }, [clearSuccess]); - const handleClear = () => { - if (confirm("are you sure?")) { - clearMessage(id); - } - }; - const originalVal = setting?.expires_in ?? 0; - const showClear = type == "channel" && (channel?.owner == loginUser?.uid || loginUser?.is_admin); - return ( -
-
-

{t("title")}

-

{t("desc")}

-
-
- title)} - values={options.map(({ value }) => value)} - value={value || 0} - onChange={handleChange} - /> -
- {originalVal !== value && } - {showClear && <> -
-

{t("clear_title")}

-

{t("clear_desc")}

-
-
- {t("clear")} -
- - } -
- ); + const [value, setValue] = useState(setting?.expires_in ?? 0); + const { t } = useTranslation("setting", { keyPrefix: "auto_delete_msg" }); + const { t: ct } = useTranslation(); + const options = [ + { title: t("off"), value: 0 }, + { title: t("5_min"), value: 5 * 60 }, + { title: t("10_min"), value: 10 * 60 }, + { title: t("1_hour"), value: 60 * 60 }, + { title: t("1_day"), value: 24 * 60 * 60 }, + { title: t("1_week"), value: 7 * 24 * 60 * 60 } + ]; + const handleSave = () => { + const dto = + type == "dm" + ? { users: [{ uid: id, expires_in: value }] } + : { groups: [{ gid: id, expires_in: value }] }; + updateSetting(dto); + }; + const handleReset = () => { + setValue(setting?.expires_in ?? 0); + }; + const handleChange = (val: number) => { + setValue(val); + }; + useEffect(() => { + if (isSuccess) { + toast.success(ct("tip.update")); + } + }, [isSuccess]); + useEffect(() => { + if (clearSuccess) { + toast.success("Cleared!"); + } + }, [clearSuccess]); + const handleClear = () => { + if (confirm("are you sure?")) { + clearMessage(id); + } + }; + const originalVal = setting?.expires_in ?? 0; + const showClear = type == "channel" && (channel?.owner == loginUser?.uid || loginUser?.is_admin); + return ( +
+
+

{t("title")}

+

{t("desc")}

+
+
+ title)} + values={options.map(({ value }) => value)} + value={value || 0} + onChange={handleChange} + /> +
+ {originalVal !== value && } + {showClear && ( + <> +
+

{t("clear_title")}

+

{t("clear_desc")}

+
+
+ + {t("clear")} + +
+ + )} +
+ ); }; -export default AutoDeleteMessages; \ No newline at end of file +export default AutoDeleteMessages; diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index bf41d4cc..01c80f97 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,4 +1,5 @@ import { FC, ImgHTMLAttributes, useState } from "react"; + import { getInitials } from "../utils"; interface Props extends ImgHTMLAttributes { @@ -53,7 +54,9 @@ const Avatar: FC = ({ color: type === "channel" ? "#475467" : "#FFFFFF" }} > - {initials} + + {initials} + ); }; diff --git a/src/components/AvatarUploader.tsx b/src/components/AvatarUploader.tsx index ef703967..345fcabc 100644 --- a/src/components/AvatarUploader.tsx +++ b/src/components/AvatarUploader.tsx @@ -1,9 +1,10 @@ import { ChangeEvent, FC, useState } from "react"; -import Avatar from "./Avatar"; -import uploadIcon from "@/assets/icons/upload.image.svg?url"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; +import uploadIcon from "@/assets/icons/upload.image.svg?url"; +import Avatar from "./Avatar"; + type UID = number; interface Props { size?: number; @@ -41,11 +42,20 @@ const AvatarUploader: FC = ({ setUploading(false); }; - return ( -
+
- + {!disabled && ( <>
@@ -63,7 +73,13 @@ const AvatarUploader: FC = ({ )}
- {!disabled && icon} + {!disabled && ( + icon + )}
); }; diff --git a/src/components/BlankPlaceholder.tsx b/src/components/BlankPlaceholder.tsx index d44a75f2..fe87178c 100644 --- a/src/components/BlankPlaceholder.tsx +++ b/src/components/BlankPlaceholder.tsx @@ -3,16 +3,15 @@ import { useTranslation } from "react-i18next"; import { NavLink } from "react-router-dom"; import Linkify from "linkify-react"; -import ChannelModal from "./ChannelModal"; -import InviteModal from "./InviteModal"; +import { useAppSelector } from "@/app/store"; import IconEdit from "@/assets/icons/edit.svg"; import IconChat from "@/assets/icons/placeholder.chat.svg"; -import IconAsk from "@/assets/icons/placeholder.question.svg"; -import IconInvite from "@/assets/icons/placeholder.invite.svg"; import IconDownload from "@/assets/icons/placeholder.download.svg"; +import IconInvite from "@/assets/icons/placeholder.invite.svg"; +import IconAsk from "@/assets/icons/placeholder.question.svg"; +import ChannelModal from "./ChannelModal"; +import InviteModal from "./InviteModal"; import UsersModal from "./UsersModal"; -import { useAppSelector } from "@/app/store"; - interface Props { type?: "chat" | "user"; @@ -24,7 +23,13 @@ const classes = { }; const BlankPlaceholder: FC = ({ type = "chat" }) => { const { t } = useTranslation("welcome"); - const { server, isAdmin, upgraded } = useAppSelector((store) => { return { server: store.server, isAdmin: store.authData.user?.is_admin, upgraded: store.server.upgraded }; }); + const { server, isAdmin, upgraded } = useAppSelector((store) => { + return { + server: store.server, + isAdmin: store.authData.user?.is_admin, + upgraded: store.server.upgraded + }; + }); const [inviteModalVisible, setInviteModalVisible] = useState(false); const [createChannelVisible, setCreateChannelVisible] = useState(false); const [userListVisible, setUserListVisible] = useState(false); @@ -37,33 +42,46 @@ const BlankPlaceholder: FC = ({ type = "chat" }) => { const toggleInviteModalVisible = () => { setInviteModalVisible((prev) => !prev); }; - const chatTip = - type == "chat" ? t("start_by_channel") : t("start_by_dm"); + const chatTip = type == "chat" ? t("start_by_channel") : t("start_by_dm"); const chatHandler = type == "chat" ? toggleChannelModalVisible : toggleUserListVisible; return ( <>
-

{t("title", { name: server.name })}

+

+ {t("title", { name: server.name })} +

- { - return <> - - {content} - - ; - }, + return ( + <> + + {content} + + + ); + } } - } - }> + }} + > {server.description ? server.description : t("desc")} - {isAdmin && - - } + {isAdmin && ( + + + + )}

@@ -71,21 +89,32 @@ const BlankPlaceholder: FC = ({ type = "chat" }) => {
{t("invite")}
- - {!upgraded && <> - - -
{t("download")}
-
- - -
{t("help")}
-
- - } + {!upgraded && ( + <> + + +
{t("download")}
+
+ + +
{t("help")}
+
+ + )}
{createChannelVisible && ( diff --git a/src/components/Channel.tsx b/src/components/Channel.tsx index 1da18c0e..10e8e94b 100644 --- a/src/components/Channel.tsx +++ b/src/components/Channel.tsx @@ -1,8 +1,8 @@ import { FC } from "react"; -import Avatar from "./Avatar"; -import { useAppSelector } from "@/app/store"; import clsx from "clsx"; +import { useAppSelector } from "@/app/store"; +import Avatar from "./Avatar"; interface Props { interactive?: boolean; @@ -24,9 +24,16 @@ const Channel: FC = ({ interactive = true, id, compact = false, avatarSiz return (
-
+
= ({ interactive = true, id, compact = false, avatarSiz
{!compact && (
- {name} ({is_public ? totalMemberCount : members.length}) + {name} ( + {is_public ? totalMemberCount : members.length})
)}
diff --git a/src/components/ChannelIcon.tsx b/src/components/ChannelIcon.tsx index 73b114b0..ea16c663 100644 --- a/src/components/ChannelIcon.tsx +++ b/src/components/ChannelIcon.tsx @@ -1,6 +1,7 @@ import { FC } from "react"; -import HashIcon from "@/assets/icons/channel.svg"; + import LockHashIcon from "@/assets/icons/channel.private.svg"; +import HashIcon from "@/assets/icons/channel.svg"; interface Props { personal?: boolean; @@ -11,7 +12,11 @@ interface Props { const ChannelIcon: FC = ({ personal = false, muted = false, className = "" }) => { return (
- {personal ? : } + {personal ? ( + + ) : ( + + )}
); }; diff --git a/src/components/ChannelModal/index.tsx b/src/components/ChannelModal/index.tsx index 93682be2..287f73c7 100644 --- a/src/components/ChannelModal/index.tsx +++ b/src/components/ChannelModal/index.tsx @@ -1,20 +1,20 @@ -import { useState, useEffect, FC, MouseEvent, ChangeEvent } from "react"; +import { ChangeEvent, FC, MouseEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; -import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import i18n from "@/i18n"; import clsx from "clsx"; -import Modal from "../Modal"; -import Button from "../styled/Button"; -import ChannelIcon from "../ChannelIcon"; -import User from "../User"; -import StyledCheckbox from "../styled/Checkbox"; -import StyledToggle from "../styled/Toggle"; -import useFilteredUsers from "@/hooks/useFilteredUsers"; import { useCreateChannelMutation, useSendChannelMsgMutation } from "@/app/services/channel"; import { useAppSelector } from "@/app/store"; import { CreateChannelDTO } from "@/types/channel"; -import i18n from "@/i18n"; +import useFilteredUsers from "@/hooks/useFilteredUsers"; +import ChannelIcon from "../ChannelIcon"; +import Modal from "../Modal"; +import Button from "../styled/Button"; +import StyledCheckbox from "../styled/Checkbox"; +import StyledToggle from "../styled/Toggle"; +import User from "../User"; interface Props { personal?: boolean; @@ -66,7 +66,7 @@ const ChannelModal: FC = ({ personal = false, closeModal }) => { }, [isError]); useEffect(() => { - const id = typeof newChannel == 'object' ? newChannel.gid : newChannel; + const id = typeof newChannel == "object" ? newChannel.gid : newChannel; if (isSuccess && id && channelData[id]) { const name = channelData[id].name; // 发个欢迎消息 @@ -78,7 +78,6 @@ const ChannelModal: FC = ({ personal = false, closeModal }) => { } }, [isSuccess, newChannel, channelData]); - const handleNameInput = (evt: ChangeEvent) => { setData((prev) => ({ ...prev, name: evt.target.value })); }; @@ -138,18 +137,31 @@ const ChannelModal: FC = ({ personal = false, closeModal }) => { )}
)} -
-

{t("create_channel")}

+
+

+ {t("create_channel")} +

- {!is_public - ? t("create_private_channel_desc") - : t("create_channel_desc")} + {!is_public ? t("create_private_channel_desc") : t("create_channel_desc")}

{t("channel_name")}
- - + +
diff --git a/src/components/DeleteMessageConfirm.tsx b/src/components/DeleteMessageConfirm.tsx index 13891b40..16ac98db 100644 --- a/src/components/DeleteMessageConfirm.tsx +++ b/src/components/DeleteMessageConfirm.tsx @@ -1,10 +1,11 @@ import { FC, useState } from "react"; import { useTranslation } from "react-i18next"; + import useDeleteMessage from "@/hooks/useDeleteMessage"; -import StyledModal from "./styled/Modal"; -import Button from "./styled/Button"; -import Modal from "./Modal"; import PreviewMessage from "./Message/PreviewMessage"; +import Modal from "./Modal"; +import Button from "./styled/Button"; +import StyledModal from "./styled/Modal"; interface Props { closeModal: (b: boolean) => void; diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx index 52d10118..dbaf94e7 100644 --- a/src/components/Divider.tsx +++ b/src/components/Divider.tsx @@ -1,14 +1,24 @@ import { FC } from "react"; import clsx from "clsx"; + interface Props { content: string; - className?: string + className?: string; } const Divider: FC = ({ content, className = "" }) => { - return
- {content} -
; + return ( +
+ + {content} + +
+ ); }; export default Divider; diff --git a/src/components/ErrorCatcher.tsx b/src/components/ErrorCatcher.tsx index 21ddcca8..65f6e936 100644 --- a/src/components/ErrorCatcher.tsx +++ b/src/components/ErrorCatcher.tsx @@ -1,27 +1,25 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode } from "react"; import { ErrorBoundary } from "react-error-boundary"; type FallbackProps = { - error: Error, - resetErrorBoundary: (...args: Array) => void -} + error: Error; + resetErrorBoundary: (...args: Array) => void; +}; function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { - return ( -
-

Something went wrong:

-
{error.message}
- -
- ); + return ( +
+

Something went wrong:

+
{error.message}
+ +
+ ); } type Props = { - children: ReactNode -} - -const ErrorCatcher = ({ children }: Props) => { - return ( - {children} - ); + children: ReactNode; }; -export default ErrorCatcher; \ No newline at end of file +const ErrorCatcher = ({ children }: Props) => { + return {children}; +}; + +export default ErrorCatcher; diff --git a/src/components/FileBox/index.tsx b/src/components/FileBox/index.tsx index 586b1fc8..7975a637 100644 --- a/src/components/FileBox/index.tsx +++ b/src/components/FileBox/index.tsx @@ -1,16 +1,17 @@ import { FC, ReactElement } from "react"; import clsx from "clsx"; + +import { useAppSelector } from "@/app/store"; +import { formatBytes, fromNowTime, getFileIcon } from "@/utils"; +import IconDownload from "@/assets/icons/download.svg"; import { - VideoPreview, AudioPreview, + CodePreview, + DocPreview, ImagePreview, PdfPreview, - CodePreview, - DocPreview + VideoPreview } from "./preview"; -import { getFileIcon, formatBytes, fromNowTime } from "@/utils"; -import IconDownload from "@/assets/icons/download.svg"; -import { useAppSelector } from "@/app/store"; interface Data { file_type: string; @@ -86,12 +87,19 @@ const FileBox: FC = ({ return (
{icon}
- {name} + + {name} + {formatBytes(size)} {fromNowTime(created_at)} @@ -100,7 +108,11 @@ const FileBox: FC = ({
- +
diff --git a/src/components/FileBox/preview/Code.tsx b/src/components/FileBox/preview/Code.tsx index c3a10c12..871ed56b 100644 --- a/src/components/FileBox/preview/Code.tsx +++ b/src/components/FileBox/preview/Code.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, FC } from "react"; +import { FC, useEffect, useState } from "react"; interface Props { url: string; @@ -17,7 +17,11 @@ const Code: FC = ({ url }) => { }, [url]); if (!content) return null; - return
{content}
; + return ( +
+ {content} +
+ ); }; export default Code; diff --git a/src/components/FileBox/preview/Doc.tsx b/src/components/FileBox/preview/Doc.tsx index 926cedd3..fd812a22 100644 --- a/src/components/FileBox/preview/Doc.tsx +++ b/src/components/FileBox/preview/Doc.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, FC } from "react"; +import { FC, useEffect, useState } from "react"; interface Props { url: string; @@ -17,7 +17,9 @@ const Doc: FC = ({ url }) => { }, [url]); if (!content) return null; - return
{content}
; + return ( +
{content}
+ ); }; export default Doc; diff --git a/src/components/FileBox/preview/Image.tsx b/src/components/FileBox/preview/Image.tsx index e003d5f7..8544847c 100644 --- a/src/components/FileBox/preview/Image.tsx +++ b/src/components/FileBox/preview/Image.tsx @@ -1,9 +1,10 @@ import { FC, useEffect, useState } from "react"; -import clsx from "clsx"; import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query"; import { LineWobble } from "@uiball/loaders"; +import clsx from "clsx"; import { useLazyPreCheckFileFromUrlQuery } from "@/app/services/message"; + interface Props { url: string; alt?: string; @@ -46,13 +47,23 @@ const ImageBox: FC = ({ url, alt }) => { } }, [isSuccess, error, url]); - return ( -
- {status == "loaded" ? {alt} : ( - status == "loading" ? : - - status == 404 ? File not found, removed maybe : Load image error +
+ {status == "loaded" ? ( + {alt} + ) : status == "loading" ? ( + + + + ) : status == 404 ? ( + File not found, removed maybe + ) : ( + Load image error )}
); diff --git a/src/components/FileBox/preview/Video.tsx b/src/components/FileBox/preview/Video.tsx index db412592..e2c7fb87 100644 --- a/src/components/FileBox/preview/Video.tsx +++ b/src/components/FileBox/preview/Video.tsx @@ -1,4 +1,5 @@ import React, { FC } from "react"; + interface Props { url: string; } diff --git a/src/components/FileBox/preview/index.tsx b/src/components/FileBox/preview/index.tsx index cbefb81a..786a5f7f 100644 --- a/src/components/FileBox/preview/index.tsx +++ b/src/components/FileBox/preview/index.tsx @@ -1,8 +1,8 @@ -import VideoPreview from "./Video"; import AudioPreview from "./Audio"; -import ImagePreview from "./Image"; -import PdfPreview from "./Pdf"; import CodePreview from "./Code"; import DocPreview from "./Doc"; +import ImagePreview from "./Image"; +import PdfPreview from "./Pdf"; +import VideoPreview from "./Video"; export { VideoPreview, AudioPreview, ImagePreview, PdfPreview, CodePreview, DocPreview }; diff --git a/src/components/FileMessage/AudioMessage.tsx b/src/components/FileMessage/AudioMessage.tsx index 2cf32c82..ea68b818 100644 --- a/src/components/FileMessage/AudioMessage.tsx +++ b/src/components/FileMessage/AudioMessage.tsx @@ -1,37 +1,47 @@ -import { useState } from 'react'; -import { formatBytes } from '../../utils'; +import { useState } from "react"; +import clsx from "clsx"; + import IconDownload from "@/assets/icons/download.svg"; import IconAudio from "@/assets/icons/file.audio.svg"; -import clsx from 'clsx'; +import { formatBytes } from "../../utils"; type Props = { - url: string, - name: string, - size: number, - download: string -} - -const AudioMessage = ({ url, name, size, download }: Props) => { - const [canPlay, setCanPlay] = useState(false); - const handleCanPlay = () => { - setCanPlay(true); - }; - const _size = formatBytes(size); - return ( -
-
-
- -
- {name} - {_size} -
-
- -
-
- ); + url: string; + name: string; + size: number; + download: string; }; -export default AudioMessage; \ No newline at end of file +const AudioMessage = ({ url, name, size, download }: Props) => { + const [canPlay, setCanPlay] = useState(false); + const handleCanPlay = () => { + setCanPlay(true); + }; + const _size = formatBytes(size); + return ( +
+
+
+ +
+ + {name} + + {_size} +
+
+ + + +
+
+ ); +}; + +export default AudioMessage; diff --git a/src/components/FileMessage/ImageMessage.tsx b/src/components/FileMessage/ImageMessage.tsx index 5f8ff25e..fb760fc3 100644 --- a/src/components/FileMessage/ImageMessage.tsx +++ b/src/components/FileMessage/ImageMessage.tsx @@ -1,5 +1,6 @@ -import { useState, useEffect, FC } from "react"; -import { Ping, LineWobble } from '@uiball/loaders'; +import { FC, useEffect, useState } from "react"; +import { LineWobble, Ping } from "@uiball/loaders"; + import { getDefaultSize, isMobile } from "@/utils"; type Props = { @@ -21,7 +22,10 @@ const ImageMessage: FC = ({ }) => { const url = thumbnail || content; const [status, setStatus] = useState<"loading" | "error" | "loaded">("loading"); - const { width = 0, height = 0 } = getDefaultSize(properties, { min: 200, max: isMobile() ? 300 : 480 }); + const { width = 0, height = 0 } = getDefaultSize(properties, { + min: 200, + max: isMobile() ? 300 : 480 + }); useEffect(() => { const img = new Image(); img.onload = () => { @@ -34,25 +38,29 @@ const ImageMessage: FC = ({ }, [url]); return ( -
{uploading && (
- + {progress}%
)} - {status == "error" ?

load image error

: - status == "loading" ?

+ {status == "error" ? ( +

+ load image error +

+ ) : status == "loading" ? ( +

-

: + ) : ( + = ({ data-origin={content} data-download={download} src={url} - />} + /> + )}
); }; diff --git a/src/components/FileMessage/Progress.tsx b/src/components/FileMessage/Progress.tsx index 579b5539..75f8e154 100644 --- a/src/components/FileMessage/Progress.tsx +++ b/src/components/FileMessage/Progress.tsx @@ -1,4 +1,5 @@ import { FC } from "react"; + interface Props { value: number; width?: string; @@ -7,7 +8,10 @@ interface Props { const Progress: FC = ({ value, width = "100%" }) => { return (
-
+
); }; diff --git a/src/components/FileMessage/VideoMessage.tsx b/src/components/FileMessage/VideoMessage.tsx index 378de907..14458ad6 100644 --- a/src/components/FileMessage/VideoMessage.tsx +++ b/src/components/FileMessage/VideoMessage.tsx @@ -1,67 +1,79 @@ -import { SyntheticEvent, useState } from 'react'; +import { SyntheticEvent, useState } from "react"; import { Orbit } from "@uiball/loaders"; -import { formatBytes } from '../../utils'; + import IconDownload from "@/assets/icons/download.svg"; import IconVideo from "@/assets/icons/file.video.svg"; +import { formatBytes } from "../../utils"; + // import clsx from 'clsx'; type Props = { - url: string, - name: string, - size: number, - download: string -} - -const VideoMessage = ({ url, name, size, download }: Props) => { - const [canPlay, setCanPlay] = useState(false); - const [error, setError] = useState(false); - const _size = formatBytes(size); - const handlePlay = (evt: SyntheticEvent) => { - const infoEle = evt.currentTarget.previousSibling; - if (infoEle) { - (infoEle as HTMLDivElement).classList.add("hidden"); - } - }; - const handlePause = (evt: SyntheticEvent) => { - const infoEle = evt.currentTarget.previousSibling; - if (infoEle) { - (infoEle as HTMLDivElement).classList.remove("hidden"); - } - }; - const handleCanPlay = () => { - setCanPlay(true); - }; - const handleError = () => { - setCanPlay(false); - setError(true); - }; - const tipClass = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'; - return ( -
-
-
-
- -
- {name} - {_size} -
-
- -
- {!canPlay ? -
- -
: - (error ? - Error : - null) - } - -
- ); + url: string; + name: string; + size: number; + download: string; }; -export default VideoMessage; \ No newline at end of file +const VideoMessage = ({ url, name, size, download }: Props) => { + const [canPlay, setCanPlay] = useState(false); + const [error, setError] = useState(false); + const _size = formatBytes(size); + const handlePlay = (evt: SyntheticEvent) => { + const infoEle = evt.currentTarget.previousSibling; + if (infoEle) { + (infoEle as HTMLDivElement).classList.add("hidden"); + } + }; + const handlePause = (evt: SyntheticEvent) => { + const infoEle = evt.currentTarget.previousSibling; + if (infoEle) { + (infoEle as HTMLDivElement).classList.remove("hidden"); + } + }; + const handleCanPlay = () => { + setCanPlay(true); + }; + const handleError = () => { + setCanPlay(false); + setError(true); + }; + const tipClass = "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"; + return ( +
+
+
+
+ +
+ + {name} + + {_size} +
+
+ + + +
+ {!canPlay ? ( +
+ +
+ ) : error ? ( + Error + ) : null} + +
+ ); +}; + +export default VideoMessage; diff --git a/src/components/FileMessage/index.tsx b/src/components/FileMessage/index.tsx index 8bb8cffb..60a4645a 100644 --- a/src/components/FileMessage/index.tsx +++ b/src/components/FileMessage/index.tsx @@ -1,17 +1,18 @@ import { FC, useEffect, useState } from "react"; -import ImageMessage from "./ImageMessage"; -import useRemoveLocalMessage from "@/hooks/useRemoveLocalMessage"; -import useUploadFile from "@/hooks/useUploadFile"; -import useSendMessage from "@/hooks/useSendMessage"; -import Progress from "./Progress"; -import { getFileIcon, formatBytes, isImage, getImageSize, fromNowTime } from "@/utils"; -import { useAppSelector } from "@/app/store"; -import IconDownload from "@/assets/icons/download.svg"; -import IconClose from "@/assets/icons/close.circle.svg"; -import VideoMessage from "./VideoMessage"; -import AudioMessage from "./AudioMessage"; import clsx from "clsx"; + +import { useAppSelector } from "@/app/store"; import { ChatContext } from "@/types/common"; +import useRemoveLocalMessage from "@/hooks/useRemoveLocalMessage"; +import useSendMessage from "@/hooks/useSendMessage"; +import useUploadFile from "@/hooks/useUploadFile"; +import { formatBytes, fromNowTime, getFileIcon, getImageSize, isImage } from "@/utils"; +import IconClose from "@/assets/icons/close.circle.svg"; +import IconDownload from "@/assets/icons/download.svg"; +import AudioMessage from "./AudioMessage"; +import ImageMessage from "./ImageMessage"; +import Progress from "./Progress"; +import VideoMessage from "./VideoMessage"; const isLocalFile = (content: string) => { return content.startsWith("blob:"); @@ -55,7 +56,14 @@ const FileMessage: FC = ({ from: from_uid, to }); - const { stopUploading, data, uploadFile, progress, isSuccess: uploadSuccess, isError } = useUploadFile(); + const { + stopUploading, + data, + uploadFile, + progress, + isSuccess: uploadSuccess, + isError + } = useUploadFile(); const fromUser = useAppSelector((store) => store.users.byId[from_uid]); const { size = 0, name, content_type } = properties ?? {}; useEffect(() => { @@ -145,30 +153,23 @@ const FileMessage: FC = ({ ); // video if (content_type.startsWith("video") && !sending) - return ( - - ); + return ; // audio if (content_type.startsWith("audio") && !sending) - return ( - - ); + return ; return ( -
+
{icon}
- {name} + + {name} + {sending ? ( @@ -188,7 +189,11 @@ const FileMessage: FC = ({ {sending ? ( ) : ( - + )} diff --git a/src/components/ForwardModal/index.tsx b/src/components/ForwardModal/index.tsx index 56609153..921e7962 100644 --- a/src/components/ForwardModal/index.tsx +++ b/src/components/ForwardModal/index.tsx @@ -1,19 +1,20 @@ -import { useState, MouseEvent, ChangeEvent, FC } from "react"; +import { ChangeEvent, FC, MouseEvent, useState } from "react"; import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; -import Modal from "../Modal"; -import Button from "../styled/Button"; -import Input from "../styled/Input"; -import Channel from "../Channel"; -import User from "../User"; -import Reply from "../Message/Reply"; -import useForwardMessage from "@/hooks/useForwardMessage"; -import useSendMessage from "@/hooks/useSendMessage"; import useFilteredChannels from "@/hooks/useFilteredChannels"; import useFilteredUsers from "@/hooks/useFilteredUsers"; +import useForwardMessage from "@/hooks/useForwardMessage"; +import useSendMessage from "@/hooks/useSendMessage"; import CloseIcon from "@/assets/icons/close.circle.svg"; +import Channel from "../Channel"; +import Reply from "../Message/Reply"; +import Modal from "../Modal"; +import Button from "../styled/Button"; import StyledCheckbox from "../styled/Checkbox"; -import { useTranslation } from "react-i18next"; +import Input from "../styled/Input"; +import User from "../User"; + interface IProps { mids: number[]; closeModal: () => void; @@ -140,7 +141,10 @@ const ForwardModal: FC = ({ mids, closeModal }) => { return (
  • - +
  • ); })} @@ -158,7 +162,7 @@ const ForwardModal: FC = ({ mids, closeModal }) => { >
    - ); +import IconArrow from "@/assets/icons/arrow.left.svg"; + +type Props = { + path?: string; + className?: string; }; -export default GoBackNav; \ No newline at end of file +const GoBackNav = ({ path, className = "" }: Props) => { + const navigate = useNavigate(); + const isChannelChatPage = useMatch("/chat/channel/:channel_id"); + const isDMChatPage = useMatch("/chat/dm/:user_id"); + const isProfilePage = useMatch("/users/:user_id"); + // console.log("routt", isChannelChatPage, isDMChatPage); + const isChatPage = !!isChannelChatPage || !!isDMChatPage; + const handleBack = () => { + if (path) { + navigate(path); + } else if (isChatPage) { + navigate("/chat"); + } else if (isProfilePage) { + navigate("/users"); + } else { + navigate(-1); + } + }; + return ( + + ); +}; + +export default GoBackNav; diff --git a/src/components/GoogleLoginButton.tsx b/src/components/GoogleLoginButton.tsx index 0b89dffc..005eff75 100644 --- a/src/components/GoogleLoginButton.tsx +++ b/src/components/GoogleLoginButton.tsx @@ -1,12 +1,12 @@ import { FC, Suspense, useEffect, useState } from "react"; -import { GoogleLogin, GoogleOAuthProvider } from "@react-oauth/google"; import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { GoogleLogin, GoogleOAuthProvider } from "@react-oauth/google"; + import { KEY_LOCAL_MAGIC_TOKEN } from "@/app/config"; +import { useLoginMutation } from "@/app/services/auth"; import IconGoogle from "@/assets/icons/google.svg"; import Button from "./styled/Button"; -import { useLoginMutation } from "@/app/services/auth"; -import { useTranslation } from "react-i18next"; - interface Props { loadError?: boolean; @@ -43,14 +43,17 @@ const GoogleLoginInner: FC = ({ type = "login", loaded, loadError }) => { }, [error]); return ( - +

    {t("inactive.desc")}

    +
    -
    ); }; diff --git a/src/components/InviteLink.tsx b/src/components/InviteLink.tsx index 034cf92d..b2a8b5e5 100644 --- a/src/components/InviteLink.tsx +++ b/src/components/InviteLink.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; import { useTranslation } from "react-i18next"; + import useInviteLink from "@/hooks/useInviteLink"; -import Input from "./styled/Input"; -import Button from "./styled/Button"; +import IconQuestion from "@/assets/icons/question.svg"; import QRCode from "./QRCode"; -import IconQuestion from '@/assets/icons/question.svg'; +import Button from "./styled/Button"; +import Input from "./styled/Input"; type Props = {}; const InviteLink: FC = () => { @@ -18,13 +19,21 @@ const InviteLink: FC = () => {

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

    -
    diff --git a/src/components/InviteModal/AddMembers.tsx b/src/components/InviteModal/AddMembers.tsx index 834a3566..5369c785 100644 --- a/src/components/InviteModal/AddMembers.tsx +++ b/src/components/InviteModal/AddMembers.tsx @@ -1,13 +1,14 @@ -import { useState, useEffect, FC, MouseEvent, ChangeEvent } from "react"; +import { ChangeEvent, FC, MouseEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; + +import { useAddMembersMutation } from "@/app/services/channel"; +import { useAppSelector } from "@/app/store"; +import useFilteredUsers from "@/hooks/useFilteredUsers"; +import CloseIcon from "@/assets/icons/close.svg"; import Button from "../styled/Button"; +import StyledCheckbox from "../styled/Checkbox"; import Input from "../styled/Input"; import User from "../User"; -import StyledCheckbox from "../styled/Checkbox"; -import { useAddMembersMutation } from "@/app/services/channel"; -import CloseIcon from "@/assets/icons/close.svg"; -import useFilteredUsers from "@/hooks/useFilteredUsers"; -import { useAppSelector } from "@/app/store"; interface Props { cid?: number; @@ -60,9 +61,16 @@ const AddMembers: FC = ({ cid = 0, closeModal }) => {
      {selects.map((uid) => { return ( -
    • +
    • {userData[uid]?.name} - +
    • ); })} @@ -97,7 +105,11 @@ const AddMembers: FC = ({ cid = 0, closeModal }) => { ); })}
    -
    diff --git a/src/components/InviteModal/InviteByEmail.tsx b/src/components/InviteModal/InviteByEmail.tsx index 3a6b691e..de93079f 100644 --- a/src/components/InviteModal/InviteByEmail.tsx +++ b/src/components/InviteModal/InviteByEmail.tsx @@ -1,6 +1,7 @@ -import { useEffect, useState, ChangeEvent, FC, useRef } from "react"; +import { ChangeEvent, FC, useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; + import { useSendLoginMagicLinkMutation } from "@/app/services/auth"; import useInviteLink from "@/hooks/useInviteLink"; import QRCode from "../QRCode"; @@ -12,7 +13,6 @@ interface Props { } const InviteByEmail: FC = ({ cid }) => { - const { t } = useTranslation("chat"); const { t: ct } = useTranslation(); const [email, setEmail] = useState(""); @@ -27,7 +27,6 @@ const InviteByEmail: FC = ({ cid }) => { }, [linkCopied]); useEffect(() => { if (isSuccess) { - toast.success("Email Sent!"); } }, [isSuccess]); @@ -49,7 +48,9 @@ const InviteByEmail: FC = ({ cid }) => { return (
    - +
    = ({ cid }) => { placeholder={enableSMTP ? "Enter Email" : t("enable_smtp")} />
    -
    - +
    -
    -
    - {!generating && } -
    +
    {!generating && }
    {t("invite_link_expire")} -
    diff --git a/src/components/InviteModal/index.tsx b/src/components/InviteModal/index.tsx index 691951af..6f2250a3 100644 --- a/src/components/InviteModal/index.tsx +++ b/src/components/InviteModal/index.tsx @@ -1,11 +1,11 @@ -import InviteByEmail from "./InviteByEmail"; -import AddMembers from "./AddMembers"; -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"; +import { useAppSelector } from "@/app/store"; +import CloseIcon from "@/assets/icons/close.svg"; +import Modal from "../Modal"; +import AddMembers from "./AddMembers"; +import InviteByEmail from "./InviteByEmail"; interface Props { type?: "server" | "channel"; diff --git a/src/components/LeaveChannel/LeaveConfirmModal.tsx b/src/components/LeaveChannel/LeaveConfirmModal.tsx index a7fcbe88..92d1dcfb 100644 --- a/src/components/LeaveChannel/LeaveConfirmModal.tsx +++ b/src/components/LeaveChannel/LeaveConfirmModal.tsx @@ -1,11 +1,12 @@ -import { useEffect, FC } from "react"; +import { FC, useEffect } from "react"; import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; + import useLeaveChannel from "@/hooks/useLeaveChannel"; import Modal from "../Modal"; -import StyledModal from "../styled/Modal"; import Button from "../styled/Button"; -import { useTranslation } from "react-i18next"; +import StyledModal from "../styled/Modal"; interface Props { id: number; @@ -31,11 +32,7 @@ const LeaveConfirmModal: FC = ({ id, closeModal, handleNextStep }) => {
    diff --git a/src/components/ManageMembers/MemberList.tsx b/src/components/ManageMembers/MemberList.tsx index 3295d421..cfef6751 100644 --- a/src/components/ManageMembers/MemberList.tsx +++ b/src/components/ManageMembers/MemberList.tsx @@ -1,161 +1,170 @@ -import { useEffect, FC, useRef } from "react"; +import { FC, useEffect, useRef } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { ViewportList } from "react-viewport-list"; import Tippy from "@tippyjs/react"; import { hideAll } from "tippy.js"; -import toast from "react-hot-toast"; -import { ViewportList } from 'react-viewport-list'; -import { useTranslation } from "react-i18next"; + import { useUpdateUserMutation } from "@/app/services/user"; -import User from "../User"; -import IconMore from "@/assets/icons/more.svg"; -import IconOwner from "@/assets/icons/owner.svg"; +import { useAppSelector } from "@/app/store"; +import useUserOperation from "@/hooks/useUserOperation"; import IconArrowDown from "@/assets/icons/arrow.down.mini.svg"; import IconCheck from "@/assets/icons/check.sign.svg"; -import useUserOperation from "@/hooks/useUserOperation"; -import { useAppSelector } from "@/app/store"; +import IconMore from "@/assets/icons/more.svg"; +import IconOwner from "@/assets/icons/owner.svg"; +import User from "../User"; interface Props { - cid?: number; + cid?: number; } const MemberList: FC = ({ cid }) => { - const ref = useRef( - null, - ); - const { t } = useTranslation("member"); - const { t: ct } = useTranslation(); - const { users, channels, loginUser } = useAppSelector((store) => { - return { - users: store.users, - channels: store.channels, - loginUser: store.authData.user - }; - }); - const { copyEmail, removeFromChannel, removeUser } = useUserOperation({ cid }); - const [updateUser, { isSuccess: updateSuccess }] = useUpdateUserMutation(); - - useEffect(() => { - if (updateSuccess) { - toast.success(ct("tip.update")); - } - }, [updateSuccess]); - - const handleToggleRole = ({ - ignore = false, - uid, - isAdmin = true - }: { - ignore: boolean; - uid: number; - isAdmin: boolean; - }) => { - hideAll(); - if (ignore) return; - updateUser({ id: uid, is_admin: isAdmin }); + const ref = useRef(null); + const { t } = useTranslation("member"); + const { t: ct } = useTranslation(); + const { users, channels, loginUser } = useAppSelector((store) => { + return { + users: store.users, + channels: store.channels, + loginUser: store.authData.user }; + }); + const { copyEmail, removeFromChannel, removeUser } = useUserOperation({ cid }); + const [updateUser, { isSuccess: updateSuccess }] = useUpdateUserMutation(); - const channel = cid ? channels.byId[cid] : null; - const uids = channel ? (channel.is_public ? users.ids : channel.members) : users.ids; - return
      - - {(uid) => { - const currUser = users.byId[uid]; - if (!currUser) return null; - const { name, email, is_admin } = currUser; - const owner = channel && channel.owner == uid; - const switchRoleVisible = loginUser?.is_admin && loginUser.uid !== uid && uid !== 1; - const dotsVisible = email || loginUser?.is_admin; - const canRemove = loginUser?.is_admin && loginUser?.uid != uid && uid !== 1; - const canRemoveFromChannel = - channel && channel.owner == loginUser?.uid && loginUser?.uid != uid; - return ( -
    • -
      - -
      - - {name} {owner && } - - {email} -
      -
      -
      - {switchRoleVisible ? -
    • - {t("admin")} - {is_admin && } -
    • -
    • - {t("user")} - {!is_admin && } -
    • -
    - } - > - - {is_admin ? t("admin") : t("user")} - - - : - - {is_admin ? t("admin") : t("user")}} + useEffect(() => { + if (updateSuccess) { + toast.success(ct("tip.update")); + } + }, [updateSuccess]); - {dotsVisible && ( - - {email && ( -
  • - {ct("action.copy_email")} -
  • - )} - {canRemoveFromChannel && ( -
  • - {t("remove_from_channel")} -
  • - )} - {canRemove && ( -
  • - {ct("action.remove")} -
  • - )} - - } - > -
    - -
    -
    - )} -
    - - ); - }} - - ; + const handleToggleRole = ({ + ignore = false, + uid, + isAdmin = true + }: { + ignore: boolean; + uid: number; + isAdmin: boolean; + }) => { + hideAll(); + if (ignore) return; + updateUser({ id: uid, is_admin: isAdmin }); + }; + + const channel = cid ? channels.byId[cid] : null; + const uids = channel ? (channel.is_public ? users.ids : channel.members) : users.ids; + return ( +
      + + {(uid) => { + const currUser = users.byId[uid]; + if (!currUser) return null; + const { name, email, is_admin } = currUser; + const owner = channel && channel.owner == uid; + const switchRoleVisible = loginUser?.is_admin && loginUser.uid !== uid && uid !== 1; + const dotsVisible = email || loginUser?.is_admin; + const canRemove = loginUser?.is_admin && loginUser?.uid != uid && uid !== 1; + const canRemoveFromChannel = + channel && channel.owner == loginUser?.uid && loginUser?.uid != uid; + return ( +
    • +
      + +
      + + {name} {owner && } + + + {email} + +
      +
      +
      + {switchRoleVisible ? ( + +
    • + {t("admin")} + {is_admin && } +
    • +
    • + {t("user")} + {!is_admin && } +
    • +
    + } + > + + {is_admin ? t("admin") : t("user")} + + + + ) : ( + + {is_admin ? t("admin") : t("user")} + + )} + + {dotsVisible && ( + + {email && ( +
  • + {ct("action.copy_email")} +
  • + )} + {canRemoveFromChannel && ( +
  • + {t("remove_from_channel")} +
  • + )} + {canRemove && ( +
  • + {ct("action.remove")} +
  • + )} + + } + > +
    + +
    +
    + )} +
    + + ); + }} + + + ); }; export default MemberList; diff --git a/src/components/ManageMembers/index.tsx b/src/components/ManageMembers/index.tsx index f35d77af..2df8f6b4 100644 --- a/src/components/ManageMembers/index.tsx +++ b/src/components/ManageMembers/index.tsx @@ -1,7 +1,8 @@ import { FC } from "react"; -import InviteLink from "../InviteLink"; -import { useAppSelector } from "@/app/store"; import { useTranslation } from "react-i18next"; + +import { useAppSelector } from "@/app/store"; +import InviteLink from "../InviteLink"; import MemberList from "./MemberList"; interface Props { @@ -20,9 +21,7 @@ const ManageMembers: FC = ({ cid }) => { {loginUser?.is_admin && }

    {t("manage_members")}

    -

    - {t("manage_tip")} -

    +

    {t("manage_tip")}

    diff --git a/src/components/Manifest/Prompt.tsx b/src/components/Manifest/Prompt.tsx index 2542d654..605e1d96 100644 --- a/src/components/Manifest/Prompt.tsx +++ b/src/components/Manifest/Prompt.tsx @@ -1,9 +1,10 @@ import { FC } from "react"; -import Modal from "../Modal"; -import IconClose from "@/assets/icons/close.svg"; -import Button from "../styled/Button"; import { useTranslation } from "react-i18next"; +import IconClose from "@/assets/icons/close.svg"; +import Modal from "../Modal"; +import Button from "../styled/Button"; + interface Props { handleInstall?: () => void; closePrompt?: () => void; diff --git a/src/components/Manifest/index.tsx b/src/components/Manifest/index.tsx index 92129e01..c7f7e40a 100644 --- a/src/components/Manifest/index.tsx +++ b/src/components/Manifest/index.tsx @@ -1,8 +1,10 @@ -import { useEffect, useState, FC } from "react"; -import Prompt from "./Prompt"; -import usePWAInstallPrompt from "@/hooks/usePWAInstallPrompt"; +import { FC, useEffect, useState } from "react"; + import { BeforeInstallPromptEvent } from "@/types/global"; -interface IProps { } +import usePWAInstallPrompt from "@/hooks/usePWAInstallPrompt"; +import Prompt from "./Prompt"; + +interface IProps {} const Manifest: FC = () => { const { setCanceled, prompted, setDeferredPrompt, showPrompt } = usePWAInstallPrompt(); const [popup, setPopup] = useState(false); diff --git a/src/components/MarkdownEditor/index.tsx b/src/components/MarkdownEditor/index.tsx index d4a06ea1..9f20bf2e 100644 --- a/src/components/MarkdownEditor/index.tsx +++ b/src/components/MarkdownEditor/index.tsx @@ -1,14 +1,15 @@ -import { useRef, useEffect, FC } from "react"; +import { FC, useEffect, useRef } from "react"; import { Editor } from "@toast-ui/react-editor"; + import "prismjs/themes/prism.css"; import "@toast-ui/editor/dist/toastui-editor.css"; -import '@toast-ui/editor/dist/theme/toastui-editor-dark.css'; +import "@toast-ui/editor/dist/theme/toastui-editor-dark.css"; import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight.css"; import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js"; import useUploadFile from "@/hooks/useUploadFile"; -import Button from "../styled/Button"; import { isDarkMode } from "@/utils"; +import Button from "../styled/Button"; type Props = { updateDraft?: (draft: string) => void; @@ -32,7 +33,6 @@ const MarkdownEditor: FC = ({ useEffect(() => { const editor = editorRef?.current; if (editor) { - const editorInstance = editor.getInstance(); editorInstance.removeHook("addImageBlobHook"); editorInstance.addHook("addImageBlobHook", async (blob, callback) => { diff --git a/src/components/MarkdownRender.tsx b/src/components/MarkdownRender.tsx index 065598fd..2b6db712 100644 --- a/src/components/MarkdownRender.tsx +++ b/src/components/MarkdownRender.tsx @@ -1,8 +1,10 @@ -import { useRef, FC } from "react"; +import { FC, useRef } from "react"; + import "prismjs/themes/prism.css"; -import { Viewer } from "@toast-ui/react-editor"; import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin-code-syntax-highlight-all.js"; -import '@toast-ui/editor/dist/toastui-editor-viewer.css'; +import { Viewer } from "@toast-ui/react-editor"; + +import "@toast-ui/editor/dist/toastui-editor-viewer.css"; // import '@toast-ui/editor/dist/theme/toastui-editor-dark.css'; // import '@toast-ui/editor/dist/toastui-editor.css'; // import '@toast-ui/editor/dist/theme/toastui-editor-dark.css'; @@ -12,7 +14,6 @@ import "@toast-ui/editor-plugin-code-syntax-highlight/dist/toastui-editor-plugin import { isDarkMode } from "../utils"; import ImagePreview from "./ImagePreview"; - interface IProps { content: string; } @@ -22,7 +23,11 @@ const MarkdownRender: FC = ({ content }) => { <>
    - +
    ); diff --git a/src/components/Message/Commands.tsx b/src/components/Message/Commands.tsx index e4f8a28f..034e407e 100644 --- a/src/components/Message/Commands.tsx +++ b/src/components/Message/Commands.tsx @@ -1,27 +1,29 @@ -import { useState, useRef, FC } from "react"; +import { FC, useRef, useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import Tippy from "@tippyjs/react"; +import clsx from "clsx"; import { hideAll } from "tippy.js"; -import toast from "react-hot-toast"; + import { updateSelectMessages } from "@/app/slices/ui"; -import ContextMenu, { Item } from "../ContextMenu"; -import Tooltip from "../Tooltip"; +import { ChatContext } from "@/types/common"; import useFavMessage from "@/hooks/useFavMessage"; import useSendMessage from "@/hooks/useSendMessage"; -import ReactionPicker from "./ReactionPicker"; -import replyIcon from "@/assets/icons/reply.svg?url"; -import reactIcon from "@/assets/icons/reaction.svg?url"; -import editIcon from "@/assets/icons/edit.svg?url"; import IconBookmark from "@/assets/icons/bookmark.add.svg"; -import IconPin from "@/assets/icons/pin.svg"; -import IconForward from "@/assets/icons/forward.svg"; -import IconSelect from "@/assets/icons/select.svg"; import IconDelete from "@/assets/icons/delete.svg"; +import editIcon from "@/assets/icons/edit.svg?url"; +import IconForward from "@/assets/icons/forward.svg"; import moreIcon from "@/assets/icons/more.svg?url"; +import IconPin from "@/assets/icons/pin.svg"; +import reactIcon from "@/assets/icons/reaction.svg?url"; +import replyIcon from "@/assets/icons/reply.svg?url"; +import IconSelect from "@/assets/icons/select.svg"; +import ContextMenu, { Item } from "../ContextMenu"; +import Tooltip from "../Tooltip"; +import ReactionPicker from "./ReactionPicker"; import useMessageOperation from "./useMessageOperation"; -import { useTranslation } from "react-i18next"; -import clsx from "clsx"; -import { ChatContext } from "@/types/common"; + type Props = { isSelf: boolean; context: ChatContext; @@ -29,7 +31,13 @@ type Props = { mid: number; toggleEditMessage: () => void; }; -const Commands: FC = ({ isSelf, context = "dm", contextId = 0, mid = 0, toggleEditMessage }) => { +const Commands: FC = ({ + isSelf, + context = "dm", + contextId = 0, + mid = 0, + toggleEditMessage +}) => { const { t } = useTranslation(); const { canDelete, @@ -89,10 +97,12 @@ const Commands: FC = ({ isSelf, context = "dm", contextId = 0, mid = 0, t <>
      + )} + > = ({ mid, cancelEdit }) => { }; if (!msg) return null; - return (
      @@ -80,10 +80,16 @@ const EditMessage: FC = ({ mid, cancelEdit }) => {
      - esc to + esc to{" "} + - enter to + enter to{" "} +
      diff --git a/src/components/Message/ExpireTimer.tsx b/src/components/Message/ExpireTimer.tsx index ea3fa326..563c3ce0 100644 --- a/src/components/Message/ExpireTimer.tsx +++ b/src/components/Message/ExpireTimer.tsx @@ -1,83 +1,83 @@ -import dayjs from 'dayjs'; -import { FC, useEffect, useState, useCallback } from 'react'; -import { useDispatch } from 'react-redux'; -import { removeMessage } from '../../app/slices/message'; -import { removeChannelMsg } from '../../app/slices/message.channel'; -import { removeUserMsg } from '../../app/slices/message.user'; -import IconTimer from '@/assets/icons/timer.svg'; -import { ChatContext } from '../../types/common'; +import { FC, useCallback, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import dayjs from "dayjs"; + +import IconTimer from "@/assets/icons/timer.svg"; +import { removeMessage } from "../../app/slices/message"; +import { removeChannelMsg } from "../../app/slices/message.channel"; +import { removeUserMsg } from "../../app/slices/message.user"; +import { ChatContext } from "../../types/common"; + type Props = { - context: ChatContext, - contextId: number, - mid: number; - expiresIn: number; - createAt: number; + context: ChatContext; + contextId: number; + mid: number; + expiresIn: number; + createAt: number; }; const ExpireTimer: FC = ({ mid, createAt, expiresIn, context, contextId }) => { - const [countdown, setCountdown] = useState(); - const dispatch = useDispatch(); - const clearMsgFromClient = useCallback( - () => { - if (context == "dm") { - dispatch(removeUserMsg({ mid, id: contextId })); - } else { - dispatch(removeChannelMsg({ mid, id: contextId })); + const [countdown, setCountdown] = useState(); + const dispatch = useDispatch(); + const clearMsgFromClient = useCallback(() => { + if (context == "dm") { + dispatch(removeUserMsg({ mid, id: contextId })); + } else { + dispatch(removeChannelMsg({ mid, id: contextId })); + } + dispatch(removeMessage(mid)); + }, [context, contextId, mid]); - } - dispatch(removeMessage(mid)); - }, - [context, contextId, mid], - ); - - useEffect(() => { - if (expiresIn > 0 && createAt > 0) { - const expire_time = createAt + expiresIn * 1000; - if (dayjs().isAfter(new Date(expire_time))) { - // 已过期,立即删除 - clearMsgFromClient(); - - } else { - // 倒计时 - setCountdown(Math.floor((expire_time - new Date().getTime()) / 1000)); - } - } - }, [expiresIn, createAt]); - useEffect(() => { - let timer = 0; - if (typeof countdown !== "undefined") { - if (countdown > 0) { - timer = window.setTimeout(() => { - setCountdown(prev => { - const _prev = prev ?? 0; - return _prev - 1; - }); - }, 1000); - } else { - // 倒计时结束 - console.log("countdown over", mid, countdown); - clearMsgFromClient(); - } - } - return () => { - if (typeof countdown !== "undefined") { - clearTimeout(timer); - } - }; - }, [countdown, mid]); - if (!countdown) return null; - const duration = dayjs.duration(countdown * 1000); - const day = duration.days() !== 0 ? `${duration.days()} day` : ""; - const hours = duration.hours() !== 0 ? `${duration.hours().toString().padStart(2, '0')}:` : ""; - const minutes = duration.minutes() !== 0 ? `${duration.minutes().toString().padStart(2, '0')}:` : ""; - const formatted_countdown = `${day} ${hours}${minutes}${duration.seconds().toString().padStart(2, '0')}`; - return ( -
      - - {formatted_countdown} -
      - ); + useEffect(() => { + if (expiresIn > 0 && createAt > 0) { + const expire_time = createAt + expiresIn * 1000; + if (dayjs().isAfter(new Date(expire_time))) { + // 已过期,立即删除 + clearMsgFromClient(); + } else { + // 倒计时 + setCountdown(Math.floor((expire_time - new Date().getTime()) / 1000)); + } + } + }, [expiresIn, createAt]); + useEffect(() => { + let timer = 0; + if (typeof countdown !== "undefined") { + if (countdown > 0) { + timer = window.setTimeout(() => { + setCountdown((prev) => { + const _prev = prev ?? 0; + return _prev - 1; + }); + }, 1000); + } else { + // 倒计时结束 + console.log("countdown over", mid, countdown); + clearMsgFromClient(); + } + } + return () => { + if (typeof countdown !== "undefined") { + clearTimeout(timer); + } + }; + }, [countdown, mid]); + if (!countdown) return null; + const duration = dayjs.duration(countdown * 1000); + const day = duration.days() !== 0 ? `${duration.days()} day` : ""; + const hours = duration.hours() !== 0 ? `${duration.hours().toString().padStart(2, "0")}:` : ""; + const minutes = + duration.minutes() !== 0 ? `${duration.minutes().toString().padStart(2, "0")}:` : ""; + const formatted_countdown = `${day} ${hours}${minutes}${duration + .seconds() + .toString() + .padStart(2, "0")}`; + return ( +
      + + {formatted_countdown} +
      + ); }; export default ExpireTimer; - diff --git a/src/components/Message/FavoredMessage.tsx b/src/components/Message/FavoredMessage.tsx index 682650af..ef69fbc2 100644 --- a/src/components/Message/FavoredMessage.tsx +++ b/src/components/Message/FavoredMessage.tsx @@ -1,7 +1,8 @@ import { FC, ReactElement, useEffect, useState } from "react"; -import renderContent from "./renderContent"; -import Avatar from "../Avatar"; + import useFavMessage from "@/hooks/useFavMessage"; +import Avatar from "../Avatar"; +import renderContent from "./renderContent"; type Props = { id?: string; @@ -17,20 +18,34 @@ const FavoredMessage: FC = ({ id = "" }) => { const favorite_mids = messages.map(({ from_mid }) => +from_mid) || []; setMsgs( -
      +
      {messages.map((msg, idx) => { const { user = {}, download, content, content_type, properties, thumbnail } = msg; return ( -
      +
      {user && (
      - +
      )}
      - {user?.name || "Deleted User"} + + {user?.name || "Deleted User"} +
      {renderContent({ diff --git a/src/components/Message/ForwardedMessage.tsx b/src/components/Message/ForwardedMessage.tsx index 9b504339..4365abed 100644 --- a/src/components/Message/ForwardedMessage.tsx +++ b/src/components/Message/ForwardedMessage.tsx @@ -1,10 +1,11 @@ -import { useEffect, useState, FC, ReactElement } from "react"; -import renderContent from "./renderContent"; -import Avatar from "../Avatar"; -import IconForward from "@/assets/icons/forward.svg"; -import useNormalizeMessage from "@/hooks/useNormalizeMessage"; +import { FC, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; + import { ChatContext } from "@/types/common"; +import useNormalizeMessage from "@/hooks/useNormalizeMessage"; +import IconForward from "@/assets/icons/forward.svg"; +import Avatar from "../Avatar"; +import renderContent from "./renderContent"; type Props = { context: ChatContext; @@ -22,7 +23,10 @@ const ForwardedMessage: FC = ({ context, to, from_uid, id }) => { const forward_mids = messages.map(({ from_mid }) => from_mid) || []; // console.log("fff", messages); setForwards( -
      +

      {t("action.forward")} @@ -31,7 +35,10 @@ const ForwardedMessage: FC = ({ context, to, from_uid, id }) => { {messages.map((msg, idx) => { const { user = {}, download, content, content_type, properties, thumbnail } = msg; return ( -
      +
      {user && (
      diff --git a/src/components/Message/Mention.tsx b/src/components/Message/Mention.tsx index c335c624..f5cbe9d7 100644 --- a/src/components/Message/Mention.tsx +++ b/src/components/Message/Mention.tsx @@ -1,7 +1,8 @@ // import { FC, ReactNode } from "react"; import Tippy from "@tippyjs/react"; -import Profile from "../Profile"; + import { useAppSelector } from "@/app/store"; +import Profile from "../Profile"; interface Props { uid: number; diff --git a/src/components/Message/PinMessageModal.tsx b/src/components/Message/PinMessageModal.tsx index 06dc9090..0b5833a6 100644 --- a/src/components/Message/PinMessageModal.tsx +++ b/src/components/Message/PinMessageModal.tsx @@ -1,13 +1,12 @@ -import { FC } from "react"; -import { useEffect } from "react"; +import { FC, useEffect } from "react"; import toast from "react-hot-toast"; -import usePinMessage from "@/hooks/usePinMessage"; -import StyledModal from "../styled/Modal"; -import Button from "../styled/Button"; -import Modal from "../Modal"; -import PreviewMessage from "./PreviewMessage"; import { useTranslation } from "react-i18next"; +import usePinMessage from "@/hooks/usePinMessage"; +import Modal from "../Modal"; +import Button from "../styled/Button"; +import StyledModal from "../styled/Modal"; +import PreviewMessage from "./PreviewMessage"; interface Props { closeModal: () => void; diff --git a/src/components/Message/PreviewMessage.tsx b/src/components/Message/PreviewMessage.tsx index abc77aee..02223a7f 100644 --- a/src/components/Message/PreviewMessage.tsx +++ b/src/components/Message/PreviewMessage.tsx @@ -1,12 +1,13 @@ import { FC } from "react"; import clsx from "clsx"; -import renderContent from "./renderContent"; -import Avatar from "../Avatar"; + import { useAppSelector } from "@/app/store"; +import Avatar from "../Avatar"; +import renderContent from "./renderContent"; interface Props { mid?: number; - context?: "forward" | "pin" + context?: "forward" | "pin"; } const PreviewMessage: FC = ({ mid = 0, context = "forward" }) => { @@ -19,15 +20,32 @@ const PreviewMessage: FC = ({ mid = 0, context = "forward" }) => { const pinMsg = context == "pin"; const forwardMsg = context == "forward"; return ( -
      +
      - +
      {name}
      -
      +
      {renderContent({ content_type, content, diff --git a/src/components/Message/Reaction.tsx b/src/components/Message/Reaction.tsx index e75a7d6f..f084ec1a 100644 --- a/src/components/Message/Reaction.tsx +++ b/src/components/Message/Reaction.tsx @@ -1,13 +1,13 @@ import { FC } from "react"; import Tippy from "@tippyjs/react"; import { hideAll } from "tippy.js"; -import ReactionItem, { Emojis, ReactionMap } from "../ReactionItem"; -import ReactionPicker from "./ReactionPicker"; -import Tooltip from "../Tooltip"; -import { useReactMessageMutation } from "@/app/services/message"; -import IconAddEmoji from "@/assets/icons/add.emoji.svg"; -import { useAppSelector } from "@/app/store"; +import { useReactMessageMutation } from "@/app/services/message"; +import { useAppSelector } from "@/app/store"; +import IconAddEmoji from "@/assets/icons/add.emoji.svg"; +import ReactionItem, { Emojis, ReactionMap } from "../ReactionItem"; +import Tooltip from "../Tooltip"; +import ReactionPicker from "./ReactionPicker"; const ReactionDetails = ({ uids = [], @@ -28,7 +28,11 @@ const ReactionDetails = ({ ? `${names.join(", ")} and ${names.length - 3} others reacted with` : `${names.join(", ")} reacted with`; return ( -
      +
      @@ -65,7 +69,9 @@ const Reaction: FC = ({ mid, reactions = null, readOnly = false }) => { return uids.length > 0 ? ( = ({ mid, reactions = null, readOnly = false }) => { - {uids.length > 1 ? {`${uids.length}`} : null} + {uids.length > 1 ? ( + {`${uids.length}`} + ) : null} ) : null; })} @@ -94,7 +102,7 @@ const Reaction: FC = ({ mid, reactions = null, readOnly = false }) => { content={} > diff --git a/src/components/Message/ReactionPicker.tsx b/src/components/Message/ReactionPicker.tsx index be07a47c..5f4a8700 100644 --- a/src/components/Message/ReactionPicker.tsx +++ b/src/components/Message/ReactionPicker.tsx @@ -1,8 +1,9 @@ import { FC } from "react"; -import { useReactMessageMutation } from "@/app/services/message"; + import { Emojis } from "@/app/config"; -import Emoji from "../ReactionItem"; +import { useReactMessageMutation } from "@/app/services/message"; import { useAppSelector } from "@/app/store"; +import Emoji from "../ReactionItem"; type Props = { mid: number; @@ -25,17 +26,22 @@ const ReactionPicker: FC = ({ mid, hidePicker }) => { if (el) { el.scrollIntoViewIfNeeded(false); } - }; return (
      -

      - +
      diff --git a/src/components/Send/UploadFileList/index.tsx b/src/components/Send/UploadFileList/index.tsx index 8aca2df3..33091fbc 100644 --- a/src/components/Send/UploadFileList/index.tsx +++ b/src/components/Send/UploadFileList/index.tsx @@ -1,23 +1,18 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; + +import { ChatContext } from "@/types/common"; +import useUploadFile from "@/hooks/useUploadFile"; +import { formatBytes, getFileIcon } from "@/utils"; +import DeleteIcon from "@/assets/icons/delete.svg"; +import EditIcon from "@/assets/icons/edit.svg"; import { useMixedEditor } from "../../MixedInput"; import EditFileDetailsModal from "./EditFileDetails"; -import { getFileIcon, formatBytes } from "@/utils"; -import useUploadFile from "@/hooks/useUploadFile"; -import EditIcon from "@/assets/icons/edit.svg"; -import DeleteIcon from "@/assets/icons/delete.svg"; -import { ChatContext } from "@/types/common"; type EditProps = { index: number; name: string; }; -export default function UploadFileList({ - context, - id -}: { - context: ChatContext; - id: number; -}) { +export default function UploadFileList({ context, id }: { context: ChatContext; id: number }) { const editor = useMixedEditor(`${context}_${id}`); const [editInfo, setEditInfo] = useState(null); const { stageFiles, updateStageFile, removeStageFile } = useUploadFile({ @@ -57,14 +52,26 @@ export default function UploadFileList({
        {stageFiles.map(({ name, url, size, type }, idx: number) => { return ( -
      • +
      • - {type.startsWith("image") ? image : getFileIcon(type, name)} + {type.startsWith("image") ? ( + image + ) : ( + getFileIcon(type, name) + )}
        -

        {name}

        +

        + {name} +

        {formatBytes(size)}
          -
        • +
        • = ({ id }) => { const { t } = useTranslation("chat"); - const { unblockThisContact, blocked } = useUserOperation({ uid: context == "dm" ? id : undefined, cid: context == "channel" ? id : undefined }); + const { unblockThisContact, blocked } = useUserOperation({ + uid: context == "dm" ? id : undefined, + cid: context == "channel" ? id : undefined + }); const { resetStageFiles } = useUploadFile({ context, id }); const { getDraft, getUpdateDraft } = useDraft({ context, id }); const editor = useMixedEditor(`${context}_${id}`); @@ -88,7 +90,7 @@ const Send: FC = ({ // send text msgs for await (const msg of msgs) { const { type: content_type, content, properties = {} } = msg; - if ((content as string).trim() === '') continue; // 空消息不发送 + if ((content as string).trim() === "") continue; // 空消息不发送 properties.local_id = properties.local_id ?? +new Date(); await sendMessage({ id, @@ -146,10 +148,14 @@ const Send: FC = ({ context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : []; const isMarkdownMode = mode == Modes.markdown; if (context == "dm" && blocked) { - return
          - {t("contact_block_tip")} - {t("unblock")} -
          ; + return ( +
          + {t("contact_block_tip")} + + {t("unblock")} + +
          + ); } return ( <> @@ -157,13 +163,22 @@ const Send: FC = ({ {/* PC input */}
        + } + header={ +
        + +
        + + {name} + {description} +
        +
        + } + users={ + + } + voice={} + /> + ); } export default memo(ChannelChat, (prev, next) => prev.cid == next.cid); diff --git a/src/routes/chat/DMChat/index.tsx b/src/routes/chat/DMChat/index.tsx index f2b4002a..329e1802 100644 --- a/src/routes/chat/DMChat/index.tsx +++ b/src/routes/chat/DMChat/index.tsx @@ -1,15 +1,17 @@ import { FC, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import Tippy from "@tippyjs/react"; -import FavList from "../FavList"; -import Tooltip from "@/components/Tooltip"; -import FavIcon from "@/assets/icons/bookmark.svg"; -import User from "@/components/User"; -import Layout from "../Layout"; + import { useAppSelector } from "@/app/store"; import GoBackNav from "@/components/GoBackNav"; import ServerVersionChecker from "@/components/ServerVersionChecker"; +import Tooltip from "@/components/Tooltip"; +import User from "@/components/User"; +import FavIcon from "@/assets/icons/bookmark.svg"; +import FavList from "../FavList"; +import Layout from "../Layout"; import VoiceChat from "../VoiceChat"; + type Props = { uid: number; dropFiles?: File[]; @@ -18,7 +20,7 @@ const DMChat: FC = ({ uid = 0, dropFiles }) => { const navigate = useNavigate(); const { currUser } = useAppSelector((store) => { return { - currUser: store.users.byId[uid], + currUser: store.users.byId[uid] }; }); useEffect(() => { diff --git a/src/routes/chat/FavList.tsx b/src/routes/chat/FavList.tsx index ff699dc8..448dc987 100644 --- a/src/routes/chat/FavList.tsx +++ b/src/routes/chat/FavList.tsx @@ -1,9 +1,10 @@ -import { MouseEvent, FC } from "react"; +import { FC, MouseEvent } from "react"; import { useTranslation } from "react-i18next"; + import FavoredMessage from "@/components/Message/FavoredMessage"; -import IconSurprise from "@/assets/icons/emoji.surprise.svg"; -import IconRemove from "@/assets/icons/close.svg"; import useFavMessage from "@/hooks/useFavMessage"; +import IconRemove from "@/assets/icons/close.svg"; +import IconSurprise from "@/assets/icons/emoji.surprise.svg"; type Props = { cid?: number; uid?: number }; const FavList: FC = ({ cid = null, uid = null }) => { @@ -17,17 +18,24 @@ const FavList: FC = ({ cid = null, uid = null }) => { const noFavs = favorites.length == 0; return (
        -

        {t('fav_msg')}({favorites.length})

        +

        + {t("fav_msg")}({favorites.length}) +

        {noFavs ? (
        -
        {t("fav_empty_tip")}
        +
        + {t("fav_empty_tip")} +
        ) : (
          {favorites.map(({ id }) => { return ( -
        • +
        • +
          ); diff --git a/src/routes/chat/GuestChannelChat/index.tsx b/src/routes/chat/GuestChannelChat/index.tsx index 65db67ed..b7ba2dbe 100644 --- a/src/routes/chat/GuestChannelChat/index.tsx +++ b/src/routes/chat/GuestChannelChat/index.tsx @@ -1,10 +1,10 @@ // import { useState, useEffect } from "react"; // import { NavLink } from "react-router-dom"; // import { useTranslation } from "react-i18next"; -import ChannelIcon from "@/components/ChannelIcon"; -import Layout from "../Layout"; import { useAppSelector } from "@/app/store"; +import ChannelIcon from "@/components/ChannelIcon"; import GoBackNav from "@/components/GoBackNav"; +import Layout from "../Layout"; type Props = { cid?: number; @@ -13,7 +13,7 @@ export default function GuestChannelChat({ cid = 0 }: Props) { // const { t } = useTranslation("chat"); const { data } = useAppSelector((store) => { return { - data: store.channels.byId[cid], + data: store.channels.byId[cid] }; }); if (!data) return null; diff --git a/src/routes/chat/GuestSessionList/Session.tsx b/src/routes/chat/GuestSessionList/Session.tsx index 1a6b7e0d..2e441ea2 100644 --- a/src/routes/chat/GuestSessionList/Session.tsx +++ b/src/routes/chat/GuestSessionList/Session.tsx @@ -1,11 +1,12 @@ // @ts-nocheck -import { useState, useEffect, FC } from "react"; -import clsx from "clsx"; -import { renderPreviewMessage } from "../../chat/utils"; -import Avatar from "@/components/Avatar"; +import { FC, useEffect, useState } from "react"; import { NavLink } from "react-router-dom"; +import clsx from "clsx"; + import { useAppSelector } from "@/app/store"; +import Avatar from "@/components/Avatar"; import { fromNowTime } from "@/utils"; +import { renderPreviewMessage } from "../../chat/utils"; interface IProps { id: number; @@ -39,13 +40,33 @@ const Session: FC = ({ id, mid }) => { return (
        • - clsx(`nav flex gap-2 rounded-lg p-2 w-full md:hover:bg-gray-500/20`, linkActive && "bg-gray-500/20")} to={`/chat/channel/${id}`}> + + clsx( + `nav flex gap-2 rounded-lg p-2 w-full md:hover:bg-gray-500/20`, + linkActive && "bg-gray-500/20" + ) + } + to={`/chat/channel/${id}`} + >
          - +
          - + {name} @@ -53,7 +74,9 @@ const Session: FC = ({ id, mid }) => {
          - {renderPreviewMessage(previewMsg)} + + {renderPreviewMessage(previewMsg)} +
          diff --git a/src/routes/chat/GuestSessionList/index.tsx b/src/routes/chat/GuestSessionList/index.tsx index 39fe3e8b..2cbfd4b7 100644 --- a/src/routes/chat/GuestSessionList/index.tsx +++ b/src/routes/chat/GuestSessionList/index.tsx @@ -1,8 +1,9 @@ -import { FC } from "react"; -import { useState, useEffect } from "react"; -import Session from "./Session"; +import { FC, useEffect, useState } from "react"; + import { useAppSelector } from "@/app/store"; import LoginTip from "../Layout/LoginTip"; +import Session from "./Session"; + export interface ChatSession { key: string; id: number; diff --git a/src/routes/chat/Layout/AddContactTip.tsx b/src/routes/chat/Layout/AddContactTip.tsx index ff48ba80..1b77e48c 100644 --- a/src/routes/chat/Layout/AddContactTip.tsx +++ b/src/routes/chat/Layout/AddContactTip.tsx @@ -1,42 +1,57 @@ -import { useTranslation } from 'react-i18next'; -import IconAdd from '@/assets/icons/add.person.svg'; -import IconBlock from '@/assets/icons/block.svg'; -import { useAppSelector } from '../../../app/store'; -import { useUpdateContactStatusMutation } from '../../../app/services/user'; -import { ContactAction } from '../../../types/user'; +import { useTranslation } from "react-i18next"; + +import IconAdd from "@/assets/icons/add.person.svg"; +import IconBlock from "@/assets/icons/block.svg"; +import { useUpdateContactStatusMutation } from "../../../app/services/user"; +import { useAppSelector } from "../../../app/store"; +import { ContactAction } from "../../../types/user"; type Props = { - uid: number -} - -const AddContactTip = (props: Props) => { - const { t } = useTranslation("chat"); - const [updateContactStatus] = useUpdateContactStatusMutation(); - const { targetUser, enableContact } = useAppSelector(store => { - return { targetUser: store.users.byId[props.uid], enableContact: store.server.contact_verification_enable }; - }); - const handleContactStatus = (action: ContactAction) => { - updateContactStatus({ target_uid: props.uid, action }); - }; - const itemClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-gray-50 dark:bg-gray-800 text-sm pt-3.5 pb-3`; - if (!targetUser || !enableContact) return null; - if (targetUser.status == "added") return null; - const blocked = targetUser.status == "blocked"; - return ( -
          -

          {blocked ? t("contact_block_tip") : t("contact_tip")}

          -
            - {!blocked &&
          • - - {t("add_contact")} -
          • } -
          • - - {blocked ? t("unblock") : t("block")} -
          • -
          -
          - ); + uid: number; }; -export default AddContactTip; \ No newline at end of file +const AddContactTip = (props: Props) => { + const { t } = useTranslation("chat"); + const [updateContactStatus] = useUpdateContactStatusMutation(); + const { targetUser, enableContact } = useAppSelector((store) => { + return { + targetUser: store.users.byId[props.uid], + enableContact: store.server.contact_verification_enable + }; + }); + const handleContactStatus = (action: ContactAction) => { + updateContactStatus({ target_uid: props.uid, action }); + }; + const itemClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-gray-50 dark:bg-gray-800 text-sm pt-3.5 pb-3`; + if (!targetUser || !enableContact) return null; + if (targetUser.status == "added") return null; + const blocked = targetUser.status == "blocked"; + return ( +
          +

          + {blocked ? t("contact_block_tip") : t("contact_tip")} +

          +
            + {!blocked && ( +
          • + + {t("add_contact")} +
          • + )} +
          • + + {blocked ? t("unblock") : t("block")} +
          • +
          +
          + ); +}; + +export default AddContactTip; diff --git a/src/routes/chat/Layout/DMVoicing.tsx b/src/routes/chat/Layout/DMVoicing.tsx index 5a67d519..9a6117f8 100644 --- a/src/routes/chat/Layout/DMVoicing.tsx +++ b/src/routes/chat/Layout/DMVoicing.tsx @@ -4,152 +4,224 @@ import { useEffect } from "react"; import { useDispatch } from "react-redux"; import clsx from "clsx"; -import { useAppSelector } from "@/app/store"; -import Tooltip from "@/components/Tooltip"; -import IconCallOff from '@/assets/icons/call.off.svg'; -import IconCallAnswer from '@/assets/icons/call.svg'; -import IconMicOff from '@/assets/icons/mic.off.svg'; // import IconMic from '@/assets/icons/mic.on.svg'; // import IconCamera from '@/assets/icons/camera.svg'; // import IconCameraOff from '@/assets/icons/camera.off.svg'; // import IconScreen from '@/assets/icons/share.screen.svg'; import { updateCallInfo } from "@/app/slices/voice"; -import { useVoice } from "@/components/Voice"; -import { playAgoraVideo } from "@/utils"; +import { useAppSelector } from "@/app/store"; import Avatar from "@/components/Avatar"; +import Tooltip from "@/components/Tooltip"; +import { useVoice } from "@/components/Voice"; import Operations from "@/components/Voice/Operations"; +import { playAgoraVideo } from "@/utils"; +import IconCallOff from "@/assets/icons/call.off.svg"; +import IconCallAnswer from "@/assets/icons/call.svg"; +import IconMicOff from "@/assets/icons/mic.off.svg"; type VoicingMember = { - id: number, - muted: boolean, - video: boolean, - speaking: boolean, - name: string, - avatar?: string -} + id: number; + muted: boolean; + video: boolean; + speaking: boolean; + name: string; + avatar?: string; +}; type BlockProps = { - onlyToSelf: boolean, - sendByMe: boolean; - connected: boolean; - from: VoicingMember, - to: VoicingMember -} + onlyToSelf: boolean; + sendByMe: boolean; + connected: boolean; + from: VoicingMember; + to: VoicingMember; +}; const VoicingBlocks = ({ onlyToSelf, sendByMe, connected, from, to }: BlockProps) => { - const blocks = [from, to].map(({ id, speaking, name, avatar, video, muted }, idx) => { - const showWaiting = idx == 1 && !connected && !sendByMe && !onlyToSelf; - const showToWaiting = idx == 1 && !connected && sendByMe; - const isMyself = sendByMe ? idx == 0 : idx == 1; - const hiddenFrom = onlyToSelf && idx == 0; - return
          -
          - {speaking &&
          } - {showToWaiting &&
          } - -
          -
          - {/* camera video */} -
          - {video && - {name} - } - {muted && !isMyself && - - } -
          ; - } + const blocks = [from, to].map(({ id, speaking, name, avatar, video, muted }, idx) => { + const showWaiting = idx == 1 && !connected && !sendByMe && !onlyToSelf; + const showToWaiting = idx == 1 && !connected && sendByMe; + const isMyself = sendByMe ? idx == 0 : idx == 1; + const hiddenFrom = onlyToSelf && idx == 0; + return ( +
          +
          + {speaking && ( +
          + )} + {showToWaiting && ( +
          + )} + +
          +
          + {/* camera video */} +
          + {video && ( + + {name} + + )} + {muted && !isMyself && ( + + + + )} +
          ); - return <>{blocks}; + }); + return <>{blocks}; }; type Props = { - uid: number -} + uid: number; +}; const DMVoice = ({ uid }: Props) => { - const dispatch = useDispatch(); - // const { t } = useTranslation("chat"); - const { voice: { - callingFrom, callingTo, voicingMembers - }, userData, loginUser } = useAppSelector(store => { - return { - voice: store.voice, - userData: store.users.byId, - loginUser: store.authData.user - }; + const dispatch = useDispatch(); + // const { t } = useTranslation("chat"); + const { + voice: { callingFrom, callingTo, voicingMembers }, + userData, + loginUser + } = useAppSelector((store) => { + return { + voice: store.voice, + userData: store.users.byId, + loginUser: store.authData.user + }; + }); + const { leave, joinVoice, joining } = useVoice({ id: callingTo, context: "dm" }); + useEffect(() => { + const ids = voicingMembers.ids; + ids.forEach((id) => { + playAgoraVideo(id); }); - const { leave, joinVoice, joining } = useVoice({ id: callingTo, context: "dm" }); - useEffect(() => { - const ids = voicingMembers.ids; - ids.forEach(id => { - playAgoraVideo(id); - }); - }, [voicingMembers.ids]); - if (![callingFrom, callingTo].includes(uid)) return null; + }, [voicingMembers.ids]); + if (![callingFrom, callingTo].includes(uid)) return null; - const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom]; - const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo]; - const sendByMe = loginUser?.uid !== toUid; - const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo; - const handleCancel = () => { - console.log('cancel'); - if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) { - leave(); - } - dispatch(updateCallInfo({ from: 0, to: 0 })); - }; - const handleAnswer = () => { - joinVoice(); - }; - const connected = voicingMembers.ids.length == 2; - // const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo; - const { speakingVolume: toSpeakingVol = 0, muted: toMuted = false, video: toVideo = false, shareScreen: toScreen = false } = voicingMembers.byId[callingTo] ?? {}; - const toSpeaking = toSpeakingVol > 50; - const { speakingVolume: fromSpeakingVol = 0, muted: fromMuted = false, video: fromVideo = false, shareScreen: fromScreen = false } = voicingMembers.byId[callingFrom] ?? {}; - const fromSpeaking = fromSpeakingVol > 50; - return ( -
          -
          - -
          -
          - {(sendByMe || connected || onlyToSelf) && - - } - {!sendByMe && !connected && !onlyToSelf && - - - - } - {connected && <> - - - } -
          -
          - ); + const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom]; + const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo]; + const sendByMe = loginUser?.uid !== toUid; + const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo; + const handleCancel = () => { + console.log("cancel"); + if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) { + leave(); + } + dispatch(updateCallInfo({ from: 0, to: 0 })); + }; + const handleAnswer = () => { + joinVoice(); + }; + const connected = voicingMembers.ids.length == 2; + // const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo; + const { + speakingVolume: toSpeakingVol = 0, + muted: toMuted = false, + video: toVideo = false, + shareScreen: toScreen = false + } = voicingMembers.byId[callingTo] ?? {}; + const toSpeaking = toSpeakingVol > 50; + const { + speakingVolume: fromSpeakingVol = 0, + muted: fromMuted = false, + video: fromVideo = false, + shareScreen: fromScreen = false + } = voicingMembers.byId[callingFrom] ?? {}; + const fromSpeaking = fromSpeakingVol > 50; + return ( +
          +
          + +
          +
          + {(sendByMe || connected || onlyToSelf) && ( + + + + )} + {!sendByMe && !connected && !onlyToSelf && ( + + + + )} + {connected && ( + <> + + + )} +
          +
          + ); }; -export default DMVoice; \ No newline at end of file +export default DMVoice; diff --git a/src/routes/chat/Layout/DnDTip.tsx b/src/routes/chat/Layout/DnDTip.tsx index 5d38f306..2b2cc129 100644 --- a/src/routes/chat/Layout/DnDTip.tsx +++ b/src/routes/chat/Layout/DnDTip.tsx @@ -1,21 +1,23 @@ // import { useEffect } from "react"; -import { ChatPrefixes } from "@/app/config"; import { useTranslation } from "react-i18next"; + +import { ChatPrefixes } from "@/app/config"; import { ChatContext } from "@/types/common"; + type Props = { - context: ChatContext, - name: string + context: ChatContext; + name: string; }; const DnDTip = ({ context, name }: Props) => { const { t } = useTranslation("chat"); return ( -
          +
          -

          {`${t("send_to")} ${ChatPrefixes[context]}${name}`}

          +

          {`${t("send_to")} ${ + ChatPrefixes[context] + }${name}`}

          Photos accept jpg, png, max size limit to 10M.
          diff --git a/src/routes/chat/Layout/LicenseOutdatedTip.tsx b/src/routes/chat/Layout/LicenseOutdatedTip.tsx index 83d18e04..3e00fc61 100644 --- a/src/routes/chat/Layout/LicenseOutdatedTip.tsx +++ b/src/routes/chat/Layout/LicenseOutdatedTip.tsx @@ -1,7 +1,9 @@ // import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; + import Button from "@/components/styled/Button"; + // type Props = {}; const LicenseUpgradeTip = () => { diff --git a/src/routes/chat/Layout/LoginTip.tsx b/src/routes/chat/Layout/LoginTip.tsx index 10b711ed..ec870033 100644 --- a/src/routes/chat/Layout/LoginTip.tsx +++ b/src/routes/chat/Layout/LoginTip.tsx @@ -1,13 +1,15 @@ // import { useEffect } from "react"; -import clsx from "clsx"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; +import clsx from "clsx"; + import { resetAuthData } from "@/app/slices/auth.data"; import Button from "@/components/styled/Button"; import useLogout from "@/hooks/useLogout"; + type Props = { - placement?: "session" | "chat" + placement?: "session" | "chat"; }; const LoginTip = ({ placement = "chat" }: Props) => { @@ -23,14 +25,21 @@ const LoginTip = ({ placement = "chat" }: Props) => { }; return ( -
          +
          👋 {t("sign_in_tip")} - +
          ); }; diff --git a/src/routes/chat/Layout/NewMessageBottomTip.tsx b/src/routes/chat/Layout/NewMessageBottomTip.tsx index b35fb4db..67bc31e5 100644 --- a/src/routes/chat/Layout/NewMessageBottomTip.tsx +++ b/src/routes/chat/Layout/NewMessageBottomTip.tsx @@ -1,51 +1,51 @@ -import clsx from 'clsx'; // import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useAppSelector } from '../../../app/store'; -import getUnreadCount from '../utils'; -import { ChatContext } from '../../../types/common'; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + +import { useAppSelector } from "../../../app/store"; +import { ChatContext } from "../../../types/common"; +import getUnreadCount from "../utils"; type Props = { - context: ChatContext, - id: number, - scrollToBottom?: () => void -} + context: ChatContext; + id: number; + scrollToBottom?: () => void; +}; // linear-gradient(135deg,_#3C8CE7_0%,_#00EAFF_100%) const NewMessageBottomTip = ({ context, id, scrollToBottom }: Props) => { - const { t } = useTranslation("chat"); - const { - readIndex, - mids, - messageData, - loginUid, - } = useAppSelector((store) => { - return { - readIndex: context == "channel" ? store.footprint.readChannels[id] : store.footprint.readUsers[id], - mids: context == "dm" ? store.userMessage.byId[id] : store.channelMessage[id], - selects: store.ui.selectMessages[`${context}_${id}`], - loginUid: store.authData.user?.uid ?? 0, - data: context == "channel" ? store.channels.byId[id] : undefined, - messageData: store.message || {} - }; - }); - const { unreads = 0 } = getUnreadCount({ - mids, - readIndex, - messageData, - loginUid - }); - console.log("unreads", unreads); + const { t } = useTranslation("chat"); + const { readIndex, mids, messageData, loginUid } = useAppSelector((store) => { + return { + readIndex: + context == "channel" ? store.footprint.readChannels[id] : store.footprint.readUsers[id], + mids: context == "dm" ? store.userMessage.byId[id] : store.channelMessage[id], + selects: store.ui.selectMessages[`${context}_${id}`], + loginUid: store.authData.user?.uid ?? 0, + data: context == "channel" ? store.channels.byId[id] : undefined, + messageData: store.message || {} + }; + }); + const { unreads = 0 } = getUnreadCount({ + mids, + readIndex, + messageData, + loginUid + }); + console.log("unreads", unreads); - - return ( - - ); + unreads > 0 ? "flex" : "hidden" + )} + > + + + ); }; -export default NewMessageBottomTip; \ No newline at end of file +export default NewMessageBottomTip; diff --git a/src/routes/chat/Layout/NewMessageTopTip.tsx b/src/routes/chat/Layout/NewMessageTopTip.tsx index 2d8cbca5..af1e9d0a 100644 --- a/src/routes/chat/Layout/NewMessageTopTip.tsx +++ b/src/routes/chat/Layout/NewMessageTopTip.tsx @@ -1,46 +1,49 @@ -import clsx from 'clsx'; -import { useState, useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { triggerScrollHeight } from './MessageFeed'; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + +import { triggerScrollHeight } from "./MessageFeed"; type Props = { - count: number, - queryKey: string -} + count: number; + queryKey: string; +}; // linear-gradient(135deg,_#3C8CE7_0%,_#00EAFF_100%) const NewMessageBottomTip = ({ count, queryKey }: Props) => { - const [visible, setVisible] = useState(false); - const { t } = useTranslation("chat"); - useEffect(() => { - const container = document.querySelector(queryKey) as HTMLElement; - if (container) { - const { scrollHeight, scrollTop, offsetHeight } = container; - const deltaNum = scrollHeight - scrollTop - offsetHeight; - const showTheTip = deltaNum > triggerScrollHeight && count > 0; - console.log("show the tip", showTheTip); - setVisible(showTheTip); - } - }, [queryKey, count]); - const handleMarkRead = () => { - const container = document.querySelector(queryKey) as HTMLElement; - if (container) { - // scroll to bottom - container.scrollTop = container.scrollHeight; - setVisible(false); - } - }; - return ( - + ); }; -export default NewMessageBottomTip; \ No newline at end of file +export default NewMessageBottomTip; diff --git a/src/routes/chat/Layout/Operations.tsx b/src/routes/chat/Layout/Operations.tsx index 072c0e0c..60742f6b 100644 --- a/src/routes/chat/Layout/Operations.tsx +++ b/src/routes/chat/Layout/Operations.tsx @@ -1,17 +1,18 @@ import { FC, useState } from "react"; -import { useKey } from "rooks"; import toast from "react-hot-toast"; -import useDeleteMessage from "@/hooks/useDeleteMessage"; -import useFavMessage from "@/hooks/useFavMessage"; +import { useKey } from "rooks"; + import { updateSelectMessages } from "@/app/slices/ui"; -import IconForward from "@/assets/icons/forward.svg"; -import IconBookmark from "@/assets/icons/bookmark.svg"; -import IconDelete from "@/assets/icons/delete.svg"; -import IconClose from "@/assets/icons/close.circle.svg"; -import ForwardModal from "@/components/ForwardModal"; -import DeleteMessageConfirmModal from "@/components/DeleteMessageConfirm"; import { useAppDispatch, useAppSelector } from "@/app/store"; import { ChatContext } from "@/types/common"; +import DeleteMessageConfirmModal from "@/components/DeleteMessageConfirm"; +import ForwardModal from "@/components/ForwardModal"; +import useDeleteMessage from "@/hooks/useDeleteMessage"; +import useFavMessage from "@/hooks/useFavMessage"; +import IconBookmark from "@/assets/icons/bookmark.svg"; +import IconClose from "@/assets/icons/close.circle.svg"; +import IconDelete from "@/assets/icons/delete.svg"; +import IconForward from "@/assets/icons/forward.svg"; type Props = { context: ChatContext; @@ -66,10 +67,17 @@ const Operations: FC = ({ context, id }) => { - - +
          {forwardModalVisible && } {deleteModalVisible && ( diff --git a/src/routes/chat/Layout/VirtualMessageFeed/CustomHeader.tsx b/src/routes/chat/Layout/VirtualMessageFeed/CustomHeader.tsx index a81f367a..90e3ccf6 100644 --- a/src/routes/chat/Layout/VirtualMessageFeed/CustomHeader.tsx +++ b/src/routes/chat/Layout/VirtualMessageFeed/CustomHeader.tsx @@ -1,54 +1,60 @@ -import { NavLink, useLocation } from 'react-router-dom'; -import clsx from 'clsx'; -import { useTranslation } from 'react-i18next'; -import { Waveform } from '@uiball/loaders'; +import { useTranslation } from "react-i18next"; +import { NavLink, useLocation } from "react-router-dom"; +import { Waveform } from "@uiball/loaders"; +import clsx from "clsx"; -import { useAppSelector } from '@/app/store'; +import { useAppSelector } from "@/app/store"; import EditIcon from "@/assets/icons/edit.svg"; type ChannelHeaderProps = { - cid: number -} + cid: number; +}; const ChannelHeader = ({ cid }: ChannelHeaderProps) => { - const { pathname } = useLocation(); - const { t } = useTranslation("chat"); - const { data, loginUser } = useAppSelector(store => { - return { - loginUser: store.authData.user, - data: store.channels.byId[cid] - - }; - }); - return ( -
          -

          {t("welcome_channel", { name: data?.name })}

          -

          {t("welcome_desc", { name: data?.name })}

          - {loginUser?.is_admin && ( - - - {t("edit_channel")} - - )} -
          - ); + const { pathname } = useLocation(); + const { t } = useTranslation("chat"); + const { data, loginUser } = useAppSelector((store) => { + return { + loginUser: store.authData.user, + data: store.channels.byId[cid] + }; + }); + return ( +
          +

          + {t("welcome_channel", { name: data?.name })} +

          +

          {t("welcome_desc", { name: data?.name })}

          + {loginUser?.is_admin && ( + + + {t("edit_channel")} + + )} +
          + ); }; type Props = { - context?: { - id: number, - isChannel: boolean, - loadingMore: boolean - } -} + context?: { + id: number; + isChannel: boolean; + loadingMore: boolean; + }; +}; const CustomHeader = ({ context }: Props) => { - if (!context) return null; - const { id, isChannel, loadingMore } = context; - return <> - {isChannel ? : null} -
          - -
          - ; + if (!context) return null; + const { id, isChannel, loadingMore } = context; + return ( + <> + {isChannel ? : null} +
          + +
          + + ); }; -export default CustomHeader; \ No newline at end of file +export default CustomHeader; diff --git a/src/routes/chat/Layout/VirtualMessageFeed/CustomList.tsx b/src/routes/chat/Layout/VirtualMessageFeed/CustomList.tsx index 6e948aae..15be7f81 100644 --- a/src/routes/chat/Layout/VirtualMessageFeed/CustomList.tsx +++ b/src/routes/chat/Layout/VirtualMessageFeed/CustomList.tsx @@ -1,10 +1,9 @@ -import { forwardRef } from 'react'; +import { forwardRef } from "react"; // @ts-ignore const CustomList = forwardRef(({ style, ...props }, ref) => { - // @ts-ignore - return
          ; + // @ts-ignore + return
          ; }); - -export default CustomList; \ No newline at end of file +export default CustomList; diff --git a/src/routes/chat/Layout/VirtualMessageFeed/index.tsx b/src/routes/chat/Layout/VirtualMessageFeed/index.tsx index 29016a0b..7d601158 100644 --- a/src/routes/chat/Layout/VirtualMessageFeed/index.tsx +++ b/src/routes/chat/Layout/VirtualMessageFeed/index.tsx @@ -1,147 +1,154 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useDispatch } from "react-redux"; +import { Virtuoso, VirtuosoHandle } from "react-virtuoso"; // import clsx from 'clsx'; // import { useTranslation } from 'react-i18next'; -import { useDebounce } from 'rooks'; -import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { useDebounce } from "rooks"; -import { useLazyLoadMoreMessagesQuery, useReadMessageMutation } from '@/app/services/message'; -import { useAppSelector } from '@/app/store'; -import { renderMessageFragment } from '../../utils'; +import { useLazyLoadMoreMessagesQuery, useReadMessageMutation } from "@/app/services/message"; +import { updateHistoryMark } from "@/app/slices/footprint"; +import { useAppSelector } from "@/app/store"; +import { ChatContext } from "@/types/common"; +import { renderMessageFragment } from "../../utils"; import NewMessageBottomTip from "../NewMessageBottomTip"; -import CustomList from './CustomList'; -import CustomHeader from './CustomHeader'; -import { useDispatch } from 'react-redux'; -import { updateHistoryMark } from '@/app/slices/footprint'; -import { ChatContext } from '@/types/common'; +import CustomHeader from "./CustomHeader"; +import CustomList from "./CustomList"; + type Props = { - context: ChatContext, - id: number -} + context: ChatContext; + id: number; +}; // const firstMsgIndex = 10000; // let prevMids: number[] = []; const VirtualMessageFeed = ({ context, id }: Props) => { - const dispatch = useDispatch(); - // const { t } = useTranslation("chat"); - // const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex); - const [atBottom, setAtBottom] = useState(false); - const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] = useLazyLoadMoreMessagesQuery(); - const vList = useRef(null); - const [updateReadIndex] = useReadMessageMutation(); - const updateReadDebounced = useDebounce(updateReadIndex, 300); - const { - historyMid = "", - mids = [], - selects, - messageData, - loginUser, - footprint - } = useAppSelector((store) => { - return { - historyMid: context == "dm" ? store.footprint.historyUsers[id] : store.footprint.historyChannels[id], - mids: context == "dm" ? store.userMessage.byId[id] : store.channelMessage[id], - selects: store.ui.selectMessages[`${context}_${id}`], - footprint: store.footprint, - loginUser: store.authData.user, - messageData: store.message || {} - }; - }); + const dispatch = useDispatch(); + // const { t } = useTranslation("chat"); + // const [firstItemIndex, setFirstItemIndex] = useState(firstMsgIndex); + const [atBottom, setAtBottom] = useState(false); + const [loadMoreMessage, { isLoading: loadingMore, isSuccess, data: historyData }] = + useLazyLoadMoreMessagesQuery(); + const vList = useRef(null); + const [updateReadIndex] = useReadMessageMutation(); + const updateReadDebounced = useDebounce(updateReadIndex, 300); + const { + historyMid = "", + mids = [], + selects, + messageData, + loginUser, + footprint + } = useAppSelector((store) => { + return { + historyMid: + context == "dm" ? store.footprint.historyUsers[id] : store.footprint.historyChannels[id], + mids: context == "dm" ? store.userMessage.byId[id] : store.channelMessage[id], + selects: store.ui.selectMessages[`${context}_${id}`], + footprint: store.footprint, + loginUser: store.authData.user, + messageData: store.message || {} + }; + }); - useEffect(() => { - if (isSuccess && historyData) { - if (historyData.length == 0) { - // 到顶了 - dispatch(updateHistoryMark({ type: context, id, mid: "reached" })); - } else { - // 记录最新的mid - const [{ mid }] = historyData; - dispatch(updateHistoryMark({ type: context, id, mid: `${mid}` })); - } - } - }, [isSuccess, historyData, mids, context, id]); - // useEffect(() => { - // console.log("diff mids", prevMids, mids); - // const newCount = mids.length - prevMids.length; - // setFirstItemIndex((prev) => prev - newCount); - // }, [mids]); + useEffect(() => { + if (isSuccess && historyData) { + if (historyData.length == 0) { + // 到顶了 + dispatch(updateHistoryMark({ type: context, id, mid: "reached" })); + } else { + // 记录最新的mid + const [{ mid }] = historyData; + dispatch(updateHistoryMark({ type: context, id, mid: `${mid}` })); + } + } + }, [isSuccess, historyData, mids, context, id]); + // useEffect(() => { + // console.log("diff mids", prevMids, mids); + // const newCount = mids.length - prevMids.length; + // setFirstItemIndex((prev) => prev - newCount); + // }, [mids]); - // 加载更多 - const handleTopStateChange = (isTop: boolean) => { - console.log("reach top ", isTop); - if (isTop) { - if (historyMid === "reached") return; - let lastMid = mids.slice(0, 1)[0]; - if (historyMid) { - lastMid = +historyMid; - } - // prevMids = mids; - loadMoreMessage({ context, id, mid: lastMid }); - } - }; - // 自动跟随 - const handleFollowOutput = (isAtBottom: boolean) => { - const [lastMid] = mids ? mids.slice(-1) : [0]; - const ts = new Date().getTime(); - // tricky - const isSentByMyself = ts - lastMid < 1000; - if (isAtBottom || isSentByMyself) { - return isAtBottom ? "smooth" : true; - } else { - return false; - } - }; - // 滚动到底部 - const handleScrollBottom = useCallback(() => { - const vl = vList!.current; - if (vl) { - vl.scrollToIndex(mids.length - 1); - } - }, [mids]); - const handleBottomStateChange = (bottom: boolean) => { - setAtBottom(bottom); - }; - const readIndex = context == "channel" ? footprint.readChannels[id] : footprint.readUsers[id]; - return <> - { - // 计算出真正的index - // const idx = index - firstItemIndex; - const curr = messageData[mid]; - if (!curr) return
          ; - const isFirst = idx == 0; - const prev = isFirst ? null : messageData[mids[idx - 1]]; - const read = curr?.from_uid == loginUser?.uid || mid <= readIndex; - return renderMessageFragment({ - selectMode: !!selects, - updateReadIndex: updateReadDebounced, - read, - prev, - curr, - contextId: id, - context - }); - }} /> - {!atBottom && } + // 加载更多 + const handleTopStateChange = (isTop: boolean) => { + console.log("reach top ", isTop); + if (isTop) { + if (historyMid === "reached") return; + let lastMid = mids.slice(0, 1)[0]; + if (historyMid) { + lastMid = +historyMid; + } + // prevMids = mids; + loadMoreMessage({ context, id, mid: lastMid }); + } + }; + // 自动跟随 + const handleFollowOutput = (isAtBottom: boolean) => { + const [lastMid] = mids ? mids.slice(-1) : [0]; + const ts = new Date().getTime(); + // tricky + const isSentByMyself = ts - lastMid < 1000; + if (isAtBottom || isSentByMyself) { + return isAtBottom ? "smooth" : true; + } else { + return false; + } + }; + // 滚动到底部 + const handleScrollBottom = useCallback(() => { + const vl = vList!.current; + if (vl) { + vl.scrollToIndex(mids.length - 1); + } + }, [mids]); + const handleBottomStateChange = (bottom: boolean) => { + setAtBottom(bottom); + }; + const readIndex = context == "channel" ? footprint.readChannels[id] : footprint.readUsers[id]; + return ( + <> + { + // 计算出真正的index + // const idx = index - firstItemIndex; + const curr = messageData[mid]; + if (!curr) return
          ; + const isFirst = idx == 0; + const prev = isFirst ? null : messageData[mids[idx - 1]]; + const read = curr?.from_uid == loginUser?.uid || mid <= readIndex; + return renderMessageFragment({ + selectMode: !!selects, + updateReadIndex: updateReadDebounced, + read, + prev, + curr, + contextId: id, + context + }); + }} + /> + {!atBottom && ( + + )} - ; + ); }; -export default VirtualMessageFeed; \ No newline at end of file +export default VirtualMessageFeed; diff --git a/src/routes/chat/Layout/index.tsx b/src/routes/chat/Layout/index.tsx index 98289eaa..41475c6e 100644 --- a/src/routes/chat/Layout/index.tsx +++ b/src/routes/chat/Layout/index.tsx @@ -1,25 +1,23 @@ -import { useRef, useEffect, FC, ReactElement } from "react"; +import { FC, ReactElement, useEffect, useRef } from "react"; import { useDrop } from "react-dnd"; import { NativeTypes } from "react-dnd-html5-backend"; -import clsx from "clsx"; import { toast } from "react-hot-toast"; +import clsx from "clsx"; - -import Send from "@/components/Send"; -import Operations from "./Operations"; -import useUploadFile from "@/hooks/useUploadFile"; import { useAppSelector } from "@/app/store"; -import LoginTip from "./LoginTip"; -import useLicense from "@/hooks/useLicense"; -import LicenseUpgradeTip from "./LicenseOutdatedTip"; -import DnDTip from "./DnDTip"; -import IconWarning from '@/assets/icons/warning.svg'; -import ImagePreview from "@/components/ImagePreview"; - -import VirtualMessageFeed from "./VirtualMessageFeed"; -import DMVoice from "./DMVoicing"; -import AddContactTip from "./AddContactTip"; import { ChatContext } from "@/types/common"; +import ImagePreview from "@/components/ImagePreview"; +import Send from "@/components/Send"; +import useLicense from "@/hooks/useLicense"; +import useUploadFile from "@/hooks/useUploadFile"; +import IconWarning from "@/assets/icons/warning.svg"; +import AddContactTip from "./AddContactTip"; +import DMVoice from "./DMVoicing"; +import DnDTip from "./DnDTip"; +import LicenseUpgradeTip from "./LicenseOutdatedTip"; +import LoginTip from "./LoginTip"; +import Operations from "./Operations"; +import VirtualMessageFeed from "./VirtualMessageFeed"; interface Props { readonly?: boolean; @@ -62,10 +60,10 @@ const Layout: FC = ({ // console.log("iii", inputMode); if (inputMode !== "text") { toast("DnD not allowed in this input mode", { - icon: , + icon: }); return; - }; + } if (files.length) { const filesData = files.map((file) => { const { size, type, name } = file; @@ -99,8 +97,11 @@ const Layout: FC = ({
          {header} -
          -
          +
          +
          {context == "dm" && } {context == "dm" && } {/* 消息流 */} @@ -121,15 +122,23 @@ const Layout: FC = ({
          - {aside &&
          - {aside} -
          } - {users &&
          {users}
          } - {voice &&
          {voice}
          } - {!readonly && inputMode == "text" && isActive && ( - + {aside && ( +
          + {aside} +
          )} -
          + {users &&
          {users}
          } + {voice && ( +
          + {voice} +
          + )} + {!readonly && inputMode == "text" && isActive && } + ); }; diff --git a/src/routes/chat/LoadMore.tsx b/src/routes/chat/LoadMore.tsx index dbf48b7d..72c42a8f 100644 --- a/src/routes/chat/LoadMore.tsx +++ b/src/routes/chat/LoadMore.tsx @@ -1,11 +1,10 @@ import { FC } from "react"; import { Waveform } from "@uiball/loaders"; -type Props = { -}; +type Props = {}; const LoadMore: FC = () => { return ( -
          +
          ); diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index cf1e7f1c..a1d06c34 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -1,92 +1,142 @@ // import React from 'react'; -import { useTranslation } from 'react-i18next'; -import clsx from 'clsx'; -import { useAppSelector } from '../../app/store'; -import User from '../../components/User'; -import IconMic from '@/assets/icons/mic.on.svg'; -import IconMicOff from '@/assets/icons/mic.off.svg'; -import IconCallOff from '@/assets/icons/headphone.svg'; -import IconSoundOn from '@/assets/icons/sound.on.svg'; -import IconSoundOff from '@/assets/icons/sound.off.svg'; -import IconCamera from '@/assets/icons/camera.svg'; -import IconScreen from '@/assets/icons/share.screen.svg'; -import { useVoice } from '../../components/Voice'; -import Signal from '../../components/Signal'; -import Tooltip from '../../components/Tooltip'; -import { ChatContext } from '../../types/common'; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + +import IconCamera from "@/assets/icons/camera.svg"; +import IconCallOff from "@/assets/icons/headphone.svg"; +import IconMicOff from "@/assets/icons/mic.off.svg"; +import IconMic from "@/assets/icons/mic.on.svg"; +import IconScreen from "@/assets/icons/share.screen.svg"; +import IconSoundOff from "@/assets/icons/sound.off.svg"; +import IconSoundOn from "@/assets/icons/sound.on.svg"; +import { useAppSelector } from "../../app/store"; +import Signal from "../../components/Signal"; +import Tooltip from "../../components/Tooltip"; +import User from "../../components/User"; +import { useVoice } from "../../components/Voice"; +import { ChatContext } from "../../types/common"; type Props = { - id: number, - context?: ChatContext -} - -const RTCWidget = ({ id, context = "channel" }: Props) => { - const { t } = useTranslation("chat"); - const { leave, voicingInfo, setMute, setDeafen, joining = true, openCamera, closeCamera, startShareScreen, stopShareScreen } = useVoice({ context, id }); - const { callFrom, callTo, loginUser, channelData, userData } = useAppSelector(store => { - return { - callFrom: store.voice.callingFrom, - callTo: store.voice.callingTo, - userData: store.users.byId, - channelData: store.channels.byId, - loginUser: store.authData.user, - }; - }); - if (!loginUser || !voicingInfo || joining) return null; - // const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name; - // if (!name) return null; - const isReConnecting = voicingInfo.connectionState == "RECONNECTING"; - const targetDMUsername = callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name; - return ( -
          -
          -
          -
          - -
          - {isReConnecting ? `Voice Reconnecting...` : `Voice Connected`} - - {voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : targetDMUsername} - -
          -
          - - - -
          -
          - - -
          -
          -
          -
          - -
          - {loginUser.name} - #{loginUser.uid} -
          -
          -
          - - {voicingInfo.deafen ? : } - - - {voicingInfo.muted ? - - : - } - -
          -
          -
          - ); + id: number; + context?: ChatContext; }; -export default RTCWidget; \ No newline at end of file +const RTCWidget = ({ id, context = "channel" }: Props) => { + const { t } = useTranslation("chat"); + const { + leave, + voicingInfo, + setMute, + setDeafen, + joining = true, + openCamera, + closeCamera, + startShareScreen, + stopShareScreen + } = useVoice({ context, id }); + const { callFrom, callTo, loginUser, channelData, userData } = useAppSelector((store) => { + return { + callFrom: store.voice.callingFrom, + callTo: store.voice.callingTo, + userData: store.users.byId, + channelData: store.channels.byId, + loginUser: store.authData.user + }; + }); + if (!loginUser || !voicingInfo || joining) return null; + // const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name; + // if (!name) return null; + const isReConnecting = voicingInfo.connectionState == "RECONNECTING"; + const targetDMUsername = + callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name; + return ( +
          +
          +
          +
          + +
          + + {isReConnecting ? `Voice Reconnecting...` : `Voice Connected`} + + + {voicingInfo.context == "channel" ? "Channel" : "DM"} /{" "} + {voicingInfo.context == "channel" + ? channelData[voicingInfo.id]?.name + : targetDMUsername} + +
          +
          + + + +
          +
          + + +
          +
          +
          +
          + +
          + {loginUser.name} + #{loginUser.uid} +
          +
          +
          + + {voicingInfo.deafen ? ( + + ) : ( + + )} + + + {voicingInfo.muted ? ( + + ) : ( + + )} + +
          +
          +
          + ); +}; + +export default RTCWidget; diff --git a/src/routes/chat/SessionList/ContextMenu.tsx b/src/routes/chat/SessionList/ContextMenu.tsx index 0e7cba69..c556f404 100644 --- a/src/routes/chat/SessionList/ContextMenu.tsx +++ b/src/routes/chat/SessionList/ContextMenu.tsx @@ -1,16 +1,22 @@ import { FC, ReactElement } from "react"; -import Tippy from "@tippyjs/react"; +import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import { useNavigate, useLocation, useMatch } from "react-router-dom"; -import { usePinChatMutation, useUnpinChatMutation, useUpdateMuteSettingMutation } from "@/app/services/user"; +import { useLocation, useMatch, useNavigate } from "react-router-dom"; +import Tippy from "@tippyjs/react"; + import { useReadMessageMutation } from "@/app/services/message"; +import { + usePinChatMutation, + useUnpinChatMutation, + useUpdateMuteSettingMutation +} from "@/app/services/user"; import { removeUserSession } from "@/app/slices/message.user"; +import { useAppSelector } from "@/app/store"; +import { ChatContext } from "@/types/common"; import ContextMenu, { Item } from "@/components/ContextMenu"; import useUserOperation from "@/hooks/useUserOperation"; -import { useAppSelector } from "@/app/store"; -import { useTranslation } from "react-i18next"; -import { ChatContext } from "@/types/common"; import { compareVersion } from "@/utils"; + type Props = { context: ChatContext; pinned: boolean; @@ -93,57 +99,57 @@ const SessionContextMenu: FC = ({ const items = context == "dm" ? [ - pinVisible && { - title: pinTxt, - handler: handlePinChat - }, - { - title: t("action.mark_read"), - handler: handleReadAll - }, - { - title: t("setting"), - handler: handleDMSetting - }, - canCopyEmail && { - title: t("action.copy_email"), - handler: copyEmail - }, - { - title: t("action.hide_session"), - danger: true, - handler: handleRemoveSession - } - ] + pinVisible && { + title: pinTxt, + handler: handlePinChat + }, + { + title: t("action.mark_read"), + handler: handleReadAll + }, + { + title: t("setting"), + handler: handleDMSetting + }, + canCopyEmail && { + title: t("action.copy_email"), + handler: copyEmail + }, + { + title: t("action.hide_session"), + danger: true, + handler: handleRemoveSession + } + ] : [ - pinVisible && { - title: pinTxt, - handler: handlePinChat - }, - { - 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 - }, - canInviteChannel && { - title: t("action.invite_people"), - handler: setInviteChannelId.bind(null, id) - }, - canDeleteChannel && { - title: t("action.delete_channel"), - danger: true, - handler: deleteChannel.bind(null, id) - } - ]; + pinVisible && { + title: pinTxt, + handler: handlePinChat + }, + { + 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 + }, + canInviteChannel && { + title: t("action.invite_people"), + handler: setInviteChannelId.bind(null, id) + }, + canDeleteChannel && { + title: t("action.delete_channel"), + danger: true, + handler: deleteChannel.bind(null, id) + } + ]; return ( = ({ mid: number; is_public: boolean; }>(); - const { callingFrom, callingTo, messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceList } = useAppSelector( - (store) => { - return { - callingFrom: store.voice.callingFrom, - callingTo: store.voice.callingTo, - voiceList: store.voice.list, - mids: type == "dm" ? store.userMessage.byId[id] : store.channelMessage[id], - loginUid: store.authData.user?.uid || 0, - readIndex: - type == "dm" ? store.footprint.readUsers[id] : store.footprint.readChannels[id], - messageData: store.message, - userData: store.users.byId, - channelData: store.channels.byId, - muted: type == "dm" ? store.footprint.muteUsers[id] : store.footprint.muteChannels[id] - }; - } - ); + const { + callingFrom, + callingTo, + messageData, + userData, + channelData, + readIndex, + loginUid, + mids, + muted, + voiceList + } = useAppSelector((store) => { + return { + callingFrom: store.voice.callingFrom, + callingTo: store.voice.callingTo, + voiceList: store.voice.list, + mids: type == "dm" ? store.userMessage.byId[id] : store.channelMessage[id], + loginUid: store.authData.user?.uid || 0, + readIndex: type == "dm" ? store.footprint.readUsers[id] : store.footprint.readChannels[id], + messageData: store.message, + userData: store.users.byId, + channelData: store.channels.byId, + muted: type == "dm" ? store.footprint.muteUsers[id] : store.footprint.muteChannels[id] + }; + }); useEffect(() => { const tmp = type == "dm" ? userData[id] : channelData[id]; @@ -106,9 +115,12 @@ const Session: FC = ({ messageData, loginUid }); - const isVoicing = type == "channel" ? voiceList.some((item) => { - return item.context == type && item.id === id; - }) : (id == callingFrom || id == callingTo); + const isVoicing = + type == "channel" + ? voiceList.some((item) => { + return item.context == type && item.id === id; + }) + : id == callingFrom || id == callingTo; console.log("unreads", unreads, isCurrentPath); return ( @@ -125,7 +137,14 @@ const Session: FC = ({ > clsx(`nav flex gap-2 rounded-lg p-2 w-full`, isActive && "shadow-[inset_0_0_0_2px_#52edff]", linkActive && "bg-gray-500/20", pinned ? "md:hover:bg-gray-300/20" : "md:hover:bg-gray-500/20")} + className={({ isActive: linkActive }) => + clsx( + `nav flex gap-2 rounded-lg p-2 w-full`, + isActive && "shadow-[inset_0_0_0_2px_#52edff]", + linkActive && "bg-gray-500/20", + pinned ? "md:hover:bg-gray-300/20" : "md:hover:bg-gray-500/20" + ) + } to={navPath} onContextMenu={handleContextMenuEvent} > @@ -147,7 +166,12 @@ const Session: FC = ({
          - + {name} {!is_public && } @@ -157,10 +181,21 @@ const Session: FC = ({
          - 0 ? `w-36` : ``)}>{renderPreviewMessage(previewMsg)} - {(unreads > 0 && !isCurrentPath) ? - {unreads > 99 ? "99+" : unreads} - : (muted && )} + 0 ? `w-36` : ``)}> + {renderPreviewMessage(previewMsg)} + + {unreads > 0 && !isCurrentPath ? ( + + {unreads > 99 ? "99+" : unreads} + + ) : ( + muted && + )}
          diff --git a/src/routes/chat/SessionList/index.tsx b/src/routes/chat/SessionList/index.tsx index cdd9ad44..b1f6d944 100644 --- a/src/routes/chat/SessionList/index.tsx +++ b/src/routes/chat/SessionList/index.tsx @@ -1,12 +1,13 @@ -import { FC, memo, useRef } from "react"; -import { useState, useEffect } from "react"; +import { FC, memo, useEffect, useRef, useState } from "react"; import { ViewportList } from "react-viewport-list"; -import Session from "./Session"; -import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal"; -import InviteModal from "@/components/InviteModal"; + import { useAppSelector } from "@/app/store"; import { ChatContext } from "@/types/common"; import { PinChatTargetChannel, PinChatTargetUser } from "@/types/sse"; +import InviteModal from "@/components/InviteModal"; +import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal"; +import Session from "./Session"; + export interface ChatSession { type: ChatContext; id: number; @@ -37,7 +38,7 @@ const SessionList: FC = ({ tempSession }) => { }); useEffect(() => { - // const pinDMs= + // const pinDMs= const getSessionObj = (id: number, type: "dm" | "channel") => { const mids = type == "dm" ? userMessage[id] : channelMessage[id]; if (!mids || mids.length == 0) { @@ -47,45 +48,53 @@ const SessionList: FC = ({ tempSession }) => { const mid = [...mids].sort((a, b) => +a - +b).pop(); return { id, mid, type } as ChatSession; }; - const pinTmps = pins.map((p) => { - const { target } = p; - if ("uid" in target) { - return getSessionObj((target as PinChatTargetUser).uid, "dm"); - } - if ("gid" in target) { - return getSessionObj((target as PinChatTargetChannel).gid, "channel"); - } - return null; - }).filter((p) => !!p); - const channelPinIds = pins.map(p => { - if (p.target.gid) { - return p.target.gid; - } - return null; - - }).filter(id => !!id); - const dmPinIds = pins.map(p => { - if (p.target.uid) { - return p.target.uid; - } - return null; - - }).filter(id => !!id); - const cSessions = channelIDs.filter(id => { - return !channelPinIds.includes(id); - }).map((id) => { - return getSessionObj(id, "channel"); - }); - const uSessions = DMs.filter(id => { + const pinTmps = pins + .map((p) => { + const { target } = p; + if ("uid" in target) { + return getSessionObj((target as PinChatTargetUser).uid, "dm"); + } + if ("gid" in target) { + return getSessionObj((target as PinChatTargetChannel).gid, "channel"); + } + return null; + }) + .filter((p) => !!p); + const channelPinIds = pins + .map((p) => { + if (p.target.gid) { + return p.target.gid; + } + return null; + }) + .filter((id) => !!id); + const dmPinIds = pins + .map((p) => { + if (p.target.uid) { + return p.target.uid; + } + return null; + }) + .filter((id) => !!id); + const cSessions = channelIDs + .filter((id) => { + return !channelPinIds.includes(id); + }) + .map((id) => { + return getSessionObj(id, "channel"); + }); + const uSessions = DMs.filter((id) => { return !dmPinIds.includes(id); }).map((id) => { return getSessionObj(id, "dm"); }); - const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => { - const { mid: aMid = 0 } = a; - const { mid: bMid = 0 } = b; - return bMid - aMid; - }); + const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort( + (a, b) => { + const { mid: aMid = 0 } = a; + const { mid: bMid = 0 } = b; + return bMid - aMid; + } + ); // console.log("before qqqq", temps); const newSessions = tempSession ? [tempSession, ...temps] : temps; // console.log("qqqq", newSessions); @@ -105,29 +114,27 @@ const SessionList: FC = ({ tempSession }) => { return ( <> - {pinSessions.length ?
            - {pinSessions.map((p) => { - const { type, id, mid = 0 } = p; - const key = `${type}_${id}`; - return ( - - ); - })} -
          : null} + {pinSessions.length ? ( +
            + {pinSessions.map((p) => { + const { type, id, mid = 0 } = p; + const key = `${type}_${id}`; + return ( + + ); + })} +
          + ) : null}
            - + {(s) => { const { type, id, mid = 0 } = s; const key = `${type}_${id}`; diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index 5effb496..92b99651 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -1,20 +1,26 @@ // import { useEffect } from 'react'; // import { useDispatch } from 'react-redux'; -import VoiceManagement from './VoiceManagement'; -import { useVoice } from '../../../components/Voice'; -import { ChatContext } from '../../../types/common'; +import { useVoice } from "../../../components/Voice"; +import { ChatContext } from "../../../types/common"; +import VoiceManagement from "./VoiceManagement"; type Props = { - visible: boolean, - context?: ChatContext, - id: number, -} + visible: boolean; + context?: ChatContext; + id: number; +}; const Dashboard = ({ context = "channel", id, visible }: Props) => { const { voicingInfo } = useVoice({ id, context }); - return
            - -
            ; + return ( +
            + +
            + ); }; -export default Dashboard; \ No newline at end of file +export default Dashboard; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index f9c24403..c5999a9a 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -1,110 +1,128 @@ -import clsx from 'clsx'; -import { useEffect } from 'react'; +import { useEffect } from "react"; +import { useDispatch } from "react-redux"; +import clsx from "clsx"; -import { VoicingInfo, updatePin } from '../../../app/slices/voice'; -import { useAppSelector } from '../../../app/store'; -import Avatar from '../../../components/Avatar'; -import IconMic from '@/assets/icons/mic.on.svg'; -import IconMicOff from '@/assets/icons/mic.off.svg'; -import IconFullscreen from '@/assets/icons/fullscreen.svg'; -import { playAgoraVideo } from '@/utils'; -import { ChatContext } from '../../../types/common'; -import Operations from '@/components/Voice/Operations'; -import { updateChannelVisibleAside } from '@/app/slices/footprint'; -import { useDispatch } from 'react-redux'; +import { updateChannelVisibleAside } from "@/app/slices/footprint"; +import Operations from "@/components/Voice/Operations"; +import { playAgoraVideo } from "@/utils"; +import IconFullscreen from "@/assets/icons/fullscreen.svg"; +import IconMicOff from "@/assets/icons/mic.off.svg"; +import IconMic from "@/assets/icons/mic.on.svg"; +import { updatePin, VoicingInfo } from "../../../app/slices/voice"; +import { useAppSelector } from "../../../app/store"; +import Avatar from "../../../components/Avatar"; +import { ChatContext } from "../../../types/common"; type Props = { - id: number, - context: ChatContext, - info: VoicingInfo | null, -} - -const VoiceManagement = ({ id, context, info, }: Props) => { - const dispatch = useDispatch(); - const { userData, voicingMembers } = useAppSelector(store => { - return { - userData: store.users.byId, - voicingMembers: store.voice.voicingMembers - }; - }); - useEffect(() => { - const ids = voicingMembers.ids; - ids.forEach(id => { - playAgoraVideo(id); - }); - }, [voicingMembers.ids]); - const handleFullscreen = (uid?: number) => { - if (context == "channel") { - dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); - if (uid) { - dispatch(updatePin({ uid, action: "pin" })); - } - } - }; - if (!info) return null; - const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`); - const members = voicingMembers.ids; - const membersData = voicingMembers.byId; - if (info.joining) { - return
            - Connecting to voice channel... -
            ; - } - if (info.connectionState == "RECONNECTING") { - return
            - - Reconnecting... - - Please check network connection! -
            ; - } - return ( -
            -
              - {members.map((uid) => { - const curr = userData[uid]; - if (!curr) return null; - const { muted, speakingVolume = 0 } = membersData[uid]; - const speaking = speakingVolume > 50; - return
            • -
              -
              -
              - {speaking &&
              } - -
              - - {curr?.name} - -
              -
              - {/* {deafen ? : } */} - {muted ? : } -
              -
              -
              -
              - {/* camera placeholder */} -
              - -
              -
            • ; - })} -
            -
            - -
            -
            - ); + id: number; + context: ChatContext; + info: VoicingInfo | null; }; -export default VoiceManagement; \ No newline at end of file +const VoiceManagement = ({ id, context, info }: Props) => { + const dispatch = useDispatch(); + const { userData, voicingMembers } = useAppSelector((store) => { + return { + userData: store.users.byId, + voicingMembers: store.voice.voicingMembers + }; + }); + useEffect(() => { + const ids = voicingMembers.ids; + ids.forEach((id) => { + playAgoraVideo(id); + }); + }, [voicingMembers.ids]); + const handleFullscreen = (uid?: number) => { + if (context == "channel") { + dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); + if (uid) { + dispatch(updatePin({ uid, action: "pin" })); + } + } + }; + if (!info) return null; + const nameClass = clsx( + `text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white` + ); + const members = voicingMembers.ids; + const membersData = voicingMembers.byId; + if (info.joining) { + return ( +
            + Connecting to voice channel... +
            + ); + } + if (info.connectionState == "RECONNECTING") { + return ( +
            + Reconnecting... + Please check network connection! +
            + ); + } + return ( +
            +
              + {members.map((uid) => { + const curr = userData[uid]; + if (!curr) return null; + const { muted, speakingVolume = 0 } = membersData[uid]; + const speaking = speakingVolume > 50; + return ( +
            • +
              +
              +
              + {speaking && ( +
              + )} + +
              + + {curr?.name} + +
              +
              + {/* {deafen ? : } */} + {muted ? ( + + ) : ( + + )} +
              +
              +
              +
              + {/* camera placeholder */} +
              + +
              +
            • + ); + })} +
            +
            + +
            +
            + ); +}; + +export default VoiceManagement; diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index 230d21c2..361f4c98 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -1,77 +1,88 @@ // import React from 'react'; // import Tippy from '@tippyjs/react'; // import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useDispatch } from 'react-redux'; -import { updateChannelVisibleAside, updateDMVisibleAside } from '../../../app/slices/footprint'; -import { useAppSelector } from '../../../app/store'; -import IconHeadphone from '@/assets/icons/headphone.svg'; -import Tooltip from '../../../components/Tooltip'; -import { useVoice } from '../../../components/Voice'; -import { useGetAgoraStatusQuery } from '../../../app/services/server'; -import { ChatContext } from '../../../types/common'; -import { updateCallInfo } from '../../../app/slices/voice'; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; + +import { useGetAgoraStatusQuery } from "@/app/services/server"; +import { updateChannelVisibleAside, updateDMVisibleAside } from "@/app/slices/footprint"; +import { updateCallInfo } from "@/app/slices/voice"; +import { useAppSelector } from "@/app/store"; +import { ChatContext } from "@/types/common"; +import Tooltip from "@/components/Tooltip"; +import { useVoice } from "@/components/Voice"; +import IconHeadphone from "@/assets/icons/headphone.svg"; type Props = { - context?: ChatContext - id: number, -} - -const VoiceChat = ({ id, context = "channel" }: Props) => { - const { joinVoice, joined, joining = false, joinedAtThisContext } = useVoice({ id, context }); - const dispatch = useDispatch(); - const { loginUser, voiceList, visibleAside } = useAppSelector(store => { - return { - loginUser: store.authData.user, - contextData: context == "channel" ? store.channels.byId[id] : store.users.byId[id], - voiceList: store.voice.list, - visibleAside: context == "channel" ? store.footprint.channelAsides[id] : null, - }; - }); - const { data: enabled } = useGetAgoraStatusQuery(); - const { t } = useTranslation("chat"); - const toggleDashboard = () => { - const data = { - id, - aside: visibleAside == "voice" ? null : "voice" as const - }; - dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); - }; - const handleJoin = () => { - if (joining || joined) { - alert("You have joined another channel, please leave first!"); - return; - } - joinVoice(); - const data = { - id, - aside: "voice" as const - }; - dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); - // 实时显示calling box - if (!joinedAtThisContext && context == "dm") { - dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: id, calling: false })); - } - }; - if (!loginUser || !enabled) return null; - const visible = visibleAside == "voice"; - const memberCount = voiceList.find((v) => v.context == context && v.id == id)?.memberCount ?? 0; - const badgeClass = `absolute -top-2 -right-2 w-4 h-4 rounded-full bg-primary-400 text-white `; - return ( - -
          • - - {visible ? - null - : <> - {memberCount > 0 && {memberCount}} - - + - - } -
          • -
            - ); + context?: ChatContext; + id: number; }; -export default VoiceChat; \ No newline at end of file +const VoiceChat = ({ id, context = "channel" }: Props) => { + const { joinVoice, joined, joining = false, joinedAtThisContext } = useVoice({ id, context }); + const dispatch = useDispatch(); + const { loginUser, voiceList, visibleAside } = useAppSelector((store) => { + return { + loginUser: store.authData.user, + contextData: context == "channel" ? store.channels.byId[id] : store.users.byId[id], + voiceList: store.voice.list, + visibleAside: context == "channel" ? store.footprint.channelAsides[id] : null + }; + }); + const { data: enabled } = useGetAgoraStatusQuery(); + const { t } = useTranslation("chat"); + const toggleDashboard = () => { + const data = { + id, + aside: visibleAside == "voice" ? null : ("voice" as const) + }; + dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); + }; + const handleJoin = () => { + if (joining || joined) { + alert("You have joined another channel, please leave first!"); + return; + } + joinVoice(); + const data = { + id, + aside: "voice" as const + }; + dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); + // 实时显示calling box + if (!joinedAtThisContext && context == "dm") { + dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: id, calling: false })); + } + }; + if (!loginUser || !enabled) return null; + const visible = visibleAside == "voice"; + const memberCount = voiceList.find((v) => v.context == context && v.id == id)?.memberCount ?? 0; + const badgeClass = `absolute -top-2 -right-2 w-4 h-4 rounded-full bg-primary-400 text-white `; + return ( + +
          • + + {visible ? null : ( + <> + {memberCount > 0 && ( + + {memberCount} + + )} + + + + + + )} +
          • +
            + ); +}; + +export default VoiceChat; diff --git a/src/routes/chat/VoiceFullscreen.tsx b/src/routes/chat/VoiceFullscreen.tsx index b997863c..c922e3f1 100644 --- a/src/routes/chat/VoiceFullscreen.tsx +++ b/src/routes/chat/VoiceFullscreen.tsx @@ -1,126 +1,184 @@ -import { useEffect, useState, MouseEvent } from 'react'; -import { useDispatch } from 'react-redux'; -import clsx from 'clsx'; -import IconMic from '@/assets/icons/mic.on.svg'; -import IconMicOff from '@/assets/icons/mic.off.svg'; -import IconPin from '@/assets/icons/pin.svg'; -import IconExitScreen from '@/assets/icons/fullscreen.exit.svg'; -import { ChatContext } from '../../types/common'; -import { useAppSelector } from '../../app/store'; -import Avatar from '../../components/Avatar'; -import { playAgoraVideo } from '../../utils'; -import Tooltip from '../../components/Tooltip'; -import { useVoice } from '../../components/Voice'; -import { updatePin } from '../../app/slices/voice'; -import Operations from '@/components/Voice/Operations'; +import { MouseEvent, useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import clsx from "clsx"; + +import Operations from "@/components/Voice/Operations"; +import IconExitScreen from "@/assets/icons/fullscreen.exit.svg"; +import IconMicOff from "@/assets/icons/mic.off.svg"; +import IconMic from "@/assets/icons/mic.on.svg"; +import IconPin from "@/assets/icons/pin.svg"; +import { updatePin } from "../../app/slices/voice"; +import { useAppSelector } from "../../app/store"; +import Avatar from "../../components/Avatar"; +import Tooltip from "../../components/Tooltip"; +import { useVoice } from "../../components/Voice"; +import { ChatContext } from "../../types/common"; +import { playAgoraVideo } from "../../utils"; type Props = { - context: ChatContext, - id: number -} - -const VoiceFullscreen = ({ id, context }: Props) => { - const dispatch = useDispatch(); - const [speakingUid, setSpeakingUid] = useState(0); - const { voicingInfo, exitFullscreen } = useVoice({ id, context }); - const { name, userData, voicingMembers } = useAppSelector(store => { - return { - userData: store.users.byId, - voicingMembers: store.voice.voicingMembers, - name: context == "channel" ? store.channels.byId[id].name : store.users.byId[id].name - }; - }); - useEffect(() => { - const ids = voicingMembers.ids; - ids.forEach((id) => { - playAgoraVideo(id); - const { speakingVolume = 0 } = voicingMembers.byId[id]; - const speaking = speakingVolume > 50; - if (speaking) { - setSpeakingUid(id); - } - }); - - }, [voicingMembers]); - - const handleDoubleClick = (evt: MouseEvent) => { - const uid = evt.currentTarget.dataset.uid; - if (uid) { - dispatch(updatePin({ uid: +uid, action: "pin" })); - } - }; - - const handlePin = (evt: MouseEvent) => { - const uid = evt.currentTarget.dataset.uid ?? ""; - const action = evt.currentTarget.dataset.action ?? "pin"; - if (action == "unpin") { - dispatch(updatePin({ uid: +uid, action })); - } else { - dispatch(updatePin({ uid: +uid, action: "pin" })); - } - }; - if (!voicingInfo) return null; - const pinUid = voicingMembers.pin; - const _name = context == "channel" ? `# ${name}` : `@ ${name}`; - const members = voicingMembers.ids; - const membersData = voicingMembers.byId; - const hasPin = typeof pinUid !== "undefined"; - return ( -
            - {/* top */} -
            - {_name} -
            - -
            -
            - {/* middle */} -
              - {members.map((uid) => { - const curr = userData[uid]; - if (!curr) return null; - const { muted, speakingVolume = 0, shareScreen } = membersData[uid]; - const speaking = speakingVolume > 50; - const special = hasPin ? pinUid == uid : uid == speakingUid; - const disablePin = special && !hasPin; - return
            • -
              - {speaking &&
              } - -
              - {shareScreen ?
              : null} - {!disablePin && - - - } - - {curr?.name} - -
              - {muted ? : } -
              -
              - {/* camera placeholder */} -
              -
            • ; - })} - -
            - {/* bottom */} -
            - -
            -
            - ); + context: ChatContext; + id: number; }; -export default VoiceFullscreen; \ No newline at end of file +const VoiceFullscreen = ({ id, context }: Props) => { + const dispatch = useDispatch(); + const [speakingUid, setSpeakingUid] = useState(0); + const { voicingInfo, exitFullscreen } = useVoice({ id, context }); + const { name, userData, voicingMembers } = useAppSelector((store) => { + return { + userData: store.users.byId, + voicingMembers: store.voice.voicingMembers, + name: context == "channel" ? store.channels.byId[id].name : store.users.byId[id].name + }; + }); + useEffect(() => { + const ids = voicingMembers.ids; + ids.forEach((id) => { + playAgoraVideo(id); + const { speakingVolume = 0 } = voicingMembers.byId[id]; + const speaking = speakingVolume > 50; + if (speaking) { + setSpeakingUid(id); + } + }); + }, [voicingMembers]); + + const handleDoubleClick = (evt: MouseEvent) => { + const uid = evt.currentTarget.dataset.uid; + if (uid) { + dispatch(updatePin({ uid: +uid, action: "pin" })); + } + }; + + const handlePin = (evt: MouseEvent) => { + const uid = evt.currentTarget.dataset.uid ?? ""; + const action = evt.currentTarget.dataset.action ?? "pin"; + if (action == "unpin") { + dispatch(updatePin({ uid: +uid, action })); + } else { + dispatch(updatePin({ uid: +uid, action: "pin" })); + } + }; + if (!voicingInfo) return null; + const pinUid = voicingMembers.pin; + const _name = context == "channel" ? `# ${name}` : `@ ${name}`; + const members = voicingMembers.ids; + const membersData = voicingMembers.byId; + const hasPin = typeof pinUid !== "undefined"; + return ( +
            + {/* top */} +
            + {_name} +
            + +
            +
            + {/* middle */} +
              + {members.map((uid) => { + const curr = userData[uid]; + if (!curr) return null; + const { muted, speakingVolume = 0, shareScreen } = membersData[uid]; + const speaking = speakingVolume > 50; + const special = hasPin ? pinUid == uid : uid == speakingUid; + const disablePin = special && !hasPin; + return ( +
            • +
              + {speaking && ( +
              + )} + +
              + {shareScreen ? ( +
              + ) : null} + {!disablePin && ( + + + + )} + + {curr?.name} + +
              + {muted ? ( + + ) : ( + + )} +
              +
              + {/* camera placeholder */} +
              +
            • + ); + })} +
            + {/* bottom */} +
            + +
            +
            + ); +}; + +export default VoiceFullscreen; diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx index 8df88b1b..5aa121ee 100644 --- a/src/routes/chat/index.tsx +++ b/src/routes/chat/index.tsx @@ -2,19 +2,19 @@ import { memo, useState } from "react"; import { useMatch, useParams } from "react-router-dom"; import clsx from "clsx"; +import { useAppSelector } from "@/app/store"; import BlankPlaceholder from "@/components/BlankPlaceholder"; +import ChannelModal from "@/components/ChannelModal"; +import ErrorCatcher from "@/components/ErrorCatcher"; import Server from "@/components/Server"; +import UsersModal from "@/components/UsersModal"; import ChannelChat from "./ChannelChat"; import DMChat from "./DMChat"; -import UsersModal from "@/components/UsersModal"; -import ChannelModal from "@/components/ChannelModal"; -import SessionList from "./SessionList"; -import { useAppSelector } from "@/app/store"; import GuestBlankPlaceholder from "./GuestBlankPlaceholder"; import GuestChannelChat from "./GuestChannelChat"; import GuestSessionList from "./GuestSessionList"; -import ErrorCatcher from "@/components/ErrorCatcher"; import RTCWidget from "./RTCWidget"; +import SessionList from "./SessionList"; import VoiceFullscreen from "./VoiceFullscreen"; function ChatPage() { @@ -24,12 +24,19 @@ function ChatPage() { const [channelModalVisible, setChannelModalVisible] = useState(false); const [usersModalVisible, setUsersModalVisible] = useState(false); const { channel_id = 0, user_id = 0 } = useParams(); - const { sessionUids, isGuest, aside = "", callingTo } = useAppSelector((store) => { + const { + sessionUids, + isGuest, + aside = "", + callingTo + } = useAppSelector((store) => { return { callingTo: store.voice.callingTo, isGuest: store.authData.guest, sessionUids: store.userMessage.ids, - aside: channel_id ? store.footprint.channelAsides[+channel_id] : store.footprint.dmAsides[store.voice.callingTo] + aside: channel_id + ? store.footprint.channelAsides[+channel_id] + : store.footprint.dmAsides[store.voice.callingTo] }; }); const toggleUsersModalVisible = () => { @@ -39,16 +46,18 @@ function ChatPage() { setChannelModalVisible((prev) => !prev); }; const toggleSessionList = () => { - setSessionListVisible(prev => !prev); + setSessionListVisible((prev) => !prev); }; - const tmpSession = user_id == 0 ? undefined : - sessionUids.findIndex((i) => i == +user_id) == -1 + const tmpSession = + user_id == 0 + ? undefined + : sessionUids.findIndex((i) => i == +user_id) == -1 ? { - mid: 0, - unread: 0, - id: +user_id, - type: "dm" as const - } + mid: 0, + unread: 0, + id: +user_id, + type: "dm" as const + } : undefined; // console.log("temp uid", tmpUid); const placeholderVisible = channel_id == 0 && user_id == 0; @@ -66,24 +75,39 @@ function ChatPage() { )} {usersModalVisible && } -
            - {sessionListVisible &&
            } -
            +
            + {sessionListVisible && ( +
            + )} +
            {isGuest ? : }
            -
            +
            {voiceFullscreenVisible && } {placeholderVisible && (isGuest ? : )} {channelChatVisible && - (isGuest ? ( - - ) : ( - - ))} + (isGuest ? : )} {dmChatVisible && }
            diff --git a/src/routes/chat/utils.tsx b/src/routes/chat/utils.tsx index 6af58f03..4dbbaffb 100644 --- a/src/routes/chat/utils.tsx +++ b/src/routes/chat/utils.tsx @@ -1,16 +1,17 @@ import React from "react"; -import dayjs from "dayjs"; import { useDispatch } from "react-redux"; -import { isImage } from "@/utils"; +import dayjs from "dayjs"; + import { ContentTypes } from "@/app/config"; -import Checkbox from "@/components/styled/Checkbox"; -import Divider from "@/components/Divider"; -import Message from "@/components/Message"; import { updateSelectMessages } from "@/app/slices/ui"; import { useAppSelector } from "@/app/store"; -import LinkifyText from "@/components/LinkifyText"; -import i18n from "../../i18n"; import { ChatContext } from "@/types/common"; +import Divider from "@/components/Divider"; +import LinkifyText from "@/components/LinkifyText"; +import Message from "@/components/Message"; +import Checkbox from "@/components/styled/Checkbox"; +import { isImage } from "@/utils"; +import i18n from "../../i18n"; export function getUnreadCount({ mids = [], @@ -105,12 +106,19 @@ const MessageWrapper = ({ selectMode = false, context, id, mid, divider, childre return (
            {divider} -
            +
            {selectMode && } {children}
            {selectMode && ( -
            +
            )}
            ); @@ -140,7 +148,7 @@ export const renderMessageFragment = ({ const local_id = curr.properties?.local_id; let divider = null; let time = dayjs(created_at).format("YYYY/MM/DD"); - if (!prev && typeof prev !== 'undefined') { + if (!prev && typeof prev !== "undefined") { // 首条信息 divider = time; } else if (prev) { diff --git a/src/routes/favs/index.tsx b/src/routes/favs/index.tsx index 18afb250..cebc1ab4 100644 --- a/src/routes/favs/index.tsx +++ b/src/routes/favs/index.tsx @@ -1,19 +1,19 @@ -import { useState, useEffect, MouseEvent } from "react"; -import dayjs from "dayjs"; +import { MouseEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; +import dayjs from "dayjs"; +import { ContentTypes } from "@/app/config"; +import { Favorite } from "@/app/slices/favorites"; +import { useAppSelector } from "@/app/store"; import FavoredMessage from "@/components/Message/FavoredMessage"; -import IconAudio from "@/assets/icons/file.audio.svg"; -import IconVideo from "@/assets/icons/file.video.svg"; -import IconUnknown from "@/assets/icons/file.unknown.svg"; -import IconImage from "@/assets/icons/file.image.svg"; +import useFavMessage from "@/hooks/useFavMessage"; import IconChannel from "@/assets/icons/channel.svg"; import IconRemove from "@/assets/icons/close.svg"; -import { ContentTypes } from "@/app/config"; -import { useAppSelector } from "@/app/store"; -import { Favorite } from "@/app/slices/favorites"; -import useFavMessage from "@/hooks/useFavMessage"; +import IconAudio from "@/assets/icons/file.audio.svg"; +import IconImage from "@/assets/icons/file.image.svg"; +import IconUnknown from "@/assets/icons/file.unknown.svg"; +import IconVideo from "@/assets/icons/file.video.svg"; type filter = "audio" | "video" | "image" | ""; @@ -127,11 +127,16 @@ function FavsPage() { return (
          • {icon} - {title} + + {title} +
          • ); })} @@ -145,11 +150,22 @@ function FavsPage() { source: { gid, uid } } ] = messages; - const tip = - {gid ? <> {channelData[gid]?.name} : <> - From {userData[uid]?.name} - } - ; + const tip = ( + + {gid ? ( + <> + {channelData[gid]?.name} + + ) : ( + <> + From{" "} + + {userData[uid]?.name} + + + )} + + ); return (

            @@ -158,7 +174,11 @@ function FavsPage() {

            -
            diff --git a/src/routes/guest.tsx b/src/routes/guest.tsx index eb3f232d..263160ec 100644 --- a/src/routes/guest.tsx +++ b/src/routes/guest.tsx @@ -1,5 +1,6 @@ // import { useEffect } from "react"; import { Navigate } from "react-router-dom"; + import { useGuestLoginQuery } from "../app/services/auth"; import { useAppSelector } from "../app/store"; diff --git a/src/routes/home/Menu.tsx b/src/routes/home/Menu.tsx index 2bcf55bc..703f95f6 100644 --- a/src/routes/home/Menu.tsx +++ b/src/routes/home/Menu.tsx @@ -1,7 +1,9 @@ import { FC } from "react"; import { NavLink, useLocation } from "react-router-dom"; + import Tooltip from "@/components/Tooltip"; import IconSetting from "@/assets/icons/setting.svg"; + type Props = {}; const Menu: FC = () => { const { pathname } = useLocation(); diff --git a/src/routes/home/MobileNavs.tsx b/src/routes/home/MobileNavs.tsx index 3182b003..5d938790 100644 --- a/src/routes/home/MobileNavs.tsx +++ b/src/routes/home/MobileNavs.tsx @@ -1,79 +1,95 @@ -import clsx from 'clsx'; -import React from 'react'; -import { NavLink, useLocation, useMatch } from 'react-router-dom'; -import { useAppSelector } from '../../app/store'; +import React from "react"; +import { NavLink, useLocation, useMatch } from "react-router-dom"; +import clsx from "clsx"; import ChatIcon from "@/assets/icons/chat.svg"; -import UserIcon from "@/assets/icons/user.svg"; import SettingIcon from "@/assets/icons/setting.svg"; +import UserIcon from "@/assets/icons/user.svg"; +import { useAppSelector } from "../../app/store"; // type Props = {} const MobileNavs = () => { - const isHomePath = useMatch(`/`); - const { pathname } = useLocation(); - const isChatHomePath = useMatch(`/chat`); - const isDMChat = useMatch(`/chat/dm/:user_id`); - // const isSettingPage = useMatch(`/setting`); - const isChannelChat = useMatch(`/chat/channel/:channel_id`); - const { - ui: { - rememberedNavs: { chat: chatPath, user: userPath } - } - } = useAppSelector((store) => { - return { - ui: store.ui, - loginUid: store.authData.user?.uid, - guest: store.authData.guest - }; - }); + const isHomePath = useMatch(`/`); + const { pathname } = useLocation(); + const isChatHomePath = useMatch(`/chat`); + const isDMChat = useMatch(`/chat/dm/:user_id`); + // const isSettingPage = useMatch(`/setting`); + const isChannelChat = useMatch(`/chat/channel/:channel_id`); + const { + ui: { + rememberedNavs: { chat: chatPath, user: userPath } + } + } = useAppSelector((store) => { + return { + ui: store.ui, + loginUid: store.authData.user?.uid, + guest: store.authData.guest + }; + }); - const linkClass = `flex`; - const isChatPage = isHomePath || pathname.startsWith("/chat"); - const isChattingPage = !!isDMChat || !!isChannelChat; - // console.log("rrr", isDMChat, isChannelChat); + const linkClass = `flex`; + const isChatPage = isHomePath || pathname.startsWith("/chat"); + const isChattingPage = !!isDMChat || !!isChannelChat; + // console.log("rrr", isDMChat, isChannelChat); - // 有点绕 - const chatNav = isChatHomePath ? "/chat" : chatPath || "/chat"; - const userNav = userPath || "/users"; - return ( -
              -
            • - `${linkClass}`} - to={chatNav} - > - {({ isActive }) => { - const active = isActive || isChatPage; - return
              - - Chats -
              ; - }} -
              -
            • -
            • - `${linkClass}`} to={userNav}> - {({ isActive: active }) => { - return
              - - Contacts -
              ; - }} -
              -
            • -
            • - `${linkClass}`} to={'/setting'}> - {({ isActive: active }) => { - return
              - - Settings -
              ; - }} -
              -
            • -
            - ); + // 有点绕 + const chatNav = isChatHomePath ? "/chat" : chatPath || "/chat"; + const userNav = userPath || "/users"; + return ( +
              +
            • + `${linkClass}`} to={chatNav}> + {({ isActive }) => { + const active = isActive || isChatPage; + return ( +
              + + + Chats + +
              + ); + }} +
              +
            • +
            • + `${linkClass}`} to={userNav}> + {({ isActive: active }) => { + return ( +
              + + + Contacts + +
              + ); + }} +
              +
            • +
            • + `${linkClass}`} to={"/setting"}> + {({ isActive: active }) => { + return ( +
              + + + Settings + +
              + ); + }} +
              +
            • +
            + ); }; -export default MobileNavs; \ No newline at end of file +export default MobileNavs; diff --git a/src/routes/home/User.tsx b/src/routes/home/User.tsx index c5caed00..1d6f3fed 100644 --- a/src/routes/home/User.tsx +++ b/src/routes/home/User.tsx @@ -1,7 +1,8 @@ import { FC } from "react"; import { NavLink, useLocation } from "react-router-dom"; -import Avatar from "@/components/Avatar"; + import { useAppSelector } from "@/app/store"; +import Avatar from "@/components/Avatar"; interface Props { uid: number; @@ -16,7 +17,13 @@ const User: FC = ({ uid }) => {
            - +
            diff --git a/src/routes/home/index.tsx b/src/routes/home/index.tsx index 838221fa..91e3abdb 100644 --- a/src/routes/home/index.tsx +++ b/src/routes/home/index.tsx @@ -1,27 +1,26 @@ import { memo, useEffect } from "react"; -import { Outlet, NavLink, useLocation, useMatch } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; +import { NavLink, Outlet, useLocation, useMatch } from "react-router-dom"; -import User from "./User"; -import Loading from "@/components/Loading"; -import Menu from "./Menu"; -import usePreload from "@/hooks/usePreload"; -import Tooltip from "@/components/Tooltip"; -import Notification from "@/components/Notification"; -import Manifest from "@/components/Manifest"; -import ChatIcon from "@/assets/icons/chat.svg"; -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 MobileNavs from "./MobileNavs"; import { updateRememberedNavs } from "@/app/slices/ui"; -import UnreadTabTip from "@/components/UnreadTabTip"; +import { useAppSelector } from "@/app/store"; +import Loading from "@/components/Loading"; +import Manifest from "@/components/Manifest"; +import Notification from "@/components/Notification"; import ReLoginModal from "@/components/ReLoginModal"; -import Voice from "@/components/Voice"; import ServerVersionChecker from "@/components/ServerVersionChecker"; - +import Tooltip from "@/components/Tooltip"; +import UnreadTabTip from "@/components/UnreadTabTip"; +import Voice from "@/components/Voice"; +import usePreload from "@/hooks/usePreload"; +import FavIcon from "@/assets/icons/bookmark.svg"; +import ChatIcon from "@/assets/icons/chat.svg"; +import FolderIcon from "@/assets/icons/folder.svg"; +import UserIcon from "@/assets/icons/user.svg"; +import Menu from "./Menu"; +import MobileNavs from "./MobileNavs"; +import User from "./User"; function HomePage() { const { t } = useTranslation(); @@ -69,45 +68,76 @@ function HomePage() { <> {roleChanged && } {!guest && } - {!guest && - - } + {!guest && ( + + + + )} {!guest && }
            {!guest && ( -
            +
            {loginUid && } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index c6c17390..d211e263 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,10 +1,20 @@ -import { useEffect, lazy } from "react"; -import { Route, Routes, HashRouter } from "react-router-dom"; -import { Provider } from "react-redux"; +import { lazy, useEffect } from "react"; import toast from "react-hot-toast"; +import { Provider } from "react-redux"; +import { HashRouter, Route, Routes } from "react-router-dom"; import { isEqual } from "lodash"; +import Meta from "@/components/Meta"; +import useDeviceToken from "@/components/Notification/useDeviceToken"; +import RequireAuth from "@/components/RequireAuth"; +import RequireNoAuth from "@/components/RequireNoAuth"; +import RequireSingleTab from "@/components/RequireSingleTab"; +import { vapidKey } from "../app/config"; +import store, { useAppSelector } from "../app/store"; import NotFoundPage from "./404"; +import InvitePrivate from "./invitePrivate"; +import LazyIt from "./lazy"; + // import Welcome from './Welcome' // const HomePage = lazy(() => import("./home")); // const ChatPage = lazy(() => import("./chat")); @@ -25,15 +35,7 @@ const ResourceManagement = lazy(() => import("./resources")); const GuestLogin = lazy(() => import("./guest")); const ChatPage = lazy(() => import("./chat")); const HomePage = lazy(() => import("./home")); -import RequireAuth from "@/components/RequireAuth"; -import RequireNoAuth from "@/components/RequireNoAuth"; -import Meta from "@/components/Meta"; -import LazyIt from './lazy'; -import store, { useAppSelector } from "../app/store"; -import useDeviceToken from "@/components/Notification/useDeviceToken"; -import { vapidKey } from "../app/config"; -import RequireSingleTab from "@/components/RequireSingleTab"; -import InvitePrivate from "./invitePrivate"; + let toastId: string; const PageRoutes = () => { const { @@ -57,10 +59,40 @@ const PageRoutes = () => { return ( - } /> - } /> - } /> - } /> + + + + } + /> + + + + + + } + /> + + + + } + /> + + + + } + /> { } > - } /> - } /> + + + + } + /> + + + + } + /> { } /> - } /> + + + + } + /> { } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> { } /> - } /> + + + + } + /> { }; export default function ReduxRoutes() { - return ( diff --git a/src/routes/invitePrivate/index.tsx b/src/routes/invitePrivate/index.tsx index b767ae6a..f632c441 100644 --- a/src/routes/invitePrivate/index.tsx +++ b/src/routes/invitePrivate/index.tsx @@ -1,61 +1,72 @@ -import { useEffect } from 'react'; -import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { useJoinPrivateChannelMutation, useLazyGetChannelQuery } from '../../app/services/channel'; -import StyledButton from '../../components/styled/Button'; -import { useCheckMagicTokenValidMutation } from '../../app/services/auth'; -import { useAppSelector } from '../../app/store'; +import { useEffect } from "react"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; + +import { useCheckMagicTokenValidMutation } from "../../app/services/auth"; +import { useJoinPrivateChannelMutation, useLazyGetChannelQuery } from "../../app/services/channel"; +import { useAppSelector } from "../../app/store"; +import StyledButton from "../../components/styled/Button"; const InvitePrivate = () => { - const { channel_id } = useParams(); - const server = useAppSelector(store => store.server); - const navigateTo = useNavigate(); - const [joinChannel, { isLoading, data, isSuccess }] = useJoinPrivateChannelMutation(); - const [fetchChannelInfo, { data: channel, isSuccess: fetchChannelSuccess }] = useLazyGetChannelQuery(); - const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] = - useCheckMagicTokenValidMutation(); - let [searchParams] = useSearchParams(new URLSearchParams(location.search)); - const magic_token = searchParams.get("magic_token") ?? ""; - useEffect(() => { - if (channel_id) { - fetchChannelInfo(+channel_id); - } - }, [channel_id]); - useEffect(() => { - if (magic_token) { - checkTokenInvalid(magic_token); - } - }, [magic_token]); - useEffect(() => { - if (data && isSuccess) { - // joinChannel(data) - navigateTo(`/chat/channel/${data.gid}`); - } - }, [isSuccess, data]); - const handleJoin = async () => { - const resp = await joinChannel({ magic_token }); - if ("error" in resp) { - if (resp.error.originalStatus === 409) { - // alert("The invite link is invalid or expired"); - } - } - }; - if (!fetchChannelSuccess) return null; - return ( -
            -
            -
            - server logo -

            - {server.name} -

            -
            - - {checkingToken ? "Checking..." : isTokenValid ? <>You are invited to join private channel #{channel?.name} : "The invite link is invalid or expired"} - - Join -
            + const { channel_id } = useParams(); + const server = useAppSelector((store) => store.server); + const navigateTo = useNavigate(); + const [joinChannel, { isLoading, data, isSuccess }] = useJoinPrivateChannelMutation(); + const [fetchChannelInfo, { data: channel, isSuccess: fetchChannelSuccess }] = + useLazyGetChannelQuery(); + const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] = + useCheckMagicTokenValidMutation(); + let [searchParams] = useSearchParams(new URLSearchParams(location.search)); + const magic_token = searchParams.get("magic_token") ?? ""; + useEffect(() => { + if (channel_id) { + fetchChannelInfo(+channel_id); + } + }, [channel_id]); + useEffect(() => { + if (magic_token) { + checkTokenInvalid(magic_token); + } + }, [magic_token]); + useEffect(() => { + if (data && isSuccess) { + // joinChannel(data) + navigateTo(`/chat/channel/${data.gid}`); + } + }, [isSuccess, data]); + const handleJoin = async () => { + const resp = await joinChannel({ magic_token }); + if ("error" in resp) { + if (resp.error.originalStatus === 409) { + // alert("The invite link is invalid or expired"); + } + } + }; + if (!fetchChannelSuccess) return null; + return ( +
            +
            +
            + server logo +

            {server.name}

            - ); + + {checkingToken ? ( + "Checking..." + ) : isTokenValid ? ( + <> + You are invited to join private channel{" "} + #{channel?.name} + + ) : ( + "The invite link is invalid or expired" + )} + + + Join + +
            +
            + ); }; -export default InvitePrivate; \ No newline at end of file +export default InvitePrivate; diff --git a/src/routes/lazy.tsx b/src/routes/lazy.tsx index 95198ead..6bed532d 100644 --- a/src/routes/lazy.tsx +++ b/src/routes/lazy.tsx @@ -1,16 +1,13 @@ -import { ReactNode, Suspense, FC } from "react"; +import { FC, ReactNode, Suspense } from "react"; + import Loading from "@/components/Loading"; type Props = { children?: ReactNode; -} +}; const Lazy: FC = ({ children }) => { - return ( - }> - {children} - - ); + return }>{children}; }; export default Lazy; diff --git a/src/routes/login/MagicLinkLogin.tsx b/src/routes/login/MagicLinkLogin.tsx index eb127027..87cd71ca 100644 --- a/src/routes/login/MagicLinkLogin.tsx +++ b/src/routes/login/MagicLinkLogin.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "react-router-dom"; + import Button from "@/components/styled/Button"; export default function MagicLinkLogin() { @@ -7,5 +8,9 @@ export default function MagicLinkLogin() { navigate("/send_magic_link"); }; - return ; + return ( + + ); } diff --git a/src/routes/login/MetamaskLoginButton.tsx b/src/routes/login/MetamaskLoginButton.tsx index 05ed54ee..a8f835d1 100644 --- a/src/routes/login/MetamaskLoginButton.tsx +++ b/src/routes/login/MetamaskLoginButton.tsx @@ -1,12 +1,14 @@ /* eslint-disable no-undef */ -import { useState, useEffect, useRef } from "react"; -import MetaMaskOnboarding from "@metamask/onboarding"; -import { useLazyGetMetamaskNonceQuery } from "@/app/services/auth"; -import metamaskSvg from "@/assets/icons/metamask.svg?url"; -import { LoginCredential } from "@/types/auth"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import MetaMaskOnboarding from "@metamask/onboarding"; + +import { useLazyGetMetamaskNonceQuery } from "@/app/services/auth"; +import { LoginCredential } from "@/types/auth"; import { AuthType } from "@/types/common"; import Button from "@/components/styled/Button"; +import metamaskSvg from "@/assets/icons/metamask.svg?url"; + // import toast from "react-hot-toast"; export default function MetamaskLoginButton({ @@ -14,7 +16,7 @@ export default function MetamaskLoginButton({ type = "login" }: { login: (params: LoginCredential) => void; - type?: AuthType + type?: AuthType; }) { const { t } = useTranslation("auth"); const [requesting, setRequesting] = useState(false); @@ -93,7 +95,11 @@ export default function MetamaskLoginButton({ } }; return ( - diff --git a/src/routes/login/OidcLoginButton.tsx b/src/routes/login/OidcLoginButton.tsx index eedb0539..0ebc163a 100644 --- a/src/routes/login/OidcLoginButton.tsx +++ b/src/routes/login/OidcLoginButton.tsx @@ -1,16 +1,18 @@ /* eslint-disable no-undef */ import { FC, useState } from "react"; -import StyledModal from "@/components/styled/Modal"; +import { useTranslation } from "react-i18next"; + +import { OIDCConfig } from "@/types/auth"; +import { AuthType } from "@/types/common"; import Modal from "@/components/Modal"; import StyledButton from "@/components/styled/Button"; -import OidcLoginEntry from "./OidcLoginEntry"; -import { OIDCConfig } from "@/types/auth"; -import { useTranslation } from "react-i18next"; -import { AuthType } from "@/types/common"; import Button from "@/components/styled/Button"; +import StyledModal from "@/components/styled/Modal"; +import OidcLoginEntry from "./OidcLoginEntry"; + interface IProps { issuers?: OIDCConfig[]; - type?: AuthType + type?: AuthType; } const OidcLoginButton: FC = ({ issuers, type = "login" }) => { const { t } = useTranslation("auth"); diff --git a/src/routes/login/OidcLoginEntry.tsx b/src/routes/login/OidcLoginEntry.tsx index 50ed474d..38fedfa2 100644 --- a/src/routes/login/OidcLoginEntry.tsx +++ b/src/routes/login/OidcLoginEntry.tsx @@ -1,7 +1,8 @@ -import { useEffect, FC } from "react"; +import { FC, useEffect } from "react"; + import { useGetOpenidMutation } from "@/app/services/auth"; -import Button from "@/components/styled/Button"; import { OIDCConfig } from "@/types/auth"; +import Button from "@/components/styled/Button"; const OidcLoginEntry: FC<{ issuer: OIDCConfig }> = ({ issuer }) => { const [getOpenId, { data, isLoading, isSuccess }] = useGetOpenidMutation(); @@ -21,7 +22,11 @@ const OidcLoginEntry: FC<{ issuer: OIDCConfig }> = ({ issuer }) => { }, [data, isSuccess]); return ( - diff --git a/src/routes/login/SignUpLink.tsx b/src/routes/login/SignUpLink.tsx index 49fa1a1e..75f61b29 100644 --- a/src/routes/login/SignUpLink.tsx +++ b/src/routes/login/SignUpLink.tsx @@ -13,7 +13,9 @@ export default function SignUpLink() { return (
            {t("login.no_account")} - {t("sign_up")} + + {t("sign_up")} +
            ); } diff --git a/src/routes/login/SocialLoginButtons.tsx b/src/routes/login/SocialLoginButtons.tsx index 267a9b21..49736b35 100644 --- a/src/routes/login/SocialLoginButtons.tsx +++ b/src/routes/login/SocialLoginButtons.tsx @@ -1,52 +1,53 @@ -import { useEffect } from 'react'; -import toast from 'react-hot-toast'; +import { useEffect } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + import { useGetLoginConfigQuery } from "@/app/services/server"; +import GithubLoginButton from "@/components/GithubLoginButton"; +import GoogleLoginButton from "@/components/GoogleLoginButton"; import useGithubAuthConfig from "@/hooks/useGithubAuthConfig"; import useGoogleAuthConfig from "@/hooks/useGoogleAuthConfig"; -import GoogleLoginButton from "@/components/GoogleLoginButton"; -import GithubLoginButton from "@/components/GithubLoginButton"; +import { useLoginMutation } from "../../app/services/auth"; +import { AuthType } from "../../types/common"; +import { LoginConfig } from "../../types/server"; import MetamaskLoginButton from "./MetamaskLoginButton"; import OidcLoginButton from "./OidcLoginButton"; -import { useLoginMutation } from '../../app/services/auth'; -import { LoginConfig } from '../../types/server'; -import { AuthType } from '../../types/common'; -import { useTranslation } from 'react-i18next'; type Props = { - type?: AuthType -} - -const SocialLoginButtons = ({ type = "login" }: Props) => { - const { t: ct } = useTranslation(); - const [login, { isSuccess }] = useLoginMutation(); - const { config: githubAuthConfig } = useGithubAuthConfig(); - const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery(); - const { clientId } = useGoogleAuthConfig(); - - useEffect(() => { - if (isSuccess) { - toast.success(ct("tip.login")); - // navigateTo("/"); - } - }, [isSuccess]); - if (!loginConfigSuccess) return null; - const { - github: enableGithubLogin, - google: enableGoogleLogin, - metamask: enableMetamaskLogin, - oidc = [], - } = loginConfig as LoginConfig; - const googleLogin = enableGoogleLogin && !!clientId; - return ( - <> - {googleLogin && } - {enableGithubLogin && ( - - )} - {enableMetamaskLogin && } - {oidc.length > 0 && } - - ); + type?: AuthType; }; -export default SocialLoginButtons; \ No newline at end of file +const SocialLoginButtons = ({ type = "login" }: Props) => { + const { t: ct } = useTranslation(); + const [login, { isSuccess }] = useLoginMutation(); + const { config: githubAuthConfig } = useGithubAuthConfig(); + const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery(); + const { clientId } = useGoogleAuthConfig(); + + useEffect(() => { + if (isSuccess) { + toast.success(ct("tip.login")); + // navigateTo("/"); + } + }, [isSuccess]); + if (!loginConfigSuccess) return null; + const { + github: enableGithubLogin, + google: enableGoogleLogin, + metamask: enableMetamaskLogin, + oidc = [] + } = loginConfig as LoginConfig; + const googleLogin = enableGoogleLogin && !!clientId; + return ( + <> + {googleLogin && } + {enableGithubLogin && ( + + )} + {enableMetamaskLogin && } + {oidc.length > 0 && } + + ); +}; + +export default SocialLoginButtons; diff --git a/src/routes/login/index.tsx b/src/routes/login/index.tsx index 77c0c52f..ea74628b 100644 --- a/src/routes/login/index.tsx +++ b/src/routes/login/index.tsx @@ -1,23 +1,23 @@ /* eslint-disable no-undef */ -import { useState, useEffect, FormEvent, ChangeEvent } from "react"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; -import BASE_URL from "@/app/config"; +import { useTranslation } from "react-i18next"; +import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query"; -import Input from "@/components/styled/Input"; -import Button from "@/components/styled/Button"; -import MagicLinkLogin from "./MagicLinkLogin"; -import SignUpLink from "./SignUpLink"; +import BASE_URL from "@/app/config"; import { useLoginMutation } from "@/app/services/auth"; import { useGetLoginConfigQuery, useGetSMTPStatusQuery } from "@/app/services/server"; -import useGoogleAuthConfig from "@/hooks/useGoogleAuthConfig"; -import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query"; -import { useTranslation } from "react-i18next"; -import SocialLoginButtons from "./SocialLoginButtons"; -import Divider from "@/components/Divider"; import { useAppSelector } from "@/app/store"; +import Divider from "@/components/Divider"; +import Button from "@/components/styled/Button"; +import Input from "@/components/styled/Input"; +import useGoogleAuthConfig from "@/hooks/useGoogleAuthConfig"; +import MagicLinkLogin from "./MagicLinkLogin"; +import SignUpLink from "./SignUpLink"; +import SocialLoginButtons from "./SocialLoginButtons"; export default function LoginPage() { - const serverName = useAppSelector(store => store.server.name); + const serverName = useAppSelector((store) => store.server.name); const { t } = useTranslation("auth"); const { t: ct } = useTranslation(); const { data: enableSMTP, isLoading: loadingSMTPStatus } = useGetSMTPStatusQuery(); @@ -129,8 +129,14 @@ export default function LoginPage() {
            - logo -

            {t("login.title", { name: serverName })}

            + logo +

            + {t("login.title", { name: serverName })} +

            {t("login.desc")}
            @@ -160,7 +166,6 @@ export default function LoginPage() {
            {hasDivider && }
            - {enableMagicLink && } diff --git a/src/routes/oauth/index.tsx b/src/routes/oauth/index.tsx index b8dc3da5..ffe9df6c 100644 --- a/src/routes/oauth/index.tsx +++ b/src/routes/oauth/index.tsx @@ -1,10 +1,11 @@ -import { useState, useEffect } from "react"; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; + import { useLoginMutation } from "@/app/services/auth"; -import toast from "react-hot-toast"; import { setAuthData } from "@/app/slices/auth.data"; -import { useTranslation } from "react-i18next"; export default function OAuthPage() { const { t: ct } = useTranslation(); diff --git a/src/routes/onboarding/index.tsx b/src/routes/onboarding/index.tsx index 5cb778d8..f80c5721 100644 --- a/src/routes/onboarding/index.tsx +++ b/src/routes/onboarding/index.tsx @@ -1,17 +1,16 @@ import React, { useState } from "react"; import { Helmet } from "react-helmet"; -import { useWizard, Wizard } from 'react-use-wizard'; - -import WelcomePage from "./steps/welcome-page"; -import ServerName from "./steps/server-name"; -import AdminAccount from "./steps/admin-account"; -import WhoCanSignUp from "./steps/who-can-sign-up"; -import InviteLink from "./steps/invite-link"; -import DonePage from "./steps/done-page"; -import steps from "./steps"; import { useTranslation } from "react-i18next"; +import { useWizard, Wizard } from "react-use-wizard"; import clsx from "clsx"; +import steps from "./steps"; +import AdminAccount from "./steps/admin-account"; +import DonePage from "./steps/done-page"; +import InviteLink from "./steps/invite-link"; +import ServerName from "./steps/server-name"; +import WelcomePage from "./steps/welcome-page"; +import WhoCanSignUp from "./steps/who-can-sign-up"; const Navigator = () => { const { activeStep, goToStep } = useWizard(); @@ -22,7 +21,12 @@ const Navigator = () => {
            {steps.map((stepToRender, indexToRender) => { const clickable = canJumpTo.includes(stepToRender.name); - const itemClass = clsx(`text-sm text-gray-600`, clickable && "cursor-pointer md:hover:text-gray-500", indexToRender === activeStep && "font-bold text-black", indexToRender >= activeStep && "text-gray-400"); + const itemClass = clsx( + `text-sm text-gray-600`, + clickable && "cursor-pointer md:hover:text-gray-500", + indexToRender === activeStep && "font-bold text-black", + indexToRender >= activeStep && "text-gray-400" + ); const nodeCls = `${itemClass}`; return ( diff --git a/src/routes/onboarding/steps/UpdateFrontendURL.tsx b/src/routes/onboarding/steps/UpdateFrontendURL.tsx index 6e74c0e3..d160563c 100644 --- a/src/routes/onboarding/steps/UpdateFrontendURL.tsx +++ b/src/routes/onboarding/steps/UpdateFrontendURL.tsx @@ -1,53 +1,65 @@ -import { useState, useEffect, ChangeEvent } from 'react'; +import { ChangeEvent, useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import StyledInput from "@/components/styled/Input"; -import StyledButton from "@/components/styled/Button"; -import { useGetFrontendUrlQuery, useUpdateFrontendUrlMutation } from "@/app/services/server"; -import InfoIcon from '@/assets/icons/info.svg'; -import { useTranslation } from 'react-i18next'; +import { useTranslation } from "react-i18next"; +import { useGetFrontendUrlQuery, useUpdateFrontendUrlMutation } from "@/app/services/server"; +import StyledButton from "@/components/styled/Button"; +import StyledInput from "@/components/styled/Input"; +import InfoIcon from "@/assets/icons/info.svg"; type Props = { - refreshInviteLink: () => void -} - -const UpdateFrontendURL = ({ refreshInviteLink }: Props) => { - const { t } = useTranslation("welcome", { keyPrefix: "onboarding" }); - const { t: ct } = useTranslation(); - const [updateUrl, { isSuccess, isLoading }] = useUpdateFrontendUrlMutation(); - const { data, isSuccess: getUrlSuccess } = useGetFrontendUrlQuery(); - const [frontUrl, setFrontUrl] = useState(location.origin); - const handleUpdateUrl = (evt: ChangeEvent) => { - setFrontUrl(evt.target.value); - }; - const updateFrontUrl = () => { - updateUrl(frontUrl); - }; - useEffect(() => { - if (getUrlSuccess && data) { - setFrontUrl(data); - } - }, [getUrlSuccess]); - useEffect(() => { - if (isSuccess) { - refreshInviteLink(); - toast.success(ct("tip.update")); - } - }, [isSuccess]); - return ( -
            - -
            - {t("update_domain_tip")} -
            - - - {ct("action.update")} - -
            -
            -
            - ); + refreshInviteLink: () => void; }; -export default UpdateFrontendURL; \ No newline at end of file +const UpdateFrontendURL = ({ refreshInviteLink }: Props) => { + const { t } = useTranslation("welcome", { keyPrefix: "onboarding" }); + const { t: ct } = useTranslation(); + const [updateUrl, { isSuccess, isLoading }] = useUpdateFrontendUrlMutation(); + const { data, isSuccess: getUrlSuccess } = useGetFrontendUrlQuery(); + const [frontUrl, setFrontUrl] = useState(location.origin); + const handleUpdateUrl = (evt: ChangeEvent) => { + setFrontUrl(evt.target.value); + }; + const updateFrontUrl = () => { + updateUrl(frontUrl); + }; + useEffect(() => { + if (getUrlSuccess && data) { + setFrontUrl(data); + } + }, [getUrlSuccess]); + useEffect(() => { + if (isSuccess) { + refreshInviteLink(); + toast.success(ct("tip.update")); + } + }, [isSuccess]); + return ( +
            + +
            + + {t("update_domain_tip")} + +
            + + + {ct("action.update")} + +
            +
            +
            + ); +}; + +export default UpdateFrontendURL; diff --git a/src/routes/onboarding/steps/admin-account.tsx b/src/routes/onboarding/steps/admin-account.tsx index 53c3065a..552f73b3 100644 --- a/src/routes/onboarding/steps/admin-account.tsx +++ b/src/routes/onboarding/steps/admin-account.tsx @@ -1,18 +1,19 @@ -import { useEffect, useState, FC, useRef } from "react"; +import { FC, useEffect, useRef, useState } from "react"; import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; -import StyledInput from "@/components/styled/Input"; -import StyledButton from "@/components/styled/Button"; +import { useWizard } from "react-use-wizard"; + +import { useLoginMutation } from "@/app/services/auth"; import { useCreateAdminMutation, useGetServerQuery, useUpdateServerMutation } from "@/app/services/server"; -import { useLoginMutation } from "@/app/services/auth"; import { updateInitialized } from "@/app/slices/auth.data"; import { useAppSelector } from "@/app/store"; -import { useTranslation } from "react-i18next"; -import { useWizard } from "react-use-wizard"; +import StyledButton from "@/components/styled/Button"; +import StyledInput from "@/components/styled/Input"; type Props = { serverName: string; @@ -23,8 +24,9 @@ const AdminAccount: FC = ({ serverName }) => { const formRef = useRef(); const loggedIn = useAppSelector((store) => !!store.authData.token); const dispatch = useDispatch(); - const [createAdmin, { isLoading: isSigningUp, isError: signUpError, isSuccess: signUpOk }] = useCreateAdminMutation(); - const [login, { isLoading: isLoggingIn, isError: loginError, }] = useLoginMutation(); + const [createAdmin, { isLoading: isSigningUp, isError: signUpError, isSuccess: signUpOk }] = + useCreateAdminMutation(); + const [login, { isLoading: isLoggingIn, isError: loginError }] = useLoginMutation(); const { data: serverData } = useGetServerQuery(); const [updateServer, { isLoading: isUpdatingServer, isSuccess: isUpdatedServer }] = useUpdateServerMutation(); diff --git a/src/routes/onboarding/steps/done-page.tsx b/src/routes/onboarding/steps/done-page.tsx index 6ad8c3a8..77046674 100644 --- a/src/routes/onboarding/steps/done-page.tsx +++ b/src/routes/onboarding/steps/done-page.tsx @@ -1,8 +1,8 @@ +import { Trans, useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; + import StyledButton from "@/components/styled/Button"; import PlayIcon from "@/assets/icons/play.svg?url"; -import { Trans, useTranslation } from "react-i18next"; - export default function DonePage({ serverName }: { serverName: string }) { const { t } = useTranslation("welcome", { keyPrefix: "onboarding" }); @@ -17,7 +17,10 @@ export default function DonePage({ serverName }: { serverName: string }) { - navigate("/")}> + navigate("/")} + > play icon {t("enter")} diff --git a/src/routes/onboarding/steps/invite-link.tsx b/src/routes/onboarding/steps/invite-link.tsx index 0558e3c0..9edae477 100644 --- a/src/routes/onboarding/steps/invite-link.tsx +++ b/src/routes/onboarding/steps/invite-link.tsx @@ -1,10 +1,10 @@ - import { useTranslation } from "react-i18next"; import { useWizard } from "react-use-wizard"; -import StyledInput from "@/components/styled/Input"; -import StyledButton from "@/components/styled/Button"; -import useInviteLink from "@/hooks/useInviteLink"; + import ServerVersionChecker from "@/components/ServerVersionChecker"; +import StyledButton from "@/components/styled/Button"; +import StyledInput from "@/components/styled/Input"; +import useInviteLink from "@/hooks/useInviteLink"; import UpdateFrontendURL from "./UpdateFrontendURL"; export default function InviteLink() { @@ -19,8 +19,16 @@ export default function InviteLink() { {t("last_tip")} {t("last_desc")}
            - - + + {linkCopied ? "Copied" : ct("action.copy")}
            diff --git a/src/routes/onboarding/steps/server-name.tsx b/src/routes/onboarding/steps/server-name.tsx index 8e0a040c..cdcb40e9 100644 --- a/src/routes/onboarding/steps/server-name.tsx +++ b/src/routes/onboarding/steps/server-name.tsx @@ -1,10 +1,11 @@ import { FC } from "react"; import toast from "react-hot-toast"; -import StyledInput from "@/components/styled/Input"; -import StyledButton from "@/components/styled/Button"; import { useTranslation } from "react-i18next"; import { useWizard } from "react-use-wizard"; +import StyledButton from "@/components/styled/Button"; +import StyledInput from "@/components/styled/Input"; + type Props = { serverName: string; setServerName: (name: string) => void; @@ -16,9 +17,7 @@ const ServerName: FC = ({ serverName, setServerName }) => { return (
            {t("new_server")} - - {t("server_desc")} - + {t("server_desc")} {t("welcome")} - - {t("welcome_desc")} - - + {t("welcome_desc")} + play icon {t("start")} diff --git a/src/routes/onboarding/steps/who-can-sign-up.tsx b/src/routes/onboarding/steps/who-can-sign-up.tsx index 56639b56..1be579ca 100644 --- a/src/routes/onboarding/steps/who-can-sign-up.tsx +++ b/src/routes/onboarding/steps/who-can-sign-up.tsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import toast from "react-hot-toast"; -import StyledRadio from "@/components/styled/Radio"; -import StyledButton from "@/components/styled/Button"; -import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "@/app/services/server"; -import { WhoCanSignUp } from "@/types/server"; import { useTranslation } from "react-i18next"; import { useWizard } from "react-use-wizard"; +import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "@/app/services/server"; +import { WhoCanSignUp } from "@/types/server"; +import StyledButton from "@/components/styled/Button"; +import StyledRadio from "@/components/styled/Radio"; + export default function SignUpSetting() { const { t } = useTranslation("welcome"); const { t: st } = useTranslation("setting"); @@ -39,17 +40,18 @@ export default function SignUpSetting() { if (isSuccess) nextStep(); }, [isSuccess]); - return (
            {t("onboarding.invite_title")} {t("onboarding.invite_desc")} - {value && } + {value && ( + + )}
            Magic link Sent
            -

            Login to your email client, and continue next step

            +

            + Login to your email client, and continue next step +

            You can close this window now.

            ); diff --git a/src/routes/reg/RegWithUsername.tsx b/src/routes/reg/RegWithUsername.tsx index 90295ae9..29240669 100644 --- a/src/routes/reg/RegWithUsername.tsx +++ b/src/routes/reg/RegWithUsername.tsx @@ -1,14 +1,18 @@ -import { useState, useEffect, FC, FormEvent, ChangeEvent } from "react"; -import { useDispatch } from "react-redux"; -import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { ChangeEvent, FC, FormEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { useParams } from "react-router-dom"; + +import { + useCheckMagicTokenValidMutation, + useLoginMutation, + useRegisterMutation +} from "@/app/services/auth"; import { setAuthData } from "@/app/slices/auth.data"; -import Input from "@/components/styled/Input"; import Button from "@/components/styled/Button"; -import { useLoginMutation, useCheckMagicTokenValidMutation } from "@/app/services/auth"; +import Input from "@/components/styled/Input"; import ExpiredTip from "./ExpiredTip"; -import { useRegisterMutation } from "@/app/services/auth"; import { useMagicToken } from "./index"; const RegWithUsername: FC = () => { @@ -100,7 +104,9 @@ const RegWithUsername: FC = () => { return ( <>
            -

            {t("reg.input_name")}

            +

            + {t("reg.input_name")} +

            {t("reg.input_name_tip")} diff --git a/src/routes/reg/Register.tsx b/src/routes/reg/Register.tsx index d370cc5d..fdd231e8 100644 --- a/src/routes/reg/Register.tsx +++ b/src/routes/reg/Register.tsx @@ -1,38 +1,43 @@ -import { useState, useEffect, ChangeEvent, FormEvent } from "react"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import BASE_URL, { KEY_LOCAL_MAGIC_TOKEN } from "@/app/config"; -import Input from "@/components/styled/Input"; -import Button from "@/components/styled/Button"; -import { useLazyCheckEmailQuery, useRegisterMutation, useSendRegMagicLinkMutation } from "@/app/services/auth"; -import EmailNextTip from "./EmailNextStepTip"; -import SignInLink from "./SignInLink"; -import Divider from "@/components/Divider"; - -import { useGetLoginConfigQuery } from "@/app/services/server"; -import SocialLoginButtons from "../login/SocialLoginButtons"; -import { setAuthData } from "@/app/slices/auth.data"; import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router-dom"; + +import BASE_URL, { KEY_LOCAL_MAGIC_TOKEN } from "@/app/config"; +import { + useLazyCheckEmailQuery, + useRegisterMutation, + useSendRegMagicLinkMutation +} from "@/app/services/auth"; +import { useGetLoginConfigQuery } from "@/app/services/server"; +import { setAuthData } from "@/app/slices/auth.data"; import { useAppSelector } from "@/app/store"; +import Divider from "@/components/Divider"; +import Button from "@/components/styled/Button"; +import Input from "@/components/styled/Input"; +import SocialLoginButtons from "../login/SocialLoginButtons"; +import EmailNextTip from "./EmailNextStepTip"; import { useMagicToken } from "./index"; +import SignInLink from "./SignInLink"; interface AuthForm { - name?: string, + name?: string; email: string; password: string; confirmPassword: string; } export default function Register() { - const serverName = useAppSelector(store => store.server.name); + const serverName = useAppSelector((store) => store.server.name); const { t } = useTranslation("auth"); const { t: ct } = useTranslation(); const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] = useSendRegMagicLinkMutation(); const dispatch = useDispatch(); const { token: magic_token } = useMagicToken(); - const [register, { isLoading: registering, data: regData, isSuccess: regSuccess }] = useRegisterMutation(); + const [register, { isLoading: registering, data: regData, isSuccess: regSuccess }] = + useRegisterMutation(); const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery(); const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery(); @@ -113,13 +118,13 @@ export default function Register() { }); }; if (!loginConfigSuccess) return null; - const { - who_can_sign_up: whoCanSignUp - } = loginConfig; + const { who_can_sign_up: whoCanSignUp } = loginConfig; // magic token 没有并且没有开放注册 if (whoCanSignUp !== "EveryOne" && !magic_token) // todo: i18n - return Sign up method is updated to Invitation Link Only; + return ( + Sign up method is updated to Invitation Link Only + ); const { name, email, password, confirmPassword } = input; if (data?.mail_is_sent) return ; const isLoading = registering || signingUp || checkingEmail; @@ -127,23 +132,36 @@ export default function Register() { return ( <>
            - logo -

            {t("reg.title", { name: serverName })}

            + logo +

            + {t("reg.title", { name: serverName })} +

            {t("reg.desc")}
            -
            + {/* 不存在 magic token */} - {!magic_token && } + {!magic_token && ( + + )}
            diff --git a/src/routes/reg/SignInLink.tsx b/src/routes/reg/SignInLink.tsx index f3a76afd..2944054e 100644 --- a/src/routes/reg/SignInLink.tsx +++ b/src/routes/reg/SignInLink.tsx @@ -1,7 +1,8 @@ -import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; -import { NavLink, useSearchParams } from 'react-router-dom'; -import { isMobile } from '../../utils'; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { NavLink, useSearchParams } from "react-router-dom"; + +import { isMobile } from "../../utils"; export default function SignInLink({ token }: { token?: string }) { const { t } = useTranslation("auth"); @@ -10,14 +11,18 @@ export default function SignInLink({ token }: { token?: string }) { useEffect(() => { // 移动端访问,则跳转 if (isMobile() && !!token && ctx == "app") { - location.href = `https://join.voce.chat/download?link=${encodeURIComponent(`${location.origin}?magic_token=${token}`)}`; + location.href = `https://join.voce.chat/download?link=${encodeURIComponent( + `${location.origin}?magic_token=${token}` + )}`; } }, [token, ctx]); return (
            {t("reg.have_account")} - {t("sign_in")} + + {t("sign_in")} +
            ); } diff --git a/src/routes/reg/index.tsx b/src/routes/reg/index.tsx index a2ccd82f..56d2d3ab 100644 --- a/src/routes/reg/index.tsx +++ b/src/routes/reg/index.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { Outlet, useOutletContext, useSearchParams } from "react-router-dom"; + type ContextType = { token: string }; export default function RegContainer() { const [token, setToken] = useState(""); @@ -20,4 +21,4 @@ export default function RegContainer() { export function useMagicToken() { return useOutletContext(); -} \ No newline at end of file +} diff --git a/src/routes/resources/Filter/Channel.tsx b/src/routes/resources/Filter/Channel.tsx index 6fd9a2dc..533902e0 100644 --- a/src/routes/resources/Filter/Channel.tsx +++ b/src/routes/resources/Filter/Channel.tsx @@ -1,8 +1,9 @@ import { FC } from "react"; -import CheckSign from "@/assets/icons/check.sign.svg"; + import ChannelIcon from "@/components/ChannelIcon"; -import Search from "../Search"; import useFilteredChannels from "@/hooks/useFilteredChannels"; +import CheckSign from "@/assets/icons/check.sign.svg"; +import Search from "../Search"; type Props = { select: number; @@ -20,14 +21,23 @@ const Channel: FC = ({ select = 0, updateFilter }) => {
              -
            • +
            • - Any Channel + + Any Channel + {!select && }
            • {channels.map(({ gid, is_public, name }) => { return ( -
            • +
            • {name} {select == gid && } diff --git a/src/routes/resources/Filter/Date.tsx b/src/routes/resources/Filter/Date.tsx index 8415a45c..11d401c8 100644 --- a/src/routes/resources/Filter/Date.tsx +++ b/src/routes/resources/Filter/Date.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; -import CheckSign from "@/assets/icons/check.sign.svg"; +import CheckSign from "@/assets/icons/check.sign.svg"; export const Dates = { today: { @@ -32,15 +32,24 @@ const DateFilter: FC = ({ select = "", updateFilter }) => { return (
                -
              • +
              • Any Time {!select && }
              • {Object.entries(Dates).map(([_key, { title }]) => { return ( -
              • +
              • {title} - {select == _key && } + {select == _key && ( + + )}
              • ); })} diff --git a/src/routes/resources/Filter/From.tsx b/src/routes/resources/Filter/From.tsx index 94fde918..eedf5ba7 100644 --- a/src/routes/resources/Filter/From.tsx +++ b/src/routes/resources/Filter/From.tsx @@ -1,8 +1,9 @@ import { FC } from "react"; -import Search from "../Search"; -import CheckSign from "@/assets/icons/check.sign.svg"; + import User from "@/components/User"; import useFilteredUsers from "@/hooks/useFilteredUsers"; +import CheckSign from "@/assets/icons/check.sign.svg"; +import Search from "../Search"; type Props = { select: number; @@ -20,7 +21,10 @@ const From: FC = ({ select = "", updateFilter }) => {
                -
              • +
              • Anyone {!select && }
              • @@ -28,7 +32,9 @@ const From: FC = ({ select = "", updateFilter }) => { return (
              • - {select == uid && } + {select == uid && ( + + )}
              • ); })} diff --git a/src/routes/resources/Filter/Type.tsx b/src/routes/resources/Filter/Type.tsx index 5441b141..5da3c0a9 100644 --- a/src/routes/resources/Filter/Type.tsx +++ b/src/routes/resources/Filter/Type.tsx @@ -1,13 +1,14 @@ -import IconPdf from "@/assets/icons/file.pdf.svg"; -import IconAudio from "@/assets/icons/file.audio.svg"; -import IconVideo from "@/assets/icons/file.video.svg"; -import IconUnknown from "@/assets/icons/file.unknown.svg"; -import IconDoc from "@/assets/icons/file.doc.svg"; -import IconCode from "@/assets/icons/file.code.svg"; -import IconImage from "@/assets/icons/file.image.svg"; -import CheckSign from "@/assets/icons/check.sign.svg"; import { FC } from "react"; +import CheckSign from "@/assets/icons/check.sign.svg"; +import IconAudio from "@/assets/icons/file.audio.svg"; +import IconCode from "@/assets/icons/file.code.svg"; +import IconDoc from "@/assets/icons/file.doc.svg"; +import IconImage from "@/assets/icons/file.image.svg"; +import IconPdf from "@/assets/icons/file.pdf.svg"; +import IconUnknown from "@/assets/icons/file.unknown.svg"; +import IconVideo from "@/assets/icons/file.video.svg"; + export const FileTypes = { doc: { title: "Documents", @@ -50,15 +51,24 @@ const Type: FC = ({ select = "", updateFilter }) => { return (
                  -
                • +
                • Any Type {!select && }
                • {Object.entries(FileTypes).map(([type, { title, icon }]) => { return ( -
                • +
                • {icon} {title} - {select == type && } + {select == type && ( + + )}
                • ); })} diff --git a/src/routes/resources/Filter/index.tsx b/src/routes/resources/Filter/index.tsx index 96a68443..1ce3af1c 100644 --- a/src/routes/resources/Filter/index.tsx +++ b/src/routes/resources/Filter/index.tsx @@ -1,17 +1,23 @@ import { useState } from "react"; +import { useTranslation } from "react-i18next"; import Tippy from "@tippyjs/react"; import clsx from "clsx"; -import { useTranslation } from "react-i18next"; + +import { useAppSelector } from "@/app/store"; import Avatar from "@/components/Avatar"; +import ArrowDown from "@/assets/icons/arrow.down.svg"; +import FilterChannel from "./Channel"; import FilterDate, { Dates } from "./Date"; import FilterFrom from "./From"; -import FilterChannel from "./Channel"; import FilterType, { FileTypes } from "./Type"; -import ArrowDown from "@/assets/icons/arrow.down.svg"; -import { useAppSelector } from "@/app/store"; const getClass = (selected: boolean) => { - return clsx(`cursor-pointer flex items-center gap-1 md:gap-2 shadow rounded-lg p-1 md:py-2 md:px-3 text-xs text-gray-900 dark:text-gray-200`, selected ? 'text-white bg-primary-400' : 'border border-solid border-gray-300 dark:border-gray-400 '); + return clsx( + `cursor-pointer flex items-center gap-1 md:gap-2 shadow rounded-lg p-1 md:py-2 md:px-3 text-xs text-gray-900 dark:text-gray-200`, + selected + ? "text-white bg-primary-400" + : "border border-solid border-gray-300 dark:border-gray-400 " + ); }; export default function Filter({ filter, updateFilter }) { const { t } = useTranslation("file"); @@ -48,7 +54,6 @@ export default function Filter({ filter, updateFilter }) { from: fromVisible } = filtersVisible; - return (
                  } > -
                  +
                  {from && ( } > -
                  +
                  {type ? FileTypes[type].title : t("type")}
                  @@ -114,10 +113,7 @@ export default function Filter({ filter, updateFilter }) { placement="bottom-start" content={} > -
                  +
                  {date ? Dates[date].title : t("date")}
                  diff --git a/src/routes/resources/Search.tsx b/src/routes/resources/Search.tsx index 08ebe610..361e7156 100644 --- a/src/routes/resources/Search.tsx +++ b/src/routes/resources/Search.tsx @@ -1,7 +1,9 @@ -import clsx from "clsx"; import { ChangeEvent, FC } from "react"; import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + import IconSearch from "@/assets/icons/search.svg"; + interface Props { value?: string; updateSearchValue?: (value: string) => void; @@ -17,9 +19,19 @@ const Search: FC = ({ value = "", updateSearchValue = null, embed = false }; return ( -
                  +
                  - +
                  ); }; diff --git a/src/routes/resources/View.tsx b/src/routes/resources/View.tsx index aa8b4592..5296bb51 100644 --- a/src/routes/resources/View.tsx +++ b/src/routes/resources/View.tsx @@ -1,14 +1,19 @@ import { MouseEvent } from "react"; import { useDispatch } from "react-redux"; -import { updateFileListView } from "@/app/slices/ui"; -import IconList from "@/assets/icons/file.list.svg"; -import IconGrid from "@/assets/icons/file.grid.svg"; import clsx from "clsx"; -const getClass = (selected: boolean) => clsx(`cursor-pointer p-2 box-border flex-center`, selected && `border border-solid border-primary-400 shadow rounded-lg`); +import { updateFileListView } from "@/app/slices/ui"; +import IconGrid from "@/assets/icons/file.grid.svg"; +import IconList from "@/assets/icons/file.list.svg"; + +const getClass = (selected: boolean) => + clsx( + `cursor-pointer p-2 box-border flex-center`, + selected && `border border-solid border-primary-400 shadow rounded-lg` + ); type Props = { - view?: "item" | "grid" -} + view?: "item" | "grid"; +}; export default function View({ view = "item" }: Props) { const dispatch = useDispatch(); const handleChangeView = (evt: MouseEvent) => { @@ -18,7 +23,9 @@ export default function View({ view = "item" }: Props) { }; const isGrid = view == "grid"; return ( -
                    +
                    • diff --git a/src/routes/resources/index.tsx b/src/routes/resources/index.tsx index 082b73bc..259a8b2b 100644 --- a/src/routes/resources/index.tsx +++ b/src/routes/resources/index.tsx @@ -1,12 +1,14 @@ // @ts-nocheck -import { useState, useEffect, useRef, memo } from "react"; -import Masonry from "masonry-layout"; -import View from "./View"; -import Search from "./Search"; -import Filter from "./Filter"; -import FileBox from "@/components/FileBox"; -import { useAppSelector } from "@/app/store"; +import { memo, useEffect, useRef, useState } from "react"; import clsx from "clsx"; +import Masonry from "masonry-layout"; + +import { useAppSelector } from "@/app/store"; +import FileBox from "@/components/FileBox"; +import Filter from "./Filter"; +import Search from "./Search"; +import View from "./View"; + const checkFilter = (data, filter, channelMessage) => { let selected = true; const { mid, file_type, created_at, from_uid, properties } = data; @@ -89,10 +91,14 @@ function ResourceManagement({ fileMessages }) {
                  -
                  +
                  {fileMessages.map((id) => { const data = message[id]; if (!data) return null; diff --git a/src/routes/sendMagicLink/SentTip.tsx b/src/routes/sendMagicLink/SentTip.tsx index ffa1d1c0..20425d78 100644 --- a/src/routes/sendMagicLink/SentTip.tsx +++ b/src/routes/sendMagicLink/SentTip.tsx @@ -1,7 +1,8 @@ -import Button from "@/components/styled/Button"; import { FC, MouseEvent } from "react"; import { useTranslation } from "react-i18next"; +import Button from "@/components/styled/Button"; + interface Props { email: string; reset?: (e: MouseEvent) => void; @@ -11,10 +12,10 @@ const SentTip: FC = ({ email, reset }) => { const { t } = useTranslation("auth"); return (
                  -

                  {t("check_email")}

                  - - {t("check_email_desc", { email })} - +

                  + {t("check_email")} +

                  + {t("check_email_desc", { email })} diff --git a/src/routes/sendMagicLink/index.tsx b/src/routes/sendMagicLink/index.tsx index 05e8fedb..2eacc3ec 100644 --- a/src/routes/sendMagicLink/index.tsx +++ b/src/routes/sendMagicLink/index.tsx @@ -1,20 +1,21 @@ -import { useState, useEffect, ChangeEvent, FormEvent } from "react"; -import { useNavigate } from "react-router-dom"; +import { ChangeEvent, FormEvent, useEffect, useState } from "react"; import toast from "react-hot-toast"; -import BASE_URL from "@/app/config"; -import Input from "@/components/styled/Input"; -import Button from "@/components/styled/Button"; -import { useSendLoginMagicLinkMutation } from "@/app/services/auth"; -import SentTip from "./SentTip"; import { useTranslation } from "react-i18next"; -import SocialLoginButtons from "../login/SocialLoginButtons"; -import Divider from "@/components/Divider"; -import SignInLink from "../reg/SignInLink"; +import { useNavigate } from "react-router-dom"; + +import BASE_URL from "@/app/config"; +import { useSendLoginMagicLinkMutation } from "@/app/services/auth"; import { useAppSelector } from "@/app/store"; +import Divider from "@/components/Divider"; +import Button from "@/components/styled/Button"; +import Input from "@/components/styled/Input"; +import SocialLoginButtons from "../login/SocialLoginButtons"; +import SignInLink from "../reg/SignInLink"; +import SentTip from "./SentTip"; export default function SendMagicLinkPage() { const { t } = useTranslation("auth"); - const serverName = useAppSelector(store => store.server.name); + const serverName = useAppSelector((store) => store.server.name); const [sent, setSent] = useState(false); const [sendMagicLink, { isSuccess, isLoading, error }] = useSendLoginMagicLinkMutation(); const navigateTo = useNavigate(); @@ -73,8 +74,14 @@ export default function SendMagicLinkPage() { ) : ( <>
                  - logo -

                  {t("enter")} {serverName}

                  + logo +

                  + {t("enter")} {serverName}{" "} +

                  {t("placeholder_email")}
                  @@ -94,7 +101,9 @@ export default function SendMagicLinkPage() {
                  - +
                  diff --git a/src/routes/setting/APIConfig.tsx b/src/routes/setting/APIConfig.tsx index bd389cfc..a5e56c91 100644 --- a/src/routes/setting/APIConfig.tsx +++ b/src/routes/setting/APIConfig.tsx @@ -1,18 +1,18 @@ import { useEffect } from "react"; -import Tippy from "@tippyjs/react"; import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import Tippy from "@tippyjs/react"; import { hideAll } from "tippy.js"; -import Input from "@/components/styled/Input"; -import Toggle from "@/components/styled/Toggle"; -import Button from "@/components/styled/Button"; + import { useGetThirdPartySecretQuery, useUpdateThirdPartySecretMutation } from "@/app/services/server"; -import useConfig from "@/hooks/useConfig"; import { LoginConfig } from "@/types/server"; -import { useTranslation } from "react-i18next"; - +import Button from "@/components/styled/Button"; +import Input from "@/components/styled/Input"; +import Toggle from "@/components/styled/Toggle"; +import useConfig from "@/hooks/useConfig"; export default function APIConfig() { const { t } = useTranslation("setting"); @@ -40,7 +40,10 @@ export default function APIConfig() { checked={thirdParty} />
                  - +
                  -
                  - {t("third_app.update_tip")} -
                  +
                  {t("third_app.update_tip")}
                  diff --git a/src/routes/setting/APIDocument.tsx b/src/routes/setting/APIDocument.tsx index 65080e1e..a9d677a1 100644 --- a/src/routes/setting/APIDocument.tsx +++ b/src/routes/setting/APIDocument.tsx @@ -1,69 +1,86 @@ // import React from 'react'; -import { useTranslation } from 'react-i18next'; -import { useAppSelector } from '../../app/store'; -import IconCopy from '@/assets/icons/copy.svg'; -import useCopy from '../../hooks/useCopy'; +import { useTranslation } from "react-i18next"; + +import IconCopy from "@/assets/icons/copy.svg"; +import { useAppSelector } from "../../app/store"; +import useCopy from "../../hooks/useCopy"; + // type Props = {} const APIUrl = `${location.origin}/api/swagger`; const APIDocument = () => { - const token = useAppSelector(store => store.authData.token); - const { copy } = useCopy(); - const { t } = useTranslation("setting"); - const handleCopy = () => { - copy(token); - }; - return ( -
                  -
                  - {t("api_doc.desc")} + const token = useAppSelector((store) => store.authData.token); + const { copy } = useCopy(); + const { t } = useTranslation("setting"); + const handleCopy = () => { + copy(token); + }; + return ( +
                  +
                  {t("api_doc.desc")}
                  +
                  +

                  + {t("api_doc.access")} + + {APIUrl} + +

                  +
                  +
                  +

                  + {t("api_doc.use_method")} +

                  +
                  +
                  +

                  {t("api_doc.step_1")}

                  + {/*
                  */} + step 1 + {/*
                  */} +
                  +
                  +

                  + {t("api_doc.step_2")}{" "} + + ({t("api_doc.step_2_desc")}) + +

                  +
                  +

                  + {token} + +

                  -
                  -

                  - {t("api_doc.access")} - - {APIUrl} - -

                  -
                  -
                  -

                  - {t("api_doc.use_method")} -

                  -
                  -
                  -

                  - {t("api_doc.step_1")} -

                  - {/*
                  */} - step 1 - {/*
                  */} -
                  -
                  -

                  - {t("api_doc.step_2")} - ({t("api_doc.step_2_desc")}) - -

                  -
                  -

                  - {token} - -

                  -
                  - step 2 -
                  -
                  -

                  - {t("api_doc.last")} -

                  - step 3 -
                  -
                  - -
                  - -
                  - ); + step 2 +
                  +
                  +

                  + {t("api_doc.last")} +

                  + step 3 +
                  +
                  +
                  + + ); }; -export default APIDocument; \ No newline at end of file +export default APIDocument; diff --git a/src/routes/setting/BotConfig/BotAPIKeys.tsx b/src/routes/setting/BotConfig/BotAPIKeys.tsx index 86a476cb..7f045958 100644 --- a/src/routes/setting/BotConfig/BotAPIKeys.tsx +++ b/src/routes/setting/BotConfig/BotAPIKeys.tsx @@ -1,82 +1,111 @@ // import Tippy from '@tippyjs/react'; -import dayjs from 'dayjs'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useGetBotAPIKeysQuery } from '../../../app/services/user'; -import IconDelete from '@/assets/icons/delete.svg'; -import IconAdd from '@/assets/icons/add.svg'; -import CreateAPIKeyModal from './CreateAPIKeyModal'; -import DeleteAPIKeyModal from './DeleteAPIKeyModal'; -import clsx from 'clsx'; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; +import dayjs from "dayjs"; + +import IconAdd from "@/assets/icons/add.svg"; +import IconDelete from "@/assets/icons/delete.svg"; +import { useGetBotAPIKeysQuery } from "../../../app/services/user"; +import CreateAPIKeyModal from "./CreateAPIKeyModal"; +import DeleteAPIKeyModal from "./DeleteAPIKeyModal"; type Props = { - uid: number -} -type DeleteAPIKeyProps = { uid: number, kid: number } + uid: number; +}; +type DeleteAPIKeyProps = { uid: number; kid: number }; const tdClass = "p-1 whitespace-nowrap text-xs text-gray-500 dark:text-gray-200 align-middle px-1"; const BotAPIKeys = ({ uid }: Props) => { - const { t } = useTranslation("setting", { keyPrefix: "bot" }); - const [currentUid, setCurrentUid] = useState(); - const [deleteApiKey, setDeleteApiKey] = useState(); - const { data, refetch } = useGetBotAPIKeysQuery(uid); - const toggleCreateModal = (param?: number) => { - if (!param) refetch(); - setCurrentUid(param); - }; - const toggleDeleteModal = (param?: DeleteAPIKeyProps) => { - if (!param) refetch(); - setDeleteApiKey(param); - }; - if (!data) return null; - const colWidths = ["w-20", "w-[166px]", "w-36", "w-15", "w-10"]; - return ( -
                  -
                  - - - - {[t("col_key_name"), t('col_key_value'), t('col_key_create_time'), t('col_key_last_used'), ""].map((title, idx) => )} - - - - {data.length > 0 ? data.map(ak => { - const { id, name, key, created_at, last_used } = ak; - return - - - - - - ; - }) : - - } - -
                  - {title} -
                  - {name} - - {`${key.slice(0, 4)} ... ... ${key.slice(-6)}`} - - {dayjs(created_at).format("YYYY-MM-DD HH:mm:ss")} - - {last_used ? dayjs(last_used).format("YYYY-MM-DD HH:mm:ss") : "Unused"} - - -
                  - {t("no_api_key")} -
                  - -
                  - {currentUid && } - {deleteApiKey && } -
                  - ); + const { t } = useTranslation("setting", { keyPrefix: "bot" }); + const [currentUid, setCurrentUid] = useState(); + const [deleteApiKey, setDeleteApiKey] = useState(); + const { data, refetch } = useGetBotAPIKeysQuery(uid); + const toggleCreateModal = (param?: number) => { + if (!param) refetch(); + setCurrentUid(param); + }; + const toggleDeleteModal = (param?: DeleteAPIKeyProps) => { + if (!param) refetch(); + setDeleteApiKey(param); + }; + if (!data) return null; + const colWidths = ["w-20", "w-[166px]", "w-36", "w-15", "w-10"]; + return ( +
                  +
                  + + + + {[ + t("col_key_name"), + t("col_key_value"), + t("col_key_create_time"), + t("col_key_last_used"), + "" + ].map((title, idx) => ( + + ))} + + + + {data.length > 0 ? ( + data.map((ak) => { + const { id, name, key, created_at, last_used } = ak; + return ( + + + + + + + + ); + }) + ) : ( + + + + )} + +
                  + {title} +
                  {name} + {`${key.slice(0, 4)} ... ... ${key.slice(-6)}`} + {dayjs(created_at).format("YYYY-MM-DD HH:mm:ss")} + {last_used ? dayjs(last_used).format("YYYY-MM-DD HH:mm:ss") : "Unused"} + + +
                  + {t("no_api_key")} +
                  + +
                  + {currentUid && ( + + )} + {deleteApiKey && ( + + )} +
                  + ); }; -export default BotAPIKeys; \ No newline at end of file +export default BotAPIKeys; diff --git a/src/routes/setting/BotConfig/CreateAPIKeyModal.tsx b/src/routes/setting/BotConfig/CreateAPIKeyModal.tsx index 204a58e9..8fbcb459 100644 --- a/src/routes/setting/BotConfig/CreateAPIKeyModal.tsx +++ b/src/routes/setting/BotConfig/CreateAPIKeyModal.tsx @@ -1,88 +1,96 @@ -import { useRef, useEffect } from 'react'; -import { toast } from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import { useCreateBotAPIKeyMutation } from '../../../app/services/user'; -import Modal from '../../../components/Modal'; -import Button from '../../../components/styled/Button'; -import Input from '../../../components/styled/Input'; -import StyledModal from '../../../components/styled/Modal'; -import useCopy from '../../../hooks/useCopy'; +import { useEffect, useRef } from "react"; +import { toast } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + +import { useCreateBotAPIKeyMutation } from "../../../app/services/user"; +import Modal from "../../../components/Modal"; +import Button from "../../../components/styled/Button"; +import Input from "../../../components/styled/Input"; +import StyledModal from "../../../components/styled/Modal"; +import useCopy from "../../../hooks/useCopy"; type Props = { - uid: number, - closeModal: () => void -} + uid: number; + closeModal: () => void; +}; const CreateAPIKeyModal = ({ closeModal, uid }: Props) => { - const { copy } = useCopy(); - const [createBotAPIKey, { error, isSuccess, isLoading, data = "" }] = useCreateBotAPIKeyMutation(); - const formRef = useRef(null); - const { t } = useTranslation("setting", { keyPrefix: "bot" }); - const { t: ct } = useTranslation(); - // const [input, setInput] = useState(""); - const handleCreateBot = () => { - if (!formRef || !formRef.current) return; - const formEle = formRef.current as HTMLFormElement; - if (!formEle.checkValidity()) { - formEle.reportValidity(); - return; - } - createBotAPIKey({ - uid, - name: formEle.querySelector('input')?.value || "" - }); - }; - useEffect(() => { - if (error) { - switch (error.status) { - case 406: - toast.error("Invalid Webhook URL!"); - break; - case 409: - toast.error("Name Already Exists!"); - break; - default: - break; - } - } - }, [error]); - const handleCopy = () => { - copy(data); - toast.success("API Key Copied!"); - closeModal(); - }; + const { copy } = useCopy(); + const [createBotAPIKey, { error, isSuccess, isLoading, data = "" }] = + useCreateBotAPIKeyMutation(); + const formRef = useRef(null); + const { t } = useTranslation("setting", { keyPrefix: "bot" }); + const { t: ct } = useTranslation(); + // const [input, setInput] = useState(""); + const handleCreateBot = () => { + if (!formRef || !formRef.current) return; + const formEle = formRef.current as HTMLFormElement; + if (!formEle.checkValidity()) { + formEle.reportValidity(); + return; + } + createBotAPIKey({ + uid, + name: formEle.querySelector("input")?.value || "" + }); + }; + useEffect(() => { + if (error) { + switch (error.status) { + case 406: + toast.error("Invalid Webhook URL!"); + break; + case 409: + toast.error("Name Already Exists!"); + break; + default: + break; + } + } + }, [error]); + const handleCopy = () => { + copy(data); + toast.success("API Key Copied!"); + closeModal(); + }; - return ( - - - {t("key_copy_and_close")} - : <> - - - - } - > - {isSuccess ?
                  -
                  - {data} -
                  - ⚠️ {t("create_key_warning")} -
                  -
                  - :
                  -
                  - - -
                  -
                  } -
                  -
                  - ); + return ( + + {t("key_copy_and_close")} + ) : ( + <> + + + + ) + } + > + {isSuccess ? ( +
                  +
                  + {data} +
                  {" "} +
                  ⚠️ {t("create_key_warning")}
                  +
                  + ) : ( +
                  +
                  + + +
                  +
                  + )} +
                  +
                  + ); }; -export default CreateAPIKeyModal; \ No newline at end of file +export default CreateAPIKeyModal; diff --git a/src/routes/setting/BotConfig/CreateModal.tsx b/src/routes/setting/BotConfig/CreateModal.tsx index 0598624f..30d9b0c4 100644 --- a/src/routes/setting/BotConfig/CreateModal.tsx +++ b/src/routes/setting/BotConfig/CreateModal.tsx @@ -1,97 +1,101 @@ -import { useRef, useState, useEffect } from 'react'; -import { toast } from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import { useCreateUserMutation } from '../../../app/services/user'; -import Modal from '../../../components/Modal'; -import Button from '../../../components/styled/Button'; -import Input from '../../../components/styled/Input'; -import StyledModal from '../../../components/styled/Modal'; -import { BASE_ORIGIN } from '../../../app/config'; +import { useEffect, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + +import { BASE_ORIGIN } from "../../../app/config"; +import { useCreateUserMutation } from "../../../app/services/user"; +import Modal from "../../../components/Modal"; +import Button from "../../../components/styled/Button"; +import Input from "../../../components/styled/Input"; +import StyledModal from "../../../components/styled/Modal"; type Props = { - closeModal: () => void -} + closeModal: () => void; +}; type CreateDTO = { - name: string, - webhook_url?: string -} + name: string; + webhook_url?: string; +}; const CreateModal = ({ closeModal }: Props) => { - const [createUser, { isSuccess, isLoading, error }] = useCreateUserMutation(); - const formRef = useRef(null); - const { t } = useTranslation("setting", { keyPrefix: "bot" }); - const { t: ct } = useTranslation(); - // const [input, setInput] = useState(""); - const handleCreateBot = () => { - if (!formRef || !formRef.current) return; - const formEle = formRef.current as HTMLFormElement; - if (!formEle.checkValidity()) { - formEle.reportValidity(); - return; + const [createUser, { isSuccess, isLoading, error }] = useCreateUserMutation(); + const formRef = useRef(null); + const { t } = useTranslation("setting", { keyPrefix: "bot" }); + const { t: ct } = useTranslation(); + // const [input, setInput] = useState(""); + const handleCreateBot = () => { + if (!formRef || !formRef.current) return; + const formEle = formRef.current as HTMLFormElement; + if (!formEle.checkValidity()) { + formEle.reportValidity(); + return; + } + const myFormData = new FormData(formEle); + const formDataObj: CreateDTO = { name: "" }; + myFormData.forEach((value, key) => { + if (value) { + formDataObj[key] = value; + } + }); + const hostname = new URL(BASE_ORIGIN).hostname; + createUser({ + is_bot: true, + is_admin: false, + gender: 1, + email: `bot_${new Date().getTime()}@${hostname}`, + password: "", + ...formDataObj + }); + }; + useEffect(() => { + if (error) { + switch (error.status) { + case 406: + toast.error("Invalid Webhook URL!"); + break; - } - const myFormData = new FormData(formEle); - const formDataObj: CreateDTO = { name: "" }; - myFormData.forEach((value, key) => { - if (value) { - formDataObj[key] = value; - } - }); - const hostname = new URL(BASE_ORIGIN).hostname; - createUser({ - is_bot: true, - is_admin: false, - gender: 1, - email: `bot_${new Date().getTime()}@${hostname}`, - password: "", - ...formDataObj - }); - }; - useEffect(() => { - if (error) { - switch (error.status) { - case 406: - toast.error("Invalid Webhook URL!"); - break; + default: + break; + } + } + }, [error]); + useEffect(() => { + if (isSuccess) { + toast.success("Create Bot Successfully!"); + closeModal(); + } + }, [isSuccess]); - default: - break; - } + return ( + + + + + } - }, [error]); - useEffect(() => { - if (isSuccess) { - toast.success("Create Bot Successfully!"); - closeModal(); - } - }, [isSuccess]); - - return ( - - - - - - } - > -
                  -
                  - - -
                  -
                  - - -
                  -
                  -
                  -
                  - ); + > +
                  +
                  + + +
                  +
                  + + +
                  +
                  +
                  +
                  + ); }; -export default CreateModal; \ No newline at end of file +export default CreateModal; diff --git a/src/routes/setting/BotConfig/DeleteAPIKeyModal.tsx b/src/routes/setting/BotConfig/DeleteAPIKeyModal.tsx index e40844ac..a2d02e51 100644 --- a/src/routes/setting/BotConfig/DeleteAPIKeyModal.tsx +++ b/src/routes/setting/BotConfig/DeleteAPIKeyModal.tsx @@ -1,48 +1,50 @@ -import { useEffect } from 'react'; -import { toast } from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import { useLazyDeleteBotAPIKeyQuery } from '../../../app/services/user'; -import Modal from '../../../components/Modal'; -import Button from '../../../components/styled/Button'; -import StyledModal from '../../../components/styled/Modal'; +import { useEffect } from "react"; +import { toast } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + +import { useLazyDeleteBotAPIKeyQuery } from "../../../app/services/user"; +import Modal from "../../../components/Modal"; +import Button from "../../../components/styled/Button"; +import StyledModal from "../../../components/styled/Modal"; type Props = { - uid: number, - kid: number, - closeModal: () => void -} + uid: number; + kid: number; + closeModal: () => void; +}; const DeleteAPIKeyModal = ({ closeModal, uid, kid }: Props) => { - const [deleteKey, { isSuccess, isLoading }] = useLazyDeleteBotAPIKeyQuery(); - const { t } = useTranslation("setting", { keyPrefix: "bot" }); - const { t: ct } = useTranslation(); - // const [input, setInput] = useState(""); - const handleDeleteBot = () => { - deleteKey({ uid, kid }); - }; - useEffect(() => { - if (isSuccess) { - toast.success(ct("tip.delete")); - closeModal(); - } - }, [isSuccess]); + const [deleteKey, { isSuccess, isLoading }] = useLazyDeleteBotAPIKeyQuery(); + const { t } = useTranslation("setting", { keyPrefix: "bot" }); + const { t: ct } = useTranslation(); + // const [input, setInput] = useState(""); + const handleDeleteBot = () => { + deleteKey({ uid, kid }); + }; + useEffect(() => { + if (isSuccess) { + toast.success(ct("tip.delete")); + closeModal(); + } + }, [isSuccess]); - return ( - - - - - - } - > - - - ); + return ( + + + + + + } + > + + ); }; -export default DeleteAPIKeyModal; \ No newline at end of file +export default DeleteAPIKeyModal; diff --git a/src/routes/setting/BotConfig/DeleteModal.tsx b/src/routes/setting/BotConfig/DeleteModal.tsx index 5447724c..7937b10f 100644 --- a/src/routes/setting/BotConfig/DeleteModal.tsx +++ b/src/routes/setting/BotConfig/DeleteModal.tsx @@ -1,48 +1,50 @@ -import { useEffect } from 'react'; -import { toast } from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import { useLazyDeleteUserQuery } from '../../../app/services/user'; -import Modal from '../../../components/Modal'; -import Button from '../../../components/styled/Button'; -import StyledModal from '../../../components/styled/Modal'; +import { useEffect } from "react"; +import { toast } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + +import { useLazyDeleteUserQuery } from "../../../app/services/user"; +import Modal from "../../../components/Modal"; +import Button from "../../../components/styled/Button"; +import StyledModal from "../../../components/styled/Modal"; type Props = { - uid: number, - name: string, - closeModal: () => void -} + uid: number; + name: string; + closeModal: () => void; +}; const DeleteModal = ({ closeModal, uid, name }: Props) => { - const [deleteUser, { isSuccess, isLoading }] = useLazyDeleteUserQuery(); - const { t } = useTranslation("setting", { keyPrefix: "bot" }); - const { t: ct } = useTranslation(); - // const [input, setInput] = useState(""); - const handleDeleteBot = () => { - deleteUser(uid); - }; - useEffect(() => { - if (isSuccess) { - toast.success(ct("tip.delete")); - closeModal(); - } - }, [isSuccess]); + const [deleteUser, { isSuccess, isLoading }] = useLazyDeleteUserQuery(); + const { t } = useTranslation("setting", { keyPrefix: "bot" }); + const { t: ct } = useTranslation(); + // const [input, setInput] = useState(""); + const handleDeleteBot = () => { + deleteUser(uid); + }; + useEffect(() => { + if (isSuccess) { + toast.success(ct("tip.delete")); + closeModal(); + } + }, [isSuccess]); - return ( - - - - - - } - > - - - ); + return ( + + + + + + } + > + + ); }; -export default DeleteModal; \ No newline at end of file +export default DeleteModal; diff --git a/src/routes/setting/BotConfig/TestAPIKeyModal.tsx b/src/routes/setting/BotConfig/TestAPIKeyModal.tsx index 27d21f07..cc4fdc81 100644 --- a/src/routes/setting/BotConfig/TestAPIKeyModal.tsx +++ b/src/routes/setting/BotConfig/TestAPIKeyModal.tsx @@ -1,86 +1,118 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from "react"; // import { toast } from 'react-hot-toast'; -import { useTranslation } from 'react-i18next'; -import Modal from '../../../components/Modal'; -import Button from '../../../components/styled/Button'; -import Textarea from '../../../components/styled/Textarea'; -import StyledModal from '../../../components/styled/Modal'; -import { useLazyGetBotRelatedChannelsQuery, useSendMessageByBotMutation } from '../../../app/services/server'; -import clsx from 'clsx'; -import { MessageTypes } from '../../../app/config'; +import { useTranslation } from "react-i18next"; +import clsx from "clsx"; + +import { MessageTypes } from "../../../app/config"; +import { + useLazyGetBotRelatedChannelsQuery, + useSendMessageByBotMutation +} from "../../../app/services/server"; +import Modal from "../../../components/Modal"; +import Button from "../../../components/styled/Button"; +import StyledModal from "../../../components/styled/Modal"; +import Textarea from "../../../components/styled/Textarea"; type Props = { - closeModal: () => void -} + closeModal: () => void; +}; const TestAPIKeyModal = ({ closeModal }: Props) => { - const [currCid, setCurrCid] = useState(null); - const [msgType, setMsgType] = useState("text"); - const [getChannels, { data }] = useLazyGetBotRelatedChannelsQuery(); - const [sendMessage] = useSendMessageByBotMutation(); - const inputRef = useRef(); - const msgInputRef = useRef(); - const [key, setKey] = useState(""); - // const { t } = useTranslation("setting", { keyPrefix: "bot" }); - const { t: ct } = useTranslation(); - const handleSetKey = () => { - const input = inputRef?.current; - if (input && input.value) { - setKey(input.value); - } - }; - const handleSetChannel = (cid: number) => { - setCurrCid(cid); - }; - const handleSetMsgType = (type: string) => { - setMsgType(type); - }; - const handleSend = () => { - const input = msgInputRef?.current; - if (input && input.value && currCid) { - sendMessage({ cid: currCid, api_key: key, type: msgType, content: input.value }); - } - }; - useEffect(() => { - if (key) { - getChannels({ api_key: key }); - } - }, [key]); + const [currCid, setCurrCid] = useState(null); + const [msgType, setMsgType] = useState("text"); + const [getChannels, { data }] = useLazyGetBotRelatedChannelsQuery(); + const [sendMessage] = useSendMessageByBotMutation(); + const inputRef = useRef(); + const msgInputRef = useRef(); + const [key, setKey] = useState(""); + // const { t } = useTranslation("setting", { keyPrefix: "bot" }); + const { t: ct } = useTranslation(); + const handleSetKey = () => { + const input = inputRef?.current; + if (input && input.value) { + setKey(input.value); + } + }; + const handleSetChannel = (cid: number) => { + setCurrCid(cid); + }; + const handleSetMsgType = (type: string) => { + setMsgType(type); + }; + const handleSend = () => { + const input = msgInputRef?.current; + if (input && input.value && currCid) { + sendMessage({ cid: currCid, api_key: key, type: msgType, content: input.value }); + } + }; + useEffect(() => { + if (key) { + getChannels({ api_key: key }); + } + }, [key]); + return ( + + + + + + } + > + {key ? ( + data ? ( +
                    + {data.map(({ gid, name, is_public }) => { + return ( +
                  • + # {name} {!is_public ? "🔒" : ""} +
                  • + ); + })} +
                  + ) : null + ) : ( +