feat: 增加emoji表情管理

release/desktop
moonrailgun 3 years ago
parent 9642f2c436
commit 71ff176a66

@ -9,6 +9,7 @@
"moduleResolution": "node",
"strict": true,
"importsNotUsedAsValues": "error",
"resolveJsonModule": true,
"typeRoots": ["./node_modules/@types", "../node_modules/@types", "./types"]
}
}

@ -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",

@ -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 (
<Menu>
{context.hasContext && (
<Menu.Item
key="reply"
icon={<Icon icon="mdi:reply" />}
onClick={() => context.setReplyMsg(payload)}
>
{t('回复')}
</Menu.Item>
)}
{(isGroupOwner || isMessageAuthor) && (
<Menu.Item
key="recall"
icon={<Icon icon="mdi:restore" />}
onClick={handleRecallMessage}
>
{t('撤回')}
</Menu.Item>
)}
{/* 仅群组管理员可见 */}
{isGroupOwner && (
<Menu.Item
key="delete"
danger={true}
icon={<Icon icon="mdi:delete-outline" />}
onClick={handleDeleteMessage}
>
{t('删除')}
</Menu.Item>
)}
</Menu>
);
}
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) => (
<div className="px-0.5 w-6 h-6 flex justify-center items-center opacity-60 hover:opacity-100">
<Icon icon={props.icon} />
</div>
);
/**
*
*/
@ -114,7 +61,8 @@ const NormalMessage: React.FC<ChatMessageItemProps> = 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 (
<div
@ -156,25 +104,40 @@ const NormalMessage: React.FC<ChatMessageItemProps> = React.memo((props) => {
</div>
{/* 操作 */}
<Dropdown
overlay={actions}
placement="bottomLeft"
trigger={['click']}
onVisibleChange={setIsActionBtnActive}
<div
className={clsx(
'bg-white dark:bg-black rounded absolute right-2 cursor-pointer -top-3 shadow-sm flex',
{
'opacity-0 group-hover:opacity-100 bg-opacity-80 hover:bg-opacity-100':
!isActionBtnActive,
'opacity-100 bg-opacity-100': isActionBtnActive,
}
)}
>
<div
className={clsx(
'bg-white dark:bg-black rounded px-0.5 absolute right-2 cursor-pointer w-6 h-6 -top-3 flex justify-center items-center shadow-sm',
{
'opacity-0 group-hover:opacity-100 bg-opacity-80 hover:bg-opacity-100':
!isActionBtnActive,
'opacity-100 bg-opacity-100': isActionBtnActive,
}
)}
<DevContainer>
<Dropdown
overlay={emojiAction}
placement="bottomLeft"
trigger={['click']}
onVisibleChange={setIsActionBtnActive}
>
<div>
<MessageActionIcon icon="mdi:emoticon-happy-outline" />
</div>
</Dropdown>
</DevContainer>
<Dropdown
overlay={moreActions}
placement="bottomLeft"
trigger={['click']}
onVisibleChange={setIsActionBtnActive}
>
<Icon icon="mdi:dots-horizontal" />
</div>
</Dropdown>
<div>
<MessageActionIcon icon="mdi:dots-horizontal" />
</div>
</Dropdown>
</div>
</div>
);
});

@ -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 (
<Menu>
{context.hasContext && (
<Menu.Item
key="reply"
icon={<Icon icon="mdi:reply" />}
onClick={() => context.setReplyMsg(payload)}
>
{t('回复')}
</Menu.Item>
)}
{(isGroupOwner || isMessageAuthor) && (
<Menu.Item
key="recall"
icon={<Icon icon="mdi:restore" />}
onClick={handleRecallMessage}
>
{t('撤回')}
</Menu.Item>
)}
{/* 仅群组管理员可见 */}
{isGroupOwner && (
<Menu.Item
key="delete"
danger={true}
icon={<Icon icon="mdi:delete-outline" />}
onClick={handleDeleteMessage}
>
{t('删除')}
</Menu.Item>
)}
</Menu>
);
}

@ -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 <EmojiPanel onSelect={handleSelect} />;
}

@ -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<EmojiPickerProps> = React.memo((props) => {
const handleSelect = useCallback(
(emoji: EmojiData) => {
const code = emoji.colons;
if (isValidStr(code)) {
props.onSelect(code);
}
},
[props.onSelect]
);
return (
<NimblePicker set="twitter" data={emojiData} onSelect={handleSelect} />
);
});
EmojiPicker.displayName = 'EmojiPicker';
export default EmojiPicker;

@ -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 <EmojiPicker onSelect={props.onSelect} />;
});
EmojiPanel.displayName = 'EmojiPanel';

@ -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==

Loading…
Cancel
Save