diff --git a/src/app/config.js b/src/app/config.js
index 7a352b3e..82dcfb9c 100644
--- a/src/app/config.js
+++ b/src/app/config.js
@@ -8,12 +8,14 @@ export const ContentTypes = {
image: "image/png",
imageJPG: "image/jpeg",
file: "rustchat/file",
+ formData: "multipart/form-data",
json: "application/json",
};
export const googleClientID =
"418687074928-naojba82n9ktf0rkvnqoor4nhr54ql1b.apps.googleusercontent.com";
// "840319286941-6ds7lbvk55eq8mjortf68cb2ll65lprt.apps.googleusercontent.com";
export const tokenHeader = "X-API-Key";
+export const FILE_SLICE_SIZE = 1000 * 200 * 8; //200kb
export const KEY_TOKEN = "RUSTCHAT_TOKEN";
export const KEY_EXPIRE = "RUSTCHAT_TOKEN_EXPIRE";
export const KEY_REFRESH_TOKEN = "RUSTCHAT_REFRESH_TOKEN";
diff --git a/src/app/slices/message.js b/src/app/slices/message.js
index 1cdb63be..a08a6923 100644
--- a/src/app/slices/message.js
+++ b/src/app/slices/message.js
@@ -1,7 +1,8 @@
import { createSlice } from "@reduxjs/toolkit";
-import BASE_URL from "../config";
+import BASE_URL, { ContentTypes } from "../config";
const initialState = {
replying: {},
+ fileMessages: [],
};
const messageSlice = createSlice({
name: "message",
@@ -11,7 +12,7 @@ const messageSlice = createSlice({
return initialState;
},
fullfillMessage(state, action) {
- return action.payload;
+ return Object.assign({ ...initialState }, action.payload);
},
updateMessage(state, action) {
const { mid, ...rest } = action.payload;
@@ -33,6 +34,17 @@ const messageSlice = createSlice({
// 如果是正发送,并且已存在,则不覆盖
if (sending && state[mid]) return;
const isImage = content_type.startsWith("image");
+ const isFile = content_type == ContentTypes.file;
+ // file
+ if (!sending && isFile) {
+ data.file_path = content;
+ data.content = `${BASE_URL}/resource/file?file_path=${encodeURIComponent(
+ data.file_path
+ )}`;
+ // 加入file message 列表
+ state.fileMessages.unshift(mid);
+ }
+ // image
if (!sending && isImage) {
data.image_id = content;
data.content = `${BASE_URL}/resource/image?id=${encodeURIComponent(
@@ -50,6 +62,11 @@ const messageSlice = createSlice({
: [action.payload];
mids.forEach((id) => {
delete state[id];
+ // 从file message 列表删掉
+ const fidIdx = state.fileMessages.findIndex((fid) => fid == id);
+ if (fidIdx > -1) {
+ state.fileMessages.splice(fidIdx, 1);
+ }
});
},
addReplyingMessage(state, action) {
diff --git a/src/app/slices/message.user.js b/src/app/slices/message.user.js
index 898649bb..52478bc4 100644
--- a/src/app/slices/message.user.js
+++ b/src/app/slices/message.user.js
@@ -23,6 +23,10 @@ const userMsgSlice = createSlice({
if (midExsited || localMsgExsited) return;
state.byId[id].push(+mid);
+ // 只要有新消息,就检查下
+ if (state.ids.findIndex((uid) => uid == id) == -1) {
+ state.ids.push(+id);
+ }
} else {
state.byId[id] = [+mid];
state.ids.push(+id);
diff --git a/src/common/component/Message/FileBox.js b/src/common/component/Message/FileBox.js
new file mode 100644
index 00000000..86701a53
--- /dev/null
+++ b/src/common/component/Message/FileBox.js
@@ -0,0 +1,81 @@
+// import React from 'react'
+import styled from "styled-components";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { useSelector } from "react-redux";
+import { getFileIcon, formatBytes } from "../../utils";
+import IconDownload from "../../../assets/icons/download.svg";
+dayjs.extend(relativeTime);
+const Styled = styled.div`
+ padding: 8px;
+ background: #f3f4f6;
+ border: 1px solid #d4d4d4;
+ box-sizing: border-box;
+ border-radius: 6px;
+ width: 370px;
+ 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%;
+ .name {
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 20px;
+ color: #1c1c1e;
+ }
+ .details {
+ font-weight: 400;
+ font-size: 12px;
+ line-height: 18px;
+ color: #616161;
+ display: flex;
+ gap: 16px;
+ .from strong {
+ font-weight: bold;
+ }
+ }
+ }
+ .download {
+ white-space: nowrap;
+ }
+`;
+export default function FileBox({
+ file_type,
+ name,
+ size,
+ created_at,
+ from_uid,
+ content,
+}) {
+ const fromUser = useSelector((store) => store.contacts.byId[from_uid]);
+ const icon = getFileIcon(file_type, name);
+ if (!content || !fromUser || !name) return null;
+ console.log("file content", content, name);
+ return (
+
+ {icon}
+
+ {name}
+
+ {formatBytes(size)}
+ {dayjs(created_at).fromNow()}
+
+ by {fromUser.name}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/common/component/Message/Reply.js b/src/common/component/Message/Reply.js
index 13a83528..4840ab8a 100644
--- a/src/common/component/Message/Reply.js
+++ b/src/common/component/Message/Reply.js
@@ -2,6 +2,7 @@
import styled from "styled-components";
import { useSelector } from "react-redux";
import { ContentTypes } from "../../../app/config";
+import { getFileIcon } from "../../utils";
import Avatar from "../Avatar";
const Styled = styled.div`
cursor: pointer;
@@ -37,15 +38,26 @@ const Styled = styled.div`
font-size: 14px;
line-height: 20px;
color: #616161;
+ display: flex;
+ align-items: center;
.pic {
display: inherit;
max-width: 34px;
}
+ .icon {
+ width: 15px;
+ height: 20px;
+ }
+ .name {
+ margin-left: 5px;
+ font-size: 10px;
+ color: #555;
+ }
}
/* padding-left: 10px; */
`;
const renderContent = (data) => {
- const { content_type, content, thumbnail } = data;
+ const { content_type, content, thumbnail, properties } = data;
let res = null;
switch (content_type) {
case ContentTypes.text:
@@ -55,6 +67,18 @@ const renderContent = (data) => {
case ContentTypes.imageJPG:
res =
;
break;
+ case ContentTypes.file:
+ {
+ const { file_type, name } = properties;
+ const icon = getFileIcon(file_type, name);
+ res = (
+ <>
+ {icon}
+ {name}
+ >
+ );
+ }
+ break;
default:
break;
diff --git a/src/common/component/Message/index.js b/src/common/component/Message/index.js
index 87442d13..6e99e3f6 100644
--- a/src/common/component/Message/index.js
+++ b/src/common/component/Message/index.js
@@ -99,6 +99,8 @@ function Message({ contextId = 0, mid = "", context = "user" }) {
/>
) : (
renderContent({
+ from_uid: fromUid,
+ created_at: time,
content_type,
properties,
content,
diff --git a/src/common/component/Message/renderContent.js b/src/common/component/Message/renderContent.js
index 81b0e387..5d40e2e3 100644
--- a/src/common/component/Message/renderContent.js
+++ b/src/common/component/Message/renderContent.js
@@ -2,7 +2,10 @@ import Linkify from "react-linkify";
import dayjs from "dayjs";
import MrakdownRender from "../MrakdownRender";
import { getDefaultSize } from "../../utils";
+import FileBox from "./FileBox";
const renderContent = ({
+ from_uid,
+ created_at,
properties,
content_type,
content,
@@ -53,7 +56,22 @@ const renderContent = ({
className="img preview"
style={{ width: `${width}px`, height: `${height}px` }}
data-origin={content}
- src={thumbnail}
+ src={thumbnail || content}
+ />
+ );
+ }
+ break;
+ case "rustchat/file":
+ {
+ const { size, name, file_type } = properties;
+ ctn = (
+
);
}
diff --git a/src/common/component/Message/styled.js b/src/common/component/Message/styled.js
index f6a818c2..cf8c5a3e 100644
--- a/src/common/component/Message/styled.js
+++ b/src/common/component/Message/styled.js
@@ -30,7 +30,7 @@ const StyledMsg = styled.div`
border-radius: 50%;
}
}
- .details {
+ > .details {
width: 100%;
display: flex;
flex-direction: column;
@@ -74,8 +74,9 @@ const StyledMsg = styled.div`
cursor: pointer;
}
a {
+ text-decoration: none;
border-radius: 2px;
- background-color: #f5feff;
+ /* background-color: #f5feff; */
padding: 2px;
color: #1fe1f9;
}
diff --git a/src/common/hook/useSendFileMessage.js b/src/common/hook/useSendFileMessage.js
new file mode 100644
index 00000000..5778d7d8
--- /dev/null
+++ b/src/common/hook/useSendFileMessage.js
@@ -0,0 +1,124 @@
+import { useState, useRef } from "react";
+// import { ContentTypes } from "../../app/config";
+// import { sliceFile } from "../utils";
+import { FILE_SLICE_SIZE } from "../../app/config";
+import {
+ usePrepareUploadFileMutation,
+ useUploadFileMutation,
+} from "../../app/services/message";
+import { useSendMsgMutation } from "../../app/services/contact";
+import { useSendChannelMsgMutation } from "../../app/services/channel";
+
+export default function useUploadImageMessage({
+ context = "user",
+ from = null,
+ to = null,
+}) {
+ // const slicedRef = useRef(false);
+ const [uploadingFile, setUploadingFile] = useState(false);
+ const sliceUploadedCountRef = useRef(0);
+ const totalSliceCountRef = useRef(1);
+ // const [uploadedSliceCount, setUploadedSliceCount] = useState(0)
+ const [
+ prepareUploadFile,
+ { isLoading: isPreparing, isSuccess: isPrepared },
+ ] = usePrepareUploadFileMutation();
+ const [
+ uploadFile,
+ { isLoading: isUploading, isSuccess: isUploaded, isError: uploadFileError },
+ ] = useUploadFileMutation();
+ const [
+ sendChannelMsg,
+ {
+ isLoading: channelSending,
+ isSuccess: channelSuccess,
+ isError: channelError,
+ },
+ ] = useSendChannelMsgMutation();
+
+ const [
+ sendUserMsg,
+ { isLoading: userSending, isSuccess: userSuccess, isError: userError },
+ ] = useSendMsgMutation();
+ const sendFn = context == "user" ? sendUserMsg : sendChannelMsg;
+ const uploadChunk = async (data) => {
+ const { file_id, chunk, is_last } = data;
+ const formData = new FormData();
+ formData.append("file_id", file_id);
+ formData.append("chunk_data", chunk);
+ formData.append("chunk_is_last", is_last);
+ return uploadFile(formData);
+ };
+ const sendFileMessage = async (file) => {
+ if (!file) return;
+ const { name, type: file_type, size: file_size } = file;
+ // 拿file id
+ const { data: file_id } = await prepareUploadFile();
+ console.log("file id", file_id);
+
+ let uploadResult = null;
+ totalSliceCountRef.current = 1;
+ setUploadingFile(true);
+ // 2MB
+ if (file_size <= 1000 * 1000 * 2) {
+ // 一次性上传文件
+ uploadResult = await uploadChunk({ file_id, chunk: file, is_last: true });
+ sliceUploadedCountRef.current = 1;
+ } else {
+ // 分片上传文件
+ totalSliceCountRef.current = Math.ceil(file_size / FILE_SLICE_SIZE);
+ const totalSliceCount = totalSliceCountRef.current;
+ const _arr = new Array(totalSliceCount);
+ // const chunk=file.slice(block_size * index, block_size * (index + 1));
+
+ for await (const [idx] of _arr.entries()) {
+ try {
+ const chunk = file.slice(
+ FILE_SLICE_SIZE * idx,
+ FILE_SLICE_SIZE * (idx + 1),
+ file_type
+ );
+
+ if (idx == _arr.length - 1) {
+ uploadResult = await uploadChunk({ file_id, chunk, is_last: true });
+ } else {
+ await uploadChunk({ file_id, chunk, is_last: false });
+ }
+ sliceUploadedCountRef.current = sliceUploadedCountRef.current + 1;
+ } catch (error) {
+ return;
+ }
+ }
+ console.log("wtfff", uploadResult);
+ }
+ setUploadingFile(false);
+ const {
+ data: { path, size, hash },
+ } = uploadResult;
+ const content = JSON.stringify({
+ name,
+ size,
+ hash,
+ path,
+ });
+ console.log("upload content", content);
+ await sendFn({
+ id: to,
+ content,
+ type: "file",
+ properties: { file_type },
+ from_uid: from,
+ });
+ };
+ const isSending =
+ userSending || channelSending || isPreparing || uploadingFile;
+ return {
+ progress: isSending
+ ? sliceUploadedCountRef.current / totalSliceCountRef.current
+ : 1,
+ sendFileMessage,
+ isError: channelError || userError || uploadFileError,
+ isSending,
+ isSuccess: (channelSuccess || userSuccess) && isPrepared,
+ };
+}
diff --git a/src/common/hook/useSendImageMessage.js b/src/common/hook/useSendImageMessage.js
new file mode 100644
index 00000000..b518a62c
--- /dev/null
+++ b/src/common/hook/useSendImageMessage.js
@@ -0,0 +1,39 @@
+// import second from 'first'
+import { useSendChannelMsgMutation } from "../../app/services/channel";
+import { useSendMsgMutation } from "../../app/services/contact";
+export default function useSendImageMessage({
+ context = "user",
+ from = null,
+ to = null,
+}) {
+ const [
+ sendChannelMsg,
+ {
+ isLoading: channelSending,
+ isSuccess: channelSuccess,
+ isError: channelError,
+ },
+ ] = useSendChannelMsgMutation();
+ const [
+ sendUserMsg,
+ { isLoading: userSending, isSuccess: userSuccess, isError: userError },
+ ] = useSendMsgMutation();
+ const uploadFn = context == "user" ? sendUserMsg : sendChannelMsg;
+ const sendImageMessage = (file) => {
+ if (!file) return;
+ const { name, size, type } = file;
+ uploadFn({
+ id: to,
+ content: file,
+ properties: { name, size, type, local_id: new Date().getTime() },
+ type: "image",
+ from_uid: from,
+ });
+ };
+ return {
+ sendImageMessage,
+ isError: channelError || userError,
+ isSending: userSending || channelSending,
+ isSuccess: channelSuccess || userSuccess,
+ };
+}
diff --git a/src/common/utils.js b/src/common/utils.js
index f27e07cb..da504e65 100644
--- a/src/common/utils.js
+++ b/src/common/utils.js
@@ -1,3 +1,10 @@
+import IconPdf from "../assets/icons/file.pdf.svg";
+import IconAudio from "../assets/icons/file.audio.svg";
+import IconVideo from "../assets/icons/file.video.svg";
+import IconUnkown from "../assets/icons/file.unkown.svg";
+import IconDoc from "../assets/icons/file.doc.svg";
+import IconCode from "../assets/icons/file.code.svg";
+import IconImage from "../assets/icons/file.image.svg";
export const isObjectEqual = (obj1, obj2) => {
let o1 = Object.entries(obj1 ?? {})
.sort()
@@ -7,6 +14,17 @@ export const isObjectEqual = (obj1, obj2) => {
.toString();
return o1 === o2;
};
+export const isTreatAsImage = (file) => {
+ let isImage = false;
+ if (!file) return isImage;
+ const { type, size } = file;
+ if (type.startsWith("image")) {
+ // 10MB
+ return size < 1000 * 1000;
+ }
+ return isImage;
+};
+
export const getNonNullValues = (obj, whiteList = ["log_id"]) => {
const tmp = {};
Object.keys(obj).forEach((k) => {
@@ -86,3 +104,63 @@ export const getInitialsAvatar = ({
/* istanbul ignore next */
return canvas.toDataURL("image/png");
};
+/**
+ * @param {File|Blob} - file to slice
+ * @param {Number} - chunksAmount
+ * @return {Array} - an array of Blobs
+ **/
+export function sliceFile(file, chunksAmount) {
+ if (!file) return null;
+ let byteIndex = 0;
+ let chunks = [];
+
+ for (let i = 0; i < chunksAmount; i += 1) {
+ let byteEnd = Math.ceil((file.size / chunksAmount) * (i + 1));
+ chunks.push(file.slice(byteIndex, byteEnd));
+ byteIndex += byteEnd - byteIndex;
+ }
+
+ return chunks;
+}
+export const getFileIcon = (type, name = "") => {
+ let icon = null;
+
+ const checks = {
+ image: /^image/gi,
+ audio: /^audio/gi,
+ video: /^video/gi,
+ code: /(json|javascript|java|rb|c|php|xml|css|html)$/gi,
+ doc: /^text/gi,
+ pdf: /\/pdf$/gi,
+ };
+ const _arr = name.split(".");
+ const _type = type || _arr[_arr.length - 1];
+ switch (true) {
+ case checks.image.test(_type):
+ {
+ console.log("image");
+ icon = ;
+ }
+ break;
+ case checks.pdf.test(_type):
+ icon = ;
+ break;
+ case checks.code.test(_type):
+ icon = ;
+ break;
+ case checks.doc.test(_type):
+ icon = ;
+ break;
+ case checks.audio.test(_type):
+ icon = ;
+ break;
+ case checks.video.test(_type):
+ icon = ;
+ break;
+
+ default:
+ icon = ;
+ break;
+ }
+ return icon;
+};
diff --git a/src/routes/home/index.js b/src/routes/home/index.js
index b33accf8..7dd1090d 100644
--- a/src/routes/home/index.js
+++ b/src/routes/home/index.js
@@ -62,7 +62,6 @@ export default function HomePage() {
{/* */}
- {/* */}