refactor: add more TS constraints
This commit is contained in:
Vendored
+3
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user