feat: brand new send file UX

This commit is contained in:
zerosoul
2022-04-22 17:04:58 +08:00
parent c24ee53967
commit b34eee726b
28 changed files with 1003 additions and 291 deletions
+2 -1
View File
@@ -33,7 +33,8 @@ const messageSlice = createSlice({
data.file_path data.file_path
)}&download=true`; )}&download=true`;
// image // image
const isPic = isImage(properties.file_type, properties.size); const props = properties ?? {};
const isPic = isImage(props.content_type, props.size);
if (isPic) { if (isPic) {
data.thumbnail = `${BASE_URL}/resource/file?file_path=${encodeURIComponent( data.thumbnail = `${BASE_URL}/resource/file?file_path=${encodeURIComponent(
data.file_path data.file_path
+79 -1
View File
@@ -3,9 +3,14 @@ import { Views } from "../config";
const initialState = { const initialState = {
online: true, online: true,
ready: false, ready: false,
userGuide: {
visible: false,
step: 1,
},
inputMode: "text", inputMode: "text",
menuExpand: false, menuExpand: false,
fileListView: Views.item, fileListView: Views.grid,
uploadFiles: {},
}; };
const uiSlice = createSlice({ const uiSlice = createSlice({
name: "ui", name: "ui",
@@ -29,6 +34,77 @@ const uiSlice = createSlice({
updateFileListView(state, action) { updateFileListView(state, action) {
state.fileListView = action.payload; state.fileListView = action.payload;
}, },
updateUserGuide(state, action) {
const obj = action.payload || {};
Object.keys(obj).forEach((key) => {
state.userGuide[key] = obj[key];
});
},
updateUploadFiles(state, action) {
const {
context = "channel",
id = null,
operation = "add",
...rest
} = action.payload;
if (!id || !context) return;
const _key = `${context}_${id}`;
let files = state.uploadFiles[_key];
switch (operation) {
case "add":
{
const { data } = rest;
const isArray = Array.isArray(data);
console.log("add opt", data, files, isArray);
if (files) {
if (isArray) {
data.forEach((item) => {
files.push(item);
});
// files = [...files, ...data];
} else {
files.push(rest);
}
} else {
state.uploadFiles[_key] = isArray ? data : [data];
}
}
break;
case "reset":
{
state.uploadFiles[_key] = [];
}
break;
case "remove":
{
const { index } = rest;
const file = files[index];
if (file) {
files.splice(index, 1);
URL.revokeObjectURL(file.url);
}
}
break;
case "update":
{
const { index, name } = rest;
const file = files[index];
if (file) {
file.name = name;
}
}
break;
default:
break;
}
},
}, },
}); });
export const { export const {
@@ -38,5 +114,7 @@ export const {
updateInputMode, updateInputMode,
toggleMenuExpand, toggleMenuExpand,
updateFileListView, updateFileListView,
updateUploadFiles,
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="M12 3.25C13.4346 3.25 14.6126 4.34848 14.7388 5.75019L19 5.75C19.4142 5.75 19.75 6.08579 19.75 6.5C19.75 6.8797 19.4678 7.19349 19.1018 7.24315L19 7.25H18.417L17.1499 18.2292C17.0335 19.2384 16.179 20 15.1631 20H8.83688C7.821 20 6.9665 19.2384 6.85006 18.2292L5.582 7.25H5C4.6203 7.25 4.30651 6.96785 4.25685 6.60177L4.25 6.5C4.25 6.1203 4.53215 5.80651 4.89823 5.75685L5 5.75L9.26119 5.75019C9.38741 4.34848 10.5654 3.25 12 3.25ZM10.5 9.5C10.2545 9.5 10.0504 9.65477 10.0081 9.85886L10 9.9375V16.0625L10.0081 16.1411C10.0504 16.3452 10.2545 16.5 10.5 16.5C10.7455 16.5 10.9496 16.3452 10.9919 16.1411L11 16.0625V9.9375L10.9919 9.85886C10.9496 9.65477 10.7455 9.5 10.5 9.5ZM13.5 9.5C13.2545 9.5 13.0504 9.65477 13.0081 9.85886L13 9.9375V16.0625L13.0081 16.1411C13.0504 16.3452 13.2545 16.5 13.5 16.5C13.7455 16.5 13.9496 16.3452 13.9919 16.1411L14 16.0625V9.9375L13.9919 9.85886C13.9496 9.65477 13.7455 9.5 13.5 9.5ZM12 4.75C11.3952 4.75 10.8908 5.17947 10.775 5.75005H13.225C13.1092 5.17947 12.6048 4.75 12 4.75Z" fill="#D92D20"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -60,7 +60,7 @@ const renderPreview = (data) => {
export default function FileBox({ export default function FileBox({
preview = false, preview = false,
flex, flex,
file_type, file_type = "",
name, name,
size, size,
created_at, created_at,
+24
View File
@@ -0,0 +1,24 @@
// import React from 'react'
import { getDefaultSize } from "../../utils";
export default function Image({
thumbnail,
download,
content,
properties = {},
}) {
const { width = 0, height = 0 } = getDefaultSize(properties);
console.log("image props", properties, width, height);
return (
<img
className="img preview"
style={{
width: width ? `${width}px` : "",
height: height ? `${height}px` : "",
}}
data-meta={JSON.stringify(properties)}
data-origin={content}
data-download={download}
src={thumbnail || download || content}
/>
);
}
@@ -0,0 +1,21 @@
// import React from 'react'
import styled from "styled-components";
const Styled = styled.div`
background: #ecfdff;
border-radius: 4px;
height: 8px;
overflow: hidden;
.progress {
transition: all 0.25s ease;
height: 8px;
background: #088ab2;
border-radius: 4px;
}
`;
export default function Progress({ value, width = "100%" }) {
return (
<Styled style={{ width }}>
<div className="progress" style={{ width: `${value}%` }}></div>
</Styled>
);
}
+147
View File
@@ -0,0 +1,147 @@
import { useEffect, useState } from "react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { useSelector } from "react-redux";
import Styled from "./styled";
import Image from "./Image";
import useRemoveLocalMessage from "../../hook/useRemoveLocalMessage";
import useUploadFile from "../../hook/useUploadFile";
import useSendMessage from "../../hook/useSendMessage";
import Progress from "./Progress";
import { getFileIcon, formatBytes, isImage, getImageSize } from "../../utils";
import IconDownload from "../../../assets/icons/download.svg";
import IconClose from "../../../assets/icons/close.circle.svg";
dayjs.extend(relativeTime);
export default function FileMessage({
context = "",
to = null,
created_at,
from_uid = null,
content = "",
download = "",
thumbnail = "",
properties = { local_id: 0, name: "", size: 0, content_type: "" },
}) {
const [imageSize, setImageSize] = useState(null);
const [uploadinFile, setUploadinFile] = useState(false);
const removeLocalMessage = useRemoveLocalMessage({ context, id: to });
const {
sendMessage,
isSuccess: sendMessageSuccess,
isSending,
} = useSendMessage({
context,
from: from_uid,
to,
});
const {
stopUploading,
data,
uploadFile,
progress,
isSuccess: uploadSuccess,
} = useUploadFile();
const fromUser = useSelector((store) => store.contacts.byId[from_uid]);
const { size, name, content_type } = properties ?? {};
useEffect(() => {
const handleUpSend = async ({ url, name, type }) => {
try {
setUploadinFile(true);
if (type.startsWith("image")) {
const size = await getImageSize(url);
setImageSize(size);
}
let file = await fetch(url)
.then((r) => r.blob())
.then((blobFile) => new File([blobFile], name, { type }));
await uploadFile(file);
setUploadinFile(false);
} catch (error) {
setUploadinFile(false);
console.log("fetch local file error", error);
}
};
// local file
if (content && typeof content == "string" && content.startsWith("blob:")) {
handleUpSend({ url: content, name, type: content_type });
}
}, [content, name, content_type]);
useEffect(() => {
const props = properties ?? {};
const propsV2 = imageSize ? { ...props, ...imageSize } : props;
if (uploadSuccess) {
// 把已经上传的东西当做消息发出去
const { path } = data;
sendMessage({
ignoreLocal: true,
type: "file",
content: { path },
properties: propsV2,
});
}
}, [uploadSuccess, data, properties]);
useEffect(() => {
if (sendMessageSuccess) {
// 回收本地资源
// URL.revokeObjectURL(content);
}
}, [sendMessageSuccess, content]);
const handleCancel = () => {
stopUploading();
removeLocalMessage(properties.local_id);
};
if (!properties) return null;
const icon = getFileIcon(content_type, name);
if (!content || !fromUser || !name) return null;
console.log("file content", content, name, content_type, size);
if (isImage(content_type, size))
return (
<Image
properties={{ ...imageSize, ...properties }}
size={size}
content={content}
download={download}
thumbnail={thumbnail}
/>
);
const sending = uploadinFile || isSending;
return (
<Styled className={`file_message ${sending ? "sending" : ""}`}>
<div className="basic">
{icon}
<div className="info">
<span className="name">{name}</span>
<span className="details">
{/* <Progress value={30} width={"80%"} /> */}
{sending ? (
<Progress value={progress} width={"80%"} />
) : (
<>
<i className="size">{formatBytes(size)}</i>
<i className="time">{dayjs(created_at).fromNow()}</i>
<i className="from">
by <strong>{fromUser.name}</strong>
</i>
</>
)}
</span>
</div>
{/* <IconClose className="cancel" /> */}
{sending ? (
<IconClose className="cancel" onClick={handleCancel} />
) : (
<a
className="download"
download={name}
href={`${content}&download=true`}
>
<IconDownload />
</a>
)}
</div>
</Styled>
);
}
@@ -0,0 +1,61 @@
import styled from "styled-components";
const Styled = styled.div`
background: #f3f4f6;
border: 1px solid #d4d4d4;
box-sizing: border-box;
border-radius: 6px;
width: 370px;
height: 66px;
/* height: fit-content; */
&.sending {
/* opacity: 0.9; */
}
* {
user-select: text;
}
.basic {
padding: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
.icon {
width: 36px;
height: 48px;
}
.info {
display: flex;
flex-direction: column;
gap: 4px;
width: 100%;
overflow: hidden;
.name {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #1c1c1e;
white-space: nowrap;
text-overflow: ellipsis;
}
.details {
white-space: nowrap;
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #616161;
display: flex;
gap: 16px;
.from strong {
font-weight: bold;
}
}
}
.download {
white-space: nowrap;
}
.cancel {
cursor: pointer;
}
}
`;
export default Styled;
+4 -4
View File
@@ -67,7 +67,7 @@ export default function ImagePreviewModal({
// }); // });
// }; // };
if (!data) return null; if (!data) return null;
const { originUrl, name, type } = data; const { originUrl, downloadLink, name, type } = data;
return ( return (
<Modal> <Modal>
<StyledWrapper> <StyledWrapper>
@@ -84,9 +84,9 @@ export default function ImagePreviewModal({
className="origin" className="origin"
download={name} download={name}
type={type} type={type}
href={originUrl} href={downloadLink || originUrl}
// target="_blank" target="_blank"
// rel="noreferrer" rel="noreferrer"
> >
Download original Download original
</a> </a>
+3 -3
View File
@@ -116,9 +116,9 @@ const renderContent = (data) => {
break; break;
case ContentTypes.file: case ContentTypes.file:
{ {
const { file_type, name, size } = properties; const { content_type, name, size } = properties;
const icon = getFileIcon(file_type, name); const icon = getFileIcon(content_type, name);
if (isImage(file_type, size)) { if (isImage(content_type, size)) {
res = <img className="pic" src={thumbnail} />; res = <img className="pic" src={thumbnail} />;
} else { } else {
res = ( res = (
+4
View File
@@ -47,6 +47,7 @@ function Message({
sending = false, sending = false,
content, content,
thumbnail, thumbnail,
download,
content_type = "text/plain", content_type = "text/plain",
edited, edited,
properties, properties,
@@ -114,12 +115,15 @@ function Message({
/> />
) : ( ) : (
renderContent({ renderContent({
context,
to: contextId,
from_uid: fromUid, from_uid: fromUid,
created_at: time, created_at: time,
content_type, content_type,
properties, properties,
content, content,
thumbnail, thumbnail,
download,
edited, edited,
}) })
)} )}
+74 -39
View File
@@ -1,27 +1,28 @@
import React from "react"; import React, { useState, useEffect } from "react";
// import * as linkfy from "linkifyjs";
import Linkit from "react-linkify"; import Linkit from "react-linkify";
import dayjs from "dayjs"; import dayjs from "dayjs";
import BASE_URL, { ContentTypes } from "../../../app/config";
import Mention from "./Mention"; import Mention from "./Mention";
import MrakdownRender from "../MrakdownRender"; import MrakdownRender from "../MrakdownRender";
import { getDefaultSize, isImage } from "../../utils"; import FileMessage from "../FileMessage";
import FileBox from "../FileBox";
import URLPreview from "./URLPreview"; import URLPreview from "./URLPreview";
import reactStringReplace from "react-string-replace"; import reactStringReplace from "react-string-replace";
import { useGetArchiveMessageQuery } from "../../../app/services/message";
const renderContent = ({ const renderContent = ({
context,
to,
from_uid, from_uid,
created_at, created_at,
properties, properties,
content_type, content_type,
content, content,
download,
thumbnail, thumbnail,
edited = false, edited = false,
}) => { }) => {
let ctn = null; let ctn = null;
switch (content_type) { switch (content_type) {
case "text/plain": case ContentTypes.text:
ctn = ( ctn = (
<> <>
<Linkit <Linkit
@@ -65,43 +66,43 @@ const renderContent = ({
</> </>
); );
break; break;
case "text/markdown": case ContentTypes.markdown:
{ {
ctn = <MrakdownRender content={content} />; ctn = <MrakdownRender content={content} />;
} }
break; break;
case "rustchat/file": case ContentTypes.file:
{ {
const { size, name, file_type } = properties; // const { size, name, file_type } = properties;
if (isImage(file_type, size)) { ctn = (
const { width, height } = getDefaultSize(properties); <FileMessage
ctn = ( content_type={""}
<img properties={properties}
className="img preview" context={context}
style={{ width: `${width}px`, height: `${height}px` }} to={to}
data-meta={JSON.stringify({ download={download}
width, thumbnail={thumbnail}
height, from_uid={from_uid}
name, created_at={created_at}
file_type, content={content}
size, />
})} );
data-origin={content} }
src={thumbnail || content} break;
/> case ContentTypes.archive:
); {
} else { // const { size, name, file_type } = properties;
ctn = ( ctn = (
<FileBox <ForwardedMessage
from_uid={from_uid} properties={properties}
created_at={created_at} context={context}
content={content} to={to}
size={size} from_uid={from_uid}
name={name} created_at={created_at}
file_type={file_type} id={content}
/> thumbnail={thumbnail}
); />
} );
} }
break; break;
@@ -110,5 +111,39 @@ const renderContent = ({
} }
return ctn; return ctn;
}; };
const ForwardedMessage = ({ context, to, from_uid, id }) => {
const [forwards, setForwards] = useState(null);
const { data, isSuccess } = useGetArchiveMessageQuery(id);
useEffect(() => {
if (isSuccess && data) {
const msgs = data.messages.map(
({ content, file_id, thumbnail_id, content_type, properties }, idx) => {
const transformedContent =
content_type == ContentTypes.file
? `${BASE_URL}/resource/archive/attachment?file_path=${id}&attachment_id=${file_id}`
: content;
const thumbnail =
content_type == ContentTypes.file
? `${BASE_URL}/resource/archive/attachment?file_path=${id}&attachment_id=${thumbnail_id}`
: "";
return renderContent({
context,
to,
from_uid,
content: transformedContent,
content_type,
properties,
thumbnail,
});
}
);
setForwards(msgs);
}
}, [isSuccess, data]);
console.log("archive data", data);
if (!id) return null;
return forwards;
};
export default renderContent; export default renderContent;
+2 -2
View File
@@ -70,10 +70,10 @@ const StyledMsg = styled.div`
font-size: 10px; font-size: 10px;
} }
&.sending { &.sending {
opacity: 0.5; opacity: 0.9;
} }
.img { .img {
max-width: 400px; max-width: 480px;
cursor: pointer; cursor: pointer;
} }
> .link { > .link {
+29 -41
View File
@@ -1,5 +1,6 @@
import { useRef, useState, useCallback } from "react"; import { useRef, useEffect, useState, useCallback } from "react";
import { useKey } from "rooks"; import { useKey } from "rooks";
import { useDispatch } from "react-redux";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { Editor, Transforms } from "slate"; import { Editor, Transforms } from "slate";
import { import {
@@ -9,7 +10,7 @@ import {
createTrailingBlockPlugin, createTrailingBlockPlugin,
createNodeIdPlugin, createNodeIdPlugin,
createParagraphPlugin, createParagraphPlugin,
createImagePlugin, // createImagePlugin,
createSoftBreakPlugin, createSoftBreakPlugin,
createComboboxPlugin, createComboboxPlugin,
createMentionPlugin, createMentionPlugin,
@@ -22,18 +23,14 @@ import {
ELEMENT_PARAGRAPH, ELEMENT_PARAGRAPH,
getPlateEditorRef, getPlateEditorRef,
// usePlateEditorRef, // usePlateEditorRef,
ELEMENT_IMAGE, // ELEMENT_IMAGE,
MentionCombobox, MentionCombobox,
// usePlateStore
} from "@udecode/plate"; } from "@udecode/plate";
import { ReactEditor } from "slate-react"; import { ReactEditor } from "slate-react";
import Styled from "./styled"; import Styled from "./styled";
// import StyledCombobox from "./StyledCombobox";
import ImageElement from "./ImageElement";
import { CONFIG } from "./config"; import { CONFIG } from "./config";
import Contact from "../Contact"; import Contact from "../Contact";
import useUploadFile from "../../hook/useUploadFile"; import { updateUploadFiles } from "../../../app/slices/ui";
// import Mention from "./Mention";
export const TEXT_EDITOR_PREFIX = "rustchat_text_editor"; export const TEXT_EDITOR_PREFIX = "rustchat_text_editor";
export const setEditorFocus = (edtr) => { export const setEditorFocus = (edtr) => {
console.log("focus", edtr); console.log("focus", edtr);
@@ -51,7 +48,7 @@ export const clearEditorAndFocus = (edtr) => {
}; };
let components = createPlateUI({ let components = createPlateUI({
[ELEMENT_IMAGE]: ImageElement, // [ELEMENT_IMAGE]: ImageElement,
// customize your components by plugin key // customize your components by plugin key
}); });
const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }]; const initialValue = [{ type: ELEMENT_PARAGRAPH, children: [{ text: "" }] }];
@@ -61,20 +58,40 @@ const Plugins = ({
sendMessages, sendMessages,
members = [], members = [],
}) => { }) => {
const dispatch = useDispatch();
const enableMentions = members.length > 0; const enableMentions = members.length > 0;
const { uploadFile } = useUploadFile();
const filesRef = useRef([]); const filesRef = useRef([]);
// const plateEditor = usePlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
const contactData = useSelector((store) => store.contacts.byId); const contactData = useSelector((store) => store.contacts.byId);
const [msgs, setMsgs] = useState([]); const [msgs, setMsgs] = useState([]);
const [cmdKey, setCmdKey] = useState(false); const [cmdKey, setCmdKey] = useState(false);
const editableRef = useRef(null); const editableRef = useRef(null);
// const editor = useEditorRef();
const initialProps = { const initialProps = {
...CONFIG.editableProps, ...CONFIG.editableProps,
className: "box", className: "box",
placeholder, placeholder,
}; };
useEffect(() => {
const handlePasteEvent = (evt) => {
const files = [...evt.clipboardData.files];
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
console.log("paste event name", name);
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
const [context, to] = id.split("_");
console.log("paste event", context, to, files, evt);
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
};
window.addEventListener("paste", handlePasteEvent);
return () => {
window.removeEventListener("paste", handlePasteEvent);
};
// window.addEventListener("paste")
}, []);
useKey( useKey(
"Enter", "Enter",
@@ -108,19 +125,6 @@ const Plugins = ({
); );
const pluginArr = [ const pluginArr = [
createParagraphPlugin(), createParagraphPlugin(),
createImagePlugin({
options: {
uploadImage: async (dataUrl) => {
console.log("upload image", dataUrl);
const resp = await fetch(dataUrl);
const blob = await resp.blob();
const { thumbnail, ...rest } = await uploadFile(blob);
const { name, file_type, size, path, hash } = rest;
filesRef.current.push({ name, file_type, size, path, hash });
return thumbnail;
},
},
}),
createNodeIdPlugin(), createNodeIdPlugin(),
createSoftBreakPlugin(CONFIG.softBreak), createSoftBreakPlugin(CONFIG.softBreak),
createTrailingBlockPlugin(CONFIG.trailingBlock), createTrailingBlockPlugin(CONFIG.trailingBlock),
@@ -153,18 +157,6 @@ const Plugins = ({
const handleChange = useCallback( const handleChange = useCallback(
async (val) => { async (val) => {
console.log("tmps changed", val); console.log("tmps changed", val);
// const wtf = getMentionOnSelectItem();
// const plateEditor = getPlateEditorRef(`${TEXT_EDITOR_PREFIX}_${id}`);
// const currentMentionInput = findMentionInput(plateEditor);
// const items = comboboxSelectors.filteredItems();
// console.log(
// "mention check",
// items,
// isSelectionInMentionInput(plateEditor)
// );
// if (items?.length == 0 && isSelectionInMentionInput(plateEditor)) {
// removeMentionInput(plateEditor, currentMentionInput[1]);
// }
const tmps = []; const tmps = [];
const getMixedText = (children) => { const getMixedText = (children) => {
const mentions = []; const mentions = [];
@@ -270,7 +262,3 @@ const Plugins = ({
); );
}; };
export default Plugins; export default Plugins;
// export default memo(Plugins, (prevs, nexts) => {
// console.log("placeholder", prevs.placeholder, nexts.placeholder);
// return prevs.placeholder == nexts.placeholder;
// });
+4 -8
View File
@@ -8,6 +8,7 @@ import { getFileIcon, isImage } from "../../utils";
import { removeReplyingMessage } from "../../../app/slices/message"; import { removeReplyingMessage } from "../../../app/slices/message";
import styled from "styled-components"; import styled from "styled-components";
const Styled = styled.div` const Styled = styled.div`
background-color: #f3f4f6;
z-index: 999; z-index: 999;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -16,11 +17,6 @@ const Styled = styled.div`
gap: 16px; gap: 16px;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-top-right-radius: 8px; border-top-right-radius: 8px;
background-color: #f3f4f6;
position: absolute;
left: 0;
top: 0;
transform: translateY(-100%);
width: 100%; width: 100%;
padding: 12px 16px; padding: 12px 16px;
white-space: nowrap; white-space: nowrap;
@@ -99,11 +95,11 @@ const renderContent = (data) => {
break; break;
case ContentTypes.file: case ContentTypes.file:
{ {
const { file_type, name, size } = properties; const { content_type, name, size } = properties;
if (isImage(file_type, size)) { if (isImage(content_type, size)) {
res = <img className="pic" src={pictureIcon} />; res = <img className="pic" src={pictureIcon} />;
} else { } else {
const icon = getFileIcon(file_type, name); const icon = getFileIcon(content_type, name);
res = ( res = (
<> <>
{icon} {icon}
+35 -41
View File
@@ -1,8 +1,8 @@
import { useState, useRef } from "react"; import { useRef } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useDispatch } from "react-redux";
import { updateUploadFiles } from "../../../app/slices/ui";
import Tooltip from "../../component/Tooltip"; import Tooltip from "../../component/Tooltip";
import UploadModal from "../../component/UploadModal";
import AddIcon from "../../../assets/icons/add.solid.svg"; import AddIcon from "../../../assets/icons/add.solid.svg";
import MarkdownIcon from "../../../assets/icons/markdown.svg"; import MarkdownIcon from "../../../assets/icons/markdown.svg";
const Styled = styled.div` const Styled = styled.div`
@@ -40,51 +40,45 @@ const Styled = styled.div`
} }
`; `;
export default function Toolbar({ toggleMode, mode, to, context }) { export default function Toolbar({ toggleMode, mode, to, context }) {
const [files, setFiles] = useState([]); const dispatch = useDispatch();
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const resetFiles = () => {
setFiles([]);
fileInputRef.current.value = "";
};
const handleUpload = (evt) => { const handleUpload = (evt) => {
const files = [...evt.target.files]; const files = [...evt.target.files];
console.log("files", files); console.log("files", files);
setFiles([...evt.target.files]); const filesData = files.map((file) => {
const { size, type, name } = file;
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
fileInputRef.current.value = null;
fileInputRef.current.value = "";
// setFiles([...evt.target.files]);
}; };
return ( return (
<> <Styled className={mode}>
{files.length !== 0 && ( <div className="md" onClick={toggleMode}>
<UploadModal <Tooltip placement="top" tip="Markdown">
context={context} <MarkdownIcon className={mode} />
files={files}
sendTo={to}
closeModal={resetFiles}
/>
)}
<Styled className={mode}>
<div className="md" onClick={toggleMode}>
<Tooltip placement="top" tip="Markdown">
<MarkdownIcon className={mode} />
</Tooltip>
</div>
<Tooltip placement="top" tip="Upload">
<div className="add">
<AddIcon />
<label htmlFor="file">
<input
size={24}
ref={fileInputRef}
multiple={false}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</label>
</div>
</Tooltip> </Tooltip>
</Styled> </div>
</> <Tooltip placement="top" tip="Upload">
<div className="add">
<AddIcon />
<label htmlFor="file">
<input
size={24}
ref={fileInputRef}
multiple={true}
onChange={handleUpload}
type="file"
name="file"
id="file"
/>
</label>
</div>
</Tooltip>
</Styled>
); );
} }
@@ -0,0 +1,78 @@
import { useState } from "react";
import styled from "styled-components";
import Modal from "../../Modal";
import Button from "../../styled/Button";
import Input from "../../styled/Input";
import CloseIcon from "../../../../assets/icons/close.svg";
const StyledWrapper = styled.div`
background: #fff;
display: flex;
flex-direction: column;
filter: drop-shadow(0px 25px 50px rgba(31, 41, 55, 0.25));
border-radius: 12px;
padding: 16px;
width: 406px;
.title {
display: flex;
align-items: center;
justify-content: space-between;
font-style: normal;
font-weight: 600;
font-size: 18px;
line-height: 28px;
color: #344054;
width: 100%;
.close {
cursor: pointer;
}
}
.input {
padding: 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
label {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #475467;
}
}
.btns {
margin-top: 32px;
gap: 16px;
display: flex;
width: 100%;
justify-content: flex-end;
}
`;
export default function EditFileDetails({ name, closeModal, updateName }) {
const [fileName, setFileName] = useState(name);
const handleNameChange = (evt) => {
setFileName(evt.target.value);
};
const handleUpdate = () => {
updateName(fileName);
closeModal();
};
return (
<Modal>
<StyledWrapper>
<h4 className="title">
File Details <CloseIcon className="close" onClick={closeModal} />
</h4>
<div className="input">
<label htmlFor="name">Name</label>
<Input id="name" value={fileName} onChange={handleNameChange} />
</div>
<div className="btns">
<Button className="ghost cancel" onClick={closeModal}>
Cancel
</Button>
<Button onClick={handleUpdate}>Save Changes</Button>
</div>
</StyledWrapper>
</Modal>
);
}
@@ -0,0 +1,83 @@
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import Styled from "./styled";
import EditFileDetailsModal from "./EditFileDetails";
import { updateUploadFiles } from "../../../../app/slices/ui";
import { getFileIcon, formatBytes } from "../../../utils";
import EditIcon from "../../../../assets/icons/edit.svg";
import DeleteIcon from "../../../../assets/icons/delete.svg";
export default function UploadFileList({ context = "", id = null }) {
const [editInfo, setEditInfo] = useState(null);
const dispatch = useDispatch();
const files = useSelector(
(store) => store.ui.uploadFiles[`${context}_${id}`]
);
const toggleModalVisible = (info) => {
setEditInfo((prev) => (prev ? null : info));
};
const handleOpenEditModal = (idx) => {
const info = files[idx];
if (!info) return;
toggleModalVisible({ ...info, index: idx });
};
const handleRemove = (idx) => {
dispatch(
updateUploadFiles({ context, id, operation: "remove", index: idx })
);
};
const updateFileName = (name) => {
if (!name) return;
const { index } = editInfo;
dispatch(
updateUploadFiles({ context, id, operation: "update", index, name })
);
};
if (!context || !id || !files || files.length == 0) return null;
console.log("upload files", files);
return (
<>
{editInfo && (
<EditFileDetailsModal
name={editInfo.name}
updateName={updateFileName}
closeModal={toggleModalVisible}
/>
)}
<Styled>
{files.map(({ name, url, size, type }, idx) => {
return (
<li className="file" key={url}>
<div className="preview">
{type.startsWith("image") ? (
<img src={url} alt="image" />
) : (
getFileIcon(type, name)
)}
</div>
<h4 className="name">{name}</h4>
<span className="size">{formatBytes(size)}</span>
<ul className="opts">
<li
className="opt edit"
onClick={handleOpenEditModal.bind(null, idx)}
>
<EditIcon />
</li>
<li
className="opt delete"
data-index={idx}
onClick={handleRemove.bind(null, idx)}
>
<DeleteIcon />
</li>
</ul>
</li>
);
})}
</Styled>
</>
);
}
@@ -0,0 +1,71 @@
import styled from "styled-components";
const Styled = styled.ul`
width: 100%;
overflow: auto;
display: flex;
justify-content: flex-start;
gap: 24px;
width: 100%;
padding: 24px 16px 16px 16px;
background: #e5e7eb;
box-shadow: 0px 1px 0px rgba(0, 0, 0, 0.05);
border-radius: 8px 8px 0px 0px;
.file {
position: relative;
display: flex;
flex-direction: column;
background: #fcfcfd;
border-radius: 4px;
padding: 8px;
.preview {
display: flex;
justify-content: center;
align-items: center;
width: 160px;
height: 160px;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.name {
width: 160px;
margin: 16px 0 2px 0;
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #1c1c1e;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.size {
font-weight: 400;
font-size: 12px;
line-height: 18px;
color: #616161;
}
.opts {
visibility: hidden;
background: inherit;
border: 1px solid rgba(0, 0, 0, 0.08);
box-sizing: border-box;
border-radius: 6px;
display: flex;
align-items: center;
position: absolute;
right: -20px;
top: -10px;
.opt {
padding: 4px;
cursor: pointer;
}
}
&:hover .opts {
visibility: visible;
}
}
`;
export default Styled;
+87 -72
View File
@@ -3,12 +3,15 @@ import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
// import { useKey } from "rooks"; // import { useKey } from "rooks";
import { getPlateEditorRef } from "@udecode/plate"; import { getPlateEditorRef } from "@udecode/plate";
import { updateInputMode } from "../../../app/slices/ui";
import useSendMessage from "../../hook/useSendMessage";
import useAddLocalFileMessage from "../../hook/useAddLocalFileMessage";
import { removeReplyingMessage } from "../../../app/slices/message"; import { removeReplyingMessage } from "../../../app/slices/message";
import { useSendChannelMsgMutation } from "../../../app/services/channel"; import { updateInputMode, updateUploadFiles } from "../../../app/slices/ui";
import { useSendMsgMutation } from "../../../app/services/contact"; import { ContentTypes } from "../../../app/config";
import { useReplyMessageMutation } from "../../../app/services/message";
import StyledSend from "./styled"; import StyledSend from "./styled";
import UploadFileList from "./UploadFileList";
import Replying from "./Replying"; import Replying from "./Replying";
import Toolbar from "./Toolbar"; import Toolbar from "./Toolbar";
import EmojiPicker from "./EmojiPicker"; import EmojiPicker from "./EmojiPicker";
@@ -31,21 +34,20 @@ function Send({
id = "", id = "",
}) { }) {
const [markdownEditor, setMarkdownEditor] = useState(null); const [markdownEditor, setMarkdownEditor] = useState(null);
const [replyMessage] = useReplyMessageMutation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const addLocalFileMesage = useAddLocalFileMessage({ context, to: id });
// 谁发的 // 谁发的
const { from_uid, replying_mid = null, mode } = useSelector((store) => { const { from_uid, replying_mid = null, mode, uploadFiles } = useSelector(
return { (store) => {
mode: store.ui.inputMode, return {
from_uid: store.authData.uid, mode: store.ui.inputMode,
replying_mid: store.message.replying[id], from_uid: store.authData.uid,
}; replying_mid: store.message.replying[id],
}); uploadFiles: store.ui.uploadFiles[`${context}_${id}`],
};
const [sendMsg] = useSendMsgMutation(); }
const [sendChannelMsg] = useSendChannelMsgMutation(); );
const sendMessage = context == "channel" ? sendChannelMsg : sendMsg; const { sendMessage } = useSendMessage({ context, from: from_uid, to: id });
// const sendingMessage = userSending || channelSending;
useEffect(() => { useEffect(() => {
if (replying_mid) { if (replying_mid) {
@@ -73,80 +75,93 @@ function Send({
} }
}; };
const handleSendMessage = async (msgs = []) => { const handleSendMessage = async (msgs = []) => {
if (!msgs || msgs.length == 0 || !id) return; if (!id) return;
for await (const msg of msgs) { if (msgs && msgs.length) {
console.log("send msg", msg); // send text msgs
const { type: content_type, content, properties = {} } = msg; for await (const msg of msgs) {
if (replying_mid) { console.log("send msg", msg);
console.log("replying", replying_mid); const { type: content_type, content, properties = {} } = msg;
await replyMessage({ properties.local_id = properties.local_id ?? new Date().getTime();
id,
reply_mid: replying_mid,
type: content_type,
content,
context,
from_uid,
});
dispatch(removeReplyingMessage(id));
} else {
await sendMessage({ await sendMessage({
id, id,
reply_mid: replying_mid,
type: content_type, type: content_type,
content, content,
from_uid, from_uid,
properties, properties,
}); });
if (replying_mid) {
dispatch(removeReplyingMessage(id));
}
} }
} }
// send files
if (uploadFiles && uploadFiles.length !== 0) {
uploadFiles.forEach((fileInfo) => {
const ts = new Date().getTime();
const { url, name, size, type } = fileInfo;
const tmpMsg = {
mid: ts,
content: url,
content_type: ContentTypes.file,
created_at: ts,
properties: {
content_type: type,
name,
size,
local_id: ts,
},
from_uid,
sending: true,
};
addLocalFileMesage(tmpMsg);
});
dispatch(updateUploadFiles({ context, id, operation: "reset" }));
}
}; };
const sendMarkdown = async (content) => { const sendMarkdown = async (content) => {
if (replying_mid) { sendMessage({
console.log("replying", replying_mid); id,
await replyMessage({ reply_mid: replying_mid,
id, type: "markdown",
reply_mid: replying_mid, content,
type: "markdown", from_uid,
content, properties: { local_id: new Date().getTime() },
context, });
from_uid,
});
dispatch(removeReplyingMessage(id));
} else {
sendMessage({
id,
type: "markdown",
content,
from_uid,
properties: { local_id: new Date().getTime() },
});
}
}; };
const toggleMode = () => { const toggleMode = () => {
dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text)); dispatch(updateInputMode(mode == Modes.text ? Modes.markdown : Modes.text));
}; };
const placeholder = `Send to ${Types[context]}${name} `; const placeholder = `Send to ${Types[context]}${name} `;
return ( return (
<StyledSend <StyledSend className={`send ${replying_mid ? "reply" : ""} ${context}`}>
className={`send ${mode} ${replying_mid ? "reply" : ""} ${context}`}
>
{replying_mid && <Replying mid={replying_mid} id={id} />} {replying_mid && <Replying mid={replying_mid} id={id} />}
<EmojiPicker selectEmoji={insertEmoji} /> <UploadFileList context={context} id={id} />
{mode == Modes.text && (
<MixedInput <div className={`send_box ${mode}`}>
members={members} <EmojiPicker selectEmoji={insertEmoji} />
id={`${context}_${id}`} {mode == Modes.text && (
placeholder={placeholder} <MixedInput
sendMessages={handleSendMessage} members={members}
id={`${context}_${id}`}
placeholder={placeholder}
sendMessages={handleSendMessage}
/>
)}
<Toolbar
context={context}
to={id}
mode={mode}
toggleMode={toggleMode}
/> />
)} {mode == Modes.markdown && (
<Toolbar context={context} to={id} mode={mode} toggleMode={toggleMode} /> <MarkdownEditor
{mode == Modes.markdown && ( placeholder={placeholder}
<MarkdownEditor setEditorInstance={setMarkdownEditor}
placeholder={placeholder} sendMarkdown={sendMarkdown}
setEditorInstance={setMarkdownEditor} />
sendMarkdown={sendMarkdown} )}
/> </div>
)}
</StyledSend> </StyledSend>
); );
} }
+21 -19
View File
@@ -6,26 +6,28 @@ const StyledSend = styled.div`
border-radius: var(--br); border-radius: var(--br);
width: 100%; width: 100%;
width: -webkit-fill-available; width: -webkit-fill-available;
display: flex; .send_box {
justify-content: space-between; display: flex;
align-items: flex-start; justify-content: space-between;
gap: 15px; align-items: flex-start;
padding: 14px 18px; gap: 15px;
&.markdown { padding: 14px 18px;
display: grid; &.markdown {
grid-template-columns: 1fr 1fr; display: grid;
grid-template-rows: auto auto; grid-template-columns: 1fr 1fr;
gap: 0; grid-template-rows: auto auto;
.input { gap: 0;
grid-column: span 2; .input {
grid-column: span 2;
}
}
&.reply {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.input {
width: 100%;
} }
}
&.reply {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.input {
width: 100%;
} }
`; `;
+15
View File
@@ -0,0 +1,15 @@
// import React from 'react'
import { useDispatch } from "react-redux";
import { addMessage } from "../../app/slices/message";
import { addChannelMsg } from "../../app/slices/message.channel";
import { addUserMsg } from "../../app/slices/message.user";
export default function useAddLocalFileMessage({ context, to }) {
const dispatch = useDispatch();
const addContextMessage = context == "channel" ? addChannelMsg : addUserMsg;
const addLocalFileMesage = (data) => {
dispatch(addMessage(data));
dispatch(addContextMessage({ id: to, mid: data.mid }));
};
return addLocalFileMesage;
}
+15
View File
@@ -0,0 +1,15 @@
// import second from 'first'
import { useDispatch } from "react-redux";
import { removeMessage } from "../../app/slices/message";
import { removeChannelMsg } from "../../app/slices/message.channel";
import { removeUserMsg } from "../../app/slices/message.user";
export default function useRemoveLocalMessage({ context = "user", id = 0 }) {
const dispatch = useDispatch();
const removeContextMessage =
context == "channel" ? removeChannelMsg : removeUserMsg;
const removeLocalMessage = (mid) => {
dispatch(removeContextMessage({ id, mid }));
dispatch(removeMessage(mid));
};
return removeLocalMessage;
}
+70 -16
View File
@@ -1,11 +1,20 @@
// import second from 'first' // import second from 'first'
import { removeReplyingMessage } from "../../app/slices/message";
import { useSendChannelMsgMutation } from "../../app/services/channel"; import { useSendChannelMsgMutation } from "../../app/services/channel";
import { useSendMsgMutation } from "../../app/services/contact"; import { useSendMsgMutation } from "../../app/services/contact";
export default function useSendMessage({ import { useReplyMessageMutation } from "../../app/services/message";
context = "user", export default function useSendMessage(
from = null, props = {
to = null, context: "user",
}) { from: null,
to: null,
}
) {
const { context = "user", from = null, to = null } = props;
const [
replyMessage,
{ isError: replyErr, isLoading: replying, isSuccess: replySuccess },
] = useReplyMessageMutation();
const [ const [
sendChannelMsg, sendChannelMsg,
{ {
@@ -19,19 +28,64 @@ export default function useSendMessage({
{ isLoading: userSending, isSuccess: userSuccess, isError: userError }, { isLoading: userSending, isSuccess: userSuccess, isError: userError },
] = useSendMsgMutation(); ] = useSendMsgMutation();
const sendFn = context == "user" ? sendUserMsg : sendChannelMsg; const sendFn = context == "user" ? sendUserMsg : sendChannelMsg;
const sendMessage = ({ type = "text", content, properties = {} }) => { const sendMessages = async ({
sendFn({ type = "text",
id: to, content,
content, users = [],
properties: { ...properties, local_id: new Date().getTime() }, channels = [],
type, }) => {
from_uid: from, if (users.length) {
}); for await (const uid of users) {
await sendUserMsg({
type,
id: uid,
content,
});
}
}
if (channels.length) {
for await (const cid of channels) {
await sendChannelMsg({
type,
id: cid,
content,
});
}
}
};
const sendMessage = async ({
type = "text",
content,
properties = {},
reply_mid = null,
...rest
}) => {
if (reply_mid) {
await replyMessage({
id: to,
reply_mid,
type,
content,
context,
from_uid: from,
});
removeReplyingMessage(to);
} else {
await sendFn({
id: to,
content,
properties: { ...properties },
type,
from_uid: from,
...rest,
});
}
}; };
return { return {
sendMessages,
sendMessage, sendMessage,
isError: channelError || userError, isError: channelError || userError || replyErr,
isSending: userSending || channelSending, isSending: userSending || channelSending || replying,
isSuccess: channelSuccess || userSuccess, isSuccess: channelSuccess || userSuccess || replySuccess,
}; };
} }
+13 -5
View File
@@ -8,7 +8,8 @@ import {
export default function useUploadFile() { export default function useUploadFile() {
const [data, setData] = useState(null); const [data, setData] = useState(null);
const [uploadingFile, setUploadingFile] = useState(false); // const [uploadingFile, setUploadingFile] = useState(false);
const canneledRef = useRef(false);
const sliceUploadedCountRef = useRef(0); const sliceUploadedCountRef = useRef(0);
const totalSliceCountRef = useRef(1); const totalSliceCountRef = useRef(1);
const [ const [
@@ -20,7 +21,7 @@ export default function useUploadFile() {
{ isLoading: isUploading, isSuccess: isUploaded, isError: uploadFileError }, { isLoading: isUploading, isSuccess: isUploaded, isError: uploadFileError },
] = useUploadFileMutation(); ] = useUploadFileMutation();
const uploadChunk = async (data) => { const uploadChunk = (data) => {
const { file_id, chunk, is_last } = data; const { file_id, chunk, is_last } = data;
const formData = new FormData(); const formData = new FormData();
formData.append("file_id", file_id); formData.append("file_id", file_id);
@@ -44,9 +45,10 @@ export default function useUploadFile() {
console.log("file id", file_id); console.log("file id", file_id);
let uploadResult = null; let uploadResult = null;
canneledRef.current = false;
totalSliceCountRef.current = 1; totalSliceCountRef.current = 1;
sliceUploadedCountRef.current = 0; sliceUploadedCountRef.current = 0;
setUploadingFile(true); // setUploadingFile(true);
// 2MB // 2MB
if (file_size <= 1000 * 1000 * 2) { if (file_size <= 1000 * 1000 * 2) {
// 一次性上传文件 // 一次性上传文件
@@ -60,6 +62,8 @@ export default function useUploadFile() {
// const chunk=file.slice(block_size * index, block_size * (index + 1)); // const chunk=file.slice(block_size * index, block_size * (index + 1));
for await (const [idx] of _arr.entries()) { for await (const [idx] of _arr.entries()) {
// 退出循环
if (canneledRef.current) break;
try { try {
const chunk = file.slice( const chunk = file.slice(
FILE_SLICE_SIZE * idx, FILE_SLICE_SIZE * idx,
@@ -81,7 +85,7 @@ export default function useUploadFile() {
} }
console.log("wtfff", uploadResult); console.log("wtfff", uploadResult);
} }
setUploadingFile(false); // setUploadingFile(false);
const { const {
data: { path, size, hash }, data: { path, size, hash },
} = uploadResult; } = uploadResult;
@@ -101,9 +105,13 @@ export default function useUploadFile() {
setData(res); setData(res);
return res; return res;
}; };
const stopUploading = () => {
canneledRef.current = true;
};
return { return {
stopUploading,
data, data,
isUploading: uploadingFile, isUploading: isPreparing || isUploading,
progress: Number( progress: Number(
(sliceUploadedCountRef.current / totalSliceCountRef.current) * 100 (sliceUploadedCountRef.current / totalSliceCountRef.current) * 100
).toFixed(2), ).toFixed(2),
+21 -2
View File
@@ -38,9 +38,13 @@ export const getNonNullValues = (obj, whiteList = ["log_id"]) => {
}); });
return tmp; return tmp;
}; };
export function getDefaultSize(size = null, min = 240) { export function getDefaultSize(size = null, min = 480) {
if (!size) return { width: 0, height: 0 }; if (!size) return { width: 0, height: 0 };
const { width: oWidth, height: oHeight } = size; const { width: oWidth, height: oHeight } = size;
if (oWidth == oHeight) {
const tmp = min > oWidth ? oWidth : min;
return { width: tmp, height: tmp };
}
const isVertical = oWidth > oHeight ? false : true; const isVertical = oWidth > oHeight ? false : true;
let dWidth = 0; let dWidth = 0;
let dHeight = 0; let dHeight = 0;
@@ -49,7 +53,6 @@ export function getDefaultSize(size = null, min = 240) {
dWidth = (oWidth / oHeight) * dHeight; dWidth = (oWidth / oHeight) * dHeight;
} else { } else {
dWidth = oWidth >= min ? min : oWidth; dWidth = oWidth >= min ? min : oWidth;
// dHeight = oHeight >= min ? min : oHeight;
dHeight = (oHeight / oWidth) * dWidth; dHeight = (oHeight / oWidth) * dWidth;
} }
return { width: dWidth, height: dHeight }; return { width: dWidth, height: dHeight };
@@ -65,6 +68,22 @@ export function formatBytes(bytes, decimals = 2) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
} }
export const getImageSize = (url) => {
const size = { width: 0, height: 0 };
if (!url) return size;
return new Promise((resolve) => {
const img = new Image();
img.src = url;
img.onload = () => {
size.width = img.width;
size.height = img.height;
resolve(size);
};
img.onerror = () => {
resolve(size);
};
});
};
export const getInitials = (name) => { export const getInitials = (name) => {
const arr = name.split(" ").filter((n) => !!n); const arr = name.split(" ").filter((n) => !!n);
return arr return arr
+34 -35
View File
@@ -1,10 +1,11 @@
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 { NativeTypes } from "react-dnd-html5-backend"; import { NativeTypes } from "react-dnd-html5-backend";
import styled from "styled-components"; import styled from "styled-components";
import { updateUploadFiles } from "../../app/slices/ui";
import ImagePreviewModal from "../../common/component/ImagePreviewModal"; import ImagePreviewModal from "../../common/component/ImagePreviewModal";
import UploadModal from "../../common/component/UploadModal";
const StyledWrapper = styled.article` const StyledWrapper = styled.article`
position: relative; position: relative;
width: 100%; width: 100%;
@@ -110,34 +111,39 @@ export default function Layout({
header, header,
aside = null, aside = null,
contacts = null, contacts = null,
dropFiles = [], // dropFiles = [],
context = "channel", context = "channel",
to = null, to = null,
}) { }) {
const messagesContainer = useRef(null); const dispatch = useDispatch();
const [files, setFiles] = useState([]);
const [previewImage, setPreviewImage] = useState(null);
const [{ isActive }, drop] = useDrop(() => ({
accept: [NativeTypes.FILE],
drop({ files }) {
console.log("drop files", files);
if (files.length) {
setFiles((prevs) => [...prevs, ...files]);
}
},
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
}));
useEffect(() => {
if (dropFiles.length) {
setFiles((prevs) => [...prevs, ...dropFiles]);
}
}, [dropFiles]);
const resetFiles = () => { const messagesContainer = useRef(null);
setFiles([]); const [previewImage, setPreviewImage] = useState(null);
}; const [{ isActive }, drop] = useDrop(
() => ({
accept: [NativeTypes.FILE],
drop({ files }) {
console.log("drop files", files, context, to);
if (files.length) {
const filesData = files.map((file) => {
const { size, type, name } = file;
const url = URL.createObjectURL(file);
return { size, type, name, url };
});
dispatch(updateUploadFiles({ context, id: to, data: filesData }));
}
},
collect: (monitor) => ({
isActive: monitor.canDrop() && monitor.isOver(),
}),
}),
[context, to]
);
// useEffect(() => {
// if (dropFiles?.length) {
// setFiles((prevs) => [...prevs, ...dropFiles]);
// }
// }, [dropFiles]);
const closePreviewModal = () => { const closePreviewModal = () => {
setPreviewImage(null); setPreviewImage(null);
@@ -156,8 +162,9 @@ export default function Layout({
target.classList.contains("preview") target.classList.contains("preview")
) { ) {
const originUrl = target.dataset.origin || target.src; const originUrl = target.dataset.origin || target.src;
const downloadLink = target.dataset.download || target.src;
const meta = JSON.parse(target.dataset.meta || "{}"); const meta = JSON.parse(target.dataset.meta || "{}");
setPreviewImage({ originUrl, ...meta }); setPreviewImage({ originUrl, downloadLink, ...meta });
} }
}, },
true true
@@ -196,14 +203,6 @@ export default function Layout({
</div> </div>
</div> </div>
</StyledWrapper> </StyledWrapper>
{files.length !== 0 && (
<UploadModal
context={context}
files={files}
sendTo={to}
closeModal={resetFiles}
/>
)}
</> </>
); );
} }
+2 -1
View File
@@ -66,7 +66,8 @@ export const renderPreviewMessage = (message = null) => {
break; break;
case ContentTypes.file: case ContentTypes.file:
{ {
if (isImage(properties.file_type, properties.size)) { const props = properties ?? {};
if (isImage(props.content_type, props.size)) {
res = `[image]`; res = `[image]`;
} else { } else {
res = `[file]`; res = `[file]`;