refactor: add typescript definition

This commit is contained in:
HD
2022-06-28 10:08:36 +08:00
parent e0bbbf4f30
commit 2a3535ea13
30 changed files with 325 additions and 204 deletions
+9 -7
View File
@@ -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 }) {
+14 -10
View File
@@ -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<GetServerResponse, void>({
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<GetThirdPartySecretResponse, void>({
query: () => ({
// headers: {
// "content-type": "text/plain",
@@ -35,7 +42,7 @@ export const serverApi = createApi({
}),
keepUnusedDataFor: 0
}),
updateThirdPartySecret: builder.mutation({
updateThirdPartySecret: builder.mutation<UpdateThirdPartySecretResponse, void>({
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<string, void>({
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<GetLoginConfigResponse, void>({
query: () => ({ url: `admin/login/config` })
}),
updateLoginConfig: builder.mutation({
+27 -30
View File
@@ -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<User[]>) {
console.log('set Contacts store', action);
fullfillContacts(state, action: PayloadAction<StoredUser[]>) {
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<number>) {
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);
+4 -4
View File
@@ -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<State>) {
return action.payload;
},
addChannelMsg(state, action) {
addChannelMsg(state, action: PayloadAction<ChatEvent>) {
const { id, mid, local_id = null } = action.payload;
if (state[id]) {
const midExsited = state[id]!.findIndex((id) => id == mid) > -1;
+13 -3
View File
@@ -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<Props> = ({ 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,
)}
</StyledWrapper>
);
}
};
export default Channel;
+30 -15
View File
@@ -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<Props> = ({ 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<CreateChannelDTO>({
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<HTMLInputElement>) => {
setData((prev) => ({ ...prev, name: evt.target.value }));
};
const handleInputChange = (evt) => {
const handleInputChange = (evt: ChangeEvent<HTMLInputElement>) => {
updateInput(evt.target.value);
};
const toggleCheckMember = ({ currentTarget }) => {
const toggleCheckMember = ({ currentTarget }: MouseEvent<HTMLLIElement>) => {
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 (
<Modal>
<StyledWrapper>
@@ -152,4 +165,6 @@ export default function ChannelModal({ personal = false, closeModal }) {
</StyledWrapper>
</Modal>
);
}
};
export default ChannelModal;
+1 -1
View File
@@ -6,7 +6,7 @@ import ContextMenu from "../ContextMenu";
interface Props {
enable?: boolean;
uid: number;
cid: number;
cid?: number;
visible: boolean;
hide: () => void;
children: ReactElement;
+1 -1
View File
@@ -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;
-1
View File
@@ -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";
+37 -12
View File
@@ -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<Props> = ({
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<ImageSize | null>(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({
</div>
</Styled>
);
}
};
export default FileMessage;
+26 -16
View File
@@ -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<Props> = ({ 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<number[]>([]);
const [selectedChannels, setSelectedChannels] = useState<number[]>([]);
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<HTMLLIElement>) => {
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
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 (
<Modal>
<StyledWrapper>
@@ -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 (
<li
key={gid}
@@ -107,7 +116,6 @@ export default function ForwardModal({ mids, closeModal }) {
contacts.map((u) => {
const { uid } = u;
const checked = selectedMembers.includes(uid);
console.log({ checked });
return (
<li
key={uid}
@@ -179,4 +187,6 @@ export default function ForwardModal({ mids, closeModal }) {
</StyledWrapper>
</Modal>
);
}
};
export default ForwardModal;
+1 -1
View File
@@ -60,7 +60,7 @@ const GoogleLoginButton: FC<Props> = ({ clientId }) => {
return (
<StyledSocialButton disabled={!loaded || isLoading} onClick={handleGoogleLogin}>
<img className="icon" src={googleSvg} alt="google icon" />
{loaded ? `Sign in with Google` : `Initailizing`}
{loaded ? "Sign in with Google" : "Initializing"}
</StyledSocialButton>
);
};
+21 -10
View File
@@ -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<Props> = ({ 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<SVGSVGElement | HTMLLIElement>) => {
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<HTMLInputElement>) => {
updateInput(evt.target.value);
};
if (!channel) return null;
const { members: uids } = channel;
const contactIds = contacts.map(({ uid }) => uid);
console.log("selects", selects);
return (
<Styled>
<div className="filter">
@@ -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}
>
<StyledCheckbox
disabled={added}
@@ -183,4 +192,6 @@ export default function AddMembers({ cid = null, closeModal }) {
</Button>
</Styled>
);
}
};
export default AddMembers;
+14 -3
View File
@@ -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<Props> = ({ 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
</Styled>
</Modal>
);
}
};
export default InviteModal;
+9 -3
View File
@@ -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<Props> = ({ cid = null }) => {
const { contacts, channels, loginUser } = useAppSelector((store) => {
return {
contacts: store.contacts,
@@ -246,4 +250,6 @@ export default function ManageMembers({ cid = null }) {
</ul>
</StyledWrapper>
);
}
};
export default ManageMembers;
@@ -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<Props> = ({ content }) => {
const MarkdownRender: FC<Props> = ({ content }) => {
const mdContainer = useRef<HTMLDivElement>(null);
const [previewImage, setPreviewImage] = useState(null);
const [previewImage, setPreviewImage] = useState<PreviewImageData | null>(null);
useEffect(() => {
const container = mdContainer?.current;
@@ -77,4 +77,4 @@ const MrakdownRender: FC<Props> = ({ content }) => {
);
};
export default MrakdownRender;
export default MarkdownRender;
@@ -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<Props> = ({ 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 }) {
</div>
</StyledWrapper>
);
}
};
export default PreviewMessage;
+2 -2
View File
@@ -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 = (
<div className="md">
<MrakdownRender content={content} />
<MarkdownRender content={content} />
</div>
);
break;
@@ -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 = <MrakdownRender content={content} />;
ctn = <MarkdownRender content={content} />;
}
break;
case ContentTypes.file:
+4 -7
View File
@@ -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 (
<>
<Helmet>
<link rel="icon" href={`${BASE_URL}/resource/organization/logo`} />
{isSuccess && <title>{data.name} Web App</title>}
</Helmet>
</>
<Helmet>
<link rel="icon" href={`${BASE_URL}/resource/organization/logo`} />
{isSuccess && <title>{data.name} Web App</title>}
</Helmet>
);
}
+1 -1
View File
@@ -13,7 +13,7 @@ import { useAppSelector } from "../../../app/store";
interface Props {
uid: number;
type: string;
cid: number;
cid?: number;
}
const Profile: FC<Props> = ({ uid = null, type = "embed", cid = null }) => {
+2 -2
View File
@@ -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 = (
<div className="md">
<MrakdownRender content={content} />
<MarkdownRender content={content} />
</div>
);
break;
+5 -3
View File
@@ -51,19 +51,21 @@ export default function Toolbar({
to,
context
}) {
// todo: check code logic
const { addStageFile } = useUploadFile({ context, id: to });
const fileInputRef = useRef<HTMLInputElement>(null);
const handleUpload = (evt: ChangeEvent<HTMLInputElement>) => {
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]);
};
+1 -1
View File
@@ -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<object>({});
const [updateLoginConfig, { isSuccess: LoginUpdated }] = useUpdateLoginConfigMutation();
const [updateSMTPConfig, { isSuccess: SMTPUpdated }] = useUpdateSMTPConfigMutation();
const [getAgoraConfig, { data: agoraConfig }] = useLazyGetAgoraConfigQuery();
+4
View File
@@ -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({});
+1 -1
View File
@@ -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,
+25 -30
View File
@@ -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(() => {
+2 -1
View File
@@ -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":
+11 -8
View File
@@ -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) => {
+44 -22
View File
@@ -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<ImageSize> => {
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}/`