Commit ece66d8a by Sixong.Zhu

消息结构调整

parent 15e55bff
<template>
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<i
class="el-icon-warning-outline"
v-else-if="fileFailed2Load"
title="[语音加载失败]"
></i>
</div>
</template>
<script lang="ts">
import { Component, Ref } from "vue-property-decorator";
import BaseMessage from "./index";
import VoiceIcon from "./voice.vue";
@Component({ components: { VoiceIcon } })
export default class Index extends BaseMessage {
@Ref("audio")
private readonly audioRef!: HTMLAudioElement;
private playing = false;
private get duration() {
const v = this.messageBody.msg.duration as number;
return v || 0;
}
private get durationInSecond() {
return Math.round(this.duration / 1000);
}
private get getVoiceMessageWidth() {
if (this.fileFailed2Load) {
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
}
if (d >= 60) {
return 200;
}
return 60 + d;
}
private play() {
if (this.audioRef?.paused) {
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
}
}
private onPlay() {
this.playing = true;
}
private onPause() {
this.playing = false;
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
<template> <template>
<span class="file-icon" :title="value" v-html="html"></span> <span class="file-icon" :title="value" v-html="html"></span>
</template> </template>
<script lang="ts"> <script lang="ts">
...@@ -9,70 +9,70 @@ import { FileType, getSvg } from "./file-controller"; ...@@ -9,70 +9,70 @@ import { FileType, getSvg } from "./file-controller";
@Component({ components: {} }) @Component({ components: {} })
export default class FileIcon extends Vue { export default class FileIcon extends Vue {
@Model("update") @Model("update")
private value!: FileType; private value!: FileType;
private get audio() { private get audio() {
return this.value === FileType.Audio; return this.value === FileType.Audio;
} }
private get excel() { private get excel() {
return this.value === FileType.Excel; return this.value === FileType.Excel;
} }
private get image() { private get image() {
return this.value === FileType.Image; return this.value === FileType.Image;
} }
private get others() { private get others() {
return this.value === FileType.Others; return this.value === FileType.Others;
} }
private get pdf() { private get pdf() {
return this.value === FileType.Pdf; return this.value === FileType.Pdf;
} }
private get ppt() { private get ppt() {
return this.value === FileType.Ppt; return this.value === FileType.Ppt;
} }
private get rp() { private get rp() {
return this.value === FileType.Rp; return this.value === FileType.Rp;
} }
private get txt() { private get txt() {
return this.value === FileType.Txt; return this.value === FileType.Txt;
} }
private get video() { private get video() {
return this.value === FileType.Video; return this.value === FileType.Video;
} }
private get word() { private get word() {
return this.value === FileType.Word; return this.value === FileType.Word;
} }
private get xmid() { private get xmid() {
return this.value === FileType.Xmind; return this.value === FileType.Xmind;
} }
private get zip() { private get zip() {
return this.value === FileType.Zip; return this.value === FileType.Zip;
} }
private get html() { private get html() {
return getSvg(this.value); return getSvg(this.value);
} }
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.file-icon { .file-icon {
margin-left: 10px; margin-left: 10px;
svg { /deep/ svg {
max-width: 36px; max-width: 36px;
max-height: 36px; max-height: 36px;
} }
} }
</style> </style>
<template>
<div class="msg-detail file-message d-flex" @dblclick="openFile">
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ format(messageBody.msg.size) }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
<a
class="
d-flex
align-items-center
justify-content-center
download-icon
"
:href="messageRealUrl"
:download="getAttachment"
title="下载文件"
>
<img src="~@/customer-service/imgs/download.png" alt="Download" />
</a>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
import { FileType, getFileType } from "./file-controller";
const k = 1024,
m = 1024 * k,
g = 1024 * m,
t = 1024 * g;
function formatSize(size: number) {
if (size === undefined || size === null) {
return "";
}
if (size < k) {
return size + " B";
}
if (size < m) {
return Number((size / k).toFixed(2)) + " KB";
}
if (size < g) {
return Number((size / m).toFixed(2)) + " MB";
}
if (size < t) {
return Number((size / g).toFixed(2)) + " GB";
}
return Number((size / t).toFixed(2)) + " TB";
}
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
}
return "文件下载";
}
private get fileIcon() {
if (this.value) {
return getFileType(this.messageBody.msg.name);
}
return FileType.Others;
}
private format(v: number) {
return formatSize(v);
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
<template>
<div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
@dblclick="openFile"
>
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
@error="onImageError"
/>
<file-icon v-else-if="fileFailed2Load" :value="image404"></file-icon>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import { FileType } from "./file-controller";
import BaseMessage from "./index";
import FileIcon from "./file-icon.vue";
@Component({ components: { FileIcon } })
export default class Index extends BaseMessage {
private readonly image404 = FileType.Image_404;
private onImageError() {
this.fileFailed2Load = true;
this.messageRealUrl = "";
}
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
import { Component, Vue, Model, Prop } from "vue-property-decorator";
import { Message } from "@/customer-service/model";
import { isAccessibleUrl } from "@/customer-service/service/tools";
@Component({ components: {} })
export default class BaseMessage extends Vue {
@Model()
protected readonly value!: Message;
@Prop()
protected readonly userName!: string;
protected messageRealUrl = "";
protected fileFailed2Load = false;
protected loadingRealUrl = false;
protected get messageBody(): { eid?: string; oid?: string; msg: any } {
if (this.value) {
try {
return { ...this.value, msg: JSON.parse(this.value.msg) };
} catch {
return {
...this.value,
msg: JSON.parse(this.value.msg.replace(/\n/g, "\\n")),
};
}
}
return { msg: { text: "" } };
}
protected openFile() {
this.$emit("open", this.messageRealUrl);
}
protected buildMessageUrl() {
if (this.messageRealUrl || this.loadingRealUrl) {
return;
}
const url = this.messageBody.msg.url as string;
if (url) {
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
} else {
this.fileFailed2Load = true;
}
}
}
<template>
<div
class="msg-detail inline-text"
v-html="format2Link(messageBody.msg.text || emptyText)"
></div>
</template>
<script lang="ts">
import { replaceText2Link } from "@/utils/isUrl";
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {
private readonly emptyText = " ";
private format2Link(text: string) {
return replaceText2Link(text);
}
}
</script>
<style lang="less" scoped></style>
<template>
<div
class="
msg-detail
video-message
d-flex
align-items-center
justify-content-center
"
>
<video-player-icon @click.native="openFile"></video-player-icon>
</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
import VideoPlayerIcon from "./video-player-icon.vue";
@Component({ components: { VideoPlayerIcon } })
export default class Index extends BaseMessage {
mounted() {
this.buildMessageUrl();
}
}
</script>
<style lang="less" scoped></style>
...@@ -32,20 +32,18 @@ ...@@ -32,20 +32,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator" import { Component, Prop, Vue, Watch } from "vue-property-decorator";
;
@Component({ components: {} }) @Component({ components: {} })
export default class VoiceIcon extends Vue { export default class VoiceIcon extends Vue {
@Prop({ default: 25 }) @Prop({ default: 25 })
private size!: number private size!: number;
@Prop() @Prop()
private loading!: boolean private loading!: boolean;
private status = 0 private status = 0;
private interval = 0 private interval = 0;
@Watch("loading") @Watch("loading")
private onLoadingChanged() { private onLoadingChanged() {
......
<template>
<div class="msg-detail withdraw-message">{{ userName }}撤回了一条消息</div>
</template>
<script lang="ts">
import { Component } from "vue-property-decorator";
import BaseMessage from "./index";
@Component({ components: {} })
export default class Index extends BaseMessage {}
</script>
<style lang="less" scoped></style>
...@@ -197,8 +197,7 @@ export default class MessageList extends Vue { ...@@ -197,8 +197,7 @@ export default class MessageList extends Vue {
this.scollWrapper.addEventListener("scroll", this.handleScroll); this.scollWrapper.addEventListener("scroll", this.handleScroll);
this.saveScrollToBottomFunc(this.scrollToNewMsg); this.saveScrollToBottomFunc(this.scrollToNewMsg);
this.scrollToNewMsg(); this.scrollToNewMsg();
// force scroll end this.scroll2End(200);
setTimeout(() => this.scrollToNewMsg(), 100);
} }
public beforeDestroy() { public beforeDestroy() {
...@@ -216,12 +215,9 @@ export default class MessageList extends Vue { ...@@ -216,12 +215,9 @@ export default class MessageList extends Vue {
) as HTMLElement; ) as HTMLElement;
if (wrap) { if (wrap) {
if (delay) { if (delay) {
setTimeout(() => { return setTimeout(() => (wrap.scrollTop = wrap.scrollHeight), delay);
wrap.scrollTop = 100000;
}, delay);
return;
} }
wrap.scrollTop = 100000; wrap.scrollTop = wrap.scrollHeight;
} }
}); });
} }
......
...@@ -14,110 +14,16 @@ ...@@ -14,110 +14,16 @@
> >
{{ userName }} {{ userName }}
</div> </div>
<!-- Image -->
<div
class="msg-detail image-message"
:class="{ 'image-404': fileFailed2Load }"
v-if="messageType === 'image'"
@dblclick="openFile"
>
<img
v-if="messageRealUrl"
:src="messageRealUrl"
:title="messageBody.msg.name"
:alt="messageBody.msg.name"
@error="onImageError"
/>
<file-icon
v-else-if="fileFailed2Load"
:value="image404"
></file-icon>
</div>
<!-- File -->
<div
class="msg-detail file-message d-flex"
v-else-if="messageType === 'file'"
@dblclick="openFile"
>
<div class="file-message-info">
<div
class="text-nowrap text-truncate file-message-name"
:title="messageBody.msg.name"
>
{{ messageBody.msg.name }}
</div>
<div class="text-hint">
{{ messageBody.msg.size | formatSize }}
</div>
</div>
<file-icon :value="fileIcon"></file-icon>
</div>
<!-- Audio -->
<div
class="msg-detail voice-message d-flex align-items-center"
:class="{ playing: playing, 'can-play': messageRealUrl }"
v-else-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div class="d-flex align-items-center" v-if="messageRealUrl">
<voice-icon :loading="playing"></voice-icon>
<audio ref="audio" @play="onPlay" @pause="onPause">
<source type="audio/aac" :src="messageRealUrl" />
</audio>
<span v-if="duration" class="duration text-nowrap text-hint"
>{{ durationInSecond }}s</span
>
</div>
<i <component
class="el-icon-warning-outline" :is="messageComponent"
v-else-if="fileFailed2Load" :user-name="userName"
title="[语音加载失败]" v-if="messageComponent"
></i> v-model="data"
</div> @open="openFile"
<!-- Video --> />
<div
class="
msg-detail
video-message
d-flex
align-items-center
justify-content-center
"
v-else-if="messageType === 'video'"
>
<video-player-icon @click.native="openFile"></video-player-icon>
</div>
<div
class="msg-detail withdraw-message"
v-else-if="messageType === 'withdraw'"
>
{{ userName }}撤回了一条消息
</div>
<!-- Text -->
<div
class="msg-detail inline-text"
v-else
v-html="format2Link(messageBody.msg.text || emptyText)"
></div>
</div> </div>
<a
v-if="!isSendingMessage && messageType === 'file'"
class="
d-flex
align-items-center
justify-content-center
download-icon
"
:href="messageRealUrl | downloadUrl(getAttachment)"
:download="getAttachment"
title="下载文件"
>
<img src="~@/customer-service/imgs/download.png" alt="Download" />
</a>
<i <i
class="el-icon-warning text-danger" class="el-icon-warning text-danger"
v-if="failed" v-if="failed"
...@@ -160,22 +66,16 @@ import { Component, Inject, Mixins, Prop, Ref } from "vue-property-decorator"; ...@@ -160,22 +66,16 @@ import { Component, Inject, Mixins, Prop, Ref } from "vue-property-decorator";
import { Filters } from "../mixin/filter"; import { Filters } from "../mixin/filter";
import * as dto from "../model"; import * as dto from "../model";
import { isAccessibleUrl } from "../service/tools";
import { replaceText2Link } from "../utils";
import chat from "./../xim"; import chat from "./../xim";
import { import {
FileType,
getFileType,
isAudio, isAudio,
isImage, isImage,
isVideo, isVideo,
MAX_FILE_SIZE, MAX_FILE_SIZE,
MAX_IMAGE_SIZE, MAX_IMAGE_SIZE,
} from "./file-controller"; } from "./message-item/file-controller";
import FileIcon from "./file-icon.vue";
import VideoPlayerIcon from "./video-player-icon.vue";
import VoiceIcon from "./voice.vue";
import WhoReadList from "./who-read-list.vue"; import WhoReadList from "./who-read-list.vue";
import avatar from "@/customer-service/components/avatar.vue"; import avatar from "@/customer-service/components/avatar.vue";
...@@ -183,10 +83,35 @@ import { chatStore, ChatStore } from "@/customer-service/store/model"; ...@@ -183,10 +83,35 @@ import { chatStore, ChatStore } from "@/customer-service/store/model";
import ximInstance from "../xim/xim"; import ximInstance from "../xim/xim";
import { dbController } from "../database"; import { dbController } from "../database";
import ImageMessage from "./message-item/image-message.vue";
import FileMessage from "./message-item/file-message.vue";
import AudioMessage from "./message-item/audio-message.vue";
import VideoMessage from "./message-item/video-message.vue";
import TextMessage from "./message-item/text-message.vue";
import WithdrawMessage from "./message-item/withdraw-message.vue";
const twoMinutes = 2 * 60 * 1000; const twoMinutes = 2 * 60 * 1000;
const messageMapping = new Map<dto.MessageType, string>([
[dto.MessageType.Image, "image-message"],
[dto.MessageType.File, "file-message"],
[dto.MessageType.Video, "video-message"],
[dto.MessageType.Voice, "audio-message"],
[dto.MessageType.Text, "text-message"],
[dto.MessageType.Withdraw, "withdraw-message"],
]);
@Component({ @Component({
components: { FileIcon, VoiceIcon, WhoReadList, VideoPlayerIcon, avatar }, components: {
WhoReadList,
avatar,
ImageMessage,
FileMessage,
AudioMessage,
VideoMessage,
TextMessage,
WithdrawMessage,
},
}) })
export default class Message extends Mixins(Filters) { export default class Message extends Mixins(Filters) {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_UID) @chatStore.State(ChatStore.STATE_CHAT_CURRENT_USER_UID)
...@@ -204,34 +129,21 @@ export default class Message extends Mixins(Filters) { ...@@ -204,34 +129,21 @@ export default class Message extends Mixins(Filters) {
@chatStore.Mutation(ChatStore.MUTATION_WITHDRAW) @chatStore.Mutation(ChatStore.MUTATION_WITHDRAW)
private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW; private readonly executeWithDraw!: ChatStore.MUTATION_WITHDRAW;
/**
* tbd: 文件消息所在的域名的url,逻辑需要补充
*/
private messageRealUrl = "";
private fileFailed2Load = false;
private image404 = FileType.Image_404;
private loadingRealUrl = false;
@Prop({ type: Object, default: () => Object.create(null) }) @Prop({ type: Object, default: () => Object.create(null) })
private data!: dto.Message; private data!: dto.Message;
@Prop() @Prop()
private isSendingMessage!: boolean; private readonly isSendingMessage!: boolean;
@Prop() @Prop()
private failed!: boolean; private readonly failed!: boolean;
@Prop({ default: "circle" }) @Prop({ default: "circle" })
private shape!: string; private readonly shape!: string;
@Ref("audio")
private readonly audioRef!: HTMLAudioElement;
private playing = false;
@Inject({ default: false }) readonly showReadSummary!: boolean; @Inject({ default: false }) readonly showReadSummary!: boolean;
private emptyText = " "; private messageComponent = "";
private readListVisibility = false; private readListVisibility = false;
...@@ -267,13 +179,6 @@ export default class Message extends Mixins(Filters) { ...@@ -267,13 +179,6 @@ export default class Message extends Mixins(Filters) {
return { msg: { text: "" } }; return { msg: { text: "" } };
} }
private get getAttachment() {
if (this.messageBody) {
return this.messageBody.msg.name;
}
return "文件下载";
}
private get isMyMessage() { private get isMyMessage() {
if (this.isSendingMessage) { if (this.isSendingMessage) {
return true; return true;
...@@ -281,31 +186,10 @@ export default class Message extends Mixins(Filters) { ...@@ -281,31 +186,10 @@ export default class Message extends Mixins(Filters) {
const senderEid = this.messageBody.eid; const senderEid = this.messageBody.eid;
return senderEid!.toString() === this.chatMyId!.toString(); return senderEid!.toString() === this.chatMyId!.toString();
// if (this.messageBody) {
// const msg = this.messageBody;
// if (this.chatSource) {
// const source = msg.msg.source;
// if (source) {
// return (
// source === this.chatSource &&
// senderEid === this.chatMyId
// );
// }
// if (this.org && this.messageBody.oid) {
// return (
// this.messageBody.oid === this.org &&
// senderEid === this.chatMyId
// );
// }
// return false;
// }
// return senderEid === this.chatMyId;
// }
// return false;
} }
mounted() { created() {
this.buildMessageUrl(); this.messageComponent = messageMapping.get(this.messageType) as string;
} }
private get userName() { private get userName() {
...@@ -337,14 +221,6 @@ export default class Message extends Mixins(Filters) { ...@@ -337,14 +221,6 @@ export default class Message extends Mixins(Filters) {
return ""; return "";
} }
private get fileIcon() {
if (this.data) {
return getFileType(this.messageBody.msg.name);
}
return FileType.Others;
}
private get messageType() { private get messageType() {
const type = this.data?.type; const type = this.data?.type;
if (type === "file") { if (type === "file") {
...@@ -354,119 +230,39 @@ export default class Message extends Mixins(Filters) { ...@@ -354,119 +230,39 @@ export default class Message extends Mixins(Filters) {
if (size) { if (size) {
const outImageSize = size > MAX_IMAGE_SIZE; const outImageSize = size > MAX_IMAGE_SIZE;
if (!outImageSize && isImage(name)) { if (!outImageSize && isImage(name)) {
return "image"; return dto.MessageType.Image;
} }
const outSize = size > MAX_FILE_SIZE; const outSize = size > MAX_FILE_SIZE;
if (!outSize) { if (!outSize) {
if (isAudio(name)) { if (isAudio(name)) {
return "voice"; return dto.MessageType.Voice;
} }
if (isVideo(name)) { if (isVideo(name)) {
return "video"; return dto.MessageType.Video;
} }
} }
} }
} }
} }
return this.data?.type; return type;
}
// 图片的样式设置,通过js偶尔会有高度计算不准确, 直接通过css的处理目前看可以达到对应效果
// private get imageStyle() {
// if (this.messageBody == null) return {};
// if (this.messageBody.msg.w == null) return {};
// if (this.messageBody.msg.h == null) return {};
// const w = this.messageBody.msg.w;
// const h = this.messageBody.msg.h;
// const maxWidth = 300;
// const maxHeight = maxWidth * (h / w);
// return { width: `${w}px`, height: `${h}px`, maxHeight: `${maxHeight}px` };
// }
private get duration() {
const v = this.messageBody.msg.duration as number;
return v || 0;
}
private get durationInSecond() {
return Math.round(this.duration / 1000);
}
private get getVoiceMessageWidth() {
if (this.fileFailed2Load) {
return 35;
}
const d = this.duration / 1000;
if (d <= 3) {
return 60;
}
if (d >= 60) {
return 200;
}
return 60 + d;
} }
private isCustomer() { private isCustomer() {
return !this.showReadSummary; return !this.showReadSummary;
} }
private buildMessageUrl() { private openFile(url: string) {
if (this.messageRealUrl || this.loadingRealUrl) {
return;
}
const url = this.messageBody.msg.url as string;
if (url) {
if (isAccessibleUrl(url)) {
return (this.messageRealUrl = url);
}
this.loadingRealUrl = true;
} else {
this.fileFailed2Load = true;
}
}
private openFile() {
if (this.isSendingMessage) { if (this.isSendingMessage) {
return; return;
} }
if (this.failed || this.fileFailed2Load) { if (this.failed) {
return; return;
} }
const copy = { ...this.messageBody.msg }; const copy = { ...this.messageBody.msg };
if (this.messageRealUrl) { copy.url = url;
copy.url = this.messageRealUrl;
}
this.$emit("open", { type: this.messageType, msg: copy }); this.$emit("open", { type: this.messageType, msg: copy });
} }
private play() {
if (this.audioRef?.paused) {
this.audioRef?.load();
this.audioRef?.play();
} else {
this.audioRef?.pause();
}
}
private onPlay() {
this.playing = true;
}
private onPause() {
this.playing = false;
}
private format2Link(text: string) {
return replaceText2Link(text);
}
private onImageError() {
this.fileFailed2Load = true;
this.messageRealUrl = "";
}
private withdraw() { private withdraw() {
ximInstance.withdraw(this.chatId!, this.data.id).finally(() => { ximInstance.withdraw(this.chatId!, this.data.id).finally(() => {
dbController dbController
...@@ -633,6 +429,10 @@ i.msg-avatar { ...@@ -633,6 +429,10 @@ i.msg-avatar {
margin: 0; margin: 0;
} }
} }
/deep/ .file-icon {
margin-left: 0;
}
} }
&.voice-message { &.voice-message {
...@@ -669,7 +469,7 @@ i.msg-avatar { ...@@ -669,7 +469,7 @@ i.msg-avatar {
max-width: 130px; max-width: 130px;
} }
img { /deep/ img {
max-width: 300px; max-width: 300px;
} }
} }
......
...@@ -73,7 +73,7 @@ import { ...@@ -73,7 +73,7 @@ import {
MESSAGE_FILE_EMPTY, MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE, MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE, MESSAGE_IMAGE_TOO_LARGE,
} from "../components/file-controller"; } from "../components/message-item/file-controller";
import { EmojiService } from "../service/emoji"; import { EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model"; import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils"; import { formatFileSize } from "../utils";
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment