From 628148fb6fbd32c526200e300fba2efde3729e7e Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Fri, 6 May 2022 22:28:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E8=BE=93=E5=85=A5=E8=A1=A8=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/plugins/com.msgbyte.bbcode/src/index.tsx | 21 ++++++++++ .../com.msgbyte.bbcode/src/tags/EmojiTag.tsx | 11 ++++++ .../com.msgbyte.bbcode/src/tags/__all__.ts | 2 + web/src/App.tsx | 2 +- .../ChatBox/ChatInputBox/Emotion.tsx | 38 +++++++++++++++++++ .../ChatBox/ChatInputBox/context.tsx | 5 ++- .../components/ChatBox/ChatInputBox/index.tsx | 20 +++++++++- .../ChatBox/ChatMessageList/Item.less | 14 +++++++ .../ChatBox/ChatMessageList/Item.tsx | 2 +- .../__tests__/preprocessMessage.spec.ts | 12 ++++++ web/src/components/ChatBox/index.tsx | 3 +- .../components/ChatBox/preprocessMessage.tsx | 15 ++++++++ web/src/components/ErrorBoundary.tsx | 18 ++++----- web/src/plugin/common/reg.ts | 1 + web/src/plugin/component/index.tsx | 1 + 15 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 web/plugins/com.msgbyte.bbcode/src/tags/EmojiTag.tsx create mode 100644 web/src/components/ChatBox/ChatInputBox/Emotion.tsx create mode 100644 web/src/components/ChatBox/__tests__/preprocessMessage.spec.ts create mode 100644 web/src/components/ChatBox/preprocessMessage.tsx diff --git a/web/plugins/com.msgbyte.bbcode/src/index.tsx b/web/plugins/com.msgbyte.bbcode/src/index.tsx index 9d394191..e5cda518 100644 --- a/web/plugins/com.msgbyte.bbcode/src/index.tsx +++ b/web/plugins/com.msgbyte.bbcode/src/index.tsx @@ -21,4 +21,25 @@ regMessageTextDecorators(() => ({ return `[img]${plain}[/img]`; }, mention: (userId, userName) => `[at=${userId}]${userName}[/at]`, + emoji: (emojiCode) => `[emoji]${stripColons(emojiCode)}[/emoji]`, })); + +/** + * Removes colons on either side + * of the string if present + */ +function stripColons(str: string): string { + const colonIndex = str.indexOf(':'); + if (colonIndex > -1) { + // :emoji: (http://www.emoji-cheat-sheet.com/) + if (colonIndex === str.length - 1) { + str = str.substring(0, colonIndex); + return stripColons(str); + } else { + str = str.substr(colonIndex + 1); + return stripColons(str); + } + } + + return str; +} diff --git a/web/plugins/com.msgbyte.bbcode/src/tags/EmojiTag.tsx b/web/plugins/com.msgbyte.bbcode/src/tags/EmojiTag.tsx new file mode 100644 index 00000000..8aa6f865 --- /dev/null +++ b/web/plugins/com.msgbyte.bbcode/src/tags/EmojiTag.tsx @@ -0,0 +1,11 @@ +import { Emoji } from '@capital/component'; +import React from 'react'; +import type { TagProps } from '../bbcode/type'; + +export const EmojiTag: React.FC = React.memo((props) => { + const { node } = props; + const code = node.content.join(''); + + return ; +}); +EmojiTag.displayName = 'EmojiTag'; diff --git a/web/plugins/com.msgbyte.bbcode/src/tags/__all__.ts b/web/plugins/com.msgbyte.bbcode/src/tags/__all__.ts index 82d9f619..5017451c 100644 --- a/web/plugins/com.msgbyte.bbcode/src/tags/__all__.ts +++ b/web/plugins/com.msgbyte.bbcode/src/tags/__all__.ts @@ -4,6 +4,7 @@ import { ImgTag } from './ImgTag'; import { MentionTag } from './MentionTag'; import { PlainText } from './PlainText'; import { UrlTag } from './UrlTag'; +import { EmojiTag } from './EmojiTag'; import './styles.less'; @@ -12,3 +13,4 @@ registerBBCodeTag('url', UrlTag); registerBBCodeTag('img', ImgTag); registerBBCodeTag('code', CodeTag); registerBBCodeTag('at', MentionTag); +registerBBCodeTag('emoji', EmojiTag); diff --git a/web/src/App.tsx b/web/src/App.tsx index b0d1964f..af388569 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -42,7 +42,7 @@ const AppContainer: React.FC = React.memo((props) => { id="tailchat-app" className={clsx( 'tailchat-app', - 'absolute inset-0 select-none', + 'absolute inset-0 select-none overflow-hidden', { dark: isDarkMode, }, diff --git a/web/src/components/ChatBox/ChatInputBox/Emotion.tsx b/web/src/components/ChatBox/ChatInputBox/Emotion.tsx new file mode 100644 index 00000000..a6c4a490 --- /dev/null +++ b/web/src/components/ChatBox/ChatInputBox/Emotion.tsx @@ -0,0 +1,38 @@ +import { Icon } from '@/components/Icon'; +import { Dropdown } from 'antd'; +import React, { useCallback, useState } from 'react'; +import { useChatInputActionContext } from './context'; +import { EmojiPanel } from '@/components/Emoji'; + +export const ChatInputEmotion: React.FC = React.memo(() => { + const actionContext = useChatInputActionContext(); + const { appendMsg } = actionContext; + + const [visible, setVisible] = useState(false); + + const handleSelect = useCallback( + async (code: string) => { + appendMsg(code); + setVisible(false); + }, + [appendMsg] + ); + + const menu = ; + + return ( + + + + ); +}); +ChatInputEmotion.displayName = 'ChatInputEmotion'; diff --git a/web/src/components/ChatBox/ChatInputBox/context.tsx b/web/src/components/ChatBox/ChatInputBox/context.tsx index 53751534..9b0268fe 100644 --- a/web/src/components/ChatBox/ChatInputBox/context.tsx +++ b/web/src/components/ChatBox/ChatInputBox/context.tsx @@ -6,9 +6,12 @@ import type { SuggestionDataItem } from 'react-mentions'; */ export interface ChatInputActionContextProps { sendMsg: (message: string) => void; + appendMsg: (message: string) => void; } export const ChatInputActionContext = - React.createContext(null); + React.createContext( + {} as ChatInputActionContextProps + ); ChatInputActionContext.displayName = 'ChatInputContext'; export function useChatInputActionContext() { diff --git a/web/src/components/ChatBox/ChatInputBox/index.tsx b/web/src/components/ChatBox/ChatInputBox/index.tsx index da0a21ad..d1917389 100644 --- a/web/src/components/ChatBox/ChatInputBox/index.tsx +++ b/web/src/components/ChatBox/ChatInputBox/index.tsx @@ -12,6 +12,7 @@ import { SendMessagePayloadMeta, useSharedEventHandler, } from 'tailchat-shared'; +import { ChatInputEmotion } from './Emotion'; interface ChatInputBoxProps { onSendMsg: (msg: string, meta?: SendMessagePayloadMeta) => void; @@ -28,6 +29,15 @@ export const ChatInputBox: React.FC = React.memo((props) => { inputRef.current?.focus(); }, [message, mentions]); + const handleAppendMsg = useCallback( + (append: string) => { + setMessage(message + append); + + inputRef.current?.focus(); + }, + [message] + ); + const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (isEnterHotkey(e.nativeEvent)) { @@ -71,7 +81,12 @@ export const ChatInputBox: React.FC = React.memo((props) => { }); return ( - +
= React.memo((props) => { onPaste={handlePaste} /> -
+
+
diff --git a/web/src/components/ChatBox/ChatMessageList/Item.less b/web/src/components/ChatBox/ChatMessageList/Item.less index 465b4e3b..d62d9f51 100644 --- a/web/src/components/ChatBox/ChatMessageList/Item.less +++ b/web/src/components/ChatBox/ChatMessageList/Item.less @@ -12,8 +12,22 @@ align-items: center; } } + + .chat-message-item_body { + .emoji-mart-emoji { + height: 16px; + vertical-align: middle; + display: inline-block; + margin: 0 4px; + + > span { + vertical-align: top; + } + } + } } + .chat-message-item_action-popover { padding-top: 0; diff --git a/web/src/components/ChatBox/ChatMessageList/Item.tsx b/web/src/components/ChatBox/ChatMessageList/Item.tsx index eb1b8910..2011e3ba 100644 --- a/web/src/components/ChatBox/ChatMessageList/Item.tsx +++ b/web/src/components/ChatBox/ChatMessageList/Item.tsx @@ -104,7 +104,7 @@ const NormalMessage: React.FC = React.memo((props) => { )} {/* 消息内容 */} -
+
{getMessageRender(payload.content)} diff --git a/web/src/components/ChatBox/__tests__/preprocessMessage.spec.ts b/web/src/components/ChatBox/__tests__/preprocessMessage.spec.ts new file mode 100644 index 00000000..2b94f06a --- /dev/null +++ b/web/src/components/ChatBox/__tests__/preprocessMessage.spec.ts @@ -0,0 +1,12 @@ +import { regMessageTextDecorators } from '@/plugin/common'; +import { preprocessMessage } from '../preprocessMessage'; + +regMessageTextDecorators(() => ({ + emoji: (code) => `[emoji]${code.substring(1, code.length - 1)}[/emoji]`, +})); + +test('preprocessMessage', () => { + expect(preprocessMessage('anystring :face: anystring :heart:')).toBe( + 'anystring [emoji]face[/emoji] anystring [emoji]heart[/emoji]' + ); +}); diff --git a/web/src/components/ChatBox/index.tsx b/web/src/components/ChatBox/index.tsx index 5155ddda..6e6c7841 100644 --- a/web/src/components/ChatBox/index.tsx +++ b/web/src/components/ChatBox/index.tsx @@ -5,6 +5,7 @@ import { ChatBoxPlaceholder } from './ChatBoxPlaceholder'; import { ChatInputBox } from './ChatInputBox'; import { ChatMessageList } from './ChatMessageList'; import { ChatReply } from './ChatReply'; +import { preprocessMessage } from './preprocessMessage'; import { useMessageAck } from './useMessageAck'; type ChatBoxProps = @@ -59,7 +60,7 @@ const ChatBoxInner: React.FC = React.memo((props) => { handleSendMessage({ converseId: props.converseId, groupId: props.groupId, - content: msg, + content: preprocessMessage(msg), meta, }); }} diff --git a/web/src/components/ChatBox/preprocessMessage.tsx b/web/src/components/ChatBox/preprocessMessage.tsx new file mode 100644 index 00000000..3b6e96de --- /dev/null +++ b/web/src/components/ChatBox/preprocessMessage.tsx @@ -0,0 +1,15 @@ +import { getMessageTextDecorators } from '@/plugin/common'; + +const emojiNameRegex = /:([a-zA-Z0-9_\-\+]+):/g; + +/** + * 预加工待发送的消息 + */ +export function preprocessMessage(message: string) { + /** + * 预加工emoji + */ + return message.replaceAll(emojiNameRegex, (code) => + getMessageTextDecorators().emoji(code) + ); +} diff --git a/web/src/components/ErrorBoundary.tsx b/web/src/components/ErrorBoundary.tsx index 8e2bb424..e77fe406 100644 --- a/web/src/components/ErrorBoundary.tsx +++ b/web/src/components/ErrorBoundary.tsx @@ -38,16 +38,14 @@ export class ErrorBoundary extends React.Component< typeof description === 'undefined' ? componentStack : description; if (error) { return ( -
- -

{t('页面出现了一些问题')}

-

{errorMessage}

- - } - /> -
+ +

{t('页面出现了一些问题')}

+

{errorMessage}

+ + } + /> ); } diff --git a/web/src/plugin/common/reg.ts b/web/src/plugin/common/reg.ts index 6aebce07..6078b84e 100644 --- a/web/src/plugin/common/reg.ts +++ b/web/src/plugin/common/reg.ts @@ -106,6 +106,7 @@ const defaultMessageTextDecorators = { url: (plain: string) => plain, image: (plain: string, attrs: Record) => plain, mention: (userId: string, userName: string) => `@${userName}`, + emoji: (emojiCode: string) => emojiCode, }; const [_getMessageTextDecorators, regMessageTextDecorators] = buildRegFn< () => Partial diff --git a/web/src/plugin/component/index.tsx b/web/src/plugin/component/index.tsx index f44b2259..02f7e46b 100644 --- a/web/src/plugin/component/index.tsx +++ b/web/src/plugin/component/index.tsx @@ -32,3 +32,4 @@ export { export { Loading } from '@/components/Loading'; export { SidebarView } from '@/components/SidebarView'; export { GroupPanelSelector } from '@/components/GroupPanelSelector'; +export { Emoji } from '@/components/Emoji';