feat: 增加内置的文件发送功能

pull/70/head
moonrailgun 2 years ago
parent a70c5e47a3
commit 469f34134c

@ -51,7 +51,7 @@ export async function uploadFile(
return data;
} catch (e) {
showToasts(`${t('上传失败')}: ${t('可能是图片体积过大')}`, 'error');
showToasts(`${t('上传失败')}: ${t('可能是文件体积过大')}`, 'error');
console.error(`${t('上传失败')}: ${_get(e, 'message')}`);
throw e;
}

@ -28,6 +28,14 @@ regMessageTextDecorators(() => ({
return `[img]${plain}[/img]`;
},
card: (plain, attrs) => {
const h = [
'card',
...Object.entries(attrs).map(([k, v]) => `${k}=${v}`),
].join(' ');
return `[${h}]${plain}[/card]`;
},
mention: (userId, userName) => `[at=${userId}]${userName}[/at]`,
emoji: (emojiCode) => `[emoji]${stripColons(emojiCode)}[/emoji]`,
serialize: (plain: string) => (serialize ? serialize(plain) : plain),

@ -0,0 +1,17 @@
import { Card } from '@capital/component';
import React from 'react';
import type { TagProps } from '../bbcode/type';
export const CardTag: React.FC<TagProps> = React.memo((props) => {
const { node } = props;
const label = node.content.join('');
const attrs = node.attrs ?? {};
const payload: any = {
label,
...attrs,
};
return <Card type={payload.type} payload={payload} />;
});
CardTag.displayName = 'CardTag';

@ -10,6 +10,7 @@ import { BoldTag } from './BoldTag';
import { ItalicTag } from './ItalicTag';
import { UnderlinedTag } from './UnderlinedTag';
import { DeleteTag } from './DeleteTag';
import { CardTag } from './CardTag';
import './styles.less';
@ -28,3 +29,4 @@ registerBBCodeTag('at', MentionTag);
registerBBCodeTag('emoji', EmojiTag);
registerBBCodeTag('markdown', MarkdownTag);
registerBBCodeTag('md', MarkdownTag); // alias
registerBBCodeTag('card', CardTag); // alias

@ -0,0 +1,45 @@
import { downloadUrl } from '@/utils/file-helper';
import React from 'react';
import { Icon } from 'tailchat-design';
import { useMemoizedFn, t } from 'tailchat-shared';
import { IconBtn } from '../IconBtn';
import { CardWrapper } from './Wrapper';
export interface FileCardPayload {
label: string;
url: string;
}
export const FileCard: React.FC<{
payload: FileCardPayload;
}> = React.memo((props) => {
const payload = props.payload ?? {};
const handleDownload = useMemoizedFn(() => {
downloadUrl(payload.url, payload.label);
});
return (
<CardWrapper>
<div className="flex items-center">
<div className="mr-3 overflow-hidden">
<div className="flex text-lg items-center">
<Icon icon="mdi:paperclip" />
<span className="ml-1">{t('文件')}</span>
</div>
<div className="text-sm text-black text-opacity-60 dark:text-white dark:text-opacity-60">
{payload.label}
</div>
</div>
<IconBtn
title={t('下载')}
icon="mdi:cloud-download-outline"
onClick={handleDownload}
/>
</div>
</CardWrapper>
);
});
FileCard.displayName = 'FileCard';

@ -0,0 +1,14 @@
import React from 'react';
export const CardWrapper: React.FC<React.PropsWithChildren> = React.memo(
(props) => {
return (
<div className="w-3/4">
<div className="border border-black border-opacity-20 rounded-md p-2 bg-black bg-opacity-5 dark:bg-black dark:bg-opacity-10 inline-flex overflow-hidden">
{props.children}
</div>
</div>
);
}
);
CardWrapper.displayName = 'CardWrapper';

@ -0,0 +1,17 @@
import React from 'react';
import { t } from 'tailchat-shared';
import { FileCard, FileCardPayload } from './FileCard';
import { CardWrapper } from './Wrapper';
interface Props {
type: 'file';
payload: FileCardPayload;
}
export const Card: React.FC<Props> = React.memo((props) => {
if (props.type === 'file') {
return <FileCard payload={props.payload} />;
}
return <CardWrapper>{t('未知的卡片类型')}</CardWrapper>;
});
Card.displayName = 'Card';

@ -8,7 +8,7 @@ import { Dropdown, Menu } from 'antd';
import React, { useState } from 'react';
import { t } from 'tailchat-shared';
import { useChatInputActionContext } from './context';
import { uploadMessageImage } from './utils';
import { uploadMessageFile, uploadMessageImage } from './utils';
import clsx from 'clsx';
export const ChatInputAddon: React.FC = React.memo(() => {
@ -31,6 +31,19 @@ export const ChatInputAddon: React.FC = React.memo(() => {
}
};
const handleSendFile = (files: FileList) => {
// 发送文件
const file = files[0];
if (file) {
// 发送图片
uploadMessageFile(file).then(({ name, url }) => {
actionContext.sendMsg(
getMessageTextDecorators().card(name, { type: 'file', url })
);
});
}
};
const menu = (
<Menu>
<FileSelector
@ -40,6 +53,10 @@ export const ChatInputAddon: React.FC = React.memo(() => {
<Menu.Item>{t('发送图片')}</Menu.Item>
</FileSelector>
<FileSelector onSelected={handleSendFile}>
<Menu.Item>{t('发送文件')}</Menu.Item>
</FileSelector>
{pluginChatInputActions.map((item, i) => (
<Menu.Item
key={item.label + i}

@ -3,7 +3,7 @@ import { t, useMemoizedFn } from 'tailchat-shared';
import { DropTargetMonitor, useDrop } from 'react-dnd';
import { NativeTypes } from 'react-dnd-html5-backend';
import { useChatInputActionContext } from './context';
import { uploadMessageImage } from './utils';
import { uploadMessageFile, uploadMessageImage } from './utils';
import { getMessageTextDecorators } from '@/plugin/common';
import { Icon } from 'tailchat-design';
@ -11,15 +11,21 @@ export const ChatDropArea: React.FC = React.memo(() => {
const actionContext = useChatInputActionContext();
const handleDrop = useMemoizedFn((files: File[]) => {
const images = files.filter((f) => f.type.startsWith('image/'));
if (images.length > 0) {
// 目前只取一张
const img = images[0];
uploadMessageImage(img).then(({ url, width, height }) => {
const file = files[0];
if (file.type.startsWith('image/')) {
// 发送图片
uploadMessageImage(file).then(({ url, width, height }) => {
actionContext?.sendMsg(
getMessageTextDecorators().image(url, { width, height })
);
});
} else {
// 发送文件
uploadMessageFile(file).then(({ url, name }) => {
actionContext?.sendMsg(
getMessageTextDecorators().card(name, { type: 'file', url })
);
});
}
});

@ -50,3 +50,18 @@ export function uploadMessageImage(image: File): Promise<{
});
});
}
/**
*
*/
export async function uploadMessageFile(file: File): Promise<{
name: string;
url: string;
}> {
const fileInfo = await uploadFile(file);
return {
name: file.name || fileInfo.etag,
url: fileInfo.url,
};
}

@ -131,6 +131,7 @@ export const [getMessageRender, regMessageRender] = buildRegFn<
const defaultMessageTextDecorators = {
url: (url: string, label?: string) => url,
image: (plain: string, attrs: Record<string, unknown>) => plain,
card: (plain: string, payload: Record<string, unknown>) => plain,
mention: (userId: string, userName: string) => `@${userName}`,
emoji: (emojiCode: string) => emojiCode,
serialize: (plain: string) => plain,

@ -59,3 +59,4 @@ export { UserAvatar } from '@/components/UserAvatar';
export { UserName } from '@/components/UserName';
export { Markdown } from '@/components/Markdown';
export { Webview, WebviewKeepAlive } from '@/components/Webview';
export { Card } from '@/components/Card';

@ -87,11 +87,11 @@ export async function blobUrlToFile(
}
/**
* Bloburl
* url
*/
export async function downloadBlobUrl(blobUrl: string, fileName: string) {
export function downloadUrl(url: string, fileName: string) {
const a = document.createElement('a');
a.href = blobUrl;
a.href = url;
a.download = fileName; // 这里填保存成的文件名
a.click();
}
@ -99,9 +99,9 @@ export async function downloadBlobUrl(blobUrl: string, fileName: string) {
/**
* Blob
*/
export async function downloadBlob(blob: Blob, fileName: string) {
export function downloadBlob(blob: Blob, fileName: string) {
const url = String(URL.createObjectURL(blob));
downloadBlobUrl(url, fileName);
downloadUrl(url, fileName);
URL.revokeObjectURL(url);
}

Loading…
Cancel
Save