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}/`