feat: lots of updates

This commit is contained in:
zerosoul
2022-02-10 23:35:25 +08:00
parent e8b6c2b0f1
commit d05c5ff8f2
33 changed files with 1072 additions and 331 deletions
+5 -1
View File
@@ -1,16 +1,19 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useSelector, useDispatch } from "react-redux";
import Modal from "../Modal";
import ChannelIcon from "../ChannelIcon";
import Contact from "../Contact";
import StyledWrapper from "./styled";
import useFilteredUsers from "../../hook/useFilteredUsers";
import { addChannel } from "../../../app/slices/channels";
import { useCreateChannelMutation } from "../../../app/services/channel";
export default function ChannelModal({ personal = false, closeModal }) {
const navigateTo = useNavigate();
const dispatch = useDispatch();
const [data, setData] = useState({
name: "",
dsecription: "",
@@ -47,6 +50,7 @@ export default function ChannelModal({ personal = false, closeModal }) {
if (isSuccess) {
toast.success("create new channel success");
closeModal();
dispatch(addChannel(newChannel));
navigateTo(`/chat/channel/${newChannel.gid}`);
}
}, [isSuccess, newChannel]);
+42 -10
View File
@@ -1,9 +1,10 @@
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import styled from "styled-components";
import dayjs from "dayjs";
import { useDispatch } from "react-redux";
import { useInViewRef } from "rooks";
import Avatar from "./Avatar";
import BASE_URL from "../../app/config";
import { useGetContactsQuery } from "../../app/services/contact";
import { setChannelMsgRead } from "../../app/slices/message.channel";
import { setUserMsgRead } from "../../app/slices/message.user";
@@ -50,9 +51,32 @@ const StyledMsg = styled.div`
&.pending {
opacity: 0.5;
}
.img {
max-width: 400px;
}
}
}
`;
const renderContent = (type, content) => {
let ctn = null;
switch (type) {
case "text/plain":
ctn = content;
break;
case "image/jpeg":
ctn = (
<img
className="img"
src={`${BASE_URL}/resource/image?id=${encodeURIComponent(content)}`}
/>
);
break;
default:
break;
}
return ctn;
};
export default function Message({
gid = "",
mid = "",
@@ -60,28 +84,34 @@ export default function Message({
fromUid,
time,
content,
content_type = "text/plain",
unread = false,
pending,
}) {
const [myRef, inView] = useInViewRef();
const disptach = useDispatch();
// const wrapperRef = useRef(null);
const avatarRef = useRef(null);
const { data: contacts } = useGetContactsQuery();
useEffect(() => {
// if (wrapperRef) {
// wrapperRef.current.scrollIntoView();
if (inView && unread) {
const setMsgRead = gid ? setChannelMsgRead : setUserMsgRead;
disptach(setMsgRead({ id: gid || uid, mid }));
if (!unread) {
avatarRef.current?.scrollIntoView(false);
}
}, [unread]);
useEffect(() => {
if (inView) {
if (unread) {
const setMsgRead = gid ? setChannelMsgRead : setUserMsgRead;
disptach(setMsgRead({ id: gid || uid, mid }));
}
}
// }
}, [gid, mid, uid, unread, inView]);
if (!contacts) return null;
const currUser = contacts.find((c) => c.uid == fromUid) || {};
return (
<StyledMsg ref={myRef}>
<div className="avatar" data-uid={uid}>
<div className="avatar" data-uid={uid} ref={avatarRef}>
<Avatar url={currUser.avatar} id={fromUid} name={currUser.name} />
</div>
<div className="details">
@@ -89,7 +119,9 @@ export default function Message({
<span className="name">{currUser.name}</span>
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
</div>
<div className={`down ${pending ? "pending" : ""}`}>{content}</div>
<div className={`down ${pending ? "pending" : ""}`}>
{renderContent(content_type, content)}
</div>
</div>
</StyledMsg>
);
+25 -5
View File
@@ -10,19 +10,35 @@ import {
addChannel,
deleteChannel,
} from "../../app/slices/channels";
import { clearAuthData, setUsersVersion } from "../../app/slices/auth.data";
import {
clearAuthData,
setUsersVersion,
setAfterMid,
} from "../../app/slices/auth.data";
import { addChannelMsg } from "../../app/slices/message.channel";
import { addUserMsg } from "../../app/slices/message.user";
const NotificationHub = ({ token, usersVersion = 0 }) => {
const getQueryString = (params = {}) => {
const sp = new URLSearchParams();
Object.entries(params).forEach(([key, val]) => {
if (val) {
sp.append(key, val);
}
});
return sp.toString();
};
const NotificationHub = ({ token, usersVersion = 0, afterMid = 0 }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
useEffect(() => {
let sse = null;
if (token) {
sse = new EventSource(
`${BASE_URL}/user/events?api-key=${token}&users_version=${usersVersion}`
`${BASE_URL}/user/events?${getQueryString({
"api-key": token,
users_version: usersVersion,
after_mid: afterMid,
})}`
);
sse.onopen = () => {
console.info("sse opened");
@@ -40,7 +56,7 @@ const NotificationHub = ({ token, usersVersion = 0 }) => {
sse.close();
}
};
}, [token]);
}, [token, usersVersion, afterMid]);
const handleSSEMessage = (data) => {
const { type } = data;
@@ -89,11 +105,15 @@ const NotificationHub = ({ token, usersVersion = 0 }) => {
case "chat":
// console.log("chat data", data);
if (data.gid) {
// channel msg
const { gid, ...rest } = data;
dispatch(addChannelMsg({ id: gid, ...rest }));
} else {
// user msg
dispatch(addUserMsg({ id: data.from_uid, ...data }));
}
// 更新after_mid
dispatch(setAfterMid({ mid: data.mid }));
break;
default:
@@ -0,0 +1,119 @@
import { useState, useEffect } from "react";
import { useDispatch } from "react-redux";
// import toast from "react-hot-toast";
// import { useNavigate } from "react-router-dom";
// import { useSelector, useDispatch } from "react-redux";
import { useSendChannelMsgMutation } from "../../../../app/services/channel";
import { useSendMsgMutation } from "../../../../app/services/contact";
import { addChannelMsg } from "../../../../app/slices/message.channel";
import { addUserMsg } from "../../../../app/slices/message.user";
import Modal from "../../Modal";
import StyledWrapper from "./styled";
export default function UploadModal({
type = "user",
sendTo = 0,
files = [],
closeModal,
}) {
const dispatch = useDispatch();
const [
sendChannelMsg,
{
error: sendChannelError,
isLoading: channelSending,
isSuccess: sendChannelSuccess,
data: sendChannelData,
},
] = useSendChannelMsgMutation();
const [
sendUserMsg,
{
error: sendUserError,
isLoading: userSending,
isSuccess: sendUserSuccess,
data: sendUserData,
},
] = useSendMsgMutation();
const [blobs, setBlobs] = useState([]);
useEffect(() => {
files.forEach((file) => {
var fileReader = new FileReader();
fileReader.onloadend = (e) => {
const { name, size, type } = file;
const obj = { name, size, type };
let dataUrl = e.target.result;
console.log({ dataUrl }, e.target);
// let blob = new Blob([arrayBuffer]);
obj.data = dataUrl;
setBlobs((prevs) => {
return [...prevs, obj];
});
};
fileReader.readAsDataURL(file);
});
}, [files]);
const handleUpload = () => {
const uploadFn = type == "user" ? sendUserMsg : sendChannelMsg;
uploadFn({ id: sendTo, content: files[0], type: "image" });
};
useEffect(() => {
if (sendUserSuccess) {
dispatch(addUserMsg({ id: sendTo, ...sendUserData, unread: false }));
closeModal();
}
}, [sendUserSuccess, sendUserData]);
useEffect(() => {
if (sendChannelSuccess) {
const { gid, ...rest } = sendChannelData;
dispatch(addChannelMsg({ id: gid, ...rest, unread: false }));
closeModal();
}
}, [sendChannelSuccess, sendChannelData]);
if (!sendTo) return null;
return (
<Modal>
<StyledWrapper className="animate__animated animate__fadeInDown animate__faster">
<h3 className="head">Upload a file</h3>
<p className="intro">Photos accept jpg, png, max size limit to 10M.</p>
<ul className="list">
{blobs.map((b, idx) => {
console.log({ b });
return (
<li key={idx} className="item">
<img
src={b.data}
// src={URL.createObjectURL(b.data)}
alt="thumb"
className="thumb"
/>
<div className="right">
<div className="name">
<span contentEditable className="input">
{b.name}
</span>
<i className="tip">(click title to change name)</i>
</div>
<i className="size">{`${Math.floor(b.size / 1000)}KB`}</i>
</div>
</li>
);
})}
</ul>
<div className="btns">
<button className="btn cancel" onClick={closeModal}>
Cancel
</button>
<button
className="btn upload"
disabled={channelSending || userSending}
onClick={handleUpload}
>
{channelSending || userSending ? "Uploading" : `Upload`}
</button>
</div>
</StyledWrapper>
</Modal>
);
}
@@ -0,0 +1,96 @@
import styled from "styled-components";
const StyledWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 16px;
background-color: #fff;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
.head {
font-style: normal;
font-weight: 600;
font-size: 20px;
line-height: 30px;
margin-bottom: 8px;
}
.intro {
font-style: normal;
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #6b7280;
}
.list {
padding-top: 32px;
display: flex;
flex-direction: column;
gap: 8px;
.item {
padding: 16px;
border: 1px solid rgba(116, 127, 141, 0.2);
border-radius: 6px;
display: flex;
align-items: center;
gap: 16px;
min-width: 473px;
.thumb {
object-fit: cover;
width: 64px;
height: 64px;
border-radius: 6px;
}
.right {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
.name {
font-style: normal;
.input {
user-select: text;
line-height: 20px;
font-size: 14px;
font-weight: 600;
margin-right: 8px;
}
.tip {
line-height: 18px;
font-size: 12px;
color: #78787c;
}
}
.size {
font-size: 14px;
line-height: 20px;
color: #616161;
}
}
}
}
.btns {
padding-top: 32px;
padding-bottom: 16px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 16px;
width: 100%;
.btn {
color: #fff;
padding: 8px 16px;
background: #1fe1f9;
/* shadow-base */
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
border-radius: 4px;
&.cancel {
color: #333;
border: 1px solid #e5e7eb;
background-color: #fff;
}
}
}
`;
export default StyledWrapper;
+71 -33
View File
@@ -10,18 +10,32 @@ import { useSendMsgMutation } from "../../../app/services/contact";
import { addChannelMsg } from "../../../app/slices/message.channel";
import { addUserMsg } from "../../../app/slices/message.user";
import StyledSend from "./styled";
import UploadModal from "./UploadModal";
const Types = {
channel: "#",
user: "@",
};
export default function Send({ name, type = "channel", id = "" }) {
export default function Send({
name,
type = "channel",
id = "",
dragFiles = [],
}) {
const [files, setFiles] = useState([]);
const inputRef = useRef();
const [emojiPicker, setEmojiPicker] = useState(false);
const [shift, setShift] = useState(false);
const [enter, setEnter] = useState(false);
const [msg, setMsg] = useState("");
const dispatch = useDispatch();
console.log("send drag files", dragFiles);
useEffect(() => {
if (dragFiles.length) {
setFiles((prev) => [...prev, ...dragFiles]);
}
}, [dragFiles]);
const toggleEmojiPicker = () => {
setEmojiPicker((prev) => !prev);
};
@@ -79,15 +93,20 @@ export default function Send({ name, type = "channel", id = "" }) {
useEffect(() => {
inputRef.current.focus();
}, [msg]);
const handleUpload = (evt) => {
setFiles([...evt.target.files]);
};
const resetFiles = () => {
setFiles([]);
};
const handleSendMessage = () => {
if (!msg || !type || !id) return;
switch (type) {
case "channel":
sendChannelMsg({ gid: id, message: msg });
sendChannelMsg({ id, content: msg });
break;
case "user":
sendMsg({ uid: id, message: msg });
sendMsg({ id, content: msg });
break;
default:
@@ -95,35 +114,54 @@ export default function Send({ name, type = "channel", id = "" }) {
}
};
return (
<StyledSend className="send">
<MdAdd className="addon" size={20} color="#78787C" />
<div className="input">
<TextareaAutosize
// autoFocus
ref={inputRef}
className="content"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
onChange={handleMsgChange}
value={msg}
placeholder={`${Types[type]}${name} 发消息`}
<>
<StyledSend className="send">
<div className="addon">
<MdAdd size={20} color="#78787C" />
<input
multiple={true}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</div>
<div className="input">
<TextareaAutosize
// autoFocus
ref={inputRef}
className="content"
maxRows={8}
minRows={1}
onKeyDown={handleInputKeydown}
onChange={handleMsgChange}
value={msg}
placeholder={`${Types[type]}${name} 发消息`}
/>
</div>
<div className="emoji">
<button className="toggle" onClick={toggleEmojiPicker}>
😄
</button>
{emojiPicker && (
<div className="picker">
<Picker
onSelect={handleEmojiSelect}
showPreview={false}
showSkinTones={false}
/>
</div>
)}
</div>
</StyledSend>
{files.length !== 0 && (
<UploadModal
type={type}
files={files}
sendTo={id}
closeModal={resetFiles}
/>
</div>
<div className="emoji">
<button className="toggle" onClick={toggleEmojiPicker}>
😄
</button>
{emojiPicker && (
<div className="picker">
<Picker
onSelect={handleEmojiSelect}
showPreview={false}
showSkinTones={false}
/>
</div>
)}
</div>
</StyledSend>
)}
</>
);
}
+10
View File
@@ -16,6 +16,16 @@ const StyledSend = styled.div`
/* margin: 0 16px; */
.addon {
cursor: pointer;
position: relative;
input {
opacity: 0;
cursor: pointer;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
}
.input {
width: 100%;