feat: forward message UX
This commit is contained in:
@@ -25,8 +25,8 @@ const StyledWrapper = styled.div`
|
||||
}
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: ${({ size }) => `${size}px`};
|
||||
height: ${({ size }) => `${size}px`};
|
||||
position: relative;
|
||||
img {
|
||||
border-radius: 50%;
|
||||
@@ -66,6 +66,7 @@ export default function Contact({
|
||||
uid = "",
|
||||
popover = false,
|
||||
compact = false,
|
||||
avatarSize = 32,
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const curr = useSelector((store) => store.contacts.byId[uid]);
|
||||
@@ -75,6 +76,7 @@ export default function Contact({
|
||||
if (!curr) return null;
|
||||
return (
|
||||
<StyledWrapper
|
||||
size={avatarSize}
|
||||
onDoubleClick={dm ? handleDoubleClick : null}
|
||||
className={`${interactive ? "interactive" : ""} ${
|
||||
compact ? "compact" : ""
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
// import { useNavigate } from "react-router-dom";
|
||||
// import { useSelector, useDispatch } from "react-redux";
|
||||
import Modal from "../Modal";
|
||||
import Button from "../styled/Button";
|
||||
import Input from "../styled/Input";
|
||||
import Contact from "../Contact";
|
||||
import Reply from "../Message/Reply";
|
||||
import StyledWrapper from "./styled";
|
||||
import CloseIcon from "../../../assets/icons/close.circle.svg";
|
||||
import StyledCheckbox from "../../component/styled/Checkbox";
|
||||
import useFilteredUsers from "../../hook/useFilteredUsers";
|
||||
// import { addChannel } from "../../../app/slices/channels";
|
||||
|
||||
// import { useCreateChannelMutation } from "../../../app/services/channel";
|
||||
|
||||
export default function ForwardModal({ mid, closeModal }) {
|
||||
const [members, setMembers] = useState([]);
|
||||
const { contacts, input, updateInput } = useFilteredUsers();
|
||||
// const { conactsData, loginUid } = useSelector((store) => {
|
||||
// return { conactsData: store.contacts.byId, loginUid: store.authData.uid };
|
||||
// });
|
||||
const handleInputChange = (evt) => {
|
||||
updateInput(evt.target.value);
|
||||
};
|
||||
const toggleCheckMember = ({ currentTarget }) => {
|
||||
const { uid } = currentTarget.dataset;
|
||||
let tmp = members.includes(+uid)
|
||||
? members.filter((m) => m != uid)
|
||||
: [...members, +uid];
|
||||
console.log(uid, currentTarget);
|
||||
setMembers(tmp);
|
||||
};
|
||||
const removeSelected = (uid) => {
|
||||
setMembers(members.filter((m) => m != uid));
|
||||
};
|
||||
const selectedCount = members.length ? `(${members.length})` : null;
|
||||
return (
|
||||
<Modal>
|
||||
<StyledWrapper>
|
||||
<div className="left">
|
||||
<div className="search">
|
||||
<input
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Search user or channel"
|
||||
/>
|
||||
</div>
|
||||
{contacts && (
|
||||
<ul className="users">
|
||||
{contacts.map((u) => {
|
||||
const { uid } = u;
|
||||
const checked = members.includes(uid);
|
||||
console.log({ checked });
|
||||
return (
|
||||
<li
|
||||
key={uid}
|
||||
data-uid={uid}
|
||||
className="user"
|
||||
onClick={toggleCheckMember}
|
||||
>
|
||||
<StyledCheckbox
|
||||
readOnly
|
||||
checked={checked}
|
||||
name="cb"
|
||||
id="cb"
|
||||
/>
|
||||
<Contact uid={uid} interactive={false} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className={`right`}>
|
||||
<h3 className="title">Send To {selectedCount}</h3>
|
||||
<ul className="selected">
|
||||
{members.map((uid) => {
|
||||
return (
|
||||
<li key={uid} className="item">
|
||||
<Contact
|
||||
key={uid}
|
||||
uid={uid}
|
||||
interactive={false}
|
||||
avatarSize={40}
|
||||
/>
|
||||
<CloseIcon
|
||||
className="remove"
|
||||
onClick={removeSelected.bind(null, uid)}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="reply">
|
||||
<Reply mid={mid} interactive={false} />
|
||||
</div>
|
||||
<Input className="input" placeholder="Leave a message"></Input>
|
||||
<div className="btns">
|
||||
<Button onClick={closeModal} className="normal cancel">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="normal" disabled={members.length == 0}>
|
||||
Send To {selectedCount}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
display: flex;
|
||||
/* max-width: 604px; */
|
||||
max-height: 514px;
|
||||
min-height: 400px;
|
||||
background: #fff;
|
||||
box-shadow: 0px 25px 50px rgba(31, 41, 55, 0.25);
|
||||
border-radius: var(--br);
|
||||
transition: all 0.5s ease;
|
||||
overflow: hidden;
|
||||
.left {
|
||||
width: 260px;
|
||||
/* height: 100%; */
|
||||
box-shadow: inset -1px 0px 0px rgba(0, 0, 0, 0.1);
|
||||
overflow-y: scroll;
|
||||
.search {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 99;
|
||||
background: #fff;
|
||||
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1);
|
||||
padding: 16px;
|
||||
width: calc(100% - 1px);
|
||||
input {
|
||||
outline: none;
|
||||
width: -webkit-fill-available;
|
||||
padding: 10px 8px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
border-radius: var(--br);
|
||||
}
|
||||
}
|
||||
.users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* height: 260px; */
|
||||
padding-bottom: 20px;
|
||||
/* overflow-y: scroll; */
|
||||
.user {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
/* margin: 0 4px; */
|
||||
width: -webkit-fill-available;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background: rgba(116, 127, 141, 0.1);
|
||||
}
|
||||
> div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
/* height: 100%; */
|
||||
/* justify-content: space-between; */
|
||||
padding: 16px 32px 32px 32px;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #344054;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.selected {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
padding: 10px 0;
|
||||
overflow: scroll;
|
||||
.item {
|
||||
position: relative;
|
||||
.remove {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
.reply {
|
||||
width: 280px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.input {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.btns {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
export default StyledWrapper;
|
||||
@@ -12,7 +12,9 @@ import EmojiPicker from "./EmojiPicker";
|
||||
import replyIcon from "../../../assets/icons/reply.svg?url";
|
||||
import reactIcon from "../../../assets/icons/reaction.svg?url";
|
||||
import editIcon from "../../../assets/icons/edit.svg?url";
|
||||
import bookmarkIcon from "../../../assets/icons/bookmark.svg?url";
|
||||
import moreIcon from "../../../assets/icons/more.svg?url";
|
||||
import ForwardModal from "../ForwardModal";
|
||||
const StyledCmds = styled.ul`
|
||||
z-index: 9999;
|
||||
position: absolute;
|
||||
@@ -60,6 +62,7 @@ export default function Commands({
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
const [forwardModalVisible, setForwardModalVisible] = useState(false);
|
||||
const [tippyVisible, setTippyVisible] = useState(false);
|
||||
const currUid = useSelector((store) => store.authData.uid);
|
||||
const cmdsRef = useRef(null);
|
||||
@@ -71,7 +74,10 @@ export default function Commands({
|
||||
hideAll();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleForwardModal = () => {
|
||||
hideAll();
|
||||
setForwardModalVisible((prev) => !prev);
|
||||
};
|
||||
const toggleDeleteModal = () => {
|
||||
hideAll();
|
||||
setDeleteModalVisible((prev) => !prev);
|
||||
@@ -111,6 +117,11 @@ export default function Commands({
|
||||
</Tooltip>
|
||||
</li>
|
||||
)}
|
||||
<li className="cmd">
|
||||
<Tooltip placement="top" tip="Add to Favorites">
|
||||
<img src={bookmarkIcon} className="toggler" alt="icon bookmark" />
|
||||
</Tooltip>
|
||||
</li>
|
||||
<Tippy
|
||||
onShow={handleTippyVisible.bind(null, true)}
|
||||
onHide={handleTippyVisible.bind(null, false)}
|
||||
@@ -121,6 +132,9 @@ export default function Commands({
|
||||
<StyledMenu className="menu">
|
||||
{/* <li className="item">Edit Message</li> */}
|
||||
<li className="item underline">Pin Message</li>
|
||||
<li className="item" onClick={toggleForwardModal}>
|
||||
Forward
|
||||
</li>
|
||||
<li className="item" onClick={handleReply.bind(null, true)}>
|
||||
Reply
|
||||
</li>
|
||||
@@ -142,6 +156,9 @@ export default function Commands({
|
||||
{deleteModalVisible && (
|
||||
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
|
||||
)}
|
||||
{forwardModalVisible && (
|
||||
<ForwardModal mid={mid} closeModal={toggleForwardModal} />
|
||||
)}
|
||||
</StyledCmds>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,23 +4,26 @@ import Tippy from "@tippyjs/react";
|
||||
import Profile from "../Profile";
|
||||
import styled from "styled-components";
|
||||
const Styled = styled.span`
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
color: #1fe1f9;
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
export default function Mention({ uid }) {
|
||||
export default function Mention({ uid, popover = true }) {
|
||||
const contactsData = useSelector((store) => store.contacts.byId);
|
||||
const user = contactsData[uid];
|
||||
if (!user) return null;
|
||||
return (
|
||||
<Tippy
|
||||
disabled={!popover}
|
||||
duration={0}
|
||||
interactive
|
||||
placement="top"
|
||||
trigger="click"
|
||||
content={<Profile uid={uid} type="card" />}
|
||||
>
|
||||
<Styled>{`@${user.name}`}</Styled>
|
||||
<Styled className={popover ? "clickable" : ""}>{`@${user.name}`}</Styled>
|
||||
</Tippy>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
// import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useSelector } from "react-redux";
|
||||
import reactStringReplace from "react-string-replace";
|
||||
import MrakdownRender from "../MrakdownRender";
|
||||
|
||||
import Mention from "./Mention";
|
||||
import { ContentTypes } from "../../../app/config";
|
||||
import { getFileIcon, isImage } from "../../utils";
|
||||
|
||||
import Avatar from "../Avatar";
|
||||
const Styled = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px;
|
||||
@@ -15,6 +16,9 @@ const Styled = styled.div`
|
||||
border-radius: var(--br);
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -89,7 +93,19 @@ const renderContent = (data) => {
|
||||
let res = null;
|
||||
switch (content_type) {
|
||||
case ContentTypes.text:
|
||||
res = <span className="txt"> {content}</span>;
|
||||
res = (
|
||||
<span className="txt">
|
||||
{reactStringReplace(
|
||||
content,
|
||||
/(\s{1}\@[0-9]+\s{1})/g,
|
||||
(match, idx) => {
|
||||
console.log("match", match);
|
||||
const uid = match.trim().slice(1);
|
||||
return <Mention key={idx} uid={uid} popover={false} />;
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case ContentTypes.markdown:
|
||||
res = (
|
||||
@@ -120,7 +136,7 @@ const renderContent = (data) => {
|
||||
}
|
||||
return res;
|
||||
};
|
||||
export default function Reply({ mid }) {
|
||||
export default function Reply({ mid, interactive = true }) {
|
||||
const { data, users } = useSelector((store) => {
|
||||
return { data: store.message[mid], users: store.contacts.byId };
|
||||
});
|
||||
@@ -140,7 +156,11 @@ export default function Reply({ mid }) {
|
||||
const currUser = users[data.from_uid];
|
||||
if (!currUser) return null;
|
||||
return (
|
||||
<Styled data-mid={mid} className="reply" onClick={handleClick}>
|
||||
<Styled
|
||||
data-mid={mid}
|
||||
className={`reply ${interactive ? "clickable" : ""}`}
|
||||
onClick={interactive ? handleClick : null}
|
||||
>
|
||||
<div className="user">
|
||||
<Avatar className="avatar" url={currUser.avatar} name={currUser.name} />
|
||||
<span className="name">{currUser.name}</span>
|
||||
|
||||
@@ -36,6 +36,11 @@ const StyledButton = styled.button`
|
||||
background: none;
|
||||
color: #1fe1f9;
|
||||
}
|
||||
&.cancel {
|
||||
border: 1px solid #e5e7eb;
|
||||
background: none;
|
||||
color: #374151;
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledButton;
|
||||
|
||||
@@ -17,12 +17,16 @@ const StyledInput = styled.input`
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0px 1px 2px rgba(31, 41, 55, 0.08);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--br);
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: #333;
|
||||
padding: 8px;
|
||||
outline: none;
|
||||
&.higher {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
&.large {
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
|
||||
Reference in New Issue
Block a user