diff --git a/src/app/services/message.js b/src/app/services/message.js
index 4bc95ad0..60b58a95 100644
--- a/src/app/services/message.js
+++ b/src/app/services/message.js
@@ -1,5 +1,5 @@
import { createApi } from "@reduxjs/toolkit/query/react";
-// import BASE_URL from "../config";
+import { ContentTypes } from "../config";
import baseQuery from "./base.query";
@@ -8,10 +8,13 @@ export const messageApi = createApi({
baseQuery,
endpoints: (builder) => ({
editMessage: builder.mutation({
- query: ({ mid, data }) => ({
+ query: ({ mid, content, type = "text" }) => ({
+ headers: {
+ "content-type": ContentTypes[type],
+ },
url: `/message/${mid}/edit`,
method: "PUT",
- body: data,
+ body: content,
}),
}),
likeMessage: builder.mutation({
diff --git a/src/app/slices/message.channel.js b/src/app/slices/message.channel.js
index 219091fd..198b5730 100644
--- a/src/app/slices/message.channel.js
+++ b/src/app/slices/message.channel.js
@@ -48,12 +48,38 @@ const channelMsgSlice = createSlice({
delete state[id][mid];
}
},
- likeChannelMsg(state, action) {
- const { id, mid, action: emoji } = action.payload;
- console.log("add user likes message", id, mid, state[id][mid]);
+ updateChannelMsg(state, action) {
+ const { id, mid, content, time } = action.payload;
+ console.log("update channel message", id, mid);
if (state[id][mid]) {
+ state[id][mid].content = content;
+ state[id][mid].edited = time;
+ }
+ },
+ likeChannelMsg(state, action) {
+ const { id, from_uid, mid, action: reaction } = action.payload;
+ console.log("channel likes: likes", id, mid, from_uid, reaction);
+ if (state[id] && state[id][mid]) {
+ if (!state[id][mid].likes) {
+ console.log("channel likes: initial ");
+ state[id][mid].likes = {};
+ }
const currLikes = state[id][mid].likes;
- state[id][mid].likes = currLikes ? [...currLikes, emoji] : [emoji];
+ // state[id][mid].likes = currLikes ? [...currLikes, reaction] : [reaction];
+ if (currLikes[reaction]) {
+ if (currLikes[reaction].includes(from_uid)) {
+ const idx = currLikes[reaction].findIndex((id) => {
+ return id == from_uid;
+ });
+ console.log("remove reaction", currLikes[reaction], idx, from_uid);
+ currLikes[reaction].splice(idx, 1);
+ } else {
+ currLikes[reaction].push(from_uid);
+ }
+ } else {
+ currLikes[reaction] = [from_uid];
+ }
+ // state[id][mid].likes = currLikes;
}
},
setChannelMsgRead(state, action) {
@@ -71,6 +97,7 @@ const channelMsgSlice = createSlice({
},
});
export const {
+ updateChannelMsg,
deleteChannelMsg,
likeChannelMsg,
clearChannelMsg,
diff --git a/src/app/slices/message.user.js b/src/app/slices/message.user.js
index 161cbbb0..b7911672 100644
--- a/src/app/slices/message.user.js
+++ b/src/app/slices/message.user.js
@@ -39,11 +39,37 @@ const userMsgSlice = createSlice({
}
},
likeUserMsg(state, action) {
- const { id, mid, action: emoji } = action.payload;
- console.log("add user likes", id, mid, state[id][mid]);
- if (state[id][mid]) {
+ const { id, from_uid, mid, action: reaction } = action.payload;
+ console.log("user likes: likes", id, mid, from_uid, reaction);
+ if (state[id] && state[id][mid]) {
+ if (!state[id][mid].likes) {
+ console.log("user likes: initial ");
+ state[id][mid].likes = {};
+ }
const currLikes = state[id][mid].likes;
- state[id][mid].likes = currLikes ? [...currLikes, emoji] : [emoji];
+ // state[id][mid].likes = currLikes ? [...currLikes, reaction] : [reaction];
+ if (currLikes[reaction]) {
+ if (currLikes[reaction].includes(from_uid)) {
+ const idx = currLikes[reaction].findIndex((id) => {
+ return id == from_uid;
+ });
+ console.log("remove reaction", currLikes[reaction], idx, from_uid);
+ currLikes[reaction].splice(idx, 1);
+ } else {
+ currLikes[reaction].push(from_uid);
+ }
+ } else {
+ currLikes[reaction] = [from_uid];
+ }
+ // state[id][mid].likes = currLikes;
+ }
+ },
+ updateUserMsg(state, action) {
+ const { id, mid, content, time } = action.payload;
+ console.log("update user message", id, mid);
+ if (state[id][mid]) {
+ state[id][mid].content = content;
+ state[id][mid].edited = time;
}
},
deleteUserMsg(state, action) {
@@ -68,6 +94,7 @@ const userMsgSlice = createSlice({
},
});
export const {
+ updateUserMsg,
likeUserMsg,
deleteUserMsg,
clearUserMsg,
diff --git a/src/common/component/ChannelModal/index.js b/src/common/component/ChannelModal/index.js
index aa8aa570..1870a1e7 100644
--- a/src/common/component/ChannelModal/index.js
+++ b/src/common/component/ChannelModal/index.js
@@ -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(() => {
diff --git a/src/common/component/CurrentUser.js b/src/common/component/CurrentUser.js
index 65565221..72dd5986 100644
--- a/src/common/component/CurrentUser.js
+++ b/src/common/component/CurrentUser.js
@@ -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;
diff --git a/src/common/component/Message/Commands.js b/src/common/component/Message/Commands.js
index edb49b65..731e4f7d 100644
--- a/src/common/component/Message/Commands.js
+++ b/src/common/component/Message/Commands.js
@@ -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({
/>
{emojiPopVisible && (
-
+
)}
{currUid == uid ? (
-
+
{menuVisible && (
- Edit Message
+ {/* Edit Message */}
Pin Message
Reply
{currUid == uid && (
diff --git a/src/common/component/Message/EditMessage.js b/src/common/component/Message/EditMessage.js
new file mode 100644
index 00000000..4305ed08
--- /dev/null
+++ b/src/common/component/Message/EditMessage.js
@@ -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 (
+
+
+
+ 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`}
+ />
+
+
+
+ esc to
+
+
+ enter to{" "}
+
+
+
+
+ );
+}
diff --git a/src/common/component/Message/EmojiPicker.js b/src/common/component/Message/EmojiPicker.js
index 58b122b2..ac65835c 100644
--- a/src/common/component/Message/EmojiPicker.js
+++ b/src/common/component/Message/EmojiPicker.js
@@ -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 (
diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js
index 1299d318..a933727c 100644
--- a/src/common/component/Message/index.js
+++ b/src/common/component/Message/index.js
@@ -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({
{dayjs(time).format("YYYY-MM-DD h:mm:ss A")}
{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 ? (
1 ? count : ""}
- key={l}
+ key={reaction}
>
- {emojis[l]}
+ {emojis[reaction]}
- {count > 1 ? {`+${count}`} : null}
+ {uids.length > 1 ? {`+${uids.length}`} : null}
- );
+ ) : null;
})}
)}
- {renderContent(content_type, content)}
+ {edit ? (
+
+ ) : (
+ renderContent(content_type, content, edited)
+ )}
-
+ {!edit && (
+ uids.includes(currUser.uid))
+ .map(([reaction]) => {
+ return reaction;
+ })}
+ mid={mid}
+ uid={fromUid}
+ toggleMenu={toggleMenu}
+ menuVisible={menuVisible}
+ emojiPopVisible={emojiPopVisible}
+ toggleEmojiPopover={toggleEmojiPopover}
+ toggleEditMessage={toggleEditMessage}
+ />
+ )}
);
}
diff --git a/src/common/component/Message/renderContent.js b/src/common/component/Message/renderContent.js
index 82733aaa..eec55c84 100644
--- a/src/common/component/Message/renderContent.js
+++ b/src/common/component/Message/renderContent.js
@@ -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 = (
- (
-
- {decoratedText}
-
+ <>
+ (
+
+ {decoratedText}
+
+ )}
+ >
+ {content}
+
+ {edited && (
+
+ (edited)
+
)}
- >
- {content}
-
+ >
);
break;
case "image/jpeg":
diff --git a/src/common/component/Message/styled.js b/src/common/component/Message/styled.js
index 2d7d0b4a..69689cd3 100644
--- a/src/common/component/Message/styled.js
+++ b/src/common/component/Message/styled.js
@@ -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;
}
diff --git a/src/common/component/NotificationHub/useMessageHandler.js b/src/common/component/NotificationHub/useMessageHandler.js
index 643aaf02..c1395fc0 100644
--- a/src/common/component/NotificationHub/useMessageHandler.js
+++ b/src/common/component/NotificationHub/useMessageHandler.js
@@ -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;
diff --git a/src/common/component/Send/index.js b/src/common/component/Send/index.js
index 11da35b1..4291d85c 100644
--- a/src/common/component/Send/index.js
+++ b/src/common/component/Send/index.js
@@ -92,6 +92,12 @@ export default function Send({
+ e.currentTarget.setSelectionRange(
+ e.currentTarget.value.length,
+ e.currentTarget.value.length
+ )
+ }
ref={inputRef}
className="content"
maxRows={8}
diff --git a/src/common/utils.js b/src/common/utils.js
index a6c71a3c..82bba118 100644
--- a/src/common/utils.js
+++ b/src/common/utils.js
@@ -46,8 +46,12 @@ export const getInitialsAvatar = ({
context.rect(0, 0, canvas.width, canvas.height);
context.fillStyle = background;
context.fill();
- context.font = `${weight} ${initial_size || height / 2}px ${fontFamily}`;
+ // 两个字符,自动缩放40
+ context.font = `${weight} ${
+ ((initial_size || height) - (initials.length == 2 ? 40 : 0)) / 2
+ }px ${fontFamily}`;
context.textAlign = "center";
+
context.textBaseline = "middle";
context.fillStyle = foreground;
context.fillText(initials, width / 2, height / 2);
diff --git a/src/routes/chat/ChannelChat/index.js b/src/routes/chat/ChannelChat/index.js
index 1fecabc4..4c39215b 100644
--- a/src/routes/chat/ChannelChat/index.js
+++ b/src/routes/chat/ChannelChat/index.js
@@ -104,7 +104,7 @@ export default function ChannelChat({
.map(([mid, msg]) => {
if (!msg) return null;
const {
- likes = null,
+ likes = {},
pending = false,
from_uid,
content,
@@ -112,9 +112,11 @@ export default function ChannelChat({
created_at,
unread,
removed = false,
+ edited,
} = msg;
return (