diff --git a/src/app/services/message.js b/src/app/services/message.js
index f4dfccbe..928ad702 100644
--- a/src/app/services/message.js
+++ b/src/app/services/message.js
@@ -76,6 +76,13 @@ export const messageApi = createApi({
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({
query: ({ reply_mid, content, type = "text" }) => ({
headers: {
@@ -114,6 +121,7 @@ export const messageApi = createApi({
});
export const {
+ usePinMessageMutation,
useLazyGetArchiveMessageQuery,
useGetArchiveMessageQuery,
useLazyGetOGInfoQuery,
diff --git a/src/app/slices/ui.js b/src/app/slices/ui.js
index 9545d491..8a6854b6 100644
--- a/src/app/slices/ui.js
+++ b/src/app/slices/ui.js
@@ -11,6 +11,7 @@ const initialState = {
menuExpand: false,
fileListView: Views.grid,
uploadFiles: {},
+ selectMessages: {},
};
const uiSlice = createSlice({
name: "ui",
@@ -40,6 +41,7 @@ const uiSlice = createSlice({
state.userGuide[key] = obj[key];
});
},
+
updateUploadFiles(state, action) {
const {
context = "channel",
@@ -105,6 +107,33 @@ const uiSlice = createSlice({
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 {
@@ -115,6 +144,7 @@ export const {
toggleMenuExpand,
updateFileListView,
updateUploadFiles,
+ updateSelectMessages,
updateUserGuide,
} = uiSlice.actions;
export default uiSlice.reducer;
diff --git a/src/assets/icons/forward.svg b/src/assets/icons/forward.svg
new file mode 100644
index 00000000..70dcf2da
--- /dev/null
+++ b/src/assets/icons/forward.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/common/component/Avatar.js b/src/common/component/Avatar.js
index 806b1808..73c01aaa 100644
--- a/src/common/component/Avatar.js
+++ b/src/common/component/Avatar.js
@@ -14,7 +14,8 @@ export default function Avatar({
}
const tmp = getInitialsAvatar({
initials: getInitials(name),
- background: type == "channel" ? "#4ea758" : undefined,
+ background: type == "channel" ? "#EAECF0" : undefined,
+ foreground: type == "channel" ? "#475467" : undefined,
});
setSrc(tmp);
};
@@ -22,7 +23,8 @@ export default function Avatar({
if (!url) {
const tmp = getInitialsAvatar({
initials: getInitials(name),
- background: type == "channel" ? "#4ea758" : undefined,
+ background: type == "channel" ? "#EAECF0" : undefined,
+ foreground: type == "channel" ? "#475467" : undefined,
});
setSrc(tmp);
} else {
diff --git a/src/common/component/Contact.js b/src/common/component/Contact.js
index 47a90e67..ec7e0381 100644
--- a/src/common/component/Contact.js
+++ b/src/common/component/Contact.js
@@ -35,12 +35,13 @@ const StyledWrapper = styled.div`
}
.status {
position: absolute;
- bottom: 0;
- right: -2px;
+ bottom: -2px;
+ right: -4px;
width: 10px;
height: 10px;
+ box-sizing: content-box;
border-radius: 50%;
- outline: 2px solid #fff;
+ border: 2px solid #fff;
&.online {
background-color: #22c55e;
}
diff --git a/src/common/component/ForwardModal/index.js b/src/common/component/ForwardModal/index.js
index ea28a1ef..fead6fbf 100644
--- a/src/common/component/ForwardModal/index.js
+++ b/src/common/component/ForwardModal/index.js
@@ -16,7 +16,7 @@ import CloseIcon from "../../../assets/icons/close.circle.svg";
import StyledCheckbox from "../../component/styled/Checkbox";
import toast from "react-hot-toast";
-export default function ForwardModal({ mid, closeModal }) {
+export default function ForwardModal({ mids, closeModal }) {
const [appendText, setAppendText] = useState("");
const { sendMessages } = useSendMessage();
const { forwardMessage, forwarding } = useForwardMessage();
@@ -45,7 +45,7 @@ export default function ForwardModal({ mid, closeModal }) {
};
const handleForward = async () => {
await forwardMessage({
- mids: [mid],
+ mids: mids,
users: selectedMembers,
channels: selectedChannels,
});
@@ -170,8 +170,10 @@ export default function ForwardModal({ mid, closeModal }) {
);
})}
-
-
+
+ {mids.map((mid) => (
+
+ ))}
.reply {
+ background: none;
+ }
}
.input {
margin-bottom: 32px;
diff --git a/src/common/component/Message/Commands.js b/src/common/component/Message/Commands.js
index c7c66af0..873bcfcd 100644
--- a/src/common/component/Message/Commands.js
+++ b/src/common/component/Message/Commands.js
@@ -4,6 +4,7 @@ import styled from "styled-components";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
// import toast from "react-hot-toast";
+import { updateSelectMessages } from "../../../app/slices/ui";
import { addReplyingMessage } from "../../../app/slices/message";
import StyledMenu from "../styled/Menu";
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 moreIcon from "../../../assets/icons/more.svg?url";
import ForwardModal from "../ForwardModal";
+import PinMessageModal from "./PinMessageModal";
const StyledCmds = styled.ul`
z-index: 9999;
position: absolute;
@@ -50,12 +52,14 @@ const StyledCmds = styled.ul`
}
`;
export default function Commands({
+ context = "user",
contextId = 0,
mid = 0,
from_uid = 0,
toggleEditMessage,
}) {
const dispatch = useDispatch();
+ const [pinModalVisible, setPinModalVisible] = useState(false);
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [forwardModalVisible, setForwardModalVisible] = useState(false);
const [tippyVisible, setTippyVisible] = useState(false);
@@ -77,9 +81,17 @@ export default function Commands({
hideAll();
setDeleteModalVisible((prev) => !prev);
};
+ const togglePinModal = () => {
+ hideAll();
+ setPinModalVisible((prev) => !prev);
+ };
const handleTippyVisible = (visible = true) => {
setTippyVisible(visible);
};
+ const handleSelect = (mid) => {
+ dispatch(updateSelectMessages({ context, id: contextId, data: mid }));
+ hideAll();
+ };
return (
{/* Edit Message */}
- {/* Pin Message */}
+ {context == "channel" && (
+
+ Pin Message
+
+ )}
Forward
Reply
+
+ Select
+
{currUid == from_uid && (
Delete Message
@@ -153,7 +172,14 @@ export default function Commands({
)}
{forwardModalVisible && (
-
+
+ )}
+ {pinModalVisible && (
+
)}
);
diff --git a/src/common/component/Message/PinMessageModal.js b/src/common/component/Message/PinMessageModal.js
new file mode 100644
index 00000000..6d54ef92
--- /dev/null
+++ b/src/common/component/Message/PinMessageModal.js
@@ -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 (
+
+
+
+
+ >
+ }
+ title="Pin It"
+ description="You sure you want to pin this message to #gerenal?"
+ >
+
+
+
+ );
+}
diff --git a/src/common/component/Message/Reply.js b/src/common/component/Message/Reply.js
index 9e404e5e..15b2ec66 100644
--- a/src/common/component/Message/Reply.js
+++ b/src/common/component/Message/Reply.js
@@ -38,6 +38,7 @@ const Styled = styled.div`
}
}
.content {
+ overflow: hidden;
font-weight: 500;
font-size: 14px;
line-height: 20px;
diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js
index 23eb0623..6cf87ac2 100644
--- a/src/common/component/Message/index.js
+++ b/src/common/component/Message/index.js
@@ -17,6 +17,7 @@ import Tooltip from "../Tooltip";
dayjs.extend(isToday);
dayjs.extend(isYesterday);
function Message({
+ readOnly = false,
contextId = 0,
mid = "",
context = "user",
@@ -77,8 +78,13 @@ function Message({
console.log("render message");
// return null;
return (
-
+
{currUser.name}
@@ -130,8 +136,9 @@ function Message({
{reactions && }
- {!edit && (
+ {!edit && !readOnly && (
.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 { normalizeMessage, messages } = useNormalizeMessage();
const [forwards, setForwards] = useState(null);
@@ -123,25 +150,54 @@ const ForwardedMessage = ({ context, to, from_uid, id }) => {
useEffect(() => {
if (messages) {
setForwards(
- messages.map((msg) => {
- const {
- download,
- content,
- content_type,
- properties,
- thumbnail,
- } = msg;
- return renderContent({
- download,
- context,
- to,
- from_uid,
- content,
- content_type,
- properties,
- thumbnail,
- });
- })
+
+
+
+ Forwarded
+
+
+ {messages.map((msg, idx) => {
+ const {
+ user = {},
+ created_at,
+ download,
+ content,
+ content_type,
+ properties,
+ thumbnail,
+ } = msg;
+ return (
+
+ {user && (
+
+ )}
+
+
+ {user?.name}
+
+ {dayjs(created_at).format("YYYY-MM-DD h:mm:ss A")}
+
+
+
+ {renderContent({
+ download,
+ context,
+ to,
+ from_uid,
+ content,
+ content_type,
+ properties,
+ thumbnail,
+ })}
+
+
+
+ );
+ })}
+
+
);
}
}, [messages, context, to, from_uid]);
diff --git a/src/common/component/Message/styled.js b/src/common/component/Message/styled.js
index 6db0a4a2..b186e888 100644
--- a/src/common/component/Message/styled.js
+++ b/src/common/component/Message/styled.js
@@ -17,11 +17,16 @@ const StyledMsg = styled.div`
}
&:hover,
&.preview {
+ content-visibility: inherit;
+ contain-intrinsic-size: inherit;
background: #f5f6f7;
.cmds {
visibility: visible;
}
}
+ &.readonly:hover {
+ background: none;
+ }
.avatar {
flex-shrink: 0;
cursor: pointer;
diff --git a/src/common/component/Send/index.js b/src/common/component/Send/index.js
index 99387bf2..dc2132bb 100644
--- a/src/common/component/Send/index.js
+++ b/src/common/component/Send/index.js
@@ -26,10 +26,8 @@ const Modes = {
markdown: "markdown",
};
function Send({
- name,
- context = "channel",
- members = [],
// 发给谁,或者是channel,或者是user
+ context = "channel",
id = "",
}) {
const editor = useMixedEditor(`${context}_${id}`);
@@ -37,16 +35,25 @@ function Send({
const dispatch = useDispatch();
const addLocalFileMesage = useAddLocalFileMessage({ context, to: id });
// 谁发的
- const { from_uid, replying_mid = null, mode, uploadFiles } = useSelector(
- (store) => {
- return {
- mode: store.ui.inputMode,
- from_uid: store.authData.uid,
- replying_mid: store.message.replying[id],
- uploadFiles: store.ui.uploadFiles[`${context}_${id}`],
- };
- }
- );
+ const {
+ from_uid,
+ replying_mid = null,
+ mode,
+ uploadFiles,
+ channelsData,
+ 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 });
useEffect(() => {
@@ -121,7 +128,15 @@ function Send({
const toggleMode = () => {
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 members =
+ context == "channel"
+ ? channelsData[id]?.is_public
+ ? uids
+ : channelsData[id]?.members
+ : [];
return (
{replying_mid && }
diff --git a/src/common/component/styled/Menu.js b/src/common/component/styled/Menu.js
index a0f18d7a..f8f75ea1 100644
--- a/src/common/component/styled/Menu.js
+++ b/src/common/component/styled/Menu.js
@@ -7,7 +7,7 @@ const StyledMenu = styled.ul`
background-color: #fff;
box-shadow: 0px 20px 25px 20px rgba(31, 41, 55, 0.1),
0px 10px 10px rgba(31, 41, 55, 0.04);
- border-radius: var(--br);
+ border-radius: 12px;
.item {
white-space: nowrap;
cursor: pointer;
diff --git a/src/common/hook/useNormalizeMessage.js b/src/common/hook/useNormalizeMessage.js
index ad736921..e8ea8bf1 100644
--- a/src/common/hook/useNormalizeMessage.js
+++ b/src/common/hook/useNormalizeMessage.js
@@ -11,7 +11,14 @@ export default function useNormalizeMessage() {
useEffect(() => {
if (data && isSuccess) {
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 =
content_type == ContentTypes.file
? `${BASE_URL}/resource/archive/attachment?file_path=${filePath}&attachment_id=${file_id}`
@@ -24,7 +31,13 @@ export default function useNormalizeMessage() {
content_type == ContentTypes.file
? `${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 {
+ user,
content: transformedContent,
content_type,
properties,
diff --git a/src/routes/chat/ChannelChat/index.js b/src/routes/chat/ChannelChat/index.js
index ced58a9a..d56f0d51 100644
--- a/src/routes/chat/ChannelChat/index.js
+++ b/src/routes/chat/ChannelChat/index.js
@@ -5,7 +5,6 @@ import { useReadMessageMutation } from "../../../app/services/message";
import useChatScroll from "../../../common/hook/useChatScroll";
import ChannelIcon from "../../../common/component/ChannelIcon";
import Tooltip from "../../../common/component/Tooltip";
-import Send from "../../../common/component/Send";
import Contact from "../../../common/component/Contact";
import Layout from "../Layout";
import { renderMessageFragment } from "../utils";
@@ -13,7 +12,7 @@ import EditIcon from "../../../assets/icons/edit.svg";
import alertIcon from "../../../assets/icons/alert.svg?url";
import peopleIcon from "../../../assets/icons/people.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 boardosIcon from "../../../assets/icons/app.boardos.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 dispatch = useDispatch();
const {
+ selects,
msgIds,
userIds,
data,
@@ -44,6 +44,7 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
footprint,
} = useSelector((store) => {
return {
+ selects: store.ui.selectMessages[`channel_${cid}`],
footprint: store.footprint,
loginUser: store.contacts.byId[store.authData.uid],
loginUid: store.authData.uid,
@@ -150,52 +151,40 @@ export default function ChannelChat({ cid = "", dropFiles = [] }) {
) : null
}
>
-
-
-
-
-
Welcome to #{name} !
-
- This is the start of the #{name} channel.{" "}
-
-
-
- Edit Channel
-
-
-
- {[...msgIds]
- .sort((a, b) => {
- return Number(a) - Number(b);
- })
- .map((mid, idx) => {
- const curr = messageData[mid];
- if (!curr) return null;
- const isFirst = idx == 0;
- const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
- const read = curr?.from_uid == loginUid || mid <= readIndex;
- return renderMessageFragment({
- updateReadIndex: updateReadDebounced,
- read,
- isFirst,
- prev,
- curr,
- contextId: cid,
- context: "channel",
- });
- })}
-
-
-
+
+
+
Welcome to #{name} !
+
This is the start of the #{name} channel.
+
+
+ Edit Channel
+
+
+
+ {[...msgIds]
+ .sort((a, b) => {
+ return Number(a) - Number(b);
+ })
+ .map((mid, idx) => {
+ const curr = messageData[mid];
+ if (!curr) return null;
+ const isFirst = idx == 0;
+ const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
+ const read = curr?.from_uid == loginUid || mid <= readIndex;
+ return renderMessageFragment({
+ selectMode: !!selects,
+ updateReadIndex: updateReadDebounced,
+ read,
+ isFirst,
+ prev,
+ curr,
+ contextId: cid,
+ context: "channel",
+ });
+ })}
{/* {unreads != 0 && (
diff --git a/src/routes/chat/ChannelChat/styled.js b/src/routes/chat/ChannelChat/styled.js
index 86345845..953bd69e 100644
--- a/src/routes/chat/ChannelChat/styled.js
+++ b/src/routes/chat/ChannelChat/styled.js
@@ -82,68 +82,53 @@ export const StyledContacts = styled.div`
}
`;
export const StyledChannelChat = styled.article`
- position: relative;
+ padding: 18px 16px;
width: 100%;
- /* margin-bottom: 120px; */
- > .wrapper {
- background-color: #fff;
+ height: 100%;
+ height: -webkit-fill-available;
+ overflow: auto;
+ > .info {
+ padding-top: 62px;
display: flex;
flex-direction: column;
- padding: 0;
- padding-bottom: 16px;
- height: calc(100vh - 56px - 22px);
- .chat {
- padding: 18px 16px;
- height: 100%;
- 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; */
- }
+ align-items: flex-start;
+ gap: 8px;
+ .title {
+ font-weight: bold;
+ font-size: 36px;
+ line-height: 44px;
}
- > .send {
- margin: 0 16px;
+ .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; */
+ }
`;
diff --git a/src/routes/chat/DMChat/index.js b/src/routes/chat/DMChat/index.js
index 1bfadd33..6009a093 100644
--- a/src/routes/chat/DMChat/index.js
+++ b/src/routes/chat/DMChat/index.js
@@ -4,11 +4,10 @@ import { useDebounce } from "rooks";
import Tooltip from "../../../common/component/Tooltip";
import alertIcon from "../../../assets/icons/alert.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 webrowseIcon from "../../../assets/icons/app.webrowse.svg?url";
import useChatScroll from "../../../common/hook/useChatScroll";
-import Send from "../../../common/component/Send";
import { useReadMessageMutation } from "../../../app/services/message";
import Contact from "../../../common/component/Contact";
import Layout from "../Layout";
@@ -19,17 +18,23 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
const updateReadDebounced = useDebounce(updateReadIndex, 300);
console.log("dm files", dropFiles);
// const [mids, setMids] = useState([]);
- const { msgIds, currUser, messageData, footprint, loginUid } = useSelector(
- (store) => {
- return {
- loginUid: store.authData.uid,
- footprint: store.footprint,
- currUser: store.contacts.byId[uid],
- msgIds: store.userMessage.byId[uid] || [],
- messageData: store.message,
- };
- }
- );
+ const {
+ msgIds,
+ currUser,
+ messageData,
+ footprint,
+ loginUid,
+ selects,
+ } = 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);
if (!currUser) return null;
@@ -74,32 +79,25 @@ export default function DMChat({ uid = "", dropFiles = [] }) {
}
>
-
-
- {[...msgIds]
- .sort((a, b) => {
- return Number(a) - Number(b);
- })
- .map((mid, idx) => {
- const curr = messageData[mid];
- const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
- const read = curr?.from_uid == loginUid || mid <= readIndex;
- return renderMessageFragment({
- updateReadIndex: updateReadDebounced,
- read,
- prev,
- curr,
- contextId: uid,
- context: "user",
- });
- })}
-
-
+
+ {[...msgIds]
+ .sort((a, b) => {
+ return Number(a) - Number(b);
+ })
+ .map((mid, idx) => {
+ const curr = messageData[mid];
+ const prev = idx == 0 ? null : messageData[msgIds[idx - 1]];
+ const read = curr?.from_uid == loginUid || mid <= readIndex;
+ return renderMessageFragment({
+ selectMode: !!selects,
+ updateReadIndex: updateReadDebounced,
+ read,
+ prev,
+ curr,
+ contextId: uid,
+ context: "user",
+ });
+ })}
);
diff --git a/src/routes/chat/DMChat/styled.js b/src/routes/chat/DMChat/styled.js
index 6131a63b..09a31415 100644
--- a/src/routes/chat/DMChat/styled.js
+++ b/src/routes/chat/DMChat/styled.js
@@ -28,20 +28,9 @@ export const StyledHeader = styled.header`
}
`;
export const StyledDMChat = styled.article`
- position: relative;
- display: flex;
- flex-direction: column;
width: 100%;
- padding: 0;
- padding-bottom: 16px;
- height: calc(100vh - 56px - 22px);
- > .chat {
- padding: 18px 16px;
- height: 100%;
- height: -webkit-fill-available;
- overflow: auto;
- }
- > .send {
- margin: 0 16px;
- }
+ padding: 18px 16px;
+ height: 100%;
+ height: -webkit-fill-available;
+ overflow: auto;
`;
diff --git a/src/routes/chat/Layout/Operations.js b/src/routes/chat/Layout/Operations.js
new file mode 100644
index 00000000..bac43b8f
--- /dev/null
+++ b/src/routes/chat/Layout/Operations.js
@@ -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 (
+ <>
+
+
+
+
+
+
+ {forwardModalVisible && (
+
+ )}
+ >
+ );
+}
diff --git a/src/routes/chat/Layout.js b/src/routes/chat/Layout/index.js
similarity index 50%
rename from src/routes/chat/Layout.js
rename to src/routes/chat/Layout/index.js
index 4196b16f..a439797b 100644
--- a/src/routes/chat/Layout.js
+++ b/src/routes/chat/Layout/index.js
@@ -1,117 +1,20 @@
import { useState, useRef, useEffect } from "react";
import { useDrop } from "react-dnd";
-import { useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { NativeTypes } from "react-dnd-html5-backend";
-import styled from "styled-components";
-import { updateUploadFiles } from "../../app/slices/ui";
-import ImagePreviewModal from "../../common/component/ImagePreviewModal";
-const StyledWrapper = 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;
- .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;
- }
- }
- }
- }
-`;
+import { updateUploadFiles } from "../../../app/slices/ui";
+import ImagePreviewModal from "../../../common/component/ImagePreviewModal";
+import Send from "../../../common/component/Send";
+import Styled from "./styled";
+import Operations from "./Operations";
export default function Layout({
children,
header,
aside = null,
contacts = null,
- // dropFiles = [],
+ dropFiles = [],
context = "channel",
to = null,
}) {
@@ -119,6 +22,9 @@ export default function Layout({
const messagesContainer = useRef(null);
const [previewImage, setPreviewImage] = useState(null);
+ const selects = useSelector(
+ (store) => store.ui.selectMessages[`${context}_${to}`]
+ );
const [{ isActive }, drop] = useDrop(
() => ({
accept: [NativeTypes.FILE],
@@ -139,11 +45,16 @@ export default function Layout({
}),
[context, to]
);
- // useEffect(() => {
- // if (dropFiles?.length) {
- // setFiles((prevs) => [...prevs, ...dropFiles]);
- // }
- // }, [dropFiles]);
+ useEffect(() => {
+ if (dropFiles?.length) {
+ const filesData = dropFiles.map((file) => {
+ const { size, type, name } = file;
+ const url = URL.createObjectURL(file);
+ return { size, type, name, url };
+ });
+ dispatch(updateUploadFiles({ context, id: to, data: filesData }));
+ }
+ }, [dropFiles]);
const closePreviewModal = () => {
setPreviewImage(null);
@@ -177,10 +88,16 @@ export default function Layout({
{previewImage && (
)}
-
+
{header}
- {children}
+
+ {children}
+
+
+ {selects && }
+
+
{contacts && {contacts}
}
{aside && {aside}
}
@@ -202,7 +119,7 @@ export default function Layout({
-
+
>
);
}
diff --git a/src/routes/chat/Layout/styled.js b/src/routes/chat/Layout/styled.js
new file mode 100644
index 00000000..65ad82e8
--- /dev/null
+++ b/src/routes/chat/Layout/styled.js
@@ -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;
diff --git a/src/routes/chat/utils.js b/src/routes/chat/utils.js
index 4df3badb..a26cf0e2 100644
--- a/src/routes/chat/utils.js
+++ b/src/routes/chat/utils.js
@@ -1,9 +1,13 @@
import React from "react";
import dayjs from "dayjs";
+import styled from "styled-components";
+import { useDispatch, useSelector } from "react-redux";
import { isImage } from "../../common/utils";
import { ContentTypes } from "../../app/config";
+import Checkbox from "../../common/component/styled/Checkbox";
import Divider from "../../common/component/Divider";
import Message from "../../common/component/Message";
+import { updateSelectMessages } from "../../app/slices/ui";
// function debounce(callback, wait = 2000, immediate = false) {
// let timeout = null;
// return function () {
@@ -86,7 +90,66 @@ export const renderPreviewMessage = (message = null) => {
}
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 (
+
+
+ {/* {React.cloneElement(children, { readOnly: selected })} */}
+ {children}
+ {selectMode && (
+
+ )}
+
+ );
+};
export const renderMessageFragment = ({
+ selectMode = false,
isFirst = false,
read = true,
updateReadIndex,
@@ -111,15 +174,23 @@ export const renderMessageFragment = ({
return (
{divider && }
-
+ selectMode={selectMode}
+ >
+
+
);
};