From 71ff176a66ad3ffee572b2cc0ebf8d1e624eca84 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Wed, 17 Nov 2021 17:38:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0emoji=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tsconfig.json | 1 + web/package.json | 2 + .../ChatBox/ChatMessageList/Item.tsx | 125 ++++++------------ .../useChatMessageItemAction.tsx | 71 ++++++++++ .../useChatMessageReaction.tsx | 16 +++ web/src/components/EmojiPanel/Picker.tsx | 80 +++++++++++ web/src/components/EmojiPanel/index.tsx | 19 +++ yarn.lock | 24 +++- 8 files changed, 256 insertions(+), 82 deletions(-) create mode 100644 web/src/components/ChatBox/ChatMessageList/useChatMessageItemAction.tsx create mode 100644 web/src/components/ChatBox/ChatMessageList/useChatMessageReaction.tsx create mode 100644 web/src/components/EmojiPanel/Picker.tsx create mode 100644 web/src/components/EmojiPanel/index.tsx diff --git a/tsconfig.json b/tsconfig.json index ee574a85..2dfb7932 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "moduleResolution": "node", "strict": true, "importsNotUsedAsValues": "error", + "resolveJsonModule": true, "typeRoots": ["./node_modules/@types", "../node_modules/@types", "./types"] } } diff --git a/web/package.json b/web/package.json index be53c9c8..d9af4a3b 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,7 @@ "@loadable/component": "^5.15.0", "antd": "^4.16.6", "clsx": "^1.1.1", + "emoji-mart": "^3.0.1", "is-hotkey": "^0.2.0", "jsonschema": "^1.4.0", "jwt-decode": "^3.1.2", @@ -51,6 +52,7 @@ "@testing-library/react-hooks": "^7.0.1", "@types/copy-webpack-plugin": "^8.0.0", "@types/dts-generator": "^2.1.6", + "@types/emoji-mart": "^3.0.8", "@types/is-hotkey": "^0.1.5", "@types/loadable__component": "^5.13.4", "@types/mini-css-extract-plugin": "^1.4.3", diff --git a/web/src/components/ChatBox/ChatMessageList/Item.tsx b/web/src/components/ChatBox/ChatMessageList/Item.tsx index d24c0cb1..a78e903d 100644 --- a/web/src/components/ChatBox/ChatMessageList/Item.tsx +++ b/web/src/components/ChatBox/ChatMessageList/Item.tsx @@ -7,80 +7,21 @@ import { SYSTEM_USERID, t, useCachedUserInfo, - useChatBoxContext, MessageHelper, - recallMessage, useAsync, getCachedUserInfo, - useAsyncRequest, - deleteMessage, - useGroupInfoContext, - useUserInfo, } from 'tailchat-shared'; import { Avatar } from '@/components/Avatar'; import { useRenderPluginMessageInterpreter } from './useRenderPluginMessageInterpreter'; import { getMessageRender } from '@/plugin/common'; import { Icon } from '@iconify/react'; -import { Divider, Dropdown, Menu } from 'antd'; +import { Divider, Dropdown } from 'antd'; import { UserName } from '@/components/UserName'; import './item.less'; import clsx from 'clsx'; - -/** - * 消息的会话操作 - */ -function useChatMessageItemAction(payload: ChatMessage): React.ReactElement { - const context = useChatBoxContext(); - const groupInfo = useGroupInfoContext(); - const userInfo = useUserInfo(); - - const [, handleRecallMessage] = useAsyncRequest(() => { - return recallMessage(payload._id); - }, [payload._id]); - - const [, handleDeleteMessage] = useAsyncRequest(() => { - return deleteMessage(payload._id); - }, [payload._id]); - - const isGroupOwner = groupInfo && groupInfo.owner === userInfo?._id; // - const isMessageAuthor = payload.author === userInfo?._id; - - return ( - - {context.hasContext && ( - } - onClick={() => context.setReplyMsg(payload)} - > - {t('回复')} - - )} - - {(isGroupOwner || isMessageAuthor) && ( - } - onClick={handleRecallMessage} - > - {t('撤回')} - - )} - - {/* 仅群组管理员可见 */} - {isGroupOwner && ( - } - onClick={handleDeleteMessage} - > - {t('删除')} - - )} - - ); -} +import { useChatMessageItemAction } from './useChatMessageItemAction'; +import { useChatMessageReaction } from './useChatMessageReaction'; +import { DevContainer } from '@/components/DevContainer'; /** * 消息引用 @@ -106,6 +47,12 @@ const MessageQuote: React.FC<{ payload: ChatMessage }> = React.memo( ); MessageQuote.displayName = 'MessageQuote'; +const MessageActionIcon: React.FC<{ icon: string }> = (props) => ( +
+ +
+); + /** * 普通消息 */ @@ -114,7 +61,8 @@ const NormalMessage: React.FC = React.memo((props) => { const userInfo = useCachedUserInfo(payload.author ?? ''); const [isActionBtnActive, setIsActionBtnActive] = useState(false); - const actions = useChatMessageItemAction(payload); + const emojiAction = useChatMessageReaction(payload); + const moreActions = useChatMessageItemAction(payload); return (
= React.memo((props) => {
{/* 操作 */} - -
+ +
+ +
+
+ + + - -
-
+
+ +
+ + ); }); diff --git a/web/src/components/ChatBox/ChatMessageList/useChatMessageItemAction.tsx b/web/src/components/ChatBox/ChatMessageList/useChatMessageItemAction.tsx new file mode 100644 index 00000000..b43ea2de --- /dev/null +++ b/web/src/components/ChatBox/ChatMessageList/useChatMessageItemAction.tsx @@ -0,0 +1,71 @@ +import { Icon } from '@iconify/react'; +import { Menu } from 'antd'; +import React from 'react'; +import { + ChatMessage, + deleteMessage, + recallMessage, + t, + useAsyncRequest, + useChatBoxContext, + useGroupInfoContext, + useUserInfo, +} from 'tailchat-shared'; + +/** + * 消息的会话操作 + */ +export function useChatMessageItemAction( + payload: ChatMessage +): React.ReactElement { + const context = useChatBoxContext(); + const groupInfo = useGroupInfoContext(); + const userInfo = useUserInfo(); + + const [, handleRecallMessage] = useAsyncRequest(() => { + return recallMessage(payload._id); + }, [payload._id]); + + const [, handleDeleteMessage] = useAsyncRequest(() => { + return deleteMessage(payload._id); + }, [payload._id]); + + const isGroupOwner = groupInfo && groupInfo.owner === userInfo?._id; // + const isMessageAuthor = payload.author === userInfo?._id; + + return ( + + {context.hasContext && ( + } + onClick={() => context.setReplyMsg(payload)} + > + {t('回复')} + + )} + + {(isGroupOwner || isMessageAuthor) && ( + } + onClick={handleRecallMessage} + > + {t('撤回')} + + )} + + {/* 仅群组管理员可见 */} + {isGroupOwner && ( + } + onClick={handleDeleteMessage} + > + {t('删除')} + + )} + + ); +} diff --git a/web/src/components/ChatBox/ChatMessageList/useChatMessageReaction.tsx b/web/src/components/ChatBox/ChatMessageList/useChatMessageReaction.tsx new file mode 100644 index 00000000..7e48832f --- /dev/null +++ b/web/src/components/ChatBox/ChatMessageList/useChatMessageReaction.tsx @@ -0,0 +1,16 @@ +import { EmojiPanel } from '@/components/EmojiPanel'; +import React, { useCallback } from 'react'; +import type { ChatMessage } from 'tailchat-shared'; + +/** + * 消息的反应信息操作 + */ +export function useChatMessageReaction( + payload: ChatMessage +): React.ReactElement { + const handleSelect = useCallback((code: string) => { + console.log('code', code); + }, []); + + return ; +} diff --git a/web/src/components/EmojiPanel/Picker.tsx b/web/src/components/EmojiPanel/Picker.tsx new file mode 100644 index 00000000..13ea2bb3 --- /dev/null +++ b/web/src/components/EmojiPanel/Picker.tsx @@ -0,0 +1,80 @@ +import React, { useCallback } from 'react'; +import { NimblePicker, Data, EmojiData } from 'emoji-mart'; +import data from 'emoji-mart/data/twitter.json'; + +import 'emoji-mart/css/emoji-mart.css'; +import { isValidStr } from 'tailchat-shared'; + +const emojiData: Data = { + compressed: true, + categories: [ + { + id: 'people', + name: 'Smileys & People', + emojis: data.categories[0].emojis, + }, + { + id: 'nature', + name: 'Animals & Nature', + emojis: data.categories[1].emojis, + }, + { + id: 'foods', + name: 'Food & Drink', + emojis: data.categories[2].emojis, + }, + { + id: 'activity', + name: 'Activities', + emojis: data.categories[3].emojis, + }, + { + id: 'places', + name: 'Travel & Places', + emojis: data.categories[4].emojis, + }, + { + id: 'objects', + name: 'Objects', + emojis: data.categories[5].emojis, + }, + { + id: 'symbols', + name: 'Symbols', + emojis: data.categories[6].emojis, + }, + { + id: 'flags', + name: 'Flags', + emojis: data.categories[7].emojis, + }, + ], + emojis: data.emojis, + aliases: data.aliases, +}; + +interface EmojiPickerProps { + onSelect: (code: string) => void; +} + +/** + * emoji表情面板 + */ +const EmojiPicker: React.FC = React.memo((props) => { + const handleSelect = useCallback( + (emoji: EmojiData) => { + const code = emoji.colons; + if (isValidStr(code)) { + props.onSelect(code); + } + }, + [props.onSelect] + ); + + return ( + + ); +}); +EmojiPicker.displayName = 'EmojiPicker'; + +export default EmojiPicker; diff --git a/web/src/components/EmojiPanel/index.tsx b/web/src/components/EmojiPanel/index.tsx new file mode 100644 index 00000000..8450e8b1 --- /dev/null +++ b/web/src/components/EmojiPanel/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Loadable } from '../Loadable'; + +const EmojiPicker = Loadable( + () => + import( + /* webpackChunkName: 'emoji-picker' */ /* webpackPrefetch: true */ './Picker' + ) +); + +/** + * emoji表情面板 + */ +export const EmojiPanel: React.FC<{ + onSelect: (code: string) => void; +}> = React.memo((props) => { + return ; +}); +EmojiPanel.displayName = 'EmojiPanel'; diff --git a/yarn.lock b/yarn.lock index febbaa91..7074b1a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1054,6 +1054,13 @@ core-js-pure "^3.15.0" regenerator-runtime "^0.13.4" +"@babel/runtime@^7.0.0": + version "7.16.3" + resolved "https://registry.npmmirror.com/@babel/runtime/download/@babel/runtime-7.16.3.tgz?cache=0&sync_timestamp=1636494920863&other_urls=https%3A%2F%2Fregistry.npmmirror.com%2F%40babel%2Fruntime%2Fdownload%2F%40babel%2Fruntime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.5", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.7", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.14.6" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" @@ -1858,6 +1865,13 @@ "@types/bluebird" "*" typescript "*" +"@types/emoji-mart@^3.0.8": + version "3.0.8" + resolved "https://registry.npmmirror.com/@types/emoji-mart/download/@types/emoji-mart-3.0.8.tgz#b0de7aabf39b5c4b6e378432bb3b99dbf903bc04" + integrity sha1-sN56q/ObXEtuN4QyuzuZ2/kDvAQ= + dependencies: + "@types/react" "*" + "@types/eslint-scope@^3.7.0": version "3.7.0" resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.0.tgz#4792816e31119ebd506902a482caec4951fabd86" @@ -4538,6 +4552,14 @@ emittery@^0.8.1: resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860" integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== +emoji-mart@^3.0.1: + version "3.0.1" + resolved "https://registry.npm.taobao.org/emoji-mart/download/emoji-mart-3.0.1.tgz#9ce86706e02aea0506345f98464814a662ca54c6" + integrity sha1-nOhnBuAq6gUGNF+YRkgUpmLKVMY= + dependencies: + "@babel/runtime" "^7.0.0" + prop-types "^15.6.0" + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -8566,7 +8588,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==