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

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

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

@ -28,6 +28,14 @@ regMessageTextDecorators(() => ({
return `[img]${plain}[/img]`; 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]`, mention: (userId, userName) => `[at=${userId}]${userName}[/at]`,
emoji: (emojiCode) => `[emoji]${stripColons(emojiCode)}[/emoji]`, emoji: (emojiCode) => `[emoji]${stripColons(emojiCode)}[/emoji]`,
serialize: (plain: string) => (serialize ? serialize(plain) : plain), 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 { ItalicTag } from './ItalicTag';
import { UnderlinedTag } from './UnderlinedTag'; import { UnderlinedTag } from './UnderlinedTag';
import { DeleteTag } from './DeleteTag'; import { DeleteTag } from './DeleteTag';
import { CardTag } from './CardTag';
import './styles.less'; import './styles.less';
@ -28,3 +29,4 @@ registerBBCodeTag('at', MentionTag);
registerBBCodeTag('emoji', EmojiTag); registerBBCodeTag('emoji', EmojiTag);
registerBBCodeTag('markdown', MarkdownTag); registerBBCodeTag('markdown', MarkdownTag);
registerBBCodeTag('md', MarkdownTag); // alias 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 React, { useState } from 'react';
import { t } from 'tailchat-shared'; import { t } from 'tailchat-shared';
import { useChatInputActionContext } from './context'; import { useChatInputActionContext } from './context';
import { uploadMessageImage } from './utils'; import { uploadMessageFile, uploadMessageImage } from './utils';
import clsx from 'clsx'; import clsx from 'clsx';
export const ChatInputAddon: React.FC = React.memo(() => { 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 = ( const menu = (
<Menu> <Menu>
<FileSelector <FileSelector
@ -40,6 +53,10 @@ export const ChatInputAddon: React.FC = React.memo(() => {
<Menu.Item>{t('发送图片')}</Menu.Item> <Menu.Item>{t('发送图片')}</Menu.Item>
</FileSelector> </FileSelector>
<FileSelector onSelected={handleSendFile}>
<Menu.Item>{t('发送文件')}</Menu.Item>
</FileSelector>
{pluginChatInputActions.map((item, i) => ( {pluginChatInputActions.map((item, i) => (
<Menu.Item <Menu.Item
key={item.label + i} key={item.label + i}

@ -3,7 +3,7 @@ import { t, useMemoizedFn } from 'tailchat-shared';
import { DropTargetMonitor, useDrop } from 'react-dnd'; import { DropTargetMonitor, useDrop } from 'react-dnd';
import { NativeTypes } from 'react-dnd-html5-backend'; import { NativeTypes } from 'react-dnd-html5-backend';
import { useChatInputActionContext } from './context'; import { useChatInputActionContext } from './context';
import { uploadMessageImage } from './utils'; import { uploadMessageFile, uploadMessageImage } from './utils';
import { getMessageTextDecorators } from '@/plugin/common'; import { getMessageTextDecorators } from '@/plugin/common';
import { Icon } from 'tailchat-design'; import { Icon } from 'tailchat-design';
@ -11,15 +11,21 @@ export const ChatDropArea: React.FC = React.memo(() => {
const actionContext = useChatInputActionContext(); const actionContext = useChatInputActionContext();
const handleDrop = useMemoizedFn((files: File[]) => { const handleDrop = useMemoizedFn((files: File[]) => {
const images = files.filter((f) => f.type.startsWith('image/')); const file = files[0];
if (images.length > 0) { if (file.type.startsWith('image/')) {
// 目前只取一张 // 发送图片
const img = images[0]; uploadMessageImage(file).then(({ url, width, height }) => {
uploadMessageImage(img).then(({ url, width, height }) => {
actionContext?.sendMsg( actionContext?.sendMsg(
getMessageTextDecorators().image(url, { width, height }) 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 = { const defaultMessageTextDecorators = {
url: (url: string, label?: string) => url, url: (url: string, label?: string) => url,
image: (plain: string, attrs: Record<string, unknown>) => plain, image: (plain: string, attrs: Record<string, unknown>) => plain,
card: (plain: string, payload: Record<string, unknown>) => plain,
mention: (userId: string, userName: string) => `@${userName}`, mention: (userId: string, userName: string) => `@${userName}`,
emoji: (emojiCode: string) => emojiCode, emoji: (emojiCode: string) => emojiCode,
serialize: (plain: string) => plain, serialize: (plain: string) => plain,

@ -59,3 +59,4 @@ export { UserAvatar } from '@/components/UserAvatar';
export { UserName } from '@/components/UserName'; export { UserName } from '@/components/UserName';
export { Markdown } from '@/components/Markdown'; export { Markdown } from '@/components/Markdown';
export { Webview, WebviewKeepAlive } from '@/components/Webview'; 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'); const a = document.createElement('a');
a.href = blobUrl; a.href = url;
a.download = fileName; // 这里填保存成的文件名 a.download = fileName; // 这里填保存成的文件名
a.click(); a.click();
} }
@ -99,9 +99,9 @@ export async function downloadBlobUrl(blobUrl: string, fileName: string) {
/** /**
* Blob * Blob
*/ */
export async function downloadBlob(blob: Blob, fileName: string) { export function downloadBlob(blob: Blob, fileName: string) {
const url = String(URL.createObjectURL(blob)); const url = String(URL.createObjectURL(blob));
downloadBlobUrl(url, fileName); downloadUrl(url, fileName);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }

Loading…
Cancel
Save