From 768100a0db6db6a6086434c8aaec1ee35befdd4d Mon Sep 17 00:00:00 2001 From: HD Date: Wed, 22 Jun 2022 16:29:21 +0800 Subject: [PATCH 1/4] chore: typo, conactsData -> contactsData, dsecription -> description, remove console.log --- src/common/component/ChannelModal/index.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/common/component/ChannelModal/index.tsx b/src/common/component/ChannelModal/index.tsx index cf61127b..4af65e47 100644 --- a/src/common/component/ChannelModal/index.tsx +++ b/src/common/component/ChannelModal/index.tsx @@ -14,13 +14,13 @@ import { useCreateChannelMutation } from "../../../app/services/channel"; import { useAppSelector } from "../../../app/store"; export default function ChannelModal({ personal = false, closeModal }) { - const { conactsData, loginUid } = useAppSelector((store) => { - return { conactsData: store.contacts.byId, loginUid: store.authData.uid }; + const { contactsData, loginUid } = useAppSelector((store) => { + return { contactsData: store.contacts.byId, loginUid: store.authData.uid }; }); const navigateTo = useNavigate(); const [data, setData] = useState({ name: "", - dsecription: "", + description: "", members: [loginUid], is_public: !personal }); @@ -71,14 +71,12 @@ export default function ChannelModal({ personal = false, closeModal }) { const { members } = data; const { uid } = currentTarget.dataset; let tmp = members.includes(+uid) ? members.filter((m) => m != uid) : [...members, +uid]; - console.log(uid, currentTarget); setData((prev) => { return { ...prev, members: tmp }; }); - console.log({ data }); }; - console.log("contacts", contacts); - const loginUser = conactsData[loginUid]; + + const loginUser = contactsData[loginUid]; if (!loginUser) return null; const { name, members, is_public } = data; return ( @@ -98,7 +96,6 @@ export default function ChannelModal({ personal = false, closeModal }) { {contacts.map((u) => { const { uid } = u; const checked = members.includes(uid); - console.log({ checked }); return (
  • Date: Wed, 22 Jun 2022 17:35:30 +0800 Subject: [PATCH 2/4] refactor: add typescript definition and fix type error(null -> undefined) --- src/common/component/Contact/index.tsx | 29 +++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/common/component/Contact/index.tsx b/src/common/component/Contact/index.tsx index 0019ac4c..46e23efc 100644 --- a/src/common/component/Contact/index.tsx +++ b/src/common/component/Contact/index.tsx @@ -1,3 +1,4 @@ +import { FC } from "react"; import { useNavigate } from "react-router-dom"; import Tippy from "@tippyjs/react"; import IconOwner from "../../../assets/icons/owner.svg"; @@ -8,17 +9,29 @@ import StyledWrapper from "./styled"; import useContextMenu from "../../hook/useContextMenu"; import { useAppSelector } from "../../../app/store"; -export default function Contact({ - cid = null, +interface Props { + cid: number; + uid: number; + owner?: number; + dm?: boolean; + interactive?: boolean; + popover?: boolean; + compact?: boolean; + avatarSize?: number; + enableContextMenu?: boolean; +} + +const Contact: FC = ({ + cid, + uid, owner = false, dm = false, interactive = true, - uid = "", popover = false, compact = false, avatarSize = 32, enableContextMenu = false -}) { +}) => { const navigate = useNavigate(); const { visible: contextMenuVisible, handleContextMenuEvent, hideContextMenu } = useContextMenu(); const curr = useAppSelector((store) => store.contacts.byId[uid]); @@ -43,10 +56,10 @@ export default function Contact({ content={} >
    @@ -58,4 +71,6 @@ export default function Contact({ ); -} +}; + +export default Contact; From b66919114dd25d14ea87722107cfece49e722333 Mon Sep 17 00:00:00 2001 From: HD Date: Wed, 22 Jun 2022 17:36:05 +0800 Subject: [PATCH 3/4] refactor: add typescript definition --- src/common/component/Contact/ContextMenu.tsx | 100 ++++++++++--------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/src/common/component/Contact/ContextMenu.tsx b/src/common/component/Contact/ContextMenu.tsx index f4401258..628ad95e 100644 --- a/src/common/component/Contact/ContextMenu.tsx +++ b/src/common/component/Contact/ContextMenu.tsx @@ -1,8 +1,18 @@ +import { FC, ReactElement } from "react"; import Tippy from "@tippyjs/react"; import useContactOperation from "../../hook/useContactOperation"; import ContextMenu from "../ContextMenu"; -export default function ContactContextMenu({ enable = false, uid, cid, visible, hide, children }) { +interface Props { + enable?: boolean; + uid: number; + cid: number; + visible: boolean; + hide: () => void; + children: ReactElement; +} + +const ContactContextMenu: FC = ({ enable = false, uid, cid, visible, hide, children }) => { const { canCall, call, @@ -18,48 +28,48 @@ export default function ContactContextMenu({ enable = false, uid, cid, visible, cid }); return ( - <> - - } - > - {children} - - + + } + > + {children} + ); -} +}; + +export default ContactContextMenu; From e0bbbf4f30561732be3b6cd3bac55807f209a5ca Mon Sep 17 00:00:00 2001 From: HD Date: Thu, 23 Jun 2022 22:58:13 +0800 Subject: [PATCH 4/4] refactor: add typescript definition and fix some type error --- src/app/services/auth.ts | 22 ++- src/app/slices/auth.data.ts | 16 +- src/app/slices/channels.ts | 46 ++--- src/app/slices/footprint.ts | 76 ++++---- src/app/slices/server.ts | 32 +++- src/common/component/Avatar.tsx | 15 +- src/common/component/AvatarUploader.tsx | 25 ++- src/common/component/BlankPlaceholder.tsx | 19 +- src/common/component/ContextMenu.tsx | 2 +- src/common/component/DeleteMessageConfirm.tsx | 5 +- src/common/component/FAQ.tsx | 1 - src/common/component/FileBox/index.tsx | 43 +++-- .../component/FileBox/preview/Audio.tsx | 12 +- src/common/component/FileBox/preview/Code.tsx | 12 +- src/common/component/FileBox/preview/Doc.tsx | 12 +- .../component/FileBox/preview/Image.tsx | 15 +- src/common/component/FileBox/preview/Pdf.tsx | 12 +- .../component/FileBox/preview/Video.tsx | 13 +- .../component/FileMessage/ImageMessage.tsx | 27 +-- src/common/component/GithubLoginButton.tsx | 20 ++- src/common/component/ImagePreviewModal.tsx | 45 +++-- src/common/component/Loading.tsx | 38 ++-- src/common/component/Modal.tsx | 22 ++- src/common/component/ReactionItem.tsx | 27 ++- src/common/component/SaveTip.tsx | 4 +- src/common/component/Search.tsx | 3 +- src/common/component/Server.tsx | 2 +- src/common/component/UserGuide/index.tsx | 21 ++- src/common/component/styled/Modal.tsx | 14 +- src/common/hook/useChatScroll.ts | 5 +- src/common/hook/useContextMenu.tsx | 20 ++- src/common/hook/useRemoveLocalMessage.ts | 18 +- src/common/hook/useSendMessage.ts | 45 +++-- src/react-app-env.d.ts | 74 +++++++- src/routes/settingChannel/navs.tsx | 6 +- src/types/auth.ts | 3 - src/types/channel.ts | 18 +- src/types/global.d.ts | 8 +- src/types/sse.ts | 166 +++++++++++------- 39 files changed, 640 insertions(+), 324 deletions(-) diff --git a/src/app/services/auth.ts b/src/app/services/auth.ts index 357ee71f..f273f6da 100644 --- a/src/app/services/auth.ts +++ b/src/app/services/auth.ts @@ -3,7 +3,7 @@ import { nanoid } from "@reduxjs/toolkit"; import baseQuery from "./base.query"; import { setAuthData, updateToken, resetAuthData, updateInitialized } from "../slices/auth.data"; import BASE_URL, { KEY_DEVICE_KEY } from "../config"; -import { AuthData } from '../../types/auth'; +import { AuthData } from "../../types/auth"; const getDeviceId = () => { let d = localStorage.getItem(KEY_DEVICE_KEY); @@ -18,7 +18,7 @@ export const authApi = createApi({ reducerPath: "authApi", baseQuery, endpoints: (builder) => ({ - login: builder.mutation({ + login: builder.mutation({ query: (credentials) => ({ url: "token/login", method: "POST", @@ -30,17 +30,21 @@ export const authApi = createApi({ }), transformResponse: (data: AuthData) => { const { avatar_updated_at } = data.user; - data.user.avatar = - avatar_updated_at == 0 - ? "" - : `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}`; - return data; + return { + ...data, + user: { + ...data.user, + avatar: + avatar_updated_at == 0 + ? "" + : `${BASE_URL}/resource/avatar?uid=${data.user.uid}&t=${avatar_updated_at}` + } + }; }, async onQueryStarted(params, { dispatch, queryFulfilled }) { try { const { data } = await queryFulfilled; if (data) { - console.log("login resp", data); dispatch(setAuthData(data)); } } catch { @@ -119,7 +123,7 @@ export const authApi = createApi({ }, url: `user/send_login_magic_link?email=${encodeURIComponent(email)}`, method: "POST", - responseHandler: (response) => response.text() + responseHandler: (response: Response) => response.text() // body: { email } }) }), diff --git a/src/app/slices/auth.data.ts b/src/app/slices/auth.data.ts index ed8446a5..224fdb5e 100644 --- a/src/app/slices/auth.data.ts +++ b/src/app/slices/auth.data.ts @@ -1,26 +1,29 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { KEY_EXPIRE, KEY_PWA_INSTALLED, KEY_REFRESH_TOKEN, KEY_TOKEN, KEY_UID } from "../config"; -import { AuthData, AuthToken } from "../../types/auth"; +import { AuthData, AuthToken, User } from "../../types/auth"; interface State { initialized: boolean; uid: string | null; + user: User | null; token: string | null; - expireTime: string | number; // todo + expireTime: number; refreshToken: string | null; } const initialState: State = { initialized: true, uid: null, + user: null, token: localStorage.getItem(KEY_TOKEN), - expireTime: localStorage.getItem(KEY_EXPIRE) || +new Date(), + expireTime: Number(localStorage.getItem(KEY_EXPIRE) || +new Date()), refreshToken: localStorage.getItem(KEY_REFRESH_TOKEN) }; const emptyState: State = { initialized: true, uid: null, + user: null, token: null, expireTime: +new Date(), refreshToken: null @@ -30,20 +33,20 @@ const authDataSlice = createSlice({ name: "authData", initialState, reducers: { - setAuthData(state, action: PayloadAction) { + setAuthData(state, { payload }: PayloadAction) { const { initialized = true, user: { uid }, token, refresh_token, expired_in = 0 - } = action.payload; + } = payload; state.initialized = initialized; state.uid = `${uid}`; + state.user = payload.user; state.token = token; state.refreshToken = refresh_token; // 当前时间往后推expire时长 - console.log("expire", expired_in); const expireTime = +new Date() + Number(expired_in) * 1000; state.expireTime = expireTime; // set local data @@ -53,7 +56,6 @@ const authDataSlice = createSlice({ localStorage.setItem(KEY_UID, `${uid}`); }, resetAuthData() { - console.log("clear auth data"); // remove local data localStorage.removeItem(KEY_EXPIRE); localStorage.removeItem(KEY_TOKEN); diff --git a/src/app/slices/channels.ts b/src/app/slices/channels.ts index 024f64d7..07001c54 100644 --- a/src/app/slices/channels.ts +++ b/src/app/slices/channels.ts @@ -1,11 +1,15 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import BASE_URL from '../config'; -import { getNonNullValues } from '../../common/utils'; -import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from '../../types/channel'; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import BASE_URL from "../config"; +import { getNonNullValues } from "../../common/utils"; +import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel"; + +interface StoredChannel extends Channel { + icon: string; +} interface State { ids: number[]; - byId: { [id: number]: Channel | undefined }; + byId: { [id: number]: StoredChannel | undefined }; } const initialState: State = { @@ -23,16 +27,15 @@ const channelsSlice = createSlice({ fullfillChannels(state, action: PayloadAction) { const channels = action.payload || []; state.ids = channels.map(({ gid }) => gid); - state.byId = Object.fromEntries( - channels.map((c) => { - const { gid, avatar_updated_at } = c; - c.icon = - avatar_updated_at == 0 - ? '' - : `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`; - return [gid, c]; - }) - ); + channels.forEach((c) => { + state.byId[c.gid] = { + ...c, + icon: + c.avatar_updated_at == 0 + ? "" + : `${BASE_URL}/resource/group_avatar?gid=${c.gid}&t=${c.avatar_updated_at}` + }; + }); }, addChannel(state, action: PayloadAction) { const ch = action.payload; @@ -44,26 +47,23 @@ const channelsSlice = createSlice({ ...ch, icon: avatar_updated_at == 0 - ? '' + ? "" : `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}` }; }, updateChannel(state, action: PayloadAction) { - const ignoreInPublic = ['add_member', 'remove_member']; + const ignoreInPublic = ["add_member", "remove_member"]; const { gid, operation, members = [], ...rest } = action.payload; const currChannel = state.byId[gid]; - if ( - !currChannel || - (currChannel.is_public && ignoreInPublic.includes(operation)) - ) return; + if (!currChannel || (currChannel.is_public && ignoreInPublic.includes(operation))) return; switch (operation) { - case 'remove_member': { + case "remove_member": { state.byId[gid]!.members = state.byId[gid]!.members.filter( (id) => members.findIndex((uid) => uid == id) == -1 ); break; } - case 'add_member': { + case "add_member": { const currs = state.byId[gid]!.members; const _set = new Set([...currs, ...members]); state.byId[gid]!.members = Array.from(_set); diff --git a/src/app/slices/footprint.ts b/src/app/slices/footprint.ts index 26dda682..989da516 100644 --- a/src/app/slices/footprint.ts +++ b/src/app/slices/footprint.ts @@ -1,12 +1,13 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { MuteChannel, MuteUser } from "../../types/sse"; export interface State { usersVersion: number; afterMid: number; - readUsers: {}; - readChannels: {}; - muteUsers: {}; - muteChannels: {}; + readUsers: { [uid: number]: number }; + readChannels: { [gid: number]: number }; + muteUsers: { [uid: number]: { expired_at: number } | undefined }; + muteChannels: { [gid: number]: { expired_at: number } | undefined }; } const initialState: State = { @@ -18,6 +19,13 @@ const initialState: State = { muteChannels: {} }; +interface UpdateMutePayload { + remove_users: number[]; + remove_groups: number[]; + add_users: MuteUser[]; + add_groups: MuteChannel[]; +} + const footprintSlice = createSlice({ name: "footprint", initialState, @@ -49,55 +57,51 @@ const footprintSlice = createSlice({ updateAfterMid(state, action: PayloadAction) { state.afterMid = action.payload; }, - updateMute(state, action) { + updateMute(state, action: PayloadAction) { const payload = action.payload || {}; Object.keys(payload).forEach((key) => { switch (key) { - case "remove_users": - { - const uids = payload.remove_users; - uids.forEach((id) => { - delete state.muteUsers[id]; - }); - } + case "remove_users": { + const uids = payload.remove_users; + uids.forEach((id) => { + delete state.muteUsers[id]; + }); break; - case "remove_groups": - { - const gids = payload.remove_groups; - gids.forEach((id) => { - delete state.muteChannels[id]; - }); - } + } + case "remove_groups": { + const gids = payload.remove_groups; + gids.forEach((id) => { + delete state.muteChannels[id]; + }); break; - case "add_users": - { - const mutes = payload.add_users; - mutes.forEach(({ uid, expired_in = null }) => { - state.muteUsers[uid] = { expired_in }; - }); - } + } + case "add_users": { + const mutes = payload.add_users; + mutes.forEach(({ uid, expired_at }) => { + state.muteUsers[uid] = { expired_at }; + }); break; - case "add_groups": - { - const mutes = payload.add_groups; - mutes.forEach(({ gid, expired_in = null }) => { - state.muteChannels[gid] = { expired_in }; - }); - } + } + case "add_groups": { + const mutes = payload.add_groups; + mutes.forEach(({ gid, expired_at }) => { + state.muteChannels[gid] = { expired_at }; + }); break; + } default: break; } }); }, - updateReadUsers(state, action) { + updateReadUsers(state, action: PayloadAction<{ uid: number; mid: number }[]>) { const reads = action.payload || []; if (reads.length == 0) return; reads.forEach(({ uid, mid }) => { state.readUsers[uid] = mid; }); }, - updateReadChannels(state, action) { + updateReadChannels(state, action: PayloadAction<{ gid: number; mid: number }[]>) { const reads = action.payload || []; if (reads.length == 0) return; reads.forEach(({ gid, mid }) => { diff --git a/src/app/slices/server.ts b/src/app/slices/server.ts index e85e6460..e5d7593d 100644 --- a/src/app/slices/server.ts +++ b/src/app/slices/server.ts @@ -1,5 +1,16 @@ -import { createSlice } from "@reduxjs/toolkit"; -const initialState = { +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + name: string; + description: string; + logo: string; + inviteLink: { + link: string; + expire: number; + }; +} + +const initialState: State = { name: "", description: "", logo: "", @@ -8,6 +19,7 @@ const initialState = { expire: 0 } }; + const serverSlice = createSlice({ name: "server", initialState, @@ -15,24 +27,28 @@ const serverSlice = createSlice({ resetServer() { return initialState; }, - fullfillServer(state, action) { + fullfillServer(state, action: PayloadAction) { const { inviteLink = { link: "", expire: 0 }, + logo = "", // todo: check missed logo property name = "", description = "" } = action.payload || {}; - return { name, description, inviteLink }; + return { name, logo, description, inviteLink }; }, - updateInfo(state, action) { + updateInfo(state, action: PayloadAction>) { const values = action.payload || {}; - Object.keys(values).forEach((_key) => { - state[_key] = values[_key]; - }); + // todo: check and remove old logic + // Object.keys(values).forEach((_key) => { + // state[_key] = values[_key]; + // }); + state = { ...state, ...values }; } } }); + export const { updateInfo, resetServer, fullfillServer } = serverSlice.actions; export default serverSlice.reducer; diff --git a/src/common/component/Avatar.tsx b/src/common/component/Avatar.tsx index 7d663695..f834fbba 100644 --- a/src/common/component/Avatar.tsx +++ b/src/common/component/Avatar.tsx @@ -1,10 +1,17 @@ -import { useState, useEffect, memo } from "react"; +import { useState, useEffect, memo, SyntheticEvent, FC } from "react"; import { getInitials, getInitialsAvatar } from "../utils"; -const Avatar = ({ url = "", name = "unkonw name", type = "user", ...rest }) => { +interface Props { + url?: string; + name?: string; + type?: "user" | "channel"; +} + +const Avatar: FC = ({ url = "", name = "unknown name", type = "user", ...rest }) => { // console.log("avatar url", url); const [src, setSrc] = useState(""); - const handleError = (err) => { + + const handleError = (err: SyntheticEvent) => { console.log("load avatar error", err); const tmp = getInitialsAvatar({ initials: getInitials(name), @@ -13,6 +20,7 @@ const Avatar = ({ url = "", name = "unkonw name", type = "user", ...rest }) => { }); setSrc(tmp); }; + useEffect(() => { if (!url) { const tmp = getInitialsAvatar({ @@ -26,6 +34,7 @@ const Avatar = ({ url = "", name = "unkonw name", type = "user", ...rest }) => { } }, [url, name]); if (!src) return null; + return ; }; diff --git a/src/common/component/AvatarUploader.tsx b/src/common/component/AvatarUploader.tsx index 8fd0492d..9e3e334e 100644 --- a/src/common/component/AvatarUploader.tsx +++ b/src/common/component/AvatarUploader.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { ChangeEvent, FC, useState } from "react"; import styled from "styled-components"; import Avatar from "./Avatar"; import uploadIcon from "../../assets/icons/upload.image.svg?url"; @@ -61,21 +61,31 @@ const StyledWrapper = styled.div` } `; -export default function AvatarUploader({ +interface Props { + url?: string; + name?: string; + type?: "user"; + disabled?: boolean; + uploadImage: (file: File) => Promise; +} + +const AvatarUploader: FC = ({ url = "", name = "", type = "user", uploadImage, disabled = false -}) { +}) => { const [uploading, setUploading] = useState(false); - const handleUpload = async (evt) => { + const handleUpload = async (evt: ChangeEvent) => { if (uploading) return; - const [file] = evt.target.files; + if (!evt.target.files) return; + const [file] = Array.from(evt.target.files); setUploading(true); await uploadImage(file); setUploading(false); }; + return (
    @@ -87,6 +97,7 @@ export default function AvatarUploader({ multiple={false} onChange={handleUpload} type="file" + // todo: xss accept="image/*" name="avatar" id="avatar" @@ -97,4 +108,6 @@ export default function AvatarUploader({ {!disabled && icon} ); -} +}; + +export default AvatarUploader; diff --git a/src/common/component/BlankPlaceholder.tsx b/src/common/component/BlankPlaceholder.tsx index 305a3fb9..3a87198a 100644 --- a/src/common/component/BlankPlaceholder.tsx +++ b/src/common/component/BlankPlaceholder.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { FC, useState } from "react"; import styled from "styled-components"; import { NavLink } from "react-router-dom"; import ChannelModal from "./ChannelModal"; @@ -67,7 +67,11 @@ const Styled = styled.div` } `; -export default function BlankPlaceholder({ type = "chat" }) { +interface Props { + type?: "chat"; +} + +const BlankPlaceholder: FC = ({ type = "chat" }) => { const server = useAppSelector((store) => store.server); const [inviteModalVisible, setInviteModalVisible] = useState(false); const [createChannelVisible, setCreateChannelVisible] = useState(false); @@ -83,7 +87,8 @@ export default function BlankPlaceholder({ type = "chat" }) { }; const chatTip = type == "chat" ? "Create a Channel to Start a Conversation" : "Send a Direct Message"; - const chatHanlder = type == "chat" ? toggleChannelModalVisible : toggleContactListVisible; + const chatHandler = type == "chat" ? toggleChannelModalVisible : toggleContactListVisible; + return ( <> @@ -99,7 +104,7 @@ export default function BlankPlaceholder({ type = "chat" }) {
    Invite your friends or teammates
    -
    +
    {chatTip}
    @@ -109,7 +114,7 @@ export default function BlankPlaceholder({ type = "chat" }) { -
    Got quesions? Visit our help center
    +
    Got questions? Visit our help center
    @@ -120,4 +125,6 @@ export default function BlankPlaceholder({ type = "chat" }) { {inviteModalVisible && } ); -} +}; + +export default BlankPlaceholder; diff --git a/src/common/component/ContextMenu.tsx b/src/common/component/ContextMenu.tsx index ab20a953..426d7a01 100644 --- a/src/common/component/ContextMenu.tsx +++ b/src/common/component/ContextMenu.tsx @@ -1,7 +1,7 @@ import { FC, MouseEvent } from "react"; import StyledMenu from "./styled/Menu"; -interface Item { +export interface Item { title: string; icon: string; handler: (e: MouseEvent) => void; diff --git a/src/common/component/DeleteMessageConfirm.tsx b/src/common/component/DeleteMessageConfirm.tsx index 300c5399..68717925 100644 --- a/src/common/component/DeleteMessageConfirm.tsx +++ b/src/common/component/DeleteMessageConfirm.tsx @@ -11,8 +11,7 @@ interface Props { } const DeleteMessageConfirmModal: FC = ({ closeModal, mids = [] }) => { - // const dispatch = useDispatch(); - const mid_arr = mids ? (Array.isArray(mids) ? mids : [mids]) : []; + const mid_arr: number[] = mids ? (Array.isArray(mids) ? mids : [mids]) : []; const [ids] = useState(mid_arr); const { deleteMessage, isDeleting } = useDeleteMessage(); const handleDelete = async () => { @@ -40,7 +39,7 @@ const DeleteMessageConfirmModal: FC = ({ closeModal, mids = [] }) => { ids.length > 1 ? "these messages" : "this message" }?`} > - {ids.length == 1 && } + {ids.length === 1 && } ); diff --git a/src/common/component/FAQ.tsx b/src/common/component/FAQ.tsx index 1cf26f7f..3d2264e8 100644 --- a/src/common/component/FAQ.tsx +++ b/src/common/component/FAQ.tsx @@ -9,7 +9,6 @@ const Styled = styled.div` export default function FAQ() { const { data: serverVersion } = useGetServerVersionQuery(); - console.log("build time", serverVersion); return (
    Client Version: {process.env.VERSION}
    diff --git a/src/common/component/FileBox/index.tsx b/src/common/component/FileBox/index.tsx index 0367d3f4..0f99112b 100644 --- a/src/common/component/FileBox/index.tsx +++ b/src/common/component/FileBox/index.tsx @@ -1,7 +1,6 @@ -// import React from 'react' +import { FC, ReactElement } from "react"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; -import { useSelector } from "react-redux"; import Styled from "./styled"; import { VideoPreview, @@ -13,13 +12,20 @@ import { } from "./preview"; import { getFileIcon, formatBytes } from "../../utils"; import IconDownload from "../../../assets/icons/download.svg"; +import { useAppSelector } from "../../../app/store"; -// todo +// todo: move to root file dayjs.extend(relativeTime); -const renderPreview = (data) => { +interface Data { + file_type: string; + name: string; + content: string; +} + +const renderPreview = (data: Data) => { const { file_type, name, content } = data; - let preview = null; + let preview: null | ReactElement = null; const checks = { image: /^image/gi, @@ -60,20 +66,31 @@ const renderPreview = (data) => { } return preview; }; -export default function FileBox({ - preview = false, + +interface Props { + preview?: boolean; + flex: boolean; + file_type: string; + name: string; + size: number; + created_at: number; + from_uid: number; + content: string; +} + +const FileBox: FC = ({ + preview, flex, - file_type = "", + file_type, name, size, created_at, from_uid, content -}) { - const fromUser = useSelector((store) => store.contacts.byId[from_uid]); +}) => { + const fromUser = useAppSelector((store) => store.contacts.byId[from_uid]); const icon = getFileIcon(file_type, name); if (!content || !fromUser || !name) return null; - console.log("file content", content, name, flex); const previewContent = renderPreview({ file_type, content, name }); const withPreview = preview && previewContent; return ( @@ -101,4 +118,6 @@ export default function FileBox({ {withPreview &&
    {previewContent}
    }
    ); -} +}; + +export default FileBox; diff --git a/src/common/component/FileBox/preview/Audio.tsx b/src/common/component/FileBox/preview/Audio.tsx index 5ca105c1..3b6f6dc4 100644 --- a/src/common/component/FileBox/preview/Audio.tsx +++ b/src/common/component/FileBox/preview/Audio.tsx @@ -1,4 +1,4 @@ -import { ReactEventHandler, useState } from "react"; +import { FC, ReactEventHandler, useState } from "react"; import styled from "styled-components"; const Styled = styled.div` @@ -17,7 +17,11 @@ const Styled = styled.div` } `; -export default function Audio({ url = "" }) { +interface Props { + url: string; +} + +const Audio: FC = ({ url }) => { const [err, setErr] = useState(false); const handleError: ReactEventHandler = (err) => { @@ -35,4 +39,6 @@ export default function Audio({ url = "" }) { )} ); -} +}; + +export default Audio; diff --git a/src/common/component/FileBox/preview/Code.tsx b/src/common/component/FileBox/preview/Code.tsx index dcf4ccab..625e4b46 100644 --- a/src/common/component/FileBox/preview/Code.tsx +++ b/src/common/component/FileBox/preview/Code.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, FC } from "react"; import styled from "styled-components"; const Styled = styled.div` @@ -12,7 +12,11 @@ const Styled = styled.div` color: #eee; `; -export default function Code({ url = "" }) { +interface Props { + url: string; +} + +const Code: FC = ({ url }) => { const [content, setContent] = useState(""); useEffect(() => { const getContent = async (url: string) => { @@ -26,4 +30,6 @@ export default function Code({ url = "" }) { if (!content) return null; return {content}; -} +}; + +export default Code; diff --git a/src/common/component/FileBox/preview/Doc.tsx b/src/common/component/FileBox/preview/Doc.tsx index fa30be86..69aae0b8 100644 --- a/src/common/component/FileBox/preview/Doc.tsx +++ b/src/common/component/FileBox/preview/Doc.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, FC } from "react"; import styled from "styled-components"; const Styled = styled.div` @@ -11,7 +11,11 @@ const Styled = styled.div` word-break: break-all; `; -export default function Doc({ url = "" }) { +interface Props { + url: string; +} + +const Doc: FC = ({ url }) => { const [content, setContent] = useState(""); useEffect(() => { const getContent = async (url: string) => { @@ -25,4 +29,6 @@ export default function Doc({ url = "" }) { if (!content) return null; return {content}; -} +}; + +export default Doc; diff --git a/src/common/component/FileBox/preview/Image.tsx b/src/common/component/FileBox/preview/Image.tsx index 35eb5885..b93d25d1 100644 --- a/src/common/component/FileBox/preview/Image.tsx +++ b/src/common/component/FileBox/preview/Image.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { FC } from "react"; import styled from "styled-components"; const Styled = styled.div` @@ -11,11 +11,18 @@ const Styled = styled.div` } `; -export default function Image({ url = "" }) { +interface Props { + url: string; + alt?: string; +} + +const Image: FC = ({ url, alt }) => { if (!url) return null; return ( - + {alt} ); -} +}; + +export default Image; diff --git a/src/common/component/FileBox/preview/Pdf.tsx b/src/common/component/FileBox/preview/Pdf.tsx index e4335b62..8421f91c 100644 --- a/src/common/component/FileBox/preview/Pdf.tsx +++ b/src/common/component/FileBox/preview/Pdf.tsx @@ -1,4 +1,5 @@ import styled from "styled-components"; +import { FC } from "react"; const Styled = styled.div` padding: 8px; @@ -10,14 +11,17 @@ const Styled = styled.div` } `; -export default function Pdf({ url = "" }) { +interface Props { + url: string; +} + +const Pdf: FC = ({ url }) => { // const [content, setContent] = useState(""); // const [pageNumber, setPageNumber] = useState(1); // const [numPages, setNumPages] = useState(null); // const onDocumentLoadSuccess = ({ numPages }) => { // setNumPages(numPages); // }; - if (!url) return null; return ( @@ -26,4 +30,6 @@ export default function Pdf({ url = "" }) { */} ); -} +}; + +export default Pdf; diff --git a/src/common/component/FileBox/preview/Video.tsx b/src/common/component/FileBox/preview/Video.tsx index c746c66e..c1507e79 100644 --- a/src/common/component/FileBox/preview/Video.tsx +++ b/src/common/component/FileBox/preview/Video.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { FC } from "react"; import styled from "styled-components"; const Styled = styled.div` @@ -10,11 +10,16 @@ const Styled = styled.div` } `; -export default function Video({ url = "" }) { - if (!url) return null; +interface Props { + url: string; +} + +const Video: FC = ({ url }) => { return ( ); -} +}; + +export default Video; diff --git a/src/common/component/FileMessage/ImageMessage.tsx b/src/common/component/FileMessage/ImageMessage.tsx index c3c6e16b..243416a9 100644 --- a/src/common/component/FileMessage/ImageMessage.tsx +++ b/src/common/component/FileMessage/ImageMessage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, FC } from "react"; import styled from "styled-components"; import { CircularProgressbar, buildStyles } from "react-circular-progressbar"; import "react-circular-progressbar/dist/styles.css"; @@ -33,17 +33,25 @@ const Styled = styled.div` } `; -export default function ImageMessage({ +interface Props { + uploading: boolean; + progress: number; + thumbnail: string; + download: string; + content: string; + properties: { width: number; height: number }; +} + +const ImageMessage: FC = ({ uploading, progress, thumbnail, download, content, - properties = {} -}) { + properties +}) => { const [url, setUrl] = useState(thumbnail); const { width = 0, height = 0 } = getDefaultSize(properties); - console.log("image props", properties, width, height); useEffect(() => { const newUrl = thumbnail; const img = new Image(); @@ -64,10 +72,7 @@ export default function ImageMessage({
    @@ -85,4 +90,6 @@ export default function ImageMessage({ /> ); -} +}; + +export default ImageMessage; diff --git a/src/common/component/GithubLoginButton.tsx b/src/common/component/GithubLoginButton.tsx index b27fe845..77f9d3f8 100644 --- a/src/common/component/GithubLoginButton.tsx +++ b/src/common/component/GithubLoginButton.tsx @@ -1,5 +1,4 @@ -// import { useState, useEffect } from "react"; -// import { useGoogleLogin } from "react-google-login"; +import { FC } from "react"; import IconGithub from "../../assets/icons/github.svg"; import styled from "styled-components"; import Button from "./styled/Button"; @@ -14,16 +13,23 @@ const StyledSocialButton = styled(Button)` color: #344054; border: 1px solid #d0d5dd; background: none !important; + .icon { width: 24px; height: 24px; } `; -export default function GithubLoginButton({ config = {} }) { +interface Props { + config: { + client_id: string; + }; +} + +const GithubLoginButton: FC = ({ config }) => { const { client_id } = config; const handleGithubLogin = () => { - location.href = `http://github.com/login/oauth/authorize?client_id=${client_id}`; + location.href = `https://github.com/login/oauth/authorize?client_id=${client_id}`; // console.log("github login"); }; // console.log("google login ", loaded); @@ -31,7 +37,9 @@ export default function GithubLoginButton({ config = {} }) { Sign in with Github - {/* {loaded ? `Sign in with Github` : `Initailizing`} */} + {/* {loaded ? `Sign in with Github` : `Initializing`} */} ); -} +}; + +export default GithubLoginButton; diff --git a/src/common/component/ImagePreviewModal.tsx b/src/common/component/ImagePreviewModal.tsx index 22c97db1..759cd796 100644 --- a/src/common/component/ImagePreviewModal.tsx +++ b/src/common/component/ImagePreviewModal.tsx @@ -1,16 +1,16 @@ -import { useRef, useState, useEffect } from "react"; +import { useRef, useState, useEffect, FC } from "react"; import styled, { keyframes } from "styled-components"; import { useOutsideClick, useKey } from "rooks"; import { Ring } from "@uiball/loaders"; import Modal from "./Modal"; const AniFadeIn = keyframes` -from{ -background: transparent; -} -to{ - background: rgba(1, 1, 1, 0.9); -} + from { + background: transparent; + } + to { + background: rgba(1, 1, 1, 0.9); + } `; const StyledWrapper = styled.div` @@ -23,6 +23,7 @@ const StyledWrapper = styled.div` display: flex; align-items: center; justify-content: center; + .box { position: relative; transition: all 0.2s ease; @@ -30,14 +31,17 @@ const StyledWrapper = styled.div` flex-direction: column; justify-content: flex-start; gap: 10px; + > .loading { position: absolute; top: 40%; left: 50%; transform: translateX(-50%); } + > .image { overflow: auto; + img { max-width: 70vw; max-height: 80vh; @@ -46,13 +50,16 @@ const StyledWrapper = styled.div` object-fit: contain; */ } } + &.loading .image img { filter: blur(2px); } + .origin { font-weight: bold; font-size: 14px; color: #aaa; + &:hover { text-decoration: underline; color: #fff; @@ -61,11 +68,26 @@ const StyledWrapper = styled.div` } `; -export default function ImagePreviewModal({ download = true, data, closeModal }) { +export interface PreviewImageData { + originUrl: string; + thumbnail: string; + downloadLink: string; + name: string; + type: string; +} + +interface Props { + download?: boolean; + data?: PreviewImageData; + closeModal: () => void; +} + +const ImagePreviewModal: FC = ({ download = true, data, closeModal }) => { const [url, setUrl] = useState(data?.thumbnail); const [loading, setLoading] = useState(true); - const wrapperRef = useRef(); + const wrapperRef = useRef(null); useOutsideClick(wrapperRef, closeModal); + useKey( "Escape", () => { @@ -74,6 +96,7 @@ export default function ImagePreviewModal({ download = true, data, closeModal }) }, { eventTypes: ["keyup"] } ); + useEffect(() => { if (data) { const { originUrl } = data; @@ -126,4 +149,6 @@ export default function ImagePreviewModal({ download = true, data, closeModal })
    ); -} +}; + +export default ImagePreviewModal; diff --git a/src/common/component/Loading.tsx b/src/common/component/Loading.tsx index d98316c5..52afe018 100644 --- a/src/common/component/Loading.tsx +++ b/src/common/component/Loading.tsx @@ -1,16 +1,16 @@ -// import { useState, useEffect } from "react"; +import { FC } from "react"; import styled, { keyframes } from "styled-components"; import { Ring } from "@uiball/loaders"; import Button from "./styled/Button"; import useLogout from "../hook/useLogout"; const DelayVisible = keyframes` -from{ -opacity: 0; -} -to{ -opacity: 1; -} + from { + opacity: 0; + } + to { + opacity: 1; + } `; const StyledWrapper = styled.div` @@ -21,17 +21,15 @@ const StyledWrapper = styled.div` gap: 15px; align-items: center; justify-content: center; + &.fullscreen { width: 100vw; height: 100vh; } - .loading { - display: flex; - align-items: center; - justify-content: center; - } + .reload { opacity: 0; + &.visible { animation: ${DelayVisible} 1s forwards; animation-delay: 30s; @@ -39,20 +37,26 @@ const StyledWrapper = styled.div` } `; -export default function Loading({ reload = false, fullscreen = false }) { +interface Props { + reload?: boolean; + fullscreen?: boolean; +} + +const Loading: FC = ({ reload = false, fullscreen = false }) => { const { clearLocalData } = useLogout(); const handleReload = () => { clearLocalData(); - // todo: only firefox has argument - location.reload(true); + location.reload(); }; return ( - + ); -} +}; + +export default Loading; diff --git a/src/common/component/Modal.tsx b/src/common/component/Modal.tsx index da344ab2..9e747384 100644 --- a/src/common/component/Modal.tsx +++ b/src/common/component/Modal.tsx @@ -1,11 +1,19 @@ -import { useEffect, useState } from "react"; +import { FC, ReactNode, useEffect, useState } from "react"; import { createPortal } from "react-dom"; -export default function Modal({ id = "root-modal", mask = true, children }) { - const [wrapper, setWrapper] = useState(null); - // let eleRef = useRef(null); +interface Props { + id?: string; + mask?: boolean; + children?: ReactNode; +} + +// todo: check memory leak +const Modal: FC = ({ id = "root-modal", mask = true, children }) => { + const [wrapper, setWrapper] = useState(null); + useEffect(() => { const modalRoot = document.getElementById(id); + if (!modalRoot) return; if (mask) { modalRoot.classList.add("mask"); } @@ -17,7 +25,9 @@ export default function Modal({ id = "root-modal", mask = true, children }) { modalRoot.removeChild(wrapper); }; }, [id, mask]); + if (!wrapper) return null; - console.log("create portal"); return createPortal(children, wrapper); -} +}; + +export default Modal; diff --git a/src/common/component/ReactionItem.tsx b/src/common/component/ReactionItem.tsx index ee063385..d3d33e02 100644 --- a/src/common/component/ReactionItem.tsx +++ b/src/common/component/ReactionItem.tsx @@ -1,3 +1,4 @@ +import { FC, ReactElement } from "react"; import EmojiThumbUp from "../../assets/icons/emoji.thumb.up.svg"; import EmojiThumbDown from "../../assets/icons/emoji.thumb.down.svg"; import EmojiSmile from "../../assets/icons/emoji.smile.svg"; @@ -7,7 +8,18 @@ import EmojiHeart from "../../assets/icons/emoji.heart.svg"; import EmojiRocket from "../../assets/icons/emoji.rocket.svg"; import EmojiLook from "../../assets/icons/emoji.look.svg"; -const Emojis = { +interface Emojis { + "👍": ReactElement; + "👎": ReactElement; + "😄": ReactElement; + "👀": ReactElement; + "🚀": ReactElement; + "❤️": ReactElement; + "🙁": ReactElement; + "🎉": ReactElement; +} + +const emojis: Emojis = { "👍": , "👎": , "😄": , @@ -18,8 +30,13 @@ const Emojis = { "🎉": }; -export default function ReactionItem({ native = "" }) { - if (!native || !Emojis[native]) return null; - - return Emojis[native]; +interface Props { + native?: keyof Emojis; } + +const ReactionItem: FC = ({ native }) => { + if (!native) return null; + return emojis[native] ?? null; +}; + +export default ReactionItem; diff --git a/src/common/component/SaveTip.tsx b/src/common/component/SaveTip.tsx index 661bcb5e..7c60291f 100644 --- a/src/common/component/SaveTip.tsx +++ b/src/common/component/SaveTip.tsx @@ -14,7 +14,7 @@ const StyledWrapper = styled.div` background: #fff; /* gap: 20px; */ border: 1px solid #e5e7eb; - 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: 25px; .txt { padding: 8px; @@ -37,7 +37,7 @@ const StyledWrapper = styled.div` background: #1fe1f9; border: 1px solid #1fe1f9; box-sizing: border-box; - box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05); + box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05); border-radius: 25px; &.reset { background: none; diff --git a/src/common/component/Search.tsx b/src/common/component/Search.tsx index b8980501..2d406267 100644 --- a/src/common/component/Search.tsx +++ b/src/common/component/Search.tsx @@ -34,11 +34,10 @@ const StyledWrapper = styled.div` `; export default function Search() { - console.log("searching"); return (
    - + search icon
    diff --git a/src/common/component/Server.tsx b/src/common/component/Server.tsx index 32a7145b..795ca418 100644 --- a/src/common/component/Server.tsx +++ b/src/common/component/Server.tsx @@ -68,7 +68,7 @@ export default function Server() {
    - + logo

    diff --git a/src/common/component/UserGuide/index.tsx b/src/common/component/UserGuide/index.tsx index 524b824f..d680383f 100644 --- a/src/common/component/UserGuide/index.tsx +++ b/src/common/component/UserGuide/index.tsx @@ -1,10 +1,11 @@ +import { FC, ReactElement } from "react"; import styled, { createGlobalStyle } from "styled-components"; import Tippy from "@tippyjs/react"; -import { roundArrow } from "tippy.js"; +import { Placement, roundArrow } from "tippy.js"; import "tippy.js/dist/svg-arrow.css"; import { updateUserGuide } from "../../../app/slices/ui"; import steps from "./steps"; -import { useAppDispatch, useAppSelector } from "../../../app/store"; +import { useAppDispatch } from "../../../app/store"; const Styled = styled.div` display: flex; @@ -57,15 +58,21 @@ const OverrideTippyArrowColor = createGlobalStyle` } `; -export default function UserGuide({ step = 1, placement = "right-start", delay = null, children }) { +interface Props { + step?: number; + placement: Placement; + delay?: number | [number | null, number | null]; + children: ReactElement; +} + +const UserGuide: FC = ({ step = 1, placement = "right-start", delay, children }) => { const dispatch = useAppDispatch(); - const { visible, step: reduxStep } = useAppSelector((store) => store.ui.userGuide); const currStep = steps[step - 1]; const isLastStep = steps.length == step; // if (!visible) return children; if (!currStep) return null; const { title, description } = currStep; - const defaultDuration = [300, 250]; + const defaultDuration: [number, number] = [300, 250]; const handleSkip = () => { // to do dispatch(updateUserGuide({ visible: false })); @@ -104,4 +111,6 @@ export default function UserGuide({ step = 1, placement = "right-start", delay = ); -} +}; + +export default UserGuide; diff --git a/src/common/component/styled/Modal.tsx b/src/common/component/styled/Modal.tsx index d622ef5a..21ab8995 100644 --- a/src/common/component/styled/Modal.tsx +++ b/src/common/component/styled/Modal.tsx @@ -1,4 +1,4 @@ -import { FC, ReactElement } from "react"; +import { FC, ReactNode } from "react"; import styled from "styled-components"; const Styled = styled.div` @@ -43,18 +43,12 @@ const Styled = styled.div` interface Props { title?: string; description?: string; - buttons: ReactElement[] | ReactElement; - children?: ReactElement; + buttons?: ReactNode; + children?: ReactNode; className?: string; } -const StyledModal: FC = ({ - title = "", - description = "", - buttons = null, - children, - ...props -}) => { +const StyledModal: FC = ({ title = "", description = "", buttons, children, ...props }) => { return ( {title &&

    {title}

    } diff --git a/src/common/hook/useChatScroll.ts b/src/common/hook/useChatScroll.ts index e309ce05..a1cb0b0c 100644 --- a/src/common/hook/useChatScroll.ts +++ b/src/common/hook/useChatScroll.ts @@ -1,9 +1,8 @@ import { useRef, useEffect } from "react"; // import { useDebounce } from "rooks"; -function useChatScroll(deps = []) { - // todo: ref should be initialized to null - const ref = useRef(); +function useChatScroll() { + const ref = useRef(null); // useEffect(() => { // console.log("chat scroll", ref); // if (ref.current) { diff --git a/src/common/hook/useContextMenu.tsx b/src/common/hook/useContextMenu.tsx index 02bbc4ac..a15e946b 100644 --- a/src/common/hook/useContextMenu.tsx +++ b/src/common/hook/useContextMenu.tsx @@ -1,14 +1,19 @@ -import { useState } from "react"; +import { useState, MouseEvent, ReactElement } from "react"; import { hideAll } from "tippy.js"; import Tippy from "@tippyjs/react"; -import Menu from "../component/ContextMenu"; +import Menu, { Item } from "../component/ContextMenu"; + +interface ContextMenuProps { + key: string | number; + children: ReactElement; + items: Item[]; +} export default function useContextMenu(placement = "right-start") { const [visible, setVisible] = useState(false); // for tippy.js const [offset, setOffset] = useState({ x: 0, y: 0 }); - const handleContextMenuEvent = (evt) => { - // console.log("context menu event", evt, evt.currentTarget); + const handleContextMenuEvent = (evt: MouseEvent) => { hideAll(); evt.preventDefault(); const { currentTarget, clientX, clientY } = evt; @@ -22,14 +27,14 @@ export default function useContextMenu(placement = "right-start") { y = top - clientY; } setOffset({ x, y }); - setVisible(true); - console.log("offset", x, y); }; + const hideContextMenu = () => { setVisible(false); }; - const ContextMenu = ({ key, items, children }) => { + + const ContextMenu = ({ key, items, children }: ContextMenuProps) => { return ( ); }; + return { ContextMenu, offset, diff --git a/src/common/hook/useRemoveLocalMessage.ts b/src/common/hook/useRemoveLocalMessage.ts index 1668395e..3be31d96 100644 --- a/src/common/hook/useRemoveLocalMessage.ts +++ b/src/common/hook/useRemoveLocalMessage.ts @@ -1,14 +1,20 @@ -// import second from 'first' -import { useDispatch } from "react-redux"; import { removeMessage } from "../../app/slices/message"; import { removeChannelMsg } from "../../app/slices/message.channel"; import { removeUserMsg } from "../../app/slices/message.user"; -export default function useRemoveLocalMessage({ context = "user", id = 0 }) { - const dispatch = useDispatch(); +import { useAppDispatch } from "../../app/store"; + +// todo: check usage +interface Props { + context: "user" | "channel"; + id: number; +} + +export default function useRemoveLocalMessage({ context = "user", id = 0 }: Props) { + const dispatch = useAppDispatch(); const removeContextMessage = context == "channel" ? removeChannelMsg : removeUserMsg; - const removeLocalMessage = (mid) => { + + return (mid: number) => { dispatch(removeContextMessage({ id, mid })); dispatch(removeMessage(mid)); }; - return removeLocalMessage; } diff --git a/src/common/hook/useSendMessage.ts b/src/common/hook/useSendMessage.ts index 73e29b0f..91a6fbf9 100644 --- a/src/common/hook/useSendMessage.ts +++ b/src/common/hook/useSendMessage.ts @@ -1,14 +1,34 @@ -// import second from 'first' +import toast from "react-hot-toast"; import { removeReplyingMessage, addReplyingMessage } from "../../app/slices/message"; import { useSendChannelMsgMutation } from "../../app/services/channel"; import { useSendMsgMutation } from "../../app/services/contact"; import { useReplyMessageMutation } from "../../app/services/message"; -import { useDispatch, useSelector } from "react-redux"; -import toast from "react-hot-toast"; -export default function useSendMessage(props) { +import { useAppDispatch, useAppSelector } from "../../app/store"; + +interface Props { + context: "user" | "channel"; + from: number; + to: number; +} + +interface SendMessagesDTO { + type: "text"; + content: string; + users: number[]; + channels: number[]; +} + +interface SendMessageDTO { + type: "text"; + content: string; + properties: object; + reply_mid?: number; +} + +export default function useSendMessage(props?: Props) { const { context = "user", from = null, to = null } = props || {}; - const dispatch = useDispatch(); - const stageFiles = useSelector((store) => store.ui.uploadFiles[`${context}_${to}`] || []); + const dispatch = useAppDispatch(); + const stageFiles = useAppSelector((store) => store.ui.uploadFiles[`${context}_${to}`] || []); const [replyMessage, { isError: replyErr, isLoading: replying, isSuccess: replySuccess }] = useReplyMessageMutation(); const [ @@ -18,7 +38,12 @@ export default function useSendMessage(props) { const [sendUserMsg, { isLoading: userSending, isSuccess: userSuccess, isError: userError }] = useSendMsgMutation(); const sendFn = context == "user" ? sendUserMsg : sendChannelMsg; - const sendMessages = async ({ type = "text", content, users = [], channels = [] }) => { + const sendMessages = async ({ + type = "text", + content, + users = [], + channels = [] + }: SendMessagesDTO) => { if (users.length) { for await (const uid of users) { await sendUserMsg({ @@ -42,9 +67,9 @@ export default function useSendMessage(props) { type = "text", content, properties = {}, - reply_mid = null, + reply_mid, ...rest - }) => { + }: SendMessageDTO) => { if (reply_mid) { removeReplying(); await replyMessage({ @@ -66,7 +91,7 @@ export default function useSendMessage(props) { }); } }; - const setReplying = (mid) => { + const setReplying = (mid: number) => { if (stageFiles.length !== 0) { toast.error("Only text is supported when replying a message"); return; diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 6431bc5f..8380f0f0 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -1 +1,73 @@ -/// +/// +/// +/// + +declare namespace NodeJS { + interface ProcessEnv { + readonly NODE_ENV: "development" | "production" | "test"; + readonly PUBLIC_URL: string; + } +} + +declare module "*.avif" { + const src: string; + export default src; +} + +declare module "*.bmp" { + const src: string; + export default src; +} + +declare module "*.gif" { + const src: string; + export default src; +} + +declare module "*.jpg" { + const src: string; + export default src; +} + +declare module "*.jpeg" { + const src: string; + export default src; +} + +declare module "*.png" { + const src: string; + export default src; +} + +declare module "*.webp" { + const src: string; + export default src; +} + +// fix type error: TS2307: Cannot find module '...svg?url' or its corresponding type declarations. +declare module "*.svg?url" { + const src: string; + export default src; +} + +declare module "*.svg" { + import * as React from "react"; + + const ReactComponent: React.FunctionComponent & { title?: string }>; + export default ReactComponent; +} + +declare module "*.module.css" { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module "*.module.scss" { + const classes: { readonly [key: string]: string }; + export default classes; +} + +declare module "*.module.sass" { + const classes: { readonly [key: string]: string }; + export default classes; +} diff --git a/src/routes/settingChannel/navs.tsx b/src/routes/settingChannel/navs.tsx index 77b108ee..07f5b7bd 100644 --- a/src/routes/settingChannel/navs.tsx +++ b/src/routes/settingChannel/navs.tsx @@ -1,7 +1,8 @@ import Overview from "./Overview"; import ManageMembers from "../../common/component/ManageMembers"; -const useNavs = (channelId) => { - const navs = [ + +const useNavs = (channelId: number) => { + return [ { title: "General", items: [ @@ -36,7 +37,6 @@ const useNavs = (channelId) => { // ], // }, ]; - return navs; }; export default useNavs; diff --git a/src/types/auth.ts b/src/types/auth.ts index 7277059e..d7368a42 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -1,4 +1,3 @@ - export interface AuthToken { // common server_id: string; @@ -7,11 +6,9 @@ export interface AuthToken { expired_in: number; } -// todo: check gender values export type Gender = 0 | 1; export interface User { - // avatar: string; // todo: check transform data in redux slice uid: number; email: string; name: string; diff --git a/src/types/channel.ts b/src/types/channel.ts index 124bee86..7d1f4a20 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -1,18 +1,8 @@ +export interface ChannelMember {} -export interface ChannelMember { +export interface Message {} -} - -// todo: check message fields -export interface ChannelMessage { - mid: number; -} - -export type ContentType = - 'text/plain' | - 'text/markdown' | - 'rustchat/file' | - 'rustchat/archive'; +export type ContentType = "text/plain" | "text/markdown" | "rustchat/file" | "rustchat/archive"; export interface PinnedMessage { mid: number; @@ -38,7 +28,7 @@ export interface Channel { } export interface UpdateChannelDTO { - operation: 'add_member' | 'remove_member'; + operation: "add_member" | "remove_member"; members?: number[]; // type = 'group_changed' diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 07a05c54..0a11b24a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,10 +1,10 @@ -import { PrecacheEntry } from "workbox-precaching/src/_types"; -import localforage from "localforage"; - export declare global { + import { PrecacheEntry } from "workbox-precaching/src/_types"; + import localforage from "localforage"; + interface Window { __WB_MANIFEST: Array; skipWaiting: () => void; - CACHE: { [key: string]: typeof localforage }; + CACHE: { [key: string]: typeof localforage | undefined }; } } diff --git a/src/types/sse.ts b/src/types/sse.ts index 51d630e0..fae57eb6 100644 --- a/src/types/sse.ts +++ b/src/types/sse.ts @@ -1,24 +1,24 @@ -import { User } from './auth'; -import { Channel, ContentType } from './channel'; +import { User } from "./auth"; +import { Channel, ContentType } from "./channel"; export interface ReadyEvent { - type: 'ready'; + type: "ready"; } export interface UsersSnapshotEvent { - type: 'users_snapshot'; + type: "users_snapshot"; users: User[]; version: number; } // todo: check if create_by field exists -export interface UserLog extends Omit { +export interface UserLog extends Omit { log_id: number; - action: 'create' | 'update' | 'delete'; + action: "create" | "update" | "delete"; } export interface UsersLogEvent { - type: 'users_log'; + type: "users_log"; logs: UserLog[]; } @@ -28,87 +28,126 @@ export interface UserState { } export interface UsersStateEvent { - type: 'users_state'; + type: "users_state"; users: UserState[]; } -interface UsersStateChangedEvent extends UserState{ - type: 'users_state_changed'; +interface UsersStateChangedEvent extends UserState { + type: "users_state_changed"; } -interface UserSettingsEvent { - type: 'user_settings'; - mute_users?: { uid: number; expired_at: number; }[]; - mute_groups?: { gid: number; expired_at: number; }[]; - read_index_users?: { uid: number; mid: number; }[]; - read_index_groups?: { gid: number; mid: number; }[]; - burn_after_reading_users?: { uid: number; expires_in: number; }[]; - burn_after_reading_groups?: { gid: number; expires_in: number; }[]; +export interface MuteUser { + uid: number; + expired_at: number; } -interface UserSettingsChangedEvent { - type: 'user_settings_changed'; +export interface MuteChannel { + gid: number; + expired_at: number; +} + +export interface UserSettingsEvent { + type: "user_settings"; + mute_users?: MuteUser[]; + mute_groups?: MuteChannel[]; + read_index_users?: { uid: number; mid: number }[]; + read_index_groups?: { gid: number; mid: number }[]; + burn_after_reading_users?: { uid: number; expires_in: number }[]; + burn_after_reading_groups?: { gid: number; expires_in: number }[]; +} + +export interface UserSettingsChangedEvent { + type: "user_settings_changed"; from_device?: string; - add_mute_users?: { uid: number; expired_at: number; }[]; + add_mute_users?: MuteUser[]; remove_mute_users?: number[]; - add_mute_groups?: { gid: number; expired_at: number; }[]; + add_mute_groups?: MuteChannel[]; remove_mute_groups?: number[]; - read_index_users?: { uid: number; mid: number; }[]; - read_index_groups?: { gid: number; mid: number; }[]; - burn_after_reading_users?: { uid: number; expires_in: number; }[]; - burn_after_reading_groups?: { gid: number; expires_in: number; }[]; + read_index_users?: { uid: number; mid: number }[]; + read_index_groups?: { gid: number; mid: number }[]; + burn_after_reading_users?: { uid: number; expires_in: number }[]; + burn_after_reading_groups?: { gid: number; expires_in: number }[]; } -interface RelatedGroupsEvent { - type: 'related_groups'; +export interface RelatedGroupsEvent { + type: "related_groups"; groups: Channel[]; } -interface ChatEvent { - type: 'chat'; +export interface NormalMessage { + type: "normal"; + properties: {}; + content_type: ContentType; + content: string; + expires_in: number; +} + +export interface EditReactionDetail { + properties: {}; + content_type: ContentType; + content: string; + type: "edit"; +} +export interface LikeReactionDetail { + action: string; + type: "like"; +} +export interface DeleteReactionDetail { + type: "delete"; +} +export interface ReactionMessage { + type: "reaction"; + mid: number; // original message id + detail: EditReactionDetail | LikeReactionDetail | DeleteReactionDetail; +} + +export interface ReplyMessage { + type: "reply"; + mid: number; // original message id + properties: {}; + content_type: ContentType; + content: string; +} + +export interface ChatEvent { + type: "chat"; mid: number; from_uid: number; created_at: number; target: { uid: number }; - detail: { - properties: {}; - content: string; - content_type: ContentType; - expires_in: number; - type: 'normal'; - }; + detail: NormalMessage | ReactionMessage | ReplyMessage; } interface KickEvent { - type: 'kick'; + type: "kick"; reason: string; } interface UserJoinedGroupEvent { - type: 'user_joined_group'; + type: "user_joined_group"; gid: number; uid: number[]; } interface UserLeavedGroupEvent { - type: 'user_leaved_group'; + type: "user_leaved_group"; gid: number; uid: number[]; } interface JoinedGroupEvent { - type: 'joined_group'; + type: "joined_group"; group: Channel; } interface KickFromGroupEvent { - type: 'kick_from_group'; + type: "kick_from_group"; gid: number; reason: string; } interface GroupChangedEvent { - type: 'group_changed'; + type: "group_changed"; gid: number; name: string; description: string; @@ -117,7 +156,7 @@ interface GroupChangedEvent { } interface PinnedMessageUpdatedEvent { - type: 'pinned_message_updated'; + type: "pinned_message_updated"; gid: number; mid: number; msg: { @@ -127,28 +166,29 @@ interface PinnedMessageUpdatedEvent { properties: {}; content: string; content_type: ContentType; - } + }; } interface HeartbeatEvent { - type: 'heartbeat'; + type: "heartbeat"; time: number; } -export type ServerEvent = ReadyEvent | - UsersSnapshotEvent | - UsersLogEvent | - UsersStateEvent | - UsersStateChangedEvent | - UserSettingsEvent | - UserSettingsChangedEvent | - RelatedGroupsEvent | - ChatEvent | - KickEvent | - UserJoinedGroupEvent | - UserLeavedGroupEvent | - JoinedGroupEvent | - KickFromGroupEvent | - GroupChangedEvent | - PinnedMessageUpdatedEvent | - HeartbeatEvent; +export type ServerEvent = + | ReadyEvent + | UsersSnapshotEvent + | UsersLogEvent + | UsersStateEvent + | UsersStateChangedEvent + | UserSettingsEvent + | UserSettingsChangedEvent + | RelatedGroupsEvent + | ChatEvent + | KickEvent + | UserJoinedGroupEvent + | UserLeavedGroupEvent + | JoinedGroupEvent + | KickFromGroupEvent + | GroupChangedEvent + | PinnedMessageUpdatedEvent + | HeartbeatEvent;