feat: lots updates
This commit is contained in:
@@ -7,7 +7,6 @@ import ChannelIcon from "../ChannelIcon";
|
||||
import Contact from "../Contact";
|
||||
import StyledWrapper from "./styled";
|
||||
import useFilteredUsers from "../../hook/useFilteredUsers";
|
||||
import { useGetChannelsQuery } from "../../../app/services/channel";
|
||||
import { useCreateChannelMutation } from "../../../app/services/channel";
|
||||
|
||||
export default function ChannelModal({ personal = false, closeModal }) {
|
||||
@@ -19,7 +18,6 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
is_public: !personal,
|
||||
});
|
||||
const { contacts, input, updateInput } = useFilteredUsers();
|
||||
const { refetch: refetchChannels } = useGetChannelsQuery();
|
||||
const [
|
||||
createChannel,
|
||||
{ isSuccess, isError, isLoading, data: newChannel },
|
||||
@@ -49,7 +47,6 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
if (isSuccess) {
|
||||
toast.success("create new channel success");
|
||||
closeModal();
|
||||
refetchChannels();
|
||||
navigateTo(`/chat/channel/${newChannel.gid}`);
|
||||
}
|
||||
}, [isSuccess, newChannel]);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// import React from 'react';
|
||||
import { useEffect } 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 { useGetContactsQuery } from "../../app/services/contact";
|
||||
|
||||
import { setChannelMsgRead } from "../../app/slices/message.channel";
|
||||
import { setUserMsgRead } from "../../app/slices/message.user";
|
||||
const StyledMsg = styled.div`
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -44,24 +46,50 @@ const StyledMsg = styled.div`
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
&.pending {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default function Message({ uid, time, content }) {
|
||||
export default function Message({
|
||||
gid = "",
|
||||
mid = "",
|
||||
uid,
|
||||
fromUid,
|
||||
time,
|
||||
content,
|
||||
unread = false,
|
||||
pending,
|
||||
}) {
|
||||
const [myRef, inView] = useInViewRef();
|
||||
const disptach = useDispatch();
|
||||
// const wrapperRef = 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 }));
|
||||
}
|
||||
// }
|
||||
}, [gid, mid, uid, unread, inView]);
|
||||
|
||||
if (!contacts) return null;
|
||||
const currUser = contacts.find((c) => c.uid == uid);
|
||||
const currUser = contacts.find((c) => c.uid == fromUid) || {};
|
||||
return (
|
||||
<StyledMsg>
|
||||
<StyledMsg ref={myRef}>
|
||||
<div className="avatar" data-uid={uid}>
|
||||
<Avatar url={currUser.avatar} id={uid} name={currUser.name} />
|
||||
<Avatar url={currUser.avatar} id={fromUid} name={currUser.name} />
|
||||
</div>
|
||||
<div className="details">
|
||||
<div className="up">
|
||||
<span className="name">{currUser.name}</span>
|
||||
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
|
||||
</div>
|
||||
<div className="down">{content}</div>
|
||||
<div className={`down ${pending ? "pending" : ""}`}>{content}</div>
|
||||
</div>
|
||||
</StyledMsg>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import React, { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
import BASE_URL from "../../app/config";
|
||||
import {
|
||||
setChannels,
|
||||
addChannel,
|
||||
deleteChannel,
|
||||
} from "../../app/slices/channels";
|
||||
import { clearAuthData } from "../../app/slices/auth.data";
|
||||
|
||||
import { addChannelMsg } from "../../app/slices/message.channel";
|
||||
import { addUserMsg } from "../../app/slices/message.user";
|
||||
|
||||
const NotificationHub = () => {
|
||||
const NotificationHub = ({ token }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { token } = useSelector((store) => store.authData);
|
||||
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
let sse = null;
|
||||
if (token) {
|
||||
@@ -24,6 +33,7 @@ const NotificationHub = () => {
|
||||
};
|
||||
}
|
||||
return () => {
|
||||
console.log("re-run see init");
|
||||
if (sse) {
|
||||
sse.close();
|
||||
}
|
||||
@@ -31,12 +41,44 @@ const NotificationHub = () => {
|
||||
}, [token]);
|
||||
const handleSSEMessage = (data) => {
|
||||
const { type } = data;
|
||||
|
||||
switch (type) {
|
||||
case "heartbeat":
|
||||
console.log("heartbeat");
|
||||
break;
|
||||
case "kick":
|
||||
{
|
||||
console.log("kicked");
|
||||
switch (data.reason) {
|
||||
case "login_from_other_device":
|
||||
dispatch(clearAuthData());
|
||||
navigate("/login");
|
||||
toast("kicked from the other device");
|
||||
break;
|
||||
case "delete_user":
|
||||
dispatch(clearAuthData());
|
||||
navigate("/login");
|
||||
toast("sorry, your account has been deleted");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "related_groups":
|
||||
console.log("joined group list", data);
|
||||
dispatch(setChannels(data.groups));
|
||||
break;
|
||||
case "joined_group":
|
||||
console.log("joined group list", data.group);
|
||||
dispatch(addChannel(data.group));
|
||||
break;
|
||||
case "kick_from_group":
|
||||
console.log("joined group list", data.gid);
|
||||
dispatch(deleteChannel(data.gid));
|
||||
break;
|
||||
case "chat":
|
||||
console.log("chat data", data);
|
||||
// console.log("chat data", data);
|
||||
if (data.gid) {
|
||||
const { gid, ...rest } = data;
|
||||
dispatch(addChannelMsg({ id: gid, ...rest }));
|
||||
@@ -46,10 +88,13 @@ const NotificationHub = () => {
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("sse event data", data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
export default NotificationHub;
|
||||
function compareToken(prevHub, nextHub) {
|
||||
return prevHub.token === nextHub.token;
|
||||
}
|
||||
export default React.memo(NotificationHub, compareToken);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// import React from 'react'
|
||||
import { Navigate } from "react-router-dom";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
export default function RequireAuth({ children, redirectTo = "/login" }) {
|
||||
const { token } = useSelector((store) => store.authData);
|
||||
return token ? children : <Navigate to={redirectTo} />;
|
||||
}
|
||||
@@ -21,6 +21,7 @@ const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
.input {
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: none;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { 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";
|
||||
|
||||
const StyledSend = styled.div`
|
||||
position: sticky;
|
||||
top: calc(100vh - 90px);
|
||||
left: 16px;
|
||||
background: #f5f6f7;
|
||||
border-radius: 8px;
|
||||
width: 884px;
|
||||
min-height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 4px 18px;
|
||||
.addon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.content {
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #616161;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
background: green;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
`;
|
||||
const Types = {
|
||||
channel: "#",
|
||||
user: "@",
|
||||
};
|
||||
export default function Send({ name, type = "channel", id = "" }) {
|
||||
const dispatch = useDispatch();
|
||||
const [
|
||||
sendMsg,
|
||||
{ isLoading: sending, isSuccess: sendSuccess, data: sendData },
|
||||
] = useSendMsgMutation();
|
||||
const [
|
||||
sendChannelMsg,
|
||||
{
|
||||
isLoading: channelSending,
|
||||
isSuccess: sendChannelSuccess,
|
||||
data: sendChannelData,
|
||||
},
|
||||
] = useSendChannelMsgMutation();
|
||||
const [msg, setMsg] = useState("");
|
||||
const handleMsgChange = (evt) => {
|
||||
setMsg(evt.target.value);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (sendSuccess) {
|
||||
dispatch(addUserMsg({ id, ...sendData }));
|
||||
setMsg("");
|
||||
}
|
||||
}, [sendSuccess, sendData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sendChannelSuccess) {
|
||||
const { gid, ...rest } = sendChannelData;
|
||||
dispatch(addChannelMsg({ id: gid, ...rest }));
|
||||
setMsg("");
|
||||
}
|
||||
}, [sendChannelSuccess, sendChannelData]);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!msg || !type || !id) return;
|
||||
switch (type) {
|
||||
case "channel":
|
||||
sendChannelMsg({ gid: id, message: msg });
|
||||
break;
|
||||
case "user":
|
||||
sendMsg({ uid: id, message: msg });
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<StyledSend className="send">
|
||||
<MdAdd className="addon" size={20} color="#78787C" />
|
||||
<div className="input">
|
||||
<TextareaAutosize
|
||||
className="content"
|
||||
maxRows={8}
|
||||
minRows={1}
|
||||
onChange={handleMsgChange}
|
||||
value={msg}
|
||||
placeholder={`给 ${Types[type]}${name} 发消息`}
|
||||
/>
|
||||
<button
|
||||
disabled={sending || channelSending}
|
||||
className="btn"
|
||||
onClick={handleSendMessage}
|
||||
>
|
||||
{/* {sending ? `Sending` : `Send`} */}
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
<div className="emoji">emoji</div>
|
||||
</StyledSend>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { MdAdd } from "react-icons/md";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useKey } from "rooks";
|
||||
import "emoji-mart/css/emoji-mart.css";
|
||||
import { Picker } from "emoji-mart";
|
||||
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 StyledSend from "./styled";
|
||||
|
||||
const Types = {
|
||||
channel: "#",
|
||||
user: "@",
|
||||
};
|
||||
export default function Send({ name, type = "channel", id = "" }) {
|
||||
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();
|
||||
const toggleEmojiPicker = () => {
|
||||
setEmojiPicker((prev) => !prev);
|
||||
};
|
||||
const [
|
||||
sendMsg,
|
||||
{ isLoading: sending, isSuccess: sendSuccess, data: sendData },
|
||||
] = useSendMsgMutation();
|
||||
const [
|
||||
sendChannelMsg,
|
||||
{
|
||||
isLoading: channelSending,
|
||||
isSuccess: sendChannelSuccess,
|
||||
data: sendChannelData,
|
||||
},
|
||||
] = useSendChannelMsgMutation();
|
||||
useKey(
|
||||
"Shift",
|
||||
(e) => {
|
||||
console.log("shift", e.type);
|
||||
setShift(e.type == "keydown");
|
||||
},
|
||||
{ eventTypes: ["keydown", "keyup"], target: inputRef }
|
||||
);
|
||||
const handleMsgChange = (evt) => {
|
||||
if (enter && !shift) {
|
||||
handleSendMessage();
|
||||
} else {
|
||||
setMsg(evt.target.value);
|
||||
// inputRef.current.focus();
|
||||
}
|
||||
};
|
||||
const handleInputKeydown = (e) => {
|
||||
setEnter(e.key === "Enter");
|
||||
};
|
||||
const handleEmojiSelect = (emoji) => {
|
||||
console.log(emoji);
|
||||
setMsg((prev) => `${prev}${emoji.native}`);
|
||||
// inputRef.current.focus();
|
||||
toggleEmojiPicker();
|
||||
};
|
||||
useEffect(() => {
|
||||
if (sendSuccess) {
|
||||
dispatch(addUserMsg({ id, ...sendData, unread: false }));
|
||||
setMsg("");
|
||||
}
|
||||
}, [sendSuccess, sendData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sendChannelSuccess) {
|
||||
const { gid, ...rest } = sendChannelData;
|
||||
dispatch(addChannelMsg({ id: gid, ...rest, unread: false }));
|
||||
setMsg("");
|
||||
}
|
||||
}, [sendChannelSuccess, sendChannelData]);
|
||||
useEffect(() => {
|
||||
inputRef.current.focus();
|
||||
}, [msg]);
|
||||
|
||||
const handleSendMessage = () => {
|
||||
if (!msg || !type || !id) return;
|
||||
switch (type) {
|
||||
case "channel":
|
||||
sendChannelMsg({ gid: id, message: msg });
|
||||
break;
|
||||
case "user":
|
||||
sendMsg({ uid: id, message: msg });
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
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} 发消息`}
|
||||
/>
|
||||
</div>
|
||||
<div className="emoji">
|
||||
<button className="toggle" onClick={toggleEmojiPicker}>
|
||||
😄
|
||||
</button>
|
||||
{emojiPicker && (
|
||||
<div className="picker">
|
||||
<Picker
|
||||
onSelect={handleEmojiSelect}
|
||||
showPreview={false}
|
||||
showSkinTones={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</StyledSend>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledSend = styled.div`
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #f5f6f7;
|
||||
border-radius: 8px;
|
||||
width: calc(100% - 32px);
|
||||
min-height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 4px 18px;
|
||||
/* margin: 0 16px; */
|
||||
.addon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.input {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
.content {
|
||||
outline: none;
|
||||
padding: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #616161;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
background: green;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
.emoji {
|
||||
position: relative;
|
||||
.toggle {
|
||||
font-size: 22px;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
.picker {
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
right: 0;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledSend;
|
||||
Reference in New Issue
Block a user