refactor: add more TS constraints

This commit is contained in:
Tristan Yang
2022-07-04 18:15:24 +08:00
parent 8330e45674
commit ff7fe05b58
18 changed files with 48 additions and 202 deletions
+3 -1
View File
@@ -26,5 +26,7 @@
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"cSpell.words": ["btns", "oidc", "tippyjs", "vocechat"]
"cSpell.words": ["btns", "oidc", "tippyjs", "vocechat"],
"reactSnippets.settings.prettierEnabled": true,
"reactSnippets.settings.importReactOnTop": false
}
+1 -1
View File
@@ -4,7 +4,7 @@ import BASE_URL from "../config";
import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel";
interface StoredChannel extends Channel {
icon: string;
icon?: string;
}
interface State {
+2 -4
View File
@@ -1,6 +1,4 @@
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 +16,7 @@ const channelMsgSlice = createSlice({
fullfillChannelMsg(state, action: PayloadAction<State>) {
return action.payload;
},
addChannelMsg(state, action: PayloadAction<ChatEvent>) {
addChannelMsg(state, action: PayloadAction<{ id: number; mid: number; local_id?: any }>) {
const { id, mid, local_id = null } = action.payload;
if (state[id]) {
const midExsited = state[id]!.findIndex((id) => id == mid) > -1;
@@ -29,7 +27,7 @@ const channelMsgSlice = createSlice({
state[id] = [+mid];
}
},
removeChannelMsg(state, action: PayloadAction<ChannelMessage & EntityId>) {
removeChannelMsg(state, action: PayloadAction<{ id: number; mid: number }>) {
const { id, mid } = action.payload;
if (state[id]) {
const idx = state[id]!.findIndex((i) => i == mid);
-15
View File
@@ -4,10 +4,6 @@ import { Views } from "../config";
export interface State {
online: boolean;
ready: boolean;
userGuide: {
visible: boolean;
step: number;
};
inputMode: "text";
menuExpand: boolean;
// todo
@@ -25,10 +21,6 @@ export interface State {
const initialState: State = {
online: true,
ready: false,
userGuide: {
visible: false,
step: 1
},
inputMode: "text",
menuExpand: false,
fileListView: Views.grid,
@@ -77,12 +69,6 @@ const uiSlice = createSlice({
const { key, value } = action.payload;
state.draftMixedText[key] = value;
},
updateUserGuide(state, action) {
const obj = action.payload || {};
Object.keys(obj).forEach((key) => {
state.userGuide[key] = obj[key];
});
},
updateUploadFiles(state, action) {
const { context = "channel", id = null, operation = "add", ...rest } = action.payload;
if (!id || !context) return;
@@ -177,7 +163,6 @@ export const {
updateFileListView,
updateUploadFiles,
updateSelectMessages,
updateUserGuide,
updateDraftMarkdown,
updateDraftMixedText,
updateRemeberedNavs
+2
View File
@@ -2,6 +2,8 @@ import { useState, useEffect, memo, SyntheticEvent, FC } from "react";
import { getInitials, getInitialsAvatar } from "../utils";
interface Props {
className?: string;
alt?: string;
url?: string;
name?: string;
type?: "user" | "channel";
-1
View File
@@ -15,7 +15,6 @@ const StyledWrapper = styled.div`
height: 100%;
border-radius: 50%;
background-color: #eee;
/* border: 1px solid #eee; */
img {
object-fit: cover;
width: 100%;
+2 -2
View File
@@ -63,14 +63,14 @@ const Channel: FC<Props> = ({ interactive = true, id, compact = false, avatarSiz
});
if (!channel) return null;
const { name, members = [], is_public, avatar } = channel;
const { name, members = [], is_public, icon } = channel;
return (
<StyledWrapper
size={avatarSize}
className={`${interactive ? "interactive" : ""} ${compact ? "compact" : ""}`}
>
<div className="avatar">
<Avatar type="channel" url={avatar} name={"#"} alt="avatar" />
<Avatar type="channel" url={icon} name={"#"} alt="avatar" />
</div>
{!compact && (
<div className="name">
-2
View File
@@ -70,7 +70,6 @@ export default function CurrentUser() {
if (!currUser) return null;
const { uid, name, avatar } = currUser;
return (
// <UserGuide step={1}>
<StyledWrapper>
<div className="profile">
<Avatar url={avatar} name={name} alt="user avatar" className="avatar" />
@@ -86,6 +85,5 @@ export default function CurrentUser() {
</div>
)}
</StyledWrapper>
// </UserGuide>
);
}
@@ -23,7 +23,6 @@ const DeleteMessageConfirmModal: FC<Props> = ({ closeModal, mids = [] }) => {
return (
<Modal>
<StyledModal
// className="animate__animated animate__fadeInDown animate__faster"
buttons={
<>
<Button className="cancel" onClick={closeModal.bind(null, false)}>
@@ -18,7 +18,6 @@ const StyledWrapper = styled.div`
transition: all 0.5s ease;
width: 100vw;
height: 100vh;
/* background-color: rgba(1, 1, 1, 0.9); */
animation: ${AniFadeIn} 0.3s ease-in-out forwards;
display: flex;
align-items: center;
@@ -45,9 +44,6 @@ const StyledWrapper = styled.div`
img {
max-width: 70vw;
max-height: 80vh;
/* width: 100%;
height: 100%;
object-fit: contain; */
}
}
@@ -121,7 +117,6 @@ const ImagePreviewModal: FC<Props> = ({ download = true, data, closeModal }) =>
<div className={`box ${loading ? "loading" : ""}`} ref={wrapperRef}>
<div className="image">
<img
// onLoadedMetadata={handleMetaDataLoaded}
src={url}
alt="preview"
className={`animate__animated animate__fadeIn animate__faster`}
+1 -1
View File
@@ -140,7 +140,7 @@ const ManageMembers: FC<Props> = ({ cid }) => {
if (ignore) return;
updateUser({ id: uid, is_admin: isAdmin });
};
const channel = channels.byId[cid] ?? null;
const channel = cid ? channels.byId[cid] : null;
const uids = channel ? (channel.is_public ? users.ids : channel.members) : users.ids;
return (
+11 -4
View File
@@ -1,19 +1,26 @@
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { formatBytes, isTreatAsImage, getFileIcon } from "../../utils";
export default function FileItem({ file = null }) {
const [icon, setIcon] = useState(null);
const [icon, setIcon] = useState<React.ReactElement | null>(null);
useEffect(() => {
let localUrl = "";
if (file) {
const { type, name } = file;
if (isTreatAsImage(file)) {
// todo: memory leak, use revokeObjectURL before component didMount
setIcon(<img src={URL.createObjectURL(file)} alt="thumb" className="thumb" />);
// use revokeObjectURL before component didMount
localUrl = URL.createObjectURL(file);
setIcon(<img src={localUrl} alt="thumb" className="thumb" />);
return;
}
console.log("file type", type, name);
setIcon(getFileIcon(type, name));
}
return () => {
if (localUrl) {
URL.revokeObjectURL(localUrl);
}
};
}, [file]);
console.log("current file", file);
if (!file) return null;
-2
View File
@@ -8,7 +8,6 @@ const StyledWrapper = styled.div`
padding: 8px;
border-radius: 8px;
user-select: none;
&.interactive {
&:hover,
&.active {
@@ -47,7 +46,6 @@ const StyledWrapper = styled.div`
}
}
.name {
/* user-select: text; */
font-weight: 600;
font-size: 14px;
line-height: 20px;
-116
View File
@@ -1,116 +0,0 @@
import { FC, ReactElement } from "react";
import styled, { createGlobalStyle } from "styled-components";
import Tippy from "@tippyjs/react";
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 } from "../../../app/store";
const Styled = styled.div`
display: flex;
flex-direction: column;
padding: 24px;
background: #06aed4;
border-radius: var(--br);
box-shadow: 0 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
color: #ffffff;
min-width: 362px;
.title {
font-weight: 700;
font-size: 20px;
line-height: 30px;
margin-bottom: 4px;
}
.desc {
font-weight: 400;
font-size: 14px;
line-height: 20px;
margin-bottom: 24px;
}
.btns {
display: flex;
justify-content: space-between;
width: 100%;
align-items: center;
button {
cursor: pointer;
color: inherit;
font-weight: 500;
font-size: 14px;
line-height: 20px;
border: none;
background: none;
&.skip {
font-weight: 400;
}
&.next {
background: rgba(0, 0, 0, 0.16);
padding: 8px 14px;
border-radius: var(--br);
}
}
}
`;
const OverrideTippyArrowColor = createGlobalStyle`
.user-guide-tippy .tippy-svg-arrow svg path{
fill:#06aed4 ;
}
`;
interface Props {
step?: number;
placement: Placement;
delay?: number | [number | null, number | null];
children: ReactElement;
}
const UserGuide: FC<Props> = ({ step = 1, placement = "right-start", delay, children }) => {
const dispatch = useAppDispatch();
const currStep = steps[step - 1];
const isLastStep = steps.length == step;
// if (!visible) return children;
if (!currStep) return null;
const { title, description } = currStep;
const defaultDuration: [number, number] = [300, 250];
const handleSkip = () => {
// to do
dispatch(updateUserGuide({ visible: false }));
};
const handleNext = () => {
dispatch(updateUserGuide({ step: step + 1 }));
};
return (
<>
<OverrideTippyArrowColor />
<Tippy
interactive
className="user-guide-tippy"
arrow={roundArrow}
visible={true}
offset={[0, 18]}
duration={delay ? defaultDuration : 0}
delay={delay ?? [150, 0]}
placement={placement}
content={
<Styled>
<h4 className="title">{title}</h4>
<p className="desc">{description}</p>
<div className="btns">
<button className="skip" onClick={handleSkip}>
Skip
</button>
<button className="next" onClick={handleNext}>
{isLastStep ? `Done` : `Next Step`}
</button>
</div>
</Styled>
}
>
{children}
</Tippy>
</>
);
};
export default UserGuide;
-29
View File
@@ -1,29 +0,0 @@
interface Step {
title: string;
description: string;
}
const steps: Step[] = [
{
title: "Step 1",
description: "hello world 1"
},
{
title: "Step 2",
description: "hello world 1"
},
{
title: "Step 3",
description: "hello world 1"
},
{
title: "Step 4",
description: "hello world 1"
},
{
title: "Step 5",
description: "hello world 1"
}
];
export default steps;
+6 -1
View File
@@ -4,7 +4,12 @@ import { addMessage } from "../../app/slices/message";
import { addChannelMsg } from "../../app/slices/message.channel";
import { addUserMsg } from "../../app/slices/message.user";
export default function useAddLocalFileMessage({ context, to }) {
interface IProps {
context: "channel" | "uesr";
to: number;
}
export default function useAddLocalFileMessage({ context, to }: IProps) {
const dispatch = useDispatch();
const addContextMessage = context == "channel" ? addChannelMsg : addUserMsg;
const addLocalFileMesage = (data) => {
+8 -7
View File
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import { copyImageToClipboard } from "copy-image-clipboard";
import toast from "react-hot-toast";
const useCopy = (config) => {
const useCopy = (config: { enableToast: boolean } | void) => {
const { enableToast = true } = config || {};
const [copied, setCopied] = useState(false);
@@ -12,26 +12,27 @@ const useCopy = (config) => {
}
}, [copied]);
const copyToClipboard = (str) => {
const copyToClipboard = (str: string) => {
const el = document.createElement("textarea");
el.value = str;
el.setAttribute("readonly", "");
el.style.position = "absolute";
el.style.left = "-9999px";
document.body.appendChild(el);
const selected =
document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
const selection = document.getSelection();
if (!selection) return false;
const selected = selection.rangeCount > 0 ? selection.getRangeAt(0) : false;
el.select();
const success = document.execCommand("copy");
document.body.removeChild(el);
if (selected) {
document.getSelection().removeAllRanges();
document.getSelection().addRange(selected);
selection.removeAllRanges();
selection.addRange(selected);
}
return success;
};
const copy = (text, isImage = false) => {
const copy = (text: string, isImage = false) => {
let inter = 0;
console.log("copy", text, isImage);
if (!copied) {
+12 -10
View File
@@ -8,12 +8,12 @@ import { useLazyDeleteUserQuery } from "../../app/services/user";
import useConfig from "./useConfig";
import useCopy from "./useCopy";
import { useAppSelector } from "../../app/store";
interface Props {
interface IProps {
uid?: number;
cid?: number;
}
const useUserOperation: FC<Props> = ({ uid, cid }) => {
const [passedUid, setPassedUid] = useState(undefined);
const useUserOperation = ({ uid, cid }: IProps) => {
const [passedUid, setPassedUid] = useState<number | undefined>(undefined);
const { values: agoraConfig } = useConfig("agora");
const isUserDetailPath = useMatch(`/users/${uid}`);
const [removeUser, { isSuccess: removeUserSuccess }] = useLazyDeleteUserQuery();
@@ -29,7 +29,7 @@ const useUserOperation: FC<Props> = ({ uid, cid }) => {
});
useEffect(() => {
setPassedUid(uid ?? loginUser.uid);
setPassedUid(uid ?? loginUser?.uid);
}, [uid, loginUser]);
useEffect(() => {
@@ -41,21 +41,23 @@ const useUserOperation: FC<Props> = ({ uid, cid }) => {
}
}, [removeSuccess, removeUserSuccess, isUserDetailPath]);
const handleRemoveFromChannel = (id) => {
const handleRemoveFromChannel = (id: number) => {
if (!cid) return;
const isNumber = !Number.isNaN(+id);
const finalId = isNumber ? id || passedUid : passedUid;
if (!finalId) return;
removeInChannel({ id: +cid, members: [+finalId] });
hideAll();
};
const handleRemove = (id) => {
const handleRemove = (id: number) => {
const isNumber = !Number.isNaN(+id);
const finalId = isNumber ? id || passedUid : passedUid;
removeUser(finalId);
hideAll();
};
const copyEmail = (email) => {
const copyEmail = (email: string) => {
const isString = typeof email == "string";
const finalEmail = isString ? email || user?.email : user?.email;
copy(finalEmail);
@@ -67,15 +69,15 @@ const useUserOperation: FC<Props> = ({ uid, cid }) => {
};
const call = () => {
toast.success("Cooming Soon...");
toast.success("Coming Soon...");
hideAll();
};
const isAdmin = loginUser?.is_admin;
const loginUid = loginUser?.uid;
const canRemoveFromChannel =
cid && !channel?.is_public && (isAdmin || channel?.owner == loginUid);
const canCall = agoraConfig.enabled && loginUid != uid;
const canRemove = isAdmin && loginUid != uid && !cid;
const canCall: boolean = agoraConfig.enabled && loginUid != uid;
const canRemove: boolean = isAdmin && loginUid != uid && !cid;
return {
canRemove,