feat: lots of updates
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user