refactor: send and message display

This commit is contained in:
zerosoul
2022-03-18 16:33:21 +08:00
parent 7bb8e9a914
commit 184f68b094
19 changed files with 353 additions and 157 deletions
+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.11904 0C3.91105 0 0.5 3.41105 0.5 7.61905C0.5 11.827 3.91105 15.2381 8.11904 15.2381C12.3271 15.2381 15.7381 11.827 15.7381 7.61905C15.7381 3.41105 12.3263 0 8.11904 0ZM11.7053 10.128L10.628 11.2053L8.11904 8.69714L5.61009 11.2053L4.53277 10.128L7.04095 7.61905L4.532 5.1101L5.60934 4.03353L8.11829 6.54248L10.6272 4.03353L11.7045 5.1101L9.19638 7.61905L11.7053 10.128Z" fill="#6C737F"/>
</svg>

After

Width:  |  Height:  |  Size: 503 B

+12
View File
@@ -0,0 +1,12 @@
<svg width="15" height="20" viewBox="0 0 15 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7682_4291)">
<path d="M15 6.10417V18.6667C15 19.1333 15 19.3667 14.9083 19.5458C14.8292 19.7 14.7 19.8292 14.5458 19.9083C14.3667 20 14.1333 20 13.6667 20H1.33333C0.866666 20 0.633332 20 0.454166 19.9083C0.299999 19.8292 0.170833 19.7 0.0916666 19.5458C-1.17968e-07 19.3667 -8.10971e-08 19.1333 1.52874e-10 18.6667L3.0314e-06 1.33333C3.11265e-06 0.866666 3.15408e-06 0.633333 0.0916699 0.454166C0.170837 0.3 0.300003 0.170833 0.45417 0.0916664C0.633337 -1.24176e-07 0.86667 -8.10971e-08 1.33334 1.5288e-10L8.89584 1.32307e-06C9.30417 1.3939e-06 9.50834 1.43113e-06 9.7 0.0458348C9.80417 0.0708348 9.90417 0.104168 10 0.150001V3.66667C10 4.13333 10 4.36667 10.0917 4.54583C10.1708 4.7 10.3 4.82917 10.4542 4.90833C10.6333 5 10.8667 5 11.3333 5H14.85C14.8958 5.09583 14.9292 5.19583 14.9542 5.3C15 5.49167 15 5.69583 15 6.10417Z" fill="#A5F3FC"/>
<path d="M14.2208 4.22074C14.5083 4.50824 14.65 4.64991 14.7542 4.82074C14.7917 4.87908 14.8208 4.93741 14.85 4.99991H11.3333C10.8667 4.99991 10.6333 4.99991 10.4542 4.90824C10.3 4.82907 10.1708 4.69991 10.0917 4.54574C10 4.36657 10 4.13324 10 3.66658V0.149902C10.0625 0.179069 10.1208 0.208236 10.1792 0.245735C10.35 0.349902 10.4917 0.491569 10.7792 0.779069L14.2208 4.22074Z" fill="#06B6D4"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.6666 9.99992C6.6666 10.9204 5.92041 11.6666 4.99993 11.6666C4.07945 11.6666 3.33326 10.9204 3.33326 9.99992C3.33326 9.07944 4.07945 8.33325 4.99993 8.33325C5.92041 8.33325 6.6666 9.07944 6.6666 9.99992ZM7.00992 14.9011L9.16658 11.6661L11.6666 16.6661H3.33325L5.83325 13.3328L7.00992 14.9011Z" fill="#0891B2"/>
</g>
<defs>
<clipPath id="clip0_7682_4291">
<rect width="15" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+8 -3
View File
@@ -66,10 +66,13 @@ export default function Commands({
const currUid = useSelector((store) => store.authData.uid);
const menuRef = useRef(null);
const handleReply = () => {
const handleReply = (fromMenu = false) => {
if (contextId) {
dispatch(addReplyingMessage({ id: contextId, mid }));
}
if (fromMenu) {
toggleMenu();
}
// toast.success("cooming soon");
};
@@ -93,7 +96,7 @@ export default function Commands({
<img src={editIcon} alt="icon edit" />
</li>
) : (
<li className="cmd" onClick={handleReply}>
<li className="cmd" onClick={handleReply.bind(null, false)}>
<img src={replyIcon} alt="icon reply" />
</li>
)}
@@ -104,7 +107,9 @@ export default function Commands({
<StyledMenu className="menu" ref={menuRef}>
{/* <li className="item">Edit Message</li> */}
<li className="item underline">Pin Message</li>
<li className="item">Reply</li>
<li className="item" onClick={handleReply.bind(null, true)}>
Reply
</li>
{currUid == from_uid && (
<li className="item danger" onClick={toggleDeleteModal}>
Delete Message
@@ -9,7 +9,7 @@ export default function PreviewMessage({ mid = 0 }) {
return { msg: store.message[mid], contactsData: store.contacts.byId };
});
if (!msg) return null;
const { from_uid, created_at, content_type, content } = msg;
const { from_uid, created_at, content_type, content, thumbnail } = msg;
const { name, avatar } = contactsData[from_uid];
return (
<StyledWrapper className={`preview`}>
@@ -23,7 +23,9 @@ export default function PreviewMessage({ mid = 0 }) {
{dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}
</i>
</div>
<div className={`down`}>{renderContent(content_type, content)}</div>
<div className={`down`}>
{renderContent({ content_type, content, thumbnail })}
</div>
</div>
</StyledWrapper>
);
+1 -1
View File
@@ -7,7 +7,7 @@ import { useReactMessageMutation } from "../../../app/services/message";
// import { Emojis } from "../../../app/config";
import addEmojiIcon from "../../../assets/icons/add.emoji.svg?url";
const StyledWrapper = styled.span`
z-index: 99;
/* z-index: 99; */
position: relative;
margin-top: 8px;
margin-bottom: 4px;
+71 -7
View File
@@ -1,15 +1,79 @@
import React from "react";
// import React from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
import { ContentTypes } from "../../../app/config";
import Avatar from "../Avatar";
const Styled = styled.div`
color: #aaa;
font-size: 12px;
margin-bottom: -10px;
display: flex;
align-items: center;
padding: 16px;
background: #e5e7eb;
border-radius: var(--br);
gap: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 4px;
.user {
display: flex;
align-items: center;
gap: 4px;
.avatar {
width: 16px;
height: 16px;
border-radius: 50%;
}
.name {
font-style: normal;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #06b6d4;
}
}
.content {
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #616161;
.pic {
display: inherit;
max-width: 34px;
}
}
/* padding-left: 10px; */
`;
const renderContent = (data) => {
const { content_type, content, thumbnail } = data;
let res = null;
switch (content_type) {
case ContentTypes.text:
res = content;
break;
case ContentTypes.image:
case ContentTypes.imageJPG:
res = <img className="pic" src={thumbnail} />;
break;
default:
break;
}
return res;
};
export default function Reply({ mid }) {
const data = useSelector((store) => store.message[mid]);
const { data, users } = useSelector((store) => {
return { data: store.message[mid], users: store.contacts.byId };
});
if (!data) return null;
return <Styled className="reply">{data.content}</Styled>;
const currUser = users[data.from_uid];
if (!currUser) return null;
return (
<Styled className="reply">
<div className="user">
<Avatar className="avatar" url={currUser.avatar} name={currUser.name} />
<span className="name">{currUser.name}</span>
</div>
<div className="content">{renderContent(data)}</div>
</Styled>
);
}
+10 -2
View File
@@ -65,8 +65,10 @@ function Message({ contextId = 0, mid = "", read = true, context = "user" }) {
created_at: time,
sending,
content,
thumbnail,
content_type = "text/plain",
edited,
properties,
} = message;
const reactions = reactionMessageData[mid];
const currUser = contactsData[fromUid] || {};
@@ -89,12 +91,12 @@ function Message({ contextId = 0, mid = "", read = true, context = "user" }) {
</div>
</Tippy>
<div className="details">
{reply_mid && <Reply mid={reply_mid} />}
<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 ${sending ? "sending" : ""}`}>
{reply_mid && <Reply mid={reply_mid} />}
{edit ? (
<EditMessage
content={content}
@@ -102,7 +104,13 @@ function Message({ contextId = 0, mid = "", read = true, context = "user" }) {
cancelEdit={toggleEditMessage}
/>
) : (
renderContent(content_type, content, edited)
renderContent({
content_type,
properties,
content,
thumbnail,
edited,
})
)}
{reactions && <Reaction mid={mid} reactions={reactions} />}
</div>
+20 -13
View File
@@ -1,10 +1,16 @@
import Linkify from "react-linkify";
import dayjs from "dayjs";
import BASE_URL from "../../../app/config";
import MrakdownRender from "../MrakdownRender";
const renderContent = (type, content, edited = false) => {
import { getDefaultSize } from "../../utils";
const renderContent = ({
properties,
content_type,
content,
thumbnail,
edited = false,
}) => {
let ctn = null;
switch (type) {
switch (content_type) {
case "text/plain":
ctn = (
<>
@@ -40,16 +46,17 @@ const renderContent = (type, content, edited = false) => {
break;
case "image/png":
case "image/jpeg":
ctn = (
<img
className="img preview"
src={
content.startsWith("blob")
? content
: `${BASE_URL}/resource/image?id=${encodeURIComponent(content)}`
}
/>
);
{
const { width, height } = getDefaultSize(properties);
ctn = (
<img
className="img preview"
style={{ width: `${width}px`, height: `${height}px` }}
data-origin={content}
src={thumbnail}
/>
);
}
break;
default:
+72 -7
View File
@@ -1,8 +1,73 @@
// import React from 'react'
import { useDispatch, useSelector } from "react-redux";
import { MdClose } from "react-icons/md";
import { ContentTypes } from "../../../app/config";
import closeIcon from "../../../assets/icons/close.circle.svg?url";
import pictureIcon from "../../../assets/icons/picture.svg?url";
import { removeReplyingMessage } from "../../../app/slices/message";
import styled from "styled-components";
const Styled = styled.div`
z-index: 999;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
justify-content: flex-start;
gap: 16px;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
background-color: #f3f4f6;
position: absolute;
left: 0;
top: 0;
transform: translateY(-100%);
width: 100%;
padding: 12px 16px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.prefix {
color: #667085;
font-style: normal;
font-weight: 400;
font-size: 14px;
line-height: 20px;
em {
font-weight: bold;
color: #363f53;
}
}
.content {
font-weight: 500;
color: #616161;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 30px;
}
.close {
background: none;
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
}
`;
const renderContent = (data) => {
const { content_type, content } = data;
let res = null;
switch (content_type) {
case ContentTypes.text:
res = content;
break;
case ContentTypes.image:
case ContentTypes.imageJPG:
res = <img className="pic" src={pictureIcon} />;
break;
default:
break;
}
return res;
};
export default function Replying({ id, mid }) {
const { msg, contactsData } = useSelector((store) => {
return { contactsData: store.contacts.byId, msg: store.message[mid] };
@@ -15,14 +80,14 @@ export default function Replying({ id, mid }) {
const { from_uid } = msg;
const user = contactsData[from_uid];
return (
<div className="reply">
<span className="txt">
Replying to
<em>{user?.name}</em>
<Styled className="reply">
<span className="prefix">
Replying to <em>{user?.name}</em>
</span>
<span className="content">{renderContent(msg)}</span>
<button className="close" onClick={removeReply}>
<MdClose size={20} color="#78787C" />
<img src={closeIcon} alt="close icon" />
</button>
</div>
</Styled>
);
}
+40 -27
View File
@@ -1,10 +1,11 @@
import { useState, useEffect } from "react";
// import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
// import toast from "react-hot-toast";
// import { useNavigate } from "react-router-dom";
import { useSendChannelMsgMutation } from "../../../../app/services/channel";
import { useSendMsgMutation } from "../../../../app/services/contact";
import Modal from "../../Modal";
import { formatBytes } from "../../../utils";
import Button from "../../styled/Button";
import StyledWrapper from "./styled";
@@ -22,27 +23,40 @@ export default function UploadModal({
{ isLoading: channelSending },
] = useSendChannelMsgMutation();
const [sendUserMsg, { isLoading: userSending }] = 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 [properties, setProperties] = useState([]);
// useEffect(() => {
// files.forEach((file, idx) => {
// const { name, size, type } = file;
// setProperties((prevs) => {
// prevs[idx] = { name, size, type };
// return prevs;
// });
// var fileReader = new FileReader();
// fileReader.onloadend = (e) => {
// let dataUrl = e.target.result;
// let tmp = new Image();
// tmp.src = dataUrl;
// tmp.onload = function () {
// console.log("image load", this.width, this.height);
// setProperties((prevs) => {
// prevs[idx].width = this.width;
// prevs[idx].height = this.height;
// return prevs;
// });
// };
// };
// fileReader.readAsDataURL(file);
// });
// }, [files]);
const handleUpload = () => {
const uploadFn = type == "user" ? sendUserMsg : sendChannelMsg;
uploadFn({ id: sendTo, content: files[0], type: "image", from_uid });
uploadFn({
id: sendTo,
content: files[0],
// properties: properties[0],
type: "image",
from_uid,
});
closeModal();
};
if (!sendTo) return null;
@@ -68,22 +82,21 @@ export default function UploadModal({
className="animate__animated animate__fadeInDown animate__faster"
>
<ul className="list">
{blobs.map((b, idx) => {
console.log({ b });
{files.map((f, idx) => {
console.log({ f });
return (
<li key={idx} className="item">
<img
src={b.data}
// src={URL.createObjectURL(b.data)}
src={URL.createObjectURL(f)}
alt="thumb"
className="thumb"
/>
<div className="right">
<div className="name">
<span className="input">{b.name}</span>
<i className="tip">(click title to change name)</i>
<span className="input">{f.name}</span>
{/* <i className="tip">(click title to change name)</i> */}
</div>
<i className="size">{`${Math.floor(b.size / 1000)}KB`}</i>
<i className="size">{formatBytes(f.size)}</i>
</div>
</li>
);
+1 -1
View File
@@ -113,7 +113,7 @@ export default function Send({
return (
<>
<StyledSend className={`send ${replying_mid ? "reply" : ""}`}>
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${type}`}>
{replying_mid && <Replying mid={replying_mid} id={id} />}
<div className="input">
+4 -25
View File
@@ -2,7 +2,7 @@ import styled from "styled-components";
const StyledSend = styled.div`
position: absolute;
bottom: 15px;
bottom: -70px;
left: 50%;
transform: translateX(-50%);
background: #e5e7eb;
@@ -15,6 +15,9 @@ const StyledSend = styled.div`
gap: 5px;
padding: 4px 18px;
/* margin: 0 16px; */
&.user {
bottom: 10px;
}
&.reply {
border-top-left-radius: 0;
border-top-right-radius: 0;
@@ -49,30 +52,6 @@ const StyledSend = styled.div`
transform: translateY(-50%);
}
}
.reply {
display: flex;
justify-content: space-between;
align-items: center;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
background-color: #f2f2f5;
position: absolute;
left: 0;
top: 0;
transform: translateY(-100%);
width: 100%;
padding: 6px 18px;
.txt {
color: #aaa;
font-size: 12px;
em {
font-weight: bold;
color: #333;
padding: 0 5px;
}
}
}
`;
export default StyledSend;
+18
View File
@@ -16,6 +16,24 @@ export const getNonNullValues = (obj, whiteList = ["log_id"]) => {
});
return tmp;
};
export function getDefaultSize(size = null, width = 240) {
if (!size) return { width: 0, height: 0 };
const { width: oWidth, height: oHeight } = size;
const dWidth = oWidth >= width ? width : oWidth;
const dHeight = (oHeight / oWidth) * dWidth;
return { width: dWidth, height: dHeight };
}
export function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return "0 Bytes";
const k = 1000;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
export const getInitials = (name) => {
const arr = name.split(" ").filter((n) => !!n);
return arr
-1
View File
@@ -127,7 +127,6 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
</div>
<Send dragFiles={dragFiles} id={cid} type="channel" name={name} />
<div className="placeholder"></div>
</StyledChannelChat>
{/* {unreads != 0 && (
<StyledNotification>
@@ -1,13 +1,11 @@
// import React from 'react'
import { NavLink, useNavigate } from "react-router-dom";
import { useDrop } from "react-dnd";
import { NativeTypes } from "react-dnd-html5-backend";
import { useDispatch, useSelector } from "react-redux";
import useContextMenu from "../../common/hook/useContextMenu";
import ContextMenu from "../../common/component/ContextMenu";
import { toggleChannelSetting } from "../../app/slices/ui";
import ChannelIcon from "../../common/component/ChannelIcon";
import { getUnreadCount } from "./utils";
import { toggleChannelSetting } from "../../../app/slices/ui";
import ChannelIcon from "../../../common/component/ChannelIcon";
import { getUnreadCount } from "../utils";
const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -44,7 +42,10 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
const unreads = getUnreadCount(mids, messageData);
return (
<NavLink
onContextMenu={contextMenuEventHandler}
data-cid={id}
onContextMenu={(evt) => {
contextMenuEventHandler(evt, id);
}}
ref={drop}
key={id}
className={`link ${isActive ? "drop_over" : ""}`}
@@ -65,58 +66,5 @@ const NavItem = ({ id, setFiles, contextMenuEventHandler }) => {
</NavLink>
);
};
export default function ChannelList({ setDropFiles }) {
const channelIds = useSelector((store) => store.channels.ids);
const {
visible: contextMenuVisible,
posX,
posY,
hideContextMenu,
handleContextMenuEvent,
} = useContextMenu();
return (
<>
{channelIds.map((cid) => {
return (
<NavItem
contextMenuEventHandler={handleContextMenuEvent}
key={cid}
id={cid}
setFiles={setDropFiles}
/>
);
})}
{contextMenuVisible ? (
<ContextMenu
hideMenu={hideContextMenu}
posX={posX}
posY={posY}
items={[
{
title: "Mark As Read",
underline: true,
},
{
title: "Mute",
},
{
title: "Notification Settings",
underline: true,
},
{
title: "Edit Channel",
underline: true,
},
{
title: "Invite People",
},
{
title: "Delete Channel",
danger: true,
},
]}
/>
) : null}
</>
);
}
export default NavItem;
+77
View File
@@ -0,0 +1,77 @@
import { useState } from "react";
import { useSelector } from "react-redux";
import useContextMenu from "../../../common/hook/useContextMenu";
import ContextMenu from "../../../common/component/ContextMenu";
import DeleteConfirmModal from "../../../common/component/ChannelSetting/DeleteConfirmModal";
import NavItem from "./NavItem";
export default function ChannelList({ setDropFiles }) {
const [removeConfirmVisible, setRemoveConfirmVisible] = useState(false);
const [currId, setCurrId] = useState(null);
const { channelIds, channelData } = useSelector((store) => {
return { channelIds: store.channels.ids, channelData: store.channels.byId };
});
const {
visible: contextMenuVisible,
posX,
posY,
hideContextMenu,
handleContextMenuEvent,
} = useContextMenu();
const handleContextMenuClick = (evt, id) => {
console.log("wtf", evt, id);
setCurrId(id);
handleContextMenuEvent(evt);
};
const toggleRemoveConfirm = () => {
setRemoveConfirmVisible((prev) => !prev);
};
const { is_public } = channelData[currId] || {};
return (
<>
{channelIds.map((cid) => {
return (
<NavItem
contextMenuEventHandler={handleContextMenuClick}
key={cid}
id={cid}
setFiles={setDropFiles}
/>
);
})}
{contextMenuVisible ? (
<ContextMenu
hideMenu={hideContextMenu}
posX={posX}
posY={posY}
items={[
{
title: "Mark As Read",
underline: true,
},
{
title: "Mute",
},
{
title: "Notification Settings",
underline: true,
},
is_public
? null
: {
title: "Invite People",
},
{
title: "Delete Channel",
danger: true,
handler: toggleRemoveConfirm,
},
]}
/>
) : null}
{removeConfirmVisible && (
<DeleteConfirmModal id={currId} closeModal={toggleRemoveConfirm} />
)}
</>
);
}
-1
View File
@@ -75,7 +75,6 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
})}
</div>
</StyledDMChat>
<div className="placeholder"></div>
<Send
dragFiles={dragFiles}
type="user"
-4
View File
@@ -56,8 +56,4 @@ export const StyledDMChat = styled.article`
overflow-y: scroll;
overflow-x: visible;
}
.placeholder {
width: 100%;
height: 80px;
}
`;
+2 -1
View File
@@ -96,13 +96,14 @@ export default function Layout({
useEffect(() => {
if (messagesContainer) {
const container = messagesContainer.current;
// 点击查看大图
container.addEventListener(
"click",
(evt) => {
console.log(evt);
const { target } = evt;
if (target.nodeType == 1 && target.classList.contains("preview")) {
setPreviewImage(target.src);
setPreviewImage(target.dataset.origin || target.src);
}
},
true