feat: message edit and reaction
This commit is contained in:
@@ -42,6 +42,10 @@ export default function ChannelModal({ personal = false, closeModal }) {
|
||||
toast("please input channel name");
|
||||
return;
|
||||
}
|
||||
if (data.is_public) {
|
||||
// 公共频道 不必有members
|
||||
delete data.members;
|
||||
}
|
||||
createChannel(data);
|
||||
};
|
||||
useEffect(() => {
|
||||
|
||||
@@ -8,6 +8,7 @@ const StyledWrapper = styled.div`
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: 8px;
|
||||
width: 94%;
|
||||
width: -webkit-fill-available;
|
||||
border-radius: 25px;
|
||||
padding: 7px 8px 7px 4px;
|
||||
|
||||
@@ -42,10 +42,12 @@ export default function Commands({
|
||||
message = null,
|
||||
mid = 0,
|
||||
uid = 0,
|
||||
reactions = [],
|
||||
menuVisible,
|
||||
toggleMenu,
|
||||
emojiPopVisible,
|
||||
toggleEmojiPopover,
|
||||
toggleEditMessage,
|
||||
}) {
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
|
||||
@@ -70,10 +72,14 @@ export default function Commands({
|
||||
/>
|
||||
</li>
|
||||
{emojiPopVisible && (
|
||||
<EmojiPicker mid={mid} hidePicker={toggleEmojiPopover} />
|
||||
<EmojiPicker
|
||||
reactions={reactions}
|
||||
mid={mid}
|
||||
hidePicker={toggleEmojiPopover}
|
||||
/>
|
||||
)}
|
||||
{currUid == uid ? (
|
||||
<li className="cmd" onClick={handleClick}>
|
||||
<li className="cmd" onClick={toggleEditMessage}>
|
||||
<img
|
||||
src="https://static.nicegoodthings.com/project/rustchat/icon.edit.svg"
|
||||
alt="icon edit"
|
||||
@@ -95,7 +101,7 @@ export default function Commands({
|
||||
</li>
|
||||
{menuVisible && (
|
||||
<StyledMenu className="menu" ref={menuRef}>
|
||||
<li className="item">Edit Message</li>
|
||||
{/* <li className="item">Edit Message</li> */}
|
||||
<li className="item underline">Pin Message</li>
|
||||
<li className="item">Reply</li>
|
||||
{currUid == uid && (
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import TextareaAutosize from "react-textarea-autosize";
|
||||
import { useKey } from "rooks";
|
||||
import { useEditMessageMutation } from "../../../app/services/message";
|
||||
const StyledWrapper = styled.div`
|
||||
width: 100%;
|
||||
.input {
|
||||
background: #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
textarea {
|
||||
outline: none;
|
||||
width: 100%;
|
||||
background: none;
|
||||
resize: unset;
|
||||
user-select: text;
|
||||
color: #374151;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
}
|
||||
.opts {
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
.opt {
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
button {
|
||||
padding: 0 4px;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #06b6d4;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default function EditMessage({ content, mid, cancelEdit }) {
|
||||
const inputRef = useRef();
|
||||
const [shift, setShift] = useState(false);
|
||||
const [enter, setEnter] = useState(false);
|
||||
const [currMsg, setCurrMsg] = useState(content);
|
||||
const [edit, { isLoading: isEditing, isSuccess }] = useEditMessageMutation();
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
cancelEdit();
|
||||
}
|
||||
}, [isSuccess]);
|
||||
|
||||
useKey(
|
||||
"Shift",
|
||||
(e) => {
|
||||
console.log("shift", e.type);
|
||||
setShift(e.type == "keydown");
|
||||
},
|
||||
{ eventTypes: ["keydown", "keyup"], target: inputRef }
|
||||
);
|
||||
// cancel by esc
|
||||
useKey(
|
||||
"Escape",
|
||||
() => {
|
||||
console.log("cancel edit");
|
||||
cancelEdit();
|
||||
},
|
||||
{ eventTypes: ["keydown", "keyup"], target: inputRef }
|
||||
);
|
||||
const handleMsgChange = (evt) => {
|
||||
if (enter && !shift) {
|
||||
handleSave();
|
||||
} else {
|
||||
setCurrMsg(evt.target.value);
|
||||
}
|
||||
};
|
||||
const handleInputKeydown = (e) => {
|
||||
console.log("keydown event", e);
|
||||
// if(e.key==="Esc")
|
||||
setEnter(e.key === "Enter");
|
||||
};
|
||||
const handleSave = () => {
|
||||
edit({ mid, content: currMsg });
|
||||
};
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="input">
|
||||
<TextareaAutosize
|
||||
autoFocus
|
||||
onFocus={(e) =>
|
||||
e.currentTarget.setSelectionRange(
|
||||
e.currentTarget.value.length,
|
||||
e.currentTarget.value.length
|
||||
)
|
||||
}
|
||||
ref={inputRef}
|
||||
className="content"
|
||||
maxRows={8}
|
||||
minRows={1}
|
||||
onKeyDown={handleInputKeydown}
|
||||
onChange={handleMsgChange}
|
||||
value={currMsg}
|
||||
placeholder={`Edit Message`}
|
||||
/>
|
||||
</div>
|
||||
<div className="opts">
|
||||
<span className="opt">
|
||||
esc to <button onClick={cancelEdit}>cancel</button>
|
||||
</span>
|
||||
<span className="opt">
|
||||
enter to{" "}
|
||||
<button onClick={handleSave}>{isEditing ? "saving" : `save`}</button>
|
||||
</span>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,8 @@ const StyledPicker = styled.div`
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
font-size: 30px;
|
||||
&:hover {
|
||||
&:hover,
|
||||
&.reacted {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
@@ -32,7 +33,7 @@ const emojis = {
|
||||
ok: "👌",
|
||||
like: "❤️",
|
||||
};
|
||||
export default function EmojiPicker({ mid, hidePicker }) {
|
||||
export default function EmojiPicker({ mid, reactions = [], hidePicker }) {
|
||||
const wrapperRef = useRef(null);
|
||||
const [reactMessage, { isLoading }] = useLikeMessageMutation();
|
||||
useOutsideClick(wrapperRef, hidePicker);
|
||||
@@ -46,7 +47,7 @@ export default function EmojiPicker({ mid, hidePicker }) {
|
||||
{Object.entries(emojis).map(([key, emoji]) => {
|
||||
return (
|
||||
<li
|
||||
className="emoji"
|
||||
className={`emoji ${reactions.includes(key) ? "reacted" : ""}`}
|
||||
key={key}
|
||||
onClick={handleReact.bind(null, key)}
|
||||
>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { setUserMsgRead } from "../../../app/slices/message.user";
|
||||
import StyledWrapper from "./styled";
|
||||
import Commands from "./Commands";
|
||||
import { emojis } from "./EmojiPicker";
|
||||
import EditMessage from "./EditMessage";
|
||||
import renderContent from "./renderContent";
|
||||
export default function Message({
|
||||
gid = "",
|
||||
@@ -22,9 +23,11 @@ export default function Message({
|
||||
unread = false,
|
||||
pending,
|
||||
removed = false,
|
||||
likes,
|
||||
edited = false,
|
||||
likes = {},
|
||||
}) {
|
||||
const [myRef, inView] = useInViewRef();
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [emojiPopVisible, setEmojiPopVisible] = useState(false);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const disptach = useDispatch();
|
||||
@@ -33,6 +36,9 @@ export default function Message({
|
||||
const toggleMenu = () => {
|
||||
setMenuVisible((prev) => !prev);
|
||||
};
|
||||
const toggleEditMessage = () => {
|
||||
setEdit((prev) => !prev);
|
||||
};
|
||||
const toggleEmojiPopover = () => {
|
||||
setEmojiPopVisible((prev) => !prev);
|
||||
};
|
||||
@@ -73,44 +79,56 @@ export default function Message({
|
||||
<i className="time">{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}</i>
|
||||
{likes && (
|
||||
<span className="likes">
|
||||
{Object.entries(
|
||||
likes.reduce((acc, val) => {
|
||||
return { ...acc, [val]: (acc[val] || 0) + 1 };
|
||||
}, {})
|
||||
).map(([l, count]) => {
|
||||
return (
|
||||
{Object.entries(likes).map(([reaction, uids]) => {
|
||||
return uids.length > 0 ? (
|
||||
<i
|
||||
className="like"
|
||||
// data-count={count > 1 ? count : ""}
|
||||
key={l}
|
||||
key={reaction}
|
||||
>
|
||||
{emojis[l]}
|
||||
{emojis[reaction]}
|
||||
|
||||
{count > 1 ? <em>{`+${count}`} </em> : null}
|
||||
{uids.length > 1 ? <em>{`+${uids.length}`} </em> : null}
|
||||
</i>
|
||||
);
|
||||
) : null;
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={`down ${pending ? "pending" : ""}`}>
|
||||
{renderContent(content_type, content)}
|
||||
{edit ? (
|
||||
<EditMessage
|
||||
content={content}
|
||||
mid={mid}
|
||||
cancelEdit={toggleEditMessage}
|
||||
/>
|
||||
) : (
|
||||
renderContent(content_type, content, edited)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Commands
|
||||
message={{
|
||||
name: currUser.name,
|
||||
avatar: currUser.avatar,
|
||||
time,
|
||||
content: renderContent(content_type, content),
|
||||
}}
|
||||
mid={mid}
|
||||
uid={fromUid}
|
||||
toggleMenu={toggleMenu}
|
||||
menuVisible={menuVisible}
|
||||
emojiPopVisible={emojiPopVisible}
|
||||
toggleEmojiPopover={toggleEmojiPopover}
|
||||
/>
|
||||
{!edit && (
|
||||
<Commands
|
||||
message={{
|
||||
name: currUser.name,
|
||||
avatar: currUser.avatar,
|
||||
time,
|
||||
content: renderContent(content_type, content),
|
||||
}}
|
||||
reactions={Object.entries(likes ?? {})
|
||||
.filter(([reaction, uids = []]) => uids.includes(currUser.uid))
|
||||
.map(([reaction]) => {
|
||||
return reaction;
|
||||
})}
|
||||
mid={mid}
|
||||
uid={fromUid}
|
||||
toggleMenu={toggleMenu}
|
||||
menuVisible={menuVisible}
|
||||
emojiPopVisible={emojiPopVisible}
|
||||
toggleEmojiPopover={toggleEmojiPopover}
|
||||
toggleEditMessage={toggleEditMessage}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import Linkify from "react-linkify";
|
||||
import dayjs from "dayjs";
|
||||
import BASE_URL from "../../../app/config";
|
||||
const renderContent = (type, content) => {
|
||||
const renderContent = (type, content, edited = false) => {
|
||||
let ctn = null;
|
||||
switch (type) {
|
||||
case "text/plain":
|
||||
ctn = (
|
||||
<Linkify
|
||||
componentDecorator={(decoratedHref, decoratedText, key) => (
|
||||
<a target="blank" href={decoratedHref} key={key}>
|
||||
{decoratedText}
|
||||
</a>
|
||||
<>
|
||||
<Linkify
|
||||
componentDecorator={(decoratedHref, decoratedText, key) => (
|
||||
<a target="blank" href={decoratedHref} key={key}>
|
||||
{decoratedText}
|
||||
</a>
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Linkify>
|
||||
{edited && (
|
||||
<span
|
||||
className="edited"
|
||||
title={dayjs(edited).format("YYYY-MM-DD h:mm:ss A")}
|
||||
>
|
||||
(edited)
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</Linkify>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
case "image/jpeg":
|
||||
|
||||
@@ -26,6 +26,7 @@ const StyledMsg = styled.div`
|
||||
}
|
||||
}
|
||||
.details {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -78,6 +79,11 @@ const StyledMsg = styled.div`
|
||||
line-height: 20px;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
.edited {
|
||||
margin-left: 5px;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
}
|
||||
&.pending {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// import React from "react";
|
||||
import {
|
||||
updateChannelMsg,
|
||||
addChannelMsg,
|
||||
deleteChannelMsg,
|
||||
likeChannelMsg,
|
||||
} from "../../../app/slices/message.channel";
|
||||
import {
|
||||
updateUserMsg,
|
||||
addUserMsg,
|
||||
deleteUserMsg,
|
||||
likeUserMsg,
|
||||
@@ -14,15 +16,27 @@ import { useDispatch } from "react-redux";
|
||||
export default function useMessageHandler(currUser) {
|
||||
const { showNotification } = useNotification();
|
||||
const dispatch = useDispatch();
|
||||
const handleReaction = ({ from = "user", id, mid, detail = {} }) => {
|
||||
const dispatchReaction = ({
|
||||
to = "user",
|
||||
id,
|
||||
from_uid,
|
||||
mid,
|
||||
created_at,
|
||||
detail = {},
|
||||
}) => {
|
||||
const { type = "" } = detail;
|
||||
const deleteMsg = from == "user" ? deleteUserMsg : deleteChannelMsg;
|
||||
const likeMsg = from == "user" ? likeUserMsg : likeChannelMsg;
|
||||
const updateMsg = to == "user" ? updateUserMsg : updateChannelMsg;
|
||||
const deleteMsg = to == "user" ? deleteUserMsg : deleteChannelMsg;
|
||||
const likeMsg = to == "user" ? likeUserMsg : likeChannelMsg;
|
||||
switch (type) {
|
||||
case "edit":
|
||||
{
|
||||
const { content } = detail;
|
||||
dispatch(updateMsg({ id, mid, content, time: created_at }));
|
||||
}
|
||||
break;
|
||||
case "like":
|
||||
dispatch(likeMsg({ id, mid, action: detail.action }));
|
||||
dispatch(likeMsg({ id, from_uid, mid, action: detail.action }));
|
||||
break;
|
||||
case "delete":
|
||||
dispatch(deleteMsg({ id, mid }));
|
||||
@@ -32,98 +46,20 @@ export default function useMessageHandler(currUser) {
|
||||
break;
|
||||
}
|
||||
};
|
||||
const handleUserMessage = ({
|
||||
self = false,
|
||||
uid,
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
mid,
|
||||
detailMid,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
}) => {
|
||||
switch (type) {
|
||||
case "normal":
|
||||
dispatch(
|
||||
addUserMsg({
|
||||
// 此处需要特别注意
|
||||
id: self ? uid : from_uid,
|
||||
from_uid: from_uid,
|
||||
unread: !self,
|
||||
created_at,
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "reaction":
|
||||
handleReaction({
|
||||
from: "user",
|
||||
id: self ? uid : from_uid,
|
||||
mid: detailMid,
|
||||
detail,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (!self && type == "normal") {
|
||||
const dispatchAddMessage = ({ to = "user", id, self = false, common }) => {
|
||||
const addMessage = to == "user" ? addUserMsg : addChannelMsg;
|
||||
dispatch(
|
||||
addMessage({
|
||||
id, // 自己发的 就不用标记未读
|
||||
unread: !self,
|
||||
...common,
|
||||
})
|
||||
);
|
||||
if (!self) {
|
||||
showNotification({
|
||||
body: content,
|
||||
body: common.content,
|
||||
data: {
|
||||
path: `/chat/dm/${from_uid}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const handleChannelMessage = ({
|
||||
gid,
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
mid,
|
||||
detailMid,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
}) => {
|
||||
const isSelf = from_uid == currUser.uid;
|
||||
switch (type) {
|
||||
case "normal":
|
||||
dispatch(
|
||||
addChannelMsg({
|
||||
id: gid,
|
||||
from_uid,
|
||||
// 自己发的 就不用标记未读
|
||||
unread: !isSelf,
|
||||
created_at,
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "reaction":
|
||||
handleReaction({ from: "channel", id: gid, mid: detailMid, detail });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// group message notification
|
||||
if (!isSelf && type == "normal") {
|
||||
showNotification({
|
||||
body: content,
|
||||
data: {
|
||||
path: `/chat/channel/${gid}`,
|
||||
path: `/chat/${to}/${id}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -143,35 +79,37 @@ export default function useMessageHandler(currUser) {
|
||||
detail = {},
|
||||
},
|
||||
} = data;
|
||||
if (typeof target.gid !== "undefined") {
|
||||
// channel message
|
||||
handleChannelMessage({
|
||||
gid: target.gid,
|
||||
created_at,
|
||||
mid,
|
||||
detailMid,
|
||||
from_uid,
|
||||
content,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
});
|
||||
} else {
|
||||
const isSelf = data.from_uid == currUser.uid;
|
||||
handleUserMessage({
|
||||
self: isSelf,
|
||||
uid: target.uid,
|
||||
from_uid,
|
||||
created_at,
|
||||
content,
|
||||
mid,
|
||||
detailMid,
|
||||
content_type,
|
||||
expires_in,
|
||||
type,
|
||||
detail,
|
||||
});
|
||||
const to = typeof target.gid !== "undefined" ? "channel" : "user";
|
||||
const self = from_uid == currUser.uid;
|
||||
const id = to == "user" ? (self ? target.uid : from_uid) : target.gid;
|
||||
switch (type) {
|
||||
case "normal":
|
||||
{
|
||||
dispatchAddMessage({
|
||||
to,
|
||||
id,
|
||||
self,
|
||||
common: {
|
||||
mid,
|
||||
content,
|
||||
content_type,
|
||||
from_uid,
|
||||
created_at,
|
||||
expires_in,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "reaction": {
|
||||
dispatchReaction({
|
||||
to,
|
||||
id,
|
||||
from_uid,
|
||||
created_at,
|
||||
mid: detailMid,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
return handleMessage;
|
||||
|
||||
@@ -92,6 +92,12 @@ export default function Send({
|
||||
<div className="input">
|
||||
<TextareaAutosize
|
||||
// autoFocus
|
||||
onFocus={(e) =>
|
||||
e.currentTarget.setSelectionRange(
|
||||
e.currentTarget.value.length,
|
||||
e.currentTarget.value.length
|
||||
)
|
||||
}
|
||||
ref={inputRef}
|
||||
className="content"
|
||||
maxRows={8}
|
||||
|
||||
Reference in New Issue
Block a user