feat: message edit and reaction

This commit is contained in:
zerosoul
2022-03-08 11:24:02 +08:00
parent 05896932b5
commit c99be639dc
16 changed files with 357 additions and 177 deletions
@@ -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(() => {
+1
View File
@@ -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;
+9 -3
View File
@@ -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 && (
+122
View File
@@ -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>
);
}
+4 -3
View File
@@ -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)}
>
+44 -26
View File
@@ -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>
);
}
+20 -9
View File
@@ -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":
+6
View File
@@ -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;
+6
View File
@@ -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}