From 2a3535ea1315d3f6f42b0e6273e40c271d8eb705 Mon Sep 17 00:00:00 2001 From: HD Date: Tue, 28 Jun 2022 10:08:36 +0800 Subject: [PATCH 1/7] refactor: add typescript definition --- src/app/services/contact.ts | 16 +++-- src/app/services/server.ts | 24 ++++--- src/app/slices/contacts.ts | 57 ++++++++-------- src/app/slices/message.channel.ts | 8 +-- src/common/component/Channel.tsx | 16 ++++- src/common/component/ChannelModal/index.tsx | 45 ++++++++----- src/common/component/Contact/ContextMenu.tsx | 2 +- src/common/component/Contact/index.tsx | 2 +- src/common/component/CurrentUser.tsx | 1 - src/common/component/FileMessage/index.tsx | 49 ++++++++++---- src/common/component/ForwardModal/index.tsx | 42 +++++++----- src/common/component/GoogleLoginButton.tsx | 2 +- .../component/InviteModal/AddMembers.tsx | 31 ++++++--- src/common/component/InviteModal/index.tsx | 17 ++++- src/common/component/ManageMembers.tsx | 12 +++- ...{MrakdownRender.tsx => MarkdownRender.tsx} | 10 +-- .../component/Message/PreviewMessage.tsx | 11 +++- src/common/component/Message/Reply.tsx | 4 +- .../component/Message/renderContent.tsx | 4 +- src/common/component/Meta.tsx | 11 ++-- src/common/component/Profile/index.tsx | 2 +- src/common/component/Send/Replying.tsx | 4 +- src/common/component/Send/Toolbar.tsx | 8 ++- src/common/hook/useConfig.ts | 2 +- src/common/hook/useGithubAuthConfig.ts | 4 ++ src/common/hook/useInviteLink.ts | 2 +- src/common/hook/useStreaming/chat.handler.ts | 55 +++++++--------- src/common/hook/useStreaming/index.ts | 3 +- src/common/hook/useUploadFile.ts | 19 +++--- src/common/utils.tsx | 66 ++++++++++++------- 30 files changed, 325 insertions(+), 204 deletions(-) rename src/common/component/{MrakdownRender.tsx => MarkdownRender.tsx} (86%) diff --git a/src/app/services/contact.ts b/src/app/services/contact.ts index b2712913..52ae3373 100644 --- a/src/app/services/contact.ts +++ b/src/app/services/contact.ts @@ -8,6 +8,7 @@ import { fullfillContacts } from "../slices/contacts"; import BASE_URL, { ContentTypes } from "../config"; import { onMessageSendStarted } from "./handlers"; import handleChatMessage from "../../common/hook/useStreaming/chat.handler"; +import { User } from "../../types/auth"; export const contactApi = createApi({ reducerPath: "contactApi", @@ -15,14 +16,15 @@ export const contactApi = createApi({ endpoints: (builder) => ({ getContacts: builder.query({ query: () => ({ url: `user` }), - transformResponse: (data) => { + transformResponse: (data: User[]) => { return data.map((user) => { - const avatar = - user.avatar_updated_at == 0 - ? "" - : `${BASE_URL}/resource/avatar?uid=${user.uid}&t=${user.avatar_updated_at}`; - user.avatar = avatar; - return user; + return { + ...user, + avatar: + user.avatar_updated_at == 0 + ? "" + : `${BASE_URL}/resource/avatar?uid=${user.uid}&t=${user.avatar_updated_at}` + }; }); }, async onQueryStarted(data, { dispatch, queryFulfilled }) { diff --git a/src/app/services/server.ts b/src/app/services/server.ts index 612e1572..f58b4b48 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -5,11 +5,18 @@ import baseQuery from "./base.query"; const defaultExpireDuration = 7 * 24 * 60 * 60; +// todo: doc +interface GetServerResponse {} +interface GetThirdPartySecretResponse {} +interface UpdateThirdPartySecretResponse {} +// interface GetServerVersionResponse {} +interface GetLoginConfigResponse {} + export const serverApi = createApi({ reducerPath: "serverApi", baseQuery, endpoints: (builder) => ({ - getServer: builder.query({ + getServer: builder.query({ query: () => ({ url: `admin/system/organization` }), transformResponse: (data) => { data.logo = `${BASE_URL}/resource/organization/logo?t=${+new Date()}`; @@ -24,7 +31,7 @@ export const serverApi = createApi({ } } }), - getThirdPartySecret: builder.query({ + getThirdPartySecret: builder.query({ query: () => ({ // headers: { // "content-type": "text/plain", @@ -35,7 +42,7 @@ export const serverApi = createApi({ }), keepUnusedDataFor: 0 }), - updateThirdPartySecret: builder.mutation({ + updateThirdPartySecret: builder.mutation({ query: () => ({ url: `/admin/system/third_party_secret`, method: "POST", @@ -45,14 +52,11 @@ export const serverApi = createApi({ getMetrics: builder.query({ query: () => ({ url: `/admin/system/metrics` }) }), - getServerVersion: builder.query({ + getServerVersion: builder.query({ query: () => ({ - headers: { - // "content-type": "text/plain", - accept: "text/plain" - }, + headers: { accept: "text/plain" }, url: `/admin/system/version`, - responseHandler: (response) => response.text() + responseHandler: (response: Response) => response.text() }) }), getFirebaseConfig: builder.query({ @@ -115,7 +119,7 @@ export const serverApi = createApi({ body: data }) }), - getLoginConfig: builder.query({ + getLoginConfig: builder.query({ query: () => ({ url: `admin/login/config` }) }), updateLoginConfig: builder.mutation({ diff --git a/src/app/slices/contacts.ts b/src/app/slices/contacts.ts index ef303402..b210a500 100644 --- a/src/app/slices/contacts.ts +++ b/src/app/slices/contacts.ts @@ -1,8 +1,8 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { getNonNullValues } from '../../common/utils'; -import BASE_URL from '../config'; -import { User } from '../../types/auth'; -import { UserLog, UserState } from '../../types/sse'; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { getNonNullValues } from "../../common/utils"; +import BASE_URL from "../config"; +import { User } from "../../types/auth"; +import { UserLog, UserState } from "../../types/sse"; export interface StoredUser extends User { online?: boolean; @@ -20,31 +20,21 @@ const initialState: State = { }; const contactsSlice = createSlice({ - name: 'contacts', + name: "contacts", initialState, reducers: { resetContacts() { return initialState; }, - fullfillContacts(state, action: PayloadAction) { - console.log('set Contacts store', action); + fullfillContacts(state, action: PayloadAction) { const contacts = action.payload || []; state.ids = contacts.map(({ uid }) => uid); - // old code - // state.byId = Object.fromEntries( - // contacts.map((c) => { - // const { uid } = c; - // return [uid, c]; - // }) - // ); - - contacts.forEach(u => { - state.byId[u.uid] = { - ...u, - // todo: add avatar field - avatar: '' - }; - }); + state.byId = Object.fromEntries( + contacts.map((c) => { + const { uid } = c; + return [uid, c]; + }) + ); }, removeContact(state, action: PayloadAction) { const uid = action.payload; @@ -55,28 +45,35 @@ const contactsSlice = createSlice({ const changeLogs = action.payload; changeLogs.forEach(({ action, uid, ...rest }) => { switch (action) { - case 'update': { + case "update": { const vals = getNonNullValues(rest); if (state.byId[uid]) { Object.keys(vals).forEach((k) => { - state.byId[uid][k] = vals[k]; - if (k == 'avatar_updated_at') { + state.byId[uid]![k] = vals[k]; + if (k == "avatar_updated_at") { state.byId[uid]!.avatar = `${BASE_URL}/resource/avatar?uid=${uid}&t=${vals[k]}`; } }); } break; } - case 'create': { - // todo: missing properties avatar, create_by - state.byId[uid] = { uid, ...rest }; + case "create": { + state.byId[uid] = { + uid, + avatar: + rest.avatar_updated_at === 0 + ? "" + : `${BASE_URL}/resource/avatar?uid=${uid}&t=${rest.avatar_updated_at}`, + create_by: "", // todo: missing properties create_by + ...rest + }; const idx = state.ids.findIndex((i) => i == uid); if (idx == -1) { state.ids.push(uid); } break; } - case 'delete': { + case "delete": { const idx = state.ids.findIndex((i) => i == uid); if (idx > -1) { state.ids.splice(idx, 1); diff --git a/src/app/slices/message.channel.ts b/src/app/slices/message.channel.ts index cbfb3571..a18420d4 100644 --- a/src/app/slices/message.channel.ts +++ b/src/app/slices/message.channel.ts @@ -1,6 +1,6 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ChannelMessage } from '../../types/channel'; -import { EntityId } from '../../types/common'; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { EntityId } from "../../types/common"; +import { ChatEvent } from "../../types/sse"; export interface State { [gid: number]: number[] | undefined; @@ -18,7 +18,7 @@ const channelMsgSlice = createSlice({ fullfillChannelMsg(state, action: PayloadAction) { return action.payload; }, - addChannelMsg(state, action) { + addChannelMsg(state, action: PayloadAction) { const { id, mid, local_id = null } = action.payload; if (state[id]) { const midExsited = state[id]!.findIndex((id) => id == mid) > -1; diff --git a/src/common/component/Channel.tsx b/src/common/component/Channel.tsx index 4faf4167..df054136 100644 --- a/src/common/component/Channel.tsx +++ b/src/common/component/Channel.tsx @@ -1,3 +1,4 @@ +import { FC } from "react"; import styled from "styled-components"; import Avatar from "./Avatar"; import { useAppSelector } from "../../app/store"; @@ -46,14 +47,21 @@ const StyledWrapper = styled.div` } `; -export default function Channel({ interactive = true, id = "", compact = false, avatarSize = 32 }) { +interface Props { + interactive?: boolean; + id: number; + compact?: boolean; + avatarSize?: number; +} + +const Channel: FC = ({ interactive = true, id, compact = false, avatarSize = 32 }) => { const { channel, totalMemberCount } = useAppSelector((store) => { return { channel: store.channels.byId[id], totalMemberCount: store.contacts.ids.length }; }); - console.log("channel item", id, channel); + if (!channel) return null; const { name, members = [], is_public, avatar } = channel; return ( @@ -71,4 +79,6 @@ export default function Channel({ interactive = true, id = "", compact = false, )} ); -} +}; + +export default Channel; diff --git a/src/common/component/ChannelModal/index.tsx b/src/common/component/ChannelModal/index.tsx index 4af65e47..dfbf2717 100644 --- a/src/common/component/ChannelModal/index.tsx +++ b/src/common/component/ChannelModal/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, FC, MouseEvent, ChangeEvent } from "react"; import toast from "react-hot-toast"; import { useNavigate } from "react-router-dom"; import Modal from "../Modal"; @@ -6,24 +6,35 @@ import Button from "../styled/Button"; import ChannelIcon from "../ChannelIcon"; import Contact from "../Contact"; import StyledWrapper from "./styled"; -// import StyledToggle from "../../component/styled/Toggle"; import StyledCheckbox from "../styled/Checkbox"; import useFilteredUsers from "../../hook/useFilteredUsers"; - import { useCreateChannelMutation } from "../../../app/services/channel"; import { useAppSelector } from "../../../app/store"; -export default function ChannelModal({ personal = false, closeModal }) { +interface Props { + personal?: boolean; + closeModal: () => void; +} + +interface CreateChannelDTO { + name: string; + description: string; + members?: number[]; + is_public: boolean; +} + +const ChannelModal: FC = ({ personal = false, closeModal }) => { const { contactsData, loginUid } = useAppSelector((store) => { return { contactsData: store.contacts.byId, loginUid: store.authData.uid }; }); const navigateTo = useNavigate(); - const [data, setData] = useState({ + const [data, setData] = useState({ name: "", description: "", members: [loginUid], is_public: !personal }); + const { contacts, input, updateInput } = useFilteredUsers(); const [createChannel, { isSuccess, isError, isLoading, data: newChannelId }] = useCreateChannelMutation(); @@ -35,6 +46,7 @@ export default function ChannelModal({ personal = false, closeModal }) { // }); // }; const handleCreate = () => { + // todo: add field validation (maxLength, text format, trim) if (!data.name) { toast("please input channel name"); return; @@ -46,11 +58,13 @@ export default function ChannelModal({ personal = false, closeModal }) { createChannel(data); }; + // todo: delete the following code and use common error handler instead useEffect(() => { if (isError) { toast.error("create new channel failed"); } }, [isError]); + useEffect(() => { if (isSuccess) { toast.success("create new channel success"); @@ -59,26 +73,25 @@ export default function ChannelModal({ personal = false, closeModal }) { } }, [isSuccess, newChannelId]); - const handleNameInput = (evt) => { - setData((prev) => { - return { ...prev, name: evt.target.value }; - }); + const handleNameInput = (evt: ChangeEvent) => { + setData((prev) => ({ ...prev, name: evt.target.value })); }; - const handleInputChange = (evt) => { + + const handleInputChange = (evt: ChangeEvent) => { updateInput(evt.target.value); }; - const toggleCheckMember = ({ currentTarget }) => { + + const toggleCheckMember = ({ currentTarget }: MouseEvent) => { const { members } = data; const { uid } = currentTarget.dataset; let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid]; - setData((prev) => { - return { ...prev, members: tmp }; - }); + setData((prev) => ({ ...prev, members: tmp })); }; const loginUser = contactsData[loginUid]; if (!loginUser) return null; const { name, members, is_public } = data; + return ( @@ -152,4 +165,6 @@ export default function ChannelModal({ personal = false, closeModal }) { ); -} +}; + +export default ChannelModal; diff --git a/src/common/component/Contact/ContextMenu.tsx b/src/common/component/Contact/ContextMenu.tsx index 628ad95e..a2e8ff88 100644 --- a/src/common/component/Contact/ContextMenu.tsx +++ b/src/common/component/Contact/ContextMenu.tsx @@ -6,7 +6,7 @@ import ContextMenu from "../ContextMenu"; interface Props { enable?: boolean; uid: number; - cid: number; + cid?: number; visible: boolean; hide: () => void; children: ReactElement; diff --git a/src/common/component/Contact/index.tsx b/src/common/component/Contact/index.tsx index 46e23efc..e53a82cd 100644 --- a/src/common/component/Contact/index.tsx +++ b/src/common/component/Contact/index.tsx @@ -10,8 +10,8 @@ import useContextMenu from "../../hook/useContextMenu"; import { useAppSelector } from "../../../app/store"; interface Props { - cid: number; uid: number; + cid?: number; owner?: number; dm?: boolean; interactive?: boolean; diff --git a/src/common/component/CurrentUser.tsx b/src/common/component/CurrentUser.tsx index 710b6c78..701e7fb8 100644 --- a/src/common/component/CurrentUser.tsx +++ b/src/common/component/CurrentUser.tsx @@ -1,5 +1,4 @@ import styled from "styled-components"; -import { useSelector } from "react-redux"; import soundIcon from "../../assets/icons/sound.on.svg?url"; import micIcon from "../../assets/icons/mic.on.svg?url"; import Avatar from "./Avatar"; diff --git a/src/common/component/FileMessage/index.tsx b/src/common/component/FileMessage/index.tsx index ce80bdd7..5d27e27e 100644 --- a/src/common/component/FileMessage/index.tsx +++ b/src/common/component/FileMessage/index.tsx @@ -1,16 +1,16 @@ -import { useEffect, useState } from "react"; +import { FC, useEffect, useState } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { useSelector } from "react-redux"; import Styled from "./styled"; import ImageMessage from "./ImageMessage"; import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage"; import useUploadFile from "../../hook/useUploadFile"; import useSendMessage from "../../hook/useSendMessage"; import Progress from "./Progress"; -import { getFileIcon, formatBytes, isImage, getImageSize } from "../../utils"; +import { getFileIcon, formatBytes, isImage, getImageSize, ImageSize } from "../../utils"; // import { ReactComponent as IconDownload } from "../../../assets/icons/download.svg"; // import { ReactComponent as IconClose } from "../../../assets/icons/close.circle.svg"; +import { useAppSelector } from "../../../app/store"; import IconDownload from "../../../assets/icons/download.svg"; import IconClose from "../../../assets/icons/close.circle.svg"; @@ -21,17 +21,33 @@ const isLocalFile = (content: string) => { return content.startsWith("blob:"); }; -export default function FileMessage({ - context = "", - to = null, +interface Props { + context: "user" | "channel"; + to: number; + created_at: number; + from_uid?: number; + content: string; + download: string; + thumbnail: string; + properties: { + local_id: number; + name: string; + size: number; + content_type: string; + }; +} + +const FileMessage: FC = ({ + context, + to, created_at, from_uid = null, content = "", download = "", thumbnail = "", properties = { local_id: 0, name: "", size: 0, content_type: "" } -}) { - const [imageSize, setImageSize] = useState(null); +}) => { + const [imageSize, setImageSize] = useState(null); const [uploadingFile, setUploadingFile] = useState(false); const removeLocalMessage = useRemoveLocalMessage({ context, id: to }); const { @@ -44,10 +60,18 @@ export default function FileMessage({ to }); const { stopUploading, data, uploadFile, progress, isSuccess: uploadSuccess } = useUploadFile(); - const fromUser = useSelector((store) => store.contacts.byId[from_uid]); + const fromUser = useAppSelector((store) => store.contacts.byId[from_uid]); const { size, name, content_type } = properties ?? {}; useEffect(() => { - const handleUpSend = async ({ url, name, type }) => { + const handleUpSend = async ({ + url, + name, + type + }: { + url: string; + name: string; + type: string; + }) => { try { setUploadingFile(true); if (type.startsWith("image")) { @@ -102,7 +126,6 @@ export default function FileMessage({ if (!content || !name) return null; - console.log("file content", content, name, content_type, size); const sending = uploadingFile || isSending; if (isImage(content_type, size)) @@ -152,4 +175,6 @@ export default function FileMessage({ ); -} +}; + +export default FileMessage; diff --git a/src/common/component/ForwardModal/index.tsx b/src/common/component/ForwardModal/index.tsx index 9bb14657..6c95414b 100644 --- a/src/common/component/ForwardModal/index.tsx +++ b/src/common/component/ForwardModal/index.tsx @@ -1,11 +1,10 @@ -import { useState, MouseEvent } from "react"; -// import toast from "react-hot-toast"; +import { useState, MouseEvent, FC, ChangeEvent } from "react"; +import toast from "react-hot-toast"; import Modal from "../Modal"; import Button from "../styled/Button"; import Input from "../styled/Input"; import Channel from "../Channel"; import Contact from "../Contact"; -// import Channel from "../Channel"; import Reply from "../Message/Reply"; import StyledWrapper from "./styled"; import useForwardMessage from "../../hook/useForwardMessage"; @@ -14,14 +13,18 @@ import useFilteredChannels from "../../hook/useFilteredChannels"; import useFilteredUsers from "../../hook/useFilteredUsers"; import CloseIcon from "../../../assets/icons/close.circle.svg"; import StyledCheckbox from "../styled/Checkbox"; -import toast from "react-hot-toast"; -export default function ForwardModal({ mids, closeModal }) { +interface Props { + mids: number[]; + closeModal: () => void; +} + +const ForwardModal: FC = ({ mids, closeModal }) => { const [appendText, setAppendText] = useState(""); const { sendMessages } = useSendMessage(); const { forwardMessage, forwarding } = useForwardMessage(); - const [selectedMembers, setSelectedMembers] = useState([]); - const [selectedChannels, setSelectedChannels] = useState([]); + const [selectedMembers, setSelectedMembers] = useState([]); + const [selectedChannels, setSelectedChannels] = useState([]); const { channels, // input: channelInput, @@ -31,17 +34,20 @@ export default function ForwardModal({ mids, closeModal }) { // const { conactsData, loginUid } = useSelector((store) => { // return { conactsData: store.contacts.byId, loginUid: store.authData.uid }; // }); + const toggleCheck = ({ currentTarget }: MouseEvent) => { const { id, type = "user" } = currentTarget.dataset; - const ids = type == "user" ? selectedMembers : selectedChannels; + const ids: number[] = type == "user" ? selectedMembers : selectedChannels; const updateState = type == "user" ? setSelectedMembers : setSelectedChannels; - let tmp = ids.includes(+id) ? ids.filter((m) => m != id) : [...ids, +id]; - console.log(id, currentTarget); + const id_num = Number(id); + let tmp = ids.includes(id_num) ? ids.filter((m) => m !== id_num) : [...ids, id_num]; updateState(tmp); }; - const updateAppendText = (evt) => { + + const updateAppendText = (evt: ChangeEvent) => { setAppendText(evt.target.value); }; + const handleForward = async () => { await forwardMessage({ mids: mids.map((mid) => +mid), @@ -58,21 +64,25 @@ export default function ForwardModal({ mids, closeModal }) { toast.success("Forward Message Successfully"); closeModal(); }; - const removeSelected = (id, from = "user") => { + + const removeSelected = (id: number, from = "user") => { if (from == "user") { setSelectedMembers(selectedMembers.filter((m) => m != id)); } else { setSelectedChannels(selectedChannels.filter((cid) => cid != id)); } }; - const handleSearchChange = (evt) => { + + const handleSearchChange = (evt: ChangeEvent) => { const newVal = evt.target.value; updateChannelInput(newVal); updateInput(newVal); }; + let selectedCount = selectedMembers.length + selectedChannels.length; const sendButtonDisabled = (selectedChannels.length == 0 && selectedMembers.length == 0) || forwarding; + return ( @@ -89,7 +99,6 @@ export default function ForwardModal({ mids, closeModal }) { channels.map((c) => { const { gid } = c; const checked = selectedChannels.includes(gid); - console.log({ checked }); return (
  • { const { uid } = u; const checked = selectedMembers.includes(uid); - console.log({ checked }); return (
  • ); -} +}; + +export default ForwardModal; diff --git a/src/common/component/GoogleLoginButton.tsx b/src/common/component/GoogleLoginButton.tsx index 9e5fd5c6..12730ee9 100644 --- a/src/common/component/GoogleLoginButton.tsx +++ b/src/common/component/GoogleLoginButton.tsx @@ -60,7 +60,7 @@ const GoogleLoginButton: FC = ({ clientId }) => { return ( google icon - {loaded ? `Sign in with Google` : `Initailizing`} + {loaded ? "Sign in with Google" : "Initializing"} ); }; diff --git a/src/common/component/InviteModal/AddMembers.tsx b/src/common/component/InviteModal/AddMembers.tsx index 82d0aa5b..1bfb8764 100644 --- a/src/common/component/InviteModal/AddMembers.tsx +++ b/src/common/component/InviteModal/AddMembers.tsx @@ -1,6 +1,5 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, FC, MouseEvent, ChangeEvent } from "react"; import styled from "styled-components"; -import { useSelector } from "react-redux"; import toast from "react-hot-toast"; import Button from "../styled/Button"; import Input from "../styled/Input"; @@ -9,6 +8,7 @@ import StyledCheckbox from "../styled/Checkbox"; import { useAddMembersMutation } from "../../../app/services/channel"; import CloseIcon from "../../../assets/icons/close.svg"; import useFilteredUsers from "../../hook/useFilteredUsers"; +import { useAppSelector } from "../../../app/store"; const Styled = styled.div` padding-top: 16px; @@ -21,7 +21,7 @@ const Styled = styled.div` margin-bottom: 12px; background: #ffffff; border: 1px solid #e5e7eb; - box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08); + box-shadow: 0 1px 2px rgba(31, 41, 55, 0.08); border-radius: 4px; .selects { display: flex; @@ -96,10 +96,16 @@ const Styled = styled.div` line-height: 20px; } `; -export default function AddMembers({ cid = null, closeModal }) { + +interface Props { + cid: null | number; + closeModal: () => void; +} + +const AddMembers: FC = ({ cid = null, closeModal }) => { const [addMembers, { isLoading: isAdding, isSuccess }] = useAddMembersMutation(); const [selects, setSelects] = useState([]); - const { channel, contactData } = useSelector((store) => { + const { channel, contactData } = useAppSelector((store) => { return { channel: store.channels.byId[cid], contactData: store.contacts.byId @@ -116,7 +122,8 @@ export default function AddMembers({ cid = null, closeModal }) { addMembers({ id: cid, members: selects }); }; const { input, updateInput, contacts = [] } = useFilteredUsers(); - const toggleCheckMember = ({ currentTarget }) => { + + const toggleCheckMember = ({ currentTarget }: MouseEvent) => { const { uid } = currentTarget.dataset; if (selects.includes(+uid)) { setSelects((prevs) => { @@ -126,13 +133,15 @@ export default function AddMembers({ cid = null, closeModal }) { setSelects([...selects, +uid]); } }; - const handleFilterInput = (evt) => { + + const handleFilterInput = (evt: ChangeEvent) => { updateInput(evt.target.value); }; + if (!channel) return null; const { members: uids } = channel; const contactIds = contacts.map(({ uid }) => uid); - console.log("selects", selects); + return (
    @@ -164,7 +173,7 @@ export default function AddMembers({ cid = null, closeModal }) { key={uid} data-uid={uid} className="user" - onClick={added ? null : toggleCheckMember} + onClick={added ? undefined : toggleCheckMember} > ); -} +}; + +export default AddMembers; diff --git a/src/common/component/InviteModal/index.tsx b/src/common/component/InviteModal/index.tsx index b58b0282..ce53531d 100644 --- a/src/common/component/InviteModal/index.tsx +++ b/src/common/component/InviteModal/index.tsx @@ -4,12 +4,13 @@ import AddMembers from "./AddMembers"; import CloseIcon from "../../../assets/icons/close.svg"; import Modal from "../Modal"; import { useAppSelector } from "../../../app/store"; +import { FC } from "react"; const Styled = styled.div` display: flex; flex-direction: column; background: #fff; - box-shadow: 0px 25px 50px rgba(31, 41, 55, 0.25); + box-shadow: 0 25px 50px rgba(31, 41, 55, 0.25); border-radius: var(--br); padding: 16px; min-width: 408px; @@ -29,7 +30,15 @@ const Styled = styled.div` `; // type: server,channel -export default function InviteModal({ type = "server", cid = null, title = "", closeModal }) { + +interface Props { + type?: "server" | "channel"; + cid?: null | number; + title?: string; + closeModal: () => void; +} + +const InviteModal: FC = ({ type = "server", cid = null, title = "", closeModal }) => { const { channel, server } = useAppSelector((store) => { return { channel: store.channels.byId[cid], @@ -49,4 +58,6 @@ export default function InviteModal({ type = "server", cid = null, title = "", c ); -} +}; + +export default InviteModal; diff --git a/src/common/component/ManageMembers.tsx b/src/common/component/ManageMembers.tsx index f94a63b9..8bac4f53 100644 --- a/src/common/component/ManageMembers.tsx +++ b/src/common/component/ManageMembers.tsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { FC, useEffect } from "react"; import styled from "styled-components"; import Tippy from "@tippyjs/react"; import { hideAll } from "tippy.js"; @@ -115,7 +115,11 @@ const StyledWrapper = styled.section` } `; -export default function ManageMembers({ cid = null }) { +interface Props { + cid?: number; +} + +const ManageMembers: FC = ({ cid = null }) => { const { contacts, channels, loginUser } = useAppSelector((store) => { return { contacts: store.contacts, @@ -246,4 +250,6 @@ export default function ManageMembers({ cid = null }) { ); -} +}; + +export default ManageMembers; diff --git a/src/common/component/MrakdownRender.tsx b/src/common/component/MarkdownRender.tsx similarity index 86% rename from src/common/component/MrakdownRender.tsx rename to src/common/component/MarkdownRender.tsx index 4a8fef1f..b322a2a2 100644 --- a/src/common/component/MrakdownRender.tsx +++ b/src/common/component/MarkdownRender.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef, FC } from "react"; +import { useEffect, useState, useRef, FC, MouseEvent } from "react"; import "prismjs/themes/prism.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"; @@ -11,7 +11,7 @@ import codeSyntaxHighlight from "@toast-ui/editor-plugin-code-syntax-highlight/d import { Viewer } from "@toast-ui/react-editor"; import styled from "styled-components"; -import ImagePreviewModal from "./ImagePreviewModal"; +import ImagePreviewModal, { PreviewImageData } from "./ImagePreviewModal"; const Styled = styled.div` * { @@ -28,9 +28,9 @@ interface Props { content: string; } -const MrakdownRender: FC = ({ content }) => { +const MarkdownRender: FC = ({ content }) => { const mdContainer = useRef(null); - const [previewImage, setPreviewImage] = useState(null); + const [previewImage, setPreviewImage] = useState(null); useEffect(() => { const container = mdContainer?.current; @@ -77,4 +77,4 @@ const MrakdownRender: FC = ({ content }) => { ); }; -export default MrakdownRender; +export default MarkdownRender; diff --git a/src/common/component/Message/PreviewMessage.tsx b/src/common/component/Message/PreviewMessage.tsx index 7e9153a8..e6cdda43 100644 --- a/src/common/component/Message/PreviewMessage.tsx +++ b/src/common/component/Message/PreviewMessage.tsx @@ -3,8 +3,13 @@ import renderContent from "./renderContent"; import Avatar from "../Avatar"; import StyledWrapper from "./styled"; import { useAppSelector } from "../../../app/store"; +import { FC } from "react"; -export default function PreviewMessage({ mid = 0 }) { +interface Props { + mid?: number; +} + +const PreviewMessage: FC = ({ mid = 0 }) => { const { msg, contactsData } = useAppSelector((store) => { return { msg: store.message[mid], contactsData: store.contacts.byId }; }); @@ -33,4 +38,6 @@ export default function PreviewMessage({ mid = 0 }) {
    ); -} +}; + +export default PreviewMessage; diff --git a/src/common/component/Message/Reply.tsx b/src/common/component/Message/Reply.tsx index 7ea472fb..c73ff2de 100644 --- a/src/common/component/Message/Reply.tsx +++ b/src/common/component/Message/Reply.tsx @@ -1,7 +1,7 @@ import React, { MouseEvent, FC } from "react"; import styled from "styled-components"; import reactStringReplace from "react-string-replace"; -import MrakdownRender from "../MrakdownRender"; +import MarkdownRender from "../MarkdownRender"; import Mention from "./Mention"; import { ContentTypes } from "../../../app/config"; import { getFileIcon, isImage } from "../../utils"; @@ -122,7 +122,7 @@ const renderContent = (data) => { case ContentTypes.markdown: res = (
    - +
    ); break; diff --git a/src/common/component/Message/renderContent.tsx b/src/common/component/Message/renderContent.tsx index 6f123777..bd34fdec 100644 --- a/src/common/component/Message/renderContent.tsx +++ b/src/common/component/Message/renderContent.tsx @@ -5,7 +5,7 @@ import reactStringReplace from "react-string-replace"; import { ContentTypes } from "../../../app/config"; import Mention from "./Mention"; import ForwardedMessage from "./ForwardedMessage"; -import MrakdownRender from "../MrakdownRender"; +import MarkdownRender from "../MarkdownRender"; import FileMessage from "../FileMessage"; import URLPreview from "./URLPreview"; @@ -54,7 +54,7 @@ const renderContent = ({ break; case ContentTypes.markdown: { - ctn = ; + ctn = ; } break; case ContentTypes.file: diff --git a/src/common/component/Meta.tsx b/src/common/component/Meta.tsx index ad514e8d..2f76b4d6 100644 --- a/src/common/component/Meta.tsx +++ b/src/common/component/Meta.tsx @@ -1,4 +1,3 @@ -// import React from 'react' import { Helmet } from "react-helmet"; import BASE_URL from "../../app/config"; import { useGetServerQuery } from "../../app/services/server"; @@ -7,11 +6,9 @@ export default function Meta() { const { data, isSuccess } = useGetServerQuery(); return ( - <> - - - {isSuccess && {data.name} Web App} - - + + + {isSuccess && {data.name} Web App} + ); } diff --git a/src/common/component/Profile/index.tsx b/src/common/component/Profile/index.tsx index fe4151af..af0292e2 100644 --- a/src/common/component/Profile/index.tsx +++ b/src/common/component/Profile/index.tsx @@ -13,7 +13,7 @@ import { useAppSelector } from "../../../app/store"; interface Props { uid: number; type: string; - cid: number; + cid?: number; } const Profile: FC = ({ uid = null, type = "embed", cid = null }) => { diff --git a/src/common/component/Send/Replying.tsx b/src/common/component/Send/Replying.tsx index 85a1a961..0a1f6242 100644 --- a/src/common/component/Send/Replying.tsx +++ b/src/common/component/Send/Replying.tsx @@ -2,7 +2,7 @@ import reactStringReplace from "react-string-replace"; import styled from "styled-components"; import Mention from "../Message/Mention"; import { ContentTypes } from "../../../app/config"; -import MrakdownRender from "../MrakdownRender"; +import MarkdownRender from "../MarkdownRender"; import closeIcon from "../../../assets/icons/close.circle.svg?url"; import pictureIcon from "../../../assets/icons/picture.svg?url"; import { getFileIcon, isImage } from "../../utils"; @@ -91,7 +91,7 @@ const renderContent = (data) => { case ContentTypes.markdown: res = (
    - +
    ); break; diff --git a/src/common/component/Send/Toolbar.tsx b/src/common/component/Send/Toolbar.tsx index 4f5f0214..27cf11f8 100644 --- a/src/common/component/Send/Toolbar.tsx +++ b/src/common/component/Send/Toolbar.tsx @@ -51,19 +51,21 @@ export default function Toolbar({ to, context }) { - // todo: check code logic const { addStageFile } = useUploadFile({ context, id: to }); const fileInputRef = useRef(null); const handleUpload = (evt: ChangeEvent) => { - const files = [...evt.target.files]; - console.log("files", files); + if (!evt.target.files) return; + const files = Array.from(evt.target.files); const filesData = files.map((file) => { const { size, type, name } = file; const url = URL.createObjectURL(file); return { size, type, name, url }; }); addStageFile(filesData); + // todo: check code logic + // @ts-ignore fileInputRef.current.value = null; + // @ts-ignore fileInputRef.current.value = ""; // setFiles([...evt.target.files]); }; diff --git a/src/common/hook/useConfig.ts b/src/common/hook/useConfig.ts index 699c3b03..c1842e9d 100644 --- a/src/common/hook/useConfig.ts +++ b/src/common/hook/useConfig.ts @@ -17,7 +17,7 @@ let originalValue: null | object = null; export default function useConfig(config = "smtp") { const [changed, setChanged] = useState(false); - const [values, setValues] = useState({}); + const [values, setValues] = useState({}); const [updateLoginConfig, { isSuccess: LoginUpdated }] = useUpdateLoginConfigMutation(); const [updateSMTPConfig, { isSuccess: SMTPUpdated }] = useUpdateSMTPConfigMutation(); const [getAgoraConfig, { data: agoraConfig }] = useLazyGetAgoraConfigQuery(); diff --git a/src/common/hook/useGithubAuthConfig.ts b/src/common/hook/useGithubAuthConfig.ts index 5fa29c4a..79678d9f 100644 --- a/src/common/hook/useGithubAuthConfig.ts +++ b/src/common/hook/useGithubAuthConfig.ts @@ -4,6 +4,10 @@ import { useUpdateGithubAuthConfigMutation } from "../../app/services/server"; +interface GithubAuthConfig { + client_id: string; +} + export default function useGithubAuthConfig() { const [changed, setChanged] = useState(false); const [config, setConfig] = useState({}); diff --git a/src/common/hook/useInviteLink.ts b/src/common/hook/useInviteLink.ts index 4ebc3842..2acd46bc 100644 --- a/src/common/hook/useInviteLink.ts +++ b/src/common/hook/useInviteLink.ts @@ -19,7 +19,6 @@ export default function useInviteLink(cid: string | null = "") { useEffect(() => { const _link = channelInviteLink; - console.log("fetching", channelInviteLink); if (_link && smtpStatusFetchSuccess) { // const tmpURL = new URL(_link); // tmpURL.searchParams.set("code", SMTPEnabled); @@ -29,6 +28,7 @@ export default function useInviteLink(cid: string | null = "") { const genServerLink = () => { generateChannelInviteLink(); }; + return { enableSMTP: SMTPEnabled, generating: generatingChannelLink, diff --git a/src/common/hook/useStreaming/chat.handler.ts b/src/common/hook/useStreaming/chat.handler.ts index fc247061..fab52444 100644 --- a/src/common/hook/useStreaming/chat.handler.ts +++ b/src/common/hook/useStreaming/chat.handler.ts @@ -6,8 +6,10 @@ import { addFileMessage, removeFileMessage } from "../../../app/slices/message.f import { addUserMsg, removeUserMsg } from "../../../app/slices/message.user"; import { updateAfterMid } from "../../../app/slices/footprint"; import { ContentTypes } from "../../../app/config"; +import { ChatEvent } from "../../../types/sse"; +import { AppDispatch, RootState } from "../../../app/store"; -const handler = (data, dispatch, currState) => { +const handler = (data: ChatEvent, dispatch: AppDispatch, currState: RootState) => { const { mid, from_uid, @@ -45,37 +47,30 @@ const handler = (data, dispatch, currState) => { // 此处有点绕 const id = to == "user" ? (self ? target.uid : from_uid) : target.gid; const readIndex = (to == "user" ? readUsers[id] : readChannels[id]) || 0; - const read = self ? true : mid < readIndex ? true : false; + const read = self ? true : mid < readIndex; switch (type) { - case "normal": - { - batch(() => { - dispatch( - addMessage({ - mid, - // 如果是自己发的消息,就是已读 - read, - ...common - }) - ); - // 未推送完 or 不是自己发的消息 - console.log("curr state", ready, loginUid, common.from_uid); - // if (!ready || loginUid != common.from_uid) { - dispatch( - appendMessage({ - id, - mid, - local_id: properties ? properties.local_id : null - }) - ); - // 加到file message 列表 - if (content_type == ContentTypes.file) { - dispatch(addFileMessage(mid)); - } - // } - }); - } + case "normal": { + batch(() => { + // 如果是自己发的消息,就是已读 + dispatch(addMessage({ mid, read, ...common })); + // 未推送完 or 不是自己发的消息 + console.log("curr state", ready, loginUid, common.from_uid); + // if (!ready || loginUid != common.from_uid) { + dispatch( + appendMessage({ + id, + mid, + local_id: properties ? properties.local_id : null + }) + ); + // 加到file message 列表 + if (content_type == ContentTypes.file) { + dispatch(addFileMessage(mid)); + } + // } + }); break; + } case "reply": { batch(() => { diff --git a/src/common/hook/useStreaming/index.ts b/src/common/hook/useStreaming/index.ts index 9d7e9fd5..e7623ba7 100644 --- a/src/common/hook/useStreaming/index.ts +++ b/src/common/hook/useStreaming/index.ts @@ -22,6 +22,7 @@ import { updateUsersByLogs, updateUsersStatus } from "../../../app/slices/contac import { resetAuthData } from "../../../app/slices/auth.data"; import chatMessageHandler from "./chat.handler"; import store, { useAppDispatch, useAppSelector } from "../../../app/store"; +import { ServerEvent } from "../../../types/sse"; class RetriableError extends Error {} @@ -115,7 +116,7 @@ export default function useStreaming() { console.log("sse debug: error message fatal"); throw new FatalError(evt.data); } - const data = JSON.parse(evt.data); + const data: ServerEvent = JSON.parse(evt.data); const { type } = data; switch (type) { case "heartbeat": diff --git a/src/common/hook/useUploadFile.ts b/src/common/hook/useUploadFile.ts index 80ef54ab..57cff86f 100644 --- a/src/common/hook/useUploadFile.ts +++ b/src/common/hook/useUploadFile.ts @@ -5,7 +5,8 @@ import BASE_URL, { FILE_SLICE_SIZE } from "../../app/config"; import { usePrepareUploadFileMutation, useUploadFileMutation } from "../../app/services/message"; import { useAppDispatch, useAppSelector } from "../../app/store"; -export default function useUploadFile(props = {}) { +// todo: check props type +export default function useUploadFile(props: { context: string; id: string } | object = {}) { const { context = "", id = "" } = props; const dispatch = useAppDispatch(); const { stageFiles, replying } = useAppSelector((store) => { @@ -16,7 +17,7 @@ export default function useUploadFile(props = {}) { }); const [data, setData] = useState(null); // const [uploadingFile, setUploadingFile] = useState(false); - const canneledRef = useRef(false); + const canceledRef = useRef(false); const sliceUploadedCountRef = useRef(0); const totalSliceCountRef = useRef(1); const [prepareUploadFile, { isLoading: isPreparing, isSuccess: isPrepared }] = @@ -26,16 +27,18 @@ export default function useUploadFile(props = {}) { { isLoading: isUploading, isSuccess: isUploaded, isError: uploadFileError } ] = useUploadFileMutation(); - const uploadChunk = (data) => { + const uploadChunk = (data: { file_id: string; chunk: File; is_last: boolean }) => { const { file_id, chunk, is_last } = data; const formData = new FormData(); formData.append("file_id", file_id); formData.append("chunk_data", chunk); - formData.append("chunk_is_last", is_last); + formData.append("chunk_is_last", `${is_last}`); + // old code + // formData.append("chunk_is_last", is_last); return uploadFileFn(formData); }; - const uploadFile = async (file) => { + const uploadFile = async (file?: File) => { if (!file) return; setData(null); const { @@ -51,7 +54,7 @@ export default function useUploadFile(props = {}) { console.log("file id", file_id); let uploadResult = null; - canneledRef.current = false; + canceledRef.current = false; totalSliceCountRef.current = 1; sliceUploadedCountRef.current = 0; // setUploadingFile(true); @@ -69,7 +72,7 @@ export default function useUploadFile(props = {}) { for await (const [idx] of _arr.entries()) { // 退出循环 - if (canneledRef.current) break; + if (canceledRef.current) break; try { const chunk = file.slice(FILE_SLICE_SIZE * idx, FILE_SLICE_SIZE * (idx + 1), file_type); @@ -109,7 +112,7 @@ export default function useUploadFile(props = {}) { }; const stopUploading = () => { - canneledRef.current = true; + canceledRef.current = true; }; const removeStageFile = (idx) => { diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 82a5c05f..530b1f1c 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -11,7 +11,7 @@ export const isImage = (file_type = "", size = 0) => { return file_type.startsWith("image") && size <= FILE_IMAGE_SIZE; }; -const deepSortObject = (obj) => { +const deepSortObject = (obj: object): any => { return Object.fromEntries( Object.entries(obj) .map((entry) => [ @@ -22,7 +22,7 @@ const deepSortObject = (obj) => { ); }; -export const isObjectEqual = (obj1, obj2) => { +export const isObjectEqual = (obj1: any, obj2: any) => { // Check for reference equal if (obj1 === obj2) return true; // Check for deep equal @@ -30,7 +30,8 @@ export const isObjectEqual = (obj1, obj2) => { let o2 = JSON.stringify(deepSortObject(obj2 ?? {})); return o1 === o2; }; -export const isTreatAsImage = (file) => { + +export const isTreatAsImage = (file: File | null) => { let isImage = false; if (!file) return isImage; const { type, size } = file; @@ -41,8 +42,8 @@ export const isTreatAsImage = (file) => { return isImage; }; -export const getNonNullValues = (obj, whiteList = ["log_id"]) => { - const tmp = {}; +export const getNonNullValues = (obj: any, whiteList = ["log_id"]): any => { + const tmp: any = {}; Object.keys(obj).forEach((k) => { if (!whiteList.includes(k) && obj[k] !== null) { tmp[k] = obj[k]; @@ -50,14 +51,20 @@ export const getNonNullValues = (obj, whiteList = ["log_id"]) => { }); return tmp; }; -export function getDefaultSize(size = null, min = 480) { + +interface Size { + width: number; + height: number; +} + +export function getDefaultSize(size: Size | null = null, min: number = 480) { if (!size) return { width: 0, height: 0 }; const { width: oWidth, height: oHeight } = size; if (oWidth == oHeight) { const tmp = min > oWidth ? oWidth : min; return { width: tmp, height: tmp }; } - const isVertical = oWidth > oHeight ? false : true; + const isVertical = oWidth <= oHeight; let dWidth = 0; let dHeight = 0; if (isVertical) { @@ -69,7 +76,8 @@ export function getDefaultSize(size = null, min = 480) { } return { width: dWidth, height: dHeight }; } -export function formatBytes(bytes, decimals = 2) { + +export function formatBytes(bytes: number, decimals = 2) { if (bytes === 0) return "0 Bytes"; const k = 1000; @@ -80,9 +88,16 @@ export function formatBytes(bytes, decimals = 2) { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; } -export const getImageSize = (url) => { - const size = { width: 0, height: 0 }; - if (!url) return size; + +export interface ImageSize { + width: number; + height: number; +} + +export const getImageSize = (url: string): Promise => { + const size: ImageSize = { width: 0, height: 0 }; + // todo: check inactive code + // if (!url) return size; return new Promise((resolve) => { const img = new Image(); img.src = url; @@ -96,13 +111,15 @@ export const getImageSize = (url) => { }; }); }; -export const getInitials = (name) => { + +export const getInitials = (name: string) => { const arr = name.split(" ").filter((n) => !!n); return arr .map((t) => t[0]) .join("") .toUpperCase(); }; + export const getInitialsAvatar = ({ initials = "UK", initial_size = 0, @@ -121,7 +138,7 @@ export const getInitialsAvatar = ({ canvas.style.width = `${width}px`; canvas.style.height = `${height}px`; - const context = canvas.getContext("2d"); + const context = canvas.getContext("2d")!; context.scale(devicePixelRatio, devicePixelRatio); context.rect(0, 0, canvas.width, canvas.height); context.fillStyle = background; @@ -139,12 +156,8 @@ export const getInitialsAvatar = ({ /* istanbul ignore next */ return canvas.toDataURL("image/png"); }; -/** - * @param {File|Blob} - file to slice - * @param {Number} - chunksAmount - * @return {Array} - an array of Blobs - **/ -export function sliceFile(file, chunksAmount) { + +export function sliceFile(file: File | null, chunksAmount: number) { if (!file) return null; let byteIndex = 0; let chunks = []; @@ -157,7 +170,8 @@ export function sliceFile(file, chunksAmount) { return chunks; } -export const getFileIcon = (type, name = "") => { + +export const getFileIcon = (type: string, name: string = "") => { let icon = null; const checks = { @@ -199,10 +213,18 @@ export const getFileIcon = (type, name = "") => { } return icon; }; -export const normalizeArchiveData = (data = null, filePath = null, uid = null) => { + +export const normalizeArchiveData = ( + data = null, + filePath: string | null = null, + uid: number | null = null +) => { if (!data || !filePath) return []; const { messages, users } = data; - const getUrls = (uid, { content, content_type, file_id, thumbnail_id, filePath, avatar }) => { + const getUrls = ( + uid: number, + { content, content_type, file_id, thumbnail_id, filePath, avatar } + ) => { // uid存在,则favorite,否则archive const prefix = uid ? `${BASE_URL}/favorite/attachment/${uid}/${filePath}/` From 8b5a1212cd56c27b06416ae7aef950d5500aba62 Mon Sep 17 00:00:00 2001 From: HD Date: Tue, 28 Jun 2022 10:15:21 +0800 Subject: [PATCH 2/7] refactor: typo IconUnkown -> IconUnknown --- src/common/utils.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 8c738942..926754cc 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -2,7 +2,7 @@ import BASE_URL, { FILE_IMAGE_SIZE, ContentTypes } from "../app/config"; 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 IconUnkown from "../assets/icons/file.unkown.svg"; +import IconUnknown from "../assets/icons/file.unkown.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"; @@ -175,7 +175,7 @@ export const getFileIcon = (type, name = "") => { break; default: - icon = ; + icon = ; break; } return icon; From d01cb5b2a8f3e2b4adb46ac0a99ef4aab3811749 Mon Sep 17 00:00:00 2001 From: HD Date: Tue, 28 Jun 2022 10:17:00 +0800 Subject: [PATCH 3/7] refactor: remove useless judgment logic --- src/common/utils.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 926754cc..5f490813 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -11,15 +11,13 @@ export const isImage = (file_type = "", size = 0) => { return file_type.startsWith("image") && size <= FILE_IMAGE_SIZE; }; -export const isTreatAsImage = (file) => { - let isImage = false; - if (!file) return isImage; +export const isTreatAsImage = (file: File) => { const { type, size } = file; if (type.startsWith("image")) { // 10MB return size < 1000 * 1000; } - return isImage; + return false; }; export const getNonNullValues = (obj, whiteList = ["log_id"]) => { From ab5f9e6c0753231a406fae0eb470a7752b4c654a Mon Sep 17 00:00:00 2001 From: HD Date: Tue, 28 Jun 2022 10:17:45 +0800 Subject: [PATCH 4/7] refactor: simplify judgment conditions --- src/common/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 5f490813..608b4226 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -36,7 +36,7 @@ export function getDefaultSize(size = null, min = 480) { const tmp = min > oWidth ? oWidth : min; return { width: tmp, height: tmp }; } - const isVertical = oWidth > oHeight ? false : true; + const isVertical = oWidth <= oHeight; let dWidth = 0; let dHeight = 0; if (isVertical) { From 6bf64e38fcc1ff57087101ebdc52c7c2a791b003 Mon Sep 17 00:00:00 2001 From: HD Date: Tue, 28 Jun 2022 10:31:19 +0800 Subject: [PATCH 5/7] refactor: remove getNonNullValues, replace with _.ominBy and _.isNull (lodash) ref: https://stackoverflow.com/a/70554734/11408375 --- src/app/slices/channels.ts | 5 +++-- src/app/slices/contacts.ts | 5 +++-- src/common/utils.tsx | 10 +--------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/app/slices/channels.ts b/src/app/slices/channels.ts index 07001c54..6ac87b0e 100644 --- a/src/app/slices/channels.ts +++ b/src/app/slices/channels.ts @@ -1,6 +1,6 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { isNull, omitBy } from "lodash"; import BASE_URL from "../config"; -import { getNonNullValues } from "../../common/utils"; import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel"; interface StoredChannel extends Channel { @@ -70,7 +70,8 @@ const channelsSlice = createSlice({ break; } default: - state.byId[gid] = { ...state.byId[gid]!, ...getNonNullValues(rest) }; + // old code: state.byId[gid] = { ...state.byId[gid]!, ...getNonNullValues(rest) }; + state.byId[gid] = { ...state.byId[gid]!, ...omitBy(rest, isNull) }; break; } }, diff --git a/src/app/slices/contacts.ts b/src/app/slices/contacts.ts index b210a500..75036418 100644 --- a/src/app/slices/contacts.ts +++ b/src/app/slices/contacts.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { getNonNullValues } from "../../common/utils"; +import { isNull, omitBy } from "lodash"; import BASE_URL from "../config"; import { User } from "../../types/auth"; import { UserLog, UserState } from "../../types/sse"; @@ -46,9 +46,10 @@ const contactsSlice = createSlice({ changeLogs.forEach(({ action, uid, ...rest }) => { switch (action) { case "update": { - const vals = getNonNullValues(rest); + const vals = omitBy(rest, isNull); if (state.byId[uid]) { Object.keys(vals).forEach((k) => { + // @ts-ignore state.byId[uid]![k] = vals[k]; if (k == "avatar_updated_at") { state.byId[uid]!.avatar = `${BASE_URL}/resource/avatar?uid=${uid}&t=${vals[k]}`; diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 608b4226..5b87e231 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -20,15 +20,6 @@ export const isTreatAsImage = (file: File) => { return false; }; -export const getNonNullValues = (obj, whiteList = ["log_id"]) => { - const tmp = {}; - Object.keys(obj).forEach((k) => { - if (!whiteList.includes(k) && obj[k] !== null) { - tmp[k] = obj[k]; - } - }); - return tmp; -}; export function getDefaultSize(size = null, min = 480) { if (!size) return { width: 0, height: 0 }; const { width: oWidth, height: oHeight } = size; @@ -48,6 +39,7 @@ export function getDefaultSize(size = null, min = 480) { } return { width: dWidth, height: dHeight }; } + export function formatBytes(bytes, decimals = 2) { if (bytes === 0) return "0 Bytes"; From ccfee28e133562f32e33d4cb2c61cc15157f0499 Mon Sep 17 00:00:00 2001 From: HD Date: Tue, 28 Jun 2022 10:35:11 +0800 Subject: [PATCH 6/7] fix: file size calculation error MDN: The Blob interface's size property returns the size of the Blob or File in bytes --- src/common/utils.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common/utils.tsx b/src/common/utils.tsx index 5b87e231..9a56b776 100644 --- a/src/common/utils.tsx +++ b/src/common/utils.tsx @@ -14,8 +14,7 @@ export const isImage = (file_type = "", size = 0) => { export const isTreatAsImage = (file: File) => { const { type, size } = file; if (type.startsWith("image")) { - // 10MB - return size < 1000 * 1000; + return size < 1024 * 1024; // 10MB } return false; }; From 003353e43fbd35f153071c88b6823e59f6a54ed0 Mon Sep 17 00:00:00 2001 From: HD Date: Wed, 29 Jun 2022 16:54:58 +0800 Subject: [PATCH 7/7] refactor: add typescript support to project --- src/app/services/auth.ts | 8 ++- src/app/services/server.ts | 9 +-- src/common/component/AvatarUploader.tsx | 4 +- src/common/component/ChannelIcon.tsx | 4 +- src/common/component/ChannelModal/index.tsx | 22 +++---- src/common/component/GoogleLoginButton.tsx | 21 +++---- .../component/StyledSettingContainer.tsx | 31 ++++++---- src/common/component/styled/Select.tsx | 33 +++++++--- src/common/hook/useLogout.ts | 4 ++ src/common/hook/useNormalizeMessage.ts | 4 +- src/routes/index.tsx | 11 ++-- src/routes/invite/index.tsx | 57 +++++++++-------- src/routes/onboarding/index.tsx | 13 ++-- src/routes/onboarding/useServerSetup.ts | 15 +++-- src/routes/reg/ExpiredTip.tsx | 1 - src/routes/reg/RegWithUsername.tsx | 61 ++++++++++--------- src/routes/reg/Register.tsx | 25 +++++--- src/routes/reg/index.tsx | 2 - src/routes/resources/Search.tsx | 15 ++++- src/routes/sendMagicLink/index.tsx | 6 +- src/routes/sendMagicLink/styled.tsx | 3 +- src/routes/setting/APIConfig.tsx | 4 +- src/routes/setting/LogoutConfirmModal.tsx | 29 +++++---- src/routes/setting/ProfileBasicEditModal.tsx | 22 +++++-- src/routes/setting/UpdatePasswordModal.tsx | 10 ++- .../setting/config/IssuerList/index.tsx | 30 ++++++--- src/routes/setting/config/Tooltip.tsx | 5 +- src/routes/setting/index.tsx | 21 +++---- src/routes/settingChannel/Overview.tsx | 22 ++++--- src/routes/settingChannel/index.tsx | 59 ++++++++++-------- src/routes/settingChannel/navs.tsx | 19 +++++- src/types/channel.ts | 7 +++ src/types/server.ts | 5 +- 33 files changed, 358 insertions(+), 224 deletions(-) diff --git a/src/app/services/auth.ts b/src/app/services/auth.ts index f836cdf8..008565e2 100644 --- a/src/app/services/auth.ts +++ b/src/app/services/auth.ts @@ -51,7 +51,7 @@ export const authApi = createApi({ } } }), - register: builder.mutation({ + register: builder.mutation({ query: (data) => ({ url: `user/register`, method: "POST", @@ -143,10 +143,12 @@ export const authApi = createApi({ url: `/user/check_email?email=${encodeURIComponent(email)}` }) }), - getCredentials: builder.query({ + // todo: check return type + getCredentials: builder.query({ query: () => ({ url: "/token/credentials" }) }), - logout: builder.query({ + // todo: check return type + logout: builder.query({ query: () => ({ url: "token/logout" }), async onQueryStarted(params, { dispatch, queryFulfilled }) { try { diff --git a/src/app/services/server.ts b/src/app/services/server.ts index 5d66ce09..2a20276a 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -10,11 +10,12 @@ import { LoginConfig, Server, TestEmailDTO, - NewAdminDTO, + CreateAdminDTO, SMTPConfig, AgoraConfig, GithubAuthConfig } from "../../types/server"; + const defaultExpireDuration = 7 * 24 * 60 * 60; export const serverApi = createApi({ @@ -169,7 +170,7 @@ export const serverApi = createApi({ }), updateServer: builder.mutation({ query: (data) => ({ - url: `admin/system/organization`, + url: "admin/system/organization", method: "POST", body: data }), @@ -184,9 +185,9 @@ export const serverApi = createApi({ } } }), - createAdmin: builder.mutation({ + createAdmin: builder.mutation({ query: (data) => ({ - url: `/admin/system/create_admin`, + url: "/admin/system/create_admin", method: "POST", body: data }) diff --git a/src/common/component/AvatarUploader.tsx b/src/common/component/AvatarUploader.tsx index 9e3e334e..2e648c50 100644 --- a/src/common/component/AvatarUploader.tsx +++ b/src/common/component/AvatarUploader.tsx @@ -64,9 +64,9 @@ const StyledWrapper = styled.div` interface Props { url?: string; name?: string; - type?: "user"; + type?: "user" | "channel"; disabled?: boolean; - uploadImage: (file: File) => Promise; + uploadImage: (file: File) => void; } const AvatarUploader: FC = ({ diff --git a/src/common/component/ChannelIcon.tsx b/src/common/component/ChannelIcon.tsx index b469f1ec..b477ff10 100644 --- a/src/common/component/ChannelIcon.tsx +++ b/src/common/component/ChannelIcon.tsx @@ -6,7 +6,7 @@ import LockHashIcon from "../../assets/icons/channel.private.svg"; interface Props { personal?: boolean; muted?: boolean; - className: string; + className?: string; } const Styled = styled.div` @@ -16,7 +16,7 @@ const Styled = styled.div` } `; -const ChannelIcon: FC = ({ personal = false, muted = false, className }) => { +const ChannelIcon: FC = ({ personal = false, muted = false, className = "" }) => { return ( {personal ? : } diff --git a/src/common/component/ChannelModal/index.tsx b/src/common/component/ChannelModal/index.tsx index dfbf2717..34518b57 100644 --- a/src/common/component/ChannelModal/index.tsx +++ b/src/common/component/ChannelModal/index.tsx @@ -10,19 +10,13 @@ import StyledCheckbox from "../styled/Checkbox"; import useFilteredUsers from "../../hook/useFilteredUsers"; import { useCreateChannelMutation } from "../../../app/services/channel"; import { useAppSelector } from "../../../app/store"; +import { CreateChannelDTO } from "../../../types/channel"; interface Props { personal?: boolean; closeModal: () => void; } -interface CreateChannelDTO { - name: string; - description: string; - members?: number[]; - is_public: boolean; -} - const ChannelModal: FC = ({ personal = false, closeModal }) => { const { contactsData, loginUid } = useAppSelector((store) => { return { contactsData: store.contacts.byId, loginUid: store.authData.uid }; @@ -31,7 +25,7 @@ const ChannelModal: FC = ({ personal = false, closeModal }) => { const [data, setData] = useState({ name: "", description: "", - members: [loginUid], + members: loginUid ? [Number(loginUid)] : [], is_public: !personal }); @@ -82,13 +76,15 @@ const ChannelModal: FC = ({ personal = false, closeModal }) => { }; const toggleCheckMember = ({ currentTarget }: MouseEvent) => { - const { members } = data; + const members = data.members ?? []; const { uid } = currentTarget.dataset; - let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid]; + const uidNum = Number(uid); + let tmp = members.includes(uidNum) ? members.filter((m) => m != uidNum) : [...members, uidNum]; setData((prev) => ({ ...prev, members: tmp })); }; - const loginUser = contactsData[loginUid]; + if (!loginUid) return null; + const loginUser = contactsData[Number(loginUid)]; if (!loginUser) return null; const { name, members, is_public } = data; @@ -108,13 +104,13 @@ const ChannelModal: FC = ({ personal = false, closeModal }) => {
      {contacts.map((u) => { const { uid } = u; - const checked = members.includes(uid); + const checked = members ? members.includes(uid) : false; return (
    • = ({ type = "login", clientId }) => { const [login, { isSuccess, isLoading, error }] = useLoginMutation(); - //拿本地存的magic token + // 拿本地存的magic token const magic_token = localStorage.getItem(KEY_LOCAL_MAGIC_TOKEN); const { signIn, loaded } = useGoogleLogin({ onScriptLoadFailure: (wtf) => { console.error("google login script load failure", wtf); }, clientId, - onSuccess: ({ tokenId, ...rest }) => { - console.info("google oauth success", tokenId, rest); - login({ - magic_token, - id_token: tokenId, - type: "google" - }); + onSuccess: (res) => { + if ("code" in res) { + console.error(`google login failed: ${res.code}`); + } else { + login({ magic_token, id_token: res.tokenId, type: "google" }); + } }, onFailure: (wtf) => { console.error("google login failure", wtf); @@ -57,7 +56,7 @@ const GoogleLoginButton: FC = ({ type = "login", clientId }) => { } }, [isSuccess]); useEffect(() => { - if (error) { + if (error && "status" in error) { switch (error.status) { case 410: toast.error( @@ -71,14 +70,14 @@ const GoogleLoginButton: FC = ({ type = "login", clientId }) => { return; } }, [error]); + const handleGoogleLogin = () => { signIn(); }; - // console.log("google login ", loaded); return ( - + {loaded ? `${type === "login" ? "Sign in" : "Sign up"} with Google` : `Initializing`} ); diff --git a/src/common/component/StyledSettingContainer.tsx b/src/common/component/StyledSettingContainer.tsx index b0474ba5..97f2cda7 100644 --- a/src/common/component/StyledSettingContainer.tsx +++ b/src/common/component/StyledSettingContainer.tsx @@ -1,18 +1,21 @@ -import { FC, ReactElement } from "react"; +import { FC, ReactNode } from "react"; import styled from "styled-components"; import { useLocation, NavLink } from "react-router-dom"; import backIcon from "../../assets/icons/arrow.left.svg?url"; +import { Nav } from "../../routes/settingChannel/navs"; const StyledWrapper = styled.div` width: 100vw; height: 100vh; display: flex; + > .left { max-height: 100vh; overflow: scroll; padding: 32px 16px; min-width: 212px; background-color: #f5f6f7; + > .title { cursor: pointer; font-weight: bold; @@ -21,16 +24,16 @@ const StyledWrapper = styled.div` color: #1c1c1e; margin-bottom: 32px; padding-left: 24px; - background: url(${backIcon}); background-size: 16px; - background-repeat: no-repeat; - background-position: left; + background: url(${backIcon}) no-repeat left; } + > .items { display: flex; flex-direction: column; gap: 2px; margin-bottom: 36px; + &:before { padding-left: 12px; content: attr(data-title); @@ -40,31 +43,37 @@ const StyledWrapper = styled.div` color: #6b7280; margin-bottom: 2px; } + .item { font-weight: 500; font-size: 14px; line-height: 20px; color: #44494f; border-radius: 4px; + &:hover, &.curr { background: #e7e5e4; } + > a { display: block; padding: 4px 12px; } } + &.danger .item { cursor: pointer; padding: 4px 12px; color: #ef4444; + &:hover { background: none; } } } } + > .right { background-color: #fff; width: 100%; @@ -72,6 +81,7 @@ const StyledWrapper = styled.div` /* max-height: -webkit-fill-available; */ overflow: auto; padding: 32px; + > .title { font-weight: bold; font-size: 20px; @@ -82,21 +92,18 @@ const StyledWrapper = styled.div` } `; -interface Nav { - name?: string; +export interface Danger { title: string; - // todo - items: []; - component: ReactElement; + handler: () => void; } interface Props { closeModal: () => void; title?: string; navs: Nav[]; - dangers: []; - nav: Nav; - children: ReactElement; + dangers: Danger[]; + nav: { title: string; name?: string; component?: ReactNode }; + children: ReactNode; } const StyledSettingContainer: FC = ({ diff --git a/src/common/component/styled/Select.tsx b/src/common/component/styled/Select.tsx index ec4e4ecd..69e555df 100644 --- a/src/common/component/styled/Select.tsx +++ b/src/common/component/styled/Select.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { FC, useState } from "react"; import styled from "styled-components"; import Tippy from "@tippyjs/react"; import IconSelect from "../../../assets/icons/check.sign.svg"; @@ -26,21 +26,34 @@ const Styled = styled.div` } `; -const CURRENT_NOT_SET = {}; +export interface Option { + icon?: string; + title: string; + value: string; + selected: boolean; + underline?: boolean; +} -export default function Select({ options = [], updateSelect = null, current = CURRENT_NOT_SET }) { +interface Props { + options: Option[]; + updateSelect: null | ((option: Partial
    • - - {(current !== CURRENT_NOT_SET ? current : curr)?.title || `Select`} - + {(current !== null ? current : curr)?.title || "Select"} ); -} +}; + +export default Select; diff --git a/src/common/hook/useLogout.ts b/src/common/hook/useLogout.ts index 28dc54d7..b7f60997 100644 --- a/src/common/hook/useLogout.ts +++ b/src/common/hook/useLogout.ts @@ -11,6 +11,10 @@ import { useLazyLogoutQuery } from "../../app/services/auth"; export default function useLogout() { const dispatch = useDispatch(); const [logout, { isLoading, isSuccess }] = useLazyLogoutQuery(); + // todo: remove batch + // If you're using React 18, you do not need to use the batch API. React 18 automatically + // batches all state updates, no matter where they're queued. + // ref: https://react-redux.js.org/api/batch const clearLocalData = () => { batch(() => { dispatch(resetFootprint()); diff --git a/src/common/hook/useNormalizeMessage.ts b/src/common/hook/useNormalizeMessage.ts index 1d06a7d4..b42c7a3e 100644 --- a/src/common/hook/useNormalizeMessage.ts +++ b/src/common/hook/useNormalizeMessage.ts @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; -import { normalizeArchiveData } from "../utils"; +import { normalizeArchiveData, ArchiveMessage } from "../utils"; import { useLazyGetArchiveMessageQuery } from "../../app/services/message"; export default function useNormalizeMessage() { const [filePath, setFilePath] = useState(null); - const [normalizedMessages, setNormalizedMessages] = useState(null); + const [normalizedMessages, setNormalizedMessages] = useState(null); const [getArchiveMessage, { data, isError, isLoading, isSuccess }] = useLazyGetArchiveMessageQuery(); useEffect(() => { diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 1b9d1cd3..b493cf78 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,6 +1,6 @@ import { Suspense, useEffect, lazy } from "react"; import { Route, Routes, HashRouter } from "react-router-dom"; -import { Provider, useSelector } from "react-redux"; +import { Provider } from "react-redux"; import toast from "react-hot-toast"; import NotFoundPage from "./404"; // import Welcome from './Welcome' @@ -25,16 +25,19 @@ import Meta from "../common/component/Meta"; import HomePage from "./home"; import ChatPage from "./chat"; import Loading from "../common/component/Loading"; -import store from "../app/store"; +import store, { useAppSelector } from "../app/store"; const PageRoutes = () => { - const { ui: { online }, fileMessages } = useSelector((store) => { + const { + ui: { online }, + fileMessages + } = useAppSelector((store) => { return { ui: store.ui, fileMessages: store.fileMessage }; }); // 掉线检测 useEffect(() => { - let toastId = '0'; + let toastId = "0"; if (!online) { toast.error("Network Offline!", { duration: Infinity }); } else { diff --git a/src/routes/invite/index.tsx b/src/routes/invite/index.tsx index 38bf0827..e73d6608 100644 --- a/src/routes/invite/index.tsx +++ b/src/routes/invite/index.tsx @@ -1,79 +1,84 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, FormEvent, ChangeEvent, FC } from "react"; import StyledWrapper from "./styled"; import { Navigate } from "react-router-dom"; import toast from "react-hot-toast"; import BASE_URL from "../../app/config"; import { useRegisterMutation } from "../../app/services/auth"; import { useCheckMagicTokenValidMutation } from "../../app/services/auth"; -import { useSelector } from "react-redux"; +import { useAppSelector } from "../../app/store"; -export default function InvitePage() { - const { token: loginToken } = useSelector((store) => store.authData); +interface AuthForm { + name: string; + email: string; + password: string; +} + +const InvitePage: FC = () => { + const { token: loginToken } = useAppSelector((store) => store.authData); const [secondPwd, setSecondPwd] = useState(""); const [samePwd, setSamePwd] = useState(true); - const [token, setToken] = useState(""); + const [token, setToken] = useState(""); const [valid, setValid] = useState(false); // const [sp] = useSearchParams(); // const navigateTo = useNavigate(); const [register, { data, isLoading, isSuccess, isError, error }] = useRegisterMutation(); const [checkToken, { data: isValid, isLoading: checkLoading, isSuccess: checkSuccess }] = useCheckMagicTokenValidMutation(); + useEffect(() => { - // console.log(search); const query = new URLSearchParams(location.search); setToken(query.get("token")); }, []); + useEffect(() => { if (token) { checkToken(token); } }, [token]); + useEffect(() => { if (checkSuccess) { - console.log({ isValid }); setValid(isValid); } else { setValid(false); } }, [checkSuccess, isValid]); - const [input, setInput] = useState({ - name: "", - email: "", - password: "" - }); + const [input, setInput] = useState({ name: "", email: "", password: "" }); - const handleReg = (evt) => { + const handleReg = (evt: FormEvent) => { evt.preventDefault(); if (!samePwd) { toast.error("two passwords not same"); return; } - console.log("wtf", input); register({ ...input, magic_token: token, gender: 1 }); }; - const handleInput = (evt) => { - const { type } = evt.target.dataset; + + const handleInput = (evt: ChangeEvent) => { + const { type } = evt.target.dataset as { type: keyof AuthForm }; const { value } = evt.target; - console.log(type, value); setInput((prev) => { prev[type] = value; return { ...prev }; }); }; - const handleSecondPwdInput = (evt) => { + + const handleSecondPwdInput = (evt: ChangeEvent) => { const { value } = evt.target; setSecondPwd(value); }; + const handlePwdCheck = () => { if (secondPwd) { setSamePwd(secondPwd == input.password); } }; + useEffect(() => { if (!samePwd) { toast.error("two passwords not same"); @@ -88,8 +93,7 @@ export default function InvitePage() { location.href = `/#/login`; // navigateTo("/login",); }, 500); - } else if (isError) { - console.log("register failed", error); + } else if (isError && error && "data" in error) { switch (error.status) { case 400: toast.error("Register Failed: please check inputs"); @@ -102,6 +106,8 @@ export default function InvitePage() { email_conflict: "email conflict", name_conflict: "name conflict" }; + // todo + // @ts-ignore toast.error(`Register Failed: ${tips[error.data?.reason]}`); break; } @@ -114,9 +120,10 @@ export default function InvitePage() { const { email, password, name } = input; if (loginToken) return ; - if (!token) return "token not found"; - if (checkLoading) return "checking token valid"; - if (!valid) return "invite token expires or invalid"; + if (!token) return <>token not found; + if (checkLoading) return <>checking token valid; + if (!valid) return <>invite token expires or invalid; + return (
      @@ -168,4 +175,6 @@ export default function InvitePage() {
      ); -} +}; + +export default InvitePage; diff --git a/src/routes/onboarding/index.tsx b/src/routes/onboarding/index.tsx index c00252f0..3094a7c6 100644 --- a/src/routes/onboarding/index.tsx +++ b/src/routes/onboarding/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { FC } from "react"; import { Helmet } from "react-helmet"; import { Swiper, SwiperSlide } from "swiper/react"; import "swiper/css"; @@ -11,9 +11,14 @@ import DonePage from "./steps/donePage"; import useServerSetup, { steps } from "./useServerSetup"; import StyledOnboardingPage from "./styled"; -function Navigator({ step, setStep }) { +interface Props { + step: string; + setStep: (step: string) => void; +} + +const Navigator: FC = ({ step, setStep }) => { const index = steps.map((value) => value.name).indexOf(step); - const canJumpTo = steps.find((value) => value.name === step).canJumpTo || []; + const canJumpTo = steps.find((value) => value.name === step)?.canJumpTo || []; return (
      @@ -41,7 +46,7 @@ function Navigator({ step, setStep }) { })}
      ); -} +}; export default function OnboardingPage() { const serverSetup = useServerSetup(); diff --git a/src/routes/onboarding/useServerSetup.ts b/src/routes/onboarding/useServerSetup.ts index 56d3fe90..2ac07d89 100644 --- a/src/routes/onboarding/useServerSetup.ts +++ b/src/routes/onboarding/useServerSetup.ts @@ -1,7 +1,14 @@ import { useCallback, useState } from "react"; +import { Swiper } from "swiper/types"; // `name` for in-code usage, `label` for display -const steps = [ +export interface Step { + name: string; + label: string; + canJumpTo?: string[]; +} + +const steps: Step[] = [ { name: "welcomePage", label: "Welcome Page" @@ -31,12 +38,12 @@ const steps = [ ]; export default function useServerSetup() { - const [swiper, setSwiper] = useState(null); + const [swiper, setSwiper] = useState(null); - const [index, setIndex] = useState(0); + const [index, setIndex] = useState(0); const step = steps[index].name; const setStep = useCallback( - (name) => { + (name: string) => { const newIndex = steps.map((step) => step.name).indexOf(name); setIndex(newIndex); if (swiper !== null) { diff --git a/src/routes/reg/ExpiredTip.tsx b/src/routes/reg/ExpiredTip.tsx index fa295df8..ad8cea60 100644 --- a/src/routes/reg/ExpiredTip.tsx +++ b/src/routes/reg/ExpiredTip.tsx @@ -1,4 +1,3 @@ -// import React from "react"; import styled from "styled-components"; const Styled = styled.div` diff --git a/src/routes/reg/RegWithUsername.tsx b/src/routes/reg/RegWithUsername.tsx index 7221778c..833d7004 100644 --- a/src/routes/reg/RegWithUsername.tsx +++ b/src/routes/reg/RegWithUsername.tsx @@ -1,17 +1,15 @@ -/* eslint-disable no-undef */ -import { useState, useEffect } from "react"; +import { useState, useEffect, FC, FormEvent, ChangeEvent } from "react"; import { useDispatch } from "react-redux"; import { useParams } from "react-router-dom"; +import toast from "react-hot-toast"; import { setAuthData } from "../../app/slices/auth.data"; - import Input from "../../common/component/styled/Input"; import Button from "../../common/component/styled/Button"; import { useLoginMutation, useCheckMagicTokenValidMutation } from "../../app/services/auth"; -import toast from "react-hot-toast"; import ExpiredTip from "./ExpiredTip"; import { useRegisterMutation } from "../../app/services/auth"; -export default function RegWithUsername() { +const RegWithUsername: FC = () => { const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] = useCheckMagicTokenValidMutation(); const [ @@ -28,7 +26,8 @@ export default function RegWithUsername() { const [username, setUsername] = useState(""); const query = new URLSearchParams(location.search); // const githubCode = query.get("gcode"); - const token = query.get("magic_token"); + // todo: check if query param exists + const token = query.get("magic_token") as string; useEffect(() => { if (token) { checkTokenInvalid(token); @@ -36,25 +35,29 @@ export default function RegWithUsername() { }, [token]); useEffect(() => { - switch (loginError?.status) { - case 401: - toast.error("Invalided Token"); - break; - - default: - break; + if (loginError && "status" in loginError) { + switch (loginError.status) { + case 401: + toast.error("Invalided Token"); + break; + default: + break; + } } }, [loginError]); - useEffect(() => { - switch (regError?.status) { - case 409: - toast.error("Something Conflicted!"); - break; - default: - break; + useEffect(() => { + if (regError && "status" in regError) { + switch (regError.status) { + case 409: + toast.error("Something Conflicted!"); + break; + default: + break; + } } }, [regError]); + useEffect(() => { const isSuccess = loginSuccess || regSuccess; const data = loginData || regData; @@ -67,7 +70,7 @@ export default function RegWithUsername() { } }, [loginSuccess, regSuccess, loginData, regData]); - const handleAuth = (evt) => { + const handleAuth = (evt: FormEvent) => { evt.preventDefault(); if (from == "reg") { register({ @@ -83,13 +86,12 @@ export default function RegWithUsername() { } }; - const handleInput = (evt) => { - const { value } = evt.target; - setUsername(value); + const handleInput = (evt: ChangeEvent) => { + setUsername(evt.target.value); }; - console.log("ffff", from); - if (!token) return "No Token"; - if (checkingToken) return "Checking Magic Link..."; + + if (!token) return <>"No Token"; + if (checkingToken) return <>"Checking Magic Link..."; if (!isTokenValid) return ; const isLoading = loginLoading || regLoading; const isSuccess = loginSuccess || regSuccess; @@ -112,9 +114,12 @@ export default function RegWithUsername() { onChange={handleInput} /> ); -} +}; + +export default RegWithUsername; diff --git a/src/routes/reg/Register.tsx b/src/routes/reg/Register.tsx index cefb98b5..9c24d018 100644 --- a/src/routes/reg/Register.tsx +++ b/src/routes/reg/Register.tsx @@ -1,6 +1,5 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, ChangeEvent, FormEvent } from "react"; import toast from "react-hot-toast"; -// import { useNavigate } from "react-router-dom"; import BASE_URL, { KEY_LOCAL_MAGIC_TOKEN } from "../../app/config"; import Input from "../../common/component/styled/Input"; import Button from "../../common/component/styled/Button"; @@ -13,13 +12,19 @@ import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig"; import GoogleLoginButton from "../../common/component/GoogleLoginButton"; import GithubLoginButton from "../../common/component/GithubLoginButton"; +interface AuthForm { + email: string; + password: string; + confirmPassword: string; +} + export default function Reg() { const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] = useSendRegMagicLinkMutation(); const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery(); // const navigateTo = useNavigate(); const [magicToken, setMagicToken] = useState(""); - const [input, setInput] = useState({ + const [input, setInput] = useState({ email: "", password: "", confirmPassword: "" @@ -47,7 +52,7 @@ export default function Reg() { } }, [isSuccess, data]); - const handleReg = async (evt) => { + const handleReg = async (evt: FormEvent) => { evt.preventDefault(); const { email, password, confirmPassword } = input; if (password !== confirmPassword) { @@ -55,7 +60,6 @@ export default function Reg() { return; } const { data: canReg } = await checkEmail(email); - console.log("can reg", canReg); if (canReg) { sendRegMagicLink({ magic_token: magicToken, @@ -67,21 +71,23 @@ export default function Reg() { } // sendMagicLink(email); }; + const handleCompare = () => { const { password, confirmPassword } = input; if (password !== confirmPassword) { toast.error("Not Same Password!"); } }; - const handleInput = (evt) => { - const { type } = evt.target.dataset; + + const handleInput = (evt: ChangeEvent) => { + const { type } = evt.target.dataset as { type: keyof AuthForm }; const { value } = evt.target; - // console.log(type, value); setInput((prev) => { prev[type] = value; return { ...prev }; }); }; + const { clientId } = useGoogleAuthConfig(); const { config: githubAuthConfig } = useGithubAuthConfig(); const { data: loginConfig, isSuccess: loginConfigSuccess } = useGetLoginConfigQuery(); @@ -94,10 +100,11 @@ export default function Reg() { const googleLogin = enableGoogleLogin && clientId; // magic token 没有并且没有开放注册 if (whoCanSignUp !== "EveryOne" && !magicToken) - return `Sign up method is updated to Invitation Link Only`; + return <>Sign up method is updated to Invitation Link Only; const { email, password, confirmPassword } = input; if (data?.mail_is_sent) return ; const isLoading = signingUp || checkingEmail; + return ( <>
      diff --git a/src/routes/reg/index.tsx b/src/routes/reg/index.tsx index a47c69a5..5216c5b1 100644 --- a/src/routes/reg/index.tsx +++ b/src/routes/reg/index.tsx @@ -2,8 +2,6 @@ import { Outlet } from "react-router-dom"; import StyledWrapper from "./styled"; export default function RegContainer() { - // const isRegHome = useMatch(`/register`); - return (
      diff --git a/src/routes/resources/Search.tsx b/src/routes/resources/Search.tsx index 11f21796..d8ee3173 100644 --- a/src/routes/resources/Search.tsx +++ b/src/routes/resources/Search.tsx @@ -1,3 +1,4 @@ +import { ChangeEvent, FC } from "react"; import styled from "styled-components"; import iconSearch from "../../assets/icons/search.svg?url"; @@ -24,8 +25,14 @@ const Styled = styled.div` } `; -export default function Search({ value = "", updateSearchValue = null, embed = false }) { - const handleChange = (evt) => { +interface Props { + value?: string; + updateSearchValue?: (value: string) => void; + embed?: boolean; +} + +const Search: FC = ({ value = "", updateSearchValue = null, embed = false }) => { + const handleChange = (evt: ChangeEvent) => { if (updateSearchValue) { updateSearchValue(evt.target.value); } @@ -36,4 +43,6 @@ export default function Search({ value = "", updateSearchValue = null, embed = f ); -} +}; + +export default Search; diff --git a/src/routes/sendMagicLink/index.tsx b/src/routes/sendMagicLink/index.tsx index 575722a7..f20cbbe7 100644 --- a/src/routes/sendMagicLink/index.tsx +++ b/src/routes/sendMagicLink/index.tsx @@ -22,8 +22,7 @@ export default function SendMagicLinkPage() { }, [isSuccess]); useEffect(() => { - if (error) { - console.log(error); + if (error && "status" in error) { switch (error.status) { case "PARSING_ERROR": toast.error(error.data); @@ -32,7 +31,7 @@ export default function SendMagicLinkPage() { toast.error("Username or Password Incorrect"); break; case 404: - toast.error("Account not exsit"); + toast.error("Account not exist"); break; default: toast.error("Something Error"); @@ -53,7 +52,6 @@ export default function SendMagicLinkPage() { const handleInput = (evt: ChangeEvent) => { const { value } = evt.target; - console.log(value); setEmail(value); }; diff --git a/src/routes/sendMagicLink/styled.tsx b/src/routes/sendMagicLink/styled.tsx index cd232fbd..883c3c12 100644 --- a/src/routes/sendMagicLink/styled.tsx +++ b/src/routes/sendMagicLink/styled.tsx @@ -1,4 +1,5 @@ import styled from "styled-components"; + const StyledWrapper = styled.div` display: flex; justify-content: center; @@ -8,7 +9,7 @@ const StyledWrapper = styled.div` max-width: 440px; padding: 32px 40px 32px 40px; /* border: 1px solid #eee; */ - box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); + box-shadow: 0 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06); border-radius: 12px; .tips { display: flex; diff --git a/src/routes/setting/APIConfig.tsx b/src/routes/setting/APIConfig.tsx index c09f1509..f3d91a91 100644 --- a/src/routes/setting/APIConfig.tsx +++ b/src/routes/setting/APIConfig.tsx @@ -86,10 +86,10 @@ export default function APIConfig() { Are you sure to update API secret? Previous secret will be invalided
      - -
      diff --git a/src/routes/setting/LogoutConfirmModal.tsx b/src/routes/setting/LogoutConfirmModal.tsx index aa165059..a6a3567e 100644 --- a/src/routes/setting/LogoutConfirmModal.tsx +++ b/src/routes/setting/LogoutConfirmModal.tsx @@ -1,12 +1,12 @@ -// import React from "react"; -import { useEffect, useState } from "react"; +import { ChangeEvent, FC, useEffect, useState } from "react"; import styled from "styled-components"; -// import { useNavigate } from "react-router-dom"; - +import toast from "react-hot-toast"; +import Modal from "../../common/component/Modal"; import StyledModal from "../../common/component/styled/Modal"; import Button from "../../common/component/styled/Button"; import Checkbox from "../../common/component/styled/Checkbox"; import useLogout from "../../common/hook/useLogout"; + const StyledConfirm = styled(StyledModal)` .clear { font-weight: normal; @@ -26,21 +26,25 @@ const StyledConfirm = styled(StyledModal)` } } `; -import Modal from "../../common/component/Modal"; -import toast from "react-hot-toast"; -export default function LogoutConfirmModal({ closeModal }) { + +interface Props { + closeModal: () => void; +} + +const LogoutConfirmModal: FC = ({ closeModal }) => { const [clearLocal, setClearLocal] = useState(false); const { logout, exited, exiting, clearLocalData } = useLogout(); const handleLogout = () => { logout(); }; - const handleCheck = (evt) => { + + const handleCheck = (evt: ChangeEvent) => { setClearLocal(evt.target.checked); }; + useEffect(() => { if (exited) { if (clearLocal) { - console.log("clear all store"); clearLocalData(); } toast.success("Logout Successfully"); @@ -50,6 +54,7 @@ export default function LogoutConfirmModal({ closeModal }) { // location.reload(); } }, [exited, clearLocal]); + return ( } @@ -74,4 +79,6 @@ export default function LogoutConfirmModal({ closeModal }) { ); -} +}; + +export default LogoutConfirmModal; diff --git a/src/routes/setting/ProfileBasicEditModal.tsx b/src/routes/setting/ProfileBasicEditModal.tsx index dda5e841..92956bab 100644 --- a/src/routes/setting/ProfileBasicEditModal.tsx +++ b/src/routes/setting/ProfileBasicEditModal.tsx @@ -1,5 +1,4 @@ -// import React from "react"; -import { useEffect, useState } from "react"; +import { ChangeEvent, FC, useEffect, useState } from "react"; import styled from "styled-components"; import toast from "react-hot-toast"; import Input from "../../common/component/styled/Input"; @@ -24,18 +23,27 @@ const StyledEdit = styled(StyledModal)` } `; -export default function ProfileBasicEditModal({ +interface Props { + label?: string; + valueKey?: string; + value?: string; + title?: string; + intro?: string; + closeModal: () => void; +} + +const ProfileBasicEditModal: FC = ({ label = "Username", valueKey = "name", value = "", title = "Change your username", intro = "Enter a new username and your existing password.", closeModal -}) { +}) => { const [input, setInput] = useState(value); // const dispatch = useDispatch(); const [update, { isLoading, isSuccess }] = useUpdateInfoMutation(); - const handleChange = (evt) => { + const handleChange = (evt: ChangeEvent) => { setInput(evt.target.value); }; const handleUpdate = () => { @@ -69,4 +77,6 @@ export default function ProfileBasicEditModal({ ); -} +}; + +export default ProfileBasicEditModal; diff --git a/src/routes/setting/UpdatePasswordModal.tsx b/src/routes/setting/UpdatePasswordModal.tsx index 976f401e..0b286ef6 100644 --- a/src/routes/setting/UpdatePasswordModal.tsx +++ b/src/routes/setting/UpdatePasswordModal.tsx @@ -27,9 +27,15 @@ interface Props { closeModal: () => void; } +interface BaseForm { + current: string; + newPassword: string; + confirmPassword: string; +} + const ProfileBasicEditModal: FC = ({ closeModal }) => { const { data } = useGetCredentialsQuery(); - const [input, setInput] = useState({ + const [input, setInput] = useState({ current: "", newPassword: "", confirmPassword: "" @@ -37,7 +43,7 @@ const ProfileBasicEditModal: FC = ({ closeModal }) => { // const dispatch = useDispatch(); const [updatePassword, { isLoading, isSuccess }] = useUpdatePasswordMutation(); const handleChange = (evt: ChangeEvent) => { - const { type } = evt.target.dataset; + const { type } = evt.target.dataset as { type: keyof BaseForm }; setInput((prev) => { return { ...prev, [type]: evt.target.value }; }); diff --git a/src/routes/setting/config/IssuerList/index.tsx b/src/routes/setting/config/IssuerList/index.tsx index f28684dd..cd267553 100644 --- a/src/routes/setting/config/IssuerList/index.tsx +++ b/src/routes/setting/config/IssuerList/index.tsx @@ -1,15 +1,25 @@ -import { ChangeEvent, useState } from "react"; -import Select from "../../../../common/component/styled/Select"; +import { ChangeEvent, FC, useState } from "react"; +import Select, { Option } from "../../../../common/component/styled/Select"; import Button from "../../../../common/component/styled/Button"; import Input from "../../../../common/component/styled/Input"; import Toggle from "../../../../common/component/styled/Toggle"; import options from "./items.json"; import Styled from "./styled"; -// import IconPlus from "../../../../assets/icons/plus.circle.svg"; import IconMinus from "../../../../assets/icons/minus.circle.svg"; -export default function IssuerList({ issuers = [], onChange }) { - const [select, setSelect] = useState({}); +interface Issuer { + domain: string; + enable: boolean; + favicon: string; +} + +interface Props { + issuers: Issuer[]; + onChange: (issuers: Issuer[]) => void; +} + +const IssuerList: FC = ({ issuers = [], onChange }) => { + const [select, setSelect] = useState | null>(null); const [newDomain, setNewDomain] = useState(""); const handleNewDomain = (evt: ChangeEvent) => { @@ -87,7 +97,9 @@ export default function IssuerList({ issuers = [], onChange }) {