feat: select multiple messages and forward

This commit is contained in:
zerosoul
2022-04-26 11:29:03 +08:00
parent f5ce856061
commit fbedaa8825
24 changed files with 702 additions and 329 deletions
+8
View File
@@ -76,6 +76,13 @@ export const messageApi = createApi({
url: `/resource/archive?file_path=${encodeURIComponent(file_path)}`, url: `/resource/archive?file_path=${encodeURIComponent(file_path)}`,
}), }),
}), }),
pinMessage: builder.mutation({
query: ({ gid, mid }) => ({
url: `/group/${gid}/pin`,
method: "POST",
body: { mid },
}),
}),
replyMessage: builder.mutation({ replyMessage: builder.mutation({
query: ({ reply_mid, content, type = "text" }) => ({ query: ({ reply_mid, content, type = "text" }) => ({
headers: { headers: {
@@ -114,6 +121,7 @@ export const messageApi = createApi({
}); });
export const { export const {
usePinMessageMutation,
useLazyGetArchiveMessageQuery, useLazyGetArchiveMessageQuery,
useGetArchiveMessageQuery, useGetArchiveMessageQuery,
useLazyGetOGInfoQuery, useLazyGetOGInfoQuery,
+30
View File
@@ -11,6 +11,7 @@ const initialState = {
menuExpand: false, menuExpand: false,
fileListView: Views.grid, fileListView: Views.grid,
uploadFiles: {}, uploadFiles: {},
selectMessages: {},
}; };
const uiSlice = createSlice({ const uiSlice = createSlice({
name: "ui", name: "ui",
@@ -40,6 +41,7 @@ const uiSlice = createSlice({
state.userGuide[key] = obj[key]; state.userGuide[key] = obj[key];
}); });
}, },
updateUploadFiles(state, action) { updateUploadFiles(state, action) {
const { const {
context = "channel", context = "channel",
@@ -105,6 +107,33 @@ const uiSlice = createSlice({
break; break;
} }
}, },
updateSelectMessages(state, action) {
const {
context = "channel",
id = null,
operation = "add",
data = null,
} = action.payload;
let currData = state.selectMessages[`${context}_${id}`];
switch (operation) {
case "add": {
currData = currData ? [...currData, data] : [data];
break;
}
case "remove": {
currData = currData.filter((mid) => mid != data);
break;
}
case "reset": {
currData = null;
break;
}
default:
break;
}
state.selectMessages[`${context}_${id}`] = currData;
},
}, },
}); });
export const { export const {
@@ -115,6 +144,7 @@ export const {
toggleMenuExpand, toggleMenuExpand,
updateFileListView, updateFileListView,
updateUploadFiles, updateUploadFiles,
updateSelectMessages,
updateUserGuide, updateUserGuide,
} = uiSlice.actions; } = uiSlice.actions;
export default uiSlice.reducer; export default uiSlice.reducer;
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.2958 16.2945C13.9055 16.6853 13.906 17.3184 14.2968 17.7087C14.6876 18.0989 15.3207 18.0985 15.711 17.7077L20.7076 12.7043C21.0976 12.3137 21.0974 11.681 20.7072 11.2906L15.7106 6.29297C15.3201 5.9024 14.6869 5.90234 14.2964 6.29282C13.9058 6.6833 13.9057 7.31647 14.2962 7.70703L17.589 11H11C6.66509 11 3.13546 14.4478 3.00381 18.7508L3 19C3 19.5523 3.44772 20 4 20C4.55228 20 5 19.5523 5 19C5 15.7616 7.56557 13.1224 10.7751 13.0041L11 13H17.586L14.2958 16.2945Z" fill="#667085"/>
</svg>

After

Width:  |  Height:  |  Size: 599 B

+4 -2
View File
@@ -14,7 +14,8 @@ export default function Avatar({
} }
const tmp = getInitialsAvatar({ const tmp = getInitialsAvatar({
initials: getInitials(name), initials: getInitials(name),
background: type == "channel" ? "#4ea758" : undefined, background: type == "channel" ? "#EAECF0" : undefined,
foreground: type == "channel" ? "#475467" : undefined,
}); });
setSrc(tmp); setSrc(tmp);
}; };
@@ -22,7 +23,8 @@ export default function Avatar({
if (!url) { if (!url) {
const tmp = getInitialsAvatar({ const tmp = getInitialsAvatar({
initials: getInitials(name), initials: getInitials(name),
background: type == "channel" ? "#4ea758" : undefined, background: type == "channel" ? "#EAECF0" : undefined,
foreground: type == "channel" ? "#475467" : undefined,
}); });
setSrc(tmp); setSrc(tmp);
} else { } else {
+4 -3
View File
@@ -35,12 +35,13 @@ const StyledWrapper = styled.div`
} }
.status { .status {
position: absolute; position: absolute;
bottom: 0; bottom: -2px;
right: -2px; right: -4px;
width: 10px; width: 10px;
height: 10px; height: 10px;
box-sizing: content-box;
border-radius: 50%; border-radius: 50%;
outline: 2px solid #fff; border: 2px solid #fff;
&.online { &.online {
background-color: #22c55e; background-color: #22c55e;
} }
+6 -4
View File
@@ -16,7 +16,7 @@ import CloseIcon from "../../../assets/icons/close.circle.svg";
import StyledCheckbox from "../../component/styled/Checkbox"; import StyledCheckbox from "../../component/styled/Checkbox";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
export default function ForwardModal({ mid, closeModal }) { export default function ForwardModal({ mids, closeModal }) {
const [appendText, setAppendText] = useState(""); const [appendText, setAppendText] = useState("");
const { sendMessages } = useSendMessage(); const { sendMessages } = useSendMessage();
const { forwardMessage, forwarding } = useForwardMessage(); const { forwardMessage, forwarding } = useForwardMessage();
@@ -45,7 +45,7 @@ export default function ForwardModal({ mid, closeModal }) {
}; };
const handleForward = async () => { const handleForward = async () => {
await forwardMessage({ await forwardMessage({
mids: [mid], mids: mids,
users: selectedMembers, users: selectedMembers,
channels: selectedChannels, channels: selectedChannels,
}); });
@@ -170,8 +170,10 @@ export default function ForwardModal({ mid, closeModal }) {
); );
})} })}
</ul> </ul>
<div className="reply"> <div className="msgs">
<Reply mid={mid} interactive={false} /> {mids.map((mid) => (
<Reply key={mid} mid={mid} interactive={false} />
))}
</div> </div>
<Input <Input
className="input" className="input"
+10 -2
View File
@@ -62,7 +62,7 @@ const StyledWrapper = styled.div`
align-items: flex-start; align-items: flex-start;
/* height: 100%; */ /* height: 100%; */
/* justify-content: space-between; */ /* justify-content: space-between; */
padding: 16px 32px 32px 32px; padding: 16px;
box-sizing: border-box; box-sizing: border-box;
.title { .title {
font-weight: 600; font-weight: 600;
@@ -87,9 +87,17 @@ const StyledWrapper = styled.div`
} }
} }
} }
.reply { .msgs {
border-radius: var(--br);
padding: 8px;
max-height: 200px;
overflow: auto;
background-color: #f4f4f5;
width: 280px; width: 280px;
margin-bottom: 4px; margin-bottom: 4px;
> .reply {
background: none;
}
} }
.input { .input {
margin-bottom: 32px; margin-bottom: 32px;
+28 -2
View File
@@ -4,6 +4,7 @@ import styled from "styled-components";
import Tippy from "@tippyjs/react"; import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js"; import { hideAll } from "tippy.js";
// import toast from "react-hot-toast"; // import toast from "react-hot-toast";
import { updateSelectMessages } from "../../../app/slices/ui";
import { addReplyingMessage } from "../../../app/slices/message"; import { addReplyingMessage } from "../../../app/slices/message";
import StyledMenu from "../styled/Menu"; import StyledMenu from "../styled/Menu";
import Tooltip from "../../component/Tooltip"; import Tooltip from "../../component/Tooltip";
@@ -15,6 +16,7 @@ import editIcon from "../../../assets/icons/edit.svg?url";
// import bookmarkIcon from "../../../assets/icons/bookmark.svg?url"; // import bookmarkIcon from "../../../assets/icons/bookmark.svg?url";
import moreIcon from "../../../assets/icons/more.svg?url"; import moreIcon from "../../../assets/icons/more.svg?url";
import ForwardModal from "../ForwardModal"; import ForwardModal from "../ForwardModal";
import PinMessageModal from "./PinMessageModal";
const StyledCmds = styled.ul` const StyledCmds = styled.ul`
z-index: 9999; z-index: 9999;
position: absolute; position: absolute;
@@ -50,12 +52,14 @@ const StyledCmds = styled.ul`
} }
`; `;
export default function Commands({ export default function Commands({
context = "user",
contextId = 0, contextId = 0,
mid = 0, mid = 0,
from_uid = 0, from_uid = 0,
toggleEditMessage, toggleEditMessage,
}) { }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [pinModalVisible, setPinModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false); const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [forwardModalVisible, setForwardModalVisible] = useState(false); const [forwardModalVisible, setForwardModalVisible] = useState(false);
const [tippyVisible, setTippyVisible] = useState(false); const [tippyVisible, setTippyVisible] = useState(false);
@@ -77,9 +81,17 @@ export default function Commands({
hideAll(); hideAll();
setDeleteModalVisible((prev) => !prev); setDeleteModalVisible((prev) => !prev);
}; };
const togglePinModal = () => {
hideAll();
setPinModalVisible((prev) => !prev);
};
const handleTippyVisible = (visible = true) => { const handleTippyVisible = (visible = true) => {
setTippyVisible(visible); setTippyVisible(visible);
}; };
const handleSelect = (mid) => {
dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
hideAll();
};
return ( return (
<StyledCmds <StyledCmds
ref={cmdsRef} ref={cmdsRef}
@@ -127,13 +139,20 @@ export default function Commands({
content={ content={
<StyledMenu className="menu"> <StyledMenu className="menu">
{/* <li className="item">Edit Message</li> */} {/* <li className="item">Edit Message</li> */}
{/* <li className="item underline">Pin Message</li> */} {context == "channel" && (
<li className="item underline" onClick={togglePinModal}>
Pin Message
</li>
)}
<li className="item" onClick={toggleForwardModal}> <li className="item" onClick={toggleForwardModal}>
Forward Forward
</li> </li>
<li className="item" onClick={handleReply.bind(null, true)}> <li className="item" onClick={handleReply.bind(null, true)}>
Reply Reply
</li> </li>
<li className="item" onClick={handleSelect.bind(null, mid)}>
Select
</li>
{currUid == from_uid && ( {currUid == from_uid && (
<li className="item danger" onClick={toggleDeleteModal}> <li className="item danger" onClick={toggleDeleteModal}>
Delete Message Delete Message
@@ -153,7 +172,14 @@ export default function Commands({
<DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} /> <DeleteMessageConfirm closeModal={toggleDeleteModal} mid={mid} />
)} )}
{forwardModalVisible && ( {forwardModalVisible && (
<ForwardModal mid={mid} closeModal={toggleForwardModal} /> <ForwardModal mids={[mid]} closeModal={toggleForwardModal} />
)}
{pinModalVisible && (
<PinMessageModal
mid={mid}
gid={contextId}
closeModal={togglePinModal}
/>
)} )}
</StyledCmds> </StyledCmds>
); );
@@ -0,0 +1,60 @@
// import React from "react";
import { useEffect } from "react";
import styled from "styled-components";
const StyledPinModal = styled(StyledModal)`
min-width: 406px;
.title,
.desc {
text-align: left;
}
.preview {
border: 1px solid #f2f4f7;
max-height: 256px;
overflow: auto;
background: none;
overflow-x: hidden;
}
`;
// import { useDispatch } from "react-redux";
// import BASE_URL from "../../app/config";
import { usePinMessageMutation } from "../../../app/services/message";
import StyledModal from "../styled/Modal";
import Button from "../styled/Button";
import Modal from "../Modal";
import PreviewMessage from "./PreviewMessage";
export default function PinMessageModal({ closeModal, mid = 0, gid = 0 }) {
// const dispatch = useDispatch();
const [pinMessage, { isLoading, isSuccess }] = usePinMessageMutation();
const handlePin = () => {
pinMessage({ mid, gid });
};
useEffect(() => {
if (isSuccess) {
closeModal();
}
}, [isSuccess]);
if (!mid) return null;
return (
<Modal>
<StyledPinModal
// className="animate__animated animate__fadeInDown animate__faster"
buttons={
<>
<Button onClick={closeModal} className="cancel">
Cancel
</Button>
<Button disabled={isLoading} onClick={handlePin} className="main">
{isLoading ? "Pining" : `Pin It`}
</Button>
</>
}
title="Pin It"
description="You sure you want to pin this message to #gerenal?"
>
<PreviewMessage mid={mid} />
</StyledPinModal>
</Modal>
);
}
+1
View File
@@ -38,6 +38,7 @@ const Styled = styled.div`
} }
} }
.content { .content {
overflow: hidden;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
line-height: 20px; line-height: 20px;
+10 -3
View File
@@ -17,6 +17,7 @@ import Tooltip from "../Tooltip";
dayjs.extend(isToday); dayjs.extend(isToday);
dayjs.extend(isYesterday); dayjs.extend(isYesterday);
function Message({ function Message({
readOnly = false,
contextId = 0, contextId = 0,
mid = "", mid = "",
context = "user", context = "user",
@@ -77,8 +78,13 @@ function Message({
console.log("render message"); console.log("render message");
// return null; // return null;
return ( return (
<StyledWrapper data-msg-mid={mid} ref={inviewRef} className={`message`}> <StyledWrapper
data-msg-mid={mid}
ref={inviewRef}
className={`message ${readOnly ? "readonly" : ""}`}
>
<Tippy <Tippy
disabled={readOnly}
duration={0} duration={0}
interactive interactive
placement="left" placement="left"
@@ -94,7 +100,7 @@ function Message({
<span className="name">{currUser.name}</span> <span className="name">{currUser.name}</span>
<Tooltip <Tooltip
delay={200} delay={200}
disabled={!timePrefix} disabled={!timePrefix || readOnly}
placement="top" placement="top"
tip={dayjsTime.format("YYYY-MM-DD h:mm:ss A")} tip={dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
> >
@@ -130,8 +136,9 @@ function Message({
{reactions && <Reaction mid={mid} reactions={reactions} />} {reactions && <Reaction mid={mid} reactions={reactions} />}
</div> </div>
</div> </div>
{!edit && ( {!edit && !readOnly && (
<Commands <Commands
context={context}
contextId={contextId} contextId={contextId}
mid={mid} mid={mid}
from_uid={fromUid} from_uid={fromUid}
+75 -19
View File
@@ -1,12 +1,16 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Linkit from "react-linkify"; import Linkit from "react-linkify";
import styled from "styled-components";
import dayjs from "dayjs"; import dayjs from "dayjs";
import StyledMsg from "./styled";
import { ContentTypes } from "../../../app/config"; import { ContentTypes } from "../../../app/config";
import Mention from "./Mention"; import Mention from "./Mention";
import useNormalizeMessage from "../../hook/useNormalizeMessage"; import useNormalizeMessage from "../../hook/useNormalizeMessage";
import MrakdownRender from "../MrakdownRender"; import MrakdownRender from "../MrakdownRender";
import FileMessage from "../FileMessage"; import FileMessage from "../FileMessage";
import URLPreview from "./URLPreview"; import URLPreview from "./URLPreview";
import Avatar from "../Avatar";
import IconForward from "../../../assets/icons/forward.svg";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
const renderContent = ({ const renderContent = ({
context, context,
@@ -111,6 +115,29 @@ const renderContent = ({
} }
return ctn; return ctn;
}; };
const StyledForward = styled.div`
display: flex;
flex-direction: column;
border-radius: var(--br);
background-color: #f4f4f5;
padding: 8px;
> .tip {
display: flex;
align-items: center;
gap: 4px;
.icon {
width: 16px;
height: 16px;
path {
fill: #98a2b3;
}
}
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #98a2b3;
}
`;
const ForwardedMessage = ({ context, to, from_uid, id }) => { const ForwardedMessage = ({ context, to, from_uid, id }) => {
const { normalizeMessage, messages } = useNormalizeMessage(); const { normalizeMessage, messages } = useNormalizeMessage();
const [forwards, setForwards] = useState(null); const [forwards, setForwards] = useState(null);
@@ -123,25 +150,54 @@ const ForwardedMessage = ({ context, to, from_uid, id }) => {
useEffect(() => { useEffect(() => {
if (messages) { if (messages) {
setForwards( setForwards(
messages.map((msg) => { <StyledForward>
const { <h4 className="tip">
download, <IconForward className="icon" />
content, Forwarded
content_type, </h4>
properties, <div className="list">
thumbnail, {messages.map((msg, idx) => {
} = msg; const {
return renderContent({ user = {},
download, created_at,
context, download,
to, content,
from_uid, content_type,
content, properties,
content_type, thumbnail,
properties, } = msg;
thumbnail, return (
}); <StyledMsg key={idx}>
}) {user && (
<div className="avatar">
<Avatar url={user.avatar} name={user.name} />
</div>
)}
<div className="details">
<div className="up">
<span className="name">{user?.name}</span>
<i className="time">
{dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}
</i>
</div>
<div className="down">
{renderContent({
download,
context,
to,
from_uid,
content,
content_type,
properties,
thumbnail,
})}
</div>
</div>
</StyledMsg>
);
})}
</div>
</StyledForward>
); );
} }
}, [messages, context, to, from_uid]); }, [messages, context, to, from_uid]);
+5
View File
@@ -17,11 +17,16 @@ const StyledMsg = styled.div`
} }
&:hover, &:hover,
&.preview { &.preview {
content-visibility: inherit;
contain-intrinsic-size: inherit;
background: #f5f6f7; background: #f5f6f7;
.cmds { .cmds {
visibility: visible; visibility: visible;
} }
} }
&.readonly:hover {
background: none;
}
.avatar { .avatar {
flex-shrink: 0; flex-shrink: 0;
cursor: pointer; cursor: pointer;
+28 -13
View File
@@ -26,10 +26,8 @@ const Modes = {
markdown: "markdown", markdown: "markdown",
}; };
function Send({ function Send({
name,
context = "channel",
members = [],
// 发给谁,或者是channel,或者是user // 发给谁,或者是channel,或者是user
context = "channel",
id = "", id = "",
}) { }) {
const editor = useMixedEditor(`${context}_${id}`); const editor = useMixedEditor(`${context}_${id}`);
@@ -37,16 +35,25 @@ function Send({
const dispatch = useDispatch(); const dispatch = useDispatch();
const addLocalFileMesage = useAddLocalFileMessage({ context, to: id }); const addLocalFileMesage = useAddLocalFileMessage({ context, to: id });
// 谁发的 // 谁发的
const { from_uid, replying_mid = null, mode, uploadFiles } = useSelector( const {
(store) => { from_uid,
return { replying_mid = null,
mode: store.ui.inputMode, mode,
from_uid: store.authData.uid, uploadFiles,
replying_mid: store.message.replying[id], channelsData,
uploadFiles: store.ui.uploadFiles[`${context}_${id}`], contactsData,
}; uids,
} } = useSelector((store) => {
); return {
channelsData: store.channels.byId,
uids: store.contacts.ids,
contactsData: store.contacts.byId,
mode: store.ui.inputMode,
from_uid: store.authData.uid,
replying_mid: store.message.replying[id],
uploadFiles: store.ui.uploadFiles[`${context}_${id}`],
};
});
const { sendMessage } = useSendMessage({ context, from: from_uid, to: id }); const { sendMessage } = useSendMessage({ context, from: from_uid, to: id });
useEffect(() => { useEffect(() => {
@@ -121,7 +128,15 @@ function Send({
const toggleMode = () => { const toggleMode = () => {
dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text)); dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text));
}; };
const name =
context == "channel" ? channelsData[id]?.name : contactsData[id]?.name;
const placeholder = `Send to ${Types[context]}${name} `; const placeholder = `Send to ${Types[context]}${name} `;
const members =
context == "channel"
? channelsData[id]?.is_public
? uids
: channelsData[id]?.members
: [];
return ( return (
<StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}> <StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}>
{replying_mid && <Replying mid={replying_mid} id={id} />} {replying_mid && <Replying mid={replying_mid} id={id} />}
+1 -1
View File
@@ -7,7 +7,7 @@ const StyledMenu = styled.ul`
background-color: #fff; background-color: #fff;
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1), box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
0px 10px 10px rgba(31, 41, 55, 0.04); 0px 10px 10px rgba(31, 41, 55, 0.04);
border-radius: var(--br); border-radius: 12px;
.item { .item {
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
+14 -1
View File
@@ -11,7 +11,14 @@ export default function useNormalizeMessage() {
useEffect(() => { useEffect(() => {
if (data && isSuccess) { if (data && isSuccess) {
const msgs = data.messages.map( const msgs = data.messages.map(
({ content, file_id, thumbnail_id, content_type, properties }) => { ({
content,
file_id,
thumbnail_id,
content_type,
properties,
from_user,
}) => {
const transformedContent = const transformedContent =
content_type == ContentTypes.file content_type == ContentTypes.file
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}` ? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}`
@@ -24,7 +31,13 @@ export default function useNormalizeMessage() {
content_type == ContentTypes.file content_type == ContentTypes.file
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}&download=true` ? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}&download=true`
: ""; : "";
let user = { ...(data.users[from_user] || {}) };
user.avatar =
typeof user.avatar !== "undefined"
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${user.avatar}`
: "";
return { return {
user,
content: transformedContent, content: transformedContent,
content_type, content_type,
properties, properties,
+37 -48
View File
@@ -5,7 +5,6 @@ import { useReadMessageMutation } from "../../../app/services/message";
import useChatScroll from "../../../common/hook/useChatScroll"; import useChatScroll from "../../../common/hook/useChatScroll";
import ChannelIcon from "../../../common/component/ChannelIcon"; import ChannelIcon from "../../../common/component/ChannelIcon";
import Tooltip from "../../../common/component/Tooltip"; import Tooltip from "../../../common/component/Tooltip";
import Send from "../../../common/component/Send";
import Contact from "../../../common/component/Contact"; import Contact from "../../../common/component/Contact";
import Layout from "../Layout"; import Layout from "../Layout";
import { renderMessageFragment } from "../utils"; import { renderMessageFragment } from "../utils";
@@ -13,7 +12,7 @@ import EditIcon from "../../../assets/icons/edit.svg";
import alertIcon from "../../../assets/icons/alert.svg?url"; import alertIcon from "../../../assets/icons/alert.svg?url";
import peopleIcon from "../../../assets/icons/people.svg?url"; import peopleIcon from "../../../assets/icons/people.svg?url";
import pinIcon from "../../../assets/icons/pin.svg?url"; import pinIcon from "../../../assets/icons/pin.svg?url";
import searchIcon from "../../../assets/icons/search.svg?url"; // import searchIcon from "../../../assets/icons/search.svg?url";
import headphoneIcon from "../../../assets/icons/headphone.svg?url"; import headphoneIcon from "../../../assets/icons/headphone.svg?url";
import boardosIcon from "../../../assets/icons/app.boardos.svg?url"; import boardosIcon from "../../../assets/icons/app.boardos.svg?url";
import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url"; import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
@@ -35,6 +34,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
const [addMemberModalVisible, setAddMemberModalVisible] = useState(false); const [addMemberModalVisible, setAddMemberModalVisible] = useState(false);
// const dispatch = useDispatch(); // const dispatch = useDispatch();
const { const {
selects,
msgIds, msgIds,
userIds, userIds,
data, data,
@@ -44,6 +44,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
footprint, footprint,
} = useSelector((store) => { } = useSelector((store) => {
return { return {
selects: store.ui.selectMessages[`channel_${cid}`],
footprint: store.footprint, footprint: store.footprint,
loginUser: store.contacts.byId[store.authData.uid], loginUser: store.contacts.byId[store.authData.uid],
loginUid: store.authData.uid, loginUid: store.authData.uid,
@@ -150,52 +151,40 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
) : null ) : null
} }
> >
<StyledChannelChat> <StyledChannelChat ref={ref}>
<div className="wrapper"> <div className="info">
<div className="chat" ref={ref}> <h2 className="title">Welcome to #{name} !</h2>
<div className="info"> <p className="desc">This is the start of the #{name} channel. </p>
<h2 className="title">Welcome to #{name} !</h2> <NavLink
<p className="desc"> to={`/setting/channel/${cid}?f=${pathname}`}
This is the start of the #{name} channel.{" "} className="edit"
</p> >
<NavLink <EditIcon className="icon" />
to={`/setting/channel/${cid}?f=${pathname}`} Edit Channel
className="edit" </NavLink>
> </div>
<EditIcon className="icon" /> <div className="feed">
Edit Channel {[...msgIds]
</NavLink> .sort((a, b) => {
</div> return Number(a) - Number(b);
<div className="feed"> })
{[...msgIds] .map((mid, idx) => {
.sort((a, b) => { const curr = messageData[mid];
return Number(a) - Number(b); if (!curr) return null;
}) const isFirst = idx == 0;
.map((mid, idx) => { const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
const curr = messageData[mid]; const read = curr?.from_uid == loginUid || mid <= readIndex;
if (!curr) return null; return renderMessageFragment({
const isFirst = idx == 0; selectMode: !!selects,
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]]; updateReadIndex: updateReadDebounced,
const read = curr?.from_uid == loginUid || mid <= readIndex; read,
return renderMessageFragment({ isFirst,
updateReadIndex: updateReadDebounced, prev,
read, curr,
isFirst, contextId: cid,
prev, context: "channel",
curr, });
contextId: cid, })}
context: "channel",
});
})}
</div>
</div>
<Send
key={cid}
id={cid}
context="channel"
name={name}
members={memberIds}
/>
</div> </div>
</StyledChannelChat> </StyledChannelChat>
{/* {unreads != 0 && ( {/* {unreads != 0 && (
+43 -58
View File
@@ -82,68 +82,53 @@ export const StyledContacts = styled.div`
} }
`; `;
export const StyledChannelChat = styled.article` export const StyledChannelChat = styled.article`
position: relative; padding: 18px 16px;
width: 100%; width: 100%;
/* margin-bottom: 120px; */ height: 100%;
> .wrapper { height: -webkit-fill-available;
background-color: #fff; overflow: auto;
> .info {
padding-top: 62px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0; align-items: flex-start;
padding-bottom: 16px; gap: 8px;
height: calc(100vh - 56px - 22px); .title {
.chat { font-weight: bold;
padding: 18px 16px; font-size: 36px;
height: 100%; line-height: 44px;
height: -webkit-fill-available;
overflow: auto;
> .info {
padding-top: 62px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
.title {
font-weight: bold;
font-size: 36px;
line-height: 44px;
}
.desc {
color: #78787c;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.edit {
display: flex;
align-items: center;
gap: 4px;
.icon {
width: 16px;
height: 16px;
path {
fill: #3c8ce7;
}
}
padding: 0;
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
}
> .feed {
/* display: flex;
flex-direction: column-reverse; */
}
} }
> .send { .desc {
margin: 0 16px; color: #78787c;
font-weight: 500;
font-size: 16px;
line-height: 24px;
}
.edit {
display: flex;
align-items: center;
gap: 4px;
.icon {
width: 16px;
height: 16px;
path {
fill: #3c8ce7;
}
}
padding: 0;
font-style: normal;
font-weight: 500;
font-size: 16px;
line-height: 24px;
background: linear-gradient(135deg, #3c8ce7 0%, #00eaff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
} }
} }
> .feed {
/* display: flex;
flex-direction: column-reverse; */
}
`; `;
+37 -39
View File
@@ -4,11 +4,10 @@ import { useDebounce } from "rooks";
import Tooltip from "../../../common/component/Tooltip"; import Tooltip from "../../../common/component/Tooltip";
import alertIcon from "../../../assets/icons/alert.svg?url"; import alertIcon from "../../../assets/icons/alert.svg?url";
import pinIcon from "../../../assets/icons/pin.svg?url"; import pinIcon from "../../../assets/icons/pin.svg?url";
import searchIcon from "../../../assets/icons/search.svg?url"; // import searchIcon from "../../../assets/icons/search.svg?url";
import boardosIcon from "../../../assets/icons/app.boardos.svg?url"; import boardosIcon from "../../../assets/icons/app.boardos.svg?url";
import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url"; import webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
import useChatScroll from "../../../common/hook/useChatScroll"; import useChatScroll from "../../../common/hook/useChatScroll";
import Send from "../../../common/component/Send";
import { useReadMessageMutation } from "../../../app/services/message"; import { useReadMessageMutation } from "../../../app/services/message";
import Contact from "../../../common/component/Contact"; import Contact from "../../../common/component/Contact";
import Layout from "../Layout"; import Layout from "../Layout";
@@ -19,17 +18,23 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
const updateReadDebounced = useDebounce(updateReadIndex, 300); const updateReadDebounced = useDebounce(updateReadIndex, 300);
console.log("dm files", dropFiles); console.log("dm files", dropFiles);
// const [mids, setMids] = useState([]); // const [mids, setMids] = useState([]);
const { msgIds, currUser, messageData, footprint, loginUid } = useSelector( const {
(store) => { msgIds,
return { currUser,
loginUid: store.authData.uid, messageData,
footprint: store.footprint, footprint,
currUser: store.contacts.byId[uid], loginUid,
msgIds: store.userMessage.byId[uid] || [], selects,
messageData: store.message, } = useSelector((store) => {
}; return {
} selects: store.ui.selectMessages[`user_${uid}`],
); loginUid: store.authData.uid,
footprint: store.footprint,
currUser: store.contacts.byId[uid],
msgIds: store.userMessage.byId[uid] || [],
messageData: store.message,
};
});
const ref = useChatScroll(msgIds); const ref = useChatScroll(msgIds);
if (!currUser) return null; if (!currUser) return null;
@@ -74,32 +79,25 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
</StyledHeader> </StyledHeader>
} }
> >
<StyledDMChat> <StyledDMChat ref={ref}>
<div className="chat" ref={ref}> {[...msgIds]
{[...msgIds] .sort((a, b) => {
.sort((a, b) => { return Number(a) - Number(b);
return Number(a) - Number(b); })
}) .map((mid, idx) => {
.map((mid, idx) => { const curr = messageData[mid];
const curr = messageData[mid]; const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
const prev = idx == 0 ? null : messageData[msgIds[idx - 1]]; const read = curr?.from_uid == loginUid || mid <= readIndex;
const read = curr?.from_uid == loginUid || mid <= readIndex; return renderMessageFragment({
return renderMessageFragment({ selectMode: !!selects,
updateReadIndex: updateReadDebounced, updateReadIndex: updateReadDebounced,
read, read,
prev, prev,
curr, curr,
contextId: uid, contextId: uid,
context: "user", context: "user",
}); });
})} })}
</div>
<Send
key={currUser?.uid}
context="user"
name={currUser?.name}
id={currUser?.uid}
/>
</StyledDMChat> </StyledDMChat>
</Layout> </Layout>
); );
+4 -15
View File
@@ -28,20 +28,9 @@ export const StyledHeader = styled.header`
} }
`; `;
export const StyledDMChat = styled.article` export const StyledDMChat = styled.article`
position: relative;
display: flex;
flex-direction: column;
width: 100%; width: 100%;
padding: 0; padding: 18px 16px;
padding-bottom: 16px; height: 100%;
height: calc(100vh - 56px - 22px); height: -webkit-fill-available;
> .chat { overflow: auto;
padding: 18px 16px;
height: 100%;
height: -webkit-fill-available;
overflow: auto;
}
> .send {
margin: 0 16px;
}
`; `;
+68
View File
@@ -0,0 +1,68 @@
import { useState } from "react";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { useKey } from "rooks";
import { updateSelectMessages } from "../../../app/slices/ui";
import IconForward from "../../../assets/icons/forward.svg";
import IconBookmark from "../../../assets/icons/bookmark.svg";
import IconDelete from "../../../assets/icons/delete.svg";
import IconClose from "../../../assets/icons/close.circle.svg";
import ForwardModal from "../../../common/component/ForwardModal";
const Styled = styled.div`
position: relative;
padding: 16px;
/* padding-bottom: 0; */
display: flex;
gap: 32px;
align-items: center;
justify-content: center;
box-shadow: 0px -1px 0px rgba(0, 0, 0, 0.05);
.opt {
padding: 8px;
background: #f2f4f7;
border-radius: var(--br);
}
.close {
cursor: pointer;
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
}
`;
export default function Operations({ context, id }) {
const mids = useSelector(
(store) => store.ui.selectMessages[`${context}_${id}`]
);
const [forwardModalVisible, setForwardModalVisible] = useState(false);
const dispatch = useDispatch();
const handleClose = () => {
dispatch(updateSelectMessages({ context, id, operation: "reset" }));
};
const toggleForwardModal = () => {
setForwardModalVisible((prev) => !prev);
};
useKey("Escape", (evt) => {
console.log("Escape keypress", evt);
dispatch(updateSelectMessages({ context, id, operation: "reset" }));
});
return (
<>
<Styled>
<button className="opt" onClick={toggleForwardModal}>
<IconForward />
</button>
<button className="opt">
<IconBookmark />
</button>
<button className="opt">
<IconDelete />
</button>
<IconClose className="close" onClick={handleClose} />
</Styled>
{forwardModalVisible && (
<ForwardModal mids={mids} closeModal={toggleForwardModal} />
)}
</>
);
}
@@ -1,117 +1,20 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { useDrop } from "react-dnd"; import { useDrop } from "react-dnd";
import { useDispatch } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { NativeTypes } from "react-dnd-html5-backend"; import { NativeTypes } from "react-dnd-html5-backend";
import styled from "styled-components"; import { updateUploadFiles } from "../../../app/slices/ui";
import { updateUploadFiles } from "../../app/slices/ui"; import ImagePreviewModal from "../../../common/component/ImagePreviewModal";
import ImagePreviewModal from "../../common/component/ImagePreviewModal"; import Send from "../../../common/component/Send";
const StyledWrapper = styled.article` import Styled from "./styled";
position: relative; import Operations from "./Operations";
width: 100%;
background: #fff;
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
> .head {
box-sizing: content-box;
height: 56px;
padding: 0 20px;
/* box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1); */
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
> .main {
height: calc(100vh - 56px - 22px);
width: 100%;
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
.members {
box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1);
/* margin-top: 1px; */
/* border-top: 1px solid transparent; */
}
> .aside {
padding: 12px;
position: absolute;
right: 0;
top: -56px;
transform: translateX(100%);
display: flex;
flex-direction: column;
.divider {
border: none;
background-color: #d4d4d4;
width: 16px;
height: 1px;
margin: 18px auto;
}
.tools,
.apps {
display: flex;
flex-direction: column;
}
.tools {
gap: 24px;
.tool {
cursor: pointer;
}
}
.apps {
gap: 15px;
}
}
}
.drag_tip {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
visibility: hidden;
/* pointer-events: none; */
&.visible {
visibility: visible;
}
.box {
padding: 16px;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
background: #52edff;
.inner {
padding: 16px;
padding-top: 64px;
border: 2px dashed #a5f3fc;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
.head {
font-weight: 600;
font-size: 20px;
line-height: 30px;
}
.intro {
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
}
}
}
`;
export default function Layout({ export default function Layout({
children, children,
header, header,
aside = null, aside = null,
contacts = null, contacts = null,
// dropFiles = [], dropFiles = [],
context = "channel", context = "channel",
to = null, to = null,
}) { }) {
@@ -119,6 +22,9 @@ export default function Layout({
const messagesContainer = useRef(null); const messagesContainer = useRef(null);
const [previewImage, setPreviewImage] = useState(null); const [previewImage, setPreviewImage] = useState(null);
const selects = useSelector(
(store) => store.ui.selectMessages[`${context}_${to}`]
);
const [{ isActive }, drop] = useDrop( const [{ isActive }, drop] = useDrop(
() => ({ () => ({
accept: [NativeTypes.FILE], accept: [NativeTypes.FILE],
@@ -139,11 +45,16 @@ export default function Layout({
}), }),
[context, to] [context, to]
); );
// useEffect(() => { useEffect(() => {
// if (dropFiles?.length) { if (dropFiles?.length) {
// setFiles((prevs) => [...prevs, ...dropFiles]); const filesData = dropFiles.map((file) => {
// } const { size, type, name } = file;
// }, [dropFiles]); const url = URL.createObjectURL(file);
return { size, type, name, url };
});
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
}, [dropFiles]);
const closePreviewModal = () => { const closePreviewModal = () => {
setPreviewImage(null); setPreviewImage(null);
@@ -177,10 +88,16 @@ export default function Layout({
{previewImage && ( {previewImage && (
<ImagePreviewModal data={previewImage} closeModal={closePreviewModal} /> <ImagePreviewModal data={previewImage} closeModal={closePreviewModal} />
)} )}
<StyledWrapper ref={drop}> <Styled ref={drop}>
{header} {header}
<main className="main" ref={messagesContainer}> <main className="main" ref={messagesContainer}>
{children} <div className="chat">
{children}
<div className={`send ${selects ? "selecting" : ""}`}>
<Send key={to} id={to} context={context} />
{selects && <Operations context={context} id={to} />}
</div>
</div>
{contacts && <div className="members">{contacts}</div>} {contacts && <div className="members">{contacts}</div>}
{aside && <div className="aside">{aside}</div>} {aside && <div className="aside">{aside}</div>}
</main> </main>
@@ -202,7 +119,7 @@ export default function Layout({
</div> </div>
</div> </div>
</div> </div>
</StyledWrapper> </Styled>
</> </>
); );
} }
+119
View File
@@ -0,0 +1,119 @@
import styled from "styled-components";
const Styled = styled.article`
position: relative;
width: 100%;
background: #fff;
border-top-right-radius: 16px;
border-bottom-right-radius: 16px;
> .head {
box-sizing: content-box;
height: 56px;
padding: 0 20px;
/* box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.1); */
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
> .main {
height: calc(100vh - 56px - 22px);
width: 100%;
display: flex;
align-items: flex-start;
justify-content: space-between;
position: relative;
> .chat {
width: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 0;
/* padding-bottom: 16px; */
height: calc(100vh - 56px - 22px);
> .send {
padding: 0 16px 16px 16px;
&.selecting {
padding: 0;
> .send {
display: none;
}
}
}
}
.members {
box-shadow: inset 0px 10px 2px -10px rgba(0, 0, 0, 0.1);
/* margin-top: 1px; */
/* border-top: 1px solid transparent; */
}
> .aside {
padding: 12px;
position: absolute;
right: 0;
top: -56px;
transform: translateX(100%);
display: flex;
flex-direction: column;
.divider {
border: none;
background-color: #d4d4d4;
width: 16px;
height: 1px;
margin: 18px auto;
}
.tools,
.apps {
display: flex;
flex-direction: column;
}
.tools {
gap: 24px;
.tool {
cursor: pointer;
}
}
.apps {
gap: 15px;
}
}
}
.drag_tip {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
visibility: hidden;
/* pointer-events: none; */
&.visible {
visibility: visible;
}
.box {
padding: 16px;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 8px;
background: #52edff;
.inner {
padding: 16px;
padding-top: 64px;
border: 2px dashed #a5f3fc;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
color: #fff;
.head {
font-weight: 600;
font-size: 20px;
line-height: 30px;
}
.intro {
font-weight: normal;
font-size: 14px;
line-height: 20px;
}
}
}
}
`;
export default Styled;
+78 -7
View File
@@ -1,9 +1,13 @@
import React from "react"; import React from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { isImage } from "../../common/utils"; import { isImage } from "../../common/utils";
import { ContentTypes } from "../../app/config"; import { ContentTypes } from "../../app/config";
import Checkbox from "../../common/component/styled/Checkbox";
import Divider from "../../common/component/Divider"; import Divider from "../../common/component/Divider";
import Message from "../../common/component/Message"; import Message from "../../common/component/Message";
import { updateSelectMessages } from "../../app/slices/ui";
// function debounce(callback, wait = 2000, immediate = false) { // function debounce(callback, wait = 2000, immediate = false) {
// let timeout = null; // let timeout = null;
// return function () { // return function () {
@@ -86,7 +90,66 @@ export const renderPreviewMessage = (message = null) => {
} }
return res; return res;
}; };
const StyledWrapper = styled.div`
position: relative;
width: 100%;
display: flex;
align-items: flex-start;
gap: 8px;
> .overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
> .check {
display: none;
margin-top: 18px;
margin-left: 8px;
}
> .message {
flex: 1;
}
&.select {
/* cursor: pointer; */
&:hover {
border-radius: var(--br);
background: #f5f6f7;
}
> .check {
display: block;
}
}
`;
const MessageWrapper = ({ selectMode = false, context, id, mid, children }) => {
const dispatch = useDispatch();
const selects = useSelector(
(store) => store.ui.selectMessages[`${context}_${id}`]
);
const selected = !!(selects && selects.find((s) => s == mid));
const toggleSelect = () => {
const operation = selected ? "remove" : "add";
dispatch(updateSelectMessages({ context, id, operation, data: mid }));
};
return (
<StyledWrapper className={selectMode ? "select" : ""}>
<Checkbox className="check" checked={selected} />
{/* {React.cloneElement(children, { readOnly: selected })} */}
{children}
{selectMode && (
<div
className="overlay"
onClick={selectMode ? toggleSelect : null}
></div>
)}
</StyledWrapper>
);
};
export const renderMessageFragment = ({ export const renderMessageFragment = ({
selectMode = false,
isFirst = false, isFirst = false,
read = true, read = true,
updateReadIndex, updateReadIndex,
@@ -111,15 +174,23 @@ export const renderMessageFragment = ({
return ( return (
<React.Fragment key={mid}> <React.Fragment key={mid}>
{divider && <Divider content={divider}></Divider>} {divider && <Divider content={divider}></Divider>}
<Message <MessageWrapper
isFirst={isFirst}
updateReadIndex={updateReadIndex}
read={read}
context={context} context={context}
id={contextId}
mid={mid} mid={mid}
key={mid} selectMode={selectMode}
contextId={contextId} >
/> <Message
readOnly={selectMode}
isFirst={isFirst}
updateReadIndex={updateReadIndex}
read={read}
context={context}
mid={mid}
key={mid}
contextId={contextId}
/>
</MessageWrapper>
</React.Fragment> </React.Fragment>
); );
}; };